mirror of
https://github.com/Ranchero-Software/NetNewsWire.git
synced 2024-12-11 08:56:54 +01:00
4c54d2c4ff
An arrow element is now inserted into the dom to orient the bubble to the fnref element that opened it. This is offset in the opposite direction to the bubble if the bubble overhangs one side or the other. Additionally, if both sides overhang then no positioning takes place. This is the first step towards supporting more narrow windows and mobile clients.
148 lines
4.5 KiB
JavaScript
148 lines
4.5 KiB
JavaScript
(function () {
|
|
// @ts-check
|
|
/** @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;
|
|
}
|
|
|
|
/** @type {<T extends any[]>(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 POPOVER_INNER_CLS = `${clsPrefix}popover-inner`;
|
|
const POPOVER_ARROW_CLS = `${clsPrefix}popover-arrow`;
|
|
|
|
/**
|
|
* @param {Node} content
|
|
* @returns {HTMLElement}
|
|
*/
|
|
function footnoteMarkup(content) {
|
|
const popover = newEl("div", POPOVER_CLS);
|
|
const arrow = newEl("div", POPOVER_ARROW_CLS);
|
|
const inner = newEl("div", POPOVER_INNER_CLS);
|
|
popover.appendChild(inner);
|
|
popover.appendChild(arrow);
|
|
inner.appendChild(content);
|
|
return popover;
|
|
}
|
|
|
|
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}`).insertBefore(this.popover, fnref);
|
|
this.reposition();
|
|
|
|
/** @type {(ev:MouseEvent) => void} */
|
|
this.clickoutHandler = (ev) => {
|
|
if (!(ev.target instanceof Element)) return;
|
|
if (ev.target.closest(`.${POPOVER_CLS}`) === this.popover) return;
|
|
if (ev.target === this.fnref) {
|
|
ev.stopPropagation();
|
|
ev.preventDefault();
|
|
}
|
|
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);
|
|
|
|
const rightOverhang = center + popoverHalfWidth + marginRight > window.innerWidth;
|
|
const leftOverhang = center - (popoverHalfWidth + marginLeft) < 0;
|
|
|
|
let offset = 0;
|
|
if (leftOverhang && !rightOverhang) {
|
|
offset = -((center + popoverHalfWidth + marginRight) - window.innerWidth);
|
|
}
|
|
else if (rightOverhang && !leftOverhang) {
|
|
offset = (popoverHalfWidth + marginLeft) - center;
|
|
}
|
|
this.popover.style.transform = `translate(${offset}px)`;
|
|
this.popover.querySelector(`.${POPOVER_ARROW_CLS}`).style.transform = `translate(${-offset}px) rotate(45deg)`;
|
|
}
|
|
}
|
|
|
|
/** @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);
|
|
}
|
|
}
|
|
|
|
// Handle clicks on the footnote reference
|
|
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);
|
|
installContainer(ev.target);
|
|
void new Footnote(fragFromContents(content), ev.target);
|
|
});
|
|
|
|
// Handle clicks on the footnote reverse link
|
|
document.addEventListener("click", (ev) =>
|
|
{
|
|
if (!(ev.target && ev.target instanceof HTMLAnchorElement)) return;
|
|
if (!ev.target.matches(".footnotes .reversefootnote")) return;
|
|
const hash = ev.target.hash;
|
|
if (!hash) return;
|
|
const fnref = document.getElementById(hash.substring(1));
|
|
|
|
window.scrollTo({ top: fnref.getBoundingClientRect().top + window.scrollY });
|
|
ev.preventDefault();
|
|
});
|
|
}());
|