A few more comments and code style refactors for item previews
This commit is contained in:
parent
b2b16a2edc
commit
8ad0025e32
1 changed files with 61 additions and 32 deletions
|
@ -3,10 +3,12 @@ class OutfitViewer extends HTMLElement {
|
|||
|
||||
constructor() {
|
||||
super();
|
||||
this.#internals = this.attachInternals();
|
||||
this.#internals = this.attachInternals(); // for CSS `:state()`
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -67,6 +69,8 @@ class OutfitLayer extends HTMLElement {
|
|||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
// When this `<outfit-layer>` leaves the DOM, stop listening for iframe
|
||||
// messages, if we were.
|
||||
window.removeEventListener("message", this.#onMessage);
|
||||
}
|
||||
|
||||
|
@ -83,33 +87,40 @@ class OutfitLayer extends HTMLElement {
|
|||
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.
|
||||
// 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.
|
||||
// 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"));
|
||||
this.iframe.addEventListener("error", () =>
|
||||
this.#setStatus("error"),
|
||||
);
|
||||
} 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 }) {
|
||||
// Ignore messages that aren't from *our* frame.
|
||||
if (source !== this.iframe.contentWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate the incoming status message, then set our status to match.
|
||||
if (data.type === "status") {
|
||||
if (data.status === "loaded") {
|
||||
this.#setStatus("loaded");
|
||||
|
@ -118,16 +129,22 @@ class OutfitLayer extends HTMLElement {
|
|||
this.#setStatus("error");
|
||||
} else {
|
||||
throw new Error(
|
||||
`<outfit-layer> got unexpected status: ${JSON.stringify(data.status)}`,
|
||||
`<outfit-layer> got unexpected status: ` +
|
||||
JSON.stringify(data.status),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
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) {
|
||||
this.#internals.states.delete("loading");
|
||||
this.#internals.states.delete("loaded");
|
||||
|
@ -135,6 +152,9 @@ class OutfitLayer extends HTMLElement {
|
|||
this.#internals.states.add(newStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether CSS selector `:state(has-animations)` matches this element.
|
||||
*/
|
||||
#setHasAnimations(hasAnimations) {
|
||||
if (hasAnimations) {
|
||||
this.#internals.states.add("has-animations");
|
||||
|
@ -144,7 +164,13 @@ class OutfitLayer extends HTMLElement {
|
|||
}
|
||||
|
||||
#sendMessageToIframe(message) {
|
||||
// If we have no frame or it hasn't loaded, ignore this message.
|
||||
if (this.iframe?.contentWindow == null) {
|
||||
console.debug(
|
||||
`Ignoring message, frame not loaded: `,
|
||||
this.iframe,
|
||||
message,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -161,27 +187,30 @@ customElements.define("outfit-layer", OutfitLayer);
|
|||
// 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.)
|
||||
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) => {
|
||||
// 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") {
|
||||
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;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
event.detail.render = (a, b) => morphWithOutfitLayers(a, b);
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue