@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.
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
+ }
@@ -3,4 +3,5 @@ export * from "./donation-amount";
3
3
  export * from "./donation-frequency";
4
4
  export * from "./processing-fees";
5
5
  export * from "./remember-me-events";
6
+ export * from "./iframe-queue-events";
6
7
  export * from "./country";
@@ -3,4 +3,5 @@ export * from "./donation-amount";
3
3
  export * from "./donation-frequency";
4
4
  export * from "./processing-fees";
5
5
  export * from "./remember-me-events";
6
+ export * from "./iframe-queue-events";
6
7
  export * from "./country";
@@ -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
+ }