@vectoriox/iox-builder 10.0.2 → 10.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/vectoriox-iox-builder.mjs +1074 -804
- package/fesm2022/vectoriox-iox-builder.mjs.map +1 -1
- package/index.d.ts +81 -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, 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),
|
|
@@ -4359,7 +4828,7 @@ var CanvasShowState;
|
|
|
4359
4828
|
*/
|
|
4360
4829
|
class CanvasDesignStateService {
|
|
4361
4830
|
constructor() {
|
|
4362
|
-
this._state$ = new BehaviorSubject({ revealed: new Set(), selectedTriggers: new Set(), pinned: new Set(), shown: new Map(), items: new Map() });
|
|
4831
|
+
this._state$ = new BehaviorSubject({ revealed: new Set(), selectedTriggers: new Set(), pinned: new Set(), triggeredBySelection: new Set(), shown: new Map(), items: new Map() });
|
|
4363
4832
|
this.state$ = this._state$.asObservable();
|
|
4364
4833
|
this._pagePath$ = new BehaviorSubject(null);
|
|
4365
4834
|
/** The app path of the page on the canvas (`/work`), or null when unknown. */
|
|
@@ -4384,11 +4853,12 @@ class CanvasDesignStateService {
|
|
|
4384
4853
|
const ids = (reveal) => path.filter(n => selectionRevealOf(n) === reveal).map(n => n.id).filter((id) => !!id);
|
|
4385
4854
|
const revealed = new Set(ids(SelectionReveal.WhileSelected));
|
|
4386
4855
|
const selectedTriggers = new Set(path.filter(n => isExclusiveTrigger(n)).map(n => n.id).filter((id) => !!id));
|
|
4387
|
-
const
|
|
4856
|
+
const triggeredBySelection = new Set(path.map(displayTriggerOf).filter((id) => !!id));
|
|
4857
|
+
const { revealed: was, pinned: held, selectedTriggers: wasTriggers, triggeredBySelection: wasTriggered } = this._state$.value;
|
|
4388
4858
|
const pinned = new Set([...held, ...ids(SelectionReveal.UntilClosed)]);
|
|
4389
|
-
if (sameSet(revealed, was) && sameSet(pinned, held) && sameSet(selectedTriggers, wasTriggers))
|
|
4859
|
+
if (sameSet(revealed, was) && sameSet(pinned, held) && sameSet(selectedTriggers, wasTriggers) && sameSet(triggeredBySelection, wasTriggered))
|
|
4390
4860
|
return;
|
|
4391
|
-
this._state$.next({ ...this._state$.value, revealed, pinned, selectedTriggers });
|
|
4861
|
+
this._state$.next({ ...this._state$.value, revealed, pinned, selectedTriggers, triggeredBySelection });
|
|
4392
4862
|
}
|
|
4393
4863
|
/** Closed on the canvas (its X, its backdrop, the eye): no longer pinned or switched open. */
|
|
4394
4864
|
dismiss(nodeId) {
|
|
@@ -4438,13 +4908,17 @@ class CanvasDesignStateService {
|
|
|
4438
4908
|
const { revealed, pinned, shown } = this._state$.value;
|
|
4439
4909
|
return revealed.has(nodeId) || pinned.has(nodeId) || shown.get(nodeId) === CanvasShowState.Open;
|
|
4440
4910
|
}
|
|
4911
|
+
/** True when the canvas shows an item because the selection is styled "when it is on" (the switcher's hint). */
|
|
4912
|
+
isTriggeredBySelection(triggerId) {
|
|
4913
|
+
return !!triggerId && this._state$.value.triggeredBySelection.has(triggerId);
|
|
4914
|
+
}
|
|
4441
4915
|
/** True when the part is shown only because the selection is inside it (the switcher's hint line). */
|
|
4442
4916
|
isRevealedBySelection(nodeId) {
|
|
4443
4917
|
return !!nodeId && this._state$.value.revealed.has(nodeId);
|
|
4444
4918
|
}
|
|
4445
4919
|
/** A new page on the canvas: nothing is revealed or switched open. */
|
|
4446
4920
|
reset() {
|
|
4447
|
-
this._state$.next({ revealed: new Set(), selectedTriggers: new Set(), pinned: new Set(), shown: new Map(), items: new Map() });
|
|
4921
|
+
this._state$.next({ revealed: new Set(), selectedTriggers: new Set(), pinned: new Set(), triggeredBySelection: new Set(), shown: new Map(), items: new Map() });
|
|
4448
4922
|
}
|
|
4449
4923
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: CanvasDesignStateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
4450
4924
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.31", ngImport: i0, type: CanvasDesignStateService }); }
|
|
@@ -4497,10 +4971,13 @@ class CompositeCanvasService {
|
|
|
4497
4971
|
const cssId = node.styleId ?? node.id;
|
|
4498
4972
|
const rootId = () => compositeScopeId(cssId, compositeRowKeys(element));
|
|
4499
4973
|
if (isExclusiveRoot(node)) {
|
|
4974
|
+
const tree = node;
|
|
4500
4975
|
const runtime = this.zone.runOutsideAngular(() => new ExclusiveRuntime(element, {
|
|
4976
|
+
kind: exclusiveKindOf(node) ?? undefined,
|
|
4501
4977
|
rootId,
|
|
4502
|
-
behaviour:
|
|
4503
|
-
label: () =>
|
|
4978
|
+
behaviour: exclusiveBehaviourOf(tree),
|
|
4979
|
+
label: () => exclusiveLabelOf(tree),
|
|
4980
|
+
defaultKey: () => exclusiveDefaultOf(tree),
|
|
4504
4981
|
observe: true,
|
|
4505
4982
|
forced: () => this.forcedTab(node, element),
|
|
4506
4983
|
// The switcher follows the TEMPLATE's clicks; a preview row's copy switches on its own.
|
|
@@ -4542,13 +5019,21 @@ class CompositeCanvasService {
|
|
|
4542
5019
|
const entry = node.id ? this.attached.get(node.id) : undefined;
|
|
4543
5020
|
entry?.runtime.setBehaviour(accordionBehaviour(node));
|
|
4544
5021
|
const tabs = node.id ? this.exclusive.get(node.id) : undefined;
|
|
4545
|
-
tabs?.runtime.setBehaviour(
|
|
5022
|
+
tabs?.runtime.setBehaviour(exclusiveBehaviourOf(node));
|
|
5023
|
+
}
|
|
5024
|
+
/** A Segmented control's On by default changed (CC4b): the canvas shows the new default, like a click on it. */
|
|
5025
|
+
showDefault(node) {
|
|
5026
|
+
const runtime = node.id ? this.exclusive.get(node.id)?.runtime : undefined;
|
|
5027
|
+
const key = exclusiveDefaultOf(node);
|
|
5028
|
+
if (runtime && key)
|
|
5029
|
+
runtime.activate(key);
|
|
4546
5030
|
}
|
|
4547
5031
|
/** The runtime of an attached Accordion / Disclosure root (the e2e harness and the panels read its state). */
|
|
4548
5032
|
runtimeOf(nodeId) {
|
|
4549
5033
|
return (nodeId && this.attached.get(nodeId)?.runtime) || null;
|
|
4550
5034
|
}
|
|
4551
|
-
/** The runtime of an attached
|
|
5035
|
+
/** The runtime of an attached exclusive root — a Tabs (CC4a) or a Segmented control (CC4b): "Showing on canvas" reads
|
|
5036
|
+
* its items and the one on. */
|
|
4552
5037
|
tabsRuntimeOf(nodeId) {
|
|
4553
5038
|
return (nodeId && this.exclusive.get(nodeId)?.runtime) || null;
|
|
4554
5039
|
}
|
|
@@ -4605,13 +5090,14 @@ class CompositeCanvasService {
|
|
|
4605
5090
|
}
|
|
4606
5091
|
return forced;
|
|
4607
5092
|
}
|
|
4608
|
-
/** The
|
|
5093
|
+
/** The item forced on: the tab whose panel the selection sits in (the template's, in a preview row's copy) — or
|
|
5094
|
+
* the item the selection is styled "when it is on" by (CC4b: a price shown when Yearly is on turns Yearly on). */
|
|
4609
5095
|
forcedTab(node, root) {
|
|
4610
5096
|
const state = this.designState;
|
|
4611
5097
|
const runtime = node.id ? this.exclusive.get(node.id)?.runtime : undefined;
|
|
4612
5098
|
if (!state || !runtime)
|
|
4613
5099
|
return null;
|
|
4614
|
-
for (const id of state.snapshot.revealed) {
|
|
5100
|
+
for (const id of [...state.snapshot.revealed, ...state.snapshot.triggeredBySelection]) {
|
|
4615
5101
|
const key = runtime.keyAt(root.querySelector(`.iox-node-${CSS.escape(id)}`));
|
|
4616
5102
|
if (key)
|
|
4617
5103
|
return key;
|
|
@@ -4675,6 +5161,18 @@ const DIRECTIVE_HOST_TYPES = new Set([
|
|
|
4675
5161
|
// the engine never had it (DS2.1).
|
|
4676
5162
|
'Form', 'Field', 'FieldGroup', 'SubmitButton', 'FormSuccess', 'FormError',
|
|
4677
5163
|
]);
|
|
5164
|
+
/**
|
|
5165
|
+
* Node types whose canvas host draws NO box (CC1.1): a Dialog is its fixed stage when open and nothing when
|
|
5166
|
+
* closed (the live site's closed `<dialog>` is display: none). A block host was a full-width flex item of its
|
|
5167
|
+
* parent — the CC1 drawer inside the header took a wrapped line plus a row gap, 24px the live header never had.
|
|
5168
|
+
*/
|
|
5169
|
+
const BOXLESS_HOST_TYPES = new Set(['Dialog']);
|
|
5170
|
+
/** The inline `display` the canvas gives a node's custom-element host, or null to leave the host alone. */
|
|
5171
|
+
function canvasHostDisplay(type) {
|
|
5172
|
+
if (BOXLESS_HOST_TYPES.has(type))
|
|
5173
|
+
return 'contents';
|
|
5174
|
+
return DIRECTIVE_HOST_TYPES.has(type) ? null : 'block';
|
|
5175
|
+
}
|
|
4678
5176
|
const CONTAINER_TAGS = ['div', 'section', 'article', 'header', 'footer', 'main', 'nav', 'ul', 'ol', 'li'];
|
|
4679
5177
|
/** A Heading inside a button (an Accordion header, CC2) — phrasing content only. */
|
|
4680
5178
|
const PHRASING_HEADING_TAG = 'span';
|
|
@@ -5045,9 +5543,9 @@ class RenderDirective {
|
|
|
5045
5543
|
// properly in layout and CDK can capture its bounding rect. SKIP for directive-hosts: the
|
|
5046
5544
|
// host is a real element with its own display, and an inline style here out-ranks both
|
|
5047
5545
|
// `:host` and the styleProps `.iox-node` rule (it would e.g. force Link's <a> to block).
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5546
|
+
const hostDisplay = canvasHostDisplay(node.type);
|
|
5547
|
+
if (nativeEl && hostDisplay)
|
|
5548
|
+
nativeEl.style.display = hostDisplay;
|
|
5051
5549
|
// Preview clones (repeater/slider-generated rows) are display-only — only the
|
|
5052
5550
|
// template node is selectable/editable. Skip overlay registration + interaction
|
|
5053
5551
|
// wiring for them so they can't be hovered, selected, or edited on the canvas.
|
|
@@ -5699,10 +6197,12 @@ var CompositePresetId;
|
|
|
5699
6197
|
CompositePresetId["Accordion"] = "accordion";
|
|
5700
6198
|
CompositePresetId["Disclosure"] = "disclosure";
|
|
5701
6199
|
CompositePresetId["Tabs"] = "tabs";
|
|
6200
|
+
CompositePresetId["Segmented"] = "segmented";
|
|
5702
6201
|
CompositePresetId["Lightbox"] = "lightbox";
|
|
5703
6202
|
})(CompositePresetId || (CompositePresetId = {}));
|
|
5704
6203
|
/** Where a composite sits in the Components panel (composite plan §6d): content blocks, or the interactive
|
|
5705
|
-
* composites (the Accordion — CC2, the Disclosure — CC2.1, Tabs — CC4a
|
|
6204
|
+
* composites (the Accordion — CC2, the Disclosure — CC2.1, Tabs — CC4a, the Segmented control — CC4b; the floating
|
|
6205
|
+
* family later). */
|
|
5706
6206
|
var CompositePaletteGroup;
|
|
5707
6207
|
(function (CompositePaletteGroup) {
|
|
5708
6208
|
CompositePaletteGroup["ContentBlocks"] = "content-blocks";
|
|
@@ -5739,6 +6239,11 @@ const COMPOSITE_PRESETS = Object.freeze([
|
|
|
5739
6239
|
description: 'Tabs that switch what shows below them — three tabs, the first on. Every tab and panel is its own element.',
|
|
5740
6240
|
build: create => buildTabsRecipe(create),
|
|
5741
6241
|
},
|
|
6242
|
+
{
|
|
6243
|
+
id: CompositePresetId.Segmented, group: CompositePaletteGroup.Interactive, name: 'Segmented', icon: 'ph-thin ph-pill',
|
|
6244
|
+
description: 'One of a few, always one on — Monthly · Yearly. Style anything "when Yearly is on" to swap what shows.',
|
|
6245
|
+
build: create => buildSegmentedRecipe(create),
|
|
6246
|
+
},
|
|
5742
6247
|
{
|
|
5743
6248
|
id: CompositePresetId.Lightbox, group: CompositePaletteGroup.Interactive, name: 'Lightbox', icon: 'ph-thin ph-frame-corners',
|
|
5744
6249
|
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.',
|
|
@@ -5882,339 +6387,65 @@ function overlayGlobals(globals) {
|
|
|
5882
6387
|
else if (globalCanvasLayer(node) === GlobalCanvasLayer.Overlay)
|
|
5883
6388
|
pinnedAfter.push(node);
|
|
5884
6389
|
}
|
|
5885
|
-
return { before, pinnedAfter };
|
|
5886
|
-
}
|
|
5887
|
-
|
|
5888
|
-
/** Counts for ONE node (the selection). Breakpoints with nothing overridden are omitted. */
|
|
5889
|
-
function nodeBreakpointCounts(node) {
|
|
5890
|
-
const counts = {};
|
|
5891
|
-
if (!node?.responsive)
|
|
5892
|
-
return counts;
|
|
5893
|
-
for (const { id } of BREAKPOINTS) {
|
|
5894
|
-
if (id === Breakpoint.Base)
|
|
5895
|
-
continue;
|
|
5896
|
-
const n = countOverrides(node.responsive, id);
|
|
5897
|
-
if (n)
|
|
5898
|
-
counts[id] = n;
|
|
5899
|
-
}
|
|
5900
|
-
return counts;
|
|
5901
|
-
}
|
|
5902
|
-
/** Counts summed over whole trees (the page, when nothing is selected). */
|
|
5903
|
-
function treeBreakpointCounts(...roots) {
|
|
5904
|
-
const counts = {};
|
|
5905
|
-
const visit = (nodes) => {
|
|
5906
|
-
for (const node of nodes ?? []) {
|
|
5907
|
-
for (const [bp, n] of Object.entries(nodeBreakpointCounts(node))) {
|
|
5908
|
-
counts[bp] = (counts[bp] ?? 0) + n;
|
|
5909
|
-
}
|
|
5910
|
-
visit(node.children);
|
|
5911
|
-
}
|
|
5912
|
-
};
|
|
5913
|
-
roots.forEach(visit);
|
|
5914
|
-
return counts;
|
|
5915
|
-
}
|
|
5916
|
-
|
|
5917
|
-
/**
|
|
5918
|
-
* Resolve where "click to insert" puts a new element, from the current selection — the rule the
|
|
5919
|
-
* components panel and the canvas "+" share (builder-usability-plan.md, B2):
|
|
5920
|
-
*
|
|
5921
|
-
* nothing selected → appended to the page root
|
|
5922
|
-
* a CONTAINER selected → appended INSIDE it (its last child)
|
|
5923
|
-
* a LEAF selected → inserted AFTER it, as its next sibling
|
|
5924
|
-
*
|
|
5925
|
-
* `isContainer` decides what can hold children — the builder passes "has a registered drop zone",
|
|
5926
|
-
* the same test a drag uses, so a click can never insert somewhere a drop couldn't.
|
|
5927
|
-
* A selection not found in `roots` (stale) falls back to appending to the root.
|
|
5928
|
-
*/
|
|
5929
|
-
function resolveInsertTarget(roots, selected, isContainer) {
|
|
5930
|
-
const append = (children, parent) => ({ children, index: children.length, parent });
|
|
5931
|
-
if (!selected)
|
|
5932
|
-
return append(roots, null);
|
|
5933
|
-
const path = nodeAncestorPath(roots, selected);
|
|
5934
|
-
if (!path)
|
|
5935
|
-
return append(roots, null);
|
|
5936
|
-
if (isContainer(selected)) {
|
|
5937
|
-
if (!selected.children)
|
|
5938
|
-
selected.children = [];
|
|
5939
|
-
return append(selected.children, selected);
|
|
5940
|
-
}
|
|
5941
|
-
const parent = path.length >= 2 ? path[path.length - 2] : null;
|
|
5942
|
-
const siblings = parent ? (parent.children ?? (parent.children = [])) : roots;
|
|
5943
|
-
return { children: siblings, index: siblings.indexOf(selected) + 1, parent };
|
|
5944
|
-
}
|
|
5945
|
-
|
|
5946
|
-
/**
|
|
5947
|
-
* STRUCTURE OPS (A4) — the one-click layout changes people actually make at a narrow width, as pure
|
|
5948
|
-
* "what to write" functions. Each returns a flat style map the caller stores in the node's
|
|
5949
|
-
* `responsive[bp]` layer (or in the base styles at Base) through the shared `setOverride`.
|
|
5950
|
-
*
|
|
5951
|
-
* All of it is ordinary CSS the SSR engine already renders — there is no second tree and no
|
|
5952
|
-
* per-width HTML. See responsive-breakpoints-plan.md (D5).
|
|
5953
|
-
*/
|
|
5954
|
-
/** CSS lengths that are a FIXED pixel size — the ones that stop a layout fitting a phone. */
|
|
5955
|
-
const PX_LENGTH = /^-?\d+(\.\d+)?px$/i;
|
|
5956
|
-
/**
|
|
5957
|
-
* Hide / show at a width. Hiding is `display: none` INSIDE that breakpoint's rules — the node stays
|
|
5958
|
-
* in the DOM and in the SSR output (one HTML document serves every width), so hidden content still
|
|
5959
|
-
* ships bytes and is still indexed. Showing again clears the override rather than guessing a value.
|
|
5960
|
-
*
|
|
5961
|
-
* @returns the props to write, or `null` meaning "clear the display override at this breakpoint".
|
|
5962
|
-
*/
|
|
5963
|
-
function visibilityProps(hidden) {
|
|
5964
|
-
return hidden ? { display: 'none' } : null;
|
|
5965
|
-
}
|
|
5966
|
-
/** True when this node is hidden by the resolved styles at some width. */
|
|
5967
|
-
function isHiddenAt(resolvedStyleProps) {
|
|
5968
|
-
return String(resolvedStyleProps?.['display'] ?? '') === 'none';
|
|
5969
|
-
}
|
|
5970
|
-
/**
|
|
5971
|
-
* Stack a container's children in one column — the most common mobile edit, and four separate
|
|
5972
|
-
* controls today (display, direction, wrap, columns). A grid becomes a single-column grid; anything
|
|
5973
|
-
* else becomes a column flex box.
|
|
5974
|
-
*/
|
|
5975
|
-
function stackChildrenProps(resolvedStyleProps) {
|
|
5976
|
-
const display = String(resolvedStyleProps?.['display'] ?? '');
|
|
5977
|
-
if (display === 'grid' || display === 'inline-grid') {
|
|
5978
|
-
return { gridTemplateColumns: 'repeat(1, minmax(0, 1fr))' };
|
|
5979
|
-
}
|
|
5980
|
-
return { display: 'flex', flexDirection: 'column', flexWrap: 'nowrap' };
|
|
5981
|
-
}
|
|
5982
|
-
/**
|
|
5983
|
-
* Collapse a grid to `count` columns (a flex row is stacked instead — it has no column track).
|
|
5984
|
-
* `count` is clamped to at least 1.
|
|
5985
|
-
*/
|
|
5986
|
-
function collapseColumnsProps(resolvedStyleProps, count) {
|
|
5987
|
-
const columns = Math.max(1, Math.floor(count));
|
|
5988
|
-
const display = String(resolvedStyleProps?.['display'] ?? '');
|
|
5989
|
-
if (display === 'grid' || display === 'inline-grid') {
|
|
5990
|
-
return { gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` };
|
|
5991
|
-
}
|
|
5992
|
-
return columns === 1 ? stackChildrenProps(resolvedStyleProps) : { flexWrap: 'wrap' };
|
|
5993
|
-
}
|
|
5994
|
-
/** How many equal columns a `repeat(N, …)` track list describes, or null when it isn't one. */
|
|
5995
|
-
function columnCountOf(resolvedStyleProps) {
|
|
5996
|
-
const match = /^repeat\(\s*(\d+)\s*,/.exec(String(resolvedStyleProps?.['gridTemplateColumns'] ?? ''));
|
|
5997
|
-
return match ? Number(match[1]) : null;
|
|
5998
|
-
}
|
|
5999
|
-
/**
|
|
6000
|
-
* Make fixed pixel sizes fluid: `width: 520px` → `width: 100%` with the original kept as
|
|
6001
|
-
* `max-width`, so the element still never exceeds its designed size on a wide screen. `minWidth` in
|
|
6002
|
-
* px is dropped to 0 — it is what actually forces horizontal scrolling on a phone.
|
|
6003
|
-
*
|
|
6004
|
-
* @returns the props to write; EMPTY when there is nothing fixed to fluidise.
|
|
6005
|
-
*/
|
|
6006
|
-
function fluidiseProps(resolvedStyleProps) {
|
|
6007
|
-
const out = {};
|
|
6008
|
-
const width = String(resolvedStyleProps?.['width'] ?? '');
|
|
6009
|
-
const minWidth = String(resolvedStyleProps?.['minWidth'] ?? '');
|
|
6010
|
-
if (PX_LENGTH.test(width)) {
|
|
6011
|
-
out['width'] = '100%';
|
|
6012
|
-
const maxWidth = String(resolvedStyleProps?.['maxWidth'] ?? '');
|
|
6013
|
-
if (!maxWidth || maxWidth === 'none')
|
|
6014
|
-
out['maxWidth'] = width;
|
|
6015
|
-
}
|
|
6016
|
-
if (PX_LENGTH.test(minWidth) && parseFloat(minWidth) > 0)
|
|
6017
|
-
out['minWidth'] = '0px';
|
|
6018
|
-
return out;
|
|
6390
|
+
return { before, pinnedAfter };
|
|
6019
6391
|
}
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6392
|
+
|
|
6393
|
+
/** Counts for ONE node (the selection). Breakpoints with nothing overridden are omitted. */
|
|
6394
|
+
function nodeBreakpointCounts(node) {
|
|
6395
|
+
const counts = {};
|
|
6396
|
+
if (!node?.responsive)
|
|
6397
|
+
return counts;
|
|
6398
|
+
for (const { id } of BREAKPOINTS) {
|
|
6399
|
+
if (id === Breakpoint.Base)
|
|
6400
|
+
continue;
|
|
6401
|
+
const n = countOverrides(node.responsive, id);
|
|
6402
|
+
if (n)
|
|
6403
|
+
counts[id] = n;
|
|
6404
|
+
}
|
|
6405
|
+
return counts;
|
|
6023
6406
|
}
|
|
6024
|
-
/**
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
const none = Array.from({ length: Math.max(0, count) }, () => null);
|
|
6038
|
-
if (from < 0 || from >= count || to < 0 || to >= count || from === to)
|
|
6039
|
-
return none;
|
|
6040
|
-
// Visual order after the move, as a list of CHILD indices.
|
|
6041
|
-
const sequence = Array.from({ length: count }, (_, i) => i);
|
|
6042
|
-
sequence.splice(to, 0, ...sequence.splice(from, 1));
|
|
6043
|
-
const orderByChild = none.slice();
|
|
6044
|
-
sequence.forEach((childIndex, position) => { orderByChild[childIndex] = position; });
|
|
6045
|
-
return orderByChild;
|
|
6407
|
+
/** Counts summed over whole trees (the page, when nothing is selected). */
|
|
6408
|
+
function treeBreakpointCounts(...roots) {
|
|
6409
|
+
const counts = {};
|
|
6410
|
+
const visit = (nodes) => {
|
|
6411
|
+
for (const node of nodes ?? []) {
|
|
6412
|
+
for (const [bp, n] of Object.entries(nodeBreakpointCounts(node))) {
|
|
6413
|
+
counts[bp] = (counts[bp] ?? 0) + n;
|
|
6414
|
+
}
|
|
6415
|
+
visit(node.children);
|
|
6416
|
+
}
|
|
6417
|
+
};
|
|
6418
|
+
roots.forEach(visit);
|
|
6419
|
+
return counts;
|
|
6046
6420
|
}
|
|
6047
6421
|
|
|
6048
6422
|
/**
|
|
6049
|
-
*
|
|
6050
|
-
*
|
|
6423
|
+
* Resolve where "click to insert" puts a new element, from the current selection — the rule the
|
|
6424
|
+
* components panel and the canvas "+" share (builder-usability-plan.md, B2):
|
|
6051
6425
|
*
|
|
6052
|
-
*
|
|
6053
|
-
*
|
|
6054
|
-
*
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
|
|
6059
|
-
AuditCode["FixedWidth"] = "fixed-width";
|
|
6060
|
-
AuditCode["TapTarget"] = "tap-target";
|
|
6061
|
-
AuditCode["TinyText"] = "tiny-text";
|
|
6062
|
-
AuditCode["UnboundedImage"] = "unbounded-image";
|
|
6063
|
-
/** A slider showing more slides than the width can carry — each one unreadably narrow. */
|
|
6064
|
-
AuditCode["SlidesOverflow"] = "slides-overflow";
|
|
6065
|
-
/** An out-of-flow node whose box lands outside the frame, so it is unreachable at this width. */
|
|
6066
|
-
AuditCode["OffscreenAbsolute"] = "offscreen-absolute";
|
|
6067
|
-
/** Form content inside a link or a button — a STRUCTURE finding, true at every width (form-nesting-audit.util). */
|
|
6068
|
-
AuditCode["FormInLink"] = "form-in-link";
|
|
6069
|
-
/** An icon-only button or link with no Accessible name — announced as an unnamed control (CC1 N6). */
|
|
6070
|
-
AuditCode["UnnamedControl"] = "unnamed-control";
|
|
6071
|
-
/** A composite whose items come from content, and its source returned no rows (CC2, board 9): the
|
|
6072
|
-
* canvas shows the placeholder, the live site renders nothing. */
|
|
6073
|
-
AuditCode["EmptySource"] = "empty-source";
|
|
6074
|
-
})(AuditCode || (AuditCode = {}));
|
|
6075
|
-
/** A fix that changes the tree, not a style or a trait — applied as one page-history step. */
|
|
6076
|
-
var StructureFix;
|
|
6077
|
-
(function (StructureFix) {
|
|
6078
|
-
/** Move the form out of the link or button holding it, to right after it. */
|
|
6079
|
-
StructureFix["MoveFormOut"] = "move-form-out";
|
|
6080
|
-
})(StructureFix || (StructureFix = {}));
|
|
6081
|
-
var AuditSeverity;
|
|
6082
|
-
(function (AuditSeverity) {
|
|
6083
|
-
AuditSeverity["Error"] = "error";
|
|
6084
|
-
AuditSeverity["Warning"] = "warning";
|
|
6085
|
-
AuditSeverity["Info"] = "info";
|
|
6086
|
-
})(AuditSeverity || (AuditSeverity = {}));
|
|
6087
|
-
/** WCAG 2.5.8 minimum target size. */
|
|
6088
|
-
const MIN_TAP_PX = 44;
|
|
6089
|
-
/** Below this, iOS Safari zooms the page when an adjacent input is focused. */
|
|
6090
|
-
const MIN_TEXT_PX = 12;
|
|
6091
|
-
/** Types a visitor taps. */
|
|
6092
|
-
const TAPPABLE = new Set(['Button', 'ButtonBlock', 'Link', 'LinkedContainer']);
|
|
6093
|
-
/** Narrower than this, a slide shows too little of its content to be worth showing at all. */
|
|
6094
|
-
const MIN_SLIDE_PX = 200;
|
|
6095
|
-
/** Out-of-flow positioning that can put a node outside the frame. */
|
|
6096
|
-
const OUT_OF_FLOW = new Set(['absolute', 'fixed']);
|
|
6097
|
-
const nameOf = (node) => node.label || node.type;
|
|
6098
|
-
const pxOf = (value) => {
|
|
6099
|
-
const match = /^(-?\d+(?:\.\d+)?)px$/i.exec(String(value ?? ''));
|
|
6100
|
-
return match ? Number(match[1]) : null;
|
|
6101
|
-
};
|
|
6102
|
-
/**
|
|
6103
|
-
* Audit nodes laid out at `viewportWidth`. Returns findings ordered by severity (errors first),
|
|
6104
|
-
* then by the order the nodes were given — so the worst thing is at the top of the list.
|
|
6426
|
+
* nothing selected → appended to the page root
|
|
6427
|
+
* a CONTAINER selected → appended INSIDE it (its last child)
|
|
6428
|
+
* a LEAF selected → inserted AFTER it, as its next sibling
|
|
6429
|
+
*
|
|
6430
|
+
* `isContainer` decides what can hold children — the builder passes "has a registered drop zone",
|
|
6431
|
+
* the same test a drag uses, so a click can never insert somewhere a drop couldn't.
|
|
6432
|
+
* A selection not found in `roots` (stale) falls back to appending to the root.
|
|
6105
6433
|
*/
|
|
6106
|
-
function
|
|
6107
|
-
const
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
if (
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
message: `${name} is ${Math.round(node.width)}px wide in a ${viewportWidth}px viewport — the page scrolls sideways.`,
|
|
6118
|
-
fix: canFluidise(styles) ? fluidiseProps(styles) : { maxWidth: '100%' },
|
|
6119
|
-
});
|
|
6120
|
-
}
|
|
6121
|
-
else if (canFluidise(styles)) {
|
|
6122
|
-
// 2. A fixed px size that still fits, but will not on a narrower phone.
|
|
6123
|
-
const width = pxOf(styles['width']);
|
|
6124
|
-
findings.push({
|
|
6125
|
-
nodeId: node.id, nodeName: name, code: AuditCode.FixedWidth, severity: AuditSeverity.Warning,
|
|
6126
|
-
message: width !== null
|
|
6127
|
-
? `${name} has a fixed width of ${width}px — it cannot shrink on a narrower screen.`
|
|
6128
|
-
: `${name} has a fixed minimum width — it forces sideways scrolling below that size.`,
|
|
6129
|
-
fix: fluidiseProps(styles),
|
|
6130
|
-
});
|
|
6131
|
-
}
|
|
6132
|
-
// 3. Tap targets below the WCAG minimum.
|
|
6133
|
-
if (TAPPABLE.has(node.type) && node.width > 0 && node.height > 0
|
|
6134
|
-
&& (node.width < MIN_TAP_PX || node.height < MIN_TAP_PX)) {
|
|
6135
|
-
findings.push({
|
|
6136
|
-
nodeId: node.id, nodeName: name, code: AuditCode.TapTarget, severity: AuditSeverity.Warning,
|
|
6137
|
-
message: `${name} is ${Math.round(node.width)}×${Math.round(node.height)}px — below the ${MIN_TAP_PX}px minimum tap target.`,
|
|
6138
|
-
fix: { minHeight: `${MIN_TAP_PX}px`, minWidth: `${MIN_TAP_PX}px` },
|
|
6139
|
-
});
|
|
6140
|
-
}
|
|
6141
|
-
// 4. Text too small to read (and small enough to trigger iOS zoom).
|
|
6142
|
-
const fontSize = pxOf(styles['fontSize']);
|
|
6143
|
-
if (fontSize !== null && fontSize < MIN_TEXT_PX) {
|
|
6144
|
-
findings.push({
|
|
6145
|
-
nodeId: node.id, nodeName: name, code: AuditCode.TinyText, severity: AuditSeverity.Warning,
|
|
6146
|
-
message: `${name} renders at ${fontSize}px — under the ${MIN_TEXT_PX}px readable minimum.`,
|
|
6147
|
-
fix: { fontSize: `${MIN_TEXT_PX}px` },
|
|
6148
|
-
});
|
|
6149
|
-
}
|
|
6150
|
-
// 5. An image with a fixed height and no ratio crops unpredictably as the width changes.
|
|
6151
|
-
if (node.type === 'Image' && pxOf(styles['height']) !== null
|
|
6152
|
-
&& !String(styles['aspectRatio'] ?? '').trim().replace(/^auto$/, '')) {
|
|
6153
|
-
findings.push({
|
|
6154
|
-
nodeId: node.id, nodeName: name, code: AuditCode.UnboundedImage, severity: AuditSeverity.Info,
|
|
6155
|
-
message: `${name} has a fixed height and no aspect ratio — it crops unpredictably as the width changes.`,
|
|
6156
|
-
fix: { height: 'auto', aspectRatio: '16 / 9' },
|
|
6157
|
-
});
|
|
6158
|
-
}
|
|
6159
|
-
// 6. A slider showing more slides than this width can carry. NOT a CSS problem: slide count
|
|
6160
|
-
// is a component trait, so the fix is a per-breakpoint TRAIT override (P3-G1), not a style.
|
|
6161
|
-
if (node.type === 'SliderContainer' && node.width > 0) {
|
|
6162
|
-
const perView = Number(node.inputs?.['slidesPerView']);
|
|
6163
|
-
if (Number.isFinite(perView) && perView > 1) {
|
|
6164
|
-
const slideWidth = node.width / perView;
|
|
6165
|
-
if (slideWidth < MIN_SLIDE_PX) {
|
|
6166
|
-
const fits = Math.max(1, Math.floor(node.width / MIN_SLIDE_PX));
|
|
6167
|
-
findings.push({
|
|
6168
|
-
nodeId: node.id, nodeName: name, code: AuditCode.SlidesOverflow, severity: AuditSeverity.Warning,
|
|
6169
|
-
message: `${name} shows ${perView} slides across ${Math.round(node.width)}px — ${Math.round(slideWidth)}px each. Show ${fits} at this width.`,
|
|
6170
|
-
inputFix: { slidesPerView: fits },
|
|
6171
|
-
});
|
|
6172
|
-
}
|
|
6173
|
-
}
|
|
6174
|
-
}
|
|
6175
|
-
// 7. An icon-only control with no name (WCAG 4.1.2): a screen reader says "button" and nothing
|
|
6176
|
-
// else. No auto-fix — only the author knows what it does; the finding selects it.
|
|
6177
|
-
if (node.unnamedControl) {
|
|
6178
|
-
findings.push({
|
|
6179
|
-
nodeId: node.id, nodeName: name, code: AuditCode.UnnamedControl, severity: AuditSeverity.Error,
|
|
6180
|
-
message: `${name} shows only an icon and has no accessible name — screen readers announce it as an unnamed control. Name it in Bindings ▸ Accessibility.`,
|
|
6181
|
-
});
|
|
6182
|
-
}
|
|
6183
|
-
// 7b. A composite from content whose source has no rows (CC2 R3): the visitor sees nothing there.
|
|
6184
|
-
if (node.emptySource) {
|
|
6185
|
-
findings.push({
|
|
6186
|
-
nodeId: node.id, nodeName: name, code: AuditCode.EmptySource, severity: AuditSeverity.Warning,
|
|
6187
|
-
message: `${name} in ${node.emptySource.section}: its source ${node.emptySource.alias} has no rows.`,
|
|
6188
|
-
});
|
|
6189
|
-
}
|
|
6190
|
-
// 8. An out-of-flow node that has left the frame. Measured, not inferred: a negative offset
|
|
6191
|
-
// is legitimate design until it actually puts the content beyond the edge.
|
|
6192
|
-
if (OUT_OF_FLOW.has(String(styles['position'] ?? '')) && node.left !== undefined && node.width > 0) {
|
|
6193
|
-
const right = node.left + node.width;
|
|
6194
|
-
const offLeft = right <= 0.5;
|
|
6195
|
-
const offRight = node.left >= viewportWidth - 0.5;
|
|
6196
|
-
if (offLeft || offRight) {
|
|
6197
|
-
findings.push({
|
|
6198
|
-
nodeId: node.id, nodeName: name, code: AuditCode.OffscreenAbsolute, severity: AuditSeverity.Error,
|
|
6199
|
-
message: `${name} is positioned ${offLeft ? 'left of' : 'beyond'} the ${viewportWidth}px viewport — it cannot be seen or tapped at this width.`,
|
|
6200
|
-
// No blind auto-fix: the author's intent (a decorative overflow vs a mistake)
|
|
6201
|
-
// is not knowable from geometry. Selecting the node is the useful action.
|
|
6202
|
-
});
|
|
6203
|
-
}
|
|
6204
|
-
}
|
|
6434
|
+
function resolveInsertTarget(roots, selected, isContainer) {
|
|
6435
|
+
const append = (children, parent) => ({ children, index: children.length, parent });
|
|
6436
|
+
if (!selected)
|
|
6437
|
+
return append(roots, null);
|
|
6438
|
+
const path = nodeAncestorPath(roots, selected);
|
|
6439
|
+
if (!path)
|
|
6440
|
+
return append(roots, null);
|
|
6441
|
+
if (isContainer(selected)) {
|
|
6442
|
+
if (!selected.children)
|
|
6443
|
+
selected.children = [];
|
|
6444
|
+
return append(selected.children, selected);
|
|
6205
6445
|
}
|
|
6206
|
-
const
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
.sort((a, b) => rank[a.finding.severity] - rank[b.finding.severity] || a.index - b.index)
|
|
6210
|
-
.map(entry => entry.finding);
|
|
6211
|
-
}
|
|
6212
|
-
/** Counts per severity — the per-breakpoint summary strip. */
|
|
6213
|
-
function summarise(findings) {
|
|
6214
|
-
const out = { [AuditSeverity.Error]: 0, [AuditSeverity.Warning]: 0, [AuditSeverity.Info]: 0 };
|
|
6215
|
-
for (const finding of findings)
|
|
6216
|
-
out[finding.severity]++;
|
|
6217
|
-
return out;
|
|
6446
|
+
const parent = path.length >= 2 ? path[path.length - 2] : null;
|
|
6447
|
+
const siblings = parent ? (parent.children ?? (parent.children = [])) : roots;
|
|
6448
|
+
return { children: siblings, index: siblings.indexOf(selected) + 1, parent };
|
|
6218
6449
|
}
|
|
6219
6450
|
|
|
6220
6451
|
const FINDING_KEY = 'builder.chrome.auditFormInLink';
|
|
@@ -7146,10 +7377,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
|
|
|
7146
7377
|
* An accordion's and a disclosure's parts are named by the part alone (`trigger`, "Accordion · trigger", board 3);
|
|
7147
7378
|
* a card keeps its full role (`card.title`, "Card · card.title", board 8).
|
|
7148
7379
|
*/
|
|
7149
|
-
const SHORT_ROLE_KINDS = new Set([CompositeKind.Accordion, CompositeKind.Disclosure, CompositeKind.Tabs]);
|
|
7380
|
+
const SHORT_ROLE_KINDS = new Set([CompositeKind.Accordion, CompositeKind.Disclosure, CompositeKind.Tabs, CompositeKind.Segmented]);
|
|
7150
7381
|
/** A composite kind's glyph (Phosphor, weight-less): the badge, the root's crumb and its tree row carry it
|
|
7151
7382
|
* (board 3: "the composite root carrying its kind icon"). A card keeps the generic stack glyph. */
|
|
7152
|
-
const KIND_GLYPHS = {
|
|
7383
|
+
const KIND_GLYPHS = {
|
|
7384
|
+
[CompositeKind.Accordion]: 'ph-rows', [CompositeKind.Disclosure]: 'ph-caret-circle-down', [CompositeKind.Tabs]: 'ph-tabs', [CompositeKind.Segmented]: 'ph-pill',
|
|
7385
|
+
slider: 'ph-slideshow',
|
|
7386
|
+
};
|
|
7153
7387
|
const DEFAULT_KIND_GLYPH = 'ph-stack';
|
|
7154
7388
|
/** The tree icon of an accordion's structural parts (board 3): rows on the root, a stack on an item and a
|
|
7155
7389
|
* panel, a cursor on the header, a sparkle on the icon. Other parts keep their type's icon. */
|
|
@@ -7171,6 +7405,10 @@ const PART_ICONS = {
|
|
|
7171
7405
|
[TabsRole.Trigger]: 'ph-thin ph-cursor',
|
|
7172
7406
|
[TabsRole.Panels]: 'ph-thin ph-stack',
|
|
7173
7407
|
[TabsRole.Panel]: 'ph-thin ph-stack',
|
|
7408
|
+
// CC4b — the Segmented control (board 10): its pill glyph on the root, a cursor on an option, a seal on its badge.
|
|
7409
|
+
[SegmentedRole.Segmented]: 'ph-thin ph-pill',
|
|
7410
|
+
[SegmentedRole.Option]: 'ph-thin ph-cursor',
|
|
7411
|
+
[SegmentedRole.Badge]: 'ph-thin ph-seal',
|
|
7174
7412
|
// CC3 — a Slider's controls (board 6): a cursor on the buttons, six dots on the pagination.
|
|
7175
7413
|
[SliderRole.Pause]: 'ph-thin ph-cursor',
|
|
7176
7414
|
[SliderRole.Prev]: 'ph-thin ph-cursor',
|
|
@@ -7206,6 +7444,8 @@ function compositePartSub(node, ancestors = []) {
|
|
|
7206
7444
|
return accordionItemLabel(node) || null;
|
|
7207
7445
|
if (node.role === TabsRole.Trigger)
|
|
7208
7446
|
return tabLabel(node) || null;
|
|
7447
|
+
if (node.role === SegmentedRole.Option)
|
|
7448
|
+
return segmentedOptionLabel(node) || null;
|
|
7209
7449
|
if (node.role === TabsRole.Panel) {
|
|
7210
7450
|
const root = exclusiveRootOf(node, ancestors);
|
|
7211
7451
|
const tab = root ? tabTriggerOf(root, node) : undefined;
|
|
@@ -7269,7 +7509,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.31", ngImpo
|
|
|
7269
7509
|
const FROM_ROW_KEY = 'builder.chrome.fromRow';
|
|
7270
7510
|
const ADD_ITEM_KEY = 'builder.chrome.addItem';
|
|
7271
7511
|
/** The kind label a composite part's selection badge shows (Card v2: "Card · card.title"). */
|
|
7272
|
-
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' };
|
|
7512
|
+
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' };
|
|
7273
7513
|
const VIDEO_TYPE = 'Video';
|
|
7274
7514
|
const VIDEO_KIND_KEY = 'builder.chrome.kindVideo';
|
|
7275
7515
|
const VIDEO_POSTER_ONLY_KEY = 'builder.chrome.videoPosterOnly';
|
|
@@ -7278,9 +7518,12 @@ const VIDEO_FILE_KEY = 'builder.chrome.videoFile';
|
|
|
7278
7518
|
const ADD_ITEM_ROLES = new Set([
|
|
7279
7519
|
AccordionRole.Accordion, AccordionRole.Item, AccordionRole.Trigger, AccordionRole.Panel,
|
|
7280
7520
|
TabsRole.Tabs, TabsRole.List, TabsRole.Trigger, TabsRole.Panels, TabsRole.Panel,
|
|
7521
|
+
SegmentedRole.Segmented, SegmentedRole.Option,
|
|
7281
7522
|
]);
|
|
7282
7523
|
/** Tabs say "Add tab" (board 3) — the button adds a tab AND its panel. */
|
|
7283
7524
|
const ADD_TAB_KEY = 'builder.chrome.addTab';
|
|
7525
|
+
/** A Segmented control says "Add option" (board 10). */
|
|
7526
|
+
const ADD_OPTION_KEY = 'builder.chrome.addOption';
|
|
7284
7527
|
class OverlayComponent {
|
|
7285
7528
|
constructor(elRef, instanceRegistry, overlayService, viewportService, presetRegistry, symbolRegistry, interactionPresetRegistry, interactionEngine, clipboard, styleRegistry, panelEventService, cdr, dragEngine, labels) {
|
|
7286
7529
|
this.elRef = elRef;
|
|
@@ -7531,7 +7774,8 @@ class OverlayComponent {
|
|
|
7531
7774
|
return ADD_ITEM_ROLES.has(this.selectEntry?.node?.role ?? '');
|
|
7532
7775
|
}
|
|
7533
7776
|
get addItemLabel() {
|
|
7534
|
-
const
|
|
7777
|
+
const node = this.selectEntry?.node;
|
|
7778
|
+
const key = isTabsPart(node) ? ADD_TAB_KEY : isSegmentedPart(node) ? ADD_OPTION_KEY : ADD_ITEM_KEY;
|
|
7535
7779
|
return this.builderLabels(key, BUILDER_CHROME_EN[key]);
|
|
7536
7780
|
}
|
|
7537
7781
|
onAddItem(event) {
|
|
@@ -8138,7 +8382,7 @@ function walk(nodes, depth, out, ctx, inTemplate, templateOf, ancestors) {
|
|
|
8138
8382
|
node,
|
|
8139
8383
|
icon: bound ? BOUND_ICON : (compositePartIcon(node.role) || ctx.iconOf(node.type) || DEFAULT_ICON),
|
|
8140
8384
|
label,
|
|
8141
|
-
hiddenAtRest: isHiddenAtRest(node),
|
|
8385
|
+
hiddenAtRest: isHiddenAtRest(node) || (!!ctx.onAtRest && hiddenUntilItemOn(node, ctx.onAtRest)),
|
|
8142
8386
|
depth,
|
|
8143
8387
|
expanded,
|
|
8144
8388
|
hasChildren,
|
|
@@ -8381,6 +8625,7 @@ class LayerTreeComponent {
|
|
|
8381
8625
|
.replace('{source}', sourceTypeLabel(rows.alias, this.dataSources?.findByAlias(rows.alias), this.typeLabels))
|
|
8382
8626
|
.replace('{count}', String(rows.count))
|
|
8383
8627
|
: null,
|
|
8628
|
+
onAtRest: itemsOnAtRest([...(this.layout ?? []), ...(this.globalElements ?? [])]),
|
|
8384
8629
|
};
|
|
8385
8630
|
this.flatTree = flattenLayerTree(this.layout, ctx);
|
|
8386
8631
|
this.flatGlobalTree = flattenLayerTree(this.globalElements, ctx);
|
|
@@ -8694,7 +8939,10 @@ class BuilderComponent {
|
|
|
8694
8939
|
return out;
|
|
8695
8940
|
}
|
|
8696
8941
|
refreshStructureFindings() {
|
|
8697
|
-
this.structureFindings =
|
|
8942
|
+
this.structureFindings = [
|
|
8943
|
+
...formNestingFindings(this.layout, this.globalElements, this.labels),
|
|
8944
|
+
...compositeStructureFindings(this.layout, this.globalElements, this.labels),
|
|
8945
|
+
];
|
|
8698
8946
|
}
|
|
8699
8947
|
/** A structure fix moves nodes on the page: ONE layout change, so ONE undo step. */
|
|
8700
8948
|
applyStructureFix(finding) {
|
|
@@ -9684,6 +9932,9 @@ class BuilderComponent {
|
|
|
9684
9932
|
else if (action === NodeAction.Duplicate) {
|
|
9685
9933
|
if (this.duplicateTabPairOf(node))
|
|
9686
9934
|
return;
|
|
9935
|
+
// The same acceptance rule as the toolbar's Duplicate (a fifth Segmented option is refused, CC4b).
|
|
9936
|
+
if (this.dragEngine.refuseInsert(node, this.siblingAncestors(node)))
|
|
9937
|
+
return;
|
|
9687
9938
|
const clone = this.deepCloneWithNewIds(node);
|
|
9688
9939
|
result.parent.splice(result.index + 1, 0, clone);
|
|
9689
9940
|
this.cdr.detectChanges();
|
|
@@ -9944,6 +10195,8 @@ class BuilderComponent {
|
|
|
9944
10195
|
const root = exclusiveRootOf(node, path);
|
|
9945
10196
|
if (!root)
|
|
9946
10197
|
return false;
|
|
10198
|
+
if (isSegmentedRoot(root))
|
|
10199
|
+
return this.addSegmentedOptionTo(root, path);
|
|
9947
10200
|
if (isTabsFromContent(root))
|
|
9948
10201
|
return true;
|
|
9949
10202
|
const anchor = path.find(n => isExclusiveItemPart(n)) ?? null;
|
|
@@ -9955,6 +10208,23 @@ class BuilderComponent {
|
|
|
9955
10208
|
this.panelEventService.emit(PanelEventTypes.SELECT_NODE, { node: pair.tab });
|
|
9956
10209
|
return true;
|
|
9957
10210
|
}
|
|
10211
|
+
/** Toolbar ▸ Add option on a Segmented part (CC4b): a fresh option after the selection's own option (or at the end
|
|
10212
|
+
* for the root) — ONE undo step, then it is selected. A fifth is refused, said in a toast (Q40). */
|
|
10213
|
+
addSegmentedOptionTo(root, path) {
|
|
10214
|
+
const tree = root;
|
|
10215
|
+
if (segmentedAddRefusal(tree)) {
|
|
10216
|
+
this.dragEngine.insertRefused$.next({ reason: InsertRefusal.SegmentedMaxOptions, message: insertRefusalLabel(InsertRefusal.SegmentedMaxOptions, this.labels) });
|
|
10217
|
+
return true;
|
|
10218
|
+
}
|
|
10219
|
+
const anchor = path.find(n => n.role === SegmentedRole.Option) ?? null;
|
|
10220
|
+
const factory = builderCompositeFactory(t => this.registry.createConfig(t), t => this.registry.baselineStyleProps(t));
|
|
10221
|
+
const option = addSegmentedOption(tree, factory, anchor);
|
|
10222
|
+
if (!option)
|
|
10223
|
+
return true;
|
|
10224
|
+
this.panelEventService.emit(PanelEventTypes.NODE_STRUCTURE_CHANGED, { node: root });
|
|
10225
|
+
this.panelEventService.emit(PanelEventTypes.SELECT_NODE, { node: option });
|
|
10226
|
+
return true;
|
|
10227
|
+
}
|
|
9958
10228
|
/** Duplicate on a tab or a panel (toolbar, layer tree): the PAIR is duplicated under one new key. True when handled. */
|
|
9959
10229
|
duplicateTabPairOf(node) {
|
|
9960
10230
|
const root = tabsRootForPart(this.layout, this.globalElements, node);
|
|
@@ -13835,5 +14105,5 @@ class BuilderSpacerComponentConfig extends ComponentConfig {
|
|
|
13835
14105
|
* Generated bundle index. Do not edit.
|
|
13836
14106
|
*/
|
|
13837
14107
|
|
|
13838
|
-
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, 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 };
|
|
14108
|
+
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 };
|
|
13839
14109
|
//# sourceMappingURL=vectoriox-iox-builder.mjs.map
|