class OutfitLayer extends HTMLElement { #internals; constructor() { super(); this.#internals = this.attachInternals(); } connectedCallback() { setTimeout(() => this.#connectToChildren(), 0); } disconnectedCallback() { window.removeEventListener("message", this.#onMessage); } #connectToChildren() { const image = this.querySelector("img"); const iframe = this.querySelector("iframe"); if (image) { image.addEventListener("load", () => this.#setStatus("loaded")); image.addEventListener("error", () => this.#setStatus("error")); this.#setStatus(image.complete ? "loaded" : "loading"); } else if (iframe) { this.iframe = iframe; window.addEventListener("message", (m) => this.#onMessage(m)); this.#setStatus("loading"); } else { throw new Error( `<outfit-layer> must contain an <img> or <iframe> tag`, ); } } #onMessage({ source, data }) { if (source !== this.iframe.contentWindow) { return; } if ( data.type === "status" && ["loaded", "error"].includes(data.status) ) { this.#setStatus(data.status); } else { throw new Error( `<outfit-layer> got unexpected message: ${JSON.stringify(data)}`, ); } } #setStatus(newStatus) { this.#internals.states.clear(); this.#internals.states.add(newStatus); } } customElements.define("outfit-layer", OutfitLayer); // Morph turbo-frames on this page, to reuse asset nodes when we want to—very // important for movies!—but ensure that it *doesn't* do its usual behavior of // aggressively reusing existing <outfit-layer> nodes for entirely different // assets. (It's a lot clearer for managing the loading state, and not showing // old incorrect layers!) (We also tried using `id` to enforce this… no luck.) addEventListener("turbo:before-frame-render", (event) => { if (typeof Idiomorph !== "undefined") { event.detail.render = (currentElement, newElement) => { Idiomorph.morph(currentElement, newElement.innerHTML, { morphStyle: "innerHTML", callbacks: { beforeNodeMorphed: (currentNode, newNode) => { // If Idiomorph wants to transform an <outfit-layer> to // have a different data-asset-id attribute, we replace // the node ourselves and abort the morph. if ( newNode.tagName === "OUTFIT-LAYER" && newNode.getAttribute("data-asset-id") !== currentNode.getAttribute("data-asset-id") ) { currentNode.replaceWith(newNode); return false; } }, }, }); }; } });