"use strict"; class IPUBElement extends HTMLElement { /** * @protected * @type {Readonly} */ static observedAttributes = ["id", "debug"]; connectedCallback() { this.#ensureID(); } attributeChangedCallback(name, _oldValue, _newValue) { switch (name) { case "id": this.#ensureID(); break; case "debug": if (this.hasAttribute("debug")) { this.setDebug(true); } else { this.setDebug(false); } break; } } /** * @private */ #ensureID() { if (!this.id) { this.id = hashFromHTML(this); } } /** * @returns {boolean} */ getDebug() { return this.hasAttribute("debug"); } /** * @param {boolean} state */ setDebug(state) { if (state) { if (!this.hasAttribute("debug")) { this.setAttribute("debug", "true"); } if (!this.style.getPropertyValue(IPUBElement.#PROPERTY_DEBUG_COLOR)) { this.style.setProperty( IPUBElement.#PROPERTY_DEBUG_COLOR, `#${hashFromHTML(this)}`, ); } } else { if (!state && this.hasAttribute("debug")) { this.removeAttribute("debug"); } if ( !state && this.style.getPropertyValue(IPUBElement.#PROPERTY_DEBUG_COLOR) ) { this.style.removeProperty(IPUBElement.#PROPERTY_DEBUG_COLOR); } } getAllDescendants(this) .filter((el) => el.tagName.startsWith("ipub-")) .forEach((el) => { el.setDebug?.(state); }); } /** * @private * @type {Readonly} */ static #PROPERTY_DEBUG_COLOR = "--ipub-debug-color"; } globalThis.addEventListener("load", () => { console.info("IPUB: Starting IPUB elements"); console.log("IPUB: Defining custom element "); globalThis.customElements.define(IPUBBody.elementName, IPUBBody); }); class IPUBBody extends IPUBElement { static elementName = `ipub-body`; connectedCallback() { super.connectedCallback(); this.setAttribute("aria-busy", "true"); // TODO?: Move IPUBCover's "can-play" logic to here console.log("IPUBBody: Defining custom element "); globalThis.customElements.define(IPUBCover.elementName, IPUBCover); /** @type {IPUBCover} */ const cover = this.querySelector("ipub-cover"); if (!cover) { // TODO: automatically create IPUBCover element if it doesn't exists console.error("IPUBBody: Document doesn't has element"); this.#initElements(); this.setAttribute("aria-busy", "false"); return; } cover.onclose = () => { this.#initElements(); }; cover.cover(); this.setAttribute("aria-busy", "false"); } /** * @private */ #initElements() { for (const e of [ IPUBAudio, IPUBBackground, IPUBImage, IPUBInteraction, IPUBSoundtrack, ]) { console.info(`IPUBBody: Defining custom element <${e.elementName}>`); globalThis.customElements.define(e.elementName, e); } if (this.getDebug()) { // HACK: Re-trigger IPUBElement debugging logic console.debug("IPUBBody: triggeing debugger"); this.setAttribute("debug", "true"); } } } class IPUBCover extends IPUBElement { static elementName = `ipub-cover`; /** * @type {() => void} callback */ onclose = () => {}; connectedCallback() { super.connectedCallback(); } cover() { console.debug("IPUBCover: Setting up cover"); this.setAttribute("aria-busy", "true"); const dialog = this.querySelector("dialog"); // HACK: Test if we can autoplay interactions, soundtracks, etc /** @type {HTMLMediaElement | null} */ const media = this.parentElement.querySelector("audio") ?? this.parentElement.querySelector("video"); if (!media) { console.log("IPUBCover: no media element found, removing cover"); dialog.close(); this.onclose(); return; } const pastVolume = media.volume; media.volume = 0.1; // don't let the user hear the test audio media .play() .then(() => { media.pause(); media.volume = pastVolume; media.currentTime = 0; console.debug("IPUBCover: Can autoplay interactions, removing cover"); dialog.close(); this.onclose(); }) .catch(() => { console.debug( "IPUBCover: Cannot autoplay interactions, covering content", ); dialog.show(); dialog.parentElement.addEventListener("click", () => { dialog.close(); this.onclose(); }); this.setAttribute("aria-busy", "false"); return; }); } } class IPUBAudio extends IPUBElement { static elementName = "ipub-audio"; /** * @param {boolean} [forced=false] * @throws {Error} * @returns {Promise} */ play(forced = false) { if (!this.#audioElement.readyState > HTMLMediaElement.HAVE_CURRENT_DATA) { throw new Error("IPUBAudio: audio is not ready"); } if (forced) { this.setAttribute("forced", "true"); } return this.#audioElement.play(); } /** * @param {boolean} [forced=false] */ pause(forced = false) { if (forced) { this.setAttribute("forced", "true"); } this.#audioElement.pause(); } /** * @param {boolean} state */ setLoop(state) { this.#audioElement.loop = state; } /** * @returns {boolean} */ getLoop() { return this.#audioElement.loop; } connectedCallback() { super.connectedCallback(); const audio = this.querySelector("audio"); if (!audio) { console.error("IPUBAudio: Missing child