@vectoriox/iox-ui 5.37.0 → 5.39.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/actions-core/index.d.ts +6 -1
- package/composite-core/index.d.ts +684 -28
- package/design-core/index.d.ts +452 -6
- package/fesm2022/vectoriox-iox-ui-actions-core.mjs +12 -4
- package/fesm2022/vectoriox-iox-ui-actions-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-composite-core.mjs +1761 -143
- package/fesm2022/vectoriox-iox-ui-composite-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-design-core.mjs +555 -6
- package/fesm2022/vectoriox-iox-ui-design-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-generation-core.mjs +534 -12
- package/fesm2022/vectoriox-iox-ui-generation-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-media-core.mjs +571 -0
- package/fesm2022/vectoriox-iox-ui-media-core.mjs.map +1 -0
- package/fesm2022/vectoriox-iox-ui.mjs +494 -127
- package/fesm2022/vectoriox-iox-ui.mjs.map +1 -1
- package/generation-core/index.d.ts +210 -4
- package/index.d.ts +217 -30
- package/media-core/index.d.ts +277 -0
- package/media-core/package.json +3 -0
- package/package.json +5 -1
- package/src/actions-core/element-actions.ts +12 -3
- package/src/composite-core/accordion-runtime.ts +3 -15
- package/src/composite-core/accordion.ts +4 -2
- package/src/composite-core/composite-kinds.ts +121 -7
- package/src/composite-core/composite-node.ts +4 -0
- package/src/composite-core/composite-runtime.ts +68 -0
- package/src/composite-core/exclusive-runtime.ts +358 -0
- package/src/composite-core/lightbox.ts +192 -0
- package/src/composite-core/public-api.ts +8 -2
- package/src/composite-core/slider-runtime.ts +180 -0
- package/src/composite-core/slider.ts +378 -0
- package/src/composite-core/structure.ts +38 -5
- package/src/composite-core/tabs.ts +628 -0
- package/src/design-core/contrast-audit.ts +5 -0
- package/src/design-core/element-styles.ts +34 -3
- package/src/design-core/flavor-spec.generated.ts +517 -2
- package/src/generation-core/baseline-roles.ts +2 -0
- package/src/generation-core/compose-site.ts +147 -4
- package/src/generation-core/node-baselines.generated.ts +156 -6
- package/src/generation-core/patterns.ts +20 -0
- package/src/generation-core/site-spec.generated.ts +210 -0
- package/src/media-core/public-api.ts +9 -0
- package/src/media-core/video-runtime.ts +239 -0
- package/src/media-core/video.ts +443 -0
- package/styles/iox-design.css +112 -0
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import { readOwnedTrait, writeOwnedTrait, elementStyleById, switchElementStyle, LinkType, setColorScheme, COLOR_SCHEME_TRAIT } from '@vectoriox/iox-ui/design-core';
|
|
1
|
+
import { readOwnedTrait, writeOwnedTrait, elementStyleById, switchElementStyle, LinkType, setColorScheme, COLOR_SCHEME_TRAIT, roleClass } from '@vectoriox/iox-ui/design-core';
|
|
2
2
|
|
|
3
|
+
/** What a recipe asks its consumer to create: a NEW node of `type`, the way that consumer creates one
|
|
4
|
+
* (the builder's `createConfig`, generation-core's `createSerializedNode`), with these props and this
|
|
5
|
+
* element style. `key` is stable per recipe node — a consumer that needs deterministic ids builds them
|
|
6
|
+
* from it (generation-core: `<root id>-<key>`). */
|
|
7
|
+
/** `elementStyle: NO_ELEMENT_STYLE` asks for a node that wears NO element style (undefined / null = the type's default,
|
|
8
|
+
* as both node factories have always read it): a migrated slider's bare chevrons, a lightbox's slide (CC3). */
|
|
9
|
+
const NO_ELEMENT_STYLE = '';
|
|
3
10
|
/** A recipe part: a NEW node (`create`) playing `role` under `label`, with the layout `base` styles its recipe
|
|
4
11
|
* gives it — its LOOK is its element style. Shared by every recipe (Accordion, Disclosure). */
|
|
5
12
|
function makeCompositePart(create, spec, role, label, base) {
|
|
@@ -118,15 +125,18 @@ function removeFromComposite(root, node) {
|
|
|
118
125
|
}
|
|
119
126
|
|
|
120
127
|
/**
|
|
121
|
-
* The
|
|
122
|
-
*
|
|
123
|
-
* same attribute planners and
|
|
124
|
-
*
|
|
128
|
+
* The composites' kinds and roles. Two families:
|
|
129
|
+
* - OPEN/CLOSE (CC2 Accordion, CC2.1 Disclosure — composite plan T9 / T21): a Disclosure is a one-item accordion whose
|
|
130
|
+
* ROOT is its item, so both run the same state machine, attribute planners and runtime (`accordion-runtime.ts`);
|
|
131
|
+
* - EXCLUSIVE (CC4 Tabs — composite T17; the Segmented control joins in CC4b): exactly one item is on, a trigger and
|
|
132
|
+
* its panel paired by `itemKey` (`tabs.ts`, `exclusive-runtime.ts`).
|
|
133
|
+
* Every consumer that treats "an item", "a trigger" or "a panel" alike asks these predicates, never a role literal.
|
|
125
134
|
*/
|
|
126
135
|
var CompositeKind;
|
|
127
136
|
(function (CompositeKind) {
|
|
128
137
|
CompositeKind["Accordion"] = "accordion";
|
|
129
138
|
CompositeKind["Disclosure"] = "disclosure";
|
|
139
|
+
CompositeKind["Tabs"] = "tabs";
|
|
130
140
|
})(CompositeKind || (CompositeKind = {}));
|
|
131
141
|
var AccordionRole;
|
|
132
142
|
(function (AccordionRole) {
|
|
@@ -150,21 +160,53 @@ var DisclosureRole;
|
|
|
150
160
|
DisclosureRole["Icon"] = "disclosure.icon";
|
|
151
161
|
DisclosureRole["Panel"] = "disclosure.panel";
|
|
152
162
|
})(DisclosureRole || (DisclosureRole = {}));
|
|
163
|
+
/**
|
|
164
|
+
* TABS (CC4a — composite T17). The tab LIST holds the triggers, the PANELS holder holds the panels; a tab and its panel
|
|
165
|
+
* share an `itemKey` (never their index — moving a tab keeps its own panel). From content (R3) the list and the panels
|
|
166
|
+
* holder are both Repeaters over the SAME source, one row giving its tab AND its panel (paired by the row's id).
|
|
167
|
+
*/
|
|
168
|
+
var TabsRole;
|
|
169
|
+
(function (TabsRole) {
|
|
170
|
+
TabsRole["Tabs"] = "tabs";
|
|
171
|
+
TabsRole["List"] = "tabs.list";
|
|
172
|
+
TabsRole["Trigger"] = "tabs.trigger";
|
|
173
|
+
TabsRole["Panels"] = "tabs.panels";
|
|
174
|
+
TabsRole["Panel"] = "tabs.panel";
|
|
175
|
+
})(TabsRole || (TabsRole = {}));
|
|
153
176
|
const COMPOSITE_KIND_ROLES = {
|
|
154
177
|
[CompositeKind.Accordion]: { root: AccordionRole.Accordion, item: AccordionRole.Item, trigger: AccordionRole.Trigger, panel: AccordionRole.Panel, icon: AccordionRole.Icon },
|
|
155
178
|
[CompositeKind.Disclosure]: { root: DisclosureRole.Disclosure, item: DisclosureRole.Disclosure, trigger: DisclosureRole.Trigger, panel: DisclosureRole.Panel, icon: DisclosureRole.Icon },
|
|
156
179
|
};
|
|
180
|
+
const EXCLUSIVE_KIND_ROLES = {
|
|
181
|
+
[CompositeKind.Tabs]: { root: TabsRole.Tabs, list: TabsRole.List, trigger: TabsRole.Trigger, panels: TabsRole.Panels, panel: TabsRole.Panel },
|
|
182
|
+
};
|
|
157
183
|
const KINDS = Object.values(CompositeKind);
|
|
158
|
-
const
|
|
184
|
+
const TOGGLE_KINDS = Object.keys(COMPOSITE_KIND_ROLES);
|
|
185
|
+
const EXCLUSIVE_KINDS$1 = Object.keys(EXCLUSIVE_KIND_ROLES);
|
|
186
|
+
const rolesOf = (pick) => new Set(TOGGLE_KINDS.map(k => COMPOSITE_KIND_ROLES[k][pick]));
|
|
187
|
+
const exclusiveRolesOf = (pick) => new Set(EXCLUSIVE_KINDS$1.map(k => EXCLUSIVE_KIND_ROLES[k][pick]).filter((r) => !!r));
|
|
159
188
|
const ROOT_ROLES = rolesOf('root');
|
|
160
189
|
const ITEM_ROLES = rolesOf('item');
|
|
161
190
|
const TRIGGER_ROLES = rolesOf('trigger');
|
|
162
191
|
const PANEL_ROLES = rolesOf('panel');
|
|
192
|
+
const EXCLUSIVE_ROOT_ROLES = exclusiveRolesOf('root');
|
|
193
|
+
const EXCLUSIVE_LIST_ROLES = exclusiveRolesOf('list');
|
|
194
|
+
const EXCLUSIVE_TRIGGER_ROLES = exclusiveRolesOf('trigger');
|
|
195
|
+
const EXCLUSIVE_PANELS_ROLES = exclusiveRolesOf('panels');
|
|
196
|
+
const EXCLUSIVE_PANEL_ROLES = exclusiveRolesOf('panel');
|
|
163
197
|
/** The kind a role belongs to (`disclosure.panel` → Disclosure), or null for any other role. */
|
|
164
198
|
function compositeKindOfRole(role) {
|
|
165
199
|
const head = role?.split('.')[0];
|
|
166
200
|
return KINDS.includes(head ?? '') ? head : null;
|
|
167
201
|
}
|
|
202
|
+
/** True for a kind the open/close runtime runs. */
|
|
203
|
+
function isToggleKind(kind) {
|
|
204
|
+
return !!kind && TOGGLE_KINDS.includes(kind);
|
|
205
|
+
}
|
|
206
|
+
/** True for a kind the exclusive runtime runs. */
|
|
207
|
+
function isExclusiveKind(kind) {
|
|
208
|
+
return !!kind && EXCLUSIVE_KINDS$1.includes(kind);
|
|
209
|
+
}
|
|
168
210
|
/** A root that runs the open/close runtime (an Accordion or a Disclosure). */
|
|
169
211
|
function isCompositeRoot(node) {
|
|
170
212
|
return !!node?.role && ROOT_ROLES.has(node.role);
|
|
@@ -211,6 +253,61 @@ function compositeItemOf(node, path) {
|
|
|
211
253
|
}
|
|
212
254
|
return null;
|
|
213
255
|
}
|
|
256
|
+
// ── the exclusive family (CC4a Tabs) ───────────────────────────────────────────────────────────
|
|
257
|
+
/** A root that runs the exclusive runtime (Tabs). */
|
|
258
|
+
function isExclusiveRoot(node) {
|
|
259
|
+
return !!node?.role && EXCLUSIVE_ROOT_ROLES.has(node.role);
|
|
260
|
+
}
|
|
261
|
+
/** The holder of an exclusive composite's triggers (a tab list): it takes only triggers. */
|
|
262
|
+
function isExclusiveList(node) {
|
|
263
|
+
return !!node?.role && EXCLUSIVE_LIST_ROLES.has(node.role);
|
|
264
|
+
}
|
|
265
|
+
/** One of an exclusive composite's triggers (a tab): pressing it turns its item on. */
|
|
266
|
+
function isExclusiveTrigger(node) {
|
|
267
|
+
return !!node?.role && EXCLUSIVE_TRIGGER_ROLES.has(node.role);
|
|
268
|
+
}
|
|
269
|
+
/** The holder of an exclusive composite's panels: it takes only panels. */
|
|
270
|
+
function isExclusivePanels(node) {
|
|
271
|
+
return !!node?.role && EXCLUSIVE_PANELS_ROLES.has(node.role);
|
|
272
|
+
}
|
|
273
|
+
/** One of an exclusive composite's panels (a tab panel): shown only while its item is on. */
|
|
274
|
+
function isExclusivePanel(node) {
|
|
275
|
+
return !!node?.role && EXCLUSIVE_PANEL_ROLES.has(node.role);
|
|
276
|
+
}
|
|
277
|
+
/** True for a trigger or a panel — a part that IS one item of an exclusive composite (it carries the `itemKey`). */
|
|
278
|
+
function isExclusiveItemPart(node) {
|
|
279
|
+
return isExclusiveTrigger(node) || isExclusivePanel(node);
|
|
280
|
+
}
|
|
281
|
+
/** True for any part of a Tabs (its root included). */
|
|
282
|
+
function isTabsPart(node) {
|
|
283
|
+
return compositeKindOfRole(node?.role) === CompositeKind.Tabs;
|
|
284
|
+
}
|
|
285
|
+
/** A root of either family — anything that runs a composite runtime. */
|
|
286
|
+
function isAnyCompositeRoot(node) {
|
|
287
|
+
return isCompositeRoot(node) || isExclusiveRoot(node);
|
|
288
|
+
}
|
|
289
|
+
/** The exclusive root a node belongs to (the node itself when it is one), or null. `path` = its ancestors, root first. */
|
|
290
|
+
function exclusiveRootOf(node, path) {
|
|
291
|
+
if (isExclusiveRoot(node))
|
|
292
|
+
return node;
|
|
293
|
+
for (let i = path.length - 1; i >= 0; i--)
|
|
294
|
+
if (isExclusiveRoot(path[i]))
|
|
295
|
+
return path[i];
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
/** The trigger or panel a node sits in (itself when it is one) inside an exclusive composite, or null. A nested
|
|
299
|
+
* exclusive root ends the walk. */
|
|
300
|
+
function exclusiveItemPartOf(node, path) {
|
|
301
|
+
if (isExclusiveItemPart(node))
|
|
302
|
+
return node;
|
|
303
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
304
|
+
if (isExclusiveItemPart(path[i]))
|
|
305
|
+
return path[i];
|
|
306
|
+
if (isExclusiveRoot(path[i]))
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
214
311
|
|
|
215
312
|
/**
|
|
216
313
|
* ACCORDION (CC2 — composite K1; complex-components boards 3, 4 and 9, APPROVED 2026-10-20). Not a
|
|
@@ -271,7 +368,7 @@ const ACCORDION_WORDS = [
|
|
|
271
368
|
{ question: 'Write the third question', answer: 'Answer it plainly, in two or three short sentences.' },
|
|
272
369
|
];
|
|
273
370
|
const ACCORDION_NEW_ITEM_WORDS = { question: 'Write a new question', answer: 'Answer it plainly, in two or three short sentences.' };
|
|
274
|
-
var Type$
|
|
371
|
+
var Type$4;
|
|
275
372
|
(function (Type) {
|
|
276
373
|
Type["Container"] = "Container";
|
|
277
374
|
Type["LinkedContainer"] = "LinkedContainer";
|
|
@@ -279,8 +376,8 @@ var Type$2;
|
|
|
279
376
|
Type["Text"] = "Text";
|
|
280
377
|
Type["Icon"] = "Icon";
|
|
281
378
|
Type["Repeater"] = "Repeater";
|
|
282
|
-
})(Type$
|
|
283
|
-
var Style$
|
|
379
|
+
})(Type$4 || (Type$4 = {}));
|
|
380
|
+
var Style$4;
|
|
284
381
|
(function (Style) {
|
|
285
382
|
Style["Root"] = "box";
|
|
286
383
|
Style["Item"] = "accordion-item";
|
|
@@ -289,14 +386,14 @@ var Style$2;
|
|
|
289
386
|
Style["Icon"] = "icon";
|
|
290
387
|
Style["Panel"] = "accordion-panel";
|
|
291
388
|
Style["Answer"] = "body";
|
|
292
|
-
})(Style$
|
|
293
|
-
const ZERO_BOX$
|
|
389
|
+
})(Style$4 || (Style$4 = {}));
|
|
390
|
+
const ZERO_BOX$3 = '0px 0px 0px 0px';
|
|
294
391
|
const HEADING_LEVELS = ['h2', 'h3', 'h4', 'h5', 'h6'];
|
|
295
392
|
const DEFAULT_LEVEL = 'h3';
|
|
296
393
|
/** A linked trait the header must never carry: it is a button, not a link. */
|
|
297
394
|
const LINK_TYPE$1 = 'linkType';
|
|
298
395
|
/** What each part holds in the recipe — layout only; its LOOK is its element style. */
|
|
299
|
-
const PART_BASE$
|
|
396
|
+
const PART_BASE$2 = {
|
|
300
397
|
// The items are a column a token apart (`accordion-gap`: 0 for a divider list, 12px for cards).
|
|
301
398
|
[AccordionRole.Accordion]: { display: 'flex', flexDirection: 'column', gap: 'var(--iox-accordion-gap)', width: '100%', minHeight: 'auto', height: 'auto' },
|
|
302
399
|
[AccordionRole.Item]: { display: 'block', width: '100%', minHeight: 'auto', height: 'auto' },
|
|
@@ -307,16 +404,16 @@ const PART_BASE$1 = {
|
|
|
307
404
|
};
|
|
308
405
|
/** The question INHERITS the header's ink (board 4's wears table: the Question reads no colour token), so the
|
|
309
406
|
* header's hover ink reaches the words; and it sits in the row with no margin of its own. */
|
|
310
|
-
const QUESTION_BASE = { color: 'inherit', margin: ZERO_BOX$
|
|
311
|
-
const ANSWER_BASE = { margin: ZERO_BOX$
|
|
312
|
-
function make$
|
|
313
|
-
return makeCompositePart(create, { key, type, props, elementStyle: style }, role, label, role ? PART_BASE$
|
|
407
|
+
const QUESTION_BASE = { color: 'inherit', margin: ZERO_BOX$3, flexGrow: '1', minWidth: '0px' };
|
|
408
|
+
const ANSWER_BASE = { margin: ZERO_BOX$3, padding: ZERO_BOX$3, minHeight: 'auto' };
|
|
409
|
+
function make$3(create, key, type, role, style, props = {}, label) {
|
|
410
|
+
return makeCompositePart(create, { key, type, props, elementStyle: style }, role, label, role ? PART_BASE$2[role] : undefined);
|
|
314
411
|
}
|
|
315
412
|
/** The icon of an item (an accordion's, or a disclosure's — `role`); its Item-open state (the turn) is driven by
|
|
316
413
|
* ITS item (`stateTriggers`). ONE builder for both kinds. */
|
|
317
414
|
function buildCompositeIcon(create, key, icon, itemId, role = AccordionRole.Icon) {
|
|
318
415
|
const glyph = ACCORDION_ICON_GLYPH[icon];
|
|
319
|
-
const node = makeCompositePart(create, { key, type: Type$
|
|
416
|
+
const node = makeCompositePart(create, { key, type: Type$4.Icon, props: { iconClass: glyph.iconClass }, elementStyle: Style$4.Icon }, role, 'Icon', PART_BASE$2[AccordionRole.Icon]);
|
|
320
417
|
node.stateStyles = { ...(node.stateStyles ?? {}), [ITEM_OPEN_STATE]: { rotate: glyph.turn } };
|
|
321
418
|
if (itemId)
|
|
322
419
|
node.stateTriggers = { ...(node.stateTriggers ?? {}), [ITEM_OPEN_STATE]: itemId };
|
|
@@ -324,14 +421,14 @@ function buildCompositeIcon(create, key, icon, itemId, role = AccordionRole.Icon
|
|
|
324
421
|
}
|
|
325
422
|
/** One item: header (question + icon) and panel (answer). `key` makes every part's recipe key unique. */
|
|
326
423
|
function buildAccordionItem(create, words, itemKey, icon = AccordionIcon.Chevron) {
|
|
327
|
-
const item = make$
|
|
424
|
+
const item = make$3(create, `${itemKey}`, Type$4.Container, AccordionRole.Item, Style$4.Item, {}, 'Item');
|
|
328
425
|
item.itemKey = itemKey;
|
|
329
|
-
const trigger = make$
|
|
330
|
-
const question = make$
|
|
426
|
+
const trigger = make$3(create, `${itemKey}-trigger`, Type$4.LinkedContainer, AccordionRole.Trigger, Style$4.Trigger, {}, 'Header');
|
|
427
|
+
const question = make$3(create, `${itemKey}-question`, Type$4.Heading, null, Style$4.Question, { content: words.question, level: DEFAULT_LEVEL }, 'Heading');
|
|
331
428
|
writeStyles(question, QUESTION_BASE);
|
|
332
429
|
trigger.children = [question, ...(icon === AccordionIcon.None ? [] : [buildCompositeIcon(create, `${itemKey}-icon`, icon, item.id)])];
|
|
333
|
-
const panel = make$
|
|
334
|
-
const answer = make$
|
|
430
|
+
const panel = make$3(create, `${itemKey}-panel`, Type$4.Container, AccordionRole.Panel, Style$4.Panel, {}, 'Panel');
|
|
431
|
+
const answer = make$3(create, `${itemKey}-answer`, Type$4.Text, null, Style$4.Answer, { content: words.answer, tag: 'p' }, 'Text');
|
|
335
432
|
writeStyles(answer, ANSWER_BASE);
|
|
336
433
|
panel.children = [answer];
|
|
337
434
|
item.children = [trigger, panel];
|
|
@@ -339,7 +436,7 @@ function buildAccordionItem(create, words, itemKey, icon = AccordionIcon.Chevron
|
|
|
339
436
|
}
|
|
340
437
|
/** A fresh Accordion: three items, one open at a time, the first open by default (board 4). */
|
|
341
438
|
function buildAccordionRecipe(create, options = {}) {
|
|
342
|
-
const root = make$
|
|
439
|
+
const root = make$3(create, 'accordion', Type$4.Container, AccordionRole.Accordion, Style$4.Root, {}, 'Accordion');
|
|
343
440
|
writeProp(root, CompositeBehaviourTrait.Multiple, options.multiple ?? false);
|
|
344
441
|
writeProp(root, CompositeBehaviourTrait.DefaultOn, options.defaultOn ?? AccordionDefaultOn.First);
|
|
345
442
|
const words = options.items ?? ACCORDION_WORDS;
|
|
@@ -378,7 +475,7 @@ function accordionItemOf(node, path) {
|
|
|
378
475
|
}
|
|
379
476
|
/** True when the root takes its items from content (a Repeater bound to a list, R3). */
|
|
380
477
|
function isAccordionFromContent(root) {
|
|
381
|
-
return !!root && root.type === Type$
|
|
478
|
+
return !!root && root.type === Type$4.Repeater;
|
|
382
479
|
}
|
|
383
480
|
/** The root's items in order — every item (Manual), or the one template item (From content). */
|
|
384
481
|
function accordionItems(root) {
|
|
@@ -402,7 +499,8 @@ function setAccordionBehaviour(root, patch) {
|
|
|
402
499
|
}
|
|
403
500
|
/** The roles of the kind an item belongs to (an accordion item, or a disclosure — its own item). */
|
|
404
501
|
function kindRolesOf(node) {
|
|
405
|
-
|
|
502
|
+
const kind = compositeKindOfRole(node.role);
|
|
503
|
+
return COMPOSITE_KIND_ROLES[isToggleKind(kind) ? kind : CompositeKind.Accordion];
|
|
406
504
|
}
|
|
407
505
|
/** The header of an item (either kind). */
|
|
408
506
|
function accordionTriggerOf(item) {
|
|
@@ -424,9 +522,9 @@ function accordionQuestionOf(item) {
|
|
|
424
522
|
let text;
|
|
425
523
|
if (trigger)
|
|
426
524
|
walkComposite(trigger, n => {
|
|
427
|
-
if (!hit && n.type === Type$
|
|
525
|
+
if (!hit && n.type === Type$4.Heading)
|
|
428
526
|
hit = n;
|
|
429
|
-
if (!text && n.type === Type$
|
|
527
|
+
if (!text && n.type === Type$4.Text)
|
|
430
528
|
text = n;
|
|
431
529
|
});
|
|
432
530
|
return hit ?? text;
|
|
@@ -436,15 +534,15 @@ function accordionAnswerOf(item) {
|
|
|
436
534
|
const panel = accordionPanelOf(item);
|
|
437
535
|
let hit;
|
|
438
536
|
if (panel)
|
|
439
|
-
walkComposite(panel, n => { if (!hit && n.type === Type$
|
|
537
|
+
walkComposite(panel, n => { if (!hit && n.type === Type$4.Text)
|
|
440
538
|
hit = n; });
|
|
441
539
|
return hit;
|
|
442
540
|
}
|
|
443
|
-
const stripTags = (v) => String(v ?? '').replace(/<[^>]*>/g, '').trim();
|
|
541
|
+
const stripTags$1 = (v) => String(v ?? '').replace(/<[^>]*>/g, '').trim();
|
|
444
542
|
/** An item's name in the Items list and the layer tree: its question's words. */
|
|
445
543
|
function accordionItemLabel(item) {
|
|
446
544
|
const q = accordionQuestionOf(item);
|
|
447
|
-
return q ? stripTags(readProp(q, 'content')) : '';
|
|
545
|
+
return q ? stripTags$1(readProp(q, 'content')) : '';
|
|
448
546
|
}
|
|
449
547
|
/** Items ▸ rename: the question's words. */
|
|
450
548
|
function renameAccordionItem(item, question) {
|
|
@@ -457,7 +555,7 @@ function accordionHeadingLevel(trigger) {
|
|
|
457
555
|
const visit = (n) => {
|
|
458
556
|
if (level !== undefined)
|
|
459
557
|
return;
|
|
460
|
-
if (n.type === Type$
|
|
558
|
+
if (n.type === Type$4.Heading) {
|
|
461
559
|
level = readProp(n, 'level');
|
|
462
560
|
return;
|
|
463
561
|
}
|
|
@@ -637,6 +735,7 @@ function rowItemKey(row, index) {
|
|
|
637
735
|
const DOM_ID_PREFIX = {
|
|
638
736
|
[CompositeKind.Accordion]: 'iox-acc',
|
|
639
737
|
[CompositeKind.Disclosure]: 'iox-dis',
|
|
738
|
+
[CompositeKind.Tabs]: 'iox-tab',
|
|
640
739
|
};
|
|
641
740
|
/** The ARIA pairing ids of an item: unique per composite (its SCOPE id — `compositeScopeId`) and per item (its key). */
|
|
642
741
|
function accordionDomIds(rootId, itemKey, kind = CompositeKind.Accordion) {
|
|
@@ -724,14 +823,14 @@ function accordionPanelAttributes(state) {
|
|
|
724
823
|
function serializeDomAttributes(attrs, esc) {
|
|
725
824
|
return Object.entries(attrs).map(([k, v]) => (v === '' ? ` ${k}` : ` ${k}="${esc(v)}"`)).join('');
|
|
726
825
|
}
|
|
727
|
-
const SHORT_TEXT = new Set(['text']);
|
|
728
|
-
const LONG_TEXT = new Set(['textarea', 'richText', 'editor']);
|
|
826
|
+
const SHORT_TEXT$1 = new Set(['text']);
|
|
827
|
+
const LONG_TEXT$1 = new Set(['textarea', 'richText', 'editor']);
|
|
729
828
|
/** R3 (board 9 Q19): switching to From content binds the question to the first short-text field and the
|
|
730
829
|
* answer to the first long-text field. ONE rule for the builder's switch and the composer. */
|
|
731
830
|
function pickAccordionAutoBinds(fields) {
|
|
732
831
|
const isString = (f) => !f.type || f.type === 'string';
|
|
733
|
-
const question = fields.find(f => isString(f) && SHORT_TEXT.has(String(f.controller ?? 'text')))?.name ?? null;
|
|
734
|
-
const answer = fields.find(f => isString(f) && LONG_TEXT.has(String(f.controller ?? '')) && f.name !== question)?.name ?? null;
|
|
832
|
+
const question = fields.find(f => isString(f) && SHORT_TEXT$1.has(String(f.controller ?? 'text')))?.name ?? null;
|
|
833
|
+
const answer = fields.find(f => isString(f) && LONG_TEXT$1.has(String(f.controller ?? '')) && f.name !== question)?.name ?? null;
|
|
735
834
|
return { question, answer };
|
|
736
835
|
}
|
|
737
836
|
/** The bound props of the question and the answer (each a content trait). */
|
|
@@ -811,12 +910,12 @@ function accordionPartStyle(node, name) {
|
|
|
811
910
|
*/
|
|
812
911
|
const ITEM_ON_EVENT = 'iox-item-on';
|
|
813
912
|
const ITEM_OFF_EVENT = 'iox-item-off';
|
|
814
|
-
const KIND_ROOT = (kind) => `[${COMPOSITE_ATTRIBUTE}="${kind}"]`;
|
|
815
|
-
const ROOT_SELECTOR = Object.
|
|
913
|
+
const KIND_ROOT$1 = (kind) => `[${COMPOSITE_ATTRIBUTE}="${kind}"]`;
|
|
914
|
+
const ROOT_SELECTOR$1 = Object.keys(COMPOSITE_KIND_ROLES).map(KIND_ROOT$1).join(', ');
|
|
816
915
|
/** An accordion's items are its `.iox-role-accordion-item` children; a disclosure is its own item. */
|
|
817
916
|
const ACCORDION_ITEM_SELECTOR = '.iox-role-accordion-item';
|
|
818
|
-
const ITEM_SELECTOR = `${ACCORDION_ITEM_SELECTOR}, ${KIND_ROOT(CompositeKind.Disclosure)}`;
|
|
819
|
-
const PANEL_SELECTOR = '.iox-role-accordion-panel, .iox-role-disclosure-panel';
|
|
917
|
+
const ITEM_SELECTOR = `${ACCORDION_ITEM_SELECTOR}, ${KIND_ROOT$1(CompositeKind.Disclosure)}`;
|
|
918
|
+
const PANEL_SELECTOR$1 = '.iox-role-accordion-panel, .iox-role-disclosure-panel';
|
|
820
919
|
const BUTTON_SELECTOR = `.${AccordionClass.Button}`;
|
|
821
920
|
const TRACK_SELECTOR = `.${AccordionClass.Track}`;
|
|
822
921
|
const REGION_SELECTOR = `.${AccordionClass.Region}`;
|
|
@@ -825,16 +924,16 @@ const MAX_CLOSE_MS = 600;
|
|
|
825
924
|
const REDUCED_MOTION = '(prefers-reduced-motion: reduce)';
|
|
826
925
|
/** The composite (accordion or disclosure) an element belongs to (the element itself when it is a root). */
|
|
827
926
|
function closestAccordion(el) {
|
|
828
|
-
return el?.closest?.(ROOT_SELECTOR) ?? null;
|
|
927
|
+
return el?.closest?.(ROOT_SELECTOR$1) ?? null;
|
|
829
928
|
}
|
|
830
929
|
/** The item an element sits in (itself when it is one) — an accordion item, or a disclosure. */
|
|
831
930
|
function closestAccordionItem(el) {
|
|
832
931
|
return el?.closest?.(ITEM_SELECTOR) ?? null;
|
|
833
932
|
}
|
|
834
|
-
const kindOf = (root) => root.getAttribute(COMPOSITE_ATTRIBUTE) === CompositeKind.Disclosure ? CompositeKind.Disclosure : CompositeKind.Accordion;
|
|
933
|
+
const kindOf$1 = (root) => root.getAttribute(COMPOSITE_ATTRIBUTE) === CompositeKind.Disclosure ? CompositeKind.Disclosure : CompositeKind.Accordion;
|
|
835
934
|
/** The composite root whose items an item belongs to: a disclosure is its own root. */
|
|
836
|
-
function
|
|
837
|
-
return kindOf(item) === CompositeKind.Disclosure && item.matches(KIND_ROOT(CompositeKind.Disclosure)) ? item : closestAccordion(item.parentElement);
|
|
935
|
+
function accordionRootOfItem(item) {
|
|
936
|
+
return kindOf$1(item) === CompositeKind.Disclosure && item.matches(KIND_ROOT$1(CompositeKind.Disclosure)) ? item : closestAccordion(item.parentElement);
|
|
838
937
|
}
|
|
839
938
|
/** Subscribe to an item turning on / off — what the `itemOn` / `itemOff` interaction triggers listen to. The
|
|
840
939
|
* item is the element's own item (the element itself when it is one). Returns the unsubscribe. */
|
|
@@ -850,20 +949,20 @@ function onAccordionItemState(el, on, handler) {
|
|
|
850
949
|
function isAccordionItemOn(el) {
|
|
851
950
|
return !!closestAccordionItem(el)?.hasAttribute(ON_ATTRIBUTE);
|
|
852
951
|
}
|
|
853
|
-
const nodeIdOf = (el) => {
|
|
952
|
+
const nodeIdOf$1 = (el) => {
|
|
854
953
|
for (const c of Array.from(el.classList))
|
|
855
954
|
if (c.startsWith('iox-node-'))
|
|
856
955
|
return c.slice('iox-node-'.length);
|
|
857
956
|
return '';
|
|
858
957
|
};
|
|
859
|
-
function behaviourOf(root) {
|
|
958
|
+
function behaviourOf$1(root) {
|
|
860
959
|
const raw = root.getAttribute(DEFAULT_ON_ATTRIBUTE);
|
|
861
960
|
return {
|
|
862
961
|
multiple: root.hasAttribute(MULTIPLE_ATTRIBUTE),
|
|
863
962
|
defaultOn: ACCORDION_DEFAULT_ONS.includes(raw) ? raw : AccordionDefaultOn.First,
|
|
864
963
|
};
|
|
865
964
|
}
|
|
866
|
-
function setAttr(el, name, value) {
|
|
965
|
+
function setAttr$1(el, name, value) {
|
|
867
966
|
if (!el)
|
|
868
967
|
return;
|
|
869
968
|
if (value === null) {
|
|
@@ -918,8 +1017,8 @@ class AccordionRuntime {
|
|
|
918
1017
|
return;
|
|
919
1018
|
this.open(this.keyOf(item, items));
|
|
920
1019
|
};
|
|
921
|
-
this.kind = options.kind ?? kindOf(root);
|
|
922
|
-
this.behaviourValue = options.behaviour ?? behaviourOf(root);
|
|
1020
|
+
this.kind = options.kind ?? kindOf$1(root);
|
|
1021
|
+
this.behaviourValue = options.behaviour ?? behaviourOf$1(root);
|
|
923
1022
|
root.setAttribute(COMPOSITE_ATTRIBUTE, this.kind);
|
|
924
1023
|
RUNTIMES.set(root, this);
|
|
925
1024
|
// The server rendered the first state; with no server state the root's default decides.
|
|
@@ -1052,7 +1151,7 @@ class AccordionRuntime {
|
|
|
1052
1151
|
}
|
|
1053
1152
|
const forced = this.options.forced?.() ?? new Set();
|
|
1054
1153
|
const own = this.options.rootId;
|
|
1055
|
-
const rootId = (typeof own === 'function' ? own() : own) ?? this.root.getAttribute(SCOPE_ATTRIBUTE) ?? nodeIdOf(this.root);
|
|
1154
|
+
const rootId = (typeof own === 'function' ? own() : own) ?? this.root.getAttribute(SCOPE_ATTRIBUTE) ?? nodeIdOf$1(this.root);
|
|
1056
1155
|
const next = new Set();
|
|
1057
1156
|
items.forEach((item, index) => {
|
|
1058
1157
|
const key = this.keyOf(item, items);
|
|
@@ -1069,31 +1168,31 @@ class AccordionRuntime {
|
|
|
1069
1168
|
for (const item of items) {
|
|
1070
1169
|
const key = this.keyOf(item, items);
|
|
1071
1170
|
if (next.has(key) && !was.has(key))
|
|
1072
|
-
dispatch(item, ITEM_ON_EVENT);
|
|
1171
|
+
dispatch$1(item, ITEM_ON_EVENT);
|
|
1073
1172
|
else if (!next.has(key) && was.has(key))
|
|
1074
|
-
dispatch(item, ITEM_OFF_EVENT);
|
|
1173
|
+
dispatch$1(item, ITEM_OFF_EVENT);
|
|
1075
1174
|
}
|
|
1076
1175
|
}
|
|
1077
1176
|
stampItem(item, key, index, on, rootId, first) {
|
|
1078
1177
|
const ids = accordionDomIds(rootId, key, this.kind);
|
|
1079
1178
|
if (!item.hasAttribute(ITEM_KEY_ATTRIBUTE))
|
|
1080
1179
|
item.setAttribute(ITEM_KEY_ATTRIBUTE, key);
|
|
1081
|
-
setAttr(item, ON_ATTRIBUTE, on ? '' : null);
|
|
1180
|
+
setAttr$1(item, ON_ATTRIBUTE, on ? '' : null);
|
|
1082
1181
|
const button = this.own(item, BUTTON_SELECTOR);
|
|
1083
1182
|
const track = this.own(item, TRACK_SELECTOR);
|
|
1084
1183
|
const region = this.own(item, REGION_SELECTOR);
|
|
1085
|
-
const panel = this.own(item, PANEL_SELECTOR);
|
|
1086
|
-
setAttr(button, 'type', 'button');
|
|
1087
|
-
setAttr(button, 'id', ids.trigger);
|
|
1088
|
-
setAttr(button, 'aria-expanded', on ? 'true' : 'false');
|
|
1089
|
-
setAttr(button, 'aria-controls', ids.panel);
|
|
1090
|
-
setAttr(button, ON_ATTRIBUTE, on ? '' : null);
|
|
1091
|
-
setAttr(track, ON_ATTRIBUTE, on ? '' : null);
|
|
1092
|
-
setAttr(panel, ON_ATTRIBUTE, on ? '' : null);
|
|
1184
|
+
const panel = this.own(item, PANEL_SELECTOR$1);
|
|
1185
|
+
setAttr$1(button, 'type', 'button');
|
|
1186
|
+
setAttr$1(button, 'id', ids.trigger);
|
|
1187
|
+
setAttr$1(button, 'aria-expanded', on ? 'true' : 'false');
|
|
1188
|
+
setAttr$1(button, 'aria-controls', ids.panel);
|
|
1189
|
+
setAttr$1(button, ON_ATTRIBUTE, on ? '' : null);
|
|
1190
|
+
setAttr$1(track, ON_ATTRIBUTE, on ? '' : null);
|
|
1191
|
+
setAttr$1(panel, ON_ATTRIBUTE, on ? '' : null);
|
|
1093
1192
|
const labelled = hasLabelledRegion(this.kind);
|
|
1094
|
-
setAttr(region, 'role', labelled ? 'region' : null);
|
|
1095
|
-
setAttr(region, 'id', ids.panel);
|
|
1096
|
-
setAttr(region, 'aria-labelledby', labelled ? ids.trigger : null);
|
|
1193
|
+
setAttr$1(region, 'role', labelled ? 'region' : null);
|
|
1194
|
+
setAttr$1(region, 'id', ids.panel);
|
|
1195
|
+
setAttr$1(region, 'aria-labelledby', labelled ? ids.trigger : null);
|
|
1097
1196
|
if (!region)
|
|
1098
1197
|
return;
|
|
1099
1198
|
const pending = this.closing.get(key);
|
|
@@ -1102,7 +1201,7 @@ class AccordionRuntime {
|
|
|
1102
1201
|
clearTimeout(pending);
|
|
1103
1202
|
this.closing.delete(key);
|
|
1104
1203
|
}
|
|
1105
|
-
setAttr(region, 'hidden', null);
|
|
1204
|
+
setAttr$1(region, 'hidden', null);
|
|
1106
1205
|
return;
|
|
1107
1206
|
}
|
|
1108
1207
|
if (region.hasAttribute('hidden') || pending)
|
|
@@ -1110,13 +1209,13 @@ class AccordionRuntime {
|
|
|
1110
1209
|
// Closing: the track collapses first (its transition), THEN the region is hidden — hiding it at once
|
|
1111
1210
|
// would collapse it with no motion. At first paint, or under reduced motion, it is hidden at once.
|
|
1112
1211
|
if (first || this.reduced()) {
|
|
1113
|
-
setAttr(region, 'hidden', UNTIL_FOUND);
|
|
1212
|
+
setAttr$1(region, 'hidden', UNTIL_FOUND);
|
|
1114
1213
|
return;
|
|
1115
1214
|
}
|
|
1116
1215
|
const hide = () => {
|
|
1117
1216
|
this.closing.delete(key);
|
|
1118
1217
|
if (!item.hasAttribute(ON_ATTRIBUTE))
|
|
1119
|
-
setAttr(region, 'hidden', UNTIL_FOUND);
|
|
1218
|
+
setAttr$1(region, 'hidden', UNTIL_FOUND);
|
|
1120
1219
|
};
|
|
1121
1220
|
this.closing.set(key, setTimeout(hide, this.closeDelay(track)));
|
|
1122
1221
|
}
|
|
@@ -1139,7 +1238,7 @@ class AccordionRuntime {
|
|
|
1139
1238
|
return this.items().map(i => this.own(i, BUTTON_SELECTOR)).filter((b) => !!b);
|
|
1140
1239
|
}
|
|
1141
1240
|
}
|
|
1142
|
-
function dispatch(el, type) {
|
|
1241
|
+
function dispatch$1(el, type) {
|
|
1143
1242
|
const Ctor = globalThis.CustomEvent;
|
|
1144
1243
|
if (Ctor)
|
|
1145
1244
|
el.dispatchEvent(new Ctor(type, { bubbles: false }));
|
|
@@ -1154,7 +1253,7 @@ function attachAccordionAt(root, options = {}) {
|
|
|
1154
1253
|
/** Attach the runtime to every accordion under `container` that has none yet. Returns the new runtimes. */
|
|
1155
1254
|
function attachAccordions(container, options = {}) {
|
|
1156
1255
|
const out = [];
|
|
1157
|
-
container.querySelectorAll(ROOT_SELECTOR).forEach(root => {
|
|
1256
|
+
container.querySelectorAll(ROOT_SELECTOR$1).forEach(root => {
|
|
1158
1257
|
const runtime = attachAccordionAt(root, options);
|
|
1159
1258
|
if (runtime)
|
|
1160
1259
|
out.push(runtime);
|
|
@@ -1168,14 +1267,14 @@ function detachAccordion(runtime) {
|
|
|
1168
1267
|
/** Every live runtime by its root — the canvas's and the site's alike (a runtime registers itself and leaves on
|
|
1169
1268
|
* destroy), so an item action finds the runtime of whatever item it targets. */
|
|
1170
1269
|
const RUNTIMES = new WeakMap();
|
|
1171
|
-
const ROW_INDEX_ATTRIBUTE = 'data-iox-row-idx';
|
|
1270
|
+
const ROW_INDEX_ATTRIBUTE$1 = 'data-iox-row-idx';
|
|
1172
1271
|
/** The repeated rows an element sits in, outermost first — each row's index as its row wrappers stamp it
|
|
1173
1272
|
* (`data-iox-row-idx`: the builder's Repeater rows, the engine's component path). The builder folds them into a
|
|
1174
1273
|
* composite's scope id (`compositeScopeId`, vectoriox/iox-ui#20); the element itself is not counted. */
|
|
1175
1274
|
function compositeRowKeys(el) {
|
|
1176
1275
|
const keys = [];
|
|
1177
|
-
for (let at = el?.parentElement?.closest?.(`[${ROW_INDEX_ATTRIBUTE}]`); at; at = at.parentElement?.closest(`[${ROW_INDEX_ATTRIBUTE}]`)) {
|
|
1178
|
-
keys.unshift(at.getAttribute(ROW_INDEX_ATTRIBUTE) ?? '');
|
|
1276
|
+
for (let at = el?.parentElement?.closest?.(`[${ROW_INDEX_ATTRIBUTE$1}]`); at; at = at.parentElement?.closest(`[${ROW_INDEX_ATTRIBUTE$1}]`)) {
|
|
1277
|
+
keys.unshift(at.getAttribute(ROW_INDEX_ATTRIBUTE$1) ?? '');
|
|
1179
1278
|
}
|
|
1180
1279
|
return keys;
|
|
1181
1280
|
}
|
|
@@ -1191,18 +1290,6 @@ var CompositeItemAction;
|
|
|
1191
1290
|
CompositeItemAction["Close"] = "close";
|
|
1192
1291
|
CompositeItemAction["Toggle"] = "toggle";
|
|
1193
1292
|
})(CompositeItemAction || (CompositeItemAction = {}));
|
|
1194
|
-
/**
|
|
1195
|
-
* Run an item action on the element of an ITEM (an accordion item, or a disclosure) — the `openItem` /
|
|
1196
|
-
* `closeItem` / `toggleItem` interaction actions, on the canvas and on the live site through this one function.
|
|
1197
|
-
* Returns whether the item shows open afterwards, or null when `el` is in no running composite.
|
|
1198
|
-
*/
|
|
1199
|
-
function runCompositeItemAction(el, action) {
|
|
1200
|
-
const item = closestAccordionItem(el);
|
|
1201
|
-
const runtime = item ? accordionRuntimeOf(rootOfItem(item)) : null;
|
|
1202
|
-
if (!item || !runtime)
|
|
1203
|
-
return null;
|
|
1204
|
-
return runtime.run(action, runtime.keyOf(item));
|
|
1205
|
-
}
|
|
1206
1293
|
/** How much of an opened item must already be on screen for the action NOT to scroll to it (its header row). */
|
|
1207
1294
|
const VISIBLE_HEAD_PX = 48;
|
|
1208
1295
|
/** True when an item an action just opened is out of view and the page should scroll to it (T14: "opens that item
|
|
@@ -1219,21 +1306,21 @@ const DISCLOSURE_WORDS = {
|
|
|
1219
1306
|
};
|
|
1220
1307
|
/** A disclosure's one item key — its root IS its item. */
|
|
1221
1308
|
const DISCLOSURE_ITEM_KEY = 'k1';
|
|
1222
|
-
var Type$
|
|
1309
|
+
var Type$3;
|
|
1223
1310
|
(function (Type) {
|
|
1224
1311
|
Type["Container"] = "Container";
|
|
1225
1312
|
Type["LinkedContainer"] = "LinkedContainer";
|
|
1226
1313
|
Type["Text"] = "Text";
|
|
1227
|
-
})(Type$
|
|
1228
|
-
var Style$
|
|
1314
|
+
})(Type$3 || (Type$3 = {}));
|
|
1315
|
+
var Style$3;
|
|
1229
1316
|
(function (Style) {
|
|
1230
1317
|
Style["Root"] = "box";
|
|
1231
1318
|
Style["Trigger"] = "accordion-trigger";
|
|
1232
1319
|
Style["Word"] = "body";
|
|
1233
1320
|
Style["Panel"] = "accordion-panel";
|
|
1234
1321
|
Style["Body"] = "body";
|
|
1235
|
-
})(Style$
|
|
1236
|
-
const ZERO_BOX$
|
|
1322
|
+
})(Style$3 || (Style$3 = {}));
|
|
1323
|
+
const ZERO_BOX$2 = '0px 0px 0px 0px';
|
|
1237
1324
|
const HIDDEN = 'none';
|
|
1238
1325
|
const INHERIT = 'inherit';
|
|
1239
1326
|
/** A word is a flex item of the toggle — `block` (never `inline`: the compiler drops a value equal to the CSS initial,
|
|
@@ -1242,7 +1329,7 @@ const SHOWN = 'block';
|
|
|
1242
1329
|
/** What each part holds — layout only; its LOOK is its element style. The trigger sizes to its words (a button in
|
|
1243
1330
|
* the flow, not a full-width header row — `fit-content` keeps it from stretching) and the panel spans the root. The
|
|
1244
1331
|
* trigger is `flex`, not `inline-flex`: it is a flex item of the root, which blockifies it on the site anyway. */
|
|
1245
|
-
const PART_BASE = {
|
|
1332
|
+
const PART_BASE$1 = {
|
|
1246
1333
|
[DisclosureRole.Disclosure]: { display: 'flex', flexDirection: 'column', width: '100%', minHeight: 'auto', height: 'auto' },
|
|
1247
1334
|
// The toggle reads the text around it (the page's body type, through the root) — a LinkedContainer is born with
|
|
1248
1335
|
// its own literal font, which an accordion's heading masks and a disclosure's words would show.
|
|
@@ -1252,17 +1339,17 @@ const PART_BASE = {
|
|
|
1252
1339
|
},
|
|
1253
1340
|
// A word is just its words: none of a Text's own box (its type's 10px padding, its full width) — the toggle's
|
|
1254
1341
|
// padding is the click target.
|
|
1255
|
-
[DisclosureRole.More]: { display: SHOWN, margin: ZERO_BOX$
|
|
1256
|
-
[DisclosureRole.Less]: { display: HIDDEN, margin: ZERO_BOX$
|
|
1342
|
+
[DisclosureRole.More]: { display: SHOWN, margin: ZERO_BOX$2, padding: ZERO_BOX$2, width: 'auto' },
|
|
1343
|
+
[DisclosureRole.Less]: { display: HIDDEN, margin: ZERO_BOX$2, padding: ZERO_BOX$2, width: 'auto' },
|
|
1257
1344
|
[DisclosureRole.Panel]: { display: 'block', width: '100%', minHeight: 'auto', height: 'auto' },
|
|
1258
1345
|
};
|
|
1259
|
-
const BODY_BASE = { margin: ZERO_BOX$
|
|
1260
|
-
function make$
|
|
1261
|
-
return makeCompositePart(create, { key, type, props, elementStyle: style }, role, label, role ? PART_BASE[role] : undefined);
|
|
1346
|
+
const BODY_BASE$1 = { margin: ZERO_BOX$2, padding: ZERO_BOX$2, minHeight: 'auto' };
|
|
1347
|
+
function make$2(create, key, type, role, style, props, label) {
|
|
1348
|
+
return makeCompositePart(create, { key, type, props, elementStyle: style }, role, label, role ? PART_BASE$1[role] : undefined);
|
|
1262
1349
|
}
|
|
1263
1350
|
/** One of the trigger's two words: shown while the disclosure is closed (`more`) or open (`less`). */
|
|
1264
1351
|
function buildWord(create, role, words, rootId) {
|
|
1265
|
-
const node = make$
|
|
1352
|
+
const node = make$2(create, role === DisclosureRole.More ? 'more' : 'less', Type$3.Text, role, Style$3.Word, { content: words, tag: 'span' }, 'Text');
|
|
1266
1353
|
node.stateStyles = { ...(node.stateStyles ?? {}), [ITEM_OPEN_STATE]: { display: role === DisclosureRole.More ? HIDDEN : SHOWN } };
|
|
1267
1354
|
if (rootId)
|
|
1268
1355
|
node.stateTriggers = { ...(node.stateTriggers ?? {}), [ITEM_OPEN_STATE]: rootId };
|
|
@@ -1271,19 +1358,19 @@ function buildWord(create, role, words, rootId) {
|
|
|
1271
1358
|
/** A fresh Disclosure: "Read more" opens the text below and reads "Read less" while it is open; closed by default. */
|
|
1272
1359
|
function buildDisclosureRecipe(create, options = {}) {
|
|
1273
1360
|
const words = { ...DISCLOSURE_WORDS, ...(options.words ?? {}) };
|
|
1274
|
-
const root = make$
|
|
1361
|
+
const root = make$2(create, 'disclosure', Type$3.Container, DisclosureRole.Disclosure, Style$3.Root, {}, 'Disclosure');
|
|
1275
1362
|
root.itemKey = DISCLOSURE_ITEM_KEY;
|
|
1276
1363
|
writeProp(root, CompositeBehaviourTrait.DefaultOn, options.open ? AccordionDefaultOn.First : AccordionDefaultOn.None);
|
|
1277
1364
|
const icon = options.icon ?? AccordionIcon.Chevron;
|
|
1278
|
-
const trigger = make$
|
|
1365
|
+
const trigger = make$2(create, 'trigger', Type$3.LinkedContainer, DisclosureRole.Trigger, Style$3.Trigger, {}, 'Toggle');
|
|
1279
1366
|
trigger.children = [
|
|
1280
1367
|
buildWord(create, DisclosureRole.More, words.more, root.id),
|
|
1281
1368
|
buildWord(create, DisclosureRole.Less, words.less, root.id),
|
|
1282
1369
|
...(icon === AccordionIcon.None ? [] : [buildCompositeIcon(create, 'icon', icon, root.id, DisclosureRole.Icon)]),
|
|
1283
1370
|
];
|
|
1284
|
-
const panel = make$
|
|
1285
|
-
const body = make$
|
|
1286
|
-
writeStyles(body, BODY_BASE);
|
|
1371
|
+
const panel = make$2(create, 'panel', Type$3.Container, DisclosureRole.Panel, Style$3.Panel, {}, 'Panel');
|
|
1372
|
+
const body = make$2(create, 'body', Type$3.Text, null, Style$3.Body, { content: words.body, tag: 'p' }, 'Text');
|
|
1373
|
+
writeStyles(body, BODY_BASE$1);
|
|
1287
1374
|
panel.children = [body];
|
|
1288
1375
|
root.children = [trigger, panel];
|
|
1289
1376
|
return root;
|
|
@@ -1445,7 +1532,7 @@ const CARD_WORDS = {
|
|
|
1445
1532
|
};
|
|
1446
1533
|
/** The breakpoint whose layer stacks an "Image side" card (Breakpoint.Mobile, ≤767px). */
|
|
1447
1534
|
const CARD_STACK_BREAKPOINT = 'mobile';
|
|
1448
|
-
var Type;
|
|
1535
|
+
var Type$2;
|
|
1449
1536
|
(function (Type) {
|
|
1450
1537
|
Type["Container"] = "Container";
|
|
1451
1538
|
Type["Image"] = "Image";
|
|
@@ -1453,8 +1540,8 @@ var Type;
|
|
|
1453
1540
|
Type["Text"] = "Text";
|
|
1454
1541
|
Type["Icon"] = "Icon";
|
|
1455
1542
|
Type["ButtonBlock"] = "ButtonBlock";
|
|
1456
|
-
})(Type || (Type = {}));
|
|
1457
|
-
var Style;
|
|
1543
|
+
})(Type$2 || (Type$2 = {}));
|
|
1544
|
+
var Style$2;
|
|
1458
1545
|
(function (Style) {
|
|
1459
1546
|
Style["Card"] = "card";
|
|
1460
1547
|
Style["Box"] = "box";
|
|
@@ -1467,7 +1554,7 @@ var Style;
|
|
|
1467
1554
|
Style["Ghost"] = "button-ghost";
|
|
1468
1555
|
Style["Icon"] = "icon";
|
|
1469
1556
|
Style["Scrim"] = "media-scrim";
|
|
1470
|
-
})(Style || (Style = {}));
|
|
1557
|
+
})(Style$2 || (Style$2 = {}));
|
|
1471
1558
|
var Scheme;
|
|
1472
1559
|
(function (Scheme) {
|
|
1473
1560
|
Scheme["Default"] = "default";
|
|
@@ -1477,11 +1564,11 @@ const LINK_TYPE = 'linkType';
|
|
|
1477
1564
|
const SAVE_ICON = 'ph ph-bookmark-simple';
|
|
1478
1565
|
/** A value an element style writes — read from the contract, never retyped. */
|
|
1479
1566
|
const styleValue = (style, trait) => String(elementStyleById(style)?.base[trait] ?? '');
|
|
1480
|
-
const CARD_PAD = styleValue(Style.Card, 'padding');
|
|
1481
|
-
const IMAGE_RADIUS = styleValue(Style.Media, 'borderRadius');
|
|
1482
|
-
const TITLE_SIZE = styleValue(Style.Title, 'fontSize');
|
|
1567
|
+
const CARD_PAD = styleValue(Style$2.Card, 'padding');
|
|
1568
|
+
const IMAGE_RADIUS = styleValue(Style$2.Media, 'borderRadius');
|
|
1569
|
+
const TITLE_SIZE = styleValue(Style$2.Title, 'fontSize');
|
|
1483
1570
|
const LEAD_SIZE = 'var(--iox-size-lead)';
|
|
1484
|
-
const ZERO_BOX = '0px 0px 0px 0px';
|
|
1571
|
+
const ZERO_BOX$1 = '0px 0px 0px 0px';
|
|
1485
1572
|
/** What every card part holds in every layout — the recipe's box. */
|
|
1486
1573
|
const CARD_BASE = {
|
|
1487
1574
|
// No height: a card is a grid or flex item, and the cell's stretch is what makes a row's cards equal.
|
|
@@ -1489,25 +1576,25 @@ const CARD_BASE = {
|
|
|
1489
1576
|
[CardRole.Card]: { display: 'flex', gap: '14px', width: '100%', minWidth: '0px', overflow: 'hidden', position: 'relative' },
|
|
1490
1577
|
[CardRole.Media]: { display: 'block', width: '100%', minHeight: 'auto', flexGrow: '0', flexShrink: '0' },
|
|
1491
1578
|
[CardRole.Content]: { display: 'flex', flexDirection: 'column', gap: '8px', width: '100%', minWidth: '0px', minHeight: 'auto' },
|
|
1492
|
-
[CardRole.Eyebrow]: { margin: ZERO_BOX, padding: ZERO_BOX, minHeight: 'auto' },
|
|
1493
|
-
[CardRole.Title]: { margin: ZERO_BOX },
|
|
1494
|
-
[CardRole.Text]: { margin: ZERO_BOX, padding: ZERO_BOX, minHeight: 'auto' },
|
|
1579
|
+
[CardRole.Eyebrow]: { margin: ZERO_BOX$1, padding: ZERO_BOX$1, minHeight: 'auto' },
|
|
1580
|
+
[CardRole.Title]: { margin: ZERO_BOX$1 },
|
|
1581
|
+
[CardRole.Text]: { margin: ZERO_BOX$1, padding: ZERO_BOX$1, minHeight: 'auto' },
|
|
1495
1582
|
// Buttons pinned to the bottom (board 8 Q: equal heights + buttons pinned): the content column grows,
|
|
1496
1583
|
// and the row's auto top margin takes the slack.
|
|
1497
1584
|
[CardRole.Actions]: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: '8px', margin: 'auto 0px 0px 0px', padding: '6px 0px 0px 0px', width: '100%', minHeight: 'auto' },
|
|
1498
1585
|
};
|
|
1499
|
-
const IMAGE_BASE = { display: 'block', width: '100%', maxWidth: '100%' };
|
|
1586
|
+
const IMAGE_BASE$2 = { display: 'block', width: '100%', maxWidth: '100%' };
|
|
1500
1587
|
/** A button's label and a pill's words: inherit the type and ink of what holds them. */
|
|
1501
|
-
const LABEL_BASE = { width: 'auto', minHeight: 'auto', padding: ZERO_BOX, margin: ZERO_BOX };
|
|
1588
|
+
const LABEL_BASE$1 = { width: 'auto', minHeight: 'auto', padding: ZERO_BOX$1, margin: ZERO_BOX$1 };
|
|
1502
1589
|
const INLINE_ICON = { fontSize: '1em' };
|
|
1503
1590
|
const IN_FLOW = { position: 'relative', top: 'auto', right: 'auto', bottom: 'auto', left: 'auto' };
|
|
1504
|
-
const GROWING_CONTENT = { position: 'static', flexGrow: '1', flexShrink: '1', flexBasis: '0%', padding: ZERO_BOX };
|
|
1591
|
+
const GROWING_CONTENT = { position: 'static', flexGrow: '1', flexShrink: '1', flexBasis: '0%', padding: ZERO_BOX$1 };
|
|
1505
1592
|
const LAYOUTS = {
|
|
1506
1593
|
[CardLayout.Top]: {
|
|
1507
1594
|
card: { flexDirection: 'column', alignItems: 'stretch', justifyContent: 'flex-start', minHeight: 'auto', padding: CARD_PAD },
|
|
1508
1595
|
media: { ...IN_FLOW, flexBasis: 'auto' },
|
|
1509
1596
|
image: { aspectRatio: '4 / 3', height: 'auto', minHeight: 'auto', borderRadius: IMAGE_RADIUS },
|
|
1510
|
-
content: GROWING_CONTENT, contentStyle: Style.Box, contentScheme: Scheme.Default,
|
|
1597
|
+
content: GROWING_CONTENT, contentStyle: Style$2.Box, contentScheme: Scheme.Default,
|
|
1511
1598
|
title: { fontSize: TITLE_SIZE },
|
|
1512
1599
|
badgeOnMedia: true,
|
|
1513
1600
|
},
|
|
@@ -1515,7 +1602,7 @@ const LAYOUTS = {
|
|
|
1515
1602
|
card: { flexDirection: 'row', alignItems: 'stretch', justifyContent: 'flex-start', minHeight: 'auto', padding: CARD_PAD },
|
|
1516
1603
|
media: { ...IN_FLOW, flexBasis: '42%' },
|
|
1517
1604
|
image: { aspectRatio: 'auto', height: '100%', minHeight: '180px', borderRadius: IMAGE_RADIUS },
|
|
1518
|
-
content: GROWING_CONTENT, contentStyle: Style.Box, contentScheme: Scheme.Default,
|
|
1605
|
+
content: GROWING_CONTENT, contentStyle: Style$2.Box, contentScheme: Scheme.Default,
|
|
1519
1606
|
title: { fontSize: TITLE_SIZE },
|
|
1520
1607
|
badgeOnMedia: true,
|
|
1521
1608
|
stack: {
|
|
@@ -1525,11 +1612,11 @@ const LAYOUTS = {
|
|
|
1525
1612
|
},
|
|
1526
1613
|
},
|
|
1527
1614
|
[CardLayout.Overlay]: {
|
|
1528
|
-
card: { flexDirection: 'column', alignItems: 'stretch', justifyContent: 'flex-end', minHeight: '340px', padding: ZERO_BOX },
|
|
1615
|
+
card: { flexDirection: 'column', alignItems: 'stretch', justifyContent: 'flex-end', minHeight: '340px', padding: ZERO_BOX$1 },
|
|
1529
1616
|
media: { position: 'absolute', top: '0px', right: '0px', bottom: '0px', left: '0px', flexBasis: 'auto' },
|
|
1530
1617
|
image: { aspectRatio: 'auto', height: '100%', minHeight: 'auto', borderRadius: '0px' },
|
|
1531
1618
|
content: { position: 'relative', flexGrow: '0', flexShrink: '0', flexBasis: 'auto', padding: `72px ${CARD_PAD} ${CARD_PAD} ${CARD_PAD}` },
|
|
1532
|
-
contentStyle: Style.Scrim, contentScheme: Scheme.Inverse,
|
|
1619
|
+
contentStyle: Style$2.Scrim, contentScheme: Scheme.Inverse,
|
|
1533
1620
|
title: { fontSize: TITLE_SIZE },
|
|
1534
1621
|
badgeOnMedia: false,
|
|
1535
1622
|
},
|
|
@@ -1537,7 +1624,7 @@ const LAYOUTS = {
|
|
|
1537
1624
|
card: { flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-start', minHeight: 'auto', padding: CARD_PAD },
|
|
1538
1625
|
media: { ...IN_FLOW, flexBasis: '72px' },
|
|
1539
1626
|
image: { aspectRatio: '1 / 1', height: 'auto', minHeight: 'auto', borderRadius: IMAGE_RADIUS },
|
|
1540
|
-
content: GROWING_CONTENT, contentStyle: Style.Box, contentScheme: Scheme.Default,
|
|
1627
|
+
content: GROWING_CONTENT, contentStyle: Style$2.Box, contentScheme: Scheme.Default,
|
|
1541
1628
|
title: { fontSize: LEAD_SIZE },
|
|
1542
1629
|
badgeOnMedia: false,
|
|
1543
1630
|
partsOff: [CardPart.Text, CardPart.Actions],
|
|
@@ -1548,7 +1635,7 @@ const STACK_KEYS = {
|
|
|
1548
1635
|
media: Object.keys(LAYOUTS[CardLayout.Side].stack.media),
|
|
1549
1636
|
image: Object.keys(LAYOUTS[CardLayout.Side].stack.image),
|
|
1550
1637
|
};
|
|
1551
|
-
function make(create, key, type, role, elementStyle, props = {}, label) {
|
|
1638
|
+
function make$1(create, key, type, role, elementStyle, props = {}, label) {
|
|
1552
1639
|
const node = create({ key, type, props, elementStyle });
|
|
1553
1640
|
if (role)
|
|
1554
1641
|
node.role = role;
|
|
@@ -1558,16 +1645,16 @@ function make(create, key, type, role, elementStyle, props = {}, label) {
|
|
|
1558
1645
|
return node;
|
|
1559
1646
|
}
|
|
1560
1647
|
function buttonLabel(create, key, words) {
|
|
1561
|
-
const label = make(create, key, Type.Text, null, Style.Label, { content: words, tag: 'span' });
|
|
1562
|
-
writeStyles(label, LABEL_BASE);
|
|
1648
|
+
const label = make$1(create, key, Type$2.Text, null, Style$2.Label, { content: words, tag: 'span' });
|
|
1649
|
+
writeStyles(label, LABEL_BASE$1);
|
|
1563
1650
|
return label;
|
|
1564
1651
|
}
|
|
1565
1652
|
/** Each optional part (and the locked title) as the recipe builds it. */
|
|
1566
1653
|
const PART_BUILDERS = {
|
|
1567
1654
|
[CardPart.Media]: (create) => {
|
|
1568
|
-
const media = make(create, 'media', Type.Container, CardRole.Media, Style.Box, {}, 'Media');
|
|
1569
|
-
const image = make(create, 'image', Type.Image, null, Style.Media, { src: '', alt: '' });
|
|
1570
|
-
writeStyles(image, IMAGE_BASE);
|
|
1655
|
+
const media = make$1(create, 'media', Type$2.Container, CardRole.Media, Style$2.Box, {}, 'Media');
|
|
1656
|
+
const image = make$1(create, 'image', Type$2.Image, null, Style$2.Media, { src: '', alt: '' });
|
|
1657
|
+
writeStyles(image, IMAGE_BASE$2);
|
|
1571
1658
|
media.children = [image];
|
|
1572
1659
|
return media;
|
|
1573
1660
|
},
|
|
@@ -1577,14 +1664,14 @@ const PART_BUILDERS = {
|
|
|
1577
1664
|
badge.label = 'Badge';
|
|
1578
1665
|
return badge;
|
|
1579
1666
|
},
|
|
1580
|
-
[CardPart.Eyebrow]: (create, words) => make(create, 'eyebrow', Type.Text, CardRole.Eyebrow, Style.Eyebrow, { content: words.eyebrow, tag: 'p' }, 'Eyebrow'),
|
|
1581
|
-
[CardPart.Text]: (create, words) => make(create, 'text', Type.Text, CardRole.Text, Style.Body, { content: words.text, tag: 'p' }, 'Text'),
|
|
1667
|
+
[CardPart.Eyebrow]: (create, words) => make$1(create, 'eyebrow', Type$2.Text, CardRole.Eyebrow, Style$2.Eyebrow, { content: words.eyebrow, tag: 'p' }, 'Eyebrow'),
|
|
1668
|
+
[CardPart.Text]: (create, words) => make$1(create, 'text', Type$2.Text, CardRole.Text, Style$2.Body, { content: words.text, tag: 'p' }, 'Text'),
|
|
1582
1669
|
[CardPart.Actions]: (create, words) => {
|
|
1583
|
-
const actions = make(create, 'actions', Type.Container, CardRole.Actions, Style.Box, {}, 'Buttons');
|
|
1584
|
-
const primary = make(create, 'primary', Type.ButtonBlock, null, Style.Primary);
|
|
1670
|
+
const actions = make$1(create, 'actions', Type$2.Container, CardRole.Actions, Style$2.Box, {}, 'Buttons');
|
|
1671
|
+
const primary = make$1(create, 'primary', Type$2.ButtonBlock, null, Style$2.Primary);
|
|
1585
1672
|
primary.children = [buttonLabel(create, 'primary-label', words.primary)];
|
|
1586
|
-
const ghost = make(create, 'secondary', Type.ButtonBlock, null, Style.Ghost);
|
|
1587
|
-
const icon = make(create, 'secondary-icon', Type.Icon, null, Style.Icon, { iconClass: SAVE_ICON });
|
|
1673
|
+
const ghost = make$1(create, 'secondary', Type$2.ButtonBlock, null, Style$2.Ghost);
|
|
1674
|
+
const icon = make$1(create, 'secondary-icon', Type$2.Icon, null, Style$2.Icon, { iconClass: SAVE_ICON });
|
|
1588
1675
|
writeStyles(icon, INLINE_ICON);
|
|
1589
1676
|
ghost.children = [icon, buttonLabel(create, 'secondary-label', words.secondary)];
|
|
1590
1677
|
actions.children = [primary, ghost];
|
|
@@ -1592,15 +1679,15 @@ const PART_BUILDERS = {
|
|
|
1592
1679
|
},
|
|
1593
1680
|
};
|
|
1594
1681
|
function buildTitle(create, words) {
|
|
1595
|
-
return make(create, 'title', Type.Heading, CardRole.Title, Style.Title, { content: words.title, level: 'h3', [LINK_TYPE]: LinkType.None }, 'Title');
|
|
1682
|
+
return make$1(create, 'title', Type$2.Heading, CardRole.Title, Style$2.Title, { content: words.title, level: 'h3', [LINK_TYPE]: LinkType.None }, 'Title');
|
|
1596
1683
|
}
|
|
1597
1684
|
/** A fresh Card v2: every part a node with its role and element style, laid out by `options.layout`. */
|
|
1598
1685
|
function buildCardRecipe(create, options = {}) {
|
|
1599
1686
|
const words = { ...CARD_WORDS, ...(options.words ?? {}) };
|
|
1600
1687
|
const tone = options.badgeTone ?? BadgeTone.Accent;
|
|
1601
1688
|
const on = (part) => options.parts?.[part] !== false;
|
|
1602
|
-
const card = make(create, 'card', Type.Container, CardRole.Card, Style.Card, {}, 'Card');
|
|
1603
|
-
const content = make(create, 'content', Type.Container, CardRole.Content, Style.Box, {}, 'Content');
|
|
1689
|
+
const card = make$1(create, 'card', Type$2.Container, CardRole.Card, Style$2.Card, {}, 'Card');
|
|
1690
|
+
const content = make$1(create, 'content', Type$2.Container, CardRole.Content, Style$2.Box, {}, 'Content');
|
|
1604
1691
|
content.children = [
|
|
1605
1692
|
...(on(CardPart.Eyebrow) ? [PART_BUILDERS[CardPart.Eyebrow](create, words, tone)] : []),
|
|
1606
1693
|
buildTitle(create, words),
|
|
@@ -1631,7 +1718,7 @@ function cardRootOf(node, path) {
|
|
|
1631
1718
|
function cardPartsOf(card) {
|
|
1632
1719
|
return Object.fromEntries(CARD_PARTS.map(part => [part, !!findByRole(card, CARD_PART_ROLE[part])]));
|
|
1633
1720
|
}
|
|
1634
|
-
const imageOf = (media) => media?.children?.find(c => c.type === Type.Image);
|
|
1721
|
+
const imageOf = (media) => media?.children?.find(c => c.type === Type$2.Image);
|
|
1635
1722
|
/** Put the badge where the layout says: the Media's top corner, or the head of the content column
|
|
1636
1723
|
* (Image behind text, Compact, or no Media at all). The corner's insets are logical, in iox-design.css
|
|
1637
1724
|
* (mirrored in RTL); `position` is the node's own, because the editing canvas forces every node's
|
|
@@ -1663,7 +1750,7 @@ function applyPartLayout(card, spec, part, baseline) {
|
|
|
1663
1750
|
setStack(part, spec.stack?.media, STACK_KEYS.media);
|
|
1664
1751
|
const image = imageOf(part);
|
|
1665
1752
|
if (image) {
|
|
1666
|
-
writeStyles(image, { ...IMAGE_BASE, ...spec.image });
|
|
1753
|
+
writeStyles(image, { ...IMAGE_BASE$2, ...spec.image });
|
|
1667
1754
|
setStack(image, spec.stack?.image, STACK_KEYS.image);
|
|
1668
1755
|
}
|
|
1669
1756
|
return;
|
|
@@ -1835,6 +1922,317 @@ function setCardLinked(card, on) {
|
|
|
1835
1922
|
return writeProp(title, LINK_TYPE, next);
|
|
1836
1923
|
}
|
|
1837
1924
|
|
|
1925
|
+
/**
|
|
1926
|
+
* THE ACCESSIBLE CAROUSEL (CC3 — complex-components plan §7c, board 6; board 9 Q13). The Slider's controls are no
|
|
1927
|
+
* longer drawn by Swiper: Previous, Next, Pause and Pagination are REAL nodes with roles — selectable on the canvas,
|
|
1928
|
+
* in the layer tree and the breadcrumb, and styled on their own (R2) — handed to Swiper as its navigation and
|
|
1929
|
+
* pagination elements.
|
|
1930
|
+
*
|
|
1931
|
+
* SliderContainer (.swiper)
|
|
1932
|
+
* ├─ ButtonBlock `button-secondary` role slider.pause FIRST focus stop; present while Autoplay is on, locked
|
|
1933
|
+
* ├─ ButtonBlock `button-secondary` role slider.prev › Icon caret (points the reading way in RTL)
|
|
1934
|
+
* ├─ ButtonBlock `button-secondary` role slider.next › Icon caret
|
|
1935
|
+
* ├─ (the slides — `.swiper-wrapper` › SliderSlide …)
|
|
1936
|
+
* └─ Container role slider.pagination Swiper draws the dots (or "3 / 6") inside it
|
|
1937
|
+
*
|
|
1938
|
+
* The parts render INSIDE the `.swiper` element, never inside `.swiper-wrapper` (slider.md: a `.swiper-slide` must be
|
|
1939
|
+
* the wrapper's direct child): the parts that come before the first slide in the tree render before the wrapper, the
|
|
1940
|
+
* rest after it. By default they sit over the slides like Swiper's own controls (the shared CSS, iox-design.css, at
|
|
1941
|
+
* zero specificity — a part's own styles win), so a saved slider migrated from Swiper-drawn controls keeps its look.
|
|
1942
|
+
*
|
|
1943
|
+
* The Bindings switches stay the source of truth: Navigation arrows ⇔ Previous + Next, Pagination ⇔ Pagination,
|
|
1944
|
+
* Autoplay ⇔ Pause (WCAG 2.2.2 — "added while Autoplay is on, cannot be removed"). `syncSliderParts` is the one edit.
|
|
1945
|
+
*/
|
|
1946
|
+
var SliderRole;
|
|
1947
|
+
(function (SliderRole) {
|
|
1948
|
+
SliderRole["Pause"] = "slider.pause";
|
|
1949
|
+
SliderRole["Prev"] = "slider.prev";
|
|
1950
|
+
SliderRole["Next"] = "slider.next";
|
|
1951
|
+
SliderRole["Pagination"] = "slider.pagination";
|
|
1952
|
+
})(SliderRole || (SliderRole = {}));
|
|
1953
|
+
/** Parts in the order they are stamped: the pause control first (APG: the first focus stop), the dots last. */
|
|
1954
|
+
const SLIDER_PART_ROLES = [SliderRole.Pause, SliderRole.Prev, SliderRole.Next, SliderRole.Pagination];
|
|
1955
|
+
const SLIDER_TYPE = 'SliderContainer';
|
|
1956
|
+
const SLIDE_TYPE = 'SliderSlide';
|
|
1957
|
+
/** The Bindings switches that own the parts. */
|
|
1958
|
+
var SliderFlag;
|
|
1959
|
+
(function (SliderFlag) {
|
|
1960
|
+
SliderFlag["Navigation"] = "enableNavigation";
|
|
1961
|
+
SliderFlag["Pagination"] = "enablePagination";
|
|
1962
|
+
SliderFlag["Autoplay"] = "enableAutoplay";
|
|
1963
|
+
})(SliderFlag || (SliderFlag = {}));
|
|
1964
|
+
const FLAG_PARTS = [
|
|
1965
|
+
[SliderFlag.Autoplay, [SliderRole.Pause]],
|
|
1966
|
+
[SliderFlag.Navigation, [SliderRole.Prev, SliderRole.Next]],
|
|
1967
|
+
[SliderFlag.Pagination, [SliderRole.Pagination]],
|
|
1968
|
+
];
|
|
1969
|
+
/** The trait that names the carousel (Slider ▸ Accessibility ▸ Carousel name) — the A11y trait every named part uses. */
|
|
1970
|
+
const SLIDER_NAME_TRAIT = 'ariaLabel';
|
|
1971
|
+
function isSliderPart(node) {
|
|
1972
|
+
return !!node?.role && SLIDER_PART_ROLES.includes(node.role);
|
|
1973
|
+
}
|
|
1974
|
+
function isSlider(node) {
|
|
1975
|
+
return node?.type === SLIDER_TYPE;
|
|
1976
|
+
}
|
|
1977
|
+
/** The slider's own part playing `role` (a direct child), or undefined. */
|
|
1978
|
+
function sliderPart(slider, role) {
|
|
1979
|
+
return (slider.children ?? []).find(c => c?.role === role);
|
|
1980
|
+
}
|
|
1981
|
+
/** True when the slider's controls are parts (it was created or migrated in CC3) — Swiper then draws none of its own. */
|
|
1982
|
+
function hasSliderParts(slider) {
|
|
1983
|
+
return (slider.children ?? []).some(isSliderPart);
|
|
1984
|
+
}
|
|
1985
|
+
/** A slider's children split as they render: parts before the first slide, the slides (into `.swiper-wrapper`), and
|
|
1986
|
+
* the parts after it. */
|
|
1987
|
+
function splitSliderChildren(children) {
|
|
1988
|
+
const lead = [];
|
|
1989
|
+
const slides = [];
|
|
1990
|
+
const trail = [];
|
|
1991
|
+
for (const child of children ?? []) {
|
|
1992
|
+
if (!isSliderPart(child))
|
|
1993
|
+
slides.push(child);
|
|
1994
|
+
else
|
|
1995
|
+
(slides.length ? trail : lead).push(child);
|
|
1996
|
+
}
|
|
1997
|
+
return { lead, slides, trail };
|
|
1998
|
+
}
|
|
1999
|
+
/** The slides only (the template slide of a bound slider is `sliderSlides(children)[0]`). */
|
|
2000
|
+
function sliderSlides(children) {
|
|
2001
|
+
return (children ?? []).filter(c => !isSliderPart(c));
|
|
2002
|
+
}
|
|
2003
|
+
// ── the parts' recipe ───────────────────────────────────────────────────────────────────────────────
|
|
2004
|
+
var Type$1;
|
|
2005
|
+
(function (Type) {
|
|
2006
|
+
Type["ButtonBlock"] = "ButtonBlock";
|
|
2007
|
+
Type["Icon"] = "Icon";
|
|
2008
|
+
Type["Container"] = "Container";
|
|
2009
|
+
})(Type$1 || (Type$1 = {}));
|
|
2010
|
+
var Style$1;
|
|
2011
|
+
(function (Style) {
|
|
2012
|
+
Style["Control"] = "button-secondary";
|
|
2013
|
+
Style["Icon"] = "icon";
|
|
2014
|
+
})(Style$1 || (Style$1 = {}));
|
|
2015
|
+
/** Each part's glyph (Phosphor) and its default accessible name. */
|
|
2016
|
+
const SLIDER_PART_GLYPH = {
|
|
2017
|
+
[SliderRole.Pause]: 'ph ph-pause',
|
|
2018
|
+
[SliderRole.Prev]: 'ph ph-caret-left',
|
|
2019
|
+
[SliderRole.Next]: 'ph ph-caret-right',
|
|
2020
|
+
};
|
|
2021
|
+
const SLIDER_PLAY_GLYPH = 'ph-fill ph-play';
|
|
2022
|
+
const PART_LABEL = {
|
|
2023
|
+
[SliderRole.Pause]: 'Pause',
|
|
2024
|
+
[SliderRole.Prev]: 'Previous',
|
|
2025
|
+
[SliderRole.Next]: 'Next',
|
|
2026
|
+
[SliderRole.Pagination]: 'Pagination',
|
|
2027
|
+
};
|
|
2028
|
+
const ZERO$1 = '0px 0px 0px 0px';
|
|
2029
|
+
/** A round 44px control (board 6 `.st-ctl`), over the slides (z above `.swiper-wrapper`). The PLACE (which edge) is the
|
|
2030
|
+
* shared CSS's, in logical insets, so a carousel reads the same way in RTL. */
|
|
2031
|
+
const CONTROL_BASE = {
|
|
2032
|
+
position: 'absolute', zIndex: '10', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
2033
|
+
width: '44px', height: '44px', minWidth: '44px', padding: ZERO$1, margin: ZERO$1, borderRadius: '999px',
|
|
2034
|
+
};
|
|
2035
|
+
const ICON_BASE = { fontSize: '20px', color: 'inherit' };
|
|
2036
|
+
/** The arrows' vertical middle over the slides: half the 44px control above the centre line. */
|
|
2037
|
+
const ARROW_TOP = 'calc(50% - 22px)';
|
|
2038
|
+
const PAGINATION_BASE = {
|
|
2039
|
+
position: 'absolute', zIndex: '10', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px',
|
|
2040
|
+
width: 'auto', minHeight: 'auto', height: 'auto', padding: ZERO$1, margin: ZERO$1, color: 'var(--iox-accent)',
|
|
2041
|
+
};
|
|
2042
|
+
const SWIPER_THEME_COLOR = '#007aff';
|
|
2043
|
+
function legacyLookOf(slider) {
|
|
2044
|
+
const color = readProp(slider, 'navigationColor');
|
|
2045
|
+
const size = Number(readProp(slider, 'navigationSize'));
|
|
2046
|
+
const dots = readProp(slider, 'paginationColor');
|
|
2047
|
+
return {
|
|
2048
|
+
navigationColor: typeof color === 'string' && color ? color : SWIPER_THEME_COLOR,
|
|
2049
|
+
navigationSize: Number.isFinite(size) && size > 0 ? size : 44,
|
|
2050
|
+
paginationColor: typeof dots === 'string' && dots ? dots : SWIPER_THEME_COLOR,
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
2053
|
+
/** One part, from the recipe (board 6: `button-secondary` controls, token dots) or in a migrated slider's legacy look. */
|
|
2054
|
+
function buildSliderPart(create, role, legacy) {
|
|
2055
|
+
if (role === SliderRole.Pagination) {
|
|
2056
|
+
const dots = makeCompositePart(create, { key: 'pagination', type: Type$1.Container, props: {}, elementStyle: NO_ELEMENT_STYLE }, role, PART_LABEL[role], legacy?.paginationColor ? { ...PAGINATION_BASE, color: legacy.paginationColor } : PAGINATION_BASE);
|
|
2057
|
+
dots.children = [];
|
|
2058
|
+
return dots;
|
|
2059
|
+
}
|
|
2060
|
+
const key = role.split('.')[1];
|
|
2061
|
+
// Previous / Next sit at the vertical middle (their own `top`, so a part set back in flow drops it); the inline edge
|
|
2062
|
+
// is the shared CSS's, logical, so the arrows swap sides in RTL.
|
|
2063
|
+
const placed = role === SliderRole.Pause ? CONTROL_BASE : { ...CONTROL_BASE, top: ARROW_TOP };
|
|
2064
|
+
const base = legacy
|
|
2065
|
+
? { ...placed, backgroundColor: 'transparent', borderWidth: ZERO$1, color: legacy.navigationColor ?? SWIPER_THEME_COLOR, boxShadow: 'none' }
|
|
2066
|
+
: placed;
|
|
2067
|
+
// Previous / Next carry their accessible name (Swiper's A11y module reads it); the pause control's name is its
|
|
2068
|
+
// runtime's — it says what pressing it will do.
|
|
2069
|
+
const props = { tag: 'button', type: 'button' };
|
|
2070
|
+
if (role !== SliderRole.Pause)
|
|
2071
|
+
props[SLIDER_NAME_TRAIT] = role === SliderRole.Prev ? SLIDER_WORDS.en.prev : SLIDER_WORDS.en.next;
|
|
2072
|
+
const button = makeCompositePart(create, { key, type: Type$1.ButtonBlock, props, elementStyle: legacy ? NO_ELEMENT_STYLE : Style$1.Control }, role, PART_LABEL[role], base);
|
|
2073
|
+
const iconBase = legacy ? { ...ICON_BASE, fontSize: `${Math.round((legacy.navigationSize ?? 44) * 0.6)}px` } : ICON_BASE;
|
|
2074
|
+
const icon = makeCompositePart(create, { key: `${key}-icon`, type: Type$1.Icon, props: { iconClass: SLIDER_PART_GLYPH[role] }, elementStyle: legacy ? NO_ELEMENT_STYLE : Style$1.Icon }, null, 'Icon', iconBase);
|
|
2075
|
+
button.children = [icon];
|
|
2076
|
+
return button;
|
|
2077
|
+
}
|
|
2078
|
+
const truthy = (v) => v === true || v === 'true';
|
|
2079
|
+
/** The parts the slider's switches call for, in stamping order. */
|
|
2080
|
+
function wantedSliderParts(slider) {
|
|
2081
|
+
return FLAG_PARTS.filter(([flag]) => truthy(readProp(slider, flag))).flatMap(([, roles]) => roles);
|
|
2082
|
+
}
|
|
2083
|
+
/**
|
|
2084
|
+
* Make the slider's parts match its switches — ONE edit (a palette switch, the migration, the composer). Missing
|
|
2085
|
+
* parts are stamped from the recipe (or, `legacy`, in the migrated look) at their place: the pause control, Previous
|
|
2086
|
+
* and Next before the first slide, the pagination after the last; parts whose switch is off are removed. Everything
|
|
2087
|
+
* else (the slides, the author's edits to an existing part) is kept.
|
|
2088
|
+
*/
|
|
2089
|
+
function syncSliderParts(slider, create, legacy) {
|
|
2090
|
+
const wanted = new Set(wantedSliderParts(slider));
|
|
2091
|
+
const children = [...(slider.children ?? [])];
|
|
2092
|
+
const removed = children.filter(c => isSliderPart(c) && !wanted.has(c.role));
|
|
2093
|
+
const kept = children.filter(c => !removed.includes(c));
|
|
2094
|
+
const added = [];
|
|
2095
|
+
for (const role of SLIDER_PART_ROLES) {
|
|
2096
|
+
if (!wanted.has(role) || kept.some(c => c.role === role))
|
|
2097
|
+
continue;
|
|
2098
|
+
const part = buildSliderPart(create, role, legacy);
|
|
2099
|
+
added.push(part);
|
|
2100
|
+
if (role === SliderRole.Pagination) {
|
|
2101
|
+
kept.push(part);
|
|
2102
|
+
}
|
|
2103
|
+
else {
|
|
2104
|
+
// Before the first slide, after the parts that come before it in stamping order.
|
|
2105
|
+
const order = SLIDER_PART_ROLES.indexOf(role);
|
|
2106
|
+
const firstSlide = kept.findIndex(c => !isSliderPart(c));
|
|
2107
|
+
let at = firstSlide < 0 ? kept.length : firstSlide;
|
|
2108
|
+
for (let i = 0; i < kept.length; i++) {
|
|
2109
|
+
const r = kept[i].role;
|
|
2110
|
+
if (isSliderPart(kept[i]) && SLIDER_PART_ROLES.indexOf(r) > order && i < at) {
|
|
2111
|
+
at = i;
|
|
2112
|
+
break;
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
kept.splice(at, 0, part);
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
if (added.length || removed.length)
|
|
2119
|
+
slider.children = kept;
|
|
2120
|
+
return { added, removed };
|
|
2121
|
+
}
|
|
2122
|
+
/** True when a saved slider still relies on Swiper-drawn controls: a switch is on and no part exists yet. */
|
|
2123
|
+
function needsSliderMigration(slider) {
|
|
2124
|
+
return !!slider && isSlider(slider) && !hasSliderParts(slider) && wantedSliderParts(slider).length > 0;
|
|
2125
|
+
}
|
|
2126
|
+
/**
|
|
2127
|
+
* The migration of a saved slider (board 9 Q13): its Swiper-drawn arrows, dots — and, when Autoplay is on, the pause
|
|
2128
|
+
* control WCAG 2.2.2 requires — become parts, in the place and the colours Swiper drew them. Idempotent: a slider
|
|
2129
|
+
* that already has parts is left alone. True when it changed.
|
|
2130
|
+
*/
|
|
2131
|
+
function migrateSliderControls(slider, create) {
|
|
2132
|
+
if (!needsSliderMigration(slider))
|
|
2133
|
+
return false;
|
|
2134
|
+
const change = syncSliderParts(slider, create, legacyLookOf(slider));
|
|
2135
|
+
return change.added.length > 0;
|
|
2136
|
+
}
|
|
2137
|
+
/**
|
|
2138
|
+
* A switch flipped in the panel: on a slider still on Swiper-drawn controls, migrate it first (so its other controls
|
|
2139
|
+
* keep their look), then stamp / remove the parts the new switch calls for. ONE edit.
|
|
2140
|
+
*/
|
|
2141
|
+
function setSliderFlag(slider, flag, on, create) {
|
|
2142
|
+
const wasLegacy = !hasSliderParts(slider);
|
|
2143
|
+
const before = wantedSliderParts(slider);
|
|
2144
|
+
const migrated = [];
|
|
2145
|
+
if (wasLegacy && before.length) {
|
|
2146
|
+
syncSliderParts(slider, create, legacyLookOf(slider)).added.forEach(p => migrated.push(p));
|
|
2147
|
+
}
|
|
2148
|
+
writeProp(slider, flag, on);
|
|
2149
|
+
const change = syncSliderParts(slider, create);
|
|
2150
|
+
return { added: [...migrated, ...change.added], removed: change.removed };
|
|
2151
|
+
}
|
|
2152
|
+
const SLIDER_WORDS = {
|
|
2153
|
+
en: {
|
|
2154
|
+
stop: 'Stop automatic slide show', start: 'Start automatic slide show', prev: 'Previous slide', next: 'Next slide',
|
|
2155
|
+
carousel: 'carousel', slide: 'slide', slideLabel: '{{index}} of {{slidesLength}}', goTo: 'Go to slide {{index}}',
|
|
2156
|
+
},
|
|
2157
|
+
he: {
|
|
2158
|
+
stop: 'עצירת המצגת האוטומטית', start: 'הפעלת המצגת האוטומטית', prev: 'השקופית הקודמת', next: 'השקופית הבאה',
|
|
2159
|
+
carousel: 'קרוסלה', slide: 'שקופית', slideLabel: '{{index}} מתוך {{slidesLength}}', goTo: 'מעבר לשקופית {{index}}',
|
|
2160
|
+
},
|
|
2161
|
+
};
|
|
2162
|
+
function sliderWords(lang) {
|
|
2163
|
+
return (lang ?? '').toLowerCase().startsWith('he') ? SLIDER_WORDS.he : SLIDER_WORDS.en;
|
|
2164
|
+
}
|
|
2165
|
+
/** The wrapper's live region (APG): silent while the slides move on their own, polite otherwise. */
|
|
2166
|
+
function sliderLiveRegion(rotating) {
|
|
2167
|
+
return rotating ? 'off' : 'polite';
|
|
2168
|
+
}
|
|
2169
|
+
/**
|
|
2170
|
+
* The pause control's state (APG carousel): its NAME says what it will do ("Stop automatic slide show" while it
|
|
2171
|
+
* rotates, "Start automatic slide show" when stopped). An author who named the control keeps the name, and the
|
|
2172
|
+
* state is `aria-pressed` instead (pressed = stopped).
|
|
2173
|
+
*/
|
|
2174
|
+
function sliderPauseAttributes(rotating, words = SLIDER_WORDS.en, authorName) {
|
|
2175
|
+
const name = (authorName ?? '').trim();
|
|
2176
|
+
return name
|
|
2177
|
+
? { 'aria-label': name, 'aria-pressed': rotating ? 'false' : 'true' }
|
|
2178
|
+
: { 'aria-label': rotating ? words.stop : words.start };
|
|
2179
|
+
}
|
|
2180
|
+
/** The pause control's glyph: pause while it rotates, play when stopped. */
|
|
2181
|
+
function sliderPauseGlyph(rotating) {
|
|
2182
|
+
return rotating ? SLIDER_PART_GLYPH[SliderRole.Pause] : SLIDER_PLAY_GLYPH;
|
|
2183
|
+
}
|
|
2184
|
+
/** Board 6's sample quotes — PLACEHOLDERS: testimonials are facts the studio gives, never invented. */
|
|
2185
|
+
const CAROUSEL_QUOTES = [
|
|
2186
|
+
{ quote: 'A sample quote, two lines long, in the client’s own words.', who: '[Client name] · [Project, city]' },
|
|
2187
|
+
{ quote: 'Testimonials are facts: replace these with words a client actually said.', who: '[Client name] · [Project, city]' },
|
|
2188
|
+
{ quote: 'A third sample quote so the rotation has somewhere to go.', who: '[Client name] · [Project, city]' },
|
|
2189
|
+
];
|
|
2190
|
+
/** A quote card. The carousel's controls sit in a ROW above the slides and the dots below them (board 6), so the
|
|
2191
|
+
* card needs no room kept for an overlay. */
|
|
2192
|
+
const QUOTE_SLIDE_BASE = {
|
|
2193
|
+
width: '100%', aspectRatio: 'auto', minHeight: 'auto', height: 'auto', display: 'flex', flexDirection: 'column', gap: '12px', padding: '32px 32px 32px 32px',
|
|
2194
|
+
};
|
|
2195
|
+
/** Board 6's carousel: the pause control, Previous and Next in one row above the slides (in flow — they read in the
|
|
2196
|
+
* page's direction), the dots under them. */
|
|
2197
|
+
const ROW_CONTROL_BASE = { position: 'static', display: 'inline-flex', margin: '0px 4px 12px 4px' };
|
|
2198
|
+
const ROW_DOTS_BASE = { position: 'static', margin: '12px 0px 0px 0px' };
|
|
2199
|
+
const QUOTE_TEXT_BASE = { margin: ZERO$1, padding: ZERO$1, minHeight: 'auto', width: 'auto' };
|
|
2200
|
+
/**
|
|
2201
|
+
* The testimonial carousel (board 6): a named Slider, one quote per slide (each a `card`), rotating every 5s with its
|
|
2202
|
+
* control parts — the pause control first, Previous · Next, the dots. ONE recipe for the composer and the builder.
|
|
2203
|
+
*/
|
|
2204
|
+
function buildCarouselRecipe(create, options = {}) {
|
|
2205
|
+
const slider = makeCompositePart(create, { key: 'carousel', type: SLIDER_TYPE, props: {}, elementStyle: NO_ELEMENT_STYLE }, null, 'Carousel', { width: '100%' });
|
|
2206
|
+
for (const [name, value] of Object.entries({
|
|
2207
|
+
// Rewind: Previous and Next are always there to press (board 6) — neither greys out at an end.
|
|
2208
|
+
slidesPerView: 1, spaceBetween: 0, loop: 'rewind', enableNavigation: true, enablePagination: true, enableAutoplay: true, autoplayDelay: 5000,
|
|
2209
|
+
autoplayPauseOnMouseEnter: true, autoHeight: true, [SLIDER_NAME_TRAIT]: options.name ?? 'What clients say',
|
|
2210
|
+
}))
|
|
2211
|
+
writeProp(slider, name, value);
|
|
2212
|
+
slider.children = (options.quotes ?? CAROUSEL_QUOTES).map((q, i) => {
|
|
2213
|
+
const slide = makeCompositePart(create, { key: `slide-${i + 1}`, type: SLIDE_TYPE, props: {}, elementStyle: 'card' }, null, 'Slide', QUOTE_SLIDE_BASE);
|
|
2214
|
+
slide.children = [
|
|
2215
|
+
makeCompositePart(create, { key: `quote-${i + 1}`, type: 'Text', props: { content: q.quote, tag: 'p' }, elementStyle: 'lead' }, null, 'Quote', QUOTE_TEXT_BASE),
|
|
2216
|
+
makeCompositePart(create, { key: `who-${i + 1}`, type: 'Text', props: { content: q.who, tag: 'p' }, elementStyle: 'small' }, null, 'Client', QUOTE_TEXT_BASE),
|
|
2217
|
+
];
|
|
2218
|
+
return slide;
|
|
2219
|
+
});
|
|
2220
|
+
syncSliderParts(slider, create);
|
|
2221
|
+
for (const part of slider.children ?? []) {
|
|
2222
|
+
if (part.role === SliderRole.Pagination)
|
|
2223
|
+
writeStyles(part, ROW_DOTS_BASE);
|
|
2224
|
+
else if (isSliderPart(part))
|
|
2225
|
+
writeStyles(part, ROW_CONTROL_BASE);
|
|
2226
|
+
}
|
|
2227
|
+
return slider;
|
|
2228
|
+
}
|
|
2229
|
+
/** The slide a lightbox opens at for the clicked row (clamped to the slides there are). */
|
|
2230
|
+
function slideIndexForRow(rowIndex, slideCount) {
|
|
2231
|
+
if (!Number.isFinite(rowIndex) || rowIndex < 0 || slideCount <= 0)
|
|
2232
|
+
return 0;
|
|
2233
|
+
return Math.min(Math.floor(rowIndex), slideCount - 1);
|
|
2234
|
+
}
|
|
2235
|
+
|
|
1838
2236
|
const COMPOSITE_ROLE_SPECS = {
|
|
1839
2237
|
[CardRole.Card]: { parent: null },
|
|
1840
2238
|
[CardRole.Media]: { parent: CardRole.Card, part: CardPart.Media },
|
|
@@ -1857,6 +2255,21 @@ const COMPOSITE_ROLE_SPECS = {
|
|
|
1857
2255
|
[DisclosureRole.More]: { parent: DisclosureRole.Trigger },
|
|
1858
2256
|
[DisclosureRole.Less]: { parent: DisclosureRole.Trigger },
|
|
1859
2257
|
[DisclosureRole.Icon]: { parent: DisclosureRole.Trigger },
|
|
2258
|
+
// CC4a — a Tabs always keeps its tab list and its panels holder; a tab lives in the list and a panel in the
|
|
2259
|
+
// holder (each may be deleted — with its pair — but never moved out); the list takes only tabs, the holder only
|
|
2260
|
+
// panels, and a panel takes anything.
|
|
2261
|
+
[TabsRole.Tabs]: { parent: null },
|
|
2262
|
+
[TabsRole.List]: { parent: TabsRole.Tabs, locked: true, accepts: [TabsRole.Trigger] },
|
|
2263
|
+
[TabsRole.Panels]: { parent: TabsRole.Tabs, locked: true, accepts: [TabsRole.Panel] },
|
|
2264
|
+
[TabsRole.Trigger]: { parent: TabsRole.List, pinned: true },
|
|
2265
|
+
[TabsRole.Panel]: { parent: TabsRole.Panels, pinned: true },
|
|
2266
|
+
// CC3 — a Slider's controls are its own direct children (Swiper is handed them). The pause control stays while
|
|
2267
|
+
// Autoplay is on (WCAG 2.2.2): the Autoplay switch removes it, never Delete. The arrows and the dots follow
|
|
2268
|
+
// their switches and may be deleted.
|
|
2269
|
+
[SliderRole.Pause]: { parent: null, parentType: SLIDER_TYPE, locked: true },
|
|
2270
|
+
[SliderRole.Prev]: { parent: null, parentType: SLIDER_TYPE, pinned: true },
|
|
2271
|
+
[SliderRole.Next]: { parent: null, parentType: SLIDER_TYPE, pinned: true },
|
|
2272
|
+
[SliderRole.Pagination]: { parent: null, parentType: SLIDER_TYPE, pinned: true },
|
|
1860
2273
|
};
|
|
1861
2274
|
var StructureEdit;
|
|
1862
2275
|
(function (StructureEdit) {
|
|
@@ -1870,6 +2283,8 @@ var StructureRefusal;
|
|
|
1870
2283
|
StructureRefusal["LockedPart"] = "locked-part";
|
|
1871
2284
|
/** A locked part can only live directly inside its own parent role. */
|
|
1872
2285
|
StructureRefusal["LockedPlacement"] = "locked-placement";
|
|
2286
|
+
/** The new parent takes only certain roles (a tab list takes only tabs, CC4a). */
|
|
2287
|
+
StructureRefusal["NotAccepted"] = "not-accepted";
|
|
1873
2288
|
})(StructureRefusal || (StructureRefusal = {}));
|
|
1874
2289
|
/**
|
|
1875
2290
|
* Why the structure guard refuses `edit` on `node`, or null when it is allowed. `ancestors` (Move only) =
|
|
@@ -1877,26 +2292,1229 @@ var StructureRefusal;
|
|
|
1877
2292
|
*/
|
|
1878
2293
|
function structureRefusal(node, edit, ancestors = []) {
|
|
1879
2294
|
const spec = node?.role ? COMPOSITE_ROLE_SPECS[node.role] : undefined;
|
|
1880
|
-
|
|
2295
|
+
const parent = ancestors[ancestors.length - 1];
|
|
2296
|
+
if (edit === StructureEdit.Move && acceptsRefusal(node, parent))
|
|
2297
|
+
return StructureRefusal.NotAccepted;
|
|
2298
|
+
if (!spec?.locked && !spec?.pinned)
|
|
1881
2299
|
return null;
|
|
1882
2300
|
if (edit === StructureEdit.Delete)
|
|
1883
|
-
return StructureRefusal.LockedPart;
|
|
1884
|
-
const
|
|
1885
|
-
return
|
|
2301
|
+
return spec.locked ? StructureRefusal.LockedPart : null;
|
|
2302
|
+
const placed = spec.parentType ? parent?.type === spec.parentType : parent?.role === spec.parent;
|
|
2303
|
+
return placed ? null : StructureRefusal.LockedPlacement;
|
|
1886
2304
|
}
|
|
1887
2305
|
/** True when `node` is a locked part (shown with a lock and no delete in the layer tree). Deleting a whole
|
|
1888
2306
|
* composite is always allowed — only its locked parts are refused. */
|
|
1889
2307
|
function isLockedPart(node) {
|
|
1890
2308
|
return !!(node?.role && COMPOSITE_ROLE_SPECS[node.role]?.locked);
|
|
1891
2309
|
}
|
|
2310
|
+
/** Why `parent` refuses `node` as a child (it takes only certain roles), or null. */
|
|
2311
|
+
function acceptsRefusal(node, parent) {
|
|
2312
|
+
const accepts = parent?.role ? COMPOSITE_ROLE_SPECS[parent.role]?.accepts : undefined;
|
|
2313
|
+
if (!accepts)
|
|
2314
|
+
return null;
|
|
2315
|
+
return node?.role && accepts.includes(node.role) ? null : StructureRefusal.NotAccepted;
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
const SLIDE_VISIBLE = 'swiper-slide-visible';
|
|
2319
|
+
const RUNTIME_KEY = '__ioxCarousel';
|
|
2320
|
+
const ROTATION_EVENTS = 'autoplayStart autoplayStop autoplayPause autoplayResume';
|
|
2321
|
+
const SLIDE_EVENTS = 'slideChange transitionEnd resize slidesLengthChange';
|
|
2322
|
+
const AUTHOR_NAME_KEY = '__ioxAuthorName';
|
|
2323
|
+
/** The name the AUTHOR gave the pause control (its rendered `aria-label` before this runtime first relabelled it). */
|
|
2324
|
+
function authorNameOf(el) {
|
|
2325
|
+
if (!el)
|
|
2326
|
+
return null;
|
|
2327
|
+
const host = el;
|
|
2328
|
+
if (host[AUTHOR_NAME_KEY] === undefined)
|
|
2329
|
+
host[AUTHOR_NAME_KEY] = el.getAttribute('aria-label') ?? '';
|
|
2330
|
+
return host[AUTHOR_NAME_KEY] || null;
|
|
2331
|
+
}
|
|
2332
|
+
/** The part of `role` that belongs to THIS slider (never one of a slider nested in a slide). */
|
|
2333
|
+
function sliderPartElement(sliderEl, role) {
|
|
2334
|
+
const cls = `iox-role-${role.replace('.', '-')}`;
|
|
2335
|
+
for (const el of Array.from(sliderEl.querySelectorAll(`.${cls}`))) {
|
|
2336
|
+
if (el.closest('.swiper') === sliderEl && !el.closest('.swiper-wrapper'))
|
|
2337
|
+
return el;
|
|
2338
|
+
}
|
|
2339
|
+
return null;
|
|
2340
|
+
}
|
|
2341
|
+
class CarouselA11y {
|
|
2342
|
+
constructor(swiper, options = {}) {
|
|
2343
|
+
this.swiper = swiper;
|
|
2344
|
+
/** The visitor pressed pause: rotation stays stopped until they press it again. */
|
|
2345
|
+
this.stoppedByVisitor = false;
|
|
2346
|
+
this.focusInside = false;
|
|
2347
|
+
/** The pointer is over the carousel and Swiper holds rotation for it (`pauseOnMouseEnter`). */
|
|
2348
|
+
this.hovering = false;
|
|
2349
|
+
this.cleanups = [];
|
|
2350
|
+
const el = swiper.el;
|
|
2351
|
+
this.words = options.words ?? sliderWords(el.ownerDocument?.documentElement?.getAttribute('lang'));
|
|
2352
|
+
this.pause = sliderPartElement(el, SliderRole.Pause);
|
|
2353
|
+
this.authorName = authorNameOf(this.pause);
|
|
2354
|
+
this.listen(el, 'focusin', () => this.onFocusIn());
|
|
2355
|
+
this.listen(el, 'pointerenter', () => { this.hovering = this.hoverPauses; this.sync(); });
|
|
2356
|
+
this.listen(el, 'pointerleave', () => { this.hovering = false; this.sync(); });
|
|
2357
|
+
this.listen(el, 'focusout', (e) => this.onFocusOut(e));
|
|
2358
|
+
if (this.pause)
|
|
2359
|
+
this.listen(this.pause, 'click', (e) => { e.preventDefault(); this.togglePause(); });
|
|
2360
|
+
const sync = () => this.sync();
|
|
2361
|
+
swiper.on(ROTATION_EVENTS, sync);
|
|
2362
|
+
swiper.on(SLIDE_EVENTS, sync);
|
|
2363
|
+
this.cleanups.push(() => { swiper.off(ROTATION_EVENTS, sync); swiper.off(SLIDE_EVENTS, sync); });
|
|
2364
|
+
el[RUNTIME_KEY] = this;
|
|
2365
|
+
this.sync();
|
|
2366
|
+
}
|
|
2367
|
+
/** True while the slides move on their own: autoplay runs and nothing holds it — not focus inside, not the
|
|
2368
|
+
* pointer over it. (Swiper's own `paused` also flips during every transition, so it is not read here.) */
|
|
2369
|
+
get rotating() {
|
|
2370
|
+
return !!this.swiper.autoplay?.running && !this.focusInside && !this.hovering;
|
|
2371
|
+
}
|
|
2372
|
+
get hoverPauses() {
|
|
2373
|
+
const autoplay = this.swiper.params?.autoplay;
|
|
2374
|
+
return typeof autoplay === 'object' && !!autoplay?.pauseOnMouseEnter;
|
|
2375
|
+
}
|
|
2376
|
+
/** The pause control: stop rotation for good, or start it again. */
|
|
2377
|
+
togglePause() {
|
|
2378
|
+
const a = this.swiper.autoplay;
|
|
2379
|
+
if (!a)
|
|
2380
|
+
return;
|
|
2381
|
+
if (a.running) {
|
|
2382
|
+
this.stoppedByVisitor = true;
|
|
2383
|
+
a.stop();
|
|
2384
|
+
}
|
|
2385
|
+
else {
|
|
2386
|
+
this.stoppedByVisitor = false;
|
|
2387
|
+
a.start();
|
|
2388
|
+
// Focus is on the pause control itself — inside the carousel: the rotation it just started waits until
|
|
2389
|
+
// focus leaves (APG), but the control already reads "stop".
|
|
2390
|
+
if (this.focusInside)
|
|
2391
|
+
a.pause();
|
|
2392
|
+
}
|
|
2393
|
+
this.sync();
|
|
2394
|
+
}
|
|
2395
|
+
destroy() {
|
|
2396
|
+
this.cleanups.splice(0).forEach(fn => fn());
|
|
2397
|
+
delete this.swiper.el[RUNTIME_KEY];
|
|
2398
|
+
}
|
|
2399
|
+
listen(target, type, fn) {
|
|
2400
|
+
target.addEventListener(type, fn);
|
|
2401
|
+
this.cleanups.push(() => target.removeEventListener(type, fn));
|
|
2402
|
+
}
|
|
2403
|
+
onFocusIn() {
|
|
2404
|
+
this.focusInside = true;
|
|
2405
|
+
if (this.swiper.autoplay?.running && !this.swiper.autoplay.paused)
|
|
2406
|
+
this.swiper.autoplay.pause();
|
|
2407
|
+
this.sync();
|
|
2408
|
+
}
|
|
2409
|
+
onFocusOut(e) {
|
|
2410
|
+
const next = e.relatedTarget;
|
|
2411
|
+
if (next && this.swiper.el.contains(next))
|
|
2412
|
+
return;
|
|
2413
|
+
this.focusInside = false;
|
|
2414
|
+
if (!this.stoppedByVisitor && this.swiper.autoplay?.running && this.swiper.autoplay.paused)
|
|
2415
|
+
this.swiper.autoplay.resume();
|
|
2416
|
+
this.sync();
|
|
2417
|
+
}
|
|
2418
|
+
/** Re-state the carousel: the live region, the pause control, the slides out of view. */
|
|
2419
|
+
sync() {
|
|
2420
|
+
if (this.swiper.destroyed)
|
|
2421
|
+
return;
|
|
2422
|
+
const rotating = this.rotating;
|
|
2423
|
+
this.swiper.wrapperEl?.setAttribute('aria-live', sliderLiveRegion(rotating));
|
|
2424
|
+
if (this.pause) {
|
|
2425
|
+
const running = !!this.swiper.autoplay?.running;
|
|
2426
|
+
const attrs = sliderPauseAttributes(running, this.words, this.authorName);
|
|
2427
|
+
for (const [name, value] of Object.entries(attrs))
|
|
2428
|
+
this.pause.setAttribute(name, value);
|
|
2429
|
+
if (!('aria-pressed' in attrs))
|
|
2430
|
+
this.pause.removeAttribute('aria-pressed');
|
|
2431
|
+
const icon = this.pause.querySelector('i');
|
|
2432
|
+
if (icon)
|
|
2433
|
+
icon.className = `${sliderPauseGlyph(running)} ${Array.from(icon.classList).filter(c => !c.startsWith('ph')).join(' ')}`.trim();
|
|
2434
|
+
}
|
|
2435
|
+
this.syncHiddenSlides();
|
|
2436
|
+
}
|
|
2437
|
+
syncHiddenSlides() {
|
|
2438
|
+
const slides = this.swiper.slides ?? [];
|
|
2439
|
+
const anyVisibleMarked = slides.some(s => s.classList?.contains(SLIDE_VISIBLE));
|
|
2440
|
+
slides.forEach((slide, i) => {
|
|
2441
|
+
const visible = anyVisibleMarked ? slide.classList.contains(SLIDE_VISIBLE) : i === this.swiper.activeIndex;
|
|
2442
|
+
if (visible) {
|
|
2443
|
+
slide.removeAttribute('inert');
|
|
2444
|
+
slide.removeAttribute('aria-hidden');
|
|
2445
|
+
}
|
|
2446
|
+
else {
|
|
2447
|
+
slide.setAttribute('inert', '');
|
|
2448
|
+
slide.setAttribute('aria-hidden', 'true');
|
|
2449
|
+
}
|
|
2450
|
+
});
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
/** The carousel runtime attached to a slider element, if any. */
|
|
2454
|
+
function carouselOf(el) {
|
|
2455
|
+
return el?.[RUNTIME_KEY] ?? null;
|
|
2456
|
+
}
|
|
2457
|
+
/** Attach once per Swiper (a re-init gets a fresh runtime). */
|
|
2458
|
+
function attachCarouselA11y(swiper, options = {}) {
|
|
2459
|
+
carouselOf(swiper.el)?.destroy();
|
|
2460
|
+
return new CarouselA11y(swiper, options);
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
/**
|
|
2464
|
+
* THE LIGHTBOX (CC3 — complex-components plan §7d, board 6). Not a component: a Dialog holding a Slider bound to the
|
|
2465
|
+
* SAME source as the grid of thumbnails. The one new thing is `openDialog`'s "Open at the clicked item"
|
|
2466
|
+
* (`slideTo: 'row'`): a thumbnail in a repeater row opens the lightbox at ITS slide.
|
|
2467
|
+
*
|
|
2468
|
+
* Dialog `dialog` · centre · full viewport · Inverse scheme "Lightbox" (a free-standing Dialog: board A4)
|
|
2469
|
+
* ├─ ButtonBlock `button-ghost` "Close" › Icon × click → closeDialog
|
|
2470
|
+
* └─ SliderContainer one slide in view · keyboard · the source's rows
|
|
2471
|
+
* ├─ Previous · Next (slider.prev / slider.next, button-secondary) hidden on a phone (swipe)
|
|
2472
|
+
* ├─ SliderSlide › Image `media` (contain, keeps its alt) + Text `small` caption
|
|
2473
|
+
* └─ Pagination (slider.pagination) — the counter "3 / 6"
|
|
2474
|
+
*
|
|
2475
|
+
* Keyboard ← → are the Slider's Keyboard module; Esc and the focus trap are the `<dialog>`'s; focus returns to the
|
|
2476
|
+
* thumbnail that opened it (the dialog opener's own rule).
|
|
2477
|
+
*/
|
|
2478
|
+
const SLIDE_TO_ROW = 'row';
|
|
2479
|
+
/** The attribute a renderer writes on every repeated row (the context-state provenance, both engine paths, the canvas). */
|
|
2480
|
+
const ROW_INDEX_ATTRIBUTE = 'data-iox-row-idx';
|
|
2481
|
+
const LIGHTBOX_LABEL = 'Lightbox';
|
|
2482
|
+
const DIALOG = 'Dialog';
|
|
2483
|
+
const REPEATER = 'Repeater';
|
|
2484
|
+
const SOURCE = 'source';
|
|
2485
|
+
const SOURCE_PATH = 'sourcePath';
|
|
2486
|
+
const ZERO = '0px 0px 0px 0px';
|
|
2487
|
+
const DIALOG_BASE = {
|
|
2488
|
+
width: '100vw', maxWidth: '100vw', height: '100dvh', maxHeight: '100dvh', borderRadius: '0px', padding: '18px 18px 18px 18px',
|
|
2489
|
+
display: 'flex', flexDirection: 'column', gap: '0px',
|
|
2490
|
+
};
|
|
2491
|
+
/** The top row (board 6): the counter at its start, Close at its end — Close sits above the slider, which rises into
|
|
2492
|
+
* the row so its counter shares it. */
|
|
2493
|
+
const CLOSE_BASE = {
|
|
2494
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center', width: '44px', height: '44px', minWidth: '44px',
|
|
2495
|
+
padding: ZERO, margin: ZERO, borderRadius: '999px', alignSelf: 'flex-end', flexShrink: '0', position: 'relative', zIndex: '20',
|
|
2496
|
+
};
|
|
2497
|
+
const SLIDER_BASE = { width: '100%', height: '100%', minHeight: '0px', flexGrow: '1', margin: '-44px 0px 0px 0px' };
|
|
2498
|
+
const SLIDE_BASE = {
|
|
2499
|
+
width: '100%', height: '100%', aspectRatio: 'auto', display: 'flex', flexDirection: 'column', alignItems: 'center',
|
|
2500
|
+
justifyContent: 'center', gap: '12px', padding: '56px 64px 16px 64px',
|
|
2501
|
+
};
|
|
2502
|
+
const IMAGE_BASE$1 = { width: '100%', height: 'auto', maxHeight: '72dvh', minHeight: '0px', objectFit: 'contain', aspectRatio: '4 / 3', flexShrink: '1' };
|
|
2503
|
+
const CAPTION_BASE = { textAlign: 'center', margin: ZERO, padding: ZERO, minHeight: 'auto', width: 'auto' };
|
|
2504
|
+
/** The counter "3 / 6" sits at the START of the top row, Close at its end (board 6). */
|
|
2505
|
+
const COUNTER_BASE = { color: 'var(--iox-text-muted)', top: '12px', alignItems: 'flex-start', justifyContent: 'flex-start' };
|
|
2506
|
+
/** On a phone the arrows give way to a swipe (board 6, "the arrows hide on touch-only widths"). */
|
|
2507
|
+
const LIGHTBOX_ARROWS_HIDE_AT = 'mobile';
|
|
2508
|
+
/** A click on `node` runs one instant action on `target`. */
|
|
2509
|
+
function onClick(node, type, target, params) {
|
|
2510
|
+
node.interactions = [{
|
|
2511
|
+
id: `${node.id ?? 'node'}-click`, trigger: 'click',
|
|
2512
|
+
actions: [{ type, target, duration: 0, delay: 0, easing: 'ease', ...(params ? { params } : {}) }],
|
|
2513
|
+
}];
|
|
2514
|
+
}
|
|
2515
|
+
/** A binding of `prop` to the row field `field` of `source` (`item` = the row itself). */
|
|
2516
|
+
function bindRow(node, prop, source, field) {
|
|
2517
|
+
const bindings = (node.bindings ?? []).filter((b) => b.prop !== prop);
|
|
2518
|
+
bindings.push({ prop, source, path: field === 'item' ? '' : field });
|
|
2519
|
+
node.bindings = bindings;
|
|
2520
|
+
}
|
|
2521
|
+
function buildSlide(create, key, options, rowAlias) {
|
|
2522
|
+
const slide = makeCompositePart(create, { key, type: SLIDE_TYPE, props: {}, elementStyle: NO_ELEMENT_STYLE }, null, 'Slide', SLIDE_BASE);
|
|
2523
|
+
const image = makeCompositePart(create, { key: `${key}-image`, type: 'Image', props: { src: '', alt: '' }, elementStyle: 'media' }, null, 'Image', IMAGE_BASE$1);
|
|
2524
|
+
const caption = makeCompositePart(create, { key: `${key}-caption`, type: 'Text', props: { content: 'Caption', tag: 'p' }, elementStyle: 'small' }, null, 'Caption', CAPTION_BASE);
|
|
2525
|
+
if (rowAlias) {
|
|
2526
|
+
bindRow(image, 'src', rowAlias, options.imageField ?? 'item');
|
|
2527
|
+
if (options.altField)
|
|
2528
|
+
bindRow(image, 'alt', rowAlias, options.altField);
|
|
2529
|
+
if (options.captionField)
|
|
2530
|
+
bindRow(caption, 'content', rowAlias, options.captionField);
|
|
2531
|
+
}
|
|
2532
|
+
slide.children = options.captionField || !rowAlias ? [image, caption] : [image];
|
|
2533
|
+
return slide;
|
|
2534
|
+
}
|
|
2535
|
+
/** A fresh lightbox: the Dialog, its Close part and the Slider (bound to `source`, or three manual slides). */
|
|
2536
|
+
function buildLightboxRecipe(create, options = {}) {
|
|
2537
|
+
const dialog = makeCompositePart(create, { key: 'lightbox', type: DIALOG, props: {}, elementStyle: 'dialog' }, null, LIGHTBOX_LABEL, DIALOG_BASE);
|
|
2538
|
+
writeProp(dialog, 'placement', 'center');
|
|
2539
|
+
writeProp(dialog, 'closeOnBackdrop', true);
|
|
2540
|
+
writeProp(dialog, 'showCloseButton', false);
|
|
2541
|
+
writeProp(dialog, 'ariaLabel', 'Gallery');
|
|
2542
|
+
setColorScheme(dialog, 'inverse');
|
|
2543
|
+
const close = makeCompositePart(create, { key: 'close', type: 'ButtonBlock', props: { tag: 'button', type: 'button', ariaLabel: 'Close' }, elementStyle: 'button-ghost' }, null, 'Close', CLOSE_BASE);
|
|
2544
|
+
close.children = [makeCompositePart(create, { key: 'close-icon', type: 'Icon', props: { iconClass: 'ph ph-x' }, elementStyle: 'icon' }, null, 'Icon', { fontSize: '22px', color: 'inherit' })];
|
|
2545
|
+
onClick(close, 'closeDialog', dialog.id ?? '');
|
|
2546
|
+
const slider = makeCompositePart(create, { key: 'slider', type: SLIDER_TYPE, props: {}, elementStyle: NO_ELEMENT_STYLE }, null, 'Slider', SLIDER_BASE);
|
|
2547
|
+
for (const [name, value] of Object.entries({
|
|
2548
|
+
slidesPerView: 1, spaceBetween: 0, loop: 'off', enableNavigation: true, enablePagination: true, paginationType: 'fraction',
|
|
2549
|
+
enableKeyboard: true, ariaLabel: 'Gallery', limit: 50,
|
|
2550
|
+
}))
|
|
2551
|
+
writeProp(slider, name, value);
|
|
2552
|
+
const rowAlias = options.source ? lastSegment(options.sourcePath) || options.source : null;
|
|
2553
|
+
if (options.source) {
|
|
2554
|
+
writeProp(slider, SOURCE, options.source);
|
|
2555
|
+
if (options.sourcePath)
|
|
2556
|
+
writeProp(slider, SOURCE_PATH, options.sourcePath);
|
|
2557
|
+
}
|
|
2558
|
+
const prev = buildSliderPart(create, SliderRole.Prev);
|
|
2559
|
+
const next = buildSliderPart(create, SliderRole.Next);
|
|
2560
|
+
for (const arrow of [prev, next]) {
|
|
2561
|
+
const layers = { ...(arrow.responsive ?? {}) };
|
|
2562
|
+
layers[LIGHTBOX_ARROWS_HIDE_AT] = { styleProps: { display: 'none' } };
|
|
2563
|
+
arrow.responsive = layers;
|
|
2564
|
+
}
|
|
2565
|
+
const counter = buildSliderPart(create, SliderRole.Pagination);
|
|
2566
|
+
counter.label = 'Counter';
|
|
2567
|
+
writeStyles(counter, COUNTER_BASE);
|
|
2568
|
+
const slides = rowAlias ? [buildSlide(create, 'slide', options, rowAlias)] : [1, 2, 3].map(n => buildSlide(create, `slide-${n}`, options, null));
|
|
2569
|
+
slider.children = [prev, next, ...slides, counter];
|
|
2570
|
+
dialog.children = [close, slider];
|
|
2571
|
+
return dialog;
|
|
2572
|
+
}
|
|
2573
|
+
const lastSegment = (path) => (path ? String(path).split('.').filter(Boolean).pop() ?? '' : '');
|
|
2574
|
+
/** What a Repeater (its `source` binding) or a bound Slider (its `source` / `sourcePath` traits) repeats over. */
|
|
2575
|
+
function repeatSourceOf(node) {
|
|
2576
|
+
if (!node || (node.type !== REPEATER && node.type !== SLIDER_TYPE))
|
|
2577
|
+
return null;
|
|
2578
|
+
const binding = (node.bindings ?? []).find(b => b?.prop === SOURCE);
|
|
2579
|
+
if (binding?.source)
|
|
2580
|
+
return { source: binding.source, path: binding.path ?? '' };
|
|
2581
|
+
const source = readProp(node, SOURCE);
|
|
2582
|
+
if (typeof source !== 'string' || !source)
|
|
2583
|
+
return null;
|
|
2584
|
+
const path = readProp(node, SOURCE_PATH);
|
|
2585
|
+
return { source, path: typeof path === 'string' ? path : '' };
|
|
2586
|
+
}
|
|
2587
|
+
function sameRepeatSource(a, b) {
|
|
2588
|
+
return !!a && !!b && a.source === b.source && (a.path ?? '') === (b.path ?? '');
|
|
2589
|
+
}
|
|
2590
|
+
/** The Slider a lightbox Dialog holds (the first one, depth first), or null. */
|
|
2591
|
+
function lightboxSliderOf(dialog) {
|
|
2592
|
+
for (const child of dialog?.children ?? []) {
|
|
2593
|
+
if (child.type === SLIDER_TYPE)
|
|
2594
|
+
return child;
|
|
2595
|
+
const below = lightboxSliderOf(child);
|
|
2596
|
+
if (below)
|
|
2597
|
+
return below;
|
|
2598
|
+
}
|
|
2599
|
+
return null;
|
|
2600
|
+
}
|
|
2601
|
+
/** The repeated rows the element is in: the nearest Repeater or bound Slider among its ancestors (`path` = root → the
|
|
2602
|
+
* element, inclusive). */
|
|
2603
|
+
function rowSourceOf(path) {
|
|
2604
|
+
for (let i = path.length - 2; i >= 0; i--) {
|
|
2605
|
+
const source = repeatSourceOf(path[i]);
|
|
2606
|
+
if (source)
|
|
2607
|
+
return source;
|
|
2608
|
+
}
|
|
2609
|
+
return null;
|
|
2610
|
+
}
|
|
2611
|
+
/**
|
|
2612
|
+
* Interactions ▸ openDialog ▸ "Open at the clicked item" is offered only when it can work (board 9): the clicked
|
|
2613
|
+
* element sits in a repeater row AND the target Dialog holds a Slider bound to the same source.
|
|
2614
|
+
*/
|
|
2615
|
+
function canOpenAtClickedItem(ownerPath, dialog) {
|
|
2616
|
+
if (!dialog || dialog.type !== DIALOG)
|
|
2617
|
+
return false;
|
|
2618
|
+
return sameRepeatSource(rowSourceOf(ownerPath), repeatSourceOf(lightboxSliderOf(dialog)));
|
|
2619
|
+
}
|
|
2620
|
+
/** The repeated row an element sits in (its row index), 0 when it is in none (the canvas' template row). */
|
|
2621
|
+
function rowIndexOf(el) {
|
|
2622
|
+
const row = el?.closest?.(`[${ROW_INDEX_ATTRIBUTE}]`);
|
|
2623
|
+
const index = Number(row?.getAttribute(ROW_INDEX_ATTRIBUTE));
|
|
2624
|
+
return Number.isInteger(index) && index >= 0 ? index : 0;
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2627
|
+
/**
|
|
2628
|
+
* TABS (CC4a — composite K2 T17–T20; complex-components boards 3 and 9, APPROVED 2026-10-20). Not a component: a
|
|
2629
|
+
* recipe of ordinary nodes, each part carrying a role, so every tab and every panel is selectable on the canvas, in
|
|
2630
|
+
* the layer tree and the breadcrumb, and wears its own element style (R2).
|
|
2631
|
+
*
|
|
2632
|
+
* Container `box` role tabs traits activateOn · orientation · ariaLabel (the list's name)
|
|
2633
|
+
* ├─ Container `tab-list` role tabs.list → role=tablist (locked) — or a Repeater (From content)
|
|
2634
|
+
* │ └─ ButtonBlock `tab` ×3 role tabs.trigger itemKey k1… → <button role=tab> › Text `button-label`
|
|
2635
|
+
* └─ Container `box` role tabs.panels (locked) — or a Repeater (From content)
|
|
2636
|
+
* └─ Container `box` ×3 role tabs.panel the SAME itemKey as its tab → role=tabpanel, hidden while off
|
|
2637
|
+
* ├─ Image `media` 4 / 3
|
|
2638
|
+
* └─ Text `body`
|
|
2639
|
+
*
|
|
2640
|
+
* A tab and its panel are a PAIR by `itemKey` — never by index, so a tab moved to the end still shows its own panel.
|
|
2641
|
+
* The behaviour is BUILT IN (composite plan D1 = B): exactly one item is on (`exclusive`), the APG tabs pattern with
|
|
2642
|
+
* AUTOMATIC activation (arrows move focus and activate, roving tabindex) — one runtime (`exclusive-runtime.ts`) on the
|
|
2643
|
+
* builder canvas and both engine paths, from the attribute planners below.
|
|
2644
|
+
*/
|
|
2645
|
+
var TabsBehaviourTrait;
|
|
2646
|
+
(function (TabsBehaviourTrait) {
|
|
2647
|
+
/** Which pointer gesture turns a tab on: a click (always) or also a hover. */
|
|
2648
|
+
TabsBehaviourTrait["ActivateOn"] = "activateOn";
|
|
2649
|
+
/** The tab list across the top, or beside the panels. */
|
|
2650
|
+
TabsBehaviourTrait["Orientation"] = "orientation";
|
|
2651
|
+
/** The tab list's accessible name (`aria-label` on the role=tablist element) — the root's, edited in Accessibility. */
|
|
2652
|
+
TabsBehaviourTrait["Label"] = "ariaLabel";
|
|
2653
|
+
})(TabsBehaviourTrait || (TabsBehaviourTrait = {}));
|
|
2654
|
+
var TabsActivateOn;
|
|
2655
|
+
(function (TabsActivateOn) {
|
|
2656
|
+
TabsActivateOn["Click"] = "click";
|
|
2657
|
+
TabsActivateOn["Hover"] = "hover";
|
|
2658
|
+
})(TabsActivateOn || (TabsActivateOn = {}));
|
|
2659
|
+
var TabsOrientation;
|
|
2660
|
+
(function (TabsOrientation) {
|
|
2661
|
+
TabsOrientation["Horizontal"] = "horizontal";
|
|
2662
|
+
TabsOrientation["Vertical"] = "vertical";
|
|
2663
|
+
})(TabsOrientation || (TabsOrientation = {}));
|
|
2664
|
+
const TABS_ACTIVATE_ONS = [TabsActivateOn.Click, TabsActivateOn.Hover];
|
|
2665
|
+
const TABS_ORIENTATIONS = [TabsOrientation.Horizontal, TabsOrientation.Vertical];
|
|
2666
|
+
/** Tabs from content default to six rows: more tabs do not fit a row (board 9 Q13). */
|
|
2667
|
+
const TABS_CONTENT_LIMIT = 6;
|
|
2668
|
+
/** The breakpoint whose layer stacks a VERTICAL tabs (the list above the panels) and a panel's picture above its words. */
|
|
2669
|
+
const TABS_STACK_BREAKPOINT = CARD_STACK_BREAKPOINT;
|
|
2670
|
+
/** The words a fresh Tabs starts with — generic, the author's to replace (Card v2.1: never invented facts). */
|
|
2671
|
+
const TABS_WORDS = [
|
|
2672
|
+
{ label: 'First tab', text: 'Say what this tab is about in a sentence or two. Its picture sits beside it.' },
|
|
2673
|
+
{ label: 'Second tab', text: 'Say what this tab is about in a sentence or two. Its picture sits beside it.' },
|
|
2674
|
+
{ label: 'Third tab', text: 'Say what this tab is about in a sentence or two. Its picture sits beside it.' },
|
|
2675
|
+
];
|
|
2676
|
+
const TABS_NEW_TAB_WORDS = { label: 'New tab', text: 'Say what this tab is about in a sentence or two. Its picture sits beside it.' };
|
|
2677
|
+
var Type;
|
|
2678
|
+
(function (Type) {
|
|
2679
|
+
Type["Container"] = "Container";
|
|
2680
|
+
Type["ButtonBlock"] = "ButtonBlock";
|
|
2681
|
+
Type["Text"] = "Text";
|
|
2682
|
+
Type["Image"] = "Image";
|
|
2683
|
+
Type["Repeater"] = "Repeater";
|
|
2684
|
+
})(Type || (Type = {}));
|
|
2685
|
+
var Style;
|
|
2686
|
+
(function (Style) {
|
|
2687
|
+
Style["Root"] = "box";
|
|
2688
|
+
Style["List"] = "tab-list";
|
|
2689
|
+
Style["Tab"] = "tab";
|
|
2690
|
+
Style["Label"] = "button-label";
|
|
2691
|
+
Style["Panels"] = "box";
|
|
2692
|
+
Style["Panel"] = "box";
|
|
2693
|
+
Style["Media"] = "media";
|
|
2694
|
+
Style["Body"] = "body";
|
|
2695
|
+
})(Style || (Style = {}));
|
|
2696
|
+
const ZERO_BOX = '0px 0px 0px 0px';
|
|
2697
|
+
/** The list's hairline, as its element style writes it — read from the contract, never retyped. */
|
|
2698
|
+
const LIST_EDGE = String(elementStyleById(Style.List)?.base['borderWidth'] ?? ZERO_BOX);
|
|
2699
|
+
/** What each part holds — layout only; its LOOK is its element style. The list WRAPS at narrow widths (board 3). */
|
|
2700
|
+
const PART_BASE = {
|
|
2701
|
+
[TabsRole.Tabs]: { display: 'flex', width: '100%', minHeight: 'auto', height: 'auto' },
|
|
2702
|
+
[TabsRole.List]: { display: 'flex', gap: '4px', minHeight: 'auto', height: 'auto' },
|
|
2703
|
+
[TabsRole.Trigger]: { width: 'auto', minHeight: 'auto', height: 'auto', cursor: 'pointer' },
|
|
2704
|
+
[TabsRole.Panels]: { display: 'block', width: '100%', minWidth: '0px', minHeight: 'auto', height: 'auto' },
|
|
2705
|
+
// Board 3: a panel is any nodes at all — the recipe's is a picture beside its words.
|
|
2706
|
+
[TabsRole.Panel]: { display: 'grid', gridTemplateColumns: '1fr 1.2fr', gap: '24px', alignItems: 'center', width: '100%', minHeight: 'auto', height: 'auto' },
|
|
2707
|
+
};
|
|
2708
|
+
const LABEL_BASE = { width: 'auto', minHeight: 'auto', padding: ZERO_BOX, margin: ZERO_BOX };
|
|
2709
|
+
const IMAGE_BASE = { display: 'block', width: '100%', maxWidth: '100%', aspectRatio: '4 / 3', height: 'auto', minHeight: 'auto' };
|
|
2710
|
+
const BODY_BASE = { margin: ZERO_BOX, padding: ZERO_BOX, minHeight: 'auto' };
|
|
2711
|
+
/** On a phone a panel's picture sits above its words. */
|
|
2712
|
+
const PANEL_STACK = { gridTemplateColumns: '1fr' };
|
|
2713
|
+
const ORIENTATION_LAYOUT = {
|
|
2714
|
+
[TabsOrientation.Horizontal]: {
|
|
2715
|
+
root: { flexDirection: 'column', alignItems: 'stretch', gap: '20px' },
|
|
2716
|
+
list: { flexDirection: 'row', flexWrap: 'wrap', width: '100%', flexShrink: '1', borderWidth: LIST_EDGE },
|
|
2717
|
+
},
|
|
2718
|
+
[TabsOrientation.Vertical]: {
|
|
2719
|
+
root: { flexDirection: 'row', alignItems: 'flex-start', gap: '24px' },
|
|
2720
|
+
list: { flexDirection: 'column', flexWrap: 'nowrap', width: 'auto', flexShrink: '0', borderWidth: ZERO_BOX },
|
|
2721
|
+
rootStack: { flexDirection: 'column', alignItems: 'stretch', gap: '20px' },
|
|
2722
|
+
listStack: { width: '100%' },
|
|
2723
|
+
},
|
|
2724
|
+
};
|
|
2725
|
+
const ROOT_STACK_KEYS = ['flexDirection', 'alignItems', 'gap'];
|
|
2726
|
+
const LIST_STACK_KEYS = ['width'];
|
|
2727
|
+
function make(create, key, type, role, style, props = {}, label) {
|
|
2728
|
+
return makeCompositePart(create, { key, type, props, elementStyle: style }, role, label, role ? PART_BASE[role] : undefined);
|
|
2729
|
+
}
|
|
2730
|
+
/** One tab and its panel, sharing `itemKey`. */
|
|
2731
|
+
function buildTabPair(create, words, itemKey) {
|
|
2732
|
+
const tab = make(create, `${itemKey}-tab`, Type.ButtonBlock, TabsRole.Trigger, Style.Tab, {}, 'Tab');
|
|
2733
|
+
tab.itemKey = itemKey;
|
|
2734
|
+
const label = make(create, `${itemKey}-label`, Type.Text, null, Style.Label, { content: words.label, tag: 'span' }, 'Text');
|
|
2735
|
+
writeStyles(label, LABEL_BASE);
|
|
2736
|
+
tab.children = [label];
|
|
2737
|
+
const panel = make(create, `${itemKey}-panel`, Type.Container, TabsRole.Panel, Style.Panel, {}, 'Panel');
|
|
2738
|
+
panel.itemKey = itemKey;
|
|
2739
|
+
writeLayerStyles(panel, TABS_STACK_BREAKPOINT, PANEL_STACK);
|
|
2740
|
+
const image = make(create, `${itemKey}-image`, Type.Image, null, Style.Media, { src: '', alt: '' }, 'Image');
|
|
2741
|
+
writeStyles(image, IMAGE_BASE);
|
|
2742
|
+
const text = make(create, `${itemKey}-text`, Type.Text, null, Style.Body, { content: words.text, tag: 'p' }, 'Text');
|
|
2743
|
+
writeStyles(text, BODY_BASE);
|
|
2744
|
+
panel.children = [image, text];
|
|
2745
|
+
return { tab, panel };
|
|
2746
|
+
}
|
|
2747
|
+
/** A fresh Tabs: three tabs, the first on, activated by a click, the list across the top (board 3). */
|
|
2748
|
+
function buildTabsRecipe(create, options = {}) {
|
|
2749
|
+
const root = make(create, 'tabs', Type.Container, TabsRole.Tabs, Style.Root, {}, 'Tabs');
|
|
2750
|
+
writeProp(root, TabsBehaviourTrait.ActivateOn, options.activateOn ?? TabsActivateOn.Click);
|
|
2751
|
+
writeProp(root, TabsBehaviourTrait.Orientation, options.orientation ?? TabsOrientation.Horizontal);
|
|
2752
|
+
if (options.label)
|
|
2753
|
+
writeProp(root, TabsBehaviourTrait.Label, options.label);
|
|
2754
|
+
const list = make(create, 'list', Type.Container, TabsRole.List, Style.List, {}, 'Tab list');
|
|
2755
|
+
const panels = make(create, 'panels', Type.Container, TabsRole.Panels, Style.Panels, {}, 'Panels');
|
|
2756
|
+
const pairs = (options.tabs ?? TABS_WORDS).map((w, i) => buildTabPair(create, w, `k${i + 1}`));
|
|
2757
|
+
list.children = pairs.map(p => p.tab);
|
|
2758
|
+
panels.children = pairs.map(p => p.panel);
|
|
2759
|
+
root.children = [list, panels];
|
|
2760
|
+
applyOrientation(root, options.orientation ?? TabsOrientation.Horizontal);
|
|
2761
|
+
return root;
|
|
2762
|
+
}
|
|
2763
|
+
// ── reading a tree ──────────────────────────────────────────────────────────────────────────────
|
|
2764
|
+
function isTabsRoot(node) {
|
|
2765
|
+
return node?.role === TabsRole.Tabs;
|
|
2766
|
+
}
|
|
2767
|
+
/** The tab list (the holder of the triggers). */
|
|
2768
|
+
function tabsListOf(root) {
|
|
2769
|
+
return (root.children ?? []).find(c => c.role === TabsRole.List);
|
|
2770
|
+
}
|
|
2771
|
+
/** The holder of the panels. */
|
|
2772
|
+
function tabsPanelsOf(root) {
|
|
2773
|
+
return (root.children ?? []).find(c => c.role === TabsRole.Panels);
|
|
2774
|
+
}
|
|
2775
|
+
/** The tabs in order — every tab (Manual), or the one template tab (From content). */
|
|
2776
|
+
function tabsTriggers(root) {
|
|
2777
|
+
return (tabsListOf(root)?.children ?? []).filter(c => c.role === TabsRole.Trigger);
|
|
2778
|
+
}
|
|
2779
|
+
/** The panels in their holder's order (their order does not pair them — their key does). */
|
|
2780
|
+
function tabsPanels(root) {
|
|
2781
|
+
return (tabsPanelsOf(root)?.children ?? []).filter(c => c.role === TabsRole.Panel);
|
|
2782
|
+
}
|
|
2783
|
+
/** True when the Tabs takes its tabs from content (its list is a Repeater, R3). */
|
|
2784
|
+
function isTabsFromContent(root) {
|
|
2785
|
+
return !!root && tabsListOf(root)?.type === Type.Repeater;
|
|
2786
|
+
}
|
|
2787
|
+
/** The panel paired with `tab` (the same key), or undefined. From content the one template panel pairs the template tab. */
|
|
2788
|
+
function tabPanelOf(root, tab) {
|
|
2789
|
+
if (isTabsFromContent(root))
|
|
2790
|
+
return tabsPanels(root)[0];
|
|
2791
|
+
return tab.itemKey ? tabsPanels(root).find(p => p.itemKey === tab.itemKey) : undefined;
|
|
2792
|
+
}
|
|
2793
|
+
/** The tab paired with `panel` (the same key), or undefined. */
|
|
2794
|
+
function tabTriggerOf(root, panel) {
|
|
2795
|
+
if (isTabsFromContent(root))
|
|
2796
|
+
return tabsTriggers(root)[0];
|
|
2797
|
+
return panel.itemKey ? tabsTriggers(root).find(t => t.itemKey === panel.itemKey) : undefined;
|
|
2798
|
+
}
|
|
2799
|
+
/** The other half of a tab ⇄ panel pair, or undefined for any other node. */
|
|
2800
|
+
function tabsPartnerOf(root, node) {
|
|
2801
|
+
if (node.role === TabsRole.Trigger)
|
|
2802
|
+
return tabPanelOf(root, node);
|
|
2803
|
+
if (node.role === TabsRole.Panel)
|
|
2804
|
+
return tabTriggerOf(root, node);
|
|
2805
|
+
return undefined;
|
|
2806
|
+
}
|
|
2807
|
+
/** The words a tab shows: its first Text's. */
|
|
2808
|
+
function tabLabelNodeOf(tab) {
|
|
2809
|
+
let hit;
|
|
2810
|
+
walkComposite(tab, n => { if (!hit && n !== tab && n.type === Type.Text)
|
|
2811
|
+
hit = n; });
|
|
2812
|
+
return hit;
|
|
2813
|
+
}
|
|
2814
|
+
const stripTags = (v) => String(v ?? '').replace(/<[^>]*>/g, '').trim();
|
|
2815
|
+
/** A tab's name in the Items list, the layer tree and the item-action picker: its words. */
|
|
2816
|
+
function tabLabel(tab) {
|
|
2817
|
+
const label = tabLabelNodeOf(tab);
|
|
2818
|
+
return label ? stripTags(readProp(label, 'content')) : '';
|
|
2819
|
+
}
|
|
2820
|
+
/** Items ▸ rename: the tab's words. */
|
|
2821
|
+
function renameTab(tab, words) {
|
|
2822
|
+
const label = tabLabelNodeOf(tab);
|
|
2823
|
+
return !!label && writeProp(label, 'content', words);
|
|
2824
|
+
}
|
|
2825
|
+
/** The first Text inside a panel (the words From content binds to). */
|
|
2826
|
+
function tabPanelTextOf(panel) {
|
|
2827
|
+
let hit;
|
|
2828
|
+
walkComposite(panel, n => { if (!hit && n !== panel && n.type === Type.Text)
|
|
2829
|
+
hit = n; });
|
|
2830
|
+
return hit;
|
|
2831
|
+
}
|
|
2832
|
+
/** The first Image inside a panel. */
|
|
2833
|
+
function tabPanelImageOf(panel) {
|
|
2834
|
+
let hit;
|
|
2835
|
+
walkComposite(panel, n => { if (!hit && n.type === Type.Image)
|
|
2836
|
+
hit = n; });
|
|
2837
|
+
return hit;
|
|
2838
|
+
}
|
|
2839
|
+
/** The root's behaviour (an unset trait reads its default: a click, the list across the top). */
|
|
2840
|
+
function tabsBehaviour(root) {
|
|
2841
|
+
const on = root ? readProp(root, TabsBehaviourTrait.ActivateOn) : undefined;
|
|
2842
|
+
const orientation = root ? readProp(root, TabsBehaviourTrait.Orientation) : undefined;
|
|
2843
|
+
return {
|
|
2844
|
+
activateOn: TABS_ACTIVATE_ONS.includes(on) ? on : TabsActivateOn.Click,
|
|
2845
|
+
orientation: TABS_ORIENTATIONS.includes(orientation) ? orientation : TabsOrientation.Horizontal,
|
|
2846
|
+
};
|
|
2847
|
+
}
|
|
2848
|
+
/** The tab list's accessible name (empty when unnamed). */
|
|
2849
|
+
function tabsLabel(root) {
|
|
2850
|
+
return root ? String(readProp(root, TabsBehaviourTrait.Label) ?? '').trim() : '';
|
|
2851
|
+
}
|
|
2852
|
+
/** Write the orientation preset into the root's and the list's layout (both orientations write the same keys). */
|
|
2853
|
+
function applyOrientation(root, orientation) {
|
|
2854
|
+
const spec = ORIENTATION_LAYOUT[orientation];
|
|
2855
|
+
const list = tabsListOf(root);
|
|
2856
|
+
writeStyles(root, spec.root);
|
|
2857
|
+
if (list)
|
|
2858
|
+
writeStyles(list, spec.list);
|
|
2859
|
+
if (spec.rootStack)
|
|
2860
|
+
writeLayerStyles(root, TABS_STACK_BREAKPOINT, spec.rootStack);
|
|
2861
|
+
else
|
|
2862
|
+
clearLayerStyles(root, TABS_STACK_BREAKPOINT, ROOT_STACK_KEYS);
|
|
2863
|
+
if (list) {
|
|
2864
|
+
if (spec.listStack)
|
|
2865
|
+
writeLayerStyles(list, TABS_STACK_BREAKPOINT, spec.listStack);
|
|
2866
|
+
else
|
|
2867
|
+
clearLayerStyles(list, TABS_STACK_BREAKPOINT, LIST_STACK_KEYS);
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
/** Behaviour ▸ Activate on / Orientation (and Accessibility ▸ the list's name). False when the root has no such trait. */
|
|
2871
|
+
function setTabsBehaviour(root, patch) {
|
|
2872
|
+
let ok = true;
|
|
2873
|
+
if (patch.activateOn !== undefined)
|
|
2874
|
+
ok = writeProp(root, TabsBehaviourTrait.ActivateOn, patch.activateOn) && ok;
|
|
2875
|
+
if (patch.orientation !== undefined) {
|
|
2876
|
+
ok = writeProp(root, TabsBehaviourTrait.Orientation, patch.orientation) && ok;
|
|
2877
|
+
applyOrientation(root, patch.orientation);
|
|
2878
|
+
}
|
|
2879
|
+
if (patch.label !== undefined)
|
|
2880
|
+
ok = writeProp(root, TabsBehaviourTrait.Label, patch.label) && ok;
|
|
2881
|
+
return ok;
|
|
2882
|
+
}
|
|
2883
|
+
// ── editing (Items list, selection toolbar ▸ Add tab, the structure guard) ──────────────────────
|
|
2884
|
+
/** A key no tab of `root` holds yet (`k4`…). */
|
|
2885
|
+
function nextTabKey(root) {
|
|
2886
|
+
const taken = new Set([...tabsTriggers(root), ...tabsPanels(root)].map(n => n.itemKey).filter(Boolean));
|
|
2887
|
+
let n = tabsTriggers(root).length + 1;
|
|
2888
|
+
while (taken.has(`k${n}`))
|
|
2889
|
+
n++;
|
|
2890
|
+
return `k${n}`;
|
|
2891
|
+
}
|
|
2892
|
+
/** Items ▸ Add tab (and the toolbar's "Add tab"): a fresh tab after `after` (or at the end) AND its panel. */
|
|
2893
|
+
function addTabPair(root, create, after, words = TABS_NEW_TAB_WORDS) {
|
|
2894
|
+
const list = tabsListOf(root);
|
|
2895
|
+
const panels = tabsPanelsOf(root);
|
|
2896
|
+
if (!list || !panels || isTabsFromContent(root))
|
|
2897
|
+
return null;
|
|
2898
|
+
const pair = buildTabPair(create, words, nextTabKey(root));
|
|
2899
|
+
const tabs = [...(list.children ?? [])];
|
|
2900
|
+
const afterTab = after ? (after.role === TabsRole.Panel ? tabTriggerOf(root, after) : after) : null;
|
|
2901
|
+
const at = afterTab ? tabs.indexOf(afterTab) : -1;
|
|
2902
|
+
tabs.splice(at < 0 ? tabs.length : at + 1, 0, pair.tab);
|
|
2903
|
+
list.children = tabs;
|
|
2904
|
+
const afterPanel = afterTab ? tabPanelOf(root, afterTab) : null;
|
|
2905
|
+
const held = [...(panels.children ?? [])];
|
|
2906
|
+
const pAt = afterPanel ? held.indexOf(afterPanel) : -1;
|
|
2907
|
+
held.splice(pAt < 0 ? held.length : pAt + 1, 0, pair.panel);
|
|
2908
|
+
panels.children = held;
|
|
2909
|
+
return pair;
|
|
2910
|
+
}
|
|
2911
|
+
function moveIn(holder, node, toIndex, role) {
|
|
2912
|
+
const children = [...(holder.children ?? [])];
|
|
2913
|
+
const from = children.indexOf(node);
|
|
2914
|
+
if (from < 0)
|
|
2915
|
+
return;
|
|
2916
|
+
children.splice(from, 1);
|
|
2917
|
+
const same = children.filter(c => c.role === role);
|
|
2918
|
+
const anchor = same[Math.max(0, Math.min(toIndex, same.length))];
|
|
2919
|
+
children.splice(anchor ? children.indexOf(anchor) : children.length, 0, node);
|
|
2920
|
+
holder.children = children;
|
|
2921
|
+
}
|
|
2922
|
+
/** Items ▸ drag to reorder: move a tab — and its panel, so the layer tree reads in the same order — to `toIndex`.
|
|
2923
|
+
* The pairing never depended on the order. False when `tab` is not one of the root's tabs. */
|
|
2924
|
+
function moveTabPair(root, tab, toIndex) {
|
|
2925
|
+
const list = tabsListOf(root);
|
|
2926
|
+
const panels = tabsPanelsOf(root);
|
|
2927
|
+
if (!list || tab.role !== TabsRole.Trigger || !(list.children ?? []).includes(tab))
|
|
2928
|
+
return false;
|
|
2929
|
+
const panel = tabPanelOf(root, tab);
|
|
2930
|
+
moveIn(list, tab, toIndex, TabsRole.Trigger);
|
|
2931
|
+
if (panel && panels)
|
|
2932
|
+
moveIn(panels, panel, toIndex, TabsRole.Panel);
|
|
2933
|
+
return true;
|
|
2934
|
+
}
|
|
2935
|
+
/** Remove a tab and its panel (the pair) — what "Delete tab and its panel" does. True when the tab was removed. */
|
|
2936
|
+
function removeTabPair(root, node) {
|
|
2937
|
+
const tab = node.role === TabsRole.Panel ? tabTriggerOf(root, node) : node;
|
|
2938
|
+
const list = tabsListOf(root);
|
|
2939
|
+
if (!tab || !list || tab.role !== TabsRole.Trigger)
|
|
2940
|
+
return false;
|
|
2941
|
+
const panel = tabPanelOf(root, tab);
|
|
2942
|
+
list.children = (list.children ?? []).filter(c => c !== tab);
|
|
2943
|
+
const panels = tabsPanelsOf(root);
|
|
2944
|
+
if (panel && panels)
|
|
2945
|
+
panels.children = (panels.children ?? []).filter(c => c !== panel);
|
|
2946
|
+
return true;
|
|
2947
|
+
}
|
|
2948
|
+
var TabsEditRefusal;
|
|
2949
|
+
(function (TabsEditRefusal) {
|
|
2950
|
+
/** A Tabs keeps at least one tab (composite plan T18). */
|
|
2951
|
+
TabsEditRefusal["LastTab"] = "last-tab";
|
|
2952
|
+
})(TabsEditRefusal || (TabsEditRefusal = {}));
|
|
2953
|
+
/** Why removing `node` (a tab or a panel) from `root` is refused, or null. */
|
|
2954
|
+
function tabsRemovalRefusal(root, node) {
|
|
2955
|
+
if (!root || (node?.role !== TabsRole.Trigger && node?.role !== TabsRole.Panel))
|
|
2956
|
+
return null;
|
|
2957
|
+
return tabsTriggers(root).length <= 1 ? TabsEditRefusal.LastTab : null;
|
|
2958
|
+
}
|
|
2959
|
+
/** Every "Item open" (`trigger-on`) state inside a tab or a panel points at ITS tab or panel — the part that carries
|
|
2960
|
+
* `data-iox-on` while its item is on. */
|
|
2961
|
+
function tabsItemTriggerIds(root) {
|
|
2962
|
+
for (const part of [...tabsTriggers(root), ...tabsPanels(root)]) {
|
|
2963
|
+
if (!part.id)
|
|
2964
|
+
continue;
|
|
2965
|
+
walkComposite(part, n => {
|
|
2966
|
+
if (n === part || !n.stateStyles?.[ITEM_OPEN_STATE])
|
|
2967
|
+
return;
|
|
2968
|
+
n.stateTriggers = { ...(n.stateTriggers ?? {}), [ITEM_OPEN_STATE]: part.id };
|
|
2969
|
+
});
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
function tabsScope(rootId, behaviour, keys, label = '') {
|
|
2973
|
+
return { rootId, behaviour, keys, active: keys[0] ?? null, label };
|
|
2974
|
+
}
|
|
2975
|
+
/** The item keys a manual Tabs pairs by — its tabs' own keys, in order (a tab with none by its index). */
|
|
2976
|
+
function manualTabKeys(root) {
|
|
2977
|
+
return tabsTriggers(root).map((t, i) => t.itemKey || `item-${i}`);
|
|
2978
|
+
}
|
|
2979
|
+
/** One item's render state: its key, whether it is on, and its ARIA ids (`iox-tab-<scope>-<key>-h` / `-p`). */
|
|
2980
|
+
function tabsItemState(scope, key) {
|
|
2981
|
+
return { key, on: scope.active === key, ids: accordionDomIds(scope.rootId, key, CompositeKind.Tabs) };
|
|
2982
|
+
}
|
|
2983
|
+
const ACTIVATE_ON_ATTRIBUTE = 'data-iox-activate-on';
|
|
2984
|
+
const ORIENTATION_ATTRIBUTE = 'data-iox-orientation';
|
|
2985
|
+
function tabsRootAttributes(behaviour, scopeId) {
|
|
2986
|
+
return {
|
|
2987
|
+
[COMPOSITE_ATTRIBUTE]: CompositeKind.Tabs,
|
|
2988
|
+
[ACTIVATE_ON_ATTRIBUTE]: behaviour.activateOn,
|
|
2989
|
+
[ORIENTATION_ATTRIBUTE]: behaviour.orientation,
|
|
2990
|
+
...(scopeId ? { [SCOPE_ATTRIBUTE]: scopeId } : {}),
|
|
2991
|
+
};
|
|
2992
|
+
}
|
|
2993
|
+
/** The tab list: `role=tablist`, vertical said aloud, named by the root's label. */
|
|
2994
|
+
function tabListAttributes(behaviour, label = '') {
|
|
2995
|
+
return {
|
|
2996
|
+
role: 'tablist',
|
|
2997
|
+
...(behaviour.orientation === TabsOrientation.Vertical ? { 'aria-orientation': 'vertical' } : {}),
|
|
2998
|
+
...(label ? { 'aria-label': label } : {}),
|
|
2999
|
+
};
|
|
3000
|
+
}
|
|
3001
|
+
/** A tab: a real button that selects its panel — selected = the one tab in the Tab order (roving tabindex). */
|
|
3002
|
+
function tabTriggerAttributes(state) {
|
|
3003
|
+
return {
|
|
3004
|
+
type: 'button',
|
|
3005
|
+
role: 'tab',
|
|
3006
|
+
id: state.ids.trigger,
|
|
3007
|
+
'aria-selected': state.on ? 'true' : 'false',
|
|
3008
|
+
'aria-controls': state.ids.panel,
|
|
3009
|
+
tabindex: state.on ? '0' : '-1',
|
|
3010
|
+
[ITEM_KEY_ATTRIBUTE]: state.key,
|
|
3011
|
+
...(state.on ? { [ON_ATTRIBUTE]: '' } : {}),
|
|
3012
|
+
};
|
|
3013
|
+
}
|
|
3014
|
+
/** A panel: `role=tabpanel`, named by its tab, a Tab stop of its own, and `hidden` (display:none) while off. */
|
|
3015
|
+
function tabPanelAttributes(state) {
|
|
3016
|
+
return {
|
|
3017
|
+
role: 'tabpanel',
|
|
3018
|
+
id: state.ids.panel,
|
|
3019
|
+
'aria-labelledby': state.ids.trigger,
|
|
3020
|
+
tabindex: '0',
|
|
3021
|
+
[ITEM_KEY_ATTRIBUTE]: state.key,
|
|
3022
|
+
...(state.on ? { [ON_ATTRIBUTE]: '' } : { hidden: '' }),
|
|
3023
|
+
};
|
|
3024
|
+
}
|
|
3025
|
+
/**
|
|
3026
|
+
* The render scope of one rendered Tabs — ONE decision for both engine paths (and the composer's checks): its scope id
|
|
3027
|
+
* (its node id + the rows around it, vectoriox/iox-ui#20), its behaviour, its list's name and every item key in order —
|
|
3028
|
+
* a manual tab's own `itemKey`, or, From content, each ROW's own id (R3: never its index — a reorder in the CMS keeps
|
|
3029
|
+
* every tab with its own panel). `rows` = the rows the tab list's Repeater resolved (null for a manual Tabs).
|
|
3030
|
+
*/
|
|
3031
|
+
function tabsScopeFor(root, rootCssId, rows, rowKeys = []) {
|
|
3032
|
+
const keys = rows ? rows.map((row, i) => rowItemKey(row, i)) : manualTabKeys(root);
|
|
3033
|
+
return tabsScope(compositeScopeId(rootCssId, rowKeys), tabsBehaviour(root), keys, tabsLabel(root));
|
|
3034
|
+
}
|
|
3035
|
+
/** The key a manual tab or panel renders with: its own key, else its index among its kind. */
|
|
3036
|
+
function tabPartKey(node, index) {
|
|
3037
|
+
return node.itemKey || `item-${index}`;
|
|
3038
|
+
}
|
|
3039
|
+
/**
|
|
3040
|
+
* The attributes a part of a Tabs writes on its own element (both engine paths, SSR included): the root's data
|
|
3041
|
+
* attributes (its scope id when it sits in repeated rows), the list's role and name, a tab's and a panel's state by
|
|
3042
|
+
* `key` (its own key, or — From content — its row's). Anything else writes nothing.
|
|
3043
|
+
*/
|
|
3044
|
+
function tabsPartAttributes(node, scope, key, rowScoped = false) {
|
|
3045
|
+
switch (node.role) {
|
|
3046
|
+
case TabsRole.Tabs: return tabsRootAttributes(scope.behaviour, rowScoped ? scope.rootId : undefined);
|
|
3047
|
+
case TabsRole.List: return tabListAttributes(scope.behaviour, scope.label);
|
|
3048
|
+
case TabsRole.Trigger: return key ? tabTriggerAttributes(tabsItemState(scope, key)) : {};
|
|
3049
|
+
case TabsRole.Panel: return key ? tabPanelAttributes(tabsItemState(scope, key)) : {};
|
|
3050
|
+
default: return {};
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
/**
|
|
3054
|
+
* The APG tabs keys on a tab (automatic activation): → / ← move along a horizontal list — mirrored under RTL — and ↓ /
|
|
3055
|
+
* ↑ along a vertical one; Home / End jump. Every move wraps. Enter / Space are the button's own.
|
|
3056
|
+
*/
|
|
3057
|
+
function tabsKeyAction(key, orientation, rtl = false) {
|
|
3058
|
+
if (key === 'Home')
|
|
3059
|
+
return AccordionKeyAction.First;
|
|
3060
|
+
if (key === 'End')
|
|
3061
|
+
return AccordionKeyAction.Last;
|
|
3062
|
+
if (orientation === TabsOrientation.Vertical) {
|
|
3063
|
+
if (key === 'ArrowDown')
|
|
3064
|
+
return AccordionKeyAction.Next;
|
|
3065
|
+
if (key === 'ArrowUp')
|
|
3066
|
+
return AccordionKeyAction.Previous;
|
|
3067
|
+
return null;
|
|
3068
|
+
}
|
|
3069
|
+
if (key === 'ArrowRight')
|
|
3070
|
+
return rtl ? AccordionKeyAction.Previous : AccordionKeyAction.Next;
|
|
3071
|
+
if (key === 'ArrowLeft')
|
|
3072
|
+
return rtl ? AccordionKeyAction.Next : AccordionKeyAction.Previous;
|
|
3073
|
+
return null;
|
|
3074
|
+
}
|
|
3075
|
+
// ── Items from content (R3, composite T39) ──────────────────────────────────────────────────────
|
|
3076
|
+
const SHORT_TEXT = new Set(['text']);
|
|
3077
|
+
const LONG_TEXT = new Set(['textarea', 'richText', 'editor']);
|
|
3078
|
+
const IMAGE_CONTROLLERS = new Set(['image', 'media', 'file']);
|
|
3079
|
+
/** R3 (board 3 "Tabs from content"): switching to From content binds the tab's words to the first short-text field,
|
|
3080
|
+
* the panel's words to the first long-text field and its picture to the first image field. ONE rule for the builder's
|
|
3081
|
+
* switch and the composer. */
|
|
3082
|
+
function pickTabsAutoBinds(fields) {
|
|
3083
|
+
const isString = (f) => !f.type || f.type === 'string';
|
|
3084
|
+
const label = fields.find(f => isString(f) && SHORT_TEXT.has(String(f.controller ?? 'text')))?.name ?? null;
|
|
3085
|
+
const text = fields.find(f => isString(f) && LONG_TEXT.has(String(f.controller ?? '')) && f.name !== label)?.name ?? null;
|
|
3086
|
+
const image = fields.find(f => IMAGE_CONTROLLERS.has(String(f.controller ?? '')) && f.name !== label && f.name !== text)?.name ?? null;
|
|
3087
|
+
return { label, text, image };
|
|
3088
|
+
}
|
|
3089
|
+
/** The bound props of a tab's words, a panel's words and its picture. */
|
|
3090
|
+
const TABS_LABEL_BIND_PROP = 'content';
|
|
3091
|
+
const TABS_TEXT_BIND_PROP = 'content';
|
|
3092
|
+
const TABS_IMAGE_BIND_PROP = 'src';
|
|
3093
|
+
/**
|
|
3094
|
+
* Manual → From content (board 9 "Items: Manual · From content"): the list becomes `listRepeater` and the panels
|
|
3095
|
+
* holder `panelsRepeater` (Repeater nodes the caller created with the same ids, roles, names, element styles and
|
|
3096
|
+
* styles — a type change is the consumer's). The FIRST tab and ITS panel become the templates (their parts, styles
|
|
3097
|
+
* and interactions kept; their keys dropped — a row's key is its id); every other pair is removed. Returns the removed
|
|
3098
|
+
* nodes.
|
|
3099
|
+
*/
|
|
3100
|
+
function tabsToContent(root, listRepeater, panelsRepeater) {
|
|
3101
|
+
const [tab, ...restTabs] = tabsTriggers(root);
|
|
3102
|
+
const panel = tab ? tabPanelOf(root, tab) : undefined;
|
|
3103
|
+
const restPanels = tabsPanels(root).filter(p => p !== panel);
|
|
3104
|
+
if (tab)
|
|
3105
|
+
delete tab.itemKey;
|
|
3106
|
+
if (panel)
|
|
3107
|
+
delete panel.itemKey;
|
|
3108
|
+
listRepeater.role = TabsRole.List;
|
|
3109
|
+
panelsRepeater.role = TabsRole.Panels;
|
|
3110
|
+
listRepeater.children = tab ? [tab] : [];
|
|
3111
|
+
panelsRepeater.children = panel ? [panel] : [];
|
|
3112
|
+
root.children = (root.children ?? []).map(c => (c.role === TabsRole.List ? listRepeater : c.role === TabsRole.Panels ? panelsRepeater : c));
|
|
3113
|
+
return [...restTabs, ...restPanels];
|
|
3114
|
+
}
|
|
3115
|
+
function writeRowIn(node, row, resolve) {
|
|
3116
|
+
walkComposite(node, n => {
|
|
3117
|
+
const bindings = n.bindings ?? [];
|
|
3118
|
+
for (const b of bindings) {
|
|
3119
|
+
const value = resolve(row, b);
|
|
3120
|
+
if (value !== undefined && value !== null)
|
|
3121
|
+
writeProp(n, b.prop, value);
|
|
3122
|
+
}
|
|
3123
|
+
if (bindings.length)
|
|
3124
|
+
n.bindings = [];
|
|
3125
|
+
});
|
|
3126
|
+
}
|
|
3127
|
+
/**
|
|
3128
|
+
* From content → Manual: the rows come in as static pairs — each a copy of the template tab and panel (`clone`, which
|
|
3129
|
+
* gives the copy fresh ids) whose bound words are written in as its own and whose bindings are dropped. `list` and
|
|
3130
|
+
* `panels` are the manual holders the caller created. Returns the new pairs.
|
|
3131
|
+
*/
|
|
3132
|
+
function tabsToManual(root, list, panels, rows, clone, resolve) {
|
|
3133
|
+
const [tabTemplate] = tabsTriggers(root);
|
|
3134
|
+
const panelTemplate = tabsPanels(root)[0];
|
|
3135
|
+
list.role = TabsRole.List;
|
|
3136
|
+
panels.role = TabsRole.Panels;
|
|
3137
|
+
const pairs = tabTemplate && panelTemplate ? rows.map((row, i) => {
|
|
3138
|
+
const tab = clone(tabTemplate);
|
|
3139
|
+
const panel = clone(panelTemplate);
|
|
3140
|
+
tab.itemKey = panel.itemKey = `k${i + 1}`;
|
|
3141
|
+
writeRowIn(tab, row, resolve);
|
|
3142
|
+
writeRowIn(panel, row, resolve);
|
|
3143
|
+
return { tab, panel };
|
|
3144
|
+
}) : [];
|
|
3145
|
+
list.children = pairs.map(p => p.tab);
|
|
3146
|
+
panels.children = pairs.map(p => p.panel);
|
|
3147
|
+
root.children = (root.children ?? []).map(c => (c.role === TabsRole.List ? list : c.role === TabsRole.Panels ? panels : c));
|
|
3148
|
+
return pairs;
|
|
3149
|
+
}
|
|
3150
|
+
|
|
3151
|
+
const KIND_ROOT = (kind) => `[${COMPOSITE_ATTRIBUTE}="${kind}"]`;
|
|
3152
|
+
const EXCLUSIVE_KINDS = Object.keys(EXCLUSIVE_KIND_ROLES);
|
|
3153
|
+
const ROOT_SELECTOR = EXCLUSIVE_KINDS.map(KIND_ROOT).join(', ');
|
|
3154
|
+
const roleSelector = (pick) => EXCLUSIVE_KINDS.map(k => EXCLUSIVE_KIND_ROLES[k][pick]).filter((r) => !!r).map(r => `.${roleClass(r)}`).join(', ');
|
|
3155
|
+
const LIST_SELECTOR = roleSelector('list');
|
|
3156
|
+
const TRIGGER_SELECTOR = roleSelector('trigger');
|
|
3157
|
+
const PANEL_SELECTOR = roleSelector('panel');
|
|
3158
|
+
const ITEM_PART_SELECTOR = `${TRIGGER_SELECTOR}, ${PANEL_SELECTOR}`;
|
|
3159
|
+
const TABLIST_SELECTOR = '[role="tablist"]';
|
|
3160
|
+
/** The exclusive composite an element belongs to (the element itself when it is a root). */
|
|
3161
|
+
function closestExclusive(el) {
|
|
3162
|
+
return el?.closest?.(ROOT_SELECTOR) ?? null;
|
|
3163
|
+
}
|
|
3164
|
+
/** The trigger or panel an element sits in (itself when it is one) — its ITEM part — or null. */
|
|
3165
|
+
function closestExclusiveItemPart(el) {
|
|
3166
|
+
return el?.closest?.(ITEM_PART_SELECTOR) ?? null;
|
|
3167
|
+
}
|
|
3168
|
+
const kindOf = (root) => {
|
|
3169
|
+
const raw = root.getAttribute(COMPOSITE_ATTRIBUTE);
|
|
3170
|
+
return isExclusiveKind(raw) ? raw : CompositeKind.Tabs;
|
|
3171
|
+
};
|
|
3172
|
+
const nodeIdOf = (el) => {
|
|
3173
|
+
for (const c of Array.from(el.classList))
|
|
3174
|
+
if (c.startsWith('iox-node-'))
|
|
3175
|
+
return c.slice('iox-node-'.length);
|
|
3176
|
+
return '';
|
|
3177
|
+
};
|
|
3178
|
+
function behaviourOf(root) {
|
|
3179
|
+
const on = root.getAttribute(ACTIVATE_ON_ATTRIBUTE);
|
|
3180
|
+
const orientation = root.getAttribute(ORIENTATION_ATTRIBUTE);
|
|
3181
|
+
return {
|
|
3182
|
+
activateOn: TABS_ACTIVATE_ONS.includes(on) ? on : TabsActivateOn.Click,
|
|
3183
|
+
orientation: TABS_ORIENTATIONS.includes(orientation) ? orientation : TabsOrientation.Horizontal,
|
|
3184
|
+
};
|
|
3185
|
+
}
|
|
3186
|
+
function setAttr(el, name, value) {
|
|
3187
|
+
if (!el)
|
|
3188
|
+
return;
|
|
3189
|
+
if (value === null) {
|
|
3190
|
+
if (el.hasAttribute(name))
|
|
3191
|
+
el.removeAttribute(name);
|
|
3192
|
+
}
|
|
3193
|
+
else if (el.getAttribute(name) !== value)
|
|
3194
|
+
el.setAttribute(name, value);
|
|
3195
|
+
}
|
|
3196
|
+
function dispatch(el, type) {
|
|
3197
|
+
const Ctor = globalThis.CustomEvent;
|
|
3198
|
+
if (el && Ctor)
|
|
3199
|
+
el.dispatchEvent(new Ctor(type, { bubbles: false }));
|
|
3200
|
+
}
|
|
3201
|
+
class ExclusiveRuntime {
|
|
3202
|
+
constructor(root, options = {}) {
|
|
3203
|
+
this.root = root;
|
|
3204
|
+
this.options = options;
|
|
3205
|
+
this.shown = null;
|
|
3206
|
+
this.observer = null;
|
|
3207
|
+
this.stampQueued = false;
|
|
3208
|
+
this.awaitingItems = false;
|
|
3209
|
+
this.onClick = (event) => {
|
|
3210
|
+
const trigger = this.ownTrigger(event.target);
|
|
3211
|
+
if (!trigger)
|
|
3212
|
+
return;
|
|
3213
|
+
event.preventDefault();
|
|
3214
|
+
this.activate(this.keyOf(trigger));
|
|
3215
|
+
};
|
|
3216
|
+
this.onHover = (event) => {
|
|
3217
|
+
if (this.behaviourValue.activateOn !== TabsActivateOn.Hover)
|
|
3218
|
+
return;
|
|
3219
|
+
const trigger = this.ownTrigger(event.target);
|
|
3220
|
+
if (trigger)
|
|
3221
|
+
this.activate(this.keyOf(trigger));
|
|
3222
|
+
};
|
|
3223
|
+
this.onKeydown = (event) => {
|
|
3224
|
+
const e = event;
|
|
3225
|
+
const trigger = this.ownTrigger(e.target);
|
|
3226
|
+
if (!trigger)
|
|
3227
|
+
return;
|
|
3228
|
+
const rtl = isRtl(trigger);
|
|
3229
|
+
const action = tabsKeyAction(e.key, this.behaviourValue.orientation, rtl);
|
|
3230
|
+
if (!action)
|
|
3231
|
+
return;
|
|
3232
|
+
const triggers = this.triggers();
|
|
3233
|
+
const target = triggers[accordionFocusIndex(action, triggers.indexOf(trigger), triggers.length)];
|
|
3234
|
+
if (!target)
|
|
3235
|
+
return;
|
|
3236
|
+
e.preventDefault();
|
|
3237
|
+
target.focus();
|
|
3238
|
+
this.activate(this.keyOf(target));
|
|
3239
|
+
};
|
|
3240
|
+
this.kind = kindOf(root);
|
|
3241
|
+
this.behaviourValue = options.behaviour ?? behaviourOf(root);
|
|
3242
|
+
root.setAttribute(COMPOSITE_ATTRIBUTE, this.kind);
|
|
3243
|
+
EXCLUSIVE_RUNTIMES.set(root, this);
|
|
3244
|
+
// The server rendered the first state (every trigger keyed, one on); with no server state the first is on.
|
|
3245
|
+
const triggers = this.triggers();
|
|
3246
|
+
const serverOn = !options.behaviour ? triggers.find(t => t.hasAttribute(ON_ATTRIBUTE) && t.hasAttribute(ITEM_KEY_ATTRIBUTE)) : undefined;
|
|
3247
|
+
this.live = serverOn ? this.keyOf(serverOn) : (triggers[0] ? this.keyOf(triggers[0]) : null);
|
|
3248
|
+
this.awaitingItems = !triggers.length;
|
|
3249
|
+
// Capture: on the builder canvas every part's own listener stops the click (it selects that part), and the tab
|
|
3250
|
+
// must still turn on — the canvas is a live runtime.
|
|
3251
|
+
root.addEventListener('click', this.onClick, true);
|
|
3252
|
+
root.addEventListener('mouseover', this.onHover, true);
|
|
3253
|
+
root.addEventListener('keydown', this.onKeydown);
|
|
3254
|
+
if (options.observe && typeof MutationObserver !== 'undefined') {
|
|
3255
|
+
this.observer = new MutationObserver(() => this.queueStamp());
|
|
3256
|
+
this.observer.observe(root, { childList: true, subtree: true });
|
|
3257
|
+
}
|
|
3258
|
+
this.stamp(true);
|
|
3259
|
+
}
|
|
3260
|
+
get behaviour() {
|
|
3261
|
+
return this.behaviourValue;
|
|
3262
|
+
}
|
|
3263
|
+
/** The behaviour changed in the panel (Activate on, Orientation). */
|
|
3264
|
+
setBehaviour(behaviour) {
|
|
3265
|
+
this.behaviourValue = behaviour;
|
|
3266
|
+
this.root.setAttribute(ACTIVATE_ON_ATTRIBUTE, behaviour.activateOn);
|
|
3267
|
+
this.root.setAttribute(ORIENTATION_ATTRIBUTE, behaviour.orientation);
|
|
3268
|
+
this.stamp();
|
|
3269
|
+
}
|
|
3270
|
+
/** THIS composite's triggers in document order (a nested composite's are its own). */
|
|
3271
|
+
triggers() {
|
|
3272
|
+
return this.own(TRIGGER_SELECTOR);
|
|
3273
|
+
}
|
|
3274
|
+
/** THIS composite's panels in document order. */
|
|
3275
|
+
panels() {
|
|
3276
|
+
return this.own(PANEL_SELECTOR);
|
|
3277
|
+
}
|
|
3278
|
+
/** The key on right now (forced ?? live). */
|
|
3279
|
+
get activeKey() {
|
|
3280
|
+
return this.shown;
|
|
3281
|
+
}
|
|
3282
|
+
/** The key the live state holds (before anything forced). */
|
|
3283
|
+
get liveKey() {
|
|
3284
|
+
return this.live;
|
|
3285
|
+
}
|
|
3286
|
+
/** The key of a trigger or a panel: its `data-iox-item-key`, else its index among its kind. */
|
|
3287
|
+
keyOf(part) {
|
|
3288
|
+
const own = part.getAttribute(ITEM_KEY_ATTRIBUTE);
|
|
3289
|
+
if (own)
|
|
3290
|
+
return own;
|
|
3291
|
+
const peers = part.matches(TRIGGER_SELECTOR) ? this.triggers() : this.panels();
|
|
3292
|
+
return `item-${peers.indexOf(part)}`;
|
|
3293
|
+
}
|
|
3294
|
+
/** The key of the item an element sits in (a part inside a tab or a panel), or null. */
|
|
3295
|
+
keyAt(el) {
|
|
3296
|
+
const part = closestExclusiveItemPart(el);
|
|
3297
|
+
return part && closestExclusive(part) === this.root ? this.keyOf(part) : null;
|
|
3298
|
+
}
|
|
3299
|
+
/** Turn `key` on (a click, a key, a hover, the `openItem` action, "Showing on canvas"). */
|
|
3300
|
+
activate(key) {
|
|
3301
|
+
if (!this.keys().includes(key))
|
|
3302
|
+
return;
|
|
3303
|
+
const changed = key !== this.live;
|
|
3304
|
+
this.live = key;
|
|
3305
|
+
this.stamp();
|
|
3306
|
+
if (changed)
|
|
3307
|
+
this.options.onChange?.(key);
|
|
3308
|
+
}
|
|
3309
|
+
/** Run an item action on `key`: Open turns it on; Close and Toggle do nothing (one item is always on). True when it
|
|
3310
|
+
* is on afterwards. */
|
|
3311
|
+
run(open, key) {
|
|
3312
|
+
if (open)
|
|
3313
|
+
this.activate(key);
|
|
3314
|
+
return this.shown === key;
|
|
3315
|
+
}
|
|
3316
|
+
/** The forced key changed (builder design state): show it. */
|
|
3317
|
+
refresh() {
|
|
3318
|
+
this.stamp();
|
|
3319
|
+
}
|
|
3320
|
+
destroy() {
|
|
3321
|
+
if (EXCLUSIVE_RUNTIMES.get(this.root) === this)
|
|
3322
|
+
EXCLUSIVE_RUNTIMES.delete(this.root);
|
|
3323
|
+
this.root.removeEventListener('click', this.onClick, true);
|
|
3324
|
+
this.root.removeEventListener('mouseover', this.onHover, true);
|
|
3325
|
+
this.root.removeEventListener('keydown', this.onKeydown);
|
|
3326
|
+
this.observer?.disconnect();
|
|
3327
|
+
this.observer = null;
|
|
3328
|
+
}
|
|
3329
|
+
keys() {
|
|
3330
|
+
return this.triggers().map(t => this.keyOf(t));
|
|
3331
|
+
}
|
|
3332
|
+
/** The element(s) that ARE the tablist: the ones a renderer already gave the role (the engine's component-path
|
|
3333
|
+
* Repeater puts it on its inner root, which holds the tabs), else the elements carrying the list's role class. */
|
|
3334
|
+
lists() {
|
|
3335
|
+
const stamped = this.own(TABLIST_SELECTOR);
|
|
3336
|
+
return stamped.length ? stamped : this.own(LIST_SELECTOR);
|
|
3337
|
+
}
|
|
3338
|
+
own(selector) {
|
|
3339
|
+
return Array.from(this.root.querySelectorAll(selector)).filter(el => closestExclusive(el.parentElement) === this.root);
|
|
3340
|
+
}
|
|
3341
|
+
queueStamp() {
|
|
3342
|
+
if (this.stampQueued)
|
|
3343
|
+
return;
|
|
3344
|
+
this.stampQueued = true;
|
|
3345
|
+
const run = () => { this.stampQueued = false; this.stamp(); };
|
|
3346
|
+
if (typeof queueMicrotask === 'function')
|
|
3347
|
+
queueMicrotask(run);
|
|
3348
|
+
else
|
|
3349
|
+
Promise.resolve().then(run);
|
|
3350
|
+
}
|
|
3351
|
+
rootId() {
|
|
3352
|
+
const own = this.options.rootId;
|
|
3353
|
+
return (typeof own === 'function' ? own() : own) ?? this.root.getAttribute(SCOPE_ATTRIBUTE) ?? nodeIdOf(this.root);
|
|
3354
|
+
}
|
|
3355
|
+
/** Write every part's state into its markup; announce what changed. */
|
|
3356
|
+
stamp(first = false) {
|
|
3357
|
+
const triggers = this.triggers();
|
|
3358
|
+
const keys = triggers.map(t => this.keyOf(t));
|
|
3359
|
+
if (this.awaitingItems && keys.length) {
|
|
3360
|
+
this.awaitingItems = false;
|
|
3361
|
+
this.live = keys[0];
|
|
3362
|
+
first = true;
|
|
3363
|
+
}
|
|
3364
|
+
if (this.live === null || !keys.includes(this.live))
|
|
3365
|
+
this.live = keys[0] ?? null;
|
|
3366
|
+
const forced = this.options.forced?.() ?? null;
|
|
3367
|
+
const next = forced !== null && keys.includes(forced) ? forced : this.live;
|
|
3368
|
+
const rootId = this.rootId();
|
|
3369
|
+
const vertical = this.behaviourValue.orientation === TabsOrientation.Vertical;
|
|
3370
|
+
for (const list of this.lists()) {
|
|
3371
|
+
setAttr(list, 'role', 'tablist');
|
|
3372
|
+
setAttr(list, 'aria-orientation', vertical ? 'vertical' : null);
|
|
3373
|
+
if (this.options.label)
|
|
3374
|
+
setAttr(list, 'aria-label', this.options.label() || null);
|
|
3375
|
+
}
|
|
3376
|
+
const panels = this.panels();
|
|
3377
|
+
const panelOf = (key) => panels.find(p => this.keyOf(p) === key) ?? null;
|
|
3378
|
+
for (const trigger of triggers) {
|
|
3379
|
+
const key = this.keyOf(trigger);
|
|
3380
|
+
const on = key === next;
|
|
3381
|
+
const ids = accordionDomIds(rootId, key, CompositeKind.Tabs);
|
|
3382
|
+
setAttr(trigger, ITEM_KEY_ATTRIBUTE, key);
|
|
3383
|
+
setAttr(trigger, 'type', trigger.tagName === 'BUTTON' ? 'button' : null);
|
|
3384
|
+
setAttr(trigger, 'role', 'tab');
|
|
3385
|
+
setAttr(trigger, 'id', ids.trigger);
|
|
3386
|
+
setAttr(trigger, 'aria-selected', on ? 'true' : 'false');
|
|
3387
|
+
setAttr(trigger, 'aria-controls', panelOf(key) ? ids.panel : null);
|
|
3388
|
+
setAttr(trigger, 'tabindex', on ? '0' : '-1');
|
|
3389
|
+
setAttr(trigger, ON_ATTRIBUTE, on ? '' : null);
|
|
3390
|
+
}
|
|
3391
|
+
for (const panel of panels) {
|
|
3392
|
+
const key = this.keyOf(panel);
|
|
3393
|
+
const on = key === next;
|
|
3394
|
+
const ids = accordionDomIds(rootId, key, CompositeKind.Tabs);
|
|
3395
|
+
setAttr(panel, ITEM_KEY_ATTRIBUTE, key);
|
|
3396
|
+
setAttr(panel, 'role', 'tabpanel');
|
|
3397
|
+
setAttr(panel, 'id', ids.panel);
|
|
3398
|
+
setAttr(panel, 'aria-labelledby', ids.trigger);
|
|
3399
|
+
setAttr(panel, 'tabindex', '0');
|
|
3400
|
+
setAttr(panel, ON_ATTRIBUTE, on ? '' : null);
|
|
3401
|
+
setAttr(panel, 'hidden', on ? null : '');
|
|
3402
|
+
}
|
|
3403
|
+
const was = first ? null : this.shown;
|
|
3404
|
+
this.shown = next;
|
|
3405
|
+
if (was === next && !first)
|
|
3406
|
+
return;
|
|
3407
|
+
if (was !== null) {
|
|
3408
|
+
dispatch(triggers.find(t => this.keyOf(t) === was), ITEM_OFF_EVENT);
|
|
3409
|
+
dispatch(panelOf(was), ITEM_OFF_EVENT);
|
|
3410
|
+
}
|
|
3411
|
+
if (next !== null) {
|
|
3412
|
+
dispatch(triggers.find(t => this.keyOf(t) === next), ITEM_ON_EVENT);
|
|
3413
|
+
dispatch(panelOf(next), ITEM_ON_EVENT);
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
ownTrigger(target) {
|
|
3417
|
+
const trigger = target?.closest?.(TRIGGER_SELECTOR);
|
|
3418
|
+
return trigger && closestExclusive(trigger.parentElement) === this.root ? trigger : null;
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
/** The direction the tab is laid out in — the computed `direction` (a page's own CSS included), else its nearest `dir`. */
|
|
3422
|
+
function isRtl(el) {
|
|
3423
|
+
let computed = '';
|
|
3424
|
+
try {
|
|
3425
|
+
computed = el.ownerDocument?.defaultView?.getComputedStyle(el).direction ?? '';
|
|
3426
|
+
}
|
|
3427
|
+
catch {
|
|
3428
|
+
computed = '';
|
|
3429
|
+
}
|
|
3430
|
+
if (computed)
|
|
3431
|
+
return computed === 'rtl';
|
|
3432
|
+
return el.closest('[dir]')?.getAttribute('dir') === 'rtl';
|
|
3433
|
+
}
|
|
3434
|
+
/** Every live exclusive runtime by its root — so an item action finds the runtime of whatever item it targets. */
|
|
3435
|
+
const EXCLUSIVE_RUNTIMES = new WeakMap();
|
|
3436
|
+
/** The runtime running an exclusive root, or null. */
|
|
3437
|
+
function exclusiveRuntimeOf(root) {
|
|
3438
|
+
return (root && EXCLUSIVE_RUNTIMES.get(root)) || null;
|
|
3439
|
+
}
|
|
3440
|
+
/** Attach the runtime to one rendered exclusive root, unless it already runs one. Returns the NEW runtime, or null. */
|
|
3441
|
+
function attachExclusiveAt(root, options = {}) {
|
|
3442
|
+
if (EXCLUSIVE_RUNTIMES.has(root))
|
|
3443
|
+
return null;
|
|
3444
|
+
return new ExclusiveRuntime(root, options);
|
|
3445
|
+
}
|
|
3446
|
+
/** True when `el` is (or sits in) an exclusive item part — a tab or a panel. */
|
|
3447
|
+
function isExclusiveItemOn(el) {
|
|
3448
|
+
return !!closestExclusiveItemPart(el)?.hasAttribute(ON_ATTRIBUTE);
|
|
3449
|
+
}
|
|
3450
|
+
/** Subscribe to the item part `el` sits in turning on / off. Returns the unsubscribe. */
|
|
3451
|
+
function onExclusiveItemState(el, on, handler) {
|
|
3452
|
+
const part = closestExclusiveItemPart(el);
|
|
3453
|
+
if (!part)
|
|
3454
|
+
return () => undefined;
|
|
3455
|
+
const type = on ? ITEM_ON_EVENT : ITEM_OFF_EVENT;
|
|
3456
|
+
part.addEventListener(type, handler);
|
|
3457
|
+
return () => part.removeEventListener(type, handler);
|
|
3458
|
+
}
|
|
3459
|
+
|
|
3460
|
+
/** Attach the runtime a rendered composite root needs (its `data-iox-composite`), unless it runs one already. Returns
|
|
3461
|
+
* the NEW runtime, or null. */
|
|
3462
|
+
function attachCompositeAt(root, options = {}) {
|
|
3463
|
+
const kind = root.getAttribute(COMPOSITE_ATTRIBUTE);
|
|
3464
|
+
if (isExclusiveKind(kind))
|
|
3465
|
+
return attachExclusiveAt(root, { observe: options.observe });
|
|
3466
|
+
return attachAccordionAt(root, { observe: options.observe, reducedMotion: options.reducedMotion });
|
|
3467
|
+
}
|
|
3468
|
+
/** The nearer of the item elements `el` sits in — an accordion item / a disclosure, or a tab / a tab panel. */
|
|
3469
|
+
function nearestItem(el) {
|
|
3470
|
+
const toggle = closestAccordionItem(el);
|
|
3471
|
+
const exclusive = closestExclusiveItemPart(el);
|
|
3472
|
+
if (!toggle && !exclusive)
|
|
3473
|
+
return null;
|
|
3474
|
+
if (toggle && (!exclusive || exclusive.contains(toggle)))
|
|
3475
|
+
return { el: toggle, exclusive: false };
|
|
3476
|
+
return { el: exclusive, exclusive: true };
|
|
3477
|
+
}
|
|
3478
|
+
/** Subscribe to the item `el` sits in turning on / off — what the `itemOn` / `itemOff` interaction triggers listen to,
|
|
3479
|
+
* in both families. Returns the unsubscribe. */
|
|
3480
|
+
function onCompositeItemState(el, on, handler) {
|
|
3481
|
+
const item = nearestItem(el);
|
|
3482
|
+
if (!item)
|
|
3483
|
+
return () => undefined;
|
|
3484
|
+
return item.exclusive ? onExclusiveItemState(item.el, on, handler) : onAccordionItemState(item.el, on, handler);
|
|
3485
|
+
}
|
|
3486
|
+
/** True when the item `el` sits in is on now (an open accordion item, the active tab or its panel). */
|
|
3487
|
+
function isCompositeItemOn(el) {
|
|
3488
|
+
const item = nearestItem(el);
|
|
3489
|
+
if (!item)
|
|
3490
|
+
return false;
|
|
3491
|
+
return item.exclusive ? isExclusiveItemOn(item.el) : isAccordionItemOn(item.el);
|
|
3492
|
+
}
|
|
3493
|
+
/**
|
|
3494
|
+
* Run an item action on the element of an ITEM — an accordion item, a disclosure, or a tab (CC4a: `openItem` turns it
|
|
3495
|
+
* on; a Tabs always shows one, so Close and Toggle leave it) — the `openItem` / `closeItem` / `toggleItem` interaction
|
|
3496
|
+
* actions, on the canvas and on the live site through this one function. Returns whether the item shows on
|
|
3497
|
+
* afterwards, or null when `el` is in no running composite.
|
|
3498
|
+
*/
|
|
3499
|
+
function runCompositeItemAction(el, action) {
|
|
3500
|
+
const item = nearestItem(el);
|
|
3501
|
+
if (!item)
|
|
3502
|
+
return null;
|
|
3503
|
+
if (item.exclusive) {
|
|
3504
|
+
const runtime = exclusiveRuntimeOf(closestExclusive(item.el));
|
|
3505
|
+
return runtime ? runtime.run(action === CompositeItemAction.Open, runtime.keyOf(item.el)) : null;
|
|
3506
|
+
}
|
|
3507
|
+
const runtime = accordionRuntimeOf(accordionRootOfItem(item.el));
|
|
3508
|
+
return runtime ? runtime.run(action, runtime.keyOf(item.el)) : null;
|
|
3509
|
+
}
|
|
1892
3510
|
|
|
1893
3511
|
/**
|
|
1894
3512
|
* @vectoriox/iox-ui/composite-core — composites built from the builder's own parts (composite-components
|
|
1895
3513
|
* plan §3a). FRAMEWORK-FREE: the builder, the SSR engine and iox-cms (generation-core's composer, the site
|
|
1896
3514
|
* build) read the same recipes, roles and structure rules. Card v2 + Badge / Chip (T23 / T25) are the
|
|
1897
3515
|
* first; the Accordion (CC2) adds the first composite RUNTIME (accordion-runtime.ts — DOM only, no
|
|
1898
|
-
* framework), which the Disclosure (CC2.1) runs too — a one-item accordion whose root is its item; Tabs
|
|
1899
|
-
*
|
|
3516
|
+
* framework), which the Disclosure (CC2.1) runs too — a one-item accordion whose root is its item; Tabs (CC4a) run the
|
|
3517
|
+
* EXCLUSIVE runtime (exclusive-runtime.ts — one item always on); composite-runtime.ts is the one door to both.
|
|
1900
3518
|
* ⚠️ Consumers wire the usual trio — ng-package raw-ts assets, jest moduleNameMapper, tsconfig paths.
|
|
1901
3519
|
*/
|
|
1902
3520
|
|
|
@@ -1904,5 +3522,5 @@ function isLockedPart(node) {
|
|
|
1904
3522
|
* Generated bundle index. Do not edit.
|
|
1905
3523
|
*/
|
|
1906
3524
|
|
|
1907
|
-
export { ACCORDION_BIND_PROP, ACCORDION_CONTENT_LIMIT, ACCORDION_DEFAULT_ONS, ACCORDION_ICONS, ACCORDION_ICON_GLYPH, ACCORDION_NEW_ITEM_WORDS, ACCORDION_WORDS, AccordionClass, AccordionDefaultOn, AccordionEditRefusal, AccordionIcon, AccordionKeyAction, AccordionRole, AccordionRuntime, BADGE_TONES, BADGE_TONE_ICONS, BadgeTone, CARD_LAYOUTS, CARD_LAYOUT_CUSTOM, CARD_PARTS, CARD_PART_ROLE, CARD_STACK_BREAKPOINT, CARD_WORDS, CHIP_ICON, CHIP_STYLE, COMPOSITE_ATTRIBUTE, COMPOSITE_KIND_ROLES, COMPOSITE_ROLE_SPECS, CardLayout, CardPart, CardRole, CompositeBehaviourTrait, CompositeItemAction, CompositeKind, DEFAULT_ON_ATTRIBUTE, DISCLOSURE_ITEM_KEY, DISCLOSURE_WORDS, DisclosureRole, ITEM_KEY_ATTRIBUTE, ITEM_OFF_EVENT, ITEM_ON_EVENT, ITEM_OPEN_STATE, MULTIPLE_ATTRIBUTE, ON_ATTRIBUTE, ON_STATE, PILL_LABEL_STYLE, SCOPE_ATTRIBUTE, StructureEdit, StructureRefusal, UNTIL_FOUND, accordionAnswerOf, accordionBehaviour, accordionDomIds, accordionFocusIndex, accordionHeadingLevel, accordionItemAttributes, accordionItemLabel, accordionItemOf, accordionItemState, accordionItems, accordionKeyAction, accordionPanelAttributes, accordionPanelOf, accordionPartStyle, accordionQuestionOf, accordionRegionAttributes, accordionRemovalRefusal, accordionRootAttributes, accordionRootOf, accordionRuntimeOf, accordionScope, accordionToContent, accordionToManual, accordionTrackAttributes, accordionTriggerAttributes, accordionTriggerOf, addAccordionItem, applyCardLayout, attachAccordionAt, attachAccordions, badgeStyleId, badgeToneOf, buildAccordionItem, buildAccordionRecipe, buildBadge, buildCardRecipe, buildChip, buildCompositeIcon, buildDisclosureRecipe, cardPartsOf, cardRootOf, cardTitleOf, clearLayerStyles, closeAccordionItem, closestAccordion, closestAccordionItem, compositeItemOf, compositeItems, compositeKindOfRole, compositeRootOf, compositeRowKeys, compositeScopeId, detachAccordion, detectAccordionIcon, detectCardLayout, disclosureOpenByDefault, domSafeKey, findByRole, hasLabelledRegion, initialAccordionOpen, isAccordionFromContent, isAccordionItemOn, isAccordionPart, isAccordionRoot, isCardLinked, isCardRoot, isCompositeItem, isCompositePanel, isCompositeRoot, isCompositeTrigger, isDisclosurePart, isDisclosureRoot, isLockedPart, isPillNode, itemOpenTriggerIds, makeCompositePart, moveAccordionItem, nextAccordionState, nextItemKey, onAccordionItemState, openAccordionItem, parentInComposite, pickAccordionAutoBinds, readLayerStyle, readProp, readStyle, removeFromComposite, renameAccordionItem, rowItemKey, runCompositeItemAction, serializeDomAttributes, setAccordionBehaviour, setAccordionIcon, setBadgeTone, setCardLinked, setCardPart, shouldScrollToItem, structureRefusal, walkComposite, writeLayerStyles, writeProp, writeStyles };
|
|
3525
|
+
export { ACCORDION_BIND_PROP, ACCORDION_CONTENT_LIMIT, ACCORDION_DEFAULT_ONS, ACCORDION_ICONS, ACCORDION_ICON_GLYPH, ACCORDION_NEW_ITEM_WORDS, ACCORDION_WORDS, ACTIVATE_ON_ATTRIBUTE, AccordionClass, AccordionDefaultOn, AccordionEditRefusal, AccordionIcon, AccordionKeyAction, AccordionRole, AccordionRuntime, BADGE_TONES, BADGE_TONE_ICONS, BadgeTone, CARD_LAYOUTS, CARD_LAYOUT_CUSTOM, CARD_PARTS, CARD_PART_ROLE, CARD_STACK_BREAKPOINT, CARD_WORDS, CAROUSEL_QUOTES, CHIP_ICON, CHIP_STYLE, COMPOSITE_ATTRIBUTE, COMPOSITE_KIND_ROLES, COMPOSITE_ROLE_SPECS, CardLayout, CardPart, CardRole, CarouselA11y, CompositeBehaviourTrait, CompositeItemAction, CompositeKind, DEFAULT_ON_ATTRIBUTE, DISCLOSURE_ITEM_KEY, DISCLOSURE_WORDS, DisclosureRole, EXCLUSIVE_KIND_ROLES, ExclusiveRuntime, ITEM_KEY_ATTRIBUTE, ITEM_OFF_EVENT, ITEM_ON_EVENT, ITEM_OPEN_STATE, LIGHTBOX_ARROWS_HIDE_AT, LIGHTBOX_LABEL, MULTIPLE_ATTRIBUTE, NO_ELEMENT_STYLE, ON_ATTRIBUTE, ON_STATE, ORIENTATION_ATTRIBUTE, PILL_LABEL_STYLE, ROW_INDEX_ATTRIBUTE, SCOPE_ATTRIBUTE, SLIDER_NAME_TRAIT, SLIDER_PART_GLYPH, SLIDER_PART_ROLES, SLIDER_PLAY_GLYPH, SLIDER_TYPE, SLIDER_WORDS, SLIDE_TO_ROW, SLIDE_TYPE, SWIPER_THEME_COLOR, SliderFlag, SliderRole, StructureEdit, StructureRefusal, TABS_ACTIVATE_ONS, TABS_CONTENT_LIMIT, TABS_IMAGE_BIND_PROP, TABS_LABEL_BIND_PROP, TABS_NEW_TAB_WORDS, TABS_ORIENTATIONS, TABS_STACK_BREAKPOINT, TABS_TEXT_BIND_PROP, TABS_WORDS, TabsActivateOn, TabsBehaviourTrait, TabsEditRefusal, TabsOrientation, TabsRole, UNTIL_FOUND, acceptsRefusal, accordionAnswerOf, accordionBehaviour, accordionDomIds, accordionFocusIndex, accordionHeadingLevel, accordionItemAttributes, accordionItemLabel, accordionItemOf, accordionItemState, accordionItems, accordionKeyAction, accordionPanelAttributes, accordionPanelOf, accordionPartStyle, accordionQuestionOf, accordionRegionAttributes, accordionRemovalRefusal, accordionRootAttributes, accordionRootOf, accordionRootOfItem, accordionRuntimeOf, accordionScope, accordionToContent, accordionToManual, accordionTrackAttributes, accordionTriggerAttributes, accordionTriggerOf, addAccordionItem, addTabPair, applyCardLayout, attachAccordionAt, attachAccordions, attachCarouselA11y, attachCompositeAt, attachExclusiveAt, badgeStyleId, badgeToneOf, buildAccordionItem, buildAccordionRecipe, buildBadge, buildCardRecipe, buildCarouselRecipe, buildChip, buildCompositeIcon, buildDisclosureRecipe, buildLightboxRecipe, buildSliderPart, buildTabPair, buildTabsRecipe, canOpenAtClickedItem, cardPartsOf, cardRootOf, cardTitleOf, carouselOf, clearLayerStyles, closeAccordionItem, closestAccordion, closestAccordionItem, closestExclusive, closestExclusiveItemPart, compositeItemOf, compositeItems, compositeKindOfRole, compositeRootOf, compositeRowKeys, compositeScopeId, detachAccordion, detectAccordionIcon, detectCardLayout, disclosureOpenByDefault, domSafeKey, exclusiveItemPartOf, exclusiveRootOf, exclusiveRuntimeOf, findByRole, hasLabelledRegion, hasSliderParts, initialAccordionOpen, isAccordionFromContent, isAccordionItemOn, isAccordionPart, isAccordionRoot, isAnyCompositeRoot, isCardLinked, isCardRoot, isCompositeItem, isCompositeItemOn, isCompositePanel, isCompositeRoot, isCompositeTrigger, isDisclosurePart, isDisclosureRoot, isExclusiveItemOn, isExclusiveItemPart, isExclusiveKind, isExclusiveList, isExclusivePanel, isExclusivePanels, isExclusiveRoot, isExclusiveTrigger, isLockedPart, isPillNode, isSlider, isSliderPart, isTabsFromContent, isTabsPart, isTabsRoot, isToggleKind, itemOpenTriggerIds, lightboxSliderOf, makeCompositePart, manualTabKeys, migrateSliderControls, moveAccordionItem, moveTabPair, needsSliderMigration, nextAccordionState, nextItemKey, nextTabKey, onAccordionItemState, onCompositeItemState, onExclusiveItemState, openAccordionItem, parentInComposite, pickAccordionAutoBinds, pickTabsAutoBinds, readLayerStyle, readProp, readStyle, removeFromComposite, removeTabPair, renameAccordionItem, renameTab, repeatSourceOf, rowIndexOf, rowItemKey, rowSourceOf, runCompositeItemAction, sameRepeatSource, serializeDomAttributes, setAccordionBehaviour, setAccordionIcon, setBadgeTone, setCardLinked, setCardPart, setSliderFlag, setTabsBehaviour, shouldScrollToItem, slideIndexForRow, sliderLiveRegion, sliderPart, sliderPartElement, sliderPauseAttributes, sliderPauseGlyph, sliderSlides, sliderWords, splitSliderChildren, structureRefusal, syncSliderParts, tabLabel, tabLabelNodeOf, tabListAttributes, tabPanelAttributes, tabPanelImageOf, tabPanelOf, tabPanelTextOf, tabPartKey, tabTriggerAttributes, tabTriggerOf, tabsBehaviour, tabsItemState, tabsItemTriggerIds, tabsKeyAction, tabsLabel, tabsListOf, tabsPanels, tabsPanelsOf, tabsPartAttributes, tabsPartnerOf, tabsRemovalRefusal, tabsRootAttributes, tabsScope, tabsScopeFor, tabsToContent, tabsToManual, tabsTriggers, walkComposite, wantedSliderParts, writeLayerStyles, writeProp, writeStyles };
|
|
1908
3526
|
//# sourceMappingURL=vectoriox-iox-ui-composite-core.mjs.map
|