@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/README.md +3 -3
- package/dist/browser/global.d.ts +16 -5
- package/dist/browser/global.global.js +164 -87
- package/dist/browser/global.global.js.map +1 -1
- package/dist/index.cjs +164 -87
- 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 +164 -87
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,10 +42,10 @@ Requires Node 20+ when bundling. Runtime target is ES2020.
|
|
|
42
42
|
Use the browser build when you do not have a bundler and need to run the package directly in HTML, Shopify, WordPress, or any traditional template.
|
|
43
43
|
|
|
44
44
|
```html
|
|
45
|
-
<script src="https://unpkg.com/@samline/forms@2.
|
|
45
|
+
<script src="https://unpkg.com/@samline/forms@2.3.1/dist/browser/global.global.js"></script>
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
> Pin the version in production. Replace `2.
|
|
48
|
+
> Pin the version in production. Replace `2.3.1` with the version you ship.
|
|
49
49
|
|
|
50
50
|
The browser bundle exposes a single global: `window.Forms`.
|
|
51
51
|
|
|
@@ -55,7 +55,7 @@ The browser bundle exposes a single global: `window.Forms`.
|
|
|
55
55
|
<button type="submit">Send</button>
|
|
56
56
|
</form>
|
|
57
57
|
|
|
58
|
-
<script src="https://unpkg.com/@samline/forms@2.
|
|
58
|
+
<script src="https://unpkg.com/@samline/forms@2.3.1/dist/browser/global.global.js"></script>
|
|
59
59
|
<script>
|
|
60
60
|
const contactForm = window.Forms.newForm({ id: 'contact-form' })
|
|
61
61
|
|
package/dist/browser/global.d.ts
CHANGED
|
@@ -2,14 +2,25 @@ type FormatType = 'general' | 'phone' | 'numeral' | 'date' | 'time' | 'creditCar
|
|
|
2
2
|
interface FieldFormatConfig {
|
|
3
3
|
/** One of the supported `@samline/formatter` `FormatType` values. */
|
|
4
4
|
type: FormatType;
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Canonical name of the formatted field. The value the backend
|
|
7
|
+
* ultimately receives (the `raw` output of the formatter) is
|
|
8
|
+
* exposed under this name; the formatter is responsible for
|
|
9
|
+
* keeping it in sync with the visible input.
|
|
10
|
+
*/
|
|
6
11
|
field: string | string[];
|
|
7
12
|
/**
|
|
8
|
-
*
|
|
9
|
-
* `${fieldName}
|
|
10
|
-
*
|
|
13
|
+
* Name of the visible input that shows the formatted value to
|
|
14
|
+
* the user. Defaults to `${fieldName}_displayed`.
|
|
15
|
+
*
|
|
16
|
+
* The visible is **renamed** from `field` to `displayField` on
|
|
17
|
+
* the first `format()` call (idempotent on re-binding). A hidden
|
|
18
|
+
* `<input type="hidden" name="<field>">` is created next to it
|
|
19
|
+
* to carry the raw value. Both names are first-class in the
|
|
20
|
+
* controller's API (`getValue`, `getField`, `setValue`, `watch`,
|
|
21
|
+
* `getData`) — see `docs/api/format.md` for the full contract.
|
|
11
22
|
*/
|
|
12
|
-
|
|
23
|
+
displayField?: string;
|
|
13
24
|
/**
|
|
14
25
|
* Format-specific options forwarded to `@samline/formatter`.
|
|
15
26
|
* Any key documented for the chosen `type` is accepted (e.g.
|
|
@@ -4665,33 +4665,45 @@ var Forms = (() => {
|
|
|
4665
4665
|
}
|
|
4666
4666
|
return value.replace(/([!"#$%&'()*+,./:;<=>?@\[\\\]^`{|}~])/g, "\\$1");
|
|
4667
4667
|
};
|
|
4668
|
-
var
|
|
4668
|
+
var findVisibleField = (form2, displayName) => {
|
|
4669
|
+
const node = form2.querySelector(
|
|
4670
|
+
`[name="${cssEscape(displayName)}"]:not([type="hidden"])`
|
|
4671
|
+
);
|
|
4672
|
+
if (node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement) {
|
|
4673
|
+
return node;
|
|
4674
|
+
}
|
|
4675
|
+
return null;
|
|
4676
|
+
};
|
|
4677
|
+
var findHiddenMirror = (form2, fieldName) => {
|
|
4669
4678
|
const byAttr = form2.querySelector(
|
|
4670
4679
|
`[${FORMATTER_RAW_ATTRIBUTE}="${cssEscape(fieldName)}"]`
|
|
4671
4680
|
);
|
|
4672
4681
|
if (byAttr) return byAttr;
|
|
4673
4682
|
const byName = form2.querySelector(
|
|
4674
|
-
`[name="${cssEscape(
|
|
4683
|
+
`input[type="hidden"][name="${cssEscape(fieldName)}"]`
|
|
4675
4684
|
);
|
|
4676
4685
|
if (byName) return byName;
|
|
4677
|
-
const byNameOld = form2.querySelector(
|
|
4678
|
-
`[name="${cssEscape(`${fieldName}Raw`)}"]`
|
|
4679
|
-
);
|
|
4680
|
-
if (byNameOld) return byNameOld;
|
|
4681
4686
|
return null;
|
|
4682
4687
|
};
|
|
4683
|
-
var
|
|
4684
|
-
const existing =
|
|
4688
|
+
var ensureHiddenMirror = (form2, fieldName) => {
|
|
4689
|
+
const existing = findHiddenMirror(form2, fieldName);
|
|
4685
4690
|
if (existing) return existing;
|
|
4686
4691
|
const input = document.createElement("input");
|
|
4687
4692
|
input.type = "hidden";
|
|
4688
|
-
input.name =
|
|
4693
|
+
input.name = fieldName;
|
|
4689
4694
|
input.setAttribute("aria-hidden", "true");
|
|
4690
4695
|
input.tabIndex = -1;
|
|
4691
4696
|
input.setAttribute(FORMATTER_RAW_ATTRIBUTE, fieldName);
|
|
4692
4697
|
form2.appendChild(input);
|
|
4693
4698
|
return input;
|
|
4694
4699
|
};
|
|
4700
|
+
var renameVisibleField = (visible, displayName) => {
|
|
4701
|
+
if (visible.name === displayName) return;
|
|
4702
|
+
visible.name = displayName;
|
|
4703
|
+
};
|
|
4704
|
+
var restoreVisibleName = (visible, originalName) => {
|
|
4705
|
+
visible.name = originalName;
|
|
4706
|
+
};
|
|
4695
4707
|
var applyFormattedValue = (visible, mirror, formatted, raw, inputType) => {
|
|
4696
4708
|
const rawInput = visible.value;
|
|
4697
4709
|
const caret = clamp(
|
|
@@ -4809,6 +4821,7 @@ var Forms = (() => {
|
|
|
4809
4821
|
};
|
|
4810
4822
|
|
|
4811
4823
|
// src/api/format.ts
|
|
4824
|
+
var UNBOUND = null;
|
|
4812
4825
|
var registry = /* @__PURE__ */ new WeakMap();
|
|
4813
4826
|
var getRegistry = (state2) => {
|
|
4814
4827
|
let bucket = registry.get(state2);
|
|
@@ -4819,99 +4832,132 @@ var Forms = (() => {
|
|
|
4819
4832
|
return bucket;
|
|
4820
4833
|
};
|
|
4821
4834
|
var resolveFieldNames = (config) => Array.isArray(config.field) ? config.field : [config.field];
|
|
4822
|
-
var addListener = (state2, target, type, handler) => {
|
|
4823
|
-
|
|
4824
|
-
|
|
4835
|
+
var addListener = (state2, target, type, handler, useCapture) => {
|
|
4836
|
+
if (useCapture) {
|
|
4837
|
+
target.addEventListener(type, handler, { capture: true });
|
|
4838
|
+
} else {
|
|
4839
|
+
target.addEventListener(type, handler);
|
|
4840
|
+
}
|
|
4841
|
+
const entry = { element: target, type, handler };
|
|
4842
|
+
if (useCapture !== void 0) entry.capture = useCapture;
|
|
4843
|
+
state2.listeners.push(entry);
|
|
4825
4844
|
};
|
|
4826
|
-
var
|
|
4845
|
+
var readInputType = (event) => {
|
|
4846
|
+
const candidate = event;
|
|
4847
|
+
return typeof candidate.inputType === "string" ? candidate.inputType : void 0;
|
|
4848
|
+
};
|
|
4849
|
+
var buildHandler = (visible, mirror, formatFn, formatType, formatOptions) => {
|
|
4827
4850
|
const handler = (event) => {
|
|
4828
|
-
if (!
|
|
4829
|
-
|
|
4830
|
-
const
|
|
4851
|
+
if (!visible.isConnected || !mirror.isConnected) return;
|
|
4852
|
+
const isVisible = event.target === visible;
|
|
4853
|
+
const isMirror = event.target === mirror;
|
|
4854
|
+
if (!isVisible && !isMirror) return;
|
|
4855
|
+
const rawInput = isMirror ? mirror.value : visible.value;
|
|
4831
4856
|
const inputType = readInputType(event);
|
|
4832
4857
|
const { formatted, raw } = formatFn(rawInput, formatType, formatOptions);
|
|
4833
4858
|
if (!formatted && !raw) {
|
|
4834
|
-
|
|
4835
|
-
if (mirror
|
|
4859
|
+
visible.value = "";
|
|
4860
|
+
if (mirror.value !== "") mirror.value = "";
|
|
4836
4861
|
return;
|
|
4837
4862
|
}
|
|
4838
|
-
applyFormattedValue(
|
|
4839
|
-
void mirrorFieldName;
|
|
4863
|
+
applyFormattedValue(visible, mirror, formatted, raw, inputType);
|
|
4840
4864
|
};
|
|
4841
4865
|
return handler;
|
|
4842
4866
|
};
|
|
4843
|
-
var readInputType = (event) => {
|
|
4844
|
-
const candidate = event;
|
|
4845
|
-
return typeof candidate.inputType === "string" ? candidate.inputType : void 0;
|
|
4846
|
-
};
|
|
4847
4867
|
var applyFormat = async (state2, helpers, config) => {
|
|
4848
4868
|
if (!state2.element || !state2.api) return;
|
|
4849
|
-
const formatter = await loadFormatter();
|
|
4850
|
-
if (!formatter) return;
|
|
4851
4869
|
const formatType = config.type;
|
|
4852
4870
|
const formatOptions = config.options;
|
|
4853
4871
|
const fieldNames = resolveFieldNames(config);
|
|
4854
4872
|
const bucket = getRegistry(state2);
|
|
4873
|
+
const phase1Entries = [];
|
|
4855
4874
|
for (const fieldName of fieldNames) {
|
|
4856
|
-
const
|
|
4857
|
-
|
|
4858
|
-
const
|
|
4859
|
-
(f2) => f2 instanceof HTMLInputElement && f2.type !== "
|
|
4875
|
+
const displayName = config.displayField ?? `${fieldName}_displayed`;
|
|
4876
|
+
const canonicalFields = helpers.getFieldsByName(fieldName);
|
|
4877
|
+
const candidates = canonicalFields.filter(
|
|
4878
|
+
(f2) => f2 instanceof HTMLInputElement && f2.type !== "hidden" || f2 instanceof HTMLTextAreaElement
|
|
4860
4879
|
);
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
});
|
|
4878
|
-
const current = field.value;
|
|
4879
|
-
if (current !== "") {
|
|
4880
|
-
const { formatted, raw } = formatter.format(current, formatType, formatOptions);
|
|
4881
|
-
applyFormattedValue(field, mirror, formatted, raw);
|
|
4882
|
-
}
|
|
4883
|
-
continue;
|
|
4884
|
-
}
|
|
4885
|
-
const handler = buildHandler(
|
|
4886
|
-
field,
|
|
4887
|
-
mirror,
|
|
4888
|
-
formatter.format,
|
|
4889
|
-
formatType,
|
|
4890
|
-
formatOptions,
|
|
4891
|
-
trackerMirrorName
|
|
4892
|
-
);
|
|
4893
|
-
addListener(state2, state2.element, "input", handler);
|
|
4894
|
-
bucket.set(fieldName, {
|
|
4895
|
-
fields: /* @__PURE__ */ new Set([field]),
|
|
4896
|
-
mirrorName: trackerMirrorName,
|
|
4897
|
-
handler,
|
|
4880
|
+
let visible = candidates[0] ?? null;
|
|
4881
|
+
if (!visible) {
|
|
4882
|
+
visible = findVisibleField(state2.element, displayName);
|
|
4883
|
+
}
|
|
4884
|
+
if (!visible) continue;
|
|
4885
|
+
const existing = bucket.get(fieldName);
|
|
4886
|
+
let mirror = findHiddenMirror(state2.element, fieldName);
|
|
4887
|
+
const mirrorIsOwned = !mirror;
|
|
4888
|
+
if (!mirror) mirror = ensureHiddenMirror(state2.element, fieldName);
|
|
4889
|
+
renameVisibleField(visible, displayName);
|
|
4890
|
+
if (existing) {
|
|
4891
|
+
existing.displayName = displayName;
|
|
4892
|
+
existing.mirrorIsOwned = mirrorIsOwned;
|
|
4893
|
+
state2.formattedFields.set(fieldName, {
|
|
4894
|
+
config,
|
|
4895
|
+
displayName,
|
|
4898
4896
|
mirrorIsOwned
|
|
4899
4897
|
});
|
|
4898
|
+
phase1Entries.push(existing);
|
|
4899
|
+
} else {
|
|
4900
|
+
const entry = {
|
|
4901
|
+
canonicalName: fieldName,
|
|
4902
|
+
displayName,
|
|
4903
|
+
visible,
|
|
4904
|
+
mirror,
|
|
4905
|
+
mirrorIsOwned,
|
|
4906
|
+
handler: UNBOUND
|
|
4907
|
+
};
|
|
4908
|
+
bucket.set(fieldName, entry);
|
|
4900
4909
|
state2.formattedFields.set(fieldName, {
|
|
4901
4910
|
config,
|
|
4902
|
-
|
|
4911
|
+
displayName,
|
|
4903
4912
|
mirrorIsOwned
|
|
4904
4913
|
});
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4914
|
+
phase1Entries.push(entry);
|
|
4915
|
+
}
|
|
4916
|
+
}
|
|
4917
|
+
const formatter = await loadFormatter();
|
|
4918
|
+
if (!formatter) {
|
|
4919
|
+
rollbackPhase1(phase1Entries, bucket, state2.formattedFields);
|
|
4920
|
+
return;
|
|
4921
|
+
}
|
|
4922
|
+
for (const entry of phase1Entries) {
|
|
4923
|
+
if (entry.handler !== UNBOUND) {
|
|
4924
|
+
const current = entry.visible.value;
|
|
4925
|
+
if (current !== "") {
|
|
4926
|
+
const { formatted, raw } = formatter.format(current, formatType, formatOptions);
|
|
4927
|
+
applyFormattedValue(entry.visible, entry.mirror, formatted, raw);
|
|
4911
4928
|
}
|
|
4929
|
+
continue;
|
|
4930
|
+
}
|
|
4931
|
+
const handler = buildHandler(
|
|
4932
|
+
entry.visible,
|
|
4933
|
+
entry.mirror,
|
|
4934
|
+
formatter.format,
|
|
4935
|
+
formatType,
|
|
4936
|
+
formatOptions
|
|
4937
|
+
);
|
|
4938
|
+
addListener(state2, state2.element, "input", handler, true);
|
|
4939
|
+
entry.handler = handler;
|
|
4940
|
+
const initial = entry.visible.value;
|
|
4941
|
+
if (initial !== "") {
|
|
4942
|
+
const { formatted, raw } = formatter.format(initial, formatType, formatOptions);
|
|
4943
|
+
applyFormattedValue(entry.visible, entry.mirror, formatted, raw);
|
|
4944
|
+
} else if (entry.mirrorIsOwned && entry.mirror.value !== "") {
|
|
4945
|
+
entry.mirror.value = "";
|
|
4912
4946
|
}
|
|
4913
4947
|
}
|
|
4914
4948
|
};
|
|
4949
|
+
var rollbackPhase1 = (entries, bucket, registry2) => {
|
|
4950
|
+
for (const entry of entries) {
|
|
4951
|
+
if (entry.visible.isConnected) {
|
|
4952
|
+
restoreVisibleName(entry.visible, entry.canonicalName);
|
|
4953
|
+
}
|
|
4954
|
+
if (entry.mirrorIsOwned && entry.mirror.isConnected) {
|
|
4955
|
+
entry.mirror.remove();
|
|
4956
|
+
}
|
|
4957
|
+
bucket.delete(entry.canonicalName);
|
|
4958
|
+
registry2.delete(entry.canonicalName);
|
|
4959
|
+
}
|
|
4960
|
+
};
|
|
4915
4961
|
var createFormat = (state2, helpers) => (config) => {
|
|
4916
4962
|
if (state2.isDestroyed) return state2.api;
|
|
4917
4963
|
if (!state2.element) return state2.api;
|
|
@@ -4919,6 +4965,14 @@ var Forms = (() => {
|
|
|
4919
4965
|
return state2.api;
|
|
4920
4966
|
};
|
|
4921
4967
|
var createFormatAll = createFormat;
|
|
4968
|
+
var resolveCanonicalForName = (state2, name) => {
|
|
4969
|
+
const bucket = registry.get(state2);
|
|
4970
|
+
if (!bucket) return null;
|
|
4971
|
+
for (const [canonical, entry] of bucket) {
|
|
4972
|
+
if (canonical === name || entry.displayName === name) return canonical;
|
|
4973
|
+
}
|
|
4974
|
+
return null;
|
|
4975
|
+
};
|
|
4922
4976
|
var cleanupFormatRegistry = (state2) => {
|
|
4923
4977
|
if (!state2.element) {
|
|
4924
4978
|
registry.delete(state2);
|
|
@@ -4930,10 +4984,14 @@ var Forms = (() => {
|
|
|
4930
4984
|
return;
|
|
4931
4985
|
}
|
|
4932
4986
|
for (const [fieldName, entry] of bucket) {
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4987
|
+
if (entry.handler !== UNBOUND) {
|
|
4988
|
+
state2.element.removeEventListener("input", entry.handler, true);
|
|
4989
|
+
}
|
|
4990
|
+
if (entry.visible.isConnected) {
|
|
4991
|
+
restoreVisibleName(entry.visible, fieldName);
|
|
4992
|
+
}
|
|
4993
|
+
if (entry.mirrorIsOwned && entry.mirror.isConnected) {
|
|
4994
|
+
entry.mirror.remove();
|
|
4937
4995
|
}
|
|
4938
4996
|
bucket.delete(fieldName);
|
|
4939
4997
|
state2.formattedFields.delete(fieldName);
|
|
@@ -4946,7 +5004,12 @@ var Forms = (() => {
|
|
|
4946
5004
|
if (state2.isDestroyed) return;
|
|
4947
5005
|
state2.isDestroyed = true;
|
|
4948
5006
|
for (const listener of state2.listeners) {
|
|
4949
|
-
listener.
|
|
5007
|
+
const options = listener.capture ? { capture: true } : void 0;
|
|
5008
|
+
if (options) {
|
|
5009
|
+
listener.element.removeEventListener(listener.type, listener.handler, options);
|
|
5010
|
+
} else {
|
|
5011
|
+
listener.element.removeEventListener(listener.type, listener.handler);
|
|
5012
|
+
}
|
|
4950
5013
|
}
|
|
4951
5014
|
state2.listeners.length = 0;
|
|
4952
5015
|
if (state2.autoSubmitTimer) {
|
|
@@ -5383,9 +5446,15 @@ var Forms = (() => {
|
|
|
5383
5446
|
const snapshot = state2.api.getState();
|
|
5384
5447
|
state2.subscribers.forEach((listener) => listener(snapshot));
|
|
5385
5448
|
};
|
|
5386
|
-
const addListener2 = (node, type, handler) => {
|
|
5387
|
-
|
|
5388
|
-
|
|
5449
|
+
const addListener2 = (node, type, handler, capture) => {
|
|
5450
|
+
if (capture) {
|
|
5451
|
+
node.addEventListener(type, handler, { capture: true });
|
|
5452
|
+
} else {
|
|
5453
|
+
node.addEventListener(type, handler);
|
|
5454
|
+
}
|
|
5455
|
+
const entry = { element: node, type, handler };
|
|
5456
|
+
if (capture !== void 0) entry.capture = capture;
|
|
5457
|
+
state2.listeners.push(entry);
|
|
5389
5458
|
};
|
|
5390
5459
|
const clearFieldCache = () => state2.fieldCache.clear();
|
|
5391
5460
|
const getFieldsByName = (name) => {
|
|
@@ -5401,6 +5470,12 @@ var Forms = (() => {
|
|
|
5401
5470
|
for (const field of getNamedFields(state2.element)) {
|
|
5402
5471
|
names.add(field.name);
|
|
5403
5472
|
}
|
|
5473
|
+
for (const [canonical, entry] of state2.formattedFields) {
|
|
5474
|
+
names.add(canonical);
|
|
5475
|
+
if (entry.displayName && entry.displayName !== canonical) {
|
|
5476
|
+
names.add(entry.displayName);
|
|
5477
|
+
}
|
|
5478
|
+
}
|
|
5404
5479
|
return Array.from(names);
|
|
5405
5480
|
};
|
|
5406
5481
|
const getValues = () => {
|
|
@@ -5486,14 +5561,16 @@ var Forms = (() => {
|
|
|
5486
5561
|
const name = target2.name;
|
|
5487
5562
|
if (!name) return;
|
|
5488
5563
|
clearFieldCache();
|
|
5564
|
+
const canonical = resolveCanonicalForName(state2, name) ?? name;
|
|
5489
5565
|
if (normalizedOptions.clearManualErrorsOnChange) {
|
|
5490
|
-
delete state2.manualErrors[
|
|
5566
|
+
delete state2.manualErrors[canonical];
|
|
5491
5567
|
}
|
|
5492
|
-
syncVisualState([
|
|
5493
|
-
if (state2.isValidated && state2.validators[
|
|
5494
|
-
validateNames([
|
|
5568
|
+
syncVisualState([canonical]);
|
|
5569
|
+
if (state2.isValidated && state2.validators[canonical]) {
|
|
5570
|
+
validateNames([canonical]);
|
|
5495
5571
|
}
|
|
5496
|
-
emitFieldWatchers(
|
|
5572
|
+
emitFieldWatchers(canonical);
|
|
5573
|
+
if (canonical !== name) emitFieldWatchers(name);
|
|
5497
5574
|
notifySubscribers();
|
|
5498
5575
|
scheduleAutoSubmit();
|
|
5499
5576
|
};
|