@vectoriox/iox-ui 5.80.0 → 5.82.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 +48 -9
- package/fesm2022/vectoriox-iox-ui-design-core.mjs +76 -11
- package/fesm2022/vectoriox-iox-ui-design-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-forms-core.mjs +96 -1
- package/fesm2022/vectoriox-iox-ui-forms-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-generation-core.mjs +110 -0
- package/fesm2022/vectoriox-iox-ui-generation-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui.mjs +847 -76
- package/fesm2022/vectoriox-iox-ui.mjs.map +1 -1
- package/forms-core/index.d.ts +83 -2
- package/index.d.ts +368 -34
- package/package.json +1 -1
- package/src/design-core/element-styles.ts +14 -2
- package/src/design-core/entrance-timing.ts +14 -3
- package/src/design-core/flavor-spec.generated.ts +12 -6
- package/src/design-core/public-api.ts +1 -0
- package/src/design-core/theme-color.ts +41 -0
- package/src/forms-core/public-api.ts +1 -0
- package/src/forms-core/submit-lifecycle.ts +128 -0
- package/src/generation-core/node-baselines.generated.ts +110 -0
- package/styles/iox-design.css +33 -11
- package/styles/iox-forms.css +20 -0
|
@@ -1556,7 +1556,15 @@ const MARKER_FUNS = [
|
|
|
1556
1556
|
['markerColor', 'color'],
|
|
1557
1557
|
['markerFontSize', 'fontSize'],
|
|
1558
1558
|
];
|
|
1559
|
-
const TRANSITION_KEYS = ['transitionDuration', 'transitionTimingFunction', 'transitionDelay'];
|
|
1559
|
+
const TRANSITION_KEYS = ['transitionDuration', 'transitionTimingFunction', 'transitionDelay', 'transitionProperty'];
|
|
1560
|
+
/** The Transition ▸ Properties trait (G2): what a transition names, comma-separated. Unset, the renderers fill in the
|
|
1561
|
+
* properties the node's states change (`withStateTransitions`); a transition never names `all` unless an author
|
|
1562
|
+
* typed it. */
|
|
1563
|
+
const TRANSITION_PROPERTY_KEY = 'transitionProperty';
|
|
1564
|
+
/** The property list a `transitionProperty` value names (`color, transform` → two entries). */
|
|
1565
|
+
function transitionPropertyList(value) {
|
|
1566
|
+
return String(value ?? '').split(',').map(p => p.trim()).filter(Boolean);
|
|
1567
|
+
}
|
|
1560
1568
|
// ─── Max lines (line clamp) ─────────────────────────────────────────────────────
|
|
1561
1569
|
/** "Max lines" (Card v2, board 8 — opt-in on a Title / Text): a whole number clamps the text to that many
|
|
1562
1570
|
* lines with an ellipsis. It composes into the four declarations a line clamp takes; anything else
|
|
@@ -1740,13 +1748,15 @@ function composeVirtualTraits(raw, base = raw) {
|
|
|
1740
1748
|
const transformStr = buildFns(TRANSFORM_FUNS, merged(TRANSFORM_FUNS));
|
|
1741
1749
|
if (transformStr)
|
|
1742
1750
|
result['transform'] = transformStr;
|
|
1743
|
-
// transition — only
|
|
1751
|
+
// transition — only when it is timed (a non-zero duration) and names what it moves: one entry per property
|
|
1752
|
+
// (G2, Emil 183 — never `transition: all`); nothing to name, nothing to transition.
|
|
1744
1753
|
const dur = raw['transitionDuration'] ?? base['transitionDuration'];
|
|
1745
1754
|
const ease = raw['transitionTimingFunction'] ?? base['transitionTimingFunction'];
|
|
1746
1755
|
const del = raw['transitionDelay'] ?? base['transitionDelay'];
|
|
1747
|
-
|
|
1756
|
+
const props = transitionPropertyList(raw[TRANSITION_PROPERTY_KEY] ?? base[TRANSITION_PROPERTY_KEY]);
|
|
1757
|
+
if (dur && ease && dur !== '0ms' && dur !== '0s' && props.length) {
|
|
1748
1758
|
const delPart = del && del !== '0ms' && del !== '0s' ? ` ${del}` : '';
|
|
1749
|
-
result['transition'] =
|
|
1759
|
+
result['transition'] = props.map(p => `${p} ${dur} ${ease}${delPart}`).join(', ');
|
|
1750
1760
|
}
|
|
1751
1761
|
// Max lines: the clamp takes over the node's display and overflow.
|
|
1752
1762
|
const clamp = lineClamp(raw[MAX_LINES_KEY] ?? base[MAX_LINES_KEY]);
|
|
@@ -1858,6 +1868,8 @@ const NOOP_INITIAL_VALUES = {
|
|
|
1858
1868
|
'aspect-ratio': ['auto'],
|
|
1859
1869
|
'mix-blend-mode': ['normal'],
|
|
1860
1870
|
'transform': ['none'], 'box-shadow': ['none'],
|
|
1871
|
+
// G3: the Transform origin trait's default is the CSS initial (the centre) — never emitted on every node.
|
|
1872
|
+
'transform-origin': ['center', 'center center', '50% 50%', '50% 50% 0', '50% 50% 0px'],
|
|
1861
1873
|
// overflow initial is `visible`. A self-describing node serialises the Overflow trait's default,
|
|
1862
1874
|
// so WITHOUT this the emitted `overflow: visible` would override component baselines that clip
|
|
1863
1875
|
// (e.g. `.iox-card__img-wrap`, `.iox-slider`, `.iox-gallery` all set `overflow: hidden`), breaking
|
|
@@ -1978,12 +1990,41 @@ var ComponentState;
|
|
|
1978
1990
|
* `targetedStateSelector`. On a node OUTSIDE every item (composite T20, CC4a) the author PICKS the item — "when tab
|
|
1979
1991
|
* Night is on" — the same state and the same `:has()` scoped to the nearest common ancestor. */
|
|
1980
1992
|
ComponentState["ItemOpen"] = "trigger-on";
|
|
1993
|
+
/** G4: a control that cannot be used — a disabled button, or one marked aria-disabled. */
|
|
1994
|
+
ComponentState["Disabled"] = "disabled";
|
|
1995
|
+
/** G4: busy — a submit button while its form sends (`syncSubmitButtons`), a form while it sends. */
|
|
1996
|
+
ComponentState["Busy"] = "busy";
|
|
1997
|
+
/** G4: chosen — a toggle button pressed, an option checked or selected. */
|
|
1998
|
+
ComponentState["Selected"] = "selected";
|
|
1999
|
+
/** G4: the page has scrolled (a header that firms up once the page moves) — `SCROLLED_ATTRIBUTE` on the root. */
|
|
2000
|
+
ComponentState["Scrolled"] = "scrolled";
|
|
1981
2001
|
})(ComponentState || (ComponentState = {}));
|
|
1982
|
-
/** The
|
|
2002
|
+
/** The page has scrolled: written on the document element by the live site (and on the canvas page by the builder). */
|
|
2003
|
+
const SCROLLED_ATTRIBUTE = 'data-iox-scrolled';
|
|
2004
|
+
/** The selector each (non-targeted) component state compiles to — an attribute, or an `:is()` of the ways a node can
|
|
2005
|
+
* be in it (a native `:disabled` or `aria-disabled`; pressed / checked / selected), so one rule covers every markup. */
|
|
1983
2006
|
const COMPONENT_STATE_SELECTOR = {
|
|
1984
2007
|
[ComponentState.Current]: '[aria-current="page"]',
|
|
1985
2008
|
[ComponentState.On]: '[data-iox-on]',
|
|
2009
|
+
[ComponentState.Disabled]: ':is(:disabled, [aria-disabled="true"])',
|
|
2010
|
+
[ComponentState.Busy]: '[aria-busy="true"]',
|
|
2011
|
+
[ComponentState.Selected]: ':is([aria-pressed="true"], [aria-checked="true"], [aria-selected="true"])',
|
|
2012
|
+
[ComponentState.Scrolled]: `:is([${SCROLLED_ATTRIBUTE}], [${SCROLLED_ATTRIBUTE}] *)`,
|
|
1986
2013
|
};
|
|
2014
|
+
/** Which types offer each control state (G4) in the Style panel's Component state group. */
|
|
2015
|
+
const G4_STATE_TYPES = [
|
|
2016
|
+
[ComponentState.Disabled, ['ButtonBlock', 'Button', 'SubmitButton']],
|
|
2017
|
+
[ComponentState.Busy, ['SubmitButton', 'ButtonBlock', 'Form']],
|
|
2018
|
+
[ComponentState.Selected, ['ButtonBlock', 'LinkedContainer']],
|
|
2019
|
+
];
|
|
2020
|
+
/** Scrolled is offered on the site header and its parts — generation-core's NavRole family (`nav`, `nav.brand`…), the
|
|
2021
|
+
* drawer's parts aside (the drawer is a dialog over the page). Spelled here — the main entry does not import
|
|
2022
|
+
* generation-core; style-compiler.spec pins it to the enum. */
|
|
2023
|
+
const SCROLLED_STATE_ROLE = 'nav';
|
|
2024
|
+
const DRAWER_ROLE_PREFIX = 'nav.drawer';
|
|
2025
|
+
function offersScrolled(role) {
|
|
2026
|
+
return !!role && (role === SCROLLED_STATE_ROLE || role.startsWith(`${SCROLLED_STATE_ROLE}.`)) && !role.startsWith(DRAWER_ROLE_PREFIX);
|
|
2027
|
+
}
|
|
1987
2028
|
/** The composite roles that are themselves "on" when their item is open (they carry `data-iox-on`): an accordion's
|
|
1988
2029
|
* item, header and panel; a disclosure (its own item), its toggle and its panel (CC2.1); a tab and its panel (CC4a —
|
|
1989
2030
|
* "Active"). The main entry does not import composite-core, so the roles are spelled here — `style-compiler.spec` pins
|
|
@@ -2008,6 +2049,11 @@ function componentStatesFor(type, role, insideItem = false, itemsToPick = false)
|
|
|
2008
2049
|
out.push(ComponentState.Current);
|
|
2009
2050
|
if (insideItem || itemsToPick)
|
|
2010
2051
|
out.push(ComponentState.ItemOpen);
|
|
2052
|
+
for (const [state, types] of G4_STATE_TYPES)
|
|
2053
|
+
if (type && types.includes(type))
|
|
2054
|
+
out.push(state);
|
|
2055
|
+
if (offersScrolled(role))
|
|
2056
|
+
out.push(ComponentState.Scrolled);
|
|
2011
2057
|
return out;
|
|
2012
2058
|
}
|
|
2013
2059
|
/**
|
|
@@ -2042,29 +2088,72 @@ function hasHoverState(stateStyles) {
|
|
|
2042
2088
|
* The base-rule transition a hovering node gets when it declares none of its own, so a hover
|
|
2043
2089
|
* animates instead of snapping. ONE copy for the builder canvas, the style panel and the engine.
|
|
2044
2090
|
*
|
|
2045
|
-
* Two
|
|
2091
|
+
* Two timings, chosen by the element-style MARKER (§3b-bis): a node wearing an element style takes
|
|
2046
2092
|
* the motion tokens; a node without one (saved before DS2, not adopted) keeps the pre-DS2 literal
|
|
2047
|
-
*
|
|
2093
|
+
* timing — seeding an org's tokens must never change how a legacy page hovers. WHAT transitions is the
|
|
2094
|
+
* list of properties the node's states change (G2 — never `transition: all`).
|
|
2048
2095
|
*/
|
|
2049
|
-
const
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2096
|
+
const HOVER_DEFAULT_TRANSITION_TRAITS = {
|
|
2097
|
+
transitionDuration: 'var(--iox-dur-fast)', transitionTimingFunction: 'var(--iox-ease-standard)',
|
|
2098
|
+
};
|
|
2099
|
+
const LEGACY_HOVER_TRANSITION_TRAITS = { transitionDuration: '0.2s', transitionTimingFunction: 'ease' };
|
|
2100
|
+
/** The default hover timing for a node that wears `elementStyle` (or none). */
|
|
2101
|
+
function defaultHoverTransitionTraits(elementStyle) {
|
|
2102
|
+
return elementStyle ? HOVER_DEFAULT_TRANSITION_TRAITS : LEGACY_HOVER_TRANSITION_TRAITS;
|
|
2103
|
+
}
|
|
2104
|
+
const isFilled = (v) => v !== undefined && v !== null && String(v).trim() !== '';
|
|
2105
|
+
const isZeroDuration = (v) => /^-?0+(\.0+)?(ms|s)?$/.test(String(v ?? '').trim());
|
|
2106
|
+
/** True when the RAW style map times a transition of its own (an explicit `transition`, or the
|
|
2107
|
+
* transitionDuration / TimingFunction an element style writes). */
|
|
2057
2108
|
function hasOwnTransition(styleProps) {
|
|
2058
2109
|
const sp = styleProps ?? {};
|
|
2059
|
-
if (sp['transition']
|
|
2110
|
+
if (isFilled(sp['transition']))
|
|
2060
2111
|
return true;
|
|
2061
|
-
return
|
|
2112
|
+
return isFilled(sp['transitionDuration']) && !isZeroDuration(sp['transitionDuration']) && isFilled(sp['transitionTimingFunction']);
|
|
2062
2113
|
}
|
|
2063
|
-
/**
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2114
|
+
/** Keys a composed state map holds that are not properties a transition can name. */
|
|
2115
|
+
const NOT_TRANSITIONED = new Set(['__markerStyles', 'WebkitBackdropFilter', 'transition', 'transitionDuration', 'transitionTimingFunction', 'transitionDelay', 'transitionProperty']);
|
|
2116
|
+
/**
|
|
2117
|
+
* The CSS properties a node's states change — what its transition names (G2, Emil 183: never `transition: all`).
|
|
2118
|
+
* Every state the node holds, at every breakpoint, composed as the renderers compose it (translateX / scaleX → one
|
|
2119
|
+
* `transform`, a gradient background → `background-image`…), in a stable order so both renderers emit one string.
|
|
2120
|
+
*/
|
|
2121
|
+
function stateTransitionProperties(stateStyles, responsive) {
|
|
2122
|
+
const maps = [];
|
|
2123
|
+
for (const map of Object.values(stateStyles ?? {}))
|
|
2124
|
+
if (map)
|
|
2125
|
+
maps.push(map);
|
|
2126
|
+
for (const layer of Object.values(responsive ?? {}))
|
|
2127
|
+
for (const map of Object.values(layer?.stateStyles ?? {}))
|
|
2128
|
+
if (map)
|
|
2129
|
+
maps.push(map);
|
|
2130
|
+
const out = [];
|
|
2131
|
+
for (const map of maps) {
|
|
2132
|
+
for (const key of Object.keys(composeVirtualTraits(map, map))) {
|
|
2133
|
+
if (NOT_TRANSITIONED.has(key))
|
|
2134
|
+
continue;
|
|
2135
|
+
const prop = styleKeyToKebab(key);
|
|
2136
|
+
if (!out.includes(prop))
|
|
2137
|
+
out.push(prop);
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
return out;
|
|
2141
|
+
}
|
|
2142
|
+
/**
|
|
2143
|
+
* `styleProps` as a renderer compiles them: a hovering node with no timing of its own takes the default hover timing,
|
|
2144
|
+
* and a node whose transition names no properties gets the ones its states change. An author's own `transitionProperty`
|
|
2145
|
+
* (or a raw `transition`) always wins. Pure: the node's stored styles are never touched.
|
|
2146
|
+
*/
|
|
2147
|
+
function withStateTransitions(styleProps, stateStyles, elementStyle, responsive) {
|
|
2148
|
+
let out = styleProps;
|
|
2149
|
+
if (hasHoverState(stateStyles) && !hasOwnTransition(styleProps))
|
|
2150
|
+
out = { ...out, ...defaultHoverTransitionTraits(elementStyle) };
|
|
2151
|
+
if (!isFilled(out['transitionProperty']) && !isFilled(out['transition'])) {
|
|
2152
|
+
const props = stateTransitionProperties(stateStyles, responsive);
|
|
2153
|
+
if (props.length)
|
|
2154
|
+
out = { ...out, transitionProperty: props.join(', ') };
|
|
2155
|
+
}
|
|
2156
|
+
return out;
|
|
2068
2157
|
}
|
|
2069
2158
|
/**
|
|
2070
2159
|
* Per-type default `width` that the builder applies from each component's config
|
|
@@ -2088,12 +2177,13 @@ const DEFAULT_WIDTH_BY_TYPE = {
|
|
|
2088
2177
|
* behaviour, which the engine must re-apply explicitly for parity. Returns `"prop: value"`
|
|
2089
2178
|
* declarations to append AFTER the node's own compiled declarations.
|
|
2090
2179
|
*
|
|
2180
|
+
* The default hover transition is no longer one of them: it is composed into the node's own rule by
|
|
2181
|
+
* `withStateTransitions` (both renderers), listing the properties its states change (G2).
|
|
2182
|
+
*
|
|
2091
2183
|
* @param type Node type (registry key).
|
|
2092
2184
|
* @param styleProps The node's persisted styleProps.
|
|
2093
|
-
* @param hasHoverState Whether the node has a non-empty hover / parent-hover state.
|
|
2094
|
-
* @param elementStyle The element style the node wears — picks the default hover transition.
|
|
2095
2185
|
*/
|
|
2096
|
-
function renderDefaultDeclarations(type, styleProps
|
|
2186
|
+
function renderDefaultDeclarations(type, styleProps) {
|
|
2097
2187
|
const sp = styleProps ?? {};
|
|
2098
2188
|
const out = [];
|
|
2099
2189
|
const isOutOfFlow = sp['position'] === 'fixed' || sp['position'] === 'absolute';
|
|
@@ -2121,12 +2211,6 @@ function renderDefaultDeclarations(type, styleProps, hasHoverState, elementStyle
|
|
|
2121
2211
|
if (sp['borderWidth'] && sp['borderStyle'] === undefined) {
|
|
2122
2212
|
out.push('border-style: solid');
|
|
2123
2213
|
}
|
|
2124
|
-
// Default transition so own/parent hover animates smoothly (parity with the builder). Skipped
|
|
2125
|
-
// when the node composes its own (an element style's transitionDuration) — appended after the
|
|
2126
|
-
// node's declarations, it would otherwise beat them in the engine only.
|
|
2127
|
-
if (hasHoverState && !hasOwnTransition(sp)) {
|
|
2128
|
-
out.push(`transition: ${defaultHoverTransition(elementStyle)}`);
|
|
2129
|
-
}
|
|
2130
2214
|
// Image defaults reapplied when unset (the config defaults aren't always persisted). height:auto
|
|
2131
2215
|
// is only a DEFAULT — when the user sets an explicit height (e.g. 100%) it must WIN, so guard it
|
|
2132
2216
|
// on `sp['height'] === undefined` (matching the builder, which respects a configured height). An
|
|
@@ -4387,6 +4471,101 @@ function normaliseErrors(raw) {
|
|
|
4387
4471
|
return Object.keys(errors).length ? errors : undefined;
|
|
4388
4472
|
}
|
|
4389
4473
|
|
|
4474
|
+
/**
|
|
4475
|
+
* The submit LIFECYCLE's decisions — what a form shows after the server answers, what it says when a
|
|
4476
|
+
* submit fails, what a client needs to validate a form, and how its submit button follows its state.
|
|
4477
|
+
*
|
|
4478
|
+
* Two runtimes act on these and must agree: `IoxFormComponent` (the engine's component path) and the
|
|
4479
|
+
* string-form runtime (every header / footer global and every string-rendered page —
|
|
4480
|
+
* vectoriox/iox-client-engine#13). The builder canvas reads `submitButtonView` too, for its Sending state.
|
|
4481
|
+
*
|
|
4482
|
+
* ⚠️ Framework-free, like the rest of forms-core: no Angular, no DOM.
|
|
4483
|
+
*/
|
|
4484
|
+
/** Which of the form's four states is showing — written on the form as `data-iox-state`. */
|
|
4485
|
+
var FormRenderState;
|
|
4486
|
+
(function (FormRenderState) {
|
|
4487
|
+
FormRenderState["Idle"] = "idle";
|
|
4488
|
+
FormRenderState["Submitting"] = "submitting";
|
|
4489
|
+
FormRenderState["Success"] = "success";
|
|
4490
|
+
FormRenderState["Error"] = "error";
|
|
4491
|
+
})(FormRenderState || (FormRenderState = {}));
|
|
4492
|
+
/** The attribute every renderer writes a form's state on (the canvas switcher, both live paths). */
|
|
4493
|
+
const FORM_STATE_ATTRIBUTE = 'data-iox-state';
|
|
4494
|
+
/**
|
|
4495
|
+
* The server's answer → what the visitor sees. Only a confirmed success is Success; per-field verdicts
|
|
4496
|
+
* land on the fields; anything else (a dropped network included) is the designed Error block.
|
|
4497
|
+
*/
|
|
4498
|
+
function submitOutcome(response) {
|
|
4499
|
+
if (response?.ok)
|
|
4500
|
+
return { state: FormRenderState.Success, reset: true, ...(response.redirect ? { redirect: response.redirect } : {}) };
|
|
4501
|
+
if (response?.errors && Object.keys(response.errors).length)
|
|
4502
|
+
return { state: FormRenderState.Idle, errors: response.errors, reset: false };
|
|
4503
|
+
return { state: FormRenderState.Error, reset: false };
|
|
4504
|
+
}
|
|
4505
|
+
/** What the live region says after a refused submit: how many fields need attention, or that it did not send. */
|
|
4506
|
+
function failureAnnouncement(errors) {
|
|
4507
|
+
const count = errors ? Object.keys(errors).length : 0;
|
|
4508
|
+
if (!count)
|
|
4509
|
+
return 'That did not send.';
|
|
4510
|
+
return `${count} ${count === 1 ? 'field needs' : 'fields need'} attention.`;
|
|
4511
|
+
}
|
|
4512
|
+
const FORM_FIELDS_ATTRIBUTE = 'data-iox-form-fields';
|
|
4513
|
+
function clientFormSpec(spec) {
|
|
4514
|
+
return { feedback: spec.feedback ?? FeedbackTiming.Smart, fields: [...(spec.fields ?? [])] };
|
|
4515
|
+
}
|
|
4516
|
+
/** The attribute's value back into a spec, or null when it is missing or not one. */
|
|
4517
|
+
function parseClientFormSpec(raw) {
|
|
4518
|
+
if (!raw)
|
|
4519
|
+
return null;
|
|
4520
|
+
try {
|
|
4521
|
+
const parsed = JSON.parse(raw);
|
|
4522
|
+
if (!parsed || !Array.isArray(parsed.fields))
|
|
4523
|
+
return null;
|
|
4524
|
+
return { feedback: typeof parsed.feedback === 'string' ? parsed.feedback : FeedbackTiming.Smart, fields: parsed.fields };
|
|
4525
|
+
}
|
|
4526
|
+
catch {
|
|
4527
|
+
return null;
|
|
4528
|
+
}
|
|
4529
|
+
}
|
|
4530
|
+
/** While its form sends, a submit button is busy, and disabled unless its author turned that off — ONE rule for the
|
|
4531
|
+
* canvas's Sending state, the component path and the string path. Its "Sending…" label is the shared CSS's, keyed on
|
|
4532
|
+
* the same `data-iox-state`. */
|
|
4533
|
+
function submitButtonView(state, disableWhileSubmitting = true) {
|
|
4534
|
+
const busy = state === FormRenderState.Submitting;
|
|
4535
|
+
return { busy, disabled: busy && disableWhileSubmitting !== false };
|
|
4536
|
+
}
|
|
4537
|
+
/** On a submit button whose author turned "Disable while sending" off. */
|
|
4538
|
+
const SUBMIT_KEEP_ENABLED_ATTRIBUTE = 'data-iox-keep-enabled';
|
|
4539
|
+
var SubmitLabelClass;
|
|
4540
|
+
(function (SubmitLabelClass) {
|
|
4541
|
+
SubmitLabelClass["Labels"] = "iox-submit__labels";
|
|
4542
|
+
SubmitLabelClass["Idle"] = "iox-submit__idle";
|
|
4543
|
+
SubmitLabelClass["Busy"] = "iox-submit__busy";
|
|
4544
|
+
})(SubmitLabelClass || (SubmitLabelClass = {}));
|
|
4545
|
+
/**
|
|
4546
|
+
* A submit button's words, as markup: its label, or — when it has a "While sending" label — both, stacked in one
|
|
4547
|
+
* cell (the button keeps the longer one's width and never jumps). The string renderer's copy of
|
|
4548
|
+
* `IoxSubmitButtonComponent`'s template; `escape` is the renderer's.
|
|
4549
|
+
*/
|
|
4550
|
+
function submitButtonLabelsHtml(label, loadingLabel, escape) {
|
|
4551
|
+
if (!loadingLabel)
|
|
4552
|
+
return escape(label);
|
|
4553
|
+
return `<span class="${SubmitLabelClass.Labels}"><span class="${SubmitLabelClass.Idle}">${escape(label)}</span>` +
|
|
4554
|
+
`<span class="${SubmitLabelClass.Busy}">${escape(loadingLabel)}</span></span>`;
|
|
4555
|
+
}
|
|
4556
|
+
const FORM_HONEYPOT_CLASS = 'iox-form__hp';
|
|
4557
|
+
const FORM_ANNOUNCER_ATTRIBUTE = 'data-iox-form-announcer';
|
|
4558
|
+
/**
|
|
4559
|
+
* The honeypot and the failure announcer every form holds, as markup — what `IoxFormComponent`'s template
|
|
4560
|
+
* renders, for the string renderer. The honeypot is off-screen (some bots skip hidden inputs), out of the
|
|
4561
|
+
* Tab order and the accessibility tree; the announcer is present from the start (a live region added at
|
|
4562
|
+
* the moment it gains text is not reliably announced).
|
|
4563
|
+
*/
|
|
4564
|
+
function formChromeHtml(honeypotField) {
|
|
4565
|
+
return `<div class="${FORM_HONEYPOT_CLASS}" aria-hidden="true"><input type="text" name="${honeypotField}" tabindex="-1" autocomplete="off"></div>` +
|
|
4566
|
+
`<div class="${FORM_HONEYPOT_CLASS}" ${FORM_ANNOUNCER_ATTRIBUTE} role="status" aria-live="assertive" aria-atomic="true"></div>`;
|
|
4567
|
+
}
|
|
4568
|
+
|
|
4390
4569
|
/**
|
|
4391
4570
|
* FORM PRESETS — a complete, working form as one insert: its fields, its submit label and the words of
|
|
4392
4571
|
* its Success and Error blocks. ONE definition (W1.1): the builder's Forms tray builds a preset from
|
|
@@ -4663,6 +4842,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
|
|
|
4663
4842
|
type: Injectable
|
|
4664
4843
|
}] });
|
|
4665
4844
|
|
|
4845
|
+
const SUBMIT_BUTTON_SELECTOR = '.iox-submit';
|
|
4846
|
+
const FORM_SELECTOR = 'form, .iox-form';
|
|
4847
|
+
/**
|
|
4848
|
+
* Put every submit button of `form` in the form's state (vectoriox/iox-ui#42): busy (`aria-busy`) and — unless its
|
|
4849
|
+
* author turned it off — disabled while the form sends; itself again otherwise. The ONE applier: the component path's
|
|
4850
|
+
* form, the string runtime and the canvas's Sending state all call it, from the shared `submitButtonView`. A button
|
|
4851
|
+
* inside a nested form belongs to that form, not this one.
|
|
4852
|
+
*/
|
|
4853
|
+
function syncSubmitButtons(form, state) {
|
|
4854
|
+
if (!form?.querySelectorAll)
|
|
4855
|
+
return;
|
|
4856
|
+
for (const button of Array.from(form.querySelectorAll(SUBMIT_BUTTON_SELECTOR))) {
|
|
4857
|
+
if (button.closest?.(FORM_SELECTOR) !== form)
|
|
4858
|
+
continue;
|
|
4859
|
+
const view = submitButtonView(state, !button.hasAttribute(SUBMIT_KEEP_ENABLED_ATTRIBUTE));
|
|
4860
|
+
if (view.busy)
|
|
4861
|
+
button.setAttribute('aria-busy', 'true');
|
|
4862
|
+
else
|
|
4863
|
+
button.removeAttribute('aria-busy');
|
|
4864
|
+
if (view.disabled)
|
|
4865
|
+
button.setAttribute('disabled', '');
|
|
4866
|
+
else
|
|
4867
|
+
button.removeAttribute('disabled');
|
|
4868
|
+
}
|
|
4869
|
+
}
|
|
4870
|
+
|
|
4666
4871
|
/**
|
|
4667
4872
|
* How a host makes the form actually send. The SSR engine provides one (an HttpClient POST); the
|
|
4668
4873
|
* builder canvas deliberately does NOT — an author pressing Submit while designing must never file
|
|
@@ -4672,14 +4877,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
|
|
|
4672
4877
|
const IOX_FORM_TRANSPORT = new InjectionToken('IOX_FORM_TRANSPORT');
|
|
4673
4878
|
/** Where a successful submit sends the visitor. Provided by the host; absent on the canvas. */
|
|
4674
4879
|
const IOX_FORM_NAVIGATOR = new InjectionToken('IOX_FORM_NAVIGATOR');
|
|
4675
|
-
/** Which of the form's four states the canvas / runtime is showing. */
|
|
4676
|
-
var FormRenderState;
|
|
4677
|
-
(function (FormRenderState) {
|
|
4678
|
-
FormRenderState["Idle"] = "idle";
|
|
4679
|
-
FormRenderState["Submitting"] = "submitting";
|
|
4680
|
-
FormRenderState["Success"] = "success";
|
|
4681
|
-
FormRenderState["Error"] = "error";
|
|
4682
|
-
})(FormRenderState || (FormRenderState = {}));
|
|
4683
4880
|
/**
|
|
4684
4881
|
* Directive-host Form: the real `<form>` IS the host — no wrapper, no inner/outer split. Children
|
|
4685
4882
|
* (fields, layout, the submit button, and the two state blocks) render into it via `childrenHost`.
|
|
@@ -4697,6 +4894,13 @@ var FormRenderState;
|
|
|
4697
4894
|
* which are styleable, translated and announced. Validation still runs — ours does it.
|
|
4698
4895
|
*/
|
|
4699
4896
|
class IoxFormComponent {
|
|
4897
|
+
/** Which state shows. Every change puts the submit buttons in it too (busy / disabled — iox-ui#42), through the
|
|
4898
|
+
* ONE applier the string runtime and the canvas call. */
|
|
4899
|
+
set renderState(state) {
|
|
4900
|
+
this._renderState = state;
|
|
4901
|
+
syncSubmitButtons(this.host?.nativeElement, state);
|
|
4902
|
+
}
|
|
4903
|
+
get renderState() { return this._renderState; }
|
|
4700
4904
|
/**
|
|
4701
4905
|
* Constructor injection rather than `inject()`, for the same reason as Field: this library's
|
|
4702
4906
|
* specs construct components directly. `runtime` is provided by THIS component (see
|
|
@@ -4712,7 +4916,7 @@ class IoxFormComponent {
|
|
|
4712
4916
|
this.formId = '';
|
|
4713
4917
|
/** Author-facing name — what the submissions inbox groups by. */
|
|
4714
4918
|
this.name = '';
|
|
4715
|
-
this.
|
|
4919
|
+
this._renderState = FormRenderState.Idle;
|
|
4716
4920
|
this.feedback = FeedbackTiming.Smart;
|
|
4717
4921
|
/**
|
|
4718
4922
|
* Move focus to the first field that failed, instead of leaving the visitor to hunt for red
|
|
@@ -4798,30 +5002,28 @@ class IoxFormComponent {
|
|
|
4798
5002
|
this.runtime.setSubmitting(false);
|
|
4799
5003
|
this.applyResponse(response);
|
|
4800
5004
|
}
|
|
4801
|
-
/** Turn the server's answer into what the visitor sees
|
|
5005
|
+
/** Turn the server's answer into what the visitor sees — the shared `submitOutcome`, which the string runtime
|
|
5006
|
+
* applies too. Public so a host-owned transport can reuse it. */
|
|
4802
5007
|
applyResponse(response) {
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
this.
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
}
|
|
4814
|
-
this.renderState = FormRenderState.Error;
|
|
5008
|
+
const outcome = submitOutcome(response);
|
|
5009
|
+
this.renderState = outcome.state;
|
|
5010
|
+
if (outcome.errors) {
|
|
5011
|
+
this.runtime.applyErrors(outcome.errors);
|
|
5012
|
+
this.announce(outcome.errors);
|
|
5013
|
+
if (this.focusFirstProblem)
|
|
5014
|
+
this.focusFirstInvalid();
|
|
5015
|
+
return;
|
|
5016
|
+
}
|
|
5017
|
+
if (outcome.state === FormRenderState.Error) {
|
|
4815
5018
|
this.announce(null);
|
|
4816
5019
|
return;
|
|
4817
5020
|
}
|
|
4818
|
-
this.renderState = FormRenderState.Success;
|
|
4819
5021
|
// Clear the values so a visitor who stays on the page does not see their old answers behind
|
|
4820
5022
|
// the success message, and cannot resubmit them by pressing enter.
|
|
4821
5023
|
this.runtime.reset();
|
|
4822
5024
|
this.resetControls();
|
|
4823
|
-
if (
|
|
4824
|
-
this.navigate(
|
|
5025
|
+
if (outcome.redirect && this.navigate)
|
|
5026
|
+
this.navigate(outcome.redirect);
|
|
4825
5027
|
}
|
|
4826
5028
|
/** The honeypot input's value, if the host rendered one. Absent reads as empty — never as spam. */
|
|
4827
5029
|
honeypotValue() {
|
|
@@ -4843,10 +5045,7 @@ class IoxFormComponent {
|
|
|
4843
5045
|
announce(errors) {
|
|
4844
5046
|
if (!this.announceFailure)
|
|
4845
5047
|
return;
|
|
4846
|
-
const
|
|
4847
|
-
const next = count
|
|
4848
|
-
? `${count} ${count === 1 ? 'field needs' : 'fields need'} attention.`
|
|
4849
|
-
: 'That did not send.';
|
|
5048
|
+
const next = failureAnnouncement(errors);
|
|
4850
5049
|
this.announcement = '';
|
|
4851
5050
|
// A microtask is enough for the empty value to be committed before the new one lands.
|
|
4852
5051
|
Promise.resolve().then(() => { this.announcement = next; });
|
|
@@ -4880,7 +5079,7 @@ class IoxFormComponent {
|
|
|
4880
5079
|
aria-live="assertive"
|
|
4881
5080
|
aria-atomic="true">{{ announcement }}</div>
|
|
4882
5081
|
<ng-container #childrenHost></ng-container>
|
|
4883
|
-
`, isInline: true, styles: [":host{box-sizing:border-box}
|
|
5082
|
+
`, isInline: true, styles: [":host{box-sizing:border-box}\n"], dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] }); }
|
|
4884
5083
|
}
|
|
4885
5084
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: IoxFormComponent, decorators: [{
|
|
4886
5085
|
type: Component,
|
|
@@ -4904,7 +5103,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
|
|
|
4904
5103
|
'[attr.data-iox-state]': 'renderState',
|
|
4905
5104
|
'[attr.aria-busy]': "renderState === 'submitting' ? 'true' : null",
|
|
4906
5105
|
'(submit)': 'onSubmit($event)',
|
|
4907
|
-
}, providers: [FormRuntimeService], standalone: false, styles: [":host{box-sizing:border-box}
|
|
5106
|
+
}, providers: [FormRuntimeService], standalone: false, styles: [":host{box-sizing:border-box}\n"] }]
|
|
4908
5107
|
}], ctorParameters: () => [{ type: FormRuntimeService }, { type: i0.ElementRef }, { type: undefined, decorators: [{
|
|
4909
5108
|
type: Optional
|
|
4910
5109
|
}, {
|
|
@@ -5398,10 +5597,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
|
|
|
5398
5597
|
*
|
|
5399
5598
|
* It carries no destination configuration of its own — "what happens with the data" is an ordered
|
|
5400
5599
|
* action list on the FORM (decision D9), so there is nothing here to wire and nothing to get out of
|
|
5401
|
-
* sync. The button owns only its own appearance
|
|
5600
|
+
* sync. The button owns only its own appearance.
|
|
5402
5601
|
*
|
|
5403
|
-
*
|
|
5404
|
-
*
|
|
5602
|
+
* It follows its FORM's state (vectoriox/iox-ui#42): its two labels are both in the markup and the shared
|
|
5603
|
+
* iox-forms.css shows "While sending" while the form's `data-iox-state` is submitting — on the canvas's Sending
|
|
5604
|
+
* state, the component path and the string path alike; busy and disabled are written by the ONE applier
|
|
5605
|
+
* (`syncSubmitButtons`) the form's runtimes call. Before, the button switched only on a `submitting` input
|
|
5606
|
+
* nothing ever wrote. The string renderer writes the same markup (forms-core `submitButtonLabelsHtml`).
|
|
5405
5607
|
*/
|
|
5406
5608
|
class IoxSubmitButtonComponent {
|
|
5407
5609
|
constructor() {
|
|
@@ -5409,19 +5611,17 @@ class IoxSubmitButtonComponent {
|
|
|
5409
5611
|
this.label = 'Submit';
|
|
5410
5612
|
this.loadingLabel = 'Sending…';
|
|
5411
5613
|
this.disableWhileSubmitting = true;
|
|
5412
|
-
/** Driven by the form's lifecycle, not by the author. */
|
|
5413
|
-
this.submitting = false;
|
|
5414
5614
|
}
|
|
5415
5615
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: IoxSubmitButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
5416
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "
|
|
5616
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.31", type: IoxSubmitButtonComponent, isStandalone: false, selector: "button[iox-submit]", inputs: { nodeId: "nodeId", label: "label", loadingLabel: "loadingLabel", disableWhileSubmitting: "disableWhileSubmitting" }, host: { properties: { "class": "'iox-submit iox-node-' + nodeId", "attr.type": "'submit'", "attr.data-iox-keep-enabled": "disableWhileSubmitting === false ? \"\" : null" } }, ngImport: i0, template: `@if (loadingLabel) {<span class="iox-submit__labels"><span class="iox-submit__idle">{{ label }}</span><span class="iox-submit__busy">{{ loadingLabel }}</span></span>} @else {<ng-container>{{ label }}</ng-container>}`, isInline: true, styles: [":host{box-sizing:border-box;cursor:pointer}\n"] }); }
|
|
5417
5617
|
}
|
|
5418
5618
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: IoxSubmitButtonComponent, decorators: [{
|
|
5419
5619
|
type: Component,
|
|
5420
|
-
args: [{ selector: 'button[iox-submit]', template:
|
|
5620
|
+
args: [{ selector: 'button[iox-submit]', template: `@if (loadingLabel) {<span class="iox-submit__labels"><span class="iox-submit__idle">{{ label }}</span><span class="iox-submit__busy">{{ loadingLabel }}</span></span>} @else {<ng-container>{{ label }}</ng-container>}`, host: {
|
|
5421
5621
|
'[class]': "'iox-submit iox-node-' + nodeId",
|
|
5422
5622
|
'[attr.type]': "'submit'",
|
|
5423
|
-
|
|
5424
|
-
'[attr.
|
|
5623
|
+
// forms-core SUBMIT_KEEP_ENABLED_ATTRIBUTE — read by `syncSubmitButtons`.
|
|
5624
|
+
'[attr.data-iox-keep-enabled]': 'disableWhileSubmitting === false ? "" : null',
|
|
5425
5625
|
}, standalone: false, styles: [":host{box-sizing:border-box;cursor:pointer}\n"] }]
|
|
5426
5626
|
}], propDecorators: { nodeId: [{
|
|
5427
5627
|
type: Input
|
|
@@ -5431,8 +5631,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
|
|
|
5431
5631
|
type: Input
|
|
5432
5632
|
}], disableWhileSubmitting: [{
|
|
5433
5633
|
type: Input
|
|
5434
|
-
}], submitting: [{
|
|
5435
|
-
type: Input
|
|
5436
5634
|
}] } });
|
|
5437
5635
|
|
|
5438
5636
|
/**
|
|
@@ -6029,6 +6227,98 @@ function partClassName(type, partKey) {
|
|
|
6029
6227
|
return PART_CLASS_NAMES[type]?.[partKey];
|
|
6030
6228
|
}
|
|
6031
6229
|
|
|
6230
|
+
/**
|
|
6231
|
+
* WHERE a state rule lands — the ONE selector planner both renderers emit state and part rules through
|
|
6232
|
+
* (vectoriox/iox-client-engine#14). Before it the builder and the engine each kept a copy: the builder matched a
|
|
6233
|
+
* structural state (first / last / odd / even child) on the OUTER element, the engine on `.iox-node` — on the
|
|
6234
|
+
* engine's component path the INNER element, the only child of its host, so "last child" matched every item; and
|
|
6235
|
+
* the engine dropped every part state but Wrong / Accepted while the builder applied a part's hover at all times.
|
|
6236
|
+
*
|
|
6237
|
+
* Returns full selectors (with their dots). Framework-free.
|
|
6238
|
+
*/
|
|
6239
|
+
/**
|
|
6240
|
+
* Properties that govern how a node sits in its PARENT's layout (flex / grid item, block child) — they live on the
|
|
6241
|
+
* OUTER element: the component's host, or the one element the string renderer and the canvas's collapsed hosts write.
|
|
6242
|
+
* Everything else is "inner". One list for both renderers (it was typed twice).
|
|
6243
|
+
*/
|
|
6244
|
+
const OUTER_STYLE_PROPS = new Set([
|
|
6245
|
+
'margin', 'marginTop', 'marginRight', 'marginBottom', 'marginLeft',
|
|
6246
|
+
'alignSelf', 'flexGrow', 'flexShrink', 'flexBasis', 'order',
|
|
6247
|
+
'gridColumn', 'gridRow', 'justifySelf',
|
|
6248
|
+
'width',
|
|
6249
|
+
]);
|
|
6250
|
+
/** Structural pseudo-classes — matched on the element that IS the sibling among siblings (the outer). */
|
|
6251
|
+
const STRUCTURAL_STATES = ['first-child', 'last-child', 'nth-child(odd)', 'nth-child(even)'];
|
|
6252
|
+
/** States whose pseudo-class lives on the PARENT (`parent-hover` → the parent's `:hover`). */
|
|
6253
|
+
const RELATIONAL_STATE_PSEUDO = { 'parent-hover': 'hover' };
|
|
6254
|
+
function isStructuralState(state) {
|
|
6255
|
+
return STRUCTURAL_STATES.includes(state);
|
|
6256
|
+
}
|
|
6257
|
+
/**
|
|
6258
|
+
* The selectors of `cssId`'s rule for `state`, or null when the state cannot be scoped yet (a relational state with
|
|
6259
|
+
* no parent, a targeted one with no scope — never an unscoped, page-global rule).
|
|
6260
|
+
*
|
|
6261
|
+
* A structural state is matched on the outer element, and its styles reach the node's own element whether that is
|
|
6262
|
+
* the same element (the string path, a one-element node, the canvas's collapsed hosts) or inside it (a component's
|
|
6263
|
+
* host, a Repeater's row wrapper); its outer props stay on the outer element. Every other state is the node's own.
|
|
6264
|
+
*/
|
|
6265
|
+
function planStateRule(cssId, state, scope = {}) {
|
|
6266
|
+
if (TARGETED_STATE_SUFFIX[state]) {
|
|
6267
|
+
const body = scope.scopeId && scope.triggerId ? targetedStateSelector(state, scope.scopeId, scope.triggerId, cssId) : null;
|
|
6268
|
+
return body ? { inner: `.${body}` } : null;
|
|
6269
|
+
}
|
|
6270
|
+
const relational = RELATIONAL_STATE_PSEUDO[state];
|
|
6271
|
+
if (relational)
|
|
6272
|
+
return scope.parentId ? { inner: `.iox-node-${scope.parentId}:${relational} .iox-node-${cssId}` } : null;
|
|
6273
|
+
const validationClass = VALIDATION_STATE_CLASS[state];
|
|
6274
|
+
if (validationClass)
|
|
6275
|
+
return { inner: `.${buildPartSelector(cssId, undefined, validationClass)}` };
|
|
6276
|
+
if (isStructuralState(state)) {
|
|
6277
|
+
const outer = `.iox-outer-${cssId}:${state}`;
|
|
6278
|
+
return { inner: `${outer}.iox-node-${cssId}, ${outer} .iox-node-${cssId}`, outer };
|
|
6279
|
+
}
|
|
6280
|
+
return { inner: `.iox-node-${cssId}${stateSelectorSuffix(state)}` };
|
|
6281
|
+
}
|
|
6282
|
+
/**
|
|
6283
|
+
* The selector of a PART's rule (a Field's label, control, help, error — `PART_CLASS_NAMES`), optionally in a state:
|
|
6284
|
+
* Wrong / Accepted and a component state on the NODE (that is where the class / attribute is), Hover / Pressed /
|
|
6285
|
+
* Focus on the PART itself. Null for a state a part does not take (structural, relational, targeted).
|
|
6286
|
+
*/
|
|
6287
|
+
function planPartRule(cssId, partClass, state) {
|
|
6288
|
+
if (!state)
|
|
6289
|
+
return `.${buildPartSelector(cssId, partClass)}`;
|
|
6290
|
+
const validationClass = VALIDATION_STATE_CLASS[state];
|
|
6291
|
+
if (validationClass)
|
|
6292
|
+
return `.${buildPartSelector(cssId, partClass, validationClass)}`;
|
|
6293
|
+
if (isStructuralState(state) || RELATIONAL_STATE_PSEUDO[state] || TARGETED_STATE_SUFFIX[state])
|
|
6294
|
+
return null;
|
|
6295
|
+
const suffix = stateSelectorSuffix(state);
|
|
6296
|
+
// An attribute state (`current`, `on`) is the node's; a pseudo-class is the part's own.
|
|
6297
|
+
return suffix.startsWith('[') ? `.iox-node-${cssId}${suffix} .${partClass}` : `.iox-node-${cssId} .${partClass}${suffix}`;
|
|
6298
|
+
}
|
|
6299
|
+
/** Split a style map into the node's own props and its OUTER_STYLE_PROPS. */
|
|
6300
|
+
function splitOuterProps(styles) {
|
|
6301
|
+
const inner = {};
|
|
6302
|
+
const outer = {};
|
|
6303
|
+
for (const [k, v] of Object.entries(styles))
|
|
6304
|
+
(OUTER_STYLE_PROPS.has(k) ? outer : inner)[k] = v;
|
|
6305
|
+
return { inner, outer };
|
|
6306
|
+
}
|
|
6307
|
+
/**
|
|
6308
|
+
* The devices a hover rule is for (G1 — Emil 91, mobile-native M1): a phone has no hover, and a tap fires a false
|
|
6309
|
+
* one that STICKS until the next tap elsewhere. Every hover, parent-hover and trigger-hover rule — the node's own and
|
|
6310
|
+
* a part's — is emitted inside this query by both renderers; `:active` (a real press on touch) never is.
|
|
6311
|
+
*/
|
|
6312
|
+
const HOVER_MEDIA_QUERY = '(hover: hover) and (pointer: fine)';
|
|
6313
|
+
/** True for a state driven by a pointer hovering something (its own, its parent's, a chosen trigger's). */
|
|
6314
|
+
function isHoverState(state) {
|
|
6315
|
+
return !!state && HOVER_STATES.includes(state);
|
|
6316
|
+
}
|
|
6317
|
+
/** `css` as it is emitted for `state`: inside `HOVER_MEDIA_QUERY` for a hover state, unchanged otherwise. */
|
|
6318
|
+
function gateHoverRule(state, css) {
|
|
6319
|
+
return css && isHoverState(state) ? `@media ${HOVER_MEDIA_QUERY} {\n${css}\n}` : css;
|
|
6320
|
+
}
|
|
6321
|
+
|
|
6032
6322
|
/**
|
|
6033
6323
|
* "This link is the page you're on" (CC1, N2). ONE decision for the builder canvas and both engine
|
|
6034
6324
|
* paths: a link whose target is the current page renders `aria-current="page"` — in the raw SSR too —
|
|
@@ -6284,6 +6574,96 @@ function clipFrames(params, inlineSign = 1) {
|
|
|
6284
6574
|
return [{ opacity: 1, clipPath: revealClipPath(0, from, inlineSign) }, { opacity: 1, clipPath: revealClipPath(SHOWN_PERCENT, from, inlineSign) }];
|
|
6285
6575
|
}
|
|
6286
6576
|
const INLINE_FROMS = new Set([SceneRevealFrom.Start, SceneRevealFrom.End]);
|
|
6577
|
+
/** The id every interaction run's Animation carries — how a re-trigger finds the runs it interrupts. */
|
|
6578
|
+
const INTERACTION_ANIMATION_ID = 'iox-interaction';
|
|
6579
|
+
/**
|
|
6580
|
+
* What a run that has ended leaves on its element (vectoriox/iox-client-engine#12, iox-ui#41) — ONE rule for the
|
|
6581
|
+
* canvas and the live site.
|
|
6582
|
+
*
|
|
6583
|
+
* `Release`: the last frame IS the element's natural state (an entrance, an exit played back, a one-shot like a
|
|
6584
|
+
* rotate). The run is cancelled and no frame is written inline, so the stylesheet applies again — the element
|
|
6585
|
+
* style's `:hover` / `:active` transforms included. Before, the last frame (`transform: translateY(0)`) was locked
|
|
6586
|
+
* inline on the site and kept by `fill: both` on the canvas, and an element that had entered never pressed again.
|
|
6587
|
+
*
|
|
6588
|
+
* `Hold`: the last frame HIDES the element (an exit, an entrance played back): it is kept, inline.
|
|
6589
|
+
*/
|
|
6590
|
+
var SettleMode;
|
|
6591
|
+
(function (SettleMode) {
|
|
6592
|
+
SettleMode["Release"] = "release";
|
|
6593
|
+
SettleMode["Hold"] = "hold";
|
|
6594
|
+
})(SettleMode || (SettleMode = {}));
|
|
6595
|
+
function settleModeFor(type, reverse = false) {
|
|
6596
|
+
const hides = reverse ? ENTRANCE_ACTION_TYPES.has(type) : EXIT_ACTION_TYPES.has(type);
|
|
6597
|
+
return hides ? SettleMode.Hold : SettleMode.Release;
|
|
6598
|
+
}
|
|
6599
|
+
/** Every CSS property a run's frames touch. */
|
|
6600
|
+
function framePropertyNames(frames) {
|
|
6601
|
+
const names = new Set();
|
|
6602
|
+
for (const frame of frames ?? [])
|
|
6603
|
+
for (const key of Object.keys(frame))
|
|
6604
|
+
names.add(keyframePropertyName(key));
|
|
6605
|
+
return [...names];
|
|
6606
|
+
}
|
|
6607
|
+
/**
|
|
6608
|
+
* Leave `el` as a run that has ended — or one that never plays, under reduced motion — should (`SettleMode`). The
|
|
6609
|
+
* frame is written (Hold) or released (Release) BEFORE `anim` is cancelled, so nothing flashes between the two.
|
|
6610
|
+
*/
|
|
6611
|
+
function settleInteraction(el, frames, mode, options = {}, anim) {
|
|
6612
|
+
if (mode === SettleMode.Hold) {
|
|
6613
|
+
for (const [prop, value] of keyframeStyleEntries(finalKeyframe(frames))) {
|
|
6614
|
+
if (!options.skip?.has(prop))
|
|
6615
|
+
el.style.setProperty(prop, value);
|
|
6616
|
+
}
|
|
6617
|
+
}
|
|
6618
|
+
else {
|
|
6619
|
+
for (const prop of framePropertyNames(frames))
|
|
6620
|
+
el.style.removeProperty(prop);
|
|
6621
|
+
}
|
|
6622
|
+
anim?.cancel();
|
|
6623
|
+
}
|
|
6624
|
+
/** The keyframe properties an Animation animates (camelCase, as `getKeyframes` names them). */
|
|
6625
|
+
function animatedKeys(anim) {
|
|
6626
|
+
const keyframes = anim.effect?.getKeyframes?.() ?? [];
|
|
6627
|
+
const skip = new Set(['offset', 'computedOffset', 'easing', 'composite']);
|
|
6628
|
+
return keyframes.flatMap(k => Object.keys(k).filter(key => !skip.has(key)));
|
|
6629
|
+
}
|
|
6630
|
+
/**
|
|
6631
|
+
* Interrupt (Emil 52/59/62): the interaction runs in flight on `el` over the same properties are stopped — never
|
|
6632
|
+
* stacked — and the new run starts from what the element shows NOW (its presentation value), not from frame 0: a
|
|
6633
|
+
* hover-out halfway through the hover-in turns back from where it is, a re-trigger continues. Untouched when
|
|
6634
|
+
* nothing is running (the run starts from its own first frame, as before).
|
|
6635
|
+
*/
|
|
6636
|
+
function framesFromLiveValue(el, frames) {
|
|
6637
|
+
const first = frames[0];
|
|
6638
|
+
if (!first)
|
|
6639
|
+
return [...frames];
|
|
6640
|
+
const keys = new Set(Object.keys(first));
|
|
6641
|
+
const running = (el.getAnimations?.() ?? [])
|
|
6642
|
+
.filter(a => a.id === INTERACTION_ANIMATION_ID && animatedKeys(a).some(k => keys.has(k)));
|
|
6643
|
+
if (!running.length)
|
|
6644
|
+
return [...frames];
|
|
6645
|
+
const style = el.ownerDocument?.defaultView?.getComputedStyle(el);
|
|
6646
|
+
const live = { ...first };
|
|
6647
|
+
for (const key of keys) {
|
|
6648
|
+
const value = style?.getPropertyValue(keyframePropertyName(key));
|
|
6649
|
+
if (value)
|
|
6650
|
+
live[key] = value;
|
|
6651
|
+
}
|
|
6652
|
+
for (const anim of running)
|
|
6653
|
+
anim.cancel();
|
|
6654
|
+
return [live, ...frames.slice(1)];
|
|
6655
|
+
}
|
|
6656
|
+
/**
|
|
6657
|
+
* Play an element action and settle it when it ends — THE call both runtimes make for an animated action: it starts
|
|
6658
|
+
* from the live value (`framesFromLiveValue`, inside `animateAction`) and ends by the ONE settle rule. A run that is
|
|
6659
|
+
* interrupted is never settled: the run that interrupted it owns the element now.
|
|
6660
|
+
*/
|
|
6661
|
+
function playInteraction(el, action, frames, timing, options = {}) {
|
|
6662
|
+
const anim = animateAction(el, action, frames, { fill: 'both', ...timing }, { reverse: options.reverse });
|
|
6663
|
+
const mode = settleModeFor(action.type, options.reverse);
|
|
6664
|
+
anim.finished?.then(() => settleInteraction(el, frames, mode, options, anim)).catch(() => { });
|
|
6665
|
+
return anim;
|
|
6666
|
+
}
|
|
6287
6667
|
/**
|
|
6288
6668
|
* Start an element action's animation: THE call both runtimes make. Most actions are one `element.animate`; a text
|
|
6289
6669
|
* reveal plays its pieces and a clip reveal opens from the side the page's direction makes of Start and End, then takes
|
|
@@ -6297,15 +6677,22 @@ function animateAction(el, action, frames, timing, options = {}) {
|
|
|
6297
6677
|
return reveal;
|
|
6298
6678
|
}
|
|
6299
6679
|
if (action.type !== RevealActionType.Clip)
|
|
6300
|
-
return el.animate(frames, timing);
|
|
6680
|
+
return tagged(el.animate(framesFromLiveValue(el, frames), timing));
|
|
6301
6681
|
const rtl = INLINE_FROMS.has(revealFromOf(action.params)) && el.ownerDocument.defaultView?.getComputedStyle(el).direction === 'rtl';
|
|
6302
6682
|
const mirrored = rtl ? clipFrames(action.params, -1) : frames;
|
|
6303
6683
|
const played = rtl && options.reverse ? [...mirrored].reverse() : mirrored;
|
|
6304
|
-
const anim = el.animate(played, timing);
|
|
6684
|
+
const anim = tagged(el.animate(framesFromLiveValue(el, played), timing));
|
|
6305
6685
|
if (!options.reverse)
|
|
6306
6686
|
anim.addEventListener('finish', () => anim.cancel(), { once: true });
|
|
6307
6687
|
return anim;
|
|
6308
6688
|
}
|
|
6689
|
+
function tagged(anim) {
|
|
6690
|
+
try {
|
|
6691
|
+
anim.id = INTERACTION_ANIMATION_ID;
|
|
6692
|
+
}
|
|
6693
|
+
catch { /* a stub without an id */ }
|
|
6694
|
+
return anim;
|
|
6695
|
+
}
|
|
6309
6696
|
/** `translateX` → `translate-x`, `opacity` → `opacity`: the CSS property name of a keyframe key. */
|
|
6310
6697
|
function keyframePropertyName(key) {
|
|
6311
6698
|
return key.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
@@ -6365,6 +6752,145 @@ function finalKeyframe(frames) {
|
|
|
6365
6752
|
return typeof last['clipPath'] === 'string' && FULLY_SHOWN_CLIP.test(last['clipPath']) ? { ...last, clipPath: 'none' } : last;
|
|
6366
6753
|
}
|
|
6367
6754
|
|
|
6755
|
+
/**
|
|
6756
|
+
* What an interaction's easing may be (G6, decision D8): one of the five CSS keywords, a `cubic-bezier()`, or the
|
|
6757
|
+
* NAME of one of the look's easing tokens — so an author's entrance moves on the same curve as the site's buttons and
|
|
6758
|
+
* dialogs (Emil 21: one token system), and Spring plays the look's spring (a `linear()` curve, `springToLinear`).
|
|
6759
|
+
* Both runtimes resolve it the same way, through the CSS custom property on the element itself.
|
|
6760
|
+
*/
|
|
6761
|
+
var EasingKeyword;
|
|
6762
|
+
(function (EasingKeyword) {
|
|
6763
|
+
EasingKeyword["Linear"] = "linear";
|
|
6764
|
+
EasingKeyword["Ease"] = "ease";
|
|
6765
|
+
EasingKeyword["EaseIn"] = "ease-in";
|
|
6766
|
+
EasingKeyword["EaseOut"] = "ease-out";
|
|
6767
|
+
EasingKeyword["EaseInOut"] = "ease-in-out";
|
|
6768
|
+
})(EasingKeyword || (EasingKeyword = {}));
|
|
6769
|
+
/** The look's easing tokens an interaction may name (their `--iox-<name>` holds the curve). */
|
|
6770
|
+
var EasingToken;
|
|
6771
|
+
(function (EasingToken) {
|
|
6772
|
+
EasingToken["Standard"] = "ease-standard";
|
|
6773
|
+
EasingToken["Enter"] = "ease-enter";
|
|
6774
|
+
EasingToken["Exit"] = "ease-exit";
|
|
6775
|
+
EasingToken["Spring"] = "ease-spring";
|
|
6776
|
+
})(EasingToken || (EasingToken = {}));
|
|
6777
|
+
const EASING_KEYWORDS = Object.values(EasingKeyword);
|
|
6778
|
+
const EASING_TOKENS = Object.values(EasingToken);
|
|
6779
|
+
/** What an unknown or unresolvable easing plays as: a deceleration, never ease-in (Emil 17). */
|
|
6780
|
+
const DEFAULT_INTERACTION_EASING = EasingKeyword.EaseOut;
|
|
6781
|
+
const NUMBER = '\\s*(-?\\d*\\.?\\d+)\\s*';
|
|
6782
|
+
const CUBIC_BEZIER = new RegExp(`^cubic-bezier\\(${NUMBER},${NUMBER},${NUMBER},${NUMBER}\\)$`);
|
|
6783
|
+
/** A well-formed `cubic-bezier(x1, y1, x2, y2)` whose x values sit in [0, 1] (what CSS and WAAPI accept). */
|
|
6784
|
+
function isCubicBezier(value) {
|
|
6785
|
+
const m = CUBIC_BEZIER.exec(String(value ?? '').trim());
|
|
6786
|
+
if (!m)
|
|
6787
|
+
return false;
|
|
6788
|
+
const [x1, , x2] = [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
6789
|
+
return x1 >= 0 && x1 <= 1 && x2 >= 0 && x2 <= 1;
|
|
6790
|
+
}
|
|
6791
|
+
function isEasingToken(value) {
|
|
6792
|
+
return EASING_TOKENS.includes(String(value ?? ''));
|
|
6793
|
+
}
|
|
6794
|
+
/** True for anything an interaction may store as its easing. */
|
|
6795
|
+
function isInteractionEasing(value) {
|
|
6796
|
+
return EASING_KEYWORDS.includes(String(value ?? '')) || isEasingToken(value) || isCubicBezier(value);
|
|
6797
|
+
}
|
|
6798
|
+
/** A token's value is a timing function WAAPI plays: a keyword, a cubic-bezier, or a `linear()` / `steps()` curve (the
|
|
6799
|
+
* spring's). Read by shape, so it answers alike in every browser, on the server and in a spec. */
|
|
6800
|
+
const TIMING_FUNCTION = /^(linear|steps)\(.+\)$/;
|
|
6801
|
+
function supportedEasing(value) {
|
|
6802
|
+
return EASING_KEYWORDS.includes(value) || isCubicBezier(value) || TIMING_FUNCTION.test(value);
|
|
6803
|
+
}
|
|
6804
|
+
/**
|
|
6805
|
+
* The timing function WAAPI plays for `easing` on `el`: a keyword or a curve as stored; a token name → the token's
|
|
6806
|
+
* value as the element sees it (`--iox-ease-spring` → its `linear()`), so a section scheme or the canvas's token scope
|
|
6807
|
+
* applies; anything else, or a token the element cannot resolve → `fallback`.
|
|
6808
|
+
*/
|
|
6809
|
+
function resolveInteractionEasing(easing, el, fallback = DEFAULT_INTERACTION_EASING) {
|
|
6810
|
+
const value = String(easing ?? '').trim();
|
|
6811
|
+
if (EASING_KEYWORDS.includes(value) || isCubicBezier(value))
|
|
6812
|
+
return value;
|
|
6813
|
+
if (!isEasingToken(value))
|
|
6814
|
+
return fallback;
|
|
6815
|
+
const resolved = el?.ownerDocument?.defaultView?.getComputedStyle(el).getPropertyValue(`--iox-${value}`)?.trim();
|
|
6816
|
+
return resolved && supportedEasing(resolved) ? resolved : fallback;
|
|
6817
|
+
}
|
|
6818
|
+
|
|
6819
|
+
/**
|
|
6820
|
+
* When a pointer event should move something (G7 — Emil 85/86/91, mobile-native M1/M5). Both runtimes ask here.
|
|
6821
|
+
*
|
|
6822
|
+
* · A hover trigger answers a mouse or a pen only: a finger has no hover, and a tap's emulated mouseenter left the
|
|
6823
|
+
* hover state on until the next tap elsewhere.
|
|
6824
|
+
* · Press feedback lands on pointer-DOWN, at once — the click (pointer-up) commits. Only an action that is pure
|
|
6825
|
+
* feedback moves early: an animation of the pressed element itself that hides nothing and shows nothing. Opening,
|
|
6826
|
+
* closing, navigating and toggling stay on the click, so dragging away still cancels them. A keyboard press (Enter,
|
|
6827
|
+
* Space) has no pointer-down, so it never animates (Emil 2).
|
|
6828
|
+
*/
|
|
6829
|
+
var PointerKind;
|
|
6830
|
+
(function (PointerKind) {
|
|
6831
|
+
PointerKind["Mouse"] = "mouse";
|
|
6832
|
+
PointerKind["Pen"] = "pen";
|
|
6833
|
+
PointerKind["Touch"] = "touch";
|
|
6834
|
+
})(PointerKind || (PointerKind = {}));
|
|
6835
|
+
const HOVERING_POINTERS = new Set([PointerKind.Mouse, PointerKind.Pen]);
|
|
6836
|
+
/** True for a pointer that really hovers — a mouse or a pen, never a finger. An event with no pointer type (a synthetic
|
|
6837
|
+
* `mouseenter`) counts as a mouse. */
|
|
6838
|
+
function isHoverPointer(event) {
|
|
6839
|
+
const kind = event?.pointerType;
|
|
6840
|
+
return !kind || HOVERING_POINTERS.has(kind);
|
|
6841
|
+
}
|
|
6842
|
+
/** True for an action that is press FEEDBACK: it animates the pressed element itself and neither hides nor shows it. */
|
|
6843
|
+
function isPressFeedback(action) {
|
|
6844
|
+
if (!action || action.disabled)
|
|
6845
|
+
return false;
|
|
6846
|
+
if (action.target && action.target !== 'self')
|
|
6847
|
+
return false;
|
|
6848
|
+
if (ENTRANCE_ACTION_TYPES.has(action.type) || EXIT_ACTION_TYPES.has(action.type) || action.type === VisibilityActionType.Toggle)
|
|
6849
|
+
return false;
|
|
6850
|
+
return !!buildInteractionKeyframes(action.type, action.params);
|
|
6851
|
+
}
|
|
6852
|
+
/** A click interaction's actions split by when they run: the feedback on pointer-down, the rest on the click. */
|
|
6853
|
+
function splitPressFeedback(actions) {
|
|
6854
|
+
const onPress = [];
|
|
6855
|
+
const onClick = [];
|
|
6856
|
+
for (const action of actions ?? [])
|
|
6857
|
+
(isPressFeedback(action) ? onPress : onClick).push(action);
|
|
6858
|
+
return { onPress, onClick };
|
|
6859
|
+
}
|
|
6860
|
+
|
|
6861
|
+
/** How far the page has to move before it counts as scrolled — past a trackpad's rest jitter. */
|
|
6862
|
+
const SCROLLED_THRESHOLD_PX = 8;
|
|
6863
|
+
/** True once `offset` has passed the threshold. */
|
|
6864
|
+
function isScrolled(offset, threshold = SCROLLED_THRESHOLD_PX) {
|
|
6865
|
+
return Number.isFinite(offset) && offset > threshold;
|
|
6866
|
+
}
|
|
6867
|
+
/**
|
|
6868
|
+
* Keep `SCROLLED_ATTRIBUTE` on `target` while `source` has scrolled past the threshold — what the Scrolled state (G4)
|
|
6869
|
+
* compiles against, on the live site (the window → the document element) and on the canvas (its scroller → the page).
|
|
6870
|
+
* One passive listener, read once per frame, written only when it changes. Returns the stop.
|
|
6871
|
+
*/
|
|
6872
|
+
function watchScrolled(source, target, threshold = SCROLLED_THRESHOLD_PX) {
|
|
6873
|
+
let frame = 0;
|
|
6874
|
+
const raf = typeof requestAnimationFrame === 'function' ? requestAnimationFrame : (fn) => setTimeout(() => fn(0), 16);
|
|
6875
|
+
const caf = typeof cancelAnimationFrame === 'function' ? cancelAnimationFrame : (id) => clearTimeout(id);
|
|
6876
|
+
const sync = () => {
|
|
6877
|
+
frame = 0;
|
|
6878
|
+
const scrolled = isScrolled(source.scrollY ?? source.scrollTop ?? 0, threshold);
|
|
6879
|
+
if (scrolled !== target.hasAttribute(SCROLLED_ATTRIBUTE))
|
|
6880
|
+
target.toggleAttribute(SCROLLED_ATTRIBUTE, scrolled);
|
|
6881
|
+
};
|
|
6882
|
+
const onScroll = () => { if (!frame)
|
|
6883
|
+
frame = raf(sync); };
|
|
6884
|
+
source.addEventListener('scroll', onScroll, { passive: true });
|
|
6885
|
+
sync();
|
|
6886
|
+
return () => {
|
|
6887
|
+
source.removeEventListener('scroll', onScroll);
|
|
6888
|
+
if (frame)
|
|
6889
|
+
caf(frame);
|
|
6890
|
+
target.removeAttribute(SCROLLED_ATTRIBUTE);
|
|
6891
|
+
};
|
|
6892
|
+
}
|
|
6893
|
+
|
|
6368
6894
|
/**
|
|
6369
6895
|
* page-style — the ONE compiler that turns a page's style settings into CSS.
|
|
6370
6896
|
*
|
|
@@ -8179,6 +8705,251 @@ function playDualLayerTransition(resolved, layers) {
|
|
|
8179
8705
|
return inc.animate(resolved.inFrames, resolved.inOptions).finished.then(() => undefined, () => undefined);
|
|
8180
8706
|
}
|
|
8181
8707
|
|
|
8708
|
+
const FIELD_ATTRIBUTE = 'data-iox-field';
|
|
8709
|
+
const CONTROL_CLASS = 'iox-field__control';
|
|
8710
|
+
const ERROR_CLASS = 'iox-field__error';
|
|
8711
|
+
const HELP_CLASS = 'iox-field__help';
|
|
8712
|
+
const INVALID_CLASS = 'iox-invalid';
|
|
8713
|
+
const BOOTED = '__ioxStringForm';
|
|
8714
|
+
/**
|
|
8715
|
+
* A form the SSR engine's string renderer wrote — every header / footer global, every string-rendered page —
|
|
8716
|
+
* run with the behaviour `IoxFormComponent` gives a component-path form (vectoriox/iox-client-engine#13): the
|
|
8717
|
+
* forms-core runtime decides when an error shows, a valid submit POSTs through the host's transport with the
|
|
8718
|
+
* anti-spam fields, the form moves idle → submitting → success / error on `data-iox-state` (the shared CSS shows
|
|
8719
|
+
* the designed block), the first problem takes focus and the failure is announced. Nothing posts natively.
|
|
8720
|
+
*
|
|
8721
|
+
* Its fields come from the form itself (`data-iox-form-fields`, the spec minus its destinations), so the
|
|
8722
|
+
* runtime needs no node tree. Idempotent per element.
|
|
8723
|
+
*/
|
|
8724
|
+
class StringFormRuntime {
|
|
8725
|
+
constructor(form, spec, options = {}) {
|
|
8726
|
+
this.form = form;
|
|
8727
|
+
this.spec = spec;
|
|
8728
|
+
this.options = options;
|
|
8729
|
+
this.state = INITIAL_FORM_STATE;
|
|
8730
|
+
this.inFlight = false;
|
|
8731
|
+
this.renderedAt = Date.now();
|
|
8732
|
+
this.listeners = [];
|
|
8733
|
+
this.messages = options.messages ?? defaultMessageResolver;
|
|
8734
|
+
this.listen('input', e => this.onEdit(e));
|
|
8735
|
+
this.listen('change', e => this.onEdit(e));
|
|
8736
|
+
this.listen('focusout', e => this.onLeave(e));
|
|
8737
|
+
this.listen('submit', e => this.onSubmit(e));
|
|
8738
|
+
form[BOOTED] = this;
|
|
8739
|
+
}
|
|
8740
|
+
destroy() {
|
|
8741
|
+
for (const [type, fn] of this.listeners)
|
|
8742
|
+
this.form.removeEventListener(type, fn);
|
|
8743
|
+
this.listeners.length = 0;
|
|
8744
|
+
delete this.form[BOOTED];
|
|
8745
|
+
}
|
|
8746
|
+
/** The visitor's answers, keyed by the spec's field names. */
|
|
8747
|
+
values() {
|
|
8748
|
+
const out = {};
|
|
8749
|
+
for (const field of this.spec.fields)
|
|
8750
|
+
out[field.name] = this.valueOf(field);
|
|
8751
|
+
return out;
|
|
8752
|
+
}
|
|
8753
|
+
listen(type, fn) {
|
|
8754
|
+
this.form.addEventListener(type, fn);
|
|
8755
|
+
this.listeners.push([type, fn]);
|
|
8756
|
+
}
|
|
8757
|
+
fieldNameOf(event) {
|
|
8758
|
+
const target = event.target;
|
|
8759
|
+
if (!target?.classList?.contains(CONTROL_CLASS))
|
|
8760
|
+
return null;
|
|
8761
|
+
return target.closest(`[${FIELD_ATTRIBUTE}]`)?.getAttribute(FIELD_ATTRIBUTE) ?? null;
|
|
8762
|
+
}
|
|
8763
|
+
onEdit(event) {
|
|
8764
|
+
const name = this.fieldNameOf(event);
|
|
8765
|
+
if (!name)
|
|
8766
|
+
return;
|
|
8767
|
+
this.state = onFieldChange(this.state, name, this.values(), this.fullSpec());
|
|
8768
|
+
this.render();
|
|
8769
|
+
}
|
|
8770
|
+
onLeave(event) {
|
|
8771
|
+
const name = this.fieldNameOf(event);
|
|
8772
|
+
if (!name)
|
|
8773
|
+
return;
|
|
8774
|
+
this.state = onFieldBlur(this.state, name, this.values(), this.fullSpec());
|
|
8775
|
+
this.render();
|
|
8776
|
+
}
|
|
8777
|
+
onSubmit(event) {
|
|
8778
|
+
// Never the browser's own submission: it would reload the page with the answers in the address and send nothing.
|
|
8779
|
+
event.preventDefault();
|
|
8780
|
+
if (this.inFlight)
|
|
8781
|
+
return;
|
|
8782
|
+
const values = this.values();
|
|
8783
|
+
this.state = onSubmitAttempt(this.state, values, this.fullSpec());
|
|
8784
|
+
this.render();
|
|
8785
|
+
if (Object.keys(this.state.errors).length) {
|
|
8786
|
+
this.announce(this.state.errors);
|
|
8787
|
+
this.focusFirstInvalid();
|
|
8788
|
+
return;
|
|
8789
|
+
}
|
|
8790
|
+
if (!this.options.transport)
|
|
8791
|
+
return;
|
|
8792
|
+
void this.send(values);
|
|
8793
|
+
}
|
|
8794
|
+
async send(values) {
|
|
8795
|
+
this.inFlight = true;
|
|
8796
|
+
this.setRenderState(FormRenderState.Submitting);
|
|
8797
|
+
let response;
|
|
8798
|
+
try {
|
|
8799
|
+
response = await this.options.transport({
|
|
8800
|
+
formId: this.form.getAttribute('data-iox-form') ?? '',
|
|
8801
|
+
values: withAntiSpamFields(values, this.renderedAt, this.honeypotValue()),
|
|
8802
|
+
});
|
|
8803
|
+
}
|
|
8804
|
+
catch {
|
|
8805
|
+
// The call never reached the server — the Error block, never Success.
|
|
8806
|
+
response = { ok: false };
|
|
8807
|
+
}
|
|
8808
|
+
this.inFlight = false;
|
|
8809
|
+
this.apply(response);
|
|
8810
|
+
}
|
|
8811
|
+
apply(response) {
|
|
8812
|
+
const outcome = submitOutcome(response);
|
|
8813
|
+
this.setRenderState(outcome.state);
|
|
8814
|
+
if (outcome.errors) {
|
|
8815
|
+
this.state = { ...this.state, submitted: true, submitting: false, errors: { ...outcome.errors } };
|
|
8816
|
+
this.render();
|
|
8817
|
+
this.announce(outcome.errors);
|
|
8818
|
+
this.focusFirstInvalid();
|
|
8819
|
+
return;
|
|
8820
|
+
}
|
|
8821
|
+
if (outcome.state === FormRenderState.Error) {
|
|
8822
|
+
this.announce(null);
|
|
8823
|
+
return;
|
|
8824
|
+
}
|
|
8825
|
+
this.form.reset();
|
|
8826
|
+
this.state = resetFormState();
|
|
8827
|
+
this.render();
|
|
8828
|
+
if (outcome.redirect)
|
|
8829
|
+
this.options.navigate?.(outcome.redirect);
|
|
8830
|
+
}
|
|
8831
|
+
setRenderState(state) {
|
|
8832
|
+
this.form.setAttribute(FORM_STATE_ATTRIBUTE, state);
|
|
8833
|
+
if (state === FormRenderState.Submitting)
|
|
8834
|
+
this.form.setAttribute('aria-busy', 'true');
|
|
8835
|
+
else
|
|
8836
|
+
this.form.removeAttribute('aria-busy');
|
|
8837
|
+
syncSubmitButtons(this.form, state);
|
|
8838
|
+
this.state = { ...this.state, submitting: state === FormRenderState.Submitting };
|
|
8839
|
+
}
|
|
8840
|
+
fullSpec() {
|
|
8841
|
+
return { formId: '', name: '', feedback: this.spec.feedback, fields: this.spec.fields, actions: [], antiSpam: { honeypot: true, minFillMs: 0 } };
|
|
8842
|
+
}
|
|
8843
|
+
fieldElement(name) {
|
|
8844
|
+
// Compared, never interpolated into a selector: a field key may hold characters a selector chokes on.
|
|
8845
|
+
for (const el of Array.from(this.form.querySelectorAll(`[${FIELD_ATTRIBUTE}]`))) {
|
|
8846
|
+
if (el.getAttribute(FIELD_ATTRIBUTE) === name && el.closest('form') === this.form)
|
|
8847
|
+
return el;
|
|
8848
|
+
}
|
|
8849
|
+
return null;
|
|
8850
|
+
}
|
|
8851
|
+
controlsOf(field) {
|
|
8852
|
+
return Array.from(field.querySelectorAll(`.${CONTROL_CLASS}`));
|
|
8853
|
+
}
|
|
8854
|
+
valueOf(spec) {
|
|
8855
|
+
const el = this.fieldElement(spec.name);
|
|
8856
|
+
if (!el)
|
|
8857
|
+
return undefined;
|
|
8858
|
+
const controls = this.controlsOf(el);
|
|
8859
|
+
const first = controls[0];
|
|
8860
|
+
if (!first)
|
|
8861
|
+
return undefined;
|
|
8862
|
+
if (first.type === 'radio')
|
|
8863
|
+
return controls.find(c => c.checked)?.value ?? '';
|
|
8864
|
+
if (first.type === 'checkbox')
|
|
8865
|
+
return first.checked;
|
|
8866
|
+
if (first.type === 'range')
|
|
8867
|
+
return Number(first.value);
|
|
8868
|
+
return first.value;
|
|
8869
|
+
}
|
|
8870
|
+
/** Every field's error, aria-invalid, describedby and invalid class — as `IoxFieldComponent` renders them. */
|
|
8871
|
+
render() {
|
|
8872
|
+
for (const field of this.spec.fields) {
|
|
8873
|
+
const el = this.fieldElement(field.name);
|
|
8874
|
+
if (!el)
|
|
8875
|
+
continue;
|
|
8876
|
+
const error = this.state.errors[field.name];
|
|
8877
|
+
const shown = !!error && shouldShowError(field.name, this.state, this.spec.feedback);
|
|
8878
|
+
const text = shown ? this.messages(error.messageKey, error.params) : '';
|
|
8879
|
+
el.classList.toggle(INVALID_CLASS, shown && !!text);
|
|
8880
|
+
this.renderError(el, shown ? text : '');
|
|
8881
|
+
}
|
|
8882
|
+
}
|
|
8883
|
+
renderError(field, text) {
|
|
8884
|
+
const controls = this.controlsOf(field);
|
|
8885
|
+
const id = (controls[0]?.id || `iox-field-${field.getAttribute(FIELD_ATTRIBUTE)}`) + '-error';
|
|
8886
|
+
let p = field.querySelector(`.${ERROR_CLASS}`);
|
|
8887
|
+
if (text) {
|
|
8888
|
+
if (!p) {
|
|
8889
|
+
p = field.ownerDocument.createElement('p');
|
|
8890
|
+
p.className = ERROR_CLASS;
|
|
8891
|
+
p.id = id;
|
|
8892
|
+
p.setAttribute('role', 'alert');
|
|
8893
|
+
field.appendChild(p);
|
|
8894
|
+
}
|
|
8895
|
+
p.textContent = text;
|
|
8896
|
+
}
|
|
8897
|
+
else {
|
|
8898
|
+
p?.remove();
|
|
8899
|
+
}
|
|
8900
|
+
const helpId = field.querySelector(`.${HELP_CLASS}`)?.id;
|
|
8901
|
+
const describedBy = [helpId, text ? id : ''].filter(Boolean).join(' ');
|
|
8902
|
+
for (const control of controls) {
|
|
8903
|
+
if (text)
|
|
8904
|
+
control.setAttribute('aria-invalid', 'true');
|
|
8905
|
+
else
|
|
8906
|
+
control.removeAttribute('aria-invalid');
|
|
8907
|
+
if (describedBy)
|
|
8908
|
+
control.setAttribute('aria-describedby', describedBy);
|
|
8909
|
+
else
|
|
8910
|
+
control.removeAttribute('aria-describedby');
|
|
8911
|
+
}
|
|
8912
|
+
}
|
|
8913
|
+
announce(errors) {
|
|
8914
|
+
const region = this.form.querySelector(`[${FORM_ANNOUNCER_ATTRIBUTE}]`);
|
|
8915
|
+
if (!region)
|
|
8916
|
+
return;
|
|
8917
|
+
// Cleared first: a live region speaks on CHANGE, and the same failure twice must be said twice.
|
|
8918
|
+
region.textContent = '';
|
|
8919
|
+
const next = failureAnnouncement(errors);
|
|
8920
|
+
Promise.resolve().then(() => { region.textContent = next; });
|
|
8921
|
+
}
|
|
8922
|
+
focusFirstInvalid() {
|
|
8923
|
+
for (const field of this.spec.fields) {
|
|
8924
|
+
if (!this.state.errors[field.name])
|
|
8925
|
+
continue;
|
|
8926
|
+
const el = this.fieldElement(field.name);
|
|
8927
|
+
this.controlsOf(el ?? this.form)[0]?.focus();
|
|
8928
|
+
return;
|
|
8929
|
+
}
|
|
8930
|
+
}
|
|
8931
|
+
honeypotValue() {
|
|
8932
|
+
return this.form.querySelector(`input[name="${HONEYPOT_FIELD}"]`)?.value ?? '';
|
|
8933
|
+
}
|
|
8934
|
+
}
|
|
8935
|
+
/** The runtime already running a form element, if any. */
|
|
8936
|
+
function stringFormRuntimeOf(form) {
|
|
8937
|
+
return form?.[BOOTED] ?? null;
|
|
8938
|
+
}
|
|
8939
|
+
/**
|
|
8940
|
+
* Run a string-rendered form (one carrying `data-iox-form-fields`) — once: a form already running is returned as is.
|
|
8941
|
+
* Null for an element that is not one (a component-path form never carries the attribute; it runs itself).
|
|
8942
|
+
*/
|
|
8943
|
+
function attachStringForm(form, options = {}) {
|
|
8944
|
+
if (!form || form.tagName !== 'FORM')
|
|
8945
|
+
return null;
|
|
8946
|
+
const running = stringFormRuntimeOf(form);
|
|
8947
|
+
if (running)
|
|
8948
|
+
return running;
|
|
8949
|
+
const spec = parseClientFormSpec(form.getAttribute(FORM_FIELDS_ATTRIBUTE));
|
|
8950
|
+
return spec ? new StringFormRuntime(form, spec, options) : null;
|
|
8951
|
+
}
|
|
8952
|
+
|
|
8182
8953
|
/*
|
|
8183
8954
|
* Public API Surface of iox-ui
|
|
8184
8955
|
*/
|
|
@@ -8187,5 +8958,5 @@ function playDualLayerTransition(resolved, layers) {
|
|
|
8187
8958
|
* Generated bundle index. Do not edit.
|
|
8188
8959
|
*/
|
|
8189
8960
|
|
|
8190
|
-
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_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, ENTRANCE_ACTION_TYPES, ENTRANCE_FALLBACK_CSS, ENTRANCE_NOSCRIPT_CSS, ENTRANCE_PRESTATE_ATTRIBUTE, EXIT_ACTION_TYPES, FormRenderState, FormRuntimeService, GALLERY_EFFECTS, GALLERY_EFFECT_DESCRIPTORS, GalleryLoop,
|
|
8961
|
+
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 };
|
|
8191
8962
|
//# sourceMappingURL=vectoriox-iox-ui.mjs.map
|