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