@defra/forms-engine-plugin 4.14.1 → 4.14.2-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Attaches {@link handleButtonClick} to every button that carries the
3
+ * `prevent-multiple-clicks` CSS class so that double-submissions are blocked
4
+ * across the page.
5
+ *
6
+ * Safe to call multiple times — adding the same listener twice on a given
7
+ * element has no effect (the browser deduplicates identical listener/options
8
+ * pairs).
9
+ */
10
+ export function initDebounceClick(): void;
@@ -0,0 +1,58 @@
1
+ /** How long (ms) a button stays locked after the first click. */
2
+ const DEBOUNCE_TIMEOUT_MS = 10_000;
3
+
4
+ /**
5
+ * Shared debounce logic used by both the click and keydown handlers.
6
+ * @param {Event} event
7
+ */
8
+ function handleActivation(event) {
9
+ const button = /** @type {HTMLButtonElement} */event.currentTarget;
10
+ if (button.dataset.debouncing === 'true') {
11
+ event.preventDefault();
12
+ event.stopImmediatePropagation();
13
+ return;
14
+ }
15
+ button.dataset.debouncing = 'true';
16
+ setTimeout(() => {
17
+ delete button.dataset.debouncing;
18
+ }, DEBOUNCE_TIMEOUT_MS);
19
+ }
20
+
21
+ /**
22
+ * Click handler that prevents a button from being activated more than once
23
+ * within {@link DEBOUNCE_TIMEOUT_MS}.
24
+ * @param {MouseEvent} event
25
+ */
26
+ function handleButtonClick(event) {
27
+ handleActivation(event);
28
+ }
29
+
30
+ /**
31
+ * Keydown handler that prevents a button from being activated more than once
32
+ * within {@link DEBOUNCE_TIMEOUT_MS} when submitted via Enter or Space.
33
+ * @param {KeyboardEvent} event
34
+ */
35
+ function handleButtonKeydown(event) {
36
+ if (event.key === 'Enter' || event.key === ' ') {
37
+ handleActivation(event);
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Attaches {@link handleButtonClick} to every button that carries the
43
+ * `prevent-multiple-clicks` CSS class so that double-submissions are blocked
44
+ * across the page.
45
+ *
46
+ * Safe to call multiple times — adding the same listener twice on a given
47
+ * element has no effect (the browser deduplicates identical listener/options
48
+ * pairs).
49
+ */
50
+ export function initDebounceClick() {
51
+ const buttons = /** @type {NodeListOf<HTMLButtonElement>} */
52
+ document.querySelectorAll('.prevent-multiple-clicks');
53
+ for (const button of buttons) {
54
+ button.addEventListener('click', handleButtonClick);
55
+ button.addEventListener('keydown', handleButtonKeydown);
56
+ }
57
+ }
58
+ //# sourceMappingURL=debounce-click.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"debounce-click.js","names":["DEBOUNCE_TIMEOUT_MS","handleActivation","event","button","currentTarget","dataset","debouncing","preventDefault","stopImmediatePropagation","setTimeout","handleButtonClick","handleButtonKeydown","key","initDebounceClick","buttons","document","querySelectorAll","addEventListener"],"sources":["../../../src/client/javascripts/debounce-click.js"],"sourcesContent":["/** How long (ms) a button stays locked after the first click. */\nconst DEBOUNCE_TIMEOUT_MS = 10_000\n\n/**\n * Shared debounce logic used by both the click and keydown handlers.\n * @param {Event} event\n */\nfunction handleActivation(event) {\n const button = /** @type {HTMLButtonElement} */ (event.currentTarget)\n\n if (button.dataset.debouncing === 'true') {\n event.preventDefault()\n event.stopImmediatePropagation()\n return\n }\n\n button.dataset.debouncing = 'true'\n\n setTimeout(() => {\n delete button.dataset.debouncing\n }, DEBOUNCE_TIMEOUT_MS)\n}\n\n/**\n * Click handler that prevents a button from being activated more than once\n * within {@link DEBOUNCE_TIMEOUT_MS}.\n * @param {MouseEvent} event\n */\nfunction handleButtonClick(event) {\n handleActivation(event)\n}\n\n/**\n * Keydown handler that prevents a button from being activated more than once\n * within {@link DEBOUNCE_TIMEOUT_MS} when submitted via Enter or Space.\n * @param {KeyboardEvent} event\n */\nfunction handleButtonKeydown(event) {\n if (event.key === 'Enter' || event.key === ' ') {\n handleActivation(event)\n }\n}\n\n/**\n * Attaches {@link handleButtonClick} to every button that carries the\n * `prevent-multiple-clicks` CSS class so that double-submissions are blocked\n * across the page.\n *\n * Safe to call multiple times — adding the same listener twice on a given\n * element has no effect (the browser deduplicates identical listener/options\n * pairs).\n */\nexport function initDebounceClick() {\n const buttons = /** @type {NodeListOf<HTMLButtonElement>} */ (\n document.querySelectorAll('.prevent-multiple-clicks')\n )\n\n for (const button of buttons) {\n button.addEventListener('click', handleButtonClick)\n button.addEventListener('keydown', handleButtonKeydown)\n }\n}\n"],"mappings":"AAAA;AACA,MAAMA,mBAAmB,GAAG,MAAM;;AAElC;AACA;AACA;AACA;AACA,SAASC,gBAAgBA,CAACC,KAAK,EAAE;EAC/B,MAAMC,MAAM,GAAG,gCAAkCD,KAAK,CAACE,aAAc;EAErE,IAAID,MAAM,CAACE,OAAO,CAACC,UAAU,KAAK,MAAM,EAAE;IACxCJ,KAAK,CAACK,cAAc,CAAC,CAAC;IACtBL,KAAK,CAACM,wBAAwB,CAAC,CAAC;IAChC;EACF;EAEAL,MAAM,CAACE,OAAO,CAACC,UAAU,GAAG,MAAM;EAElCG,UAAU,CAAC,MAAM;IACf,OAAON,MAAM,CAACE,OAAO,CAACC,UAAU;EAClC,CAAC,EAAEN,mBAAmB,CAAC;AACzB;;AAEA;AACA;AACA;AACA;AACA;AACA,SAASU,iBAAiBA,CAACR,KAAK,EAAE;EAChCD,gBAAgB,CAACC,KAAK,CAAC;AACzB;;AAEA;AACA;AACA;AACA;AACA;AACA,SAASS,mBAAmBA,CAACT,KAAK,EAAE;EAClC,IAAIA,KAAK,CAACU,GAAG,KAAK,OAAO,IAAIV,KAAK,CAACU,GAAG,KAAK,GAAG,EAAE;IAC9CX,gBAAgB,CAACC,KAAK,CAAC;EACzB;AACF;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASW,iBAAiBA,CAAA,EAAG;EAClC,MAAMC,OAAO,GAAG;EACdC,QAAQ,CAACC,gBAAgB,CAAC,0BAA0B,CACrD;EAED,KAAK,MAAMb,MAAM,IAAIW,OAAO,EAAE;IAC5BX,MAAM,CAACc,gBAAgB,CAAC,OAAO,EAAEP,iBAAiB,CAAC;IACnDP,MAAM,CAACc,gBAAgB,CAAC,SAAS,EAAEN,mBAAmB,CAAC;EACzD;AACF","ignoreList":[]}
@@ -0,0 +1,191 @@
1
+ import { initDebounceClick } from "./debounce-click.js";
2
+ const DEBOUNCE_TIMEOUT_MS = 10_000;
3
+
4
+ /**
5
+ * @param {string} [extraClasses]
6
+ * @returns {HTMLButtonElement}
7
+ */
8
+ function makeButton(extraClasses = '') {
9
+ const button = document.createElement('button');
10
+ button.className = `prevent-multiple-clicks ${extraClasses}`.trim();
11
+ document.body.appendChild(button);
12
+ return button;
13
+ }
14
+
15
+ /**
16
+ * @param {HTMLButtonElement} button
17
+ * @returns {MouseEvent}
18
+ */
19
+ function click(button) {
20
+ const event = new MouseEvent('click', {
21
+ bubbles: true,
22
+ cancelable: true
23
+ });
24
+ button.dispatchEvent(event);
25
+ return event;
26
+ }
27
+
28
+ /**
29
+ * @param {HTMLButtonElement} button
30
+ * @param {string} key
31
+ * @returns {KeyboardEvent}
32
+ */
33
+ function keydown(button, key) {
34
+ const event = new KeyboardEvent('keydown', {
35
+ key,
36
+ bubbles: true,
37
+ cancelable: true
38
+ });
39
+ button.dispatchEvent(event);
40
+ return event;
41
+ }
42
+ afterEach(() => {
43
+ document.body.innerHTML = '';
44
+ });
45
+ describe('initDebounceClick', () => {
46
+ it('attaches a click listener to every .prevent-multiple-clicks button', () => {
47
+ const b1 = makeButton();
48
+ const b2 = makeButton();
49
+ const spy1 = jest.fn();
50
+ const spy2 = jest.fn();
51
+ b1.addEventListener('click', spy1);
52
+ b2.addEventListener('click', spy2);
53
+ initDebounceClick();
54
+ click(b1);
55
+ click(b2);
56
+ expect(spy1).toHaveBeenCalledTimes(1);
57
+ expect(spy2).toHaveBeenCalledTimes(1);
58
+ });
59
+ it('does not attach to buttons that lack the class', () => {
60
+ const plain = document.createElement('button');
61
+ document.body.appendChild(plain);
62
+ const spy = jest.fn();
63
+ plain.addEventListener('click', spy);
64
+ initDebounceClick();
65
+ click(plain);
66
+
67
+ // Listener still runs — debounce was never applied
68
+ expect(plain.dataset.debouncing).toBeUndefined();
69
+ });
70
+ });
71
+ describe('handleButtonClick (via initDebounceClick)', () => {
72
+ beforeEach(() => {
73
+ jest.useFakeTimers();
74
+ });
75
+ afterEach(() => {
76
+ jest.useRealTimers();
77
+ });
78
+ it('sets data-debouncing="true" on the first click', () => {
79
+ const button = makeButton();
80
+ initDebounceClick();
81
+ click(button);
82
+ expect(button.dataset.debouncing).toBe('true');
83
+ });
84
+ it('allows a second click after the debounce timeout expires', () => {
85
+ const button = makeButton();
86
+ const spy = jest.fn();
87
+ button.addEventListener('click', spy);
88
+ initDebounceClick();
89
+ click(button);
90
+ jest.advanceTimersByTime(DEBOUNCE_TIMEOUT_MS);
91
+ click(button);
92
+ expect(button.dataset.debouncing).toBe('true');
93
+ expect(spy).toHaveBeenCalledTimes(2);
94
+ });
95
+ it('removes data-debouncing after the timeout', () => {
96
+ const button = makeButton();
97
+ initDebounceClick();
98
+ click(button);
99
+ expect(button.dataset.debouncing).toBe('true');
100
+ jest.advanceTimersByTime(DEBOUNCE_TIMEOUT_MS);
101
+ expect(button.dataset.debouncing).toBeUndefined();
102
+ });
103
+ it('prevents the default action on a duplicate click', () => {
104
+ const button = makeButton();
105
+ initDebounceClick();
106
+ click(button);
107
+ const duplicate = new MouseEvent('click', {
108
+ bubbles: true,
109
+ cancelable: true
110
+ });
111
+ button.dispatchEvent(duplicate);
112
+ expect(duplicate.defaultPrevented).toBe(true);
113
+ });
114
+ it('stops immediate propagation on a duplicate click', () => {
115
+ const button = makeButton();
116
+ initDebounceClick();
117
+ click(button);
118
+ const subsequent = jest.fn();
119
+ button.addEventListener('click', subsequent);
120
+ click(button);
121
+ expect(subsequent).not.toHaveBeenCalled();
122
+ });
123
+ it('does not fire listeners registered after the handler for a click within the timeout window', () => {
124
+ const button = makeButton();
125
+ initDebounceClick();
126
+ // Registered after initDebounceClick so the debounce handler runs first and
127
+ // can call stopImmediatePropagation before this listener is reached.
128
+ const spy = jest.fn();
129
+ button.addEventListener('click', spy);
130
+ click(button);
131
+ jest.advanceTimersByTime(DEBOUNCE_TIMEOUT_MS - 1);
132
+ click(button);
133
+
134
+ // First click let through, second is still within the window — spy blocked
135
+ expect(spy).toHaveBeenCalledTimes(1);
136
+ });
137
+ });
138
+ describe('handleButtonKeydown (via initDebounceClick)', () => {
139
+ beforeEach(() => {
140
+ jest.useFakeTimers();
141
+ });
142
+ afterEach(() => {
143
+ jest.useRealTimers();
144
+ });
145
+ it('sets data-debouncing="true" on Enter', () => {
146
+ const button = makeButton();
147
+ initDebounceClick();
148
+ keydown(button, 'Enter');
149
+ expect(button.dataset.debouncing).toBe('true');
150
+ });
151
+ it('sets data-debouncing="true" on Space', () => {
152
+ const button = makeButton();
153
+ initDebounceClick();
154
+ keydown(button, ' ');
155
+ expect(button.dataset.debouncing).toBe('true');
156
+ });
157
+ it('ignores keys other than Enter and Space', () => {
158
+ const button = makeButton();
159
+ initDebounceClick();
160
+ keydown(button, 'Tab');
161
+ expect(button.dataset.debouncing).toBeUndefined();
162
+ });
163
+ it('prevents default on a duplicate Enter keydown', () => {
164
+ const button = makeButton();
165
+ initDebounceClick();
166
+ keydown(button, 'Enter');
167
+ const duplicate = keydown(button, 'Enter');
168
+ expect(duplicate.defaultPrevented).toBe(true);
169
+ });
170
+ it('stops immediate propagation on a duplicate keydown within the timeout window', () => {
171
+ const button = makeButton();
172
+ initDebounceClick();
173
+ keydown(button, 'Enter');
174
+ const spy = jest.fn();
175
+ button.addEventListener('keydown', spy);
176
+ keydown(button, 'Enter');
177
+ expect(spy).not.toHaveBeenCalled();
178
+ });
179
+ it('allows a second keydown after the debounce timeout expires', () => {
180
+ const button = makeButton();
181
+ const spy = jest.fn();
182
+ button.addEventListener('keydown', spy);
183
+ initDebounceClick();
184
+ keydown(button, 'Enter');
185
+ jest.advanceTimersByTime(DEBOUNCE_TIMEOUT_MS);
186
+ keydown(button, 'Enter');
187
+ expect(button.dataset.debouncing).toBe('true');
188
+ expect(spy).toHaveBeenCalledTimes(2);
189
+ });
190
+ });
191
+ //# sourceMappingURL=debounce-click.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"debounce-click.test.js","names":["initDebounceClick","DEBOUNCE_TIMEOUT_MS","makeButton","extraClasses","button","document","createElement","className","trim","body","appendChild","click","event","MouseEvent","bubbles","cancelable","dispatchEvent","keydown","key","KeyboardEvent","afterEach","innerHTML","describe","it","b1","b2","spy1","jest","fn","spy2","addEventListener","expect","toHaveBeenCalledTimes","plain","spy","dataset","debouncing","toBeUndefined","beforeEach","useFakeTimers","useRealTimers","toBe","advanceTimersByTime","duplicate","defaultPrevented","subsequent","not","toHaveBeenCalled"],"sources":["../../../src/client/javascripts/debounce-click.test.js"],"sourcesContent":["import { initDebounceClick } from '~/src/client/javascripts/debounce-click.js'\n\nconst DEBOUNCE_TIMEOUT_MS = 10_000\n\n/**\n * @param {string} [extraClasses]\n * @returns {HTMLButtonElement}\n */\nfunction makeButton(extraClasses = '') {\n const button = document.createElement('button')\n button.className = `prevent-multiple-clicks ${extraClasses}`.trim()\n document.body.appendChild(button)\n return button\n}\n\n/**\n * @param {HTMLButtonElement} button\n * @returns {MouseEvent}\n */\nfunction click(button) {\n const event = new MouseEvent('click', { bubbles: true, cancelable: true })\n button.dispatchEvent(event)\n return event\n}\n\n/**\n * @param {HTMLButtonElement} button\n * @param {string} key\n * @returns {KeyboardEvent}\n */\nfunction keydown(button, key) {\n const event = new KeyboardEvent('keydown', {\n key,\n bubbles: true,\n cancelable: true\n })\n button.dispatchEvent(event)\n return event\n}\n\nafterEach(() => {\n document.body.innerHTML = ''\n})\n\ndescribe('initDebounceClick', () => {\n it('attaches a click listener to every .prevent-multiple-clicks button', () => {\n const b1 = makeButton()\n const b2 = makeButton()\n const spy1 = jest.fn()\n const spy2 = jest.fn()\n b1.addEventListener('click', spy1)\n b2.addEventListener('click', spy2)\n\n initDebounceClick()\n\n click(b1)\n click(b2)\n\n expect(spy1).toHaveBeenCalledTimes(1)\n expect(spy2).toHaveBeenCalledTimes(1)\n })\n\n it('does not attach to buttons that lack the class', () => {\n const plain = document.createElement('button')\n document.body.appendChild(plain)\n const spy = jest.fn()\n plain.addEventListener('click', spy)\n\n initDebounceClick()\n click(plain)\n\n // Listener still runs — debounce was never applied\n expect(plain.dataset.debouncing).toBeUndefined()\n })\n})\n\ndescribe('handleButtonClick (via initDebounceClick)', () => {\n beforeEach(() => {\n jest.useFakeTimers()\n })\n\n afterEach(() => {\n jest.useRealTimers()\n })\n\n it('sets data-debouncing=\"true\" on the first click', () => {\n const button = makeButton()\n initDebounceClick()\n\n click(button)\n\n expect(button.dataset.debouncing).toBe('true')\n })\n\n it('allows a second click after the debounce timeout expires', () => {\n const button = makeButton()\n const spy = jest.fn()\n button.addEventListener('click', spy)\n initDebounceClick()\n\n click(button)\n jest.advanceTimersByTime(DEBOUNCE_TIMEOUT_MS)\n click(button)\n\n expect(button.dataset.debouncing).toBe('true')\n expect(spy).toHaveBeenCalledTimes(2)\n })\n\n it('removes data-debouncing after the timeout', () => {\n const button = makeButton()\n initDebounceClick()\n\n click(button)\n expect(button.dataset.debouncing).toBe('true')\n\n jest.advanceTimersByTime(DEBOUNCE_TIMEOUT_MS)\n expect(button.dataset.debouncing).toBeUndefined()\n })\n\n it('prevents the default action on a duplicate click', () => {\n const button = makeButton()\n initDebounceClick()\n click(button)\n\n const duplicate = new MouseEvent('click', {\n bubbles: true,\n cancelable: true\n })\n button.dispatchEvent(duplicate)\n\n expect(duplicate.defaultPrevented).toBe(true)\n })\n\n it('stops immediate propagation on a duplicate click', () => {\n const button = makeButton()\n initDebounceClick()\n click(button)\n\n const subsequent = jest.fn()\n button.addEventListener('click', subsequent)\n\n click(button)\n\n expect(subsequent).not.toHaveBeenCalled()\n })\n\n it('does not fire listeners registered after the handler for a click within the timeout window', () => {\n const button = makeButton()\n initDebounceClick()\n // Registered after initDebounceClick so the debounce handler runs first and\n // can call stopImmediatePropagation before this listener is reached.\n const spy = jest.fn()\n button.addEventListener('click', spy)\n\n click(button)\n jest.advanceTimersByTime(DEBOUNCE_TIMEOUT_MS - 1)\n click(button)\n\n // First click let through, second is still within the window — spy blocked\n expect(spy).toHaveBeenCalledTimes(1)\n })\n})\n\ndescribe('handleButtonKeydown (via initDebounceClick)', () => {\n beforeEach(() => {\n jest.useFakeTimers()\n })\n\n afterEach(() => {\n jest.useRealTimers()\n })\n\n it('sets data-debouncing=\"true\" on Enter', () => {\n const button = makeButton()\n initDebounceClick()\n\n keydown(button, 'Enter')\n\n expect(button.dataset.debouncing).toBe('true')\n })\n\n it('sets data-debouncing=\"true\" on Space', () => {\n const button = makeButton()\n initDebounceClick()\n\n keydown(button, ' ')\n\n expect(button.dataset.debouncing).toBe('true')\n })\n\n it('ignores keys other than Enter and Space', () => {\n const button = makeButton()\n initDebounceClick()\n\n keydown(button, 'Tab')\n\n expect(button.dataset.debouncing).toBeUndefined()\n })\n\n it('prevents default on a duplicate Enter keydown', () => {\n const button = makeButton()\n initDebounceClick()\n keydown(button, 'Enter')\n\n const duplicate = keydown(button, 'Enter')\n\n expect(duplicate.defaultPrevented).toBe(true)\n })\n\n it('stops immediate propagation on a duplicate keydown within the timeout window', () => {\n const button = makeButton()\n initDebounceClick()\n keydown(button, 'Enter')\n\n const spy = jest.fn()\n button.addEventListener('keydown', spy)\n keydown(button, 'Enter')\n\n expect(spy).not.toHaveBeenCalled()\n })\n\n it('allows a second keydown after the debounce timeout expires', () => {\n const button = makeButton()\n const spy = jest.fn()\n button.addEventListener('keydown', spy)\n initDebounceClick()\n\n keydown(button, 'Enter')\n jest.advanceTimersByTime(DEBOUNCE_TIMEOUT_MS)\n keydown(button, 'Enter')\n\n expect(button.dataset.debouncing).toBe('true')\n expect(spy).toHaveBeenCalledTimes(2)\n })\n})\n"],"mappings":"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","ignoreList":[]}
@@ -7,9 +7,11 @@ export * as map from "../../client/javascripts/map.js";
7
7
  export * as geospatialMap from "../../client/javascripts/geospatial-map.js";
8
8
  export const initAllGovuk: typeof initAllGovukImp;
9
9
  export const initAllAutocomplete: typeof initAllAutocompleteImp;
10
+ export const initDebounceClick: typeof initDebounceClickImp;
10
11
  export const initFileUpload: typeof initFileUploadImp;
11
12
  export const initPreviewCloseLink: typeof initPreviewCloseLinkImp;
12
13
  import { initAllGovuk as initAllGovukImp } from '../../client/javascripts/govuk.js';
13
14
  import { initAllAutocomplete as initAllAutocompleteImp } from '../../client/javascripts/autocomplete.js';
15
+ import { initDebounceClick as initDebounceClickImp } from '../../client/javascripts/debounce-click.js';
14
16
  import { initFileUpload as initFileUploadImp } from '../../client/javascripts/file-upload.js';
15
17
  import { initPreviewCloseLink as initPreviewCloseLinkImp } from '../../client/javascripts/preview-close-link.js';
@@ -1,4 +1,5 @@
1
1
  import { initAllAutocomplete as initAllAutocompleteImp } from "./autocomplete.js";
2
+ import { initDebounceClick as initDebounceClickImp } from "./debounce-click.js";
2
3
  import { initFileUpload as initFileUploadImp } from "./file-upload.js";
3
4
  import { initAllGovuk as initAllGovukImp } from "./govuk.js";
4
5
  import { initPreviewCloseLink as initPreviewCloseLinkImp } from "./preview-close-link.js";
@@ -7,6 +8,7 @@ export * as map from "./map.js";
7
8
  export * as geospatialMap from "./geospatial-map.js";
8
9
  export const initAllGovuk = initAllGovukImp;
9
10
  export const initAllAutocomplete = initAllAutocompleteImp;
11
+ export const initDebounceClick = initDebounceClickImp;
10
12
  export const initFileUpload = initFileUploadImp;
11
13
  export const initPreviewCloseLink = initPreviewCloseLinkImp;
12
14
 
@@ -16,6 +18,7 @@ export const initPreviewCloseLink = initPreviewCloseLinkImp;
16
18
  export function initAll() {
17
19
  initAllGovuk();
18
20
  initAllAutocomplete();
21
+ initDebounceClick();
19
22
  initFileUpload();
20
23
  initPreviewCloseLink();
21
24
  }
@@ -1 +1 @@
1
- {"version":3,"file":"shared.js","names":["initAllAutocomplete","initAllAutocompleteImp","initFileUpload","initFileUploadImp","initAllGovuk","initAllGovukImp","initPreviewCloseLink","initPreviewCloseLinkImp","initMaps","map","geospatialMap","initAll"],"sources":["../../../src/client/javascripts/shared.js"],"sourcesContent":["import { initAllAutocomplete as initAllAutocompleteImp } from '~/src/client/javascripts/autocomplete.js'\nimport { initFileUpload as initFileUploadImp } from '~/src/client/javascripts/file-upload.js'\nimport { initAllGovuk as initAllGovukImp } from '~/src/client/javascripts/govuk.js'\nimport { initPreviewCloseLink as initPreviewCloseLinkImp } from '~/src/client/javascripts/preview-close-link.js'\nexport { initMaps } from '~/src/client/javascripts/map.js'\nexport * as map from '~/src/client/javascripts/map.js'\nexport * as geospatialMap from '~/src/client/javascripts/geospatial-map.js'\n\nexport const initAllGovuk = initAllGovukImp\nexport const initAllAutocomplete = initAllAutocompleteImp\nexport const initFileUpload = initFileUploadImp\nexport const initPreviewCloseLink = initPreviewCloseLinkImp\n\n/**\n * Initialise all clientside components (but not maps as this will be an opt-in for now given the additional UMD assets that are required)\n */\nexport function initAll() {\n initAllGovuk()\n initAllAutocomplete()\n initFileUpload()\n initPreviewCloseLink()\n}\n"],"mappings":"AAAA,SAASA,mBAAmB,IAAIC,sBAAsB;AACtD,SAASC,cAAc,IAAIC,iBAAiB;AAC5C,SAASC,YAAY,IAAIC,eAAe;AACxC,SAASC,oBAAoB,IAAIC,uBAAuB;AACxD,SAASC,QAAQ;AACjB,OAAO,KAAKC,GAAG;AACf,OAAO,KAAKC,aAAa;AAEzB,OAAO,MAAMN,YAAY,GAAGC,eAAe;AAC3C,OAAO,MAAML,mBAAmB,GAAGC,sBAAsB;AACzD,OAAO,MAAMC,cAAc,GAAGC,iBAAiB;AAC/C,OAAO,MAAMG,oBAAoB,GAAGC,uBAAuB;;AAE3D;AACA;AACA;AACA,OAAO,SAASI,OAAOA,CAAA,EAAG;EACxBP,YAAY,CAAC,CAAC;EACdJ,mBAAmB,CAAC,CAAC;EACrBE,cAAc,CAAC,CAAC;EAChBI,oBAAoB,CAAC,CAAC;AACxB","ignoreList":[]}
1
+ {"version":3,"file":"shared.js","names":["initAllAutocomplete","initAllAutocompleteImp","initDebounceClick","initDebounceClickImp","initFileUpload","initFileUploadImp","initAllGovuk","initAllGovukImp","initPreviewCloseLink","initPreviewCloseLinkImp","initMaps","map","geospatialMap","initAll"],"sources":["../../../src/client/javascripts/shared.js"],"sourcesContent":["import { initAllAutocomplete as initAllAutocompleteImp } from '~/src/client/javascripts/autocomplete.js'\nimport { initDebounceClick as initDebounceClickImp } from '~/src/client/javascripts/debounce-click.js'\nimport { initFileUpload as initFileUploadImp } from '~/src/client/javascripts/file-upload.js'\nimport { initAllGovuk as initAllGovukImp } from '~/src/client/javascripts/govuk.js'\nimport { initPreviewCloseLink as initPreviewCloseLinkImp } from '~/src/client/javascripts/preview-close-link.js'\nexport { initMaps } from '~/src/client/javascripts/map.js'\nexport * as map from '~/src/client/javascripts/map.js'\nexport * as geospatialMap from '~/src/client/javascripts/geospatial-map.js'\n\nexport const initAllGovuk = initAllGovukImp\nexport const initAllAutocomplete = initAllAutocompleteImp\nexport const initDebounceClick = initDebounceClickImp\nexport const initFileUpload = initFileUploadImp\nexport const initPreviewCloseLink = initPreviewCloseLinkImp\n\n/**\n * Initialise all clientside components (but not maps as this will be an opt-in for now given the additional UMD assets that are required)\n */\nexport function initAll() {\n initAllGovuk()\n initAllAutocomplete()\n initDebounceClick()\n initFileUpload()\n initPreviewCloseLink()\n}\n"],"mappings":"AAAA,SAASA,mBAAmB,IAAIC,sBAAsB;AACtD,SAASC,iBAAiB,IAAIC,oBAAoB;AAClD,SAASC,cAAc,IAAIC,iBAAiB;AAC5C,SAASC,YAAY,IAAIC,eAAe;AACxC,SAASC,oBAAoB,IAAIC,uBAAuB;AACxD,SAASC,QAAQ;AACjB,OAAO,KAAKC,GAAG;AACf,OAAO,KAAKC,aAAa;AAEzB,OAAO,MAAMN,YAAY,GAAGC,eAAe;AAC3C,OAAO,MAAMP,mBAAmB,GAAGC,sBAAsB;AACzD,OAAO,MAAMC,iBAAiB,GAAGC,oBAAoB;AACrD,OAAO,MAAMC,cAAc,GAAGC,iBAAiB;AAC/C,OAAO,MAAMG,oBAAoB,GAAGC,uBAAuB;;AAE3D;AACA;AACA;AACA,OAAO,SAASI,OAAOA,CAAA,EAAG;EACxBP,YAAY,CAAC,CAAC;EACdN,mBAAmB,CAAC,CAAC;EACrBE,iBAAiB,CAAC,CAAC;EACnBE,cAAc,CAAC,CAAC;EAChBI,oBAAoB,CAAC,CAAC;AACxB","ignoreList":[]}
@@ -75,11 +75,15 @@
75
75
  {% set isDeclaration = declaration or components | length %}
76
76
  {% set paymentPending = paymentRequired and not paymentState %}
77
77
 
78
+ {# The prevent-multiple-clicks CSS class wires up to debounce-click.js to disable the button for 10s.
79
+ For those with JS enabled, it will help prevent multiple submissions when a large form is taking a while to submit.
80
+ When a better fix is implemented, this class can be removed and preventDoubleClick should be set back to true. #}
78
81
  {{ govukButton({
79
82
  text: "Pay and submit" if paymentPending else ("Accept and submit" if isDeclaration else "Submit"),
83
+ classes: "prevent-multiple-clicks",
80
84
  name: "action",
81
85
  value: "send",
82
- preventDoubleClick: true
86
+ preventDoubleClick: false
83
87
  }) }}
84
88
 
85
89
  {% if allowSaveAndExit %}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra/forms-engine-plugin",
3
- "version": "4.14.1",
3
+ "version": "4.14.2-beta",
4
4
  "description": "Defra forms engine",
5
5
  "type": "module",
6
6
  "files": [
@@ -0,0 +1,62 @@
1
+ /** How long (ms) a button stays locked after the first click. */
2
+ const DEBOUNCE_TIMEOUT_MS = 10_000
3
+
4
+ /**
5
+ * Shared debounce logic used by both the click and keydown handlers.
6
+ * @param {Event} event
7
+ */
8
+ function handleActivation(event) {
9
+ const button = /** @type {HTMLButtonElement} */ (event.currentTarget)
10
+
11
+ if (button.dataset.debouncing === 'true') {
12
+ event.preventDefault()
13
+ event.stopImmediatePropagation()
14
+ return
15
+ }
16
+
17
+ button.dataset.debouncing = 'true'
18
+
19
+ setTimeout(() => {
20
+ delete button.dataset.debouncing
21
+ }, DEBOUNCE_TIMEOUT_MS)
22
+ }
23
+
24
+ /**
25
+ * Click handler that prevents a button from being activated more than once
26
+ * within {@link DEBOUNCE_TIMEOUT_MS}.
27
+ * @param {MouseEvent} event
28
+ */
29
+ function handleButtonClick(event) {
30
+ handleActivation(event)
31
+ }
32
+
33
+ /**
34
+ * Keydown handler that prevents a button from being activated more than once
35
+ * within {@link DEBOUNCE_TIMEOUT_MS} when submitted via Enter or Space.
36
+ * @param {KeyboardEvent} event
37
+ */
38
+ function handleButtonKeydown(event) {
39
+ if (event.key === 'Enter' || event.key === ' ') {
40
+ handleActivation(event)
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Attaches {@link handleButtonClick} to every button that carries the
46
+ * `prevent-multiple-clicks` CSS class so that double-submissions are blocked
47
+ * across the page.
48
+ *
49
+ * Safe to call multiple times — adding the same listener twice on a given
50
+ * element has no effect (the browser deduplicates identical listener/options
51
+ * pairs).
52
+ */
53
+ export function initDebounceClick() {
54
+ const buttons = /** @type {NodeListOf<HTMLButtonElement>} */ (
55
+ document.querySelectorAll('.prevent-multiple-clicks')
56
+ )
57
+
58
+ for (const button of buttons) {
59
+ button.addEventListener('click', handleButtonClick)
60
+ button.addEventListener('keydown', handleButtonKeydown)
61
+ }
62
+ }
@@ -1,4 +1,5 @@
1
1
  import { initAllAutocomplete as initAllAutocompleteImp } from './autocomplete.js'
2
+ import { initDebounceClick as initDebounceClickImp } from './debounce-click.js'
2
3
  import { initFileUpload as initFileUploadImp } from './file-upload.js'
3
4
  import { initAllGovuk as initAllGovukImp } from './govuk.js'
4
5
  import { initPreviewCloseLink as initPreviewCloseLinkImp } from './preview-close-link.js'
@@ -8,6 +9,7 @@ export * as geospatialMap from './geospatial-map.js'
8
9
 
9
10
  export const initAllGovuk = initAllGovukImp
10
11
  export const initAllAutocomplete = initAllAutocompleteImp
12
+ export const initDebounceClick = initDebounceClickImp
11
13
  export const initFileUpload = initFileUploadImp
12
14
  export const initPreviewCloseLink = initPreviewCloseLinkImp
13
15
 
@@ -17,6 +19,7 @@ export const initPreviewCloseLink = initPreviewCloseLinkImp
17
19
  export function initAll() {
18
20
  initAllGovuk()
19
21
  initAllAutocomplete()
22
+ initDebounceClick()
20
23
  initFileUpload()
21
24
  initPreviewCloseLink()
22
25
  }
@@ -75,11 +75,15 @@
75
75
  {% set isDeclaration = declaration or components | length %}
76
76
  {% set paymentPending = paymentRequired and not paymentState %}
77
77
 
78
+ {# The prevent-multiple-clicks CSS class wires up to debounce-click.js to disable the button for 10s.
79
+ For those with JS enabled, it will help prevent multiple submissions when a large form is taking a while to submit.
80
+ When a better fix is implemented, this class can be removed and preventDoubleClick should be set back to true. #}
78
81
  {{ govukButton({
79
82
  text: "Pay and submit" if paymentPending else ("Accept and submit" if isDeclaration else "Submit"),
83
+ classes: "prevent-multiple-clicks",
80
84
  name: "action",
81
85
  value: "send",
82
- preventDoubleClick: true
86
+ preventDoubleClick: false
83
87
  }) }}
84
88
 
85
89
  {% if allowSaveAndExit %}