From 007de6b9f12be8805d478d65c11504702198eac0 Mon Sep 17 00:00:00 2001 From: "Gustavo \"Guz\" L de Mello" Date: Thu, 16 Oct 2025 15:02:34 -0300 Subject: [PATCH] feat(ipub): sticky background implementation via web components --- .epub/example/OEBPS/scripts/ipub.js | 140 +++++++++++++++++- .../example/OEBPS/sections/section0001.xhtml | 10 +- .epub/example/OEBPS/styles/stylesheet.css | 82 +++++++++- 3 files changed, 216 insertions(+), 16 deletions(-) diff --git a/.epub/example/OEBPS/scripts/ipub.js b/.epub/example/OEBPS/scripts/ipub.js index 763fb2a..7feb888 100644 --- a/.epub/example/OEBPS/scripts/ipub.js +++ b/.epub/example/OEBPS/scripts/ipub.js @@ -1,22 +1,146 @@ "use strict"; +/** + * @param {string} str + * @returns {string} + */ +function hashString(str) { + return Array.from(str).reduce( + (s, c) => (Math.imul(31, s) + c.charCodeAt(0)) | 0, + 0, + ); +} + +class IPUBBackground extends HTMLElement { + static elementName = "ipub-background"; + static observedAttributes = ["sticky", "fade", "id"]; + + /** + * @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); + } + }), + ); + })(); + + attributeChangedCallback(name, _oldValue, _newValue) { + console.debug("IPUBBackground: attribute changed", name); + switch (name) { + case "id": { + if (!this.id) { + console.warn( + `IPUBBackground: no ID specified, assigning one based on innerHTML`, + this, + ); + this.id = hashString(this.innerHTML); + } + break; + } + case "fade": { + const image = this.querySelector("img"); + if (image) { + console.debug( + `IPUBBackground: ipub-background#${this.id} to observer`, + ); + + if (this.hasAttribute("fade")) { + IPUBBackground.#observer.observe(image); + } else { + IPUBBackground.#observer.unobserve(image); + } + + const perc = getPercentageInView(image); + if (perc > 0) { + this.fade(perc); + } + } + break; + } + } + } + + /** + * @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}%`); + } + } +} + globalThis.addEventListener("load", () => { console.log("IPUB SCRIPT LOADED"); + customElements.define(IPUBBackground.elementName, IPUBBackground); + /** @type {Map} */ const onScreenMap = new Map(); const observer = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.intersectionRatio > 0) { - console.debug( - `IntersectionObserver: adding element #${e.target.id} to onScreenMap`, - ); + // console.debug( + // `IntersectionObserver: adding element #${e.target.id} to onScreenMap`, + // ); onScreenMap.set(e.target.id, e.target); } else { - console.debug( - `IntersectionObserver: removing element #${e.target.id} to onScreenMap`, - ); + // console.debug( + // `IntersectionObserver: removing element #${e.target.id} to onScreenMap`, + // ); onScreenMap.delete(e.target.id); } }); @@ -31,7 +155,7 @@ globalThis.addEventListener("load", () => { document.addEventListener("scroll", async () => { for (const [id, element] of onScreenMap) { const perc = getPercentageInView(element); - console.debug(`Element #${id} is now ${perc}% on screen`); + // console.debug(`Element #${id} is now ${perc}% on screen`); const played = element.getAttribute("data-ipub-trigger-played") == "true"; @@ -52,7 +176,7 @@ async function playIpubElement(element) { /** @type {HTMLAudioElement} */ const audio = element; - await audio.play(); + // await audio.play(); break; } diff --git a/.epub/example/OEBPS/sections/section0001.xhtml b/.epub/example/OEBPS/sections/section0001.xhtml index e026502..9cd02d6 100644 --- a/.epub/example/OEBPS/sections/section0001.xhtml +++ b/.epub/example/OEBPS/sections/section0001.xhtml @@ -1,7 +1,7 @@ - +