@lightning-out/lwc-shell 2.2.0-rc.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 +137 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.esm.js +791 -0
- package/dist/index.iife.js +801 -0
- package/dist/index.iife.prod.js +3 -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 +151 -0
- package/package.json +45 -0
- package/scripts/templates/vendor-banner.js +2 -0
- package/scripts/templates/vendor-meta.xml +5 -0
- package/scripts/vendor-build.mjs +76 -0
|
@@ -0,0 +1,801 @@
|
|
|
1
|
+
/*! @lightning-out/lwc-shell v2.2.0-rc.1 (2026-02-27) */
|
|
2
|
+
var LwcShell = (function (exports) {
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* EmbeddingResizer - Handles dynamic iframe/container resizing
|
|
7
|
+
* Uses ResizeObserver to monitor element size changes and notify the host
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Generates a pseudo-random alphanumeric identifier string for unique
|
|
11
|
+
* element IDs or temporary identifiers (not cryptographically secure).
|
|
12
|
+
*
|
|
13
|
+
* @returns Random alphanumeric string
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* getUUID(); // 'k2j8f5l9m'
|
|
17
|
+
*/
|
|
18
|
+
function getUUID() {
|
|
19
|
+
return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(36);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Symbols used as property / method keys for the dirty-state contract.
|
|
24
|
+
*
|
|
25
|
+
* These symbols are attached to the host LWC component by `<lwc-shell>`
|
|
26
|
+
* (via the `hostComponent` setter) so that Salesforce's tab-close flow can
|
|
27
|
+
* invoke `DoCloseConfirmation` and identify the component by `Guid`.
|
|
28
|
+
*
|
|
29
|
+
* See `InternalHostLwcShell.hostComponent` setter for wiring details,
|
|
30
|
+
* and the `SelfManagedDirtyComponent` object in `dirtyStateModal.ts` for
|
|
31
|
+
* the full contract description.
|
|
32
|
+
*/
|
|
33
|
+
const Guid = Symbol("Guid");
|
|
34
|
+
const DoCloseConfirmation = Symbol("DoCloseConfirmation");
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Symbol-keyed contract for components that self-manage their dirty state.
|
|
38
|
+
*
|
|
39
|
+
* - `DoCloseConfirmation` — The component **must** define this as a function
|
|
40
|
+
* that returns a `Promise` resolving to one of:
|
|
41
|
+
* - `'SUCCESS'` – dirty state resolved, tab closure should continue.
|
|
42
|
+
* - `'CANCEL'` – the user cancelled the operation.
|
|
43
|
+
* - `'ERROR'` – a problem occurred resolving the dirty state.
|
|
44
|
+
*
|
|
45
|
+
* If the promise resolves to `'SUCCESS'`, the component **must** have
|
|
46
|
+
* already dispatched a `'privatelightningselfmanageddirtystatechanged'`
|
|
47
|
+
* event with `isUnsaved: false` to clear its dirty state.
|
|
48
|
+
*
|
|
49
|
+
* - `Guid` — Optionally, the component can define this as a property
|
|
50
|
+
* containing a globally unique string. If undefined, a GUID will be
|
|
51
|
+
* generated automatically.
|
|
52
|
+
*/
|
|
53
|
+
const SelfManagedDirtyComponent = {
|
|
54
|
+
DoCloseConfirmation,
|
|
55
|
+
Guid,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* InternalHostLwcShell
|
|
60
|
+
*
|
|
61
|
+
* A standard Web Component (custom element) that embeds an iframe-based widget
|
|
62
|
+
* with bridge communication, sandbox management, fullscreen support, and
|
|
63
|
+
* dirty-state tracking.
|
|
64
|
+
*
|
|
65
|
+
* Registered as `<lwc-shell>`.
|
|
66
|
+
*
|
|
67
|
+
* Ported from the LWC `widgetContainerLo` component (formerly known as
|
|
68
|
+
* WidgetContainer) — all Lightning Web Component framework dependencies have
|
|
69
|
+
* been removed so this can run as a plain custom element inside any LWC host.
|
|
70
|
+
*
|
|
71
|
+
* **How customers use this:**
|
|
72
|
+
* 1. The build produces `dist/index.esm.js` which bundles this class.
|
|
73
|
+
* 2. Customers copy that file into their SFDX project as an LWC entity
|
|
74
|
+
* (e.g. `c/lwcShell`) and deploy it to their Salesforce org.
|
|
75
|
+
* 3. A wrapper LWC imports `'c/lwcShell'` and creates `<lwc-shell>`
|
|
76
|
+
* imperatively via `document.createElement('lwc-shell')`.
|
|
77
|
+
*
|
|
78
|
+
* See the README and the `productRegistrationWidgetLo` demo for a full recipe.
|
|
79
|
+
*/
|
|
80
|
+
const BASE_TOKENS = ["allow-scripts", "allow-pointer-lock"];
|
|
81
|
+
const OPTIONAL_TOKENS = ["allow-downloads", "allow-forms", "allow-modals"];
|
|
82
|
+
const BLOCKED_TOKENS = ["allow-same-origin", "allow-top-navigation", "allow-popups"];
|
|
83
|
+
const STATES = {
|
|
84
|
+
LOADING: "state-loading",
|
|
85
|
+
LOADED: "state-loaded",
|
|
86
|
+
};
|
|
87
|
+
const STYLES = /* css */ `
|
|
88
|
+
:host {
|
|
89
|
+
display: block;
|
|
90
|
+
position: relative;
|
|
91
|
+
min-height: 100px;
|
|
92
|
+
border: 1px solid #ddd;
|
|
93
|
+
border-radius: 4px;
|
|
94
|
+
overflow: hidden;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.container {
|
|
98
|
+
width: 100%;
|
|
99
|
+
height: 100%;
|
|
100
|
+
position: relative;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.frame {
|
|
104
|
+
visibility: hidden;
|
|
105
|
+
display: block;
|
|
106
|
+
width: 100%;
|
|
107
|
+
height: 100%;
|
|
108
|
+
border: none;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.container[data-state='state-loaded'] .frame {
|
|
112
|
+
visibility: visible;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* Fullscreen overlay */
|
|
116
|
+
.overlayBackdrop {
|
|
117
|
+
position: fixed;
|
|
118
|
+
inset: 0;
|
|
119
|
+
width: 100vw;
|
|
120
|
+
height: 100vh;
|
|
121
|
+
z-index: 9998;
|
|
122
|
+
background: rgba(0, 0, 0, 0.8);
|
|
123
|
+
backdrop-filter: blur(4px);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.overlayClose {
|
|
127
|
+
position: fixed;
|
|
128
|
+
top: 24px;
|
|
129
|
+
right: 24px;
|
|
130
|
+
z-index: 9999;
|
|
131
|
+
width: 32px;
|
|
132
|
+
height: 32px;
|
|
133
|
+
background: rgba(0, 0, 0, 0.7);
|
|
134
|
+
color: #fff;
|
|
135
|
+
border-radius: 50%;
|
|
136
|
+
border: none;
|
|
137
|
+
display: flex;
|
|
138
|
+
align-items: center;
|
|
139
|
+
justify-content: center;
|
|
140
|
+
font-size: 16px;
|
|
141
|
+
font-weight: bold;
|
|
142
|
+
cursor: pointer;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.frameFull {
|
|
146
|
+
position: fixed;
|
|
147
|
+
inset: 0;
|
|
148
|
+
width: 90vw;
|
|
149
|
+
height: 90vh !important;
|
|
150
|
+
margin: 5vh 5vw;
|
|
151
|
+
z-index: 10000;
|
|
152
|
+
background: #fff;
|
|
153
|
+
border-radius: 8px;
|
|
154
|
+
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
|
|
155
|
+
}
|
|
156
|
+
`;
|
|
157
|
+
class InternalHostLwcShell extends HTMLElement {
|
|
158
|
+
_shadow;
|
|
159
|
+
_iframe = null;
|
|
160
|
+
_container = null;
|
|
161
|
+
_currentState = STATES.LOADING;
|
|
162
|
+
_hasExplicitResize = false;
|
|
163
|
+
_readinessTimeout = null;
|
|
164
|
+
_isFullscreen = false;
|
|
165
|
+
_lastThemeData = {};
|
|
166
|
+
_lastPayloadData = {};
|
|
167
|
+
_bridgeReady = false;
|
|
168
|
+
_hasSentTheme = false;
|
|
169
|
+
_hasSentData = false;
|
|
170
|
+
_src = null;
|
|
171
|
+
_srcdoc = null;
|
|
172
|
+
_sandbox = null;
|
|
173
|
+
_title = "Embedded widget";
|
|
174
|
+
_view = "compact";
|
|
175
|
+
_dirtyComponentGuid = getUUID();
|
|
176
|
+
_beforeUnloadHandler = null;
|
|
177
|
+
_dirtyStateModal = null;
|
|
178
|
+
_pendingDirtyActions = new Map();
|
|
179
|
+
_debugEnabled = true;
|
|
180
|
+
static get observedAttributes() {
|
|
181
|
+
return ["src", "srcdoc", "sandbox", "title", "view", "debug"];
|
|
182
|
+
}
|
|
183
|
+
constructor() {
|
|
184
|
+
super();
|
|
185
|
+
this._shadow = this.attachShadow({ mode: "closed" });
|
|
186
|
+
}
|
|
187
|
+
connectedCallback() {
|
|
188
|
+
this._renderInitial();
|
|
189
|
+
this._setupMessageListener();
|
|
190
|
+
this._log("connectedCallback");
|
|
191
|
+
}
|
|
192
|
+
disconnectedCallback() {
|
|
193
|
+
window.removeEventListener("message", this._handleMessage);
|
|
194
|
+
if (this._readinessTimeout) {
|
|
195
|
+
clearTimeout(this._readinessTimeout);
|
|
196
|
+
this._readinessTimeout = null;
|
|
197
|
+
}
|
|
198
|
+
this._log("disconnectedCallback");
|
|
199
|
+
}
|
|
200
|
+
attributeChangedCallback(name, oldVal, newVal) {
|
|
201
|
+
if (oldVal === newVal)
|
|
202
|
+
return;
|
|
203
|
+
switch (name) {
|
|
204
|
+
case "src":
|
|
205
|
+
this.src = newVal;
|
|
206
|
+
break;
|
|
207
|
+
case "srcdoc":
|
|
208
|
+
this.srcdoc = newVal;
|
|
209
|
+
break;
|
|
210
|
+
case "sandbox":
|
|
211
|
+
this.sandbox = newVal;
|
|
212
|
+
break;
|
|
213
|
+
case "title":
|
|
214
|
+
this.title = newVal;
|
|
215
|
+
break;
|
|
216
|
+
case "view":
|
|
217
|
+
this.view = newVal;
|
|
218
|
+
break;
|
|
219
|
+
case "debug":
|
|
220
|
+
this.debug = newVal !== null;
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
get src() {
|
|
225
|
+
return this._src;
|
|
226
|
+
}
|
|
227
|
+
set src(v) {
|
|
228
|
+
const val = v || null;
|
|
229
|
+
if (this._src === val)
|
|
230
|
+
return;
|
|
231
|
+
this._src = val;
|
|
232
|
+
if (val !== null) {
|
|
233
|
+
this.setAttribute("src", val);
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
this.removeAttribute("src");
|
|
237
|
+
}
|
|
238
|
+
this._updateIframeSrc();
|
|
239
|
+
}
|
|
240
|
+
get srcdoc() {
|
|
241
|
+
return this._srcdoc;
|
|
242
|
+
}
|
|
243
|
+
set srcdoc(v) {
|
|
244
|
+
const val = v || null;
|
|
245
|
+
if (this._srcdoc === val)
|
|
246
|
+
return;
|
|
247
|
+
this._srcdoc = val;
|
|
248
|
+
if (val !== null) {
|
|
249
|
+
this.setAttribute("srcdoc", val);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
this.removeAttribute("srcdoc");
|
|
253
|
+
}
|
|
254
|
+
this._updateIframeSrc();
|
|
255
|
+
}
|
|
256
|
+
get sandbox() {
|
|
257
|
+
return this._sandbox;
|
|
258
|
+
}
|
|
259
|
+
set sandbox(v) {
|
|
260
|
+
const val = v || null;
|
|
261
|
+
if (this._sandbox === val)
|
|
262
|
+
return;
|
|
263
|
+
this._sandbox = val;
|
|
264
|
+
if (val !== null) {
|
|
265
|
+
this.setAttribute("sandbox", val);
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
this.removeAttribute("sandbox");
|
|
269
|
+
}
|
|
270
|
+
this._applySandbox();
|
|
271
|
+
}
|
|
272
|
+
get title() {
|
|
273
|
+
return this._title;
|
|
274
|
+
}
|
|
275
|
+
set title(v) {
|
|
276
|
+
const val = v || "Embedded widget";
|
|
277
|
+
if (this._title === val)
|
|
278
|
+
return;
|
|
279
|
+
this._title = val;
|
|
280
|
+
this.setAttribute("title", val);
|
|
281
|
+
this._updateTitle();
|
|
282
|
+
}
|
|
283
|
+
get view() {
|
|
284
|
+
return this._view;
|
|
285
|
+
}
|
|
286
|
+
set view(v) {
|
|
287
|
+
const val = v === "full" ? "full" : "compact";
|
|
288
|
+
if (this._view === val)
|
|
289
|
+
return;
|
|
290
|
+
this._view = val;
|
|
291
|
+
this.setAttribute("view", val);
|
|
292
|
+
this._updateViewDOM();
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Controls debug logging. Toggle via:
|
|
296
|
+
* - HTML attribute: `<lwc-shell debug>`
|
|
297
|
+
* - Programmatically: `shell.debug = true`
|
|
298
|
+
*/
|
|
299
|
+
get debug() {
|
|
300
|
+
return this._debugEnabled;
|
|
301
|
+
}
|
|
302
|
+
set debug(v) {
|
|
303
|
+
const val = !!v;
|
|
304
|
+
if (this._debugEnabled === val)
|
|
305
|
+
return;
|
|
306
|
+
this._debugEnabled = val;
|
|
307
|
+
if (val) {
|
|
308
|
+
this.setAttribute("debug", "");
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
this.removeAttribute("debug");
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
get _SelfManagedDirtyComponent() {
|
|
315
|
+
return SelfManagedDirtyComponent;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* The LightningModal class used to show the unsaved-changes confirmation.
|
|
319
|
+
* Wrapper LWC sets this via: `shell.dirtyStateModal = DirtyStateModal;`
|
|
320
|
+
* where `DirtyStateModal` is the class imported from `'c/dirtyStateModal'`.
|
|
321
|
+
*/
|
|
322
|
+
get dirtyStateModal() {
|
|
323
|
+
return this._dirtyStateModal;
|
|
324
|
+
}
|
|
325
|
+
set dirtyStateModal(v) {
|
|
326
|
+
this._dirtyStateModal = v || null;
|
|
327
|
+
this._log("dirtyStateModal set", !!v);
|
|
328
|
+
}
|
|
329
|
+
updateData(newData) {
|
|
330
|
+
if (!newData || typeof newData !== "object")
|
|
331
|
+
return;
|
|
332
|
+
Object.entries(newData).forEach(([key, value]) => {
|
|
333
|
+
const dataAttr = `data-${String(key).replace(/[A-Z]/g, "-$&").toLowerCase()}`;
|
|
334
|
+
this.setAttribute(dataAttr, String(value));
|
|
335
|
+
});
|
|
336
|
+
const payload = this._collectDataAttributes();
|
|
337
|
+
this._lastPayloadData = payload;
|
|
338
|
+
if (this._bridgeReady) {
|
|
339
|
+
this._postToIframe("data", payload);
|
|
340
|
+
this._hasSentData = true;
|
|
341
|
+
this._log("send data", payload);
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
this._log("queue data until bridge ready", payload);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
refreshTheme() {
|
|
348
|
+
this._sendInitialTheme();
|
|
349
|
+
}
|
|
350
|
+
get _isFullView() {
|
|
351
|
+
return this._view === "full";
|
|
352
|
+
}
|
|
353
|
+
get _frameClass() {
|
|
354
|
+
return this._isFullView ? "frame frameFull" : "frame";
|
|
355
|
+
}
|
|
356
|
+
_log(...args) {
|
|
357
|
+
if (this._debugEnabled) {
|
|
358
|
+
// eslint-disable-next-line no-console
|
|
359
|
+
console.log("[InternalHostLwcShell]", JSON.stringify(args, null, 2));
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
_renderInitial() {
|
|
363
|
+
const shadow = this._shadow;
|
|
364
|
+
shadow.innerHTML = `
|
|
365
|
+
<style>${STYLES}</style>
|
|
366
|
+
<div class="container" data-state="${this._currentState}"
|
|
367
|
+
tabindex="0" role="region" aria-label="${this._title}">
|
|
368
|
+
<iframe
|
|
369
|
+
class="${this._frameClass}"
|
|
370
|
+
title="${this._title}"
|
|
371
|
+
aria-label="Interactive widget content"
|
|
372
|
+
></iframe>
|
|
373
|
+
</div>
|
|
374
|
+
`;
|
|
375
|
+
this._container = shadow.querySelector(".container");
|
|
376
|
+
this._iframe = shadow.querySelector("iframe");
|
|
377
|
+
this._container.addEventListener("click", () => this._handleContainerClick());
|
|
378
|
+
this._applySandbox();
|
|
379
|
+
this._updateIframeSrc();
|
|
380
|
+
this._sendInitialTheme();
|
|
381
|
+
this._log("renderInitial: iframe ready");
|
|
382
|
+
}
|
|
383
|
+
_updateContainerState() {
|
|
384
|
+
if (this._container) {
|
|
385
|
+
this._container.setAttribute("data-state", this._currentState);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
_updateTitle() {
|
|
389
|
+
if (this._iframe) {
|
|
390
|
+
this._iframe.setAttribute("title", this._title);
|
|
391
|
+
}
|
|
392
|
+
if (this._container) {
|
|
393
|
+
this._container.setAttribute("aria-label", this._title);
|
|
394
|
+
}
|
|
395
|
+
this._log("title", this._title);
|
|
396
|
+
}
|
|
397
|
+
_updateViewDOM() {
|
|
398
|
+
if (this._iframe) {
|
|
399
|
+
this._iframe.className = this._frameClass;
|
|
400
|
+
}
|
|
401
|
+
if (!this._container)
|
|
402
|
+
return;
|
|
403
|
+
const existingBackdrop = this._container.querySelector(".overlayBackdrop");
|
|
404
|
+
const existingClose = this._container.querySelector(".overlayClose");
|
|
405
|
+
if (existingBackdrop)
|
|
406
|
+
existingBackdrop.remove();
|
|
407
|
+
if (existingClose)
|
|
408
|
+
existingClose.remove();
|
|
409
|
+
if (this._isFullView) {
|
|
410
|
+
const backdrop = document.createElement("div");
|
|
411
|
+
backdrop.className = "overlayBackdrop";
|
|
412
|
+
backdrop.setAttribute("aria-hidden", "true");
|
|
413
|
+
const closeBtn = document.createElement("button");
|
|
414
|
+
closeBtn.className = "overlayClose";
|
|
415
|
+
closeBtn.type = "button";
|
|
416
|
+
closeBtn.setAttribute("aria-label", "Close fullscreen");
|
|
417
|
+
closeBtn.textContent = "\u2715";
|
|
418
|
+
closeBtn.addEventListener("click", this._exitFullscreen);
|
|
419
|
+
this._container.appendChild(backdrop);
|
|
420
|
+
this._container.appendChild(closeBtn);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
_setState(newState) {
|
|
424
|
+
this._currentState = newState;
|
|
425
|
+
this._updateContainerState();
|
|
426
|
+
if (newState === STATES.LOADED) {
|
|
427
|
+
this._sendInitialData();
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
_setupMessageListener() {
|
|
431
|
+
window.addEventListener("message", this._handleMessage);
|
|
432
|
+
}
|
|
433
|
+
_handleMessage = (event) => {
|
|
434
|
+
const sourceWin = this._iframe?.contentWindow;
|
|
435
|
+
if (event.source !== sourceWin) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
const payload = event.data || {};
|
|
439
|
+
const { type, data } = payload;
|
|
440
|
+
this._log("receive", type, data);
|
|
441
|
+
if (type === "bridge-event") {
|
|
442
|
+
const { eventType, detail } = data || {};
|
|
443
|
+
this._log("bridge-event", eventType, detail);
|
|
444
|
+
if (eventType === "resize") {
|
|
445
|
+
this._hasExplicitResize = true;
|
|
446
|
+
this._handleResize(detail);
|
|
447
|
+
}
|
|
448
|
+
else if (eventType === "widget-ready") {
|
|
449
|
+
this._handleWidgetReady();
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
throw new RangeError(`Invalid bridge event ${eventType}`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
else if (type === "custom-event") {
|
|
456
|
+
const { eventType, detail } = data || {};
|
|
457
|
+
this._log("custom-event", eventType, detail);
|
|
458
|
+
if (eventType === "fullscreen-request") {
|
|
459
|
+
const shouldRunDefault = this.dispatchEvent(new CustomEvent(eventType, {
|
|
460
|
+
detail,
|
|
461
|
+
bubbles: true,
|
|
462
|
+
cancelable: true,
|
|
463
|
+
}));
|
|
464
|
+
if (shouldRunDefault) {
|
|
465
|
+
this._handleFullscreenRequest();
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
else if (eventType === "mfe:dirty-state-detected") {
|
|
469
|
+
this._handleDirtyStateDetected();
|
|
470
|
+
}
|
|
471
|
+
else if (eventType === "mfe:dirty-state-reset") {
|
|
472
|
+
this._handleDirtyStateReset();
|
|
473
|
+
}
|
|
474
|
+
else if (eventType === "mfe:dirty-state-modal-handle-status") {
|
|
475
|
+
this._handleDirtyStateModalStatus(detail);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
else if (type === "bridge-ready") {
|
|
479
|
+
this._handleBridgeReady();
|
|
480
|
+
}
|
|
481
|
+
else if (type === "bridge-error") {
|
|
482
|
+
this._handleBridgeError(data);
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
_dispatchSelfManagedDirtyEvent(isUnsaved) {
|
|
486
|
+
const evt = new CustomEvent("privatelightningselfmanageddirtystatechanged", {
|
|
487
|
+
bubbles: true,
|
|
488
|
+
composed: true,
|
|
489
|
+
cancelable: true,
|
|
490
|
+
detail: {
|
|
491
|
+
isUnsaved,
|
|
492
|
+
unsavedComponent: this,
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
this.dispatchEvent(evt);
|
|
496
|
+
}
|
|
497
|
+
_toggleBeforeUnload(enable) {
|
|
498
|
+
if (enable) {
|
|
499
|
+
if (!this._beforeUnloadHandler) {
|
|
500
|
+
this._beforeUnloadHandler = (e) => {
|
|
501
|
+
e.preventDefault();
|
|
502
|
+
// Chrome requires returnValue to be set
|
|
503
|
+
e.returnValue = "";
|
|
504
|
+
return "";
|
|
505
|
+
};
|
|
506
|
+
window.addEventListener("beforeunload", this._beforeUnloadHandler);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
else if (this._beforeUnloadHandler) {
|
|
510
|
+
window.removeEventListener("beforeunload", this._beforeUnloadHandler);
|
|
511
|
+
this._beforeUnloadHandler = null;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
_handleDirtyStateDetected() {
|
|
515
|
+
this._toggleBeforeUnload(true);
|
|
516
|
+
this._dispatchSelfManagedDirtyEvent(true);
|
|
517
|
+
}
|
|
518
|
+
_handleDirtyStateReset() {
|
|
519
|
+
this._toggleBeforeUnload(false);
|
|
520
|
+
this._dispatchSelfManagedDirtyEvent(false);
|
|
521
|
+
}
|
|
522
|
+
_handleDirtyStateModalStatus(detail) {
|
|
523
|
+
const { correlationId, isSuccess } = detail || {};
|
|
524
|
+
const handlers = this._pendingDirtyActions.get(correlationId);
|
|
525
|
+
if (!handlers) {
|
|
526
|
+
this._log("No pending handler for correlationId", correlationId);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
this._pendingDirtyActions.delete(correlationId);
|
|
530
|
+
handlers.resolve(isSuccess);
|
|
531
|
+
}
|
|
532
|
+
_handleResize({ height }) {
|
|
533
|
+
const evt = new CustomEvent("resize", {
|
|
534
|
+
detail: { height },
|
|
535
|
+
cancelable: true,
|
|
536
|
+
});
|
|
537
|
+
this.dispatchEvent(evt);
|
|
538
|
+
if (!evt.defaultPrevented && !this._isFullscreen && typeof height === "number" && Number.isFinite(height)) {
|
|
539
|
+
if (this._iframe)
|
|
540
|
+
this._iframe.style.height = height + "px";
|
|
541
|
+
this._log("applied resize", height);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
_handleWidgetReady() {
|
|
545
|
+
if (this._readinessTimeout) {
|
|
546
|
+
clearTimeout(this._readinessTimeout);
|
|
547
|
+
this._readinessTimeout = null;
|
|
548
|
+
}
|
|
549
|
+
this.dispatchEvent(new CustomEvent("widget-ready", { bubbles: true }));
|
|
550
|
+
this._log("widget-ready");
|
|
551
|
+
}
|
|
552
|
+
_handleFullscreenRequest() {
|
|
553
|
+
if (!this._isFullscreen) {
|
|
554
|
+
this._enterFullscreen();
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
_enterFullscreen() {
|
|
558
|
+
this._isFullscreen = true;
|
|
559
|
+
this._view = "full";
|
|
560
|
+
this._updateViewDOM();
|
|
561
|
+
if (this._iframe)
|
|
562
|
+
this._iframe.style.height = "100%";
|
|
563
|
+
this.updateData({ view: "full" });
|
|
564
|
+
this.dispatchEvent(new CustomEvent("fullscreen-entered", {
|
|
565
|
+
detail: { element: this },
|
|
566
|
+
bubbles: true,
|
|
567
|
+
}));
|
|
568
|
+
this._log("enter fullscreen");
|
|
569
|
+
}
|
|
570
|
+
_exitFullscreen = () => {
|
|
571
|
+
this._isFullscreen = false;
|
|
572
|
+
this._view = "compact";
|
|
573
|
+
this._updateViewDOM();
|
|
574
|
+
if (this._iframe)
|
|
575
|
+
this._iframe.style.height = "";
|
|
576
|
+
this.updateData({ view: "compact" });
|
|
577
|
+
this.dispatchEvent(new CustomEvent("fullscreen-exited", {
|
|
578
|
+
detail: { element: this },
|
|
579
|
+
bubbles: true,
|
|
580
|
+
}));
|
|
581
|
+
this._log("exit fullscreen");
|
|
582
|
+
};
|
|
583
|
+
_handleBridgeReady() {
|
|
584
|
+
this._bridgeReady = true;
|
|
585
|
+
this._setState(STATES.LOADED);
|
|
586
|
+
this._log("bridge-ready");
|
|
587
|
+
}
|
|
588
|
+
_handleBridgeError(errorData) {
|
|
589
|
+
this.dispatchEvent(new CustomEvent("widget-bridge-error", { detail: errorData }));
|
|
590
|
+
this._log("bridge-error", errorData);
|
|
591
|
+
}
|
|
592
|
+
_handleContainerClick() {
|
|
593
|
+
// placeholder
|
|
594
|
+
}
|
|
595
|
+
_applySandbox() {
|
|
596
|
+
const frame = this._iframe;
|
|
597
|
+
if (!frame)
|
|
598
|
+
return;
|
|
599
|
+
const tokens = this._computeSandboxTokens();
|
|
600
|
+
frame.setAttribute("sandbox", tokens);
|
|
601
|
+
this._log("sandbox", tokens);
|
|
602
|
+
}
|
|
603
|
+
_computeSandboxTokens() {
|
|
604
|
+
const tokens = [...BASE_TOKENS];
|
|
605
|
+
if (this._sandbox) {
|
|
606
|
+
const requested = String(this._sandbox).split(/\s+/).filter(Boolean);
|
|
607
|
+
requested.forEach((t) => {
|
|
608
|
+
if (OPTIONAL_TOKENS.includes(t) && !tokens.includes(t))
|
|
609
|
+
tokens.push(t);
|
|
610
|
+
if (BLOCKED_TOKENS.includes(t)) {
|
|
611
|
+
this.dispatchEvent(new CustomEvent("security-violation", {
|
|
612
|
+
detail: {
|
|
613
|
+
type: "blocked-sandbox-token",
|
|
614
|
+
token: t,
|
|
615
|
+
element: this,
|
|
616
|
+
},
|
|
617
|
+
}));
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
return tokens.join(" ");
|
|
622
|
+
}
|
|
623
|
+
_updateIframeSrc() {
|
|
624
|
+
const frame = this._iframe;
|
|
625
|
+
if (!frame)
|
|
626
|
+
return;
|
|
627
|
+
// reset tracking
|
|
628
|
+
this._hasExplicitResize = false;
|
|
629
|
+
this._currentState = STATES.LOADING;
|
|
630
|
+
this._updateContainerState();
|
|
631
|
+
// clear existing
|
|
632
|
+
frame.removeAttribute("src");
|
|
633
|
+
frame.removeAttribute("srcdoc");
|
|
634
|
+
if (this._src) {
|
|
635
|
+
frame.setAttribute("src", this._src);
|
|
636
|
+
}
|
|
637
|
+
else if (this._srcdoc) {
|
|
638
|
+
try {
|
|
639
|
+
frame.setAttribute("srcdoc", this._srcdoc);
|
|
640
|
+
}
|
|
641
|
+
catch {
|
|
642
|
+
frame.setAttribute("src", "about:blank");
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
// load events
|
|
646
|
+
frame.onload = this._handleIframeLoad;
|
|
647
|
+
frame.onerror = this._handleIframeError;
|
|
648
|
+
this._log("updateIframeSrc", this._src ? "src" : this._srcdoc ? "srcdoc" : "blank");
|
|
649
|
+
}
|
|
650
|
+
_handleIframeLoad = () => {
|
|
651
|
+
this.dispatchEvent(new CustomEvent("iframe-loaded", {
|
|
652
|
+
detail: { element: this },
|
|
653
|
+
bubbles: true,
|
|
654
|
+
}));
|
|
655
|
+
this._log("iframe-loaded");
|
|
656
|
+
this._readinessTimeout = setTimeout(() => {
|
|
657
|
+
if (this._currentState !== STATES.LOADED) {
|
|
658
|
+
this.dispatchEvent(new CustomEvent("widget-readiness-warning", {
|
|
659
|
+
detail: {
|
|
660
|
+
element: this,
|
|
661
|
+
message: "Widget may not be using ChatBridge for readiness signaling",
|
|
662
|
+
},
|
|
663
|
+
bubbles: true,
|
|
664
|
+
}));
|
|
665
|
+
this._log("widget-readiness-warning");
|
|
666
|
+
}
|
|
667
|
+
}, 3000);
|
|
668
|
+
};
|
|
669
|
+
_handleIframeError = (error) => {
|
|
670
|
+
this.dispatchEvent(new CustomEvent("widget-error", {
|
|
671
|
+
detail: { error, element: this },
|
|
672
|
+
}));
|
|
673
|
+
};
|
|
674
|
+
_postToIframe(type, data) {
|
|
675
|
+
const frame = this._iframe;
|
|
676
|
+
const win = frame && frame.contentWindow;
|
|
677
|
+
if (!win)
|
|
678
|
+
return;
|
|
679
|
+
try {
|
|
680
|
+
const msgType = `salesforce-${type}`;
|
|
681
|
+
win.postMessage("BRIDGE-JSON:" + JSON.stringify({ type: msgType, data }), "*");
|
|
682
|
+
this._log("postMessage", msgType, data);
|
|
683
|
+
}
|
|
684
|
+
catch (e) {
|
|
685
|
+
this._log("postMessage error", e instanceof Error ? e.message : String(e));
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
_collectDataAttributes() {
|
|
689
|
+
const out = {};
|
|
690
|
+
for (let i = 0; i < this.attributes.length; i++) {
|
|
691
|
+
const a = this.attributes[i];
|
|
692
|
+
if (a.name.startsWith("data-")) {
|
|
693
|
+
const raw = a.name.replace(/^data-/, "");
|
|
694
|
+
// convert kebab-case to camelCase
|
|
695
|
+
const camel = raw.replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
|
|
696
|
+
out[camel] = a.value;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return out;
|
|
700
|
+
}
|
|
701
|
+
_sendInitialTheme() {
|
|
702
|
+
const computed = getComputedStyle(this);
|
|
703
|
+
const theme = {};
|
|
704
|
+
for (let i = 0; i < computed.length; i++) {
|
|
705
|
+
const name = computed[i];
|
|
706
|
+
if (name.startsWith("--")) {
|
|
707
|
+
const val = computed.getPropertyValue(name).trim();
|
|
708
|
+
if (val)
|
|
709
|
+
theme[name] = val;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
this._lastThemeData = theme;
|
|
713
|
+
if (this._bridgeReady) {
|
|
714
|
+
this._postToIframe("theme", theme);
|
|
715
|
+
this._hasSentTheme = true;
|
|
716
|
+
this._log("send theme", theme);
|
|
717
|
+
}
|
|
718
|
+
else {
|
|
719
|
+
this._log("queue theme until bridge ready", theme);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
_sendInitialData() {
|
|
723
|
+
if (this._lastThemeData && Object.keys(this._lastThemeData).length) {
|
|
724
|
+
this._postToIframe("theme", this._lastThemeData);
|
|
725
|
+
this._hasSentTheme = true;
|
|
726
|
+
this._log("send theme (initial)", this._lastThemeData);
|
|
727
|
+
}
|
|
728
|
+
const payload = this._collectDataAttributes();
|
|
729
|
+
this._lastPayloadData = { ...payload };
|
|
730
|
+
this._postToIframe("data", this._lastPayloadData);
|
|
731
|
+
this._hasSentData = true;
|
|
732
|
+
this._log("send data (initial)", this._lastPayloadData);
|
|
733
|
+
this._postToIframe("bridge-ready");
|
|
734
|
+
}
|
|
735
|
+
[Guid] = this._dirtyComponentGuid;
|
|
736
|
+
[DoCloseConfirmation]() {
|
|
737
|
+
if (!this._dirtyStateModal) {
|
|
738
|
+
this._log("DoCloseConfirmation: no modal configured, auto-resolving SUCCESS");
|
|
739
|
+
return Promise.resolve("SUCCESS");
|
|
740
|
+
}
|
|
741
|
+
if (typeof this._dirtyStateModal.open !== "function") {
|
|
742
|
+
this._log("DoCloseConfirmation: modal.open is not a function, auto-resolving SUCCESS");
|
|
743
|
+
return Promise.resolve("SUCCESS");
|
|
744
|
+
}
|
|
745
|
+
const dirtStateModalConfig = {
|
|
746
|
+
modalFields: {
|
|
747
|
+
label: `Unsaved Changes in ${this._title}`,
|
|
748
|
+
description: "You have unsaved changes. If you leave, your changes will be lost.",
|
|
749
|
+
buttons: [
|
|
750
|
+
{
|
|
751
|
+
buttonKey: "cancel",
|
|
752
|
+
buttonLabel: "Cancel",
|
|
753
|
+
},
|
|
754
|
+
{
|
|
755
|
+
buttonKey: "discard",
|
|
756
|
+
buttonLabel: "Discard Changes",
|
|
757
|
+
buttonVariant: "brand",
|
|
758
|
+
},
|
|
759
|
+
],
|
|
760
|
+
},
|
|
761
|
+
size: "small",
|
|
762
|
+
};
|
|
763
|
+
return this._dirtyStateModal
|
|
764
|
+
.open(dirtStateModalConfig)
|
|
765
|
+
.then((buttonKey) => {
|
|
766
|
+
if (!buttonKey) {
|
|
767
|
+
return "CANCEL";
|
|
768
|
+
}
|
|
769
|
+
if (buttonKey === "discard") {
|
|
770
|
+
this._dispatchSelfManagedDirtyEvent(false);
|
|
771
|
+
return "SUCCESS";
|
|
772
|
+
}
|
|
773
|
+
else if (buttonKey === "cancel") {
|
|
774
|
+
return "CANCEL";
|
|
775
|
+
}
|
|
776
|
+
else {
|
|
777
|
+
return "ERROR";
|
|
778
|
+
}
|
|
779
|
+
})
|
|
780
|
+
.catch((err) => {
|
|
781
|
+
this._log("DoCloseConfirmation: error", err);
|
|
782
|
+
return "ERROR";
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
// ---------------------------------------------------------------------------
|
|
787
|
+
// Register the custom element
|
|
788
|
+
// ---------------------------------------------------------------------------
|
|
789
|
+
if (!window.customElements.get("lwc-shell")) {
|
|
790
|
+
window.customElements.define("lwc-shell", InternalHostLwcShell);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
exports.InternalHostLwcShell = InternalHostLwcShell;
|
|
794
|
+
exports.default = InternalHostLwcShell;
|
|
795
|
+
|
|
796
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
797
|
+
|
|
798
|
+
return exports;
|
|
799
|
+
|
|
800
|
+
})({});
|
|
801
|
+
//# sourceMappingURL=index.iife.js.map
|