diff --git a/4chan_x.user.js b/4chan_x.user.js index 45d3af7b6..5b8131429 100644 --- a/4chan_x.user.js +++ b/4chan_x.user.js @@ -72,7 +72,7 @@ */ (function() { - var $, $$, Anonymize, AutoGif, Conf, Config, ExpandComment, ExpandThread, Favicon, FileInfo, Filter, GetTitle, ImageExpand, ImageHover, Keybinds, Main, Nav, Options, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, Quotify, Redirect, ReplyHiding, ReportButton, RevealSpoilers, Sauce, StrikethroughQuotes, ThreadHiding, ThreadStats, Threading, Time, TitlePost, UI, Unread, Updater, Watcher, d, g, _base; + var $, $$, Anonymize, AutoGif, Conf, Config, ExpandComment, ExpandThread, Favicon, FileInfo, Filter, GetTitle, ImageExpand, ImageHover, Keybinds, Main, Nav, Options, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, Quotify, Redirect, ReplyHiding, ReportButton, RevealSpoilers, Sauce, StrikethroughQuotes, ThreadHiding, ThreadStats, Time, TitlePost, UI, Unread, Updater, Watcher, d, g, _base; Config = { main: { @@ -153,8 +153,7 @@ sauces: ['http://iqdb.org/?url=$1', 'http://www.google.com/searchbyimage?image_url=$1', '#http://tineye.com/search?url=$1', '#http://saucenao.com/search.php?db=999&url=$1', '#http://3d.iqdb.org/?url=$1', '#http://regex.info/exif.cgi?imgurl=$2', '# uploaders:', '#http://imgur.com/upload?url=$2', '#http://omploader.org/upload?url1=$2', '# "View Same" in archives:', '#http://archive.foolz.us/$4/image/$3/', '#https://archive.installgentoo.net/$4/image/$3'].join('\n'), time: '%m/%d/%y(%a)%H:%M', backlink: '>>%id', - fileInfoR: '%l (%s, %r)', - fileInfoT: '%l (%s, %r)', + fileInfo: '%l (%p%s, %r)', favicon: 'ferongr', hotkeys: { openQR: ['i', 'Open QR with post number inserted'], @@ -217,22 +216,20 @@ rect = el.getBoundingClientRect(); UI.dx = e.clientX - rect.left; UI.dy = e.clientY - rect.top; - UI.width = d.body.clientWidth - el.offsetWidth; - return UI.height = d.body.clientHeight - el.offsetHeight; + UI.width = d.documentElement.clientWidth - rect.width; + return UI.height = d.documentElement.clientHeight - rect.height; }, drag: function(e) { - var bottom, left, right, style, top; + var left, style, top; left = e.clientX - UI.dx; top = e.clientY - UI.dy; - left = left < 10 ? 0 : UI.width - left < 10 ? null : left; - top = top < 10 ? 0 : UI.height - top < 10 ? null : top; - right = left === null ? 0 : null; - bottom = top === null ? 0 : null; + left = left < 10 ? '0px' : UI.width - left < 10 ? null : left + 'px'; + top = top < 10 ? '0px' : UI.height - top < 10 ? null : top + 'px'; style = UI.el.style; + style.left = left; style.top = top; - style.right = right; - style.bottom = bottom; - return style.left = left; + style.right = left === null ? '0px' : null; + return style.bottom = top === null ? '0px' : null; }, dragend: function() { var el; @@ -242,20 +239,19 @@ return d.removeEventListener('mouseup', UI.dragend, false); }, hover: function(e) { - var clientHeight, clientWidth, clientX, clientY, el, height, style, top, _ref; + var clientHeight, clientWidth, clientX, clientY, height, style, top, _ref; clientX = e.clientX, clientY = e.clientY; - el = UI.el; - style = el.style; - _ref = d.body, clientHeight = _ref.clientHeight, clientWidth = _ref.clientWidth; - height = el.offsetHeight; + style = UI.el.style; + _ref = d.documentElement, clientHeight = _ref.clientHeight, clientWidth = _ref.clientWidth; + height = UI.el.offsetHeight; top = clientY - 120; - style.top = clientHeight <= height || top <= 0 ? 0 : top + height >= clientHeight ? clientHeight - height : top; + style.top = clientHeight <= height || top <= 0 ? '0px' : top + height >= clientHeight ? clientHeight - height + 'px' : top + 'px'; if (clientX <= clientWidth - 400) { - style.left = clientX + 45; + style.left = clientX + 45 + 'px'; return style.right = null; } else { style.left = null; - return style.right = clientWidth - clientX + 45; + return style.right = clientWidth - clientX + 45 + 'px'; } }, hoverend: function() { @@ -617,12 +613,12 @@ }; }, node: function(post) { - var el, filter, firstThread, isOP, key, result, thisThread, value, _i, _len, _ref; + var filter, firstThread, isOP, key, result, root, thisThread, value, _i, _len, _ref; if (post.isInlined) { return; } - post.isOP = post["class"] === 'op'; - isOP = post.isOP, el = post.el; + isOP = post.id === post.threadId; + root = post.root; for (key in Filter.filters) { value = Filter[key](post); if (value === false) { @@ -637,38 +633,32 @@ if (result === true) { if (isOP) { if (!g.REPLY) { - ThreadHiding.hide(post.el.parentNode); + ThreadHiding.hide(root.parentNode); } else { continue; } } else { - ReplyHiding.hide(post.root); + ReplyHiding.hide(root); } return; } - if (isOP) { - $.addClass(el, result[0]); - } else { - $.addClass(el.parentNode, result[0]); - } + $.addClass((isOP ? root.parentNode : root), result[0]); if (isOP && result[1] && !g.REPLY) { - thisThread = el.parentNode; - if (firstThread = $('div[class=op]')) { - $.before(firstThread.parentNode, [thisThread, thisThread.nextElementSibling]); + thisThread = root.parentNode; + if (firstThread = $('div[class=thread]')) { + $.before(firstThread, [thisThread, thisThread.nextElementSibling]); } } } } }, name: function(post) { - var name; - name = post.isOP ? $('.postername', post.el) : $('.commentpostername', post.el); - return name.textContent; + return $('.name', post.el).textContent; }, uniqueid: function(post) { var uid; if (uid = $('.posteruid', post.el)) { - return uid.textContent; + return uid.textContent.slice(5, -1); } return false; }, @@ -681,44 +671,42 @@ }, mod: function(post) { var mod; - if (mod = (post.isOP ? $('.commentpostername', post.el) : $('.commentpostername ~ .commentpostername', post.el))) { + if (mod = $('.capcode', post.el)) { return mod.textContent; } return false; }, email: function(post) { var mail; - if (mail = $('.linkmail', post.el)) { - return mail.href; + if (mail = $('.useremail', post.el)) { + return mail.pathname; } return false; }, subject: function(post) { - var sub; - sub = post.isOP ? $('.filetitle', post.el) : $('.replytitle', post.el); - return sub.textContent; + return $('.subject', post.el).textContent || false; }, comment: function(post) { var data, i, nodes, text, _i, _ref; text = []; - nodes = d.evaluate('.//br|.//text()', post.el.lastChild, null, 7, null); + nodes = d.evaluate('.//br|.//text()', post.el.lastElementChild, null, 7, null); for (i = _i = 0, _ref = nodes.snapshotLength; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) { text.push((data = nodes.snapshotItem(i).data) ? data : '\n'); } return text.join(''); }, filename: function(post) { - var file, filesize; - filesize = post.filesize; - if (filesize && (file = $('span', filesize))) { + var file, fileInfo; + fileInfo = post.fileInfo; + if (fileInfo && (file = $('.fileText > span', fileInfo))) { return file.title; } return false; }, dimensions: function(post) { - var filesize, match; - filesize = post.filesize; - if (filesize && (match = filesize.textContent.match(/\d+x\d+/))) { + var fileInfo, match; + fileInfo = post.fileInfo; + if (fileInfo && (match = fileInfo.textContent.match(/\d+x\d+/))) { return match[0]; } return false; @@ -735,7 +723,7 @@ var img; img = post.img; if (img) { - return img.getAttribute('md5'); + return img.dataset.md5; } return false; } @@ -753,7 +741,7 @@ _ref = post.quotes; for (_i = 0, _len = _ref.length; _i < _len; _i++) { quote = _ref[_i]; - if ((el = $.id(quote.hash.slice(1))) && el.parentNode.parentNode.parentNode.hidden) { + if ((el = $.id(quote.hash.slice(1))) && el.hidden) { $.addClass(quote, 'filtered'); if (Conf['Recursive Filtering']) { ReplyHiding.hide(post.root); @@ -766,22 +754,21 @@ ExpandComment = { init: function() { var a, _i, _len, _ref; - _ref = $$('.abbr > a'); + _ref = $$('.abbr'); for (_i = 0, _len = _ref.length; _i < _len; _i++) { a = _ref[_i]; - $.on(a, 'click', ExpandComment.expand); + $.on(a.firstElementChild, 'click', ExpandComment.expand); } }, expand: function(e) { var a, replyID, threadID, _, _ref; e.preventDefault(); - _ref = this.href.match(/(\d+)#(\d+)/), _ = _ref[0], threadID = _ref[1], replyID = _ref[2]; + _ref = this.href.match(/(\d+)#p(\d+)/), _ = _ref[0], threadID = _ref[1], replyID = _ref[2]; this.textContent = "Loading " + replyID + "..."; - threadID = this.pathname.split('/').pop() || $.x('ancestor::div[@class="thread"]/div', this).id; a = this; - return $.cache(this.pathname, (function() { + return $.cache(this.pathname, function() { return ExpandComment.parse(this, a, threadID, replyID); - })); + }); }, parse: function(req, a, threadID, replyID) { var doc, href, node, post, quote, quotes, _i, _len; @@ -791,16 +778,15 @@ } doc = d.implementation.createHTMLDocument(''); doc.documentElement.innerHTML = req.response; - Threading.op($('body > form', doc).firstChild); - node = d.importNode(doc.getElementById(replyID)); + node = d.importNode(doc.getElementById("m" + replyID)); quotes = node.getElementsByClassName('quotelink'); for (_i = 0, _len = quotes.length; _i < _len; _i++) { quote = quotes[_i]; - if (quote.hash === (href = quote.getAttribute('href'))) { - quote.pathname = "/" + g.BOARD + "/res/" + threadID; - } else if (href !== quote.href) { - quote.href = "res/" + href; + href = quote.getAttribute('href'); + if (href[0] === '/') { + continue; } + quote.href = "res/" + href; } post = { el: node, @@ -823,51 +809,44 @@ if (Conf['Indicate Cross-thread Quotes']) { QuoteCT.node(post); } - return $.replace(a.parentNode.parentNode, node.lastChild); + return $.replace(a.parentNode.parentNode, node); } }; ExpandThread = { init: function() { var a, span, _i, _len, _ref, _results; - _ref = $$('.omittedposts'); + _ref = $$('.summary'); _results = []; for (_i = 0, _len = _ref.length; _i < _len; _i++) { span = _ref[_i]; a = $.el('a', { textContent: "+ " + span.textContent, - className: 'omittedposts', + className: 'summary desktop', href: 'javascript:;' }); - $.on(a, 'click', ExpandThread.cb.toggle); + $.on(a, 'click', function() { + return ExpandThread.toggle(this.parentNode); + }); _results.push($.replace(span, a)); } return _results; }, - cb: { - toggle: function() { - var thread; - thread = this.parentNode; - return ExpandThread.toggle(thread); - } - }, toggle: function(thread) { - var a, backlink, num, pathname, prev, table, threadID, _i, _len, _ref, _ref1, _results; - threadID = thread.firstChild.id; - pathname = "/" + g.BOARD + "/res/" + threadID; - a = $('.omittedposts', thread); + var a, backlink, num, pathname, replies, reply, _i, _j, _len, _len1, _ref; + pathname = "/" + g.BOARD + "/res/" + thread.id.slice(1); + a = $('.summary', thread); switch (a.textContent[0]) { case '+': - if ((_ref = $('.op .container', thread)) != null) { - _ref.textContent = ''; - } a.textContent = a.textContent.replace('+', '\u00d7 Loading...'); - return $.cache(pathname, (function() { - return ExpandThread.parse(this, pathname, thread, a); - })); + $.cache(pathname, function() { + return ExpandThread.parse(this, thread, a); + }); + break; case '\u00d7': a.textContent = a.textContent.replace('\u00d7 Loading...', '+'); - return $.cache.requests[pathname].abort(); + $.cache.requests[pathname].abort(); + break; case '-': a.textContent = a.textContent.replace('-', '+'); num = (function() { @@ -881,25 +860,23 @@ return 5; } })(); - table = $.x("following::br[@clear]/preceding::table[" + num + "]", a); - while ((prev = table.previousSibling) && (prev.nodeName !== 'A')) { - $.rm(prev); + replies = $$('.replyContainer', thread); + replies.splice(replies.length - num, num); + for (_i = 0, _len = replies.length; _i < _len; _i++) { + reply = replies[_i]; + $.rm(reply); } - _ref1 = $$('.backlink', $('.op', thread)); - _results = []; - for (_i = 0, _len = _ref1.length; _i < _len; _i++) { - backlink = _ref1[_i]; + _ref = $$('.backlink', a.previousElementSibling); + for (_j = 0, _len1 = _ref.length; _j < _len1; _j++) { + backlink = _ref[_j]; if (!$.id(backlink.hash.slice(1))) { - _results.push($.rm(backlink)); - } else { - _results.push(void 0); + $.rm(backlink); } } - return _results; } }, - parse: function(req, pathname, thread, a) { - var doc, href, link, next, nodes, quote, reply, table, _i, _j, _len, _len1, _ref, _ref1; + parse: function(req, thread, a) { + var backlink, doc, href, id, link, nodes, post, quote, reply, threadID, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1, _ref2, _ref3; if (req.status !== 200) { a.textContent = "" + req.status + " " + req.statusText; $.off(a, 'click', ExpandThread.cb.toggle); @@ -908,97 +885,180 @@ a.textContent = a.textContent.replace('\u00d7 Loading...', '-'); doc = d.implementation.createHTMLDocument(''); doc.documentElement.innerHTML = req.response; + threadID = thread.id.slice(1); nodes = []; - _ref = $$('.reply', doc); + _ref = $$('.replyContainer', doc); for (_i = 0, _len = _ref.length; _i < _len; _i++) { reply = _ref[_i]; - table = d.importNode(reply.parentNode.parentNode.parentNode); - _ref1 = $$('.quotelink', table); + reply = d.importNode(reply); + _ref1 = $$('.quotelink', reply); for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { quote = _ref1[_j]; - if (quote.hash === (href = quote.getAttribute('href'))) { - quote.pathname = pathname; - } else if (href !== quote.href) { - quote.href = "res/" + href; + href = quote.getAttribute('href'); + if (href[0] === '/') { + continue; } + quote.href = "res/" + href; } - link = $('.quotejs', table); - link.href = "res/" + thread.firstChild.id + "#" + reply.id; - link.nextSibling.href = "res/" + thread.firstChild.id + "#q" + reply.id; - nodes.push(table); + id = reply.id.slice(2); + link = $('.postInfo > .postNum > a:first-child', reply); + link.href = "res/" + threadID + "#p" + id; + link.nextSibling.href = "res/" + threadID + "#q" + id; + nodes.push(reply); } - while ((next = a.nextSibling) && !next.clear) { - $.rm(next); + _ref2 = $$('.summary ~ .replyContainer', a.parentNode); + for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) { + post = _ref2[_k]; + $.rm(post); } - return $.before(next, nodes); + _ref3 = $$('.backlink', a.previousElementSibling); + for (_l = 0, _len3 = _ref3.length; _l < _len3; _l++) { + backlink = _ref3[_l]; + if (!$.id(backlink.hash.slice(1))) { + $.rm(backlink); + } + } + return $.after(a, nodes); + } + }; + + ThreadHiding = { + init: function() { + var a, hiddenThreads, thread, _i, _len, _ref; + hiddenThreads = $.get("hiddenThreads/" + g.BOARD + "/", {}); + _ref = $$('.thread'); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + thread = _ref[_i]; + a = $.el('a', { + className: 'hide_thread_button', + innerHTML: '[ - ]', + href: 'javascript:;' + }); + $.on(a, 'click', ThreadHiding.cb); + $.prepend(thread, a); + if (thread.id.slice(1) in hiddenThreads) { + ThreadHiding.hide(thread); + } + } + }, + cb: function() { + return ThreadHiding.toggle(this.parentNode); + }, + toggle: function(thread) { + var hiddenThreads, id; + hiddenThreads = $.get("hiddenThreads/" + g.BOARD + "/", {}); + id = thread.id.slice(1); + if (thread.hidden || /\bhidden_thread\b/.test(thread.firstChild.className)) { + ThreadHiding.show(thread); + delete hiddenThreads[id]; + } else { + ThreadHiding.hide(thread); + hiddenThreads[id] = Date.now(); + } + return $.set("hiddenThreads/" + g.BOARD + "/", hiddenThreads); + }, + hide: function(thread) { + var a, num, opInfo, span, text; + if (!Conf['Show Stubs']) { + thread.hidden = true; + thread.nextElementSibling.hidden = true; + return; + } + if (thread.firstChild.className === 'block') { + return; + } + num = 0; + if (span = $('.summary', thread)) { + num = Number(span.textContent.match(/\d+/)); + } + num += $$('.opContainer ~ .replyContainer', thread).length; + text = num === 1 ? '1 reply' : "" + num + " replies"; + opInfo = $('.op > .postInfo > .nameBlock', thread).textContent; + a = $('.hide_thread_button', thread); + $.addClass(a, 'hidden_thread'); + a.firstChild.textContent = '[ + ]'; + return $.add(a, $.tn(" " + opInfo + " (" + text + ")")); + }, + show: function(thread) { + var a; + a = $('.hide_thread_button', thread); + $.removeClass(a, 'hidden_thread'); + a.innerHTML = '[ - ]'; + thread.hidden = false; + return thread.nextElementSibling.hidden = false; } }; ReplyHiding = { init: function() { - this.td = $.el('td', { - noWrap: true, - className: 'replyhider', - innerHTML: '[ - ]' - }); return Main.callbacks.push(this.node); }, node: function(post) { - var td; - if (post["class"]) { + var button; + if (post.isInlined || /\bop\b/.test(post["class"])) { return; } - td = ReplyHiding.td.cloneNode(true); - $.on(td.firstChild, 'click', ReplyHiding.toggle); - $.replace(post.el.previousSibling, td); + button = post.root.firstElementChild; + $.addClass(button, 'hide_reply_button'); + button.innerHTML = '[ - ]'; + $.on(button.firstChild, 'click', ReplyHiding.toggle); if (post.id in g.hiddenReplies) { return ReplyHiding.hide(post.root); } }, toggle: function() { - var id, parent, quote, table, _i, _j, _len, _len1, _ref, _ref1; - parent = this.parentNode; - if (parent.className === 'replyhider') { - ReplyHiding.hide(parent.parentNode.parentNode.parentNode); - id = parent.nextSibling.id; - _ref = $$(".quotelink[href='#" + id + "'], .backlink[href='#" + id + "']"); - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - quote = _ref[_i]; - $.addClass(quote, 'filtered'); - } - g.hiddenReplies[id] = Date.now(); - } else { - table = parent.nextSibling; - table.hidden = false; - $.rm(parent); - id = table.firstChild.firstChild.lastChild.id; - _ref1 = $$(".quotelink[href$='#" + id + "'], .backlink[href='#" + id + "']"); - for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { - quote = _ref1[_j]; + var button, id, quote, quotes, root, _i, _j, _len, _len1; + button = this.parentNode; + root = button.parentNode; + id = root.id.slice(2); + quotes = $$(".quotelink[href$='#p" + id + "'], .backlink[href='#p" + id + "']"); + if (/\bstub\b/.test(button.className)) { + ReplyHiding.show(root); + for (_i = 0, _len = quotes.length; _i < _len; _i++) { + quote = quotes[_i]; $.removeClass(quote, 'filtered'); } delete g.hiddenReplies[id]; + } else { + ReplyHiding.hide(root); + for (_j = 0, _len1 = quotes.length; _j < _len1; _j++) { + quote = quotes[_j]; + $.addClass(quote, 'filtered'); + } + g.hiddenReplies[id] = Date.now(); } return $.set("hiddenReplies/" + g.BOARD + "/", g.hiddenReplies); }, - hide: function(table) { - var div, name, trip, uid, _ref, _ref1; - if (table.hidden) { + hide: function(root) { + var button, el, stub; + button = root.firstElementChild; + if (button.hidden) { return; } - table.hidden = true; + button.hidden = true; + el = root.lastElementChild; + el.hidden = true; if (!Conf['Show Stubs']) { return; } - name = $('.commentpostername', table).textContent; - uid = ((_ref = $('.posteruid', table)) != null ? _ref.textContent : void 0) || ''; - trip = ((_ref1 = $('.postertrip', table)) != null ? _ref1.textContent : void 0) || ''; - div = $.el('div', { - className: 'stub', - innerHTML: "[ + ] " + name + " " + uid + " " + trip + "" + stub = $.el('div', { + className: 'hide_reply_button stub', + innerHTML: '[ + ] ' }); - $.on(div.firstChild, 'click', ReplyHiding.toggle); - return $.before(table, div); + $.add(stub.firstChild, $.tn($('.nameBlock', el).textContent)); + $.on(stub.firstChild, 'click', ReplyHiding.toggle); + return $.after(button, stub); + }, + show: function(root) { + var button, el; + el = root.lastElementChild; + button = root.firstElementChild; + el.hidden = false; + button.hidden = false; + if (!Conf['Show Stubs']) { + return; + } + return $.rm(button.nextElementSibling); } }; @@ -1013,7 +1073,7 @@ return $.on(d, 'keydown', Keybinds.keydown); }, keydown: function(e) { - var key, o, range, selEnd, selStart, ta, thread, value, _ref, _ref1; + var key, link, o, range, selEnd, selStart, ta, thread, value; if (!(key = Keybinds.keyCode(e)) || /TEXTAREA|INPUT/.test(e.target.nodeName) && !(e.altKey || e.ctrlKey || e.keyCode === 27)) { return; } @@ -1071,16 +1131,16 @@ Keybinds.img(thread, true); break; case Conf.zero: - window.location = "/" + g.BOARD + "/0#0"; + window.location = "/" + g.BOARD + "/0#delform"; break; case Conf.nextPage: - if ((_ref = $('input[value=Next]')) != null) { - _ref.click(); + if (link = $('link[rel=next]', d.head)) { + window.location = link.href; } break; case Conf.previousPage: - if ((_ref1 = $('input[value=Previous]')) != null) { - _ref1.click(); + if (link = $('link[rel=prev]', d.head)) { + window.location.href = link.href; } break; case Conf.nextThread: @@ -1198,13 +1258,13 @@ if (all) { return $.id('imageExpand').click(); } else { - thumb = $('img[md5]', $('.replyhl', thread) || thread); + thumb = $('img[data-md5]', $('.post.highlight', thread) || thread); return ImageExpand.toggle(thumb.parentNode); } }, qr: function(thread, quote) { if (quote) { - QR.quote.call($('.quotejs + .quotejs', $('.replyhl', thread) || thread)); + QR.quote.call($('.postInfo > .postNum > a[title="Quote this post"]', $('.post.highlight', thread) || thread)); } else { QR.open(); } @@ -1212,7 +1272,7 @@ }, open: function(thread, tab) { var id, url; - id = thread.firstChild.id; + id = thread.id.slice(1); url = "//boards.4chan.org/" + g.BOARD + "/res/" + id; if (tab) { return $.open(url); @@ -1221,29 +1281,25 @@ } }, hl: function(delta, thread) { - var next, rect, replies, reply, td, _i, _len; - if (td = $('.replyhl', thread)) { - td.className = 'reply'; - td.removeAttribute('tabindex'); - rect = td.getBoundingClientRect(); - if (rect.bottom >= 0 && rect.top <= d.body.clientHeight) { - next = delta === +1 ? $.x('following::td[@class="reply"]', td) : $.x('preceding::td[@class="reply"]', td); + var next, post, rect, replies, reply, _i, _len; + if (post = $('.reply.highlight', thread)) { + $.removeClass(post, 'highlight'); + post.removeAttribute('tabindex'); + rect = post.getBoundingClientRect(); + if (rect.bottom >= 0 && rect.top <= d.documentElement.clientHeight) { + next = $.x('child::div[contains(@class,"post reply")]', delta === +1 ? post.parentNode.nextElementSibling : post.parentNode.previousElementSibling); if (!next) { - td.className = 'replyhl'; - td.tabIndex = 0; - td.focus(); + this.focus(post); return; } if (!(g.REPLY || $.x('ancestor::div[@class="thread"]', next) === thread)) { return; } rect = next.getBoundingClientRect(); - if (rect.top < 0 || rect.bottom > d.body.clientHeight) { + if (rect.top < 0 || rect.bottom > d.documentElement.clientHeight) { next.scrollIntoView(delta === -1); } - next.className = 'replyhl'; - next.tabIndex = 0; - next.focus(); + this.focus(next); return; } } @@ -1254,13 +1310,16 @@ for (_i = 0, _len = replies.length; _i < _len; _i++) { reply = replies[_i]; rect = reply.getBoundingClientRect(); - if (delta === +1 && rect.top >= 0 || delta === -1 && rect.bottom <= d.body.clientHeight) { - reply.className = 'replyhl'; - reply.tabIndex = 0; - reply.focus(); + if (delta === +1 && rect.top >= 0 || delta === -1 && rect.bottom <= d.documentElement.clientHeight) { + this.focus(reply); return; } } + }, + focus: function(post) { + $.addClass(post, 'highlight'); + post.tabIndex = 0; + return post.focus(); } }; @@ -1312,7 +1371,7 @@ return thread; } } - return $('form[name=delform]'); + return $('.board'); }, scroll: function(delta) { var i, rect, thread, top, _ref, _ref1; @@ -1347,7 +1406,7 @@ } return $('textarea', QR.el).focus(); }); - $.before($('form[name=post]'), link); + $.before($.id('postForm'), link); } script = $.el('script', { textContent: 'Recaptcha.focus_response_field=function(){}' @@ -1365,7 +1424,7 @@ return $.on(d, 'dragstart dragend', QR.drag); }, node: function(post) { - return $.on($('.quotejs + .quotejs', post.el), 'click', QR.quote); + return $.on($('.postInfo > .postNum > a[title="Quote this post"]', post.el), 'click', QR.quote); }, open: function() { if (QR.el) { @@ -1429,15 +1488,15 @@ if (data == null) { data = {}; } + if (!QR.el) { + return; + } if (g.dead) { value = 404; disabled = true; QR.cooldown.auto = false; } value = QR.cooldown.seconds || data.progress || value; - if (!QR.el) { - return; - } input = QR.status.input; input.value = QR.cooldown.auto && Conf['Cooldown'] ? value ? "Auto " + value : 'Auto' : value || 'Submit'; return input.disabled = disabled || false; @@ -1484,12 +1543,12 @@ } QR.open(); if (!g.REPLY) { - $('select', QR.el).value = $.x('ancestor::div[@class="thread"]', this).firstChild.id; + $('select', QR.el).value = $.x('ancestor::div[@class="thread"]', this).id.slice(1); } - id = this.previousElementSibling.hash.slice(1); + id = this.previousSibling.hash.slice(2); text = ">>" + id + "\n"; sel = window.getSelection(); - if ((s = sel.toString()) && id === ((_ref = $.x('ancestor-or-self::blockquote/preceding-sibling::input', sel.anchorNode)) != null ? _ref.name : void 0)) { + if ((s = sel.toString()) && id === ((_ref = $.x('ancestor-or-self::blockquote', sel.anchorNode)) != null ? _ref.id.match(/\d+$/)[0] : void 0)) { s = s.replace(/\n/g, '\n>'); text += ">" + s + "\n"; } @@ -1497,7 +1556,6 @@ caretPos = ta.selectionStart; QR.selected.el.lastChild.textContent = QR.selected.com = ta.value = ta.value.slice(0, caretPos) + text + ta.value.slice(ta.selectionEnd); ta.focus(); - ta.selectionEnd = ta.selectionStart = caretPos + text.length; range = caretPos + text.length; return ta.setSelectionRange(range, range); }, @@ -1783,7 +1841,7 @@ this.timeout = Date.now() + 26 * $.MINUTE; challenge = this.challenge.firstChild.value; this.img.alt = challenge; - this.img.src = "http://www.google.com/recaptcha/api/image?c=" + challenge; + this.img.src = "//www.google.com/recaptcha/api/image?c=" + challenge; return this.input.value = null; }, count: function(count) { @@ -1819,7 +1877,7 @@ } }, dialog: function() { - var e, fileInput, mimeTypes, name, spoiler, ta, thread, threads, _i, _j, _len, _len1, _ref, _ref1; + var e, 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 \ @@ -1841,7 +1899,7 @@ }); ta.style.cssText = $.get('QR.size', ''); } - mimeTypes = $('.rules').firstElementChild.textContent.trim().match(/: (.+)/)[1].toLowerCase().replace(/\w+/g, function(type) { + mimeTypes = $('ul.rules').firstElementChild.textContent.trim().match(/: (.+)/)[1].toLowerCase().replace(/\w+/g, function(type) { switch (type) { case 'jpg': return 'image/jpeg'; @@ -1853,18 +1911,19 @@ }); QR.mimeTypes = mimeTypes.split(', '); QR.mimeTypes.push(''); - fileInput = $('[type=file]', QR.el); - fileInput.max = $('[name=MAX_FILE_SIZE]').value; + fileInput = $('input[type=file]', QR.el); + fileInput.max = $('input[name=MAX_FILE_SIZE]').value; fileInput.accept = mimeTypes; - QR.spoiler = !!$('#com_submit + label'); + QR.spoiler = !!$('input[name=spoiler]'); spoiler = $('#spoilerLabel', QR.el); spoiler.hidden = !QR.spoiler; if (!g.REPLY) { threads = ''; - _ref = $$('.op'); + _ref = $$('.thread'); for (_i = 0, _len = _ref.length; _i < _len; _i++) { thread = _ref[_i]; - threads += ""; + id = thread.id.slice(1); + threads += ""; } $.prepend($('.move > span', QR.el), $.el('select', { innerHTML: threads, @@ -1901,8 +1960,9 @@ for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { name = _ref1[_j]; $.on($("[name=" + name + "]", QR.el), 'input keyup change paste', function() { + var _ref2; QR.selected[this.name] = this.value; - if (QR.cooldown.auto && QR.selected === QR.replies[0] && parseInt(QR.status.input.value.match(/\d+/)) < 6) { + if (QR.cooldown.auto && QR.selected === QR.replies[0] && (0 < (_ref2 = QR.cooldown.seconds) && _ref2 < 6)) { return QR.cooldown.auto = false; } }); @@ -1920,7 +1980,7 @@ } return _results; }); - QR.status.input = $('[type=submit]', QR.el); + QR.status.input = $('input[type=submit]', QR.el); QR.status(); QR.cooldown.init(); QR.captcha.init(); @@ -1993,7 +2053,7 @@ upfile: reply.file, spoiler: reply.spoiler, mode: 'regist', - pwd: (m = d.cookie.match(/4chan_pass=([^;]+)/)) ? decodeURIComponent(m[1]) : $('[name=pwd]').value, + pwd: (m = d.cookie.match(/4chan_pass=([^;]+)/)) ? decodeURIComponent(m[1]) : $('input[name=pwd]').value, recaptcha_challenge_field: challenge, recaptcha_response_field: response + ' ' }; @@ -2032,7 +2092,7 @@ } } }; - return QR.ajax = $.ajax($('form[name=post]').action, callbacks, opts); + return QR.ajax = $.ajax($.id('postForm').parentNode.action, callbacks, opts); }, response: function(html) { var b, doc, err, node, persona, postNumber, reply, thread, _, _ref; @@ -2132,7 +2192,7 @@ } }, dialog: function() { - var arr, back, checked, description, dialog, favicon, fileInfoR, fileInfoT, hiddenNum, hiddenThreads, indicator, indicators, input, key, li, obj, overlay, ta, time, tr, ul, _i, _j, _len, _len1, _ref, _ref1, _ref2, _ref3; + var arr, back, checked, description, dialog, favicon, fileInfo, hiddenNum, hiddenThreads, indicator, indicators, input, key, li, obj, overlay, ta, time, tr, ul, _i, _j, _len, _len1, _ref, _ref1, _ref2, _ref3; dialog = $.el('div', { id: 'options', className: 'reply dialog', @@ -2210,16 +2270,13 @@ \
File Info Formatting is disabled.
\ \
Unread Favicon is disabled.
\ Unread favicons
\ @@ -2274,16 +2331,13 @@ } (back = $('[name=backlink]', dialog)).value = Conf['backlink']; (time = $('[name=time]', dialog)).value = Conf['time']; - (fileInfoR = $('[name=fileInfoR]', dialog)).value = Conf['fileInfoR']; - (fileInfoT = $('[name=fileInfoT]', dialog)).value = Conf['fileInfoT']; + (fileInfo = $('[name=fileInfo]', dialog)).value = Conf['fileInfo']; $.on(back, 'keyup', $.cb.value); $.on(back, 'keyup', Options.backlink); $.on(time, 'keyup', $.cb.value); $.on(time, 'keyup', Options.time); - $.on(fileInfoR, 'keyup', $.cb.value); - $.on(fileInfoR, 'keyup', Options.fileInfo); - $.on(fileInfoT, 'keyup', $.cb.value); - $.on(fileInfoT, 'keyup', Options.fileInfo); + $.on(fileInfo, 'keyup', $.cb.value); + $.on(fileInfo, 'keyup', Options.fileInfo); favicon = $('select', dialog); favicon.value = Conf['favicon']; $.on(favicon, 'change', $.cb.value); @@ -2322,8 +2376,7 @@ d.body.style.setProperty('overflow', 'hidden', null); Options.backlink.call(back); Options.time.call(time); - Options.fileInfo.call(fileInfoR); - Options.fileInfo.call(fileInfoT); + Options.fileInfo.call(fileInfo); return Options.favicon.call(favicon); }, close: function() { @@ -2358,19 +2411,17 @@ return $.id('backlinkPreview').textContent = Conf['backlink'].replace(/%id/, '123456789'); }, fileInfo: function() { - var type; - type = this.name === 'fileInfoR' ? 0 : 1; FileInfo.data = { - link: '1329791824.png', - size: 996, + link: 'javascript:;', + spoiler: true, + size: '276', unit: 'KB', - resolution: '1366x768', - fullname: '[a.f.k.] Sayonara Zetsubou Sensei - 09.avi_snapshot_03.34_[2011.02.20_06.58.00].jpg', - shortname: '[a.f.k.] Sayonara Zetsubou Sen(...).jpg', - type: type + resolution: '1280x720', + fullname: 'd9bb2efc98dd0df141a94399ff5880b7.jpg', + shortname: 'd9bb2efc98dd0df141a94399ff5880(...).jpg' }; FileInfo.setFormats(); - return $.id("" + this.name + "Preview").innerHTML = FileInfo.funks[type](FileInfo); + return $.id('fileInfoPreview').innerHTML = FileInfo.funk(FileInfo); }, favicon: function() { Favicon["switch"](); @@ -2379,119 +2430,6 @@ } }; - Threading = { - op: function(node) { - var nodes, op; - nodes = []; - while (node.nodeName !== 'BLOCKQUOTE') { - nodes.push(node); - node = node.nextSibling; - } - nodes.push(node); - node = node.nextSibling; - op = $.el('div', { - className: 'op' - }); - $.add(op, nodes); - op.id = $('input', op).name; - return $.before(node, op); - }, - thread: function(node) { - var div, nodes; - node = Threading.op(node); - if (g.REPLY) { - return; - } - nodes = []; - while (node.nodeName !== 'HR') { - nodes.push(node); - node = node.nextElementSibling; - } - div = $.el('div', { - className: 'thread' - }); - $.add(div, nodes); - $.before(node, div); - node = node.nextElementSibling; - if (!(node.align || node.nodeName === 'CENTER')) { - return Threading.thread(node); - } - } - }; - - ThreadHiding = { - init: function() { - var a, hiddenThreads, op, thread, _i, _len, _ref; - hiddenThreads = $.get("hiddenThreads/" + g.BOARD + "/", {}); - _ref = $$('.thread'); - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - thread = _ref[_i]; - op = $('.op', thread); - a = $.el('a', { - textContent: '[ - ]', - href: 'javascript:;' - }); - $.on(a, 'click', ThreadHiding.cb); - $.prepend(op, a); - if (op.id in hiddenThreads) { - ThreadHiding.hide(thread); - } - } - }, - cb: function() { - return ThreadHiding.toggle(this.parentNode.parentNode); - }, - toggle: function(thread) { - var hiddenThreads, id; - hiddenThreads = $.get("hiddenThreads/" + g.BOARD + "/", {}); - id = $('.op', thread).id; - if (thread.hidden || thread.firstChild.className === 'block') { - ThreadHiding.show(thread); - delete hiddenThreads[id]; - } else { - ThreadHiding.hide(thread); - hiddenThreads[id] = Date.now(); - } - return $.set("hiddenThreads/" + g.BOARD + "/", hiddenThreads); - }, - hide: function(thread) { - var a, div, name, num, op, span, text, trip, uid, _ref, _ref1; - if (!Conf['Show Stubs']) { - thread.hidden = true; - thread.nextSibling.hidden = true; - return; - } - if (thread.firstChild.className === 'block') { - return; - } - num = 0; - if (span = $('.omittedposts', thread)) { - num = Number(span.textContent.match(/\d+/)[0]); - } - num += $$('.op ~ table', thread).length; - text = num === 1 ? '1 reply' : "" + num + " replies"; - op = $('.op', thread); - name = $('.postername', op).textContent; - uid = ((_ref = $('.posteruid', op)) != null ? _ref.textContent : void 0) || ''; - trip = ((_ref1 = $('.postertrip', op)) != null ? _ref1.textContent : void 0) || ''; - a = $.el('a', { - innerHTML: "[ + ] " + name + " " + uid + " " + trip + " (" + text + ")", - href: 'javascript:;' - }); - $.on(a, 'click', ThreadHiding.cb); - div = $.el('div', { - className: 'block' - }); - $.add(div, a); - return $.prepend(thread, div); - }, - show: function(thread, id) { - $.rm($('.block', thread)); - thread.hidden = false; - return thread.nextSibling.hidden = false; - } - }; - Updater = { init: function() { var checkbox, checked, dialog, html, input, name, title, _i, _len, _ref; @@ -2507,7 +2445,7 @@ dialog = UI.dialog('updater', 'bottom: 0; right: 0;', html); this.count = $('#count', dialog); this.timer = $('#timer', dialog); - this.br = $('br[clear]'); + this.thread = $.id("t" + g.THREAD_ID); _ref = $$('input', dialog); for (_i = 0, _len = _ref.length; _i < _len; _i++) { input = _ref[_i]; @@ -2566,7 +2504,7 @@ }; }, update: function() { - var doc, id, newPosts, nodes, reply, scroll, _i, _len, _ref; + var count, doc, id, lastPost, nodes, reply, scroll, _i, _len, _ref; if (this.status === 404) { Updater.timer.textContent = ''; Updater.count.textContent = 404; @@ -2583,7 +2521,7 @@ return; } Updater.retryCoef = 10; - Updater.timer.textContent = '-' + Conf['Interval']; + Updater.timer.textContent = "-" + Conf['Interval']; /* Status Code 304: Not modified By sending the `If-Modified-Since` header we get a proper status code, and no response. @@ -2601,25 +2539,26 @@ Updater.lastModified = this.getResponseHeader('Last-Modified'); doc = d.implementation.createHTMLDocument(''); doc.documentElement.innerHTML = this.response; - id = $('input', Updater.br.previousElementSibling).name; + lastPost = Updater.thread.lastElementChild; + id = lastPost.id.slice(2); nodes = []; - _ref = $$('.reply', doc).reverse(); + _ref = $$('.replyContainer', doc).reverse(); for (_i = 0, _len = _ref.length; _i < _len; _i++) { reply = _ref[_i]; - if (reply.id <= id) { + if (reply.id.slice(2) <= id) { break; } - nodes.push(reply.parentNode.parentNode.parentNode); + nodes.push(reply); } - newPosts = nodes.length; - scroll = Conf['Scrolling'] && Updater.scrollBG() && newPosts && Updater.br.previousElementSibling.getBoundingClientRect().bottom - d.body.clientHeight < 25; + count = nodes.length; + scroll = Conf['Scrolling'] && Updater.scrollBG() && count && lastPost.getBoundingClientRect().bottom - d.documentElement.clientHeight < 25; if (Conf['Verbose']) { - Updater.count.textContent = "+" + newPosts; - Updater.count.className = newPosts ? 'new' : null; + Updater.count.textContent = "+" + count; + Updater.count.className = count ? 'new' : null; } - $.before(Updater.br, nodes.reverse()); + $.add(Updater.thread, nodes.reverse()); if (scroll) { - return Updater.br.previousSibling.scrollIntoView(); + return nodes[0].scrollIntoView(); } } }, @@ -2638,7 +2577,7 @@ }, retry: function() { this.count.textContent = 'Retry'; - this.count.className = ''; + this.count.className = null; return this.update(); }, update: function() { @@ -2660,13 +2599,13 @@ Watcher = { init: function() { - var favicon, html, input, inputs, _i, _len; + var favicon, html, input, _i, _len, _ref; html = '
Thread Watcher
'; this.dialog = UI.dialog('watcher', 'top: 50px; left: 0px;', html); $.add(d.body, this.dialog); - inputs = $$('.op > input'); - for (_i = 0, _len = inputs.length; _i < _len; _i++) { - input = inputs[_i]; + _ref = $$('.op input'); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + input = _ref[_i]; favicon = $.el('img', { className: 'favicon' }); @@ -2764,15 +2703,17 @@ return Main.callbacks.push(this.node); }, node: function(post) { - var name, node; - if (post["class"] === 'inline') { + var name, parent, trip; + if (post.isInlined && !post.isCrosspost) { return; } - name = $('.commentpostername, .postername', post.el); + name = $('.name', post.el); name.textContent = 'Anonymous'; - node = name.nextElementSibling; - if (node.className === 'postertrip' || node.nodeName === 'A') { - return $.rm(node); + if ((trip = name.nextElementSibling) && trip.className === 'postertrip') { + $.rm(trip); + } + if ((parent = name.parentNode).className === 'useremail' && !/^sage$/i.test(parent.pathname)) { + return $.replace(parent, name); } } }; @@ -2807,7 +2748,7 @@ case '$2': return "' + img.href + '"; case '$3': - return "' + img.firstChild.getAttribute('md5').replace(/\=*$/, '') + '"; + return "' + img.firstChild.dataset.md5.replace(/\=*$/, '') + '"; case '$4': return g.BOARD; } @@ -2827,7 +2768,7 @@ node: function(post) { var img, link, nodes, _i, _len, _ref; img = post.img; - if (post["class"] === 'inline' || !img) { + if (post.isInlined && !post.isCrosspost || !img) { return; } img = img.parentNode; @@ -2835,9 +2776,9 @@ _ref = Sauce.links; for (_i = 0, _len = _ref.length; _i < _len; _i++) { link = _ref[_i]; - nodes.push($.tn(' '), link(img)); + nodes.push($.tn('\u00A0'), link(img)); } - return $.add(post.filesize, nodes); + return $.add(post.fileInfo, nodes); } }; @@ -2848,11 +2789,10 @@ node: function(post) { var img; img = post.img; - if (!(img && /^Spoil/.test(img.alt)) || post["class"] === 'inline') { + if (!(img && /^Spoiler/.test(img.alt)) || post.isInlined && !post.isCrosspost) { return; } - img.removeAttribute('height'); - img.removeAttribute('width'); + img.removeAttribute('style'); return img.src = "//thumbs.4chan.org" + (img.parentNode.pathname.replace(/src(\/\d+).+$/, 'thumb$1s.jpg')); } }; @@ -2865,11 +2805,11 @@ if ($.isDST()) { chanOffset--; } - this.parse = Date.parse('10/11/11(Tue)18:53') === 1318351980000 ? function(node) { - return new Date(Date.parse(node.textContent) + chanOffset * $.HOUR); - } : function(node) { + this.parse = Date.parse('10/11/11(Tue)18:53') === 1318351980000 ? function(text) { + return new Date(Date.parse(text) + chanOffset * $.HOUR); + } : function(text) { var day, hour, min, month, year, _, _ref; - _ref = node.textContent.match(/(\d+)\/(\d+)\/(\d+)\(\w+\)(\d+):(\d+)/), _ = _ref[0], month = _ref[1], day = _ref[2], year = _ref[3], hour = _ref[4], min = _ref[5]; + _ref = text.match(/(\d+)\/(\d+)\/(\d+)\(\w+\)(\d+):(\d+)/), _ = _ref[0], month = _ref[1], day = _ref[2], year = _ref[3], hour = _ref[4], min = _ref[5]; year = "20" + year; month--; hour = chanOffset + Number(hour); @@ -2878,17 +2818,13 @@ return Main.callbacks.push(this.node); }, node: function(post) { - var node, time; - if (post["class"] === 'inline') { + var node; + if (post.isInlined && !post.isCrosspost) { return; } - node = $('.posttime', post.el) || $('span[id]', post.el).previousSibling; - Time.date = Time.parse(node); - time = $.el('time', { - textContent: ' ' + Time.funk(Time) + ' ' - }); - time.setAttribute('datetime', Time.date.toISOString()); - return $.replace(node, time); + node = $('.postInfo > .dateTime', post.el); + Time.date = Time.parse(node.textContent); + return node.textContent = Time.funk(Time); }, foo: function() { var code; @@ -2976,42 +2912,34 @@ return Main.callbacks.push(this.node); }, node: function(post) { - var data, link, node, regexp, resolution, size, span, unit, _, _ref; - if (post["class"] === 'inline' || !(node = post.filesize)) { + var alt, node, span; + if (post.isInlined && !post.isCrosspost || !post.fileInfo) { return; } - regexp = /^File: (<.+>)-\((?:Spoiler Image, )?([\d\.]+) (\w+), (\d+x\d+|PDF)/; - _ref = node.innerHTML.match(regexp), _ = _ref[0], link = _ref[1], size = _ref[2], unit = _ref[3], resolution = _ref[4]; - data = { - link: link, - size: size, - unit: unit, - resolution: resolution + node = post.fileInfo.firstElementChild; + alt = post.img.alt; + span = $('span', node); + FileInfo.data = { + link: post.img.parentNode.href, + spoiler: /^Spoiler/.test(alt), + size: alt.match(/\d+/)[0], + unit: alt.match(/\w+$/)[0], + resolution: span.previousSibling.textContent.match(/\d+x\d+|PDF/)[0], + fullname: span.title, + shortname: span.textContent }; - if (span = $('span', node)) { - data.fullname = span.title; - data.shortname = span.textContent; - } - data.type = +(!span); - FileInfo.data = data; - return node.innerHTML = FileInfo.funks[data.type](FileInfo); + return node.innerHTML = FileInfo.funk(FileInfo); }, setFormats: function() { - var code, format, funks, i, param, _i; - funks = []; - for (i = _i = 0; _i <= 1; i = ++_i) { - format = i ? Conf['fileInfoT'] : Conf['fileInfoR']; - param = i ? /%([BKlMrs])/g : /%([BKlLMnNrs])/g; - code = format.replace(param, function(s, c) { - if (c in FileInfo.formatters) { - return "' + f.formatters." + c + "() + '"; - } else { - return s; - } - }); - funks.push(Function('f', "return '" + code + "'")); - } - return this.funks = funks; + var code; + code = Conf['fileInfo'].replace(/%([BKlLMnNprs])/g, function(s, c) { + if (c in FileInfo.formatters) { + return "' + f.formatters." + c + "() + '"; + } else { + return s; + } + }); + return this.funk = Function('f', "return '" + code + "'"); }, convertUnit: function(unitT) { var i, size, unitF, units; @@ -3040,25 +2968,28 @@ }, formatters: { l: function() { - if (FileInfo.data.type === 0) { - return FileInfo.data.link.replace(/>\d+\.\w+" + (this.n()) + "<"); - } else { - return FileInfo.data.link; - } + return "" + (this.n()) + ""; }, L: function() { - return FileInfo.data.link.replace(/>\d+\.\w+" + FileInfo.data.fullname + "<"); + return "" + (this.N()) + ""; }, n: function() { if (FileInfo.data.fullname === FileInfo.data.shortname) { return FileInfo.data.fullname; } else { - return "" + FileInfo.data.fullname + "" + FileInfo.data.shortname + ""; + return "" + FileInfo.data.shortname + "" + FileInfo.data.fullname + ""; } }, N: function() { return FileInfo.data.fullname; }, + p: function() { + if (FileInfo.data.spoiler) { + return 'Spoiler, '; + } else { + return ''; + } + }, s: function() { return "" + FileInfo.data.size + " " + FileInfo.data.unit; }, @@ -3078,18 +3009,19 @@ }; GetTitle = function(thread) { - var el, span; - el = $('.filetitle', thread); + var el, op, span; + op = $('.op', thread); + el = $('.subject', op); if (!el.textContent) { - el = $('blockquote', thread); + el = $('blockquote', op); if (!el.textContent) { - el = $('.postername', thread); + el = $('.nameBlock', op); } } span = $.el('span', { innerHTML: el.innerHTML.replace(/
/g, ' ') }); - return "/" + g.BOARD + "/ - " + span.textContent; + return "/" + g.BOARD + "/ - " + (span.textContent.trim()); }; TitlePost = { @@ -3106,7 +3038,7 @@ return Main.callbacks.push(this.node); }, node: function(post) { - var a, container, el, link, qid, quote, quotes, root, _i, _len, _ref; + var a, container, el, link, qid, quote, quotes, _i, _len, _ref; if (post.isInlined) { return; } @@ -3114,17 +3046,17 @@ _ref = post.quotes; for (_i = 0, _len = _ref.length; _i < _len; _i++) { quote = _ref[_i]; - if (qid = quote.hash.slice(1)) { + if (qid = quote.hash.slice(2)) { quotes[qid] = true; } } a = $.el('a', { - href: "#" + post.id, - className: post.root.hidden ? 'filtered backlink' : 'backlink', + href: "#p" + post.id, + className: post.el.hidden ? 'filtered backlink' : 'backlink', textContent: QuoteBacklink.funk(post.id) }); for (qid in quotes) { - if (!(el = $.id(qid)) || el.className === 'op' && !Conf['OP Backlinks']) { + if (!(el = $.id("pi" + qid)) || !Conf['OP Backlinks'] && /\bop\b/.test(el.parentNode.className)) { continue; } link = a.cloneNode(true); @@ -3136,16 +3068,14 @@ } else { link.setAttribute('onclick', "replyhl('" + post.id + "');"); } - if (!((container = $('.container', el)) && container.parentNode === el)) { + if (!(container = $.id("blc" + qid))) { container = $.el('span', { - className: 'container' + className: 'container', + id: "blc" + qid }); - $.add(container, [$.tn(' '), link]); - root = $('.reportbutton', el) || $('span[id]', el); - $.after(root, container); - } else { - $.add(container, [$.tn(' '), link]); + $.add(el, container); } + $.add(container, [$.tn(' '), link]); } } }; @@ -3177,11 +3107,11 @@ return; } e.preventDefault(); - id = this.hash.slice(1); + id = this.hash.slice(2); if (/\binlined\b/.test(this.className)) { QuoteInline.rm(this, id); } else { - if ($.x("ancestor::*[@id='" + id + "']", this)) { + if ($.x("ancestor::div[contains(@id,'p" + id + "')]", this)) { return; } QuoteInline.add(this, id); @@ -3189,61 +3119,64 @@ return this.classList.toggle('inlined'); }, add: function(q, id) { - var el, i, inline, pathname, root, table, threadID; - root = q.parentNode.nodeName === 'FONT' ? q.parentNode : q.nextSibling ? q.nextSibling : q; - if (el = $.id(id)) { - inline = QuoteInline.table(id, el.innerHTML); - if ((i = Unread.replies.indexOf(el.parentNode.parentNode.parentNode)) !== -1) { + var clonePost, el, i, inline, pathname, root; + root = $.x('ancestor::*[parent::blockquote]', q); + if (el = $.id("p" + id)) { + if (/\bop\b/.test(el.className)) { + $.removeClass(el.parentNode, 'qphl'); + } else { + $.removeClass(el, 'qphl'); + } + clonePost = QuoteInline.clone(id, el); + if (/\bbacklink\b/.test(q.className)) { + $.after(q.parentNode, clonePost); + if (Conf['Forward Hiding']) { + $.addClass(el.parentNode, 'forwarded'); + ++el.dataset.forwarded || (el.dataset.forwarded = 1); + } + } else { + $.after(root, clonePost); + } + if ((i = Unread.replies.indexOf(el)) !== -1) { Unread.replies.splice(i, 1); Unread.update(true); } - if (/\bbacklink\b/.test(q.className)) { - $.after(q.parentNode, inline); - if (Conf['Forward Hiding']) { - table = $.x('ancestor::table', el); - $.addClass(table, 'forwarded'); - ++table.title || (table.title = 1); - } - return; - } - return $.after(root, inline); - } else { - inline = $.el('td', { - className: 'reply inline', - id: "i" + id, - innerHTML: "Loading " + id + "..." - }); - $.after(root, inline); - pathname = q.pathname; - threadID = pathname.split('/').pop(); - return $.cache(pathname, (function() { - return QuoteInline.parse(this, pathname, id, threadID, inline); - })); + return; } + inline = $.el('div', { + className: 'inline', + id: "i" + id, + textContent: "Loading " + id + "..." + }); + $.after(root, inline); + pathname = q.pathname; + return $.cache(pathname, function() { + return QuoteInline.parse(this, pathname, id, inline); + }); }, rm: function(q, id) { - var inlined, table, _i, _len, _ref; - table = $.x("following::*[@id='i" + id + "']", q); - $.rm(table); + var div, inlined, _i, _len, _ref; + div = $.x("following::div[@id='i_pc" + id + "']", q); + $.rm(div); if (!Conf['Forward Hiding']) { return; } - _ref = $$('.backlink.inlined', table); + _ref = $$('.backlink.inlined', div); for (_i = 0, _len = _ref.length; _i < _len; _i++) { inlined = _ref[_i]; - table = $.x('ancestor::table', $.id(inlined.hash.slice(1))); - if (!--table.title) { - $.removeClass(table, 'forwarded'); + div = $.id(inlined.hash.slice(1)); + if (!--div.dataset.forwarded) { + $.removeClass(div.parentNode, 'forwarded'); } } if (/\bbacklink\b/.test(q.className)) { - table = $.x('ancestor::table', $.id(id)); - if (!--table.title) { - return $.removeClass(table, 'forwarded'); + div = $.id("p" + id); + if (!--div.dataset.forwarded) { + return $.removeClass(div.parentNode, 'forwarded'); } } }, - parse: function(req, pathname, id, threadID, inline) { + parse: function(req, pathname, id, inline) { var doc, href, link, newInline, node, quote, _i, _len, _ref; if (!inline.parentNode) { return; @@ -3254,29 +3187,36 @@ } doc = d.implementation.createHTMLDocument(''); doc.documentElement.innerHTML = req.response; - node = id === threadID ? Threading.op($('body > form', doc).firstChild) : doc.getElementById(id); - newInline = QuoteInline.table(id, node.innerHTML); + node = doc.getElementById("p" + id); + newInline = QuoteInline.clone(id, node); _ref = $$('.quotelink', newInline); for (_i = 0, _len = _ref.length; _i < _len; _i++) { quote = _ref[_i]; - if ((href = quote.getAttribute('href')) === quote.hash) { - quote.pathname = pathname; - } else if (!g.REPLY && href !== quote.href) { - quote.href = "res/" + href; + href = quote.getAttribute('href'); + if (href[0] === '/') { + continue; } + quote.href = "res/" + href; } - link = $('.quotejs', newInline); - link.href = "" + pathname + "#" + id; + link = $('.postInfo > .postNum > a:first-child', newInline); + link.href = "" + pathname + "#p" + id; link.nextSibling.href = "" + pathname + "#q" + id; - $.addClass(newInline, 'crossquote'); + $.addClass(newInline, 'crosspost'); return $.replace(inline, newInline); }, - table: function(id, html) { - return $.el('table', { - className: 'inline', - id: "i" + id, - innerHTML: "" + html + "" + clone: function(id, el) { + var clone, node, _i, _len, _ref; + clone = $.el('div', { + className: 'postContainer inline', + id: "i_pc" + id }); + $.add(clone, el.cloneNode(true)); + _ref = $$('[id]', clone); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + node = _ref[_i]; + node.id = "i_" + node.id; + } + return clone; } }; @@ -3300,36 +3240,38 @@ } }, mouseover: function(e) { - var el, id, node, qp, quote, replyID, threadID, _i, _len, _ref; + var el, id, qp, quote, replyID, _i, _len, _ref; if (/\binlined\b/.test(this.className)) { return; } qp = UI.el = $.el('div', { id: 'qp', - className: 'reply dialog' + className: 'reply dialog post' }); $.add(d.body, qp); - id = this.hash.slice(1); - if (el = $.id(id)) { + id = this.hash.slice(2); + if (el = $.id("p" + id)) { qp.innerHTML = el.innerHTML; if (Conf['Quote Highlighting']) { - $.addClass(el, 'qphl'); + if (/\bop\b/.test(el.className)) { + $.addClass(el.parentNode, 'qphl'); + } else { + $.addClass(el, 'qphl'); + } } - node = /\bbacklink\b/.test(this.className) ? this.parentNode : $.x('ancestor::blockquote', this); - replyID = $.x('preceding-sibling::input', node).name; + replyID = $.x('ancestor::div[contains(@class,"postContainer")]', this).id.slice(2); _ref = $$('.quotelink, .backlink', qp); for (_i = 0, _len = _ref.length; _i < _len; _i++) { quote = _ref[_i]; - if (quote.hash.slice(1) === replyID) { + if (quote.hash.slice(2) === replyID) { $.addClass(quote, 'forwardlink'); } } } else { qp.textContent = "Loading " + id + "..."; - threadID = this.pathname.split('/').pop() || $.x('ancestor::div[@class="thread"]', this).firstChild.id; - $.cache(this.pathname, (function() { - return QuotePreview.parse(this, id, threadID); - })); + $.cache(this.pathname, function() { + return QuotePreview.parse(this, id); + }); UI.hover(e); } $.on(this, 'mousemove', UI.hover); @@ -3337,15 +3279,19 @@ }, mouseout: function() { var el; - if (el = $.id(this.hash.slice(1))) { - $.removeClass(el, 'qphl'); - } UI.hoverend(); + if (el = $.id(this.hash.slice(1))) { + if (/\bop\b/.test(el.className)) { + $.removeClass(el.parentNode, 'qphl'); + } else { + $.removeClass(el, 'qphl'); + } + } $.off(this, 'mousemove', UI.hover); return $.off(this, 'mouseout click', QuotePreview.mouseout); }, - parse: function(req, id, threadID) { - var doc, node, post, qp; + parse: function(req, id) { + var doc, fileInfo, img, node, post, qp; if (!((qp = UI.el) && qp.textContent === ("Loading " + id + "..."))) { return; } @@ -3355,13 +3301,18 @@ } doc = d.implementation.createHTMLDocument(''); doc.documentElement.innerHTML = req.response; - node = id === threadID ? Threading.op($('body > form', doc).firstChild) : doc.getElementById(id); + node = doc.getElementById("p" + id); qp.innerHTML = node.innerHTML; post = { - root: qp, - filesize: $('.filesize', qp), - img: $('img[md5]', qp) + el: qp }; + if (fileInfo = $('.fileInfo', qp)) { + img = fileInfo.nextElementSibling.firstElementChild; + if (img.alt !== 'File deleted.') { + post.fileInfo = fileInfo; + post.img = img; + } + } if (Conf['Image Auto-Gif']) { AutoGif.node(post); } @@ -3380,13 +3331,13 @@ }, node: function(post) { var quote, _i, _len, _ref; - if (post["class"] === 'inline') { + if (post.isInlined && !post.isCrosspost) { return; } _ref = post.quotes; for (_i = 0, _len = _ref.length; _i < _len; _i++) { quote = _ref[_i]; - if (quote.hash.slice(1) === post.threadId) { + if (quote.hash.slice(2) === post.threadId) { $.add(quote, $.tn('\u00A0(OP)')); } } @@ -3399,7 +3350,7 @@ }, node: function(post) { var path, quote, _i, _len, _ref; - if (post["class"] === 'inline') { + if (post.isInlined && !post.isCrosspost) { return; } _ref = post.quotes; @@ -3422,10 +3373,10 @@ }, node: function(post) { var a, board, data, i, id, index, m, node, nodes, quote, quotes, snapshot, text, _i, _j, _len, _ref; - if (post["class"] === 'inline') { + if (post.isInlined && !post.isCrosspost) { return; } - snapshot = d.evaluate('.//text()[not(parent::a)]', post.el.lastChild, null, 6, null); + snapshot = d.evaluate('.//text()[not(parent::a)]', post.el.lastElementChild, null, 6, null); for (i = _i = 0, _ref = snapshot.snapshotLength; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) { node = snapshot.snapshotItem(i); data = node.data; @@ -3440,12 +3391,12 @@ nodes.push($.tn(text)); } id = quote.match(/\d+$/)[0]; - board = (m = quote.match(/^>>>\/([a-z\d]+)/)) ? m[1] : $('.quotejs', post.el).pathname.split('/')[1]; + board = (m = quote.match(/^>>>\/([a-z\d]+)/)) ? m[1] : $('.postInfo > .postNum > a:first-child', post.el).pathname.split('/')[1]; nodes.push(a = $.el('a', { textContent: "" + quote + "\u00A0(Dead)" })); if (board === g.BOARD && $.id(id)) { - a.href = "#" + id; + a.href = "#p" + id; a.className = 'quotelink'; a.setAttribute('onclick', "replyhl('" + id + "');"); } else { @@ -3466,7 +3417,7 @@ ReportButton = { init: function() { this.a = $.el('a', { - className: 'reportbutton', + className: 'report_button', innerHTML: '[ ! ]', href: 'javascript:;' }); @@ -3474,9 +3425,9 @@ }, node: function(post) { var a; - if (!(a = $('.reportbutton', post.el))) { + if (!(a = $('.report_button', post.el))) { a = ReportButton.a.cloneNode(true); - $.after($('span[id]', post.el), [$.tn(' '), a]); + $.add($('.postInfo', post.el), a); } return $.on(a, 'click', ReportButton.report); }, @@ -3522,7 +3473,7 @@ imgcount = $.id('imagecount'); imgcount.textContent = ++ThreadStats.images; if (ThreadStats.images > ThreadStats.imgLimit) { - return imgcount.className = 'warning'; + return $.addClass(imgcount, 'warning'); } } }; @@ -3537,20 +3488,21 @@ replies: [], foresee: [], node: function(post) { - var count, index; + var count, el, index; if ((index = Unread.foresee.indexOf(post.id)) !== -1) { Unread.foresee.splice(index, 1); return; } - if (post.root.hidden || post["class"]) { + el = post.el; + if (el.hidden || /\bop\b/.test(post["class"]) || post.isInlined) { return; } - count = Unread.replies.push(post.root); + count = Unread.replies.push(el); return Unread.update(count === 1); }, scroll: function() { var bottom, height, i, reply, _i, _len, _ref; - height = d.body.clientHeight; + height = d.documentElement.clientHeight; _ref = Unread.replies; for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) { reply = _ref[i]; @@ -3757,17 +3709,18 @@ return Main.callbacks.push(this.node); }, node: function(post) { - var img, src; - if (post.root.hidden || !post.img) { + var gif, img, src; + img = post.img; + if (post.el.hidden || !img) { return; } - src = post.img.parentNode.href; - if (/gif$/.test(src) && !/spoiler/.test(post.img.src)) { - img = $.el('img'); - $.on(img, 'load', function() { - return post.img.src = src; + src = img.parentNode.href; + if (/gif$/.test(src) && !/spoiler/.test(img.src)) { + gif = $.el('img'); + $.on(gif, 'load', function() { + return img.src = src; }); - return img.src = src; + return gif.src = src; } } }; @@ -3784,7 +3737,7 @@ } a = post.img.parentNode; $.on(a, 'click', ImageExpand.cb.toggle); - if (ImageExpand.on && !post.root.hidden && post["class"] !== 'inline') { + if (ImageExpand.on && !post.el.hidden) { return ImageExpand.expand(post.img); } }, @@ -3800,7 +3753,7 @@ var i, thumb, thumbs, _i, _j, _k, _len, _len1, _len2, _ref; ImageExpand.on = this.checked; if (ImageExpand.on) { - thumbs = $$('img[md5]'); + thumbs = $$('img[data-md5]'); if (Conf['Expand From Current']) { for (i = _i = 0, _len = thumbs.length; _i < _len; i = ++_i) { thumb = thumbs[i]; @@ -3815,7 +3768,7 @@ ImageExpand.expand(thumb); } } else { - _ref = $$('img[md5][hidden]'); + _ref = $$('img[data-md5][hidden]'); for (_k = 0, _len2 = _ref.length; _k < _len2; _k++) { thumb = _ref[_k]; ImageExpand.contract(thumb); @@ -3837,7 +3790,7 @@ case 'fit screen': klass = 'fitwidth fitheight'; } - $('body > form').className = klass; + $.id('delform').className = klass; if (/\bfitheight\b/.test(klass)) { $.on(window, 'resize', ImageExpand.resize); if (!ImageExpand.style) { @@ -3913,7 +3866,7 @@ }); }, dialog: function() { - var controls, form, imageType, select; + var controls, imageType, select; controls = $.el('div', { id: 'imgControls', innerHTML: "" @@ -3925,11 +3878,10 @@ $.on(select, 'change', $.cb.value); $.on(select, 'change', ImageExpand.cb.typeChange); $.on($('input', controls), 'click', ImageExpand.cb.all); - form = $('body > form'); - return $.prepend(form, controls); + return $.prepend($.id('delform'), controls); }, resize: function() { - return ImageExpand.style.textContent = ".fitheight img[md5] + img {max-height:" + d.body.clientHeight + "px;}"; + return ImageExpand.style.textContent = ".fitheight img[data-md5] + img {max-height:" + d.documentElement.clientHeight + "px;}"; } }; @@ -3948,7 +3900,6 @@ val = Conf[key]; Conf[key] = $.get(key, val); } - $.on(window, 'message', Main.message); switch (location.hostname) { case 'sys.4chan.org': if (/report/.test(location.search)) { @@ -3971,11 +3922,12 @@ } $.ready(Options.init); if (Conf['Quick Reply'] && Conf['Hide Original Post Form'] && g.BOARD !== 'f') { - Main.css += 'form[name=post] { display: none; }'; + Main.css += '#postForm { display: none; }'; } Main.addStyle(); now = Date.now(); if (Conf['Check for Updates'] && $.get('lastUpdate', 0) < now - 6 * $.HOUR) { + $.on(window, 'message', Main.message); $.ready(function() { return $.add(d.head, $.el('script', { src: 'https://raw.github.com/mayhemydg/4chan-x/master/latest.js' @@ -4058,7 +4010,7 @@ return $.ready(Main.ready); }, ready: function() { - var MutationObserver, form, nav, node, nodes, observer, _i, _j, _len, _len1, _ref, _ref1; + var MutationObserver, a, board, nav, node, nodes, observer, _i, _j, _len, _len1, _ref, _ref1; if (d.title === '4chan - 404') { Redirect.init(); return; @@ -4068,13 +4020,13 @@ } $.addClass(d.body, "chanx_" + (Main.version.split('.')[1])); $.addClass(d.body, $.engine); - _ref = ['navtop', 'navbot']; + _ref = ['boardNavDesktop', 'boardNavDesktopFoot']; for (_i = 0, _len = _ref.length; _i < _len; _i++) { nav = _ref[_i]; - $.addClass($("a[href$='/" + g.BOARD + "/']", $.id(nav)), 'current'); + if (a = $("a[href$='/" + g.BOARD + "/']", $.id(nav))) { + $.addClass(a, 'current'); + } } - form = $('form[name=delform]'); - Threading.thread(form.firstElementChild); Favicon.init(); if (Conf['Quick Reply']) { QR.init(); @@ -4114,9 +4066,7 @@ } } else { if (Conf['Thread Hiding']) { - setTimeout(function() { - return ThreadHiding.init(); - }); + ThreadHiding.init(); } if (Conf['Thread Expansion']) { setTimeout(function() { @@ -4134,8 +4084,9 @@ }); } } + board = $('.board'); nodes = []; - _ref1 = $$('.op, a + table', form); + _ref1 = $$('.postContainer', board); for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { node = _ref1[_j]; nodes.push(Main.preParse(node)); @@ -4143,12 +4094,12 @@ Main.node(nodes, true); if (MutationObserver = window.WebKitMutationObserver || window.MozMutationObserver || window.OMutationObserver || window.MutationObserver) { observer = new MutationObserver(Main.observer); - return observer.observe(form, { + return observer.observe(board, { childList: true, subtree: true }); } else { - return $.on(form, 'DOMNodeInserted', Main.listener); + return $.on(board, 'DOMNodeInserted', Main.listener); } }, flatten: function(parent, obj) { @@ -4180,20 +4131,29 @@ } }, preParse: function(node) { - var klass, post; - klass = node.className; + var el, fileInfo, img, post, rootClass; + rootClass = node.className; + el = $('.post', node); post = { root: node, - el: klass === 'op' ? node : node.firstChild.firstChild.lastChild, - "class": klass, - id: node.getElementsByTagName('input')[0].name, - threadId: g.THREAD_ID || $.x('ancestor::div[@class="thread"]', node).firstChild.id, - isInlined: /\binline\b/.test(klass), - filesize: node.getElementsByClassName('filesize')[0] || false, - quotes: node.getElementsByClassName('quotelink'), - backlinks: node.getElementsByClassName('backlink') + el: el, + "class": el.className, + id: el.id.slice(1), + threadId: g.THREAD_ID || $.x('ancestor::div[@class="thread"]', node).id.slice(1), + isInlined: /\binline\b/.test(rootClass), + isCrosspost: /\bcrosspost\b/.test(rootClass), + quotes: el.getElementsByClassName('quotelink'), + backlinks: el.getElementsByClassName('backlink'), + fileInfo: false, + img: false }; - post.img = post.filesize ? node.getElementsByTagName('img')[0] : false; + if (fileInfo = $('.fileInfo', el)) { + img = fileInfo.nextElementSibling.firstElementChild; + if (img.alt !== 'File deleted.') { + post.fileInfo = fileInfo; + post.img = img; + } + } return post; }, node: function(nodes, notify) { @@ -4221,7 +4181,7 @@ _ref = mutation.addedNodes; for (_j = 0, _len1 = _ref.length; _j < _len1; _j++) { addedNode = _ref[_j]; - if (addedNode.nodeName === 'TABLE') { + if (/\bpostContainer\b/.test(addedNode.className)) { nodes.push(Main.preParse(addedNode)); } } @@ -4233,7 +4193,7 @@ listener: function(e) { var target; target = e.target; - if (target.nodeName === 'TABLE') { + if (/\bpostContainer\b/.test(addedNode.className)) { return Main.node([Main.preParse(target)]); } }, @@ -4242,8 +4202,9 @@ callbacks: [], css: '\ /* dialog styling */\ -.dialog {\ +.dialog.reply {\ border: 1px solid rgba(0,0,0,.25);\ + padding: 0;\ }\ .move {\ cursor: move;\ @@ -4254,16 +4215,25 @@ label, .favicon {\ a[href="javascript:;"] {\ text-decoration: none;\ }\ -\ -.block ~ *,\ -#content > [name=tab]:not(:checked) + div,\ -#updater:not(:hover) > :not(.move),\ -#qp > input, #qp .inline, .forwarded {\ - display: none;\ +.warning {\ + color: red;\ }\ \ -.autohide:not(:hover) > form {\ - display: none;\ +.hide_thread_button {\ + float: left;\ +}\ +\ +.hidden_thread ~ *,\ +[hidden],\ +#content > [name=tab]:not(:checked) + div,\ +#updater:not(:hover) > :not(.move),\ +.autohide:not(:hover) > form,\ +#qp input, #qp .inline, .forwarded {\ + display: none !important;\ +}\ +\ +h1 {\ + text-align: center;\ }\ #qr > .move {\ min-width: 300px;\ @@ -4404,6 +4374,8 @@ a[href="javascript:;"] {\ }\ .field {\ border: 1px solid #CCC;\ + box-sizing: border-box;\ + -moz-box-sizing: border-box;\ color: #333;\ font: 13px sans-serif;\ margin: 0;\ @@ -4427,6 +4399,7 @@ textarea.field {\ min-height: 120px;\ }\ .field:only-child {\ + display: block;\ min-width: 100%;\ }\ .captcha {\ @@ -4436,6 +4409,7 @@ textarea.field {\ text-align: center;\ }\ .captcha > img {\ + display: block;\ height: 57px;\ width: 300px;\ }\ @@ -4450,41 +4424,21 @@ textarea.field {\ width: 30%;\ }\ \ -.new {\ - background: lime;\ -}\ -.warning {\ - color: red;\ -}\ -.replyhider {\ - vertical-align: top;\ -}\ -\ -.filesize + br + a {\ - float: left;\ - pointer-events: none;\ -}\ -.filename:hover > .fntrunc,\ -.filename:not(:hover) > .fnfull {\ +.fileText:hover .fntrunc,\ +.fileText:not(:hover) .fnfull {\ display: none;\ }\ -img[md5], img[md5] + img {\ - pointer-events: all;\ -}\ -.fitwidth img[md5] + img {\ +.fitwidth img[data-md5] + img {\ max-width: 100%;\ }\ -.gecko > .fitwidth img[md5] + img,\ -.presto > .fitwidth img[md5] + img {\ - width: 100%;\ -}\ +\ /* revealed spoilers do not have height/width,\ this fixes "expanded" auto-gifs */\ -img[md5] {\ +.op > div > a > img[data-md5] {\ max-height: 252px;\ max-width: 252px;\ }\ -input ~ a > img[md5] {\ +.reply > div > a > img[data-md5] {\ max-height: 127px;\ max-width: 127px;\ }\ @@ -4537,18 +4491,20 @@ input ~ a > img[md5] {\ #options label {\ text-decoration: underline;\ }\ -#content > div {\ +#content {\ height: 450px;\ overflow: auto;\ }\ #content textarea {\ + box-sizing: border-box;\ + -moz-box-sizing: border-box;\ margin: 0;\ min-height: 100px;\ resize: vertical;\ width: 100%;\ }\ #sauces {\ - height: 320px;\ + height: 300px;\ }\ \ #updater {\ @@ -4561,9 +4517,8 @@ input ~ a > img[md5] {\ border: none;\ background: transparent;\ }\ -\ -#stats {\ - border: none;\ +.new {\ + background: lime;\ }\ \ #watcher {\ @@ -4587,36 +4542,44 @@ input ~ a > img[md5] {\ text-decoration: underline;\ }\ \ -#qp {\ - padding-bottom: 5px;\ -}\ -#qp > a > img {\ +#qp img {\ max-height: 300px;\ max-width: 500px;\ }\ .qphl {\ outline: 2px solid rgba(216, 94, 49, .7);\ }\ +.qphl.opContainer {\ + outline-offset: -2px;\ +}\ +div.opContainer {\ + display: block !important;\ +}\ .inlined {\ opacity: .5;\ }\ -.inline .reply {\ +.inline {\ + overflow: hidden;\ background-color: rgba(255, 255, 255, 0.15);\ border: 1px solid rgba(128, 128, 128, 0.5);\ }\ -.filetitle, .replytitle, .postername, .commentpostername, .postertrip {\ +.inline .post {\ background: none;\ + border: none;\ }\ -.filter_highlight.op,\ -.filter_highlight > td[id] {\ +.filter_highlight.thread > .opContainer {\ + box-shadow: inset 5px 0 rgba(255,0,0,0.5);\ +}\ +.filter_highlight > .reply {\ box-shadow: -5px 0 rgba(255,0,0,0.5);\ }\ .filtered {\ - text-decoration: line-through;\ + text-decoration: underline line-through;\ }\ .quotelink.forwardlink,\ .backlink.forwardlink {\ - color: #4C4CA9;\ + text-decoration: none;\ + border-bottom: 1px dashed;\ }\ ' }; diff --git a/changelog b/changelog index 2d4031218..7803125da 100644 --- a/changelog +++ b/changelog @@ -1,5 +1,6 @@ master - Mayhem + Add Spoiler indicator option in File Info Formatting Remove archive.no-ip.org archive redirections. 2.29.5 diff --git a/script.coffee b/script.coffee index 5f63ae6f5..856e298f8 100644 --- a/script.coffee +++ b/script.coffee @@ -114,8 +114,7 @@ Config = ].join '\n' time: '%m/%d/%y(%a)%H:%M' backlink: '>>%id' - fileInfoR: '%l (%s, %r)' - fileInfoT: '%l (%s, %r)' + fileInfo: '%l (%p%s, %r)' favicon: 'ferongr' hotkeys: # QR & Options @@ -175,32 +174,29 @@ UI = d.addEventListener 'mouseup', UI.dragend, false #distance from pointer to el edge is constant; calculate it here. # XXX opera reports el.offsetLeft / el.offsetTop as 0 - rect = el.getBoundingClientRect() - UI.dx = e.clientX - rect.left - UI.dy = e.clientY - rect.top - #factor out el from document dimensions - UI.width = d.body.clientWidth - el.offsetWidth - UI.height = d.body.clientHeight - el.offsetHeight + rect = el.getBoundingClientRect() + UI.dx = e.clientX - rect.left + UI.dy = e.clientY - rect.top + UI.width = d.documentElement.clientWidth - rect.width + UI.height = d.documentElement.clientHeight - rect.height drag: (e) -> left = e.clientX - UI.dx - top = e.clientY - UI.dy + top = e.clientY - UI.dy left = - if left < 10 then 0 + if left < 10 then '0px' else if UI.width - left < 10 then null - else left + else left + 'px' top = - if top < 10 then 0 + if top < 10 then '0px' else if UI.height - top < 10 then null - else top - right = if left is null then 0 else null - bottom = if top is null then 0 else null + else top + 'px' #using null instead of '' is 4% faster #these 4 statements are 40% faster than 1 style.cssText {style} = UI.el - style.top = top - style.right = right - style.bottom = bottom style.left = left + style.top = top + style.right = if left is null then '0px' else null + style.bottom = if top is null then '0px' else null dragend: -> #$ coffee -bpe '{a} = {b} = c' #var a, b; @@ -211,26 +207,25 @@ UI = d.removeEventListener 'mouseup', UI.dragend, false hover: (e) -> {clientX, clientY} = e - {el} = UI - {style} = el - {clientHeight, clientWidth} = d.body - height = el.offsetHeight + {style} = UI.el + {clientHeight, clientWidth} = d.documentElement + height = UI.el.offsetHeight top = clientY - 120 style.top = if clientHeight <= height or top <= 0 - 0 + '0px' else if top + height >= clientHeight - clientHeight - height + clientHeight - height + 'px' else - top + top + 'px' if clientX <= clientWidth - 400 - style.left = clientX + 45 + style.left = clientX + 45 + 'px' style.right = null else style.left = null - style.right = clientWidth - clientX + 45 + style.right = clientWidth - clientX + 45 + 'px' hoverend: -> $.rm UI.el @@ -513,8 +508,8 @@ Filter = node: (post) -> return if post.isInlined - post.isOP = post.class is 'op' - {isOP, el} = post + isOP = post.id is post.threadId + {root} = post for key of Filter.filters value = Filter[key] post if value is false @@ -528,62 +523,57 @@ Filter = if result is true if isOP unless g.REPLY - ThreadHiding.hide post.el.parentNode + ThreadHiding.hide root.parentNode else continue else - ReplyHiding.hide post.root + ReplyHiding.hide root return # Highlight - if isOP - $.addClass el, result[0] - else - $.addClass el.parentNode, result[0] + $.addClass (if isOP then root.parentNode else root), result[0] if isOP and result[1] and not g.REPLY - # Put the highlighted OPs' threads on top of the board pages... - thisThread = el.parentNode + # Put the highlighted OPs' thread on top of the board page... + thisThread = root.parentNode # ...before the first non highlighted thread. - if firstThread = $ 'div[class=op]' - $.before firstThread.parentNode, [thisThread, thisThread.nextElementSibling] + if firstThread = $ 'div[class=thread]' + $.before firstThread, [thisThread, thisThread.nextElementSibling] name: (post) -> - name = if post.isOP then $ '.postername', post.el else $ '.commentpostername', post.el - name.textContent + $('.name', post.el).textContent uniqueid: (post) -> if uid = $ '.posteruid', post.el - return uid.textContent + return uid.textContent[5...-1] false tripcode: (post) -> if trip = $ '.postertrip', post.el return trip.textContent false mod: (post) -> - if mod = (if post.isOP then $ '.commentpostername', post.el else $ '.commentpostername ~ .commentpostername', post.el) + if mod = $ '.capcode', post.el return mod.textContent false email: (post) -> - if mail = $ '.linkmail', post.el - return mail.href + if mail = $ '.useremail', post.el + return mail.pathname false subject: (post) -> - sub = if post.isOP then $ '.filetitle', post.el else $ '.replytitle', post.el - sub.textContent + $('.subject', post.el).textContent or false comment: (post) -> text = [] # XPathResult.ORDERED_NODE_SNAPSHOT_TYPE is 7 - nodes = d.evaluate './/br|.//text()', post.el.lastChild, null, 7, null + nodes = d.evaluate './/br|.//text()', post.el.lastElementChild, null, 7, null for i in [0...nodes.snapshotLength] text.push if data = nodes.snapshotItem(i).data then data else '\n' text.join '' filename: (post) -> - {filesize} = post - if filesize and file = $ 'span', filesize + {fileInfo} = post + if fileInfo and file = $ '.fileText > span', fileInfo return file.title false dimensions: (post) -> - {filesize} = post - if filesize and match = filesize.textContent.match /\d+x\d+/ + {fileInfo} = post + if fileInfo and match = fileInfo.textContent.match /\d+x\d+/ return match[0] false filesize: (post) -> @@ -594,7 +584,7 @@ Filter = md5: (post) -> {img} = post if img - return img.getAttribute 'md5' + return img.dataset.md5 false StrikethroughQuotes = @@ -603,23 +593,22 @@ StrikethroughQuotes = node: (post) -> return if post.isInlined for quote in post.quotes - if (el = $.id quote.hash[1..]) and el.parentNode.parentNode.parentNode.hidden + if (el = $.id quote.hash[1..]) and el.hidden $.addClass quote, 'filtered' ReplyHiding.hide post.root if Conf['Recursive Filtering'] return ExpandComment = init: -> - for a in $$ '.abbr > a' - $.on a, 'click', ExpandComment.expand + for a in $$ '.abbr' + $.on a.firstElementChild, 'click', ExpandComment.expand return expand: (e) -> e.preventDefault() - [_, threadID, replyID] = @href.match /(\d+)#(\d+)/ + [_, threadID, replyID] = @href.match /(\d+)#p(\d+)/ @textContent = "Loading #{replyID}..." - threadID = @pathname.split('/').pop() or $.x('ancestor::div[@class="thread"]/div', @).id a = @ - $.cache @pathname, (-> ExpandComment.parse @, a, threadID, replyID) + $.cache @pathname, -> ExpandComment.parse @, a, threadID, replyID parse: (req, a, threadID, replyID) -> if req.status isnt 200 a.textContent = "#{req.status} #{req.statusText}" @@ -628,17 +617,15 @@ ExpandComment = doc = d.implementation.createHTMLDocument '' doc.documentElement.innerHTML = req.response - Threading.op $('body > form', doc).firstChild # Import the node to fix quote.hashes # as they're empty when in a different document. - node = d.importNode doc.getElementById replyID + node = d.importNode doc.getElementById "m#{replyID}" quotes = node.getElementsByClassName 'quotelink' for quote in quotes - if quote.hash is href = quote.getAttribute 'href' # Add pathname to in-thread quotes - quote.pathname = "/#{g.BOARD}/res/#{threadID}" - else if href isnt quote.href # Fix cross-thread links, not cross-board ones - quote.href = "res/#{href}" + href = quote.getAttribute 'href' + continue if href[0] is '/' # Cross-board quote + quote.href = "res/#{href}" # Fix pathnames post = el: node threadId: threadID @@ -654,35 +641,28 @@ ExpandComment = QuoteOP.node post if Conf['Indicate Cross-thread Quotes'] QuoteCT.node post - $.replace a.parentNode.parentNode, node.lastChild + $.replace a.parentNode.parentNode, node ExpandThread = init: -> - for span in $$ '.omittedposts' + for span in $$ '.summary' a = $.el 'a', textContent: "+ #{span.textContent}" - className: 'omittedposts' + className: 'summary desktop' href: 'javascript:;' - $.on a, 'click', ExpandThread.cb.toggle + $.on a, 'click', -> ExpandThread.toggle @parentNode $.replace span, a - cb: - toggle: -> - thread = @parentNode - ExpandThread.toggle thread - toggle: (thread) -> - threadID = thread.firstChild.id - pathname = "/#{g.BOARD}/res/#{threadID}" - a = $ '.omittedposts', thread + pathname = "/#{g.BOARD}/res/#{thread.id[1..]}" + a = $ '.summary', thread # \u00d7 is × switch a.textContent[0] when '+' - $('.op .container', thread)?.textContent = '' a.textContent = a.textContent.replace '+', '\u00d7 Loading...' - $.cache pathname, (-> ExpandThread.parse @, pathname, thread, a) + $.cache pathname, -> ExpandThread.parse @, thread, a when '\u00d7' a.textContent = a.textContent.replace '\u00d7 Loading...', '+' @@ -695,14 +675,15 @@ ExpandThread = when 'b', 'vg' then 3 when 't' then 1 else 5 - table = $.x "following::br[@clear]/preceding::table[#{num}]", a - while (prev = table.previousSibling) and (prev.nodeName isnt 'A') - $.rm prev - for backlink in $$ '.backlink', $ '.op', thread - $.rm backlink if !$.id backlink.hash[1..] + replies = $$ '.replyContainer', thread + replies.splice replies.length - num, num + for reply in replies + $.rm reply + for backlink in $$ '.backlink', a.previousElementSibling + $.rm backlink unless $.id backlink.hash[1..] + return - - parse: (req, pathname, thread, a) -> + parse: (req, thread, a) -> if req.status isnt 200 a.textContent = "#{req.status} #{req.statusText}" $.off a, 'click', ExpandThread.cb.toggle @@ -713,74 +694,138 @@ ExpandThread = doc = d.implementation.createHTMLDocument '' doc.documentElement.innerHTML = req.response - nodes = [] - for reply in $$ '.reply', doc - table = d.importNode reply.parentNode.parentNode.parentNode - for quote in $$ '.quotelink', table - if quote.hash is href = quote.getAttribute 'href' # Add pathname to in-thread quotes - quote.pathname = pathname - else if href isnt quote.href # Fix cross-thread links, not cross-board ones - quote.href = "res/#{href}" - link = $ '.quotejs', table - link.href = "res/#{thread.firstChild.id}##{reply.id}" - link.nextSibling.href = "res/#{thread.firstChild.id}#q#{reply.id}" - nodes.push table + threadID = thread.id[1..] + nodes = [] + for reply in $$ '.replyContainer', doc + reply = d.importNode reply + for quote in $$ '.quotelink', reply + href = quote.getAttribute 'href' + continue if href[0] is '/' # Cross-board quote + quote.href = "res/#{href}" # Fix pathnames + id = reply.id[2..] + link = $ '.postInfo > .postNum > a:first-child', reply + link.href = "res/#{threadID}#p#{id}" + link.nextSibling.href = "res/#{threadID}#q#{id}" + nodes.push reply # eat everything, then replace with fresh full posts - while (next = a.nextSibling) and not next.clear #br[clear] - $.rm next - $.before next, nodes + for post in $$ '.summary ~ .replyContainer', a.parentNode + $.rm post + for backlink in $$ '.backlink', a.previousElementSibling + $.rm backlink unless $.id backlink.hash[1..] + $.after a, nodes + +ThreadHiding = + init: -> + hiddenThreads = $.get "hiddenThreads/#{g.BOARD}/", {} + for thread in $$ '.thread' + a = $.el 'a', + className: 'hide_thread_button' + innerHTML: '[ - ]' + href: 'javascript:;' + $.on a, 'click', ThreadHiding.cb + $.prepend thread, a + + if thread.id[1..] of hiddenThreads + ThreadHiding.hide thread + return + + cb: -> + ThreadHiding.toggle @parentNode + + toggle: (thread) -> + hiddenThreads = $.get "hiddenThreads/#{g.BOARD}/", {} + id = thread.id[1..] + if thread.hidden or /\bhidden_thread\b/.test thread.firstChild.className + ThreadHiding.show thread + delete hiddenThreads[id] + else + ThreadHiding.hide thread + hiddenThreads[id] = Date.now() + $.set "hiddenThreads/#{g.BOARD}/", hiddenThreads + + hide: (thread) -> + unless Conf['Show Stubs'] + thread.hidden = true + thread.nextElementSibling.hidden = true + return + + return if thread.firstChild.className is 'block' # already hidden by filter + + num = 0 + if span = $ '.summary', thread + num = Number span.textContent.match /\d+/ + num += $$('.opContainer ~ .replyContainer', thread).length + text = if num is 1 then '1 reply' else "#{num} replies" + opInfo = $('.op > .postInfo > .nameBlock', thread).textContent + + a = $ '.hide_thread_button', thread + $.addClass a, 'hidden_thread' + a.firstChild.textContent = '[ + ]' + $.add a, $.tn " #{opInfo} (#{text})" + + show: (thread) -> + a = $ '.hide_thread_button', thread + $.removeClass a, 'hidden_thread' + a.innerHTML = '[ - ]' + thread.hidden = false + thread.nextElementSibling.hidden = false ReplyHiding = init: -> - @td = $.el 'td', - noWrap: true - className: 'replyhider' - innerHTML: '[ - ]' Main.callbacks.push @node node: (post) -> - return if post.class - td = ReplyHiding.td.cloneNode true - $.on td.firstChild, 'click', ReplyHiding.toggle - $.replace post.el.previousSibling, td + return if post.isInlined or /\bop\b/.test post.class + button = post.root.firstElementChild + $.addClass button, 'hide_reply_button' + button.innerHTML = '[ - ]' + $.on button.firstChild, 'click', ReplyHiding.toggle if post.id of g.hiddenReplies ReplyHiding.hide post.root toggle: -> - parent = @parentNode - if parent.className is 'replyhider' - ReplyHiding.hide parent.parentNode.parentNode.parentNode - id = parent.nextSibling.id - for quote in $$ ".quotelink[href='##{id}'], .backlink[href='##{id}']" - $.addClass quote, 'filtered' - g.hiddenReplies[id] = Date.now() - else - table = parent.nextSibling - table.hidden = false - $.rm parent - id = table.firstChild.firstChild.lastChild.id - for quote in $$ ".quotelink[href$='##{id}'], .backlink[href='##{id}']" + button = @parentNode + root = button.parentNode + id = root.id[2..] + quotes = $$ ".quotelink[href$='#p#{id}'], .backlink[href='#p#{id}']" + if /\bstub\b/.test button.className + ReplyHiding.show root + for quote in quotes $.removeClass quote, 'filtered' delete g.hiddenReplies[id] + else + ReplyHiding.hide root + for quote in quotes + $.addClass quote, 'filtered' + g.hiddenReplies[id] = Date.now() $.set "hiddenReplies/#{g.BOARD}/", g.hiddenReplies - hide: (table) -> - return if table.hidden # already hidden by filter - - table.hidden = true + hide: (root) -> + button = root.firstElementChild + return if button.hidden # already hidden once by filter + button.hidden = true + el = root.lastElementChild + el.hidden = true return unless Conf['Show Stubs'] - name = $('.commentpostername', table).textContent - uid = $('.posteruid', table)?.textContent or '' - trip = $('.postertrip', table)?.textContent or '' + stub = $.el 'div', + className: 'hide_reply_button stub' + innerHTML: '[ + ] ' + $.add stub.firstChild, $.tn $('.nameBlock', el).textContent + $.on stub.firstChild, 'click', ReplyHiding.toggle + $.after button, stub - div = $.el 'div', - className: 'stub' - innerHTML: "[ + ] #{name} #{uid} #{trip}" - $.on div.firstChild, 'click', ReplyHiding.toggle - $.before table, div + show: (root) -> + el = root.lastElementChild + button = root.firstElementChild + el.hidden = false + button.hidden = false + + return unless Conf['Show Stubs'] + + $.rm button.nextElementSibling Keybinds = init: -> @@ -839,11 +884,13 @@ Keybinds = Keybinds.img thread, true # Board Navigation when Conf.zero - window.location = "/#{g.BOARD}/0#0" + window.location = "/#{g.BOARD}/0#delform" when Conf.nextPage - $('input[value=Next]')?.click() + if link = $ 'link[rel=next]', d.head + window.location = link.href when Conf.previousPage - $('input[value=Previous]')?.click() + if link = $ 'link[rel=prev]', d.head + window.location.href = link.href # Thread Navigation when Conf.nextThread return if g.REPLY @@ -896,18 +943,18 @@ Keybinds = if all $.id('imageExpand').click() else - thumb = $ 'img[md5]', $('.replyhl', thread) or thread + thumb = $ 'img[data-md5]', $('.post.highlight', thread) or thread ImageExpand.toggle thumb.parentNode qr: (thread, quote) -> if quote - QR.quote.call $ '.quotejs + .quotejs', $('.replyhl', thread) or thread + QR.quote.call $ '.postInfo > .postNum > a[title="Quote this post"]', $('.post.highlight', thread) or thread else QR.open() $('textarea', QR.el).focus() open: (thread, tab) -> - id = thread.firstChild.id + id = thread.id[1..] url = "//boards.4chan.org/#{g.BOARD}/res/#{id}" if tab $.open url @@ -915,40 +962,36 @@ Keybinds = location.href = url hl: (delta, thread) -> - if td = $ '.replyhl', thread - td.className = 'reply' - td.removeAttribute 'tabindex' - rect = td.getBoundingClientRect() - if rect.bottom >= 0 and rect.top <= d.body.clientHeight # We're at least partially visible - next = - if delta is +1 - $.x 'following::td[@class="reply"]', td - else - $.x 'preceding::td[@class="reply"]', td + if post = $ '.reply.highlight', thread + $.removeClass post, 'highlight' + post.removeAttribute 'tabindex' + rect = post.getBoundingClientRect() + if rect.bottom >= 0 and rect.top <= d.documentElement.clientHeight # We're at least partially visible + next = $.x 'child::div[contains(@class,"post reply")]', + if delta is +1 then post.parentNode.nextElementSibling else post.parentNode.previousElementSibling unless next - td.className = 'replyhl' - td.tabIndex = 0 - td.focus() + @focus post return return unless g.REPLY or $.x('ancestor::div[@class="thread"]', next) is thread rect = next.getBoundingClientRect() - if rect.top < 0 or rect.bottom > d.body.clientHeight + if rect.top < 0 or rect.bottom > d.documentElement.clientHeight next.scrollIntoView delta is -1 - next.className = 'replyhl' - next.tabIndex = 0 - next.focus() + @focus next return replies = $$ '.reply', thread replies.reverse() if delta is -1 for reply in replies rect = reply.getBoundingClientRect() - if delta is +1 and rect.top >= 0 or delta is -1 and rect.bottom <= d.body.clientHeight - reply.className = 'replyhl' - reply.tabIndex = 0 - reply.focus() + if delta is +1 and rect.top >= 0 or delta is -1 and rect.bottom <= d.documentElement.clientHeight + @focus reply return + focus: (post) -> + $.addClass post, 'highlight' + post.tabIndex = 0 + post.focus() + Nav = # ◀ ▶ init: -> @@ -988,7 +1031,7 @@ Nav = if full return [thread, i, rect] return thread - return $ 'form[name=delform]' + return $ '.board' scroll: (delta) -> [thread, i, rect] = Nav.getThread true @@ -1014,9 +1057,9 @@ QR = link = $.el 'h1', innerHTML: "#{if g.REPLY then 'Quick Reply' else 'New Thread'}" $.on link.firstChild, 'click', -> QR.open() - $('select', QR.el).value = 'new' unless g.REPLY + $('select', QR.el).value = 'new' unless g.REPLY $('textarea', QR.el).focus() - $.before $('form[name=post]'), link + $.before $.id('postForm'), link # Prevent original captcha input from being focused on reload. script = $.el 'script', @@ -1032,7 +1075,7 @@ QR = $.on d, 'dragstart dragend', QR.drag node: (post) -> - $.on $('.quotejs + .quotejs', post.el), 'click', QR.quote + $.on $('.postInfo > .postNum > a[title="Quote this post"]', post.el), 'click', QR.quote open: -> if QR.el @@ -1076,12 +1119,12 @@ QR = $('.warning', QR.el).textContent = null status: (data={}) -> + return unless QR.el if g.dead value = 404 disabled = true QR.cooldown.auto = false value = QR.cooldown.seconds or data.progress or value - return unless QR.el {input} = QR.status input.value = if QR.cooldown.auto and Conf['Cooldown'] @@ -1115,14 +1158,13 @@ QR = e?.preventDefault() QR.open() unless g.REPLY - $('select', QR.el).value = $.x('ancestor::div[@class="thread"]', @).firstChild.id - + $('select', QR.el).value = $.x('ancestor::div[@class="thread"]', @).id[1..] # Make sure we get the correct number, even with XXX censors - id = @previousElementSibling.hash[1..] + id = @previousSibling.hash[2..] text = ">>#{id}\n" sel = window.getSelection() - if (s = sel.toString()) and id is $.x('ancestor-or-self::blockquote/preceding-sibling::input', sel.anchorNode)?.name + if (s = sel.toString()) and id is $.x('ancestor-or-self::blockquote', sel.anchorNode)?.id.match(/\d+$/)[0] s = s.replace /\n/g, '\n>' text += ">#{s}\n" @@ -1136,7 +1178,6 @@ QR = ta.value[...caretPos] + text + ta.value[ta.selectionEnd..] ta.focus() # Move the caret to the end of the new quote. - ta.selectionEnd = ta.selectionStart = caretPos + text.length range = caretPos + text.length ta.setSelectionRange range, range @@ -1357,7 +1398,7 @@ QR = @timeout = Date.now() + 26*$.MINUTE challenge = @challenge.firstChild.value @img.alt = challenge - @img.src = "http://www.google.com/recaptcha/api/image?c=#{challenge}" + @img.src = "//www.google.com/recaptcha/api/image?c=#{challenge}" @input.value = null count: (count) -> @input.placeholder = switch count @@ -1405,7 +1446,7 @@ QR = ta.style.cssText = $.get 'QR.size', '' # Allow only this board's supported files. - mimeTypes = $('.rules').firstElementChild.textContent.trim().match(/: (.+)/)[1].toLowerCase().replace /\w+/g, (type) -> + mimeTypes = $('ul.rules').firstElementChild.textContent.trim().match(/: (.+)/)[1].toLowerCase().replace /\w+/g, (type) -> switch type when 'jpg' 'image/jpeg' @@ -1416,19 +1457,20 @@ QR = QR.mimeTypes = mimeTypes.split ', ' # Add empty mimeType to avoid errors with URLs selected in Window's file dialog. QR.mimeTypes.push '' - fileInput = $ '[type=file]', QR.el - fileInput.max = $('[name=MAX_FILE_SIZE]').value + fileInput = $ 'input[type=file]', QR.el + fileInput.max = $('input[name=MAX_FILE_SIZE]').value fileInput.accept = mimeTypes - QR.spoiler = !!$ '#com_submit + label' + QR.spoiler = !!$ 'input[name=spoiler]' spoiler = $ '#spoilerLabel', QR.el spoiler.hidden = !QR.spoiler unless g.REPLY # Make a list with visible threads and an option to create a new one. threads = '' - for thread in $$ '.op' - threads += "" + for thread in $$ '.thread' + id = thread.id[1..] + threads += "" $.prepend $('.move > span', QR.el), $.el 'select' innerHTML: threads title: 'Create a new thread / Reply to a thread' @@ -1454,7 +1496,7 @@ QR = QR.selected[@name] = @value # Disable auto-posting if you're typing in the first reply # during the last 5 seconds of the cooldown. - if QR.cooldown.auto and QR.selected is QR.replies[0] and parseInt(QR.status.input.value.match /\d+/) < 6 + if QR.cooldown.auto and QR.selected is QR.replies[0] and 0 < QR.cooldown.seconds < 6 QR.cooldown.auto = false # sync between tabs $.sync 'QR.persona', (persona) -> @@ -1463,7 +1505,7 @@ QR = QR.selected[key] = val $("[name=#{key}]", QR.el).value = val - QR.status.input = $ '[type=submit]', QR.el + QR.status.input = $ 'input[type=submit]', QR.el QR.status() QR.cooldown.init() QR.captcha.init() @@ -1537,7 +1579,7 @@ QR = upfile: reply.file spoiler: reply.spoiler mode: 'regist' - pwd: if m = d.cookie.match(/4chan_pass=([^;]+)/) then decodeURIComponent m[1] else $('[name=pwd]').value + pwd: if m = d.cookie.match(/4chan_pass=([^;]+)/) then decodeURIComponent m[1] else $('input[name=pwd]').value recaptcha_challenge_field: challenge recaptcha_response_field: response + ' ' @@ -1566,7 +1608,7 @@ QR = # Uploading... QR.status progress: "#{Math.round e.loaded / e.total * 100}%" - QR.ajax = $.ajax $('form[name=post]').action, callbacks, opts + QR.ajax = $.ajax $.id('postForm').parentNode.action, callbacks, opts response: (html) -> doc = d.implementation.createHTMLDocument '' @@ -1730,16 +1772,13 @@ Options =
File Info Formatting is disabled.
Unread Favicon is disabled.
Unread favicons
@@ -1787,18 +1826,15 @@ Options = $.on ta, 'change', $.cb.value #rice - (back = $ '[name=backlink]', dialog).value = Conf['backlink'] - (time = $ '[name=time]', dialog).value = Conf['time'] - (fileInfoR = $ '[name=fileInfoR]', dialog).value = Conf['fileInfoR'] - (fileInfoT = $ '[name=fileInfoT]', dialog).value = Conf['fileInfoT'] + (back = $ '[name=backlink]', dialog).value = Conf['backlink'] + (time = $ '[name=time]', dialog).value = Conf['time'] + (fileInfo = $ '[name=fileInfo]', dialog).value = Conf['fileInfo'] $.on back, 'keyup', $.cb.value $.on back, 'keyup', Options.backlink $.on time, 'keyup', $.cb.value $.on time, 'keyup', Options.time - $.on fileInfoR, 'keyup', $.cb.value - $.on fileInfoR, 'keyup', Options.fileInfo - $.on fileInfoT, 'keyup', $.cb.value - $.on fileInfoT, 'keyup', Options.fileInfo + $.on fileInfo, 'keyup', $.cb.value + $.on fileInfo, 'keyup', Options.fileInfo favicon = $ 'select', dialog favicon.value = Conf['favicon'] $.on favicon, 'change', $.cb.value @@ -1831,8 +1867,7 @@ Options = Options.backlink.call back Options.time.call time - Options.fileInfo.call fileInfoR - Options.fileInfo.call fileInfoT + Options.fileInfo.call fileInfo Options.favicon.call favicon close: -> @@ -1860,118 +1895,21 @@ Options = backlink: -> $.id('backlinkPreview').textContent = Conf['backlink'].replace /%id/, '123456789' fileInfo: -> - type = if @name is 'fileInfoR' then 0 else 1 FileInfo.data = - link: '1329791824.png' - size: 996 + link: 'javascript:;' + spoiler: true + size: '276' unit: 'KB' - resolution: '1366x768' - fullname: '[a.f.k.] Sayonara Zetsubou Sensei - 09.avi_snapshot_03.34_[2011.02.20_06.58.00].jpg' - shortname: '[a.f.k.] Sayonara Zetsubou Sen(...).jpg' - type: type + resolution: '1280x720' + fullname: 'd9bb2efc98dd0df141a94399ff5880b7.jpg' + shortname: 'd9bb2efc98dd0df141a94399ff5880(...).jpg' FileInfo.setFormats() - $.id("#{@name}Preview").innerHTML = FileInfo.funks[type] FileInfo + $.id('fileInfoPreview').innerHTML = FileInfo.funk FileInfo favicon: -> Favicon.switch() Unread.update true @nextElementSibling.innerHTML = " " -Threading = - op: (node) -> - nodes = [] - until node.nodeName is 'BLOCKQUOTE' - nodes.push node - node = node.nextSibling - nodes.push node # Add the blockquote. - node = node.nextSibling - op = $.el 'div', - className: 'op' - $.add op, nodes - op.id = $('input', op).name - $.before node, op - - thread: (node) -> - node = Threading.op node - - return if g.REPLY - - nodes = [] - until node.nodeName is 'HR' - nodes.push node - node = node.nextElementSibling # Skip text nodes. - div = $.el 'div', - className: 'thread' - $.add div, nodes - $.before node, div - - node = node.nextElementSibling - # {N,}SFW - unless node.align or node.nodeName is 'CENTER' - Threading.thread node - -ThreadHiding = - init: -> - hiddenThreads = $.get "hiddenThreads/#{g.BOARD}/", {} - for thread in $$ '.thread' - op = $ '.op', thread - a = $.el 'a', - textContent: '[ - ]' - href: 'javascript:;' - $.on a, 'click', ThreadHiding.cb - $.prepend op, a - - if op.id of hiddenThreads - ThreadHiding.hide thread - return - - cb: -> - ThreadHiding.toggle @parentNode.parentNode - - toggle: (thread) -> - hiddenThreads = $.get "hiddenThreads/#{g.BOARD}/", {} - id = $('.op', thread).id - if thread.hidden or thread.firstChild.className is 'block' - ThreadHiding.show thread - delete hiddenThreads[id] - else - ThreadHiding.hide thread - hiddenThreads[id] = Date.now() - $.set "hiddenThreads/#{g.BOARD}/", hiddenThreads - - hide: (thread) -> - unless Conf['Show Stubs'] - thread.hidden = true - thread.nextSibling.hidden = true - return - - return if thread.firstChild.className is 'block' # already hidden by filter - - num = 0 - if span = $ '.omittedposts', thread - num = Number span.textContent.match(/\d+/)[0] - num += $$('.op ~ table', thread).length - text = if num is 1 then '1 reply' else "#{num} replies" - op = $ '.op', thread - name = $('.postername', op).textContent - uid = $('.posteruid', op)?.textContent or '' - trip = $('.postertrip', op)?.textContent or '' - - a = $.el 'a', - innerHTML: "[ + ] #{name} #{uid} #{trip} (#{text})" - href: 'javascript:;' - $.on a, 'click', ThreadHiding.cb - - div = $.el 'div', - className: 'block' - - $.add div, a - $.prepend thread, div - - show: (thread, id) -> - $.rm $ '.block', thread - thread.hidden = false - thread.nextSibling.hidden = false - Updater = init: -> html = "
-#{Conf['Interval']}
" @@ -1989,9 +1927,9 @@ Updater = dialog = UI.dialog 'updater', 'bottom: 0; right: 0;', html - @count = $ '#count', dialog - @timer = $ '#timer', dialog - @br = $ 'br[clear]' + @count = $ '#count', dialog + @timer = $ '#timer', dialog + @thread = $.id "t#{g.THREAD_ID}" for input in $$ 'input', dialog if input.type is 'checkbox' @@ -2055,7 +1993,7 @@ Updater = return Updater.retryCoef = 10 - Updater.timer.textContent = '-' + Conf['Interval'] + Updater.timer.textContent = "-#{Conf['Interval']}" ### Status Code 304: Not modified @@ -2066,29 +2004,30 @@ Updater = if @status is 304 if Conf['Verbose'] Updater.count.textContent = '+0' - Updater.count.className = null + Updater.count.className = null return Updater.lastModified = @getResponseHeader 'Last-Modified' doc = d.implementation.createHTMLDocument '' doc.documentElement.innerHTML = @response - id = $('input', Updater.br.previousElementSibling).name + lastPost = Updater.thread.lastElementChild + id = lastPost.id[2..] nodes = [] - for reply in $$('.reply', doc).reverse() - break if reply.id <= id #make sure to not insert older posts - nodes.push reply.parentNode.parentNode.parentNode #table + for reply in $$('.replyContainer', doc).reverse() + break if reply.id[2..] <= id #make sure to not insert older posts + nodes.push reply - newPosts = nodes.length - scroll = Conf['Scrolling'] && Updater.scrollBG() && newPosts && - Updater.br.previousElementSibling.getBoundingClientRect().bottom - d.body.clientHeight < 25 + count = nodes.length + scroll = Conf['Scrolling'] && Updater.scrollBG() && count && + lastPost.getBoundingClientRect().bottom - d.documentElement.clientHeight < 25 if Conf['Verbose'] - Updater.count.textContent = "+#{newPosts}" - Updater.count.className = if newPosts then 'new' else null + Updater.count.textContent = "+#{count}" + Updater.count.className = if count then 'new' else null - $.before Updater.br, nodes.reverse() + $.add Updater.thread, nodes.reverse() if scroll - Updater.br.previousSibling.scrollIntoView() + nodes[0].scrollIntoView() timeout: -> Updater.timeoutID = setTimeout Updater.timeout, 1000 @@ -2104,7 +2043,7 @@ Updater = retry: -> @count.textContent = 'Retry' - @count.className = '' + @count.className = null @update() update: -> @@ -2122,8 +2061,7 @@ Watcher = $.add d.body, @dialog #add watch buttons - inputs = $$ '.op > input' - for input in inputs + for input in $$ '.op input' favicon = $.el 'img', className: 'favicon' $.on favicon, 'click', @cb.toggle @@ -2202,12 +2140,13 @@ Anonymize = init: -> Main.callbacks.push @node node: (post) -> - return if post.class is 'inline' - name = $ '.commentpostername, .postername', post.el + return if post.isInlined and not post.isCrosspost + name = $ '.name', post.el name.textContent = 'Anonymous' - node = name.nextElementSibling - if node.className is 'postertrip' or node.nodeName is 'A' - $.rm node + if (trip = name.nextElementSibling) and trip.className is 'postertrip' + $.rm trip + if (parent = name.parentNode).className is 'useremail' and not /^sage$/i.test parent.pathname + $.replace parent, name Sauce = init: -> @@ -2228,7 +2167,7 @@ Sauce = when '$2' "' + img.href + '" when '$3' - "' + img.firstChild.getAttribute('md5').replace(/\=*$/, '') + '" + "' + img.firstChild.dataset.md5.replace(/\=*$/, '') + '" when '$4' g.BOARD href = Function 'img', "return '#{href}'" @@ -2242,23 +2181,23 @@ Sauce = node: (post) -> {img} = post - return if post.class is 'inline' or not img + return if post.isInlined and not post.isCrosspost or not img img = img.parentNode nodes = [] for link in Sauce.links - nodes.push $.tn(' '), link img - $.add post.filesize, nodes + # \u00A0 is nbsp + nodes.push $.tn('\u00A0'), link img + $.add post.fileInfo, nodes RevealSpoilers = init: -> Main.callbacks.push @node node: (post) -> {img} = post - if not (img and /^Spoil/.test img.alt) or post.class is 'inline' + if not (img and /^Spoiler/.test img.alt) or post.isInlined and not post.isCrosspost return - img.removeAttribute 'height' - img.removeAttribute 'width' - img.src = "//thumbs.4chan.org#{img.parentNode.pathname.replace(/src(\/\d+).+$/, 'thumb$1s.jpg')}" + img.removeAttribute 'style' + img.src = "//thumbs.4chan.org#{img.parentNode.pathname.replace /src(\/\d+).+$/, 'thumb$1s.jpg'}" Time = init: -> @@ -2271,11 +2210,11 @@ Time = @parse = if Date.parse('10/11/11(Tue)18:53') is 1318351980000 - (node) -> new Date Date.parse(node.textContent) + chanOffset*$.HOUR + (text) -> new Date Date.parse(text) + chanOffset*$.HOUR else # Firefox and Opera do not parse 4chan's time format correctly - (node) -> + (text) -> [_, month, day, year, hour, min] = - node.textContent.match /(\d+)\/(\d+)\/(\d+)\(\w+\)(\d+):(\d+)/ + text.match /(\d+)\/(\d+)\/(\d+)\(\w+\)(\d+):(\d+)/ year = "20#{year}" month-- # Months start at 0 hour = chanOffset + Number hour @@ -2283,15 +2222,10 @@ Time = Main.callbacks.push @node node: (post) -> - return if post.class is 'inline' - # .posttime exists on every board except /f/ - node = $('.posttime', post.el) or $('span[id]', post.el).previousSibling - Time.date = Time.parse node - time = $.el 'time', - textContent: ' ' + Time.funk(Time) + ' ' - # Set the datetime attribute, ISO'd. - time.setAttribute 'datetime', Time.date.toISOString() - $.replace node, time + return if post.isInlined and not post.isCrosspost + node = $ '.postInfo > .dateTime', post.el + Time.date = Time.parse node.textContent + node.textContent = Time.funk(Time) foo: -> code = Conf['time'].replace /%([A-Za-z])/g, (s, c) -> if c of Time.formatters @@ -2346,33 +2280,26 @@ FileInfo = @setFormats() Main.callbacks.push @node node: (post) -> - return if post.class is 'inline' or not node = post.filesize - regexp = /^File: (<.+>)-\((?:Spoiler Image, )?([\d\.]+) (\w+), (\d+x\d+|PDF)/ - [_, link, size, unit, resolution] = - node.innerHTML.match regexp - data = - link: link - size: size - unit: unit - resolution: resolution - if span = $ 'span', node - data.fullname = span.title - data.shortname = span.textContent - data.type = +!span - FileInfo.data = data - node.innerHTML = FileInfo.funks[data.type] FileInfo + return if post.isInlined and not post.isCrosspost or not post.fileInfo + node = post.fileInfo.firstElementChild + alt = post.img.alt + span = $ 'span', node + FileInfo.data = + link: post.img.parentNode.href + spoiler: /^Spoiler/.test alt + size: alt.match(/\d+/)[0] + unit: alt.match(/\w+$/)[0] + resolution: span.previousSibling.textContent.match(/\d+x\d+|PDF/)[0] + fullname: span.title + shortname: span.textContent + node.innerHTML = FileInfo.funk FileInfo setFormats: -> - funks = [] - for i in [0..1] - format = if i then Conf['fileInfoT'] else Conf['fileInfoR'] - param = if i then /%([BKlMrs])/g else /%([BKlLMnNrs])/g - code = format.replace param, (s, c) -> - if c of FileInfo.formatters - "' + f.formatters.#{c}() + '" - else - s - funks.push Function 'f', "return '#{code}'" - @funks = funks + code = Conf['fileInfo'].replace /%([BKlLMnNprs])/g, (s, c) -> + if c of FileInfo.formatters + "' + f.formatters.#{c}() + '" + else + s + @funk = Function 'f', "return '#{code}'" convertUnit: (unitT) -> size = @data.size unitF = @data.unit @@ -2388,18 +2315,15 @@ FileInfo = size = size.toFixed 2 "#{size} #{unitT}" formatters: - l: -> - if FileInfo.data.type is 0 - FileInfo.data.link.replace />\d+\.\w+#{@n()}<" - else - FileInfo.data.link - L: -> FileInfo.data.link.replace />\d+\.\w+#{FileInfo.data.fullname}<" + l: -> "#{@n()}" + L: -> "#{@N()}" n: -> if FileInfo.data.fullname is FileInfo.data.shortname FileInfo.data.fullname else - "#{FileInfo.data.fullname}#{FileInfo.data.shortname}" + "#{FileInfo.data.shortname}#{FileInfo.data.fullname}" N: -> FileInfo.data.fullname + p: -> if FileInfo.data.spoiler then 'Spoiler, ' else '' s: -> "#{FileInfo.data.size} #{FileInfo.data.unit}" B: -> FileInfo.convertUnit 'B' K: -> FileInfo.convertUnit 'KB' @@ -2407,13 +2331,14 @@ FileInfo = r: -> FileInfo.data.resolution GetTitle = (thread) -> - el = $ '.filetitle', thread - if not el.textContent - el = $ 'blockquote', thread - if not el.textContent - el = $ '.postername', thread + op = $ '.op', thread + el = $ '.subject', op + unless el.textContent + el = $ 'blockquote', op + unless el.textContent + el = $ '.nameBlock', op span = $.el 'span', innerHTML: el.innerHTML.replace /
/g, ' ' - "/#{g.BOARD}/ - #{span.textContent}" + "/#{g.BOARD}/ - #{span.textContent.trim()}" TitlePost = init: -> @@ -2429,16 +2354,16 @@ QuoteBacklink = quotes = {} for quote in post.quotes # Don't process >>>/b/. - if qid = quote.hash[1..] + if qid = quote.hash[2..] # Duplicate quotes get overwritten. quotes[qid] = true a = $.el 'a', - href: "##{post.id}" - className: if post.root.hidden then 'filtered backlink' else 'backlink' + href: "#p#{post.id}" + className: if post.el.hidden then 'filtered backlink' else 'backlink' textContent: QuoteBacklink.funk post.id for qid of quotes # Don't backlink the OP. - continue if !(el = $.id qid) or el.className is 'op' and !Conf['OP Backlinks'] + continue if !(el = $.id "pi#{qid}") or !Conf['OP Backlinks'] and /\bop\b/.test el.parentNode.className link = a.cloneNode true if Conf['Quote Preview'] $.on link, 'mouseover', QuotePreview.mouseover @@ -2446,13 +2371,12 @@ QuoteBacklink = $.on link, 'click', QuoteInline.toggle else link.setAttribute 'onclick', "replyhl('#{post.id}');" - unless (container = $ '.container', el) and container.parentNode is el - container = $.el 'span', className: 'container' - $.add container, [$.tn(' '), link] - root = $('.reportbutton', el) or $('span[id]', el) - $.after root, container - else - $.add container, [$.tn(' '), link] + unless container = $.id "blc#{qid}" + container = $.el 'span', + className: 'container' + id: "blc#{qid}" + $.add el, container + $.add container, [$.tn(' '), link] return QuoteInline = @@ -2469,53 +2393,56 @@ QuoteInline = toggle: (e) -> return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0 e.preventDefault() - id = @hash[1..] + id = @hash[2..] if /\binlined\b/.test @className QuoteInline.rm @, id else - return if $.x "ancestor::*[@id='#{id}']", @ + return if $.x "ancestor::div[contains(@id,'p#{id}')]", @ QuoteInline.add @, id @classList.toggle 'inlined' add: (q, id) -> - root = if q.parentNode.nodeName is 'FONT' then q.parentNode else if q.nextSibling then q.nextSibling else q - if el = $.id id - inline = QuoteInline.table id, el.innerHTML - if (i = Unread.replies.indexOf el.parentNode.parentNode.parentNode) isnt -1 + root = $.x 'ancestor::*[parent::blockquote]', q + if el = $.id "p#{id}" + if /\bop\b/.test el.className + $.removeClass el.parentNode, 'qphl' + else + $.removeClass el, 'qphl' + clonePost = QuoteInline.clone id, el + if /\bbacklink\b/.test q.className + $.after q.parentNode, clonePost + if Conf['Forward Hiding'] + $.addClass el.parentNode, 'forwarded' + # Will only unhide if there's no inlined backlinks of it anymore. + ++el.dataset.forwarded or el.dataset.forwarded = 1 + else + $.after root, clonePost + if (i = Unread.replies.indexOf el) isnt -1 Unread.replies.splice i, 1 Unread.update true - if /\bbacklink\b/.test q.className - $.after q.parentNode, inline - if Conf['Forward Hiding'] - table = $.x 'ancestor::table', el - $.addClass table, 'forwarded' - # Will only unhide if there's no inlined backlinks of it anymore. - ++table.title or table.title = 1 - return - $.after root, inline - else - inline = $.el 'td', - className: 'reply inline' - id: "i#{id}" - innerHTML: "Loading #{id}..." - $.after root, inline - {pathname} = q - threadID = pathname.split('/').pop() - $.cache pathname, (-> QuoteInline.parse @, pathname, id, threadID, inline) + return + + inline = $.el 'div', + className: 'inline' + id: "i#{id}" + textContent: "Loading #{id}..." + $.after root, inline + {pathname} = q + $.cache pathname, -> QuoteInline.parse @, pathname, id, inline rm: (q, id) -> - #select the corresponding table or loading td - table = $.x "following::*[@id='i#{id}']", q - $.rm table + # select the corresponding inlined quote or loading quote + div = $.x "following::div[@id='i_pc#{id}']", q + $.rm div return unless Conf['Forward Hiding'] - for inlined in $$ '.backlink.inlined', table - table = $.x 'ancestor::table', $.id inlined.hash[1..] - $.removeClass table, 'forwarded' unless --table.title + for inlined in $$ '.backlink.inlined', div + div = $.id inlined.hash[1..] + $.removeClass div.parentNode, 'forwarded' unless --div.dataset.forwarded if /\bbacklink\b/.test q.className - table = $.x 'ancestor::table', $.id id - $.removeClass table, 'forwarded' unless --table.title + div = $.id "p#{id}" + $.removeClass div.parentNode, 'forwarded' unless --div.dataset.forwarded - parse: (req, pathname, id, threadID, inline) -> + parse: (req, pathname, id, inline) -> return unless inline.parentNode if req.status isnt 200 @@ -2525,27 +2452,27 @@ QuoteInline = doc = d.implementation.createHTMLDocument '' doc.documentElement.innerHTML = req.response - node = - if id is threadID #OP - Threading.op $('body > form', doc).firstChild - else - doc.getElementById id - newInline = QuoteInline.table id, node.innerHTML + node = doc.getElementById "p#{id}" + newInline = QuoteInline.clone id, node for quote in $$ '.quotelink', newInline - if (href = quote.getAttribute 'href') is quote.hash #add pathname to normal quotes - quote.pathname = pathname - else if !g.REPLY and href isnt quote.href #fix x-thread links, not x-board ones - quote.href = "res/#{href}" - link = $ '.quotejs', newInline - link.href = "#{pathname}##{id}" + href = quote.getAttribute 'href' + continue if href[0] is '/' # Cross-board quote + quote.href = "res/#{href}" # Fix pathnames + link = $ '.postInfo > .postNum > a:first-child', newInline + link.href = "#{pathname}#p#{id}" link.nextSibling.href = "#{pathname}#q#{id}" - $.addClass newInline, 'crossquote' + $.addClass newInline, 'crosspost' $.replace inline, newInline - table: (id, html) -> - $.el 'table', - className: 'inline' - id: "i#{id}" - innerHTML: "#{html}" + + clone: (id, el) -> + clone = $.el 'div', + className: 'postContainer inline' + id: "i_pc#{id}" + $.add clone, el.cloneNode true + for node in $$ '[id]', clone + # Don't mess with other features + node.id = "i_#{node.id}" + clone QuotePreview = init: -> @@ -2560,36 +2487,37 @@ QuotePreview = return if /\binlined\b/.test @className qp = UI.el = $.el 'div', id: 'qp' - className: 'reply dialog' + className: 'reply dialog post' $.add d.body, qp - id = @hash[1..] - if el = $.id id + id = @hash[2..] + if el = $.id "p#{id}" qp.innerHTML = el.innerHTML - $.addClass el, 'qphl' if Conf['Quote Highlighting'] - node = - if /\bbacklink\b/.test @className - @parentNode + if Conf['Quote Highlighting'] + if /\bop\b/.test el.className + $.addClass el.parentNode, 'qphl' else - $.x 'ancestor::blockquote', @ - replyID = $.x('preceding-sibling::input', node).name + $.addClass el, 'qphl' + replyID = $.x('ancestor::div[contains(@class,"postContainer")]', @).id[2..] for quote in $$ '.quotelink, .backlink', qp - if quote.hash[1..] is replyID + if quote.hash[2..] is replyID $.addClass quote, 'forwardlink' else qp.textContent = "Loading #{id}..." - threadID = @pathname.split('/').pop() or $.x('ancestor::div[@class="thread"]', @).firstChild.id - $.cache @pathname, (-> QuotePreview.parse @, id, threadID) + $.cache @pathname, -> QuotePreview.parse @, id UI.hover e $.on @, 'mousemove', UI.hover $.on @, 'mouseout click', QuotePreview.mouseout mouseout: -> - if el = $.id @hash[1..] - $.removeClass el, 'qphl' UI.hoverend() + if el = $.id @hash[1..] + if /\bop\b/.test el.className + $.removeClass el.parentNode, 'qphl' + else + $.removeClass el, 'qphl' $.off @, 'mousemove', UI.hover $.off @, 'mouseout click', QuotePreview.mouseout - parse: (req, id, threadID) -> + parse: (req, id) -> return unless (qp = UI.el) and qp.textContent is "Loading #{id}..." if req.status isnt 200 @@ -2599,16 +2527,15 @@ QuotePreview = doc = d.implementation.createHTMLDocument '' doc.documentElement.innerHTML = req.response - node = - if id is threadID #OP - Threading.op $('body > form', doc).firstChild - else - doc.getElementById id + node = doc.getElementById "p#{id}" qp.innerHTML = node.innerHTML post = - root: qp - filesize: $ '.filesize', qp - img: $ 'img[md5]', qp + el: qp + if fileInfo = $ '.fileInfo', qp + img = fileInfo.nextElementSibling.firstElementChild + if img.alt isnt 'File deleted.' + post.fileInfo = fileInfo + post.img = img if Conf['Image Auto-Gif'] AutoGif.node post if Conf['Time Formatting'] @@ -2620,9 +2547,9 @@ QuoteOP = init: -> Main.callbacks.push @node node: (post) -> - return if post.class is 'inline' + return if post.isInlined and not post.isCrosspost for quote in post.quotes - if quote.hash[1..] is post.threadId + if quote.hash[2..] is post.threadId # \u00A0 is nbsp $.add quote, $.tn '\u00A0(OP)' return @@ -2631,7 +2558,7 @@ QuoteCT = init: -> Main.callbacks.push @node node: (post) -> - return if post.class is 'inline' + return if post.isInlined and not post.isCrosspost for quote in post.quotes unless quote.hash # Make sure this isn't a link to the board we're on. @@ -2647,11 +2574,11 @@ Quotify = init: -> Main.callbacks.push @node node: (post) -> - return if post.class is 'inline' + return if post.isInlined and not post.isCrosspost # XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE is 6 # Get all the text nodes that are not inside an anchor. - snapshot = d.evaluate './/text()[not(parent::a)]', post.el.lastChild, null, 6, null + snapshot = d.evaluate './/text()[not(parent::a)]', post.el.lastElementChild, null, 6, null for i in [0...snapshot.snapshotLength] node = snapshot.snapshotItem i @@ -2675,14 +2602,14 @@ Quotify = m[1] else # Get the post's board, whether it's inlined or not. - $('.quotejs', post.el).pathname.split('/')[1] + $('.postInfo > .postNum > a:first-child', post.el).pathname.split('/')[1] nodes.push a = $.el 'a', # \u00A0 is nbsp textContent: "#{quote}\u00A0(Dead)" if board is g.BOARD and $.id id - a.href = "##{id}" + a.href = "#p#{id}" a.className = 'quotelink' a.setAttribute 'onclick', "replyhl('#{id}');" else @@ -2702,14 +2629,14 @@ Quotify = ReportButton = init: -> @a = $.el 'a', - className: 'reportbutton' + className: 'report_button' innerHTML: '[ ! ]' href: 'javascript:;' Main.callbacks.push @node node: (post) -> - unless a = $ '.reportbutton', post.el + unless a = $ '.report_button', post.el a = ReportButton.a.cloneNode true - $.after $('span[id]', post.el), [$.tn(' '), a] + $.add $('.postInfo', post.el), a $.on a, 'click', ReportButton.report report: -> url = "//sys.4chan.org/#{g.BOARD}/imgboard.php?mode=report&no=#{$.x('preceding-sibling::input', @).name}" @@ -2739,7 +2666,7 @@ ThreadStats = imgcount = $.id 'imagecount' imgcount.textContent = ++ThreadStats.images if ThreadStats.images > ThreadStats.imgLimit - imgcount.className = 'warning' + $.addClass imgcount, 'warning' Unread = init: -> @@ -2755,12 +2682,13 @@ Unread = if (index = Unread.foresee.indexOf post.id) isnt -1 Unread.foresee.splice index, 1 return - return if post.root.hidden or post.class - count = Unread.replies.push post.root + {el} = post + return if el.hidden or /\bop\b/.test(post.class) or post.isInlined + count = Unread.replies.push el Unread.update count is 1 scroll: -> - height = d.body.clientHeight + height = d.documentElement.clientHeight for reply, i in Unread.replies {bottom} = reply.getBoundingClientRect() if bottom > height #post is not completely read @@ -2915,14 +2843,15 @@ AutoGif = return if g.BOARD is 'gif' Main.callbacks.push @node node: (post) -> - return if post.root.hidden or not post.img - src = post.img.parentNode.href - if /gif$/.test(src) and !/spoiler/.test post.img.src - img = $.el 'img' - $.on img, 'load', -> + {img} = post + return if post.el.hidden or not img + src = img.parentNode.href + if /gif$/.test(src) and !/spoiler/.test img.src + gif = $.el 'img' + $.on gif, 'load', -> # Replace the thumbnail once the GIF has finished loading. - post.img.src = src - img.src = src + img.src = src + gif.src = src ImageExpand = init: -> @@ -2933,7 +2862,7 @@ ImageExpand = return unless post.img a = post.img.parentNode $.on a, 'click', ImageExpand.cb.toggle - if ImageExpand.on and !post.root.hidden and post.class isnt 'inline' + if ImageExpand.on and !post.el.hidden ImageExpand.expand post.img cb: toggle: (e) -> @@ -2943,7 +2872,7 @@ ImageExpand = all: -> ImageExpand.on = @checked if ImageExpand.on #expand - thumbs = $$ 'img[md5]' + thumbs = $$ 'img[data-md5]' if Conf['Expand From Current'] for thumb, i in thumbs if thumb.getBoundingClientRect().top > 0 @@ -2952,7 +2881,7 @@ ImageExpand = for thumb in thumbs ImageExpand.expand thumb else #contract - for thumb in $$ 'img[md5][hidden]' + for thumb in $$ 'img[data-md5][hidden]' ImageExpand.contract thumb return typeChange: -> @@ -2965,7 +2894,7 @@ ImageExpand = klass = 'fitheight' when 'fit screen' klass = 'fitwidth fitheight' - $('body > form').className = klass + $.id('delform').className = klass if /\bfitheight\b/.test klass $.on window, 'resize', ImageExpand.resize unless ImageExpand.style @@ -3033,11 +2962,10 @@ ImageExpand = $.on select, 'change', ImageExpand.cb.typeChange $.on $('input', controls), 'click', ImageExpand.cb.all - form = $ 'body > form' - $.prepend form, controls + $.prepend $.id('delform'), controls resize: -> - ImageExpand.style.textContent = ".fitheight img[md5] + img {max-height:#{d.body.clientHeight}px;}" + ImageExpand.style.textContent = ".fitheight img[data-md5] + img {max-height:#{d.documentElement.clientHeight}px;}" Main = init: -> @@ -3050,12 +2978,10 @@ Main = g.REPLY = true g.THREAD_ID = pathname[2] - #load values from localStorage + # Load values from localStorage. for key, val of Conf Conf[key] = $.get key, val - $.on window, 'message', Main.message - switch location.hostname when 'sys.4chan.org' if /report/.test location.search @@ -3070,12 +2996,13 @@ Main = $.ready Options.init if Conf['Quick Reply'] and Conf['Hide Original Post Form'] and g.BOARD isnt 'f' - Main.css += 'form[name=post] { display: none; }' + Main.css += '#postForm { display: none; }' Main.addStyle() now = Date.now() if Conf['Check for Updates'] and $.get('lastUpdate', 0) < now - 6*$.HOUR + $.on window, 'message', Main.message $.ready -> $.add d.head, $.el 'script', src: 'https://raw.github.com/mayhemydg/4chan-x/master/latest.js' $.set 'lastUpdate', now @@ -3160,10 +3087,10 @@ Main = return $.addClass d.body, "chanx_#{Main.version.split('.')[1]}" $.addClass d.body, $.engine - for nav in ['navtop', 'navbot'] - $.addClass $("a[href$='/#{g.BOARD}/']", $.id nav), 'current' - form = $ 'form[name=delform]' - Threading.thread form.firstElementChild + for nav in ['boardNavDesktop', 'boardNavDesktopFoot'] + if a = $ "a[href$='/#{g.BOARD}/']", $.id nav + # Gotta make it work in temporary boards. + $.addClass a, 'current' Favicon.init() # Major features. @@ -3197,7 +3124,7 @@ Main = else #not reply if Conf['Thread Hiding'] - setTimeout -> ThreadHiding.init() + ThreadHiding.init() if Conf['Thread Expansion'] setTimeout -> ExpandThread.init() @@ -3208,18 +3135,19 @@ Main = if Conf['Index Navigation'] setTimeout -> Nav.init() + board = $ '.board' nodes = [] - for node in $$ '.op, a + table', form + for node in $$ '.postContainer', board nodes.push Main.preParse node Main.node nodes, true if MutationObserver = window.WebKitMutationObserver or window.MozMutationObserver or window.OMutationObserver or window.MutationObserver observer = new MutationObserver Main.observer - observer.observe form, + observer.observe board, childList: true subtree: true else - $.on form, 'DOMNodeInserted', Main.listener + $.on board, 'DOMNodeInserted', Main.listener flatten: (parent, obj) -> if obj instanceof Array @@ -3244,18 +3172,25 @@ Main = window.location = "https://raw.github.com/mayhemydg/4chan-x/#{version}/4chan_x.user.js" preParse: (node) -> - klass = node.className + rootClass = node.className + el = $ '.post', node post = - root: node - el: if klass is 'op' then node else node.firstChild.firstChild.lastChild - class: klass - id: node.getElementsByTagName('input')[0].name - threadId: g.THREAD_ID or $.x('ancestor::div[@class="thread"]', node).firstChild.id - isInlined: /\binline\b/.test klass - filesize: node.getElementsByClassName('filesize')[0] or false - quotes: node.getElementsByClassName 'quotelink' - backlinks: node.getElementsByClassName 'backlink' - post.img = if post.filesize then node.getElementsByTagName('img')[0] else false + root: node + el: el + class: el.className + id: el.id[1..] + threadId: g.THREAD_ID or $.x('ancestor::div[@class="thread"]', node).id[1..] + isInlined: /\binline\b/.test rootClass + isCrosspost: /\bcrosspost\b/.test rootClass + quotes: el.getElementsByClassName 'quotelink' + backlinks: el.getElementsByClassName 'backlink' + fileInfo: false + img: false + if fileInfo = $ '.fileInfo', el + img = fileInfo.nextElementSibling.firstElementChild + if img.alt isnt 'File deleted.' + post.fileInfo = fileInfo + post.img = img post node: (nodes, notify) -> for callback in Main.callbacks @@ -3268,19 +3203,22 @@ Main = nodes = [] for mutation in mutations for addedNode in mutation.addedNodes - nodes.push Main.preParse addedNode if addedNode.nodeName is 'TABLE' + if /\bpostContainer\b/.test addedNode.className + nodes.push Main.preParse addedNode Main.node nodes if nodes.length listener: (e) -> {target} = e - Main.node [Main.preParse target] if target.nodeName is 'TABLE' + if /\bpostContainer\b/.test addedNode.className + Main.node [Main.preParse target] namespace: '4chan_x.' version: '2.29.5' callbacks: [] css: ' /* dialog styling */ -.dialog { +.dialog.reply { border: 1px solid rgba(0,0,0,.25); + padding: 0; } .move { cursor: move; @@ -3291,16 +3229,25 @@ label, .favicon { a[href="javascript:;"] { text-decoration: none; } - -.block ~ *, -#content > [name=tab]:not(:checked) + div, -#updater:not(:hover) > :not(.move), -#qp > input, #qp .inline, .forwarded { - display: none; +.warning { + color: red; } -.autohide:not(:hover) > form { - display: none; +.hide_thread_button { + float: left; +} + +.hidden_thread ~ *, +[hidden], +#content > [name=tab]:not(:checked) + div, +#updater:not(:hover) > :not(.move), +.autohide:not(:hover) > form, +#qp input, #qp .inline, .forwarded { + display: none !important; +} + +h1 { + text-align: center; } #qr > .move { min-width: 300px; @@ -3441,6 +3388,8 @@ a[href="javascript:;"] { } .field { border: 1px solid #CCC; + box-sizing: border-box; + -moz-box-sizing: border-box; color: #333; font: 13px sans-serif; margin: 0; @@ -3464,6 +3413,7 @@ textarea.field { min-height: 120px; } .field:only-child { + display: block; min-width: 100%; } .captcha { @@ -3473,6 +3423,7 @@ textarea.field { text-align: center; } .captcha > img { + display: block; height: 57px; width: 300px; } @@ -3487,41 +3438,21 @@ textarea.field { width: 30%; } -.new { - background: lime; -} -.warning { - color: red; -} -.replyhider { - vertical-align: top; -} - -.filesize + br + a { - float: left; - pointer-events: none; -} -.filename:hover > .fntrunc, -.filename:not(:hover) > .fnfull { +.fileText:hover .fntrunc, +.fileText:not(:hover) .fnfull { display: none; } -img[md5], img[md5] + img { - pointer-events: all; -} -.fitwidth img[md5] + img { +.fitwidth img[data-md5] + img { max-width: 100%; } -.gecko > .fitwidth img[md5] + img, -.presto > .fitwidth img[md5] + img { - width: 100%; -} + /* revealed spoilers do not have height/width, this fixes "expanded" auto-gifs */ -img[md5] { +.op > div > a > img[data-md5] { max-height: 252px; max-width: 252px; } -input ~ a > img[md5] { +.reply > div > a > img[data-md5] { max-height: 127px; max-width: 127px; } @@ -3574,18 +3505,20 @@ input ~ a > img[md5] { #options label { text-decoration: underline; } -#content > div { +#content { height: 450px; overflow: auto; } #content textarea { + box-sizing: border-box; + -moz-box-sizing: border-box; margin: 0; min-height: 100px; resize: vertical; width: 100%; } #sauces { - height: 320px; + height: 300px; } #updater { @@ -3598,9 +3531,8 @@ input ~ a > img[md5] { border: none; background: transparent; } - -#stats { - border: none; +.new { + background: lime; } #watcher { @@ -3624,36 +3556,44 @@ input ~ a > img[md5] { text-decoration: underline; } -#qp { - padding-bottom: 5px; -} -#qp > a > img { +#qp img { max-height: 300px; max-width: 500px; } .qphl { outline: 2px solid rgba(216, 94, 49, .7); } +.qphl.opContainer { + outline-offset: -2px; +} +div.opContainer { + display: block !important; +} .inlined { opacity: .5; } -.inline .reply { +.inline { + overflow: hidden; background-color: rgba(255, 255, 255, 0.15); border: 1px solid rgba(128, 128, 128, 0.5); } -.filetitle, .replytitle, .postername, .commentpostername, .postertrip { +.inline .post { background: none; + border: none; } -.filter_highlight.op, -.filter_highlight > td[id] { +.filter_highlight.thread > .opContainer { + box-shadow: inset 5px 0 rgba(255,0,0,0.5); +} +.filter_highlight > .reply { box-shadow: -5px 0 rgba(255,0,0,0.5); } .filtered { - text-decoration: line-through; + text-decoration: underline line-through; } .quotelink.forwardlink, .backlink.forwardlink { - color: #4C4CA9; + text-decoration: none; + border-bottom: 1px dashed; } '