UNPKG

@viamedici-spc/configurator-framer-host

Version:

Provides a Web Component for host applications to embed a Framer based Viamedici SPC configurator.

258 lines (257 loc) 12.4 kB
function A(r) { var e = []; if (r.length === 0) return ""; if (typeof r[0] != "string") throw new TypeError("Url must be a string. Received " + r[0]); if (r[0].match(/^[^/:]+:\/*$/) && r.length > 1) { var i = r.shift(); r[0] = i + r[0]; } r[0].match(/^file:\/\/\//) ? r[0] = r[0].replace(/^([^/:]+):\/*/, "$1:///") : r[0] = r[0].replace(/^([^/:]+):\/*/, "$1://"); for (var t = 0; t < r.length; t++) { var s = r[t]; if (typeof s != "string") throw new TypeError("Url must be a string. Received " + s); s !== "" && (t > 0 && (s = s.replace(/^[\/]+/, "")), t < r.length - 1 ? s = s.replace(/[\/]+$/, "") : s = s.replace(/[\/]+$/, "/"), e.push(s)); } var o = e.join("/"); o = o.replace(/\/(\?|&|#[^!])/g, "$1"); var l = o.split("?"); return o = l.shift() + (l.length > 0 ? "?" : "") + l.join("&"), o; } function x() { var r; return typeof arguments[0] == "object" ? r = arguments[0] : r = [].slice.call(arguments), A(r); } const b = class d extends HTMLElement { constructor() { super(), this.src = null, this.proxyBaseUrl = d.proxyBaseUrl, this.hasConnected = !1; } static get observedAttributes() { return d.attributeNames; } connectedCallback() { this.hasConnected = !0, this.src && (console.log("[Configurator] Web Component connected -> reinitialize the configurator app."), this.bootstrapApp()); } attributeChangedCallback(e, i, t) { e === "src" && t !== i ? (this.src = t, this.hasConnected && this.isConnected && this.src && (console.log("[Configurator] The URL was changed -> reinitialize the configurator app."), this.bootstrapApp())) : e === "proxy-base-url" && t !== i && (this.proxyBaseUrl = t ?? d.proxyBaseUrl); } async bootstrapApp() { try { let e; try { const a = x(this.proxyBaseUrl, `?url=${this.src}`); e = await fetch(a); } catch (a) { console.error("[Configurator] Failed to bootstrap configurator app: Failed to load index.html.", a); return; } for (; this.firstChild; ) this.removeChild(this.firstChild); const i = await e.text(), t = new DOMParser().parseFromString(i, "text/html"), s = t.querySelector("style[data-framer-css-ssr-minified]") || t.querySelector("style[data-framer-css-ssr]"), o = t.querySelector("style[data-framer-fonts-ssr-minified]") || t.querySelector("style[data-framer-font-css]"), l = t.querySelector("div#main"), n = t.querySelector('script[data-framer-bundle="main"]'), m = t.querySelector('script[type="importmap"][data-framer-importmap]'); if (s && s.textContent) { const a = [ "html,body,#main", "h1,h2,h3,h4,h5,h6,p,figure", "body,input,textarea,select,button", "*" ], c = s.textContent.split("}").filter((h) => { const S = h.trim().replace(/,\s+/g, ","); return !a.some((H) => S.startsWith(H)); }).map((h) => h.trim()).join(`} `), f = document.createElement("style"); for (const h of s.attributes) f.setAttribute(h.name, h.value); f.textContent = c, this.appendChild(f); } else console.warn("[Configurator] Bootstrapping configurator app: Could not find the CSS style element."); if (o && o.textContent) { const a = document.createElement("style"); for (const c of o.attributes) a.setAttribute(c.name, c.value); a.textContent = o.textContent, this.appendChild(a); } else console.warn("[Configurator] Bootstrapping configurator app: Could not find the fonts style element."); if (l ? this.appendChild(l.cloneNode()) : console.warn("[Configurator] Bootstrapping configurator app: Could not find the main div."), n && n instanceof HTMLScriptElement) if (n.src) { const a = document.createElement("script"); a.type = "module", a.src = n.src, a.setAttribute("async", ""), this.appendChild(a); } else if (n.textContent) { const a = document.createElement("script"); a.type = "module", a.textContent = n.textContent, this.appendChild(a); } else console.error("[Configurator] Script tag found, but no src or content available."); else console.warn("[Configurator] Bootstrapping configurator app: Could not find the main entry script."); if (m && m.textContent) { const a = document.createElement("script"); a.type = "importmap", a.textContent = m.textContent, this.appendChild(a); } } catch (e) { console.error("[Configurator] Failed to bootstrap configurator app", e); } } }; b.proxyBaseUrl = "https://spc.viamedici.io/framer/host-proxy"; b.attributeNames = ["src", "proxy-base-url"]; let C = b; const p = "data-", O = "spc.configurator.parameters", P = "spc.configurator.parameters.request"; function u(r) { return r.startsWith(p) && r.length > p.length; } function L(r) { return u(r) ? r.slice(p.length) : null; } function M(r) { const e = {}; for (const i of Array.from(r.attributes)) { const t = L(i.name); t != null && (e[t] = i.value); } return e; } function N(r) { return typeof r == "object" && r !== null && r.type === P; } const v = class y extends HTMLElement { constructor() { super(), this.src = null, this.hasConnected = !1, this.iframe = null, this.autoHeightHandler = null, this.allowedOrigin = null, this.parametersHandler = null, this.parameterObserver = null, this.iframeLoadHandler = null; } static get observedAttributes() { return y.attributeNames; } connectedCallback() { this.hasConnected = !0, this.src && (console.log("[Configurator] Web Component connected -> reinitialize the configurator app."), this.bootstrapApp()); } disconnectedCallback() { this.removeAutoHeightSync(), this.removeParameterSync(); } attributeChangedCallback(e, i, t) { e === "src" && t !== i && (this.src = t, this.hasConnected && this.isConnected && this.src && (console.log("[Configurator] The src was changed -> reinitialize the configurator app."), this.iframe ? (this.iframe.src = this.src, this.updateAllowedOrigin()) : this.bootstrapApp())); } bootstrapApp() { if (!this.src) { console.error("[Configurator] Cannot bootstrap iframe: src is missing."); return; } for (this.removeAutoHeightSync(), this.removeParameterSync(); this.firstChild; ) this.removeChild(this.firstChild); this.iframe = document.createElement("iframe"), this.iframe.src = this.src, this.updateAllowedOrigin(), this.iframe.width = "100%", this.iframe.height = "100%", this.iframe.style.border = "none", this.appendChild(this.iframe), this.useParameterSync(), this.getAttribute("no-auto-height") != null && this.getAttribute("no-auto-height") != "false" || this.useAutoHeightSync(); } updateAllowedOrigin() { try { this.allowedOrigin = this.src ? new URL(this.src).origin : null; } catch { console.warn("[Configurator] Invalid src URL provided, cannot derive allowed origin. Height synchronisation and parameter forwarding are disabled."), this.allowedOrigin = null; } } /** * Bridges custom parameters into the sandboxed iframe. The embedded app cannot read the * host element's attributes across the iframe boundary, so we forward them via postMessage: * - reply to the app's handshake request (reliable initial delivery once its listener is up), * - push on every parameter attribute change (hot-reload), * - push once on iframe load (covers apps that never change parameters afterwards). * Outbound messages always target the derived origin, never "*". */ useParameterSync() { var e; this.parametersHandler = (i) => { !this.iframe || i.source !== this.iframe.contentWindow || N(i.data) && this.postParameters(); }, window.addEventListener("message", this.parametersHandler), this.parameterObserver = new MutationObserver((i) => { i.some((t) => t.type === "attributes" && t.attributeName != null && u(t.attributeName)) && this.postParameters(); }), this.parameterObserver.observe(this, { attributes: !0 }), this.iframeLoadHandler = () => this.postParameters(), (e = this.iframe) == null || e.addEventListener("load", this.iframeLoadHandler); } postParameters() { var i; if (!((i = this.iframe) != null && i.contentWindow) || !this.allowedOrigin) return; const e = { type: O, parameters: M(this) }; this.iframe.contentWindow.postMessage(e, this.allowedOrigin); } removeParameterSync() { var e; this.parametersHandler && (window.removeEventListener("message", this.parametersHandler), this.parametersHandler = null), this.iframe && this.iframeLoadHandler && this.iframe.removeEventListener("load", this.iframeLoadHandler), this.iframeLoadHandler = null, (e = this.parameterObserver) == null || e.disconnect(), this.parameterObserver = null; } useAutoHeightSync() { this.autoHeightHandler = (e) => { var i; this.allowedOrigin && e.origin !== this.allowedOrigin || ((i = e.data) == null ? void 0 : i.type) !== "spc.configurator.height" || typeof e.data.height != "number" || (console.log("[Configurator] Received new height from iFrame -> Apply", e.data.height), this.iframe.style.height = `${e.data.height + 10}px`); }, window.addEventListener("message", this.autoHeightHandler); } removeAutoHeightSync() { this.autoHeightHandler && (window.removeEventListener("message", this.autoHeightHandler), this.autoHeightHandler = null); } }; v.attributeNames = ["src", "no-auto-height"]; let E = v; const w = class g extends HTMLElement { constructor() { super(), this.child = null, this.isIsolated = !1, this.parameterObserver = null; } static get observedAttributes() { return ["isolated", ...g.childAttributes]; } connectedCallback() { this.initialize(), this.startParameterObserver(); } disconnectedCallback() { this.stopParameterObserver(), this.child && this.contains(this.child) && this.removeChild(this.child), this.child = null; } attributeChangedCallback(e, i, t) { if (e === "isolated") { const s = t !== null && t !== "false"; if (s !== this.isIsolated) { console.log("[Configurator] The isolation mode was changed -> reinitialize the configurator app."), this.isIsolated = s, this.initialize(); return; } } this.child && i !== t && (t === null ? this.child.removeAttribute(e) : this.child.setAttribute(e, t)); } initialize() { const e = this.getAttribute("isolated"); this.isIsolated = e !== null && e !== "false"; const i = this.isIsolated ? "spc-embedded-configurator-iframe" : "spc-embedded-configurator-native"; if (this.child && this.child.tagName.toLowerCase() === i) return; this.child && this.removeChild(this.child); const t = document.createElement(i); for (const { name: s, value: o } of Array.from(this.attributes)) (g.childAttributes.some((n) => n === s) || u(s)) && o !== null && t.setAttribute(s, o); this.appendChild(t), this.child = t; } /** * Custom parameter attributes (`data-*`) can have arbitrary names, so they cannot be * enumerated in the static `observedAttributes`. A MutationObserver forwards their * changes to the active child. It is intentionally scoped to parameter attributes only, * so it never overlaps with `attributeChangedCallback`, which owns the known attributes. */ startParameterObserver() { this.parameterObserver || (this.parameterObserver = new MutationObserver((e) => { if (this.child) for (const i of e) { const t = i.attributeName; if (i.type !== "attributes" || t == null || !u(t)) continue; const s = this.getAttribute(t); s === null ? this.child.removeAttribute(t) : this.child.setAttribute(t, s); } }), this.parameterObserver.observe(this, { attributes: !0 })); } stopParameterObserver() { var e; (e = this.parameterObserver) == null || e.disconnect(), this.parameterObserver = null; } }; w.childAttributes = [...C.attributeNames, ...E.attributeNames]; let R = w; customElements.define("spc-embedded-configurator", R); customElements.define("spc-embedded-configurator-native", C); customElements.define("spc-embedded-configurator-iframe", E); export { C as EmbeddedConfiguratorNative, R as EmbeddedConfiguratorStrategy };