Merge branch 'v3' into Av2

Conflicts:
	builds/4chan-X.meta.js
	css/style.css
	lib/ui.coffee
	package.json
	src/features.coffee
	src/qr.coffee
This commit is contained in:
Zixaphir 2013-04-14 15:28:57 -07:00
commit 30880e499a
25 changed files with 27878 additions and 9406 deletions

1
.gitignore vendored
View File

@ -1,4 +1,3 @@
builds/
node_modules/ node_modules/
tmp/ tmp/
4chan_x.user.js 4chan_x.user.js

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,17 @@
- Fix resurrecting dead quotelinks on HTTP.
### 3.0.6 - *2013-04-14*
- Fix regression concerning thread selection when quoting on the index.
### 3.0.5 - *2013-04-14*
- `Scroll to Last Read Post` is now optional, enabled by default.
- The QR won't auto-hide when auto-hide is enabled and one of its input is focused. Doesn't work on Firefox.
- Added the `Remember QR Size` setting back in, disabled by default. Only on Firefox.
- Fix QR remembering the file spoiler state when it shouldn't.
- Fix QR cooldown in Opera.
### 3.0.4 - *2013-04-11* ### 3.0.4 - *2013-04-11*
- More minor fixes. - More minor fixes.
@ -9,7 +23,7 @@
### 3.0.2 - *2013-04-09* ### 3.0.2 - *2013-04-09*
- Added a setting in the Header's menu to move it at the bottom of the screen. - Added a setting in the Header's menu to move it at the bottom of the screen.
- Added Cooldown setting back in. - Added the `Cooldown` setting back in.
- Fixed the Header going above posts when following quotelinks for example. - Fixed the Header going above posts when following quotelinks for example.
- Fixed a bug where dead quotelinks would disappear. - Fixed a bug where dead quotelinks would disappear.

View File

@ -1,15 +1,16 @@
module.exports = (grunt) -> module.exports = (grunt) ->
pkg = grunt.file.readJSON 'package.json' pkg = grunt.file.readJSON 'package.json'
concatOptions =
process:
data: pkg
# Project configuration. # Project configuration.
grunt.initConfig grunt.initConfig
pkg: pkg pkg: pkg
concat: concat:
coffee: coffee:
options: options: concatOptions
process:
data: pkg
src: [ src: [
'src/config.coffee' 'src/config.coffee'
'src/globals.coffee' 'src/globals.coffee'
@ -27,9 +28,7 @@ module.exports = (grunt) ->
dest: 'tmp/script.coffee' dest: 'tmp/script.coffee'
crx: crx:
options: options: concatOptions
process:
data: pkg
files: files:
'builds/crx/manifest.json': 'src/manifest.json' 'builds/crx/manifest.json': 'src/manifest.json'
'builds/crx/script.js': [ 'builds/crx/script.js': [
@ -38,9 +37,7 @@ module.exports = (grunt) ->
] ]
userjs: userjs:
options: options: concatOptions
process:
data: pkg
src: [ src: [
'src/metadata.js' 'src/metadata.js'
'src/banner.js' 'src/banner.js'
@ -49,12 +46,10 @@ module.exports = (grunt) ->
dest: 'builds/<%= pkg.name %>.js' dest: 'builds/<%= pkg.name %>.js'
userscript: userscript:
options: options: concatOptions
process:
data: pkg
files: files:
'<%= pkg.name %>.meta.js': 'src/metadata.js' 'builds/<%= pkg.name %>.meta.js': 'src/metadata.js'
'<%= pkg.name %>.user.js': [ 'builds/<%= pkg.name %>.user.js': [
'src/metadata.js' 'src/metadata.js'
'src/banner.js' 'src/banner.js'
'tmp/script.js' 'tmp/script.js'
@ -170,22 +165,29 @@ module.exports = (grunt) ->
grunt.registerTask 'patch', [ grunt.registerTask 'patch', [
'bump' 'bump'
'reloadPkh'
'updcl:3' 'updcl:3'
] ]
grunt.registerTask 'minor', [ grunt.registerTask 'minor', [
'bump:minor' 'bump:minor'
'reloadPkh'
'updcl:2' 'updcl:2'
] ]
grunt.registerTask 'major', [ grunt.registerTask 'major', [
'bump:major' 'bump:major'
'reloadPkh'
'updcl:1' 'updcl:1'
] ]
grunt.registerTask 'updcl', 'Update the changelog', (i) -> grunt.registerTask 'reloadPkg', 'Reload the package', ->
# Update the `pkg` object with the new version. # Update the `pkg` object with the new version.
pkg = grunt.file.readJSON('package.json'); pkg = grunt.file.readJSON('package.json')
concatOptions.process.data = pkg
grunt.log.ok('pkg reloaded.')
grunt.registerTask 'updcl', 'Update the changelog', (i) ->
# i is the number of #s for markdown. # i is the number of #s for markdown.
version = [] version = []
version.length = +i + 1 version.length = +i + 1

File diff suppressed because one or more lines are too long

13596
builds/appchan-x.user.js Normal file

File diff suppressed because one or more lines are too long

BIN
builds/crx/icon128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 B

BIN
builds/crx/icon16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 B

BIN
builds/crx/icon48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 B

21
builds/crx/manifest.json Normal file
View File

@ -0,0 +1,21 @@
{
"name": "appchan x",
"version": "2.0.0",
"manifest_version": 2,
"description": "The most comprehensive 4chan userscript.",
"icons": {
"16": "icon16.png",
"48": "icon48.png",
"128": "icon128.png"
},
"content_scripts": [{
"js": ["script.js"],
"matches": ["*://api.4chan.org/*","*://boards.4chan.org/*","*://images.4chan.org/*","*://sys.4chan.org/*"],
"run_at": "document_start"
}],
"homepage_url": "http://zixaphir.github.com/appchan-x/",
"minimum_chrome_version": "26",
"permissions": [
"storage"
]
}

13521
builds/crx/script.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -6,9 +6,7 @@ body > div.navLinks > a:first-of-type::after,
#{if Conf['Announcements'] is 'slideout' then '#globalMessage::after,' else ''} #{if Conf['Announcements'] is 'slideout' then '#globalMessage::after,' else ''}
#boardNavDesktopFoot::after, #boardNavDesktopFoot::after,
body > a[style="cursor: pointer; float: right;"]::after, body > a[style="cursor: pointer; float: right;"]::after,
#imgControls .expand-all-shortcut, #img-controls,
#imgControls .contract-all-shortcut,
#imgControls .menu-button,
#catalog::after, #catalog::after,
#fappeTyme { #fappeTyme {
z-index: 18; z-index: 18;
@ -34,9 +32,7 @@ body > div.navLinks > a:first-of-type:hover,
#{if Conf['Announcements'] is 'slideout' then '#globalMessage:hover,' else ''} #{if Conf['Announcements'] is 'slideout' then '#globalMessage:hover,' else ''}
#boardNavDesktopFoot:hover, #boardNavDesktopFoot:hover,
body > a[style="cursor: pointer; float: right;"]:hover, body > a[style="cursor: pointer; float: right;"]:hover,
#imgControls .expand-all-shortcut, #img-controls,
#imgControls .contract-all-shortcut,
#imgControls .menu-button,
#catalog:hover { #catalog:hover {
z-index: 17; z-index: 17;
} }
@ -62,8 +58,7 @@ body > a[style="cursor: pointer; float: right;"]::after {
cursor: pointer; cursor: pointer;
background-position: 0 -75px; background-position: 0 -75px;
} }
#imgControls .expand-all-shortcut, #img-controls {
#imgControls .contract-all-shortcut {
background-position: 0 -90px; background-position: 0 -90px;
} }
#navtopright .exlinksOptionsLink::after { #navtopright .exlinksOptionsLink::after {
@ -78,9 +73,7 @@ body > a[style="cursor: pointer; float: right;"]::after {
} }
#boardNavDesktopFoot:hover::after, #boardNavDesktopFoot:hover::after,
#globalMessage:hover::after, #globalMessage:hover::after,
#imgControls .expand-all-shortcut:hover, #img-controls:hover,
#imgControls .contract-all-shortcut:hover,
#imgControls .menu-button:hover,
#navlinks a:hover, #navlinks a:hover,
#appchanOptions:hover, #appchanOptions:hover,
#navtopright .exlinksOptionsLink:hover::after, #navtopright .exlinksOptionsLink:hover::after,

