@vectoriox/iox-builder 10.0.3 → 10.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/vectoriox-iox-builder.mjs +1067 -803
- package/fesm2022/vectoriox-iox-builder.mjs.map +1 -1
- package/index.d.ts +79 -8
- package/package.json +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import {
|
|
2
|
+
import { InjectionToken, Injectable, Optional, Inject, inject, NgZone, EventEmitter, createComponent, Output, Input, Directive, Component, ChangeDetectionStrategy, ViewChild, effect, ContentChildren, NO_ERRORS_SCHEMA, NgModule } from '@angular/core';
|
|
3
3
|
import * as i2 from '@angular/common';
|
|
4
4
|
import { DOCUMENT, CommonModule } from '@angular/common';
|
|
5
5
|
import * as i17 from '@angular/forms';
|
|
@@ -17,12 +17,12 @@ import { TooltipModule } from 'primeng/tooltip';
|
|
|
17
17
|
import * as i20 from 'primeng/dialog';
|
|
18
18
|
import { DialogModule } from 'primeng/dialog';
|
|
19
19
|
import * as i1$1 from '@vectoriox/iox-ui';
|
|
20
|
-
import { resolveBoundValue, VALIDATION_STATES as VALIDATION_STATES$1, VALIDATION_STATE_CLASS as VALIDATION_STATE_CLASS$1, DEFAULT_WIDTH_BY_TYPE, TEXT_DIRECTION_KEY, TEXT_ALIGN_KEY, TextAlign, COLOR_SCHEME_TRAIT, COLOR_SCHEMES, ColorScheme, LinkType, MAX_LINES_KEY, A11yTrait, BREAKPOINTS, Breakpoint, withSchemeInk, stripSuppressedProps, DeclarationContext, buildPartSelector, planResponsiveRules, resolveStyleProps, targetedStateSelector, stateSelectorSuffix, compileTokenCssBlocks, catalogFontId, fontsForTokens, GOOGLE_FONTS_FILE_ORIGIN, googleFontHref, compileDeclarations, wrapInBreakpointQuery, BreakpointQueryKind, NEW_NODE_STYLE_DEFAULTS, defaultElementStyleFor, applyElementStyle, planDataSource, dataSourcePlanToRequest, normalizeCollectionResult, resolveRepeaterItems, getByPath, computeRelative, ContextStore, ENTRANCE_ACTION_TYPES, EXIT_ACTION_TYPES, ComponentInstanceRegistryService, isActionEnabled, isContextAction, contextActionEffect, resolveContextValue, effectiveActionType, INTERACTION_HIDDEN_ATTRIBUTE, motionAllowed, finalKeyframe, keyframeStyleEntries, buildInteractionKeyframes, declaredInputNames, emptyImagePlaceholder, syncNodeDesignClasses, nodeTraitValue, syncNodeA11yAttributes, nodeA11yAttributes, safeSetInput, colorSchemeOf, composeVirtualTraits, withDefaultHoverTransition, stateNamesIn, partClassName, switchElementStyle, releaseElementStyle, countOverrides, setOverride, breakpointDef, breakpointForWidth, FormRenderState, needsAccessibleName, resolveInputs, setInputOverride, barReserveClass, collectContextSlotKeys, withContextSlots, compilePageStyle, nodeUsesContext, IS_PREVIEW, buttonBlockHref, bootSlider, IoxPageModule, PALETTE_PRESETS, elementStyleById, PART_CLASS_NAMES } from '@vectoriox/iox-ui';
|
|
20
|
+
import { nodeCssId, needsGroupName, resolveBoundValue, VALIDATION_STATES as VALIDATION_STATES$1, VALIDATION_STATE_CLASS as VALIDATION_STATE_CLASS$1, DEFAULT_WIDTH_BY_TYPE, TEXT_DIRECTION_KEY, TEXT_ALIGN_KEY, TextAlign, COLOR_SCHEME_TRAIT, COLOR_SCHEMES, ColorScheme, LinkType, MAX_LINES_KEY, A11yTrait, BREAKPOINTS, Breakpoint, withSchemeInk, stripSuppressedProps, DeclarationContext, buildPartSelector, planResponsiveRules, resolveStyleProps, targetedStateSelector, stateSelectorSuffix, compileTokenCssBlocks, catalogFontId, fontsForTokens, GOOGLE_FONTS_FILE_ORIGIN, googleFontHref, compileDeclarations, isLastEmittedStateKey, wrapInBreakpointQuery, BreakpointQueryKind, NEW_NODE_STYLE_DEFAULTS, defaultElementStyleFor, applyElementStyle, planDataSource, dataSourcePlanToRequest, normalizeCollectionResult, resolveRepeaterItems, getByPath, computeRelative, ContextStore, ENTRANCE_ACTION_TYPES, EXIT_ACTION_TYPES, ComponentInstanceRegistryService, isActionEnabled, isContextAction, contextActionEffect, resolveContextValue, effectiveActionType, INTERACTION_HIDDEN_ATTRIBUTE, motionAllowed, finalKeyframe, keyframeStyleEntries, buildInteractionKeyframes, declaredInputNames, emptyImagePlaceholder, syncNodeDesignClasses, nodeTraitValue, syncNodeA11yAttributes, nodeA11yAttributes, safeSetInput, colorSchemeOf, composeVirtualTraits, withDefaultHoverTransition, stateNamesIn, partClassName, switchElementStyle, releaseElementStyle, countOverrides, setOverride, breakpointDef, breakpointForWidth, FormRenderState, needsAccessibleName, resolveInputs, setInputOverride, barReserveClass, collectContextSlotKeys, withContextSlots, compilePageStyle, nodeUsesContext, IS_PREVIEW, buttonBlockHref, bootSlider, IoxPageModule, PALETTE_PRESETS, elementStyleById, PART_CLASS_NAMES } from '@vectoriox/iox-ui';
|
|
21
21
|
export { ComponentInstanceRegistryService, VIRTUAL_TRAIT_KEYS, composeVirtualTraits } from '@vectoriox/iox-ui';
|
|
22
|
-
import {
|
|
22
|
+
import { StructureEdit, isAccordionPart, isDisclosurePart, isSliderPart, isTabsPart, isSegmentedPart, structureRefusal, StructureRefusal, SegmentedRole, SEGMENTED_MAX_OPTIONS, AccordionRole, SEGMENTED_MIN_OPTIONS, isExclusiveItemPart, TabsRole, ITEM_OPEN_STATE, readStyle, isSegmentedRoot, segmentedDefaultOn, segmentedOptions, isTabsRoot, isTabsFromContent, tabsTriggers, isAccordionRoot, isAccordionFromContent, accordionItems, initialAccordionOpen, accordionBehaviour, exclusiveItemLabel, exclusiveRootOf, tabTriggerOf, tabLabel, removeTabPair, tabPanelOf, tabsListOf, tabsPanelsOf, nextTabKey, CompositeBehaviourTrait, ACCORDION_DEFAULT_ONS, TabsBehaviourTrait, TABS_ACTIVATE_ONS, TABS_ORIENTATIONS, onCompositeItemState, isCompositeItemOn, runCompositeItemAction, shouldScrollToItem, rowIndexOf as rowIndexOf$1, slideIndexForRow, DisclosureRole, isExclusiveTrigger, compositeScopeId, compositeRowKeys, isExclusiveRoot, ExclusiveRuntime, exclusiveDefaultOf, exclusiveLabelOf, exclusiveBehaviourOf, exclusiveKindOf, compositeKindOfRole, AccordionRuntime, isToggleKind, closestAccordionItem, isAnyCompositeRoot, isExclusiveList, buildCardRecipe, buildBadge, buildChip, buildAccordionRecipe, buildDisclosureRecipe, buildTabsRecipe, buildSegmentedRecipe, buildLightboxRecipe, NO_ELEMENT_STYLE, CompositeKind, SliderRole, accordionItemLabel, segmentedOptionLabel, isExclusivePanels, isLockedPart, accordionRootOf, accordionItemOf, addAccordionItem, addTabPair, segmentedAddRefusal, addSegmentedOption, isCompositeItem, accordionHeadingLevel, isCompositePanel, isCompositeTrigger, splitSliderChildren, hasSliderParts } from '@vectoriox/iox-ui/composite-core';
|
|
23
|
+
import { formContentInHost, isInteractiveHost, findFormContentInHosts } from '@vectoriox/iox-ui/forms-core';
|
|
23
24
|
import { Subject, BehaviorSubject, of, forkJoin } from 'rxjs';
|
|
24
25
|
import interact from 'interactjs';
|
|
25
|
-
import { formContentInHost, isInteractiveHost, findFormContentInHosts } from '@vectoriox/iox-ui/forms-core';
|
|
26
26
|
import { map, catchError } from 'rxjs/operators';
|
|
27
27
|
import * as i1 from '@angular/common/http';
|
|
28
28
|
import { itemActionOf, BEHAVIOUR_ACTION_TYPES, SERVER_ELEMENT_ACTION_TYPES, CONSENT_ACTION_TYPES, opensAtClickedItem } from '@vectoriox/iox-ui/actions-core';
|
|
@@ -33,230 +33,6 @@ import * as i18 from 'primeng/api';
|
|
|
33
33
|
import { planVideo, VideoSource, VIDEO_PROVIDER_LABEL, VideoTrait, VideoMode } from '@vectoriox/iox-ui/media-core';
|
|
34
34
|
import { ConsentCategory } from '@vectoriox/iox-ui/consent-core';
|
|
35
35
|
|
|
36
|
-
/**
|
|
37
|
-
* node-path — pure ancestor-chain helpers for the builder's node tree.
|
|
38
|
-
*
|
|
39
|
-
* The builder needs a node's ancestor chain in several places: the breadcrumb bar,
|
|
40
|
-
* "select parent", and the parent-scoped style traits (Child Layout). Each of those
|
|
41
|
-
* used to re-walk the tree with its own private recursion. `nodeAncestorPath` is the
|
|
42
|
-
* single walk they all derive from.
|
|
43
|
-
*
|
|
44
|
-
* Builder-only on purpose: the SSR engine never needs a breadcrumb, so this stays in
|
|
45
|
-
* `iox-builder` rather than `@vectoriox/iox-ui` (which is the home for logic BOTH
|
|
46
|
-
* renderers consume). Structural helpers the two renderers share live in
|
|
47
|
-
* `@vectoriox/iox-ui` `style-compiler/tree-relations.ts`.
|
|
48
|
-
*/
|
|
49
|
-
/**
|
|
50
|
-
* The chain of nodes from a root down to (and including) `target`, matched by
|
|
51
|
-
* REFERENCE identity. Returns `null` when `target` is not in the tree.
|
|
52
|
-
*
|
|
53
|
-
* Depth-first, pre-order; first match wins. Reference identity (not `id`) is the right
|
|
54
|
-
* matcher here because the builder mutates nodes in place and repeater/slider clones
|
|
55
|
-
* deliberately share a template's `styleId` — matching by id would return the template's
|
|
56
|
-
* path for a clone.
|
|
57
|
-
*/
|
|
58
|
-
function nodeAncestorPath(roots, target) {
|
|
59
|
-
if (!roots?.length || !target)
|
|
60
|
-
return null;
|
|
61
|
-
for (const node of roots) {
|
|
62
|
-
if (node === target)
|
|
63
|
-
return [node];
|
|
64
|
-
const childPath = nodeAncestorPath(node.children, target);
|
|
65
|
-
if (childPath)
|
|
66
|
-
return [node, ...childPath];
|
|
67
|
-
}
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* The immediate parent of `target`, or `null` when it is a root node or not in the tree.
|
|
72
|
-
* Derived from `nodeAncestorPath` so there is exactly one tree walk to keep correct.
|
|
73
|
-
*/
|
|
74
|
-
function findParentOf(roots, target) {
|
|
75
|
-
const path = nodeAncestorPath(roots, target);
|
|
76
|
-
return path && path.length >= 2 ? path[path.length - 2] : null;
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* The ancestor chain across BOTH builder trees — the page layout and the global
|
|
80
|
-
* elements overlay, which are separate roots. Returns the first tree that contains
|
|
81
|
-
* the node, or `null` when neither does.
|
|
82
|
-
*/
|
|
83
|
-
function nodeAncestorPathInEither(layout, globalElements, target) {
|
|
84
|
-
return nodeAncestorPath(layout, target) ?? nodeAncestorPath(globalElements, target);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* A tab and its panel go TOGETHER on the canvas (CC4a — composite T18): deleting either deletes both ("Delete tab and
|
|
89
|
-
* its panel"), duplicating either duplicates both under one fresh key. The pairing is composite-core's (`itemKey`);
|
|
90
|
-
* this is the builder's tree surgery over it, shared by every path (toolbar, layer tree, the host's Cmd+D / Delete).
|
|
91
|
-
*/
|
|
92
|
-
/** The Tabs a tab or a panel belongs to — a MANUAL one (From content, the rows are not nodes) — or null. */
|
|
93
|
-
function tabsRootForPart(layout, globals, node) {
|
|
94
|
-
if (!isExclusiveItemPart(node))
|
|
95
|
-
return null;
|
|
96
|
-
const path = nodeAncestorPathInEither(layout, globals, node) ?? [];
|
|
97
|
-
const root = exclusiveRootOf(node, path.filter(n => n !== node));
|
|
98
|
-
return root && !isTabsFromContent(root) ? root : null;
|
|
99
|
-
}
|
|
100
|
-
/** The words the pair is named by in the confirm: its tab's. */
|
|
101
|
-
function tabPairLabel(root, node) {
|
|
102
|
-
const tree = root;
|
|
103
|
-
const tab = node.role === TabsRole.Panel ? tabTriggerOf(tree, node) : node;
|
|
104
|
-
return tab ? tabLabel(tab) : '';
|
|
105
|
-
}
|
|
106
|
-
/** Delete a tab and its panel. True when the pair was removed. */
|
|
107
|
-
function deleteTabPair(root, node) {
|
|
108
|
-
return removeTabPair(root, node);
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Duplicate a tab and its panel: copies (`clone` gives fresh ids) placed right after the originals, both under ONE new
|
|
112
|
-
* key — so the copy's tab shows the copy's panel. Returns the new tab, or null when `node` is no tab or panel of `root`.
|
|
113
|
-
*/
|
|
114
|
-
function duplicateTabPair(root, node, clone) {
|
|
115
|
-
const tree = root;
|
|
116
|
-
const tab = (node.role === TabsRole.Panel ? tabTriggerOf(tree, node) : node);
|
|
117
|
-
const panel = tab ? tabPanelOf(tree, tab) : undefined;
|
|
118
|
-
const list = tabsListOf(tree);
|
|
119
|
-
const panels = tabsPanelsOf(tree);
|
|
120
|
-
if (!tab || !panel || !list?.children || !panels?.children)
|
|
121
|
-
return null;
|
|
122
|
-
const key = nextTabKey(tree);
|
|
123
|
-
const tabCopy = clone(tab);
|
|
124
|
-
const panelCopy = clone(panel);
|
|
125
|
-
tabCopy.itemKey = panelCopy.itemKey = key;
|
|
126
|
-
list.children = insertAfter(list.children, tab, tabCopy);
|
|
127
|
-
panels.children = insertAfter(panels.children, panel, panelCopy);
|
|
128
|
-
return tabCopy;
|
|
129
|
-
}
|
|
130
|
-
function insertAfter(list, anchor, node) {
|
|
131
|
-
const out = [...list];
|
|
132
|
-
out.splice(out.indexOf(anchor) + 1, 0, node);
|
|
133
|
-
return out;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
var PanelEventTypes;
|
|
137
|
-
(function (PanelEventTypes) {
|
|
138
|
-
PanelEventTypes["ELEMENT_SELECT"] = "elementselect";
|
|
139
|
-
PanelEventTypes["ELEMENT_DESELECT"] = "elementdeselect";
|
|
140
|
-
PanelEventTypes["PANEL_SELECTED"] = "panelselect";
|
|
141
|
-
PanelEventTypes["PANEL_OPEN"] = "panelopen";
|
|
142
|
-
PanelEventTypes["PANEL_CLOSE"] = "panelclose";
|
|
143
|
-
PanelEventTypes["BINDING_CHANGED"] = "bindingchanged";
|
|
144
|
-
PanelEventTypes["NODE_RENDERED"] = "noderendered";
|
|
145
|
-
/** Open the Style panel in preset-edit mode for the given preset ID. */
|
|
146
|
-
PanelEventTypes["PRESET_EDIT"] = "presetedit";
|
|
147
|
-
/** Fired after a preset is created, updated, or deleted so panels refresh their list. */
|
|
148
|
-
PanelEventTypes["PRESETS_CHANGED"] = "presetschanged";
|
|
149
|
-
/** Fired when a linked symbol instance's styles change — propagates to all instances. */
|
|
150
|
-
PanelEventTypes["SYMBOLS_CHANGED"] = "symbolschanged";
|
|
151
|
-
/** Fired on every structural or style mutation — used by BuilderHistoryService to snapshot. */
|
|
152
|
-
PanelEventTypes["LAYOUT_CHANGED"] = "layoutchanged";
|
|
153
|
-
/** Open the Interactions panel in preset-edit mode for the given preset ID. */
|
|
154
|
-
PanelEventTypes["INTERACTION_PRESET_EDIT"] = "interactionpresetedit";
|
|
155
|
-
/** Fired after an interaction preset is created, updated, or deleted so panels refresh. */
|
|
156
|
-
PanelEventTypes["INTERACTION_PRESETS_CHANGED"] = "interactionpresetschanged";
|
|
157
|
-
/** Open the Interactions panel and EXPAND a specific interaction for editing (data = interaction
|
|
158
|
-
* id). Emitted when the overlay interaction badge is clicked so that badge → "edit THIS
|
|
159
|
-
* interaction", without firing the element's (live) animation. */
|
|
160
|
-
PanelEventTypes["INTERACTION_EDIT"] = "interactionedit";
|
|
161
|
-
/** A node's `interactions` were added/removed/edited. Carries `{ node }`.
|
|
162
|
-
* Repeater/Slider listen so their PREVIEW CLONES are re-cloned with the new interactions —
|
|
163
|
-
* clones are attached by ELEMENT at render time, so without this a new interaction only starts
|
|
164
|
-
* working after a save + reload. */
|
|
165
|
-
PanelEventTypes["INTERACTION_CHANGED"] = "interactionchanged";
|
|
166
|
-
/** CLICK-TO-INSERT (B2): a components-panel entry was clicked. Carries the same payload a drag
|
|
167
|
-
* would (`string` component type, or a block / symbol object). The builder resolves WHERE from
|
|
168
|
-
* the current selection — see `resolveInsertTarget`. */
|
|
169
|
-
PanelEventTypes["INSERT_COMPONENT"] = "insertcomponent";
|
|
170
|
-
/** Insert prebuilt node(s) as GLOBAL elements (data = ComponentNode[]) — the consent preset's click and
|
|
171
|
-
* the site header's click or drop (CC1.1). The HOST handles it (it owns the globals, their undo step and
|
|
172
|
-
* the one-header rule); the builder only routes a dropped global payload here (`globalInsert$`). */
|
|
173
|
-
PanelEventTypes["INSERT_GLOBAL"] = "insertglobal";
|
|
174
|
-
/** A panel makes `data.node` a GLOBAL element at `data.position` (default before) — the same move as the
|
|
175
|
-
* toolbar's Make Global (the Style panel's Global switch, Bindings ▸ Phone menu ▸ "Make it the site header"). */
|
|
176
|
-
PanelEventTypes["MAKE_GLOBAL"] = "make-global";
|
|
177
|
-
/** A panel returns the global `data.node` to the page — the toolbar's Remove Global. */
|
|
178
|
-
PanelEventTypes["REMOVE_GLOBAL"] = "remove-global";
|
|
179
|
-
/** A panel moved the global `data.node` to `data.position` (before / after the page content). */
|
|
180
|
-
PanelEventTypes["GLOBAL_POSITION"] = "update-global-position";
|
|
181
|
-
/** A click on a compound component's PART (Field label/control/help/…) — the style panel
|
|
182
|
-
* activates that part (data = { part }). Doron 2026-10-10: "both shall be selectable". */
|
|
183
|
-
PanelEventTypes["PART_SELECT"] = "partselect";
|
|
184
|
-
/** A panel wants to select a NODE it already has a reference to (e.g. the Form's nested field
|
|
185
|
-
* list). Carries `{ node }`. `OverlayService.setSelect()` alone only updates the overlay's own
|
|
186
|
-
* visual state — `selectedItem`/`selectedPath` (breadcrumb) and every other panel's idea of
|
|
187
|
-
* "what's selected" stay stale unless the SAME `_emitElementSelect` a real canvas click goes
|
|
188
|
-
* through also runs. This event is that path for code outside `BuilderComponent` itself,
|
|
189
|
-
* mirroring what `onTreeNodeSelect`/`onCrumbSelect` already do internally. */
|
|
190
|
-
PanelEventTypes["SELECT_NODE"] = "selectnode";
|
|
191
|
-
/** A panel rewrote a node's SUBTREE in place — its children (a Card's Parts), its layout styles (Card ▸
|
|
192
|
-
* Layout), a part's element style or scheme. Carries `{ node }`. The builder re-renders the subtree,
|
|
193
|
-
* re-registers every node's rules and design classes, refreshes a Repeater holding it, and records ONE
|
|
194
|
-
* undo step (LAYOUT_CHANGED). */
|
|
195
|
-
PanelEventTypes["NODE_STRUCTURE_CHANGED"] = "nodestructurechanged";
|
|
196
|
-
/** A Repeater's rows changed on the canvas (fetched, re-fetched, its source cleared). Carries `{ nodeId,
|
|
197
|
-
* count }`. Not a page edit — never an undo step: the layer tree redraws its "N more rows" line. */
|
|
198
|
-
PanelEventTypes["REPEATER_ROWS_CHANGED"] = "repeaterrowschanged";
|
|
199
|
-
})(PanelEventTypes || (PanelEventTypes = {}));
|
|
200
|
-
class PanelEventService {
|
|
201
|
-
constructor() {
|
|
202
|
-
this.panelEventSubject = new Subject();
|
|
203
|
-
}
|
|
204
|
-
subscribe(next) {
|
|
205
|
-
return this.panelEventSubject.asObservable().subscribe(next);
|
|
206
|
-
}
|
|
207
|
-
emit(type, data) {
|
|
208
|
-
this.panelEventSubject.next({ type, data });
|
|
209
|
-
}
|
|
210
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: PanelEventService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
211
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: PanelEventService }); }
|
|
212
|
-
}
|
|
213
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: PanelEventService, decorators: [{
|
|
214
|
-
type: Injectable
|
|
215
|
-
}] });
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Deep-clones a ComponentNode tree and overrides trait defaults
|
|
219
|
-
* according to each node's bindings resolved against `item`.
|
|
220
|
-
*
|
|
221
|
-
* Each clone gets a new `id` (for Angular tracking / overlay registration)
|
|
222
|
-
* but shares the original node's CSS class by setting `styleId = originalId`.
|
|
223
|
-
* This means all repeated preview rows point to the same CSS rule block as the
|
|
224
|
-
* template node — the style engine only needs one rule per logical slot.
|
|
225
|
-
*/
|
|
226
|
-
function cloneNodesForItem(nodes, item) {
|
|
227
|
-
return nodes.map(node => {
|
|
228
|
-
// Capture the CSS-owning id before clone
|
|
229
|
-
const cssId = node.styleId ?? node.id;
|
|
230
|
-
const clone = JSON.parse(JSON.stringify(node));
|
|
231
|
-
clone.id = Math.random().toString(36).substr(2, 9);
|
|
232
|
-
// Point this clone and all its descendants at the original CSS rule.
|
|
233
|
-
clone.styleId = cssId;
|
|
234
|
-
// Mark as a preview clone: display-only, not selectable/editable on the canvas.
|
|
235
|
-
// (Distinct from styleId, which duplication/paste also uses.) The render directive
|
|
236
|
-
// skips overlay registration for nodes carrying this flag.
|
|
237
|
-
clone.__previewClone = true;
|
|
238
|
-
if (clone.bindings?.length && item) {
|
|
239
|
-
for (const binding of clone.bindings) {
|
|
240
|
-
// Path lookup AND pipes (UI "Filters"). Resolving the path alone left a `date` or
|
|
241
|
-
// `uppercase` filter unapplied on every preview row, so the canvas showed the raw
|
|
242
|
-
// value while the SSR engine showed the formatted one — see binding-filters-plan.md.
|
|
243
|
-
const value = resolveBoundValue(item, binding);
|
|
244
|
-
if (value !== undefined) {
|
|
245
|
-
for (const trait of clone.traits || []) {
|
|
246
|
-
if (trait.name === binding.prop) {
|
|
247
|
-
trait.default = value;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
if (clone.children?.length) {
|
|
254
|
-
clone.children = cloneNodesForItem(clone.children, item);
|
|
255
|
-
}
|
|
256
|
-
return clone;
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
|
|
260
36
|
/**
|
|
261
37
|
* Canvas data preview used to come from host-provided content services (IOX_CONTENT_SERVICE /
|
|
262
38
|
* IOX_SHOP_SERVICE), whose `getContent(contentType, orgId)` hit the ADMIN resources endpoint and
|
|
@@ -346,6 +122,14 @@ const BUILDER_CHROME_EN = Object.freeze({
|
|
|
346
122
|
'builder.chrome.deletePairTitle': 'Delete tab and its panel?',
|
|
347
123
|
'builder.chrome.deletePairBody': '“{tab}” and the panel it shows go together: deleting one deletes both. Undo brings them back.',
|
|
348
124
|
'builder.chrome.deletePair': 'Delete tab and its panel',
|
|
125
|
+
'builder.chrome.kindSegmented': 'Segmented',
|
|
126
|
+
'builder.chrome.addOption': 'Add option',
|
|
127
|
+
'builder.chrome.refuseLockedSegmentedPartMove': 'This part belongs to its Segmented control — an option stays in it, and its badge in its option.',
|
|
128
|
+
'builder.chrome.refuseSegmentedAccepts': 'A Segmented control holds only its options. Put it beside the control instead.',
|
|
129
|
+
'builder.chrome.refuseSegmentedMinOptions': 'A Segmented control keeps at least two options. Delete the whole control instead.',
|
|
130
|
+
'builder.chrome.refuseSegmentedMaxOptions': 'A Segmented control holds four options at most — past four, use Tabs or a dropdown.',
|
|
131
|
+
'builder.chrome.auditShownBeforeControl': 'Shows “when an item is on” but comes before {control} in reading order — a screen reader reads it before the choice that changes it. Move it after the control.',
|
|
132
|
+
'builder.chrome.auditUnnamedGroup': 'A Segmented control with no accessible name — a screen reader says “radio group” and nothing more. Name it in Bindings ▸ Accessibility.',
|
|
349
133
|
'builder.chrome.cancel': 'Cancel',
|
|
350
134
|
'builder.chrome.fromContent': 'From {source} · {count} rows',
|
|
351
135
|
'builder.chrome.emptySource': 'No rows in “{alias}”.',
|
|
@@ -380,194 +164,26 @@ const CANVAS_EMPTY_IMAGE_LABEL_EN = BUILDER_CHROME_EN[CANVAS_EMPTY_IMAGE_LABEL_K
|
|
|
380
164
|
*/
|
|
381
165
|
const EDITOR_CHROME_ATTRIBUTE = 'data-iox-chrome';
|
|
382
166
|
|
|
383
|
-
/** The
|
|
384
|
-
|
|
385
|
-
return ds?.request?.cms?.contentType || ds?.request?.shop?.contentType || '';
|
|
386
|
-
}
|
|
167
|
+
/** The node type that scopes a form. Kept here so the rule has one name, not a scattered literal. */
|
|
168
|
+
const FORM_TYPE = 'Form';
|
|
387
169
|
/**
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
170
|
+
* The form the current selection belongs to — the NEAREST Form ancestor, or the selection itself
|
|
171
|
+
* when a Form is selected.
|
|
172
|
+
*
|
|
173
|
+
* This is the selection-side expression of decision D1: a field belongs to its nearest ancestor
|
|
174
|
+
* form. Searching the path from the deepest node outward is what makes "nearest" true, and it is
|
|
175
|
+
* why a nested form (were one ever allowed) could not capture an outer form's toolbar.
|
|
176
|
+
*
|
|
177
|
+
* @param path Ancestor chain from the root down to the selected node, as `selectedPath` holds it.
|
|
391
178
|
*/
|
|
392
|
-
function
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
* new ids. Used by every author-time clone path (duplicate, paste, block/external drop) so a copied
|
|
401
|
-
* structure stays self-consistent.
|
|
402
|
-
*
|
|
403
|
-
* Absolute node-id references that break under duplication if left unmapped:
|
|
404
|
-
* - `stateTriggers[state]` — the trigger-hover target (see style-engine.md → Targeted states)
|
|
405
|
-
* - interaction `action.target` / `reverseActions.target`
|
|
406
|
-
* A reference pointing OUTSIDE the cloned subtree (id not in the map) is left untouched — the copy
|
|
407
|
-
* intentionally still points at the external node.
|
|
408
|
-
*
|
|
409
|
-
* TRAIT values are NOT ids and are copied verbatim — with one exception. A trait that carries an
|
|
410
|
-
* IDENTITY rather than a setting must be reissued, or the copy silently shares the original's
|
|
411
|
-
* identity. `formId` is the first: two forms with the same id share one submissions stream, and the
|
|
412
|
-
* server resolves a submission against whichever page it reaches first. See REGENERATED_TRAITS.
|
|
413
|
-
*
|
|
414
|
-
* `styleId` strategy:
|
|
415
|
-
* - `'clear'` (default) — drop styleId so the clone OWNS its own CSS rule (independent copy).
|
|
416
|
-
* - `'regenerate'` — keep a styleId but give it a fresh value (legacy builder.component path).
|
|
417
|
-
*/
|
|
418
|
-
/**
|
|
419
|
-
* Trait names whose value is an IDENTITY, not a setting, and must therefore be reissued on every
|
|
420
|
-
* clone. Add a name here the moment a trait starts identifying something rather than describing it.
|
|
421
|
-
*
|
|
422
|
-
* `formId` — a form's submissions stream. Duplicating a page, pasting a form or dropping a saved
|
|
423
|
-
* block twice used to produce two forms claiming the same stream.
|
|
424
|
-
*/
|
|
425
|
-
const REGENERATED_TRAITS = new Set(['formId']);
|
|
426
|
-
/** Reissue identity-carrying traits on a node, preserving the original's prefix where it had one. */
|
|
427
|
-
function reissueIdentityTraits(node) {
|
|
428
|
-
for (const trait of (node.traits ?? [])) {
|
|
429
|
-
if (!trait?.name || !REGENERATED_TRAITS.has(trait.name))
|
|
430
|
-
continue;
|
|
431
|
-
if (trait.default === undefined || trait.default === '')
|
|
432
|
-
continue;
|
|
433
|
-
// Keep the human-readable prefix a preset gave it (`contact-a1b2c3` → `contact-<new>`), so a
|
|
434
|
-
// duplicated contact form is still recognisably a contact form in the inbox.
|
|
435
|
-
const prefix = String(trait.default).split('-')[0];
|
|
436
|
-
trait.default = `${prefix}-${freshId()}`;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
/**
|
|
440
|
-
* A NEW node is born with its identity: an empty identity trait (a palette Form's `formId`) is issued,
|
|
441
|
-
* one already set (a preset's `contact-…`) is kept. A Form inserted from the palette used to keep
|
|
442
|
-
* `formId: ''` — its submissions posted to `forms//submit` and nothing was ever stored.
|
|
443
|
-
*/
|
|
444
|
-
function issueIdentityTraits(root) {
|
|
445
|
-
for (const trait of (root.traits ?? [])) {
|
|
446
|
-
if (!trait?.name || !REGENERATED_TRAITS.has(trait.name))
|
|
447
|
-
continue;
|
|
448
|
-
if (trait.default === undefined || trait.default === '')
|
|
449
|
-
trait.default = `${IDENTITY_PREFIX[trait.name] ?? trait.name}-${freshId()}`;
|
|
450
|
-
}
|
|
451
|
-
for (const child of root.children ?? [])
|
|
452
|
-
if (child)
|
|
453
|
-
issueIdentityTraits(child);
|
|
454
|
-
}
|
|
455
|
-
/** The readable prefix a freshly issued identity carries (`form-a1b2c3`). */
|
|
456
|
-
const IDENTITY_PREFIX = { formId: 'form' };
|
|
457
|
-
function regenerateNodeIds(root, opts = {}) {
|
|
458
|
-
const idMap = new Map();
|
|
459
|
-
// A composite item's identity (CC2 `itemKey`) is reissued per copy, consistently inside it: a duplicated
|
|
460
|
-
// accordion item must not share its original's ARIA ids (composite plan §3).
|
|
461
|
-
const keyMap = new Map();
|
|
462
|
-
const assign = (node) => {
|
|
463
|
-
const oldId = node.id;
|
|
464
|
-
node.id = freshId();
|
|
465
|
-
reissueIdentityTraits(node);
|
|
466
|
-
if (node.itemKey) {
|
|
467
|
-
if (!keyMap.has(node.itemKey))
|
|
468
|
-
keyMap.set(node.itemKey, `k-${freshId()}`);
|
|
469
|
-
node.itemKey = keyMap.get(node.itemKey);
|
|
470
|
-
}
|
|
471
|
-
if (opts.styleId === 'regenerate') {
|
|
472
|
-
if (node.styleId)
|
|
473
|
-
node.styleId = freshId();
|
|
474
|
-
}
|
|
475
|
-
else {
|
|
476
|
-
node.styleId = undefined;
|
|
477
|
-
}
|
|
478
|
-
if (oldId)
|
|
479
|
-
idMap.set(oldId, node.id);
|
|
480
|
-
(node.children || []).forEach(assign);
|
|
481
|
-
};
|
|
482
|
-
assign(root);
|
|
483
|
-
remapNodeReferences(root, idMap);
|
|
484
|
-
}
|
|
485
|
-
/** Remap absolute node-id references inside a subtree to their new ids (per `idMap`). Ids not in
|
|
486
|
-
* the map (references to nodes outside the cloned subtree) are preserved. */
|
|
487
|
-
function remapNodeReferences(node, idMap) {
|
|
488
|
-
const triggers = node.stateTriggers;
|
|
489
|
-
if (triggers) {
|
|
490
|
-
for (const state of Object.keys(triggers)) {
|
|
491
|
-
const mapped = idMap.get(triggers[state]);
|
|
492
|
-
if (mapped)
|
|
493
|
-
triggers[state] = mapped;
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
for (const interaction of node.interactions || []) {
|
|
497
|
-
const actions = [...(interaction.actions || []), ...(interaction.reverseActions || [])];
|
|
498
|
-
for (const action of actions) {
|
|
499
|
-
if (action?.target && action.target !== 'self') {
|
|
500
|
-
const mapped = idMap.get(action.target);
|
|
501
|
-
if (mapped)
|
|
502
|
-
action.target = mapped;
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
(node.children || []).forEach(child => remapNodeReferences(child, idMap));
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
/**
|
|
510
|
-
* The text on the drop indicator's chip — "into Card grid · position 2 of 3" (B2).
|
|
511
|
-
*
|
|
512
|
-
* The most common drag failure is landing a node one level off and not noticing; naming the PARENT
|
|
513
|
-
* and the slot makes the target unambiguous before the pointer is released.
|
|
514
|
-
*
|
|
515
|
-
* @param containerName The target container's label or type; blank / unknown → "the page".
|
|
516
|
-
* @param index Insert index within the container (0-based).
|
|
517
|
-
* @param childCount How many children it has now — the node being dropped makes `childCount + 1`.
|
|
518
|
-
*/
|
|
519
|
-
function dropTargetLabel(containerName, index, childCount) {
|
|
520
|
-
const name = (containerName ?? '').trim() || 'the page';
|
|
521
|
-
const total = Math.max(0, childCount) + 1;
|
|
522
|
-
const position = Math.min(Math.max(index, 0), total - 1) + 1;
|
|
523
|
-
return `into ${name} · position ${position} of ${total}`;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
/**
|
|
527
|
-
* Per-frame exponential smoothing step — moves `current` a fraction of the way
|
|
528
|
-
* toward `target`. Used to ease the drag ghost toward the pointer instead of
|
|
529
|
-
* snapping to it 1:1 on every dragmove (see DragEngineService._ghostFollowStep).
|
|
530
|
-
*/
|
|
531
|
-
function easeToward(current, target, smoothing) {
|
|
532
|
-
return current + (target - current) * smoothing;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
/**
|
|
536
|
-
* Decides which element a drag gesture should actually operate on.
|
|
537
|
-
*
|
|
538
|
-
* A drag must never silently descend into a nested child just because the pointer happened to
|
|
539
|
-
* be over it while its ANCESTOR is the currently selected node, and the overlay's drag-handle
|
|
540
|
-
* (which carries no node of its own — it lives outside the canvas DOM entirely) must always
|
|
541
|
-
* redirect to whatever is currently selected. In both cases the redirect target is the
|
|
542
|
-
* currently-selected element; otherwise the gesture proceeds on its natural origin unchanged.
|
|
543
|
-
*/
|
|
544
|
-
function resolveDragTarget(el, selectedElement, isDragHandle) {
|
|
545
|
-
if (!selectedElement || el === selectedElement)
|
|
546
|
-
return el;
|
|
547
|
-
const isDescendantOfSelected = selectedElement.contains(el);
|
|
548
|
-
return (isDragHandle || isDescendantOfSelected) ? selectedElement : el;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
/** The node type that scopes a form. Kept here so the rule has one name, not a scattered literal. */
|
|
552
|
-
const FORM_TYPE = 'Form';
|
|
553
|
-
/**
|
|
554
|
-
* The form the current selection belongs to — the NEAREST Form ancestor, or the selection itself
|
|
555
|
-
* when a Form is selected.
|
|
556
|
-
*
|
|
557
|
-
* This is the selection-side expression of decision D1: a field belongs to its nearest ancestor
|
|
558
|
-
* form. Searching the path from the deepest node outward is what makes "nearest" true, and it is
|
|
559
|
-
* why a nested form (were one ever allowed) could not capture an outer form's toolbar.
|
|
560
|
-
*
|
|
561
|
-
* @param path Ancestor chain from the root down to the selected node, as `selectedPath` holds it.
|
|
562
|
-
*/
|
|
563
|
-
function nearestFormNode(path) {
|
|
564
|
-
if (!path?.length)
|
|
565
|
-
return null;
|
|
566
|
-
for (let i = path.length - 1; i >= 0; i--) {
|
|
567
|
-
if (path[i]?.type === FORM_TYPE)
|
|
568
|
-
return path[i];
|
|
569
|
-
}
|
|
570
|
-
return null;
|
|
179
|
+
function nearestFormNode(path) {
|
|
180
|
+
if (!path?.length)
|
|
181
|
+
return null;
|
|
182
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
183
|
+
if (path[i]?.type === FORM_TYPE)
|
|
184
|
+
return path[i];
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
571
187
|
}
|
|
572
188
|
|
|
573
189
|
/**
|
|
@@ -614,6 +230,14 @@ var InsertRefusal;
|
|
|
614
230
|
/** A tab or a panel deleted ALONE: the pair goes together — the host offers "Delete tab and its panel". Never said
|
|
615
231
|
* as a refusal toast. */
|
|
616
232
|
InsertRefusal["PairedPart"] = "pairedPart";
|
|
233
|
+
/** A Segmented control's option (or its badge) moved out of it (CC4b). */
|
|
234
|
+
InsertRefusal["LockedSegmentedPartMove"] = "lockedSegmentedPartMove";
|
|
235
|
+
/** Something other than an option dropped into a Segmented control (CC4b). */
|
|
236
|
+
InsertRefusal["SegmentedAccepts"] = "segmentedAccepts";
|
|
237
|
+
/** A Segmented control's last two options (Q40): it keeps at least two. */
|
|
238
|
+
InsertRefusal["SegmentedMinOptions"] = "segmentedMinOptions";
|
|
239
|
+
/** A fifth option (Q40): a Segmented control holds four at most. */
|
|
240
|
+
InsertRefusal["SegmentedMaxOptions"] = "segmentedMaxOptions";
|
|
617
241
|
})(InsertRefusal || (InsertRefusal = {}));
|
|
618
242
|
const FORM_STATE_BLOCK_TYPES = new Set(['FormSuccess', 'FormError']);
|
|
619
243
|
/** The builder-chrome label that says each refusal on screen (English in `BUILDER_CHROME_EN`). */
|
|
@@ -635,6 +259,10 @@ const INSERT_REFUSAL_LABEL_KEYS = Object.freeze({
|
|
|
635
259
|
[InsertRefusal.TabsAccepts]: 'builder.chrome.refuseTabsAccepts',
|
|
636
260
|
[InsertRefusal.LastTab]: 'builder.chrome.refuseLastTab',
|
|
637
261
|
[InsertRefusal.PairedPart]: 'builder.chrome.deletePairTitle',
|
|
262
|
+
[InsertRefusal.LockedSegmentedPartMove]: 'builder.chrome.refuseLockedSegmentedPartMove',
|
|
263
|
+
[InsertRefusal.SegmentedAccepts]: 'builder.chrome.refuseSegmentedAccepts',
|
|
264
|
+
[InsertRefusal.SegmentedMinOptions]: 'builder.chrome.refuseSegmentedMinOptions',
|
|
265
|
+
[InsertRefusal.SegmentedMaxOptions]: 'builder.chrome.refuseSegmentedMaxOptions',
|
|
638
266
|
});
|
|
639
267
|
/** The refusals that are not said as a toast: the host asks instead (a tab's pair). */
|
|
640
268
|
const SILENT_REFUSALS = new Set([InsertRefusal.PairedPart]);
|
|
@@ -649,56 +277,894 @@ function lockedRefusal(node, edit) {
|
|
|
649
277
|
return move ? InsertRefusal.LockedSliderPartMove : InsertRefusal.LockedSliderPause;
|
|
650
278
|
if (isTabsPart(node))
|
|
651
279
|
return move ? InsertRefusal.LockedTabsPartMove : InsertRefusal.LockedTabsPart;
|
|
280
|
+
if (isSegmentedPart(node))
|
|
281
|
+
return InsertRefusal.LockedSegmentedPartMove;
|
|
652
282
|
return move ? InsertRefusal.LockedPartMove : InsertRefusal.LockedPart;
|
|
653
283
|
}
|
|
654
|
-
/** The refusal in the host's language (the library's English when no host resolver is provided). */
|
|
655
|
-
function insertRefusalLabel(reason, labels = defaultBuilderLabels) {
|
|
656
|
-
const key = INSERT_REFUSAL_LABEL_KEYS[reason];
|
|
657
|
-
return labels(key, BUILDER_CHROME_EN[key]);
|
|
284
|
+
/** The refusal in the host's language (the library's English when no host resolver is provided). */
|
|
285
|
+
function insertRefusalLabel(reason, labels = defaultBuilderLabels) {
|
|
286
|
+
const key = INSERT_REFUSAL_LABEL_KEYS[reason];
|
|
287
|
+
return labels(key, BUILDER_CHROME_EN[key]);
|
|
288
|
+
}
|
|
289
|
+
function containsType(node, type) {
|
|
290
|
+
if (node.type === type)
|
|
291
|
+
return true;
|
|
292
|
+
return (node.children ?? []).some(child => containsType(child, type));
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Why `node` may not be inserted under `ancestors` (outermost first, the new parent last; empty = the page),
|
|
296
|
+
* or null when it may. `node` is the whole subtree being placed — a Container holding a Form is refused
|
|
297
|
+
* inside a Form just as a bare Form is, and a link that holds a form is refused wherever it goes. Reordering a node inside its CURRENT parent is not an insert and
|
|
298
|
+
* never asks.
|
|
299
|
+
*/
|
|
300
|
+
function insertRefusal(node, ancestors) {
|
|
301
|
+
if (!node)
|
|
302
|
+
return null;
|
|
303
|
+
if (node.type && FORM_STATE_BLOCK_TYPES.has(node.type))
|
|
304
|
+
return InsertRefusal.StateBlockAlone;
|
|
305
|
+
if (ancestors.some(a => a?.type === FORM_TYPE) && containsType(node, FORM_TYPE))
|
|
306
|
+
return InsertRefusal.NestedForm;
|
|
307
|
+
if (formContentInHost(node, ancestors.some(isInteractiveHost)))
|
|
308
|
+
return InsertRefusal.FormInInteractive;
|
|
309
|
+
const parent = ancestors[ancestors.length - 1];
|
|
310
|
+
const structural = structureRefusal(node, StructureEdit.Move, ancestors);
|
|
311
|
+
if (structural === StructureRefusal.NotAccepted)
|
|
312
|
+
return isSegmentedPart(parent) ? InsertRefusal.SegmentedAccepts : InsertRefusal.TabsAccepts;
|
|
313
|
+
if (structural)
|
|
314
|
+
return lockedRefusal(node, StructureEdit.Move);
|
|
315
|
+
// Q40: a fifth option (a paste, a duplicate, a drop from another control) is refused.
|
|
316
|
+
if (node.role === SegmentedRole.Option && parent?.role === SegmentedRole.Segmented
|
|
317
|
+
&& (parent.children ?? []).filter(c => c?.role === SegmentedRole.Option).length >= SEGMENTED_MAX_OPTIONS)
|
|
318
|
+
return InsertRefusal.SegmentedMaxOptions;
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
/** Why `node` may not be REMOVED from where it is (deleted, cut, made global), or null when it may: a
|
|
322
|
+
* composite's locked part stays — its Parts switch or deleting the whole composite are the ways out — an
|
|
323
|
+
* accordion keeps its last item, Tabs their last tab, and a tab or a panel never leaves without its pair (the host
|
|
324
|
+
* offers to delete both). `siblings` = the list `node` sits in (where the caller has it). */
|
|
325
|
+
function removalRefusal(node, siblings) {
|
|
326
|
+
if (structureRefusal(node, StructureEdit.Delete))
|
|
327
|
+
return lockedRefusal(node, StructureEdit.Delete);
|
|
328
|
+
if (node?.role === AccordionRole.Item && siblings && siblings.filter(s => s?.role === AccordionRole.Item).length <= 1)
|
|
329
|
+
return InsertRefusal.LastItem;
|
|
330
|
+
// A Segmented control's option goes alone (it has no panel) — and the last two stay (Q40).
|
|
331
|
+
if (node?.role === SegmentedRole.Option) {
|
|
332
|
+
return siblings && siblings.filter(s => s?.role === SegmentedRole.Option).length <= SEGMENTED_MIN_OPTIONS ? InsertRefusal.SegmentedMinOptions : null;
|
|
333
|
+
}
|
|
334
|
+
if (isExclusiveItemPart(node)) {
|
|
335
|
+
const role = node.role === TabsRole.Trigger ? TabsRole.Trigger : TabsRole.Panel;
|
|
336
|
+
if (siblings && siblings.filter(s => s?.role === role).length <= 1)
|
|
337
|
+
return InsertRefusal.LastTab;
|
|
338
|
+
return InsertRefusal.PairedPart;
|
|
339
|
+
}
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Nodes styled "WHEN AN ITEM IS ON" from outside it (composite T20; CC4b's price "when Yearly is on") — what the builder
|
|
345
|
+
* says about them: hidden at rest in the layer tree (the eye-slash, board 10), the reading-order audit finding (Q34) and
|
|
346
|
+
* the Style panel's "never shown" warning (Q38). Pure: every caller passes the trees.
|
|
347
|
+
*/
|
|
348
|
+
const HIDDEN = 'none';
|
|
349
|
+
const DISPLAY = 'display';
|
|
350
|
+
const tree = (n) => n;
|
|
351
|
+
const cssId = (n) => nodeCssId(n) ?? n.id ?? '';
|
|
352
|
+
const stateOf = (n, state) => n.stateStyles?.[state];
|
|
353
|
+
/** The item a node's `trigger-on` state SHOWS it by — its state changes `display` — as that item's css id, else null. */
|
|
354
|
+
function displayTriggerOf(node) {
|
|
355
|
+
const trigger = node.stateTriggers?.[ITEM_OPEN_STATE];
|
|
356
|
+
const display = stateOf(node, ITEM_OPEN_STATE)?.[DISPLAY];
|
|
357
|
+
return trigger && display !== undefined && display !== '' ? trigger : null;
|
|
358
|
+
}
|
|
359
|
+
/** True when the node is hidden at its base (its own `display: none`). */
|
|
360
|
+
function hiddenAtBase(node) {
|
|
361
|
+
return String(readStyle(tree(node), DISPLAY) ?? '') === HIDDEN;
|
|
362
|
+
}
|
|
363
|
+
/** The css ids of every Manual composite item ON at rest — a Segmented control's default option, a Tabs' first tab, an
|
|
364
|
+
* accordion's items open by default. From-content rows are not nodes and are left out. */
|
|
365
|
+
function itemsOnAtRest(roots) {
|
|
366
|
+
const on = new Set();
|
|
367
|
+
const visit = (n) => {
|
|
368
|
+
if (isSegmentedRoot(n)) {
|
|
369
|
+
const key = segmentedDefaultOn(tree(n));
|
|
370
|
+
const option = segmentedOptions(tree(n)).find((o, i) => (o.itemKey || `item-${i}`) === key);
|
|
371
|
+
if (option)
|
|
372
|
+
on.add(cssId(option));
|
|
373
|
+
}
|
|
374
|
+
else if (isTabsRoot(n) && !isTabsFromContent(tree(n))) {
|
|
375
|
+
const [first] = tabsTriggers(tree(n));
|
|
376
|
+
if (first)
|
|
377
|
+
on.add(cssId(first));
|
|
378
|
+
}
|
|
379
|
+
else if (isAccordionRoot(n) && !isAccordionFromContent(tree(n))) {
|
|
380
|
+
const items = accordionItems(tree(n));
|
|
381
|
+
const open = initialAccordionOpen(items.map((it, i) => it.itemKey || `item-${i}`), accordionBehaviour(tree(n)).defaultOn);
|
|
382
|
+
items.forEach((it, i) => { if (open.has(it.itemKey || `item-${i}`))
|
|
383
|
+
on.add(cssId(it)); });
|
|
384
|
+
}
|
|
385
|
+
(n.children ?? []).forEach(visit);
|
|
386
|
+
};
|
|
387
|
+
roots.forEach(visit);
|
|
388
|
+
return on;
|
|
389
|
+
}
|
|
390
|
+
/** Hidden at rest because it shows only "when an item is on" and that item is not on at rest (board 10's eye-slash on
|
|
391
|
+
* Price · Yearly; Price · Monthly is on screen at rest, Monthly being the default). */
|
|
392
|
+
function hiddenUntilItemOn(node, onAtRest) {
|
|
393
|
+
const trigger = displayTriggerOf(node);
|
|
394
|
+
if (!trigger || !hiddenAtBase(node))
|
|
395
|
+
return false;
|
|
396
|
+
return String(stateOf(node, ITEM_OPEN_STATE)?.[DISPLAY]) !== HIDDEN && !onAtRest.has(trigger);
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Q38: a node whose base is `display: none` and that NOTHING shows — no state, no breakpoint layer, no interaction that
|
|
400
|
+
* targets it. Deleting the "when Yearly is on" state of a price makes the price disappear for good; the Style panel
|
|
401
|
+
* says so. `targeted` = the ids interactions target anywhere on the page.
|
|
402
|
+
*/
|
|
403
|
+
function neverShown(node, targeted = new Set()) {
|
|
404
|
+
if (!hiddenAtBase(node))
|
|
405
|
+
return false;
|
|
406
|
+
if (node.id && targeted.has(node.id))
|
|
407
|
+
return false;
|
|
408
|
+
const shows = (map) => !!map && map[DISPLAY] !== undefined && map[DISPLAY] !== '' && map[DISPLAY] !== HIDDEN;
|
|
409
|
+
for (const state of Object.values((node.stateStyles ?? {})))
|
|
410
|
+
if (shows(state))
|
|
411
|
+
return false;
|
|
412
|
+
for (const layer of Object.values(node.responsive ?? {})) {
|
|
413
|
+
if (shows(layer?.styleProps))
|
|
414
|
+
return false;
|
|
415
|
+
for (const state of Object.values((layer?.stateStyles ?? {})))
|
|
416
|
+
if (shows(state))
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
/** Every node id an interaction on the page targets (its actions' and reverse actions' `target`). */
|
|
422
|
+
function interactionTargets(roots) {
|
|
423
|
+
const out = new Set();
|
|
424
|
+
const visit = (n) => {
|
|
425
|
+
for (const ix of (n.interactions ?? [])) {
|
|
426
|
+
for (const a of [...(ix.actions ?? []), ...(ix.reverseActions ?? [])])
|
|
427
|
+
if (a.target)
|
|
428
|
+
out.add(a.target);
|
|
429
|
+
}
|
|
430
|
+
(n.children ?? []).forEach(visit);
|
|
431
|
+
};
|
|
432
|
+
roots.forEach(visit);
|
|
433
|
+
return out;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Q34 (no live region): a screen reader hears the new price because the control comes BEFORE it in reading order — so
|
|
437
|
+
* a node shown "when X is on" that sits before X (outside X) is read before the choice that changes it. One finding per
|
|
438
|
+
* such node. Reading order = document order across `roots` (the page's globals before / after it as given).
|
|
439
|
+
*/
|
|
440
|
+
function shownBeforeControl(roots) {
|
|
441
|
+
const order = new Map();
|
|
442
|
+
const byCss = new Map();
|
|
443
|
+
const nodes = [];
|
|
444
|
+
const visit = (n) => {
|
|
445
|
+
order.set(cssId(n), nodes.length);
|
|
446
|
+
if (!byCss.has(cssId(n)))
|
|
447
|
+
byCss.set(cssId(n), n);
|
|
448
|
+
nodes.push(n);
|
|
449
|
+
(n.children ?? []).forEach(visit);
|
|
450
|
+
};
|
|
451
|
+
roots.forEach(visit);
|
|
452
|
+
const out = [];
|
|
453
|
+
for (const node of nodes) {
|
|
454
|
+
const trigger = displayTriggerOf(node);
|
|
455
|
+
const control = trigger ? byCss.get(trigger) : undefined;
|
|
456
|
+
if (!control || contains(control, node) || contains(node, control))
|
|
457
|
+
continue;
|
|
458
|
+
if ((order.get(cssId(node)) ?? 0) < (order.get(trigger) ?? 0))
|
|
459
|
+
out.push({ node, control });
|
|
460
|
+
}
|
|
461
|
+
return out;
|
|
462
|
+
}
|
|
463
|
+
function contains(outer, inner) {
|
|
464
|
+
return (outer.children ?? []).some(c => c === inner || contains(c, inner));
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* STRUCTURE OPS (A4) — the one-click layout changes people actually make at a narrow width, as pure
|
|
469
|
+
* "what to write" functions. Each returns a flat style map the caller stores in the node's
|
|
470
|
+
* `responsive[bp]` layer (or in the base styles at Base) through the shared `setOverride`.
|
|
471
|
+
*
|
|
472
|
+
* All of it is ordinary CSS the SSR engine already renders — there is no second tree and no
|
|
473
|
+
* per-width HTML. See responsive-breakpoints-plan.md (D5).
|
|
474
|
+
*/
|
|
475
|
+
/** CSS lengths that are a FIXED pixel size — the ones that stop a layout fitting a phone. */
|
|
476
|
+
const PX_LENGTH = /^-?\d+(\.\d+)?px$/i;
|
|
477
|
+
/**
|
|
478
|
+
* Hide / show at a width. Hiding is `display: none` INSIDE that breakpoint's rules — the node stays
|
|
479
|
+
* in the DOM and in the SSR output (one HTML document serves every width), so hidden content still
|
|
480
|
+
* ships bytes and is still indexed. Showing again clears the override rather than guessing a value.
|
|
481
|
+
*
|
|
482
|
+
* @returns the props to write, or `null` meaning "clear the display override at this breakpoint".
|
|
483
|
+
*/
|
|
484
|
+
function visibilityProps(hidden) {
|
|
485
|
+
return hidden ? { display: 'none' } : null;
|
|
486
|
+
}
|
|
487
|
+
/** True when this node is hidden by the resolved styles at some width. */
|
|
488
|
+
function isHiddenAt(resolvedStyleProps) {
|
|
489
|
+
return String(resolvedStyleProps?.['display'] ?? '') === 'none';
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Stack a container's children in one column — the most common mobile edit, and four separate
|
|
493
|
+
* controls today (display, direction, wrap, columns). A grid becomes a single-column grid; anything
|
|
494
|
+
* else becomes a column flex box.
|
|
495
|
+
*/
|
|
496
|
+
function stackChildrenProps(resolvedStyleProps) {
|
|
497
|
+
const display = String(resolvedStyleProps?.['display'] ?? '');
|
|
498
|
+
if (display === 'grid' || display === 'inline-grid') {
|
|
499
|
+
return { gridTemplateColumns: 'repeat(1, minmax(0, 1fr))' };
|
|
500
|
+
}
|
|
501
|
+
return { display: 'flex', flexDirection: 'column', flexWrap: 'nowrap' };
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Collapse a grid to `count` columns (a flex row is stacked instead — it has no column track).
|
|
505
|
+
* `count` is clamped to at least 1.
|
|
506
|
+
*/
|
|
507
|
+
function collapseColumnsProps(resolvedStyleProps, count) {
|
|
508
|
+
const columns = Math.max(1, Math.floor(count));
|
|
509
|
+
const display = String(resolvedStyleProps?.['display'] ?? '');
|
|
510
|
+
if (display === 'grid' || display === 'inline-grid') {
|
|
511
|
+
return { gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` };
|
|
512
|
+
}
|
|
513
|
+
return columns === 1 ? stackChildrenProps(resolvedStyleProps) : { flexWrap: 'wrap' };
|
|
514
|
+
}
|
|
515
|
+
/** How many equal columns a `repeat(N, …)` track list describes, or null when it isn't one. */
|
|
516
|
+
function columnCountOf(resolvedStyleProps) {
|
|
517
|
+
const match = /^repeat\(\s*(\d+)\s*,/.exec(String(resolvedStyleProps?.['gridTemplateColumns'] ?? ''));
|
|
518
|
+
return match ? Number(match[1]) : null;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Make fixed pixel sizes fluid: `width: 520px` → `width: 100%` with the original kept as
|
|
522
|
+
* `max-width`, so the element still never exceeds its designed size on a wide screen. `minWidth` in
|
|
523
|
+
* px is dropped to 0 — it is what actually forces horizontal scrolling on a phone.
|
|
524
|
+
*
|
|
525
|
+
* @returns the props to write; EMPTY when there is nothing fixed to fluidise.
|
|
526
|
+
*/
|
|
527
|
+
function fluidiseProps(resolvedStyleProps) {
|
|
528
|
+
const out = {};
|
|
529
|
+
const width = String(resolvedStyleProps?.['width'] ?? '');
|
|
530
|
+
const minWidth = String(resolvedStyleProps?.['minWidth'] ?? '');
|
|
531
|
+
if (PX_LENGTH.test(width)) {
|
|
532
|
+
out['width'] = '100%';
|
|
533
|
+
const maxWidth = String(resolvedStyleProps?.['maxWidth'] ?? '');
|
|
534
|
+
if (!maxWidth || maxWidth === 'none')
|
|
535
|
+
out['maxWidth'] = width;
|
|
536
|
+
}
|
|
537
|
+
if (PX_LENGTH.test(minWidth) && parseFloat(minWidth) > 0)
|
|
538
|
+
out['minWidth'] = '0px';
|
|
539
|
+
return out;
|
|
540
|
+
}
|
|
541
|
+
/** True when `fluidiseProps` would change anything — drives the button's disabled state. */
|
|
542
|
+
function canFluidise(resolvedStyleProps) {
|
|
543
|
+
return Object.keys(fluidiseProps(resolvedStyleProps)).length > 0;
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Reorder children AT ONE WIDTH using CSS `order`, leaving the tree — and so the DOM and
|
|
547
|
+
* screen-reader order — untouched. Returns one value per child BY CHILD INDEX: the `order` it must
|
|
548
|
+
* take, or `null` for "no override needed".
|
|
549
|
+
*
|
|
550
|
+
* Either every child gets an explicit `order` or none does: CSS defaults `order` to 0, so a partial
|
|
551
|
+
* set would mix overridden children with unordered ones and read as an arbitrary interleave.
|
|
552
|
+
*
|
|
553
|
+
* @param count How many children there are.
|
|
554
|
+
* @param from Index of the child being moved.
|
|
555
|
+
* @param to Index it should occupy.
|
|
556
|
+
*/
|
|
557
|
+
function reorderProps(count, from, to) {
|
|
558
|
+
const none = Array.from({ length: Math.max(0, count) }, () => null);
|
|
559
|
+
if (from < 0 || from >= count || to < 0 || to >= count || from === to)
|
|
560
|
+
return none;
|
|
561
|
+
// Visual order after the move, as a list of CHILD indices.
|
|
562
|
+
const sequence = Array.from({ length: count }, (_, i) => i);
|
|
563
|
+
sequence.splice(to, 0, ...sequence.splice(from, 1));
|
|
564
|
+
const orderByChild = none.slice();
|
|
565
|
+
sequence.forEach((childIndex, position) => { orderByChild[childIndex] = position; });
|
|
566
|
+
return orderByChild;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* RESPONSIVE AUDIT (A5) — find what is broken at a width, from the node tree plus ONE measurement
|
|
571
|
+
* pass. A builder that lets you author five widths must also tell you which of them are broken.
|
|
572
|
+
*
|
|
573
|
+
* Pure: the caller measures (one `getBoundingClientRect` per node, off the animation path) and
|
|
574
|
+
* passes the numbers in. Every finding that can be repaired carries the exact props to write, so a
|
|
575
|
+
* fix is one `setOverride` per key — one undo step, and it shows as an ordinary override.
|
|
576
|
+
*/
|
|
577
|
+
var AuditCode;
|
|
578
|
+
(function (AuditCode) {
|
|
579
|
+
AuditCode["Overflow"] = "overflow";
|
|
580
|
+
AuditCode["FixedWidth"] = "fixed-width";
|
|
581
|
+
AuditCode["TapTarget"] = "tap-target";
|
|
582
|
+
AuditCode["TinyText"] = "tiny-text";
|
|
583
|
+
AuditCode["UnboundedImage"] = "unbounded-image";
|
|
584
|
+
/** A slider showing more slides than the width can carry — each one unreadably narrow. */
|
|
585
|
+
AuditCode["SlidesOverflow"] = "slides-overflow";
|
|
586
|
+
/** An out-of-flow node whose box lands outside the frame, so it is unreachable at this width. */
|
|
587
|
+
AuditCode["OffscreenAbsolute"] = "offscreen-absolute";
|
|
588
|
+
/** Form content inside a link or a button — a STRUCTURE finding, true at every width (form-nesting-audit.util). */
|
|
589
|
+
AuditCode["FormInLink"] = "form-in-link";
|
|
590
|
+
/** An icon-only button or link with no Accessible name — announced as an unnamed control (CC1 N6). */
|
|
591
|
+
AuditCode["UnnamedControl"] = "unnamed-control";
|
|
592
|
+
/** A composite whose items come from content, and its source returned no rows (CC2, board 9): the
|
|
593
|
+
* canvas shows the placeholder, the live site renders nothing. */
|
|
594
|
+
AuditCode["EmptySource"] = "empty-source";
|
|
595
|
+
/** CC4b (Q34): a node shown "when X is on" placed BEFORE its control X in reading order — a STRUCTURE finding. */
|
|
596
|
+
AuditCode["ShownBeforeControl"] = "shown-before-control";
|
|
597
|
+
/** CC4b: a Segmented control's radio group with no accessible name — a STRUCTURE finding. */
|
|
598
|
+
AuditCode["UnnamedGroup"] = "unnamed-group";
|
|
599
|
+
})(AuditCode || (AuditCode = {}));
|
|
600
|
+
/** A fix that changes the tree, not a style or a trait — applied as one page-history step. */
|
|
601
|
+
var StructureFix;
|
|
602
|
+
(function (StructureFix) {
|
|
603
|
+
/** Move the form out of the link or button holding it, to right after it. */
|
|
604
|
+
StructureFix["MoveFormOut"] = "move-form-out";
|
|
605
|
+
})(StructureFix || (StructureFix = {}));
|
|
606
|
+
var AuditSeverity;
|
|
607
|
+
(function (AuditSeverity) {
|
|
608
|
+
AuditSeverity["Error"] = "error";
|
|
609
|
+
AuditSeverity["Warning"] = "warning";
|
|
610
|
+
AuditSeverity["Info"] = "info";
|
|
611
|
+
})(AuditSeverity || (AuditSeverity = {}));
|
|
612
|
+
/** WCAG 2.5.8 minimum target size. */
|
|
613
|
+
const MIN_TAP_PX = 44;
|
|
614
|
+
/** Below this, iOS Safari zooms the page when an adjacent input is focused. */
|
|
615
|
+
const MIN_TEXT_PX = 12;
|
|
616
|
+
/** Types a visitor taps. */
|
|
617
|
+
const TAPPABLE = new Set(['Button', 'ButtonBlock', 'Link', 'LinkedContainer']);
|
|
618
|
+
/** Narrower than this, a slide shows too little of its content to be worth showing at all. */
|
|
619
|
+
const MIN_SLIDE_PX = 200;
|
|
620
|
+
/** Out-of-flow positioning that can put a node outside the frame. */
|
|
621
|
+
const OUT_OF_FLOW = new Set(['absolute', 'fixed']);
|
|
622
|
+
const nameOf = (node) => node.label || node.type;
|
|
623
|
+
const pxOf = (value) => {
|
|
624
|
+
const match = /^(-?\d+(?:\.\d+)?)px$/i.exec(String(value ?? ''));
|
|
625
|
+
return match ? Number(match[1]) : null;
|
|
626
|
+
};
|
|
627
|
+
/**
|
|
628
|
+
* Audit nodes laid out at `viewportWidth`. Returns findings ordered by severity (errors first),
|
|
629
|
+
* then by the order the nodes were given — so the worst thing is at the top of the list.
|
|
630
|
+
*/
|
|
631
|
+
function auditNodes(nodes, viewportWidth) {
|
|
632
|
+
const findings = [];
|
|
633
|
+
for (const node of nodes) {
|
|
634
|
+
const name = nameOf(node);
|
|
635
|
+
const styles = node.styleProps ?? {};
|
|
636
|
+
if (String(styles['display'] ?? '') === 'none')
|
|
637
|
+
continue; // hidden here — nothing to report
|
|
638
|
+
// 1. Wider than the viewport: the page scrolls sideways.
|
|
639
|
+
if (node.width > viewportWidth + 0.5) {
|
|
640
|
+
findings.push({
|
|
641
|
+
nodeId: node.id, nodeName: name, code: AuditCode.Overflow, severity: AuditSeverity.Error,
|
|
642
|
+
message: `${name} is ${Math.round(node.width)}px wide in a ${viewportWidth}px viewport — the page scrolls sideways.`,
|
|
643
|
+
fix: canFluidise(styles) ? fluidiseProps(styles) : { maxWidth: '100%' },
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
else if (canFluidise(styles)) {
|
|
647
|
+
// 2. A fixed px size that still fits, but will not on a narrower phone.
|
|
648
|
+
const width = pxOf(styles['width']);
|
|
649
|
+
findings.push({
|
|
650
|
+
nodeId: node.id, nodeName: name, code: AuditCode.FixedWidth, severity: AuditSeverity.Warning,
|
|
651
|
+
message: width !== null
|
|
652
|
+
? `${name} has a fixed width of ${width}px — it cannot shrink on a narrower screen.`
|
|
653
|
+
: `${name} has a fixed minimum width — it forces sideways scrolling below that size.`,
|
|
654
|
+
fix: fluidiseProps(styles),
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
// 3. Tap targets below the WCAG minimum.
|
|
658
|
+
if (TAPPABLE.has(node.type) && node.width > 0 && node.height > 0
|
|
659
|
+
&& (node.width < MIN_TAP_PX || node.height < MIN_TAP_PX)) {
|
|
660
|
+
findings.push({
|
|
661
|
+
nodeId: node.id, nodeName: name, code: AuditCode.TapTarget, severity: AuditSeverity.Warning,
|
|
662
|
+
message: `${name} is ${Math.round(node.width)}×${Math.round(node.height)}px — below the ${MIN_TAP_PX}px minimum tap target.`,
|
|
663
|
+
fix: { minHeight: `${MIN_TAP_PX}px`, minWidth: `${MIN_TAP_PX}px` },
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
// 4. Text too small to read (and small enough to trigger iOS zoom).
|
|
667
|
+
const fontSize = pxOf(styles['fontSize']);
|
|
668
|
+
if (fontSize !== null && fontSize < MIN_TEXT_PX) {
|
|
669
|
+
findings.push({
|
|
670
|
+
nodeId: node.id, nodeName: name, code: AuditCode.TinyText, severity: AuditSeverity.Warning,
|
|
671
|
+
message: `${name} renders at ${fontSize}px — under the ${MIN_TEXT_PX}px readable minimum.`,
|
|
672
|
+
fix: { fontSize: `${MIN_TEXT_PX}px` },
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
// 5. An image with a fixed height and no ratio crops unpredictably as the width changes.
|
|
676
|
+
if (node.type === 'Image' && pxOf(styles['height']) !== null
|
|
677
|
+
&& !String(styles['aspectRatio'] ?? '').trim().replace(/^auto$/, '')) {
|
|
678
|
+
findings.push({
|
|
679
|
+
nodeId: node.id, nodeName: name, code: AuditCode.UnboundedImage, severity: AuditSeverity.Info,
|
|
680
|
+
message: `${name} has a fixed height and no aspect ratio — it crops unpredictably as the width changes.`,
|
|
681
|
+
fix: { height: 'auto', aspectRatio: '16 / 9' },
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
// 6. A slider showing more slides than this width can carry. NOT a CSS problem: slide count
|
|
685
|
+
// is a component trait, so the fix is a per-breakpoint TRAIT override (P3-G1), not a style.
|
|
686
|
+
if (node.type === 'SliderContainer' && node.width > 0) {
|
|
687
|
+
const perView = Number(node.inputs?.['slidesPerView']);
|
|
688
|
+
if (Number.isFinite(perView) && perView > 1) {
|
|
689
|
+
const slideWidth = node.width / perView;
|
|
690
|
+
if (slideWidth < MIN_SLIDE_PX) {
|
|
691
|
+
const fits = Math.max(1, Math.floor(node.width / MIN_SLIDE_PX));
|
|
692
|
+
findings.push({
|
|
693
|
+
nodeId: node.id, nodeName: name, code: AuditCode.SlidesOverflow, severity: AuditSeverity.Warning,
|
|
694
|
+
message: `${name} shows ${perView} slides across ${Math.round(node.width)}px — ${Math.round(slideWidth)}px each. Show ${fits} at this width.`,
|
|
695
|
+
inputFix: { slidesPerView: fits },
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
// 7. An icon-only control with no name (WCAG 4.1.2): a screen reader says "button" and nothing
|
|
701
|
+
// else. No auto-fix — only the author knows what it does; the finding selects it.
|
|
702
|
+
if (node.unnamedControl) {
|
|
703
|
+
findings.push({
|
|
704
|
+
nodeId: node.id, nodeName: name, code: AuditCode.UnnamedControl, severity: AuditSeverity.Error,
|
|
705
|
+
message: `${name} shows only an icon and has no accessible name — screen readers announce it as an unnamed control. Name it in Bindings ▸ Accessibility.`,
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
// 7b. A composite from content whose source has no rows (CC2 R3): the visitor sees nothing there.
|
|
709
|
+
if (node.emptySource) {
|
|
710
|
+
findings.push({
|
|
711
|
+
nodeId: node.id, nodeName: name, code: AuditCode.EmptySource, severity: AuditSeverity.Warning,
|
|
712
|
+
message: `${name} in ${node.emptySource.section}: its source ${node.emptySource.alias} has no rows.`,
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
// 8. An out-of-flow node that has left the frame. Measured, not inferred: a negative offset
|
|
716
|
+
// is legitimate design until it actually puts the content beyond the edge.
|
|
717
|
+
if (OUT_OF_FLOW.has(String(styles['position'] ?? '')) && node.left !== undefined && node.width > 0) {
|
|
718
|
+
const right = node.left + node.width;
|
|
719
|
+
const offLeft = right <= 0.5;
|
|
720
|
+
const offRight = node.left >= viewportWidth - 0.5;
|
|
721
|
+
if (offLeft || offRight) {
|
|
722
|
+
findings.push({
|
|
723
|
+
nodeId: node.id, nodeName: name, code: AuditCode.OffscreenAbsolute, severity: AuditSeverity.Error,
|
|
724
|
+
message: `${name} is positioned ${offLeft ? 'left of' : 'beyond'} the ${viewportWidth}px viewport — it cannot be seen or tapped at this width.`,
|
|
725
|
+
// No blind auto-fix: the author's intent (a decorative overflow vs a mistake)
|
|
726
|
+
// is not knowable from geometry. Selecting the node is the useful action.
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
const rank = { [AuditSeverity.Error]: 0, [AuditSeverity.Warning]: 1, [AuditSeverity.Info]: 2 };
|
|
732
|
+
return findings
|
|
733
|
+
.map((finding, index) => ({ finding, index }))
|
|
734
|
+
.sort((a, b) => rank[a.finding.severity] - rank[b.finding.severity] || a.index - b.index)
|
|
735
|
+
.map(entry => entry.finding);
|
|
736
|
+
}
|
|
737
|
+
/** Counts per severity — the per-breakpoint summary strip. */
|
|
738
|
+
function summarise(findings) {
|
|
739
|
+
const out = { [AuditSeverity.Error]: 0, [AuditSeverity.Warning]: 0, [AuditSeverity.Info]: 0 };
|
|
740
|
+
for (const finding of findings)
|
|
741
|
+
out[finding.severity]++;
|
|
742
|
+
return out;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const SHOWN_BEFORE_KEY = 'builder.chrome.auditShownBeforeControl';
|
|
746
|
+
const UNNAMED_GROUP_KEY = 'builder.chrome.auditUnnamedGroup';
|
|
747
|
+
const CONTROL_PLACEHOLDER = '{control}';
|
|
748
|
+
/**
|
|
749
|
+
* The composites' STRUCTURE findings (CC4b, board 10) — true at every width, so the audit lists them beside the form
|
|
750
|
+
* nesting ones:
|
|
751
|
+
* - Q34: a node shown "when X is on" that sits BEFORE its control X in reading order — a screen reader reads it before
|
|
752
|
+
* the choice that changes it (there is no live region, on purpose);
|
|
753
|
+
* - a Segmented control's radio group with no accessible name.
|
|
754
|
+
* No auto-fix: selecting the finding selects the node.
|
|
755
|
+
*/
|
|
756
|
+
function compositeStructureFindings(page, globals, labels = defaultBuilderLabels) {
|
|
757
|
+
const say = (key) => labels(key, BUILDER_CHROME_EN[key]);
|
|
758
|
+
const findings = [];
|
|
759
|
+
for (const { node, control } of shownBeforeControl([...page, ...globals])) {
|
|
760
|
+
if (!node.id)
|
|
761
|
+
continue;
|
|
762
|
+
findings.push({
|
|
763
|
+
nodeId: node.id, nodeName: node.label || node.type, code: AuditCode.ShownBeforeControl, severity: AuditSeverity.Warning,
|
|
764
|
+
message: say(SHOWN_BEFORE_KEY).replace(CONTROL_PLACEHOLDER, exclusiveItemLabel(control) || control.label || control.type),
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
const visit = (n) => {
|
|
768
|
+
if (n.id && needsGroupName(n)) {
|
|
769
|
+
findings.push({ nodeId: n.id, nodeName: n.label || n.type, code: AuditCode.UnnamedGroup, severity: AuditSeverity.Error, message: say(UNNAMED_GROUP_KEY) });
|
|
770
|
+
}
|
|
771
|
+
(n.children ?? []).forEach(visit);
|
|
772
|
+
};
|
|
773
|
+
[...page, ...globals].forEach(visit);
|
|
774
|
+
return findings;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* node-path — pure ancestor-chain helpers for the builder's node tree.
|
|
779
|
+
*
|
|
780
|
+
* The builder needs a node's ancestor chain in several places: the breadcrumb bar,
|
|
781
|
+
* "select parent", and the parent-scoped style traits (Child Layout). Each of those
|
|
782
|
+
* used to re-walk the tree with its own private recursion. `nodeAncestorPath` is the
|
|
783
|
+
* single walk they all derive from.
|
|
784
|
+
*
|
|
785
|
+
* Builder-only on purpose: the SSR engine never needs a breadcrumb, so this stays in
|
|
786
|
+
* `iox-builder` rather than `@vectoriox/iox-ui` (which is the home for logic BOTH
|
|
787
|
+
* renderers consume). Structural helpers the two renderers share live in
|
|
788
|
+
* `@vectoriox/iox-ui` `style-compiler/tree-relations.ts`.
|
|
789
|
+
*/
|
|
790
|
+
/**
|
|
791
|
+
* The chain of nodes from a root down to (and including) `target`, matched by
|
|
792
|
+
* REFERENCE identity. Returns `null` when `target` is not in the tree.
|
|
793
|
+
*
|
|
794
|
+
* Depth-first, pre-order; first match wins. Reference identity (not `id`) is the right
|
|
795
|
+
* matcher here because the builder mutates nodes in place and repeater/slider clones
|
|
796
|
+
* deliberately share a template's `styleId` — matching by id would return the template's
|
|
797
|
+
* path for a clone.
|
|
798
|
+
*/
|
|
799
|
+
function nodeAncestorPath(roots, target) {
|
|
800
|
+
if (!roots?.length || !target)
|
|
801
|
+
return null;
|
|
802
|
+
for (const node of roots) {
|
|
803
|
+
if (node === target)
|
|
804
|
+
return [node];
|
|
805
|
+
const childPath = nodeAncestorPath(node.children, target);
|
|
806
|
+
if (childPath)
|
|
807
|
+
return [node, ...childPath];
|
|
808
|
+
}
|
|
809
|
+
return null;
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* The immediate parent of `target`, or `null` when it is a root node or not in the tree.
|
|
813
|
+
* Derived from `nodeAncestorPath` so there is exactly one tree walk to keep correct.
|
|
814
|
+
*/
|
|
815
|
+
function findParentOf(roots, target) {
|
|
816
|
+
const path = nodeAncestorPath(roots, target);
|
|
817
|
+
return path && path.length >= 2 ? path[path.length - 2] : null;
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* The ancestor chain across BOTH builder trees — the page layout and the global
|
|
821
|
+
* elements overlay, which are separate roots. Returns the first tree that contains
|
|
822
|
+
* the node, or `null` when neither does.
|
|
823
|
+
*/
|
|
824
|
+
function nodeAncestorPathInEither(layout, globalElements, target) {
|
|
825
|
+
return nodeAncestorPath(layout, target) ?? nodeAncestorPath(globalElements, target);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* A tab and its panel go TOGETHER on the canvas (CC4a — composite T18): deleting either deletes both ("Delete tab and
|
|
830
|
+
* its panel"), duplicating either duplicates both under one fresh key. The pairing is composite-core's (`itemKey`);
|
|
831
|
+
* this is the builder's tree surgery over it, shared by every path (toolbar, layer tree, the host's Cmd+D / Delete).
|
|
832
|
+
*/
|
|
833
|
+
/** The Tabs a tab or a panel belongs to — a MANUAL one (From content, the rows are not nodes) — or null (a Segmented
|
|
834
|
+
* control's option has no pair: it is duplicated and deleted alone, CC4b). */
|
|
835
|
+
function tabsRootForPart(layout, globals, node) {
|
|
836
|
+
if (!isExclusiveItemPart(node) || !isTabsPart(node))
|
|
837
|
+
return null;
|
|
838
|
+
const path = nodeAncestorPathInEither(layout, globals, node) ?? [];
|
|
839
|
+
const root = exclusiveRootOf(node, path.filter(n => n !== node));
|
|
840
|
+
return root && !isTabsFromContent(root) ? root : null;
|
|
841
|
+
}
|
|
842
|
+
/** The words the pair is named by in the confirm: its tab's. */
|
|
843
|
+
function tabPairLabel(root, node) {
|
|
844
|
+
const tree = root;
|
|
845
|
+
const tab = node.role === TabsRole.Panel ? tabTriggerOf(tree, node) : node;
|
|
846
|
+
return tab ? tabLabel(tab) : '';
|
|
847
|
+
}
|
|
848
|
+
/** Delete a tab and its panel. True when the pair was removed. */
|
|
849
|
+
function deleteTabPair(root, node) {
|
|
850
|
+
return removeTabPair(root, node);
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Duplicate a tab and its panel: copies (`clone` gives fresh ids) placed right after the originals, both under ONE new
|
|
854
|
+
* key — so the copy's tab shows the copy's panel. Returns the new tab, or null when `node` is no tab or panel of `root`.
|
|
855
|
+
*/
|
|
856
|
+
function duplicateTabPair(root, node, clone) {
|
|
857
|
+
const tree = root;
|
|
858
|
+
const tab = (node.role === TabsRole.Panel ? tabTriggerOf(tree, node) : node);
|
|
859
|
+
const panel = tab ? tabPanelOf(tree, tab) : undefined;
|
|
860
|
+
const list = tabsListOf(tree);
|
|
861
|
+
const panels = tabsPanelsOf(tree);
|
|
862
|
+
if (!tab || !panel || !list?.children || !panels?.children)
|
|
863
|
+
return null;
|
|
864
|
+
const key = nextTabKey(tree);
|
|
865
|
+
const tabCopy = clone(tab);
|
|
866
|
+
const panelCopy = clone(panel);
|
|
867
|
+
tabCopy.itemKey = panelCopy.itemKey = key;
|
|
868
|
+
list.children = insertAfter(list.children, tab, tabCopy);
|
|
869
|
+
panels.children = insertAfter(panels.children, panel, panelCopy);
|
|
870
|
+
return tabCopy;
|
|
871
|
+
}
|
|
872
|
+
function insertAfter(list, anchor, node) {
|
|
873
|
+
const out = [...list];
|
|
874
|
+
out.splice(out.indexOf(anchor) + 1, 0, node);
|
|
875
|
+
return out;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
var PanelEventTypes;
|
|
879
|
+
(function (PanelEventTypes) {
|
|
880
|
+
PanelEventTypes["ELEMENT_SELECT"] = "elementselect";
|
|
881
|
+
PanelEventTypes["ELEMENT_DESELECT"] = "elementdeselect";
|
|
882
|
+
PanelEventTypes["PANEL_SELECTED"] = "panelselect";
|
|
883
|
+
PanelEventTypes["PANEL_OPEN"] = "panelopen";
|
|
884
|
+
PanelEventTypes["PANEL_CLOSE"] = "panelclose";
|
|
885
|
+
PanelEventTypes["BINDING_CHANGED"] = "bindingchanged";
|
|
886
|
+
PanelEventTypes["NODE_RENDERED"] = "noderendered";
|
|
887
|
+
/** Open the Style panel in preset-edit mode for the given preset ID. */
|
|
888
|
+
PanelEventTypes["PRESET_EDIT"] = "presetedit";
|
|
889
|
+
/** Fired after a preset is created, updated, or deleted so panels refresh their list. */
|
|
890
|
+
PanelEventTypes["PRESETS_CHANGED"] = "presetschanged";
|
|
891
|
+
/** Fired when a linked symbol instance's styles change — propagates to all instances. */
|
|
892
|
+
PanelEventTypes["SYMBOLS_CHANGED"] = "symbolschanged";
|
|
893
|
+
/** Fired on every structural or style mutation — used by BuilderHistoryService to snapshot. */
|
|
894
|
+
PanelEventTypes["LAYOUT_CHANGED"] = "layoutchanged";
|
|
895
|
+
/** Open the Interactions panel in preset-edit mode for the given preset ID. */
|
|
896
|
+
PanelEventTypes["INTERACTION_PRESET_EDIT"] = "interactionpresetedit";
|
|
897
|
+
/** Fired after an interaction preset is created, updated, or deleted so panels refresh. */
|
|
898
|
+
PanelEventTypes["INTERACTION_PRESETS_CHANGED"] = "interactionpresetschanged";
|
|
899
|
+
/** Open the Interactions panel and EXPAND a specific interaction for editing (data = interaction
|
|
900
|
+
* id). Emitted when the overlay interaction badge is clicked so that badge → "edit THIS
|
|
901
|
+
* interaction", without firing the element's (live) animation. */
|
|
902
|
+
PanelEventTypes["INTERACTION_EDIT"] = "interactionedit";
|
|
903
|
+
/** A node's `interactions` were added/removed/edited. Carries `{ node }`.
|
|
904
|
+
* Repeater/Slider listen so their PREVIEW CLONES are re-cloned with the new interactions —
|
|
905
|
+
* clones are attached by ELEMENT at render time, so without this a new interaction only starts
|
|
906
|
+
* working after a save + reload. */
|
|
907
|
+
PanelEventTypes["INTERACTION_CHANGED"] = "interactionchanged";
|
|
908
|
+
/** CLICK-TO-INSERT (B2): a components-panel entry was clicked. Carries the same payload a drag
|
|
909
|
+
* would (`string` component type, or a block / symbol object). The builder resolves WHERE from
|
|
910
|
+
* the current selection — see `resolveInsertTarget`. */
|
|
911
|
+
PanelEventTypes["INSERT_COMPONENT"] = "insertcomponent";
|
|
912
|
+
/** Insert prebuilt node(s) as GLOBAL elements (data = ComponentNode[]) — the consent preset's click and
|
|
913
|
+
* the site header's click or drop (CC1.1). The HOST handles it (it owns the globals, their undo step and
|
|
914
|
+
* the one-header rule); the builder only routes a dropped global payload here (`globalInsert$`). */
|
|
915
|
+
PanelEventTypes["INSERT_GLOBAL"] = "insertglobal";
|
|
916
|
+
/** A panel makes `data.node` a GLOBAL element at `data.position` (default before) — the same move as the
|
|
917
|
+
* toolbar's Make Global (the Style panel's Global switch, Bindings ▸ Phone menu ▸ "Make it the site header"). */
|
|
918
|
+
PanelEventTypes["MAKE_GLOBAL"] = "make-global";
|
|
919
|
+
/** A panel returns the global `data.node` to the page — the toolbar's Remove Global. */
|
|
920
|
+
PanelEventTypes["REMOVE_GLOBAL"] = "remove-global";
|
|
921
|
+
/** A panel moved the global `data.node` to `data.position` (before / after the page content). */
|
|
922
|
+
PanelEventTypes["GLOBAL_POSITION"] = "update-global-position";
|
|
923
|
+
/** A click on a compound component's PART (Field label/control/help/…) — the style panel
|
|
924
|
+
* activates that part (data = { part }). Doron 2026-10-10: "both shall be selectable". */
|
|
925
|
+
PanelEventTypes["PART_SELECT"] = "partselect";
|
|
926
|
+
/** A panel wants to select a NODE it already has a reference to (e.g. the Form's nested field
|
|
927
|
+
* list). Carries `{ node }`. `OverlayService.setSelect()` alone only updates the overlay's own
|
|
928
|
+
* visual state — `selectedItem`/`selectedPath` (breadcrumb) and every other panel's idea of
|
|
929
|
+
* "what's selected" stay stale unless the SAME `_emitElementSelect` a real canvas click goes
|
|
930
|
+
* through also runs. This event is that path for code outside `BuilderComponent` itself,
|
|
931
|
+
* mirroring what `onTreeNodeSelect`/`onCrumbSelect` already do internally. */
|
|
932
|
+
PanelEventTypes["SELECT_NODE"] = "selectnode";
|
|
933
|
+
/** A panel rewrote a node's SUBTREE in place — its children (a Card's Parts), its layout styles (Card ▸
|
|
934
|
+
* Layout), a part's element style or scheme. Carries `{ node }`. The builder re-renders the subtree,
|
|
935
|
+
* re-registers every node's rules and design classes, refreshes a Repeater holding it, and records ONE
|
|
936
|
+
* undo step (LAYOUT_CHANGED). */
|
|
937
|
+
PanelEventTypes["NODE_STRUCTURE_CHANGED"] = "nodestructurechanged";
|
|
938
|
+
/** A Repeater's rows changed on the canvas (fetched, re-fetched, its source cleared). Carries `{ nodeId,
|
|
939
|
+
* count }`. Not a page edit — never an undo step: the layer tree redraws its "N more rows" line. */
|
|
940
|
+
PanelEventTypes["REPEATER_ROWS_CHANGED"] = "repeaterrowschanged";
|
|
941
|
+
})(PanelEventTypes || (PanelEventTypes = {}));
|
|
942
|
+
class PanelEventService {
|
|
943
|
+
constructor() {
|
|
944
|
+
this.panelEventSubject = new Subject();
|
|
945
|
+
}
|
|
946
|
+
subscribe(next) {
|
|
947
|
+
return this.panelEventSubject.asObservable().subscribe(next);
|
|
948
|
+
}
|
|
949
|
+
emit(type, data) {
|
|
950
|
+
this.panelEventSubject.next({ type, data });
|
|
951
|
+
}
|
|
952
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: PanelEventService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
953
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: PanelEventService }); }
|
|
954
|
+
}
|
|
955
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: PanelEventService, decorators: [{
|
|
956
|
+
type: Injectable
|
|
957
|
+
}] });
|
|
958
|
+
|
|
959
|
+
/**
|
|
960
|
+
* Deep-clones a ComponentNode tree and overrides trait defaults
|
|
961
|
+
* according to each node's bindings resolved against `item`.
|
|
962
|
+
*
|
|
963
|
+
* Each clone gets a new `id` (for Angular tracking / overlay registration)
|
|
964
|
+
* but shares the original node's CSS class by setting `styleId = originalId`.
|
|
965
|
+
* This means all repeated preview rows point to the same CSS rule block as the
|
|
966
|
+
* template node — the style engine only needs one rule per logical slot.
|
|
967
|
+
*/
|
|
968
|
+
function cloneNodesForItem(nodes, item) {
|
|
969
|
+
return nodes.map(node => {
|
|
970
|
+
// Capture the CSS-owning id before clone
|
|
971
|
+
const cssId = node.styleId ?? node.id;
|
|
972
|
+
const clone = JSON.parse(JSON.stringify(node));
|
|
973
|
+
clone.id = Math.random().toString(36).substr(2, 9);
|
|
974
|
+
// Point this clone and all its descendants at the original CSS rule.
|
|
975
|
+
clone.styleId = cssId;
|
|
976
|
+
// Mark as a preview clone: display-only, not selectable/editable on the canvas.
|
|
977
|
+
// (Distinct from styleId, which duplication/paste also uses.) The render directive
|
|
978
|
+
// skips overlay registration for nodes carrying this flag.
|
|
979
|
+
clone.__previewClone = true;
|
|
980
|
+
if (clone.bindings?.length && item) {
|
|
981
|
+
for (const binding of clone.bindings) {
|
|
982
|
+
// Path lookup AND pipes (UI "Filters"). Resolving the path alone left a `date` or
|
|
983
|
+
// `uppercase` filter unapplied on every preview row, so the canvas showed the raw
|
|
984
|
+
// value while the SSR engine showed the formatted one — see binding-filters-plan.md.
|
|
985
|
+
const value = resolveBoundValue(item, binding);
|
|
986
|
+
if (value !== undefined) {
|
|
987
|
+
for (const trait of clone.traits || []) {
|
|
988
|
+
if (trait.name === binding.prop) {
|
|
989
|
+
trait.default = value;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
if (clone.children?.length) {
|
|
996
|
+
clone.children = cloneNodesForItem(clone.children, item);
|
|
997
|
+
}
|
|
998
|
+
return clone;
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/** The content type a data source reads (`faqs`), or '' for a source that reads none (external, static). */
|
|
1003
|
+
function sourceContentType(ds) {
|
|
1004
|
+
return ds?.request?.cms?.contentType || ds?.request?.shop?.contentType || '';
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* What the builder's chrome calls the list a source reads — the CONTENT TYPE's label ("From FAQ", board 3), as the
|
|
1008
|
+
* host knows it (`IOX_CONTENT_TYPE_LABELS`), else the source's alias. ONE answer for the layer tree's
|
|
1009
|
+
* "From FAQ · 3 rows", the Repeater's empty-source hint and the Items section's source options.
|
|
1010
|
+
*/
|
|
1011
|
+
function sourceTypeLabel(alias, ds, resolve) {
|
|
1012
|
+
const type = sourceContentType(ds);
|
|
1013
|
+
return (type && resolve(type)) || alias;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
const freshId = () => Math.random().toString(36).substr(2, 9);
|
|
1017
|
+
/**
|
|
1018
|
+
* Assign fresh ids to a cloned ComponentNode subtree AND remap intra-subtree references to the
|
|
1019
|
+
* new ids. Used by every author-time clone path (duplicate, paste, block/external drop) so a copied
|
|
1020
|
+
* structure stays self-consistent.
|
|
1021
|
+
*
|
|
1022
|
+
* Absolute node-id references that break under duplication if left unmapped:
|
|
1023
|
+
* - `stateTriggers[state]` — the trigger-hover target (see style-engine.md → Targeted states)
|
|
1024
|
+
* - interaction `action.target` / `reverseActions.target`
|
|
1025
|
+
* A reference pointing OUTSIDE the cloned subtree (id not in the map) is left untouched — the copy
|
|
1026
|
+
* intentionally still points at the external node.
|
|
1027
|
+
*
|
|
1028
|
+
* TRAIT values are NOT ids and are copied verbatim — with one exception. A trait that carries an
|
|
1029
|
+
* IDENTITY rather than a setting must be reissued, or the copy silently shares the original's
|
|
1030
|
+
* identity. `formId` is the first: two forms with the same id share one submissions stream, and the
|
|
1031
|
+
* server resolves a submission against whichever page it reaches first. See REGENERATED_TRAITS.
|
|
1032
|
+
*
|
|
1033
|
+
* `styleId` strategy:
|
|
1034
|
+
* - `'clear'` (default) — drop styleId so the clone OWNS its own CSS rule (independent copy).
|
|
1035
|
+
* - `'regenerate'` — keep a styleId but give it a fresh value (legacy builder.component path).
|
|
1036
|
+
*/
|
|
1037
|
+
/**
|
|
1038
|
+
* Trait names whose value is an IDENTITY, not a setting, and must therefore be reissued on every
|
|
1039
|
+
* clone. Add a name here the moment a trait starts identifying something rather than describing it.
|
|
1040
|
+
*
|
|
1041
|
+
* `formId` — a form's submissions stream. Duplicating a page, pasting a form or dropping a saved
|
|
1042
|
+
* block twice used to produce two forms claiming the same stream.
|
|
1043
|
+
*/
|
|
1044
|
+
const REGENERATED_TRAITS = new Set(['formId']);
|
|
1045
|
+
/** Reissue identity-carrying traits on a node, preserving the original's prefix where it had one. */
|
|
1046
|
+
function reissueIdentityTraits(node) {
|
|
1047
|
+
for (const trait of (node.traits ?? [])) {
|
|
1048
|
+
if (!trait?.name || !REGENERATED_TRAITS.has(trait.name))
|
|
1049
|
+
continue;
|
|
1050
|
+
if (trait.default === undefined || trait.default === '')
|
|
1051
|
+
continue;
|
|
1052
|
+
// Keep the human-readable prefix a preset gave it (`contact-a1b2c3` → `contact-<new>`), so a
|
|
1053
|
+
// duplicated contact form is still recognisably a contact form in the inbox.
|
|
1054
|
+
const prefix = String(trait.default).split('-')[0];
|
|
1055
|
+
trait.default = `${prefix}-${freshId()}`;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* A NEW node is born with its identity: an empty identity trait (a palette Form's `formId`) is issued,
|
|
1060
|
+
* one already set (a preset's `contact-…`) is kept. A Form inserted from the palette used to keep
|
|
1061
|
+
* `formId: ''` — its submissions posted to `forms//submit` and nothing was ever stored.
|
|
1062
|
+
*/
|
|
1063
|
+
function issueIdentityTraits(root) {
|
|
1064
|
+
for (const trait of (root.traits ?? [])) {
|
|
1065
|
+
if (!trait?.name || !REGENERATED_TRAITS.has(trait.name))
|
|
1066
|
+
continue;
|
|
1067
|
+
if (trait.default === undefined || trait.default === '')
|
|
1068
|
+
trait.default = `${IDENTITY_PREFIX[trait.name] ?? trait.name}-${freshId()}`;
|
|
1069
|
+
}
|
|
1070
|
+
for (const child of root.children ?? [])
|
|
1071
|
+
if (child)
|
|
1072
|
+
issueIdentityTraits(child);
|
|
1073
|
+
}
|
|
1074
|
+
/** The readable prefix a freshly issued identity carries (`form-a1b2c3`). */
|
|
1075
|
+
const IDENTITY_PREFIX = { formId: 'form' };
|
|
1076
|
+
function regenerateNodeIds(root, opts = {}) {
|
|
1077
|
+
const idMap = new Map();
|
|
1078
|
+
// A composite item's identity (CC2 `itemKey`) is reissued per copy, consistently inside it: a duplicated
|
|
1079
|
+
// accordion item must not share its original's ARIA ids (composite plan §3).
|
|
1080
|
+
const keyMap = new Map();
|
|
1081
|
+
const assign = (node) => {
|
|
1082
|
+
const oldId = node.id;
|
|
1083
|
+
node.id = freshId();
|
|
1084
|
+
reissueIdentityTraits(node);
|
|
1085
|
+
if (node.itemKey) {
|
|
1086
|
+
if (!keyMap.has(node.itemKey))
|
|
1087
|
+
keyMap.set(node.itemKey, `k-${freshId()}`);
|
|
1088
|
+
node.itemKey = keyMap.get(node.itemKey);
|
|
1089
|
+
}
|
|
1090
|
+
if (opts.styleId === 'regenerate') {
|
|
1091
|
+
if (node.styleId)
|
|
1092
|
+
node.styleId = freshId();
|
|
1093
|
+
}
|
|
1094
|
+
else {
|
|
1095
|
+
node.styleId = undefined;
|
|
1096
|
+
}
|
|
1097
|
+
if (oldId)
|
|
1098
|
+
idMap.set(oldId, node.id);
|
|
1099
|
+
(node.children || []).forEach(assign);
|
|
1100
|
+
};
|
|
1101
|
+
assign(root);
|
|
1102
|
+
remapNodeReferences(root, idMap);
|
|
1103
|
+
}
|
|
1104
|
+
/** Remap absolute node-id references inside a subtree to their new ids (per `idMap`). Ids not in
|
|
1105
|
+
* the map (references to nodes outside the cloned subtree) are preserved. */
|
|
1106
|
+
function remapNodeReferences(node, idMap) {
|
|
1107
|
+
const triggers = node.stateTriggers;
|
|
1108
|
+
if (triggers) {
|
|
1109
|
+
for (const state of Object.keys(triggers)) {
|
|
1110
|
+
const mapped = idMap.get(triggers[state]);
|
|
1111
|
+
if (mapped)
|
|
1112
|
+
triggers[state] = mapped;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
for (const interaction of node.interactions || []) {
|
|
1116
|
+
const actions = [...(interaction.actions || []), ...(interaction.reverseActions || [])];
|
|
1117
|
+
for (const action of actions) {
|
|
1118
|
+
if (action?.target && action.target !== 'self') {
|
|
1119
|
+
const mapped = idMap.get(action.target);
|
|
1120
|
+
if (mapped)
|
|
1121
|
+
action.target = mapped;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
(node.children || []).forEach(child => remapNodeReferences(child, idMap));
|
|
658
1126
|
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* The text on the drop indicator's chip — "into Card grid · position 2 of 3" (B2).
|
|
1130
|
+
*
|
|
1131
|
+
* The most common drag failure is landing a node one level off and not noticing; naming the PARENT
|
|
1132
|
+
* and the slot makes the target unambiguous before the pointer is released.
|
|
1133
|
+
*
|
|
1134
|
+
* @param containerName The target container's label or type; blank / unknown → "the page".
|
|
1135
|
+
* @param index Insert index within the container (0-based).
|
|
1136
|
+
* @param childCount How many children it has now — the node being dropped makes `childCount + 1`.
|
|
1137
|
+
*/
|
|
1138
|
+
function dropTargetLabel(containerName, index, childCount) {
|
|
1139
|
+
const name = (containerName ?? '').trim() || 'the page';
|
|
1140
|
+
const total = Math.max(0, childCount) + 1;
|
|
1141
|
+
const position = Math.min(Math.max(index, 0), total - 1) + 1;
|
|
1142
|
+
return `into ${name} · position ${position} of ${total}`;
|
|
663
1143
|
}
|
|
1144
|
+
|
|
664
1145
|
/**
|
|
665
|
-
*
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
* never asks.
|
|
1146
|
+
* Per-frame exponential smoothing step — moves `current` a fraction of the way
|
|
1147
|
+
* toward `target`. Used to ease the drag ghost toward the pointer instead of
|
|
1148
|
+
* snapping to it 1:1 on every dragmove (see DragEngineService._ghostFollowStep).
|
|
669
1149
|
*/
|
|
670
|
-
function
|
|
671
|
-
|
|
672
|
-
return null;
|
|
673
|
-
if (node.type && FORM_STATE_BLOCK_TYPES.has(node.type))
|
|
674
|
-
return InsertRefusal.StateBlockAlone;
|
|
675
|
-
if (ancestors.some(a => a?.type === FORM_TYPE) && containsType(node, FORM_TYPE))
|
|
676
|
-
return InsertRefusal.NestedForm;
|
|
677
|
-
if (formContentInHost(node, ancestors.some(isInteractiveHost)))
|
|
678
|
-
return InsertRefusal.FormInInteractive;
|
|
679
|
-
const structural = structureRefusal(node, StructureEdit.Move, ancestors);
|
|
680
|
-
if (structural === StructureRefusal.NotAccepted)
|
|
681
|
-
return InsertRefusal.TabsAccepts;
|
|
682
|
-
if (structural)
|
|
683
|
-
return lockedRefusal(node, StructureEdit.Move);
|
|
684
|
-
return null;
|
|
1150
|
+
function easeToward(current, target, smoothing) {
|
|
1151
|
+
return current + (target - current) * smoothing;
|
|
685
1152
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
*
|
|
689
|
-
*
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
return null;
|
|
1153
|
+
|
|
1154
|
+
/**
|
|
1155
|
+
* Decides which element a drag gesture should actually operate on.
|
|
1156
|
+
*
|
|
1157
|
+
* A drag must never silently descend into a nested child just because the pointer happened to
|
|
1158
|
+
* be over it while its ANCESTOR is the currently selected node, and the overlay's drag-handle
|
|
1159
|
+
* (which carries no node of its own — it lives outside the canvas DOM entirely) must always
|
|
1160
|
+
* redirect to whatever is currently selected. In both cases the redirect target is the
|
|
1161
|
+
* currently-selected element; otherwise the gesture proceeds on its natural origin unchanged.
|
|
1162
|
+
*/
|
|
1163
|
+
function resolveDragTarget(el, selectedElement, isDragHandle) {
|
|
1164
|
+
if (!selectedElement || el === selectedElement)
|
|
1165
|
+
return el;
|
|
1166
|
+
const isDescendantOfSelected = selectedElement.contains(el);
|
|
1167
|
+
return (isDragHandle || isDescendantOfSelected) ? selectedElement : el;
|
|
702
1168
|
}
|
|
703
1169
|
|
|
704
1170
|
/**
|
|
@@ -1281,6 +1747,9 @@ class TypographyGroupStyleConfig extends GroupStyleConfig {
|
|
|
1281
1747
|
], TextAlign.Start),
|
|
1282
1748
|
new TraitConfig('fontStyle', '', TraitInputType.SelectButton, { items: [{ value: 'italic', icon: 'ph-thin ph-text-italic', label: 'Italic' }], allowEmpty: true, noneValue: 'normal' }, 'normal', true),
|
|
1283
1749
|
new TraitConfig('textDecoration', '', TraitInputType.SelectButton, { items: [{ value: 'underline', icon: 'ph-thin ph-text-underline', label: 'Underline' }], allowEmpty: true, noneValue: 'none' }, 'none', true),
|
|
1750
|
+
// CC4b: tabular figures (the Price style's) — every digit one width, so a price that changes never shifts.
|
|
1751
|
+
// `normal` is the CSS initial and a compiler no-op, so it never leaks onto nodes.
|
|
1752
|
+
new TraitConfig('fontVariantNumeric', '', TraitInputType.SelectButton, { items: [{ value: 'tabular-nums', icon: 'ph-thin ph-text-columns', label: 'Tabular figures' }], allowEmpty: true, noneValue: 'normal' }, 'normal', true),
|
|
1284
1753
|
new TraitConfig('_sep_stroke', 'Stroke', TraitInputType.SectionLabel),
|
|
1285
1754
|
new TraitConfig('WebkitTextStrokeWidth', 'Width', TraitInputType.Scrub, { min: 0, max: 10, step: 0.5, units: ['px'] }, '0px', true),
|
|
1286
1755
|
new TraitConfig('WebkitTextStrokeColor', 'Color', TraitInputType.Color, { allowGradient: false, allowTransparent: true, formats: ['hex', 'rgb', 'hsl'] }, 'transparent', true),
|
|
@@ -2020,8 +2489,14 @@ class StyleRegistryService {
|
|
|
2020
2489
|
this._flushPending = false;
|
|
2021
2490
|
if (!this.styleEl)
|
|
2022
2491
|
return;
|
|
2023
|
-
// The token base first, so variables are available to all subsequent rules.
|
|
2024
|
-
|
|
2492
|
+
// The token base first, so variables are available to all subsequent rules. A node's FOCUS rule comes after
|
|
2493
|
+
// its other state rules (CC4b): `on` and `focus` tie on specificity, so a focused option that is on keeps its
|
|
2494
|
+
// focus ring — the order the engine emits too (shared `isLastEmittedStateKey`).
|
|
2495
|
+
const entries = Array.from(this.rules.entries());
|
|
2496
|
+
const rest = [
|
|
2497
|
+
...entries.filter(([key]) => !isLastEmittedStateKey(key)).map(([, css]) => css),
|
|
2498
|
+
...entries.filter(([key]) => isLastEmittedStateKey(key)).map(([, css]) => css),
|
|
2499
|
+
];
|
|
2025
2500
|
// Breakpoint rules LAST, widest → narrowest, each as a canvas container query.
|
|
2026
2501
|
const breakpoints = BREAKPOINTS
|
|
2027
2502
|
.map(({ id }) => this.responsiveRules.get(id))
|
|
@@ -4359,7 +4834,7 @@ var CanvasShowState;
|
|
|
4359
4834
|
*/
|
|
4360
4835
|
class CanvasDesignStateService {
|
|
4361
4836
|
constructor() {
|
|
4362
|
-
this._state$ = new BehaviorSubject({ revealed: new Set(), selectedTriggers: new Set(), pinned: new Set(), shown: new Map(), items: new Map() });
|
|
4837
|
+
this._state$ = new BehaviorSubject({ revealed: new Set(), selectedTriggers: new Set(), pinned: new Set(), triggeredBySelection: new Set(), shown: new Map(), items: new Map() });
|
|
4363
4838
|
this.state$ = this._state$.asObservable();
|
|
4364
4839
|
this._pagePath$ = new BehaviorSubject(null);
|
|
4365
4840
|
/** The app path of the page on the canvas (`/work`), or null when unknown. */
|
|
@@ -4384,11 +4859,12 @@ class CanvasDesignStateService {
|
|
|
4384
4859
|
const ids = (reveal) => path.filter(n => selectionRevealOf(n) === reveal).map(n => n.id).filter((id) => !!id);
|
|
4385
4860
|
const revealed = new Set(ids(SelectionReveal.WhileSelected));
|
|
4386
4861
|
const selectedTriggers = new Set(path.filter(n => isExclusiveTrigger(n)).map(n => n.id).filter((id) => !!id));
|
|
4387
|
-
const
|
|
4862
|
+
const triggeredBySelection = new Set(path.map(displayTriggerOf).filter((id) => !!id));
|
|
4863
|
+
const { revealed: was, pinned: held, selectedTriggers: wasTriggers, triggeredBySelection: wasTriggered } = this._state$.value;
|
|
4388
4864
|
const pinned = new Set([...held, ...ids(SelectionReveal.UntilClosed)]);
|
|
4389
|
-
if (sameSet(revealed, was) && sameSet(pinned, held) && sameSet(selectedTriggers, wasTriggers))
|
|
4865
|
+
if (sameSet(revealed, was) && sameSet(pinned, held) && sameSet(selectedTriggers, wasTriggers) && sameSet(triggeredBySelection, wasTriggered))
|
|
4390
4866
|
return;
|
|
4391
|
-
this._state$.next({ ...this._state$.value, revealed, pinned, selectedTriggers });
|
|
4867
|
+
this._state$.next({ ...this._state$.value, revealed, pinned, selectedTriggers, triggeredBySelection });
|
|
4392
4868
|
}
|
|
4393
4869
|
/** Closed on the canvas (its X, its backdrop, the eye): no longer pinned or switched open. */
|
|
4394
4870
|
dismiss(nodeId) {
|
|
@@ -4438,13 +4914,17 @@ class CanvasDesignStateService {
|
|
|
4438
4914
|
const { revealed, pinned, shown } = this._state$.value;
|
|
4439
4915
|
return revealed.has(nodeId) || pinned.has(nodeId) || shown.get(nodeId) === CanvasShowState.Open;
|
|
4440
4916
|
}
|
|
4917
|
+
/** True when the canvas shows an item because the selection is styled "when it is on" (the switcher's hint). */
|
|
4918
|
+
isTriggeredBySelection(triggerId) {
|
|
4919
|
+
return !!triggerId && this._state$.value.triggeredBySelection.has(triggerId);
|
|
4920
|
+
}
|
|
4441
4921
|
/** True when the part is shown only because the selection is inside it (the switcher's hint line). */
|
|
4442
4922
|
isRevealedBySelection(nodeId) {
|
|
4443
4923
|
return !!nodeId && this._state$.value.revealed.has(nodeId);
|
|
4444
4924
|
}
|
|
4445
4925
|
/** A new page on the canvas: nothing is revealed or switched open. */
|
|
4446
4926
|
reset() {
|
|
4447
|
-
this._state$.next({ revealed: new Set(), selectedTriggers: new Set(), pinned: new Set(), shown: new Map(), items: new Map() });
|
|
4927
|
+
this._state$.next({ revealed: new Set(), selectedTriggers: new Set(), pinned: new Set(), triggeredBySelection: new Set(), shown: new Map(), items: new Map() });
|
|
4448
4928
|
}
|
|
4449
4929
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: CanvasDesignStateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
4450
4930
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: CanvasDesignStateService }); }
|
|
@@ -4497,10 +4977,13 @@ class CompositeCanvasService {
|
|
|
4497
4977
|
const cssId = node.styleId ?? node.id;
|
|
4498
4978
|
const rootId = () => compositeScopeId(cssId, compositeRowKeys(element));
|
|
4499
4979
|
if (isExclusiveRoot(node)) {
|
|
4980
|
+
const tree = node;
|
|
4500
4981
|
const runtime = this.zone.runOutsideAngular(() => new ExclusiveRuntime(element, {
|
|
4982
|
+
kind: exclusiveKindOf(node) ?? undefined,
|
|
4501
4983
|
rootId,
|
|
4502
|
-
behaviour:
|
|
4503
|
-
label: () =>
|
|
4984
|
+
behaviour: exclusiveBehaviourOf(tree),
|
|
4985
|
+
label: () => exclusiveLabelOf(tree),
|
|
4986
|
+
defaultKey: () => exclusiveDefaultOf(tree),
|
|
4504
4987
|
observe: true,
|
|
4505
4988
|
forced: () => this.forcedTab(node, element),
|
|
4506
4989
|
// The switcher follows the TEMPLATE's clicks; a preview row's copy switches on its own.
|
|
@@ -4542,13 +5025,21 @@ class CompositeCanvasService {
|
|
|
4542
5025
|
const entry = node.id ? this.attached.get(node.id) : undefined;
|
|
4543
5026
|
entry?.runtime.setBehaviour(accordionBehaviour(node));
|
|
4544
5027
|
const tabs = node.id ? this.exclusive.get(node.id) : undefined;
|
|
4545
|
-
tabs?.runtime.setBehaviour(
|
|
5028
|
+
tabs?.runtime.setBehaviour(exclusiveBehaviourOf(node));
|
|
5029
|
+
}
|
|
5030
|
+
/** A Segmented control's On by default changed (CC4b): the canvas shows the new default, like a click on it. */
|
|
5031
|
+
showDefault(node) {
|
|
5032
|
+
const runtime = node.id ? this.exclusive.get(node.id)?.runtime : undefined;
|
|
5033
|
+
const key = exclusiveDefaultOf(node);
|
|
5034
|
+
if (runtime && key)
|
|
5035
|
+
runtime.activate(key);
|
|
4546
5036
|
}
|
|
4547
5037
|
/** The runtime of an attached Accordion / Disclosure root (the e2e harness and the panels read its state). */
|
|
4548
5038
|
runtimeOf(nodeId) {
|
|
4549
5039
|
return (nodeId && this.attached.get(nodeId)?.runtime) || null;
|
|
4550
5040
|
}
|
|
4551
|
-
/** The runtime of an attached
|
|
5041
|
+
/** The runtime of an attached exclusive root — a Tabs (CC4a) or a Segmented control (CC4b): "Showing on canvas" reads
|
|
5042
|
+
* its items and the one on. */
|
|
4552
5043
|
tabsRuntimeOf(nodeId) {
|
|
4553
5044
|
return (nodeId && this.exclusive.get(nodeId)?.runtime) || null;
|
|
4554
5045
|
}
|
|
@@ -4605,13 +5096,14 @@ class CompositeCanvasService {
|
|
|
4605
5096
|
}
|
|
4606
5097
|
return forced;
|
|
4607
5098
|
}
|
|
4608
|
-
/** The
|
|
5099
|
+
/** The item forced on: the tab whose panel the selection sits in (the template's, in a preview row's copy) — or
|
|
5100
|
+
* the item the selection is styled "when it is on" by (CC4b: a price shown when Yearly is on turns Yearly on). */
|
|
4609
5101
|
forcedTab(node, root) {
|
|
4610
5102
|
const state = this.designState;
|
|
4611
5103
|
const runtime = node.id ? this.exclusive.get(node.id)?.runtime : undefined;
|
|
4612
5104
|
if (!state || !runtime)
|
|
4613
5105
|
return null;
|
|
4614
|
-
for (const id of state.snapshot.revealed) {
|
|
5106
|
+
for (const id of [...state.snapshot.revealed, ...state.snapshot.triggeredBySelection]) {
|
|
4615
5107
|
const key = runtime.keyAt(root.querySelector(`.iox-node-${CSS.escape(id)}`));
|
|
4616
5108
|
if (key)
|
|
4617
5109
|
return key;
|
|
@@ -5711,10 +6203,12 @@ var CompositePresetId;
|
|
|
5711
6203
|
CompositePresetId["Accordion"] = "accordion";
|
|
5712
6204
|
CompositePresetId["Disclosure"] = "disclosure";
|
|
5713
6205
|
CompositePresetId["Tabs"] = "tabs";
|
|
6206
|
+
CompositePresetId["Segmented"] = "segmented";
|
|
5714
6207
|
CompositePresetId["Lightbox"] = "lightbox";
|
|
5715
6208
|
})(CompositePresetId || (CompositePresetId = {}));
|
|
5716
6209
|
/** Where a composite sits in the Components panel (composite plan §6d): content blocks, or the interactive
|
|
5717
|
-
* composites (the Accordion — CC2, the Disclosure — CC2.1, Tabs — CC4a
|
|
6210
|
+
* composites (the Accordion — CC2, the Disclosure — CC2.1, Tabs — CC4a, the Segmented control — CC4b; the floating
|
|
6211
|
+
* family later). */
|
|
5718
6212
|
var CompositePaletteGroup;
|
|
5719
6213
|
(function (CompositePaletteGroup) {
|
|
5720
6214
|
CompositePaletteGroup["ContentBlocks"] = "content-blocks";
|
|
@@ -5751,6 +6245,11 @@ const COMPOSITE_PRESETS = Object.freeze([
|
|
|
5751
6245
|
description: 'Tabs that switch what shows below them — three tabs, the first on. Every tab and panel is its own element.',
|
|
5752
6246
|
build: create => buildTabsRecipe(create),
|
|
5753
6247
|
},
|
|
6248
|
+
{
|
|
6249
|
+
id: CompositePresetId.Segmented, group: CompositePaletteGroup.Interactive, name: 'Segmented', icon: 'ph-thin ph-pill',
|
|
6250
|
+
description: 'One of a few, always one on — Monthly · Yearly. Style anything "when Yearly is on" to swap what shows.',
|
|
6251
|
+
build: create => buildSegmentedRecipe(create),
|
|
6252
|
+
},
|
|
5754
6253
|
{
|
|
5755
6254
|
id: CompositePresetId.Lightbox, group: CompositePaletteGroup.Interactive, name: 'Lightbox', icon: 'ph-thin ph-frame-corners',
|
|
5756
6255
|
description: 'A full-screen dialog with a slider — bind its slider to your gallery\'s source, and each thumbnail opens it at its own image.',
|
|
@@ -5894,339 +6393,65 @@ function overlayGlobals(globals) {
|
|
|
5894
6393
|
else if (globalCanvasLayer(node) === GlobalCanvasLayer.Overlay)
|
|
5895
6394
|
pinnedAfter.push(node);
|
|
5896
6395
|
}
|
|
5897
|
-
return { before, pinnedAfter };
|
|
5898
|
-
}
|
|
5899
|
-
|
|
5900
|
-
/** Counts for ONE node (the selection). Breakpoints with nothing overridden are omitted. */
|
|
5901
|
-
function nodeBreakpointCounts(node) {
|
|
5902
|
-
const counts = {};
|
|
5903
|
-
if (!node?.responsive)
|
|
5904
|
-
return counts;
|
|
5905
|
-
for (const { id } of BREAKPOINTS) {
|
|
5906
|
-
if (id === Breakpoint.Base)
|
|
5907
|
-
continue;
|
|
5908
|
-
const n = countOverrides(node.responsive, id);
|
|
5909
|
-
if (n)
|
|
5910
|
-
counts[id] = n;
|
|
5911
|
-
}
|
|
5912
|
-
return counts;
|
|
5913
|
-
}
|
|
5914
|
-
/** Counts summed over whole trees (the page, when nothing is selected). */
|
|
5915
|
-
function treeBreakpointCounts(...roots) {
|
|
5916
|
-
const counts = {};
|
|
5917
|
-
const visit = (nodes) => {
|
|
5918
|
-
for (const node of nodes ?? []) {
|
|
5919
|
-
for (const [bp, n] of Object.entries(nodeBreakpointCounts(node))) {
|
|
5920
|
-
counts[bp] = (counts[bp] ?? 0) + n;
|
|
5921
|
-
}
|
|
5922
|
-
visit(node.children);
|
|
5923
|
-
}
|
|
5924
|
-
};
|
|
5925
|
-
roots.forEach(visit);
|
|
5926
|
-
return counts;
|
|
5927
|
-
}
|
|
5928
|
-
|
|
5929
|
-
/**
|
|
5930
|
-
* Resolve where "click to insert" puts a new element, from the current selection — the rule the
|
|
5931
|
-
* components panel and the canvas "+" share (builder-usability-plan.md, B2):
|
|
5932
|
-
*
|
|
5933
|
-
* nothing selected → appended to the page root
|
|
5934
|
-
* a CONTAINER selected → appended INSIDE it (its last child)
|
|
5935
|
-
* a LEAF selected → inserted AFTER it, as its next sibling
|
|
5936
|
-
*
|
|
5937
|
-
* `isContainer` decides what can hold children — the builder passes "has a registered drop zone",
|
|
5938
|
-
* the same test a drag uses, so a click can never insert somewhere a drop couldn't.
|
|
5939
|
-
* A selection not found in `roots` (stale) falls back to appending to the root.
|
|
5940
|
-
*/
|
|
5941
|
-
function resolveInsertTarget(roots, selected, isContainer) {
|
|
5942
|
-
const append = (children, parent) => ({ children, index: children.length, parent });
|
|
5943
|
-
if (!selected)
|
|
5944
|
-
return append(roots, null);
|
|
5945
|
-
const path = nodeAncestorPath(roots, selected);
|
|
5946
|
-
if (!path)
|
|
5947
|
-
return append(roots, null);
|
|
5948
|
-
if (isContainer(selected)) {
|
|
5949
|
-
if (!selected.children)
|
|
5950
|
-
selected.children = [];
|
|
5951
|
-
return append(selected.children, selected);
|
|
5952
|
-
}
|
|
5953
|
-
const parent = path.length >= 2 ? path[path.length - 2] : null;
|
|
5954
|
-
const siblings = parent ? (parent.children ?? (parent.children = [])) : roots;
|
|
5955
|
-
return { children: siblings, index: siblings.indexOf(selected) + 1, parent };
|
|
5956
|
-
}
|
|
5957
|
-
|
|
5958
|
-
/**
|
|
5959
|
-
* STRUCTURE OPS (A4) — the one-click layout changes people actually make at a narrow width, as pure
|
|
5960
|
-
* "what to write" functions. Each returns a flat style map the caller stores in the node's
|
|
5961
|
-
* `responsive[bp]` layer (or in the base styles at Base) through the shared `setOverride`.
|
|
5962
|
-
*
|
|
5963
|
-
* All of it is ordinary CSS the SSR engine already renders — there is no second tree and no
|
|
5964
|
-
* per-width HTML. See responsive-breakpoints-plan.md (D5).
|
|
5965
|
-
*/
|
|
5966
|
-
/** CSS lengths that are a FIXED pixel size — the ones that stop a layout fitting a phone. */
|
|
5967
|
-
const PX_LENGTH = /^-?\d+(\.\d+)?px$/i;
|
|
5968
|
-
/**
|
|
5969
|
-
* Hide / show at a width. Hiding is `display: none` INSIDE that breakpoint's rules — the node stays
|
|
5970
|
-
* in the DOM and in the SSR output (one HTML document serves every width), so hidden content still
|
|
5971
|
-
* ships bytes and is still indexed. Showing again clears the override rather than guessing a value.
|
|
5972
|
-
*
|
|
5973
|
-
* @returns the props to write, or `null` meaning "clear the display override at this breakpoint".
|
|
5974
|
-
*/
|
|
5975
|
-
function visibilityProps(hidden) {
|
|
5976
|
-
return hidden ? { display: 'none' } : null;
|
|
5977
|
-
}
|
|
5978
|
-
/** True when this node is hidden by the resolved styles at some width. */
|
|
5979
|
-
function isHiddenAt(resolvedStyleProps) {
|
|
5980
|
-
return String(resolvedStyleProps?.['display'] ?? '') === 'none';
|
|
5981
|
-
}
|
|
5982
|
-
/**
|
|
5983
|
-
* Stack a container's children in one column — the most common mobile edit, and four separate
|
|
5984
|
-
* controls today (display, direction, wrap, columns). A grid becomes a single-column grid; anything
|
|
5985
|
-
* else becomes a column flex box.
|
|
5986
|
-
*/
|
|
5987
|
-
function stackChildrenProps(resolvedStyleProps) {
|
|
5988
|
-
const display = String(resolvedStyleProps?.['display'] ?? '');
|
|
5989
|
-
if (display === 'grid' || display === 'inline-grid') {
|
|
5990
|
-
return { gridTemplateColumns: 'repeat(1, minmax(0, 1fr))' };
|
|
5991
|
-
}
|
|
5992
|
-
return { display: 'flex', flexDirection: 'column', flexWrap: 'nowrap' };
|
|
5993
|
-
}
|
|
5994
|
-
/**
|
|
5995
|
-
* Collapse a grid to `count` columns (a flex row is stacked instead — it has no column track).
|
|
5996
|
-
* `count` is clamped to at least 1.
|
|
5997
|
-
*/
|
|
5998
|
-
function collapseColumnsProps(resolvedStyleProps, count) {
|
|
5999
|
-
const columns = Math.max(1, Math.floor(count));
|
|
6000
|
-
const display = String(resolvedStyleProps?.['display'] ?? '');
|
|
6001
|
-
if (display === 'grid' || display === 'inline-grid') {
|
|
6002
|
-
return { gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` };
|
|
6003
|
-
}
|
|
6004
|
-
return columns === 1 ? stackChildrenProps(resolvedStyleProps) : { flexWrap: 'wrap' };
|
|
6005
|
-
}
|
|
6006
|
-
/** How many equal columns a `repeat(N, …)` track list describes, or null when it isn't one. */
|
|
6007
|
-
function columnCountOf(resolvedStyleProps) {
|
|
6008
|
-
const match = /^repeat\(\s*(\d+)\s*,/.exec(String(resolvedStyleProps?.['gridTemplateColumns'] ?? ''));
|
|
6009
|
-
return match ? Number(match[1]) : null;
|
|
6010
|
-
}
|
|
6011
|
-
/**
|
|
6012
|
-
* Make fixed pixel sizes fluid: `width: 520px` → `width: 100%` with the original kept as
|
|
6013
|
-
* `max-width`, so the element still never exceeds its designed size on a wide screen. `minWidth` in
|
|
6014
|
-
* px is dropped to 0 — it is what actually forces horizontal scrolling on a phone.
|
|
6015
|
-
*
|
|
6016
|
-
* @returns the props to write; EMPTY when there is nothing fixed to fluidise.
|
|
6017
|
-
*/
|
|
6018
|
-
function fluidiseProps(resolvedStyleProps) {
|
|
6019
|
-
const out = {};
|
|
6020
|
-
const width = String(resolvedStyleProps?.['width'] ?? '');
|
|
6021
|
-
const minWidth = String(resolvedStyleProps?.['minWidth'] ?? '');
|
|
6022
|
-
if (PX_LENGTH.test(width)) {
|
|
6023
|
-
out['width'] = '100%';
|
|
6024
|
-
const maxWidth = String(resolvedStyleProps?.['maxWidth'] ?? '');
|
|
6025
|
-
if (!maxWidth || maxWidth === 'none')
|
|
6026
|
-
out['maxWidth'] = width;
|
|
6027
|
-
}
|
|
6028
|
-
if (PX_LENGTH.test(minWidth) && parseFloat(minWidth) > 0)
|
|
6029
|
-
out['minWidth'] = '0px';
|
|
6030
|
-
return out;
|
|
6396
|
+
return { before, pinnedAfter };
|
|
6031
6397
|
}
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6398
|
+
|
|
6399
|
+
/** Counts for ONE node (the selection). Breakpoints with nothing overridden are omitted. */
|
|
6400
|
+
function nodeBreakpointCounts(node) {
|
|
6401
|
+
const counts = {};
|
|
6402
|
+
if (!node?.responsive)
|
|
6403
|
+
return counts;
|
|
6404
|
+
for (const { id } of BREAKPOINTS) {
|
|
6405
|
+
if (id === Breakpoint.Base)
|
|
6406
|
+
continue;
|
|
6407
|
+
const n = countOverrides(node.responsive, id);
|
|
6408
|
+
if (n)
|
|
6409
|
+
counts[id] = n;
|
|
6410
|
+
}
|
|
6411
|
+
return counts;
|
|
6035
6412
|
}
|
|
6036
|
-
/**
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
const none = Array.from({ length: Math.max(0, count) }, () => null);
|
|
6050
|
-
if (from < 0 || from >= count || to < 0 || to >= count || from === to)
|
|
6051
|
-
return none;
|
|
6052
|
-
// Visual order after the move, as a list of CHILD indices.
|
|
6053
|
-
const sequence = Array.from({ length: count }, (_, i) => i);
|
|
6054
|
-
sequence.splice(to, 0, ...sequence.splice(from, 1));
|
|
6055
|
-
const orderByChild = none.slice();
|
|
6056
|
-
sequence.forEach((childIndex, position) => { orderByChild[childIndex] = position; });
|
|
6057
|
-
return orderByChild;
|
|
6413
|
+
/** Counts summed over whole trees (the page, when nothing is selected). */
|
|
6414
|
+
function treeBreakpointCounts(...roots) {
|
|
6415
|
+
const counts = {};
|
|
6416
|
+
const visit = (nodes) => {
|
|
6417
|
+
for (const node of nodes ?? []) {
|
|
6418
|
+
for (const [bp, n] of Object.entries(nodeBreakpointCounts(node))) {
|
|
6419
|
+
counts[bp] = (counts[bp] ?? 0) + n;
|
|
6420
|
+
}
|
|
6421
|
+
visit(node.children);
|
|
6422
|
+
}
|
|
6423
|
+
};
|
|
6424
|
+
roots.forEach(visit);
|
|
6425
|
+
return counts;
|
|
6058
6426
|
}
|
|
6059
6427
|
|
|
6060
6428
|
/**
|
|
6061
|
-
*
|
|
6062
|
-
*
|
|
6429
|
+
* Resolve where "click to insert" puts a new element, from the current selection — the rule the
|
|
6430
|
+
* components panel and the canvas "+" share (builder-usability-plan.md, B2):
|
|
6063
6431
|
*
|
|
6064
|
-
*
|
|
6065
|
-
*
|
|
6066
|
-
*
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
AuditCode["FixedWidth"] = "fixed-width";
|
|
6072
|
-
AuditCode["TapTarget"] = "tap-target";
|
|
6073
|
-
AuditCode["TinyText"] = "tiny-text";
|
|
6074
|
-
AuditCode["UnboundedImage"] = "unbounded-image";
|
|
6075
|
-
/** A slider showing more slides than the width can carry — each one unreadably narrow. */
|
|
6076
|
-
AuditCode["SlidesOverflow"] = "slides-overflow";
|
|
6077
|
-
/** An out-of-flow node whose box lands outside the frame, so it is unreachable at this width. */
|
|
6078
|
-
AuditCode["OffscreenAbsolute"] = "offscreen-absolute";
|
|
6079
|
-
/** Form content inside a link or a button — a STRUCTURE finding, true at every width (form-nesting-audit.util). */
|
|
6080
|
-
AuditCode["FormInLink"] = "form-in-link";
|
|
6081
|
-
/** An icon-only button or link with no Accessible name — announced as an unnamed control (CC1 N6). */
|
|
6082
|
-
AuditCode["UnnamedControl"] = "unnamed-control";
|
|
6083
|
-
/** A composite whose items come from content, and its source returned no rows (CC2, board 9): the
|
|
6084
|
-
* canvas shows the placeholder, the live site renders nothing. */
|
|
6085
|
-
AuditCode["EmptySource"] = "empty-source";
|
|
6086
|
-
})(AuditCode || (AuditCode = {}));
|
|
6087
|
-
/** A fix that changes the tree, not a style or a trait — applied as one page-history step. */
|
|
6088
|
-
var StructureFix;
|
|
6089
|
-
(function (StructureFix) {
|
|
6090
|
-
/** Move the form out of the link or button holding it, to right after it. */
|
|
6091
|
-
StructureFix["MoveFormOut"] = "move-form-out";
|
|
6092
|
-
})(StructureFix || (StructureFix = {}));
|
|
6093
|
-
var AuditSeverity;
|
|
6094
|
-
(function (AuditSeverity) {
|
|
6095
|
-
AuditSeverity["Error"] = "error";
|
|
6096
|
-
AuditSeverity["Warning"] = "warning";
|
|
6097
|
-
AuditSeverity["Info"] = "info";
|
|
6098
|
-
})(AuditSeverity || (AuditSeverity = {}));
|
|
6099
|
-
/** WCAG 2.5.8 minimum target size. */
|
|
6100
|
-
const MIN_TAP_PX = 44;
|
|
6101
|
-
/** Below this, iOS Safari zooms the page when an adjacent input is focused. */
|
|
6102
|
-
const MIN_TEXT_PX = 12;
|
|
6103
|
-
/** Types a visitor taps. */
|
|
6104
|
-
const TAPPABLE = new Set(['Button', 'ButtonBlock', 'Link', 'LinkedContainer']);
|
|
6105
|
-
/** Narrower than this, a slide shows too little of its content to be worth showing at all. */
|
|
6106
|
-
const MIN_SLIDE_PX = 200;
|
|
6107
|
-
/** Out-of-flow positioning that can put a node outside the frame. */
|
|
6108
|
-
const OUT_OF_FLOW = new Set(['absolute', 'fixed']);
|
|
6109
|
-
const nameOf = (node) => node.label || node.type;
|
|
6110
|
-
const pxOf = (value) => {
|
|
6111
|
-
const match = /^(-?\d+(?:\.\d+)?)px$/i.exec(String(value ?? ''));
|
|
6112
|
-
return match ? Number(match[1]) : null;
|
|
6113
|
-
};
|
|
6114
|
-
/**
|
|
6115
|
-
* Audit nodes laid out at `viewportWidth`. Returns findings ordered by severity (errors first),
|
|
6116
|
-
* then by the order the nodes were given — so the worst thing is at the top of the list.
|
|
6432
|
+
* nothing selected → appended to the page root
|
|
6433
|
+
* a CONTAINER selected → appended INSIDE it (its last child)
|
|
6434
|
+
* a LEAF selected → inserted AFTER it, as its next sibling
|
|
6435
|
+
*
|
|
6436
|
+
* `isContainer` decides what can hold children — the builder passes "has a registered drop zone",
|
|
6437
|
+
* the same test a drag uses, so a click can never insert somewhere a drop couldn't.
|
|
6438
|
+
* A selection not found in `roots` (stale) falls back to appending to the root.
|
|
6117
6439
|
*/
|
|
6118
|
-
function
|
|
6119
|
-
const
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
if (
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
message: `${name} is ${Math.round(node.width)}px wide in a ${viewportWidth}px viewport — the page scrolls sideways.`,
|
|
6130
|
-
fix: canFluidise(styles) ? fluidiseProps(styles) : { maxWidth: '100%' },
|
|
6131
|
-
});
|
|
6132
|
-
}
|
|
6133
|
-
else if (canFluidise(styles)) {
|
|
6134
|
-
// 2. A fixed px size that still fits, but will not on a narrower phone.
|
|
6135
|
-
const width = pxOf(styles['width']);
|
|
6136
|
-
findings.push({
|
|
6137
|
-
nodeId: node.id, nodeName: name, code: AuditCode.FixedWidth, severity: AuditSeverity.Warning,
|
|
6138
|
-
message: width !== null
|
|
6139
|
-
? `${name} has a fixed width of ${width}px — it cannot shrink on a narrower screen.`
|
|
6140
|
-
: `${name} has a fixed minimum width — it forces sideways scrolling below that size.`,
|
|
6141
|
-
fix: fluidiseProps(styles),
|
|
6142
|
-
});
|
|
6143
|
-
}
|
|
6144
|
-
// 3. Tap targets below the WCAG minimum.
|
|
6145
|
-
if (TAPPABLE.has(node.type) && node.width > 0 && node.height > 0
|
|
6146
|
-
&& (node.width < MIN_TAP_PX || node.height < MIN_TAP_PX)) {
|
|
6147
|
-
findings.push({
|
|
6148
|
-
nodeId: node.id, nodeName: name, code: AuditCode.TapTarget, severity: AuditSeverity.Warning,
|
|
6149
|
-
message: `${name} is ${Math.round(node.width)}×${Math.round(node.height)}px — below the ${MIN_TAP_PX}px minimum tap target.`,
|
|
6150
|
-
fix: { minHeight: `${MIN_TAP_PX}px`, minWidth: `${MIN_TAP_PX}px` },
|
|
6151
|
-
});
|
|
6152
|
-
}
|
|
6153
|
-
// 4. Text too small to read (and small enough to trigger iOS zoom).
|
|
6154
|
-
const fontSize = pxOf(styles['fontSize']);
|
|
6155
|
-
if (fontSize !== null && fontSize < MIN_TEXT_PX) {
|
|
6156
|
-
findings.push({
|
|
6157
|
-
nodeId: node.id, nodeName: name, code: AuditCode.TinyText, severity: AuditSeverity.Warning,
|
|
6158
|
-
message: `${name} renders at ${fontSize}px — under the ${MIN_TEXT_PX}px readable minimum.`,
|
|
6159
|
-
fix: { fontSize: `${MIN_TEXT_PX}px` },
|
|
6160
|
-
});
|
|
6161
|
-
}
|
|
6162
|
-
// 5. An image with a fixed height and no ratio crops unpredictably as the width changes.
|
|
6163
|
-
if (node.type === 'Image' && pxOf(styles['height']) !== null
|
|
6164
|
-
&& !String(styles['aspectRatio'] ?? '').trim().replace(/^auto$/, '')) {
|
|
6165
|
-
findings.push({
|
|
6166
|
-
nodeId: node.id, nodeName: name, code: AuditCode.UnboundedImage, severity: AuditSeverity.Info,
|
|
6167
|
-
message: `${name} has a fixed height and no aspect ratio — it crops unpredictably as the width changes.`,
|
|
6168
|
-
fix: { height: 'auto', aspectRatio: '16 / 9' },
|
|
6169
|
-
});
|
|
6170
|
-
}
|
|
6171
|
-
// 6. A slider showing more slides than this width can carry. NOT a CSS problem: slide count
|
|
6172
|
-
// is a component trait, so the fix is a per-breakpoint TRAIT override (P3-G1), not a style.
|
|
6173
|
-
if (node.type === 'SliderContainer' && node.width > 0) {
|
|
6174
|
-
const perView = Number(node.inputs?.['slidesPerView']);
|
|
6175
|
-
if (Number.isFinite(perView) && perView > 1) {
|
|
6176
|
-
const slideWidth = node.width / perView;
|
|
6177
|
-
if (slideWidth < MIN_SLIDE_PX) {
|
|
6178
|
-
const fits = Math.max(1, Math.floor(node.width / MIN_SLIDE_PX));
|
|
6179
|
-
findings.push({
|
|
6180
|
-
nodeId: node.id, nodeName: name, code: AuditCode.SlidesOverflow, severity: AuditSeverity.Warning,
|
|
6181
|
-
message: `${name} shows ${perView} slides across ${Math.round(node.width)}px — ${Math.round(slideWidth)}px each. Show ${fits} at this width.`,
|
|
6182
|
-
inputFix: { slidesPerView: fits },
|
|
6183
|
-
});
|
|
6184
|
-
}
|
|
6185
|
-
}
|
|
6186
|
-
}
|
|
6187
|
-
// 7. An icon-only control with no name (WCAG 4.1.2): a screen reader says "button" and nothing
|
|
6188
|
-
// else. No auto-fix — only the author knows what it does; the finding selects it.
|
|
6189
|
-
if (node.unnamedControl) {
|
|
6190
|
-
findings.push({
|
|
6191
|
-
nodeId: node.id, nodeName: name, code: AuditCode.UnnamedControl, severity: AuditSeverity.Error,
|
|
6192
|
-
message: `${name} shows only an icon and has no accessible name — screen readers announce it as an unnamed control. Name it in Bindings ▸ Accessibility.`,
|
|
6193
|
-
});
|
|
6194
|
-
}
|
|
6195
|
-
// 7b. A composite from content whose source has no rows (CC2 R3): the visitor sees nothing there.
|
|
6196
|
-
if (node.emptySource) {
|
|
6197
|
-
findings.push({
|
|
6198
|
-
nodeId: node.id, nodeName: name, code: AuditCode.EmptySource, severity: AuditSeverity.Warning,
|
|
6199
|
-
message: `${name} in ${node.emptySource.section}: its source ${node.emptySource.alias} has no rows.`,
|
|
6200
|
-
});
|
|
6201
|
-
}
|
|
6202
|
-
// 8. An out-of-flow node that has left the frame. Measured, not inferred: a negative offset
|
|
6203
|
-
// is legitimate design until it actually puts the content beyond the edge.
|
|
6204
|
-
if (OUT_OF_FLOW.has(String(styles['position'] ?? '')) && node.left !== undefined && node.width > 0) {
|
|
6205
|
-
const right = node.left + node.width;
|
|
6206
|
-
const offLeft = right <= 0.5;
|
|
6207
|
-
const offRight = node.left >= viewportWidth - 0.5;
|
|
6208
|
-
if (offLeft || offRight) {
|
|
6209
|
-
findings.push({
|
|
6210
|
-
nodeId: node.id, nodeName: name, code: AuditCode.OffscreenAbsolute, severity: AuditSeverity.Error,
|
|
6211
|
-
message: `${name} is positioned ${offLeft ? 'left of' : 'beyond'} the ${viewportWidth}px viewport — it cannot be seen or tapped at this width.`,
|
|
6212
|
-
// No blind auto-fix: the author's intent (a decorative overflow vs a mistake)
|
|
6213
|
-
// is not knowable from geometry. Selecting the node is the useful action.
|
|
6214
|
-
});
|
|
6215
|
-
}
|
|
6216
|
-
}
|
|
6440
|
+
function resolveInsertTarget(roots, selected, isContainer) {
|
|
6441
|
+
const append = (children, parent) => ({ children, index: children.length, parent });
|
|
6442
|
+
if (!selected)
|
|
6443
|
+
return append(roots, null);
|
|
6444
|
+
const path = nodeAncestorPath(roots, selected);
|
|
6445
|
+
if (!path)
|
|
6446
|
+
return append(roots, null);
|
|
6447
|
+
if (isContainer(selected)) {
|
|
6448
|
+
if (!selected.children)
|
|
6449
|
+
selected.children = [];
|
|
6450
|
+
return append(selected.children, selected);
|
|
6217
6451
|
}
|
|
6218
|
-
const
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
.sort((a, b) => rank[a.finding.severity] - rank[b.finding.severity] || a.index - b.index)
|
|
6222
|
-
.map(entry => entry.finding);
|
|
6223
|
-
}
|
|
6224
|
-
/** Counts per severity — the per-breakpoint summary strip. */
|
|
6225
|
-
function summarise(findings) {
|
|
6226
|
-
const out = { [AuditSeverity.Error]: 0, [AuditSeverity.Warning]: 0, [AuditSeverity.Info]: 0 };
|
|
6227
|
-
for (const finding of findings)
|
|
6228
|
-
out[finding.severity]++;
|
|
6229
|
-
return out;
|
|
6452
|
+
const parent = path.length >= 2 ? path[path.length - 2] : null;
|
|
6453
|
+
const siblings = parent ? (parent.children ?? (parent.children = [])) : roots;
|
|
6454
|
+
return { children: siblings, index: siblings.indexOf(selected) + 1, parent };
|
|
6230
6455
|
}
|
|
6231
6456
|
|
|
6232
6457
|
const FINDING_KEY = 'builder.chrome.auditFormInLink';
|
|
@@ -7158,10 +7383,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
|
|
|
7158
7383
|
* An accordion's and a disclosure's parts are named by the part alone (`trigger`, "Accordion · trigger", board 3);
|
|
7159
7384
|
* a card keeps its full role (`card.title`, "Card · card.title", board 8).
|
|
7160
7385
|
*/
|
|
7161
|
-
const SHORT_ROLE_KINDS = new Set([CompositeKind.Accordion, CompositeKind.Disclosure, CompositeKind.Tabs]);
|
|
7386
|
+
const SHORT_ROLE_KINDS = new Set([CompositeKind.Accordion, CompositeKind.Disclosure, CompositeKind.Tabs, CompositeKind.Segmented]);
|
|
7162
7387
|
/** A composite kind's glyph (Phosphor, weight-less): the badge, the root's crumb and its tree row carry it
|
|
7163
7388
|
* (board 3: "the composite root carrying its kind icon"). A card keeps the generic stack glyph. */
|
|
7164
|
-
const KIND_GLYPHS = {
|
|
7389
|
+
const KIND_GLYPHS = {
|
|
7390
|
+
[CompositeKind.Accordion]: 'ph-rows', [CompositeKind.Disclosure]: 'ph-caret-circle-down', [CompositeKind.Tabs]: 'ph-tabs', [CompositeKind.Segmented]: 'ph-pill',
|
|
7391
|
+
slider: 'ph-slideshow',
|
|
7392
|
+
};
|
|
7165
7393
|
const DEFAULT_KIND_GLYPH = 'ph-stack';
|
|
7166
7394
|
/** The tree icon of an accordion's structural parts (board 3): rows on the root, a stack on an item and a
|
|
7167
7395
|
* panel, a cursor on the header, a sparkle on the icon. Other parts keep their type's icon. */
|
|
@@ -7183,6 +7411,10 @@ const PART_ICONS = {
|
|
|
7183
7411
|
[TabsRole.Trigger]: 'ph-thin ph-cursor',
|
|
7184
7412
|
[TabsRole.Panels]: 'ph-thin ph-stack',
|
|
7185
7413
|
[TabsRole.Panel]: 'ph-thin ph-stack',
|
|
7414
|
+
// CC4b — the Segmented control (board 10): its pill glyph on the root, a cursor on an option, a seal on its badge.
|
|
7415
|
+
[SegmentedRole.Segmented]: 'ph-thin ph-pill',
|
|
7416
|
+
[SegmentedRole.Option]: 'ph-thin ph-cursor',
|
|
7417
|
+
[SegmentedRole.Badge]: 'ph-thin ph-seal',
|
|
7186
7418
|
// CC3 — a Slider's controls (board 6): a cursor on the buttons, six dots on the pagination.
|
|
7187
7419
|
[SliderRole.Pause]: 'ph-thin ph-cursor',
|
|
7188
7420
|
[SliderRole.Prev]: 'ph-thin ph-cursor',
|
|
@@ -7218,6 +7450,8 @@ function compositePartSub(node, ancestors = []) {
|
|
|
7218
7450
|
return accordionItemLabel(node) || null;
|
|
7219
7451
|
if (node.role === TabsRole.Trigger)
|
|
7220
7452
|
return tabLabel(node) || null;
|
|
7453
|
+
if (node.role === SegmentedRole.Option)
|
|
7454
|
+
return segmentedOptionLabel(node) || null;
|
|
7221
7455
|
if (node.role === TabsRole.Panel) {
|
|
7222
7456
|
const root = exclusiveRootOf(node, ancestors);
|
|
7223
7457
|
const tab = root ? tabTriggerOf(root, node) : undefined;
|
|
@@ -7281,7 +7515,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
|
|
|
7281
7515
|
const FROM_ROW_KEY = 'builder.chrome.fromRow';
|
|
7282
7516
|
const ADD_ITEM_KEY = 'builder.chrome.addItem';
|
|
7283
7517
|
/** The kind label a composite part's selection badge shows (Card v2: "Card · card.title"). */
|
|
7284
|
-
const COMPOSITE_KIND_KEYS = { card: 'builder.chrome.kindCard', accordion: 'builder.chrome.kindAccordion', disclosure: 'builder.chrome.kindDisclosure', tabs: 'builder.chrome.kindTabs', slider: 'builder.chrome.kindSlider', nav: 'builder.chrome.kindNav' };
|
|
7518
|
+
const COMPOSITE_KIND_KEYS = { card: 'builder.chrome.kindCard', accordion: 'builder.chrome.kindAccordion', disclosure: 'builder.chrome.kindDisclosure', tabs: 'builder.chrome.kindTabs', segmented: 'builder.chrome.kindSegmented', slider: 'builder.chrome.kindSlider', nav: 'builder.chrome.kindNav' };
|
|
7285
7519
|
const VIDEO_TYPE = 'Video';
|
|
7286
7520
|
const VIDEO_KIND_KEY = 'builder.chrome.kindVideo';
|
|
7287
7521
|
const VIDEO_POSTER_ONLY_KEY = 'builder.chrome.videoPosterOnly';
|
|
@@ -7290,9 +7524,12 @@ const VIDEO_FILE_KEY = 'builder.chrome.videoFile';
|
|
|
7290
7524
|
const ADD_ITEM_ROLES = new Set([
|
|
7291
7525
|
AccordionRole.Accordion, AccordionRole.Item, AccordionRole.Trigger, AccordionRole.Panel,
|
|
7292
7526
|
TabsRole.Tabs, TabsRole.List, TabsRole.Trigger, TabsRole.Panels, TabsRole.Panel,
|
|
7527
|
+
SegmentedRole.Segmented, SegmentedRole.Option,
|
|
7293
7528
|
]);
|
|
7294
7529
|
/** Tabs say "Add tab" (board 3) — the button adds a tab AND its panel. */
|
|
7295
7530
|
const ADD_TAB_KEY = 'builder.chrome.addTab';
|
|
7531
|
+
/** A Segmented control says "Add option" (board 10). */
|
|
7532
|
+
const ADD_OPTION_KEY = 'builder.chrome.addOption';
|
|
7296
7533
|
class OverlayComponent {
|
|
7297
7534
|
constructor(elRef, instanceRegistry, overlayService, viewportService, presetRegistry, symbolRegistry, interactionPresetRegistry, interactionEngine, clipboard, styleRegistry, panelEventService, cdr, dragEngine, labels) {
|
|
7298
7535
|
this.elRef = elRef;
|
|
@@ -7543,7 +7780,8 @@ class OverlayComponent {
|
|
|
7543
7780
|
return ADD_ITEM_ROLES.has(this.selectEntry?.node?.role ?? '');
|
|
7544
7781
|
}
|
|
7545
7782
|
get addItemLabel() {
|
|
7546
|
-
const
|
|
7783
|
+
const node = this.selectEntry?.node;
|
|
7784
|
+
const key = isTabsPart(node) ? ADD_TAB_KEY : isSegmentedPart(node) ? ADD_OPTION_KEY : ADD_ITEM_KEY;
|
|
7547
7785
|
return this.builderLabels(key, BUILDER_CHROME_EN[key]);
|
|
7548
7786
|
}
|
|
7549
7787
|
onAddItem(event) {
|
|
@@ -8150,7 +8388,7 @@ function walk(nodes, depth, out, ctx, inTemplate, templateOf, ancestors) {
|
|
|
8150
8388
|
node,
|
|
8151
8389
|
icon: bound ? BOUND_ICON : (compositePartIcon(node.role) || ctx.iconOf(node.type) || DEFAULT_ICON),
|
|
8152
8390
|
label,
|
|
8153
|
-
hiddenAtRest: isHiddenAtRest(node),
|
|
8391
|
+
hiddenAtRest: isHiddenAtRest(node) || (!!ctx.onAtRest && hiddenUntilItemOn(node, ctx.onAtRest)),
|
|
8154
8392
|
depth,
|
|
8155
8393
|
expanded,
|
|
8156
8394
|
hasChildren,
|
|
@@ -8393,6 +8631,7 @@ class LayerTreeComponent {
|
|
|
8393
8631
|
.replace('{source}', sourceTypeLabel(rows.alias, this.dataSources?.findByAlias(rows.alias), this.typeLabels))
|
|
8394
8632
|
.replace('{count}', String(rows.count))
|
|
8395
8633
|
: null,
|
|
8634
|
+
onAtRest: itemsOnAtRest([...(this.layout ?? []), ...(this.globalElements ?? [])]),
|
|
8396
8635
|
};
|
|
8397
8636
|
this.flatTree = flattenLayerTree(this.layout, ctx);
|
|
8398
8637
|
this.flatGlobalTree = flattenLayerTree(this.globalElements, ctx);
|
|
@@ -8706,7 +8945,10 @@ class BuilderComponent {
|
|
|
8706
8945
|
return out;
|
|
8707
8946
|
}
|
|
8708
8947
|
refreshStructureFindings() {
|
|
8709
|
-
this.structureFindings =
|
|
8948
|
+
this.structureFindings = [
|
|
8949
|
+
...formNestingFindings(this.layout, this.globalElements, this.labels),
|
|
8950
|
+
...compositeStructureFindings(this.layout, this.globalElements, this.labels),
|
|
8951
|
+
];
|
|
8710
8952
|
}
|
|
8711
8953
|
/** A structure fix moves nodes on the page: ONE layout change, so ONE undo step. */
|
|
8712
8954
|
applyStructureFix(finding) {
|
|
@@ -9696,6 +9938,9 @@ class BuilderComponent {
|
|
|
9696
9938
|
else if (action === NodeAction.Duplicate) {
|
|
9697
9939
|
if (this.duplicateTabPairOf(node))
|
|
9698
9940
|
return;
|
|
9941
|
+
// The same acceptance rule as the toolbar's Duplicate (a fifth Segmented option is refused, CC4b).
|
|
9942
|
+
if (this.dragEngine.refuseInsert(node, this.siblingAncestors(node)))
|
|
9943
|
+
return;
|
|
9699
9944
|
const clone = this.deepCloneWithNewIds(node);
|
|
9700
9945
|
result.parent.splice(result.index + 1, 0, clone);
|
|
9701
9946
|
this.cdr.detectChanges();
|
|
@@ -9956,6 +10201,8 @@ class BuilderComponent {
|
|
|
9956
10201
|
const root = exclusiveRootOf(node, path);
|
|
9957
10202
|
if (!root)
|
|
9958
10203
|
return false;
|
|
10204
|
+
if (isSegmentedRoot(root))
|
|
10205
|
+
return this.addSegmentedOptionTo(root, path);
|
|
9959
10206
|
if (isTabsFromContent(root))
|
|
9960
10207
|
return true;
|
|
9961
10208
|
const anchor = path.find(n => isExclusiveItemPart(n)) ?? null;
|
|
@@ -9967,6 +10214,23 @@ class BuilderComponent {
|
|
|
9967
10214
|
this.panelEventService.emit(PanelEventTypes.SELECT_NODE, { node: pair.tab });
|
|
9968
10215
|
return true;
|
|
9969
10216
|
}
|
|
10217
|
+
/** Toolbar ▸ Add option on a Segmented part (CC4b): a fresh option after the selection's own option (or at the end
|
|
10218
|
+
* for the root) — ONE undo step, then it is selected. A fifth is refused, said in a toast (Q40). */
|
|
10219
|
+
addSegmentedOptionTo(root, path) {
|
|
10220
|
+
const tree = root;
|
|
10221
|
+
if (segmentedAddRefusal(tree)) {
|
|
10222
|
+
this.dragEngine.insertRefused$.next({ reason: InsertRefusal.SegmentedMaxOptions, message: insertRefusalLabel(InsertRefusal.SegmentedMaxOptions, this.labels) });
|
|
10223
|
+
return true;
|
|
10224
|
+
}
|
|
10225
|
+
const anchor = path.find(n => n.role === SegmentedRole.Option) ?? null;
|
|
10226
|
+
const factory = builderCompositeFactory(t => this.registry.createConfig(t), t => this.registry.baselineStyleProps(t));
|
|
10227
|
+
const option = addSegmentedOption(tree, factory, anchor);
|
|
10228
|
+
if (!option)
|
|
10229
|
+
return true;
|
|
10230
|
+
this.panelEventService.emit(PanelEventTypes.NODE_STRUCTURE_CHANGED, { node: root });
|
|
10231
|
+
this.panelEventService.emit(PanelEventTypes.SELECT_NODE, { node: option });
|
|
10232
|
+
return true;
|
|
10233
|
+
}
|
|
9970
10234
|
/** Duplicate on a tab or a panel (toolbar, layer tree): the PAIR is duplicated under one new key. True when handled. */
|
|
9971
10235
|
duplicateTabPairOf(node) {
|
|
9972
10236
|
const root = tabsRootForPart(this.layout, this.globalElements, node);
|
|
@@ -13847,5 +14111,5 @@ class BuilderSpacerComponentConfig extends ComponentConfig {
|
|
|
13847
14111
|
* Generated bundle index. Do not edit.
|
|
13848
14112
|
*/
|
|
13849
14113
|
|
|
13850
|
-
export { ACTION_TYPE_OPTIONS, AuditCode, AuditPanelComponent, AuditSeverity, BODY_CRUMB_LABEL, BOUND_ICON, BREAKPOINT_PRESENTATION, BUILDER_CANVAS_CONTAINER, BUILDER_CHROME_EN, BUILDER_FONT_LINK_ATTR, BUILDER_TOKEN_SCOPE_CLASS, BackgroundGroupStyleConfig, BorderGroupStyleConfig, BreadcrumbBarComponent, BuilderButtonBlockComponent, BuilderButtonComponentConfig, BuilderClipboardService, BuilderComponent, BuilderContainerComponent, BuilderContainerComponentConfig, BuilderDataSourceService, BuilderDialogComponent, BuilderDividerComponentConfig, BuilderHeadingComponentConfig, BuilderHistoryService, BuilderIconComponentConfig, BuilderImageComponentConfig, BuilderLinkComponentConfig, BuilderLinkedContainerComponent, BuilderLinkedContainerConfig, BuilderListComponentConfig, BuilderListItemComponentConfig, BuilderMode, BuilderRepeaterComponent, BuilderSpacerComponentConfig, ButtonBlockComponentConfig, CANVAS_EMPTY_IMAGE_LABEL_EN, CANVAS_EMPTY_IMAGE_LABEL_KEY, CANVAS_ROOT_RESET_CSS, COMPOSITE_PRESETS, CanvasDesignStateService, CanvasShowState, CardComponentConfig, ChildLayoutGroupStyleConfig, ComponentConfig, ComponentRegistryService, CompositeCanvasService, CompositePaletteGroup, CompositePresetId, DEFAULT_ESSENTIAL_TRAITS, DESCRIPTOR_MAX_LENGTH, DRAG_HANDLE_ATTR, DROP_REFUSED_CLASS, DataSourceRegistryService, DataSourceType, DialogComponentConfig, DimensionsGroupStyleConfig, DragEngineService, EASING_OPTIONS$1 as EASING_OPTIONS, EDITOR_CHROME_ATTRIBUTE, ESSENTIAL_TRAITS_BY_TYPE, EffectsGroupStyleConfig, FIELD_KIND_PRESETS, FIELD_PARTS, FORM_STATE_BLOCK_TYPES, FORM_TYPE, FieldComponentConfig, FieldGroupComponentConfig, FormComponentConfig, FormErrorComponentConfig, FormStateService, FormSuccessComponentConfig, GLOBALS_AFTER_CLASS, GalleryConfig, GlobalCanvasLayer, GroupStyleConfig, HIDDEN_AT_REST_LABEL_KEY, HIDDEN_AT_REST_ROLES, HIDDEN_AT_REST_TYPES, INSERT_REFUSAL_LABEL_KEYS, INTERACTION_STATES, INVERSE_ACTION, IOX_BUILDER_CANVAS, IOX_BUILDER_LABELS, IOX_CONTENT_TYPE_LABELS, IOX_DATA_SOURCE_ENDPOINTS, InsertRefusal, InteractionEngineService, InteractionPresetRegistryService, IoxBuilderModule, IoxDraggableDirective, IoxDropzoneDirective, LAYOUT_PRESETS, LayerTreeComponent, LayoutGroupStyleConfig, ListGroupStyleConfig, NodeAction, OverlayComponent, OverlayService, PALETTE_PRESET_DEFS, PanelChildComponent, PanelComponent, PanelEventService, PanelEventTypes, PanelTypes, PositionGroupStyleConfig, PresetRegistryService, RELATIONAL_STATES, RELATIONAL_STATE_BASE, REPEATER_SOURCE_PROP, REPEATER_TYPE, REVEALED_WHILE_SELECTED_ROLES, ROOT_ESCAPE_PX, ROUTE_ANIMATION_OPTIONS, RenderDirective, RepeaterComponentConfig, RootZone, SCREEN_WIDTH_OPTIONS, SILENT_REFUSALS, STRUCTURAL_STATES, SUPPORTED_STATES, SectionComponent, SectionComponentConfig, SelectionReveal, SizeGroupStyleConfig, SliderContainerComponent, SliderContainerConfig, SliderSlideComponent, SliderSlideConfig, SpacingGroupStyleConfig, StructureFix, StyleCategory, StyleRegistryService, TraitConfig as StyleTraitConfig, SubmitButtonComponentConfig, SymbolRegistryService, TARGETED_STATES, TARGETED_STATE_BASE, TOGGLE_PART_ROLES, TRIGGER_OPTIONS, TextBlockComponentConfig, ToolbarAction, ToolbarComponent, TraitConfig, TraitInputType, TypographyGroupStyleConfig, UNITS_ALL, UNITS_DEG, UNITS_FIXED, UNITS_NO_VW, VALIDATION_STATES, VALIDATION_STATE_CLASS, VideoConfig, ViewportService, ZOOM_OPTIONS, arrayItemSchemaFields, arraySchemaFields, auditNodes, bindingLabel, bindingLabels, buildCompositePreset, buildConsentPreset, buildElementStyleSpecimen, buildFieldKindPreset, buildFlavorSpecimen, buildFormPreset, buildFullStyleTraits, buildLayoutPreset, buildPalettePreset, buildPresetStyleTraits, builderCompositeFactory, canFluidise, canvasEmptyImageLabel, canvasHostDisplay, collapseColumnsProps, columnCountOf, defaultBuilderLabels, defaultPageSettings, deleteTabPair, dropChildren, dropTargetLabel, duplicateTabPair, escapesRoot, essentialTraitsFor, findNodeLocation, findParentOf, flattenLayerTree, flattenSchemaFields, fluidiseProps, generateNodeId, globalCanvasLayer, insertRefusal, insertRefusalLabel, isHiddenAt, isHiddenAtRest, isPinnedGlobal, isSpecimen, markSpecimen, matchesControlSearch, nearestFormNode, nodeAncestorPath, nodeAncestorPathInEither, nodeDescriptor, nodeStyleMap, overlayGlobals, pathToNode, refreshSubtree, regenerateNodeIds, removalRefusal, reorderProps, repeaterSourceAlias, repeaterSubLabel, resolveInsertTarget, resolveTraitControllerType, resolveTraitOptions, retypeNode, rootEscapeBand, rootZone, selectionRevealOf, setStyleTrait, setTrait, shortDescriptor, sourceContentType, sourceTypeLabel, stackChildrenProps, summarise, syncEmptyImagePlaceholder, syncNodeDesign, syncNodeInputs, tabPairLabel, tabsRootForPart, textNode, upsertNodeStyles, visibilityProps, walkNodes, wrapNode };
|
|
14114
|
+
export { ACTION_TYPE_OPTIONS, AuditCode, AuditPanelComponent, AuditSeverity, BODY_CRUMB_LABEL, BOUND_ICON, BREAKPOINT_PRESENTATION, BUILDER_CANVAS_CONTAINER, BUILDER_CHROME_EN, BUILDER_FONT_LINK_ATTR, BUILDER_TOKEN_SCOPE_CLASS, BackgroundGroupStyleConfig, BorderGroupStyleConfig, BreadcrumbBarComponent, BuilderButtonBlockComponent, BuilderButtonComponentConfig, BuilderClipboardService, BuilderComponent, BuilderContainerComponent, BuilderContainerComponentConfig, BuilderDataSourceService, BuilderDialogComponent, BuilderDividerComponentConfig, BuilderHeadingComponentConfig, BuilderHistoryService, BuilderIconComponentConfig, BuilderImageComponentConfig, BuilderLinkComponentConfig, BuilderLinkedContainerComponent, BuilderLinkedContainerConfig, BuilderListComponentConfig, BuilderListItemComponentConfig, BuilderMode, BuilderRepeaterComponent, BuilderSpacerComponentConfig, ButtonBlockComponentConfig, CANVAS_EMPTY_IMAGE_LABEL_EN, CANVAS_EMPTY_IMAGE_LABEL_KEY, CANVAS_ROOT_RESET_CSS, COMPOSITE_PRESETS, CanvasDesignStateService, CanvasShowState, CardComponentConfig, ChildLayoutGroupStyleConfig, ComponentConfig, ComponentRegistryService, CompositeCanvasService, CompositePaletteGroup, CompositePresetId, DEFAULT_ESSENTIAL_TRAITS, DESCRIPTOR_MAX_LENGTH, DRAG_HANDLE_ATTR, DROP_REFUSED_CLASS, DataSourceRegistryService, DataSourceType, DialogComponentConfig, DimensionsGroupStyleConfig, DragEngineService, EASING_OPTIONS$1 as EASING_OPTIONS, EDITOR_CHROME_ATTRIBUTE, ESSENTIAL_TRAITS_BY_TYPE, EffectsGroupStyleConfig, FIELD_KIND_PRESETS, FIELD_PARTS, FORM_STATE_BLOCK_TYPES, FORM_TYPE, FieldComponentConfig, FieldGroupComponentConfig, FormComponentConfig, FormErrorComponentConfig, FormStateService, FormSuccessComponentConfig, GLOBALS_AFTER_CLASS, GalleryConfig, GlobalCanvasLayer, GroupStyleConfig, HIDDEN_AT_REST_LABEL_KEY, HIDDEN_AT_REST_ROLES, HIDDEN_AT_REST_TYPES, INSERT_REFUSAL_LABEL_KEYS, INTERACTION_STATES, INVERSE_ACTION, IOX_BUILDER_CANVAS, IOX_BUILDER_LABELS, IOX_CONTENT_TYPE_LABELS, IOX_DATA_SOURCE_ENDPOINTS, InsertRefusal, InteractionEngineService, InteractionPresetRegistryService, IoxBuilderModule, IoxDraggableDirective, IoxDropzoneDirective, LAYOUT_PRESETS, LayerTreeComponent, LayoutGroupStyleConfig, ListGroupStyleConfig, NodeAction, OverlayComponent, OverlayService, PALETTE_PRESET_DEFS, PanelChildComponent, PanelComponent, PanelEventService, PanelEventTypes, PanelTypes, PositionGroupStyleConfig, PresetRegistryService, RELATIONAL_STATES, RELATIONAL_STATE_BASE, REPEATER_SOURCE_PROP, REPEATER_TYPE, REVEALED_WHILE_SELECTED_ROLES, ROOT_ESCAPE_PX, ROUTE_ANIMATION_OPTIONS, RenderDirective, RepeaterComponentConfig, RootZone, SCREEN_WIDTH_OPTIONS, SILENT_REFUSALS, STRUCTURAL_STATES, SUPPORTED_STATES, SectionComponent, SectionComponentConfig, SelectionReveal, SizeGroupStyleConfig, SliderContainerComponent, SliderContainerConfig, SliderSlideComponent, SliderSlideConfig, SpacingGroupStyleConfig, StructureFix, StyleCategory, StyleRegistryService, TraitConfig as StyleTraitConfig, SubmitButtonComponentConfig, SymbolRegistryService, TARGETED_STATES, TARGETED_STATE_BASE, TOGGLE_PART_ROLES, TRIGGER_OPTIONS, TextBlockComponentConfig, ToolbarAction, ToolbarComponent, TraitConfig, TraitInputType, TypographyGroupStyleConfig, UNITS_ALL, UNITS_DEG, UNITS_FIXED, UNITS_NO_VW, VALIDATION_STATES, VALIDATION_STATE_CLASS, VideoConfig, ViewportService, ZOOM_OPTIONS, arrayItemSchemaFields, arraySchemaFields, auditNodes, bindingLabel, bindingLabels, buildCompositePreset, buildConsentPreset, buildElementStyleSpecimen, buildFieldKindPreset, buildFlavorSpecimen, buildFormPreset, buildFullStyleTraits, buildLayoutPreset, buildPalettePreset, buildPresetStyleTraits, builderCompositeFactory, canFluidise, canvasEmptyImageLabel, canvasHostDisplay, collapseColumnsProps, columnCountOf, compositeStructureFindings, defaultBuilderLabels, defaultPageSettings, deleteTabPair, displayTriggerOf, dropChildren, dropTargetLabel, duplicateTabPair, escapesRoot, essentialTraitsFor, findNodeLocation, findParentOf, flattenLayerTree, flattenSchemaFields, fluidiseProps, generateNodeId, globalCanvasLayer, hiddenAtBase, hiddenUntilItemOn, insertRefusal, insertRefusalLabel, interactionTargets, isHiddenAt, isHiddenAtRest, isPinnedGlobal, isSpecimen, itemsOnAtRest, markSpecimen, matchesControlSearch, nearestFormNode, neverShown, nodeAncestorPath, nodeAncestorPathInEither, nodeDescriptor, nodeStyleMap, overlayGlobals, pathToNode, refreshSubtree, regenerateNodeIds, removalRefusal, reorderProps, repeaterSourceAlias, repeaterSubLabel, resolveInsertTarget, resolveTraitControllerType, resolveTraitOptions, retypeNode, rootEscapeBand, rootZone, selectionRevealOf, setStyleTrait, setTrait, shortDescriptor, shownBeforeControl, sourceContentType, sourceTypeLabel, stackChildrenProps, summarise, syncEmptyImagePlaceholder, syncNodeDesign, syncNodeInputs, tabPairLabel, tabsRootForPart, textNode, upsertNodeStyles, visibilityProps, walkNodes, wrapNode };
|
|
13851
14115
|
//# sourceMappingURL=vectoriox-iox-builder.mjs.map
|