@4site/engrid-scripts 0.25.1 → 0.25.5

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,658 @@
1
+ /**
2
+ * Iframe Queue — load embedded EN pages sequentially.
3
+ *
4
+ * **This component is opt-in.** Like `OptInLadder`, it is exported from
5
+ * `@4site/engrid-scripts` but is **not** auto-constructed by ENgrid's
6
+ * core bootstrap (`app.ts`). To use it, instantiate it once in your
7
+ * theme's bootstrap:
8
+ *
9
+ * ```ts
10
+ * import { IframeQueue } from "@4site/engrid-scripts";
11
+ * new IframeQueue();
12
+ * ```
13
+ *
14
+ * On client themes that don't use this component, **nothing in this
15
+ * file runs**: no `message` listener is registered, no singleton is
16
+ * allocated, no bundle code beyond the unused class definition.
17
+ *
18
+ * **Why this exists.** Engaging Networks' platform handles concurrent
19
+ * iframe submissions inconsistently — when several embedded EN forms
20
+ * are submitted in parallel (e.g. QCB opt-ins for postal mail, mobile
21
+ * phone, and double opt-in email), roughly 40% of records are lost.
22
+ * Loading the iframes sequentially (without `?chain`) resolves the
23
+ * issue. This component generalises that pattern.
24
+ *
25
+ * **What it does.** In _parent_ mode (top-level page) it holds an
26
+ * ordered queue of {@link IframeQueueItem} configs and processes them
27
+ * one at a time: create iframe → wait for `load` → post a populate
28
+ * message with field values → wait for the embedded page to reach a
29
+ * Thank You page → advance. In _embedded_ mode (running inside an
30
+ * iframe owned by an IframeQueue parent) it listens for the populate
31
+ * message, fills the form fields via {@link ENGrid.setFieldValue}, and
32
+ * submits via {@link EnForm.submitForm} when `autoSubmit` is true.
33
+ *
34
+ * **Why not `?chain`?** Engaging Networks' `?chain` URL parameter is
35
+ * unreliable for sequential iframe submission; the agreed solution is
36
+ * to pass field data via `postMessage` instead. The queue defensively
37
+ * strips any `chain` query parameter from queued URLs.
38
+ *
39
+ * **Page ID matching.** The Thank-You-page ping (sent by the iFrame
40
+ * component, see iframe.ts) carries the Page ID of the submitting
41
+ * form. The queue compares it against the Page ID parsed from the
42
+ * queued URL so that pings from unrelated EN iframes on the same
43
+ * parent page (such as an Embedded Ecard iframe) are ignored.
44
+ *
45
+ * **Events.** Lifecycle events are dispatched via the
46
+ * {@link IframeQueueEvents} singleton. External code subscribes there
47
+ * rather than holding a reference to the queue itself.
48
+ *
49
+ * @example Programmatic API
50
+ * const queue = IframeQueue.getInstance();
51
+ * queue.enqueue({
52
+ * url: "https://example.org/page/123/data/1",
53
+ * fields: { "supporter.emailAddress": "donor@example.org" },
54
+ * autoSubmit: true,
55
+ * });
56
+ * queue.process().then(() => console.log("done"));
57
+ *
58
+ * @example Declarative API (set on the EN page before the bundle loads)
59
+ * window.EngridIframeQueue = {
60
+ * items: [
61
+ * { url: "https://example.org/page/123/data/1",
62
+ * fields: { "supporter.emailAddress": "donor@example.org" } },
63
+ * ],
64
+ * autoStart: true,
65
+ * };
66
+ */
67
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
68
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
69
+ return new (P || (P = Promise))(function (resolve, reject) {
70
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
71
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
72
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
73
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
74
+ });
75
+ };
76
+ import { ENGrid, EngridLogger } from ".";
77
+ import { EnForm, IframeQueueEvents } from "./events";
78
+ import { IframeQueueOptionsDefaults, } from "./interfaces/iframe-queue-options";
79
+ /** Wire-format type for the populate message sent parent → iframe. */
80
+ const MSG_POPULATE = "engrid-iframe-queue:populate";
81
+ /** Wire-format type for the Thank-You-page ping sent iframe → parent. */
82
+ const MSG_THANK_YOU = "engrid-iframe-queue:thank-you";
83
+ /** Wire-format type for an error message sent iframe → parent. */
84
+ const MSG_ERROR = "engrid-iframe-queue:error";
85
+ /** Default per-item timeout in milliseconds. */
86
+ const DEFAULT_TIMEOUT_MS = 30000;
87
+ /**
88
+ * Parameters that are automatically inherited from the parent page
89
+ * onto each queued iframe URL. These are all ENgrid loader / dev-mode
90
+ * flags — adding them to the parent is meant to affect "the ENgrid
91
+ * bundle running on this browser tab," which conceptually includes
92
+ * the embedded forms loaded by the queue.
93
+ *
94
+ * For each key, the value is resolved with the same precedence used by
95
+ * `loader.ts#getOption`:
96
+ * 1. The item's own URL — if the consumer hard-coded the param on
97
+ * the iframe URL, that wins.
98
+ * 2. The parent page's URL parameter (`?assets=local`).
99
+ * 3. `window.EngridLoader[key]` on the parent page — useful when EN
100
+ * strips URL params on the Thank You page, so themes set
101
+ * `<script>window.EngridLoader = { assets: 'local' };</script>`
102
+ * to pin the bundle source.
103
+ *
104
+ * Notable use case: any of the three works for forcing local-asset
105
+ * loading on every queued QCB iframe during testing.
106
+ */
107
+ const PROPAGATED_PARENT_PARAMS = [
108
+ "assets",
109
+ "engridjs",
110
+ "engridcss",
111
+ "repo-name",
112
+ "repo-owner",
113
+ "debug",
114
+ "mode",
115
+ ];
116
+ /** Default visually-hidden style for queue iframes. */
117
+ const DEFAULT_HIDDEN_STYLE = {
118
+ position: "absolute",
119
+ width: "1px",
120
+ height: "1px",
121
+ left: "-9999px",
122
+ top: "0",
123
+ opacity: "0",
124
+ border: "0",
125
+ };
126
+ export class IframeQueue {
127
+ /**
128
+ * Returns the shared IframeQueue singleton. The bootstrap in app.ts
129
+ * instantiates this once via `new IframeQueue()`, but consumers that
130
+ * need to enqueue items programmatically should always go through
131
+ * `getInstance()` so they share the same queue state.
132
+ */
133
+ static getInstance() {
134
+ if (!IframeQueue.instance) {
135
+ IframeQueue.instance = new IframeQueue();
136
+ }
137
+ return IframeQueue.instance;
138
+ }
139
+ constructor() {
140
+ this.logger = new EngridLogger("IframeQueue", "white", "#1f6feb", "🚂");
141
+ this.events = IframeQueueEvents.getInstance();
142
+ this._form = EnForm.getInstance();
143
+ this.queue = [];
144
+ this._isProcessing = false;
145
+ this._aborted = false;
146
+ this.inFlightPromise = null;
147
+ // Singleton guard: if called via `new IframeQueue()` after an
148
+ // instance already exists (e.g. by app.ts), return the existing
149
+ // instance so behaviour stays consistent with `getInstance()`.
150
+ if (IframeQueue.instance) {
151
+ return IframeQueue.instance;
152
+ }
153
+ IframeQueue.instance = this;
154
+ if (this.inIframe()) {
155
+ this.setupEmbeddedMode();
156
+ }
157
+ else {
158
+ this.setupParentMode();
159
+ }
160
+ }
161
+ // ---------------------------------------------------------------------------
162
+ // Public API (parent mode)
163
+ // ---------------------------------------------------------------------------
164
+ /** Whether the queue is currently processing. */
165
+ get isProcessing() {
166
+ return this._isProcessing;
167
+ }
168
+ /** Number of items currently in the queue (not counting the in-flight item). */
169
+ get size() {
170
+ return this.queue.length;
171
+ }
172
+ /**
173
+ * Add an item to the back of the queue. Items are processed in
174
+ * insertion order. Calling `enqueue` while the queue is processing is
175
+ * supported — the new item joins the chain and will be picked up
176
+ * after the current item completes.
177
+ */
178
+ enqueue(item) {
179
+ if (!item || typeof item.url !== "string" || !item.url) {
180
+ this.logger.danger("enqueue() called with invalid item; ignoring");
181
+ return;
182
+ }
183
+ this.queue.push(item);
184
+ this.logger.log(`enqueue: ${item.url} (queue size = ${this.queue.length})`);
185
+ }
186
+ /**
187
+ * Add many items at once, preserving order. Equivalent to calling
188
+ * {@link enqueue} repeatedly.
189
+ */
190
+ enqueueAll(items) {
191
+ if (!Array.isArray(items))
192
+ return;
193
+ for (const item of items)
194
+ this.enqueue(item);
195
+ }
196
+ /**
197
+ * Begin processing the queue. Resolves when the queue drains
198
+ * successfully and rejects on the first error. If already processing,
199
+ * returns the in-flight promise so callers don't start a second drain.
200
+ */
201
+ process() {
202
+ if (this._isProcessing && this.inFlightPromise) {
203
+ this.logger.log("process: already processing; returning in-flight promise");
204
+ return this.inFlightPromise;
205
+ }
206
+ if (this.queue.length === 0) {
207
+ this.logger.log("process: queue empty; nothing to do");
208
+ return Promise.resolve();
209
+ }
210
+ this._aborted = false;
211
+ this._isProcessing = true;
212
+ this.inFlightPromise = this.drain()
213
+ .then(() => {
214
+ this.events.dispatchChainComplete();
215
+ })
216
+ .finally(() => {
217
+ this._isProcessing = false;
218
+ this.inFlightPromise = null;
219
+ });
220
+ return this.inFlightPromise;
221
+ }
222
+ /**
223
+ * Empty the queue without processing. Stops the in-flight item if
224
+ * any (the in-flight item rejects with an abort error which is
225
+ * surfaced via `onChainError`).
226
+ */
227
+ clear() {
228
+ this.logger.log(`clear: dropping ${this.queue.length} queued item(s)`);
229
+ this.queue = [];
230
+ this._aborted = true;
231
+ }
232
+ // ---------------------------------------------------------------------------
233
+ // Parent-mode internals
234
+ // ---------------------------------------------------------------------------
235
+ /**
236
+ * In parent mode the constructor checks `window.EngridIframeQueue`
237
+ * for declarative startup config, enqueues those items, and (if
238
+ * `autoStart` is true) calls `process()` after DOMContentLoaded.
239
+ */
240
+ setupParentMode() {
241
+ this.logger.log("setupParentMode");
242
+ const config = this.readWindowConfig();
243
+ if (!config)
244
+ return;
245
+ if (Array.isArray(config.items) && config.items.length > 0) {
246
+ this.enqueueAll(config.items);
247
+ }
248
+ const shouldAutoStart = typeof config.autoStart === "boolean"
249
+ ? config.autoStart
250
+ : this.queue.length > 0;
251
+ if (!shouldAutoStart || this.queue.length === 0)
252
+ return;
253
+ const start = () => {
254
+ this.process().catch((err) => {
255
+ this.logger.danger(`Auto-started queue rejected: ${err}`);
256
+ });
257
+ };
258
+ if (document.readyState !== "loading") {
259
+ start();
260
+ }
261
+ else {
262
+ document.addEventListener("DOMContentLoaded", start);
263
+ }
264
+ }
265
+ /**
266
+ * Reads `window.EngridIframeQueue` and returns merged options, or
267
+ * null if no valid config is present.
268
+ */
269
+ readWindowConfig() {
270
+ const raw = window
271
+ .EngridIframeQueue;
272
+ if (!raw || typeof raw !== "object")
273
+ return null;
274
+ return Object.assign(Object.assign({}, IframeQueueOptionsDefaults), raw);
275
+ }
276
+ /** Process queued items strictly one at a time. */
277
+ drain() {
278
+ var _a;
279
+ return __awaiter(this, void 0, void 0, function* () {
280
+ while (this.queue.length > 0) {
281
+ if (this._aborted) {
282
+ this.logger.log("drain: aborted; stopping");
283
+ return;
284
+ }
285
+ const item = this.queue.shift();
286
+ try {
287
+ yield this.processItem(item);
288
+ }
289
+ catch (err) {
290
+ const error = err instanceof Error ? err : new Error(String(err));
291
+ this.events.dispatchItemError(item, error);
292
+ try {
293
+ (_a = item.onError) === null || _a === void 0 ? void 0 : _a.call(item, error);
294
+ }
295
+ catch (cbErr) {
296
+ this.logger.danger(`onError callback threw: ${cbErr}`);
297
+ }
298
+ this.events.dispatchChainError({
299
+ message: error.message,
300
+ failedItem: item,
301
+ cause: error,
302
+ });
303
+ // Abort the rest of the chain.
304
+ this.queue = [];
305
+ throw error;
306
+ }
307
+ }
308
+ });
309
+ }
310
+ /**
311
+ * Process a single item: create the iframe, post populate, wait for
312
+ * the matching Thank-You ping (or error/timeout). Resolves on success
313
+ * and rejects on error/timeout.
314
+ */
315
+ processItem(item) {
316
+ return new Promise((resolve, reject) => {
317
+ var _a, _b;
318
+ const url = this.prepareIframeUrl(item.url);
319
+ const expectedPageId = ENGrid.getPageIdFromUrl(url);
320
+ if (!expectedPageId) {
321
+ reject(new Error(`IframeQueue: could not parse Page ID from URL "${item.url}".`));
322
+ return;
323
+ }
324
+ this.events.dispatchItemStart(item);
325
+ const container = (_a = item.container) !== null && _a !== void 0 ? _a : document.body;
326
+ const iframe = this.createIframe(url, item.iframeStyle);
327
+ const timeoutMs = (_b = item.timeout) !== null && _b !== void 0 ? _b : DEFAULT_TIMEOUT_MS;
328
+ let settled = false;
329
+ let timeoutId = null;
330
+ const detachListeners = () => {
331
+ if (timeoutId !== null) {
332
+ window.clearTimeout(timeoutId);
333
+ timeoutId = null;
334
+ }
335
+ window.removeEventListener("message", onMessage);
336
+ iframe.removeEventListener("load", onIframeLoad);
337
+ iframe.removeEventListener("error", onIframeError);
338
+ };
339
+ const removeIframe = () => {
340
+ if (iframe.parentNode) {
341
+ iframe.parentNode.removeChild(iframe);
342
+ }
343
+ };
344
+ const succeed = () => {
345
+ var _a;
346
+ if (settled)
347
+ return;
348
+ settled = true;
349
+ detachListeners();
350
+ removeIframe();
351
+ this.events.dispatchItemComplete(item);
352
+ try {
353
+ (_a = item.onComplete) === null || _a === void 0 ? void 0 : _a.call(item);
354
+ }
355
+ catch (cbErr) {
356
+ this.logger.danger(`onComplete callback threw: ${cbErr}`);
357
+ }
358
+ resolve();
359
+ };
360
+ const fail = (error) => {
361
+ if (settled)
362
+ return;
363
+ settled = true;
364
+ detachListeners();
365
+ if (this.shouldKeepIframeOnError(item)) {
366
+ this.markIframeFailed(iframe, error);
367
+ this.logger.danger(`Item failed — iframe kept in DOM for inspection: ${error.message}`);
368
+ }
369
+ else {
370
+ removeIframe();
371
+ }
372
+ reject(error);
373
+ };
374
+ const onMessage = (event) => {
375
+ var _a;
376
+ // Only accept messages from this specific iframe — origin
377
+ // string matching is unreliable because EN may serve embedded
378
+ // pages from different subdomains. `event.source` identity is
379
+ // what matters here.
380
+ if (event.source !== iframe.contentWindow)
381
+ return;
382
+ const data = event.data;
383
+ if (!data || typeof data !== "object" || !data.type)
384
+ return;
385
+ if (data.type === MSG_THANK_YOU) {
386
+ if (data.pageId !== expectedPageId) {
387
+ this.logger.log(`Ignoring thank-you ping with mismatched pageId ` +
388
+ `(expected ${expectedPageId}, got ${data.pageId})`);
389
+ return;
390
+ }
391
+ this.logger.log(`Item complete: ${url} (pageId ${expectedPageId})`);
392
+ succeed();
393
+ }
394
+ else if (data.type === MSG_ERROR) {
395
+ if (data.pageId !== expectedPageId)
396
+ return;
397
+ fail(new Error(`IframeQueue: embedded page reported error: ${(_a = data.message) !== null && _a !== void 0 ? _a : "unknown error"}`));
398
+ }
399
+ };
400
+ const onIframeLoad = () => {
401
+ var _a, _b;
402
+ if (settled)
403
+ return;
404
+ const populate = {
405
+ type: MSG_POPULATE,
406
+ pageId: expectedPageId,
407
+ fields: (_a = item.fields) !== null && _a !== void 0 ? _a : {},
408
+ autoSubmit: item.autoSubmit !== false, // default true
409
+ };
410
+ this.logger.log(`Posting populate to iframe (pageId=${expectedPageId}, ` +
411
+ `fieldCount=${Object.keys(populate.fields).length}, ` +
412
+ `autoSubmit=${populate.autoSubmit})`);
413
+ // Use "*" for the same reason origin matching is skipped on
414
+ // inbound messages — EN may serve embedded pages from a
415
+ // different subdomain than the host page.
416
+ (_b = iframe.contentWindow) === null || _b === void 0 ? void 0 : _b.postMessage(populate, "*");
417
+ };
418
+ const onIframeError = () => {
419
+ fail(new Error(`IframeQueue: iframe failed to load: ${url}`));
420
+ };
421
+ window.addEventListener("message", onMessage);
422
+ iframe.addEventListener("load", onIframeLoad);
423
+ iframe.addEventListener("error", onIframeError);
424
+ timeoutId = window.setTimeout(() => {
425
+ fail(new Error(`IframeQueue: timed out after ${timeoutMs}ms waiting for ` +
426
+ `Thank-You-page ping from ${url}`));
427
+ }, timeoutMs);
428
+ this.logger.log(`Item start: ${url} (pageId ${expectedPageId}, timeout ${timeoutMs}ms)`);
429
+ container.appendChild(iframe);
430
+ });
431
+ }
432
+ /**
433
+ * Normalise the URL for a queued iframe:
434
+ * 1. Strip any `chain` query parameter defensively — the queue
435
+ * replaces `?chain` with sequential processing.
436
+ * 2. Inherit a small allowlist of loader / dev-mode params (see
437
+ * {@link PROPAGATED_PARENT_PARAMS}) when they're not already set
438
+ * on the item URL. Each key is resolved with the same precedence
439
+ * `loader.ts#getOption` uses: parent URL param first, then
440
+ * `window.EngridLoader[key]`.
441
+ *
442
+ * Item-specified params always take precedence over inherited ones.
443
+ * Returns the original string unchanged if URL parsing fails.
444
+ */
445
+ prepareIframeUrl(rawUrl) {
446
+ let url;
447
+ try {
448
+ url = new URL(rawUrl, window.location.href);
449
+ }
450
+ catch (_a) {
451
+ return rawUrl;
452
+ }
453
+ url.searchParams.delete("chain");
454
+ const parentUrlParams = this.getParentSearchParams();
455
+ const parentLoader = this.getParentEngridLoader();
456
+ const inherited = [];
457
+ for (const key of PROPAGATED_PARENT_PARAMS) {
458
+ if (url.searchParams.has(key))
459
+ continue;
460
+ let value = null;
461
+ let source = "";
462
+ if (parentUrlParams) {
463
+ const v = parentUrlParams.get(key);
464
+ if (v !== null) {
465
+ value = v;
466
+ source = "url";
467
+ }
468
+ }
469
+ if (value === null && parentLoader) {
470
+ const v = parentLoader[key];
471
+ if (typeof v === "string" && v !== "") {
472
+ value = v;
473
+ source = "EngridLoader";
474
+ }
475
+ }
476
+ if (value !== null) {
477
+ url.searchParams.set(key, value);
478
+ inherited.push(`${key}=${value} (from parent ${source})`);
479
+ }
480
+ }
481
+ if (inherited.length > 0) {
482
+ this.logger.log(`Inherited parent params on iframe URL: ${inherited.join(", ")}`);
483
+ }
484
+ return url.href;
485
+ }
486
+ /** Returns the parent page's URLSearchParams, or null on failure. */
487
+ getParentSearchParams() {
488
+ try {
489
+ return new URL(window.location.href).searchParams;
490
+ }
491
+ catch (_a) {
492
+ return null;
493
+ }
494
+ }
495
+ /**
496
+ * Returns the parent page's `window.EngridLoader` object if set, or
497
+ * null. Used by {@link prepareIframeUrl} as a fallback source for
498
+ * loader/dev-mode param values when EN has stripped URL parameters
499
+ * from the Thank You page.
500
+ */
501
+ getParentEngridLoader() {
502
+ const w = window;
503
+ if (!w.EngridLoader || typeof w.EngridLoader !== "object")
504
+ return null;
505
+ return w.EngridLoader;
506
+ }
507
+ /**
508
+ * Decide whether to leave a failed iframe in the DOM (for
509
+ * inspection) instead of removing it. True when the item explicitly
510
+ * asks for it via `keepIframeOnError`, OR whenever ENgrid debug
511
+ * mode is on (since debugging is when this is useful and we don't
512
+ * want to make consumers opt in just to inspect failures).
513
+ */
514
+ shouldKeepIframeOnError(item) {
515
+ if (item.keepIframeOnError)
516
+ return true;
517
+ try {
518
+ return ENGrid.debug === true;
519
+ }
520
+ catch (_a) {
521
+ return false;
522
+ }
523
+ }
524
+ /**
525
+ * Reposition and style a failed iframe so it's visible in the
526
+ * viewport (overriding the visually-hidden default), and tag it
527
+ * with a class + tooltip so the developer knows why it's there.
528
+ * Right-click the iframe → Inspect frame to dive in.
529
+ */
530
+ markIframeFailed(iframe, error) {
531
+ Object.assign(iframe.style, {
532
+ position: "fixed",
533
+ top: "10px",
534
+ right: "10px",
535
+ bottom: "auto",
536
+ left: "auto",
537
+ width: "min(600px, 90vw)",
538
+ height: "min(500px, 80vh)",
539
+ opacity: "1",
540
+ zIndex: "99999",
541
+ border: "3px solid #d33",
542
+ background: "white",
543
+ boxShadow: "0 4px 24px rgba(0, 0, 0, 0.25)",
544
+ });
545
+ iframe.classList.add("engrid-iframe--queue-failed");
546
+ iframe.title = `Iframe Queue: failed item — ${error.message}`;
547
+ }
548
+ /** Create a hidden iframe element for a queue item. */
549
+ createIframe(url, styleOverride) {
550
+ const iframe = document.createElement("iframe");
551
+ iframe.setAttribute("src", url);
552
+ iframe.setAttribute("frameborder", "0");
553
+ iframe.setAttribute("scrolling", "no");
554
+ iframe.setAttribute("aria-hidden", "true");
555
+ iframe.setAttribute("title", "ENgrid Iframe Queue");
556
+ iframe.classList.add("engrid-iframe", "engrid-iframe--queue");
557
+ const style = Object.assign(Object.assign({}, DEFAULT_HIDDEN_STYLE), (styleOverride !== null && styleOverride !== void 0 ? styleOverride : {}));
558
+ Object.assign(iframe.style, style);
559
+ return iframe;
560
+ }
561
+ // ---------------------------------------------------------------------------
562
+ // Embedded-mode internals
563
+ // ---------------------------------------------------------------------------
564
+ /**
565
+ * In embedded mode we register a `message` listener that accepts
566
+ * populate messages from `window.parent`, fills form fields, and
567
+ * (optionally) submits. The Thank-You-page ping is sent by the iFrame
568
+ * component (iframe.ts) — not here — so this method does not need to
569
+ * concern itself with completion signalling.
570
+ */
571
+ setupEmbeddedMode() {
572
+ this.logger.log("setupEmbeddedMode");
573
+ window.addEventListener("message", (event) => {
574
+ if (event.source !== window.parent)
575
+ return;
576
+ const data = event.data;
577
+ if (!data || typeof data !== "object" || data.type !== MSG_POPULATE) {
578
+ return;
579
+ }
580
+ this.handlePopulate(data);
581
+ });
582
+ }
583
+ /** Handle a populate message sent by an IframeQueue parent. */
584
+ handlePopulate(data) {
585
+ var _a, _b;
586
+ const fields = (_a = data.fields) !== null && _a !== void 0 ? _a : {};
587
+ const autoSubmit = data.autoSubmit !== false;
588
+ this.logger.log(`Received populate (pageId=${data.pageId}, ` +
589
+ `fieldCount=${Object.keys(fields).length}, autoSubmit=${autoSubmit})`);
590
+ try {
591
+ for (const [name, value] of Object.entries(fields)) {
592
+ // Pass `dispatchEvents = true` so each field fires
593
+ // `change` + `blur` after the value is set. Without that,
594
+ // EN's form-validation state machine doesn't see the new
595
+ // values and leaves `en__submit--disabled` on the submit
596
+ // button, causing the auto-click below to no-op.
597
+ ENGrid.setFieldValue(name, value, true, true);
598
+ }
599
+ if (autoSubmit) {
600
+ // Defer slightly so any synchronous EN dependency parsing in
601
+ // setFieldValue settles before the form is submitted.
602
+ window.setTimeout(() => {
603
+ // Belt-and-braces: clear EN's "submit disabled" state in
604
+ // case its validators didn't re-evaluate (e.g. async
605
+ // validators that hadn't completed when the events fired).
606
+ this.forceEnableSubmitButton();
607
+ this._form.submitForm();
608
+ }, 0);
609
+ }
610
+ }
611
+ catch (err) {
612
+ const error = err instanceof Error ? err : new Error(String(err));
613
+ this.logger.danger(`handlePopulate failed: ${error.message}`);
614
+ window.parent.postMessage({
615
+ type: MSG_ERROR,
616
+ pageId: (_b = data.pageId) !== null && _b !== void 0 ? _b : ENGrid.getPageID(),
617
+ message: error.message,
618
+ }, "*");
619
+ }
620
+ }
621
+ /**
622
+ * Strip every "disabled" marker from the EN submit button so the
623
+ * programmatic `submitForm()` click is honoured. Removes:
624
+ * - the `disabled` DOM property/attribute on the button,
625
+ * - the `en__submit--disabled` BEM modifier (EN's own class),
626
+ * - the `en__submit--disabled` modifier on the `.en__submit`
627
+ * wrapper (some templates style the wrapper instead),
628
+ * - ENgrid's own loader markup if a previous `disableSubmit()`
629
+ * call left it in place.
630
+ *
631
+ * Used only by embedded-mode populate flow when `autoSubmit` is on.
632
+ */
633
+ forceEnableSubmitButton() {
634
+ const button = document.querySelector("form .en__submit button");
635
+ if (button) {
636
+ if (button.disabled)
637
+ button.disabled = false;
638
+ button.removeAttribute("disabled");
639
+ button.classList.remove("en__submit--disabled");
640
+ }
641
+ const wrapper = document.querySelector(".en__submit");
642
+ if (wrapper) {
643
+ wrapper.classList.remove("en__submit--disabled");
644
+ }
645
+ }
646
+ // ---------------------------------------------------------------------------
647
+ // Helpers
648
+ // ---------------------------------------------------------------------------
649
+ /** True when this script is executing inside an iframe. */
650
+ inIframe() {
651
+ try {
652
+ return window.self !== window.top;
653
+ }
654
+ catch (_a) {
655
+ return true;
656
+ }
657
+ }
658
+ }
package/dist/iframe.d.ts CHANGED
@@ -4,6 +4,22 @@ export declare class iFrame {
4
4
  private logger;
5
5
  constructor();
6
6
  private onLoaded;
7
+ /**
8
+ * Posts a `engrid-iframe-queue:thank-you` message to the parent window
9
+ * when the embedded EN page reaches its Thank You page (the last page
10
+ * in the page sequence). Carries the Page ID of the submitting form so
11
+ * the IframeQueue parent can match the ping against the queued item it
12
+ * is waiting on, ignoring pings from unrelated EN iframes that may exist
13
+ * on the same parent page (e.g. an Embedded Ecard iframe).
14
+ *
15
+ * Only fires when:
16
+ * - the script is running inside an iframe (already guaranteed by the
17
+ * code path that calls onLoaded()), AND
18
+ * - the embedded page is a Thank You page (ENGrid.isThankYouPage()).
19
+ *
20
+ * Consumed by: IframeQueue (engrid/packages/scripts/src/iframe-queue.ts).
21
+ */
22
+ private sendIframeQueueThankYouPing;
7
23
  private sendIframeHeight;
8
24
  private sendIframeFormStatus;
9
25
  private getIFrameByEvent;