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
tmp-crx/
tmp-userjs/
tmp-userscript/
builds/4chan-X-Chrome.zip
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
*2013-06-17*
**seaweedchan**:
- 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'
]
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:
options: concatOptions
files:
@ -94,7 +84,6 @@ module.exports = (grunt) ->
build: [
'concat:meta'
'build-crx'
'build-userjs'
'build-userscript'
]
@ -137,7 +126,6 @@ module.exports = (grunt) ->
clean:
builds: 'builds'
tmpcrx: 'tmp-crx'
tmpuserjs: 'tmp-userjs'
tmpuserscript: 'tmp-userscript'
grunt.loadNpmTasks 'grunt-bump'
@ -171,14 +159,6 @@ module.exports = (grunt) ->
'clean:tmpcrx'
]
grunt.registerTask 'build-userjs', [
'set-build:userjs'
'concat:coffee'
'coffee:script'
'concat:userjs'
'clean:tmpuserjs'
]
grunt.registerTask 'build-userscript', [
'set-build:userscript'
'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.
* 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.
- 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"
}],
"homepage_url": "http://seaweedchan.github.io/4chan-x/",
"minimum_chrome_version": "26",
"minimum_chrome_version": "27",
"minimum_opera_version": "15",
"permissions": [
"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": {
"grunt": "~0.4.1",
"grunt-bump": "~0.0.2",
"grunt-concurrent": "~0.2.0",
"grunt-contrib-clean": "~0.4.1",
"grunt-bump": "~0.0.11",
"grunt-concurrent": "~0.3.0",
"grunt-contrib-clean": "~0.5.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-copy": "~0.4.1",
"grunt-contrib-watch": "~0.4.4",
"grunt-shell": "~0.2.2"
"grunt-contrib-watch": "~0.5.0",
"grunt-shell": "~0.3.1"
},
"repository": {
"type": "git",

View File

@ -21,7 +21,6 @@ Redirect =
Redirect.post[boardID] = archive
unless boardID of Redirect.file or !archive.files.contains boardID
Redirect.file[boardID] = archive
return
archives:
'Foolz':
@ -29,7 +28,7 @@ Redirect =
'http': false
'https': true
'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']
'NSFW Foolz':
@ -63,14 +62,6 @@ Redirect =
'boards': ['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':
'domain': 'archive.foolzashit.com'
'http': true
@ -82,7 +73,7 @@ Redirect =
'Install Gentoo':
'domain': 'archive.installgentoo.net'
'http': true
'https': true
'https': false
'software': 'fuuka'
'boards': ['diy', 'g', 'sci']
'files': []
@ -109,6 +100,14 @@ Redirect =
'software': 'fuuka'
'boards': ['3', 'cgl', 'ck', 'fa', 'ic', 'jp', 'lit', 'q', 'tg', '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) ->
archive = (if dest is 'search' then Redirect.thread else Redirect[dest])[data.boardID]

View File

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

View File

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

View File

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

View File

@ -19,7 +19,7 @@ Get =
if index then post.clones[index] else post
postFromNode: (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
postDataFromLink: (link) ->
if link.hostname is 'boards.4chan.org'
@ -28,9 +28,8 @@ Get =
threadID = path[3]
postID = link.hash[2..]
else # resurrected quote
boardID = link.dataset.boardid
threadID = link.dataset.threadid or 0
postID = link.dataset.postid
{boardID, threadID, postID} = link.dataset
threadID or= 0
return {
boardID: boardID
threadID: +threadID
@ -185,7 +184,7 @@ Get =
# quotes
.replace /((&gt;){2}(&gt;\/[a-z\d]+\/)?\d+)/g, '<span class=deadlink>$1</span>'
threadID = data.thread_num
threadID = +data.thread_num
o =
# id
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 = {}
c = console
d = document

View File

@ -70,7 +70,7 @@ Header =
return unless Main.isThisPageLegit()
# Wait for #boardNavMobile instead of #boardNavDesktop,
# 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
$.add d.body, Header.hover
@setBarPosition Conf['Bottom Header']
@ -131,7 +131,7 @@ Header =
list = $ '#custom-board-list', Header.bar
$.rmAll list
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) ->
if /^[^\w@]/.test t
return $.tn t
@ -166,7 +166,7 @@ Header =
a.textContent
if m = t.match /-(index|catalog)/
a.setAttribute 'data-only', m[1]
a.dataset.only = m[1]
a.href = "//boards.4chan.org/#{board}/"
if m[1] is 'catalog'
a.href += 'catalog'

View File

@ -19,7 +19,7 @@ Main =
$.get Conf, Main.initFeatures
$.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
initFeatures: (items) ->
@ -141,8 +141,6 @@ Main =
<% if (type === 'crx') { %>
$.addClass doc, 'webkit'
$.addClass doc, 'blink'
<% } else if (type === 'userjs') { %>
$.addClass doc, 'presto'
<% } else { %>
$.addClass doc, 'gecko'
<% } %>
@ -166,13 +164,9 @@ Main =
$.addClass doc, style
setStyle()
return unless mainStyleSheet
if window.MutationObserver
observer = new MutationObserver setStyle
observer.observe mainStyleSheet,
attributes: true
attributeFilter: ['href']
else
$.on mainStyleSheet, 'DOMAttrModified', setStyle
new MutationObserver(setStyle).observe mainStyleSheet,
attributes: true
attributeFilter: ['href']
initReady: ->
if d.title is '4chan - 404 Not Found'
@ -192,20 +186,18 @@ Main =
threads = []
posts = []
for boardChild in board.children
continue unless $.hasClass boardChild, 'thread'
thread = new Thread boardChild.id[1..], g.BOARD
for threadRoot in $$ '.board > .thread', board
thread = new Thread +threadRoot.id[1..], g.BOARD
threads.push thread
for threadChild in boardChild.children
continue unless $.hasClass threadChild, 'postContainer'
for postRoot in $$ '.thread > .postContainer', threadRoot
try
posts.push new Post threadChild, thread, g.BOARD
posts.push new Post postRoot, thread, g.BOARD
catch err
# Skip posts that we failed to parse.
unless errors
errors = []
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
Main.handleErrors errors if errors
@ -373,7 +365,7 @@ Main =
unless 'thisPageIsLegit' of Main
Main.thisPageIsLegit = location.hostname is 'boards.4chan.org' 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
css: """

View File

@ -466,7 +466,6 @@ Settings =
$.cb.checked.call @
usercss: ->
CustomCSS.update()
keybinds: (section) ->
section.innerHTML = """
<%= 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}) ->
o = {
root: root
el: el
style: el.style
cb: cb
endEvents: endEvents
latestEvent: latestEvent
root
el
style: el.style
cb
endEvents
latestEvent
clientHeight: doc.clientHeight
clientWidth: doc.clientWidth
}
@ -327,6 +327,11 @@ UI = do ->
if $.x 'ancestor::div[contains(@class,"inline")][1]', root
$.on d, 'keydown', o.hoverend
$.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) ->
@latestEvent = e
@ -357,6 +362,10 @@ UI = do ->
$.off @root, @endEvents, @hoverend
$.off d, 'keydown', @hoverend
$.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

View File

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

View File

@ -1,6 +1,6 @@
<div class=warning #{if Conf['Filter'] then 'hidden' else ''}><code>Filter</code> is disabled.</div>
<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>
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.
@ -26,4 +26,4 @@
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>.
</li>
</ul>
</ul>

View File

@ -49,7 +49,7 @@ $.id = (id) ->
d.getElementById id
$.ready = (fc) ->
if d.readyState in ['interactive', 'complete']
unless d.readyState is 'loading'
$.queueTask fc
return
cb = ->
@ -221,9 +221,7 @@ $.event = (event, detail, root=d) ->
$.open = (URL) ->
<% if (type === 'userscript') { %>
# XXX fix GM opening file://// for protocol-less URLs.
# https://github.com/greasemonkey/greasemonkey/issues/1719
GM_openInTab ($.el 'a', href: URL).href
$.open = (URL) -> GM_openInTab URL
<% } else { %>
window.open URL, '_blank'
<% } %>
@ -298,29 +296,21 @@ $.minmax = (value, min, max) ->
value
)
$.syncing = {}
$.item = (key, val) ->
item = {}
item[key] = val
item
$.sync = do ->
$.syncing = {}
<% if (type === 'crx') { %>
$.sync = do ->
chrome.storage.onChanged.addListener (changes) ->
for key of changes
if cb = $.syncing[key]
cb changes[key].newValue
return
(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 = [
# filters
'name',
@ -372,6 +362,7 @@ $.get = (key, val, cb) ->
if syncItems
count++
chrome.storage.sync.get syncItems, done
$.set = do ->
items = {}
localItems = {}
@ -396,54 +387,14 @@ $.set = do ->
$.extend items, key
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 { %>
# 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) ->
unless keys instanceof Array
keys = [keys]

View File

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

View File

@ -16,29 +16,34 @@ class Post
quotelinks: []
backlinks: info.getElementsByClassName 'backlink'
unless @isReply = $.hasClass post, 'reply'
@thread.OP = @
@thread.isSticky = !!$ '.stickyIcon', info
@thread.isClosed = !!$ '.closedIcon', info
@info = {}
if subject = $ '.subject', info
if subject = $ '.subject', info
@nodes.subject = subject
@info.subject = subject.textContent
if name = $ '.name', info
if name = $ '.name', info
@nodes.name = name
@info.name = name.textContent
if email = $ '.useremail', info
if email = $ '.useremail', info
@nodes.email = email
@info.email = decodeURIComponent email.href[7..]
if tripcode = $ '.postertrip', info
if tripcode = $ '.postertrip', info
@nodes.tripcode = tripcode
@info.tripcode = tripcode.textContent
if uniqueID = $ '.posteruid', info
if uniqueID = $ '.posteruid', info
@nodes.uniqueID = uniqueID
@info.uniqueID = uniqueID.firstElementChild.textContent
if capcode = $ '.capcode.hand', info
if capcode = $ '.capcode.hand', info
@nodes.capcode = capcode
@info.capcode = capcode.textContent.replace '## ', ''
if flag = $ '.countryFlag', info
if flag = $ '.flag, .countryFlag', info
@nodes.flag = flag
@info.flag = flag.title
if date = $ '.dateTime', info
if date = $ '.dateTime', info
@nodes.date = date
@info.date = new Date date.dataset.utc * 1000
if Conf['Quick Reply']
@ -50,43 +55,7 @@ class Post
@parseComment()
@parseQuotes()
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
@parseFile(that)
@clones = []
g.posts[@fullID] = thread.posts[@] = board.posts[@] = @
@ -110,18 +79,18 @@ class Post
nodes = d.evaluate './/br|.//text()', bq, null, 7, null
i = 0
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, ''
parseQuotes: ->
quotes = {}
for quotelink in $$ '.quotelink', @nodes.comment
# Don't add board links. (>>>/b/)
hash = quotelink.hash
{hash} = quotelink
continue unless hash
# Don't add catalog links. (>>>/b/catalog or >>>/b/search)
pathname = quotelink.pathname
{pathname} = quotelink
continue if /catalog$/.test pathname
# Don't add rules links. (>>>/a/rules)
@ -130,14 +99,49 @@ class Post
@nodes.quotelinks.push quotelink
# Don't count capcode replies as quotes. (Admin/Mod/Dev Replies: ...)
continue if quotelink.parentNode.parentNode.className is 'capcodeReplies'
# Don't count capcode replies as quotes in OPs. (Admin/Mod/Dev Replies: ...)
continue if !@isReply and $.hasClass quotelink.parentNode.parentNode, 'capcodeReplies'
# Basically, only add quotes that link to posts on an imageboard.
quotes["#{pathname.split('/')[1]}.#{hash[2..]}"] = true
return if @isClone
@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) ->
now or= new Date()
if file
@ -192,10 +196,12 @@ class Post
quotelink.textContent = quotelink.textContent.replace '\u00A0(Dead)', ''
$.rmClass quotelink, 'deadlink'
return
addClone: (context) ->
new Clone @, context
rmClone: (index) ->
@clones.splice index, 1
for clone in @clones[index..]
clone.nodes.root.setAttribute 'data-clone', index++
return
clone.nodes.root.dataset.clone = index++
return

View File

@ -2,8 +2,7 @@ class Thread
callbacks: []
toString: -> @ID
constructor: (ID, @board) ->
@ID = +ID
constructor: (@ID, @board) ->
@fullID = "#{@board}.#{@ID}"
@posts = {}
@ -11,4 +10,4 @@ class Thread
kill: ->
@isDead = true
@timeOfDeath = Date.now()
@timeOfDeath = Date.now()

View File

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

View File

@ -52,19 +52,6 @@ ImageExpand =
return
setFitness: ->
(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) ->
{thumb} = post.file

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -60,8 +60,7 @@ Nav =
# unless we're not at the beginning of the current thread
# (and thus wanting to move to beginning)
# or we're above the first thread and don't want to skip it
unless (delta is -1 and Math.ceil(top) < 0) or (delta is +1 and top > 1)
i += delta
if (delta is -1 and top > -5) or (delta is +1 and top < 5)
top = threads[i + delta]?.getBoundingClientRect().top - topMargin
top = threads[i]?.getBoundingClientRect().top - topMargin
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.
This saves bandwidth for both the user and the servers and avoid unnecessary computation.
###
# XXX 304 -> 0 in Opera
[text, klass] = if [0, 304].contains req.status
[text, klass] = if req.status is 304
[null, null]
else
["#{req.statusText} (#{req.status})", 'warning']

View File

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

View File

@ -101,8 +101,8 @@ QR =
$.rmClass QR.captcha.nodes.input, 'error'
if Conf['QR Shortcut']
$.toggleClass $('.qr-shortcut'), 'disabled'
for i in QR.posts
QR.posts[0].rm()
for post in QR.posts.splice 0, QR.posts.length, new QR.post true
post.delete()
QR.cooldown.auto = false
QR.status()
focusin: ->
@ -150,7 +150,8 @@ QR =
status: ->
return unless QR.nodes
if g.DEAD
{thread} = QR.posts[0]
if thread isnt 'new' and g.threads["#{g.BOARD}.#{thread}"].isDead
value = 404
disabled = true
QR.cooldown.auto = false
@ -371,14 +372,10 @@ QR =
e?.preventDefault()
return unless QR.postingIsEnabled
sel = d.getSelection()
selectionRoot = $.x 'ancestor::div[contains(@class,"postContainer")][1]', sel.anchorNode
post = Get.postFromNode @
{OP} = Get.contextFromLink(@).thread
text = ">>#{post}\n"
if (s = sel.toString().trim()) and post.nodes.root is selectionRoot
# XXX Opera doesn't retain `\n`s?
sel = d.getSelection()
post = Get.postFromNode @
text = ">>#{post}\n"
if (s = sel.toString().trim()) and post is Get.postFromNode sel.anchorNode
s = s.replace /\n/g, '\n>'
text += ">#{s}\n"
@ -389,7 +386,7 @@ QR =
$.addClass QR.nodes.el, 'dump'
QR.cooldown.auto = true
{com, thread} = QR.nodes
thread.value = OP.ID unless com.value
thread.value = Get.contextFromNode(@).thread unless com.value
caretPos = com.selectionStart
# Replace selection for text.
@ -447,7 +444,7 @@ QR =
QR.nodes.fileInput.click()
fileInput: (files) ->
if @ instanceof Element #or files instanceof Event # file input
if files instanceof Event # file input
files = [@files...]
QR.nodes.fileInput.value = null # Don't hold the files from being modified on windows
{length} = files
@ -504,7 +501,7 @@ QR =
for elm in $$ '*', el
$.on elm, 'blur', QR.focusout
$.on elm, 'focus', QR.focusin
<% } %>
<% } %>
$.on el, 'click', @select.bind @
$.on @nodes.rm, 'click', (e) => e.stopPropagation(); @rm()
$.on @nodes.label, 'click', (e) => e.stopPropagation()
@ -553,7 +550,7 @@ QR =
@unlock()
rm: ->
$.rm @nodes.el
@delete()
index = QR.posts.indexOf @
if QR.posts.length is 1
new QR.post true
@ -561,7 +558,9 @@ QR =
else if @ is QR.selected
(QR.posts[index-1] or QR.posts[index+1]).select()
QR.posts.splice index, 1
return unless window.URL
QR.status()
delete: ->
$.rm @nodes.el
URL.revokeObjectURL @URL
lock: (lock=true) ->
@ -603,15 +602,18 @@ QR =
if input.type is 'checkbox'
@spoiler = input.checked
return
{value} = input
@[input.dataset.name] = value
return if input.nodeName isnt 'TEXTAREA'
@nodes.span.textContent = value
QR.characterCount()
# 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
{name} = input.dataset
@[name] = input.value
switch name
when 'thread'
QR.status()
when 'com'
@nodes.span.textContent = @com
QR.characterCount()
# 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: ->
return unless @ is QR.selected
@ -625,26 +627,15 @@ QR =
@filename = "#{file.name} (#{$.bytesToString file.size})"
@nodes.el.title = @filename
@nodes.label.hidden = false if QR.spoiler
URL.revokeObjectURL @URL if window.URL
URL.revokeObjectURL @URL
@showFileData()
unless /^image/.test file.type
@nodes.el.style.backgroundImage = null
return
@setThumbnail()
setThumbnail: (fileURL) ->
# XXX Opera does not support blob URL
setThumbnail: ->
# 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.onload = =>
@ -656,7 +647,7 @@ QR =
s *= 3 if @file.type is 'image/gif' # let them animate
{height, width} = img
if height < s or width < s
@URL = fileURL if window.URL
@URL = fileURL
@nodes.el.style.backgroundImage = "url(#{@URL})"
return
if height <= width
@ -669,10 +660,6 @@ QR =
cv.height = img.height = height
cv.width = img.width = width
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
applyBlob = (blob) =>
@URL = URL.createObjectURL blob
@ -690,6 +677,7 @@ QR =
applyBlob new Blob [ui8a], type: 'image/png'
fileURL = URL.createObjectURL @file
img.src = fileURL
rmFile: ->
@ -699,7 +687,6 @@ QR =
@nodes.el.style.backgroundImage = null
@nodes.label.hidden = true if QR.spoiler
@showFileData()
return unless window.URL
URL.revokeObjectURL @URL
showFileData: ->
@ -724,33 +711,26 @@ QR =
@nodes.span.textContent = @com
reader.readAsText file
dragStart: ->
$.addClass @, 'drag'
dragStart: -> $.addClass @, 'drag'
dragEnd: -> $.rmClass @, 'drag'
dragEnter: -> $.addClass @, 'over'
dragLeave: -> $.rmClass @, 'over'
dragEnd: ->
$.rmClass @, 'drag'
dragEnter: ->
$.addClass @, 'over'
dragLeave: ->
$.rmClass @, 'over'
dragOver: (e) ->
e.preventDefault()
e.dataTransfer.dropEffect = 'move'
drop: ->
el = $ '.drag', @parentNode
$.rmClass el, 'drag' # Opera doesn't fire dragEnd if we drop it on something else
$.rmClass @, 'over'
$.rmClass @, 'over'
return unless @draggable
el = $ '.drag', @parentNode
index = (el) -> [el.parentNode.children...].indexOf el
oldIndex = index el
newIndex = index @
(if oldIndex < newIndex then $.after else $.before) @, el
post = QR.posts.splice(oldIndex, 1)[0]
QR.posts.splice newIndex, 0, post
QR.status()
captcha:
init: ->
@ -779,19 +759,15 @@ QR =
img: imgContainer.firstChild
input: input
if window.MutationObserver
observer = new MutationObserver @load.bind @
observer.observe @nodes.challenge,
childList: true
else
$.on @nodes.challenge, 'DOMNodeInserted', @load.bind @
new MutationObserver(@load.bind @).observe @nodes.challenge,
childList: true
$.on imgContainer, 'click', @reload.bind @
$.on input, 'keydown', @keydown.bind @
$.on input, 'focus', -> $.addClass QR.nodes.el, 'focus'
$.on input, 'blur', -> $.rmClass QR.nodes.el, 'focus'
$.get 'captchas', [], (item) =>
@sync item['captchas']
$.get 'captchas', [], ({captchas}) =>
@sync captchas
$.sync 'captchas', @sync
# start with an uncached captcha
@reload()
@ -800,12 +776,13 @@ QR =
# XXX Firefox lacks focusin/focusout support.
$.on input, 'blur', QR.focusout
$.on input, 'focus', QR.focusin
<% } %>
<% } %>
$.addClass QR.nodes.el, 'has-captcha'
$.after QR.nodes.com.parentNode, [imgContainer, input]
sync: (@captchas) ->
sync: (captchas) ->
QR.captcha.captchas = captchas
QR.captcha.count()
getOne: ->
@ -922,10 +899,6 @@ QR =
# Add empty mimeType to avoid errors with URLs selected in Window's file dialog.
QR.mimeTypes.push ''
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]'
if QR.spoiler
@ -960,7 +933,7 @@ QR =
for elm in $$ '*', QR.nodes.el
$.on elm, 'blur', QR.focusout
$.on elm, 'focus', QR.focusin
<% } %>
<% } %>
$.on dialog, 'focusin', QR.focusin
$.on dialog, 'focusout', QR.focusout
$.on nodes.autohide, 'change', QR.toggleHide
@ -1109,11 +1082,6 @@ QR =
QR.status()
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
delete QR.req
@ -1206,15 +1174,15 @@ QR =
QR.cooldown.set {req, post, isReply}
if threadID is postID # new thread
URL = "/#{g.BOARD}/res/#{threadID}"
URL = if threadID is postID # new thread
"/#{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
URL = "/#{g.BOARD}/res/#{threadID}#p#{postID}"
"/#{g.BOARD}/res/#{threadID}#p#{postID}"
if URL
if Conf['Open Post in New Tab']
$.open "/#{g.BOARD}/res/#{threadID}"
$.open URL
else
window.location = "/#{g.BOARD}/res/#{threadID}"
window.location = URL
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
e.preventDefault()
{boardID, threadID, postID} = Get.postDataFromLink @
context = Get.contextFromLink @
context = Get.contextFromNode @
if $.hasClass @, 'inlined'
QuoteInline.rm @, boardID, threadID, postID, context
else
@ -107,4 +107,4 @@ QuoteInline =
{boardID, threadID, postID} = Get.postDataFromLink inlined
QuoteInline.rm inlined, boardID, threadID, postID, context
$.rmClass inlined, 'inlined'
return
return

