@base44-preview/vite-plugin 0.2.21-pr.35.60ab963 → 0.2.22-pr.36.69a0b76
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 +15 -0
- package/dist/injections/layer-dropdown/consts.d.ts.map +1 -0
- package/dist/injections/layer-dropdown/consts.js +36 -0
- package/dist/injections/layer-dropdown/consts.js.map +1 -0
- package/dist/injections/layer-dropdown/controller.d.ts +4 -0
- package/dist/injections/layer-dropdown/controller.d.ts.map +1 -0
- package/dist/injections/layer-dropdown/controller.js +85 -0
- package/dist/injections/layer-dropdown/controller.js.map +1 -0
- package/dist/injections/layer-dropdown/dropdown-ui.d.ts +13 -0
- package/dist/injections/layer-dropdown/dropdown-ui.d.ts.map +1 -0
- package/dist/injections/layer-dropdown/dropdown-ui.js +158 -0
- package/dist/injections/layer-dropdown/dropdown-ui.js.map +1 -0
- package/dist/injections/layer-dropdown/types.d.ts +21 -0
- package/dist/injections/layer-dropdown/types.d.ts.map +1 -0
- package/dist/injections/layer-dropdown/types.js +3 -0
- package/dist/injections/layer-dropdown/types.js.map +1 -0
- package/dist/injections/layer-dropdown/utils.d.ts +23 -0
- package/dist/injections/layer-dropdown/utils.d.ts.map +1 -0
- package/dist/injections/layer-dropdown/utils.js +113 -0
- package/dist/injections/layer-dropdown/utils.js.map +1 -0
- package/dist/injections/utils.d.ts +6 -0
- package/dist/injections/utils.d.ts.map +1 -1
- package/dist/injections/utils.js +25 -0
- 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 +79 -64
- 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/consts.ts +44 -0
- package/src/injections/layer-dropdown/controller.ts +105 -0
- package/src/injections/layer-dropdown/dropdown-ui.ts +221 -0
- package/src/injections/layer-dropdown/types.ts +24 -0
- package/src/injections/layer-dropdown/utils.ts +138 -0
- package/src/injections/utils.ts +34 -0
- package/src/injections/visual-edit-agent.ts +88 -74
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/** Controller that encapsulates layer-dropdown integration logic */
|
|
2
|
+
|
|
3
|
+
import { getElementSelectorId } from "../utils.js";
|
|
4
|
+
import { buildLayerChain } from "./utils.js";
|
|
5
|
+
import {
|
|
6
|
+
enhanceLabelWithChevron,
|
|
7
|
+
showDropdown,
|
|
8
|
+
closeDropdown,
|
|
9
|
+
isDropdownOpen,
|
|
10
|
+
} from "./dropdown-ui.js";
|
|
11
|
+
import type { LayerInfo, LayerControllerDeps, LayerController } from "./types.js";
|
|
12
|
+
|
|
13
|
+
export function createLayerController(deps: LayerControllerDeps): LayerController {
|
|
14
|
+
let layerPreviewOverlay: HTMLDivElement | null = null;
|
|
15
|
+
let escapeHandler: ((e: KeyboardEvent) => void) | null = null;
|
|
16
|
+
let dropdownSourceElement: Element | null = null;
|
|
17
|
+
|
|
18
|
+
const clearLayerPreview = () => {
|
|
19
|
+
if (layerPreviewOverlay && layerPreviewOverlay.parentNode) {
|
|
20
|
+
layerPreviewOverlay.remove();
|
|
21
|
+
}
|
|
22
|
+
layerPreviewOverlay = null;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const showLayerPreview = (layer: LayerInfo) => {
|
|
26
|
+
clearLayerPreview();
|
|
27
|
+
if (getElementSelectorId(layer.element) === deps.getSelectedElementId()) return;
|
|
28
|
+
|
|
29
|
+
layerPreviewOverlay = deps.createPreviewOverlay(layer.element);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const selectElementFromLayer = (layer: LayerInfo) => {
|
|
33
|
+
clearLayerPreview();
|
|
34
|
+
closeDropdown();
|
|
35
|
+
if (escapeHandler) {
|
|
36
|
+
document.removeEventListener("keydown", escapeHandler, true);
|
|
37
|
+
escapeHandler = null;
|
|
38
|
+
}
|
|
39
|
+
dropdownSourceElement = null;
|
|
40
|
+
|
|
41
|
+
const firstOverlay = deps.selectElement(layer.element);
|
|
42
|
+
attachToOverlay(firstOverlay, layer.element);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const reselectDropdownSource = () => {
|
|
46
|
+
if (escapeHandler) {
|
|
47
|
+
document.removeEventListener("keydown", escapeHandler, true);
|
|
48
|
+
escapeHandler = null;
|
|
49
|
+
}
|
|
50
|
+
if (dropdownSourceElement) {
|
|
51
|
+
selectElementFromLayer({
|
|
52
|
+
element: dropdownSourceElement,
|
|
53
|
+
tagName: dropdownSourceElement.tagName.toLowerCase(),
|
|
54
|
+
selectorId: getElementSelectorId(dropdownSourceElement),
|
|
55
|
+
});
|
|
56
|
+
dropdownSourceElement = null;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const attachToOverlay = (
|
|
61
|
+
overlay: HTMLDivElement | undefined,
|
|
62
|
+
element: Element
|
|
63
|
+
) => {
|
|
64
|
+
if (!overlay) return;
|
|
65
|
+
|
|
66
|
+
const label = overlay.querySelector("div") as HTMLDivElement | null;
|
|
67
|
+
if (!label) return;
|
|
68
|
+
|
|
69
|
+
const layers = buildLayerChain(element);
|
|
70
|
+
if (layers.length <= 1) return;
|
|
71
|
+
|
|
72
|
+
const currentId = getElementSelectorId(element);
|
|
73
|
+
enhanceLabelWithChevron(label);
|
|
74
|
+
|
|
75
|
+
label.addEventListener("click", (e: MouseEvent) => {
|
|
76
|
+
e.stopPropagation();
|
|
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
|
+
}
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const cleanup = () => {
|
|
100
|
+
clearLayerPreview();
|
|
101
|
+
closeDropdown();
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return { attachToOverlay, cleanup };
|
|
105
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/** Dropdown UI component for layer navigation */
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DROPDOWN_CONTAINER_STYLES,
|
|
5
|
+
DROPDOWN_ITEM_BASE_STYLES,
|
|
6
|
+
DROPDOWN_ITEM_ACTIVE_COLOR,
|
|
7
|
+
DROPDOWN_ITEM_ACTIVE_BG,
|
|
8
|
+
DROPDOWN_ITEM_ACTIVE_FONT_WEIGHT,
|
|
9
|
+
DROPDOWN_ITEM_HOVER_BG,
|
|
10
|
+
DEPTH_INDENT_PX,
|
|
11
|
+
LABEL_CHEVRON,
|
|
12
|
+
LAYER_DROPDOWN_ATTR,
|
|
13
|
+
} from "./consts.js";
|
|
14
|
+
import { applyStyles, getLayerDisplayName } from "./utils.js";
|
|
15
|
+
import type { LayerInfo, OnLayerSelect, OnLayerHover, OnLayerHoverEnd } from "./types.js";
|
|
16
|
+
|
|
17
|
+
let activeDropdown: HTMLDivElement | null = null;
|
|
18
|
+
let outsideMousedownHandler: ((e: MouseEvent) => void) | null = null;
|
|
19
|
+
let activeOnHoverEnd: OnLayerHoverEnd | null = null;
|
|
20
|
+
let activeKeydownHandler: ((e: KeyboardEvent) => void) | null = null;
|
|
21
|
+
|
|
22
|
+
function createDropdownItem(
|
|
23
|
+
layer: LayerInfo,
|
|
24
|
+
isActive: boolean,
|
|
25
|
+
onSelect: OnLayerSelect,
|
|
26
|
+
onHover?: OnLayerHover,
|
|
27
|
+
onHoverEnd?: OnLayerHoverEnd
|
|
28
|
+
): HTMLDivElement {
|
|
29
|
+
const item = document.createElement("div");
|
|
30
|
+
item.textContent = getLayerDisplayName(layer);
|
|
31
|
+
applyStyles(item, DROPDOWN_ITEM_BASE_STYLES);
|
|
32
|
+
|
|
33
|
+
const depth = layer.depth ?? 0;
|
|
34
|
+
if (depth > 0) {
|
|
35
|
+
item.style.paddingLeft = `${12 + depth * DEPTH_INDENT_PX}px`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (isActive) {
|
|
39
|
+
item.style.color = DROPDOWN_ITEM_ACTIVE_COLOR;
|
|
40
|
+
item.style.backgroundColor = DROPDOWN_ITEM_ACTIVE_BG;
|
|
41
|
+
item.style.fontWeight = DROPDOWN_ITEM_ACTIVE_FONT_WEIGHT;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
item.addEventListener("mouseenter", () => {
|
|
45
|
+
if (!isActive) item.style.backgroundColor = DROPDOWN_ITEM_HOVER_BG;
|
|
46
|
+
if (onHover) onHover(layer);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
item.addEventListener("mouseleave", () => {
|
|
50
|
+
if (!isActive) item.style.backgroundColor = "transparent";
|
|
51
|
+
if (onHoverEnd) onHoverEnd();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
item.addEventListener("click", (e: MouseEvent) => {
|
|
55
|
+
e.stopPropagation();
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
onSelect(layer);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return item;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Create the dropdown DOM element with layer items */
|
|
64
|
+
export function createDropdownElement(
|
|
65
|
+
layers: LayerInfo[],
|
|
66
|
+
currentSelectorId: string | null,
|
|
67
|
+
onSelect: OnLayerSelect,
|
|
68
|
+
onHover?: OnLayerHover,
|
|
69
|
+
onHoverEnd?: OnLayerHoverEnd
|
|
70
|
+
): HTMLDivElement {
|
|
71
|
+
const container = document.createElement("div");
|
|
72
|
+
container.setAttribute(LAYER_DROPDOWN_ATTR, "true");
|
|
73
|
+
applyStyles(container, DROPDOWN_CONTAINER_STYLES);
|
|
74
|
+
|
|
75
|
+
layers.forEach((layer) => {
|
|
76
|
+
const isActive = layer.selectorId === currentSelectorId;
|
|
77
|
+
container.appendChild(createDropdownItem(layer, isActive, onSelect, onHover, onHoverEnd));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return container;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Add chevron indicator and pointer-events to the label */
|
|
84
|
+
export function enhanceLabelWithChevron(label: HTMLDivElement): void {
|
|
85
|
+
if (label.textContent?.includes(LABEL_CHEVRON)) return;
|
|
86
|
+
|
|
87
|
+
label.textContent = label.textContent + LABEL_CHEVRON;
|
|
88
|
+
label.style.cursor = "pointer";
|
|
89
|
+
label.style.userSelect = "none";
|
|
90
|
+
label.style.pointerEvents = "auto";
|
|
91
|
+
label.setAttribute(LAYER_DROPDOWN_ATTR, "true");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function setupKeyboardNavigation(
|
|
95
|
+
dropdown: HTMLDivElement,
|
|
96
|
+
layers: LayerInfo[],
|
|
97
|
+
currentSelectorId: string | null,
|
|
98
|
+
onSelect: OnLayerSelect,
|
|
99
|
+
onHover?: OnLayerHover,
|
|
100
|
+
onHoverEnd?: OnLayerHoverEnd
|
|
101
|
+
): void {
|
|
102
|
+
const items = Array.from(dropdown.children) as HTMLDivElement[];
|
|
103
|
+
let focusedIndex = layers.findIndex((l) => l.selectorId === currentSelectorId);
|
|
104
|
+
|
|
105
|
+
const setFocusedItem = (index: number) => {
|
|
106
|
+
if (focusedIndex >= 0 && focusedIndex < items.length) {
|
|
107
|
+
const prev = items[focusedIndex]!;
|
|
108
|
+
if (prev.style.color !== DROPDOWN_ITEM_ACTIVE_COLOR) {
|
|
109
|
+
prev.style.backgroundColor = "transparent";
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
focusedIndex = index;
|
|
113
|
+
if (focusedIndex >= 0 && focusedIndex < items.length) {
|
|
114
|
+
const cur = items[focusedIndex]!;
|
|
115
|
+
if (cur.style.color !== DROPDOWN_ITEM_ACTIVE_COLOR) {
|
|
116
|
+
cur.style.backgroundColor = DROPDOWN_ITEM_HOVER_BG;
|
|
117
|
+
}
|
|
118
|
+
cur.scrollIntoView({ block: "nearest" });
|
|
119
|
+
if (onHover) onHover(layers[focusedIndex]!);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
activeKeydownHandler = (e: KeyboardEvent) => {
|
|
124
|
+
if (e.key === "ArrowDown") {
|
|
125
|
+
e.preventDefault();
|
|
126
|
+
e.stopPropagation();
|
|
127
|
+
setFocusedItem(focusedIndex < items.length - 1 ? focusedIndex + 1 : 0);
|
|
128
|
+
} else if (e.key === "ArrowUp") {
|
|
129
|
+
e.preventDefault();
|
|
130
|
+
e.stopPropagation();
|
|
131
|
+
setFocusedItem(focusedIndex > 0 ? focusedIndex - 1 : items.length - 1);
|
|
132
|
+
} else if (e.key === "Enter" && focusedIndex >= 0) {
|
|
133
|
+
e.preventDefault();
|
|
134
|
+
e.stopPropagation();
|
|
135
|
+
if (onHoverEnd) onHoverEnd();
|
|
136
|
+
onSelect(layers[focusedIndex]!);
|
|
137
|
+
closeDropdown();
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
document.addEventListener("keydown", activeKeydownHandler, true);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function setupOutsideClickHandler(
|
|
144
|
+
dropdown: HTMLDivElement,
|
|
145
|
+
label: HTMLDivElement
|
|
146
|
+
): void {
|
|
147
|
+
setTimeout(() => {
|
|
148
|
+
outsideMousedownHandler = (e: MouseEvent) => {
|
|
149
|
+
const target = e.target as Node;
|
|
150
|
+
if (!dropdown.contains(target) && target !== label) {
|
|
151
|
+
closeDropdown();
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
document.addEventListener("mousedown", outsideMousedownHandler, true);
|
|
155
|
+
}, 0);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Show the dropdown below the label element */
|
|
159
|
+
export function showDropdown(
|
|
160
|
+
label: HTMLDivElement,
|
|
161
|
+
layers: LayerInfo[],
|
|
162
|
+
currentSelectorId: string | null,
|
|
163
|
+
onSelect: OnLayerSelect,
|
|
164
|
+
onHover?: OnLayerHover,
|
|
165
|
+
onHoverEnd?: OnLayerHoverEnd
|
|
166
|
+
): void {
|
|
167
|
+
closeDropdown();
|
|
168
|
+
|
|
169
|
+
const dropdown = createDropdownElement(
|
|
170
|
+
layers,
|
|
171
|
+
currentSelectorId,
|
|
172
|
+
(layer) => {
|
|
173
|
+
if (onHoverEnd) onHoverEnd();
|
|
174
|
+
onSelect(layer);
|
|
175
|
+
closeDropdown();
|
|
176
|
+
},
|
|
177
|
+
onHover,
|
|
178
|
+
onHoverEnd
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const overlay = label.parentElement;
|
|
182
|
+
if (!overlay) return;
|
|
183
|
+
|
|
184
|
+
dropdown.style.top = `${label.offsetTop + label.offsetHeight + 2}px`;
|
|
185
|
+
dropdown.style.left = `${label.offsetLeft}px`;
|
|
186
|
+
|
|
187
|
+
overlay.appendChild(dropdown);
|
|
188
|
+
activeDropdown = dropdown;
|
|
189
|
+
activeOnHoverEnd = onHoverEnd ?? null;
|
|
190
|
+
|
|
191
|
+
setupKeyboardNavigation(dropdown, layers, currentSelectorId, onSelect, onHover, onHoverEnd);
|
|
192
|
+
setupOutsideClickHandler(dropdown, label);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Close the active dropdown and clean up listeners */
|
|
196
|
+
export function closeDropdown(): void {
|
|
197
|
+
if (activeOnHoverEnd) {
|
|
198
|
+
activeOnHoverEnd();
|
|
199
|
+
activeOnHoverEnd = null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (activeDropdown && activeDropdown.parentNode) {
|
|
203
|
+
activeDropdown.remove();
|
|
204
|
+
}
|
|
205
|
+
activeDropdown = null;
|
|
206
|
+
|
|
207
|
+
if (outsideMousedownHandler) {
|
|
208
|
+
document.removeEventListener("mousedown", outsideMousedownHandler, true);
|
|
209
|
+
outsideMousedownHandler = null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (activeKeydownHandler) {
|
|
213
|
+
document.removeEventListener("keydown", activeKeydownHandler, true);
|
|
214
|
+
activeKeydownHandler = null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Check if a dropdown is currently visible */
|
|
219
|
+
export function isDropdownOpen(): boolean {
|
|
220
|
+
return activeDropdown !== null;
|
|
221
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Shared types for the layer-dropdown module */
|
|
2
|
+
|
|
3
|
+
export interface LayerInfo {
|
|
4
|
+
element: Element;
|
|
5
|
+
tagName: string;
|
|
6
|
+
selectorId: string | null;
|
|
7
|
+
depth?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type OnLayerSelect = (layer: LayerInfo) => void;
|
|
11
|
+
export type OnLayerHover = (layer: LayerInfo) => void;
|
|
12
|
+
export type OnLayerHoverEnd = () => void;
|
|
13
|
+
|
|
14
|
+
export interface LayerControllerDeps {
|
|
15
|
+
createPreviewOverlay: (element: Element) => HTMLDivElement;
|
|
16
|
+
getSelectedElementId: () => string | null;
|
|
17
|
+
selectElement: (element: Element) => HTMLDivElement | undefined;
|
|
18
|
+
onDeselect: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface LayerController {
|
|
22
|
+
attachToOverlay: (overlay: HTMLDivElement | undefined, element: Element) => void;
|
|
23
|
+
cleanup: () => void;
|
|
24
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/** DOM utilities for the layer-dropdown module */
|
|
2
|
+
|
|
3
|
+
import { isInstrumentedElement, getElementSelectorId } from "../utils.js";
|
|
4
|
+
import { MAX_PARENT_DEPTH, MAX_CHILD_DEPTH } from "./consts.js";
|
|
5
|
+
|
|
6
|
+
import type { LayerInfo } from "./types.js";
|
|
7
|
+
|
|
8
|
+
/** Apply a style map to an element */
|
|
9
|
+
export function applyStyles(element: HTMLElement, styles: Record<string, string>): void {
|
|
10
|
+
Object.assign(element.style, styles);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Display name for a layer — just the real tag name */
|
|
14
|
+
export function getLayerDisplayName(layer: LayerInfo): string {
|
|
15
|
+
return layer.tagName;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Collect instrumented descendants up to `maxDepth` instrumented nesting levels.
|
|
20
|
+
* Non-instrumented wrappers are walked through without counting toward depth.
|
|
21
|
+
* Results are in DOM order.
|
|
22
|
+
*/
|
|
23
|
+
export function getInstrumentedDescendants(
|
|
24
|
+
parent: Element,
|
|
25
|
+
maxDepth: number
|
|
26
|
+
): LayerInfo[] {
|
|
27
|
+
const result: LayerInfo[] = [];
|
|
28
|
+
|
|
29
|
+
function walk(el: Element, instrDepth: number): void {
|
|
30
|
+
if (instrDepth > maxDepth) return;
|
|
31
|
+
for (let i = 0; i < el.children.length; i++) {
|
|
32
|
+
const child = el.children[i]!;
|
|
33
|
+
if (isInstrumentedElement(child)) {
|
|
34
|
+
result.push({
|
|
35
|
+
element: child,
|
|
36
|
+
tagName: child.tagName.toLowerCase(),
|
|
37
|
+
selectorId: getElementSelectorId(child),
|
|
38
|
+
});
|
|
39
|
+
walk(child, instrDepth + 1);
|
|
40
|
+
} else {
|
|
41
|
+
walk(child, instrDepth);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
walk(parent, 1);
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build the layer chain for the dropdown:
|
|
52
|
+
*
|
|
53
|
+
* Parents – up to MAX_PARENT_DEPTH instrumented ancestors, outer → inner.
|
|
54
|
+
* Current – the selected element.
|
|
55
|
+
* Children – instrumented descendants within MAX_CHILD_DEPTH levels, DOM order.
|
|
56
|
+
*
|
|
57
|
+
* Each item carries a `depth` for visual indentation.
|
|
58
|
+
*/
|
|
59
|
+
export function buildLayerChain(selectedElement: Element): LayerInfo[] {
|
|
60
|
+
// --- Parents (walk up, collect at most MAX_PARENT_DEPTH) ---
|
|
61
|
+
const parents: LayerInfo[] = [];
|
|
62
|
+
let current = selectedElement.parentElement;
|
|
63
|
+
while (
|
|
64
|
+
current &&
|
|
65
|
+
current !== document.documentElement &&
|
|
66
|
+
current !== document.body &&
|
|
67
|
+
parents.length < MAX_PARENT_DEPTH
|
|
68
|
+
) {
|
|
69
|
+
if (isInstrumentedElement(current)) {
|
|
70
|
+
parents.push({
|
|
71
|
+
element: current,
|
|
72
|
+
tagName: current.tagName.toLowerCase(),
|
|
73
|
+
selectorId: getElementSelectorId(current),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
current = current.parentElement;
|
|
77
|
+
}
|
|
78
|
+
// Reverse so outermost parent comes first
|
|
79
|
+
parents.reverse();
|
|
80
|
+
|
|
81
|
+
// --- Build the chain with depth ---
|
|
82
|
+
const chain: LayerInfo[] = [];
|
|
83
|
+
const baseDepth = 0;
|
|
84
|
+
|
|
85
|
+
// Parents: depth 0, 1, …
|
|
86
|
+
parents.forEach((p, i) => {
|
|
87
|
+
chain.push({ ...p, depth: baseDepth + i });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Self
|
|
91
|
+
const selfDepth = parents.length;
|
|
92
|
+
chain.push({
|
|
93
|
+
element: selectedElement,
|
|
94
|
+
tagName: selectedElement.tagName.toLowerCase(),
|
|
95
|
+
selectorId: getElementSelectorId(selectedElement),
|
|
96
|
+
depth: selfDepth,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Children: up to MAX_CHILD_DEPTH instrumented levels below selected
|
|
100
|
+
const descendants = getInstrumentedDescendants(
|
|
101
|
+
selectedElement,
|
|
102
|
+
MAX_CHILD_DEPTH
|
|
103
|
+
);
|
|
104
|
+
// Assign visual depth: we need to track the instrumented nesting to set depth correctly
|
|
105
|
+
assignDescendantDepths(selectedElement, descendants, selfDepth + 1);
|
|
106
|
+
|
|
107
|
+
chain.push(...descendants);
|
|
108
|
+
return chain;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Walk the DOM tree again to assign correct visual depth to each descendant.
|
|
113
|
+
* This avoids storing depth during collection and keeps the API simple.
|
|
114
|
+
*/
|
|
115
|
+
function assignDescendantDepths(
|
|
116
|
+
root: Element,
|
|
117
|
+
descendants: LayerInfo[],
|
|
118
|
+
startDepth: number
|
|
119
|
+
): void {
|
|
120
|
+
// Build a set for O(1) lookup
|
|
121
|
+
const descSet = new Set(descendants.map((d) => d.element));
|
|
122
|
+
// Map element → LayerInfo for mutation
|
|
123
|
+
const descMap = new Map(descendants.map((d) => [d.element, d]));
|
|
124
|
+
|
|
125
|
+
function walk(el: Element, instrDepth: number): void {
|
|
126
|
+
for (let i = 0; i < el.children.length; i++) {
|
|
127
|
+
const child = el.children[i]!;
|
|
128
|
+
if (descSet.has(child)) {
|
|
129
|
+
descMap.get(child)!.depth = startDepth + instrDepth - 1;
|
|
130
|
+
walk(child, instrDepth + 1);
|
|
131
|
+
} else {
|
|
132
|
+
walk(child, instrDepth);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
walk(root, 1);
|
|
138
|
+
}
|
package/src/injections/utils.ts
CHANGED
|
@@ -1,3 +1,37 @@
|
|
|
1
|
+
/** Check if an element has instrumentation attributes */
|
|
2
|
+
export function isInstrumentedElement(element: Element): boolean {
|
|
3
|
+
const htmlEl = element as HTMLElement;
|
|
4
|
+
return !!(
|
|
5
|
+
htmlEl.dataset?.sourceLocation || htmlEl.dataset?.visualSelectorId
|
|
6
|
+
);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Get the selector ID from an element's data attributes (prefers source-location) */
|
|
10
|
+
export function getElementSelectorId(element: Element): string | null {
|
|
11
|
+
const htmlEl = element as HTMLElement;
|
|
12
|
+
return (
|
|
13
|
+
htmlEl.dataset?.sourceLocation ||
|
|
14
|
+
htmlEl.dataset?.visualSelectorId ||
|
|
15
|
+
null
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Find the nearest instrumented ancestor (not the element itself) */
|
|
20
|
+
export function getImmediateInstrumentedParent(element: Element): Element | null {
|
|
21
|
+
let current = element.parentElement;
|
|
22
|
+
while (
|
|
23
|
+
current &&
|
|
24
|
+
current !== document.documentElement &&
|
|
25
|
+
current !== document.body
|
|
26
|
+
) {
|
|
27
|
+
if (isInstrumentedElement(current)) {
|
|
28
|
+
return current;
|
|
29
|
+
}
|
|
30
|
+
current = current.parentElement;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
1
35
|
/** Find elements by ID - first try data-source-location, fallback to data-visual-selector-id */
|
|
2
36
|
export function findElementsById(id: string | null): Element[] {
|
|
3
37
|
if (!id) return [];
|