class OutfitViewer extends HTMLElement { #internals; constructor() { super(); this.#internals = this.attachInternals(); // for CSS `:state()` } connectedCallback() { // The `` is connected to the DOM right before its // children are. So, to engage with the children, wait a tick! 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); this.#setIsPlayingCookie(playPauseToggle.checked); }); } #setIsPlaying(isPlaying) { // TODO: Listen for changes to the child list, and add `playing` when new // nodes arrive, if playing. const thirtyDays = 60 * 60 * 24 * 30; if (isPlaying) { this.#internals.states.add("playing"); for (const layer of this.querySelectorAll("outfit-layer")) { layer.play(); } } else { this.#internals.states.delete("playing"); for (const layer of this.querySelectorAll("outfit-layer")) { layer.pause(); } } } #setIsPlayingCookie(isPlaying) { const thirtyDays = 60 * 60 * 24 * 30; const value = isPlaying ? "true" : "false"; document.cookie = `DTIOutfitViewerIsPlaying=${value};max-age=${thirtyDays}`; } } class OutfitLayer extends HTMLElement { #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() { // When this `` leaves the DOM, stop listening for iframe // messages, if we were. window.removeEventListener("message", this.#onMessage); } play() { this.#sendMessageToIframe({ type: "play" }); } pause() { this.#sendMessageToIframe({ type: "pause" }); } #connectToChildren() { const image = this.querySelector("img"); const iframe = this.querySelector("iframe"); if (image) { // If this is an image layer, track its loading state by listening // to the load/error events, and initialize based on whether it's // already `complete` (which it can be if it loaded from cache). this.#setStatus(image.complete ? "loaded" : "loading"); image.addEventListener("load", () => this.#setStatus("loaded")); image.addEventListener("error", () => this.#setStatus("error")); } else if (iframe) { this.iframe = iframe; // Initialize status to `loading`, and asynchronously request a // status message from the iframe if it managed to load before this // triggers (impressive, but I think I've seen it happen!). Then, // wait for messages or error events from the iframe to update // status further if needed. this.#setStatus("loading"); this.#sendMessageToIframe({ type: "requestStatus" }); window.addEventListener("message", (m) => this.#onMessage(m)); this.iframe.addEventListener("error", () => this.#setStatus("error"), ); } else { throw new Error( ` must contain an or