From b6cf7220c47183cdf64076417e00b551c6e4e2cd Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Sat, 9 Feb 2013 18:40:18 +0100 Subject: [PATCH] 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. --- 4chan_x.user.js | 785 +++++++++++++++++++++++++++----------------- changelog | 7 +- css/style.css | 75 +++-- lib/$.coffee | 18 +- lib/ui.coffee | 262 +++++++++++++-- src/features.coffee | 311 +++++++++--------- src/main.coffee | 63 ++-- 7 files changed, 962 insertions(+), 559 deletions(-) diff --git a/4chan_x.user.js b/4chan_x.user.js index 6545fee5d..1b332b27f 100644 --- a/4chan_x.user.js +++ b/4chan_x.user.js @@ -20,7 +20,7 @@ // @icon https://github.com/MayhemYDG/4chan-x/raw/stable/img/icon.gif // ==/UserScript== -/* 4chan X Alpha - Version 3.0.0 - 2013-01-27 +/* 4chan X Alpha - Version 3.0.0 - 2013-02-09 * http://mayhemydg.github.com/4chan-x/ * * Copyright (c) 2009-2011 James Campos @@ -43,7 +43,7 @@ */ (function() { - var $, $$, Anonymize, ArchiveLink, AutoGIF, Board, Build, Clone, Conf, Config, DeleteLink, DownloadLink, FileInfo, Filter, Get, ImageHover, Main, Menu, Post, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, Quotify, Recursive, Redirect, ReplyHiding, ReportLink, RevealSpoilers, Sauce, Thread, ThreadHiding, ThreadUpdater, Time, UI, d, g, _base, + var $, $$, Anonymize, ArchiveLink, AutoGIF, Board, Build, Clone, Conf, Config, DeleteLink, DownloadLink, FileInfo, Filter, Get, Header, ImageHover, Main, Menu, Post, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, Quotify, Recursive, Redirect, ReplyHiding, ReportLink, RevealSpoilers, Sauce, Settings, Thread, ThreadHiding, ThreadUpdater, Time, UI, d, g, _base, __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }, __hasProp = {}.hasOwnProperty, __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; @@ -190,7 +190,7 @@ }; UI = (function() { - var dialog, drag, dragend, dragstart, hover, hoverend, hoverstart, touchend, touchmove; + var Menu, dialog, drag, dragend, dragstart, hover, hoverend, hoverstart, touchend, touchmove; dialog = function(id, position, html) { var el, move; el = d.createElement('div'); @@ -203,6 +203,226 @@ move.addEventListener('mousedown', dragstart, false); return el; }; + Menu = (function() { + var close, currentMenu, lastToggledButton; + + currentMenu = null; + + lastToggledButton = null; + + function Menu(type) { + this.type = type; + $.on(d, 'AddMenuEntry', this.addEntryListener.bind(this)); + this.close = close.bind(this); + this.entries = []; + } + + Menu.prototype.makeMenu = function() { + var menu; + menu = $.el('div', { + className: 'reply dialog', + id: 'menu', + tabIndex: 0 + }); + $.on(menu, 'click', function(e) { + return e.stopPropagation(); + }); + $.on(menu, 'keydown', this.keybinds.bind(this)); + return menu; + }; + + Menu.prototype.toggle = function(e, button, data) { + var previousButton; + e.preventDefault(); + e.stopPropagation(); + if (currentMenu) { + previousButton = lastToggledButton; + this.close(); + if (previousButton === button) { + return; + } + } + if (!this.entries.length) { + return; + } + return this.open(button, data); + }; + + Menu.prototype.open = function(button, data) { + var bLeft, bRect, bTop, cHeight, cWidth, entry, left, mRect, menu, style, top, _i, _len, _ref; + menu = this.makeMenu(); + currentMenu = menu; + lastToggledButton = button; + _ref = this.entries; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + entry = _ref[_i]; + this.insertEntry(entry, menu, data); + } + this.focus($('.entry', menu)); + $.on(d, 'click', this.close); + $.add(d.body, menu); + 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 = bRect.top + bRect.height + mRect.height < cHeight ? bTop + bRect.height + 2 : bTop - mRect.height - 2; + left = bRect.left + mRect.width < cWidth ? bLeft : bLeft + bRect.width - mRect.width; + style = menu.style; + style.top = top + 'px'; + style.left = left + 'px'; + return menu.focus(); + }; + + Menu.prototype.insertEntry = function(entry, parent, data) { + var child, submenu, _i, _len, _ref; + if (typeof entry.open === 'function') { + if (!entry.open(data)) { + return; + } + } + $.add(parent, entry.el); + if (!entry.children) { + return; + } + if (submenu = $('.submenu', entry.el)) { + $.rm(submenu); + } + submenu = $.el('div', { + className: 'reply dialog submenu' + }); + _ref = entry.children; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + child = _ref[_i]; + this.insertEntry(child, submenu, data); + } + $.add(entry.el, submenu); + }; + + close = function() { + $.rm(currentMenu); + currentMenu = null; + lastToggledButton = null; + return $.off(d, 'click', this.close); + }; + + Menu.prototype.keybinds = function(e) { + var entry, next, subEntry, submenu; + entry = $('.focused', currentMenu); + while (subEntry = $('.focused', entry)) { + entry = subEntry; + } + switch (e.keyCode) { + case 27: + lastToggledButton.focus(); + this.close(); + break; + case 13: + case 32: + entry.click(); + break; + case 38: + if (next = entry.previousElementSibling) { + this.focus(next); + } + break; + case 40: + if (next = entry.nextElementSibling) { + this.focus(next); + } + break; + case 39: + if ((submenu = $('.submenu', entry)) && (next = submenu.firstElementChild)) { + this.focus(next); + } + break; + case 37: + if (next = $.x('parent::*[contains(@class,"submenu")]/parent::*', entry)) { + this.focus(next); + } + break; + default: + return; + } + e.preventDefault(); + return e.stopPropagation(); + }; + + Menu.prototype.focus = function(entry) { + var bottom, cHeight, cWidth, eRect, focused, left, right, sRect, style, submenu, top, _i, _len, _ref; + while (focused = $.x('parent::*/child::*[contains(@class,"focused")]', entry)) { + $.rmClass(focused, 'focused'); + } + _ref = $$('.focused', entry); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + focused = _ref[_i]; + $.rmClass(focused, 'focused'); + } + $.addClass(entry, 'focused'); + if (!(submenu = $('.submenu', entry))) { + return; + } + 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; + style.top = top; + style.bottom = bottom; + style.left = left; + return style.right = right; + }; + + Menu.prototype.addEntry = function(entry) { + this.parseEntry(entry); + return this.entries.push(entry); + }; + + Menu.prototype.parseEntry = function(entry) { + var child, children, el, _i, _len; + el = entry.el, children = entry.children; + $.addClass(el, 'entry'); + $.on(el, 'focus mouseover', (function(e) { + e.stopPropagation(); + return this.focus(el); + }).bind(this)); + if (!children) { + return; + } + $.addClass(el, 'has-submenu'); + for (_i = 0, _len = children.length; _i < _len; _i++) { + child = children[_i]; + this.parseEntry(child); + } + }; + + Menu.prototype.addEntryListener = function(e) { + var entry; + entry = e.detail; + if (entry.type !== this.type) { + return; + } + return this.addEntry(entry); + }; + + return Menu; + + })(); dragstart = function(e) { var el, isTouching, o, rect, screenHeight, screenWidth; e.preventDefault(); @@ -250,33 +470,19 @@ } }; drag = function(e) { - var left, top; - left = e.clientX - this.dx; - top = e.clientY - this.dy; - if (left < 10) { - left = 0; - } else if (this.width - left < 10) { - left = null; - } - if (top < 10) { - top = 0; - } else if (this.height - top < 10) { - top = null; - } - if (left === null) { - this.style.left = null; - this.style.right = '0%'; - } else { - this.style.left = left / this.screenWidth * 100 + '%'; - this.style.right = null; - } - if (top === null) { - this.style.top = null; - return this.style.bottom = '0%'; - } else { - this.style.top = top / this.screenHeight * 100 + '%'; - return this.style.bottom = null; - } + var bottom, clientX, clientY, left, right, style, top; + clientX = e.clientX, clientY = e.clientY; + left = clientX - this.dx; + left = left < 10 ? 0 : this.width - left < 10 ? null : left / this.screenWidth * 100 + '%'; + top = clientY - this.dy; + top = top < 10 ? 0 : this.height - top < 10 ? null : top / this.screenHeight * 100 + '%'; + right = left === null ? 0 : null; + bottom = top === null ? 0 : null; + style = this.style; + style.left = left; + style.right = right; + style.top = top; + return style.bottom = bottom; }; touchend = function(e) { var touch, _i, _len, _ref; @@ -321,18 +527,22 @@ return root.addEventListener('mousemove', o.hover, false); }; hover = function(e) { - var clientX, height, top; + var clientX, clientY, height, left, right, style, top; height = this.el.offsetHeight; - top = e.clientY - 120; - this.style.top = this.clientHeight <= height || top <= 0 ? '0px' : top + height >= this.clientHeight ? this.clientHeight - height + 'px' : top + 'px'; - clientX = e.clientX; + clientX = e.clientX, clientY = e.clientY; + top = clientY - 120; + top = this.clientHeight <= height || top <= 0 ? 0 : top + height >= this.clientHeight ? this.clientHeight - height : top; if (clientX <= this.clientWidth - 400) { - this.style.left = clientX + 45 + 'px'; - return this.style.right = null; + left = clientX + 45 + 'px'; + right = null; } else { - this.style.left = null; - return this.style.right = this.clientWidth - clientX + 45 + 'px'; + left = null; + right = this.clientWidth - clientX + 45 + 'px'; } + style = this.style; + style.top = top + 'px'; + style.left = left; + return style.right = right; }; hoverend = function() { var event, _i, _len, _ref; @@ -349,6 +559,7 @@ }; return { dialog: dialog, + Menu: Menu, hover: hoverstart }; })(); @@ -482,20 +693,23 @@ return Conf[this.name] = this.value; } }, + asap: function(test, cb) { + if (test()) { + return cb(); + } else { + return setTimeout($.asap, 25, test, cb); + } + }, addStyle: function(css) { - var f, style; + var style; style = $.el('style', { textContent: css }); - f = function() { - var root; - if (root = d.head || d.documentElement) { - return $.add(root, style); - } else { - return setTimeout(f, 20); - } - }; - f(); + $.asap((function() { + return d.head; + }), (function() { + return $.add(d.head, style); + })); return style; }, x: function(path, root) { @@ -679,6 +893,135 @@ } }); + Header = { + init: function() { + var boardList, boardListButton, boardTitle, headerBar, menuButton, toggleBar; + this.menu = new UI.Menu('header'); + this.headerEl = $.el('div', { + id: 'header', + innerHTML: '
' + }); + headerBar = $('#header-bar', this.headerEl); + if ($.get('autohideHeaderBar', false)) { + $.addClass(headerBar, 'autohide'); + } + menuButton = $.el('a', { + className: 'menu-button', + innerHTML: '[]', + href: 'javascript:;' + }); + $.on(menuButton, 'click', this.menuToggle); + boardListButton = $.el('span', { + className: 'show-board-list-button', + innerHTML: '[+]', + title: 'Toggle the board list.' + }); + $.on(boardListButton, 'click', this.toggleBoardList); + boardTitle = $.el('a', { + className: 'board-name', + innerHTML: "/" + g.BOARD + "/ - ...", + 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', this.toggleBar); + $.prepend(headerBar, [menuButton, boardListButton, $.tn(' '), boardTitle, boardList, toggleBar]); + try { + this.setBoardList(); + } catch (err) { + $.log(err, 'Header - board list'); + } + return $.asap((function() { + return d.body; + }), function() { + return $.prepend(d.body, Header.headerEl); + }); + }, + setBoardList: function() { + Get.boardsConfig(function(boardsConfig) { + return $('.board-title', Header.headerEl).textContent = boardsConfig[g.BOARD].title; + }); + return $.ready(function() { + var nav, _ref; + if (nav = $.id('boardNavDesktop')) { + if ((_ref = $("a[href$='/" + g.BOARD + "/']", nav)) != null) { + _ref.className = 'current'; + } + return $.add($('.board-list', Header.headerEl), Array.prototype.slice.call(nav.childNodes)); + } + }); + }, + toggleBoardList: function() { + var headerEl, node, showBoardList; + node = this.firstElementChild.firstChild; + if (showBoardList = $.hasClass(this, 'show-board-list-button')) { + this.className = 'hide-board-list-button'; + node.data = node.data.replace('+', '-'); + } else { + this.className = 'show-board-list-button'; + node.data = node.data.replace('-', '+'); + } + headerEl = Header.headerEl; + $('.board-name', headerEl).hidden = showBoardList; + return $('.board-list', headerEl).hidden = !showBoardList; + }, + toggleBar: function() { + var bool; + bool = $.id('header-bar').classList.toggle('autohide'); + return $.set('autohideHeaderBar', bool); + }, + menuToggle: function(e) { + return Header.menu.toggle(e, this, g); + } + }; + + Settings = { + init: function() { + var link, settings; + link = $.el('a', { + className: 'settings-link', + textContent: '4chan X Settings', + href: 'javascript:;' + }); + $.on(link, 'click', Settings.open); + Header.menu.addEntry({ + el: link + }); + link = $.el('a', { + className: 'fourchan-settings-link', + textContent: '4chan Settings', + href: 'javascript:;' + }); + $.on(link, 'click', function() { + return $.id('settingsWindowLink').click(); + }); + Header.menu.addEntry({ + el: link, + open: function() { + return !Conf['Disable 4chan\'s extension']; + } + }); + if (!Conf['Disable 4chan\'s extension']) { + return; + } + settings = JSON.parse(localStorage.getItem('4chan-settings')) || {}; + if (settings.disableAll) { + return; + } + settings.disableAll = true; + return localStorage.setItem('4chan-settings', JSON.stringify(settings)); + }, + open: function() { + return Header.menu.close(); + } + }; + Filter = { filters: {}, init: function() { @@ -892,7 +1235,7 @@ type = _ref[_i]; entry.children.push(Filter.menu.createSubEntry(type[0], type[1])); } - return Menu.addEntry(entry); + return Menu.menu.addEntry(entry); }, createSubEntry: function(text, type) { var el; @@ -1038,7 +1381,7 @@ makeStub = $.el('label', { innerHTML: " Make stub" }); - return Menu.addEntry({ + return Menu.menu.addEntry({ el: div, open: function(post) { var thread; @@ -1240,7 +1583,7 @@ makeStub = $.el('label', { innerHTML: " Make stub" }); - return Menu.addEntry({ + return Menu.menu.addEntry({ el: div, open: function(post) { if (!post.isReply || post.isClone) { @@ -1426,24 +1769,21 @@ }; Menu = { - entries: [], init: function() { - $.on(d, 'AddMenuEntry', function(e) { - return Menu.addEntry(e.detail); - }); + this.menu = new UI.Menu('post'); return Post.prototype.callbacks.push({ name: 'Menu', cb: this.node }); }, node: function() { - var a; - a = Menu.makeButton(this); + var button; + button = Menu.makeButton(this); if (this.isClone) { - $.replace($('.menu-button', this.nodes.info), a); + $.replace($('.menu-button', this.nodes.info), button); return; } - return $.add(this.nodes.info, [$.tn('\u00A0'), a]); + return $.add(this.nodes.info, [$.tn('\u00A0'), button]); }, makeButton: function(post) { var a; @@ -1459,181 +1799,10 @@ $.on(a, 'click', Menu.toggle); return a; }, - makeMenu: function() { - var menu; - menu = $.el('div', { - className: 'reply dialog', - id: 'menu', - tabIndex: 0 - }); - $.on(menu, 'click', function(e) { - return e.stopPropagation(); - }); - $.on(menu, 'keydown', Menu.keybinds); - return menu; - }, toggle: function(e) { - var lastToggledButton, post; - e.preventDefault(); - e.stopPropagation(); - if (Menu.currentMenu) { - lastToggledButton = Menu.lastToggledButton; - Menu.close(); - if (lastToggledButton === this) { - return; - } - } - Menu.lastToggledButton = this; + var post; post = this.dataset.clone ? Get.postFromRoot($.x('ancestor::div[contains(@class,"postContainer")][1]', this)) : g.posts[this.dataset.postid]; - return Menu.open(this, post); - }, - open: function(button, post) { - var bLeft, bRect, bTop, entry, mRect, menu, _i, _len, _ref; - menu = Menu.makeMenu(); - Menu.currentMenu = menu; - _ref = Menu.entries; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - entry = _ref[_i]; - Menu.insertEntry(entry, menu, post); - } - Menu.focus($('.entry', menu)); - $.on(d, 'click', Menu.close); - $.add(d.body, menu); - 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 = bRect.top + bRect.height + mRect.height < d.documentElement.clientHeight ? bTop + bRect.height + 2 + 'px' : bTop - mRect.height - 2 + 'px'; - menu.style.left = bRect.left + mRect.width < d.documentElement.clientWidth ? bLeft + 'px' : bLeft + bRect.width - mRect.width + 'px'; - return menu.focus(); - }, - insertEntry: function(entry, parent, post) { - var child, submenu, _i, _len, _ref; - if (typeof entry.open === 'function') { - if (!entry.open(post)) { - return; - } - } - $.add(parent, entry.el); - if (!entry.children) { - return; - } - if (submenu = $('.submenu', entry.el)) { - $.rm(submenu); - } - submenu = $.el('div', { - className: 'reply dialog submenu' - }); - $.add(entry.el, submenu); - _ref = entry.children; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - child = _ref[_i]; - Menu.insertEntry(child, submenu, post); - } - }, - close: function() { - $.rm(Menu.currentMenu); - delete Menu.currentMenu; - delete Menu.lastToggledButton; - return $.off(d, 'click', Menu.close); - }, - keybinds: function(e) { - var entry, next, subEntry, submenu; - entry = $('.focused', Menu.currentMenu); - while (subEntry = $('.focused', entry)) { - entry = subEntry; - } - switch (Keybinds.keyCode(e) || e.keyCode) { - case 'Esc': - Menu.lastToggledButton.focus(); - Menu.close(); - break; - case 13: - case 32: - entry.click(); - break; - case 'Up': - if (next = entry.previousElementSibling) { - Menu.focus(next); - } - break; - case 'Down': - if (next = entry.nextElementSibling) { - Menu.focus(next); - } - break; - case 'Right': - if ((submenu = $('.submenu', entry)) && (next = submenu.firstElementChild)) { - Menu.focus(next); - } - break; - case 'Left': - if (next = $.x('parent::*[contains(@class,"submenu")]/parent::*', entry)) { - Menu.focus(next); - } - break; - default: - return; - } - e.preventDefault(); - return e.stopPropagation(); - }, - focus: function(entry) { - var bottom, eRect, focused, left, right, sRect, style, submenu, top, _i, _len, _ref; - while (focused = $.x('parent::*/child::*[contains(@class,"focused")]', entry)) { - $.rmClass(focused, 'focused'); - } - _ref = $$('.focused', entry); - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - focused = _ref[_i]; - $.rmClass(focused, 'focused'); - } - $.addClass(entry, 'focused'); - if (!(submenu = $('.submenu', entry))) { - return; - } - 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; - style.top = top; - style.bottom = bottom; - style.left = left; - return style.right = right; - }, - addEntry: function(entry) { - Menu.parseEntry(entry); - return Menu.entries.push(entry); - }, - parseEntry: function(entry) { - var child, children, el, _i, _len; - el = entry.el, children = entry.children; - $.addClass(el, 'entry'); - $.on(el, 'focus mouseover', function(e) { - e.stopPropagation(); - return Menu.focus(this); - }); - if (!children) { - return; - } - $.addClass(el, 'has-submenu'); - for (_i = 0, _len = children.length; _i < _len; _i++) { - child = children[_i]; - Menu.parseEntry(child); - } + return Menu.menu.toggle(e, this, post); } }; @@ -1646,7 +1815,7 @@ textContent: 'Report this post' }); $.on(a, 'click', ReportLink.report); - return Menu.addEntry({ + return Menu.menu.addEntry({ el: a, open: function(post) { ReportLink.post = post; @@ -1695,7 +1864,7 @@ return !!post.file; } }; - Menu.addEntry({ + Menu.menu.addEntry({ el: div, open: function(post) { var node, seconds; @@ -1802,7 +1971,7 @@ className: 'download-link', textContent: 'Download file' }); - return Menu.addEntry({ + return Menu.menu.addEntry({ el: a, open: function(post) { if (!post.file) { @@ -1840,7 +2009,7 @@ type = _ref[_i]; entry.children.push(this.createSubEntry(type[0], type[1])); } - return Menu.addEntry(entry); + return Menu.menu.addEntry(entry); }, createSubEntry: function(text, type) { var el, open; @@ -2182,6 +2351,55 @@ }; Get = { + boardsConfig: (function() { + var boardsConfig, callbacks, parseBoardsConfig; + boardsConfig = null; + callbacks = []; + parseBoardsConfig = function() { + var board, boardName, callback, _i, _j, _len, _len1, _ref; + if (this.status !== 200) { + return; + } + boardsConfig = {}; + _ref = JSON.parse(this.response).boards; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + board = _ref[_i]; + boardName = board.board; + delete board.board; + boardsConfig[boardName] = board; + } + for (_j = 0, _len1 = callbacks.length; _j < _len1; _j++) { + callback = callbacks[_j]; + callback(boardsConfig); + } + callbacks = null; + boardsConfig.lastModified = this.getResponseHeader('Last-Modified'); + return $.set('boardsConfig', boardsConfig); + }; + return function(cb) { + var lastModified; + if (boardsConfig) { + cb(boardsConfig); + return; + } + if (boardsConfig = $.get('boardsConfig', null)) { + cb(boardsConfig); + lastModified = boardsConfig.lastModified; + } else { + if (callbacks.push(cb) > 1) { + return; + } + lastModified = 0; + } + return $.ajax('//api.4chan.org/boards.json', { + onloadend: parseBoardsConfig + }, { + headers: { + 'If-Modified-Since': lastModified + } + }); + }; + })(), postFromRoot: function(root) { var board, index, link, post, postID; link = $('a[title="Highlight this post"]', root); @@ -3680,6 +3898,9 @@ _ref = Get.allQuotelinksLinkingTo(this); for (_i = 0, _len = _ref.length; _i < _len; _i++) { quotelink = _ref[_i]; + if ($.hasClass(quotelink, 'deadlink')) { + continue; + } $.add(quotelink, $.tn('\u00A0(Dead)')); $.addClass(quotelink, 'deadlink'); } @@ -3832,14 +4053,8 @@ g.THREAD = +pathname[3]; } switch (location.hostname) { - case 'boards.4chan.org': - Main.initHeader(); - if (g.VIEW === 'catalog') { - return; - } - return Main.initFeatures(); case 'sys.4chan.org': - break; + return; case 'images.4chan.org': $.ready(function() { var url; @@ -3850,48 +4065,27 @@ } } }); + return; + } + if (g.VIEW === 'catalog') { + return; } - }, - initHeader: function() { $.addStyle(Main.css); - Main.header = $.el('div', { - className: 'reply', - innerHTML: '
' - }); - return $.ready(Main.initHeaderReady); - }, - initHeaderReady: function() { - var header, nav, settings, _ref, _ref1, _ref2; - header = Main.header; - $.prepend(d.body, 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: '[Settings]' - }); - $.on(settings.firstElementChild, 'click', Main.settings); - $.add(nav, settings); - if ((_ref = $("a[href$='/" + g.BOARD + "/']", nav)) != null) { - _ref.className = 'current'; - } + $.asap((function() { + return d.body; + }), (function() { + $.addClass(d.body, $.engine); + return $.addClass(d.body, 'fourchan_x'); + })); + try { + Header.init(); + } catch (err) { + $.log(err, 'Header'); } - $.addClass(d.body, $.engine); - $.addClass(d.body, 'fourchan_x'); - if ((_ref1 = $('link[href*=mobile]', d.head)) != null) { - _ref1.disabled = true; - } - return (_ref2 = $.id('boardNavDesktopFoot')) != null ? _ref2.hidden = true : void 0; - }, - initFeatures: function() { - var settings; - if (Conf['Disable 4chan\'s extension']) { - settings = JSON.parse(localStorage.getItem('4chan-settings')) || {}; - settings.disableAll = true; - localStorage.setItem('4chan-settings', JSON.stringify(settings)); + try { + Settings.init(); + } catch (err) { + $.log(err, 'Settings'); } if (Conf['Resurrect Quotes']) { try { @@ -4073,10 +4267,10 @@ $.log(err, 'Thread Updater'); } } - return $.ready(Main.initFeaturesReady); + return $.ready(Main.initReady); }, - initFeaturesReady: function() { - var boardChild, posts, thread, threadChild, threads, _i, _j, _len, _len1, _ref, _ref1; + initReady: function() { + var boardChild, posts, thread, threadChild, threads, _i, _j, _len, _len1, _ref, _ref1, _ref2; if (d.title === '4chan - 404 Not Found') { if (Conf['404 Redirect'] && g.VIEW === 'thread') { location.href = Redirect.to({ @@ -4087,22 +4281,22 @@ } return; } - if (!$.id('navtopright')) { - return; + if ((_ref = $('link[href*=mobile]', d.head)) != null) { + _ref.disabled = true; } threads = []; posts = []; - _ref = $('.board').children; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - boardChild = _ref[_i]; + _ref1 = $('.board').children; + for (_i = 0, _len = _ref1.length; _i < _len; _i++) { + boardChild = _ref1[_i]; if (!$.hasClass(boardChild, 'thread')) { continue; } thread = new Thread(boardChild.id.slice(1), g.BOARD); threads.push(thread); - _ref1 = boardChild.children; - for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { - threadChild = _ref1[_j]; + _ref2 = boardChild.children; + for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) { + threadChild = _ref2[_j]; if (!$.hasClass(threadChild, 'postContainer')) { continue; } @@ -4132,10 +4326,7 @@ } } }, - settings: function() { - return alert('Here be settings'); - }, - css: "/* general */\n.dialog.reply {\n display: block;\n border: 1px solid rgba(0, 0, 0, .25);\n padding: 0;\n}\n.move {\n cursor: move;\n}\nlabel {\n cursor: pointer;\n}\na[href=\"javascript:;\"] {\n text-decoration: none;\n}\n.warning {\n color: red;\n}\n\n/* 4chan style fixes */\n.opContainer, .op {\n display: block !important;\n}\n.post {\n overflow: visible !important;\n}\n[hidden] {\n display: none !important;\n}\n\n/* fixed, z-index */\n#qp, #ihover,\n#updater, #stats,\n#boardNavDesktop.reply,\n#qr, #watcher {\n position: fixed;\n}\n#qp, #ihover {\n z-index: 100;\n}\n#updater, #stats {\n z-index: 90;\n}\n#boardNavDesktop.reply:hover {\n z-index: 80;\n}\n#qr {\n z-index: 50;\n}\n#watcher {\n z-index: 30;\n}\n#boardNavDesktop.reply {\n z-index: 10;\n}\n\n\n/* header */\nbody.fourchan_x {\n margin-top: 2.5em;\n}\n#boardNavDesktop.reply {\n border-width: 0 0 1px;\n padding: 4px;\n top: 0;\n right: 0;\n left: 0;\n transition: opacity .1s ease-in-out;\n -o-transition: opacity .1s ease-in-out;\n -moz-transition: opacity .1s ease-in-out;\n -webkit-transition: opacity .1s ease-in-out;\n}\n#boardNavDesktop.reply:not(:hover) {\n opacity: .4;\n transition: opacity 1.5s .5s ease-in-out;\n -o-transition: opacity 1.5s .5s ease-in-out;\n -moz-transition: opacity 1.5s .5s ease-in-out;\n -webkit-transition: opacity 1.5s .5s ease-in-out;\n}\n#boardNavDesktop.reply a {\n margin: -1px;\n}\n#settings {\n float: right;\n}\n\n/* thread updater */\n#updater {\n text-align: right;\n}\n#updater:not(:hover) {\n background: none;\n border: none;\n}\n#updater input[type=number] {\n width: 4em;\n}\n#updater:not(:hover) > div:not(.move) {\n display: none;\n}\n.new {\n color: limegreen;\n}\n\n/* quote */\n.quotelink.deadlink {\n text-decoration: underline !important;\n}\n.deadlink:not(.quotelink) {\n text-decoration: none !important;\n}\n.inlined {\n opacity: .5;\n}\n#qp input, .forwarded {\n display: none;\n}\n.quotelink.forwardlink,\n.backlink.forwardlink {\n text-decoration: none;\n border-bottom: 1px dashed;\n}\n.filtered {\n text-decoration: underline line-through;\n}\n.inline {\n border: 1px solid rgba(128, 128, 128, .5);\n display: table;\n margin: 2px 0;\n}\n.inline .post {\n border: 0 !important;\n display: table !important;\n margin: 0 !important;\n padding: 1px 2px !important;\n}\n#qp {\n padding: 2px 2px 5px;\n}\n#qp .post {\n border: none;\n margin: 0;\n padding: 0;\n}\n#qp img {\n max-height: 300px;\n max-width: 500px;\n}\n.qphl {\n box-shadow: 0 0 0 2px rgba(216, 94, 49, .7);\n}\n\n/* file */\n.fileText:hover .fntrunc,\n.fileText:not(:hover) .fnfull {\n display: none;\n}\n#ihover {\n box-sizing: border-box;\n -moz-box-sizing: border-box;\n max-height: 100%;\n max-width: 75%;\n padding-bottom: 16px;\n}\n\n/* Filter */\n.opContainer.filter-highlight {\n box-shadow: inset 5px 0 rgba(255, 0, 0, .5);\n}\n.opContainer.filter-highlight.qphl {\n box-shadow: inset 5px 0 rgba(255, 0, 0, .5),\n 0 0 0 2px rgba(216, 94, 49, .7);\n}\n.filter-highlight > .reply {\n box-shadow: -5px 0 rgba(255, 0, 0, .5);\n}\n.filter-highlight > .reply.qphl {\n box-shadow: -5px 0 rgba(255, 0, 0, .5),\n 0 0 0 2px rgba(216, 94, 49, .7)\n}\n\n/* Thread & Reply Hiding */\n.hide-thread-button,\n.hide-reply-button {\n float: left;\n margin-right: 2px;\n}\n.stub ~ .sideArrows,\n.stub ~ .hide-reply-button,\n.stub ~ .post {\n display: none !important;\n}\n\n/* Menu */\n.menu-button {\n display: inline-block;\n}\n.menu-button > span {\n border-top: 6px solid;\n border-right: 4px solid transparent;\n border-left: 4px solid transparent;\n display: inline-block;\n margin: 2px;\n vertical-align: middle;\n}\n#menu {\n position: absolute;\n outline: none;\n}\n.entry {\n border-bottom: 1px solid rgba(0, 0, 0, .25);\n cursor: pointer;\n display: block;\n outline: none;\n padding: 3px 7px;\n position: relative;\n text-decoration: none;\n white-space: nowrap;\n}\n.entry:last-child {\n border: none;\n}\n.focused.entry {\n background: rgba(255, 255, 255, .33);\n}\n.entry.has-submenu {\n padding-right: 20px;\n}\n.has-submenu::after {\n content: \"\";\n border-left: 6px solid;\n border-top: 4px solid transparent;\n border-bottom: 4px solid transparent;\n display: inline-block;\n margin: 4px;\n position: absolute;\n right: 3px;\n}\n.has-submenu:not(.focused) > .submenu {\n display: none;\n}\n.submenu {\n position: absolute;\n margin: -1px 0;\n}\n.entry input {\n margin: 0;\n}" + css: "/* general */\n.dialog.reply {\n display: block;\n border: 1px solid rgba(0, 0, 0, .25);\n padding: 0;\n}\n.move {\n cursor: move;\n}\nlabel {\n cursor: pointer;\n}\na[href=\"javascript:;\"] {\n text-decoration: none;\n}\n.warning {\n color: red;\n}\n\n/* 4chan style fixes */\n.opContainer, .op {\n display: block !important;\n}\n.post {\n overflow: visible !important;\n}\n[hidden] {\n display: none !important;\n}\n\n/* fixed, z-index */\n#qp, #ihover,\n#updater, #stats,\n#header,\n#qr, #watcher {\n position: fixed;\n}\n#qp, #ihover {\n z-index: 100;\n}\n#menu {\n z-index: 95;\n}\n#updater, #stats {\n z-index: 90;\n}\n#header:hover {\n z-index: 80;\n}\n#qr {\n z-index: 50;\n}\n#watcher {\n z-index: 30;\n}\n#header {\n z-index: 10;\n}\n\n/* XXX support different styles */\n#header-bar {\n font-size: 9pt;\n color: #89A;\n background-color: #D6DAF0;\n border-color: #B7C5D9;\n border-width: 0 0 1px;\n border-style: solid;\n}\n\n/* header */\nbody.fourchan_x {\n margin-top: 2em;\n}\n#header {\n top: 0;\n right: 0;\n left: 0;\n}\n#header-bar {\n padding: 4px;\n position: relative;\n transition: all .1s ease-in-out;\n -o-transition: all .1s ease-in-out;\n -moz-transition: all .1s ease-in-out;\n -webkit-transition: all .1s ease-in-out;\n}\n#header-bar.autohide:not(:hover) {\n transform: translateY(-100%);\n -o-transform: translateY(-100%);\n -moz-transform: translateY(-100%);\n -webkit-transform: translateY(-100%);\n transition: all .75s .25s ease-in-out;\n -o-transition: all .75s .25s ease-in-out;\n -moz-transition: all .75s .25s ease-in-out;\n -webkit-transition: all .75s .25s ease-in-out;\n}\n#toggle-header-bar {\n cursor: n-resize;\n left: 0;\n right: 0;\n bottom: -8px;\n height: 10px;\n position: absolute;\n}\n#header-bar.autohide #toggle-header-bar {\n cursor: s-resize;\n}\n#header-bar a {\n text-decoration: none;\n padding: 1px;\n}\n#header-bar > .menu-button {\n float: right;\n padding: 0;\n}\nbody > #boardNavDesktop,\n#navtopright,\n#boardNavDesktopFoot {\n display: none !important;\n}\n\n/* thread updater */\n#updater {\n text-align: right;\n}\n#updater:not(:hover) {\n background: none;\n border: none;\n}\n#updater input[type=number] {\n width: 4em;\n}\n#updater:not(:hover) > div:not(.move) {\n display: none;\n}\n.new {\n color: limegreen;\n}\n\n/* quote */\n.quotelink.deadlink {\n text-decoration: underline !important;\n}\n.deadlink:not(.quotelink) {\n text-decoration: none !important;\n}\n.inlined {\n opacity: .5;\n}\n#qp input, .forwarded {\n display: none;\n}\n.quotelink.forwardlink,\n.backlink.forwardlink {\n text-decoration: none;\n border-bottom: 1px dashed;\n}\n.filtered {\n text-decoration: underline line-through;\n}\n.inline {\n border: 1px solid rgba(128, 128, 128, .5);\n display: table;\n margin: 2px 0;\n}\n.inline .post {\n border: 0 !important;\n display: table !important;\n margin: 0 !important;\n padding: 1px 2px !important;\n}\n#qp {\n padding: 2px 2px 5px;\n}\n#qp .post {\n border: none;\n margin: 0;\n padding: 0;\n}\n#qp img {\n max-height: 300px;\n max-width: 500px;\n}\n.qphl {\n box-shadow: 0 0 0 2px rgba(216, 94, 49, .7);\n}\n\n/* file */\n.fileText:hover .fntrunc,\n.fileText:not(:hover) .fnfull {\n display: none;\n}\n#ihover {\n box-sizing: border-box;\n -moz-box-sizing: border-box;\n max-height: 100%;\n max-width: 75%;\n padding-bottom: 16px;\n}\n\n/* Filter */\n.opContainer.filter-highlight {\n box-shadow: inset 5px 0 rgba(255, 0, 0, .5);\n}\n.opContainer.filter-highlight.qphl {\n box-shadow: inset 5px 0 rgba(255, 0, 0, .5),\n 0 0 0 2px rgba(216, 94, 49, .7);\n}\n.filter-highlight > .reply {\n box-shadow: -5px 0 rgba(255, 0, 0, .5);\n}\n.filter-highlight > .reply.qphl {\n box-shadow: -5px 0 rgba(255, 0, 0, .5),\n 0 0 0 2px rgba(216, 94, 49, .7)\n}\n\n/* Thread & Reply Hiding */\n.hide-thread-button,\n.hide-reply-button {\n float: left;\n margin-right: 2px;\n}\n.stub ~ .sideArrows,\n.stub ~ .hide-reply-button,\n.stub ~ .post {\n display: none !important;\n}\n\n/* Menu */\n.menu-button {\n display: inline-block;\n}\n.menu-button > span {\n border-top: 6px solid;\n border-right: 4px solid transparent;\n border-left: 4px solid transparent;\n display: inline-block;\n margin: 2px;\n vertical-align: middle;\n}\n#menu {\n position: absolute;\n outline: none;\n}\n.entry {\n border-bottom: 1px solid rgba(0, 0, 0, .25);\n cursor: pointer;\n display: block;\n outline: none;\n padding: 3px 7px;\n position: relative;\n text-decoration: none;\n white-space: nowrap;\n}\n.entry:last-child {\n border: none;\n}\n.focused.entry {\n background: rgba(255, 255, 255, .33);\n}\n.entry.has-submenu {\n padding-right: 20px;\n}\n.has-submenu::after {\n content: \"\";\n border-left: 6px solid;\n border-top: 4px solid transparent;\n border-bottom: 4px solid transparent;\n display: inline-block;\n margin: 4px;\n position: absolute;\n right: 3px;\n}\n.has-submenu:not(.focused) > .submenu {\n display: none;\n}\n.submenu {\n position: absolute;\n margin: -1px 0;\n}\n.entry input {\n margin: 0;\n}" }; Main.init(); diff --git a/changelog b/changelog index f6e40629e..84dc78d7e 100644 --- a/changelog +++ b/changelog @@ -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. diff --git a/css/style.css b/css/style.css index 0056fb247..e7f7f150a 100644 --- a/css/style.css +++ b/css/style.css @@ -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 */ diff --git a/lib/$.coffee b/lib/$.coffee index 8621e6040..c1d923466 100644 --- a/lib/$.coffee +++ b/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 diff --git a/lib/ui.coffee b/lib/ui.coffee index 9380bd3fa..2db8dad3c 100644 --- a/lib/ui.coffee +++ b/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 } )() diff --git a/src/features.coffee b/src/features.coffee index a447e6fda..b092153ba 100644 --- a/src/features.coffee +++ b/src/features.coffee @@ -1,3 +1,109 @@ +Header = + init: -> + @menu = new UI.Menu 'header' + + @headerEl = $.el 'div', + id: 'header' + innerHTML: '
' + + headerBar = $('#header-bar', @headerEl) + if $.get 'autohideHeaderBar', false + $.addClass headerBar, 'autohide' + + menuButton = $.el 'a', + className: 'menu-button' + innerHTML: '[]' + href: 'javascript:;' + $.on menuButton, 'click', @menuToggle + + boardListButton = $.el 'span', + className: 'show-board-list-button' + innerHTML: '[+]' + title: 'Toggle the board list.' + $.on boardListButton, 'click', @toggleBoardList + + boardTitle = $.el 'a', + className: 'board-name' + innerHTML: "/#{g.BOARD}/ - ..." + 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: " Make stub" - Menu.addEntry + Menu.menu.addEntry el: div open: (post) -> {thread} = post @@ -493,7 +599,7 @@ ReplyHiding = makeStub = $.el 'label', innerHTML: " 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] diff --git a/src/main.coffee b/src/main.coffee index b6f81ce61..e29798627 100644 --- a/src/main.coffee +++ b/src/main.coffee @@ -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: '
' - $.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: '[Settings]' - $.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()