@samline/forms 2.8.0 → 2.10.0

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.
@@ -1,4 +1,4 @@
1
- import { u as FormsApi, v as FormsAvailable, F as FormTarget, a as FormControllerOptions, b as FormController, N as NewFormInput } from '../registry-GzopYPCR.cjs';
1
+ import { v as FormsApi, w as FormsAvailable, F as FormTarget, a as FormControllerOptions, b as FormController, N as NewFormInput } from '../registry-CPe_lf8-.cjs';
2
2
 
3
3
  declare const Forms: FormsApi;
4
4
  declare const form: (target: FormTarget, options?: FormControllerOptions) => FormController;
@@ -1,4 +1,4 @@
1
- import { u as FormsApi, v as FormsAvailable, F as FormTarget, a as FormControllerOptions, b as FormController, N as NewFormInput } from '../registry-GzopYPCR.js';
1
+ import { v as FormsApi, w as FormsAvailable, F as FormTarget, a as FormControllerOptions, b as FormController, N as NewFormInput } from '../registry-CPe_lf8-.js';
2
2
 
3
3
  declare const Forms: FormsApi;
4
4
  declare const form: (target: FormTarget, options?: FormControllerOptions) => FormController;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  browser
3
- } from "../chunk-23VZAYA3.js";
3
+ } from "../chunk-JAEFD56I.js";
4
4
 
5
5
  // src/browser/global.ts
6
6
  var Forms = browser;
