@samline/forms 2.2.2 → 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,50 +50,75 @@ 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
  };
80
- var applyFormattedValue = (visible, mirror, formatted, raw) => {
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
+ };
92
+ var applyFormattedValue = (visible, mirror, formatted, raw, inputType) => {
81
93
  const rawInput = visible.value;
82
94
  const caret = clamp(
83
95
  visible.selectionStart ?? rawInput.length,
84
96
  0,
85
97
  rawInput.length
86
98
  );
99
+ if (mirror && mirror.value !== raw) mirror.value = raw;
87
100
  if (rawInput === formatted) {
88
- if (mirror && mirror.value !== raw) mirror.value = raw;
89
- return caret;
101
+ const newCaret2 = pullCaretPastLeadingDelimiter(caret, formatted, inputType);
102
+ if (newCaret2 !== caret) restoreCursor(visible, newCaret2);
103
+ return newCaret2;
90
104
  }
91
105
  visible.value = formatted;
92
- if (mirror) mirror.value = raw;
93
- const newCaret = computeCursorPosition(caret, rawInput, formatted);
106
+ const newCaret = pullCaretPastLeadingDelimiter(
107
+ computeCursorPosition(caret, rawInput, formatted),
108
+ formatted,
109
+ inputType
110
+ );
94
111
  restoreCursor(visible, newCaret);
95
112
  return newCaret;
96
113
  };
114
+ var pullCaretPastLeadingDelimiter = (caret, value, inputType) => {
115
+ if (!inputType || !inputType.startsWith("delete")) return caret;
116
+ let cursor = caret;
117
+ while (cursor > 0 && isFormatChar(value[cursor - 1])) {
118
+ cursor -= 1;
119
+ }
120
+ return cursor;
121
+ };
97
122
  var clamp = (value, min, max) => Math.max(min, Math.min(max, value));
