forked from OpenNeo/impress
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() {
|
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,9 +187,7 @@ 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.)
|
||||||
addEventListener("turbo:before-frame-render", (event) => {
|
function morphWithOutfitLayers(currentElement, newElement) {
|
||||||
if (typeof Idiomorph !== "undefined") {
|
|
||||||
event.detail.render = (currentElement, newElement) => {
|
|
||||||
Idiomorph.morph(currentElement, newElement.innerHTML, {
|
Idiomorph.morph(currentElement, newElement.innerHTML, {
|
||||||
morphStyle: "innerHTML",
|
morphStyle: "innerHTML",
|
||||||
callbacks: {
|
callbacks: {
|
||||||
|
@ -182,6 +206,11 @@ 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") {
|
||||||
|
event.detail.render = (a, b) => morphWithOutfitLayers(a, b);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue