"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}%`); } } } class IPUBContent extends HTMLElement { static elementName = "ipub-content"; } globalThis.addEventListener("load", () => { console.info("IPUB: STARTING DEFINITIONS"); [IPUBBackground, IPUBContent].forEach((e) => { console.info(`IPUB: Defining custom element <${e.elementName}>`); globalThis.customElements.define(e.elementName, e); }); console.info("IPUB: FINISHED DEFINITIONS"); /** @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`, // ); onScreenMap.set(e.target.id, e.target); } else { // console.debug( // `IntersectionObserver: removing element #${e.target.id} to onScreenMap`, // ); onScreenMap.delete(e.target.id); } }); }); for (const element of document.querySelectorAll( `[data-ipub-trigger="on-screen"]`, )) { observer.observe(element); } document.addEventListener("scroll", async () => { for (const [id, element] of onScreenMap) { const perc = getPercentageInView(element); // console.debug(`Element #${id} is now ${perc}% on screen`); const played = element.getAttribute("data-ipub-trigger-played") == "true"; if (perc >= 100 && !played) { await playIpubElement(element); element.setAttribute("data-ipub-trigger-played", "true"); } } }); }); /** * @param {Element} element */ async function playIpubElement(element) { switch (element.tagName) { case "audio": { /** @type {HTMLAudioElement} */ const audio = element; // await audio.play(); break; } default: break; } } /** * @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); }