262 lines
6.0 KiB
JavaScript
262 lines
6.0 KiB
JavaScript
"use strict";
|
|
|
|
class IPUBElement extends HTMLElement {
|
|
static observedAttributes = ["id"];
|
|
|
|
connectedCallback() {
|
|
this.#ensureID();
|
|
}
|
|
|
|
attributeChangedCallback(_name, _oldValue, _newValue) {
|
|
this.#ensureID();
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
#ensureID() {
|
|
if (!this.id) {
|
|
this.id = hashFromHTML(this);
|
|
}
|
|
}
|
|
}
|
|
|
|
class IPUBBackground extends IPUBElement {
|
|
static elementName = "ipub-background";
|
|
static observedAttributes = ["nofade"].concat(super.observedAttributes);
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
static #observer = (() => {
|
|
/** @type {Map<string, IPUBBackground>} */
|
|
const instancesOnScreen = new Map();
|
|
|
|
document.addEventListener("scroll", () => {
|
|
for (const [_, instance] of instancesOnScreen) {
|
|
const perc = getPercentageInView(
|
|
instance.querySelector("img") || instance,
|
|
);
|
|
instance.fade(perc);
|
|
}
|
|
});
|
|
|
|
return new IntersectionObserver((entries) =>
|
|
entries.forEach((e) => {
|
|
let instance = e.target.parentElement;
|
|
|
|
if (
|
|
instance.tagName.toLowerCase() !==
|
|
IPUBBackground.elementName.toLowerCase()
|
|
) {
|
|
instance = instance.parentElement;
|
|
}
|
|
|
|
if (
|
|
instance.tagName.toLowerCase() !==
|
|
IPUBBackground.elementName.toLowerCase()
|
|
) {
|
|
console.error(
|
|
"IPUBBackground: malformed <ipub-background> element",
|
|
e.target,
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (e.intersectionRatio > 0 && instance.id) {
|
|
instancesOnScreen.set(instance.id, instance);
|
|
} else if (instance.id) {
|
|
instancesOnScreen.delete(instance.id);
|
|
}
|
|
}),
|
|
);
|
|
})();
|
|
|
|
connectedCallback() {
|
|
super.connectedCallback();
|
|
|
|
const image = this.querySelector("img");
|
|
if (!image) {
|
|
console.error("IPUBBackground: missing <img> element inside background");
|
|
return;
|
|
}
|
|
|
|
// INFO: We don't need to fade the first background
|
|
if (this.matches(":first-of-type") || this.hasAttribute("nofade")) {
|
|
IPUBBackground.#observer.unobserve(image);
|
|
return;
|
|
}
|
|
|
|
IPUBBackground.#observer.observe(image);
|
|
const perc = getPercentageInView(image);
|
|
if (perc > 0) {
|
|
this.fade(perc);
|
|
}
|
|
}
|
|
|
|
attributeChangedCallback(name, _oldValue, _newValue) {
|
|
super.attributeChangedCallback();
|
|
|
|
if (name !== "nofade") {
|
|
return;
|
|
}
|
|
|
|
const image = this.querySelector("img");
|
|
if (!image) {
|
|
console.error("IPUBBackground: missing <img> element inside background");
|
|
return;
|
|
}
|
|
|
|
// INFO: We don't need to fade the first background
|
|
if (this.matches(":first-of-type") || this.hasAttribute("nofade")) {
|
|
IPUBBackground.#observer.unobserve(image);
|
|
return;
|
|
}
|
|
|
|
IPUBBackground.#observer.observe(image);
|
|
const perc = getPercentageInView(image);
|
|
if (perc > 0) {
|
|
this.fade(perc);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {number} perc
|
|
* @throws {Error}
|
|
* @returns {void | Promise<void>}
|
|
*/
|
|
fade(perc) {
|
|
console.debug(`IPUBBackground: ${this.id} is ${perc} on screen`);
|
|
|
|
if (!this.style.getPropertyValue("--ipub-fade")) {
|
|
this.style.setProperty("--ipub-fade", `${perc}%`);
|
|
return;
|
|
}
|
|
|
|
const currentPerc = this.style.getPropertyValue("--ipub-fade");
|
|
if (currentPerc === "100%" && perc >= 100) {
|
|
return;
|
|
}
|
|
|
|
if (perc % 10 === 0) {
|
|
this.style.setProperty("--ipub-fade", `${perc}%`);
|
|
}
|
|
}
|
|
}
|
|
|
|
class IPUBBody extends IPUBElement {
|
|
static elementName = "ipub-body";
|
|
|
|
static defineContentElements() {
|
|
for (const e of [
|
|
IPUBBackground,
|
|
IPUBImage,
|
|
IPUBInteraction,
|
|
]) {
|
|
console.info(`IPUBBody: Defining custom element <${e.elementName}>`);
|
|
globalThis.customElements.define(e.elementName, e);
|
|
}
|
|
}
|
|
|
|
connectedCallback() {
|
|
super.connectedCallback();
|
|
|
|
this.setAttribute("aria-busy", "true");
|
|
|
|
IPUBBody.defineContentElements();
|
|
|
|
this.setAttribute("aria-busy", "false");
|
|
}
|
|
}
|
|
|
|
globalThis.addEventListener("load", () => {
|
|
console.info("IPUB: Starting IPUB elements");
|
|
|
|
console.log("IPUB: Defining custom element <ipub-body>");
|
|
globalThis.customElements.define(IPUBBody.elementName, IPUBBody);
|
|
});
|
|
|
|
class IPUBImage extends IPUBElement {
|
|
static elementName = "ipub-image";
|
|
}
|
|
|
|
class IPUBInteraction extends IPUBElement {
|
|
static elementName = "ipub-interaction";
|
|
}
|
|
|
|
}
|
|
});
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
/**
|
|
*/
|
|
|
|
/**
|
|
* @param {Readonly<Element>} el
|
|
* @param {number} [length=6]
|
|
* @returns {string}
|
|
*
|
|
* @copyright This function contains code from a hash algorithm by
|
|
* {@link https://stackoverflow.com/a/16348977|Joe Freeman} and
|
|
* {@link https://stackoverflow.com/a/3426956|Cristian Sanchez} at
|
|
* StackOverflow, licensed under {@link https://creativecommons.org/licenses/by-sa/3.0/|CC BY-SA 3.0}.
|
|
*/
|
|
function hashFromHTML(el, length = 6) {
|
|
const hexLength = length / 2;
|
|
if (hexLength % 2 !== 0) {
|
|
hexLength + 1;
|
|
}
|
|
|
|
let hash = 0;
|
|
for (const char of el.innerHTML) {
|
|
hash = char.charCodeAt(0) + ((hash << 5) - hash);
|
|
}
|
|
|
|
let hex = "";
|
|
for (let i = 0; i < hexLength; i++) {
|
|
hex += ((hash >> (i * 8)) & 0xff).toString(16).padStart(2, "0");
|
|
}
|
|
|
|
return hex.substring(0, length);
|
|
}
|
|
|
|
/**
|
|
* @param {Element} element
|
|
* @returns {number}
|
|
*/
|
|
function getPercentageInView(element) {
|
|
const viewTop = globalThis.pageYOffset;
|
|
const viewBottom = viewTop + globalThis.innerHeight;
|
|
|
|
const rect = element.getBoundingClientRect();
|
|
const elementTop = rect.y + viewTop;
|
|
const elementBottom = rect.y + rect.height + viewTop;
|
|
|
|
if (viewTop > elementBottom || viewBottom < elementTop) {
|
|
return 0;
|
|
}
|
|
|
|
if (
|
|
(viewTop < elementTop && viewBottom > elementBottom) ||
|
|
(elementTop < viewTop && elementBottom > viewBottom)
|
|
) {
|
|
return 100;
|
|
}
|
|
|
|
let inView = rect.height;
|
|
|
|
if (elementTop < viewTop) {
|
|
inView = rect.height - (globalThis.pageYOffset - elementTop);
|
|
}
|
|
|
|
if (elementBottom > viewBottom) {
|
|
inView = inView - (elementBottom - viewBottom);
|
|
}
|
|
|
|
return Math.round((inView / globalThis.innerHeight) * 100);
|
|
}
|