@samline/forms 2.2.3 → 2.3.1

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,130 @@ 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);
270
+ };
271
+ var readInputType = (event) => {
272
+ const candidate = event;
273
+ return typeof candidate.inputType === "string" ? candidate.inputType : void 0;
251
274
  };
252
- var buildHandler = (field, mirror, formatFn, formatType, formatOptions, mirrorFieldName) => {
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);
299
+ const phase1Entries = [];
281
300
  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
301
+ const displayName = config.displayField ?? `${fieldName}_displayed`;
302
+ const canonicalFields = helpers.getFieldsByName(fieldName);
303
+ const candidates = canonicalFields.filter(
304
+ (f) => f instanceof HTMLInputElement && f.type !== "hidden" || f instanceof HTMLTextAreaElement
286
305
  );
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,
306
+ let visible = candidates[0] ?? null;
307
+ if (!visible) {
308
+ visible = findVisibleField(state2.element, displayName);
309
+ }
310
+ if (!visible) continue;
311
+ const existing = bucket.get(fieldName);
312
+ let mirror = findHiddenMirror(state2.element, fieldName);
313
+ const mirrorIsOwned = !mirror;
314
+ if (!mirror) mirror = ensureHiddenMirror(state2.element, fieldName);
315
+ renameVisibleField(visible, displayName);
316
+ if (existing) {
317
+ existing.displayName = displayName;
318
+ existing.mirrorIsOwned = mirrorIsOwned;
319
+ state2.formattedFields.set(fieldName, {
320
+ config,
321
+ displayName,
324
322
  mirrorIsOwned
325
323
  });
324
+ phase1Entries.push(existing);
325
+ } else {
326
+ const entry = {
327
+ canonicalName: fieldName,
328
+ displayName,
329
+ visible,
330
+ mirror,
331
+ mirrorIsOwned,
332
+ handler: UNBOUND
333
+ };
334
+ bucket.set(fieldName, entry);
326
335
  state2.formattedFields.set(fieldName, {
327
336
  config,
328
- mirrorName: trackerMirrorName,
337
+ displayName,
329
338
  mirrorIsOwned
330
339
  });
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 = "";
340
+ phase1Entries.push(entry);
341
+ }
342
+ }
343
+ const formatter = await loadFormatter();
344
+ if (!formatter) {
345
+ rollbackPhase1(phase1Entries, bucket, state2.formattedFields);
346
+ return;
347
+ }
348
+ for (const entry of phase1Entries) {
349
+ if (entry.handler !== UNBOUND) {
350
+ const current = entry.visible.value;
351
+ if (current !== "") {
352
+ const { formatted, raw } = formatter.format(current, formatType, formatOptions);
353
+ applyFormattedValue(entry.visible, entry.mirror, formatted, raw);
337
354
  }
355
+ continue;
356
+ }
357
+ const handler = buildHandler(
358
+ entry.visible,
359
+ entry.mirror,
360
+ formatter.format,
361
+ formatType,
362
+ formatOptions
363
+ );
364
+ addListener(state2, state2.element, "input", handler, true);
365
+ entry.handler = handler;
366
+ const initial = entry.visible.value;
367
+ if (initial !== "") {
368
+ const { formatted, raw } = formatter.format(initial, formatType, formatOptions);
369
+ applyFormattedValue(entry.visible, entry.mirror, formatted, raw);
370
+ } else if (entry.mirrorIsOwned && entry.mirror.value !== "") {
371
+ entry.mirror.value = "";
372
+ }
373
+ }
374
+ };
375
+ var rollbackPhase1 = (entries, bucket, registry2) => {
376
+ for (const entry of entries) {
377
+ if (entry.visible.isConnected) {
378
+ restoreVisibleName(entry.visible, entry.canonicalName);
379
+ }
380
+ if (entry.mirrorIsOwned && entry.mirror.isConnected) {
381
+ entry.mirror.remove();
338
382
  }
383
+ bucket.delete(entry.canonicalName);
384
+ registry2.delete(entry.canonicalName);
339
385
  }
340
386
  };
