merge master

This commit is contained in:
James Campos 2011-08-06 20:57:19 -07:00
commit 0a6149f0ac
5 changed files with 825 additions and 667 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,13 @@
# Installing
[master](https://github.com/aeosynth/4chan-x/raw/master/4chan_x.user.js) - bleeding edge. exciting new features, exciting new bugs.
[stable](https://github.com/aeosynth/4chan-x/raw/stable/4chan_x.user.js) - tries to be bug free.
# Building # Building
[install nodejs and npm](https://github.com/joyent/node/wiki/Installation), [install nodejs and npm](https://github.com/joyent/node/wiki/Installation),
install [coffee-script](https://github.com/jashkenas/coffee-script/) with install [coffee-script](https://github.com/jashkenas/coffee-script/) with
`npm install -g coffee-script`, clone 4chan x, cd into it and run `npm install -g coffee-script`, clone 4chan x, cd into it and run
`npm link coffee-script`. actually build it with `cake dev &`. `npm link coffee-script`. actually build it with `cake build`. for development
kill the process with `killall node`. (continuous builds), run `cake dev &`. kill the process with `killall node`.

View File

@ -1,4 +1,19 @@
github github
- mayhem:
- fix post links in expanded threads
- fix 4chan X in closed threads
- aeosynth:
- only auto scroll focused tabs
- quote inlining: only work on unmodified left-click
- select multiple files (one at a time)
- captcha caching
- qr: optional auto hiding
- copy old textarea value
- scroll to bottom of page if post isn't found (thumbnail generation takes
time)
- only scroll focused tabs
- time: %e, %k, %l
- reverted hovering fix
2.17.1 2.17.1
- mayhem: - mayhem:

2
header
View File

@ -2,7 +2,7 @@
// @name 4chan x // @name 4chan x
// @namespace aeosynth // @namespace aeosynth
// @description Adds various features. // @description Adds various features.
// @version 11.8.4.0 // @version 11.8.6.0
// @copyright 2009-2011 James Campos <james.r.campos@gmail.com> // @copyright 2009-2011 James Campos <james.r.campos@gmail.com>
// @license MIT; http://en.wikipedia.org/wiki/Mit_license // @license MIT; http://en.wikipedia.org/wiki/Mit_license
// @include http://boards.4chan.org/* // @include http://boards.4chan.org/*

View File

@ -34,6 +34,7 @@ config =
'Cooldown': [true, 'Prevent \'flood detected\' errors'] 'Cooldown': [true, 'Prevent \'flood detected\' errors']
'Quick Reply': [true, 'Reply without leaving the page'] 'Quick Reply': [true, 'Reply without leaving the page']
'Persistent QR': [false, 'Quick reply won\'t disappear after posting. Only in replies.'] 'Persistent QR': [false, 'Quick reply won\'t disappear after posting. Only in replies.']
'Auto Hide QR': [true, 'Automatically auto-hide the quick reply when posting']
Quoting: Quoting:
'Quote Backlinks': [true, 'Add quote backlinks'] 'Quote Backlinks': [true, 'Add quote backlinks']
'OP Backlinks': [false, 'Add backlinks to the OP'] 'OP Backlinks': [false, 'Add backlinks to the OP']
@ -48,6 +49,7 @@ config =
'#http://tineye.com/search?url=' '#http://tineye.com/search?url='
'#http://saucenao.com/search.php?db=999&url=' '#http://saucenao.com/search.php?db=999&url='
'#http://imgur.com/upload?url=' '#http://imgur.com/upload?url='
'#http://anonym.to/?'
].join '\n' ].join '\n'
time: '%m/%d/%y(%a)%H:%M' time: '%m/%d/%y(%a)%H:%M'
hotkeys: hotkeys:
@ -200,6 +202,8 @@ $.extend = (object, properties) ->
object object
$.extend $, $.extend $,
id: (id) ->
d.getElementById id
globalEval: (code) -> globalEval: (code) ->
script = $.el 'script', script = $.el 'script',
textContent: "(#{code})()" textContent: "(#{code})()"
@ -430,7 +434,7 @@ expandThread =
while (prev = table.previousSibling) and (prev.nodeName is 'TABLE') while (prev = table.previousSibling) and (prev.nodeName is 'TABLE')
$.rm prev $.rm prev
for backlink in $$ '.op a.backlink' for backlink in $$ '.op a.backlink'
$.rm backlink if !d.getElementById backlink.hash[1..] $.rm backlink if !$.id backlink.hash[1..]
parse: (req, pathname, thread, a) -> parse: (req, pathname, thread, a) ->
@ -449,9 +453,13 @@ expandThread =
body = $.el 'body', body = $.el 'body',
innerHTML: req.responseText innerHTML: req.responseText
for quote in $$ 'a.quotelink', body for reply in $$ 'td[id]', body
if quote.getAttribute('href') is quote.hash for quote in $$ 'a.quotelink', reply
quote.pathname = pathname if quote.getAttribute('href') is quote.hash
quote.pathname = pathname
link = $ 'a.quotejs', reply
link.href = "res/#{thread.firstChild.id}##{reply.id}"
link.nextSibling.href = "res/#{thread.firstChild.id}#q#{reply.id}"
tables = $$ 'form[name=delform] table', body tables = $$ 'form[name=delform] table', body
tables.pop() tables.pop()
for table in tables for table in tables
@ -515,100 +523,100 @@ keybinds =
init: -> init: ->
for node in $$ '[accesskey]' for node in $$ '[accesskey]'
node.removeAttribute 'accesskey' node.removeAttribute 'accesskey'
$.bind d, 'keydown', keybinds.cb.keydown $.bind d, 'keydown', keybinds.keydown
cb: keydown: (e) ->
keydown: (e) -> updater.focus = true
return if e.target.nodeName in ['TEXTAREA', 'INPUT'] and not e.altKey and not e.ctrlKey and not (e.keyCode is 27) return if e.target.nodeName in ['TEXTAREA', 'INPUT'] and not e.altKey and not e.ctrlKey and not (e.keyCode is 27)
return unless key = keybinds.cb.keyCode e return unless key = keybinds.keyCode e
thread = nav.getThread() thread = nav.getThread()
switch key switch key
when conf.close when conf.close
if o = $ '#overlay' if o = $ '#overlay'
$.rm o $.rm o
else if qr.el else if qr.el
qr.close() qr.close()
when conf.spoiler when conf.spoiler
ta = e.target ta = e.target
return unless ta.nodeName is 'TEXTAREA' return unless ta.nodeName is 'TEXTAREA'
value = ta.value value = ta.value
selStart = ta.selectionStart selStart = ta.selectionStart
selEnd = ta.selectionEnd selEnd = ta.selectionEnd
valStart = value[0...selStart] + '[spoiler]' valStart = value[0...selStart] + '[spoiler]'
valMid = value[selStart...selEnd] valMid = value[selStart...selEnd]
valEnd = '[/spoiler]' + value[selEnd..] valEnd = '[/spoiler]' + value[selEnd..]
ta.value = valStart + valMid + valEnd ta.value = valStart + valMid + valEnd
range = valStart.length + valMid.length range = valStart.length + valMid.length
ta.setSelectionRange range, range ta.setSelectionRange range, range
when conf.zero when conf.zero
window.location = "/#{g.BOARD}/0#0" window.location = "/#{g.BOARD}/0#0"
when conf.openEmptyQR when conf.openEmptyQR
keybinds.qr thread keybinds.qr thread
when conf.nextReply when conf.nextReply
keybinds.hl.next thread keybinds.hl.next thread
when conf.previousReply when conf.previousReply
keybinds.hl.prev thread keybinds.hl.prev thread
when conf.expandAllImages when conf.expandAllImages
keybinds.img thread, true keybinds.img thread, true
when conf.openThread when conf.openThread
keybinds.open thread keybinds.open thread
when conf.expandThread when conf.expandThread
expandThread.toggle thread expandThread.toggle thread
when conf.openQR when conf.openQR
keybinds.qr thread, true keybinds.qr thread, true
when conf.expandImages when conf.expandImages
keybinds.img thread keybinds.img thread
when conf.nextThread when conf.nextThread
nav.next() nav.next()
when conf.openThreadTab when conf.openThreadTab
keybinds.open thread, true keybinds.open thread, true
when conf.previousThread when conf.previousThread
nav.prev() nav.prev()
when conf.update when conf.update
updater.update() updater.update()
when conf.watch when conf.watch
watcher.toggle thread watcher.toggle thread
when conf.hide when conf.hide
threadHiding.toggle thread threadHiding.toggle thread
when conf.nextPage when conf.nextPage
$('input[value=Next]')?.click() $('input[value=Next]')?.click()
when conf.previousPage when conf.previousPage
$('input[value=Previous]')?.click() $('input[value=Previous]')?.click()
when conf.submit when conf.submit
if qr.el if qr.el
qr.submit.call $ 'form', qr.el qr.submit.call $ 'form', qr.el
else
$('.postarea form').submit()
when conf.unreadCountTo0
unread.replies.length = 0
unread.updateTitle()
Favicon.update()
else else
return $('.postarea form').submit()
e.preventDefault() when conf.unreadCountTo0
unread.replies.length = 0
keyCode: (e) -> unread.updateTitle()
kc = e.keyCode Favicon.update()
if 65 <= kc <= 90 #A-Z
key = String.fromCharCode kc
if !e.shiftKey
key = key.toLowerCase()
else if 48 <= kc <= 57 #0-9
key = String.fromCharCode kc
else if kc is 27
key = 'Esc'
else if kc is 8
key = ''
else else
key = null return
if key e.preventDefault()
if e.altKey then key = 'alt+' + key
if e.ctrlKey then key = 'ctrl+' + key keyCode: (e) ->
key kc = e.keyCode
if 65 <= kc <= 90 #A-Z
key = String.fromCharCode kc
if !e.shiftKey
key = key.toLowerCase()
else if 48 <= kc <= 57 #0-9
key = String.fromCharCode kc
else if kc is 27
key = 'Esc'
else if kc is 8
key = ''
else
key = null
if key
if e.altKey then key = 'alt+' + key
if e.ctrlKey then key = 'ctrl+' + key
key
img: (thread, all) -> img: (thread, all) ->
if all if all
@ -623,7 +631,7 @@ keybinds =
qrLink = $ "span[id^=nothread] a:not(:first-child)", thread qrLink = $ "span[id^=nothread] a:not(:first-child)", thread
if quote if quote
qr.quote qrLink qr.quote.call qrLink
else else
unless qr.el unless qr.el
qr.dialog qrLink qr.dialog qrLink
@ -786,18 +794,28 @@ options =
<caption>Format specifiers <a href=http://en.wikipedia.org/wiki/Date_%28Unix%29#Formatting>(source)</a></caption> <caption>Format specifiers <a href=http://en.wikipedia.org/wiki/Date_%28Unix%29#Formatting>(source)</a></caption>
<tbody> <tbody>
<tr><th>Specifier</th><th>Description</th><th>Values/Example</th></tr> <tr><th>Specifier</th><th>Description</th><th>Values/Example</th></tr>
<tr><td>%a</td><td>weekday, abbreviated</td><td>Sat</td></tr> <tr><th colspan=3>Year</th></tr>
<tr><td>%A</td><td>weekday, full</td><td>Saturday</td></tr> <tr><td>%y</td><td>two digit year</td><td>00-99</td></tr>
<tr><th colspan=3>Month</th></tr>
<tr><td>%b</td><td>month, abbreviated</td><td>Jun</td></tr> <tr><td>%b</td><td>month, abbreviated</td><td>Jun</td></tr>
<tr><td>%B</td><td>month, full length</td><td>June</td></tr> <tr><td>%B</td><td>month, full length</td><td>June</td></tr>
<tr><td>%d</td><td>day of the month, zero padded</td><td>03</td></tr>
<tr><td>%H</td><td>hour (24 hour clock) zero padded</td><td>13</td></tr>
<tr><td>%I (uppercase i)</td><td>hour (12 hour clock) zero padded</td><td>02</td></tr>
<tr><td>%m</td><td>month, zero padded</td><td>06</td></tr> <tr><td>%m</td><td>month, zero padded</td><td>06</td></tr>
<tr><th colspan=3>Day</th></tr>
<tr><td>%a</td><td>weekday, abbreviated</td><td>Sat</td></tr>
<tr><td>%A</td><td>weekday, full</td><td>Saturday</td></tr>
<tr><td>%d</td><td>day of the month, zero padded</td><td>03</td></tr>
<tr><td>%e</td><td>day of the month</td><td>3</td></tr>
<tr><th colspan=3>Time</th></tr>
<tr><td>%H</td><td>hour (24 hour clock) zero padded</td><td>13</td></tr>
<tr><td>%l (lowercase L)</td><td>hour (12 hour clock)</td><td>1</td></tr>
<tr><td>%I (uppercase i)</td><td>hour (12 hour clock) zero padded</td><td>01</td></tr>
<tr><td>%k</td><td>hour (24 hour clock)</td><td>13</td></tr>
<tr><td>%M</td><td>minutes, zero padded</td><td>54</td></tr> <tr><td>%M</td><td>minutes, zero padded</td><td>54</td></tr>
<tr><td>%p</td><td>upper case AM or PM</td><td>PM</td></tr> <tr><td>%p</td><td>upper case AM or PM</td><td>PM</td></tr>
<tr><td>%P</td><td>lower case am or pm</td><td>pm</td></tr> <tr><td>%P</td><td>lower case am or pm</td><td>pm</td></tr>
<tr><td>%y</td><td>two digit year</td><td>00-99</td></tr>
</tbody> </tbody>
</table> </table>
</div> </div>
@ -892,7 +910,7 @@ options =
keybind: (e) -> keybind: (e) ->
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
return unless (key = keybinds.cb.keyCode e)? return unless (key = keybinds.keyCode e)?
@value = key @value = key
$.setValue @name, key $.setValue @name, key
conf[@name] = key conf[@name] = key
@ -936,7 +954,7 @@ cooldown =
cb: -> cb: ->
submits = $$ '#com_submit' submits = $$ '#com_submit'
if --cooldown.duration if --cooldown.duration > 0
for submit in submits for submit in submits
submit.value = cooldown.duration submit.value = cooldown.duration
else else
@ -945,61 +963,182 @@ cooldown =
submit.disabled = false submit.disabled = false
submit.value = 'Submit' submit.value = 'Submit'
if qr.el and $('#auto', qr.el).checked if qr.el and $('#auto', qr.el).checked
qr.submit.call $ 'form', qr.el qr.autoPost()
qr = qr =
# TODO
# error handling
# persistent captcha
# rm Recaptcha
# error too large error should happen on attach
init: -> init: ->
g.callbacks.push qr.cb.node g.callbacks.push qr.node
$.bind window, 'message', qr.message
$.bind $('#recaptcha_challenge_field_holder'), 'DOMNodeInserted', qr.captchaNode
qr.captcha = []
iframe = $.el 'iframe', iframe = $.el 'iframe',
name: 'iframe' name: 'iframe'
hidden: true hidden: true
$.append d.body, iframe $.append d.body, iframe
$.bind window, 'message', qr.cb.message
#hack - nuke id so it doesn't grab focus when reloading #hack - nuke id so it doesn't grab focus when reloading
$('#recaptcha_response_field').id = '' $('#recaptcha_response_field').id = ''
autohide: attach: ->
set: -> $('#auto', qr.el).checked = true
$('#autohide:not(:checked)', qr.el)?.click() fileDiv = $.el 'div', innerHTML: '<input type=file name=upfile><a>X</a>'
unset: -> $.bind fileDiv.lastChild, 'click', (-> $.rm @parentNode)
$('#autohide:checked', qr.el)?.click() $.prepend qr.files, fileDiv
cb: attachNext: ->
autohide: (e) -> fileDiv = $.rm qr.files.lastChild
if @checked file = fileDiv.firstChild
$.addClass qr.el, 'auto' oldFile = $ '#qr_form input[type=file]', qr.el
$.replace oldFile, file
autoPost: ->
responseField = $ '#recaptcha_response_field', qr.el
if !responseField.value and captcha = qr.captcha.shift()
$('#recaptcha_challenge_field', qr.el).value = captcha.challenge
responseField.value = captcha.response
responseField.nextSibling.textContent = qr.captcha.length + ' captcha cached'
qr.submit.call $ 'form', qr.el
captchaNode: (e) ->
return unless qr.el
{target} = e
$('img', qr.el).src = "http://www.google.com/recaptcha/api/image?c=" + target.value
$('#recaptcha_challenge_field', qr.el).value = target.value
captchaKeydown: (e) ->
if e.keyCode is 13 and cooldown.duration # press enter to enable auto-post if cooldown is still running
$('#auto', qr.el).checked = true
$('#autohide', qr.el).checked = true if conf['Auto Hide QR']
qr.captchaPush.call this
captchaPush: ->
l = qr.captcha.push
challenge: $('#recaptcha_challenge_field', qr.el).value
response: @value
@nextSibling.textContent = l + ' captcha cached'
Recaptcha.reload()
@value = ''
close: ->
$.rm qr.el
qr.el = null
dialog: (link) ->
submitValue = $('#com_submit').value
submitDisabled = if $('#com_submit').disabled then 'disabled' else ''
#FIXME inlined cross-thread quotes
THREAD_ID = g.THREAD_ID or $.x('ancestor::div[@class="thread"]/div', link).id
spoiler = if $('.postarea label') then '<label> [<input type=checkbox name=spoiler>Spoiler Image?]</label>' else ''
challenge = $('#recaptcha_challenge_field').value
html = "
<a id=close title=close>X</a>
<input type=checkbox id=autohide title=autohide>
<div class=move>
<input class=inputtext type=text name=name placeholder=Name form=qr_form>
Quick Reply
</div>
<form name=post action=http://sys.4chan.org/#{g.BOARD}/post method=POST enctype=multipart/form-data target=iframe id=qr_form>
<input type=hidden name=resto value=#{THREAD_ID}>
<input type=hidden name=recaptcha_challenge_field id=recaptcha_challenge_field value=#{challenge}>
<div><input class=inputtext type=text name=email placeholder=E-mail>#{spoiler}</div>
<div><input class=inputtext type=text name=sub placeholder=Subject><input type=submit value=#{submitValue} id=com_submit #{submitDisabled}><label><input type=checkbox id=auto>auto</label></div>
<div><textarea class=inputtext name=com placeholder=Comment></textarea></div>
<div><img src=http://www.google.com/recaptcha/api/image?c=#{challenge}></div>
<div><input class=inputtext type=text name=recaptcha_response_field placeholder=Verification required autocomplete=off id=recaptcha_response_field>0 captcha cached</div>
<div><input type=file name=upfile></div>
<div><input class=inputtext type=password name=pwd maxlength=8 placeholder=Password><input type=hidden name=mode value=regist><a name=attach>attach another file</a></div>
</form>
<div id=files></div>
<a id=error class=error></a>
"
qr.el = ui.dialog 'qr', top: '0px', left: '0px', html
qr.files = $ '#files', qr.el
qr.refresh()
$('textarea', qr.el).value = $('textarea').value
$.bind $('input[name=name]', qr.el), 'mousedown', (e) -> e.stopPropagation()
$.bind $('#close', qr.el), 'click', qr.close
$.bind $('form', qr.el), 'submit', qr.submit
$.bind $('a[name=attach]', qr.el), 'click', qr.attach
$.bind $('img', qr.el), 'click', Recaptcha.reload
$.bind $('#recaptcha_response_field', qr.el), 'keydown', Recaptcha.listener
$.bind $('#recaptcha_response_field', qr.el), 'keydown', qr.captchaKeydown
$.append d.body, qr.el
message: (e) ->
Recaptcha.reload()
$('iframe[name=iframe]').src = 'about:blank'
{data} = e
if data # error message
data = JSON.parse data
$.extend $('#error', qr.el), data
$('#recaptcha_response_field', qr.el).value = ''
$('#autohide', qr.el).checked = false
if data.textContent is 'You seem to have mistyped the verification.'
if qr.captcha.length
qr.autoPost()
else if data.textContent is 'Error: Duplicate file entry detected.' and qr.files.childElementCount
$('textarea', qr.el).value += '\n' + data.textContent + ' ' + data.href
qr.attachNext()
if qr.captcha.length
qr.autoPost()
return
if qr.el
if g.REPLY and (conf['Persistent QR'] or qr.files.childElementCount)
qr.refresh()
if qr.files.childElementCount
qr.attachNext()
else else
$.removeClass qr.el, 'auto' qr.close()
if conf['Cooldown']
duration = if qr.sage then 60 else 30
$.setValue g.BOARD+'/cooldown', Date.now() + duration * 1000
cooldown.start()
message: (e) -> node: (root) ->
Recaptcha.reload() quote = $ 'a.quotejs:not(:first-child)', root
$('iframe[name=iframe]').src = 'about:blank' $.bind quote, 'click', qr.quote
{data} = e quote: (e) ->
if data # error message e.preventDefault() if e
$('input[name=recaptcha_response_field]', qr.el).value = ''
$.extend $('#error', qr.el), JSON.parse data
qr.autohide.unset()
return
if qr.el if qr.el
if g.REPLY and conf['Persistent QR'] $('#autohide', qr.el).checked = false
qr.refresh() else
else qr.dialog @
qr.close()
if conf['Cooldown']
duration = if qr.sage then 60 else 30
$.setValue g.BOARD+'/cooldown', Date.now() + duration * 1000
cooldown.start()
node: (root) -> id = @textContent
quote = $ 'a.quotejs:not(:first-child)', root text = ">>#{id}\n"
$.bind quote, 'click', qr.cb.quote
quote: (e) -> selection = window.getSelection()
e.preventDefault() if s = selection.toString()
qr.quote @ selectionID = $.x('preceding::input[@type="checkbox"][1]', selection.anchorNode)?.name
if selectionID == id
s = s.replace /\n/g, '\n>'
text += ">#{s}\n"
ta = $ 'textarea', qr.el
ta.focus()
ta.value += text
refresh: ->
auto = $('#auto', qr.el).checked
$('form', qr.el).reset()
$('#auto', qr.el).checked = auto
c = d.cookie
$('input[name=name]', qr.el).value = if m = c.match(/4chan_name=([^;]+)/) then decodeURIComponent m[1] else ''
$('input[name=email]', qr.el).value = if m = c.match(/4chan_email=([^;]+)/) then decodeURIComponent m[1] else ''
$('input[name=pwd]', qr.el).value = if m = c.match(/4chan_pass=([^;]+)/) then decodeURIComponent m[1] else $('input[name=pwd]').value
submit: (e) -> submit: (e) ->
if conf['Auto Watch Reply'] and conf['Thread Watcher'] if conf['Auto Watch Reply'] and conf['Thread Watcher']
@ -1007,7 +1146,7 @@ qr =
watcher.watch null, g.THREAD_ID watcher.watch null, g.THREAD_ID
else else
id = $('input[name=resto]', qr.el).value id = $('input[name=resto]', qr.el).value
op = d.getElementById id op = $.id id
if $('img.favicon', op).src is Favicon.empty if $('img.favicon', op).src is Favicon.empty
watcher.watch op, id watcher.watch op, id
@ -1024,84 +1163,9 @@ qr =
else if isQR else if isQR
if !e then @submit() if !e then @submit()
$('#error', qr.el).textContent = '' $('#error', qr.el).textContent = ''
qr.autohide.set() $('#autohide', qr.el).checked = true if conf['Auto Hide QR']
qr.sage = /sage/i.test $('input[name=email]', @).value qr.sage = /sage/i.test $('input[name=email]', @).value
quote: (link) ->
if qr.el
qr.autohide.unset()
else
qr.dialog link
id = link.textContent
text = ">>#{id}\n"
selection = window.getSelection()
if s = selection.toString()
selectionID = $.x('preceding::input[@type="checkbox"][1]', selection.anchorNode)?.name
if selectionID == id
s = s.replace /\n/g, '\n>'
text += ">#{s}\n"
ta = $ 'textarea', qr.el
ta.focus()
ta.value += text
refresh: ->
$('form', qr.el).reset()
c = d.cookie
$('input[name=name]', qr.el).value = if m = c.match(/4chan_name=([^;]+)/) then decodeURIComponent m[1] else ''
$('input[name=email]', qr.el).value = if m = c.match(/4chan_email=([^;]+)/) then decodeURIComponent m[1] else ''
$('input[name=pwd]', qr.el).value = if m = c.match(/4chan_pass=([^;]+)/) then decodeURIComponent m[1] else $('input[name=pwd]').value
dialog: (link) ->
submitValue = $('#com_submit').value
submitDisabled = if $('#com_submit').disabled then 'disabled' else ''
#FIXME inlined cross-thread quotes
THREAD_ID = g.THREAD_ID or $.x('ancestor::div[@class="thread"]/div', link).id
spoiler = if $('.postarea label') then '<label> [<input type=checkbox name=spoiler>Spoiler Image?]</label>' else ''
challenge = $('input[name=recaptcha_challenge_field]').value
html = "
<div class=move>
<input class=inputtext type=text name=name placeholder=Name form=qr_form>
Quick Reply
<input type=checkbox id=autohide title=autohide>
<a name=close title=close>X</a>
</div>
<form name=post action=http://sys.4chan.org/#{g.BOARD}/post method=POST enctype=multipart/form-data target=iframe id=qr_form>
<input type=hidden name=resto value=#{THREAD_ID}>
<input type=hidden name=recaptcha_challenge_field value=#{challenge}>
<div><input class=inputtext type=text name=email placeholder=E-mail>#{spoiler}</div>
<div><input class=inputtext type=text name=sub placeholder=Subject><input type=submit value=#{submitValue} id=com_submit #{submitDisabled}><label><input type=checkbox id=auto>auto</label></div>
<div><textarea class=inputtext name=com placeholder=Comment></textarea></div>
<div><img src=http://www.google.com/recaptcha/api/image?c=#{challenge}></div>
<div><input class=inputtext type=text name=recaptcha_response_field placeholder=Verification required autocomplete=off></div>
<div><input type=file name=upfile></div>
<div><input class=inputtext type=password name=pwd maxlength=8 placeholder=Password><input type=hidden name=mode value=regist></div>
</form>
<a id=error class=error></a>
"
qr.el = ui.dialog 'qr', top: '0px', left: '0px', html
qr.refresh()
$.bind $('input[name=name]', qr.el), 'mousedown', (e) -> e.stopPropagation()
$.bind $('#autohide', qr.el), 'click', qr.cb.autohide
$.bind $('a[name=close]', qr.el), 'click', qr.close
$.bind $('form', qr.el), 'submit', qr.submit
$.bind $('img', qr.el), 'click', Recaptcha.reload
$.bind $('input[name=recaptcha_response_field]', qr.el), 'keydown', Recaptcha.listener
$.append d.body, qr.el
persist: ->
qr.dialog()
qr.autohide.set()
close: ->
$.rm qr.el
qr.el = null
sys: -> sys: ->
if recaptcha = $ '#recaptcha_response_field' #post reporting if recaptcha = $ '#recaptcha_response_field' #post reporting
$.bind recaptcha, 'keydown', Recaptcha.listener $.bind recaptcha, 'keydown', Recaptcha.listener
@ -1126,23 +1190,26 @@ qr =
if c.nodeType is 8 #comment node if c.nodeType is 8 #comment node
[_, thread, id] = c.textContent.match(/thread:(\d+),no:(\d+)/) [_, thread, id] = c.textContent.match(/thread:(\d+),no:(\d+)/)
noko = /auto_noko/.test location.search {search} = location
if thread is '0' cooldown = /cooldown/.test search
if /auto_watch/.test location.search noko = /noko/ .test search
window.location = "http://boards.4chan.org/#{g.BOARD}/res/#{id}#watch" sage = /sage/ .test search
else if noko watch = /watch/ .test search
window.location = "http://boards.4chan.org/#{g.BOARD}/res/#{id}"
else if /cooldown/.test location.search url = "http://boards.4chan.org/#{g.BOARD}"
duration = Date.now() + 30000
duration += 30000 if /sage/.test location.search if watch and thread is '0'
if noko url += "/res/#{id}?watch"
window.location = "http://boards.4chan.org/#{g.BOARD}/res/#{thread}?cooldown=#{duration}##{id}"
else
window.location = "http://boards.4chan.org/#{g.BOARD}?cooldown=#{duration}"
else if noko else if noko
window.location = "http://boards.4chan.org/#{g.BOARD}/res/#{thread}##{id}" url += '/res/'
else url += if thread is '0' then id else thread
window.location = "http://boards.4chan.org/#{g.BOARD}" if cooldown
duration = Date.now() + (if sage then 60 else 30) * 1000
url += '?cooldown=' + duration
if noko
url += '#' + id
window.location = url
threading = threading =
init: -> init: ->
@ -1254,6 +1321,9 @@ threadHiding =
updater = updater =
init: -> init: ->
if conf['Scrolling']
$.bind window, 'focus', (-> updater.focus = true)
$.bind window, 'blur', (-> updater.focus = false)
html = "<div class=move><span id=count></span> <span id=timer>-#{conf['Interval']}</span></div>" html = "<div class=move><span id=count></span> <span id=timer>-#{conf['Interval']}</span></div>"
{checkbox} = config.updater {checkbox} = config.updater
for name of checkbox for name of checkbox
@ -1329,7 +1399,7 @@ updater =
while (reply = replies.pop()) and (reply.id > id) while (reply = replies.pop()) and (reply.id > id)
arr.push reply.parentNode.parentNode.parentNode #table arr.push reply.parentNode.parentNode.parentNode #table
scroll = conf['Scrolling'] && arr.length && (d.body.scrollHeight - d.body.clientHeight - window.scrollY < 20) scroll = conf['Scrolling'] && updater.focus && arr.length && (d.body.scrollHeight - d.body.clientHeight - window.scrollY < 20)
updater.timer.textContent = '-' + conf['Interval'] updater.timer.textContent = '-' + conf['Interval']
if conf['Verbose'] if conf['Verbose']
@ -1499,9 +1569,10 @@ Time =
$.replace s, time $.replace s, time
foo: -> foo: ->
code = conf['time'].replace /%([A-Za-z])/g, (s, c) -> code = conf['time'].replace /%([A-Za-z])/g, (s, c) ->
switch c if c of Time.formatters
when 'a', 'A', 'b', 'B', 'd', 'H', 'I', 'm', 'M', 'p', 'P', 'y' then "' + Time.#{c}() + '" "' + Time.formatters.#{c}() + '"
else s else
s
Time.funk = Function 'Time', "return '#{code}'" Time.funk = Function 'Time', "return '#{code}'"
day: [ day: [
'Sunday' 'Sunday'
@ -1527,18 +1598,22 @@ Time =
'December' 'December'
] ]
zeroPad: (n) -> if n < 10 then '0' + n else n zeroPad: (n) -> if n < 10 then '0' + n else n
a: -> @day[@date.getDay()][...3] formatters:
A: -> @day[@date.getDay()] a: -> Time.day[Time.date.getDay()][...3]
b: -> @month[@date.getMonth()][...3] A: -> Time.day[Time.date.getDay()]
B: -> @month[@date.getMonth()] b: -> Time.month[Time.date.getMonth()][...3]
d: -> @zeroPad @date.getDate() B: -> Time.month[Time.date.getMonth()]
H: -> @zeroPad @date.getHours() d: -> Time.zeroPad Time.date.getDate()
I: -> @zeroPad @date.getHours() % 12 or 12 e: -> Time.date.getDate()
m: -> @zeroPad @date.getMonth() + 1 H: -> Time.zeroPad Time.date.getHours()
M: -> @zeroPad @date.getMinutes() I: -> Time.zeroPad Time.date.getHours() % 12 or 12
p: -> if @date.getHours() < 12 then 'AM' else 'PM' k: -> Time.date.getHours()
P: -> if @date.getHours() < 12 then 'am' else 'pm' l: -> Time.date.getHours() % 12 or 12
y: -> @date.getFullYear() - 2000 m: -> Time.zeroPad Time.date.getMonth() + 1
M: -> Time.zeroPad Time.date.getMinutes()
p: -> if Time.date.getHours() < 12 then 'AM' else 'PM'
P: -> if Time.date.getHours() < 12 then 'am' else 'pm'
y: -> Time.date.getFullYear() - 2000
titlePost = titlePost =
init: -> init: ->
@ -1558,7 +1633,7 @@ quoteBacklink =
#duplicate quotes get overwritten #duplicate quotes get overwritten
quotes[qid] = quote quotes[qid] = quote
for qid of quotes for qid of quotes
continue unless el = d.getElementById qid continue unless el = $.id qid
#don't backlink the op #don't backlink the op
continue if !conf['OP Backlinks'] and el.className is 'op' continue if !conf['OP Backlinks'] and el.className is 'op'
link = $.el 'a', link = $.el 'a',
@ -1585,30 +1660,19 @@ quoteInline =
quote.removeAttribute 'onclick' quote.removeAttribute 'onclick'
$.bind quote, 'click', quoteInline.toggle $.bind quote, 'click', quoteInline.toggle
toggle: (e) -> toggle: (e) ->
return if e.shiftKey or e.altKey or e.ctrlKey or e.button isnt 0
e.preventDefault() e.preventDefault()
###
https://bugzilla.mozilla.org/show_bug.cgi?id=674955
`mouseout` does not fire when element removed
RESOLVED INVALID
inline a post, then hover over an inlined quote / image, then remove
the inlined post by clicking `enter` on the still-focused link - the
mouseout event doesn't fire, and the quote preview / image hover remains.
we can prevent this sequence by `blur`-ing the clicked links. chrome
doesn't focus clicked links anyway.
###
@blur()
id = @hash[1..] id = @hash[1..]
if table = $ "#i#{id}", $.x 'ancestor::td[1]', @ if table = $ "#i#{id}", $.x 'ancestor::td[1]', @
$.rm table $.rm table
$.removeClass @, 'inlined' $.removeClass @, 'inlined'
for inlined in $$ 'input', table for inlined in $$ 'input', table
if hidden = d.getElementById inlined.name if hidden = $.id inlined.name
$.show $.x 'ancestor::table[1]', hidden $.show $.x 'ancestor::table[1]', hidden
return return
root = if @parentNode.nodeName is 'FONT' then @parentNode else if @nextSibling then @nextSibling else @ root = if @parentNode.nodeName is 'FONT' then @parentNode else if @nextSibling then @nextSibling else @
if el = d.getElementById id if el = $.id id
inline = quoteInline.table id, el.innerHTML inline = quoteInline.table id, el.innerHTML
if @className is 'backlink' if @className is 'backlink'
return if $("a.backlink[href='##{id}']", el) return if $("a.backlink[href='##{id}']", el)
@ -1670,7 +1734,7 @@ quotePreview =
$.append d.body, qp $.append d.body, qp
id = @hash[1..] id = @hash[1..]
if el = d.getElementById id if el = $.id id
qp.innerHTML = el.innerHTML qp.innerHTML = el.innerHTML
$.addClass el, 'qphl' if conf['Quote Highlighting'] $.addClass el, 'qphl' if conf['Quote Highlighting']
if /backlink/.test @className if /backlink/.test @className
@ -1683,7 +1747,7 @@ quotePreview =
threadID = @pathname.split('/').pop() or $.x('ancestor::div[@class="thread"]/div', @).id threadID = @pathname.split('/').pop() or $.x('ancestor::div[@class="thread"]/div', @).id
$.cache @pathname, (-> quotePreview.parse @, id, threadID) $.cache @pathname, (-> quotePreview.parse @, id, threadID)
mouseout: -> mouseout: ->
$.removeClass el, 'qphl' if el = d.getElementById @hash[1..] $.removeClass el, 'qphl' if el = $.id @hash[1..]
ui.hoverend() ui.hoverend()
parse: (req, id, threadID) -> parse: (req, id, threadID) ->
return unless (qp = ui.el) and (qp.innerHTML is "Loading #{id}...") return unless (qp = ui.el) and (qp.innerHTML is "Loading #{id}...")
@ -1767,6 +1831,7 @@ unread =
Favicon.update() Favicon.update()
scroll: (e) -> scroll: (e) ->
updater.focus = true
height = d.body.clientHeight height = d.body.clientHeight
for reply, i in unread.replies for reply, i in unread.replies
{bottom} = reply.getBoundingClientRect() {bottom} = reply.getBoundingClientRect()
@ -1826,21 +1891,12 @@ Recaptcha =
#hack to tab from comment straight to recaptcha #hack to tab from comment straight to recaptcha
for el in $$ '#recaptcha_table a' for el in $$ '#recaptcha_table a'
el.tabIndex = 1 el.tabIndex = 1
$.bind $('#recaptcha_challenge_field_holder'), 'DOMNodeInserted', Recaptcha.reloaded
$.bind $('#recaptcha_response_field'), 'keydown', Recaptcha.listener $.bind $('#recaptcha_response_field'), 'keydown', Recaptcha.listener
listener: (e) -> listener: (e) ->
if e.keyCode is 8 and @value is '' # backspace to reload if e.keyCode is 8 and @value is '' # backspace to reload
Recaptcha.reload() Recaptcha.reload()
if e.keyCode is 13 and cooldown.duration # press enter to enable auto-post if cooldown is still running
$('#auto', qr.el).checked = true
qr.autohide.set()
reload: -> reload: ->
window.location = 'javascript:Recaptcha.reload()' window.location = 'javascript:Recaptcha.reload()'
reloaded: (e) ->
return unless qr.el
{target} = e
$('img', qr.el).src = "http://www.google.com/recaptcha/api/image?c=" + target.value
$('input[name=recaptcha_challenge_field]', qr.el).value = target.value
nodeInserted = (e) -> nodeInserted = (e) ->
{target} = e {target} = e
@ -2090,17 +2146,23 @@ main =
$.addStyle main.css $.addStyle main.css
if (form = $ 'form[name=post]') and canPost = !!$ '#recaptcha_response_field' #recaptcha may be blocked, eg by noscript
if (form = $ 'form[name=post]') and (canPost = !!$ '#recaptcha_response_field')
Recaptcha.init() Recaptcha.init()
$.bind form, 'submit', qr.submit $.bind form, 'submit', qr.submit
#major features #major features
threading.init() threading.init()
if conf['Auto Noko'] # scroll to bottom if post isn't found
$('.postarea form').action += '?auto_noko' # thumbnail generation takes time
if g.REPLY and (id = location.hash[1..]) and /\d/.test(id[0]) and !$.id(id)
scrollTo 0, d.body.scrollHeight
if conf['Cooldown'] if conf['Auto Noko'] and canPost
form.action += '?noko'
if conf['Cooldown'] and canPost
cooldown.init() cooldown.init()
if conf['Image Expansion'] if conf['Image Expansion']
@ -2127,7 +2189,7 @@ main =
if conf['Reply Hiding'] if conf['Reply Hiding']
replyHiding.init() replyHiding.init()
if canPost and conf['Quick Reply'] if conf['Quick Reply'] and canPost
qr.init() qr.init()
if conf['Report Button'] if conf['Report Button']
@ -2158,8 +2220,10 @@ main =
if conf['Image Preloading'] if conf['Image Preloading']
imgPreloading.init() imgPreloading.init()
if conf['Quick Reply'] and conf['Persistent QR'] if conf['Quick Reply'] and conf['Persistent QR'] and canPost
qr.persist() qr.dialog()
if conf['Auto Hide QR']
$('#autohide', qr.el).checked = true
if conf['Post in Title'] if conf['Post in Title']
titlePost.init() titlePost.init()
@ -2174,7 +2238,7 @@ main =
nav.init() nav.init()
if conf['Auto Watch'] and conf['Thread Watcher'] and if conf['Auto Watch'] and conf['Thread Watcher'] and
location.hash is '#watch' and $('img.favicon').src is Favicon.empty /watch/.test(location.search) and $('img.favicon').src is Favicon.empty
watcher.watch null, g.THREAD_ID watcher.watch null, g.THREAD_ID
else #not reply else #not reply
@ -2191,7 +2255,7 @@ main =
expandComment.init() expandComment.init()
if conf['Auto Watch'] if conf['Auto Watch']
$('.postarea form').action += '?auto_watch' $('.postarea form').action += '?watch'
for op in $$ 'div.op' for op in $$ 'div.op'
for callback in g.callbacks for callback in g.callbacks
@ -2303,6 +2367,9 @@ main =
#qr { #qr {
position: fixed; position: fixed;
max-height: 100%;
overflow-x: hidden;
overflow-y: auto;
} }
#qr > div.move { #qr > div.move {
text-align: right; text-align: right;
@ -2320,7 +2387,10 @@ main =
width: 100%; width: 100%;
height: 120px; height: 120px;
} }
#qr.auto:not(:hover) > form { #qr #close, #qr #autohide {
float: right;
}
#qr:not(:hover) > #autohide:checked ~ form {
height: 0; height: 0;
overflow: hidden; overflow: hidden;
} }
@ -2389,6 +2459,10 @@ main =
[hidden] { [hidden] {
display: none; display: none;
} }
#files > input {
display: block;
}
' '
main.init() main.init()