View File

@ -22,8 +22,9 @@ QuotePreview =
qp = $.el 'div',
id: 'qp'
className: 'dialog'
$.add Header.hover, qp
Get.postClone boardID, threadID, postID, qp, Get.contextFromLink @
Get.postClone boardID, threadID, postID, qp, Get.contextFromNode @
UI.hover
root: @
@ -33,21 +34,6 @@ QuotePreview =
cb: QuotePreview.mouseout
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}"]
if Conf['Quote Highlighting']

View File

@ -21,6 +21,9 @@ Quotify =
if deadlink.parentNode.className is 'prettyprint'
# Don't quotify deadlinks inside code tags,
# 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...]
return
@ -48,9 +51,8 @@ Quotify =
target: '_blank'
textContent: "#{quote}\u00A0(Dead)"
a.setAttribute 'data-boardid', boardID
a.setAttribute 'data-threadid', post.thread.ID
a.setAttribute 'data-postid', postID
$.extend a.dataset, {boardID, threadID: post.thread.ID, postID}
else if redirect = Redirect.to 'thread', {boardID, threadID: 0, postID}
# Replace the .deadlink span if we can redirect.
a = $.el 'a',
@ -60,9 +62,8 @@ Quotify =
textContent: "#{quote}\u00A0(Dead)"
if Redirect.to 'post', {boardID, postID}
# Make it function as a normal quote if we can fetch the post.
$.addClass a, 'quotelink'
a.setAttribute 'data-boardid', boardID
a.setAttribute 'data-postid', postID
$.addClass a, 'quotelink'
$.extend a.dataset, {boardID, postID}
unless @quotes.contains quoteID
@quotes.push quoteID
@ -73,4 +74,4 @@ Quotify =
$.replace deadlink, a
if $.hasClass a, 'quotelink'
@nodes.quotelinks.push a
@nodes.quotelinks.push a