@4site/engrid-scripts 0.25.1 → 0.25.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/dist/engrid.d.ts +10 -0
- package/dist/engrid.js +18 -0
- package/dist/events/iframe-queue-events.d.ts +62 -0
- package/dist/events/iframe-queue-events.js +90 -0
- package/dist/events/index.d.ts +1 -0
- package/dist/events/index.js +1 -0
- package/dist/iframe-queue.d.ts +197 -0
- package/dist/iframe-queue.js +658 -0
- package/dist/iframe.d.ts +16 -0
- package/dist/iframe.js +34 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/interfaces/iframe-queue-options.d.ts +101 -0
- package/dist/interfaces/iframe-queue-options.js +17 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/engrid.d.ts
CHANGED
|
@@ -15,6 +15,16 @@ export declare abstract class ENGrid {
|
|
|
15
15
|
static getPageNumber(): any;
|
|
16
16
|
static isThankYouPage(): boolean;
|
|
17
17
|
static getPageID(): any;
|
|
18
|
+
/**
|
|
19
|
+
* Parse the numeric Page ID out of a Engaging Networks URL.
|
|
20
|
+
* EN page URLs follow the pattern `https://<host>/page/<PAGE_ID>/<slug>/...`.
|
|
21
|
+
* Used by the Iframe Queue component to match Thank-You-page pings from
|
|
22
|
+
* embedded iframes against the queued URL that was submitted.
|
|
23
|
+
*
|
|
24
|
+
* @param url Full URL string to parse.
|
|
25
|
+
* @returns The numeric Page ID, or 0 if it could not be parsed.
|
|
26
|
+
*/
|
|
27
|
+
static getPageIdFromUrl(url: string): number;
|
|
18
28
|
static getClientID(): any;
|
|
19
29
|
static getDataCenter(): "us" | "ca";
|
|
20
30
|
static getPageType(): "DONATION" | "ECARD" | "SURVEY" | "EMAILTOTARGET" | "ADVOCACY" | "SUBSCRIBEFORM" | "EVENT" | "SUPPORTERHUB" | "UNSUBSCRIBE" | "TWEETPAGE" | "UNKNOWN";
|
package/dist/engrid.js
CHANGED
|
@@ -178,6 +178,24 @@ export class ENGrid {
|
|
|
178
178
|
return window.pageJson.campaignPageId;
|
|
179
179
|
return 0;
|
|
180
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* Parse the numeric Page ID out of a Engaging Networks URL.
|
|
183
|
+
* EN page URLs follow the pattern `https://<host>/page/<PAGE_ID>/<slug>/...`.
|
|
184
|
+
* Used by the Iframe Queue component to match Thank-You-page pings from
|
|
185
|
+
* embedded iframes against the queued URL that was submitted.
|
|
186
|
+
*
|
|
187
|
+
* @param url Full URL string to parse.
|
|
188
|
+
* @returns The numeric Page ID, or 0 if it could not be parsed.
|
|
189
|
+
*/
|
|
190
|
+
static getPageIdFromUrl(url) {
|
|
191
|
+
if (!url)
|
|
192
|
+
return 0;
|
|
193
|
+
const match = url.match(/\/page\/(\d+)(?:\/|$|\?|#)/);
|
|
194
|
+
if (!match)
|
|
195
|
+
return 0;
|
|
196
|
+
const id = parseInt(match[1], 10);
|
|
197
|
+
return Number.isFinite(id) ? id : 0;
|
|
198
|
+
}
|
|
181
199
|
// Return the client ID
|
|
182
200
|
static getClientID() {
|
|
183
201
|
if ("pageJson" in window)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Singleton event hub for the Iframe Queue component.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the structure of RememberMeEvents: private constructor,
|
|
5
|
+
* static `getInstance()`, internal dispatchers exposed via `.asEvent()`
|
|
6
|
+
* getters, and `dispatch*` methods called by the IframeQueue class.
|
|
7
|
+
*
|
|
8
|
+
* External code subscribes to these events to react to queue
|
|
9
|
+
* lifecycle without holding a reference to the IframeQueue itself.
|
|
10
|
+
* The TNC Bequest Lightbox, for example, will subscribe to
|
|
11
|
+
* `onChainComplete` so it only opens after the QCB opt-in chain has
|
|
12
|
+
* finished submitting.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* IframeQueueEvents.getInstance().onChainComplete.subscribe(() => {
|
|
16
|
+
* openBequestLightbox();
|
|
17
|
+
* });
|
|
18
|
+
*/
|
|
19
|
+
import { IframeQueueItem, IframeQueueErrorPayload } from "../interfaces/iframe-queue-options";
|
|
20
|
+
export declare class IframeQueueEvents {
|
|
21
|
+
private logger;
|
|
22
|
+
private _onChainComplete;
|
|
23
|
+
private _onChainError;
|
|
24
|
+
private _onItemStart;
|
|
25
|
+
private _onItemComplete;
|
|
26
|
+
private _onItemError;
|
|
27
|
+
private static instance;
|
|
28
|
+
private constructor();
|
|
29
|
+
/** Returns the shared IframeQueueEvents singleton. */
|
|
30
|
+
static getInstance(): IframeQueueEvents;
|
|
31
|
+
/**
|
|
32
|
+
* Fires once when the entire queue completes successfully.
|
|
33
|
+
* Use to trigger work that must wait for all chained iframe submits
|
|
34
|
+
* (e.g. opening a bequest lightbox after QCB opt-ins are recorded).
|
|
35
|
+
*/
|
|
36
|
+
get onChainComplete(): import("strongly-typed-events").ISignal;
|
|
37
|
+
/**
|
|
38
|
+
* Fires when the queue aborts due to an error (timeout, iframe load
|
|
39
|
+
* error, or error message from an embedded page). Carries the failed
|
|
40
|
+
* item (if known) and the underlying error.
|
|
41
|
+
*/
|
|
42
|
+
get onChainError(): import("strongly-typed-events").ISimpleEvent<IframeQueueErrorPayload>;
|
|
43
|
+
/** Fires immediately before an item begins processing. */
|
|
44
|
+
get onItemStart(): import("strongly-typed-events").ISimpleEvent<IframeQueueItem>;
|
|
45
|
+
/** Fires when an item completes (its iframe reached its Thank You page). */
|
|
46
|
+
get onItemComplete(): import("strongly-typed-events").ISimpleEvent<IframeQueueItem>;
|
|
47
|
+
/** Fires when an item fails. The queue aborts after this event. */
|
|
48
|
+
get onItemError(): import("strongly-typed-events").ISimpleEvent<{
|
|
49
|
+
item: IframeQueueItem;
|
|
50
|
+
error: Error;
|
|
51
|
+
}>;
|
|
52
|
+
/** Internal — called by IframeQueue when the queue drains successfully. */
|
|
53
|
+
dispatchChainComplete(): void;
|
|
54
|
+
/** Internal — called by IframeQueue when the queue aborts on error. */
|
|
55
|
+
dispatchChainError(payload: IframeQueueErrorPayload): void;
|
|
56
|
+
/** Internal — called by IframeQueue immediately before an item starts. */
|
|
57
|
+
dispatchItemStart(item: IframeQueueItem): void;
|
|
58
|
+
/** Internal — called by IframeQueue when an item finishes successfully. */
|
|
59
|
+
dispatchItemComplete(item: IframeQueueItem): void;
|
|
60
|
+
/** Internal — called by IframeQueue when an item errors. */
|
|
61
|
+
dispatchItemError(item: IframeQueueItem, error: Error): void;
|
|
62
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Singleton event hub for the Iframe Queue component.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the structure of RememberMeEvents: private constructor,
|
|
5
|
+
* static `getInstance()`, internal dispatchers exposed via `.asEvent()`
|
|
6
|
+
* getters, and `dispatch*` methods called by the IframeQueue class.
|
|
7
|
+
*
|
|
8
|
+
* External code subscribes to these events to react to queue
|
|
9
|
+
* lifecycle without holding a reference to the IframeQueue itself.
|
|
10
|
+
* The TNC Bequest Lightbox, for example, will subscribe to
|
|
11
|
+
* `onChainComplete` so it only opens after the QCB opt-in chain has
|
|
12
|
+
* finished submitting.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* IframeQueueEvents.getInstance().onChainComplete.subscribe(() => {
|
|
16
|
+
* openBequestLightbox();
|
|
17
|
+
* });
|
|
18
|
+
*/
|
|
19
|
+
import { SignalDispatcher, SimpleEventDispatcher } from "strongly-typed-events";
|
|
20
|
+
import { EngridLogger } from "..";
|
|
21
|
+
export class IframeQueueEvents {
|
|
22
|
+
constructor() {
|
|
23
|
+
this.logger = new EngridLogger("IframeQueueEvents");
|
|
24
|
+
this._onChainComplete = new SignalDispatcher();
|
|
25
|
+
this._onChainError = new SimpleEventDispatcher();
|
|
26
|
+
this._onItemStart = new SimpleEventDispatcher();
|
|
27
|
+
this._onItemComplete = new SimpleEventDispatcher();
|
|
28
|
+
this._onItemError = new SimpleEventDispatcher();
|
|
29
|
+
}
|
|
30
|
+
/** Returns the shared IframeQueueEvents singleton. */
|
|
31
|
+
static getInstance() {
|
|
32
|
+
if (!IframeQueueEvents.instance) {
|
|
33
|
+
IframeQueueEvents.instance = new IframeQueueEvents();
|
|
34
|
+
}
|
|
35
|
+
return IframeQueueEvents.instance;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Fires once when the entire queue completes successfully.
|
|
39
|
+
* Use to trigger work that must wait for all chained iframe submits
|
|
40
|
+
* (e.g. opening a bequest lightbox after QCB opt-ins are recorded).
|
|
41
|
+
*/
|
|
42
|
+
get onChainComplete() {
|
|
43
|
+
return this._onChainComplete.asEvent();
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Fires when the queue aborts due to an error (timeout, iframe load
|
|
47
|
+
* error, or error message from an embedded page). Carries the failed
|
|
48
|
+
* item (if known) and the underlying error.
|
|
49
|
+
*/
|
|
50
|
+
get onChainError() {
|
|
51
|
+
return this._onChainError.asEvent();
|
|
52
|
+
}
|
|
53
|
+
/** Fires immediately before an item begins processing. */
|
|
54
|
+
get onItemStart() {
|
|
55
|
+
return this._onItemStart.asEvent();
|
|
56
|
+
}
|
|
57
|
+
/** Fires when an item completes (its iframe reached its Thank You page). */
|
|
58
|
+
get onItemComplete() {
|
|
59
|
+
return this._onItemComplete.asEvent();
|
|
60
|
+
}
|
|
61
|
+
/** Fires when an item fails. The queue aborts after this event. */
|
|
62
|
+
get onItemError() {
|
|
63
|
+
return this._onItemError.asEvent();
|
|
64
|
+
}
|
|
65
|
+
/** Internal — called by IframeQueue when the queue drains successfully. */
|
|
66
|
+
dispatchChainComplete() {
|
|
67
|
+
this.logger.log("dispatchChainComplete");
|
|
68
|
+
this._onChainComplete.dispatch();
|
|
69
|
+
}
|
|
70
|
+
/** Internal — called by IframeQueue when the queue aborts on error. */
|
|
71
|
+
dispatchChainError(payload) {
|
|
72
|
+
this.logger.log(`dispatchChainError: ${payload.message}`);
|
|
73
|
+
this._onChainError.dispatch(payload);
|
|
74
|
+
}
|
|
75
|
+
/** Internal — called by IframeQueue immediately before an item starts. */
|
|
76
|
+
dispatchItemStart(item) {
|
|
77
|
+
this.logger.log(`dispatchItemStart: ${item.url}`);
|
|
78
|
+
this._onItemStart.dispatch(item);
|
|
79
|
+
}
|
|
80
|
+
/** Internal — called by IframeQueue when an item finishes successfully. */
|
|
81
|
+
dispatchItemComplete(item) {
|
|
82
|
+
this.logger.log(`dispatchItemComplete: ${item.url}`);
|
|
83
|
+
this._onItemComplete.dispatch(item);
|
|
84
|
+
}
|
|
85
|
+
/** Internal — called by IframeQueue when an item errors. */
|
|
86
|
+
dispatchItemError(item, error) {
|
|
87
|
+
this.logger.log(`dispatchItemError: ${item.url} - ${error.message}`);
|
|
88
|
+
this._onItemError.dispatch({ item, error });
|
|
89
|
+
}
|
|
90
|
+
}
|
package/dist/events/index.d.ts
CHANGED
package/dist/events/index.js
CHANGED
|
@@ -0,0 +1,197 @@
|
|
|
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
|
+
import { IframeQueueItem } from "./interfaces/iframe-queue-options";
|
|
68
|
+
export declare class IframeQueue {
|
|
69
|
+
private static instance;
|
|
70
|
+
private logger;
|
|
71
|
+
private events;
|
|
72
|
+
private _form;
|
|
73
|
+
private queue;
|
|
74
|
+
private _isProcessing;
|
|
75
|
+
private _aborted;
|
|
76
|
+
private inFlightPromise;
|
|
77
|
+
/**
|
|
78
|
+
* Returns the shared IframeQueue singleton. The bootstrap in app.ts
|
|
79
|
+
* instantiates this once via `new IframeQueue()`, but consumers that
|
|
80
|
+
* need to enqueue items programmatically should always go through
|
|
81
|
+
* `getInstance()` so they share the same queue state.
|
|
82
|
+
*/
|
|
83
|
+
static getInstance(): IframeQueue;
|
|
84
|
+
constructor();
|
|
85
|
+
/** Whether the queue is currently processing. */
|
|
86
|
+
get isProcessing(): boolean;
|
|
87
|
+
/** Number of items currently in the queue (not counting the in-flight item). */
|
|
88
|
+
get size(): number;
|
|
89
|
+
/**
|
|
90
|
+
* Add an item to the back of the queue. Items are processed in
|
|
91
|
+
* insertion order. Calling `enqueue` while the queue is processing is
|
|
92
|
+
* supported — the new item joins the chain and will be picked up
|
|
93
|
+
* after the current item completes.
|
|
94
|
+
*/
|
|
95
|
+
enqueue(item: IframeQueueItem): void;
|
|
96
|
+
/**
|
|
97
|
+
* Add many items at once, preserving order. Equivalent to calling
|
|
98
|
+
* {@link enqueue} repeatedly.
|
|
99
|
+
*/
|
|
100
|
+
enqueueAll(items: IframeQueueItem[]): void;
|
|
101
|
+
/**
|
|
102
|
+
* Begin processing the queue. Resolves when the queue drains
|
|
103
|
+
* successfully and rejects on the first error. If already processing,
|
|
104
|
+
* returns the in-flight promise so callers don't start a second drain.
|
|
105
|
+
*/
|
|
106
|
+
process(): Promise<void>;
|
|
107
|
+
/**
|
|
108
|
+
* Empty the queue without processing. Stops the in-flight item if
|
|
109
|
+
* any (the in-flight item rejects with an abort error which is
|
|
110
|
+
* surfaced via `onChainError`).
|
|
111
|
+
*/
|
|
112
|
+
clear(): void;
|
|
113
|
+
/**
|
|
114
|
+
* In parent mode the constructor checks `window.EngridIframeQueue`
|
|
115
|
+
* for declarative startup config, enqueues those items, and (if
|
|
116
|
+
* `autoStart` is true) calls `process()` after DOMContentLoaded.
|
|
117
|
+
*/
|
|
118
|
+
private setupParentMode;
|
|
119
|
+
/**
|
|
120
|
+
* Reads `window.EngridIframeQueue` and returns merged options, or
|
|
121
|
+
* null if no valid config is present.
|
|
122
|
+
*/
|
|
123
|
+
private readWindowConfig;
|
|
124
|
+
/** Process queued items strictly one at a time. */
|
|
125
|
+
private drain;
|
|
126
|
+
/**
|
|
127
|
+
* Process a single item: create the iframe, post populate, wait for
|
|
128
|
+
* the matching Thank-You ping (or error/timeout). Resolves on success
|
|
129
|
+
* and rejects on error/timeout.
|
|
130
|
+
*/
|
|
131
|
+
private processItem;
|
|
132
|
+
/**
|
|
133
|
+
* Normalise the URL for a queued iframe:
|
|
134
|
+
* 1. Strip any `chain` query parameter defensively — the queue
|
|
135
|
+
* replaces `?chain` with sequential processing.
|
|
136
|
+
* 2. Inherit a small allowlist of loader / dev-mode params (see
|
|
137
|
+
* {@link PROPAGATED_PARENT_PARAMS}) when they're not already set
|
|
138
|
+
* on the item URL. Each key is resolved with the same precedence
|
|
139
|
+
* `loader.ts#getOption` uses: parent URL param first, then
|
|
140
|
+
* `window.EngridLoader[key]`.
|
|
141
|
+
*
|
|
142
|
+
* Item-specified params always take precedence over inherited ones.
|
|
143
|
+
* Returns the original string unchanged if URL parsing fails.
|
|
144
|
+
*/
|
|
145
|
+
private prepareIframeUrl;
|
|
146
|
+
/** Returns the parent page's URLSearchParams, or null on failure. */
|
|
147
|
+
private getParentSearchParams;
|
|
148
|
+
/**
|
|
149
|
+
* Returns the parent page's `window.EngridLoader` object if set, or
|
|
150
|
+
* null. Used by {@link prepareIframeUrl} as a fallback source for
|
|
151
|
+
* loader/dev-mode param values when EN has stripped URL parameters
|
|
152
|
+
* from the Thank You page.
|
|
153
|
+
*/
|
|
154
|
+
private getParentEngridLoader;
|
|
155
|
+
/**
|
|
156
|
+
* Decide whether to leave a failed iframe in the DOM (for
|
|
157
|
+
* inspection) instead of removing it. True when the item explicitly
|
|
158
|
+
* asks for it via `keepIframeOnError`, OR whenever ENgrid debug
|
|
159
|
+
* mode is on (since debugging is when this is useful and we don't
|
|
160
|
+
* want to make consumers opt in just to inspect failures).
|
|
161
|
+
*/
|
|
162
|
+
private shouldKeepIframeOnError;
|
|
163
|
+
/**
|
|
164
|
+
* Reposition and style a failed iframe so it's visible in the
|
|
165
|
+
* viewport (overriding the visually-hidden default), and tag it
|
|
166
|
+
* with a class + tooltip so the developer knows why it's there.
|
|
167
|
+
* Right-click the iframe → Inspect frame to dive in.
|
|
168
|
+
*/
|
|
169
|
+
private markIframeFailed;
|
|
170
|
+
/** Create a hidden iframe element for a queue item. */
|
|
171
|
+
private createIframe;
|
|
172
|
+
/**
|
|
173
|
+
* In embedded mode we register a `message` listener that accepts
|
|
174
|
+
* populate messages from `window.parent`, fills form fields, and
|
|
175
|
+
* (optionally) submits. The Thank-You-page ping is sent by the iFrame
|
|
176
|
+
* component (iframe.ts) — not here — so this method does not need to
|
|
177
|
+
* concern itself with completion signalling.
|
|
178
|
+
*/
|
|
179
|
+
private setupEmbeddedMode;
|
|
180
|
+
/** Handle a populate message sent by an IframeQueue parent. */
|
|
181
|
+
private handlePopulate;
|
|
182
|
+
/**
|
|
183
|
+
* Strip every "disabled" marker from the EN submit button so the
|
|
184
|
+
* programmatic `submitForm()` click is honoured. Removes:
|
|
185
|
+
* - the `disabled` DOM property/attribute on the button,
|
|
186
|
+
* - the `en__submit--disabled` BEM modifier (EN's own class),
|
|
187
|
+
* - the `en__submit--disabled` modifier on the `.en__submit`
|
|
188
|
+
* wrapper (some templates style the wrapper instead),
|
|
189
|
+
* - ENgrid's own loader markup if a previous `disableSubmit()`
|
|
190
|
+
* call left it in place.
|
|
191
|
+
*
|
|
192
|
+
* Used only by embedded-mode populate flow when `autoSubmit` is on.
|
|
193
|
+
*/
|
|
194
|
+
private forceEnableSubmitButton;
|
|
195
|
+
/** True when this script is executing inside an iframe. */
|
|
196
|
+
private inIframe;
|
|
197
|
+
}
|
|
@@ -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;
|
package/dist/iframe.js
CHANGED
|
@@ -137,6 +137,11 @@ export class iFrame {
|
|
|
137
137
|
window.parent.postMessage({
|
|
138
138
|
scroll: this.shouldScroll(),
|
|
139
139
|
}, "*");
|
|
140
|
+
// Iframe Queue: signal Thank-You-page completion to the parent window.
|
|
141
|
+
// The IframeQueue component (in parent mode) listens for this ping and
|
|
142
|
+
// matches it by Page ID to advance to the next queued iframe. Fires
|
|
143
|
+
// exactly once per Thank-You-page load. See iframe-queue.ts.
|
|
144
|
+
this.sendIframeQueueThankYouPing();
|
|
140
145
|
// On click fire the resize event
|
|
141
146
|
document.addEventListener("click", (e) => {
|
|
142
147
|
this.logger.log("iFrame Event - click");
|
|
@@ -147,6 +152,35 @@ export class iFrame {
|
|
|
147
152
|
// Watch for errors and send the height
|
|
148
153
|
ENGrid.watchForError(this.sendIframeHeight.bind(this));
|
|
149
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Posts a `engrid-iframe-queue:thank-you` message to the parent window
|
|
157
|
+
* when the embedded EN page reaches its Thank You page (the last page
|
|
158
|
+
* in the page sequence). Carries the Page ID of the submitting form so
|
|
159
|
+
* the IframeQueue parent can match the ping against the queued item it
|
|
160
|
+
* is waiting on, ignoring pings from unrelated EN iframes that may exist
|
|
161
|
+
* on the same parent page (e.g. an Embedded Ecard iframe).
|
|
162
|
+
*
|
|
163
|
+
* Only fires when:
|
|
164
|
+
* - the script is running inside an iframe (already guaranteed by the
|
|
165
|
+
* code path that calls onLoaded()), AND
|
|
166
|
+
* - the embedded page is a Thank You page (ENGrid.isThankYouPage()).
|
|
167
|
+
*
|
|
168
|
+
* Consumed by: IframeQueue (engrid/packages/scripts/src/iframe-queue.ts).
|
|
169
|
+
*/
|
|
170
|
+
sendIframeQueueThankYouPing() {
|
|
171
|
+
if (!ENGrid.isThankYouPage())
|
|
172
|
+
return;
|
|
173
|
+
const pageId = ENGrid.getPageID();
|
|
174
|
+
const message = {
|
|
175
|
+
type: "engrid-iframe-queue:thank-you",
|
|
176
|
+
pageId,
|
|
177
|
+
pageNumber: ENGrid.getPageNumber(),
|
|
178
|
+
pageCount: ENGrid.getPageCount(),
|
|
179
|
+
url: window.location.href,
|
|
180
|
+
};
|
|
181
|
+
this.logger.log(`iFrame Event - Iframe Queue thank-you ping (pageId=${pageId})`);
|
|
182
|
+
window.parent.postMessage(message, "*");
|
|
183
|
+
}
|
|
150
184
|
sendIframeHeight() {
|
|
151
185
|
let height = document.body.offsetHeight;
|
|
152
186
|
this.logger.log("iFrame Event - Sending iFrame height of: " + height + "px"); // check the message is being sent correctly
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export { UpsellOptions, UpsellOptionsDefaults, } from "./interfaces/upsell-optio
|
|
|
4
4
|
export { TranslateOptions, TranslateOptionsDefaults, } from "./interfaces/translate-options";
|
|
5
5
|
export { ExitIntentOptions, ExitIntentOptionsDefaults, } from "./interfaces/exit-intent-options";
|
|
6
6
|
export { FrequencyUpsellOptions, FrequencyUpsellOptionsDefaults, FrequencyUpsellABTestConfig, FrequencyUpsellConfig, } from "./interfaces/frequency-upsell-options";
|
|
7
|
+
export { IframeQueueOptions, IframeQueueOptionsDefaults, IframeQueueItem, IframeQueueErrorPayload, } from "./interfaces/iframe-queue-options";
|
|
7
8
|
export * from "./loader";
|
|
8
9
|
export * from "./app";
|
|
9
10
|
export * from "./amount-label";
|
|
@@ -18,6 +19,7 @@ export * from "./click-to-expand";
|
|
|
18
19
|
export * from "./advocacy";
|
|
19
20
|
export * from "./data-attributes";
|
|
20
21
|
export * from "./iframe";
|
|
22
|
+
export * from "./iframe-queue";
|
|
21
23
|
export * from "./input-has-value-and-focus";
|
|
22
24
|
export * from "./input-placeholders";
|
|
23
25
|
export * from "./media-attribution";
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ export { UpsellOptionsDefaults, } from "./interfaces/upsell-options";
|
|
|
4
4
|
export { TranslateOptionsDefaults, } from "./interfaces/translate-options";
|
|
5
5
|
export { ExitIntentOptionsDefaults, } from "./interfaces/exit-intent-options";
|
|
6
6
|
export { FrequencyUpsellOptionsDefaults, } from "./interfaces/frequency-upsell-options";
|
|
7
|
+
export { IframeQueueOptionsDefaults, } from "./interfaces/iframe-queue-options";
|
|
7
8
|
export * from "./loader";
|
|
8
9
|
export * from "./app";
|
|
9
10
|
export * from "./amount-label";
|
|
@@ -18,6 +19,7 @@ export * from "./click-to-expand";
|
|
|
18
19
|
export * from "./advocacy";
|
|
19
20
|
export * from "./data-attributes";
|
|
20
21
|
export * from "./iframe";
|
|
22
|
+
export * from "./iframe-queue";
|
|
21
23
|
export * from "./input-has-value-and-focus";
|
|
22
24
|
export * from "./input-placeholders";
|
|
23
25
|
export * from "./media-attribution";
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration interfaces for the Iframe Queue component.
|
|
3
|
+
*
|
|
4
|
+
* The Iframe Queue loads a sequence of embedded Engaging Networks pages
|
|
5
|
+
* one at a time, passes field values into them via `postMessage`, and
|
|
6
|
+
* exposes a global `IframeQueueEvents` instance so external code can
|
|
7
|
+
* subscribe to chain-completion. See iframe-queue.ts for the component.
|
|
8
|
+
*
|
|
9
|
+
* Configuration may be supplied either programmatically (via
|
|
10
|
+
* `IframeQueue.getInstance().enqueue(...).process()`) or declaratively
|
|
11
|
+
* by setting `window.EngridIframeQueue` on the host EN page before
|
|
12
|
+
* the ENgrid bundle loads.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* A single embedded EN page to load and submit as part of the queue.
|
|
16
|
+
*
|
|
17
|
+
* The queue creates an iframe for each item, posts an
|
|
18
|
+
* `engrid-iframe-queue:populate` message to the iframe with the
|
|
19
|
+
* item's `fields`, and waits for an `engrid-iframe-queue:thank-you`
|
|
20
|
+
* ping (sent by the iFrame component on the embedded Thank You page)
|
|
21
|
+
* before advancing to the next item.
|
|
22
|
+
*/
|
|
23
|
+
export interface IframeQueueItem {
|
|
24
|
+
/**
|
|
25
|
+
* Full URL of the EN page to load. The queue strips any `chain` query
|
|
26
|
+
* parameter defensively; do not rely on `?chain` for data passing.
|
|
27
|
+
*/
|
|
28
|
+
url: string;
|
|
29
|
+
/**
|
|
30
|
+
* Field name → value pairs to populate in the embedded form via
|
|
31
|
+
* postMessage. Field names match the `name` attribute of the EN form
|
|
32
|
+
* input (e.g. `supporter.emailAddress`).
|
|
33
|
+
*/
|
|
34
|
+
fields?: Record<string, string>;
|
|
35
|
+
/**
|
|
36
|
+
* If true, the embedded iframe submits the form once fields are
|
|
37
|
+
* populated. Default: true.
|
|
38
|
+
*/
|
|
39
|
+
autoSubmit?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Maximum time (ms) to wait for the Thank-You-page ping before
|
|
42
|
+
* treating the item as failed and aborting the queue. Default: 30000.
|
|
43
|
+
*/
|
|
44
|
+
timeout?: number;
|
|
45
|
+
/**
|
|
46
|
+
* Element to append the iframe to. Default: document.body.
|
|
47
|
+
*/
|
|
48
|
+
container?: HTMLElement;
|
|
49
|
+
/**
|
|
50
|
+
* Override iframe styles. Default: visually hidden
|
|
51
|
+
* (position:absolute; width:1px; height:1px; left:-9999px; opacity:0;).
|
|
52
|
+
*/
|
|
53
|
+
iframeStyle?: Partial<CSSStyleDeclaration>;
|
|
54
|
+
/**
|
|
55
|
+
* Optional per-item callback fired when the item completes
|
|
56
|
+
* successfully. Useful for ad-hoc consumers that don't want to
|
|
57
|
+
* subscribe to the global IframeQueueEvents singleton.
|
|
58
|
+
*/
|
|
59
|
+
onComplete?: () => void;
|
|
60
|
+
/**
|
|
61
|
+
* Optional per-item callback fired when the item fails (timeout,
|
|
62
|
+
* iframe load error, or message error from the embedded page).
|
|
63
|
+
*/
|
|
64
|
+
onError?: (error: Error) => void;
|
|
65
|
+
/**
|
|
66
|
+
* If true, when this item fails (timeout / load error / embedded
|
|
67
|
+
* error message) the queue does NOT remove the iframe from the DOM
|
|
68
|
+
* and instead repositions it as a visible, framed overlay so a
|
|
69
|
+
* developer can inspect it (right-click → Inspect frame, view its
|
|
70
|
+
* console, etc.). Default: false.
|
|
71
|
+
*
|
|
72
|
+
* Independent of this flag, failed iframes are also kept and made
|
|
73
|
+
* visible automatically whenever ENgrid debug mode is on
|
|
74
|
+
* (`window.EngridOptions.Debug === true` or `?debug=true`).
|
|
75
|
+
*/
|
|
76
|
+
keepIframeOnError?: boolean;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Payload dispatched by `IframeQueueEvents.onChainError` when the queue
|
|
80
|
+
* aborts due to an error. `failedItem` may be undefined if the error
|
|
81
|
+
* occurred before any item started processing.
|
|
82
|
+
*/
|
|
83
|
+
export interface IframeQueueErrorPayload {
|
|
84
|
+
message: string;
|
|
85
|
+
failedItem?: IframeQueueItem;
|
|
86
|
+
cause?: Error;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Declarative configuration set on `window.EngridIframeQueue` to
|
|
90
|
+
* preload items and optionally auto-start the queue at page load.
|
|
91
|
+
*/
|
|
92
|
+
export interface IframeQueueOptions {
|
|
93
|
+
/** Items to enqueue at startup. */
|
|
94
|
+
items?: IframeQueueItem[];
|
|
95
|
+
/**
|
|
96
|
+
* Whether to call `process()` automatically once items are enqueued
|
|
97
|
+
* at startup. Default: true if `items` is non-empty, else false.
|
|
98
|
+
*/
|
|
99
|
+
autoStart?: boolean;
|
|
100
|
+
}
|
|
101
|
+
export declare const IframeQueueOptionsDefaults: IframeQueueOptions;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration interfaces for the Iframe Queue component.
|
|
3
|
+
*
|
|
4
|
+
* The Iframe Queue loads a sequence of embedded Engaging Networks pages
|
|
5
|
+
* one at a time, passes field values into them via `postMessage`, and
|
|
6
|
+
* exposes a global `IframeQueueEvents` instance so external code can
|
|
7
|
+
* subscribe to chain-completion. See iframe-queue.ts for the component.
|
|
8
|
+
*
|
|
9
|
+
* Configuration may be supplied either programmatically (via
|
|
10
|
+
* `IframeQueue.getInstance().enqueue(...).process()`) or declaratively
|
|
11
|
+
* by setting `window.EngridIframeQueue` on the host EN page before
|
|
12
|
+
* the ENgrid bundle loads.
|
|
13
|
+
*/
|
|
14
|
+
export const IframeQueueOptionsDefaults = {
|
|
15
|
+
items: [],
|
|
16
|
+
autoStart: true,
|
|
17
|
+
};
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const AppVersion = "0.25.
|
|
1
|
+
export declare const AppVersion = "0.25.2";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const AppVersion = "0.25.
|
|
1
|
+
export const AppVersion = "0.25.2";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@4site/engrid-scripts",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.2",
|
|
4
4
|
"description": "Scripts for the ENGRID Project",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"strongly-typed-events": "^2.0.9"
|
|
26
26
|
},
|
|
27
27
|
"homepage": "https://github.com/4site-interactive-studios/engrid#readme",
|
|
28
|
-
"gitHead": "
|
|
28
|
+
"gitHead": "9ca2921968167be0cea2cfdbb35a85109e9d46bc",
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"nodemon": "^2.0.20"
|
|
31
31
|
}
|