config = main: Enhancing: '404 Redirect': [true, 'Redirect dead threads and images'] 'Keybinds': [true, 'Binds actions to keys'] 'Time Formatting': [true, 'Arbitrarily formatted timestamps, using your local time'] 'Report Button': [true, 'Add report buttons'] 'Comment Expansion': [true, 'Expand too long comments'] 'Thread Expansion': [true, 'View all replies'] 'Index Navigation': [true, 'Navigate to previous / next thread'] 'Reply Navigation': [false, 'Navigate to top / bottom of thread'] 'Check for Updates': [true, 'Check for updated versions of 4chan X'] Filtering: 'Anonymize': [false, 'Make everybody anonymous'] 'Filter': [false, 'Self-moderation placebo'] 'Filter OPs': [false, 'Filter OPs along with their threads'] 'Reply Hiding': [true, 'Hide single replies'] 'Thread Hiding': [true, 'Hide entire threads'] 'Show Stubs': [true, 'Of hidden threads / replies'] Imaging: 'Image Auto-Gif': [false, 'Animate gif thumbnails'] 'Image Expansion': [true, 'Expand images'] 'Image Hover': [false, 'Show full image on mouseover'] 'Sauce': [true, 'Add sauce to images'] 'Reveal Spoilers': [false, 'Replace spoiler thumbnails by the original thumbnail'] Monitoring: 'Thread Updater': [true, 'Update threads. Has more options in its own dialog.'] 'Unread Count': [true, 'Show unread post count in tab title'] 'Post in Title': [true, 'Show the op\'s post in the tab title'] 'Thread Stats': [true, 'Display reply and image count'] 'Thread Watcher': [true, 'Bookmark threads'] 'Auto Watch': [true, 'Automatically watch threads that you start'] 'Auto Watch Reply': [false, 'Automatically watch threads that you reply to'] Posting: 'Quick Reply': [true, 'Reply without leaving the page.'] 'Cooldown': [true, 'Prevent "flood detected" errors.'] 'Persistent QR': [false, 'The Quick reply won\'t disappear after posting.'] 'Auto Hide QR': [true, 'Automatically hide the quick reply when posting.'] 'Remember Subject': [false, 'Remember the subject field, instead of resetting after posting.'] 'Remember Spoiler': [false, 'Remember the spoiler state, instead of resetting after posting.'] Quoting: 'Quote Backlinks': [true, 'Add quote backlinks'] 'OP Backlinks': [false, 'Add backlinks to the OP'] 'Quote Highlighting': [true, 'Highlight the previewed post'] 'Quote Inline': [true, 'Show quoted post inline on quote click'] 'Quote Preview': [true, 'Show quote content on hover'] 'Indicate OP quote': [true, 'Add \'(OP)\' to OP quotes'] 'Indicate Cross-thread Quotes': [true, 'Add \'(Cross-thread)\' to cross-threads quotes'] 'Forward Hiding': [true, 'Hide original posts of inlined backlinks'] filter: name: '' tripcode: '' email: '' subject: '' comment: '' filename: '' filesize: '' md5: '' flavors: [ 'http://iqdb.org/?url=' 'http://google.com/searchbyimage?image_url=' '#http://tineye.com/search?url=' '#http://saucenao.com/search.php?db=999&url=' '#http://3d.iqdb.org/?url=' '#http://regex.info/exif.cgi?imgurl=' '#http://imgur.com/upload?url=' '#http://ompldr.org/upload?url1=' ].join '\n' time: '%m/%d/%y(%a)%H:%M' backlink: '>>%id' favicon: 'ferongr' hotkeys: openOptions: 'ctrl+o' close: 'Esc' spoiler: 'ctrl+s' openQR: 'i' openEmptyQR: 'I' submit: 'alt+s' nextReply: 'J' previousReply: 'K' nextThread: 'n' previousThread: 'p' nextPage: 'L' previousPage: 'H' zero: '0' openThreadTab: 'o' openThread: 'O' expandThread: 'e' watch: 'w' hide: 'x' expandImages: 'm' expandAllImages: 'M' update: 'u' unreadCountTo0: 'z' updater: checkbox: 'Scrolling': [false, 'Scroll updated posts into view. Only enabled at bottom of page.'] 'Scroll BG': [false, 'Scroll background tabs'] 'Verbose': [true, 'Show countdown timer, new post count'] 'Auto Update': [true, 'Automatically fetch new posts'] 'Interval': 30 # XXX Chrome can't into {log} = console # XXX GreaseMonkey can't into console.log.bind log = console.log.bind? console # flatten the config conf = {} (flatten = (parent, obj) -> if obj.length #array if typeof obj[0] is 'boolean' conf[parent] = obj[0] else conf[parent] = obj else if typeof obj is 'object' for key, val of obj flatten key, val else #constant conf[parent] = obj ) null, config NAMESPACE = '4chan_x.' VERSION = '2.23.7' SECOND = 1000 MINUTE = 60*SECOND HOUR = 60*MINUTE DAY = 24*HOUR engine = /WebKit|Presto|Gecko/.exec(navigator.userAgent)[0].toLowerCase() d = document g = callbacks: [] ui = dialog: (id, position, html) -> el = d.createElement 'div' el.className = 'reply dialog' el.innerHTML = html el.id = id el.style.cssText = if saved = localStorage["#{NAMESPACE}#{id}.position"] then saved else position el.querySelector('div.move').addEventListener 'mousedown', ui.dragstart, false el dragstart: (e) -> #prevent text selection e.preventDefault() ui.el = el = @parentNode d.addEventListener 'mousemove', ui.drag, false d.addEventListener 'mouseup', ui.dragend, false #distance from pointer to el edge is constant; calculate it here. # XXX opera reports el.offsetLeft / el.offsetTop as 0 rect = el.getBoundingClientRect() ui.dx = e.clientX - rect.left ui.dy = e.clientY - rect.top #factor out el from document dimensions ui.width = d.body.clientWidth - el.offsetWidth ui.height = d.body.clientHeight - el.offsetHeight drag: (e) -> left = e.clientX - ui.dx top = e.clientY - ui.dy left = if left < 10 then 0 else if ui.width - left < 10 then null else left top = if top < 10 then 0 else if ui.height - top < 10 then null else top right = if left is null then 0 else null bottom = if top is null then 0 else null #using null instead of '' is 4% faster #these 4 statements are 40% faster than 1 style.cssText {style} = ui.el style.top = top style.right = right style.bottom = bottom style.left = left dragend: -> #$ coffee -bpe '{a} = {b} = c' #var a, b; #a = (b = c.b, c).a; {el} = ui localStorage["#{NAMESPACE}#{el.id}.position"] = el.style.cssText d.removeEventListener 'mousemove', ui.drag, false d.removeEventListener 'mouseup', ui.dragend, false hover: (e) -> {clientX, clientY} = e {el} = ui {style} = el {clientHeight, clientWidth} = d.body height = el.offsetHeight top = clientY - 120 style.top = if clientHeight < height or top < 0 0 else if top + height > clientHeight clientHeight - height else top if clientX < clientWidth - 400 style.left = clientX + 45 style.right = null else style.left = null style.right = clientWidth - clientX + 45 hoverend: -> ui.el.parentNode.removeChild ui.el ### loosely follows the jquery api: http://api.jquery.com/ not chainable ### $ = (selector, root=d.body) -> root.querySelector selector $.extend = (object, properties) -> for key, val of properties object[key] = val object $.extend $, id: (id) -> d.getElementById id ajax: (url, cb, opts={}) -> {type, event, headers} = opts type or= 'get' event or= 'onload' r = new XMLHttpRequest() r.open type, url, true for key, val of headers r.setRequestHeader key, val r[event] = cb r.send() r cache: (url, cb) -> if req = $.cache.requests[url] if req.readyState is 4 cb.call req else req.callbacks.push cb else req = $.ajax url, (-> cb.call @ for cb in @callbacks) req.callbacks = [cb] $.cache.requests[url] = req cb: checked: -> $.set @name, @checked conf[@name] = @checked value: -> $.set @name, @value conf[@name] = @value addStyle: (css) -> style = $.el 'style', textContent: css $.add d.head, style style x: (path, root=d.body) -> d.evaluate(path, root, null, XPathResult.ANY_UNORDERED_NODE_TYPE, null). singleNodeValue tn: (s) -> d.createTextNode s replace: (root, el) -> root.parentNode.replaceChild el, root addClass: (el, className) -> el.classList.add className removeClass: (el, className) -> el.classList.remove className rm: (el) -> el.parentNode.removeChild el add: (parent, children...) -> for child in children parent.appendChild child prepend: (parent, child) -> parent.insertBefore child, parent.firstChild after: (root, el) -> root.parentNode.insertBefore el, root.nextSibling before: (root, el) -> root.parentNode.insertBefore el, root el: (tag, properties) -> el = d.createElement tag $.extend el, properties if properties el on: (el, eventType, handler) -> el.addEventListener eventType, handler, false off: (el, eventType, handler) -> el.removeEventListener eventType, handler, false isDST: -> ### http://en.wikipedia.org/wiki/Eastern_Time_Zone Its UTC time offset is −5 hrs (UTC−05) during standard time and −4 hrs (UTC−04) during daylight saving time. Since 2007, the local time changes at 02:00 EST to 03:00 EDT on the second Sunday in March and returns at 02:00 EDT to 01:00 EST on the first Sunday in November, in the U.S. as well as in Canada. 0200 EST (UTC-05) = 0700 UTC 0200 EDT (UTC-04) = 0600 UTC ### D = new Date() date = D.getUTCDate() day = D.getUTCDay() hours = D.getUTCHours() month = D.getUTCMonth() #this is the easy part if month < 2 or 10 < month return false if 2 < month < 10 return true # (sunday's date) = (today's date) - (number of days past sunday) # date is not zero-indexed sunday = date - day if month is 2 #before second sunday if sunday < 8 return false #during second sunday if sunday < 15 and day is 0 if hours < 7 return false return true #after second sunday return true #month is 10 # before first sunday if sunday < 1 return true # during first sunday if sunday < 8 and day is 0 if hours < 6 return true return false #after first sunday return false $.cache.requests = {} if GM_deleteValue? $.extend $, delete: (name) -> name = NAMESPACE + name GM_deleteValue name get: (name, defaultValue) -> name = NAMESPACE + name if value = GM_getValue name JSON.parse value else defaultValue openInTab: (url) -> GM_openInTab url set: (name, value) -> name = NAMESPACE + name # for `storage` events localStorage[name] = JSON.stringify value GM_setValue name, JSON.stringify value else $.extend $, delete: (name) -> name = NAMESPACE + name delete localStorage[name] get: (name, defaultValue) -> name = NAMESPACE + name if value = localStorage[name] JSON.parse value else defaultValue openInTab: (url) -> window.open url, "_blank" set: (name, value) -> name = NAMESPACE + name localStorage[name] = JSON.stringify value #load values from localStorage for key, val of conf conf[key] = $.get key, val $$ = (selector, root=d.body) -> Array::slice.call root.querySelectorAll selector filter = regexps: {} callbacks: [] init: -> for key of config.filter unless m = conf[key].match /^\/.+\/\w*$/gm continue @regexps[key] = [] for filter in m f = filter.match /^\/(.+)\/(\w*)$/ try @regexps[key].push RegExp f[1], f[2] catch e alert e.message #only execute what's filterable @callbacks.push @[key] g.callbacks.push @node node: (root) -> unless root.className if filter.callbacks.some((callback) -> callback root) replyHiding.hideHide $ 'td:not([nowrap])', root else if root.className is 'op' and not g.REPLY and conf['Filter OPs'] if filter.callbacks.some((callback) -> callback root) threadHiding.hideHide root.parentNode test: (key, value) -> filter.regexps[key].some (regexp) -> regexp.test value name: (root) -> name = if root.className is 'op' then $ '.postername', root else $ '.commentpostername', root filter.test 'name', name.textContent tripcode: (root) -> if trip = $ '.postertrip', root filter.test 'tripcode', trip.textContent email: (root) -> if mail = $ '.linkmail', root filter.test 'email', mail.href subject: (root) -> sub = if root.className is 'op' then $ '.filetitle', root else $ '.replytitle', root filter.test 'subject', sub.textContent comment: (root) -> filter.test 'comment', ($.el 'a', innerHTML: $('blockquote', root).innerHTML.replace /
/g, '\n').textContent filename: (root) -> if file = $ '.filesize span', root filter.test 'filename', file.title filesize: (root) -> if img = $ 'img[md5]', root filter.test 'filesize', img.alt md5: (root) -> if img = $ 'img[md5]', root filter.test 'md5', img.getAttribute('md5') strikethroughQuotes = init: -> g.callbacks.push (root) -> return if root.className is 'inline' for quote in $$ '.quotelink', root if el = $.id quote.hash[1..] if el.parentNode.parentNode.parentNode.hidden $.addClass quote, 'filtered' expandComment = init: -> for a in $$ '.abbr a' $.on a, 'click', expandComment.expand expand: (e) -> e.preventDefault() [_, threadID, replyID] = @href.match /(\d+)#(\d+)/ @textContent = "Loading #{replyID}..." threadID = @pathname.split('/').pop() or $.x('ancestor::div[@class="thread"]/div', @).id a = @ $.cache @pathname, (-> expandComment.parse @, a, threadID, replyID) parse: (req, a, threadID, replyID) -> if req.status isnt 200 a.textContent = "#{req.status} #{req.statusText}" return body = $.el 'body', innerHTML: req.responseText if threadID is replyID #OP bq = $ 'blockquote', body else #css selectors don't like ids starting with numbers, # getElementById only works for root document. for reply in $$ 'td[id]', body if reply.id == replyID bq = $ 'blockquote', reply break for quote in $$ '.quotelink', bq if quote.getAttribute('href') is quote.hash quote.pathname = "/#{g.BOARD}/res/#{threadID}" if quote.hash[1..] is threadID quote.innerHTML += ' (OP)' if conf['Quote Preview'] $.on quote, 'mouseover', quotePreview.mouseover $.on quote, 'mousemove', ui.hover $.on quote, 'mouseout', quotePreview.mouseout if conf['Quote Inline'] $.on quote, 'click', quoteInline.toggle $.replace a.parentNode.parentNode, bq expandThread = init: -> for span in $$ '.omittedposts' a = $.el 'a', textContent: "+ #{span.textContent}" className: 'omittedposts' href: 'javascript:;' $.on a, 'click', expandThread.cb.toggle $.replace span, a cb: toggle: -> thread = @parentNode expandThread.toggle thread toggle: (thread) -> threadID = thread.firstChild.id pathname = "/#{g.BOARD}/res/#{threadID}" a = $ '.omittedposts', thread switch a.textContent[0] when '+' $('.op .container', thread)?.innerHTML = '' a.textContent = a.textContent.replace '+', 'X Loading...' $.cache pathname, (-> expandThread.parse @, pathname, thread, a) when 'X' a.textContent = a.textContent.replace 'X Loading...', '+' #FIXME this will kill all callbacks $.cache[pathname].abort() when '-' a.textContent = a.textContent.replace '-', '+' #goddamit moot num = switch g.BOARD when 'b' then 3 when 't' then 1 else 5 table = $.x "following::br[@clear]/preceding::table[#{num}]", a while (prev = table.previousSibling) and (prev.nodeName is 'TABLE') $.rm prev for backlink in $$ '.op a.backlink' $.rm backlink if !$.id backlink.hash[1..] parse: (req, pathname, thread, a) -> if req.status isnt 200 a.textContent = "#{req.status} #{req.statusText}" $.off a, 'click', expandThread.cb.toggle return a.textContent = a.textContent.replace 'X Loading...', '-' body = $.el 'body', innerHTML: req.responseText frag = d.createDocumentFragment() for reply in $$ '.reply', body for quote in $$ '.quotelink', reply if (href = quote.getAttribute('href')) is quote.hash #add pathname to normal quotes quote.pathname = pathname else if href isnt quote.href #fix x-thread links, not x-board ones quote.href = "res/#{href}" link = $ '.quotejs', reply link.href = "res/#{thread.firstChild.id}##{reply.id}" link.nextSibling.href = "res/#{thread.firstChild.id}#q#{reply.id}" $.add frag, reply.parentNode.parentNode.parentNode # eat everything, then replace with fresh full posts while (next = a.nextSibling) and not next.clear #br[clear] $.rm next br = next $.before br, frag replyHiding = init: -> g.callbacks.push (root) -> return unless dd = $ '.doubledash', root dd.className = 'replyhider' a = $.el 'a', textContent: '[ - ]' href: 'javascript:;' $.on a, 'click', replyHiding.cb.hide $.replace dd.firstChild, a reply = dd.nextSibling id = reply.id if id of g.hiddenReplies replyHiding.hide reply cb: hide: -> reply = @parentNode.nextSibling replyHiding.hide reply show: -> div = @parentNode table = div.nextSibling replyHiding.show table $.rm div hide: (reply) -> replyHiding.hideHide reply id = reply.id for quote in $$ ".quotelink[href='##{id}'], .backlink[href='##{id}']" $.addClass quote, 'filtered' g.hiddenReplies[id] = Date.now() $.set "hiddenReplies/#{g.BOARD}/", g.hiddenReplies hideHide: (reply) -> table = reply.parentNode.parentNode.parentNode return if table.hidden #already hidden by filter table.hidden = true if conf['Show Stubs'] name = $('.commentpostername', reply).textContent trip = $('.postertrip', reply)?.textContent or '' a = $.el 'a', textContent: "[ + ] #{name} #{trip}" href: 'javascript:;' $.on a, 'click', replyHiding.cb.show div = $.el 'div', className: 'stub' $.add div, a $.before table, div show: (table) -> table.hidden = false id = $('td[id]', table).id for quote in $$ ".quotelink[href='##{id}'], .backlink[href='##{id}']" $.removeClass quote, 'filtered' delete g.hiddenReplies[id] $.set "hiddenReplies/#{g.BOARD}/", g.hiddenReplies keybinds = init: -> for node in $$ '[accesskey]' node.removeAttribute 'accesskey' $.on d, 'keydown', keybinds.keydown keydown: (e) -> updater.focus = true if not (key = keybinds.keyCode(e)) or /TEXTAREA|INPUT/.test(e.target.nodeName) and not (e.altKey or e.ctrlKey or e.keyCode is 27) return thread = nav.getThread() switch key when conf.openOptions options.dialog() unless $.id 'overlay' when conf.close if o = $.id 'overlay' $.rm o else if qr.el qr.close() when conf.spoiler ta = e.target return unless ta.nodeName is 'TEXTAREA' value = ta.value selStart = ta.selectionStart selEnd = ta.selectionEnd valStart = value[0...selStart] + '[spoiler]' valMid = value[selStart...selEnd] valEnd = '[/spoiler]' + value[selEnd..] ta.value = valStart + valMid + valEnd range = valStart.length + valMid.length ta.setSelectionRange range, range when conf.zero window.location = "/#{g.BOARD}/0#0" when conf.openEmptyQR keybinds.qr thread when conf.openQR keybinds.qr thread, true when conf.nextReply keybinds.hl.next thread when conf.previousReply keybinds.hl.prev thread when conf.expandAllImages keybinds.img thread, true when conf.openThread keybinds.open thread when conf.expandThread expandThread.toggle thread when conf.expandImages keybinds.img thread when conf.nextThread nav.next() when conf.openThreadTab keybinds.open thread, true when conf.previousThread nav.prev() when conf.update updater.update() when conf.watch watcher.toggle thread when conf.hide threadHiding.toggle thread when conf.nextPage $('input[value=Next]')?.click() when conf.previousPage $('input[value=Previous]')?.click() when conf.submit qr.submit() if qr.el when conf.unreadCountTo0 unread.replies = [] unread.updateTitle() Favicon.update() else return e.preventDefault() keyCode: (e) -> key = switch kc = e.keyCode when 8 '' when 27 'Esc' when 37 'Left' when 38 'Up' when 39 'Right' when 40 'Down' when 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90 #0-9, A-Z c = String.fromCharCode kc if e.shiftKey then c else c.toLowerCase() else null if key if e.altKey then key = 'alt+' + key if e.ctrlKey then key = 'ctrl+' + key key img: (thread, all) -> if all $("#imageExpand").click() else thumb = $ 'img[md5]', $('.replyhl', thread) or thread imgExpand.toggle thumb.parentNode qr: (thread, quote) -> if quote qr.quote.call $ '.quotejs + .quotejs', $('.replyhl', thread) or thread else qr.open() $('textarea', qr.el).focus() open: (thread, tab) -> id = thread.firstChild.id url = "http://boards.4chan.org/#{g.BOARD}/res/#{id}" if tab $.openInTab url else location.href = url hl: next: (thread) -> if td = $ 'td.replyhl', thread td.className = 'reply' rect = td.getBoundingClientRect() if rect.top > 0 and rect.bottom < d.body.clientHeight #you're fully visible next = $.x 'following::td[@class="reply"]', td return if $.x('ancestor::div[@class="thread"]', next) isnt thread rect = next.getBoundingClientRect() if rect.top > 0 and rect.bottom < d.body.clientHeight #and so is the next next.className = 'replyhl' return replies = $$ 'td.reply', thread for reply in replies top = reply.getBoundingClientRect().top if top > 0 reply.className = 'replyhl' return prev: (thread) -> if td = $ 'td.replyhl', thread td.className = 'reply' rect = td.getBoundingClientRect() if rect.top > 0 and rect.bottom < d.body.clientHeight #you're fully visible prev = $.x 'preceding::td[@class="reply"][1]', td rect = prev.getBoundingClientRect() if rect.top > 0 and rect.bottom < d.body.clientHeight #and so is the prev prev.className = 'replyhl' return replies = $$ 'td.reply', thread replies.reverse() height = d.body.clientHeight for reply in replies bot = reply.getBoundingClientRect().bottom if bot < height reply.className = 'replyhl' return nav = # ◀ ▶ init: -> span = $.el 'span', id: 'navlinks' prev = $.el 'a', textContent: '▲' href: 'javascript:;' next = $.el 'a', textContent: '▼' href: 'javascript:;' $.on prev, 'click', nav.prev $.on next, 'click', nav.next $.add span, prev, $.tn(' '), next $.add d.body, span prev: -> nav.scroll -1 next: -> nav.scroll +1 threads: [] getThread: (full) -> nav.threads = $$ '.thread:not([hidden])' for thread, i in nav.threads rect = thread.getBoundingClientRect() {bottom} = rect if bottom > 0 #we have not scrolled past if full return [thread, i, rect] return thread return null scroll: (delta) -> if g.REPLY if delta is -1 window.scrollTo 0,0 else window.scrollTo 0, d.body.scrollHeight return [thread, i, rect] = nav.getThread true {top} = rect #unless we're not at the beginning of the current thread # (and thus wanting to move to beginning) # or we're above the first thread and don't want to skip it unless (delta is -1 and Math.ceil(top) < 0) or (delta is +1 and top > 1) i += delta if i is -1 if g.PAGENUM is 0 window.scrollTo 0, 0 else window.location = "#{g.PAGENUM - 1}#0" return if delta is +1 # if we're at the last thread, or we're at the bottom of the page. # kind of hackish, what we really need to do is make nav.getThread smarter. if i is nav.threads.length or (innerHeight + pageYOffset == d.body.scrollHeight) if $ 'table.pages input[value="Next"]' window.location = "#{g.PAGENUM + 1}#0" return #TODO sfx {top} = nav.threads[i].getBoundingClientRect() window.scrollBy 0, top qr = init: -> return unless $ 'form[name=post]' g.callbacks.push (root) -> $.on $('.quotejs + .quotejs', root), 'click', qr.quote if conf['Persistent QR'] qr.dialog() $.id('autohide').click() if conf['Auto Hide QR'] open: -> if qr.el qr.el.hidden = false $.id('autohide').checked = false qr.hide() else qr.dialog() close: -> qr.el.hidden = true d.activeElement.blur() hide: -> if $.id('autohide').checked $.addClass qr.el, 'autohide' else $.removeClass qr.el, 'autohide' error: (err) -> $('.error', qr.el).textContent = err qr.open() alert err cleanError: -> $('.error', qr.el).textContent = null quote: (e) -> e?.preventDefault() qr.open() id = @textContent text = ">>#{id}\n" sel = window.getSelection() if (s = sel.toString()) and id is $.x('ancestor-or-self::blockquote/preceding-sibling::input', sel.anchorNode)?.name s = s.replace /\n/g, '\n>' text += ">#{s}\n" ta = $ 'textarea', qr.el caretPos = ta.selectionStart #replace selection for text ta.value = ta.value[0...caretPos] + text + ta.value[ta.selectionEnd...ta.value.length] ta.focus() #move the caret to the end of the new quote ta.selectionEnd = ta.selectionStart = caretPos + text.length fileInput: -> qr.cleanError() if @files.length is 1 file = @files[0] if file.size > @max qr.error 'File too large.' else if -1 is qr.mimeTypes.indexOf file.type qr.error 'Unsupported file type.' else # modify selected reply's file return 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 # add new reply # set reply's file $.addClass qr.el, 'dump' dialog: -> # create a new thread or select thread to reply to unless g.REPLY threads = '' for thread in $$ '.op' threads += "