@base44-preview/vite-plugin 0.2.22-pr.36.69a0b76 → 0.2.22-pr.36.6dec368
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/dist/injections/layer-dropdown/consts.d.ts +5 -1
- package/dist/injections/layer-dropdown/consts.d.ts.map +1 -1
- package/dist/injections/layer-dropdown/consts.js +5 -1
- package/dist/injections/layer-dropdown/consts.js.map +1 -1
- package/dist/injections/layer-dropdown/controller.d.ts +2 -2
- package/dist/injections/layer-dropdown/controller.d.ts.map +1 -1
- package/dist/injections/layer-dropdown/controller.js +37 -34
- package/dist/injections/layer-dropdown/controller.js.map +1 -1
- package/dist/injections/layer-dropdown/dropdown-ui.d.ts +3 -3
- package/dist/injections/layer-dropdown/dropdown-ui.d.ts.map +1 -1
- package/dist/injections/layer-dropdown/dropdown-ui.js +31 -17
- package/dist/injections/layer-dropdown/dropdown-ui.js.map +1 -1
- package/dist/injections/layer-dropdown/types.d.ts +6 -1
- package/dist/injections/layer-dropdown/types.d.ts.map +1 -1
- package/dist/injections/layer-dropdown/utils.d.ts +4 -2
- package/dist/injections/layer-dropdown/utils.d.ts.map +1 -1
- package/dist/injections/layer-dropdown/utils.js +75 -57
- package/dist/injections/layer-dropdown/utils.js.map +1 -1
- package/dist/injections/utils.d.ts +0 -2
- package/dist/injections/utils.d.ts.map +1 -1
- package/dist/injections/utils.js +0 -13
- package/dist/injections/utils.js.map +1 -1
- package/dist/injections/visual-edit-agent.d.ts.map +1 -1
- package/dist/injections/visual-edit-agent.js +8 -7
- package/dist/injections/visual-edit-agent.js.map +1 -1
- package/dist/statics/index.mjs +1 -1
- package/dist/statics/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/injections/layer-dropdown/LAYERS.md +258 -0
- package/src/injections/layer-dropdown/consts.ts +6 -1
- package/src/injections/layer-dropdown/controller.ts +40 -36
- package/src/injections/layer-dropdown/dropdown-ui.ts +35 -28
- package/src/injections/layer-dropdown/types.ts +7 -1
- package/src/injections/layer-dropdown/utils.ts +88 -64
- package/src/injections/utils.ts +0 -16
- package/src/injections/visual-edit-agent.ts +9 -7
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# Layer Dropdown Feature
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The layer dropdown lets users navigate the instrumented DOM hierarchy around a selected element. Clicking the chevron (`▾`) on an element's label reveals a dropdown showing **ancestors**, **siblings**, **the selected element**, and its **children** — all with depth-based indentation.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
grandparent (depth 0)
|
|
9
|
+
parent (depth 1)
|
|
10
|
+
sibling-1 (depth 2)
|
|
11
|
+
★ selected (depth 2) ← highlighted in blue
|
|
12
|
+
child-a (depth 3)
|
|
13
|
+
child-b (depth 3)
|
|
14
|
+
sibling-2 (depth 2)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Children are only expanded for the selected element, not for siblings.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Architecture
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
┌─────────────┐ ┌────────────────┐ ┌────────────────┐
|
|
25
|
+
│ types.ts │◄────│ controller.ts │────►│ dropdown-ui.ts│
|
|
26
|
+
│ consts.ts │◄────│ (orchestrator)│ │ (rendering) │
|
|
27
|
+
└─────────────┘ └───────┬────────┘ └────────────────┘
|
|
28
|
+
│
|
|
29
|
+
┌─────▼──────┐
|
|
30
|
+
│ utils.ts │
|
|
31
|
+
│ (DOM walk) │
|
|
32
|
+
└────────────┘
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
| File | Role |
|
|
36
|
+
|------|------|
|
|
37
|
+
| **types.ts** | `LayerInfo`, callback types, `LayerControllerDeps`, `LayerController` |
|
|
38
|
+
| **consts.ts** | Style maps, depth limits, chevron character, attribute name |
|
|
39
|
+
| **utils.ts** | DOM traversal — parent walking, sibling/descendant collection, depth assignment |
|
|
40
|
+
| **dropdown-ui.ts** | Creates the dropdown DOM, positions it, handles hover/click/keyboard |
|
|
41
|
+
| **controller.ts** | Stateful factory that wires everything together: build chain, show dropdown, handle selection |
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Data Model
|
|
46
|
+
|
|
47
|
+
### `LayerInfo`
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
interface LayerInfo {
|
|
51
|
+
element: Element; // DOM reference
|
|
52
|
+
tagName: string; // lowercase tag name ("div", "button")
|
|
53
|
+
selectorId: string | null; // data-source-location or data-visual-selector-id
|
|
54
|
+
depth?: number; // visual indentation level (set during chain building)
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
An element is **instrumented** if it has `dataset.sourceLocation` or `dataset.visualSelectorId`. Only instrumented elements appear in the dropdown.
|
|
59
|
+
|
|
60
|
+
### Key Constants
|
|
61
|
+
|
|
62
|
+
| Constant | Value | Purpose |
|
|
63
|
+
|----------|-------|---------|
|
|
64
|
+
| `MAX_PARENT_DEPTH` | `2` | Max ancestor levels shown above selected |
|
|
65
|
+
| `MAX_CHILD_DEPTH` | `2` | Max descendant levels shown below selected |
|
|
66
|
+
| `DEPTH_INDENT_PX` | `10` | Extra left-padding per depth level |
|
|
67
|
+
| `LABEL_CHEVRON` | `" ▾"` | Appended to label text when dropdown is available |
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## How the Chain Is Built (`utils.ts`)
|
|
72
|
+
|
|
73
|
+
`buildLayerChain(selectedElement)` is the entry point. It delegates to focused helper functions:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
export function buildLayerChain(selectedElement: Element): LayerInfo[] {
|
|
77
|
+
const parents = collectInstrumentedParents(selectedElement);
|
|
78
|
+
const chain: LayerInfo[] = [];
|
|
79
|
+
const selfDepth = appendParentsWithDepth(chain, parents);
|
|
80
|
+
|
|
81
|
+
const instrParent = getImmediateInstrParent(parents);
|
|
82
|
+
if (instrParent) {
|
|
83
|
+
const siblings = collectSiblings(instrParent, selectedElement);
|
|
84
|
+
appendSiblingsWithSelected(chain, siblings, selectedElement, selfDepth);
|
|
85
|
+
} else {
|
|
86
|
+
appendSelfAndDescendants(chain, selectedElement, selfDepth);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return chain;
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Helper Functions
|
|
94
|
+
|
|
95
|
+
| Function | Purpose |
|
|
96
|
+
|----------|---------|
|
|
97
|
+
| `toLayerInfo(element, depth?)` | Convert a DOM element to a `LayerInfo` object |
|
|
98
|
+
| `collectInstrumentedParents(el)` | Walk up the DOM, collect up to `MAX_PARENT_DEPTH` instrumented ancestors, return outermost-first |
|
|
99
|
+
| `appendParentsWithDepth(chain, parents)` | Push parents into chain with `depth = index` (0, 1, ...), return count as `selfDepth` |
|
|
100
|
+
| `getImmediateInstrParent(parents)` | Return the innermost parent's DOM element, or `null` if `parents` is empty |
|
|
101
|
+
| `collectSiblings(parent, selectedEl)` | Call `getInstrumentedDescendants(parent, 1)` to get all first-level instrumented children in DOM order; safety-guard ensures `selectedEl` is always included |
|
|
102
|
+
| `appendSiblingsWithSelected(chain, siblings, selectedEl, depth)` | Iterate siblings at `selfDepth`; for the selected element, expand children via `appendSelfAndDescendants`; for others, just add at `selfDepth` |
|
|
103
|
+
| `appendSelfAndDescendants(chain, el, depth)` | Push element + its descendants (up to `MAX_CHILD_DEPTH` levels) with assigned depths |
|
|
104
|
+
| `getInstrumentedDescendants(parent, maxDepth)` | Recursive DOM walk — instrumented children increment depth, non-instrumented wrappers are transparent. Results in DOM order |
|
|
105
|
+
| `assignDescendantDepths(root, descendants, startDepth)` | Second-pass walk assigning `depth = startDepth + instrDepth - 1` using a `Set`/`Map` for O(1) lookups |
|
|
106
|
+
|
|
107
|
+
### Chain-Building Steps
|
|
108
|
+
|
|
109
|
+
**Step 1 — `collectInstrumentedParents`**: Walk up from `selectedElement.parentElement`, skipping `document.body` and `document.documentElement`. Collect up to `MAX_PARENT_DEPTH` instrumented ancestors, then reverse so outermost comes first.
|
|
110
|
+
|
|
111
|
+
**Step 2 — `appendParentsWithDepth`**: Each parent gets `depth = index` (0, 1, ...). The count becomes `selfDepth`.
|
|
112
|
+
|
|
113
|
+
**Step 3 — `getImmediateInstrParent`**: Extract the innermost instrumented parent's element. If `parents` is empty, returns `null` (root-level element).
|
|
114
|
+
|
|
115
|
+
**Step 4 — `collectSiblings`** (when parent exists): Call `getInstrumentedDescendants(instrParent, 1)` to get all first-level instrumented children of the parent in DOM order — this naturally includes the selected element among its siblings. A safety guard ensures the selected element is always present.
|
|
116
|
+
|
|
117
|
+
**Step 5 — `appendSiblingsWithSelected`**: Iterate siblings in DOM order, all at `selfDepth`. For the selected element only, delegate to `appendSelfAndDescendants` which also collects and appends children. For other siblings, just push at `selfDepth` with no child expansion.
|
|
118
|
+
|
|
119
|
+
**Fallback** (no parent): If no instrumented parent exists (root-level element), skip sibling collection and call `appendSelfAndDescendants` directly — just self + children.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Controller Lifecycle (`controller.ts`)
|
|
124
|
+
|
|
125
|
+
`createLayerController(deps)` returns `{ attachToOverlay, cleanup }`.
|
|
126
|
+
|
|
127
|
+
### Dependency Injection
|
|
128
|
+
|
|
129
|
+
| Dep | Called When |
|
|
130
|
+
|-----|------------|
|
|
131
|
+
| `createPreviewOverlay(el)` | User hovers a dropdown item |
|
|
132
|
+
| `getSelectedElementId()` | Checking if hovered item is already selected |
|
|
133
|
+
| `selectElement(el)` | User clicks a dropdown item — performs selection, returns overlay |
|
|
134
|
+
| `onDeselect()` | Dropdown opens — temporarily clears selection for hover previews |
|
|
135
|
+
|
|
136
|
+
### `attachToOverlay(overlay, element)`
|
|
137
|
+
|
|
138
|
+
1. Extract the label `<div>` from the overlay.
|
|
139
|
+
2. Call `buildLayerChain(element)`.
|
|
140
|
+
3. **Guard**: if `layers.length <= 1`, return early — no chevron, no click handler. This is how the chevron is hidden when there's nothing to navigate to.
|
|
141
|
+
4. Append chevron via `enhanceLabelWithChevron(label)`.
|
|
142
|
+
5. Bind click handler to label (toggle behavior).
|
|
143
|
+
|
|
144
|
+
### Click Handler Flow
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
Label clicked
|
|
148
|
+
├─ Dropdown already open → close + reselect source element
|
|
149
|
+
└─ Dropdown closed →
|
|
150
|
+
1. Save source element (for Escape restore)
|
|
151
|
+
2. deps.onDeselect() (clear selection visual)
|
|
152
|
+
3. Register Escape key listener (capture phase)
|
|
153
|
+
4. showDropdown(label, layers, callbacks...)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### State Transitions
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
CLOSED ──[click chevron]──► OPEN
|
|
160
|
+
OPEN ──[click chevron]──► CLOSED (reselect original)
|
|
161
|
+
OPEN ──[Escape]──────────► CLOSED (reselect original)
|
|
162
|
+
OPEN ──[click item]─────► CLOSED → NEW ELEMENT SELECTED → reattach
|
|
163
|
+
OPEN ──[click outside]──► CLOSED
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Selection is recursive — `selectElementFromLayer` calls `deps.selectElement()`, gets a new overlay, and calls `attachToOverlay()` again on it.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Dropdown UI (`dropdown-ui.ts`)
|
|
171
|
+
|
|
172
|
+
### Global State (singleton)
|
|
173
|
+
|
|
174
|
+
Only one dropdown can be active at a time:
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
let activeDropdown: HTMLDivElement | null = null;
|
|
178
|
+
let outsideMousedownHandler: ((e: MouseEvent) => void) | null = null;
|
|
179
|
+
let activeOnHoverEnd: OnLayerHoverEnd | null = null;
|
|
180
|
+
let activeKeydownHandler: ((e: KeyboardEvent) => void) | null = null;
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### `showDropdown(label, layers, currentSelectorId, onSelect, onHover, onHoverEnd)`
|
|
184
|
+
|
|
185
|
+
1. Close any existing dropdown.
|
|
186
|
+
2. Create dropdown element with all layer items.
|
|
187
|
+
3. Position it below the label (`offsetTop + offsetHeight + 2px` gap).
|
|
188
|
+
4. Append to the overlay's parent element.
|
|
189
|
+
5. Set up keyboard navigation and outside-click handler.
|
|
190
|
+
|
|
191
|
+
### Dropdown Item Rendering
|
|
192
|
+
|
|
193
|
+
Each `LayerInfo` becomes a `<div>` row:
|
|
194
|
+
|
|
195
|
+
- **Display name**: `layer.tagName` (e.g., "div", "section")
|
|
196
|
+
- **Indentation**: `paddingLeft = 12 + depth * 10` px
|
|
197
|
+
- **Active (selected) item**: blue text (`#526cff`), light blue bg (`#DBEAFE`), semi-bold
|
|
198
|
+
- **Hover state**: light gray bg (`#f1f5f9`)
|
|
199
|
+
- **Events**: `mouseenter` → preview, `mouseleave` → clear preview, `click` → select
|
|
200
|
+
|
|
201
|
+
### Keyboard Navigation
|
|
202
|
+
|
|
203
|
+
- **Arrow Down/Up**: Circular focus movement through items
|
|
204
|
+
- **Enter**: Select focused item
|
|
205
|
+
- All listeners use capture phase to intercept before bubbling
|
|
206
|
+
|
|
207
|
+
### Outside Click
|
|
208
|
+
|
|
209
|
+
Registered via `setTimeout(..., 0)` to prevent the opening click from immediately closing. Uses `mousedown` in capture phase for responsive dismissal.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Edge Cases
|
|
214
|
+
|
|
215
|
+
| Scenario | Behavior |
|
|
216
|
+
|----------|----------|
|
|
217
|
+
| No instrumented parent (root element) | Falls back to self + children only (no siblings) |
|
|
218
|
+
| Only child (no other siblings) | Siblings list contains just self — same result as before |
|
|
219
|
+
| Parent is `document.body`/`html` | Excluded by parent-walk guard, so `parents` is empty |
|
|
220
|
+
| `layers.length <= 1` | No chevron appended, no click handler attached |
|
|
221
|
+
| Element not in siblings list | Safety guard appends it |
|
|
222
|
+
| Non-instrumented wrapper elements | Walked through transparently during descendant collection |
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Data Flow Summary
|
|
227
|
+
|
|
228
|
+
```
|
|
229
|
+
User selects element
|
|
230
|
+
↓
|
|
231
|
+
attachToOverlay(overlay, element)
|
|
232
|
+
↓
|
|
233
|
+
buildLayerChain(element)
|
|
234
|
+
├── collectInstrumentedParents() → parents[] (outermost first)
|
|
235
|
+
├── appendParentsWithDepth() → chain gets parents at depth 0,1,...
|
|
236
|
+
├── getImmediateInstrParent() → instrParent or null
|
|
237
|
+
├── collectSiblings(instrParent) → siblings[] (DOM order)
|
|
238
|
+
└── appendSiblingsWithSelected()
|
|
239
|
+
├── sibling → chain at selfDepth (no expansion)
|
|
240
|
+
├── ★ selected → appendSelfAndDescendants()
|
|
241
|
+
│ ├── self at selfDepth
|
|
242
|
+
│ ├── getInstrumentedDescendants(self, 2) → children[]
|
|
243
|
+
│ └── assignDescendantDepths() → children get depth values
|
|
244
|
+
└── sibling → chain at selfDepth (no expansion)
|
|
245
|
+
↓
|
|
246
|
+
LayerInfo[] with depth values
|
|
247
|
+
↓
|
|
248
|
+
layers.length > 1 ? enhanceLabelWithChevron() : return
|
|
249
|
+
↓
|
|
250
|
+
User clicks chevron → showDropdown()
|
|
251
|
+
↓
|
|
252
|
+
createDropdownElement(layers, currentId, callbacks)
|
|
253
|
+
└── createDropdownItem() for each layer (indented by depth)
|
|
254
|
+
↓
|
|
255
|
+
User hovers item → showLayerPreview() → preview overlay
|
|
256
|
+
User clicks item → selectElementFromLayer() → new selection → reattach
|
|
257
|
+
User presses Escape → closeDropdown() → reselect original
|
|
258
|
+
```
|
|
@@ -33,7 +33,12 @@ export const DROPDOWN_ITEM_HOVER_BG = "#f1f5f9";
|
|
|
33
33
|
|
|
34
34
|
export const DEPTH_INDENT_PX = 10;
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
/** Chevron shown when dropdown is collapsed (click to expand) */
|
|
37
|
+
export const CHEVRON_COLLAPSED = " \u25BE";
|
|
38
|
+
/** Chevron shown when dropdown is expanded (click to collapse) */
|
|
39
|
+
export const CHEVRON_EXPANDED = " \u25B4";
|
|
40
|
+
|
|
41
|
+
export const BASE_PADDING_PX = 12;
|
|
37
42
|
|
|
38
43
|
export const LAYER_DROPDOWN_ATTR = "data-layer-dropdown";
|
|
39
44
|
|
|
@@ -8,12 +8,12 @@ import {
|
|
|
8
8
|
closeDropdown,
|
|
9
9
|
isDropdownOpen,
|
|
10
10
|
} from "./dropdown-ui.js";
|
|
11
|
-
import type { LayerInfo,
|
|
11
|
+
import type { LayerInfo, LayerControllerConfig, LayerController } from "./types.js";
|
|
12
12
|
|
|
13
|
-
export function createLayerController(
|
|
13
|
+
export function createLayerController(config: LayerControllerConfig): LayerController {
|
|
14
14
|
let layerPreviewOverlay: HTMLDivElement | null = null;
|
|
15
15
|
let escapeHandler: ((e: KeyboardEvent) => void) | null = null;
|
|
16
|
-
let
|
|
16
|
+
let dropdownSourceLayer: LayerInfo | null = null;
|
|
17
17
|
|
|
18
18
|
const clearLayerPreview = () => {
|
|
19
19
|
if (layerPreviewOverlay && layerPreviewOverlay.parentNode) {
|
|
@@ -24,36 +24,59 @@ export function createLayerController(deps: LayerControllerDeps): LayerControlle
|
|
|
24
24
|
|
|
25
25
|
const showLayerPreview = (layer: LayerInfo) => {
|
|
26
26
|
clearLayerPreview();
|
|
27
|
-
if (getElementSelectorId(layer.element) ===
|
|
27
|
+
if (getElementSelectorId(layer.element) === config.getSelectedElementId()) return;
|
|
28
28
|
|
|
29
|
-
layerPreviewOverlay =
|
|
29
|
+
layerPreviewOverlay = config.createPreviewOverlay(layer.element);
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
-
const
|
|
32
|
+
const selectLayer = (layer: LayerInfo) => {
|
|
33
33
|
clearLayerPreview();
|
|
34
34
|
closeDropdown();
|
|
35
35
|
if (escapeHandler) {
|
|
36
36
|
document.removeEventListener("keydown", escapeHandler, true);
|
|
37
37
|
escapeHandler = null;
|
|
38
38
|
}
|
|
39
|
-
|
|
39
|
+
dropdownSourceLayer = null;
|
|
40
40
|
|
|
41
|
-
const firstOverlay =
|
|
41
|
+
const firstOverlay = config.selectElement(layer.element);
|
|
42
42
|
attachToOverlay(firstOverlay, layer.element);
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
-
const
|
|
45
|
+
const restoreSelection = () => {
|
|
46
46
|
if (escapeHandler) {
|
|
47
47
|
document.removeEventListener("keydown", escapeHandler, true);
|
|
48
48
|
escapeHandler = null;
|
|
49
49
|
}
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
if (dropdownSourceLayer) {
|
|
51
|
+
selectLayer(dropdownSourceLayer);
|
|
52
|
+
dropdownSourceLayer = null;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const handleLabelClick = (e: MouseEvent, label: HTMLDivElement, element: Element, layers: LayerInfo[], currentId: string | null) => {
|
|
57
|
+
e.stopPropagation();
|
|
58
|
+
e.preventDefault();
|
|
59
|
+
if (isDropdownOpen()) {
|
|
60
|
+
closeDropdown();
|
|
61
|
+
restoreSelection();
|
|
62
|
+
} else {
|
|
63
|
+
dropdownSourceLayer = {
|
|
64
|
+
element,
|
|
65
|
+
tagName: element.tagName.toLowerCase(),
|
|
66
|
+
selectorId: currentId,
|
|
67
|
+
};
|
|
68
|
+
config.onDeselect();
|
|
69
|
+
|
|
70
|
+
escapeHandler = (ev: KeyboardEvent) => {
|
|
71
|
+
if (ev.key === "Escape") {
|
|
72
|
+
ev.stopPropagation();
|
|
73
|
+
closeDropdown();
|
|
74
|
+
restoreSelection();
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
document.addEventListener("keydown", escapeHandler, true);
|
|
78
|
+
|
|
79
|
+
showDropdown(label, layers, currentId, { onSelect: selectLayer, onHover: showLayerPreview, onHoverEnd: clearLayerPreview });
|
|
57
80
|
}
|
|
58
81
|
};
|
|
59
82
|
|
|
@@ -73,26 +96,7 @@ export function createLayerController(deps: LayerControllerDeps): LayerControlle
|
|
|
73
96
|
enhanceLabelWithChevron(label);
|
|
74
97
|
|
|
75
98
|
label.addEventListener("click", (e: MouseEvent) => {
|
|
76
|
-
e
|
|
77
|
-
e.preventDefault();
|
|
78
|
-
if (isDropdownOpen()) {
|
|
79
|
-
closeDropdown();
|
|
80
|
-
reselectDropdownSource();
|
|
81
|
-
} else {
|
|
82
|
-
dropdownSourceElement = element;
|
|
83
|
-
deps.onDeselect();
|
|
84
|
-
|
|
85
|
-
escapeHandler = (ev: KeyboardEvent) => {
|
|
86
|
-
if (ev.key === "Escape") {
|
|
87
|
-
ev.stopPropagation();
|
|
88
|
-
closeDropdown();
|
|
89
|
-
reselectDropdownSource();
|
|
90
|
-
}
|
|
91
|
-
};
|
|
92
|
-
document.addEventListener("keydown", escapeHandler, true);
|
|
93
|
-
|
|
94
|
-
showDropdown(label, layers, currentId, selectElementFromLayer, showLayerPreview, clearLayerPreview);
|
|
95
|
-
}
|
|
99
|
+
handleLabelClick(e, label, element, layers, currentId);
|
|
96
100
|
});
|
|
97
101
|
};
|
|
98
102
|
|
|
@@ -8,23 +8,24 @@ import {
|
|
|
8
8
|
DROPDOWN_ITEM_ACTIVE_FONT_WEIGHT,
|
|
9
9
|
DROPDOWN_ITEM_HOVER_BG,
|
|
10
10
|
DEPTH_INDENT_PX,
|
|
11
|
-
|
|
11
|
+
BASE_PADDING_PX,
|
|
12
|
+
CHEVRON_COLLAPSED,
|
|
13
|
+
CHEVRON_EXPANDED,
|
|
12
14
|
LAYER_DROPDOWN_ATTR,
|
|
13
15
|
} from "./consts.js";
|
|
14
16
|
import { applyStyles, getLayerDisplayName } from "./utils.js";
|
|
15
|
-
import type { LayerInfo,
|
|
17
|
+
import type { LayerInfo, DropdownCallbacks } from "./types.js";
|
|
16
18
|
|
|
17
19
|
let activeDropdown: HTMLDivElement | null = null;
|
|
20
|
+
let activeLabel: HTMLDivElement | null = null;
|
|
18
21
|
let outsideMousedownHandler: ((e: MouseEvent) => void) | null = null;
|
|
19
|
-
let activeOnHoverEnd:
|
|
22
|
+
let activeOnHoverEnd: (() => void) | null = null;
|
|
20
23
|
let activeKeydownHandler: ((e: KeyboardEvent) => void) | null = null;
|
|
21
24
|
|
|
22
25
|
function createDropdownItem(
|
|
23
26
|
layer: LayerInfo,
|
|
24
27
|
isActive: boolean,
|
|
25
|
-
onSelect:
|
|
26
|
-
onHover?: OnLayerHover,
|
|
27
|
-
onHoverEnd?: OnLayerHoverEnd
|
|
28
|
+
{ onSelect, onHover, onHoverEnd }: DropdownCallbacks
|
|
28
29
|
): HTMLDivElement {
|
|
29
30
|
const item = document.createElement("div");
|
|
30
31
|
item.textContent = getLayerDisplayName(layer);
|
|
@@ -32,7 +33,7 @@ function createDropdownItem(
|
|
|
32
33
|
|
|
33
34
|
const depth = layer.depth ?? 0;
|
|
34
35
|
if (depth > 0) {
|
|
35
|
-
item.style.paddingLeft = `${
|
|
36
|
+
item.style.paddingLeft = `${BASE_PADDING_PX + depth * DEPTH_INDENT_PX}px`;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
if (isActive) {
|
|
@@ -64,9 +65,7 @@ function createDropdownItem(
|
|
|
64
65
|
export function createDropdownElement(
|
|
65
66
|
layers: LayerInfo[],
|
|
66
67
|
currentSelectorId: string | null,
|
|
67
|
-
|
|
68
|
-
onHover?: OnLayerHover,
|
|
69
|
-
onHoverEnd?: OnLayerHoverEnd
|
|
68
|
+
callbacks: DropdownCallbacks
|
|
70
69
|
): HTMLDivElement {
|
|
71
70
|
const container = document.createElement("div");
|
|
72
71
|
container.setAttribute(LAYER_DROPDOWN_ATTR, "true");
|
|
@@ -74,7 +73,7 @@ export function createDropdownElement(
|
|
|
74
73
|
|
|
75
74
|
layers.forEach((layer) => {
|
|
76
75
|
const isActive = layer.selectorId === currentSelectorId;
|
|
77
|
-
container.appendChild(createDropdownItem(layer, isActive,
|
|
76
|
+
container.appendChild(createDropdownItem(layer, isActive, callbacks));
|
|
78
77
|
});
|
|
79
78
|
|
|
80
79
|
return container;
|
|
@@ -82,11 +81,13 @@ export function createDropdownElement(
|
|
|
82
81
|
|
|
83
82
|
/** Add chevron indicator and pointer-events to the label */
|
|
84
83
|
export function enhanceLabelWithChevron(label: HTMLDivElement): void {
|
|
85
|
-
|
|
84
|
+
const t = label.textContent ?? "";
|
|
85
|
+
if (t.endsWith(CHEVRON_COLLAPSED) || t.endsWith(CHEVRON_EXPANDED)) return;
|
|
86
86
|
|
|
87
|
-
label.textContent =
|
|
87
|
+
label.textContent = t + CHEVRON_COLLAPSED;
|
|
88
88
|
label.style.cursor = "pointer";
|
|
89
89
|
label.style.userSelect = "none";
|
|
90
|
+
label.style.whiteSpace = "nowrap";
|
|
90
91
|
label.style.pointerEvents = "auto";
|
|
91
92
|
label.setAttribute(LAYER_DROPDOWN_ATTR, "true");
|
|
92
93
|
}
|
|
@@ -95,9 +96,7 @@ function setupKeyboardNavigation(
|
|
|
95
96
|
dropdown: HTMLDivElement,
|
|
96
97
|
layers: LayerInfo[],
|
|
97
98
|
currentSelectorId: string | null,
|
|
98
|
-
onSelect:
|
|
99
|
-
onHover?: OnLayerHover,
|
|
100
|
-
onHoverEnd?: OnLayerHoverEnd
|
|
99
|
+
{ onSelect, onHover, onHoverEnd }: DropdownCallbacks
|
|
101
100
|
): void {
|
|
102
101
|
const items = Array.from(dropdown.children) as HTMLDivElement[];
|
|
103
102
|
let focusedIndex = layers.findIndex((l) => l.selectorId === currentSelectorId);
|
|
@@ -160,22 +159,21 @@ export function showDropdown(
|
|
|
160
159
|
label: HTMLDivElement,
|
|
161
160
|
layers: LayerInfo[],
|
|
162
161
|
currentSelectorId: string | null,
|
|
163
|
-
|
|
164
|
-
onHover?: OnLayerHover,
|
|
165
|
-
onHoverEnd?: OnLayerHoverEnd
|
|
162
|
+
callbacks: DropdownCallbacks
|
|
166
163
|
): void {
|
|
167
164
|
closeDropdown();
|
|
168
165
|
|
|
169
166
|
const dropdown = createDropdownElement(
|
|
170
167
|
layers,
|
|
171
168
|
currentSelectorId,
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
onSelect(layer)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
169
|
+
{
|
|
170
|
+
...callbacks,
|
|
171
|
+
onSelect: (layer) => {
|
|
172
|
+
if (callbacks.onHoverEnd) callbacks.onHoverEnd();
|
|
173
|
+
callbacks.onSelect(layer);
|
|
174
|
+
closeDropdown();
|
|
175
|
+
},
|
|
176
|
+
}
|
|
179
177
|
);
|
|
180
178
|
|
|
181
179
|
const overlay = label.parentElement;
|
|
@@ -186,14 +184,23 @@ export function showDropdown(
|
|
|
186
184
|
|
|
187
185
|
overlay.appendChild(dropdown);
|
|
188
186
|
activeDropdown = dropdown;
|
|
189
|
-
|
|
187
|
+
activeLabel = label;
|
|
188
|
+
if (label.textContent?.endsWith(CHEVRON_COLLAPSED.trim())) {
|
|
189
|
+
label.textContent = label.textContent.slice(0, -CHEVRON_COLLAPSED.length) + CHEVRON_EXPANDED;
|
|
190
|
+
}
|
|
191
|
+
activeOnHoverEnd = callbacks.onHoverEnd ?? null;
|
|
190
192
|
|
|
191
|
-
setupKeyboardNavigation(dropdown, layers, currentSelectorId,
|
|
193
|
+
setupKeyboardNavigation(dropdown, layers, currentSelectorId, callbacks);
|
|
192
194
|
setupOutsideClickHandler(dropdown, label);
|
|
193
195
|
}
|
|
194
196
|
|
|
195
197
|
/** Close the active dropdown and clean up listeners */
|
|
196
198
|
export function closeDropdown(): void {
|
|
199
|
+
if (activeLabel?.textContent?.includes(CHEVRON_EXPANDED)) {
|
|
200
|
+
activeLabel.textContent = activeLabel.textContent.replace(CHEVRON_EXPANDED, CHEVRON_COLLAPSED);
|
|
201
|
+
}
|
|
202
|
+
activeLabel = null;
|
|
203
|
+
|
|
197
204
|
if (activeOnHoverEnd) {
|
|
198
205
|
activeOnHoverEnd();
|
|
199
206
|
activeOnHoverEnd = null;
|
|
@@ -11,7 +11,13 @@ export type OnLayerSelect = (layer: LayerInfo) => void;
|
|
|
11
11
|
export type OnLayerHover = (layer: LayerInfo) => void;
|
|
12
12
|
export type OnLayerHoverEnd = () => void;
|
|
13
13
|
|
|
14
|
-
export interface
|
|
14
|
+
export interface DropdownCallbacks {
|
|
15
|
+
onSelect: OnLayerSelect;
|
|
16
|
+
onHover?: OnLayerHover;
|
|
17
|
+
onHoverEnd?: OnLayerHoverEnd;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface LayerControllerConfig {
|
|
15
21
|
createPreviewOverlay: (element: Element) => HTMLDivElement;
|
|
16
22
|
getSelectedElementId: () => string | null;
|
|
17
23
|
selectElement: (element: Element) => HTMLDivElement | undefined;
|