@vectoriox/iox-ui 5.82.2 → 5.83.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +1559 -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 +307 -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 +358 -10
|
@@ -23,6 +23,11 @@ var ControlType;
|
|
|
23
23
|
/** A slider over a numeric range — min/max/step ride the field. */
|
|
24
24
|
ControlType["Range"] = "range";
|
|
25
25
|
ControlType["Radio"] = "radio";
|
|
26
|
+
/** One or several of a few visible choices — chips (the default) or a list of radios / checkboxes (premium N1).
|
|
27
|
+
* Its `multiple` trait picks the value's shape: a string, or a list of strings. */
|
|
28
|
+
ControlType["Choices"] = "choices";
|
|
29
|
+
/** A date the visitor picks from the look's calendar (premium N8) — a button that reads the date in words and a
|
|
30
|
+
* hidden input carrying the ISO `YYYY-MM-DD` value. Never the browser's own date box. */
|
|
26
31
|
ControlType["Date"] = "date";
|
|
27
32
|
ControlType["Time"] = "time";
|
|
28
33
|
ControlType["File"] = "file";
|
|
@@ -44,7 +49,20 @@ var RuleName;
|
|
|
44
49
|
RuleName["Accepted"] = "accepted";
|
|
45
50
|
RuleName["FileType"] = "fileType";
|
|
46
51
|
RuleName["FileSize"] = "fileSize";
|
|
52
|
+
/** IMPLIED, never authored: an answer of a Choices / Radio / Dropdown must be one of the field's own options
|
|
53
|
+
* (premium N1) — a hand-crafted POST cannot file a value the form never offered. */
|
|
54
|
+
RuleName["OneOf"] = "oneOf";
|
|
55
|
+
/** IMPLIED, never authored: a Date's answer is a real `YYYY-MM-DD` day (premium N8). */
|
|
56
|
+
RuleName["ValidDate"] = "validDate";
|
|
47
57
|
})(RuleName || (RuleName = {}));
|
|
58
|
+
/** How a Choices field draws its options (premium N1). */
|
|
59
|
+
var ChoiceAppearance;
|
|
60
|
+
(function (ChoiceAppearance) {
|
|
61
|
+
/** Each option a chip — the label is the chip; the native input inside it is visually hidden. */
|
|
62
|
+
ChoiceAppearance["Chips"] = "chips";
|
|
63
|
+
/** The options as rows of the look's own radio / checkbox (DS9.1). */
|
|
64
|
+
ChoiceAppearance["List"] = "list";
|
|
65
|
+
})(ChoiceAppearance || (ChoiceAppearance = {}));
|
|
48
66
|
/** Named regexes so an author picks "Email" rather than writing one. */
|
|
49
67
|
var PatternPreset;
|
|
50
68
|
(function (PatternPreset) {
|
|
@@ -85,8 +103,21 @@ const IMPLIED_PATTERN = Object.freeze({
|
|
|
85
103
|
[ControlType.Tel]: PatternPreset.Phone,
|
|
86
104
|
[ControlType.Url]: PatternPreset.Url,
|
|
87
105
|
});
|
|
88
|
-
/**
|
|
89
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Whether this field's answer is a LIST rather than one value — a file list, or a Choices field that takes several
|
|
108
|
+
* (premium N1). Per FIELD, not per control: the same Choices control is single or multiple by its `multiple` trait.
|
|
109
|
+
* ONE reading for collect, validate, the runtimes' transport and the server (replaced the control-only
|
|
110
|
+
* `MULTI_VALUE_CONTROLS`, which nothing read).
|
|
111
|
+
*/
|
|
112
|
+
function isMultiValueField(field) {
|
|
113
|
+
if (!field)
|
|
114
|
+
return false;
|
|
115
|
+
if (field.control === ControlType.File)
|
|
116
|
+
return true;
|
|
117
|
+
return field.control === ControlType.Choices && field.multiple === true;
|
|
118
|
+
}
|
|
119
|
+
/** The controls whose answers must be one of the field's own options (the implied `oneOf` rule). */
|
|
120
|
+
const OPTION_CONTROLS = new Set([ControlType.Choices, ControlType.Radio, ControlType.Select]);
|
|
90
121
|
|
|
91
122
|
// The default message for every rule, as an i18n KEY — never a rendered string.
|
|
92
123
|
//
|
|
@@ -117,6 +148,26 @@ const RULE_MESSAGE_KEYS = Object.freeze({
|
|
|
117
148
|
[RuleName.Accepted]: `${MESSAGE_NAMESPACE}.accepted`,
|
|
118
149
|
[RuleName.FileType]: `${MESSAGE_NAMESPACE}.fileType`,
|
|
119
150
|
[RuleName.FileSize]: `${MESSAGE_NAMESPACE}.fileSize`,
|
|
151
|
+
[RuleName.OneOf]: `${MESSAGE_NAMESPACE}.oneOf`,
|
|
152
|
+
[RuleName.ValidDate]: `${MESSAGE_NAMESPACE}.validDate`,
|
|
153
|
+
});
|
|
154
|
+
/**
|
|
155
|
+
* What "required" says on a field you CHOOSE rather than type into (premium N1 / N8): "Choose an option", "Choose at
|
|
156
|
+
* least one", "Choose a date" — never "This field is required" beside a row of chips.
|
|
157
|
+
*/
|
|
158
|
+
const CHOICE_MESSAGE_KEYS = Object.freeze({
|
|
159
|
+
One: `${MESSAGE_NAMESPACE}.choose`,
|
|
160
|
+
Some: `${MESSAGE_NAMESPACE}.chooseSome`,
|
|
161
|
+
Date: `${MESSAGE_NAMESPACE}.chooseDate`,
|
|
162
|
+
});
|
|
163
|
+
/** A Date outside its Earliest / Latest. The calendar refuses those days, so this is a stale page or a hand POST. */
|
|
164
|
+
const DATE_MESSAGE_KEYS = Object.freeze({
|
|
165
|
+
Min: `${MESSAGE_NAMESPACE}.dateMin`,
|
|
166
|
+
Max: `${MESSAGE_NAMESPACE}.dateMax`,
|
|
167
|
+
});
|
|
168
|
+
/** The submit button's own words (premium N2): what it says once its form has sent, when the author wrote none. */
|
|
169
|
+
const SUBMIT_MESSAGE_KEYS = Object.freeze({
|
|
170
|
+
Sent: 'forms.submit.sent',
|
|
120
171
|
});
|
|
121
172
|
/**
|
|
122
173
|
* A pattern failure deserves better than "Invalid format". Each preset gets its own key so the
|
|
@@ -162,7 +213,265 @@ const DEFAULT_EN_MESSAGES = Object.freeze({
|
|
|
162
213
|
[FORM_ERROR_KEYS.Spam]: 'We couldn’t accept this submission',
|
|
163
214
|
[FORM_ERROR_KEYS.RateLimited]: 'Too many attempts — please wait a moment and try again',
|
|
164
215
|
[FORM_ERROR_KEYS.Server]: 'Something went wrong on our side — your answers are still here',
|
|
216
|
+
[RULE_MESSAGE_KEYS[RuleName.OneOf]]: 'Choose one of the options',
|
|
217
|
+
[RULE_MESSAGE_KEYS[RuleName.ValidDate]]: 'Choose a date from the calendar',
|
|
218
|
+
[CHOICE_MESSAGE_KEYS.One]: 'Choose an option',
|
|
219
|
+
[CHOICE_MESSAGE_KEYS.Some]: 'Choose at least one',
|
|
220
|
+
[CHOICE_MESSAGE_KEYS.Date]: 'Choose a date',
|
|
221
|
+
[DATE_MESSAGE_KEYS.Min]: 'Choose a later date',
|
|
222
|
+
[DATE_MESSAGE_KEYS.Max]: 'Choose an earlier date',
|
|
223
|
+
[SUBMIT_MESSAGE_KEYS.Sent]: 'Sent',
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* DATES — the date picker's pure half (premium components N8) and the date rules' one reading.
|
|
228
|
+
*
|
|
229
|
+
* A Date field's value is an ISO day, `YYYY-MM-DD`, compared as a string (the format sorts as the calendar does).
|
|
230
|
+
* Its `min` / `max` rules take an ISO day OR an offset in days from today (`{ offsetDays: 0 }` = today), so "from
|
|
231
|
+
* today on" stays true tomorrow without a re-save. ONE reading for the validator (client AND iox-cms), the calendar
|
|
232
|
+
* (which days it refuses) and the builder's Earliest / Latest controls.
|
|
233
|
+
*
|
|
234
|
+
* The calendar model (six rows of seven, roving focus, the APG keys) lives here too, so the DOM runtime only draws
|
|
235
|
+
* what this decides — the canvas, the engine's component path and its string path draw the SAME month.
|
|
236
|
+
*
|
|
237
|
+
* ⚠️ Framework-free, like the rest of forms-core. Local-time `Date` objects are used only to walk the calendar; a
|
|
238
|
+
* day leaves this file as an ISO string.
|
|
239
|
+
*/
|
|
240
|
+
const ISO_DAY = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
241
|
+
const OFFSET = /^[+-]?\d+$/;
|
|
242
|
+
const DAYS_IN_WEEK = 7;
|
|
243
|
+
/** Always six rows, so the panel never changes size between months (APG; Emil 74 — one object, one size). */
|
|
244
|
+
const CALENDAR_ROWS = 6;
|
|
245
|
+
const pad = (n, width = 2) => String(n).padStart(width, '0');
|
|
246
|
+
/** The ISO day of a local `Date`. */
|
|
247
|
+
function isoOfDate(d) {
|
|
248
|
+
return `${pad(d.getFullYear(), 4)}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
249
|
+
}
|
|
250
|
+
/** A local `Date` at midnight for an ISO day, or null when it is not one (2026-02-30 is not). */
|
|
251
|
+
function dateOfIso(iso) {
|
|
252
|
+
const m = typeof iso === 'string' ? ISO_DAY.exec(iso.trim()) : null;
|
|
253
|
+
if (!m)
|
|
254
|
+
return null;
|
|
255
|
+
const [y, mo, d] = [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
256
|
+
const date = new Date(y, mo - 1, d);
|
|
257
|
+
date.setFullYear(y);
|
|
258
|
+
return date.getFullYear() === y && date.getMonth() === mo - 1 && date.getDate() === d ? date : null;
|
|
259
|
+
}
|
|
260
|
+
/** True for a real calendar day written `YYYY-MM-DD`. */
|
|
261
|
+
function isIsoDate(value) {
|
|
262
|
+
return dateOfIso(value) !== null;
|
|
263
|
+
}
|
|
264
|
+
/** Today, as the device that runs this sees it (the visitor's local day in a browser; the server's on iox-cms). */
|
|
265
|
+
function todayIso(now = new Date()) {
|
|
266
|
+
return isoOfDate(now);
|
|
267
|
+
}
|
|
268
|
+
/** `iso` moved by `days` (negative goes back). */
|
|
269
|
+
function addDaysIso(iso, days) {
|
|
270
|
+
const d = dateOfIso(iso);
|
|
271
|
+
if (!d)
|
|
272
|
+
return iso;
|
|
273
|
+
return isoOfDate(new Date(d.getFullYear(), d.getMonth(), d.getDate() + days));
|
|
274
|
+
}
|
|
275
|
+
/** `iso` moved by `months`, its day clamped to the target month's last (31 Jan + 1 month = 28/29 Feb). */
|
|
276
|
+
function addMonthsIso(iso, months) {
|
|
277
|
+
const d = dateOfIso(iso);
|
|
278
|
+
if (!d)
|
|
279
|
+
return iso;
|
|
280
|
+
const first = new Date(d.getFullYear(), d.getMonth() + months, 1);
|
|
281
|
+
const last = new Date(first.getFullYear(), first.getMonth() + 1, 0).getDate();
|
|
282
|
+
return isoOfDate(new Date(first.getFullYear(), first.getMonth(), Math.min(d.getDate(), last)));
|
|
283
|
+
}
|
|
284
|
+
/** `iso` held inside [min, max] (either may be absent). */
|
|
285
|
+
function clampIso(iso, min, max) {
|
|
286
|
+
if (min && iso < min)
|
|
287
|
+
return min;
|
|
288
|
+
if (max && iso > max)
|
|
289
|
+
return max;
|
|
290
|
+
return iso;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* A Field's `earliest` / `latest` trait → the rule parameter. The trait is one string so the panel and a saved page
|
|
294
|
+
* stay simple: `''` = any day · an integer = that many days from today (`'0'` = today, `'30'` = a month ahead) · an
|
|
295
|
+
* ISO day. Anything else reads as no bound.
|
|
296
|
+
*/
|
|
297
|
+
function parseDateBound(raw) {
|
|
298
|
+
if (typeof raw === 'number' && Number.isInteger(raw))
|
|
299
|
+
return { offsetDays: raw };
|
|
300
|
+
if (typeof raw !== 'string')
|
|
301
|
+
return null;
|
|
302
|
+
const text = raw.trim();
|
|
303
|
+
if (!text)
|
|
304
|
+
return null;
|
|
305
|
+
if (OFFSET.test(text))
|
|
306
|
+
return { offsetDays: Number(text) };
|
|
307
|
+
return isIsoDate(text) ? { date: text } : null;
|
|
308
|
+
}
|
|
309
|
+
/** The `min` / `max` rules a Date field's Earliest / Latest traits give it (none for "any"). */
|
|
310
|
+
function dateBoundRules(earliest, latest) {
|
|
311
|
+
const rules = [];
|
|
312
|
+
const min = parseDateBound(earliest);
|
|
313
|
+
const max = parseDateBound(latest);
|
|
314
|
+
if (min)
|
|
315
|
+
rules.push({ name: RuleName.Min, params: { ...min } });
|
|
316
|
+
if (max)
|
|
317
|
+
rules.push({ name: RuleName.Max, params: { ...max } });
|
|
318
|
+
return rules;
|
|
319
|
+
}
|
|
320
|
+
/** True when a rule's params are a DATE bound (an ISO day or an offset), never a number bound. */
|
|
321
|
+
function isDateBoundParams(params) {
|
|
322
|
+
return !!params && (typeof params['date'] === 'string' || Number.isInteger(params['offsetDays']));
|
|
323
|
+
}
|
|
324
|
+
/** The ISO day a bound names today, or null when it names none. */
|
|
325
|
+
function resolveDateBound(params, today) {
|
|
326
|
+
if (!params)
|
|
327
|
+
return null;
|
|
328
|
+
if (typeof params['date'] === 'string' && isIsoDate(params['date']))
|
|
329
|
+
return params['date'];
|
|
330
|
+
if (Number.isInteger(params['offsetDays']))
|
|
331
|
+
return addDaysIso(today, params['offsetDays']);
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
/** The first and last day a Date field accepts today — the calendar's refused days and the validator's range. */
|
|
335
|
+
function fieldDateRange(field, today) {
|
|
336
|
+
let min = null;
|
|
337
|
+
let max = null;
|
|
338
|
+
for (const rule of field?.rules ?? []) {
|
|
339
|
+
if (rule.name === RuleName.Min)
|
|
340
|
+
min = resolveDateBound(rule.params, today) ?? min;
|
|
341
|
+
if (rule.name === RuleName.Max)
|
|
342
|
+
max = resolveDateBound(rule.params, today) ?? max;
|
|
343
|
+
}
|
|
344
|
+
return { min, max };
|
|
345
|
+
}
|
|
346
|
+
/** True when the field is a date picker (the control that reads these rules as days). */
|
|
347
|
+
function isDateField(field) {
|
|
348
|
+
return field?.control === ControlType.Date;
|
|
349
|
+
}
|
|
350
|
+
// ─── the week, the month, the keys ───────────────────────────────────────────
|
|
351
|
+
/** Regions whose week starts on Sunday when the runtime cannot say (Intl.Locale#getWeekInfo is not everywhere). */
|
|
352
|
+
const SUNDAY_REGIONS = new Set(['US', 'CA', 'IL', 'JP', 'BR', 'MX', 'PH', 'IN', 'KR', 'TW', 'ZA', 'AU']);
|
|
353
|
+
const SUNDAY_LANGUAGES = new Set(['he', 'iw', 'ja', 'ko']);
|
|
354
|
+
/**
|
|
355
|
+
* The first day of the week for a locale, 0 = Sunday … 6 = Saturday. `Intl.Locale(…).getWeekInfo()` (or the older
|
|
356
|
+
* `weekInfo` getter) when the runtime has it, else a small region table; a bare `en` reads as the US.
|
|
357
|
+
*/
|
|
358
|
+
function weekStartFor(locale) {
|
|
359
|
+
const tag = locale || 'en';
|
|
360
|
+
try {
|
|
361
|
+
const Ctor = Intl.Locale;
|
|
362
|
+
if (Ctor) {
|
|
363
|
+
const loc = new Ctor(tag);
|
|
364
|
+
const info = typeof loc.getWeekInfo === 'function' ? loc.getWeekInfo() : loc.weekInfo;
|
|
365
|
+
const first = Number(info?.firstDay);
|
|
366
|
+
if (first >= 1 && first <= 7)
|
|
367
|
+
return first % DAYS_IN_WEEK;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
catch { /* an unknown tag falls through to the table */ }
|
|
371
|
+
const [language, region] = tag.split(/[-_]/);
|
|
372
|
+
if (region && SUNDAY_REGIONS.has(region.toUpperCase()))
|
|
373
|
+
return 0;
|
|
374
|
+
if (!region && (language === 'en' || SUNDAY_LANGUAGES.has(language)))
|
|
375
|
+
return 0;
|
|
376
|
+
return 1;
|
|
377
|
+
}
|
|
378
|
+
/** The month `focus` sits in, as six rows of seven — the ONE grid every renderer draws. */
|
|
379
|
+
function calendarMonth(focus, options) {
|
|
380
|
+
const at = dateOfIso(focus) ?? dateOfIso(options.today) ?? new Date();
|
|
381
|
+
const year = at.getFullYear();
|
|
382
|
+
const month = at.getMonth();
|
|
383
|
+
const lead = (new Date(year, month, 1).getDay() - options.weekStart + DAYS_IN_WEEK) % DAYS_IN_WEEK;
|
|
384
|
+
const days = new Date(year, month + 1, 0).getDate();
|
|
385
|
+
const weeks = [];
|
|
386
|
+
for (let r = 0; r < CALENDAR_ROWS; r++) {
|
|
387
|
+
const row = [];
|
|
388
|
+
for (let c = 0; c < DAYS_IN_WEEK; c++) {
|
|
389
|
+
const n = r * DAYS_IN_WEEK + c - lead + 1;
|
|
390
|
+
if (n < 1 || n > days) {
|
|
391
|
+
row.push(null);
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
const iso = isoOfDate(new Date(year, month, n));
|
|
395
|
+
row.push({
|
|
396
|
+
iso, day: n,
|
|
397
|
+
today: iso === options.today,
|
|
398
|
+
selected: iso === options.selected,
|
|
399
|
+
disabled: (!!options.min && iso < options.min) || (!!options.max && iso > options.max),
|
|
400
|
+
focus: iso === focus,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
weeks.push(row);
|
|
404
|
+
}
|
|
405
|
+
const monthKey = (iso) => (iso ? iso.slice(0, 7) : null);
|
|
406
|
+
const here = monthKey(isoOfDate(new Date(year, month, 1)));
|
|
407
|
+
const minMonth = monthKey(options.min);
|
|
408
|
+
const maxMonth = monthKey(options.max);
|
|
409
|
+
return {
|
|
410
|
+
year, month, weeks,
|
|
411
|
+
weekdays: Array.from({ length: DAYS_IN_WEEK }, (_, i) => (options.weekStart + i) % DAYS_IN_WEEK),
|
|
412
|
+
canPrev: !minMonth || here > minMonth,
|
|
413
|
+
canNext: !maxMonth || here < maxMonth,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
/** The keys the grid answers (APG date picker dialog). */
|
|
417
|
+
var CalendarKey;
|
|
418
|
+
(function (CalendarKey) {
|
|
419
|
+
CalendarKey["Left"] = "ArrowLeft";
|
|
420
|
+
CalendarKey["Right"] = "ArrowRight";
|
|
421
|
+
CalendarKey["Up"] = "ArrowUp";
|
|
422
|
+
CalendarKey["Down"] = "ArrowDown";
|
|
423
|
+
CalendarKey["Home"] = "Home";
|
|
424
|
+
CalendarKey["End"] = "End";
|
|
425
|
+
CalendarKey["PageUp"] = "PageUp";
|
|
426
|
+
CalendarKey["PageDown"] = "PageDown";
|
|
427
|
+
})(CalendarKey || (CalendarKey = {}));
|
|
428
|
+
const KEY_MOVES = {
|
|
429
|
+
// The arrows follow the READING direction: in RTL "→" is a day back (APG; mirrored).
|
|
430
|
+
[CalendarKey.Right]: (iso, c) => addDaysIso(iso, c.ahead),
|
|
431
|
+
[CalendarKey.Left]: (iso, c) => addDaysIso(iso, -c.ahead),
|
|
432
|
+
[CalendarKey.Down]: iso => addDaysIso(iso, DAYS_IN_WEEK),
|
|
433
|
+
[CalendarKey.Up]: iso => addDaysIso(iso, -DAYS_IN_WEEK),
|
|
434
|
+
[CalendarKey.Home]: (iso, c) => addDaysIso(iso, -c.dow),
|
|
435
|
+
[CalendarKey.End]: (iso, c) => addDaysIso(iso, DAYS_IN_WEEK - 1 - c.dow),
|
|
436
|
+
// A month; with Shift, a year.
|
|
437
|
+
[CalendarKey.PageUp]: (iso, c) => addMonthsIso(iso, c.shift ? -12 : -1),
|
|
438
|
+
[CalendarKey.PageDown]: (iso, c) => addMonthsIso(iso, c.shift ? 12 : 1),
|
|
439
|
+
};
|
|
440
|
+
/**
|
|
441
|
+
* Where a key moves the focused day, held inside [min, max] — or null for a key the grid does not answer (Enter,
|
|
442
|
+
* Space and Escape are the runtime's: they pick or close, they never move).
|
|
443
|
+
*/
|
|
444
|
+
function calendarKeyTarget(key, focus, options) {
|
|
445
|
+
const move = KEY_MOVES[key];
|
|
446
|
+
const at = dateOfIso(focus);
|
|
447
|
+
if (!move || !at)
|
|
448
|
+
return null;
|
|
449
|
+
const dow = (at.getDay() - options.weekStart + DAYS_IN_WEEK) % DAYS_IN_WEEK;
|
|
450
|
+
return clampIso(move(focus, { ahead: options.rtl ? -1 : 1, dow, shift: !!options.shift }), options.min, options.max);
|
|
451
|
+
}
|
|
452
|
+
// ─── words ───────────────────────────────────────────────────────────────────
|
|
453
|
+
/** The date in words, in the site's language: "Thu, 20 November" — the year only when it is not this year's. */
|
|
454
|
+
function formatDateInWords(iso, locale, today) {
|
|
455
|
+
const d = dateOfIso(iso);
|
|
456
|
+
if (!d)
|
|
457
|
+
return '';
|
|
458
|
+
const sameYear = iso.slice(0, 4) === today.slice(0, 4);
|
|
459
|
+
try {
|
|
460
|
+
return new Intl.DateTimeFormat(locale || undefined, { weekday: 'short', day: 'numeric', month: 'long', ...(sameYear ? {} : { year: 'numeric' }) }).format(d);
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
return iso;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
const DATE_PICKER_WORDS = Object.freeze({
|
|
467
|
+
en: { choose: 'Choose a date', previousMonth: 'Previous month', nextMonth: 'Next month', close: 'Close' },
|
|
468
|
+
he: { choose: 'בחרו תאריך', previousMonth: 'החודש הקודם', nextMonth: 'החודש הבא', close: 'סגירה' },
|
|
165
469
|
});
|
|
470
|
+
/** The words for a language tag (`he-IL` → Hebrew); English for anything else. */
|
|
471
|
+
function datePickerWords(lang) {
|
|
472
|
+
const language = String(lang ?? '').toLowerCase().split(/[-_]/)[0];
|
|
473
|
+
return language === 'he' || language === 'iw' ? DATE_PICKER_WORDS.he : DATE_PICKER_WORDS.en;
|
|
474
|
+
}
|
|
166
475
|
|
|
167
476
|
// The validator. ONE implementation, run by the builder canvas, the SSR engine and `iox-cms`.
|
|
168
477
|
//
|
|
@@ -180,14 +489,14 @@ const DEFAULT_EN_MESSAGES = Object.freeze({
|
|
|
180
489
|
* number" at someone who simply left it blank — the single most common validation-UX bug.
|
|
181
490
|
* The FIRST failure wins: a visitor fixes one thing at a time.
|
|
182
491
|
*/
|
|
183
|
-
function validateField(value, spec, form = {}) {
|
|
492
|
+
function validateField(value, spec, form = {}, options = {}) {
|
|
184
493
|
const rules = effectiveRules(spec);
|
|
185
494
|
const empty = isEmpty(value, spec.control);
|
|
186
495
|
for (const rule of rules) {
|
|
187
496
|
const appliesToEmpty = rule.name === RuleName.Required || rule.name === RuleName.Accepted;
|
|
188
497
|
if (empty && !appliesToEmpty)
|
|
189
498
|
continue;
|
|
190
|
-
const failure = check(rule, value, spec, form);
|
|
499
|
+
const failure = check(rule, value, spec, form, options);
|
|
191
500
|
if (failure) {
|
|
192
501
|
return {
|
|
193
502
|
valid: false,
|
|
@@ -202,16 +511,33 @@ function validateField(value, spec, form = {}) {
|
|
|
202
511
|
* Validate a whole submission. Every field is checked (not short-circuited at the first bad one) so
|
|
203
512
|
* the visitor sees every problem at once instead of fixing them one reload at a time.
|
|
204
513
|
*/
|
|
205
|
-
function validateForm(values, spec) {
|
|
514
|
+
function validateForm(values, spec, options = {}) {
|
|
206
515
|
const errors = {};
|
|
207
516
|
for (const field of spec.fields ?? []) {
|
|
208
|
-
const result = validateField(values?.[field.name], field, values ?? {});
|
|
517
|
+
const result = validateField(values?.[field.name], field, values ?? {}, options);
|
|
209
518
|
if (!result.valid) {
|
|
210
519
|
errors[field.name] = { messageKey: result.messageKey, params: result.params };
|
|
211
520
|
}
|
|
212
521
|
}
|
|
213
522
|
return { valid: Object.keys(errors).length === 0, errors };
|
|
214
523
|
}
|
|
524
|
+
/**
|
|
525
|
+
* The answers in the shape each field takes (premium N1): a field that takes several answers gets a LIST (one answer
|
|
526
|
+
* posted on its own is a list of one; nothing is an empty list). ONE reading for a host's transport and the server,
|
|
527
|
+
* before validation — `isMultiValueField` decides. A single-answer field is left as posted: two answers to it is the
|
|
528
|
+
* validator's to refuse, never quietly cut to one.
|
|
529
|
+
*/
|
|
530
|
+
function normaliseAnswers(values, spec) {
|
|
531
|
+
const out = { ...(values ?? {}) };
|
|
532
|
+
for (const field of spec.fields ?? []) {
|
|
533
|
+
if (!isMultiValueField(field) || field.control === ControlType.File)
|
|
534
|
+
continue;
|
|
535
|
+
if (!(field.name in out))
|
|
536
|
+
continue;
|
|
537
|
+
out[field.name] = chosen(out[field.name]);
|
|
538
|
+
}
|
|
539
|
+
return out;
|
|
540
|
+
}
|
|
215
541
|
/** The name of the first field with an error, in spec order — what the browser should focus. */
|
|
216
542
|
function firstInvalidField(spec, errors) {
|
|
217
543
|
for (const field of spec.fields ?? [])
|
|
@@ -240,8 +566,54 @@ function effectiveRules(spec) {
|
|
|
240
566
|
if (preset && !has(RuleName.Pattern)) {
|
|
241
567
|
implied.push({ name: RuleName.Pattern, params: { preset } });
|
|
242
568
|
}
|
|
569
|
+
// An answer to a field of CHOICES must be one the form offered (premium N1) — only when the options are the
|
|
570
|
+
// field's own: options from a data source are not known here, so they are not judged.
|
|
571
|
+
if (OPTION_CONTROLS.has(spec.control) && !spec.optionsSource && (spec.options ?? []).length) {
|
|
572
|
+
implied.push({ name: RuleName.OneOf });
|
|
573
|
+
}
|
|
574
|
+
// A Date's answer is a real day before any bound is asked about it (premium N8).
|
|
575
|
+
if (spec.control === ControlType.Date)
|
|
576
|
+
implied.push({ name: RuleName.ValidDate });
|
|
243
577
|
return [...implied, ...declared];
|
|
244
578
|
}
|
|
579
|
+
/** "Required" in the words of the control: choose, not fill in. */
|
|
580
|
+
function requiredMessageKey(spec) {
|
|
581
|
+
if (spec.control === ControlType.Date)
|
|
582
|
+
return CHOICE_MESSAGE_KEYS.Date;
|
|
583
|
+
if (isMultiValueField(spec) && spec.control === ControlType.Choices)
|
|
584
|
+
return CHOICE_MESSAGE_KEYS.Some;
|
|
585
|
+
if (OPTION_CONTROLS.has(spec.control))
|
|
586
|
+
return CHOICE_MESSAGE_KEYS.One;
|
|
587
|
+
return RULE_MESSAGE_KEYS[RuleName.Required];
|
|
588
|
+
}
|
|
589
|
+
/** The answers a field of choices gave, as a list — a single answer is a list of one, nothing is an empty list. */
|
|
590
|
+
function chosen(value) {
|
|
591
|
+
if (Array.isArray(value))
|
|
592
|
+
return value;
|
|
593
|
+
return value === null || value === undefined || value === '' ? [] : [value];
|
|
594
|
+
}
|
|
595
|
+
/** The `oneOf` verdict: every answer is one of the options, and a single-answer field got at most one. */
|
|
596
|
+
function isOneOf(value, spec) {
|
|
597
|
+
const offered = new Set((spec.options ?? []).map(o => String(o.value)));
|
|
598
|
+
const answers = chosen(value);
|
|
599
|
+
if (!isMultiValueField(spec) && answers.length > 1)
|
|
600
|
+
return false;
|
|
601
|
+
return answers.every(a => (typeof a === 'string' || typeof a === 'number') && offered.has(String(a)));
|
|
602
|
+
}
|
|
603
|
+
/** A Date bound, as a failure or null — the bound resolved against today, widened by the server's slack. */
|
|
604
|
+
function dateBoundFailure(rule, value, options) {
|
|
605
|
+
const today = options.today ?? todayIso();
|
|
606
|
+
const slack = Math.max(0, Math.trunc(options.dateSlackDays ?? 0));
|
|
607
|
+
const bound = resolveDateBound(rule.params, today);
|
|
608
|
+
if (!bound || !isIsoDate(value))
|
|
609
|
+
return null;
|
|
610
|
+
const day = String(value);
|
|
611
|
+
if (rule.name === RuleName.Min && day < addDaysIso(bound, -slack))
|
|
612
|
+
return { messageKey: DATE_MESSAGE_KEYS.Min };
|
|
613
|
+
if (rule.name === RuleName.Max && day > addDaysIso(bound, slack))
|
|
614
|
+
return { messageKey: DATE_MESSAGE_KEYS.Max };
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
245
617
|
/** Empty means "the visitor gave us nothing" — per control type, not a blanket falsy check. */
|
|
246
618
|
function isEmpty(value, control) {
|
|
247
619
|
if (value === null || value === undefined)
|
|
@@ -254,15 +626,24 @@ function isEmpty(value, control) {
|
|
|
254
626
|
return value.trim() === '';
|
|
255
627
|
return false;
|
|
256
628
|
}
|
|
257
|
-
function check(rule, value, spec, form) {
|
|
629
|
+
function check(rule, value, spec, form, options) {
|
|
258
630
|
const fail = (messageKey, params) => ({ messageKey, params });
|
|
259
631
|
const defaultKey = RULE_MESSAGE_KEYS[rule.name];
|
|
260
632
|
const n = Number(rule.params?.['n']);
|
|
633
|
+
// A Date's min / max are DAYS (an ISO day or an offset from today), never a number to parseFloat — `2026-11-06`
|
|
634
|
+
// read as 2026 is what the numeric reading did.
|
|
635
|
+
if ((rule.name === RuleName.Min || rule.name === RuleName.Max) && spec.control === ControlType.Date && isDateBoundParams(rule.params)) {
|
|
636
|
+
return dateBoundFailure(rule, value, options);
|
|
637
|
+
}
|
|
261
638
|
switch (rule.name) {
|
|
262
639
|
case RuleName.Required:
|
|
263
|
-
return isEmpty(value, spec.control) ? fail(
|
|
640
|
+
return isEmpty(value, spec.control) ? fail(requiredMessageKey(spec)) : null;
|
|
264
641
|
case RuleName.Accepted:
|
|
265
642
|
return value === true ? null : fail(defaultKey);
|
|
643
|
+
case RuleName.OneOf:
|
|
644
|
+
return isOneOf(value, spec) ? null : fail(defaultKey);
|
|
645
|
+
case RuleName.ValidDate:
|
|
646
|
+
return isIsoDate(value) ? null : fail(defaultKey);
|
|
266
647
|
case RuleName.MinLength:
|
|
267
648
|
return text(value).length < n ? fail(defaultKey, { n }) : null;
|
|
268
649
|
case RuleName.MaxLength:
|
|
@@ -406,6 +787,68 @@ function uniqueFieldName(name, taken) {
|
|
|
406
787
|
n++;
|
|
407
788
|
return `${name}${n}`;
|
|
408
789
|
}
|
|
790
|
+
/** A Field's own key, before the form makes it unique: the author's override, else derived from the label. */
|
|
791
|
+
function baseFieldName(props) {
|
|
792
|
+
const own = typeof props?.name === 'string' ? props.name : '';
|
|
793
|
+
return own || deriveFieldName(String(props?.label ?? ''));
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* THE naming rule (vectoriox/iox-ui#43): a form's fields, in document order, each its base name made unique against
|
|
797
|
+
* the names before it — `email`, `email2`. The spec (`collectFormSpec`), the component path's runtime and the engine's
|
|
798
|
+
* string renderer all name a field from here, so the input's `name` and the saved spec's key can never differ.
|
|
799
|
+
*/
|
|
800
|
+
function uniqueFieldNames(baseNames) {
|
|
801
|
+
const taken = new Set();
|
|
802
|
+
return baseNames.map(base => {
|
|
803
|
+
const name = uniqueFieldName(base, taken);
|
|
804
|
+
taken.add(name);
|
|
805
|
+
return name;
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
/** The submitted name of every Field of a Form, in document order — `uniqueFieldNames` over its fields. */
|
|
809
|
+
function formFieldNames(node) {
|
|
810
|
+
if (!node || node.type !== FORM_NODE_TYPE)
|
|
811
|
+
return [];
|
|
812
|
+
return uniqueFieldNames(collectFieldNodes(node.children ?? []).map(f => baseFieldName(f.props)));
|
|
813
|
+
}
|
|
814
|
+
/** Each Field node of a Form → the name it submits under (`formFieldNames`, by node). */
|
|
815
|
+
function formFieldNameMap(node) {
|
|
816
|
+
const out = new Map();
|
|
817
|
+
if (!node || node.type !== FORM_NODE_TYPE)
|
|
818
|
+
return out;
|
|
819
|
+
const fields = collectFieldNodes(node.children ?? []);
|
|
820
|
+
uniqueFieldNames(fields.map(f => baseFieldName(f.props))).forEach((name, i) => out.set(fields[i], name));
|
|
821
|
+
return out;
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* A Field's spec from its props and the name its form gave it — ONE reading for the saved spec and the component
|
|
825
|
+
* path's runtime (it was written twice: `IoxFieldComponent.toSpec` never carried the options source, the default or a
|
|
826
|
+
* Date's bounds). A Date's Earliest / Latest (`earliest` / `latest`) become its `min` / `max` rules (premium N8).
|
|
827
|
+
*/
|
|
828
|
+
function fieldSpecOf(props, name) {
|
|
829
|
+
const p = props ?? {};
|
|
830
|
+
const control = p['control'] || ControlType.Text;
|
|
831
|
+
const spec = { name, control, label: p['label'] ?? '' };
|
|
832
|
+
if (p['required'])
|
|
833
|
+
spec.required = true;
|
|
834
|
+
const declared = normaliseRules(p['rules']);
|
|
835
|
+
const bounds = control === ControlType.Date
|
|
836
|
+
? dateBoundRules(p['earliest'], p['latest']).filter(b => !declared.some(r => r.name === b.name))
|
|
837
|
+
: [];
|
|
838
|
+
const rules = [...declared, ...bounds];
|
|
839
|
+
if (rules.length)
|
|
840
|
+
spec.rules = rules;
|
|
841
|
+
const options = fieldOptionsOf(p);
|
|
842
|
+
if (options.length)
|
|
843
|
+
spec.options = options;
|
|
844
|
+
if (p['optionsSource'])
|
|
845
|
+
spec.optionsSource = p['optionsSource'];
|
|
846
|
+
if (p['defaultValue'] !== undefined && p['defaultValue'] !== '')
|
|
847
|
+
spec.defaultValue = p['defaultValue'];
|
|
848
|
+
if (control === ControlType.Choices && p['multiple'] === true)
|
|
849
|
+
spec.multiple = true;
|
|
850
|
+
return spec;
|
|
851
|
+
}
|
|
409
852
|
/**
|
|
410
853
|
* Walk a Form subtree and produce its spec.
|
|
411
854
|
*
|
|
@@ -416,13 +859,9 @@ function collectFormSpec(node) {
|
|
|
416
859
|
if (!node || node.type !== FORM_NODE_TYPE)
|
|
417
860
|
return null;
|
|
418
861
|
const props = node.props ?? {};
|
|
419
|
-
const
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
const spec = toFieldSpec(fieldNode, taken);
|
|
423
|
-
taken.add(spec.name);
|
|
424
|
-
fields.push(spec);
|
|
425
|
-
}
|
|
862
|
+
const fieldNodes = collectFieldNodes(node.children ?? []);
|
|
863
|
+
const names = uniqueFieldNames(fieldNodes.map(f => baseFieldName(f.props)));
|
|
864
|
+
const fields = fieldNodes.map((f, i) => fieldSpecOf(f.props, names[i]));
|
|
426
865
|
return {
|
|
427
866
|
formId: props['formId'] ?? '',
|
|
428
867
|
name: props['name'] ?? 'Form',
|
|
@@ -453,31 +892,6 @@ function collectFieldNodes(nodes) {
|
|
|
453
892
|
}
|
|
454
893
|
return out;
|
|
455
894
|
}
|
|
456
|
-
function toFieldSpec(node, taken) {
|
|
457
|
-
const props = node.props ?? {};
|
|
458
|
-
const label = props['label'] ?? '';
|
|
459
|
-
// The author's explicit override wins; otherwise derive from the label.
|
|
460
|
-
const name = uniqueFieldName(props['name'] || deriveFieldName(label), taken);
|
|
461
|
-
const spec = {
|
|
462
|
-
name,
|
|
463
|
-
control: props['control'] ?? ControlType.Text,
|
|
464
|
-
label,
|
|
465
|
-
};
|
|
466
|
-
if (props['required'])
|
|
467
|
-
spec.required = true;
|
|
468
|
-
const rules = normaliseRules(props['rules']);
|
|
469
|
-
if (rules.length)
|
|
470
|
-
spec.rules = rules;
|
|
471
|
-
const options = fieldOptionsOf(props);
|
|
472
|
-
if (options.length)
|
|
473
|
-
spec.options = options;
|
|
474
|
-
if (props['optionsSource'])
|
|
475
|
-
spec.optionsSource = props['optionsSource'];
|
|
476
|
-
if (props['defaultValue'] !== undefined && props['defaultValue'] !== '') {
|
|
477
|
-
spec.defaultValue = props['defaultValue'];
|
|
478
|
-
}
|
|
479
|
-
return spec;
|
|
480
|
-
}
|
|
481
895
|
/** Drop anything that isn't a rule we know — a hand-edited page must not break validation. */
|
|
482
896
|
function normaliseRules(raw) {
|
|
483
897
|
if (!Array.isArray(raw))
|
|
@@ -606,8 +1020,9 @@ const TEXTUAL = [
|
|
|
606
1020
|
ControlType.Text, ControlType.Email, ControlType.Tel, ControlType.Url,
|
|
607
1021
|
ControlType.Password, ControlType.Textarea,
|
|
608
1022
|
];
|
|
609
|
-
/** Controls with a comparable magnitude.
|
|
610
|
-
|
|
1023
|
+
/** Controls with a comparable magnitude. A Date is not one of them: its bounds are DAYS (an ISO day or "N days from
|
|
1024
|
+
* today", `dates.ts`), set by the Field's own Earliest / Latest — a number box here read `2026-11-06` as 2026. */
|
|
1025
|
+
const NUMERIC = [ControlType.Number, ControlType.Time, ControlType.Range];
|
|
611
1026
|
const RULE_DESCRIPTORS = Object.freeze([
|
|
612
1027
|
{
|
|
613
1028
|
name: RuleName.MinLength, labelKey: 'forms.rules.minLength',
|
|
@@ -794,12 +1209,16 @@ function parseClientFormSpec(raw) {
|
|
|
794
1209
|
return null;
|
|
795
1210
|
}
|
|
796
1211
|
}
|
|
797
|
-
/**
|
|
798
|
-
*
|
|
799
|
-
*
|
|
1212
|
+
/**
|
|
1213
|
+
* While its form sends, a submit button is busy, and disabled unless its author turned that off; once the form has
|
|
1214
|
+
* SENT it stays disabled, reading "Sent" with its check — nothing sends a lead twice (premium N2, the success in place).
|
|
1215
|
+
* ONE rule for the canvas's Sending / Success states, the component path and the string path. Its labels are the shared
|
|
1216
|
+
* CSS's, keyed on the same `data-iox-state`.
|
|
1217
|
+
*/
|
|
800
1218
|
function submitButtonView(state, disableWhileSubmitting = true) {
|
|
801
1219
|
const busy = state === FormRenderState.Submitting;
|
|
802
|
-
|
|
1220
|
+
const sent = state === FormRenderState.Success;
|
|
1221
|
+
return { busy, disabled: sent || (busy && disableWhileSubmitting !== false) };
|
|
803
1222
|
}
|
|
804
1223
|
/** On a submit button whose author turned "Disable while sending" off. */
|
|
805
1224
|
const SUBMIT_KEEP_ENABLED_ATTRIBUTE = 'data-iox-keep-enabled';
|
|
@@ -808,17 +1227,30 @@ var SubmitLabelClass;
|
|
|
808
1227
|
SubmitLabelClass["Labels"] = "iox-submit__labels";
|
|
809
1228
|
SubmitLabelClass["Idle"] = "iox-submit__idle";
|
|
810
1229
|
SubmitLabelClass["Busy"] = "iox-submit__busy";
|
|
1230
|
+
SubmitLabelClass["Sent"] = "iox-submit__sent";
|
|
1231
|
+
SubmitLabelClass["Check"] = "iox-submit__check";
|
|
811
1232
|
})(SubmitLabelClass || (SubmitLabelClass = {}));
|
|
1233
|
+
/** The check the Sent label draws once (premium N2 — the rare-tier beat, Emil 135/136): one stroke, `pathLength` 100 so
|
|
1234
|
+
* the shared CSS draws it by `stroke-dashoffset` whatever its real length. */
|
|
1235
|
+
const SUBMIT_CHECK_SVG = `<svg class="${SubmitLabelClass.Check}" width="16" height="16" viewBox="0 0 24 24" aria-hidden="true" focusable="false">` +
|
|
1236
|
+
'<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>';
|
|
1237
|
+
/** "Sent" in the site's language, for a button whose author wrote no Sent words (a string-rendered site has no
|
|
1238
|
+
* translation service — the document's `lang` picks, like the date picker's words). */
|
|
1239
|
+
const SENT_WORDS = Object.freeze({ en: 'Sent', he: 'נשלח' });
|
|
1240
|
+
function sentWord(lang) {
|
|
1241
|
+
const language = String(lang ?? '').toLowerCase().split(/[-_]/)[0];
|
|
1242
|
+
return SENT_WORDS[language === 'iw' ? 'he' : language] ?? SENT_WORDS['en'];
|
|
1243
|
+
}
|
|
812
1244
|
/**
|
|
813
|
-
* A submit button's words, as markup: its
|
|
814
|
-
*
|
|
815
|
-
* `IoxSubmitButtonComponent`'s template; `escape` is the renderer's.
|
|
1245
|
+
* A submit button's words, as markup: its three labels stacked in one grid cell (the button keeps the longest one's
|
|
1246
|
+
* width and never jumps), each shown by the form's state through the shared CSS — swapped in place behind a 2px blur
|
|
1247
|
+
* (Emil 87/139). The string renderer's copy of `IoxSubmitButtonComponent`'s template; `escape` is the renderer's.
|
|
816
1248
|
*/
|
|
817
|
-
function submitButtonLabelsHtml(
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
`<span class="${SubmitLabelClass.
|
|
1249
|
+
function submitButtonLabelsHtml(labels, escape) {
|
|
1250
|
+
const busy = labels.busy || labels.idle;
|
|
1251
|
+
return `<span class="${SubmitLabelClass.Labels}"><span class="${SubmitLabelClass.Idle}">${escape(labels.idle)}</span>` +
|
|
1252
|
+
`<span class="${SubmitLabelClass.Busy}">${escape(busy)}</span>` +
|
|
1253
|
+
`<span class="${SubmitLabelClass.Sent}">${SUBMIT_CHECK_SVG}<span>${escape(labels.sent)}</span></span></span>`;
|
|
822
1254
|
}
|
|
823
1255
|
const FORM_HONEYPOT_CLASS = 'iox-form__hp';
|
|
824
1256
|
const FORM_ANNOUNCER_ATTRIBUTE = 'data-iox-form-announcer';
|
|
@@ -846,7 +1278,14 @@ var FormPresetId;
|
|
|
846
1278
|
FormPresetId["Newsletter"] = "newsletter";
|
|
847
1279
|
FormPresetId["Application"] = "application";
|
|
848
1280
|
FormPresetId["Booking"] = "booking";
|
|
1281
|
+
/** Premium N2: modelled on Doron's iox.studio brief — name, a phone that is also WhatsApp, the business, what they
|
|
1282
|
+
* need and a budget as chips, Send with WhatsApp beside it. */
|
|
1283
|
+
FormPresetId["Lead"] = "lead";
|
|
849
1284
|
})(FormPresetId || (FormPresetId = {}));
|
|
1285
|
+
/** The presets a generated plan may name (the plan wire's `form` enum) — every preset not held back by `inPlan`. */
|
|
1286
|
+
function planFormPresetIds() {
|
|
1287
|
+
return FORM_PRESETS.filter(p => p.inPlan !== false).map(p => p.id);
|
|
1288
|
+
}
|
|
850
1289
|
// forms-core is imported by both renderers (the field runtime), and a renderer that never reads the presets
|
|
851
1290
|
|
|
852
1291
|
// to it by form-presets.spec) — a read of `FormPresetId.X` counts as a side effect to a bundler and kept the
|
|
@@ -902,7 +1341,8 @@ const FORM_PRESETS = /* @__PURE__ */ Object.freeze([
|
|
|
902
1341
|
fields: [
|
|
903
1342
|
{ label: 'Name', control: 'text', required: true },
|
|
904
1343
|
{ label: 'Email', control: 'email', required: true },
|
|
905
|
-
|
|
1344
|
+
// The first day a booking may ask for is today (N8): the calendar refuses the past.
|
|
1345
|
+
{ label: 'Date', control: 'date', required: true, earliest: '0' },
|
|
906
1346
|
{ label: 'People', control: 'number', required: true },
|
|
907
1347
|
{ label: 'Notes', control: 'textarea', rows: 3, help: 'Allergies, access needs, anything useful' },
|
|
908
1348
|
],
|
|
@@ -910,6 +1350,27 @@ const FORM_PRESETS = /* @__PURE__ */ Object.freeze([
|
|
|
910
1350
|
successText: 'Request received — we’ll confirm by email.',
|
|
911
1351
|
errorText: 'We couldn’t send your request. Please try again.',
|
|
912
1352
|
},
|
|
1353
|
+
{
|
|
1354
|
+
id: 'lead', name: 'Lead', icon: 'ph-thin ph-handshake',
|
|
1355
|
+
description: 'Name, phone, the business and what they need, as chips. Stored, with WhatsApp beside Send.',
|
|
1356
|
+
formName: 'Lead form',
|
|
1357
|
+
fields: [
|
|
1358
|
+
{ label: 'Name', control: 'text', required: true },
|
|
1359
|
+
{ label: 'Phone / WhatsApp', control: 'tel', required: true },
|
|
1360
|
+
{ label: 'What is your business?', control: 'text' },
|
|
1361
|
+
// The words ARE the values: what arrives in the inbox and the email reads as the visitor chose it.
|
|
1362
|
+
{ label: 'What do you need?', control: 'choices', multiple: true,
|
|
1363
|
+
optionsText: 'Website\nBranding\nPhotos & video\nNot sure yet' },
|
|
1364
|
+
// Neutral sizes the author renames — never a price or a currency a studio could be held to.
|
|
1365
|
+
{ label: 'Estimated budget', control: 'choices',
|
|
1366
|
+
optionsText: 'Small project\nMedium\nLarge\nNot sure yet' },
|
|
1367
|
+
],
|
|
1368
|
+
submitLabel: 'Send',
|
|
1369
|
+
successText: 'Thanks — we’ll get back to you soon.',
|
|
1370
|
+
errorText: 'We couldn’t send your details. Please try again — your answers are still here.',
|
|
1371
|
+
channel: { kind: 'whatsapp', label: 'Message us on WhatsApp' },
|
|
1372
|
+
inPlan: false,
|
|
1373
|
+
},
|
|
913
1374
|
]);
|
|
914
1375
|
/** The preset with this id, or undefined. */
|
|
915
1376
|
function formPresetById(id) {
|
|
@@ -990,6 +1451,222 @@ function findFormContentInHosts(roots) {
|
|
|
990
1451
|
return out;
|
|
991
1452
|
}
|
|
992
1453
|
|
|
1454
|
+
/**
|
|
1455
|
+
* THE SECOND CHANNEL (premium components N2) — "or message us on WhatsApp", the button a form keeps beside its submit,
|
|
1456
|
+
* before AND after it sends.
|
|
1457
|
+
*
|
|
1458
|
+
* It is an ordinary node in the form's actions row (a ButtonBlock that links) carrying the ROLE `form.channel`, so the
|
|
1459
|
+
* shared CSS keeps it on screen when the form succeeds and the Publishable gate can find it. Its address is the
|
|
1460
|
+
* OWNER's fact: the builder builds it from what the author types (a number, an address, a link) and never invents one —
|
|
1461
|
+
* inserted empty, it blocks publishing until it is filled (generation-core `auditSite`, `empty-channel`).
|
|
1462
|
+
*
|
|
1463
|
+
* Framework-free, like the rest of forms-core.
|
|
1464
|
+
*/
|
|
1465
|
+
/** The role the channel's node carries (`iox-role-form-channel`). */
|
|
1466
|
+
const FORM_CHANNEL_ROLE = 'form.channel';
|
|
1467
|
+
/** The role of the row that holds the submit button and the channel (`iox-role-form-actions`). */
|
|
1468
|
+
const FORM_ACTIONS_ROLE = 'form.actions';
|
|
1469
|
+
var ChannelKind;
|
|
1470
|
+
(function (ChannelKind) {
|
|
1471
|
+
ChannelKind["WhatsApp"] = "whatsapp";
|
|
1472
|
+
ChannelKind["Phone"] = "phone";
|
|
1473
|
+
ChannelKind["Email"] = "email";
|
|
1474
|
+
ChannelKind["Link"] = "link";
|
|
1475
|
+
})(ChannelKind || (ChannelKind = {}));
|
|
1476
|
+
const CHANNEL_KINDS = [ChannelKind.WhatsApp, ChannelKind.Phone, ChannelKind.Email, ChannelKind.Link];
|
|
1477
|
+
const WHATSAPP_PREFIX = 'https://wa.me/';
|
|
1478
|
+
const TEL_PREFIX = 'tel:';
|
|
1479
|
+
const MAILTO_PREFIX = 'mailto:';
|
|
1480
|
+
const HTTP = /^https?:\/\//i;
|
|
1481
|
+
const EMAIL = /^[^\s@]+@[^\s@.]+(\.[^\s@.]+)+$/;
|
|
1482
|
+
/** A phone number's own characters; anything else is typing noise (spaces, dashes, brackets). */
|
|
1483
|
+
const DIGITS = /\d/g;
|
|
1484
|
+
/** WhatsApp's click-to-chat wants the full international number: digits only, no `+`, no leading `00`. */
|
|
1485
|
+
const INTERNATIONAL_00 = /^00/;
|
|
1486
|
+
/** A number shorter than this is not a number anyone can be reached on. */
|
|
1487
|
+
const MIN_DIGITS = 6;
|
|
1488
|
+
/**
|
|
1489
|
+
* The address a channel links to, built from what the author typed — or `''` when it gives nothing a visitor could
|
|
1490
|
+
* use (no digits, not an email). Never a guess: a WhatsApp number keeps exactly the digits typed (the panel asks for
|
|
1491
|
+
* the country code), a link without a scheme gets `https://`.
|
|
1492
|
+
*/
|
|
1493
|
+
function channelHref(kind, input) {
|
|
1494
|
+
const raw = typeof input === 'string' ? input.trim() : '';
|
|
1495
|
+
if (!raw)
|
|
1496
|
+
return '';
|
|
1497
|
+
switch (kind) {
|
|
1498
|
+
case ChannelKind.WhatsApp: {
|
|
1499
|
+
const digits = (raw.match(DIGITS) ?? []).join('').replace(INTERNATIONAL_00, '');
|
|
1500
|
+
return digits.length >= MIN_DIGITS ? `${WHATSAPP_PREFIX}${digits}` : '';
|
|
1501
|
+
}
|
|
1502
|
+
case ChannelKind.Phone: {
|
|
1503
|
+
const digits = (raw.match(DIGITS) ?? []).join('');
|
|
1504
|
+
if (digits.length < MIN_DIGITS)
|
|
1505
|
+
return '';
|
|
1506
|
+
return `${TEL_PREFIX}${raw.startsWith('+') ? '+' : ''}${digits}`;
|
|
1507
|
+
}
|
|
1508
|
+
case ChannelKind.Email:
|
|
1509
|
+
return EMAIL.test(raw) ? `${MAILTO_PREFIX}${raw}` : '';
|
|
1510
|
+
case ChannelKind.Link:
|
|
1511
|
+
return HTTP.test(raw) ? raw : `https://${raw.replace(/^\/+/, '')}`;
|
|
1512
|
+
default:
|
|
1513
|
+
return '';
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
/** A channel's address back into what the panel shows: its kind and the value the author typed. */
|
|
1517
|
+
function parseChannelHref(href) {
|
|
1518
|
+
const text = typeof href === 'string' ? href.trim() : '';
|
|
1519
|
+
if (text.toLowerCase().startsWith(WHATSAPP_PREFIX))
|
|
1520
|
+
return { kind: ChannelKind.WhatsApp, value: text.slice(WHATSAPP_PREFIX.length).split(/[?#]/)[0] };
|
|
1521
|
+
if (text.toLowerCase().startsWith(TEL_PREFIX))
|
|
1522
|
+
return { kind: ChannelKind.Phone, value: text.slice(TEL_PREFIX.length) };
|
|
1523
|
+
if (text.toLowerCase().startsWith(MAILTO_PREFIX))
|
|
1524
|
+
return { kind: ChannelKind.Email, value: text.slice(MAILTO_PREFIX.length).split('?')[0] };
|
|
1525
|
+
if (!text)
|
|
1526
|
+
return { kind: ChannelKind.WhatsApp, value: '' };
|
|
1527
|
+
return { kind: ChannelKind.Link, value: text };
|
|
1528
|
+
}
|
|
1529
|
+
/** True when the channel goes somewhere a visitor can use — the gate's "filled". */
|
|
1530
|
+
function isChannelFilled(href) {
|
|
1531
|
+
const { kind, value } = parseChannelHref(href);
|
|
1532
|
+
return !!value && channelHref(kind, value) !== '';
|
|
1533
|
+
}
|
|
1534
|
+
/** A WhatsApp or web channel opens in a new tab; a call or an email hands over to the device. */
|
|
1535
|
+
function channelOpensNewTab(kind) {
|
|
1536
|
+
return kind === ChannelKind.WhatsApp || kind === ChannelKind.Link;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
/**
|
|
1540
|
+
* A Field as MARKUP — the string renderer's copy of `IoxFieldComponent`'s template, in ONE place (it lived in the
|
|
1541
|
+
* engine's `layout-renderer.service.ts`, a second copy beside the component). The engine's string path writes it; the
|
|
1542
|
+
* component mirrors it element for element, so hydration replaces like with like and the shared CSS (iox-forms.css,
|
|
1543
|
+
* the contract's field baseline) reaches both.
|
|
1544
|
+
*
|
|
1545
|
+
* Choices (premium N1): a `<fieldset>` whose `<legend>` is the label, its options native radios (one answer — the
|
|
1546
|
+
* arrow keys move and pick, with no script) or checkboxes (several), each inside a `<label>` that IS the chip; the
|
|
1547
|
+
* input is visually hidden, never removed. Date (N8): a `<button>` that says the date in words and opens the look's
|
|
1548
|
+
* calendar, with a hidden input carrying the ISO day — the raw SSR is the final markup (nothing shifts on hydrate).
|
|
1549
|
+
*
|
|
1550
|
+
* Framework-free, like the rest of forms-core: `escape` is the caller's.
|
|
1551
|
+
*/
|
|
1552
|
+
/** The classes the markup carries — the runtimes and the shared CSS key on these, never on a literal. */
|
|
1553
|
+
var FieldClass;
|
|
1554
|
+
(function (FieldClass) {
|
|
1555
|
+
FieldClass["Root"] = "iox-field";
|
|
1556
|
+
FieldClass["Label"] = "iox-field__label";
|
|
1557
|
+
FieldClass["LabelHidden"] = "iox-field__label--hidden";
|
|
1558
|
+
FieldClass["Required"] = "iox-field__required";
|
|
1559
|
+
FieldClass["Control"] = "iox-field__control";
|
|
1560
|
+
FieldClass["Help"] = "iox-field__help";
|
|
1561
|
+
FieldClass["Error"] = "iox-field__error";
|
|
1562
|
+
FieldClass["Group"] = "iox-field__group";
|
|
1563
|
+
FieldClass["Option"] = "iox-field__option";
|
|
1564
|
+
FieldClass["Choices"] = "iox-field__choices";
|
|
1565
|
+
FieldClass["ChoicesChips"] = "iox-field__choices--chips";
|
|
1566
|
+
FieldClass["ChoicesList"] = "iox-field__choices--list";
|
|
1567
|
+
FieldClass["Chips"] = "iox-field__chips";
|
|
1568
|
+
FieldClass["Chip"] = "iox-field__chip";
|
|
1569
|
+
FieldClass["ChipInput"] = "iox-field__control--chip";
|
|
1570
|
+
FieldClass["DateButton"] = "iox-field__control--date";
|
|
1571
|
+
FieldClass["DateValue"] = "iox-field__date-value";
|
|
1572
|
+
FieldClass["DateIcon"] = "iox-field__date-icon";
|
|
1573
|
+
FieldClass["DateInput"] = "iox-field__date-input";
|
|
1574
|
+
})(FieldClass || (FieldClass = {}));
|
|
1575
|
+
/** On the date button: the raw Earliest / Latest (`'0'`, an ISO day), resolved by the visitor's own today. */
|
|
1576
|
+
const DATE_MIN_ATTRIBUTE = 'data-iox-date-min';
|
|
1577
|
+
const DATE_MAX_ATTRIBUTE = 'data-iox-date-max';
|
|
1578
|
+
/** On the date's words while no day is chosen (the placeholder's colour keys on it). */
|
|
1579
|
+
const DATE_EMPTY_ATTRIBUTE = 'data-iox-empty';
|
|
1580
|
+
/** The calendar glyph inside the date button: one path set, drawn in currentColor, never announced. */
|
|
1581
|
+
const DATE_ICON_SVG = '<svg class="iox-field__date-icon" width="20" height="20" viewBox="0 0 24 24" aria-hidden="true" focusable="false">' +
|
|
1582
|
+
'<rect x="3.5" y="5" width="17" height="15.5" rx="2.5" fill="none" stroke="currentColor" stroke-width="1.6"/>' +
|
|
1583
|
+
'<path d="M3.5 10h17M8 3v4M16 3v4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>';
|
|
1584
|
+
/** The controls that are a plain `<input type=…>`; anything else unknown is a text input. */
|
|
1585
|
+
const INPUT_TYPES = new Set(['email', 'tel', 'url', 'number', 'password', 'time', 'file', 'hidden']);
|
|
1586
|
+
/** The `<input type>` of a control that renders as a plain input. */
|
|
1587
|
+
function inputTypeOf(control) {
|
|
1588
|
+
return INPUT_TYPES.has(control) ? control : 'text';
|
|
1589
|
+
}
|
|
1590
|
+
/** A Choices field's appearance; anything unknown reads as chips (the default). */
|
|
1591
|
+
function choiceAppearanceOf(props) {
|
|
1592
|
+
return props?.['appearance'] === ChoiceAppearance.List ? ChoiceAppearance.List : ChoiceAppearance.Chips;
|
|
1593
|
+
}
|
|
1594
|
+
/** A Field's whole element, as the string renderer writes it. */
|
|
1595
|
+
function fieldHtml(props, options) {
|
|
1596
|
+
const p = props ?? {};
|
|
1597
|
+
const { name, id, escape: esc } = options;
|
|
1598
|
+
const control = String(p['control'] ?? ControlType.Text);
|
|
1599
|
+
const required = !!p['required'];
|
|
1600
|
+
const help = p['help'] ? `<p class="${FieldClass.Help}" id="${id}-help">${esc(String(p['help']))}</p>` : '';
|
|
1601
|
+
const describedBy = p['help'] ? ` aria-describedby="${id}-help"` : '';
|
|
1602
|
+
const labelClass = p['labelHidden'] ? `${FieldClass.Label} ${FieldClass.LabelHidden}` : FieldClass.Label;
|
|
1603
|
+
const mark = required ? `<span class="${FieldClass.Required}" aria-hidden="true">*</span>` : '';
|
|
1604
|
+
const text = esc(String(p['label'] ?? ''));
|
|
1605
|
+
const label = `<label class="${labelClass}" id="${id}-label" for="${id}">${text}${mark}</label>`;
|
|
1606
|
+
const hidden = control === ControlType.Hidden ? ' hidden' : '';
|
|
1607
|
+
const open = `<div class="${FieldClass.Root} ${options.cls}" data-iox-field="${esc(name)}"${hidden}${options.attrs ?? ''}>`;
|
|
1608
|
+
if (control === ControlType.Choices) {
|
|
1609
|
+
return `${open}${choicesHtml(p, options, `<legend class="${labelClass}" id="${id}-label">${text}${mark}</legend>`, describedBy)}${help}</div>`;
|
|
1610
|
+
}
|
|
1611
|
+
return `${open}${label}${controlHtml(p, control, options, describedBy)}${help}</div>`;
|
|
1612
|
+
}
|
|
1613
|
+
function controlHtml(p, control, options, describedBy) {
|
|
1614
|
+
const { id, escape: esc } = options;
|
|
1615
|
+
const name = esc(options.name);
|
|
1616
|
+
const required = p['required'] ? ' required' : '';
|
|
1617
|
+
const placeholder = p['placeholder'] ? ` placeholder="${esc(String(p['placeholder']))}"` : '';
|
|
1618
|
+
const choices = fieldOptionsOf(p);
|
|
1619
|
+
const option = (o) => `<option value="${esc(o.value)}">${esc(o.label)}</option>`;
|
|
1620
|
+
const num = (v, d) => (Number.isFinite(Number(v)) && v !== '' && v !== null ? Number(v) : d);
|
|
1621
|
+
switch (control) {
|
|
1622
|
+
case ControlType.Textarea:
|
|
1623
|
+
return `<textarea class="${FieldClass.Control}" id="${id}" name="${name}" rows="${num(p['rows'], 4)}"${placeholder}${required}${describedBy}></textarea>`;
|
|
1624
|
+
case ControlType.Select:
|
|
1625
|
+
return `<select class="${FieldClass.Control}" id="${id}" name="${name}"${required}${describedBy}>` +
|
|
1626
|
+
`<option value="">${esc(String(p['placeholder'] ?? ''))}</option>${choices.map(option).join('')}</select>`;
|
|
1627
|
+
case ControlType.Switch:
|
|
1628
|
+
return `<input class="${FieldClass.Control} iox-field__control--switch" type="checkbox" role="switch" id="${id}" name="${name}"${describedBy}>`;
|
|
1629
|
+
case ControlType.Range:
|
|
1630
|
+
return `<input class="${FieldClass.Control} iox-field__control--range" type="range" id="${id}" name="${name}" ` +
|
|
1631
|
+
`min="${num(p['rangeMin'], 0)}" max="${num(p['rangeMax'], 100)}" step="${num(p['rangeStep'], 1)}"${describedBy}>`;
|
|
1632
|
+
case ControlType.Radio:
|
|
1633
|
+
return `<div class="${FieldClass.Group}" role="radiogroup" aria-labelledby="${id}-label">` +
|
|
1634
|
+
choices.map(o => `<label class="${FieldClass.Option}"><input class="${FieldClass.Control} iox-field__control--radio" type="radio" name="${name}" ` +
|
|
1635
|
+
`value="${esc(o.value)}"><span>${esc(o.label)}</span></label>`).join('') + `</div>`;
|
|
1636
|
+
case ControlType.Checkbox:
|
|
1637
|
+
return `<input class="${FieldClass.Control} iox-field__control--checkbox" type="checkbox" id="${id}" name="${name}"${required}${describedBy}>`;
|
|
1638
|
+
case ControlType.Date:
|
|
1639
|
+
return dateHtml(p, options, describedBy);
|
|
1640
|
+
default:
|
|
1641
|
+
return `<input class="${FieldClass.Control}" id="${id}" name="${name}" type="${inputTypeOf(control)}"${placeholder}${required}${describedBy}>`;
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
/** A Choices field's fieldset: the legend, then the options as chips or as a list. */
|
|
1645
|
+
function choicesHtml(p, options, legend, describedBy) {
|
|
1646
|
+
const { escape: esc } = options;
|
|
1647
|
+
const name = esc(options.name);
|
|
1648
|
+
const multiple = p['multiple'] === true;
|
|
1649
|
+
const type = multiple ? 'checkbox' : 'radio';
|
|
1650
|
+
const chips = choiceAppearanceOf(p) === ChoiceAppearance.Chips;
|
|
1651
|
+
const items = fieldOptionsOf(p).map(o => chips
|
|
1652
|
+
? `<label class="${FieldClass.Chip}"><input class="${FieldClass.Control} ${FieldClass.ChipInput}" type="${type}" name="${name}" value="${esc(o.value)}"><span>${esc(o.label)}</span></label>`
|
|
1653
|
+
: `<label class="${FieldClass.Option}"><input class="${FieldClass.Control}" type="${type}" name="${name}" value="${esc(o.value)}"><span>${esc(o.label)}</span></label>`).join('');
|
|
1654
|
+
const look = chips ? FieldClass.ChoicesChips : FieldClass.ChoicesList;
|
|
1655
|
+
const row = chips ? FieldClass.Chips : FieldClass.Group;
|
|
1656
|
+
return `<fieldset class="${FieldClass.Choices} ${look}"${describedBy}>${legend}<div class="${row}">${items}</div></fieldset>`;
|
|
1657
|
+
}
|
|
1658
|
+
/** The date picker's button + hidden input. The calendar itself is the runtime's, opened on press. */
|
|
1659
|
+
function dateHtml(p, options, describedBy) {
|
|
1660
|
+
const { id, escape: esc } = options;
|
|
1661
|
+
const words = datePickerWords(options.lang);
|
|
1662
|
+
const empty = esc(String(p['placeholder'] || words.choose));
|
|
1663
|
+
const bound = (attr, raw) => (typeof raw === 'string' && raw.trim() !== '') || typeof raw === 'number' ? ` ${attr}="${esc(String(raw).trim())}"` : '';
|
|
1664
|
+
return `<button type="button" class="${FieldClass.Control} ${FieldClass.DateButton}" id="${id}" aria-haspopup="dialog" aria-expanded="false" ` +
|
|
1665
|
+
`aria-labelledby="${id}-label ${id}-value"${describedBy}${bound(DATE_MIN_ATTRIBUTE, p['earliest'])}${bound(DATE_MAX_ATTRIBUTE, p['latest'])}>` +
|
|
1666
|
+
`<span class="${FieldClass.DateValue}" id="${id}-value" ${DATE_EMPTY_ATTRIBUTE}>${empty}</span>${DATE_ICON_SVG}</button>` +
|
|
1667
|
+
`<input type="hidden" class="${FieldClass.DateInput}" name="${esc(options.name)}" value="">`;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
993
1670
|
/*
|
|
994
1671
|
* @vectoriox/iox-ui/forms-core — the shared, FRAMEWORK-FREE core of the forms feature.
|
|
995
1672
|
*
|
|
@@ -1006,5 +1683,5 @@ function findFormContentInHosts(roots) {
|
|
|
1006
1683
|
* Generated bundle index. Do not edit.
|
|
1007
1684
|
*/
|
|
1008
1685
|
|
|
1009
|
-
export { ControlType, DEFAULT_EN_MESSAGES, FIELD_GROUP_NODE_TYPE, FIELD_NODE_TYPE, FORM_ANNOUNCER_ATTRIBUTE, FORM_CONTENT_TYPES, FORM_ERROR_KEYS, FORM_FIELDS_ATTRIBUTE, FORM_HONEYPOT_CLASS, FORM_NODE_TYPE, FORM_PRESETS, FORM_STATE_ATTRIBUTE, FeedbackTiming, FormPresetId, FormRenderState, HONEYPOT_FIELD, IMPLIED_PATTERN, INITIAL_FORM_STATE, INTERACTIVE_HOST_TAGS, InteractiveTag, MESSAGE_NAMESPACE,
|
|
1686
|
+
export { CALENDAR_ROWS, CHANNEL_KINDS, CHOICE_MESSAGE_KEYS, CalendarKey, ChannelKind, ChoiceAppearance, ControlType, DATE_EMPTY_ATTRIBUTE, DATE_ICON_SVG, DATE_MAX_ATTRIBUTE, DATE_MESSAGE_KEYS, DATE_MIN_ATTRIBUTE, DATE_PICKER_WORDS, DEFAULT_EN_MESSAGES, FIELD_GROUP_NODE_TYPE, FIELD_NODE_TYPE, FORM_ACTIONS_ROLE, FORM_ANNOUNCER_ATTRIBUTE, FORM_CHANNEL_ROLE, FORM_CONTENT_TYPES, FORM_ERROR_KEYS, FORM_FIELDS_ATTRIBUTE, FORM_HONEYPOT_CLASS, FORM_NODE_TYPE, FORM_PRESETS, FORM_STATE_ATTRIBUTE, FeedbackTiming, FieldClass, FormPresetId, FormRenderState, HONEYPOT_FIELD, IMPLIED_PATTERN, INITIAL_FORM_STATE, INTERACTIVE_HOST_TAGS, InteractiveTag, MESSAGE_NAMESPACE, OPTION_CONTROLS, PATTERN_MESSAGE_KEYS, PATTERN_PRESETS, PATTERN_PRESET_OPTIONS, PatternPreset, RENDERED_AT_FIELD, RULE_DESCRIPTORS, RULE_MESSAGE_KEYS, RuleName, RuleParamKind, STORE_ACTION, SUBMIT_BUTTON_NODE_TYPE, SUBMIT_CHECK_SVG, SUBMIT_KEEP_ENABLED_ATTRIBUTE, SUBMIT_MESSAGE_KEYS, SubmitLabelClass, addDaysIso, addMonthsIso, addableRules, applyServerErrors, baseFieldName, calendarKeyTarget, calendarMonth, channelHref, channelOpensNewTab, choiceAppearanceOf, clampIso, clientFormSpec, collectFieldNodes, collectFormSpec, dateBoundRules, dateOfIso, datePickerWords, deriveFieldName, failureAnnouncement, fieldDateRange, fieldHtml, fieldOptionsOf, fieldSpecOf, findFormContentInHosts, firstInvalidField, formChromeHtml, formContentInHost, formFieldNameMap, formFieldNames, formPresetById, formatDateInWords, inputTypeOf, interpretSubmitResponse, isChannelFilled, isDateBoundParams, isDateField, isFormContent, isInteractiveHost, isIsoDate, isMultiValueField, isoOfDate, normaliseAnswers, onFieldBlur, onFieldChange, onSubmitAttempt, parseChannelHref, parseClientFormSpec, parseDateBound, parseFieldOptions, planFormPresetIds, resetFormState, resolveDateBound, rulesFor, sentWord, shouldShowError, submitButtonLabelsHtml, submitButtonView, submitEndpointPath, submitOutcome, todayIso, uniqueFieldName, uniqueFieldNames, validateField, validateForm, weekStartFor, withAntiSpamFields };
|
|
1010
1687
|
//# sourceMappingURL=vectoriox-iox-ui-forms-core.mjs.map
|