@linhey/react-debug-inspector 1.0.0 → 1.2.1
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/LICENSE +21 -0
- package/README.md +75 -2
- package/dist/index.d.mts +1 -2
- package/dist/index.d.ts +1 -2
- package/dist/index.js +557 -82
- package/dist/index.mjs +557 -82
- package/package.json +23 -2
package/dist/index.js
CHANGED
|
@@ -29,28 +29,42 @@ module.exports = __toCommonJS(index_exports);
|
|
|
29
29
|
function babelPluginDebugLabel() {
|
|
30
30
|
return {
|
|
31
31
|
visitor: {
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
32
|
+
Program(programPath, state) {
|
|
33
|
+
const filename = state.file.opts.filename || state.filename || "";
|
|
34
|
+
const cwd = state.cwd || (typeof process !== "undefined" ? process.cwd() : "");
|
|
35
|
+
const getRelativePath = (absolutePath) => {
|
|
36
|
+
if (!absolutePath) return "unknown";
|
|
37
|
+
if (absolutePath.startsWith(cwd)) {
|
|
38
|
+
return absolutePath.slice(cwd.length + 1);
|
|
39
|
+
}
|
|
40
|
+
return absolutePath;
|
|
41
|
+
};
|
|
42
|
+
const relativePath = getRelativePath(filename);
|
|
43
|
+
programPath.traverse({
|
|
44
|
+
FunctionDeclaration(path) {
|
|
45
|
+
const name = path.node.id?.name;
|
|
46
|
+
if (!name || name[0] !== name[0].toUpperCase()) return;
|
|
47
|
+
injectAllJSX(path, name, relativePath);
|
|
48
|
+
},
|
|
49
|
+
VariableDeclarator(path) {
|
|
50
|
+
const name = path.node.id?.name;
|
|
51
|
+
if (!name || name[0] !== name[0].toUpperCase()) return;
|
|
52
|
+
const init = path.node.init;
|
|
53
|
+
if (init && (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression")) {
|
|
54
|
+
injectAllJSX(path.get("init"), name, relativePath);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
44
58
|
}
|
|
45
59
|
}
|
|
46
60
|
};
|
|
47
|
-
function injectAllJSX(path, componentName) {
|
|
61
|
+
function injectAllJSX(path, componentName, filePath) {
|
|
48
62
|
path.traverse({
|
|
49
63
|
JSXElement(jsxPath) {
|
|
50
64
|
const { openingElement } = jsxPath.node;
|
|
51
65
|
const tagName = getTagName(openingElement.name);
|
|
52
66
|
const line = openingElement.loc?.start.line || "0";
|
|
53
|
-
const debugId = `${componentName}:${tagName}:${line}`;
|
|
67
|
+
const debugId = `${filePath}:${componentName}:${tagName}:${line}`;
|
|
54
68
|
const alreadyHas = openingElement.attributes.some(
|
|
55
69
|
(attr) => attr.type === "JSXAttribute" && attr.name?.name === "data-debug"
|
|
56
70
|
);
|
|
@@ -75,7 +89,30 @@ function babelPluginDebugLabel() {
|
|
|
75
89
|
// src/runtime.ts
|
|
76
90
|
function initInspector() {
|
|
77
91
|
if (typeof window === "undefined") return;
|
|
92
|
+
const win = window;
|
|
93
|
+
const doc = document;
|
|
94
|
+
const scopedWindow = win;
|
|
95
|
+
scopedWindow.__reactDebugInspectorCleanup__?.();
|
|
78
96
|
let isInspecting = false;
|
|
97
|
+
let overlay = null;
|
|
98
|
+
let tooltip = null;
|
|
99
|
+
let actionMenu = null;
|
|
100
|
+
let latestContext = null;
|
|
101
|
+
let lastHoveredDebugEl = null;
|
|
102
|
+
let latestHoverEvent = null;
|
|
103
|
+
let pendingHoverFrame = false;
|
|
104
|
+
let anchorUpdatePending = false;
|
|
105
|
+
const edgeOffset = 24;
|
|
106
|
+
const successColor = "#10b981";
|
|
107
|
+
const defaultOverlayBg = "rgba(14, 165, 233, 0.15)";
|
|
108
|
+
const defaultOverlayBorder = "#0ea5e9";
|
|
109
|
+
const actionButtons = {};
|
|
110
|
+
const scheduleFrame = (cb) => {
|
|
111
|
+
if (typeof win.requestAnimationFrame === "function") {
|
|
112
|
+
return win.requestAnimationFrame(cb);
|
|
113
|
+
}
|
|
114
|
+
return win.setTimeout(() => cb(Date.now()), 16);
|
|
115
|
+
};
|
|
79
116
|
const toggleBtn = document.createElement("button");
|
|
80
117
|
toggleBtn.innerHTML = "\u{1F3AF}";
|
|
81
118
|
toggleBtn.title = "\u5F00\u542F\u7EC4\u4EF6\u5B9A\u4F4D\u5668";
|
|
@@ -98,22 +135,397 @@ function initInspector() {
|
|
|
98
135
|
justify-content: center;
|
|
99
136
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
100
137
|
`;
|
|
101
|
-
|
|
102
|
-
const
|
|
138
|
+
doc.body.appendChild(toggleBtn);
|
|
139
|
+
const applyAnchor = (anchor) => {
|
|
140
|
+
toggleBtn.style.top = anchor.startsWith("top") ? `${edgeOffset}px` : "";
|
|
141
|
+
toggleBtn.style.bottom = anchor.startsWith("bottom") ? `${edgeOffset}px` : "";
|
|
142
|
+
if (anchor.endsWith("left")) {
|
|
143
|
+
toggleBtn.style.left = `${edgeOffset}px`;
|
|
144
|
+
toggleBtn.style.right = "";
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
toggleBtn.style.right = `${edgeOffset}px`;
|
|
148
|
+
toggleBtn.style.left = "";
|
|
149
|
+
};
|
|
150
|
+
const getVisibleDialogs = () => {
|
|
151
|
+
const candidates = Array.from(
|
|
152
|
+
document.querySelectorAll('[role="dialog"], dialog[open], [aria-modal="true"]')
|
|
153
|
+
);
|
|
154
|
+
return candidates.filter((node) => {
|
|
155
|
+
if (node.getAttribute("aria-hidden") === "true") return false;
|
|
156
|
+
if (node.getAttribute("data-aria-hidden") === "true") return false;
|
|
157
|
+
const style = window.getComputedStyle(node);
|
|
158
|
+
return style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0";
|
|
159
|
+
});
|
|
160
|
+
};
|
|
161
|
+
const getPreferredHost = (dialogs) => {
|
|
162
|
+
if (dialogs.length === 0) return document.body;
|
|
163
|
+
const topDialog = dialogs[dialogs.length - 1];
|
|
164
|
+
const portalHost = topDialog.closest("[data-radix-portal]");
|
|
165
|
+
if (portalHost) return topDialog;
|
|
166
|
+
return document.body;
|
|
167
|
+
};
|
|
168
|
+
const ensureToggleHost = (host) => {
|
|
169
|
+
if (toggleBtn.parentElement !== host) {
|
|
170
|
+
host.appendChild(toggleBtn);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
const ensureToggleVisible = () => {
|
|
174
|
+
toggleBtn.removeAttribute("aria-hidden");
|
|
175
|
+
toggleBtn.removeAttribute("data-aria-hidden");
|
|
176
|
+
toggleBtn.removeAttribute("inert");
|
|
177
|
+
if ("inert" in toggleBtn) {
|
|
178
|
+
toggleBtn.inert = false;
|
|
179
|
+
}
|
|
180
|
+
toggleBtn.style.pointerEvents = "auto";
|
|
181
|
+
};
|
|
182
|
+
const isIgnorableObstacle = (el) => {
|
|
183
|
+
if (el === toggleBtn || el === overlay || el === tooltip || el === actionMenu) return true;
|
|
184
|
+
if (el instanceof HTMLElement) {
|
|
185
|
+
if (el.contains(toggleBtn) || toggleBtn.contains(el)) return true;
|
|
186
|
+
if (overlay && el.contains(overlay)) return true;
|
|
187
|
+
if (tooltip && el.contains(tooltip)) return true;
|
|
188
|
+
if (actionMenu && (el.contains(actionMenu) || actionMenu.contains(el))) return true;
|
|
189
|
+
if (el === doc.body || el === doc.documentElement) return true;
|
|
190
|
+
if (el.getAttribute("data-inspector-ignore") === "true") return true;
|
|
191
|
+
const style = window.getComputedStyle(el);
|
|
192
|
+
if (style.pointerEvents === "none") return true;
|
|
193
|
+
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return true;
|
|
194
|
+
}
|
|
195
|
+
return false;
|
|
196
|
+
};
|
|
197
|
+
const sampleAnchorObstructionScore = () => {
|
|
198
|
+
const pickStack = typeof document.elementsFromPoint === "function" ? (x, y) => document.elementsFromPoint(x, y) : (x, y) => {
|
|
199
|
+
const one = doc.elementFromPoint?.(x, y);
|
|
200
|
+
return one ? [one] : [];
|
|
201
|
+
};
|
|
202
|
+
const rect = toggleBtn.getBoundingClientRect();
|
|
203
|
+
const points = [
|
|
204
|
+
[rect.left + rect.width / 2, rect.top + rect.height / 2],
|
|
205
|
+
[rect.left + 2, rect.top + 2],
|
|
206
|
+
[rect.right - 2, rect.top + 2],
|
|
207
|
+
[rect.left + 2, rect.bottom - 2],
|
|
208
|
+
[rect.right - 2, rect.bottom - 2]
|
|
209
|
+
];
|
|
210
|
+
let score = 0;
|
|
211
|
+
for (const [x, y] of points) {
|
|
212
|
+
const stack = pickStack(x, y);
|
|
213
|
+
const blocker = stack.find((el) => !isIgnorableObstacle(el));
|
|
214
|
+
if (blocker) score += 1;
|
|
215
|
+
}
|
|
216
|
+
return score;
|
|
217
|
+
};
|
|
218
|
+
const pickBestAnchor = (preferLeft = false) => {
|
|
219
|
+
const candidates = preferLeft ? ["bottom-left", "top-left", "bottom-right", "top-right"] : ["bottom-right", "top-right", "bottom-left", "top-left"];
|
|
220
|
+
let best = candidates[0];
|
|
221
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
222
|
+
for (const anchor of candidates) {
|
|
223
|
+
applyAnchor(anchor);
|
|
224
|
+
const score = sampleAnchorObstructionScore();
|
|
225
|
+
if (score < bestScore) {
|
|
226
|
+
bestScore = score;
|
|
227
|
+
best = anchor;
|
|
228
|
+
}
|
|
229
|
+
if (score === 0) break;
|
|
230
|
+
}
|
|
231
|
+
applyAnchor(best);
|
|
232
|
+
};
|
|
233
|
+
const updateAnchorForDialogs = () => {
|
|
234
|
+
const dialogs = getVisibleDialogs();
|
|
235
|
+
ensureToggleHost(getPreferredHost(dialogs));
|
|
236
|
+
ensureToggleVisible();
|
|
237
|
+
if (dialogs.length > 0) {
|
|
238
|
+
pickBestAnchor(true);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
pickBestAnchor(false);
|
|
242
|
+
};
|
|
243
|
+
const scheduleAnchorUpdate = () => {
|
|
244
|
+
if (anchorUpdatePending) return;
|
|
245
|
+
anchorUpdatePending = true;
|
|
246
|
+
scheduleFrame(() => {
|
|
247
|
+
anchorUpdatePending = false;
|
|
248
|
+
updateAnchorForDialogs();
|
|
249
|
+
});
|
|
250
|
+
};
|
|
251
|
+
const formatDebugId = (debugId) => {
|
|
252
|
+
const parts = debugId.split(":");
|
|
253
|
+
if (parts.length === 4) {
|
|
254
|
+
const [filePath, componentName, tagName, line] = parts;
|
|
255
|
+
const fileName = filePath.split("/").pop() || filePath;
|
|
256
|
+
return `${fileName} \u203A ${componentName} \u203A ${tagName}:${line}`;
|
|
257
|
+
}
|
|
258
|
+
return debugId.replace(/:/g, " \u203A ");
|
|
259
|
+
};
|
|
260
|
+
const normalizeText = (value) => value.replace(/\s+/g, " ").trim();
|
|
261
|
+
const truncateText = (value, limit = 500) => {
|
|
262
|
+
if (value.length <= limit) return value;
|
|
263
|
+
return `${value.slice(0, limit - 3)}...`;
|
|
264
|
+
};
|
|
265
|
+
const suppressEvent = (event, { preventDefault = false, immediate = false } = {}) => {
|
|
266
|
+
if (preventDefault) {
|
|
267
|
+
event.preventDefault();
|
|
268
|
+
}
|
|
269
|
+
event.stopPropagation();
|
|
270
|
+
if (immediate) {
|
|
271
|
+
event.stopImmediatePropagation?.();
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
const extractTextContent = (target) => {
|
|
275
|
+
const ariaLabel = normalizeText(target.getAttribute("aria-label") || "");
|
|
276
|
+
if (ariaLabel) return truncateText(ariaLabel);
|
|
277
|
+
if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement) {
|
|
278
|
+
const inputValue = normalizeText(target.value || "");
|
|
279
|
+
if (inputValue) return truncateText(inputValue);
|
|
280
|
+
}
|
|
281
|
+
const alt = normalizeText(target.getAttribute("alt") || "");
|
|
282
|
+
if (alt) return truncateText(alt);
|
|
283
|
+
const title = normalizeText(target.getAttribute("title") || "");
|
|
284
|
+
if (title) return truncateText(title);
|
|
285
|
+
const textValue = normalizeText(target.innerText || target.textContent || "");
|
|
286
|
+
if (textValue) return truncateText(textValue);
|
|
287
|
+
return null;
|
|
288
|
+
};
|
|
289
|
+
const resolveImageTarget = (target) => {
|
|
290
|
+
const imageEl = target instanceof HTMLImageElement ? target : target.querySelector("img") || target.closest("img");
|
|
291
|
+
if (imageEl instanceof HTMLImageElement) {
|
|
292
|
+
const url = imageEl.currentSrc || imageEl.src;
|
|
293
|
+
if (!url) return null;
|
|
294
|
+
return {
|
|
295
|
+
url,
|
|
296
|
+
alt: imageEl.alt || "",
|
|
297
|
+
title: imageEl.title || "",
|
|
298
|
+
filename: url.split("/").pop()?.split("?")[0] || "",
|
|
299
|
+
source: "img"
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
let node = target;
|
|
303
|
+
while (node) {
|
|
304
|
+
const bg = window.getComputedStyle(node).backgroundImage;
|
|
305
|
+
const match = bg.match(/url\(["']?(.*?)["']?\)/);
|
|
306
|
+
if (match?.[1]) {
|
|
307
|
+
const url = match[1];
|
|
308
|
+
return {
|
|
309
|
+
url,
|
|
310
|
+
alt: node.getAttribute("aria-label") || "",
|
|
311
|
+
title: node.getAttribute("title") || "",
|
|
312
|
+
filename: url.split("/").pop()?.split("?")[0] || "",
|
|
313
|
+
source: "background"
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
node = node.parentElement;
|
|
317
|
+
}
|
|
318
|
+
return null;
|
|
319
|
+
};
|
|
320
|
+
const buildImageMetadataText = (image, debugId) => [
|
|
321
|
+
"[image]",
|
|
322
|
+
`url: ${image.url}`,
|
|
323
|
+
`alt: ${image.alt}`,
|
|
324
|
+
`title: ${image.title}`,
|
|
325
|
+
`filename: ${image.filename}`,
|
|
326
|
+
`debugId: ${debugId}`
|
|
327
|
+
].join("\n");
|
|
328
|
+
const buildCopyAllPayload = (context) => {
|
|
329
|
+
const textValue = extractTextContent(context.debugEl);
|
|
330
|
+
const image = resolveImageTarget(context.debugEl);
|
|
331
|
+
const lines = [
|
|
332
|
+
"[debug]",
|
|
333
|
+
`id: ${context.debugId}`,
|
|
334
|
+
`display: ${formatDebugId(context.debugId)}`
|
|
335
|
+
];
|
|
336
|
+
if (textValue) {
|
|
337
|
+
lines.push("", "[text]", `value: ${textValue}`);
|
|
338
|
+
}
|
|
339
|
+
if (image) {
|
|
340
|
+
lines.push("", "[image]", `url: ${image.url}`, `alt: ${image.alt}`, `title: ${image.title}`, `filename: ${image.filename}`);
|
|
341
|
+
}
|
|
342
|
+
return lines.join("\n");
|
|
343
|
+
};
|
|
344
|
+
const syncActionMenuVisibility = (context) => {
|
|
345
|
+
actionButtons.id && (actionButtons.id.style.display = "");
|
|
346
|
+
actionButtons.all && (actionButtons.all.style.display = "");
|
|
347
|
+
if (actionButtons.text) {
|
|
348
|
+
actionButtons.text.style.display = extractTextContent(context.debugEl) ? "" : "none";
|
|
349
|
+
}
|
|
350
|
+
if (actionButtons.image) {
|
|
351
|
+
actionButtons.image.style.display = resolveImageTarget(context.debugEl) ? "" : "none";
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
const setOverlayTone = (tone) => {
|
|
355
|
+
if (!overlay) return;
|
|
356
|
+
if (tone === "success") {
|
|
357
|
+
overlay.style.background = "rgba(16, 185, 129, 0.2)";
|
|
358
|
+
overlay.style.borderColor = successColor;
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
overlay.style.background = "rgba(245, 158, 11, 0.18)";
|
|
362
|
+
overlay.style.borderColor = "#f59e0b";
|
|
363
|
+
};
|
|
364
|
+
const resetOverlayTone = () => {
|
|
365
|
+
if (!overlay) return;
|
|
366
|
+
overlay.style.background = defaultOverlayBg;
|
|
367
|
+
overlay.style.borderColor = defaultOverlayBorder;
|
|
368
|
+
};
|
|
369
|
+
const showCopyFeedback = (message, tone) => {
|
|
370
|
+
if (!tooltip) return;
|
|
371
|
+
tooltip.textContent = tone === "success" ? `\u2705 ${message}` : `\u26A0\uFE0F ${message}`;
|
|
372
|
+
tooltip.style.color = tone === "success" ? successColor : "#f59e0b";
|
|
373
|
+
tooltip.title = latestContext?.debugId || "";
|
|
374
|
+
setOverlayTone(tone);
|
|
375
|
+
};
|
|
376
|
+
const resetTooltipContent = (context) => {
|
|
377
|
+
if (!tooltip) return;
|
|
378
|
+
tooltip.textContent = formatDebugId(context.debugId);
|
|
379
|
+
tooltip.style.color = "#38bdf8";
|
|
380
|
+
tooltip.title = context.debugId;
|
|
381
|
+
resetOverlayTone();
|
|
382
|
+
};
|
|
383
|
+
const positionActionMenu = (context) => {
|
|
384
|
+
if (!tooltip || !actionMenu) return;
|
|
385
|
+
const rect = context.rect;
|
|
386
|
+
const menuWidth = actionMenu.offsetWidth || 248;
|
|
387
|
+
const menuHeight = actionMenu.offsetHeight || 40;
|
|
388
|
+
const tooltipRect = tooltip.getBoundingClientRect();
|
|
389
|
+
let left = tooltipRect.right + 8;
|
|
390
|
+
if (left + menuWidth > win.innerWidth - 8) {
|
|
391
|
+
left = Math.max(8, tooltipRect.left - menuWidth - 8);
|
|
392
|
+
}
|
|
393
|
+
let top = tooltipRect.top;
|
|
394
|
+
if (top + menuHeight > win.innerHeight - 8) {
|
|
395
|
+
top = Math.max(8, rect.bottom - menuHeight);
|
|
396
|
+
}
|
|
397
|
+
if (top < 8) {
|
|
398
|
+
top = Math.min(win.innerHeight - menuHeight - 8, rect.bottom + 8);
|
|
399
|
+
}
|
|
400
|
+
actionMenu.style.left = `${left}px`;
|
|
401
|
+
actionMenu.style.top = `${top}px`;
|
|
402
|
+
};
|
|
403
|
+
const showActionMenu = (context) => {
|
|
404
|
+
if (!actionMenu) return;
|
|
405
|
+
syncActionMenuVisibility(context);
|
|
406
|
+
actionMenu.style.display = "flex";
|
|
407
|
+
positionActionMenu(context);
|
|
408
|
+
};
|
|
409
|
+
const hideActionMenu = () => {
|
|
410
|
+
if (!actionMenu) return;
|
|
411
|
+
actionMenu.style.display = "none";
|
|
412
|
+
};
|
|
413
|
+
const copyText = async (value) => {
|
|
414
|
+
await navigator.clipboard.writeText(value);
|
|
415
|
+
};
|
|
416
|
+
const copyImageBinary = async (image, debugId) => {
|
|
417
|
+
if (image.url.startsWith("data:")) {
|
|
418
|
+
await copyText(buildImageMetadataText(image, debugId));
|
|
419
|
+
return "metadata";
|
|
420
|
+
}
|
|
421
|
+
const canWriteBinary = typeof navigator.clipboard?.write === "function" && typeof window.ClipboardItem === "function";
|
|
422
|
+
if (canWriteBinary) {
|
|
423
|
+
try {
|
|
424
|
+
const response = await win.fetch(image.url);
|
|
425
|
+
if (!response.ok) throw new Error(`failed:${response.status}`);
|
|
426
|
+
const blob = await response.blob();
|
|
427
|
+
if (!blob.type.startsWith("image/")) throw new Error("not-image");
|
|
428
|
+
const item = new window.ClipboardItem({ [blob.type]: blob });
|
|
429
|
+
await navigator.clipboard.write([item]);
|
|
430
|
+
return "binary";
|
|
431
|
+
} catch {
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
await copyText(buildImageMetadataText(image, debugId));
|
|
435
|
+
return "metadata";
|
|
436
|
+
};
|
|
437
|
+
const performCopyAction = async (action, context) => {
|
|
438
|
+
if (action === "id") {
|
|
439
|
+
await copyText(context.debugId);
|
|
440
|
+
showCopyFeedback("\u5DF2\u590D\u5236 Debug ID", "success");
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
if (action === "text") {
|
|
444
|
+
const textValue = extractTextContent(context.debugEl);
|
|
445
|
+
if (!textValue) return;
|
|
446
|
+
await copyText(textValue);
|
|
447
|
+
showCopyFeedback("\u5DF2\u590D\u5236\u6587\u6848", "success");
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (action === "image") {
|
|
451
|
+
const image = resolveImageTarget(context.debugEl);
|
|
452
|
+
if (!image) {
|
|
453
|
+
showCopyFeedback("\u672A\u627E\u5230\u56FE\u7247", "warning");
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const copyResult = await copyImageBinary(image, context.debugId);
|
|
457
|
+
showCopyFeedback(copyResult === "binary" ? "\u5DF2\u590D\u5236\u56FE\u7247" : "\u5DF2\u590D\u5236\u56FE\u7247\u4FE1\u606F", "success");
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
await copyText(buildCopyAllPayload(context));
|
|
461
|
+
showCopyFeedback("\u5DF2\u590D\u5236\u5168\u90E8\u4FE1\u606F", "success");
|
|
462
|
+
};
|
|
463
|
+
const inspectByPointer = (event) => {
|
|
464
|
+
if (!isInspecting || !overlay || !tooltip) return;
|
|
465
|
+
const target = event.target;
|
|
466
|
+
if (!target) return;
|
|
467
|
+
if (target === toggleBtn || target === overlay || target === tooltip || target.closest('[data-inspector-ignore="true"]')) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
const debugEl = target.closest("[data-debug]");
|
|
471
|
+
if (debugEl && debugEl === lastHoveredDebugEl && latestContext) {
|
|
472
|
+
showActionMenu(latestContext);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
if (!debugEl) {
|
|
476
|
+
overlay.style.display = "none";
|
|
477
|
+
tooltip.style.display = "none";
|
|
478
|
+
hideActionMenu();
|
|
479
|
+
latestContext = null;
|
|
480
|
+
lastHoveredDebugEl = null;
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
const debugId = debugEl.getAttribute("data-debug") || "";
|
|
484
|
+
const rect = debugEl.getBoundingClientRect();
|
|
485
|
+
latestContext = { debugEl, debugId, rect };
|
|
486
|
+
lastHoveredDebugEl = debugEl;
|
|
487
|
+
overlay.style.display = "block";
|
|
488
|
+
overlay.style.top = `${rect.top}px`;
|
|
489
|
+
overlay.style.left = `${rect.left}px`;
|
|
490
|
+
overlay.style.width = `${rect.width}px`;
|
|
491
|
+
overlay.style.height = `${rect.height}px`;
|
|
492
|
+
tooltip.style.display = "block";
|
|
493
|
+
resetTooltipContent(latestContext);
|
|
494
|
+
const tooltipY = rect.top < 30 ? rect.bottom + 4 : rect.top - 28;
|
|
495
|
+
tooltip.style.top = `${tooltipY}px`;
|
|
496
|
+
tooltip.style.left = `${rect.left}px`;
|
|
497
|
+
showActionMenu(latestContext);
|
|
498
|
+
};
|
|
499
|
+
const stopInspecting = () => {
|
|
500
|
+
isInspecting = false;
|
|
501
|
+
latestContext = null;
|
|
502
|
+
lastHoveredDebugEl = null;
|
|
503
|
+
toggleBtn.style.transform = "scale(1)";
|
|
504
|
+
toggleBtn.style.background = "#0ea5e9";
|
|
505
|
+
document.body.style.cursor = "";
|
|
506
|
+
if (overlay) overlay.style.display = "none";
|
|
507
|
+
if (tooltip) tooltip.style.display = "none";
|
|
508
|
+
hideActionMenu();
|
|
509
|
+
resetOverlayTone();
|
|
510
|
+
};
|
|
511
|
+
const dialogObserver = new MutationObserver(() => {
|
|
512
|
+
scheduleAnchorUpdate();
|
|
513
|
+
});
|
|
514
|
+
overlay = document.createElement("div");
|
|
103
515
|
overlay.style.cssText = `
|
|
104
516
|
position: fixed;
|
|
105
517
|
pointer-events: none;
|
|
106
518
|
z-index: 9999998;
|
|
107
|
-
background:
|
|
108
|
-
border: 2px dashed
|
|
519
|
+
background: ${defaultOverlayBg};
|
|
520
|
+
border: 2px dashed ${defaultOverlayBorder};
|
|
109
521
|
display: none;
|
|
110
522
|
transition: all 0.1s ease-out;
|
|
111
523
|
`;
|
|
112
|
-
|
|
113
|
-
|
|
524
|
+
doc.body.appendChild(overlay);
|
|
525
|
+
tooltip = document.createElement("div");
|
|
114
526
|
tooltip.style.cssText = `
|
|
115
527
|
position: fixed;
|
|
116
|
-
pointer-events:
|
|
528
|
+
pointer-events: auto;
|
|
117
529
|
z-index: 9999999;
|
|
118
530
|
background: #1e293b;
|
|
119
531
|
color: #38bdf8;
|
|
@@ -126,79 +538,142 @@ function initInspector() {
|
|
|
126
538
|
display: none;
|
|
127
539
|
white-space: nowrap;
|
|
128
540
|
transition: all 0.1s ease-out;
|
|
541
|
+
cursor: help;
|
|
129
542
|
`;
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
543
|
+
doc.body.appendChild(tooltip);
|
|
544
|
+
actionMenu = document.createElement("div");
|
|
545
|
+
actionMenu.setAttribute("data-inspector-ignore", "true");
|
|
546
|
+
actionMenu.style.cssText = `
|
|
547
|
+
position: fixed;
|
|
548
|
+
z-index: 10000000;
|
|
549
|
+
display: none;
|
|
550
|
+
gap: 6px;
|
|
551
|
+
padding: 6px;
|
|
552
|
+
border-radius: 10px;
|
|
553
|
+
background: rgba(15, 23, 42, 0.96);
|
|
554
|
+
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.28);
|
|
555
|
+
border: 1px solid rgba(148, 163, 184, 0.25);
|
|
556
|
+
align-items: center;
|
|
557
|
+
`;
|
|
558
|
+
const actionDefinitions = [
|
|
559
|
+
{ action: "id", label: "\u590D\u5236 ID" },
|
|
560
|
+
{ action: "text", label: "\u590D\u5236\u6587\u6848" },
|
|
561
|
+
{ action: "image", label: "\u590D\u5236\u56FE\u7247" },
|
|
562
|
+
{ action: "all", label: "\u5168\u90E8\u590D\u5236" }
|
|
563
|
+
];
|
|
564
|
+
for (const definition of actionDefinitions) {
|
|
565
|
+
const button = document.createElement("button");
|
|
566
|
+
button.type = "button";
|
|
567
|
+
button.textContent = definition.label;
|
|
568
|
+
button.dataset.inspectorIgnore = "true";
|
|
569
|
+
button.dataset.inspectorAction = definition.action;
|
|
570
|
+
button.style.cssText = `
|
|
571
|
+
border: 0;
|
|
572
|
+
border-radius: 8px;
|
|
573
|
+
padding: 6px 10px;
|
|
574
|
+
font-size: 12px;
|
|
575
|
+
font-weight: 600;
|
|
576
|
+
color: #e2e8f0;
|
|
577
|
+
background: rgba(30, 41, 59, 0.95);
|
|
578
|
+
cursor: pointer;
|
|
579
|
+
white-space: nowrap;
|
|
580
|
+
`;
|
|
581
|
+
button.addEventListener("click", async (event) => {
|
|
582
|
+
suppressEvent(event, { preventDefault: true });
|
|
583
|
+
if (!latestContext) return;
|
|
584
|
+
await performCopyAction(definition.action, latestContext);
|
|
585
|
+
});
|
|
586
|
+
actionButtons[definition.action] = button;
|
|
587
|
+
actionMenu.appendChild(button);
|
|
588
|
+
}
|
|
589
|
+
doc.body.appendChild(actionMenu);
|
|
590
|
+
const stopTogglePropagation = (event) => {
|
|
591
|
+
suppressEvent(event);
|
|
138
592
|
};
|
|
139
|
-
|
|
593
|
+
const shieldedEvents = ["pointerdown", "pointerup", "mousedown", "mouseup", "click", "touchstart", "touchend"];
|
|
594
|
+
for (const eventName of shieldedEvents) {
|
|
595
|
+
toggleBtn.addEventListener(eventName, stopTogglePropagation);
|
|
596
|
+
}
|
|
597
|
+
const handleToggleClick = (event) => {
|
|
598
|
+
suppressEvent(event);
|
|
140
599
|
isInspecting = !isInspecting;
|
|
141
600
|
if (isInspecting) {
|
|
142
601
|
toggleBtn.style.transform = "scale(0.9)";
|
|
143
602
|
toggleBtn.style.background = "#ef4444";
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
stopInspecting();
|
|
603
|
+
doc.body.style.cursor = "crosshair";
|
|
604
|
+
return;
|
|
147
605
|
}
|
|
606
|
+
stopInspecting();
|
|
607
|
+
};
|
|
608
|
+
toggleBtn.onclick = handleToggleClick;
|
|
609
|
+
const handleMouseMove = (event) => {
|
|
610
|
+
if (!isInspecting) return;
|
|
611
|
+
latestHoverEvent = event;
|
|
612
|
+
if (pendingHoverFrame) return;
|
|
613
|
+
pendingHoverFrame = true;
|
|
614
|
+
scheduleFrame(() => {
|
|
615
|
+
pendingHoverFrame = false;
|
|
616
|
+
if (!latestHoverEvent) return;
|
|
617
|
+
inspectByPointer(latestHoverEvent);
|
|
618
|
+
});
|
|
148
619
|
};
|
|
149
|
-
|
|
620
|
+
const handleWindowClick = (event) => {
|
|
150
621
|
if (!isInspecting) return;
|
|
151
|
-
const target =
|
|
152
|
-
if (target
|
|
622
|
+
const target = event.target;
|
|
623
|
+
if (!target || target === toggleBtn) return;
|
|
624
|
+
if (target.closest('[data-inspector-ignore="true"]')) {
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
suppressEvent(event, { preventDefault: true, immediate: true });
|
|
153
628
|
const debugEl = target.closest("[data-debug]");
|
|
154
|
-
if (debugEl) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
overlay.style.display = "block";
|
|
158
|
-
overlay.style.top = rect.top + "px";
|
|
159
|
-
overlay.style.left = rect.left + "px";
|
|
160
|
-
overlay.style.width = rect.width + "px";
|
|
161
|
-
overlay.style.height = rect.height + "px";
|
|
162
|
-
tooltip.style.display = "block";
|
|
163
|
-
tooltip.textContent = debugId;
|
|
164
|
-
tooltip.style.color = "#38bdf8";
|
|
165
|
-
const tooltipY = rect.top < 30 ? rect.bottom + 4 : rect.top - 28;
|
|
166
|
-
tooltip.style.top = tooltipY + "px";
|
|
167
|
-
tooltip.style.left = rect.left + "px";
|
|
168
|
-
} else {
|
|
169
|
-
overlay.style.display = "none";
|
|
170
|
-
tooltip.style.display = "none";
|
|
629
|
+
if (!debugEl) {
|
|
630
|
+
stopInspecting();
|
|
631
|
+
return;
|
|
171
632
|
}
|
|
633
|
+
const debugId = debugEl.getAttribute("data-debug");
|
|
634
|
+
if (!debugId) return;
|
|
635
|
+
copyText(debugId).then(() => {
|
|
636
|
+
showCopyFeedback("Copied!", "success");
|
|
637
|
+
win.setTimeout(stopInspecting, 600);
|
|
638
|
+
});
|
|
639
|
+
};
|
|
640
|
+
const handleKeyDown = (event) => {
|
|
641
|
+
if (event.key === "Escape" && isInspecting) stopInspecting();
|
|
642
|
+
};
|
|
643
|
+
const handleBeforeUnload = () => {
|
|
644
|
+
dialogObserver.disconnect();
|
|
645
|
+
};
|
|
646
|
+
dialogObserver.observe(doc.body, {
|
|
647
|
+
childList: true,
|
|
648
|
+
subtree: true,
|
|
649
|
+
attributes: true,
|
|
650
|
+
attributeFilter: ["style", "class", "open", "aria-hidden", "data-aria-hidden", "inert"]
|
|
172
651
|
});
|
|
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
|
-
window.addEventListener("keydown", (e) => {
|
|
200
|
-
if (e.key === "Escape" && isInspecting) stopInspecting();
|
|
201
|
-
});
|
|
652
|
+
win.addEventListener("beforeunload", handleBeforeUnload, { once: true });
|
|
653
|
+
win.addEventListener("resize", scheduleAnchorUpdate);
|
|
654
|
+
win.addEventListener("scroll", scheduleAnchorUpdate, true);
|
|
655
|
+
doc.addEventListener("mousemove", handleMouseMove);
|
|
656
|
+
win.addEventListener("click", handleWindowClick, { capture: true });
|
|
657
|
+
win.addEventListener("keydown", handleKeyDown);
|
|
658
|
+
updateAnchorForDialogs();
|
|
659
|
+
scopedWindow.__reactDebugInspectorCleanup__ = () => {
|
|
660
|
+
dialogObserver.disconnect();
|
|
661
|
+
win.removeEventListener("beforeunload", handleBeforeUnload);
|
|
662
|
+
win.removeEventListener("resize", scheduleAnchorUpdate);
|
|
663
|
+
win.removeEventListener("scroll", scheduleAnchorUpdate, true);
|
|
664
|
+
doc.removeEventListener("mousemove", handleMouseMove);
|
|
665
|
+
win.removeEventListener("click", handleWindowClick, { capture: true });
|
|
666
|
+
win.removeEventListener("keydown", handleKeyDown);
|
|
667
|
+
for (const eventName of shieldedEvents) {
|
|
668
|
+
toggleBtn.removeEventListener(eventName, stopTogglePropagation);
|
|
669
|
+
}
|
|
670
|
+
toggleBtn.onclick = null;
|
|
671
|
+
overlay?.remove();
|
|
672
|
+
tooltip?.remove();
|
|
673
|
+
actionMenu?.remove();
|
|
674
|
+
toggleBtn.remove();
|
|
675
|
+
delete scopedWindow.__reactDebugInspectorCleanup__;
|
|
676
|
+
};
|
|
202
677
|
}
|
|
203
678
|
|
|
204
679
|
// src/index.ts
|