View File

@ -24,12 +24,7 @@ body > a[style="cursor: pointer; float: right;"]::after {
#{align}: #{position[i++]}px; #{align}: #{position[i++]}px;
} }
/* Expand Images */ /* Expand Images */
#imgControls .expand-all-shortcut, #img-controls {
#imgControls .contract-all-shortcut {
#{align}: #{position[i++]}px;
}
/* Expand Images Menu */
#imgControls .menu-button {
#{align}: #{position[i++]}px; #{align}: #{position[i++]}px;
} }
/* 4chan Catalog */ /* 4chan Catalog */
@ -66,9 +61,7 @@ div.navLinks > a:first-of-type::after {
#appchanOptions, #appchanOptions,
#watcher::after, #watcher::after,
#globalMessage::after, #globalMessage::after,
#imgControls .expand-all-shortcut, #img-controls,
#imgControls .contract-all-shortcut,
#imgControls .menu-button,
#fappeTyme, #fappeTyme,
div.navLinks > a:first-of-type::after, div.navLinks > a:first-of-type::after,
#catalog::after, #catalog::after,

View File

@ -1,11 +1,6 @@
/* Expand Images */ /* Expand Images */
#imgControls .expand-all-shortcut, #img-controls {
#imgControls .contract-all-shortcut {
top: #{position[i++]}px;
}
/* Expand Images Menu */
#imgControls .menu-button {
top: #{position[i++]}px; top: #{position[i++]}px;
} }
/* 4chan X Options */ /* 4chan X Options */
@ -64,9 +59,7 @@ div.navLinks > a:first-of-type::after {
#appchanOptions, #appchanOptions,
#boardNavDesktopFoot::after, #boardNavDesktopFoot::after,
#globalMessage::after, #globalMessage::after,
#imgControls .expand-all-shortcut, #img-controls,
#imgControls .contract-all-shortcut,
#imgControls .menu-button,
#fappeTyme, #fappeTyme,
#{if _conf["Slideout Watcher"] then "#watcher::after," else ""} #{if _conf["Slideout Watcher"] then "#watcher::after," else ""}
body > a[style="cursor: pointer; float: right;"]::after, body > a[style="cursor: pointer; float: right;"]::after,

View File

@ -118,7 +118,7 @@ hr {
opacity: 0.5; opacity: 0.5;
} }
/* Symbols */ /* Symbols */
.dropmarker { .drop-marker {
vertical-align: middle; vertical-align: middle;
display: inline-block; display: inline-block;
margin: 2px 2px 3px; margin: 2px 2px 3px;
@ -126,6 +126,12 @@ hr {
border-right: .3em solid transparent; border-right: .3em solid transparent;
border-left: .3em solid transparent; border-left: .3em solid transparent;
} }
.brackets-wrap::before {
content: "\\00a0[";
}
.brackets-wrap::after {
content: "]\\00a0";
}
/* Thread / Reply Nav */ /* Thread / Reply Nav */
#navlinks a { #navlinks a {
position: fixed; position: fixed;
@ -140,30 +146,7 @@ hr {
#boardNavDesktop { #boardNavDesktop {
z-index: 6; z-index: 6;
border-width: 1px; border-width: 1px;
#{{
"sticky top": "
position: fixed;
top: 0;
border-top-width: 0;
#{if _conf["Rounded Edges"] then "border-radius: 0 0 3px 3px;" else ""}"
"sticky bottom": "
position: fixed;
bottom: 0;
border-bottom-width: 0;
#{if _conf["Rounded Edges"] then "border-radius: 3px 3px 0 0;" else ""}"
"top": "
position: absolute; position: absolute;
top: 0;
border-top-width: 0;
#{if _conf["Rounded Edges"] then "border-radius: 0 0 3px 3px;" else ""}"
"hide": "
position: fixed;
top: 110%;"
}[_conf['Boards Navigation']]}
#{ #{
if _conf['4chan SS Navigation'] then " if _conf['4chan SS Navigation'] then "
left: 0; left: 0;
@ -182,6 +165,71 @@ else "
" else ""} " else ""}
text-align: #{_conf["Navigation Alignment"]}; text-align: #{_conf["Navigation Alignment"]};
} }
.fixed #boardNavDesktop {
position: fixed;
}
.top #boardNavDesktop {
top: 0;
border-top-width: 0;
#{if _conf["Rounded Edges"] then "border-radius: 0 0 3px 3px;" else ""}"
}
.fixed.bottom #boardNavDesktop {
bottom: 0;
border-bottom-width: 0;
#{if _conf["Rounded Edges"] then "border-radius: 3px 3px 0 0;" else ""}"
}
.hide #boardNavDesktop {
position: fixed;
top: 110%;
bottom: auto;
}
/* Header Autohide */
.fixed #boardNavDesktop.autohide:not(:hover) {
box-shadow: none;
transition: all .8s .6s cubic-bezier(.55, .055, .675, .19);
}
.fixed.top #boardNavDesktop.autohide:not(:hover) {
margin-bottom: -1em;
-webkit-transform: translateY(-100%);
transform: translateY(-100%);
}
.fixed.bottom #boardNavDesktop.autohide:not(:hover) {
-webkit-transform: translateY(100%);
transform: translateY(100%);
}
#toggle-header-bar {
left: 0;
right: 0;
height: 10px;
position: absolute;
}
#boardNavDesktop #toggle-header-bar {
display: none;
}
.fixed #boardNavDesktop #toggle-header-bar {
display: block;
}
.fixed #boardNavDesktop #toggle-header-bar {
cursor: n-resize;
}
.fixed.top boardNavDesktop #toggle-header-bar {
top: 100%;
}
.fixed.bottom #boardNavDesktop #toggle-header-bar {
bottom: 100%;
}
.fixed #boardNavDesktop #header-bar.autohide #toggle-header-bar {
cursor: s-resize;
}
/* Notifications */
.notification {
display: block;
overflow: hidden;
width: 300px;
}
.close {
float: right;
}
/* Pagination */ /* Pagination */
.pagelist { .pagelist {
border-width: 1px; border-width: 1px;
@ -443,27 +491,6 @@ else "
z-index: 10; z-index: 10;
} }
/* Image Expansion */ /* Image Expansion */
#imgControls a.menu-button {
margin: 0;
margin: 1px;
border: 2px solid;
border-radius: 10px;
height: 14px;
width: 14px;
#{Style.sizing}: border-box;
}
#imgControls .menu-button::after {
content: '';
font-size: 10px;
position: absolute;
bottom: 50%;
right: 50%;
#{agent}transform: translate(50%, 60%);
display: block;
border-top: 6px solid rgb(130, 130, 130);
border-left: 3px solid transparent;
border-right: 3px solid transparent;
}
.fit-width .full-image { .fit-width .full-image {
max-width: 100%; max-width: 100%;
width: 100%; width: 100%;
@ -691,7 +718,7 @@ hide: "
.menu-button { .menu-button {
position: relative; position: relative;
} }
.menu-button, .post .menu-button,
.hide-thread-button, .hide-thread-button,
.hide-reply-button { .hide-reply-button {
float: right; float: right;
@ -1037,7 +1064,7 @@ input:checked + .rice {
#{agent}transform: translateX(#{xOffset}93%); #{agent}transform: translateX(#{xOffset}93%);
} }
#qr:hover, #qr:hover,
#qr.focus, #qr.has-focus,
#qr.dump { #qr.dump {
#{agent}transform: translate(0); #{agent}transform: translate(0);
}" }"
@ -1051,7 +1078,7 @@ input:checked + .rice {
#{agent}transform: translateX(#{xOffset}100%); #{agent}transform: translateX(#{xOffset}100%);
} }
#qr:hover, #qr:hover,
#qr.focus, #qr.has-focus,
#qr.dump { #qr.dump {
#{agent}transform: translateX(0); #{agent}transform: translateX(0);
} }
@ -1067,7 +1094,7 @@ input:checked + .rice {
cursor: default; cursor: default;
} }
#qr:hover #qrtab, #qr:hover #qrtab,
#qr.focus #qrtab, #qr.has-focus #qrtab,
#qr.dump #qrtab { #qr.dump #qrtab {
opacity: 0; opacity: 0;
#{Style.sidebarLocation[0]}: #{252 + Style.sidebarOffset.W}px; #{Style.sidebarLocation[0]}: #{252 + Style.sidebarOffset.W}px;
@ -1090,7 +1117,7 @@ input:checked + .rice {
#{agent}transition: opacity .3s ease-in-out 1s; #{agent}transition: opacity .3s ease-in-out 1s;
} }
#qr:hover, #qr:hover,
#qr.focus, #qr.has-focus,
#qr.dump { #qr.dump {
opacity: 1; opacity: 1;
#{agent}transition: opacity .3s linear; #{agent}transition: opacity .3s linear;
@ -1103,7 +1130,7 @@ unless _conf['Post Form Style'] is 'tabbed slideout'
unless _conf['Post Form Style'] is 'float' or _conf['Show Post Form Header'] unless _conf['Post Form Style'] is 'float' or _conf['Show Post Form Header']
"#qrtab { display: none; }" "#qrtab { display: none; }"
else unless _conf['Post Form Style'] is 'slideout' else unless _conf['Post Form Style'] is 'slideout'
".autohide:not(:hover):not(.focus) > form { display: none !important; }" ".autohide:not(:hover):not(.has-focus) > form { display: none !important; }"
else "" else ""
) + "#qrtab { margin-bottom: 1px; }" ) + "#qrtab { margin-bottom: 1px; }"
else ""} else ""}
@ -1114,7 +1141,7 @@ if _conf['Post Form Style'] isnt 'float' and _conf["Post Form Slideout Transitio
#{agent}transition: #{agent}transform .3s ease-in-out 1s; #{agent}transition: #{agent}transform .3s ease-in-out 1s;
} }
#qr:hover, #qr:hover,
#qr.focus, #qr.has-focus,
#qr.dump { #qr.dump {
#{agent}transition: #{agent}transform .3s linear; #{agent}transition: #{agent}transform .3s linear;
} }

