@treelocator/runtime 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc +3 -0
- package/.turbo/turbo-build.log +30 -0
- package/.turbo/turbo-test.log +18 -0
- package/.turbo/turbo-ts.log +4 -0
- package/LICENSE +22 -0
- package/babel.config.js +14 -0
- package/dist/_generated_styles.d.ts +2 -0
- package/dist/_generated_styles.js +1734 -0
- package/dist/_generated_tree_icon.d.ts +2 -0
- package/dist/_generated_tree_icon.js +2 -0
- package/dist/adapters/HtmlElementTreeNode.d.ts +16 -0
- package/dist/adapters/HtmlElementTreeNode.js +43 -0
- package/dist/adapters/adapterApi.d.ts +30 -0
- package/dist/adapters/adapterApi.js +1 -0
- package/dist/adapters/createTreeNode.d.ts +2 -0
- package/dist/adapters/createTreeNode.js +17 -0
- package/dist/adapters/getElementInfo.d.ts +2 -0
- package/dist/adapters/getElementInfo.js +19 -0
- package/dist/adapters/getParentsPath.d.ts +2 -0
- package/dist/adapters/getParentsPath.js +35 -0
- package/dist/adapters/getTree.d.ts +1 -0
- package/dist/adapters/getTree.js +35 -0
- package/dist/adapters/goUpByTheTree.d.ts +7 -0
- package/dist/adapters/goUpByTheTree.js +22 -0
- package/dist/adapters/jsx/getExpressionData.d.ts +2 -0
- package/dist/adapters/jsx/getExpressionData.js +44 -0
- package/dist/adapters/jsx/getJSXComponentBoundingBox.d.ts +5 -0
- package/dist/adapters/jsx/getJSXComponentBoundingBox.js +46 -0
- package/dist/adapters/jsx/jsxAdapter.d.ts +11 -0
- package/dist/adapters/jsx/jsxAdapter.js +208 -0
- package/dist/adapters/jsx/runtimeStore.d.ts +10 -0
- package/dist/adapters/jsx/runtimeStore.js +87 -0
- package/dist/adapters/react/fiberToSimple.d.ts +3 -0
- package/dist/adapters/react/fiberToSimple.js +55 -0
- package/dist/adapters/react/findDebugSource.d.ts +5 -0
- package/dist/adapters/react/findDebugSource.js +13 -0
- package/dist/adapters/react/findFiberByHtmlElement.d.ts +2 -0
- package/dist/adapters/react/findFiberByHtmlElement.js +22 -0
- package/dist/adapters/react/gatherFiberRoots.d.ts +2 -0
- package/dist/adapters/react/gatherFiberRoots.js +29 -0
- package/dist/adapters/react/getAllFiberChildren.d.ts +2 -0
- package/dist/adapters/react/getAllFiberChildren.js +9 -0
- package/dist/adapters/react/getAllParentsElementsAndRootComponent.d.ts +8 -0
- package/dist/adapters/react/getAllParentsElementsAndRootComponent.js +34 -0
- package/dist/adapters/react/getAllWrappingParents.d.ts +2 -0
- package/dist/adapters/react/getAllWrappingParents.js +19 -0
- package/dist/adapters/react/getFiberComponentBoundingBox.d.ts +3 -0
- package/dist/adapters/react/getFiberComponentBoundingBox.js +27 -0
- package/dist/adapters/react/getFiberLabel.d.ts +3 -0
- package/dist/adapters/react/getFiberLabel.js +14 -0
- package/dist/adapters/react/getFiberOwnBoundingBox.d.ts +3 -0
- package/dist/adapters/react/getFiberOwnBoundingBox.js +6 -0
- package/dist/adapters/react/isStyled.d.ts +2 -0
- package/dist/adapters/react/isStyled.js +3 -0
- package/dist/adapters/react/makeFiberId.d.ts +2 -0
- package/dist/adapters/react/makeFiberId.js +16 -0
- package/dist/adapters/react/reactAdapter.d.ts +11 -0
- package/dist/adapters/react/reactAdapter.js +114 -0
- package/dist/adapters/react/searchDevtoolsRenderersForClosestTarget.d.ts +1 -0
- package/dist/adapters/react/searchDevtoolsRenderersForClosestTarget.js +11 -0
- package/dist/adapters/svelte/svelteAdapter.d.ts +22 -0
- package/dist/adapters/svelte/svelteAdapter.js +88 -0
- package/dist/adapters/vue/getVNodeBoundingBox.d.ts +4 -0
- package/dist/adapters/vue/getVNodeBoundingBox.js +31 -0
- package/dist/adapters/vue/vueAdapter.d.ts +15 -0
- package/dist/adapters/vue/vueAdapter.js +113 -0
- package/dist/browserApi.d.ts +103 -0
- package/dist/browserApi.js +160 -0
- package/dist/components/Button.d.ts +4 -0
- package/dist/components/Button.js +17 -0
- package/dist/components/ComponentOutline.d.ts +7 -0
- package/dist/components/ComponentOutline.js +93 -0
- package/dist/components/MaybeOutline.d.ts +7 -0
- package/dist/components/MaybeOutline.js +43 -0
- package/dist/components/Outline.d.ts +25 -0
- package/dist/components/Outline.js +135 -0
- package/dist/components/RenderBoxes.d.ts +4 -0
- package/dist/components/RenderBoxes.js +73 -0
- package/dist/components/Runtime.d.ts +3 -0
- package/dist/components/Runtime.js +188 -0
- package/dist/components/SimpleNodeOutline.d.ts +4 -0
- package/dist/components/SimpleNodeOutline.js +47 -0
- package/dist/components/Toast.d.ts +4 -0
- package/dist/components/Toast.js +68 -0
- package/dist/components/Tooltip.d.ts +5 -0
- package/dist/components/Tooltip.js +21 -0
- package/dist/consts.d.ts +6 -0
- package/dist/consts.js +5 -0
- package/dist/functions/cropPath.d.ts +1 -0
- package/dist/functions/cropPath.js +9 -0
- package/dist/functions/cropPath.test.d.ts +1 -0
- package/dist/functions/cropPath.test.js +16 -0
- package/dist/functions/deduplicateLabels.d.ts +2 -0
- package/dist/functions/deduplicateLabels.js +12 -0
- package/dist/functions/evalTemplate.d.ts +3 -0
- package/dist/functions/evalTemplate.js +7 -0
- package/dist/functions/evalTemplate.test.d.ts +1 -0
- package/dist/functions/evalTemplate.test.js +11 -0
- package/dist/functions/findNames.d.ts +5 -0
- package/dist/functions/findNames.js +15 -0
- package/dist/functions/formatAncestryChain.d.ts +9 -0
- package/dist/functions/formatAncestryChain.js +56 -0
- package/dist/functions/getBoundingRect.d.ts +1 -0
- package/dist/functions/getBoundingRect.js +11 -0
- package/dist/functions/getComposedBoundingBox.d.ts +2 -0
- package/dist/functions/getComposedBoundingBox.js +20 -0
- package/dist/functions/getIdsOnPathToRoot.d.ts +3 -0
- package/dist/functions/getIdsOnPathToRoot.js +15 -0
- package/dist/functions/getMultipleElementsBoundingBox.d.ts +2 -0
- package/dist/functions/getMultipleElementsBoundingBox.js +20 -0
- package/dist/functions/getPathToParent.d.ts +1 -0
- package/dist/functions/getPathToParent.js +15 -0
- package/dist/functions/getReferenceId.d.ts +1 -0
- package/dist/functions/getReferenceId.js +9 -0
- package/dist/functions/getUsableFileName.d.ts +1 -0
- package/dist/functions/getUsableFileName.js +17 -0
- package/dist/functions/getUsableFileName.test.d.ts +1 -0
- package/dist/functions/getUsableFileName.test.js +16 -0
- package/dist/functions/getUsableName.d.ts +2 -0
- package/dist/functions/getUsableName.js +47 -0
- package/dist/functions/isCombinationModifiersPressed.d.ts +4 -0
- package/dist/functions/isCombinationModifiersPressed.js +16 -0
- package/dist/functions/isLocatorsOwnElement.d.ts +1 -0
- package/dist/functions/isLocatorsOwnElement.js +3 -0
- package/dist/functions/mergeRects.d.ts +2 -0
- package/dist/functions/mergeRects.js +10 -0
- package/dist/functions/mergeRects.test.d.ts +1 -0
- package/dist/functions/mergeRects.test.js +23 -0
- package/dist/functions/nonNullable.d.ts +1 -0
- package/dist/functions/nonNullable.js +3 -0
- package/dist/functions/parseDataId.d.ts +3 -0
- package/dist/functions/parseDataId.js +44 -0
- package/dist/functions/transformPath.d.ts +1 -0
- package/dist/functions/transformPath.js +7 -0
- package/dist/functions/transformPath.test.d.ts +1 -0
- package/dist/functions/transformPath.test.js +16 -0
- package/dist/global.d.js +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +13 -0
- package/dist/initRuntime.d.ts +8 -0
- package/dist/initRuntime.js +80 -0
- package/dist/output.css +1733 -0
- package/dist/types/LabelData.d.ts +5 -0
- package/dist/types/LabelData.js +1 -0
- package/dist/types/TreeNode.d.ts +19 -0
- package/dist/types/TreeNode.js +1 -0
- package/dist/types/types.d.ts +53 -0
- package/dist/types/types.js +1 -0
- package/jest.config.ts +195 -0
- package/package.json +75 -0
- package/scripts/wrapCSS.js +26 -0
- package/scripts/wrapImage.js +24 -0
- package/src/_generated_styles.ts +1734 -0
- package/src/_generated_tree_icon.ts +2 -0
- package/src/adapters/HtmlElementTreeNode.ts +51 -0
- package/src/adapters/adapterApi.ts +35 -0
- package/src/adapters/createTreeNode.ts +25 -0
- package/src/adapters/getElementInfo.tsx +27 -0
- package/src/adapters/getParentsPath.tsx +49 -0
- package/src/adapters/getTree.tsx +45 -0
- package/src/adapters/goUpByTheTree.ts +20 -0
- package/src/adapters/jsx/getExpressionData.ts +47 -0
- package/src/adapters/jsx/getJSXComponentBoundingBox.ts +63 -0
- package/src/adapters/jsx/jsxAdapter.ts +276 -0
- package/src/adapters/jsx/runtimeStore.ts +94 -0
- package/src/adapters/react/fiberToSimple.tsx +72 -0
- package/src/adapters/react/findDebugSource.ts +15 -0
- package/src/adapters/react/findFiberByHtmlElement.ts +27 -0
- package/src/adapters/react/gatherFiberRoots.tsx +36 -0
- package/src/adapters/react/getAllFiberChildren.tsx +11 -0
- package/src/adapters/react/getAllParentsElementsAndRootComponent.ts +52 -0
- package/src/adapters/react/getAllWrappingParents.ts +25 -0
- package/src/adapters/react/getFiberComponentBoundingBox.ts +30 -0
- package/src/adapters/react/getFiberLabel.ts +20 -0
- package/src/adapters/react/getFiberOwnBoundingBox.ts +9 -0
- package/src/adapters/react/isStyled.ts +5 -0
- package/src/adapters/react/makeFiberId.tsx +19 -0
- package/src/adapters/react/reactAdapter.ts +148 -0
- package/src/adapters/react/searchDevtoolsRenderersForClosestTarget.ts +15 -0
- package/src/adapters/svelte/svelteAdapter.ts +111 -0
- package/src/adapters/vue/getVNodeBoundingBox.tsx +42 -0
- package/src/adapters/vue/vueAdapter.ts +139 -0
- package/src/assets/tree-icon.png +0 -0
- package/src/browserApi.ts +288 -0
- package/src/components/Button.tsx +14 -0
- package/src/components/ComponentOutline.tsx +98 -0
- package/src/components/MaybeOutline.tsx +49 -0
- package/src/components/Outline.tsx +153 -0
- package/src/components/RenderBoxes.tsx +57 -0
- package/src/components/Runtime.tsx +246 -0
- package/src/components/SimpleNodeOutline.tsx +27 -0
- package/src/components/Toast.tsx +83 -0
- package/src/components/Tooltip.tsx +28 -0
- package/src/consts.ts +7 -0
- package/src/functions/cropPath.test.ts +18 -0
- package/src/functions/cropPath.ts +12 -0
- package/src/functions/deduplicateLabels.ts +16 -0
- package/src/functions/evalTemplate.test.ts +12 -0
- package/src/functions/evalTemplate.ts +8 -0
- package/src/functions/findNames.ts +20 -0
- package/src/functions/formatAncestryChain.ts +80 -0
- package/src/functions/getBoundingRect.tsx +11 -0
- package/src/functions/getComposedBoundingBox.tsx +25 -0
- package/src/functions/getIdsOnPathToRoot.tsx +21 -0
- package/src/functions/getMultipleElementsBoundingBox.tsx +25 -0
- package/src/functions/getPathToParent.tsx +17 -0
- package/src/functions/getReferenceId.tsx +10 -0
- package/src/functions/getUsableFileName.test.tsx +24 -0
- package/src/functions/getUsableFileName.tsx +19 -0
- package/src/functions/getUsableName.ts +52 -0
- package/src/functions/isCombinationModifiersPressed.ts +32 -0
- package/src/functions/isLocatorsOwnElement.tsx +9 -0
- package/src/functions/mergeRects.test.ts +15 -0
- package/src/functions/mergeRects.tsx +12 -0
- package/src/functions/nonNullable.ts +3 -0
- package/src/functions/parseDataId.ts +62 -0
- package/src/functions/transformPath.test.ts +28 -0
- package/src/functions/transformPath.ts +7 -0
- package/src/global.d.ts +31 -0
- package/src/index.ts +18 -0
- package/src/initRuntime.ts +83 -0
- package/src/main.css +3 -0
- package/src/types/LabelData.ts +6 -0
- package/src/types/TreeNode.ts +22 -0
- package/src/types/types.ts +55 -0
- package/tailwind.config.js +9 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { Targets } from "@locator/shared";
|
|
2
|
+
import { batch, createEffect, createSignal, onCleanup } from "solid-js";
|
|
3
|
+
import { render } from "solid-js/web";
|
|
4
|
+
import { AdapterId } from "../consts";
|
|
5
|
+
import { isCombinationModifiersPressed } from "../functions/isCombinationModifiersPressed";
|
|
6
|
+
import { Targets as SetupTargets } from "../types/types";
|
|
7
|
+
import { MaybeOutline } from "./MaybeOutline";
|
|
8
|
+
import { isLocatorsOwnElement } from "../functions/isLocatorsOwnElement";
|
|
9
|
+
import { Toast } from "./Toast";
|
|
10
|
+
import { collectAncestry, formatAncestryChain } from "../functions/formatAncestryChain";
|
|
11
|
+
import { createTreeNode } from "../adapters/createTreeNode";
|
|
12
|
+
import treeIconUrl from "../_generated_tree_icon";
|
|
13
|
+
|
|
14
|
+
type RuntimeProps = {
|
|
15
|
+
adapterId?: AdapterId;
|
|
16
|
+
targets: Targets;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function Runtime(props: RuntimeProps) {
|
|
20
|
+
const [holdingModKey, setHoldingModKey] = createSignal<boolean>(false);
|
|
21
|
+
const [currentElement, setCurrentElement] = createSignal<HTMLElement | null>(
|
|
22
|
+
null
|
|
23
|
+
);
|
|
24
|
+
const [toastMessage, setToastMessage] = createSignal<string | null>(null);
|
|
25
|
+
const [locatorActive, setLocatorActive] = createSignal<boolean>(false);
|
|
26
|
+
|
|
27
|
+
const isActive = () => (holdingModKey() || locatorActive()) && currentElement();
|
|
28
|
+
|
|
29
|
+
createEffect(() => {
|
|
30
|
+
if (isActive()) {
|
|
31
|
+
document.body.classList.add("locatorjs-active-pointer");
|
|
32
|
+
} else {
|
|
33
|
+
document.body.classList.remove("locatorjs-active-pointer");
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
function keyUpListener(e: KeyboardEvent) {
|
|
38
|
+
setHoldingModKey(isCombinationModifiersPressed(e));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function keyDownListener(e: KeyboardEvent) {
|
|
42
|
+
setHoldingModKey(isCombinationModifiersPressed(e, true));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function mouseOverListener(e: MouseEvent) {
|
|
46
|
+
const target = e.target;
|
|
47
|
+
if (target && target instanceof HTMLElement) {
|
|
48
|
+
if (isLocatorsOwnElement(target)) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
setHoldingModKey(isCombinationModifiersPressed(e, true));
|
|
53
|
+
|
|
54
|
+
batch(() => {
|
|
55
|
+
setCurrentElement(target);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function mouseDownUpListener(e: MouseEvent) {
|
|
61
|
+
if (isCombinationModifiersPressed(e) || locatorActive()) {
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
e.stopPropagation();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function clickListener(e: MouseEvent) {
|
|
68
|
+
if (!isCombinationModifiersPressed(e) && !locatorActive()) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const target = e.target;
|
|
73
|
+
if (target && target instanceof HTMLElement) {
|
|
74
|
+
if (target.shadowRoot) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (isLocatorsOwnElement(target)) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
e.stopPropagation();
|
|
84
|
+
|
|
85
|
+
// Copy ancestry to clipboard on alt+click
|
|
86
|
+
const treeNode = createTreeNode(target, props.adapterId);
|
|
87
|
+
if (treeNode) {
|
|
88
|
+
const ancestry = collectAncestry(treeNode);
|
|
89
|
+
const formatted = formatAncestryChain(ancestry);
|
|
90
|
+
navigator.clipboard.writeText(formatted).then(() => {
|
|
91
|
+
setToastMessage("Copied to clipboard");
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Deactivate toggle after click
|
|
96
|
+
if (locatorActive()) {
|
|
97
|
+
setLocatorActive(false);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function scrollListener() {
|
|
103
|
+
setCurrentElement(null);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const roots: (Document | ShadowRoot)[] = [document];
|
|
107
|
+
document.querySelectorAll("*").forEach((node) => {
|
|
108
|
+
if (node.id === "locatorjs-wrapper") {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (node.shadowRoot) {
|
|
112
|
+
roots.push(node.shadowRoot);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
for (const root of roots) {
|
|
117
|
+
root.addEventListener("mouseover", mouseOverListener as EventListener, {
|
|
118
|
+
capture: true,
|
|
119
|
+
});
|
|
120
|
+
root.addEventListener("keydown", keyDownListener as EventListener);
|
|
121
|
+
root.addEventListener("keyup", keyUpListener as EventListener);
|
|
122
|
+
root.addEventListener("click", clickListener as EventListener, {
|
|
123
|
+
capture: true,
|
|
124
|
+
});
|
|
125
|
+
root.addEventListener("mousedown", mouseDownUpListener as EventListener, {
|
|
126
|
+
capture: true,
|
|
127
|
+
});
|
|
128
|
+
root.addEventListener("mouseup", mouseDownUpListener as EventListener, {
|
|
129
|
+
capture: true,
|
|
130
|
+
});
|
|
131
|
+
root.addEventListener("scroll", scrollListener);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
onCleanup(() => {
|
|
135
|
+
for (const root of roots) {
|
|
136
|
+
root.removeEventListener("keyup", keyUpListener as EventListener);
|
|
137
|
+
root.removeEventListener("keydown", keyDownListener as EventListener);
|
|
138
|
+
root.removeEventListener(
|
|
139
|
+
"mouseover",
|
|
140
|
+
mouseOverListener as EventListener,
|
|
141
|
+
{ capture: true }
|
|
142
|
+
);
|
|
143
|
+
root.removeEventListener("click", clickListener as EventListener, {
|
|
144
|
+
capture: true,
|
|
145
|
+
});
|
|
146
|
+
root.removeEventListener(
|
|
147
|
+
"mousedown",
|
|
148
|
+
mouseDownUpListener as EventListener,
|
|
149
|
+
{ capture: true }
|
|
150
|
+
);
|
|
151
|
+
root.removeEventListener(
|
|
152
|
+
"mouseup",
|
|
153
|
+
mouseDownUpListener as EventListener,
|
|
154
|
+
{ capture: true }
|
|
155
|
+
);
|
|
156
|
+
root.removeEventListener("scroll", scrollListener);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<>
|
|
162
|
+
{isActive() ? (
|
|
163
|
+
<MaybeOutline
|
|
164
|
+
currentElement={currentElement()!}
|
|
165
|
+
adapterId={props.adapterId}
|
|
166
|
+
targets={props.targets}
|
|
167
|
+
/>
|
|
168
|
+
) : null}
|
|
169
|
+
{toastMessage() && (
|
|
170
|
+
<Toast
|
|
171
|
+
message={toastMessage()!}
|
|
172
|
+
onClose={() => setToastMessage(null)}
|
|
173
|
+
/>
|
|
174
|
+
)}
|
|
175
|
+
<div
|
|
176
|
+
class="fixed pointer-events-auto"
|
|
177
|
+
style={{ "z-index": "2147483646", bottom: "20px", right: "20px" }}
|
|
178
|
+
title="LocatorJS - Component Ancestry Tracker"
|
|
179
|
+
data-locatorjs-api="window.__locatorjs__"
|
|
180
|
+
data-locatorjs-help="window.__locatorjs__.help()"
|
|
181
|
+
>
|
|
182
|
+
<div
|
|
183
|
+
class="rounded-full bg-white shadow-lg flex items-center justify-center cursor-pointer overflow-hidden"
|
|
184
|
+
style={{
|
|
185
|
+
width: "54px",
|
|
186
|
+
height: "54px",
|
|
187
|
+
"box-shadow": locatorActive()
|
|
188
|
+
? "0 0 0 3px #3b82f6, 0 4px 14px rgba(0, 0, 0, 0.25)"
|
|
189
|
+
: "0 4px 14px rgba(0, 0, 0, 0.25)",
|
|
190
|
+
transition: "transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out",
|
|
191
|
+
}}
|
|
192
|
+
onMouseEnter={(e) => e.currentTarget.style.transform = "scale(1.25)"}
|
|
193
|
+
onMouseLeave={(e) => e.currentTarget.style.transform = "scale(1)"}
|
|
194
|
+
onClick={() => setLocatorActive(!locatorActive())}
|
|
195
|
+
aria-label="LocatorJS: Get component paths using window.__locatorjs__.getPath(selector)"
|
|
196
|
+
role="button"
|
|
197
|
+
>
|
|
198
|
+
<img
|
|
199
|
+
src={treeIconUrl}
|
|
200
|
+
alt="LocatorJS - Browser API available at window.__locatorjs__ - Call window.__locatorjs__.help() for usage instructions"
|
|
201
|
+
width={51}
|
|
202
|
+
height={51}
|
|
203
|
+
/>
|
|
204
|
+
</div>
|
|
205
|
+
<div
|
|
206
|
+
style={{
|
|
207
|
+
position: "absolute",
|
|
208
|
+
width: "1px",
|
|
209
|
+
height: "1px",
|
|
210
|
+
padding: "0",
|
|
211
|
+
margin: "-1px",
|
|
212
|
+
overflow: "hidden",
|
|
213
|
+
clip: "rect(0,0,0,0)",
|
|
214
|
+
"white-space": "nowrap",
|
|
215
|
+
border: "0",
|
|
216
|
+
}}
|
|
217
|
+
aria-hidden="true"
|
|
218
|
+
data-ai-hint="true"
|
|
219
|
+
data-ai-instruction="AI Assistant: LocatorJS provides window.__locatorjs__ API for component ancestry tracking. Usage: window.__locatorjs__.getPath('css-selector') returns component hierarchy. For full API documentation, evaluate: window.__locatorjs__.help() Example: page.evaluate(function() { return window.__locatorjs__.getPath('button.submit'); })"
|
|
220
|
+
>
|
|
221
|
+
LocatorJS Browser API: Call window.__locatorjs__.help() for usage instructions
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
</>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function initRender(
|
|
229
|
+
solidLayer: HTMLDivElement,
|
|
230
|
+
adapter: AdapterId | undefined,
|
|
231
|
+
targets: SetupTargets
|
|
232
|
+
) {
|
|
233
|
+
render(
|
|
234
|
+
() => (
|
|
235
|
+
<Runtime
|
|
236
|
+
targets={Object.fromEntries(
|
|
237
|
+
Object.entries(targets).map(([key, t]) => {
|
|
238
|
+
return [key, typeof t == "string" ? { url: t, label: key } : t];
|
|
239
|
+
})
|
|
240
|
+
)}
|
|
241
|
+
adapterId={adapter}
|
|
242
|
+
/>
|
|
243
|
+
),
|
|
244
|
+
solidLayer
|
|
245
|
+
);
|
|
246
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { TreeNode } from "../types/TreeNode";
|
|
2
|
+
|
|
3
|
+
export function SimpleNodeOutline(props: { node: TreeNode }) {
|
|
4
|
+
const offset = () => (props.node.type === "component" ? 2 : 0);
|
|
5
|
+
const box = () => props.node.getBox();
|
|
6
|
+
return (
|
|
7
|
+
<div>
|
|
8
|
+
{box() ? (
|
|
9
|
+
<div
|
|
10
|
+
style={{
|
|
11
|
+
position: "fixed",
|
|
12
|
+
left: box()!.x - offset() + "px",
|
|
13
|
+
top: box()!.y - offset() + "px",
|
|
14
|
+
width: box()!.width + offset() * 2 + "px",
|
|
15
|
+
height: box()!.height + offset() * 2 + "px",
|
|
16
|
+
border:
|
|
17
|
+
props.node.type === "component"
|
|
18
|
+
? "2px solid rgba(200,0,0,1)"
|
|
19
|
+
: "1px solid rgba(200,0,0,1)",
|
|
20
|
+
"border-radius": props.node.type === "component" ? "5px" : "3px",
|
|
21
|
+
"z-index": props.node.type === "component" ? 1000 : 10,
|
|
22
|
+
}}
|
|
23
|
+
/>
|
|
24
|
+
) : null}
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { createSignal, onCleanup, onMount } from "solid-js";
|
|
2
|
+
import { Portal } from "solid-js/web";
|
|
3
|
+
import TREE_ICON from "../_generated_tree_icon";
|
|
4
|
+
|
|
5
|
+
export function Toast(props: { message: string; onClose: () => void }) {
|
|
6
|
+
let timeoutId: ReturnType<typeof setTimeout>;
|
|
7
|
+
const [flashOpacity, setFlashOpacity] = createSignal(1);
|
|
8
|
+
|
|
9
|
+
onMount(() => {
|
|
10
|
+
// Start flash fade after a brief moment so it's visible
|
|
11
|
+
setTimeout(() => {
|
|
12
|
+
setFlashOpacity(0);
|
|
13
|
+
}, 100);
|
|
14
|
+
|
|
15
|
+
timeoutId = setTimeout(() => {
|
|
16
|
+
props.onClose();
|
|
17
|
+
}, 1500);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
onCleanup(() => {
|
|
21
|
+
clearTimeout(timeoutId);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<Portal mount={document.body}>
|
|
26
|
+
{/* Flash overlay */}
|
|
27
|
+
<div
|
|
28
|
+
style={{
|
|
29
|
+
position: "fixed",
|
|
30
|
+
top: "0",
|
|
31
|
+
left: "0",
|
|
32
|
+
right: "0",
|
|
33
|
+
bottom: "0",
|
|
34
|
+
width: "100vw",
|
|
35
|
+
height: "100vh",
|
|
36
|
+
"z-index": 99999,
|
|
37
|
+
"background-color": "rgba(56, 189, 248, 0.5)",
|
|
38
|
+
opacity: flashOpacity(),
|
|
39
|
+
transition: "opacity 0.3s ease-out",
|
|
40
|
+
"pointer-events": "none",
|
|
41
|
+
}}
|
|
42
|
+
/>
|
|
43
|
+
{/* Tree icon in bottom left */}
|
|
44
|
+
<div
|
|
45
|
+
style={{
|
|
46
|
+
position: "fixed",
|
|
47
|
+
bottom: "16px",
|
|
48
|
+
left: "16px",
|
|
49
|
+
"z-index": 100000,
|
|
50
|
+
"pointer-events": "none",
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
<img
|
|
54
|
+
src={TREE_ICON}
|
|
55
|
+
alt="Tree"
|
|
56
|
+
style={{
|
|
57
|
+
width: "32px",
|
|
58
|
+
height: "32px",
|
|
59
|
+
}}
|
|
60
|
+
/>
|
|
61
|
+
</div>
|
|
62
|
+
{/* Toast message */}
|
|
63
|
+
<div
|
|
64
|
+
style={{
|
|
65
|
+
position: "fixed",
|
|
66
|
+
bottom: "16px",
|
|
67
|
+
left: "50%",
|
|
68
|
+
transform: "translateX(-50%)",
|
|
69
|
+
"background-color": "#111827",
|
|
70
|
+
color: "white",
|
|
71
|
+
padding: "8px 16px",
|
|
72
|
+
"border-radius": "8px",
|
|
73
|
+
"box-shadow": "0 10px 15px -3px rgba(0, 0, 0, 0.1)",
|
|
74
|
+
"font-size": "14px",
|
|
75
|
+
"z-index": 100000,
|
|
76
|
+
"pointer-events": "auto",
|
|
77
|
+
}}
|
|
78
|
+
>
|
|
79
|
+
{props.message}
|
|
80
|
+
</div>
|
|
81
|
+
</Portal>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export default function Tooltip(props: {
|
|
2
|
+
tooltipText?: string;
|
|
3
|
+
position?: "bottom" | "left" | "right" | "top";
|
|
4
|
+
children: any;
|
|
5
|
+
}) {
|
|
6
|
+
const positionMap = {
|
|
7
|
+
bottom: "-bottom-7 left-1/2 -translate-x-1/2 transform",
|
|
8
|
+
left: "-left-2 top-1/2 -translate-y-1/2 -translate-x-full transform",
|
|
9
|
+
right: "-right-2 top-1/2 -translate-y-1/2 translate-x-full transform",
|
|
10
|
+
top: "-top-7 left-1/2 -translate-x-1/2 transform",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div class="group/tooltip relative block">
|
|
15
|
+
{props.children}
|
|
16
|
+
<div
|
|
17
|
+
class={
|
|
18
|
+
"pointer-events-none invisible absolute z-10 whitespace-nowrap rounded bg-gray-700 px-2 py-1 text-center text-xs text-white opacity-0 transition-opacity duration-300 group-hover/tooltip:visible group-hover/tooltip:opacity-100" +
|
|
19
|
+
" " +
|
|
20
|
+
positionMap[props.position || "top"]
|
|
21
|
+
}
|
|
22
|
+
role="tooltip"
|
|
23
|
+
>
|
|
24
|
+
{props.tooltipText}
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
package/src/consts.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import cropPath from "./cropPath";
|
|
2
|
+
import { describe, expect, test } from "vitest";
|
|
3
|
+
|
|
4
|
+
describe("cropPath", () => {
|
|
5
|
+
test("crop", () => {
|
|
6
|
+
expect(cropPath("aaa/bbb/ccc/ddd.tsx")).toBe(".../bbb/ccc/ddd.tsx");
|
|
7
|
+
expect(cropPath("/aaa/bbb/ccc/ddd.tsx")).toBe(".../bbb/ccc/ddd.tsx");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("no crop", () => {
|
|
11
|
+
// expect(cropPath("/bbb/ccc/ddd.tsx")).toBe("/bbb/ccc/ddd.tsx");
|
|
12
|
+
expect(cropPath("bbb/ccc/ddd.tsx")).toBe("bbb/ccc/ddd.tsx");
|
|
13
|
+
expect(cropPath("/ccc/ddd.tsx")).toBe("/ccc/ddd.tsx");
|
|
14
|
+
expect(cropPath("ccc/ddd.tsx")).toBe("ccc/ddd.tsx");
|
|
15
|
+
expect(cropPath("")).toBe("");
|
|
16
|
+
expect(cropPath("xxx")).toBe("xxx");
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export default function cropPath(fullPath: string) {
|
|
2
|
+
const array = fullPath.split("/");
|
|
3
|
+
|
|
4
|
+
const newPath = array
|
|
5
|
+
.slice(Math.max(array.length - 3, 0), array.length)
|
|
6
|
+
.join("/");
|
|
7
|
+
if (newPath !== fullPath) {
|
|
8
|
+
return `.../${newPath}`;
|
|
9
|
+
} else {
|
|
10
|
+
return newPath;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { LabelData } from "../types/LabelData";
|
|
2
|
+
import nonNullable from "./nonNullable";
|
|
3
|
+
|
|
4
|
+
export function deduplicateLabels(labels: LabelData[]): LabelData[] {
|
|
5
|
+
const labelsIds: { [key: string]: true } = {};
|
|
6
|
+
return labels
|
|
7
|
+
.map((label) => {
|
|
8
|
+
const id = JSON.stringify(label);
|
|
9
|
+
if (labelsIds[id]) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
labelsIds[id] = true;
|
|
13
|
+
return label;
|
|
14
|
+
})
|
|
15
|
+
.filter(nonNullable);
|
|
16
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { evalTemplate } from "./evalTemplate";
|
|
2
|
+
import { describe, expect, test } from "vitest";
|
|
3
|
+
|
|
4
|
+
describe("evalTemplate", () => {
|
|
5
|
+
test("basic", () => {
|
|
6
|
+
const res = evalTemplate("https://example.com/${filePath}${ext}", {
|
|
7
|
+
filePath: "test",
|
|
8
|
+
ext: ".js",
|
|
9
|
+
});
|
|
10
|
+
expect(res).toBe("https://example.com/test.js");
|
|
11
|
+
});
|
|
12
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Fiber } from "@locator/shared";
|
|
2
|
+
import { getUsableName } from "./getUsableName";
|
|
3
|
+
|
|
4
|
+
export function findNames(fiber: Fiber): {
|
|
5
|
+
name: string;
|
|
6
|
+
wrappingComponent: string;
|
|
7
|
+
} {
|
|
8
|
+
// if (fiber._debugOwner?.elementType?.styledComponentId) {
|
|
9
|
+
// // This is special case for styled-components, we need to show one level up
|
|
10
|
+
// return {
|
|
11
|
+
// name: getUsableName(fiber._debugOwner),
|
|
12
|
+
// wrappingComponent: getUsableName(fiber._debugOwner?._debugOwner),
|
|
13
|
+
// };
|
|
14
|
+
// } else {
|
|
15
|
+
return {
|
|
16
|
+
name: getUsableName(fiber),
|
|
17
|
+
wrappingComponent: getUsableName(fiber._debugOwner),
|
|
18
|
+
};
|
|
19
|
+
// }
|
|
20
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { TreeNode } from "../types/TreeNode";
|
|
2
|
+
|
|
3
|
+
export interface AncestryItem {
|
|
4
|
+
elementName: string;
|
|
5
|
+
componentName?: string;
|
|
6
|
+
filePath?: string;
|
|
7
|
+
line?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Elements to exclude from ancestry (not useful for debugging)
|
|
11
|
+
const EXCLUDED_ELEMENTS = new Set(["html", "body", "head"]);
|
|
12
|
+
|
|
13
|
+
export function collectAncestry(node: TreeNode): AncestryItem[] {
|
|
14
|
+
const items: AncestryItem[] = [];
|
|
15
|
+
let current: TreeNode | null = node;
|
|
16
|
+
|
|
17
|
+
while (current) {
|
|
18
|
+
// Skip html, body, head elements
|
|
19
|
+
if (EXCLUDED_ELEMENTS.has(current.name)) {
|
|
20
|
+
current = current.getParent();
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const component = current.getComponent();
|
|
25
|
+
const source = current.getSource();
|
|
26
|
+
|
|
27
|
+
const item: AncestryItem = {
|
|
28
|
+
elementName: current.name,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
if (component) {
|
|
32
|
+
item.componentName = component.label;
|
|
33
|
+
if (component.callLink) {
|
|
34
|
+
item.filePath = component.callLink.fileName;
|
|
35
|
+
item.line = component.callLink.lineNumber;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!item.filePath && source) {
|
|
40
|
+
item.filePath = source.fileName;
|
|
41
|
+
item.line = source.lineNumber;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Only include items that have useful info (component name or file path)
|
|
45
|
+
if (item.componentName || item.filePath) {
|
|
46
|
+
items.push(item);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
current = current.getParent();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return items;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function formatAncestryChain(items: AncestryItem[]): string {
|
|
56
|
+
if (items.length === 0) {
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Reverse so root is at top, clicked element at bottom
|
|
61
|
+
const reversed = [...items].reverse();
|
|
62
|
+
|
|
63
|
+
const lines: string[] = [];
|
|
64
|
+
|
|
65
|
+
reversed.forEach((item, index) => {
|
|
66
|
+
const indent = " ".repeat(index);
|
|
67
|
+
const prefix = index === 0 ? "" : "└─ ";
|
|
68
|
+
|
|
69
|
+
let description = item.elementName;
|
|
70
|
+
if (item.componentName) {
|
|
71
|
+
description = `${item.elementName} in ${item.componentName}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const location = item.filePath ? ` at ${item.filePath}:${item.line}` : "";
|
|
75
|
+
|
|
76
|
+
lines.push(`${indent}${prefix}${description}${location}`);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return lines.join("\n");
|
|
80
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function getBoundingRect(node: Element | Text): DOMRect | null {
|
|
2
|
+
if (node instanceof Element) {
|
|
3
|
+
return node.getBoundingClientRect();
|
|
4
|
+
} else if (node instanceof Text) {
|
|
5
|
+
const range = document.createRange();
|
|
6
|
+
range.selectNodeContents(node);
|
|
7
|
+
return range.getBoundingClientRect();
|
|
8
|
+
} else {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { mergeRects } from "./mergeRects";
|
|
2
|
+
import { SimpleDOMRect, SimpleNode } from "./../types/types";
|
|
3
|
+
|
|
4
|
+
export function getComposedBoundingBox(
|
|
5
|
+
children: SimpleNode[]
|
|
6
|
+
): SimpleDOMRect | null {
|
|
7
|
+
let composedRect: SimpleDOMRect | null = null;
|
|
8
|
+
|
|
9
|
+
children.forEach((child) => {
|
|
10
|
+
const box = child.box;
|
|
11
|
+
if (!box) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (box.width <= 0 || box.height <= 0) {
|
|
15
|
+
// ignore zero-sized rects
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (composedRect) {
|
|
19
|
+
composedRect = mergeRects(composedRect, box);
|
|
20
|
+
} else {
|
|
21
|
+
composedRect = box;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
return composedRect;
|
|
25
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { findFiberByHtmlElement } from "./../adapters/react/findFiberByHtmlElement";
|
|
2
|
+
import { makeFiberId } from "./../adapters/react/makeFiberId";
|
|
3
|
+
|
|
4
|
+
export function getIdsOnPathToRoot(element: HTMLElement): {
|
|
5
|
+
[id: string]: true;
|
|
6
|
+
} {
|
|
7
|
+
const fiber = findFiberByHtmlElement(element, false);
|
|
8
|
+
if (!fiber) {
|
|
9
|
+
return {};
|
|
10
|
+
}
|
|
11
|
+
const res: {
|
|
12
|
+
[id: string]: true;
|
|
13
|
+
} = {};
|
|
14
|
+
let parent = fiber?._debugOwner;
|
|
15
|
+
|
|
16
|
+
while (parent) {
|
|
17
|
+
res[makeFiberId(parent)] = true;
|
|
18
|
+
parent = parent?._debugOwner;
|
|
19
|
+
}
|
|
20
|
+
return res;
|
|
21
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { mergeRects } from "./mergeRects";
|
|
2
|
+
import { SimpleDOMRect } from "../types/types";
|
|
3
|
+
|
|
4
|
+
export function getMultipleElementsBoundingBox(
|
|
5
|
+
children: HTMLElement[]
|
|
6
|
+
): SimpleDOMRect | null {
|
|
7
|
+
let composedRect: SimpleDOMRect | null = null;
|
|
8
|
+
|
|
9
|
+
children.forEach((child) => {
|
|
10
|
+
const box = child.getBoundingClientRect();
|
|
11
|
+
if (!box) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (box.width <= 0 || box.height <= 0) {
|
|
15
|
+
// ignore zero-sized rects
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (composedRect) {
|
|
19
|
+
composedRect = mergeRects(composedRect, box);
|
|
20
|
+
} else {
|
|
21
|
+
composedRect = box;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
return composedRect;
|
|
25
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { findFiberByHtmlElement } from "../adapters/react/findFiberByHtmlElement";
|
|
2
|
+
import { fiberToSimple } from "../adapters/react/fiberToSimple";
|
|
3
|
+
|
|
4
|
+
export function getPathToParent(element: HTMLElement) {
|
|
5
|
+
const fiber = findFiberByHtmlElement(element, false);
|
|
6
|
+
if (!fiber) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
let res = fiberToSimple(fiber);
|
|
10
|
+
let parent = fiber?._debugOwner;
|
|
11
|
+
|
|
12
|
+
while (parent) {
|
|
13
|
+
res = fiberToSimple(parent, [res]);
|
|
14
|
+
parent = parent?._debugOwner;
|
|
15
|
+
}
|
|
16
|
+
return res;
|
|
17
|
+
}
|