@vectoriox/iox-ui 5.82.1 → 5.83.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/design-core/index.d.ts +200 -0
- package/fesm2022/vectoriox-iox-ui-design-core.mjs +215 -1
- package/fesm2022/vectoriox-iox-ui-design-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-forms-core.mjs +733 -56
- package/fesm2022/vectoriox-iox-ui-forms-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-generation-core.mjs +40 -4
- package/fesm2022/vectoriox-iox-ui-generation-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui.mjs +1547 -135
- package/fesm2022/vectoriox-iox-ui.mjs.map +1 -1
- package/forms-core/index.d.ts +366 -18
- package/generation-core/index.d.ts +3 -0
- package/index.d.ts +303 -25
- package/package.json +1 -1
- package/src/design-core/contrast-audit.ts +4 -0
- package/src/design-core/flavor-spec.generated.ts +209 -1
- package/src/forms-core/channel.ts +85 -0
- package/src/forms-core/collect.ts +63 -30
- package/src/forms-core/dates.ts +299 -0
- package/src/forms-core/field-markup.ts +159 -0
- package/src/forms-core/form-presets.ts +51 -2
- package/src/forms-core/form.model.ts +34 -2
- package/src/forms-core/messages.ts +31 -0
- package/src/forms-core/public-api.ts +3 -0
- package/src/forms-core/rule-catalog.ts +3 -2
- package/src/forms-core/submit-lifecycle.ts +42 -12
- package/src/forms-core/validate.ts +90 -4
- package/src/generation-core/audit-site.ts +9 -1
- package/src/generation-core/compose-site.ts +22 -3
- package/src/generation-core/node-baselines.generated.ts +10 -0
- package/src/generation-core/plan-wire.ts +2 -2
- package/styles/iox-design.css +14 -2
- package/styles/iox-forms.css +349 -10
|
@@ -3402,7 +3402,23 @@ export const SPEC_ELEMENT_STYLES = [
|
|
|
3402
3402
|
"padding": "var(--iox-badge-pad)",
|
|
3403
3403
|
"fontFamily": "var(--iox-font-label)",
|
|
3404
3404
|
"fontSize": "var(--iox-badge-size)",
|
|
3405
|
-
"fontWeight": "var(--iox-weight-label)"
|
|
3405
|
+
"fontWeight": "var(--iox-weight-label)",
|
|
3406
|
+
"transitionDuration": "var(--iox-dur-fast)",
|
|
3407
|
+
"transitionTimingFunction": "var(--iox-ease-standard)"
|
|
3408
|
+
},
|
|
3409
|
+
"hover": {
|
|
3410
|
+
"borderColor": "var(--iox-text)"
|
|
3411
|
+
},
|
|
3412
|
+
"selected": {
|
|
3413
|
+
"backgroundColor": "var(--iox-accent-text)",
|
|
3414
|
+
"color": "var(--iox-bg)",
|
|
3415
|
+
"borderColor": "var(--iox-accent-text)"
|
|
3416
|
+
},
|
|
3417
|
+
"focus": {
|
|
3418
|
+
"boxShadow": "var(--iox-focus-ring)"
|
|
3419
|
+
},
|
|
3420
|
+
"disabled": {
|
|
3421
|
+
"opacity": "0.55"
|
|
3406
3422
|
}
|
|
3407
3423
|
},
|
|
3408
3424
|
{
|
|
@@ -3434,6 +3450,56 @@ export const SPEC_ELEMENT_STYLES = [
|
|
|
3434
3450
|
"color": "var(--iox-media-scrim-ink)"
|
|
3435
3451
|
}
|
|
3436
3452
|
},
|
|
3453
|
+
{
|
|
3454
|
+
"id": "calendar",
|
|
3455
|
+
"label": "Calendar",
|
|
3456
|
+
"group": "Forms",
|
|
3457
|
+
"appliesTo": [],
|
|
3458
|
+
"base": {
|
|
3459
|
+
"backgroundColor": "var(--iox-surface)",
|
|
3460
|
+
"color": "var(--iox-text)",
|
|
3461
|
+
"borderRadius": "var(--iox-dialog-radius)",
|
|
3462
|
+
"boxShadow": "var(--iox-dialog-shadow)",
|
|
3463
|
+
"fontFamily": "var(--iox-font-body)"
|
|
3464
|
+
}
|
|
3465
|
+
},
|
|
3466
|
+
{
|
|
3467
|
+
"id": "calendar-day",
|
|
3468
|
+
"label": "Calendar day",
|
|
3469
|
+
"group": "Forms",
|
|
3470
|
+
"appliesTo": [],
|
|
3471
|
+
"base": {
|
|
3472
|
+
"backgroundColor": "transparent",
|
|
3473
|
+
"color": "var(--iox-text)",
|
|
3474
|
+
"borderStyle": "solid",
|
|
3475
|
+
"borderWidth": "var(--iox-border-width)",
|
|
3476
|
+
"borderColor": "transparent",
|
|
3477
|
+
"borderRadius": "var(--iox-radius-pill)",
|
|
3478
|
+
"fontFamily": "var(--iox-font-body)",
|
|
3479
|
+
"fontSize": "var(--iox-size-body)",
|
|
3480
|
+
"fontVariantNumeric": "tabular-nums",
|
|
3481
|
+
"transitionDuration": "var(--iox-dur-fast)",
|
|
3482
|
+
"transitionTimingFunction": "var(--iox-ease-standard)"
|
|
3483
|
+
},
|
|
3484
|
+
"hover": {
|
|
3485
|
+
"backgroundColor": "var(--iox-surface-2)"
|
|
3486
|
+
},
|
|
3487
|
+
"current": {
|
|
3488
|
+
"borderColor": "var(--iox-line-strong)"
|
|
3489
|
+
},
|
|
3490
|
+
"selected": {
|
|
3491
|
+
"backgroundColor": "var(--iox-accent-text)",
|
|
3492
|
+
"color": "var(--iox-bg)",
|
|
3493
|
+
"borderColor": "var(--iox-accent-text)"
|
|
3494
|
+
},
|
|
3495
|
+
"focus": {
|
|
3496
|
+
"boxShadow": "var(--iox-focus-ring)"
|
|
3497
|
+
},
|
|
3498
|
+
"disabled": {
|
|
3499
|
+
"color": "var(--iox-text-muted)",
|
|
3500
|
+
"opacity": "0.4"
|
|
3501
|
+
}
|
|
3502
|
+
},
|
|
3437
3503
|
{
|
|
3438
3504
|
"id": "notice-success",
|
|
3439
3505
|
"label": "Success message",
|
|
@@ -3697,6 +3763,112 @@ export const SPEC_FIELD_CLASS_BASELINE = {
|
|
|
3697
3763
|
},
|
|
3698
3764
|
".iox-es-field .iox-field__control--switch:checked": {
|
|
3699
3765
|
"background": "var(--iox-accent-text)"
|
|
3766
|
+
},
|
|
3767
|
+
".iox-es-field .iox-field__chip": {
|
|
3768
|
+
"background-color": "transparent",
|
|
3769
|
+
"color": "var(--iox-text)",
|
|
3770
|
+
"border-style": "solid",
|
|
3771
|
+
"border-width": "var(--iox-border-width)",
|
|
3772
|
+
"border-color": "var(--iox-chip-border)",
|
|
3773
|
+
"border-radius": "var(--iox-badge-radius)",
|
|
3774
|
+
"padding": "var(--iox-badge-pad)",
|
|
3775
|
+
"font-family": "var(--iox-font-label)",
|
|
3776
|
+
"font-size": "var(--iox-badge-size)",
|
|
3777
|
+
"font-weight": "var(--iox-weight-label)",
|
|
3778
|
+
"transition": "border-color var(--iox-dur-fast) var(--iox-ease-standard), background-color var(--iox-dur-fast) var(--iox-ease-standard), color var(--iox-dur-fast) var(--iox-ease-standard), box-shadow var(--iox-dur-fast) var(--iox-ease-standard), opacity var(--iox-dur-fast) var(--iox-ease-standard), transform var(--iox-dur-fast) var(--iox-ease-standard)"
|
|
3779
|
+
},
|
|
3780
|
+
".iox-es-field .iox-field__chip:active:not(:has(:disabled))": {
|
|
3781
|
+
"transform": "scale(var(--iox-press-scale))"
|
|
3782
|
+
},
|
|
3783
|
+
".iox-es-field .iox-field__chip:hover:not(:has(:disabled))": {
|
|
3784
|
+
"border-color": "var(--iox-text)"
|
|
3785
|
+
},
|
|
3786
|
+
".iox-es-field .iox-field__chip:has(:checked)": {
|
|
3787
|
+
"background-color": "var(--iox-accent-text)",
|
|
3788
|
+
"color": "var(--iox-bg)",
|
|
3789
|
+
"border-color": "var(--iox-accent-text)"
|
|
3790
|
+
},
|
|
3791
|
+
".iox-es-field .iox-field__chip:has(:focus-visible)": {
|
|
3792
|
+
"box-shadow": "var(--iox-focus-ring)",
|
|
3793
|
+
"outline": "2px solid transparent",
|
|
3794
|
+
"outline-offset": "2px"
|
|
3795
|
+
},
|
|
3796
|
+
".iox-es-field .iox-field__chip:has(:disabled)": {
|
|
3797
|
+
"opacity": "0.55"
|
|
3798
|
+
},
|
|
3799
|
+
".iox-es-field .iox-field__date-value[data-iox-empty]": {
|
|
3800
|
+
"color": "var(--iox-text-muted)"
|
|
3801
|
+
},
|
|
3802
|
+
".iox-es-field .iox-field__date-icon": {
|
|
3803
|
+
"color": "var(--iox-text-muted)"
|
|
3804
|
+
},
|
|
3805
|
+
".iox-es-field .iox-field__control--date[aria-expanded='true']": {
|
|
3806
|
+
"border-color": "var(--iox-accent-text)",
|
|
3807
|
+
"box-shadow": "var(--iox-field-focus)"
|
|
3808
|
+
},
|
|
3809
|
+
".iox-es-field .iox-cal": {
|
|
3810
|
+
"background-color": "var(--iox-surface)",
|
|
3811
|
+
"color": "var(--iox-text)",
|
|
3812
|
+
"border-radius": "var(--iox-dialog-radius)",
|
|
3813
|
+
"box-shadow": "var(--iox-dialog-shadow), 0 0 0 var(--iox-border-width) var(--iox-line)",
|
|
3814
|
+
"font-family": "var(--iox-font-body)"
|
|
3815
|
+
},
|
|
3816
|
+
".iox-es-field .iox-cal .iox-cal__title": {
|
|
3817
|
+
"font-family": "var(--iox-font-display)",
|
|
3818
|
+
"font-weight": "var(--iox-weight-heading)",
|
|
3819
|
+
"color": "var(--iox-text)"
|
|
3820
|
+
},
|
|
3821
|
+
".iox-es-field .iox-cal .iox-cal__weekday": {
|
|
3822
|
+
"font-family": "var(--iox-font-label)",
|
|
3823
|
+
"font-size": "var(--iox-size-label)",
|
|
3824
|
+
"font-weight": "var(--iox-weight-label)",
|
|
3825
|
+
"color": "var(--iox-text-muted)"
|
|
3826
|
+
},
|
|
3827
|
+
".iox-es-field .iox-cal .iox-cal__grab": {
|
|
3828
|
+
"background": "var(--iox-line-strong)"
|
|
3829
|
+
},
|
|
3830
|
+
".iox-es-field .iox-cal .iox-cal__nav button:hover:not(:disabled)": {
|
|
3831
|
+
"background-color": "var(--iox-surface-2)"
|
|
3832
|
+
},
|
|
3833
|
+
".iox-es-field .iox-cal .iox-cal__nav button:focus-visible": {
|
|
3834
|
+
"box-shadow": "var(--iox-focus-ring)",
|
|
3835
|
+
"outline": "2px solid transparent",
|
|
3836
|
+
"outline-offset": "2px"
|
|
3837
|
+
},
|
|
3838
|
+
".iox-es-field .iox-cal .iox-cal__day": {
|
|
3839
|
+
"background-color": "transparent",
|
|
3840
|
+
"color": "var(--iox-text)",
|
|
3841
|
+
"border-style": "solid",
|
|
3842
|
+
"border-width": "var(--iox-border-width)",
|
|
3843
|
+
"border-color": "transparent",
|
|
3844
|
+
"border-radius": "var(--iox-radius-pill)",
|
|
3845
|
+
"font-family": "var(--iox-font-body)",
|
|
3846
|
+
"font-size": "var(--iox-size-body)",
|
|
3847
|
+
"font-variant-numeric": "tabular-nums",
|
|
3848
|
+
"transition": "background-color var(--iox-dur-fast) var(--iox-ease-standard), color var(--iox-dur-fast) var(--iox-ease-standard), border-color var(--iox-dur-fast) var(--iox-ease-standard), box-shadow var(--iox-dur-fast) var(--iox-ease-standard), opacity var(--iox-dur-fast) var(--iox-ease-standard), transform var(--iox-dur-fast) var(--iox-ease-standard)"
|
|
3849
|
+
},
|
|
3850
|
+
".iox-es-field .iox-cal .iox-cal__day:active:not([aria-disabled='true'])": {
|
|
3851
|
+
"transform": "scale(var(--iox-press-scale))"
|
|
3852
|
+
},
|
|
3853
|
+
".iox-es-field .iox-cal .iox-cal__day:hover:not([aria-disabled='true'])": {
|
|
3854
|
+
"background-color": "var(--iox-surface-2)"
|
|
3855
|
+
},
|
|
3856
|
+
".iox-es-field .iox-cal .iox-cal__day[aria-current='date']": {
|
|
3857
|
+
"border-color": "var(--iox-line-strong)"
|
|
3858
|
+
},
|
|
3859
|
+
".iox-es-field .iox-cal .iox-cal__cell[aria-selected='true'] > .iox-cal__day": {
|
|
3860
|
+
"background-color": "var(--iox-accent-text)",
|
|
3861
|
+
"color": "var(--iox-bg)",
|
|
3862
|
+
"border-color": "var(--iox-accent-text)"
|
|
3863
|
+
},
|
|
3864
|
+
".iox-es-field .iox-cal .iox-cal__day[aria-disabled='true']": {
|
|
3865
|
+
"color": "var(--iox-text-muted)",
|
|
3866
|
+
"opacity": "0.4"
|
|
3867
|
+
},
|
|
3868
|
+
".iox-es-field .iox-cal .iox-cal__day:focus-visible": {
|
|
3869
|
+
"box-shadow": "var(--iox-focus-ring)",
|
|
3870
|
+
"outline": "2px solid transparent",
|
|
3871
|
+
"outline-offset": "2px"
|
|
3700
3872
|
}
|
|
3701
3873
|
} as const;
|
|
3702
3874
|
|
|
@@ -3943,6 +4115,42 @@ export const SPEC_CONTRAST_AUDIT = {
|
|
|
3943
4115
|
"on": "var(--iox-surface)",
|
|
3944
4116
|
"min": 3
|
|
3945
4117
|
},
|
|
4118
|
+
{
|
|
4119
|
+
"part": "calendar text",
|
|
4120
|
+
"fg": "var(--iox-text)",
|
|
4121
|
+
"on": "var(--iox-surface)",
|
|
4122
|
+
"min": 4.5
|
|
4123
|
+
},
|
|
4124
|
+
{
|
|
4125
|
+
"part": "calendar weekday",
|
|
4126
|
+
"fg": "var(--iox-text-muted)",
|
|
4127
|
+
"on": "var(--iox-surface)",
|
|
4128
|
+
"min": 4.5
|
|
4129
|
+
},
|
|
4130
|
+
{
|
|
4131
|
+
"part": "calendar today ring",
|
|
4132
|
+
"fg": "var(--iox-line-strong)",
|
|
4133
|
+
"on": "var(--iox-surface)",
|
|
4134
|
+
"min": 3
|
|
4135
|
+
},
|
|
4136
|
+
{
|
|
4137
|
+
"part": "calendar chosen day",
|
|
4138
|
+
"fg": "var(--iox-accent-text)",
|
|
4139
|
+
"on": "var(--iox-surface)",
|
|
4140
|
+
"min": 3
|
|
4141
|
+
},
|
|
4142
|
+
{
|
|
4143
|
+
"part": "calendar chosen day number",
|
|
4144
|
+
"fg": "var(--iox-bg)",
|
|
4145
|
+
"on": "var(--iox-accent-text)",
|
|
4146
|
+
"min": 4.5
|
|
4147
|
+
},
|
|
4148
|
+
{
|
|
4149
|
+
"part": "calendar day hover",
|
|
4150
|
+
"fg": "var(--iox-text)",
|
|
4151
|
+
"on": "var(--iox-surface-2)",
|
|
4152
|
+
"min": 4.5
|
|
4153
|
+
},
|
|
3946
4154
|
{
|
|
3947
4155
|
"part": "checkbox · radio checked",
|
|
3948
4156
|
"fg": [
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE SECOND CHANNEL (premium components N2) — "or message us on WhatsApp", the button a form keeps beside its submit,
|
|
3
|
+
* before AND after it sends.
|
|
4
|
+
*
|
|
5
|
+
* It is an ordinary node in the form's actions row (a ButtonBlock that links) carrying the ROLE `form.channel`, so the
|
|
6
|
+
* shared CSS keeps it on screen when the form succeeds and the Publishable gate can find it. Its address is the
|
|
7
|
+
* OWNER's fact: the builder builds it from what the author types (a number, an address, a link) and never invents one —
|
|
8
|
+
* inserted empty, it blocks publishing until it is filled (generation-core `auditSite`, `empty-channel`).
|
|
9
|
+
*
|
|
10
|
+
* Framework-free, like the rest of forms-core.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** The role the channel's node carries (`iox-role-form-channel`). */
|
|
14
|
+
export const FORM_CHANNEL_ROLE = 'form.channel';
|
|
15
|
+
/** The role of the row that holds the submit button and the channel (`iox-role-form-actions`). */
|
|
16
|
+
export const FORM_ACTIONS_ROLE = 'form.actions';
|
|
17
|
+
|
|
18
|
+
export enum ChannelKind {
|
|
19
|
+
WhatsApp = 'whatsapp',
|
|
20
|
+
Phone = 'phone',
|
|
21
|
+
Email = 'email',
|
|
22
|
+
Link = 'link',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const CHANNEL_KINDS: ReadonlyArray<ChannelKind> = [ChannelKind.WhatsApp, ChannelKind.Phone, ChannelKind.Email, ChannelKind.Link];
|
|
26
|
+
|
|
27
|
+
const WHATSAPP_PREFIX = 'https://wa.me/';
|
|
28
|
+
const TEL_PREFIX = 'tel:';
|
|
29
|
+
const MAILTO_PREFIX = 'mailto:';
|
|
30
|
+
const HTTP = /^https?:\/\//i;
|
|
31
|
+
const EMAIL = /^[^\s@]+@[^\s@.]+(\.[^\s@.]+)+$/;
|
|
32
|
+
/** A phone number's own characters; anything else is typing noise (spaces, dashes, brackets). */
|
|
33
|
+
const DIGITS = /\d/g;
|
|
34
|
+
/** WhatsApp's click-to-chat wants the full international number: digits only, no `+`, no leading `00`. */
|
|
35
|
+
const INTERNATIONAL_00 = /^00/;
|
|
36
|
+
/** A number shorter than this is not a number anyone can be reached on. */
|
|
37
|
+
const MIN_DIGITS = 6;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The address a channel links to, built from what the author typed — or `''` when it gives nothing a visitor could
|
|
41
|
+
* use (no digits, not an email). Never a guess: a WhatsApp number keeps exactly the digits typed (the panel asks for
|
|
42
|
+
* the country code), a link without a scheme gets `https://`.
|
|
43
|
+
*/
|
|
44
|
+
export function channelHref(kind: ChannelKind | string, input: unknown): string {
|
|
45
|
+
const raw = typeof input === 'string' ? input.trim() : '';
|
|
46
|
+
if (!raw) return '';
|
|
47
|
+
switch (kind) {
|
|
48
|
+
case ChannelKind.WhatsApp: {
|
|
49
|
+
const digits = (raw.match(DIGITS) ?? []).join('').replace(INTERNATIONAL_00, '');
|
|
50
|
+
return digits.length >= MIN_DIGITS ? `${WHATSAPP_PREFIX}${digits}` : '';
|
|
51
|
+
}
|
|
52
|
+
case ChannelKind.Phone: {
|
|
53
|
+
const digits = (raw.match(DIGITS) ?? []).join('');
|
|
54
|
+
if (digits.length < MIN_DIGITS) return '';
|
|
55
|
+
return `${TEL_PREFIX}${raw.startsWith('+') ? '+' : ''}${digits}`;
|
|
56
|
+
}
|
|
57
|
+
case ChannelKind.Email:
|
|
58
|
+
return EMAIL.test(raw) ? `${MAILTO_PREFIX}${raw}` : '';
|
|
59
|
+
case ChannelKind.Link:
|
|
60
|
+
return HTTP.test(raw) ? raw : `https://${raw.replace(/^\/+/, '')}`;
|
|
61
|
+
default:
|
|
62
|
+
return '';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A channel's address back into what the panel shows: its kind and the value the author typed. */
|
|
67
|
+
export function parseChannelHref(href: unknown): { kind: ChannelKind; value: string } {
|
|
68
|
+
const text = typeof href === 'string' ? href.trim() : '';
|
|
69
|
+
if (text.toLowerCase().startsWith(WHATSAPP_PREFIX)) return { kind: ChannelKind.WhatsApp, value: text.slice(WHATSAPP_PREFIX.length).split(/[?#]/)[0] };
|
|
70
|
+
if (text.toLowerCase().startsWith(TEL_PREFIX)) return { kind: ChannelKind.Phone, value: text.slice(TEL_PREFIX.length) };
|
|
71
|
+
if (text.toLowerCase().startsWith(MAILTO_PREFIX)) return { kind: ChannelKind.Email, value: text.slice(MAILTO_PREFIX.length).split('?')[0] };
|
|
72
|
+
if (!text) return { kind: ChannelKind.WhatsApp, value: '' };
|
|
73
|
+
return { kind: ChannelKind.Link, value: text };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** True when the channel goes somewhere a visitor can use — the gate's "filled". */
|
|
77
|
+
export function isChannelFilled(href: unknown): boolean {
|
|
78
|
+
const { kind, value } = parseChannelHref(href);
|
|
79
|
+
return !!value && channelHref(kind, value) !== '';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** A WhatsApp or web channel opens in a new tab; a call or an email hands over to the device. */
|
|
83
|
+
export function channelOpensNewTab(kind: ChannelKind | string): boolean {
|
|
84
|
+
return kind === ChannelKind.WhatsApp || kind === ChannelKind.Link;
|
|
85
|
+
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
SubmitAction,
|
|
16
16
|
ValidationRule,
|
|
17
17
|
} from './form.model';
|
|
18
|
+
import { dateBoundRules } from './dates';
|
|
18
19
|
import { fieldOptionsOf } from './options';
|
|
19
20
|
|
|
20
21
|
/** The minimum a node must look like. Structural on purpose: this file knows no renderer. */
|
|
@@ -59,6 +60,65 @@ export function uniqueFieldName(name: string, taken: ReadonlySet<string>): strin
|
|
|
59
60
|
return `${name}${n}`;
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
/** A Field's own key, before the form makes it unique: the author's override, else derived from the label. */
|
|
64
|
+
export function baseFieldName(props: { name?: unknown; label?: unknown } | null | undefined): string {
|
|
65
|
+
const own = typeof props?.name === 'string' ? props.name : '';
|
|
66
|
+
return own || deriveFieldName(String(props?.label ?? ''));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* THE naming rule (vectoriox/iox-ui#43): a form's fields, in document order, each its base name made unique against
|
|
71
|
+
* the names before it — `email`, `email2`. The spec (`collectFormSpec`), the component path's runtime and the engine's
|
|
72
|
+
* string renderer all name a field from here, so the input's `name` and the saved spec's key can never differ.
|
|
73
|
+
*/
|
|
74
|
+
export function uniqueFieldNames(baseNames: ReadonlyArray<string>): string[] {
|
|
75
|
+
const taken = new Set<string>();
|
|
76
|
+
return baseNames.map(base => {
|
|
77
|
+
const name = uniqueFieldName(base, taken);
|
|
78
|
+
taken.add(name);
|
|
79
|
+
return name;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The submitted name of every Field of a Form, in document order — `uniqueFieldNames` over its fields. */
|
|
84
|
+
export function formFieldNames(node: FormNodeLike | null | undefined): string[] {
|
|
85
|
+
if (!node || node.type !== FORM_NODE_TYPE) return [];
|
|
86
|
+
return uniqueFieldNames(collectFieldNodes(node.children ?? []).map(f => baseFieldName(f.props)));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Each Field node of a Form → the name it submits under (`formFieldNames`, by node). */
|
|
90
|
+
export function formFieldNameMap<N extends FormNodeLike>(node: N | null | undefined): Map<FormNodeLike, string> {
|
|
91
|
+
const out = new Map<FormNodeLike, string>();
|
|
92
|
+
if (!node || node.type !== FORM_NODE_TYPE) return out;
|
|
93
|
+
const fields = collectFieldNodes(node.children ?? []);
|
|
94
|
+
uniqueFieldNames(fields.map(f => baseFieldName(f.props))).forEach((name, i) => out.set(fields[i], name));
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* A Field's spec from its props and the name its form gave it — ONE reading for the saved spec and the component
|
|
100
|
+
* path's runtime (it was written twice: `IoxFieldComponent.toSpec` never carried the options source, the default or a
|
|
101
|
+
* Date's bounds). A Date's Earliest / Latest (`earliest` / `latest`) become its `min` / `max` rules (premium N8).
|
|
102
|
+
*/
|
|
103
|
+
export function fieldSpecOf(props: Record<string, any> | null | undefined, name: string): FieldSpec {
|
|
104
|
+
const p = props ?? {};
|
|
105
|
+
const control = (p['control'] as ControlType) || ControlType.Text;
|
|
106
|
+
const spec: FieldSpec = { name, control, label: p['label'] ?? '' };
|
|
107
|
+
if (p['required']) spec.required = true;
|
|
108
|
+
const declared = normaliseRules(p['rules']);
|
|
109
|
+
const bounds = control === ControlType.Date
|
|
110
|
+
? dateBoundRules(p['earliest'], p['latest']).filter(b => !declared.some(r => r.name === b.name))
|
|
111
|
+
: [];
|
|
112
|
+
const rules = [...declared, ...bounds];
|
|
113
|
+
if (rules.length) spec.rules = rules;
|
|
114
|
+
const options = fieldOptionsOf(p);
|
|
115
|
+
if (options.length) spec.options = options;
|
|
116
|
+
if (p['optionsSource']) spec.optionsSource = p['optionsSource'];
|
|
117
|
+
if (p['defaultValue'] !== undefined && p['defaultValue'] !== '') spec.defaultValue = p['defaultValue'];
|
|
118
|
+
if (control === ControlType.Choices && p['multiple'] === true) spec.multiple = true;
|
|
119
|
+
return spec;
|
|
120
|
+
}
|
|
121
|
+
|
|
62
122
|
/**
|
|
63
123
|
* Walk a Form subtree and produce its spec.
|
|
64
124
|
*
|
|
@@ -69,13 +129,9 @@ export function collectFormSpec(node: FormNodeLike | null | undefined): FormSpec
|
|
|
69
129
|
if (!node || node.type !== FORM_NODE_TYPE) return null;
|
|
70
130
|
const props = node.props ?? {};
|
|
71
131
|
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
const spec = toFieldSpec(fieldNode, taken);
|
|
76
|
-
taken.add(spec.name);
|
|
77
|
-
fields.push(spec);
|
|
78
|
-
}
|
|
132
|
+
const fieldNodes = collectFieldNodes(node.children ?? []);
|
|
133
|
+
const names = uniqueFieldNames(fieldNodes.map(f => baseFieldName(f.props)));
|
|
134
|
+
const fields: FieldSpec[] = fieldNodes.map((f, i) => fieldSpecOf(f.props, names[i]));
|
|
79
135
|
|
|
80
136
|
return {
|
|
81
137
|
formId: props['formId'] ?? '',
|
|
@@ -104,29 +160,6 @@ export function collectFieldNodes(nodes: readonly FormNodeLike[]): FormNodeLike[
|
|
|
104
160
|
return out;
|
|
105
161
|
}
|
|
106
162
|
|
|
107
|
-
function toFieldSpec(node: FormNodeLike, taken: ReadonlySet<string>): FieldSpec {
|
|
108
|
-
const props = node.props ?? {};
|
|
109
|
-
const label = props['label'] ?? '';
|
|
110
|
-
// The author's explicit override wins; otherwise derive from the label.
|
|
111
|
-
const name = uniqueFieldName(props['name'] || deriveFieldName(label), taken);
|
|
112
|
-
|
|
113
|
-
const spec: FieldSpec = {
|
|
114
|
-
name,
|
|
115
|
-
control: (props['control'] as ControlType) ?? ControlType.Text,
|
|
116
|
-
label,
|
|
117
|
-
};
|
|
118
|
-
if (props['required']) spec.required = true;
|
|
119
|
-
const rules = normaliseRules(props['rules']);
|
|
120
|
-
if (rules.length) spec.rules = rules;
|
|
121
|
-
const options = fieldOptionsOf(props);
|
|
122
|
-
if (options.length) spec.options = options;
|
|
123
|
-
if (props['optionsSource']) spec.optionsSource = props['optionsSource'];
|
|
124
|
-
if (props['defaultValue'] !== undefined && props['defaultValue'] !== '') {
|
|
125
|
-
spec.defaultValue = props['defaultValue'];
|
|
126
|
-
}
|
|
127
|
-
return spec;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
163
|
/** Drop anything that isn't a rule we know — a hand-edited page must not break validation. */
|
|
131
164
|
function normaliseRules(raw: unknown): ValidationRule[] {
|
|
132
165
|
if (!Array.isArray(raw)) return [];
|