@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.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,126 @@ 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);
210
229
  };
211
- var buildHandler = (field, mirror, formatFn, formatType, formatOptions, mirrorFieldName) => {
230
+ var readInputType = (event) => {
231
+ const candidate = event;
232
+ return typeof candidate.inputType === "string" ? candidate.inputType : void 0;
233
+ };
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);
240
258
  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
259
+ const displayName = config.displayField ?? `${fieldName}_displayed`;
260
+ const canonicalFields = helpers.getFieldsByName(fieldName);
261
+ const candidates = canonicalFields.filter(
262
+ (f) => f instanceof HTMLInputElement && f.type !== "hidden" || f instanceof HTMLTextAreaElement
245
263
  );
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,
264
+ let visible = candidates[0] ?? null;
265
+ if (!visible) {
266
+ visible = findVisibleField(state2.element, displayName);
267
+ }
268
+ if (!visible) continue;
269
+ const existing = bucket.get(fieldName);
270
+ let mirror = findHiddenMirror(state2.element, fieldName);
271
+ const mirrorIsOwned = !mirror;
272
+ if (!mirror) mirror = ensureHiddenMirror(state2.element, fieldName);
273
+ renameVisibleField(visible, displayName);
274
+ if (existing) {
275
+ existing.displayName = displayName;
276
+ existing.mirrorIsOwned = mirrorIsOwned;
277
+ state2.formattedFields.set(fieldName, {
278
+ config,
279
+ displayName,
283
280
  mirrorIsOwned
284
281
  });
282
+ } else {
283
+ bucket.set(fieldName, {
284
+ canonicalName: fieldName,
285
+ displayName,
286
+ visible,
287
+ mirror,
288
+ mirrorIsOwned,
289
+ handler: UNBOUND
290
+ });
285
291
  state2.formattedFields.set(fieldName, {
286
292
  config,
287
- mirrorName: trackerMirrorName,
293
+ displayName,
288
294
  mirrorIsOwned
289
295
  });
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 = "";
296
+ }
297
+ }
298
+ const formatter = await loadFormatter();
299
+ if (!formatter) {
300
+ rollbackPhase1(bucket, state2.formattedFields);
301
+ return;
302
+ }
303
+ for (const [, entry] of bucket) {
304
+ if (entry.handler !== UNBOUND) {
305
+ const current = entry.visible.value;
306
+ if (current !== "") {
307
+ const { formatted, raw } = formatter.format(current, formatType, formatOptions);
308
+ applyFormattedValue(entry.visible, entry.mirror, formatted, raw);
296
309
  }
310
+ continue;
311
+ }
312
+ const handler = buildHandler(
313
+ entry.visible,
314
+ entry.mirror,
315
+ formatter.format,
316
+ formatType,
317
+ formatOptions
318
+ );
319
+ addListener(state2, state2.element, "input", handler, true);
320
+ entry.handler = handler;
321
+ const initial = entry.visible.value;
322
+ if (initial !== "") {
323
+ const { formatted, raw } = formatter.format(initial, formatType, formatOptions);
324
+ applyFormattedValue(entry.visible, entry.mirror, formatted, raw);
325
+ } else if (entry.mirrorIsOwned && entry.mirror.value !== "") {
326
+ entry.mirror.value = "";
327
+ }
328
+ }
329
+ };
330
+ var rollbackPhase1 = (bucket, registry2) => {
331
+ for (const [fieldName, entry] of bucket) {
332
+ if (entry.visible.isConnected) {
333
+ restoreVisibleName(entry.visible, fieldName);
297
334
  }
335
+ if (entry.mirrorIsOwned && entry.mirror.isConnected) {
336
+ entry.mirror.remove();
337
+ }
338
+ bucket.delete(fieldName);
339
+ registry2.delete(fieldName);
298
340
  }
299
341
  };
