@defra/forms-engine-plugin 4.14.0 → 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.
- package/.public/javascripts/application.min.js +1 -1
- package/.public/javascripts/application.min.js.map +1 -1
- package/.public/javascripts/shared.min.js +1 -1
- package/.public/javascripts/shared.min.js.map +1 -1
- package/.server/client/javascripts/debounce-click.d.ts +10 -0
- package/.server/client/javascripts/debounce-click.js +58 -0
- package/.server/client/javascripts/debounce-click.js.map +1 -0
- package/.server/client/javascripts/debounce-click.test.js +191 -0
- package/.server/client/javascripts/debounce-click.test.js.map +1 -0
- package/.server/client/javascripts/geospatial-map.d.ts +18 -0
- package/.server/client/javascripts/geospatial-map.js +4 -4
- package/.server/client/javascripts/geospatial-map.js.map +1 -1
- package/.server/client/javascripts/shared.d.ts +2 -0
- package/.server/client/javascripts/shared.js +3 -0
- package/.server/client/javascripts/shared.js.map +1 -1
- package/.server/server/plugins/engine/views/summary.html +5 -1
- package/package.json +1 -1
- package/src/client/javascripts/debounce-click.js +62 -0
- package/src/client/javascripts/geospatial-map.js +13 -4
- package/src/client/javascripts/shared.js +3 -0
- package/src/server/plugins/engine/views/summary.html +5 -1
|
@@ -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":[]}
|
|
@@ -51,6 +51,23 @@ export function focusFeature(feature: Feature, mapProvider: MapLibreMap): void;
|
|
|
51
51
|
* @param {boolean} [readonly] - render the list item in readonly mode
|
|
52
52
|
*/
|
|
53
53
|
export function createFeatureHTML(feature: Feature, index: number, mapId: string, disabled?: boolean, readonly?: boolean): string;
|
|
54
|
+
/**
|
|
55
|
+
* Factory closure to manage the UI
|
|
56
|
+
* @param {GeoJSON} geojson - the features
|
|
57
|
+
* @param {InteractiveMap} map - the map
|
|
58
|
+
* @param {string} mapId - the ID of the map
|
|
59
|
+
* @param {HTMLDivElement} listEl - where to render the feature list
|
|
60
|
+
* @param {HTMLTextAreaElement} geospatialInput - the geospatial textarea
|
|
61
|
+
* @param { UIManagerOptions | undefined } options - extra options such as allowable geometry types
|
|
62
|
+
*/
|
|
63
|
+
export function getUIManager(geojson: GeoJSON, map: InteractiveMap, mapId: string, listEl: HTMLDivElement, geospatialInput: HTMLTextAreaElement, options: UIManagerOptions | undefined): {
|
|
64
|
+
renderList: RenderList;
|
|
65
|
+
renderValue: RenderValue;
|
|
66
|
+
listEl: HTMLDivElement;
|
|
67
|
+
toggleActionButtons: (hidden: boolean) => void;
|
|
68
|
+
focusDescriptionInput: () => void;
|
|
69
|
+
getAllowableGeometryTypes: () => string[];
|
|
70
|
+
};
|
|
54
71
|
export type GeoJSON = {
|
|
55
72
|
/**
|
|
56
73
|
* - the GeoJSON type string
|
|
@@ -210,4 +227,5 @@ import type { Feature } from '../../server/plugins/engine/types.js';
|
|
|
210
227
|
import type { InteractiveMap } from '../../client/javascripts/map.js';
|
|
211
228
|
import type { FeatureCollection } from '../../server/plugins/engine/types.js';
|
|
212
229
|
import type { MapLibreMap } from '../../client/javascripts/map.js';
|
|
230
|
+
import type { UIManagerOptions } from '../../client/javascripts/map.js';
|
|
213
231
|
import type { Geometry } from '../../server/plugins/engine/types.js';
|
|
@@ -186,7 +186,7 @@ export function processGeospatial(config, geospatial, index) {
|
|
|
186
186
|
} = createMap(mapId, initConfig, config);
|
|
187
187
|
const featuresManager = getFeaturesManager(geojson);
|
|
188
188
|
const activeFeatureManager = getActiveFeatureManager();
|
|
189
|
-
const geometryTypes = geospatial.dataset.geometrytypes
|
|
189
|
+
const geometryTypes = geospatial.dataset.geometrytypes;
|
|
190
190
|
const options = {
|
|
191
191
|
geometryTypes
|
|
192
192
|
};
|
|
@@ -506,15 +506,15 @@ function getValueRenderer(geojson, geospatialInput) {
|
|
|
506
506
|
* @param {string} mapId - the ID of the map
|
|
507
507
|
* @param {HTMLDivElement} listEl - where to render the feature list
|
|
508
508
|
* @param {HTMLTextAreaElement} geospatialInput - the geospatial textarea
|
|
509
|
-
* @param {UIManagerOptions} options - extra options such as allowable geometry types
|
|
509
|
+
* @param { UIManagerOptions | undefined } options - extra options such as allowable geometry types
|
|
510
510
|
*/
|
|
511
|
-
function getUIManager(geojson, map, mapId, listEl, geospatialInput, options) {
|
|
511
|
+
export function getUIManager(geojson, map, mapId, listEl, geospatialInput, options) {
|
|
512
512
|
/**
|
|
513
513
|
* Get a CSV list of geometry types the user can create
|
|
514
514
|
* @returns {string[]}
|
|
515
515
|
*/
|
|
516
516
|
function getAllowableGeometryTypes() {
|
|
517
|
-
return options
|
|
517
|
+
return options?.geometryTypes ? options.geometryTypes.split(',') : ['point', 'line', 'shape'];
|
|
518
518
|
}
|
|
519
519
|
|
|
520
520
|
/**
|