View File

@ -211,10 +211,6 @@ a[style="cursor: pointer; float: right;"] ~ div[style^="width: 100%;"] > table {
#navlinks a:last-of-type { #navlinks a:last-of-type {
border-top: 11px solid rgb(130,130,130); border-top: 11px solid rgb(130,130,130);
} }
#imgControls .menu-button {
border-color: rgb(130,130,130);
color: rgb(130,130,130);
}
#charCount { #charCount {
color: #{(if Style.lightTheme then "rgba(0,0,0,0.7)" else "rgba(255,255,255,0.7)")}; color: #{(if Style.lightTheme then "rgba(0,0,0,0.7)" else "rgba(255,255,255,0.7)")};
} }
@ -396,8 +392,7 @@ a .name {
#globalMessage::after, #globalMessage::after,
#boardNavDesktopFoot::after, #boardNavDesktopFoot::after,
a[style="cursor: pointer; float: right;"]::after, a[style="cursor: pointer; float: right;"]::after,
#imgControls .expand-all-shortcut, #img-controls,
#imgControls .contract-all-shortcut,
#catalog::after, #catalog::after,
#fappeTyme { #fappeTyme {
background-image: url('#{icons}'); background-image: url('#{icons}');

View File

@ -140,8 +140,16 @@ $.extend $,
el.classList.toggle className el.classList.toggle className
hasClass: (el, className) -> hasClass: (el, className) ->
el.classList.contains className el.classList.contains className
rm: (el) -> rm: do ->
el.parentNode.removeChild el if 'remove' of Element.prototype
(el) -> el.remove()
else
(el) -> el.parentNode?.removeChild el
rmAll: (root) ->
# jsperf.com/emptify-element
while node = root.firstChild
$.rm node
return
tn: (s) -> tn: (s) ->
d.createTextNode s d.createTextNode s
frag: -> frag: ->

View File

@ -4,7 +4,8 @@ UI = do ->
className: 'dialog' className: 'dialog'
innerHTML: html innerHTML: html
id: id id: id
el.style.cssText = localStorage.getItem("#{g.NAMESPACE}#{id}.position") or position $.get "#{id}.position", position, (item) ->
el.style.cssText = item["#{id}.position"]
move = $ '.move', el move = $ '.move', el
$.on move, 'touchstart mousedown', dragstart $.on move, 'touchstart mousedown', dragstart
for child in move.children for child in move.children
@ -280,7 +281,7 @@ UI = do ->
else # mouseup else # mouseup
$.off d, 'mousemove', @move $.off d, 'mousemove', @move
$.off d, 'mouseup', @up $.off d, 'mouseup', @up
localStorage.setItem "#{g.NAMESPACE}#{@id}.position", @style.cssText $.set "#{@id}.position", @style.cssText
hoverstart = ({root, el, latestEvent, endEvents, asapTest, cb, close}) -> hoverstart = ({root, el, latestEvent, endEvents, asapTest, cb, close}) ->
o = { o = {

View File

@ -240,7 +240,6 @@ Style =
$ '#navtopright .exlinksOptionsLink', d.body $ '#navtopright .exlinksOptionsLink', d.body
notCatalog and $ 'body > a[style="cursor: pointer; float: right;"]', d.body notCatalog and $ 'body > a[style="cursor: pointer; float: right;"]', d.body
notEither and _conf['Image Expansion'] notEither and _conf['Image Expansion']
notEither and _conf['Image Expansion']
notEither notEither
g.VIEW is 'thread' g.VIEW is 'thread'
notEither and _conf['Fappe Tyme'] notEither and _conf['Fappe Tyme']
@ -265,7 +264,6 @@ Style =
position = aligner( position = aligner(
2 + (if _conf["4chan Banner"] is "at sidebar top" then (Style.logoOffset + 19) else 0) 2 + (if _conf["4chan Banner"] is "at sidebar top" then (Style.logoOffset + 19) else 0)
[ [
notEither and _conf['Image Expansion']
notEither and _conf['Image Expansion'] notEither and _conf['Image Expansion']
notCatalog notCatalog
_conf['Slideout Navigation'] isnt 'hide' _conf['Slideout Navigation'] isnt 'hide'
@ -314,12 +312,11 @@ Style =
Style.padding.pages.property = _conf["Pagination"].split(" ") Style.padding.pages.property = _conf["Pagination"].split(" ")
Style.padding.pages.property = Style.padding.pages.property[Style.padding.pages.property.length - 1] Style.padding.pages.property = Style.padding.pages.property[Style.padding.pages.property.length - 1]
css = "body::before {\n" css = "body::before {\n"
if _conf["4chan SS Emulation"] if Style.padding.pages and (_conf["Pagination"] is "sticky top" or _conf["Pagination"] is "sticky bottom")
if Style.padding.pages and (_conf["Pagination"] is "sticky top" or _conf["Pagination"] is "sticky bottom") css += " #{Style.padding.pages.property}: #{Style.padding.pages.offsetHeight}px !important;\n"
css += " #{Style.padding.pages.property}: #{Style.padding.pages.offsetHeight}px !important;\n"
if _conf["Boards Navigation"] is "sticky top" or _conf["Boards Navigation"] is "sticky bottom" if _conf["Boards Navigation"] is "sticky top" or _conf["Boards Navigation"] is "sticky bottom"
css += " #{Style.padding.nav.property}: #{Style.padding.nav.offsetHeight}px !important;\n" css += " #{Style.padding.nav.property}: #{Style.padding.nav.offsetHeight}px !important;\n"
css += """ css += """
} }

View File

@ -13,6 +13,10 @@ Config =
false false
'Compatibility between <%= meta.name %> and 4chan\'s inline extension is NOT guaranteed.' 'Compatibility between <%= meta.name %> and 4chan\'s inline extension is NOT guaranteed.'
] ]
'Fixed Header': [
false
'Mayhem X\'s Fixed Header (kinda).'
]
'Custom Board Navigation': [ 'Custom Board Navigation': [
false false
'Show custom links instead of the full board list.' 'Show custom links instead of the full board list.'
@ -225,6 +229,12 @@ Config =
false false
'Remember the subject field, instead of resetting after posting.' 'Remember the subject field, instead of resetting after posting.'
] ]
<% if (type === 'userscript') { %>
'Remember QR Size': [
false
'Remember the size of the Quick reply.'
]
<% } %>
'Remember Spoiler': [ 'Remember Spoiler': [
false false
'Remember the spoiler state, instead of resetting after posting.' 'Remember the spoiler state, instead of resetting after posting.'
@ -586,11 +596,6 @@ Config =
] ]
Navigation: Navigation:
'Boards Navigation': [
'sticky top'
'The position of 4chan board navigation'
['sticky top', 'sticky bottom', 'top', 'hide']
]
'Navigation Alignment': [ 'Navigation Alignment': [
'center' 'center'
'Change the text alignment of the navigation.' 'Change the text alignment of the navigation.'
@ -716,7 +721,11 @@ http://iqdb.org/?url=%TURL
#//archive.installgentoo.net/%board/image/%MD5;text:View same on installgentoo /%board/ #//archive.installgentoo.net/%board/image/%MD5;text:View same on installgentoo /%board/
""" """
'Custom CSS': false 'Boards Navigation': 'sticky top'
'Custom CSS': false
'Bottom header': false
'Header auto-hide': false 'Header auto-hide': false

View File

@ -1,13 +1,37 @@
Header = Header =
init: -> init: ->
@bar = $.el 'div', @menuButton = $.el 'span',
id: 'notifications' className: 'menu-button'
@shortcuts = $.el 'span', innerHTML: '<a class=brackets-wrap href=javascript:;><i class=drop-marker></i></a>'
id: 'shortcuts'
@hover = $.el 'div',
id: 'hoverUI'
$.on window, 'load hashchange', Header.hashScroll @menu = new UI.Menu 'header'
$.on @menuButton, 'click', @menuToggle
$.on @toggle, 'mousedown', @toggleBarVisibility
$.on window, 'load hashchange', Header.hashScroll
@positionToggler = $.el 'span',
textContent: 'Header Position'
className: 'header-position-link'
{createSubEntry} = Header
subEntries = []
for setting in ['sticky top', 'sticky bottom', 'top', 'hide']
subEntries.push createSubEntry setting
$.event 'AddMenuEntry',
type: 'header'
el: @positionToggler
order: 108
subEntries: subEntries
@headerToggler = $.el 'label',
innerHTML: "<input type=checkbox #{if Conf['Header auto-hide'] then 'checked' else ''}> Auto-hide header"
$.on @headerToggler.firstElementChild, 'change', @toggleBarVisibility
$.event 'AddMenuEntry',
type: 'header'
el: @headerToggler
order: 109
$.on d, 'CreateNotification', @createNotification $.on d, 'CreateNotification', @createNotification
@ -20,18 +44,48 @@ Header =
$.ready -> $.ready ->
$.add d.body, Header.hover $.add d.body, Header.hover
setBoardList: -> bar: $.el 'div',
id: 'notifications'
shortcuts: $.el 'span',
id: 'shortcuts'
hover: $.el 'div',
id: 'hoverUI'
toggle: $.el 'div',
id: 'toggle-header-bar'
createSubEntry: (setting)->
label = $.el 'label',
textContent: "#{setting}"
$.on label, 'click', Header.setBarPosition
el: label
setBoardList: ->
Header.nav = nav = $.id 'boardNavDesktop' Header.nav = nav = $.id 'boardNavDesktop'
if a = $ "a[href*='/#{g.BOARD}/']", nav if a = $ "a[href*='/#{g.BOARD}/']", nav
a.className = 'current' a.className = 'current'
fullBoardList = $.el 'span', fullBoardList = $.el 'span',
id: 'full-board-list' id: 'full-board-list'
hidden: true hidden: true
customBoardList = $.el 'span', customBoardList = $.el 'span',
id: 'custom-board-list' id: 'custom-board-list'
Header.setBarPosition.call textContent: "#{Conf['Boards Navigation']}"
$.sync 'Boards Navigation', ->
Header.setBarPosition.call textContent: "#{Conf['Boards Navigation']}"
Header.setBarVisibility Conf['Header auto-hide']
$.sync 'Header auto-hide', Header.setBarVisibility
$.add fullBoardList, [nav.childNodes...] $.add fullBoardList, [nav.childNodes...]
$.add nav, [customBoardList, fullBoardList, Header.shortcuts, $ '#navtopright', fullBoardList, Header.bar] $.add nav, [Header.menuButton, customBoardList, fullBoardList, Header.shortcuts, $('#navtopright', fullBoardList), Header.toggle]
$.add d.body, Header.bar
if Conf['Custom Board Navigation'] if Conf['Custom Board Navigation']
Header.generateBoardList Conf['boardnav'] Header.generateBoardList Conf['boardnav']
@ -47,7 +101,7 @@ Header =
generateBoardList: (text) -> generateBoardList: (text) ->
list = $ '#custom-board-list', Header.nav list = $ '#custom-board-list', Header.nav
list.innerHTML = null $.rmAll list
return unless text return unless text
as = $$('#full-board-list a', Header.nav)[0...-2] # ignore the Settings and Home links as = $$('#full-board-list a', Header.nav)[0...-2] # ignore the Settings and Home links
nodes = text.match(/[\w@]+(-(all|title|full|index|catalog|text:"[^"]+"))*|[^\w@]+/g).map (t) -> nodes = text.match(/[\w@]+(-(all|title|full|index|catalog|text:"[^"]+"))*|[^\w@]+/g).map (t) ->
@ -92,8 +146,41 @@ Header =
custom.hidden = !showBoardList custom.hidden = !showBoardList
full.hidden = showBoardList full.hidden = showBoardList
setBarPosition: ->
$.event 'CloseMenu'
switch @textContent
when 'sticky top'
$.addClass doc, 'top'
$.addClass doc, 'fixed'
$.rmClass doc, 'bottom'
$.rmClass doc, 'hide'
when 'sticky bottom'
$.rmClass doc, 'top'
$.addClass doc, 'fixed'
$.addClass doc, 'bottom'
$.rmClass doc, 'hide'
when 'top'
$.addClass doc, 'top'
$.rmClass doc, 'fixed'
$.rmClass doc, 'bottom'
$.rmClass doc, 'hide'
when 'hide'
$.rmClass doc, 'top'
$.rmClass doc, 'fixed'
$.rmClass doc, 'bottom'
$.addClass doc, 'hide'
Conf['Boards Navigation'] = @textContent
$.set 'Boards Navigation', @textContent
setBarVisibility: (hide) ->
Header.headerToggler.firstElementChild.checked = hide
(if hide then $.addClass else $.rmClass) Header.nav, 'autohide'
hashScroll: -> hashScroll: ->
return unless post = $.id @location.hash[1..] return unless post = $.id @location.hash[1..]
return if (Get.postFromRoot post).isHidden
Header.scrollToPost post Header.scrollToPost post
scrollToPost: (post) -> scrollToPost: (post) ->
@ -103,12 +190,29 @@ Header =
top += - headRect.top - headRect.height top += - headRect.top - headRect.height
(if $.engine is 'webkit' then d.body else doc).scrollTop += top (if $.engine is 'webkit' then d.body else doc).scrollTop += top
toggleBarVisibility: (e) ->
return if e.type is 'mousedown' and e.button isnt 0 # not LMB
hide = if @nodeName is 'INPUT'
@checked
else
!$.hasClass Header.nav, 'autohide'
Header.setBarVisibility hide
message = if hide
'The header bar will automatically hide itself.'
else
'The header bar will remain visible.'
new Notification 'info', message, 2
$.set 'Header auto-hide', hide
addShortcut: (el) -> addShortcut: (el) ->
shortcut = $.el 'span', shortcut = $.el 'span',
className: 'shortcut' className: 'shortcut'
$.add shortcut, [$.tn(' ['), el, $.tn(']')] $.add shortcut, [$.tn(' ['), el, $.tn(']')]
$.add Header.shortcuts, shortcut $.add Header.shortcuts, shortcut
menuToggle: (e) ->
Header.menu.toggle e, @, g
createNotification: (e) -> createNotification: (e) ->
{type, content, lifetime, cb} = e.detail {type, content, lifetime, cb} = e.detail
notif = new Notification type, content, lifetime notif = new Notification type, content, lifetime
@ -144,7 +248,7 @@ class Notification
setTimeout @close, @timeout * $.SECOND if @timeout setTimeout @close, @timeout * $.SECOND if @timeout
close = -> close = ->
$.rm @el if @el.parentNode $.rm @el
CatalogLinks = CatalogLinks =
init: -> init: ->
@ -193,7 +297,6 @@ CatalogLinks =
else else
"//boards.4chan.org/#{board}/catalog" "//boards.4chan.org/#{board}/catalog"
) )
ready: -> ready: ->
if catalogLink = ($('.pages.cataloglink a', d.body) or $ '[href=".././catalog"]', d.body) if catalogLink = ($('.pages.cataloglink a', d.body) or $ '[href=".././catalog"]', d.body)
if g.VIEW isnt 'thread' if g.VIEW isnt 'thread'
@ -967,7 +1070,7 @@ Menu =
(post) -> (post) ->
a or= $.el 'a', a or= $.el 'a',
className: 'menu-button' className: 'menu-button'
innerHTML: '[<span class=dropmarker></span>]' innerHTML: '[<span class=drop-marker></span>]'
href: 'javascript:;' href: 'javascript:;'
clone = a.cloneNode true clone = a.cloneNode true
clone.setAttribute 'data-postid', post.fullID clone.setAttribute 'data-postid', post.fullID
@ -1008,7 +1111,7 @@ ReportLink =
DeleteLink = DeleteLink =
init: -> init: ->
return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Delete Link'] or !Conf['Quick Reply'] return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Delete Link']
div = $.el 'div', div = $.el 'div',
className: 'delete-link' className: 'delete-link'
@ -1060,20 +1163,22 @@ DeleteLink =
else else
$.id('delPassword').value $.id('delPassword').value
fileOnly = $.hasClass @, 'delete-file'
form = form =
mode: 'usrdel' mode: 'usrdel'
onlyimgdel: $.hasClass @, 'delete-file' onlyimgdel: fileOnly
pwd: pwd pwd: pwd
form[post.ID] = 'delete' form[post.ID] = 'delete'
link = @ link = @
$.ajax $.id('delform').action.replace("/#{g.BOARD}/", "/#{post.board}/"), $.ajax $.id('delform').action.replace("/#{g.BOARD}/", "/#{post.board}/"),
onload: -> DeleteLink.load link, post, @response onload: -> DeleteLink.load link, post, fileOnly, @response
onerror: -> DeleteLink.error link onerror: -> DeleteLink.error link
, ,
cred: true cred: true
form: $.formData form form: $.formData form
load: (link, post, html) -> load: (link, post, fileOnly, html) ->
tmpDoc = d.implementation.createHTMLDocument '' tmpDoc = d.implementation.createHTMLDocument ''
tmpDoc.documentElement.innerHTML = html tmpDoc.documentElement.innerHTML = html
if tmpDoc.title is '4chan - Banned' # Ban/warn check if tmpDoc.title is '4chan - Banned' # Ban/warn check
@ -1084,7 +1189,7 @@ DeleteLink =
else else
if tmpDoc.title is 'Updating index...' if tmpDoc.title is 'Updating index...'
# We're 100% sure. # We're 100% sure.
(post.origin or post).kill() (post.origin or post).kill fileOnly
s = 'Deleted' s = 'Deleted'
link.textContent = s link.textContent = s
error: (link) -> error: (link) ->
@ -1360,7 +1465,7 @@ Keybinds =
location.href = url location.href = url
hl: (delta, thread) -> hl: (delta, thread) ->
if Conf['Bottom header'] if Conf['Fixed Header'] and Conf['Bottom header']
topMargin = 0 topMargin = 0
else else
headRect = Header.bar.getBoundingClientRect() headRect = Header.bar.getBoundingClientRect()
@ -1398,7 +1503,13 @@ Keybinds =
Nav = Nav =
init: -> init: ->
return if g.VIEW is 'index' and !Conf['Index Navigation'] or g.VIEW is 'thread' and !Conf['Reply Navigation'] switch g.VIEW
when 'index'
return unless Conf['Index Navigation']
when 'thread'
return unless Conf['Reply Navigation']
else # catalog
return
span = $.el 'span', span = $.el 'span',
id: 'navlinks' id: 'navlinks'
@ -1470,11 +1581,13 @@ Redirect =
when 'c' when 'c'
"//archive.nyafuu.org/#{boardID}/full_image/#{filename}" "//archive.nyafuu.org/#{boardID}/full_image/#{filename}"
post: (boardID, postID) -> post: (boardID, postID) ->
# XXX foolz had HSTS set for 120 days, which broke XHR+CORS+Redirection when on HTTP.
# Remove necessary HTTPS procotol in September 2013.
switch boardID switch boardID
when 'a', 'co', 'gd', 'jp', 'm', 'q', 'sp', 'tg', 'tv', 'v', 'vg', 'vp', 'vr', 'wsg' when 'a', 'co', 'gd', 'jp', 'm', 'q', 'sp', 'tg', 'tv', 'v', 'vg', 'vp', 'vr', 'wsg'
"//archive.foolz.us/_/api/chan/post/?board=#{boardID}&num=#{postID}" "https://archive.foolz.us/_/api/chan/post/?board=#{boardID}&num=#{postID}"
when 'u' when 'u'
"//nsfw.foolz.us/_/api/chan/post/?board=#{boardID}&num=#{postID}" "https://nsfw.foolz.us/_/api/chan/post/?board=#{boardID}&num=#{postID}"
when 'c', 'int', 'out', 'po' when 'c', 'int', 'out', 'po'
"//archive.thedarkcave.org/_/api/chan/post/?board=#{boardID}&num=#{postID}" "//archive.thedarkcave.org/_/api/chan/post/?board=#{boardID}&num=#{postID}"
# for fuuka-based archives: # for fuuka-based archives:
@ -1490,9 +1603,9 @@ Redirect =
Redirect.path '//archive.thedarkcave.org', 'foolfuuka', data Redirect.path '//archive.thedarkcave.org', 'foolfuuka', data
when 'ck', 'fa', 'lit', 's4s' when 'ck', 'fa', 'lit', 's4s'
Redirect.path '//fuuka.warosu.org', 'fuuka', data Redirect.path '//fuuka.warosu.org', 'fuuka', data
when 'diy', 'sci' when 'diy', 'g', 'sci'
Redirect.path '//archive.installgentoo.net', 'fuuka', data Redirect.path '//archive.installgentoo.net', 'fuuka', data
when 'cgl', 'g', 'mu', 'w' when 'cgl', 'mu', 'w'
Redirect.path '//rbt.asia', 'fuuka', data Redirect.path '//rbt.asia', 'fuuka', data
when 'an', 'fit', 'k', 'mlp', 'r9k', 'toy', 'x' when 'an', 'fit', 'k', 'mlp', 'r9k', 'toy', 'x'
Redirect.path 'http://archive.heinessen.com', 'fuuka', data Redirect.path 'http://archive.heinessen.com', 'fuuka', data
@ -1865,10 +1978,10 @@ Get =
# Get rid of the side arrows. # Get rid of the side arrows.
{nodes} = clone {nodes} = clone
nodes.root.innerHTML = null $.rmAll nodes.root
$.add nodes.root, nodes.post $.add nodes.root, nodes.post
root.innerHTML = null $.rmAll root
$.add root, nodes.root $.add root, nodes.root
fetchedPost: (req, boardID, threadID, postID, root, context) -> fetchedPost: (req, boardID, threadID, postID, root, context) ->
# In case of multiple callbacks for the same request, # In case of multiple callbacks for the same request,
@ -2662,41 +2775,21 @@ ImageExpand =
init: -> init: ->
return if g.VIEW is 'catalog' or !Conf['Image Expansion'] return if g.VIEW is 'catalog' or !Conf['Image Expansion']
wrapper = $.el 'div', @EAI = $.el 'a',
id: 'imgControls' id: 'img-controls'
innerHTML: """ className: 'expand-all-shortcut'
<a class='expand-all-shortcut' title='Expand All Images' href='javascript:;'></a> title: 'Expand All Images'
<a class='menu-button' href='javascript:;'></a> href: 'javascript:;'
"""
@EAI = wrapper.firstElementChild
$.on @EAI, 'click', ImageExpand.cb.toggleAll $.on @EAI, 'click', ImageExpand.cb.toggleAll
@opmenu = new UI.Menu 'imageexpand'
$.on $('.menu-button', wrapper), 'click', @menuToggle
for type, config of Config.imageExpansion
label = $.el 'label',
innerHTML: "<input type=checkbox name='#{type}'> #{type}"
input = label.firstElementChild
if ['Fit width', 'Fit height'].contains type
$.on input, 'change', ImageExpand.cb.setFitness
if config
label.title = config[1]
input.checked = Conf[type]
$.event 'change', null, input
$.on input, 'change', $.cb.checked
$.event 'AddMenuEntry',
type: 'imageexpand'
el: label
$.asap (-> $.id 'delform'), ->
$.prepend $.id('delform'), wrapper
Post::callbacks.push Post::callbacks.push
name: 'Image Expansion' name: 'Image Expansion'
cb: @node cb: @node
$.asap (-> $.id 'delform'), ->
$.prepend $.id('delform'), ImageExpand.EAI
node: -> node: ->
return unless @file?.isImage return unless @file?.isImage
{thumb} = @file {thumb} = @file
@ -2843,7 +2936,7 @@ ImageExpand =
el = $.el 'span', el = $.el 'span',
textContent: 'Image Expansion' textContent: 'Image Expansion'
className: 'image-expansion-link' className: 'image-expansion-link'
{createSubEntry} = ImageExpand.menu {createSubEntry} = ImageExpand.menu
subEntries = [] subEntries = []
@ -3190,18 +3283,22 @@ Unread =
threadID: @ID threadID: @ID
defaultValue: 0 defaultValue: 0
Unread.addPosts posts Unread.addPosts posts
if (hash = location.hash.match /\d+/) and post = @posts[hash[0]]
Header.scrollToPost post.nodes.root
else if Unread.posts.length
# Scroll to before the first unread post.
$.x('preceding-sibling::div[contains(@class,"postContainer")][1]', Unread.posts[0].nodes.root).scrollIntoView false
else if posts.length
# Scroll to the last read post.
Header.scrollToPost posts[posts.length - 1].nodes.root
$.on d, 'ThreadUpdate', Unread.onUpdate $.on d, 'ThreadUpdate', Unread.onUpdate
$.on d, 'scroll visibilitychange', Unread.read $.on d, 'scroll visibilitychange', Unread.read
$.on d, 'visibilitychange', Unread.setLine if Conf['Unread Line'] $.on d, 'visibilitychange', Unread.setLine if Conf['Unread Line']
return unless Conf['Scroll to Last Read Post']
# Let the header's onload callback handle it.
return if (hash = location.hash.match /\d+/) and hash[0] of @posts
if Unread.posts.length
# Scroll to before the first unread post.
while root = $.x 'preceding-sibling::div[contains(@class,"postContainer")][1]', Unread.posts[0].nodes.root
break unless (Get.postFromRoot root).isHidden
root.scrollIntoView false
else if posts.length
# Scroll to the last read post.
Header.scrollToPost posts[posts.length - 1].nodes.root
sync: -> sync: ->
lastReadPost = Unread.db.get lastReadPost = Unread.db.get
boardID: Unread.thread.board.ID boardID: Unread.thread.board.ID
@ -3287,7 +3384,7 @@ Unread =
{root} = post.nodes {root} = post.nodes
if root isnt $ '.thread > .replyContainer', root.parentNode # not the first reply if root isnt $ '.thread > .replyContainer', root.parentNode # not the first reply
$.before root, Unread.hr $.before root, Unread.hr
else if Unread.hr.parentNode else
$.rm Unread.hr $.rm Unread.hr
update: <% if (type === 'crx') { %>(dontrepeat) <% } %>-> update: <% if (type === 'crx') { %>(dontrepeat) <% } %>->
@ -3581,7 +3678,7 @@ ThreadUpdater =
unless d.hidden unless d.hidden
# Lower the max refresh rate limit on visible tabs. # Lower the max refresh rate limit on visible tabs.
j = Math.min j, 7 j = Math.min j, 7
ThreadUpdater.seconds = ThreadUpdater.seconds =
if Conf['Optional Increase'] if Conf['Optional Increase']
Math.max i, [0, 5, 10, 15, 20, 30, 60, 90, 120, 240, 300][j] Math.max i, [0, 5, 10, 15, 20, 30, 60, 90, 120, 240, 300][j]
else else
@ -3781,7 +3878,7 @@ ThreadWatcher =
$.add div, [x, $.tn(' '), link] $.add div, [x, $.tn(' '), link]
nodes.push div nodes.push div
ThreadWatcher.dialog.innerHTML = '' $.rmAll ThreadWatcher.dialog
$.add ThreadWatcher.dialog, nodes $.add ThreadWatcher.dialog, nodes
watched = watched[g.BOARD] or {} watched = watched[g.BOARD] or {}
@ -4035,7 +4132,7 @@ Linkify =
embedder: (a) -> embedder: (a) ->
return [a] unless Conf['Embedding'] return [a] unless Conf['Embedding']
callbacks = -> callbacks = ->
a.textContent = switch @status a.textContent = switch @status
when 200, 304 when 200, 304
@ -4066,16 +4163,17 @@ Linkify =
$.on embed, 'click', Linkify.toggle $.on embed, 'click', Linkify.toggle
if Conf['Link Title'] and (service = type.title) if Conf['Link Title'] and (service = type.title)
titles = $.get 'CachedTitles', {} $.get 'CachedTitles', {}, (item) ->
titles = item['CachedTitles']
if title = titles[match[1]] if title = titles[match[1]]
a.textContent = title[0] a.textContent = title[0]
embed.setAttribute 'data-title', title[0] embed.setAttribute 'data-title', title[0]
else else
try try
$.cache service.api.call(a), callbacks $.cache service.api.call(a), callbacks
catch err catch err
a.innerHTML = "[#{key}] <span class=warning>Title Link Blocked</span> (are you using NoScript?)</a>" a.innerHTML = "[#{key}] <span class=warning>Title Link Blocked</span> (are you using NoScript?)</a>"
return [a, $.tn(' '), embed] return [a, $.tn(' '), embed]
return [a] return [a]

View File

@ -432,7 +432,7 @@ Main =
# c.timeEnd 'All initializations' # c.timeEnd 'All initializations'
$.on d, 'AddCallback', Main.addCallback $.on d, 'AddCallback', Main.addCallback
$.ready Main.initReady $.ready Main.initReady
initReady: -> initReady: ->

View File

@ -11,9 +11,9 @@ QR =
href: 'javascript:;' href: 'javascript:;'
$.on sc, 'click', -> $.on sc, 'click', ->
if !QR.nodes or QR.nodes.el.hidden if !QR.nodes or QR.nodes.el.hidden
$.event 'CloseMenu'
QR.open() QR.open()
QR.nodes.com.focus() QR.nodes.com.focus()
QR.resetThreadSelector()
else else
QR.close() QR.close()
$.toggleClass @, 'disabled' $.toggleClass @, 'disabled'
@ -21,8 +21,7 @@ QR =
Header.addShortcut sc Header.addShortcut sc
if Conf['Hide Original Post Form'] if Conf['Hide Original Post Form']
$.asap (-> doc), -> $.asap (-> doc), -> $.addClass doc, 'hide-original-post-form'
$.addClass doc, 'hide-original-post-form'
$.on d, '4chanXInitFinished', @initReady $.on d, '4chanXInitFinished', @initReady
@ -81,6 +80,10 @@ QR =
QR.status() QR.status()
if !Conf['Remember Spoiler'] and QR.nodes.spoiler.checked if !Conf['Remember Spoiler'] and QR.nodes.spoiler.checked
QR.nodes.spoiler.click() QR.nodes.spoiler.click()
focusin: ->
$.addClass QR.nodes.el, 'has-focus'
focusout: ->
$.rmClass QR.nodes.el, 'has-focus'
hide: -> hide: ->
d.activeElement.blur() d.activeElement.blur()
$.addClass QR.nodes.el, 'autohide' $.addClass QR.nodes.el, 'autohide'
@ -213,7 +216,7 @@ QR =
now = Date.now() now = Date.now()
post = QR.posts[0] post = QR.posts[0]
isReply = QR.nodes.thread.value isnt 'new' isReply = post.thread isnt 'new'
isSage = /sage/i.test post.email isSage = /sage/i.test post.email
hasFile = !!post.file hasFile = !!post.file
seconds = null seconds = null
@ -272,19 +275,19 @@ QR =
text += ">#{s}\n" text += ">#{s}\n"
QR.open() QR.open()
ta = QR.nodes.com {com, thread} = QR.nodes
QR.nodes.thread.value = OP.ID unless ta.value thread.value = OP.ID unless com.value
caretPos = ta.selectionStart caretPos = com.selectionStart
# Replace selection for text. # Replace selection for text.
ta.value = ta.value[...caretPos] + text + ta.value[ta.selectionEnd..] com.value = com.value[...caretPos] + text + com.value[com.selectionEnd..]
# Move the caret to the end of the new quote. # Move the caret to the end of the new quote.
range = caretPos + text.length range = caretPos + text.length
ta.setSelectionRange range, range com.setSelectionRange range, range
ta.focus() com.focus()
# Fire the 'input' event QR.selected.save com
$.event 'input', null, ta QR.selected.save thread
characterCount: -> characterCount: ->
counter = QR.nodes.charCount counter = QR.nodes.charCount
@ -362,12 +365,6 @@ QR =
post = new QR.post() post = new QR.post()
post.setFile file post.setFile file
$.addClass QR.nodes.el, 'dump' $.addClass QR.nodes.el, 'dump'
resetThreadSelector: ->
if g.VIEW is 'thread'
QR.nodes.thread.value = g.THREADID
else
QR.nodes.thread.value = 'new'
posts: [] posts: []
@ -386,8 +383,12 @@ QR =
spoiler: $ 'input', el spoiler: $ 'input', el
span: el.lastChild span: el.lastChild
@nodes.spoiler.checked = @spoiler <% if (type === 'userscript') { %>
# XXX Firefox lacks focusin/focusout support.
for elm in $$ '*', el
$.on elm, 'blur', QR.focusout
$.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()
@ -399,9 +400,14 @@ QR =
for event in ['dragStart', 'dragEnter', 'dragLeave', 'dragOver', 'dragEnd', 'drop'] for event in ['dragStart', 'dragEnter', 'dragLeave', 'dragOver', 'dragEnd', 'drop']
$.on el, event.toLowerCase(), @[event] $.on el, event.toLowerCase(), @[event]
@thread = if g.VIEW is 'thread'
g.THREADID
else
'new'
prev = QR.posts[QR.posts.length - 1] prev = QR.posts[QR.posts.length - 1]
QR.posts.push @ QR.posts.push @
@spoiler = if prev and Conf['Remember Spoiler'] @nodes.spoiler.checked = @spoiler = if prev and Conf['Remember Spoiler']
prev.spoiler prev.spoiler
else else
false false
@ -435,7 +441,7 @@ QR =
lock: (lock=true) -> lock: (lock=true) ->
@isLocked = lock @isLocked = lock
return unless @ is QR.selected return unless @ is QR.selected
for name in ['name', 'email', 'sub', 'com', 'spoiler'] for name in ['thread', 'name', 'email', 'sub', 'com', 'spoiler']
QR.nodes[name].disabled = lock QR.nodes[name].disabled = lock
@nodes.rm.style.visibility = @nodes.rm.style.visibility =
QR.nodes.fileRM.style.visibility = if lock then 'hidden' else '' QR.nodes.fileRM.style.visibility = if lock then 'hidden' else ''
@ -461,12 +467,15 @@ QR =
load: -> load: ->
# Load this post's values. # Load this post's values.
for name in ['name', 'email', 'sub', 'com'] for name in ['thread', 'name', 'email', 'sub', 'com']
QR.nodes[name].value = @[name] or null QR.nodes[name].value = @[name] or null
@showFileData() @showFileData()
QR.characterCount() QR.characterCount()
save: (input) -> save: (input) ->
if input.type is 'checkbox'
@spoiler = input.checked
return
{value} = input {value} = input
@[input.dataset.name] = value @[input.dataset.name] = value
return if input.nodeName isnt 'TEXTAREA' return if input.nodeName isnt 'TEXTAREA'
@ -481,7 +490,7 @@ QR =
return unless @ is QR.selected return unless @ is QR.selected
# Do this in case people use extensions # Do this in case people use extensions
# that do not trigger the `input` event. # that do not trigger the `input` event.
for name in ['name', 'email', 'sub', 'com'] for name in ['thread', 'name', 'email', 'sub', 'com', 'spoiler']
@save QR.nodes[name] @save QR.nodes[name]
return return
@ -565,8 +574,8 @@ QR =
@showFileData() @showFileData()
return unless window.URL return unless window.URL
URL.revokeObjectURL @URL URL.revokeObjectURL @URL
showFileData: (hide) -> showFileData: ->
if @file if @file
QR.nodes.filename.textContent = @filename QR.nodes.filename.textContent = @filename
QR.nodes.filename.title = @filename QR.nodes.filename.title = @filename
@ -661,6 +670,12 @@ QR =
# start with an uncached captcha # start with an uncached captcha
@reload() @reload()
<% if (type === 'userscript') { %>
# XXX Firefox lacks focusin/focusout support.
$.on input, 'blur', QR.focusout
$.on input, 'focus', QR.focusin
<% } %>
$.addClass QR.nodes.el, 'has-captcha' $.addClass QR.nodes.el, 'has-captcha'
$.after QR.nodes.dumpList.parentElement, [imgContainer, input] $.after QR.nodes.dumpList.parentElement, [imgContainer, input]
@ -847,10 +862,17 @@ QR =
$.add nodes.thread, $.el 'option', $.add nodes.thread, $.el 'option',
value: thread value: thread
textContent: "Thread No.#{thread}" textContent: "Thread No.#{thread}"
QR.resetThreadSelector()
$.on nodes.filename.parentNode, 'click keyup', QR.openFileInput $.on nodes.filename.parentNode, 'click keyup', QR.openFileInput
<% if (type === 'userscript') { %>
# XXX Firefox lacks focusin/focusout support.
for elm in $$ '*', QR.nodes.el
$.on elm, 'blur', QR.focusout
$.on elm, 'focus', QR.focusin
<% } %>
$.on QR.nodes.el, 'focusin', QR.focusin
$.on QR.nodes.el, 'focusout', QR.focusout
$.on nodes.autohide, 'change', QR.toggleHide $.on nodes.autohide, 'change', QR.toggleHide
$.on nodes.close, 'click', QR.close $.on nodes.close, 'click', QR.close
$.on nodes.dumpButton, 'click', -> nodes.el.classList.toggle 'dump' $.on nodes.dumpButton, 'click', -> nodes.el.classList.toggle 'dump'
@ -861,13 +883,21 @@ QR =
QR.selected.rmFile() QR.selected.rmFile()
$.on nodes.spoiler, 'change', -> QR.selected.nodes.spoiler.click() $.on nodes.spoiler, 'change', -> QR.selected.nodes.spoiler.click()
$.on nodes.fileInput, 'change', QR.fileInput $.on nodes.fileInput, 'change', QR.fileInput
new QR.post true
# save selected post's data # save selected post's data
for name in ['name', 'email', 'sub', 'com'] for name in ['name', 'email', 'sub', 'com']
$.on nodes[name], 'input', -> QR.selected.save @ $.on nodes[name], 'input', -> QR.selected.save @
$.on nodes[name], 'focus', -> $.addClass nodes.el, 'focus' $.on nodes.thread, 'change', -> QR.selected.save @
$.on nodes[name], 'blur', -> $.rmClass nodes.el, 'focus'
<% if (type === 'userscript') { %>
if Conf['Remember QR Size']
$.get 'QR Size', '', (item) ->
nodes.com.style.cssText = item['QR Size']
$.on nodes.com, 'mouseup', (e) ->
return if e.button isnt 0
$.set 'QR Size', @style.cssText
<% } %>
new QR.post true
QR.status() QR.status()
QR.cooldown.init() QR.cooldown.init()
@ -897,7 +927,7 @@ QR =
post.forceSave() post.forceSave()
if g.BOARD.ID is 'f' if g.BOARD.ID is 'f'
filetag = QR.nodes.flashTag.value filetag = QR.nodes.flashTag.value
threadID = QR.nodes.thread.value threadID = post.thread
thread = g.BOARD.threads[threadID] thread = g.BOARD.threads[threadID]
# prevent errors # prevent errors
@ -985,6 +1015,11 @@ 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
@ -1026,6 +1061,12 @@ QR =
# Too many frequent mistyped captchas will auto-ban you! # Too many frequent mistyped captchas will auto-ban you!
# On connection error, the post most likely didn't go through. # On connection error, the post most likely didn't go through.
QR.cooldown.set delay: 2 QR.cooldown.set delay: 2
else if err.textContent and m = err.textContent.match /wait\s(\d+)\ssecond/i
QR.cooldown.auto = if QR.captcha.isEnabled
!!QR.captcha.captchas.length
else
true
QR.cooldown.set delay: m[1]
else # stop auto-posting else # stop auto-posting
QR.cooldown.auto = false QR.cooldown.auto = false
QR.status() QR.status()

View File

@ -124,7 +124,7 @@ Settings =
$.rmClass selected, 'tab-selected' $.rmClass selected, 'tab-selected'
$.addClass $(".tab-#{@hyphenatedTitle}", Settings.dialog), 'tab-selected' $.addClass $(".tab-#{@hyphenatedTitle}", Settings.dialog), 'tab-selected'
section = $ 'section', Settings.dialog section = $ 'section', Settings.dialog
section.innerHTML = null $.rmAll section
section.className = "section-#{@hyphenatedTitle}" section.className = "section-#{@hyphenatedTitle}"
@open section, mode @open section, mode
section.scrollTop = 0 section.scrollTop = 0
@ -188,6 +188,13 @@ Settings =
$.delete ['hiddenThreads', 'hiddenPosts'] $.delete ['hiddenThreads', 'hiddenPosts']
$.after $('input[name="Stubs"]', section).parentNode.parentNode, div $.after $('input[name="Stubs"]', section).parentNode.parentNode, div
div = $.el 'div',
innerHTML: "<button>Reset Header</button><span class=description>: Unhide the navigation bar."
unhide = $ 'button', div
$.on unhide, 'click', ->
Header.setBarPosition.call textContent: "sticky top"
$.after $('input[name="Check for Updates"]', section).parentNode.parentNode, div
export: (now, data) -> export: (now, data) ->
unless typeof now is 'number' unless typeof now is 'number'
now = Date.now() now = Date.now()
@ -213,7 +220,7 @@ Settings =
return return
# XXX Firefox won't let us download automatically. # XXX Firefox won't let us download automatically.
p = $ '.imp-exp-result', Settings.dialog p = $ '.imp-exp-result', Settings.dialog
p.innerHTML = null $.rmAll p
$.add p, a $.add p, a
import: -> import: ->
@ -340,7 +347,7 @@ Settings =
selectFilter: -> selectFilter: ->
div = @nextElementSibling div = @nextElementSibling
if (name = @value) isnt 'guide' if (name = @value) isnt 'guide'
div.innerHTML = null $.rmAll div
ta = $.el 'textarea', ta = $.el 'textarea',
name: name name: name
className: 'field' className: 'field'
@ -824,7 +831,7 @@ Settings =
mascotHide = $.el "div", mascotHide = $.el "div",
id: "mascot_hide" id: "mascot_hide"
className: "reply" className: "reply"
innerHTML: "Hide Categories <span class=dropmarker></span><div></div>" innerHTML: "Hide Categories <span class=drop-marker></span><div></div>"
keys = Object.keys Mascots keys = Object.keys Mascots
keys.sort() keys.sort()