Merge branch 'menu'

This commit is contained in:
Nicolas Stepien 2012-07-04 21:54:56 +02:00
commit a63dad61b6
3 changed files with 900 additions and 143 deletions

View File

@ -77,7 +77,7 @@
*/ */
(function() { (function() {
var $, $$, Anonymize, AutoGif, Conf, Config, DeleteButton, ExpandComment, ExpandThread, Favicon, FileInfo, Filter, Get, 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; var $, $$, Anonymize, ArchiveLink, AutoGif, Conf, Config, DeleteLink, DownloadLink, ExpandComment, ExpandThread, Favicon, FileInfo, Filter, Get, ImageExpand, ImageHover, Keybinds, Main, Menu, Nav, Options, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, Quotify, Redirect, ReplyHiding, ReportLink, RevealSpoilers, Sauce, StrikethroughQuotes, ThreadHiding, ThreadStats, Time, TitlePost, UI, Unread, Updater, Watcher, d, g, _base;
Config = { Config = {
main: { main: {
@ -86,8 +86,6 @@
'Keybinds': [true, 'Binds actions to keys'], 'Keybinds': [true, 'Binds actions to keys'],
'Time Formatting': [true, 'Arbitrarily formatted timestamps, using your local time'], 'Time Formatting': [true, 'Arbitrarily formatted timestamps, using your local time'],
'File Info Formatting': [true, 'Reformats the file information'], 'File Info Formatting': [true, 'Reformats the file information'],
'Report Button': [true, 'Add report buttons'],
'Delete Button': [false, 'Add delete buttons'],
'Comment Expansion': [true, 'Expand too long comments'], 'Comment Expansion': [true, 'Expand too long comments'],
'Thread Expansion': [true, 'View all replies'], 'Thread Expansion': [true, 'View all replies'],
'Index Navigation': [true, 'Navigate to previous / next thread'], 'Index Navigation': [true, 'Navigate to previous / next thread'],
@ -110,6 +108,13 @@
'Reveal Spoilers': [false, 'Replace spoiler thumbnails by the original thumbnail'], 'Reveal Spoilers': [false, 'Replace spoiler thumbnails by the original thumbnail'],
'Expand From Current': [false, 'Expand images from current position to thread end.'] 'Expand From Current': [false, 'Expand images from current position to thread end.']
}, },
Menu: {
'Menu': [true, 'Add a drop-down menu in posts.'],
'Report Link': [true, 'Add a report link to the menu.'],
'Delete Link': [true, 'Add a delete link to the menu.'],
'Download Link': [true, 'Add a download with original filename link to the menu. Chrome-only currently.'],
'Archive Link': [true, 'Add an archive link to the menu.']
},
Monitoring: { Monitoring: {
'Thread Updater': [true, 'Update threads. Has more options in its own dialog.'], 'Thread Updater': [true, 'Update threads. Has more options in its own dialog.'],
'Unread Count': [true, 'Show unread post count in tab title'], 'Unread Count': [true, 'Show unread post count in tab title'],
@ -419,7 +424,7 @@
}, },
nodes: function(nodes) { nodes: function(nodes) {
var frag, node, _i, _len; var frag, node, _i, _len;
if (nodes instanceof Node) { if (!(nodes instanceof Array)) {
return nodes; return nodes;
} }
frag = d.createDocumentFragment(); frag = d.createDocumentFragment();
@ -712,8 +717,12 @@
filename: function(post) { filename: function(post) {
var file, fileInfo; var file, fileInfo;
fileInfo = post.fileInfo; fileInfo = post.fileInfo;
if (fileInfo && (file = $('.fileText > span', fileInfo))) { if (fileInfo) {
return file.title; if (file = $('.fileText > span', fileInfo)) {
return file.title;
} else {
return fileInfo.firstElementChild.dataset.filename;
}
} }
return false; return false;
}, },
@ -740,6 +749,74 @@
return img.dataset.md5; return img.dataset.md5;
} }
return false; return false;
},
menuInit: function() {
var div, entry, type, _i, _len, _ref;
div = $.el('div', {
textContent: 'Filter'
});
entry = {
el: div,
open: function() {
return true;
},
children: []
};
_ref = [['Name', 'name'], ['Unique ID', 'uniqueid'], ['Tripcode', 'tripcode'], ['Admin/Mod', 'mod'], ['E-mail', 'email'], ['Subject', 'subject'], ['Comment', 'comment'], ['Country', 'country'], ['Filename', 'filename'], ['Image dimensions', 'dimensions'], ['Filesize', 'filesize'], ['Image MD5', 'md5']];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
type = _ref[_i];
entry.children.push(Filter.createSubEntry(type[0], type[1]));
}
return Menu.addEntry(entry);
},
createSubEntry: function(text, type) {
var el, onclick, open;
el = $.el('a', {
href: 'javascript:;',
textContent: text
});
onclick = null;
open = function(post) {
var value;
value = Filter[type](post);
if (value === false) {
return false;
}
$.off(el, 'click', onclick);
onclick = function() {
var re, save, select, ta, tl;
re = type === 'md5' ? value : value.replace(/\/|\\|\^|\$|\n|\.|\(|\)|\{|\}|\[|\]|\?|\*|\+|\|/g, function(c) {
if (c === '\n') {
return '\\n';
} else if (c === '\\') {
return '\\\\';
} else {
return "\\" + c;
}
});
re = type === 'md5' ? "/" + value + "/" : "/^" + re + "$/";
if (/\bop\b/.test(post["class"])) {
re += ';op:yes';
}
save = (save = $.get(type, '')) ? "" + save + "\n" + re : re;
$.set(type, save);
Options.dialog();
select = $('select[name=filter]', $.id('options'));
select.value = type;
$.event(select, new Event('change'));
$.id('filter_tab').checked = true;
ta = select.nextElementSibling;
tl = ta.textLength;
ta.setSelectionRange(tl, tl);
return ta.focus();
};
$.on(el, 'click', onclick);
return true;
};
return {
el: el,
open: open
};
} }
}; };
@ -910,7 +987,7 @@
quote.href = "res/" + href; quote.href = "res/" + href;
} }
id = reply.id.slice(2); id = reply.id.slice(2);
link = $('.postInfo > .postNum > a[title="Highlight this post"]', reply); link = $('.postNum > a[title="Highlight this post"]', reply);
link.href = "res/" + threadID + "#p" + id; link.href = "res/" + threadID + "#p" + id;
link.nextSibling.href = "res/" + threadID + "#q" + id; link.nextSibling.href = "res/" + threadID + "#q" + id;
nodes.push(reply); nodes.push(reply);
@ -951,7 +1028,7 @@
} }
}, },
cb: function() { cb: function() {
return ThreadHiding.toggle(this.parentNode); return ThreadHiding.toggle($.x('ancestor::div[parent::div[@class="board"]]', this));
}, },
toggle: function(thread) { toggle: function(thread) {
var hiddenThreads, id; var hiddenThreads, id;
@ -967,7 +1044,7 @@
return $.set("hiddenThreads/" + g.BOARD + "/", hiddenThreads); return $.set("hiddenThreads/" + g.BOARD + "/", hiddenThreads);
}, },
hide: function(thread, show_stub) { hide: function(thread, show_stub) {
var a, num, opInfo, span, text; var a, menuButton, num, opInfo, span, stub, text;
if (show_stub == null) { if (show_stub == null) {
show_stub = Conf['Show Stubs']; show_stub = Conf['Show Stubs'];
} }
@ -986,19 +1063,24 @@
num += $$('.opContainer ~ .replyContainer', thread).length; num += $$('.opContainer ~ .replyContainer', thread).length;
text = num === 1 ? '1 reply' : "" + num + " replies"; text = num === 1 ? '1 reply' : "" + num + " replies";
opInfo = $('.op > .postInfo > .nameBlock', thread).textContent; opInfo = $('.op > .postInfo > .nameBlock', thread).textContent;
a = $.el('a', { stub = $.el('div', {
className: 'hide_thread_button hidden_thread', className: 'hide_thread_button hidden_thread',
innerHTML: '<span>[ + ]</span>', innerHTML: '<a href="javascript:;"><span>[ + ]</span> </a>'
href: 'javascript:;'
}); });
$.add(a, $.tn(" " + opInfo + " (" + text + ")")); a = stub.firstChild;
$.on(a, 'click', ThreadHiding.cb); $.on(a, 'click', ThreadHiding.cb);
return $.prepend(thread, a); $.add(a, $.tn("" + opInfo + " (" + text + ")"));
if (Conf['Menu']) {
menuButton = Menu.a.cloneNode(true);
$.on(menuButton, 'click', Menu.toggle);
$.add(stub, [$.tn(' '), menuButton]);
}
return $.prepend(thread, stub);
}, },
show: function(thread) { show: function(thread) {
var a; var stub;
if (a = $('.hidden_thread', thread)) { if (stub = $('.hidden_thread', thread)) {
$.rm(a); $.rm(stub);
} }
thread.hidden = false; thread.hidden = false;
return thread.nextElementSibling.hidden = false; return thread.nextElementSibling.hidden = false;
@ -1046,7 +1128,7 @@
return $.set("hiddenReplies/" + g.BOARD + "/", g.hiddenReplies); return $.set("hiddenReplies/" + g.BOARD + "/", g.hiddenReplies);
}, },
hide: function(root, show_stub) { hide: function(root, show_stub) {
var a, el, side, stub; var a, el, menuButton, side, stub;
if (show_stub == null) { if (show_stub == null) {
show_stub = Conf['Show Stubs']; show_stub = Conf['Show Stubs'];
} }
@ -1065,8 +1147,13 @@
innerHTML: '<a href="javascript:;"><span>[ + ]</span> </a>' innerHTML: '<a href="javascript:;"><span>[ + ]</span> </a>'
}); });
a = stub.firstChild; a = stub.firstChild;
$.add(a, $.tn($('.nameBlock', el).textContent));
$.on(a, 'click', ReplyHiding.toggle); $.on(a, 'click', ReplyHiding.toggle);
$.add(a, $.tn($('.nameBlock', el).textContent));
if (Conf['Menu']) {
menuButton = Menu.a.cloneNode(true);
$.on(menuButton, 'click', Menu.toggle);
$.add(stub, [$.tn(' '), menuButton]);
}
return $.prepend(root, stub); return $.prepend(root, stub);
}, },
show: function(root) { show: function(root) {
@ -1079,6 +1166,186 @@
} }
}; };
Menu = {
entries: [],
init: function() {
this.a = $.el('a', {
className: 'menu_button',
href: 'javascript:;',
innerHTML: '[<span></span>]'
});
this.el = $.el('div', {
className: 'reply dialog',
id: 'menu',
tabIndex: 0
});
$.on(this.el, 'click', function(e) {
return e.stopPropagation();
});
$.on(this.el, 'keydown', this.keybinds);
$.on(d, 'AddMenuEntry', function(e) {
return Menu.addEntry(e.detail);
});
return Main.callbacks.push(this.node);
},
node: function(post) {
var a;
if (post.isInlined && !post.isCrosspost) {
a = $('.menu_button', post.el);
} else {
a = Menu.a.cloneNode(true);
$.add($('.postInfo', post.el), a);
}
return $.on(a, 'click', Menu.toggle);
},
toggle: function(e) {
var lastOpener, post;
e.preventDefault();
e.stopPropagation();
if (Menu.el.parentNode) {
lastOpener = Menu.lastOpener;
Menu.close();
if (lastOpener === this) {
return;
}
}
Menu.lastOpener = this;
post = /\bhidden_thread\b/.test(this.parentNode.className) ? $.x('ancestor::div[parent::div[@class="board"]]/child::div[contains(@class,"opContainer")]', this) : $.x('ancestor::div[contains(@class,"postContainer")][1]', this);
return Menu.open(this, Main.preParse(post));
},
open: function(button, post) {
var bLeft, bRect, bTop, el, entry, funk, mRect, _i, _len, _ref;
el = Menu.el;
el.setAttribute('data-id', post.ID);
el.setAttribute('data-rootid', post.root.id);
funk = function(entry, parent) {
var child, children, subMenu, _i, _len;
children = entry.children;
if (!entry.open(post)) {
return;
}
$.add(parent, entry.el);
if (!children) {
return;
}
if (subMenu = $('.subMenu', entry.el)) {
$.rm(subMenu);
}
subMenu = $.el('div', {
className: 'reply dialog subMenu'
});
$.add(entry.el, subMenu);
for (_i = 0, _len = children.length; _i < _len; _i++) {
child = children[_i];
funk(child, subMenu);
}
};
_ref = Menu.entries;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
entry = _ref[_i];
funk(entry, el);
}
Menu.focus($('.entry', Menu.el));
$.on(d, 'click', Menu.close);
$.add(d.body, el);
mRect = el.getBoundingClientRect();
bRect = button.getBoundingClientRect();
bTop = d.documentElement.scrollTop + d.body.scrollTop + bRect.top;
bLeft = d.documentElement.scrollLeft + d.body.scrollLeft + bRect.left;
el.style.top = bRect.top + bRect.height + mRect.height < d.documentElement.clientHeight ? bTop + bRect.height + 2 + 'px' : bTop - mRect.height - 2 + 'px';
el.style.left = bRect.left + mRect.width < d.documentElement.clientWidth ? bLeft + 'px' : bLeft + bRect.width - mRect.width + 'px';
return el.focus();
},
close: function() {
var el, focused, _i, _len, _ref;
el = Menu.el;
$.rm(el);
_ref = $$('.focused.entry', el);
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
focused = _ref[_i];
$.rmClass(focused, 'focused');
}
el.innerHTML = null;
el.removeAttribute('style');
delete Menu.lastOpener;
delete Menu.focusedEntry;
return $.off(d, 'click', Menu.close);
},
keybinds: function(e) {
var el, next, subMenu;
el = Menu.focusedEntry;
switch (Keybinds.keyCode(e) || e.keyCode) {
case 'Esc':
Menu.lastOpener.focus();
Menu.close();
break;
case 13:
case 32:
el.click();
break;
case 'Up':
if (next = el.previousElementSibling) {
Menu.focus(next);
}
break;
case 'Down':
if (next = el.nextElementSibling) {
Menu.focus(next);
}
break;
case 'Right':
if ((subMenu = $('.subMenu', el)) && (next = subMenu.firstElementChild)) {
Menu.focus(next);
}
break;
case 'Left':
if (next = $.x('parent::*[contains(@class,"subMenu")]/parent::*', el)) {
Menu.focus(next);
}
break;
default:
return;
}
e.preventDefault();
return e.stopPropagation();
},
focus: function(el) {
var focused, _i, _len, _ref;
if (focused = $.x('parent::*/child::*[contains(@class,"focused")]', el)) {
$.rmClass(focused, 'focused');
}
_ref = $$('.focused', el);
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
focused = _ref[_i];
$.rmClass(focused, 'focused');
}
Menu.focusedEntry = el;
return $.addClass(el, 'focused');
},
addEntry: function(entry) {
var funk;
funk = function(entry) {
var child, children, el, _i, _len;
el = entry.el, children = entry.children;
$.addClass(el, 'entry');
$.on(el, 'focus mouseover', function(e) {
e.stopPropagation();
return Menu.focus(this);
});
if (!children) {
return;
}
$.addClass(el, 'hasSubMenu');
for (_i = 0, _len = children.length; _i < _len; _i++) {
child = children[_i];
funk(child);
}
};
funk(entry);
return Menu.entries.push(entry);
}
};
Keybinds = { Keybinds = {
init: function() { init: function() {
var node, _i, _len, _ref; var node, _i, _len, _ref;
@ -1300,7 +1567,7 @@
}, },
qr: function(thread, quote) { qr: function(thread, quote) {
if (quote) { if (quote) {
QR.quote.call($('.postInfo > .postNum > a[title="Quote this post"]', $('.post.highlight', thread) || thread)); QR.quote.call($('.postNum > a[title="Quote this post"]', $('.post.highlight', thread) || thread));
} else { } else {
QR.open(); QR.open();
} }
@ -1455,7 +1722,7 @@
return $.on(d, 'dragstart dragend', QR.drag); return $.on(d, 'dragstart dragend', QR.drag);
}, },
node: function(post) { node: function(post) {
return $.on($('.postInfo > .postNum > a[title="Quote this post"]', post.el), 'click', QR.quote); return $.on($('.postNum > a[title="Quote this post"]', post.el), 'click', QR.quote);
}, },
open: function() { open: function() {
if (QR.el) { if (QR.el) {
@ -3078,6 +3345,7 @@
fullname: span.title, fullname: span.title,
shortname: span.textContent shortname: span.textContent
}; };
node.setAttribute('data-filename', span.title);
return node.innerHTML = FileInfo.funk(FileInfo); return node.innerHTML = FileInfo.funk(FileInfo);
}, },
setFormats: function() { setFormats: function() {
@ -3211,7 +3479,7 @@
} }
quote.href = "/" + board + "/res/" + href; quote.href = "/" + board + "/res/" + href;
} }
link = $('.postInfo > .postNum > a[title="Highlight this post"]', pc); link = $('.postNum > a[title="Highlight this post"]', pc);
link.href = "/" + board + "/res/" + threadID + "#p" + postID; link.href = "/" + board + "/res/" + threadID + "#p" + postID;
link.nextSibling.href = "/" + board + "/res/" + threadID + "#q" + postID; link.nextSibling.href = "/" + board + "/res/" + threadID + "#q" + postID;
$.replace(root.firstChild, pc); $.replace(root.firstChild, pc);
@ -3369,13 +3637,13 @@
})); }));
$.after((isOP ? piM : pi), file); $.after((isOP ? piM : pi), file);
} }
$.replace(root.firstChild, pc); $.replace(root.firstChild, Get.cleanPost(pc));
if (cb) { if (cb) {
return cb(); return cb();
} }
}, },
cleanPost: function(root) { cleanPost: function(root) {
var child, el, inline, inlined, now, post, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1, _ref2, _ref3; var child, el, els, inline, inlined, now, post, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1, _ref2;
post = $('.post', root); post = $('.post', root);
_ref = Array.prototype.slice.call(root.childNodes); _ref = Array.prototype.slice.call(root.childNodes);
for (_i = 0, _len = _ref.length; _i < _len; _i++) { for (_i = 0, _len = _ref.length; _i < _len; _i++) {
@ -3395,9 +3663,10 @@
$.rmClass(inlined, 'inlined'); $.rmClass(inlined, 'inlined');
} }
now = Date.now(); now = Date.now();
_ref3 = $$('[id]', root); els = $$('[id]', root);
for (_l = 0, _len3 = _ref3.length; _l < _len3; _l++) { els.push(root);
el = _ref3[_l]; for (_l = 0, _len3 = els.length; _l < _len3; _l++) {
el = els[_l];
el.id = "" + now + "_" + el.id; el.id = "" + now + "_" + el.id;
} }
$.rmClass(root, 'forwarded'); $.rmClass(root, 'forwarded');
@ -3754,7 +4023,7 @@
nodes.push($.tn(text)); nodes.push($.tn(text));
} }
id = quote.match(/\d+$/)[0]; id = quote.match(/\d+$/)[0];
board = (m = quote.match(/^>>>\/([a-z\d]+)/)) ? m[1] : $('.postInfo > .postNum > a[title="Highlight this post"]', post.el).pathname.split('/')[1]; board = (m = quote.match(/^>>>\/([a-z\d]+)/)) ? m[1] : $('.postNum > a[title="Highlight this post"]', post.el).pathname.split('/')[1];
nodes.push(a = $.el('a', { nodes.push(a = $.el('a', {
textContent: "" + quote + "\u00A0(Dead)" textContent: "" + quote + "\u00A0(Dead)"
})); }));
@ -3782,46 +4051,44 @@
} }
}; };
DeleteButton = { DeleteLink = {
init: function() { init: function() {
this.a = $.el('a', { var a;
className: 'delete_button', a = $.el('a', {
innerHTML: '[&nbsp;&times;&nbsp;]', className: 'delete_link',
href: 'javascript:;' href: 'javascript:;'
}); });
return Main.callbacks.push(this.node); return Menu.addEntry({
}, el: a,
node: function(post) { open: function(post) {
var a; if (post.isArchived) {
if (!(a = $('.delete_button', post.el))) { return false;
a = DeleteButton.a.cloneNode(true); }
$.add($('.postInfo', post.el), a); a.textContent = 'Delete this post';
} $.on(a, 'click', DeleteLink["delete"]);
return $.on(a, 'click', DeleteButton["delete"]); return true;
}
});
}, },
"delete": function() { "delete": function() {
var board, form, id, m, pwd, self; var board, form, id, m, pwd, self;
$.off(this, 'click', DeleteButton["delete"]); $.off(this, 'click', DeleteLink["delete"]);
this.innerHTML = '[&nbsp;Deleting...&nbsp;]'; this.textContent = 'Deleting...';
if (m = d.cookie.match(/4chan_pass=([^;]+)/)) { pwd = (m = d.cookie.match(/4chan_pass=([^;]+)/)) ? decodeURIComponent(m[1]) : $.id('delPassword').value;
pwd = decodeURIComponent(m[1]); id = this.parentNode.dataset.id;
} else { board = $('.postNum > a[title="Highlight this post"]', $.id(this.parentNode.dataset.rootid)).pathname.split('/')[1];
pwd = $.id('delPassword').value;
}
id = $.x('preceding-sibling::input', this).name;
board = $.x('preceding-sibling::span[1]/a', this).pathname.match(/\w+/)[0];
self = this; self = this;
form = { form = {
mode: 'usrdel', mode: 'usrdel',
pwd: pwd pwd: pwd
}; };
form[id] = 'delete'; form[id] = 'delete';
return $.ajax("https://sys.4chan.org/" + board + "/imgboard.php", { return $.ajax($.id('delform').action.replace("/" + g.BOARD + "/", "/" + board + "/"), {
onload: function() { onload: function() {
return DeleteButton.load(self, this.response); return DeleteLink.load(self, this.response);
}, },
onerror: function() { onerror: function() {
return DeleteButton.error(self); return DeleteLink.error(self);
} }
}, { }, {
form: $.formData(form) form: $.formData(form)
@ -3835,44 +4102,93 @@
s = 'Banned!'; s = 'Banned!';
} else if (msg = doc.getElementById('errmsg')) { } else if (msg = doc.getElementById('errmsg')) {
s = msg.textContent; s = msg.textContent;
$.on(self, 'click', DeleteButton["delete"]); $.on(self, 'click', DeleteLink["delete"]);
} else { } else {
s = 'Deleted'; s = 'Deleted';
} }
return self.innerHTML = "[&nbsp;" + s + "&nbsp;]"; return self.textContent = s;
}, },
error: function(self) { error: function(self) {
self.innerHTML = '[&nbsp;Connection error, please retry.&nbsp;]'; self.textContent = 'Connection error, please retry.';
return $.on(self, 'click', DeleteButton["delete"]); return $.on(self, 'click', DeleteLink["delete"]);
} }
}; };
ReportButton = { ReportLink = {
init: function() { init: function() {
this.a = $.el('a', {
className: 'report_button',
innerHTML: '[&nbsp;!&nbsp;]',
href: 'javascript:;'
});
return Main.callbacks.push(this.node);
},
node: function(post) {
var a; var a;
if (!(a = $('.report_button', post.el))) { a = $.el('a', {
a = ReportButton.a.cloneNode(true); className: 'report_link',
$.add($('.postInfo', post.el), a); href: 'javascript:;',
} textContent: 'Report this post'
return $.on(a, 'click', ReportButton.report); });
$.on(a, 'click', this.report);
return Menu.addEntry({
el: a,
open: function(post) {
return post.isArchived === false;
}
});
}, },
report: function() { report: function() {
var id, set, url; var a, id, set, url;
url = "//sys.4chan.org/" + g.BOARD + "/imgboard.php?mode=report&no=" + ($.x('preceding-sibling::input', this).name); a = $('.postNum > a[title="Highlight this post"]', $.id(this.parentNode.dataset.rootid));
url = "//sys.4chan.org/" + (a.pathname.split('/')[1]) + "/imgboard.php?mode=report&no=" + this.parentNode.dataset.id;
id = Date.now(); id = Date.now();
set = "toolbar=0,scrollbars=0,location=0,status=1,menubar=0,resizable=1,width=685,height=200"; set = "toolbar=0,scrollbars=0,location=0,status=1,menubar=0,resizable=1,width=685,height=200";
return window.open(url, id, set); return window.open(url, id, set);
} }
}; };
DownloadLink = {
init: function() {
var a;
if ($.el('a').download === void 0) {
return;
}
a = $.el('a', {
className: 'download_link',
textContent: 'Download file'
});
return Menu.addEntry({
el: a,
open: function(post) {
var fileText;
if (!post.img) {
return false;
}
a.href = post.img.parentNode.href;
fileText = post.fileInfo.firstElementChild;
a.download = Conf['File Info Formatting'] ? fileText.dataset.filename : $('span', fileText).title;
return true;
}
});
}
};
ArchiveLink = {
init: function() {
var a;
a = $.el('a', {
className: 'archive_link',
target: '_blank',
textContent: 'Archived post'
});
return Menu.addEntry({
el: a,
open: function(post) {
var href, path;
path = $('.postNum > a[title="Highlight this post"]', post.el).pathname.split('/');
if ((href = Redirect.thread(path[1], path[3], post.ID)) === ("//boards.4chan.org/" + path[1] + "/")) {
return false;
}
a.href = href;
return true;
}
});
}
};
ThreadStats = { ThreadStats = {
init: function() { init: function() {
var dialog; var dialog;
@ -4531,11 +4847,23 @@
if (Conf['Image Hover']) { if (Conf['Image Hover']) {
ImageHover.init(); ImageHover.init();
} }
if (Conf['Report Button']) { if (Conf['Menu']) {
ReportButton.init(); Menu.init();
} if (Conf['Report Link']) {
if (Conf['Delete Button']) { ReportLink.init();
DeleteButton.init(); }
if (Conf['Delete Link']) {
DeleteLink.init();
}
if (Conf['Filter']) {
Filter.menuInit();
}
if (Conf['Download Link']) {
DownloadLink.init();
}
if (Conf['Archive Link']) {
ArchiveLink.init();
}
} }
if (Conf['Resurrect Quotes']) { if (Conf['Resurrect Quotes']) {
Quotify.init(); Quotify.init();
@ -4799,6 +5127,60 @@ a[href="javascript:;"] {\
display: none !important;\ display: none !important;\
}\ }\
\ \
.menu_button {\
display: inline-block;\
}\
.menu_button > span {\
border-top: .5em solid;\
border-right: .3em solid transparent;\
border-left: .3em solid transparent;\
display: inline-block;\
margin: 2px;\
vertical-align: middle;\
}\
#menu {\
position: absolute;\
outline: none;\
}\
.entry {\
border-bottom: 1px solid rgba(0, 0, 0, .25);\
cursor: pointer;\
display: block;\
outline: none;\
padding: 3px 7px;\
position: relative;\
text-decoration: none;\
white-space: nowrap;\
}\
.entry:last-child {\
border: none;\
}\
.focused.entry {\
background: rgba(255, 255, 255, .33);\
}\
.entry.hasSubMenu {\
padding-right: 1.5em;\
}\
.hasSubMenu::after {\
content: "";\
border-left: .5em solid;\
border-top: .3em solid transparent;\
border-bottom: .3em solid transparent;\
display: inline-block;\
margin: .3em;\
position: absolute;\
right: 3px;\
}\
.hasSubMenu:not(.focused) > .subMenu {\
display: none;\
}\
.subMenu {\
position: absolute;\
left: 100%;\
top: 0;\
margin-top: -1px;\
}\
\
h1 {\ h1 {\
text-align: center;\ text-align: center;\
}\ }\

View File

@ -1,5 +1,11 @@
master master
- Mayhem - Mayhem
New feature: Menu, which
- replaces and includes Report Button and Delete Button.
- add one-click Filter buttons.
- add download links to automatically save the file with its original filename. Chrome-only currently.
- add archive links.
- can integrate features from external userscripts/extensions, see https://github.com/MayhemYDG/4chan-x/wiki/Menu-API
The updater's refresh interval will now increase gradually in inactive threads. The updater's refresh interval will now increase gradually in inactive threads.
The updater's refresh interval is now limited to 5 seconds minimum. The updater's refresh interval is now limited to 5 seconds minimum.

View File

@ -5,8 +5,6 @@ Config =
'Keybinds': [true, 'Binds actions to keys'] 'Keybinds': [true, 'Binds actions to keys']
'Time Formatting': [true, 'Arbitrarily formatted timestamps, using your local time'] 'Time Formatting': [true, 'Arbitrarily formatted timestamps, using your local time']
'File Info Formatting': [true, 'Reformats the file information'] 'File Info Formatting': [true, 'Reformats the file information']
'Report Button': [true, 'Add report buttons']
'Delete Button': [false, 'Add delete buttons']
'Comment Expansion': [true, 'Expand too long comments'] 'Comment Expansion': [true, 'Expand too long comments']
'Thread Expansion': [true, 'View all replies'] 'Thread Expansion': [true, 'View all replies']
'Index Navigation': [true, 'Navigate to previous / next thread'] 'Index Navigation': [true, 'Navigate to previous / next thread']
@ -26,6 +24,12 @@ Config =
'Sauce': [true, 'Add sauce to images'] 'Sauce': [true, 'Add sauce to images']
'Reveal Spoilers': [false, 'Replace spoiler thumbnails by the original thumbnail'] 'Reveal Spoilers': [false, 'Replace spoiler thumbnails by the original thumbnail']
'Expand From Current': [false, 'Expand images from current position to thread end.'] 'Expand From Current': [false, 'Expand images from current position to thread end.']
Menu:
'Menu': [true, 'Add a drop-down menu in posts.']
'Report Link': [true, 'Add a report link to the menu.']
'Delete Link': [true, 'Add a delete link to the menu.']
'Download Link': [true, 'Add a download with original filename link to the menu. Chrome-only currently.']
'Archive Link': [true, 'Add an archive link to the menu.']
Monitoring: Monitoring:
'Thread Updater': [true, 'Update threads. Has more options in its own dialog.'] 'Thread Updater': [true, 'Update threads. Has more options in its own dialog.']
'Unread Count': [true, 'Show unread post count in tab title'] 'Unread Count': [true, 'Show unread post count in tab title']
@ -325,7 +329,10 @@ $.extend $,
tn: (s) -> tn: (s) ->
d.createTextNode s d.createTextNode s
nodes: (nodes) -> nodes: (nodes) ->
if nodes instanceof Node # In (at least) Chrome, elements created inside different
# scripts/window contexts inherit from unequal prototypes.
# window_ext1.Node !== window_ext2.Node
unless nodes instanceof Array
return nodes return nodes
frag = d.createDocumentFragment() frag = d.createDocumentFragment()
for node in nodes for node in nodes
@ -562,8 +569,11 @@ Filter =
false false
filename: (post) -> filename: (post) ->
{fileInfo} = post {fileInfo} = post
if fileInfo and file = $ '.fileText > span', fileInfo if fileInfo
return file.title if file = $ '.fileText > span', fileInfo
return file.title
else
return fileInfo.firstElementChild.dataset.filename
false false
dimensions: (post) -> dimensions: (post) ->
{fileInfo} = post {fileInfo} = post
@ -581,6 +591,99 @@ Filter =
return img.dataset.md5 return img.dataset.md5
false false
menuInit: ->
div = $.el 'div',
textContent: 'Filter'
entry =
el: div
open: -> true
children: []
for type in [
['Name', 'name']
['Unique ID', 'uniqueid']
['Tripcode', 'tripcode']
['Admin/Mod', 'mod']
['E-mail', 'email']
['Subject', 'subject']
['Comment', 'comment']
['Country', 'country']
['Filename', 'filename']
['Image dimensions', 'dimensions']
['Filesize', 'filesize']
['Image MD5', 'md5']
]
# Add a sub entry for each filter type.
entry.children.push Filter.createSubEntry type[0], type[1]
Menu.addEntry entry
createSubEntry: (text, type) ->
el = $.el 'a',
href: 'javascript:;'
textContent: text
# Define the onclick var outside of open's scope to $.off it properly.
onclick = null
open = (post) ->
value = Filter[type] post
return false if value is false
$.off el, 'click', onclick
onclick = ->
# Convert value -> regexp, unless type is md5
re = if type is 'md5' then value else value.replace ///
/
| \\
| \^
| \$
| \n
| \.
| \(
| \)
| \{
| \}
| \[
| \]
| \?
| \*
| \+
| \|
///g, (c) ->
if c is '\n'
'\\n'
else if c is '\\'
'\\\\'
else
"\\#{c}"
re =
if type is 'md5'
"/#{value}/"
else
"/^#{re}$/"
if /\bop\b/.test post.class
re += ';op:yes'
# Add a new line before the regexp unless the text is empty.
save = if save = $.get type, '' then "#{save}\n#{re}" else re
$.set type, save
# Open the options and display & focus the relevant filter textarea.
Options.dialog()
select = $ 'select[name=filter]', $.id 'options'
select.value = type
$.event select, new Event 'change'
$.id('filter_tab').checked = true
ta = select.nextElementSibling
tl = ta.textLength
ta.setSelectionRange tl, tl
ta.focus()
$.on el, 'click', onclick
true
return el: el, open: open
StrikethroughQuotes = StrikethroughQuotes =
init: -> init: ->
Main.callbacks.push @node Main.callbacks.push @node
@ -696,7 +799,7 @@ ExpandThread =
continue if href[0] is '/' # Cross-board quote continue if href[0] is '/' # Cross-board quote
quote.href = "res/#{href}" # Fix pathnames quote.href = "res/#{href}" # Fix pathnames
id = reply.id[2..] id = reply.id[2..]
link = $ '.postInfo > .postNum > a[title="Highlight this post"]', reply link = $ '.postNum > a[title="Highlight this post"]', reply
link.href = "res/#{threadID}#p#{id}" link.href = "res/#{threadID}#p#{id}"
link.nextSibling.href = "res/#{threadID}#q#{id}" link.nextSibling.href = "res/#{threadID}#q#{id}"
nodes.push reply nodes.push reply
@ -724,7 +827,7 @@ ThreadHiding =
return return
cb: -> cb: ->
ThreadHiding.toggle @parentNode ThreadHiding.toggle $.x 'ancestor::div[parent::div[@class="board"]]', @
toggle: (thread) -> toggle: (thread) ->
hiddenThreads = $.get "hiddenThreads/#{g.BOARD}/", {} hiddenThreads = $.get "hiddenThreads/#{g.BOARD}/", {}
@ -752,17 +855,21 @@ ThreadHiding =
text = if num is 1 then '1 reply' else "#{num} replies" text = if num is 1 then '1 reply' else "#{num} replies"
opInfo = $('.op > .postInfo > .nameBlock', thread).textContent opInfo = $('.op > .postInfo > .nameBlock', thread).textContent
a = $.el 'a', stub = $.el 'div',
className: 'hide_thread_button hidden_thread' className: 'hide_thread_button hidden_thread'
innerHTML: '<span>[ + ]</span>' innerHTML: '<a href="javascript:;"><span>[ + ]</span> </a>'
href: 'javascript:;' a = stub.firstChild
$.add a, $.tn " #{opInfo} (#{text})" $.on a, 'click', ThreadHiding.cb
$.on a, 'click', ThreadHiding.cb $.add a, $.tn "#{opInfo} (#{text})"
$.prepend thread, a if Conf['Menu']
menuButton = Menu.a.cloneNode true
$.on menuButton, 'click', Menu.toggle
$.add stub, [$.tn(' '), menuButton]
$.prepend thread, stub
show: (thread) -> show: (thread) ->
if a = $ '.hidden_thread', thread if stub = $ '.hidden_thread', thread
$.rm a $.rm stub
thread.hidden = false thread.hidden = false
thread.nextElementSibling.hidden = false thread.nextElementSibling.hidden = false
@ -810,8 +917,12 @@ ReplyHiding =
className: 'hide_reply_button stub' className: 'hide_reply_button stub'
innerHTML: '<a href="javascript:;"><span>[ + ]</span> </a>' innerHTML: '<a href="javascript:;"><span>[ + ]</span> </a>'
a = stub.firstChild a = stub.firstChild
$.add a, $.tn $('.nameBlock', el).textContent
$.on a, 'click', ReplyHiding.toggle $.on a, 'click', ReplyHiding.toggle
$.add a, $.tn $('.nameBlock', el).textContent
if Conf['Menu']
menuButton = Menu.a.cloneNode true
$.on menuButton, 'click', Menu.toggle
$.add stub, [$.tn(' '), menuButton]
$.prepend root, stub $.prepend root, stub
show: (root) -> show: (root) ->
@ -820,6 +931,155 @@ ReplyHiding =
$('.sideArrows', root).hidden = false $('.sideArrows', root).hidden = false
$('.post', root).hidden = false $('.post', root).hidden = false
Menu =
entries: []
init: ->
@a = $.el 'a',
className: 'menu_button'
href: 'javascript:;'
innerHTML: '[<span></span>]'
@el = $.el 'div',
className: 'reply dialog'
id: 'menu'
tabIndex: 0
$.on @el, 'click', (e) -> e.stopPropagation()
$.on @el, 'keydown', @keybinds
# Doc here: https://github.com/MayhemYDG/4chan-x/wiki/Menu-API
$.on d, 'AddMenuEntry', (e) -> Menu.addEntry e.detail
Main.callbacks.push @node
node: (post) ->
if post.isInlined and !post.isCrosspost
a = $ '.menu_button', post.el
else
a = Menu.a.cloneNode true
$.add $('.postInfo', post.el), a
$.on a, 'click', Menu.toggle
toggle: (e) ->
e.preventDefault()
e.stopPropagation()
if Menu.el.parentNode
# Close if it's already opened.
# Reopen if we clicked on another button.
{lastOpener} = Menu
Menu.close()
return if lastOpener is @
Menu.lastOpener = @
post =
if /\bhidden_thread\b/.test @parentNode.className
$.x 'ancestor::div[parent::div[@class="board"]]/child::div[contains(@class,"opContainer")]', @
else
$.x 'ancestor::div[contains(@class,"postContainer")][1]', @
Menu.open @, Main.preParse post
open: (button, post) ->
{el} = Menu
# XXX GM/Scriptish require setAttribute
el.setAttribute 'data-id', post.ID
el.setAttribute 'data-rootid', post.root.id
funk = (entry, parent) ->
{children} = entry
return unless entry.open post
$.add parent, entry.el
return unless children
if subMenu = $ '.subMenu', entry.el
# Reset sub menu, remove irrelevant entries.
$.rm subMenu
subMenu = $.el 'div',
className: 'reply dialog subMenu'
$.add entry.el, subMenu
for child in children
funk child, subMenu
return
for entry in Menu.entries
funk entry, el
Menu.focus $ '.entry', Menu.el
$.on d, 'click', Menu.close
$.add d.body, el
# Position
mRect = el.getBoundingClientRect()
bRect = button.getBoundingClientRect()
bTop = d.documentElement.scrollTop + d.body.scrollTop + bRect.top
bLeft = d.documentElement.scrollLeft + d.body.scrollLeft + bRect.left
el.style.top =
if bRect.top + bRect.height + mRect.height < d.documentElement.clientHeight
bTop + bRect.height + 2 + 'px'
else
bTop - mRect.height - 2 + 'px'
el.style.left =
if bRect.left + mRect.width < d.documentElement.clientWidth
bLeft + 'px'
else
bLeft + bRect.width - mRect.width + 'px'
el.focus()
close: ->
{el} = Menu
$.rm el
for focused in $$ '.focused.entry', el
$.rmClass focused, 'focused'
el.innerHTML = null
el.removeAttribute 'style'
delete Menu.lastOpener
delete Menu.focusedEntry
$.off d, 'click', Menu.close
keybinds: (e) ->
el = Menu.focusedEntry
switch Keybinds.keyCode(e) or e.keyCode
when 'Esc'
Menu.lastOpener.focus()
Menu.close()
when 13, 32 # 'Enter', 'Space'
el.click()
when 'Up'
if next = el.previousElementSibling
Menu.focus next
when 'Down'
if next = el.nextElementSibling
Menu.focus next
when 'Right'
if (subMenu = $ '.subMenu', el) and next = subMenu.firstElementChild
Menu.focus next
when 'Left'
if next = $.x 'parent::*[contains(@class,"subMenu")]/parent::*', el
Menu.focus next
else
return
e.preventDefault()
e.stopPropagation()
focus: (el) ->
if focused = $.x 'parent::*/child::*[contains(@class,"focused")]', el
$.rmClass focused, 'focused'
for focused in $$ '.focused', el
$.rmClass focused, 'focused'
Menu.focusedEntry = el
$.addClass el, 'focused'
addEntry: (entry) ->
funk = (entry) ->
{el, children} = entry
$.addClass el, 'entry'
$.on el, 'focus mouseover', (e) ->
e.stopPropagation()
Menu.focus @
return unless children
$.addClass el, 'hasSubMenu'
for child in children
funk child
return
funk entry
Menu.entries.push entry
Keybinds = Keybinds =
init: -> init: ->
for node in $$ '[accesskey]' for node in $$ '[accesskey]'
@ -951,7 +1211,7 @@ Keybinds =
qr: (thread, quote) -> qr: (thread, quote) ->
if quote if quote
QR.quote.call $ '.postInfo > .postNum > a[title="Quote this post"]', $('.post.highlight', thread) or thread QR.quote.call $ '.postNum > a[title="Quote this post"]', $('.post.highlight', thread) or thread
else else
QR.open() QR.open()
$('textarea', QR.el).focus() $('textarea', QR.el).focus()
@ -1072,7 +1332,7 @@ QR =
$.on d, 'dragstart dragend', QR.drag $.on d, 'dragstart dragend', QR.drag
node: (post) -> node: (post) ->
$.on $('.postInfo > .postNum > a[title="Quote this post"]', post.el), 'click', QR.quote $.on $('.postNum > a[title="Quote this post"]', post.el), 'click', QR.quote
open: -> open: ->
if QR.el if QR.el
@ -1358,9 +1618,9 @@ QR =
QR.characterCount.call $ 'textarea', QR.el QR.characterCount.call $ 'textarea', QR.el
$('#spoiler', QR.el).checked = @spoiler $('#spoiler', QR.el).checked = @spoiler
dragStart: -> dragStart: ->
$.addClass @, 'drag' $.addClass @, 'drag'
dragEnter: -> dragEnter: ->
$.addClass @, 'over' $.addClass @, 'over'
dragLeave: -> dragLeave: ->
$.rmClass @, 'over' $.rmClass @, 'over'
dragOver: (e) -> dragOver: (e) ->
@ -2390,6 +2650,8 @@ FileInfo =
resolution: span.previousSibling.textContent.match(/\d+x\d+|PDF/)[0] resolution: span.previousSibling.textContent.match(/\d+x\d+|PDF/)[0]
fullname: span.title fullname: span.title
shortname: span.textContent shortname: span.textContent
# XXX GM/Scriptish
node.setAttribute 'data-filename', span.title
node.innerHTML = FileInfo.funk FileInfo node.innerHTML = FileInfo.funk FileInfo
setFormats: -> setFormats: ->
code = Conf['fileInfo'].replace /%([BKlLMnNprs])/g, (s, c) -> code = Conf['fileInfo'].replace /%([BKlLMnNprs])/g, (s, c) ->
@ -2473,7 +2735,7 @@ Get =
href = quote.getAttribute 'href' href = quote.getAttribute 'href'
continue if href[0] is '/' # Cross-board quote, or board link continue if href[0] is '/' # Cross-board quote, or board link
quote.href = "/#{board}/res/#{href}" # Fix pathnames quote.href = "/#{board}/res/#{href}" # Fix pathnames
link = $ '.postInfo > .postNum > a[title="Highlight this post"]', pc link = $ '.postNum > a[title="Highlight this post"]', pc
link.href = "/#{board}/res/#{threadID}#p#{postID}" link.href = "/#{board}/res/#{threadID}#p#{postID}"
link.nextSibling.href = "/#{board}/res/#{threadID}#q#{postID}" link.nextSibling.href = "/#{board}/res/#{threadID}#q#{postID}"
@ -2640,7 +2902,7 @@ Get =
innerHTML: "<img #{thumb_src} alt='#{if data.media_status isnt 'available' then "Error: #{data.media_status}, " else ''}#{if spoiler then 'Spoiler Image, ' else ''}#{filesize}' data-md5=#{data.media_hash} style='height: #{data.preview_h}px; width: #{data.preview_w}px;'>" innerHTML: "<img #{thumb_src} alt='#{if data.media_status isnt 'available' then "Error: #{data.media_status}, " else ''}#{if spoiler then 'Spoiler Image, ' else ''}#{filesize}' data-md5=#{data.media_hash} style='height: #{data.preview_h}px; width: #{data.preview_w}px;'>"
$.after (if isOP then piM else pi), file $.after (if isOP then piM else pi), file
$.replace root.firstChild, pc $.replace root.firstChild, Get.cleanPost pc
cb() if cb cb() if cb
cleanPost: (root) -> cleanPost: (root) ->
post = $ '.post', root post = $ '.post', root
@ -2655,7 +2917,9 @@ Get =
# Don't mess with other features # Don't mess with other features
now = Date.now() now = Date.now()
for el in $$ '[id]', root els = $$ '[id]', root
els.push root
for el in els
el.id = "#{now}_#{el.id}" el.id = "#{now}_#{el.id}"
$.rmClass root, 'forwarded' $.rmClass root, 'forwarded'
@ -2924,7 +3188,7 @@ Quotify =
m[1] m[1]
else else
# Get the post's board, whether it's inlined or not. # Get the post's board, whether it's inlined or not.
$('.postInfo > .postNum > a[title="Highlight this post"]', post.el).pathname.split('/')[1] $('.postNum > a[title="Highlight this post"]', post.el).pathname.split('/')[1]
nodes.push a = $.el 'a', nodes.push a = $.el 'a',
# \u00A0 is nbsp # \u00A0 is nbsp
@ -2957,28 +3221,32 @@ Quotify =
$.replace node, nodes $.replace node, nodes
return return
DeleteButton = DeleteLink =
init: -> init: ->
@a = $.el 'a', a = $.el 'a',
className: 'delete_button' className: 'delete_link'
innerHTML: '[&nbsp;&times;&nbsp;]'
href: 'javascript:;' href: 'javascript:;'
Main.callbacks.push @node Menu.addEntry
node: (post) -> el: a
unless a = $ '.delete_button', post.el open: (post) ->
a = DeleteButton.a.cloneNode true if post.isArchived
$.add $('.postInfo', post.el), a return false
$.on a, 'click', DeleteButton.delete a.textContent = 'Delete this post'
$.on a, 'click', DeleteLink.delete
true
delete: -> delete: ->
$.off @, 'click', DeleteButton.delete $.off @, 'click', DeleteLink.delete
@innerHTML = '[&nbsp;Deleting...&nbsp;]' @textContent = 'Deleting...'
if m = d.cookie.match /4chan_pass=([^;]+)/ pwd =
pwd = decodeURIComponent m[1] if m = d.cookie.match /4chan_pass=([^;]+)/
else decodeURIComponent m[1]
pwd = $.id('delPassword').value else
id = $.x('preceding-sibling::input', @).name $.id('delPassword').value
board = $.x('preceding-sibling::span[1]/a', @).pathname.match(/\w+/)[0]
id = @parentNode.dataset.id
board = $('.postNum > a[title="Highlight this post"]',
$.id @parentNode.dataset.rootid).pathname.split('/')[1]
self = this self = this
form = form =
@ -2986,13 +3254,12 @@ DeleteButton =
pwd: pwd pwd: pwd
form[id] = 'delete' form[id] = 'delete'
$.ajax "https://sys.4chan.org/#{board}/imgboard.php", { $.ajax $.id('delform').action.replace("/#{g.BOARD}/", "/#{board}/"), {
onload: -> DeleteButton.load self, @response onload: -> DeleteLink.load self, @response
onerror: -> DeleteButton.error self onerror: -> DeleteLink.error self
}, { }, {
form: $.formData form form: $.formData form
} }
load: (self, html) -> load: (self, html) ->
doc = d.implementation.createHTMLDocument '' doc = d.implementation.createHTMLDocument ''
doc.documentElement.innerHTML = html doc.documentElement.innerHTML = html
@ -3000,32 +3267,68 @@ DeleteButton =
s = 'Banned!' s = 'Banned!'
else if msg = doc.getElementById 'errmsg' # error! else if msg = doc.getElementById 'errmsg' # error!
s = msg.textContent s = msg.textContent
$.on self, 'click', DeleteButton.delete $.on self, 'click', DeleteLink.delete
else else
s = 'Deleted' s = 'Deleted'
self.innerHTML = "[&nbsp;#{s}&nbsp;]" self.textContent = s
error: (self) -> error: (self) ->
self.innerHTML = '[&nbsp;Connection error, please retry.&nbsp;]' self.textContent = 'Connection error, please retry.'
$.on self, 'click', DeleteButton.delete $.on self, 'click', DeleteLink.delete
ReportButton = ReportLink =
init: -> init: ->
@a = $.el 'a', a = $.el 'a',
className: 'report_button' className: 'report_link'
innerHTML: '[&nbsp;!&nbsp;]'
href: 'javascript:;' href: 'javascript:;'
Main.callbacks.push @node textContent: 'Report this post'
node: (post) -> $.on a, 'click', @report
unless a = $ '.report_button', post.el Menu.addEntry
a = ReportButton.a.cloneNode true el: a
$.add $('.postInfo', post.el), a open: (post) ->
$.on a, 'click', ReportButton.report post.isArchived is false
report: -> report: ->
url = "//sys.4chan.org/#{g.BOARD}/imgboard.php?mode=report&no=#{$.x('preceding-sibling::input', @).name}" a = $ '.postNum > a[title="Highlight this post"]', $.id @parentNode.dataset.rootid
url = "//sys.4chan.org/#{a.pathname.split('/')[1]}/imgboard.php?mode=report&no=#{@parentNode.dataset.id}"
id = Date.now() id = Date.now()
set = "toolbar=0,scrollbars=0,location=0,status=1,menubar=0,resizable=1,width=685,height=200" set = "toolbar=0,scrollbars=0,location=0,status=1,menubar=0,resizable=1,width=685,height=200"
window.open url, id, set window.open url, id, set
DownloadLink =
init: ->
# Test for download feature support.
return if $.el('a').download is undefined
a = $.el 'a',
className: 'download_link'
textContent: 'Download file'
Menu.addEntry
el: a
open: (post) ->
unless post.img
return false
a.href = post.img.parentNode.href
fileText = post.fileInfo.firstElementChild
a.download =
if Conf['File Info Formatting']
fileText.dataset.filename
else
$('span', fileText).title
true
ArchiveLink =
init: ->
a = $.el 'a',
className: 'archive_link'
target: '_blank'
textContent: 'Archived post'
Menu.addEntry
el: a
open: (post) ->
path = $('.postNum > a[title="Highlight this post"]', post.el).pathname.split '/'
if (href = Redirect.thread path[1], path[3], post.ID) is "//boards.4chan.org/#{path[1]}/"
return false
a.href = href
true
ThreadStats = ThreadStats =
init: -> init: ->
dialog = UI.dialog 'stats', 'bottom: 0; left: 0;', '<div class=move><span id=postcount>0</span> / <span id=imagecount>0</span></div>' dialog = UI.dialog 'stats', 'bottom: 0; left: 0;', '<div class=move><span id=postcount>0</span> / <span id=imagecount>0</span></div>'
@ -3506,11 +3809,23 @@ Main =
if Conf['Image Hover'] if Conf['Image Hover']
ImageHover.init() ImageHover.init()
if Conf['Report Button'] if Conf['Menu']
ReportButton.init() Menu.init()
if Conf['Delete Button'] if Conf['Report Link']
DeleteButton.init() ReportLink.init()
if Conf['Delete Link']
DeleteLink.init()
if Conf['Filter']
Filter.menuInit()
if Conf['Download Link']
DownloadLink.init()
if Conf['Archive Link']
ArchiveLink.init()
if Conf['Resurrect Quotes'] if Conf['Resurrect Quotes']
Quotify.init() Quotify.init()
@ -3716,6 +4031,60 @@ a[href="javascript:;"] {
display: none !important; display: none !important;
} }
.menu_button {
display: inline-block;
}
.menu_button > span {
border-top: .5em solid;
border-right: .3em solid transparent;
border-left: .3em solid transparent;
display: inline-block;
margin: 2px;
vertical-align: middle;
}
#menu {
position: absolute;
outline: none;
}
.entry {
border-bottom: 1px solid rgba(0, 0, 0, .25);
cursor: pointer;
display: block;
outline: none;
padding: 3px 7px;
position: relative;
text-decoration: none;
white-space: nowrap;
}
.entry:last-child {
border: none;
}
.focused.entry {
background: rgba(255, 255, 255, .33);
}
.entry.hasSubMenu {
padding-right: 1.5em;
}
.hasSubMenu::after {
content: "";
border-left: .5em solid;
border-top: .3em solid transparent;
border-bottom: .3em solid transparent;
display: inline-block;
margin: .3em;
position: absolute;
right: 3px;
}
.hasSubMenu:not(.focused) > .subMenu {
display: none;
}
.subMenu {
position: absolute;
left: 100%;
top: 0;
margin-top: -1px;
}
h1 { h1 {
text-align: center; text-align: center;
} }