@samline/forms 2.2.3 → 2.3.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.
package/dist/index.cjs CHANGED
@@ -91,33 +91,45 @@ var cssEscape = (value) => {
91
91
  }
92
92
  return value.replace(/([!"#$%&'()*+,./:;<=>?@\[\\\]^`{|}~])/g, "\\$1");
93
93
  };
94
- var findRawMirror = (form2, fieldName) => {
94
+ var findVisibleField = (form2, displayName) => {
95
+ const node = form2.querySelector(
96
+ `[name="${cssEscape(displayName)}"]:not([type="hidden"])`
97
+ );
98
+ if (node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement) {
99
+ return node;
100
+ }
101
+ return null;
102
+ };
103
+ var findHiddenMirror = (form2, fieldName) => {
95
104
  const byAttr = form2.querySelector(
96
105
  `[${FORMATTER_RAW_ATTRIBUTE}="${cssEscape(fieldName)}"]`
97
106
  );
98
107
  if (byAttr) return byAttr;
99
108
  const byName = form2.querySelector(
100
- `[name="${cssEscape(`${fieldName}_raw`)}"]`
109
+ `input[type="hidden"][name="${cssEscape(fieldName)}"]`
101
110
  );
102
111
  if (byName) return byName;
103
- const byNameOld = form2.querySelector(
104
- `[name="${cssEscape(`${fieldName}Raw`)}"]`
105
- );
106
- if (byNameOld) return byNameOld;
107
112
  return null;
108
113
  };
109
- var ensureRawMirror = (form2, fieldName) => {
110
- const existing = findRawMirror(form2, fieldName);
114
+ var ensureHiddenMirror = (form2, fieldName) => {
115
+ const existing = findHiddenMirror(form2, fieldName);
111
116
  if (existing) return existing;
112
117
  const input = document.createElement("input");
113
118
  input.type = "hidden";
114
- input.name = `${fieldName}_raw`;
119
+ input.name = fieldName;
115
120
  input.setAttribute("aria-hidden", "true");
116
121
  input.tabIndex = -1;
117
122
  input.setAttribute(FORMATTER_RAW_ATTRIBUTE, fieldName);
118
123
  form2.appendChild(input);
119
124
  return input;
120
125
  };
126
+ var renameVisibleField = (visible, displayName) => {
127
+ if (visible.name === displayName) return;
128
+ visible.name = displayName;
129
+ };
130
+ var restoreVisibleName = (visible, originalName) => {
131
+ visible.name = originalName;
132
+ };
121
133
  var applyFormattedValue = (visible, mirror, formatted, raw, inputType) => {
122
134
  const rawInput = visible.value;
123
135
  const caret = clamp(
@@ -235,6 +247,7 @@ var loadFormatter = () => {
235
247
  };
236
248
 
237
249
  // src/api/format.ts
250
+ var UNBOUND = null;
238
251
  var registry = /* @__PURE__ */ new WeakMap();
239
252
  var getRegistry = (state2) => {
240
253
  let bucket = registry.get(state2);
@@ -245,97 +258,126 @@ var getRegistry = (state2) => {
245
258
  return bucket;
246
259
  };
247
260
  var resolveFieldNames = (config) => Array.isArray(config.field) ? config.field : [config.field];
248
- var addListener = (state2, target, type, handler) => {
249
- target.addEventListener(type, handler);
250
- state2.listeners.push({ element: target, type, handler });
261
+ var addListener = (state2, target, type, handler, useCapture) => {
262
+ if (useCapture) {
263
+ target.addEventListener(type, handler, { capture: true });
264
+ } else {
265
+ target.addEventListener(type, handler);
266
+ }
267
+ const entry = { element: target, type, handler };
268
+ if (useCapture !== void 0) entry.capture = useCapture;
269
+ state2.listeners.push(entry);
251
270
  };
252
- var buildHandler = (field, mirror, formatFn, formatType, formatOptions, mirrorFieldName) => {
271
+ var readInputType = (event) => {
272
+ const candidate = event;
273
+ return typeof candidate.inputType === "string" ? candidate.inputType : void 0;
274
+ };
275
+ var buildHandler = (visible, mirror, formatFn, formatType, formatOptions) => {
253
276
  const handler = (event) => {
254
- if (!field.isConnected) return;
255
- if (event.target !== field) return;
256
- const rawInput = event.target.value;
277
+ if (!visible.isConnected || !mirror.isConnected) return;
278
+ const isVisible = event.target === visible;
279
+ const isMirror = event.target === mirror;
280
+ if (!isVisible && !isMirror) return;
281
+ const rawInput = isMirror ? mirror.value : visible.value;
257
282
  const inputType = readInputType(event);
258
283
  const { formatted, raw } = formatFn(rawInput, formatType, formatOptions);
259
284
  if (!formatted && !raw) {
260
- field.value = "";
261
- if (mirror && mirror.value !== "") mirror.value = "";
285
+ visible.value = "";
286
+ if (mirror.value !== "") mirror.value = "";
262
287
  return;
263
288
  }
264
- applyFormattedValue(field, mirror, formatted, raw, inputType);
265
- void mirrorFieldName;
289
+ applyFormattedValue(visible, mirror, formatted, raw, inputType);
266
290
  };
267
291
  return handler;
268
292
  };
269
- var readInputType = (event) => {
270
- const candidate = event;
271
- return typeof candidate.inputType === "string" ? candidate.inputType : void 0;
272
- };
273
293
  var applyFormat = async (state2, helpers, config) => {
274
294
  if (!state2.element || !state2.api) return;
275
- const formatter = await loadFormatter();
276
- if (!formatter) return;
277
295
  const formatType = config.type;
278
296
  const formatOptions = config.options;
279
297
  const fieldNames = resolveFieldNames(config);
280
298
  const bucket = getRegistry(state2);
281
299
  for (const fieldName of fieldNames) {
282
- const fields = helpers.getFieldsByName(fieldName);
283
- if (fields.length === 0) continue;
284
- const writable = fields.filter(
285
- (f) => f instanceof HTMLInputElement && f.type !== "checkbox" && f.type !== "radio" && f.type !== "file" && f.type !== "submit" && f.type !== "button" || f instanceof HTMLTextAreaElement
300
+ const displayName = config.displayField ?? `${fieldName}_displayed`;
301
+ const canonicalFields = helpers.getFieldsByName(fieldName);
302
+ const candidates = canonicalFields.filter(
303
+ (f) => f instanceof HTMLInputElement && f.type !== "hidden" || f instanceof HTMLTextAreaElement
286
304
  );
287
- if (writable.length === 0) continue;
288
- for (const field of writable) {
289
- const existing = bucket.get(fieldName);
290
- const mirrorName = config.rawField ?? `${fieldName}_raw`;
291
- let mirror = findRawMirror(state2.element, fieldName) ?? null;
292
- const mirrorIsOwned = !mirror;
293
- if (!mirror) mirror = ensureRawMirror(state2.element, fieldName);
294
- if (mirror && mirror.name !== mirrorName) mirror.name = mirrorName;
295
- const trackerMirrorName = mirror?.name ?? mirrorName;
296
- if (existing) {
297
- existing.mirrorName = trackerMirrorName;
298
- existing.mirrorIsOwned = mirrorIsOwned;
299
- state2.formattedFields.set(fieldName, {
300
- config,
301
- mirrorName: trackerMirrorName,
302
- mirrorIsOwned
303
- });
304
- const current = field.value;
305
- if (current !== "") {
306
- const { formatted, raw } = formatter.format(current, formatType, formatOptions);
307
- applyFormattedValue(field, mirror, formatted, raw);
308
- }
309
- continue;
310
- }
311
- const handler = buildHandler(
312
- field,
313
- mirror,
314
- formatter.format,
315
- formatType,
316
- formatOptions,
317
- trackerMirrorName
318
- );
319
- addListener(state2, state2.element, "input", handler);
320
- bucket.set(fieldName, {
321
- fields: /* @__PURE__ */ new Set([field]),
322
- mirrorName: trackerMirrorName,
323
- handler,
305
+ let visible = candidates[0] ?? null;
306
+ if (!visible) {
307
+ visible = findVisibleField(state2.element, displayName);
308
+ }
309
+ if (!visible) continue;
310
+ const existing = bucket.get(fieldName);
311
+ let mirror = findHiddenMirror(state2.element, fieldName);
312
+ const mirrorIsOwned = !mirror;
313
+ if (!mirror) mirror = ensureHiddenMirror(state2.element, fieldName);
314
+ renameVisibleField(visible, displayName);
315
+ if (existing) {
316
+ existing.displayName = displayName;
317
+ existing.mirrorIsOwned = mirrorIsOwned;
318
+ state2.formattedFields.set(fieldName, {
319
+ config,
320
+ displayName,
324
321
  mirrorIsOwned
325
322
  });
323
+ } else {
324
+ bucket.set(fieldName, {
325
+ canonicalName: fieldName,
326
+ displayName,
327
+ visible,
328
+ mirror,
329
+ mirrorIsOwned,
330
+ handler: UNBOUND
331
+ });
326
332
  state2.formattedFields.set(fieldName, {
327
333
  config,
328
- mirrorName: trackerMirrorName,
334
+ displayName,
329
335
  mirrorIsOwned
330
336
  });
331
- const initial = field.value;
332
- if (initial !== "") {
333
- const { formatted, raw } = formatter.format(initial, formatType, formatOptions);
334
- applyFormattedValue(field, mirror, formatted, raw);
335
- } else if (mirrorIsOwned && mirror && mirror.value !== "") {
336
- mirror.value = "";
337
+ }
338
+ }
339
+ const formatter = await loadFormatter();
340
+ if (!formatter) {
341
+ rollbackPhase1(bucket, state2.formattedFields);
342
+ return;
343
+ }
344
+ for (const [, entry] of bucket) {
345
+ if (entry.handler !== UNBOUND) {
346
+ const current = entry.visible.value;
347
+ if (current !== "") {
348
+ const { formatted, raw } = formatter.format(current, formatType, formatOptions);
349
+ applyFormattedValue(entry.visible, entry.mirror, formatted, raw);
337
350
  }
351
+ continue;
352
+ }
353
+ const handler = buildHandler(
354
+ entry.visible,
355
+ entry.mirror,
356
+ formatter.format,
357
+ formatType,
358
+ formatOptions
359
+ );
360
+ addListener(state2, state2.element, "input", handler, true);
361
+ entry.handler = handler;
362
+ const initial = entry.visible.value;
363
+ if (initial !== "") {
364
+ const { formatted, raw } = formatter.format(initial, formatType, formatOptions);
365
+ applyFormattedValue(entry.visible, entry.mirror, formatted, raw);
366
+ } else if (entry.mirrorIsOwned && entry.mirror.value !== "") {
367
+ entry.mirror.value = "";
368
+ }
369
+ }
370
+ };
371
+ var rollbackPhase1 = (bucket, registry2) => {
372
+ for (const [fieldName, entry] of bucket) {
373
+ if (entry.visible.isConnected) {
374
+ restoreVisibleName(entry.visible, fieldName);
338
375
  }
376
+ if (entry.mirrorIsOwned && entry.mirror.isConnected) {
377
+ entry.mirror.remove();
378
+ }
379
+ bucket.delete(fieldName);
380
+ registry2.delete(fieldName);
339
381
  }
340
382
  };
341
383
  var createFormat = (state2, helpers) => (config) => {
@@ -345,6 +387,14 @@ var createFormat = (state2, helpers) => (config) => {
345
387
  return state2.api;
346
388
  };
347
389
  var createFormatAll = createFormat;
390
+ var resolveCanonicalForName = (state2, name) => {
391
+ const bucket = registry.get(state2);
392
+ if (!bucket) return null;
393
+ for (const [canonical, entry] of bucket) {
394
+ if (canonical === name || entry.displayName === name) return canonical;
395
+ }
396
+ return null;
397
+ };
348
398
  var cleanupFormatRegistry = (state2) => {
349
399
  if (!state2.element) {
350
400
  registry.delete(state2);
@@ -356,10 +406,14 @@ var cleanupFormatRegistry = (state2) => {
356
406
  return;
357
407
  }
358
408
  for (const [fieldName, entry] of bucket) {
359
- state2.element.removeEventListener("input", entry.handler);
360
- if (entry.mirrorIsOwned) {
361
- const mirror = findRawMirror(state2.element, entry.mirrorName);
362
- mirror?.remove();
409
+ if (entry.handler !== UNBOUND) {
410
+ state2.element.removeEventListener("input", entry.handler, true);
411
+ }
412
+ if (entry.visible.isConnected) {
413
+ restoreVisibleName(entry.visible, fieldName);
414
+ }
415
+ if (entry.mirrorIsOwned && entry.mirror.isConnected) {
416
+ entry.mirror.remove();
363
417
  }
364
418
  bucket.delete(fieldName);
365
419
  state2.formattedFields.delete(fieldName);
@@ -372,7 +426,12 @@ var createDestroy = (state2, _helpers) => () => {
372
426
  if (state2.isDestroyed) return;
373
427
  state2.isDestroyed = true;
374
428
  for (const listener of state2.listeners) {
375
- listener.element.removeEventListener(listener.type, listener.handler);
429
+ const options = listener.capture ? { capture: true } : void 0;
430
+ if (options) {
431
+ listener.element.removeEventListener(listener.type, listener.handler, options);
432
+ } else {
433
+ listener.element.removeEventListener(listener.type, listener.handler);
434
+ }
376
435
  }
377
436
  state2.listeners.length = 0;
378
437
  if (state2.autoSubmitTimer) {
@@ -820,9 +879,15 @@ var createFormController = (target, options = {}) => {
820
879
  const snapshot = state2.api.getState();
821
880
  state2.subscribers.forEach((listener) => listener(snapshot));
822
881
  };
823
- const addListener2 = (node, type, handler) => {
824
- node.addEventListener(type, handler);
825
- state2.listeners.push({ element: node, type, handler });
882
+ const addListener2 = (node, type, handler, capture) => {
883
+ if (capture) {
884
+ node.addEventListener(type, handler, { capture: true });
885
+ } else {
886
+ node.addEventListener(type, handler);
887
+ }
888
+ const entry = { element: node, type, handler };
889
+ if (capture !== void 0) entry.capture = capture;
890
+ state2.listeners.push(entry);
826
891
  };
827
892
  const clearFieldCache = () => state2.fieldCache.clear();
828
893
  const getFieldsByName = (name) => {
@@ -838,6 +903,12 @@ var createFormController = (target, options = {}) => {
838
903
  for (const field of getNamedFields(state2.element)) {
839
904
  names.add(field.name);
840
905
  }
906
+ for (const [canonical, entry] of state2.formattedFields) {
907
+ names.add(canonical);
908
+ if (entry.displayName && entry.displayName !== canonical) {
909
+ names.add(entry.displayName);
910
+ }
911
+ }
841
912
  return Array.from(names);
842
913
  };
843
914
  const getValues = () => {
@@ -923,14 +994,16 @@ var createFormController = (target, options = {}) => {
923
994
  const name = target2.name;
924
995
  if (!name) return;
925
996
  clearFieldCache();
997
+ const canonical = resolveCanonicalForName(state2, name) ?? name;
926
998
  if (normalizedOptions.clearManualErrorsOnChange) {
927
- delete state2.manualErrors[name];
999
+ delete state2.manualErrors[canonical];
928
1000
  }
929
- syncVisualState([name]);
930
- if (state2.isValidated && state2.validators[name]) {
931
- validateNames([name]);
1001
+ syncVisualState([canonical]);
1002
+ if (state2.isValidated && state2.validators[canonical]) {
1003
+ validateNames([canonical]);
932
1004
  }
933
- emitFieldWatchers(name);
1005
+ emitFieldWatchers(canonical);
1006
+ if (canonical !== name) emitFieldWatchers(name);
934
1007
  notifySubscribers();
935
1008
  scheduleAutoSubmit();
936
1009
  };