@verbb/formie-browser 1.0.7 → 1.0.9
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 +24 -0
- package/dist/chunks/_survey-presentations-RbSqcQph.js +4 -0
- package/dist/chunks/{address-state-BQ0xGV9n.js → address-state-BwE01m2_.js} +1 -1
- package/dist/chunks/{api-DztsCJ4r.js → api-BYe9YPko.js} +194 -153
- package/dist/chunks/{bpoint-2qDxlKD9.js → bpoint-C0VOftX9.js} +1 -1
- package/dist/chunks/{combobox-D-2b42wn.js → combobox-C0EuExSI.js} +1 -1
- package/dist/chunks/{eway-CImXrB4g.js → eway-CS6Y1Yvo.js} +1 -1
- package/dist/chunks/{file-upload-D49m8-DR.js → file-upload-rqfZNqlH.js} +1 -1
- package/dist/chunks/{go-cardless-Bz7HHtH0.js → go-cardless-BTPD668l.js} +1 -1
- package/dist/chunks/{mollie-BtnB3KGh.js → mollie-CXgBwvo6.js} +1 -1
- package/dist/chunks/{moneris-Bd4vUk8b.js → moneris-B6oLu7nj.js} +1 -1
- package/dist/chunks/{opayo-B-eRIXo-.js → opayo-DPUA5CKr.js} +1 -1
- package/dist/chunks/{paddle-BUFeEU4d.js → paddle-GRlsm5mm.js} +1 -1
- package/dist/chunks/{paypal-8Euw7Hrn.js → paypal-CdcZnf6r.js} +2 -2
- package/dist/chunks/{payway-dV0SzJxy.js → payway-CfmYOS__.js} +2 -2
- package/dist/chunks/{repeater-B5leVxZU.js → repeater-BVnPUqTj.js} +1 -1
- package/dist/chunks/rich-text-Bg_--4TG.js +349 -0
- package/dist/chunks/{square-reyJukb7.js → square-BY--5M-X.js} +1 -1
- package/dist/chunks/{stripe-BC-2PBvY.js → stripe-uusYjWty.js} +2 -2
- package/dist/chunks/{summary-D3AjxpYN.js → summary-Dks0gXS3.js} +42 -38
- package/dist/chunks/survey-likert-DkE6bR7B.js +20 -0
- package/dist/chunks/survey-rank-DN6x4IZM.js +127 -0
- package/dist/chunks/survey-rating-BnsNL9U5.js +88 -0
- package/dist/chunks/{table-C-lkQN6I.js → table-CAjCJHrL.js} +1 -1
- package/dist/chunks/{upload-manager-BpqWSHmU.js → upload-manager-W-QDgxKD.js} +1 -1
- package/dist/css/formie-theme.css +1 -0
- package/dist/css/theme/_tokens.css +10 -5
- package/dist/css/theme/fields/_check-radio.css +1 -1
- package/dist/css/theme/fields/_combobox.css +1 -1
- package/dist/css/theme/fields/_file.css +3 -3
- package/dist/css/theme/fields/_input.css +1 -1
- package/dist/css/theme/fields/_repeater.css +1 -1
- package/dist/css/theme/fields/_rich-text.css +1 -1
- package/dist/css/theme/fields/_select.css +1 -1
- package/dist/css/theme/fields/_survey-presentations.css +396 -0
- package/dist/css/theme/fields/_table.css +1 -1
- package/dist/css/theme/fields/_upload-manager.css +3 -3
- package/dist/css/theme/integrations/_paypal.css +1 -1
- package/dist/css/theme/integrations/_payway.css +1 -1
- package/dist/css/theme/integrations/_stripe.css +1 -1
- package/dist/index.js +398 -376
- 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/page-client-event.d.ts +8 -4
- package/dist/js/core/page-client-event.d.ts.map +1 -1
- package/dist/js/core/submit-flow.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 +1 -0
- package/dist/js/core/submit-result-ui.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/fields.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/css/formie-theme.css +1 -0
- package/src/css/theme/_tokens.css +10 -5
- package/src/css/theme/fields/_check-radio.css +1 -1
- package/src/css/theme/fields/_combobox.css +1 -1
- package/src/css/theme/fields/_file.css +3 -3
- package/src/css/theme/fields/_input.css +1 -1
- package/src/css/theme/fields/_repeater.css +1 -1
- package/src/css/theme/fields/_rich-text.css +1 -1
- package/src/css/theme/fields/_select.css +1 -1
- package/src/css/theme/fields/_survey-presentations.css +396 -0
- package/src/css/theme/fields/_table.css +1 -1
- package/src/css/theme/fields/_upload-manager.css +3 -3
- package/src/css/theme/integrations/_paypal.css +1 -1
- package/src/css/theme/integrations/_payway.css +1 -1
- package/src/css/theme/integrations/_stripe.css +1 -1
- package/src/js/contracts/schema.ts +6 -0
- package/src/js/core/create-formie-client.ts +14 -1
- package/src/js/core/page-client-event.ts +86 -18
- package/src/js/core/submit-flow.ts +3 -1
- package/src/js/core/submit-result-state.ts +18 -2
- package/src/js/core/submit-result-ui.ts +47 -9
- 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/fields.ts +14 -2
- package/dist/chunks/rich-text-D0sb8bmX.js +0 -349
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { FormAction } from '#contracts/common';
|
|
2
2
|
import type { FormSubmitResult } from '#contracts/schema';
|
|
3
3
|
import { dispatchFormieDomEvent } from '#core/dom-events';
|
|
4
|
-
import { dispatchPageClientEventForSubmit } from '#core/page-client-event';
|
|
4
|
+
import { dispatchPageClientEventForSubmit, dispatchResolvedClientEvents } from '#core/page-client-event';
|
|
5
5
|
import { syncPageTabErrors } from '#core/page-tab-errors';
|
|
6
6
|
import { addThemeClasses, removeThemeClasses, toggleThemeClasses } from '#theme/theme-classes';
|
|
7
7
|
import { createDebug } from '#utils/debug';
|
|
@@ -335,6 +335,18 @@ export function applyPageState(form: HTMLFormElement, nextPageId: string): void
|
|
|
335
335
|
toggleThemeClasses(tab, form, 'tabCurrent', isCurrent);
|
|
336
336
|
toggleThemeClasses(tab, form, 'tabComplete', isComplete);
|
|
337
337
|
|
|
338
|
+
const tabLink = tab.querySelector('[data-formie-tab-link]');
|
|
339
|
+
|
|
340
|
+
if (tabLink) {
|
|
341
|
+
toggleThemeClasses(tabLink, form, 'tabLinkCurrent', isCurrent);
|
|
342
|
+
|
|
343
|
+
if (isCurrent) {
|
|
344
|
+
removeThemeClasses(tabLink, form, 'tabLinkInactive');
|
|
345
|
+
} else {
|
|
346
|
+
addThemeClasses(tabLink, form, 'tabLinkInactive');
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
338
350
|
if (isCurrent) {
|
|
339
351
|
tab.setAttribute('aria-current', 'page');
|
|
340
352
|
} else {
|
|
@@ -568,7 +580,11 @@ export function applySubmitResultState(form: HTMLFormElement, result: FormSubmit
|
|
|
568
580
|
return;
|
|
569
581
|
}
|
|
570
582
|
|
|
571
|
-
|
|
583
|
+
if (Array.isArray(result.clientEvents) && result.clientEvents.length > 0) {
|
|
584
|
+
dispatchResolvedClientEvents(form, result.clientEvents);
|
|
585
|
+
} else {
|
|
586
|
+
dispatchPageClientEventForSubmit(form, action);
|
|
587
|
+
}
|
|
572
588
|
|
|
573
589
|
if (result.nextPage?.id) {
|
|
574
590
|
// Advancing pages is treated as a fresh validation cycle for the next page.
|
|
@@ -367,6 +367,31 @@ export function renderFormErrors(form: HTMLFormElement, formErrors: string[]): v
|
|
|
367
367
|
});
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
+
function isPaymentFollowUpResult(result: FormSubmitResult): boolean {
|
|
371
|
+
if (result.ok || result.keepSubmitLoading !== true) {
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const meta = (result.meta || {}) as Record<string, unknown>;
|
|
376
|
+
const paymentStatus = String(meta.paymentStatus || '');
|
|
377
|
+
|
|
378
|
+
return paymentStatus === 'actionRequired' || paymentStatus === 'pending';
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function renderFormNotice(form: HTMLFormElement, message: string): void {
|
|
382
|
+
const container = ensureFormErrorContainer(form);
|
|
383
|
+
const messageContainer = ensureFormErrorMessageContainer(form, container);
|
|
384
|
+
|
|
385
|
+
addThemeClasses(container, form, 'errors');
|
|
386
|
+
|
|
387
|
+
const noticeNode = document.createElement('div');
|
|
388
|
+
noticeNode.setAttribute('data-formie-notice', 'true');
|
|
389
|
+
noticeNode.setAttribute('role', 'status');
|
|
390
|
+
addThemeClasses(noticeNode, form, 'message');
|
|
391
|
+
noticeNode.textContent = message;
|
|
392
|
+
messageContainer.appendChild(noticeNode);
|
|
393
|
+
}
|
|
394
|
+
|
|
370
395
|
function shouldRenderSuccessMessage(form: HTMLFormElement, result: FormSubmitResult): boolean {
|
|
371
396
|
if (!result.message || result.nextPage || result.redirect) {
|
|
372
397
|
// Page changes and redirects are transitional outcomes, not terminal
|
|
@@ -432,15 +457,28 @@ export function applySubmitResultUi(form: HTMLFormElement, result: FormSubmitRes
|
|
|
432
457
|
return;
|
|
433
458
|
}
|
|
434
459
|
|
|
435
|
-
if (result.
|
|
436
|
-
|
|
437
|
-
|
|
460
|
+
if (!result.ok) {
|
|
461
|
+
if (isPaymentFollowUpResult(result)) {
|
|
462
|
+
const meta = (result.meta || {}) as Record<string, unknown>;
|
|
463
|
+
const message = String(meta.paymentMessage || '').trim();
|
|
438
464
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
465
|
+
if (message) {
|
|
466
|
+
renderFormNotice(form, message);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (result.fieldErrors) {
|
|
473
|
+
renderFieldErrors(form, result.fieldErrors);
|
|
474
|
+
}
|
|
444
475
|
|
|
445
|
-
|
|
476
|
+
if (result.formErrors?.length) {
|
|
477
|
+
renderFormErrors(form, result.formErrors);
|
|
478
|
+
} else if (!result.fieldErrors && result.message) {
|
|
479
|
+
renderFormErrors(form, [result.message]);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
focusFirstValidationError(form);
|
|
483
|
+
}
|
|
446
484
|
}
|
|
@@ -21,6 +21,9 @@ export const builtinFieldModuleLoaders: Record<string, () => Promise<FormieModul
|
|
|
21
21
|
'rich-text': () => import('#modules/fields/rich-text').then((module) => module.richTextModule),
|
|
22
22
|
'signature': () => import('#modules/fields/signature').then((module) => module.signatureModule),
|
|
23
23
|
'summary': () => import('#modules/fields/summary').then((module) => module.summaryModule),
|
|
24
|
+
'survey-likert': () => import('#modules/fields/survey-likert').then((module) => module.surveyLikertModule),
|
|
25
|
+
'survey-rank': () => import('#modules/fields/survey-rank').then((module) => module.surveyRankModule),
|
|
26
|
+
'survey-rating': () => import('#modules/fields/survey-rating').then((module) => module.surveyRatingModule),
|
|
24
27
|
'table': () => import('#modules/fields/table').then((module) => module.tableModule),
|
|
25
28
|
'text-limit': () => import('#modules/fields/text-limit').then((module) => module.textLimitModule),
|
|
26
29
|
};
|
|
@@ -11,6 +11,8 @@ import { requestText } from '#utils/http';
|
|
|
11
11
|
const BLOCKS_SELECTOR = '[data-formie-summary-blocks]';
|
|
12
12
|
const CONTAINER_SELECTOR = '[data-formie-summary-container]';
|
|
13
13
|
const SUMMARY_ACTION = 'formie/fields/get-summary-html';
|
|
14
|
+
const THEME_CONFIG_ATTR = 'data-formie-theme-config';
|
|
15
|
+
const FRONTEND_THEME_ATTR = 'data-formie-frontend-theme';
|
|
14
16
|
const MODULE_ID = 'summary';
|
|
15
17
|
const debug = createDebug('fields', 'summary');
|
|
16
18
|
|
|
@@ -18,6 +20,8 @@ ensureModuleStyles(MODULE_ID, [summaryCss]);
|
|
|
18
20
|
|
|
19
21
|
type SummaryRequestState = {
|
|
20
22
|
accessToken: string | null;
|
|
23
|
+
themeConfig: string | null;
|
|
24
|
+
frontendTheme: string | null;
|
|
21
25
|
};
|
|
22
26
|
|
|
23
27
|
function getSummaryRequestUrl(): string {
|
|
@@ -27,12 +31,14 @@ function getSummaryRequestUrl(): string {
|
|
|
27
31
|
return url.toString();
|
|
28
32
|
}
|
|
29
33
|
|
|
30
|
-
function getSummaryRequestState(field: HTMLElement): SummaryRequestState {
|
|
34
|
+
function getSummaryRequestState(field: HTMLElement, form: HTMLFormElement): SummaryRequestState {
|
|
31
35
|
const summaryTokenInput = field.querySelector('[data-formie-summary-token]') as HTMLInputElement | null;
|
|
32
36
|
const accessToken = summaryTokenInput?.value?.trim() || null;
|
|
33
37
|
|
|
34
38
|
return {
|
|
35
39
|
accessToken,
|
|
40
|
+
themeConfig: form.getAttribute(THEME_CONFIG_ATTR)?.trim() || null,
|
|
41
|
+
frontendTheme: form.getAttribute(FRONTEND_THEME_ATTR)?.trim() || null,
|
|
36
42
|
};
|
|
37
43
|
}
|
|
38
44
|
|
|
@@ -47,6 +53,14 @@ async function requestSummaryHtml(form: HTMLFormElement, state: SummaryRequestSt
|
|
|
47
53
|
formData.set('action', SUMMARY_ACTION);
|
|
48
54
|
formData.set('accessToken', state.accessToken);
|
|
49
55
|
|
|
56
|
+
if (state.themeConfig) {
|
|
57
|
+
formData.set('themeConfig', state.themeConfig);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (state.frontendTheme) {
|
|
61
|
+
formData.set('frontendTheme', state.frontendTheme);
|
|
62
|
+
}
|
|
63
|
+
|
|
50
64
|
return requestText(getSummaryRequestUrl(), {
|
|
51
65
|
method: 'POST',
|
|
52
66
|
body: formData,
|
|
@@ -101,7 +115,7 @@ function initSummaryField(field: HTMLElement, root: Element): () => void {
|
|
|
101
115
|
toggleThemeClasses(blocks, form, 'loading', false);
|
|
102
116
|
};
|
|
103
117
|
|
|
104
|
-
const initialState = getSummaryRequestState(field);
|
|
118
|
+
const initialState = getSummaryRequestState(field, form);
|
|
105
119
|
setLoadingState(!!initialState.accessToken);
|
|
106
120
|
|
|
107
121
|
const queueFetch = (): void => {
|
|
@@ -114,7 +128,7 @@ function initSummaryField(field: HTMLElement, root: Element): () => void {
|
|
|
114
128
|
};
|
|
115
129
|
|
|
116
130
|
const fetchSummary = debounce(async() => {
|
|
117
|
-
const state = getSummaryRequestState(field);
|
|
131
|
+
const state = getSummaryRequestState(field, form);
|
|
118
132
|
|
|
119
133
|
if (!getBlocks() || !state.accessToken) {
|
|
120
134
|
debug.warn('Missing state for fetch.', state);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import surveyPresentationsCss from '#theme-css/fields/_survey-presentations.css?inline';
|
|
2
|
+
|
|
3
|
+
import type { FormieModuleDefinition } from '#contracts/modules';
|
|
4
|
+
import { ensureModuleStyles } from '#modules/styles';
|
|
5
|
+
import { createDebug } from '#utils/debug';
|
|
6
|
+
|
|
7
|
+
const FIELD_SELECTOR = '[data-formie-likert-field-layout]';
|
|
8
|
+
const MODULE_ID = 'survey-likert';
|
|
9
|
+
const debug = createDebug('fields', 'survey-likert');
|
|
10
|
+
|
|
11
|
+
ensureModuleStyles(MODULE_ID, [surveyPresentationsCss]);
|
|
12
|
+
|
|
13
|
+
export const surveyLikertModule: FormieModuleDefinition = {
|
|
14
|
+
id: MODULE_ID,
|
|
15
|
+
kind: 'field',
|
|
16
|
+
match: (ctx) => {
|
|
17
|
+
return ctx.target instanceof HTMLElement && (
|
|
18
|
+
ctx.target.matches(FIELD_SELECTOR) ||
|
|
19
|
+
!!ctx.target.querySelector(FIELD_SELECTOR)
|
|
20
|
+
);
|
|
21
|
+
},
|
|
22
|
+
setup: async(ctx) => {
|
|
23
|
+
if (!(ctx.target instanceof HTMLElement)) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const fields = ctx.target.matches(FIELD_SELECTOR)
|
|
28
|
+
? [ctx.target]
|
|
29
|
+
: Array.from(ctx.target.querySelectorAll(FIELD_SELECTOR)).filter((field): field is HTMLElement => {
|
|
30
|
+
return field instanceof HTMLElement;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
debug.log('Module setup.', {
|
|
34
|
+
fieldCount: fields.length,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
await ctx.emit('formie:module:survey-likert:init', {
|
|
38
|
+
count: fields.length,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
destroy: () => {
|
|
43
|
+
debug.log('Module destroy.', {
|
|
44
|
+
fieldCount: fields.length,
|
|
45
|
+
});
|
|
46
|
+
void ctx.emit('formie:module:survey-likert:destroy', {});
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
};
|
|
@@ -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
|
+
};
|