300
342
  var createFormat = (state2, helpers) => (config) => {
@@ -304,6 +346,14 @@ var createFormat = (state2, helpers) => (config) => {
304
346
  return state2.api;
305
347
  };
306
348
  var createFormatAll = createFormat;
349
+ var resolveCanonicalForName = (state2, name) => {
350
+ const bucket = registry.get(state2);
351
+ if (!bucket) return null;
352
+ for (const [canonical, entry] of bucket) {
353
+ if (canonical === name || entry.displayName === name) return canonical;
354
+ }
355
+ return null;
356
+ };
307
357
  var cleanupFormatRegistry = (state2) => {
308
358
  if (!state2.element) {
309
359
  registry.delete(state2);
@@ -315,10 +365,14 @@ var cleanupFormatRegistry = (state2) => {
315
365
  return;
316
366
  }
317
367
  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();
368
+ if (entry.handler !== UNBOUND) {
369
+ state2.element.removeEventListener("input", entry.handler, true);
370
+ }
371
+ if (entry.visible.isConnected) {
372
+ restoreVisibleName(entry.visible, fieldName);
373
+ }
374
+ if (entry.mirrorIsOwned && entry.mirror.isConnected) {
375
+ entry.mirror.remove();
322
376
  }
323
377
  bucket.delete(fieldName);
324
378
  state2.formattedFields.delete(fieldName);
@@ -331,7 +385,12 @@ var createDestroy = (state2, _helpers) => () => {
331
385
  if (state2.isDestroyed) return;
332
386
  state2.isDestroyed = true;
333
387
  for (const listener of state2.listeners) {
334
- listener.element.removeEventListener(listener.type, listener.handler);
388
+ const options = listener.capture ? { capture: true } : void 0;
389
+ if (options) {
390
+ listener.element.removeEventListener(listener.type, listener.handler, options);
391
+ } else {
392
+ listener.element.removeEventListener(listener.type, listener.handler);
393
+ }
335
394
  }
336
395
  state2.listeners.length = 0;
337
396
  if (state2.autoSubmitTimer) {
@@ -779,9 +838,15 @@ var createFormController = (target, options = {}) => {
779
838
  const snapshot = state2.api.getState();
780
839
  state2.subscribers.forEach((listener) => listener(snapshot));
781
840
  };
782
- const addListener2 = (node, type, handler) => {
783
- node.addEventListener(type, handler);
784
- state2.listeners.push({ element: node, type, handler });
841
+ const addListener2 = (node, type, handler, capture) => {
842
+ if (capture) {
843
+ node.addEventListener(type, handler, { capture: true });
844
+ } else {
845
+ node.addEventListener(type, handler);
846
+ }
847
+ const entry = { element: node, type, handler };
848
+ if (capture !== void 0) entry.capture = capture;
849
+ state2.listeners.push(entry);
785
850
  };
786
851
  const clearFieldCache = () => state2.fieldCache.clear();
787
852
  const getFieldsByName = (name) => {
@@ -797,6 +862,12 @@ var createFormController = (target, options = {}) => {
797
862
  for (const field of getNamedFields(state2.element)) {
798
863
  names.add(field.name);
799
864
  }
865
+ for (const [canonical, entry] of state2.formattedFields) {
866
+ names.add(canonical);
867
+ if (entry.displayName && entry.displayName !== canonical) {
868
+ names.add(entry.displayName);
869
+ }
870
+ }
800
871
  return Array.from(names);
801
872
  };
802
873
  const getValues = () => {
@@ -882,14 +953,16 @@ var createFormController = (target, options = {}) => {
882
953
  const name = target2.name;
883
954
  if (!name) return;
884
955
  clearFieldCache();
956
+ const canonical = resolveCanonicalForName(state2, name) ?? name;
885
957
  if (normalizedOptions.clearManualErrorsOnChange) {
886
- delete state2.manualErrors[name];
958
+ delete state2.manualErrors[canonical];
887
959
  }
888
- syncVisualState([name]);
889
- if (state2.isValidated && state2.validators[name]) {
890
- validateNames([name]);
960
+ syncVisualState([canonical]);
961
+ if (state2.isValidated && state2.validators[canonical]) {
962
+ validateNames([canonical]);
891
963
  }
892
- emitFieldWatchers(name);
964
+ emitFieldWatchers(canonical);
965
+ if (canonical !== name) emitFieldWatchers(name);
893
966
  notifySubscribers();
894
967
  scheduleAutoSubmit();
895
968
  };