@@ -372,12 +372,37 @@ var restoreCursor = (field, position) => {
372
372
  var state = null;
373
373
  var MISSING_MESSAGE = "[samline/forms] The `format()` and `formatAll()` methods require the @samline/formatter package. Install it with: npm i @samline/formatter (or pnpm/bun/yarn equivalent).";
374
374
  var warned = false;
375
+ var phonePromise = null;
376
+ var PHONE_MESSAGE = "[samline/forms] Phone formatting needs `@samline/formatter/phone` (@samline/formatter >= 3.0.0 makes phones opt-in) and it could not be loaded.";
375
377
  var warnOnce = () => {
376
378
  if (warned) return;
377
379
  warned = true;
378
380
  console.error(MISSING_MESSAGE);
379
381
  };
380
- var loadFormatter = () => {
382
+ var ensurePhone = (formatter) => {
383
+ if (typeof formatter.isPhoneRegistered !== "function") return Promise.resolve(true);
384
+ if (formatter.isPhoneRegistered()) return Promise.resolve(true);
385
+ if (!phonePromise) {
386
+ phonePromise = (async () => {
387
+ try {
388
+ const mod = await import("@samline/formatter/phone");
389
+ mod.registerPhone();
390
+ return true;
391
+ } catch {
392
+ console.error(PHONE_MESSAGE);
393
+ phonePromise = null;
394
+ return false;
395
+ }
396
+ })();
397
+ }
398
+ return phonePromise;
399
+ };
400
+ var loadFormatter = async (options = {}) => {
401
+ const formatter = await loadFormatterModule();
402
+ if (!formatter || !options.phone) return formatter;
403
+ return await ensurePhone(formatter) ? formatter : null;
404
+ };
405
+ var loadFormatterModule = () => {
381
406
  if (state && state.status === "resolved") {
382
407
  if (!state.module) warnOnce();
383
408
  return Promise.resolve(state.module);
@@ -512,7 +537,7 @@ var applyFormat = async (state2, helpers, config) => {
512
537
  phase1Entries.push(entry);
513
538
  }
514
539
  }
515
- const formatter = await loadFormatter();
540
+ const formatter = await loadFormatter({ phone: formatType === "phone" });
516
541
  if (state2.isDestroyed) {
517
542
  rollbackPhase1(phase1Entries, bucket, state2.formattedFields);
518
543
  return;
@@ -651,6 +676,7 @@ var createDestroy = (state2, _helpers) => () => {
651
676
  }
652
677
  }
653
678
  state2.watchedFields.clear();
679
+ state2.groupWatchers.clear();
654
680
  state2.subscribers.clear();
655
681
  state2.submitHandlers.clear();
656
682
  state2.fieldCache.clear();
@@ -847,9 +873,9 @@ var writeFieldValue = (fields, value) => {
847
873
  continue;
848
874
  }
849
875
  if (field instanceof HTMLSelectElement && field.multiple) {
850
- const selectedValues = normalizedArray ?? [normalizedValue];
876
+ const selectedValues2 = normalizedArray ?? [normalizedValue];
851
877
  for (const option of field.options) {
852
- option.selected = selectedValues.includes(option.value);
878
+ option.selected = selectedValues2.includes(option.value);
853
879
  }
854
880
  continue;
855
881
  }
@@ -884,8 +910,7 @@ var submitForm = (form2) => {
884
910
  var createGetValue = (state2, helpers) => (name) => readFieldValue(helpers.getFieldsByName(name));
885
911
 
886
912
  // src/api/observe.ts
887
- var createObserve = (state2, helpers) => (field, callback) => {
888
- if (state2.isDestroyed) return () => void 0;
913
+ var observeField = (state2, field, callback) => {
889
914
  const callbacks = state2.watchedFields.get(field) ?? /* @__PURE__ */ new Set();
890
915
  callbacks.add(callback);
891
916
  state2.watchedFields.set(field, callbacks);
@@ -900,6 +925,26 @@ var createObserve = (state2, helpers) => (field, callback) => {
900
925
  }
901
926
  };
902
927
  };
928
+ var observeFields = (state2, fields, callback) => {
929
+ const names = Array.from(new Set(fields));
930
+ if (names.length === 0) return () => void 0;
931
+ const entry = { fields: names, callback };
932
+ state2.groupWatchers.add(entry);
933
+ if (state2.element) {
934
+ const values = Object.fromEntries(names.map((name) => [name, state2.api.getValue(name)]));
935
+ callback(values, null, state2.element, state2.api.getState());
936
+ }
937
+ return () => {
938
+ state2.groupWatchers.delete(entry);
939
+ };
940
+ };
941
+ var createObserve = (state2, _helpers) => {
942
+ function observe(field, callback) {
943
+ if (state2.isDestroyed) return () => void 0;
944
+ return Array.isArray(field) ? observeFields(state2, field, callback) : observeField(state2, field, callback);
945
+ }
946
+ return observe;
947
+ };
903
948
 
904
949
  // src/api/on-submit.ts
905
950
  var createOnSubmit = (state2, _helpers) => (callback, preventDefault = true) => {
@@ -971,6 +1016,75 @@ var createSetErrors = (state2, helpers) => (fields) => {
971
1016
  return state2.api;
972
1017
  };
973
1018
 
1019
+ // src/api/set-options.ts
1020
+ var isGroup = (entry) => typeof entry === "object" && entry !== null && Array.isArray(entry.options);
1021
+ var createOption = (entry) => {
1022
+ const option = document.createElement("option");
1023
+ if (typeof entry === "object") {
1024
+ option.value = String(entry.value);
1025
+ option.textContent = entry.label ?? String(entry.value);
1026
+ option.disabled = Boolean(entry.disabled);
1027
+ if (entry.selected) option.defaultSelected = true;
1028
+ } else {
1029
+ option.value = String(entry);
1030
+ option.textContent = String(entry);
1031
+ }
1032
+ return option;
1033
+ };
1034
+ var buildNodes = (options) => {
1035
+ const nodes = [];
1036
+ let hasExplicitSelection = false;
1037
+ const track = (option) => {
1038
+ if (option.defaultSelected) hasExplicitSelection = true;
1039
+ return option;
1040
+ };
1041
+ for (const entry of options) {
1042
+ if (entry === null || entry === void 0) continue;
1043
+ if (isGroup(entry)) {
1044
+ const group = document.createElement("optgroup");
1045
+ group.label = entry.label;
1046
+ group.disabled = Boolean(entry.disabled);
1047
+ for (const child of entry.options) {
1048
+ if (child === null || child === void 0) continue;
1049
+ group.appendChild(track(createOption(child)));
1050
+ }
1051
+ nodes.push(group);
1052
+ continue;
1053
+ }
1054
+ nodes.push(track(createOption(entry)));
1055
+ }
1056
+ return { nodes, hasExplicitSelection };
1057
+ };
1058
+ var selectedValues = (select) => Array.from(select.selectedOptions, (option) => option.value);
1059
+ var restoreSelection = (select, previous) => {
1060
+ if (previous.length === 0) return;
1061
+ const options = Array.from(select.options);
1062
+ if (select.multiple) {
1063
+ for (const option of options) option.selected = previous.includes(option.value);
1064
+ return;
1065
+ }
1066
+ const match = options.find((option) => option.value === previous[0]);
1067
+ if (match) match.selected = true;
1068
+ };
1069
+ var sameValues = (left, right) => left.length === right.length && left.every((value, index) => value === right[index]);
1070
+ var createSetOptions = (state2, helpers) => (name, options, config = {}) => {
1071
+ if (!state2.element || state2.isDestroyed) return state2.api;
1072
+ const selects = helpers.getFieldsByName(name).filter((field) => field instanceof HTMLSelectElement);
1073
+ if (selects.length === 0) return state2.api;
1074
+ const keepValue = config.keepValue ?? true;
1075
+ const changed = [];
1076
+ for (const select of selects) {
1077
+ const previous = selectedValues(select);
1078
+ const { nodes, hasExplicitSelection } = buildNodes(options);
1079
+ select.replaceChildren(...nodes);
1080
+ if (keepValue && !hasExplicitSelection) restoreSelection(select, previous);
1081
+ if (!sameValues(previous, selectedValues(select))) changed.push(select);
1082
+ }
1083
+ helpers.commitOwnMutations(selects);
1084
+ changed[0]?.dispatchEvent(new Event("input", { bubbles: true }));
1085
+ return state2.api;
1086
+ };
1087
+
974
1088
  // src/api/set-value.ts
975
1089
  var createSetValue = (state2, helpers) => (name, value) => {
976
1090
  if (state2.isDestroyed) return state2.api;
@@ -994,28 +1108,46 @@ var createSubscribe = (state2, _helpers) => (listener) => {
994
1108
  };
995
1109
 
996
1110
  // src/api/unwatch.ts
997
- var createUnwatch = (state2, _helpers) => (field, callback) => {
998
- if (!field) {
999
- state2.watchedFields.clear();
1000
- return state2.api;
1001
- }
1002
- if (!callback) {
1003
- state2.watchedFields.delete(field);
1111
+ var sameFields = (left, right) => left.length === right.size && left.every((name) => right.has(name));
1112
+ var createUnwatch = (state2, _helpers) => {
1113
+ function unwatch(field, callback) {
1114
+ if (field === void 0 || field === "") {
1115
+ state2.watchedFields.clear();
1116
+ state2.groupWatchers.clear();
1117
+ return state2.api;
1118
+ }
1119
+ if (Array.isArray(field)) {
1120
+ const names = new Set(field);
1121
+ for (const entry of Array.from(state2.groupWatchers)) {
1122
+ if (!sameFields(entry.fields, names)) continue;
1123
+ if (callback && entry.callback !== callback) continue;
1124
+ state2.groupWatchers.delete(entry);
1125
+ }
1126
+ return state2.api;
1127
+ }
1128
+ if (!callback) {
1129
+ state2.watchedFields.delete(field);
1130
+ return state2.api;
1131
+ }
1132
+ const callbacks = state2.watchedFields.get(field);
1133
+ callbacks?.delete(callback);
1134
+ if (callbacks && callbacks.size === 0) {
1135
+ state2.watchedFields.delete(field);
1136
+ }
1004
1137
  return state2.api;
1005
1138
  }
1006
- const callbacks = state2.watchedFields.get(field);
1007
- callbacks?.delete(callback);
1008
- if (callbacks && callbacks.size === 0) {
1009
- state2.watchedFields.delete(field);
1010
- }
1011
- return state2.api;
1139
+ return unwatch;
1012
1140
  };
1013
1141
 
1014
1142
  // src/api/watch.ts
1015
- var createWatch = (state2, helpers) => (field, callback) => {
1143
+ var createWatch = (state2, helpers) => {
1016
1144
  const observe = createObserve(state2, helpers);
1017
- observe(field, callback);
1018
- return state2.api;
1145
+ function watch(field, callback) {
1146
+ if (Array.isArray(field)) observe(field, callback);
1147
+ else observe(field, callback);
1148
+ return state2.api;
1149
+ }
1150
+ return watch;
1019
1151
  };
1020
1152
 
1021
1153
  // src/core/controller.ts
@@ -1042,6 +1174,7 @@ var createFormController = (target, options = {}) => {
1042
1174
  attributes,
1043
1175
  validators: normalizedOptions.validators ?? {},
1044
1176
  watchedFields: /* @__PURE__ */ new Map(),
1177
+ groupWatchers: /* @__PURE__ */ new Set(),
1045
1178
  subscribers: /* @__PURE__ */ new Set(),
1046
1179
  submitHandlers: /* @__PURE__ */ new Set(),
1047
1180
  fieldCache: /* @__PURE__ */ new Map(),
@@ -1212,7 +1345,7 @@ var createFormController = (target, options = {}) => {
1212
1345
  }
1213
1346
  return affected;
1214
1347
  };
1215
- const emitFieldWatchers = (name) => {
1348
+ const emitSingleWatchers = (name) => {
1216
1349
  if (!state2.element || !state2.api) return;
1217
1350
  const callbacks = state2.watchedFields.get(name);
1218
1351
  if (!callbacks || callbacks.size === 0) return;
@@ -1221,6 +1354,23 @@ var createFormController = (target, options = {}) => {
1221
1354
  const snapshot = state2.api.getState();
1222
1355
  callbacks.forEach((callback) => callback(value, fieldElement, state2.element, snapshot));
1223
1356
  };
1357
+ const emitGroupWatchers = (names) => {
1358
+ if (!state2.element || !state2.api || state2.groupWatchers.size === 0) return;
1359
+ let snapshot;
1360
+ for (const entry of Array.from(state2.groupWatchers)) {
1361
+ const changed = names ? names.find((name) => entry.fields.includes(name)) : null;
1362
+ if (changed === void 0) continue;
1363
+ snapshot ?? (snapshot = state2.api.getState());
1364
+ const values = Object.fromEntries(
1365
+ entry.fields.map((field) => [field, state2.api.getValue(field)])
1366
+ );
1367
+ entry.callback(values, changed, state2.element, snapshot);
1368
+ }
1369
+ };
1370
+ const emitFieldWatchers = (name) => {
1371
+ emitSingleWatchers(name);
1372
+ emitGroupWatchers([name]);
1373
+ };
1224
1374
  const runAutoSubmit = () => {
1225
1375
  state2.autoSubmitTimer = null;
1226
1376
  if (!state2.element || state2.isDestroyed || !state2.autoSubmitEnabled) return;
@@ -1241,8 +1391,20 @@ var createFormController = (target, options = {}) => {
1241
1391
  }
1242
1392
  runAutoSubmit();
1243
1393
  };
1394
+ const selectSnapshots = /* @__PURE__ */ new WeakMap();
1395
+ const readSelectSnapshot = (select) => Array.from(select.selectedOptions, (option) => option.value).join("\0");
1396
+ const snapshotSelect = (select) => {
1397
+ selectSnapshots.set(select, readSelectSnapshot(select));
1398
+ };
1399
+ const snapshotAllSelects = () => {
1400
+ if (!state2.element) return;
1401
+ for (const field of getNamedFields(state2.element)) {
1402
+ if (field instanceof HTMLSelectElement) snapshotSelect(field);
1403
+ }
1404
+ };
1244
1405
  const emitAllFieldWatchers = () => {
1245
- for (const name of Array.from(state2.watchedFields.keys())) emitFieldWatchers(name);
1406
+ for (const name of Array.from(state2.watchedFields.keys())) emitSingleWatchers(name);
1407
+ emitGroupWatchers(null);
1246
1408
  };
1247
1409
  const applyReset = () => {
1248
1410
  if (!state2.element || state2.isDestroyed) return;
@@ -1254,6 +1416,7 @@ var createFormController = (target, options = {}) => {
1254
1416
  state2.validationElementErrors.clear();
1255
1417
  clearAttributes(state2.element, [attributes.error, attributes.filled, "aria-invalid"]);
1256
1418
  syncVisualState();
1419
+ snapshotAllSelects();
1257
1420
  emitAllFieldWatchers();
1258
1421
  notifySubscribers();
1259
1422
  };
@@ -1268,7 +1431,8 @@ var createFormController = (target, options = {}) => {
1268
1431
  validateNames,
1269
1432
  emitFieldWatchers,
1270
1433
  scheduleAutoSubmit,
1271
- applyReset
1434
+ applyReset,
1435
+ commitOwnMutations: (selects) => commitOwnMutations(selects)
1272
1436
  };
1273
1437
  const handleDelegatedEvent = (event) => {
1274
1438
  const target2 = event.target;
@@ -1276,6 +1440,7 @@ var createFormController = (target, options = {}) => {
1276
1440
  const name = target2.name;
1277
1441
  if (!name) return;
1278
1442
  clearFieldCache();
1443
+ if (target2 instanceof HTMLSelectElement) snapshotSelect(target2);
1279
1444
  const canonical = resolveCanonicalForName(state2, name) ?? name;
1280
1445
  const displayName = resolveDisplayNameForName(state2, name);
1281
1446
  if (normalizedOptions.clearManualErrorsOnChange) {
@@ -1286,8 +1451,9 @@ var createFormController = (target, options = {}) => {
1286
1451
  if (state2.isValidated && affectedValidationNames.some((field) => state2.validators[field] !== void 0)) {
1287
1452
  validateNames(affectedValidationNames);
1288
1453
  }
1289
- emitFieldWatchers(canonical);
1290
- if (canonical !== name) emitFieldWatchers(name);
1454
+ emitSingleWatchers(canonical);
1455
+ if (canonical !== name) emitSingleWatchers(name);
1456
+ emitGroupWatchers(canonical !== name ? [canonical, name] : [canonical]);
1291
1457
  notifySubscribers();
1292
1458
  scheduleAutoSubmit();
1293
1459
  };
@@ -1316,6 +1482,10 @@ var createFormController = (target, options = {}) => {
1316
1482
  };
1317
1483
  const handleSubmitEvent = (event) => {
1318
1484
  if (!state2.element || state2.isDestroyed || !state2.api) return;
1485
+ if (!state2.isAutoSubmitting && state2.autoSubmitTimer) {
1486
+ clearTimeout(state2.autoSubmitTimer);
1487
+ state2.autoSubmitTimer = null;
1488
+ }
1319
1489
  if (normalizedOptions.clearErrorsOnSubmit) state2.manualErrors = {};
1320
1490
  const validation = state2.api.validate();
1321
1491
  const handlers = Array.from(state2.submitHandlers);
@@ -1368,18 +1538,62 @@ var createFormController = (target, options = {}) => {
1368
1538
  const cancel = () => clearTimeout(timer);
1369
1539
  state2.cleanups.add(cancel);
1370
1540
  };
1541
+ const touchesFields = (node) => node instanceof Element && (isFieldElement(node) || node.querySelector("input, select, textarea") !== null);
1542
+ const isStructural = (record) => record.type === "attributes" || Array.from(record.addedNodes).some(touchesFields) || Array.from(record.removedNodes).some(touchesFields);
1543
+ const isOptionNode = (node) => node instanceof HTMLOptionElement || node instanceof HTMLOptGroupElement;
1544
+ const optionListOwner = (record) => {
1545
+ if (record.type !== "childList") return null;
1546
+ const touchesOptions = Array.from(record.addedNodes).some(isOptionNode) || Array.from(record.removedNodes).some(isOptionNode);
1547
+ if (!touchesOptions) return null;
1548
+ const target2 = record.target;
1549
+ if (target2 instanceof HTMLSelectElement) return target2;
1550
+ if (target2 instanceof HTMLOptGroupElement && target2.parentElement instanceof HTMLSelectElement) {
1551
+ return target2.parentElement;
1552
+ }
1553
+ return null;
1554
+ };
1555
+ const handleMutations = (records) => {
1556
+ if (state2.isDestroyed) return;
1557
+ const structural = records.some(isStructural);
1558
+ const changedNames = /* @__PURE__ */ new Set();
1559
+ for (const record of records) {
1560
+ const select = optionListOwner(record);
1561
+ if (!select || !select.name) continue;
1562
+ const next = readSelectSnapshot(select);
1563
+ if (selectSnapshots.get(select) !== next) changedNames.add(select.name);
1564
+ selectSnapshots.set(select, next);
1565
+ }
1566
+ if (!structural && changedNames.size === 0) return;
1567
+ clearFieldCache();
1568
+ if (structural) {
1569
+ snapshotAllSelects();
1570
+ if (state2.isValidated) validateNames();
1571
+ else syncVisualState();
1572
+ } else {
1573
+ const affected = /* @__PURE__ */ new Set();
1574
+ for (const name of changedNames) {
1575
+ for (const field of getAffectedValidationNames(name)) affected.add(field);
1576
+ }
1577
+ const names = Array.from(affected);
1578
+ syncVisualState(Array.from(changedNames));
1579
+ if (state2.isValidated && names.some((field) => state2.validators[field] !== void 0)) {
1580
+ validateNames(names);
1581
+ }
1582
+ }
1583
+ for (const name of changedNames) emitFieldWatchers(name);
1584
+ notifySubscribers();
1585
+ };
1586
+ const commitOwnMutations = (selects) => {
1587
+ selects.forEach(snapshotSelect);
1588
+ const pending = state2.mutationObserver?.takeRecords() ?? [];
1589
+ const foreign = pending.filter(
1590
+ (record) => !selects.some((select) => select.contains(record.target))
1591
+ );
1592
+ if (foreign.length > 0) handleMutations(foreign);
1593
+ };
1371
1594
  const startMutationObserver = () => {
1372
1595
  if (!state2.element || typeof MutationObserver === "undefined") return;
1373
- const touchesFields = (node) => node instanceof Element && (isFieldElement(node) || node.querySelector("input, select, textarea") !== null);
1374
- const isRelevant = (record) => record.type === "attributes" || Array.from(record.addedNodes).some(touchesFields) || Array.from(record.removedNodes).some(touchesFields);
1375
- state2.mutationObserver = new MutationObserver((records) => {
1376
- if (state2.isDestroyed || !records.some(isRelevant)) return;
1377
- clearFieldCache();
1378
- if (state2.isValidated) {
1379
- validateNames();
1380
- } else syncVisualState();
1381
- notifySubscribers();
1382
- });
1596
+ state2.mutationObserver = new MutationObserver(handleMutations);
1383
1597
  state2.mutationObserver.observe(state2.element, {
1384
1598
  childList: true,
1385
1599
  subtree: true,
@@ -1408,6 +1622,7 @@ var createFormController = (target, options = {}) => {
1408
1622
  setErrors: createSetErrors(state2, helpers),
1409
1623
  clearErrors: createClearErrors(state2, helpers),
1410
1624
  setValue: createSetValue(state2, helpers),
1625
+ setOptions: createSetOptions(state2, helpers),
1411
1626
  getValue: createGetValue(state2, helpers),
1412
1627
  getField: createGetField(state2, helpers),
1413
1628
  validate: createValidate(state2, helpers),
@@ -1427,6 +1642,7 @@ var createFormController = (target, options = {}) => {
1427
1642
  addListener2(document, "input", handleExternalDelegatedEvent);
1428
1643
  addListener2(state2.element, "submit", handleSubmitEvent);
1429
1644
  addListener2(state2.element, "reset", handleResetEvent);
1645
+ snapshotAllSelects();
1430
1646
  startMutationObserver();
1431
1647
  }
1432
1648
  if (normalizedOptions.autoSubmit) {
@@ -1486,4 +1702,4 @@ export {
1486
1702
  form,
1487
1703
  browser
1488
1704
  };
1489
- //# sourceMappingURL=chunk-23VZAYA3.js.map
1705
+ //# sourceMappingURL=chunk-JAEFD56I.js.map