"use strict"; class IPUBElement extends HTMLElement { static observedAttributes = ["id"]; connectedCallback() { this.ensureID(); } attributeChangedCallback(_name, _oldValue, _newValue) { this.ensureID(); } ensureID() { if (this.id) { return; } // INFO: Hash algorithm by Joe Freeman & Cristian Sanchez at StackOverflow: // https://stackoverflow.com/a/16348977 // https://stackoverflow.com/a/3426956 // // Licensed under CC BY-SA 3.0 (https://creativecommons.org/licenses/by-sa/3.0/) let hash = 0; this.innerHTML.split("").forEach((char) => { hash = char.charCodeAt(0) + ((hash << 5) - hash); }); let id = ""; for (let i = 0; i < 3; i++) { id += ((hash >> (i * 8)) & 0xff).toString(16).padStart(2, "0"); } this.id = id; } } class IPUBBackground extends IPUBElement { static elementName = "ipub-background"; static observedAttributes = ["sticky", "fade"].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); } }), ); })(); attributeChangedCallback(name, _oldValue, _newValue) { super.attributeChangedCallback(); if (name === "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); } } } } /** * @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 IPUBElement { static elementName = "ipub-content"; } class IPUBImage extends IPUBElement { static elementName = "ipub-image"; } globalThis.addEventListener("load", () => { console.info("IPUB: STARTING DEFINITIONS"); [IPUBBackground, IPUBContent, IPUBImage].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); }