"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} */ 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 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 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 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} */ 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 "); globalThis.customElements.define(IPUBBody.elementName, IPUBBody); }); class IPUBImage extends IPUBElement { static elementName = "ipub-image"; } class IPUBInteraction extends IPUBElement { static elementName = "ipub-interaction"; } } }); } /** */ /** * @param {Readonly} 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); }