#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.
This commit is contained in:
Andrew Brehaut 2019-09-22 14:06:51 +12:00
parent 20f8fe91df
commit 5fea81971b
3 changed files with 159 additions and 107 deletions

View File

@ -3,6 +3,7 @@
<style> <style>
</style> </style>
<script src="main.js"></script> <script src="main.js"></script>
<script src="newsfoot.js" async="async"></script>
</head> </head>
<body> <body>
</body> </body>

View File

@ -181,3 +181,51 @@ img[src*="feedblitz"],
img[src*="share-buttons"] { img[src*="share-buttons"] {
display: none !important; 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;
}

View File

@ -1,119 +1,122 @@
(function () {
// @ts-check
/** @param {Node | null} el */
const remove = (el) => { if (el) el.parentElement.removeChild(el) };
// @ts-check const stripPx = (s) => +s.slice(0, -2);
/** @param {Node | null} el */
const remove = (el) => { if (el) el.parentElement.removeChild(el) };
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 /** @type {<T extends any[]>(fn: (...args: T) => void, t: number) => ((...args: T) => void)} */
* @param {string} cls function debounce(f, ms) {
* @returns HTMLElement let t = Date.now();
*/ return (...args) => {
function newEl(tag, cls) { const now = Date.now();
const el = document.createElement(tag); if (now - t < ms) return;
el.classList.add(cls); t = now;
return el; f(...args);
} };
}
/** @type {<T extends any[]>(fn: (...args: T) => void, t: number) => ((...args: T) => void)} */ const clsPrefix = "newsfoot-footnote-";
function debounce(f, ms) { const CONTAINER_CLS = `${clsPrefix}container`;
let t = Date.now(); const POPOVER_CLS = `${clsPrefix}popover`;
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`;
/**
* @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 {Node} content
* @param {Element} fnref * @returns {HTMLElement}
*/ */
constructor(content, fnref) { function footnoteMarkup(content) {
this.popover = footnoteMarkup(content); const popover = newEl("div", POPOVER_CLS);
this.style = window.getComputedStyle(this.popover); popover.appendChild(content);
this.fnref = fnref; return popover;
this.fnref.closest(`.${CONTAINER_CLS}`).appendChild(this.popover); }
this.reposition();
/** @type {(ev:MouseEvent) => void} */ class Footnote {
this.clickoutHandler = (ev) => { /**
if (!(ev.target instanceof Element)) return; * @param {Node} content
if (ev.target.closest(`.${POPOVER_CLS}`) === this.popover) return; * @param {Element} fnref
this.cleanup(); */
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);
} }
document.addEventListener("click", this.clickoutHandler, {capture: true});
this.resizeHandler = debounce(() => this.reposition(), 20); cleanup() {
window.addEventListener("resize", this.resizeHandler); remove(this.popover);
} document.removeEventListener("click", this.clickoutHandler, {capture: true});
window.removeEventListener("resize", this.resizeHandler);
cleanup() { delete this.popover;
remove(this.popover); delete this.clickoutHandler;
document.removeEventListener("click", this.clickoutHandler, {capture: true}); delete this.resizeHandler;
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; 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)`;
} }
this.popover.style.transform = `translate(${offset}px)`;
} }
}
/** @param {Node} n */ /** @param {Node} n */
function fragFromContents(n) { function fragFromContents(n) {
const frag = document.createDocumentFragment(); const frag = document.createDocumentFragment();
n.childNodes.forEach((ch) => frag.appendChild(ch)); n.childNodes.forEach((ch) => frag.appendChild(ch));
return frag; 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) => { /** @param {HTMLAnchorElement} a */
if (!(ev.target && ev.target instanceof HTMLAnchorElement)) return; function installContainer(a) {
if (!ev.target.matches(".footnote")) return; if (!a.parentElement.matches(`.${CONTAINER_CLS}`)) {
ev.preventDefault(); const container = newEl("div", CONTAINER_CLS);
a.parentElement.insertBefore(container, a);
container.appendChild(a);
}
}
const content = document.querySelector(`[id='${ev.target.hash.substring(1)}']`).cloneNode(true); document.addEventListener("click", (ev) => {
if (content instanceof HTMLElement) remove(content.querySelector(".reversefootnote")); if (!(ev.target && ev.target instanceof HTMLAnchorElement)) return;
installContainer(ev.target); if (!ev.target.matches(".footnote")) return;
void new Footnote(fragFromContents(content), ev.target); 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);
});
}());