Most of the Header is done, see changelog file.
Add 4chan and 4chan X settings links in the header's menu.
Add $.asap.
Add UI.Menu.
The menu API now requires a type ("post" or "header").
Add Get.boardsConfig.
This commit is contained in:
parent
c54ddff83d
commit
b6cf7220c4
785
4chan_x.user.js
785
4chan_x.user.js
File diff suppressed because one or more lines are too long
@ -1,11 +1,16 @@
|
||||
alpha
|
||||
- Mayhem
|
||||
Major rewrite of 4chan X.
|
||||
New feature, the Header:
|
||||
Access the list of boards directly from the Header.
|
||||
Access to settings and extra features easily from the Header's menu.
|
||||
Can be auto-hidden.
|
||||
Added touch and multi-touch support for dragging windows.
|
||||
The Thread Updater will pause when offline, and resume when online.
|
||||
Added Thread & Post Hiding in the Menu, with individual settings.
|
||||
Thread & Post Hiding Buttons can now be disabled in the settings.
|
||||
Recursive Hiding will be automatically applied when manually hiding a post.
|
||||
Fix Chrome's install warning that 4chan X would execute on all domains.
|
||||
Fix Chrome's install warning saying that 4chan X would execute on all domains.
|
||||
Fix Quote Backlinks not affecting inlined quotes.
|
||||
Fix Quote Highlighting not affecting inlined quotes.
|
||||
|
||||
|
||||
@ -31,17 +31,20 @@ a[href="javascript:;"] {
|
||||
/* fixed, z-index */
|
||||
#qp, #ihover,
|
||||
#updater, #stats,
|
||||
#boardNavDesktop.reply,
|
||||
#header,
|
||||
#qr, #watcher {
|
||||
position: fixed;
|
||||
}
|
||||
#qp, #ihover {
|
||||
z-index: 100;
|
||||
}
|
||||
#menu {
|
||||
z-index: 95;
|
||||
}
|
||||
#updater, #stats {
|
||||
z-index: 90;
|
||||
}
|
||||
#boardNavDesktop.reply:hover {
|
||||
#header:hover {
|
||||
z-index: 80;
|
||||
}
|
||||
#qr {
|
||||
@ -50,38 +53,70 @@ a[href="javascript:;"] {
|
||||
#watcher {
|
||||
z-index: 30;
|
||||
}
|
||||
#boardNavDesktop.reply {
|
||||
#header {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* XXX support different styles */
|
||||
#header-bar {
|
||||
font-size: 9pt;
|
||||
color: #89A;
|
||||
background-color: #D6DAF0;
|
||||
border-color: #B7C5D9;
|
||||
border-width: 0 0 1px;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
/* header */
|
||||
body.fourchan_x {
|
||||
margin-top: 2.5em;
|
||||
margin-top: 2em;
|
||||
}
|
||||
#boardNavDesktop.reply {
|
||||
border-width: 0 0 1px;
|
||||
padding: 4px;
|
||||
#header {
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
transition: opacity .1s ease-in-out;
|
||||
-o-transition: opacity .1s ease-in-out;
|
||||
-moz-transition: opacity .1s ease-in-out;
|
||||
-webkit-transition: opacity .1s ease-in-out;
|
||||
}
|
||||
#boardNavDesktop.reply:not(:hover) {
|
||||
opacity: .4;
|
||||
transition: opacity 1.5s .5s ease-in-out;
|
||||
-o-transition: opacity 1.5s .5s ease-in-out;
|
||||
-moz-transition: opacity 1.5s .5s ease-in-out;
|
||||
-webkit-transition: opacity 1.5s .5s ease-in-out;
|
||||
#header-bar {
|
||||
padding: 4px;
|
||||
position: relative;
|
||||
transition: all .1s ease-in-out;
|
||||
-o-transition: all .1s ease-in-out;
|
||||
-moz-transition: all .1s ease-in-out;
|
||||
-webkit-transition: all .1s ease-in-out;
|
||||
}
|
||||
#boardNavDesktop.reply a {
|
||||
margin: -1px;
|
||||
#header-bar.autohide:not(:hover) {
|
||||
transform: translateY(-100%);
|
||||
-o-transform: translateY(-100%);
|
||||
-moz-transform: translateY(-100%);
|
||||
-webkit-transform: translateY(-100%);
|
||||
transition: all .75s .25s ease-in-out;
|
||||
-o-transition: all .75s .25s ease-in-out;
|
||||
-moz-transition: all .75s .25s ease-in-out;
|
||||
-webkit-transition: all .75s .25s ease-in-out;
|
||||
}
|
||||
#settings {
|
||||
#toggle-header-bar {
|
||||
cursor: n-resize;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: -8px;
|
||||
height: 10px;
|
||||
position: absolute;
|
||||
}
|
||||
#header-bar.autohide #toggle-header-bar {
|
||||
cursor: s-resize;
|
||||
}
|
||||
#header-bar a {
|
||||
text-decoration: none;
|
||||
padding: 1px;
|
||||
}
|
||||
#header-bar > .menu-button {
|
||||
float: right;
|
||||
padding: 0;
|
||||
}
|
||||
body > #boardNavDesktop,
|
||||
#navtopright,
|
||||
#boardNavDesktopFoot {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* thread updater */
|
||||
|
||||
18
lib/$.coffee
18
lib/$.coffee
@ -78,19 +78,19 @@ $.extend $,
|
||||
value: ->
|
||||
$.set @name, @value.trim()
|
||||
Conf[@name] = @value
|
||||
asap: (test, cb) ->
|
||||
if test()
|
||||
cb()
|
||||
else
|
||||
setTimeout $.asap, 25, test, cb
|
||||
addStyle: (css) ->
|
||||
style = $.el 'style',
|
||||
textContent: css
|
||||
# That's terrible.
|
||||
# XXX tmp fix for scriptish:
|
||||
# XXX fix for scriptish:
|
||||
# https://github.com/scriptish/scriptish/issues/16
|
||||
f = ->
|
||||
# XXX Only Chrome has no d.head on document-start.
|
||||
if root = d.head or d.documentElement
|
||||
$.add root, style
|
||||
else
|
||||
setTimeout f, 20
|
||||
f()
|
||||
$.asap (-> d.head), (->
|
||||
$.add d.head, style
|
||||
)
|
||||
style
|
||||
x: (path, root=d.body) ->
|
||||
# XPathResult.ANY_UNORDERED_NODE_TYPE === 8
|
||||
|
||||
262
lib/ui.coffee
262
lib/ui.coffee
@ -10,6 +10,180 @@ UI = (->
|
||||
move.addEventListener 'mousedown', dragstart, false
|
||||
el
|
||||
|
||||
|
||||
class Menu
|
||||
currentMenu = null
|
||||
lastToggledButton = null
|
||||
|
||||
constructor: (@type) ->
|
||||
# Doc here: https://github.com/MayhemYDG/4chan-x/wiki/Menu-API
|
||||
$.on d, 'AddMenuEntry', @addEntryListener.bind @
|
||||
@close = close.bind @
|
||||
@entries = []
|
||||
|
||||
makeMenu: ->
|
||||
menu = $.el 'div',
|
||||
className: 'reply dialog'
|
||||
id: 'menu'
|
||||
tabIndex: 0
|
||||
$.on menu, 'click', (e) -> e.stopPropagation()
|
||||
$.on menu, 'keydown', @keybinds.bind @
|
||||
menu
|
||||
|
||||
toggle: (e, button, data) ->
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
if currentMenu
|
||||
# Close if it's already opened.
|
||||
# Reopen if we clicked on another button.
|
||||
previousButton = lastToggledButton
|
||||
@close()
|
||||
return if previousButton is button
|
||||
|
||||
return unless @entries.length
|
||||
@open button, data
|
||||
|
||||
open: (button, data) ->
|
||||
menu = @makeMenu()
|
||||
currentMenu = menu
|
||||
lastToggledButton = button
|
||||
|
||||
for entry in @entries
|
||||
@insertEntry entry, menu, data
|
||||
|
||||
@focus $ '.entry', menu
|
||||
$.on d, 'click', @close
|
||||
$.add d.body, menu
|
||||
|
||||
# Position
|
||||
mRect = menu.getBoundingClientRect()
|
||||
bRect = button.getBoundingClientRect()
|
||||
bTop = d.documentElement.scrollTop + d.body.scrollTop + bRect.top
|
||||
bLeft = d.documentElement.scrollLeft + d.body.scrollLeft + bRect.left
|
||||
cHeight = d.documentElement.clientHeight
|
||||
cWidth = d.documentElement.clientWidth
|
||||
top =
|
||||
if bRect.top + bRect.height + mRect.height < cHeight
|
||||
bTop + bRect.height + 2
|
||||
else
|
||||
bTop - mRect.height - 2
|
||||
left =
|
||||
if bRect.left + mRect.width < cWidth
|
||||
bLeft
|
||||
else
|
||||
bLeft + bRect.width - mRect.width
|
||||
{style} = menu
|
||||
style.top = top + 'px'
|
||||
style.left = left + 'px'
|
||||
|
||||
menu.focus()
|
||||
|
||||
insertEntry: (entry, parent, data) ->
|
||||
if typeof entry.open is 'function'
|
||||
return unless entry.open data
|
||||
$.add parent, entry.el
|
||||
|
||||
return unless entry.children
|
||||
if submenu = $ '.submenu', entry.el
|
||||
# Reset sub menu, remove irrelevant entries.
|
||||
$.rm submenu
|
||||
submenu = $.el 'div',
|
||||
className: 'reply dialog submenu'
|
||||
for child in entry.children
|
||||
@insertEntry child, submenu, data
|
||||
$.add entry.el, submenu
|
||||
return
|
||||
|
||||
close = ->
|
||||
$.rm currentMenu
|
||||
currentMenu = null
|
||||
lastToggledButton = null
|
||||
$.off d, 'click', @close
|
||||
|
||||
keybinds: (e) ->
|
||||
entry = $ '.focused', currentMenu
|
||||
while subEntry = $ '.focused', entry
|
||||
entry = subEntry
|
||||
|
||||
switch e.keyCode
|
||||
when 27 # Esc
|
||||
lastToggledButton.focus()
|
||||
@close()
|
||||
when 13, 32 # Enter, Space
|
||||
entry.click()
|
||||
when 38 # Up
|
||||
if next = entry.previousElementSibling
|
||||
@focus next
|
||||
when 40 # Down
|
||||
if next = entry.nextElementSibling
|
||||
@focus next
|
||||
when 39 # Right
|
||||
if (submenu = $ '.submenu', entry) and next = submenu.firstElementChild
|
||||
@focus next
|
||||
when 37 # Left
|
||||
if next = $.x 'parent::*[contains(@class,"submenu")]/parent::*', entry
|
||||
@focus next
|
||||
else
|
||||
return
|
||||
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
focus: (entry) ->
|
||||
while focused = $.x 'parent::*/child::*[contains(@class,"focused")]', entry
|
||||
$.rmClass focused, 'focused'
|
||||
for focused in $$ '.focused', entry
|
||||
$.rmClass focused, 'focused'
|
||||
$.addClass entry, 'focused'
|
||||
|
||||
# Submenu positioning.
|
||||
return unless submenu = $ '.submenu', entry
|
||||
sRect = submenu.getBoundingClientRect()
|
||||
eRect = entry.getBoundingClientRect()
|
||||
cHeight = d.documentElement.clientHeight
|
||||
cWidth = d.documentElement.clientWidth
|
||||
if eRect.top + sRect.height < cHeight
|
||||
top = '0px'
|
||||
bottom = 'auto'
|
||||
else
|
||||
top = 'auto'
|
||||
bottom = '0px'
|
||||
if eRect.right + sRect.width < cWidth
|
||||
left = '100%'
|
||||
right = 'auto'
|
||||
else
|
||||
left = 'auto'
|
||||
right = '100%'
|
||||
{style} = submenu
|
||||
style.top = top
|
||||
style.bottom = bottom
|
||||
style.left = left
|
||||
style.right = right
|
||||
|
||||
addEntry: (entry) ->
|
||||
@parseEntry entry
|
||||
@entries.push entry
|
||||
|
||||
parseEntry: (entry) ->
|
||||
{el, children} = entry
|
||||
$.addClass el, 'entry'
|
||||
$.on el, 'focus mouseover', ((e) ->
|
||||
e.stopPropagation()
|
||||
@focus el
|
||||
).bind @
|
||||
return unless children
|
||||
$.addClass el, 'has-submenu'
|
||||
for child in children
|
||||
@parseEntry child
|
||||
return
|
||||
|
||||
addEntryListener: (e) ->
|
||||
entry = e.detail
|
||||
return if entry.type isnt @type
|
||||
@addEntry entry
|
||||
|
||||
|
||||
dragstart = (e) ->
|
||||
# prevent text selection
|
||||
e.preventDefault()
|
||||
@ -49,24 +223,42 @@ UI = (->
|
||||
drag.call @, touch
|
||||
return
|
||||
drag = (e) ->
|
||||
left = e.clientX - @dx
|
||||
top = e.clientY - @dy
|
||||
if left < 10 then left = 0
|
||||
else if @width - left < 10 then left = null
|
||||
if top < 10 then top = 0
|
||||
else if @height - top < 10 then top = null
|
||||
if left is null
|
||||
@style.left = null
|
||||
@style.right = '0%'
|
||||
else
|
||||
@style.left = left / @screenWidth * 100 + '%'
|
||||
@style.right = null
|
||||
if top is null
|
||||
@style.top = null
|
||||
@style.bottom = '0%'
|
||||
else
|
||||
@style.top = top / @screenHeight * 100 + '%'
|
||||
@style.bottom = null
|
||||
{clientX, clientY} = e
|
||||
|
||||
left = clientX - @dx
|
||||
left =
|
||||
if left < 10
|
||||
0
|
||||
else if @width - left < 10
|
||||
null
|
||||
else
|
||||
left / @screenWidth * 100 + '%'
|
||||
|
||||
top = clientY - @dy
|
||||
top =
|
||||
if top < 10
|
||||
0
|
||||
else if @height - top < 10
|
||||
null
|
||||
else
|
||||
top / @screenHeight * 100 + '%'
|
||||
|
||||
right =
|
||||
if left is null
|
||||
0
|
||||
else
|
||||
null
|
||||
bottom =
|
||||
if top is null
|
||||
0
|
||||
else
|
||||
null
|
||||
|
||||
{style} = @
|
||||
style.left = left
|
||||
style.right = right
|
||||
style.top = top
|
||||
style.bottom = bottom
|
||||
touchend = (e) ->
|
||||
for touch in e.changedTouches
|
||||
if touch.identifier is @identifier
|
||||
@ -99,22 +291,28 @@ UI = (->
|
||||
root.addEventListener 'mousemove', o.hover, false
|
||||
hover = (e) ->
|
||||
height = @el.offsetHeight
|
||||
top = e.clientY - 120
|
||||
@style.top =
|
||||
if @clientHeight <= height or top <= 0
|
||||
'0px'
|
||||
else if top + height >= @clientHeight
|
||||
@clientHeight - height + 'px'
|
||||
else
|
||||
top + 'px'
|
||||
{clientX, clientY} = e
|
||||
|
||||
top = clientY - 120
|
||||
top =
|
||||
if @clientHeight <= height or top <= 0
|
||||
0
|
||||
else if top + height >= @clientHeight
|
||||
@clientHeight - height
|
||||
else
|
||||
top
|
||||
|
||||
{clientX} = e
|
||||
if clientX <= @clientWidth - 400
|
||||
@style.left = clientX + 45 + 'px'
|
||||
@style.right = null
|
||||
left = clientX + 45 + 'px'
|
||||
right = null
|
||||
else
|
||||
@style.left = null
|
||||
@style.right = @clientWidth - clientX + 45 + 'px'
|
||||
left = null
|
||||
right = @clientWidth - clientX + 45 + 'px'
|
||||
|
||||
{style} = @
|
||||
style.top = top + 'px'
|
||||
style.left = left
|
||||
style.right = right
|
||||
hoverend = ->
|
||||
@el.parentNode.removeChild @el
|
||||
for event in @events
|
||||
@ -122,8 +320,10 @@ UI = (->
|
||||
@root.removeEventListener 'mousemove', @hover, false
|
||||
@cb.call @ if @cb
|
||||
|
||||
|
||||
return {
|
||||
dialog: dialog
|
||||
Menu: Menu
|
||||
hover: hoverstart
|
||||
}
|
||||
)()
|
||||
|
||||
@ -1,3 +1,109 @@
|
||||
Header =
|
||||
init: ->
|
||||
@menu = new UI.Menu 'header'
|
||||
|
||||
@headerEl = $.el 'div',
|
||||
id: 'header'
|
||||
innerHTML: '<div id=header-bar></div><div id=notifications></div>'
|
||||
|
||||
headerBar = $('#header-bar', @headerEl)
|
||||
if $.get 'autohideHeaderBar', false
|
||||
$.addClass headerBar, 'autohide'
|
||||
|
||||
menuButton = $.el 'a',
|
||||
className: 'menu-button'
|
||||
innerHTML: '[<span></span>]'
|
||||
href: 'javascript:;'
|
||||
$.on menuButton, 'click', @menuToggle
|
||||
|
||||
boardListButton = $.el 'span',
|
||||
className: 'show-board-list-button'
|
||||
innerHTML: '[<a href=javascript:;>+</a>]'
|
||||
title: 'Toggle the board list.'
|
||||
$.on boardListButton, 'click', @toggleBoardList
|
||||
|
||||
boardTitle = $.el 'a',
|
||||
className: 'board-name'
|
||||
innerHTML: "<span class=board-path>/#{g.BOARD}/</span> - <span class=board-title>...</span>"
|
||||
href: "/#{g.BOARD}/"
|
||||
boardList = $.el 'span',
|
||||
className: 'board-list'
|
||||
hidden: true
|
||||
|
||||
toggleBar = $.el 'div',
|
||||
id: 'toggle-header-bar'
|
||||
title: 'Toggle the header bar.'
|
||||
$.on toggleBar, 'click', @toggleBar
|
||||
|
||||
$.prepend headerBar, [menuButton, boardListButton, $.tn(' '), boardTitle, boardList, toggleBar]
|
||||
|
||||
try
|
||||
@setBoardList()
|
||||
catch err
|
||||
# XXX handle error
|
||||
$.log err, 'Header - board list'
|
||||
|
||||
$.asap (-> d.body), ->
|
||||
$.prepend d.body, Header.headerEl
|
||||
|
||||
setBoardList: ->
|
||||
Get.boardsConfig (boardsConfig) ->
|
||||
$('.board-title', Header.headerEl).textContent = boardsConfig[g.BOARD].title
|
||||
$.ready ->
|
||||
if nav = $.id 'boardNavDesktop'
|
||||
$("a[href$='/#{g.BOARD}/']", nav)?.className = 'current'
|
||||
$.add $('.board-list', Header.headerEl),
|
||||
Array::slice.call nav.childNodes
|
||||
|
||||
toggleBoardList: ->
|
||||
node = @firstElementChild.firstChild
|
||||
if showBoardList = $.hasClass @, 'show-board-list-button'
|
||||
@className = 'hide-board-list-button'
|
||||
node.data = node.data.replace '+', '-'
|
||||
else
|
||||
@className = 'show-board-list-button'
|
||||
node.data = node.data.replace '-', '+'
|
||||
{headerEl} = Header
|
||||
$('.board-name', headerEl).hidden = showBoardList
|
||||
$('.board-list', headerEl).hidden = !showBoardList
|
||||
|
||||
toggleBar: ->
|
||||
bool = $.id('header-bar').classList.toggle 'autohide'
|
||||
$.set 'autohideHeaderBar', bool
|
||||
|
||||
menuToggle: (e) ->
|
||||
Header.menu.toggle e, @, g
|
||||
|
||||
Settings =
|
||||
init: ->
|
||||
# 4chan X settings link
|
||||
link = $.el 'a',
|
||||
className: 'settings-link'
|
||||
textContent: '4chan X Settings'
|
||||
href: 'javascript:;'
|
||||
$.on link, 'click', Settings.open
|
||||
Header.menu.addEntry
|
||||
el: link
|
||||
|
||||
# 4chan settings link
|
||||
link = $.el 'a',
|
||||
className: 'fourchan-settings-link'
|
||||
textContent: '4chan Settings'
|
||||
href: 'javascript:;'
|
||||
$.on link, 'click', -> $.id('settingsWindowLink').click()
|
||||
Header.menu.addEntry
|
||||
el: link
|
||||
open: -> !Conf['Disable 4chan\'s extension']
|
||||
|
||||
return unless Conf['Disable 4chan\'s extension']
|
||||
settings = JSON.parse(localStorage.getItem '4chan-settings') or {}
|
||||
return if settings.disableAll
|
||||
settings.disableAll = true
|
||||
localStorage.setItem '4chan-settings', JSON.stringify settings
|
||||
open: ->
|
||||
Header.menu.close()
|
||||
# Here be settings
|
||||
|
||||
Filter =
|
||||
filters: {}
|
||||
init: ->
|
||||
@ -195,7 +301,7 @@ Filter =
|
||||
# Add a sub entry for each filter type.
|
||||
entry.children.push Filter.menu.createSubEntry type[0], type[1]
|
||||
|
||||
Menu.addEntry entry
|
||||
Menu.menu.addEntry entry
|
||||
|
||||
createSubEntry: (text, type) ->
|
||||
el = $.el 'a',
|
||||
@ -345,7 +451,7 @@ ThreadHiding =
|
||||
makeStub = $.el 'label',
|
||||
innerHTML: "<input type=checkbox checked=#{Conf['Stubs']}> Make stub"
|
||||
|
||||
Menu.addEntry
|
||||
Menu.menu.addEntry
|
||||
el: div
|
||||
open: (post) ->
|
||||
{thread} = post
|
||||
@ -493,7 +599,7 @@ ReplyHiding =
|
||||
makeStub = $.el 'label',
|
||||
innerHTML: "<input type=checkbox name=makeStub checked=#{Conf['Stubs']}> Make stub"
|
||||
|
||||
Menu.addEntry
|
||||
Menu.menu.addEntry
|
||||
el: div
|
||||
open: (post) ->
|
||||
if !post.isReply or post.isClone
|
||||
@ -619,20 +725,18 @@ Recursive =
|
||||
return
|
||||
|
||||
Menu =
|
||||
entries: []
|
||||
init: ->
|
||||
# Doc here: https://github.com/MayhemYDG/4chan-x/wiki/Menu-API
|
||||
$.on d, 'AddMenuEntry', (e) -> Menu.addEntry e.detail
|
||||
@menu = new UI.Menu 'post'
|
||||
Post::callbacks.push
|
||||
name: 'Menu'
|
||||
cb: @node
|
||||
|
||||
node: ->
|
||||
a = Menu.makeButton @
|
||||
button = Menu.makeButton @
|
||||
if @isClone
|
||||
$.replace $('.menu-button', @nodes.info), a
|
||||
$.replace $('.menu-button', @nodes.info), button
|
||||
return
|
||||
$.add @nodes.info, [$.tn('\u00A0'), a]
|
||||
$.add @nodes.info, [$.tn('\u00A0'), button]
|
||||
|
||||
makeButton: (post) ->
|
||||
a = $.el 'a',
|
||||
@ -644,158 +748,13 @@ Menu =
|
||||
$.on a, 'click', Menu.toggle
|
||||
a
|
||||
|
||||
makeMenu: ->
|
||||
menu = $.el 'div',
|
||||
className: 'reply dialog'
|
||||
id: 'menu'
|
||||
tabIndex: 0
|
||||
$.on menu, 'click', (e) -> e.stopPropagation()
|
||||
$.on menu, 'keydown', Menu.keybinds
|
||||
menu
|
||||
|
||||
toggle: (e) ->
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
if Menu.currentMenu
|
||||
# Close if it's already opened.
|
||||
# Reopen if we clicked on another button.
|
||||
{lastToggledButton} = Menu
|
||||
Menu.close()
|
||||
return if lastToggledButton is @
|
||||
|
||||
Menu.lastToggledButton = @
|
||||
post =
|
||||
if @dataset.clone
|
||||
Get.postFromRoot $.x 'ancestor::div[contains(@class,"postContainer")][1]', @
|
||||
else
|
||||
g.posts[@dataset.postid]
|
||||
Menu.open @, post
|
||||
|
||||
open: (button, post) ->
|
||||
menu = Menu.makeMenu()
|
||||
Menu.currentMenu = menu
|
||||
|
||||
for entry in Menu.entries
|
||||
Menu.insertEntry entry, menu, post
|
||||
|
||||
Menu.focus $ '.entry', menu
|
||||
$.on d, 'click', Menu.close
|
||||
$.add d.body, menu
|
||||
|
||||
# Position
|
||||
mRect = menu.getBoundingClientRect()
|
||||
bRect = button.getBoundingClientRect()
|
||||
bTop = d.documentElement.scrollTop + d.body.scrollTop + bRect.top
|
||||
bLeft = d.documentElement.scrollLeft + d.body.scrollLeft + bRect.left
|
||||
menu.style.top =
|
||||
if bRect.top + bRect.height + mRect.height < d.documentElement.clientHeight
|
||||
bTop + bRect.height + 2 + 'px'
|
||||
else
|
||||
bTop - mRect.height - 2 + 'px'
|
||||
menu.style.left =
|
||||
if bRect.left + mRect.width < d.documentElement.clientWidth
|
||||
bLeft + 'px'
|
||||
else
|
||||
bLeft + bRect.width - mRect.width + 'px'
|
||||
|
||||
menu.focus()
|
||||
|
||||
insertEntry: (entry, parent, post) ->
|
||||
if typeof entry.open is 'function'
|
||||
return unless entry.open post
|
||||
$.add parent, entry.el
|
||||
|
||||
return unless entry.children
|
||||
if submenu = $ '.submenu', entry.el
|
||||
# Reset sub menu, remove irrelevant entries.
|
||||
$.rm submenu
|
||||
submenu = $.el 'div',
|
||||
className: 'reply dialog submenu'
|
||||
$.add entry.el, submenu
|
||||
for child in entry.children
|
||||
Menu.insertEntry child, submenu, post
|
||||
return
|
||||
|
||||
close: ->
|
||||
$.rm Menu.currentMenu
|
||||
delete Menu.currentMenu
|
||||
delete Menu.lastToggledButton
|
||||
$.off d, 'click', Menu.close
|
||||
|
||||
keybinds: (e) ->
|
||||
entry = $ '.focused', Menu.currentMenu
|
||||
while subEntry = $ '.focused', entry
|
||||
entry = subEntry
|
||||
|
||||
switch Keybinds.keyCode(e) or e.keyCode
|
||||
when 'Esc'
|
||||
Menu.lastToggledButton.focus()
|
||||
Menu.close()
|
||||
when 13, 32 # 'Enter', 'Space'
|
||||
entry.click()
|
||||
when 'Up'
|
||||
if next = entry.previousElementSibling
|
||||
Menu.focus next
|
||||
when 'Down'
|
||||
if next = entry.nextElementSibling
|
||||
Menu.focus next
|
||||
when 'Right'
|
||||
if (submenu = $ '.submenu', entry) and next = submenu.firstElementChild
|
||||
Menu.focus next
|
||||
when 'Left'
|
||||
if next = $.x 'parent::*[contains(@class,"submenu")]/parent::*', entry
|
||||
Menu.focus next
|
||||
else
|
||||
return
|
||||
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
focus: (entry) ->
|
||||
while focused = $.x 'parent::*/child::*[contains(@class,"focused")]', entry
|
||||
$.rmClass focused, 'focused'
|
||||
for focused in $$ '.focused', entry
|
||||
$.rmClass focused, 'focused'
|
||||
$.addClass entry, 'focused'
|
||||
|
||||
# Submenu positioning.
|
||||
return unless submenu = $ '.submenu', entry
|
||||
sRect = submenu.getBoundingClientRect()
|
||||
eRect = entry.getBoundingClientRect()
|
||||
if eRect.top + sRect.height < d.documentElement.clientHeight
|
||||
top = '0px'
|
||||
bottom = 'auto'
|
||||
else
|
||||
top = 'auto'
|
||||
bottom = '0px'
|
||||
if eRect.right + sRect.width < d.documentElement.clientWidth
|
||||
left = '100%'
|
||||
right = 'auto'
|
||||
else
|
||||
left = 'auto'
|
||||
right = '100%'
|
||||
{style} = submenu
|
||||
style.top = top
|
||||
style.bottom = bottom
|
||||
style.left = left
|
||||
style.right = right
|
||||
|
||||
addEntry: (entry) ->
|
||||
Menu.parseEntry entry
|
||||
Menu.entries.push entry
|
||||
|
||||
parseEntry: (entry) ->
|
||||
{el, children} = entry
|
||||
$.addClass el, 'entry'
|
||||
$.on el, 'focus mouseover', (e) ->
|
||||
e.stopPropagation()
|
||||
Menu.focus @
|
||||
return unless children
|
||||
$.addClass el, 'has-submenu'
|
||||
for child in children
|
||||
Menu.parseEntry child
|
||||
return
|
||||
Menu.menu.toggle e, @, post
|
||||
|
||||
ReportLink =
|
||||
init: ->
|
||||
@ -804,7 +763,7 @@ ReportLink =
|
||||
href: 'javascript:;'
|
||||
textContent: 'Report this post'
|
||||
$.on a, 'click', ReportLink.report
|
||||
Menu.addEntry
|
||||
Menu.menu.addEntry
|
||||
el: a
|
||||
open: (post) ->
|
||||
ReportLink.post = post
|
||||
@ -841,7 +800,7 @@ DeleteLink =
|
||||
$.on fileEl, 'click', DeleteLink.delete
|
||||
!!post.file
|
||||
|
||||
Menu.addEntry
|
||||
Menu.menu.addEntry
|
||||
el: div
|
||||
open: (post) ->
|
||||
return false if post.isDead
|
||||
@ -927,7 +886,7 @@ DownloadLink =
|
||||
a = $.el 'a',
|
||||
className: 'download-link'
|
||||
textContent: 'Download file'
|
||||
Menu.addEntry
|
||||
Menu.menu.addEntry
|
||||
el: a
|
||||
open: (post) ->
|
||||
return false unless post.file
|
||||
@ -962,7 +921,7 @@ ArchiveLink =
|
||||
# Add a sub entry for each type.
|
||||
entry.children.push @createSubEntry type[0], type[1]
|
||||
|
||||
Menu.addEntry entry
|
||||
Menu.menu.addEntry entry
|
||||
|
||||
createSubEntry: (text, type) ->
|
||||
el = $.el 'a',
|
||||
@ -1341,6 +1300,40 @@ Build =
|
||||
container
|
||||
|
||||
Get =
|
||||
boardsConfig: (->
|
||||
boardsConfig = null
|
||||
callbacks = []
|
||||
|
||||
parseBoardsConfig = ->
|
||||
return if @status isnt 200
|
||||
boardsConfig = {}
|
||||
for board in JSON.parse(@response).boards
|
||||
boardName = board.board
|
||||
delete board.board
|
||||
boardsConfig[boardName] = board
|
||||
for callback in callbacks
|
||||
callback boardsConfig
|
||||
callbacks = null
|
||||
boardsConfig.lastModified = @getResponseHeader 'Last-Modified'
|
||||
$.set 'boardsConfig', boardsConfig
|
||||
|
||||
(cb) ->
|
||||
# Configs were already loaded previously, callback and stop.
|
||||
if boardsConfig
|
||||
cb boardsConfig
|
||||
return
|
||||
|
||||
# Load configs, callback, check for updates.
|
||||
if boardsConfig = $.get 'boardsConfig', null
|
||||
cb boardsConfig
|
||||
lastModified = boardsConfig.lastModified
|
||||
else
|
||||
return if callbacks.push(cb) > 1
|
||||
lastModified = 0
|
||||
|
||||
$.ajax '//api.4chan.org/boards.json', onloadend: parseBoardsConfig,
|
||||
headers: 'If-Modified-Since': lastModified
|
||||
)()
|
||||
postFromRoot: (root) ->
|
||||
link = $ 'a[title="Highlight this post"]', root
|
||||
board = link.pathname.split('/')[1]
|
||||
|
||||
@ -158,6 +158,7 @@ class Post
|
||||
# Get quote/backlinks to this post
|
||||
# and paint them (Dead).
|
||||
for quotelink in Get.allQuotelinksLinkingTo @
|
||||
continue if $.hasClass quotelink, 'deadlink'
|
||||
$.add quotelink, $.tn '\u00A0(Dead)'
|
||||
$.addClass quotelink, 'deadlink'
|
||||
return
|
||||
@ -267,10 +268,6 @@ Main =
|
||||
g.THREAD = +pathname[3]
|
||||
|
||||
switch location.hostname
|
||||
when 'boards.4chan.org'
|
||||
Main.initHeader()
|
||||
return if g.VIEW is 'catalog'
|
||||
Main.initFeatures()
|
||||
when 'sys.4chan.org'
|
||||
return
|
||||
when 'images.4chan.org'
|
||||
@ -280,41 +277,25 @@ Main =
|
||||
location.href = url if url
|
||||
return
|
||||
|
||||
initHeader: ->
|
||||
return if g.VIEW is 'catalog'
|
||||
|
||||
$.addStyle Main.css
|
||||
Main.header = $.el 'div',
|
||||
className: 'reply'
|
||||
innerHTML: '<div class=extra></div>'
|
||||
$.ready Main.initHeaderReady
|
||||
$.asap (-> d.body), (->
|
||||
$.addClass d.body, $.engine
|
||||
$.addClass d.body, 'fourchan_x'
|
||||
)
|
||||
|
||||
initHeaderReady: ->
|
||||
header = Main.header
|
||||
$.prepend d.body, header
|
||||
try
|
||||
Header.init()
|
||||
catch err
|
||||
# XXX handle error
|
||||
$.log err, 'Header'
|
||||
|
||||
if nav = $.id 'boardNavDesktop'
|
||||
header.id = nav.id
|
||||
$.prepend header, nav
|
||||
nav.id = nav.className = null
|
||||
nav.lastElementChild.hidden = true
|
||||
settings = $.el 'span',
|
||||
id: 'settings'
|
||||
innerHTML: '[<a href=javascript:;>Settings</a>]'
|
||||
$.on settings.firstElementChild, 'click', Main.settings
|
||||
$.add nav, settings
|
||||
$("a[href$='/#{g.BOARD}/']", nav)?.className = 'current'
|
||||
|
||||
$.addClass d.body, $.engine
|
||||
$.addClass d.body, 'fourchan_x'
|
||||
|
||||
# disable the mobile layout
|
||||
$('link[href*=mobile]', d.head)?.disabled = true
|
||||
$.id('boardNavDesktopFoot')?.hidden = true
|
||||
|
||||
initFeatures: ->
|
||||
if Conf['Disable 4chan\'s extension']
|
||||
settings = JSON.parse(localStorage.getItem '4chan-settings') or {}
|
||||
settings.disableAll = true
|
||||
localStorage.setItem '4chan-settings', JSON.stringify settings
|
||||
try
|
||||
Settings.init()
|
||||
catch err
|
||||
# XXX handle error
|
||||
$.log err, 'Settings'
|
||||
|
||||
if Conf['Resurrect Quotes']
|
||||
try
|
||||
@ -497,9 +478,9 @@ Main =
|
||||
# XXX handle error
|
||||
$.log err, 'Thread Updater'
|
||||
|
||||
$.ready Main.initFeaturesReady
|
||||
$.ready Main.initReady
|
||||
|
||||
initFeaturesReady: ->
|
||||
initReady: ->
|
||||
if d.title is '4chan - 404 Not Found'
|
||||
if Conf['404 Redirect'] and g.VIEW is 'thread'
|
||||
location.href = Redirect.to
|
||||
@ -508,7 +489,8 @@ Main =
|
||||
postID: location.hash
|
||||
return
|
||||
|
||||
return unless $.id 'navtopright'
|
||||
# disable the mobile layout
|
||||
$('link[href*=mobile]', d.head)?.disabled = true
|
||||
|
||||
threads = []
|
||||
posts = []
|
||||
@ -543,9 +525,6 @@ Main =
|
||||
$.log err.stack
|
||||
return
|
||||
|
||||
settings: ->
|
||||
alert 'Here be settings'
|
||||
|
||||
css: """<%= grunt.file.read('css/style.css') %>"""
|
||||
|
||||
Main.init()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user