Merge branch 'v3' of git://github.com/MayhemYDG/4chan-x into v3

Conflicts:
	CHANGELOG.md
	CONTRIBUTING.md
	Gruntfile.js
	README.md
	changelog-old
	css/style.css
	html/Monitoring/ThreadStats.html
	html/Posting/QR.html
	json/archives.json
	package.json
	src/Archive/Redirect.coffee
	src/General/Header.coffee
	src/General/Main.coffee
	src/General/Settings.coffee
	src/General/UI.coffee
	src/General/html/Settings/Filter-guide.html
	src/General/lib/$.coffee
	src/General/lib/clone.class
	src/General/lib/post.class
	src/Images/ImageHover.coffee
	src/Menu/Menu.coffee
	src/Monitoring/ThreadUpdater.coffee
	src/Posting/QuickReply.coffee
	src/Quotelinks/QuotePreview.coffee
	src/Quotelinks/Quotify.coffee
This commit is contained in:
Zixaphir 2013-07-21 08:28:35 -07:00
commit 582d067a10
43 changed files with 2231 additions and 1020 deletions

3
.gitignore vendored
View File

@ -2,8 +2,7 @@ node_modules/
*~ *~
*.db *.db
tmp-crx/ tmp-crx/
tmp-userjs/
tmp-userscript/ tmp-userscript/
builds/4chan-X-Chrome.zip builds/4chan-X-Chrome.zip
builds/4chan-X-Opera.nex builds/4chan-X-Opera.nex
Gruntfile.js Gruntfile.js

View File

@ -1,6 +1,11 @@
**MayhemYDG**:
- Fix impossibility to create new threads when in dead threads.
- Drop Opera <15 support.
- Fix flag filtering on /sp/ and /int/.
- Minor fixes.
### v1.2.17 ### v1.2.17
*2013-06-17* *2013-06-17*
**seaweedchan**: **seaweedchan**:
- Fix full images being forced onto their own line - Fix full images being forced onto their own line

