diff --git a/4chan_x.user.js b/4chan_x.user.js index d9b4c4953..3e470fdf9 100644 --- a/4chan_x.user.js +++ b/4chan_x.user.js @@ -2,7 +2,7 @@ // @name 4chan x // @namespace aeosynth // @description Adds various features. -// @version 11.8.4.0 +// @version 11.8.6.0 // @copyright 2009-2011 James Campos // @license MIT; http://en.wikipedia.org/wiki/Mit_license // @include http://boards.4chan.org/* @@ -100,7 +100,8 @@ 'Auto Noko': [true, 'Always redirect to your post'], 'Cooldown': [true, 'Prevent \'flood detected\' errors'], 'Quick Reply': [true, 'Reply without leaving the page'], - 'Persistent QR': [false, 'Quick reply won\'t disappear after posting. Only in replies.'] + 'Persistent QR': [false, 'Quick reply won\'t disappear after posting. Only in replies.'], + 'Auto Hide QR': [true, 'Automatically auto-hide the quick reply when posting'] }, Quoting: { 'Quote Backlinks': [true, 'Add quote backlinks'], @@ -111,7 +112,7 @@ 'Indicate OP quote': [true, 'Add \'(OP)\' to OP quotes'] } }, - flavors: ['http://regex.info/exif.cgi?url=', 'http://iqdb.org/?url=', 'http://google.com/searchbyimage?image_url=', '#http://tineye.com/search?url=', '#http://saucenao.com/search.php?db=999&url=', '#http://imgur.com/upload?url='].join('\n'), + flavors: ['http://regex.info/exif.cgi?url=', 'http://iqdb.org/?url=', 'http://google.com/searchbyimage?image_url=', '#http://tineye.com/search?url=', '#http://saucenao.com/search.php?db=999&url=', '#http://imgur.com/upload?url=', '#http://anonym.to/?'].join('\n'), time: '%m/%d/%y(%a)%H:%M', hotkeys: { close: 'Esc', @@ -292,6 +293,9 @@ return object; }; $.extend($, { + id: function(id) { + return d.getElementById(id); + }, globalEval: function(code) { var script; script = $.el('script', { @@ -614,13 +618,13 @@ _results = []; for (_i = 0, _len = _ref2.length; _i < _len; _i++) { backlink = _ref2[_i]; - _results.push(!d.getElementById(backlink.hash.slice(1)) ? $.rm(backlink) : void 0); + _results.push(!$.id(backlink.hash.slice(1)) ? $.rm(backlink) : void 0); } return _results; } }, parse: function(req, pathname, thread, a) { - var body, br, next, quote, table, tables, _i, _j, _len, _len2, _ref, _results; + var body, br, link, next, quote, reply, table, tables, _i, _j, _k, _len, _len2, _len3, _ref, _ref2, _results; if (req.status !== 200) { a.textContent = "" + req.status + " " + req.statusText; $.unbind(a, 'click', expandThread.cb.toggle); @@ -634,18 +638,25 @@ body = $.el('body', { innerHTML: req.responseText }); - _ref = $$('a.quotelink', body); + _ref = $$('td[id]', body); for (_i = 0, _len = _ref.length; _i < _len; _i++) { - quote = _ref[_i]; - if (quote.getAttribute('href') === quote.hash) { - quote.pathname = pathname; + reply = _ref[_i]; + _ref2 = $$('a.quotelink', reply); + for (_j = 0, _len2 = _ref2.length; _j < _len2; _j++) { + quote = _ref2[_j]; + if (quote.getAttribute('href') === quote.hash) { + quote.pathname = pathname; + } } + link = $('a.quotejs', reply); + link.href = "res/" + thread.firstChild.id + "#" + reply.id; + link.nextSibling.href = "res/" + thread.firstChild.id + "#q" + reply.id; } tables = $$('form[name=delform] table', body); tables.pop(); _results = []; - for (_j = 0, _len2 = tables.length; _j < _len2; _j++) { - table = tables[_j]; + for (_k = 0, _len3 = tables.length; _k < _len3; _k++) { + table = tables[_k]; _results.push($.before(br, table)); } return _results; @@ -722,140 +733,139 @@ node = _ref[_i]; node.removeAttribute('accesskey'); } - return $.bind(d, 'keydown', keybinds.cb.keydown); + return $.bind(d, 'keydown', keybinds.keydown); }, - cb: { - keydown: function(e) { - var o, range, selEnd, selStart, ta, thread, valEnd, valMid, valStart, value, _ref, _ref2, _ref3; - if (((_ref = e.target.nodeName) === 'TEXTAREA' || _ref === 'INPUT') && !e.altKey && !e.ctrlKey && !(e.keyCode === 27)) { - return; - } - if (!(key = keybinds.cb.keyCode(e))) { - return; - } - thread = nav.getThread(); - switch (key) { - case conf.close: - if (o = $('#overlay')) { - $.rm(o); - } else if (qr.el) { - qr.close(); - } - break; - case conf.spoiler: - ta = e.target; - if (ta.nodeName !== 'TEXTAREA') { - return; - } - value = ta.value; - selStart = ta.selectionStart; - selEnd = ta.selectionEnd; - valStart = value.slice(0, selStart) + '[spoiler]'; - valMid = value.slice(selStart, selEnd); - valEnd = '[/spoiler]' + value.slice(selEnd); - ta.value = valStart + valMid + valEnd; - range = valStart.length + valMid.length; - ta.setSelectionRange(range, range); - break; - case conf.zero: - window.location = "/" + g.BOARD + "/0#0"; - break; - case conf.openEmptyQR: - keybinds.qr(thread); - break; - case conf.nextReply: - keybinds.hl.next(thread); - break; - case conf.previousReply: - keybinds.hl.prev(thread); - break; - case conf.expandAllImages: - keybinds.img(thread, true); - break; - case conf.openThread: - keybinds.open(thread); - break; - case conf.expandThread: - expandThread.toggle(thread); - break; - case conf.openQR: - keybinds.qr(thread, true); - break; - case conf.expandImages: - keybinds.img(thread); - break; - case conf.nextThread: - nav.next(); - break; - case conf.openThreadTab: - keybinds.open(thread, true); - break; - case conf.previousThread: - nav.prev(); - break; - case conf.update: - updater.update(); - break; - case conf.watch: - watcher.toggle(thread); - break; - case conf.hide: - threadHiding.toggle(thread); - break; - case conf.nextPage: - if ((_ref2 = $('input[value=Next]')) != null) { - _ref2.click(); - } - break; - case conf.previousPage: - if ((_ref3 = $('input[value=Previous]')) != null) { - _ref3.click(); - } - break; - case conf.submit: - if (qr.el) { - qr.submit.call($('form', qr.el)); - } else { - $('.postarea form').submit(); - } - break; - case conf.unreadCountTo0: - unread.replies.length = 0; - unread.updateTitle(); - Favicon.update(); - break; - default: - return; - } - return e.preventDefault(); - }, - keyCode: function(e) { - var kc; - kc = e.keyCode; - if ((65 <= kc && kc <= 90)) { - key = String.fromCharCode(kc); - if (!e.shiftKey) { - key = key.toLowerCase(); - } - } else if ((48 <= kc && kc <= 57)) { - key = String.fromCharCode(kc); - } else if (kc === 27) { - key = 'Esc'; - } else if (kc === 8) { - key = ''; - } else { - key = null; - } - if (key) { - if (e.altKey) { - key = 'alt+' + key; - } - if (e.ctrlKey) { - key = 'ctrl+' + key; - } - } - return key; + keydown: function(e) { + var o, range, selEnd, selStart, ta, thread, valEnd, valMid, valStart, value, _ref, _ref2, _ref3; + updater.focus = true; + if (((_ref = e.target.nodeName) === 'TEXTAREA' || _ref === 'INPUT') && !e.altKey && !e.ctrlKey && !(e.keyCode === 27)) { + return; } + if (!(key = keybinds.keyCode(e))) { + return; + } + thread = nav.getThread(); + switch (key) { + case conf.close: + if (o = $('#overlay')) { + $.rm(o); + } else if (qr.el) { + qr.close(); + } + break; + case conf.spoiler: + ta = e.target; + if (ta.nodeName !== 'TEXTAREA') { + return; + } + value = ta.value; + selStart = ta.selectionStart; + selEnd = ta.selectionEnd; + valStart = value.slice(0, selStart) + '[spoiler]'; + valMid = value.slice(selStart, selEnd); + valEnd = '[/spoiler]' + value.slice(selEnd); + ta.value = valStart + valMid + valEnd; + range = valStart.length + valMid.length; + ta.setSelectionRange(range, range); + break; + case conf.zero: + window.location = "/" + g.BOARD + "/0#0"; + break; + case conf.openEmptyQR: + keybinds.qr(thread); + break; + case conf.nextReply: + keybinds.hl.next(thread); + break; + case conf.previousReply: + keybinds.hl.prev(thread); + break; + case conf.expandAllImages: + keybinds.img(thread, true); + break; + case conf.openThread: + keybinds.open(thread); + break; + case conf.expandThread: + expandThread.toggle(thread); + break; + case conf.openQR: + keybinds.qr(thread, true); + break; + case conf.expandImages: + keybinds.img(thread); + break; + case conf.nextThread: + nav.next(); + break; + case conf.openThreadTab: + keybinds.open(thread, true); + break; + case conf.previousThread: + nav.prev(); + break; + case conf.update: + updater.update(); + break; + case conf.watch: + watcher.toggle(thread); + break; + case conf.hide: + threadHiding.toggle(thread); + break; + case conf.nextPage: + if ((_ref2 = $('input[value=Next]')) != null) { + _ref2.click(); + } + break; + case conf.previousPage: + if ((_ref3 = $('input[value=Previous]')) != null) { + _ref3.click(); + } + break; + case conf.submit: + if (qr.el) { + qr.submit.call($('form', qr.el)); + } else { + $('.postarea form').submit(); + } + break; + case conf.unreadCountTo0: + unread.replies.length = 0; + unread.updateTitle(); + Favicon.update(); + break; + default: + return; + } + return e.preventDefault(); + }, + keyCode: function(e) { + var kc; + kc = e.keyCode; + if ((65 <= kc && kc <= 90)) { + key = String.fromCharCode(kc); + if (!e.shiftKey) { + key = key.toLowerCase(); + } + } else if ((48 <= kc && kc <= 57)) { + key = String.fromCharCode(kc); + } else if (kc === 27) { + key = 'Esc'; + } else if (kc === 8) { + key = ''; + } else { + key = null; + } + if (key) { + if (e.altKey) { + key = 'alt+' + key; + } + if (e.ctrlKey) { + key = 'ctrl+' + key; + } + } + return key; }, img: function(thread, all) { var root, thumb; @@ -873,7 +883,7 @@ qrLink = $("span[id^=nothread] a:not(:first-child)", thread); } if (quote) { - return qr.quote(qrLink); + return qr.quote.call(qrLink); } else { if (!qr.el) { qr.dialog(qrLink); @@ -1043,7 +1053,7 @@ var arr, checked, description, dialog, hiddenNum, hiddenThreads, hidingul, html, input, key, li, link, main, obj, overlay, ul, _i, _j, _k, _len, _len2, _len3, _ref, _ref2, _ref3, _ref4; hiddenThreads = $.getValue("hiddenThreads/" + g.BOARD + "/", {}); hiddenNum = Object.keys(g.hiddenReplies).length + Object.keys(hiddenThreads).length; - html = "

