From a9b427f0d23032cfc25849ff66796bcdffb0e4a8 Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Tue, 12 Feb 2013 23:05:09 +0100 Subject: [PATCH] Remove the 'Open Reply in New Tab' config and make it the default behavior. Various QR fixes. Tiny styling adjustments. You can now create threads when outside of the index. Allow selection-to-quote to work on any text inside the quoted post, not just the comment. Close #789. --- 4chan_x.user.js | 272 +++++++++++++++++++++++--------------------- changelog | 5 +- css/burichan.css | 8 ++ css/futaba.css | 8 ++ css/photon.css | 8 ++ css/style.css | 149 +++++++++++------------- css/tomorrow.css | 8 ++ css/yotsuba-b.css | 8 ++ css/yotsuba.css | 8 ++ src/config.coffee | 1 - src/features.coffee | 183 ++++++++++++++--------------- src/main.coffee | 57 ++++++---- 12 files changed, 385 insertions(+), 330 deletions(-) diff --git a/4chan_x.user.js b/4chan_x.user.js index 895d20262..a4dad4a7e 100644 --- a/4chan_x.user.js +++ b/4chan_x.user.js @@ -100,7 +100,6 @@ 'Quick Reply': [true, 'WMD.'], 'Persistent QR': [false, 'The Quick reply won\'t disappear after posting.'], 'Auto Hide QR': [false, 'Automatically hide the quick reply when posting.'], - 'Open Reply in New Tab': [false, 'Open replies posted from the board pages in a new tab.'], 'Remember Subject': [false, 'Remember the subject field, instead of resetting after posting.'], 'Remember Spoiler': [false, 'Remember the spoiler state, instead of resetting after posting.'], 'Hide Original Post Form': [true, 'Replace the normal post form with a shortcut to open the QR.'] @@ -1825,40 +1824,51 @@ QR = { init: function() { var link; - if (!Conf['Quick Reply']) { + if (g.VIEW === 'catalog' || !Conf['Quick Reply']) { return; } if (Conf['Hide Original Post Form']) { - Main.css += "#postForm, .postingMode {\n display: none;\n}"; + $.addClass(doc, 'hide-original-post-form'); } link = $.el('a', { + className: 'qr-shortcut', textContent: 'Quick Reply', href: 'javascript:;' }); $.on(link, 'click', function() { Header.menu.close(); - return QR.open(); + QR.open(); + if (g.BOARD.ID === 'f') { + if (g.VIEW === 'index') { + QR.threadSelector.value = '9999'; + } + } else if (g.VIEW === 'thread') { + QR.threadSelector.value = g.THREAD; + } else { + QR.threadSelector.value = 'new'; + } + return $('textarea', QR.el).focus(); }); $.event('AddMenuEntry', { type: 'header', el: link }); - Post.prototype.callbacks.push({ + $.on(d, 'dragover', QR.dragOver); + $.on(d, 'drop', QR.dropFile); + $.on(d, 'dragstart dragend', QR.drag); + $.on(d, '4chanXInitFinished', function() { + if (!Conf['Persistent QR']) { + return; + } + QR.open(); + if (Conf['Auto Hide QR']) { + return QR.hide(); + } + }); + return Post.prototype.callbacks.push({ name: 'Quick Reply', cb: this.node }); - return $.ready(this.readyInit); - }, - readyInit: function() { - if (Conf['Persistent QR']) { - QR.open(); - if (Conf['Auto Hide QR']) { - QR.hide(); - } - } - $.on(d, 'dragover', QR.dragOver); - $.on(d, 'drop', QR.dropFile); - return $.on(d, 'dragstart dragend', QR.drag); }, node: function() { return $.on($('a[title="Quote this post"]', this.nodes.info), 'click', QR.quote); @@ -1866,16 +1876,17 @@ open: function() { if (QR.el) { QR.el.hidden = false; - return QR.unhide(); - } else { - try { - return QR.dialog(); - } catch (err) { - return Main.handleErrors({ - message: 'Quick Reply dialog creation crashed.', - error: err - }); - } + QR.unhide(); + return; + } + try { + return QR.dialog(); + } catch (err) { + delete QR.el; + return Main.handleErrors({ + message: 'Quick Reply dialog creation crashed.', + error: err + }); } }, close: function() { @@ -1895,7 +1906,7 @@ if (!Conf['Remember Spoiler'] && (spoiler = $.id('spoiler')).checked) { spoiler.click(); } - return QR.cleanError(); + return QR.cleanNotification(); }, hide: function() { d.activeElement.blur(); @@ -1907,7 +1918,11 @@ return $.id('autohide').checked = false; }, toggleHide: function() { - return this.checked && QR.hide() || QR.unhide(); + if (this.checked) { + return QR.hide(); + } else { + return QR.unhide(); + } }, error: function(err) { var el; @@ -1926,7 +1941,7 @@ } return QR.lastNotification = new Notification('warning', el); }, - cleanError: function() { + cleanNotification: function() { var _ref; if ((_ref = QR.lastNotification) != null) { _ref.close(); @@ -1948,14 +1963,11 @@ } value = data.progress || QR.cooldown.seconds || value; input = QR.status.input; - input.value = QR.cooldown.auto && Conf['Cooldown'] ? value ? "Auto " + value : 'Auto' : value || 'Submit'; + input.value = QR.cooldown.auto ? value ? "Auto " + value : 'Auto' : value || 'Submit'; return input.disabled = disabled || false; }, cooldown: { init: function() { - if (!Conf['Cooldown']) { - return; - } QR.cooldown.types = { thread: (function() { switch (g.BOARD) { @@ -1993,9 +2005,6 @@ }, set: function(data) { var cooldown, hasFile, isReply, isSage, start, type; - if (!Conf['Cooldown']) { - return; - } start = Date.now(); if (data.delay) { cooldown = { @@ -2032,7 +2041,8 @@ QR.status(); return; } - if ((isReply = g.REPLY ? true : QR.threadSelector.value !== 'new')) { + isReply = g.BOARD.ID === 'f' && g.VIEW === 'thread' ? true : QR.threadSelector.value !== 'new'; + if (isReply) { post = QR.replies[0]; isSage = /sage/i.test(post.email); hasFile = !!post.file; @@ -2073,19 +2083,20 @@ } }, quote: function(e) { - var caretPos, id, range, s, sel, ta, text, _ref; + var caretPos, post, range, s, sel, selectionRoot, ta, text; if (e != null) { e.preventDefault(); } QR.open(); ta = $('textarea', QR.el); - if (!(g.REPLY || ta.value)) { + if (QR.threadSelector && !ta.value && g.BOARD.ID !== 'f') { QR.threadSelector.value = $.x('ancestor::div[parent::div[@class="board"]]', this).id.slice(1); } - id = this.previousSibling.hash.slice(2); - text = ">>" + id + "\n"; + post = Get.postFromRoot($.x('ancestor-or-self::div[contains(@class,"postContainer")][1]', this)); + text = ">>" + post + "\n"; sel = d.getSelection(); - if ((s = sel.toString().trim()) && id === ((_ref = $.x('ancestor-or-self::blockquote', sel.anchorNode)) != null ? _ref.id.match(/\d+$/)[0] : void 0)) { + selectionRoot = $.x('ancestor-or-self::div[contains(@class,"postContainer")][1]', sel.anchorNode); + if ((s = sel.toString().trim()) && post.nodes.root === selectionRoot) { s = s.replace(/\n/g, '\n>'); text += ">" + s + "\n"; } @@ -2125,7 +2136,7 @@ }, fileInput: function() { var file, _i, _len, _ref; - QR.cleanError(); + QR.cleanNotification(); if (this.files.length === 1) { file = this.files[0]; if (file.size > this.max) { @@ -2175,7 +2186,7 @@ this.spoiler = prev && Conf['Remember Spoiler'] ? prev.spoiler : false; this.com = null; this.el = $.el('a', { - className: 'thumbnail', + className: 'qrpreview', draggable: true, href: 'javascript:;', innerHTML: '×' @@ -2333,7 +2344,7 @@ }; _Class.prototype.rm = function() { - var index, _base; + var index, _ref; QR.resetFileInput(); $.rm(this.el); index = QR.replies.indexOf(this); @@ -2343,7 +2354,7 @@ (QR.replies[index - 1] || QR.replies[index + 1]).select(); } QR.replies.splice(index, 1); - return typeof (_base = window.URL || window.webkitURL).revokeObjectURL === "function" ? _base.revokeObjectURL(this.url) : void 0; + return (_ref = window.URL || window.webkitURL) != null ? _ref.revokeObjectURL(this.url) : void 0; }; return _Class; @@ -2418,7 +2429,7 @@ }, load: function() { var challenge; - this.timeout = Date.now() + 4 * $.MINUTE; + this.timeout = Date.now() + $.unsafeWindow.RecaptchaState.timeout * $.SECOND - $.MINUTE; challenge = this.challenge.firstChild.value; this.img.alt = challenge; this.img.src = "//www.google.com/recaptcha/api/image?c=" + challenge; @@ -2438,7 +2449,7 @@ return this.input.alt = count; }, reload: function(focus) { - $.globalEval('javascript:Recaptcha.reload("t")'); + $.unsafeWindow.Recaptcha.reload('r'); if (focus) { return QR.captcha.input.focus(); } @@ -2457,25 +2468,8 @@ } }, dialog: function() { - var fileInput, id, mimeTypes, name, spoiler, ta, thread, threads, _i, _j, _len, _len1, _ref, _ref1; - QR.el = UI.dialog('qr', 'top:0;right:0;', '\ -
\ - Quick Reply \ - ×\ -
\ -
\ -
\ - \ -
\ -
\ - \ -
'); - if (Conf['Remember QR size'] && $.engine === 'gecko') { - $.on(ta = $('textarea', QR.el), 'mouseup', function() { - return $.set('QR.size', this.style.cssText); - }); - ta.style.cssText = $.get('QR.size', ''); - } + var fileInput, key, mimeTypes, name, span, spoiler, ta, thread, threads, _i, _len, _ref, _ref1; + QR.el = UI.dialog('qr', 'top:0;right:0;', "
Quick Reply ×
\n
\n
\n \n
\n
\n \n
"); mimeTypes = $('ul.rules').firstElementChild.textContent.trim().match(/: (.+)/)[1].toLowerCase().replace(/\w+/g, function(type) { switch (type) { case 'jpg': @@ -2500,23 +2494,32 @@ spoiler.hidden = !QR.spoiler; QR.charaCounter = $('#charCount', QR.el); ta = $('textarea', QR.el); - if (!g.REPLY) { - threads = ''; - _ref = $$('.thread'); - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - thread = _ref[_i]; - id = thread.id.slice(1); - threads += ""; + span = $('.move > span', QR.el); + if (g.BOARD.ID === 'f') { + if (g.VIEW === 'index') { + QR.threadSelector = $('select[name=filetag]').cloneNode(true); } - QR.threadSelector = g.BOARD === 'f' ? $('select[name=filetag]').cloneNode(true) : $.el('select', { - innerHTML: threads, + } else { + QR.threadSelector = $.el('select', { title: 'Create a new thread / Reply to a thread' }); - $.prepend($('.move > span', QR.el), QR.threadSelector); - $.on(QR.threadSelector, 'mousedown', function(e) { - return e.stopPropagation(); - }); + threads = ''; + _ref = g.BOARD.threads; + for (key in _ref) { + thread = _ref[key]; + threads += ""; + } + QR.threadSelector.innerHTML = threads; + if (g.VIEW === 'thread') { + QR.threadSelector.value = g.THREAD; + } } + if (QR.threadSelector) { + $.prepend(span, QR.threadSelector); + } + $.on(span, 'mousedown', function(e) { + return e.stopPropagation(); + }); $.on($('#autohide', QR.el), 'change', QR.toggleHide); $.on($('.close', QR.el), 'click', QR.close); $.on($('#dump', QR.el), 'click', function() { @@ -2541,8 +2544,8 @@ }); new QR.reply().select(); _ref1 = ['name', 'email', 'sub', 'com']; - for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { - name = _ref1[_j]; + for (_i = 0, _len = _ref1.length; _i < _len; _i++) { + name = _ref1[_i]; $.on($("[name=" + name + "]", QR.el), 'input', function() { var _ref2; QR.selected[this.name] = this.value; @@ -2570,19 +2573,19 @@ } QR.abort(); reply = QR.replies[0]; - if (g.BOARD === 'f' && !g.REPLY) { + if (g.BOARD.ID === 'f' && g.VIEW === 'index') { filetag = QR.threadSelector.value; threadID = 'new'; } else { - threadID = g.THREAD_ID || QR.threadSelector.value; + threadID = QR.threadSelector.value; } if (threadID === 'new') { threadID = null; - if (((_ref = g.BOARD) === 'vg' || _ref === 'q') && !reply.sub) { + if (((_ref = g.BOARD.ID) === 'vg' || _ref === 'q') && !reply.sub) { err = 'New threads require a subject.'; } else if (!(reply.file || (textOnly = !!$('input[name=textonly]', $.id('postForm'))))) { err = 'No file selected.'; - } else if (g.BOARD === 'f' && filetag === '9999') { + } else if (g.BOARD.ID === 'f' && filetag === '9999') { err = 'Invalid tag specified.'; } } else if (!(reply.com || reply.file)) { @@ -2619,7 +2622,7 @@ QR.error(err); return; } - QR.cleanError(); + QR.cleanNotification(); QR.cooldown.auto = QR.replies.length > 1; if (Conf['Auto Hide QR'] && !QR.cooldown.auto) { QR.hide(); @@ -2727,13 +2730,11 @@ post: reply, isReply: threadID !== '0' }); + QR.cooldown.auto = QR.replies.length > 1; if (threadID === '0') { - location.pathname = "/" + g.BOARD + "/res/" + postID; - } else { - QR.cooldown.auto = QR.replies.length > 1; - if (Conf['Open Reply in New Tab'] && !g.REPLY && !QR.cooldown.auto) { - $.open("//boards.4chan.org/" + g.BOARD + "/res/" + threadID + "#p" + postID); - } + $.open("/" + g.BOARD + "/res/" + postID); + } else if (g.VIEW === 'reply' && !QR.cooldown.auto) { + $.open("//boards.4chan.org/" + g.BOARD + "/res/" + threadID + "#p" + postID); } if (Conf['Persistent QR'] || QR.cooldown.auto) { reply.rm(); @@ -3346,7 +3347,7 @@ container = $.el('div', { id: "pc" + postID, className: "postContainer " + (isOP ? 'op' : 'reply') + "Container", - innerHTML: (isOP ? '' : "
>>
") + ("
") + ("' + (isOP ? fileHTML : '') + ("' + (isOP ? '' : fileHTML) + ("
" + (comment || '') + "
") + '
' + innerHTML: (isOP ? '' : "
>>
") + ("
") + ("' + (isOP ? fileHTML : '') + ("' + (isOP ? '' : fileHTML) + ("
" + (comment || '') + "
") + '
' }); _ref = $$('.quotelink', container); for (_i = 0, _len = _ref.length; _i < _len; _i++) { @@ -4880,14 +4881,17 @@ } Post.prototype.kill = function(img) { - var quotelink, _i, _len, _ref; + var now, quotelink, _i, _len, _ref; + now = Date.now(); if (this.file && !this.file.isDead) { this.file.isDead = true; + this.file.timeOfDeath = now; } if (img) { return; } this.isDead = true; + this.timeOfDeath = now; $.addClass(this.nodes.root, 'dead'); _ref = Get.allQuotelinksLinkingTo(this); for (_i = 0, _len = _ref.length; _i < _len; _i++) { @@ -5112,15 +5116,15 @@ if ((_ref = $('link[href*=mobile]', d.head)) != null) { _ref.disabled = true; } + $.addClass(doc, $.engine); + $.addClass(doc, 'fourchan-x'); $.addStyle(Main.css); - style = null; + style = 'yotsuba-b'; mainStyleSheet = $('link[title=switch]', d.head); styleSheets = $$('link[rel="alternate stylesheet"]', d.head); setStyle = function() { var styleSheet, _i, _len; - if (style) { - $.rmClass(doc, style); - } + $.rmClass(doc, style); for (_i = 0, _len = styleSheets.length; _i < _len; _i++) { styleSheet = styleSheets[_i]; if (styleSheet.href === mainStyleSheet.href) { @@ -5130,9 +5134,10 @@ } return $.addClass(doc, style); }; - $.addClass(doc, $.engine); - $.addClass(doc, 'fourchan-x'); setStyle(); + if (!mainStyleSheet) { + return; + } if (MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.OMutationObserver) { observer = new MutationObserver(setStyle); return observer.observe(mainStyleSheet, { @@ -5144,7 +5149,7 @@ } }, initReady: function() { - var boardChild, errors, posts, thread, threadChild, threads, _i, _j, _len, _len1, _ref, _ref1; + var board, boardChild, errors, posts, thread, threadChild, threads, _i, _j, _len, _len1, _ref, _ref1; if (!$.hasClass(doc, 'fourchan-x')) { Main.initStyle(); } @@ -5158,40 +5163,43 @@ } return; } - threads = []; - posts = []; - _ref = $('.board').children; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - boardChild = _ref[_i]; - if (!$.hasClass(boardChild, 'thread')) { - continue; - } - thread = new Thread(boardChild.id.slice(1), g.BOARD); - threads.push(thread); - _ref1 = boardChild.children; - for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { - threadChild = _ref1[_j]; - if (!$.hasClass(threadChild, 'postContainer')) { + if (board = $('.board')) { + threads = []; + posts = []; + _ref = board.children; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + boardChild = _ref[_i]; + if (!$.hasClass(boardChild, 'thread')) { continue; } - try { - posts.push(new Post(threadChild, thread, g.BOARD)); - } catch (err) { - if (!errors) { - errors = []; + thread = new Thread(boardChild.id.slice(1), g.BOARD); + threads.push(thread); + _ref1 = boardChild.children; + for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { + threadChild = _ref1[_j]; + if (!$.hasClass(threadChild, 'postContainer')) { + continue; + } + try { + posts.push(new Post(threadChild, thread, g.BOARD)); + } catch (err) { + if (!errors) { + errors = []; + } + errors.push({ + message: "Parsing of Post No." + (threadChild.id.match(/\d+/)) + " failed. Post will be skipped.", + error: err + }); } - errors.push({ - message: "Parsing of Post No." + (threadChild.id.match(/\d+/)) + " failed. Post will be skipped.", - error: err - }); } } + if (errors) { + Main.handleErrors(errors); + } + Main.callbackNodes(Thread, threads); + Main.callbackNodes(Post, posts); } - if (errors) { - Main.handleErrors(errors); - } - Main.callbackNodes(Thread, threads); - return Main.callbackNodes(Post, posts); + return $.event('4chanXInitFinished'); }, callbackNodes: function(klass, nodes) { var callback, errors, i, len, node, _i, _j, _len, _ref; @@ -5262,7 +5270,7 @@ }); return [message, error]; }, - css: "/* general */\n.dialog {\nbox-shadow: 0 1px 2px rgba(0, 0, 0, .15);\nborder: 1px solid;\ndisplay: block;\npadding: 0;\n}\n.move {\ncursor: move;\n}\nlabel {\ncursor: pointer;\n}\na[href=\"javascript:;\"] {\ntext-decoration: none;\n}\n.warning {\ncolor: red;\n}\n\n/* 4chan style fixes */\n.opContainer, .op {\ndisplay: block !important;\n}\n.post {\noverflow: visible !important;\n}\n[hidden] {\ndisplay: none !important;\n}\n\n/* fixed, z-index */\n#qp, #ihover,\n#updater, #stats,\n#header,\n#qr, #watcher {\nposition: fixed;\n}\n#notifications {\nz-index: 80;\n}\n#qp, #ihover {\nz-index: 70;\n}\n#menu {\nz-index: 60;\n}\n#updater, #stats {\nz-index: 50;\n}\n#header:hover {\nz-index: 40;\n}\n#qr {\nz-index: 30;\n}\n#header {\nz-index: 20;\n}\n#watcher {\nz-index: 10;\n}\n\n/* Header */\n.fourchan-x body {\nmargin-top: 2em;\n}\n.fourchan-x #boardNavDesktop,\n.fourchan-x #navtopright,\n.fourchan-x #boardNavDesktopFoot {\ndisplay: none !important;\n}\n#header {\ntop: 0;\nright: 0;\nleft: 0;\n}\n#header-bar {\nborder-width: 0 0 1px;\npadding: 4px;\nposition: relative;\ntransition: all .1s ease-in-out;\n-o-transition: all .1s ease-in-out;\n-moz-transition: all .1s ease-in-out;\n-webkit-transition: all .1s ease-in-out;\n}\n#header-bar.autohide:not(:hover) {\nmargin-bottom: -1em;\ntransform: translateY(-100%);\n-o-transform: translateY(-100%);\n-moz-transform: translateY(-100%);\n-webkit-transform: translateY(-100%);\ntransition: all .75s .25s ease-in-out;\n-o-transition: all .75s .25s ease-in-out;\n-moz-transition: all .75s .25s ease-in-out;\n-webkit-transition: all .75s .25s ease-in-out;\n}\n#toggle-header-bar {\ncursor: n-resize;\nleft: 0;\nright: 0;\nbottom: -8px;\nheight: 10px;\nposition: absolute;\n}\n#header-bar.autohide #toggle-header-bar {\ncursor: s-resize;\n}\n#header-bar a {\ntext-decoration: none;\npadding: 1px;\n}\n#header-bar > .menu-button {\nfloat: right;\npadding: 0;\n}\n\n/* notifications */\n#notifications {\ntext-align: center;\n}\n.notification {\ncolor: #FFF;\nfont-weight: 700;\ntext-shadow: 0 1px 2px rgba(0, 0, 0, .5);\nbox-shadow: 0 1px 2px rgba(0, 0, 0, .15);\nborder-radius: 2px;\nmargin: 1px auto;\nwidth: 500px;\nmax-width: 100%;\nposition: relative;\ntransition: all .25s ease-in-out;\n-o-transition: all .25s ease-in-out;\n-moz-transition: all .25s ease-in-out;\n-webkit-transition: all .25s ease-in-out;\n}\n.notification.error {\nbackground-color: hsla(0, 100%, 40%, .9);\n}\n.notification.warning {\nbackground-color: hsla(36, 100%, 40%, .9);\n}\n.notification.info {\nbackground-color: hsla(200, 100%, 40%, .9);\n}\n.notification.success {\nbackground-color: hsla(104, 100%, 40%, .9);\n}\n.notification > .close {\ncolor: white;\npadding: 4px 6px;\ntop: 0;\nright: 0;\nposition: absolute;\n}\n.message {\nbox-sizing: border-box;\npadding: 4px 20px;\nmax-height: 200px;\nwidth: 100%;\noverflow: auto;\n}\n\n/* thread updater */\n#updater {\ntext-align: right;\n}\n#updater:not(:hover) {\nbackground: none;\nborder: none;\n}\n#updater input[type=number] {\nwidth: 4em;\n}\n#updater:not(:hover) > div:not(.move) {\ndisplay: none;\n}\n.new {\ncolor: limegreen;\n}\n\n/* quote */\n.quotelink.deadlink {\ntext-decoration: underline !important;\n}\n.deadlink:not(.quotelink) {\ntext-decoration: none !important;\n}\n.inlined {\nopacity: .5;\n}\n#qp input, .forwarded {\ndisplay: none;\n}\n.quotelink.forwardlink,\n.backlink.forwardlink {\ntext-decoration: none;\nborder-bottom: 1px dashed;\n}\n.filtered {\ntext-decoration: underline line-through;\n}\n.inline {\nborder: 1px solid;\ndisplay: table;\nmargin: 2px 0;\n}\n.inline .post {\nborder: 0 !important;\nbackground-color: transparent !important;\ndisplay: table !important;\nmargin: 0 !important;\npadding: 1px 2px !important;\n}\n#qp {\npadding: 2px 2px 5px;\n}\n#qp .post {\nborder: none;\nmargin: 0;\npadding: 0;\n}\n#qp img {\nmax-height: 300px;\nmax-width: 500px;\n}\n.qphl {\nbox-shadow: 0 0 0 2px rgba(216, 94, 49, .7);\n}\n\n/* file */\n.fileText:hover .fntrunc,\n.fileText:not(:hover) .fnfull {\ndisplay: none;\n}\n#ihover {\nbox-sizing: border-box;\n-moz-box-sizing: border-box;\nmax-height: 100%;\nmax-width: 75%;\npadding-bottom: 16px;\n}\n\n/* Filter */\n.opContainer.filter-highlight {\nbox-shadow: inset 5px 0 rgba(255, 0, 0, .5);\n}\n.opContainer.filter-highlight.qphl {\nbox-shadow: inset 5px 0 rgba(255, 0, 0, .5),\n 0 0 0 2px rgba(216, 94, 49, .7);\n}\n.filter-highlight > .reply {\nbox-shadow: -5px 0 rgba(255, 0, 0, .5);\n}\n.filter-highlight > .reply.qphl {\nbox-shadow: -5px 0 rgba(255, 0, 0, .5),\n 0 0 0 2px rgba(216, 94, 49, .7);\n}\n\n/* Thread & Reply Hiding */\n.hide-thread-button,\n.hide-reply-button {\nfloat: left;\nmargin-right: 2px;\n}\n.stub ~ .sideArrows,\n.stub ~ .hide-reply-button,\n.stub ~ .post {\ndisplay: none !important;\n}\n\n/* QR */\n#qr > .move {\nmin-width: 300px;\noverflow: hidden;\nbox-sizing: border-box;\n-moz-box-sizing: border-box;\npadding: 0 2px;\n}\n#qr > .move > span {\nfloat: right;\n}\n#autohide, .close, #qr select, #dump, .remove, .captchaimg, #qr div.warning {\ncursor: pointer;\n}\n#qr select,\n#qr > form {\nmargin: 0;\n}\n#dump {\nbackground: -webkit-linear-gradient(#EEE, #CCC);\nbackground: -moz-linear-gradient(#EEE, #CCC);\nbackground: -o-linear-gradient(#EEE, #CCC);\nbackground: linear-gradient(#EEE, #CCC);\nwidth: 10%;\n}\n.gecko #dump {\npadding: 1px 0 2px;\n}\n#dump:hover, #dump:focus {\nbackground: -webkit-linear-gradient(#FFF, #DDD);\nbackground: -moz-linear-gradient(#FFF, #DDD);\nbackground: -o-linear-gradient(#FFF, #DDD);\nbackground: linear-gradient(#FFF, #DDD);\n}\n#dump:active, .dump #dump:not(:hover):not(:focus) {\nbackground: -webkit-linear-gradient(#CCC, #DDD);\nbackground: -moz-linear-gradient(#CCC, #DDD);\nbackground: -o-linear-gradient(#CCC, #DDD);\nbackground: linear-gradient(#CCC, #DDD);\n}\n#qr:not(.dump) #replies, .dump > form > label {\ndisplay: none;\n}\n#replies {\ndisplay: block;\nheight: 100px;\nposition: relative;\n-webkit-user-select: none;\n-moz-user-select: none;\n-o-user-select: none;\nuser-select: none;\n}\n#replies > div {\ncounter-reset: thumbnails;\ntop: 0; right: 0; bottom: 0; left: 0;\nmargin: 0; padding: 0;\noverflow: hidden;\nposition: absolute;\nwhite-space: pre;\n}\n#replies > div:hover {\nbottom: -10px;\noverflow-x: auto;\nz-index: 1;\n}\n.thumbnail {\nbackground-color: rgba(0,0,0,.2) !important;\nbackground-position: 50% 20% !important;\nbackground-size: cover !important;\nborder: 1px solid #666;\nbox-sizing: border-box;\n-moz-box-sizing: border-box;\ncursor: move;\ndisplay: inline-block;\nheight: 90px; width: 90px;\nmargin: 5px; padding: 2px;\nopacity: .5;\noutline: none;\noverflow: hidden;\nposition: relative;\ntext-shadow: 0 1px 1px #000;\n-webkit-transition: opacity .25s ease-in-out;\n-moz-transition: opacity .25s ease-in-out;\n-o-transition: opacity .25s ease-in-out;\ntransition: opacity .25s ease-in-out;\nvertical-align: top;\n}\n.thumbnail:hover, .thumbnail:focus {\nopacity: .9;\n}\n.thumbnail#selected {\nopacity: 1;\n}\n.thumbnail::before {\ncounter-increment: thumbnails;\ncontent: counter(thumbnails);\ncolor: #FFF;\nfont-weight: 700;\npadding: 3px;\nposition: absolute;\ntop: 0;\nright: 0;\ntext-shadow: 0 0 3px #000, 0 0 8px #000;\n}\n.thumbnail.drag {\nbox-shadow: 0 0 10px rgba(0,0,0,.5);\n}\n.thumbnail.over {\nborder-color: #FFF;\n}\n.thumbnail > span {\ncolor: #FFF;\n}\n.remove {\nbackground: none;\ncolor: #E00;\nfont-weight: 700;\npadding: 3px;\n}\n.remove:hover::after {\ncontent: \" Remove\";\n}\n.thumbnail > label {\nbackground: rgba(0,0,0,.5);\ncolor: #FFF;\nright: 0; bottom: 0; left: 0;\nposition: absolute;\ntext-align: center;\n}\n.thumbnail > label > input {\nmargin: 0;\n}\n#addReply {\ncolor: #333;\nfont-size: 3.5em;\nline-height: 100px;\n}\n#addReply:hover, #addReply:focus {\ncolor: #000;\n}\n.field {\nborder: 1px solid #CCC;\nbox-sizing: border-box;\n-moz-box-sizing: border-box;\ncolor: #333;\nfont: 13px sans-serif;\nmargin: 0;\npadding: 2px 4px 3px;\n-webkit-transition: color .25s, border .25s;\n-moz-transition: color .25s, border .25s;\n-o-transition: color .25s, border .25s;\ntransition: color .25s, border .25s;\n}\n.field:-moz-placeholder,\n.field:hover:-moz-placeholder {\ncolor: #AAA;\n}\n.field:hover, .field:focus {\nborder-color: #999;\ncolor: #000;\noutline: none;\n}\n#qr > form > div:first-child > .field:not(#dump) {\nwidth: 30%;\n}\n#qr textarea.field {\ndisplay: -webkit-box;\nmin-height: 160px;\nmin-width: 100%;\n}\n#qr.captcha textarea.field {\nmin-height: 120px;\n}\n.textarea {\nposition: relative;\n}\n#charCount {\ncolor: #000;\nbackground: hsla(0, 0%, 100%, .5);\nfont-size: 8pt;\nmargin: 1px;\nposition: absolute;\nbottom: 0;\nright: 0;\npointer-events: none;\n}\n#charCount.warning {\ncolor: red;\n}\n.captchainput > .field {\nmin-width: 100%;\n}\n.captchaimg {\nbackground: #FFF;\noutline: 1px solid #CCC;\noutline-offset: -1px;\ntext-align: center;\n}\n.captchaimg > img {\ndisplay: block;\nheight: 57px;\nwidth: 300px;\n}\n#qr [type=file] {\nmargin: 1px 0;\nwidth: 70%;\n}\n#qr [type=submit] {\nmargin: 1px 0;\npadding: 1px; /* not Gecko */\nwidth: 30%;\n}\n.gecko #qr [type=submit] {\npadding: 0 1px; /* Gecko does not respect box-sizing: border-box */\n}\n\n/* Menu */\n.menu-button {\ndisplay: inline-block;\n}\n.menu-button > span {\nborder-top: 6px solid;\nborder-right: 4px solid transparent;\nborder-left: 4px solid transparent;\ndisplay: inline-block;\nmargin: 2px;\nvertical-align: middle;\n}\n#menu {\nposition: absolute;\noutline: none;\n}\n.entry {\ncursor: pointer;\ndisplay: block;\noutline: none;\npadding: 3px 7px;\nposition: relative;\ntext-decoration: none;\nwhite-space: nowrap;\n}\n.entry.has-submenu {\npadding-right: 20px;\n}\n.has-submenu::after {\ncontent: \"\";\nborder-left: 6px solid;\nborder-top: 4px solid transparent;\nborder-bottom: 4px solid transparent;\ndisplay: inline-block;\nmargin: 4px;\nposition: absolute;\nright: 3px;\n}\n.has-submenu:not(.focused) > .submenu {\ndisplay: none;\n}\n.submenu {\nposition: absolute;\nmargin: -1px 0;\n}\n.entry input {\nmargin: 0;\n}\n\n/* general */\n:root.yotsuba .dialog {\nbackground-color: #F0E0D6;\nborder-color: #D9BFB7;\n}\n\n/* Header */\n:root.yotsuba #header-bar {\nfont-size: 9pt;\ncolor: #B86;\n}\n:root.yotsuba #header-bar a {\ncolor: #800000;\n}\n\n/* quote */\n:root.yotsuba .inline {\nborder-color: #D9BFB7;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* Menu */\n:root.yotsuba .entry:not(:last-child) {\nborder-bottom: 1px solid #D9BFB7;\n}\n:root.yotsuba .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n\n/* general */\n:root.yotsuba-b .dialog {\nbackground-color: #D6DAF0;\nborder-color: #B7C5D9;\n}\n\n/* Header */\n:root.yotsuba-b #header-bar {\nfont-size: 9pt;\ncolor: #89A;\n}\n:root.yotsuba-b #header-bar a {\ncolor: #34345C;\n}\n\n/* quote */\n:root.yotsuba-b .inline {\nborder-color: #B7C5D9;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* Menu */\n:root.yotsuba-b .entry:not(:last-child) {\nborder-bottom: 1px solid #B7C5D9;\n}\n:root.yotsuba-b .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n\n/* general */\n:root.futaba .dialog {\nbackground-color: #F0E0D6;\nborder-color: #D9BFB7;\n}\n\n/* Header */\n:root.futaba #header-bar {\nfont-size: 11pt;\ncolor: #B86;\n}\n:root.futaba #header-bar a {\ncolor: #800000;\n}\n\n/* quote */\n:root.futaba .inline {\nborder-color: #D9BFB7;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* Menu */\n:root.futaba .entry:not(:last-child) {\nborder-bottom: 1px solid #D9BFB7;\n}\n:root.futaba .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n\n/* general */\n:root.burichan .dialog {\nbackground-color: #D6DAF0;\nborder-color: #B7C5D9;\n}\n\n/* Header */\n:root.burichan #header-bar {\nfont-size: 11pt;\ncolor: #89A;\n}\n:root.burichan #header-bar a {\ncolor: #34345C;\n}\n\n/* quote */\n:root.burichan .inline {\nborder-color: #B7C5D9;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* Menu */\n:root.burichan .entry:not(:last-child) {\nborder-bottom: 1px solid #B7C5D9;\n}\n:root.burichan .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n\n/* general */\n:root.tomorrow .dialog {\nbackground-color: #282A2E;\nborder-color: #111;\n}\n\n/* Header */\n:root.tomorrow #header-bar {\nfont-size: 9pt;\ncolor: #C5C8C6;\n}\n:root.tomorrow #header-bar a {\ncolor: #81A2BE;\n}\n\n/* quote */\n:root.tomorrow .inline {\nborder-color: #111;\nbackground-color: rgba(0, 0, 0, .14);\n}\n\n/* Menu */\n:root.tomorrow .entry:not(:last-child) {\nborder-bottom: 1px solid #111;\n}\n:root.tomorrow .focused.entry {\nbackground: rgba(0, 0, 0, .33);\n}\n\n/* general */\n:root.photon .dialog {\nbackground-color: #DDD;\nborder-color: #CCC;\n}\n\n/* Header */\n:root.photon #header-bar {\nfont-size: 9pt;\ncolor: #333;\n}\n:root.photon #header-bar a {\ncolor: #FF6600;\n}\n\n/* quote */\n:root.photon .inline {\nborder-color: #CCC;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* Menu */\n:root.photon .entry:not(:last-child) {\nborder-bottom: 1px solid #CCC;\n}\n:root.photon .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n" + css: "/* general */\n.dialog {\nbox-shadow: 0 1px 2px rgba(0, 0, 0, .15);\nborder: 1px solid;\ndisplay: block;\npadding: 0;\n}\n.field {\nborder: 1px solid #CCC;\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\ncolor: #333;\nfont: 13px sans-serif;\nmargin: 0;\npadding: 2px 4px 3px;\noutline: none;\n-webkit-transition: color .25s, border-color .25s;\ntransition: color .25s, border-color .25s;\n}\n.field:-moz-placeholder,\n.field:hover:-moz-placeholder {\ncolor: #AAA !important;\n}\n.field:hover {\nborder-color: #999;\n}\n.field:hover, .field:focus {\ncolor: #000;\n}\n.move {\ncursor: move;\n}\nlabel {\ncursor: pointer;\n}\na[href=\"javascript:;\"] {\ntext-decoration: none;\n}\n.warning {\ncolor: red;\n}\n\n/* 4chan style fixes */\n.opContainer, .op {\ndisplay: block !important;\n}\n.post {\noverflow: visible !important;\n}\n[hidden] {\ndisplay: none !important;\n}\n\n/* fixed, z-index */\n#qp, #ihover,\n#updater, #stats,\n#header,\n#qr, #watcher {\nposition: fixed;\n}\n#notifications {\nz-index: 80;\n}\n#qp, #ihover {\nz-index: 70;\n}\n#menu {\nz-index: 60;\n}\n#updater, #stats {\nz-index: 50;\n}\n#header:hover {\nz-index: 40;\n}\n#qr {\nz-index: 30;\n}\n#header {\nz-index: 20;\n}\n#watcher {\nz-index: 10;\n}\n\n/* Header */\n.fourchan-x body {\nmargin-top: 2em;\n}\n.fourchan-x #boardNavDesktop,\n.fourchan-x #navtopright,\n.fourchan-x #boardNavDesktopFoot {\ndisplay: none !important;\n}\n#header {\ntop: 0;\nright: 0;\nleft: 0;\n}\n#header-bar {\nborder-width: 0 0 1px;\npadding: 4px;\nposition: relative;\n-webkit-transition: all .1s ease-in-out;\ntransition: all .1s ease-in-out;\n}\n#header-bar.autohide:not(:hover) {\nmargin-bottom: -1em;\n-webkit-transform: translateY(-100%);\ntransform: translateY(-100%);\n-webkit-transition: all .75s .25s ease-in-out;\ntransition: all .75s .25s ease-in-out;\n}\n#toggle-header-bar {\ncursor: n-resize;\nleft: 0;\nright: 0;\nbottom: -8px;\nheight: 10px;\nposition: absolute;\n}\n#header-bar.autohide #toggle-header-bar {\ncursor: s-resize;\n}\n#header-bar a {\ntext-decoration: none;\npadding: 1px;\n}\n#header-bar > .menu-button {\nfloat: right;\npadding: 0;\n}\n\n/* notifications */\n#notifications {\ntext-align: center;\n}\n.notification {\ncolor: #FFF;\nfont-weight: 700;\ntext-shadow: 0 1px 2px rgba(0, 0, 0, .5);\nbox-shadow: 0 1px 2px rgba(0, 0, 0, .15);\nborder-radius: 2px;\nmargin: 1px auto;\nwidth: 500px;\nmax-width: 100%;\nposition: relative;\n-webkit-transition: all .25s ease-in-out;\ntransition: all .25s ease-in-out;\n}\n.notification.error {\nbackground-color: hsla(0, 100%, 40%, .9);\n}\n.notification.warning {\nbackground-color: hsla(36, 100%, 40%, .9);\n}\n.notification.info {\nbackground-color: hsla(200, 100%, 40%, .9);\n}\n.notification.success {\nbackground-color: hsla(104, 100%, 40%, .9);\n}\n.notification > .close {\ncolor: white;\npadding: 4px 6px;\ntop: 0;\nright: 0;\nposition: absolute;\n}\n.message {\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\npadding: 4px 20px;\nmax-height: 200px;\nwidth: 100%;\noverflow: auto;\n}\n\n/* thread updater */\n#updater {\ntext-align: right;\n}\n#updater:not(:hover) {\nbackground: none;\nborder: none;\n}\n#updater input[type=number] {\nwidth: 4em;\n}\n#updater:not(:hover) > div:not(.move) {\ndisplay: none;\n}\n.new {\ncolor: limegreen;\n}\n\n/* quote */\n.quotelink.deadlink {\ntext-decoration: underline !important;\n}\n.deadlink:not(.quotelink) {\ntext-decoration: none !important;\n}\n.inlined {\nopacity: .5;\n}\n#qp input, .forwarded {\ndisplay: none;\n}\n.quotelink.forwardlink,\n.backlink.forwardlink {\ntext-decoration: none;\nborder-bottom: 1px dashed;\n}\n.filtered {\ntext-decoration: underline line-through;\n}\n.inline {\nborder: 1px solid;\ndisplay: table;\nmargin: 2px 0;\n}\n.inline .post {\nborder: 0 !important;\nbackground-color: transparent !important;\ndisplay: table !important;\nmargin: 0 !important;\npadding: 1px 2px !important;\n}\n#qp {\npadding: 2px 2px 5px;\n}\n#qp .post {\nborder: none;\nmargin: 0;\npadding: 0;\n}\n#qp img {\nmax-height: 300px;\nmax-width: 500px;\n}\n.qphl {\nbox-shadow: 0 0 0 2px rgba(216, 94, 49, .7);\n}\n\n/* file */\n.fileText:hover .fntrunc,\n.fileText:not(:hover) .fnfull {\ndisplay: none;\n}\n#ihover {\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\nmax-height: 100%;\nmax-width: 75%;\npadding-bottom: 16px;\n}\n\n/* Filter */\n.opContainer.filter-highlight {\nbox-shadow: inset 5px 0 rgba(255, 0, 0, .5);\n}\n.opContainer.filter-highlight.qphl {\nbox-shadow: inset 5px 0 rgba(255, 0, 0, .5),\n 0 0 0 2px rgba(216, 94, 49, .7);\n}\n.filter-highlight > .reply {\nbox-shadow: -5px 0 rgba(255, 0, 0, .5);\n}\n.filter-highlight > .reply.qphl {\nbox-shadow: -5px 0 rgba(255, 0, 0, .5),\n 0 0 0 2px rgba(216, 94, 49, .7);\n}\n\n/* Thread & Reply Hiding */\n.hide-thread-button,\n.hide-reply-button {\nfloat: left;\nmargin-right: 2px;\n}\n.stub ~ .sideArrows,\n.stub ~ .hide-reply-button,\n.stub ~ .post {\ndisplay: none !important;\n}\n\n/* QR */\n.hide-original-post-form #postForm,\n.hide-original-post-form .postingMode {\ndisplay: none;\n}\n#qr > .move {\nmin-width: 300px;\noverflow: hidden;\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\npadding: 0 2px;\n}\n#qr > .move > span {\nfloat: right;\n}\n#autohide, .close, #qr select, #dump, .remove, .captchaimg, #qr div.warning {\ncursor: pointer;\n}\n#qr select {\nmargin: 0;\n}\n#dump {\nbackground: -webkit-linear-gradient(#EEE, #CCC);\nbackground: linear-gradient(#EEE, #CCC);\nborder: 1px solid #CCC;\nmargin: 0;\npadding: 2px 4px 3px;\noutline: none;\nwidth: 10%;\n}\n.gecko #dump {\npadding: 1px 0 2px;\n}\n#dump:hover, #dump:focus {\nbackground: -webkit-linear-gradient(#FFF, #DDD);\nbackground: linear-gradient(#FFF, #DDD);\n}\n#dump:active, .dump #dump:not(:hover):not(:focus) {\nbackground: -webkit-linear-gradient(#CCC, #DDD);\nbackground: linear-gradient(#CCC, #DDD);\n}\n#qr:not(.dump) #replies, .dump > form > label {\ndisplay: none;\n}\n#replies {\ndisplay: block;\nheight: 100px;\nposition: relative;\n-webkit-user-select: none;\n-moz-user-select: none;\n-o-user-select: none;\nuser-select: none;\n}\n#replies > div {\ncounter-reset: qrpreviews;\ntop: 0; right: 0; bottom: 0; left: 0;\nmargin: 0; padding: 0;\noverflow: hidden;\nposition: absolute;\nwhite-space: pre;\n}\n#replies > div:hover {\nbottom: -10px;\noverflow-x: auto;\nz-index: 1;\n}\n.qrpreview {\nbackground-position: 50% 20%;\nbackground-size: cover;\nborder: 1px solid #808080;\ncolor: #FFF !important;\nfont-size: 12px;\n-moz-box-sizing: border-box;\nbox-sizing: border-box;\ncursor: move;\ndisplay: inline-block;\nheight: 90px; width: 90px;\nmargin: 5px; padding: 2px;\nopacity: .6;\noutline: none;\noverflow: hidden;\nposition: relative;\ntext-shadow: 0 1px 1px #000;\n-webkit-transition: opacity .25s ease-in-out;\ntransition: opacity .25s ease-in-out;\nvertical-align: top;\n}\n.qrpreview:hover, .qrpreview:focus {\nopacity: .9;\ncolor: #FFF !important;\n}\n.qrpreview#selected {\nopacity: 1;\n}\n.qrpreview::before {\ncounter-increment: qrpreviews;\ncontent: counter(qrpreviews);\nfont-weight: 700;\ntext-shadow: 0 0 3px #000, 0 0 5px #000;\nposition: absolute;\ntop: 3px; right: 3px;\n}\n.qrpreview.drag {\nborder-color: red;\nborder-style: dashed;\n}\n.qrpreview.over {\nborder-color: #FFF;\nborder-style: dashed;\n}\n.remove {\ncolor: #E00 !important;\nfont-weight: 700;\npadding: 3px;\n}\n.remove:hover::after {\ncontent: ' Remove';\n}\n.qrpreview > label {\nbackground: rgba(0, 0, 0, .5);\nright: 0; bottom: 0; left: 0;\nposition: absolute;\ntext-align: center;\n}\n.qrpreview > label > input {\nmargin: 1px 0;\nvertical-align: bottom;\n}\n#addReply {\nfont-size: 3.5em;\nline-height: 100px;\n}\n#qr > form > div:first-child > .field:not(#dump) {\nwidth: 30%;\n}\n#qr textarea.field {\ndisplay: -webkit-box;\nmin-height: 160px;\nmin-width: 100%;\n}\n#qr.captcha textarea.field {\nmin-height: 120px;\n}\n.textarea {\nposition: relative;\n}\n#charCount {\ncolor: #000;\nbackground: hsla(0, 0%, 100%, .5);\nfont-size: 8pt;\nmargin: 1px;\nposition: absolute;\nbottom: 0;\nright: 0;\npointer-events: none;\n}\n#charCount.warning {\ncolor: red;\n}\n.captchainput > .field {\nmin-width: 100%;\n}\n.captchaimg {\nbackground: #FFF;\noutline: 1px solid #CCC;\noutline-offset: -1px;\ntext-align: center;\n}\n.captchaimg > img {\ndisplay: block;\nheight: 57px;\nwidth: 300px;\n}\n#qr [type=file] {\nmargin: 1px 0;\nwidth: 70%;\n}\n#qr [type=submit] {\nmargin: 1px 0;\npadding: 1px; /* not Gecko */\nwidth: 30%;\n}\n.gecko #qr [type=submit] {\npadding: 0 1px; /* Gecko does not respect box-sizing: border-box */\n}\n\n/* Menu */\n.menu-button {\ndisplay: inline-block;\n}\n.menu-button > span {\nborder-top: 6px solid;\nborder-right: 4px solid transparent;\nborder-left: 4px solid transparent;\ndisplay: inline-block;\nmargin: 2px;\nvertical-align: middle;\n}\n#menu {\nposition: absolute;\noutline: none;\n}\n.entry {\ncursor: pointer;\ndisplay: block;\noutline: none;\npadding: 3px 7px;\nposition: relative;\ntext-decoration: none;\nwhite-space: nowrap;\n}\n.entry.has-submenu {\npadding-right: 20px;\n}\n.has-submenu::after {\ncontent: \"\";\nborder-left: 6px solid;\nborder-top: 4px solid transparent;\nborder-bottom: 4px solid transparent;\ndisplay: inline-block;\nmargin: 4px;\nposition: absolute;\nright: 3px;\n}\n.has-submenu:not(.focused) > .submenu {\ndisplay: none;\n}\n.submenu {\nposition: absolute;\nmargin: -1px 0;\n}\n.entry input {\nmargin: 0;\n}\n\n/* general */\n:root.yotsuba .dialog {\nbackground-color: #F0E0D6;\nborder-color: #D9BFB7;\n}\n:root.yotsuba .field:focus {\nborder-color: #EA8;\n}\n\n/* Header */\n:root.yotsuba #header-bar {\nfont-size: 9pt;\ncolor: #B86;\n}\n:root.yotsuba #header-bar a {\ncolor: #800000;\n}\n\n/* quote */\n:root.yotsuba .inline {\nborder-color: #D9BFB7;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n:root.yotsuba .qrpreview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.yotsuba .entry:not(:last-child) {\nborder-bottom: 1px solid #D9BFB7;\n}\n:root.yotsuba .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n\n/* general */\n:root.yotsuba-b .dialog {\nbackground-color: #D6DAF0;\nborder-color: #B7C5D9;\n}\n:root.yotsuba-b .field:focus {\nborder-color: #98E;\n}\n\n/* Header */\n:root.yotsuba-b #header-bar {\nfont-size: 9pt;\ncolor: #89A;\n}\n:root.yotsuba-b #header-bar a {\ncolor: #34345C;\n}\n\n/* quote */\n:root.yotsuba-b .inline {\nborder-color: #B7C5D9;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n:root.yotsuba-b .qrpreview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.yotsuba-b .entry:not(:last-child) {\nborder-bottom: 1px solid #B7C5D9;\n}\n:root.yotsuba-b .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n\n/* general */\n:root.futaba .dialog {\nbackground-color: #F0E0D6;\nborder-color: #D9BFB7;\n}\n:root.futaba .field:focus {\nborder-color: #EA8;\n}\n\n/* Header */\n:root.futaba #header-bar {\nfont-size: 11pt;\ncolor: #B86;\n}\n:root.futaba #header-bar a {\ncolor: #800000;\n}\n\n/* quote */\n:root.futaba .inline {\nborder-color: #D9BFB7;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n:root.futaba .qrpreview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.futaba .entry:not(:last-child) {\nborder-bottom: 1px solid #D9BFB7;\n}\n:root.futaba .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n\n/* general */\n:root.burichan .dialog {\nbackground-color: #D6DAF0;\nborder-color: #B7C5D9;\n}\n:root.burichan .field:focus {\nborder-color: #98E;\n}\n\n/* Header */\n:root.burichan #header-bar {\nfont-size: 11pt;\ncolor: #89A;\n}\n:root.burichan #header-bar a {\ncolor: #34345C;\n}\n\n/* quote */\n:root.burichan .inline {\nborder-color: #B7C5D9;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n:root.burichan .qrpreview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.burichan .entry:not(:last-child) {\nborder-bottom: 1px solid #B7C5D9;\n}\n:root.burichan .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n\n/* general */\n:root.tomorrow .dialog {\nbackground-color: #282A2E;\nborder-color: #111;\n}\n:root.tomorrow .field:focus {\nborder-color: #000;\n}\n\n/* Header */\n:root.tomorrow #header-bar {\nfont-size: 9pt;\ncolor: #C5C8C6;\n}\n:root.tomorrow #header-bar a {\ncolor: #81A2BE;\n}\n\n/* quote */\n:root.tomorrow .inline {\nborder-color: #111;\nbackground-color: rgba(0, 0, 0, .14);\n}\n\n/* QR */\n:root.tomorrow .qrpreview {\nbackground-color: rgba(255, 255, 255, .15);\n}\n\n/* Menu */\n:root.tomorrow .entry:not(:last-child) {\nborder-bottom: 1px solid #111;\n}\n:root.tomorrow .focused.entry {\nbackground: rgba(0, 0, 0, .33);\n}\n\n/* general */\n:root.photon .dialog {\nbackground-color: #DDD;\nborder-color: #CCC;\n}\n:root.photon .field:focus {\nborder-color: #EA8;\n}\n\n/* Header */\n:root.photon #header-bar {\nfont-size: 9pt;\ncolor: #333;\n}\n:root.photon #header-bar a {\ncolor: #FF6600;\n}\n\n/* quote */\n:root.photon .inline {\nborder-color: #CCC;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* QR */\n:root.photon .qrpreview {\nbackground-color: rgba(0, 0, 0, .15);\n}\n\n/* Menu */\n:root.photon .entry:not(:last-child) {\nborder-bottom: 1px solid #CCC;\n}\n:root.photon .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n" }; Main.init(); diff --git a/changelog b/changelog index ccb25c433..ee00e0d90 100644 --- a/changelog +++ b/changelog @@ -5,8 +5,11 @@ alpha Access the list of boards directly from the Header. From the Header's menu, access to: Settings - Quick Reply + Quick Reply shortcut Can be auto-hidden. + QR changes: + Creating threads outside of the index is now possible. + Selection-to-quote also applies to selected text inside the post, not just inside the comment. Added touch and multi-touch support for dragging windows. The Thread Updater will pause when offline, and resume when online. Added Thread & Post Hiding in the Menu, with individual settings. diff --git a/css/burichan.css b/css/burichan.css index 33ad10d4a..48fafa818 100644 --- a/css/burichan.css +++ b/css/burichan.css @@ -3,6 +3,9 @@ background-color: #D6DAF0; border-color: #B7C5D9; } +:root.burichan .field:focus { + border-color: #98E; +} /* Header */ :root.burichan #header-bar { @@ -19,6 +22,11 @@ background-color: rgba(255, 255, 255, .14); } +/* QR */ +:root.burichan .qrpreview { + background-color: rgba(0, 0, 0, .15); +} + /* Menu */ :root.burichan .entry:not(:last-child) { border-bottom: 1px solid #B7C5D9; diff --git a/css/futaba.css b/css/futaba.css index 9e98aee89..6b2486999 100644 --- a/css/futaba.css +++ b/css/futaba.css @@ -3,6 +3,9 @@ background-color: #F0E0D6; border-color: #D9BFB7; } +:root.futaba .field:focus { + border-color: #EA8; +} /* Header */ :root.futaba #header-bar { @@ -19,6 +22,11 @@ background-color: rgba(255, 255, 255, .14); } +/* QR */ +:root.futaba .qrpreview { + background-color: rgba(0, 0, 0, .15); +} + /* Menu */ :root.futaba .entry:not(:last-child) { border-bottom: 1px solid #D9BFB7; diff --git a/css/photon.css b/css/photon.css index d50a8cc14..e509d5d56 100644 --- a/css/photon.css +++ b/css/photon.css @@ -3,6 +3,9 @@ background-color: #DDD; border-color: #CCC; } +:root.photon .field:focus { + border-color: #EA8; +} /* Header */ :root.photon #header-bar { @@ -19,6 +22,11 @@ background-color: rgba(255, 255, 255, .14); } +/* QR */ +:root.photon .qrpreview { + background-color: rgba(0, 0, 0, .15); +} + /* Menu */ :root.photon .entry:not(:last-child) { border-bottom: 1px solid #CCC; diff --git a/css/style.css b/css/style.css index cc3c571a9..bffd10050 100644 --- a/css/style.css +++ b/css/style.css @@ -5,6 +5,28 @@ display: block; padding: 0; } +.field { + border: 1px solid #CCC; + -moz-box-sizing: border-box; + box-sizing: border-box; + color: #333; + font: 13px sans-serif; + margin: 0; + padding: 2px 4px 3px; + outline: none; + -webkit-transition: color .25s, border-color .25s; + transition: color .25s, border-color .25s; +} +.field:-moz-placeholder, +.field:hover:-moz-placeholder { + color: #AAA !important; +} +.field:hover { + border-color: #999; +} +.field:hover, .field:focus { + color: #000; +} .move { cursor: move; } @@ -79,21 +101,15 @@ a[href="javascript:;"] { border-width: 0 0 1px; padding: 4px; position: relative; - transition: all .1s ease-in-out; - -o-transition: all .1s ease-in-out; - -moz-transition: all .1s ease-in-out; -webkit-transition: all .1s ease-in-out; + transition: all .1s ease-in-out; } #header-bar.autohide:not(:hover) { margin-bottom: -1em; - transform: translateY(-100%); - -o-transform: translateY(-100%); - -moz-transform: translateY(-100%); -webkit-transform: translateY(-100%); - transition: all .75s .25s ease-in-out; - -o-transition: all .75s .25s ease-in-out; - -moz-transition: all .75s .25s ease-in-out; + transform: translateY(-100%); -webkit-transition: all .75s .25s ease-in-out; + transition: all .75s .25s ease-in-out; } #toggle-header-bar { cursor: n-resize; @@ -129,10 +145,8 @@ a[href="javascript:;"] { width: 500px; max-width: 100%; position: relative; - transition: all .25s ease-in-out; - -o-transition: all .25s ease-in-out; - -moz-transition: all .25s ease-in-out; -webkit-transition: all .25s ease-in-out; + transition: all .25s ease-in-out; } .notification.error { background-color: hsla(0, 100%, 40%, .9); @@ -154,6 +168,7 @@ a[href="javascript:;"] { position: absolute; } .message { + -moz-box-sizing: border-box; box-sizing: border-box; padding: 4px 20px; max-height: 200px; @@ -234,8 +249,8 @@ a[href="javascript:;"] { display: none; } #ihover { - box-sizing: border-box; -moz-box-sizing: border-box; + box-sizing: border-box; max-height: 100%; max-width: 75%; padding-bottom: 16px; @@ -270,11 +285,15 @@ a[href="javascript:;"] { } /* QR */ +.hide-original-post-form #postForm, +.hide-original-post-form .postingMode { + display: none; +} #qr > .move { min-width: 300px; overflow: hidden; - box-sizing: border-box; -moz-box-sizing: border-box; + box-sizing: border-box; padding: 0 2px; } #qr > .move > span { @@ -283,15 +302,16 @@ a[href="javascript:;"] { #autohide, .close, #qr select, #dump, .remove, .captchaimg, #qr div.warning { cursor: pointer; } -#qr select, -#qr > form { +#qr select { margin: 0; } #dump { background: -webkit-linear-gradient(#EEE, #CCC); - background: -moz-linear-gradient(#EEE, #CCC); - background: -o-linear-gradient(#EEE, #CCC); background: linear-gradient(#EEE, #CCC); + border: 1px solid #CCC; + margin: 0; + padding: 2px 4px 3px; + outline: none; width: 10%; } .gecko #dump { @@ -299,14 +319,10 @@ a[href="javascript:;"] { } #dump:hover, #dump:focus { background: -webkit-linear-gradient(#FFF, #DDD); - background: -moz-linear-gradient(#FFF, #DDD); - background: -o-linear-gradient(#FFF, #DDD); background: linear-gradient(#FFF, #DDD); } #dump:active, .dump #dump:not(:hover):not(:focus) { background: -webkit-linear-gradient(#CCC, #DDD); - background: -moz-linear-gradient(#CCC, #DDD); - background: -o-linear-gradient(#CCC, #DDD); background: linear-gradient(#CCC, #DDD); } #qr:not(.dump) #replies, .dump > form > label { @@ -322,7 +338,7 @@ a[href="javascript:;"] { user-select: none; } #replies > div { - counter-reset: thumbnails; + counter-reset: qrpreviews; top: 0; right: 0; bottom: 0; left: 0; margin: 0; padding: 0; overflow: hidden; @@ -334,103 +350,72 @@ a[href="javascript:;"] { overflow-x: auto; z-index: 1; } -.thumbnail { - background-color: rgba(0,0,0,.2) !important; - background-position: 50% 20% !important; - background-size: cover !important; - border: 1px solid #666; - box-sizing: border-box; +.qrpreview { + background-position: 50% 20%; + background-size: cover; + border: 1px solid #808080; + color: #FFF !important; + font-size: 12px; -moz-box-sizing: border-box; + box-sizing: border-box; cursor: move; display: inline-block; height: 90px; width: 90px; margin: 5px; padding: 2px; - opacity: .5; + opacity: .6; outline: none; overflow: hidden; position: relative; text-shadow: 0 1px 1px #000; -webkit-transition: opacity .25s ease-in-out; - -moz-transition: opacity .25s ease-in-out; - -o-transition: opacity .25s ease-in-out; transition: opacity .25s ease-in-out; vertical-align: top; } -.thumbnail:hover, .thumbnail:focus { +.qrpreview:hover, .qrpreview:focus { opacity: .9; + color: #FFF !important; } -.thumbnail#selected { +.qrpreview#selected { opacity: 1; } -.thumbnail::before { - counter-increment: thumbnails; - content: counter(thumbnails); - color: #FFF; +.qrpreview::before { + counter-increment: qrpreviews; + content: counter(qrpreviews); font-weight: 700; - padding: 3px; + text-shadow: 0 0 3px #000, 0 0 5px #000; position: absolute; - top: 0; - right: 0; - text-shadow: 0 0 3px #000, 0 0 8px #000; + top: 3px; right: 3px; } -.thumbnail.drag { - box-shadow: 0 0 10px rgba(0,0,0,.5); +.qrpreview.drag { + border-color: red; + border-style: dashed; } -.thumbnail.over { +.qrpreview.over { border-color: #FFF; -} -.thumbnail > span { - color: #FFF; + border-style: dashed; } .remove { - background: none; - color: #E00; + color: #E00 !important; font-weight: 700; padding: 3px; } .remove:hover::after { - content: " Remove"; + content: ' Remove'; } -.thumbnail > label { - background: rgba(0,0,0,.5); - color: #FFF; +.qrpreview > label { + background: rgba(0, 0, 0, .5); right: 0; bottom: 0; left: 0; position: absolute; text-align: center; } -.thumbnail > label > input { - margin: 0; +.qrpreview > label > input { + margin: 1px 0; + vertical-align: bottom; } #addReply { - color: #333; font-size: 3.5em; line-height: 100px; } -#addReply:hover, #addReply:focus { - color: #000; -} -.field { - border: 1px solid #CCC; - box-sizing: border-box; - -moz-box-sizing: border-box; - color: #333; - font: 13px sans-serif; - margin: 0; - padding: 2px 4px 3px; - -webkit-transition: color .25s, border .25s; - -moz-transition: color .25s, border .25s; - -o-transition: color .25s, border .25s; - transition: color .25s, border .25s; -} -.field:-moz-placeholder, -.field:hover:-moz-placeholder { - color: #AAA; -} -.field:hover, .field:focus { - border-color: #999; - color: #000; - outline: none; -} #qr > form > div:first-child > .field:not(#dump) { width: 30%; } diff --git a/css/tomorrow.css b/css/tomorrow.css index 071be1ffe..1fa04bac5 100644 --- a/css/tomorrow.css +++ b/css/tomorrow.css @@ -3,6 +3,9 @@ background-color: #282A2E; border-color: #111; } +:root.tomorrow .field:focus { + border-color: #000; +} /* Header */ :root.tomorrow #header-bar { @@ -19,6 +22,11 @@ background-color: rgba(0, 0, 0, .14); } +/* QR */ +:root.tomorrow .qrpreview { + background-color: rgba(255, 255, 255, .15); +} + /* Menu */ :root.tomorrow .entry:not(:last-child) { border-bottom: 1px solid #111; diff --git a/css/yotsuba-b.css b/css/yotsuba-b.css index c421c83ed..767289b74 100644 --- a/css/yotsuba-b.css +++ b/css/yotsuba-b.css @@ -3,6 +3,9 @@ background-color: #D6DAF0; border-color: #B7C5D9; } +:root.yotsuba-b .field:focus { + border-color: #98E; +} /* Header */ :root.yotsuba-b #header-bar { @@ -19,6 +22,11 @@ background-color: rgba(255, 255, 255, .14); } +/* QR */ +:root.yotsuba-b .qrpreview { + background-color: rgba(0, 0, 0, .15); +} + /* Menu */ :root.yotsuba-b .entry:not(:last-child) { border-bottom: 1px solid #B7C5D9; diff --git a/css/yotsuba.css b/css/yotsuba.css index 897f198ab..b51f3e1ee 100644 --- a/css/yotsuba.css +++ b/css/yotsuba.css @@ -3,6 +3,9 @@ background-color: #F0E0D6; border-color: #D9BFB7; } +:root.yotsuba .field:focus { + border-color: #EA8; +} /* Header */ :root.yotsuba #header-bar { @@ -19,6 +22,11 @@ background-color: rgba(255, 255, 255, .14); } +/* QR */ +:root.yotsuba .qrpreview { + background-color: rgba(0, 0, 0, .15); +} + /* Menu */ :root.yotsuba .entry:not(:last-child) { border-bottom: 1px solid #D9BFB7; diff --git a/src/config.coffee b/src/config.coffee index 84e55d97d..2237ac603 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -45,7 +45,6 @@ Config = 'Quick Reply': [true, 'WMD.'] 'Persistent QR': [false, 'The Quick reply won\'t disappear after posting.'] 'Auto Hide QR': [false, 'Automatically hide the quick reply when posting.'] - 'Open Reply in New Tab': [false, 'Open replies posted from the board pages in a new tab.'] 'Remember Subject': [false, 'Remember the subject field, instead of resetting after posting.'] 'Remember Spoiler': [false, 'Remember the spoiler state, instead of resetting after posting.'] 'Hide Original Post Form': [true, 'Replace the normal post form with a shortcut to open the QR.'] diff --git a/src/features.coffee b/src/features.coffee index 25d1a70d3..04d5b677e 100644 --- a/src/features.coffee +++ b/src/features.coffee @@ -762,38 +762,41 @@ Recursive = QR = init: -> - return unless Conf['Quick Reply'] + return if g.VIEW is 'catalog' or !Conf['Quick Reply'] if Conf['Hide Original Post Form'] - Main.css += """ - #postForm, .postingMode { - display: none; - } - """ + $.addClass doc, 'hide-original-post-form' link = $.el 'a', + className: 'qr-shortcut' textContent: 'Quick Reply' href: 'javascript:;' $.on link, 'click', -> Header.menu.close() QR.open() + if g.BOARD.ID is 'f' + if g.VIEW is 'index' + QR.threadSelector.value = '9999' + else if g.VIEW is 'thread' + QR.threadSelector.value = g.THREAD + else + QR.threadSelector.value = 'new' + $('textarea', QR.el).focus() $.event 'AddMenuEntry', type: 'header' el: link - Post::callbacks.push - name: 'Quick Reply' - cb: @node - - $.ready @readyInit - - readyInit: -> - if Conf['Persistent QR'] - QR.open() - QR.hide() if Conf['Auto Hide QR'] $.on d, 'dragover', QR.dragOver $.on d, 'drop', QR.dropFile $.on d, 'dragstart dragend', QR.drag + $.on d, '4chanXInitFinished', -> + return unless Conf['Persistent QR'] + QR.open() + QR.hide() if Conf['Auto Hide QR'] + + Post::callbacks.push + name: 'Quick Reply' + cb: @node node: -> $.on $('a[title="Quote this post"]', @nodes.info), 'click', QR.quote @@ -802,13 +805,14 @@ QR = if QR.el QR.el.hidden = false QR.unhide() - else - try - QR.dialog() - catch err - Main.handleErrors - message: 'Quick Reply dialog creation crashed.' - error: err + return + try + QR.dialog() + catch err + delete QR.el + Main.handleErrors + message: 'Quick Reply dialog creation crashed.' + error: err close: -> QR.el.hidden = true QR.abort() @@ -821,7 +825,7 @@ QR = QR.resetFileInput() if not Conf['Remember Spoiler'] and (spoiler = $.id 'spoiler').checked spoiler.click() - QR.cleanError() + QR.cleanNotification() hide: -> d.activeElement.blur() $.addClass QR.el, 'autohide' @@ -830,7 +834,10 @@ QR = $.rmClass QR.el, 'autohide' $.id('autohide').checked = false toggleHide: -> - @checked and QR.hide() or QR.unhide() + if @checked + QR.hide() + else + QR.unhide() error: (err) -> QR.open() @@ -844,20 +851,20 @@ QR = $('[autocomplete]', QR.el).focus() alert el.textContent if d.hidden QR.lastNotification = new Notification 'warning', el - cleanError: -> + cleanNotification: -> QR.lastNotification?.close() delete QR.lastNotification status: (data={}) -> return unless QR.el - if g.dead + if g.dead # XXX value = 404 disabled = true QR.cooldown.auto = false value = data.progress or QR.cooldown.seconds or value {input} = QR.status input.value = - if QR.cooldown.auto and Conf['Cooldown'] + if QR.cooldown.auto if value then "Auto #{value}" else 'Auto' else value or 'Submit' @@ -865,7 +872,6 @@ QR = cooldown: init: -> - return unless Conf['Cooldown'] QR.cooldown.types = thread: switch g.BOARD when 'q' then 86400 @@ -883,12 +889,11 @@ QR = QR.cooldown.count() sync: (cooldowns) -> # Add each cooldowns, don't overwrite everything in case we - # still need to purge one in the current tab to auto-post. + # still need to prune one in the current tab to auto-post. for id of cooldowns QR.cooldown.cooldowns[id] = cooldowns[id] QR.cooldown.start() set: (data) -> - return unless Conf['Cooldown'] start = Date.now() if data.delay cooldown = delay: data.delay @@ -926,7 +931,12 @@ QR = QR.status() return - if (isReply = if g.REPLY then true else QR.threadSelector.value isnt 'new') + isReply = + if g.BOARD.ID is 'f' and g.VIEW is 'thread' + true + else + QR.threadSelector.value isnt 'new' + if isReply post = QR.replies[0] isSage = /sage/i.test post.email hasFile = !!post.file @@ -973,14 +983,15 @@ QR = e?.preventDefault() QR.open() ta = $ 'textarea', QR.el - unless g.REPLY or ta.value + if QR.threadSelector and !ta.value and g.BOARD.ID isnt 'f' QR.threadSelector.value = $.x('ancestor::div[parent::div[@class="board"]]', @).id[1..] # Make sure we get the correct number, even with XXX censors - id = @previousSibling.hash[2..] - text = ">>#{id}\n" + post = Get.postFromRoot $.x 'ancestor-or-self::div[contains(@class,"postContainer")][1]', @ + text = ">>#{post}\n" sel = d.getSelection() - if (s = sel.toString().trim()) and id is $.x('ancestor-or-self::blockquote', sel.anchorNode)?.id.match(/\d+$/)[0] + selectionRoot = $.x 'ancestor-or-self::div[contains(@class,"postContainer")][1]', sel.anchorNode + if (s = sel.toString().trim()) and post.nodes.root is selectionRoot # XXX Opera doesn't retain `\n`s? s = s.replace /\n/g, '\n>' text += ">#{s}\n" @@ -1019,7 +1030,7 @@ QR = QR.fileInput.call e.dataTransfer $.addClass QR.el, 'dump' fileInput: -> - QR.cleanError() + QR.cleanNotification() # Set or change current reply's file. if @files.length is 1 file = @files[0] @@ -1063,7 +1074,7 @@ QR = @com = null @el = $.el 'a', - className: 'thumbnail' + className: 'qrpreview' draggable: true href: 'javascript:;' innerHTML: '×' @@ -1187,7 +1198,7 @@ QR = else if @el.id is 'selected' (QR.replies[index-1] or QR.replies[index+1]).select() QR.replies.splice index, 1 - (window.URL or window.webkitURL).revokeObjectURL? @url + (window.URL or window.webkitURL)?.revokeObjectURL @url captcha: init: -> @@ -1235,9 +1246,8 @@ QR = @count captchas.length @reload() load: -> - # Timeout is available at RecaptchaState.timeout in seconds. - # We use 5-1 minutes to give upload some time. - @timeout = Date.now() + 4*$.MINUTE + # -1 minute to give upload some time. + @timeout = Date.now() + $.unsafeWindow.RecaptchaState.timeout * $.SECOND - $.MINUTE challenge = @challenge.firstChild.value @img.alt = challenge @img.src = "//www.google.com/recaptcha/api/image?c=#{challenge}" @@ -1252,8 +1262,8 @@ QR = "Verification (#{count} cached captchas)" @input.alt = count # For XTRM RICE. reload: (focus) -> - # the "t" argument prevents the input from being focused - $.globalEval 'javascript:Recaptcha.reload("t")' + # the 't' argument prevents the input from being focused + $.unsafeWindow.Recaptcha.reload 'r' # Focus if we meant to. QR.captcha.input.focus() if focus keydown: (e) -> @@ -1267,23 +1277,16 @@ QR = e.preventDefault() dialog: -> - QR.el = UI.dialog 'qr', 'top:0;right:0;', ' -
- Quick Reply - × -
-
-
- -
-
- -
' - - if Conf['Remember QR size'] and $.engine is 'gecko' - $.on ta = $('textarea', QR.el), 'mouseup', -> - $.set 'QR.size', @style.cssText - ta.style.cssText = $.get 'QR.size', '' + QR.el = UI.dialog 'qr', 'top:0;right:0;', """ +
Quick Reply ×
+
+
+ +
+
+ +
+ """ # Allow only this board's supported files. mimeTypes = $('ul.rules').firstElementChild.textContent.trim().match(/: (.+)/)[1].toLowerCase().replace /\w+/g, (type) -> @@ -1310,21 +1313,24 @@ QR = QR.charaCounter = $ '#charCount', QR.el ta = $ 'textarea', QR.el - unless g.REPLY - # Make a list with visible threads and an option to create a new one. + span = $('.move > span', QR.el) + + # Make a list of visible threads. + if g.BOARD.ID is 'f' + if g.VIEW is 'index' + QR.threadSelector = $('select[name=filetag]').cloneNode true + else + QR.threadSelector = $.el 'select', + title: 'Create a new thread / Reply to a thread' threads = '' - for thread in $$ '.thread' - id = thread.id[1..] - threads += "" - QR.threadSelector = - if g.BOARD is 'f' - $('select[name=filetag]').cloneNode true - else - $.el 'select' - innerHTML: threads - title: 'Create a new thread / Reply to a thread' - $.prepend $('.move > span', QR.el), QR.threadSelector - $.on QR.threadSelector, 'mousedown', (e) -> e.stopPropagation() + for key, thread of g.BOARD.threads + threads += "" + QR.threadSelector.innerHTML = threads + if g.VIEW is 'thread' + QR.threadSelector.value = g.THREAD + if QR.threadSelector + $.prepend span, QR.threadSelector + $.on span, 'mousedown', (e) -> e.stopPropagation() $.on $('#autohide', QR.el), 'change', QR.toggleHide $.on $('.close', QR.el), 'click', QR.close $.on $('#dump', QR.el), 'click', -> QR.el.classList.toggle 'dump' @@ -1366,20 +1372,20 @@ QR = QR.abort() reply = QR.replies[0] - if g.BOARD is 'f' and not g.REPLY + if g.BOARD.ID is 'f' and g.VIEW is 'index' filetag = QR.threadSelector.value threadID = 'new' else - threadID = g.THREAD_ID or QR.threadSelector.value + threadID = QR.threadSelector.value # prevent errors if threadID is 'new' threadID = null - if g.BOARD in ['vg', 'q'] and !reply.sub + if g.BOARD.ID in ['vg', 'q'] and !reply.sub err = 'New threads require a subject.' else unless reply.file or textOnly = !!$ 'input[name=textonly]', $.id 'postForm' err = 'No file selected.' - else if g.BOARD is 'f' and filetag is '9999' + else if g.BOARD.ID is 'f' and filetag is '9999' err = 'Invalid tag specified.' else unless reply.com or reply.file err = 'No file selected.' @@ -1412,7 +1418,7 @@ QR = QR.status() QR.error err return - QR.cleanError() + QR.cleanNotification() # Enable auto-posting if we have stuff to post, disable it otherwise. QR.cooldown.auto = QR.replies.length > 1 @@ -1530,14 +1536,13 @@ QR = post: reply isReply: threadID isnt '0' + # Enable auto-posting if we have stuff to post, disable it otherwise. + QR.cooldown.auto = QR.replies.length > 1 + if threadID is '0' # new thread - # auto-noko - location.pathname = "/#{g.BOARD}/res/#{postID}" - else - # Enable auto-posting if we have stuff to post, disable it otherwise. - QR.cooldown.auto = QR.replies.length > 1 - if Conf['Open Reply in New Tab'] and !g.REPLY and !QR.cooldown.auto - $.open "//boards.4chan.org/#{g.BOARD}/res/#{threadID}#p#{postID}" + $.open "/#{g.BOARD}/res/#{postID}" + else if g.VIEW is 'reply' and !QR.cooldown.auto # posting from the index + $.open "//boards.4chan.org/#{g.BOARD}/res/#{threadID}#p#{postID}" if Conf['Persistent QR'] or QR.cooldown.auto reply.rm() @@ -2092,7 +2097,7 @@ Build = '
' + "No." + "No." + " + now = Date.now() if @file and !@file.isDead @file.isDead = true + @file.timeOfDeath = now return if img @isDead = true + @timeOfDeath = now $.addClass @nodes.root, 'dead' # XXX style dead posts. @@ -325,21 +328,22 @@ Main = initStyle: -> # disable the mobile layout $('link[href*=mobile]', d.head)?.disabled = true + $.addClass doc, $.engine + $.addClass doc, 'fourchan-x' $.addStyle Main.css - style = null + style = 'yotsuba-b' mainStyleSheet = $ 'link[title=switch]', d.head styleSheets = $$ 'link[rel="alternate stylesheet"]', d.head setStyle = -> - $.rmClass doc, style if style + $.rmClass doc, style for styleSheet in styleSheets if styleSheet.href is mainStyleSheet.href style = styleSheet.title.toLowerCase().replace('new', '').trim().replace /\s+/g, '-' break $.addClass doc, style - $.addClass doc, $.engine - $.addClass doc, 'fourchan-x' setStyle() + return unless mainStyleSheet if MutationObserver = window.MutationObserver or window.WebKitMutationObserver or window.OMutationObserver observer = new MutationObserver setStyle observer.observe mainStyleSheet, @@ -351,7 +355,7 @@ Main = initReady: -> unless $.hasClass doc, 'fourchan-x' - # Something might go wrong! + # Something might have gone wrong! Main.initStyle() if d.title is '4chan - 404 Not Found' @@ -362,28 +366,31 @@ Main = postID: location.hash return - threads = [] - posts = [] + if board = $ '.board' + threads = [] + posts = [] - for boardChild in $('.board').children - continue unless $.hasClass boardChild, 'thread' - thread = new Thread boardChild.id[1..], g.BOARD - threads.push thread - for threadChild in boardChild.children - continue unless $.hasClass threadChild, 'postContainer' - try - posts.push new Post threadChild, thread, g.BOARD - catch err - # Skip posts that we failed to parse. - unless errors - errors = [] - errors.push - message: "Parsing of Post No.#{threadChild.id.match(/\d+/)} failed. Post will be skipped." - error: err - Main.handleErrors errors if errors + for boardChild in board.children + continue unless $.hasClass boardChild, 'thread' + thread = new Thread boardChild.id[1..], g.BOARD + threads.push thread + for threadChild in boardChild.children + continue unless $.hasClass threadChild, 'postContainer' + try + posts.push new Post threadChild, thread, g.BOARD + catch err + # Skip posts that we failed to parse. + unless errors + errors = [] + errors.push + message: "Parsing of Post No.#{threadChild.id.match(/\d+/)} failed. Post will be skipped." + error: err + Main.handleErrors errors if errors - Main.callbackNodes Thread, threads - Main.callbackNodes Post, posts + Main.callbackNodes Thread, threads + Main.callbackNodes Post, posts + + $.event '4chanXInitFinished' callbackNodes: (klass, nodes) -> # get the nodes' length only once