49
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,49 @@
## Reporting bugs and suggestions
Reporting bugs:
1. Make sure both your **browser** and **4chan X** are up to date.
2. Disable your other extensions & scripts to identify [conflicts](https://github.com/MayhemYDG/4chan-x/wiki/FAQ#known-conflicting-extensions).
3. If your issue persists, open a [new issue](https://github.com/MayhemYDG/4chan-x/issues) with the following information:
1. Precise steps to reproduce the problem, with the expected and actual results.
2. Console errors, if any.
3. Browser version.
4. Your exported settings. If your settings contains sensible information (e.g. personas), edit the text file manually.
Open your console with:
- `Ctrl + Shift + J` on Chrome and Opera.
- `Ctrl + Shift + K` on Firefox.
Respect these guidelines:
- Describe the issue clearly, put some effort into it. A one-liner isn't a good enough description.
- If you want to get your suggestion implemented sooner, make it convincing.
- If you want to criticize, make it convincing and constructive.
- Be mature. Act like an idiot and you will be blocked without warning.
## Development & Contribution
### Get started
- Install [node.js](http://nodejs.org/).
- Install [Grunt's CLI](http://gruntjs.com/) with `npm install -g grunt-cli`.
- Clone 4chan X.
- `cd` into it.
- Install/Update 4chan X dependencies with `npm install`.
### Build
- Build with `grunt`.
- Continuously build with `grunt watch`.
### Release
- Update the version with `grunt patch`, `grunt minor` or `grunt major`.
- Release with `grunt release`.
Note: this is only used to release new 4chan X versions, and is **not** needed or wanted in pull requests.
### Contribute
- Edit the CoffeeScript sources.
- If the edits affect regular users, edit the changelog.
- Open a pull request.

View File

@ -54,16 +54,6 @@ module.exports = (grunt) ->
'tmp-<%= pkg.type %>/script.js' 'tmp-<%= pkg.type %>/script.js'
] ]
userjs:
options: concatOptions
src: [
'src/General/meta/botproc.js'
'src/General/meta/metadata.js'
'src/General/meta/banner.js'
'tmp-<%= pkg.type %>/script.js'
]
dest: 'builds/<%= pkg.name %>.js'
userscript: userscript:
options: concatOptions options: concatOptions
files: files:
@ -94,7 +84,6 @@ module.exports = (grunt) ->
build: [ build: [
'concat:meta' 'concat:meta'
'build-crx' 'build-crx'
'build-userjs'
'build-userscript' 'build-userscript'
] ]
@ -137,7 +126,6 @@ module.exports = (grunt) ->
clean: clean:
builds: 'builds' builds: 'builds'
tmpcrx: 'tmp-crx' tmpcrx: 'tmp-crx'
tmpuserjs: 'tmp-userjs'
tmpuserscript: 'tmp-userscript' tmpuserscript: 'tmp-userscript'
grunt.loadNpmTasks 'grunt-bump' grunt.loadNpmTasks 'grunt-bump'
@ -171,14 +159,6 @@ module.exports = (grunt) ->
'clean:tmpcrx' 'clean:tmpcrx'
] ]
grunt.registerTask 'build-userjs', [
'set-build:userjs'
'concat:coffee'
'coffee:script'
'concat:userjs'
'clean:tmpuserjs'
]
grunt.registerTask 'build-userscript', [ grunt.registerTask 'build-userscript', [
'set-build:userscript' 'set-build:userscript'
'concat:coffee' 'concat:coffee'

View File

@ -1,5 +1,5 @@
/* /*
* 4chan X - Version 1.2.17 - 2013-06-18 * 4chan X - Version 1.2.17 - 2013-07-21
* *
* Licensed under the MIT license. * Licensed under the MIT license.
* https://github.com/seaweedchan/4chan-x/blob/master/LICENSE * https://github.com/seaweedchan/4chan-x/blob/master/LICENSE

View File

@ -41,4 +41,4 @@ Note: this is only used to release new 4chan X versions, and is **not** needed o
- Edit the CoffeeScript sources. - Edit the CoffeeScript sources.
- If the edits affect regular users, edit the changelog. - If the edits affect regular users, edit the changelog.
- Open a pull request. - Open a pull request.

File diff suppressed because one or more lines are too long

View File

@ -15,7 +15,8 @@
"run_at": "document_start" "run_at": "document_start"
}], }],
"homepage_url": "http://seaweedchan.github.io/4chan-x/", "homepage_url": "http://seaweedchan.github.io/4chan-x/",
"minimum_chrome_version": "26", "minimum_chrome_version": "27",
"minimum_opera_version": "15",
"permissions": [ "permissions": [
"storage" "storage"
] ]

File diff suppressed because one or more lines are too long

1327
changelog-old Normal file

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

@ -21,15 +21,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,14 +62,6 @@ 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
@ -82,7 +73,7 @@ Redirect =
'Install Gentoo': 'Install Gentoo':
'domain': 'archive.installgentoo.net' 'domain': 'archive.installgentoo.net'
'http': true 'http': true
'https': true 'https': false
'software': 'fuuka' 'software': 'fuuka'
'boards': ['diy', 'g', 'sci'] 'boards': ['diy', 'g', 'sci']
'files': [] 'files': []
@ -109,6 +100,14 @@ Redirect =
'software': 'fuuka' 'software': 'fuuka'
'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]

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=fourchanx-link>&nbsp;#{if type is 'hide' then '-' else '+'}&nbsp;</span>" innerHTML: "<span class=fourchanx-link>&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,9 +1,3 @@
<% if (type === 'userjs') { %>
# Opera doesn't support the @match metadata key,
# return 4chan X here if we're not on 4chan.
return unless /^(boards|images|sys)\.4chan\.org$/.test location.hostname
<% } %>
Conf = {} Conf = {}
c = console c = console
d = document d = document

View File

@ -70,7 +70,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']), Header.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']
@ -131,7 +131,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
@ -166,7 +166,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

@ -19,7 +19,7 @@ Main =
$.get Conf, Main.initFeatures $.get Conf, Main.initFeatures
$.on d, '4chanMainInit', Main.initStyle $.on d, '4chanMainInit', Main.initStyle
$.asap (-> d.head and $('link[rel="shortcut icon"]', d.head) or d.readyState in ['interactive', 'complete']), $.asap (-> d.head and $('link[rel="shortcut icon"]', d.head) or d.readyState isnt 'loading'),
Main.initStyle Main.initStyle
initFeatures: (items) -> initFeatures: (items) ->
@ -141,8 +141,6 @@ Main =
<% if (type === 'crx') { %> <% if (type === 'crx') { %>
$.addClass doc, 'webkit' $.addClass doc, 'webkit'
$.addClass doc, 'blink' $.addClass doc, 'blink'
<% } else if (type === 'userjs') { %>
$.addClass doc, 'presto'
<% } else { %> <% } else { %>
$.addClass doc, 'gecko' $.addClass doc, 'gecko'
<% } %> <% } %>
@ -166,13 +164,9 @@ Main =
$.addClass doc, style $.addClass doc, style
setStyle() setStyle()
return unless mainStyleSheet return unless mainStyleSheet
if window.MutationObserver new MutationObserver(setStyle).observe mainStyleSheet,
observer = new MutationObserver setStyle attributes: true
observer.observe mainStyleSheet, attributeFilter: ['href']
attributes: true
attributeFilter: ['href']
else
$.on mainStyleSheet, 'DOMAttrModified', setStyle
initReady: -> initReady: ->
if d.title is '4chan - 404 Not Found' if d.title is '4chan - 404 Not Found'
@ -192,20 +186,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
@ -373,7 +365,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
css: """ css: """

View File

@ -466,7 +466,6 @@ Settings =
$.cb.checked.call @ $.cb.checked.call @
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

@ -306,12 +306,12 @@ UI = do ->
hoverstart = ({root, el, latestEvent, endEvents, asapTest, cb}) -> hoverstart = ({root, el, latestEvent, endEvents, asapTest, cb}) ->
o = { o = {
root: root root
el: el el
style: el.style style: el.style
cb: cb cb
endEvents: endEvents endEvents
latestEvent: latestEvent latestEvent
clientHeight: doc.clientHeight clientHeight: doc.clientHeight
clientWidth: doc.clientWidth clientWidth: doc.clientWidth
} }
@ -327,6 +327,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
@ -357,6 +362,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

View File

@ -301,10 +301,8 @@ a {
box-sizing: border-box; box-sizing: border-box;
box-shadow: 0 0 15px rgba(0, 0, 0, .15); box-shadow: 0 0 15px rgba(0, 0, 0, .15);
height: 600px; height: 600px;
min-height: 0;
max-height: 100%; max-height: 100%;
width: 900px; width: 900px;
min-width: 0;
max-width: 100%; max-width: 100%;
margin: auto; margin: auto;
padding: 3px; padding: 3px;
@ -312,7 +310,6 @@ a {
left: 50%; left: 50%;
-moz-transform: translate(-50%, -50%); -moz-transform: translate(-50%, -50%);
-webkit-transform: translate(-50%, -50%); -webkit-transform: translate(-50%, -50%);
-o-transform: translate(-50%, -50%);
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
} }
#fourchanx-settings > nav { #fourchanx-settings > nav {
@ -389,14 +386,17 @@ a {
position: absolute; position: absolute;
} }
.section-advanced .note { .section-advanced .note {
font-size: 0.8em; font-size: 0.8em;
font-style: italic; font-style: italic;
margin-left: 10px; margin-left: 10px;
} }
.section-advanced .note code { .section-advanced .note code {
font-style: normal; font-style: normal;
font-size: 11px; font-size: 11px;
} }
.section-keybinds .field {
font-family: monospace;
}
#fourchanx-settings fieldset { #fourchanx-settings fieldset {
border: 1px solid; border: 1px solid;
border-radius: 3px; border-radius: 3px;
@ -530,7 +530,8 @@ a.hide-announcement {
.deadlink { .deadlink {
text-decoration: none !important; text-decoration: none !important;
} }
.backlink.deadlink:not(.forwardlink), .quotelink.deadlink:not(.forwardlink) { .backlink.deadlink:not(.forwardlink),
.quotelink.deadlink:not(.forwardlink) {
text-decoration: underline !important; text-decoration: underline !important;
} }
.inlined { .inlined {
@ -573,8 +574,6 @@ a.hide-announcement {
padding: 2px 2px 5px; padding: 2px 2px 5px;
} }
#qp img { #qp img {
max-height: 300px;
max-width: 500px;
max-height: 80vh; max-height: 80vh;
max-width: 50vw; max-width: 50vw;
} }
@ -610,8 +609,7 @@ a.hide-announcement {
:root.fit-width .full-image { :root.fit-width .full-image {
max-width: 100%; max-width: 100%;
} }
:root.gecko.fit-width .full-image, :root.gecko.fit-width .full-image {
:root.presto.fit-width .full-image {
width: 100%; width: 100%;
} }
#ihover { #ihover {
@ -668,7 +666,10 @@ a.hide-announcement {
#file-n-submit:not(.has-file) #qr-filerm { #file-n-submit:not(.has-file) #qr-filerm {
display: none; display: none;
} }
#qr select, #dump-button, .remove, .captcha-img { #qr select,
#dump-button,
.remove,
.captcha-img {
cursor: pointer; cursor: pointer;
} }
#qr { #qr {
@ -725,7 +726,8 @@ a.hide-announcement {
height: 9em; height: 9em;
} }
input.field.tripped:not(:hover):not(:focus) { input.field.tripped:not(:hover):not(:focus) {
color: transparent !important; text-shadow: none !important; color: transparent !important;
text-shadow: none !important;
} }
#qr textarea { #qr textarea {
resize: both; resize: both;
@ -907,7 +909,9 @@ a:only-of-type > .remove {
.qr-preview > label { .qr-preview > label {
background: rgba(0,0,0,.5); background: rgba(0,0,0,.5);
color: #fff; color: #fff;
right: 0; bottom: 0; left: 0; right: 0;
bottom: 0;
left: 0;
position: absolute; position: absolute;
text-align: center; text-align: center;
} }

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.
@ -26,4 +26,4 @@
Highlighted OPs will have their threads put on top of board pages by default.<br> Highlighted OPs will have their threads put on top of board pages by default.<br>
For example: <code>top:yes;</code> or <code>top:no;</code>. For example: <code>top:yes;</code> or <code>top:no;</code>.
</li> </li>
</ul> </ul>

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 = {}
@ -396,54 +387,14 @@ $.set = do ->
$.extend items, key $.extend items, key
set() set()
<% } else if (type === 'userjs') { %>
do ->
# http://www.opera.com/docs/userjs/specs/#scriptstorage
# http://www.opera.com/docs/userjs/using/#securepages
# The scriptStorage object is available only during
# the main User JavaScript thread, being therefore
# accessible only in the main body of the user script.
# To access the storage object later, keep a reference
# to the object.
{scriptStorage} = opera
$.delete = (keys) ->
unless keys instanceof Array
keys = [keys]
for key in keys
key = g.NAMESPACE + key
localStorage.removeItem key
delete scriptStorage[key]
return
$.get = (key, val, cb) ->
if typeof cb is 'function'
items = $.item key, val
else
items = key
cb = val
$.queueTask ->
for key of items
if val = scriptStorage[g.NAMESPACE + key]
items[key] = JSON.parse val
cb items
$.set = do ->
set = (key, val) ->
key = g.NAMESPACE + key
val = JSON.stringify val
if key of $.syncing
# for `storage` events
localStorage.setItem key, val
scriptStorage[key] = val
(keys, val) ->
if typeof keys is 'string'
set keys, val
return
for key, val of keys
set key, val
return
return
<% } 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
if Conf['Quick Reply'] if Conf['Quick Reply']
@ -50,43 +55,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[@] = @
@ -110,18 +79,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)
@ -130,14 +99,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
@ -192,10 +196,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 = {}
@ -11,4 +10,4 @@ class Thread
kill: -> kill: ->
@isDead = true @isDead = true
@timeOfDeath = Date.now() @timeOfDeath = Date.now()

View File

@ -15,7 +15,8 @@
"run_at": "document_start" "run_at": "document_start"
}], }],
"homepage_url": "<%= meta.page %>", "homepage_url": "<%= meta.page %>",
"minimum_chrome_version": "26", "minimum_chrome_version": "27",
"minimum_opera_version": "15",
"permissions": [ "permissions": [
"storage" "storage"
] ]

View File

@ -52,19 +52,6 @@ ImageExpand =
return return
setFitness: -> setFitness: ->
(if @checked then $.addClass else $.rmClass) doc, @name.toLowerCase().replace /\s+/g, '-' (if @checked then $.addClass else $.rmClass) doc, @name.toLowerCase().replace /\s+/g, '-'
<% if (type === 'userjs') { %>
# XXX Opera doesn't support CSS vh.
return unless @name is 'Fit height'
if @checked
$.on window, 'resize', ImageExpand.resize
unless ImageExpand.style
ImageExpand.style = $.addStyle null
ImageExpand.resize()
else
$.off window, 'resize', ImageExpand.resize
resize: ->
ImageExpand.style.textContent = ":root.fit-height .full-image {max-height:#{doc.clientHeight}px}"
<% } %>
toggle: (post) -> toggle: (post) ->
{thumb} = post.file {thumb} = post.file

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'
@ -48,4 +48,4 @@ ImageHover =
post.kill() post.kill()
else if postObj.filedeleted else if postObj.filedeleted
clearTimeout timeoutID clearTimeout timeoutID
post.kill true post.kill true

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 fourchanx-link' className: 'menu-button fourchanx-link'
innerHTML: '<i></i>' innerHTML: '<i></i>'
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

@ -95,10 +95,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

@ -60,8 +60,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

@ -157,8 +157,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

@ -101,8 +101,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()
focusin: -> focusin: ->
@ -150,7 +150,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
@ -371,14 +372,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"
@ -389,7 +386,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
caretPos = com.selectionStart caretPos = com.selectionStart
# Replace selection for text. # Replace selection for text.
@ -447,7 +444,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
@ -504,7 +501,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()
@ -553,7 +550,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
@ -561,7 +558,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) ->
@ -603,15 +602,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
@ -625,26 +627,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 = =>
@ -656,7 +647,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
@ -669,10 +660,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
@ -690,6 +677,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: ->
@ -699,7 +687,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: ->
@ -724,33 +711,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'
dragLeave: -> $.rmClass @, 'over'
dragEnd: ->
$.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: ->
@ -779,19 +759,15 @@ 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()
@ -800,12 +776,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.com.parentNode, [imgContainer, input] $.after QR.nodes.com.parentNode, [imgContainer, input]
sync: (@captchas) -> sync: (captchas) ->
QR.captcha.captchas = captchas
QR.captcha.count() QR.captcha.count()
getOne: -> getOne: ->
@ -922,10 +899,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
@ -960,7 +933,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
@ -1109,11 +1082,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
@ -1206,15 +1174,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
@ -107,4 +107,4 @@ QuoteInline =
{boardID, threadID, postID} = Get.postDataFromLink inlined {boardID, threadID, postID} = Get.postDataFromLink inlined
QuoteInline.rm inlined, boardID, threadID, postID, context QuoteInline.rm inlined, boardID, threadID, postID, context
$.rmClass inlined, 'inlined' $.rmClass inlined, 'inlined'
return return

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: @
@ -33,21 +34,6 @@ QuotePreview =
cb: QuotePreview.mouseout cb: QuotePreview.mouseout
asapTest: -> qp.firstElementChild asapTest: -> qp.firstElementChild
<% if (type === 'userjs') { %>
# XXX Opera workaround for "no mouseout fired" bug.
# Remove it once Opera uses Blink.
root = @
workaround = (e) ->
if @ is root
e.stopPropagation()
return
$.event 'mouseout', null, root
$.off d, 'mousemove', workaround
$.off root, 'mousemove', workaround
$.on d, 'mousemove', workaround
$.on root, 'mousemove', workaround
<% } %>
return unless origin = g.posts["#{boardID}.#{postID}"] return unless origin = g.posts["#{boardID}.#{postID}"]
if Conf['Quote Highlighting'] if Conf['Quote Highlighting']

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
@ -73,4 +74,4 @@ Quotify =
$.replace deadlink, a $.replace deadlink, a
if $.hasClass a, 'quotelink' if $.hasClass a, 'quotelink'
@nodes.quotelinks.push a @nodes.quotelinks.push a