@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
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {
|
|
2
|
+
accordionRootOfItem, accordionRuntimeOf, AccordionRuntime, AccordionRuntimeOptions, attachAccordionAt, closestAccordionItem, CompositeItemAction,
|
|
3
|
+
isAccordionItemOn, onAccordionItemState,
|
|
4
|
+
} from './accordion-runtime';
|
|
5
|
+
import { COMPOSITE_ATTRIBUTE } from './accordion';
|
|
6
|
+
import { CompositeKind, isExclusiveKind } from './composite-kinds';
|
|
7
|
+
import {
|
|
8
|
+
attachExclusiveAt, closestExclusive, closestExclusiveItemPart, ExclusiveRuntime, exclusiveRuntimeOf, ExclusiveRuntimeOptions, isExclusiveItemOn,
|
|
9
|
+
onExclusiveItemState,
|
|
10
|
+
} from './exclusive-runtime';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The composites' runtimes behind ONE door (CC4a): the open/close runtime (Accordion, Disclosure) and the exclusive
|
|
14
|
+
* runtime (Tabs; the Segmented control in CC4b). The engine attaches whichever a rendered root needs; the interaction
|
|
15
|
+
* engines (builder canvas and live site) ask these for `itemOn` / `itemOff` and run the item actions through them —
|
|
16
|
+
* never a family's own function, so a new kind joins without touching either engine.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export type CompositeRuntime = AccordionRuntime | ExclusiveRuntime;
|
|
20
|
+
|
|
21
|
+
/** Attach the runtime a rendered composite root needs (its `data-iox-composite`), unless it runs one already. Returns
|
|
22
|
+
* the NEW runtime, or null. */
|
|
23
|
+
export function attachCompositeAt(root: HTMLElement, options: Omit<AccordionRuntimeOptions, 'rootId' | 'behaviour'> & Omit<ExclusiveRuntimeOptions, 'behaviour'> = {}): CompositeRuntime | null {
|
|
24
|
+
const kind = root.getAttribute(COMPOSITE_ATTRIBUTE) as CompositeKind | null;
|
|
25
|
+
if (isExclusiveKind(kind)) return attachExclusiveAt(root, { observe: options.observe });
|
|
26
|
+
return attachAccordionAt(root, { observe: options.observe, reducedMotion: options.reducedMotion });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The nearer of the item elements `el` sits in — an accordion item / a disclosure, or a tab / a tab panel. */
|
|
30
|
+
function nearestItem(el: Element | null | undefined): { el: HTMLElement; exclusive: boolean } | null {
|
|
31
|
+
const toggle = closestAccordionItem(el);
|
|
32
|
+
const exclusive = closestExclusiveItemPart(el);
|
|
33
|
+
if (!toggle && !exclusive) return null;
|
|
34
|
+
if (toggle && (!exclusive || exclusive.contains(toggle))) return { el: toggle, exclusive: false };
|
|
35
|
+
return { el: exclusive!, exclusive: true };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Subscribe to the item `el` sits in turning on / off — what the `itemOn` / `itemOff` interaction triggers listen to,
|
|
39
|
+
* in both families. Returns the unsubscribe. */
|
|
40
|
+
export function onCompositeItemState(el: Element, on: boolean, handler: () => void): () => void {
|
|
41
|
+
const item = nearestItem(el);
|
|
42
|
+
if (!item) return () => undefined;
|
|
43
|
+
return item.exclusive ? onExclusiveItemState(item.el, on, handler) : onAccordionItemState(item.el, on, handler);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** True when the item `el` sits in is on now (an open accordion item, the active tab or its panel). */
|
|
47
|
+
export function isCompositeItemOn(el: Element): boolean {
|
|
48
|
+
const item = nearestItem(el);
|
|
49
|
+
if (!item) return false;
|
|
50
|
+
return item.exclusive ? isExclusiveItemOn(item.el) : isAccordionItemOn(item.el);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Run an item action on the element of an ITEM — an accordion item, a disclosure, or a tab (CC4a: `openItem` turns it
|
|
55
|
+
* on; a Tabs always shows one, so Close and Toggle leave it) — the `openItem` / `closeItem` / `toggleItem` interaction
|
|
56
|
+
* actions, on the canvas and on the live site through this one function. Returns whether the item shows on
|
|
57
|
+
* afterwards, or null when `el` is in no running composite.
|
|
58
|
+
*/
|
|
59
|
+
export function runCompositeItemAction(el: Element | null | undefined, action: CompositeItemAction): boolean | null {
|
|
60
|
+
const item = nearestItem(el);
|
|
61
|
+
if (!item) return null;
|
|
62
|
+
if (item.exclusive) {
|
|
63
|
+
const runtime = exclusiveRuntimeOf(closestExclusive(item.el));
|
|
64
|
+
return runtime ? runtime.run(action === CompositeItemAction.Open, runtime.keyOf(item.el)) : null;
|
|
65
|
+
}
|
|
66
|
+
const runtime = accordionRuntimeOf(accordionRootOfItem(item.el));
|
|
67
|
+
return runtime ? runtime.run(action, runtime.keyOf(item.el)) : null;
|
|
68
|
+
}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { roleClass } from '@vectoriox/iox-ui/design-core';
|
|
2
|
+
|
|
3
|
+
import { accordionDomIds, accordionFocusIndex, COMPOSITE_ATTRIBUTE, ITEM_KEY_ATTRIBUTE, ON_ATTRIBUTE, SCOPE_ATTRIBUTE } from './accordion';
|
|
4
|
+
import { ITEM_OFF_EVENT, ITEM_ON_EVENT } from './accordion-runtime';
|
|
5
|
+
import { CompositeKind, EXCLUSIVE_KIND_ROLES, ExclusiveCompositeKind, isExclusiveKind } from './composite-kinds';
|
|
6
|
+
import {
|
|
7
|
+
ACTIVATE_ON_ATTRIBUTE, ORIENTATION_ATTRIBUTE, TABS_ACTIVATE_ONS, TABS_ORIENTATIONS, TabsActivateOn, TabsBehaviour, tabsKeyAction, TabsOrientation,
|
|
8
|
+
} from './tabs';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The EXCLUSIVE composites' RUNTIME (CC4a Tabs; the Segmented control runs it too in CC4b) — ONE copy, DOM only, for
|
|
12
|
+
* the builder canvas and the live site (both engine paths render the same markup from `tabs.ts`'s attribute planners):
|
|
13
|
+
*
|
|
14
|
+
* [root] data-iox-composite="tabs" data-iox-activate-on data-iox-orientation [data-iox-scope]
|
|
15
|
+
* .iox-role-tabs-list role=tablist [aria-orientation=vertical]
|
|
16
|
+
* .iox-role-tabs-trigger role=tab id aria-selected aria-controls tabindex 0|-1 data-iox-item-key [data-iox-on]
|
|
17
|
+
* .iox-role-tabs-panels
|
|
18
|
+
* .iox-role-tabs-panel role=tabpanel id aria-labelledby tabindex=0 data-iox-item-key [hidden] | [data-iox-on]
|
|
19
|
+
*
|
|
20
|
+
* Exactly one item is on, always (a Tabs with tabs never shows none). It owns: a click on a trigger turns its item on
|
|
21
|
+
* (and a hover, when the root says `activate-on=hover`); the APG tabs keys with AUTOMATIC activation — ← → along a
|
|
22
|
+
* horizontal list (mirrored under RTL), ↑ ↓ along a vertical one, Home / End — moving focus AND turning the item on,
|
|
23
|
+
* the roving tabindex keeping one trigger in the Tab order. A trigger and its panel pair by `data-iox-item-key`
|
|
24
|
+
* (else their index among their kind — the builder's From-content rows). Every change dispatches `iox-item-on` /
|
|
25
|
+
* `iox-item-off` on the trigger AND its panel (the `itemOn` / `itemOff` interaction triggers). The item shown is
|
|
26
|
+
* FORCED ?? LIVE: the builder forces one (a selection inside an inactive panel); the live site forces nothing. The
|
|
27
|
+
* server's first state is trusted (no flash).
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
export interface ExclusiveRuntimeOptions {
|
|
31
|
+
/** The scope id the ARIA ids are built from (else `data-iox-scope`, else the root's `iox-node-<id>`). A function is
|
|
32
|
+
* read at every stamp (the builder canvas: a preview row's index is known once it is in the page). */
|
|
33
|
+
rootId?: string | (() => string);
|
|
34
|
+
/** The behaviour (else read from the root's data attributes). */
|
|
35
|
+
behaviour?: TabsBehaviour;
|
|
36
|
+
/** The list's accessible name, stamped by the runtime (the builder canvas; the engine renders it). */
|
|
37
|
+
label?: () => string;
|
|
38
|
+
/** The key forced on, on top of the live state (the builder's design state), or null. */
|
|
39
|
+
forced?: () => string | null;
|
|
40
|
+
/** The live item changed (a click, a key, a hover, an action) — the builder's "Showing on canvas" follows it. */
|
|
41
|
+
onChange?: (key: string) => void;
|
|
42
|
+
/** Re-stamp when the markup under the root changes (the builder canvas re-renders parts). */
|
|
43
|
+
observe?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const KIND_ROOT = (kind: ExclusiveCompositeKind) => `[${COMPOSITE_ATTRIBUTE}="${kind}"]`;
|
|
47
|
+
const EXCLUSIVE_KINDS = Object.keys(EXCLUSIVE_KIND_ROLES) as ExclusiveCompositeKind[];
|
|
48
|
+
const ROOT_SELECTOR = EXCLUSIVE_KINDS.map(KIND_ROOT).join(', ');
|
|
49
|
+
const roleSelector = (pick: 'list' | 'trigger' | 'panel') =>
|
|
50
|
+
EXCLUSIVE_KINDS.map(k => EXCLUSIVE_KIND_ROLES[k][pick]).filter((r): r is string => !!r).map(r => `.${roleClass(r)}`).join(', ');
|
|
51
|
+
const LIST_SELECTOR = roleSelector('list');
|
|
52
|
+
const TRIGGER_SELECTOR = roleSelector('trigger');
|
|
53
|
+
const PANEL_SELECTOR = roleSelector('panel');
|
|
54
|
+
const ITEM_PART_SELECTOR = `${TRIGGER_SELECTOR}, ${PANEL_SELECTOR}`;
|
|
55
|
+
const TABLIST_SELECTOR = '[role="tablist"]';
|
|
56
|
+
|
|
57
|
+
/** The exclusive composite an element belongs to (the element itself when it is a root). */
|
|
58
|
+
export function closestExclusive(el: Element | null | undefined): HTMLElement | null {
|
|
59
|
+
return (el?.closest?.(ROOT_SELECTOR) as HTMLElement | null) ?? null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The trigger or panel an element sits in (itself when it is one) — its ITEM part — or null. */
|
|
63
|
+
export function closestExclusiveItemPart(el: Element | null | undefined): HTMLElement | null {
|
|
64
|
+
return (el?.closest?.(ITEM_PART_SELECTOR) as HTMLElement | null) ?? null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const kindOf = (root: Element): ExclusiveCompositeKind => {
|
|
68
|
+
const raw = root.getAttribute(COMPOSITE_ATTRIBUTE) as CompositeKind | null;
|
|
69
|
+
return isExclusiveKind(raw) ? raw : CompositeKind.Tabs;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const nodeIdOf = (el: Element): string => {
|
|
73
|
+
for (const c of Array.from(el.classList)) if (c.startsWith('iox-node-')) return c.slice('iox-node-'.length);
|
|
74
|
+
return '';
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
function behaviourOf(root: Element): TabsBehaviour {
|
|
78
|
+
const on = root.getAttribute(ACTIVATE_ON_ATTRIBUTE);
|
|
79
|
+
const orientation = root.getAttribute(ORIENTATION_ATTRIBUTE);
|
|
80
|
+
return {
|
|
81
|
+
activateOn: TABS_ACTIVATE_ONS.includes(on as TabsActivateOn) ? on as TabsActivateOn : TabsActivateOn.Click,
|
|
82
|
+
orientation: TABS_ORIENTATIONS.includes(orientation as TabsOrientation) ? orientation as TabsOrientation : TabsOrientation.Horizontal,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function setAttr(el: Element | null | undefined, name: string, value: string | null): void {
|
|
87
|
+
if (!el) return;
|
|
88
|
+
if (value === null) { if (el.hasAttribute(name)) el.removeAttribute(name); }
|
|
89
|
+
else if (el.getAttribute(name) !== value) el.setAttribute(name, value);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function dispatch(el: Element | null | undefined, type: string): void {
|
|
93
|
+
const Ctor = (globalThis as { CustomEvent?: typeof CustomEvent }).CustomEvent;
|
|
94
|
+
if (el && Ctor) el.dispatchEvent(new Ctor(type, { bubbles: false }));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export class ExclusiveRuntime {
|
|
98
|
+
private live: string | null;
|
|
99
|
+
private shown: string | null = null;
|
|
100
|
+
private observer: MutationObserver | null = null;
|
|
101
|
+
private stampQueued = false;
|
|
102
|
+
private behaviourValue: TabsBehaviour;
|
|
103
|
+
private awaitingItems = false;
|
|
104
|
+
readonly kind: ExclusiveCompositeKind;
|
|
105
|
+
|
|
106
|
+
constructor(readonly root: HTMLElement, private readonly options: ExclusiveRuntimeOptions = {}) {
|
|
107
|
+
this.kind = kindOf(root);
|
|
108
|
+
this.behaviourValue = options.behaviour ?? behaviourOf(root);
|
|
109
|
+
root.setAttribute(COMPOSITE_ATTRIBUTE, this.kind);
|
|
110
|
+
EXCLUSIVE_RUNTIMES.set(root, this);
|
|
111
|
+
// The server rendered the first state (every trigger keyed, one on); with no server state the first is on.
|
|
112
|
+
const triggers = this.triggers();
|
|
113
|
+
const serverOn = !options.behaviour ? triggers.find(t => t.hasAttribute(ON_ATTRIBUTE) && t.hasAttribute(ITEM_KEY_ATTRIBUTE)) : undefined;
|
|
114
|
+
this.live = serverOn ? this.keyOf(serverOn) : (triggers[0] ? this.keyOf(triggers[0]) : null);
|
|
115
|
+
this.awaitingItems = !triggers.length;
|
|
116
|
+
// Capture: on the builder canvas every part's own listener stops the click (it selects that part), and the tab
|
|
117
|
+
// must still turn on — the canvas is a live runtime.
|
|
118
|
+
root.addEventListener('click', this.onClick, true);
|
|
119
|
+
root.addEventListener('mouseover', this.onHover, true);
|
|
120
|
+
root.addEventListener('keydown', this.onKeydown);
|
|
121
|
+
if (options.observe && typeof MutationObserver !== 'undefined') {
|
|
122
|
+
this.observer = new MutationObserver(() => this.queueStamp());
|
|
123
|
+
this.observer.observe(root, { childList: true, subtree: true });
|
|
124
|
+
}
|
|
125
|
+
this.stamp(true);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
get behaviour(): TabsBehaviour {
|
|
129
|
+
return this.behaviourValue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The behaviour changed in the panel (Activate on, Orientation). */
|
|
133
|
+
setBehaviour(behaviour: TabsBehaviour): void {
|
|
134
|
+
this.behaviourValue = behaviour;
|
|
135
|
+
this.root.setAttribute(ACTIVATE_ON_ATTRIBUTE, behaviour.activateOn);
|
|
136
|
+
this.root.setAttribute(ORIENTATION_ATTRIBUTE, behaviour.orientation);
|
|
137
|
+
this.stamp();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** THIS composite's triggers in document order (a nested composite's are its own). */
|
|
141
|
+
triggers(): HTMLElement[] {
|
|
142
|
+
return this.own(TRIGGER_SELECTOR);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** THIS composite's panels in document order. */
|
|
146
|
+
panels(): HTMLElement[] {
|
|
147
|
+
return this.own(PANEL_SELECTOR);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The key on right now (forced ?? live). */
|
|
151
|
+
get activeKey(): string | null {
|
|
152
|
+
return this.shown;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The key the live state holds (before anything forced). */
|
|
156
|
+
get liveKey(): string | null {
|
|
157
|
+
return this.live;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The key of a trigger or a panel: its `data-iox-item-key`, else its index among its kind. */
|
|
161
|
+
keyOf(part: Element): string {
|
|
162
|
+
const own = part.getAttribute(ITEM_KEY_ATTRIBUTE);
|
|
163
|
+
if (own) return own;
|
|
164
|
+
const peers = part.matches(TRIGGER_SELECTOR) ? this.triggers() : this.panels();
|
|
165
|
+
return `item-${peers.indexOf(part as HTMLElement)}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** The key of the item an element sits in (a part inside a tab or a panel), or null. */
|
|
169
|
+
keyAt(el: Element | null | undefined): string | null {
|
|
170
|
+
const part = closestExclusiveItemPart(el);
|
|
171
|
+
return part && closestExclusive(part) === this.root ? this.keyOf(part) : null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Turn `key` on (a click, a key, a hover, the `openItem` action, "Showing on canvas"). */
|
|
175
|
+
activate(key: string): void {
|
|
176
|
+
if (!this.keys().includes(key)) return;
|
|
177
|
+
const changed = key !== this.live;
|
|
178
|
+
this.live = key;
|
|
179
|
+
this.stamp();
|
|
180
|
+
if (changed) this.options.onChange?.(key);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Run an item action on `key`: Open turns it on; Close and Toggle do nothing (one item is always on). True when it
|
|
184
|
+
* is on afterwards. */
|
|
185
|
+
run(open: boolean, key: string): boolean {
|
|
186
|
+
if (open) this.activate(key);
|
|
187
|
+
return this.shown === key;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** The forced key changed (builder design state): show it. */
|
|
191
|
+
refresh(): void {
|
|
192
|
+
this.stamp();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
destroy(): void {
|
|
196
|
+
if (EXCLUSIVE_RUNTIMES.get(this.root) === this) EXCLUSIVE_RUNTIMES.delete(this.root);
|
|
197
|
+
this.root.removeEventListener('click', this.onClick, true);
|
|
198
|
+
this.root.removeEventListener('mouseover', this.onHover, true);
|
|
199
|
+
this.root.removeEventListener('keydown', this.onKeydown);
|
|
200
|
+
this.observer?.disconnect();
|
|
201
|
+
this.observer = null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private keys(): string[] {
|
|
205
|
+
return this.triggers().map(t => this.keyOf(t));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** The element(s) that ARE the tablist: the ones a renderer already gave the role (the engine's component-path
|
|
209
|
+
* Repeater puts it on its inner root, which holds the tabs), else the elements carrying the list's role class. */
|
|
210
|
+
private lists(): HTMLElement[] {
|
|
211
|
+
const stamped = this.own(TABLIST_SELECTOR);
|
|
212
|
+
return stamped.length ? stamped : this.own(LIST_SELECTOR);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private own(selector: string): HTMLElement[] {
|
|
216
|
+
return Array.from(this.root.querySelectorAll<HTMLElement>(selector)).filter(el => closestExclusive(el.parentElement) === this.root);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private queueStamp(): void {
|
|
220
|
+
if (this.stampQueued) return;
|
|
221
|
+
this.stampQueued = true;
|
|
222
|
+
const run = () => { this.stampQueued = false; this.stamp(); };
|
|
223
|
+
if (typeof queueMicrotask === 'function') queueMicrotask(run); else Promise.resolve().then(run);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private rootId(): string {
|
|
227
|
+
const own = this.options.rootId;
|
|
228
|
+
return (typeof own === 'function' ? own() : own) ?? this.root.getAttribute(SCOPE_ATTRIBUTE) ?? nodeIdOf(this.root);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Write every part's state into its markup; announce what changed. */
|
|
232
|
+
private stamp(first = false): void {
|
|
233
|
+
const triggers = this.triggers();
|
|
234
|
+
const keys = triggers.map(t => this.keyOf(t));
|
|
235
|
+
if (this.awaitingItems && keys.length) {
|
|
236
|
+
this.awaitingItems = false;
|
|
237
|
+
this.live = keys[0];
|
|
238
|
+
first = true;
|
|
239
|
+
}
|
|
240
|
+
if (this.live === null || !keys.includes(this.live)) this.live = keys[0] ?? null;
|
|
241
|
+
const forced = this.options.forced?.() ?? null;
|
|
242
|
+
const next = forced !== null && keys.includes(forced) ? forced : this.live;
|
|
243
|
+
const rootId = this.rootId();
|
|
244
|
+
const vertical = this.behaviourValue.orientation === TabsOrientation.Vertical;
|
|
245
|
+
for (const list of this.lists()) {
|
|
246
|
+
setAttr(list, 'role', 'tablist');
|
|
247
|
+
setAttr(list, 'aria-orientation', vertical ? 'vertical' : null);
|
|
248
|
+
if (this.options.label) setAttr(list, 'aria-label', this.options.label() || null);
|
|
249
|
+
}
|
|
250
|
+
const panels = this.panels();
|
|
251
|
+
const panelOf = (key: string) => panels.find(p => this.keyOf(p) === key) ?? null;
|
|
252
|
+
for (const trigger of triggers) {
|
|
253
|
+
const key = this.keyOf(trigger);
|
|
254
|
+
const on = key === next;
|
|
255
|
+
const ids = accordionDomIds(rootId, key, CompositeKind.Tabs);
|
|
256
|
+
setAttr(trigger, ITEM_KEY_ATTRIBUTE, key);
|
|
257
|
+
setAttr(trigger, 'type', trigger.tagName === 'BUTTON' ? 'button' : null);
|
|
258
|
+
setAttr(trigger, 'role', 'tab');
|
|
259
|
+
setAttr(trigger, 'id', ids.trigger);
|
|
260
|
+
setAttr(trigger, 'aria-selected', on ? 'true' : 'false');
|
|
261
|
+
setAttr(trigger, 'aria-controls', panelOf(key) ? ids.panel : null);
|
|
262
|
+
setAttr(trigger, 'tabindex', on ? '0' : '-1');
|
|
263
|
+
setAttr(trigger, ON_ATTRIBUTE, on ? '' : null);
|
|
264
|
+
}
|
|
265
|
+
for (const panel of panels) {
|
|
266
|
+
const key = this.keyOf(panel);
|
|
267
|
+
const on = key === next;
|
|
268
|
+
const ids = accordionDomIds(rootId, key, CompositeKind.Tabs);
|
|
269
|
+
setAttr(panel, ITEM_KEY_ATTRIBUTE, key);
|
|
270
|
+
setAttr(panel, 'role', 'tabpanel');
|
|
271
|
+
setAttr(panel, 'id', ids.panel);
|
|
272
|
+
setAttr(panel, 'aria-labelledby', ids.trigger);
|
|
273
|
+
setAttr(panel, 'tabindex', '0');
|
|
274
|
+
setAttr(panel, ON_ATTRIBUTE, on ? '' : null);
|
|
275
|
+
setAttr(panel, 'hidden', on ? null : '');
|
|
276
|
+
}
|
|
277
|
+
const was = first ? null : this.shown;
|
|
278
|
+
this.shown = next;
|
|
279
|
+
if (was === next && !first) return;
|
|
280
|
+
if (was !== null) {
|
|
281
|
+
dispatch(triggers.find(t => this.keyOf(t) === was), ITEM_OFF_EVENT);
|
|
282
|
+
dispatch(panelOf(was), ITEM_OFF_EVENT);
|
|
283
|
+
}
|
|
284
|
+
if (next !== null) {
|
|
285
|
+
dispatch(triggers.find(t => this.keyOf(t) === next), ITEM_ON_EVENT);
|
|
286
|
+
dispatch(panelOf(next), ITEM_ON_EVENT);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private ownTrigger(target: EventTarget | null): HTMLElement | null {
|
|
291
|
+
const trigger = (target as Element | null)?.closest?.(TRIGGER_SELECTOR) as HTMLElement | null;
|
|
292
|
+
return trigger && closestExclusive(trigger.parentElement) === this.root ? trigger : null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private readonly onClick = (event: Event): void => {
|
|
296
|
+
const trigger = this.ownTrigger(event.target);
|
|
297
|
+
if (!trigger) return;
|
|
298
|
+
event.preventDefault();
|
|
299
|
+
this.activate(this.keyOf(trigger));
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
private readonly onHover = (event: Event): void => {
|
|
303
|
+
if (this.behaviourValue.activateOn !== TabsActivateOn.Hover) return;
|
|
304
|
+
const trigger = this.ownTrigger(event.target);
|
|
305
|
+
if (trigger) this.activate(this.keyOf(trigger));
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
private readonly onKeydown = (event: Event): void => {
|
|
309
|
+
const e = event as KeyboardEvent;
|
|
310
|
+
const trigger = this.ownTrigger(e.target);
|
|
311
|
+
if (!trigger) return;
|
|
312
|
+
const rtl = isRtl(trigger);
|
|
313
|
+
const action = tabsKeyAction(e.key, this.behaviourValue.orientation, rtl);
|
|
314
|
+
if (!action) return;
|
|
315
|
+
const triggers = this.triggers();
|
|
316
|
+
const target = triggers[accordionFocusIndex(action, triggers.indexOf(trigger), triggers.length)];
|
|
317
|
+
if (!target) return;
|
|
318
|
+
e.preventDefault();
|
|
319
|
+
target.focus();
|
|
320
|
+
this.activate(this.keyOf(target));
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** The direction the tab is laid out in — the computed `direction` (a page's own CSS included), else its nearest `dir`. */
|
|
325
|
+
function isRtl(el: Element): boolean {
|
|
326
|
+
let computed = '';
|
|
327
|
+
try { computed = el.ownerDocument?.defaultView?.getComputedStyle(el).direction ?? ''; } catch { computed = ''; }
|
|
328
|
+
if (computed) return computed === 'rtl';
|
|
329
|
+
return el.closest('[dir]')?.getAttribute('dir') === 'rtl';
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Every live exclusive runtime by its root — so an item action finds the runtime of whatever item it targets. */
|
|
333
|
+
const EXCLUSIVE_RUNTIMES = new WeakMap<Element, ExclusiveRuntime>();
|
|
334
|
+
|
|
335
|
+
/** The runtime running an exclusive root, or null. */
|
|
336
|
+
export function exclusiveRuntimeOf(root: Element | null | undefined): ExclusiveRuntime | null {
|
|
337
|
+
return (root && EXCLUSIVE_RUNTIMES.get(root)) || null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Attach the runtime to one rendered exclusive root, unless it already runs one. Returns the NEW runtime, or null. */
|
|
341
|
+
export function attachExclusiveAt(root: HTMLElement, options: Omit<ExclusiveRuntimeOptions, 'behaviour'> = {}): ExclusiveRuntime | null {
|
|
342
|
+
if (EXCLUSIVE_RUNTIMES.has(root)) return null;
|
|
343
|
+
return new ExclusiveRuntime(root, options);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** True when `el` is (or sits in) an exclusive item part — a tab or a panel. */
|
|
347
|
+
export function isExclusiveItemOn(el: Element): boolean {
|
|
348
|
+
return !!closestExclusiveItemPart(el)?.hasAttribute(ON_ATTRIBUTE);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Subscribe to the item part `el` sits in turning on / off. Returns the unsubscribe. */
|
|
352
|
+
export function onExclusiveItemState(el: Element, on: boolean, handler: () => void): () => void {
|
|
353
|
+
const part = closestExclusiveItemPart(el);
|
|
354
|
+
if (!part) return () => undefined;
|
|
355
|
+
const type = on ? ITEM_ON_EVENT : ITEM_OFF_EVENT;
|
|
356
|
+
part.addEventListener(type, handler);
|
|
357
|
+
return () => part.removeEventListener(type, handler);
|
|
358
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { setColorScheme } from '@vectoriox/iox-ui/design-core';
|
|
2
|
+
|
|
3
|
+
import { CompositeNode, CompositeNodeFactory, makeCompositePart, NO_ELEMENT_STYLE, readProp, writeProp, writeStyles } from './composite-node';
|
|
4
|
+
import { buildSliderPart, SLIDE_TYPE, SLIDER_TYPE, SliderRole } from './slider';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* THE LIGHTBOX (CC3 — complex-components plan §7d, board 6). Not a component: a Dialog holding a Slider bound to the
|
|
8
|
+
* SAME source as the grid of thumbnails. The one new thing is `openDialog`'s "Open at the clicked item"
|
|
9
|
+
* (`slideTo: 'row'`): a thumbnail in a repeater row opens the lightbox at ITS slide.
|
|
10
|
+
*
|
|
11
|
+
* Dialog `dialog` · centre · full viewport · Inverse scheme "Lightbox" (a free-standing Dialog: board A4)
|
|
12
|
+
* ├─ ButtonBlock `button-ghost` "Close" › Icon × click → closeDialog
|
|
13
|
+
* └─ SliderContainer one slide in view · keyboard · the source's rows
|
|
14
|
+
* ├─ Previous · Next (slider.prev / slider.next, button-secondary) hidden on a phone (swipe)
|
|
15
|
+
* ├─ SliderSlide › Image `media` (contain, keeps its alt) + Text `small` caption
|
|
16
|
+
* └─ Pagination (slider.pagination) — the counter "3 / 6"
|
|
17
|
+
*
|
|
18
|
+
* Keyboard ← → are the Slider's Keyboard module; Esc and the focus trap are the `<dialog>`'s; focus returns to the
|
|
19
|
+
* thumbnail that opened it (the dialog opener's own rule).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export const SLIDE_TO_ROW = 'row';
|
|
23
|
+
/** The attribute a renderer writes on every repeated row (the context-state provenance, both engine paths, the canvas). */
|
|
24
|
+
export const ROW_INDEX_ATTRIBUTE = 'data-iox-row-idx';
|
|
25
|
+
export const LIGHTBOX_LABEL = 'Lightbox';
|
|
26
|
+
const DIALOG = 'Dialog';
|
|
27
|
+
const REPEATER = 'Repeater';
|
|
28
|
+
const SOURCE = 'source';
|
|
29
|
+
const SOURCE_PATH = 'sourcePath';
|
|
30
|
+
|
|
31
|
+
export interface LightboxOptions {
|
|
32
|
+
/** The list the slides come from (the grid's own source). Empty = three manual slides. */
|
|
33
|
+
source?: string;
|
|
34
|
+
sourcePath?: string;
|
|
35
|
+
/** The row field each slide's image and caption read (`item` = the row itself, a bare URL). */
|
|
36
|
+
imageField?: string;
|
|
37
|
+
captionField?: string;
|
|
38
|
+
/** The image's alt binding; '' = decorative (a bare URL row has no words of its own). */
|
|
39
|
+
altField?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const ZERO = '0px 0px 0px 0px';
|
|
43
|
+
const DIALOG_BASE: Readonly<Record<string, string>> = {
|
|
44
|
+
width: '100vw', maxWidth: '100vw', height: '100dvh', maxHeight: '100dvh', borderRadius: '0px', padding: '18px 18px 18px 18px',
|
|
45
|
+
display: 'flex', flexDirection: 'column', gap: '0px',
|
|
46
|
+
};
|
|
47
|
+
/** The top row (board 6): the counter at its start, Close at its end — Close sits above the slider, which rises into
|
|
48
|
+
* the row so its counter shares it. */
|
|
49
|
+
const CLOSE_BASE: Readonly<Record<string, string>> = {
|
|
50
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center', width: '44px', height: '44px', minWidth: '44px',
|
|
51
|
+
padding: ZERO, margin: ZERO, borderRadius: '999px', alignSelf: 'flex-end', flexShrink: '0', position: 'relative', zIndex: '20',
|
|
52
|
+
};
|
|
53
|
+
const SLIDER_BASE: Readonly<Record<string, string>> = { width: '100%', height: '100%', minHeight: '0px', flexGrow: '1', margin: '-44px 0px 0px 0px' };
|
|
54
|
+
const SLIDE_BASE: Readonly<Record<string, string>> = {
|
|
55
|
+
width: '100%', height: '100%', aspectRatio: 'auto', display: 'flex', flexDirection: 'column', alignItems: 'center',
|
|
56
|
+
justifyContent: 'center', gap: '12px', padding: '56px 64px 16px 64px',
|
|
57
|
+
};
|
|
58
|
+
const IMAGE_BASE: Readonly<Record<string, string>> = { width: '100%', height: 'auto', maxHeight: '72dvh', minHeight: '0px', objectFit: 'contain', aspectRatio: '4 / 3', flexShrink: '1' };
|
|
59
|
+
const CAPTION_BASE: Readonly<Record<string, string>> = { textAlign: 'center', margin: ZERO, padding: ZERO, minHeight: 'auto', width: 'auto' };
|
|
60
|
+
/** The counter "3 / 6" sits at the START of the top row, Close at its end (board 6). */
|
|
61
|
+
const COUNTER_BASE: Readonly<Record<string, string>> = { color: 'var(--iox-text-muted)', top: '12px', alignItems: 'flex-start', justifyContent: 'flex-start' };
|
|
62
|
+
/** On a phone the arrows give way to a swipe (board 6, "the arrows hide on touch-only widths"). */
|
|
63
|
+
export const LIGHTBOX_ARROWS_HIDE_AT = 'mobile';
|
|
64
|
+
|
|
65
|
+
interface WithInteractions { interactions?: unknown[] }
|
|
66
|
+
|
|
67
|
+
/** A click on `node` runs one instant action on `target`. */
|
|
68
|
+
function onClick(node: CompositeNode, type: string, target: string, params?: Record<string, unknown>): void {
|
|
69
|
+
(node as WithInteractions).interactions = [{
|
|
70
|
+
id: `${node.id ?? 'node'}-click`, trigger: 'click',
|
|
71
|
+
actions: [{ type, target, duration: 0, delay: 0, easing: 'ease', ...(params ? { params } : {}) }],
|
|
72
|
+
}];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A binding of `prop` to the row field `field` of `source` (`item` = the row itself). */
|
|
76
|
+
function bindRow(node: CompositeNode, prop: string, source: string, field: string): void {
|
|
77
|
+
const bindings = ((node as { bindings?: unknown[] }).bindings ?? []).filter((b) => (b as { prop?: string }).prop !== prop);
|
|
78
|
+
bindings.push({ prop, source, path: field === 'item' ? '' : field });
|
|
79
|
+
(node as { bindings?: unknown[] }).bindings = bindings;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function buildSlide(create: CompositeNodeFactory, key: string, options: LightboxOptions, rowAlias: string | null): CompositeNode {
|
|
83
|
+
const slide = makeCompositePart(create, { key, type: SLIDE_TYPE, props: {}, elementStyle: NO_ELEMENT_STYLE }, null, 'Slide', SLIDE_BASE);
|
|
84
|
+
const image = makeCompositePart(create, { key: `${key}-image`, type: 'Image', props: { src: '', alt: '' }, elementStyle: 'media' }, null, 'Image', IMAGE_BASE);
|
|
85
|
+
const caption = makeCompositePart(create, { key: `${key}-caption`, type: 'Text', props: { content: 'Caption', tag: 'p' }, elementStyle: 'small' }, null, 'Caption', CAPTION_BASE);
|
|
86
|
+
if (rowAlias) {
|
|
87
|
+
bindRow(image, 'src', rowAlias, options.imageField ?? 'item');
|
|
88
|
+
if (options.altField) bindRow(image, 'alt', rowAlias, options.altField);
|
|
89
|
+
if (options.captionField) bindRow(caption, 'content', rowAlias, options.captionField);
|
|
90
|
+
}
|
|
91
|
+
slide.children = options.captionField || !rowAlias ? [image, caption] : [image];
|
|
92
|
+
return slide;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** A fresh lightbox: the Dialog, its Close part and the Slider (bound to `source`, or three manual slides). */
|
|
96
|
+
export function buildLightboxRecipe(create: CompositeNodeFactory, options: LightboxOptions = {}): CompositeNode {
|
|
97
|
+
const dialog = makeCompositePart(create, { key: 'lightbox', type: DIALOG, props: {}, elementStyle: 'dialog' }, null, LIGHTBOX_LABEL, DIALOG_BASE);
|
|
98
|
+
writeProp(dialog, 'placement', 'center');
|
|
99
|
+
writeProp(dialog, 'closeOnBackdrop', true);
|
|
100
|
+
writeProp(dialog, 'showCloseButton', false);
|
|
101
|
+
writeProp(dialog, 'ariaLabel', 'Gallery');
|
|
102
|
+
setColorScheme(dialog, 'inverse');
|
|
103
|
+
|
|
104
|
+
const close = makeCompositePart(create, { key: 'close', type: 'ButtonBlock', props: { tag: 'button', type: 'button', ariaLabel: 'Close' }, elementStyle: 'button-ghost' }, null, 'Close', CLOSE_BASE);
|
|
105
|
+
close.children = [makeCompositePart(create, { key: 'close-icon', type: 'Icon', props: { iconClass: 'ph ph-x' }, elementStyle: 'icon' }, null, 'Icon', { fontSize: '22px', color: 'inherit' })];
|
|
106
|
+
onClick(close, 'closeDialog', dialog.id ?? '');
|
|
107
|
+
|
|
108
|
+
const slider = makeCompositePart(create, { key: 'slider', type: SLIDER_TYPE, props: {}, elementStyle: NO_ELEMENT_STYLE }, null, 'Slider', SLIDER_BASE);
|
|
109
|
+
for (const [name, value] of Object.entries({
|
|
110
|
+
slidesPerView: 1, spaceBetween: 0, loop: 'off', enableNavigation: true, enablePagination: true, paginationType: 'fraction',
|
|
111
|
+
enableKeyboard: true, ariaLabel: 'Gallery', limit: 50,
|
|
112
|
+
})) writeProp(slider, name, value);
|
|
113
|
+
const rowAlias = options.source ? lastSegment(options.sourcePath) || options.source : null;
|
|
114
|
+
if (options.source) {
|
|
115
|
+
writeProp(slider, SOURCE, options.source);
|
|
116
|
+
if (options.sourcePath) writeProp(slider, SOURCE_PATH, options.sourcePath);
|
|
117
|
+
}
|
|
118
|
+
const prev = buildSliderPart(create, SliderRole.Prev);
|
|
119
|
+
const next = buildSliderPart(create, SliderRole.Next);
|
|
120
|
+
for (const arrow of [prev, next]) {
|
|
121
|
+
const layers = { ...(arrow.responsive ?? {}) };
|
|
122
|
+
layers[LIGHTBOX_ARROWS_HIDE_AT] = { styleProps: { display: 'none' } };
|
|
123
|
+
arrow.responsive = layers;
|
|
124
|
+
}
|
|
125
|
+
const counter = buildSliderPart(create, SliderRole.Pagination);
|
|
126
|
+
counter.label = 'Counter';
|
|
127
|
+
writeStyles(counter, COUNTER_BASE);
|
|
128
|
+
const slides = rowAlias ? [buildSlide(create, 'slide', options, rowAlias)] : [1, 2, 3].map(n => buildSlide(create, `slide-${n}`, options, null));
|
|
129
|
+
slider.children = [prev, next, ...slides, counter];
|
|
130
|
+
dialog.children = [close, slider];
|
|
131
|
+
return dialog;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const lastSegment = (path: string | undefined): string => (path ? String(path).split('.').filter(Boolean).pop() ?? '' : '');
|
|
135
|
+
|
|
136
|
+
// ── "Open at the clicked item" ─────────────────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
export interface RepeatSource {
|
|
139
|
+
source: string;
|
|
140
|
+
path: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** What a Repeater (its `source` binding) or a bound Slider (its `source` / `sourcePath` traits) repeats over. */
|
|
144
|
+
export function repeatSourceOf(node: CompositeNode | null | undefined): RepeatSource | null {
|
|
145
|
+
if (!node || (node.type !== REPEATER && node.type !== SLIDER_TYPE)) return null;
|
|
146
|
+
const binding = ((node as { bindings?: Array<{ prop?: string; source?: string; path?: string }> }).bindings ?? []).find(b => b?.prop === SOURCE);
|
|
147
|
+
if (binding?.source) return { source: binding.source, path: binding.path ?? '' };
|
|
148
|
+
const source = readProp(node, SOURCE);
|
|
149
|
+
if (typeof source !== 'string' || !source) return null;
|
|
150
|
+
const path = readProp(node, SOURCE_PATH);
|
|
151
|
+
return { source, path: typeof path === 'string' ? path : '' };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function sameRepeatSource(a: RepeatSource | null, b: RepeatSource | null): boolean {
|
|
155
|
+
return !!a && !!b && a.source === b.source && (a.path ?? '') === (b.path ?? '');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** The Slider a lightbox Dialog holds (the first one, depth first), or null. */
|
|
159
|
+
export function lightboxSliderOf(dialog: CompositeNode | null | undefined): CompositeNode | null {
|
|
160
|
+
for (const child of dialog?.children ?? []) {
|
|
161
|
+
if (child.type === SLIDER_TYPE) return child;
|
|
162
|
+
const below = lightboxSliderOf(child);
|
|
163
|
+
if (below) return below;
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** The repeated rows the element is in: the nearest Repeater or bound Slider among its ancestors (`path` = root → the
|
|
169
|
+
* element, inclusive). */
|
|
170
|
+
export function rowSourceOf(path: ReadonlyArray<CompositeNode>): RepeatSource | null {
|
|
171
|
+
for (let i = path.length - 2; i >= 0; i--) {
|
|
172
|
+
const source = repeatSourceOf(path[i]);
|
|
173
|
+
if (source) return source;
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Interactions ▸ openDialog ▸ "Open at the clicked item" is offered only when it can work (board 9): the clicked
|
|
180
|
+
* element sits in a repeater row AND the target Dialog holds a Slider bound to the same source.
|
|
181
|
+
*/
|
|
182
|
+
export function canOpenAtClickedItem(ownerPath: ReadonlyArray<CompositeNode>, dialog: CompositeNode | null | undefined): boolean {
|
|
183
|
+
if (!dialog || dialog.type !== DIALOG) return false;
|
|
184
|
+
return sameRepeatSource(rowSourceOf(ownerPath), repeatSourceOf(lightboxSliderOf(dialog)));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** The repeated row an element sits in (its row index), 0 when it is in none (the canvas' template row). */
|
|
188
|
+
export function rowIndexOf(el: Element | null | undefined): number {
|
|
189
|
+
const row = el?.closest?.(`[${ROW_INDEX_ATTRIBUTE}]`);
|
|
190
|
+
const index = Number(row?.getAttribute(ROW_INDEX_ATTRIBUTE));
|
|
191
|
+
return Number.isInteger(index) && index >= 0 ? index : 0;
|
|
192
|
+
}
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* plan §3a). FRAMEWORK-FREE: the builder, the SSR engine and iox-cms (generation-core's composer, the site
|
|
4
4
|
* build) read the same recipes, roles and structure rules. Card v2 + Badge / Chip (T23 / T25) are the
|
|
5
5
|
* first; the Accordion (CC2) adds the first composite RUNTIME (accordion-runtime.ts — DOM only, no
|
|
6
|
-
* framework), which the Disclosure (CC2.1) runs too — a one-item accordion whose root is its item; Tabs
|
|
7
|
-
*
|
|
6
|
+
* framework), which the Disclosure (CC2.1) runs too — a one-item accordion whose root is its item; Tabs (CC4a) run the
|
|
7
|
+
* EXCLUSIVE runtime (exclusive-runtime.ts — one item always on); composite-runtime.ts is the one door to both.
|
|
8
8
|
* ⚠️ Consumers wire the usual trio — ng-package raw-ts assets, jest moduleNameMapper, tsconfig paths.
|
|
9
9
|
*/
|
|
10
10
|
export * from './composite-node';
|
|
@@ -15,3 +15,9 @@ export * from './disclosure';
|
|
|
15
15
|
export * from './badge';
|
|
16
16
|
export * from './card';
|
|
17
17
|
export * from './structure';
|
|
18
|
+
export * from './slider';
|
|
19
|
+
export * from './slider-runtime';
|
|
20
|
+
export * from './lightbox';
|
|
21
|
+
export * from './tabs';
|
|
22
|
+
export * from './exclusive-runtime';
|
|
23
|
+
export * from './composite-runtime';
|