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.
@@ -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 draftKey =
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
- const saved = draftStorage.get(draftKey, null);
497
- if (saved && typeof saved === 'object' && Object.keys(saved).length > 0) {
498
- fill(saved, { baseline: false });
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: saved, key: draftKey },
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
- formEl.setAttribute('action', String(url));
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
- if (draftDebounceTimer) clearTimeout(draftDebounceTimer);
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
- setTimeout(() => {
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();