@verbb/formie-browser 1.0.6 → 1.0.8
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/CHANGELOG.md +23 -0
- package/dist/chunks/_survey-presentations-CFsgToRT.js +4 -0
- package/dist/chunks/{address-finder-yXU5yMjG.js → address-finder-k7_BXaRe.js} +1 -1
- package/dist/chunks/{api-DMK8NSUI.js → api-BUb6P-xu.js} +57 -51
- package/dist/chunks/{api-CmwLRq_n.js → api-C8wyLC-s.js} +235 -193
- package/dist/chunks/{bpoint-DSjqRv63.js → bpoint-CwSw_p7A.js} +1 -1
- package/dist/chunks/{captcha-eu-CWJb64_j.js → captcha-eu-CGm8Lbrj.js} +2 -2
- package/dist/chunks/{conditions-DjZOtHfS.js → conditions-B6_nzpeE.js} +95 -83
- package/dist/chunks/{eway-D3ctTbEk.js → eway-B30BMlDp.js} +2 -2
- package/dist/chunks/{friendly-captcha-v1-ByXzENYZ.js → friendly-captcha-v1-C_2reOXr.js} +1 -1
- package/dist/chunks/{friendly-captcha-v2-BPdlmhQu.js → friendly-captcha-v2-CLSAbkqx.js} +1 -1
- package/dist/chunks/{go-cardless-BwASSYSK.js → go-cardless-BiF3iLel.js} +1 -1
- package/dist/chunks/{google-address-OhKilPWE.js → google-address-BH3Zhi06.js} +1 -1
- package/dist/chunks/{hcaptcha-CdCd9oEi.js → hcaptcha-CGLAmgjO.js} +2 -2
- package/dist/chunks/{loqate-Cp2zvwpE.js → loqate-CJPzBmSm.js} +1 -1
- package/dist/chunks/{mollie-COG-ko2D.js → mollie-D__SwtcH.js} +1 -1
- package/dist/chunks/{moneris-CMH8PlRc.js → moneris-KJS1BKx7.js} +1 -1
- package/dist/chunks/{opayo-D2jsR5vh.js → opayo-A_LIkc-v.js} +4 -4
- package/dist/chunks/{paddle-DxF88RfC.js → paddle-BjbOOb0m.js} +2 -2
- package/dist/chunks/{paypal-b5pnwrA2.js → paypal-CyjHIV8v.js} +2 -2
- package/dist/chunks/{payway-BJYu9DIR.js → payway-a_cyrr3j.js} +2 -2
- package/dist/chunks/{recaptcha-enterprise-BSwi8IVX.js → recaptcha-enterprise-XPId5-bS.js} +19 -20
- package/dist/chunks/{recaptcha-shared-B4zAescW.js → recaptcha-shared-CLYJvojk.js} +15 -3
- package/dist/chunks/{recaptcha-v2-checkbox-CnDQmsKa.js → recaptcha-v2-checkbox-Cy-0PEPc.js} +2 -2
- package/dist/chunks/{recaptcha-v2-invisible-Cz5uIlp-.js → recaptcha-v2-invisible-BwRH540O.js} +2 -2
- package/dist/chunks/recaptcha-v3-No4Ykrhk.js +28 -0
- package/dist/chunks/scripts-CbQ7agX3.js +30 -0
- package/dist/chunks/{snaptcha-vz1RnZ15.js → snaptcha-DKx98oon.js} +1 -1
- package/dist/chunks/{square-BP8RWAKB.js → square-Bga6WXHV.js} +2 -2
- package/dist/chunks/{stripe-MihN63uC.js → stripe-BlqVocl3.js} +2 -2
- package/dist/chunks/{summary-D3AjxpYN.js → summary-Dks0gXS3.js} +42 -38
- package/dist/chunks/survey-likert-DmJ3klhP.js +20 -0
- package/dist/chunks/survey-rank-DeUenftw.js +127 -0
- package/dist/chunks/survey-rating-BPUYDgBJ.js +88 -0
- package/dist/chunks/{turnstile-D_Pi1CKM.js → turnstile-DJakPzQD.js} +3 -3
- package/dist/css/formie-theme.css +1 -0
- package/dist/css/theme/fields/_survey-presentations.css +396 -0
- package/dist/index.js +651 -597
- package/dist/js/contracts/schema.d.ts +5 -0
- package/dist/js/contracts/schema.d.ts.map +1 -1
- package/dist/js/core/create-formie-client.d.ts.map +1 -1
- package/dist/js/core/error-aria-live.d.ts +6 -0
- package/dist/js/core/error-aria-live.d.ts.map +1 -0
- package/dist/js/core/page-client-event.d.ts +8 -4
- package/dist/js/core/page-client-event.d.ts.map +1 -1
- package/dist/js/core/submit-result-state.d.ts.map +1 -1
- package/dist/js/core/submit-result-ui.d.ts.map +1 -1
- package/dist/js/modules/captchas/factories.d.ts.map +1 -1
- package/dist/js/modules/captchas/host.d.ts +1 -0
- package/dist/js/modules/captchas/host.d.ts.map +1 -1
- package/dist/js/modules/captchas/recaptcha-enterprise.d.ts.map +1 -1
- package/dist/js/modules/captchas/recaptcha-shared.d.ts +1 -0
- package/dist/js/modules/captchas/recaptcha-shared.d.ts.map +1 -1
- package/dist/js/modules/captchas/recaptcha-v3.d.ts.map +1 -1
- package/dist/js/modules/captchas/turnstile.d.ts.map +1 -1
- package/dist/js/modules/fields/conditions/effects.d.ts.map +1 -1
- package/dist/js/modules/fields/index.d.ts.map +1 -1
- package/dist/js/modules/fields/summary.d.ts.map +1 -1
- package/dist/js/modules/fields/survey-likert.d.ts +3 -0
- package/dist/js/modules/fields/survey-likert.d.ts.map +1 -0
- package/dist/js/modules/fields/survey-rank.d.ts +3 -0
- package/dist/js/modules/fields/survey-rank.d.ts.map +1 -0
- package/dist/js/modules/fields/survey-rating.d.ts +3 -0
- package/dist/js/modules/fields/survey-rating.d.ts.map +1 -0
- package/dist/js/transport/forms-api.d.ts.map +1 -1
- package/dist/js/utils/form-started-at.d.ts +2 -0
- package/dist/js/utils/form-started-at.d.ts.map +1 -0
- package/dist/js/utils/scripts.d.ts.map +1 -1
- package/dist/js/utils/unload-warning.d.ts.map +1 -1
- package/dist/js/validation/enter-key-guard.d.ts +2 -0
- package/dist/js/validation/enter-key-guard.d.ts.map +1 -0
- package/dist/js/validation/types.d.ts +1 -0
- package/dist/js/validation/types.d.ts.map +1 -1
- package/dist/js/validation/validator.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/css/formie-theme.css +1 -0
- package/src/css/theme/fields/_survey-presentations.css +396 -0
- package/src/js/contracts/schema.ts +6 -0
- package/src/js/core/create-formie-client.ts +21 -1
- package/src/js/core/error-aria-live.ts +45 -0
- package/src/js/core/page-client-event.ts +86 -18
- package/src/js/core/submit-result-state.ts +10 -2
- package/src/js/core/submit-result-ui.ts +8 -4
- package/src/js/modules/captchas/factories.ts +4 -0
- package/src/js/modules/captchas/host.ts +14 -2
- package/src/js/modules/captchas/recaptcha-enterprise.ts +14 -11
- package/src/js/modules/captchas/recaptcha-shared.ts +14 -0
- package/src/js/modules/captchas/recaptcha-v3.ts +7 -4
- package/src/js/modules/captchas/turnstile.ts +9 -3
- package/src/js/modules/fields/conditions/effects.ts +42 -0
- package/src/js/modules/fields/index.ts +3 -0
- package/src/js/modules/fields/summary.ts +17 -3
- package/src/js/modules/fields/survey-likert.ts +50 -0
- package/src/js/modules/fields/survey-rank.ts +385 -0
- package/src/js/modules/fields/survey-rating.ts +182 -0
- package/src/js/transport/forms-api.ts +1 -0
- package/src/js/utils/form-started-at.ts +25 -0
- package/src/js/utils/scripts.ts +16 -0
- package/src/js/utils/unload-warning.ts +1 -0
- package/src/js/validation/enter-key-guard.ts +63 -0
- package/src/js/validation/types.ts +1 -0
- package/src/js/validation/validator.ts +14 -5
- package/dist/chunks/recaptcha-v3-BtbbP111.js +0 -27
- package/dist/chunks/scripts-6CYSUGKQ.js +0 -24
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import surveyPresentationsCss from '#theme-css/fields/_survey-presentations.css?inline';
|
|
2
|
+
|
|
3
|
+
import type { FormieModuleDefinition } from '#contracts/modules';
|
|
4
|
+
import { dispatchFieldEvent } from '#modules/fields/shared';
|
|
5
|
+
import { ensureModuleStyles } from '#modules/styles';
|
|
6
|
+
import { createDebug } from '#utils/debug';
|
|
7
|
+
|
|
8
|
+
const FIELD_SELECTOR = '[data-formie-survey-rank]';
|
|
9
|
+
const LIST_SELECTOR = '[data-formie-survey-rank-list]';
|
|
10
|
+
const ITEM_SELECTOR = '[data-formie-survey-rank-item]';
|
|
11
|
+
const HANDLE_SELECTOR = '[data-formie-rank-handle]';
|
|
12
|
+
const PLACEHOLDER_CLASS = 'formie-rank-placeholder';
|
|
13
|
+
/** Insert before a sibling once the ghost centre crosses this fraction of its height (below 0.5 swaps earlier). */
|
|
14
|
+
const PLACEHOLDER_SWAP_RATIO = 0.42;
|
|
15
|
+
const MODULE_ID = 'survey-rank';
|
|
16
|
+
const debug = createDebug('fields', 'survey-rank');
|
|
17
|
+
|
|
18
|
+
ensureModuleStyles(MODULE_ID, [surveyPresentationsCss]);
|
|
19
|
+
|
|
20
|
+
type PointerOffset = {
|
|
21
|
+
x: number;
|
|
22
|
+
y: number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type GhostStyleSnapshot = {
|
|
26
|
+
position: string;
|
|
27
|
+
left: string;
|
|
28
|
+
top: string;
|
|
29
|
+
width: string;
|
|
30
|
+
zIndex: string;
|
|
31
|
+
pointerEvents: string;
|
|
32
|
+
margin: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function syncRankInputs(list: HTMLElement): void {
|
|
36
|
+
list.querySelectorAll(ITEM_SELECTOR).forEach((item, index) => {
|
|
37
|
+
if (!(item instanceof HTMLElement)) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
item.querySelectorAll('input[data-formie-rank-input]').forEach((input) => {
|
|
42
|
+
if (input instanceof HTMLInputElement) {
|
|
43
|
+
input.dataset.formieRankOrder = String(index);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getRankItems(list: HTMLElement): HTMLElement[] {
|
|
50
|
+
return Array.from(list.querySelectorAll(ITEM_SELECTOR)).filter((item): item is HTMLElement => {
|
|
51
|
+
return item instanceof HTMLElement;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getPlaceholderIndex(list: HTMLElement, placeholder: HTMLElement): number {
|
|
56
|
+
return Array.from(list.children).indexOf(placeholder);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getRankListSiblings(
|
|
60
|
+
list: HTMLElement,
|
|
61
|
+
placeholder: HTMLElement,
|
|
62
|
+
draggedItem: HTMLElement,
|
|
63
|
+
): HTMLElement[] {
|
|
64
|
+
return Array.from(list.children).filter((child): child is HTMLElement => {
|
|
65
|
+
return child instanceof HTMLElement
|
|
66
|
+
&& child !== placeholder
|
|
67
|
+
&& child !== draggedItem;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getVerticalOverlapHeight(ghostRect: DOMRect, targetRect: DOMRect): number {
|
|
72
|
+
return Math.max(0, Math.min(ghostRect.bottom, targetRect.bottom) - Math.max(ghostRect.top, targetRect.top));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function getGhostReferenceY(draggedItem: HTMLElement): number {
|
|
76
|
+
const rect = draggedItem.getBoundingClientRect();
|
|
77
|
+
|
|
78
|
+
return rect.top + (rect.height / 2);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getPlaceholderTargetIndex(
|
|
82
|
+
draggedItem: HTMLElement,
|
|
83
|
+
list: HTMLElement,
|
|
84
|
+
placeholder: HTMLElement,
|
|
85
|
+
draggingDown: boolean,
|
|
86
|
+
): number {
|
|
87
|
+
const ghostRect = draggedItem.getBoundingClientRect();
|
|
88
|
+
const referenceY = getGhostReferenceY(draggedItem);
|
|
89
|
+
const siblings = getRankListSiblings(list, placeholder, draggedItem);
|
|
90
|
+
|
|
91
|
+
for (let index = 0; index < siblings.length; index += 1) {
|
|
92
|
+
const rect = siblings[index].getBoundingClientRect();
|
|
93
|
+
const swapLine = rect.top + (rect.height * PLACEHOLDER_SWAP_RATIO);
|
|
94
|
+
const overlapRatio = getVerticalOverlapHeight(ghostRect, rect) / rect.height;
|
|
95
|
+
|
|
96
|
+
if (overlapRatio >= PLACEHOLDER_SWAP_RATIO) {
|
|
97
|
+
if (draggingDown) {
|
|
98
|
+
return Math.min(index + 1, siblings.length);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return index;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (referenceY < swapLine) {
|
|
105
|
+
return index;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return siblings.length;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function movePlaceholderToIndex(
|
|
113
|
+
list: HTMLElement,
|
|
114
|
+
placeholder: HTMLElement,
|
|
115
|
+
draggedItem: HTMLElement,
|
|
116
|
+
targetIndex: number,
|
|
117
|
+
): void {
|
|
118
|
+
const siblings = getRankListSiblings(list, placeholder, draggedItem);
|
|
119
|
+
const reference = siblings[targetIndex] ?? null;
|
|
120
|
+
|
|
121
|
+
if (reference) {
|
|
122
|
+
list.insertBefore(placeholder, reference);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
list.appendChild(placeholder);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function createPlaceholder(list: HTMLElement, item: HTMLElement): HTMLElement {
|
|
130
|
+
const placeholder = document.createElement('li');
|
|
131
|
+
|
|
132
|
+
placeholder.className = PLACEHOLDER_CLASS;
|
|
133
|
+
placeholder.setAttribute('data-formie-rank-placeholder', 'true');
|
|
134
|
+
placeholder.setAttribute('aria-hidden', 'true');
|
|
135
|
+
placeholder.style.height = `${item.offsetHeight}px`;
|
|
136
|
+
list.insertBefore(placeholder, item);
|
|
137
|
+
|
|
138
|
+
return placeholder;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function captureGhostStyles(item: HTMLElement): GhostStyleSnapshot {
|
|
142
|
+
return {
|
|
143
|
+
position: item.style.position,
|
|
144
|
+
left: item.style.left,
|
|
145
|
+
top: item.style.top,
|
|
146
|
+
width: item.style.width,
|
|
147
|
+
zIndex: item.style.zIndex,
|
|
148
|
+
pointerEvents: item.style.pointerEvents,
|
|
149
|
+
margin: item.style.margin,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function applyGhostStyles(item: HTMLElement, rect: DOMRect): void {
|
|
154
|
+
item.style.position = 'fixed';
|
|
155
|
+
item.style.left = `${rect.left}px`;
|
|
156
|
+
item.style.top = `${rect.top}px`;
|
|
157
|
+
item.style.width = `${rect.width}px`;
|
|
158
|
+
item.style.zIndex = '1000';
|
|
159
|
+
item.style.pointerEvents = 'none';
|
|
160
|
+
item.style.margin = '0';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function updateGhostPosition(item: HTMLElement, event: PointerEvent, offset: PointerOffset): void {
|
|
164
|
+
item.style.left = `${event.clientX - offset.x}px`;
|
|
165
|
+
item.style.top = `${event.clientY - offset.y}px`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function restoreGhostStyles(item: HTMLElement, snapshot: GhostStyleSnapshot): void {
|
|
169
|
+
item.style.position = snapshot.position;
|
|
170
|
+
item.style.left = snapshot.left;
|
|
171
|
+
item.style.top = snapshot.top;
|
|
172
|
+
item.style.width = snapshot.width;
|
|
173
|
+
item.style.zIndex = snapshot.zIndex;
|
|
174
|
+
item.style.pointerEvents = snapshot.pointerEvents;
|
|
175
|
+
item.style.margin = snapshot.margin;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function bindRankField(field: HTMLElement): () => void {
|
|
179
|
+
const list = field.querySelector(LIST_SELECTOR);
|
|
180
|
+
|
|
181
|
+
if (!(list instanceof HTMLElement)) {
|
|
182
|
+
debug.warn('Missing rank list; skipping field.');
|
|
183
|
+
return () => {};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let draggedItem: HTMLElement | null = null;
|
|
187
|
+
let placeholder: HTMLElement | null = null;
|
|
188
|
+
let ghostStyleSnapshot: GhostStyleSnapshot | null = null;
|
|
189
|
+
let pointerOffset: PointerOffset | null = null;
|
|
190
|
+
let activeCaptureTarget: HTMLElement | null = null;
|
|
191
|
+
let activePointerId: number | null = null;
|
|
192
|
+
let lastTargetIndex: number | null = null;
|
|
193
|
+
let startPlaceholderIndex: number | null = null;
|
|
194
|
+
let lastGhostReferenceY: number | null = null;
|
|
195
|
+
let didReorder = false;
|
|
196
|
+
const cleanups: Array<() => void> = [];
|
|
197
|
+
|
|
198
|
+
const finishDrag = () => {
|
|
199
|
+
if (draggedItem && placeholder) {
|
|
200
|
+
list.insertBefore(draggedItem, placeholder);
|
|
201
|
+
placeholder.remove();
|
|
202
|
+
} else if (placeholder) {
|
|
203
|
+
placeholder.remove();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (draggedItem && ghostStyleSnapshot) {
|
|
207
|
+
restoreGhostStyles(draggedItem, ghostStyleSnapshot);
|
|
208
|
+
draggedItem.removeAttribute('data-formie-rank-dragging');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (activeCaptureTarget && activePointerId !== null) {
|
|
212
|
+
try {
|
|
213
|
+
activeCaptureTarget.releasePointerCapture(activePointerId);
|
|
214
|
+
} catch {
|
|
215
|
+
// Pointer capture may already be released.
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
draggedItem = null;
|
|
220
|
+
placeholder = null;
|
|
221
|
+
ghostStyleSnapshot = null;
|
|
222
|
+
pointerOffset = null;
|
|
223
|
+
activeCaptureTarget = null;
|
|
224
|
+
activePointerId = null;
|
|
225
|
+
lastTargetIndex = null;
|
|
226
|
+
startPlaceholderIndex = null;
|
|
227
|
+
lastGhostReferenceY = null;
|
|
228
|
+
list.removeAttribute('data-formie-rank-sorting');
|
|
229
|
+
|
|
230
|
+
if (didReorder) {
|
|
231
|
+
syncRankInputs(list);
|
|
232
|
+
dispatchFieldEvent(field, MODULE_ID, 'reorder', {
|
|
233
|
+
rankField: field,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
didReorder = false;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const onPointerMove = (event: PointerEvent) => {
|
|
241
|
+
if (!draggedItem || !placeholder || !pointerOffset || event.pointerId !== activePointerId) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
event.preventDefault();
|
|
246
|
+
|
|
247
|
+
updateGhostPosition(draggedItem, event, pointerOffset);
|
|
248
|
+
|
|
249
|
+
const referenceY = getGhostReferenceY(draggedItem);
|
|
250
|
+
const draggingDown = lastGhostReferenceY === null || referenceY >= lastGhostReferenceY;
|
|
251
|
+
|
|
252
|
+
lastGhostReferenceY = referenceY;
|
|
253
|
+
|
|
254
|
+
const targetIndex = getPlaceholderTargetIndex(draggedItem, list, placeholder, draggingDown);
|
|
255
|
+
|
|
256
|
+
if (targetIndex === lastTargetIndex) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
lastTargetIndex = targetIndex;
|
|
261
|
+
movePlaceholderToIndex(list, placeholder, draggedItem, targetIndex);
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const onPointerUp = (event: PointerEvent) => {
|
|
265
|
+
if (event.pointerId !== activePointerId) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
document.removeEventListener('pointermove', onPointerMove);
|
|
270
|
+
document.removeEventListener('pointerup', onPointerUp);
|
|
271
|
+
document.removeEventListener('pointercancel', onPointerUp);
|
|
272
|
+
|
|
273
|
+
if (placeholder && startPlaceholderIndex !== null) {
|
|
274
|
+
didReorder = getPlaceholderIndex(list, placeholder) !== startPlaceholderIndex;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
finishDrag();
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
getRankItems(list).forEach((item) => {
|
|
281
|
+
if (!item.querySelector(HANDLE_SELECTOR)) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const onPointerDown = (event: PointerEvent) => {
|
|
286
|
+
if (event.button !== 0) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (event.target instanceof HTMLInputElement) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
event.preventDefault();
|
|
295
|
+
|
|
296
|
+
const rect = item.getBoundingClientRect();
|
|
297
|
+
|
|
298
|
+
draggedItem = item;
|
|
299
|
+
activeCaptureTarget = item;
|
|
300
|
+
activePointerId = event.pointerId;
|
|
301
|
+
pointerOffset = {
|
|
302
|
+
x: event.clientX - rect.left,
|
|
303
|
+
y: event.clientY - rect.top,
|
|
304
|
+
};
|
|
305
|
+
ghostStyleSnapshot = captureGhostStyles(item);
|
|
306
|
+
placeholder = createPlaceholder(list, item);
|
|
307
|
+
startPlaceholderIndex = getPlaceholderIndex(list, placeholder);
|
|
308
|
+
didReorder = false;
|
|
309
|
+
|
|
310
|
+
applyGhostStyles(item, rect);
|
|
311
|
+
item.setAttribute('data-formie-rank-dragging', 'true');
|
|
312
|
+
list.setAttribute('data-formie-rank-sorting', 'true');
|
|
313
|
+
item.setPointerCapture(event.pointerId);
|
|
314
|
+
lastGhostReferenceY = getGhostReferenceY(item);
|
|
315
|
+
lastTargetIndex = getPlaceholderTargetIndex(item, list, placeholder, true);
|
|
316
|
+
|
|
317
|
+
document.addEventListener('pointermove', onPointerMove);
|
|
318
|
+
document.addEventListener('pointerup', onPointerUp);
|
|
319
|
+
document.addEventListener('pointercancel', onPointerUp);
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
item.addEventListener('pointerdown', onPointerDown);
|
|
323
|
+
cleanups.push(() => {
|
|
324
|
+
item.removeEventListener('pointerdown', onPointerDown);
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
syncRankInputs(list);
|
|
329
|
+
|
|
330
|
+
return () => {
|
|
331
|
+
document.removeEventListener('pointermove', onPointerMove);
|
|
332
|
+
document.removeEventListener('pointerup', onPointerUp);
|
|
333
|
+
document.removeEventListener('pointercancel', onPointerUp);
|
|
334
|
+
cleanups.forEach((cleanup) => {
|
|
335
|
+
cleanup();
|
|
336
|
+
});
|
|
337
|
+
finishDrag();
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export const surveyRankModule: FormieModuleDefinition = {
|
|
342
|
+
id: MODULE_ID,
|
|
343
|
+
kind: 'field',
|
|
344
|
+
match: (ctx) => {
|
|
345
|
+
return ctx.target instanceof HTMLElement && (
|
|
346
|
+
ctx.target.matches(FIELD_SELECTOR) ||
|
|
347
|
+
!!ctx.target.querySelector(FIELD_SELECTOR)
|
|
348
|
+
);
|
|
349
|
+
},
|
|
350
|
+
setup: async(ctx) => {
|
|
351
|
+
if (!(ctx.target instanceof HTMLElement)) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const fields = ctx.target.matches(FIELD_SELECTOR)
|
|
356
|
+
? [ctx.target]
|
|
357
|
+
: Array.from(ctx.target.querySelectorAll(FIELD_SELECTOR)).filter((field): field is HTMLElement => {
|
|
358
|
+
return field instanceof HTMLElement;
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
debug.log('Module setup.', {
|
|
362
|
+
fieldCount: fields.length,
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const destroyBindings = fields.map((field) => {
|
|
366
|
+
return bindRankField(field);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
await ctx.emit('formie:module:survey-rank:init', {
|
|
370
|
+
count: fields.length,
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
destroy: () => {
|
|
375
|
+
destroyBindings.forEach((destroyBinding) => {
|
|
376
|
+
destroyBinding();
|
|
377
|
+
});
|
|
378
|
+
debug.log('Module destroy.', {
|
|
379
|
+
fieldCount: fields.length,
|
|
380
|
+
});
|
|
381
|
+
void ctx.emit('formie:module:survey-rank:destroy', {});
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
},
|
|
385
|
+
};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import surveyPresentationsCss from '#theme-css/fields/_survey-presentations.css?inline';
|
|
2
|
+
|
|
3
|
+
import type { FormieModuleDefinition } from '#contracts/modules';
|
|
4
|
+
import { dispatchFieldEvent } from '#modules/fields/shared';
|
|
5
|
+
import { ensureModuleStyles } from '#modules/styles';
|
|
6
|
+
import { createDebug } from '#utils/debug';
|
|
7
|
+
|
|
8
|
+
const FIELD_SELECTOR = '[data-formie-survey-rating]';
|
|
9
|
+
const STARS_SELECTOR = '[data-formie-survey-rating-stars]';
|
|
10
|
+
const OPTION_SELECTOR = '[data-formie-rating-option]';
|
|
11
|
+
const INPUT_SELECTOR = 'input[data-formie-rating-input]';
|
|
12
|
+
const PRESSED_WHILE_CHECKED = 'data-formie-rating-pressed-while-checked';
|
|
13
|
+
const MODULE_ID = 'survey-rating';
|
|
14
|
+
const debug = createDebug('fields', 'survey-rating');
|
|
15
|
+
|
|
16
|
+
ensureModuleStyles(MODULE_ID, [surveyPresentationsCss]);
|
|
17
|
+
|
|
18
|
+
function getRatingInputs(stars: HTMLElement): HTMLInputElement[] {
|
|
19
|
+
return Array.from(stars.querySelectorAll(INPUT_SELECTOR)).filter((input): input is HTMLInputElement => {
|
|
20
|
+
return input instanceof HTMLInputElement;
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function updateRatingValue(stars: HTMLElement): void {
|
|
25
|
+
const selectedIndex = getRatingInputs(stars).findIndex((input) => {
|
|
26
|
+
return input.checked;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (selectedIndex >= 0) {
|
|
30
|
+
stars.setAttribute('data-formie-rating-value', String(selectedIndex + 1));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
stars.removeAttribute('data-formie-rating-value');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function bindRatingField(field: HTMLElement): () => void {
|
|
38
|
+
const stars = field.querySelector(STARS_SELECTOR);
|
|
39
|
+
|
|
40
|
+
if (!(stars instanceof HTMLElement)) {
|
|
41
|
+
debug.warn('Missing rating stars container; skipping field.');
|
|
42
|
+
return () => {};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const cleanups: Array<() => void> = [];
|
|
46
|
+
|
|
47
|
+
const clearHover = () => {
|
|
48
|
+
stars.removeAttribute('data-formie-rating-hover');
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const setHoverFromOption = (option: HTMLElement) => {
|
|
52
|
+
const options = Array.from(stars.querySelectorAll(OPTION_SELECTOR));
|
|
53
|
+
const index = options.indexOf(option);
|
|
54
|
+
|
|
55
|
+
if (index >= 0) {
|
|
56
|
+
stars.setAttribute('data-formie-rating-hover', String(index + 1));
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
stars.querySelectorAll(OPTION_SELECTOR).forEach((option) => {
|
|
61
|
+
if (!(option instanceof HTMLElement)) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const input = option.querySelector(INPUT_SELECTOR);
|
|
66
|
+
|
|
67
|
+
const onPointerDown = () => {
|
|
68
|
+
if (!(input instanceof HTMLInputElement)) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (input.checked) {
|
|
73
|
+
input.setAttribute(PRESSED_WHILE_CHECKED, 'true');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
input.removeAttribute(PRESSED_WHILE_CHECKED);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const onMouseEnter = () => {
|
|
81
|
+
setHoverFromOption(option);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
option.addEventListener('pointerdown', onPointerDown);
|
|
85
|
+
option.addEventListener('mouseenter', onMouseEnter);
|
|
86
|
+
cleanups.push(() => {
|
|
87
|
+
option.removeEventListener('pointerdown', onPointerDown);
|
|
88
|
+
option.removeEventListener('mouseenter', onMouseEnter);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
stars.addEventListener('mouseleave', clearHover);
|
|
93
|
+
cleanups.push(() => {
|
|
94
|
+
stars.removeEventListener('mouseleave', clearHover);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
getRatingInputs(stars).forEach((input) => {
|
|
98
|
+
const onClick = (event: MouseEvent) => {
|
|
99
|
+
if (input.getAttribute(PRESSED_WHILE_CHECKED) !== 'true') {
|
|
100
|
+
updateRatingValue(stars);
|
|
101
|
+
dispatchFieldEvent(field, MODULE_ID, 'change', {
|
|
102
|
+
ratingField: field,
|
|
103
|
+
value: input.checked ? input.value : '',
|
|
104
|
+
});
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
event.preventDefault();
|
|
109
|
+
getRatingInputs(stars).forEach((candidate) => {
|
|
110
|
+
candidate.checked = false;
|
|
111
|
+
candidate.removeAttribute(PRESSED_WHILE_CHECKED);
|
|
112
|
+
});
|
|
113
|
+
updateRatingValue(stars);
|
|
114
|
+
dispatchFieldEvent(field, MODULE_ID, 'change', {
|
|
115
|
+
ratingField: field,
|
|
116
|
+
value: '',
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
input.addEventListener('click', onClick);
|
|
121
|
+
cleanups.push(() => {
|
|
122
|
+
input.removeEventListener('click', onClick);
|
|
123
|
+
input.removeAttribute(PRESSED_WHILE_CHECKED);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
updateRatingValue(stars);
|
|
128
|
+
|
|
129
|
+
return () => {
|
|
130
|
+
cleanups.forEach((cleanup) => {
|
|
131
|
+
cleanup();
|
|
132
|
+
});
|
|
133
|
+
clearHover();
|
|
134
|
+
stars.removeAttribute('data-formie-rating-value');
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const surveyRatingModule: FormieModuleDefinition = {
|
|
139
|
+
id: MODULE_ID,
|
|
140
|
+
kind: 'field',
|
|
141
|
+
match: (ctx) => {
|
|
142
|
+
return ctx.target instanceof HTMLElement && (
|
|
143
|
+
ctx.target.matches(FIELD_SELECTOR) ||
|
|
144
|
+
!!ctx.target.querySelector(FIELD_SELECTOR)
|
|
145
|
+
);
|
|
146
|
+
},
|
|
147
|
+
setup: async(ctx) => {
|
|
148
|
+
if (!(ctx.target instanceof HTMLElement)) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const fields = ctx.target.matches(FIELD_SELECTOR)
|
|
153
|
+
? [ctx.target]
|
|
154
|
+
: Array.from(ctx.target.querySelectorAll(FIELD_SELECTOR)).filter((field): field is HTMLElement => {
|
|
155
|
+
return field instanceof HTMLElement;
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
debug.log('Module setup.', {
|
|
159
|
+
fieldCount: fields.length,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const destroyBindings = fields.map((field) => {
|
|
163
|
+
return bindRatingField(field);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
await ctx.emit('formie:module:survey-rating:init', {
|
|
167
|
+
count: fields.length,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
destroy: () => {
|
|
172
|
+
destroyBindings.forEach((destroyBinding) => {
|
|
173
|
+
destroyBinding();
|
|
174
|
+
});
|
|
175
|
+
debug.log('Module destroy.', {
|
|
176
|
+
fieldCount: fields.length,
|
|
177
|
+
});
|
|
178
|
+
void ctx.emit('formie:module:survey-rating:destroy', {});
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
},
|
|
182
|
+
};
|
|
@@ -107,6 +107,7 @@ function normalizePayload(payload: Record<string, unknown>, fallbackFormError?:
|
|
|
107
107
|
}
|
|
108
108
|
: null,
|
|
109
109
|
submitData: Array.isArray(payload.submitData) ? payload.submitData : undefined,
|
|
110
|
+
clientEvents: Array.isArray(payload.clientEvents) ? payload.clientEvents as FormSubmitResult['clientEvents'] : undefined,
|
|
110
111
|
meta: payload,
|
|
111
112
|
};
|
|
112
113
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const FORM_STARTED_AT_STORAGE_PREFIX = 'formie:formStartedAt:';
|
|
2
|
+
|
|
3
|
+
export function ensureFormStartedAt(form: HTMLFormElement): void {
|
|
4
|
+
const startedAtInput = form.querySelector('input[name="formStartedAt"]') as HTMLInputElement | null;
|
|
5
|
+
|
|
6
|
+
if (!startedAtInput) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const renderIdInput = form.querySelector('input[name="renderId"]') as HTMLInputElement | null;
|
|
11
|
+
const renderId = renderIdInput?.value?.trim() ?? '';
|
|
12
|
+
const storageKey = renderId ? `${FORM_STARTED_AT_STORAGE_PREFIX}${renderId}` : null;
|
|
13
|
+
|
|
14
|
+
let startedAt = storageKey ? sessionStorage.getItem(storageKey) : null;
|
|
15
|
+
|
|
16
|
+
if (!startedAt) {
|
|
17
|
+
startedAt = String(Date.now());
|
|
18
|
+
|
|
19
|
+
if (storageKey) {
|
|
20
|
+
sessionStorage.setItem(storageKey, startedAt);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
startedAtInput.value = startedAt;
|
|
25
|
+
}
|
package/src/js/utils/scripts.ts
CHANGED
|
@@ -11,6 +11,16 @@ type LoadExternalScriptOptions = {
|
|
|
11
11
|
|
|
12
12
|
const scriptLoadCache = new Map<string, Promise<HTMLScriptElement>>();
|
|
13
13
|
|
|
14
|
+
function getDocumentCspNonce(): string | null {
|
|
15
|
+
const nonceMeta = document.querySelector('meta[property="csp-nonce"], meta[name="csp-nonce"]') as HTMLMetaElement | null;
|
|
16
|
+
|
|
17
|
+
if (!nonceMeta) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return nonceMeta.nonce || nonceMeta.getAttribute('nonce') || nonceMeta.getAttribute('content') || null;
|
|
22
|
+
}
|
|
23
|
+
|
|
14
24
|
export async function ensureGlobal<T = unknown>(globalName: string, timeoutMs = 5000): Promise<T> {
|
|
15
25
|
// Third-party SDKs often attach themselves to `window` after their script tag
|
|
16
26
|
// loads, so callers wait on the global rather than assuming load == ready.
|
|
@@ -49,6 +59,12 @@ export async function loadExternalScript({
|
|
|
49
59
|
script.src = src;
|
|
50
60
|
script.async = async;
|
|
51
61
|
script.defer = defer;
|
|
62
|
+
const nonce = getDocumentCspNonce();
|
|
63
|
+
|
|
64
|
+
if (nonce) {
|
|
65
|
+
script.setAttribute('nonce', nonce);
|
|
66
|
+
}
|
|
67
|
+
|
|
52
68
|
script.onload = () => {
|
|
53
69
|
resolve(script);
|
|
54
70
|
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { FormAction } from '#contracts/common';
|
|
2
|
+
import { getValidationScope } from '#validation/scope';
|
|
3
|
+
|
|
4
|
+
function isConditionallyInactive(element: Element): boolean {
|
|
5
|
+
return element.hasAttribute('data-formie-conditionally-hidden')
|
|
6
|
+
|| !!element.closest('[data-formie-conditionally-hidden]')
|
|
7
|
+
|| element.hasAttribute('data-formie-page-hidden')
|
|
8
|
+
|| !!element.closest('[data-formie-page-hidden]');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function isActionControlVisible(form: HTMLFormElement, action: FormAction): boolean {
|
|
12
|
+
const controls = form.querySelectorAll(`[data-formie-action="${action}"]`);
|
|
13
|
+
|
|
14
|
+
return Array.from(controls).some((control) => {
|
|
15
|
+
return !isConditionallyInactive(control);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function resolveEnterKeyAction(form: HTMLFormElement): FormAction {
|
|
20
|
+
const { final } = getValidationScope(form);
|
|
21
|
+
|
|
22
|
+
return final ? 'submit' : 'submit';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function shouldBlockEnterSubmit(form: HTMLFormElement): boolean {
|
|
26
|
+
const action = resolveEnterKeyAction(form);
|
|
27
|
+
|
|
28
|
+
return !isActionControlVisible(form, action);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function bindEnterKeyGuard(form: HTMLFormElement): () => void {
|
|
32
|
+
const handler = (event: KeyboardEvent) => {
|
|
33
|
+
if (event.key !== 'Enter' || event.defaultPrevented) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const target = event.target;
|
|
38
|
+
|
|
39
|
+
if (!(target instanceof HTMLInputElement || target instanceof HTMLSelectElement)) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (target instanceof HTMLInputElement) {
|
|
44
|
+
if (target.type === 'button' || target.type === 'submit' || target.type === 'reset' || target.type === 'file') {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!shouldBlockEnterSubmit(form)) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Browsers can still implicit-submit through hidden/disabled submit
|
|
54
|
+
// controls; block that path when conditions hide the primary action.
|
|
55
|
+
event.preventDefault();
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
form.addEventListener('keydown', handler, true);
|
|
59
|
+
|
|
60
|
+
return () => {
|
|
61
|
+
form.removeEventListener('keydown', handler, true);
|
|
62
|
+
};
|
|
63
|
+
}
|