@base44-preview/vite-plugin 0.2.22-pr.36.019589c → 0.2.22-pr.36.425440e
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 +3 -0
- package/dist/injections/layer-dropdown/consts.d.ts.map +1 -1
- package/dist/injections/layer-dropdown/consts.js +3 -0
- package/dist/injections/layer-dropdown/consts.js.map +1 -1
- 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/{component/dropdown-ui.d.ts → dropdown-ui.d.ts} +1 -4
- package/dist/injections/layer-dropdown/dropdown-ui.d.ts.map +1 -0
- package/dist/injections/layer-dropdown/{component/dropdown-ui.js → dropdown-ui.js} +76 -74
- 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 +6 -8
- package/dist/injections/layer-dropdown/utils.d.ts.map +1 -1
- package/dist/injections/layer-dropdown/utils.js +74 -33
- 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 +74 -199
- 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 +3 -0
- package/src/injections/layer-dropdown/controller.ts +105 -0
- package/src/injections/layer-dropdown/{component/dropdown-ui.ts → dropdown-ui.ts} +109 -96
- package/src/injections/layer-dropdown/types.ts +24 -0
- package/src/injections/layer-dropdown/utils.ts +88 -41
- package/src/injections/utils.ts +0 -16
- package/src/injections/visual-edit-agent.ts +83 -237
- package/dist/injections/layer-dropdown/component/dropdown-ui.d.ts.map +0 -1
- package/dist/injections/layer-dropdown/component/dropdown-ui.js.map +0 -1
|
@@ -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
|
+
}
|
|
@@ -9,20 +9,59 @@ import {
|
|
|
9
9
|
DROPDOWN_ITEM_HOVER_BG,
|
|
10
10
|
DEPTH_INDENT_PX,
|
|
11
11
|
LABEL_CHEVRON,
|
|
12
|
+
LABEL_CHEVRON_OPEN,
|
|
12
13
|
LAYER_DROPDOWN_ATTR,
|
|
13
|
-
} from "
|
|
14
|
-
import { getLayerDisplayName } from "
|
|
15
|
-
import type { LayerInfo } from "
|
|
16
|
-
|
|
17
|
-
export type OnLayerSelect = (layer: LayerInfo) => void;
|
|
18
|
-
export type OnLayerHover = (layer: LayerInfo) => void;
|
|
19
|
-
export type OnLayerHoverEnd = () => void;
|
|
14
|
+
} from "./consts.js";
|
|
15
|
+
import { applyStyles, getLayerDisplayName } from "./utils.js";
|
|
16
|
+
import type { LayerInfo, OnLayerSelect, OnLayerHover, OnLayerHoverEnd } from "./types.js";
|
|
20
17
|
|
|
21
18
|
let activeDropdown: HTMLDivElement | null = null;
|
|
19
|
+
let activeLabel: HTMLDivElement | null = null;
|
|
22
20
|
let outsideMousedownHandler: ((e: MouseEvent) => void) | null = null;
|
|
23
21
|
let activeOnHoverEnd: OnLayerHoverEnd | null = null;
|
|
24
22
|
let activeKeydownHandler: ((e: KeyboardEvent) => void) | null = null;
|
|
25
23
|
|
|
24
|
+
function createDropdownItem(
|
|
25
|
+
layer: LayerInfo,
|
|
26
|
+
isActive: boolean,
|
|
27
|
+
onSelect: OnLayerSelect,
|
|
28
|
+
onHover?: OnLayerHover,
|
|
29
|
+
onHoverEnd?: OnLayerHoverEnd
|
|
30
|
+
): HTMLDivElement {
|
|
31
|
+
const item = document.createElement("div");
|
|
32
|
+
item.textContent = getLayerDisplayName(layer);
|
|
33
|
+
applyStyles(item, DROPDOWN_ITEM_BASE_STYLES);
|
|
34
|
+
|
|
35
|
+
const depth = layer.depth ?? 0;
|
|
36
|
+
if (depth > 0) {
|
|
37
|
+
item.style.paddingLeft = `${12 + depth * DEPTH_INDENT_PX}px`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (isActive) {
|
|
41
|
+
item.style.color = DROPDOWN_ITEM_ACTIVE_COLOR;
|
|
42
|
+
item.style.backgroundColor = DROPDOWN_ITEM_ACTIVE_BG;
|
|
43
|
+
item.style.fontWeight = DROPDOWN_ITEM_ACTIVE_FONT_WEIGHT;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
item.addEventListener("mouseenter", () => {
|
|
47
|
+
if (!isActive) item.style.backgroundColor = DROPDOWN_ITEM_HOVER_BG;
|
|
48
|
+
if (onHover) onHover(layer);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
item.addEventListener("mouseleave", () => {
|
|
52
|
+
if (!isActive) item.style.backgroundColor = "transparent";
|
|
53
|
+
if (onHoverEnd) onHoverEnd();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
item.addEventListener("click", (e: MouseEvent) => {
|
|
57
|
+
e.stopPropagation();
|
|
58
|
+
e.preventDefault();
|
|
59
|
+
onSelect(layer);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return item;
|
|
63
|
+
}
|
|
64
|
+
|
|
26
65
|
/** Create the dropdown DOM element with layer items */
|
|
27
66
|
export function createDropdownElement(
|
|
28
67
|
layers: LayerInfo[],
|
|
@@ -33,54 +72,11 @@ export function createDropdownElement(
|
|
|
33
72
|
): HTMLDivElement {
|
|
34
73
|
const container = document.createElement("div");
|
|
35
74
|
container.setAttribute(LAYER_DROPDOWN_ATTR, "true");
|
|
36
|
-
|
|
37
|
-
Object.keys(DROPDOWN_CONTAINER_STYLES).forEach((key) => {
|
|
38
|
-
container.style[key as any] = DROPDOWN_CONTAINER_STYLES[key]!;
|
|
39
|
-
});
|
|
75
|
+
applyStyles(container, DROPDOWN_CONTAINER_STYLES);
|
|
40
76
|
|
|
41
77
|
layers.forEach((layer) => {
|
|
42
|
-
const item = document.createElement("div");
|
|
43
78
|
const isActive = layer.selectorId === currentSelectorId;
|
|
44
|
-
|
|
45
|
-
item.textContent = getLayerDisplayName(layer);
|
|
46
|
-
|
|
47
|
-
Object.keys(DROPDOWN_ITEM_BASE_STYLES).forEach((key) => {
|
|
48
|
-
item.style[key as any] = DROPDOWN_ITEM_BASE_STYLES[key]!;
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
// Indent based on depth
|
|
52
|
-
const depth = layer.depth ?? 0;
|
|
53
|
-
if (depth > 0) {
|
|
54
|
-
item.style.paddingLeft = `${12 + depth * DEPTH_INDENT_PX}px`;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (isActive) {
|
|
58
|
-
item.style.color = DROPDOWN_ITEM_ACTIVE_COLOR;
|
|
59
|
-
item.style.backgroundColor = DROPDOWN_ITEM_ACTIVE_BG;
|
|
60
|
-
item.style.fontWeight = DROPDOWN_ITEM_ACTIVE_FONT_WEIGHT;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
item.addEventListener("mouseenter", () => {
|
|
64
|
-
if (!isActive) {
|
|
65
|
-
item.style.backgroundColor = DROPDOWN_ITEM_HOVER_BG;
|
|
66
|
-
}
|
|
67
|
-
if (onHover) onHover(layer);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
item.addEventListener("mouseleave", () => {
|
|
71
|
-
if (!isActive) {
|
|
72
|
-
item.style.backgroundColor = "transparent";
|
|
73
|
-
}
|
|
74
|
-
if (onHoverEnd) onHoverEnd();
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
item.addEventListener("click", (e: MouseEvent) => {
|
|
78
|
-
e.stopPropagation();
|
|
79
|
-
e.preventDefault();
|
|
80
|
-
onSelect(layer);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
container.appendChild(item);
|
|
79
|
+
container.appendChild(createDropdownItem(layer, isActive, onSelect, onHover, onHoverEnd));
|
|
84
80
|
});
|
|
85
81
|
|
|
86
82
|
return container;
|
|
@@ -88,68 +84,38 @@ export function createDropdownElement(
|
|
|
88
84
|
|
|
89
85
|
/** Add chevron indicator and pointer-events to the label */
|
|
90
86
|
export function enhanceLabelWithChevron(label: HTMLDivElement): void {
|
|
91
|
-
|
|
87
|
+
const t = label.textContent ?? "";
|
|
88
|
+
if (t.endsWith(LABEL_CHEVRON) || t.endsWith(LABEL_CHEVRON_OPEN)) return;
|
|
92
89
|
|
|
93
|
-
label.textContent =
|
|
90
|
+
label.textContent = t + LABEL_CHEVRON;
|
|
94
91
|
label.style.cursor = "pointer";
|
|
95
92
|
label.style.userSelect = "none";
|
|
96
93
|
label.style.pointerEvents = "auto";
|
|
97
94
|
label.setAttribute(LAYER_DROPDOWN_ATTR, "true");
|
|
98
95
|
}
|
|
99
96
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
label: HTMLDivElement,
|
|
97
|
+
function setupKeyboardNavigation(
|
|
98
|
+
dropdown: HTMLDivElement,
|
|
103
99
|
layers: LayerInfo[],
|
|
104
100
|
currentSelectorId: string | null,
|
|
105
101
|
onSelect: OnLayerSelect,
|
|
106
102
|
onHover?: OnLayerHover,
|
|
107
103
|
onHoverEnd?: OnLayerHoverEnd
|
|
108
104
|
): void {
|
|
109
|
-
closeDropdown();
|
|
110
|
-
|
|
111
|
-
const dropdown = createDropdownElement(
|
|
112
|
-
layers,
|
|
113
|
-
currentSelectorId,
|
|
114
|
-
(layer) => {
|
|
115
|
-
if (onHoverEnd) onHoverEnd();
|
|
116
|
-
onSelect(layer);
|
|
117
|
-
closeDropdown();
|
|
118
|
-
},
|
|
119
|
-
onHover,
|
|
120
|
-
onHoverEnd
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
const overlay = label.parentElement;
|
|
124
|
-
if (!overlay) return;
|
|
125
|
-
|
|
126
|
-
// Position below the label
|
|
127
|
-
dropdown.style.top = `${label.offsetTop + label.offsetHeight + 2}px`;
|
|
128
|
-
dropdown.style.left = `${label.offsetLeft}px`;
|
|
129
|
-
|
|
130
|
-
overlay.appendChild(dropdown);
|
|
131
|
-
activeDropdown = dropdown;
|
|
132
|
-
activeOnHoverEnd = onHoverEnd ?? null;
|
|
133
|
-
|
|
134
|
-
// --- Arrow key navigation ---
|
|
135
105
|
const items = Array.from(dropdown.children) as HTMLDivElement[];
|
|
136
|
-
let focusedIndex =
|
|
106
|
+
let focusedIndex = layers.findIndex((l) => l.selectorId === currentSelectorId);
|
|
137
107
|
|
|
138
108
|
const setFocusedItem = (index: number) => {
|
|
139
|
-
// Clear previous focus styling
|
|
140
109
|
if (focusedIndex >= 0 && focusedIndex < items.length) {
|
|
141
110
|
const prev = items[focusedIndex]!;
|
|
142
|
-
|
|
143
|
-
if (!prevIsActive) {
|
|
111
|
+
if (prev.style.color !== DROPDOWN_ITEM_ACTIVE_COLOR) {
|
|
144
112
|
prev.style.backgroundColor = "transparent";
|
|
145
113
|
}
|
|
146
114
|
}
|
|
147
115
|
focusedIndex = index;
|
|
148
|
-
// Apply hover styling to new focused item
|
|
149
116
|
if (focusedIndex >= 0 && focusedIndex < items.length) {
|
|
150
117
|
const cur = items[focusedIndex]!;
|
|
151
|
-
|
|
152
|
-
if (!curIsActive) {
|
|
118
|
+
if (cur.style.color !== DROPDOWN_ITEM_ACTIVE_COLOR) {
|
|
153
119
|
cur.style.backgroundColor = DROPDOWN_ITEM_HOVER_BG;
|
|
154
120
|
}
|
|
155
121
|
cur.scrollIntoView({ block: "nearest" });
|
|
@@ -161,13 +127,11 @@ export function showDropdown(
|
|
|
161
127
|
if (e.key === "ArrowDown") {
|
|
162
128
|
e.preventDefault();
|
|
163
129
|
e.stopPropagation();
|
|
164
|
-
|
|
165
|
-
setFocusedItem(next);
|
|
130
|
+
setFocusedItem(focusedIndex < items.length - 1 ? focusedIndex + 1 : 0);
|
|
166
131
|
} else if (e.key === "ArrowUp") {
|
|
167
132
|
e.preventDefault();
|
|
168
133
|
e.stopPropagation();
|
|
169
|
-
|
|
170
|
-
setFocusedItem(prev);
|
|
134
|
+
setFocusedItem(focusedIndex > 0 ? focusedIndex - 1 : items.length - 1);
|
|
171
135
|
} else if (e.key === "Enter" && focusedIndex >= 0) {
|
|
172
136
|
e.preventDefault();
|
|
173
137
|
e.stopPropagation();
|
|
@@ -177,9 +141,12 @@ export function showDropdown(
|
|
|
177
141
|
}
|
|
178
142
|
};
|
|
179
143
|
document.addEventListener("keydown", activeKeydownHandler, true);
|
|
144
|
+
}
|
|
180
145
|
|
|
181
|
-
|
|
182
|
-
|
|
146
|
+
function setupOutsideClickHandler(
|
|
147
|
+
dropdown: HTMLDivElement,
|
|
148
|
+
label: HTMLDivElement
|
|
149
|
+
): void {
|
|
183
150
|
setTimeout(() => {
|
|
184
151
|
outsideMousedownHandler = (e: MouseEvent) => {
|
|
185
152
|
const target = e.target as Node;
|
|
@@ -191,8 +158,54 @@ export function showDropdown(
|
|
|
191
158
|
}, 0);
|
|
192
159
|
}
|
|
193
160
|
|
|
161
|
+
/** Show the dropdown below the label element */
|
|
162
|
+
export function showDropdown(
|
|
163
|
+
label: HTMLDivElement,
|
|
164
|
+
layers: LayerInfo[],
|
|
165
|
+
currentSelectorId: string | null,
|
|
166
|
+
onSelect: OnLayerSelect,
|
|
167
|
+
onHover?: OnLayerHover,
|
|
168
|
+
onHoverEnd?: OnLayerHoverEnd
|
|
169
|
+
): void {
|
|
170
|
+
closeDropdown();
|
|
171
|
+
|
|
172
|
+
const dropdown = createDropdownElement(
|
|
173
|
+
layers,
|
|
174
|
+
currentSelectorId,
|
|
175
|
+
(layer) => {
|
|
176
|
+
if (onHoverEnd) onHoverEnd();
|
|
177
|
+
onSelect(layer);
|
|
178
|
+
closeDropdown();
|
|
179
|
+
},
|
|
180
|
+
onHover,
|
|
181
|
+
onHoverEnd
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const overlay = label.parentElement;
|
|
185
|
+
if (!overlay) return;
|
|
186
|
+
|
|
187
|
+
dropdown.style.top = `${label.offsetTop + label.offsetHeight + 2}px`;
|
|
188
|
+
dropdown.style.left = `${label.offsetLeft}px`;
|
|
189
|
+
|
|
190
|
+
overlay.appendChild(dropdown);
|
|
191
|
+
activeDropdown = dropdown;
|
|
192
|
+
activeLabel = label;
|
|
193
|
+
if (label.textContent?.endsWith(LABEL_CHEVRON.trim())) {
|
|
194
|
+
label.textContent = label.textContent.slice(0, -LABEL_CHEVRON.length) + LABEL_CHEVRON_OPEN;
|
|
195
|
+
}
|
|
196
|
+
activeOnHoverEnd = onHoverEnd ?? null;
|
|
197
|
+
|
|
198
|
+
setupKeyboardNavigation(dropdown, layers, currentSelectorId, onSelect, onHover, onHoverEnd);
|
|
199
|
+
setupOutsideClickHandler(dropdown, label);
|
|
200
|
+
}
|
|
201
|
+
|
|
194
202
|
/** Close the active dropdown and clean up listeners */
|
|
195
203
|
export function closeDropdown(): void {
|
|
204
|
+
if (activeLabel?.textContent?.includes(LABEL_CHEVRON_OPEN)) {
|
|
205
|
+
activeLabel.textContent = activeLabel.textContent.replace(LABEL_CHEVRON_OPEN, LABEL_CHEVRON);
|
|
206
|
+
}
|
|
207
|
+
activeLabel = null;
|
|
208
|
+
|
|
196
209
|
if (activeOnHoverEnd) {
|
|
197
210
|
activeOnHoverEnd();
|
|
198
211
|
activeOnHoverEnd = null;
|
|
@@ -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
|
+
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
/** DOM
|
|
1
|
+
/** DOM utilities for the layer-dropdown module */
|
|
2
2
|
|
|
3
3
|
import { isInstrumentedElement, getElementSelectorId } from "../utils.js";
|
|
4
4
|
import { MAX_PARENT_DEPTH, MAX_CHILD_DEPTH } from "./consts.js";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
11
|
}
|
|
12
12
|
|
|
13
13
|
/** Display name for a layer — just the real tag name */
|
|
@@ -47,17 +47,18 @@ export function getInstrumentedDescendants(
|
|
|
47
47
|
return result;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
50
|
+
function toLayerInfo(element: Element, depth?: number): LayerInfo {
|
|
51
|
+
const info: LayerInfo = {
|
|
52
|
+
element,
|
|
53
|
+
tagName: element.tagName.toLowerCase(),
|
|
54
|
+
selectorId: getElementSelectorId(element),
|
|
55
|
+
};
|
|
56
|
+
if (depth !== undefined) info.depth = depth;
|
|
57
|
+
return info;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Collect instrumented ancestors from selected element up to MAX_PARENT_DEPTH (outermost first). */
|
|
61
|
+
function collectInstrumentedParents(selectedElement: Element): LayerInfo[] {
|
|
61
62
|
const parents: LayerInfo[] = [];
|
|
62
63
|
let current = selectedElement.parentElement;
|
|
63
64
|
while (
|
|
@@ -67,44 +68,90 @@ export function buildLayerChain(selectedElement: Element): LayerInfo[] {
|
|
|
67
68
|
parents.length < MAX_PARENT_DEPTH
|
|
68
69
|
) {
|
|
69
70
|
if (isInstrumentedElement(current)) {
|
|
70
|
-
parents.push(
|
|
71
|
-
element: current,
|
|
72
|
-
tagName: current.tagName.toLowerCase(),
|
|
73
|
-
selectorId: getElementSelectorId(current),
|
|
74
|
-
});
|
|
71
|
+
parents.push(toLayerInfo(current));
|
|
75
72
|
}
|
|
76
73
|
current = current.parentElement;
|
|
77
74
|
}
|
|
78
|
-
// Reverse so outermost parent comes first
|
|
79
75
|
parents.reverse();
|
|
76
|
+
return parents;
|
|
77
|
+
}
|
|
80
78
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const baseDepth = 0;
|
|
84
|
-
|
|
85
|
-
// Parents: depth 0, 1, …
|
|
79
|
+
/** Append parents to chain with depth 0, 1, …; returns depth of selected (parents.length). */
|
|
80
|
+
function appendParentsWithDepth(chain: LayerInfo[], parents: LayerInfo[]): number {
|
|
86
81
|
parents.forEach((p, i) => {
|
|
87
|
-
chain.push({ ...p, depth:
|
|
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,
|
|
82
|
+
chain.push({ ...p, depth: i });
|
|
97
83
|
});
|
|
84
|
+
return parents.length;
|
|
85
|
+
}
|
|
98
86
|
|
|
99
|
-
|
|
87
|
+
/** Append selected element and its descendants at the given depth. */
|
|
88
|
+
function appendSelfAndDescendants(
|
|
89
|
+
chain: LayerInfo[],
|
|
90
|
+
selectedElement: Element,
|
|
91
|
+
selfDepth: number
|
|
92
|
+
): void {
|
|
93
|
+
chain.push(toLayerInfo(selectedElement, selfDepth));
|
|
100
94
|
const descendants = getInstrumentedDescendants(
|
|
101
95
|
selectedElement,
|
|
102
96
|
MAX_CHILD_DEPTH
|
|
103
97
|
);
|
|
104
|
-
// Assign visual depth: we need to track the instrumented nesting to set depth correctly
|
|
105
98
|
assignDescendantDepths(selectedElement, descendants, selfDepth + 1);
|
|
106
|
-
|
|
107
99
|
chain.push(...descendants);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Get the innermost instrumented parent's DOM element, or null if none. */
|
|
103
|
+
function getImmediateInstrParent(parents: LayerInfo[]): Element | null {
|
|
104
|
+
return parents.length > 0 ? parents[parents.length - 1]!.element : null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Collect instrumented siblings of the selected element from its parent (DOM order). */
|
|
108
|
+
function collectSiblings(parent: Element, selectedElement: Element): LayerInfo[] {
|
|
109
|
+
const siblings = getInstrumentedDescendants(parent, 1);
|
|
110
|
+
if (!siblings.some((s) => s.element === selectedElement)) {
|
|
111
|
+
siblings.push(toLayerInfo(selectedElement));
|
|
112
|
+
}
|
|
113
|
+
return siblings;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Append siblings at selfDepth, expanding children only for the selected element. */
|
|
117
|
+
function appendSiblingsWithSelected(
|
|
118
|
+
chain: LayerInfo[],
|
|
119
|
+
siblings: LayerInfo[],
|
|
120
|
+
selectedElement: Element,
|
|
121
|
+
selfDepth: number
|
|
122
|
+
): void {
|
|
123
|
+
for (const sibling of siblings) {
|
|
124
|
+
if (sibling.element === selectedElement) {
|
|
125
|
+
appendSelfAndDescendants(chain, selectedElement, selfDepth);
|
|
126
|
+
} else {
|
|
127
|
+
chain.push({ ...sibling, depth: selfDepth });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Build the layer chain for the dropdown:
|
|
134
|
+
*
|
|
135
|
+
* Parents – up to MAX_PARENT_DEPTH instrumented ancestors, outer → inner.
|
|
136
|
+
* Siblings – instrumented children of the immediate parent, at the same depth.
|
|
137
|
+
* Current – the selected element (highlighted), with children expanded.
|
|
138
|
+
* Children – instrumented descendants within MAX_CHILD_DEPTH levels, DOM order.
|
|
139
|
+
*
|
|
140
|
+
* Each item carries a `depth` for visual indentation.
|
|
141
|
+
*/
|
|
142
|
+
export function buildLayerChain(selectedElement: Element): LayerInfo[] {
|
|
143
|
+
const parents = collectInstrumentedParents(selectedElement);
|
|
144
|
+
const chain: LayerInfo[] = [];
|
|
145
|
+
const selfDepth = appendParentsWithDepth(chain, parents);
|
|
146
|
+
|
|
147
|
+
const instrParent = getImmediateInstrParent(parents);
|
|
148
|
+
if (instrParent) {
|
|
149
|
+
const siblings = collectSiblings(instrParent, selectedElement);
|
|
150
|
+
appendSiblingsWithSelected(chain, siblings, selectedElement, selfDepth);
|
|
151
|
+
} else {
|
|
152
|
+
appendSelfAndDescendants(chain, selectedElement, selfDepth);
|
|
153
|
+
}
|
|
154
|
+
|
|
108
155
|
return chain;
|
|
109
156
|
}
|
|
110
157
|
|
package/src/injections/utils.ts
CHANGED
|
@@ -16,22 +16,6 @@ export function getElementSelectorId(element: Element): string | null {
|
|
|
16
16
|
);
|
|
17
17
|
}
|
|
18
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
|
-
|
|
35
19
|
/** Find elements by ID - first try data-source-location, fallback to data-visual-selector-id */
|
|
36
20
|
export function findElementsById(id: string | null): Element[] {
|
|
37
21
|
if (!id) return [];
|