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