"; + html = "

"; dialog = $.el('div', { id: 'options', innerHTML: html @@ -1130,7 +1140,7 @@ keybind: function(e) { e.preventDefault(); e.stopPropagation(); - if ((key = keybinds.cb.keyCode(e)) == null) { + if ((key = keybinds.keyCode(e)) == null) { return; } this.value = key; @@ -1197,7 +1207,7 @@ cb: function() { var submit, submits, _i, _j, _len, _len2, _results; submits = $$('#com_submit'); - if (--cooldown.duration) { + if (--cooldown.duration > 0) { _results = []; for (_i = 0, _len = submits.length; _i < _len; _i++) { submit = submits[_i]; @@ -1212,7 +1222,7 @@ submit.value = 'Submit'; } if (qr.el && $('#auto', qr.el).checked) { - return qr.submit.call($('form', qr.el)); + return qr.autoPost(); } } } @@ -1220,66 +1230,180 @@ qr = { init: function() { var iframe; - g.callbacks.push(qr.cb.node); + g.callbacks.push(qr.node); + $.bind(window, 'message', qr.message); + $.bind($('#recaptcha_challenge_field_holder'), 'DOMNodeInserted', qr.captchaNode); + qr.captcha = []; iframe = $.el('iframe', { name: 'iframe', hidden: true }); $.append(d.body, iframe); - $.bind(window, 'message', qr.cb.message); return $('#recaptcha_response_field').id = ''; }, - autohide: { - set: function() { - var _ref; - return (_ref = $('#autohide:not(:checked)', qr.el)) != null ? _ref.click() : void 0; - }, - unset: function() { - var _ref; - return (_ref = $('#autohide:checked', qr.el)) != null ? _ref.click() : void 0; + attach: function() { + var fileDiv; + $('#auto', qr.el).checked = true; + fileDiv = $.el('div', { + innerHTML: 'X' + }); + $.bind(fileDiv.lastChild, 'click', (function() { + return $.rm(this.parentNode); + })); + return $.prepend(qr.files, fileDiv); + }, + attachNext: function() { + var file, fileDiv, oldFile; + fileDiv = $.rm(qr.files.lastChild); + file = fileDiv.firstChild; + oldFile = $('#qr_form input[type=file]', qr.el); + return $.replace(oldFile, file); + }, + autoPost: function() { + var captcha, responseField; + responseField = $('#recaptcha_response_field', qr.el); + if (!responseField.value && (captcha = qr.captcha.shift())) { + $('#recaptcha_challenge_field', qr.el).value = captcha.challenge; + responseField.value = captcha.response; + responseField.nextSibling.textContent = qr.captcha.length + ' captcha cached'; + } + return qr.submit.call($('form', qr.el)); + }, + captchaNode: function(e) { + var target; + if (!qr.el) { + return; + } + target = e.target; + $('img', qr.el).src = "http://www.google.com/recaptcha/api/image?c=" + target.value; + return $('#recaptcha_challenge_field', qr.el).value = target.value; + }, + captchaKeydown: function(e) { + if (e.keyCode === 13 && cooldown.duration) { + $('#auto', qr.el).checked = true; + if (conf['Auto Hide QR']) { + $('#autohide', qr.el).checked = true; + } + return qr.captchaPush.call(this); } }, - cb: { - autohide: function(e) { - if (this.checked) { - return $.addClass(qr.el, 'auto'); - } else { - return $.removeClass(qr.el, 'auto'); - } - }, - message: function(e) { - var data, duration; - Recaptcha.reload(); - $('iframe[name=iframe]').src = 'about:blank'; - data = e.data; - if (data) { - $('input[name=recaptcha_response_field]', qr.el).value = ''; - $.extend($('#error', qr.el), JSON.parse(data)); - qr.autohide.unset(); - return; - } - if (qr.el) { - if (g.REPLY && conf['Persistent QR']) { - qr.refresh(); - } else { - qr.close(); + captchaPush: function() { + var l; + l = qr.captcha.push({ + challenge: $('#recaptcha_challenge_field', qr.el).value, + response: this.value + }); + this.nextSibling.textContent = l + ' captcha cached'; + Recaptcha.reload(); + return this.value = ''; + }, + close: function() { + $.rm(qr.el); + return qr.el = null; + }, + dialog: function(link) { + var THREAD_ID, challenge, html, spoiler, submitDisabled, submitValue; + submitValue = $('#com_submit').value; + submitDisabled = $('#com_submit').disabled ? 'disabled' : ''; + THREAD_ID = g.THREAD_ID || $.x('ancestor::div[@class="thread"]/div', link).id; + spoiler = $('.postarea label') ? '' : ''; + challenge = $('#recaptcha_challenge_field').value; + html = " X
Quick Reply
" + spoiler + "
0 captcha cached
attach another file
"; + qr.el = ui.dialog('qr', { + top: '0px', + left: '0px' + }, html); + qr.files = $('#files', qr.el); + qr.refresh(); + $('textarea', qr.el).value = $('textarea').value; + $.bind($('input[name=name]', qr.el), 'mousedown', function(e) { + return e.stopPropagation(); + }); + $.bind($('#close', qr.el), 'click', qr.close); + $.bind($('form', qr.el), 'submit', qr.submit); + $.bind($('a[name=attach]', qr.el), 'click', qr.attach); + $.bind($('img', qr.el), 'click', Recaptcha.reload); + $.bind($('#recaptcha_response_field', qr.el), 'keydown', Recaptcha.listener); + $.bind($('#recaptcha_response_field', qr.el), 'keydown', qr.captchaKeydown); + return $.append(d.body, qr.el); + }, + message: function(e) { + var data, duration; + Recaptcha.reload(); + $('iframe[name=iframe]').src = 'about:blank'; + data = e.data; + if (data) { + data = JSON.parse(data); + $.extend($('#error', qr.el), data); + $('#recaptcha_response_field', qr.el).value = ''; + $('#autohide', qr.el).checked = false; + if (data.textContent === 'You seem to have mistyped the verification.') { + if (qr.captcha.length) { + qr.autoPost(); + } + } else if (data.textContent === 'Error: Duplicate file entry detected.' && qr.files.childElementCount) { + $('textarea', qr.el).value += '\n' + data.textContent + ' ' + data.href; + qr.attachNext(); + if (qr.captcha.length) { + qr.autoPost(); } } - if (conf['Cooldown']) { - duration = qr.sage ? 60 : 30; - $.setValue(g.BOARD + '/cooldown', Date.now() + duration * 1000); - return cooldown.start(); - } - }, - node: function(root) { - var quote; - quote = $('a.quotejs:not(:first-child)', root); - return $.bind(quote, 'click', qr.cb.quote); - }, - quote: function(e) { - e.preventDefault(); - return qr.quote(this); + return; } + if (qr.el) { + if (g.REPLY && (conf['Persistent QR'] || qr.files.childElementCount)) { + qr.refresh(); + if (qr.files.childElementCount) { + qr.attachNext(); + } + } else { + qr.close(); + } + } + if (conf['Cooldown']) { + duration = qr.sage ? 60 : 30; + $.setValue(g.BOARD + '/cooldown', Date.now() + duration * 1000); + return cooldown.start(); + } + }, + node: function(root) { + var quote; + quote = $('a.quotejs:not(:first-child)', root); + return $.bind(quote, 'click', qr.quote); + }, + quote: function(e) { + var id, s, selection, selectionID, ta, text, _ref; + if (e) { + e.preventDefault(); + } + if (qr.el) { + $('#autohide', qr.el).checked = false; + } else { + qr.dialog(this); + } + id = this.textContent; + text = ">>" + id + "\n"; + selection = window.getSelection(); + if (s = selection.toString()) { + selectionID = (_ref = $.x('preceding::input[@type="checkbox"][1]', selection.anchorNode)) != null ? _ref.name : void 0; + if (selectionID === id) { + s = s.replace(/\n/g, '\n>'); + text += ">" + s + "\n"; + } + } + ta = $('textarea', qr.el); + ta.focus(); + return ta.value += text; + }, + refresh: function() { + var auto, c, m; + auto = $('#auto', qr.el).checked; + $('form', qr.el).reset(); + $('#auto', qr.el).checked = auto; + c = d.cookie; + $('input[name=name]', qr.el).value = (m = c.match(/4chan_name=([^;]+)/)) ? decodeURIComponent(m[1]) : ''; + $('input[name=email]', qr.el).value = (m = c.match(/4chan_email=([^;]+)/)) ? decodeURIComponent(m[1]) : ''; + return $('input[name=pwd]', qr.el).value = (m = c.match(/4chan_pass=([^;]+)/)) ? decodeURIComponent(m[1]) : $('input[name=pwd]').value; }, submit: function(e) { var id, inputfile, isQR, op; @@ -1288,7 +1412,7 @@ watcher.watch(null, g.THREAD_ID); } else { id = $('input[name=resto]', qr.el).value; - op = d.getElementById(id); + op = $.id(id); if ($('img.favicon', op).src === Favicon.empty) { watcher.watch(op, id); } @@ -1310,72 +1434,14 @@ this.submit(); } $('#error', qr.el).textContent = ''; - qr.autohide.set(); + if (conf['Auto Hide QR']) { + $('#autohide', qr.el).checked = true; + } return qr.sage = /sage/i.test($('input[name=email]', this).value); } }, - quote: function(link) { - var id, s, selection, selectionID, ta, text, _ref; - if (qr.el) { - qr.autohide.unset(); - } else { - qr.dialog(link); - } - id = link.textContent; - text = ">>" + id + "\n"; - selection = window.getSelection(); - if (s = selection.toString()) { - selectionID = (_ref = $.x('preceding::input[@type="checkbox"][1]', selection.anchorNode)) != null ? _ref.name : void 0; - if (selectionID === id) { - s = s.replace(/\n/g, '\n>'); - text += ">" + s + "\n"; - } - } - ta = $('textarea', qr.el); - ta.focus(); - return ta.value += text; - }, - refresh: function() { - var c, m; - $('form', qr.el).reset(); - c = d.cookie; - $('input[name=name]', qr.el).value = (m = c.match(/4chan_name=([^;]+)/)) ? decodeURIComponent(m[1]) : ''; - $('input[name=email]', qr.el).value = (m = c.match(/4chan_email=([^;]+)/)) ? decodeURIComponent(m[1]) : ''; - return $('input[name=pwd]', qr.el).value = (m = c.match(/4chan_pass=([^;]+)/)) ? decodeURIComponent(m[1]) : $('input[name=pwd]').value; - }, - dialog: function(link) { - var THREAD_ID, challenge, html, spoiler, submitDisabled, submitValue; - submitValue = $('#com_submit').value; - submitDisabled = $('#com_submit').disabled ? 'disabled' : ''; - THREAD_ID = g.THREAD_ID || $.x('ancestor::div[@class="thread"]/div', link).id; - spoiler = $('.postarea label') ? '' : ''; - challenge = $('input[name=recaptcha_challenge_field]').value; - html = "
Quick Reply X
" + spoiler + "
"; - qr.el = ui.dialog('qr', { - top: '0px', - left: '0px' - }, html); - qr.refresh(); - $.bind($('input[name=name]', qr.el), 'mousedown', function(e) { - return e.stopPropagation(); - }); - $.bind($('#autohide', qr.el), 'click', qr.cb.autohide); - $.bind($('a[name=close]', qr.el), 'click', qr.close); - $.bind($('form', qr.el), 'submit', qr.submit); - $.bind($('img', qr.el), 'click', Recaptcha.reload); - $.bind($('input[name=recaptcha_response_field]', qr.el), 'keydown', Recaptcha.listener); - return $.append(d.body, qr.el); - }, - persist: function() { - qr.dialog(); - return qr.autohide.set(); - }, - close: function() { - $.rm(qr.el); - return qr.el = null; - }, sys: function() { - var c, duration, id, noko, recaptcha, thread, _, _ref; + var c, duration, id, noko, recaptcha, sage, search, thread, url, watch, _, _ref; if (recaptcha = $('#recaptcha_response_field')) { $.bind(recaptcha, 'keydown', Recaptcha.listener); return; @@ -1403,28 +1469,26 @@ c = $('b').lastChild; if (c.nodeType === 8) { _ref = c.textContent.match(/thread:(\d+),no:(\d+)/), _ = _ref[0], thread = _ref[1], id = _ref[2]; - noko = /auto_noko/.test(location.search); - if (thread === '0') { - if (/auto_watch/.test(location.search)) { - return window.location = "http://boards.4chan.org/" + g.BOARD + "/res/" + id + "#watch"; - } else if (noko) { - return window.location = "http://boards.4chan.org/" + g.BOARD + "/res/" + id; - } - } else if (/cooldown/.test(location.search)) { - duration = Date.now() + 30000; - if (/sage/.test(location.search)) { - duration += 30000; - } - if (noko) { - return window.location = "http://boards.4chan.org/" + g.BOARD + "/res/" + thread + "?cooldown=" + duration + "#" + id; - } else { - return window.location = "http://boards.4chan.org/" + g.BOARD + "?cooldown=" + duration; - } + search = location.search; + cooldown = /cooldown/.test(search); + noko = /noko/.test(search); + sage = /sage/.test(search); + watch = /watch/.test(search); + url = "http://boards.4chan.org/" + g.BOARD; + if (watch && thread === '0') { + url += "/res/" + id + "?watch"; } else if (noko) { - return window.location = "http://boards.4chan.org/" + g.BOARD + "/res/" + thread + "#" + id; - } else { - return window.location = "http://boards.4chan.org/" + g.BOARD; + url += '/res/'; + url += thread === '0' ? id : thread; } + if (cooldown) { + duration = Date.now() + (sage ? 60 : 30) * 1000; + url += '?cooldown=' + duration; + } + if (noko) { + url += '#' + id; + } + return window.location = url; } } }; @@ -1555,6 +1619,14 @@ updater = { init: function() { var checkbox, checked, dialog, html, input, name, title, _i, _len, _ref; + if (conf['Scrolling']) { + $.bind(window, 'focus', (function() { + return updater.focus = true; + })); + $.bind(window, 'blur', (function() { + return updater.focus = false; + })); + } html = "
-" + conf['Interval'] + "
"; checkbox = config.updater.checkbox; for (name in checkbox) { @@ -1644,7 +1716,7 @@ while ((reply = replies.pop()) && (reply.id > id)) { arr.push(reply.parentNode.parentNode.parentNode); } - scroll = conf['Scrolling'] && arr.length && (d.body.scrollHeight - d.body.clientHeight - window.scrollY < 20); + scroll = conf['Scrolling'] && updater.focus && arr.length && (d.body.scrollHeight - d.body.clientHeight - window.scrollY < 20); updater.timer.textContent = '-' + conf['Interval']; if (conf['Verbose']) { updater.count.textContent = '+' + arr.length; @@ -1889,23 +1961,10 @@ foo: function() { var code; code = conf['time'].replace(/%([A-Za-z])/g, function(s, c) { - switch (c) { - case 'a': - case 'A': - case 'b': - case 'B': - case 'd': - case 'H': - case 'I': - case 'm': - case 'M': - case 'p': - case 'P': - case 'y': - return "' + Time." + c + "() + '"; - break; - default: - return s; + if (c in Time.formatters) { + return "' + Time.formatters." + c + "() + '"; + } else { + return s; } }); return Time.funk = Function('Time', "return '" + code + "'"); @@ -1919,49 +1978,60 @@ return n; } }, - a: function() { - return this.day[this.date.getDay()].slice(0, 3); - }, - A: function() { - return this.day[this.date.getDay()]; - }, - b: function() { - return this.month[this.date.getMonth()].slice(0, 3); - }, - B: function() { - return this.month[this.date.getMonth()]; - }, - d: function() { - return this.zeroPad(this.date.getDate()); - }, - H: function() { - return this.zeroPad(this.date.getHours()); - }, - I: function() { - return this.zeroPad(this.date.getHours() % 12 || 12); - }, - m: function() { - return this.zeroPad(this.date.getMonth() + 1); - }, - M: function() { - return this.zeroPad(this.date.getMinutes()); - }, - p: function() { - if (this.date.getHours() < 12) { - return 'AM'; - } else { - return 'PM'; + formatters: { + a: function() { + return Time.day[Time.date.getDay()].slice(0, 3); + }, + A: function() { + return Time.day[Time.date.getDay()]; + }, + b: function() { + return Time.month[Time.date.getMonth()].slice(0, 3); + }, + B: function() { + return Time.month[Time.date.getMonth()]; + }, + d: function() { + return Time.zeroPad(Time.date.getDate()); + }, + e: function() { + return Time.date.getDate(); + }, + H: function() { + return Time.zeroPad(Time.date.getHours()); + }, + I: function() { + return Time.zeroPad(Time.date.getHours() % 12 || 12); + }, + k: function() { + return Time.date.getHours(); + }, + l: function() { + return Time.date.getHours() % 12 || 12; + }, + m: function() { + return Time.zeroPad(Time.date.getMonth() + 1); + }, + M: function() { + return Time.zeroPad(Time.date.getMinutes()); + }, + p: function() { + if (Time.date.getHours() < 12) { + return 'AM'; + } else { + return 'PM'; + } + }, + P: function() { + if (Time.date.getHours() < 12) { + return 'am'; + } else { + return 'pm'; + } + }, + y: function() { + return Time.date.getFullYear() - 2000; } - }, - P: function() { - if (this.date.getHours() < 12) { - return 'am'; - } else { - return 'pm'; - } - }, - y: function() { - return this.date.getFullYear() - 2000; } }; titlePost = { @@ -1991,7 +2061,7 @@ } _results = []; for (qid in quotes) { - if (!(el = d.getElementById(qid))) { + if (!(el = $.id(qid))) { continue; } if (!conf['OP Backlinks'] && el.className === 'op') { @@ -2042,20 +2112,10 @@ }, toggle: function(e) { var el, hidden, id, inline, inlined, pathname, root, table, threadID, _i, _len, _ref; + if (e.shiftKey || e.altKey || e.ctrlKey || e.button !== 0) { + return; + } e.preventDefault(); - /* - https://bugzilla.mozilla.org/show_bug.cgi?id=674955 - `mouseout` does not fire when element removed - RESOLVED INVALID - - inline a post, then hover over an inlined quote / image, then remove - the inlined post by clicking `enter` on the still-focused link - the - mouseout event doesn't fire, and the quote preview / image hover remains. - - we can prevent this sequence by `blur`-ing the clicked links. chrome - doesn't focus clicked links anyway. - */ - this.blur(); id = this.hash.slice(1); if (table = $("#i" + id, $.x('ancestor::td[1]', this))) { $.rm(table); @@ -2063,14 +2123,14 @@ _ref = $$('input', table); for (_i = 0, _len = _ref.length; _i < _len; _i++) { inlined = _ref[_i]; - if (hidden = d.getElementById(inlined.name)) { + if (hidden = $.id(inlined.name)) { $.show($.x('ancestor::table[1]', hidden)); } } return; } root = this.parentNode.nodeName === 'FONT' ? this.parentNode : this.nextSibling ? this.nextSibling : this; - if (el = d.getElementById(id)) { + if (el = $.id(id)) { inline = quoteInline.table(id, el.innerHTML); if (this.className === 'backlink') { if ($("a.backlink[href='#" + id + "']", el)) { @@ -2166,7 +2226,7 @@ }); $.append(d.body, qp); id = this.hash.slice(1); - if (el = d.getElementById(id)) { + if (el = $.id(id)) { qp.innerHTML = el.innerHTML; if (conf['Quote Highlighting']) { $.addClass(el, 'qphl'); @@ -2191,7 +2251,7 @@ }, mouseout: function() { var el; - if (el = d.getElementById(this.hash.slice(1))) { + if (el = $.id(this.hash.slice(1))) { $.removeClass(el, 'qphl'); } return ui.hoverend(); @@ -2318,6 +2378,7 @@ }, scroll: function(e) { var bottom, height, i, reply, _len, _ref; + updater.focus = true; height = d.body.clientHeight; _ref = unread.replies; for (i = 0, _len = _ref.length; i < _len; i++) { @@ -2420,29 +2481,15 @@ el = _ref2[_i]; el.tabIndex = 1; } - $.bind($('#recaptcha_challenge_field_holder'), 'DOMNodeInserted', Recaptcha.reloaded); return $.bind($('#recaptcha_response_field'), 'keydown', Recaptcha.listener); }, listener: function(e) { if (e.keyCode === 8 && this.value === '') { - Recaptcha.reload(); - } - if (e.keyCode === 13 && cooldown.duration) { - $('#auto', qr.el).checked = true; - return qr.autohide.set(); + return Recaptcha.reload(); } }, reload: function() { return window.location = 'javascript:Recaptcha.reload()'; - }, - reloaded: function(e) { - var target; - if (!qr.el) { - return; - } - target = e.target; - $('img', qr.el).src = "http://www.google.com/recaptcha/api/image?c=" + target.value; - return $('input[name=recaptcha_challenge_field]', qr.el).value = target.value; } }; nodeInserted = function(e) { @@ -2704,10 +2751,13 @@ $.bind(form, 'submit', qr.submit); } threading.init(); - if (conf['Auto Noko']) { - $('.postarea form').action += '?auto_noko'; + if (g.REPLY && (id = location.hash.slice(1)) && /\d/.test(id[0]) && !$.id(id)) { + scrollTo(0, d.body.scrollHeight); } - if (conf['Cooldown']) { + if (conf['Auto Noko'] && canPost) { + form.action += '?noko'; + } + if (conf['Cooldown'] && canPost) { cooldown.init(); } if (conf['Image Expansion']) { @@ -2734,7 +2784,7 @@ if (conf['Reply Hiding']) { replyHiding.init(); } - if (canPost && conf['Quick Reply']) { + if (conf['Quick Reply'] && canPost) { qr.init(); } if (conf['Report Button']) { @@ -2765,8 +2815,11 @@ if (conf['Image Preloading']) { imgPreloading.init(); } - if (conf['Quick Reply'] && conf['Persistent QR']) { - qr.persist(); + if (conf['Quick Reply'] && conf['Persistent QR'] && canPost) { + qr.dialog(); + if (conf['Auto Hide QR']) { + $('#autohide', qr.el).checked = true; + } } if (conf['Post in Title']) { titlePost.init(); @@ -2780,7 +2833,7 @@ if (conf['Reply Navigation']) { nav.init(); } - if (conf['Auto Watch'] && conf['Thread Watcher'] && location.hash === '#watch' && $('img.favicon').src === Favicon.empty) { + if (conf['Auto Watch'] && conf['Thread Watcher'] && /watch/.test(location.search) && $('img.favicon').src === Favicon.empty) { watcher.watch(null, g.THREAD_ID); } } else { @@ -2797,7 +2850,7 @@ expandComment.init(); } if (conf['Auto Watch']) { - $('.postarea form').action += '?auto_watch'; + $('.postarea form').action += '?watch'; } } _ref3 = $$('div.op'); @@ -2922,6 +2975,9 @@ \ #qr {\ position: fixed;\ + max-height: 100%;\ + overflow-x: hidden;\ + overflow-y: auto;\ }\ #qr > div.move {\ text-align: right;\ @@ -2939,7 +2995,10 @@ width: 100%;\ height: 120px;\ }\ - #qr.auto:not(:hover) > form {\ + #qr #close, #qr #autohide {\ + float: right;\ + }\ + #qr:not(:hover) > #autohide:checked ~ form {\ height: 0;\ overflow: hidden;\ }\ @@ -3008,6 +3067,10 @@ [hidden] {\ display: none;\ }\ +\ + #files > input {\ + display: block;\ + }\ ' }; main.init(); diff --git a/README.md b/README.md index 97a0e5a67..03a41d8be 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,13 @@ +# Installing + +[master](https://github.com/aeosynth/4chan-x/raw/master/4chan_x.user.js) - bleeding edge. exciting new features, exciting new bugs. + +[stable](https://github.com/aeosynth/4chan-x/raw/stable/4chan_x.user.js) - tries to be bug free. + # Building [install nodejs and npm](https://github.com/joyent/node/wiki/Installation), install [coffee-script](https://github.com/jashkenas/coffee-script/) with `npm install -g coffee-script`, clone 4chan x, cd into it and run -`npm link coffee-script`. actually build it with `cake dev &`. -kill the process with `killall node`. +`npm link coffee-script`. actually build it with `cake build`. for development +(continuous builds), run `cake dev &`. kill the process with `killall node`. diff --git a/changelog b/changelog index 27155bcbc..0d84eb562 100644 --- a/changelog +++ b/changelog @@ -1,4 +1,19 @@ github +- mayhem: + - fix post links in expanded threads + - fix 4chan X in closed threads +- aeosynth: + - only auto scroll focused tabs + - quote inlining: only work on unmodified left-click + - select multiple files (one at a time) + - captcha caching + - qr: optional auto hiding + - copy old textarea value + - scroll to bottom of page if post isn't found (thumbnail generation takes + time) + - only scroll focused tabs + - time: %e, %k, %l + - reverted hovering fix 2.17.1 - mayhem: diff --git a/header b/header index f83a93977..1a3791d78 100644 --- a/header +++ b/header @@ -2,7 +2,7 @@ // @name 4chan x // @namespace aeosynth // @description Adds various features. -// @version 11.8.4.0 +// @version 11.8.6.0 // @copyright 2009-2011 James Campos // @license MIT; http://en.wikipedia.org/wiki/Mit_license // @include http://boards.4chan.org/* diff --git a/script.coffee b/script.coffee index 68bf519d1..99e5ec540 100644 --- a/script.coffee +++ b/script.coffee @@ -34,6 +34,7 @@ config = 'Cooldown': [true, 'Prevent \'flood detected\' errors'] 'Quick Reply': [true, 'Reply without leaving the page'] 'Persistent QR': [false, 'Quick reply won\'t disappear after posting. Only in replies.'] + 'Auto Hide QR': [true, 'Automatically auto-hide the quick reply when posting'] Quoting: 'Quote Backlinks': [true, 'Add quote backlinks'] 'OP Backlinks': [false, 'Add backlinks to the OP'] @@ -48,6 +49,7 @@ config = '#http://tineye.com/search?url=' '#http://saucenao.com/search.php?db=999&url=' '#http://imgur.com/upload?url=' + '#http://anonym.to/?' ].join '\n' time: '%m/%d/%y(%a)%H:%M' hotkeys: @@ -200,6 +202,8 @@ $.extend = (object, properties) -> object $.extend $, + id: (id) -> + d.getElementById id globalEval: (code) -> script = $.el 'script', textContent: "(#{code})()" @@ -430,7 +434,7 @@ expandThread = while (prev = table.previousSibling) and (prev.nodeName is 'TABLE') $.rm prev for backlink in $$ '.op a.backlink' - $.rm backlink if !d.getElementById backlink.hash[1..] + $.rm backlink if !$.id backlink.hash[1..] parse: (req, pathname, thread, a) -> @@ -449,9 +453,13 @@ expandThread = body = $.el 'body', innerHTML: req.responseText - for quote in $$ 'a.quotelink', body - if quote.getAttribute('href') is quote.hash - quote.pathname = pathname + for reply in $$ 'td[id]', body + for quote in $$ 'a.quotelink', reply + if quote.getAttribute('href') is quote.hash + quote.pathname = pathname + link = $ 'a.quotejs', reply + link.href = "res/#{thread.firstChild.id}##{reply.id}" + link.nextSibling.href = "res/#{thread.firstChild.id}#q#{reply.id}" tables = $$ 'form[name=delform] table', body tables.pop() for table in tables @@ -515,100 +523,100 @@ keybinds = init: -> for node in $$ '[accesskey]' node.removeAttribute 'accesskey' - $.bind d, 'keydown', keybinds.cb.keydown + $.bind d, 'keydown', keybinds.keydown - cb: - keydown: (e) -> - return if e.target.nodeName in ['TEXTAREA', 'INPUT'] and not e.altKey and not e.ctrlKey and not (e.keyCode is 27) - return unless key = keybinds.cb.keyCode e + keydown: (e) -> + updater.focus = true + return if e.target.nodeName in ['TEXTAREA', 'INPUT'] and not e.altKey and not e.ctrlKey and not (e.keyCode is 27) + return unless key = keybinds.keyCode e - thread = nav.getThread() - switch key - when conf.close - if o = $ '#overlay' - $.rm o - else if qr.el - qr.close() - when conf.spoiler - ta = e.target - return unless ta.nodeName is 'TEXTAREA' + thread = nav.getThread() + switch key + when conf.close + if o = $ '#overlay' + $.rm o + else if qr.el + qr.close() + when conf.spoiler + ta = e.target + return unless ta.nodeName is 'TEXTAREA' - value = ta.value - selStart = ta.selectionStart - selEnd = ta.selectionEnd + value = ta.value + selStart = ta.selectionStart + selEnd = ta.selectionEnd - valStart = value[0...selStart] + '[spoiler]' - valMid = value[selStart...selEnd] - valEnd = '[/spoiler]' + value[selEnd..] + valStart = value[0...selStart] + '[spoiler]' + valMid = value[selStart...selEnd] + valEnd = '[/spoiler]' + value[selEnd..] - ta.value = valStart + valMid + valEnd - range = valStart.length + valMid.length - ta.setSelectionRange range, range - when conf.zero - window.location = "/#{g.BOARD}/0#0" - when conf.openEmptyQR - keybinds.qr thread - when conf.nextReply - keybinds.hl.next thread - when conf.previousReply - keybinds.hl.prev thread - when conf.expandAllImages - keybinds.img thread, true - when conf.openThread - keybinds.open thread - when conf.expandThread - expandThread.toggle thread - when conf.openQR - keybinds.qr thread, true - when conf.expandImages - keybinds.img thread - when conf.nextThread - nav.next() - when conf.openThreadTab - keybinds.open thread, true - when conf.previousThread - nav.prev() - when conf.update - updater.update() - when conf.watch - watcher.toggle thread - when conf.hide - threadHiding.toggle thread - when conf.nextPage - $('input[value=Next]')?.click() - when conf.previousPage - $('input[value=Previous]')?.click() - when conf.submit - if qr.el - qr.submit.call $ 'form', qr.el - else - $('.postarea form').submit() - when conf.unreadCountTo0 - unread.replies.length = 0 - unread.updateTitle() - Favicon.update() + ta.value = valStart + valMid + valEnd + range = valStart.length + valMid.length + ta.setSelectionRange range, range + when conf.zero + window.location = "/#{g.BOARD}/0#0" + when conf.openEmptyQR + keybinds.qr thread + when conf.nextReply + keybinds.hl.next thread + when conf.previousReply + keybinds.hl.prev thread + when conf.expandAllImages + keybinds.img thread, true + when conf.openThread + keybinds.open thread + when conf.expandThread + expandThread.toggle thread + when conf.openQR + keybinds.qr thread, true + when conf.expandImages + keybinds.img thread + when conf.nextThread + nav.next() + when conf.openThreadTab + keybinds.open thread, true + when conf.previousThread + nav.prev() + when conf.update + updater.update() + when conf.watch + watcher.toggle thread + when conf.hide + threadHiding.toggle thread + when conf.nextPage + $('input[value=Next]')?.click() + when conf.previousPage + $('input[value=Previous]')?.click() + when conf.submit + if qr.el + qr.submit.call $ 'form', qr.el else - return - e.preventDefault() - - keyCode: (e) -> - kc = e.keyCode - if 65 <= kc <= 90 #A-Z - key = String.fromCharCode kc - if !e.shiftKey - key = key.toLowerCase() - else if 48 <= kc <= 57 #0-9 - key = String.fromCharCode kc - else if kc is 27 - key = 'Esc' - else if kc is 8 - key = '' + $('.postarea form').submit() + when conf.unreadCountTo0 + unread.replies.length = 0 + unread.updateTitle() + Favicon.update() else - key = null - if key - if e.altKey then key = 'alt+' + key - if e.ctrlKey then key = 'ctrl+' + key - key + return + e.preventDefault() + + keyCode: (e) -> + kc = e.keyCode + if 65 <= kc <= 90 #A-Z + key = String.fromCharCode kc + if !e.shiftKey + key = key.toLowerCase() + else if 48 <= kc <= 57 #0-9 + key = String.fromCharCode kc + else if kc is 27 + key = 'Esc' + else if kc is 8 + key = '' + else + key = null + if key + if e.altKey then key = 'alt+' + key + if e.ctrlKey then key = 'ctrl+' + key + key img: (thread, all) -> if all @@ -623,7 +631,7 @@ keybinds = qrLink = $ "span[id^=nothread] a:not(:first-child)", thread if quote - qr.quote qrLink + qr.quote.call qrLink else unless qr.el qr.dialog qrLink @@ -786,18 +794,28 @@ options = Format specifiers (source) SpecifierDescriptionValues/Example - %aweekday, abbreviatedSat - %Aweekday, fullSaturday + Year + %ytwo digit year00-99 + + Month %bmonth, abbreviatedJun %Bmonth, full lengthJune - %dday of the month, zero padded03 - %Hhour (24 hour clock) zero padded13 - %I (uppercase i)hour (12 hour clock) zero padded02 %mmonth, zero padded06 + + Day + %aweekday, abbreviatedSat + %Aweekday, fullSaturday + %dday of the month, zero padded03 + %eday of the month3 + + Time + %Hhour (24 hour clock) zero padded13 + %l (lowercase L)hour (12 hour clock)1 + %I (uppercase i)hour (12 hour clock) zero padded01 + %khour (24 hour clock)13 %Mminutes, zero padded54 %pupper case AM or PMPM %Plower case am or pmpm - %ytwo digit year00-99 @@ -892,7 +910,7 @@ options = keybind: (e) -> e.preventDefault() e.stopPropagation() - return unless (key = keybinds.cb.keyCode e)? + return unless (key = keybinds.keyCode e)? @value = key $.setValue @name, key conf[@name] = key @@ -936,7 +954,7 @@ cooldown = cb: -> submits = $$ '#com_submit' - if --cooldown.duration + if --cooldown.duration > 0 for submit in submits submit.value = cooldown.duration else @@ -945,61 +963,182 @@ cooldown = submit.disabled = false submit.value = 'Submit' if qr.el and $('#auto', qr.el).checked - qr.submit.call $ 'form', qr.el + qr.autoPost() qr = + # TODO + # error handling + # persistent captcha + # rm Recaptcha + # error too large error should happen on attach init: -> - g.callbacks.push qr.cb.node + g.callbacks.push qr.node + $.bind window, 'message', qr.message + $.bind $('#recaptcha_challenge_field_holder'), 'DOMNodeInserted', qr.captchaNode + qr.captcha = [] + iframe = $.el 'iframe', name: 'iframe' hidden: true $.append d.body, iframe - $.bind window, 'message', qr.cb.message #hack - nuke id so it doesn't grab focus when reloading $('#recaptcha_response_field').id = '' - autohide: - set: -> - $('#autohide:not(:checked)', qr.el)?.click() - unset: -> - $('#autohide:checked', qr.el)?.click() + attach: -> + $('#auto', qr.el).checked = true + fileDiv = $.el 'div', innerHTML: 'X' + $.bind fileDiv.lastChild, 'click', (-> $.rm @parentNode) + $.prepend qr.files, fileDiv - cb: - autohide: (e) -> - if @checked - $.addClass qr.el, 'auto' + attachNext: -> + fileDiv = $.rm qr.files.lastChild + file = fileDiv.firstChild + oldFile = $ '#qr_form input[type=file]', qr.el + $.replace oldFile, file + + autoPost: -> + responseField = $ '#recaptcha_response_field', qr.el + if !responseField.value and captcha = qr.captcha.shift() + $('#recaptcha_challenge_field', qr.el).value = captcha.challenge + responseField.value = captcha.response + responseField.nextSibling.textContent = qr.captcha.length + ' captcha cached' + qr.submit.call $ 'form', qr.el + + captchaNode: (e) -> + return unless qr.el + {target} = e + $('img', qr.el).src = "http://www.google.com/recaptcha/api/image?c=" + target.value + $('#recaptcha_challenge_field', qr.el).value = target.value + + captchaKeydown: (e) -> + if e.keyCode is 13 and cooldown.duration # press enter to enable auto-post if cooldown is still running + $('#auto', qr.el).checked = true + $('#autohide', qr.el).checked = true if conf['Auto Hide QR'] + qr.captchaPush.call this + + captchaPush: -> + l = qr.captcha.push + challenge: $('#recaptcha_challenge_field', qr.el).value + response: @value + @nextSibling.textContent = l + ' captcha cached' + Recaptcha.reload() + @value = '' + + close: -> + $.rm qr.el + qr.el = null + + dialog: (link) -> + submitValue = $('#com_submit').value + submitDisabled = if $('#com_submit').disabled then 'disabled' else '' + #FIXME inlined cross-thread quotes + THREAD_ID = g.THREAD_ID or $.x('ancestor::div[@class="thread"]/div', link).id + spoiler = if $('.postarea label') then '' else '' + challenge = $('#recaptcha_challenge_field').value + html = " + X + +
+ + Quick Reply +
+
+ + +
#{spoiler}
+
+
+
+
0 captcha cached
+
+
attach another file
+
+
+ + " + qr.el = ui.dialog 'qr', top: '0px', left: '0px', html + qr.files = $ '#files', qr.el + + qr.refresh() + $('textarea', qr.el).value = $('textarea').value + + $.bind $('input[name=name]', qr.el), 'mousedown', (e) -> e.stopPropagation() + $.bind $('#close', qr.el), 'click', qr.close + $.bind $('form', qr.el), 'submit', qr.submit + $.bind $('a[name=attach]', qr.el), 'click', qr.attach + $.bind $('img', qr.el), 'click', Recaptcha.reload + $.bind $('#recaptcha_response_field', qr.el), 'keydown', Recaptcha.listener + $.bind $('#recaptcha_response_field', qr.el), 'keydown', qr.captchaKeydown + + $.append d.body, qr.el + + message: (e) -> + Recaptcha.reload() + $('iframe[name=iframe]').src = 'about:blank' + + {data} = e + if data # error message + data = JSON.parse data + $.extend $('#error', qr.el), data + $('#recaptcha_response_field', qr.el).value = '' + $('#autohide', qr.el).checked = false + if data.textContent is 'You seem to have mistyped the verification.' + if qr.captcha.length + qr.autoPost() + else if data.textContent is 'Error: Duplicate file entry detected.' and qr.files.childElementCount + $('textarea', qr.el).value += '\n' + data.textContent + ' ' + data.href + qr.attachNext() + if qr.captcha.length + qr.autoPost() + return + + if qr.el + if g.REPLY and (conf['Persistent QR'] or qr.files.childElementCount) + qr.refresh() + if qr.files.childElementCount + qr.attachNext() else - $.removeClass qr.el, 'auto' + qr.close() + if conf['Cooldown'] + duration = if qr.sage then 60 else 30 + $.setValue g.BOARD+'/cooldown', Date.now() + duration * 1000 + cooldown.start() - message: (e) -> - Recaptcha.reload() - $('iframe[name=iframe]').src = 'about:blank' + node: (root) -> + quote = $ 'a.quotejs:not(:first-child)', root + $.bind quote, 'click', qr.quote - {data} = e - if data # error message - $('input[name=recaptcha_response_field]', qr.el).value = '' - $.extend $('#error', qr.el), JSON.parse data - qr.autohide.unset() - return + quote: (e) -> + e.preventDefault() if e - if qr.el - if g.REPLY and conf['Persistent QR'] - qr.refresh() - else - qr.close() - if conf['Cooldown'] - duration = if qr.sage then 60 else 30 - $.setValue g.BOARD+'/cooldown', Date.now() + duration * 1000 - cooldown.start() + if qr.el + $('#autohide', qr.el).checked = false + else + qr.dialog @ - node: (root) -> - quote = $ 'a.quotejs:not(:first-child)', root - $.bind quote, 'click', qr.cb.quote + id = @textContent + text = ">>#{id}\n" - quote: (e) -> - e.preventDefault() - qr.quote @ + selection = window.getSelection() + if s = selection.toString() + selectionID = $.x('preceding::input[@type="checkbox"][1]', selection.anchorNode)?.name + if selectionID == id + s = s.replace /\n/g, '\n>' + text += ">#{s}\n" + + ta = $ 'textarea', qr.el + ta.focus() + ta.value += text + + refresh: -> + auto = $('#auto', qr.el).checked + $('form', qr.el).reset() + $('#auto', qr.el).checked = auto + c = d.cookie + $('input[name=name]', qr.el).value = if m = c.match(/4chan_name=([^;]+)/) then decodeURIComponent m[1] else '' + $('input[name=email]', qr.el).value = if m = c.match(/4chan_email=([^;]+)/) then decodeURIComponent m[1] else '' + $('input[name=pwd]', qr.el).value = if m = c.match(/4chan_pass=([^;]+)/) then decodeURIComponent m[1] else $('input[name=pwd]').value submit: (e) -> if conf['Auto Watch Reply'] and conf['Thread Watcher'] @@ -1007,7 +1146,7 @@ qr = watcher.watch null, g.THREAD_ID else id = $('input[name=resto]', qr.el).value - op = d.getElementById id + op = $.id id if $('img.favicon', op).src is Favicon.empty watcher.watch op, id @@ -1024,84 +1163,9 @@ qr = else if isQR if !e then @submit() $('#error', qr.el).textContent = '' - qr.autohide.set() + $('#autohide', qr.el).checked = true if conf['Auto Hide QR'] qr.sage = /sage/i.test $('input[name=email]', @).value - quote: (link) -> - if qr.el - qr.autohide.unset() - else - qr.dialog link - - id = link.textContent - text = ">>#{id}\n" - - selection = window.getSelection() - if s = selection.toString() - selectionID = $.x('preceding::input[@type="checkbox"][1]', selection.anchorNode)?.name - if selectionID == id - s = s.replace /\n/g, '\n>' - text += ">#{s}\n" - - ta = $ 'textarea', qr.el - ta.focus() - ta.value += text - - refresh: -> - $('form', qr.el).reset() - c = d.cookie - $('input[name=name]', qr.el).value = if m = c.match(/4chan_name=([^;]+)/) then decodeURIComponent m[1] else '' - $('input[name=email]', qr.el).value = if m = c.match(/4chan_email=([^;]+)/) then decodeURIComponent m[1] else '' - $('input[name=pwd]', qr.el).value = if m = c.match(/4chan_pass=([^;]+)/) then decodeURIComponent m[1] else $('input[name=pwd]').value - - dialog: (link) -> - submitValue = $('#com_submit').value - submitDisabled = if $('#com_submit').disabled then 'disabled' else '' - #FIXME inlined cross-thread quotes - THREAD_ID = g.THREAD_ID or $.x('ancestor::div[@class="thread"]/div', link).id - spoiler = if $('.postarea label') then '' else '' - challenge = $('input[name=recaptcha_challenge_field]').value - html = " -
- - Quick Reply - - X -
-
- - -
#{spoiler}
-
-
-
-
-
-
-
- - " - qr.el = ui.dialog 'qr', top: '0px', left: '0px', html - - qr.refresh() - - $.bind $('input[name=name]', qr.el), 'mousedown', (e) -> e.stopPropagation() - $.bind $('#autohide', qr.el), 'click', qr.cb.autohide - $.bind $('a[name=close]', qr.el), 'click', qr.close - $.bind $('form', qr.el), 'submit', qr.submit - $.bind $('img', qr.el), 'click', Recaptcha.reload - $.bind $('input[name=recaptcha_response_field]', qr.el), 'keydown', Recaptcha.listener - - $.append d.body, qr.el - - persist: -> - qr.dialog() - qr.autohide.set() - - close: -> - $.rm qr.el - qr.el = null - sys: -> if recaptcha = $ '#recaptcha_response_field' #post reporting $.bind recaptcha, 'keydown', Recaptcha.listener @@ -1126,23 +1190,26 @@ qr = if c.nodeType is 8 #comment node [_, thread, id] = c.textContent.match(/thread:(\d+),no:(\d+)/) - noko = /auto_noko/.test location.search - if thread is '0' - if /auto_watch/.test location.search - window.location = "http://boards.4chan.org/#{g.BOARD}/res/#{id}#watch" - else if noko - window.location = "http://boards.4chan.org/#{g.BOARD}/res/#{id}" - else if /cooldown/.test location.search - duration = Date.now() + 30000 - duration += 30000 if /sage/.test location.search - if noko - window.location = "http://boards.4chan.org/#{g.BOARD}/res/#{thread}?cooldown=#{duration}##{id}" - else - window.location = "http://boards.4chan.org/#{g.BOARD}?cooldown=#{duration}" + {search} = location + cooldown = /cooldown/.test search + noko = /noko/ .test search + sage = /sage/ .test search + watch = /watch/ .test search + + url = "http://boards.4chan.org/#{g.BOARD}" + + if watch and thread is '0' + url += "/res/#{id}?watch" else if noko - window.location = "http://boards.4chan.org/#{g.BOARD}/res/#{thread}##{id}" - else - window.location = "http://boards.4chan.org/#{g.BOARD}" + url += '/res/' + url += if thread is '0' then id else thread + if cooldown + duration = Date.now() + (if sage then 60 else 30) * 1000 + url += '?cooldown=' + duration + if noko + url += '#' + id + + window.location = url threading = init: -> @@ -1254,6 +1321,9 @@ threadHiding = updater = init: -> + if conf['Scrolling'] + $.bind window, 'focus', (-> updater.focus = true) + $.bind window, 'blur', (-> updater.focus = false) html = "
-#{conf['Interval']}
" {checkbox} = config.updater for name of checkbox @@ -1329,7 +1399,7 @@ updater = while (reply = replies.pop()) and (reply.id > id) arr.push reply.parentNode.parentNode.parentNode #table - scroll = conf['Scrolling'] && arr.length && (d.body.scrollHeight - d.body.clientHeight - window.scrollY < 20) + scroll = conf['Scrolling'] && updater.focus && arr.length && (d.body.scrollHeight - d.body.clientHeight - window.scrollY < 20) updater.timer.textContent = '-' + conf['Interval'] if conf['Verbose'] @@ -1499,9 +1569,10 @@ Time = $.replace s, time foo: -> code = conf['time'].replace /%([A-Za-z])/g, (s, c) -> - switch c - when 'a', 'A', 'b', 'B', 'd', 'H', 'I', 'm', 'M', 'p', 'P', 'y' then "' + Time.#{c}() + '" - else s + if c of Time.formatters + "' + Time.formatters.#{c}() + '" + else + s Time.funk = Function 'Time', "return '#{code}'" day: [ 'Sunday' @@ -1527,18 +1598,22 @@ Time = 'December' ] zeroPad: (n) -> if n < 10 then '0' + n else n - a: -> @day[@date.getDay()][...3] - A: -> @day[@date.getDay()] - b: -> @month[@date.getMonth()][...3] - B: -> @month[@date.getMonth()] - d: -> @zeroPad @date.getDate() - H: -> @zeroPad @date.getHours() - I: -> @zeroPad @date.getHours() % 12 or 12 - m: -> @zeroPad @date.getMonth() + 1 - M: -> @zeroPad @date.getMinutes() - p: -> if @date.getHours() < 12 then 'AM' else 'PM' - P: -> if @date.getHours() < 12 then 'am' else 'pm' - y: -> @date.getFullYear() - 2000 + formatters: + a: -> Time.day[Time.date.getDay()][...3] + A: -> Time.day[Time.date.getDay()] + b: -> Time.month[Time.date.getMonth()][...3] + B: -> Time.month[Time.date.getMonth()] + d: -> Time.zeroPad Time.date.getDate() + e: -> Time.date.getDate() + H: -> Time.zeroPad Time.date.getHours() + I: -> Time.zeroPad Time.date.getHours() % 12 or 12 + k: -> Time.date.getHours() + l: -> Time.date.getHours() % 12 or 12 + m: -> Time.zeroPad Time.date.getMonth() + 1 + M: -> Time.zeroPad Time.date.getMinutes() + p: -> if Time.date.getHours() < 12 then 'AM' else 'PM' + P: -> if Time.date.getHours() < 12 then 'am' else 'pm' + y: -> Time.date.getFullYear() - 2000 titlePost = init: -> @@ -1558,7 +1633,7 @@ quoteBacklink = #duplicate quotes get overwritten quotes[qid] = quote for qid of quotes - continue unless el = d.getElementById qid + continue unless el = $.id qid #don't backlink the op continue if !conf['OP Backlinks'] and el.className is 'op' link = $.el 'a', @@ -1585,30 +1660,19 @@ quoteInline = quote.removeAttribute 'onclick' $.bind quote, 'click', quoteInline.toggle toggle: (e) -> + return if e.shiftKey or e.altKey or e.ctrlKey or e.button isnt 0 + e.preventDefault() - ### - https://bugzilla.mozilla.org/show_bug.cgi?id=674955 - `mouseout` does not fire when element removed - RESOLVED INVALID - - inline a post, then hover over an inlined quote / image, then remove - the inlined post by clicking `enter` on the still-focused link - the - mouseout event doesn't fire, and the quote preview / image hover remains. - - we can prevent this sequence by `blur`-ing the clicked links. chrome - doesn't focus clicked links anyway. - ### - @blur() id = @hash[1..] if table = $ "#i#{id}", $.x 'ancestor::td[1]', @ $.rm table $.removeClass @, 'inlined' for inlined in $$ 'input', table - if hidden = d.getElementById inlined.name + if hidden = $.id inlined.name $.show $.x 'ancestor::table[1]', hidden return root = if @parentNode.nodeName is 'FONT' then @parentNode else if @nextSibling then @nextSibling else @ - if el = d.getElementById id + if el = $.id id inline = quoteInline.table id, el.innerHTML if @className is 'backlink' return if $("a.backlink[href='##{id}']", el) @@ -1670,7 +1734,7 @@ quotePreview = $.append d.body, qp id = @hash[1..] - if el = d.getElementById id + if el = $.id id qp.innerHTML = el.innerHTML $.addClass el, 'qphl' if conf['Quote Highlighting'] if /backlink/.test @className @@ -1683,7 +1747,7 @@ quotePreview = threadID = @pathname.split('/').pop() or $.x('ancestor::div[@class="thread"]/div', @).id $.cache @pathname, (-> quotePreview.parse @, id, threadID) mouseout: -> - $.removeClass el, 'qphl' if el = d.getElementById @hash[1..] + $.removeClass el, 'qphl' if el = $.id @hash[1..] ui.hoverend() parse: (req, id, threadID) -> return unless (qp = ui.el) and (qp.innerHTML is "Loading #{id}...") @@ -1767,6 +1831,7 @@ unread = Favicon.update() scroll: (e) -> + updater.focus = true height = d.body.clientHeight for reply, i in unread.replies {bottom} = reply.getBoundingClientRect() @@ -1826,21 +1891,12 @@ Recaptcha = #hack to tab from comment straight to recaptcha for el in $$ '#recaptcha_table a' el.tabIndex = 1 - $.bind $('#recaptcha_challenge_field_holder'), 'DOMNodeInserted', Recaptcha.reloaded $.bind $('#recaptcha_response_field'), 'keydown', Recaptcha.listener listener: (e) -> if e.keyCode is 8 and @value is '' # backspace to reload Recaptcha.reload() - if e.keyCode is 13 and cooldown.duration # press enter to enable auto-post if cooldown is still running - $('#auto', qr.el).checked = true - qr.autohide.set() reload: -> window.location = 'javascript:Recaptcha.reload()' - reloaded: (e) -> - return unless qr.el - {target} = e - $('img', qr.el).src = "http://www.google.com/recaptcha/api/image?c=" + target.value - $('input[name=recaptcha_challenge_field]', qr.el).value = target.value nodeInserted = (e) -> {target} = e @@ -2090,17 +2146,23 @@ main = $.addStyle main.css - if (form = $ 'form[name=post]') and canPost = !!$ '#recaptcha_response_field' + #recaptcha may be blocked, eg by noscript + if (form = $ 'form[name=post]') and (canPost = !!$ '#recaptcha_response_field') Recaptcha.init() $.bind form, 'submit', qr.submit #major features threading.init() - if conf['Auto Noko'] - $('.postarea form').action += '?auto_noko' + # scroll to bottom if post isn't found + # thumbnail generation takes time + if g.REPLY and (id = location.hash[1..]) and /\d/.test(id[0]) and !$.id(id) + scrollTo 0, d.body.scrollHeight - if conf['Cooldown'] + if conf['Auto Noko'] and canPost + form.action += '?noko' + + if conf['Cooldown'] and canPost cooldown.init() if conf['Image Expansion'] @@ -2127,7 +2189,7 @@ main = if conf['Reply Hiding'] replyHiding.init() - if canPost and conf['Quick Reply'] + if conf['Quick Reply'] and canPost qr.init() if conf['Report Button'] @@ -2158,8 +2220,10 @@ main = if conf['Image Preloading'] imgPreloading.init() - if conf['Quick Reply'] and conf['Persistent QR'] - qr.persist() + if conf['Quick Reply'] and conf['Persistent QR'] and canPost + qr.dialog() + if conf['Auto Hide QR'] + $('#autohide', qr.el).checked = true if conf['Post in Title'] titlePost.init() @@ -2174,7 +2238,7 @@ main = nav.init() if conf['Auto Watch'] and conf['Thread Watcher'] and - location.hash is '#watch' and $('img.favicon').src is Favicon.empty + /watch/.test(location.search) and $('img.favicon').src is Favicon.empty watcher.watch null, g.THREAD_ID else #not reply @@ -2191,7 +2255,7 @@ main = expandComment.init() if conf['Auto Watch'] - $('.postarea form').action += '?auto_watch' + $('.postarea form').action += '?watch' for op in $$ 'div.op' for callback in g.callbacks @@ -2303,6 +2367,9 @@ main = #qr { position: fixed; + max-height: 100%; + overflow-x: hidden; + overflow-y: auto; } #qr > div.move { text-align: right; @@ -2320,7 +2387,10 @@ main = width: 100%; height: 120px; } - #qr.auto:not(:hover) > form { + #qr #close, #qr #autohide { + float: right; + } + #qr:not(:hover) > #autohide:checked ~ form { height: 0; overflow: hidden; } @@ -2389,6 +2459,10 @@ main = [hidden] { display: none; } + + #files > input { + display: block; + } ' main.init()