Merge branch 'master' of github.com:aeosynth/4chan-x

Conflicts:
	4chan_x.user.js
This commit is contained in:
Nicolas Stepien 2011-09-21 22:47:47 +02:00
commit 0fba0ee9bb
2 changed files with 689 additions and 797 deletions

View File

@ -61,7 +61,7 @@
*/ */
(function() { (function() {
var $, $$, DAY, Favicon, HOUR, MINUTE, Main, NAMESPACE, Recaptcha, SECOND, Time, anonymize, conf, config, cooldown, d, expandComment, expandThread, firstRun, g, getTitle, imgExpand, imgGif, imgHover, imgPreloading, key, keybinds, log, nav, nodeInserted, options, pathname, qr, quoteBacklink, quoteInline, quoteOP, quotePreview, redirect, replyHiding, reportButton, revealSpoilers, sauce, temp, threadHiding, threadStats, threading, titlePost, ui, unread, updater, val, watcher; var $, $$, DAY, Favicon, HOUR, MINUTE, Main, NAMESPACE, QR, SECOND, Time, anonymize, conf, config, d, expandComment, expandThread, firstRun, g, getTitle, imgExpand, imgGif, imgHover, imgPreloading, key, keybinds, log, nav, nodeInserted, options, pathname, quoteBacklink, quoteInline, quoteOP, quotePreview, redirect, replyHiding, reportButton, revealSpoilers, sauce, temp, threadHiding, threadStats, threading, titlePost, ui, unread, updater, val, watcher;
var __slice = Array.prototype.slice; var __slice = Array.prototype.slice;
config = { config = {
main: { main: {
@ -948,17 +948,14 @@
} }
}, },
qr: function(thread, quote) { qr: function(thread, quote) {
var qrLink;
if (!(qrLink = $('td.replyhl span[id] a:not(:first-child)', thread))) {
qrLink = $("span[id^=nothread] a:not(:first-child)", thread);
}
if (quote) { if (quote) {
return qr.quote.call(qrLink); return QR.quote.call($('a.quotejs + a', $('td.replyhl', thread) || thread));
} else { } else {
if (!qr.el) { if (QR.qr) {
qr.dialog(qrLink); return $('textarea', QR.qr).focus();
} else {
return QR.dialog('', thread != null ? thread.firstChild.id : void 0);
} }
return $('textarea', qr.el).focus();
} }
}, },
open: function(thread, tab) { open: function(thread, tab) {
@ -1210,70 +1207,28 @@
return $('#backlinkPreview').textContent = conf['backlink'].replace(/%id/, '123456789'); return $('#backlinkPreview').textContent = conf['backlink'].replace(/%id/, '123456789');
} }
}; };
cooldown = { QR = {
init: function() { init: function() {
var match, time, _; var holder;
if (match = location.search.match(/cooldown=(\d+)/)) { if (!($('form[name=post]') && $('#recaptcha_response_field'))) {
_ = match[0], time = match[1];
if ($.get(g.BOARD + '/cooldown', 0) < time) {
$.set(g.BOARD + '/cooldown', time);
}
}
if (Date.now() < $.get(g.BOARD + '/cooldown', 0)) {
cooldown.start();
}
$.bind(window, 'storage', function(e) {
if (e.key === ("" + NAMESPACE + g.BOARD + "/cooldown")) {
return cooldown.start();
}
});
if (g.REPLY) {
return $('.postarea form').action += '?cooldown';
}
},
start: function() {
var submit, _i, _len, _ref;
cooldown.duration = Math.ceil(($.get(g.BOARD + '/cooldown', 0) - Date.now()) / 1000);
if (!(cooldown.duration > 0)) {
return; return;
} }
_ref = $$('#com_submit'); g.callbacks.push(function(root) {
for (_i = 0, _len = _ref.length; _i < _len; _i++) { var quote;
submit = _ref[_i]; quote = $('a.quotejs + a', root);
submit.value = cooldown.duration; return $.bind(quote, 'click', QR.quote);
submit.disabled = true; });
} $.add(d.body, $.el('iframe', {
return setTimeout(cooldown.cb, 1000); name: 'iframe',
}, hidden: true
cb: function() { }));
var submit, submits, _i, _j, _len, _len2, _results; $('#recaptcha_response_field').id = '';
submits = $$('#com_submit'); holder = $('#recaptcha_challenge_field_holder');
if (--cooldown.duration) { $.bind(holder, 'DOMNodeInserted', QR.captchaNode);
setTimeout(cooldown.cb, 1000); QR.captchaNode({
_results = []; target: holder.firstChild
for (_i = 0, _len = submits.length; _i < _len; _i++) { });
submit = submits[_i]; QR.accept = $('.rules').textContent.match(/: (.+) /)[1].replace(/\w+/g, function(type) {
_results.push(submit.value = cooldown.duration);
}
return _results;
} else {
for (_j = 0, _len2 = submits.length; _j < _len2; _j++) {
submit = submits[_j];
submit.disabled = false;
submit.value = 'Submit';
}
return qr.autoPost();
}
}
};
qr = {
init: function() {
var iframe;
g.callbacks.push(qr.node);
$.bind($('#recaptcha_challenge_field_holder'), 'DOMNodeInserted', qr.captchaNode);
qr.captchaTime = Date.now();
qr.spoiler = $('.postarea label') ? '<label> [<input type=checkbox name=spoiler>Spoiler Image?]</label>' : '';
qr.acceptFiles = $('.rules').textContent.match(/: (.+) /)[1].replace(/\w+/g, function(type) {
switch (type) { switch (type) {
case 'JPG': case 'JPG':
return 'image/JPEG'; return 'image/JPEG';
@ -1283,307 +1238,334 @@
return 'image/' + type; return 'image/' + type;
} }
}); });
iframe = $.el('iframe', { QR.MAX_FILE_SIZE = $('input[name=MAX_FILE_SIZE]').value;
name: 'iframe', QR.spoiler = $('.postarea label') ? ' <label>[<input type=checkbox name=spoiler>Spoiler Image?]</label>' : '';
hidden: true if (conf['Persistent QR']) {
}); QR.dialog();
$.add(d.body, iframe); $('textarea', QR.qr).blur();
return $('#recaptcha_response_field').id = ''; if (conf['Auto Hide QR']) {
}, $('#autohide', QR.qr).checked = true;
attach: function() {
var fileDiv;
fileDiv = $.el('div', {
innerHTML: "<input type=file name=upfile accept='" + qr.acceptFiles + "'><a>X</a>"
});
$.bind(fileDiv.firstChild, 'change', qr.validateFileSize);
$.bind(fileDiv.lastChild, 'click', (function() {
return $.rm(this.parentNode);
}));
return $.add($('#files', qr.el), fileDiv);
},
attachNext: function() {
var file, fileDiv, oldFile;
fileDiv = $.rm($('#files div', qr.el));
file = fileDiv.firstChild;
oldFile = $('#qr_form input[type=file]', qr.el);
return $.replace(oldFile, file);
},
autoPost: function() {
if (qr.el && $('#auto', qr.el).checked) {
return qr.submit.call($('form', qr.el));
}
},
captchaNode: function(e) {
if (!qr.el) {
return;
}
val = e.target.value;
$('img', qr.el).src = "http://www.google.com/recaptcha/api/image?c=" + val;
qr.challenge = val;
return qr.captchaTime = Date.now();
},
captchaKeydown: function(e) {
var captchas;
if (!(e.keyCode === 13 && this.value)) {
return;
}
captchas = $.get('captchas', []);
captchas.push({
challenge: qr.challenge,
response: this.value,
time: qr.captchaTime
});
$.set('captchas', captchas);
$('#captchas', qr.el).textContent = captchas.length + ' captchas';
Recaptcha.reload();
this.value = '';
if (!$('textarea', qr.el).value && !$('input[type=file]', qr.el).files.length) {
return e.preventDefault();
}
},
close: function() {
$.rm(qr.el);
return qr.el = null;
},
dialog: function(link) {
var THREAD_ID, c, email, html, m, name, pwd, submitDisabled, submitValue;
c = d.cookie;
name = (m = c.match(/4chan_name=([^;]+)/)) ? decodeURIComponent(m[1]) : '';
email = (m = c.match(/4chan_email=([^;]+)/)) ? decodeURIComponent(m[1]) : '';
pwd = (m = c.match(/4chan_pass=([^;]+)/)) ? decodeURIComponent(m[1]) : $('input[name=pwd]').value;
submitValue = $('#com_submit').value;
submitDisabled = $('#com_submit').disabled ? 'disabled' : '';
THREAD_ID = g.THREAD_ID || $.x('ancestor::div[@class="thread"]/div', link).id;
qr.challenge = $('#recaptcha_challenge_field').value;
html = " <a id=close title=close>X</a> <input type=checkbox id=autohide title=autohide> <div class=move> <input class=inputtext type=text name=name value='" + name + "' placeholder=Name form=qr_form> Quick Reply </div> <div class=autohide> <form name=post action=http://sys.4chan.org/" + g.BOARD + "/post method=POST enctype=multipart/form-data target=iframe id=qr_form> <input type=hidden name=resto value=" + THREAD_ID + "> <input type=hidden name=mode value=regist> <input type=hidden name=recaptcha_challenge_field id=recaptcha_challenge_field> <input type=hidden name=recaptcha_response_field id=recaptcha_response_field> <div><input class=inputtext type=text name=email value='" + email + "' placeholder=E-mail>" + qr.spoiler + "</div> <div><input class=inputtext type=text name=sub placeholder=Subject><input type=submit value=" + submitValue + " id=com_submit " + submitDisabled + "><label><input type=checkbox id=auto>auto</label></div> <div><textarea class=inputtext name=com placeholder=Comment></textarea></div> <div><img src=http://www.google.com/recaptcha/api/image?c=" + qr.challenge + "></div> <div><input class=inputtext type=text autocomplete=off placeholder=Verification id=dummy><span id=captchas>" + ($.get('captchas', []).length) + " captchas</span></div> <div><input type=file name=upfile accept='" + qr.acceptFiles + "'></div> </form> <div id=files></div> <div><input class=inputtext type=password name=pwd value='" + pwd + "' placeholder=Password form=qr_form maxlength=8><a id=attach>attach another file</a></div> </div> <a id=error class=error></a> ";
qr.el = ui.dialog('qr', 'top: 0; left: 0;', html);
$.bind($('input[name=name]', qr.el), 'mousedown', function(e) {
return e.stopPropagation();
});
$.bind($('input[name=upfile]', qr.el), 'change', qr.validateFileSize);
$.bind($('#close', qr.el), 'click', qr.close);
$.bind($('form', qr.el), 'submit', qr.submit);
$.bind($('#attach', qr.el), 'click', qr.attach);
$.bind($('img', qr.el), 'click', Recaptcha.reload);
$.bind($('#dummy', qr.el), 'keydown', Recaptcha.listener);
$.bind($('#dummy', qr.el), 'keydown', qr.captchaKeydown);
return $.add(d.body, qr.el);
},
message: function(data) {
var duration, fileCount;
$('iframe[name=iframe]').src = 'about:blank';
fileCount = $('#files', qr.el).childElementCount;
if (data) {
data = JSON.parse(data);
$.extend($('#error', qr.el), data);
$('#recaptcha_response_field', qr.el).value = '';
$('#autohide', qr.el).checked = false;
if (data.textContent === 'You seem to have mistyped the verification.') {
setTimeout(qr.autoPost, 1000);
} else if (data.textContent === 'Error: Duplicate file entry detected.' && fileCount) {
$('textarea', qr.el).value += '\n' + data.textContent + ' ' + data.href;
qr.attachNext();
setTimeout(qr.autoPost, 1000);
}
return;
}
if (qr.el) {
if (g.REPLY && (conf['Persistent QR'] || fileCount)) {
qr.refresh();
if (fileCount) {
qr.attachNext();
}
} else {
qr.close();
} }
} }
if (conf['Cooldown']) { if (conf['Cooldown']) {
duration = qr.sage ? 60 : 30; return $.bind(window, 'storage', function(e) {
$.set(g.BOARD + '/cooldown', Date.now() + duration * 1000); if (e.key === ("" + NAMESPACE + "cooldown/" + g.BOARD)) {
return cooldown.start(); return QR.cooldown();
}
});
} }
}, },
node: function(root) { attach: function() {
var quote; var box, file, files;
quote = $('a.quotejs:not(:first-child)', root); files = $('#files', QR.qr);
return $.bind(quote, 'click', qr.quote); box = $.el('span', {
innerHTML: "<input type=file name=upfile accept='" + QR.accept + "'><img alt='click here'><a class=x>X</a>"
});
file = $('input', box);
$.bind(file, 'change', QR.change);
$.bind($('img', box), 'click', function() {
return this.previousSibling.click();
});
$.bind($('.x', box), 'click', function() {
return $.rm(this.parentNode);
});
$.add(files, box);
return file.click();
}, },
postInvalid: function() { captchaNode: function(e) {
var captcha, captchas, content, cutoff, dummy, response; QR.captcha = {
content = $('textarea', qr.el).value || $('input[type=file]', qr.el).files.length; challenge: e.target.value,
if (!content) { time: Date.now()
return 'Error: No text entered.'; };
return QR.captchaImg();
},
captchaImg: function() {
var c, qr;
qr = QR.qr;
if (!qr) {
return;
} }
/* c = QR.captcha.challenge;
captchas expire after 5 hours (emperically verified). cutoff 5 minutes return $('#captcha img', qr).src = "http://www.google.com/recaptcha/api/image?c=" + c;
before then, b/c posting takes time. },
*/ captchaPush: function(el) {
cutoff = Date.now() - 5 * HOUR + 5 * MINUTE; var captcha, captchas;
captcha = QR.captcha;
captcha.response = el.value;
captchas = $.get('captchas', []); captchas = $.get('captchas', []);
captchas.push(captcha);
$.set('captchas', captchas);
el.value = '';
QR.captchaReload();
return QR.captchaLength(captchas);
},
captchaShift: function() {
var captcha, captchas, cutoff;
captchas = $.get('captchas', []);
cutoff = Date.now() - 5 * HOUR + 5 * MINUTE;
while (captcha = captchas.shift()) { while (captcha = captchas.shift()) {
if (captcha.time > cutoff) { if (captcha.time > cutoff) {
break; break;
} }
} }
$.set('captchas', captchas); $.set('captchas', captchas);
$('#captchas', qr.el).textContent = captchas.length + ' captchas'; QR.captchaLength(captchas);
if (!captcha) { return captcha;
dummy = $('#dummy', qr.el);
if (!(response = dummy.value)) {
return 'You forgot to type in the verification';
}
captcha = {
challenge: qr.challenge,
response: response
};
dummy.value = '';
Recaptcha.reload();
}
$('#recaptcha_challenge_field', qr.el).value = captcha.challenge;
$('#recaptcha_response_field', qr.el).value = captcha.response;
return false;
}, },
quote: function(e) { captchaLength: function(captchas) {
var id, s, selection, selectionID, ta, text, _ref; captchas || (captchas = $.get('captchas', []));
if (e) { return $('#cl', QR.qr).textContent = captchas.length + ' captchas';
},
captchaReload: function() {
return window.location = 'javascript:Recaptcha.reload()';
},
change: function(e) {
var file, fr, img, qr;
file = this.files[0];
if (file.size > QR.MAX_FILE_SIZE) {
alert('Error: File too large.');
$.rm(this.parentNode);
QR.attach();
return;
}
qr = QR.qr;
fr = new FileReader();
img = this.nextSibling;
fr.onload = function(e) {
return img.src = e.target.result;
};
return fr.readAsDataURL(file);
},
close: function() {
$.rm(QR.qr);
return QR.qr = null;
},
cooldown: function() {
var b, cooldown, n, now;
if (!(g.REPLY && QR.qr)) {
return;
}
cooldown = $.get("cooldown/" + g.BOARD, 0);
now = Date.now();
n = Math.ceil((cooldown - now) / 1000);
b = $('form button', QR.qr);
if (n > 0) {
$.extend(b, {
textContent: n,
disabled: true
});
return setTimeout(QR.cooldown, 1000);
} else {
$.extend(b, {
textContent: 'Submit',
disabled: false
});
if ($('#autopost', QR.qr).checked) {
return QR.submit();
}
}
},
dialog: function(text, tid) {
var c, l, m, qr, ta;
if (text == null) {
text = '';
}
tid || (tid = g.THREAD_ID || '');
QR.qr = qr = ui.dialog('qr', 'top: 0; left: 0;', " <a class=close>X</a> <input type=checkbox id=autohide title=autohide> <div class=move> <span class=click> <button>File</button> <input form=qr_form placeholder=Subject name=sub> <input form=qr_form placeholder=Name name=name> <input form=qr_form placeholder=Email name=email> </span> </div> <div class=autohide> <textarea form=qr_form placeholder=Comment name=com></textarea> <div id=files></div> <form enctype=multipart/form-data method=post action=http://sys.4chan.org/" + g.BOARD + "/post target=iframe id=qr_form> <div hidden> <input name=pwd> <input name=mode value=regist> <input name=recaptcha_challenge_field id=challenge> <input name=recaptcha_response_field id=response> </div> <div id=captcha> <div><img></div> <span id=cl>120 Captchas</span> <input id=recaptcha_response_field autocomplete=off> </div> <div> <button>Submit</button> " + (g.REPLY ? "<label>[<input type=checkbox id=autopost title=autopost> Autopost]</label>" : '') + " <input form=qr_form placeholder=Thread name=resto value=" + tid + " " + (g.REPLY ? 'hidden' : '') + "> " + QR.spoiler + " </div> </form> </div> <a class=error></a> ");
c = d.cookie;
$('[name=name]', qr).value = (m = c.match(/4chan_name=([^;]+)/)) ? decodeURIComponent(m[1]) : '';
$('[name=email]', qr).value = (m = c.match(/4chan_email=([^;]+)/)) ? decodeURIComponent(m[1]) : '';
$('[name=pwd]', qr).value = (m = c.match(/4chan_pass=([^;]+)/)) ? decodeURIComponent(m[1]) : $('input[name=pwd]').value;
$('textarea', qr).value = text;
if (conf['Cooldown']) {
QR.cooldown();
}
$.bind($('button', qr), 'click', QR.attach);
$.bind($('.close', qr), 'click', QR.close);
$.bind($('.click', qr), 'mousedown', function(e) {
return e.stopPropagation();
});
$.bind($('form', qr), 'submit', QR.submit);
$.bind($('#recaptcha_response_field', qr), 'keydown', QR.keydown);
QR.captchaImg();
QR.captchaLength();
$.add(d.body, qr);
ta = $('textarea', qr);
l = text.length;
ta.setSelectionRange(l, l);
return ta.focus();
},
keydown: function(e) {
var kc, v;
kc = e.keyCode;
v = this.value;
if (kc === 8 && !v) {
QR.captchaReload();
return;
}
if (!(e.keyCode === 13 && v)) {
return;
}
QR.captchaPush(this);
e.preventDefault();
return QR.submit();
},
quote: function(e, blank) {
var i, id, qr, s, sel, ss, ta, text, tid, v, _base, _ref, _ref2;
if (e != null) {
e.preventDefault(); e.preventDefault();
} }
if (qr.el) { tid = (_ref = $.x('ancestor::div[@class="thread"]/div', this)) != null ? _ref.id : void 0;
$('#autohide', qr.el).checked = false;
} else {
qr.dialog(this);
}
id = this.textContent; id = this.textContent;
text = ">>" + id + "\n"; text = ">>" + id + "\n";
selection = window.getSelection(); sel = getSelection();
if (s = selection.toString()) { if (id === ((_ref2 = $.x('preceding::input[@type="checkbox"][1]', sel.anchorNode)) != null ? _ref2.name : void 0)) {
selectionID = (_ref = $.x('preceding::input[@type="checkbox"][1]', selection.anchorNode)) != null ? _ref.name : void 0; if (s = sel.toString().replace(/\n/g, '\n>')) {
if (selectionID === id) {
s = s.replace(/\n/g, '\n>');
text += ">" + s + "\n"; text += ">" + s + "\n";
} }
} }
ta = $('textarea', qr.el); qr = QR.qr;
if (!qr) {
QR.dialog(text, tid);
return;
}
$('#autohide', qr).checked = false;
ta = $('textarea', qr);
v = ta.value;
ss = ta.selectionStart;
ta.value = v.slice(0, ss) + text + v.slice(ss);
i = ss + text.length;
ta.setSelectionRange(i, i);
ta.focus(); ta.focus();
return ta.value += text; return (_base = $('[name=resto]', qr)).value || (_base.value = tid);
}, },
refresh: function() { receive: function(data) {
var m, newFile, oldFile, _ref; var cooldown, qr, row, tc, _ref, _ref2;
$('[name=sub]', qr.el).value = ''; $('iframe[name=iframe]').src = 'about:blank';
$('[name=email]', qr.el).value = (m = d.cookie.match(/4chan_email=([^;]+)/)) ? decodeURIComponent(m[1]) : ''; qr = QR.qr;
$('[name=com]', qr.el).value = ''; row = (_ref = $('#files input[form]', qr)) != null ? _ref.parentNode : void 0;
$('[name=recaptcha_response_field]', qr.el).value = ''; if (data) {
if (QR.op) {
window.location = data;
return;
}
data = JSON.parse(data);
$.extend($('a.error', qr), data);
tc = data.textContent;
if (tc === 'Error: Duplicate file entry detected.') {
if (row) {
$.rm(row);
}
setTimeout(QR.submit, 1000);
} else if (tc === 'You seem to have mistyped the verification.') {
setTimeout(QR.submit, 1000);
}
return;
}
if (row) {
$.rm(row);
}
if (conf['Persistent QR'] || ((_ref2 = $('#files input', qr)) != null ? _ref2.files.length : void 0)) {
QR.reset();
} else {
QR.close();
}
if (conf['Cooldown']) {
cooldown = Date.now() + (QR.sage ? 60 : 30) * SECOND;
$.set("cooldown/" + g.BOARD, cooldown);
return QR.cooldown();
}
},
reset: function() {
var _ref;
if (!conf['Remember Spoiler']) { if (!conf['Remember Spoiler']) {
if ((_ref = $('[name=spoiler]', qr.el)) != null) { if ((_ref = $('[name=spoiler]', QR.qr)) != null) {
_ref.checked = false; _ref.checked = false;
} }
} }
oldFile = $('[type=file]', qr.el); return $('textarea', QR.qr).value = '';
newFile = $.el('input', {
type: 'file',
name: 'upfile',
accept: qr.acceptFiles
});
return $.replace(oldFile, newFile);
}, },
submit: function(e) { submit: function(e) {
var id, msg, op; var captcha, challenge, el, id, input, op, qr, response, _ref;
if (msg = qr.postInvalid()) { if ($('form button', qr).disabled) {
if (typeof e.preventDefault === "function") { return;
}
if (!($('textarea', QR.qr).value || ((_ref = $('[type=file]', QR.qr)) != null ? _ref.files.length : void 0))) {
if (e) {
alert('Error: No text entered.');
e.preventDefault(); e.preventDefault();
} }
alert(msg);
if (msg === 'You forgot to type in the verification.') {
$('#dummy', qr.el).focus();
}
return; return;
} }
if (conf['Auto Watch Reply'] && conf['Thread Watcher']) { qr = QR.qr;
if (g.REPLY && $('img.favicon').src === Favicon.empty) { $('.error', qr).textContent = '';
watcher.watch(null, g.THREAD_ID); if (e && (el = $('#recaptcha_response_field', qr)).value) {
} else { QR.captchaPush(el);
id = $('input[name=resto]', qr.el).value; }
op = $.id(id); if (!(captcha = QR.captchaShift())) {
if ($('img.favicon', op).src === Favicon.empty) { alert('You forgot to type in the verification.');
watcher.watch(op, id); if (e != null) {
} e.preventDefault();
} }
return;
}
challenge = captcha.challenge, response = captcha.response;
$('#challenge', qr).value = challenge;
$('#response', qr).value = response;
if (conf['Auto Hide QR']) {
$('#autohide', qr).checked = true;
}
if (input = $('#files input', qr)) {
input.setAttribute('form', 'qr_form');
} }
if (!e) { if (!e) {
this.submit(); $('#qr_form', qr).submit();
} }
$('#error', qr.el).textContent = ''; QR.sage = /sage/i.test($('[name=email]', qr).value);
if (conf['Auto Hide QR']) { id = $('input[name=resto]', qr).value;
$('#autohide', qr.el).checked = true; QR.op = !id;
if (QR.op) {
$('[name=email]', qr).value = 'noko';
}
if (conf['Thread Watcher'] && conf['Auto Watch Reply']) {
op = $.id(id);
if ($('img.favicon', op).src === Favicon.empty) {
return watcher.watch(op, id);
}
} }
return qr.sage = /sage/i.test($('input[name=email]', this).value);
}, },
sys: function() { sys: function() {
var c, duration, id, noko, recaptcha, sage, search, thread, url, watch, _, _ref, _ref2; var recaptcha;
if (recaptcha = $('#recaptcha_response_field')) { if (recaptcha = $('#recaptcha_response_field')) {
$.bind(recaptcha, 'keydown', Recaptcha.listener); $.bind(recaptcha, 'keydown', QR.keydown);
return; return;
} }
/* /*
http://code.google.com/p/chromium/issues/detail?id=20773 http://code.google.com/p/chromium/issues/detail?id=20773
Let content scripts see other frames (instead of them being undefined) Let content scripts see other frames (instead of them being undefined)
To access the parent, we have to break out of the sandbox and evaluate To access the parent, we have to break out of the sandbox and evaluate
in the global context. in the global context.
*/ */
$.globalEval(function() { return $.globalEval(function() {
var data, href, node, textContent, _ref; var data, href, node, textContent, _ref;
if (node = (_ref = document.querySelector('table font b')) != null ? _ref.firstChild : void 0) { $ = function(css) {
return document.querySelector(css);
};
if (node = (_ref = $('table font b')) != null ? _ref.firstChild : void 0) {
textContent = node.textContent, href = node.href; textContent = node.textContent, href = node.href;
data = JSON.stringify({ data = JSON.stringify({
textContent: textContent, textContent: textContent,
href: href href: href
}); });
} else { } else if (node = $('meta')) {
data = ''; data = node.content.match(/url=(.+)/)[1];
if (/#/.test(data)) {
data = '';
}
} }
return parent.postMessage(data, '*'); return parent.postMessage(data, '*');
}); });
c = (_ref = $('b')) != null ? _ref.lastChild : void 0;
if (!(c && c.nodeType === 8)) {
return;
}
_ref2 = c.textContent.match(/thread:(\d+),no:(\d+)/), _ = _ref2[0], thread = _ref2[1], id = _ref2[2];
search = location.search;
cooldown = /cooldown/.test(search);
noko = /noko/.test(search);
sage = /sage/.test(search);
watch = /watch/.test(search);
url = "http://boards.4chan.org/" + g.BOARD;
if (watch && thread === '0') {
url += "/res/" + id + "?watch";
} else if (noko) {
url += '/res/';
url += thread === '0' ? id : thread;
}
if (cooldown) {
duration = Date.now() + (sage ? 60 : 30) * 1000;
url += '?cooldown=' + duration;
}
if (noko) {
url += '#' + id;
}
return window.location = url;
},
validateFileSize: function(e) {
var file;
if (!(this.files[0].size > $('input[name=MAX_FILE_SIZE]').value)) {
return;
}
file = $.el('input', {
type: 'file',
name: 'upfile',
accept: qr.acceptFiles
});
$.bind(file, 'change', qr.validateFileSize);
$.replace(this, file);
$('#error', qr.el).textContent = 'Error: File too large.';
return alert('Error: File too large.');
} }
}; };
threading = { threading = {
@ -2594,25 +2576,6 @@
} }
return location.href = url; return location.href = url;
}; };
Recaptcha = {
init: function() {
var el, _i, _len, _ref;
_ref = $$('#recaptcha_table a');
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
el = _ref[_i];
el.tabIndex = 1;
}
return $.bind($('#recaptcha_response_field'), 'keydown', Recaptcha.listener);
},
listener: function(e) {
if (e.keyCode === 8 && this.value === '') {
return Recaptcha.reload();
}
},
reload: function() {
return window.location = 'javascript:Recaptcha.reload()';
}
};
nodeInserted = function(e) { nodeInserted = function(e) {
var callback, target, _i, _len, _ref, _results; var callback, target, _i, _len, _ref, _results;
target = e.target; target = e.target;
@ -2815,9 +2778,9 @@
}; };
Main = { Main = {
init: function() { init: function() {
var callback, canPost, cutoff, form, hiddenThreads, id, lastChecked, now, op, table, timestamp, tzOffset, _i, _j, _k, _l, _len, _len2, _len3, _len4, _ref, _ref2, _ref3, _ref4, _ref5; var callback, cutoff, hiddenThreads, id, lastChecked, now, op, table, timestamp, tzOffset, _i, _j, _k, _l, _len, _len2, _len3, _len4, _ref, _ref2, _ref3, _ref4, _ref5;
if (location.hostname === 'sys.4chan.org') { if (location.hostname === 'sys.4chan.org') {
qr.sys(); QR.sys();
return; return;
} }
if (conf['404 Redirect'] && d.title === '4chan - 404' && /^\d+$/.test(g.THREAD_ID)) { if (conf['404 Redirect'] && d.title === '4chan - 404' && /^\d+$/.test(g.THREAD_ID)) {
@ -2858,26 +2821,10 @@
$.set("hiddenReplies/" + g.BOARD + "/", g.hiddenReplies); $.set("hiddenReplies/" + g.BOARD + "/", g.hiddenReplies);
} }
$.addStyle(Main.css); $.addStyle(Main.css);
if ((form = $('form[name=post]')) && (canPost = !!$('#recaptcha_response_field'))) {
Recaptcha.init();
if (g.REPLY && conf['Auto Watch Reply'] && conf['Thread Watcher']) {
$.bind(form, 'submit', function() {
if ($('img.favicon').src === Favicon.empty) {
return watcher.watch(null, g.THREAD_ID);
}
});
}
}
threading.init(); threading.init();
if (g.REPLY && (id = location.hash.slice(1)) && /\d/.test(id[0]) && !$.id(id)) { if (g.REPLY && (id = location.hash.slice(1)) && /\d/.test(id[0]) && !$.id(id)) {
scrollTo(0, d.body.scrollHeight); scrollTo(0, d.body.scrollHeight);
} }
if (conf['Auto Noko'] && canPost) {
form.action += '?noko';
}
if (conf['Cooldown'] && canPost) {
cooldown.init();
}
if (conf['Image Expansion']) { if (conf['Image Expansion']) {
imgExpand.init(); imgExpand.init();
} }
@ -2902,8 +2849,8 @@
if (conf['Reply Hiding']) { if (conf['Reply Hiding']) {
replyHiding.init(); replyHiding.init();
} }
if (conf['Quick Reply'] && canPost) { if (conf['Quick Reply']) {
qr.init(); QR.init();
} }
if (conf['Report Button']) { if (conf['Report Button']) {
reportButton.init(); reportButton.init();
@ -2933,12 +2880,6 @@
if (conf['Image Preloading']) { if (conf['Image Preloading']) {
imgPreloading.init(); imgPreloading.init();
} }
if (conf['Quick Reply'] && conf['Persistent QR'] && canPost) {
qr.dialog();
if (conf['Auto Hide QR']) {
$('#autohide', qr.el).checked = true;
}
}
if (conf['Post in Title']) { if (conf['Post in Title']) {
titlePost.init(); titlePost.init();
} }
@ -2993,7 +2934,7 @@
var data, origin; var data, origin;
origin = e.origin, data = e.data; origin = e.origin, data = e.data;
if (origin === 'http://sys.4chan.org') { if (origin === 'http://sys.4chan.org') {
return qr.message(data); return QR.receive(data);
} }
}, },
css: '\ css: '\
@ -3004,7 +2945,7 @@
div.dialog > div.move {\ div.dialog > div.move {\
cursor: move;\ cursor: move;\
}\ }\
label, a, .favicon, #qr img {\ label, a, .favicon {\
cursor: pointer;\ cursor: pointer;\
}\ }\
\ \
@ -3014,12 +2955,6 @@
.error {\ .error {\
color: red;\ color: red;\
}\ }\
#error {\
cursor: default;\
}\
#error[href] {\
cursor: pointer;\
}\
td.replyhider {\ td.replyhider {\
vertical-align: top;\ vertical-align: top;\
}\ }\
@ -3097,47 +3032,6 @@
margin: 0;\ margin: 0;\
width: 100%;\ width: 100%;\
}\ }\
\
#qr {\
position: fixed;\
max-height: 100%;\
overflow-x: hidden;\
overflow-y: auto;\
}\
#qr > div.move {\
text-align: right;\
}\
#qr input[name=name] {\
float: left;\
}\
#qr_form {\
clear: left;\
}\
#qr_form, #qr #com_submit, #qr input[name=upfile] {\
margin: 0;\
}\
#qr textarea {\
width: 100%;\
height: 125px;\
}\
#qr #close, #qr #autohide {\
float: right;\
}\
#qr:not(:hover) > #autohide:checked ~ .autohide {\
height: 0;\
overflow: hidden;\
}\
/* http://stackoverflow.com/questions/2610497/change-an-inputs-html5-placeholder-color-with-css */\
#qr input::-webkit-input-placeholder {\
color: grey;\
}\
#qr input:-moz-placeholder {\
color: grey;\
}\
/* qr reCAPTCHA */\
#qr img {\
border: 1px solid #AAA;\
}\
\ \
#updater {\ #updater {\
position: fixed;\ position: fixed;\
@ -3203,6 +3097,68 @@
#files > input {\ #files > input {\
display: block;\ display: block;\
}\ }\
#qr {\
max-height: 100%;\
overflow-y: auto;\
position: fixed;\
}\
#qr #autohide, #qr .close {\
float: right;\
}\
#qr .click input {\
width: 73px;\
}\
#qr .click * {\
float: left;\
}\
#qr form {\
margin: 0;\
}\
#qr:not(:hover) #autohide:checked ~ .autohide {\
height: 0;\
overflow: hidden;\
}\
#qr textarea {\
border: 0;\
height: 150px;\
width: 100%;\
}\
#qr #captcha {\
position: relative;\
}\
#qr #files {\
width: 300px;\
white-space: nowrap;\
overflow: auto;\
}\
#qr #files span {\
position: relative;\
}\
#qr #files a {\
position: absolute;\
left: 0;\
font-size: 50px;\
color: red;\
}\
#qr #cl {\
right: 0;\
padding: 2px;\
position: absolute;\
}\
#qr #recaptcha_response_field {\
display: inline;\
width: 100%;\
}\
#qr #files input {\
display: none;\
}\
#qr #files img {\
max-height: 100px;\
max-width: 100px;\
}\
#qr input[name=resto] {\
width: 80px;\
}\
' '
}; };
if (d.body) { if (d.body) {

View File

@ -656,15 +656,13 @@ keybinds =
imgExpand.toggle thumb.parentNode imgExpand.toggle thumb.parentNode
qr: (thread, quote) -> qr: (thread, quote) ->
unless qrLink = $ 'td.replyhl span[id] a:not(:first-child)', thread
qrLink = $ "span[id^=nothread] a:not(:first-child)", thread
if quote if quote
qr.quote.call qrLink QR.quote.call $ 'a.quotejs + a', $('td.replyhl', thread) or thread
else else
unless qr.el if QR.qr
qr.dialog qrLink $('textarea', QR.qr).focus()
$('textarea', qr.el).focus() else
QR.dialog '', thread?.firstChild.id
open: (thread, tab) -> open: (thread, tab) ->
id = thread.firstChild.id id = thread.firstChild.id
@ -936,49 +934,25 @@ options =
conf['backlink'] = @value conf['backlink'] = @value
$('#backlinkPreview').textContent = conf['backlink'].replace /%id/, '123456789' $('#backlinkPreview').textContent = conf['backlink'].replace /%id/, '123456789'
cooldown = QR =
#TODO merge into qr #captcha caching for report form
#report queueing
#check if captchas can be reused on eg dup file error
init: -> init: ->
if match = location.search.match /cooldown=(\d+)/ #can't reply in some stickies, recaptcha may be blocked, eg by noscript
[_, time] = match return unless $('form[name=post]') and $('#recaptcha_response_field')
$.set g.BOARD+'/cooldown', time if $.get(g.BOARD+'/cooldown', 0) < time g.callbacks.push (root) ->
cooldown.start() if Date.now() < $.get g.BOARD+'/cooldown', 0 quote = $ 'a.quotejs + a', root
$.bind window, 'storage', (e) -> cooldown.start() if e.key is "#{NAMESPACE}#{g.BOARD}/cooldown" $.bind quote, 'click', QR.quote
$('.postarea form').action += '?cooldown' if g.REPLY $.add d.body, $.el 'iframe',
name: 'iframe'
start: -> hidden: true
cooldown.duration = Math.ceil ($.get(g.BOARD+'/cooldown', 0) - Date.now()) / 1000 # nuke id so qr's field focuses on recaptcha reload, instead of normal form's
return unless cooldown.duration > 0 $('#recaptcha_response_field').id = ''
for submit in $$ '#com_submit' holder = $ '#recaptcha_challenge_field_holder'
submit.value = cooldown.duration $.bind holder, 'DOMNodeInserted', QR.captchaNode
submit.disabled = true QR.captchaNode target: holder.firstChild
setTimeout cooldown.cb, 1000 QR.accept = $('.rules').textContent.match(/: (.+) /)[1].replace /\w+/g, (type) ->
cb: ->
submits = $$ '#com_submit'
if --cooldown.duration
setTimeout cooldown.cb, 1000
for submit in submits
submit.value = cooldown.duration
else
for submit in submits
submit.disabled = false
submit.value = 'Submit'
qr.autoPost()
qr =
# TODO
# error handling / logging
# persistent captcha
# rm Recaptcha
# email reverts
init: ->
g.callbacks.push qr.node
$.bind $('#recaptcha_challenge_field_holder'), 'DOMNodeInserted', qr.captchaNode
qr.captchaTime = Date.now()
qr.spoiler = if $('.postarea label') then '<label> [<input type=checkbox name=spoiler>Spoiler Image?]</label>' else ''
qr.acceptFiles = $('.rules').textContent.match(/: (.+) /)[1].replace /\w+/g, (type) ->
switch type switch type
when 'JPG' when 'JPG'
'image/JPEG' 'image/JPEG'
@ -986,284 +960,261 @@ qr =
'application/' + type 'application/' + type
else else
'image/' + type 'image/' + type
QR.MAX_FILE_SIZE = $('input[name=MAX_FILE_SIZE]').value
iframe = $.el 'iframe', QR.spoiler = if $('.postarea label') then ' <label>[<input type=checkbox name=spoiler>Spoiler Image?]</label>' else ''
name: 'iframe' if conf['Persistent QR']
hidden: true QR.dialog()
$.add d.body, iframe $('textarea', QR.qr).blur()
if conf['Auto Hide QR']
#hack - nuke id so it doesn't grab focus when reloading $('#autohide', QR.qr).checked = true
$('#recaptcha_response_field').id = ''
attach: ->
fileDiv = $.el 'div', innerHTML: "<input type=file name=upfile accept='#{qr.acceptFiles}'><a>X</a>"
$.bind fileDiv.firstChild, 'change', qr.validateFileSize
$.bind fileDiv.lastChild, 'click', (-> $.rm @parentNode)
$.add $('#files', qr.el), fileDiv
attachNext: ->
fileDiv = $.rm $('#files div', qr.el)
file = fileDiv.firstChild
oldFile = $ '#qr_form input[type=file]', qr.el
$.replace oldFile, file
autoPost: ->
if qr.el and $('#auto', qr.el).checked
qr.submit.call $ 'form', qr.el
captchaNode: (e) ->
return unless qr.el
val = e.target.value
$('img', qr.el).src = "http://www.google.com/recaptcha/api/image?c=" + val
qr.challenge = val
qr.captchaTime = Date.now()
captchaKeydown: (e) ->
return unless e.keyCode is 13 and @value #enter, captcha filled
captchas = $.get 'captchas', []
captchas.push
challenge: qr.challenge
response: @value
time: qr.captchaTime
$.set 'captchas', captchas
$('#captchas', qr.el).textContent = captchas.length + ' captchas'
Recaptcha.reload()
@value = ''
if !$('textarea', qr.el).value and !$('input[type=file]', qr.el).files.length
e.preventDefault()
close: ->
$.rm qr.el
qr.el = null
dialog: (link) ->
c = d.cookie
name = if m = c.match(/4chan_name=([^;]+)/) then decodeURIComponent m[1] else ''
email = if m = c.match(/4chan_email=([^;]+)/) then decodeURIComponent m[1] else ''
pwd = if m = c.match(/4chan_pass=([^;]+)/) then decodeURIComponent m[1] else $('input[name=pwd]').value
submitValue = $('#com_submit').value
submitDisabled = if $('#com_submit').disabled then 'disabled' else ''
#FIXME inlined cross-thread quotes
THREAD_ID = g.THREAD_ID or $.x('ancestor::div[@class="thread"]/div', link).id
qr.challenge = $('#recaptcha_challenge_field').value
html = "
<a id=close title=close>X</a>
<input type=checkbox id=autohide title=autohide>
<div class=move>
<input class=inputtext type=text name=name value='#{name}' placeholder=Name form=qr_form>
Quick Reply
</div>
<div class=autohide>
<form name=post action=http://sys.4chan.org/#{g.BOARD}/post method=POST enctype=multipart/form-data target=iframe id=qr_form>
<input type=hidden name=resto value=#{THREAD_ID}>
<input type=hidden name=mode value=regist>
<input type=hidden name=recaptcha_challenge_field id=recaptcha_challenge_field>
<input type=hidden name=recaptcha_response_field id=recaptcha_response_field>
<div><input class=inputtext type=text name=email value='#{email}' placeholder=E-mail>#{qr.spoiler}</div>
<div><input class=inputtext type=text name=sub placeholder=Subject><input type=submit value=#{submitValue} id=com_submit #{submitDisabled}><label><input type=checkbox id=auto>auto</label></div>
<div><textarea class=inputtext name=com placeholder=Comment></textarea></div>
<div><img src=http://www.google.com/recaptcha/api/image?c=#{qr.challenge}></div>
<div><input class=inputtext type=text autocomplete=off placeholder=Verification id=dummy><span id=captchas>#{$.get('captchas', []).length} captchas</span></div>
<div><input type=file name=upfile accept='#{qr.acceptFiles}'></div>
</form>
<div id=files></div>
<div><input class=inputtext type=password name=pwd value='#{pwd}' placeholder=Password form=qr_form maxlength=8><a id=attach>attach another file</a></div>
</div>
<a id=error class=error></a>
"
qr.el = ui.dialog 'qr', 'top: 0; left: 0;', html
$.bind $('input[name=name]', qr.el), 'mousedown', (e) -> e.stopPropagation()
$.bind $('input[name=upfile]', qr.el), 'change', qr.validateFileSize
$.bind $('#close', qr.el), 'click', qr.close
$.bind $('form', qr.el), 'submit', qr.submit
$.bind $('#attach', qr.el), 'click', qr.attach
$.bind $('img', qr.el), 'click', Recaptcha.reload
$.bind $('#dummy', qr.el), 'keydown', Recaptcha.listener
$.bind $('#dummy', qr.el), 'keydown', qr.captchaKeydown
$.add d.body, qr.el
message: (data) ->
$('iframe[name=iframe]').src = 'about:blank'
fileCount = $('#files', qr.el).childElementCount
if data # error message
data = JSON.parse data
$.extend $('#error', qr.el), data
$('#recaptcha_response_field', qr.el).value = ''
$('#autohide', qr.el).checked = false
if data.textContent is 'You seem to have mistyped the verification.'
setTimeout qr.autoPost, 1000
else if data.textContent is 'Error: Duplicate file entry detected.' and fileCount
$('textarea', qr.el).value += '\n' + data.textContent + ' ' + data.href
qr.attachNext()
setTimeout qr.autoPost, 1000
return
if qr.el
if g.REPLY and (conf['Persistent QR'] or fileCount)
qr.refresh()
if fileCount
qr.attachNext()
else
qr.close()
if conf['Cooldown'] if conf['Cooldown']
duration = if qr.sage then 60 else 30 $.bind window, 'storage', (e) -> QR.cooldown() if e.key is "#{NAMESPACE}cooldown/#{g.BOARD}"
$.set g.BOARD+'/cooldown', Date.now() + duration * 1000 attach: ->
cooldown.start() #$('#autopost', QR.qr).checked = true
files = $ '#files', QR.qr
node: (root) -> box = $.el 'span',
quote = $ 'a.quotejs:not(:first-child)', root innerHTML: "<input type=file name=upfile accept='#{QR.accept}'><img alt='click here'><a class=x>X</a>"
$.bind quote, 'click', qr.quote file = $ 'input', box
$.bind file, 'change', QR.change
postInvalid: -> $.bind $('img', box), 'click', -> @previousSibling.click()
content = $('textarea', qr.el).value or $('input[type=file]', qr.el).files.length $.bind $('.x', box), 'click', -> $.rm @parentNode
return 'Error: No text entered.' unless content $.add files, box
file.click()
### captchaNode: (e) ->
captchas expire after 5 hours (emperically verified). cutoff 5 minutes QR.captcha =
before then, b/c posting takes time. challenge: e.target.value
### time: Date.now()
QR.captchaImg()
cutoff = Date.now() - 5*HOUR + 5*MINUTE captchaImg: ->
{qr} = QR
return unless qr
c = QR.captcha.challenge
$('#captcha img', qr).src = "http://www.google.com/recaptcha/api/image?c=#{c}"
captchaPush: (el) ->
{captcha} = QR
captcha.response = el.value
captchas = $.get 'captchas', [] captchas = $.get 'captchas', []
captchas.push captcha
$.set 'captchas', captchas
el.value = ''
QR.captchaReload()
QR.captchaLength captchas
captchaShift: ->
captchas = $.get 'captchas', []
cutoff = Date.now() - 5*HOUR + 5*MINUTE
while captcha = captchas.shift() while captcha = captchas.shift()
if captcha.time > cutoff if captcha.time > cutoff
break break
$.set 'captchas', captchas $.set 'captchas', captchas
QR.captchaLength captchas
$('#captchas', qr.el).textContent = captchas.length + ' captchas' captcha
captchaLength: (captchas) ->
unless captcha captchas or= $.get 'captchas', []
dummy = $ '#dummy', qr.el $('#cl', QR.qr).textContent = captchas.length + ' captchas'
return 'You forgot to type in the verification' unless response = dummy.value captchaReload: ->
captcha = window.location = 'javascript:Recaptcha.reload()'
challenge: qr.challenge change: (e) ->
response: response file = @files[0]
dummy.value = '' if file.size > QR.MAX_FILE_SIZE
Recaptcha.reload() alert 'Error: File too large.'
$.rm @parentNode
$('#recaptcha_challenge_field', qr.el).value = captcha.challenge QR.attach()
$('#recaptcha_response_field', qr.el).value = captcha.response return
{qr} = QR
false fr = new FileReader()
img = @nextSibling
quote: (e) -> fr.onload = (e) ->
e.preventDefault() if e img.src = e.target.result
fr.readAsDataURL file
if qr.el close: ->
$('#autohide', qr.el).checked = false $.rm QR.qr
QR.qr = null
cooldown: ->
return unless g.REPLY and QR.qr
cooldown = $.get "cooldown/#{g.BOARD}", 0
now = Date.now()
n = Math.ceil (cooldown - now) / 1000
b = $ 'form button', QR.qr
if n > 0
$.extend b,
textContent: n
disabled: true
setTimeout QR.cooldown, 1000
else else
qr.dialog @ $.extend b,
textContent: 'Submit'
disabled: false
QR.submit() if $('#autopost', QR.qr).checked
dialog: (text='', tid) ->
tid or= g.THREAD_ID or ''
QR.qr = qr = ui.dialog 'qr', 'top: 0; left: 0;', "
<a class=close>X</a>
<input type=checkbox id=autohide title=autohide>
<div class=move>
<span class=click>
<button>File</button>
<input form=qr_form placeholder=Subject name=sub>
<input form=qr_form placeholder=Name name=name>
<input form=qr_form placeholder=Email name=email>
</span>
</div>
<div class=autohide>
<textarea form=qr_form placeholder=Comment name=com></textarea>
<div id=files></div>
<form enctype=multipart/form-data method=post action=http://sys.4chan.org/#{g.BOARD}/post target=iframe id=qr_form>
<div hidden>
<input name=pwd>
<input name=mode value=regist>
<input name=recaptcha_challenge_field id=challenge>
<input name=recaptcha_response_field id=response>
</div>
<div id=captcha>
<div><img></div>
<span id=cl>120 Captchas</span>
<input id=recaptcha_response_field autocomplete=off>
</div>
<div>
<button>Submit</button>
#{if g.REPLY then "<label>[<input type=checkbox id=autopost title=autopost> Autopost]</label>" else ''}
<input form=qr_form placeholder=Thread name=resto value=#{tid} #{if g.REPLY then 'hidden' else ''}>
#{QR.spoiler}
</div>
</form>
</div>
<a class=error></a>
"
#XXX use dom methods to set values instead of injecting raw user input into your html -_-;
c = d.cookie
$('[name=name]', qr).value = if m = c.match(/4chan_name=([^;]+)/) then decodeURIComponent m[1] else ''
$('[name=email]', qr).value = if m = c.match(/4chan_email=([^;]+)/) then decodeURIComponent m[1] else ''
$('[name=pwd]', qr).value = if m = c.match(/4chan_pass=([^;]+)/) then decodeURIComponent m[1] else $('input[name=pwd]').value
$('textarea', qr).value = text
QR.cooldown() if conf['Cooldown']
$.bind $('button', qr), 'click', QR.attach
$.bind $('.close', qr), 'click', QR.close
$.bind $('.click', qr), 'mousedown', (e) -> e.stopPropagation()
$.bind $('form', qr), 'submit', QR.submit
$.bind $('#recaptcha_response_field', qr), 'keydown', QR.keydown
QR.captchaImg()
QR.captchaLength()
$.add d.body, qr
ta = $ 'textarea', qr
l = text.length
ta.setSelectionRange l, l
ta.focus()
keydown: (e) ->
kc = e.keyCode
v = @value
if kc is 8 and not v #backspace, empty
QR.captchaReload()
return
return unless e.keyCode is 13 and v #enter, not empty
QR.captchaPush @
e.preventDefault()
QR.submit() #derpy, but prevents checking for content twice
quote: (e, blank) ->
e?.preventDefault()
tid = $.x('ancestor::div[@class="thread"]/div', @)?.id
id = @textContent id = @textContent
text = ">>#{id}\n" text = ">>#{id}\n"
sel = getSelection()
selection = window.getSelection() if id == $.x('preceding::input[@type="checkbox"][1]', sel.anchorNode)?.name
if s = selection.toString() if s = sel.toString().replace /\n/g, '\n>'
selectionID = $.x('preceding::input[@type="checkbox"][1]', selection.anchorNode)?.name
if selectionID == id
s = s.replace /\n/g, '\n>'
text += ">#{s}\n" text += ">#{s}\n"
{qr} = QR
ta = $ 'textarea', qr.el if not qr
ta.focus() QR.dialog text, tid
ta.value += text
refresh: ->
$('[name=sub]', qr.el).value = ''
$('[name=email]', qr.el).value = if m = d.cookie.match(/4chan_email=([^;]+)/) then decodeURIComponent m[1] else ''
$('[name=com]', qr.el).value = ''
$('[name=recaptcha_response_field]', qr.el).value = ''
$('[name=spoiler]', qr.el)?.checked = false unless conf['Remember Spoiler']
# XXX opera doesn't allow resetting file inputs w/ file.value = ''
oldFile = $ '[type=file]', qr.el
newFile = $.el 'input', type: 'file', name: 'upfile', accept: qr.acceptFiles
$.replace oldFile, newFile
submit: (e) ->
#XXX `e` won't exist if we're here from `qr.submit.call form`.
if msg = qr.postInvalid()
e.preventDefault?()
alert msg
if msg is 'You forgot to type in the verification.'
$('#dummy', qr.el).focus()
return return
$('#autohide', qr).checked = false
if conf['Auto Watch Reply'] and conf['Thread Watcher'] ta = $ 'textarea', qr
if g.REPLY and $('img.favicon').src is Favicon.empty v = ta.value
watcher.watch null, g.THREAD_ID ss = ta.selectionStart
else ta.value = v[0...ss] + text + v[ss..]
id = $('input[name=resto]', qr.el).value i = ss + text.length
op = $.id id ta.setSelectionRange i, i
if $('img.favicon', op).src is Favicon.empty ta.focus()
watcher.watch op, id $('[name=resto]', qr).value or= tid
receive: (data) ->
if !e then @submit() $('iframe[name=iframe]').src = 'about:blank'
$('#error', qr.el).textContent = '' {qr} = QR
$('#autohide', qr.el).checked = true if conf['Auto Hide QR'] row = $('#files input[form]', qr)?.parentNode
qr.sage = /sage/i.test $('input[name=email]', @).value if data
if QR.op
window.location = data
return
data = JSON.parse data
$.extend $('a.error', qr), data
tc = data.textContent
if tc is 'Error: Duplicate file entry detected.'
$.rm row if row
setTimeout QR.submit, 1000
else if tc is 'You seem to have mistyped the verification.'
setTimeout QR.submit, 1000
return
$.rm row if row
if conf['Persistent QR'] or $('#files input', qr)?.files.length
QR.reset()
else
QR.close()
if conf['Cooldown']
cooldown = Date.now() + (if QR.sage then 60 else 30)*SECOND
$.set "cooldown/#{g.BOARD}", cooldown
QR.cooldown()
reset: ->
$('[name=spoiler]', QR.qr)?.checked = false unless conf['Remember Spoiler']
$('textarea', QR.qr).value = ''
submit: (e) ->
return if $('form button', qr).disabled
#XXX e is undefined if method is called explicitly, eg, from auto posting
unless $('textarea', QR.qr).value or $('[type=file]', QR.qr)?.files.length
if e
alert 'Error: No text entered.'
e.preventDefault()
return
{qr} = QR
$('.error', qr).textContent = ''
if e and (el = $('#recaptcha_response_field', qr)).value
QR.captchaPush el
if not captcha = QR.captchaShift()
alert 'You forgot to type in the verification.'
e?.preventDefault()
return
{challenge, response} = captcha
$('#challenge', qr).value = challenge
$('#response', qr).value = response
$('#autohide', qr).checked = true if conf['Auto Hide QR']
if input = $ '#files input', qr
input.setAttribute 'form', 'qr_form'
$('#qr_form', qr).submit() if not e
QR.sage = /sage/i.test $('[name=email]', qr).value
id = $('input[name=resto]', qr).value
QR.op = not id
$('[name=email]', qr).value = 'noko' if QR.op
if conf['Thread Watcher'] and conf['Auto Watch Reply']
op = $.id id
if $('img.favicon', op).src is Favicon.empty
watcher.watch op, id
sys: -> sys: ->
if recaptcha = $ '#recaptcha_response_field' #post reporting if recaptcha = $ '#recaptcha_response_field' #post reporting
$.bind recaptcha, 'keydown', Recaptcha.listener $.bind recaptcha, 'keydown', QR.keydown
return return
### ###
http://code.google.com/p/chromium/issues/detail?id=20773 http://code.google.com/p/chromium/issues/detail?id=20773
Let content scripts see other frames (instead of them being undefined) Let content scripts see other frames (instead of them being undefined)
To access the parent, we have to break out of the sandbox and evaluate To access the parent, we have to break out of the sandbox and evaluate
in the global context. in the global context.
### ###
$.globalEval -> $.globalEval ->
if node = document.querySelector('table font b')?.firstChild $ = (css) -> document.querySelector css
if node = $('table font b')?.firstChild
{textContent, href} = node {textContent, href} = node
data = JSON.stringify {textContent, href} data = JSON.stringify {textContent, href}
else else if node = $ 'meta'
data = '' data = node.content.match(/url=(.+)/)[1]
if /#/.test data then data = '' #not op
parent.postMessage data, '*' parent.postMessage data, '*'
#if we're an iframe, parent will blank us
c = $('b')?.lastChild
return unless c and c.nodeType is 8 #comment node
[_, thread, id] = c.textContent.match(/thread:(\d+),no:(\d+)/)
{search} = location
cooldown = /cooldown/.test search
noko = /noko/ .test search
sage = /sage/ .test search
watch = /watch/ .test search
url = "http://boards.4chan.org/#{g.BOARD}"
if watch and thread is '0'
url += "/res/#{id}?watch"
else if noko
url += '/res/'
url += if thread is '0' then id else thread
if cooldown
duration = Date.now() + (if sage then 60 else 30) * 1000
url += '?cooldown=' + duration
if noko
url += '#' + id
window.location = url
validateFileSize: (e) ->
return unless @files[0].size > $('input[name=MAX_FILE_SIZE]').value
file = $.el 'input', type: 'file', name: 'upfile', accept: qr.acceptFiles
$.bind file, 'change', qr.validateFileSize
$.replace @, file
$('#error', qr.el).textContent = 'Error: File too large.'
alert 'Error: File too large.'
threading = threading =
init: -> init: ->
@ -1972,18 +1923,6 @@ redirect = ->
url = "http://boards.4chan.org/#{g.BOARD}" url = "http://boards.4chan.org/#{g.BOARD}"
location.href = url location.href = url
Recaptcha =
init: ->
#hack to tab from comment straight to recaptcha
for el in $$ '#recaptcha_table a'
el.tabIndex = 1
$.bind $('#recaptcha_response_field'), 'keydown', Recaptcha.listener
listener: (e) ->
if e.keyCode is 8 and @value is '' # backspace to reload
Recaptcha.reload()
reload: ->
window.location = 'javascript:Recaptcha.reload()'
nodeInserted = (e) -> nodeInserted = (e) ->
{target} = e {target} = e
if target.nodeName is 'TABLE' if target.nodeName is 'TABLE'
@ -2187,7 +2126,7 @@ firstRun =
Main = Main =
init: -> init: ->
if location.hostname is 'sys.4chan.org' if location.hostname is 'sys.4chan.org'
qr.sys() QR.sys()
return return
if conf['404 Redirect'] and d.title is '4chan - 404' and /^\d+$/.test g.THREAD_ID if conf['404 Redirect'] and d.title is '4chan - 404' and /^\d+$/.test g.THREAD_ID
redirect() redirect()
@ -2224,13 +2163,6 @@ Main =
$.addStyle Main.css $.addStyle Main.css
#recaptcha may be blocked, eg by noscript
if (form = $ 'form[name=post]') and (canPost = !!$ '#recaptcha_response_field')
Recaptcha.init()
if g.REPLY and conf['Auto Watch Reply'] and conf['Thread Watcher']
$.bind form, 'submit', -> if $('img.favicon').src is Favicon.empty
watcher.watch null, g.THREAD_ID
#major features #major features
threading.init() threading.init()
@ -2239,12 +2171,6 @@ Main =
if g.REPLY and (id = location.hash[1..]) and /\d/.test(id[0]) and !$.id(id) if g.REPLY and (id = location.hash[1..]) and /\d/.test(id[0]) and !$.id(id)
scrollTo 0, d.body.scrollHeight scrollTo 0, d.body.scrollHeight
if conf['Auto Noko'] and canPost
form.action += '?noko'
if conf['Cooldown'] and canPost
cooldown.init()
if conf['Image Expansion'] if conf['Image Expansion']
imgExpand.init() imgExpand.init()
@ -2269,8 +2195,8 @@ Main =
if conf['Reply Hiding'] if conf['Reply Hiding']
replyHiding.init() replyHiding.init()
if conf['Quick Reply'] and canPost if conf['Quick Reply']
qr.init() QR.init()
if conf['Report Button'] if conf['Report Button']
reportButton.init() reportButton.init()
@ -2300,11 +2226,6 @@ Main =
if conf['Image Preloading'] if conf['Image Preloading']
imgPreloading.init() imgPreloading.init()
if conf['Quick Reply'] and conf['Persistent QR'] and canPost
qr.dialog()
if conf['Auto Hide QR']
$('#autohide', qr.el).checked = true
if conf['Post in Title'] if conf['Post in Title']
titlePost.init() titlePost.init()
@ -2345,7 +2266,7 @@ Main =
message: (e) -> message: (e) ->
{origin, data} = e {origin, data} = e
if origin is 'http://sys.4chan.org' if origin is 'http://sys.4chan.org'
qr.message data QR.receive data
css: ' css: '
/* dialog styling */ /* dialog styling */
@ -2355,7 +2276,7 @@ Main =
div.dialog > div.move { div.dialog > div.move {
cursor: move; cursor: move;
} }
label, a, .favicon, #qr img { label, a, .favicon {
cursor: pointer; cursor: pointer;
} }
@ -2365,12 +2286,6 @@ Main =
.error { .error {
color: red; color: red;
} }
#error {
cursor: default;
}
#error[href] {
cursor: pointer;
}
td.replyhider { td.replyhider {
vertical-align: top; vertical-align: top;
} }
@ -2449,47 +2364,6 @@ Main =
width: 100%; width: 100%;
} }
#qr {
position: fixed;
max-height: 100%;
overflow-x: hidden;
overflow-y: auto;
}
#qr > div.move {
text-align: right;
}
#qr input[name=name] {
float: left;
}
#qr_form {
clear: left;
}
#qr_form, #qr #com_submit, #qr input[name=upfile] {
margin: 0;
}
#qr textarea {
width: 100%;
height: 125px;
}
#qr #close, #qr #autohide {
float: right;
}
#qr:not(:hover) > #autohide:checked ~ .autohide {
height: 0;
overflow: hidden;
}
/* http://stackoverflow.com/questions/2610497/change-an-inputs-html5-placeholder-color-with-css */
#qr input::-webkit-input-placeholder {
color: grey;
}
#qr input:-moz-placeholder {
color: grey;
}
/* qr reCAPTCHA */
#qr img {
border: 1px solid #AAA;
}
#updater { #updater {
position: fixed; position: fixed;
text-align: right; text-align: right;
@ -2554,6 +2428,68 @@ Main =
#files > input { #files > input {
display: block; display: block;
} }
#qr {
max-height: 100%;
overflow-y: auto;
position: fixed;
}
#qr #autohide, #qr .close {
float: right;
}
#qr .click input {
width: 73px;
}
#qr .click * {
float: left;
}
#qr form {
margin: 0;
}
#qr:not(:hover) #autohide:checked ~ .autohide {
height: 0;
overflow: hidden;
}
#qr textarea {
border: 0;
height: 150px;
width: 100%;
}
#qr #captcha {
position: relative;
}
#qr #files {
width: 300px;
white-space: nowrap;
overflow: auto;
}
#qr #files span {
position: relative;
}
#qr #files a {
position: absolute;
left: 0;
font-size: 50px;
color: red;
}
#qr #cl {
right: 0;
padding: 2px;
position: absolute;
}
#qr #recaptcha_response_field {
display: inline;
width: 100%;
}
#qr #files input {
display: none;
}
#qr #files img {
max-height: 100px;
max-width: 100px;
}
#qr input[name=resto] {
width: 80px;
}
' '
if d.body if d.body