@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.js CHANGED
@@ -50,33 +50,45 @@ var cssEscape = (value) => {
50
50
  }
51
51
  return value.replace(/([!"#$%&'()*+,./:;<=>?@\[\\\]^`{|}~])/g, "\\$1");
52
52
  };
53
- var findRawMirror = (form2, fieldName) => {
53
+ var findVisibleField = (form2, displayName) => {
54
+ const node = form2.querySelector(
55
+ `[name="${cssEscape(displayName)}"]:not([type="hidden"])`
56
+ );
57
+ if (node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement) {
58
+ return node;
59
+ }
60
+ return null;
61
+ };
62
+ var findHiddenMirror = (form2, fieldName) => {
54
63
  const byAttr = form2.querySelector(
55
64
  `[${FORMATTER_RAW_ATTRIBUTE}="${cssEscape(fieldName)}"]`
56
65
  );
57
66
  if (byAttr) return byAttr;
58
67
  const byName = form2.querySelector(
59
- `[name="${cssEscape(`${fieldName}_raw`)}"]`
68
+ `input[type="hidden"][name="${cssEscape(fieldName)}"]`
60
69
  );
61
70
  if (byName) return byName;
62
- const byNameOld = form2.querySelector(
63
- `[name="${cssEscape(`${fieldName}Raw`)}"]`
64
- );
65
- if (byNameOld) return byNameOld;
66
71
  return null;
67
72
  };
68
- var ensureRawMirror = (form2, fieldName) => {
69
- const existing = findRawMirror(form2, fieldName);
73
+ var ensureHiddenMirror = (form2, fieldName) => {
74
+ const existing = findHiddenMirror(form2, fieldName);
70
75
  if (existing) return existing;
71
76
  const input = document.createElement("input");
72
77
  input.type = "hidden";
73
- input.name = `${fieldName}_raw`;
78
+ input.name = fieldName;
74
79
  input.setAttribute("aria-hidden", "true");
75
80
  input.tabIndex = -1;
76
81
  input.setAttribute(FORMATTER_RAW_ATTRIBUTE, fieldName);
77
82
  form2.appendChild(input);
78
83
  return input;
79
84
  };
85
+ var renameVisibleField = (visible, displayName) => {
86
+ if (visible.name === displayName) return;
87
+ visible.name = displayName;
88
+ };
89
+ var restoreVisibleName = (visible, originalName) => {
90
+ visible.name = originalName;
91
+ };
80
92
  var applyFormattedValue = (visible, mirror, formatted, raw, inputType) => {
81
93
  const rawInput = visible.value;
82
94
  const caret = clamp(
@@ -194,6 +206,7 @@ var loadFormatter = () => {
194
206
  };
195
207
 
196
208
  // src/api/format.ts
209
+ var UNBOUND = null;
197
210
  var registry = /* @__PURE__ */ new WeakMap();
198
211
  var getRegistry = (state2) => {
199
212
  let bucket = registry.get(state2);
@@ -204,97 +217,130 @@ var getRegistry = (state2) => {
204
217
  return bucket;
205
218
  };
206
219
  var resolveFieldNames = (config) => Array.isArray(config.field) ? config.field : [config.field];
207
- var addListener = (state2, target, type, handler) => {
208
- target.addEventListener(type, handler);
209
- state2.listeners.push({ element: target, type, handler });
220
+ var addListener = (state2, target, type, handler, useCapture) => {
221
+ if (useCapture) {
222
+ target.addEventListener(type, handler, { capture: true });
223
+ } else {
224
+ target.addEventListener(type, handler);
225
+ }
226
+ const entry = { element: target, type, handler };
227
+ if (useCapture !== void 0) entry.capture = useCapture;
228
+ state2.listeners.push(entry);
229
+ };
230
+ var readInputType = (event) => {
231
+ const candidate = event;
232
+ return typeof candidate.inputType === "string" ? candidate.inputType : void 0;
210
233
  };
211
- var buildHandler = (field, mirror, formatFn, formatType, formatOptions, mirrorFieldName) => {
234
+ var buildHandler = (visible, mirror, formatFn, formatType, formatOptions) => {
212
235
  const handler = (event) => {
213
- if (!field.isConnected) return;
214
- if (event.target !== field) return;
215
- const rawInput = event.target.value;
236
+ if (!visible.isConnected || !mirror.isConnected) return;
237
+ const isVisible = event.target === visible;
238
+ const isMirror = event.target === mirror;
239
+ if (!isVisible && !isMirror) return;
240
+ const rawInput = isMirror ? mirror.value : visible.value;
216
241
  const inputType = readInputType(event);
217
242
  const { formatted, raw } = formatFn(rawInput, formatType, formatOptions);
218
243
  if (!formatted && !raw) {
219
- field.value = "";
220
- if (mirror && mirror.value !== "") mirror.value = "";
244
+ visible.value = "";
245
+ if (mirror.value !== "") mirror.value = "";
221
246
  return;
222
247
  }
223
- applyFormattedValue(field, mirror, formatted, raw, inputType);
224
- void mirrorFieldName;
248
+ applyFormattedValue(visible, mirror, formatted, raw, inputType);
225
249
  };
226
250
  return handler;
227
251
  };
228
- var readInputType = (event) => {
229
- const candidate = event;
230
- return typeof candidate.inputType === "string" ? candidate.inputType : void 0;
231
- };
232
252
  var applyFormat = async (state2, helpers, config) => {
233
253
  if (!state2.element || !state2.api) return;
234
- const formatter = await loadFormatter();
235
- if (!formatter) return;
236
254
  const formatType = config.type;
237
255
  const formatOptions = config.options;
238
256
  const fieldNames = resolveFieldNames(config);
239
257
  const bucket = getRegistry(state2);
258
+ const phase1Entries = [];
240
259
  for (const fieldName of fieldNames) {
241
- const fields = helpers.getFieldsByName(fieldName);
242
- if (fields.length === 0) continue;
243
- const writable = fields.filter(
244
- (f) => f instanceof HTMLInputElement && f.type !== "checkbox" && f.type !== "radio" && f.type !== "file" && f.type !== "submit" && f.type !== "button" || f instanceof HTMLTextAreaElement
260
+ const displayName = config.displayField ?? `${fieldName}_displayed`;
261
+ const canonicalFields = helpers.getFieldsByName(fieldName);
262
+ const candidates = canonicalFields.filter(
263
+ (f) => f instanceof HTMLInputElement && f.type !== "hidden" || f instanceof HTMLTextAreaElement
245
264
  );
246
- if (writable.length === 0) continue;
247
- for (const field of writable) {
248
- const existing = bucket.get(fieldName);
249
- const mirrorName = config.rawField ?? `${fieldName}_raw`;
250
- let mirror = findRawMirror(state2.element, fieldName) ?? null;
251
- const mirrorIsOwned = !mirror;
252
- if (!mirror) mirror = ensureRawMirror(state2.element, fieldName);
253
- if (mirror && mirror.name !== mirrorName) mirror.name = mirrorName;
254
- const trackerMirrorName = mirror?.name ?? mirrorName;
255
- if (existing) {
256
- existing.mirrorName = trackerMirrorName;
257
- existing.mirrorIsOwned = mirrorIsOwned;
258
- state2.formattedFields.set(fieldName, {
259
- config,
260
- mirrorName: trackerMirrorName,
261
- mirrorIsOwned
262
- });
263
- const current = field.value;
264
- if (current !== "") {
265
- const { formatted, raw } = formatter.format(current, formatType, formatOptions);
266
- applyFormattedValue(field, mirror, formatted, raw);
267
- }
268
- continue;
269
- }
270
- const handler = buildHandler(
271
- field,
272
- mirror,
273
- formatter.format,
274
- formatType,
275
- formatOptions,
276
- trackerMirrorName
277
- );
278
- addListener(state2, state2.element, "input", handler);
279
- bucket.set(fieldName, {
280
- fields: /* @__PURE__ */ new Set([field]),
281
- mirrorName: trackerMirrorName,
282
- handler,
265
+ let visible = candidates[0] ?? null;
266
+ if (!visible) {
267
+ visible = findVisibleField(state2.element, displayName);
268
+ }
269
+ if (!visible) continue;
270
+ const existing = bucket.get(fieldName);
271
+ let mirror = findHiddenMirror(state2.element, fieldName);
272
+ const mirrorIsOwned = !mirror;
273
+ if (!mirror) mirror = ensureHiddenMirror(state2.element, fieldName);
274
+ renameVisibleField(visible, displayName);
275
+ if (existing) {
276
+ existing.displayName = displayName;
277
+ existing.mirrorIsOwned = mirrorIsOwned;
278
+ state2.formattedFields.set(fieldName, {
279
+ config,
280
+ displayName,
283
281
  mirrorIsOwned
284
282
  });
283
+ phase1Entries.push(existing);
284
+ } else {
285
+ const entry = {
286
+ canonicalName: fieldName,
287
+ displayName,
288
+ visible,
289
+ mirror,
290
+ mirrorIsOwned,
291
+ handler: UNBOUND
292
+ };
293
+ bucket.set(fieldName, entry);
285
294
  state2.formattedFields.set(fieldName, {
286
295
  config,
287
- mirrorName: trackerMirrorName,
296
+ displayName,
288
297
  mirrorIsOwned
289
298
  });
290
- const initial = field.value;
291
- if (initial !== "") {
292
- const { formatted, raw } = formatter.format(initial, formatType, formatOptions);
293
- applyFormattedValue(field, mirror, formatted, raw);
294
- } else if (mirrorIsOwned && mirror && mirror.value !== "") {
295
- mirror.value = "";
299
+ phase1Entries.push(entry);
300
+ }
301
+ }
302
+ const formatter = await loadFormatter();
303
+ if (!formatter) {
304
+ rollbackPhase1(phase1Entries, bucket, state2.formattedFields);
305
+ return;
306
+ }
307
+ for (const entry of phase1Entries) {
308
+ if (entry.handler !== UNBOUND) {
309
+ const current = entry.visible.value;
310
+ if (current !== "") {
311
+ const { formatted, raw } = formatter.format(current, formatType, formatOptions);
312
+ applyFormattedValue(entry.visible, entry.mirror, formatted, raw);
296
313
  }
314
+ continue;
315
+ }
316
+ const handler = buildHandler(
317
+ entry.visible,
318
+ entry.mirror,
319
+ formatter.format,
320
+ formatType,
321
+ formatOptions
322
+ );
323
+ addListener(state2, state2.element, "input", handler, true);
324
+ entry.handler = handler;
325
+ const initial = entry.visible.value;
326
+ if (initial !== "") {
327
+ const { formatted, raw } = formatter.format(initial, formatType, formatOptions);
328
+ applyFormattedValue(entry.visible, entry.mirror, formatted, raw);
329
+ } else if (entry.mirrorIsOwned && entry.mirror.value !== "") {
330
+ entry.mirror.value = "";
331
+ }
332
+ }
333
+ };
334
+ var rollbackPhase1 = (entries, bucket, registry2) => {
335
+ for (const entry of entries) {
336
+ if (entry.visible.isConnected) {
337
+ restoreVisibleName(entry.visible, entry.canonicalName);
338
+ }
339
+ if (entry.mirrorIsOwned && entry.mirror.isConnected) {
340
+ entry.mirror.remove();
297
341
  }
342
+ bucket.delete(entry.canonicalName);
343
+ registry2.delete(entry.canonicalName);
298
344
  }
299
345
  };
300
346
  var createFormat = (state2, helpers) => (config) => {
@@ -304,6 +350,14 @@ var createFormat = (state2, helpers) => (config) => {
304
350
  return state2.api;
305
351
  };
306
352
  var createFormatAll = createFormat;
353
+ var resolveCanonicalForName = (state2, name) => {
354
+ const bucket = registry.get(state2);
355
+ if (!bucket) return null;
356
+ for (const [canonical, entry] of bucket) {
357
+ if (canonical === name || entry.displayName === name) return canonical;
358
+ }
359
+ return null;
360
+ };
307
361
  var cleanupFormatRegistry = (state2) => {
308
362
  if (!state2.element) {
309
363
  registry.delete(state2);
@@ -315,10 +369,14 @@ var cleanupFormatRegistry = (state2) => {
315
369
  return;
316
370
  }
317
371
  for (const [fieldName, entry] of bucket) {
318
- state2.element.removeEventListener("input", entry.handler);
319
- if (entry.mirrorIsOwned) {
320
- const mirror = findRawMirror(state2.element, entry.mirrorName);
321
- mirror?.remove();
372
+ if (entry.handler !== UNBOUND) {
373
+ state2.element.removeEventListener("input", entry.handler, true);
374
+ }
375
+ if (entry.visible.isConnected) {
376
+ restoreVisibleName(entry.visible, fieldName);
377
+ }
378
+ if (entry.mirrorIsOwned && entry.mirror.isConnected) {
379
+ entry.mirror.remove();
322
380
  }
323
381
  bucket.delete(fieldName);
324
382
  state2.formattedFields.delete(fieldName);
@@ -331,7 +389,12 @@ var createDestroy = (state2, _helpers) => () => {
331
389
  if (state2.isDestroyed) return;
332
390
  state2.isDestroyed = true;
333
391
  for (const listener of state2.listeners) {
334
- listener.element.removeEventListener(listener.type, listener.handler);
392
+ const options = listener.capture ? { capture: true } : void 0;
393
+ if (options) {
394
+ listener.element.removeEventListener(listener.type, listener.handler, options);
395
+ } else {
396
+ listener.element.removeEventListener(listener.type, listener.handler);
397
+ }
335
398
  }
336
399
  state2.listeners.length = 0;
337
400
  if (state2.autoSubmitTimer) {
@@ -779,9 +842,15 @@ var createFormController = (target, options = {}) => {
779
842
  const snapshot = state2.api.getState();
780
843
  state2.subscribers.forEach((listener) => listener(snapshot));
781
844
  };
782
- const addListener2 = (node, type, handler) => {
783
- node.addEventListener(type, handler);
784
- state2.listeners.push({ element: node, type, handler });
845
+ const addListener2 = (node, type, handler, capture) => {
846
+ if (capture) {
847
+ node.addEventListener(type, handler, { capture: true });
848
+ } else {
849
+ node.addEventListener(type, handler);
850
+ }
851
+ const entry = { element: node, type, handler };
852
+ if (capture !== void 0) entry.capture = capture;
853
+ state2.listeners.push(entry);
785
854
  };
786
855
  const clearFieldCache = () => state2.fieldCache.clear();
787
856
  const getFieldsByName = (name) => {
@@ -797,6 +866,12 @@ var createFormController = (target, options = {}) => {
797
866
  for (const field of getNamedFields(state2.element)) {
798
867
  names.add(field.name);
799
868
  }
869
+ for (const [canonical, entry] of state2.formattedFields) {
870
+ names.add(canonical);
871
+ if (entry.displayName && entry.displayName !== canonical) {
872
+ names.add(entry.displayName);
873
+ }
874
+ }
800
875
  return Array.from(names);
801
876
  };
802
877
  const getValues = () => {
@@ -882,14 +957,16 @@ var createFormController = (target, options = {}) => {
882
957
  const name = target2.name;
883
958
  if (!name) return;
884
959
  clearFieldCache();
960
+ const canonical = resolveCanonicalForName(state2, name) ?? name;
885
961
  if (normalizedOptions.clearManualErrorsOnChange) {
886
- delete state2.manualErrors[name];
962
+ delete state2.manualErrors[canonical];
887
963
  }
888
- syncVisualState([name]);
889
- if (state2.isValidated && state2.validators[name]) {
890
- validateNames([name]);
964
+ syncVisualState([canonical]);
965
+ if (state2.isValidated && state2.validators[canonical]) {
966
+ validateNames([canonical]);
891
967
  }
892
- emitFieldWatchers(name);
968
+ emitFieldWatchers(canonical);
969
+ if (canonical !== name) emitFieldWatchers(name);
893
970
  notifySubscribers();
894
971
  scheduleAutoSubmit();
895
972
  };