From 5fea81971b175f98c59f3f0cded608aecc42d0b5 Mon Sep 17 00:00:00 2001 From: Andrew Brehaut Date: Sun, 22 Sep 2019 14:06:51 +1200 Subject: [PATCH] #554 newsfoot.js and css are included in the page These changes are the bare minimum required to get footnotes to appear and function on the article page. * The newsfoot.js script now wraps everything in an IIFE to prevent bleed to other scripts * Stylesheets are included in the main stylesheet, with the colors extracted out into separate selectors Currently missing the arrow pointing to the footnote link, and no consideration exists for mobile presentation beyond a max-width: 100vh on the footnote popover. --- Mac/MainWindow/Detail/page.html | 1 + Mac/MainWindow/Detail/styleSheet.css | 48 ++++++ Shared/Article Rendering/newsfoot.js | 217 ++++++++++++++------------- 3 files changed, 159 insertions(+), 107 deletions(-) diff --git a/Mac/MainWindow/Detail/page.html b/Mac/MainWindow/Detail/page.html index 97e7c4541..7482a74b0 100644 --- a/Mac/MainWindow/Detail/page.html +++ b/Mac/MainWindow/Detail/page.html @@ -3,6 +3,7 @@ + diff --git a/Mac/MainWindow/Detail/styleSheet.css b/Mac/MainWindow/Detail/styleSheet.css index 107a7cb7e..c39d376ce 100644 --- a/Mac/MainWindow/Detail/styleSheet.css +++ b/Mac/MainWindow/Detail/styleSheet.css @@ -181,3 +181,51 @@ img[src*="feedblitz"], img[src*="share-buttons"] { display: none !important; } + + +/* Newsfoot specific styles. Structural styles come first, theme styles second */ +.newsfoot-footnote-container { + position: relative; + display: inline-block; +} +.newsfoot-footnote-popover { + position: absolute; + display: block; + padding: 0em 1em; + margin: 1em; + left: -11em; + right: -11em; + max-width: none; + border-radius: 0.3em; + box-sizing: border-box; +} +a.footnote { + display: inline-block; + text-decoration: none; + padding: 0.05em 0.75em; + border-radius: 1em; + min-width: 1em; + text-align: center; + font-size: 0.8em; + line-height: 1em; + position:relative; + top: -0.1em; +} + +/* light / default */ +.newsfoot-footnote-popover { + background: #fafafa; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); + color: black; + border: 1px solid #ccc; +} +body a.footnote, +body a.footnote:visited { + background: #aaa; + color: white; + transition: background-color 200ms ease-out; +} +a.footnote:hover { + background: #666; + transition: background-color 200ms ease-out; +} diff --git a/Shared/Article Rendering/newsfoot.js b/Shared/Article Rendering/newsfoot.js index ca725c19e..7374610e7 100644 --- a/Shared/Article Rendering/newsfoot.js +++ b/Shared/Article Rendering/newsfoot.js @@ -1,119 +1,122 @@ + (function () { + // @ts-check + /** @param {Node | null} el */ + const remove = (el) => { if (el) el.parentElement.removeChild(el) }; -// @ts-check -/** @param {Node | null} el */ -const remove = (el) => { if (el) el.parentElement.removeChild(el) }; + const stripPx = (s) => +s.slice(0, -2); -const stripPx = (s) => +s.slice(0, -2); + /** @param {string} tag + * @param {string} cls + * @returns HTMLElement + */ + function newEl(tag, cls) { + const el = document.createElement(tag); + el.classList.add(cls); + return el; + } -/** @param {string} tag - * @param {string} cls - * @returns HTMLElement - */ -function newEl(tag, cls) { - const el = document.createElement(tag); - el.classList.add(cls); - return el; -} + /** @type {(fn: (...args: T) => void, t: number) => ((...args: T) => void)} */ + function debounce(f, ms) { + let t = Date.now(); + return (...args) => { + const now = Date.now(); + if (now - t < ms) return; + t = now; + f(...args); + }; + } -/** @type {(fn: (...args: T) => void, t: number) => ((...args: T) => void)} */ -function debounce(f, ms) { - let t = Date.now(); - return (...args) => { - const now = Date.now(); - if (now - t < ms) return; - t = now; - f(...args); - }; -} + const clsPrefix = "newsfoot-footnote-"; + const CONTAINER_CLS = `${clsPrefix}container`; + const POPOVER_CLS = `${clsPrefix}popover`; -const clsPrefix = "newsfoot-footnote-"; -const CONTAINER_CLS = `${clsPrefix}container`; -const POPOVER_CLS = `${clsPrefix}popover`; - -/** - * @param {Node} content - * @returns {HTMLElement} - */ -function footnoteMarkup(content) { - const popover = newEl("div", POPOVER_CLS); - popover.appendChild(content); - return popover; -} - -class Footnote { /** * @param {Node} content - * @param {Element} fnref + * @returns {HTMLElement} */ - constructor(content, fnref) { - this.popover = footnoteMarkup(content); - this.style = window.getComputedStyle(this.popover); - this.fnref = fnref; - this.fnref.closest(`.${CONTAINER_CLS}`).appendChild(this.popover); - this.reposition(); - - /** @type {(ev:MouseEvent) => void} */ - this.clickoutHandler = (ev) => { - if (!(ev.target instanceof Element)) return; - if (ev.target.closest(`.${POPOVER_CLS}`) === this.popover) return; - this.cleanup(); - } - document.addEventListener("click", this.clickoutHandler, {capture: true}); - - this.resizeHandler = debounce(() => this.reposition(), 20); - window.addEventListener("resize", this.resizeHandler); + function footnoteMarkup(content) { + const popover = newEl("div", POPOVER_CLS); + popover.appendChild(content); + return popover; } - - cleanup() { - remove(this.popover); - document.removeEventListener("click", this.clickoutHandler, {capture: true}); - window.removeEventListener("resize", this.resizeHandler); - delete this.popover; - delete this.clickoutHandler; - delete this.resizeHandler; - } - - reposition() { - const refRect = this.fnref.getBoundingClientRect(); - const center = refRect.left + (refRect.width / 2); - const popoverHalfWidth = this.popover.clientWidth / 2; - const marginLeft = stripPx(this.style.marginLeft); - const marginRight = stripPx(this.style.marginRight); - - let offset = 0; - if (center + popoverHalfWidth + marginRight > window.innerWidth) { - offset = -((center + popoverHalfWidth + marginRight) - window.innerWidth); - } - else if (center - (popoverHalfWidth + marginLeft) < 0) { - offset = (popoverHalfWidth + marginLeft) - center; - } - this.popover.style.transform = `translate(${offset}px)`; - } -} -/** @param {Node} n */ -function fragFromContents(n) { - const frag = document.createDocumentFragment(); - n.childNodes.forEach((ch) => frag.appendChild(ch)); - return frag; -} - -/** @param {HTMLAnchorElement} a */ -function installContainer(a) { - if (!a.parentElement.matches(`.${CONTAINER_CLS}`)) { - const container = newEl("div", CONTAINER_CLS); - a.parentElement.insertBefore(container, a); - container.appendChild(a); + class Footnote { + /** + * @param {Node} content + * @param {Element} fnref + */ + constructor(content, fnref) { + this.popover = footnoteMarkup(content); + this.style = window.getComputedStyle(this.popover); + this.fnref = fnref; + this.fnref.closest(`.${CONTAINER_CLS}`).appendChild(this.popover); + this.reposition(); + + /** @type {(ev:MouseEvent) => void} */ + this.clickoutHandler = (ev) => { + if (!(ev.target instanceof Element)) return; + if (ev.target.closest(`.${POPOVER_CLS}`) === this.popover) return; + this.cleanup(); + } + document.addEventListener("click", this.clickoutHandler, {capture: true}); + + this.resizeHandler = debounce(() => this.reposition(), 20); + window.addEventListener("resize", this.resizeHandler); + } + + cleanup() { + remove(this.popover); + document.removeEventListener("click", this.clickoutHandler, {capture: true}); + window.removeEventListener("resize", this.resizeHandler); + delete this.popover; + delete this.clickoutHandler; + delete this.resizeHandler; + } + + reposition() { + const refRect = this.fnref.getBoundingClientRect(); + const center = refRect.left + (refRect.width / 2); + const popoverHalfWidth = this.popover.clientWidth / 2; + const marginLeft = stripPx(this.style.marginLeft); + const marginRight = stripPx(this.style.marginRight); + + let offset = 0; + if (center + popoverHalfWidth + marginRight > window.innerWidth) { + offset = -((center + popoverHalfWidth + marginRight) - window.innerWidth); + } + else if (center - (popoverHalfWidth + marginLeft) < 0) { + offset = (popoverHalfWidth + marginLeft) - center; + } + this.popover.style.transform = `translate(${offset}px)`; + } } -} -document.addEventListener("click", (ev) => { - if (!(ev.target && ev.target instanceof HTMLAnchorElement)) return; - if (!ev.target.matches(".footnote")) return; - ev.preventDefault(); - - const content = document.querySelector(`[id='${ev.target.hash.substring(1)}']`).cloneNode(true); - if (content instanceof HTMLElement) remove(content.querySelector(".reversefootnote")); - installContainer(ev.target); - void new Footnote(fragFromContents(content), ev.target); - }); + /** @param {Node} n */ + function fragFromContents(n) { + const frag = document.createDocumentFragment(); + n.childNodes.forEach((ch) => frag.appendChild(ch)); + return frag; + } + + /** @param {HTMLAnchorElement} a */ + function installContainer(a) { + if (!a.parentElement.matches(`.${CONTAINER_CLS}`)) { + const container = newEl("div", CONTAINER_CLS); + a.parentElement.insertBefore(container, a); + container.appendChild(a); + } + } + + document.addEventListener("click", (ev) => { + if (!(ev.target && ev.target instanceof HTMLAnchorElement)) return; + if (!ev.target.matches(".footnote")) return; + ev.preventDefault(); + + const content = document.querySelector(`[id='${ev.target.hash.substring(1)}']`).cloneNode(true); + if (content instanceof HTMLElement) { + remove(content.querySelector(".reversefootnote")); + } + installContainer(ev.target); + void new Footnote(fragFromContents(content), ev.target); + }); +}());