Add Menu. Close #627. Add Report/Delete/Download/Archive Link.

This commit is contained in:
Nicolas Stepien 2013-01-25 12:11:02 +01:00
parent ddbab74efb
commit 7bf2e7dd45
4 changed files with 950 additions and 11 deletions

View File

@ -20,7 +20,7 @@
// @icon https://github.com/MayhemYDG/4chan-x/raw/stable/img/icon.gif
// ==/UserScript==
/* 4chan X Alpha - Version 3.0.0 - 2013-01-24
/* 4chan X Alpha - Version 3.0.0 - 2013-01-25
* http://mayhemydg.github.com/4chan-x/
*
* Copyright (c) 2009-2011 James Campos <james.r.campos@gmail.com>
@ -43,7 +43,7 @@
*/
(function() {
var $, $$, Anonymize, AutoGIF, Board, Build, Clone, Conf, Config, FileInfo, Get, ImageHover, Main, Post, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, Quotify, Recursive, Redirect, ReplyHiding, RevealSpoilers, Sauce, Thread, ThreadHiding, ThreadUpdater, Time, UI, d, g, _base,
var $, $$, Anonymize, ArchiveLink, AutoGIF, Board, Build, Clone, Conf, Config, DeleteLink, DownloadLink, FileInfo, Get, ImageHover, Main, Menu, Post, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, Quotify, Recursive, Redirect, ReplyHiding, ReportLink, RevealSpoilers, Sauce, Thread, ThreadHiding, ThreadUpdater, Time, UI, d, g, _base,
__indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; },
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
@ -1014,9 +1014,450 @@
}
};
Menu = {
entries: [],
init: function() {
$.on(d, 'AddMenuEntry', function(e) {
return Menu.addEntry(e.detail);
});
return Post.prototype.callbacks.push({
name: 'Menu',
cb: this.node
});
},
node: function() {
var a;
if (this.isClone) {
a = $('.menu-button', this.nodes.info);
a.setAttribute('data-clone', true);
$.on(a, 'click', Menu.toggle);
return;
}
a = Menu.makeButton(this);
return $.add(this.nodes.info, [$.tn('\u00A0'), a]);
},
makeButton: function(post) {
var a;
a = $.el('a', {
className: 'menu-button',
innerHTML: '[<span></span>]',
href: 'javascript:;'
});
a.setAttribute('data-postid', post.fullID);
$.on(a, 'click', Menu.toggle);
return a;
},
makeMenu: function() {
var menu;
menu = $.el('div', {
className: 'reply dialog',
id: 'menu',
tabIndex: 0
});
$.on(menu, 'click', function(e) {
return e.stopPropagation();
});
$.on(menu, 'keydown', Menu.keybinds);
return menu;
},
toggle: function(e) {
var lastToggledButton, post;
e.preventDefault();
e.stopPropagation();
if (Menu.currentMenu) {
lastToggledButton = Menu.lastToggledButton;
Menu.close();
if (lastToggledButton === this) {
return;
}
}
Menu.lastToggledButton = this;
post = this.dataset.clone ? Get.postFromRoot($.x('ancestor::div[contains(@class,"postContainer")][1]', this)) : g.posts[this.dataset.postid];
return Menu.open(this, post);
},
open: function(button, post) {
var bLeft, bRect, bTop, entry, mRect, menu, _i, _len, _ref;
menu = Menu.makeMenu();
Menu.currentMenu = menu;
_ref = Menu.entries;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
entry = _ref[_i];
Menu.insertEntry(entry, menu, post);
}
Menu.focus($('.entry', menu));
$.on(d, 'click', Menu.close);
$.add(d.body, menu);
mRect = menu.getBoundingClientRect();
bRect = button.getBoundingClientRect();
bTop = d.documentElement.scrollTop + d.body.scrollTop + bRect.top;
bLeft = d.documentElement.scrollLeft + d.body.scrollLeft + bRect.left;
menu.style.top = bRect.top + bRect.height + mRect.height < d.documentElement.clientHeight ? bTop + bRect.height + 2 + 'px' : bTop - mRect.height - 2 + 'px';
menu.style.left = bRect.left + mRect.width < d.documentElement.clientWidth ? bLeft + 'px' : bLeft + bRect.width - mRect.width + 'px';
return menu.focus();
},
insertEntry: function(entry, parent, post) {
var child, submenu, _i, _len, _ref;
if (!entry.open(post)) {
return;
}
$.add(parent, entry.el);
if (!entry.children) {
return;
}
if (submenu = $('.submenu', entry.el)) {
$.rm(submenu);
}
submenu = $.el('div', {
className: 'reply dialog submenu'
});
$.add(entry.el, submenu);
_ref = entry.children;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
child = _ref[_i];
Menu.insertEntry(child, submenu, post);
}
},
close: function() {
$.rm(Menu.currentMenu);
delete Menu.currentMenu;
delete Menu.lastToggledButton;
return $.off(d, 'click', Menu.close);
},
keybinds: function(e) {
var entry, next, subEntry, submenu;
entry = $('.focused', Menu.currentMenu);
while (subEntry = $('.focused', entry)) {
entry = subEntry;
}
switch (Keybinds.keyCode(e) || e.keyCode) {
case 'Esc':
Menu.lastToggledButton.focus();
Menu.close();
break;
case 13:
case 32:
entry.click();
break;
case 'Up':
if (next = entry.previousElementSibling) {
Menu.focus(next);
}
break;
case 'Down':
if (next = entry.nextElementSibling) {
Menu.focus(next);
}
break;
case 'Right':
if ((submenu = $('.submenu', entry)) && (next = submenu.firstElementChild)) {
Menu.focus(next);
}
break;
case 'Left':
if (next = $.x('parent::*[contains(@class,"submenu")]/parent::*', entry)) {
Menu.focus(next);
}
break;
default:
return;
}
e.preventDefault();
return e.stopPropagation();
},
focus: function(entry) {
var bottom, eRect, focused, left, right, sRect, style, submenu, top, _i, _len, _ref;
if (focused = $.x('parent::*/child::*[contains(@class,"focused")]', entry)) {
$.rmClass(focused, 'focused');
}
_ref = $$('.focused', entry);
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
focused = _ref[_i];
$.rmClass(focused, 'focused');
}
$.addClass(entry, 'focused');
if (!(submenu = $('.submenu', entry))) {
return;
}
sRect = submenu.getBoundingClientRect();
eRect = entry.getBoundingClientRect();
if (eRect.top + sRect.height < d.documentElement.clientHeight) {
top = '0px';
bottom = 'auto';
} else {
top = 'auto';
bottom = '0px';
}
if (eRect.right + sRect.width < d.documentElement.clientWidth) {
left = '100%';
right = 'auto';
} else {
left = 'auto';
right = '100%';
}
style = submenu.style;
style.top = top;
style.bottom = bottom;
style.left = left;
return style.right = right;
},
addEntry: function(entry) {
Menu.parseEntry(entry);
return Menu.entries.push(entry);
},
parseEntry: 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, 'has-submenu');
for (_i = 0, _len = children.length; _i < _len; _i++) {
child = children[_i];
Menu.parseEntry(child);
}
}
};
ReportLink = {
init: function() {
var a;
a = $.el('a', {
className: 'report-link',
href: 'javascript:;',
textContent: 'Report this post'
});
$.on(a, 'click', ReportLink.report);
return Menu.addEntry({
el: a,
open: function(post) {
ReportLink.post = post;
return !post.isDead;
}
});
},
report: function() {
var id, post, set, url;
post = ReportLink.post;
url = "//sys.4chan.org/" + post.board + "/imgboard.php?mode=report&no=" + post;
id = Date.now();
set = "toolbar=0,scrollbars=0,location=0,status=1,menubar=0,resizable=1,width=685,height=200";
return window.open(url, id, set);
}
};
DeleteLink = {
init: function() {
var div, fileEl, fileEntry, postEl, postEntry;
div = $.el('div', {
className: 'delete-link',
textContent: 'Delete'
});
postEl = $.el('a', {
className: 'delete-post',
href: 'javascript:;'
});
fileEl = $.el('a', {
className: 'delete-file',
href: 'javascript:;'
});
postEntry = {
el: postEl,
open: function() {
postEl.textContent = 'Post';
$.on(postEl, 'click', DeleteLink["delete"]);
return true;
}
};
fileEntry = {
el: fileEl,
open: function(post) {
fileEl.textContent = 'File';
$.on(fileEl, 'click', DeleteLink["delete"]);
return !!post.file;
}
};
Menu.addEntry({
el: div,
open: function(post) {
var node, seconds;
if (post.isDead) {
return false;
}
DeleteLink.post = post;
node = div.firstChild;
if (seconds = DeleteLink.cooldown[post.fullID]) {
node.textContent = "Delete (" + seconds + ")";
DeleteLink.cooldown.el = node;
} else {
node.textContent = 'Delete';
delete DeleteLink.cooldown.el;
}
return true;
},
children: [postEntry, fileEntry]
});
return $.on(d, 'QRPostSuccessful', this.cooldown.start);
},
"delete": function() {
var form, link, m, post, pwd;
post = DeleteLink.post;
if (DeleteLink.cooldown[post.fullID]) {
return;
}
$.off(this, 'click', DeleteLink["delete"]);
this.textContent = "Deleting " + this.textContent + "...";
pwd = (m = d.cookie.match(/4chan_pass=([^;]+)/)) ? decodeURIComponent(m[1]) : $.id('delPassword').value;
form = {
mode: 'usrdel',
onlyimgdel: $.hasClass(this, 'delete-file'),
pwd: pwd
};
form[post.ID] = 'delete';
link = this;
return $.ajax($.id('delform').action.replace("/" + g.BOARD + "/", "/" + post.board + "/"), {
onload: function() {
return DeleteLink.load(link, this.response);
},
onerror: function() {
return DeleteLink.error(link);
}
}, {
form: $.formData(form)
});
},
load: function(link, html) {
var doc, msg, s;
doc = d.implementation.createHTMLDocument('');
doc.documentElement.innerHTML = html;
if (doc.title === '4chan - Banned') {
s = 'Banned!';
} else if (msg = doc.getElementById('errmsg')) {
s = msg.textContent;
$.on(link, 'click', DeleteLink["delete"]);
} else {
s = 'Deleted';
}
return link.textContent = s;
},
error: function(link) {
link.textContent = 'Connection error, please retry.';
return $.on(link, 'click', DeleteLink["delete"]);
},
cooldown: {
start: function(e) {
var fullID, seconds;
seconds = g.BOARD.ID === 'q' ? 600 : 30;
fullID = "" + g.BOARD + "." + e.detail.postID;
return DeleteLink.cooldown.count(fullID, seconds, seconds);
},
count: function(fullID, seconds, length) {
var el;
if (!((0 <= seconds && seconds <= length))) {
return;
}
setTimeout(DeleteLink.cooldown.count, 1000, fullID, seconds - 1, length);
el = DeleteLink.cooldown.el;
if (seconds === 0) {
if (el != null) {
el.textContent = 'Delete';
}
delete DeleteLink.cooldown[fullID];
delete DeleteLink.cooldown.el;
return;
}
if (el != null) {
el.textContent = "Delete (" + seconds + ")";
}
return DeleteLink.cooldown[fullID] = seconds;
}
}
};
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) {
if (!post.file) {
return false;
}
a.href = post.file.URL;
a.download = post.file.name;
return true;
}
});
}
};
ArchiveLink = {
init: function() {
var div, entry, type, _i, _len, _ref;
div = $.el('div', {
textContent: 'Archive'
});
entry = {
el: div,
open: function(post) {
var redirect;
redirect = Redirect.to({
board: post.board,
threadID: post.thread,
postID: post.ID
});
return redirect !== ("//boards.4chan.org/" + post.board + "/");
},
children: []
};
_ref = [['Post', 'post'], ['Name', 'name'], ['Tripcode', 'tripcode'], ['E-mail', 'email'], ['Subject', 'subject'], ['Filename', 'filename'], ['Image MD5', 'md5']];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
type = _ref[_i];
entry.children.push(this.createSubEntry(type[0], type[1]));
}
return Menu.addEntry(entry);
},
createSubEntry: function(text, type) {
var el, open;
el = $.el('a', {
textContent: text,
target: '_blank'
});
if (type === 'post') {
open = function(post) {
el.href = Redirect.to({
board: post.board,
threadID: post.thread,
postID: post.ID
});
return true;
};
} else {
open = function(post) {
return true;
};
}
return {
el: el,
open: open
};
}
};
Redirect = {
image: function(board, filename) {
switch (board.ID) {
switch ("" + board) {
case 'a':
case 'co':
case 'jp':
@ -1057,7 +1498,7 @@
}
},
post: function(board, postID) {
switch (board.ID) {
switch ("" + board) {
case 'a':
case 'co':
case 'jp':
@ -1082,7 +1523,7 @@
to: function(data) {
var board, url;
board = data.board;
switch (board.ID) {
switch ("" + board) {
case 'a':
case 'co':
case 'jp':
@ -1153,7 +1594,7 @@
}
}
board = data.board, threadID = data.threadID, postID = data.postID;
if (postID) {
if (typeof postID === 'string') {
postID = postID.match(/\d+/)[0];
}
path = threadID ? "" + board + "/thread/" + threadID : "" + board + "/post/" + postID;
@ -3052,6 +3493,41 @@
} catch (err) {
$.log(err, 'Recursive');
}
if (Conf['Menu']) {
try {
Menu.init();
} catch (err) {
$.log(err, 'Menu');
}
if (Conf['Report Link']) {
try {
ReportLink.init();
} catch (err) {
$.log(err, 'Report Link', err.stack);
}
}
if (Conf['Delete Link']) {
try {
DeleteLink.init();
} catch (err) {
$.log(err, 'Delete Link');
}
}
if (Conf['Download Link']) {
try {
DownloadLink.init();
} catch (err) {
$.log(err, 'Download Link');
}
}
if (Conf['Archive Link']) {
try {
ArchiveLink.init();
} catch (err) {
$.log(err, 'Archive Link');
}
}
}
if (Conf['Quote Inline']) {
try {
QuoteInline.init();
@ -3205,7 +3681,7 @@
settings: function() {
return alert('Here be settings');
},
css: "/* general */\n.dialog.reply {\n display: block;\n border: 1px solid rgba(0, 0, 0, .25);\n padding: 0;\n}\n.move {\n cursor: move;\n}\nlabel {\n cursor: pointer;\n}\na[href=\"javascript:;\"] {\n text-decoration: none;\n}\n.warning {\n color: red;\n}\n\n/* 4chan style fixes */\n.opContainer, .op {\n display: block !important;\n}\n.post {\n overflow: visible !important;\n}\n[hidden] {\n display: none !important;\n}\n\n/* fixed, z-index */\n#qp, #ihover,\n#updater, #stats,\n#boardNavDesktop.reply,\n#qr, #watcher {\n position: fixed;\n}\n#qp, #ihover {\n z-index: 100;\n}\n#updater, #stats {\n z-index: 90;\n}\n#boardNavDesktop.reply:hover {\n z-index: 80;\n}\n#qr {\n z-index: 50;\n}\n#watcher {\n z-index: 30;\n}\n#boardNavDesktop.reply {\n z-index: 10;\n}\n\n\n/* header */\nbody.fourchan_x {\n margin-top: 2.5em;\n}\n#boardNavDesktop.reply {\n border-width: 0 0 1px;\n padding: 4px;\n top: 0;\n right: 0;\n left: 0;\n transition: opacity .1s ease-in-out;\n -o-transition: opacity .1s ease-in-out;\n -moz-transition: opacity .1s ease-in-out;\n -webkit-transition: opacity .1s ease-in-out;\n}\n#boardNavDesktop.reply:not(:hover) {\n opacity: .4;\n transition: opacity 1.5s .5s ease-in-out;\n -o-transition: opacity 1.5s .5s ease-in-out;\n -moz-transition: opacity 1.5s .5s ease-in-out;\n -webkit-transition: opacity 1.5s .5s ease-in-out;\n}\n#boardNavDesktop.reply a {\n margin: -1px;\n}\n#settings {\n float: right;\n}\n\n/* thread updater */\n#updater {\n text-align: right;\n}\n#updater:not(:hover) {\n background: none;\n border: none;\n}\n#updater input[type=number] {\n width: 4em;\n}\n#updater:not(:hover) > div:not(.move) {\n display: none;\n}\n.new {\n color: limegreen;\n}\n\n/* quote */\n.quotelink.deadlink {\n text-decoration: underline !important;\n}\n.deadlink:not(.quotelink) {\n text-decoration: none !important;\n}\n.inlined {\n opacity: .5;\n}\n#qp input, .forwarded {\n display: none;\n}\n.quotelink.forwardlink,\n.backlink.forwardlink {\n text-decoration: none;\n border-bottom: 1px dashed;\n}\n.filtered {\n text-decoration: underline line-through;\n}\n.inline {\n border: 1px solid rgba(128, 128, 128, .5);\n display: table;\n margin: 2px 0;\n}\n.inline .post {\n border: 0 !important;\n display: table !important;\n margin: 0 !important;\n padding: 1px 2px !important;\n}\n#qp {\n padding: 2px 2px 5px;\n}\n#qp .post {\n border: none;\n margin: 0;\n padding: 0;\n}\n#qp img {\n max-height: 300px;\n max-width: 500px;\n}\n.qphl {\n box-shadow: 0 0 0 2px rgba(216, 94, 49, .7);\n}\n\n/* file */\n.fileText:hover .fntrunc,\n.fileText:not(:hover) .fnfull {\n display: none;\n}\n#ihover {\n box-sizing: border-box;\n -moz-box-sizing: border-box;\n max-height: 100%;\n max-width: 75%;\n padding-bottom: 16px;\n}\n\n/* thread & reply hiding */\n.hide-thread-button,\n.hide-reply-button {\n float: left;\n margin-right: 2px;\n}\n.stub ~ .sideArrows,\n.stub ~ .hide-reply-button,\n.stub ~ .post {\n display: none !important;\n}"
css: "/* general */\n.dialog.reply {\n display: block;\n border: 1px solid rgba(0, 0, 0, .25);\n padding: 0;\n}\n.move {\n cursor: move;\n}\nlabel {\n cursor: pointer;\n}\na[href=\"javascript:;\"] {\n text-decoration: none;\n}\n.warning {\n color: red;\n}\n\n/* 4chan style fixes */\n.opContainer, .op {\n display: block !important;\n}\n.post {\n overflow: visible !important;\n}\n[hidden] {\n display: none !important;\n}\n\n/* fixed, z-index */\n#qp, #ihover,\n#updater, #stats,\n#boardNavDesktop.reply,\n#qr, #watcher {\n position: fixed;\n}\n#qp, #ihover {\n z-index: 100;\n}\n#updater, #stats {\n z-index: 90;\n}\n#boardNavDesktop.reply:hover {\n z-index: 80;\n}\n#qr {\n z-index: 50;\n}\n#watcher {\n z-index: 30;\n}\n#boardNavDesktop.reply {\n z-index: 10;\n}\n\n\n/* header */\nbody.fourchan_x {\n margin-top: 2.5em;\n}\n#boardNavDesktop.reply {\n border-width: 0 0 1px;\n padding: 4px;\n top: 0;\n right: 0;\n left: 0;\n transition: opacity .1s ease-in-out;\n -o-transition: opacity .1s ease-in-out;\n -moz-transition: opacity .1s ease-in-out;\n -webkit-transition: opacity .1s ease-in-out;\n}\n#boardNavDesktop.reply:not(:hover) {\n opacity: .4;\n transition: opacity 1.5s .5s ease-in-out;\n -o-transition: opacity 1.5s .5s ease-in-out;\n -moz-transition: opacity 1.5s .5s ease-in-out;\n -webkit-transition: opacity 1.5s .5s ease-in-out;\n}\n#boardNavDesktop.reply a {\n margin: -1px;\n}\n#settings {\n float: right;\n}\n\n/* thread updater */\n#updater {\n text-align: right;\n}\n#updater:not(:hover) {\n background: none;\n border: none;\n}\n#updater input[type=number] {\n width: 4em;\n}\n#updater:not(:hover) > div:not(.move) {\n display: none;\n}\n.new {\n color: limegreen;\n}\n\n/* quote */\n.quotelink.deadlink {\n text-decoration: underline !important;\n}\n.deadlink:not(.quotelink) {\n text-decoration: none !important;\n}\n.inlined {\n opacity: .5;\n}\n#qp input, .forwarded {\n display: none;\n}\n.quotelink.forwardlink,\n.backlink.forwardlink {\n text-decoration: none;\n border-bottom: 1px dashed;\n}\n.filtered {\n text-decoration: underline line-through;\n}\n.inline {\n border: 1px solid rgba(128, 128, 128, .5);\n display: table;\n margin: 2px 0;\n}\n.inline .post {\n border: 0 !important;\n display: table !important;\n margin: 0 !important;\n padding: 1px 2px !important;\n}\n#qp {\n padding: 2px 2px 5px;\n}\n#qp .post {\n border: none;\n margin: 0;\n padding: 0;\n}\n#qp img {\n max-height: 300px;\n max-width: 500px;\n}\n.qphl {\n box-shadow: 0 0 0 2px rgba(216, 94, 49, .7);\n}\n\n/* file */\n.fileText:hover .fntrunc,\n.fileText:not(:hover) .fnfull {\n display: none;\n}\n#ihover {\n box-sizing: border-box;\n -moz-box-sizing: border-box;\n max-height: 100%;\n max-width: 75%;\n padding-bottom: 16px;\n}\n\n/* thread & reply hiding */\n.hide-thread-button,\n.hide-reply-button {\n float: left;\n margin-right: 2px;\n}\n.stub ~ .sideArrows,\n.stub ~ .hide-reply-button,\n.stub ~ .post {\n display: none !important;\n}\n\n/* Menu */\n.menu-button {\n display: inline-block;\n}\n.menu-button > span {\n border-top: 6px solid;\n border-right: 4px solid transparent;\n border-left: 4px solid transparent;\n display: inline-block;\n margin: 2px;\n vertical-align: middle;\n}\n#menu {\n position: absolute;\n outline: none;\n}\n.entry {\n border-bottom: 1px solid rgba(0, 0, 0, .25);\n cursor: pointer;\n display: block;\n outline: none;\n padding: 3px 7px;\n position: relative;\n text-decoration: none;\n white-space: nowrap;\n}\n.entry:last-child {\n border: none;\n}\n.focused.entry {\n background: rgba(255, 255, 255, .33);\n}\n.entry.has-submenu {\n padding-right: 20px;\n}\n.has-submenu::after {\n content: \"\";\n border-left: 6px solid;\n border-top: 4px solid transparent;\n border-bottom: 4px solid transparent;\n display: inline-block;\n margin: 4px;\n position: absolute;\n right: 3px;\n}\n.has-submenu:not(.focused) > .submenu {\n display: none;\n}\n.submenu {\n position: absolute;\n margin: -1px 0;\n}"
};
Main.init();

View File

@ -174,3 +174,56 @@ body.fourchan_x {
.stub ~ .post {
display: none !important;
}
/* Menu */
.menu-button {
display: inline-block;
}
.menu-button > span {
border-top: 6px solid;
border-right: 4px solid transparent;
border-left: 4px 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.has-submenu {
padding-right: 20px;
}
.has-submenu::after {
content: "";
border-left: 6px solid;
border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
display: inline-block;
margin: 4px;
position: absolute;
right: 3px;
}
.has-submenu:not(.focused) > .submenu {
display: none;
}
.submenu {
position: absolute;
margin: -1px 0;
}

View File

@ -244,11 +244,386 @@ Recursive =
break
return
Menu =
entries: []
init: ->
# Doc here: https://github.com/MayhemYDG/4chan-x/wiki/Menu-API
$.on d, 'AddMenuEntry', (e) -> Menu.addEntry e.detail
Post::callbacks.push
name: 'Menu'
cb: @node
node: ->
if @isClone
a = $ '.menu-button', @nodes.info
a.setAttribute 'data-clone', true
$.on a, 'click', Menu.toggle
return
a = Menu.makeButton @
$.add @nodes.info, [$.tn('\u00A0'), a]
makeButton: (post) ->
a = $.el 'a',
className: 'menu-button'
innerHTML: '[<span></span>]'
href: 'javascript:;'
a.setAttribute 'data-postid', post.fullID
$.on a, 'click', Menu.toggle
a
makeMenu: ->
menu = $.el 'div',
className: 'reply dialog'
id: 'menu'
tabIndex: 0
$.on menu, 'click', (e) -> e.stopPropagation()
$.on menu, 'keydown', Menu.keybinds
menu
toggle: (e) ->
e.preventDefault()
e.stopPropagation()
if Menu.currentMenu
# Close if it's already opened.
# Reopen if we clicked on another button.
{lastToggledButton} = Menu
Menu.close()
return if lastToggledButton is @
Menu.lastToggledButton = @
post =
if @dataset.clone
Get.postFromRoot $.x 'ancestor::div[contains(@class,"postContainer")][1]', @
else
g.posts[@dataset.postid]
Menu.open @, post
open: (button, post) ->
menu = Menu.makeMenu()
Menu.currentMenu = menu
for entry in Menu.entries
Menu.insertEntry entry, menu, post
Menu.focus $ '.entry', menu
$.on d, 'click', Menu.close
$.add d.body, menu
# Position
mRect = menu.getBoundingClientRect()
bRect = button.getBoundingClientRect()
bTop = d.documentElement.scrollTop + d.body.scrollTop + bRect.top
bLeft = d.documentElement.scrollLeft + d.body.scrollLeft + bRect.left
menu.style.top =
if bRect.top + bRect.height + mRect.height < d.documentElement.clientHeight
bTop + bRect.height + 2 + 'px'
else
bTop - mRect.height - 2 + 'px'
menu.style.left =
if bRect.left + mRect.width < d.documentElement.clientWidth
bLeft + 'px'
else
bLeft + bRect.width - mRect.width + 'px'
menu.focus()
insertEntry: (entry, parent, post) ->
return unless entry.open post
$.add parent, entry.el
return unless entry.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 entry.children
Menu.insertEntry child, submenu, post
return
close: ->
$.rm Menu.currentMenu
delete Menu.currentMenu
delete Menu.lastToggledButton
$.off d, 'click', Menu.close
keybinds: (e) ->
entry = $ '.focused', Menu.currentMenu
while subEntry = $ '.focused', entry
entry = subEntry
switch Keybinds.keyCode(e) or e.keyCode
when 'Esc'
Menu.lastToggledButton.focus()
Menu.close()
when 13, 32 # 'Enter', 'Space'
entry.click()
when 'Up'
if next = entry.previousElementSibling
Menu.focus next
when 'Down'
if next = entry.nextElementSibling
Menu.focus next
when 'Right'
if (submenu = $ '.submenu', entry) and next = submenu.firstElementChild
Menu.focus next
when 'Left'
if next = $.x 'parent::*[contains(@class,"submenu")]/parent::*', entry
Menu.focus next
else
return
e.preventDefault()
e.stopPropagation()
focus: (entry) ->
if focused = $.x 'parent::*/child::*[contains(@class,"focused")]', entry
$.rmClass focused, 'focused'
for focused in $$ '.focused', entry
$.rmClass focused, 'focused'
$.addClass entry, 'focused'
# Submenu positioning.
return unless submenu = $ '.submenu', entry
sRect = submenu.getBoundingClientRect()
eRect = entry.getBoundingClientRect()
if eRect.top + sRect.height < d.documentElement.clientHeight
top = '0px'
bottom = 'auto'
else
top = 'auto'
bottom = '0px'
if eRect.right + sRect.width < d.documentElement.clientWidth
left = '100%'
right = 'auto'
else
left = 'auto'
right = '100%'
{style} = submenu
style.top = top
style.bottom = bottom
style.left = left
style.right = right
addEntry: (entry) ->
Menu.parseEntry entry
Menu.entries.push entry
parseEntry: (entry) ->
{el, children} = entry
$.addClass el, 'entry'
$.on el, 'focus mouseover', (e) ->
e.stopPropagation()
Menu.focus @
return unless children
$.addClass el, 'has-submenu'
for child in children
Menu.parseEntry child
return
ReportLink =
init: ->
a = $.el 'a',
className: 'report-link'
href: 'javascript:;'
textContent: 'Report this post'
$.on a, 'click', ReportLink.report
Menu.addEntry
el: a
open: (post) ->
ReportLink.post = post
!post.isDead
report: ->
{post} = ReportLink
url = "//sys.4chan.org/#{post.board}/imgboard.php?mode=report&no=#{post}"
id = Date.now()
set = "toolbar=0,scrollbars=0,location=0,status=1,menubar=0,resizable=1,width=685,height=200"
window.open url, id, set
DeleteLink =
init: ->
div = $.el 'div',
className: 'delete-link'
textContent: 'Delete'
postEl = $.el 'a',
className: 'delete-post'
href: 'javascript:;'
fileEl = $.el 'a',
className: 'delete-file'
href: 'javascript:;'
postEntry =
el: postEl
open: ->
postEl.textContent = 'Post'
$.on postEl, 'click', DeleteLink.delete
true
fileEntry =
el: fileEl
open: (post) ->
fileEl.textContent = 'File'
$.on fileEl, 'click', DeleteLink.delete
!!post.file
Menu.addEntry
el: div
open: (post) ->
return false if post.isDead
DeleteLink.post = post
node = div.firstChild
if seconds = DeleteLink.cooldown[post.fullID]
node.textContent = "Delete (#{seconds})"
DeleteLink.cooldown.el = node
else
node.textContent = 'Delete'
delete DeleteLink.cooldown.el
true
children: [postEntry, fileEntry]
$.on d, 'QRPostSuccessful', @cooldown.start
delete: ->
{post} = DeleteLink
return if DeleteLink.cooldown[post.fullID]
$.off @, 'click', DeleteLink.delete
@textContent = "Deleting #{@textContent}..."
pwd =
if m = d.cookie.match /4chan_pass=([^;]+)/
decodeURIComponent m[1]
else
$.id('delPassword').value
form =
mode: 'usrdel'
onlyimgdel: $.hasClass @, 'delete-file'
pwd: pwd
form[post.ID] = 'delete'
link = @
$.ajax $.id('delform').action.replace("/#{g.BOARD}/", "/#{post.board}/"), {
onload: -> DeleteLink.load link, @response
onerror: -> DeleteLink.error link
}, {
form: $.formData form
}
load: (link, html) ->
doc = d.implementation.createHTMLDocument ''
doc.documentElement.innerHTML = html
if doc.title is '4chan - Banned' # Ban/warn check
s = 'Banned!'
else if msg = doc.getElementById 'errmsg' # error!
s = msg.textContent
$.on link, 'click', DeleteLink.delete
else
s = 'Deleted'
link.textContent = s
error: (link) ->
link.textContent = 'Connection error, please retry.'
$.on link, 'click', DeleteLink.delete
cooldown:
start: (e) ->
seconds =
if g.BOARD.ID is 'q'
600
else
30
fullID = "#{g.BOARD}.#{e.detail.postID}"
DeleteLink.cooldown.count fullID, seconds, seconds
count: (fullID, seconds, length) ->
return unless 0 <= seconds <= length
setTimeout DeleteLink.cooldown.count, 1000, fullID, seconds-1, length
{el} = DeleteLink.cooldown
if seconds is 0
el?.textContent = 'Delete'
delete DeleteLink.cooldown[fullID]
delete DeleteLink.cooldown.el
return
el?.textContent = "Delete (#{seconds})"
DeleteLink.cooldown[fullID] = seconds
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) ->
return false unless post.file
a.href = post.file.URL
a.download = post.file.name
true
ArchiveLink =
init: ->
div = $.el 'div',
textContent: 'Archive'
entry =
el: div
open: (post) ->
redirect = Redirect.to
board: post.board
threadID: post.thread
postID: post.ID
redirect isnt "//boards.4chan.org/#{post.board}/"
children: []
for type in [
['Post', 'post']
['Name', 'name']
['Tripcode', 'tripcode']
['E-mail', 'email']
['Subject', 'subject']
['Filename', 'filename']
['Image MD5', 'md5']
]
# Add a sub entry for each type.
entry.children.push @createSubEntry type[0], type[1]
Menu.addEntry entry
createSubEntry: (text, type) ->
el = $.el 'a',
textContent: text
target: '_blank'
if type is 'post'
open = (post) ->
el.href = Redirect.to
board: post.board
threadID: post.thread
postID: post.ID
true
else
open = (post) ->
# value = Filter[type] post
# # We want to parse the exact same stuff as the filter does already.
# return false unless value
# el.href = Redirect.to
# board: post.board
# type: type
# value: value
# isSearch: true
true
return {
el: el
open: open
}
Redirect =
image: (board, filename) ->
# XXX need to differentiate between thumbnail only and full_image for img src=
# Do not use g.BOARD, the image url can originate from a cross-quote.
switch board.ID
switch "#{board}"
when 'a', 'co', 'jp', 'm', 'q', 'sp', 'tg', 'tv', 'v', 'vg', 'wsg'
"//archive.foolz.us/#{board}/full_image/#{filename}"
when 'u'
@ -266,7 +641,7 @@ Redirect =
when 'c'
"//archive.nyafuu.org/#{board}/full_image/#{filename}"
post: (board, postID) ->
switch board.ID
switch "#{board}"
when 'a', 'co', 'jp', 'm', 'q', 'sp', 'tg', 'tv', 'v', 'vg', 'wsg', 'dev', 'foolz'
"//archive.foolz.us/_/api/chan/post/?board=#{board}&num=#{postID}"
when 'u', 'kuku'
@ -277,7 +652,7 @@ Redirect =
# https://github.com/eksopl/fuuka/issues/27
to: (data) ->
{board} = data
switch board.ID
switch "#{board}"
when 'a', 'co', 'jp', 'm', 'q', 'sp', 'tg', 'tv', 'v', 'vg', 'wsg', 'dev', 'foolz'
url = Redirect.path '//archive.foolz.us', 'foolfuuka', data
when 'u', 'kuku'
@ -318,7 +693,7 @@ Redirect =
{board, threadID, postID} = data
# keep the number only if the location.hash was sent f.e.
postID = postID.match(/\d+/)[0] if postID
postID = postID.match(/\d+/)[0] if typeof postID is 'string'
path =
if threadID
"#{board}/thread/#{threadID}"

View File

@ -342,6 +342,41 @@ Main =
# XXX handle error
$.log err, 'Recursive'
if Conf['Menu']
try
Menu.init()
catch err
# XXX handle error
$.log err, 'Menu'
if Conf['Report Link']
try
ReportLink.init()
catch err
# XXX handle error
$.log err, 'Report Link', err.stack
if Conf['Delete Link']
try
DeleteLink.init()
catch err
# XXX handle error
$.log err, 'Delete Link'
if Conf['Download Link']
try
DownloadLink.init()
catch err
# XXX handle error
$.log err, 'Download Link'
if Conf['Archive Link']
try
ArchiveLink.init()
catch err
# XXX handle error
$.log err, 'Archive Link'
if Conf['Quote Inline']
try
QuoteInline.init()