@scaleflex/template-builder 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/integrate-template-builder/SKILL.md +27 -21
- package/CHANGELOG.md +168 -4
- package/README.md +207 -52
- package/dist/dam-store.d.ts +92 -0
- package/dist/define.cjs +1 -1
- package/dist/define.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -8
- package/dist/protocol.d.ts +60 -2
- package/dist/react.cjs +1 -1
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.ts +34 -1
- package/dist/react.js +43 -28
- package/dist/react.js.map +1 -1
- package/dist/template-builder-B9Cwo_Q-.js +651 -0
- package/dist/template-builder-B9Cwo_Q-.js.map +1 -0
- package/dist/template-builder-Byqg1q93.cjs +53 -0
- package/dist/template-builder-Byqg1q93.cjs.map +1 -0
- package/dist/template-builder.d.ts +164 -4
- package/package.json +1 -1
- package/src/dam-store.ts +388 -0
- package/src/index.ts +2 -0
- package/src/protocol.ts +64 -2
- package/src/react.ts +111 -27
- package/src/template-builder.ts +405 -7
- package/dist/template-builder-CK2Zlo7E.cjs +0 -53
- package/dist/template-builder-CK2Zlo7E.cjs.map +0 -1
- package/dist/template-builder-De0hRO4s.js +0 -380
- package/dist/template-builder-De0hRO4s.js.map +0 -1
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
import { LitElement as L, css as M, html as f, nothing as k } from "lit";
|
|
2
|
+
import { property as n, state as y } from "lit/decorators.js";
|
|
3
|
+
const de = 3, P = "design-templates:builder:ready", K = "design-templates:builder:open", $ = "design-templates:builder:close", B = "design-templates:builder:save", F = "design-templates:builder:error", x = "design-templates:builder:content-request", j = "design-templates:builder:content", q = "design-templates:builder:dirty", H = "design-templates:host:load", Q = `<?xml version="1.0" encoding="UTF-8"?>
|
|
4
|
+
<template><templateInfo><version>0.3</version></templateInfo><rootStack/><layouts/><variables/></template>`, J = "design-templates:host:config", X = "design-templates:host:saved", c = {
|
|
5
|
+
SESSION_UUID: "suuid",
|
|
6
|
+
COMPANY_UUID: "cuuid",
|
|
7
|
+
PROJECT_UUID: "puuid",
|
|
8
|
+
SASS_KEY: "sassKey",
|
|
9
|
+
FILEROBOT_TOKEN: "ftoken",
|
|
10
|
+
/**
|
|
11
|
+
* Filerobot security-template key, the alternative to a Hub session. The app
|
|
12
|
+
* exchanges it for a sass key itself and runs in a reduced mode — see
|
|
13
|
+
* `AUTH_MODES`. Sent *instead of* `sassKey` + `suuid`, never alongside them.
|
|
14
|
+
*/
|
|
15
|
+
SEC_TEMPLATE: "secTemplate",
|
|
16
|
+
IFRAME: "iframe",
|
|
17
|
+
/** Origin of the embedding page; the app uses it as postMessage targetOrigin. */
|
|
18
|
+
EMBED_ORIGIN: "embedOrigin",
|
|
19
|
+
/**
|
|
20
|
+
* Accent colour for the editor chrome, as `#rgb` / `#rrggbb`. The app derives
|
|
21
|
+
* its whole accent ramp from it. Rejected server-side if it doesn't match
|
|
22
|
+
* that shape — it ends up inside a stylesheet.
|
|
23
|
+
*/
|
|
24
|
+
BRAND_COLOR: "brandColor",
|
|
25
|
+
/** Colour scheme for the editor chrome: `light` | `dark` | `auto`. */
|
|
26
|
+
THEME: "theme"
|
|
27
|
+
}, le = {
|
|
28
|
+
SESSION: "session",
|
|
29
|
+
SEC_TEMPLATE: "secTemplate"
|
|
30
|
+
}, ce = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
31
|
+
function Y(s) {
|
|
32
|
+
return s ? `/templates/${encodeURIComponent(s)}/edit` : "/templates/new";
|
|
33
|
+
}
|
|
34
|
+
const G = "/templates/embed", _ = "https://api.filerobot.com";
|
|
35
|
+
async function V(s) {
|
|
36
|
+
if (!s.secTemplate) return s.sassKey ?? "";
|
|
37
|
+
const e = await fetch(
|
|
38
|
+
`${_}/${encodeURIComponent(s.token)}/v5/key/${encodeURIComponent(s.secTemplate)}`,
|
|
39
|
+
{ headers: { "X-Filerobot-Key": s.secTemplate } }
|
|
40
|
+
), t = await e.json().catch(() => ({}));
|
|
41
|
+
if (!e.ok || !t.key)
|
|
42
|
+
throw new Error(t.msg ?? `key exchange failed: ${e.status}`);
|
|
43
|
+
return t.key;
|
|
44
|
+
}
|
|
45
|
+
function R(s, e) {
|
|
46
|
+
const t = { "X-Filerobot-Key": e };
|
|
47
|
+
return s.secTemplate || (s.sessionUuid && (t["X-Session-Token"] = s.sessionUuid), s.companyUuid && (t["X-Company-Token"] = s.companyUuid), s.projectUuid && (t["X-Project-Token"] = s.projectUuid)), t;
|
|
48
|
+
}
|
|
49
|
+
function W(s) {
|
|
50
|
+
return /^[0-9a-f][0-9a-f-]{18,}$/i.test(s);
|
|
51
|
+
}
|
|
52
|
+
async function I(s, e, t) {
|
|
53
|
+
let o;
|
|
54
|
+
try {
|
|
55
|
+
o = await fetch(
|
|
56
|
+
`${_}/${encodeURIComponent(s.token)}/v5/files/${encodeURIComponent(t)}`,
|
|
57
|
+
{ headers: R(s, e) }
|
|
58
|
+
);
|
|
59
|
+
} catch (l) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`file lookup failed: ${l instanceof Error ? l.message : String(l)}`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
const i = await o.json().catch(() => ({}));
|
|
65
|
+
if ([400, 404, 410, 422].includes(o.status)) return null;
|
|
66
|
+
const d = i.msg ?? i.message ?? "";
|
|
67
|
+
if (!o.ok || i.status === "error") {
|
|
68
|
+
if (/not[ _-]?found|does not exist|no such file/i.test(d)) return null;
|
|
69
|
+
throw new Error(d || `file lookup failed: ${o.status}`);
|
|
70
|
+
}
|
|
71
|
+
return i.file ?? null;
|
|
72
|
+
}
|
|
73
|
+
async function z(s, e, t, o) {
|
|
74
|
+
const i = new URL(
|
|
75
|
+
`${_}/${encodeURIComponent(s.token)}/v5/files`
|
|
76
|
+
);
|
|
77
|
+
i.searchParams.set("q", `name:"${t}"`), i.searchParams.set("folder", o), i.searchParams.set("sort", "modified_at:desc"), i.searchParams.set("limit", "5");
|
|
78
|
+
const d = await fetch(i, { headers: R(s, e) }), l = await d.json().catch(() => ({}));
|
|
79
|
+
return !d.ok || l.status === "error" ? null : l.files?.find((h) => h.name === t) ?? null;
|
|
80
|
+
}
|
|
81
|
+
function Z(s) {
|
|
82
|
+
let e = 2166136261;
|
|
83
|
+
for (let t = 0; t < s.length; t++)
|
|
84
|
+
e ^= s.charCodeAt(t), e = Math.imul(e, 16777619);
|
|
85
|
+
return (e >>> 0).toString(36);
|
|
86
|
+
}
|
|
87
|
+
function ee(s) {
|
|
88
|
+
let e;
|
|
89
|
+
try {
|
|
90
|
+
e = decodeURIComponent(s);
|
|
91
|
+
} catch {
|
|
92
|
+
e = s;
|
|
93
|
+
}
|
|
94
|
+
const t = e.split("/").filter(Boolean);
|
|
95
|
+
return t.pop(), "/" + t.join("/");
|
|
96
|
+
}
|
|
97
|
+
function te(s, e) {
|
|
98
|
+
if (!s) return s;
|
|
99
|
+
let t;
|
|
100
|
+
try {
|
|
101
|
+
const i = new DOMParser().parseFromString(e, "application/xml");
|
|
102
|
+
if (i.querySelector("parsererror")) return s;
|
|
103
|
+
t = i.querySelectorAll(
|
|
104
|
+
'variable[custom_ckey][source="URL"][type="text_placeholder"]'
|
|
105
|
+
);
|
|
106
|
+
} catch {
|
|
107
|
+
return s;
|
|
108
|
+
}
|
|
109
|
+
if (t.length === 0) return s;
|
|
110
|
+
const o = new Set([...t].map((i) => i.getAttribute("name") ?? ""));
|
|
111
|
+
return s.split("&").filter(Boolean).filter((i) => !o.has(i.split("=")[0].replace(/^(\$|%24)/, ""))).join("&");
|
|
112
|
+
}
|
|
113
|
+
let se = 0;
|
|
114
|
+
const ie = /* @__PURE__ */ new Set([
|
|
115
|
+
"ERROR_SHA1_CONFLICT",
|
|
116
|
+
"SAME_ASSET_EXISTS_SKIP_UPLOAD"
|
|
117
|
+
]);
|
|
118
|
+
async function oe(s, e, t, o) {
|
|
119
|
+
if (!e.token) throw new Error("dam-store needs a token");
|
|
120
|
+
const i = await V(e);
|
|
121
|
+
if (!i) throw new Error("dam-store needs a sass key or a security template");
|
|
122
|
+
const d = !!s.templateId && W(s.templateId), l = !!o && o !== s.templateId, [h, b] = await Promise.allSettled([
|
|
123
|
+
d ? I(e, i, s.templateId) : Promise.resolve(null),
|
|
124
|
+
l ? I(e, i, o) : Promise.resolve(null)
|
|
125
|
+
]);
|
|
126
|
+
if (h.status === "rejected") throw h.reason;
|
|
127
|
+
const g = h.value;
|
|
128
|
+
let v = null;
|
|
129
|
+
if (b.status === "fulfilled") v = b.value;
|
|
130
|
+
else if (!g) throw b.reason;
|
|
131
|
+
const m = g ?? v, C = m?.url?.path ? ee(m.url.path) : t || "/", D = (s.name ?? "").trim().replace(/\.fdt$/i, "") || "template", A = m?.name ? m.name : s.templateId ? `${D}_${Z(s.templateId)}.fdt` : `${D}_${Date.now().toString(36)}-${++se}.fdt`, S = new FormData();
|
|
132
|
+
S.append(
|
|
133
|
+
"files[]",
|
|
134
|
+
new Blob([s.content], { type: "text/xml+sfxtemplate" }),
|
|
135
|
+
A
|
|
136
|
+
);
|
|
137
|
+
const N = te(s.templateQuery ?? "", s.content);
|
|
138
|
+
N && S.append(
|
|
139
|
+
"info[files[]]",
|
|
140
|
+
JSON.stringify({ custom: { template_query: N } })
|
|
141
|
+
);
|
|
142
|
+
const E = await fetch(
|
|
143
|
+
`${_}/${encodeURIComponent(e.token)}/v4/files?folder=${encodeURIComponent(C)}`,
|
|
144
|
+
{
|
|
145
|
+
method: "POST",
|
|
146
|
+
headers: { ...R(e, i), "X-Filerobot-Template": "true" },
|
|
147
|
+
body: S
|
|
148
|
+
}
|
|
149
|
+
), p = await E.json().catch(() => ({})), U = p.code ?? p.files?.failed?.[0]?.code;
|
|
150
|
+
if (U && ie.has(U)) {
|
|
151
|
+
let u = v ?? g;
|
|
152
|
+
if (u?.uuid || (u = await z(e, i, A, C).catch(() => null)), u?.uuid)
|
|
153
|
+
return {
|
|
154
|
+
uuid: u.uuid,
|
|
155
|
+
url: u.url?.cdn ?? u.url?.public ?? ""
|
|
156
|
+
};
|
|
157
|
+
throw new Error(
|
|
158
|
+
`this project's deduplication policy already stores this exact content as another file (${U}) — edit the content, or pass that file's uuid as stored-uuid so the save can resolve to it`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
if (!E.ok || p.status === "error")
|
|
162
|
+
throw new Error(p.msg ?? p.message ?? `upload failed: ${E.status}`);
|
|
163
|
+
const T = p.file?.uuid ?? p.files?.uploaded?.[0]?.uuid;
|
|
164
|
+
if (!T) throw new Error("upload succeeded but no file uuid in the response");
|
|
165
|
+
let w = null;
|
|
166
|
+
try {
|
|
167
|
+
w = await I(e, i, T);
|
|
168
|
+
} catch {
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
uuid: T,
|
|
172
|
+
url: w?.url?.cdn ?? w?.url?.public ?? p.file?.url?.cdn ?? ""
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
var re = Object.defineProperty, a = (s, e, t, o) => {
|
|
176
|
+
for (var i = void 0, d = s.length - 1, l; d >= 0; d--)
|
|
177
|
+
(l = s[d]) && (i = l(e, t, i) || i);
|
|
178
|
+
return i && re(e, t, i), i;
|
|
179
|
+
};
|
|
180
|
+
const O = class O extends L {
|
|
181
|
+
constructor() {
|
|
182
|
+
super(...arguments), this.baseUrl = "", this.token = "", this.sassKey = "", this.sessionUuid = "", this.secTemplate = "", this.companyUuid = "", this.projectUuid = "", this.templateId = "", this.mode = "inline", this.stateless = !1, this.content = "", this.newTemplate = !1, this.templateName = "", this.templateQuery = "", this.brandColor = "", this.theme = "", this.customMetadata = [], this.customMetadataLabel = "", this.damStore = !1, this.storeFolder = "/", this.storedUuid = "", this.readyTimeout = 2e4, this._status = "idle", this._open = !1, this._src = "", this._contentRequested = !1, this._reportedStatelessRequired = !1, this._isDirty = !1, this._autoOpenDecided = !1, this._onMessage = (e) => {
|
|
183
|
+
if (!this._open || !e.origin || e.origin !== this._appOrigin) return;
|
|
184
|
+
const t = this.shadowRoot?.querySelector("iframe");
|
|
185
|
+
if (!t || e.source !== t.contentWindow) return;
|
|
186
|
+
const o = e.data;
|
|
187
|
+
if (!(!o || typeof o.type != "string"))
|
|
188
|
+
switch (o.type) {
|
|
189
|
+
case P:
|
|
190
|
+
case K:
|
|
191
|
+
this._clearHandshakeTimer(), this._status !== "ready" && (this._status = "ready", this._emit("ready")), o.type === P && (this._sentConfigKey = void 0), this._maybeSendConfig(), o.type === K && this._emit("open");
|
|
192
|
+
break;
|
|
193
|
+
case B:
|
|
194
|
+
this._emit("save", o.data);
|
|
195
|
+
break;
|
|
196
|
+
case x:
|
|
197
|
+
this._contentRequested = !0, this._sentKey = void 0, this._sentSaveKey = void 0, this._maybeSendContent();
|
|
198
|
+
break;
|
|
199
|
+
case j: {
|
|
200
|
+
const i = o.data;
|
|
201
|
+
if (this._sentKey = this._contentKey(i.content), this._sentSaveKey = this._detailKey(i), this.damStore) {
|
|
202
|
+
this._pendingSaves.add(i);
|
|
203
|
+
const d = this._docEpoch, l = {
|
|
204
|
+
auth: {
|
|
205
|
+
token: this.token,
|
|
206
|
+
sassKey: this.sassKey,
|
|
207
|
+
secTemplate: this.secTemplate,
|
|
208
|
+
sessionUuid: this.sessionUuid,
|
|
209
|
+
companyUuid: this.companyUuid,
|
|
210
|
+
projectUuid: this.projectUuid
|
|
211
|
+
},
|
|
212
|
+
storeFolder: this.storeFolder || "/"
|
|
213
|
+
};
|
|
214
|
+
this._storeQueue = this._storeQueue.then(
|
|
215
|
+
() => this._storeAndEmitSave(i, d, l)
|
|
216
|
+
);
|
|
217
|
+
} else
|
|
218
|
+
this._emit("save", i);
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
case q: {
|
|
222
|
+
const i = o.data;
|
|
223
|
+
this._isDirty = !!i?.isDirty, this._emit("dirtychange", { isDirty: this._isDirty });
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
case $:
|
|
227
|
+
this._emit("close"), this.mode === "modal" && (this._open = !1);
|
|
228
|
+
break;
|
|
229
|
+
case F:
|
|
230
|
+
this._fail(o.data ?? { code: "unknown" });
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}, this._storeQueue = Promise.resolve(), this._pendingSaves = /* @__PURE__ */ new Set(), this._docEpoch = 0;
|
|
234
|
+
}
|
|
235
|
+
get status() {
|
|
236
|
+
return this._status;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Stateless mode: whether the editor holds edits that have not been handed
|
|
240
|
+
* back yet. Check this before calling `load()` — a swap discards them.
|
|
241
|
+
* Always false in DAM-backed mode, where the app owns saving.
|
|
242
|
+
*/
|
|
243
|
+
get isDirty() {
|
|
244
|
+
return this._isDirty;
|
|
245
|
+
}
|
|
246
|
+
/** Open the editor (loads the iframe). Optionally switch template first. */
|
|
247
|
+
open(e) {
|
|
248
|
+
e !== void 0 && (this.templateId = e), this._open = !0;
|
|
249
|
+
}
|
|
250
|
+
/** Close the editor and unload the iframe. Does not emit `close`. */
|
|
251
|
+
close() {
|
|
252
|
+
this._open = !1;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Stateless mode: load a template, opening the editor if needed. Equivalent
|
|
256
|
+
* to assigning `templateId` / `content` / `templateName` and calling `open()`.
|
|
257
|
+
*/
|
|
258
|
+
load({
|
|
259
|
+
content: e,
|
|
260
|
+
templateId: t,
|
|
261
|
+
name: o,
|
|
262
|
+
templateQuery: i,
|
|
263
|
+
storedUuid: d
|
|
264
|
+
}) {
|
|
265
|
+
t !== void 0 && (this.templateId = t), o !== void 0 && (this.templateName = o), i !== void 0 && (this.templateQuery = i), this.storedUuid = d ?? "", this.newTemplate = !1, this.content = e, this._open = !0;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Stateless mode: open the editor on a new, empty template, opening it if
|
|
269
|
+
* needed. Equivalent to setting `new-template` and calling `open()`.
|
|
270
|
+
*
|
|
271
|
+
* The user chooses the canvas size from the editor's empty state; `save`
|
|
272
|
+
* then hands back a complete `.fdt` document — the first one you store for
|
|
273
|
+
* this record. Pass `templateId` if you have already allocated one; leave it
|
|
274
|
+
* out and the `save` payload simply comes back without an id.
|
|
275
|
+
*
|
|
276
|
+
* Calling it again on an editor that is already showing the blank document
|
|
277
|
+
* does nothing — resending would discard whatever the user has built since.
|
|
278
|
+
* To genuinely start over, `close()` and reopen.
|
|
279
|
+
*/
|
|
280
|
+
createNew({
|
|
281
|
+
templateId: e,
|
|
282
|
+
name: t
|
|
283
|
+
} = {}) {
|
|
284
|
+
e !== void 0 && (this.templateId = e), t !== void 0 && (this.templateName = t), this.templateQuery = "", this.storedUuid = "", this.newTemplate = !0, this.content = "", this._open = !0;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Stateless mode: report back whether a `save` was persisted on your side.
|
|
288
|
+
*
|
|
289
|
+
* Optional. The editor clears its unsaved-changes flag as soon as it hands
|
|
290
|
+
* the content over, so not calling this leaves the previous behaviour intact.
|
|
291
|
+
* Calling it with `false` is what earns something: the editor restores the
|
|
292
|
+
* dirty flag and tells the user, rather than showing a failed write as saved.
|
|
293
|
+
*
|
|
294
|
+
* No-op outside stateless mode, where the app did the saving and has nothing
|
|
295
|
+
* to hear back about.
|
|
296
|
+
*/
|
|
297
|
+
confirmSave(e, t) {
|
|
298
|
+
this.stateless && this._postToApp({ type: X, data: { ok: e, message: t } });
|
|
299
|
+
}
|
|
300
|
+
connectedCallback() {
|
|
301
|
+
super.connectedCallback(), window.addEventListener("message", this._onMessage);
|
|
302
|
+
}
|
|
303
|
+
disconnectedCallback() {
|
|
304
|
+
super.disconnectedCallback(), window.removeEventListener("message", this._onMessage), this._clearHandshakeTimer(), setTimeout(() => {
|
|
305
|
+
this.isConnected || this._flushPendingSaves("widget removed before the rendering copy completed");
|
|
306
|
+
}, 0);
|
|
307
|
+
}
|
|
308
|
+
willUpdate(e) {
|
|
309
|
+
super.willUpdate(e), this._autoOpenDecided || (this._autoOpenDecided = !0, this.mode === "inline" && (this._open = !0));
|
|
310
|
+
const t = this._computeSrc();
|
|
311
|
+
t !== this._src && (this._src = t, this._status = t ? "loading" : "idle");
|
|
312
|
+
}
|
|
313
|
+
updated(e) {
|
|
314
|
+
e.has("_src") && (this._clearHandshakeTimer(), this._contentRequested = !1, this._sentKey = void 0, this._sentSaveKey = void 0, this._sentConfigKey = void 0, this._isDirty && (this._isDirty = !1, this._emit("dirtychange", { isDirty: !1 })), this._src && this._startHandshakeTimer()), (e.has("content") || e.has("newTemplate") || e.has("templateId") || e.has("templateName") || e.has("templateQuery")) && this._maybeSendContent(), (e.has("customMetadata") || e.has("customMetadataLabel")) && this._status === "ready" && this._maybeSendConfig();
|
|
315
|
+
}
|
|
316
|
+
render() {
|
|
317
|
+
const e = this._src ? f`<iframe
|
|
318
|
+
part="iframe"
|
|
319
|
+
title="Template builder"
|
|
320
|
+
src=${this._src}
|
|
321
|
+
allow="clipboard-read; clipboard-write"
|
|
322
|
+
></iframe>` : k, t = this._status === "loading" ? f`<div class="spinner" part="spinner"></div>` : k;
|
|
323
|
+
return this.mode === "modal" ? this._open ? f`<div class="overlay" part="overlay">
|
|
324
|
+
<div class="stage">${e}${t}</div>
|
|
325
|
+
</div>` : k : f`${e}${t}`;
|
|
326
|
+
}
|
|
327
|
+
_computeSrc() {
|
|
328
|
+
if (!this._open || !this.baseUrl || !this.token) return "";
|
|
329
|
+
if (this.secTemplate) {
|
|
330
|
+
if (!this.stateless)
|
|
331
|
+
return this._reportedStatelessRequired || (this._reportedStatelessRequired = !0, queueMicrotask(
|
|
332
|
+
() => this._fail({
|
|
333
|
+
code: "invalid-config",
|
|
334
|
+
message: "sec-template requires stateless mode — the app accepts a security template on the stateless embed route only."
|
|
335
|
+
})
|
|
336
|
+
)), "";
|
|
337
|
+
this._reportedStatelessRequired = !1;
|
|
338
|
+
} else if (!this.sassKey || !this.sessionUuid)
|
|
339
|
+
return "";
|
|
340
|
+
let e;
|
|
341
|
+
try {
|
|
342
|
+
const t = this.stateless ? G : Y(this.templateId || void 0), o = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
343
|
+
e = new URL(t.replace(/^\//, ""), o);
|
|
344
|
+
} catch {
|
|
345
|
+
return this._reportedBadBaseUrl !== this.baseUrl && (this._reportedBadBaseUrl = this.baseUrl, queueMicrotask(
|
|
346
|
+
() => this._fail({
|
|
347
|
+
code: "invalid-base-url",
|
|
348
|
+
message: `base-url is not a valid URL: ${this.baseUrl}`
|
|
349
|
+
})
|
|
350
|
+
)), "";
|
|
351
|
+
}
|
|
352
|
+
return this._reportedBadBaseUrl = void 0, e.searchParams.set(c.FILEROBOT_TOKEN, this.token), this.secTemplate ? e.searchParams.set(c.SEC_TEMPLATE, this.secTemplate) : (e.searchParams.set(c.SASS_KEY, this.sassKey), e.searchParams.set(c.SESSION_UUID, this.sessionUuid), this.companyUuid && e.searchParams.set(c.COMPANY_UUID, this.companyUuid), this.projectUuid && e.searchParams.set(c.PROJECT_UUID, this.projectUuid)), this.brandColor && e.searchParams.set(c.BRAND_COLOR, this.brandColor), this.theme && e.searchParams.set(c.THEME, this.theme), e.searchParams.set(c.IFRAME, "1"), e.searchParams.set(c.EMBED_ORIGIN, window.location.origin), e.toString();
|
|
353
|
+
}
|
|
354
|
+
get _appOrigin() {
|
|
355
|
+
try {
|
|
356
|
+
return new URL(this.baseUrl).origin;
|
|
357
|
+
} catch {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Emit every not-yet-emitted `dam-store` save immediately, raw content with
|
|
363
|
+
* `storeError` in place of the links. Called when waiting any longer risks
|
|
364
|
+
* the event finding no listener; a still-running upload for a flushed save
|
|
365
|
+
* is left to finish (the copy usually lands) but will not emit again.
|
|
366
|
+
*
|
|
367
|
+
* Public for framework wrappers: one that unsubscribes its listeners before
|
|
368
|
+
* unmounting must call this first, while they are still attached — the
|
|
369
|
+
* element's own disconnect-time flush fires only after the wrapper has
|
|
370
|
+
* stopped listening, and the raw save would be lost. The React wrapper does
|
|
371
|
+
* this; a vanilla host never needs to call it.
|
|
372
|
+
*/
|
|
373
|
+
flushPendingSaves(e = "widget removed before the rendering copy completed") {
|
|
374
|
+
this._flushPendingSaves(e);
|
|
375
|
+
}
|
|
376
|
+
_flushPendingSaves(e) {
|
|
377
|
+
for (const t of this._pendingSaves)
|
|
378
|
+
this._pendingSaves.delete(t), this._emit("save", { ...t, storeError: e });
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* The `dam-store` save path: upload the edited template to Filerobot, then
|
|
382
|
+
* emit `save` with the stored copy's links on the detail. The upload is the
|
|
383
|
+
* render side of the save — the CDN renders only stored files — while the
|
|
384
|
+
* raw `content` stays the host's copy exactly as without the flag.
|
|
385
|
+
*
|
|
386
|
+
* A failed upload still emits `save` (the raw data must reach the host
|
|
387
|
+
* either way), with `storeError` in place of `stored`; whether a save
|
|
388
|
+
* without a rendering copy counts as saved is the host's decision, made
|
|
389
|
+
* where it always is — the save ack.
|
|
390
|
+
*/
|
|
391
|
+
async _storeAndEmitSave(e, t, o) {
|
|
392
|
+
if (!this._pendingSaves.has(e)) return;
|
|
393
|
+
const i = (this._storeMemory?.epoch === t ? this._storeMemory.uuid : void 0) || t === this._docEpoch && this.storedUuid || void 0;
|
|
394
|
+
let d, l;
|
|
395
|
+
try {
|
|
396
|
+
d = await oe(e, o.auth, o.storeFolder, i);
|
|
397
|
+
} catch (h) {
|
|
398
|
+
l = h instanceof Error ? h.message : String(h);
|
|
399
|
+
}
|
|
400
|
+
this._pendingSaves.delete(e) && (d && (this._storeMemory = { epoch: t, uuid: d.uuid }), this._emit("save", d ? { ...e, stored: d } : { ...e, storeError: l }));
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Deliver `content` to the app once both sides are ready: it has asked, and
|
|
404
|
+
* we have something new to give it. Skips a re-send of identical content so
|
|
405
|
+
* an unrelated re-render can't discard the user's in-progress edits.
|
|
406
|
+
*/
|
|
407
|
+
_maybeSendContent() {
|
|
408
|
+
if (!this.stateless || !this._contentRequested) return;
|
|
409
|
+
const e = this.content || (this.newTemplate ? Q : "");
|
|
410
|
+
if (!e) return;
|
|
411
|
+
const t = {
|
|
412
|
+
templateId: this.templateId || void 0,
|
|
413
|
+
content: e,
|
|
414
|
+
name: this.templateName || void 0,
|
|
415
|
+
templateQuery: this.templateQuery || void 0
|
|
416
|
+
}, o = this._contentKey(e), i = JSON.stringify({
|
|
417
|
+
templateId: this.templateId || void 0,
|
|
418
|
+
content: e,
|
|
419
|
+
name: this.templateName || void 0
|
|
420
|
+
});
|
|
421
|
+
if (o === this._sentKey || o === this._sentSaveKey) {
|
|
422
|
+
this._lastDocKey = i;
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
this._postToApp({ type: H, data: t }) && (i !== this._lastDocKey && this._docEpoch++, this._lastDocKey = i, this._sentKey = o, this._sentSaveKey = void 0);
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Deliver host config to the app. Unlike content this is not requested — the
|
|
429
|
+
* app has no way to know a host means to send any — so it goes out on the
|
|
430
|
+
* ready signal and on every later change.
|
|
431
|
+
*
|
|
432
|
+
* Sending an empty model is meaningful: it is how a host clears one it set
|
|
433
|
+
* before. What is skipped is only a *repeat* of what the app already holds,
|
|
434
|
+
* and the very first send when there was never anything to say.
|
|
435
|
+
*/
|
|
436
|
+
_maybeSendConfig() {
|
|
437
|
+
const e = Array.isArray(this.customMetadata) ? this.customMetadata : [], t = typeof this.customMetadataLabel == "string" ? this.customMetadataLabel.trim() : "", o = JSON.stringify({ customMetadata: e, customMetadataLabel: t });
|
|
438
|
+
o !== this._sentConfigKey && (this._sentConfigKey === void 0 && e.length === 0 && t === "" || this._postToApp({
|
|
439
|
+
type: J,
|
|
440
|
+
data: {
|
|
441
|
+
customMetadata: e,
|
|
442
|
+
...t ? { customMetadataLabel: t } : {}
|
|
443
|
+
}
|
|
444
|
+
}) && (this._sentConfigKey = o));
|
|
445
|
+
}
|
|
446
|
+
/** Identity of a delivered template, as compared against `_sentKey`. */
|
|
447
|
+
_contentKey(e) {
|
|
448
|
+
return JSON.stringify({
|
|
449
|
+
templateId: this.templateId || void 0,
|
|
450
|
+
content: e,
|
|
451
|
+
name: this.templateName || void 0,
|
|
452
|
+
templateQuery: this.templateQuery || void 0
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Identity of a save the app handed back — the same shape as
|
|
457
|
+
* `_contentKey`, but built from the save detail rather than the props, so
|
|
458
|
+
* a host echoing the detail (whose name/query the save changed) matches.
|
|
459
|
+
*/
|
|
460
|
+
_detailKey(e) {
|
|
461
|
+
return JSON.stringify({
|
|
462
|
+
templateId: e.templateId || void 0,
|
|
463
|
+
content: e.content,
|
|
464
|
+
name: e.name || void 0,
|
|
465
|
+
templateQuery: e.templateQuery || void 0
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
/** Post into the iframe, targeted at the app origin. False if not mounted. */
|
|
469
|
+
_postToApp(e) {
|
|
470
|
+
const t = this.shadowRoot?.querySelector("iframe")?.contentWindow, o = this._appOrigin;
|
|
471
|
+
return !t || !o ? !1 : (t.postMessage(e, o), !0);
|
|
472
|
+
}
|
|
473
|
+
_startHandshakeTimer() {
|
|
474
|
+
this.readyTimeout <= 0 || (this._handshakeTimer = window.setTimeout(() => {
|
|
475
|
+
this._fail({
|
|
476
|
+
code: "handshake-timeout",
|
|
477
|
+
message: `No ready signal from ${this.baseUrl} within ${this.readyTimeout}ms. Check that this origin is in the app's frame-ancestors allowlist and that third-party cookies are not blocked.`
|
|
478
|
+
});
|
|
479
|
+
}, this.readyTimeout));
|
|
480
|
+
}
|
|
481
|
+
_clearHandshakeTimer() {
|
|
482
|
+
this._handshakeTimer !== void 0 && (window.clearTimeout(this._handshakeTimer), this._handshakeTimer = void 0);
|
|
483
|
+
}
|
|
484
|
+
_fail(e) {
|
|
485
|
+
this._clearHandshakeTimer(), this._status = "error", this._emit("error", e);
|
|
486
|
+
}
|
|
487
|
+
_emit(e, t) {
|
|
488
|
+
this.dispatchEvent(
|
|
489
|
+
new CustomEvent(e, { detail: t, bubbles: !0, composed: !0 })
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
O.styles = M`
|
|
494
|
+
:host {
|
|
495
|
+
display: block;
|
|
496
|
+
position: relative;
|
|
497
|
+
}
|
|
498
|
+
:host([mode='modal']) {
|
|
499
|
+
display: contents;
|
|
500
|
+
}
|
|
501
|
+
.overlay {
|
|
502
|
+
position: fixed;
|
|
503
|
+
inset: 0;
|
|
504
|
+
z-index: 2147483000;
|
|
505
|
+
background: rgba(0, 0, 0, 0.55);
|
|
506
|
+
display: flex;
|
|
507
|
+
}
|
|
508
|
+
.stage {
|
|
509
|
+
position: relative;
|
|
510
|
+
flex: 1;
|
|
511
|
+
display: flex;
|
|
512
|
+
}
|
|
513
|
+
iframe {
|
|
514
|
+
border: 0;
|
|
515
|
+
flex: 1;
|
|
516
|
+
width: 100%;
|
|
517
|
+
height: 100%;
|
|
518
|
+
}
|
|
519
|
+
.spinner {
|
|
520
|
+
position: absolute;
|
|
521
|
+
inset: 0;
|
|
522
|
+
margin: auto;
|
|
523
|
+
width: 32px;
|
|
524
|
+
height: 32px;
|
|
525
|
+
border: 3px solid rgba(128, 128, 128, 0.3);
|
|
526
|
+
border-top-color: currentColor;
|
|
527
|
+
border-radius: 50%;
|
|
528
|
+
animation: sfx-tb-spin 0.8s linear infinite;
|
|
529
|
+
pointer-events: none;
|
|
530
|
+
}
|
|
531
|
+
@keyframes sfx-tb-spin {
|
|
532
|
+
to {
|
|
533
|
+
transform: rotate(360deg);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
`;
|
|
537
|
+
let r = O;
|
|
538
|
+
a([
|
|
539
|
+
n({ attribute: "base-url" })
|
|
540
|
+
], r.prototype, "baseUrl");
|
|
541
|
+
a([
|
|
542
|
+
n()
|
|
543
|
+
], r.prototype, "token");
|
|
544
|
+
a([
|
|
545
|
+
n({ attribute: "sass-key" })
|
|
546
|
+
], r.prototype, "sassKey");
|
|
547
|
+
a([
|
|
548
|
+
n({ attribute: "session-uuid" })
|
|
549
|
+
], r.prototype, "sessionUuid");
|
|
550
|
+
a([
|
|
551
|
+
n({ attribute: "sec-template" })
|
|
552
|
+
], r.prototype, "secTemplate");
|
|
553
|
+
a([
|
|
554
|
+
n({ attribute: "company-uuid" })
|
|
555
|
+
], r.prototype, "companyUuid");
|
|
556
|
+
a([
|
|
557
|
+
n({ attribute: "project-uuid" })
|
|
558
|
+
], r.prototype, "projectUuid");
|
|
559
|
+
a([
|
|
560
|
+
n({ attribute: "template-id" })
|
|
561
|
+
], r.prototype, "templateId");
|
|
562
|
+
a([
|
|
563
|
+
n({ reflect: !0 })
|
|
564
|
+
], r.prototype, "mode");
|
|
565
|
+
a([
|
|
566
|
+
n({ type: Boolean, reflect: !0 })
|
|
567
|
+
], r.prototype, "stateless");
|
|
568
|
+
a([
|
|
569
|
+
n({ attribute: !1 })
|
|
570
|
+
], r.prototype, "content");
|
|
571
|
+
a([
|
|
572
|
+
n({ type: Boolean, attribute: "new-template" })
|
|
573
|
+
], r.prototype, "newTemplate");
|
|
574
|
+
a([
|
|
575
|
+
n({ attribute: "template-name" })
|
|
576
|
+
], r.prototype, "templateName");
|
|
577
|
+
a([
|
|
578
|
+
n({ attribute: "template-query" })
|
|
579
|
+
], r.prototype, "templateQuery");
|
|
580
|
+
a([
|
|
581
|
+
n({ attribute: "brand-color" })
|
|
582
|
+
], r.prototype, "brandColor");
|
|
583
|
+
a([
|
|
584
|
+
n()
|
|
585
|
+
], r.prototype, "theme");
|
|
586
|
+
a([
|
|
587
|
+
n({
|
|
588
|
+
attribute: "custom-metadata",
|
|
589
|
+
converter: {
|
|
590
|
+
fromAttribute: (s) => {
|
|
591
|
+
if (!s) return [];
|
|
592
|
+
try {
|
|
593
|
+
const e = JSON.parse(s);
|
|
594
|
+
return Array.isArray(e) ? e : [];
|
|
595
|
+
} catch {
|
|
596
|
+
return console.warn("[sfx-template-builder] custom-metadata is not valid JSON — ignoring."), [];
|
|
597
|
+
}
|
|
598
|
+
},
|
|
599
|
+
toAttribute: (s) => JSON.stringify(s ?? [])
|
|
600
|
+
}
|
|
601
|
+
})
|
|
602
|
+
], r.prototype, "customMetadata");
|
|
603
|
+
a([
|
|
604
|
+
n({ attribute: "custom-metadata-label" })
|
|
605
|
+
], r.prototype, "customMetadataLabel");
|
|
606
|
+
a([
|
|
607
|
+
n({ type: Boolean, attribute: "dam-store" })
|
|
608
|
+
], r.prototype, "damStore");
|
|
609
|
+
a([
|
|
610
|
+
n({ attribute: "store-folder" })
|
|
611
|
+
], r.prototype, "storeFolder");
|
|
612
|
+
a([
|
|
613
|
+
n({ attribute: "stored-uuid" })
|
|
614
|
+
], r.prototype, "storedUuid");
|
|
615
|
+
a([
|
|
616
|
+
n({ type: Number, attribute: "ready-timeout" })
|
|
617
|
+
], r.prototype, "readyTimeout");
|
|
618
|
+
a([
|
|
619
|
+
y()
|
|
620
|
+
], r.prototype, "_status");
|
|
621
|
+
a([
|
|
622
|
+
y()
|
|
623
|
+
], r.prototype, "_open");
|
|
624
|
+
a([
|
|
625
|
+
y()
|
|
626
|
+
], r.prototype, "_src");
|
|
627
|
+
a([
|
|
628
|
+
y()
|
|
629
|
+
], r.prototype, "_isDirty");
|
|
630
|
+
export {
|
|
631
|
+
le as A,
|
|
632
|
+
Q as B,
|
|
633
|
+
c as E,
|
|
634
|
+
J as H,
|
|
635
|
+
de as P,
|
|
636
|
+
r as S,
|
|
637
|
+
ce as a,
|
|
638
|
+
$ as b,
|
|
639
|
+
j as c,
|
|
640
|
+
x as d,
|
|
641
|
+
q as e,
|
|
642
|
+
F as f,
|
|
643
|
+
K as g,
|
|
644
|
+
P as h,
|
|
645
|
+
B as i,
|
|
646
|
+
G as j,
|
|
647
|
+
H as k,
|
|
648
|
+
X as l,
|
|
649
|
+
Y as m
|
|
650
|
+
};
|
|
651
|
+
//# sourceMappingURL=template-builder-B9Cwo_Q-.js.map
|