A few more comments and code style refactors for item previews

This commit is contained in:
Emi Matchu 2024-08-30 17:09:39 -07:00
parent b2b16a2edc
commit 8ad0025e32

View file

@ -3,10 +3,12 @@ class OutfitViewer extends HTMLElement {
constructor() { constructor() {
super(); super();
this.#internals = this.attachInternals(); this.#internals = this.attachInternals(); // for CSS `:state()`
} }
connectedCallback() { connectedCallback() {
// The `<outfit-layer>` is connected to the DOM right before its
// children are. So, to engage with the children, wait a tick!
setTimeout(() => this.#connectToChildren(), 0); setTimeout(() => this.#connectToChildren(), 0);
} }
@ -67,6 +69,8 @@ class OutfitLayer extends HTMLElement {
} }
disconnectedCallback() { disconnectedCallback() {
// When this `<outfit-layer>` leaves the DOM, stop listening for iframe
// messages, if we were.
window.removeEventListener("message", this.#onMessage); window.removeEventListener("message", this.#onMessage);
} }
@ -83,33 +87,40 @@ class OutfitLayer extends HTMLElement {
const iframe = this.querySelector("iframe"); const iframe = this.querySelector("iframe");
if (image) { if (image) {
// Initialize status based on the image's current `complete` attribute, // If this is an image layer, track its loading state by listening
// then wait for load/error events to update it further if needed. // 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"); this.#setStatus(image.complete ? "loaded" : "loading");
image.addEventListener("load", () => this.#setStatus("loaded")); image.addEventListener("load", () => this.#setStatus("loaded"));
image.addEventListener("error", () => this.#setStatus("error")); image.addEventListener("error", () => this.#setStatus("error"));
} else if (iframe) { } else if (iframe) {
this.iframe = iframe; this.iframe = iframe;
// Initialize status to `loading`, and asynchronously request a status // Initialize status to `loading`, and asynchronously request a
// message from the iframe if it managed to load before this triggers // status message from the iframe if it managed to load before this
// (impressive, but I think I've seen it happen!). Then, wait for // triggers (impressive, but I think I've seen it happen!). Then,
// messages or error events from the iframe to update status further if // wait for messages or error events from the iframe to update
// needed. // status further if needed.
this.#setStatus("loading"); this.#setStatus("loading");
this.#sendMessageToIframe({ type: "requestStatus" }); this.#sendMessageToIframe({ type: "requestStatus" });
window.addEventListener("message", (m) => this.#onMessage(m)); window.addEventListener("message", (m) => this.#onMessage(m));
this.iframe.addEventListener("error", () => this.#setStatus("error")); this.iframe.addEventListener("error", () =>
this.#setStatus("error"),
);
} else { } else {
throw new Error(`<outfit-layer> must contain an <img> or <iframe> tag`); throw new Error(
`<outfit-layer> must contain an <img> or <iframe> tag`,
);
} }
} }
#onMessage({ source, data }) { #onMessage({ source, data }) {
// Ignore messages that aren't from *our* frame.
if (source !== this.iframe.contentWindow) { if (source !== this.iframe.contentWindow) {
return; return;
} }
// Validate the incoming status message, then set our status to match.
if (data.type === "status") { if (data.type === "status") {
if (data.status === "loaded") { if (data.status === "loaded") {
this.#setStatus("loaded"); this.#setStatus("loaded");
@ -118,16 +129,22 @@ class OutfitLayer extends HTMLElement {
this.#setStatus("error"); this.#setStatus("error");
} else { } else {
throw new Error( throw new Error(
`<outfit-layer> got unexpected status: ${JSON.stringify(data.status)}`, `<outfit-layer> got unexpected status: ` +
JSON.stringify(data.status),
); );
} }
} else { } else {
throw new Error( throw new Error(
`<outfit-layer> got unexpected message: ${JSON.stringify(data)}`, `<outfit-layer> got unexpected message: ` +
JSON.stringify(data),
); );
} }
} }
/**
* Set the status value that the CSS `:state()` selector will match.
* For example, when loading, `:state(loading)` matches this element.
*/
#setStatus(newStatus) { #setStatus(newStatus) {
this.#internals.states.delete("loading"); this.#internals.states.delete("loading");
this.#internals.states.delete("loaded"); this.#internals.states.delete("loaded");
@ -135,6 +152,9 @@ class OutfitLayer extends HTMLElement {
this.#internals.states.add(newStatus); this.#internals.states.add(newStatus);
} }
/**
* Set whether CSS selector `:state(has-animations)` matches this element.
*/
#setHasAnimations(hasAnimations) { #setHasAnimations(hasAnimations) {
if (hasAnimations) { if (hasAnimations) {
this.#internals.states.add("has-animations"); this.#internals.states.add("has-animations");
@ -144,7 +164,13 @@ class OutfitLayer extends HTMLElement {
} }
#sendMessageToIframe(message) { #sendMessageToIframe(message) {
// If we have no frame or it hasn't loaded, ignore this message.
if (this.iframe?.contentWindow == null) { if (this.iframe?.contentWindow == null) {
console.debug(
`Ignoring message, frame not loaded: `,
this.iframe,
message,
);
return; return;
} }
@ -161,27 +187,30 @@ customElements.define("outfit-layer", OutfitLayer);
// aggressively reusing existing <outfit-layer> nodes for entirely different // aggressively reusing existing <outfit-layer> nodes for entirely different
// assets. (It's a lot clearer for managing the loading state, and not showing // 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.) // old incorrect layers!) (We also tried using `id` to enforce this… no luck.)
function morphWithOutfitLayers(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;
}
},
},
});
}
addEventListener("turbo:before-frame-render", (event) => { addEventListener("turbo:before-frame-render", (event) => {
// Rather than enforce Idiomorph must be loaded, let's just be resilient
// and only bother if we have it. (Replacing content is not *that* bad!)
if (typeof Idiomorph !== "undefined") { if (typeof Idiomorph !== "undefined") {
event.detail.render = (currentElement, newElement) => { event.detail.render = (a, b) => morphWithOutfitLayers(a, b);
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;
}
},
},
});
};
} }
}); });