Add Keybinds.

This commit is contained in:
Nicolas Stepien 2013-02-18 00:17:00 +01:00
parent d546654cf2
commit c77e012a52
5 changed files with 509 additions and 49 deletions

View File

@ -20,7 +20,7 @@
// @icon data:image/gif;base64,R0lGODlhEAAQAKECAAAAAGbMM////////yH5BAEKAAIALAAAAAAQABAAAAIxlI+pq+D9DAgUoFkPDlbs7lGiI2bSVnKglnJMOL6omczxVZK3dH/41AG6Lh7i6qUoAAA7
// ==/UserScript==
/* 4chan X Alpha - Version 3.0.0 - 2013-02-17
/* 4chan X Alpha - Version 3.0.0 - 2013-02-18
* http://mayhemydg.github.com/4chan-x/
*
* Copyright (c) 2009-2011 James Campos <james.r.campos@gmail.com>
@ -43,7 +43,7 @@
*/
(function() {
var $, $$, Anonymize, ArchiveLink, AutoGIF, Board, Build, Clone, Conf, Config, DeleteLink, DownloadLink, ExpandComment, ExpandThread, Favicon, FileInfo, Filter, Get, Header, ImageExpand, ImageHover, Main, Menu, Nav, Notification, Polyfill, Post, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, Quotify, Recursive, Redirect, RelativeDates, ReplyHiding, ReportLink, RevealSpoilers, Sauce, Settings, Thread, ThreadExcerpt, ThreadHiding, ThreadStats, ThreadUpdater, ThreadWatcher, Time, UI, Unread, d, doc, g,
var $, $$, Anonymize, ArchiveLink, AutoGIF, Board, Build, Clone, Conf, Config, DeleteLink, DownloadLink, ExpandComment, ExpandThread, Favicon, FileInfo, Filter, Get, Header, ImageExpand, ImageHover, Keybinds, Main, Menu, Nav, Notification, Polyfill, Post, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, Quotify, Recursive, Redirect, RelativeDates, ReplyHiding, ReportLink, RevealSpoilers, Sauce, Settings, Thread, ThreadExcerpt, ThreadHiding, ThreadStats, ThreadUpdater, ThreadWatcher, Time, UI, Unread, d, doc, g,
__indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; },
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
@ -141,28 +141,29 @@
fileInfo: '%l (%p%s, %r)',
favicon: 'ferongr',
hotkeys: {
'open QR': ['q', 'Open QR with post number inserted.'],
'open empty QR': ['Q', 'Open QR without post number inserted.'],
'open options': ['alt+o', 'Open Options.'],
'close': ['Esc', 'Close Options or QR.'],
'spoiler tags': ['ctrl+s', 'Insert spoiler tags.'],
'code tags': ['alt+c', 'Insert code tags.'],
'submit QR': ['alt+s', 'Submit post.'],
'watch': ['w', 'Watch thread.'],
'update': ['u', 'Update the thread now.'],
'expand image': ['E', 'Expand selected image.'],
'expand images': ['e', 'Expand all images.'],
'front page': ['0', 'Jump to page 0.'],
'next page': ['Right', 'Jump to the next page.'],
'previous page': ['Left', 'Jump to the previous page.'],
'next thread': ['Down', 'See next thread.'],
'previous thread': ['Up', 'See previous thread.'],
'expand thread': ['ctrl+e', 'Expand thread.'],
'open thread': ['o', 'Open thread in current tab.'],
'open thread tab': ['O', 'Open thread in new tab.'],
'next reply': ['j', 'Select next reply.'],
'previous reply': ['k', 'Select previous reply.'],
'hide': ['x', 'Hide thread.']
'Open empty QR': ['q', 'Open QR without post number inserted.'],
'Open QR': ['Shift+q', 'Open QR with post number inserted.'],
'Open options': ['Alt+o', 'Open Options.'],
'Close': ['Esc', 'Close Settings, Notifications or QR.'],
'Spoiler tags': ['Ctrl+s', 'Insert spoiler tags.'],
'Code tags': ['Alt+c', 'Insert code tags.'],
'Submit QR': ['Alt+s', 'Submit post.'],
'Watch': ['w', 'Watch thread.'],
'Update': ['u', 'Update the thread now.'],
'Expand image': ['Shift+e', 'Expand selected image.'],
'Expand images': ['e', 'Expand all images.'],
'Front page': ['0', 'Jump to page 0.'],
'Open front page': ['Shift+0', 'Open page 0 in a new tab.'],
'Next page': ['Right', 'Jump to the next page.'],
'Previous page': ['Left', 'Jump to the previous page.'],
'Next thread': ['Down', 'See next thread.'],
'Previous thread': ['Up', 'See previous thread.'],
'Expand thread': ['Ctrl+e', 'Expand thread.'],
'Open thread': ['o', 'Open thread in current tab.'],
'Open thread tab': ['Shift+o', 'Open thread in new tab.'],
'Next reply': ['j', 'Select next reply.'],
'Previous reply': ['k', 'Select previous reply.'],
'Hide': ['x', 'Hide thread.']
},
updater: {
checkbox: {
@ -2233,6 +2234,271 @@
}
};
Keybinds = {
init: function() {
if (g.VIEW === 'catalog' || !Conf['Keybinds']) {
return;
}
$.on(d, 'keydown', Keybinds.keydown);
return $.on(d, '4chanXInitFinished', function() {
var node, _i, _len, _ref, _results;
_ref = $$('[accesskey]');
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
node = _ref[_i];
_results.push(node.removeAttribute('accesskey'));
}
return _results;
});
},
keydown: function(e) {
var form, key, notification, notifications, target, thread, threadRoot, _i, _len, _ref;
if (!(key = Keybinds.keyCode(e))) {
return;
}
target = e.target;
if ((_ref = target.nodeName) === 'INPUT' || _ref === 'TEXTAREA') {
if (!((key === 'Esc') || (/(Alt|Ctrl|Meta)\+/.test(key)))) {
return;
}
}
threadRoot = Nav.getThread();
thread = Get.postFromNode($('.op', threadRoot)).thread;
switch (key) {
case Conf['Open empty QR']:
Keybinds.qr(threadRoot);
break;
case Conf['Open QR']:
Keybinds.qr(threadRoot, true);
break;
case Conf['Open options']:
Settings.open();
break;
case Conf['Close']:
if ($.id('settings')) {
Options.close();
} else if ((notifications = $$('.notification')).length) {
for (_i = 0, _len = notifications.length; _i < _len; _i++) {
notification = notifications[_i];
$('.close', notification).click();
}
} else if (QR.el) {
QR.close();
}
break;
case Conf['Spoiler tags']:
if (target.nodeName !== 'TEXTAREA') {
return;
}
Keybinds.tags('spoiler', target);
break;
case Conf['Code tags']:
if (target.nodeName !== 'TEXTAREA') {
return;
}
Keybinds.tags('code', target);
break;
case Conf['Submit QR']:
if (QR.el && !QR.status()) {
QR.submit();
}
break;
case Conf['Watch']:
ThreadWatcher.toggle(thread);
break;
case Conf['Update']:
ThreadUpdater.update();
break;
case Conf['Expand image']:
Keybinds.img(threadRoot);
break;
case Conf['Expand images']:
Keybinds.img(threadRoot, true);
break;
case Conf['Front page']:
window.location = "/" + g.BOARD + "/0#delform";
break;
case Conf['Open front page']:
$.open(url("/" + g.BOARD + "/#delform"));
break;
case Conf['Next page']:
if (form = $('.next form')) {
window.location = form.action;
}
break;
case Conf['Previous page']:
if (form = $('.prev form')) {
window.location = form.action;
}
break;
case Conf['Next thread']:
if (g.VIEW === 'thread') {
return;
}
Nav.scroll(+1);
break;
case Conf['Previous thread']:
if (g.VIEW === 'thread') {
return;
}
Nav.scroll(-1);
break;
case Conf['Expand thread']:
ExpandThread.toggle(thread);
break;
case Conf['Open thread']:
Keybinds.open(thread);
break;
case Conf['Open thread tab']:
Keybinds.open(thread, true);
break;
case Conf['Next reply']:
Keybinds.hl(+1, threadRoot);
break;
case Conf['Previous reply']:
Keybinds.hl(-1, threadRoot);
break;
case Conf['Hide']:
ThreadHiding.toggle(thread);
break;
default:
return;
}
return e.preventDefault();
},
keyCode: function(e) {
var kc, key;
key = (function() {
switch (kc = e.keyCode) {
case 8:
return '';
case 13:
return 'Enter';
case 27:
return 'Esc';
case 37:
return 'Left';
case 38:
return 'Up';
case 39:
return 'Right';
case 40:
return 'Down';
default:
if ((48 <= kc && kc <= 57) || (65 <= kc && kc <= 90)) {
return String.fromCharCode(kc).toLowerCase();
} else {
return null;
}
}
})();
if (key) {
if (e.altKey) {
key = 'Alt+' + key;
}
if (e.ctrlKey) {
key = 'Ctrl+' + key;
}
if (e.metaKey) {
key = 'Meta+' + key;
}
if (e.shiftKey) {
key = 'Shift+' + key;
}
}
return key;
},
qr: function(thread, quote) {
if (!Conf['Quick Reply']) {
return;
}
QR.open();
if (quote) {
QR.quote.call($('input', $('.post.highlight', thread) || thread));
}
return $('textarea', QR.el).focus();
},
tags: function(tag, ta) {
var range, selEnd, selStart, value;
value = ta.value;
selStart = ta.selectionStart;
selEnd = ta.selectionEnd;
ta.value = value.slice(0, selStart) + ("[" + tag + "]") + value.slice(selStart, selEnd) + ("[/" + tag + "]") + value.slice(selEnd);
range = ("[" + tag + "]").length + selEnd;
ta.setSelectionRange(range, range);
return $.event('input', null, ta);
},
img: function(thread, all) {
var input, post;
if (all) {
input = ImageExpand.expandAllInput;
input.checked = !input.checked;
return ImageExpand.cb.all.call(input);
} else {
post = Get.postFromNode($('.post.highlight', thread) || $('.op', thread));
return ImageExpand.toggle(post);
}
},
open: function(thread, tab) {
var url;
if (g.VIEW !== 'index') {
return;
}
url = "//boards.4chan.org/" + thread.board + "/res/" + thread;
if (tab) {
return $.open(url);
} else {
return location.href = url;
}
},
hl: function(delta, thread) {
var headRect, next, postEl, rect, replies, reply, root, topMargin, _i, _len;
headRect = $.id('header-bar').getBoundingClientRect();
topMargin = headRect.top + headRect.height;
if (postEl = $('.reply.highlight', thread)) {
$.rmClass(postEl, 'highlight');
rect = postEl.getBoundingClientRect();
if (rect.bottom >= topMargin && rect.top <= d.documentElement.clientHeight) {
root = postEl.parentNode;
next = $.x('child::div[contains(@class,"post reply")]', delta === +1 ? root.nextElementSibling : root.previousElementSibling);
if (!next) {
this.focus(postEl);
return;
}
if (!(g.VIEW === 'thread' || $.x('ancestor::div[parent::div[@class="board"]]', next) === thread)) {
return;
}
rect = next.getBoundingClientRect();
if (rect.top < 0 || rect.bottom > d.documentElement.clientHeight) {
if (delta === -1) {
window.scrollBy(0, rect.top - topMargin);
} else {
next.scrollIntoView(false);
}
}
this.focus(next);
return;
}
}
replies = $$('.reply', thread);
if (delta === -1) {
replies.reverse();
}
for (_i = 0, _len = replies.length; _i < _len; _i++) {
reply = replies[_i];
rect = reply.getBoundingClientRect();
if (delta === +1 && rect.top >= topMargin || delta === -1 && rect.bottom <= d.documentElement.clientHeight) {
this.focus(reply);
return;
}
}
},
focus: function(post) {
$.addClass(post, 'highlight');
return $('a[title="Highlight this post"]', post).focus();
}
};
Nav = {
init: function() {
var next, prev, span;
@ -3818,6 +4084,7 @@
switch (type) {
case 'Expand all':
$.on(input, 'change', ImageExpand.cb.all);
ImageExpand.expandAllInput = input;
break;
case 'Fit width':
case 'Fit height':
@ -5044,7 +5311,7 @@
text = "";
sel = d.getSelection();
selectionRoot = $.x('ancestor::div[contains(@class,"postContainer")][1]', sel.anchorNode);
post = Get.postFromRoot($.x('ancestor::div[contains(@class,"postContainer")][1]', this));
post = Get.postFromNode(this);
thread = g.BOARD.posts[Get.contextFromLink(this).thread];
if ((s = sel.toString().trim()) && post.nodes.root === selectionRoot) {
s = s.replace(/\n/g, '\n>');
@ -6148,6 +6415,7 @@
initFeature('Thread Updater', ThreadUpdater);
initFeature('Thread Watcher', ThreadWatcher);
initFeature('Index Navigation', Nav);
initFeature('Keybinds', Keybinds);
console.timeEnd('All initializations');
$.on(d, '4chanMainInit', Main.initStyle);
return $.ready(Main.initReady);

View File

@ -130,33 +130,34 @@ Config =
favicon: 'ferongr'
hotkeys:
# QR & Options
'open QR': ['q', 'Open QR with post number inserted.']
'open empty QR': ['Q', 'Open QR without post number inserted.']
'open options': ['alt+o', 'Open Options.']
'close': ['Esc', 'Close Options or QR.']
'spoiler tags': ['ctrl+s', 'Insert spoiler tags.']
'code tags': ['alt+c', 'Insert code tags.']
'submit QR': ['alt+s', 'Submit post.']
'Open empty QR': ['q', 'Open QR without post number inserted.']
'Open QR': ['Shift+q', 'Open QR with post number inserted.']
'Open options': ['Alt+o', 'Open Options.']
'Close': ['Esc', 'Close Settings, Notifications or QR.']
'Spoiler tags': ['Ctrl+s', 'Insert spoiler tags.']
'Code tags': ['Alt+c', 'Insert code tags.']
'Submit QR': ['Alt+s', 'Submit post.']
# Thread related
'watch': ['w', 'Watch thread.']
'update': ['u', 'Update the thread now.']
'Watch': ['w', 'Watch thread.']
'Update': ['u', 'Update the thread now.']
# Images
'expand image': ['E', 'Expand selected image.']
'expand images': ['e', 'Expand all images.']
'Expand image': ['Shift+e', 'Expand selected image.']
'Expand images': ['e', 'Expand all images.']
# Board Navigation
'front page': ['0', 'Jump to page 0.']
'next page': ['Right', 'Jump to the next page.']
'previous page': ['Left', 'Jump to the previous page.']
'Front page': ['0', 'Jump to page 0.']
'Open front page': ['Shift+0', 'Open page 0 in a new tab.']
'Next page': ['Right', 'Jump to the next page.']
'Previous page': ['Left', 'Jump to the previous page.']
# Thread Navigation
'next thread': ['Down', 'See next thread.']
'previous thread': ['Up', 'See previous thread.']
'expand thread': ['ctrl+e', 'Expand thread.']
'open thread': ['o', 'Open thread in current tab.']
'open thread tab': ['O', 'Open thread in new tab.']
'Next thread': ['Down', 'See next thread.']
'Previous thread': ['Up', 'See previous thread.']
'Expand thread': ['Ctrl+e', 'Expand thread.']
'Open thread': ['o', 'Open thread in current tab.']
'Open thread tab': ['Shift+o', 'Open thread in new tab.']
# Reply Navigation
'next reply': ['j', 'Select next reply.']
'previous reply': ['k', 'Select previous reply.']
'hide': ['x', 'Hide thread.']
'Next reply': ['j', 'Select next reply.']
'Previous reply': ['k', 'Select previous reply.']
'Hide': ['x', 'Hide thread.']
updater:
checkbox:
'Beep': [false, 'Beep on new post to completely read thread.']

View File

@ -1025,6 +1025,196 @@ ArchiveLink =
open: open
}
Keybinds =
init: ->
return if g.VIEW is 'catalog' or !Conf['Keybinds']
$.on d, 'keydown', Keybinds.keydown
$.on d, '4chanXInitFinished', ->
for node in $$ '[accesskey]'
node.removeAttribute 'accesskey'
keydown: (e) ->
return unless key = Keybinds.keyCode e
{target} = e
if target.nodeName in ['INPUT', 'TEXTAREA']
return unless (key is 'Esc') or (/(Alt|Ctrl|Meta)\+/.test key)
threadRoot = Nav.getThread()
thread = Get.postFromNode($('.op', threadRoot)).thread
switch key
# QR & Options
when Conf['Open empty QR']
Keybinds.qr threadRoot
when Conf['Open QR']
Keybinds.qr threadRoot, true
when Conf['Open options']
Settings.open()
when Conf['Close']
if $.id 'settings'
Options.close()
else if (notifications = $$ '.notification').length
for notification in notifications
$('.close', notification).click()
else if QR.el
QR.close()
when Conf['Spoiler tags']
return if target.nodeName isnt 'TEXTAREA'
Keybinds.tags 'spoiler', target
when Conf['Code tags']
return if target.nodeName isnt 'TEXTAREA'
Keybinds.tags 'code', target
when Conf['Submit QR']
QR.submit() if QR.el and !QR.status()
# Thread related
when Conf['Watch']
ThreadWatcher.toggle thread
when Conf['Update']
ThreadUpdater.update()
# Images
when Conf['Expand image']
Keybinds.img threadRoot
when Conf['Expand images']
Keybinds.img threadRoot, true
# Board Navigation
when Conf['Front page']
window.location = "/#{g.BOARD}/0#delform"
when Conf['Open front page']
$.open url "/#{g.BOARD}/#delform"
when Conf['Next page']
if form = $ '.next form'
window.location = form.action
when Conf['Previous page']
if form = $ '.prev form'
window.location = form.action
# Thread Navigation
when Conf['Next thread']
return if g.VIEW is 'thread'
Nav.scroll +1
when Conf['Previous thread']
return if g.VIEW is 'thread'
Nav.scroll -1
when Conf['Expand thread']
ExpandThread.toggle thread
when Conf['Open thread']
Keybinds.open thread
when Conf['Open thread tab']
Keybinds.open thread, true
# Reply Navigation
when Conf['Next reply']
Keybinds.hl +1, threadRoot
when Conf['Previous reply']
Keybinds.hl -1, threadRoot
when Conf['Hide']
ThreadHiding.toggle thread
else
return
e.preventDefault()
keyCode: (e) ->
key = switch kc = e.keyCode
when 8 # return
''
when 13
'Enter'
when 27
'Esc'
when 37
'Left'
when 38
'Up'
when 39
'Right'
when 40
'Down'
else
if 48 <= kc <= 57 or 65 <= kc <= 90 # 0-9, A-Z
String.fromCharCode(kc).toLowerCase()
else
null
if key
if e.altKey then key = 'Alt+' + key
if e.ctrlKey then key = 'Ctrl+' + key
if e.metaKey then key = 'Meta+' + key
if e.shiftKey then key = 'Shift+' + key
key
qr: (thread, quote) ->
return unless Conf['Quick Reply']
QR.open()
if quote
QR.quote.call $ 'input', $('.post.highlight', thread) or thread
$('textarea', QR.el).focus()
tags: (tag, ta) ->
value = ta.value
selStart = ta.selectionStart
selEnd = ta.selectionEnd
ta.value =
value[...selStart] +
"[#{tag}]" + value[selStart...selEnd] + "[/#{tag}]" +
value[selEnd..]
# Move the caret to the end of the selection.
range = "[#{tag}]".length + selEnd
ta.setSelectionRange range, range
# Fire the 'input' event
$.event 'input', null, ta
img: (thread, all) ->
if all
input = ImageExpand.expandAllInput
input.checked = !input.checked
ImageExpand.cb.all.call input
else
post = Get.postFromNode $('.post.highlight', thread) or $ '.op', thread
ImageExpand.toggle post
open: (thread, tab) ->
return if g.VIEW isnt 'index'
url = "//boards.4chan.org/#{thread.board}/res/#{thread}"
if tab
$.open url
else
location.href = url
hl: (delta, thread) ->
headRect = $.id('header-bar').getBoundingClientRect()
topMargin = headRect.top + headRect.height
if postEl = $ '.reply.highlight', thread
$.rmClass postEl, 'highlight'
rect = postEl.getBoundingClientRect()
if rect.bottom >= topMargin and rect.top <= d.documentElement.clientHeight # We're at least partially visible
root = postEl.parentNode
next = $.x 'child::div[contains(@class,"post reply")]',
if delta is +1 then root.nextElementSibling else root.previousElementSibling
unless next
@focus postEl
return
return unless g.VIEW is 'thread' or $.x('ancestor::div[parent::div[@class="board"]]', next) is thread
rect = next.getBoundingClientRect()
if rect.top < 0 or rect.bottom > d.documentElement.clientHeight
if delta is -1
window.scrollBy 0, rect.top - topMargin
else
next.scrollIntoView false
@focus next
return
replies = $$ '.reply', thread
replies.reverse() if delta is -1
for reply in replies
rect = reply.getBoundingClientRect()
if delta is +1 and rect.top >= topMargin or delta is -1 and rect.bottom <= d.documentElement.clientHeight
@focus reply
return
focus: (post) ->
$.addClass post, 'highlight'
$('a[title="Highlight this post"]', post).focus()
Nav =
init: ->
return if g.VIEW isnt 'index' or !Conf['Index Navigation']
@ -2388,6 +2578,7 @@ ImageExpand =
switch type
when 'Expand all'
$.on input, 'change', ImageExpand.cb.all
ImageExpand.expandAllInput = input
when 'Fit width', 'Fit height'
$.on input, 'change', ImageExpand.cb.setFitness
if config

View File

@ -345,6 +345,7 @@ Main =
initFeature 'Thread Updater', ThreadUpdater
initFeature 'Thread Watcher', ThreadWatcher
initFeature 'Index Navigation', Nav
initFeature 'Keybinds', Keybinds
console.timeEnd 'All initializations'
$.on d, '4chanMainInit', Main.initStyle

View File

@ -231,7 +231,7 @@ QR =
sel = d.getSelection()
selectionRoot = $.x 'ancestor::div[contains(@class,"postContainer")][1]', sel.anchorNode
post = Get.postFromRoot $.x 'ancestor::div[contains(@class,"postContainer")][1]', @
post = Get.postFromNode @
thread = g.BOARD.posts[Get.contextFromLink(@).thread]
if (s = sel.toString().trim()) and post.nodes.root is selectionRoot
@ -249,7 +249,6 @@ QR =
ta = $ 'textarea', QR.el
if QR.threadSelector and !ta.value and g.BOARD.ID isnt 'f'
QR.threadSelector.value = thread.ID
# Make sure we get the correct number, even with XXX censors
caretPos = ta.selectionStart
# Replace selection for text.