forked from OpenNeo/impress
Matchu
20202b5cd9
I thiiiink I've seen the status of a movie `<outfit-layer>` sometimes be `loading` even when it's clearly already loaded and running. I haven't been able to track down where and how that happens exactly, so this is me acting on a hunch: that maybe the I-would-have-thought-very-unlikely event that the iframe finishes loading before the `<outfit-layer>` connects with its children maybe happens more often than one might think! In this change, we set up the iframe to receive `requestStatus` messages, which it responds to with the status immediately. And we send one of these when the `<outfit-layer>` first discovers the iframe. Fingers crossed!
187 lines
5.7 KiB
JavaScript
187 lines
5.7 KiB
JavaScript
class OutfitViewer extends HTMLElement {
|
|
#internals;
|
|
|
|
constructor() {
|
|
super();
|
|
this.#internals = this.attachInternals();
|
|
}
|
|
|
|
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);
|
|
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 <outfit-layer> 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 <turbo-frame> 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);
|
|
}
|
|
|
|
play() {
|
|
this.#sendMessageToIframe({ type: "play" });
|
|
}
|
|
|
|
pause() {
|
|
this.#sendMessageToIframe({ type: "pause" });
|
|
}
|
|
|
|
#connectToChildren() {
|
|
const image = this.querySelector("img");
|
|
const iframe = this.querySelector("iframe");
|
|
|
|
if (image) {
|
|
// Initialize status based on the image's current `complete` attribute,
|
|
// then wait for load/error events to update it further if needed.
|
|
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(`<outfit-layer> must contain an <img> or <iframe> tag`);
|
|
}
|
|
}
|
|
|
|
#onMessage({ source, data }) {
|
|
if (source !== this.iframe.contentWindow) {
|
|
return;
|
|
}
|
|
|
|
if (data.type === "status") {
|
|
if (data.status === "loaded") {
|
|
this.#setStatus("loaded");
|
|
this.#setHasAnimations(data.hasAnimations);
|
|
} else if (data.status === "error") {
|
|
this.#setStatus("error");
|
|
} else {
|
|
throw new Error(
|
|
`<outfit-layer> got unexpected status: ${JSON.stringify(data.status)}`,
|
|
);
|
|
}
|
|
} else {
|
|
throw new Error(
|
|
`<outfit-layer> got unexpected message: ${JSON.stringify(data)}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
#setStatus(newStatus) {
|
|
this.#internals.states.delete("loading");
|
|
this.#internals.states.delete("loaded");
|
|
this.#internals.states.delete("error");
|
|
this.#internals.states.add(newStatus);
|
|
}
|
|
|
|
#setHasAnimations(hasAnimations) {
|
|
if (hasAnimations) {
|
|
this.#internals.states.add("has-animations");
|
|
} else {
|
|
this.#internals.states.delete("has-animations");
|
|
}
|
|
}
|
|
|
|
#sendMessageToIframe(message) {
|
|
if (this.iframe?.contentWindow == null) {
|
|
return;
|
|
}
|
|
|
|
// The frame is sandboxed (origin == null), so send to Any origin.
|
|
this.iframe.contentWindow.postMessage(message, "*");
|
|
}
|
|
}
|
|
|
|
customElements.define("outfit-viewer", OutfitViewer);
|
|
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;
|
|
}
|
|
},
|
|
},
|
|
});
|
|
};
|
|
}
|
|
});
|