@4site/engrid-scripts 0.28.1 → 0.28.3
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/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/page-background-rotation.d.ts +84 -0
- package/dist/page-background-rotation.js +557 -0
- package/dist/remember-me.d.ts +0 -23
- package/dist/remember-me.js +7 -117
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -34,6 +34,7 @@ export * from "./skip-link";
|
|
|
34
34
|
export * from "./src-defer";
|
|
35
35
|
export * from "./set-recurr-freq";
|
|
36
36
|
export * from "./page-background";
|
|
37
|
+
export * from "./page-background-rotation";
|
|
37
38
|
export * from "./neverbounce";
|
|
38
39
|
export * from "./freshaddress";
|
|
39
40
|
export * from "./progress-bar";
|
package/dist/index.js
CHANGED
|
@@ -34,6 +34,7 @@ export * from "./skip-link";
|
|
|
34
34
|
export * from "./src-defer";
|
|
35
35
|
export * from "./set-recurr-freq";
|
|
36
36
|
export * from "./page-background";
|
|
37
|
+
export * from "./page-background-rotation";
|
|
37
38
|
export * from "./neverbounce";
|
|
38
39
|
export * from "./freshaddress";
|
|
39
40
|
export * from "./progress-bar";
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
type BackgroundRotationSlideOrder = "random" | "sequential" | "true-random";
|
|
2
|
+
interface EngridPageBackgroundRotationOptions {
|
|
3
|
+
enabled: boolean;
|
|
4
|
+
interval: number;
|
|
5
|
+
initialDelay: number;
|
|
6
|
+
transitionDuration: number;
|
|
7
|
+
transitionClass: string;
|
|
8
|
+
eachImageSelector: string;
|
|
9
|
+
backgroundImageSelector: string;
|
|
10
|
+
slideOrder: BackgroundRotationSlideOrder;
|
|
11
|
+
randomStart: boolean;
|
|
12
|
+
reducedMotion: boolean;
|
|
13
|
+
rotateOnMobile: boolean;
|
|
14
|
+
mobileBreakpoint: string;
|
|
15
|
+
controls: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare class PageBackgroundRotation {
|
|
18
|
+
private logger;
|
|
19
|
+
private defaultOptions;
|
|
20
|
+
private options;
|
|
21
|
+
private container;
|
|
22
|
+
private items;
|
|
23
|
+
private layers;
|
|
24
|
+
private imageUrls;
|
|
25
|
+
private imagesWarmed;
|
|
26
|
+
private warmingScheduled;
|
|
27
|
+
private firstImagePreloaded;
|
|
28
|
+
private currentIndex;
|
|
29
|
+
private randomBag;
|
|
30
|
+
private history;
|
|
31
|
+
private isPaused;
|
|
32
|
+
private pausedForReducedMotion;
|
|
33
|
+
private interactionPauses;
|
|
34
|
+
private previousButton;
|
|
35
|
+
private pauseButton;
|
|
36
|
+
private liveRegion;
|
|
37
|
+
private rotationTimer;
|
|
38
|
+
private initialDelayTimer;
|
|
39
|
+
private initialDelayElapsed;
|
|
40
|
+
private transitionTimer;
|
|
41
|
+
private mobileMediaQuery;
|
|
42
|
+
private reducedMotionMediaQuery;
|
|
43
|
+
constructor(options?: Partial<EngridPageBackgroundRotationOptions>);
|
|
44
|
+
private shouldRun;
|
|
45
|
+
private prepareItems;
|
|
46
|
+
private applyLayerImage;
|
|
47
|
+
private preloadFirstImage;
|
|
48
|
+
private scheduleImageWarming;
|
|
49
|
+
private warmRemainingImages;
|
|
50
|
+
private getItemLayer;
|
|
51
|
+
private getItemImage;
|
|
52
|
+
private getItemImageUrl;
|
|
53
|
+
private isStaticMode;
|
|
54
|
+
private reducedMotionPreferred;
|
|
55
|
+
private updateMode;
|
|
56
|
+
private startRotationTimer;
|
|
57
|
+
private stopRotationTimer;
|
|
58
|
+
private stopRotation;
|
|
59
|
+
private finishTransition;
|
|
60
|
+
private showStaticImage;
|
|
61
|
+
private setActiveItem;
|
|
62
|
+
private setFlowLayer;
|
|
63
|
+
private rotateToNextImage;
|
|
64
|
+
private goToImage;
|
|
65
|
+
private goToNextImage;
|
|
66
|
+
private goToPreviousImage;
|
|
67
|
+
private togglePause;
|
|
68
|
+
private createControls;
|
|
69
|
+
private pauseForInteraction;
|
|
70
|
+
private resumeFromInteraction;
|
|
71
|
+
private canRotate;
|
|
72
|
+
private updatePreviousButtonState;
|
|
73
|
+
private announceImage;
|
|
74
|
+
private createControlButton;
|
|
75
|
+
private updatePauseButton;
|
|
76
|
+
private pauseIcon;
|
|
77
|
+
private playIcon;
|
|
78
|
+
private getNextIndex;
|
|
79
|
+
private getNextFromRandomBag;
|
|
80
|
+
private getRandomIndex;
|
|
81
|
+
private getItemTheme;
|
|
82
|
+
private getItemAttribution;
|
|
83
|
+
}
|
|
84
|
+
export {};
|
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
// PageBackgroundRotation handles the rotation of background images within a page-backgroundImage block
|
|
2
|
+
// By default, this feature is not enabled, and must be enabled by importing and initializing it in the client theme's onLoad block
|
|
3
|
+
// Within the page-backgroundImage block, if there is a parent div with a class of 'background-rotation', then the background image will rotate every 5 seconds
|
|
4
|
+
// The image rotates on a cross-fade transition, and the next image is randomly selected from the list of child elements with a class of 'background-image-item' within the 'background-rotation' div
|
|
5
|
+
// The random selection of the next image is done in a way that ensures that the same image is not displayed twice in a row, and that all images are displayed before any image is repeated
|
|
6
|
+
// On mobile, the background image will not rotate, and a random image in the list will be displayed as a static background image
|
|
7
|
+
// The background image will also not rotate if the user has set a preference for reduced motion in their system settings, unless controls are present which will allow the user to "start" the process manually.
|
|
8
|
+
// Figattributes/figcaptions, if included on the image, will also need to be updated to reflect the new image being displayed
|
|
9
|
+
// Each image item can include a data-theme (default 'dark') attribute, which allow for client themes to style particular elements based on the background color.
|
|
10
|
+
// Options block:
|
|
11
|
+
/**
|
|
12
|
+
* Set via the default options, overridden by the options passed to the constructor, and overridden by a window-level variable called 'EngridPageBackgroundRotationOptions' if it exists. The options are as follows:
|
|
13
|
+
* enabled: Whether the background rotation is enabled (default: true)
|
|
14
|
+
* interval: The interval in milliseconds between image rotations (default: 5000)
|
|
15
|
+
* initialDelay: The delay in milliseconds before the first rotation, giving the first image time to load (default: 10000)
|
|
16
|
+
* transitionDuration: The duration of the cross-fade transition in milliseconds (default: 500)
|
|
17
|
+
* transitionClass: The CSS class to apply to the background image container during the transition (default: 'background-rotation-transition')
|
|
18
|
+
* eachImageSelector: The CSS selector for each individual background image (default: '.page-background-image-item')
|
|
19
|
+
* backgroundImageSelector: The CSS selector for the background image container (default: '.page-background-rotation')
|
|
20
|
+
* slideOrder: The order in which the images are displayed (default: 'random' [random-bag], other options: 'sequential', 'true-random')
|
|
21
|
+
* randomStart: Whether to start the rotation at a random image (default: true)
|
|
22
|
+
* reducedMotion: Whether to respect the user's preference for reduced motion (default: true)
|
|
23
|
+
* rotateOnMobile: Whether to rotate the background image on mobile devices (default: false)
|
|
24
|
+
* mobileBreakpoint: Where to consider the layout as being "mobile" (default: ‘(max-width: 499px)’)
|
|
25
|
+
* controls: Whether to add back, pause, and forward buttons for the rotation (default: false)
|
|
26
|
+
*/
|
|
27
|
+
import { ENGrid } from "./engrid";
|
|
28
|
+
import { EngridLogger } from "./logger";
|
|
29
|
+
export class PageBackgroundRotation {
|
|
30
|
+
constructor(options = {}) {
|
|
31
|
+
var _a;
|
|
32
|
+
this.logger = new EngridLogger("PageBackgroundRotation", "white", "rebeccapurple", "🌄");
|
|
33
|
+
this.defaultOptions = {
|
|
34
|
+
enabled: true,
|
|
35
|
+
interval: 5000,
|
|
36
|
+
initialDelay: 10000,
|
|
37
|
+
transitionDuration: 500,
|
|
38
|
+
transitionClass: "background-rotation-transition",
|
|
39
|
+
eachImageSelector: ".page-background-image-item",
|
|
40
|
+
backgroundImageSelector: ".page-background-rotation",
|
|
41
|
+
slideOrder: "random",
|
|
42
|
+
randomStart: true,
|
|
43
|
+
reducedMotion: true,
|
|
44
|
+
rotateOnMobile: false,
|
|
45
|
+
mobileBreakpoint: "(max-width: 499px)",
|
|
46
|
+
controls: false,
|
|
47
|
+
};
|
|
48
|
+
this.container = null;
|
|
49
|
+
this.items = [];
|
|
50
|
+
this.layers = [];
|
|
51
|
+
this.imageUrls = [];
|
|
52
|
+
this.imagesWarmed = false;
|
|
53
|
+
this.warmingScheduled = false;
|
|
54
|
+
this.firstImagePreloaded = false;
|
|
55
|
+
this.currentIndex = -1;
|
|
56
|
+
this.randomBag = [];
|
|
57
|
+
this.history = [];
|
|
58
|
+
this.isPaused = false;
|
|
59
|
+
this.pausedForReducedMotion = false;
|
|
60
|
+
this.interactionPauses = new Set();
|
|
61
|
+
this.previousButton = null;
|
|
62
|
+
this.pauseButton = null;
|
|
63
|
+
this.liveRegion = null;
|
|
64
|
+
this.rotationTimer = null;
|
|
65
|
+
this.initialDelayTimer = null;
|
|
66
|
+
this.initialDelayElapsed = false;
|
|
67
|
+
this.transitionTimer = null;
|
|
68
|
+
this.reducedMotionMediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
69
|
+
this.options = Object.assign(Object.assign(Object.assign({}, this.defaultOptions), options), ((_a = window.EngridPageBackgroundRotationOptions) !== null && _a !== void 0 ? _a : {}));
|
|
70
|
+
this.mobileMediaQuery = window.matchMedia(this.options.mobileBreakpoint);
|
|
71
|
+
if (!this.shouldRun())
|
|
72
|
+
return;
|
|
73
|
+
this.container = document.querySelector(`.page-backgroundImage ${this.options.backgroundImageSelector}, .body-banner ${this.options.backgroundImageSelector}`);
|
|
74
|
+
this.items = Array.from(this.container.querySelectorAll(this.options.eachImageSelector));
|
|
75
|
+
this.container.style.setProperty("--background-rotation-transition-duration", `${this.options.transitionDuration}ms`);
|
|
76
|
+
document.body.style.setProperty("--background-rotation-transition-duration", `${this.options.transitionDuration}ms`);
|
|
77
|
+
this.prepareItems();
|
|
78
|
+
if (this.items.length === 1) {
|
|
79
|
+
this.showStaticImage();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
this.updateMode();
|
|
83
|
+
if (!this.options.rotateOnMobile) {
|
|
84
|
+
this.mobileMediaQuery.addEventListener("change", () => this.updateMode());
|
|
85
|
+
}
|
|
86
|
+
if (this.options.reducedMotion) {
|
|
87
|
+
this.reducedMotionMediaQuery.addEventListener("change", () => this.updateMode());
|
|
88
|
+
}
|
|
89
|
+
if (this.options.controls) {
|
|
90
|
+
this.createControls();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
shouldRun() {
|
|
94
|
+
if (!this.options.enabled) {
|
|
95
|
+
this.logger.log("Background rotation is disabled");
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
const container = document.querySelector(`.page-backgroundImage ${this.options.backgroundImageSelector}, .body-banner ${this.options.backgroundImageSelector}`);
|
|
99
|
+
if (!container)
|
|
100
|
+
return false;
|
|
101
|
+
if (!container.querySelector(this.options.eachImageSelector)) {
|
|
102
|
+
this.logger.log("No background image items found to rotate");
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
prepareItems() {
|
|
108
|
+
this.items.forEach((item, index) => {
|
|
109
|
+
const layer = this.getItemLayer(item);
|
|
110
|
+
const imageUrl = this.getItemImageUrl(item);
|
|
111
|
+
if (!imageUrl) {
|
|
112
|
+
this.logger.log("Background image item has no image source", item);
|
|
113
|
+
}
|
|
114
|
+
this.imageUrls[index] = imageUrl;
|
|
115
|
+
layer.classList.add("background-rotation-layer");
|
|
116
|
+
layer.setAttribute("aria-hidden", "true");
|
|
117
|
+
this.layers[index] = layer;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
// The inline background-image is what makes a layer fetch its image, so it is
|
|
121
|
+
// applied when the layer is first shown rather than for every layer up front
|
|
122
|
+
applyLayerImage(index) {
|
|
123
|
+
const layer = this.layers[index];
|
|
124
|
+
const imageUrl = this.imageUrls[index];
|
|
125
|
+
if (!layer || !imageUrl || layer.style.backgroundImage)
|
|
126
|
+
return;
|
|
127
|
+
layer.style.backgroundImage = `url('${imageUrl}')`;
|
|
128
|
+
}
|
|
129
|
+
// Fetches the first image ahead of other page assets so the background
|
|
130
|
+
// paints as early as possible; only ever applied to the first image shown
|
|
131
|
+
preloadFirstImage(index) {
|
|
132
|
+
if (this.firstImagePreloaded)
|
|
133
|
+
return;
|
|
134
|
+
this.firstImagePreloaded = true;
|
|
135
|
+
const imageUrl = this.imageUrls[index];
|
|
136
|
+
if (!imageUrl)
|
|
137
|
+
return;
|
|
138
|
+
const preload = document.createElement("link");
|
|
139
|
+
preload.rel = "preload";
|
|
140
|
+
preload.setAttribute("as", "image");
|
|
141
|
+
preload.href = imageUrl;
|
|
142
|
+
preload.setAttribute("fetchpriority", "high");
|
|
143
|
+
document.head.appendChild(preload);
|
|
144
|
+
}
|
|
145
|
+
// The remaining layers are applied once the page has settled, so a full set of
|
|
146
|
+
// viewport-sized images isn't competing with the form's own assets during load.
|
|
147
|
+
// Warming waits until the first image has finished loading AND a minimum 4s
|
|
148
|
+
// delay has passed, so it never competes with the first image's bandwidth.
|
|
149
|
+
// The layers are still applied ahead of the first rotation (10s initialDelay),
|
|
150
|
+
// so cross-fades don't start against an image that hasn't been fetched yet.
|
|
151
|
+
scheduleImageWarming(index) {
|
|
152
|
+
if (this.warmingScheduled || this.imagesWarmed)
|
|
153
|
+
return;
|
|
154
|
+
this.warmingScheduled = true;
|
|
155
|
+
const imageUrl = this.imageUrls[index];
|
|
156
|
+
const firstImageLoaded = new Promise((resolve) => {
|
|
157
|
+
if (!imageUrl) {
|
|
158
|
+
resolve();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const probe = new Image();
|
|
162
|
+
probe.onload = () => resolve();
|
|
163
|
+
probe.onerror = () => resolve();
|
|
164
|
+
probe.src = imageUrl;
|
|
165
|
+
});
|
|
166
|
+
const minimumDelay = new Promise((resolve) => window.setTimeout(resolve, 4000));
|
|
167
|
+
Promise.all([firstImageLoaded, minimumDelay]).then(() => this.warmRemainingImages());
|
|
168
|
+
}
|
|
169
|
+
warmRemainingImages() {
|
|
170
|
+
if (this.imagesWarmed)
|
|
171
|
+
return;
|
|
172
|
+
this.imagesWarmed = true;
|
|
173
|
+
const warm = () => this.items.forEach((_, index) => this.applyLayerImage(index));
|
|
174
|
+
const requestIdle = window.requestIdleCallback;
|
|
175
|
+
if (requestIdle) {
|
|
176
|
+
requestIdle.call(window, warm, { timeout: 3000 });
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
window.setTimeout(warm, 1000);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// The item is typically the <img> tag itself. If MediaAttribution has wrapped
|
|
183
|
+
// it in a <figure class="media-with-attribution">, the figure becomes the fade
|
|
184
|
+
// layer so its figattribution cross-fades in sync with the image
|
|
185
|
+
getItemLayer(item) {
|
|
186
|
+
var _a;
|
|
187
|
+
if (item instanceof HTMLImageElement &&
|
|
188
|
+
((_a = item.parentElement) === null || _a === void 0 ? void 0 : _a.matches("figure.media-with-attribution"))) {
|
|
189
|
+
return item.parentElement;
|
|
190
|
+
}
|
|
191
|
+
return item;
|
|
192
|
+
}
|
|
193
|
+
getItemImage(item) {
|
|
194
|
+
if (item instanceof HTMLImageElement)
|
|
195
|
+
return item;
|
|
196
|
+
return item.querySelector("img");
|
|
197
|
+
}
|
|
198
|
+
getItemImageUrl(item) {
|
|
199
|
+
const img = this.getItemImage(item);
|
|
200
|
+
if (!img)
|
|
201
|
+
return null;
|
|
202
|
+
return img.getAttribute("data-src") || img.getAttribute("src");
|
|
203
|
+
}
|
|
204
|
+
isStaticMode() {
|
|
205
|
+
// With controls enabled a reduced-motion user can still advance the
|
|
206
|
+
// images on their own, so only treat reduced motion as static mode
|
|
207
|
+
// when there are no controls
|
|
208
|
+
if (this.reducedMotionPreferred() && !this.options.controls) {
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
if (!this.options.rotateOnMobile && this.mobileMediaQuery.matches) {
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
reducedMotionPreferred() {
|
|
217
|
+
return this.options.reducedMotion && this.reducedMotionMediaQuery.matches;
|
|
218
|
+
}
|
|
219
|
+
// Starts or stops the rotation based on the current viewport and motion
|
|
220
|
+
// preferences, called on page load and whenever they change
|
|
221
|
+
updateMode() {
|
|
222
|
+
if (this.isStaticMode()) {
|
|
223
|
+
this.stopRotation();
|
|
224
|
+
if (this.currentIndex === -1) {
|
|
225
|
+
this.showStaticImage();
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
ENGrid.setBodyData("background-rotation", "static");
|
|
229
|
+
}
|
|
230
|
+
this.logger.log("Static background image mode");
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
// A reduced-motion preference (with controls enabled) starts paused so
|
|
234
|
+
// the user can advance the images on their own; if the preference is
|
|
235
|
+
// removed again, only auto-resume when the pause wasn't user-initiated
|
|
236
|
+
if (this.reducedMotionPreferred()) {
|
|
237
|
+
this.stopRotationTimer();
|
|
238
|
+
this.isPaused = true;
|
|
239
|
+
this.pausedForReducedMotion = true;
|
|
240
|
+
this.updatePauseButton();
|
|
241
|
+
this.logger.log("Auto-rotation paused for reduced motion preference");
|
|
242
|
+
}
|
|
243
|
+
else if (this.pausedForReducedMotion) {
|
|
244
|
+
this.pausedForReducedMotion = false;
|
|
245
|
+
this.isPaused = false;
|
|
246
|
+
this.updatePauseButton();
|
|
247
|
+
}
|
|
248
|
+
if (this.rotationTimer !== null || this.initialDelayTimer !== null)
|
|
249
|
+
return;
|
|
250
|
+
const startIndex = this.currentIndex !== -1
|
|
251
|
+
? this.currentIndex
|
|
252
|
+
: this.options.randomStart
|
|
253
|
+
? this.getRandomIndex()
|
|
254
|
+
: 0;
|
|
255
|
+
// Preload before the layer's background-image is applied, so the
|
|
256
|
+
// high-priority fetch is the one that hits the network first
|
|
257
|
+
this.preloadFirstImage(startIndex);
|
|
258
|
+
this.setActiveItem(startIndex);
|
|
259
|
+
this.scheduleImageWarming(startIndex);
|
|
260
|
+
ENGrid.setBodyData("background-rotation", "active");
|
|
261
|
+
if (this.canRotate())
|
|
262
|
+
this.startRotationTimer();
|
|
263
|
+
this.logger.log(`Rotating ${this.items.length} background images every ${this.options.interval}ms`);
|
|
264
|
+
}
|
|
265
|
+
// The first rotation waits for initialDelay to give the first image (and the
|
|
266
|
+
// warming of the rest) time to load; later rotations use the normal interval.
|
|
267
|
+
// If paused before the first rotation, the full initial delay re-arms on resume
|
|
268
|
+
startRotationTimer() {
|
|
269
|
+
this.stopRotationTimer();
|
|
270
|
+
if (!this.initialDelayElapsed) {
|
|
271
|
+
this.initialDelayTimer = window.setTimeout(() => {
|
|
272
|
+
this.initialDelayTimer = null;
|
|
273
|
+
this.initialDelayElapsed = true;
|
|
274
|
+
this.rotateToNextImage();
|
|
275
|
+
this.rotationTimer = window.setInterval(() => this.rotateToNextImage(), this.options.interval);
|
|
276
|
+
}, this.options.initialDelay);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
this.rotationTimer = window.setInterval(() => this.rotateToNextImage(), this.options.interval);
|
|
280
|
+
}
|
|
281
|
+
stopRotationTimer() {
|
|
282
|
+
if (this.initialDelayTimer !== null) {
|
|
283
|
+
window.clearTimeout(this.initialDelayTimer);
|
|
284
|
+
this.initialDelayTimer = null;
|
|
285
|
+
}
|
|
286
|
+
if (this.rotationTimer !== null) {
|
|
287
|
+
window.clearInterval(this.rotationTimer);
|
|
288
|
+
this.rotationTimer = null;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
stopRotation() {
|
|
292
|
+
this.stopRotationTimer();
|
|
293
|
+
this.finishTransition();
|
|
294
|
+
}
|
|
295
|
+
// Settles a cross-fade: only the current image keeps the class that makes it
|
|
296
|
+
// visible, and the in-flow layer moves to it. Runs when a transition ends, and
|
|
297
|
+
// again if the next transition starts first, so an interrupted fade can never
|
|
298
|
+
// leave a stale layer stacked on top of the current one
|
|
299
|
+
finishTransition() {
|
|
300
|
+
var _a;
|
|
301
|
+
if (this.transitionTimer !== null) {
|
|
302
|
+
window.clearTimeout(this.transitionTimer);
|
|
303
|
+
this.transitionTimer = null;
|
|
304
|
+
}
|
|
305
|
+
this.layers.forEach((layer, index) => {
|
|
306
|
+
layer.classList.remove("background-rotation-outgoing");
|
|
307
|
+
if (index === this.currentIndex)
|
|
308
|
+
return;
|
|
309
|
+
layer.classList.remove("active");
|
|
310
|
+
layer.setAttribute("aria-hidden", "true");
|
|
311
|
+
});
|
|
312
|
+
if (this.currentIndex !== -1) {
|
|
313
|
+
this.setFlowLayer(this.layers[this.currentIndex]);
|
|
314
|
+
}
|
|
315
|
+
(_a = this.container) === null || _a === void 0 ? void 0 : _a.classList.remove(this.options.transitionClass);
|
|
316
|
+
}
|
|
317
|
+
showStaticImage() {
|
|
318
|
+
const index = this.options.randomStart ? this.getRandomIndex() : 0;
|
|
319
|
+
this.preloadFirstImage(index);
|
|
320
|
+
this.setActiveItem(index);
|
|
321
|
+
ENGrid.setBodyData("background-rotation", "static");
|
|
322
|
+
}
|
|
323
|
+
setActiveItem(index, moveFlow = true) {
|
|
324
|
+
var _a;
|
|
325
|
+
const layer = this.layers[index];
|
|
326
|
+
if (!layer)
|
|
327
|
+
return;
|
|
328
|
+
this.applyLayerImage(index);
|
|
329
|
+
layer.classList.add("active");
|
|
330
|
+
layer.removeAttribute("aria-hidden");
|
|
331
|
+
this.currentIndex = index;
|
|
332
|
+
if (moveFlow)
|
|
333
|
+
this.setFlowLayer(layer);
|
|
334
|
+
const imageUrl = this.imageUrls[index];
|
|
335
|
+
if (imageUrl) {
|
|
336
|
+
document.body.style.setProperty("--background-rotation-image", `url('${imageUrl}')`);
|
|
337
|
+
}
|
|
338
|
+
setTimeout(() => {
|
|
339
|
+
ENGrid.setBodyData("background-rotation-theme", this.getItemTheme(this.items[index]));
|
|
340
|
+
}, 300);
|
|
341
|
+
this.logger.log("Active background image", index + 1, "of", this.items.length, (_a = this.getItemAttribution(this.items[index])) !== null && _a !== void 0 ? _a : "");
|
|
342
|
+
}
|
|
343
|
+
// Marks the single layer that stays in-flow to give the container its height
|
|
344
|
+
// at the <=499px breakpoint. Kept on the outgoing layer during a cross-fade
|
|
345
|
+
// so two in-flow layers never stack, and moved to the incoming layer once
|
|
346
|
+
// the transition ends (see finishTransition)
|
|
347
|
+
setFlowLayer(layer) {
|
|
348
|
+
this.layers.forEach((item) => item.classList.remove("background-rotation-flow"));
|
|
349
|
+
layer.classList.add("background-rotation-flow");
|
|
350
|
+
}
|
|
351
|
+
rotateToNextImage() {
|
|
352
|
+
this.goToImage(this.getNextIndex());
|
|
353
|
+
}
|
|
354
|
+
goToImage(nextIndex, addToHistory = true) {
|
|
355
|
+
if (nextIndex === this.currentIndex)
|
|
356
|
+
return;
|
|
357
|
+
if (addToHistory && this.currentIndex !== -1) {
|
|
358
|
+
this.history.push(this.currentIndex);
|
|
359
|
+
if (this.history.length > this.items.length * 2)
|
|
360
|
+
this.history.shift();
|
|
361
|
+
}
|
|
362
|
+
this.updatePreviousButtonState();
|
|
363
|
+
// Settle a fade that is still running before starting the next one
|
|
364
|
+
this.finishTransition();
|
|
365
|
+
const outgoingLayer = this.layers[this.currentIndex];
|
|
366
|
+
// The outgoing layer keeps .active (staying fully opaque) while the
|
|
367
|
+
// incoming layer fades in on top of it, so the composite is opaque at
|
|
368
|
+
// every point of the cross-fade — nothing behind the layers ever washes
|
|
369
|
+
// through. It loses .active in finishTransition, once it is fully covered.
|
|
370
|
+
outgoingLayer === null || outgoingLayer === void 0 ? void 0 : outgoingLayer.classList.add("background-rotation-outgoing");
|
|
371
|
+
outgoingLayer === null || outgoingLayer === void 0 ? void 0 : outgoingLayer.setAttribute("aria-hidden", "true");
|
|
372
|
+
this.container.classList.add(this.options.transitionClass);
|
|
373
|
+
this.setActiveItem(nextIndex, false);
|
|
374
|
+
this.transitionTimer = window.setTimeout(() => this.finishTransition(), this.options.transitionDuration);
|
|
375
|
+
}
|
|
376
|
+
goToNextImage() {
|
|
377
|
+
this.goToImage(this.getNextIndex());
|
|
378
|
+
this.announceImage();
|
|
379
|
+
if (this.canRotate())
|
|
380
|
+
this.startRotationTimer();
|
|
381
|
+
}
|
|
382
|
+
goToPreviousImage() {
|
|
383
|
+
const previousIndex = this.history.pop();
|
|
384
|
+
if (previousIndex === undefined) {
|
|
385
|
+
this.logger.log("No previous background image in the history");
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
this.goToImage(previousIndex, false);
|
|
389
|
+
this.announceImage();
|
|
390
|
+
if (this.canRotate())
|
|
391
|
+
this.startRotationTimer();
|
|
392
|
+
}
|
|
393
|
+
togglePause() {
|
|
394
|
+
this.isPaused = !this.isPaused;
|
|
395
|
+
// Once the user touches the pause control the pause is theirs, so a later
|
|
396
|
+
// reduced-motion change no longer auto-resumes the rotation
|
|
397
|
+
this.pausedForReducedMotion = false;
|
|
398
|
+
if (this.isPaused) {
|
|
399
|
+
this.stopRotationTimer();
|
|
400
|
+
this.logger.log("Background rotation paused");
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
if (this.canRotate())
|
|
404
|
+
this.startRotationTimer();
|
|
405
|
+
this.logger.log("Background rotation resumed");
|
|
406
|
+
}
|
|
407
|
+
this.updatePauseButton();
|
|
408
|
+
}
|
|
409
|
+
createControls() {
|
|
410
|
+
const controls = document.createElement("div");
|
|
411
|
+
controls.className = "background-rotation-controls";
|
|
412
|
+
controls.setAttribute("role", "group");
|
|
413
|
+
controls.setAttribute("aria-label", "Background image rotation controls");
|
|
414
|
+
// Pause the auto-rotation while the user is hovering or tabbing through the
|
|
415
|
+
// controls, so nobody has to chase a moving target
|
|
416
|
+
controls.addEventListener("mouseenter", () => this.pauseForInteraction("hover"));
|
|
417
|
+
controls.addEventListener("mouseleave", () => this.resumeFromInteraction("hover"));
|
|
418
|
+
controls.addEventListener("focusin", () => this.pauseForInteraction("focus"));
|
|
419
|
+
controls.addEventListener("focusout", (event) => {
|
|
420
|
+
if (!controls.contains(event.relatedTarget)) {
|
|
421
|
+
this.resumeFromInteraction("focus");
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
this.previousButton = this.createControlButton("background-rotation-prev", "Previous background image", '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg>');
|
|
425
|
+
this.previousButton.addEventListener("click", () => this.goToPreviousImage());
|
|
426
|
+
this.pauseButton = this.createControlButton("background-rotation-pause", "Pause background rotation", this.pauseIcon());
|
|
427
|
+
this.pauseButton.setAttribute("aria-pressed", "false");
|
|
428
|
+
this.pauseButton.addEventListener("click", () => this.togglePause());
|
|
429
|
+
const nextButton = this.createControlButton("background-rotation-next", "Next background image", '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 18l6-6-6-6"/></svg>');
|
|
430
|
+
nextButton.addEventListener("click", () => this.goToNextImage());
|
|
431
|
+
this.liveRegion = document.createElement("div");
|
|
432
|
+
this.liveRegion.className = "engrid__sr-only";
|
|
433
|
+
this.liveRegion.setAttribute("aria-live", "polite");
|
|
434
|
+
this.liveRegion.setAttribute("aria-atomic", "true");
|
|
435
|
+
controls.append(this.previousButton, this.pauseButton, nextButton, this.liveRegion);
|
|
436
|
+
document.body.appendChild(controls);
|
|
437
|
+
this.updatePreviousButtonState();
|
|
438
|
+
// Reflect a pause that happened before the controls existed (e.g. the
|
|
439
|
+
// reduced-motion auto-pause in updateMode)
|
|
440
|
+
this.updatePauseButton();
|
|
441
|
+
}
|
|
442
|
+
// Auto-rotation pauses while the user interacts with the controls, separately
|
|
443
|
+
// from a user-initiated pause, and resumes when the interaction ends
|
|
444
|
+
pauseForInteraction(kind) {
|
|
445
|
+
this.interactionPauses.add(kind);
|
|
446
|
+
if (!this.isPaused)
|
|
447
|
+
this.stopRotationTimer();
|
|
448
|
+
}
|
|
449
|
+
resumeFromInteraction(kind) {
|
|
450
|
+
this.interactionPauses.delete(kind);
|
|
451
|
+
if (this.canRotate() &&
|
|
452
|
+
this.rotationTimer === null &&
|
|
453
|
+
this.initialDelayTimer === null &&
|
|
454
|
+
!this.isStaticMode()) {
|
|
455
|
+
this.startRotationTimer();
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
// Auto-rotation only runs when nothing is holding it: no user-initiated pause,
|
|
459
|
+
// and no hover or focus on the controls. Using a control implies one of those
|
|
460
|
+
// interactions, so the timer can't be restarted out from under the user
|
|
461
|
+
canRotate() {
|
|
462
|
+
return !this.isPaused && this.interactionPauses.size === 0;
|
|
463
|
+
}
|
|
464
|
+
updatePreviousButtonState() {
|
|
465
|
+
if (this.previousButton) {
|
|
466
|
+
this.previousButton.disabled = this.history.length === 0;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
// Announce user-initiated image changes to screen readers. Auto-rotation is
|
|
470
|
+
// intentionally not announced to avoid interrupting every few seconds.
|
|
471
|
+
announceImage() {
|
|
472
|
+
var _a;
|
|
473
|
+
if (!this.liveRegion)
|
|
474
|
+
return;
|
|
475
|
+
const item = this.items[this.currentIndex];
|
|
476
|
+
const description = ((_a = this.getItemImage(item)) === null || _a === void 0 ? void 0 : _a.getAttribute("alt")) ||
|
|
477
|
+
this.getItemAttribution(item);
|
|
478
|
+
this.liveRegion.textContent = `Background image ${this.currentIndex + 1} of ${this.items.length}${description ? `: ${description}` : ""}`;
|
|
479
|
+
}
|
|
480
|
+
createControlButton(className, ariaLabel, icon) {
|
|
481
|
+
const button = document.createElement("button");
|
|
482
|
+
button.type = "button";
|
|
483
|
+
button.className = className;
|
|
484
|
+
button.setAttribute("aria-label", ariaLabel);
|
|
485
|
+
button.innerHTML = icon;
|
|
486
|
+
return button;
|
|
487
|
+
}
|
|
488
|
+
updatePauseButton() {
|
|
489
|
+
if (!this.pauseButton)
|
|
490
|
+
return;
|
|
491
|
+
this.pauseButton.innerHTML = this.isPaused
|
|
492
|
+
? this.playIcon()
|
|
493
|
+
: this.pauseIcon();
|
|
494
|
+
this.pauseButton.setAttribute("aria-label", this.isPaused ? "Play background rotation" : "Pause background rotation");
|
|
495
|
+
this.pauseButton.setAttribute("aria-pressed", String(this.isPaused));
|
|
496
|
+
}
|
|
497
|
+
pauseIcon() {
|
|
498
|
+
return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><rect x="6" y="4" width="4" height="16" rx="1"/><rect x="14" y="4" width="4" height="16" rx="1"/></svg>';
|
|
499
|
+
}
|
|
500
|
+
playIcon() {
|
|
501
|
+
return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg>';
|
|
502
|
+
}
|
|
503
|
+
getNextIndex() {
|
|
504
|
+
switch (this.options.slideOrder) {
|
|
505
|
+
case "sequential":
|
|
506
|
+
return (this.currentIndex + 1) % this.items.length;
|
|
507
|
+
case "true-random":
|
|
508
|
+
return this.getRandomIndex(this.currentIndex);
|
|
509
|
+
case "random":
|
|
510
|
+
default:
|
|
511
|
+
return this.getNextFromRandomBag();
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
// "Random bag" selection: every image is displayed once before any image is
|
|
515
|
+
// repeated, and the current image is never repeated back-to-back
|
|
516
|
+
getNextFromRandomBag() {
|
|
517
|
+
if (this.randomBag.length === 0) {
|
|
518
|
+
this.randomBag = this.items
|
|
519
|
+
.map((_, index) => index)
|
|
520
|
+
.filter((index) => index !== this.currentIndex);
|
|
521
|
+
for (let i = this.randomBag.length - 1; i > 0; i--) {
|
|
522
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
523
|
+
[this.randomBag[i], this.randomBag[j]] = [
|
|
524
|
+
this.randomBag[j],
|
|
525
|
+
this.randomBag[i],
|
|
526
|
+
];
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return this.randomBag.pop();
|
|
530
|
+
}
|
|
531
|
+
getRandomIndex(excludeIndex = -1) {
|
|
532
|
+
if (this.items.length <= 1)
|
|
533
|
+
return 0;
|
|
534
|
+
let index = excludeIndex;
|
|
535
|
+
while (index === excludeIndex) {
|
|
536
|
+
index = Math.floor(Math.random() * this.items.length);
|
|
537
|
+
}
|
|
538
|
+
return index;
|
|
539
|
+
}
|
|
540
|
+
// Each item can set a data-theme="light" or data-theme="dark" (default)
|
|
541
|
+
// attribute to control the .body-title h1 text color shown over its image
|
|
542
|
+
getItemTheme(item) {
|
|
543
|
+
var _a;
|
|
544
|
+
return ((_a = item.getAttribute("data-theme")) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === "light"
|
|
545
|
+
? "light"
|
|
546
|
+
: "dark";
|
|
547
|
+
}
|
|
548
|
+
// Each item carries its own figattribution/figcaption (added by the MediaAttribution
|
|
549
|
+
// component or authored directly), so it cross-fades in sync with its image
|
|
550
|
+
getItemAttribution(item) {
|
|
551
|
+
var _a;
|
|
552
|
+
const attribution = item.matches("img")
|
|
553
|
+
? (_a = item.parentElement) === null || _a === void 0 ? void 0 : _a.querySelector("figattribution, figcaption")
|
|
554
|
+
: item.querySelector("figattribution, figcaption");
|
|
555
|
+
return attribution ? attribution.textContent : null;
|
|
556
|
+
}
|
|
557
|
+
}
|
package/dist/remember-me.d.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { EnForm, RememberMeEvents } from "./events";
|
|
|
2
2
|
export declare class RememberMe {
|
|
3
3
|
_form: EnForm;
|
|
4
4
|
_events: RememberMeEvents;
|
|
5
|
-
private _frequency;
|
|
6
5
|
private remoteUrl;
|
|
7
6
|
private cookieName;
|
|
8
7
|
private fieldNames;
|
|
@@ -15,7 +14,6 @@ export declare class RememberMe {
|
|
|
15
14
|
private fieldDonationAmountRadioName;
|
|
16
15
|
private fieldDonationAmountOtherName;
|
|
17
16
|
private fieldDonationRecurrPayRadioName;
|
|
18
|
-
private fieldDonationRecurrFreqRadioName;
|
|
19
17
|
private fieldDonationAmountOtherCheckboxID;
|
|
20
18
|
private fieldOptInSelectorTarget;
|
|
21
19
|
private fieldOptInSelectorTargetLocation;
|
|
@@ -31,7 +29,6 @@ export declare class RememberMe {
|
|
|
31
29
|
fieldDonationAmountRadioName?: string;
|
|
32
30
|
fieldDonationAmountOtherName?: string;
|
|
33
31
|
fieldDonationRecurrPayRadioName?: string;
|
|
34
|
-
fieldDonationRecurrFreqRadioName?: string;
|
|
35
32
|
fieldDonationAmountOtherCheckboxID?: string;
|
|
36
33
|
fieldOptInSelectorTarget?: string;
|
|
37
34
|
fieldOptInSelectorTargetLocation?: string;
|
|
@@ -104,25 +101,5 @@ export declare class RememberMe {
|
|
|
104
101
|
* @param overwrite - A boolean indicating whether to overwrite the existing value of the fields. Defaults to false.
|
|
105
102
|
*/
|
|
106
103
|
private writeFields;
|
|
107
|
-
/**
|
|
108
|
-
* SwapAmounts replaces the donationAmt radio DOM nodes ~1 second after page
|
|
109
|
-
* load (triggered by DonationFrequency.load() setTimeout). When that happens
|
|
110
|
-
* the selection the RememberMe just wrote gets wiped out.
|
|
111
|
-
*
|
|
112
|
-
* This method subscribes to the first onFrequencyChange event and, after a
|
|
113
|
-
* short delay to let SwapAmounts finish its DOM update, re-applies only the
|
|
114
|
-
* donation amount. It unsubscribes immediately so it only fires once.
|
|
115
|
-
*
|
|
116
|
-
* To avoid overwriting a manual donor interaction, the handler checks
|
|
117
|
-
* whether the current amount selection is empty/wiped (as SwapAmounts does)
|
|
118
|
-
* OR still matches what writeFields originally set. If the donor already
|
|
119
|
-
* picked a different amount, we skip re-application.
|
|
120
|
-
*/
|
|
121
|
-
private reapplyDonationAmtAfterSwap;
|
|
122
|
-
/**
|
|
123
|
-
* Returns the currently selected donation amount value, or null if nothing
|
|
124
|
-
* is selected. Checks both predefined radio buttons and the "Other" text input.
|
|
125
|
-
*/
|
|
126
|
-
private getCurrentSelectedAmount;
|
|
127
104
|
private isJson;
|
|
128
105
|
}
|
package/dist/remember-me.js
CHANGED
|
@@ -8,7 +8,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
10
|
import * as cookie from "./cookie";
|
|
11
|
-
import { EnForm, RememberMeEvents
|
|
11
|
+
import { EnForm, RememberMeEvents } from "./events";
|
|
12
12
|
import { ENGrid } from "./engrid";
|
|
13
13
|
const tippy = require("tippy.js").default;
|
|
14
14
|
// localStorage key used to cache the per-device AES-GCM encryption key.
|
|
@@ -18,7 +18,6 @@ export class RememberMe {
|
|
|
18
18
|
constructor(options) {
|
|
19
19
|
this._form = EnForm.getInstance();
|
|
20
20
|
this._events = RememberMeEvents.getInstance();
|
|
21
|
-
this._frequency = DonationFrequency.getInstance();
|
|
22
21
|
this.iframe = null;
|
|
23
22
|
this.encryptData = options.encryptData ? options.encryptData : false;
|
|
24
23
|
this.hide = options.hide ? options.hide : false;
|
|
@@ -41,10 +40,6 @@ export class RememberMe {
|
|
|
41
40
|
options.fieldDonationRecurrPayRadioName
|
|
42
41
|
? options.fieldDonationRecurrPayRadioName
|
|
43
42
|
: "transaction.recurrpay";
|
|
44
|
-
this.fieldDonationRecurrFreqRadioName =
|
|
45
|
-
options.fieldDonationRecurrFreqRadioName
|
|
46
|
-
? options.fieldDonationRecurrFreqRadioName
|
|
47
|
-
: "transaction.recurrfreq";
|
|
48
43
|
this.fieldDonationAmountOtherCheckboxID =
|
|
49
44
|
options.fieldDonationAmountOtherCheckboxID
|
|
50
45
|
? options.fieldDonationAmountOtherCheckboxID
|
|
@@ -106,7 +101,6 @@ export class RememberMe {
|
|
|
106
101
|
}
|
|
107
102
|
else {
|
|
108
103
|
this.insertClearRememberMeLink();
|
|
109
|
-
this.reapplyDonationAmtAfterSwap();
|
|
110
104
|
}
|
|
111
105
|
}
|
|
112
106
|
});
|
|
@@ -126,9 +120,6 @@ export class RememberMe {
|
|
|
126
120
|
this.insertClearRememberMeLink();
|
|
127
121
|
}
|
|
128
122
|
this.writeFields();
|
|
129
|
-
if (hasFieldData) {
|
|
130
|
-
this.reapplyDonationAmtAfterSwap();
|
|
131
|
-
}
|
|
132
123
|
this._form.onSubmit.subscribe(() => {
|
|
133
124
|
if (this.rememberMeOptIn) {
|
|
134
125
|
this.readFields();
|
|
@@ -147,9 +138,6 @@ export class RememberMe {
|
|
|
147
138
|
this.insertClearRememberMeLink();
|
|
148
139
|
}
|
|
149
140
|
this.writeFields();
|
|
150
|
-
if (hasFieldData) {
|
|
151
|
-
this.reapplyDonationAmtAfterSwap();
|
|
152
|
-
}
|
|
153
141
|
this._form.onSubmit.subscribe(() => {
|
|
154
142
|
if (this.rememberMeOptIn) {
|
|
155
143
|
this.readFields();
|
|
@@ -483,17 +471,6 @@ export class RememberMe {
|
|
|
483
471
|
if (type === "radio" || type === "checkbox") {
|
|
484
472
|
field = document.querySelector(fieldSelector + ":checked");
|
|
485
473
|
}
|
|
486
|
-
// When the donation amount radio is set to "Other", save the actual
|
|
487
|
-
// custom value from the .other text input instead of "Other".
|
|
488
|
-
if (this.fieldNames[i] === this.fieldDonationAmountRadioName &&
|
|
489
|
-
field &&
|
|
490
|
-
field.value.toLowerCase() === "other") {
|
|
491
|
-
const otherField = document.querySelector("input[name='" + this.fieldDonationAmountOtherName + "']");
|
|
492
|
-
if (otherField && otherField.value) {
|
|
493
|
-
this.fieldData[this.fieldNames[i]] = encodeURIComponent(otherField.value);
|
|
494
|
-
continue;
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
474
|
this.fieldData[this.fieldNames[i]] = encodeURIComponent(field.value);
|
|
498
475
|
}
|
|
499
476
|
else if (field.tagName === "SELECT") {
|
|
@@ -577,36 +554,17 @@ export class RememberMe {
|
|
|
577
554
|
field.click();
|
|
578
555
|
}
|
|
579
556
|
}
|
|
580
|
-
else if (this.fieldNames[i] === this.fieldDonationRecurrFreqRadioName) {
|
|
581
|
-
// recurrfreq is a radio group — find the specific radio with the saved value and click it
|
|
582
|
-
const savedValue = this.fieldData[this.fieldNames[i]];
|
|
583
|
-
if (savedValue) {
|
|
584
|
-
const freqRadio = document.querySelector(fieldSelector + "[value='" + CSS.escape(savedValue) + "']");
|
|
585
|
-
if (freqRadio) {
|
|
586
|
-
freqRadio.click();
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
557
|
else if (this.fieldDonationAmountRadioName === this.fieldNames[i]) {
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
558
|
+
field = document.querySelector(fieldSelector +
|
|
559
|
+
"[value='" +
|
|
560
|
+
this.fieldData[this.fieldNames[i]] +
|
|
561
|
+
"']");
|
|
594
562
|
if (field) {
|
|
595
|
-
// Saved value matches a predefined radio option — just click it
|
|
596
563
|
field.click();
|
|
597
564
|
}
|
|
598
565
|
else {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
// then fill in the numeric value.
|
|
602
|
-
const otherRadio = document.querySelector(fieldSelector + "[value='Other'], " +
|
|
603
|
-
fieldSelector + "[value='other'], " +
|
|
604
|
-
fieldSelector + "[value='OTHER']");
|
|
605
|
-
if (otherRadio) {
|
|
606
|
-
otherRadio.click();
|
|
607
|
-
}
|
|
608
|
-
const otherField = document.querySelector("input[name='" + this.fieldDonationAmountOtherName + "']");
|
|
609
|
-
this.setFieldValue(otherField, savedAmt, true);
|
|
566
|
+
field = document.querySelector("input[name='" + this.fieldDonationAmountOtherName + "']");
|
|
567
|
+
this.setFieldValue(field, this.fieldData[this.fieldNames[i]], true);
|
|
610
568
|
}
|
|
611
569
|
}
|
|
612
570
|
else {
|
|
@@ -619,74 +577,6 @@ export class RememberMe {
|
|
|
619
577
|
}
|
|
620
578
|
}
|
|
621
579
|
}
|
|
622
|
-
/**
|
|
623
|
-
* SwapAmounts replaces the donationAmt radio DOM nodes ~1 second after page
|
|
624
|
-
* load (triggered by DonationFrequency.load() setTimeout). When that happens
|
|
625
|
-
* the selection the RememberMe just wrote gets wiped out.
|
|
626
|
-
*
|
|
627
|
-
* This method subscribes to the first onFrequencyChange event and, after a
|
|
628
|
-
* short delay to let SwapAmounts finish its DOM update, re-applies only the
|
|
629
|
-
* donation amount. It unsubscribes immediately so it only fires once.
|
|
630
|
-
*
|
|
631
|
-
* To avoid overwriting a manual donor interaction, the handler checks
|
|
632
|
-
* whether the current amount selection is empty/wiped (as SwapAmounts does)
|
|
633
|
-
* OR still matches what writeFields originally set. If the donor already
|
|
634
|
-
* picked a different amount, we skip re-application.
|
|
635
|
-
*/
|
|
636
|
-
reapplyDonationAmtAfterSwap() {
|
|
637
|
-
const savedAmt = this.fieldData[this.fieldDonationAmountRadioName];
|
|
638
|
-
if (!savedAmt)
|
|
639
|
-
return;
|
|
640
|
-
// Capture the amount that writeFields just set so we can detect manual changes
|
|
641
|
-
const amountAtRegistration = this.getCurrentSelectedAmount();
|
|
642
|
-
const handler = () => {
|
|
643
|
-
// SwapAmounts calls _amount.load() after swapList — give it a tick to settle
|
|
644
|
-
window.setTimeout(() => {
|
|
645
|
-
const currentAmt = this.getCurrentSelectedAmount();
|
|
646
|
-
// Only re-apply if the selection is now empty (DOM was swapped out)
|
|
647
|
-
// or still matches what we originally wrote. If the donor manually
|
|
648
|
-
// selected a different amount, respect their choice.
|
|
649
|
-
const selectionWiped = currentAmt === null || currentAmt === "";
|
|
650
|
-
const selectionUnchanged = currentAmt === amountAtRegistration;
|
|
651
|
-
if (!selectionWiped && !selectionUnchanged) {
|
|
652
|
-
return;
|
|
653
|
-
}
|
|
654
|
-
const fieldSelector = "[name='" + this.fieldDonationAmountRadioName + "']";
|
|
655
|
-
const escapedAmt = CSS.escape(savedAmt);
|
|
656
|
-
let radio = document.querySelector(fieldSelector + "[value='" + escapedAmt + "']");
|
|
657
|
-
if (radio) {
|
|
658
|
-
radio.click();
|
|
659
|
-
}
|
|
660
|
-
else {
|
|
661
|
-
// Custom amount: click "Other" radio then fill the text input
|
|
662
|
-
const otherRadio = document.querySelector(fieldSelector + "[value='Other'], " +
|
|
663
|
-
fieldSelector + "[value='other'], " +
|
|
664
|
-
fieldSelector + "[value='OTHER']");
|
|
665
|
-
if (otherRadio)
|
|
666
|
-
otherRadio.click();
|
|
667
|
-
const otherField = document.querySelector("input[name='" + this.fieldDonationAmountOtherName + "']");
|
|
668
|
-
this.setFieldValue(otherField, savedAmt, true);
|
|
669
|
-
}
|
|
670
|
-
}, 200);
|
|
671
|
-
};
|
|
672
|
-
// Subscribe once: fires on the first frequency change then auto-unsubscribes
|
|
673
|
-
this._frequency.onFrequencyChange.one(handler);
|
|
674
|
-
}
|
|
675
|
-
/**
|
|
676
|
-
* Returns the currently selected donation amount value, or null if nothing
|
|
677
|
-
* is selected. Checks both predefined radio buttons and the "Other" text input.
|
|
678
|
-
*/
|
|
679
|
-
getCurrentSelectedAmount() {
|
|
680
|
-
const fieldSelector = "[name='" + this.fieldDonationAmountRadioName + "']";
|
|
681
|
-
const checkedRadio = document.querySelector(fieldSelector + ":checked");
|
|
682
|
-
if (!checkedRadio)
|
|
683
|
-
return null;
|
|
684
|
-
if (checkedRadio.value.toLowerCase() === "other") {
|
|
685
|
-
const otherField = document.querySelector("input[name='" + this.fieldDonationAmountOtherName + "']");
|
|
686
|
-
return otherField ? otherField.value : null;
|
|
687
|
-
}
|
|
688
|
-
return checkedRadio.value;
|
|
689
|
-
}
|
|
690
580
|
isJson(str) {
|
|
691
581
|
try {
|
|
692
582
|
JSON.parse(str);
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const AppVersion = "0.28.
|
|
1
|
+
export declare const AppVersion = "0.28.3";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const AppVersion = "0.28.
|
|
1
|
+
export const AppVersion = "0.28.3";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@4site/engrid-scripts",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.3",
|
|
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": "c1aaaea0b49ed3d1f27eaaa62814d7ba02b950f7",
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"nodemon": "^2.0.20",
|
|
31
31
|
"typescript": "^4.9.5"
|