import { descendantElementIterator } from "../../core/descendantElementIterator.js"; import { IEditableAttribute } from "../../core/EditableAttributes.js"; import HtmlControl from "../../core/HtmlControl.js"; import { isInsideHtmlEditor } from "../../core/HtmlEditor.js"; import { updateIntrinsicSize } from "../../core/updateIntrinsicSize.js"; import sanitize, { sanitizeDom } from "../../services/sanitize/sanitize.js"; export default class InlineSvg extends HtmlControl { root: ShadowRoot; loading: any; loadingSrc: any; variables: string; abortController: AbortController; static observedAttributes = ["src", "mode", "width", "height", "vars"]; get editableAttributes(): IEditableAttribute[] { return [ { name: "src", type: "image" }, { name: "height", type: "size" }, { name: "width", type: "size" }, { name: "mode", type: "enum", values: [ "shadow", "dom" ] } ]; } get isShadowMode() { return this.getAttribute("mode") !== "dom"; } attributeChangedCallback(name, oldValue, newValue) { switch(name) { case "src": case "mode": this.updateImage().catch(console.error); break; case "width": updateIntrinsicSize(this, "width"); break; case "height": updateIntrinsicSize(this, "height"); break; case "vars": this.updateImage().catch(console.error); break; } } prepare() { this.updateImage().catch(console.error); updateIntrinsicSize(this, "width"); updateIntrinsicSize(this, "height"); } async updateImage() { const src = this.getAttribute("src"); if(!src) { return; } if (src === this.loadingSrc) { return; } this.loadingSrc = src; this.abortController?.abort(); const a = this.abortController = new AbortController(); const signal = a.signal; this.innerHTML = ""; if (this.isShadowMode) { this.root ??= this.attachShadow({ mode: "closed"}); this.root.innerHTML = ""; } try { const rs = await fetch(src, { signal }); if (rs.status >= 400) { const text = await rs.text(); const er = new Error(`$Http Error ${rs.status}`); (er as any).response = { status: rs.status, text }; throw er; } let svgText = await rs.text(); svgText = await this.sanitize(svgText, signal); if (signal.aborted) { return; } if(!this.isShadowMode) { await this.updateDomImage(svgText); this.loadingSrc = null; this.emitLoaded(); return; } await this.updateShadowImage(svgText); this.loadingSrc = null; this.emitLoaded(); } catch (error) { this.displayError(error); this.loadingSrc = null; this.dispatchEvent(new CustomEvent("loadError", { bubbles: true})); const { response } = error; if (response) { console.error({ error, response }) window.dispatchEvent(new ErrorEvent("error", { error })); } else { window.dispatchEvent(new ErrorEvent("error", { error })); console.error(error); } } } private emitLoaded() { if (!isInsideHtmlEditor(this)) { this.dispatchEvent(new CustomEvent("svgLoaded", { bubbles: true })); this.applyVariables(); return; } const variables = []; try { const root = this.root ?? this; const added = new Set(); // build list of variables... for(const e of descendantElementIterator(root as any, true)) { if (!/^style$/i.test(e.tagName)) { continue; } const { sheet: { cssRules }} = e as SVGStyleElement; for (let index = 0; index < cssRules.length; index++) { const { style } = cssRules.item(index) as CSSStyleRule; if (!style) { continue; } for (let j = 0; j < style.length; j++) { const name = style.item(j); if (name.startsWith("--")) { added.add(name); variables.push(`${name}: ${style.getPropertyValue(name)};`); } } } // for(const [m,token] of tokenizeRegex(e.textContent, /(\-\-[\-a-z0-9]+)/gi)) { // if (added.has(token)) { // continue; // } // added.add(token); // variables.push({ name: token, type: "color"}); // } } } catch (error) { console.error(error); } this.variables = variables.join("\n"); this.applyVariables(); this.dispatchEvent(new CustomEvent("svgLoaded", { bubbles: true })); } applyVariables() { const vars = this.getAttribute("vars"); if (!vars) { return; } const root = this.root ?? this; const firstStyle = root.querySelector("style") ?? root.firstElementChild; const style = document.createElementNS(firstStyle.namespaceURI, "style"); style.textContent = `* { ${vars} }`; firstStyle.insertAdjacentElement("afterend", style); } displayError(error: any) { this.root ??= this.attachShadow({ mode: "closed"}); this.root.innerHTML = ` `; } async sanitize(svgText: string, signal) { if (isInsideHtmlEditor(this)) { return await sanitizeDom(svgText, "image/svg+xml"); } return await sanitize(svgText, "image/svg+xml"); } async updateDomImage(svgText: string) { this.innerHTML = svgText } async updateShadowImage(svgText: string) { this.root ??= this.attachShadow({ mode: "closed"}); this.root.innerHTML = svgText; } } customElements.define("inline-svg", InlineSvg);