vite-plugin-devtools-vue2 0.1.1 → 0.1.3
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/lib/config.js +22 -0
- package/lib/events.js +80 -0
- package/lib/hook.js +12 -6
- package/lib/inspector.js +11 -5
- package/lib/main.js +4 -1
- package/lib/panel.js +562 -201
- package/lib/picker.js +55 -31
- package/lib/vuex.js +16 -9
- package/lib/walker.js +46 -18
- package/package.json +1 -1
package/lib/panel.js
CHANGED
|
@@ -3,25 +3,14 @@
|
|
|
3
3
|
// on every Vue scheduler flush.
|
|
4
4
|
|
|
5
5
|
import { LitElement, css, html } from 'lit';
|
|
6
|
+
import { DragThreshold, EdgeMargin, PanelEdge, PanelH, PanelW, SectionFilterThreshold, StoreKey } from './config.js';
|
|
6
7
|
import hook from './hook.js';
|
|
7
8
|
import { hide, highlight } from './inspector.js';
|
|
8
9
|
import { isPicking, startPicking, stopPicking } from './picker.js';
|
|
9
10
|
import { commitAll, getSnapshots, getStore, hasStore, travelTo, subscribe as vuexSubscribe } from './vuex.js';
|
|
11
|
+
import { clearEvents, getEvents, subscribe as eventsSubscribe } from './events.js';
|
|
10
12
|
import { buildTree, formatValue, getInstance } from './walker.js';
|
|
11
13
|
|
|
12
|
-
// Persisted UI state (survives reloads). Only stable, cheap bits — not the
|
|
13
|
-
// component tree / expanded set (ids are regenerated each load).
|
|
14
|
-
const STORE_KEY = 'vue-devtools:ui';
|
|
15
|
-
|
|
16
|
-
// Panel size (keep in sync with .panel CSS) — used to keep it on-screen.
|
|
17
|
-
const PANEL_W = 620;
|
|
18
|
-
const PANEL_H = 420;
|
|
19
|
-
const EDGE_MARGIN = 12;
|
|
20
|
-
// Distance from the viewport edge to the panel when open (leaves room for the
|
|
21
|
-
// floating entry to sit in the gutter, matching the official devtools).
|
|
22
|
-
const PANEL_EDGE = EDGE_MARGIN + 30 / 2;
|
|
23
|
-
const DRAG_THRESHOLD = 4;
|
|
24
|
-
|
|
25
14
|
export class VueDevToolsPanel extends LitElement {
|
|
26
15
|
static properties = {
|
|
27
16
|
tree: { state: true },
|
|
@@ -32,6 +21,8 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
32
21
|
query: { state: true },
|
|
33
22
|
tab: { state: true },
|
|
34
23
|
vuexSelected: { state: true },
|
|
24
|
+
timelineSelected: { state: true },
|
|
25
|
+
theme: { state: true },
|
|
35
26
|
renderCodeText: { state: true }
|
|
36
27
|
};
|
|
37
28
|
|
|
@@ -47,9 +38,15 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
47
38
|
this.query = '';
|
|
48
39
|
this.tab = ui.tab || 'components';
|
|
49
40
|
this.vuexSelected = 0;
|
|
41
|
+
this.timelineSelected = null;
|
|
42
|
+
this.theme = ui.theme || 'light';
|
|
50
43
|
this.renderCodeText = null;
|
|
51
44
|
this.valueExpanded = new Set();
|
|
52
45
|
this.sectionCollapsed = new Set();
|
|
46
|
+
// Nodes collapsed within search results (default: all shown expanded).
|
|
47
|
+
this._searchCollapsed = new Set();
|
|
48
|
+
// Per-section (props/data/…) key filter text, keyed by section title.
|
|
49
|
+
this._sectionFilter = {};
|
|
53
50
|
// Docked position of the entry/panel. Defaults to the bottom edge near
|
|
54
51
|
// the right; { edge: 'left'|'right'|'top'|'bottom', along: number }.
|
|
55
52
|
this._pos = ui.pos || { edge: 'bottom', along: Number.POSITIVE_INFINITY };
|
|
@@ -66,14 +63,16 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
66
63
|
// derived from `el.__vue__` anyway.
|
|
67
64
|
this._domObserver = null;
|
|
68
65
|
this._onFlush = () => this._scheduleRefresh();
|
|
69
|
-
this._onKeydown =
|
|
66
|
+
this._onKeydown = event => this._handleKeydown(event);
|
|
70
67
|
// Keep the entry/panel on-screen when the viewport shrinks. resize can
|
|
71
68
|
// fire many times per second while dragging the window edge, and
|
|
72
69
|
// _applyPos reads layout then writes styles — so coalesce to at most one
|
|
73
70
|
// call per frame via rAF to avoid layout thrashing.
|
|
74
71
|
this._resizeRaf = 0;
|
|
75
72
|
this._onResize = () => {
|
|
76
|
-
if (this._resizeRaf)
|
|
73
|
+
if (this._resizeRaf) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
77
76
|
this._resizeRaf = requestAnimationFrame(() => {
|
|
78
77
|
this._resizeRaf = 0;
|
|
79
78
|
this._applyPos();
|
|
@@ -83,8 +82,8 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
83
82
|
|
|
84
83
|
static _loadUiState() {
|
|
85
84
|
try {
|
|
86
|
-
return JSON.parse(localStorage.getItem(
|
|
87
|
-
} catch (
|
|
85
|
+
return JSON.parse(localStorage.getItem(StoreKey)) || {};
|
|
86
|
+
} catch (err) {
|
|
88
87
|
return {};
|
|
89
88
|
}
|
|
90
89
|
}
|
|
@@ -92,8 +91,8 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
92
91
|
_persistUiState() {
|
|
93
92
|
try {
|
|
94
93
|
const pos = this._pos && Number.isFinite(this._pos.along) ? this._pos : undefined;
|
|
95
|
-
localStorage.setItem(
|
|
96
|
-
} catch (
|
|
94
|
+
localStorage.setItem(StoreKey, JSON.stringify({ collapsed: this.collapsed, tab: this.tab, theme: this.theme, pos }));
|
|
95
|
+
} catch (err) {
|
|
97
96
|
/* storage unavailable — ignore */
|
|
98
97
|
}
|
|
99
98
|
}
|
|
@@ -103,97 +102,107 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
103
102
|
// the viewport; clamped to stay fully on-screen.
|
|
104
103
|
_applyPos() {
|
|
105
104
|
const entry = this.renderRoot && this.renderRoot.querySelector('.entry');
|
|
106
|
-
if (!entry)
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
if (!entry) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const pos = this._pos || { edge: 'bottom', along: Number.POSITIVE_INFINITY };
|
|
109
|
+
const margin = EdgeMargin;
|
|
109
110
|
const vw = window.innerWidth;
|
|
110
111
|
const vh = window.innerHeight;
|
|
111
112
|
const er = entry.getBoundingClientRect();
|
|
112
113
|
const ew = er.width || 40;
|
|
113
114
|
const eh = er.height || 40;
|
|
114
|
-
const clamp = (
|
|
115
|
+
const clamp = (value, max) => Math.min(Math.max(margin, value), Math.max(margin, max));
|
|
115
116
|
|
|
116
117
|
// Clamped offset of the entry along its docked edge. We derive the panel
|
|
117
118
|
// position from these numbers directly rather than re-reading the entry's
|
|
118
119
|
// live rect — on a fresh open/refresh that rect can still be stale (reads
|
|
119
120
|
// ~0), which left the entry bottom-right but the panel bottom-left.
|
|
120
|
-
const alongX = clamp(
|
|
121
|
-
const alongY = clamp(
|
|
121
|
+
const alongX = clamp(pos.along, vw - ew - margin);
|
|
122
|
+
const alongY = clamp(pos.along, vh - eh - margin);
|
|
122
123
|
|
|
123
124
|
const es = entry.style;
|
|
124
125
|
es.insetInlineStart = es.insetBlockStart = es.insetInlineEnd = es.insetBlockEnd = 'auto';
|
|
125
|
-
if (
|
|
126
|
-
es['inset' + (
|
|
126
|
+
if (pos.edge === 'right' || pos.edge === 'left') {
|
|
127
|
+
es['inset' + (pos.edge === 'right' ? 'InlineEnd' : 'InlineStart')] = margin + 'px';
|
|
127
128
|
es.insetBlockStart = alongY + 'px';
|
|
128
129
|
} else {
|
|
129
|
-
es['inset' + (
|
|
130
|
+
es['inset' + (pos.edge === 'bottom' ? 'BlockEnd' : 'BlockStart')] = margin + 'px';
|
|
130
131
|
es.insetInlineStart = alongX + 'px';
|
|
131
132
|
}
|
|
132
|
-
this.setAttribute('dock',
|
|
133
|
+
this.setAttribute('dock', pos.edge);
|
|
133
134
|
|
|
134
135
|
const panel = this.renderRoot.querySelector('.panel');
|
|
135
|
-
if (!panel)
|
|
136
|
+
if (!panel) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
136
139
|
const ps = panel.style;
|
|
137
140
|
ps.insetInlineStart = ps.insetBlockStart = ps.insetInlineEnd = ps.insetBlockEnd = 'auto';
|
|
138
141
|
// Entry center along its edge, computed from the clamped offsets above.
|
|
139
142
|
const cx = alongX + ew / 2;
|
|
140
143
|
const cy = alongY + eh / 2;
|
|
141
|
-
if (
|
|
142
|
-
ps.insetInlineEnd =
|
|
143
|
-
ps.insetBlockStart = clamp(cy -
|
|
144
|
-
} else if (
|
|
145
|
-
ps.insetInlineStart =
|
|
146
|
-
ps.insetBlockStart = clamp(cy -
|
|
147
|
-
} else if (
|
|
148
|
-
ps.insetBlockStart =
|
|
149
|
-
ps.insetInlineStart = clamp(cx -
|
|
144
|
+
if (pos.edge === 'right') {
|
|
145
|
+
ps.insetInlineEnd = PanelEdge + 'px';
|
|
146
|
+
ps.insetBlockStart = clamp(cy - PanelH / 2, vh - PanelH - margin) + 'px';
|
|
147
|
+
} else if (pos.edge === 'left') {
|
|
148
|
+
ps.insetInlineStart = PanelEdge + 'px';
|
|
149
|
+
ps.insetBlockStart = clamp(cy - PanelH / 2, vh - PanelH - margin) + 'px';
|
|
150
|
+
} else if (pos.edge === 'top') {
|
|
151
|
+
ps.insetBlockStart = PanelEdge + 'px';
|
|
152
|
+
ps.insetInlineStart = clamp(cx - PanelW / 2, vw - PanelW - margin) + 'px';
|
|
150
153
|
} else {
|
|
151
|
-
ps.insetBlockEnd =
|
|
152
|
-
ps.insetInlineStart = clamp(cx -
|
|
154
|
+
ps.insetBlockEnd = PanelEdge + 'px';
|
|
155
|
+
ps.insetInlineStart = clamp(cx - PanelW / 2, vw - PanelW - margin) + 'px';
|
|
153
156
|
}
|
|
154
157
|
}
|
|
155
158
|
|
|
156
159
|
// Drag the always-visible entry. Movement over a threshold = drag (live snap
|
|
157
160
|
// to nearest edge, panel follows); a plain click toggles the panel.
|
|
158
|
-
_startDrag(
|
|
159
|
-
if (
|
|
160
|
-
|
|
161
|
+
_startDrag(event) {
|
|
162
|
+
if (event.button !== 0) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
event.preventDefault();
|
|
161
166
|
hide(); // clear any hover highlight before dragging
|
|
162
167
|
const entry = this.renderRoot.querySelector('.entry');
|
|
163
168
|
const rect = entry.getBoundingClientRect();
|
|
164
169
|
this._drag = {
|
|
165
|
-
startX:
|
|
166
|
-
startY:
|
|
167
|
-
offX:
|
|
168
|
-
offY:
|
|
170
|
+
startX: event.clientX,
|
|
171
|
+
startY: event.clientY,
|
|
172
|
+
offX: event.clientX - rect.left,
|
|
173
|
+
offY: event.clientY - rect.top,
|
|
169
174
|
moved: false
|
|
170
175
|
};
|
|
171
|
-
this._onDragMove =
|
|
172
|
-
this._onDragUp =
|
|
176
|
+
this._onDragMove = event => this._dragMove(event);
|
|
177
|
+
this._onDragUp = event => this._dragUp(event);
|
|
173
178
|
window.addEventListener('pointermove', this._onDragMove, true);
|
|
174
179
|
window.addEventListener('pointerup', this._onDragUp, true);
|
|
175
180
|
}
|
|
176
181
|
|
|
177
|
-
_onFabPointerDown(
|
|
178
|
-
this._startDrag(
|
|
182
|
+
_onFabPointerDown(event) {
|
|
183
|
+
this._startDrag(event);
|
|
179
184
|
}
|
|
180
185
|
|
|
181
|
-
_dragMove(
|
|
182
|
-
const
|
|
183
|
-
if (!
|
|
184
|
-
|
|
185
|
-
|
|
186
|
+
_dragMove(event) {
|
|
187
|
+
const drag = this._drag;
|
|
188
|
+
if (!drag) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (!drag.moved && Math.abs(event.clientX - drag.startX) + Math.abs(event.clientY - drag.startY) < DragThreshold) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
drag.moved = true;
|
|
186
195
|
// Snap to the nearest edge live during the drag (not on release).
|
|
187
196
|
const vw = window.innerWidth;
|
|
188
197
|
const vh = window.innerHeight;
|
|
189
198
|
const dist = {
|
|
190
|
-
left:
|
|
191
|
-
right: vw -
|
|
192
|
-
top:
|
|
193
|
-
bottom: vh -
|
|
199
|
+
left: event.clientX,
|
|
200
|
+
right: vw - event.clientX,
|
|
201
|
+
top: event.clientY,
|
|
202
|
+
bottom: vh - event.clientY
|
|
194
203
|
};
|
|
195
|
-
const edge = Object.keys(dist).reduce((
|
|
196
|
-
const along = edge === 'left' || edge === 'right' ?
|
|
204
|
+
const edge = Object.keys(dist).reduce((best, current) => (dist[current] < dist[best] ? current : best));
|
|
205
|
+
const along = edge === 'left' || edge === 'right' ? event.clientY - drag.offY : event.clientX - drag.offX;
|
|
197
206
|
this._pos = { edge, along };
|
|
198
207
|
this._applyPos();
|
|
199
208
|
}
|
|
@@ -201,10 +210,12 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
201
210
|
_dragUp() {
|
|
202
211
|
window.removeEventListener('pointermove', this._onDragMove, true);
|
|
203
212
|
window.removeEventListener('pointerup', this._onDragUp, true);
|
|
204
|
-
const
|
|
213
|
+
const drag = this._drag;
|
|
205
214
|
this._drag = null;
|
|
206
|
-
if (!
|
|
207
|
-
|
|
215
|
+
if (!drag) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (!drag.moved) {
|
|
208
219
|
// plain click → toggle the panel
|
|
209
220
|
this.collapsed = !this.collapsed;
|
|
210
221
|
return;
|
|
@@ -219,6 +230,7 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
219
230
|
window.addEventListener('keydown', this._onKeydown, true);
|
|
220
231
|
window.addEventListener('resize', this._onResize);
|
|
221
232
|
this._vuexUnsub = vuexSubscribe(() => this.requestUpdate());
|
|
233
|
+
this._eventsUnsub = eventsSubscribe(() => this.requestUpdate());
|
|
222
234
|
// DOM-based fallback refresh (see constructor). Only childList/subtree —
|
|
223
235
|
// our own panel renders inside a shadow root (not observed), and the
|
|
224
236
|
// inspector highlight box only mutates via style, so this won't loop.
|
|
@@ -242,12 +254,22 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
242
254
|
this._domObserver.disconnect();
|
|
243
255
|
this._domObserver = null;
|
|
244
256
|
}
|
|
245
|
-
if (this._vuexUnsub)
|
|
257
|
+
if (this._vuexUnsub) {
|
|
258
|
+
this._vuexUnsub();
|
|
259
|
+
}
|
|
260
|
+
if (this._eventsUnsub) {
|
|
261
|
+
this._eventsUnsub();
|
|
262
|
+
}
|
|
246
263
|
stopPicking();
|
|
247
264
|
}
|
|
248
265
|
|
|
249
266
|
firstUpdated() {
|
|
250
267
|
this._applyPos();
|
|
268
|
+
this.setAttribute('theme', this.theme);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
_toggleTheme() {
|
|
272
|
+
this.theme = this.theme === 'dark' ? 'light' : 'dark';
|
|
251
273
|
}
|
|
252
274
|
|
|
253
275
|
_scheduleRefresh() {
|
|
@@ -270,16 +292,23 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
270
292
|
}
|
|
271
293
|
|
|
272
294
|
_toggle(id) {
|
|
273
|
-
if (this.expanded.has(id))
|
|
274
|
-
|
|
295
|
+
if (this.expanded.has(id)) {
|
|
296
|
+
this.expanded.delete(id);
|
|
297
|
+
} else {
|
|
298
|
+
this.expanded.add(id);
|
|
299
|
+
}
|
|
275
300
|
this.requestUpdate();
|
|
276
301
|
}
|
|
277
302
|
|
|
278
303
|
// Highlight the page DOM only while hovering a tree row (vue-devtools style).
|
|
279
304
|
_hoverEnter(id) {
|
|
280
|
-
if (this._drag)
|
|
305
|
+
if (this._drag) {
|
|
306
|
+
return; // don't highlight while dragging the entry/panel
|
|
307
|
+
}
|
|
281
308
|
const vm = getInstance(id);
|
|
282
|
-
if (vm)
|
|
309
|
+
if (vm) {
|
|
310
|
+
highlight(vm);
|
|
311
|
+
}
|
|
283
312
|
}
|
|
284
313
|
|
|
285
314
|
_hoverLeave() {
|
|
@@ -304,7 +333,9 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
304
333
|
_selectVm(vm) {
|
|
305
334
|
this.refresh();
|
|
306
335
|
const path = this._pathToVm(vm);
|
|
307
|
-
if (!path.length)
|
|
336
|
+
if (!path.length) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
308
339
|
for (let i = 0; i < path.length - 1; i++) this.expanded.add(path[i]);
|
|
309
340
|
this._select(path[path.length - 1]);
|
|
310
341
|
this._scrollToSelected = true;
|
|
@@ -319,10 +350,18 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
319
350
|
found = next;
|
|
320
351
|
return true;
|
|
321
352
|
}
|
|
322
|
-
for (const
|
|
353
|
+
for (const child of node.children || []) {
|
|
354
|
+
if (dfs(child, next)) {
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
323
358
|
return false;
|
|
324
359
|
};
|
|
325
|
-
for (const
|
|
360
|
+
for (const root of this.tree) {
|
|
361
|
+
if (dfs(root, [])) {
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
326
365
|
return found || [];
|
|
327
366
|
}
|
|
328
367
|
|
|
@@ -332,19 +371,23 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
332
371
|
// match nested inside another match is not promoted (it shows in the subtree).
|
|
333
372
|
_computeFilter() {
|
|
334
373
|
const q = this.query.trim().toLowerCase();
|
|
335
|
-
if (!q)
|
|
374
|
+
if (!q) {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
336
377
|
const matched = new Set();
|
|
337
378
|
const mark = node => {
|
|
338
|
-
if (node.name.toLowerCase().includes(q))
|
|
339
|
-
|
|
379
|
+
if (node.name.toLowerCase().includes(q)) {
|
|
380
|
+
matched.add(node.id);
|
|
381
|
+
}
|
|
382
|
+
for (const child of node.children || []) mark(child);
|
|
340
383
|
};
|
|
341
|
-
for (const
|
|
384
|
+
for (const root of this.tree) mark(root);
|
|
342
385
|
|
|
343
386
|
const roots = [];
|
|
344
387
|
const show = new Set();
|
|
345
388
|
const collect = node => {
|
|
346
389
|
show.add(node.id);
|
|
347
|
-
for (const
|
|
390
|
+
for (const child of node.children || []) collect(child);
|
|
348
391
|
};
|
|
349
392
|
const walk = (node, hasMatchedAncestor) => {
|
|
350
393
|
const isMatch = matched.has(node.id);
|
|
@@ -352,9 +395,9 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
352
395
|
roots.push(node);
|
|
353
396
|
collect(node);
|
|
354
397
|
}
|
|
355
|
-
for (const
|
|
398
|
+
for (const child of node.children || []) walk(child, hasMatchedAncestor || isMatch);
|
|
356
399
|
};
|
|
357
|
-
for (const
|
|
400
|
+
for (const root of this.tree) walk(root, false);
|
|
358
401
|
return { roots, show, q };
|
|
359
402
|
}
|
|
360
403
|
|
|
@@ -365,81 +408,130 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
365
408
|
const parent = new Map();
|
|
366
409
|
const node = new Map();
|
|
367
410
|
if (filter) {
|
|
368
|
-
const walk = (
|
|
369
|
-
node.set(
|
|
370
|
-
parent.set(
|
|
371
|
-
order.push(
|
|
372
|
-
|
|
373
|
-
|
|
411
|
+
const walk = (treeNode, parentId) => {
|
|
412
|
+
node.set(treeNode.id, treeNode);
|
|
413
|
+
parent.set(treeNode.id, parentId);
|
|
414
|
+
order.push(treeNode.id);
|
|
415
|
+
if (this._searchCollapsed.has(treeNode.id)) {
|
|
416
|
+
return; // collapsed in search
|
|
417
|
+
}
|
|
418
|
+
for (const child of treeNode.children || []) {
|
|
419
|
+
if (filter.show.has(child.id)) {
|
|
420
|
+
walk(child, treeNode.id);
|
|
421
|
+
}
|
|
374
422
|
}
|
|
375
423
|
};
|
|
376
|
-
for (const
|
|
424
|
+
for (const root of filter.roots) walk(root, null);
|
|
377
425
|
return { order, parent, node };
|
|
378
426
|
}
|
|
379
|
-
const walk = (
|
|
380
|
-
node.set(
|
|
381
|
-
parent.set(
|
|
382
|
-
order.push(
|
|
383
|
-
if (
|
|
384
|
-
for (const
|
|
427
|
+
const walk = (treeNode, parentId) => {
|
|
428
|
+
node.set(treeNode.id, treeNode);
|
|
429
|
+
parent.set(treeNode.id, parentId);
|
|
430
|
+
order.push(treeNode.id);
|
|
431
|
+
if (treeNode.children && treeNode.children.length && this.expanded.has(treeNode.id)) {
|
|
432
|
+
for (const child of treeNode.children) walk(child, treeNode.id);
|
|
385
433
|
}
|
|
386
434
|
};
|
|
387
|
-
for (const
|
|
435
|
+
for (const root of this.tree) walk(root, null);
|
|
388
436
|
return { order, parent, node };
|
|
389
437
|
}
|
|
390
438
|
|
|
391
439
|
// Arrow-key navigation once a component is selected (VS Code / devtools style):
|
|
392
440
|
// ↑/↓ move through visible rows, → step into / expand, ← step out / collapse.
|
|
393
441
|
// During search the subtree is always shown, so →/← just navigate in/out.
|
|
394
|
-
_handleKeydown(
|
|
395
|
-
if (this.collapsed
|
|
396
|
-
|
|
442
|
+
_handleKeydown(event) {
|
|
443
|
+
if (this.collapsed) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
397
449
|
// Don't hijack arrow keys while typing in a field. composedPath sees into
|
|
398
450
|
// our shadow root (the search box) as well as the app's own inputs.
|
|
399
|
-
const path =
|
|
451
|
+
const path = event.composedPath ? event.composedPath() : [];
|
|
400
452
|
const inEditable = path.some(el => el && el.tagName && (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) || el.isContentEditable));
|
|
401
|
-
if (inEditable)
|
|
453
|
+
if (inEditable) {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
402
456
|
|
|
457
|
+
// Timeline tab: ↑/↓ move between entries.
|
|
458
|
+
if (this.tab === 'timeline') {
|
|
459
|
+
if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
const entries = this._timelineEntries();
|
|
463
|
+
if (!entries.length) {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
const curIdx = entries.findIndex(item => item.id === this.timelineSelected);
|
|
467
|
+
const base = curIdx < 0 ? entries.length - 1 : curIdx;
|
|
468
|
+
const nextIdx = event.key === 'ArrowDown' ? Math.min(entries.length - 1, base + 1) : Math.max(0, base - 1);
|
|
469
|
+
this.timelineSelected = entries[nextIdx].id;
|
|
470
|
+
this._scrollToSelected = true;
|
|
471
|
+
event.preventDefault();
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Components tab: tree navigation (needs a selected node).
|
|
476
|
+
if (this.selectedId == null) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
403
479
|
const filter = this._computeFilter();
|
|
404
480
|
const searching = !!filter;
|
|
405
481
|
const { order, parent, node } = this._index(filter);
|
|
406
482
|
const id = this.selectedId;
|
|
407
483
|
const cur = node.get(id);
|
|
408
|
-
if (!cur)
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const
|
|
484
|
+
if (!cur) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
const index = order.indexOf(id);
|
|
488
|
+
if (index < 0) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const shownChildren = (cur.children || []).filter(item => !filter || filter.show.has(item.id));
|
|
412
492
|
const hasChildren = shownChildren.length > 0;
|
|
413
|
-
//
|
|
414
|
-
//
|
|
415
|
-
const isOpen = searching
|
|
493
|
+
// "open" respects the active mode: normal tree uses `expanded`, search
|
|
494
|
+
// uses `_searchCollapsed` (default open).
|
|
495
|
+
const isOpen = searching ? !this._searchCollapsed.has(id) : this.expanded.has(id);
|
|
416
496
|
let next = null;
|
|
417
497
|
|
|
418
|
-
if (
|
|
419
|
-
next = order[Math.min(order.length - 1,
|
|
420
|
-
} else if (
|
|
421
|
-
next = order[Math.max(0,
|
|
422
|
-
} else if (
|
|
498
|
+
if (event.key === 'ArrowDown') {
|
|
499
|
+
next = order[Math.min(order.length - 1, index + 1)];
|
|
500
|
+
} else if (event.key === 'ArrowUp') {
|
|
501
|
+
next = order[Math.max(0, index - 1)];
|
|
502
|
+
} else if (event.key === 'ArrowRight') {
|
|
423
503
|
// Closed with children -> open it; already open -> step into first child.
|
|
424
|
-
if (hasChildren && !isOpen)
|
|
425
|
-
|
|
426
|
-
|
|
504
|
+
if (hasChildren && !isOpen) {
|
|
505
|
+
searching ? this._searchCollapsed.delete(id) : this.expanded.add(id);
|
|
506
|
+
} else if (hasChildren && isOpen) {
|
|
507
|
+
next = shownChildren[0].id;
|
|
508
|
+
}
|
|
509
|
+
} else if (event.key === 'ArrowLeft') {
|
|
427
510
|
// Check open/closed FIRST: open -> collapse; closed (or leaf) -> parent.
|
|
428
|
-
if (isOpen && hasChildren
|
|
429
|
-
|
|
511
|
+
if (isOpen && hasChildren) {
|
|
512
|
+
searching ? this._searchCollapsed.add(id) : this.expanded.delete(id);
|
|
513
|
+
} else {
|
|
514
|
+
next = parent.get(id);
|
|
515
|
+
}
|
|
430
516
|
}
|
|
431
517
|
|
|
432
|
-
|
|
433
|
-
if (next != null)
|
|
518
|
+
event.preventDefault();
|
|
519
|
+
if (next != null) {
|
|
520
|
+
this._select(next);
|
|
521
|
+
}
|
|
434
522
|
this._scrollToSelected = true;
|
|
435
523
|
this.requestUpdate();
|
|
436
524
|
}
|
|
437
525
|
|
|
438
526
|
updated(changed) {
|
|
439
527
|
// Persist remembered UI bits across reloads.
|
|
440
|
-
if (changed && (changed.has('collapsed') || changed.has('tab'))) {
|
|
528
|
+
if (changed && (changed.has('collapsed') || changed.has('tab') || changed.has('theme'))) {
|
|
441
529
|
this._persistUiState();
|
|
442
530
|
}
|
|
531
|
+
// Reflect the theme onto the host so the CSS variable overrides apply.
|
|
532
|
+
if (changed && changed.has('theme')) {
|
|
533
|
+
this.setAttribute('theme', this.theme);
|
|
534
|
+
}
|
|
443
535
|
// Re-clamp the docked position when switching fab <-> panel (sizes differ).
|
|
444
536
|
if (changed && changed.has('collapsed')) {
|
|
445
537
|
this._applyPos();
|
|
@@ -455,16 +547,20 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
455
547
|
}
|
|
456
548
|
// When the query changes, auto-select the first match (like vue-devtools).
|
|
457
549
|
if (changed && changed.has('query') && this.query.trim()) {
|
|
458
|
-
const
|
|
459
|
-
if (
|
|
460
|
-
this._select(
|
|
550
|
+
const filter = this._computeFilter();
|
|
551
|
+
if (filter && filter.roots.length && !filter.show.has(this.selectedId)) {
|
|
552
|
+
this._select(filter.roots[0].id);
|
|
461
553
|
this._scrollToSelected = true;
|
|
462
554
|
}
|
|
463
555
|
}
|
|
464
|
-
if (!this._scrollToSelected)
|
|
556
|
+
if (!this._scrollToSelected) {
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
465
559
|
this._scrollToSelected = false;
|
|
466
560
|
const el = this.renderRoot.querySelector('.node.selected');
|
|
467
|
-
if (el)
|
|
561
|
+
if (el) {
|
|
562
|
+
el.scrollIntoView({ block: 'nearest' });
|
|
563
|
+
}
|
|
468
564
|
}
|
|
469
565
|
|
|
470
566
|
_renderNode(node, depth) {
|
|
@@ -482,8 +578,8 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
482
578
|
>
|
|
483
579
|
<span
|
|
484
580
|
class="caret-btn"
|
|
485
|
-
@click=${
|
|
486
|
-
|
|
581
|
+
@click=${event => {
|
|
582
|
+
event.stopPropagation();
|
|
487
583
|
this._toggle(node.id);
|
|
488
584
|
}}
|
|
489
585
|
>
|
|
@@ -491,7 +587,7 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
491
587
|
</span>
|
|
492
588
|
<span class="tag"><${node.name}></span>
|
|
493
589
|
</div>
|
|
494
|
-
${hasChildren && isOpen ? node.children.map(
|
|
590
|
+
${hasChildren && isOpen ? node.children.map(item => this._renderNode(item, depth + 1)) : null}
|
|
495
591
|
</div>
|
|
496
592
|
`;
|
|
497
593
|
}
|
|
@@ -499,8 +595,10 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
499
595
|
// Search result row: matched node as a subtree root, with all descendants
|
|
500
596
|
// shown (always open). Ancestors are omitted. Arrow is non-interactive here.
|
|
501
597
|
_renderSearchNode(node, depth, show) {
|
|
502
|
-
const kids = (node.children || []).filter(
|
|
598
|
+
const kids = (node.children || []).filter(item => show.has(item.id));
|
|
599
|
+
const hasKids = kids.length > 0;
|
|
503
600
|
const isSelected = node.id === this.selectedId;
|
|
601
|
+
const open = !this._searchCollapsed.has(node.id); // default open in search
|
|
504
602
|
return html`
|
|
505
603
|
<div>
|
|
506
604
|
<div
|
|
@@ -510,35 +608,97 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
510
608
|
@mouseenter=${() => this._hoverEnter(node.id)}
|
|
511
609
|
@mouseleave=${() => this._hoverLeave()}
|
|
512
610
|
>
|
|
513
|
-
<span
|
|
611
|
+
<span
|
|
612
|
+
class="caret-btn"
|
|
613
|
+
@click=${event => {
|
|
614
|
+
event.stopPropagation();
|
|
615
|
+
if (hasKids) {
|
|
616
|
+
this._toggleSearchNode(node.id);
|
|
617
|
+
}
|
|
618
|
+
}}
|
|
619
|
+
>
|
|
620
|
+
${this._caret(open && hasKids, !hasKids)}
|
|
621
|
+
</span>
|
|
514
622
|
<span class="tag"><${node.name}></span>
|
|
515
623
|
</div>
|
|
516
|
-
${kids.map(
|
|
624
|
+
${hasKids && open ? kids.map(item => this._renderSearchNode(item, depth + 1, show)) : null}
|
|
517
625
|
</div>
|
|
518
626
|
`;
|
|
519
627
|
}
|
|
520
628
|
|
|
629
|
+
_toggleSearchNode(id) {
|
|
630
|
+
if (this._searchCollapsed.has(id)) {
|
|
631
|
+
this._searchCollapsed.delete(id);
|
|
632
|
+
} else {
|
|
633
|
+
this._searchCollapsed.add(id);
|
|
634
|
+
}
|
|
635
|
+
this.requestUpdate();
|
|
636
|
+
}
|
|
637
|
+
|
|
521
638
|
// Render an object as a titled, collapsible section of expandable value rows.
|
|
522
639
|
// `editable` enables inline editing of primitive leaves (writes back into obj).
|
|
640
|
+
// Sections with many keys get a live filter input in the header.
|
|
523
641
|
_renderKvSection(title, obj, editable) {
|
|
524
|
-
const
|
|
525
|
-
if (!
|
|
642
|
+
const allKeys = obj ? Object.keys(obj) : [];
|
|
643
|
+
if (!allKeys.length) {
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
526
646
|
const collapsed = this.sectionCollapsed.has(title);
|
|
647
|
+
const showFilter = !collapsed && allKeys.length > SectionFilterThreshold;
|
|
648
|
+
const q = (this._sectionFilter[title] || '').trim().toLowerCase();
|
|
649
|
+
const keys = q ? allKeys.filter(item => item.toLowerCase().includes(q)) : allKeys;
|
|
527
650
|
return html`
|
|
528
|
-
<div class="section-title" @click=${() => this._toggleSection(title)}
|
|
529
|
-
|
|
651
|
+
<div class="section-title" @click=${() => this._toggleSection(title)}>
|
|
652
|
+
${this._caret(!collapsed, false)}
|
|
653
|
+
<span class="section-name">${title}</span>
|
|
654
|
+
${showFilter
|
|
655
|
+
? html`
|
|
656
|
+
<input
|
|
657
|
+
class="section-filter"
|
|
658
|
+
type="search"
|
|
659
|
+
placeholder="filter…"
|
|
660
|
+
.value=${this._sectionFilter[title] || ''}
|
|
661
|
+
@click=${event => event.stopPropagation()}
|
|
662
|
+
@keydown=${event => {
|
|
663
|
+
if (event.key === 'Escape') {
|
|
664
|
+
this._sectionFilter[title] = '';
|
|
665
|
+
this.requestUpdate();
|
|
666
|
+
}
|
|
667
|
+
event.stopPropagation();
|
|
668
|
+
}}
|
|
669
|
+
@input=${event => {
|
|
670
|
+
this._sectionFilter[title] = event.target.value;
|
|
671
|
+
this.requestUpdate();
|
|
672
|
+
}}
|
|
673
|
+
/>
|
|
674
|
+
`
|
|
675
|
+
: null}
|
|
676
|
+
</div>
|
|
677
|
+
${collapsed
|
|
678
|
+
? null
|
|
679
|
+
: keys.length
|
|
680
|
+
? keys.map(item => this._renderValueRow(item, obj[item], `${title}.${item}`, 0, obj, editable))
|
|
681
|
+
: html`
|
|
682
|
+
<div class="empty">No match</div>
|
|
683
|
+
`}
|
|
530
684
|
`;
|
|
531
685
|
}
|
|
532
686
|
|
|
533
687
|
_toggleSection(title) {
|
|
534
|
-
if (this.sectionCollapsed.has(title))
|
|
535
|
-
|
|
688
|
+
if (this.sectionCollapsed.has(title)) {
|
|
689
|
+
this.sectionCollapsed.delete(title);
|
|
690
|
+
} else {
|
|
691
|
+
this.sectionCollapsed.add(title);
|
|
692
|
+
}
|
|
536
693
|
this.requestUpdate();
|
|
537
694
|
}
|
|
538
695
|
|
|
539
696
|
_toggleValue(path) {
|
|
540
|
-
if (this.valueExpanded.has(path))
|
|
541
|
-
|
|
697
|
+
if (this.valueExpanded.has(path)) {
|
|
698
|
+
this.valueExpanded.delete(path);
|
|
699
|
+
} else {
|
|
700
|
+
this.valueExpanded.add(path);
|
|
701
|
+
}
|
|
542
702
|
this.requestUpdate();
|
|
543
703
|
}
|
|
544
704
|
|
|
@@ -546,7 +706,7 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
546
706
|
// `editable` is set, primitive leaves can be clicked to edit in place.
|
|
547
707
|
_renderValueRow(keyLabel, value, path, depth, parent, editable) {
|
|
548
708
|
const isObj = value !== null && typeof value === 'object';
|
|
549
|
-
const keys = isObj ? (Array.isArray(value) ? value.map((_,
|
|
709
|
+
const keys = isObj ? (Array.isArray(value) ? value.map((_, index) => index) : Object.keys(value)) : [];
|
|
550
710
|
const expandable = isObj && keys.length > 0;
|
|
551
711
|
const open = this.valueExpanded.has(path);
|
|
552
712
|
const canEdit = editable && !isObj && typeof value !== 'function';
|
|
@@ -564,18 +724,20 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
564
724
|
<input
|
|
565
725
|
class="edit-input"
|
|
566
726
|
.value=${String(value)}
|
|
567
|
-
@click=${
|
|
568
|
-
@keydown=${
|
|
569
|
-
@blur=${
|
|
727
|
+
@click=${event => event.stopPropagation()}
|
|
728
|
+
@keydown=${event => this._onEditKeydown(event, parent, keyLabel, value)}
|
|
729
|
+
@blur=${event => this._commitEdit(parent, keyLabel, event.target.value, value)}
|
|
570
730
|
/>
|
|
571
731
|
`
|
|
572
732
|
: html`
|
|
573
733
|
<span
|
|
574
734
|
class="val ${this._valClass(value)} ${canEdit ? 'editable' : ''}"
|
|
575
735
|
title=${preview}
|
|
576
|
-
@click=${
|
|
577
|
-
if (!canEdit)
|
|
578
|
-
|
|
736
|
+
@click=${event => {
|
|
737
|
+
if (!canEdit) {
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
event.stopPropagation();
|
|
579
741
|
this._editingPath = path;
|
|
580
742
|
this._focusEdit = true;
|
|
581
743
|
this.requestUpdate();
|
|
@@ -589,8 +751,8 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
589
751
|
<button
|
|
590
752
|
class="copy-btn"
|
|
591
753
|
title="Copy value"
|
|
592
|
-
@click=${
|
|
593
|
-
|
|
754
|
+
@click=${event => {
|
|
755
|
+
event.stopPropagation();
|
|
594
756
|
this._copyValue(value);
|
|
595
757
|
}}
|
|
596
758
|
>
|
|
@@ -599,7 +761,7 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
599
761
|
`
|
|
600
762
|
: null}
|
|
601
763
|
</div>
|
|
602
|
-
${expandable && open ? keys.map(
|
|
764
|
+
${expandable && open ? keys.map(item => this._renderValueRow(item, value[item], `${path}.${item}`, depth + 1, value, editable)) : null}
|
|
603
765
|
`;
|
|
604
766
|
}
|
|
605
767
|
|
|
@@ -608,7 +770,7 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
608
770
|
if (value !== null && typeof value === 'object') {
|
|
609
771
|
try {
|
|
610
772
|
text = JSON.stringify(value, null, 2);
|
|
611
|
-
} catch (
|
|
773
|
+
} catch (err) {
|
|
612
774
|
text = String(value);
|
|
613
775
|
}
|
|
614
776
|
} else {
|
|
@@ -636,17 +798,17 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
636
798
|
ta.select();
|
|
637
799
|
try {
|
|
638
800
|
document.execCommand('copy');
|
|
639
|
-
} catch (
|
|
801
|
+
} catch (err) {
|
|
640
802
|
/* ignore */
|
|
641
803
|
}
|
|
642
804
|
document.body.removeChild(ta);
|
|
643
805
|
}
|
|
644
806
|
|
|
645
|
-
_onEditKeydown(
|
|
646
|
-
|
|
647
|
-
if (
|
|
648
|
-
this._commitEdit(parent, key,
|
|
649
|
-
} else if (
|
|
807
|
+
_onEditKeydown(event, parent, key, oldValue) {
|
|
808
|
+
event.stopPropagation();
|
|
809
|
+
if (event.key === 'Enter') {
|
|
810
|
+
this._commitEdit(parent, key, event.target.value, oldValue);
|
|
811
|
+
} else if (event.key === 'Escape') {
|
|
650
812
|
this._editingPath = null;
|
|
651
813
|
this.requestUpdate();
|
|
652
814
|
}
|
|
@@ -657,34 +819,47 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
657
819
|
_commitEdit(parent, key, rawStr, oldValue) {
|
|
658
820
|
this._editingPath = null;
|
|
659
821
|
let parsed = rawStr;
|
|
660
|
-
const
|
|
661
|
-
if (
|
|
662
|
-
const
|
|
663
|
-
parsed = Number.isNaN(
|
|
664
|
-
} else if (
|
|
822
|
+
const type = typeof oldValue;
|
|
823
|
+
if (type === 'number') {
|
|
824
|
+
const num = Number(rawStr);
|
|
825
|
+
parsed = Number.isNaN(num) ? oldValue : num;
|
|
826
|
+
} else if (type === 'boolean') {
|
|
665
827
|
parsed = rawStr === 'true' || rawStr === '1';
|
|
666
828
|
} else if (oldValue === null || oldValue === undefined) {
|
|
667
829
|
try {
|
|
668
830
|
parsed = JSON.parse(rawStr);
|
|
669
|
-
} catch (
|
|
831
|
+
} catch (err) {
|
|
670
832
|
parsed = rawStr;
|
|
671
833
|
}
|
|
672
834
|
}
|
|
673
835
|
const Vue = hook.Vue;
|
|
674
836
|
if (parent) {
|
|
675
|
-
if (Vue && Vue.set)
|
|
676
|
-
|
|
837
|
+
if (Vue && Vue.set) {
|
|
838
|
+
Vue.set(parent, key, parsed);
|
|
839
|
+
} else {
|
|
840
|
+
parent[key] = parsed;
|
|
841
|
+
}
|
|
677
842
|
}
|
|
678
843
|
this.requestUpdate();
|
|
679
844
|
}
|
|
680
845
|
|
|
681
846
|
_valClass(value) {
|
|
682
|
-
if (value === null || value === undefined)
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
if (
|
|
687
|
-
|
|
847
|
+
if (value === null || value === undefined) {
|
|
848
|
+
return 'v-null';
|
|
849
|
+
}
|
|
850
|
+
const type = typeof value;
|
|
851
|
+
if (type === 'number') {
|
|
852
|
+
return 'v-num';
|
|
853
|
+
}
|
|
854
|
+
if (type === 'boolean') {
|
|
855
|
+
return 'v-bool';
|
|
856
|
+
}
|
|
857
|
+
if (type === 'string') {
|
|
858
|
+
return 'v-str';
|
|
859
|
+
}
|
|
860
|
+
if (type === 'function') {
|
|
861
|
+
return 'v-fn';
|
|
862
|
+
}
|
|
688
863
|
return 'v-obj';
|
|
689
864
|
}
|
|
690
865
|
|
|
@@ -704,20 +879,21 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
704
879
|
`;
|
|
705
880
|
}
|
|
706
881
|
const vm = getInstance(this.selectedId);
|
|
707
|
-
if (!vm)
|
|
882
|
+
if (!vm) {
|
|
708
883
|
return html`
|
|
709
884
|
<div class="empty">Component unmounted</div>
|
|
710
885
|
`;
|
|
886
|
+
}
|
|
711
887
|
|
|
712
888
|
const propsObj = vm._props || {};
|
|
713
889
|
const dataObj = vm._data || vm.$data || {};
|
|
714
890
|
const compDefs = (vm.$options && vm.$options.computed) || {};
|
|
715
891
|
const compObj = {};
|
|
716
|
-
for (const
|
|
892
|
+
for (const key of Object.keys(compDefs)) {
|
|
717
893
|
try {
|
|
718
|
-
compObj[
|
|
894
|
+
compObj[key] = vm[key];
|
|
719
895
|
} catch (err) {
|
|
720
|
-
compObj[
|
|
896
|
+
compObj[key] = `⚠ ${err && err.message}`;
|
|
721
897
|
}
|
|
722
898
|
}
|
|
723
899
|
const attrsObj = vm.$attrs || {};
|
|
@@ -760,27 +936,34 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
760
936
|
}
|
|
761
937
|
|
|
762
938
|
_vmName(vm) {
|
|
763
|
-
const
|
|
764
|
-
let
|
|
765
|
-
if (!
|
|
766
|
-
|
|
939
|
+
const options = vm.$options || {};
|
|
940
|
+
let name = options.name || options._componentTag;
|
|
941
|
+
if (!name && options.__file) {
|
|
942
|
+
name = String(options.__file)
|
|
767
943
|
.split(/[\\/]/)
|
|
768
944
|
.pop()
|
|
769
945
|
.replace(/\.vue$/, '');
|
|
770
|
-
|
|
771
|
-
|
|
946
|
+
}
|
|
947
|
+
if (!name && vm.$root === vm) {
|
|
948
|
+
name = 'Root';
|
|
949
|
+
}
|
|
950
|
+
return name || 'Anonymous';
|
|
772
951
|
}
|
|
773
952
|
|
|
774
953
|
// Ask the Vite dev server to open the component's source file in the editor.
|
|
775
954
|
_openInEditor(file) {
|
|
776
|
-
if (!file)
|
|
955
|
+
if (!file) {
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
777
958
|
fetch('/__open-in-editor?file=' + encodeURIComponent(file)).catch(() => {});
|
|
778
959
|
}
|
|
779
960
|
|
|
780
961
|
// Scroll the component's root DOM element into view and flash the highlight.
|
|
781
962
|
_scrollToComponent(vm) {
|
|
782
963
|
const el = vm && vm.$el;
|
|
783
|
-
if (!el || !el.scrollIntoView)
|
|
964
|
+
if (!el || !el.scrollIntoView) {
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
784
967
|
el.scrollIntoView({
|
|
785
968
|
behavior: 'smooth',
|
|
786
969
|
block: 'center',
|
|
@@ -803,17 +986,23 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
803
986
|
const lines = code.split('\n');
|
|
804
987
|
let min = Infinity;
|
|
805
988
|
for (let i = 1; i < lines.length; i++) {
|
|
806
|
-
if (!lines[i].trim())
|
|
989
|
+
if (!lines[i].trim()) {
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
807
992
|
const indent = lines[i].match(/^[ \t]*/)[0].length;
|
|
808
|
-
if (indent < min)
|
|
993
|
+
if (indent < min) {
|
|
994
|
+
min = indent;
|
|
995
|
+
}
|
|
809
996
|
}
|
|
810
|
-
if (!isFinite(min) || min === 0)
|
|
811
|
-
|
|
997
|
+
if (!isFinite(min) || min === 0) {
|
|
998
|
+
return code;
|
|
999
|
+
}
|
|
1000
|
+
return lines.map((item, index) => (index === 0 ? item : item.slice(min))).join('\n');
|
|
812
1001
|
}
|
|
813
1002
|
|
|
814
1003
|
render() {
|
|
815
1004
|
return html`
|
|
816
|
-
<div class="entry" @pointerdown=${
|
|
1005
|
+
<div class="entry" @pointerdown=${event => this._onFabPointerDown(event)}>
|
|
817
1006
|
<span class="fab-icon">${this._vueLogo()}</span>
|
|
818
1007
|
</div>
|
|
819
1008
|
${this.collapsed ? null : this._renderPanel()}
|
|
@@ -833,6 +1022,10 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
833
1022
|
${this._icon('vuex')}
|
|
834
1023
|
<span class="tip">Vuex</span>
|
|
835
1024
|
</button>
|
|
1025
|
+
<button class="side-tab ${this.tab === 'timeline' ? 'active' : ''}" @click=${() => (this.tab = 'timeline')}>
|
|
1026
|
+
${this._icon('timeline')}
|
|
1027
|
+
<span class="tip">Timeline</span>
|
|
1028
|
+
</button>
|
|
836
1029
|
<span class="side-spacer"></span>
|
|
837
1030
|
${this.tab === 'components'
|
|
838
1031
|
? html`
|
|
@@ -842,12 +1035,18 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
842
1035
|
</button>
|
|
843
1036
|
`
|
|
844
1037
|
: null}
|
|
1038
|
+
<button class="side-tab" @click=${() => this._toggleTheme()}>
|
|
1039
|
+
${this._icon(this.theme === 'dark' ? 'sun' : 'moon')}
|
|
1040
|
+
<span class="tip">${this.theme === 'dark' ? 'Light theme' : 'Dark theme'}</span>
|
|
1041
|
+
</button>
|
|
845
1042
|
<button class="side-tab side-tab--min" @click=${() => (this.collapsed = true)}>
|
|
846
1043
|
${this._icon('min')}
|
|
847
1044
|
<span class="tip">Minimize</span>
|
|
848
1045
|
</button>
|
|
849
1046
|
</nav>
|
|
850
|
-
<div class="main"
|
|
1047
|
+
<div class="main">
|
|
1048
|
+
${this.tab === 'components' ? this._renderComponents() : this.tab === 'vuex' ? this._renderVuex() : this._renderTimeline()}
|
|
1049
|
+
</div>
|
|
851
1050
|
${this.renderCodeText != null
|
|
852
1051
|
? html`
|
|
853
1052
|
<div class="code-overlay">
|
|
@@ -873,11 +1072,11 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
873
1072
|
type="search"
|
|
874
1073
|
placeholder="Search components…"
|
|
875
1074
|
.value=${this.query}
|
|
876
|
-
@input=${
|
|
877
|
-
@keydown=${
|
|
878
|
-
if (
|
|
1075
|
+
@input=${event => (this.query = event.target.value)}
|
|
1076
|
+
@keydown=${event => {
|
|
1077
|
+
if (event.key === 'Escape') {
|
|
879
1078
|
this.query = '';
|
|
880
|
-
|
|
1079
|
+
event.stopPropagation();
|
|
881
1080
|
}
|
|
882
1081
|
}}
|
|
883
1082
|
/>
|
|
@@ -893,8 +1092,8 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
893
1092
|
<div class="empty">No component matches</div>
|
|
894
1093
|
`
|
|
895
1094
|
: filter
|
|
896
|
-
? filter.roots.map(
|
|
897
|
-
: this.tree.map(
|
|
1095
|
+
? filter.roots.map(item => this._renderSearchNode(item, 0, filter.show))
|
|
1096
|
+
: this.tree.map(item => this._renderNode(item, 0))}
|
|
898
1097
|
</div>
|
|
899
1098
|
<div class="detail">${this._renderDetail()}</div>
|
|
900
1099
|
</div>
|
|
@@ -928,10 +1127,10 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
928
1127
|
</button>
|
|
929
1128
|
</div>
|
|
930
1129
|
${snaps.map(
|
|
931
|
-
(
|
|
932
|
-
<div class="node ${
|
|
933
|
-
<span class="mut-index">${
|
|
934
|
-
<span class="tag">${
|
|
1130
|
+
(item, index) => html`
|
|
1131
|
+
<div class="node ${index === sel ? 'selected' : ''}" @click=${() => (this.vuexSelected = index)}>
|
|
1132
|
+
<span class="mut-index">${item.base ? '' : index}</span>
|
|
1133
|
+
<span class="tag">${item.base ? 'Base State' : item.type}</span>
|
|
935
1134
|
</div>
|
|
936
1135
|
`
|
|
937
1136
|
)}
|
|
@@ -955,10 +1154,102 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
955
1154
|
`;
|
|
956
1155
|
}
|
|
957
1156
|
|
|
958
|
-
//
|
|
1157
|
+
// Merge component events + Vuex mutations into one chronological timeline
|
|
1158
|
+
// (like the vue-devtools v7 Timeline).
|
|
1159
|
+
_timelineEntries() {
|
|
1160
|
+
const entries = [];
|
|
1161
|
+
for (const event of getEvents()) {
|
|
1162
|
+
entries.push({ id: `e${event.id}`, kind: 'event', time: event.time, title: event.name, sub: `<${event.component}>`, event: event });
|
|
1163
|
+
}
|
|
1164
|
+
getSnapshots().forEach((item, index) => {
|
|
1165
|
+
if (item.base) {
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
entries.push({ id: `m${index}`, kind: 'mutation', time: item.time || 0, title: item.type, sub: 'vuex', snap: item, index });
|
|
1169
|
+
});
|
|
1170
|
+
entries.sort((first, second) => first.time - second.time);
|
|
1171
|
+
return entries;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
_renderTimeline() {
|
|
1175
|
+
const entries = this._timelineEntries();
|
|
1176
|
+
if (!entries.length) {
|
|
1177
|
+
return html`
|
|
1178
|
+
<div class="body"><div class="empty">No timeline activity yet</div></div>
|
|
1179
|
+
`;
|
|
1180
|
+
}
|
|
1181
|
+
const sel = entries.find(item => item.id === this.timelineSelected) || entries[entries.length - 1];
|
|
1182
|
+
return html`
|
|
1183
|
+
<div class="body">
|
|
1184
|
+
<div class="tree">
|
|
1185
|
+
<div class="vuex-bar">
|
|
1186
|
+
<button
|
|
1187
|
+
class="btn"
|
|
1188
|
+
title="Clear recorded events"
|
|
1189
|
+
@click=${() => {
|
|
1190
|
+
clearEvents();
|
|
1191
|
+
this.timelineSelected = null;
|
|
1192
|
+
}}
|
|
1193
|
+
>
|
|
1194
|
+
✕ Clear events
|
|
1195
|
+
</button>
|
|
1196
|
+
</div>
|
|
1197
|
+
${entries.map(
|
|
1198
|
+
item => html`
|
|
1199
|
+
<div class="node ${item.id === sel.id ? 'selected' : ''}" @click=${() => (this.timelineSelected = item.id)}>
|
|
1200
|
+
<span class="ev-time">${this._formatTime(item.time)}</span>
|
|
1201
|
+
<span class="tag">${item.title}</span>
|
|
1202
|
+
<span class="ev-comp">${item.sub}</span>
|
|
1203
|
+
</div>
|
|
1204
|
+
`
|
|
1205
|
+
)}
|
|
1206
|
+
</div>
|
|
1207
|
+
<div class="detail">${this._renderTimelineDetail(sel)}</div>
|
|
1208
|
+
</div>
|
|
1209
|
+
`;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
_renderTimelineDetail(entry) {
|
|
1213
|
+
if (!entry) {
|
|
1214
|
+
return null;
|
|
1215
|
+
}
|
|
1216
|
+
if (entry.kind === 'event') {
|
|
1217
|
+
const event = entry.event;
|
|
1218
|
+
const argsObj = {};
|
|
1219
|
+
(event.args || []).forEach((item, index) => {
|
|
1220
|
+
argsObj[index] = item;
|
|
1221
|
+
});
|
|
1222
|
+
return html`
|
|
1223
|
+
${this._renderKvSection('event', { name: event.name, from: event.component, time: new Date(event.time).toLocaleTimeString() }, false)}
|
|
1224
|
+
${event.args && event.args.length
|
|
1225
|
+
? this._renderKvSection('payload', argsObj, false)
|
|
1226
|
+
: html`
|
|
1227
|
+
<div class="empty">No payload</div>
|
|
1228
|
+
`}
|
|
1229
|
+
`;
|
|
1230
|
+
}
|
|
1231
|
+
const snap = entry.snap;
|
|
1232
|
+
const store = getStore();
|
|
1233
|
+
const payloadObj = snap.payload === undefined ? null : { payload: snap.payload };
|
|
1234
|
+
return html`
|
|
1235
|
+
<button class="btn on time-travel" @click=${() => travelTo(entry.index)}>⏱ Time Travel</button>
|
|
1236
|
+
${this._renderKvSection('mutation', { type: snap.type, time: new Date(snap.time).toLocaleTimeString() }, false)}
|
|
1237
|
+
${payloadObj ? this._renderKvSection('payload', payloadObj, false) : null}
|
|
1238
|
+
${this._renderKvSection('state', snap.state || {}, false)}
|
|
1239
|
+
${store ? this._renderKvSection('getters (live)', store.getters || {}, false) : null}
|
|
1240
|
+
`;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
_formatTime(time) {
|
|
1244
|
+
const date = new Date(time);
|
|
1245
|
+
const pad = (num, length = 2) => String(num).padStart(length, '0');
|
|
1246
|
+
return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}`;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// Plugin logo
|
|
959
1250
|
_vueLogo() {
|
|
960
1251
|
return html`
|
|
961
|
-
<svg fill-rule="evenodd" viewBox="64 64 896 896" fill="
|
|
1252
|
+
<svg fill-rule="evenodd" viewBox="64 64 896 896" fill="var(--logo-fill)" aria-hidden="true">
|
|
962
1253
|
<path
|
|
963
1254
|
d="M250.02 547.04c92.37-19.8 79.77-130.07 76.95-154.18-4.56-37.2-48.26-102.16-107.63-97.02-74.7 6.7-85.65 114.58-85.65 114.58-10.04 49.88 24.2 156.43 116.33 136.62m84.7 214.14c10.28 38.7 43.95 40.43 43.95 40.43H427V683.55h-51.74c-23.22 6.96-34.5 25.1-36.98 32.8-2.74 7.8-8.71 27.6-3.57 44.83m169.07-531.1c0-72.42-41.13-131.08-92.2-131.08-50.92 0-92.21 58.66-92.21 131.07 0 72.5 41.3 131.16 92.2 131.16 51.08 0 92.21-58.66 92.21-131.16m248.1 9.1c8.86-54.92-35.08-118.88-83.34-129.82-48.34-11.1-108.7 66.28-114.18 116.74-6.55 61.72 8.79 123.28 76.86 132.06 68.16 8.87 112.03-63.87 120.65-118.97m46.35 433.02s-105.47-81.53-167-169.6c-83.4-129.91-201.98-77.05-241.62-11.02-39.47 66.03-101 107.87-109.7 118.9-8.87 10.93-127.36 74.8-101.07 191.55 26.28 116.65 118.73 114.5 118.73 114.5s68.08 6.7 147.1-10.94C523.7 888.03 591.7 910 591.7 910s184.57 61.72 235.07-57.18c50.41-118.97-28.53-180.61-28.53-180.61M362.42 849.17c-51.83-10.36-72.47-45.65-75.13-51.7-2.57-6.13-17.24-34.55-9.45-82.85 22.39-72.41 86.23-77.63 86.23-77.63h63.85v-78.46l54.4.82.08 289.82zm205.38-.83c-53.56-13.75-56.05-51.78-56.05-51.78V643.95l56.05-.92v137.12c3.4 14.59 21.65 17.32 21.65 17.32h56.88V643.95h59.62v204.39zm323.84-397.72c0-26.35-21.89-105.72-103.15-105.72-81.43 0-92.29 74.9-92.29 127.84 0 50.54 4.31 121.13 105.4 118.8 101.15-2.15 90.04-114.41 90.04-140.92"
|
|
964
1255
|
/>
|
|
@@ -969,6 +1260,19 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
969
1260
|
// Inline stroked icons (currentColor) for the sidebar / toolbar.
|
|
970
1261
|
_icon(name) {
|
|
971
1262
|
switch (name) {
|
|
1263
|
+
case 'moon':
|
|
1264
|
+
return html`
|
|
1265
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
1266
|
+
<path d="M21 12.8A9 9 0 1111.2 3 7 7 0 0021 12.8z" />
|
|
1267
|
+
</svg>
|
|
1268
|
+
`;
|
|
1269
|
+
case 'sun':
|
|
1270
|
+
return html`
|
|
1271
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
1272
|
+
<circle cx="12" cy="12" r="4" />
|
|
1273
|
+
<path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" />
|
|
1274
|
+
</svg>
|
|
1275
|
+
`;
|
|
972
1276
|
case 'components':
|
|
973
1277
|
return html`
|
|
974
1278
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
@@ -986,6 +1290,12 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
986
1290
|
<path d="M4 11v6c0 1.7 3.6 3 8 3s8-1.3 8-3v-6" />
|
|
987
1291
|
</svg>
|
|
988
1292
|
`;
|
|
1293
|
+
case 'timeline':
|
|
1294
|
+
return html`
|
|
1295
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
1296
|
+
<path d="M3 12h4l3 8 4-16 3 8h4" />
|
|
1297
|
+
</svg>
|
|
1298
|
+
`;
|
|
989
1299
|
case 'pick':
|
|
990
1300
|
return html`
|
|
991
1301
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
@@ -1068,6 +1378,7 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
1068
1378
|
--c-null: #b45309;
|
|
1069
1379
|
--c-fn: #2563eb;
|
|
1070
1380
|
--c-obj: #475467;
|
|
1381
|
+
--logo-fill: #2932e1;
|
|
1071
1382
|
|
|
1072
1383
|
position: fixed;
|
|
1073
1384
|
inset: 0;
|
|
@@ -1077,6 +1388,25 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
1077
1388
|
font-size: 12px;
|
|
1078
1389
|
color: var(--text);
|
|
1079
1390
|
}
|
|
1391
|
+
:host([theme='dark']) {
|
|
1392
|
+
--bg: #1e1e20;
|
|
1393
|
+
--surface: #26262a;
|
|
1394
|
+
--border: #34343a;
|
|
1395
|
+
--border-strong: #3a3a42;
|
|
1396
|
+
--field-border: #3a3a42;
|
|
1397
|
+
--text: #e4e4e7;
|
|
1398
|
+
--text-strong: #f4f4f5;
|
|
1399
|
+
--muted: #8b8b93;
|
|
1400
|
+
--muted-2: #a1a1aa;
|
|
1401
|
+
--c-key: #80cbc4;
|
|
1402
|
+
--c-num: #ffcb6b;
|
|
1403
|
+
--c-bool: #c792ea;
|
|
1404
|
+
--c-str: #c3e88d;
|
|
1405
|
+
--c-null: #f78c6c;
|
|
1406
|
+
--c-fn: #82aaff;
|
|
1407
|
+
--c-obj: #b0bec5;
|
|
1408
|
+
--logo-fill: #ffffff;
|
|
1409
|
+
}
|
|
1080
1410
|
.entry {
|
|
1081
1411
|
position: fixed;
|
|
1082
1412
|
pointer-events: auto;
|
|
@@ -1440,7 +1770,23 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
1440
1770
|
cursor: pointer;
|
|
1441
1771
|
|
|
1442
1772
|
&:hover {
|
|
1443
|
-
color:
|
|
1773
|
+
color: var(--text);
|
|
1774
|
+
}
|
|
1775
|
+
& .section-filter {
|
|
1776
|
+
margin-inline-start: 6px;
|
|
1777
|
+
inline-size: 96px;
|
|
1778
|
+
padding: 1px 6px;
|
|
1779
|
+
border: 1px solid var(--field-border);
|
|
1780
|
+
border-radius: 5px;
|
|
1781
|
+
background: var(--bg);
|
|
1782
|
+
color: var(--text-strong);
|
|
1783
|
+
font-size: 11px;
|
|
1784
|
+
text-transform: none;
|
|
1785
|
+
outline: none;
|
|
1786
|
+
cursor: text;
|
|
1787
|
+
}
|
|
1788
|
+
& .section-filter:focus {
|
|
1789
|
+
border-color: var(--accent);
|
|
1444
1790
|
}
|
|
1445
1791
|
}
|
|
1446
1792
|
.vrow {
|
|
@@ -1544,6 +1890,21 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
1544
1890
|
display: inline-block;
|
|
1545
1891
|
margin-block: 4px 8px;
|
|
1546
1892
|
}
|
|
1893
|
+
.ev-time {
|
|
1894
|
+
flex: none;
|
|
1895
|
+
font-size: 10px;
|
|
1896
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
1897
|
+
color: var(--muted);
|
|
1898
|
+
}
|
|
1899
|
+
.ev-comp {
|
|
1900
|
+
margin-inline-start: auto;
|
|
1901
|
+
padding-inline-start: 8px;
|
|
1902
|
+
color: var(--muted);
|
|
1903
|
+
}
|
|
1904
|
+
.node.selected .ev-time,
|
|
1905
|
+
.node.selected .ev-comp {
|
|
1906
|
+
color: rgb(255 255 255 / 0.85);
|
|
1907
|
+
}
|
|
1547
1908
|
`;
|
|
1548
1909
|
}
|
|
1549
1910
|
|