@salesforce/lightning-out 2.2.0-rc.2
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/LICENSE.txt +21 -0
- package/README.md +64 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.esm.js +1717 -0
- package/dist/index.iife.debug.js +1724 -0
- package/dist/index.iife.prod.js +10 -0
- package/dist/index.js +9 -0
- package/package.json +39 -0
|
@@ -0,0 +1,1724 @@
|
|
|
1
|
+
/*! @salesforce/lightning-out v2.2.0-rc.2 (2026-03-18) */
|
|
2
|
+
var LO2 = (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
|
+
* Events fired by dispatchEvent
|
|
24
|
+
*/
|
|
25
|
+
const events = {
|
|
26
|
+
lo: {
|
|
27
|
+
// public
|
|
28
|
+
application: {
|
|
29
|
+
ready: "lo.application.ready",
|
|
30
|
+
error: "lo.application.error",
|
|
31
|
+
logout: "lo.application.logout",
|
|
32
|
+
auth: {
|
|
33
|
+
redirect: "lo.application.auth.redirect",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
// public
|
|
37
|
+
component: {
|
|
38
|
+
ready: "lo.component.ready",
|
|
39
|
+
error: "lo.component.error",
|
|
40
|
+
},
|
|
41
|
+
// internal
|
|
42
|
+
iframe: {
|
|
43
|
+
load: "lo.iframe.load",
|
|
44
|
+
error: "lo.iframe.error",
|
|
45
|
+
logout: "lo.iframe.logout",
|
|
46
|
+
auth: {
|
|
47
|
+
redirect: "lo.iframe.auth.redirect",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Messages sent by postMessage (all internal)
|
|
54
|
+
*/
|
|
55
|
+
const messages = {
|
|
56
|
+
lo: {
|
|
57
|
+
addEventListener: "lo.addEventListener",
|
|
58
|
+
dispatchEvent: "lo.dispatchEvent",
|
|
59
|
+
error: "lo.error",
|
|
60
|
+
getComponentData: "lo.getComponentData",
|
|
61
|
+
loaded: "lo.loaded",
|
|
62
|
+
logout: "lo.logout",
|
|
63
|
+
ready: "lo.ready",
|
|
64
|
+
redirect: "lo.redirect",
|
|
65
|
+
removeEventListener: "lo.removeEventListener",
|
|
66
|
+
setComponentData: "lo.setComponentData",
|
|
67
|
+
setComponentProps: "lo.setComponentProps",
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const logLevels = {
|
|
72
|
+
error: 0,
|
|
73
|
+
warn: 1,
|
|
74
|
+
info: 2,
|
|
75
|
+
debug: 3,
|
|
76
|
+
trace: 4,
|
|
77
|
+
};
|
|
78
|
+
class Logger {
|
|
79
|
+
static #prefix = "LO2";
|
|
80
|
+
static #level = "error";
|
|
81
|
+
#branding;
|
|
82
|
+
static set level(level) {
|
|
83
|
+
this.#level = level;
|
|
84
|
+
}
|
|
85
|
+
static set prefix(prefix) {
|
|
86
|
+
this.#prefix = prefix;
|
|
87
|
+
}
|
|
88
|
+
// This string appears first in the console, it can be used for filtering messages
|
|
89
|
+
get brand() {
|
|
90
|
+
return `${Logger.#prefix}:${this.#branding}:`;
|
|
91
|
+
}
|
|
92
|
+
constructor(branding) {
|
|
93
|
+
if (typeof branding === "string") {
|
|
94
|
+
this.#branding = branding;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
this.#branding = branding.constructor?.name;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
error(...args) {
|
|
101
|
+
if (logLevels.error <= logLevels[Logger.#level]) {
|
|
102
|
+
console.error(this.brand, ...args);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
warn(...args) {
|
|
106
|
+
if (logLevels.warn <= logLevels[Logger.#level]) {
|
|
107
|
+
console.warn(this.brand, ...args);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
info(...args) {
|
|
111
|
+
if (logLevels.info <= logLevels[Logger.#level]) {
|
|
112
|
+
console.info(this.brand, ...args);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
debug(...args) {
|
|
116
|
+
if (logLevels.debug <= logLevels[Logger.#level]) {
|
|
117
|
+
console.debug(this.brand, ...args);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
trace(...args) {
|
|
121
|
+
if (logLevels.trace <= logLevels[Logger.#level]) {
|
|
122
|
+
console.trace(this.brand, ...args);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Error class for Lightning Out
|
|
129
|
+
*/
|
|
130
|
+
const logger$4 = new Logger("LightningOutError");
|
|
131
|
+
class LightningOutError {
|
|
132
|
+
#eventTarget;
|
|
133
|
+
#branding;
|
|
134
|
+
constructor(branding) {
|
|
135
|
+
if (typeof branding === "string") {
|
|
136
|
+
this.#branding = branding;
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
this.#branding = branding.constructor?.name;
|
|
140
|
+
}
|
|
141
|
+
if (typeof branding.dispatchEvent === "function") {
|
|
142
|
+
this.#eventTarget = branding;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
#branded(message) {
|
|
146
|
+
return `${this.#branding}: ${message}`;
|
|
147
|
+
}
|
|
148
|
+
create(error) {
|
|
149
|
+
const message = typeof error === "string" ? error : error.message;
|
|
150
|
+
return new Error(this.#branded(message));
|
|
151
|
+
}
|
|
152
|
+
dispatch(type, error) {
|
|
153
|
+
const message = typeof error === "string" ? error : error.message || error.detail?.message;
|
|
154
|
+
if (this.#eventTarget) {
|
|
155
|
+
const detail = error.detail || {
|
|
156
|
+
message: this.#branded(message),
|
|
157
|
+
originalError: error,
|
|
158
|
+
};
|
|
159
|
+
const loErrorEvent = new CustomEvent(type, { detail });
|
|
160
|
+
this.#eventTarget.dispatchEvent(loErrorEvent);
|
|
161
|
+
logger$4.error(`${this.#branded("dispatched error")} -> ${type}: ${message}`);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
logger$4.error(`${this.#branded("unable to dispatch error on a non-EventTarget object")} -> ${type}: ${message}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Utility functions for Lightning Out
|
|
171
|
+
* @fileoverview Collection of utility functions used throughout the Lightning Out framework
|
|
172
|
+
* @author Salesforce ECS Team
|
|
173
|
+
* @version 2.1.0
|
|
174
|
+
*/
|
|
175
|
+
const loError = new LightningOutError("LightningOutUtils");
|
|
176
|
+
/**
|
|
177
|
+
* Converts HTML attribute/element names from kebab-case format to JavaScript property/component names in camelCase
|
|
178
|
+
* format.
|
|
179
|
+
*
|
|
180
|
+
* @param attrName - The kebab-case string to convert (should be all lowercase)
|
|
181
|
+
* @returns The camelCase string
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* kebabToCamel('data-attribute-name'); // 'dataAttributeName'
|
|
185
|
+
*/
|
|
186
|
+
function kebabToCamel(attrName) {
|
|
187
|
+
// Validate the input is all lowercase
|
|
188
|
+
if (/[A-Z]/.test(attrName)) {
|
|
189
|
+
throw loError.create(`kebabToCamel: "${attrName}" is not valid kebab-case - must be all lowercase.`);
|
|
190
|
+
}
|
|
191
|
+
// Standard kebab-case to camelCase conversion
|
|
192
|
+
return attrName.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Converts JavaScript property/component names from camelCase format to HTML attribute/element names in kebab-case
|
|
196
|
+
* format. This function performs pure transformation without validation.
|
|
197
|
+
*
|
|
198
|
+
* @param propName - The camelCase string to convert
|
|
199
|
+
* @returns The kebab-case string
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* camelToKebab('dataAttributeName'); // 'data-attribute-name'
|
|
203
|
+
*/
|
|
204
|
+
function camelToKebab(propName) {
|
|
205
|
+
// Standard camelCase to kebab-case conversion
|
|
206
|
+
return propName.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Converts JavaScript property names from camelCase format to snake_case format. Preserves existing underscores by
|
|
210
|
+
* escaping them as double underscores.
|
|
211
|
+
*
|
|
212
|
+
* @param propName - The camelCase string to convert
|
|
213
|
+
* @returns The snake_case string
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* camelToSnake('cwcMessaging'); // 'cwc_messaging'
|
|
217
|
+
* camelToSnake('my_variableName'); // 'my__variable_name'
|
|
218
|
+
*/
|
|
219
|
+
function camelToSnake(propName) {
|
|
220
|
+
return propName.replace(/([A-Z_])/g, "_$1").toLowerCase();
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Converts snake_case strings to camelCase format. Handles escaped double underscores by converting them back to single
|
|
224
|
+
* underscores.
|
|
225
|
+
*
|
|
226
|
+
* @param snakeName - The snake_case string to convert (should be all lowercase)
|
|
227
|
+
* @returns The camelCase string
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* snakeToCamel('cwc_messaging'); // 'cwcMessaging'
|
|
231
|
+
* snakeToCamel('my__variable_name'); // 'my_variableName'
|
|
232
|
+
*/
|
|
233
|
+
function snakeToCamel(snakeName) {
|
|
234
|
+
if (/[A-Z]/.test(snakeName)) {
|
|
235
|
+
throw loError.create(`snakeToCamel: "${snakeName}" is not valid snake_case - must be all lowercase.`);
|
|
236
|
+
}
|
|
237
|
+
return snakeName.replace(/_([a-z_])/g, (_, char) => char.toUpperCase());
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Transforms custom element names in kebab-name format (i.e. foo-bar) into standard Salesforce Lightning Web component
|
|
241
|
+
* names (i.e. foo/bar) for internal component resolution.
|
|
242
|
+
*
|
|
243
|
+
* @param elementName - The custom element tag name (must contain a hyphen and be all lowercase)
|
|
244
|
+
* @param isAura - Whether this is an Aura component
|
|
245
|
+
* @returns Lightning component name in namespace/componentName format (LWC) or namespace:componentName format (Aura)
|
|
246
|
+
* @throws LightningOutError if the element name input doesn't contain a hyphen or contains uppercase letters
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* elementNameToStandardName('c-my-component'); // 'c/myComponent'
|
|
250
|
+
* elementNameToStandardName('c-my-component', true); // 'c:myComponent'
|
|
251
|
+
*/
|
|
252
|
+
function elementNameToStandardName(elementName, isAura = false) {
|
|
253
|
+
// Validate the element name input is all lowercase
|
|
254
|
+
if (/[A-Z]/.test(elementName)) {
|
|
255
|
+
throw loError.create(`elementNameToStandardName: "${elementName}" is not a valid custom element name - must be all lowercase.`);
|
|
256
|
+
}
|
|
257
|
+
const nsSepIdx = elementName.indexOf("-");
|
|
258
|
+
if (nsSepIdx === -1) {
|
|
259
|
+
throw loError.create(`elementNameToStandardName: "${elementName}" is not a valid custom element name - missing hyphen character.`);
|
|
260
|
+
}
|
|
261
|
+
// Namespace part (before first hyphen) may contain underscores - convert to camelCase
|
|
262
|
+
const namespace = snakeToCamel(elementName.slice(0, nsSepIdx));
|
|
263
|
+
const name = kebabToCamel(elementName.slice(nsSepIdx + 1));
|
|
264
|
+
// Use colon separator for Aura, slash for LWC
|
|
265
|
+
const separator = isAura ? ":" : "/";
|
|
266
|
+
return `${namespace}${separator}${name}`;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Transforms standard Salesforce Lightning component names (i.e. foo/bar for LWC, foo:bar for Aura) into custom element
|
|
270
|
+
* names (i.e. foo-bar) for HTML usage.
|
|
271
|
+
*
|
|
272
|
+
* @param standardName - The Lightning component name
|
|
273
|
+
* @returns Custom element tag name in kebab-case format
|
|
274
|
+
* @throws LightningOutError if standard name doesn't contain a separator (/ or :), or if namespace contains hyphens
|
|
275
|
+
*
|
|
276
|
+
* @example
|
|
277
|
+
* standardNameToElementName('c/myComponent'); // 'c-my-component'
|
|
278
|
+
* standardNameToElementName('c:myComponent'); // 'c-my-component'
|
|
279
|
+
*/
|
|
280
|
+
function standardNameToElementName(standardName) {
|
|
281
|
+
// Detect namespace separator - slash for LWC, colon for Aura
|
|
282
|
+
const separator = standardName.includes("/") ? "/" : ":";
|
|
283
|
+
const sepIdx = standardName.indexOf(separator);
|
|
284
|
+
if (sepIdx === -1) {
|
|
285
|
+
throw loError.create(`standardNameToElementName: "${standardName}" is not a valid component name - missing namespace separator.`);
|
|
286
|
+
}
|
|
287
|
+
// Validate entire component name input must not contain hyphens
|
|
288
|
+
if (/-/.test(standardName)) {
|
|
289
|
+
throw loError.create(`standardNameToElementName: "${standardName}" is not a valid component name - must not contain hyphens.`);
|
|
290
|
+
}
|
|
291
|
+
// Validate first character is not uppercase
|
|
292
|
+
if (/^[A-Z]/.test(standardName)) {
|
|
293
|
+
throw loError.create(`standardNameToElementName: "${standardName}" is not a valid component name - first character must not be uppercase.`);
|
|
294
|
+
}
|
|
295
|
+
// Separate the namespace and component name inputs
|
|
296
|
+
const namespaceInput = standardName.slice(0, sepIdx);
|
|
297
|
+
const nameInput = standardName.slice(sepIdx + 1);
|
|
298
|
+
// Convert namespace input from camelCase to snake_case
|
|
299
|
+
const namespace = camelToSnake(namespaceInput);
|
|
300
|
+
// Convert component name input from camelCase to kebab-case
|
|
301
|
+
const name = camelToKebab(nameInput);
|
|
302
|
+
return `${namespace}-${name}`;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Checks if a name is in standard Lightning component format (contains '/' for LWC or ':' for Aura).
|
|
306
|
+
*
|
|
307
|
+
* @param name - The name to check
|
|
308
|
+
* @returns True if the name contains a forward slash or colon, false otherwise
|
|
309
|
+
*
|
|
310
|
+
* @example
|
|
311
|
+
* isStandardName('c/myComponent'); // true
|
|
312
|
+
* isStandardName('c:myComponent'); // true
|
|
313
|
+
* isStandardName('my-component'); // false
|
|
314
|
+
*/
|
|
315
|
+
function isStandardName(name) {
|
|
316
|
+
return name.includes("/") || name.includes(":");
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Applies property transformations and validation callbacks to component
|
|
320
|
+
* property changes before they are sent to Lightning components.
|
|
321
|
+
* Handles 'dataMirror' prefixes and invokes element-specific property callbacks.
|
|
322
|
+
*
|
|
323
|
+
* @param element - Element that may contain property change callbacks
|
|
324
|
+
* @param changes - Property changes to process
|
|
325
|
+
* @returns Processed property changes
|
|
326
|
+
*
|
|
327
|
+
* @example
|
|
328
|
+
* processPropertyChanges(element, { dataMirrorHeight: '100px' }); // { height: '100px' }
|
|
329
|
+
*/
|
|
330
|
+
function processPropertyChanges(element, changes) {
|
|
331
|
+
const processed = Object.entries(changes).map((entry) => {
|
|
332
|
+
// Extract the key name (already in camelCase) and it's value
|
|
333
|
+
let [key, value] = entry;
|
|
334
|
+
// Check if the key name has the special 'dataMirror' prefix, and if it does, use the suffix
|
|
335
|
+
// as the actual key name, i.e. 'dataMirrorHeight' becomes 'height'
|
|
336
|
+
const mirror = key.split("dataMirror");
|
|
337
|
+
if (mirror.length === 2 && mirror[0] === "") {
|
|
338
|
+
key = mirror[1].charAt(0).toLowerCase() + mirror[1].slice(1);
|
|
339
|
+
}
|
|
340
|
+
// Check if there is a _propertyChanged_ callback for the key, and if there is, call it to
|
|
341
|
+
// transform the value
|
|
342
|
+
const callbackName = `_propertyChanged_${key}`;
|
|
343
|
+
if (typeof element[callbackName] === "function") {
|
|
344
|
+
const callback = element[callbackName];
|
|
345
|
+
value = callback(value);
|
|
346
|
+
}
|
|
347
|
+
// Return the (possibly) transformed key name and value
|
|
348
|
+
return [key, value];
|
|
349
|
+
});
|
|
350
|
+
// This will remove duplicate key names from the object, if any, the last one prevails.
|
|
351
|
+
return Object.fromEntries(processed);
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Parses and structures URL information for debugging and logging, with
|
|
355
|
+
* specialized parsing for Salesforce authentication frontdoor URLs.
|
|
356
|
+
*
|
|
357
|
+
* @param url - URL object to analyze
|
|
358
|
+
* @returns Structured URL data with parsed parameters
|
|
359
|
+
*
|
|
360
|
+
* @example
|
|
361
|
+
* getUrlData(new URL('https://example.com/path?param=value'));
|
|
362
|
+
*/
|
|
363
|
+
function getUrlData(url) {
|
|
364
|
+
// This function is useful for debugging URLs, it understands frontdoor.jsp URLs
|
|
365
|
+
const urlData = {};
|
|
366
|
+
const paramsData = (params) => {
|
|
367
|
+
const paramsData = {};
|
|
368
|
+
for (const [key, value] of params.entries()) {
|
|
369
|
+
paramsData[key] = value;
|
|
370
|
+
}
|
|
371
|
+
return paramsData;
|
|
372
|
+
};
|
|
373
|
+
urlData.url = url.origin + url.pathname;
|
|
374
|
+
urlData.urlParams = paramsData(url.searchParams);
|
|
375
|
+
if (url.pathname === "/secur/frontdoor.jsp") {
|
|
376
|
+
const paramName = urlData.urlParams["otp"] ? "startURL" : "retURL";
|
|
377
|
+
const startURL = new URL(urlData.urlParams[paramName], "http://dummy.com");
|
|
378
|
+
urlData.urlParams[paramName] = {
|
|
379
|
+
url: startURL.pathname,
|
|
380
|
+
urlParams: paramsData(startURL.searchParams),
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
return urlData;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* IFrame manager for Lightning Out
|
|
388
|
+
*/
|
|
389
|
+
const logger$3 = new Logger("LightningOutIFrame");
|
|
390
|
+
class LightningOutIFrame {
|
|
391
|
+
#parentElement;
|
|
392
|
+
#isVisible;
|
|
393
|
+
#parentError;
|
|
394
|
+
#hiddenStyle = "display:none";
|
|
395
|
+
#visibleStyle = "border:0px; width:100%; height:100%; overflow:auto;";
|
|
396
|
+
#shadowRoot;
|
|
397
|
+
#element;
|
|
398
|
+
#window;
|
|
399
|
+
#origin;
|
|
400
|
+
#endpoint;
|
|
401
|
+
#timeoutID;
|
|
402
|
+
constructor(iframeConfig) {
|
|
403
|
+
this.#parentElement = iframeConfig.parentElement;
|
|
404
|
+
this.#isVisible = iframeConfig.isVisible;
|
|
405
|
+
// Dispatch errors to the iframe's parentElement
|
|
406
|
+
this.#parentError = new LightningOutError(iframeConfig.parentElement);
|
|
407
|
+
}
|
|
408
|
+
get iframeReady() {
|
|
409
|
+
return !!this.#window && !!this.#origin;
|
|
410
|
+
}
|
|
411
|
+
get iframeElement() {
|
|
412
|
+
return this.#element;
|
|
413
|
+
}
|
|
414
|
+
#config(window, origin) {
|
|
415
|
+
this.#window = window;
|
|
416
|
+
this.#origin = origin;
|
|
417
|
+
}
|
|
418
|
+
#messageListener = (event) => {
|
|
419
|
+
if (event.data.id !== this.#parentElement._uuid) {
|
|
420
|
+
// Ignore messages that are not meant to be received by the parent Custom Element
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
logger$3.debug("#messageListener:", `parentElement._uuid: ${this.#parentElement._uuid}`, `parentElement.localName: ${this.#parentElement.localName}`, JSON.stringify(event.data));
|
|
424
|
+
switch (event.data.type) {
|
|
425
|
+
case messages.lo.loaded: {
|
|
426
|
+
// Clear the timeout, cache its window and origin
|
|
427
|
+
this.#timeoutID = clearTimeout(this.#timeoutID);
|
|
428
|
+
this.#config(event.source, event.origin);
|
|
429
|
+
// Notify the parentElement that the iframe has successfully loaded. The origin where the iframe landed
|
|
430
|
+
// might be different than the origin used to load the frontdoor url, so notify that back.
|
|
431
|
+
this.#parentElement.dispatchEvent(new CustomEvent(events.lo.iframe.load, {
|
|
432
|
+
detail: event.origin,
|
|
433
|
+
}));
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
case messages.lo.logout: {
|
|
437
|
+
// Clear the timeout
|
|
438
|
+
this.#timeoutID = clearTimeout(this.#timeoutID);
|
|
439
|
+
// Notify the parentElement that the iframe loaded the logout page
|
|
440
|
+
this.#parentElement.dispatchEvent(new CustomEvent(events.lo.iframe.logout));
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
case messages.lo.redirect: {
|
|
444
|
+
// Clear the timeout
|
|
445
|
+
this.#timeoutID = clearTimeout(this.#timeoutID);
|
|
446
|
+
// Notify the parentElement that the iframe loaded with a redirection
|
|
447
|
+
this.#parentElement.dispatchEvent(new CustomEvent(events.lo.iframe.auth.redirect, {
|
|
448
|
+
detail: {
|
|
449
|
+
redirectUrl: event.data.redirectUrl,
|
|
450
|
+
redirectOrigin: event.origin,
|
|
451
|
+
},
|
|
452
|
+
}));
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
#init() {
|
|
458
|
+
if (!this.#element) {
|
|
459
|
+
const iframe = window.document.createElement("iframe");
|
|
460
|
+
iframe.name = "lightning_af";
|
|
461
|
+
// allow-top-navigation should only be enabled for UMA External Forms as Forms are first party and trusted.
|
|
462
|
+
// No custom code is enabled in External Forms scenario. When this assumption changed, this code needs to
|
|
463
|
+
// be reviewed and updated.
|
|
464
|
+
iframe.setAttribute("sandbox", [
|
|
465
|
+
"allow-downloads",
|
|
466
|
+
"allow-forms",
|
|
467
|
+
"allow-popups",
|
|
468
|
+
"allow-same-origin",
|
|
469
|
+
"allow-scripts",
|
|
470
|
+
"allow-top-navigation-by-user-activation",
|
|
471
|
+
].join(" "));
|
|
472
|
+
iframe.style.cssText = this.#isVisible ? this.#visibleStyle : this.#hiddenStyle;
|
|
473
|
+
this.#element = iframe;
|
|
474
|
+
this.#shadowRoot = this.#parentElement.attachShadow({
|
|
475
|
+
mode: "closed",
|
|
476
|
+
});
|
|
477
|
+
this.#shadowRoot.appendChild(this.#element);
|
|
478
|
+
// Note: In WebKit browsers (Safari/Chrome), the iframe's `onload` event
|
|
479
|
+
// will be triggered twice if the `onload` handler is attached **before**
|
|
480
|
+
// the iframe is appended to the DOM.
|
|
481
|
+
iframe.addEventListener("load", this.#loaded);
|
|
482
|
+
window.addEventListener("message", this.#messageListener);
|
|
483
|
+
}
|
|
484
|
+
return this.#element;
|
|
485
|
+
}
|
|
486
|
+
load(endpoint) {
|
|
487
|
+
const iframe = this.#init();
|
|
488
|
+
this.#endpoint = new URL(endpoint);
|
|
489
|
+
logger$3.debug(`#loadIframe: endpoint =`, getUrlData(this.#endpoint));
|
|
490
|
+
this.#config(undefined, undefined);
|
|
491
|
+
if (this.#isVisible) {
|
|
492
|
+
iframe.src = endpoint;
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
// This is to help us debug issues with hidden iframes. The target page should call postMessage like this:
|
|
496
|
+
// (window.opener ?? window.parent).postMessage();
|
|
497
|
+
if (localStorage.getItem("LightningOutIFrame:load:window.open")) {
|
|
498
|
+
window.open(endpoint, `LO2 Hidden ${this.#parentElement._uuid}`, "left=200,top=200,width=800,height=800");
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
iframe.src = endpoint;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
#loaded = () => {
|
|
506
|
+
// Browsers will call the onload event even when there is an error, so if the iframe
|
|
507
|
+
// is not ready when the timeout expires, there was an error but we don't know
|
|
508
|
+
// exactly what.
|
|
509
|
+
const timeout = 60 * 1000; // It should take less than this to receive the "lo.loaded" message
|
|
510
|
+
if (this.#timeoutID) {
|
|
511
|
+
clearTimeout(this.#timeoutID);
|
|
512
|
+
}
|
|
513
|
+
this.#timeoutID = setTimeout(() => {
|
|
514
|
+
if (!this.iframeReady) {
|
|
515
|
+
const message = "Error: Unknown error, unable to load the iframe.";
|
|
516
|
+
this.#parentError.dispatch(events.lo.iframe.error, message);
|
|
517
|
+
this.#showErrorMessage(message);
|
|
518
|
+
}
|
|
519
|
+
}, timeout);
|
|
520
|
+
};
|
|
521
|
+
destroy() {
|
|
522
|
+
if (this.#shadowRoot) {
|
|
523
|
+
this.#shadowRoot.innerHTML = "";
|
|
524
|
+
}
|
|
525
|
+
if (this.#element) {
|
|
526
|
+
this.#element.remove();
|
|
527
|
+
}
|
|
528
|
+
this.#shadowRoot = undefined;
|
|
529
|
+
this.#element = undefined;
|
|
530
|
+
this.#config(undefined, undefined);
|
|
531
|
+
}
|
|
532
|
+
#showErrorMessage(message) {
|
|
533
|
+
if (this.#isVisible && this.#endpoint) {
|
|
534
|
+
const url = new URL("/lightning/lightning.out.message.html", this.#endpoint.origin);
|
|
535
|
+
url.search = new URLSearchParams({
|
|
536
|
+
loAppOrigin: window.location.origin,
|
|
537
|
+
parentElementId: this.#parentElement._uuid,
|
|
538
|
+
message: message,
|
|
539
|
+
}).toString();
|
|
540
|
+
this.load(url.href);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
postMessage(message) {
|
|
544
|
+
if (this.#window && this.#origin) {
|
|
545
|
+
logger$3.debug("postMessage:", `parentElement: ${this.#parentElement._uuid}`, JSON.stringify(message));
|
|
546
|
+
try {
|
|
547
|
+
this.#window.postMessage(message, this.#origin);
|
|
548
|
+
}
|
|
549
|
+
catch (err) {
|
|
550
|
+
const message = `postMessage error: ${err}`;
|
|
551
|
+
this.#parentError.dispatch(events.lo.iframe.error, message);
|
|
552
|
+
throw this.#parentError.create(message);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
throw this.#parentError.create("Error attempting to postMessage on an iframe that is not ready.");
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* @file property-observer.ts
|
|
563
|
+
* @author Caridy Patiño (2025)
|
|
564
|
+
* @license MIT
|
|
565
|
+
* @description Provides the PropertyObserver class, a utility to observe property and attribute
|
|
566
|
+
* changes on any DOM element, with automatic getter/setter interception and batched notifications.
|
|
567
|
+
*/
|
|
568
|
+
const logger$2 = new Logger("PropertyObserver");
|
|
569
|
+
/**
|
|
570
|
+
* The PropertyObserver class allows developers to monitor property and attribute changes
|
|
571
|
+
* on any DOM element. It focuses on non-standard properties and attributes, automatically
|
|
572
|
+
* installing getter/setter interceptors for property changes and using MutationObserver
|
|
573
|
+
* for attribute changes.
|
|
574
|
+
*
|
|
575
|
+
* Key features:
|
|
576
|
+
* 1. Observes both element properties (el.foo = 1) and HTML attributes
|
|
577
|
+
* 2. Focuses on non-standard attributes/properties only
|
|
578
|
+
* 3. Converts between attribute names (kebab-case) and property names (camelCase)
|
|
579
|
+
* 4. Installs getter/setter interceptors for real-time property change detection
|
|
580
|
+
* 5. Uses MutationObserver for attribute change detection
|
|
581
|
+
* 6. Batches multiple changes using microtasks (except initial synchronous call)
|
|
582
|
+
*
|
|
583
|
+
* @example
|
|
584
|
+
* ```typescript
|
|
585
|
+
* const myElement = document.getElementById('my-element');
|
|
586
|
+
*
|
|
587
|
+
* function handleChanges(changes: Record<string, any>) {
|
|
588
|
+
* logger.log('Properties/attributes changed:', changes);
|
|
589
|
+
* // Example: changes might be { myProp: 'newValue', anotherProp: 42 }
|
|
590
|
+
* }
|
|
591
|
+
*
|
|
592
|
+
* const observer = new PropertyObserver(myElement!, handleChanges);
|
|
593
|
+
*
|
|
594
|
+
* // Later...
|
|
595
|
+
* myElement!.myProp = 'test'; // Will trigger callback
|
|
596
|
+
* myElement!.setAttribute('my-prop', 'test2'); // Will also trigger callback
|
|
597
|
+
*
|
|
598
|
+
* // To stop observing:
|
|
599
|
+
* observer.disconnect();
|
|
600
|
+
* ```
|
|
601
|
+
*/
|
|
602
|
+
class PropertyObserver {
|
|
603
|
+
/**
|
|
604
|
+
* The HTML element whose properties and attributes are being observed.
|
|
605
|
+
*/
|
|
606
|
+
_el;
|
|
607
|
+
/**
|
|
608
|
+
* The callback function to invoke when properties/attributes change.
|
|
609
|
+
*/
|
|
610
|
+
_cb;
|
|
611
|
+
/**
|
|
612
|
+
* A cache of the last known values of observed properties.
|
|
613
|
+
* Keys are property names (camelCase), values are their current values.
|
|
614
|
+
*/
|
|
615
|
+
_cache;
|
|
616
|
+
/**
|
|
617
|
+
* The callback to determine if a property should be observed.
|
|
618
|
+
*/
|
|
619
|
+
_shouldObserve;
|
|
620
|
+
/**
|
|
621
|
+
* Set of property names that we've installed getter/setter interceptors for.
|
|
622
|
+
*/
|
|
623
|
+
_interceptedProps;
|
|
624
|
+
/**
|
|
625
|
+
* Map of original property descriptors that we've replaced.
|
|
626
|
+
*/
|
|
627
|
+
_originalDescriptors;
|
|
628
|
+
/**
|
|
629
|
+
* The MutationObserver instance watching for attribute changes.
|
|
630
|
+
*/
|
|
631
|
+
_observer;
|
|
632
|
+
/**
|
|
633
|
+
* Flag to track if we have a pending microtask for batched changes.
|
|
634
|
+
*/
|
|
635
|
+
_changesPending;
|
|
636
|
+
/**
|
|
637
|
+
* Accumulated changes waiting to be reported in the next microtask.
|
|
638
|
+
*/
|
|
639
|
+
_pendingChanges;
|
|
640
|
+
/**
|
|
641
|
+
* Map of attribute names that don't follow standard kebab-case to camelCase conversion.
|
|
642
|
+
* Maps attribute name -> property name for special cases.
|
|
643
|
+
*/
|
|
644
|
+
_attributeExceptions = new Map([
|
|
645
|
+
["for", "htmlFor"],
|
|
646
|
+
["class", "className"],
|
|
647
|
+
["formnovalidate", "formNoValidate"],
|
|
648
|
+
["readonly", "readOnly"],
|
|
649
|
+
["maxlength", "maxLength"],
|
|
650
|
+
["minlength", "minLength"],
|
|
651
|
+
["contenteditable", "contentEditable"],
|
|
652
|
+
["spellcheck", "spellcheck"], // This one actually matches, but explicitly included for clarity
|
|
653
|
+
["novalidate", "noValidate"],
|
|
654
|
+
["autofocus", "autofocus"], // This one actually matches too
|
|
655
|
+
["autocomplete", "autocomplete"], // And this one
|
|
656
|
+
["crossorigin", "crossOrigin"],
|
|
657
|
+
]);
|
|
658
|
+
/**
|
|
659
|
+
* Creates an instance of PropertyObserver.
|
|
660
|
+
*
|
|
661
|
+
* @param target The DOM element whose properties and attributes you want to observe.
|
|
662
|
+
* @param callback
|
|
663
|
+
* The function to be invoked when one or more properties/attributes change.
|
|
664
|
+
* This function receives a single argument: an object where keys are property names
|
|
665
|
+
* (in camelCase) and values are their new values.
|
|
666
|
+
* This callback is called synchronously once upon initialization with all initial values,
|
|
667
|
+
* then asynchronously (batched via microtask) for subsequent changes.
|
|
668
|
+
* @throws {TypeError} If `target` is not a DOM Element or `callback` is not a function.
|
|
669
|
+
*/
|
|
670
|
+
constructor(target, callback, shouldObserve) {
|
|
671
|
+
if (!(target instanceof Element)) {
|
|
672
|
+
throw new TypeError("Target must be a DOM Element");
|
|
673
|
+
}
|
|
674
|
+
if (typeof callback !== "function") {
|
|
675
|
+
throw new TypeError("Callback must be a function");
|
|
676
|
+
}
|
|
677
|
+
if (shouldObserve && typeof shouldObserve !== "function") {
|
|
678
|
+
throw new TypeError("shouldObserve callback must be a function");
|
|
679
|
+
}
|
|
680
|
+
const shouldObserveDefault = (propName, isStandard) => {
|
|
681
|
+
// By default, observe only non-standard (i.e. custom) properties and attributes.
|
|
682
|
+
return isStandard === false;
|
|
683
|
+
};
|
|
684
|
+
this._el = target;
|
|
685
|
+
this._cb = callback;
|
|
686
|
+
this._cache = new Map();
|
|
687
|
+
this._shouldObserve = shouldObserve || shouldObserveDefault;
|
|
688
|
+
this._interceptedProps = new Set();
|
|
689
|
+
this._originalDescriptors = new Map();
|
|
690
|
+
this._changesPending = false;
|
|
691
|
+
this._pendingChanges = {};
|
|
692
|
+
// Perform initial scan and setup
|
|
693
|
+
this._initialScan();
|
|
694
|
+
this._setupMutationObserver();
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Disconnects the observer and restores original property descriptors.
|
|
698
|
+
* This stops observing changes and cleans up any modifications made to the element.
|
|
699
|
+
*/
|
|
700
|
+
disconnect() {
|
|
701
|
+
// Disconnect mutation observer
|
|
702
|
+
if (this._observer) {
|
|
703
|
+
this._observer.disconnect();
|
|
704
|
+
}
|
|
705
|
+
// Restore original property descriptors
|
|
706
|
+
for (const [propName, descriptor] of this._originalDescriptors) {
|
|
707
|
+
Object.defineProperty(this._el, propName, descriptor);
|
|
708
|
+
}
|
|
709
|
+
// Clear all internal state
|
|
710
|
+
this._cache.clear();
|
|
711
|
+
this._interceptedProps.clear();
|
|
712
|
+
this._originalDescriptors.clear();
|
|
713
|
+
this._pendingChanges = {};
|
|
714
|
+
this._changesPending = false;
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Performs the initial scan of the element to detect non-standard attributes
|
|
718
|
+
* and properties, then calls the callback synchronously with initial values.
|
|
719
|
+
*/
|
|
720
|
+
_initialScan() {
|
|
721
|
+
const initialValues = {};
|
|
722
|
+
// Scan attributes
|
|
723
|
+
for (const attr of Array.from(this._el.attributes)) {
|
|
724
|
+
const attrName = attr.name;
|
|
725
|
+
// Check if the attribute itself is standard
|
|
726
|
+
const isStandard = this._isStandardAttribute(attrName);
|
|
727
|
+
// Skip the attribute if the user says it should not be observed
|
|
728
|
+
if (!this._shouldObserve(attrName, isStandard)) {
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
const propName = this._attributeNameToPropName(attrName);
|
|
732
|
+
const value = attr.value;
|
|
733
|
+
initialValues[propName] = value;
|
|
734
|
+
this._cache.set(propName, value);
|
|
735
|
+
// Install property interceptor
|
|
736
|
+
this._installPropertyInterceptor(propName);
|
|
737
|
+
}
|
|
738
|
+
// Also scan for existing custom properties on the element
|
|
739
|
+
for (const propName of Object.getOwnPropertyNames(this._el)) {
|
|
740
|
+
// Check if the property itself is standard
|
|
741
|
+
const isStandard = this._isStandardProperty(propName);
|
|
742
|
+
// Skip the property if it is already processed as an attribute (check this first), or
|
|
743
|
+
// if it the user says it should not be observed.
|
|
744
|
+
if (this._cache.has(propName) || !this._shouldObserve(propName, isStandard)) {
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
// If it's an own property, it's likely custom and worth observing
|
|
748
|
+
const value = this._el[propName];
|
|
749
|
+
initialValues[propName] = value;
|
|
750
|
+
this._cache.set(propName, value);
|
|
751
|
+
// Install property interceptor
|
|
752
|
+
this._installPropertyInterceptor(propName);
|
|
753
|
+
}
|
|
754
|
+
// Call callback synchronously with initial values
|
|
755
|
+
if (Object.keys(initialValues).length > 0) {
|
|
756
|
+
try {
|
|
757
|
+
this._cb(initialValues);
|
|
758
|
+
}
|
|
759
|
+
catch (e) {
|
|
760
|
+
logger$2.error("Error in initial PropertyObserver callback:", e);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Sets up the MutationObserver to watch for attribute changes.
|
|
766
|
+
*/
|
|
767
|
+
_setupMutationObserver() {
|
|
768
|
+
this._observer = new MutationObserver((mutations) => {
|
|
769
|
+
const changes = {};
|
|
770
|
+
for (const mutation of mutations) {
|
|
771
|
+
if (mutation.type === "attributes" && mutation.attributeName) {
|
|
772
|
+
const attrName = mutation.attributeName;
|
|
773
|
+
// Check if the attribute itself is standard
|
|
774
|
+
const isStandard = this._isStandardAttribute(attrName);
|
|
775
|
+
// Skip the attribute if the user says it should not be observed
|
|
776
|
+
if (!this._shouldObserve(attrName, isStandard)) {
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
const propName = this._attributeNameToPropName(attrName);
|
|
780
|
+
const newValue = this._el.getAttribute(attrName);
|
|
781
|
+
const cachedValue = this._cache.get(propName);
|
|
782
|
+
if (newValue !== cachedValue) {
|
|
783
|
+
changes[propName] = newValue;
|
|
784
|
+
this._cache.set(propName, newValue);
|
|
785
|
+
// Install interceptor for new properties
|
|
786
|
+
if (!this._interceptedProps.has(propName)) {
|
|
787
|
+
this._installPropertyInterceptor(propName);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
if (Object.keys(changes).length > 0) {
|
|
793
|
+
this._batchChanges(changes);
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
this._observer.observe(this._el, {
|
|
797
|
+
attributes: true,
|
|
798
|
+
attributeOldValue: false, // We track old values ourselves
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Installs a getter/setter interceptor for a property to detect changes.
|
|
803
|
+
* @param propName The property name to intercept
|
|
804
|
+
*/
|
|
805
|
+
_installPropertyInterceptor(propName) {
|
|
806
|
+
if (this._interceptedProps.has(propName)) {
|
|
807
|
+
return; // Already intercepted
|
|
808
|
+
}
|
|
809
|
+
// Get current descriptor
|
|
810
|
+
const currentDescriptor = Object.getOwnPropertyDescriptor(this._el, propName) || {
|
|
811
|
+
value: this._el[propName],
|
|
812
|
+
writable: true,
|
|
813
|
+
enumerable: true,
|
|
814
|
+
configurable: true,
|
|
815
|
+
};
|
|
816
|
+
// Store original descriptor
|
|
817
|
+
this._originalDescriptors.set(propName, currentDescriptor);
|
|
818
|
+
// Create new descriptor with getter/setter
|
|
819
|
+
const newDescriptor = {
|
|
820
|
+
enumerable: currentDescriptor.enumerable,
|
|
821
|
+
configurable: currentDescriptor.configurable,
|
|
822
|
+
get: currentDescriptor.get || (() => currentDescriptor.value),
|
|
823
|
+
set: (newValue) => {
|
|
824
|
+
const oldValue = this._cache.get(propName);
|
|
825
|
+
if (newValue !== oldValue) {
|
|
826
|
+
// Update the actual property
|
|
827
|
+
if (currentDescriptor.set) {
|
|
828
|
+
currentDescriptor.set.call(this._el, newValue);
|
|
829
|
+
}
|
|
830
|
+
else {
|
|
831
|
+
currentDescriptor.value = newValue;
|
|
832
|
+
}
|
|
833
|
+
// Update cache and batch the change
|
|
834
|
+
this._cache.set(propName, newValue);
|
|
835
|
+
this._batchChanges({ [propName]: newValue });
|
|
836
|
+
}
|
|
837
|
+
},
|
|
838
|
+
};
|
|
839
|
+
// Install the new descriptor
|
|
840
|
+
Object.defineProperty(this._el, propName, newDescriptor);
|
|
841
|
+
this._interceptedProps.add(propName);
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Batches changes to be reported in the next microtask.
|
|
845
|
+
* @param changes Changes to batch
|
|
846
|
+
*/
|
|
847
|
+
_batchChanges(changes) {
|
|
848
|
+
// Merge with pending changes
|
|
849
|
+
Object.assign(this._pendingChanges, changes);
|
|
850
|
+
if (!this._changesPending) {
|
|
851
|
+
this._changesPending = true;
|
|
852
|
+
queueMicrotask(() => {
|
|
853
|
+
this._changesPending = false;
|
|
854
|
+
const changesToReport = { ...this._pendingChanges };
|
|
855
|
+
this._pendingChanges = {};
|
|
856
|
+
try {
|
|
857
|
+
this._cb(changesToReport);
|
|
858
|
+
}
|
|
859
|
+
catch (e) {
|
|
860
|
+
logger$2.error("Error in PropertyObserver callback:", e);
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Converts an attribute name (kebab-case) to a property name (camelCase).
|
|
867
|
+
* @param attrName The attribute name
|
|
868
|
+
* @returns The property name
|
|
869
|
+
*/
|
|
870
|
+
_attributeNameToPropName(attrName) {
|
|
871
|
+
// Check for special cases that don't follow standard conversion
|
|
872
|
+
if (this._attributeExceptions.has(attrName)) {
|
|
873
|
+
return this._attributeExceptions.get(attrName);
|
|
874
|
+
}
|
|
875
|
+
// Standard kebab-case to camelCase conversion
|
|
876
|
+
return attrName.replace(/-([a-z])/g, (match, letter) => letter.toUpperCase());
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Checks if an attribute name is a standard HTML attribute that should be ignored.
|
|
880
|
+
* @param attrName The attribute name (already lowercase per HTML spec)
|
|
881
|
+
* @returns True if it's a standard attribute
|
|
882
|
+
*/
|
|
883
|
+
_isStandardAttribute(attrName) {
|
|
884
|
+
// Skip known prefixes that are always standard
|
|
885
|
+
if (attrName.startsWith("data-") || attrName.startsWith("aria-") || attrName.startsWith("on")) {
|
|
886
|
+
return true;
|
|
887
|
+
}
|
|
888
|
+
// Convert attribute name to property name and check if it exists in prototypes
|
|
889
|
+
const propName = this._attributeNameToPropName(attrName);
|
|
890
|
+
return this._isStandardProperty(propName);
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Checks if a property name corresponds to a standard property that should be ignored.
|
|
894
|
+
* @param propName The property name
|
|
895
|
+
* @returns True if it's a standard property
|
|
896
|
+
*/
|
|
897
|
+
_isStandardProperty(propName) {
|
|
898
|
+
// Check if the property exists in the standard prototype chain
|
|
899
|
+
// HTMLElement.prototype inherits from Element → Node → EventTarget
|
|
900
|
+
return propName in HTMLElement.prototype;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* A registry for managing the relationship between parent `App` instances
|
|
906
|
+
* and child `Comp` instances.
|
|
907
|
+
*/
|
|
908
|
+
class Registry {
|
|
909
|
+
#loError = new LightningOutError("LightningOutRegistry");
|
|
910
|
+
appToComps = new WeakMap();
|
|
911
|
+
compToApp = new WeakMap();
|
|
912
|
+
compNameToApp = new Map();
|
|
913
|
+
/**
|
|
914
|
+
* Registers a new App instance in the registry.
|
|
915
|
+
* @param app The App instance to register.
|
|
916
|
+
* registered app.
|
|
917
|
+
*/
|
|
918
|
+
registerApplication(app) {
|
|
919
|
+
if (!this.appToComps.has(app)) {
|
|
920
|
+
this.appToComps.set(app, new Set());
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Registers a component name which is owned by a specific App
|
|
925
|
+
* @param name The component name
|
|
926
|
+
* @param app The app that owns the name
|
|
927
|
+
* @throws {LightningOutError} if the name is already registered
|
|
928
|
+
*/
|
|
929
|
+
registerComponentName(name, app) {
|
|
930
|
+
if (this.compNameToApp.has(name)) {
|
|
931
|
+
throw this.#loError.create(`"${name}" is already registered to another App.`);
|
|
932
|
+
}
|
|
933
|
+
this.compNameToApp.set(name, app);
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Registers a Comp instance and associates it with a parent App.
|
|
937
|
+
* @param comp The Comp instance to register.
|
|
938
|
+
* @param app The parent App instance. (Optional)
|
|
939
|
+
* @returns The parent App
|
|
940
|
+
* @throws {LightningOutError} If the Comp is already registered or if no parent App is found.
|
|
941
|
+
*/
|
|
942
|
+
registerComponent(comp, app) {
|
|
943
|
+
if (this.compToApp.has(comp)) {
|
|
944
|
+
throw this.#loError.create("This Comp is already registered to another App.");
|
|
945
|
+
}
|
|
946
|
+
let parentApp = app;
|
|
947
|
+
// If no app was provided, find it by component name
|
|
948
|
+
if (!parentApp) {
|
|
949
|
+
const compName = comp.localName; // inherited from Element
|
|
950
|
+
parentApp = this.compNameToApp.get(compName);
|
|
951
|
+
if (!parentApp) {
|
|
952
|
+
throw this.#loError.create(`Could not find a parent App for component "${comp.localName}"`);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
// Associate the component with its parent app
|
|
956
|
+
this.appToComps.get(parentApp).add(comp);
|
|
957
|
+
this.compToApp.set(comp, parentApp);
|
|
958
|
+
return parentApp;
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Unregister a Comp instance from the registry.
|
|
962
|
+
* @param comp The Comp instance to unregister.
|
|
963
|
+
* @returns `true` if the component was found and unregistered, otherwise `false`.
|
|
964
|
+
*/
|
|
965
|
+
unregisterComponent(comp) {
|
|
966
|
+
const app = this.compToApp.get(comp);
|
|
967
|
+
if (!app) {
|
|
968
|
+
return false;
|
|
969
|
+
}
|
|
970
|
+
const comps = this.appToComps.get(app);
|
|
971
|
+
comps?.delete(comp);
|
|
972
|
+
this.compToApp.delete(comp);
|
|
973
|
+
return true;
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Retrieves the set of Comps associated with a given App.
|
|
977
|
+
* @param app The App instance for which to retrieve components.
|
|
978
|
+
* @returns A Set of Comp instances associated with the given App.
|
|
979
|
+
* @throws {LightningOutError} If the set of components for the given app cannot be found.
|
|
980
|
+
*/
|
|
981
|
+
getComps(app) {
|
|
982
|
+
const comps = this.appToComps.get(app);
|
|
983
|
+
if (!comps) {
|
|
984
|
+
throw this.#loError.create("Unable to find set of LightningOutComponents");
|
|
985
|
+
}
|
|
986
|
+
return comps;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Singleton instance of the Registry class.
|
|
991
|
+
*/
|
|
992
|
+
const registry = new Registry();
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* LightningOutComponent class for Lightning Out
|
|
996
|
+
*/
|
|
997
|
+
const logger$1 = new Logger("LightningOutComponent");
|
|
998
|
+
// These attributes are mirrored into the iframe
|
|
999
|
+
const MIRROR = new Set([
|
|
1000
|
+
"autocapitalize",
|
|
1001
|
+
"autocorrect",
|
|
1002
|
+
"dir",
|
|
1003
|
+
"enterkeyhint",
|
|
1004
|
+
"inputmode",
|
|
1005
|
+
"lang",
|
|
1006
|
+
"spellcheck",
|
|
1007
|
+
"style", // this is mirrored but only the css variables
|
|
1008
|
+
"title",
|
|
1009
|
+
"translate",
|
|
1010
|
+
]);
|
|
1011
|
+
// These aria attributes are mirrored into the iframe (specially for extensions of lightning-input)
|
|
1012
|
+
const ARIA_MIRROR = new Set([
|
|
1013
|
+
"aria-disabled",
|
|
1014
|
+
"aria-hidden",
|
|
1015
|
+
"aria-label",
|
|
1016
|
+
"aria-live",
|
|
1017
|
+
"aria-modal",
|
|
1018
|
+
"aria-pressed",
|
|
1019
|
+
"aria-valuemax",
|
|
1020
|
+
"aria-valuemin",
|
|
1021
|
+
"aria-valuenow",
|
|
1022
|
+
]);
|
|
1023
|
+
// These attributes are not mirrored into the iframe
|
|
1024
|
+
const BLOCK = new Set([
|
|
1025
|
+
"accesskey",
|
|
1026
|
+
"autofocus",
|
|
1027
|
+
"draggable",
|
|
1028
|
+
"exportparts",
|
|
1029
|
+
"hidden",
|
|
1030
|
+
"inert",
|
|
1031
|
+
"nonce", // block for security reasons!
|
|
1032
|
+
"part",
|
|
1033
|
+
"slot",
|
|
1034
|
+
"tabindex",
|
|
1035
|
+
]);
|
|
1036
|
+
// These aria attributes are not mirrored into the iframe (they are ID refs)
|
|
1037
|
+
const ARIA_BLOCK = new Set([
|
|
1038
|
+
"aria-activedescendant",
|
|
1039
|
+
"aria-controls",
|
|
1040
|
+
"aria-describedby",
|
|
1041
|
+
"aria-details",
|
|
1042
|
+
"aria-errormessage",
|
|
1043
|
+
"aria-flowto",
|
|
1044
|
+
"aria-labelledby",
|
|
1045
|
+
"aria-owns",
|
|
1046
|
+
]);
|
|
1047
|
+
class LightningOutComponent extends HTMLElement {
|
|
1048
|
+
_uuid = getUUID();
|
|
1049
|
+
_ready = false;
|
|
1050
|
+
_standardName = elementNameToStandardName(this.localName); // May change during registration
|
|
1051
|
+
#parentApp; // The parent LightningOutApplication
|
|
1052
|
+
#loError = new LightningOutError(this);
|
|
1053
|
+
#loIFrame = new LightningOutIFrame({
|
|
1054
|
+
parentElement: this,
|
|
1055
|
+
isVisible: true,
|
|
1056
|
+
});
|
|
1057
|
+
#propObserver;
|
|
1058
|
+
#initialRender = true;
|
|
1059
|
+
#eventQueue = [];
|
|
1060
|
+
#listenerKeyMap = new WeakMap();
|
|
1061
|
+
// Seed for generating unique listener keys.
|
|
1062
|
+
#listenerKeySeed = 0;
|
|
1063
|
+
constructor() {
|
|
1064
|
+
super();
|
|
1065
|
+
logger$1.trace("constructor: called", `_uuid: ${this._uuid}`);
|
|
1066
|
+
}
|
|
1067
|
+
_getComponentURL() {
|
|
1068
|
+
const parentApp = this.#parentApp;
|
|
1069
|
+
if (!parentApp) {
|
|
1070
|
+
throw this.#loError.create("Undefined parent App!");
|
|
1071
|
+
}
|
|
1072
|
+
const compURL = parentApp._getComponentURL(this._standardName, this._uuid);
|
|
1073
|
+
return compURL;
|
|
1074
|
+
}
|
|
1075
|
+
_init() {
|
|
1076
|
+
if (!this._ready) {
|
|
1077
|
+
// We don't care about events.lo.iframe.ready, we care about messages.lo.ready
|
|
1078
|
+
this.#loIFrame.load(this._getComponentURL().href);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
#messageListener = (event) => {
|
|
1082
|
+
if (event.data.id !== this._uuid) {
|
|
1083
|
+
// Ignore messages that are not meant to be received by this instance.
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
logger$1.debug("#messageListener:", `this._uuid: ${this._uuid}`, `this._standardName: ${this._standardName}`, `event.data: ${JSON.stringify(event.data)}`);
|
|
1087
|
+
switch (event.data.type) {
|
|
1088
|
+
case messages.lo.ready: {
|
|
1089
|
+
while (this.#eventQueue.length) {
|
|
1090
|
+
const item = this.#eventQueue.shift();
|
|
1091
|
+
if (!item)
|
|
1092
|
+
continue;
|
|
1093
|
+
if (item.type === "add") {
|
|
1094
|
+
this.addEventListener(...item.args);
|
|
1095
|
+
}
|
|
1096
|
+
else if (item.type === "remove") {
|
|
1097
|
+
this.removeEventListener(...item.args);
|
|
1098
|
+
}
|
|
1099
|
+
else if (item.type === "dispatch") {
|
|
1100
|
+
this.dispatchEvent(item.event);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
this._ready = true;
|
|
1104
|
+
super.dispatchEvent(new CustomEvent(events.lo.component.ready));
|
|
1105
|
+
break;
|
|
1106
|
+
}
|
|
1107
|
+
case messages.lo.getComponentData: {
|
|
1108
|
+
this.#propObserver = new PropertyObserver(this, this.#propObserverCallback, this.#shouldObserveCallback);
|
|
1109
|
+
break;
|
|
1110
|
+
}
|
|
1111
|
+
case messages.lo.dispatchEvent: {
|
|
1112
|
+
const customEvent = new CustomEvent(event.data.name, {
|
|
1113
|
+
detail: event.data.detail,
|
|
1114
|
+
});
|
|
1115
|
+
super.dispatchEvent(customEvent);
|
|
1116
|
+
break;
|
|
1117
|
+
}
|
|
1118
|
+
case messages.lo.error: {
|
|
1119
|
+
this.#loError.dispatch(events.lo.component.error, event.data.error);
|
|
1120
|
+
break;
|
|
1121
|
+
}
|
|
1122
|
+
default: {
|
|
1123
|
+
logger$1.info(`#messageListener:`, `Unknown message received:`, {
|
|
1124
|
+
"event.data": event.data,
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
};
|
|
1129
|
+
_propertyChanged_style = (_style) => {
|
|
1130
|
+
// Iterate over the CSSStyleDeclaration object to filter the CSS variables. (Notice we don't
|
|
1131
|
+
// use the style argument, we take it from this.style).
|
|
1132
|
+
const cssDeclaration = this.style;
|
|
1133
|
+
const cssVars = [];
|
|
1134
|
+
for (let i = 0; i < cssDeclaration.length; i += 1) {
|
|
1135
|
+
const prop = cssDeclaration.item(i);
|
|
1136
|
+
if (prop.startsWith("--")) {
|
|
1137
|
+
cssVars.push(`${prop}:${cssDeclaration.getPropertyValue(prop)}`);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
return cssVars.join(";");
|
|
1141
|
+
};
|
|
1142
|
+
#propObserverCallback = (changes) => {
|
|
1143
|
+
const propsToSend = processPropertyChanges(this, changes);
|
|
1144
|
+
logger$1.debug("#propObserverCallback:", { changes, propsToSend });
|
|
1145
|
+
if (this.#initialRender) {
|
|
1146
|
+
this.#initialRender = false;
|
|
1147
|
+
this.#loIFrame.postMessage({
|
|
1148
|
+
type: messages.lo.setComponentData,
|
|
1149
|
+
componentData: {
|
|
1150
|
+
id: this._uuid,
|
|
1151
|
+
name: this._standardName,
|
|
1152
|
+
props: propsToSend,
|
|
1153
|
+
},
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
else {
|
|
1157
|
+
this.#loIFrame.postMessage({
|
|
1158
|
+
type: messages.lo.setComponentProps,
|
|
1159
|
+
componentProps: propsToSend,
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
};
|
|
1163
|
+
#shouldObserveCallback = (attrOrPropName, isStandard) => {
|
|
1164
|
+
const attrName = camelToKebab(attrOrPropName); // do this becaouse our config Sets are in kebab-case
|
|
1165
|
+
logger$1.debug("#shouldObserveCallback:", { attrOrPropName, attrName, isStandard });
|
|
1166
|
+
if (isStandard) {
|
|
1167
|
+
if (MIRROR.has(attrName) || ARIA_MIRROR.has(attrName)) {
|
|
1168
|
+
return true;
|
|
1169
|
+
}
|
|
1170
|
+
if (BLOCK.has(attrName) || ARIA_BLOCK.has(attrName) || attrName.startsWith("on")) {
|
|
1171
|
+
logger$1.warn(`"${attrName}" will not be mirrored.`);
|
|
1172
|
+
return false;
|
|
1173
|
+
}
|
|
1174
|
+
if (attrName.startsWith("data-mirror-")) {
|
|
1175
|
+
return true;
|
|
1176
|
+
}
|
|
1177
|
+
// Everything else
|
|
1178
|
+
logger$1.warn(`"${attrName}" will not be mirrored.`);
|
|
1179
|
+
return false;
|
|
1180
|
+
}
|
|
1181
|
+
else {
|
|
1182
|
+
return !attrName.startsWith("_");
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
addEventListener(eventName, listener, options) {
|
|
1186
|
+
let key = this.#listenerKeyMap.get(listener);
|
|
1187
|
+
if (!key) {
|
|
1188
|
+
key = `${eventName}_${this.#listenerKeySeed++}`;
|
|
1189
|
+
this.#listenerKeyMap.set(listener, key);
|
|
1190
|
+
}
|
|
1191
|
+
if (this.#loIFrame.iframeReady) {
|
|
1192
|
+
// @ts-ignore: Spread arguments works at runtime even though TypeScript doesn't like it
|
|
1193
|
+
super.addEventListener(...arguments);
|
|
1194
|
+
this.#loIFrame.postMessage({
|
|
1195
|
+
name: eventName,
|
|
1196
|
+
options: options,
|
|
1197
|
+
listenerKey: key,
|
|
1198
|
+
type: messages.lo.addEventListener,
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
else {
|
|
1202
|
+
this.#eventQueue.push({ type: "add", args: [eventName, listener, options] });
|
|
1203
|
+
logger$1.debug(`addEventListener:`, `#eventQueue pushed add args:`, [eventName, listener, options]);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
dispatchEvent(event) {
|
|
1207
|
+
if (event.type.startsWith("lo.")) {
|
|
1208
|
+
logger$1.debug(`dispatchEvent: dispatching event "${event.type}" to this Element only`);
|
|
1209
|
+
return super.dispatchEvent(event);
|
|
1210
|
+
}
|
|
1211
|
+
else {
|
|
1212
|
+
if (this.#loIFrame.iframeReady) {
|
|
1213
|
+
logger$1.debug(`dispatchEvent: dispatching event "${event.type}" to this Element and embedded Element inside the iframe`);
|
|
1214
|
+
const result = super.dispatchEvent(event);
|
|
1215
|
+
this.#loIFrame.postMessage({
|
|
1216
|
+
name: event.type,
|
|
1217
|
+
detail: event.detail || {},
|
|
1218
|
+
type: messages.lo.dispatchEvent,
|
|
1219
|
+
});
|
|
1220
|
+
return result;
|
|
1221
|
+
}
|
|
1222
|
+
else {
|
|
1223
|
+
logger$1.debug(`dispatchEvent: iframe not reade, queueing event "${event.type}"`);
|
|
1224
|
+
this.#eventQueue.push({ type: "dispatch", event });
|
|
1225
|
+
return true; // Assuming success since we're queueing it
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
removeEventListener(eventName, listener, options) {
|
|
1230
|
+
const key = this.#listenerKeyMap.get(listener);
|
|
1231
|
+
if (this.#loIFrame.iframeReady) {
|
|
1232
|
+
// @ts-ignore: Spread arguments works at runtime even though TypeScript doesn't like it
|
|
1233
|
+
super.removeEventListener(...arguments);
|
|
1234
|
+
this.#loIFrame.postMessage({
|
|
1235
|
+
name: eventName,
|
|
1236
|
+
options: options,
|
|
1237
|
+
listenerKey: key,
|
|
1238
|
+
type: messages.lo.removeEventListener,
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
else {
|
|
1242
|
+
this.#eventQueue.push({ type: "remove", args: [eventName, listener, options] });
|
|
1243
|
+
logger$1.debug(`removeEventListener:`, `#eventQueue pushed remove args:`, [eventName, listener, options]);
|
|
1244
|
+
}
|
|
1245
|
+
if (key) {
|
|
1246
|
+
this.#listenerKeyMap.delete(listener);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
adoptedCallback() {
|
|
1250
|
+
this.remove(); // will call disconnectedCallback
|
|
1251
|
+
throw this.#loError.create("This component cannot be rerendered for security reasons.");
|
|
1252
|
+
}
|
|
1253
|
+
connectedCallback() {
|
|
1254
|
+
logger$1.trace("connectedCallback: called", `_uuid: ${this._uuid}`);
|
|
1255
|
+
window.addEventListener("message", this.#messageListener);
|
|
1256
|
+
if (this.hasChildNodes()) {
|
|
1257
|
+
throw this.#loError.create("Should not have child nodes");
|
|
1258
|
+
}
|
|
1259
|
+
// Apply default styles
|
|
1260
|
+
this.style.display ||= "block";
|
|
1261
|
+
this.style.width ||= "100%";
|
|
1262
|
+
this.style.height ||= "100%";
|
|
1263
|
+
this.#parentApp = registry.registerComponent(this);
|
|
1264
|
+
this._standardName = this.#parentApp._getComponentStandardName(this);
|
|
1265
|
+
if (this.#parentApp._ready) {
|
|
1266
|
+
this._init();
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
disconnectedCallback() {
|
|
1270
|
+
logger$1.trace("disconnectedCallback: called", `_uuid: ${this._uuid}`);
|
|
1271
|
+
// Since disconnectedCallback is called after the DOM has been removed, destroying the iframe here may be a moot
|
|
1272
|
+
// point, but doing it for completeness.
|
|
1273
|
+
this.#loIFrame.destroy();
|
|
1274
|
+
window.removeEventListener("message", this.#messageListener);
|
|
1275
|
+
registry.unregisterComponent(this);
|
|
1276
|
+
// Stop PropertyObserver
|
|
1277
|
+
this.#propObserver?.disconnect();
|
|
1278
|
+
}
|
|
1279
|
+
connectedMoveCallback() {
|
|
1280
|
+
// Define this callback but do nothing
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
/**
|
|
1285
|
+
* Router for handling Lightning Out component URLs
|
|
1286
|
+
*/
|
|
1287
|
+
class LightningOutRouter {
|
|
1288
|
+
config;
|
|
1289
|
+
errorHandler;
|
|
1290
|
+
constructor(config, errorHandler) {
|
|
1291
|
+
this.config = config;
|
|
1292
|
+
this.errorHandler = errorHandler;
|
|
1293
|
+
if (!this.config.origin) {
|
|
1294
|
+
throw this.errorHandler(`Missing "frontdoor-url" or "org-url" attribute`);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
getComponentURL(componentName, parentElementId) {
|
|
1298
|
+
let appURL;
|
|
1299
|
+
if (this.config.sitePrefix === undefined) {
|
|
1300
|
+
// Route to LWR
|
|
1301
|
+
let lwrApp = componentName.includes("/")
|
|
1302
|
+
? this.config.lwrAppComp
|
|
1303
|
+
: componentName.includes(":")
|
|
1304
|
+
? this.config.lwrAppAura
|
|
1305
|
+
: undefined;
|
|
1306
|
+
if (lwrApp === undefined) {
|
|
1307
|
+
throw this.errorHandler(`Invalid componentName: ${componentName}`);
|
|
1308
|
+
}
|
|
1309
|
+
lwrApp = lwrApp.replace("/", "%2F"); // ¯\_(ツ)_/¯
|
|
1310
|
+
const lang = this.config.lang ? `l/${this.config.lang}/` : "";
|
|
1311
|
+
appURL = new URL(`lwr/application/amd/0/${lang}ai/${lwrApp}`, this.config.origin);
|
|
1312
|
+
}
|
|
1313
|
+
else {
|
|
1314
|
+
// Route to CLWR or TEST environment. Note: sitePrefix may be blank and that's okay.
|
|
1315
|
+
const pathname = `${this.config.sitePrefix}/lightning-out` ;
|
|
1316
|
+
appURL = new URL(pathname, this.config.origin);
|
|
1317
|
+
}
|
|
1318
|
+
appURL.searchParams.set("componentName", componentName);
|
|
1319
|
+
return this.#addCommonParams(appURL, parentElementId);
|
|
1320
|
+
}
|
|
1321
|
+
getAuthURL(parentElementId) {
|
|
1322
|
+
let appURL;
|
|
1323
|
+
if (this.config.sitePrefix === undefined) {
|
|
1324
|
+
// Route to LWR
|
|
1325
|
+
const lwrApp = this.config.lwrAppAuth.replace("/", "%2F"); // ¯\_(ツ)_/¯
|
|
1326
|
+
appURL = new URL(`lwr/application/amd/0/ai/${lwrApp}`, this.config.origin);
|
|
1327
|
+
}
|
|
1328
|
+
else {
|
|
1329
|
+
// Route to CLWR or TEST environment. Note: sitePrefix may be blank and that's okay.
|
|
1330
|
+
appURL = new URL(this.config.lwrPageAuth, this.config.origin);
|
|
1331
|
+
}
|
|
1332
|
+
return this.#addCommonParams(appURL, parentElementId);
|
|
1333
|
+
}
|
|
1334
|
+
getPageURL(pagePathname, parentElementId) {
|
|
1335
|
+
const pageURL = new URL(pagePathname, this.config.origin);
|
|
1336
|
+
return this.#addCommonParams(pageURL, parentElementId);
|
|
1337
|
+
}
|
|
1338
|
+
#addCommonParams(url, parentElementId) {
|
|
1339
|
+
url.searchParams.set("parentElementId", parentElementId);
|
|
1340
|
+
url.searchParams.set("loAppOrigin", this.config.loAppOrigin);
|
|
1341
|
+
// This helps in general but also for cache busting
|
|
1342
|
+
url.searchParams.set("loVersion", "2.2.0-rc.2");
|
|
1343
|
+
if (this.config.appId) {
|
|
1344
|
+
url.searchParams.set("appId", this.config.appId);
|
|
1345
|
+
}
|
|
1346
|
+
if (this.config.testMode) {
|
|
1347
|
+
url.searchParams.set("testMode", "true");
|
|
1348
|
+
}
|
|
1349
|
+
if (this.config.designSystem) {
|
|
1350
|
+
url.searchParams.set("designSystem", this.config.designSystem);
|
|
1351
|
+
}
|
|
1352
|
+
if (this.config.globalStyle) {
|
|
1353
|
+
url.searchParams.set("globalStyle", this.config.globalStyle);
|
|
1354
|
+
}
|
|
1355
|
+
return url;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
/**
|
|
1360
|
+
* LightningOutApplication class for Lightning Out
|
|
1361
|
+
*/
|
|
1362
|
+
const logger = new Logger("LightningOutApplication");
|
|
1363
|
+
/** Valid values for the optional design-system attribute. */
|
|
1364
|
+
const DESIGN_SYSTEM_VALUES = new Set(["slds1", "slds2", "none"]);
|
|
1365
|
+
// These props trigger LO app initialization
|
|
1366
|
+
const INIT_TRIGGERING_PROPS = new Set(["frontdoorUrl", "orgUrl"]);
|
|
1367
|
+
class LightningOutApplication extends HTMLElement {
|
|
1368
|
+
_uuid = getUUID();
|
|
1369
|
+
_ready = false;
|
|
1370
|
+
#loError = new LightningOutError(this);
|
|
1371
|
+
#loIFrame = new LightningOutIFrame({
|
|
1372
|
+
parentElement: this,
|
|
1373
|
+
isVisible: false,
|
|
1374
|
+
});
|
|
1375
|
+
#loRouter;
|
|
1376
|
+
#propObserver;
|
|
1377
|
+
#lwrAppOrigin = "";
|
|
1378
|
+
#lwrAppAuth = "lightningout/auth";
|
|
1379
|
+
#lwrAppComp = "lightningout/container";
|
|
1380
|
+
#lwrAppAura = "lightningout/auraContainer";
|
|
1381
|
+
#lwrPageAuth = "lightning/lightning.out.auth.html";
|
|
1382
|
+
#lwrPageLogout = "lightning/lightning.out.logout.html";
|
|
1383
|
+
#lwrPageAuthError = "lightning/lightning.out.auth.error.html";
|
|
1384
|
+
#logoutUrl = "/secur/logout.jsp";
|
|
1385
|
+
lwrApplication;
|
|
1386
|
+
orgUrl;
|
|
1387
|
+
#orgUrl;
|
|
1388
|
+
frontdoorUrl;
|
|
1389
|
+
#frontdoorUrl;
|
|
1390
|
+
appId;
|
|
1391
|
+
#appId;
|
|
1392
|
+
components; // CSV of component names
|
|
1393
|
+
#compNames = new Map(); // a Map of of component name->alias
|
|
1394
|
+
sitePrefix;
|
|
1395
|
+
#sitePrefix;
|
|
1396
|
+
designSystem;
|
|
1397
|
+
#designSystem;
|
|
1398
|
+
globalStyle;
|
|
1399
|
+
#globalStyle;
|
|
1400
|
+
#lang = document.documentElement.lang ?? "";
|
|
1401
|
+
constructor() {
|
|
1402
|
+
super();
|
|
1403
|
+
logger.trace("constructor: called", `_uuid: ${this._uuid}`);
|
|
1404
|
+
// We have to register this instance as early as possible
|
|
1405
|
+
registry.registerApplication(this);
|
|
1406
|
+
}
|
|
1407
|
+
#access(orgUrl) {
|
|
1408
|
+
try {
|
|
1409
|
+
this.#orgUrl = new URL(orgUrl);
|
|
1410
|
+
}
|
|
1411
|
+
catch {
|
|
1412
|
+
throw this.#loError.create(`Invalid org-url: ${orgUrl}`);
|
|
1413
|
+
}
|
|
1414
|
+
// Skip auth by simulating a successful lo.iframe.load event to continue normally
|
|
1415
|
+
this.dispatchEvent(new CustomEvent(events.lo.iframe.load, {
|
|
1416
|
+
detail: this.#orgUrl.origin,
|
|
1417
|
+
}));
|
|
1418
|
+
}
|
|
1419
|
+
#login(frontdoorUrl) {
|
|
1420
|
+
try {
|
|
1421
|
+
this.#frontdoorUrl = new URL(frontdoorUrl);
|
|
1422
|
+
this.#lwrAppOrigin = this.#frontdoorUrl.origin;
|
|
1423
|
+
// Append/override the `startURL` param in frontdoor.jsp
|
|
1424
|
+
const startURL = this.#getAuthURL();
|
|
1425
|
+
// (It is important to use the right paramName!)
|
|
1426
|
+
const paramName = this.#frontdoorUrl.searchParams.has("otp") ? "startURL" : "retURL";
|
|
1427
|
+
this.#frontdoorUrl.searchParams.set(paramName, startURL.pathname + startURL.search);
|
|
1428
|
+
// Append the `error-redirect-uri` param to frontdoor.jsp
|
|
1429
|
+
const authErrorPage = this.#getPageURL(this.#lwrPageAuthError);
|
|
1430
|
+
this.#frontdoorUrl.searchParams.set("error-redirect-uri", authErrorPage.pathname + authErrorPage.search);
|
|
1431
|
+
}
|
|
1432
|
+
catch {
|
|
1433
|
+
throw this.#loError.create(`Invalid frontdoor-url: ${frontdoorUrl}`);
|
|
1434
|
+
}
|
|
1435
|
+
this.#loIFrame.load(this.#frontdoorUrl.href);
|
|
1436
|
+
}
|
|
1437
|
+
#logout() {
|
|
1438
|
+
const logoutUrl = new URL(this.#logoutUrl, this.#lwrAppOrigin);
|
|
1439
|
+
const logoutPage = this.#getPageURL(this.#lwrPageLogout);
|
|
1440
|
+
// Append the `redirect-uri` param to logout.jsp (using the same auth page just to get the lo.loaded message)
|
|
1441
|
+
logoutUrl.searchParams.set("redirect-uri", logoutPage.pathname + logoutPage.search);
|
|
1442
|
+
// (This next piece of code should be removed after making sure `redirect-uri` works well)
|
|
1443
|
+
logoutUrl.searchParams.set("retUrl", logoutPage.pathname + logoutPage.search);
|
|
1444
|
+
this.#loIFrame.load(logoutUrl.href);
|
|
1445
|
+
// (This next piece of code should be removed after making sure `redirect-uri` works well)
|
|
1446
|
+
setTimeout(() => {
|
|
1447
|
+
this.dispatchEvent(new CustomEvent(events.lo.application.logout));
|
|
1448
|
+
}, 3000);
|
|
1449
|
+
}
|
|
1450
|
+
getRouter() {
|
|
1451
|
+
if (this.#loRouter === undefined) {
|
|
1452
|
+
const config = {
|
|
1453
|
+
origin: this.#lwrAppOrigin,
|
|
1454
|
+
lwrPageAuth: this.#lwrPageAuth,
|
|
1455
|
+
lwrAppAuth: this.#lwrAppAuth,
|
|
1456
|
+
lwrAppComp: this.#lwrAppComp,
|
|
1457
|
+
lwrAppAura: this.#lwrAppAura,
|
|
1458
|
+
sitePrefix: this.#sitePrefix,
|
|
1459
|
+
lang: this.#lang,
|
|
1460
|
+
appId: this.#appId,
|
|
1461
|
+
testMode: this.__testMode || "debug" === "test",
|
|
1462
|
+
loAppOrigin: window.location.origin,
|
|
1463
|
+
designSystem: this.#designSystem,
|
|
1464
|
+
globalStyle: this.#globalStyle,
|
|
1465
|
+
};
|
|
1466
|
+
this.#loRouter = new LightningOutRouter(config, (msg) => this.#loError.create(msg));
|
|
1467
|
+
}
|
|
1468
|
+
return this.#loRouter;
|
|
1469
|
+
}
|
|
1470
|
+
_getComponentURL(componentName, parentElementId) {
|
|
1471
|
+
return this.getRouter().getComponentURL(componentName, parentElementId);
|
|
1472
|
+
}
|
|
1473
|
+
#getAuthURL() {
|
|
1474
|
+
return this.getRouter().getAuthURL(this._uuid);
|
|
1475
|
+
}
|
|
1476
|
+
#getPageURL(pagePathname) {
|
|
1477
|
+
return this.getRouter().getPageURL(pagePathname, this._uuid);
|
|
1478
|
+
}
|
|
1479
|
+
#iframeLoaded = (event) => {
|
|
1480
|
+
// Set our internal flag
|
|
1481
|
+
this._ready = true;
|
|
1482
|
+
// The origin where the iframe landed might be different than the origin used to load the
|
|
1483
|
+
// frontdoor url, so update its value.
|
|
1484
|
+
this.#lwrAppOrigin = event.detail;
|
|
1485
|
+
// Initiate registered components
|
|
1486
|
+
this.#initComponents();
|
|
1487
|
+
// Notify the user that the application session is ready
|
|
1488
|
+
this.dispatchEvent(new CustomEvent(events.lo.application.ready));
|
|
1489
|
+
};
|
|
1490
|
+
#iframeError = (event) => {
|
|
1491
|
+
// Notify the user that the application session has failed
|
|
1492
|
+
this.#loError.dispatch(events.lo.application.error, event);
|
|
1493
|
+
};
|
|
1494
|
+
#iframeLogout = (event) => {
|
|
1495
|
+
// Notify the user that the application session is terminated
|
|
1496
|
+
this.#loError.dispatch(events.lo.application.logout, event);
|
|
1497
|
+
};
|
|
1498
|
+
#iframeAuthRedirect = (event) => {
|
|
1499
|
+
// Notify the user that an authorization redirect was issued, and pass the redirectUrl and redirectOrigin
|
|
1500
|
+
this.dispatchEvent(new CustomEvent(events.lo.application.auth.redirect, { detail: event.detail }));
|
|
1501
|
+
};
|
|
1502
|
+
#initComponents() {
|
|
1503
|
+
const myComps = registry.getComps(this);
|
|
1504
|
+
myComps.forEach((comp) => {
|
|
1505
|
+
comp._init();
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
#propObserverCallback = (changes) => {
|
|
1509
|
+
// If multiple attrs/props are being changed at the same time, process the ones that don't trigger
|
|
1510
|
+
// initialization first, and the ones that trigger initialization last.
|
|
1511
|
+
const changesFirst = {};
|
|
1512
|
+
const changesLast = {};
|
|
1513
|
+
Object.keys(changes).forEach((prop) => {
|
|
1514
|
+
if (INIT_TRIGGERING_PROPS.has(prop)) {
|
|
1515
|
+
changesLast[prop] = changes[prop];
|
|
1516
|
+
}
|
|
1517
|
+
else {
|
|
1518
|
+
changesFirst[prop] = changes[prop];
|
|
1519
|
+
}
|
|
1520
|
+
});
|
|
1521
|
+
processPropertyChanges(this, changesFirst);
|
|
1522
|
+
processPropertyChanges(this, changesLast);
|
|
1523
|
+
};
|
|
1524
|
+
#shouldObserveCallback = (propName, isStandard) => {
|
|
1525
|
+
if (isStandard) {
|
|
1526
|
+
if (propName === "lang") {
|
|
1527
|
+
return true;
|
|
1528
|
+
}
|
|
1529
|
+
return false;
|
|
1530
|
+
}
|
|
1531
|
+
else {
|
|
1532
|
+
return !propName.startsWith("_");
|
|
1533
|
+
}
|
|
1534
|
+
};
|
|
1535
|
+
_propertyChanged_lwrApplication = (lwrApplication) => {
|
|
1536
|
+
if (lwrApplication !== undefined) {
|
|
1537
|
+
const parts = lwrApplication.split("/");
|
|
1538
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
1539
|
+
throw this.#loError.create(`"${lwrApplication}" is not a valid lwr-application name, must be of the form 'namespace/name'`);
|
|
1540
|
+
}
|
|
1541
|
+
this.#lwrAppComp = lwrApplication;
|
|
1542
|
+
}
|
|
1543
|
+
};
|
|
1544
|
+
_propertyChanged_lang = (lang) => {
|
|
1545
|
+
this.#lang = lang ?? "";
|
|
1546
|
+
};
|
|
1547
|
+
_propertyChanged_orgUrl = (orgUrl) => {
|
|
1548
|
+
if (orgUrl !== undefined) {
|
|
1549
|
+
if (this.#frontdoorUrl !== undefined) {
|
|
1550
|
+
throw this.#loError.create(`Can't set "org-url" because "frontdoor-url" is already set`);
|
|
1551
|
+
}
|
|
1552
|
+
if (orgUrl === "") {
|
|
1553
|
+
this.#logout();
|
|
1554
|
+
}
|
|
1555
|
+
else {
|
|
1556
|
+
this.#access(orgUrl);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
};
|
|
1560
|
+
_propertyChanged_frontdoorUrl = (frontdoorUrl) => {
|
|
1561
|
+
if (frontdoorUrl !== undefined) {
|
|
1562
|
+
if (this.#orgUrl !== undefined) {
|
|
1563
|
+
throw this.#loError.create(`Can't set "frontdoor-url" because "org-url" is already set`);
|
|
1564
|
+
}
|
|
1565
|
+
if (frontdoorUrl === "") {
|
|
1566
|
+
this.#logout();
|
|
1567
|
+
}
|
|
1568
|
+
else {
|
|
1569
|
+
this.#login(frontdoorUrl);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
};
|
|
1573
|
+
_propertyChanged_sitePrefix = (sitePrefix) => {
|
|
1574
|
+
if (sitePrefix !== undefined) {
|
|
1575
|
+
this.#sitePrefix = sitePrefix;
|
|
1576
|
+
}
|
|
1577
|
+
};
|
|
1578
|
+
_propertyChanged_appId = (appId) => {
|
|
1579
|
+
if (appId !== undefined) {
|
|
1580
|
+
this.#appId = appId;
|
|
1581
|
+
}
|
|
1582
|
+
};
|
|
1583
|
+
_propertyChanged_globalStyle = (globalStyle) => {
|
|
1584
|
+
if (globalStyle !== undefined) {
|
|
1585
|
+
// Validate that all properties are CSS custom properties (start with --)
|
|
1586
|
+
// and normalize by removing unnecessary whitespace
|
|
1587
|
+
const properties = globalStyle
|
|
1588
|
+
.split(";")
|
|
1589
|
+
.map((prop) => prop.trim())
|
|
1590
|
+
.filter((prop) => prop.length > 0);
|
|
1591
|
+
const normalizedProperties = [];
|
|
1592
|
+
for (const property of properties) {
|
|
1593
|
+
const [key, ...valueParts] = property.split(":");
|
|
1594
|
+
const trimmedKey = key.trim();
|
|
1595
|
+
// Validate CSS custom property
|
|
1596
|
+
if (!trimmedKey.startsWith("--")) {
|
|
1597
|
+
throw this.#loError.create(`Invalid global-style: "${trimmedKey}" is not a CSS custom property. Only CSS custom properties (starting with --) are allowed.`);
|
|
1598
|
+
}
|
|
1599
|
+
// Normalize: remove extra whitespace
|
|
1600
|
+
const trimmedValue = valueParts.join(":").trim();
|
|
1601
|
+
if (trimmedValue) {
|
|
1602
|
+
normalizedProperties.push(`${trimmedKey}:${trimmedValue}`);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
// Store normalized version (removes unnecessary whitespace and keeps trailing semicolon)
|
|
1606
|
+
this.#globalStyle = normalizedProperties.join(";") + ";";
|
|
1607
|
+
}
|
|
1608
|
+
};
|
|
1609
|
+
/**
|
|
1610
|
+
* components is a comma separated list of names with optional aliases. The names can be standard (i.e. foo/bar) or
|
|
1611
|
+
* kebab (i.e. foo-bar). It is recommended to use standard names because that's what the backend runtime requires,
|
|
1612
|
+
* and converting from kebab name to standard name can be ambiguous. (W-19562914)
|
|
1613
|
+
*
|
|
1614
|
+
* Example: components = "foo/bar, baz-qux, c/myComp as alias-for-my-comp, other-comp as other-comp-alias"
|
|
1615
|
+
*/
|
|
1616
|
+
_propertyChanged_components = (components) => {
|
|
1617
|
+
// Ignore undefined initialization value
|
|
1618
|
+
if (components === undefined) {
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
components.split(",").forEach((comp) => {
|
|
1622
|
+
const [name, alias] = comp.split(" as ").map((n) => n.trim());
|
|
1623
|
+
// Register the component name and its optional alias. This callback may be called
|
|
1624
|
+
// multiple times, so if a component name is already registered by this application
|
|
1625
|
+
// instance, just skip it, so only register new names.
|
|
1626
|
+
const isStandard = isStandardName(name);
|
|
1627
|
+
// Disallow Aura-style names (namespace:name) in components; Aura is indicated via the "aura"
|
|
1628
|
+
// attribute on the component element instead.
|
|
1629
|
+
if (isStandard && name.includes(":")) {
|
|
1630
|
+
throw this.#loError.create(`"${name}" is not supported in components. Use kebab "namespace-name" in components and the "aura" attribute on the component element instead.`);
|
|
1631
|
+
}
|
|
1632
|
+
const standardName = isStandard ? name : elementNameToStandardName(name);
|
|
1633
|
+
const localName = alias || (isStandard ? standardNameToElementName(standardName) : name);
|
|
1634
|
+
if (localName && !this.#compNames.has(localName)) {
|
|
1635
|
+
this.#compNames.set(localName, standardName);
|
|
1636
|
+
try {
|
|
1637
|
+
registry.registerComponentName(localName, this);
|
|
1638
|
+
// Use a new anonymous class for each definition, extending our base class
|
|
1639
|
+
customElements.define(localName, class extends LightningOutComponent {
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
catch (err) {
|
|
1643
|
+
// This name is already registered by another application.
|
|
1644
|
+
throw this.#loError.create(`"${localName}" is already registered. ${err}`);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
});
|
|
1648
|
+
};
|
|
1649
|
+
_propertyChanged_designSystem = (designSystem) => {
|
|
1650
|
+
if (designSystem !== undefined && !DESIGN_SYSTEM_VALUES.has(designSystem)) {
|
|
1651
|
+
throw this.#loError.create(`Invalid design-system: ${designSystem}`);
|
|
1652
|
+
}
|
|
1653
|
+
this.#designSystem = designSystem;
|
|
1654
|
+
};
|
|
1655
|
+
_getComponentStandardName(component) {
|
|
1656
|
+
const localName = component.localName;
|
|
1657
|
+
const isAura = component.hasAttribute("aura");
|
|
1658
|
+
const standardName = this.#compNames.get(localName);
|
|
1659
|
+
if (!standardName) {
|
|
1660
|
+
throw this.#loError.create(`"${localName}" is not registered.`);
|
|
1661
|
+
}
|
|
1662
|
+
if (isAura) {
|
|
1663
|
+
// If this is an Aura component (has aura attribute) and the stored name is in LWC format (c/name),
|
|
1664
|
+
// convert it to Aura format (c:name)
|
|
1665
|
+
if (standardName.includes("/")) {
|
|
1666
|
+
return standardName.replace("/", ":");
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
return standardName;
|
|
1670
|
+
}
|
|
1671
|
+
get _compNames() {
|
|
1672
|
+
}
|
|
1673
|
+
connectedCallback() {
|
|
1674
|
+
logger.trace("connectedCallback: called", `_uuid: ${this._uuid}`);
|
|
1675
|
+
if (this.hasChildNodes()) {
|
|
1676
|
+
throw this.#loError.create("Should not have child nodes");
|
|
1677
|
+
}
|
|
1678
|
+
// Apply default styles
|
|
1679
|
+
this.style.display ||= "none";
|
|
1680
|
+
// Add event listeners
|
|
1681
|
+
this.addEventListener(events.lo.iframe.load, this.#iframeLoaded);
|
|
1682
|
+
this.addEventListener(events.lo.iframe.error, this.#iframeError);
|
|
1683
|
+
this.addEventListener(events.lo.iframe.logout, this.#iframeLogout);
|
|
1684
|
+
this.addEventListener(events.lo.iframe.auth.redirect, this.#iframeAuthRedirect);
|
|
1685
|
+
// Start PropertyObserver
|
|
1686
|
+
this.#propObserver = new PropertyObserver(this, this.#propObserverCallback, this.#shouldObserveCallback);
|
|
1687
|
+
}
|
|
1688
|
+
disconnectedCallback() {
|
|
1689
|
+
logger.trace("disconnectedCallback: called", `_uuid: ${this._uuid}`);
|
|
1690
|
+
// Since disconnectedCallback is called after the DOM has been removed, calling logout and destroying the iframe
|
|
1691
|
+
// here may be a moot point, but doing it for completeness.
|
|
1692
|
+
this.#logout();
|
|
1693
|
+
this.#loIFrame.destroy();
|
|
1694
|
+
// Remove event listeners
|
|
1695
|
+
this.removeEventListener(events.lo.iframe.load, this.#iframeLoaded);
|
|
1696
|
+
this.removeEventListener(events.lo.iframe.error, this.#iframeError);
|
|
1697
|
+
this.removeEventListener(events.lo.iframe.logout, this.#iframeLogout);
|
|
1698
|
+
this.removeEventListener(events.lo.iframe.auth.redirect, this.#iframeAuthRedirect);
|
|
1699
|
+
// Stop PropertyObserver
|
|
1700
|
+
this.#propObserver?.disconnect();
|
|
1701
|
+
}
|
|
1702
|
+
connectedMoveCallback() {
|
|
1703
|
+
// Define this callback but do nothing
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
/**
|
|
1708
|
+
* Core functionality for Lightning Out 2.0
|
|
1709
|
+
*/
|
|
1710
|
+
Logger.level = "debug";
|
|
1711
|
+
|
|
1712
|
+
/**
|
|
1713
|
+
* @salesforce/lightning-out
|
|
1714
|
+
*
|
|
1715
|
+
* Main entry point for Lightning Out 2.0
|
|
1716
|
+
*/
|
|
1717
|
+
window.customElements.define("lightning-out-application", LightningOutApplication);
|
|
1718
|
+
|
|
1719
|
+
exports.LightningOutApplication = LightningOutApplication;
|
|
1720
|
+
|
|
1721
|
+
return exports;
|
|
1722
|
+
|
|
1723
|
+
})({});
|
|
1724
|
+
//# sourceMappingURL=index.iife.debug.js.map
|