98
123
  var computeCursorPosition = (caretPos, rawInput, newValue) => {
99
124
  if (newValue.length === 0) return 0;
@@ -181,6 +206,7 @@ var loadFormatter = () => {
181
206
  };
182
207
 
183
208
  // src/api/format.ts
209
+ var UNBOUND = null;
184
210
  var registry = /* @__PURE__ */ new WeakMap();
185
211
  var getRegistry = (state2) => {
186
212
  let bucket = registry.get(state2);
@@ -191,92 +217,126 @@ var getRegistry = (state2) => {
191
217
  return bucket;
192
218
  };
193
219
  var resolveFieldNames = (config) => Array.isArray(config.field) ? config.field : [config.field];
194
- var addListener = (state2, target, type, handler) => {
195
- target.addEventListener(type, handler);
196
- 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;
197
233
  };
198
- var buildHandler = (field, mirror, formatFn, formatType, formatOptions, mirrorFieldName) => {
234
+ var buildHandler = (visible, mirror, formatFn, formatType, formatOptions) => {
199
235
  const handler = (event) => {
200
- if (!field.isConnected) return;
201
- if (event.target !== field) return;
202
- 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;
241
+ const inputType = readInputType(event);
203
242
  const { formatted, raw } = formatFn(rawInput, formatType, formatOptions);
204
243
  if (!formatted && !raw) {
205
- field.value = "";
206
- if (mirror && mirror.value !== "") mirror.value = "";
244
+ visible.value = "";
245
+ if (mirror.value !== "") mirror.value = "";
207
246
  return;
208
247
  }
209
- applyFormattedValue(field, mirror, formatted, raw);
210
- void mirrorFieldName;
248
+ applyFormattedValue(visible, mirror, formatted, raw, inputType);
211
249
  };
212
250
  return handler;
213
251
  };
214
252
  var applyFormat = async (state2, helpers, config) => {
215
253
  if (!state2.element || !state2.api) return;
216
- const formatter = await loadFormatter();
217
- if (!formatter) return;
218
254
  const formatType = config.type;
219
255
  const formatOptions = config.options;
220
256
  const fieldNames = resolveFieldNames(config);
221
257
  const bucket = getRegistry(state2);
222
258
  for (const fieldName of fieldNames) {
223
- const fields = helpers.getFieldsByName(fieldName);
224
- if (fields.length === 0) continue;
225
- const writable = fields.filter(
226
- (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
227
263
  );
228
- if (writable.length === 0) continue;
229
- for (const field of writable) {
230
- const existing = bucket.get(fieldName);
231
- const mirrorName = config.rawField ?? `${fieldName}_raw`;
232
- let mirror = findRawMirror(state2.element, fieldName) ?? null;
233
- const mirrorIsOwned = !mirror;
234
- if (!mirror) mirror = ensureRawMirror(state2.element, fieldName);
235
- if (mirror && mirror.name !== mirrorName) mirror.name = mirrorName;
236
- const trackerMirrorName = mirror?.name ?? mirrorName;
237
- if (existing) {
238
- existing.mirrorName = trackerMirrorName;
239
- existing.mirrorIsOwned = mirrorIsOwned;
240
- state2.formattedFields.set(fieldName, {
241
- config,
242
- mirrorName: trackerMirrorName,
243
- mirrorIsOwned
244
- });
245
- const current = field.value;
246
- if (current !== "") {
247
- const { formatted, raw } = formatter.format(current, formatType, formatOptions);
248
- applyFormattedValue(field, mirror, formatted, raw);
249
- }
250
- continue;
251
- }
252
- const handler = buildHandler(
253
- field,
254
- mirror,
255
- formatter.format,
256
- formatType,
257
- formatOptions,
258
- trackerMirrorName
259
- );
260
- addListener(state2, state2.element, "input", handler);
261
- bucket.set(fieldName, {
262
- fields: /* @__PURE__ */ new Set([field]),
263
- mirrorName: trackerMirrorName,
264
- 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,
265
280
  mirrorIsOwned
266
281
  });
282
+ } else {
283
+ bucket.set(fieldName, {
284
+ canonicalName: fieldName,
285
+ displayName,
286
+ visible,
287
+ mirror,
288
+ mirrorIsOwned,
289
+ handler: UNBOUND
290
+ });
267
291
  state2.formattedFields.set(fieldName, {
268
292
  config,
269
- mirrorName: trackerMirrorName,
293
+ displayName,
270
294
  mirrorIsOwned
271
295
  });
272
- const initial = field.value;
273
- if (initial !== "") {
274
- const { formatted, raw } = formatter.format(initial, formatType, formatOptions);
275
- applyFormattedValue(field, mirror, formatted, raw);
276
- } else if (mirrorIsOwned && mirror && mirror.value !== "") {
277
- 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);
278
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);
334
+ }
335
+ if (entry.mirrorIsOwned && entry.mirror.isConnected) {
336
+ entry.mirror.remove();
279
337
  }
338
+ bucket.delete(fieldName);
339
+ registry2.delete(fieldName);
280
340
  }
281
341
  };
282
342
  var createFormat = (state2, helpers) => (config) => {
@@ -286,6 +346,14 @@ var createFormat = (state2, helpers) => (config) => {
286
346
  return state2.api;
287
347
  };
288
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
+ };
289
357
  var cleanupFormatRegistry = (state2) => {
290
358
  if (!state2.element) {
291
359
  registry.delete(state2);
@@ -297,10 +365,14 @@ var cleanupFormatRegistry = (state2) => {
297
365
  return;
298
366
  }
299
367
  for (const [fieldName, entry] of bucket) {
300
- state2.element.removeEventListener("input", entry.handler);
301
- if (entry.mirrorIsOwned) {
302
- const mirror = findRawMirror(state2.element, entry.mirrorName);
303
- 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();
304
376
  }
305
377
  bucket.delete(fieldName);
306
378
  state2.formattedFields.delete(fieldName);
@@ -313,7 +385,12 @@ var createDestroy = (state2, _helpers) => () => {
313
385
  if (state2.isDestroyed) return;
314
386
  state2.isDestroyed = true;
315
387
  for (const listener of state2.listeners) {
316
- 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
+ }
317
394
  }
318
395
  state2.listeners.length = 0;
319
396
  if (state2.autoSubmitTimer) {
@@ -761,9 +838,15 @@ var createFormController = (target, options = {}) => {
761
838
  const snapshot = state2.api.getState();
762
839
  state2.subscribers.forEach((listener) => listener(snapshot));
763
840
  };
764
- const addListener2 = (node, type, handler) => {
765
- node.addEventListener(type, handler);
766
- 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);
767
850
  };
768
851
  const clearFieldCache = () => state2.fieldCache.clear();
769
852
  const getFieldsByName = (name) => {
@@ -779,6 +862,12 @@ var createFormController = (target, options = {}) => {
779
862
  for (const field of getNamedFields(state2.element)) {
780
863
  names.add(field.name);
781
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
+ }
782
871
  return Array.from(names);
783
872
  };
784
873
  const getValues = () => {
@@ -864,14 +953,16 @@ var createFormController = (target, options = {}) => {
864
953
  const name = target2.name;
865
954
  if (!name) return;
866
955
  clearFieldCache();
956
+ const canonical = resolveCanonicalForName(state2, name) ?? name;
867
957
  if (normalizedOptions.clearManualErrorsOnChange) {
868
- delete state2.manualErrors[name];
958
+ delete state2.manualErrors[canonical];
869
959
  }
870
- syncVisualState([name]);
871
- if (state2.isValidated && state2.validators[name]) {
872
- validateNames([name]);
960
+ syncVisualState([canonical]);
961
+ if (state2.isValidated && state2.validators[canonical]) {
962
+ validateNames([canonical]);
873
963
  }
874
- emitFieldWatchers(name);
964
+ emitFieldWatchers(canonical);
965
+ if (canonical !== name) emitFieldWatchers(name);
875
966
  notifySubscribers();
876
967
  scheduleAutoSubmit();
877
968
  };