webspresso 0.0.111 → 0.0.112
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/core/client/adapter/datastar.js +119 -30
- package/core/client/controller.js +120 -11
- package/core/client/forms.js +124 -10
- package/core/client/fragments.js +255 -95
- package/core/client/host.js +5 -1
- package/core/client/lite.js +1 -1
- package/core/client/server-fragments.js +43 -1
- package/index.d.ts +12 -1
- package/package.json +1 -1
package/core/client/forms.js
CHANGED
|
@@ -432,16 +432,29 @@ function createFormHandle(formEl, options = {}) {
|
|
|
432
432
|
'form';
|
|
433
433
|
|
|
434
434
|
let initialSnapshot = captureInitialFormSnapshot(formEl);
|
|
435
|
+
let baselineValues = parseFormElement(formEl).data;
|
|
435
436
|
let pending = false;
|
|
436
437
|
let dirty = false;
|
|
438
|
+
let inputRevision = 0;
|
|
437
439
|
let fieldErrors = {};
|
|
438
440
|
let formError = null;
|
|
441
|
+
let lastError = null;
|
|
439
442
|
let lastData = null;
|
|
443
|
+
let draftDebounceTimer = null;
|
|
444
|
+
let resetTimer = null;
|
|
440
445
|
|
|
446
|
+
const beforeSubmitListeners = new Set();
|
|
441
447
|
const successListeners = new Set();
|
|
442
448
|
const errorListeners = new Set();
|
|
443
449
|
const stateListeners = new Set();
|
|
444
450
|
|
|
451
|
+
function cancelDraftDebounce() {
|
|
452
|
+
if (draftDebounceTimer) {
|
|
453
|
+
clearTimeout(draftDebounceTimer);
|
|
454
|
+
draftDebounceTimer = null;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
445
458
|
// Form draft persistence setup (sessionStorage by default, or localStorage)
|
|
446
459
|
const rawDraftAttr = formEl
|
|
447
460
|
? formEl.getAttribute('data-w-persist-draft') || formEl.getAttribute('data-w-draft')
|
|
@@ -458,11 +471,18 @@ function createFormHandle(formEl, options = {}) {
|
|
|
458
471
|
draftStorageType = 'local';
|
|
459
472
|
}
|
|
460
473
|
|
|
461
|
-
const
|
|
474
|
+
const draftScope =
|
|
475
|
+
options.draftScope ||
|
|
476
|
+
(formEl && formEl.getAttribute && formEl.getAttribute('data-w-draft-scope')) ||
|
|
477
|
+
'';
|
|
478
|
+
|
|
479
|
+
const baseDraftKey =
|
|
462
480
|
options.draftKey ||
|
|
463
481
|
(formEl && (formEl.getAttribute('data-w-draft-key') || formEl.getAttribute('data-w-persist-key'))) ||
|
|
464
482
|
`form_draft:${formName || 'anon'}`;
|
|
465
483
|
|
|
484
|
+
let draftKey = draftScope ? `${baseDraftKey}:${draftScope}` : baseDraftKey;
|
|
485
|
+
|
|
466
486
|
const draftStorage =
|
|
467
487
|
(options.storage && options.storage[draftStorageType]) ||
|
|
468
488
|
(draftStorageType === 'local' ? defaultStorage.local : defaultStorage.session);
|
|
@@ -485,7 +505,22 @@ function createFormHandle(formEl, options = {}) {
|
|
|
485
505
|
return safeData;
|
|
486
506
|
}
|
|
487
507
|
|
|
508
|
+
function sanitizeDraftObject(raw) {
|
|
509
|
+
if (!raw || typeof raw !== 'object' || isArray(raw)) return null;
|
|
510
|
+
const clean = {};
|
|
511
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
512
|
+
if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue;
|
|
513
|
+
if (v === null || v === undefined || typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') {
|
|
514
|
+
clean[k] = v;
|
|
515
|
+
} else if (isArray(v) && v.every((item) => typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean')) {
|
|
516
|
+
clean[k] = v;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return Object.keys(clean).length > 0 ? clean : null;
|
|
520
|
+
}
|
|
521
|
+
|
|
488
522
|
function saveDraft() {
|
|
523
|
+
cancelDraftDebounce();
|
|
489
524
|
if (!formEl || !isDraftEnabled) return false;
|
|
490
525
|
const draftPayload = extractDraftValues(formEl);
|
|
491
526
|
return draftStorage.set(draftKey, draftPayload);
|
|
@@ -493,9 +528,15 @@ function createFormHandle(formEl, options = {}) {
|
|
|
493
528
|
|
|
494
529
|
function restoreDraft() {
|
|
495
530
|
if (!formEl || !isDraftEnabled) return false;
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
531
|
+
let saved = null;
|
|
532
|
+
try {
|
|
533
|
+
saved = draftStorage.get(draftKey, null);
|
|
534
|
+
} catch (_e) {
|
|
535
|
+
saved = null;
|
|
536
|
+
}
|
|
537
|
+
const cleanDraft = sanitizeDraftObject(saved);
|
|
538
|
+
if (cleanDraft) {
|
|
539
|
+
fill(cleanDraft, { baseline: false });
|
|
499
540
|
if (typeof formEl.dispatchEvent === 'function') {
|
|
500
541
|
const CustomEventCtor =
|
|
501
542
|
(formEl.ownerDocument && formEl.ownerDocument.defaultView && formEl.ownerDocument.defaultView.CustomEvent) ||
|
|
@@ -505,7 +546,7 @@ function createFormHandle(formEl, options = {}) {
|
|
|
505
546
|
new CustomEventCtor('w-form:draft-restored', {
|
|
506
547
|
bubbles: true,
|
|
507
548
|
cancelable: true,
|
|
508
|
-
detail: { form: handle, draft:
|
|
549
|
+
detail: { form: handle, draft: cleanDraft, key: draftKey },
|
|
509
550
|
})
|
|
510
551
|
);
|
|
511
552
|
}
|
|
@@ -516,6 +557,7 @@ function createFormHandle(formEl, options = {}) {
|
|
|
516
557
|
}
|
|
517
558
|
|
|
518
559
|
function clearDraft() {
|
|
560
|
+
cancelDraftDebounce();
|
|
519
561
|
if (!isDraftEnabled) return false;
|
|
520
562
|
return draftStorage.remove(draftKey);
|
|
521
563
|
}
|
|
@@ -525,6 +567,20 @@ function createFormHandle(formEl, options = {}) {
|
|
|
525
567
|
return draftStorage.has(draftKey);
|
|
526
568
|
}
|
|
527
569
|
|
|
570
|
+
function setDraftKey(nextKey, keyOpts = {}) {
|
|
571
|
+
cancelDraftDebounce();
|
|
572
|
+
if (typeof nextKey === 'string' && nextKey.trim()) {
|
|
573
|
+
draftKey = nextKey.trim();
|
|
574
|
+
if (formEl && typeof formEl.setAttribute === 'function') {
|
|
575
|
+
formEl.setAttribute('data-w-draft-key', draftKey);
|
|
576
|
+
}
|
|
577
|
+
if (keyOpts && keyOpts.restore === true && isDraftEnabled) {
|
|
578
|
+
restoreDraft();
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return handle;
|
|
582
|
+
}
|
|
583
|
+
|
|
528
584
|
function notifyStateChange() {
|
|
529
585
|
for (const fn of stateListeners) {
|
|
530
586
|
try {
|
|
@@ -654,6 +710,7 @@ function createFormHandle(formEl, options = {}) {
|
|
|
654
710
|
* Reset form controls back to their initial default values (NOT emptying non-empty defaults).
|
|
655
711
|
*/
|
|
656
712
|
function reset() {
|
|
713
|
+
cancelDraftDebounce();
|
|
657
714
|
if (!formEl) return;
|
|
658
715
|
if (typeof formEl.reset === 'function') {
|
|
659
716
|
formEl.reset();
|
|
@@ -683,6 +740,7 @@ function createFormHandle(formEl, options = {}) {
|
|
|
683
740
|
* Completely clear all user-editable controls to empty values (distinct from `reset()`).
|
|
684
741
|
*/
|
|
685
742
|
function clear() {
|
|
743
|
+
cancelDraftDebounce();
|
|
686
744
|
if (!formEl || typeof formEl.querySelectorAll !== 'function') return;
|
|
687
745
|
const controls = formEl.querySelectorAll('input, select, textarea');
|
|
688
746
|
for (let i = 0; i < controls.length; i++) {
|
|
@@ -719,11 +777,14 @@ function createFormHandle(formEl, options = {}) {
|
|
|
719
777
|
* snapshot, clears `dirty`, and clears previous validation errors.
|
|
720
778
|
*/
|
|
721
779
|
function fill(values = {}, options = {}) {
|
|
722
|
-
if (!formEl || typeof formEl.querySelectorAll !== 'function' || !values || typeof values !== 'object') {
|
|
780
|
+
if (!formEl || typeof formEl.querySelectorAll !== 'function' || !values || typeof values !== 'object' || isArray(values)) {
|
|
723
781
|
return handle;
|
|
724
782
|
}
|
|
725
783
|
|
|
726
784
|
const isBaseline = Boolean(options.baseline);
|
|
785
|
+
if (isBaseline) {
|
|
786
|
+
cancelDraftDebounce();
|
|
787
|
+
}
|
|
727
788
|
|
|
728
789
|
for (const [key, val] of Object.entries(values)) {
|
|
729
790
|
if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;
|
|
@@ -757,6 +818,7 @@ function createFormHandle(formEl, options = {}) {
|
|
|
757
818
|
ctrl.value = val === null || val === undefined ? '' : String(val);
|
|
758
819
|
}
|
|
759
820
|
} else {
|
|
821
|
+
if (val !== null && typeof val === 'object') continue;
|
|
760
822
|
ctrl.value = val === null || val === undefined ? '' : String(val);
|
|
761
823
|
}
|
|
762
824
|
|
|
@@ -768,9 +830,11 @@ function createFormHandle(formEl, options = {}) {
|
|
|
768
830
|
|
|
769
831
|
if (isBaseline) {
|
|
770
832
|
initialSnapshot = captureInitialFormSnapshot(formEl);
|
|
833
|
+
baselineValues = parseFormElement(formEl).data;
|
|
771
834
|
dirty = false;
|
|
772
835
|
fieldErrors = {};
|
|
773
836
|
formError = null;
|
|
837
|
+
lastError = null;
|
|
774
838
|
syncErrorsToDom(false);
|
|
775
839
|
} else {
|
|
776
840
|
dirty = computeFormDirty(formEl, initialSnapshot);
|
|
@@ -783,7 +847,9 @@ function createFormHandle(formEl, options = {}) {
|
|
|
783
847
|
|
|
784
848
|
function setAction(url) {
|
|
785
849
|
if (formEl && url) {
|
|
786
|
-
|
|
850
|
+
const nextAction = String(url);
|
|
851
|
+
formEl.setAttribute('action', nextAction);
|
|
852
|
+
formEl.__wCustomAction = nextAction;
|
|
787
853
|
}
|
|
788
854
|
return handle;
|
|
789
855
|
}
|
|
@@ -796,6 +862,7 @@ function createFormHandle(formEl, options = {}) {
|
|
|
796
862
|
action = action.replace(new RegExp(`:${k}\\b`, 'g'), encodeURIComponent(String(v)));
|
|
797
863
|
}
|
|
798
864
|
formEl.setAttribute('action', action);
|
|
865
|
+
formEl.__wCustomAction = action;
|
|
799
866
|
return handle;
|
|
800
867
|
}
|
|
801
868
|
|
|
@@ -870,9 +937,23 @@ function createFormHandle(formEl, options = {}) {
|
|
|
870
937
|
|
|
871
938
|
const { data: payloadData, formData, isMultipart } = parseFormElement(formEl, submitter);
|
|
872
939
|
|
|
940
|
+
// Execute beforeSubmit hooks before initiating network request
|
|
941
|
+
for (const guardFn of beforeSubmitListeners) {
|
|
942
|
+
let allowed = true;
|
|
943
|
+
try {
|
|
944
|
+
allowed = await guardFn(payloadData, { form: handle, submitter, action: actionUrl, method });
|
|
945
|
+
} catch (guardErr) {
|
|
946
|
+
return { ok: false, submitted: false, canceled: true, reason: 'before-submit-error', error: guardErr };
|
|
947
|
+
}
|
|
948
|
+
if (allowed === false) {
|
|
949
|
+
return { ok: false, submitted: false, canceled: true, reason: 'before-submit' };
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
873
953
|
// Clear previous errors before request starts
|
|
874
954
|
fieldErrors = {};
|
|
875
955
|
formError = null;
|
|
956
|
+
lastError = null;
|
|
876
957
|
syncErrorsToDom(false);
|
|
877
958
|
setPendingState(true, submitter);
|
|
878
959
|
|
|
@@ -921,6 +1002,7 @@ function createFormHandle(formEl, options = {}) {
|
|
|
921
1002
|
} else {
|
|
922
1003
|
// Current state becomes the new clean baseline after successful save
|
|
923
1004
|
initialSnapshot = captureInitialFormSnapshot(formEl);
|
|
1005
|
+
baselineValues = parseFormElement(formEl).data;
|
|
924
1006
|
}
|
|
925
1007
|
|
|
926
1008
|
if (isDraftEnabled) {
|
|
@@ -967,6 +1049,7 @@ function createFormHandle(formEl, options = {}) {
|
|
|
967
1049
|
|
|
968
1050
|
return { ok: true, submitted: true, data: responseData, refreshOk, refreshError, hookError };
|
|
969
1051
|
} catch (err) {
|
|
1052
|
+
lastError = err || null;
|
|
970
1053
|
if (err && err.kind === 'validation') {
|
|
971
1054
|
fieldErrors = { ...(err.fieldErrors || {}) };
|
|
972
1055
|
formError = err.formError || (Object.keys(fieldErrors).length === 0 ? err.message : null);
|
|
@@ -996,8 +1079,8 @@ function createFormHandle(formEl, options = {}) {
|
|
|
996
1079
|
}
|
|
997
1080
|
|
|
998
1081
|
// Listen to native submit and input/change events on `formEl`
|
|
999
|
-
let draftDebounceTimer = null;
|
|
1000
1082
|
const onDomInput = (evt) => {
|
|
1083
|
+
inputRevision += 1;
|
|
1001
1084
|
dirty = computeFormDirty(formEl, initialSnapshot);
|
|
1002
1085
|
const target = evt && evt.target;
|
|
1003
1086
|
if (target && target.getAttribute) {
|
|
@@ -1012,8 +1095,9 @@ function createFormHandle(formEl, options = {}) {
|
|
|
1012
1095
|
}
|
|
1013
1096
|
}
|
|
1014
1097
|
if (isDraftEnabled) {
|
|
1015
|
-
|
|
1098
|
+
cancelDraftDebounce();
|
|
1016
1099
|
draftDebounceTimer = setTimeout(() => {
|
|
1100
|
+
draftDebounceTimer = null;
|
|
1017
1101
|
saveDraft();
|
|
1018
1102
|
}, 150);
|
|
1019
1103
|
}
|
|
@@ -1025,7 +1109,10 @@ function createFormHandle(formEl, options = {}) {
|
|
|
1025
1109
|
};
|
|
1026
1110
|
|
|
1027
1111
|
const onDomReset = () => {
|
|
1028
|
-
|
|
1112
|
+
cancelDraftDebounce();
|
|
1113
|
+
if (resetTimer) clearTimeout(resetTimer);
|
|
1114
|
+
resetTimer = setTimeout(() => {
|
|
1115
|
+
resetTimer = null;
|
|
1029
1116
|
dirty = false;
|
|
1030
1117
|
fieldErrors = {};
|
|
1031
1118
|
formError = null;
|
|
@@ -1062,9 +1149,18 @@ function createFormHandle(formEl, options = {}) {
|
|
|
1062
1149
|
dirty = computeFormDirty(formEl, initialSnapshot);
|
|
1063
1150
|
return dirty;
|
|
1064
1151
|
},
|
|
1152
|
+
get inputRevision() {
|
|
1153
|
+
return inputRevision;
|
|
1154
|
+
},
|
|
1155
|
+
get draftKey() {
|
|
1156
|
+
return draftKey;
|
|
1157
|
+
},
|
|
1065
1158
|
get values() {
|
|
1066
1159
|
return parseFormElement(formEl).data;
|
|
1067
1160
|
},
|
|
1161
|
+
get baseline() {
|
|
1162
|
+
return { ...(baselineValues || {}) };
|
|
1163
|
+
},
|
|
1068
1164
|
get errors() {
|
|
1069
1165
|
return { ...fieldErrors };
|
|
1070
1166
|
},
|
|
@@ -1074,6 +1170,9 @@ function createFormHandle(formEl, options = {}) {
|
|
|
1074
1170
|
get formError() {
|
|
1075
1171
|
return formError;
|
|
1076
1172
|
},
|
|
1173
|
+
get lastError() {
|
|
1174
|
+
return lastError;
|
|
1175
|
+
},
|
|
1077
1176
|
get lastData() {
|
|
1078
1177
|
return lastData;
|
|
1079
1178
|
},
|
|
@@ -1093,6 +1192,7 @@ function createFormHandle(formEl, options = {}) {
|
|
|
1093
1192
|
saveDraft,
|
|
1094
1193
|
restoreDraft,
|
|
1095
1194
|
clearDraft,
|
|
1195
|
+
setDraftKey,
|
|
1096
1196
|
get hasDraft() {
|
|
1097
1197
|
return hasDraft();
|
|
1098
1198
|
},
|
|
@@ -1112,12 +1212,20 @@ function createFormHandle(formEl, options = {}) {
|
|
|
1112
1212
|
clearErrors() {
|
|
1113
1213
|
fieldErrors = {};
|
|
1114
1214
|
formError = null;
|
|
1215
|
+
lastError = null;
|
|
1115
1216
|
syncErrorsToDom(false);
|
|
1116
1217
|
notifyStateChange();
|
|
1117
1218
|
},
|
|
1118
1219
|
reset,
|
|
1119
1220
|
clear,
|
|
1120
1221
|
submit,
|
|
1222
|
+
beforeSubmit(fn) {
|
|
1223
|
+
if (typeof fn === 'function') {
|
|
1224
|
+
beforeSubmitListeners.add(fn);
|
|
1225
|
+
return () => beforeSubmitListeners.delete(fn);
|
|
1226
|
+
}
|
|
1227
|
+
return () => {};
|
|
1228
|
+
},
|
|
1121
1229
|
onSuccess(fn) {
|
|
1122
1230
|
if (typeof fn === 'function') {
|
|
1123
1231
|
successListeners.add(fn);
|
|
@@ -1140,6 +1248,11 @@ function createFormHandle(formEl, options = {}) {
|
|
|
1140
1248
|
return () => {};
|
|
1141
1249
|
},
|
|
1142
1250
|
destroy() {
|
|
1251
|
+
cancelDraftDebounce();
|
|
1252
|
+
if (resetTimer) {
|
|
1253
|
+
clearTimeout(resetTimer);
|
|
1254
|
+
resetTimer = null;
|
|
1255
|
+
}
|
|
1143
1256
|
if (formEl && typeof formEl.removeEventListener === 'function') {
|
|
1144
1257
|
formEl.removeEventListener('input', onDomInput);
|
|
1145
1258
|
formEl.removeEventListener('change', onDomInput);
|
|
@@ -1149,6 +1262,7 @@ function createFormHandle(formEl, options = {}) {
|
|
|
1149
1262
|
if (formEl) {
|
|
1150
1263
|
delete formEl.__wFormHandle;
|
|
1151
1264
|
}
|
|
1265
|
+
beforeSubmitListeners.clear();
|
|
1152
1266
|
successListeners.clear();
|
|
1153
1267
|
errorListeners.clear();
|
|
1154
1268
|
stateListeners.clear();
|