Merge branch 'v3'

Conflicts:
	.gitignore
	CHANGELOG.md
	Gruntfile.coffee
	LICENSE
	builds/crx/manifest.json
	builds/crx/script.js
	latest.js
	package.json
	src/General/Config.coffee
	src/General/Globals.coffee
	src/General/Header.coffee
	src/General/Main.coffee
	src/General/UI.coffee
	src/General/css/style.css
	src/General/meta/manifest.json
	src/Posting/QuickReply.coffee
This commit is contained in:
Zixaphir 2013-07-21 09:01:27 -07:00
commit 16b35dffce
42 changed files with 2221 additions and 1241 deletions

7
.gitignore vendored
View File

@ -2,9 +2,14 @@ node_modules/
*~ *~
*.db *.db
tmp-crx/ tmp-crx/
tmp-userjs/
tmp-userscript/ tmp-userscript/
<<<<<<< HEAD
builds/4chan-X.zip builds/4chan-X.zip
Gruntfile.js Gruntfile.js
builds/4chan-* builds/4chan-*
Gruntfile.js Gruntfile.js
=======
builds/4chan-X-Chrome.zip
builds/4chan-X-Opera.nex
Gruntfile.js
>>>>>>> v3

View File

@ -1,7 +1,10 @@
**MayhemYDG**: **MayhemYDG**:
- Remove /s4s/ from warosu archive - Remove /s4s/ from warosu archive
- Fix CAPTCHA duplication on the report page - Fix CAPTCHA duplication on the report page
- Small bug fixes - Fix impossibility to create new threads when in dead threads.
- Drop Opera <15 support.
- Fix flag filtering on /sp/ and /int/.
- Minor fixes.
**seaweedchan**: **seaweedchan**:
- Add `.active` class to `.menu-button` when clicked (and remove on menu close) - Add `.active` class to `.menu-button` when clicked (and remove on menu close)
@ -9,6 +12,10 @@
- Revert Mayhem's updater changes which caused silly issues - Revert Mayhem's updater changes which caused silly issues
- Rename `Indicate Spoilers` to `Reveal Spoilers` - Rename `Indicate Spoilers` to `Reveal Spoilers`
- If `Reveal Spoilers` is enabled but `Remove Spoilers` is not, act as if the spoiler is hovered - If `Reveal Spoilers` is enabled but `Remove Spoilers` is not, act as if the spoiler is hovered
- Add a new option to hide "4chan X has been updated to ____" notifications for those having issues with them.
- Update archives
- Add `.active` class to `.menu-button` when clicked (and remove on menu close)
- Move /v/ and /vg/ back to Foolz archive
**Tracerneo**: **Tracerneo**:
- Add ID styling for IDs with black text - Add ID styling for IDs with black text

View File

@ -183,7 +183,6 @@ module.exports = (grunt) ->
grunt.registerTask 'release', [ grunt.registerTask 'release', [
'default' 'default'
'compress:crx' 'compress:crx'
'copy:opera'
'shell:commit' 'shell:commit'
'shell:push' 'shell:push'
] ]

View File

