@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
JavaScript
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
};