From 34a11f2e74fd0b23ffde104f63fb2db287b93458 Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Tue, 12 Feb 2013 01:20:33 +0100 Subject: [PATCH] Add QR hastily. --- 4chan_x.user.js | 917 +++++++++++++++++++++++++++++++++++++++++++- css/style.css | 216 +++++++++++ src/features.coffee | 777 +++++++++++++++++++++++++++++++++++++ src/main.coffee | 1 + 4 files changed, 1909 insertions(+), 2 deletions(-) diff --git a/4chan_x.user.js b/4chan_x.user.js index d48eed6bb..d014af2bf 100644 --- a/4chan_x.user.js +++ b/4chan_x.user.js @@ -43,7 +43,7 @@ */ (function() { - var $, $$, Anonymize, ArchiveLink, AutoGIF, Board, Build, Clone, Conf, Config, DeleteLink, DownloadLink, FileInfo, Filter, Get, Header, ImageHover, Main, Menu, Notification, Post, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, Quotify, Recursive, Redirect, ReplyHiding, ReportLink, RevealSpoilers, Sauce, Settings, Thread, ThreadHiding, ThreadUpdater, Time, UI, d, doc, g, + var $, $$, Anonymize, ArchiveLink, AutoGIF, Board, Build, Clone, Conf, Config, DeleteLink, DownloadLink, FileInfo, Filter, Get, Header, ImageHover, Main, Menu, Notification, Post, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, Quotify, Recursive, Redirect, ReplyHiding, ReportLink, RevealSpoilers, Sauce, Settings, Thread, ThreadHiding, ThreadUpdater, Time, UI, d, doc, g, __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; }; @@ -1816,6 +1816,918 @@ } }; + QR = { + init: function() { + if (!Conf['Quick Reply']) { + return; + } + if (Conf['Hide Original Post Form']) { + Main.css += "#postForm, .postingMode {\n display: none;\n}"; + } + Post.prototype.callbacks.push({ + name: 'Quick Reply', + cb: this.node + }); + return $.ready(this.readyInit); + }, + readyInit: function() { + if (Conf['Persistent QR']) { + QR.dialog(); + if (Conf['Auto Hide QR']) { + QR.hide(); + } + } + $.on(d, 'dragover', QR.dragOver); + $.on(d, 'drop', QR.dropFile); + return $.on(d, 'dragstart dragend', QR.drag); + }, + node: function() { + return $.on($('a[title="Quote this post"]', this.nodes.info), 'click', QR.quote); + }, + open: function() { + if (QR.el) { + QR.el.hidden = false; + return QR.unhide(); + } else { + return QR.dialog(); + } + }, + close: function() { + var i, spoiler, _i, _len, _ref; + QR.el.hidden = true; + QR.abort(); + d.activeElement.blur(); + $.rmClass(QR.el, 'dump'); + _ref = QR.replies; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + i = _ref[_i]; + QR.replies[0].rm(); + } + QR.cooldown.auto = false; + QR.status(); + QR.resetFileInput(); + if (!Conf['Remember Spoiler'] && (spoiler = $.id('spoiler')).checked) { + spoiler.click(); + } + return QR.cleanError(); + }, + hide: function() { + d.activeElement.blur(); + $.addClass(QR.el, 'autohide'); + return $.id('autohide').checked = true; + }, + unhide: function() { + $.rmClass(QR.el, 'autohide'); + return $.id('autohide').checked = false; + }, + toggleHide: function() { + return this.checked && QR.hide() || QR.unhide(); + }, + error: function(err) { + var el; + el = $('.warning', QR.el); + if (typeof err === 'string') { + el.textContent = err; + } else { + el.innerHTML = null; + $.add(el, err); + } + QR.open(); + if (QR.captcha.isEnabled && /captcha|verification/i.test(el.textContent)) { + $('[autocomplete]', QR.el).focus(); + } + if (d.hidden) { + return alert(el.textContent); + } + }, + cleanError: function() { + return $('.warning', QR.el).textContent = null; + }, + status: function(data) { + var disabled, input, value; + if (data == null) { + data = {}; + } + if (!QR.el) { + return; + } + if (g.dead) { + value = 404; + disabled = true; + QR.cooldown.auto = false; + } + value = data.progress || QR.cooldown.seconds || value; + input = QR.status.input; + input.value = QR.cooldown.auto && Conf['Cooldown'] ? value ? "Auto " + value : 'Auto' : value || 'Submit'; + return input.disabled = disabled || false; + }, + cooldown: { + init: function() { + if (!Conf['Cooldown']) { + return; + } + QR.cooldown.types = { + thread: (function() { + switch (g.BOARD) { + case 'q': + return 86400; + case 'b': + case 'soc': + case 'r9k': + return 600; + default: + return 300; + } + })(), + sage: g.BOARD === 'q' ? 600 : 60, + file: g.BOARD === 'q' ? 300 : 30, + post: g.BOARD === 'q' ? 60 : 30 + }; + QR.cooldown.cooldowns = $.get("" + g.BOARD + ".cooldown", {}); + QR.cooldown.start(); + return $.sync("" + g.BOARD + ".cooldown", QR.cooldown.sync); + }, + start: function() { + if (QR.cooldown.isCounting) { + return; + } + QR.cooldown.isCounting = true; + return QR.cooldown.count(); + }, + sync: function(cooldowns) { + var id; + for (id in cooldowns) { + QR.cooldown.cooldowns[id] = cooldowns[id]; + } + return QR.cooldown.start(); + }, + set: function(data) { + var cooldown, hasFile, isReply, isSage, start, type; + if (!Conf['Cooldown']) { + return; + } + start = Date.now(); + if (data.delay) { + cooldown = { + delay: data.delay + }; + } else { + isSage = /sage/i.test(data.post.email); + hasFile = !!data.post.file; + isReply = data.isReply; + type = !isReply ? 'thread' : isSage ? 'sage' : hasFile ? 'file' : 'post'; + cooldown = { + isReply: isReply, + isSage: isSage, + hasFile: hasFile, + timeout: start + QR.cooldown.types[type] * $.SECOND + }; + } + QR.cooldown.cooldowns[start] = cooldown; + $.set("" + g.BOARD + ".cooldown", QR.cooldown.cooldowns); + return QR.cooldown.start(); + }, + unset: function(id) { + delete QR.cooldown.cooldowns[id]; + return $.set("" + g.BOARD + ".cooldown", QR.cooldown.cooldowns); + }, + count: function() { + var cooldown, cooldowns, elapsed, hasFile, isReply, isSage, now, post, seconds, start, type, types, update, _ref; + if (Object.keys(QR.cooldown.cooldowns).length) { + setTimeout(QR.cooldown.count, 1000); + } else { + $["delete"]("" + g.BOARD + ".cooldown"); + delete QR.cooldown.isCounting; + delete QR.cooldown.seconds; + QR.status(); + return; + } + if ((isReply = g.REPLY ? true : QR.threadSelector.value !== 'new')) { + post = QR.replies[0]; + isSage = /sage/i.test(post.email); + hasFile = !!post.file; + } + now = Date.now(); + seconds = null; + _ref = QR.cooldown, types = _ref.types, cooldowns = _ref.cooldowns; + for (start in cooldowns) { + cooldown = cooldowns[start]; + if ('delay' in cooldown) { + if (cooldown.delay) { + seconds = Math.max(seconds, cooldown.delay--); + } else { + seconds = Math.max(seconds, 0); + QR.cooldown.unset(start); + } + continue; + } + if (isReply === cooldown.isReply) { + type = !isReply ? 'thread' : isSage && cooldown.isSage ? 'sage' : hasFile && cooldown.hasFile ? 'file' : 'post'; + elapsed = Math.floor((now - start) / 1000); + if (elapsed >= 0) { + seconds = Math.max(seconds, types[type] - elapsed); + } + } + if (!((start <= now && now <= cooldown.timeout))) { + QR.cooldown.unset(start); + } + } + update = seconds !== null || !!QR.cooldown.seconds; + QR.cooldown.seconds = seconds; + if (update) { + QR.status(); + } + if (seconds === 0 && QR.cooldown.auto) { + return QR.submit(); + } + } + }, + quote: function(e) { + var caretPos, id, range, s, sel, ta, text, _ref; + if (e != null) { + e.preventDefault(); + } + QR.open(); + ta = $('textarea', QR.el); + if (!(g.REPLY || ta.value)) { + QR.threadSelector.value = $.x('ancestor::div[parent::div[@class="board"]]', this).id.slice(1); + } + id = this.previousSibling.hash.slice(2); + text = ">>" + id + "\n"; + sel = d.getSelection(); + if ((s = sel.toString().trim()) && id === ((_ref = $.x('ancestor-or-self::blockquote', sel.anchorNode)) != null ? _ref.id.match(/\d+$/)[0] : void 0)) { + s = s.replace(/\n/g, '\n>'); + text += ">" + s + "\n"; + } + caretPos = ta.selectionStart; + ta.value = ta.value.slice(0, caretPos) + text + ta.value.slice(ta.selectionEnd); + range = caretPos + text.length; + ta.setSelectionRange(range, range); + ta.focus(); + return $.event(ta, new Event('input')); + }, + characterCount: function() { + var count, counter; + counter = QR.charaCounter; + count = this.textLength; + counter.textContent = count; + counter.hidden = count < 1000; + return (count > 1500 ? $.addClass : $.rmClass)(counter, 'warning'); + }, + drag: function(e) { + var toggle; + toggle = e.type === 'dragstart' ? $.off : $.on; + toggle(d, 'dragover', QR.dragOver); + return toggle(d, 'drop', QR.dropFile); + }, + dragOver: function(e) { + e.preventDefault(); + return e.dataTransfer.dropEffect = 'copy'; + }, + dropFile: function(e) { + if (!e.dataTransfer.files.length) { + return; + } + e.preventDefault(); + QR.open(); + QR.fileInput.call(e.dataTransfer); + return $.addClass(QR.el, 'dump'); + }, + fileInput: function() { + var file, _i, _len, _ref; + QR.cleanError(); + if (this.files.length === 1) { + file = this.files[0]; + if (file.size > this.max) { + QR.error('File too large.'); + QR.resetFileInput(); + } else if (-1 === QR.mimeTypes.indexOf(file.type)) { + QR.error('Unsupported file type.'); + QR.resetFileInput(); + } else { + QR.selected.setFile(file); + } + return; + } + _ref = this.files; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + file = _ref[_i]; + if (file.size > this.max) { + QR.error("File " + file.name + " is too large."); + break; + } else if (-1 === QR.mimeTypes.indexOf(file.type)) { + QR.error("" + file.name + ": Unsupported file type."); + break; + } + if (!QR.replies[QR.replies.length - 1].file) { + QR.replies[QR.replies.length - 1].setFile(file); + } else { + new QR.reply().setFile(file); + } + } + $.addClass(QR.el, 'dump'); + return QR.resetFileInput(); + }, + resetFileInput: function() { + return $('[type=file]', QR.el).value = null; + }, + replies: [], + reply: (function() { + + function _Class() { + var persona, prev, + _this = this; + prev = QR.replies[QR.replies.length - 1]; + persona = $.get('QR.persona', {}); + this.name = prev ? prev.name : persona.name || null; + this.email = prev && !/^sage$/.test(prev.email) ? prev.email : persona.email || null; + this.sub = prev && Conf['Remember Subject'] ? prev.sub : Conf['Remember Subject'] ? persona.sub : null; + this.spoiler = prev && Conf['Remember Spoiler'] ? prev.spoiler : false; + this.com = null; + this.el = $.el('a', { + className: 'thumbnail', + draggable: true, + href: 'javascript:;', + innerHTML: '×' + }); + $('input', this.el).checked = this.spoiler; + $.on(this.el, 'click', function() { + return _this.select(); + }); + $.on($('.remove', this.el), 'click', function(e) { + e.stopPropagation(); + return _this.rm(); + }); + $.on($('label', this.el), 'click', function(e) { + return e.stopPropagation(); + }); + $.on($('input', this.el), 'change', function(e) { + _this.spoiler = e.target.checked; + if (_this.el.id === 'selected') { + return $.id('spoiler').checked = _this.spoiler; + } + }); + $.before($('#addReply', QR.el), this.el); + $.on(this.el, 'dragstart', this.dragStart); + $.on(this.el, 'dragenter', this.dragEnter); + $.on(this.el, 'dragleave', this.dragLeave); + $.on(this.el, 'dragover', this.dragOver); + $.on(this.el, 'dragend', this.dragEnd); + $.on(this.el, 'drop', this.drop); + QR.replies.push(this); + } + + _Class.prototype.setFile = function(file) { + var fileUrl, img, url, + _this = this; + this.file = file; + this.el.title = "" + file.name + " (" + ($.bytesToString(file.size)) + ")"; + if (QR.spoiler) { + $('label', this.el).hidden = false; + } + if (!/^image/.test(file.type)) { + this.el.style.backgroundImage = null; + return; + } + if (!(url = window.URL || window.webkitURL)) { + return; + } + url.revokeObjectURL(this.url); + fileUrl = url.createObjectURL(file); + img = $.el('img'); + $.on(img, 'load', function() { + var c, data, i, l, s, ui8a, _i; + s = 90 * 3; + if (img.height < s || img.width < s) { + _this.url = fileUrl; + _this.el.style.backgroundImage = "url(" + _this.url + ")"; + return; + } + if (img.height <= img.width) { + img.width = s / img.height * img.width; + img.height = s; + } else { + img.height = s / img.width * img.height; + img.width = s; + } + c = $.el('canvas'); + c.height = img.height; + c.width = img.width; + c.getContext('2d').drawImage(img, 0, 0, img.width, img.height); + data = atob(c.toDataURL().split(',')[1]); + l = data.length; + ui8a = new Uint8Array(l); + for (i = _i = 0; 0 <= l ? _i < l : _i > l; i = 0 <= l ? ++_i : --_i) { + ui8a[i] = data.charCodeAt(i); + } + _this.url = url.createObjectURL(new Blob([ui8a], { + type: 'image/png' + })); + _this.el.style.backgroundImage = "url(" + _this.url + ")"; + return typeof url.revokeObjectURL === "function" ? url.revokeObjectURL(fileUrl) : void 0; + }); + return img.src = fileUrl; + }; + + _Class.prototype.rmFile = function() { + var _base; + QR.resetFileInput(); + delete this.file; + this.el.title = null; + this.el.style.backgroundImage = null; + if (QR.spoiler) { + $('label', this.el).hidden = true; + } + return typeof (_base = window.URL || window.webkitURL).revokeObjectURL === "function" ? _base.revokeObjectURL(this.url) : void 0; + }; + + _Class.prototype.select = function() { + var data, rectEl, rectList, _i, _len, _ref, _ref1; + if ((_ref = QR.selected) != null) { + _ref.el.id = null; + } + QR.selected = this; + this.el.id = 'selected'; + rectEl = this.el.getBoundingClientRect(); + rectList = this.el.parentNode.getBoundingClientRect(); + this.el.parentNode.scrollLeft += rectEl.left + rectEl.width / 2 - rectList.left - rectList.width / 2; + _ref1 = ['name', 'email', 'sub', 'com']; + for (_i = 0, _len = _ref1.length; _i < _len; _i++) { + data = _ref1[_i]; + $("[name=" + data + "]", QR.el).value = this[data]; + } + QR.characterCount.call($('textarea', QR.el)); + return $('#spoiler', QR.el).checked = this.spoiler; + }; + + _Class.prototype.dragStart = function() { + return $.addClass(this, 'drag'); + }; + + _Class.prototype.dragEnter = function() { + return $.addClass(this, 'over'); + }; + + _Class.prototype.dragLeave = function() { + return $.rmClass(this, 'over'); + }; + + _Class.prototype.dragOver = function(e) { + e.preventDefault(); + return e.dataTransfer.dropEffect = 'move'; + }; + + _Class.prototype.drop = function() { + var el, index, newIndex, oldIndex, reply; + el = $('.drag', this.parentNode); + index = function(el) { + return Array.prototype.slice.call(el.parentNode.children).indexOf(el); + }; + oldIndex = index(el); + newIndex = index(this); + if (oldIndex < newIndex) { + $.after(this, el); + } else { + $.before(this, el); + } + reply = QR.replies.splice(oldIndex, 1)[0]; + return QR.replies.splice(newIndex, 0, reply); + }; + + _Class.prototype.dragEnd = function() { + var el; + $.rmClass(this, 'drag'); + if (el = $('.over', this.parentNode)) { + return $.rmClass(el, 'over'); + } + }; + + _Class.prototype.rm = function() { + var index, _base; + QR.resetFileInput(); + $.rm(this.el); + index = QR.replies.indexOf(this); + if (QR.replies.length === 1) { + new QR.reply().select(); + } else if (this.el.id === 'selected') { + (QR.replies[index - 1] || QR.replies[index + 1]).select(); + } + QR.replies.splice(index, 1); + return typeof (_base = window.URL || window.webkitURL).revokeObjectURL === "function" ? _base.revokeObjectURL(this.url) : void 0; + }; + + return _Class; + + })(), + captcha: { + init: function() { + var _this = this; + if (-1 !== d.cookie.indexOf('pass_enabled=')) { + return; + } + if (!(this.isEnabled = !!$.id('captchaFormPart'))) { + return; + } + if ($.id('recaptcha_challenge_field_holder')) { + return this.ready(); + } else { + this.onready = function() { + return _this.ready(); + }; + return $.on($.id('recaptcha_widget_div'), 'DOMNodeInserted', this.onready); + } + }, + ready: function() { + var _this = this; + if (this.challenge = $.id('recaptcha_challenge_field_holder')) { + $.off($.id('recaptcha_widget_div'), 'DOMNodeInserted', this.onready); + delete this.onready; + } else { + return; + } + $.addClass(QR.el, 'captcha'); + $.after($('.textarea', QR.el), $.el('div', { + className: 'captchaimg', + title: 'Reload', + innerHTML: '' + })); + $.after($('.captchaimg', QR.el), $.el('div', { + className: 'captchainput', + innerHTML: '' + })); + this.img = $('.captchaimg > img', QR.el); + this.input = $('.captchainput > input', QR.el); + $.on(this.img.parentNode, 'click', this.reload); + $.on(this.input, 'keydown', this.keydown); + $.on(this.challenge, 'DOMNodeInserted', function() { + return _this.load(); + }); + $.sync('captchas', function(arr) { + return _this.count(arr.length); + }); + this.count($.get('captchas', []).length); + return this.reload(); + }, + save: function() { + var captcha, captchas, response; + if (!(response = this.input.value)) { + return; + } + captchas = $.get('captchas', []); + while ((captcha = captchas[0]) && captcha.time < Date.now()) { + captchas.shift(); + } + captchas.push({ + challenge: this.challenge.firstChild.value, + response: response, + time: this.timeout + }); + $.set('captchas', captchas); + this.count(captchas.length); + return this.reload(); + }, + load: function() { + var challenge; + this.timeout = Date.now() + 4 * $.MINUTE; + challenge = this.challenge.firstChild.value; + this.img.alt = challenge; + this.img.src = "//www.google.com/recaptcha/api/image?c=" + challenge; + return this.input.value = null; + }, + count: function(count) { + this.input.placeholder = (function() { + switch (count) { + case 0: + return 'Verification (Shift + Enter to cache)'; + case 1: + return 'Verification (1 cached captcha)'; + default: + return "Verification (" + count + " cached captchas)"; + } + })(); + return this.input.alt = count; + }, + reload: function(focus) { + $.globalEval('javascript:Recaptcha.reload("t")'); + if (focus) { + return QR.captcha.input.focus(); + } + }, + keydown: function(e) { + var c; + c = QR.captcha; + if (e.keyCode === 8 && !c.input.value) { + c.reload(); + } else if (e.keyCode === 13 && e.shiftKey) { + c.save(); + } else { + return; + } + return e.preventDefault(); + } + }, + dialog: function() { + var fileInput, id, mimeTypes, name, spoiler, ta, thread, threads, _i, _j, _len, _len1, _ref, _ref1; + QR.el = UI.dialog('qr', 'top:0;right:0;', '\ +
\ + Quick Reply \ + ×\ +
\ +
\ +
\ + \ +
\ +
\ + \ +
\ +
'); + if (Conf['Remember QR size'] && $.engine === 'gecko') { + $.on(ta = $('textarea', QR.el), 'mouseup', function() { + return $.set('QR.size', this.style.cssText); + }); + ta.style.cssText = $.get('QR.size', ''); + } + mimeTypes = $('ul.rules').firstElementChild.textContent.trim().match(/: (.+)/)[1].toLowerCase().replace(/\w+/g, function(type) { + switch (type) { + case 'jpg': + return 'image/jpeg'; + case 'pdf': + return 'application/pdf'; + case 'swf': + return 'application/x-shockwave-flash'; + default: + return "image/" + type; + } + }); + QR.mimeTypes = mimeTypes.split(', '); + QR.mimeTypes.push(''); + fileInput = $('input[type=file]', QR.el); + fileInput.max = $('input[name=MAX_FILE_SIZE]').value; + if ($.engine !== 'presto') { + fileInput.accept = mimeTypes; + } + QR.spoiler = !!$('input[name=spoiler]'); + spoiler = $('#spoilerLabel', QR.el); + spoiler.hidden = !QR.spoiler; + QR.charaCounter = $('#charCount', QR.el); + ta = $('textarea', QR.el); + if (!g.REPLY) { + threads = ''; + _ref = $$('.thread'); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + thread = _ref[_i]; + id = thread.id.slice(1); + threads += ""; + } + QR.threadSelector = g.BOARD === 'f' ? $('select[name=filetag]').cloneNode(true) : $.el('select', { + innerHTML: threads, + title: 'Create a new thread / Reply to a thread' + }); + $.prepend($('.move > span', QR.el), QR.threadSelector); + $.on(QR.threadSelector, 'mousedown', function(e) { + return e.stopPropagation(); + }); + } + $.on($('#autohide', QR.el), 'change', QR.toggleHide); + $.on($('.close', QR.el), 'click', QR.close); + $.on($('#dump', QR.el), 'click', function() { + return QR.el.classList.toggle('dump'); + }); + $.on($('#addReply', QR.el), 'click', function() { + return new QR.reply().select(); + }); + $.on($('form', QR.el), 'submit', QR.submit); + $.on(ta, 'input', function() { + return QR.selected.el.lastChild.textContent = this.value; + }); + $.on(ta, 'input', QR.characterCount); + $.on(fileInput, 'change', QR.fileInput); + $.on(fileInput, 'click', function(e) { + if (e.shiftKey) { + return QR.selected.rmFile() || e.preventDefault(); + } + }); + $.on(spoiler.firstChild, 'change', function() { + return $('input', QR.selected.el).click(); + }); + $.on($('.warning', QR.el), 'click', QR.cleanError); + new QR.reply().select(); + _ref1 = ['name', 'email', 'sub', 'com']; + for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { + name = _ref1[_j]; + $.on($("[name=" + name + "]", QR.el), 'input', function() { + var _ref2; + QR.selected[this.name] = this.value; + if (QR.cooldown.auto && QR.selected === QR.replies[0] && (0 < (_ref2 = QR.cooldown.seconds) && _ref2 <= 5)) { + return QR.cooldown.auto = false; + } + }); + } + QR.status.input = $('input[type=submit]', QR.el); + QR.status(); + QR.cooldown.init(); + QR.captcha.init(); + $.add(d.body, QR.el); + return $.event(QR.el, new CustomEvent('QRDialogCreation', { + bubbles: true + })); + }, + submit: function(e) { + var callbacks, captcha, captchas, challenge, err, filetag, m, opts, post, reply, response, textOnly, threadID, _ref; + if (e != null) { + e.preventDefault(); + } + if (QR.cooldown.seconds) { + QR.cooldown.auto = !QR.cooldown.auto; + QR.status(); + return; + } + QR.abort(); + reply = QR.replies[0]; + if (g.BOARD === 'f' && !g.REPLY) { + filetag = QR.threadSelector.value; + threadID = 'new'; + } else { + threadID = g.THREAD_ID || QR.threadSelector.value; + } + if (threadID === 'new') { + threadID = null; + if (((_ref = g.BOARD) === 'vg' || _ref === 'q') && !reply.sub) { + err = 'New threads require a subject.'; + } else if (!(reply.file || (textOnly = !!$('input[name=textonly]', $.id('postForm'))))) { + err = 'No file selected.'; + } else if (g.BOARD === 'f' && filetag === '9999') { + err = 'Invalid tag specified.'; + } + } else if (!(reply.com || reply.file)) { + err = 'No file selected.'; + } + if (QR.captcha.isEnabled && !err) { + captchas = $.get('captchas', []); + while ((captcha = captchas[0]) && captcha.time < Date.now()) { + captchas.shift(); + } + if (captcha = captchas.shift()) { + challenge = captcha.challenge; + response = captcha.response; + } else { + challenge = QR.captcha.img.alt; + if (response = QR.captcha.input.value) { + QR.captcha.reload(); + } + } + $.set('captchas', captchas); + QR.captcha.count(captchas.length); + if (!response) { + err = 'No valid captcha.'; + } else { + response = response.trim(); + if (!/\s/.test(response)) { + response = "" + response + " " + response; + } + } + } + if (err) { + QR.cooldown.auto = false; + QR.status(); + QR.error(err); + return; + } + QR.cleanError(); + QR.cooldown.auto = QR.replies.length > 1; + if (Conf['Auto Hide QR'] && !QR.cooldown.auto) { + QR.hide(); + } + if (!QR.cooldown.auto && $.x('ancestor::div[@id="qr"]', d.activeElement)) { + d.activeElement.blur(); + } + QR.status({ + progress: '...' + }); + post = { + resto: threadID, + name: reply.name, + email: reply.email, + sub: reply.sub, + com: reply.com, + upfile: reply.file, + filetag: filetag, + spoiler: reply.spoiler, + textonly: textOnly, + mode: 'regist', + pwd: (m = d.cookie.match(/4chan_pass=([^;]+)/)) ? decodeURIComponent(m[1]) : $('input[name=pwd]').value, + recaptcha_challenge_field: challenge, + recaptcha_response_field: response + }; + callbacks = { + onload: function() { + return QR.response(this.response); + }, + onerror: function() { + QR.cooldown.auto = false; + QR.status(); + return QR.error($.el('a', { + href: '//www.4chan.org/banned', + target: '_blank', + textContent: 'Connection error, or you are banned.' + })); + } + }; + opts = { + form: $.formData(post), + upCallbacks: { + onload: function() { + return QR.status({ + progress: '...' + }); + }, + onprogress: function(e) { + return QR.status({ + progress: "" + (Math.round(e.loaded / e.total * 100)) + "%" + }); + } + } + }; + return QR.ajax = $.ajax($.id('postForm').parentNode.action, callbacks, opts); + }, + response: function(html) { + var ban, board, err, persona, postID, reply, threadID, _, _ref, _ref1; + doc = d.implementation.createHTMLDocument(''); + doc.documentElement.innerHTML = html; + if (ban = $('.banType', doc)) { + board = $('.board', doc).innerHTML; + err = $.el('span', { + innerHTML: ban.textContent.toLowerCase() === 'banned' ? ("You are banned on " + board + "! ;_;
") + "Click here to see the reason." : ("You were issued a warning on " + board + " as " + ($('.nameBlock', doc).innerHTML) + ".
") + ("Reason: " + ($('.reason', doc).innerHTML)) + }); + } else if (err = doc.getElementById('errmsg')) { + if ((_ref = $('a', err)) != null) { + _ref.target = '_blank'; + } + } else if (doc.title !== 'Post successful!') { + err = 'Connection error with sys.4chan.org.'; + } + if (err) { + if (/captcha|verification/i.test(err.textContent) || err === 'Connection error with sys.4chan.org.') { + if (/mistyped/i.test(err.textContent)) { + err = 'Error: You seem to have mistyped the CAPTCHA.'; + } + QR.cooldown.auto = QR.captcha.isEnabled ? !!$.get('captchas', []).length : err === 'Connection error with sys.4chan.org.' ? true : false; + QR.cooldown.set({ + delay: 2 + }); + } else { + QR.cooldown.auto = false; + } + QR.status(); + QR.error(err); + return; + } + reply = QR.replies[0]; + persona = $.get('QR.persona', {}); + persona = { + name: reply.name, + email: /^sage$/.test(reply.email) ? persona.email : reply.email, + sub: Conf['Remember Subject'] ? reply.sub : null + }; + $.set('QR.persona', persona); + _ref1 = doc.body.lastChild.textContent.match(/thread:(\d+),no:(\d+)/), _ = _ref1[0], threadID = _ref1[1], postID = _ref1[2]; + $.event(QR.el, new CustomEvent('QRPostSuccessful', { + bubbles: true, + detail: { + threadID: threadID, + postID: postID + } + })); + QR.cooldown.set({ + post: reply, + isReply: threadID !== '0' + }); + if (threadID === '0') { + location.pathname = "/" + g.BOARD + "/res/" + postID; + } else { + QR.cooldown.auto = QR.replies.length > 1; + if (Conf['Open Reply in New Tab'] && !g.REPLY && !QR.cooldown.auto) { + $.open("//boards.4chan.org/" + g.BOARD + "/res/" + threadID + "#p" + postID); + } + } + if (Conf['Persistent QR'] || QR.cooldown.auto) { + reply.rm(); + } else { + QR.close(); + } + QR.status(); + return QR.resetFileInput(); + }, + abort: function() { + var _ref; + if ((_ref = QR.ajax) != null) { + _ref.abort(); + } + delete QR.ajax; + return QR.status(); + } + }; + Menu = { init: function() { if (g.VIEW === 'catalog' || !Conf['Menu']) { @@ -4144,6 +5056,7 @@ initFeature('Thread Hiding', ThreadHiding); initFeature('Reply Hiding', ReplyHiding); initFeature('Recursive', Recursive); + initFeature('Quick Reply', QR); initFeature('Menu', Menu); initFeature('Report Link', ReportLink); initFeature('Thread Hiding (Menu)', ThreadHiding.menu); @@ -4324,7 +5237,7 @@ }); return [message, error]; }, - css: "/* general */\n.dialog {\nbox-shadow: 0 1px 2px rgba(0, 0, 0, .15);\nborder: 1px solid;\ndisplay: block;\npadding: 0;\n}\n.move {\ncursor: move;\n}\nlabel {\ncursor: pointer;\n}\na[href=\"javascript:;\"] {\ntext-decoration: none;\n}\n.warning {\ncolor: red;\n}\n\n/* 4chan style fixes */\n.opContainer, .op {\ndisplay: block !important;\n}\n.post {\noverflow: visible !important;\n}\n[hidden] {\ndisplay: none !important;\n}\n\n/* fixed, z-index */\n#qp, #ihover,\n#updater, #stats,\n#header,\n#qr, #watcher {\nposition: fixed;\n}\n#notifications {\nz-index: 80;\n}\n#qp, #ihover {\nz-index: 70;\n}\n#menu {\nz-index: 60;\n}\n#updater, #stats {\nz-index: 50;\n}\n#header:hover {\nz-index: 40;\n}\n#qr {\nz-index: 30;\n}\n#header {\nz-index: 20;\n}\n#watcher {\nz-index: 10;\n}\n\n/* Header */\n.fourchan-x body {\nmargin-top: 2em;\n}\n.fourchan-x #boardNavDesktop,\n.fourchan-x #navtopright,\n.fourchan-x #boardNavDesktopFoot {\ndisplay: none !important;\n}\n#header {\ntop: 0;\nright: 0;\nleft: 0;\n}\n#header-bar {\nborder-width: 0 0 1px;\npadding: 4px;\nposition: relative;\ntransition: all .1s ease-in-out;\n-o-transition: all .1s ease-in-out;\n-moz-transition: all .1s ease-in-out;\n-webkit-transition: all .1s ease-in-out;\n}\n#header-bar.autohide:not(:hover) {\nmargin-bottom: -1em;\ntransform: translateY(-100%);\n-o-transform: translateY(-100%);\n-moz-transform: translateY(-100%);\n-webkit-transform: translateY(-100%);\ntransition: all .75s .25s ease-in-out;\n-o-transition: all .75s .25s ease-in-out;\n-moz-transition: all .75s .25s ease-in-out;\n-webkit-transition: all .75s .25s ease-in-out;\n}\n#toggle-header-bar {\ncursor: n-resize;\nleft: 0;\nright: 0;\nbottom: -8px;\nheight: 10px;\nposition: absolute;\n}\n#header-bar.autohide #toggle-header-bar {\ncursor: s-resize;\n}\n#header-bar a {\ntext-decoration: none;\npadding: 1px;\n}\n#header-bar > .menu-button {\nfloat: right;\npadding: 0;\n}\n\n/* notifications */\n#notifications {\ntext-align: center;\n}\n.notification {\ncolor: #FFF;\nfont-weight: 700;\ntext-shadow: 0 1px 2px rgba(0, 0, 0, .5);\nbox-shadow: 0 1px 2px rgba(0, 0, 0, .15);\nborder-radius: 2px;\nmargin: 1px auto;\nwidth: 500px;\nmax-width: 100%;\nposition: relative;\ntransition: all .25s ease-in-out;\n-o-transition: all .25s ease-in-out;\n-moz-transition: all .25s ease-in-out;\n-webkit-transition: all .25s ease-in-out;\n}\n.notification.error {\nbackground-color: hsla(0, 100%, 40%, .9);\n}\n.notification.warning {\nbackground-color: hsla(36, 100%, 40%, .9);\n}\n.notification.info {\nbackground-color: hsla(200, 100%, 40%, .9);\n}\n.notification.success {\nbackground-color: hsla(104, 100%, 40%, .9);\n}\n.notification > .close {\ncolor: white;\npadding: 4px 6px;\ntop: 0;\nright: 0;\nposition: absolute;\n}\n.message {\nbox-sizing: border-box;\npadding: 4px 20px;\nmax-height: 200px;\nwidth: 100%;\noverflow: auto;\n}\n\n/* thread updater */\n#updater {\ntext-align: right;\n}\n#updater:not(:hover) {\nbackground: none;\nborder: none;\n}\n#updater input[type=number] {\nwidth: 4em;\n}\n#updater:not(:hover) > div:not(.move) {\ndisplay: none;\n}\n.new {\ncolor: limegreen;\n}\n\n/* quote */\n.quotelink.deadlink {\ntext-decoration: underline !important;\n}\n.deadlink:not(.quotelink) {\ntext-decoration: none !important;\n}\n.inlined {\nopacity: .5;\n}\n#qp input, .forwarded {\ndisplay: none;\n}\n.quotelink.forwardlink,\n.backlink.forwardlink {\ntext-decoration: none;\nborder-bottom: 1px dashed;\n}\n.filtered {\ntext-decoration: underline line-through;\n}\n.inline {\nborder: 1px solid;\ndisplay: table;\nmargin: 2px 0;\n}\n.inline .post {\nborder: 0 !important;\nbackground-color: transparent !important;\ndisplay: table !important;\nmargin: 0 !important;\npadding: 1px 2px !important;\n}\n#qp {\npadding: 2px 2px 5px;\n}\n#qp .post {\nborder: none;\nmargin: 0;\npadding: 0;\n}\n#qp img {\nmax-height: 300px;\nmax-width: 500px;\n}\n.qphl {\nbox-shadow: 0 0 0 2px rgba(216, 94, 49, .7);\n}\n\n/* file */\n.fileText:hover .fntrunc,\n.fileText:not(:hover) .fnfull {\ndisplay: none;\n}\n#ihover {\nbox-sizing: border-box;\n-moz-box-sizing: border-box;\nmax-height: 100%;\nmax-width: 75%;\npadding-bottom: 16px;\n}\n\n/* Filter */\n.opContainer.filter-highlight {\nbox-shadow: inset 5px 0 rgba(255, 0, 0, .5);\n}\n.opContainer.filter-highlight.qphl {\nbox-shadow: inset 5px 0 rgba(255, 0, 0, .5),\n 0 0 0 2px rgba(216, 94, 49, .7);\n}\n.filter-highlight > .reply {\nbox-shadow: -5px 0 rgba(255, 0, 0, .5);\n}\n.filter-highlight > .reply.qphl {\nbox-shadow: -5px 0 rgba(255, 0, 0, .5),\n 0 0 0 2px rgba(216, 94, 49, .7);\n}\n\n/* Thread & Reply Hiding */\n.hide-thread-button,\n.hide-reply-button {\nfloat: left;\nmargin-right: 2px;\n}\n.stub ~ .sideArrows,\n.stub ~ .hide-reply-button,\n.stub ~ .post {\ndisplay: none !important;\n}\n\n/* Menu */\n.menu-button {\ndisplay: inline-block;\n}\n.menu-button > span {\nborder-top: 6px solid;\nborder-right: 4px solid transparent;\nborder-left: 4px solid transparent;\ndisplay: inline-block;\nmargin: 2px;\nvertical-align: middle;\n}\n#menu {\nposition: absolute;\noutline: none;\n}\n.entry {\ncursor: pointer;\ndisplay: block;\noutline: none;\npadding: 3px 7px;\nposition: relative;\ntext-decoration: none;\nwhite-space: nowrap;\n}\n.entry.has-submenu {\npadding-right: 20px;\n}\n.has-submenu::after {\ncontent: \"\";\nborder-left: 6px solid;\nborder-top: 4px solid transparent;\nborder-bottom: 4px solid transparent;\ndisplay: inline-block;\nmargin: 4px;\nposition: absolute;\nright: 3px;\n}\n.has-submenu:not(.focused) > .submenu {\ndisplay: none;\n}\n.submenu {\nposition: absolute;\nmargin: -1px 0;\n}\n.entry input {\nmargin: 0;\n}\n\n/* general */\n:root.yotsuba .dialog {\nbackground-color: #F0E0D6;\nborder-color: #D9BFB7;\n}\n\n/* Header */\n:root.yotsuba #header-bar {\nfont-size: 9pt;\ncolor: #B86;\n}\n:root.yotsuba #header-bar a {\ncolor: #800000;\n}\n\n/* quote */\n:root.yotsuba .inline {\nborder-color: #D9BFB7;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* Menu */\n:root.yotsuba .entry:not(:last-child) {\nborder-bottom: 1px solid #D9BFB7;\n}\n:root.yotsuba .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n\n/* general */\n:root.yotsuba-b .dialog {\nbackground-color: #D6DAF0;\nborder-color: #B7C5D9;\n}\n\n/* Header */\n:root.yotsuba-b #header-bar {\nfont-size: 9pt;\ncolor: #89A;\n}\n:root.yotsuba-b #header-bar a {\ncolor: #34345C;\n}\n\n/* quote */\n:root.yotsuba-b .inline {\nborder-color: #B7C5D9;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* Menu */\n:root.yotsuba-b .entry:not(:last-child) {\nborder-bottom: 1px solid #B7C5D9;\n}\n:root.yotsuba-b .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n\n/* general */\n:root.futaba .dialog {\nbackground-color: #F0E0D6;\nborder-color: #D9BFB7;\n}\n\n/* Header */\n:root.futaba #header-bar {\nfont-size: 11pt;\ncolor: #B86;\n}\n:root.futaba #header-bar a {\ncolor: #800000;\n}\n\n/* quote */\n:root.futaba .inline {\nborder-color: #D9BFB7;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* Menu */\n:root.futaba .entry:not(:last-child) {\nborder-bottom: 1px solid #D9BFB7;\n}\n:root.futaba .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n\n/* general */\n:root.burichan .dialog {\nbackground-color: #D6DAF0;\nborder-color: #B7C5D9;\n}\n\n/* Header */\n:root.burichan #header-bar {\nfont-size: 11pt;\ncolor: #89A;\n}\n:root.burichan #header-bar a {\ncolor: #34345C;\n}\n\n/* quote */\n:root.burichan .inline {\nborder-color: #B7C5D9;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* Menu */\n:root.burichan .entry:not(:last-child) {\nborder-bottom: 1px solid #B7C5D9;\n}\n:root.burichan .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n\n/* general */\n:root.tomorrow .dialog {\nbackground-color: #282A2E;\nborder-color: #111;\n}\n\n/* Header */\n:root.tomorrow #header-bar {\nfont-size: 9pt;\ncolor: #C5C8C6;\n}\n:root.tomorrow #header-bar a {\ncolor: #81A2BE;\n}\n\n/* quote */\n:root.tomorrow .inline {\nborder-color: #111;\nbackground-color: rgba(0, 0, 0, .14);\n}\n\n/* Menu */\n:root.tomorrow .entry:not(:last-child) {\nborder-bottom: 1px solid #111;\n}\n:root.tomorrow .focused.entry {\nbackground: rgba(0, 0, 0, .33);\n}\n\n/* general */\n:root.photon .dialog {\nbackground-color: #DDD;\nborder-color: #CCC;\n}\n\n/* Header */\n:root.photon #header-bar {\nfont-size: 9pt;\ncolor: #333;\n}\n:root.photon #header-bar a {\ncolor: #FF6600;\n}\n\n/* quote */\n:root.photon .inline {\nborder-color: #CCC;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* Menu */\n:root.photon .entry:not(:last-child) {\nborder-bottom: 1px solid #CCC;\n}\n:root.photon .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n" + css: "/* general */\n.dialog {\nbox-shadow: 0 1px 2px rgba(0, 0, 0, .15);\nborder: 1px solid;\ndisplay: block;\npadding: 0;\n}\n.move {\ncursor: move;\n}\nlabel {\ncursor: pointer;\n}\na[href=\"javascript:;\"] {\ntext-decoration: none;\n}\n.warning {\ncolor: red;\n}\n\n/* 4chan style fixes */\n.opContainer, .op {\ndisplay: block !important;\n}\n.post {\noverflow: visible !important;\n}\n[hidden] {\ndisplay: none !important;\n}\n\n/* fixed, z-index */\n#qp, #ihover,\n#updater, #stats,\n#header,\n#qr, #watcher {\nposition: fixed;\n}\n#notifications {\nz-index: 80;\n}\n#qp, #ihover {\nz-index: 70;\n}\n#menu {\nz-index: 60;\n}\n#updater, #stats {\nz-index: 50;\n}\n#header:hover {\nz-index: 40;\n}\n#qr {\nz-index: 30;\n}\n#header {\nz-index: 20;\n}\n#watcher {\nz-index: 10;\n}\n\n/* Header */\n.fourchan-x body {\nmargin-top: 2em;\n}\n.fourchan-x #boardNavDesktop,\n.fourchan-x #navtopright,\n.fourchan-x #boardNavDesktopFoot {\ndisplay: none !important;\n}\n#header {\ntop: 0;\nright: 0;\nleft: 0;\n}\n#header-bar {\nborder-width: 0 0 1px;\npadding: 4px;\nposition: relative;\ntransition: all .1s ease-in-out;\n-o-transition: all .1s ease-in-out;\n-moz-transition: all .1s ease-in-out;\n-webkit-transition: all .1s ease-in-out;\n}\n#header-bar.autohide:not(:hover) {\nmargin-bottom: -1em;\ntransform: translateY(-100%);\n-o-transform: translateY(-100%);\n-moz-transform: translateY(-100%);\n-webkit-transform: translateY(-100%);\ntransition: all .75s .25s ease-in-out;\n-o-transition: all .75s .25s ease-in-out;\n-moz-transition: all .75s .25s ease-in-out;\n-webkit-transition: all .75s .25s ease-in-out;\n}\n#toggle-header-bar {\ncursor: n-resize;\nleft: 0;\nright: 0;\nbottom: -8px;\nheight: 10px;\nposition: absolute;\n}\n#header-bar.autohide #toggle-header-bar {\ncursor: s-resize;\n}\n#header-bar a {\ntext-decoration: none;\npadding: 1px;\n}\n#header-bar > .menu-button {\nfloat: right;\npadding: 0;\n}\n\n/* notifications */\n#notifications {\ntext-align: center;\n}\n.notification {\ncolor: #FFF;\nfont-weight: 700;\ntext-shadow: 0 1px 2px rgba(0, 0, 0, .5);\nbox-shadow: 0 1px 2px rgba(0, 0, 0, .15);\nborder-radius: 2px;\nmargin: 1px auto;\nwidth: 500px;\nmax-width: 100%;\nposition: relative;\ntransition: all .25s ease-in-out;\n-o-transition: all .25s ease-in-out;\n-moz-transition: all .25s ease-in-out;\n-webkit-transition: all .25s ease-in-out;\n}\n.notification.error {\nbackground-color: hsla(0, 100%, 40%, .9);\n}\n.notification.warning {\nbackground-color: hsla(36, 100%, 40%, .9);\n}\n.notification.info {\nbackground-color: hsla(200, 100%, 40%, .9);\n}\n.notification.success {\nbackground-color: hsla(104, 100%, 40%, .9);\n}\n.notification > .close {\ncolor: white;\npadding: 4px 6px;\ntop: 0;\nright: 0;\nposition: absolute;\n}\n.message {\nbox-sizing: border-box;\npadding: 4px 20px;\nmax-height: 200px;\nwidth: 100%;\noverflow: auto;\n}\n\n/* thread updater */\n#updater {\ntext-align: right;\n}\n#updater:not(:hover) {\nbackground: none;\nborder: none;\n}\n#updater input[type=number] {\nwidth: 4em;\n}\n#updater:not(:hover) > div:not(.move) {\ndisplay: none;\n}\n.new {\ncolor: limegreen;\n}\n\n/* quote */\n.quotelink.deadlink {\ntext-decoration: underline !important;\n}\n.deadlink:not(.quotelink) {\ntext-decoration: none !important;\n}\n.inlined {\nopacity: .5;\n}\n#qp input, .forwarded {\ndisplay: none;\n}\n.quotelink.forwardlink,\n.backlink.forwardlink {\ntext-decoration: none;\nborder-bottom: 1px dashed;\n}\n.filtered {\ntext-decoration: underline line-through;\n}\n.inline {\nborder: 1px solid;\ndisplay: table;\nmargin: 2px 0;\n}\n.inline .post {\nborder: 0 !important;\nbackground-color: transparent !important;\ndisplay: table !important;\nmargin: 0 !important;\npadding: 1px 2px !important;\n}\n#qp {\npadding: 2px 2px 5px;\n}\n#qp .post {\nborder: none;\nmargin: 0;\npadding: 0;\n}\n#qp img {\nmax-height: 300px;\nmax-width: 500px;\n}\n.qphl {\nbox-shadow: 0 0 0 2px rgba(216, 94, 49, .7);\n}\n\n/* file */\n.fileText:hover .fntrunc,\n.fileText:not(:hover) .fnfull {\ndisplay: none;\n}\n#ihover {\nbox-sizing: border-box;\n-moz-box-sizing: border-box;\nmax-height: 100%;\nmax-width: 75%;\npadding-bottom: 16px;\n}\n\n/* Filter */\n.opContainer.filter-highlight {\nbox-shadow: inset 5px 0 rgba(255, 0, 0, .5);\n}\n.opContainer.filter-highlight.qphl {\nbox-shadow: inset 5px 0 rgba(255, 0, 0, .5),\n 0 0 0 2px rgba(216, 94, 49, .7);\n}\n.filter-highlight > .reply {\nbox-shadow: -5px 0 rgba(255, 0, 0, .5);\n}\n.filter-highlight > .reply.qphl {\nbox-shadow: -5px 0 rgba(255, 0, 0, .5),\n 0 0 0 2px rgba(216, 94, 49, .7);\n}\n\n/* Thread & Reply Hiding */\n.hide-thread-button,\n.hide-reply-button {\nfloat: left;\nmargin-right: 2px;\n}\n.stub ~ .sideArrows,\n.stub ~ .hide-reply-button,\n.stub ~ .post {\ndisplay: none !important;\n}\n\n/* QR */\n#qr > .move {\nmin-width: 300px;\noverflow: hidden;\nbox-sizing: border-box;\n-moz-box-sizing: border-box;\npadding: 0 2px;\n}\n#qr > .move > span {\nfloat: right;\n}\n#autohide, .close, #qr select, #dump, .remove, .captchaimg, #qr div.warning {\ncursor: pointer;\n}\n#qr select,\n#qr > form {\nmargin: 0;\n}\n#dump {\nbackground: -webkit-linear-gradient(#EEE, #CCC);\nbackground: -moz-linear-gradient(#EEE, #CCC);\nbackground: -o-linear-gradient(#EEE, #CCC);\nbackground: linear-gradient(#EEE, #CCC);\nwidth: 10%;\n}\n.gecko #dump {\npadding: 1px 0 2px;\n}\n#dump:hover, #dump:focus {\nbackground: -webkit-linear-gradient(#FFF, #DDD);\nbackground: -moz-linear-gradient(#FFF, #DDD);\nbackground: -o-linear-gradient(#FFF, #DDD);\nbackground: linear-gradient(#FFF, #DDD);\n}\n#dump:active, .dump #dump:not(:hover):not(:focus) {\nbackground: -webkit-linear-gradient(#CCC, #DDD);\nbackground: -moz-linear-gradient(#CCC, #DDD);\nbackground: -o-linear-gradient(#CCC, #DDD);\nbackground: linear-gradient(#CCC, #DDD);\n}\n#qr:not(.dump) #replies, .dump > form > label {\ndisplay: none;\n}\n#replies {\ndisplay: block;\nheight: 100px;\nposition: relative;\n-webkit-user-select: none;\n-moz-user-select: none;\n-o-user-select: none;\nuser-select: none;\n}\n#replies > div {\ncounter-reset: thumbnails;\ntop: 0; right: 0; bottom: 0; left: 0;\nmargin: 0; padding: 0;\noverflow: hidden;\nposition: absolute;\nwhite-space: pre;\n}\n#replies > div:hover {\nbottom: -10px;\noverflow-x: auto;\nz-index: 1;\n}\n.thumbnail {\nbackground-color: rgba(0,0,0,.2) !important;\nbackground-position: 50% 20% !important;\nbackground-size: cover !important;\nborder: 1px solid #666;\nbox-sizing: border-box;\n-moz-box-sizing: border-box;\ncursor: move;\ndisplay: inline-block;\nheight: 90px; width: 90px;\nmargin: 5px; padding: 2px;\nopacity: .5;\noutline: none;\noverflow: hidden;\nposition: relative;\ntext-shadow: 0 1px 1px #000;\n-webkit-transition: opacity .25s ease-in-out;\n-moz-transition: opacity .25s ease-in-out;\n-o-transition: opacity .25s ease-in-out;\ntransition: opacity .25s ease-in-out;\nvertical-align: top;\n}\n.thumbnail:hover, .thumbnail:focus {\nopacity: .9;\n}\n.thumbnail#selected {\nopacity: 1;\n}\n.thumbnail::before {\ncounter-increment: thumbnails;\ncontent: counter(thumbnails);\ncolor: #FFF;\nfont-weight: 700;\npadding: 3px;\nposition: absolute;\ntop: 0;\nright: 0;\ntext-shadow: 0 0 3px #000, 0 0 8px #000;\n}\n.thumbnail.drag {\nbox-shadow: 0 0 10px rgba(0,0,0,.5);\n}\n.thumbnail.over {\nborder-color: #FFF;\n}\n.thumbnail > span {\ncolor: #FFF;\n}\n.remove {\nbackground: none;\ncolor: #E00;\nfont-weight: 700;\npadding: 3px;\n}\n.remove:hover::after {\ncontent: \" Remove\";\n}\n.thumbnail > label {\nbackground: rgba(0,0,0,.5);\ncolor: #FFF;\nright: 0; bottom: 0; left: 0;\nposition: absolute;\ntext-align: center;\n}\n.thumbnail > label > input {\nmargin: 0;\n}\n#addReply {\ncolor: #333;\nfont-size: 3.5em;\nline-height: 100px;\n}\n#addReply:hover, #addReply:focus {\ncolor: #000;\n}\n.field {\nborder: 1px solid #CCC;\nbox-sizing: border-box;\n-moz-box-sizing: border-box;\ncolor: #333;\nfont: 13px sans-serif;\nmargin: 0;\npadding: 2px 4px 3px;\n-webkit-transition: color .25s, border .25s;\n-moz-transition: color .25s, border .25s;\n-o-transition: color .25s, border .25s;\ntransition: color .25s, border .25s;\n}\n.field:-moz-placeholder,\n.field:hover:-moz-placeholder {\ncolor: #AAA;\n}\n.field:hover, .field:focus {\nborder-color: #999;\ncolor: #000;\noutline: none;\n}\n#qr > form > div:first-child > .field:not(#dump) {\nwidth: 30%;\n}\n#qr textarea.field {\ndisplay: -webkit-box;\nmin-height: 160px;\nmin-width: 100%;\n}\n#qr.captcha textarea.field {\nmin-height: 120px;\n}\n.textarea {\nposition: relative;\n}\n#charCount {\ncolor: #000;\nbackground: hsla(0, 0%, 100%, .5);\nfont-size: 8pt;\nmargin: 1px;\nposition: absolute;\nbottom: 0;\nright: 0;\npointer-events: none;\n}\n#charCount.warning {\ncolor: red;\n}\n.captchainput > .field {\nmin-width: 100%;\n}\n.captchaimg {\nbackground: #FFF;\noutline: 1px solid #CCC;\noutline-offset: -1px;\ntext-align: center;\n}\n.captchaimg > img {\ndisplay: block;\nheight: 57px;\nwidth: 300px;\n}\n#qr [type=file] {\nmargin: 1px 0;\nwidth: 70%;\n}\n#qr [type=submit] {\nmargin: 1px 0;\npadding: 1px; /* not Gecko */\nwidth: 30%;\n}\n.gecko #qr [type=submit] {\npadding: 0 1px; /* Gecko does not respect box-sizing: border-box */\n}\n\n/* Menu */\n.menu-button {\ndisplay: inline-block;\n}\n.menu-button > span {\nborder-top: 6px solid;\nborder-right: 4px solid transparent;\nborder-left: 4px solid transparent;\ndisplay: inline-block;\nmargin: 2px;\nvertical-align: middle;\n}\n#menu {\nposition: absolute;\noutline: none;\n}\n.entry {\ncursor: pointer;\ndisplay: block;\noutline: none;\npadding: 3px 7px;\nposition: relative;\ntext-decoration: none;\nwhite-space: nowrap;\n}\n.entry.has-submenu {\npadding-right: 20px;\n}\n.has-submenu::after {\ncontent: \"\";\nborder-left: 6px solid;\nborder-top: 4px solid transparent;\nborder-bottom: 4px solid transparent;\ndisplay: inline-block;\nmargin: 4px;\nposition: absolute;\nright: 3px;\n}\n.has-submenu:not(.focused) > .submenu {\ndisplay: none;\n}\n.submenu {\nposition: absolute;\nmargin: -1px 0;\n}\n.entry input {\nmargin: 0;\n}\n\n/* general */\n:root.yotsuba .dialog {\nbackground-color: #F0E0D6;\nborder-color: #D9BFB7;\n}\n\n/* Header */\n:root.yotsuba #header-bar {\nfont-size: 9pt;\ncolor: #B86;\n}\n:root.yotsuba #header-bar a {\ncolor: #800000;\n}\n\n/* quote */\n:root.yotsuba .inline {\nborder-color: #D9BFB7;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* Menu */\n:root.yotsuba .entry:not(:last-child) {\nborder-bottom: 1px solid #D9BFB7;\n}\n:root.yotsuba .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n\n/* general */\n:root.yotsuba-b .dialog {\nbackground-color: #D6DAF0;\nborder-color: #B7C5D9;\n}\n\n/* Header */\n:root.yotsuba-b #header-bar {\nfont-size: 9pt;\ncolor: #89A;\n}\n:root.yotsuba-b #header-bar a {\ncolor: #34345C;\n}\n\n/* quote */\n:root.yotsuba-b .inline {\nborder-color: #B7C5D9;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* Menu */\n:root.yotsuba-b .entry:not(:last-child) {\nborder-bottom: 1px solid #B7C5D9;\n}\n:root.yotsuba-b .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n\n/* general */\n:root.futaba .dialog {\nbackground-color: #F0E0D6;\nborder-color: #D9BFB7;\n}\n\n/* Header */\n:root.futaba #header-bar {\nfont-size: 11pt;\ncolor: #B86;\n}\n:root.futaba #header-bar a {\ncolor: #800000;\n}\n\n/* quote */\n:root.futaba .inline {\nborder-color: #D9BFB7;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* Menu */\n:root.futaba .entry:not(:last-child) {\nborder-bottom: 1px solid #D9BFB7;\n}\n:root.futaba .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n\n/* general */\n:root.burichan .dialog {\nbackground-color: #D6DAF0;\nborder-color: #B7C5D9;\n}\n\n/* Header */\n:root.burichan #header-bar {\nfont-size: 11pt;\ncolor: #89A;\n}\n:root.burichan #header-bar a {\ncolor: #34345C;\n}\n\n/* quote */\n:root.burichan .inline {\nborder-color: #B7C5D9;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* Menu */\n:root.burichan .entry:not(:last-child) {\nborder-bottom: 1px solid #B7C5D9;\n}\n:root.burichan .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n\n/* general */\n:root.tomorrow .dialog {\nbackground-color: #282A2E;\nborder-color: #111;\n}\n\n/* Header */\n:root.tomorrow #header-bar {\nfont-size: 9pt;\ncolor: #C5C8C6;\n}\n:root.tomorrow #header-bar a {\ncolor: #81A2BE;\n}\n\n/* quote */\n:root.tomorrow .inline {\nborder-color: #111;\nbackground-color: rgba(0, 0, 0, .14);\n}\n\n/* Menu */\n:root.tomorrow .entry:not(:last-child) {\nborder-bottom: 1px solid #111;\n}\n:root.tomorrow .focused.entry {\nbackground: rgba(0, 0, 0, .33);\n}\n\n/* general */\n:root.photon .dialog {\nbackground-color: #DDD;\nborder-color: #CCC;\n}\n\n/* Header */\n:root.photon #header-bar {\nfont-size: 9pt;\ncolor: #333;\n}\n:root.photon #header-bar a {\ncolor: #FF6600;\n}\n\n/* quote */\n:root.photon .inline {\nborder-color: #CCC;\nbackground-color: rgba(255, 255, 255, .14);\n}\n\n/* Menu */\n:root.photon .entry:not(:last-child) {\nborder-bottom: 1px solid #CCC;\n}\n:root.photon .focused.entry {\nbackground: rgba(255, 255, 255, .33);\n}\n" }; Main.init(); diff --git a/css/style.css b/css/style.css index a5827de84..cc3c571a9 100644 --- a/css/style.css +++ b/css/style.css @@ -269,6 +269,222 @@ a[href="javascript:;"] { display: none !important; } +/* QR */ +#qr > .move { + min-width: 300px; + overflow: hidden; + box-sizing: border-box; + -moz-box-sizing: border-box; + padding: 0 2px; +} +#qr > .move > span { + float: right; +} +#autohide, .close, #qr select, #dump, .remove, .captchaimg, #qr div.warning { + cursor: pointer; +} +#qr select, +#qr > form { + margin: 0; +} +#dump { + background: -webkit-linear-gradient(#EEE, #CCC); + background: -moz-linear-gradient(#EEE, #CCC); + background: -o-linear-gradient(#EEE, #CCC); + background: linear-gradient(#EEE, #CCC); + width: 10%; +} +.gecko #dump { + padding: 1px 0 2px; +} +#dump:hover, #dump:focus { + background: -webkit-linear-gradient(#FFF, #DDD); + background: -moz-linear-gradient(#FFF, #DDD); + background: -o-linear-gradient(#FFF, #DDD); + background: linear-gradient(#FFF, #DDD); +} +#dump:active, .dump #dump:not(:hover):not(:focus) { + background: -webkit-linear-gradient(#CCC, #DDD); + background: -moz-linear-gradient(#CCC, #DDD); + background: -o-linear-gradient(#CCC, #DDD); + background: linear-gradient(#CCC, #DDD); +} +#qr:not(.dump) #replies, .dump > form > label { + display: none; +} +#replies { + display: block; + height: 100px; + position: relative; + -webkit-user-select: none; + -moz-user-select: none; + -o-user-select: none; + user-select: none; +} +#replies > div { + counter-reset: thumbnails; + top: 0; right: 0; bottom: 0; left: 0; + margin: 0; padding: 0; + overflow: hidden; + position: absolute; + white-space: pre; +} +#replies > div:hover { + bottom: -10px; + overflow-x: auto; + z-index: 1; +} +.thumbnail { + background-color: rgba(0,0,0,.2) !important; + background-position: 50% 20% !important; + background-size: cover !important; + border: 1px solid #666; + box-sizing: border-box; + -moz-box-sizing: border-box; + cursor: move; + display: inline-block; + height: 90px; width: 90px; + margin: 5px; padding: 2px; + opacity: .5; + outline: none; + overflow: hidden; + position: relative; + text-shadow: 0 1px 1px #000; + -webkit-transition: opacity .25s ease-in-out; + -moz-transition: opacity .25s ease-in-out; + -o-transition: opacity .25s ease-in-out; + transition: opacity .25s ease-in-out; + vertical-align: top; +} +.thumbnail:hover, .thumbnail:focus { + opacity: .9; +} +.thumbnail#selected { + opacity: 1; +} +.thumbnail::before { + counter-increment: thumbnails; + content: counter(thumbnails); + color: #FFF; + font-weight: 700; + padding: 3px; + position: absolute; + top: 0; + right: 0; + text-shadow: 0 0 3px #000, 0 0 8px #000; +} +.thumbnail.drag { + box-shadow: 0 0 10px rgba(0,0,0,.5); +} +.thumbnail.over { + border-color: #FFF; +} +.thumbnail > span { + color: #FFF; +} +.remove { + background: none; + color: #E00; + font-weight: 700; + padding: 3px; +} +.remove:hover::after { + content: " Remove"; +} +.thumbnail > label { + background: rgba(0,0,0,.5); + color: #FFF; + right: 0; bottom: 0; left: 0; + position: absolute; + text-align: center; +} +.thumbnail > label > input { + margin: 0; +} +#addReply { + color: #333; + font-size: 3.5em; + line-height: 100px; +} +#addReply:hover, #addReply:focus { + color: #000; +} +.field { + border: 1px solid #CCC; + box-sizing: border-box; + -moz-box-sizing: border-box; + color: #333; + font: 13px sans-serif; + margin: 0; + padding: 2px 4px 3px; + -webkit-transition: color .25s, border .25s; + -moz-transition: color .25s, border .25s; + -o-transition: color .25s, border .25s; + transition: color .25s, border .25s; +} +.field:-moz-placeholder, +.field:hover:-moz-placeholder { + color: #AAA; +} +.field:hover, .field:focus { + border-color: #999; + color: #000; + outline: none; +} +#qr > form > div:first-child > .field:not(#dump) { + width: 30%; +} +#qr textarea.field { + display: -webkit-box; + min-height: 160px; + min-width: 100%; +} +#qr.captcha textarea.field { + min-height: 120px; +} +.textarea { + position: relative; +} +#charCount { + color: #000; + background: hsla(0, 0%, 100%, .5); + font-size: 8pt; + margin: 1px; + position: absolute; + bottom: 0; + right: 0; + pointer-events: none; +} +#charCount.warning { + color: red; +} +.captchainput > .field { + min-width: 100%; +} +.captchaimg { + background: #FFF; + outline: 1px solid #CCC; + outline-offset: -1px; + text-align: center; +} +.captchaimg > img { + display: block; + height: 57px; + width: 300px; +} +#qr [type=file] { + margin: 1px 0; + width: 70%; +} +#qr [type=submit] { + margin: 1px 0; + padding: 1px; /* not Gecko */ + width: 30%; +} +.gecko #qr [type=submit] { + padding: 0 1px; /* Gecko does not respect box-sizing: border-box */ +} + /* Menu */ .menu-button { display: inline-block; diff --git a/src/features.coffee b/src/features.coffee index 911dc8be5..b66c2431e 100644 --- a/src/features.coffee +++ b/src/features.coffee @@ -760,6 +760,783 @@ Recursive = break return +QR = + init: -> + return unless Conf['Quick Reply'] + + if Conf['Hide Original Post Form'] + Main.css += """ + #postForm, .postingMode { + display: none; + } + """ + + Post::callbacks.push + name: 'Quick Reply' + cb: @node + + $.ready @readyInit + + readyInit: -> + if Conf['Persistent QR'] + QR.dialog() + QR.hide() if Conf['Auto Hide QR'] + $.on d, 'dragover', QR.dragOver + $.on d, 'drop', QR.dropFile + $.on d, 'dragstart dragend', QR.drag + + node: -> + $.on $('a[title="Quote this post"]', @nodes.info), 'click', QR.quote + + open: -> + if QR.el + QR.el.hidden = false + QR.unhide() + else + QR.dialog() + close: -> + QR.el.hidden = true + QR.abort() + d.activeElement.blur() + $.rmClass QR.el, 'dump' + for i in QR.replies + QR.replies[0].rm() + QR.cooldown.auto = false + QR.status() + QR.resetFileInput() + if not Conf['Remember Spoiler'] and (spoiler = $.id 'spoiler').checked + spoiler.click() + QR.cleanError() + hide: -> + d.activeElement.blur() + $.addClass QR.el, 'autohide' + $.id('autohide').checked = true + unhide: -> + $.rmClass QR.el, 'autohide' + $.id('autohide').checked = false + toggleHide: -> + @checked and QR.hide() or QR.unhide() + + error: (err) -> + el = $ '.warning', QR.el + if typeof err is 'string' + el.textContent = err + else + el.innerHTML = null + $.add el, err + QR.open() + if QR.captcha.isEnabled and /captcha|verification/i.test el.textContent + # Focus the captcha input on captcha error. + $('[autocomplete]', QR.el).focus() + alert el.textContent if d.hidden + cleanError: -> + $('.warning', QR.el).textContent = null + + status: (data={}) -> + return unless QR.el + if g.dead + value = 404 + disabled = true + QR.cooldown.auto = false + value = data.progress or QR.cooldown.seconds or value + {input} = QR.status + input.value = + if QR.cooldown.auto and Conf['Cooldown'] + if value then "Auto #{value}" else 'Auto' + else + value or 'Submit' + input.disabled = disabled or false + + cooldown: + init: -> + return unless Conf['Cooldown'] + QR.cooldown.types = + thread: switch g.BOARD + when 'q' then 86400 + when 'b', 'soc', 'r9k' then 600 + else 300 + sage: if g.BOARD is 'q' then 600 else 60 + file: if g.BOARD is 'q' then 300 else 30 + post: if g.BOARD is 'q' then 60 else 30 + QR.cooldown.cooldowns = $.get "#{g.BOARD}.cooldown", {} + QR.cooldown.start() + $.sync "#{g.BOARD}.cooldown", QR.cooldown.sync + start: -> + return if QR.cooldown.isCounting + QR.cooldown.isCounting = true + QR.cooldown.count() + sync: (cooldowns) -> + # Add each cooldowns, don't overwrite everything in case we + # still need to purge one in the current tab to auto-post. + for id of cooldowns + QR.cooldown.cooldowns[id] = cooldowns[id] + QR.cooldown.start() + set: (data) -> + return unless Conf['Cooldown'] + start = Date.now() + if data.delay + cooldown = delay: data.delay + else + isSage = /sage/i.test data.post.email + hasFile = !!data.post.file + isReply = data.isReply + type = + unless isReply + 'thread' + else if isSage + 'sage' + else if hasFile + 'file' + else + 'post' + cooldown = + isReply: isReply + isSage: isSage + hasFile: hasFile + timeout: start + QR.cooldown.types[type] * $.SECOND + QR.cooldown.cooldowns[start] = cooldown + $.set "#{g.BOARD}.cooldown", QR.cooldown.cooldowns + QR.cooldown.start() + unset: (id) -> + delete QR.cooldown.cooldowns[id] + $.set "#{g.BOARD}.cooldown", QR.cooldown.cooldowns + count: -> + if Object.keys(QR.cooldown.cooldowns).length + setTimeout QR.cooldown.count, 1000 + else + $.delete "#{g.BOARD}.cooldown" + delete QR.cooldown.isCounting + delete QR.cooldown.seconds + QR.status() + return + + if (isReply = if g.REPLY then true else QR.threadSelector.value isnt 'new') + post = QR.replies[0] + isSage = /sage/i.test post.email + hasFile = !!post.file + now = Date.now() + seconds = null + {types, cooldowns} = QR.cooldown + + for start, cooldown of cooldowns + if 'delay' of cooldown + if cooldown.delay + seconds = Math.max seconds, cooldown.delay-- + else + seconds = Math.max seconds, 0 + QR.cooldown.unset start + continue + + if isReply is cooldown.isReply + # Only cooldowns relevant to this post can set the seconds value. + # Unset outdated cooldowns that can no longer impact us. + type = + unless isReply + 'thread' + else if isSage and cooldown.isSage + 'sage' + else if hasFile and cooldown.hasFile + 'file' + else + 'post' + elapsed = Math.floor (now - start) / 1000 + if elapsed >= 0 # clock changed since then? + seconds = Math.max seconds, types[type] - elapsed + unless start <= now <= cooldown.timeout + QR.cooldown.unset start + + # Update the status when we change posting type. + # Don't get stuck at some random number. + # Don't interfere with progress status updates. + update = seconds isnt null or !!QR.cooldown.seconds + QR.cooldown.seconds = seconds + QR.status() if update + QR.submit() if seconds is 0 and QR.cooldown.auto + + quote: (e) -> + e?.preventDefault() + QR.open() + ta = $ 'textarea', QR.el + unless g.REPLY or ta.value + QR.threadSelector.value = $.x('ancestor::div[parent::div[@class="board"]]', @).id[1..] + # Make sure we get the correct number, even with XXX censors + id = @previousSibling.hash[2..] + text = ">>#{id}\n" + + sel = d.getSelection() + if (s = sel.toString().trim()) and id is $.x('ancestor-or-self::blockquote', sel.anchorNode)?.id.match(/\d+$/)[0] + # XXX Opera doesn't retain `\n`s? + s = s.replace /\n/g, '\n>' + text += ">#{s}\n" + + caretPos = ta.selectionStart + # Replace selection for text. + ta.value = ta.value[...caretPos] + text + ta.value[ta.selectionEnd..] + # Move the caret to the end of the new quote. + range = caretPos + text.length + ta.setSelectionRange range, range + ta.focus() + + # Fire the 'input' event + $.event ta, new Event 'input' + + characterCount: -> + counter = QR.charaCounter + count = @textLength + counter.textContent = count + counter.hidden = count < 1000 + (if count > 1500 then $.addClass else $.rmClass) counter, 'warning' + + drag: (e) -> + # Let it drag anything from the page. + toggle = if e.type is 'dragstart' then $.off else $.on + toggle d, 'dragover', QR.dragOver + toggle d, 'drop', QR.dropFile + dragOver: (e) -> + e.preventDefault() + e.dataTransfer.dropEffect = 'copy' # cursor feedback + dropFile: (e) -> + # Let it only handle files from the desktop. + return unless e.dataTransfer.files.length + e.preventDefault() + QR.open() + QR.fileInput.call e.dataTransfer + $.addClass QR.el, 'dump' + fileInput: -> + QR.cleanError() + # Set or change current reply's file. + if @files.length is 1 + file = @files[0] + if file.size > @max + QR.error 'File too large.' + QR.resetFileInput() + else if -1 is QR.mimeTypes.indexOf file.type + QR.error 'Unsupported file type.' + QR.resetFileInput() + else + QR.selected.setFile file + return + # Create new replies with these files. + for file in @files + if file.size > @max + QR.error "File #{file.name} is too large." + break + else if -1 is QR.mimeTypes.indexOf file.type + QR.error "#{file.name}: Unsupported file type." + break + unless QR.replies[QR.replies.length - 1].file + # set last reply's file + QR.replies[QR.replies.length - 1].setFile file + else + new QR.reply().setFile file + $.addClass QR.el, 'dump' + QR.resetFileInput() # reset input + resetFileInput: -> + $('[type=file]', QR.el).value = null + + replies: [] + reply: class + constructor: -> + # set values, or null, to avoid 'undefined' values in inputs + prev = QR.replies[QR.replies.length-1] + persona = $.get 'QR.persona', {} + @name = if prev then prev.name else persona.name or null + @email = if prev and !/^sage$/.test prev.email then prev.email else persona.email or null + @sub = if prev and Conf['Remember Subject'] then prev.sub else if Conf['Remember Subject'] then persona.sub else null + @spoiler = if prev and Conf['Remember Spoiler'] then prev.spoiler else false + @com = null + + @el = $.el 'a', + className: 'thumbnail' + draggable: true + href: 'javascript:;' + innerHTML: '×' + $('input', @el).checked = @spoiler + $.on @el, 'click', => @select() + $.on $('.remove', @el), 'click', (e) => + e.stopPropagation() + @rm() + $.on $('label', @el), 'click', (e) => e.stopPropagation() + $.on $('input', @el), 'change', (e) => + @spoiler = e.target.checked + $.id('spoiler').checked = @spoiler if @el.id is 'selected' + $.before $('#addReply', QR.el), @el + + $.on @el, 'dragstart', @dragStart + $.on @el, 'dragenter', @dragEnter + $.on @el, 'dragleave', @dragLeave + $.on @el, 'dragover', @dragOver + $.on @el, 'dragend', @dragEnd + $.on @el, 'drop', @drop + + QR.replies.push @ + setFile: (@file) -> + @el.title = "#{file.name} (#{$.bytesToString file.size})" + $('label', @el).hidden = false if QR.spoiler + unless /^image/.test file.type + @el.style.backgroundImage = null + return + # XXX Opera does not support window.URL + return unless url = window.URL or window.webkitURL + url.revokeObjectURL @url + + # Create a redimensioned thumbnail. + fileUrl = url.createObjectURL file + img = $.el 'img' + + $.on img, 'load', => + # Generate thumbnails only if they're really big. + # Resized pictures through canvases look like ass, + # so we generate thumbnails `s` times bigger then expected + # to avoid crappy resized quality. + s = 90*3 + if img.height < s or img.width < s + @url = fileUrl + @el.style.backgroundImage = "url(#{@url})" + return + if img.height <= img.width + img.width = s / img.height * img.width + img.height = s + else + img.height = s / img.width * img.height + img.width = s + c = $.el 'canvas' + c.height = img.height + c.width = img.width + c.getContext('2d').drawImage img, 0, 0, img.width, img.height + # Support for toBlob fucking when? + data = atob c.toDataURL().split(',')[1] + + # DataUrl to Binary code from Aeosynth's 4chan X repo + l = data.length + ui8a = new Uint8Array l + for i in [0...l] + ui8a[i] = data.charCodeAt i + + @url = url.createObjectURL new Blob [ui8a], type: 'image/png' + @el.style.backgroundImage = "url(#{@url})" + url.revokeObjectURL? fileUrl + + img.src = fileUrl + rmFile: -> + QR.resetFileInput() + delete @file + @el.title = null + @el.style.backgroundImage = null + $('label', @el).hidden = true if QR.spoiler + (window.URL or window.webkitURL).revokeObjectURL? @url + select: -> + QR.selected?.el.id = null + QR.selected = @ + @el.id = 'selected' + # Scroll the list to center the focused reply. + rectEl = @el.getBoundingClientRect() + rectList = @el.parentNode.getBoundingClientRect() + @el.parentNode.scrollLeft += rectEl.left + rectEl.width/2 - rectList.left - rectList.width/2 + # Load this reply's values. + for data in ['name', 'email', 'sub', 'com'] + $("[name=#{data}]", QR.el).value = @[data] + QR.characterCount.call $ 'textarea', QR.el + $('#spoiler', QR.el).checked = @spoiler + dragStart: -> + $.addClass @, 'drag' + dragEnter: -> + $.addClass @, 'over' + dragLeave: -> + $.rmClass @, 'over' + dragOver: (e) -> + e.preventDefault() + e.dataTransfer.dropEffect = 'move' + drop: -> + el = $ '.drag', @parentNode + index = (el) -> Array::slice.call(el.parentNode.children).indexOf el + oldIndex = index el + newIndex = index @ + if oldIndex < newIndex + $.after @, el + else + $.before @, el + reply = QR.replies.splice(oldIndex, 1)[0] + QR.replies.splice newIndex, 0, reply + dragEnd: -> + $.rmClass @, 'drag' + if el = $ '.over', @parentNode + $.rmClass el, 'over' + rm: -> + QR.resetFileInput() + $.rm @el + index = QR.replies.indexOf @ + if QR.replies.length is 1 + new QR.reply().select() + else if @el.id is 'selected' + (QR.replies[index-1] or QR.replies[index+1]).select() + QR.replies.splice index, 1 + (window.URL or window.webkitURL).revokeObjectURL? @url + + captcha: + init: -> + return if -1 isnt d.cookie.indexOf 'pass_enabled=' + return unless @isEnabled = !!$.id 'captchaFormPart' + if $.id 'recaptcha_challenge_field_holder' + @ready() + else + @onready = => @ready() + $.on $.id('recaptcha_widget_div'), 'DOMNodeInserted', @onready + ready: -> + if @challenge = $.id 'recaptcha_challenge_field_holder' + $.off $.id('recaptcha_widget_div'), 'DOMNodeInserted', @onready + delete @onready + else + return + $.addClass QR.el, 'captcha' + $.after $('.textarea', QR.el), $.el 'div', + className: 'captchaimg' + title: 'Reload' + innerHTML: '' + $.after $('.captchaimg', QR.el), $.el 'div', + className: 'captchainput' + innerHTML: '' + @img = $ '.captchaimg > img', QR.el + @input = $ '.captchainput > input', QR.el + $.on @img.parentNode, 'click', @reload + $.on @input, 'keydown', @keydown + $.on @challenge, 'DOMNodeInserted', => @load() + $.sync 'captchas', (arr) => @count arr.length + @count $.get('captchas', []).length + # start with an uncached captcha + @reload() + save: -> + return unless response = @input.value + captchas = $.get 'captchas', [] + # Remove old captchas. + while (captcha = captchas[0]) and captcha.time < Date.now() + captchas.shift() + captchas.push + challenge: @challenge.firstChild.value + response: response + time: @timeout + $.set 'captchas', captchas + @count captchas.length + @reload() + load: -> + # Timeout is available at RecaptchaState.timeout in seconds. + # We use 5-1 minutes to give upload some time. + @timeout = Date.now() + 4*$.MINUTE + challenge = @challenge.firstChild.value + @img.alt = challenge + @img.src = "//www.google.com/recaptcha/api/image?c=#{challenge}" + @input.value = null + count: (count) -> + @input.placeholder = switch count + when 0 + 'Verification (Shift + Enter to cache)' + when 1 + 'Verification (1 cached captcha)' + else + "Verification (#{count} cached captchas)" + @input.alt = count # For XTRM RICE. + reload: (focus) -> + # the "t" argument prevents the input from being focused + $.globalEval 'javascript:Recaptcha.reload("t")' + # Focus if we meant to. + QR.captcha.input.focus() if focus + keydown: (e) -> + c = QR.captcha + if e.keyCode is 8 and not c.input.value + c.reload() + else if e.keyCode is 13 and e.shiftKey + c.save() + else + return + e.preventDefault() + + dialog: -> + QR.el = UI.dialog 'qr', 'top:0;right:0;', ' +
+ Quick Reply + × +
+
+
+ +
+
+ +
+
' + + if Conf['Remember QR size'] and $.engine is 'gecko' + $.on ta = $('textarea', QR.el), 'mouseup', -> + $.set 'QR.size', @style.cssText + ta.style.cssText = $.get 'QR.size', '' + + # Allow only this board's supported files. + mimeTypes = $('ul.rules').firstElementChild.textContent.trim().match(/: (.+)/)[1].toLowerCase().replace /\w+/g, (type) -> + switch type + when 'jpg' + 'image/jpeg' + when 'pdf' + 'application/pdf' + when 'swf' + 'application/x-shockwave-flash' + else + "image/#{type}" + QR.mimeTypes = mimeTypes.split ', ' + # Add empty mimeType to avoid errors with URLs selected in Window's file dialog. + QR.mimeTypes.push '' + fileInput = $ 'input[type=file]', QR.el + fileInput.max = $('input[name=MAX_FILE_SIZE]').value + fileInput.accept = mimeTypes if $.engine isnt 'presto' # Opera's accept attribute is fucked up + + QR.spoiler = !!$ 'input[name=spoiler]' + spoiler = $ '#spoilerLabel', QR.el + spoiler.hidden = !QR.spoiler + + QR.charaCounter = $ '#charCount', QR.el + ta = $ 'textarea', QR.el + + unless g.REPLY + # Make a list with visible threads and an option to create a new one. + threads = '' + for thread in $$ '.thread' + id = thread.id[1..] + threads += "" + QR.threadSelector = + if g.BOARD is 'f' + $('select[name=filetag]').cloneNode true + else + $.el 'select' + innerHTML: threads + title: 'Create a new thread / Reply to a thread' + $.prepend $('.move > span', QR.el), QR.threadSelector + $.on QR.threadSelector, 'mousedown', (e) -> e.stopPropagation() + $.on $('#autohide', QR.el), 'change', QR.toggleHide + $.on $('.close', QR.el), 'click', QR.close + $.on $('#dump', QR.el), 'click', -> QR.el.classList.toggle 'dump' + $.on $('#addReply', QR.el), 'click', -> new QR.reply().select() + $.on $('form', QR.el), 'submit', QR.submit + $.on ta, 'input', -> QR.selected.el.lastChild.textContent = @value + $.on ta, 'input', QR.characterCount + $.on fileInput, 'change', QR.fileInput + $.on fileInput, 'click', (e) -> if e.shiftKey then QR.selected.rmFile() or e.preventDefault() + $.on spoiler.firstChild, 'change', -> $('input', QR.selected.el).click() + $.on $('.warning', QR.el), 'click', QR.cleanError + + new QR.reply().select() + # save selected reply's data + for name in ['name', 'email', 'sub', 'com'] + # The input event replaces keyup, change and paste events. + $.on $("[name=#{name}]", QR.el), 'input', -> + QR.selected[@name] = @value + # Disable auto-posting if you're typing in the first reply + # during the last 5 seconds of the cooldown. + if QR.cooldown.auto and QR.selected is QR.replies[0] and 0 < QR.cooldown.seconds <= 5 + QR.cooldown.auto = false + + QR.status.input = $ 'input[type=submit]', QR.el + QR.status() + QR.cooldown.init() + QR.captcha.init() + $.add d.body, QR.el + + # Create a custom event when the QR dialog is first initialized. + # Use it to extend the QR's functionalities, or for XTRM RICE. + $.event QR.el, new CustomEvent 'QRDialogCreation', + bubbles: true + + submit: (e) -> + e?.preventDefault() + if QR.cooldown.seconds + QR.cooldown.auto = !QR.cooldown.auto + QR.status() + return + QR.abort() + + reply = QR.replies[0] + if g.BOARD is 'f' and not g.REPLY + filetag = QR.threadSelector.value + threadID = 'new' + else + threadID = g.THREAD_ID or QR.threadSelector.value + + # prevent errors + if threadID is 'new' + threadID = null + if g.BOARD in ['vg', 'q'] and !reply.sub + err = 'New threads require a subject.' + else unless reply.file or textOnly = !!$ 'input[name=textonly]', $.id 'postForm' + err = 'No file selected.' + else if g.BOARD is 'f' and filetag is '9999' + err = 'Invalid tag specified.' + else unless reply.com or reply.file + err = 'No file selected.' + + if QR.captcha.isEnabled and !err + # get oldest valid captcha + captchas = $.get 'captchas', [] + # remove old captchas + while (captcha = captchas[0]) and captcha.time < Date.now() + captchas.shift() + if captcha = captchas.shift() + challenge = captcha.challenge + response = captcha.response + else + challenge = QR.captcha.img.alt + if response = QR.captcha.input.value then QR.captcha.reload() + $.set 'captchas', captchas + QR.captcha.count captchas.length + unless response + err = 'No valid captcha.' + else + response = response.trim() + # one-word-captcha: + # If there's only one word, duplicate it. + response = "#{response} #{response}" unless /\s/.test response + + if err + # stop auto-posting + QR.cooldown.auto = false + QR.status() + QR.error err + return + QR.cleanError() + + # Enable auto-posting if we have stuff to post, disable it otherwise. + QR.cooldown.auto = QR.replies.length > 1 + if Conf['Auto Hide QR'] and not QR.cooldown.auto + QR.hide() + if not QR.cooldown.auto and $.x 'ancestor::div[@id="qr"]', d.activeElement + # Unfocus the focused element if it is one within the QR and we're not auto-posting. + d.activeElement.blur() + + # Starting to upload might take some time. + # Provide some feedback that we're starting to submit. + QR.status progress: '...' + + post = + resto: threadID + name: reply.name + email: reply.email + sub: reply.sub + com: reply.com + upfile: reply.file + filetag: filetag + spoiler: reply.spoiler + textonly: textOnly + mode: 'regist' + pwd: if m = d.cookie.match(/4chan_pass=([^;]+)/) then decodeURIComponent m[1] else $('input[name=pwd]').value + recaptcha_challenge_field: challenge + recaptcha_response_field: response + + callbacks = + onload: -> + QR.response @response + onerror: -> + # Connection error, or + # CORS disabled error on www.4chan.org/banned + QR.cooldown.auto = false + QR.status() + QR.error $.el 'a', + href: '//www.4chan.org/banned', + target: '_blank', + textContent: 'Connection error, or you are banned.' + opts = + form: $.formData post + upCallbacks: + onload: -> + # Upload done, waiting for response. + QR.status progress: '...' + onprogress: (e) -> + # Uploading... + QR.status progress: "#{Math.round e.loaded / e.total * 100}%" + + QR.ajax = $.ajax $.id('postForm').parentNode.action, callbacks, opts + + response: (html) -> + doc = d.implementation.createHTMLDocument '' + doc.documentElement.innerHTML = html + if ban = $ '.banType', doc # banned/warning + board = $('.board', doc).innerHTML + err = $.el 'span', innerHTML: + if ban.textContent.toLowerCase() is 'banned' + "You are banned on #{board}! ;_;
" + + "Click here to see the reason." + else + "You were issued a warning on #{board} as #{$('.nameBlock', doc).innerHTML}.
" + + "Reason: #{$('.reason', doc).innerHTML}" + else if err = doc.getElementById 'errmsg' # error! + $('a', err)?.target = '_blank' # duplicate image link + else if doc.title isnt 'Post successful!' + err = 'Connection error with sys.4chan.org.' + + if err + if /captcha|verification/i.test(err.textContent) or err is 'Connection error with sys.4chan.org.' + # Remove the obnoxious 4chan Pass ad. + if /mistyped/i.test err.textContent + err = 'Error: You seem to have mistyped the CAPTCHA.' + # Enable auto-post if we have some cached captchas. + QR.cooldown.auto = + if QR.captcha.isEnabled + !!$.get('captchas', []).length + else if err is 'Connection error with sys.4chan.org.' + true + else + # Something must've gone terribly wrong if you get captcha errors without captchas. + # Don't auto-post indefinitely in that case. + false + # Too many frequent mistyped captchas will auto-ban you! + # On connection error, the post most likely didn't go through. + QR.cooldown.set delay: 2 + else # stop auto-posting + QR.cooldown.auto = false + QR.status() + QR.error err + return + + reply = QR.replies[0] + + persona = $.get 'QR.persona', {} + persona = + name: reply.name + email: if /^sage$/.test reply.email then persona.email else reply.email + sub: if Conf['Remember Subject'] then reply.sub else null + $.set 'QR.persona', persona + + [_, threadID, postID] = doc.body.lastChild.textContent.match /thread:(\d+),no:(\d+)/ + + # Post/upload confirmed as successful. + $.event QR.el, new CustomEvent 'QRPostSuccessful', + bubbles: true + detail: + threadID: threadID + postID: postID + + QR.cooldown.set + post: reply + isReply: threadID isnt '0' + + if threadID is '0' # new thread + # auto-noko + location.pathname = "/#{g.BOARD}/res/#{postID}" + else + # Enable auto-posting if we have stuff to post, disable it otherwise. + QR.cooldown.auto = QR.replies.length > 1 + if Conf['Open Reply in New Tab'] and !g.REPLY and !QR.cooldown.auto + $.open "//boards.4chan.org/#{g.BOARD}/res/#{threadID}#p#{postID}" + + if Conf['Persistent QR'] or QR.cooldown.auto + reply.rm() + else + QR.close() + + QR.status() + QR.resetFileInput() + + abort: -> + QR.ajax?.abort() + delete QR.ajax + QR.status() + Menu = init: -> return if g.VIEW is 'catalog' or !Conf['Menu'] diff --git a/src/main.coffee b/src/main.coffee index 7401ec9e8..5f0fc9b83 100644 --- a/src/main.coffee +++ b/src/main.coffee @@ -295,6 +295,7 @@ Main = initFeature 'Thread Hiding', ThreadHiding initFeature 'Reply Hiding', ReplyHiding initFeature 'Recursive', Recursive + initFeature 'Quick Reply', QR initFeature 'Menu', Menu initFeature 'Report Link', ReportLink initFeature 'Thread Hiding (Menu)', ThreadHiding.menu