@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/README.md +3 -3
- package/dist/browser/global.d.ts +16 -5
- package/dist/browser/global.global.js +179 -88
- package/dist/browser/global.global.js.map +1 -1
- package/dist/index.cjs +179 -88
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -5
- package/dist/index.d.ts +16 -5
- package/dist/index.js +179 -88
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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(
|
|
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
|
|
110
|
-
const existing =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
130
|
-
|
|
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
|
-
|
|
134
|
-
|
|
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
|
-
|
|
237
|
-
|
|
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 = (
|
|
275
|
+
var buildHandler = (visible, mirror, formatFn, formatType, formatOptions) => {
|
|
240
276
|
const handler = (event) => {
|
|
241
|
-
if (!
|
|
242
|
-
|
|
243
|
-
const
|
|
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
|
-
|
|
247
|
-
if (mirror
|
|
285
|
+
visible.value = "";
|
|
286
|
+
if (mirror.value !== "") mirror.value = "";
|
|
248
287
|
return;
|
|
249
288
|
}
|
|
250
|
-
applyFormattedValue(
|
|
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
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
(f) => f instanceof HTMLInputElement && f.type !== "
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
334
|
+
displayName,
|
|
311
335
|
mirrorIsOwned
|
|
312
336
|
});
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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.
|
|
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
|
-
|
|
807
|
-
|
|
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[
|
|
999
|
+
delete state2.manualErrors[canonical];
|
|
910
1000
|
}
|
|
911
|
-
syncVisualState([
|
|
912
|
-
if (state2.isValidated && state2.validators[
|
|
913
|
-
validateNames([
|
|
1001
|
+
syncVisualState([canonical]);
|
|
1002
|
+
if (state2.isValidated && state2.validators[canonical]) {
|
|
1003
|
+
validateNames([canonical]);
|
|
914
1004
|
}
|
|
915
|
-
emitFieldWatchers(
|
|
1005
|
+
emitFieldWatchers(canonical);
|
|
1006
|
+
if (canonical !== name) emitFieldWatchers(name);
|
|
916
1007
|
notifySubscribers();
|
|
917
1008
|
scheduleAutoSubmit();
|
|
918
1009
|
};
|