341
387
  var createFormat = (state2, helpers) => (config) => {
@@ -345,6 +391,14 @@ var createFormat = (state2, helpers) => (config) => {
345
391
  return state2.api;
346
392
  };
347
393
  var createFormatAll = createFormat;
394
+ var resolveCanonicalForName = (state2, name) => {
395
+ const bucket = registry.get(state2);
396
+ if (!bucket) return null;
397
+ for (const [canonical, entry] of bucket) {
398
+ if (canonical === name || entry.displayName === name) return canonical;
399
+ }
400
+ return null;
401
+ };
348
402
  var cleanupFormatRegistry = (state2) => {
349
403
  if (!state2.element) {
350
404
  registry.delete(state2);
@@ -356,10 +410,14 @@ var cleanupFormatRegistry = (state2) => {
356
410
  return;
357
411
  }
358
412
  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();
413
+ if (entry.handler !== UNBOUND) {
414
+ state2.element.removeEventListener("input", entry.handler, true);
415
+ }
416
+ if (entry.visible.isConnected) {
417
+ restoreVisibleName(entry.visible, fieldName);
418
+ }
419
+ if (entry.mirrorIsOwned && entry.mirror.isConnected) {
420
+ entry.mirror.remove();
363
421
  }
364
422
  bucket.delete(fieldName);
365
423
  state2.formattedFields.delete(fieldName);
@@ -372,7 +430,12 @@ var createDestroy = (state2, _helpers) => () => {
372
430
  if (state2.isDestroyed) return;
373
431
  state2.isDestroyed = true;
374
432
  for (const listener of state2.listeners) {
375
- listener.element.removeEventListener(listener.type, listener.handler);
433
+ const options = listener.capture ? { capture: true } : void 0;
434
+ if (options) {
435
+ listener.element.removeEventListener(listener.type, listener.handler, options);
436
+ } else {
437
+ listener.element.removeEventListener(listener.type, listener.handler);
438
+ }
376
439
  }
377
440
  state2.listeners.length = 0;
378
441
  if (state2.autoSubmitTimer) {
@@ -820,9 +883,15 @@ var createFormController = (target, options = {}) => {
820
883
  const snapshot = state2.api.getState();
821
884
  state2.subscribers.forEach((listener) => listener(snapshot));
822
885
  };
823
- const addListener2 = (node, type, handler) => {
824
- node.addEventListener(type, handler);
825
- state2.listeners.push({ element: node, type, handler });
886
+ const addListener2 = (node, type, handler, capture) => {
887
+ if (capture) {
888
+ node.addEventListener(type, handler, { capture: true });
889
+ } else {
890
+ node.addEventListener(type, handler);
891
+ }
892
+ const entry = { element: node, type, handler };
893
+ if (capture !== void 0) entry.capture = capture;
894
+ state2.listeners.push(entry);
826
895
  };
827
896
  const clearFieldCache = () => state2.fieldCache.clear();
828
897
  const getFieldsByName = (name) => {
@@ -838,6 +907,12 @@ var createFormController = (target, options = {}) => {
838
907
  for (const field of getNamedFields(state2.element)) {
839
908
  names.add(field.name);
840
909
  }
910
+ for (const [canonical, entry] of state2.formattedFields) {
911
+ names.add(canonical);
912
+ if (entry.displayName && entry.displayName !== canonical) {
913
+ names.add(entry.displayName);
914
+ }
915
+ }
841
916
  return Array.from(names);
842
917
  };
843
918
  const getValues = () => {
@@ -923,14 +998,16 @@ var createFormController = (target, options = {}) => {
923
998
  const name = target2.name;
924
999
  if (!name) return;
925
1000
  clearFieldCache();
1001
+ const canonical = resolveCanonicalForName(state2, name) ?? name;
926
1002
  if (normalizedOptions.clearManualErrorsOnChange) {
927
- delete state2.manualErrors[name];
1003
+ delete state2.manualErrors[canonical];
928
1004
  }
929
- syncVisualState([name]);
930
- if (state2.isValidated && state2.validators[name]) {
931
- validateNames([name]);
1005
+ syncVisualState([canonical]);
1006
+ if (state2.isValidated && state2.validators[canonical]) {
1007
+ validateNames([canonical]);
932
1008
  }
933
- emitFieldWatchers(name);
1009
+ emitFieldWatchers(canonical);
1010
+ if (canonical !== name) emitFieldWatchers(name);
934
1011
  notifySubscribers();
935
1012
  scheduleAutoSubmit();
936
1013
  };