From f465564546d554ac777c09e30384e20290ff5cd6 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 | 103 ++++++++++++++++++ .../example/OEBPS/sections/section0001.xhtml | 10 +- .epub/example/OEBPS/styles/stylesheet.css | 91 ++++++++++++++-- 3 files changed, 196 insertions(+), 8 deletions(-) diff --git a/.epub/example/OEBPS/scripts/ipub.js b/.epub/example/OEBPS/scripts/ipub.js index 763fb2a..3a879e6 100644 --- a/.epub/example/OEBPS/scripts/ipub.js +++ b/.epub/example/OEBPS/scripts/ipub.js @@ -1,8 +1,111 @@ "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 + * @implements {IPUBElementOnScreen} + */ +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 !== IPUBBackground.elementName) { + instance = instance.parentElement; + } + + if (instance.tagName !== IPUBBackground.elementName) { + console.error( + "IPUBBackground: malformed element", + e.target, + ); + return; + } + + if (e.intersectionRatio > 0) { + instancesOnScreen.set(instance.id, instance); + } else { + instancesOnScreen.delete(instance.id); + } + }), + ); + })(); + + constructor() { + super(); + } + + connectedCallback() { + if (!this.id) { + console.warn( + `IPUB: ipub-background has not ID, assigning one based on innerHTML`, + this, + ); + this.id = hashString(this.innerHTML); + } + console.debug(`IPUB: Added ipub-background#${this.id} to page`); + + const image = this.querySelector("img"); + if (this.hasAttribute("fade") && image) { + console.debug(`IPUB: Added ipub-background#${this.id} to observer`); + + 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(`${this.id} is ${perc} on screen`); + + if (!this.style.getPropertyValue("--ipub-fade")) { + this.style.setProperty("--ipub-fade", `${perc}%`); + } else 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(); 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 @@ - +