@salesforce/experimental-mfe-lwc-shell 2.2.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/README.md +27 -0
- package/dist/InternalHostLwcShell.d.ts +98 -0
- package/dist/graphqlHandler.d.ts +5 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.esm.js +702 -0
- package/dist/index.iife.js +710 -0
- package/dist/index.iife.prod.js +5 -0
- package/dist/lwc/dirtyStateModal/dirtyStateModal.html +33 -0
- package/dist/lwc/dirtyStateModal/dirtyStateModal.js +82 -0
- package/dist/lwc/dirtyStateModal/dirtyStateModal.js-meta.xml +5 -0
- package/dist/utils/dirtyStateModal.d.ts +23 -0
- package/dist/utils/dirtyStateModal.d.ts.map +1 -0
- package/dist/utils/index.d.ts +13 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/symbols.d.ts +18 -0
- package/dist/utils/symbols.d.ts.map +1 -0
- package/index.d.ts +97 -0
- package/package.json +42 -0
- package/scripts/templates/vendor-banner.js +2 -0
- package/scripts/templates/vendor-meta.xml +5 -0
- package/scripts/vendor-build.mjs +37 -0
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
/*! @salesforce/experimental-mfe-lwc-shell v2.2.0-rc.2 (2026-03-20) */
|
|
2
|
+
import { gql, unstable_graphql_imperative } from 'lightning/graphql';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* EmbeddingResizer - Handles dynamic iframe/container resizing
|
|
6
|
+
* Uses ResizeObserver to monitor element size changes and notify the host
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Generates a pseudo-random alphanumeric identifier string for unique
|
|
10
|
+
* element IDs or temporary identifiers (not cryptographically secure).
|
|
11
|
+
*
|
|
12
|
+
* @returns Random alphanumeric string
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* getUUID(); // 'k2j8f5l9m'
|
|
16
|
+
*/
|
|
17
|
+
function getUUID() {
|
|
18
|
+
return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(36);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function executeGraphQL(query, variables) {
|
|
22
|
+
if (typeof query !== "string" || query.trim() === "") {
|
|
23
|
+
throw new Error("Invalid GraphQL query: query must be a non-empty string.");
|
|
24
|
+
}
|
|
25
|
+
const documentNode = gql `
|
|
26
|
+
${query}
|
|
27
|
+
`;
|
|
28
|
+
const result = await unstable_graphql_imperative({ query: documentNode, variables });
|
|
29
|
+
return { data: result?.data, errors: result?.errors };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* InternalHostLwcShell
|
|
34
|
+
*
|
|
35
|
+
* A standard Web Component (custom element) that embeds an iframe-based widget
|
|
36
|
+
* with bridge communication, sandbox management, and fullscreen support.
|
|
37
|
+
*
|
|
38
|
+
* Registered as `<lwc-shell>`.
|
|
39
|
+
*
|
|
40
|
+
* Dirty-state tracking is handled via a simple `trackdirtystate`
|
|
41
|
+
* custom-event flow: the embedded widget dispatches `trackdirtystate`
|
|
42
|
+
* with `{ isDirty, instanceId, label }`, and this shell re-dispatches
|
|
43
|
+
* the event for the host LWC to observe.
|
|
44
|
+
*
|
|
45
|
+
* **How customers use this:**
|
|
46
|
+
* 1. The build produces `dist/index.esm.js` which bundles this class.
|
|
47
|
+
* 2. Customers copy that file into their SFDX project as an LWC entity
|
|
48
|
+
* (e.g. `c/lwcShell`) and deploy it to their Salesforce org.
|
|
49
|
+
* 3. A wrapper LWC imports `'c/lwcShell'` and creates `<lwc-shell>`
|
|
50
|
+
* imperatively via `document.createElement('lwc-shell')`.
|
|
51
|
+
*
|
|
52
|
+
* See the README and the `productRegistration` demo for a full recipe.
|
|
53
|
+
*/
|
|
54
|
+
const BASE_TOKENS = ["allow-scripts", "allow-pointer-lock"];
|
|
55
|
+
const OPTIONAL_TOKENS = ["allow-downloads", "allow-forms", "allow-modals"];
|
|
56
|
+
const BLOCKED_TOKENS = ["allow-same-origin", "allow-top-navigation", "allow-popups"];
|
|
57
|
+
const STATES = {
|
|
58
|
+
LOADING: "state-loading",
|
|
59
|
+
LOADED: "state-loaded",
|
|
60
|
+
};
|
|
61
|
+
const STYLES = /* css */ `
|
|
62
|
+
:host {
|
|
63
|
+
display: block;
|
|
64
|
+
position: relative;
|
|
65
|
+
height: 100%;
|
|
66
|
+
overflow: auto;
|
|
67
|
+
box-sizing: border-box;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.container {
|
|
71
|
+
width: 100%;
|
|
72
|
+
height: 100%;
|
|
73
|
+
position: relative;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.frame {
|
|
77
|
+
visibility: hidden;
|
|
78
|
+
display: block;
|
|
79
|
+
width: 100%;
|
|
80
|
+
height: 100%;
|
|
81
|
+
border: none;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.container[data-state='state-loaded'] .frame {
|
|
85
|
+
visibility: visible;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* Fullscreen overlay */
|
|
89
|
+
.overlayBackdrop {
|
|
90
|
+
position: fixed;
|
|
91
|
+
inset: 0;
|
|
92
|
+
width: 100vw;
|
|
93
|
+
height: 100vh;
|
|
94
|
+
z-index: 9998;
|
|
95
|
+
background: rgba(0, 0, 0, 0.8);
|
|
96
|
+
backdrop-filter: blur(4px);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.overlayClose {
|
|
100
|
+
position: fixed;
|
|
101
|
+
top: 24px;
|
|
102
|
+
right: 24px;
|
|
103
|
+
z-index: 9999;
|
|
104
|
+
width: 32px;
|
|
105
|
+
height: 32px;
|
|
106
|
+
background: rgba(0, 0, 0, 0.7);
|
|
107
|
+
color: #fff;
|
|
108
|
+
border-radius: 50%;
|
|
109
|
+
border: none;
|
|
110
|
+
display: flex;
|
|
111
|
+
align-items: center;
|
|
112
|
+
justify-content: center;
|
|
113
|
+
font-size: 16px;
|
|
114
|
+
font-weight: bold;
|
|
115
|
+
cursor: pointer;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.frameFull {
|
|
119
|
+
position: fixed;
|
|
120
|
+
inset: 0;
|
|
121
|
+
width: 90vw;
|
|
122
|
+
height: 90vh !important;
|
|
123
|
+
margin: 5vh 5vw;
|
|
124
|
+
z-index: 10000;
|
|
125
|
+
background: #fff;
|
|
126
|
+
border-radius: 8px;
|
|
127
|
+
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
|
|
128
|
+
}
|
|
129
|
+
`;
|
|
130
|
+
class InternalHostLwcShell extends HTMLElement {
|
|
131
|
+
_shadow;
|
|
132
|
+
_iframe = null;
|
|
133
|
+
_container = null;
|
|
134
|
+
_currentState = STATES.LOADING;
|
|
135
|
+
_readinessTimeout = null;
|
|
136
|
+
_isFullscreen = false;
|
|
137
|
+
_preFullscreenHeight = "";
|
|
138
|
+
_lastThemeData = {};
|
|
139
|
+
_lastPayloadData = {};
|
|
140
|
+
_bridgeReady = false;
|
|
141
|
+
_shellInstanceId = getUUID();
|
|
142
|
+
_hasSentTheme = false;
|
|
143
|
+
_hasSentData = false;
|
|
144
|
+
_src = null;
|
|
145
|
+
_srcdoc = null;
|
|
146
|
+
_sandbox = null;
|
|
147
|
+
_title = "Embedded widget";
|
|
148
|
+
_view = "compact";
|
|
149
|
+
_debugEnabled = true;
|
|
150
|
+
_pendingGraphQLCount = 0;
|
|
151
|
+
static MAX_CONCURRENT_GRAPHQL = 10;
|
|
152
|
+
static get observedAttributes() {
|
|
153
|
+
return ["src", "srcdoc", "sandbox", "title", "view", "debug"];
|
|
154
|
+
}
|
|
155
|
+
constructor() {
|
|
156
|
+
super();
|
|
157
|
+
this._shadow = this.attachShadow({ mode: "closed" });
|
|
158
|
+
}
|
|
159
|
+
connectedCallback() {
|
|
160
|
+
this._renderInitial();
|
|
161
|
+
this._setupMessageListener();
|
|
162
|
+
this._log("connectedCallback");
|
|
163
|
+
}
|
|
164
|
+
disconnectedCallback() {
|
|
165
|
+
window.removeEventListener("message", this._handleMessage);
|
|
166
|
+
if (this._readinessTimeout) {
|
|
167
|
+
clearTimeout(this._readinessTimeout);
|
|
168
|
+
this._readinessTimeout = null;
|
|
169
|
+
}
|
|
170
|
+
this._log("disconnectedCallback");
|
|
171
|
+
}
|
|
172
|
+
attributeChangedCallback(name, oldVal, newVal) {
|
|
173
|
+
if (oldVal === newVal)
|
|
174
|
+
return;
|
|
175
|
+
switch (name) {
|
|
176
|
+
case "src":
|
|
177
|
+
this.src = newVal;
|
|
178
|
+
break;
|
|
179
|
+
case "srcdoc":
|
|
180
|
+
this.srcdoc = newVal;
|
|
181
|
+
break;
|
|
182
|
+
case "sandbox":
|
|
183
|
+
this.sandbox = newVal;
|
|
184
|
+
break;
|
|
185
|
+
case "title":
|
|
186
|
+
this.title = newVal;
|
|
187
|
+
break;
|
|
188
|
+
case "view":
|
|
189
|
+
this.view = newVal;
|
|
190
|
+
break;
|
|
191
|
+
case "debug":
|
|
192
|
+
this.debug = newVal !== null;
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
get src() {
|
|
197
|
+
return this._src;
|
|
198
|
+
}
|
|
199
|
+
set src(v) {
|
|
200
|
+
const val = v || null;
|
|
201
|
+
if (this._src === val)
|
|
202
|
+
return;
|
|
203
|
+
this._src = val;
|
|
204
|
+
if (val !== null) {
|
|
205
|
+
this.setAttribute("src", val);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
this.removeAttribute("src");
|
|
209
|
+
}
|
|
210
|
+
this._updateIframeSrc();
|
|
211
|
+
}
|
|
212
|
+
get srcdoc() {
|
|
213
|
+
return this._srcdoc;
|
|
214
|
+
}
|
|
215
|
+
set srcdoc(v) {
|
|
216
|
+
const val = v || null;
|
|
217
|
+
if (this._srcdoc === val)
|
|
218
|
+
return;
|
|
219
|
+
this._srcdoc = val;
|
|
220
|
+
if (val !== null) {
|
|
221
|
+
this.setAttribute("srcdoc", val);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
this.removeAttribute("srcdoc");
|
|
225
|
+
}
|
|
226
|
+
this._updateIframeSrc();
|
|
227
|
+
}
|
|
228
|
+
get sandbox() {
|
|
229
|
+
return this._sandbox;
|
|
230
|
+
}
|
|
231
|
+
set sandbox(v) {
|
|
232
|
+
const val = v || null;
|
|
233
|
+
if (this._sandbox === val)
|
|
234
|
+
return;
|
|
235
|
+
this._sandbox = val;
|
|
236
|
+
if (val !== null) {
|
|
237
|
+
this.setAttribute("sandbox", val);
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
this.removeAttribute("sandbox");
|
|
241
|
+
}
|
|
242
|
+
this._applySandbox();
|
|
243
|
+
}
|
|
244
|
+
get title() {
|
|
245
|
+
return this._title;
|
|
246
|
+
}
|
|
247
|
+
set title(v) {
|
|
248
|
+
const val = v || "Embedded widget";
|
|
249
|
+
if (this._title === val)
|
|
250
|
+
return;
|
|
251
|
+
this._title = val;
|
|
252
|
+
this.setAttribute("title", val);
|
|
253
|
+
this._updateTitle();
|
|
254
|
+
}
|
|
255
|
+
get view() {
|
|
256
|
+
return this._view;
|
|
257
|
+
}
|
|
258
|
+
set view(v) {
|
|
259
|
+
const val = v === "full" ? "full" : "compact";
|
|
260
|
+
if (this._view === val)
|
|
261
|
+
return;
|
|
262
|
+
this._view = val;
|
|
263
|
+
this.setAttribute("view", val);
|
|
264
|
+
this._updateViewDOM();
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Controls debug logging. Toggle via:
|
|
268
|
+
* - HTML attribute: `<lwc-shell debug>`
|
|
269
|
+
* - Programmatically: `shell.debug = true`
|
|
270
|
+
*/
|
|
271
|
+
get debug() {
|
|
272
|
+
return this._debugEnabled;
|
|
273
|
+
}
|
|
274
|
+
set debug(v) {
|
|
275
|
+
const val = !!v;
|
|
276
|
+
if (this._debugEnabled === val)
|
|
277
|
+
return;
|
|
278
|
+
this._debugEnabled = val;
|
|
279
|
+
if (val) {
|
|
280
|
+
this.setAttribute("debug", "");
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
this.removeAttribute("debug");
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
updateData(newData) {
|
|
287
|
+
if (!newData || typeof newData !== "object")
|
|
288
|
+
return;
|
|
289
|
+
Object.entries(newData).forEach(([key, value]) => {
|
|
290
|
+
const dataAttr = `data-${String(key).replace(/[A-Z]/g, "-$&").toLowerCase()}`;
|
|
291
|
+
this.setAttribute(dataAttr, String(value));
|
|
292
|
+
});
|
|
293
|
+
const payload = this._collectDataAttributes();
|
|
294
|
+
this._lastPayloadData = payload;
|
|
295
|
+
if (this._bridgeReady) {
|
|
296
|
+
this._postToIframe("data", payload);
|
|
297
|
+
this._hasSentData = true;
|
|
298
|
+
this._log("send data", payload);
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
this._log("queue data until bridge ready", payload);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
refreshTheme() {
|
|
305
|
+
this._sendInitialTheme();
|
|
306
|
+
}
|
|
307
|
+
get _isFullView() {
|
|
308
|
+
return this._view === "full";
|
|
309
|
+
}
|
|
310
|
+
get _frameClass() {
|
|
311
|
+
return this._isFullView ? "frame frameFull" : "frame";
|
|
312
|
+
}
|
|
313
|
+
_log(...args) {
|
|
314
|
+
if (this._debugEnabled) {
|
|
315
|
+
// eslint-disable-next-line no-console
|
|
316
|
+
console.log("[InternalHostLwcShell]", JSON.stringify(args, null, 2));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
_renderInitial() {
|
|
320
|
+
const shadow = this._shadow;
|
|
321
|
+
shadow.innerHTML = `
|
|
322
|
+
<style>${STYLES}</style>
|
|
323
|
+
<div class="container" data-state="${this._currentState}"
|
|
324
|
+
tabindex="0" role="region" aria-label="${this._title}">
|
|
325
|
+
<iframe
|
|
326
|
+
class="${this._frameClass}"
|
|
327
|
+
title="${this._title}"
|
|
328
|
+
aria-label="Interactive widget content"
|
|
329
|
+
></iframe>
|
|
330
|
+
</div>
|
|
331
|
+
`;
|
|
332
|
+
this._container = shadow.querySelector(".container");
|
|
333
|
+
this._iframe = shadow.querySelector("iframe");
|
|
334
|
+
this._container.addEventListener("click", () => this._handleContainerClick());
|
|
335
|
+
this._applySandbox();
|
|
336
|
+
this._updateIframeSrc();
|
|
337
|
+
this._sendInitialTheme();
|
|
338
|
+
this._log("renderInitial: iframe ready");
|
|
339
|
+
}
|
|
340
|
+
_updateContainerState() {
|
|
341
|
+
if (this._container) {
|
|
342
|
+
this._container.setAttribute("data-state", this._currentState);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
_updateTitle() {
|
|
346
|
+
if (this._iframe) {
|
|
347
|
+
this._iframe.setAttribute("title", this._title);
|
|
348
|
+
}
|
|
349
|
+
if (this._container) {
|
|
350
|
+
this._container.setAttribute("aria-label", this._title);
|
|
351
|
+
}
|
|
352
|
+
this._log("title", this._title);
|
|
353
|
+
}
|
|
354
|
+
_updateViewDOM() {
|
|
355
|
+
if (this._iframe) {
|
|
356
|
+
this._iframe.className = this._frameClass;
|
|
357
|
+
}
|
|
358
|
+
if (!this._container)
|
|
359
|
+
return;
|
|
360
|
+
const existingBackdrop = this._container.querySelector(".overlayBackdrop");
|
|
361
|
+
const existingClose = this._container.querySelector(".overlayClose");
|
|
362
|
+
if (existingBackdrop)
|
|
363
|
+
existingBackdrop.remove();
|
|
364
|
+
if (existingClose)
|
|
365
|
+
existingClose.remove();
|
|
366
|
+
if (this._isFullView) {
|
|
367
|
+
const backdrop = document.createElement("div");
|
|
368
|
+
backdrop.className = "overlayBackdrop";
|
|
369
|
+
backdrop.setAttribute("aria-hidden", "true");
|
|
370
|
+
const closeBtn = document.createElement("button");
|
|
371
|
+
closeBtn.className = "overlayClose";
|
|
372
|
+
closeBtn.type = "button";
|
|
373
|
+
closeBtn.setAttribute("aria-label", "Close fullscreen");
|
|
374
|
+
closeBtn.textContent = "\u2715";
|
|
375
|
+
closeBtn.addEventListener("click", this._exitFullscreen);
|
|
376
|
+
this._container.appendChild(backdrop);
|
|
377
|
+
this._container.appendChild(closeBtn);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
_setState(newState) {
|
|
381
|
+
this._currentState = newState;
|
|
382
|
+
this._updateContainerState();
|
|
383
|
+
if (newState === STATES.LOADED) {
|
|
384
|
+
this._sendInitialData();
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
_setupMessageListener() {
|
|
388
|
+
window.addEventListener("message", this._handleMessage);
|
|
389
|
+
}
|
|
390
|
+
_handleMessage = (event) => {
|
|
391
|
+
const sourceWin = this._iframe?.contentWindow;
|
|
392
|
+
if (event.source !== sourceWin) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const payload = event.data || {};
|
|
396
|
+
const { type, data, id } = payload;
|
|
397
|
+
if (id ? id !== this._shellInstanceId : type !== "bridge-ready")
|
|
398
|
+
return;
|
|
399
|
+
this._log("receive", type, data);
|
|
400
|
+
if (type === "bridge-event") {
|
|
401
|
+
const { eventType, detail } = data || {};
|
|
402
|
+
this._log("bridge-event", eventType, detail);
|
|
403
|
+
if (eventType === "resize") {
|
|
404
|
+
this._handleResize(detail);
|
|
405
|
+
}
|
|
406
|
+
else if (eventType === "widget-ready") {
|
|
407
|
+
this._handleWidgetReady();
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
throw new RangeError(`Invalid bridge event ${eventType}`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
else if (type === "custom-event") {
|
|
414
|
+
const { eventType, detail } = data || {};
|
|
415
|
+
this._log("custom-event", eventType, detail);
|
|
416
|
+
if (eventType === "fullscreen-request") {
|
|
417
|
+
const shouldRunDefault = this.dispatchEvent(new CustomEvent(eventType, {
|
|
418
|
+
detail,
|
|
419
|
+
bubbles: true,
|
|
420
|
+
cancelable: true,
|
|
421
|
+
}));
|
|
422
|
+
if (shouldRunDefault) {
|
|
423
|
+
this._handleFullscreenRequest();
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
else if (eventType === "trackdirtystate") {
|
|
427
|
+
this.dispatchEvent(new CustomEvent(eventType, { detail, bubbles: true, cancelable: true, composed: true }));
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
else if (type === "bridge-graphql-request") {
|
|
431
|
+
this._handleGraphQLRequest(data);
|
|
432
|
+
}
|
|
433
|
+
else if (type === "bridge-ready") {
|
|
434
|
+
this._handleBridgeReady();
|
|
435
|
+
}
|
|
436
|
+
else if (type === "bridge-error") {
|
|
437
|
+
this._handleBridgeError(data);
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
_handleResize({ height }) {
|
|
441
|
+
const evt = new CustomEvent("resize", {
|
|
442
|
+
detail: { height },
|
|
443
|
+
cancelable: true,
|
|
444
|
+
});
|
|
445
|
+
this.dispatchEvent(evt);
|
|
446
|
+
if (!evt.defaultPrevented && !this._isFullscreen && typeof height === "number" && Number.isFinite(height)) {
|
|
447
|
+
if (this._iframe)
|
|
448
|
+
this._iframe.style.height = height + "px";
|
|
449
|
+
this._log("applied resize", height);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
_handleWidgetReady() {
|
|
453
|
+
if (this._readinessTimeout) {
|
|
454
|
+
clearTimeout(this._readinessTimeout);
|
|
455
|
+
this._readinessTimeout = null;
|
|
456
|
+
}
|
|
457
|
+
this.dispatchEvent(new CustomEvent("widget-ready", { bubbles: true }));
|
|
458
|
+
this._log("widget-ready");
|
|
459
|
+
}
|
|
460
|
+
_handleFullscreenRequest() {
|
|
461
|
+
if (!this._isFullscreen) {
|
|
462
|
+
this._enterFullscreen();
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
_enterFullscreen() {
|
|
466
|
+
this._isFullscreen = true;
|
|
467
|
+
this._preFullscreenHeight = this._iframe?.style.height ?? "";
|
|
468
|
+
this._view = "full";
|
|
469
|
+
this._updateViewDOM();
|
|
470
|
+
if (this._iframe)
|
|
471
|
+
this._iframe.style.height = "100%";
|
|
472
|
+
this.updateData({ view: "full" });
|
|
473
|
+
this.dispatchEvent(new CustomEvent("fullscreen-entered", {
|
|
474
|
+
detail: { element: this },
|
|
475
|
+
bubbles: true,
|
|
476
|
+
}));
|
|
477
|
+
this._log("enter fullscreen");
|
|
478
|
+
}
|
|
479
|
+
_exitFullscreen = () => {
|
|
480
|
+
this._isFullscreen = false;
|
|
481
|
+
this._view = "compact";
|
|
482
|
+
this._updateViewDOM();
|
|
483
|
+
if (this._iframe)
|
|
484
|
+
this._iframe.style.height = this._preFullscreenHeight;
|
|
485
|
+
this.updateData({ view: "compact" });
|
|
486
|
+
this.dispatchEvent(new CustomEvent("fullscreen-exited", {
|
|
487
|
+
detail: { element: this },
|
|
488
|
+
bubbles: true,
|
|
489
|
+
}));
|
|
490
|
+
this._log("exit fullscreen");
|
|
491
|
+
};
|
|
492
|
+
_handleBridgeReady() {
|
|
493
|
+
this._bridgeReady = true;
|
|
494
|
+
this._postToIframe("shell-ready");
|
|
495
|
+
this._setState(STATES.LOADED);
|
|
496
|
+
}
|
|
497
|
+
_handleBridgeError(errorData) {
|
|
498
|
+
this.dispatchEvent(new CustomEvent("widget-bridge-error", { detail: errorData }));
|
|
499
|
+
this._log("bridge-error", errorData);
|
|
500
|
+
}
|
|
501
|
+
async _handleGraphQLRequest(data) {
|
|
502
|
+
if (typeof data?.requestId !== "string") {
|
|
503
|
+
this._log("_handleGraphQLRequest: invalid request data", data);
|
|
504
|
+
this._postToIframe("bridge-graphql-response", {
|
|
505
|
+
requestId: data?.requestId,
|
|
506
|
+
ok: false,
|
|
507
|
+
error: { message: "Invalid GraphQL request data" },
|
|
508
|
+
});
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const { requestId, query, variables } = data;
|
|
512
|
+
if (typeof query !== "string") {
|
|
513
|
+
this._postToIframe("bridge-graphql-response", {
|
|
514
|
+
requestId,
|
|
515
|
+
ok: false,
|
|
516
|
+
error: { message: "Invalid GraphQL query: query must be a string" },
|
|
517
|
+
});
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (variables != null && (typeof variables !== "object" || Array.isArray(variables))) {
|
|
521
|
+
this._postToIframe("bridge-graphql-response", {
|
|
522
|
+
requestId,
|
|
523
|
+
ok: false,
|
|
524
|
+
error: { message: "Invalid GraphQL variables: must be a plain object" },
|
|
525
|
+
});
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (this._pendingGraphQLCount >= InternalHostLwcShell.MAX_CONCURRENT_GRAPHQL) {
|
|
529
|
+
this._postToIframe("bridge-graphql-response", {
|
|
530
|
+
requestId,
|
|
531
|
+
ok: false,
|
|
532
|
+
error: { message: "Too many concurrent GraphQL requests" },
|
|
533
|
+
});
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
this._pendingGraphQLCount++;
|
|
537
|
+
try {
|
|
538
|
+
const result = await executeGraphQL(query, variables);
|
|
539
|
+
this._postToIframe("bridge-graphql-response", { requestId, ok: true, result });
|
|
540
|
+
}
|
|
541
|
+
catch (err) {
|
|
542
|
+
this._postToIframe("bridge-graphql-response", {
|
|
543
|
+
requestId,
|
|
544
|
+
ok: false,
|
|
545
|
+
error: { message: err?.message || "GraphQL request failed" },
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
finally {
|
|
549
|
+
this._pendingGraphQLCount--;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
_handleContainerClick() {
|
|
553
|
+
// placeholder
|
|
554
|
+
}
|
|
555
|
+
_applySandbox() {
|
|
556
|
+
const frame = this._iframe;
|
|
557
|
+
if (!frame)
|
|
558
|
+
return;
|
|
559
|
+
const tokens = this._computeSandboxTokens();
|
|
560
|
+
frame.setAttribute("sandbox", tokens);
|
|
561
|
+
this._log("sandbox", tokens);
|
|
562
|
+
}
|
|
563
|
+
_computeSandboxTokens() {
|
|
564
|
+
const tokens = [...BASE_TOKENS];
|
|
565
|
+
if (this._sandbox) {
|
|
566
|
+
const requested = String(this._sandbox).split(/\s+/).filter(Boolean);
|
|
567
|
+
requested.forEach((t) => {
|
|
568
|
+
if (OPTIONAL_TOKENS.includes(t) && !tokens.includes(t))
|
|
569
|
+
tokens.push(t);
|
|
570
|
+
if (BLOCKED_TOKENS.includes(t)) {
|
|
571
|
+
this.dispatchEvent(new CustomEvent("security-violation", {
|
|
572
|
+
detail: {
|
|
573
|
+
type: "blocked-sandbox-token",
|
|
574
|
+
token: t,
|
|
575
|
+
element: this,
|
|
576
|
+
},
|
|
577
|
+
}));
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
return tokens.join(" ");
|
|
582
|
+
}
|
|
583
|
+
_updateIframeSrc() {
|
|
584
|
+
const frame = this._iframe;
|
|
585
|
+
if (!frame)
|
|
586
|
+
return;
|
|
587
|
+
// reset tracking
|
|
588
|
+
this._bridgeReady = false;
|
|
589
|
+
this._currentState = STATES.LOADING;
|
|
590
|
+
this._updateContainerState();
|
|
591
|
+
// clear existing
|
|
592
|
+
frame.removeAttribute("src");
|
|
593
|
+
frame.removeAttribute("srcdoc");
|
|
594
|
+
if (this._src) {
|
|
595
|
+
frame.setAttribute("src", this._src);
|
|
596
|
+
}
|
|
597
|
+
else if (this._srcdoc) {
|
|
598
|
+
try {
|
|
599
|
+
frame.setAttribute("srcdoc", this._srcdoc);
|
|
600
|
+
}
|
|
601
|
+
catch {
|
|
602
|
+
frame.setAttribute("src", "about:blank");
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
// load events
|
|
606
|
+
frame.onload = this._handleIframeLoad;
|
|
607
|
+
frame.onerror = this._handleIframeError;
|
|
608
|
+
this._log("updateIframeSrc", this._src ? "src" : this._srcdoc ? "srcdoc" : "blank");
|
|
609
|
+
}
|
|
610
|
+
_handleIframeLoad = () => {
|
|
611
|
+
this.dispatchEvent(new CustomEvent("iframe-loaded", {
|
|
612
|
+
detail: { element: this },
|
|
613
|
+
bubbles: true,
|
|
614
|
+
}));
|
|
615
|
+
this._log("iframe-loaded");
|
|
616
|
+
this._readinessTimeout = setTimeout(() => {
|
|
617
|
+
if (this._currentState !== STATES.LOADED) {
|
|
618
|
+
this.dispatchEvent(new CustomEvent("widget-readiness-warning", {
|
|
619
|
+
detail: {
|
|
620
|
+
element: this,
|
|
621
|
+
message: "Widget may not be using Bridge for readiness signaling",
|
|
622
|
+
},
|
|
623
|
+
bubbles: true,
|
|
624
|
+
}));
|
|
625
|
+
this._log("widget-readiness-warning");
|
|
626
|
+
}
|
|
627
|
+
}, 3000);
|
|
628
|
+
};
|
|
629
|
+
_handleIframeError = (error) => {
|
|
630
|
+
this.dispatchEvent(new CustomEvent("widget-error", {
|
|
631
|
+
detail: { error, element: this },
|
|
632
|
+
}));
|
|
633
|
+
};
|
|
634
|
+
_postToIframe(type, data) {
|
|
635
|
+
const win = this._iframe?.contentWindow;
|
|
636
|
+
if (!win)
|
|
637
|
+
return;
|
|
638
|
+
try {
|
|
639
|
+
const msgType = `salesforce-${type}`;
|
|
640
|
+
win.postMessage({ type: msgType, data, id: this._shellInstanceId }, "*");
|
|
641
|
+
this._log("postMessage", msgType, data);
|
|
642
|
+
}
|
|
643
|
+
catch (e) {
|
|
644
|
+
this._log("postMessage error", e instanceof Error ? e.message : String(e));
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
_collectDataAttributes() {
|
|
648
|
+
const out = {};
|
|
649
|
+
for (let i = 0; i < this.attributes.length; i++) {
|
|
650
|
+
const a = this.attributes[i];
|
|
651
|
+
if (a.name.startsWith("data-")) {
|
|
652
|
+
const raw = a.name.replace(/^data-/, "");
|
|
653
|
+
// convert kebab-case to camelCase
|
|
654
|
+
const camel = raw.replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
|
|
655
|
+
out[camel] = a.value;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return out;
|
|
659
|
+
}
|
|
660
|
+
_sendInitialTheme() {
|
|
661
|
+
const computed = getComputedStyle(this);
|
|
662
|
+
const theme = {};
|
|
663
|
+
for (let i = 0; i < computed.length; i++) {
|
|
664
|
+
const name = computed[i];
|
|
665
|
+
if (name.startsWith("--")) {
|
|
666
|
+
const val = computed.getPropertyValue(name).trim();
|
|
667
|
+
if (val)
|
|
668
|
+
theme[name] = val;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
this._lastThemeData = theme;
|
|
672
|
+
if (this._bridgeReady) {
|
|
673
|
+
this._postToIframe("theme", theme);
|
|
674
|
+
this._hasSentTheme = true;
|
|
675
|
+
this._log("send theme", theme);
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
this._log("queue theme until bridge ready", theme);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
_sendInitialData() {
|
|
682
|
+
if (this._lastThemeData && Object.keys(this._lastThemeData).length) {
|
|
683
|
+
this._postToIframe("theme", this._lastThemeData);
|
|
684
|
+
this._hasSentTheme = true;
|
|
685
|
+
this._log("send theme (initial)", this._lastThemeData);
|
|
686
|
+
}
|
|
687
|
+
const payload = this._collectDataAttributes();
|
|
688
|
+
this._lastPayloadData = { ...payload };
|
|
689
|
+
this._postToIframe("data", this._lastPayloadData);
|
|
690
|
+
this._hasSentData = true;
|
|
691
|
+
this._log("send data (initial)", this._lastPayloadData);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
// ---------------------------------------------------------------------------
|
|
695
|
+
// Register the custom element
|
|
696
|
+
// ---------------------------------------------------------------------------
|
|
697
|
+
if (!window.customElements.get("lwc-shell")) {
|
|
698
|
+
window.customElements.define("lwc-shell", InternalHostLwcShell);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
export { InternalHostLwcShell, InternalHostLwcShell as default };
|
|
702
|
+
//# sourceMappingURL=index.esm.js.map
|