@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.cjs CHANGED
@@ -91,50 +91,75 @@ 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
  };
121
- var applyFormattedValue = (visible, mirror, formatted, raw) => {
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
+ };
133
+ var applyFormattedValue = (visible, mirror, formatted, raw, inputType) => {
122
134
  const rawInput = visible.value;
123
135
  const caret = clamp(
124
136
  visible.selectionStart ?? rawInput.length,
125
137
  0,
126
138
  rawInput.length
127
139
  );
140
+ if (mirror && mirror.value !== raw) mirror.value = raw;
128
141
  if (rawInput === formatted) {
129
- if (mirror && mirror.value !== raw) mirror.value = raw;
130
- return caret;
142
+ const newCaret2 = pullCaretPastLeadingDelimiter(caret, formatted, inputType);
143
+ if (newCaret2 !== caret) restoreCursor(visible, newCaret2);
144
+ return newCaret2;
131
145
  }
132
146
  visible.value = formatted;
133
- if (mirror) mirror.value = raw;
134
- const newCaret = computeCursorPosition(caret, rawInput, formatted);
147
+ const newCaret = pullCaretPastLeadingDelimiter(
148
+ computeCursorPosition(caret, rawInput, formatted),
149
+ formatted,
150
+ inputType
151
+ );
135
152
  restoreCursor(visible, newCaret);
136
153
  return newCaret;
137
154
  };
155
+ var pullCaretPastLeadingDelimiter = (caret, value, inputType) => {
156
+ if (!inputType || !inputType.startsWith("delete")) return caret;
157
+ let cursor = caret;
158
+ while (cursor > 0 && isFormatChar(value[cursor - 1])) {
159
+ cursor -= 1;
160
+ }
161
+ return cursor;
162
+ };
138
163
  var clamp = (value, min, max) => Math.max(min, Math.min(max, value));