@ -1,5 +1,5 @@
/* /*
* appchan x - Version 2.1.3 - 2013-07-07 * appchan x - Version 2.1.3 - 2013-07-21
* *
* Licensed under the MIT license. * Licensed under the MIT license.
* https://github.com/zixaphir/appchan-x/blob/master/LICENSE * https://github.com/zixaphir/appchan-x/blob/master/LICENSE

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
// ==UserScript== // ==UserScript==
// @name 4chan X // @name 4chan X
// @version 1.2.17 // @version 1.2.19
// @namespace 4chan-X // @namespace 4chan-X
// @description Cross-browser userscript for maximum lurking on 4chan. // @description Cross-browser userscript for maximum lurking on 4chan.
// @license MIT; https://github.com/seaweedchan/4chan-x/blob/master/LICENSE // @license MIT; https://github.com/seaweedchan/4chan-x/blob/master/LICENSE

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -16,6 +16,7 @@
}], }],
"homepage_url": "http://zixaphir.github.com/appchan-x/", "homepage_url": "http://zixaphir.github.com/appchan-x/",
"minimum_chrome_version": "24", "minimum_chrome_version": "24",
"minimum_opera_version": "15",
"permissions": [ "permissions": [
"storage" "storage"
] ]

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,3 @@
<div class="move" title="Post count / File count / Page count">
<span id="post-count">...</span> / <span id="file-count">...</span> / <span id="page-count">...</span>
</div>

38
html/Posting/QR.html Normal file
View File

@ -0,0 +1,38 @@
<div>
<input type="checkbox" id="autohide" title="Auto-hide">
<select data-name="thread" title="Create a new thread / Reply">
<option value="new">New thread</option>
</select>
<span class="move"></span>
<a href="javascript:;" class="close" title="Close">×</a>
</div>
<form>
<div class="persona">
<input type="button" id="dump-button" title="Dump list" value="+">
<input data-name="name" list="list-name" placeholder="Name" class="field" size="1">
<input data-name="email" list="list-email" placeholder="E-mail" class="field" size="1">
<input data-name="sub" list="list-sub" placeholder="Subject" class="field" size="1">
</div>
<div id="dump-list-container">
<div id="dump-list"></div>
<a id="add-post" href="javascript:;" title="Add a post">+</a>
</div>
<div class="textarea">
<textarea data-name="com" placeholder="Comment" class="field"></textarea>
<span id="char-count"></span>
</div>
<div id="file-n-submit">
<input type="submit">
<input type="button" id="qr-file-button" value="Choose files">
<span id="qr-filename-container">
<span id="qr-no-file">No selected file</span>
<span id="qr-filename"></span>
</span>
<a id="qr-filerm" href="javascript:;" title="Remove file">×</a>
<input type="checkbox" id="qr-file-spoiler" title="Spoiler image">
</div>
<input type="file" multiple hidden>
</form>
<datalist id="list-name"></datalist>
<datalist id="list-email"></datalist>
<datalist id="list-sub"></datalist>

View File

@ -20,15 +20,15 @@
}, },
"devDependencies": { "devDependencies": {
"grunt": "~0.4.1", "grunt": "~0.4.1",
"grunt-bump": "~0.0.2", "grunt-bump": "~0.0.11",
"grunt-concurrent": "~0.2.0", "grunt-concurrent": "~0.3.0",
"grunt-contrib-clean": "~0.4.1", "grunt-contrib-clean": "~0.5.0",
"grunt-contrib-coffee": "~0.7.0", "grunt-contrib-coffee": "~0.7.0",
"grunt-contrib-compress": "~0.5.1", "grunt-contrib-compress": "~0.5.2",
"grunt-contrib-concat": "~0.3.0", "grunt-contrib-concat": "~0.3.0",
"grunt-contrib-copy": "~0.4.1", "grunt-contrib-copy": "~0.4.1",
"grunt-contrib-watch": "~0.4.4", "grunt-contrib-watch": "~0.5.0",
"grunt-shell": "~0.2.2" "grunt-shell": "~0.3.1"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -21,7 +21,6 @@ Redirect =
Redirect.post[boardID] = archive Redirect.post[boardID] = archive
unless boardID of Redirect.file or !archive.files.contains boardID unless boardID of Redirect.file or !archive.files.contains boardID
Redirect.file[boardID] = archive Redirect.file[boardID] = archive
return
archives: archives:
'Foolz': 'Foolz':
@ -29,7 +28,7 @@ Redirect =
'http': false 'http': false
'https': true 'https': true
'software': 'foolfuuka' 'software': 'foolfuuka'
'boards': ['a', 'co', 'gd', 'jp', 'm', 'q', 'sp', 'tg', 'tv', 'v', 'vg', 'vp', 'vr', 'wsg'] 'boards': ['a', 'co', 'gd', 'jp', 'm', 'q', 'sp', 'tg', 'tv', 'vg', 'vp', 'vr', 'wsg']
'files': ['a', 'gd', 'jp', 'm', 'q', 'tg', 'vg', 'vp', 'vr', 'wsg'] 'files': ['a', 'gd', 'jp', 'm', 'q', 'tg', 'vg', 'vp', 'vr', 'wsg']
'NSFW Foolz': 'NSFW Foolz':
@ -63,25 +62,17 @@ Redirect =
'boards': ['c', 'w', 'wg'] 'boards': ['c', 'w', 'wg']
'files': ['c', 'w', 'wg'] 'files': ['c', 'w', 'wg']
'Love is Over':
'domain': 'loveisover.me'
'http': true
'https': true
'software': 'foolfuuka'
'boards': ['d', 'h', 'v']
'files': ['d', 'h', 'v']
'Foolz a Shit': 'Foolz a Shit':
'domain': 'archive.foolzashit.com' 'domain': 'archive.foolzashit.com'
'http': true 'http': true
'https': true 'https': true
'software': 'foolfuuka' 'software': 'foolfuuka'
'boards': ['adv', 'asp', 'cm', 'e', 'i', 'lgbt', 'n', 'o', 'p', 'pol', 's', 's4s', 't', 'trv', 'y'] 'boards': ['adv', 'asp', 'cm', 'i', 'lgbt', 'n', 'o', 'p', 's4s', 't', 'trv']
'files': ['adv', 'asp', 'cm', 'e', 'i', 'lgbt', 'n', 'o', 'p', 's', 's4s', 't', 'trv', 'y'] 'files': ['adv', 'asp', 'cm', 'i', 'lgbt', 'n', 'o', 'p', 's4s', 't', 'trv']
'Install Gentoo': 'Install Gentoo':
'domain': 'archive.installgentoo.net' 'domain': 'archive.installgentoo.net'
'http': true 'http': false
'https': true 'https': true
'software': 'fuuka' 'software': 'fuuka'
'boards': ['diy', 'g', 'sci'] 'boards': ['diy', 'g', 'sci']
@ -110,6 +101,14 @@ Redirect =
'boards': ['3', 'cgl', 'ck', 'fa', 'ic', 'jp', 'lit', 'q', 'tg', 'vr'] 'boards': ['3', 'cgl', 'ck', 'fa', 'ic', 'jp', 'lit', 'q', 'tg', 'vr']
'files': ['3', 'cgl', 'ck', 'fa', 'ic', 'jp', 'lit', 'q', 'vr'] 'files': ['3', 'cgl', 'ck', 'fa', 'ic', 'jp', 'lit', 'q', 'vr']
'worldathleticproject':
'domain': 'fuuka.worldathleticproject.org'
'http': true
'https': true
'software': 'foolfuuka'
'boards': ['e', 'h', 'p', 's', 'u']
'files': ['e', 'h', 'p', 's', 'u']
to: (dest, data) -> to: (dest, data) ->
archive = (if dest is 'search' then Redirect.thread else Redirect[dest])[data.boardID] archive = (if dest is 'search' then Redirect.thread else Redirect[dest])[data.boardID]
return '' unless archive return '' unless archive

View File

@ -209,7 +209,7 @@ Filter =
el = $.el 'a', el = $.el 'a',
href: 'javascript:;' href: 'javascript:;'
textContent: text textContent: text
el.setAttribute 'data-type', type el.dataset.type = type
$.on el, 'click', Filter.menu.makeFilter $.on el, 'click', Filter.menu.makeFilter
return { return {

View File

@ -113,7 +113,7 @@ ThreadHiding =
className: "#{type}-thread-button" className: "#{type}-thread-button"
innerHTML: "<span class=brackets-wrap>&nbsp;#{if type is 'hide' then '-' else '+'}&nbsp;</span>" innerHTML: "<span class=brackets-wrap>&nbsp;#{if type is 'hide' then '-' else '+'}&nbsp;</span>"
href: 'javascript:;' href: 'javascript:;'
a.setAttribute 'data-fullid', thread.fullID a.dataset.fullID = thread.fullID
$.on a, 'click', ThreadHiding.toggle $.on a, 'click', ThreadHiding.toggle
a a
@ -134,7 +134,7 @@ ThreadHiding =
toggle: (thread) -> toggle: (thread) ->
unless thread instanceof Thread unless thread instanceof Thread
thread = g.threads[@dataset.fullid] thread = g.threads[@dataset.fullID]
if thread.isHidden if thread.isHidden
ThreadHiding.show thread ThreadHiding.show thread
else else

View File

@ -108,12 +108,12 @@ Build =
capcodeStart = '' capcodeStart = ''
capcode = '' capcode = ''
flag = flag = unless flagCode
if flagCode ''
" <img src='#{staticPath}country/#{if boardID is 'pol' then 'troll/' else ''}" + else if boardID is 'pol'
flagCode.toLowerCase() + ".gif' alt=#{flagCode} title='#{flagName}' class=countryFlag>" " <img src='#{staticPath}country/troll/#{flagCode.toLowerCase()}.gif' alt=#{flagCode} title='#{flagName}' class=countryFlag>"
else else
'' " <span title='#{flagName}' class='flag flag-#{flagCode.toLowerCase()}'></span>"
if file?.isDeleted if file?.isDeleted
fileHTML = if isOP fileHTML = if isOP

View File

@ -19,7 +19,7 @@ Get =
if index then post.clones[index] else post if index then post.clones[index] else post
postFromNode: (root) -> postFromNode: (root) ->
Get.postFromRoot $.x 'ancestor::div[contains(@class,"postContainer")][1]', root Get.postFromRoot $.x 'ancestor::div[contains(@class,"postContainer")][1]', root
contextFromLink: (quotelink) -> contextFromNode: (quotelink) ->
Get.postFromRoot $.x 'ancestor::div[parent::div[@class="thread"]][1]', quotelink Get.postFromRoot $.x 'ancestor::div[parent::div[@class="thread"]][1]', quotelink
postDataFromLink: (link) -> postDataFromLink: (link) ->
if link.hostname is 'boards.4chan.org' if link.hostname is 'boards.4chan.org'
@ -28,9 +28,8 @@ Get =
threadID = path[3] threadID = path[3]
postID = link.hash[2..] postID = link.hash[2..]
else # resurrected quote else # resurrected quote
boardID = link.dataset.boardid {boardID, threadID, postID} = link.dataset
threadID = link.dataset.threadid or 0 threadID or= 0
postID = link.dataset.postid
return { return {
boardID: boardID boardID: boardID
threadID: +threadID threadID: +threadID
@ -185,7 +184,7 @@ Get =
# quotes # quotes
.replace /((&gt;){2}(&gt;\/[a-z\d]+\/)?\d+)/g, '<span class=deadlink>$1</span>' .replace /((&gt;){2}(&gt;\/[a-z\d]+\/)?\d+)/g, '<span class=deadlink>$1</span>'
threadID = data.thread_num threadID = +data.thread_num
o = o =
# id # id
postID: "#{postID}" postID: "#{postID}"

View File

@ -1,6 +1,7 @@
editTheme = {} # Currently editted theme.
editMascot = {} # Which mascot we're editting. editTheme = {}
userNavigation = {} # ... editMascot = {}
userNavigation = {}
Conf = {} Conf = {}
c = console c = console
d = document d = document

View File

@ -55,7 +55,7 @@ Header =
return unless Main.isThisPageLegit() return unless Main.isThisPageLegit()
# Wait for #boardNavMobile instead of #boardNavDesktop, # Wait for #boardNavMobile instead of #boardNavDesktop,
# it might be incomplete otherwise. # it might be incomplete otherwise.
$.asap (-> $.id('boardNavMobile') or d.readyState in ['interactive', 'complete']), @setBoardList $.asap (-> $.id('boardNavMobile') or d.readyState isnt 'loading'), Header.setBoardList
$.prepend d.body, @bar $.prepend d.body, @bar
$.add d.body, Header.hover $.add d.body, Header.hover
@setBarPosition Conf['Bottom Header'] @setBarPosition Conf['Bottom Header']
@ -106,7 +106,7 @@ Header =
list = $ '#custom-board-list', Header.bar list = $ '#custom-board-list', Header.bar
$.rmAll list $.rmAll list
return unless text return unless text
as = $$('#full-board-list a', Header.bar) as = $$ '#full-board-list a[title]', Header.bar
nodes = text.match(/[\w@]+((-(all|title|replace|full|index|catalog|url:"[^"]+[^"]"|text:"[^"]+")|\,"[^"]+[^"]"))*|[^\w@]+/g).map (t) -> nodes = text.match(/[\w@]+((-(all|title|replace|full|index|catalog|url:"[^"]+[^"]"|text:"[^"]+")|\,"[^"]+[^"]"))*|[^\w@]+/g).map (t) ->
if /^[^\w@]/.test t if /^[^\w@]/.test t
return $.tn t return $.tn t
@ -141,7 +141,7 @@ Header =
a.textContent a.textContent
if m = t.match /-(index|catalog)/ if m = t.match /-(index|catalog)/
a.setAttribute 'data-only', m[1] a.dataset.only = m[1]
a.href = "//boards.4chan.org/#{board}/" a.href = "//boards.4chan.org/#{board}/"
if m[1] is 'catalog' if m[1] is 'catalog'
a.href += 'catalog' a.href += 'catalog'

View File

@ -191,20 +191,18 @@ Main =
threads = [] threads = []
posts = [] posts = []
for boardChild in board.children for threadRoot in $$ '.board > .thread', board
continue unless $.hasClass boardChild, 'thread' thread = new Thread +threadRoot.id[1..], g.BOARD
thread = new Thread boardChild.id[1..], g.BOARD
threads.push thread threads.push thread
for threadChild in boardChild.children for postRoot in $$ '.thread > .postContainer', threadRoot
continue unless $.hasClass threadChild, 'postContainer'
try try
posts.push new Post threadChild, thread, g.BOARD posts.push new Post postRoot, thread, g.BOARD
catch err catch err
# Skip posts that we failed to parse. # Skip posts that we failed to parse.
unless errors unless errors
errors = [] errors = []
errors.push errors.push
message: "Parsing of Post No.#{threadChild.id.match(/\d+/)} failed. Post will be skipped." message: "Parsing of Post No.#{postRoot.id.match(/\d+/)} failed. Post will be skipped."
error: err error: err
Main.handleErrors errors if errors Main.handleErrors errors if errors
@ -372,7 +370,7 @@ Main =
unless 'thisPageIsLegit' of Main unless 'thisPageIsLegit' of Main
Main.thisPageIsLegit = location.hostname is 'boards.4chan.org' and Main.thisPageIsLegit = location.hostname is 'boards.4chan.org' and
!$('link[href*="favicon-status.ico"]', d.head) and !$('link[href*="favicon-status.ico"]', d.head) and
d.title not in ['4chan - Temporarily Offline', '4chan - Error'] d.title not in ['4chan - Temporarily Offline', '4chan - Error', '504 Gateway Time-out']
Main.thisPageIsLegit Main.thisPageIsLegit
Main.init() Main.init()

View File

@ -24,7 +24,8 @@ Settings =
changelog = '<%= meta.repo %>blob/<%= meta.mainBranch %>/CHANGELOG.md' changelog = '<%= meta.repo %>blob/<%= meta.mainBranch %>/CHANGELOG.md'
el = $.el 'span', el = $.el 'span',
innerHTML: "<%= meta.name %> has been updated to <a href='#{changelog}' target=_blank>version #{g.VERSION}</a>." innerHTML: "<%= meta.name %> has been updated to <a href='#{changelog}' target=_blank>version #{g.VERSION}</a>."
new Notification 'info', el, 30 if Conf['Show Updated Notifications']
new Notification 'info', el, 30
else else
$.on d, '4chanXInitFinished', Settings.open $.on d, '4chanXInitFinished', Settings.open
$.set $.set
@ -427,7 +428,6 @@ Settings =
usercss: -> usercss: ->
CustomCSS.update() CustomCSS.update()
keybinds: (section) -> keybinds: (section) ->
section.innerHTML = """ section.innerHTML = """
<%= grunt.file.read('src/General/html/Settings/Keybinds.html').replace(/>\s+</g, '><').trim() %> <%= grunt.file.read('src/General/html/Settings/Keybinds.html').replace(/>\s+</g, '><').trim() %>

View File

@ -303,13 +303,13 @@ UI = do ->
hoverstart = ({root, el, latestEvent, endEvents, asapTest, cb, close}) -> hoverstart = ({root, el, latestEvent, endEvents, asapTest, cb, close}) ->
o = { o = {
root: root root
el: el el
style: el.style style: el.style
cb: cb cb
close: close close: close
endEvents: endEvents endEvents
latestEvent: latestEvent latestEvent
clientHeight: doc.clientHeight clientHeight: doc.clientHeight
clientWidth: doc.clientWidth clientWidth: doc.clientWidth
} }
@ -325,6 +325,11 @@ UI = do ->
if $.x 'ancestor::div[contains(@class,"inline")][1]', root if $.x 'ancestor::div[contains(@class,"inline")][1]', root
$.on d, 'keydown', o.hoverend $.on d, 'keydown', o.hoverend
$.on root, 'mousemove', o.hover $.on root, 'mousemove', o.hover
<% if (type === 'userscript') { %>
# Workaround for https://github.com/MayhemYDG/4chan-x/issues/377
o.workaround = (e) -> o.hoverend() unless root.contains e.target
$.on doc, 'mousemove', o.workaround
<% } %>
hover = (e) -> hover = (e) ->
@latestEvent = e @latestEvent = e
@ -355,6 +360,10 @@ UI = do ->
$.off @root, @endEvents, @hoverend $.off @root, @endEvents, @hoverend
$.off d, 'keydown', @hoverend $.off d, 'keydown', @hoverend
$.off @root, 'mousemove', @hover $.off @root, 'mousemove', @hover
<% if (type === 'userscript') { %>
# Workaround for https://github.com/MayhemYDG/4chan-x/issues/377
$.off doc, 'mousemove', @workaround
<% } %>
@cb.call @ if @cb @cb.call @ if @cb

1057
src/General/css/style.css Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
<div class=warning #{if Conf['Filter'] then 'hidden' else ''}><code>Filter</code> is disabled.</div> <div class=warning #{if Conf['Filter'] then 'hidden' else ''}><code>Filter</code> is disabled.</div>
<p> <p>
Use <a href=https://developer.mozilla.org/en/JavaScript/Guide/Regular_Expressions>regular expressions</a>, one per line.<br> Use <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions">regular expressions</a>, one per line.<br>
Lines starting with a <code>#</code> will be ignored.<br> Lines starting with a <code>#</code> will be ignored.<br>
For example, <code>/weeaboo/i</code> will filter posts containing the string `<code>weeaboo</code>`, case-insensitive.<br> For example, <code>/weeaboo/i</code> will filter posts containing the string `<code>weeaboo</code>`, case-insensitive.<br>
MD5 filtering uses exact string matching, not regular expressions. MD5 filtering uses exact string matching, not regular expressions.

View File

@ -49,7 +49,7 @@ $.id = (id) ->
d.getElementById id d.getElementById id
$.ready = (fc) -> $.ready = (fc) ->
if d.readyState in ['interactive', 'complete'] unless d.readyState is 'loading'
$.queueTask fc $.queueTask fc
return return
cb = -> cb = ->
@ -221,9 +221,7 @@ $.event = (event, detail, root=d) ->
$.open = (URL) -> $.open = (URL) ->
<% if (type === 'userscript') { %> <% if (type === 'userscript') { %>
# XXX fix GM opening file://// for protocol-less URLs. $.open = (URL) -> GM_openInTab URL
# https://github.com/greasemonkey/greasemonkey/issues/1719
GM_openInTab ($.el 'a', href: URL).href
<% } else { %> <% } else { %>
window.open URL, '_blank' window.open URL, '_blank'
<% } %> <% } %>
@ -298,29 +296,21 @@ $.minmax = (value, min, max) ->
value value
) )
$.syncing = {} $.item = (key, val) ->
item = {}
item[key] = val
item
$.sync = do -> $.syncing = {}
<% if (type === 'crx') { %> <% if (type === 'crx') { %>
$.sync = do ->
chrome.storage.onChanged.addListener (changes) -> chrome.storage.onChanged.addListener (changes) ->
for key of changes for key of changes
if cb = $.syncing[key] if cb = $.syncing[key]
cb changes[key].newValue cb changes[key].newValue
return return
(key, cb) -> $.syncing[key] = cb (key, cb) -> $.syncing[key] = cb
<% } else { %>
window.addEventListener 'storage', (e) ->
if cb = $.syncing[e.key]
cb JSON.parse e.newValue
, false
(key, cb) -> $.syncing[g.NAMESPACE + key] = cb
<% } %>
$.item = (key, val) ->
item = {}
item[key] = val
item
<% if (type === 'crx') { %>
$.localKeys = [ $.localKeys = [
# filters # filters
'name', 'name',
@ -372,6 +362,7 @@ $.get = (key, val, cb) ->
if syncItems if syncItems
count++ count++
chrome.storage.sync.get syncItems, done chrome.storage.sync.get syncItems, done
$.set = do -> $.set = do ->
items = {} items = {}
localItems = {} localItems = {}
@ -397,8 +388,13 @@ $.set = do ->
set() set()
<% } else { %> <% } else { %>
# http://wiki.greasespot.net/Main_Page # http://wiki.greasespot.net/Main_Page
$.sync = do ->
$.on window, 'storage', (e) ->
if cb = $.syncing[e.key]
cb JSON.parse e.newValue
(key, cb) -> $.syncing[g.NAMESPACE + key] = cb
$.delete = (keys) -> $.delete = (keys) ->
unless keys instanceof Array unless keys instanceof Array
keys = [keys] keys = [keys]

View File

@ -59,5 +59,4 @@ class Clone extends Post
@isDead = true if origin.isDead @isDead = true if origin.isDead
@isClone = true @isClone = true
index = origin.clones.push(@) - 1 root.dataset.clone = origin.clones.push(@) - 1
root.setAttribute 'data-clone', index

View File

@ -16,29 +16,34 @@ class Post
quotelinks: [] quotelinks: []
backlinks: info.getElementsByClassName 'backlink' backlinks: info.getElementsByClassName 'backlink'
unless @isReply = $.hasClass post, 'reply'
@thread.OP = @
@thread.isSticky = !!$ '.stickyIcon', info
@thread.isClosed = !!$ '.closedIcon', info
@info = {} @info = {}
if subject = $ '.subject', info if subject = $ '.subject', info
@nodes.subject = subject @nodes.subject = subject
@info.subject = subject.textContent @info.subject = subject.textContent
if name = $ '.name', info if name = $ '.name', info
@nodes.name = name @nodes.name = name
@info.name = name.textContent @info.name = name.textContent
if email = $ '.useremail', info if email = $ '.useremail', info
@nodes.email = email @nodes.email = email
@info.email = decodeURIComponent email.href[7..] @info.email = decodeURIComponent email.href[7..]
if tripcode = $ '.postertrip', info if tripcode = $ '.postertrip', info
@nodes.tripcode = tripcode @nodes.tripcode = tripcode
@info.tripcode = tripcode.textContent @info.tripcode = tripcode.textContent
if uniqueID = $ '.posteruid', info if uniqueID = $ '.posteruid', info
@nodes.uniqueID = uniqueID @nodes.uniqueID = uniqueID
@info.uniqueID = uniqueID.firstElementChild.textContent @info.uniqueID = uniqueID.firstElementChild.textContent
if capcode = $ '.capcode.hand', info if capcode = $ '.capcode.hand', info
@nodes.capcode = capcode @nodes.capcode = capcode
@info.capcode = capcode.textContent.replace '## ', '' @info.capcode = capcode.textContent.replace '## ', ''
if flag = $ '.countryFlag', info if flag = $ '.flag, .countryFlag', info
@nodes.flag = flag @nodes.flag = flag
@info.flag = flag.title @info.flag = flag.title
if date = $ '.dateTime', info if date = $ '.dateTime', info
@nodes.date = date @nodes.date = date
@info.date = new Date date.dataset.utc * 1000 @info.date = new Date date.dataset.utc * 1000
@info.yours = QR.db.get @info.yours = QR.db.get
@ -48,43 +53,7 @@ class Post
@parseComment() @parseComment()
@parseQuotes() @parseQuotes()
@parseFile(that)
if (file = $ '.file', post) and thumb = $ 'img[data-md5]', file
# Supports JPG/PNG/GIF/PDF.
# Flash files are not supported.
alt = thumb.alt
anchor = thumb.parentNode
fileInfo = file.firstElementChild
@file =
info: fileInfo
text: fileInfo.firstElementChild
thumb: thumb
URL: anchor.href
size: alt.match(/[\d.]+\s\w+/)[0]
MD5: thumb.dataset.md5
isSpoiler: $.hasClass anchor, 'imgspoiler'
size = +@file.size.match(/[\d.]+/)[0]
unit = ['B', 'KB', 'MB', 'GB'].indexOf @file.size.match(/\w+$/)[0]
size *= 1024 while unit-- > 0
@file.sizeInBytes = size
@file.thumbURL =
if that.isArchived
thumb.src
else
"#{location.protocol}//thumbs.4chan.org/#{board}/thumb/#{@file.URL.match(/(\d+)\./)[1]}s.jpg"
# replace %22 with quotes, see:
# crbug.com/81193
# webk.it/62107
# https://www.w3.org/Bugs/Public/show_bug.cgi?id=16909
# http://www.whatwg.org/specs/web-apps/current-work/#multipart-form-data
@file.name = $('span[title]', fileInfo).title.replace /%22/g, '"'
if @file.isImage = /(jpg|png|gif)$/i.test @file.name
@file.dimensions = @file.text.textContent.match(/\d+x\d+/)[0]
unless @isReply = $.hasClass post, 'reply'
@thread.OP = @
@thread.isSticky = !!$ '.stickyIcon', @nodes.info
@thread.isClosed = !!$ '.closedIcon', @nodes.info
@clones = [] @clones = []
g.posts[@fullID] = thread.posts[@] = board.posts[@] = @ g.posts[@fullID] = thread.posts[@] = board.posts[@] = @
@ -108,18 +77,18 @@ class Post
nodes = d.evaluate './/br|.//text()', bq, null, 7, null nodes = d.evaluate './/br|.//text()', bq, null, 7, null
i = 0 i = 0
while i < nodes.snapshotLength while i < nodes.snapshotLength
text.push if data = nodes.snapshotItem(i++).data then data else '\n' text.push nodes.snapshotItem(i++).data or '\n'
@info.comment = text.join('').trim().replace /\s+$/gm, '' @info.comment = text.join('').trim().replace /\s+$/gm, ''
parseQuotes: -> parseQuotes: ->
quotes = {} quotes = {}
for quotelink in $$ '.quotelink', @nodes.comment for quotelink in $$ '.quotelink', @nodes.comment
# Don't add board links. (>>>/b/) # Don't add board links. (>>>/b/)
hash = quotelink.hash {hash} = quotelink
continue unless hash continue unless hash
# Don't add catalog links. (>>>/b/catalog or >>>/b/search) # Don't add catalog links. (>>>/b/catalog or >>>/b/search)
pathname = quotelink.pathname {pathname} = quotelink
continue if /catalog$/.test pathname continue if /catalog$/.test pathname
# Don't add rules links. (>>>/a/rules) # Don't add rules links. (>>>/a/rules)
@ -128,14 +97,49 @@ class Post
@nodes.quotelinks.push quotelink @nodes.quotelinks.push quotelink
# Don't count capcode replies as quotes. (Admin/Mod/Dev Replies: ...) # Don't count capcode replies as quotes in OPs. (Admin/Mod/Dev Replies: ...)
continue if quotelink.parentNode.parentNode.className is 'capcodeReplies' continue if !@isReply and $.hasClass quotelink.parentNode.parentNode, 'capcodeReplies'
# Basically, only add quotes that link to posts on an imageboard. # Basically, only add quotes that link to posts on an imageboard.
quotes["#{pathname.split('/')[1]}.#{hash[2..]}"] = true quotes["#{pathname.split('/')[1]}.#{hash[2..]}"] = true
return if @isClone return if @isClone
@quotes = Object.keys quotes @quotes = Object.keys quotes
parseFile: (that) ->
return unless (fileEl = $ '.file', @nodes.post) and thumb = $ 'img[data-md5]', fileEl
# Supports JPG/PNG/GIF/PDF.
# Flash files are not supported.
alt = thumb.alt
anchor = thumb.parentNode
fileInfo = fileEl.firstElementChild
@file =
info: fileInfo
text: fileInfo.firstElementChild
thumb: thumb
URL: anchor.href
size: alt.match(/[\d.]+\s\w+/)[0]
MD5: thumb.dataset.md5
isSpoiler: $.hasClass anchor, 'imgspoiler'
size = +@file.size.match(/[\d.]+/)[0]
unit = ['B', 'KB', 'MB', 'GB'].indexOf @file.size.match(/\w+$/)[0]
size *= 1024 while unit-- > 0
@file.sizeInBytes = size
@file.thumbURL = if that.isArchived
thumb.src
else
"#{location.protocol}//thumbs.4chan.org/#{@board}/thumb/#{@file.URL.match(/(\d+)\./)[1]}s.jpg"
@file.name = $('span[title]', fileInfo).title
<% if (type === 'crx') { %>
# replace %22 with quotes, see:
# crbug.com/81193
# webk.it/62107
# https://www.w3.org/Bugs/Public/show_bug.cgi?id=16909
# http://www.whatwg.org/specs/web-apps/current-work/#multipart-form-data
@file.name = @file.name.replace /%22/g, '"'
<% } %>
if @file.isImage = /(jpg|png|gif)$/i.test @file.name
@file.dimensions = @file.text.textContent.match(/\d+x\d+/)[0]
kill: (file, now) -> kill: (file, now) ->
now or= new Date() now or= new Date()
if file if file
@ -190,10 +194,12 @@ class Post
quotelink.textContent = quotelink.textContent.replace '\u00A0(Dead)', '' quotelink.textContent = quotelink.textContent.replace '\u00A0(Dead)', ''
$.rmClass quotelink, 'deadlink' $.rmClass quotelink, 'deadlink'
return return
addClone: (context) -> addClone: (context) ->
new Clone @, context new Clone @, context
rmClone: (index) -> rmClone: (index) ->
@clones.splice index, 1 @clones.splice index, 1
for clone in @clones[index..] for clone in @clones[index..]
clone.nodes.root.setAttribute 'data-clone', index++ clone.nodes.root.dataset.clone = index++
return return

View File

@ -2,8 +2,7 @@ class Thread
callbacks: [] callbacks: []
toString: -> @ID toString: -> @ID
constructor: (ID, @board) -> constructor: (@ID, @board) ->
@ID = +ID
@fullID = "#{@board}.#{@ID}" @fullID = "#{@board}.#{@ID}"
@posts = {} @posts = {}

View File

@ -16,6 +16,7 @@
}], }],
"homepage_url": "<%= meta.page %>", "homepage_url": "<%= meta.page %>",
"minimum_chrome_version": "24", "minimum_chrome_version": "24",
"minimum_opera_version": "15",
"permissions": [ "permissions": [
"storage" "storage"
] ]

View File

@ -13,7 +13,7 @@ ImageHover =
el = $.el 'img', el = $.el 'img',
id: 'ihover' id: 'ihover'
src: post.file.URL src: post.file.URL
el.setAttribute 'data-fullid', post.fullID el.dataset.fullID = post.fullID
$.add Header.hover, el $.add Header.hover, el
UI.hover UI.hover
root: @ root: @
@ -24,7 +24,7 @@ ImageHover =
$.on el, 'error', ImageHover.error $.on el, 'error', ImageHover.error
error: -> error: ->
return unless doc.contains @ return unless doc.contains @
post = g.posts[@dataset.fullid] post = g.posts[@dataset.fullID]
src = @src.split '/' src = @src.split '/'
if src[2] is 'images.4chan.org' if src[2] is 'images.4chan.org'

View File

@ -4,12 +4,10 @@ Sauce =
links = [] links = []
for link in Conf['sauces'].split '\n' for link in Conf['sauces'].split '\n'
continue if link[0] is '#'
try try
links.push @createSauceLink link.trim() links.push @createSauceLink link.trim() if link[0] isnt '#'
catch err catch err
# Don't add random text plz. # Don't add random text plz.
continue
return unless links.length return unless links.length
@links = links @links = links
@link = $.el 'a', target: '_blank' @link = $.el 'a', target: '_blank'

View File

@ -44,9 +44,8 @@ DeleteLink =
return if DeleteLink.cooldown.counting is post return if DeleteLink.cooldown.counting is post
$.off @, 'click', DeleteLink.delete $.off @, 'click', DeleteLink.delete
@textContent = "Deleting #{@textContent}..."
fileOnly = $.hasClass @, 'delete-file' fileOnly = $.hasClass @, 'delete-file'
@textContent = "Deleting #{if fileOnly then 'file' else 'post'}..."
form = form =
mode: 'usrdel' mode: 'usrdel'

View File

@ -8,29 +8,21 @@ Menu =
cb: @node cb: @node
node: -> node: ->
button = Menu.makeButton @
if @isClone if @isClone
$.replace $('.menu-button', @nodes.info), button button = $ '.menu-button', @nodes.info
return else
$.add @nodes.info, [$.tn('\u00A0'), button] button = Menu.makeButton @
$.add @nodes.info, [$.tn('\u00A0'), button]
$.on button, 'click', Menu.toggle
makeButton: do -> makeButton: do ->
a = null a = null
(post) -> ->
a or= $.el 'a', a or= $.el 'a',
className: 'menu-button brackets-wrap' className: 'menu-button brackets-wrap'
innerHTML: '<span class=drop-marker></span>' innerHTML: '<span class=drop-marker></span>'
href: 'javascript:;' href: 'javascript:;'
clone = a.cloneNode true a.cloneNode true
clone.setAttribute 'data-postid', post.fullID
clone.setAttribute 'data-clone', true if post.isClone
$.on clone, 'click', Menu.toggle
clone
toggle: (e) -> toggle: (e) ->
post = Menu.menu.toggle e, @, Get.postFromNode @
if @dataset.clone
Get.postFromNode @
else
g.posts[@dataset.postid]
Menu.menu.toggle e, @, post

View File

@ -93,10 +93,10 @@ Keybinds =
window.location = "/#{g.BOARD}/catalog" window.location = "/#{g.BOARD}/catalog"
# Thread Navigation # Thread Navigation
when Conf['Next thread'] when Conf['Next thread']
return if g.VIEW is 'thread' return if g.VIEW isnt 'index'
Nav.scroll +1 Nav.scroll +1
when Conf['Previous thread'] when Conf['Previous thread']
return if g.VIEW is 'thread' return if g.VIEW isnt 'index'
Nav.scroll -1 Nav.scroll -1
when Conf['Expand thread'] when Conf['Expand thread']
ExpandThread.toggle thread ExpandThread.toggle thread

View File

@ -58,8 +58,7 @@ Nav =
# unless we're not at the beginning of the current thread # unless we're not at the beginning of the current thread
# (and thus wanting to move to beginning) # (and thus wanting to move to beginning)
# or we're above the first thread and don't want to skip it # 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) if (delta is -1 and top > -5) or (delta is +1 and top < 5)
i += delta top = threads[i + delta]?.getBoundingClientRect().top - topMargin
top = threads[i]?.getBoundingClientRect().top - topMargin
window.scrollBy 0, top window.scrollBy 0, top

View File

@ -158,8 +158,7 @@ ThreadUpdater =
By sending the `If-Modified-Since` header we get a proper status code, and no response. By sending the `If-Modified-Since` header we get a proper status code, and no response.
This saves bandwidth for both the user and the servers and avoid unnecessary computation. This saves bandwidth for both the user and the servers and avoid unnecessary computation.
### ###
# XXX 304 -> 0 in Opera [text, klass] = if req.status is 304
[text, klass] = if [0, 304].contains req.status
[null, null] [null, null]
else else
["#{req.statusText} (#{req.status})", 'warning'] ["#{req.statusText} (#{req.status})", 'warning']

View File

@ -36,13 +36,11 @@ Unread =
# Let the header's onload callback handle it. # Let the header's onload callback handle it.
return if (hash = location.hash.match /\d+/) and hash[0] of Unread.thread.posts return if (hash = location.hash.match /\d+/) and hash[0] of Unread.thread.posts
if Unread.posts.length if Unread.posts.length
# Scroll to before the first unread post. # Scroll to a non-hidden, non-OP post that's before the first unread post.
prevID = 0 post = Unread.posts[0]
while root = $.x 'preceding-sibling::div[contains(@class,"postContainer")][1]', Unread.posts[0].nodes.root while root = $.x 'preceding-sibling::div[contains(@class,"replyContainer")][1]', post.nodes.root
post = Get.postFromRoot root break unless (post = Get.postFromRoot root).isHidden
break if prevID is post.ID return unless root
prevID = post.ID
break unless post.isHidden
onload = -> root.scrollIntoView false if checkPosition root onload = -> root.scrollIntoView false if checkPosition root
else else
# Scroll to the last read post. # Scroll to the last read post.
@ -188,9 +186,7 @@ Unread =
else else
Favicon.default Favicon.default
<% if (type !== 'crx') { %> <% if (type === 'userscript') { %>
# `favicon.href = href` doesn't work on Firefox. # `favicon.href = href` doesn't work on Firefox.
# `favicon.href = href` isn't enough on Opera.
# Opera won't always update the favicon if the href didn't change.
$.add d.head, Favicon.el $.add d.head, Favicon.el
<% } %> <% } %>

View File

@ -97,8 +97,8 @@ QR =
$.rmClass QR.captcha.nodes.input, 'error' $.rmClass QR.captcha.nodes.input, 'error'
if Conf['QR Shortcut'] if Conf['QR Shortcut']
$.toggleClass $('.qr-shortcut'), 'disabled' $.toggleClass $('.qr-shortcut'), 'disabled'
for i in QR.posts for post in QR.posts.splice 0, QR.posts.length, new QR.post true
QR.posts[0].rm() post.delete()
QR.cooldown.auto = false QR.cooldown.auto = false
QR.status() QR.status()
@ -152,7 +152,8 @@ QR =
status: -> status: ->
return unless QR.nodes return unless QR.nodes
if g.DEAD {thread} = QR.posts[0]
if thread isnt 'new' and g.threads["#{g.BOARD}.#{thread}"].isDead
value = 404 value = 404
disabled = true disabled = true
QR.cooldown.auto = false QR.cooldown.auto = false
@ -373,14 +374,10 @@ QR =
e?.preventDefault() e?.preventDefault()
return unless QR.postingIsEnabled return unless QR.postingIsEnabled
sel = d.getSelection() sel = d.getSelection()
selectionRoot = $.x 'ancestor::div[contains(@class,"postContainer")][1]', sel.anchorNode post = Get.postFromNode @
post = Get.postFromNode @ text = ">>#{post}\n"
{OP} = Get.contextFromLink(@).thread if (s = sel.toString().trim()) and post is Get.postFromNode sel.anchorNode
text = ">>#{post}\n"
if (s = sel.toString().trim()) and post.nodes.root is selectionRoot
# XXX Opera doesn't retain `\n`s?
s = s.replace /\n/g, '\n>' s = s.replace /\n/g, '\n>'
text += ">#{s}\n" text += ">#{s}\n"
@ -391,7 +388,7 @@ QR =
$.addClass QR.nodes.el, 'dump' $.addClass QR.nodes.el, 'dump'
QR.cooldown.auto = true QR.cooldown.auto = true
{com, thread} = QR.nodes {com, thread} = QR.nodes
thread.value = OP.ID unless com.value thread.value = Get.contextFromNode(@).thread unless com.value
thread.nextElementSibling.firstElementChild.textContent = thread.options[thread.selectedIndex].textContent thread.nextElementSibling.firstElementChild.textContent = thread.options[thread.selectedIndex].textContent
caretPos = com.selectionStart caretPos = com.selectionStart
@ -449,7 +446,7 @@ QR =
QR.nodes.fileInput.click() QR.nodes.fileInput.click()
fileInput: (files) -> fileInput: (files) ->
if @ instanceof Element #or files instanceof Event # file input if files instanceof Event # file input
files = [@files...] files = [@files...]
QR.nodes.fileInput.value = null # Don't hold the files from being modified on windows QR.nodes.fileInput.value = null # Don't hold the files from being modified on windows
{length} = files {length} = files
@ -506,7 +503,7 @@ QR =
for elm in $$ '*', el for elm in $$ '*', el
$.on elm, 'blur', QR.focusout $.on elm, 'blur', QR.focusout
$.on elm, 'focus', QR.focusin $.on elm, 'focus', QR.focusin
<% } %> <% } %>
$.on el, 'click', @select.bind @ $.on el, 'click', @select.bind @
$.on @nodes.rm, 'click', (e) => e.stopPropagation(); @rm() $.on @nodes.rm, 'click', (e) => e.stopPropagation(); @rm()
$.on @nodes.label, 'click', (e) => e.stopPropagation() $.on @nodes.label, 'click', (e) => e.stopPropagation()
@ -555,7 +552,7 @@ QR =
@unlock() @unlock()
rm: -> rm: ->
$.rm @nodes.el @delete()
index = QR.posts.indexOf @ index = QR.posts.indexOf @
if QR.posts.length is 1 if QR.posts.length is 1
new QR.post true new QR.post true
@ -563,7 +560,9 @@ QR =
else if @ is QR.selected else if @ is QR.selected
(QR.posts[index-1] or QR.posts[index+1]).select() (QR.posts[index-1] or QR.posts[index+1]).select()
QR.posts.splice index, 1 QR.posts.splice index, 1
return unless window.URL QR.status()
delete: ->
$.rm @nodes.el
URL.revokeObjectURL @URL URL.revokeObjectURL @URL
lock: (lock=true) -> lock: (lock=true) ->
@ -608,15 +607,18 @@ QR =
if input.type is 'checkbox' if input.type is 'checkbox'
@spoiler = input.checked @spoiler = input.checked
return return
{value} = input {name} = input.dataset
@[input.dataset.name] = value @[name] = input.value
return if input.nodeName isnt 'TEXTAREA' switch name
@nodes.span.textContent = value when 'thread'
QR.characterCount() QR.status()
# Disable auto-posting if you're typing in the first post when 'com'
# during the last 5 seconds of the cooldown. @nodes.span.textContent = @com
if QR.cooldown.auto and @ is QR.posts[0] and 0 < QR.cooldown.seconds <= 5 QR.characterCount()
QR.cooldown.auto = false # Disable auto-posting if you're typing in the first post
# during the last 5 seconds of the cooldown.
if QR.cooldown.auto and @ is QR.posts[0] and 0 < QR.cooldown.seconds <= 5
QR.cooldown.auto = false
forceSave: -> forceSave: ->
return unless @ is QR.selected return unless @ is QR.selected
@ -630,26 +632,15 @@ QR =
@filename = "#{file.name} (#{$.bytesToString file.size})" @filename = "#{file.name} (#{$.bytesToString file.size})"
@nodes.el.title = @filename @nodes.el.title = @filename
@nodes.label.hidden = false if QR.spoiler @nodes.label.hidden = false if QR.spoiler
URL.revokeObjectURL @URL if window.URL URL.revokeObjectURL @URL
@showFileData() @showFileData()
unless /^image/.test file.type unless /^image/.test file.type
@nodes.el.style.backgroundImage = null @nodes.el.style.backgroundImage = null
return return
@setThumbnail() @setThumbnail()
setThumbnail: (fileURL) -> setThumbnail: ->
# XXX Opera does not support blob URL
# Create a redimensioned thumbnail. # Create a redimensioned thumbnail.
unless window.URL
unless fileURL
reader = new FileReader()
reader.onload = (e) =>
@setThumbnail e.target.result
reader.readAsDataURL @file
return
else
fileURL = URL.createObjectURL @file
img = $.el 'img' img = $.el 'img'
img.onload = => img.onload = =>
@ -661,7 +652,7 @@ QR =
s *= 3 if @file.type is 'image/gif' # let them animate s *= 3 if @file.type is 'image/gif' # let them animate
{height, width} = img {height, width} = img
if height < s or width < s if height < s or width < s
@URL = fileURL if window.URL @URL = fileURL
@nodes.el.style.backgroundImage = "url(#{@URL})" @nodes.el.style.backgroundImage = "url(#{@URL})"
return return
if height <= width if height <= width
@ -674,10 +665,6 @@ QR =
cv.height = img.height = height cv.height = img.height = height
cv.width = img.width = width cv.width = img.width = width
cv.getContext('2d').drawImage img, 0, 0, width, height cv.getContext('2d').drawImage img, 0, 0, width, height
unless window.URL
@nodes.el.style.backgroundImage = "url(#{cv.toDataURL()})"
delete @URL
return
URL.revokeObjectURL fileURL URL.revokeObjectURL fileURL
applyBlob = (blob) => applyBlob = (blob) =>
@URL = URL.createObjectURL blob @URL = URL.createObjectURL blob
@ -695,6 +682,7 @@ QR =
applyBlob new Blob [ui8a], type: 'image/png' applyBlob new Blob [ui8a], type: 'image/png'
fileURL = URL.createObjectURL @file
img.src = fileURL img.src = fileURL
rmFile: -> rmFile: ->
@ -704,7 +692,6 @@ QR =
@nodes.el.style.backgroundImage = null @nodes.el.style.backgroundImage = null
@nodes.label.hidden = true if QR.spoiler @nodes.label.hidden = true if QR.spoiler
@showFileData() @showFileData()
return unless window.URL
URL.revokeObjectURL @URL URL.revokeObjectURL @URL
showFileData: -> showFileData: ->
@ -729,33 +716,26 @@ QR =
@nodes.span.textContent = @com @nodes.span.textContent = @com
reader.readAsText file reader.readAsText file
dragStart: -> dragStart: -> $.addClass @, 'drag'
$.addClass @, 'drag' dragEnd: -> $.rmClass @, 'drag'
dragEnter: -> $.addClass @, 'over'
dragEnd: -> dragLeave: -> $.rmClass @, 'over'
$.rmClass @, 'drag'
dragEnter: ->
$.addClass @, 'over'
dragLeave: ->
$.rmClass @, 'over'
dragOver: (e) -> dragOver: (e) ->
e.preventDefault() e.preventDefault()
e.dataTransfer.dropEffect = 'move' e.dataTransfer.dropEffect = 'move'
drop: -> drop: ->
el = $ '.drag', @parentNode $.rmClass @, 'over'
$.rmClass el, 'drag' # Opera doesn't fire dragEnd if we drop it on something else
$.rmClass @, 'over'
return unless @draggable return unless @draggable
el = $ '.drag', @parentNode
index = (el) -> [el.parentNode.children...].indexOf el index = (el) -> [el.parentNode.children...].indexOf el
oldIndex = index el oldIndex = index el
newIndex = index @ newIndex = index @
(if oldIndex < newIndex then $.after else $.before) @, el (if oldIndex < newIndex then $.after else $.before) @, el
post = QR.posts.splice(oldIndex, 1)[0] post = QR.posts.splice(oldIndex, 1)[0]
QR.posts.splice newIndex, 0, post QR.posts.splice newIndex, 0, post
QR.status()
captcha: captcha:
init: -> init: ->
@ -784,20 +764,17 @@ QR =
img: imgContainer.firstChild img: imgContainer.firstChild
input: input input: input
if window.MutationObserver new MutationObserver(@load.bind @).observe @nodes.challenge,
observer = new MutationObserver @load.bind @ childList: true
observer.observe @nodes.challenge,
childList: true
else
$.on @nodes.challenge, 'DOMNodeInserted', @load.bind @
$.on imgContainer, 'click', @reload.bind @ $.on imgContainer, 'click', @reload.bind @
$.on input, 'keydown', @keydown.bind @ $.on input, 'keydown', @keydown.bind @
$.on input, 'focus', -> $.addClass QR.nodes.el, 'focus' $.on input, 'focus', -> $.addClass QR.nodes.el, 'focus'
$.on input, 'blur', -> $.rmClass QR.nodes.el, 'focus' $.on input, 'blur', -> $.rmClass QR.nodes.el, 'focus'
$.get 'captchas', [], (item) => $.get 'captchas', [], ({captchas}) =>
@sync item['captchas'] @sync captchas
$.sync 'captchas', @sync $.sync 'captchas', @sync
# start with an uncached captcha # start with an uncached captcha
@reload() @reload()
@ -806,12 +783,13 @@ QR =
# XXX Firefox lacks focusin/focusout support. # XXX Firefox lacks focusin/focusout support.
$.on input, 'blur', QR.focusout $.on input, 'blur', QR.focusout
$.on input, 'focus', QR.focusin $.on input, 'focus', QR.focusin
<% } %> <% } %>
$.addClass QR.nodes.el, 'has-captcha' $.addClass QR.nodes.el, 'has-captcha'
$.after QR.nodes.dumpList.parentElement, [imgContainer, input] $.after QR.nodes.dumpList.parentElement, [imgContainer, input]
sync: (@captchas) -> sync: (captchas) ->
QR.captcha.captchas = captchas
QR.captcha.count() QR.captcha.count()
getOne: -> getOne: ->
@ -931,10 +909,6 @@ QR =
# Add empty mimeType to avoid errors with URLs selected in Window's file dialog. # Add empty mimeType to avoid errors with URLs selected in Window's file dialog.
QR.mimeTypes.push '' QR.mimeTypes.push ''
nodes.fileInput.max = $('input[name=MAX_FILE_SIZE]').value nodes.fileInput.max = $('input[name=MAX_FILE_SIZE]').value
<% if (type !== 'userjs') { %>
# Opera's accept attribute is fucked up
nodes.fileInput.accept = "text/*, #{mimeTypes}"
<% } %>
QR.spoiler = !!$ 'input[name=spoiler]' QR.spoiler = !!$ 'input[name=spoiler]'
if QR.spoiler if QR.spoiler
@ -969,7 +943,7 @@ QR =
for elm in $$ '*', QR.nodes.el for elm in $$ '*', QR.nodes.el
$.on elm, 'blur', QR.focusout $.on elm, 'blur', QR.focusout
$.on elm, 'focus', QR.focusin $.on elm, 'focus', QR.focusin
<% } %> <% } %>
$.on dialog, 'focusin', QR.focusin $.on dialog, 'focusin', QR.focusin
$.on dialog, 'focusout', QR.focusout $.on dialog, 'focusout', QR.focusout
$.on nodes.autohide, 'change', QR.toggleHide $.on nodes.autohide, 'change', QR.toggleHide
@ -1133,11 +1107,6 @@ QR =
QR.status() QR.status()
response: -> response: ->
<% if (type === 'userjs') { %>
# The upload.onload callback is not called
# or at least not in time with Opera.
QR.req.upload.onload()
<% } %>
{req} = QR {req} = QR
delete QR.req delete QR.req
@ -1229,15 +1198,15 @@ QR =
QR.cooldown.set {req, post, isReply} QR.cooldown.set {req, post, isReply}
if threadID is postID # new thread URL = if threadID is postID # new thread
URL = "/#{g.BOARD}/res/#{threadID}" "/#{g.BOARD}/res/#{threadID}"
else if g.VIEW is 'index' and !QR.cooldown.auto and Conf['Open Post in New Tab'] # replying from the index else if g.VIEW is 'index' and !QR.cooldown.auto and Conf['Open Post in New Tab'] # replying from the index
URL = "/#{g.BOARD}/res/#{threadID}#p#{postID}" "/#{g.BOARD}/res/#{threadID}#p#{postID}"
if URL if URL
if Conf['Open Post in New Tab'] if Conf['Open Post in New Tab']
$.open "/#{g.BOARD}/res/#{threadID}" $.open URL
else else
window.location = "/#{g.BOARD}/res/#{threadID}" window.location = URL
QR.status() QR.status()

View File

@ -35,7 +35,7 @@ QuoteInline =
return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0 return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0
e.preventDefault() e.preventDefault()
{boardID, threadID, postID} = Get.postDataFromLink @ {boardID, threadID, postID} = Get.postDataFromLink @
context = Get.contextFromLink @ context = Get.contextFromNode @
if $.hasClass @, 'inlined' if $.hasClass @, 'inlined'
QuoteInline.rm @, boardID, threadID, postID, context QuoteInline.rm @, boardID, threadID, postID, context
else else

View File

@ -22,8 +22,9 @@ QuotePreview =
qp = $.el 'div', qp = $.el 'div',
id: 'qp' id: 'qp'
className: 'dialog' className: 'dialog'
$.add Header.hover, qp $.add Header.hover, qp
Get.postClone boardID, threadID, postID, qp, Get.contextFromLink @ Get.postClone boardID, threadID, postID, qp, Get.contextFromNode @
UI.hover UI.hover
root: @ root: @

View File

@ -21,6 +21,9 @@ Quotify =
if deadlink.parentNode.className is 'prettyprint' if deadlink.parentNode.className is 'prettyprint'
# Don't quotify deadlinks inside code tags, # Don't quotify deadlinks inside code tags,
# un-`span` them. # un-`span` them.
# This won't be necessary once 4chan
# stops quotifying inside code tags:
# https://github.com/4chan/4chan-JS/issues/77
$.replace deadlink, [deadlink.childNodes...] $.replace deadlink, [deadlink.childNodes...]
return return
@ -48,9 +51,8 @@ Quotify =
target: '_blank' target: '_blank'
textContent: "#{quote}\u00A0(Dead)" textContent: "#{quote}\u00A0(Dead)"
a.setAttribute 'data-boardid', boardID $.extend a.dataset, {boardID, threadID: post.thread.ID, postID}
a.setAttribute 'data-threadid', post.thread.ID
a.setAttribute 'data-postid', postID
else if redirect = Redirect.to 'thread', {boardID, threadID: 0, postID} else if redirect = Redirect.to 'thread', {boardID, threadID: 0, postID}
# Replace the .deadlink span if we can redirect. # Replace the .deadlink span if we can redirect.
a = $.el 'a', a = $.el 'a',
@ -60,9 +62,8 @@ Quotify =
textContent: "#{quote}\u00A0(Dead)" textContent: "#{quote}\u00A0(Dead)"
if Redirect.to 'post', {boardID, postID} if Redirect.to 'post', {boardID, postID}
# Make it function as a normal quote if we can fetch the post. # Make it function as a normal quote if we can fetch the post.
$.addClass a, 'quotelink' $.addClass a, 'quotelink'
a.setAttribute 'data-boardid', boardID $.extend a.dataset, {boardID, postID}
a.setAttribute 'data-postid', postID
unless @quotes.contains quoteID unless @quotes.contains quoteID
@quotes.push quoteID @quotes.push quoteID