@treelocator/runtime 0.1.5 → 0.1.8
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/.turbo/turbo-build.log +13 -13
- package/.turbo/turbo-test.log +11 -10
- package/dist/adapters/HtmlElementTreeNode.d.ts +1 -0
- package/dist/adapters/HtmlElementTreeNode.js +4 -1
- package/dist/adapters/adapterApi.d.ts +1 -1
- package/dist/adapters/jsx/getExpressionData.d.ts +1 -1
- package/dist/adapters/jsx/getExpressionData.js +8 -4
- package/dist/adapters/jsx/getJSXComponentBoundingBox.d.ts +1 -1
- package/dist/adapters/jsx/getJSXComponentBoundingBox.js +11 -6
- package/dist/adapters/jsx/jsxAdapter.js +13 -8
- package/dist/adapters/react/reactAdapter.d.ts +9 -0
- package/dist/adapters/react/reactAdapter.js +48 -10
- package/dist/components/Runtime.js +77 -27
- package/dist/functions/formatAncestryChain.d.ts +7 -0
- package/dist/functions/formatAncestryChain.js +34 -7
- package/dist/functions/formatAncestryChain.test.js +41 -0
- package/dist/types/TreeNode.d.ts +6 -0
- package/package.json +2 -2
- package/src/adapters/HtmlElementTreeNode.ts +5 -2
- package/src/adapters/adapterApi.ts +1 -1
- package/src/adapters/jsx/getExpressionData.ts +9 -5
- package/src/adapters/jsx/getJSXComponentBoundingBox.ts +13 -8
- package/src/adapters/jsx/jsxAdapter.ts +14 -12
- package/src/adapters/react/reactAdapter.ts +54 -16
- package/src/components/Runtime.tsx +87 -28
- package/src/functions/formatAncestryChain.test.ts +46 -0
- package/src/functions/formatAncestryChain.ts +45 -8
- package/src/types/TreeNode.ts +6 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
|
|
2
|
-
> @treelocator/runtime@0.1.
|
|
2
|
+
> @treelocator/runtime@0.1.8 build /Users/wende/projects/locatorjs/packages/runtime
|
|
3
3
|
> concurrently pnpm:build:*
|
|
4
4
|
|
|
5
|
-
[ts]
|
|
6
|
-
[ts] > @treelocator/runtime@0.1.5 build:ts /Users/wende/projects/locatorjs/packages/runtime
|
|
7
|
-
[ts] > tsc --declaration --emitDeclarationOnly --noEmit false --outDir dist
|
|
8
|
-
[ts]
|
|
9
5
|
[tailwind]
|
|
10
|
-
[tailwind] > @treelocator/runtime@0.1.
|
|
6
|
+
[tailwind] > @treelocator/runtime@0.1.8 build:tailwind /Users/wende/projects/locatorjs/packages/runtime
|
|
11
7
|
[tailwind] > tailwindcss -i ./src/main.css -o ./dist/output.css
|
|
12
8
|
[tailwind]
|
|
13
|
-
[wrapImage]
|
|
14
|
-
[wrapImage] > @treelocator/runtime@0.1.5 build:wrapImage /Users/wende/projects/locatorjs/packages/runtime
|
|
15
|
-
[wrapImage] > node ./scripts/wrapImage.js
|
|
16
|
-
[wrapImage]
|
|
17
9
|
[babel]
|
|
18
|
-
[babel] > @treelocator/runtime@0.1.
|
|
10
|
+
[babel] > @treelocator/runtime@0.1.8 build:babel /Users/wende/projects/locatorjs/packages/runtime
|
|
19
11
|
[babel] > babel src --out-dir dist --extensions .js,.jsx,.ts,.tsx
|
|
20
12
|
[babel]
|
|
13
|
+
[ts]
|
|
14
|
+
[ts] > @treelocator/runtime@0.1.8 build:ts /Users/wende/projects/locatorjs/packages/runtime
|
|
15
|
+
[ts] > tsc --declaration --emitDeclarationOnly --noEmit false --outDir dist
|
|
16
|
+
[ts]
|
|
17
|
+
[wrapImage]
|
|
18
|
+
[wrapImage] > @treelocator/runtime@0.1.8 build:wrapImage /Users/wende/projects/locatorjs/packages/runtime
|
|
19
|
+
[wrapImage] > node ./scripts/wrapImage.js
|
|
20
|
+
[wrapImage]
|
|
21
21
|
[wrapImage] Tree icon file generated
|
|
22
22
|
[wrapImage] pnpm run build:wrapImage exited with code 0
|
|
23
23
|
[tailwind]
|
|
24
24
|
[tailwind] Rebuilding...
|
|
25
25
|
[tailwind]
|
|
26
|
-
[tailwind] Done in
|
|
26
|
+
[tailwind] Done in 164ms.
|
|
27
27
|
[tailwind] pnpm run build:tailwind exited with code 0
|
|
28
|
-
[babel] Successfully compiled 72 files with Babel (
|
|
28
|
+
[babel] Successfully compiled 72 files with Babel (409ms).
|
|
29
29
|
[babel] pnpm run build:babel exited with code 0
|
|
30
30
|
[ts] pnpm run build:ts exited with code 0
|
package/.turbo/turbo-test.log
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
|
|
2
|
-
> @treelocator/runtime@0.1.
|
|
2
|
+
> @treelocator/runtime@0.1.5 test /Users/wende/projects/locatorjs/packages/runtime
|
|
3
3
|
> vitest run
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
RUN v2.1.9 /Users/wende/projects/locatorjs/packages/runtime
|
|
7
7
|
|
|
8
|
-
✓ src/functions/
|
|
9
|
-
✓ src/functions/evalTemplate.test.ts (1 test)
|
|
10
|
-
✓ src/functions/transformPath.test.ts (3 tests)
|
|
11
|
-
✓ src/functions/
|
|
12
|
-
✓ src/functions/
|
|
8
|
+
✓ src/functions/mergeRects.test.ts (1 test) 2ms
|
|
9
|
+
✓ src/functions/evalTemplate.test.ts (1 test) 2ms
|
|
10
|
+
✓ src/functions/transformPath.test.ts (3 tests) 3ms
|
|
11
|
+
✓ src/functions/getUsableFileName.test.tsx (4 tests) 3ms
|
|
12
|
+
✓ src/functions/formatAncestryChain.test.ts (7 tests) 2ms
|
|
13
|
+
✓ src/functions/cropPath.test.ts (2 tests) 2ms
|
|
13
14
|
|
|
14
|
-
Test Files
|
|
15
|
-
Tests
|
|
16
|
-
Start at
|
|
17
|
-
Duration
|
|
15
|
+
Test Files 6 passed (6)
|
|
16
|
+
Tests 18 passed (18)
|
|
17
|
+
Start at 17:17:22
|
|
18
|
+
Duration 718ms (transform 282ms, setup 0ms, collect 336ms, tests 14ms, environment 1ms, prepare 463ms)
|
|
18
19
|
|
|
@@ -18,7 +18,7 @@ export class HtmlElementTreeNode {
|
|
|
18
18
|
getChildren() {
|
|
19
19
|
const children = Array.from(this.element.children);
|
|
20
20
|
return children.map(child => {
|
|
21
|
-
if (child instanceof HTMLElement) {
|
|
21
|
+
if (child instanceof HTMLElement || child instanceof SVGElement) {
|
|
22
22
|
// @ts-ignore
|
|
23
23
|
return new this.constructor(child);
|
|
24
24
|
} else {
|
|
@@ -40,4 +40,7 @@ export class HtmlElementTreeNode {
|
|
|
40
40
|
getComponent() {
|
|
41
41
|
throw new Error("Method not implemented.");
|
|
42
42
|
}
|
|
43
|
+
getOwnerComponents() {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
43
46
|
}
|
|
@@ -8,7 +8,7 @@ export type ElementInfo = {
|
|
|
8
8
|
};
|
|
9
9
|
export type FullElementInfo = {
|
|
10
10
|
thisElement: ElementInfo;
|
|
11
|
-
htmlElement: HTMLElement;
|
|
11
|
+
htmlElement: HTMLElement | SVGElement;
|
|
12
12
|
parentElements: ElementInfo[];
|
|
13
13
|
componentBox: SimpleDOMRect;
|
|
14
14
|
componentsLabels: LabelData[];
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { ExpressionInfo, FileStorage } from "@locator/shared";
|
|
2
|
-
export declare function getExpressionData(target:
|
|
2
|
+
export declare function getExpressionData(target: Element, fileData: FileStorage | null): ExpressionInfo | null;
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { parseDataId, parseDataPath } from "../../functions/parseDataId";
|
|
2
2
|
export function getExpressionData(target, fileData) {
|
|
3
|
+
// Use getAttribute instead of dataset to support both HTML and SVG elements
|
|
4
|
+
const dataLocatorjs = target.getAttribute("data-locatorjs");
|
|
5
|
+
const dataLocatorjsId = target.getAttribute("data-locatorjs-id");
|
|
6
|
+
|
|
3
7
|
// First check for data-locatorjs (path-based, for server components)
|
|
4
|
-
if (
|
|
5
|
-
const parsed = parseDataPath(
|
|
8
|
+
if (dataLocatorjs) {
|
|
9
|
+
const parsed = parseDataPath(dataLocatorjs);
|
|
6
10
|
if (parsed) {
|
|
7
11
|
const [, line, column] = parsed;
|
|
8
12
|
|
|
@@ -33,8 +37,8 @@ export function getExpressionData(target, fileData) {
|
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
// Fall back to data-locatorjs-id (ID-based, traditional approach)
|
|
36
|
-
if (
|
|
37
|
-
const [, id] = parseDataId(
|
|
40
|
+
if (dataLocatorjsId && fileData) {
|
|
41
|
+
const [, id] = parseDataId(dataLocatorjsId);
|
|
38
42
|
const expData = fileData.expressions[Number(id)];
|
|
39
43
|
if (expData) {
|
|
40
44
|
return expData;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { FileStorage } from "@locator/shared";
|
|
2
2
|
import type { SimpleDOMRect } from "../../types/types";
|
|
3
|
-
export declare function getJSXComponentBoundingBox(found:
|
|
3
|
+
export declare function getJSXComponentBoundingBox(found: Element, locatorData: {
|
|
4
4
|
[filename: string]: FileStorage;
|
|
5
5
|
}, componentFolder: string, componentId: number): SimpleDOMRect;
|
|
@@ -9,19 +9,24 @@ export function getJSXComponentBoundingBox(found, locatorData, componentFolder,
|
|
|
9
9
|
if (!parent) {
|
|
10
10
|
return;
|
|
11
11
|
}
|
|
12
|
-
|
|
12
|
+
// Support both HTMLElement and SVGElement
|
|
13
|
+
if (parent instanceof HTMLElement || parent instanceof SVGElement) {
|
|
14
|
+
// Use getAttribute instead of dataset to support both HTML and SVG elements
|
|
15
|
+
const dataLocatorjs = parent.getAttribute("data-locatorjs");
|
|
16
|
+
const dataLocatorjsId = parent.getAttribute("data-locatorjs-id");
|
|
17
|
+
|
|
13
18
|
// Check for either data-locatorjs (path-based) or data-locatorjs-id (ID-based)
|
|
14
|
-
if (
|
|
19
|
+
if (dataLocatorjs || dataLocatorjsId) {
|
|
15
20
|
let fileFullPath;
|
|
16
|
-
if (
|
|
17
|
-
const parsed = parseDataPath(
|
|
21
|
+
if (dataLocatorjs) {
|
|
22
|
+
const parsed = parseDataPath(dataLocatorjs);
|
|
18
23
|
if (!parsed) {
|
|
19
24
|
goParent(parent);
|
|
20
25
|
return;
|
|
21
26
|
}
|
|
22
27
|
[fileFullPath] = parsed;
|
|
23
|
-
} else if (
|
|
24
|
-
[fileFullPath] = parseDataId(
|
|
28
|
+
} else if (dataLocatorjsId) {
|
|
29
|
+
[fileFullPath] = parseDataId(dataLocatorjsId);
|
|
25
30
|
} else {
|
|
26
31
|
goParent(parent);
|
|
27
32
|
return;
|
|
@@ -5,10 +5,13 @@ import { getExpressionData } from "./getExpressionData";
|
|
|
5
5
|
import { getJSXComponentBoundingBox } from "./getJSXComponentBoundingBox";
|
|
6
6
|
export function getElementInfo(target) {
|
|
7
7
|
const found = target.closest("[data-locatorjs-id], [data-locatorjs]");
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
|
|
9
|
+
// Support both HTMLElement and SVGElement
|
|
10
|
+
// SVG elements don't have dataset, so use getAttribute instead
|
|
11
|
+
const dataId = found?.getAttribute("data-locatorjs-id");
|
|
12
|
+
const dataPath = found?.getAttribute("data-locatorjs");
|
|
13
|
+
const styledDataId = found?.getAttribute("data-locatorjs-styled");
|
|
14
|
+
if (found && (found instanceof HTMLElement || found instanceof SVGElement) && (dataId || dataPath || styledDataId)) {
|
|
12
15
|
if (!dataId && !dataPath) {
|
|
13
16
|
return null;
|
|
14
17
|
}
|
|
@@ -89,8 +92,9 @@ export function getElementInfo(target) {
|
|
|
89
92
|
}
|
|
90
93
|
export class JSXTreeNodeElement extends HtmlElementTreeNode {
|
|
91
94
|
getSource() {
|
|
92
|
-
|
|
93
|
-
const
|
|
95
|
+
// Use getAttribute instead of dataset to support both HTML and SVG elements
|
|
96
|
+
const dataId = this.element.getAttribute("data-locatorjs-id");
|
|
97
|
+
const dataPath = this.element.getAttribute("data-locatorjs");
|
|
94
98
|
if (!dataId && !dataPath) {
|
|
95
99
|
return null;
|
|
96
100
|
}
|
|
@@ -131,8 +135,9 @@ export class JSXTreeNodeElement extends HtmlElementTreeNode {
|
|
|
131
135
|
return null;
|
|
132
136
|
}
|
|
133
137
|
getComponent() {
|
|
134
|
-
|
|
135
|
-
const
|
|
138
|
+
// Use getAttribute instead of dataset to support both HTML and SVG elements
|
|
139
|
+
const dataId = this.element.getAttribute("data-locatorjs-id");
|
|
140
|
+
const dataPath = this.element.getAttribute("data-locatorjs");
|
|
136
141
|
if (!dataId && !dataPath) {
|
|
137
142
|
return null;
|
|
138
143
|
}
|
|
@@ -5,7 +5,16 @@ import { HtmlElementTreeNode } from "../HtmlElementTreeNode";
|
|
|
5
5
|
export declare function getElementInfo(found: HTMLElement): FullElementInfo | null;
|
|
6
6
|
export declare class ReactTreeNodeElement extends HtmlElementTreeNode {
|
|
7
7
|
getSource(): Source | null;
|
|
8
|
+
private fiberToTreeNodeComponent;
|
|
8
9
|
getComponent(): TreeNodeComponent | null;
|
|
10
|
+
/**
|
|
11
|
+
* Traverse the _debugOwner chain to collect all owner components.
|
|
12
|
+
* This finds wrapper components (like Sidebar) that don't render their own DOM elements
|
|
13
|
+
* but wrap other components (like GlassPanel) that do.
|
|
14
|
+
*
|
|
15
|
+
* Returns array from outermost owner (Sidebar) to innermost (GlassPanel).
|
|
16
|
+
*/
|
|
17
|
+
getOwnerComponents(): TreeNodeComponent[];
|
|
9
18
|
}
|
|
10
19
|
declare const reactAdapter: AdapterObject;
|
|
11
20
|
export default reactAdapter;
|
|
@@ -63,23 +63,61 @@ export class ReactTreeNodeElement extends HtmlElementTreeNode {
|
|
|
63
63
|
}
|
|
64
64
|
return null;
|
|
65
65
|
}
|
|
66
|
+
fiberToTreeNodeComponent(fiber) {
|
|
67
|
+
const fiberLabel = getFiberLabel(fiber, findDebugSource(fiber)?.source);
|
|
68
|
+
return {
|
|
69
|
+
label: fiberLabel.label,
|
|
70
|
+
callLink: fiberLabel.link && {
|
|
71
|
+
fileName: fiberLabel.link.filePath,
|
|
72
|
+
lineNumber: fiberLabel.link.line,
|
|
73
|
+
columnNumber: fiberLabel.link.column,
|
|
74
|
+
projectPath: fiberLabel.link.projectPath
|
|
75
|
+
} || undefined
|
|
76
|
+
};
|
|
77
|
+
}
|
|
66
78
|
getComponent() {
|
|
67
79
|
const fiber = findFiberByHtmlElement(this.element, false);
|
|
68
80
|
const componentFiber = fiber?._debugOwner;
|
|
69
81
|
if (componentFiber) {
|
|
70
|
-
|
|
71
|
-
return {
|
|
72
|
-
label: fiberLabel.label,
|
|
73
|
-
callLink: fiberLabel.link && {
|
|
74
|
-
fileName: fiberLabel.link.filePath,
|
|
75
|
-
lineNumber: fiberLabel.link.line,
|
|
76
|
-
columnNumber: fiberLabel.link.column,
|
|
77
|
-
projectPath: fiberLabel.link.projectPath
|
|
78
|
-
} || undefined
|
|
79
|
-
};
|
|
82
|
+
return this.fiberToTreeNodeComponent(componentFiber);
|
|
80
83
|
}
|
|
81
84
|
return null;
|
|
82
85
|
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Traverse the _debugOwner chain to collect all owner components.
|
|
89
|
+
* This finds wrapper components (like Sidebar) that don't render their own DOM elements
|
|
90
|
+
* but wrap other components (like GlassPanel) that do.
|
|
91
|
+
*
|
|
92
|
+
* Returns array from outermost owner (Sidebar) to innermost (GlassPanel).
|
|
93
|
+
*/
|
|
94
|
+
getOwnerComponents() {
|
|
95
|
+
const fiber = findFiberByHtmlElement(this.element, false);
|
|
96
|
+
if (!fiber) return [];
|
|
97
|
+
|
|
98
|
+
// Get the parent DOM element's owner to know when to stop
|
|
99
|
+
const parentElement = this.element.parentElement;
|
|
100
|
+
let parentOwnerFiber = null;
|
|
101
|
+
if (parentElement) {
|
|
102
|
+
const parentFiber = findFiberByHtmlElement(parentElement, false);
|
|
103
|
+
parentOwnerFiber = parentFiber?._debugOwner || null;
|
|
104
|
+
}
|
|
105
|
+
const components = [];
|
|
106
|
+
let currentFiber = fiber._debugOwner;
|
|
107
|
+
|
|
108
|
+
// Traverse up the _debugOwner chain until we hit the parent's owner or run out
|
|
109
|
+
while (currentFiber) {
|
|
110
|
+
// Stop if we've reached the parent DOM element's owner component
|
|
111
|
+
if (parentOwnerFiber && currentFiber === parentOwnerFiber) {
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
components.push(this.fiberToTreeNodeComponent(currentFiber));
|
|
115
|
+
currentFiber = currentFiber._debugOwner || null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Reverse so outermost (Sidebar) comes first, innermost (GlassPanel) last
|
|
119
|
+
return components.reverse();
|
|
120
|
+
}
|
|
83
121
|
}
|
|
84
122
|
function getTree(element) {
|
|
85
123
|
const originalRoot = new ReactTreeNodeElement(element);
|
|
@@ -39,14 +39,36 @@ function Runtime(props) {
|
|
|
39
39
|
setHoldingModKey(e.altKey);
|
|
40
40
|
}
|
|
41
41
|
function mouseOverListener(e) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
// Also update modifier state
|
|
43
|
+
setHoldingModKey(e.altKey);
|
|
44
|
+
|
|
45
|
+
// Use elementsFromPoint to find elements including ones with pointer-events-none
|
|
46
|
+
const elementsAtPoint = document.elementsFromPoint(e.clientX, e.clientY);
|
|
47
|
+
|
|
48
|
+
// Find the topmost element with locator data for highlighting
|
|
49
|
+
let element = null;
|
|
50
|
+
for (const el of elementsAtPoint) {
|
|
51
|
+
if (isLocatorsOwnElement(el)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (el instanceof HTMLElement || el instanceof SVGElement) {
|
|
55
|
+
const withLocator = el.closest('[data-locatorjs-id], [data-locatorjs]');
|
|
56
|
+
if (withLocator) {
|
|
57
|
+
element = withLocator;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Fallback to e.target
|
|
64
|
+
if (!element) {
|
|
65
|
+
const target = e.target;
|
|
66
|
+
if (target && (target instanceof HTMLElement || target instanceof SVGElement)) {
|
|
67
|
+
element = target instanceof SVGElement ? target.closest('[data-locatorjs-id], [data-locatorjs]') ?? target.closest('svg') ?? target : target;
|
|
46
68
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
69
|
+
}
|
|
70
|
+
if (element && !isLocatorsOwnElement(element)) {
|
|
71
|
+
setCurrentElement(element);
|
|
50
72
|
}
|
|
51
73
|
}
|
|
52
74
|
function mouseDownUpListener(e) {
|
|
@@ -62,31 +84,59 @@ function Runtime(props) {
|
|
|
62
84
|
if (!e.altKey && !isCombinationModifiersPressed(e) && !locatorActive()) {
|
|
63
85
|
return;
|
|
64
86
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
87
|
+
|
|
88
|
+
// Use elementsFromPoint to find all elements at click position,
|
|
89
|
+
// including ones with pointer-events-none (like canvas overlays)
|
|
90
|
+
const elementsAtPoint = document.elementsFromPoint(e.clientX, e.clientY);
|
|
91
|
+
|
|
92
|
+
// Find the topmost element with locator data
|
|
93
|
+
let element = null;
|
|
94
|
+
for (const el of elementsAtPoint) {
|
|
95
|
+
if (isLocatorsOwnElement(el)) {
|
|
96
|
+
continue;
|
|
69
97
|
}
|
|
70
|
-
if (
|
|
71
|
-
|
|
98
|
+
if (el instanceof HTMLElement || el instanceof SVGElement) {
|
|
99
|
+
// Check if this element or its closest ancestor has locator data
|
|
100
|
+
const withLocator = el.closest('[data-locatorjs-id], [data-locatorjs]');
|
|
101
|
+
if (withLocator) {
|
|
102
|
+
element = withLocator;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
72
105
|
}
|
|
73
|
-
|
|
74
|
-
e.stopPropagation();
|
|
106
|
+
}
|
|
75
107
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
navigator.clipboard.writeText(formatted).then(() => {
|
|
82
|
-
setToastMessage("Copied to clipboard");
|
|
83
|
-
});
|
|
108
|
+
// Fallback to e.target if elementsFromPoint didn't find anything
|
|
109
|
+
if (!element) {
|
|
110
|
+
const target = e.target;
|
|
111
|
+
if (target && (target instanceof HTMLElement || target instanceof SVGElement)) {
|
|
112
|
+
element = target instanceof SVGElement ? target.closest('[data-locatorjs-id], [data-locatorjs]') ?? target.closest('svg') ?? target : target;
|
|
84
113
|
}
|
|
114
|
+
}
|
|
115
|
+
if (!element) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (element instanceof HTMLElement && element.shadowRoot) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (isLocatorsOwnElement(element)) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
e.preventDefault();
|
|
125
|
+
e.stopPropagation();
|
|
85
126
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
127
|
+
// Copy ancestry to clipboard on alt+click
|
|
128
|
+
const treeNode = createTreeNode(element, props.adapterId);
|
|
129
|
+
if (treeNode) {
|
|
130
|
+
const ancestry = collectAncestry(treeNode);
|
|
131
|
+
const formatted = formatAncestryChain(ancestry);
|
|
132
|
+
navigator.clipboard.writeText(formatted).then(() => {
|
|
133
|
+
setToastMessage("Copied to clipboard");
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Deactivate toggle after click
|
|
138
|
+
if (locatorActive()) {
|
|
139
|
+
setLocatorActive(false);
|
|
90
140
|
}
|
|
91
141
|
}
|
|
92
142
|
function scrollListener() {
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { TreeNode } from "../types/TreeNode";
|
|
2
|
+
export interface OwnerComponentInfo {
|
|
3
|
+
name: string;
|
|
4
|
+
filePath?: string;
|
|
5
|
+
line?: number;
|
|
6
|
+
}
|
|
2
7
|
export interface AncestryItem {
|
|
3
8
|
elementName: string;
|
|
4
9
|
componentName?: string;
|
|
@@ -6,6 +11,8 @@ export interface AncestryItem {
|
|
|
6
11
|
line?: number;
|
|
7
12
|
id?: string;
|
|
8
13
|
nthChild?: number;
|
|
14
|
+
/** All owner components from outermost (Sidebar) to innermost (GlassPanel) */
|
|
15
|
+
ownerComponents?: OwnerComponentInfo[];
|
|
9
16
|
}
|
|
10
17
|
export declare function collectAncestry(node: TreeNode): AncestryItem[];
|
|
11
18
|
export declare function formatAncestryChain(items: AncestryItem[]): string;
|
|
@@ -19,6 +19,13 @@ function getNthChildIfAmbiguous(element) {
|
|
|
19
19
|
const index = siblings.indexOf(element);
|
|
20
20
|
return index + 1; // 1-indexed for CSS nth-child compatibility
|
|
21
21
|
}
|
|
22
|
+
function treeNodeComponentToOwnerInfo(comp) {
|
|
23
|
+
return {
|
|
24
|
+
name: comp.label,
|
|
25
|
+
filePath: comp.callLink?.fileName,
|
|
26
|
+
line: comp.callLink?.lineNumber
|
|
27
|
+
};
|
|
28
|
+
}
|
|
22
29
|
export function collectAncestry(node) {
|
|
23
30
|
const items = [];
|
|
24
31
|
let current = node;
|
|
@@ -28,7 +35,6 @@ export function collectAncestry(node) {
|
|
|
28
35
|
current = current.getParent();
|
|
29
36
|
continue;
|
|
30
37
|
}
|
|
31
|
-
const component = current.getComponent();
|
|
32
38
|
const source = current.getSource();
|
|
33
39
|
const item = {
|
|
34
40
|
elementName: current.name
|
|
@@ -47,11 +53,27 @@ export function collectAncestry(node) {
|
|
|
47
53
|
}
|
|
48
54
|
}
|
|
49
55
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
|
|
57
|
+
// Get all owner components (from outermost like Sidebar to innermost like GlassPanel)
|
|
58
|
+
const ownerComponents = current.getOwnerComponents();
|
|
59
|
+
const outermost = ownerComponents[0];
|
|
60
|
+
if (outermost) {
|
|
61
|
+
item.ownerComponents = ownerComponents.map(treeNodeComponentToOwnerInfo);
|
|
62
|
+
// Use outermost component as the primary component name
|
|
63
|
+
item.componentName = outermost.label;
|
|
64
|
+
if (outermost.callLink) {
|
|
65
|
+
item.filePath = outermost.callLink.fileName;
|
|
66
|
+
item.line = outermost.callLink.lineNumber;
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
// Fallback to single component if getOwnerComponents not available
|
|
70
|
+
const component = current.getComponent();
|
|
71
|
+
if (component) {
|
|
72
|
+
item.componentName = component.label;
|
|
73
|
+
if (component.callLink) {
|
|
74
|
+
item.filePath = component.callLink.fileName;
|
|
75
|
+
item.line = component.callLink.lineNumber;
|
|
76
|
+
}
|
|
55
77
|
}
|
|
56
78
|
}
|
|
57
79
|
if (!item.filePath && source) {
|
|
@@ -88,7 +110,12 @@ export function formatAncestryChain(items) {
|
|
|
88
110
|
selector += `#${item.id}`;
|
|
89
111
|
}
|
|
90
112
|
let description = selector;
|
|
91
|
-
|
|
113
|
+
|
|
114
|
+
// Show all owner components if available (Sidebar > GlassPanel)
|
|
115
|
+
if (item.ownerComponents && item.ownerComponents.length > 0) {
|
|
116
|
+
const componentChain = item.ownerComponents.map(c => c.name).join(" > ");
|
|
117
|
+
description = `${selector} in ${componentChain}`;
|
|
118
|
+
} else if (item.componentName) {
|
|
92
119
|
description = `${selector} in ${item.componentName}`;
|
|
93
120
|
}
|
|
94
121
|
const location = item.filePath ? ` at ${item.filePath}:${item.line}` : "";
|
|
@@ -78,4 +78,45 @@ describe("formatAncestryChain", () => {
|
|
|
78
78
|
const result = formatAncestryChain([]);
|
|
79
79
|
expect(result).toBe("");
|
|
80
80
|
});
|
|
81
|
+
it("shows all owner components when ownerComponents is provided", () => {
|
|
82
|
+
const items = [{
|
|
83
|
+
elementName: "div",
|
|
84
|
+
id: "sidebar-panel",
|
|
85
|
+
componentName: "Sidebar",
|
|
86
|
+
filePath: "src/components/game/Sidebar.jsx",
|
|
87
|
+
line: 78,
|
|
88
|
+
ownerComponents: [{
|
|
89
|
+
name: "Sidebar",
|
|
90
|
+
filePath: "src/components/game/Sidebar.jsx",
|
|
91
|
+
line: 78
|
|
92
|
+
}, {
|
|
93
|
+
name: "GlassPanel",
|
|
94
|
+
filePath: "src/components/common/GlassPanel.jsx",
|
|
95
|
+
line: 39
|
|
96
|
+
}]
|
|
97
|
+
}, {
|
|
98
|
+
elementName: "div",
|
|
99
|
+
componentName: "App",
|
|
100
|
+
filePath: "src/App.jsx",
|
|
101
|
+
line: 104
|
|
102
|
+
}];
|
|
103
|
+
const result = formatAncestryChain(items);
|
|
104
|
+
expect(result).toBe(`div in App at src/App.jsx:104
|
|
105
|
+
└─ div#sidebar-panel in Sidebar > GlassPanel at src/components/game/Sidebar.jsx:78`);
|
|
106
|
+
});
|
|
107
|
+
it("shows single owner component without arrow when only one in chain", () => {
|
|
108
|
+
const items = [{
|
|
109
|
+
elementName: "button",
|
|
110
|
+
componentName: "Button",
|
|
111
|
+
ownerComponents: [{
|
|
112
|
+
name: "Button",
|
|
113
|
+
filePath: "src/Button.tsx",
|
|
114
|
+
line: 10
|
|
115
|
+
}],
|
|
116
|
+
filePath: "src/Button.tsx",
|
|
117
|
+
line: 10
|
|
118
|
+
}];
|
|
119
|
+
const result = formatAncestryChain(items);
|
|
120
|
+
expect(result).toBe("button in Button at src/Button.tsx:10");
|
|
121
|
+
});
|
|
81
122
|
});
|
package/dist/types/TreeNode.d.ts
CHANGED
|
@@ -8,6 +8,12 @@ export interface TreeNode {
|
|
|
8
8
|
getChildren(): TreeNode[];
|
|
9
9
|
getSource(): Source | null;
|
|
10
10
|
getComponent(): TreeNodeComponent | null;
|
|
11
|
+
/**
|
|
12
|
+
* Get all owner components in the ownership chain.
|
|
13
|
+
* For React: traverses _debugOwner chain until hitting parent DOM element's owner.
|
|
14
|
+
* Returns array from outermost (user's component like Sidebar) to innermost (wrapper like GlassPanel).
|
|
15
|
+
*/
|
|
16
|
+
getOwnerComponents(): TreeNodeComponent[];
|
|
11
17
|
}
|
|
12
18
|
export type TreeNodeComponent = {
|
|
13
19
|
label: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@treelocator/runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "TreeLocatorJS runtime for component ancestry tracking. Alt+click any element to copy its component tree to clipboard. Exposes window.__treelocator__ API for browser automation (Playwright, Puppeteer, Selenium, Cypress).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"locator",
|
|
@@ -72,5 +72,5 @@
|
|
|
72
72
|
"directory": "packages/runtime"
|
|
73
73
|
},
|
|
74
74
|
"license": "MIT",
|
|
75
|
-
"gitHead": "
|
|
75
|
+
"gitHead": "69fb05167bd689b2e3602fcbc74a70912695f136"
|
|
76
76
|
}
|
|
@@ -25,9 +25,9 @@ export class HtmlElementTreeNode implements TreeNode {
|
|
|
25
25
|
const children = Array.from(this.element.children);
|
|
26
26
|
return children
|
|
27
27
|
.map((child) => {
|
|
28
|
-
if (child instanceof HTMLElement) {
|
|
28
|
+
if (child instanceof HTMLElement || child instanceof SVGElement) {
|
|
29
29
|
// @ts-ignore
|
|
30
|
-
return new this.constructor(child);
|
|
30
|
+
return new this.constructor(child as HTMLElement);
|
|
31
31
|
} else {
|
|
32
32
|
return null;
|
|
33
33
|
}
|
|
@@ -48,4 +48,7 @@ export class HtmlElementTreeNode implements TreeNode {
|
|
|
48
48
|
getComponent(): TreeNodeComponent | null {
|
|
49
49
|
throw new Error("Method not implemented.");
|
|
50
50
|
}
|
|
51
|
+
getOwnerComponents(): TreeNodeComponent[] {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
51
54
|
}
|
|
@@ -10,7 +10,7 @@ export type ElementInfo = {
|
|
|
10
10
|
|
|
11
11
|
export type FullElementInfo = {
|
|
12
12
|
thisElement: ElementInfo;
|
|
13
|
-
htmlElement: HTMLElement;
|
|
13
|
+
htmlElement: HTMLElement | SVGElement;
|
|
14
14
|
parentElements: ElementInfo[];
|
|
15
15
|
componentBox: SimpleDOMRect;
|
|
16
16
|
componentsLabels: LabelData[];
|
|
@@ -2,12 +2,16 @@ import type { ExpressionInfo, FileStorage } from "@locator/shared";
|
|
|
2
2
|
import { parseDataId, parseDataPath } from "../../functions/parseDataId";
|
|
3
3
|
|
|
4
4
|
export function getExpressionData(
|
|
5
|
-
target:
|
|
5
|
+
target: Element,
|
|
6
6
|
fileData: FileStorage | null
|
|
7
7
|
): ExpressionInfo | null {
|
|
8
|
+
// Use getAttribute instead of dataset to support both HTML and SVG elements
|
|
9
|
+
const dataLocatorjs = target.getAttribute("data-locatorjs");
|
|
10
|
+
const dataLocatorjsId = target.getAttribute("data-locatorjs-id");
|
|
11
|
+
|
|
8
12
|
// First check for data-locatorjs (path-based, for server components)
|
|
9
|
-
if (
|
|
10
|
-
const parsed = parseDataPath(
|
|
13
|
+
if (dataLocatorjs) {
|
|
14
|
+
const parsed = parseDataPath(dataLocatorjs);
|
|
11
15
|
if (parsed) {
|
|
12
16
|
const [, line, column] = parsed;
|
|
13
17
|
|
|
@@ -35,8 +39,8 @@ export function getExpressionData(
|
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
// Fall back to data-locatorjs-id (ID-based, traditional approach)
|
|
38
|
-
if (
|
|
39
|
-
const [, id] = parseDataId(
|
|
42
|
+
if (dataLocatorjsId && fileData) {
|
|
43
|
+
const [, id] = parseDataId(dataLocatorjsId);
|
|
40
44
|
const expData = fileData.expressions[Number(id)];
|
|
41
45
|
if (expData) {
|
|
42
46
|
return expData;
|
|
@@ -5,32 +5,37 @@ import type { SimpleDOMRect } from "../../types/types";
|
|
|
5
5
|
import { getExpressionData } from "./getExpressionData";
|
|
6
6
|
|
|
7
7
|
export function getJSXComponentBoundingBox(
|
|
8
|
-
found:
|
|
8
|
+
found: Element,
|
|
9
9
|
locatorData: { [filename: string]: FileStorage },
|
|
10
10
|
componentFolder: string,
|
|
11
11
|
componentId: number
|
|
12
12
|
): SimpleDOMRect {
|
|
13
13
|
let composedBox: SimpleDOMRect = found.getBoundingClientRect();
|
|
14
14
|
// Currently it works well only for components with one root element, but for components with multiple root elements we would need to track instance ids.
|
|
15
|
-
function goParent(current:
|
|
15
|
+
function goParent(current: Element) {
|
|
16
16
|
const parent = current.parentNode;
|
|
17
17
|
if (!parent) {
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
|
-
|
|
20
|
+
// Support both HTMLElement and SVGElement
|
|
21
|
+
if (parent instanceof HTMLElement || parent instanceof SVGElement) {
|
|
22
|
+
// Use getAttribute instead of dataset to support both HTML and SVG elements
|
|
23
|
+
const dataLocatorjs = parent.getAttribute("data-locatorjs");
|
|
24
|
+
const dataLocatorjsId = parent.getAttribute("data-locatorjs-id");
|
|
25
|
+
|
|
21
26
|
// Check for either data-locatorjs (path-based) or data-locatorjs-id (ID-based)
|
|
22
|
-
if (
|
|
27
|
+
if (dataLocatorjs || dataLocatorjsId) {
|
|
23
28
|
let fileFullPath: string;
|
|
24
29
|
|
|
25
|
-
if (
|
|
26
|
-
const parsed = parseDataPath(
|
|
30
|
+
if (dataLocatorjs) {
|
|
31
|
+
const parsed = parseDataPath(dataLocatorjs);
|
|
27
32
|
if (!parsed) {
|
|
28
33
|
goParent(parent);
|
|
29
34
|
return;
|
|
30
35
|
}
|
|
31
36
|
[fileFullPath] = parsed;
|
|
32
|
-
} else if (
|
|
33
|
-
[fileFullPath] = parseDataId(
|
|
37
|
+
} else if (dataLocatorjsId) {
|
|
38
|
+
[fileFullPath] = parseDataId(dataLocatorjsId);
|
|
34
39
|
} else {
|
|
35
40
|
goParent(parent);
|
|
36
41
|
return;
|
|
@@ -20,17 +20,17 @@ import { getJSXComponentBoundingBox } from "./getJSXComponentBoundingBox";
|
|
|
20
20
|
export function getElementInfo(target: HTMLElement): FullElementInfo | null {
|
|
21
21
|
const found = target.closest("[data-locatorjs-id], [data-locatorjs]");
|
|
22
22
|
|
|
23
|
+
// Support both HTMLElement and SVGElement
|
|
24
|
+
// SVG elements don't have dataset, so use getAttribute instead
|
|
25
|
+
const dataId = found?.getAttribute("data-locatorjs-id");
|
|
26
|
+
const dataPath = found?.getAttribute("data-locatorjs");
|
|
27
|
+
const styledDataId = found?.getAttribute("data-locatorjs-styled");
|
|
28
|
+
|
|
23
29
|
if (
|
|
24
30
|
found &&
|
|
25
|
-
found instanceof HTMLElement &&
|
|
26
|
-
|
|
27
|
-
(found.dataset.locatorjsId ||
|
|
28
|
-
found.dataset.locatorjs ||
|
|
29
|
-
found.dataset.locatorjsStyled)
|
|
31
|
+
(found instanceof HTMLElement || found instanceof SVGElement) &&
|
|
32
|
+
(dataId || dataPath || styledDataId)
|
|
30
33
|
) {
|
|
31
|
-
const dataId = found.dataset.locatorjsId;
|
|
32
|
-
const dataPath = found.dataset.locatorjs;
|
|
33
|
-
const styledDataId = found.dataset.locatorjsStyled;
|
|
34
34
|
|
|
35
35
|
if (!dataId && !dataPath) {
|
|
36
36
|
return null;
|
|
@@ -135,8 +135,9 @@ export function getElementInfo(target: HTMLElement): FullElementInfo | null {
|
|
|
135
135
|
|
|
136
136
|
export class JSXTreeNodeElement extends HtmlElementTreeNode {
|
|
137
137
|
getSource(): Source | null {
|
|
138
|
-
|
|
139
|
-
const
|
|
138
|
+
// Use getAttribute instead of dataset to support both HTML and SVG elements
|
|
139
|
+
const dataId = this.element.getAttribute("data-locatorjs-id");
|
|
140
|
+
const dataPath = this.element.getAttribute("data-locatorjs");
|
|
140
141
|
|
|
141
142
|
if (!dataId && !dataPath) {
|
|
142
143
|
return null;
|
|
@@ -184,8 +185,9 @@ export class JSXTreeNodeElement extends HtmlElementTreeNode {
|
|
|
184
185
|
return null;
|
|
185
186
|
}
|
|
186
187
|
getComponent(): TreeNodeComponent | null {
|
|
187
|
-
|
|
188
|
-
const
|
|
188
|
+
// Use getAttribute instead of dataset to support both HTML and SVG elements
|
|
189
|
+
const dataId = this.element.getAttribute("data-locatorjs-id");
|
|
190
|
+
const dataPath = this.element.getAttribute("data-locatorjs");
|
|
189
191
|
|
|
190
192
|
if (!dataId && !dataPath) {
|
|
191
193
|
return null;
|
|
@@ -81,30 +81,68 @@ export class ReactTreeNodeElement extends HtmlElementTreeNode {
|
|
|
81
81
|
}
|
|
82
82
|
return null;
|
|
83
83
|
}
|
|
84
|
+
|
|
85
|
+
private fiberToTreeNodeComponent(fiber: Fiber): TreeNodeComponent {
|
|
86
|
+
const fiberLabel = getFiberLabel(fiber, findDebugSource(fiber)?.source);
|
|
87
|
+
return {
|
|
88
|
+
label: fiberLabel.label,
|
|
89
|
+
callLink:
|
|
90
|
+
(fiberLabel.link && {
|
|
91
|
+
fileName: fiberLabel.link.filePath,
|
|
92
|
+
lineNumber: fiberLabel.link.line,
|
|
93
|
+
columnNumber: fiberLabel.link.column,
|
|
94
|
+
projectPath: fiberLabel.link.projectPath,
|
|
95
|
+
}) ||
|
|
96
|
+
undefined,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
84
100
|
getComponent(): TreeNodeComponent | null {
|
|
85
101
|
const fiber = findFiberByHtmlElement(this.element, false);
|
|
86
102
|
const componentFiber = fiber?._debugOwner;
|
|
87
103
|
|
|
88
104
|
if (componentFiber) {
|
|
89
|
-
|
|
90
|
-
componentFiber,
|
|
91
|
-
findDebugSource(componentFiber)?.source
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
return {
|
|
95
|
-
label: fiberLabel.label,
|
|
96
|
-
callLink:
|
|
97
|
-
(fiberLabel.link && {
|
|
98
|
-
fileName: fiberLabel.link.filePath,
|
|
99
|
-
lineNumber: fiberLabel.link.line,
|
|
100
|
-
columnNumber: fiberLabel.link.column,
|
|
101
|
-
projectPath: fiberLabel.link.projectPath,
|
|
102
|
-
}) ||
|
|
103
|
-
undefined,
|
|
104
|
-
};
|
|
105
|
+
return this.fiberToTreeNodeComponent(componentFiber);
|
|
105
106
|
}
|
|
106
107
|
return null;
|
|
107
108
|
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Traverse the _debugOwner chain to collect all owner components.
|
|
112
|
+
* This finds wrapper components (like Sidebar) that don't render their own DOM elements
|
|
113
|
+
* but wrap other components (like GlassPanel) that do.
|
|
114
|
+
*
|
|
115
|
+
* Returns array from outermost owner (Sidebar) to innermost (GlassPanel).
|
|
116
|
+
*/
|
|
117
|
+
getOwnerComponents(): TreeNodeComponent[] {
|
|
118
|
+
const fiber = findFiberByHtmlElement(this.element, false);
|
|
119
|
+
if (!fiber) return [];
|
|
120
|
+
|
|
121
|
+
// Get the parent DOM element's owner to know when to stop
|
|
122
|
+
const parentElement = this.element.parentElement;
|
|
123
|
+
let parentOwnerFiber: Fiber | null = null;
|
|
124
|
+
if (parentElement) {
|
|
125
|
+
const parentFiber = findFiberByHtmlElement(parentElement, false);
|
|
126
|
+
parentOwnerFiber = parentFiber?._debugOwner || null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const components: TreeNodeComponent[] = [];
|
|
130
|
+
let currentFiber = fiber._debugOwner;
|
|
131
|
+
|
|
132
|
+
// Traverse up the _debugOwner chain until we hit the parent's owner or run out
|
|
133
|
+
while (currentFiber) {
|
|
134
|
+
// Stop if we've reached the parent DOM element's owner component
|
|
135
|
+
if (parentOwnerFiber && currentFiber === parentOwnerFiber) {
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
components.push(this.fiberToTreeNodeComponent(currentFiber));
|
|
140
|
+
currentFiber = currentFiber._debugOwner || null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Reverse so outermost (Sidebar) comes first, innermost (GlassPanel) last
|
|
144
|
+
return components.reverse();
|
|
145
|
+
}
|
|
108
146
|
}
|
|
109
147
|
|
|
110
148
|
function getTree(element: HTMLElement): TreeState | null {
|
|
@@ -48,15 +48,41 @@ function Runtime(props: RuntimeProps) {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
function mouseOverListener(e: MouseEvent) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
// Also update modifier state
|
|
52
|
+
setHoldingModKey(e.altKey);
|
|
53
|
+
|
|
54
|
+
// Use elementsFromPoint to find elements including ones with pointer-events-none
|
|
55
|
+
const elementsAtPoint = document.elementsFromPoint(e.clientX, e.clientY);
|
|
56
|
+
|
|
57
|
+
// Find the topmost element with locator data for highlighting
|
|
58
|
+
let element: HTMLElement | null = null;
|
|
59
|
+
for (const el of elementsAtPoint) {
|
|
60
|
+
if (isLocatorsOwnElement(el as HTMLElement)) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (el instanceof HTMLElement || el instanceof SVGElement) {
|
|
64
|
+
const withLocator = el.closest('[data-locatorjs-id], [data-locatorjs]');
|
|
65
|
+
if (withLocator) {
|
|
66
|
+
element = withLocator as HTMLElement;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Fallback to e.target
|
|
73
|
+
if (!element) {
|
|
74
|
+
const target = e.target;
|
|
75
|
+
if (target && (target instanceof HTMLElement || target instanceof SVGElement)) {
|
|
76
|
+
element = target instanceof SVGElement
|
|
77
|
+
? (target.closest('[data-locatorjs-id], [data-locatorjs]') as HTMLElement | null) ??
|
|
78
|
+
(target.closest('svg') as HTMLElement | null) ??
|
|
79
|
+
(target as unknown as HTMLElement)
|
|
80
|
+
: target;
|
|
55
81
|
}
|
|
82
|
+
}
|
|
56
83
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
setHoldingModKey(e.altKey);
|
|
84
|
+
if (element && !isLocatorsOwnElement(element)) {
|
|
85
|
+
setCurrentElement(element);
|
|
60
86
|
}
|
|
61
87
|
}
|
|
62
88
|
|
|
@@ -76,33 +102,66 @@ function Runtime(props: RuntimeProps) {
|
|
|
76
102
|
return;
|
|
77
103
|
}
|
|
78
104
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
105
|
+
// Use elementsFromPoint to find all elements at click position,
|
|
106
|
+
// including ones with pointer-events-none (like canvas overlays)
|
|
107
|
+
const elementsAtPoint = document.elementsFromPoint(e.clientX, e.clientY);
|
|
108
|
+
|
|
109
|
+
// Find the topmost element with locator data
|
|
110
|
+
let element: Element | null = null;
|
|
111
|
+
for (const el of elementsAtPoint) {
|
|
112
|
+
if (isLocatorsOwnElement(el as HTMLElement)) {
|
|
113
|
+
continue;
|
|
83
114
|
}
|
|
115
|
+
if (el instanceof HTMLElement || el instanceof SVGElement) {
|
|
116
|
+
// Check if this element or its closest ancestor has locator data
|
|
117
|
+
const withLocator = el.closest('[data-locatorjs-id], [data-locatorjs]');
|
|
118
|
+
if (withLocator) {
|
|
119
|
+
element = withLocator;
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
84
124
|
|
|
85
|
-
|
|
86
|
-
|
|
125
|
+
// Fallback to e.target if elementsFromPoint didn't find anything
|
|
126
|
+
if (!element) {
|
|
127
|
+
const target = e.target;
|
|
128
|
+
if (target && (target instanceof HTMLElement || target instanceof SVGElement)) {
|
|
129
|
+
element = target instanceof SVGElement
|
|
130
|
+
? (target.closest('[data-locatorjs-id], [data-locatorjs]') as Element | null) ??
|
|
131
|
+
(target.closest('svg') as Element | null) ??
|
|
132
|
+
target
|
|
133
|
+
: target;
|
|
87
134
|
}
|
|
135
|
+
}
|
|
88
136
|
|
|
89
|
-
|
|
90
|
-
|
|
137
|
+
if (!element) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
91
140
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const ancestry = collectAncestry(treeNode);
|
|
96
|
-
const formatted = formatAncestryChain(ancestry);
|
|
97
|
-
navigator.clipboard.writeText(formatted).then(() => {
|
|
98
|
-
setToastMessage("Copied to clipboard");
|
|
99
|
-
});
|
|
100
|
-
}
|
|
141
|
+
if (element instanceof HTMLElement && element.shadowRoot) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
101
144
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
145
|
+
if (isLocatorsOwnElement(element as HTMLElement)) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
e.preventDefault();
|
|
150
|
+
e.stopPropagation();
|
|
151
|
+
|
|
152
|
+
// Copy ancestry to clipboard on alt+click
|
|
153
|
+
const treeNode = createTreeNode(element as HTMLElement, props.adapterId);
|
|
154
|
+
if (treeNode) {
|
|
155
|
+
const ancestry = collectAncestry(treeNode);
|
|
156
|
+
const formatted = formatAncestryChain(ancestry);
|
|
157
|
+
navigator.clipboard.writeText(formatted).then(() => {
|
|
158
|
+
setToastMessage("Copied to clipboard");
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Deactivate toggle after click
|
|
163
|
+
if (locatorActive()) {
|
|
164
|
+
setLocatorActive(false);
|
|
106
165
|
}
|
|
107
166
|
}
|
|
108
167
|
|
|
@@ -91,4 +91,50 @@ describe("formatAncestryChain", () => {
|
|
|
91
91
|
const result = formatAncestryChain([]);
|
|
92
92
|
expect(result).toBe("");
|
|
93
93
|
});
|
|
94
|
+
|
|
95
|
+
it("shows all owner components when ownerComponents is provided", () => {
|
|
96
|
+
const items: AncestryItem[] = [
|
|
97
|
+
{
|
|
98
|
+
elementName: "div",
|
|
99
|
+
id: "sidebar-panel",
|
|
100
|
+
componentName: "Sidebar",
|
|
101
|
+
filePath: "src/components/game/Sidebar.jsx",
|
|
102
|
+
line: 78,
|
|
103
|
+
ownerComponents: [
|
|
104
|
+
{
|
|
105
|
+
name: "Sidebar",
|
|
106
|
+
filePath: "src/components/game/Sidebar.jsx",
|
|
107
|
+
line: 78,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "GlassPanel",
|
|
111
|
+
filePath: "src/components/common/GlassPanel.jsx",
|
|
112
|
+
line: 39,
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
{ elementName: "div", componentName: "App", filePath: "src/App.jsx", line: 104 },
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
const result = formatAncestryChain(items);
|
|
120
|
+
expect(result).toBe(
|
|
121
|
+
`div in App at src/App.jsx:104
|
|
122
|
+
└─ div#sidebar-panel in Sidebar > GlassPanel at src/components/game/Sidebar.jsx:78`
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("shows single owner component without arrow when only one in chain", () => {
|
|
127
|
+
const items: AncestryItem[] = [
|
|
128
|
+
{
|
|
129
|
+
elementName: "button",
|
|
130
|
+
componentName: "Button",
|
|
131
|
+
ownerComponents: [{ name: "Button", filePath: "src/Button.tsx", line: 10 }],
|
|
132
|
+
filePath: "src/Button.tsx",
|
|
133
|
+
line: 10,
|
|
134
|
+
},
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
const result = formatAncestryChain(items);
|
|
138
|
+
expect(result).toBe("button in Button at src/Button.tsx:10");
|
|
139
|
+
});
|
|
94
140
|
});
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import { TreeNode, TreeNodeElement } from "../types/TreeNode";
|
|
1
|
+
import { TreeNode, TreeNodeComponent, TreeNodeElement } from "../types/TreeNode";
|
|
2
|
+
|
|
3
|
+
export interface OwnerComponentInfo {
|
|
4
|
+
name: string;
|
|
5
|
+
filePath?: string;
|
|
6
|
+
line?: number;
|
|
7
|
+
}
|
|
2
8
|
|
|
3
9
|
export interface AncestryItem {
|
|
4
10
|
elementName: string;
|
|
@@ -7,6 +13,8 @@ export interface AncestryItem {
|
|
|
7
13
|
line?: number;
|
|
8
14
|
id?: string;
|
|
9
15
|
nthChild?: number; // 1-indexed, only set when there are ambiguous siblings
|
|
16
|
+
/** All owner components from outermost (Sidebar) to innermost (GlassPanel) */
|
|
17
|
+
ownerComponents?: OwnerComponentInfo[];
|
|
10
18
|
}
|
|
11
19
|
|
|
12
20
|
// Elements to exclude from ancestry (not useful for debugging)
|
|
@@ -36,6 +44,16 @@ function getNthChildIfAmbiguous(element: Element): number | undefined {
|
|
|
36
44
|
return index + 1; // 1-indexed for CSS nth-child compatibility
|
|
37
45
|
}
|
|
38
46
|
|
|
47
|
+
function treeNodeComponentToOwnerInfo(
|
|
48
|
+
comp: TreeNodeComponent
|
|
49
|
+
): OwnerComponentInfo {
|
|
50
|
+
return {
|
|
51
|
+
name: comp.label,
|
|
52
|
+
filePath: comp.callLink?.fileName,
|
|
53
|
+
line: comp.callLink?.lineNumber,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
39
57
|
export function collectAncestry(node: TreeNode): AncestryItem[] {
|
|
40
58
|
const items: AncestryItem[] = [];
|
|
41
59
|
let current: TreeNode | null = node;
|
|
@@ -47,7 +65,6 @@ export function collectAncestry(node: TreeNode): AncestryItem[] {
|
|
|
47
65
|
continue;
|
|
48
66
|
}
|
|
49
67
|
|
|
50
|
-
const component = current.getComponent();
|
|
51
68
|
const source = current.getSource();
|
|
52
69
|
|
|
53
70
|
const item: AncestryItem = {
|
|
@@ -68,11 +85,26 @@ export function collectAncestry(node: TreeNode): AncestryItem[] {
|
|
|
68
85
|
}
|
|
69
86
|
}
|
|
70
87
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
88
|
+
// Get all owner components (from outermost like Sidebar to innermost like GlassPanel)
|
|
89
|
+
const ownerComponents = current.getOwnerComponents();
|
|
90
|
+
const outermost = ownerComponents[0];
|
|
91
|
+
if (outermost) {
|
|
92
|
+
item.ownerComponents = ownerComponents.map(treeNodeComponentToOwnerInfo);
|
|
93
|
+
// Use outermost component as the primary component name
|
|
94
|
+
item.componentName = outermost.label;
|
|
95
|
+
if (outermost.callLink) {
|
|
96
|
+
item.filePath = outermost.callLink.fileName;
|
|
97
|
+
item.line = outermost.callLink.lineNumber;
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
// Fallback to single component if getOwnerComponents not available
|
|
101
|
+
const component = current.getComponent();
|
|
102
|
+
if (component) {
|
|
103
|
+
item.componentName = component.label;
|
|
104
|
+
if (component.callLink) {
|
|
105
|
+
item.filePath = component.callLink.fileName;
|
|
106
|
+
item.line = component.callLink.lineNumber;
|
|
107
|
+
}
|
|
76
108
|
}
|
|
77
109
|
}
|
|
78
110
|
|
|
@@ -116,7 +148,12 @@ export function formatAncestryChain(items: AncestryItem[]): string {
|
|
|
116
148
|
}
|
|
117
149
|
|
|
118
150
|
let description = selector;
|
|
119
|
-
|
|
151
|
+
|
|
152
|
+
// Show all owner components if available (Sidebar > GlassPanel)
|
|
153
|
+
if (item.ownerComponents && item.ownerComponents.length > 0) {
|
|
154
|
+
const componentChain = item.ownerComponents.map((c) => c.name).join(" > ");
|
|
155
|
+
description = `${selector} in ${componentChain}`;
|
|
156
|
+
} else if (item.componentName) {
|
|
120
157
|
description = `${selector} in ${item.componentName}`;
|
|
121
158
|
}
|
|
122
159
|
|
package/src/types/TreeNode.ts
CHANGED
|
@@ -9,6 +9,12 @@ export interface TreeNode {
|
|
|
9
9
|
getChildren(): TreeNode[];
|
|
10
10
|
getSource(): Source | null;
|
|
11
11
|
getComponent(): TreeNodeComponent | null;
|
|
12
|
+
/**
|
|
13
|
+
* Get all owner components in the ownership chain.
|
|
14
|
+
* For React: traverses _debugOwner chain until hitting parent DOM element's owner.
|
|
15
|
+
* Returns array from outermost (user's component like Sidebar) to innermost (wrapper like GlassPanel).
|
|
16
|
+
*/
|
|
17
|
+
getOwnerComponents(): TreeNodeComponent[];
|
|
12
18
|
}
|
|
13
19
|
|
|
14
20
|
export type TreeNodeComponent = {
|