139
164
  var computeCursorPosition = (caretPos, rawInput, newValue) => {
140
165
  if (newValue.length === 0) return 0;
@@ -222,6 +247,7 @@ var loadFormatter = () => {
222
247
  };
223
248
 
224
249
  // src/api/format.ts
250
+ var UNBOUND = null;
225
251
  var registry = /* @__PURE__ */ new WeakMap();
226
252
  var getRegistry = (state2) => {
227
253
  let bucket = registry.get(state2);
@@ -232,92 +258,126 @@ var getRegistry = (state2) => {
232
258
  return bucket;
233
259
  };
234
260
  var resolveFieldNames = (config) => Array.isArray(config.field) ? config.field : [config.field];
235
- var addListener = (state2, target, type, handler) => {
236
- target.addEventListener(type, handler);
237
- 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;
238
274
  };
239
- var buildHandler = (field, mirror, formatFn, formatType, formatOptions, mirrorFieldName) => {
275
+ var buildHandler = (visible, mirror, formatFn, formatType, formatOptions) => {
240
276
  const handler = (event) => {
241
- if (!field.isConnected) return;
242
- if (event.target !== field) return;
243
- 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;
282
+ const inputType = readInputType(event);
244
283
  const { formatted, raw } = formatFn(rawInput, formatType, formatOptions);
245
284
  if (!formatted && !raw) {
246
- field.value = "";
247
- if (mirror && mirror.value !== "") mirror.value = "";
285
+ visible.value = "";
286
+ if (mirror.value !== "") mirror.value = "";
248
287
  return;
249
288
  }
250
- applyFormattedValue(field, mirror, formatted, raw);
251
- void mirrorFieldName;
289
+ applyFormattedValue(visible, mirror, formatted, raw, inputType);
252
290
  };
253
291
  return handler;
254
292
  };
255
293
  var applyFormat = async (state2, helpers, config) => {
256
294
  if (!state2.element || !state2.api) return;
257
- const formatter = await loadFormatter();
258
- if (!formatter) return;
259
295
  const formatType = config.type;
260
296
  const formatOptions = config.options;
261
297
  const fieldNames = resolveFieldNames(config);
262
298
  const bucket = getRegistry(state2);
263
299
  for (const fieldName of fieldNames) {
264
- const fields = helpers.getFieldsByName(fieldName);
265
- if (fields.length === 0) continue;
266
- const writable = fields.filter(
267
- (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
268
304
  );
269
- if (writable.length === 0) continue;
270
- for (const field of writable) {
271
- const existing = bucket.get(fieldName);
272
- const mirrorName = config.rawField ?? `${fieldName}_raw`;
273
- let mirror = findRawMirror(state2.element, fieldName) ?? null;
274
- const mirrorIsOwned = !mirror;
275
- if (!mirror) mirror = ensureRawMirror(state2.element, fieldName);
276
- if (mirror && mirror.name !== mirrorName) mirror.name = mirrorName;
277
- const trackerMirrorName = mirror?.name ?? mirrorName;
278
- if (existing) {
279
- existing.mirrorName = trackerMirrorName;
280
- existing.mirrorIsOwned = mirrorIsOwned;
281
- state2.formattedFields.set(fieldName, {
282
- config,
283
- mirrorName: trackerMirrorName,
284
- mirrorIsOwned
285
- });
286
- const current = field.value;
287
- if (current !== "") {
288
- const { formatted, raw } = formatter.format(current, formatType, formatOptions);
289
- applyFormattedValue(field, mirror, formatted, raw);
290
- }
291
- continue;
292
- }
293
- const handler = buildHandler(
294
- field,
295
- mirror,
296
- formatter.format,
297
- formatType,
298
- formatOptions,
299
- trackerMirrorName
300
- );
301
- addListener(state2, state2.element, "input", handler);
302
- bucket.set(fieldName, {
303
- fields: /* @__PURE__ */ new Set([field]),
304
- mirrorName: trackerMirrorName,
305
- 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,
306
321
  mirrorIsOwned
307
322
  });
323
+ } else {
324
+ bucket.set(fieldName, {
325
+ canonicalName: fieldName,
326
+ displayName,
327
+ visible,
328
+ mirror,
329
+ mirrorIsOwned,
330
+ handler: UNBOUND
331
+ });
308
332
  state2.formattedFields.set(fieldName, {
309
333
  config,
310
- mirrorName: trackerMirrorName,
334
+ displayName,
311
335
  mirrorIsOwned
312
336
  });
313
- const initial = field.value;
314
- if (initial !== "") {
315
- const { formatted, raw } = formatter.format(initial, formatType, formatOptions);
316
- applyFormattedValue(field, mirror, formatted, raw);
317
- } else if (mirrorIsOwned && mirror && mirror.value !== "") {
318
- 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);
319
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);
375
+ }
376
+ if (entry.mirrorIsOwned && entry.mirror.isConnected) {
377
+ entry.mirror.remove();
320
378
  }
379
+ bucket.delete(fieldName);
380
+ registry2.delete(fieldName);
321
381
  }
322
382
  };
323
383
  var createFormat = (state2, helpers) => (config) => {
@@ -327,6 +387,14 @@ var createFormat = (state2, helpers) => (config) => {
327
387
  return state2.api;
328
388
  };
329
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
+ };
330
398
  var cleanupFormatRegistry = (state2) => {
331
399
  if (!state2.element) {
332
400
  registry.delete(state2);
@@ -338,10 +406,14 @@ var cleanupFormatRegistry = (state2) => {
338
406
  return;
339
407
  }
340
408
  for (const [fieldName, entry] of bucket) {
341
- state2.element.removeEventListener("input", entry.handler);
342
- if (entry.mirrorIsOwned) {
343
- const mirror = findRawMirror(state2.element, entry.mirrorName);
344
- 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();
345
417
  }
346
418
  bucket.delete(fieldName);
347
419
  state2.formattedFields.delete(fieldName);
@@ -354,7 +426,12 @@ var createDestroy = (state2, _helpers) => () => {
354
426
  if (state2.isDestroyed) return;
355
427
  state2.isDestroyed = true;
356
428
  for (const listener of state2.listeners) {
357
- 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
+ }
358
435
  }
359
436
  state2.listeners.length = 0;
360
437
  if (state2.autoSubmitTimer) {
@@ -802,9 +879,15 @@ var createFormController = (target, options = {}) => {
802
879
  const snapshot = state2.api.getState();
803
880
  state2.subscribers.forEach((listener) => listener(snapshot));
804
881
  };
805
- const addListener2 = (node, type, handler) => {
806
- node.addEventListener(type, handler);
807
- 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);
808
891
  };
809
892
  const clearFieldCache = () => state2.fieldCache.clear();
810
893
  const getFieldsByName = (name) => {
@@ -820,6 +903,12 @@ var createFormController = (target, options = {}) => {
820
903
  for (const field of getNamedFields(state2.element)) {
821
904
  names.add(field.name);
822
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
+ }
823
912
  return Array.from(names);
824
913
  };
825
914
  const getValues = () => {
@@ -905,14 +994,16 @@ var createFormController = (target, options = {}) => {
905
994
  const name = target2.name;
906
995
  if (!name) return;
907
996
  clearFieldCache();
997
+ const canonical = resolveCanonicalForName(state2, name) ?? name;
908
998
  if (normalizedOptions.clearManualErrorsOnChange) {
909
- delete state2.manualErrors[name];
999
+ delete state2.manualErrors[canonical];
910
1000
  }
911
- syncVisualState([name]);
912
- if (state2.isValidated && state2.validators[name]) {
913
- validateNames([name]);
1001
+ syncVisualState([canonical]);
1002
+ if (state2.isValidated && state2.validators[canonical]) {
1003
+ validateNames([canonical]);
914
1004
  }
915
- emitFieldWatchers(name);
1005
+ emitFieldWatchers(canonical);
1006
+ if (canonical !== name) emitFieldWatchers(name);
916
1007
  notifySubscribers();
917
1008
  scheduleAutoSubmit();
918
1009
  };