@vectoriox/iox-ui 5.82.2 → 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 +10 -0
- package/styles/iox-forms.css +349 -10
|
@@ -3757,6 +3757,11 @@ var ControlType;
|
|
|
3757
3757
|
/** A slider over a numeric range — min/max/step ride the field. */
|
|
3758
3758
|
ControlType["Range"] = "range";
|
|
3759
3759
|
ControlType["Radio"] = "radio";
|
|
3760
|
+
/** One or several of a few visible choices — chips (the default) or a list of radios / checkboxes (premium N1).
|
|
3761
|
+
* Its `multiple` trait picks the value's shape: a string, or a list of strings. */
|
|
3762
|
+
ControlType["Choices"] = "choices";
|
|
3763
|
+
/** A date the visitor picks from the look's calendar (premium N8) — a button that reads the date in words and a
|
|
3764
|
+
* hidden input carrying the ISO `YYYY-MM-DD` value. Never the browser's own date box. */
|
|
3760
3765
|
ControlType["Date"] = "date";
|
|
3761
3766
|
ControlType["Time"] = "time";
|
|
3762
3767
|
ControlType["File"] = "file";
|
|
@@ -3778,7 +3783,20 @@ var RuleName;
|
|
|
3778
3783
|
RuleName["Accepted"] = "accepted";
|
|
3779
3784
|
RuleName["FileType"] = "fileType";
|
|
3780
3785
|
RuleName["FileSize"] = "fileSize";
|
|
3786
|
+
/** IMPLIED, never authored: an answer of a Choices / Radio / Dropdown must be one of the field's own options
|
|
3787
|
+
* (premium N1) — a hand-crafted POST cannot file a value the form never offered. */
|
|
3788
|
+
RuleName["OneOf"] = "oneOf";
|
|
3789
|
+
/** IMPLIED, never authored: a Date's answer is a real `YYYY-MM-DD` day (premium N8). */
|
|
3790
|
+
RuleName["ValidDate"] = "validDate";
|
|
3781
3791
|
})(RuleName || (RuleName = {}));
|
|
3792
|
+
/** How a Choices field draws its options (premium N1). */
|
|
3793
|
+
var ChoiceAppearance;
|
|
3794
|
+
(function (ChoiceAppearance) {
|
|
3795
|
+
/** Each option a chip — the label is the chip; the native input inside it is visually hidden. */
|
|
3796
|
+
ChoiceAppearance["Chips"] = "chips";
|
|
3797
|
+
/** The options as rows of the look's own radio / checkbox (DS9.1). */
|
|
3798
|
+
ChoiceAppearance["List"] = "list";
|
|
3799
|
+
})(ChoiceAppearance || (ChoiceAppearance = {}));
|
|
3782
3800
|
/** Named regexes so an author picks "Email" rather than writing one. */
|
|
3783
3801
|
var PatternPreset;
|
|
3784
3802
|
(function (PatternPreset) {
|
|
@@ -3819,8 +3837,21 @@ const IMPLIED_PATTERN = Object.freeze({
|
|
|
3819
3837
|
[ControlType.Tel]: PatternPreset.Phone,
|
|
3820
3838
|
[ControlType.Url]: PatternPreset.Url,
|
|
3821
3839
|
});
|
|
3822
|
-
/**
|
|
3823
|
-
|
|
3840
|
+
/**
|
|
3841
|
+
* Whether this field's answer is a LIST rather than one value — a file list, or a Choices field that takes several
|
|
3842
|
+
* (premium N1). Per FIELD, not per control: the same Choices control is single or multiple by its `multiple` trait.
|
|
3843
|
+
* ONE reading for collect, validate, the runtimes' transport and the server (replaced the control-only
|
|
3844
|
+
* `MULTI_VALUE_CONTROLS`, which nothing read).
|
|
3845
|
+
*/
|
|
3846
|
+
function isMultiValueField(field) {
|
|
3847
|
+
if (!field)
|
|
3848
|
+
return false;
|
|
3849
|
+
if (field.control === ControlType.File)
|
|
3850
|
+
return true;
|
|
3851
|
+
return field.control === ControlType.Choices && field.multiple === true;
|
|
3852
|
+
}
|
|
3853
|
+
/** The controls whose answers must be one of the field's own options (the implied `oneOf` rule). */
|
|
3854
|
+
const OPTION_CONTROLS = new Set([ControlType.Choices, ControlType.Radio, ControlType.Select]);
|
|
3824
3855
|
|
|
3825
3856
|
// The default message for every rule, as an i18n KEY — never a rendered string.
|
|
3826
3857
|
//
|
|
@@ -3851,6 +3882,26 @@ const RULE_MESSAGE_KEYS = Object.freeze({
|
|
|
3851
3882
|
[RuleName.Accepted]: `${MESSAGE_NAMESPACE}.accepted`,
|
|
3852
3883
|
[RuleName.FileType]: `${MESSAGE_NAMESPACE}.fileType`,
|
|
3853
3884
|
[RuleName.FileSize]: `${MESSAGE_NAMESPACE}.fileSize`,
|
|
3885
|
+
[RuleName.OneOf]: `${MESSAGE_NAMESPACE}.oneOf`,
|
|
3886
|
+
[RuleName.ValidDate]: `${MESSAGE_NAMESPACE}.validDate`,
|
|
3887
|
+
});
|
|
3888
|
+
/**
|
|
3889
|
+
* What "required" says on a field you CHOOSE rather than type into (premium N1 / N8): "Choose an option", "Choose at
|
|
3890
|
+
* least one", "Choose a date" — never "This field is required" beside a row of chips.
|
|
3891
|
+
*/
|
|
3892
|
+
const CHOICE_MESSAGE_KEYS = Object.freeze({
|
|
3893
|
+
One: `${MESSAGE_NAMESPACE}.choose`,
|
|
3894
|
+
Some: `${MESSAGE_NAMESPACE}.chooseSome`,
|
|
3895
|
+
Date: `${MESSAGE_NAMESPACE}.chooseDate`,
|
|
3896
|
+
});
|
|
3897
|
+
/** A Date outside its Earliest / Latest. The calendar refuses those days, so this is a stale page or a hand POST. */
|
|
3898
|
+
const DATE_MESSAGE_KEYS = Object.freeze({
|
|
3899
|
+
Min: `${MESSAGE_NAMESPACE}.dateMin`,
|
|
3900
|
+
Max: `${MESSAGE_NAMESPACE}.dateMax`,
|
|
3901
|
+
});
|
|
3902
|
+
/** The submit button's own words (premium N2): what it says once its form has sent, when the author wrote none. */
|
|
3903
|
+
const SUBMIT_MESSAGE_KEYS = Object.freeze({
|
|
3904
|
+
Sent: 'forms.submit.sent',
|
|
3854
3905
|
});
|
|
3855
3906
|
/**
|
|
3856
3907
|
* A pattern failure deserves better than "Invalid format". Each preset gets its own key so the
|
|
@@ -3896,8 +3947,266 @@ const DEFAULT_EN_MESSAGES = Object.freeze({
|
|
|
3896
3947
|
[FORM_ERROR_KEYS.Spam]: 'We couldn’t accept this submission',
|
|
3897
3948
|
[FORM_ERROR_KEYS.RateLimited]: 'Too many attempts — please wait a moment and try again',
|
|
3898
3949
|
[FORM_ERROR_KEYS.Server]: 'Something went wrong on our side — your answers are still here',
|
|
3950
|
+
[RULE_MESSAGE_KEYS[RuleName.OneOf]]: 'Choose one of the options',
|
|
3951
|
+
[RULE_MESSAGE_KEYS[RuleName.ValidDate]]: 'Choose a date from the calendar',
|
|
3952
|
+
[CHOICE_MESSAGE_KEYS.One]: 'Choose an option',
|
|
3953
|
+
[CHOICE_MESSAGE_KEYS.Some]: 'Choose at least one',
|
|
3954
|
+
[CHOICE_MESSAGE_KEYS.Date]: 'Choose a date',
|
|
3955
|
+
[DATE_MESSAGE_KEYS.Min]: 'Choose a later date',
|
|
3956
|
+
[DATE_MESSAGE_KEYS.Max]: 'Choose an earlier date',
|
|
3957
|
+
[SUBMIT_MESSAGE_KEYS.Sent]: 'Sent',
|
|
3899
3958
|
});
|
|
3900
3959
|
|
|
3960
|
+
/**
|
|
3961
|
+
* DATES — the date picker's pure half (premium components N8) and the date rules' one reading.
|
|
3962
|
+
*
|
|
3963
|
+
* A Date field's value is an ISO day, `YYYY-MM-DD`, compared as a string (the format sorts as the calendar does).
|
|
3964
|
+
* Its `min` / `max` rules take an ISO day OR an offset in days from today (`{ offsetDays: 0 }` = today), so "from
|
|
3965
|
+
* today on" stays true tomorrow without a re-save. ONE reading for the validator (client AND iox-cms), the calendar
|
|
3966
|
+
* (which days it refuses) and the builder's Earliest / Latest controls.
|
|
3967
|
+
*
|
|
3968
|
+
* The calendar model (six rows of seven, roving focus, the APG keys) lives here too, so the DOM runtime only draws
|
|
3969
|
+
* what this decides — the canvas, the engine's component path and its string path draw the SAME month.
|
|
3970
|
+
*
|
|
3971
|
+
* ⚠️ Framework-free, like the rest of forms-core. Local-time `Date` objects are used only to walk the calendar; a
|
|
3972
|
+
* day leaves this file as an ISO string.
|
|
3973
|
+
*/
|
|
3974
|
+
const ISO_DAY = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
3975
|
+
const OFFSET = /^[+-]?\d+$/;
|
|
3976
|
+
const DAYS_IN_WEEK = 7;
|
|
3977
|
+
/** Always six rows, so the panel never changes size between months (APG; Emil 74 — one object, one size). */
|
|
3978
|
+
const CALENDAR_ROWS = 6;
|
|
3979
|
+
const pad = (n, width = 2) => String(n).padStart(width, '0');
|
|
3980
|
+
/** The ISO day of a local `Date`. */
|
|
3981
|
+
function isoOfDate(d) {
|
|
3982
|
+
return `${pad(d.getFullYear(), 4)}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
3983
|
+
}
|
|
3984
|
+
/** A local `Date` at midnight for an ISO day, or null when it is not one (2026-02-30 is not). */
|
|
3985
|
+
function dateOfIso(iso) {
|
|
3986
|
+
const m = typeof iso === 'string' ? ISO_DAY.exec(iso.trim()) : null;
|
|
3987
|
+
if (!m)
|
|
3988
|
+
return null;
|
|
3989
|
+
const [y, mo, d] = [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
3990
|
+
const date = new Date(y, mo - 1, d);
|
|
3991
|
+
date.setFullYear(y);
|
|
3992
|
+
return date.getFullYear() === y && date.getMonth() === mo - 1 && date.getDate() === d ? date : null;
|
|
3993
|
+
}
|
|
3994
|
+
/** True for a real calendar day written `YYYY-MM-DD`. */
|
|
3995
|
+
function isIsoDate(value) {
|
|
3996
|
+
return dateOfIso(value) !== null;
|
|
3997
|
+
}
|
|
3998
|
+
/** Today, as the device that runs this sees it (the visitor's local day in a browser; the server's on iox-cms). */
|
|
3999
|
+
function todayIso(now = new Date()) {
|
|
4000
|
+
return isoOfDate(now);
|
|
4001
|
+
}
|
|
4002
|
+
/** `iso` moved by `days` (negative goes back). */
|
|
4003
|
+
function addDaysIso(iso, days) {
|
|
4004
|
+
const d = dateOfIso(iso);
|
|
4005
|
+
if (!d)
|
|
4006
|
+
return iso;
|
|
4007
|
+
return isoOfDate(new Date(d.getFullYear(), d.getMonth(), d.getDate() + days));
|
|
4008
|
+
}
|
|
4009
|
+
/** `iso` moved by `months`, its day clamped to the target month's last (31 Jan + 1 month = 28/29 Feb). */
|
|
4010
|
+
function addMonthsIso(iso, months) {
|
|
4011
|
+
const d = dateOfIso(iso);
|
|
4012
|
+
if (!d)
|
|
4013
|
+
return iso;
|
|
4014
|
+
const first = new Date(d.getFullYear(), d.getMonth() + months, 1);
|
|
4015
|
+
const last = new Date(first.getFullYear(), first.getMonth() + 1, 0).getDate();
|
|
4016
|
+
return isoOfDate(new Date(first.getFullYear(), first.getMonth(), Math.min(d.getDate(), last)));
|
|
4017
|
+
}
|
|
4018
|
+
/** `iso` held inside [min, max] (either may be absent). */
|
|
4019
|
+
function clampIso(iso, min, max) {
|
|
4020
|
+
if (min && iso < min)
|
|
4021
|
+
return min;
|
|
4022
|
+
if (max && iso > max)
|
|
4023
|
+
return max;
|
|
4024
|
+
return iso;
|
|
4025
|
+
}
|
|
4026
|
+
/**
|
|
4027
|
+
* A Field's `earliest` / `latest` trait → the rule parameter. The trait is one string so the panel and a saved page
|
|
4028
|
+
* stay simple: `''` = any day · an integer = that many days from today (`'0'` = today, `'30'` = a month ahead) · an
|
|
4029
|
+
* ISO day. Anything else reads as no bound.
|
|
4030
|
+
*/
|
|
4031
|
+
function parseDateBound(raw) {
|
|
4032
|
+
if (typeof raw === 'number' && Number.isInteger(raw))
|
|
4033
|
+
return { offsetDays: raw };
|
|
4034
|
+
if (typeof raw !== 'string')
|
|
4035
|
+
return null;
|
|
4036
|
+
const text = raw.trim();
|
|
4037
|
+
if (!text)
|
|
4038
|
+
return null;
|
|
4039
|
+
if (OFFSET.test(text))
|
|
4040
|
+
return { offsetDays: Number(text) };
|
|
4041
|
+
return isIsoDate(text) ? { date: text } : null;
|
|
4042
|
+
}
|
|
4043
|
+
/** The `min` / `max` rules a Date field's Earliest / Latest traits give it (none for "any"). */
|
|
4044
|
+
function dateBoundRules(earliest, latest) {
|
|
4045
|
+
const rules = [];
|
|
4046
|
+
const min = parseDateBound(earliest);
|
|
4047
|
+
const max = parseDateBound(latest);
|
|
4048
|
+
if (min)
|
|
4049
|
+
rules.push({ name: RuleName.Min, params: { ...min } });
|
|
4050
|
+
if (max)
|
|
4051
|
+
rules.push({ name: RuleName.Max, params: { ...max } });
|
|
4052
|
+
return rules;
|
|
4053
|
+
}
|
|
4054
|
+
/** True when a rule's params are a DATE bound (an ISO day or an offset), never a number bound. */
|
|
4055
|
+
function isDateBoundParams(params) {
|
|
4056
|
+
return !!params && (typeof params['date'] === 'string' || Number.isInteger(params['offsetDays']));
|
|
4057
|
+
}
|
|
4058
|
+
/** The ISO day a bound names today, or null when it names none. */
|
|
4059
|
+
function resolveDateBound(params, today) {
|
|
4060
|
+
if (!params)
|
|
4061
|
+
return null;
|
|
4062
|
+
if (typeof params['date'] === 'string' && isIsoDate(params['date']))
|
|
4063
|
+
return params['date'];
|
|
4064
|
+
if (Number.isInteger(params['offsetDays']))
|
|
4065
|
+
return addDaysIso(today, params['offsetDays']);
|
|
4066
|
+
return null;
|
|
4067
|
+
}
|
|
4068
|
+
/** The first and last day a Date field accepts today — the calendar's refused days and the validator's range. */
|
|
4069
|
+
function fieldDateRange(field, today) {
|
|
4070
|
+
let min = null;
|
|
4071
|
+
let max = null;
|
|
4072
|
+
for (const rule of field?.rules ?? []) {
|
|
4073
|
+
if (rule.name === RuleName.Min)
|
|
4074
|
+
min = resolveDateBound(rule.params, today) ?? min;
|
|
4075
|
+
if (rule.name === RuleName.Max)
|
|
4076
|
+
max = resolveDateBound(rule.params, today) ?? max;
|
|
4077
|
+
}
|
|
4078
|
+
return { min, max };
|
|
4079
|
+
}
|
|
4080
|
+
/** True when the field is a date picker (the control that reads these rules as days). */
|
|
4081
|
+
function isDateField(field) {
|
|
4082
|
+
return field?.control === ControlType.Date;
|
|
4083
|
+
}
|
|
4084
|
+
// ─── the week, the month, the keys ───────────────────────────────────────────
|
|
4085
|
+
/** Regions whose week starts on Sunday when the runtime cannot say (Intl.Locale#getWeekInfo is not everywhere). */
|
|
4086
|
+
const SUNDAY_REGIONS = new Set(['US', 'CA', 'IL', 'JP', 'BR', 'MX', 'PH', 'IN', 'KR', 'TW', 'ZA', 'AU']);
|
|
4087
|
+
const SUNDAY_LANGUAGES = new Set(['he', 'iw', 'ja', 'ko']);
|
|
4088
|
+
/**
|
|
4089
|
+
* The first day of the week for a locale, 0 = Sunday … 6 = Saturday. `Intl.Locale(…).getWeekInfo()` (or the older
|
|
4090
|
+
* `weekInfo` getter) when the runtime has it, else a small region table; a bare `en` reads as the US.
|
|
4091
|
+
*/
|
|
4092
|
+
function weekStartFor(locale) {
|
|
4093
|
+
const tag = locale || 'en';
|
|
4094
|
+
try {
|
|
4095
|
+
const Ctor = Intl.Locale;
|
|
4096
|
+
if (Ctor) {
|
|
4097
|
+
const loc = new Ctor(tag);
|
|
4098
|
+
const info = typeof loc.getWeekInfo === 'function' ? loc.getWeekInfo() : loc.weekInfo;
|
|
4099
|
+
const first = Number(info?.firstDay);
|
|
4100
|
+
if (first >= 1 && first <= 7)
|
|
4101
|
+
return first % DAYS_IN_WEEK;
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
catch { /* an unknown tag falls through to the table */ }
|
|
4105
|
+
const [language, region] = tag.split(/[-_]/);
|
|
4106
|
+
if (region && SUNDAY_REGIONS.has(region.toUpperCase()))
|
|
4107
|
+
return 0;
|
|
4108
|
+
if (!region && (language === 'en' || SUNDAY_LANGUAGES.has(language)))
|
|
4109
|
+
return 0;
|
|
4110
|
+
return 1;
|
|
4111
|
+
}
|
|
4112
|
+
/** The month `focus` sits in, as six rows of seven — the ONE grid every renderer draws. */
|
|
4113
|
+
function calendarMonth(focus, options) {
|
|
4114
|
+
const at = dateOfIso(focus) ?? dateOfIso(options.today) ?? new Date();
|
|
4115
|
+
const year = at.getFullYear();
|
|
4116
|
+
const month = at.getMonth();
|
|
4117
|
+
const lead = (new Date(year, month, 1).getDay() - options.weekStart + DAYS_IN_WEEK) % DAYS_IN_WEEK;
|
|
4118
|
+
const days = new Date(year, month + 1, 0).getDate();
|
|
4119
|
+
const weeks = [];
|
|
4120
|
+
for (let r = 0; r < CALENDAR_ROWS; r++) {
|
|
4121
|
+
const row = [];
|
|
4122
|
+
for (let c = 0; c < DAYS_IN_WEEK; c++) {
|
|
4123
|
+
const n = r * DAYS_IN_WEEK + c - lead + 1;
|
|
4124
|
+
if (n < 1 || n > days) {
|
|
4125
|
+
row.push(null);
|
|
4126
|
+
continue;
|
|
4127
|
+
}
|
|
4128
|
+
const iso = isoOfDate(new Date(year, month, n));
|
|
4129
|
+
row.push({
|
|
4130
|
+
iso, day: n,
|
|
4131
|
+
today: iso === options.today,
|
|
4132
|
+
selected: iso === options.selected,
|
|
4133
|
+
disabled: (!!options.min && iso < options.min) || (!!options.max && iso > options.max),
|
|
4134
|
+
focus: iso === focus,
|
|
4135
|
+
});
|
|
4136
|
+
}
|
|
4137
|
+
weeks.push(row);
|
|
4138
|
+
}
|
|
4139
|
+
const monthKey = (iso) => (iso ? iso.slice(0, 7) : null);
|
|
4140
|
+
const here = monthKey(isoOfDate(new Date(year, month, 1)));
|
|
4141
|
+
const minMonth = monthKey(options.min);
|
|
4142
|
+
const maxMonth = monthKey(options.max);
|
|
4143
|
+
return {
|
|
4144
|
+
year, month, weeks,
|
|
4145
|
+
weekdays: Array.from({ length: DAYS_IN_WEEK }, (_, i) => (options.weekStart + i) % DAYS_IN_WEEK),
|
|
4146
|
+
canPrev: !minMonth || here > minMonth,
|
|
4147
|
+
canNext: !maxMonth || here < maxMonth,
|
|
4148
|
+
};
|
|
4149
|
+
}
|
|
4150
|
+
/** The keys the grid answers (APG date picker dialog). */
|
|
4151
|
+
var CalendarKey;
|
|
4152
|
+
(function (CalendarKey) {
|
|
4153
|
+
CalendarKey["Left"] = "ArrowLeft";
|
|
4154
|
+
CalendarKey["Right"] = "ArrowRight";
|
|
4155
|
+
CalendarKey["Up"] = "ArrowUp";
|
|
4156
|
+
CalendarKey["Down"] = "ArrowDown";
|
|
4157
|
+
CalendarKey["Home"] = "Home";
|
|
4158
|
+
CalendarKey["End"] = "End";
|
|
4159
|
+
CalendarKey["PageUp"] = "PageUp";
|
|
4160
|
+
CalendarKey["PageDown"] = "PageDown";
|
|
4161
|
+
})(CalendarKey || (CalendarKey = {}));
|
|
4162
|
+
const KEY_MOVES = {
|
|
4163
|
+
// The arrows follow the READING direction: in RTL "→" is a day back (APG; mirrored).
|
|
4164
|
+
[CalendarKey.Right]: (iso, c) => addDaysIso(iso, c.ahead),
|
|
4165
|
+
[CalendarKey.Left]: (iso, c) => addDaysIso(iso, -c.ahead),
|
|
4166
|
+
[CalendarKey.Down]: iso => addDaysIso(iso, DAYS_IN_WEEK),
|
|
4167
|
+
[CalendarKey.Up]: iso => addDaysIso(iso, -DAYS_IN_WEEK),
|
|
4168
|
+
[CalendarKey.Home]: (iso, c) => addDaysIso(iso, -c.dow),
|
|
4169
|
+
[CalendarKey.End]: (iso, c) => addDaysIso(iso, DAYS_IN_WEEK - 1 - c.dow),
|
|
4170
|
+
// A month; with Shift, a year.
|
|
4171
|
+
[CalendarKey.PageUp]: (iso, c) => addMonthsIso(iso, c.shift ? -12 : -1),
|
|
4172
|
+
[CalendarKey.PageDown]: (iso, c) => addMonthsIso(iso, c.shift ? 12 : 1),
|
|
4173
|
+
};
|
|
4174
|
+
/**
|
|
4175
|
+
* Where a key moves the focused day, held inside [min, max] — or null for a key the grid does not answer (Enter,
|
|
4176
|
+
* Space and Escape are the runtime's: they pick or close, they never move).
|
|
4177
|
+
*/
|
|
4178
|
+
function calendarKeyTarget(key, focus, options) {
|
|
4179
|
+
const move = KEY_MOVES[key];
|
|
4180
|
+
const at = dateOfIso(focus);
|
|
4181
|
+
if (!move || !at)
|
|
4182
|
+
return null;
|
|
4183
|
+
const dow = (at.getDay() - options.weekStart + DAYS_IN_WEEK) % DAYS_IN_WEEK;
|
|
4184
|
+
return clampIso(move(focus, { ahead: options.rtl ? -1 : 1, dow, shift: !!options.shift }), options.min, options.max);
|
|
4185
|
+
}
|
|
4186
|
+
// ─── words ───────────────────────────────────────────────────────────────────
|
|
4187
|
+
/** The date in words, in the site's language: "Thu, 20 November" — the year only when it is not this year's. */
|
|
4188
|
+
function formatDateInWords(iso, locale, today) {
|
|
4189
|
+
const d = dateOfIso(iso);
|
|
4190
|
+
if (!d)
|
|
4191
|
+
return '';
|
|
4192
|
+
const sameYear = iso.slice(0, 4) === today.slice(0, 4);
|
|
4193
|
+
try {
|
|
4194
|
+
return new Intl.DateTimeFormat(locale || undefined, { weekday: 'short', day: 'numeric', month: 'long', ...(sameYear ? {} : { year: 'numeric' }) }).format(d);
|
|
4195
|
+
}
|
|
4196
|
+
catch {
|
|
4197
|
+
return iso;
|
|
4198
|
+
}
|
|
4199
|
+
}
|
|
4200
|
+
const DATE_PICKER_WORDS = Object.freeze({
|
|
4201
|
+
en: { choose: 'Choose a date', previousMonth: 'Previous month', nextMonth: 'Next month', close: 'Close' },
|
|
4202
|
+
he: { choose: 'בחרו תאריך', previousMonth: 'החודש הקודם', nextMonth: 'החודש הבא', close: 'סגירה' },
|
|
4203
|
+
});
|
|
4204
|
+
/** The words for a language tag (`he-IL` → Hebrew); English for anything else. */
|
|
4205
|
+
function datePickerWords(lang) {
|
|
4206
|
+
const language = String(lang ?? '').toLowerCase().split(/[-_]/)[0];
|
|
4207
|
+
return language === 'he' || language === 'iw' ? DATE_PICKER_WORDS.he : DATE_PICKER_WORDS.en;
|
|
4208
|
+
}
|
|
4209
|
+
|
|
3901
4210
|
// The validator. ONE implementation, run by the builder canvas, the SSR engine and `iox-cms`.
|
|
3902
4211
|
//
|
|
3903
4212
|
// Client-side validation is UX; server-side validation is truth. Because both call this function,
|
|
@@ -3914,14 +4223,14 @@ const DEFAULT_EN_MESSAGES = Object.freeze({
|
|
|
3914
4223
|
* number" at someone who simply left it blank — the single most common validation-UX bug.
|
|
3915
4224
|
* The FIRST failure wins: a visitor fixes one thing at a time.
|
|
3916
4225
|
*/
|
|
3917
|
-
function validateField(value, spec, form = {}) {
|
|
4226
|
+
function validateField(value, spec, form = {}, options = {}) {
|
|
3918
4227
|
const rules = effectiveRules(spec);
|
|
3919
4228
|
const empty = isEmpty(value, spec.control);
|
|
3920
4229
|
for (const rule of rules) {
|
|
3921
4230
|
const appliesToEmpty = rule.name === RuleName.Required || rule.name === RuleName.Accepted;
|
|
3922
4231
|
if (empty && !appliesToEmpty)
|
|
3923
4232
|
continue;
|
|
3924
|
-
const failure = check(rule, value, spec, form);
|
|
4233
|
+
const failure = check(rule, value, spec, form, options);
|
|
3925
4234
|
if (failure) {
|
|
3926
4235
|
return {
|
|
3927
4236
|
valid: false,
|
|
@@ -3936,16 +4245,33 @@ function validateField(value, spec, form = {}) {
|
|
|
3936
4245
|
* Validate a whole submission. Every field is checked (not short-circuited at the first bad one) so
|
|
3937
4246
|
* the visitor sees every problem at once instead of fixing them one reload at a time.
|
|
3938
4247
|
*/
|
|
3939
|
-
function validateForm(values, spec) {
|
|
4248
|
+
function validateForm(values, spec, options = {}) {
|
|
3940
4249
|
const errors = {};
|
|
3941
4250
|
for (const field of spec.fields ?? []) {
|
|
3942
|
-
const result = validateField(values?.[field.name], field, values ?? {});
|
|
4251
|
+
const result = validateField(values?.[field.name], field, values ?? {}, options);
|
|
3943
4252
|
if (!result.valid) {
|
|
3944
4253
|
errors[field.name] = { messageKey: result.messageKey, params: result.params };
|
|
3945
4254
|
}
|
|
3946
4255
|
}
|
|
3947
4256
|
return { valid: Object.keys(errors).length === 0, errors };
|
|
3948
4257
|
}
|
|
4258
|
+
/**
|
|
4259
|
+
* The answers in the shape each field takes (premium N1): a field that takes several answers gets a LIST (one answer
|
|
4260
|
+
* posted on its own is a list of one; nothing is an empty list). ONE reading for a host's transport and the server,
|
|
4261
|
+
* before validation — `isMultiValueField` decides. A single-answer field is left as posted: two answers to it is the
|
|
4262
|
+
* validator's to refuse, never quietly cut to one.
|
|
4263
|
+
*/
|
|
4264
|
+
function normaliseAnswers(values, spec) {
|
|
4265
|
+
const out = { ...(values ?? {}) };
|
|
4266
|
+
for (const field of spec.fields ?? []) {
|
|
4267
|
+
if (!isMultiValueField(field) || field.control === ControlType.File)
|
|
4268
|
+
continue;
|
|
4269
|
+
if (!(field.name in out))
|
|
4270
|
+
continue;
|
|
4271
|
+
out[field.name] = chosen(out[field.name]);
|
|
4272
|
+
}
|
|
4273
|
+
return out;
|
|
4274
|
+
}
|
|
3949
4275
|
/** The name of the first field with an error, in spec order — what the browser should focus. */
|
|
3950
4276
|
function firstInvalidField(spec, errors) {
|
|
3951
4277
|
for (const field of spec.fields ?? [])
|
|
@@ -3974,8 +4300,54 @@ function effectiveRules(spec) {
|
|
|
3974
4300
|
if (preset && !has(RuleName.Pattern)) {
|
|
3975
4301
|
implied.push({ name: RuleName.Pattern, params: { preset } });
|
|
3976
4302
|
}
|
|
4303
|
+
// An answer to a field of CHOICES must be one the form offered (premium N1) — only when the options are the
|
|
4304
|
+
// field's own: options from a data source are not known here, so they are not judged.
|
|
4305
|
+
if (OPTION_CONTROLS.has(spec.control) && !spec.optionsSource && (spec.options ?? []).length) {
|
|
4306
|
+
implied.push({ name: RuleName.OneOf });
|
|
4307
|
+
}
|
|
4308
|
+
// A Date's answer is a real day before any bound is asked about it (premium N8).
|
|
4309
|
+
if (spec.control === ControlType.Date)
|
|
4310
|
+
implied.push({ name: RuleName.ValidDate });
|
|
3977
4311
|
return [...implied, ...declared];
|
|
3978
4312
|
}
|
|
4313
|
+
/** "Required" in the words of the control: choose, not fill in. */
|
|
4314
|
+
function requiredMessageKey(spec) {
|
|
4315
|
+
if (spec.control === ControlType.Date)
|
|
4316
|
+
return CHOICE_MESSAGE_KEYS.Date;
|
|
4317
|
+
if (isMultiValueField(spec) && spec.control === ControlType.Choices)
|
|
4318
|
+
return CHOICE_MESSAGE_KEYS.Some;
|
|
4319
|
+
if (OPTION_CONTROLS.has(spec.control))
|
|
4320
|
+
return CHOICE_MESSAGE_KEYS.One;
|
|
4321
|
+
return RULE_MESSAGE_KEYS[RuleName.Required];
|
|
4322
|
+
}
|
|
4323
|
+
/** The answers a field of choices gave, as a list — a single answer is a list of one, nothing is an empty list. */
|
|
4324
|
+
function chosen(value) {
|
|
4325
|
+
if (Array.isArray(value))
|
|
4326
|
+
return value;
|
|
4327
|
+
return value === null || value === undefined || value === '' ? [] : [value];
|
|
4328
|
+
}
|
|
4329
|
+
/** The `oneOf` verdict: every answer is one of the options, and a single-answer field got at most one. */
|
|
4330
|
+
function isOneOf(value, spec) {
|
|
4331
|
+
const offered = new Set((spec.options ?? []).map(o => String(o.value)));
|
|
4332
|
+
const answers = chosen(value);
|
|
4333
|
+
if (!isMultiValueField(spec) && answers.length > 1)
|
|
4334
|
+
return false;
|
|
4335
|
+
return answers.every(a => (typeof a === 'string' || typeof a === 'number') && offered.has(String(a)));
|
|
4336
|
+
}
|
|
4337
|
+
/** A Date bound, as a failure or null — the bound resolved against today, widened by the server's slack. */
|
|
4338
|
+
function dateBoundFailure(rule, value, options) {
|
|
4339
|
+
const today = options.today ?? todayIso();
|
|
4340
|
+
const slack = Math.max(0, Math.trunc(options.dateSlackDays ?? 0));
|
|
4341
|
+
const bound = resolveDateBound(rule.params, today);
|
|
4342
|
+
if (!bound || !isIsoDate(value))
|
|
4343
|
+
return null;
|
|
4344
|
+
const day = String(value);
|
|
4345
|
+
if (rule.name === RuleName.Min && day < addDaysIso(bound, -slack))
|
|
4346
|
+
return { messageKey: DATE_MESSAGE_KEYS.Min };
|
|
4347
|
+
if (rule.name === RuleName.Max && day > addDaysIso(bound, slack))
|
|
4348
|
+
return { messageKey: DATE_MESSAGE_KEYS.Max };
|
|
4349
|
+
return null;
|
|
4350
|
+
}
|
|
3979
4351
|
/** Empty means "the visitor gave us nothing" — per control type, not a blanket falsy check. */
|
|
3980
4352
|
function isEmpty(value, control) {
|
|
3981
4353
|
if (value === null || value === undefined)
|
|
@@ -3988,15 +4360,24 @@ function isEmpty(value, control) {
|
|
|
3988
4360
|
return value.trim() === '';
|
|
3989
4361
|
return false;
|
|
3990
4362
|
}
|
|
3991
|
-
function check(rule, value, spec, form) {
|
|
4363
|
+
function check(rule, value, spec, form, options) {
|
|
3992
4364
|
const fail = (messageKey, params) => ({ messageKey, params });
|
|
3993
4365
|
const defaultKey = RULE_MESSAGE_KEYS[rule.name];
|
|
3994
4366
|
const n = Number(rule.params?.['n']);
|
|
4367
|
+
// A Date's min / max are DAYS (an ISO day or an offset from today), never a number to parseFloat — `2026-11-06`
|
|
4368
|
+
// read as 2026 is what the numeric reading did.
|
|
4369
|
+
if ((rule.name === RuleName.Min || rule.name === RuleName.Max) && spec.control === ControlType.Date && isDateBoundParams(rule.params)) {
|
|
4370
|
+
return dateBoundFailure(rule, value, options);
|
|
4371
|
+
}
|
|
3995
4372
|
switch (rule.name) {
|
|
3996
4373
|
case RuleName.Required:
|
|
3997
|
-
return isEmpty(value, spec.control) ? fail(
|
|
4374
|
+
return isEmpty(value, spec.control) ? fail(requiredMessageKey(spec)) : null;
|
|
3998
4375
|
case RuleName.Accepted:
|
|
3999
4376
|
return value === true ? null : fail(defaultKey);
|
|
4377
|
+
case RuleName.OneOf:
|
|
4378
|
+
return isOneOf(value, spec) ? null : fail(defaultKey);
|
|
4379
|
+
case RuleName.ValidDate:
|
|
4380
|
+
return isIsoDate(value) ? null : fail(defaultKey);
|
|
4000
4381
|
case RuleName.MinLength:
|
|
4001
4382
|
return text(value).length < n ? fail(defaultKey, { n }) : null;
|
|
4002
4383
|
case RuleName.MaxLength:
|
|
@@ -4140,6 +4521,68 @@ function uniqueFieldName(name, taken) {
|
|
|
4140
4521
|
n++;
|
|
4141
4522
|
return `${name}${n}`;
|
|
4142
4523
|
}
|
|
4524
|
+
/** A Field's own key, before the form makes it unique: the author's override, else derived from the label. */
|
|
4525
|
+
function baseFieldName(props) {
|
|
4526
|
+
const own = typeof props?.name === 'string' ? props.name : '';
|
|
4527
|
+
return own || deriveFieldName(String(props?.label ?? ''));
|
|
4528
|
+
}
|
|
4529
|
+
/**
|
|
4530
|
+
* THE naming rule (vectoriox/iox-ui#43): a form's fields, in document order, each its base name made unique against
|
|
4531
|
+
* the names before it — `email`, `email2`. The spec (`collectFormSpec`), the component path's runtime and the engine's
|
|
4532
|
+
* string renderer all name a field from here, so the input's `name` and the saved spec's key can never differ.
|
|
4533
|
+
*/
|
|
4534
|
+
function uniqueFieldNames(baseNames) {
|
|
4535
|
+
const taken = new Set();
|
|
4536
|
+
return baseNames.map(base => {
|
|
4537
|
+
const name = uniqueFieldName(base, taken);
|
|
4538
|
+
taken.add(name);
|
|
4539
|
+
return name;
|
|
4540
|
+
});
|
|
4541
|
+
}
|
|
4542
|
+
/** The submitted name of every Field of a Form, in document order — `uniqueFieldNames` over its fields. */
|
|
4543
|
+
function formFieldNames(node) {
|
|
4544
|
+
if (!node || node.type !== FORM_NODE_TYPE)
|
|
4545
|
+
return [];
|
|
4546
|
+
return uniqueFieldNames(collectFieldNodes(node.children ?? []).map(f => baseFieldName(f.props)));
|
|
4547
|
+
}
|
|
4548
|
+
/** Each Field node of a Form → the name it submits under (`formFieldNames`, by node). */
|
|
4549
|
+
function formFieldNameMap(node) {
|
|
4550
|
+
const out = new Map();
|
|
4551
|
+
if (!node || node.type !== FORM_NODE_TYPE)
|
|
4552
|
+
return out;
|
|
4553
|
+
const fields = collectFieldNodes(node.children ?? []);
|
|
4554
|
+
uniqueFieldNames(fields.map(f => baseFieldName(f.props))).forEach((name, i) => out.set(fields[i], name));
|
|
4555
|
+
return out;
|
|
4556
|
+
}
|
|
4557
|
+
/**
|
|
4558
|
+
* A Field's spec from its props and the name its form gave it — ONE reading for the saved spec and the component
|
|
4559
|
+
* path's runtime (it was written twice: `IoxFieldComponent.toSpec` never carried the options source, the default or a
|
|
4560
|
+
* Date's bounds). A Date's Earliest / Latest (`earliest` / `latest`) become its `min` / `max` rules (premium N8).
|
|
4561
|
+
*/
|
|
4562
|
+
function fieldSpecOf(props, name) {
|
|
4563
|
+
const p = props ?? {};
|
|
4564
|
+
const control = p['control'] || ControlType.Text;
|
|
4565
|
+
const spec = { name, control, label: p['label'] ?? '' };
|
|
4566
|
+
if (p['required'])
|
|
4567
|
+
spec.required = true;
|
|
4568
|
+
const declared = normaliseRules(p['rules']);
|
|
4569
|
+
const bounds = control === ControlType.Date
|
|
4570
|
+
? dateBoundRules(p['earliest'], p['latest']).filter(b => !declared.some(r => r.name === b.name))
|
|
4571
|
+
: [];
|
|
4572
|
+
const rules = [...declared, ...bounds];
|
|
4573
|
+
if (rules.length)
|
|
4574
|
+
spec.rules = rules;
|
|
4575
|
+
const options = fieldOptionsOf(p);
|
|
4576
|
+
if (options.length)
|
|
4577
|
+
spec.options = options;
|
|
4578
|
+
if (p['optionsSource'])
|
|
4579
|
+
spec.optionsSource = p['optionsSource'];
|
|
4580
|
+
if (p['defaultValue'] !== undefined && p['defaultValue'] !== '')
|
|
4581
|
+
spec.defaultValue = p['defaultValue'];
|
|
4582
|
+
if (control === ControlType.Choices && p['multiple'] === true)
|
|
4583
|
+
spec.multiple = true;
|
|
4584
|
+
return spec;
|
|
4585
|
+
}
|
|
4143
4586
|
/**
|
|
4144
4587
|
* Walk a Form subtree and produce its spec.
|
|
4145
4588
|
*
|
|
@@ -4150,13 +4593,9 @@ function collectFormSpec(node) {
|
|
|
4150
4593
|
if (!node || node.type !== FORM_NODE_TYPE)
|
|
4151
4594
|
return null;
|
|
4152
4595
|
const props = node.props ?? {};
|
|
4153
|
-
const
|
|
4154
|
-
const
|
|
4155
|
-
|
|
4156
|
-
const spec = toFieldSpec(fieldNode, taken);
|
|
4157
|
-
taken.add(spec.name);
|
|
4158
|
-
fields.push(spec);
|
|
4159
|
-
}
|
|
4596
|
+
const fieldNodes = collectFieldNodes(node.children ?? []);
|
|
4597
|
+
const names = uniqueFieldNames(fieldNodes.map(f => baseFieldName(f.props)));
|
|
4598
|
+
const fields = fieldNodes.map((f, i) => fieldSpecOf(f.props, names[i]));
|
|
4160
4599
|
return {
|
|
4161
4600
|
formId: props['formId'] ?? '',
|
|
4162
4601
|
name: props['name'] ?? 'Form',
|
|
@@ -4187,31 +4626,6 @@ function collectFieldNodes(nodes) {
|
|
|
4187
4626
|
}
|
|
4188
4627
|
return out;
|
|
4189
4628
|
}
|
|
4190
|
-
function toFieldSpec(node, taken) {
|
|
4191
|
-
const props = node.props ?? {};
|
|
4192
|
-
const label = props['label'] ?? '';
|
|
4193
|
-
// The author's explicit override wins; otherwise derive from the label.
|
|
4194
|
-
const name = uniqueFieldName(props['name'] || deriveFieldName(label), taken);
|
|
4195
|
-
const spec = {
|
|
4196
|
-
name,
|
|
4197
|
-
control: props['control'] ?? ControlType.Text,
|
|
4198
|
-
label,
|
|
4199
|
-
};
|
|
4200
|
-
if (props['required'])
|
|
4201
|
-
spec.required = true;
|
|
4202
|
-
const rules = normaliseRules(props['rules']);
|
|
4203
|
-
if (rules.length)
|
|
4204
|
-
spec.rules = rules;
|
|
4205
|
-
const options = fieldOptionsOf(props);
|
|
4206
|
-
if (options.length)
|
|
4207
|
-
spec.options = options;
|
|
4208
|
-
if (props['optionsSource'])
|
|
4209
|
-
spec.optionsSource = props['optionsSource'];
|
|
4210
|
-
if (props['defaultValue'] !== undefined && props['defaultValue'] !== '') {
|
|
4211
|
-
spec.defaultValue = props['defaultValue'];
|
|
4212
|
-
}
|
|
4213
|
-
return spec;
|
|
4214
|
-
}
|
|
4215
4629
|
/** Drop anything that isn't a rule we know — a hand-edited page must not break validation. */
|
|
4216
4630
|
function normaliseRules(raw) {
|
|
4217
4631
|
if (!Array.isArray(raw))
|
|
@@ -4340,8 +4754,9 @@ const TEXTUAL = [
|
|
|
4340
4754
|
ControlType.Text, ControlType.Email, ControlType.Tel, ControlType.Url,
|
|
4341
4755
|
ControlType.Password, ControlType.Textarea,
|
|
4342
4756
|
];
|
|
4343
|
-
/** Controls with a comparable magnitude.
|
|
4344
|
-
|
|
4757
|
+
/** Controls with a comparable magnitude. A Date is not one of them: its bounds are DAYS (an ISO day or "N days from
|
|
4758
|
+
* today", `dates.ts`), set by the Field's own Earliest / Latest — a number box here read `2026-11-06` as 2026. */
|
|
4759
|
+
const NUMERIC = [ControlType.Number, ControlType.Time, ControlType.Range];
|
|
4345
4760
|
const RULE_DESCRIPTORS = Object.freeze([
|
|
4346
4761
|
{
|
|
4347
4762
|
name: RuleName.MinLength, labelKey: 'forms.rules.minLength',
|
|
@@ -4528,12 +4943,16 @@ function parseClientFormSpec(raw) {
|
|
|
4528
4943
|
return null;
|
|
4529
4944
|
}
|
|
4530
4945
|
}
|
|
4531
|
-
/**
|
|
4532
|
-
*
|
|
4533
|
-
*
|
|
4946
|
+
/**
|
|
4947
|
+
* While its form sends, a submit button is busy, and disabled unless its author turned that off; once the form has
|
|
4948
|
+
* SENT it stays disabled, reading "Sent" with its check — nothing sends a lead twice (premium N2, the success in place).
|
|
4949
|
+
* ONE rule for the canvas's Sending / Success states, the component path and the string path. Its labels are the shared
|
|
4950
|
+
* CSS's, keyed on the same `data-iox-state`.
|
|
4951
|
+
*/
|
|
4534
4952
|
function submitButtonView(state, disableWhileSubmitting = true) {
|
|
4535
4953
|
const busy = state === FormRenderState.Submitting;
|
|
4536
|
-
|
|
4954
|
+
const sent = state === FormRenderState.Success;
|
|
4955
|
+
return { busy, disabled: sent || (busy && disableWhileSubmitting !== false) };
|
|
4537
4956
|
}
|
|
4538
4957
|
/** On a submit button whose author turned "Disable while sending" off. */
|
|
4539
4958
|
const SUBMIT_KEEP_ENABLED_ATTRIBUTE = 'data-iox-keep-enabled';
|
|
@@ -4542,17 +4961,30 @@ var SubmitLabelClass;
|
|
|
4542
4961
|
SubmitLabelClass["Labels"] = "iox-submit__labels";
|
|
4543
4962
|
SubmitLabelClass["Idle"] = "iox-submit__idle";
|
|
4544
4963
|
SubmitLabelClass["Busy"] = "iox-submit__busy";
|
|
4964
|
+
SubmitLabelClass["Sent"] = "iox-submit__sent";
|
|
4965
|
+
SubmitLabelClass["Check"] = "iox-submit__check";
|
|
4545
4966
|
})(SubmitLabelClass || (SubmitLabelClass = {}));
|
|
4546
|
-
/**
|
|
4547
|
-
*
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4967
|
+
/** The check the Sent label draws once (premium N2 — the rare-tier beat, Emil 135/136): one stroke, `pathLength` 100 so
|
|
4968
|
+
* the shared CSS draws it by `stroke-dashoffset` whatever its real length. */
|
|
4969
|
+
const SUBMIT_CHECK_SVG = `<svg class="${SubmitLabelClass.Check}" width="16" height="16" viewBox="0 0 24 24" aria-hidden="true" focusable="false">` +
|
|
4970
|
+
'<path d="M5 12.5l4.5 4.5L19 7.5" pathLength="100" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
4971
|
+
/** "Sent" in the site's language, for a button whose author wrote no Sent words (a string-rendered site has no
|
|
4972
|
+
* translation service — the document's `lang` picks, like the date picker's words). */
|
|
4973
|
+
const SENT_WORDS = Object.freeze({ en: 'Sent', he: 'נשלח' });
|
|
4974
|
+
function sentWord(lang) {
|
|
4975
|
+
const language = String(lang ?? '').toLowerCase().split(/[-_]/)[0];
|
|
4976
|
+
return SENT_WORDS[language === 'iw' ? 'he' : language] ?? SENT_WORDS['en'];
|
|
4977
|
+
}
|
|
4978
|
+
/**
|
|
4979
|
+
* A submit button's words, as markup: its three labels stacked in one grid cell (the button keeps the longest one's
|
|
4980
|
+
* width and never jumps), each shown by the form's state through the shared CSS — swapped in place behind a 2px blur
|
|
4981
|
+
* (Emil 87/139). The string renderer's copy of `IoxSubmitButtonComponent`'s template; `escape` is the renderer's.
|
|
4982
|
+
*/
|
|
4983
|
+
function submitButtonLabelsHtml(labels, escape) {
|
|
4984
|
+
const busy = labels.busy || labels.idle;
|
|
4985
|
+
return `<span class="${SubmitLabelClass.Labels}"><span class="${SubmitLabelClass.Idle}">${escape(labels.idle)}</span>` +
|
|
4986
|
+
`<span class="${SubmitLabelClass.Busy}">${escape(busy)}</span>` +
|
|
4987
|
+
`<span class="${SubmitLabelClass.Sent}">${SUBMIT_CHECK_SVG}<span>${escape(labels.sent)}</span></span></span>`;
|
|
4556
4988
|
}
|
|
4557
4989
|
const FORM_HONEYPOT_CLASS = 'iox-form__hp';
|
|
4558
4990
|
const FORM_ANNOUNCER_ATTRIBUTE = 'data-iox-form-announcer';
|
|
@@ -4580,7 +5012,14 @@ var FormPresetId;
|
|
|
4580
5012
|
FormPresetId["Newsletter"] = "newsletter";
|
|
4581
5013
|
FormPresetId["Application"] = "application";
|
|
4582
5014
|
FormPresetId["Booking"] = "booking";
|
|
5015
|
+
/** Premium N2: modelled on Doron's iox.studio brief — name, a phone that is also WhatsApp, the business, what they
|
|
5016
|
+
* need and a budget as chips, Send with WhatsApp beside it. */
|
|
5017
|
+
FormPresetId["Lead"] = "lead";
|
|
4583
5018
|
})(FormPresetId || (FormPresetId = {}));
|
|
5019
|
+
/** The presets a generated plan may name (the plan wire's `form` enum) — every preset not held back by `inPlan`. */
|
|
5020
|
+
function planFormPresetIds() {
|
|
5021
|
+
return FORM_PRESETS.filter(p => p.inPlan !== false).map(p => p.id);
|
|
5022
|
+
}
|
|
4584
5023
|
// forms-core is imported by both renderers (the field runtime), and a renderer that never reads the presets
|
|
4585
5024
|
|
|
4586
5025
|
// to it by form-presets.spec) — a read of `FormPresetId.X` counts as a side effect to a bundler and kept the
|
|
@@ -4636,7 +5075,8 @@ const FORM_PRESETS = /* @__PURE__ */ Object.freeze([
|
|
|
4636
5075
|
fields: [
|
|
4637
5076
|
{ label: 'Name', control: 'text', required: true },
|
|
4638
5077
|
{ label: 'Email', control: 'email', required: true },
|
|
4639
|
-
|
|
5078
|
+
// The first day a booking may ask for is today (N8): the calendar refuses the past.
|
|
5079
|
+
{ label: 'Date', control: 'date', required: true, earliest: '0' },
|
|
4640
5080
|
{ label: 'People', control: 'number', required: true },
|
|
4641
5081
|
{ label: 'Notes', control: 'textarea', rows: 3, help: 'Allergies, access needs, anything useful' },
|
|
4642
5082
|
],
|
|
@@ -4644,6 +5084,27 @@ const FORM_PRESETS = /* @__PURE__ */ Object.freeze([
|
|
|
4644
5084
|
successText: 'Request received — we’ll confirm by email.',
|
|
4645
5085
|
errorText: 'We couldn’t send your request. Please try again.',
|
|
4646
5086
|
},
|
|
5087
|
+
{
|
|
5088
|
+
id: 'lead', name: 'Lead', icon: 'ph-thin ph-handshake',
|
|
5089
|
+
description: 'Name, phone, the business and what they need, as chips. Stored, with WhatsApp beside Send.',
|
|
5090
|
+
formName: 'Lead form',
|
|
5091
|
+
fields: [
|
|
5092
|
+
{ label: 'Name', control: 'text', required: true },
|
|
5093
|
+
{ label: 'Phone / WhatsApp', control: 'tel', required: true },
|
|
5094
|
+
{ label: 'What is your business?', control: 'text' },
|
|
5095
|
+
// The words ARE the values: what arrives in the inbox and the email reads as the visitor chose it.
|
|
5096
|
+
{ label: 'What do you need?', control: 'choices', multiple: true,
|
|
5097
|
+
optionsText: 'Website\nBranding\nPhotos & video\nNot sure yet' },
|
|
5098
|
+
// Neutral sizes the author renames — never a price or a currency a studio could be held to.
|
|
5099
|
+
{ label: 'Estimated budget', control: 'choices',
|
|
5100
|
+
optionsText: 'Small project\nMedium\nLarge\nNot sure yet' },
|
|
5101
|
+
],
|
|
5102
|
+
submitLabel: 'Send',
|
|
5103
|
+
successText: 'Thanks — we’ll get back to you soon.',
|
|
5104
|
+
errorText: 'We couldn’t send your details. Please try again — your answers are still here.',
|
|
5105
|
+
channel: { kind: 'whatsapp', label: 'Message us on WhatsApp' },
|
|
5106
|
+
inPlan: false,
|
|
5107
|
+
},
|
|
4647
5108
|
]);
|
|
4648
5109
|
/** The preset with this id, or undefined. */
|
|
4649
5110
|
function formPresetById(id) {
|
|
@@ -4724,6 +5185,222 @@ function findFormContentInHosts(roots) {
|
|
|
4724
5185
|
return out;
|
|
4725
5186
|
}
|
|
4726
5187
|
|
|
5188
|
+
/**
|
|
5189
|
+
* THE SECOND CHANNEL (premium components N2) — "or message us on WhatsApp", the button a form keeps beside its submit,
|
|
5190
|
+
* before AND after it sends.
|
|
5191
|
+
*
|
|
5192
|
+
* It is an ordinary node in the form's actions row (a ButtonBlock that links) carrying the ROLE `form.channel`, so the
|
|
5193
|
+
* shared CSS keeps it on screen when the form succeeds and the Publishable gate can find it. Its address is the
|
|
5194
|
+
* OWNER's fact: the builder builds it from what the author types (a number, an address, a link) and never invents one —
|
|
5195
|
+
* inserted empty, it blocks publishing until it is filled (generation-core `auditSite`, `empty-channel`).
|
|
5196
|
+
*
|
|
5197
|
+
* Framework-free, like the rest of forms-core.
|
|
5198
|
+
*/
|
|
5199
|
+
/** The role the channel's node carries (`iox-role-form-channel`). */
|
|
5200
|
+
const FORM_CHANNEL_ROLE = 'form.channel';
|
|
5201
|
+
/** The role of the row that holds the submit button and the channel (`iox-role-form-actions`). */
|
|
5202
|
+
const FORM_ACTIONS_ROLE = 'form.actions';
|
|
5203
|
+
var ChannelKind;
|
|
5204
|
+
(function (ChannelKind) {
|
|
5205
|
+
ChannelKind["WhatsApp"] = "whatsapp";
|
|
5206
|
+
ChannelKind["Phone"] = "phone";
|
|
5207
|
+
ChannelKind["Email"] = "email";
|
|
5208
|
+
ChannelKind["Link"] = "link";
|
|
5209
|
+
})(ChannelKind || (ChannelKind = {}));
|
|
5210
|
+
const CHANNEL_KINDS = [ChannelKind.WhatsApp, ChannelKind.Phone, ChannelKind.Email, ChannelKind.Link];
|
|
5211
|
+
const WHATSAPP_PREFIX = 'https://wa.me/';
|
|
5212
|
+
const TEL_PREFIX = 'tel:';
|
|
5213
|
+
const MAILTO_PREFIX = 'mailto:';
|
|
5214
|
+
const HTTP = /^https?:\/\//i;
|
|
5215
|
+
const EMAIL = /^[^\s@]+@[^\s@.]+(\.[^\s@.]+)+$/;
|
|
5216
|
+
/** A phone number's own characters; anything else is typing noise (spaces, dashes, brackets). */
|
|
5217
|
+
const DIGITS = /\d/g;
|
|
5218
|
+
/** WhatsApp's click-to-chat wants the full international number: digits only, no `+`, no leading `00`. */
|
|
5219
|
+
const INTERNATIONAL_00 = /^00/;
|
|
5220
|
+
/** A number shorter than this is not a number anyone can be reached on. */
|
|
5221
|
+
const MIN_DIGITS = 6;
|
|
5222
|
+
/**
|
|
5223
|
+
* The address a channel links to, built from what the author typed — or `''` when it gives nothing a visitor could
|
|
5224
|
+
* use (no digits, not an email). Never a guess: a WhatsApp number keeps exactly the digits typed (the panel asks for
|
|
5225
|
+
* the country code), a link without a scheme gets `https://`.
|
|
5226
|
+
*/
|
|
5227
|
+
function channelHref(kind, input) {
|
|
5228
|
+
const raw = typeof input === 'string' ? input.trim() : '';
|
|
5229
|
+
if (!raw)
|
|
5230
|
+
return '';
|
|
5231
|
+
switch (kind) {
|
|
5232
|
+
case ChannelKind.WhatsApp: {
|
|
5233
|
+
const digits = (raw.match(DIGITS) ?? []).join('').replace(INTERNATIONAL_00, '');
|
|
5234
|
+
return digits.length >= MIN_DIGITS ? `${WHATSAPP_PREFIX}${digits}` : '';
|
|
5235
|
+
}
|
|
5236
|
+
case ChannelKind.Phone: {
|
|
5237
|
+
const digits = (raw.match(DIGITS) ?? []).join('');
|
|
5238
|
+
if (digits.length < MIN_DIGITS)
|
|
5239
|
+
return '';
|
|
5240
|
+
return `${TEL_PREFIX}${raw.startsWith('+') ? '+' : ''}${digits}`;
|
|
5241
|
+
}
|
|
5242
|
+
case ChannelKind.Email:
|
|
5243
|
+
return EMAIL.test(raw) ? `${MAILTO_PREFIX}${raw}` : '';
|
|
5244
|
+
case ChannelKind.Link:
|
|
5245
|
+
return HTTP.test(raw) ? raw : `https://${raw.replace(/^\/+/, '')}`;
|
|
5246
|
+
default:
|
|
5247
|
+
return '';
|
|
5248
|
+
}
|
|
5249
|
+
}
|
|
5250
|
+
/** A channel's address back into what the panel shows: its kind and the value the author typed. */
|
|
5251
|
+
function parseChannelHref(href) {
|
|
5252
|
+
const text = typeof href === 'string' ? href.trim() : '';
|
|
5253
|
+
if (text.toLowerCase().startsWith(WHATSAPP_PREFIX))
|
|
5254
|
+
return { kind: ChannelKind.WhatsApp, value: text.slice(WHATSAPP_PREFIX.length).split(/[?#]/)[0] };
|
|
5255
|
+
if (text.toLowerCase().startsWith(TEL_PREFIX))
|
|
5256
|
+
return { kind: ChannelKind.Phone, value: text.slice(TEL_PREFIX.length) };
|
|
5257
|
+
if (text.toLowerCase().startsWith(MAILTO_PREFIX))
|
|
5258
|
+
return { kind: ChannelKind.Email, value: text.slice(MAILTO_PREFIX.length).split('?')[0] };
|
|
5259
|
+
if (!text)
|
|
5260
|
+
return { kind: ChannelKind.WhatsApp, value: '' };
|
|
5261
|
+
return { kind: ChannelKind.Link, value: text };
|
|
5262
|
+
}
|
|
5263
|
+
/** True when the channel goes somewhere a visitor can use — the gate's "filled". */
|
|
5264
|
+
function isChannelFilled(href) {
|
|
5265
|
+
const { kind, value } = parseChannelHref(href);
|
|
5266
|
+
return !!value && channelHref(kind, value) !== '';
|
|
5267
|
+
}
|
|
5268
|
+
/** A WhatsApp or web channel opens in a new tab; a call or an email hands over to the device. */
|
|
5269
|
+
function channelOpensNewTab(kind) {
|
|
5270
|
+
return kind === ChannelKind.WhatsApp || kind === ChannelKind.Link;
|
|
5271
|
+
}
|
|
5272
|
+
|
|
5273
|
+
/**
|
|
5274
|
+
* A Field as MARKUP — the string renderer's copy of `IoxFieldComponent`'s template, in ONE place (it lived in the
|
|
5275
|
+
* engine's `layout-renderer.service.ts`, a second copy beside the component). The engine's string path writes it; the
|
|
5276
|
+
* component mirrors it element for element, so hydration replaces like with like and the shared CSS (iox-forms.css,
|
|
5277
|
+
* the contract's field baseline) reaches both.
|
|
5278
|
+
*
|
|
5279
|
+
* Choices (premium N1): a `<fieldset>` whose `<legend>` is the label, its options native radios (one answer — the
|
|
5280
|
+
* arrow keys move and pick, with no script) or checkboxes (several), each inside a `<label>` that IS the chip; the
|
|
5281
|
+
* input is visually hidden, never removed. Date (N8): a `<button>` that says the date in words and opens the look's
|
|
5282
|
+
* calendar, with a hidden input carrying the ISO day — the raw SSR is the final markup (nothing shifts on hydrate).
|
|
5283
|
+
*
|
|
5284
|
+
* Framework-free, like the rest of forms-core: `escape` is the caller's.
|
|
5285
|
+
*/
|
|
5286
|
+
/** The classes the markup carries — the runtimes and the shared CSS key on these, never on a literal. */
|
|
5287
|
+
var FieldClass;
|
|
5288
|
+
(function (FieldClass) {
|
|
5289
|
+
FieldClass["Root"] = "iox-field";
|
|
5290
|
+
FieldClass["Label"] = "iox-field__label";
|
|
5291
|
+
FieldClass["LabelHidden"] = "iox-field__label--hidden";
|
|
5292
|
+
FieldClass["Required"] = "iox-field__required";
|
|
5293
|
+
FieldClass["Control"] = "iox-field__control";
|
|
5294
|
+
FieldClass["Help"] = "iox-field__help";
|
|
5295
|
+
FieldClass["Error"] = "iox-field__error";
|
|
5296
|
+
FieldClass["Group"] = "iox-field__group";
|
|
5297
|
+
FieldClass["Option"] = "iox-field__option";
|
|
5298
|
+
FieldClass["Choices"] = "iox-field__choices";
|
|
5299
|
+
FieldClass["ChoicesChips"] = "iox-field__choices--chips";
|
|
5300
|
+
FieldClass["ChoicesList"] = "iox-field__choices--list";
|
|
5301
|
+
FieldClass["Chips"] = "iox-field__chips";
|
|
5302
|
+
FieldClass["Chip"] = "iox-field__chip";
|
|
5303
|
+
FieldClass["ChipInput"] = "iox-field__control--chip";
|
|
5304
|
+
FieldClass["DateButton"] = "iox-field__control--date";
|
|
5305
|
+
FieldClass["DateValue"] = "iox-field__date-value";
|
|
5306
|
+
FieldClass["DateIcon"] = "iox-field__date-icon";
|
|
5307
|
+
FieldClass["DateInput"] = "iox-field__date-input";
|
|
5308
|
+
})(FieldClass || (FieldClass = {}));
|
|
5309
|
+
/** On the date button: the raw Earliest / Latest (`'0'`, an ISO day), resolved by the visitor's own today. */
|
|
5310
|
+
const DATE_MIN_ATTRIBUTE = 'data-iox-date-min';
|
|
5311
|
+
const DATE_MAX_ATTRIBUTE = 'data-iox-date-max';
|
|
5312
|
+
/** On the date's words while no day is chosen (the placeholder's colour keys on it). */
|
|
5313
|
+
const DATE_EMPTY_ATTRIBUTE = 'data-iox-empty';
|
|
5314
|
+
/** The calendar glyph inside the date button: one path set, drawn in currentColor, never announced. */
|
|
5315
|
+
const DATE_ICON_SVG = '<svg class="iox-field__date-icon" width="20" height="20" viewBox="0 0 24 24" aria-hidden="true" focusable="false">' +
|
|
5316
|
+
'<rect x="3.5" y="5" width="17" height="15.5" rx="2.5" fill="none" stroke="currentColor" stroke-width="1.6"/>' +
|
|
5317
|
+
'<path d="M3.5 10h17M8 3v4M16 3v4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>';
|
|
5318
|
+
/** The controls that are a plain `<input type=…>`; anything else unknown is a text input. */
|
|
5319
|
+
const INPUT_TYPES = new Set(['email', 'tel', 'url', 'number', 'password', 'time', 'file', 'hidden']);
|
|
5320
|
+
/** The `<input type>` of a control that renders as a plain input. */
|
|
5321
|
+
function inputTypeOf(control) {
|
|
5322
|
+
return INPUT_TYPES.has(control) ? control : 'text';
|
|
5323
|
+
}
|
|
5324
|
+
/** A Choices field's appearance; anything unknown reads as chips (the default). */
|
|
5325
|
+
function choiceAppearanceOf(props) {
|
|
5326
|
+
return props?.['appearance'] === ChoiceAppearance.List ? ChoiceAppearance.List : ChoiceAppearance.Chips;
|
|
5327
|
+
}
|
|
5328
|
+
/** A Field's whole element, as the string renderer writes it. */
|
|
5329
|
+
function fieldHtml(props, options) {
|
|
5330
|
+
const p = props ?? {};
|
|
5331
|
+
const { name, id, escape: esc } = options;
|
|
5332
|
+
const control = String(p['control'] ?? ControlType.Text);
|
|
5333
|
+
const required = !!p['required'];
|
|
5334
|
+
const help = p['help'] ? `<p class="${FieldClass.Help}" id="${id}-help">${esc(String(p['help']))}</p>` : '';
|
|
5335
|
+
const describedBy = p['help'] ? ` aria-describedby="${id}-help"` : '';
|
|
5336
|
+
const labelClass = p['labelHidden'] ? `${FieldClass.Label} ${FieldClass.LabelHidden}` : FieldClass.Label;
|
|
5337
|
+
const mark = required ? `<span class="${FieldClass.Required}" aria-hidden="true">*</span>` : '';
|
|
5338
|
+
const text = esc(String(p['label'] ?? ''));
|
|
5339
|
+
const label = `<label class="${labelClass}" id="${id}-label" for="${id}">${text}${mark}</label>`;
|
|
5340
|
+
const hidden = control === ControlType.Hidden ? ' hidden' : '';
|
|
5341
|
+
const open = `<div class="${FieldClass.Root} ${options.cls}" data-iox-field="${esc(name)}"${hidden}${options.attrs ?? ''}>`;
|
|
5342
|
+
if (control === ControlType.Choices) {
|
|
5343
|
+
return `${open}${choicesHtml(p, options, `<legend class="${labelClass}" id="${id}-label">${text}${mark}</legend>`, describedBy)}${help}</div>`;
|
|
5344
|
+
}
|
|
5345
|
+
return `${open}${label}${controlHtml(p, control, options, describedBy)}${help}</div>`;
|
|
5346
|
+
}
|
|
5347
|
+
function controlHtml(p, control, options, describedBy) {
|
|
5348
|
+
const { id, escape: esc } = options;
|
|
5349
|
+
const name = esc(options.name);
|
|
5350
|
+
const required = p['required'] ? ' required' : '';
|
|
5351
|
+
const placeholder = p['placeholder'] ? ` placeholder="${esc(String(p['placeholder']))}"` : '';
|
|
5352
|
+
const choices = fieldOptionsOf(p);
|
|
5353
|
+
const option = (o) => `<option value="${esc(o.value)}">${esc(o.label)}</option>`;
|
|
5354
|
+
const num = (v, d) => (Number.isFinite(Number(v)) && v !== '' && v !== null ? Number(v) : d);
|
|
5355
|
+
switch (control) {
|
|
5356
|
+
case ControlType.Textarea:
|
|
5357
|
+
return `<textarea class="${FieldClass.Control}" id="${id}" name="${name}" rows="${num(p['rows'], 4)}"${placeholder}${required}${describedBy}></textarea>`;
|
|
5358
|
+
case ControlType.Select:
|
|
5359
|
+
return `<select class="${FieldClass.Control}" id="${id}" name="${name}"${required}${describedBy}>` +
|
|
5360
|
+
`<option value="">${esc(String(p['placeholder'] ?? ''))}</option>${choices.map(option).join('')}</select>`;
|
|
5361
|
+
case ControlType.Switch:
|
|
5362
|
+
return `<input class="${FieldClass.Control} iox-field__control--switch" type="checkbox" role="switch" id="${id}" name="${name}"${describedBy}>`;
|
|
5363
|
+
case ControlType.Range:
|
|
5364
|
+
return `<input class="${FieldClass.Control} iox-field__control--range" type="range" id="${id}" name="${name}" ` +
|
|
5365
|
+
`min="${num(p['rangeMin'], 0)}" max="${num(p['rangeMax'], 100)}" step="${num(p['rangeStep'], 1)}"${describedBy}>`;
|
|
5366
|
+
case ControlType.Radio:
|
|
5367
|
+
return `<div class="${FieldClass.Group}" role="radiogroup" aria-labelledby="${id}-label">` +
|
|
5368
|
+
choices.map(o => `<label class="${FieldClass.Option}"><input class="${FieldClass.Control} iox-field__control--radio" type="radio" name="${name}" ` +
|
|
5369
|
+
`value="${esc(o.value)}"><span>${esc(o.label)}</span></label>`).join('') + `</div>`;
|
|
5370
|
+
case ControlType.Checkbox:
|
|
5371
|
+
return `<input class="${FieldClass.Control} iox-field__control--checkbox" type="checkbox" id="${id}" name="${name}"${required}${describedBy}>`;
|
|
5372
|
+
case ControlType.Date:
|
|
5373
|
+
return dateHtml(p, options, describedBy);
|
|
5374
|
+
default:
|
|
5375
|
+
return `<input class="${FieldClass.Control}" id="${id}" name="${name}" type="${inputTypeOf(control)}"${placeholder}${required}${describedBy}>`;
|
|
5376
|
+
}
|
|
5377
|
+
}
|
|
5378
|
+
/** A Choices field's fieldset: the legend, then the options as chips or as a list. */
|
|
5379
|
+
function choicesHtml(p, options, legend, describedBy) {
|
|
5380
|
+
const { escape: esc } = options;
|
|
5381
|
+
const name = esc(options.name);
|
|
5382
|
+
const multiple = p['multiple'] === true;
|
|
5383
|
+
const type = multiple ? 'checkbox' : 'radio';
|
|
5384
|
+
const chips = choiceAppearanceOf(p) === ChoiceAppearance.Chips;
|
|
5385
|
+
const items = fieldOptionsOf(p).map(o => chips
|
|
5386
|
+
? `<label class="${FieldClass.Chip}"><input class="${FieldClass.Control} ${FieldClass.ChipInput}" type="${type}" name="${name}" value="${esc(o.value)}"><span>${esc(o.label)}</span></label>`
|
|
5387
|
+
: `<label class="${FieldClass.Option}"><input class="${FieldClass.Control}" type="${type}" name="${name}" value="${esc(o.value)}"><span>${esc(o.label)}</span></label>`).join('');
|
|
5388
|
+
const look = chips ? FieldClass.ChoicesChips : FieldClass.ChoicesList;
|
|
5389
|
+
const row = chips ? FieldClass.Chips : FieldClass.Group;
|
|
5390
|
+
return `<fieldset class="${FieldClass.Choices} ${look}"${describedBy}>${legend}<div class="${row}">${items}</div></fieldset>`;
|
|
5391
|
+
}
|
|
5392
|
+
/** The date picker's button + hidden input. The calendar itself is the runtime's, opened on press. */
|
|
5393
|
+
function dateHtml(p, options, describedBy) {
|
|
5394
|
+
const { id, escape: esc } = options;
|
|
5395
|
+
const words = datePickerWords(options.lang);
|
|
5396
|
+
const empty = esc(String(p['placeholder'] || words.choose));
|
|
5397
|
+
const bound = (attr, raw) => (typeof raw === 'string' && raw.trim() !== '') || typeof raw === 'number' ? ` ${attr}="${esc(String(raw).trim())}"` : '';
|
|
5398
|
+
return `<button type="button" class="${FieldClass.Control} ${FieldClass.DateButton}" id="${id}" aria-haspopup="dialog" aria-expanded="false" ` +
|
|
5399
|
+
`aria-labelledby="${id}-label ${id}-value"${describedBy}${bound(DATE_MIN_ATTRIBUTE, p['earliest'])}${bound(DATE_MAX_ATTRIBUTE, p['latest'])}>` +
|
|
5400
|
+
`<span class="${FieldClass.DateValue}" id="${id}-value" ${DATE_EMPTY_ATTRIBUTE}>${empty}</span>${DATE_ICON_SVG}</button>` +
|
|
5401
|
+
`<input type="hidden" class="${FieldClass.DateInput}" name="${esc(options.name)}" value="">`;
|
|
5402
|
+
}
|
|
5403
|
+
|
|
4727
5404
|
/*
|
|
4728
5405
|
* @vectoriox/iox-ui/forms-core — the shared, FRAMEWORK-FREE core of the forms feature.
|
|
4729
5406
|
*
|
|
@@ -4758,7 +5435,7 @@ const defaultMessageResolver = (key, params) => {
|
|
|
4758
5435
|
*/
|
|
4759
5436
|
class FormRuntimeService {
|
|
4760
5437
|
constructor() {
|
|
4761
|
-
/** Registered fields, in DOM order — the order the spec and focus-first-invalid depend on. */
|
|
5438
|
+
/** Registered fields, in DOM order — the order the spec, the unique names and focus-first-invalid depend on. */
|
|
4762
5439
|
this.fields = [];
|
|
4763
5440
|
this._state = INITIAL_FORM_STATE;
|
|
4764
5441
|
this._spec = null;
|
|
@@ -4784,19 +5461,29 @@ class FormRuntimeService {
|
|
|
4784
5461
|
this.fields.splice(index, 1);
|
|
4785
5462
|
};
|
|
4786
5463
|
}
|
|
5464
|
+
/** Every registered field's submitted name, in order — unique within the form (iox-ui#43). */
|
|
5465
|
+
names() {
|
|
5466
|
+
return uniqueFieldNames(this.fields.map(f => (f.baseName ? f.baseName() : f.spec('').name)));
|
|
5467
|
+
}
|
|
5468
|
+
/** The name the form gives one registered field, or null when it is not this form's. */
|
|
5469
|
+
nameOf(field) {
|
|
5470
|
+
const index = this.fields.indexOf(field);
|
|
5471
|
+
return index < 0 ? null : this.names()[index];
|
|
5472
|
+
}
|
|
4787
5473
|
/** The live spec, derived from what is registered right now. */
|
|
4788
5474
|
specNow() {
|
|
4789
5475
|
const base = this._spec ?? {
|
|
4790
5476
|
formId: '', name: '', feedback: FeedbackTiming.Smart,
|
|
4791
5477
|
fields: [], actions: [], antiSpam: { honeypot: true, minFillMs: 1200 },
|
|
4792
5478
|
};
|
|
4793
|
-
|
|
5479
|
+
const names = this.names();
|
|
5480
|
+
return { ...base, fields: this.fields.map((f, i) => ({ ...f.spec(names[i]), name: names[i] })) };
|
|
4794
5481
|
}
|
|
4795
5482
|
/** Current values, keyed by submitted field name. */
|
|
4796
5483
|
values() {
|
|
4797
5484
|
const out = {};
|
|
4798
|
-
|
|
4799
|
-
|
|
5485
|
+
const names = this.names();
|
|
5486
|
+
this.fields.forEach((field, i) => { out[names[i]] = field.getValue(); });
|
|
4800
5487
|
return out;
|
|
4801
5488
|
}
|
|
4802
5489
|
onChange(name) {
|
|
@@ -4829,8 +5516,7 @@ class FormRuntimeService {
|
|
|
4829
5516
|
}
|
|
4830
5517
|
/** The first invalid field in registration (DOM) order — what submit should focus. */
|
|
4831
5518
|
firstInvalidName() {
|
|
4832
|
-
for (const
|
|
4833
|
-
const name = field.spec().name;
|
|
5519
|
+
for (const name of this.names()) {
|
|
4834
5520
|
if (this._state.errors[name])
|
|
4835
5521
|
return name;
|
|
4836
5522
|
}
|
|
@@ -4843,6 +5529,45 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
|
|
|
4843
5529
|
type: Injectable
|
|
4844
5530
|
}] });
|
|
4845
5531
|
|
|
5532
|
+
/**
|
|
5533
|
+
* The shake a failing field gives after a refused submit (premium components N2) — Emil 134: the named effect for
|
|
5534
|
+
* rejected input. ONE function for the component path, the string path and the canvas (a field put in its error state
|
|
5535
|
+
* on the canvas shakes once, so the author sees the motion they are designing).
|
|
5536
|
+
*
|
|
5537
|
+
* WAAPI, `composite: 'add'`, so it moves the field from wherever its own transform puts it. A second refused submit
|
|
5538
|
+
* CANCELS the running shake and starts clean — never two stacked (Emil 52/62). Under reduced motion there is no shake:
|
|
5539
|
+
* focus and the message still happen (Emil 192 — the meaning survives without the movement).
|
|
5540
|
+
*/
|
|
5541
|
+
/** ±6 → ±4 → rest: a decaying side-to-side (the prototype's, Doron-approved). */
|
|
5542
|
+
const SHAKE_KEYFRAMES = Object.freeze([
|
|
5543
|
+
{ transform: 'translateX(0px)' },
|
|
5544
|
+
{ transform: 'translateX(-6px)' },
|
|
5545
|
+
{ transform: 'translateX(6px)' },
|
|
5546
|
+
{ transform: 'translateX(-4px)' },
|
|
5547
|
+
{ transform: 'translateX(4px)' },
|
|
5548
|
+
{ transform: 'translateX(0px)' },
|
|
5549
|
+
]);
|
|
5550
|
+
/** Under Emil's UI budget (28) and long enough to read as "no" (134). */
|
|
5551
|
+
const SHAKE_DURATION_MS = 320;
|
|
5552
|
+
/** Emil 18 — the strong ease-out. */
|
|
5553
|
+
const SHAKE_EASING = 'cubic-bezier(0.23, 1, 0.32, 1)';
|
|
5554
|
+
const RUNNING = '__ioxShake';
|
|
5555
|
+
/** Shake `el` once; returns the animation, or null when nothing plays (no element, no WAAPI, reduced motion). */
|
|
5556
|
+
function shakeElement(el, reducedMotion = prefersReducedMotion()) {
|
|
5557
|
+
if (!el)
|
|
5558
|
+
return null;
|
|
5559
|
+
const host = el;
|
|
5560
|
+
host[RUNNING]?.cancel();
|
|
5561
|
+
delete host[RUNNING];
|
|
5562
|
+
if (reducedMotion || typeof el.animate !== 'function')
|
|
5563
|
+
return null;
|
|
5564
|
+
const animation = el.animate(SHAKE_KEYFRAMES, { duration: SHAKE_DURATION_MS, easing: SHAKE_EASING, composite: 'add' });
|
|
5565
|
+
host[RUNNING] = animation;
|
|
5566
|
+
animation.onfinish = () => { if (host[RUNNING] === animation)
|
|
5567
|
+
delete host[RUNNING]; };
|
|
5568
|
+
return animation;
|
|
5569
|
+
}
|
|
5570
|
+
|
|
4846
5571
|
const SUBMIT_BUTTON_SELECTOR = '.iox-submit';
|
|
4847
5572
|
const FORM_SELECTOR = 'form, .iox-form';
|
|
4848
5573
|
/**
|
|
@@ -4963,6 +5688,9 @@ class IoxFormComponent {
|
|
|
4963
5688
|
// Never let the browser navigate: a full page POST would lose the designed Success state
|
|
4964
5689
|
// and every value the visitor typed.
|
|
4965
5690
|
event.preventDefault();
|
|
5691
|
+
// Sent is final (premium N2): the button reads "Sent" and is disabled — an Enter in a field sends nothing twice.
|
|
5692
|
+
if (this.renderState === FormRenderState.Success)
|
|
5693
|
+
return;
|
|
4966
5694
|
this.runtime.configure(this.runtime.specNow(), this.feedback);
|
|
4967
5695
|
if (!this.runtime.validateAll()) {
|
|
4968
5696
|
this.announce(this.runtime.state.errors);
|
|
@@ -5051,6 +5779,7 @@ class IoxFormComponent {
|
|
|
5051
5779
|
// A microtask is enough for the empty value to be committed before the new one lands.
|
|
5052
5780
|
Promise.resolve().then(() => { this.announcement = next; });
|
|
5053
5781
|
}
|
|
5782
|
+
/** Focus the first field that failed and shake it once (premium N2 — Emil 134; none under reduced motion). */
|
|
5054
5783
|
focusFirstInvalid() {
|
|
5055
5784
|
const name = this.runtime.firstInvalidName();
|
|
5056
5785
|
if (!name)
|
|
@@ -5063,6 +5792,7 @@ class IoxFormComponent {
|
|
|
5063
5792
|
if (element.getAttribute('data-iox-field') !== name)
|
|
5064
5793
|
continue;
|
|
5065
5794
|
element.querySelector('.iox-field__control')?.focus();
|
|
5795
|
+
shakeElement(element);
|
|
5066
5796
|
return;
|
|
5067
5797
|
}
|
|
5068
5798
|
}
|
|
@@ -5138,6 +5868,437 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
|
|
|
5138
5868
|
args: ['childrenHost', { read: ViewContainerRef, static: true }]
|
|
5139
5869
|
}] } });
|
|
5140
5870
|
|
|
5871
|
+
/**
|
|
5872
|
+
* placeFloating — where a panel anchored to a control goes (premium components N8, reused by M5's tooltip and popover).
|
|
5873
|
+
*
|
|
5874
|
+
* ONE pure decision for every floating panel: under its anchor, aligned to the anchor's reading start (the left edge in
|
|
5875
|
+
* LTR, the right in RTL); above it when there is no room below and more above; held inside the viewport with a margin;
|
|
5876
|
+
* and the transform-origin AT the anchor, so the panel grows from the control that opened it (Emil 47) — even when the
|
|
5877
|
+
* clamp moved it sideways.
|
|
5878
|
+
*
|
|
5879
|
+
* Lives in the main entry: nothing server-side places a panel, and the date picker (now) and the floating family (M5)
|
|
5880
|
+
* are DOM runtimes of the main entry. Pure — no DOM read here: the caller measures, this decides, the caller writes.
|
|
5881
|
+
*/
|
|
5882
|
+
var FloatingSide;
|
|
5883
|
+
(function (FloatingSide) {
|
|
5884
|
+
FloatingSide["Below"] = "below";
|
|
5885
|
+
FloatingSide["Above"] = "above";
|
|
5886
|
+
})(FloatingSide || (FloatingSide = {}));
|
|
5887
|
+
/** Emil's popover spacing: close enough to read as the control's, clear of its focus ring. */
|
|
5888
|
+
const FLOATING_GAP = 8;
|
|
5889
|
+
/** The viewport margin — a panel never touches the screen's edge. */
|
|
5890
|
+
const FLOATING_MARGIN = 12;
|
|
5891
|
+
const clamp = (value, min, max) => Math.min(Math.max(value, min), Math.max(min, max));
|
|
5892
|
+
/** Where `panel` goes beside `anchor` inside `viewport`. */
|
|
5893
|
+
function placeFloating(anchor, panel, viewport, options = {}) {
|
|
5894
|
+
const gap = options.gap ?? FLOATING_GAP;
|
|
5895
|
+
const margin = options.margin ?? FLOATING_MARGIN;
|
|
5896
|
+
const anchorBottom = anchor.top + anchor.height;
|
|
5897
|
+
const roomBelow = viewport.height - anchorBottom - gap - margin;
|
|
5898
|
+
const roomAbove = anchor.top - gap - margin;
|
|
5899
|
+
// Below is the default; above only when below is too short AND above is roomier (a field near the screen's foot).
|
|
5900
|
+
const side = roomBelow >= panel.height || roomBelow >= roomAbove ? FloatingSide.Below : FloatingSide.Above;
|
|
5901
|
+
const wantTop = side === FloatingSide.Below ? anchorBottom + gap : anchor.top - gap - panel.height;
|
|
5902
|
+
const top = clamp(wantTop, margin, viewport.height - margin - panel.height);
|
|
5903
|
+
const start = options.rtl ? anchor.left + anchor.width : anchor.left;
|
|
5904
|
+
const wantLeft = options.rtl ? start - panel.width : start;
|
|
5905
|
+
const left = clamp(wantLeft, margin, viewport.width - margin - panel.width);
|
|
5906
|
+
// The origin is the anchor's start corner, in the panel's own coordinates, kept on the panel's box.
|
|
5907
|
+
const originX = Math.round(clamp(start - left, 0, panel.width));
|
|
5908
|
+
const originY = side === FloatingSide.Below ? 0 : Math.round(panel.height);
|
|
5909
|
+
return { top: Math.round(top), left: Math.round(left), side, origin: `${originX}px ${originY}px` };
|
|
5910
|
+
}
|
|
5911
|
+
|
|
5912
|
+
var CalendarClass;
|
|
5913
|
+
(function (CalendarClass) {
|
|
5914
|
+
CalendarClass["Panel"] = "iox-cal";
|
|
5915
|
+
CalendarClass["Grab"] = "iox-cal__grab";
|
|
5916
|
+
CalendarClass["Head"] = "iox-cal__head";
|
|
5917
|
+
CalendarClass["Title"] = "iox-cal__title";
|
|
5918
|
+
CalendarClass["Nav"] = "iox-cal__nav";
|
|
5919
|
+
CalendarClass["Prev"] = "iox-cal__prev";
|
|
5920
|
+
CalendarClass["Next"] = "iox-cal__next";
|
|
5921
|
+
CalendarClass["Grid"] = "iox-cal__grid";
|
|
5922
|
+
CalendarClass["Weekday"] = "iox-cal__weekday";
|
|
5923
|
+
CalendarClass["Cell"] = "iox-cal__cell";
|
|
5924
|
+
CalendarClass["Day"] = "iox-cal__day";
|
|
5925
|
+
})(CalendarClass || (CalendarClass = {}));
|
|
5926
|
+
/** On the panel while it leaves (the shared CSS plays the exit on it). */
|
|
5927
|
+
const CALENDAR_CLOSING_ATTRIBUTE = 'data-iox-closing';
|
|
5928
|
+
/** On the panel the builder canvas draws for its design-time Open state (in the flow, inert). */
|
|
5929
|
+
const CALENDAR_DESIGN_ATTRIBUTE = 'data-iox-cal-design';
|
|
5930
|
+
/** A phone gets the sheet (the drawer curve, the scrim) — the same width the shared CSS keys on. */
|
|
5931
|
+
const CALENDAR_SHEET_QUERY = '(max-width: 767px)';
|
|
5932
|
+
const RUNTIME = '__ioxDatePicker';
|
|
5933
|
+
const ISO_ATTRIBUTE = 'data-iox-iso';
|
|
5934
|
+
const CHEVRON_BACK = 'M15 18l-6-6 6-6';
|
|
5935
|
+
const CHEVRON_AHEAD = 'M9 18l6-6-6-6';
|
|
5936
|
+
/** An 1 Feb 2026 is a Sunday: day `i` of the week is `SUNDAY + i`. */
|
|
5937
|
+
const A_SUNDAY = new Date(2026, 1, 1);
|
|
5938
|
+
let calendarSeq = 0;
|
|
5939
|
+
const chevron = (doc, d) => {
|
|
5940
|
+
const svg = doc.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
5941
|
+
svg.setAttribute('width', '18');
|
|
5942
|
+
svg.setAttribute('height', '18');
|
|
5943
|
+
svg.setAttribute('viewBox', '0 0 24 24');
|
|
5944
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
5945
|
+
svg.setAttribute('focusable', 'false');
|
|
5946
|
+
const path = doc.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
5947
|
+
path.setAttribute('d', d);
|
|
5948
|
+
path.setAttribute('fill', 'none');
|
|
5949
|
+
path.setAttribute('stroke', 'currentColor');
|
|
5950
|
+
path.setAttribute('stroke-width', '2');
|
|
5951
|
+
path.setAttribute('stroke-linecap', 'round');
|
|
5952
|
+
path.setAttribute('stroke-linejoin', 'round');
|
|
5953
|
+
svg.appendChild(path);
|
|
5954
|
+
return svg;
|
|
5955
|
+
};
|
|
5956
|
+
const el = (doc, tag, cls, attrs = {}) => {
|
|
5957
|
+
const node = doc.createElement(tag);
|
|
5958
|
+
if (cls)
|
|
5959
|
+
node.className = cls;
|
|
5960
|
+
for (const [k, v] of Object.entries(attrs))
|
|
5961
|
+
node.setAttribute(k, v);
|
|
5962
|
+
return node;
|
|
5963
|
+
};
|
|
5964
|
+
/** The language the field speaks: the host's, the nearest `[lang]`, the document's. */
|
|
5965
|
+
function fieldLanguage(field, lang) {
|
|
5966
|
+
return lang || field.closest('[lang]')?.getAttribute('lang') || field.ownerDocument?.documentElement?.lang || 'en';
|
|
5967
|
+
}
|
|
5968
|
+
/** The ms a transition list takes, the longest entry (duration + delay) — how long an exit plays. */
|
|
5969
|
+
function transitionMs(node) {
|
|
5970
|
+
const style = node.ownerDocument?.defaultView?.getComputedStyle(node);
|
|
5971
|
+
if (!style)
|
|
5972
|
+
return 0;
|
|
5973
|
+
const parse = (list) => list.split(',').map(t => (t.trim().endsWith('ms') ? parseFloat(t) : parseFloat(t) * 1000) || 0);
|
|
5974
|
+
const durations = parse(style.transitionDuration);
|
|
5975
|
+
const delays = parse(style.transitionDelay);
|
|
5976
|
+
return Math.max(0, ...durations.map((d, i) => d + (delays[i % delays.length] ?? 0)));
|
|
5977
|
+
}
|
|
5978
|
+
/** The panel's skeleton: grab handle, the month and its arrows, the weekday row. The body is `paintMonth`'s. */
|
|
5979
|
+
function buildPanel(doc, tag, words, locale, weekdays, rtl) {
|
|
5980
|
+
const id = `iox-cal-${++calendarSeq}`;
|
|
5981
|
+
const panel = el(doc, tag, CalendarClass.Panel, { 'aria-label': words.choose, dir: rtl ? 'rtl' : 'ltr', 'data-lenis-prevent': '' });
|
|
5982
|
+
panel.appendChild(el(doc, 'span', CalendarClass.Grab, { 'aria-hidden': 'true' }));
|
|
5983
|
+
const head = el(doc, 'div', CalendarClass.Head);
|
|
5984
|
+
const title = el(doc, 'h2', CalendarClass.Title, { id: `${id}-title`, 'aria-live': 'polite' });
|
|
5985
|
+
const nav = el(doc, 'div', CalendarClass.Nav);
|
|
5986
|
+
const prev = el(doc, 'button', CalendarClass.Prev, { type: 'button', 'aria-label': words.previousMonth, title: words.previousMonth });
|
|
5987
|
+
const next = el(doc, 'button', CalendarClass.Next, { type: 'button', 'aria-label': words.nextMonth, title: words.nextMonth });
|
|
5988
|
+
prev.appendChild(chevron(doc, CHEVRON_BACK));
|
|
5989
|
+
next.appendChild(chevron(doc, CHEVRON_AHEAD));
|
|
5990
|
+
nav.append(prev, next);
|
|
5991
|
+
head.append(title, nav);
|
|
5992
|
+
const table = el(doc, 'table', CalendarClass.Grid, { role: 'grid', 'aria-labelledby': `${id}-title` });
|
|
5993
|
+
const row = el(doc, 'tr');
|
|
5994
|
+
const short = new Intl.DateTimeFormat(locale, { weekday: 'narrow' });
|
|
5995
|
+
const long = new Intl.DateTimeFormat(locale, { weekday: 'long' });
|
|
5996
|
+
for (const dow of weekdays) {
|
|
5997
|
+
const day = new Date(A_SUNDAY.getFullYear(), A_SUNDAY.getMonth(), A_SUNDAY.getDate() + dow);
|
|
5998
|
+
const th = el(doc, 'th', CalendarClass.Weekday, { scope: 'col', abbr: long.format(day) });
|
|
5999
|
+
th.textContent = short.format(day);
|
|
6000
|
+
row.appendChild(th);
|
|
6001
|
+
}
|
|
6002
|
+
const thead = el(doc, 'thead');
|
|
6003
|
+
thead.appendChild(row);
|
|
6004
|
+
table.append(thead, el(doc, 'tbody'));
|
|
6005
|
+
panel.append(head, table);
|
|
6006
|
+
return panel;
|
|
6007
|
+
}
|
|
6008
|
+
/** Draw one month into the panel: its title, its six rows, which arrows can move. */
|
|
6009
|
+
function paintMonth(panel, model, locale) {
|
|
6010
|
+
const doc = panel.ownerDocument;
|
|
6011
|
+
const title = panel.querySelector(`.${CalendarClass.Title}`);
|
|
6012
|
+
if (title)
|
|
6013
|
+
title.textContent = new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric' }).format(new Date(model.year, model.month, 1));
|
|
6014
|
+
const full = new Intl.DateTimeFormat(locale, { dateStyle: 'full' });
|
|
6015
|
+
const body = panel.querySelector('tbody');
|
|
6016
|
+
body.textContent = '';
|
|
6017
|
+
for (const week of model.weeks) {
|
|
6018
|
+
const tr = el(doc, 'tr');
|
|
6019
|
+
for (const day of week) {
|
|
6020
|
+
const td = el(doc, 'td', CalendarClass.Cell, { role: 'gridcell' });
|
|
6021
|
+
if (day) {
|
|
6022
|
+
const button = el(doc, 'button', CalendarClass.Day, { type: 'button', tabindex: day.focus ? '0' : '-1', [ISO_ATTRIBUTE]: day.iso });
|
|
6023
|
+
button.setAttribute('aria-label', full.format(new Date(model.year, model.month, day.day)));
|
|
6024
|
+
button.textContent = String(day.day);
|
|
6025
|
+
if (day.today)
|
|
6026
|
+
button.setAttribute('aria-current', 'date');
|
|
6027
|
+
if (day.disabled)
|
|
6028
|
+
button.setAttribute('aria-disabled', 'true');
|
|
6029
|
+
if (day.selected)
|
|
6030
|
+
td.setAttribute('aria-selected', 'true');
|
|
6031
|
+
td.appendChild(button);
|
|
6032
|
+
}
|
|
6033
|
+
tr.appendChild(td);
|
|
6034
|
+
}
|
|
6035
|
+
body.appendChild(tr);
|
|
6036
|
+
}
|
|
6037
|
+
panel.querySelector(`.${CalendarClass.Prev}`)?.toggleAttribute('disabled', !model.canPrev);
|
|
6038
|
+
panel.querySelector(`.${CalendarClass.Next}`)?.toggleAttribute('disabled', !model.canNext);
|
|
6039
|
+
}
|
|
6040
|
+
/** The date field's parts. */
|
|
6041
|
+
function partsOf(field) {
|
|
6042
|
+
return {
|
|
6043
|
+
button: field.querySelector(`button.${FieldClass.DateButton}`),
|
|
6044
|
+
input: field.querySelector(`input.${FieldClass.DateInput}`),
|
|
6045
|
+
words: field.querySelector(`.${FieldClass.DateValue}`),
|
|
6046
|
+
};
|
|
6047
|
+
}
|
|
6048
|
+
/** Write a day (or nothing) into a date field: the hidden input carries it, the button says it in words. */
|
|
6049
|
+
function setDateFieldValue(field, iso, options = {}) {
|
|
6050
|
+
const { input, words } = partsOf(field);
|
|
6051
|
+
const day = isIsoDate(iso) ? iso : '';
|
|
6052
|
+
if (input)
|
|
6053
|
+
input.value = day;
|
|
6054
|
+
if (!words)
|
|
6055
|
+
return;
|
|
6056
|
+
const lang = fieldLanguage(field, options.lang);
|
|
6057
|
+
if (day) {
|
|
6058
|
+
words.textContent = formatDateInWords(day, lang, options.today ?? todayIso());
|
|
6059
|
+
words.removeAttribute(DATE_EMPTY_ATTRIBUTE);
|
|
6060
|
+
}
|
|
6061
|
+
else {
|
|
6062
|
+
words.textContent = options.placeholder ?? words.getAttribute('data-iox-placeholder') ?? datePickerWords(lang).choose;
|
|
6063
|
+
words.setAttribute(DATE_EMPTY_ATTRIBUTE, '');
|
|
6064
|
+
}
|
|
6065
|
+
}
|
|
6066
|
+
/** The days a field accepts today, from the raw Earliest / Latest its button carries. */
|
|
6067
|
+
function rangeOf(button, today) {
|
|
6068
|
+
const bound = (attr) => resolveDateBound(parseDateBound(button.getAttribute(attr) ?? ''), today);
|
|
6069
|
+
return { min: bound(DATE_MIN_ATTRIBUTE), max: bound(DATE_MAX_ATTRIBUTE) };
|
|
6070
|
+
}
|
|
6071
|
+
/** One live date field. Built by `attachDatePicker`; idempotent per field. */
|
|
6072
|
+
class DatePickerRuntime {
|
|
6073
|
+
constructor(field, options = {}) {
|
|
6074
|
+
this.field = field;
|
|
6075
|
+
this.options = options;
|
|
6076
|
+
this.panel = null;
|
|
6077
|
+
this.focusIso = '';
|
|
6078
|
+
this.closing = false;
|
|
6079
|
+
this.listeners = [];
|
|
6080
|
+
const { button, words } = partsOf(field);
|
|
6081
|
+
if (words && !words.hasAttribute('data-iox-placeholder') && words.hasAttribute(DATE_EMPTY_ATTRIBUTE)) {
|
|
6082
|
+
words.setAttribute('data-iox-placeholder', words.textContent ?? '');
|
|
6083
|
+
}
|
|
6084
|
+
if (button)
|
|
6085
|
+
this.listen(button, 'click', () => this.open());
|
|
6086
|
+
// A successful submit resets the form: a hidden input keeps its value through `form.reset()` (its value IS its
|
|
6087
|
+
// attribute), so the field clears itself. A host that renders the field (`onPick` — the component path) owns
|
|
6088
|
+
// its words and its value: the runtime never writes over what the host's view tracks.
|
|
6089
|
+
const form = field.closest('form');
|
|
6090
|
+
if (form && !options.onPick)
|
|
6091
|
+
this.listen(form, 'reset', () => setDateFieldValue(field, '', { lang: this.lang() }));
|
|
6092
|
+
field[RUNTIME] = this;
|
|
6093
|
+
}
|
|
6094
|
+
get isOpen() { return !!this.panel; }
|
|
6095
|
+
value() { return partsOf(this.field).input?.value ?? ''; }
|
|
6096
|
+
destroy() {
|
|
6097
|
+
this.dismiss(true);
|
|
6098
|
+
for (const [target, type, fn] of this.listeners)
|
|
6099
|
+
target.removeEventListener(type, fn);
|
|
6100
|
+
this.listeners.length = 0;
|
|
6101
|
+
delete this.field[RUNTIME];
|
|
6102
|
+
}
|
|
6103
|
+
open() {
|
|
6104
|
+
const { button } = partsOf(this.field);
|
|
6105
|
+
if (!button || this.panel || button.disabled)
|
|
6106
|
+
return;
|
|
6107
|
+
const doc = this.field.ownerDocument;
|
|
6108
|
+
const lang = this.lang();
|
|
6109
|
+
const rtl = this.rtl();
|
|
6110
|
+
const today = this.today();
|
|
6111
|
+
const { min, max } = rangeOf(button, today);
|
|
6112
|
+
const selected = this.value();
|
|
6113
|
+
this.focusIso = clampIso(isIsoDate(selected) ? selected : today, min, max);
|
|
6114
|
+
const weekStart = weekStartFor(lang);
|
|
6115
|
+
const model = () => calendarMonth(this.focusIso, { weekStart, today, min, max, selected: this.value() || null });
|
|
6116
|
+
const panel = buildPanel(doc, 'dialog', this.words(lang), lang, model().weekdays, rtl);
|
|
6117
|
+
const sheet = !!doc.defaultView?.matchMedia?.(CALENDAR_SHEET_QUERY).matches;
|
|
6118
|
+
paintMonth(panel, model(), lang);
|
|
6119
|
+
this.field.appendChild(panel);
|
|
6120
|
+
this.panel = panel;
|
|
6121
|
+
this.closing = false;
|
|
6122
|
+
const repaint = (focusDay) => {
|
|
6123
|
+
paintMonth(panel, model(), lang);
|
|
6124
|
+
if (focusDay)
|
|
6125
|
+
this.focusDay();
|
|
6126
|
+
};
|
|
6127
|
+
const pickable = (b) => !!b && b.getAttribute('aria-disabled') !== 'true';
|
|
6128
|
+
this.listen(panel, 'click', e => {
|
|
6129
|
+
const target = e.target;
|
|
6130
|
+
const day = target.closest?.(`.${CalendarClass.Day}`);
|
|
6131
|
+
if (day) {
|
|
6132
|
+
if (pickable(day))
|
|
6133
|
+
this.pick(day.getAttribute(ISO_ATTRIBUTE));
|
|
6134
|
+
return;
|
|
6135
|
+
}
|
|
6136
|
+
if (target.closest?.(`.${CalendarClass.Prev}`)) {
|
|
6137
|
+
this.focusIso = clampIso(addMonthsIso(this.focusIso, -1), min, max);
|
|
6138
|
+
repaint(false);
|
|
6139
|
+
return;
|
|
6140
|
+
}
|
|
6141
|
+
if (target.closest?.(`.${CalendarClass.Next}`)) {
|
|
6142
|
+
this.focusIso = clampIso(addMonthsIso(this.focusIso, 1), min, max);
|
|
6143
|
+
repaint(false);
|
|
6144
|
+
return;
|
|
6145
|
+
}
|
|
6146
|
+
// A modal dialog hears clicks on its backdrop as its own: outside the panel's box closes it.
|
|
6147
|
+
const r = panel.getBoundingClientRect();
|
|
6148
|
+
const { clientX: x, clientY: y } = e;
|
|
6149
|
+
if (x < r.left || x > r.right || y < r.top || y > r.bottom)
|
|
6150
|
+
this.close();
|
|
6151
|
+
});
|
|
6152
|
+
this.listen(panel, 'keydown', e => {
|
|
6153
|
+
const key = e;
|
|
6154
|
+
if (!key.target?.closest?.(`.${CalendarClass.Day}`))
|
|
6155
|
+
return;
|
|
6156
|
+
const next = calendarKeyTarget(key.key, this.focusIso, { weekStart, rtl, shift: key.shiftKey, min, max });
|
|
6157
|
+
if (!next)
|
|
6158
|
+
return;
|
|
6159
|
+
key.preventDefault();
|
|
6160
|
+
this.focusIso = next;
|
|
6161
|
+
repaint(true);
|
|
6162
|
+
});
|
|
6163
|
+
// Esc: the dialog's own cancel — closed the way it came, focus back on the button.
|
|
6164
|
+
this.listen(panel, 'cancel', e => { e.preventDefault(); this.close(); });
|
|
6165
|
+
if (sheet)
|
|
6166
|
+
panel.setAttribute('data-iox-cal-sheet', '');
|
|
6167
|
+
panel.showModal();
|
|
6168
|
+
if (!sheet)
|
|
6169
|
+
this.place(rtl);
|
|
6170
|
+
this.focusDay();
|
|
6171
|
+
button.setAttribute('aria-expanded', 'true');
|
|
6172
|
+
this.options.lockScroll?.();
|
|
6173
|
+
}
|
|
6174
|
+
/** Close with the exit the shared CSS plays (faster than the entrance — Emil 30), then give focus back. */
|
|
6175
|
+
close() {
|
|
6176
|
+
if (!this.panel || this.closing)
|
|
6177
|
+
return;
|
|
6178
|
+
this.closing = true;
|
|
6179
|
+
const panel = this.panel;
|
|
6180
|
+
panel.setAttribute(CALENDAR_CLOSING_ATTRIBUTE, '');
|
|
6181
|
+
partsOf(this.field).button?.setAttribute('aria-expanded', 'false');
|
|
6182
|
+
const ms = transitionMs(panel);
|
|
6183
|
+
if (ms <= 0) {
|
|
6184
|
+
this.dismiss(false);
|
|
6185
|
+
return;
|
|
6186
|
+
}
|
|
6187
|
+
setTimeout(() => this.dismiss(false), ms);
|
|
6188
|
+
}
|
|
6189
|
+
pick(iso) {
|
|
6190
|
+
const { button } = partsOf(this.field);
|
|
6191
|
+
if (this.options.onPick)
|
|
6192
|
+
this.options.onPick(iso);
|
|
6193
|
+
else
|
|
6194
|
+
setDateFieldValue(this.field, iso, { lang: this.lang(), today: this.today() });
|
|
6195
|
+
// The form's runtime hears the change on the control it knows (the button carries `iox-field__control`).
|
|
6196
|
+
button?.dispatchEvent(new Event('input', { bubbles: true }));
|
|
6197
|
+
button?.dispatchEvent(new Event('change', { bubbles: true }));
|
|
6198
|
+
this.close();
|
|
6199
|
+
}
|
|
6200
|
+
dismiss(silent) {
|
|
6201
|
+
const panel = this.panel;
|
|
6202
|
+
if (!panel)
|
|
6203
|
+
return;
|
|
6204
|
+
this.panel = null;
|
|
6205
|
+
this.closing = false;
|
|
6206
|
+
if (panel.open)
|
|
6207
|
+
panel.close();
|
|
6208
|
+
panel.remove();
|
|
6209
|
+
this.options.unlockScroll?.();
|
|
6210
|
+
const { button } = partsOf(this.field);
|
|
6211
|
+
button?.setAttribute('aria-expanded', 'false');
|
|
6212
|
+
if (!silent)
|
|
6213
|
+
button?.focus({ preventScroll: true });
|
|
6214
|
+
}
|
|
6215
|
+
focusDay() {
|
|
6216
|
+
const day = this.panel?.querySelector(`.${CalendarClass.Day}[${ISO_ATTRIBUTE}="${this.focusIso}"]`);
|
|
6217
|
+
day?.focus({ preventScroll: true });
|
|
6218
|
+
}
|
|
6219
|
+
/** Under the field, aligned to its reading start; above when there is no room — the ONE `placeFloating`. */
|
|
6220
|
+
place(rtl) {
|
|
6221
|
+
const panel = this.panel;
|
|
6222
|
+
const { button } = partsOf(this.field);
|
|
6223
|
+
const view = this.field.ownerDocument.defaultView;
|
|
6224
|
+
if (!panel || !button || !view)
|
|
6225
|
+
return;
|
|
6226
|
+
const anchor = button.getBoundingClientRect();
|
|
6227
|
+
const spot = placeFloating({ top: anchor.top, left: anchor.left, width: anchor.width, height: anchor.height }, { width: panel.offsetWidth, height: panel.offsetHeight }, { width: view.innerWidth, height: view.innerHeight }, { rtl });
|
|
6228
|
+
panel.style.top = `${spot.top}px`;
|
|
6229
|
+
panel.style.left = `${spot.left}px`;
|
|
6230
|
+
panel.style.transformOrigin = spot.origin;
|
|
6231
|
+
panel.setAttribute('data-iox-side', spot.side);
|
|
6232
|
+
}
|
|
6233
|
+
lang() { return fieldLanguage(this.field, this.options.lang); }
|
|
6234
|
+
rtl() {
|
|
6235
|
+
if (typeof this.options.rtl === 'boolean')
|
|
6236
|
+
return this.options.rtl;
|
|
6237
|
+
return this.field.ownerDocument.defaultView?.getComputedStyle(this.field).direction === 'rtl';
|
|
6238
|
+
}
|
|
6239
|
+
today() { return this.options.today?.() ?? todayIso(); }
|
|
6240
|
+
words(lang) { return { ...datePickerWords(lang), ...(this.options.words ?? {}) }; }
|
|
6241
|
+
listen(target, type, fn) {
|
|
6242
|
+
target.addEventListener(type, fn);
|
|
6243
|
+
this.listeners.push([target, type, fn]);
|
|
6244
|
+
}
|
|
6245
|
+
}
|
|
6246
|
+
/** The runtime already running a field, if any. */
|
|
6247
|
+
function datePickerOf(field) {
|
|
6248
|
+
return field?.[RUNTIME] ?? null;
|
|
6249
|
+
}
|
|
6250
|
+
/** Run a date field — once: a field already running is returned as is. Null when `field` holds no date button. */
|
|
6251
|
+
function attachDatePicker(field, options = {}) {
|
|
6252
|
+
if (!field || !partsOf(field).button)
|
|
6253
|
+
return null;
|
|
6254
|
+
return datePickerOf(field) ?? new DatePickerRuntime(field, options);
|
|
6255
|
+
}
|
|
6256
|
+
/** Run every date field under `root` (the string path: a page's or a global's markup). */
|
|
6257
|
+
function attachDatePickers(root, options = {}) {
|
|
6258
|
+
if (!root?.querySelectorAll)
|
|
6259
|
+
return [];
|
|
6260
|
+
const out = [];
|
|
6261
|
+
root.querySelectorAll(`.${FieldClass.Root}`).forEach(field => {
|
|
6262
|
+
const runtime = attachDatePicker(field, options);
|
|
6263
|
+
if (runtime)
|
|
6264
|
+
out.push(runtime);
|
|
6265
|
+
});
|
|
6266
|
+
return out;
|
|
6267
|
+
}
|
|
6268
|
+
/**
|
|
6269
|
+
* The builder canvas's design-time Open state: the SAME panel, drawn in the flow under the date button (a modal in the
|
|
6270
|
+
* top layer would escape the canvas's zoom and cover the builder), inert — the author selects the Field and styles
|
|
6271
|
+
* its Calendar parts; nothing in it takes focus or a click. `false` removes it.
|
|
6272
|
+
*/
|
|
6273
|
+
function showCalendarDesign(field, show, options = {}) {
|
|
6274
|
+
if (!field)
|
|
6275
|
+
return;
|
|
6276
|
+
const existing = field.querySelector(`:scope > [${CALENDAR_DESIGN_ATTRIBUTE}]`);
|
|
6277
|
+
if (!show) {
|
|
6278
|
+
existing?.remove();
|
|
6279
|
+
return;
|
|
6280
|
+
}
|
|
6281
|
+
if (existing)
|
|
6282
|
+
return;
|
|
6283
|
+
const { button } = partsOf(field);
|
|
6284
|
+
if (!button)
|
|
6285
|
+
return;
|
|
6286
|
+
const lang = fieldLanguage(field, options.lang);
|
|
6287
|
+
const today = options.today ?? todayIso();
|
|
6288
|
+
const { min, max } = rangeOf(button, today);
|
|
6289
|
+
const selected = (partsOf(field).input?.value || '') || null;
|
|
6290
|
+
const focus = clampIso(selected ?? today, min, max);
|
|
6291
|
+
const model = calendarMonth(focus, { weekStart: weekStartFor(lang), today, min, max, selected });
|
|
6292
|
+
const rtl = typeof options.rtl === 'boolean' ? options.rtl : field.ownerDocument.defaultView?.getComputedStyle(field).direction === 'rtl';
|
|
6293
|
+
const panel = buildPanel(field.ownerDocument, 'div', datePickerWords(lang), lang, model.weekdays, rtl);
|
|
6294
|
+
panel.setAttribute(CALENDAR_DESIGN_ATTRIBUTE, '');
|
|
6295
|
+
panel.setAttribute('inert', '');
|
|
6296
|
+
panel.setAttribute('aria-hidden', 'true');
|
|
6297
|
+
paintMonth(panel, model, lang);
|
|
6298
|
+
const after = partsOf(field).input ?? button;
|
|
6299
|
+
after.insertAdjacentElement('afterend', panel);
|
|
6300
|
+
}
|
|
6301
|
+
|
|
5141
6302
|
let fieldSeq = 0;
|
|
5142
6303
|
/**
|
|
5143
6304
|
* The compound Field: label + control + help + error + required mark, as ONE node (decision D2).
|
|
@@ -5153,8 +6314,22 @@ let fieldSeq = 0;
|
|
|
5153
6314
|
*
|
|
5154
6315
|
* ⚠️ Control type is a TRAIT, not a component per type (decision D4) — one component, one config,
|
|
5155
6316
|
* and choosing `email` also chooses email validation.
|
|
6317
|
+
*
|
|
6318
|
+
* The markup is forms-core `fieldHtml`'s, element for element (the engine's string path writes it): Choices is a
|
|
6319
|
+
* fieldset of chips or rows (premium N1), Date is a button + a hidden ISO input that opens the look's calendar (N8).
|
|
6320
|
+
* The submitted name is the FORM's unique name for this field (forms-core `uniqueFieldNames`, vectoriox/iox-ui#43).
|
|
5156
6321
|
*/
|
|
5157
6322
|
class IoxFieldComponent {
|
|
6323
|
+
set control(value) {
|
|
6324
|
+
this._control = value || ControlType.Text;
|
|
6325
|
+
this.syncDate();
|
|
6326
|
+
}
|
|
6327
|
+
get control() { return this._control; }
|
|
6328
|
+
/** Choices (N1): `chips` (the default) or `list` — the look's radio / checkbox rows. */
|
|
6329
|
+
set appearance(value) {
|
|
6330
|
+
this._appearance = choiceAppearanceOf({ appearance: value });
|
|
6331
|
+
}
|
|
6332
|
+
get appearance() { return this._appearance; }
|
|
5158
6333
|
/** Explicit choices (bound data, a hand-built node). Wins over `optionsText`. */
|
|
5159
6334
|
set options(value) {
|
|
5160
6335
|
this._options = Array.isArray(value) ? value : [];
|
|
@@ -5168,22 +6343,45 @@ class IoxFieldComponent {
|
|
|
5168
6343
|
this.refreshChoices();
|
|
5169
6344
|
}
|
|
5170
6345
|
get optionsText() { return this._optionsText; }
|
|
6346
|
+
/**
|
|
6347
|
+
* Forces the error appearance without a real failure — how the builder lets an author DESIGN
|
|
6348
|
+
* the error state (decision D5). At runtime the state comes from the runtime service instead.
|
|
6349
|
+
* Turning it on shakes the field once, so the author sees the motion they are designing (N2).
|
|
6350
|
+
*/
|
|
6351
|
+
set forceInvalid(value) {
|
|
6352
|
+
const was = this._forceInvalid;
|
|
6353
|
+
this._forceInvalid = !!value;
|
|
6354
|
+
if (this._forceInvalid && !was && this.viewReady)
|
|
6355
|
+
shakeElement(this.host?.nativeElement);
|
|
6356
|
+
}
|
|
6357
|
+
get forceInvalid() { return this._forceInvalid; }
|
|
6358
|
+
/** The builder canvas's design-time Open state for a Date (N8): the calendar drawn under the field, inert. */
|
|
6359
|
+
set designOpen(value) {
|
|
6360
|
+
this._designOpen = !!value;
|
|
6361
|
+
this.syncDesignCalendar();
|
|
6362
|
+
}
|
|
6363
|
+
get designOpen() { return this._designOpen; }
|
|
5171
6364
|
/**
|
|
5172
6365
|
* Classic constructor injection, not `inject()`: this library's specs construct components
|
|
5173
6366
|
* directly (its vitest setup cannot run `TestBed.createComponent`), and a field initializer
|
|
5174
6367
|
* calling `inject()` throws NG0203 outside an injection context.
|
|
5175
6368
|
*
|
|
5176
6369
|
* `runtime` is optional because a Field dropped OUTSIDE any Form must still render — it simply
|
|
5177
|
-
* has nothing to submit to. That is the same ancestry rule from the other side.
|
|
6370
|
+
* has nothing to submit to. That is the same ancestry rule from the other side. `isPreview` says the field is
|
|
6371
|
+
* LIVE (the engine, the admin's Preview): only then does its date button open a calendar — on the editing canvas
|
|
6372
|
+
* a click selects the field, and the calendar is the design-time Open state.
|
|
5178
6373
|
*/
|
|
5179
|
-
constructor(runtime, resolveMessage) {
|
|
6374
|
+
constructor(runtime, resolveMessage, host, isPreview, pageScroll) {
|
|
5180
6375
|
this.runtime = runtime;
|
|
5181
6376
|
this.resolveMessage = resolveMessage;
|
|
6377
|
+
this.host = host;
|
|
6378
|
+
this.isPreview = isPreview;
|
|
6379
|
+
this.pageScroll = pageScroll;
|
|
5182
6380
|
this.nodeId = '';
|
|
5183
6381
|
/** The submitted key. Blank means "derive it from the label" — authors never invent keys. */
|
|
5184
6382
|
this.name = '';
|
|
5185
6383
|
this.label = '';
|
|
5186
|
-
this.
|
|
6384
|
+
this._control = ControlType.Text;
|
|
5187
6385
|
this.placeholder = '';
|
|
5188
6386
|
this.help = '';
|
|
5189
6387
|
this.required = false;
|
|
@@ -5195,6 +6393,12 @@ class IoxFieldComponent {
|
|
|
5195
6393
|
this.rangeMax = 100;
|
|
5196
6394
|
this.rangeStep = 1;
|
|
5197
6395
|
this.accept = '';
|
|
6396
|
+
/** Choices (N1): several answers (checkboxes) or one (radios). */
|
|
6397
|
+
this.multiple = false;
|
|
6398
|
+
this._appearance = ChoiceAppearance.Chips;
|
|
6399
|
+
/** Date (N8): the first and last day it takes — '' any · N days from today · an ISO day (forms-core `dates.ts`). */
|
|
6400
|
+
this.earliest = '';
|
|
6401
|
+
this.latest = '';
|
|
5198
6402
|
/** What the radio group / dropdown renders: the shared forms-core reading of the two inputs above. */
|
|
5199
6403
|
this.choices = [];
|
|
5200
6404
|
this._options = [];
|
|
@@ -5202,33 +6406,42 @@ class IoxFieldComponent {
|
|
|
5202
6406
|
this.value = '';
|
|
5203
6407
|
/** Rules beyond `required` — carried on the node, interpreted by forms-core. */
|
|
5204
6408
|
this.rules = [];
|
|
5205
|
-
|
|
5206
|
-
* Forces the error appearance without a real failure — how the builder lets an author DESIGN
|
|
5207
|
-
* the error state (decision D5). At runtime the state comes from the runtime service instead.
|
|
5208
|
-
*/
|
|
5209
|
-
this.forceInvalid = false;
|
|
6409
|
+
this._forceInvalid = false;
|
|
5210
6410
|
/** Placeholder shown while designing the error state, so the block has something to style. */
|
|
5211
6411
|
this.designErrorText = 'Please check this field';
|
|
5212
6412
|
/** Only after a successful check — never on an untouched field. */
|
|
5213
6413
|
this.showValid = false;
|
|
6414
|
+
this._designOpen = false;
|
|
5214
6415
|
this.valueChange = new EventEmitter();
|
|
5215
6416
|
this.touched = new EventEmitter();
|
|
5216
6417
|
/** Stable per instance so `for`/`id` and `aria-describedby` always pair up. */
|
|
5217
6418
|
this.controlId = `iox-field-${++fieldSeq}`;
|
|
6419
|
+
this.registration = null;
|
|
6420
|
+
this.picker = null;
|
|
6421
|
+
this.viewReady = false;
|
|
5218
6422
|
}
|
|
5219
6423
|
ngOnInit() {
|
|
5220
|
-
this.
|
|
6424
|
+
this.registration = {
|
|
5221
6425
|
// Functions, not values: a trait edited on the canvas takes effect with no re-register.
|
|
5222
|
-
|
|
6426
|
+
baseName: () => baseFieldName({ name: this.name, label: this.label }),
|
|
6427
|
+
spec: (name) => this.toSpec(name),
|
|
5223
6428
|
getValue: () => this.value,
|
|
5224
|
-
}
|
|
6429
|
+
};
|
|
6430
|
+
this.unregister = this.runtime?.register(this.registration);
|
|
6431
|
+
}
|
|
6432
|
+
ngAfterViewInit() {
|
|
6433
|
+
this.viewReady = true;
|
|
6434
|
+
this.syncDate();
|
|
6435
|
+
this.syncDesignCalendar();
|
|
5225
6436
|
}
|
|
5226
6437
|
ngOnDestroy() {
|
|
5227
6438
|
this.unregister?.();
|
|
6439
|
+
this.picker?.destroy();
|
|
6440
|
+
this.picker = null;
|
|
5228
6441
|
}
|
|
5229
|
-
/** The submitted key: the
|
|
6442
|
+
/** The submitted key: the form's unique name for this field (iox-ui#43), else the field's own base name. */
|
|
5230
6443
|
get fieldName() {
|
|
5231
|
-
return this.
|
|
6444
|
+
return (this.registration && this.runtime?.nameOf(this.registration)) || baseFieldName({ name: this.name, label: this.label });
|
|
5232
6445
|
}
|
|
5233
6446
|
/** Failing AND allowed to say so — or forced, while an author designs the state. */
|
|
5234
6447
|
get invalid() {
|
|
@@ -5243,27 +6456,55 @@ class IoxFieldComponent {
|
|
|
5243
6456
|
const error = this.runtime?.errorFor(this.fieldName);
|
|
5244
6457
|
return error ? this.resolveMessage(error.messageKey, error.params) : '';
|
|
5245
6458
|
}
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
control: this.control
|
|
5250
|
-
|
|
5251
|
-
};
|
|
5252
|
-
if (this.required)
|
|
5253
|
-
spec.required = true;
|
|
5254
|
-
if (this.rules?.length)
|
|
5255
|
-
spec.rules = this.rules;
|
|
5256
|
-
if (this.choices.length)
|
|
5257
|
-
spec.options = this.choices;
|
|
5258
|
-
return spec;
|
|
6459
|
+
/** The field's spec under the name its form gave it — forms-core `fieldSpecOf`, the reading the saved spec uses. */
|
|
6460
|
+
toSpec(name) {
|
|
6461
|
+
return fieldSpecOf({
|
|
6462
|
+
control: this.control, label: this.label, required: this.required, rules: this.rules, options: this.choices,
|
|
6463
|
+
multiple: this.multiple, earliest: this.earliest, latest: this.latest,
|
|
6464
|
+
}, name);
|
|
5259
6465
|
}
|
|
5260
6466
|
refreshChoices() {
|
|
5261
6467
|
this.choices = fieldOptionsOf({ options: this._options, optionsText: this._optionsText });
|
|
5262
6468
|
}
|
|
5263
|
-
/** `file`, `
|
|
6469
|
+
/** `file`, `number`… map straight through; everything else is a text input (forms-core `inputTypeOf`). */
|
|
5264
6470
|
get inputType() {
|
|
5265
|
-
|
|
5266
|
-
|
|
6471
|
+
return inputTypeOf(String(this.control));
|
|
6472
|
+
}
|
|
6473
|
+
/** A Date's chosen day (the hidden input's value) — '' while none is. */
|
|
6474
|
+
get dateIso() {
|
|
6475
|
+
return isIsoDate(this.value) ? String(this.value) : '';
|
|
6476
|
+
}
|
|
6477
|
+
/** A Date's chosen day in words, in the field's language — '' while none is. */
|
|
6478
|
+
get dateWords() {
|
|
6479
|
+
const iso = this.dateIso;
|
|
6480
|
+
if (!iso)
|
|
6481
|
+
return '';
|
|
6482
|
+
const lang = this.host?.nativeElement ? fieldLanguage(this.host.nativeElement) : 'en';
|
|
6483
|
+
return formatDateInWords(iso, lang, todayIso());
|
|
6484
|
+
}
|
|
6485
|
+
/** What an unpicked Date says: the author's hint, else "Choose a date" in the field's language. */
|
|
6486
|
+
get datePlaceholder() {
|
|
6487
|
+
if (this.placeholder)
|
|
6488
|
+
return this.placeholder;
|
|
6489
|
+
return datePickerWords(this.host?.nativeElement ? fieldLanguage(this.host.nativeElement) : null).choose;
|
|
6490
|
+
}
|
|
6491
|
+
/** Whether a Choices option is chosen — one answer, or one of several. */
|
|
6492
|
+
isChosen(optionValue) {
|
|
6493
|
+
if (isMultiValueField({ control: this.control, multiple: this.multiple })) {
|
|
6494
|
+
return Array.isArray(this.value) && this.value.includes(optionValue);
|
|
6495
|
+
}
|
|
6496
|
+
return this.value === optionValue;
|
|
6497
|
+
}
|
|
6498
|
+
/** A Choices option ticked or unticked: the answer becomes the list of ticked ones, or the one picked. */
|
|
6499
|
+
toggle(optionValue, checked) {
|
|
6500
|
+
if (!isMultiValueField({ control: this.control, multiple: this.multiple })) {
|
|
6501
|
+
this.emit(optionValue);
|
|
6502
|
+
return;
|
|
6503
|
+
}
|
|
6504
|
+
const current = Array.isArray(this.value) ? this.value : [];
|
|
6505
|
+
const next = checked ? [...current.filter(v => v !== optionValue), optionValue] : current.filter(v => v !== optionValue);
|
|
6506
|
+
// In the options' order, whatever order they were ticked in.
|
|
6507
|
+
this.emit(this.choices.map(c => c.value).filter(v => next.includes(v)));
|
|
5267
6508
|
}
|
|
5268
6509
|
/** Help and error are BOTH announced, in that order, when both are present. */
|
|
5269
6510
|
get describedBy() {
|
|
@@ -5279,23 +6520,79 @@ class IoxFieldComponent {
|
|
|
5279
6520
|
this.runtime?.onChange(this.fieldName);
|
|
5280
6521
|
this.valueChange.emit(value);
|
|
5281
6522
|
}
|
|
5282
|
-
touch() {
|
|
6523
|
+
touch(event) {
|
|
6524
|
+
// Leaving one chip for the next, or the date button for its own calendar, is not leaving the FIELD.
|
|
6525
|
+
const next = event?.relatedTarget;
|
|
6526
|
+
if (next && this.host?.nativeElement?.contains?.(next))
|
|
6527
|
+
return;
|
|
5283
6528
|
// Blur is what lets Smart timing start speaking for this field.
|
|
5284
6529
|
this.runtime?.onBlur(this.fieldName);
|
|
5285
6530
|
this.touched.emit();
|
|
5286
6531
|
}
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
6532
|
+
/** A live Date runs the ONE date picker; any other control (or the canvas) runs none. */
|
|
6533
|
+
syncDate() {
|
|
6534
|
+
const element = this.host?.nativeElement;
|
|
6535
|
+
if (!this.viewReady || !element)
|
|
6536
|
+
return;
|
|
6537
|
+
const live = this.control === ControlType.Date && this.isPreview === true && typeof window !== 'undefined';
|
|
6538
|
+
if (!live) {
|
|
6539
|
+
this.picker?.destroy();
|
|
6540
|
+
this.picker = null;
|
|
6541
|
+
return;
|
|
6542
|
+
}
|
|
6543
|
+
// The view may not have rendered the button yet after a control change: attach on the next frame's DOM.
|
|
6544
|
+
queueMicrotask(() => {
|
|
6545
|
+
if (this.picker || this.control !== ControlType.Date)
|
|
6546
|
+
return;
|
|
6547
|
+
this.picker = attachDatePicker(element, {
|
|
6548
|
+
onPick: iso => this.emit(iso),
|
|
6549
|
+
lockScroll: () => this.pageScroll?.stop(),
|
|
6550
|
+
unlockScroll: () => this.pageScroll?.start(),
|
|
6551
|
+
});
|
|
6552
|
+
});
|
|
6553
|
+
}
|
|
6554
|
+
/** The canvas's Open state: the calendar drawn under the field (never on a live page). */
|
|
6555
|
+
syncDesignCalendar() {
|
|
6556
|
+
const element = this.host?.nativeElement;
|
|
6557
|
+
if (!this.viewReady || !element || this.isPreview === true)
|
|
6558
|
+
return;
|
|
6559
|
+
queueMicrotask(() => showCalendarDesign(element, this._designOpen && this.control === ControlType.Date));
|
|
6560
|
+
}
|
|
6561
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: IoxFieldComponent, deps: [{ token: FormRuntimeService, optional: true }, { token: IOX_FORM_MESSAGES }, { token: i0.ElementRef, optional: true }, { token: IS_PREVIEW, optional: true }, { token: PageScrollProvider, optional: true }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
6562
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.31", type: IoxFieldComponent, isStandalone: false, selector: "div[iox-field]", inputs: { nodeId: "nodeId", name: "name", label: "label", control: "control", placeholder: "placeholder", help: "help", required: "required", disabled: "disabled", labelHidden: "labelHidden", requiredMark: "requiredMark", rows: "rows", rangeMin: "rangeMin", rangeMax: "rangeMax", rangeStep: "rangeStep", accept: "accept", multiple: "multiple", appearance: "appearance", earliest: "earliest", latest: "latest", options: "options", optionsText: "optionsText", value: "value", rules: "rules", forceInvalid: "forceInvalid", designErrorText: "designErrorText", showValid: "showValid", designOpen: "designOpen" }, outputs: { valueChange: "valueChange", touched: "touched" }, host: { properties: { "class": "'iox-field iox-node-' + nodeId", "class.iox-invalid": "invalid", "class.iox-valid": "showValid", "attr.data-iox-field": "fieldName || null", "hidden": "control === 'hidden' ? true : null" } }, ngImport: i0, template: `
|
|
6563
|
+
@switch (control) {
|
|
6564
|
+
@case ('choices') {
|
|
6565
|
+
<fieldset [class]="'iox-field__choices ' + (appearance === 'list' ? 'iox-field__choices--list' : 'iox-field__choices--chips')"
|
|
6566
|
+
[attr.aria-describedby]="describedBy">
|
|
6567
|
+
<legend class="iox-field__label" [class.iox-field__label--hidden]="labelHidden" [id]="controlId + '-label'">{{ label }}@if (required) {<span class="iox-field__required" aria-hidden="true">{{ requiredMark }}</span>}</legend>
|
|
6568
|
+
<div [class]="appearance === 'list' ? 'iox-field__group' : 'iox-field__chips'">
|
|
6569
|
+
@for (option of choices; track option.value) {
|
|
6570
|
+
<label [class]="appearance === 'list' ? 'iox-field__option' : 'iox-field__chip'">
|
|
6571
|
+
<input [class]="appearance === 'list' ? 'iox-field__control' : 'iox-field__control iox-field__control--chip'"
|
|
6572
|
+
[type]="multiple ? 'checkbox' : 'radio'"
|
|
6573
|
+
[name]="fieldName"
|
|
6574
|
+
[value]="option.value"
|
|
6575
|
+
[checked]="isChosen(option.value)"
|
|
6576
|
+
[attr.aria-invalid]="invalid ? 'true' : null"
|
|
6577
|
+
[attr.disabled]="disabled ? '' : null"
|
|
6578
|
+
(change)="toggle(option.value, $any($event.target).checked)"
|
|
6579
|
+
(blur)="touch($event)">
|
|
6580
|
+
<span>{{ option.label }}</span>
|
|
6581
|
+
</label>
|
|
6582
|
+
}
|
|
6583
|
+
</div>
|
|
6584
|
+
</fieldset>
|
|
5295
6585
|
}
|
|
5296
|
-
|
|
6586
|
+
@default {
|
|
6587
|
+
<label class="iox-field__label"
|
|
6588
|
+
[class.iox-field__label--hidden]="labelHidden"
|
|
6589
|
+
[id]="controlId + '-label'"
|
|
6590
|
+
[attr.for]="controlId">{{ label }}@if (required) {<span class="iox-field__required" aria-hidden="true">{{ requiredMark }}</span>}</label>
|
|
6591
|
+
}
|
|
6592
|
+
}
|
|
5297
6593
|
|
|
5298
6594
|
@switch (control) {
|
|
6595
|
+
@case ('choices') {}
|
|
5299
6596
|
@case ('textarea') {
|
|
5300
6597
|
<textarea class="iox-field__control"
|
|
5301
6598
|
[id]="controlId"
|
|
@@ -5308,7 +6605,7 @@ class IoxFieldComponent {
|
|
|
5308
6605
|
[attr.disabled]="disabled ? '' : null"
|
|
5309
6606
|
[value]="value ?? ''"
|
|
5310
6607
|
(input)="emit($any($event.target).value)"
|
|
5311
|
-
(blur)="touch()"></textarea>
|
|
6608
|
+
(blur)="touch($event)"></textarea>
|
|
5312
6609
|
}
|
|
5313
6610
|
@case ('select') {
|
|
5314
6611
|
<select class="iox-field__control"
|
|
@@ -5319,7 +6616,7 @@ class IoxFieldComponent {
|
|
|
5319
6616
|
[attr.aria-describedby]="describedBy"
|
|
5320
6617
|
[attr.disabled]="disabled ? '' : null"
|
|
5321
6618
|
(change)="emit($any($event.target).value)"
|
|
5322
|
-
(blur)="touch()">
|
|
6619
|
+
(blur)="touch($event)">
|
|
5323
6620
|
<option value="" [selected]="!value">{{ placeholder || '' }}</option>
|
|
5324
6621
|
@for (option of choices; track option.value) {
|
|
5325
6622
|
<option [value]="option.value" [selected]="option.value === value">{{ option.label }}</option>
|
|
@@ -5337,7 +6634,7 @@ class IoxFieldComponent {
|
|
|
5337
6634
|
[attr.disabled]="disabled ? '' : null"
|
|
5338
6635
|
[checked]="value === true"
|
|
5339
6636
|
(change)="emit($any($event.target).checked)"
|
|
5340
|
-
(blur)="touch()">
|
|
6637
|
+
(blur)="touch($event)">
|
|
5341
6638
|
}
|
|
5342
6639
|
@case ('range') {
|
|
5343
6640
|
<input class="iox-field__control iox-field__control--range"
|
|
@@ -5351,7 +6648,7 @@ class IoxFieldComponent {
|
|
|
5351
6648
|
[attr.disabled]="disabled ? '' : null"
|
|
5352
6649
|
[value]="value ?? rangeMin"
|
|
5353
6650
|
(input)="emit(+$any($event.target).value)"
|
|
5354
|
-
(blur)="touch()">
|
|
6651
|
+
(blur)="touch($event)">
|
|
5355
6652
|
}
|
|
5356
6653
|
@case ('checkbox') {
|
|
5357
6654
|
<input class="iox-field__control iox-field__control--checkbox"
|
|
@@ -5364,7 +6661,7 @@ class IoxFieldComponent {
|
|
|
5364
6661
|
[attr.disabled]="disabled ? '' : null"
|
|
5365
6662
|
[checked]="value === true"
|
|
5366
6663
|
(change)="emit($any($event.target).checked)"
|
|
5367
|
-
(blur)="touch()">
|
|
6664
|
+
(blur)="touch($event)">
|
|
5368
6665
|
}
|
|
5369
6666
|
@case ('radio') {
|
|
5370
6667
|
<div class="iox-field__group" role="radiogroup" [attr.aria-labelledby]="controlId + '-label'">
|
|
@@ -5377,12 +6674,27 @@ class IoxFieldComponent {
|
|
|
5377
6674
|
[checked]="option.value === value"
|
|
5378
6675
|
[attr.disabled]="disabled ? '' : null"
|
|
5379
6676
|
(change)="emit(option.value)"
|
|
5380
|
-
(blur)="touch()">
|
|
6677
|
+
(blur)="touch($event)">
|
|
5381
6678
|
<span>{{ option.label }}</span>
|
|
5382
6679
|
</label>
|
|
5383
6680
|
}
|
|
5384
6681
|
</div>
|
|
5385
6682
|
}
|
|
6683
|
+
@case ('date') {
|
|
6684
|
+
<button type="button"
|
|
6685
|
+
class="iox-field__control iox-field__control--date"
|
|
6686
|
+
[id]="controlId"
|
|
6687
|
+
aria-haspopup="dialog"
|
|
6688
|
+
[attr.aria-expanded]="designOpen ? 'true' : 'false'"
|
|
6689
|
+
[attr.aria-labelledby]="controlId + '-label ' + controlId + '-value'"
|
|
6690
|
+
[attr.aria-invalid]="invalid ? 'true' : null"
|
|
6691
|
+
[attr.aria-describedby]="describedBy"
|
|
6692
|
+
[attr.disabled]="disabled ? '' : null"
|
|
6693
|
+
[attr.data-iox-date-min]="earliest || null"
|
|
6694
|
+
[attr.data-iox-date-max]="latest || null"
|
|
6695
|
+
(blur)="touch($event)"><span class="iox-field__date-value" [id]="controlId + '-value'" [attr.data-iox-empty]="dateWords ? null : ''">{{ dateWords || datePlaceholder }}</span><svg class="iox-field__date-icon" width="20" height="20" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="3.5" y="5" width="17" height="15.5" rx="2.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M3.5 10h17M8 3v4M16 3v4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></button>
|
|
6696
|
+
<input type="hidden" class="iox-field__date-input" [name]="fieldName" [value]="dateIso">
|
|
6697
|
+
}
|
|
5386
6698
|
@default {
|
|
5387
6699
|
<input class="iox-field__control"
|
|
5388
6700
|
[id]="controlId"
|
|
@@ -5396,7 +6708,7 @@ class IoxFieldComponent {
|
|
|
5396
6708
|
[attr.accept]="control === 'file' ? (accept || null) : null"
|
|
5397
6709
|
[value]="control === 'file' ? '' : (value ?? '')"
|
|
5398
6710
|
(input)="emit($any($event.target).value)"
|
|
5399
|
-
(blur)="touch()">
|
|
6711
|
+
(blur)="touch($event)">
|
|
5400
6712
|
}
|
|
5401
6713
|
}
|
|
5402
6714
|
|
|
@@ -5411,16 +6723,39 @@ class IoxFieldComponent {
|
|
|
5411
6723
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: IoxFieldComponent, decorators: [{
|
|
5412
6724
|
type: Component,
|
|
5413
6725
|
args: [{ selector: 'div[iox-field]', template: `
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
6726
|
+
@switch (control) {
|
|
6727
|
+
@case ('choices') {
|
|
6728
|
+
<fieldset [class]="'iox-field__choices ' + (appearance === 'list' ? 'iox-field__choices--list' : 'iox-field__choices--chips')"
|
|
6729
|
+
[attr.aria-describedby]="describedBy">
|
|
6730
|
+
<legend class="iox-field__label" [class.iox-field__label--hidden]="labelHidden" [id]="controlId + '-label'">{{ label }}@if (required) {<span class="iox-field__required" aria-hidden="true">{{ requiredMark }}</span>}</legend>
|
|
6731
|
+
<div [class]="appearance === 'list' ? 'iox-field__group' : 'iox-field__chips'">
|
|
6732
|
+
@for (option of choices; track option.value) {
|
|
6733
|
+
<label [class]="appearance === 'list' ? 'iox-field__option' : 'iox-field__chip'">
|
|
6734
|
+
<input [class]="appearance === 'list' ? 'iox-field__control' : 'iox-field__control iox-field__control--chip'"
|
|
6735
|
+
[type]="multiple ? 'checkbox' : 'radio'"
|
|
6736
|
+
[name]="fieldName"
|
|
6737
|
+
[value]="option.value"
|
|
6738
|
+
[checked]="isChosen(option.value)"
|
|
6739
|
+
[attr.aria-invalid]="invalid ? 'true' : null"
|
|
6740
|
+
[attr.disabled]="disabled ? '' : null"
|
|
6741
|
+
(change)="toggle(option.value, $any($event.target).checked)"
|
|
6742
|
+
(blur)="touch($event)">
|
|
6743
|
+
<span>{{ option.label }}</span>
|
|
6744
|
+
</label>
|
|
6745
|
+
}
|
|
6746
|
+
</div>
|
|
6747
|
+
</fieldset>
|
|
5420
6748
|
}
|
|
5421
|
-
|
|
6749
|
+
@default {
|
|
6750
|
+
<label class="iox-field__label"
|
|
6751
|
+
[class.iox-field__label--hidden]="labelHidden"
|
|
6752
|
+
[id]="controlId + '-label'"
|
|
6753
|
+
[attr.for]="controlId">{{ label }}@if (required) {<span class="iox-field__required" aria-hidden="true">{{ requiredMark }}</span>}</label>
|
|
6754
|
+
}
|
|
6755
|
+
}
|
|
5422
6756
|
|
|
5423
6757
|
@switch (control) {
|
|
6758
|
+
@case ('choices') {}
|
|
5424
6759
|
@case ('textarea') {
|
|
5425
6760
|
<textarea class="iox-field__control"
|
|
5426
6761
|
[id]="controlId"
|
|
@@ -5433,7 +6768,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
|
|
|
5433
6768
|
[attr.disabled]="disabled ? '' : null"
|
|
5434
6769
|
[value]="value ?? ''"
|
|
5435
6770
|
(input)="emit($any($event.target).value)"
|
|
5436
|
-
(blur)="touch()"></textarea>
|
|
6771
|
+
(blur)="touch($event)"></textarea>
|
|
5437
6772
|
}
|
|
5438
6773
|
@case ('select') {
|
|
5439
6774
|
<select class="iox-field__control"
|
|
@@ -5444,7 +6779,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
|
|
|
5444
6779
|
[attr.aria-describedby]="describedBy"
|
|
5445
6780
|
[attr.disabled]="disabled ? '' : null"
|
|
5446
6781
|
(change)="emit($any($event.target).value)"
|
|
5447
|
-
(blur)="touch()">
|
|
6782
|
+
(blur)="touch($event)">
|
|
5448
6783
|
<option value="" [selected]="!value">{{ placeholder || '' }}</option>
|
|
5449
6784
|
@for (option of choices; track option.value) {
|
|
5450
6785
|
<option [value]="option.value" [selected]="option.value === value">{{ option.label }}</option>
|
|
@@ -5462,7 +6797,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
|
|
|
5462
6797
|
[attr.disabled]="disabled ? '' : null"
|
|
5463
6798
|
[checked]="value === true"
|
|
5464
6799
|
(change)="emit($any($event.target).checked)"
|
|
5465
|
-
(blur)="touch()">
|
|
6800
|
+
(blur)="touch($event)">
|
|
5466
6801
|
}
|
|
5467
6802
|
@case ('range') {
|
|
5468
6803
|
<input class="iox-field__control iox-field__control--range"
|
|
@@ -5476,7 +6811,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
|
|
|
5476
6811
|
[attr.disabled]="disabled ? '' : null"
|
|
5477
6812
|
[value]="value ?? rangeMin"
|
|
5478
6813
|
(input)="emit(+$any($event.target).value)"
|
|
5479
|
-
(blur)="touch()">
|
|
6814
|
+
(blur)="touch($event)">
|
|
5480
6815
|
}
|
|
5481
6816
|
@case ('checkbox') {
|
|
5482
6817
|
<input class="iox-field__control iox-field__control--checkbox"
|
|
@@ -5489,7 +6824,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
|
|
|
5489
6824
|
[attr.disabled]="disabled ? '' : null"
|
|
5490
6825
|
[checked]="value === true"
|
|
5491
6826
|
(change)="emit($any($event.target).checked)"
|
|
5492
|
-
(blur)="touch()">
|
|
6827
|
+
(blur)="touch($event)">
|
|
5493
6828
|
}
|
|
5494
6829
|
@case ('radio') {
|
|
5495
6830
|
<div class="iox-field__group" role="radiogroup" [attr.aria-labelledby]="controlId + '-label'">
|
|
@@ -5502,12 +6837,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
|
|
|
5502
6837
|
[checked]="option.value === value"
|
|
5503
6838
|
[attr.disabled]="disabled ? '' : null"
|
|
5504
6839
|
(change)="emit(option.value)"
|
|
5505
|
-
(blur)="touch()">
|
|
6840
|
+
(blur)="touch($event)">
|
|
5506
6841
|
<span>{{ option.label }}</span>
|
|
5507
6842
|
</label>
|
|
5508
6843
|
}
|
|
5509
6844
|
</div>
|
|
5510
6845
|
}
|
|
6846
|
+
@case ('date') {
|
|
6847
|
+
<button type="button"
|
|
6848
|
+
class="iox-field__control iox-field__control--date"
|
|
6849
|
+
[id]="controlId"
|
|
6850
|
+
aria-haspopup="dialog"
|
|
6851
|
+
[attr.aria-expanded]="designOpen ? 'true' : 'false'"
|
|
6852
|
+
[attr.aria-labelledby]="controlId + '-label ' + controlId + '-value'"
|
|
6853
|
+
[attr.aria-invalid]="invalid ? 'true' : null"
|
|
6854
|
+
[attr.aria-describedby]="describedBy"
|
|
6855
|
+
[attr.disabled]="disabled ? '' : null"
|
|
6856
|
+
[attr.data-iox-date-min]="earliest || null"
|
|
6857
|
+
[attr.data-iox-date-max]="latest || null"
|
|
6858
|
+
(blur)="touch($event)"><span class="iox-field__date-value" [id]="controlId + '-value'" [attr.data-iox-empty]="dateWords ? null : ''">{{ dateWords || datePlaceholder }}</span><svg class="iox-field__date-icon" width="20" height="20" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="3.5" y="5" width="17" height="15.5" rx="2.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M3.5 10h17M8 3v4M16 3v4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></button>
|
|
6859
|
+
<input type="hidden" class="iox-field__date-input" [name]="fieldName" [value]="dateIso">
|
|
6860
|
+
}
|
|
5511
6861
|
@default {
|
|
5512
6862
|
<input class="iox-field__control"
|
|
5513
6863
|
[id]="controlId"
|
|
@@ -5521,7 +6871,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
|
|
|
5521
6871
|
[attr.accept]="control === 'file' ? (accept || null) : null"
|
|
5522
6872
|
[value]="control === 'file' ? '' : (value ?? '')"
|
|
5523
6873
|
(input)="emit($any($event.target).value)"
|
|
5524
|
-
(blur)="touch()">
|
|
6874
|
+
(blur)="touch($event)">
|
|
5525
6875
|
}
|
|
5526
6876
|
}
|
|
5527
6877
|
|
|
@@ -5543,6 +6893,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
|
|
|
5543
6893
|
}] }, { type: undefined, decorators: [{
|
|
5544
6894
|
type: Inject,
|
|
5545
6895
|
args: [IOX_FORM_MESSAGES]
|
|
6896
|
+
}] }, { type: i0.ElementRef, decorators: [{
|
|
6897
|
+
type: Optional
|
|
6898
|
+
}] }, { type: undefined, decorators: [{
|
|
6899
|
+
type: Optional
|
|
6900
|
+
}, {
|
|
6901
|
+
type: Inject,
|
|
6902
|
+
args: [IS_PREVIEW]
|
|
6903
|
+
}] }, { type: PageScrollProvider, decorators: [{
|
|
6904
|
+
type: Optional
|
|
5546
6905
|
}] }], propDecorators: { nodeId: [{
|
|
5547
6906
|
type: Input
|
|
5548
6907
|
}], name: [{
|
|
@@ -5573,6 +6932,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
|
|
|
5573
6932
|
type: Input
|
|
5574
6933
|
}], accept: [{
|
|
5575
6934
|
type: Input
|
|
6935
|
+
}], multiple: [{
|
|
6936
|
+
type: Input
|
|
6937
|
+
}], appearance: [{
|
|
6938
|
+
type: Input
|
|
6939
|
+
}], earliest: [{
|
|
6940
|
+
type: Input
|
|
6941
|
+
}], latest: [{
|
|
6942
|
+
type: Input
|
|
5576
6943
|
}], options: [{
|
|
5577
6944
|
type: Input
|
|
5578
6945
|
}], optionsText: [{
|
|
@@ -5587,6 +6954,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
|
|
|
5587
6954
|
type: Input
|
|
5588
6955
|
}], showValid: [{
|
|
5589
6956
|
type: Input
|
|
6957
|
+
}], designOpen: [{
|
|
6958
|
+
type: Input
|
|
5590
6959
|
}], valueChange: [{
|
|
5591
6960
|
type: Output
|
|
5592
6961
|
}], touched: [{
|
|
@@ -5600,36 +6969,60 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
|
|
|
5600
6969
|
* action list on the FORM (decision D9), so there is nothing here to wire and nothing to get out of
|
|
5601
6970
|
* sync. The button owns only its own appearance.
|
|
5602
6971
|
*
|
|
5603
|
-
* It follows its FORM's state (vectoriox/iox-ui#42): its
|
|
5604
|
-
*
|
|
5605
|
-
* state,
|
|
5606
|
-
* (`syncSubmitButtons`) the form's runtimes
|
|
5607
|
-
*
|
|
6972
|
+
* It follows its FORM's state (vectoriox/iox-ui#42, premium N2): its three labels — at rest, "While sending", "Sent"
|
|
6973
|
+
* with a check — are all in the markup, stacked in one cell, and the shared iox-forms.css shows the one the form's
|
|
6974
|
+
* `data-iox-state` names, swapped in place behind a 2px blur (Emil 87/139); the check draws once on Sent. Busy and
|
|
6975
|
+
* disabled are written by the ONE applier (`syncSubmitButtons`) the form's runtimes and the canvas call. The string
|
|
6976
|
+
* renderer writes the same markup (forms-core `submitButtonLabelsHtml`).
|
|
5608
6977
|
*/
|
|
5609
6978
|
class IoxSubmitButtonComponent {
|
|
5610
|
-
constructor() {
|
|
6979
|
+
constructor(resolveMessage = null, host = null) {
|
|
6980
|
+
this.resolveMessage = resolveMessage;
|
|
6981
|
+
this.host = host;
|
|
5611
6982
|
this.nodeId = '';
|
|
5612
6983
|
this.label = 'Submit';
|
|
5613
6984
|
this.loadingLabel = 'Sending…';
|
|
6985
|
+
/** What it says once its form has sent (N2). Empty: the catalog's "Sent", in the host's language. */
|
|
6986
|
+
this.sentLabel = '';
|
|
5614
6987
|
this.disableWhileSubmitting = true;
|
|
5615
6988
|
}
|
|
5616
|
-
|
|
5617
|
-
|
|
6989
|
+
/** The author's Sent words; else the host's translation of the catalog's; else "Sent" in the page's language. */
|
|
6990
|
+
get sentWords() {
|
|
6991
|
+
if (this.sentLabel)
|
|
6992
|
+
return this.sentLabel;
|
|
6993
|
+
const key = SUBMIT_MESSAGE_KEYS.Sent;
|
|
6994
|
+
const translated = (this.resolveMessage ?? defaultMessageResolver)(key);
|
|
6995
|
+
if (translated && translated !== key && translated !== DEFAULT_EN_MESSAGES[key])
|
|
6996
|
+
return translated;
|
|
6997
|
+
const element = this.host?.nativeElement;
|
|
6998
|
+
return sentWord(element?.closest?.('[lang]')?.getAttribute('lang') ?? element?.ownerDocument?.documentElement?.lang);
|
|
6999
|
+
}
|
|
7000
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: IoxSubmitButtonComponent, deps: [{ token: IOX_FORM_MESSAGES, optional: true }, { token: i0.ElementRef, optional: true }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
7001
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.31", type: IoxSubmitButtonComponent, isStandalone: false, selector: "button[iox-submit]", inputs: { nodeId: "nodeId", label: "label", loadingLabel: "loadingLabel", sentLabel: "sentLabel", disableWhileSubmitting: "disableWhileSubmitting" }, host: { properties: { "class": "'iox-submit iox-node-' + nodeId", "attr.type": "'submit'", "attr.data-iox-keep-enabled": "disableWhileSubmitting === false ? \"\" : null" } }, ngImport: i0, template: `<span class="iox-submit__labels"><span class="iox-submit__idle">{{ label }}</span><span class="iox-submit__busy">{{ loadingLabel || label }}</span><span class="iox-submit__sent"><svg class="iox-submit__check" width="16" height="16" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5 12.5l4.5 4.5L19 7.5" pathLength="100" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg><span>{{ sentWords }}</span></span></span>`, isInline: true, styles: [":host{box-sizing:border-box;cursor:pointer}\n"] }); }
|
|
5618
7002
|
}
|
|
5619
7003
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: IoxSubmitButtonComponent, decorators: [{
|
|
5620
7004
|
type: Component,
|
|
5621
|
-
args: [{ selector: 'button[iox-submit]', template:
|
|
7005
|
+
args: [{ selector: 'button[iox-submit]', template: `<span class="iox-submit__labels"><span class="iox-submit__idle">{{ label }}</span><span class="iox-submit__busy">{{ loadingLabel || label }}</span><span class="iox-submit__sent"><svg class="iox-submit__check" width="16" height="16" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5 12.5l4.5 4.5L19 7.5" pathLength="100" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg><span>{{ sentWords }}</span></span></span>`, host: {
|
|
5622
7006
|
'[class]': "'iox-submit iox-node-' + nodeId",
|
|
5623
7007
|
'[attr.type]': "'submit'",
|
|
5624
7008
|
// forms-core SUBMIT_KEEP_ENABLED_ATTRIBUTE — read by `syncSubmitButtons`.
|
|
5625
7009
|
'[attr.data-iox-keep-enabled]': 'disableWhileSubmitting === false ? "" : null',
|
|
5626
7010
|
}, standalone: false, styles: [":host{box-sizing:border-box;cursor:pointer}\n"] }]
|
|
5627
|
-
}],
|
|
7011
|
+
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
7012
|
+
type: Optional
|
|
7013
|
+
}, {
|
|
7014
|
+
type: Inject,
|
|
7015
|
+
args: [IOX_FORM_MESSAGES]
|
|
7016
|
+
}] }, { type: i0.ElementRef, decorators: [{
|
|
7017
|
+
type: Optional
|
|
7018
|
+
}] }], propDecorators: { nodeId: [{
|
|
5628
7019
|
type: Input
|
|
5629
7020
|
}], label: [{
|
|
5630
7021
|
type: Input
|
|
5631
7022
|
}], loadingLabel: [{
|
|
5632
7023
|
type: Input
|
|
7024
|
+
}], sentLabel: [{
|
|
7025
|
+
type: Input
|
|
5633
7026
|
}], disableWhileSubmitting: [{
|
|
5634
7027
|
type: Input
|
|
5635
7028
|
}] } });
|
|
@@ -6220,6 +7613,11 @@ const PART_CLASS_NAMES = {
|
|
|
6220
7613
|
help: 'iox-field__help',
|
|
6221
7614
|
error: 'iox-field__error',
|
|
6222
7615
|
required: 'iox-field__required',
|
|
7616
|
+
// Premium M3: a Choices field's chip (N1), a Date's calendar and each of its days (N8) — the classes forms-core
|
|
7617
|
+
// `fieldHtml` and the date picker runtime render.
|
|
7618
|
+
chip: 'iox-field__chip',
|
|
7619
|
+
calendar: 'iox-cal',
|
|
7620
|
+
calendarDay: 'iox-cal__day',
|
|
6223
7621
|
},
|
|
6224
7622
|
};
|
|
6225
7623
|
/** The class a declared part compiles against, e.g. ('Field', 'error') -> 'iox-field__error'.
|
|
@@ -8772,13 +10170,19 @@ class StringFormRuntime {
|
|
|
8772
10170
|
const name = this.fieldNameOf(event);
|
|
8773
10171
|
if (!name)
|
|
8774
10172
|
return;
|
|
10173
|
+
// Leaving one chip for the next, or the date button for its own calendar, is not leaving the FIELD: Smart timing
|
|
10174
|
+
// must not say "Choose a date" while the visitor is choosing one.
|
|
10175
|
+
const next = event.relatedTarget;
|
|
10176
|
+
if (next && this.fieldElement(name)?.contains(next))
|
|
10177
|
+
return;
|
|
8775
10178
|
this.state = onFieldBlur(this.state, name, this.values(), this.fullSpec());
|
|
8776
10179
|
this.render();
|
|
8777
10180
|
}
|
|
8778
10181
|
onSubmit(event) {
|
|
8779
10182
|
// Never the browser's own submission: it would reload the page with the answers in the address and send nothing.
|
|
8780
10183
|
event.preventDefault();
|
|
8781
|
-
|
|
10184
|
+
// Sent is final (premium N2): the button reads "Sent" and is disabled — nothing sends twice.
|
|
10185
|
+
if (this.inFlight || this.form.getAttribute(FORM_STATE_ATTRIBUTE) === FormRenderState.Success)
|
|
8782
10186
|
return;
|
|
8783
10187
|
const values = this.values();
|
|
8784
10188
|
this.state = onSubmitAttempt(this.state, values, this.fullSpec());
|
|
@@ -8856,10 +10260,16 @@ class StringFormRuntime {
|
|
|
8856
10260
|
const el = this.fieldElement(spec.name);
|
|
8857
10261
|
if (!el)
|
|
8858
10262
|
return undefined;
|
|
10263
|
+
// A Date's answer is the ISO day its hidden input carries (N8) — never the button's words.
|
|
10264
|
+
if (spec.control === ControlType.Date)
|
|
10265
|
+
return el.querySelector(`input.${FieldClass.DateInput}`)?.value ?? '';
|
|
8859
10266
|
const controls = this.controlsOf(el);
|
|
8860
10267
|
const first = controls[0];
|
|
8861
10268
|
if (!first)
|
|
8862
10269
|
return undefined;
|
|
10270
|
+
// Several answers (Choices, N1): the ticked options' values, in the options' order — forms-core decides.
|
|
10271
|
+
if (isMultiValueField(spec))
|
|
10272
|
+
return controls.filter(c => c.checked).map(c => c.value);
|
|
8863
10273
|
if (first.type === 'radio')
|
|
8864
10274
|
return controls.find(c => c.checked)?.value ?? '';
|
|
8865
10275
|
if (first.type === 'checkbox')
|
|
@@ -8920,12 +10330,14 @@ class StringFormRuntime {
|
|
|
8920
10330
|
const next = failureAnnouncement(errors);
|
|
8921
10331
|
Promise.resolve().then(() => { region.textContent = next; });
|
|
8922
10332
|
}
|
|
10333
|
+
/** Focus the first field that failed and shake it once (premium N2 — the ONE `shakeElement`). */
|
|
8923
10334
|
focusFirstInvalid() {
|
|
8924
10335
|
for (const field of this.spec.fields) {
|
|
8925
10336
|
if (!this.state.errors[field.name])
|
|
8926
10337
|
continue;
|
|
8927
10338
|
const el = this.fieldElement(field.name);
|
|
8928
10339
|
this.controlsOf(el ?? this.form)[0]?.focus();
|
|
10340
|
+
shakeElement(el);
|
|
8929
10341
|
return;
|
|
8930
10342
|
}
|
|
8931
10343
|
}
|
|
@@ -8959,5 +10371,5 @@ function attachStringForm(form, options = {}) {
|
|
|
8959
10371
|
* Generated bundle index. Do not edit.
|
|
8960
10372
|
*/
|
|
8961
10373
|
|
|
8962
|
-
export { A11yAttribute, A11yTrait, ACCESSIBLE_NAME_TYPES, AOSService, ARIA_CURRENT_PAGE, BACKDROP_TWIN_KEY, BACKDROP_WHOLE_KEY, BREAKPOINTS, Breakpoint, BreakpointQueryKind, BuilderButtonComponent, BuilderDividerComponent, BuilderHeadingComponent, BuilderIconComponent, BuilderImageComponent, BuilderLinkComponent, BuilderSpacerComponent, ButtonBlockComponent, COMPONENT_STATE_SELECTOR, CONTAINER_HTML_TAGS, CONTEXT_ACTION_TYPES, CURRENT_LINK_TYPES, CardComponent, ClientGalleryComponent, ClientListComponent, ClientListItemComponent, ClientRepeaterComponent, ClientSliderContainerComponent, ClientSliderSlideComponent, ComponentInstanceRegistryService, ComponentState, ContainerComponent, ContextStore, DECORATIVE_TYPES, DEFAULT_GALLERY_SCHEDULER, DEFAULT_INTERACTION_EASING, DEFAULT_PAGE_TRANSITION, DEFAULT_PAGE_TRANSITION_DURATION, DEFAULT_PAGE_TRANSITION_EASING, DEFAULT_PAGE_TRANSITION_PERSPECTIVE, DEFAULT_PHOSPHOR_WEIGHT, DEFAULT_WIDTH_BY_TYPE, DIALOG_CLOSE_ABOVE_ATTRIBUTE, DIALOG_NODE_TYPE, DeclarationContext, EASING_KEYWORDS, EASING_TOKENS, ENTRANCE_ACTION_TYPES, ENTRANCE_FALLBACK_CSS, ENTRANCE_NOSCRIPT_CSS, ENTRANCE_PRESTATE_ATTRIBUTE, EXIT_ACTION_TYPES, EasingKeyword, EasingToken, FormRenderState, FormRuntimeService, GALLERY_EFFECTS, GALLERY_EFFECT_DESCRIPTORS, GalleryLoop, HOVER_DEFAULT_TRANSITION_TRAITS, HOVER_MEDIA_QUERY, HOVER_STATES, INTERACTION_ANIMATION_ID, INTERACTION_HIDDEN_ATTRIBUTE, IOX_BUILDER_EVENTS, IOX_FORM_MESSAGES, IOX_FORM_NAVIGATOR, IOX_FORM_TRANSPORT, IS_PREVIEW, IoxAnimateContainer, IoxAosDirective, IoxAosModule, IoxBuilderComponentsModule, IoxComponentRegistryService, IoxDialogComponent, IoxFieldComponent, IoxFormComponent, IoxFormErrorComponent, IoxFormSuccessComponent, IoxPageComponent, IoxPageModule, IoxSubmitButtonComponent, IoxUiModule, IoxVideoComponent, LAST_EMITTED_STATES, LEGACY_DIRECTION_KEY, LEGACY_HOVER_TRANSITION_TRAITS, LEGACY_SWIPER_CONTROLS, LinkedContainerComponent, MANAGED_A11Y_ATTRIBUTES, MAX_LINES_KEY, NAMED_GROUP_ROLES, NO_PAGE_TRANSITION, ON_STATE_ROLES, OUTER_STYLE_PROPS, PAGE_STYLE_TOKENS, PAGE_TOKEN_FALLBACKS, PAGE_TRANSITION_CATEGORIES, PAGE_TRANSITION_PRESETS, PART_CLASS_NAMES, PART_PAGINATION_MODIFIER, PHONE_TRANSITION_QUERY, PIPE_DESCRIPTORS, PIPE_REGISTRY, PageScrollProvider, PageStyleField, PageStyleSource, PointerKind, REDUCED_MOTION_QUERY, RELATIONAL_STATE_PSEUDO, RevealActionType, SCROLLED_ATTRIBUTE, SCROLLED_STATE_ROLE, SCROLLED_THRESHOLD_PX, SCROLL_EASINGS, SELF_PLAYING_TRIGGERS, SLIDER_INPUTS_ATTRIBUTE, SLIDER_INPUT_NAMES, STATE_PSEUDO_CLASS, STRUCTURAL_STATES, SUPPRESSED_STYLE_PROPS_BY_TYPE, SWIPER_A11Y_OPTIONS, SWIPER_RESPONSIVE_TRAITS, SWIPER_TRIGGER_EVENTS, ScrollEasing, SectionComponent, SettleMode, StringFormRuntime, SwiperModuleName, TARGETED_STATE_SUFFIX, TEXT_ALIGN_KEY, TEXT_DIRECTION_KEY, TRANSITION_FEELS, TRANSITION_PROPERTY_KEY, TextAlign, TextBlockComponent, VALIDATION_STATES, VALIDATION_STATE_CLASS, VIRTUAL_TRAIT_GROUPS, VIRTUAL_TRAIT_KEYS, ViewPositionDirective, ViewPositionModule, VisibilityActionType, activeActions, animateAction, applyBindingPipes, applyFeel, attachStringForm, bootSlider, breakpointDef, breakpointForWidth, breakpointQueryFor, buildInteractionKeyframes, buildPartSelector, buildSwiperBreakpoints, buildTargetedStateSelector, buildTargetedSuffixSelector, buildTransitionTimeline, cascadeChain, clearInputOverride, clearOverride, closeAboveMediaQuery, collectContextSlotKeys, compileDeclarations, compilePageStyle, componentStatesFor, composeVirtualTraits, containerHtmlTag, contextActionEffect, contextSlotKeys, countOverrides, declaredInputNames, decomposeBackdropFilter, defaultHoverTransitionTraits, defaultMessageResolver, deriveSeoFromContent, describePageStyle, describeSeoFields, dialogCloseAboveMinWidth, domHasSliderParts, effectiveActionType, effectiveTransitionId, entrancePreStateStyle, entranceStartsHidden, feelFor, finalKeyframe, framesFromLiveValue, gateHoverRule, getSeoValue, hasHoverState, hasOwnPageTransition, hasOwnTransition, initialSlotValues, isActionEnabled, isContextAction, isCubicBezier, isCurrentLink, isEasingToken, isExternalOrFragment, isGradientPaint, isHoverPointer, isHoverState, isInputOverriddenAt, isInteractionEasing, isLastEmittedStateKey, isOverriddenAt, isPressFeedback, isScrolled, isStructuralState, isValidSlotKey, isWholeBackdropSet, keyframePropertyName, keyframeStyleEntries, legacyEnterToTransition, mediaQueryFor, mergeSwiperBreakpoints, migrateLegacyDirection, migrateLegacyTextAlign, motionAllowed, nearestCommonAncestorId, needsAccessibleName, needsGroupName, nodeA11yAttributes, nodeCssId, nodeTraitValue, nodeUsesContext, normalizeAppPath, normalizeGalleryImage, normalizeIconClass, orderStatesForEmit, pageTransitionOf, parentBreakpoint, parseUrlList, partClassName, planPartRule, planResponsiveRules, planScroll, planStateRule, planSwiperOptions, playDualLayerTransition, playInteraction, prefersReducedMotion, renderDefaultDeclarations, resetPageStyleField, resetSeoField, resolveBoundValue, resolveContextValue, resolveGalleryEffect, resolveGalleryUrls, resolveInputChain, resolveInputs, resolveInteractionEasing, resolvePageTransition, resolvePath, resolveSeo, resolveStateStyles, resolveStyleProps, resolveToggleVisibility, resolveValueChain, rewriteViewportUnits, safeSetInput, serializeA11yAttributes, setInputOverride, setOverride, setSeoAuto, setSeoBinding, setSeoOverride, setSeoValue, settleInteraction, settleModeFor, sliderControlElements, sliderInputsFromProps, sliderInputsOf, splitOuterProps, splitPressFeedback, stateNamesIn, statePseudoClass, stateSelectorSuffix, stateTransitionProperties, stringFormRuntimeOf, stripSuppressedProps, swiperAutoplayAllowed, swiperLoopOptions, swiperModuleNames, syncNodeA11yAttributes, syncSubmitButtons, targetedStateSelector, transitionPropertyList, viewportTransitionContext, watchScrolled, withContextSlots, withStateTransitions, wrapInBreakpointQuery, wrapInMediaQuery };
|
|
10374
|
+
export { A11yAttribute, A11yTrait, ACCESSIBLE_NAME_TYPES, AOSService, ARIA_CURRENT_PAGE, BACKDROP_TWIN_KEY, BACKDROP_WHOLE_KEY, BREAKPOINTS, Breakpoint, BreakpointQueryKind, BuilderButtonComponent, BuilderDividerComponent, BuilderHeadingComponent, BuilderIconComponent, BuilderImageComponent, BuilderLinkComponent, BuilderSpacerComponent, ButtonBlockComponent, CALENDAR_CLOSING_ATTRIBUTE, CALENDAR_DESIGN_ATTRIBUTE, CALENDAR_SHEET_QUERY, COMPONENT_STATE_SELECTOR, CONTAINER_HTML_TAGS, CONTEXT_ACTION_TYPES, CURRENT_LINK_TYPES, CalendarClass, CardComponent, ClientGalleryComponent, ClientListComponent, ClientListItemComponent, ClientRepeaterComponent, ClientSliderContainerComponent, ClientSliderSlideComponent, ComponentInstanceRegistryService, ComponentState, ContainerComponent, ContextStore, DECORATIVE_TYPES, DEFAULT_GALLERY_SCHEDULER, DEFAULT_INTERACTION_EASING, DEFAULT_PAGE_TRANSITION, DEFAULT_PAGE_TRANSITION_DURATION, DEFAULT_PAGE_TRANSITION_EASING, DEFAULT_PAGE_TRANSITION_PERSPECTIVE, DEFAULT_PHOSPHOR_WEIGHT, DEFAULT_WIDTH_BY_TYPE, DIALOG_CLOSE_ABOVE_ATTRIBUTE, DIALOG_NODE_TYPE, DatePickerRuntime, DeclarationContext, EASING_KEYWORDS, EASING_TOKENS, ENTRANCE_ACTION_TYPES, ENTRANCE_FALLBACK_CSS, ENTRANCE_NOSCRIPT_CSS, ENTRANCE_PRESTATE_ATTRIBUTE, EXIT_ACTION_TYPES, EasingKeyword, EasingToken, FLOATING_GAP, FLOATING_MARGIN, FloatingSide, FormRenderState, FormRuntimeService, GALLERY_EFFECTS, GALLERY_EFFECT_DESCRIPTORS, GalleryLoop, HOVER_DEFAULT_TRANSITION_TRAITS, HOVER_MEDIA_QUERY, HOVER_STATES, INTERACTION_ANIMATION_ID, INTERACTION_HIDDEN_ATTRIBUTE, IOX_BUILDER_EVENTS, IOX_FORM_MESSAGES, IOX_FORM_NAVIGATOR, IOX_FORM_TRANSPORT, IS_PREVIEW, IoxAnimateContainer, IoxAosDirective, IoxAosModule, IoxBuilderComponentsModule, IoxComponentRegistryService, IoxDialogComponent, IoxFieldComponent, IoxFormComponent, IoxFormErrorComponent, IoxFormSuccessComponent, IoxPageComponent, IoxPageModule, IoxSubmitButtonComponent, IoxUiModule, IoxVideoComponent, LAST_EMITTED_STATES, LEGACY_DIRECTION_KEY, LEGACY_HOVER_TRANSITION_TRAITS, LEGACY_SWIPER_CONTROLS, LinkedContainerComponent, MANAGED_A11Y_ATTRIBUTES, MAX_LINES_KEY, NAMED_GROUP_ROLES, NO_PAGE_TRANSITION, ON_STATE_ROLES, OUTER_STYLE_PROPS, PAGE_STYLE_TOKENS, PAGE_TOKEN_FALLBACKS, PAGE_TRANSITION_CATEGORIES, PAGE_TRANSITION_PRESETS, PART_CLASS_NAMES, PART_PAGINATION_MODIFIER, PHONE_TRANSITION_QUERY, PIPE_DESCRIPTORS, PIPE_REGISTRY, PageScrollProvider, PageStyleField, PageStyleSource, PointerKind, REDUCED_MOTION_QUERY, RELATIONAL_STATE_PSEUDO, RevealActionType, SCROLLED_ATTRIBUTE, SCROLLED_STATE_ROLE, SCROLLED_THRESHOLD_PX, SCROLL_EASINGS, SELF_PLAYING_TRIGGERS, SHAKE_DURATION_MS, SHAKE_EASING, SHAKE_KEYFRAMES, SLIDER_INPUTS_ATTRIBUTE, SLIDER_INPUT_NAMES, STATE_PSEUDO_CLASS, STRUCTURAL_STATES, SUPPRESSED_STYLE_PROPS_BY_TYPE, SWIPER_A11Y_OPTIONS, SWIPER_RESPONSIVE_TRAITS, SWIPER_TRIGGER_EVENTS, ScrollEasing, SectionComponent, SettleMode, StringFormRuntime, SwiperModuleName, TARGETED_STATE_SUFFIX, TEXT_ALIGN_KEY, TEXT_DIRECTION_KEY, TRANSITION_FEELS, TRANSITION_PROPERTY_KEY, TextAlign, TextBlockComponent, VALIDATION_STATES, VALIDATION_STATE_CLASS, VIRTUAL_TRAIT_GROUPS, VIRTUAL_TRAIT_KEYS, ViewPositionDirective, ViewPositionModule, VisibilityActionType, activeActions, animateAction, applyBindingPipes, applyFeel, attachDatePicker, attachDatePickers, attachStringForm, bootSlider, breakpointDef, breakpointForWidth, breakpointQueryFor, buildInteractionKeyframes, buildPartSelector, buildSwiperBreakpoints, buildTargetedStateSelector, buildTargetedSuffixSelector, buildTransitionTimeline, cascadeChain, clearInputOverride, clearOverride, closeAboveMediaQuery, collectContextSlotKeys, compileDeclarations, compilePageStyle, componentStatesFor, composeVirtualTraits, containerHtmlTag, contextActionEffect, contextSlotKeys, countOverrides, datePickerOf, declaredInputNames, decomposeBackdropFilter, defaultHoverTransitionTraits, defaultMessageResolver, deriveSeoFromContent, describePageStyle, describeSeoFields, dialogCloseAboveMinWidth, domHasSliderParts, effectiveActionType, effectiveTransitionId, entrancePreStateStyle, entranceStartsHidden, feelFor, fieldLanguage, finalKeyframe, framesFromLiveValue, gateHoverRule, getSeoValue, hasHoverState, hasOwnPageTransition, hasOwnTransition, initialSlotValues, isActionEnabled, isContextAction, isCubicBezier, isCurrentLink, isEasingToken, isExternalOrFragment, isGradientPaint, isHoverPointer, isHoverState, isInputOverriddenAt, isInteractionEasing, isLastEmittedStateKey, isOverriddenAt, isPressFeedback, isScrolled, isStructuralState, isValidSlotKey, isWholeBackdropSet, keyframePropertyName, keyframeStyleEntries, legacyEnterToTransition, mediaQueryFor, mergeSwiperBreakpoints, migrateLegacyDirection, migrateLegacyTextAlign, motionAllowed, nearestCommonAncestorId, needsAccessibleName, needsGroupName, nodeA11yAttributes, nodeCssId, nodeTraitValue, nodeUsesContext, normalizeAppPath, normalizeGalleryImage, normalizeIconClass, orderStatesForEmit, pageTransitionOf, parentBreakpoint, parseUrlList, partClassName, placeFloating, planPartRule, planResponsiveRules, planScroll, planStateRule, planSwiperOptions, playDualLayerTransition, playInteraction, prefersReducedMotion, renderDefaultDeclarations, resetPageStyleField, resetSeoField, resolveBoundValue, resolveContextValue, resolveGalleryEffect, resolveGalleryUrls, resolveInputChain, resolveInputs, resolveInteractionEasing, resolvePageTransition, resolvePath, resolveSeo, resolveStateStyles, resolveStyleProps, resolveToggleVisibility, resolveValueChain, rewriteViewportUnits, safeSetInput, serializeA11yAttributes, setDateFieldValue, setInputOverride, setOverride, setSeoAuto, setSeoBinding, setSeoOverride, setSeoValue, settleInteraction, settleModeFor, shakeElement, showCalendarDesign, sliderControlElements, sliderInputsFromProps, sliderInputsOf, splitOuterProps, splitPressFeedback, stateNamesIn, statePseudoClass, stateSelectorSuffix, stateTransitionProperties, stringFormRuntimeOf, stripSuppressedProps, swiperAutoplayAllowed, swiperLoopOptions, swiperModuleNames, syncNodeA11yAttributes, syncSubmitButtons, targetedStateSelector, transitionPropertyList, viewportTransitionContext, watchScrolled, withContextSlots, withStateTransitions, wrapInBreakpointQuery, wrapInMediaQuery };
|
|
8963
10375
|
//# sourceMappingURL=vectoriox-iox-ui.mjs.map
|