class OutfitViewer extends HTMLElement { #internals; constructor() { super(); this.#internals = this.attachInternals(); this.#setIsPlaying(false); } connectedCallback() { setTimeout(() => this.#connectToChildren(), 0); } #connectToChildren() { const playPauseToggle = document.querySelector(".play-pause-toggle"); // Read our initial playing state from the toggle, and subscribe to changes. this.#setIsPlaying(playPauseToggle.checked); playPauseToggle.addEventListener("change", () => { this.#setIsPlaying(playPauseToggle.checked); }); } #setIsPlaying(isPlaying) { // TODO: Listen for changes to the child list, and add `playing` when new // nodes arrive, if playing. if (isPlaying) { this.#internals.states.add("playing"); for (const child of this.children) { child.setAttribute("playing", ""); } } else { this.#internals.states.delete("playing"); for (const child of this.children) { child.removeAttribute("playing"); } } } } class OutfitLayer extends HTMLElement { static observedAttributes = ["playing"]; #internals; constructor() { super(); this.#internals = this.attachInternals(); // An starts in the loading state, and then might very // quickly decide it's not after `#connectToChildren`. This is to prevent a // flash of *non*-loading state, when a new layer loads in. (e.g. In the // time between our parent loading, which shows the loading // spinner; and us being marked `:state(loading)`, which shows the loading // spinner; we don't want the loading spinner to do its usual *immediate* // total fade-out; then have to fade back in again, on the usual delay.) this.#setStatus("loading"); } connectedCallback() { setTimeout(() => this.#connectToChildren(), 0); } disconnectedCallback() { window.removeEventListener("message", this.#onMessage); } attributeChangedCallback(name, oldValue, newValue) { if (name === "playing") { const isPlaying = newValue != null; this.#forwardIsPlaying(isPlaying); } } #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.iframe.addEventListener("error", () => this.#setStatus("error")); this.#setStatus("loading"); } else { throw new Error(` must contain an or