@zargaryanvh/react-component-inspector 1.0.2 → 1.0.4
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/README.md +72 -25
- package/dist/InspectionContext.d.ts +37 -35
- package/dist/InspectionContext.js +211 -253
- package/dist/InspectionHighlight.d.ts +2 -6
- package/dist/InspectionHighlight.js +258 -65
- package/dist/InspectionOverlays.d.ts +7 -0
- package/dist/InspectionOverlays.js +32 -0
- package/dist/InspectionTooltip.d.ts +6 -6
- package/dist/InspectionTooltip.js +406 -232
- package/dist/InspectionWrapper.d.ts +28 -28
- package/dist/InspectionWrapper.js +102 -102
- package/dist/autoInspection.d.ts +18 -14
- package/dist/autoInspection.js +329 -320
- package/dist/index.d.ts +10 -8
- package/dist/index.js +10 -9
- package/dist/inspection.d.ts +78 -29
- package/dist/inspection.js +493 -140
- package/dist/inspectionInterceptors.d.ts +27 -27
- package/dist/inspectionInterceptors.js +68 -64
- package/dist/useInspectionMetadata.d.ts +34 -34
- package/dist/useInspectionMetadata.js +67 -67
- package/package.json +1 -1
package/dist/autoInspection.js
CHANGED
|
@@ -1,320 +1,329 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Automatic inspection detection via data attributes
|
|
3
|
-
* Components can add data-inspection-* attributes and the system will detect them automatically
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* Extract basic element info for inspection
|
|
7
|
-
*/
|
|
8
|
-
const extractElementInfo = (element) => {
|
|
9
|
-
const tagName = element.tagName.toLowerCase();
|
|
10
|
-
// Convert className to string - it can be a DOMTokenList or string
|
|
11
|
-
let className = "";
|
|
12
|
-
if (typeof element.className === "string") {
|
|
13
|
-
className = element.className;
|
|
14
|
-
}
|
|
15
|
-
else if (element.className) {
|
|
16
|
-
// DOMTokenList - convert to string
|
|
17
|
-
className = String(element.className);
|
|
18
|
-
}
|
|
19
|
-
const id = element.id || "";
|
|
20
|
-
// Extract text preview (first 50 chars)
|
|
21
|
-
const textContent = element.textContent || "";
|
|
22
|
-
const textPreview = textContent.trim().substring(0, 50).replace(/\s+/g, " ");
|
|
23
|
-
// Try to detect MUI components from class names
|
|
24
|
-
let muiComponent;
|
|
25
|
-
if (className && typeof className === "string" && className.includes("Mui")) {
|
|
26
|
-
const muiMatch = className.match(/Mui(\w+)/);
|
|
27
|
-
if (muiMatch) {
|
|
28
|
-
muiComponent = muiMatch[1];
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return { tagName, className, id, textPreview, muiComponent };
|
|
32
|
-
};
|
|
33
|
-
/**
|
|
34
|
-
* Parse component metadata from data attributes or infer from element
|
|
35
|
-
*/
|
|
36
|
-
export const parseInspectionMetadata = (element) => {
|
|
37
|
-
if (process.env.NODE_ENV !== "development") {
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
// First, try to find explicit inspection metadata
|
|
41
|
-
const componentName = element.getAttribute("data-inspection-name");
|
|
42
|
-
const componentId = element.getAttribute("data-inspection-id");
|
|
43
|
-
if (componentName && componentId) {
|
|
44
|
-
// Has explicit metadata - use it
|
|
45
|
-
const variant = element.getAttribute("data-inspection-variant") || undefined;
|
|
46
|
-
const role = element.getAttribute("data-inspection-role") || undefined;
|
|
47
|
-
const usagePath = element.getAttribute("data-inspection-usage-path") || "Unknown";
|
|
48
|
-
const instanceIndex = parseInt(element.getAttribute("data-inspection-instance") || "0", 10);
|
|
49
|
-
const propsSignature = element.getAttribute("data-inspection-props") || "default";
|
|
50
|
-
const sourceFile = element.getAttribute("data-inspection-file") || "Unknown";
|
|
51
|
-
return {
|
|
52
|
-
componentName,
|
|
53
|
-
componentId,
|
|
54
|
-
variant,
|
|
55
|
-
role,
|
|
56
|
-
usagePath,
|
|
57
|
-
instanceIndex,
|
|
58
|
-
propsSignature,
|
|
59
|
-
sourceFile,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
// No explicit metadata - infer from element
|
|
63
|
-
const info = extractElementInfo(element);
|
|
64
|
-
// Generate component name
|
|
65
|
-
let inferredName = info.muiComponent || info.tagName.toUpperCase();
|
|
66
|
-
if (info.id) {
|
|
67
|
-
inferredName = `${inferredName} (${info.id})`;
|
|
68
|
-
}
|
|
69
|
-
else if (info.className) {
|
|
70
|
-
// Try to extract meaningful class name
|
|
71
|
-
const classes = info.className.split(/\s+/).filter(c => c && !c.startsWith("Mui") && c.length > 2);
|
|
72
|
-
if (classes.length > 0) {
|
|
73
|
-
inferredName = `${inferredName} .${classes[0]}`;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
// Generate component ID - use deterministic hash based on element's position in DOM tree
|
|
77
|
-
// This ensures the same element always gets the same ID, even without explicit metadata
|
|
78
|
-
const generateDeterministicId = (el, tagName) => {
|
|
79
|
-
if (info.id) {
|
|
80
|
-
return info.id;
|
|
81
|
-
}
|
|
82
|
-
// Create a stable identifier based on element's path in DOM tree
|
|
83
|
-
const path = [];
|
|
84
|
-
let current = el;
|
|
85
|
-
// Walk up to body, collecting tag names and indices
|
|
86
|
-
while (current && current !== document.body && current !== document.documentElement) {
|
|
87
|
-
const currentTagName = current.tagName.toLowerCase();
|
|
88
|
-
const parent = current.parentElement;
|
|
89
|
-
if (parent) {
|
|
90
|
-
// Get index among siblings with same tag name
|
|
91
|
-
const siblings = Array.from(parent.children).filter((child) => child.tagName.toLowerCase() === currentTagName);
|
|
92
|
-
const index = siblings.indexOf(current);
|
|
93
|
-
path.unshift(`${currentTagName}[${index}]`);
|
|
94
|
-
}
|
|
95
|
-
else {
|
|
96
|
-
path.unshift(currentTagName);
|
|
97
|
-
}
|
|
98
|
-
current = parent;
|
|
99
|
-
}
|
|
100
|
-
// Create hash from path (simple hash function)
|
|
101
|
-
const pathString = path.join('>');
|
|
102
|
-
let hash = 0;
|
|
103
|
-
for (let i = 0; i < pathString.length; i++) {
|
|
104
|
-
const char = pathString.charCodeAt(i);
|
|
105
|
-
hash = ((hash << 5) - hash) + char;
|
|
106
|
-
hash = hash & hash; // Convert to 32-bit integer
|
|
107
|
-
}
|
|
108
|
-
// Convert to positive hex string (6 chars)
|
|
109
|
-
const hashStr = Math.abs(hash).toString(36).substr(0, 6);
|
|
110
|
-
return `${tagName}-${hashStr}`;
|
|
111
|
-
};
|
|
112
|
-
const inferredId = generateDeterministicId(element, info.tagName);
|
|
113
|
-
// Build props signature from attributes
|
|
114
|
-
const props = [];
|
|
115
|
-
if (info.id)
|
|
116
|
-
props.push(`id=${info.id}`);
|
|
117
|
-
if (info.className) {
|
|
118
|
-
const classCount = info.className.split(/\s+/).length;
|
|
119
|
-
props.push(`classes=${classCount}`);
|
|
120
|
-
}
|
|
121
|
-
const propsSignature = props.length > 0 ? props.join(", ") : "default";
|
|
122
|
-
return {
|
|
123
|
-
componentName: inferredName,
|
|
124
|
-
componentId: inferredId,
|
|
125
|
-
variant: info.muiComponent ? info.muiComponent.toLowerCase() : undefined,
|
|
126
|
-
role: element.getAttribute("role") || element.getAttribute("aria-label") || undefined,
|
|
127
|
-
usagePath: "DOM Element",
|
|
128
|
-
instanceIndex: 0,
|
|
129
|
-
propsSignature,
|
|
130
|
-
sourceFile: "DOM",
|
|
131
|
-
};
|
|
132
|
-
};
|
|
133
|
-
/**
|
|
134
|
-
*
|
|
135
|
-
*/
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
//
|
|
267
|
-
if (
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
//
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Automatic inspection detection via data attributes
|
|
3
|
+
* Components can add data-inspection-* attributes and the system will detect them automatically
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Extract basic element info for inspection
|
|
7
|
+
*/
|
|
8
|
+
const extractElementInfo = (element) => {
|
|
9
|
+
const tagName = element.tagName.toLowerCase();
|
|
10
|
+
// Convert className to string - it can be a DOMTokenList or string
|
|
11
|
+
let className = "";
|
|
12
|
+
if (typeof element.className === "string") {
|
|
13
|
+
className = element.className;
|
|
14
|
+
}
|
|
15
|
+
else if (element.className) {
|
|
16
|
+
// DOMTokenList - convert to string
|
|
17
|
+
className = String(element.className);
|
|
18
|
+
}
|
|
19
|
+
const id = element.id || "";
|
|
20
|
+
// Extract text preview (first 50 chars)
|
|
21
|
+
const textContent = element.textContent || "";
|
|
22
|
+
const textPreview = textContent.trim().substring(0, 50).replace(/\s+/g, " ");
|
|
23
|
+
// Try to detect MUI components from class names
|
|
24
|
+
let muiComponent;
|
|
25
|
+
if (className && typeof className === "string" && className.includes("Mui")) {
|
|
26
|
+
const muiMatch = className.match(/Mui(\w+)/);
|
|
27
|
+
if (muiMatch) {
|
|
28
|
+
muiComponent = muiMatch[1];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return { tagName, className, id, textPreview, muiComponent };
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Parse component metadata from data attributes or infer from element
|
|
35
|
+
*/
|
|
36
|
+
export const parseInspectionMetadata = (element) => {
|
|
37
|
+
if (process.env.NODE_ENV !== "development") {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
// First, try to find explicit inspection metadata
|
|
41
|
+
const componentName = element.getAttribute("data-inspection-name");
|
|
42
|
+
const componentId = element.getAttribute("data-inspection-id");
|
|
43
|
+
if (componentName && componentId) {
|
|
44
|
+
// Has explicit metadata - use it
|
|
45
|
+
const variant = element.getAttribute("data-inspection-variant") || undefined;
|
|
46
|
+
const role = element.getAttribute("data-inspection-role") || undefined;
|
|
47
|
+
const usagePath = element.getAttribute("data-inspection-usage-path") || "Unknown";
|
|
48
|
+
const instanceIndex = parseInt(element.getAttribute("data-inspection-instance") || "0", 10);
|
|
49
|
+
const propsSignature = element.getAttribute("data-inspection-props") || "default";
|
|
50
|
+
const sourceFile = element.getAttribute("data-inspection-file") || "Unknown";
|
|
51
|
+
return {
|
|
52
|
+
componentName,
|
|
53
|
+
componentId,
|
|
54
|
+
variant,
|
|
55
|
+
role,
|
|
56
|
+
usagePath,
|
|
57
|
+
instanceIndex,
|
|
58
|
+
propsSignature,
|
|
59
|
+
sourceFile,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
// No explicit metadata - infer from element
|
|
63
|
+
const info = extractElementInfo(element);
|
|
64
|
+
// Generate component name
|
|
65
|
+
let inferredName = info.muiComponent || info.tagName.toUpperCase();
|
|
66
|
+
if (info.id) {
|
|
67
|
+
inferredName = `${inferredName} (${info.id})`;
|
|
68
|
+
}
|
|
69
|
+
else if (info.className) {
|
|
70
|
+
// Try to extract meaningful class name
|
|
71
|
+
const classes = info.className.split(/\s+/).filter(c => c && !c.startsWith("Mui") && c.length > 2);
|
|
72
|
+
if (classes.length > 0) {
|
|
73
|
+
inferredName = `${inferredName} .${classes[0]}`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Generate component ID - use deterministic hash based on element's position in DOM tree
|
|
77
|
+
// This ensures the same element always gets the same ID, even without explicit metadata
|
|
78
|
+
const generateDeterministicId = (el, tagName) => {
|
|
79
|
+
if (info.id) {
|
|
80
|
+
return info.id;
|
|
81
|
+
}
|
|
82
|
+
// Create a stable identifier based on element's path in DOM tree
|
|
83
|
+
const path = [];
|
|
84
|
+
let current = el;
|
|
85
|
+
// Walk up to body, collecting tag names and indices
|
|
86
|
+
while (current && current !== document.body && current !== document.documentElement) {
|
|
87
|
+
const currentTagName = current.tagName.toLowerCase();
|
|
88
|
+
const parent = current.parentElement;
|
|
89
|
+
if (parent) {
|
|
90
|
+
// Get index among siblings with same tag name
|
|
91
|
+
const siblings = Array.from(parent.children).filter((child) => child.tagName.toLowerCase() === currentTagName);
|
|
92
|
+
const index = siblings.indexOf(current);
|
|
93
|
+
path.unshift(`${currentTagName}[${index}]`);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
path.unshift(currentTagName);
|
|
97
|
+
}
|
|
98
|
+
current = parent;
|
|
99
|
+
}
|
|
100
|
+
// Create hash from path (simple hash function)
|
|
101
|
+
const pathString = path.join('>');
|
|
102
|
+
let hash = 0;
|
|
103
|
+
for (let i = 0; i < pathString.length; i++) {
|
|
104
|
+
const char = pathString.charCodeAt(i);
|
|
105
|
+
hash = ((hash << 5) - hash) + char;
|
|
106
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
107
|
+
}
|
|
108
|
+
// Convert to positive hex string (6 chars)
|
|
109
|
+
const hashStr = Math.abs(hash).toString(36).substr(0, 6);
|
|
110
|
+
return `${tagName}-${hashStr}`;
|
|
111
|
+
};
|
|
112
|
+
const inferredId = generateDeterministicId(element, info.tagName);
|
|
113
|
+
// Build props signature from attributes
|
|
114
|
+
const props = [];
|
|
115
|
+
if (info.id)
|
|
116
|
+
props.push(`id=${info.id}`);
|
|
117
|
+
if (info.className) {
|
|
118
|
+
const classCount = info.className.split(/\s+/).length;
|
|
119
|
+
props.push(`classes=${classCount}`);
|
|
120
|
+
}
|
|
121
|
+
const propsSignature = props.length > 0 ? props.join(", ") : "default";
|
|
122
|
+
return {
|
|
123
|
+
componentName: inferredName,
|
|
124
|
+
componentId: inferredId,
|
|
125
|
+
variant: info.muiComponent ? info.muiComponent.toLowerCase() : undefined,
|
|
126
|
+
role: element.getAttribute("role") || element.getAttribute("aria-label") || undefined,
|
|
127
|
+
usagePath: "DOM Element",
|
|
128
|
+
instanceIndex: 0,
|
|
129
|
+
propsSignature,
|
|
130
|
+
sourceFile: "DOM",
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* Inspect element at screen coordinates - used to start inspection immediately on mobile activation
|
|
135
|
+
*/
|
|
136
|
+
export const inspectAtPoint = (clientX, clientY, setHoveredComponent) => {
|
|
137
|
+
const target = document.elementFromPoint(clientX, clientY);
|
|
138
|
+
if (target && document.body.contains(target)) {
|
|
139
|
+
inspectElement(target, true, false, setHoveredComponent);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* Helper function to inspect an element (used by both mouse and touch events)
|
|
144
|
+
*/
|
|
145
|
+
const inspectElement = (target, isInspectionActive, isLocked, setHoveredComponent) => {
|
|
146
|
+
if (!isInspectionActive) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// If locked, don't update - keep current tooltip fixed
|
|
150
|
+
if (isLocked) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (!target || !document.body.contains(target)) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// Always show inspection for any element (not just ones with metadata)
|
|
157
|
+
// Walk up the DOM tree to find a meaningful element to inspect
|
|
158
|
+
let current = target;
|
|
159
|
+
let bestElement = null;
|
|
160
|
+
let bestMetadata = null;
|
|
161
|
+
let elementWithExplicitMetadata = null;
|
|
162
|
+
let metadataWithExplicitId = null;
|
|
163
|
+
// Skip text nodes and very small elements
|
|
164
|
+
const isMeaningfulElement = (el) => {
|
|
165
|
+
const rect = el.getBoundingClientRect();
|
|
166
|
+
// Skip elements that are too small (likely text nodes or empty spans)
|
|
167
|
+
if (rect.width < 5 && rect.height < 5) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
// Prefer elements with explicit metadata, IDs, or meaningful classes
|
|
171
|
+
return !!(el.getAttribute("data-inspection-name") ||
|
|
172
|
+
el.id ||
|
|
173
|
+
el.className ||
|
|
174
|
+
el.tagName !== "SPAN" && el.tagName !== "DIV");
|
|
175
|
+
};
|
|
176
|
+
// First pass: Look for elements with explicit inspection metadata
|
|
177
|
+
// This ensures we always find the same component regardless of which child is hovered
|
|
178
|
+
while (current) {
|
|
179
|
+
// Ensure element is still in the DOM
|
|
180
|
+
if (!document.body.contains(current)) {
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
// Check if this element has explicit inspection metadata
|
|
184
|
+
const hasExplicitMetadata = current.getAttribute("data-inspection-name") &&
|
|
185
|
+
current.getAttribute("data-inspection-id");
|
|
186
|
+
if (hasExplicitMetadata) {
|
|
187
|
+
const metadata = parseInspectionMetadata(current);
|
|
188
|
+
if (metadata) {
|
|
189
|
+
elementWithExplicitMetadata = current;
|
|
190
|
+
metadataWithExplicitId = metadata;
|
|
191
|
+
break; // Found explicit metadata, use it
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
current = current.parentElement;
|
|
195
|
+
// Stop at body to avoid inspecting the entire page
|
|
196
|
+
if (current && (current.tagName === "BODY" || current.tagName === "HTML")) {
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// If we found explicit metadata, use it (this ensures consistent IDs)
|
|
201
|
+
if (elementWithExplicitMetadata && metadataWithExplicitId) {
|
|
202
|
+
if (process.env.NODE_ENV === "development") {
|
|
203
|
+
console.log("[Inspection] Found element with explicit metadata:", metadataWithExplicitId.componentName);
|
|
204
|
+
}
|
|
205
|
+
setHoveredComponent(metadataWithExplicitId, elementWithExplicitMetadata);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
// Second pass: If no explicit metadata found, look for meaningful elements
|
|
209
|
+
// Reset current to target for second pass
|
|
210
|
+
current = target;
|
|
211
|
+
while (current) {
|
|
212
|
+
// Ensure element is still in the DOM
|
|
213
|
+
if (!document.body.contains(current)) {
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
// Check if this is a meaningful element
|
|
217
|
+
if (isMeaningfulElement(current)) {
|
|
218
|
+
const metadata = parseInspectionMetadata(current);
|
|
219
|
+
if (metadata) {
|
|
220
|
+
// Keep the first meaningful element (closest to target)
|
|
221
|
+
if (!bestElement) {
|
|
222
|
+
bestElement = current;
|
|
223
|
+
bestMetadata = metadata;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
current = current.parentElement;
|
|
228
|
+
// Stop at body to avoid inspecting the entire page
|
|
229
|
+
if (current && (current.tagName === "BODY" || current.tagName === "HTML")) {
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (bestElement && bestMetadata) {
|
|
234
|
+
if (process.env.NODE_ENV === "development") {
|
|
235
|
+
console.log("[Inspection] Found element:", bestMetadata.componentName);
|
|
236
|
+
}
|
|
237
|
+
setHoveredComponent(bestMetadata, bestElement);
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
// Fallback: show info for the target element itself
|
|
241
|
+
const fallbackMetadata = parseInspectionMetadata(target);
|
|
242
|
+
if (fallbackMetadata) {
|
|
243
|
+
setHoveredComponent(fallbackMetadata, target);
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
setHoveredComponent(null, null);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
/**
|
|
251
|
+
* Setup global mouse event listener for automatic inspection detection
|
|
252
|
+
* This works with components that have data-inspection-* attributes
|
|
253
|
+
*/
|
|
254
|
+
export const setupAutoInspection = (setHoveredComponent, isInspectionActive, isLocked) => {
|
|
255
|
+
if (process.env.NODE_ENV !== "development") {
|
|
256
|
+
return () => { }; // No-op in production
|
|
257
|
+
}
|
|
258
|
+
// Detect if device supports touch
|
|
259
|
+
const isTouchDevice = 'ontouchstart' in window ||
|
|
260
|
+
navigator.maxTouchPoints > 0 ||
|
|
261
|
+
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
|
262
|
+
const handleMouseMove = (e) => {
|
|
263
|
+
if (!isInspectionActive) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
// If locked, don't update on mouse move - keep current tooltip fixed
|
|
267
|
+
if (isLocked) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const target = e.target;
|
|
271
|
+
inspectElement(target, isInspectionActive, isLocked, setHoveredComponent);
|
|
272
|
+
};
|
|
273
|
+
// Touch move handler for mobile devices
|
|
274
|
+
const handleTouchMove = (e) => {
|
|
275
|
+
// Only process if inspection is active and we have touches
|
|
276
|
+
if (!isInspectionActive || e.touches.length === 0) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
// Get the element under the first touch point
|
|
280
|
+
const touch = e.touches[0];
|
|
281
|
+
const target = document.elementFromPoint(touch.clientX, touch.clientY);
|
|
282
|
+
if (target) {
|
|
283
|
+
inspectElement(target, isInspectionActive, isLocked, setHoveredComponent);
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
// Touch start handler for mobile - inspect on touch
|
|
287
|
+
const handleTouchStart = (e) => {
|
|
288
|
+
// Only process if inspection is active and we have touches
|
|
289
|
+
// Skip if it's a 3-finger touch (used for activation)
|
|
290
|
+
if (!isInspectionActive || e.touches.length === 3) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
// Get the element under the first touch point
|
|
294
|
+
const touch = e.touches[0];
|
|
295
|
+
const target = document.elementFromPoint(touch.clientX, touch.clientY);
|
|
296
|
+
if (target) {
|
|
297
|
+
inspectElement(target, isInspectionActive, isLocked, setHoveredComponent);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
const handleMouseLeave = () => {
|
|
301
|
+
if (isInspectionActive && !isLocked) {
|
|
302
|
+
setHoveredComponent(null, null);
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
const handleTouchEnd = () => {
|
|
306
|
+
// Clear inspection on touch end (unless locked)
|
|
307
|
+
if (isInspectionActive && !isLocked) {
|
|
308
|
+
setHoveredComponent(null, null);
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
// Add mouse event listeners
|
|
312
|
+
window.addEventListener("mousemove", handleMouseMove);
|
|
313
|
+
document.addEventListener("mouseleave", handleMouseLeave);
|
|
314
|
+
// Add touch event listeners for mobile devices
|
|
315
|
+
if (isTouchDevice) {
|
|
316
|
+
window.addEventListener("touchmove", handleTouchMove, { passive: true });
|
|
317
|
+
window.addEventListener("touchstart", handleTouchStart, { passive: true });
|
|
318
|
+
window.addEventListener("touchend", handleTouchEnd, { passive: true });
|
|
319
|
+
}
|
|
320
|
+
return () => {
|
|
321
|
+
window.removeEventListener("mousemove", handleMouseMove);
|
|
322
|
+
document.removeEventListener("mouseleave", handleMouseLeave);
|
|
323
|
+
if (isTouchDevice) {
|
|
324
|
+
window.removeEventListener("touchmove", handleTouchMove);
|
|
325
|
+
window.removeEventListener("touchstart", handleTouchStart);
|
|
326
|
+
window.removeEventListener("touchend", handleTouchEnd);
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
};
|