vite-plugin-devtools-vue2 0.1.2 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/lib/config.js +12 -7
- package/lib/events.js +21 -12
- package/lib/hook.js +12 -6
- package/lib/inspector.js +11 -5
- package/lib/main.js +3 -1
- package/lib/panel.js +476 -226
- package/lib/picker.js +55 -31
- package/lib/vuex.js +14 -8
- package/lib/walker.js +46 -18
- package/package.json +9 -1
package/lib/panel.js
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
// on every Vue scheduler flush.
|
|
4
4
|
|
|
5
5
|
import { LitElement, css, html } from 'lit';
|
|
6
|
-
import {
|
|
6
|
+
import { DragThreshold, EdgeMargin, IdleTuckDelay, IdleTuckMargin, PanelEdge, PanelH, PanelW, SectionFilterThreshold, StoreKey } from './config.js';
|
|
7
|
+
import { clearEvents, subscribe as eventsSubscribe, getEvents } from './events.js';
|
|
7
8
|
import hook from './hook.js';
|
|
8
9
|
import { hide, highlight } from './inspector.js';
|
|
9
10
|
import { isPicking, startPicking, stopPicking } from './picker.js';
|
|
10
11
|
import { commitAll, getSnapshots, getStore, hasStore, travelTo, subscribe as vuexSubscribe } from './vuex.js';
|
|
11
|
-
import { clearEvents, getEvents, subscribe as eventsSubscribe } from './events.js';
|
|
12
12
|
import { buildTree, formatValue, getInstance } from './walker.js';
|
|
13
13
|
|
|
14
14
|
export class VueDevToolsPanel extends LitElement {
|
|
@@ -22,6 +22,7 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
22
22
|
tab: { state: true },
|
|
23
23
|
vuexSelected: { state: true },
|
|
24
24
|
timelineSelected: { state: true },
|
|
25
|
+
theme: { state: true },
|
|
25
26
|
renderCodeText: { state: true }
|
|
26
27
|
};
|
|
27
28
|
|
|
@@ -38,15 +39,21 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
38
39
|
this.tab = ui.tab || 'components';
|
|
39
40
|
this.vuexSelected = 0;
|
|
40
41
|
this.timelineSelected = null;
|
|
42
|
+
this.theme = ui.theme || 'light';
|
|
41
43
|
this.renderCodeText = null;
|
|
42
44
|
this.valueExpanded = new Set();
|
|
43
45
|
this.sectionCollapsed = new Set();
|
|
46
|
+
// Nodes collapsed within search results (default: all shown expanded).
|
|
47
|
+
this._searchCollapsed = new Set();
|
|
44
48
|
// Per-section (props/data/…) key filter text, keyed by section title.
|
|
45
49
|
this._sectionFilter = {};
|
|
46
50
|
// Docked position of the entry/panel. Defaults to the bottom edge near
|
|
47
51
|
// the right; { edge: 'left'|'right'|'top'|'bottom', along: number }.
|
|
48
52
|
this._pos = ui.pos || { edge: 'bottom', along: Number.POSITIVE_INFINITY };
|
|
49
53
|
this._drag = null;
|
|
54
|
+
// When collapsed + idle, the entry tucks toward the edge (see IdleTuck*).
|
|
55
|
+
this._idleTucked = false;
|
|
56
|
+
this._idleTimer = 0;
|
|
50
57
|
this._editingPath = null;
|
|
51
58
|
this._focusEdit = false;
|
|
52
59
|
this._flushTimer = null;
|
|
@@ -59,14 +66,16 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
59
66
|
// derived from `el.__vue__` anyway.
|
|
60
67
|
this._domObserver = null;
|
|
61
68
|
this._onFlush = () => this._scheduleRefresh();
|
|
62
|
-
this._onKeydown =
|
|
69
|
+
this._onKeydown = event => this._handleKeydown(event);
|
|
63
70
|
// Keep the entry/panel on-screen when the viewport shrinks. resize can
|
|
64
71
|
// fire many times per second while dragging the window edge, and
|
|
65
72
|
// _applyPos reads layout then writes styles — so coalesce to at most one
|
|
66
73
|
// call per frame via rAF to avoid layout thrashing.
|
|
67
74
|
this._resizeRaf = 0;
|
|
68
75
|
this._onResize = () => {
|
|
69
|
-
if (this._resizeRaf)
|
|
76
|
+
if (this._resizeRaf) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
70
79
|
this._resizeRaf = requestAnimationFrame(() => {
|
|
71
80
|
this._resizeRaf = 0;
|
|
72
81
|
this._applyPos();
|
|
@@ -76,8 +85,8 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
76
85
|
|
|
77
86
|
static _loadUiState() {
|
|
78
87
|
try {
|
|
79
|
-
return JSON.parse(localStorage.getItem(
|
|
80
|
-
} catch (
|
|
88
|
+
return JSON.parse(localStorage.getItem(StoreKey)) || {};
|
|
89
|
+
} catch (err) {
|
|
81
90
|
return {};
|
|
82
91
|
}
|
|
83
92
|
}
|
|
@@ -85,8 +94,8 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
85
94
|
_persistUiState() {
|
|
86
95
|
try {
|
|
87
96
|
const pos = this._pos && Number.isFinite(this._pos.along) ? this._pos : undefined;
|
|
88
|
-
localStorage.setItem(
|
|
89
|
-
} catch (
|
|
97
|
+
localStorage.setItem(StoreKey, JSON.stringify({ collapsed: this.collapsed, tab: this.tab, theme: this.theme, pos }));
|
|
98
|
+
} catch (err) {
|
|
90
99
|
/* storage unavailable — ignore */
|
|
91
100
|
}
|
|
92
101
|
}
|
|
@@ -96,97 +105,111 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
96
105
|
// the viewport; clamped to stay fully on-screen.
|
|
97
106
|
_applyPos() {
|
|
98
107
|
const entry = this.renderRoot && this.renderRoot.querySelector('.entry');
|
|
99
|
-
if (!entry)
|
|
100
|
-
|
|
101
|
-
|
|
108
|
+
if (!entry) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const pos = this._pos || { edge: 'bottom', along: Number.POSITIVE_INFINITY };
|
|
112
|
+
const margin = EdgeMargin;
|
|
102
113
|
const vw = window.innerWidth;
|
|
103
114
|
const vh = window.innerHeight;
|
|
104
115
|
const er = entry.getBoundingClientRect();
|
|
105
116
|
const ew = er.width || 40;
|
|
106
117
|
const eh = er.height || 40;
|
|
107
|
-
const clamp = (
|
|
118
|
+
const clamp = (value, max) => Math.min(Math.max(margin, value), Math.max(margin, max));
|
|
108
119
|
|
|
109
120
|
// Clamped offset of the entry along its docked edge. We derive the panel
|
|
110
121
|
// position from these numbers directly rather than re-reading the entry's
|
|
111
122
|
// live rect — on a fresh open/refresh that rect can still be stale (reads
|
|
112
123
|
// ~0), which left the entry bottom-right but the panel bottom-left.
|
|
113
|
-
const alongX = clamp(
|
|
114
|
-
const alongY = clamp(
|
|
124
|
+
const alongX = clamp(pos.along, vw - ew - margin);
|
|
125
|
+
const alongY = clamp(pos.along, vh - eh - margin);
|
|
115
126
|
|
|
116
127
|
const es = entry.style;
|
|
128
|
+
// When collapsed and idle, tuck the entry toward the edge (smaller /
|
|
129
|
+
// negative margin) so it peeks; hovering/interacting restores it.
|
|
130
|
+
const dockMargin = this.collapsed && this._idleTucked ? IdleTuckMargin : EdgeMargin;
|
|
117
131
|
es.insetInlineStart = es.insetBlockStart = es.insetInlineEnd = es.insetBlockEnd = 'auto';
|
|
118
|
-
if (
|
|
119
|
-
es['inset' + (
|
|
132
|
+
if (pos.edge === 'right' || pos.edge === 'left') {
|
|
133
|
+
es['inset' + (pos.edge === 'right' ? 'InlineEnd' : 'InlineStart')] = dockMargin + 'px';
|
|
120
134
|
es.insetBlockStart = alongY + 'px';
|
|
121
135
|
} else {
|
|
122
|
-
es['inset' + (
|
|
136
|
+
es['inset' + (pos.edge === 'bottom' ? 'BlockEnd' : 'BlockStart')] = dockMargin + 'px';
|
|
123
137
|
es.insetInlineStart = alongX + 'px';
|
|
124
138
|
}
|
|
125
|
-
this.setAttribute('dock',
|
|
139
|
+
this.setAttribute('dock', pos.edge);
|
|
126
140
|
|
|
127
141
|
const panel = this.renderRoot.querySelector('.panel');
|
|
128
|
-
if (!panel)
|
|
142
|
+
if (!panel) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
129
145
|
const ps = panel.style;
|
|
130
146
|
ps.insetInlineStart = ps.insetBlockStart = ps.insetInlineEnd = ps.insetBlockEnd = 'auto';
|
|
131
147
|
// Entry center along its edge, computed from the clamped offsets above.
|
|
132
148
|
const cx = alongX + ew / 2;
|
|
133
149
|
const cy = alongY + eh / 2;
|
|
134
|
-
if (
|
|
135
|
-
ps.insetInlineEnd =
|
|
136
|
-
ps.insetBlockStart = clamp(cy -
|
|
137
|
-
} else if (
|
|
138
|
-
ps.insetInlineStart =
|
|
139
|
-
ps.insetBlockStart = clamp(cy -
|
|
140
|
-
} else if (
|
|
141
|
-
ps.insetBlockStart =
|
|
142
|
-
ps.insetInlineStart = clamp(cx -
|
|
150
|
+
if (pos.edge === 'right') {
|
|
151
|
+
ps.insetInlineEnd = PanelEdge + 'px';
|
|
152
|
+
ps.insetBlockStart = clamp(cy - PanelH / 2, vh - PanelH - margin) + 'px';
|
|
153
|
+
} else if (pos.edge === 'left') {
|
|
154
|
+
ps.insetInlineStart = PanelEdge + 'px';
|
|
155
|
+
ps.insetBlockStart = clamp(cy - PanelH / 2, vh - PanelH - margin) + 'px';
|
|
156
|
+
} else if (pos.edge === 'top') {
|
|
157
|
+
ps.insetBlockStart = PanelEdge + 'px';
|
|
158
|
+
ps.insetInlineStart = clamp(cx - PanelW / 2, vw - PanelW - margin) + 'px';
|
|
143
159
|
} else {
|
|
144
|
-
ps.insetBlockEnd =
|
|
145
|
-
ps.insetInlineStart = clamp(cx -
|
|
160
|
+
ps.insetBlockEnd = PanelEdge + 'px';
|
|
161
|
+
ps.insetInlineStart = clamp(cx - PanelW / 2, vw - PanelW - margin) + 'px';
|
|
146
162
|
}
|
|
147
163
|
}
|
|
148
164
|
|
|
149
165
|
// Drag the always-visible entry. Movement over a threshold = drag (live snap
|
|
150
166
|
// to nearest edge, panel follows); a plain click toggles the panel.
|
|
151
|
-
_startDrag(
|
|
152
|
-
if (
|
|
153
|
-
|
|
167
|
+
_startDrag(event) {
|
|
168
|
+
if (event.button !== 0) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
event.preventDefault();
|
|
154
172
|
hide(); // clear any hover highlight before dragging
|
|
173
|
+
this._wakeEntry();
|
|
155
174
|
const entry = this.renderRoot.querySelector('.entry');
|
|
156
175
|
const rect = entry.getBoundingClientRect();
|
|
157
176
|
this._drag = {
|
|
158
|
-
startX:
|
|
159
|
-
startY:
|
|
160
|
-
offX:
|
|
161
|
-
offY:
|
|
177
|
+
startX: event.clientX,
|
|
178
|
+
startY: event.clientY,
|
|
179
|
+
offX: event.clientX - rect.left,
|
|
180
|
+
offY: event.clientY - rect.top,
|
|
162
181
|
moved: false
|
|
163
182
|
};
|
|
164
|
-
this._onDragMove =
|
|
165
|
-
this._onDragUp =
|
|
183
|
+
this._onDragMove = event => this._dragMove(event);
|
|
184
|
+
this._onDragUp = event => this._dragUp(event);
|
|
166
185
|
window.addEventListener('pointermove', this._onDragMove, true);
|
|
167
186
|
window.addEventListener('pointerup', this._onDragUp, true);
|
|
168
187
|
}
|
|
169
188
|
|
|
170
|
-
_onFabPointerDown(
|
|
171
|
-
this._startDrag(
|
|
189
|
+
_onFabPointerDown(event) {
|
|
190
|
+
this._startDrag(event);
|
|
172
191
|
}
|
|
173
192
|
|
|
174
|
-
_dragMove(
|
|
175
|
-
const
|
|
176
|
-
if (!
|
|
177
|
-
|
|
178
|
-
|
|
193
|
+
_dragMove(event) {
|
|
194
|
+
const drag = this._drag;
|
|
195
|
+
if (!drag) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (!drag.moved && Math.abs(event.clientX - drag.startX) + Math.abs(event.clientY - drag.startY) < DragThreshold) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
drag.moved = true;
|
|
179
202
|
// Snap to the nearest edge live during the drag (not on release).
|
|
180
203
|
const vw = window.innerWidth;
|
|
181
204
|
const vh = window.innerHeight;
|
|
182
205
|
const dist = {
|
|
183
|
-
left:
|
|
184
|
-
right: vw -
|
|
185
|
-
top:
|
|
186
|
-
bottom: vh -
|
|
206
|
+
left: event.clientX,
|
|
207
|
+
right: vw - event.clientX,
|
|
208
|
+
top: event.clientY,
|
|
209
|
+
bottom: vh - event.clientY
|
|
187
210
|
};
|
|
188
|
-
const edge = Object.keys(dist).reduce((
|
|
189
|
-
const along = edge === 'left' || edge === 'right' ?
|
|
211
|
+
const edge = Object.keys(dist).reduce((best, current) => (dist[current] < dist[best] ? current : best));
|
|
212
|
+
const along = edge === 'left' || edge === 'right' ? event.clientY - drag.offY : event.clientX - drag.offX;
|
|
190
213
|
this._pos = { edge, along };
|
|
191
214
|
this._applyPos();
|
|
192
215
|
}
|
|
@@ -194,16 +217,40 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
194
217
|
_dragUp() {
|
|
195
218
|
window.removeEventListener('pointermove', this._onDragMove, true);
|
|
196
219
|
window.removeEventListener('pointerup', this._onDragUp, true);
|
|
197
|
-
const
|
|
220
|
+
const drag = this._drag;
|
|
198
221
|
this._drag = null;
|
|
199
|
-
if (!
|
|
200
|
-
|
|
222
|
+
if (!drag) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (!drag.moved) {
|
|
201
226
|
// plain click → toggle the panel
|
|
202
227
|
this.collapsed = !this.collapsed;
|
|
203
228
|
return;
|
|
204
229
|
}
|
|
205
230
|
// Position was already decided live in _dragMove; just remember it.
|
|
206
231
|
this._persistUiState();
|
|
232
|
+
this._scheduleIdleTuck();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Idle-tuck: when collapsed with no interaction, the entry slides toward the
|
|
236
|
+
// edge; hovering/dragging/opening wakes it back to the normal margin.
|
|
237
|
+
_scheduleIdleTuck() {
|
|
238
|
+
clearTimeout(this._idleTimer);
|
|
239
|
+
if (!this.collapsed) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
this._idleTimer = setTimeout(() => {
|
|
243
|
+
this._idleTucked = true;
|
|
244
|
+
this._applyPos();
|
|
245
|
+
}, IdleTuckDelay);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
_wakeEntry() {
|
|
249
|
+
clearTimeout(this._idleTimer);
|
|
250
|
+
if (this._idleTucked) {
|
|
251
|
+
this._idleTucked = false;
|
|
252
|
+
this._applyPos();
|
|
253
|
+
}
|
|
207
254
|
}
|
|
208
255
|
|
|
209
256
|
connectedCallback() {
|
|
@@ -228,6 +275,7 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
228
275
|
hook.off('flush', this._onFlush);
|
|
229
276
|
window.removeEventListener('keydown', this._onKeydown, true);
|
|
230
277
|
window.removeEventListener('resize', this._onResize);
|
|
278
|
+
clearTimeout(this._idleTimer);
|
|
231
279
|
if (this._resizeRaf) {
|
|
232
280
|
cancelAnimationFrame(this._resizeRaf);
|
|
233
281
|
this._resizeRaf = 0;
|
|
@@ -236,13 +284,23 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
236
284
|
this._domObserver.disconnect();
|
|
237
285
|
this._domObserver = null;
|
|
238
286
|
}
|
|
239
|
-
if (this._vuexUnsub)
|
|
240
|
-
|
|
287
|
+
if (this._vuexUnsub) {
|
|
288
|
+
this._vuexUnsub();
|
|
289
|
+
}
|
|
290
|
+
if (this._eventsUnsub) {
|
|
291
|
+
this._eventsUnsub();
|
|
292
|
+
}
|
|
241
293
|
stopPicking();
|
|
242
294
|
}
|
|
243
295
|
|
|
244
296
|
firstUpdated() {
|
|
245
297
|
this._applyPos();
|
|
298
|
+
this.setAttribute('theme', this.theme);
|
|
299
|
+
this._scheduleIdleTuck();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
_toggleTheme() {
|
|
303
|
+
this.theme = this.theme === 'dark' ? 'light' : 'dark';
|
|
246
304
|
}
|
|
247
305
|
|
|
248
306
|
_scheduleRefresh() {
|
|
@@ -265,16 +323,23 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
265
323
|
}
|
|
266
324
|
|
|
267
325
|
_toggle(id) {
|
|
268
|
-
if (this.expanded.has(id))
|
|
269
|
-
|
|
326
|
+
if (this.expanded.has(id)) {
|
|
327
|
+
this.expanded.delete(id);
|
|
328
|
+
} else {
|
|
329
|
+
this.expanded.add(id);
|
|
330
|
+
}
|
|
270
331
|
this.requestUpdate();
|
|
271
332
|
}
|
|
272
333
|
|
|
273
334
|
// Highlight the page DOM only while hovering a tree row (vue-devtools style).
|
|
274
335
|
_hoverEnter(id) {
|
|
275
|
-
if (this._drag)
|
|
336
|
+
if (this._drag) {
|
|
337
|
+
return; // don't highlight while dragging the entry/panel
|
|
338
|
+
}
|
|
276
339
|
const vm = getInstance(id);
|
|
277
|
-
if (vm)
|
|
340
|
+
if (vm) {
|
|
341
|
+
highlight(vm);
|
|
342
|
+
}
|
|
278
343
|
}
|
|
279
344
|
|
|
280
345
|
_hoverLeave() {
|
|
@@ -299,7 +364,9 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
299
364
|
_selectVm(vm) {
|
|
300
365
|
this.refresh();
|
|
301
366
|
const path = this._pathToVm(vm);
|
|
302
|
-
if (!path.length)
|
|
367
|
+
if (!path.length) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
303
370
|
for (let i = 0; i < path.length - 1; i++) this.expanded.add(path[i]);
|
|
304
371
|
this._select(path[path.length - 1]);
|
|
305
372
|
this._scrollToSelected = true;
|
|
@@ -314,10 +381,18 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
314
381
|
found = next;
|
|
315
382
|
return true;
|
|
316
383
|
}
|
|
317
|
-
for (const
|
|
384
|
+
for (const child of node.children || []) {
|
|
385
|
+
if (dfs(child, next)) {
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
318
389
|
return false;
|
|
319
390
|
};
|
|
320
|
-
for (const
|
|
391
|
+
for (const root of this.tree) {
|
|
392
|
+
if (dfs(root, [])) {
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
321
396
|
return found || [];
|
|
322
397
|
}
|
|
323
398
|
|
|
@@ -327,19 +402,23 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
327
402
|
// match nested inside another match is not promoted (it shows in the subtree).
|
|
328
403
|
_computeFilter() {
|
|
329
404
|
const q = this.query.trim().toLowerCase();
|
|
330
|
-
if (!q)
|
|
405
|
+
if (!q) {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
331
408
|
const matched = new Set();
|
|
332
409
|
const mark = node => {
|
|
333
|
-
if (node.name.toLowerCase().includes(q))
|
|
334
|
-
|
|
410
|
+
if (node.name.toLowerCase().includes(q)) {
|
|
411
|
+
matched.add(node.id);
|
|
412
|
+
}
|
|
413
|
+
for (const child of node.children || []) mark(child);
|
|
335
414
|
};
|
|
336
|
-
for (const
|
|
415
|
+
for (const root of this.tree) mark(root);
|
|
337
416
|
|
|
338
417
|
const roots = [];
|
|
339
418
|
const show = new Set();
|
|
340
419
|
const collect = node => {
|
|
341
420
|
show.add(node.id);
|
|
342
|
-
for (const
|
|
421
|
+
for (const child of node.children || []) collect(child);
|
|
343
422
|
};
|
|
344
423
|
const walk = (node, hasMatchedAncestor) => {
|
|
345
424
|
const isMatch = matched.has(node.id);
|
|
@@ -347,9 +426,9 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
347
426
|
roots.push(node);
|
|
348
427
|
collect(node);
|
|
349
428
|
}
|
|
350
|
-
for (const
|
|
429
|
+
for (const child of node.children || []) walk(child, hasMatchedAncestor || isMatch);
|
|
351
430
|
};
|
|
352
|
-
for (const
|
|
431
|
+
for (const root of this.tree) walk(root, false);
|
|
353
432
|
return { roots, show, q };
|
|
354
433
|
}
|
|
355
434
|
|
|
@@ -360,84 +439,139 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
360
439
|
const parent = new Map();
|
|
361
440
|
const node = new Map();
|
|
362
441
|
if (filter) {
|
|
363
|
-
const walk = (
|
|
364
|
-
node.set(
|
|
365
|
-
parent.set(
|
|
366
|
-
order.push(
|
|
367
|
-
|
|
368
|
-
|
|
442
|
+
const walk = (treeNode, parentId) => {
|
|
443
|
+
node.set(treeNode.id, treeNode);
|
|
444
|
+
parent.set(treeNode.id, parentId);
|
|
445
|
+
order.push(treeNode.id);
|
|
446
|
+
if (this._searchCollapsed.has(treeNode.id)) {
|
|
447
|
+
return; // collapsed in search
|
|
448
|
+
}
|
|
449
|
+
for (const child of treeNode.children || []) {
|
|
450
|
+
if (filter.show.has(child.id)) {
|
|
451
|
+
walk(child, treeNode.id);
|
|
452
|
+
}
|
|
369
453
|
}
|
|
370
454
|
};
|
|
371
|
-
for (const
|
|
455
|
+
for (const root of filter.roots) walk(root, null);
|
|
372
456
|
return { order, parent, node };
|
|
373
457
|
}
|
|
374
|
-
const walk = (
|
|
375
|
-
node.set(
|
|
376
|
-
parent.set(
|
|
377
|
-
order.push(
|
|
378
|
-
if (
|
|
379
|
-
for (const
|
|
458
|
+
const walk = (treeNode, parentId) => {
|
|
459
|
+
node.set(treeNode.id, treeNode);
|
|
460
|
+
parent.set(treeNode.id, parentId);
|
|
461
|
+
order.push(treeNode.id);
|
|
462
|
+
if (treeNode.children && treeNode.children.length && this.expanded.has(treeNode.id)) {
|
|
463
|
+
for (const child of treeNode.children) walk(child, treeNode.id);
|
|
380
464
|
}
|
|
381
465
|
};
|
|
382
|
-
for (const
|
|
466
|
+
for (const root of this.tree) walk(root, null);
|
|
383
467
|
return { order, parent, node };
|
|
384
468
|
}
|
|
385
469
|
|
|
386
470
|
// Arrow-key navigation once a component is selected (VS Code / devtools style):
|
|
387
471
|
// ↑/↓ move through visible rows, → step into / expand, ← step out / collapse.
|
|
388
472
|
// During search the subtree is always shown, so →/← just navigate in/out.
|
|
389
|
-
_handleKeydown(
|
|
390
|
-
if (this.collapsed
|
|
391
|
-
|
|
473
|
+
_handleKeydown(event) {
|
|
474
|
+
if (this.collapsed) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
392
480
|
// Don't hijack arrow keys while typing in a field. composedPath sees into
|
|
393
481
|
// our shadow root (the search box) as well as the app's own inputs.
|
|
394
|
-
const path =
|
|
482
|
+
const path = event.composedPath ? event.composedPath() : [];
|
|
395
483
|
const inEditable = path.some(el => el && el.tagName && (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) || el.isContentEditable));
|
|
396
|
-
if (inEditable)
|
|
484
|
+
if (inEditable) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
397
487
|
|
|
488
|
+
// Timeline tab: ↑/↓ move between entries.
|
|
489
|
+
if (this.tab === 'timeline') {
|
|
490
|
+
if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') {
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
const entries = this._timelineEntries();
|
|
494
|
+
if (!entries.length) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const curIdx = entries.findIndex(item => item.id === this.timelineSelected);
|
|
498
|
+
const base = curIdx < 0 ? entries.length - 1 : curIdx;
|
|
499
|
+
const nextIdx = event.key === 'ArrowDown' ? Math.min(entries.length - 1, base + 1) : Math.max(0, base - 1);
|
|
500
|
+
this.timelineSelected = entries[nextIdx].id;
|
|
501
|
+
this._scrollToSelected = true;
|
|
502
|
+
event.preventDefault();
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Components tab: tree navigation (needs a selected node).
|
|
507
|
+
if (this.selectedId == null) {
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
398
510
|
const filter = this._computeFilter();
|
|
399
511
|
const searching = !!filter;
|
|
400
512
|
const { order, parent, node } = this._index(filter);
|
|
401
513
|
const id = this.selectedId;
|
|
402
514
|
const cur = node.get(id);
|
|
403
|
-
if (!cur)
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
const
|
|
515
|
+
if (!cur) {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
const index = order.indexOf(id);
|
|
519
|
+
if (index < 0) {
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
const shownChildren = (cur.children || []).filter(item => !filter || filter.show.has(item.id));
|
|
407
523
|
const hasChildren = shownChildren.length > 0;
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
const isOpen = searching
|
|
524
|
+
// "open" respects the active mode: normal tree uses `expanded`, search
|
|
525
|
+
// uses `_searchCollapsed` (default open).
|
|
526
|
+
const isOpen = searching ? !this._searchCollapsed.has(id) : this.expanded.has(id);
|
|
411
527
|
let next = null;
|
|
412
528
|
|
|
413
|
-
if (
|
|
414
|
-
next = order[Math.min(order.length - 1,
|
|
415
|
-
} else if (
|
|
416
|
-
next = order[Math.max(0,
|
|
417
|
-
} else if (
|
|
529
|
+
if (event.key === 'ArrowDown') {
|
|
530
|
+
next = order[Math.min(order.length - 1, index + 1)];
|
|
531
|
+
} else if (event.key === 'ArrowUp') {
|
|
532
|
+
next = order[Math.max(0, index - 1)];
|
|
533
|
+
} else if (event.key === 'ArrowRight') {
|
|
418
534
|
// Closed with children -> open it; already open -> step into first child.
|
|
419
|
-
if (hasChildren && !isOpen)
|
|
420
|
-
|
|
421
|
-
|
|
535
|
+
if (hasChildren && !isOpen) {
|
|
536
|
+
searching ? this._searchCollapsed.delete(id) : this.expanded.add(id);
|
|
537
|
+
} else if (hasChildren && isOpen) {
|
|
538
|
+
next = shownChildren[0].id;
|
|
539
|
+
}
|
|
540
|
+
} else if (event.key === 'ArrowLeft') {
|
|
422
541
|
// Check open/closed FIRST: open -> collapse; closed (or leaf) -> parent.
|
|
423
|
-
if (isOpen && hasChildren
|
|
424
|
-
|
|
542
|
+
if (isOpen && hasChildren) {
|
|
543
|
+
searching ? this._searchCollapsed.add(id) : this.expanded.delete(id);
|
|
544
|
+
} else {
|
|
545
|
+
next = parent.get(id);
|
|
546
|
+
}
|
|
425
547
|
}
|
|
426
548
|
|
|
427
|
-
|
|
428
|
-
if (next != null)
|
|
549
|
+
event.preventDefault();
|
|
550
|
+
if (next != null) {
|
|
551
|
+
this._select(next);
|
|
552
|
+
}
|
|
429
553
|
this._scrollToSelected = true;
|
|
430
554
|
this.requestUpdate();
|
|
431
555
|
}
|
|
432
556
|
|
|
433
557
|
updated(changed) {
|
|
434
558
|
// Persist remembered UI bits across reloads.
|
|
435
|
-
if (changed && (changed.has('collapsed') || changed.has('tab'))) {
|
|
559
|
+
if (changed && (changed.has('collapsed') || changed.has('tab') || changed.has('theme'))) {
|
|
436
560
|
this._persistUiState();
|
|
437
561
|
}
|
|
562
|
+
// Reflect the theme onto the host so the CSS variable overrides apply.
|
|
563
|
+
if (changed && changed.has('theme')) {
|
|
564
|
+
this.setAttribute('theme', this.theme);
|
|
565
|
+
}
|
|
438
566
|
// Re-clamp the docked position when switching fab <-> panel (sizes differ).
|
|
439
567
|
if (changed && changed.has('collapsed')) {
|
|
440
568
|
this._applyPos();
|
|
569
|
+
// Opening wakes the entry; collapsing restarts the idle-tuck timer.
|
|
570
|
+
if (this.collapsed) {
|
|
571
|
+
this._scheduleIdleTuck();
|
|
572
|
+
} else {
|
|
573
|
+
this._wakeEntry();
|
|
574
|
+
}
|
|
441
575
|
}
|
|
442
576
|
// Focus a freshly opened inline editor.
|
|
443
577
|
if (this._focusEdit) {
|
|
@@ -450,16 +584,20 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
450
584
|
}
|
|
451
585
|
// When the query changes, auto-select the first match (like vue-devtools).
|
|
452
586
|
if (changed && changed.has('query') && this.query.trim()) {
|
|
453
|
-
const
|
|
454
|
-
if (
|
|
455
|
-
this._select(
|
|
587
|
+
const filter = this._computeFilter();
|
|
588
|
+
if (filter && filter.roots.length && !filter.show.has(this.selectedId)) {
|
|
589
|
+
this._select(filter.roots[0].id);
|
|
456
590
|
this._scrollToSelected = true;
|
|
457
591
|
}
|
|
458
592
|
}
|
|
459
|
-
if (!this._scrollToSelected)
|
|
593
|
+
if (!this._scrollToSelected) {
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
460
596
|
this._scrollToSelected = false;
|
|
461
597
|
const el = this.renderRoot.querySelector('.node.selected');
|
|
462
|
-
if (el)
|
|
598
|
+
if (el) {
|
|
599
|
+
el.scrollIntoView({ block: 'nearest' });
|
|
600
|
+
}
|
|
463
601
|
}
|
|
464
602
|
|
|
465
603
|
_renderNode(node, depth) {
|
|
@@ -477,8 +615,8 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
477
615
|
>
|
|
478
616
|
<span
|
|
479
617
|
class="caret-btn"
|
|
480
|
-
@click=${
|
|
481
|
-
|
|
618
|
+
@click=${event => {
|
|
619
|
+
event.stopPropagation();
|
|
482
620
|
this._toggle(node.id);
|
|
483
621
|
}}
|
|
484
622
|
>
|
|
@@ -486,7 +624,7 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
486
624
|
</span>
|
|
487
625
|
<span class="tag"><${node.name}></span>
|
|
488
626
|
</div>
|
|
489
|
-
${hasChildren && isOpen ? node.children.map(
|
|
627
|
+
${hasChildren && isOpen ? node.children.map(item => this._renderNode(item, depth + 1)) : null}
|
|
490
628
|
</div>
|
|
491
629
|
`;
|
|
492
630
|
}
|
|
@@ -494,8 +632,10 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
494
632
|
// Search result row: matched node as a subtree root, with all descendants
|
|
495
633
|
// shown (always open). Ancestors are omitted. Arrow is non-interactive here.
|
|
496
634
|
_renderSearchNode(node, depth, show) {
|
|
497
|
-
const kids = (node.children || []).filter(
|
|
635
|
+
const kids = (node.children || []).filter(item => show.has(item.id));
|
|
636
|
+
const hasKids = kids.length > 0;
|
|
498
637
|
const isSelected = node.id === this.selectedId;
|
|
638
|
+
const open = !this._searchCollapsed.has(node.id); // default open in search
|
|
499
639
|
return html`
|
|
500
640
|
<div>
|
|
501
641
|
<div
|
|
@@ -505,24 +645,45 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
505
645
|
@mouseenter=${() => this._hoverEnter(node.id)}
|
|
506
646
|
@mouseleave=${() => this._hoverLeave()}
|
|
507
647
|
>
|
|
508
|
-
<span
|
|
648
|
+
<span
|
|
649
|
+
class="caret-btn"
|
|
650
|
+
@click=${event => {
|
|
651
|
+
event.stopPropagation();
|
|
652
|
+
if (hasKids) {
|
|
653
|
+
this._toggleSearchNode(node.id);
|
|
654
|
+
}
|
|
655
|
+
}}
|
|
656
|
+
>
|
|
657
|
+
${this._caret(open && hasKids, !hasKids)}
|
|
658
|
+
</span>
|
|
509
659
|
<span class="tag"><${node.name}></span>
|
|
510
660
|
</div>
|
|
511
|
-
${kids.map(
|
|
661
|
+
${hasKids && open ? kids.map(item => this._renderSearchNode(item, depth + 1, show)) : null}
|
|
512
662
|
</div>
|
|
513
663
|
`;
|
|
514
664
|
}
|
|
515
665
|
|
|
666
|
+
_toggleSearchNode(id) {
|
|
667
|
+
if (this._searchCollapsed.has(id)) {
|
|
668
|
+
this._searchCollapsed.delete(id);
|
|
669
|
+
} else {
|
|
670
|
+
this._searchCollapsed.add(id);
|
|
671
|
+
}
|
|
672
|
+
this.requestUpdate();
|
|
673
|
+
}
|
|
674
|
+
|
|
516
675
|
// Render an object as a titled, collapsible section of expandable value rows.
|
|
517
676
|
// `editable` enables inline editing of primitive leaves (writes back into obj).
|
|
518
677
|
// Sections with many keys get a live filter input in the header.
|
|
519
678
|
_renderKvSection(title, obj, editable) {
|
|
520
679
|
const allKeys = obj ? Object.keys(obj) : [];
|
|
521
|
-
if (!allKeys.length)
|
|
680
|
+
if (!allKeys.length) {
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
522
683
|
const collapsed = this.sectionCollapsed.has(title);
|
|
523
|
-
const showFilter = allKeys.length >
|
|
684
|
+
const showFilter = !collapsed && allKeys.length > SectionFilterThreshold;
|
|
524
685
|
const q = (this._sectionFilter[title] || '').trim().toLowerCase();
|
|
525
|
-
const keys = q ? allKeys.filter(
|
|
686
|
+
const keys = q ? allKeys.filter(item => item.toLowerCase().includes(q)) : allKeys;
|
|
526
687
|
return html`
|
|
527
688
|
<div class="section-title" @click=${() => this._toggleSection(title)}>
|
|
528
689
|
${this._caret(!collapsed, false)}
|
|
@@ -534,16 +695,16 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
534
695
|
type="search"
|
|
535
696
|
placeholder="filter…"
|
|
536
697
|
.value=${this._sectionFilter[title] || ''}
|
|
537
|
-
@click=${
|
|
538
|
-
@keydown=${
|
|
539
|
-
if (
|
|
698
|
+
@click=${event => event.stopPropagation()}
|
|
699
|
+
@keydown=${event => {
|
|
700
|
+
if (event.key === 'Escape') {
|
|
540
701
|
this._sectionFilter[title] = '';
|
|
541
702
|
this.requestUpdate();
|
|
542
703
|
}
|
|
543
|
-
|
|
704
|
+
event.stopPropagation();
|
|
544
705
|
}}
|
|
545
|
-
@input=${
|
|
546
|
-
this._sectionFilter[title] =
|
|
706
|
+
@input=${event => {
|
|
707
|
+
this._sectionFilter[title] = event.target.value;
|
|
547
708
|
this.requestUpdate();
|
|
548
709
|
}}
|
|
549
710
|
/>
|
|
@@ -553,7 +714,7 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
553
714
|
${collapsed
|
|
554
715
|
? null
|
|
555
716
|
: keys.length
|
|
556
|
-
? keys.map(
|
|
717
|
+
? keys.map(item => this._renderValueRow(item, obj[item], `${title}.${item}`, 0, obj, editable))
|
|
557
718
|
: html`
|
|
558
719
|
<div class="empty">No match</div>
|
|
559
720
|
`}
|
|
@@ -561,14 +722,20 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
561
722
|
}
|
|
562
723
|
|
|
563
724
|
_toggleSection(title) {
|
|
564
|
-
if (this.sectionCollapsed.has(title))
|
|
565
|
-
|
|
725
|
+
if (this.sectionCollapsed.has(title)) {
|
|
726
|
+
this.sectionCollapsed.delete(title);
|
|
727
|
+
} else {
|
|
728
|
+
this.sectionCollapsed.add(title);
|
|
729
|
+
}
|
|
566
730
|
this.requestUpdate();
|
|
567
731
|
}
|
|
568
732
|
|
|
569
733
|
_toggleValue(path) {
|
|
570
|
-
if (this.valueExpanded.has(path))
|
|
571
|
-
|
|
734
|
+
if (this.valueExpanded.has(path)) {
|
|
735
|
+
this.valueExpanded.delete(path);
|
|
736
|
+
} else {
|
|
737
|
+
this.valueExpanded.add(path);
|
|
738
|
+
}
|
|
572
739
|
this.requestUpdate();
|
|
573
740
|
}
|
|
574
741
|
|
|
@@ -576,7 +743,7 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
576
743
|
// `editable` is set, primitive leaves can be clicked to edit in place.
|
|
577
744
|
_renderValueRow(keyLabel, value, path, depth, parent, editable) {
|
|
578
745
|
const isObj = value !== null && typeof value === 'object';
|
|
579
|
-
const keys = isObj ? (Array.isArray(value) ? value.map((_,
|
|
746
|
+
const keys = isObj ? (Array.isArray(value) ? value.map((_, index) => index) : Object.keys(value)) : [];
|
|
580
747
|
const expandable = isObj && keys.length > 0;
|
|
581
748
|
const open = this.valueExpanded.has(path);
|
|
582
749
|
const canEdit = editable && !isObj && typeof value !== 'function';
|
|
@@ -594,18 +761,20 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
594
761
|
<input
|
|
595
762
|
class="edit-input"
|
|
596
763
|
.value=${String(value)}
|
|
597
|
-
@click=${
|
|
598
|
-
@keydown=${
|
|
599
|
-
@blur=${
|
|
764
|
+
@click=${event => event.stopPropagation()}
|
|
765
|
+
@keydown=${event => this._onEditKeydown(event, parent, keyLabel, value)}
|
|
766
|
+
@blur=${event => this._commitEdit(parent, keyLabel, event.target.value, value)}
|
|
600
767
|
/>
|
|
601
768
|
`
|
|
602
769
|
: html`
|
|
603
770
|
<span
|
|
604
771
|
class="val ${this._valClass(value)} ${canEdit ? 'editable' : ''}"
|
|
605
772
|
title=${preview}
|
|
606
|
-
@click=${
|
|
607
|
-
if (!canEdit)
|
|
608
|
-
|
|
773
|
+
@click=${event => {
|
|
774
|
+
if (!canEdit) {
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
event.stopPropagation();
|
|
609
778
|
this._editingPath = path;
|
|
610
779
|
this._focusEdit = true;
|
|
611
780
|
this.requestUpdate();
|
|
@@ -619,8 +788,8 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
619
788
|
<button
|
|
620
789
|
class="copy-btn"
|
|
621
790
|
title="Copy value"
|
|
622
|
-
@click=${
|
|
623
|
-
|
|
791
|
+
@click=${event => {
|
|
792
|
+
event.stopPropagation();
|
|
624
793
|
this._copyValue(value);
|
|
625
794
|
}}
|
|
626
795
|
>
|
|
@@ -629,7 +798,7 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
629
798
|
`
|
|
630
799
|
: null}
|
|
631
800
|
</div>
|
|
632
|
-
${expandable && open ? keys.map(
|
|
801
|
+
${expandable && open ? keys.map(item => this._renderValueRow(item, value[item], `${path}.${item}`, depth + 1, value, editable)) : null}
|
|
633
802
|
`;
|
|
634
803
|
}
|
|
635
804
|
|
|
@@ -638,7 +807,7 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
638
807
|
if (value !== null && typeof value === 'object') {
|
|
639
808
|
try {
|
|
640
809
|
text = JSON.stringify(value, null, 2);
|
|
641
|
-
} catch (
|
|
810
|
+
} catch (err) {
|
|
642
811
|
text = String(value);
|
|
643
812
|
}
|
|
644
813
|
} else {
|
|
@@ -666,17 +835,17 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
666
835
|
ta.select();
|
|
667
836
|
try {
|
|
668
837
|
document.execCommand('copy');
|
|
669
|
-
} catch (
|
|
838
|
+
} catch (err) {
|
|
670
839
|
/* ignore */
|
|
671
840
|
}
|
|
672
841
|
document.body.removeChild(ta);
|
|
673
842
|
}
|
|
674
843
|
|
|
675
|
-
_onEditKeydown(
|
|
676
|
-
|
|
677
|
-
if (
|
|
678
|
-
this._commitEdit(parent, key,
|
|
679
|
-
} else if (
|
|
844
|
+
_onEditKeydown(event, parent, key, oldValue) {
|
|
845
|
+
event.stopPropagation();
|
|
846
|
+
if (event.key === 'Enter') {
|
|
847
|
+
this._commitEdit(parent, key, event.target.value, oldValue);
|
|
848
|
+
} else if (event.key === 'Escape') {
|
|
680
849
|
this._editingPath = null;
|
|
681
850
|
this.requestUpdate();
|
|
682
851
|
}
|
|
@@ -687,34 +856,47 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
687
856
|
_commitEdit(parent, key, rawStr, oldValue) {
|
|
688
857
|
this._editingPath = null;
|
|
689
858
|
let parsed = rawStr;
|
|
690
|
-
const
|
|
691
|
-
if (
|
|
692
|
-
const
|
|
693
|
-
parsed = Number.isNaN(
|
|
694
|
-
} else if (
|
|
859
|
+
const type = typeof oldValue;
|
|
860
|
+
if (type === 'number') {
|
|
861
|
+
const num = Number(rawStr);
|
|
862
|
+
parsed = Number.isNaN(num) ? oldValue : num;
|
|
863
|
+
} else if (type === 'boolean') {
|
|
695
864
|
parsed = rawStr === 'true' || rawStr === '1';
|
|
696
865
|
} else if (oldValue === null || oldValue === undefined) {
|
|
697
866
|
try {
|
|
698
867
|
parsed = JSON.parse(rawStr);
|
|
699
|
-
} catch (
|
|
868
|
+
} catch (err) {
|
|
700
869
|
parsed = rawStr;
|
|
701
870
|
}
|
|
702
871
|
}
|
|
703
872
|
const Vue = hook.Vue;
|
|
704
873
|
if (parent) {
|
|
705
|
-
if (Vue && Vue.set)
|
|
706
|
-
|
|
874
|
+
if (Vue && Vue.set) {
|
|
875
|
+
Vue.set(parent, key, parsed);
|
|
876
|
+
} else {
|
|
877
|
+
parent[key] = parsed;
|
|
878
|
+
}
|
|
707
879
|
}
|
|
708
880
|
this.requestUpdate();
|
|
709
881
|
}
|
|
710
882
|
|
|
711
883
|
_valClass(value) {
|
|
712
|
-
if (value === null || value === undefined)
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
if (
|
|
717
|
-
|
|
884
|
+
if (value === null || value === undefined) {
|
|
885
|
+
return 'v-null';
|
|
886
|
+
}
|
|
887
|
+
const type = typeof value;
|
|
888
|
+
if (type === 'number') {
|
|
889
|
+
return 'v-num';
|
|
890
|
+
}
|
|
891
|
+
if (type === 'boolean') {
|
|
892
|
+
return 'v-bool';
|
|
893
|
+
}
|
|
894
|
+
if (type === 'string') {
|
|
895
|
+
return 'v-str';
|
|
896
|
+
}
|
|
897
|
+
if (type === 'function') {
|
|
898
|
+
return 'v-fn';
|
|
899
|
+
}
|
|
718
900
|
return 'v-obj';
|
|
719
901
|
}
|
|
720
902
|
|
|
@@ -734,20 +916,21 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
734
916
|
`;
|
|
735
917
|
}
|
|
736
918
|
const vm = getInstance(this.selectedId);
|
|
737
|
-
if (!vm)
|
|
919
|
+
if (!vm) {
|
|
738
920
|
return html`
|
|
739
921
|
<div class="empty">Component unmounted</div>
|
|
740
922
|
`;
|
|
923
|
+
}
|
|
741
924
|
|
|
742
925
|
const propsObj = vm._props || {};
|
|
743
926
|
const dataObj = vm._data || vm.$data || {};
|
|
744
927
|
const compDefs = (vm.$options && vm.$options.computed) || {};
|
|
745
928
|
const compObj = {};
|
|
746
|
-
for (const
|
|
929
|
+
for (const key of Object.keys(compDefs)) {
|
|
747
930
|
try {
|
|
748
|
-
compObj[
|
|
931
|
+
compObj[key] = vm[key];
|
|
749
932
|
} catch (err) {
|
|
750
|
-
compObj[
|
|
933
|
+
compObj[key] = `⚠ ${err && err.message}`;
|
|
751
934
|
}
|
|
752
935
|
}
|
|
753
936
|
const attrsObj = vm.$attrs || {};
|
|
@@ -790,27 +973,34 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
790
973
|
}
|
|
791
974
|
|
|
792
975
|
_vmName(vm) {
|
|
793
|
-
const
|
|
794
|
-
let
|
|
795
|
-
if (!
|
|
796
|
-
|
|
976
|
+
const options = vm.$options || {};
|
|
977
|
+
let name = options.name || options._componentTag;
|
|
978
|
+
if (!name && options.__file) {
|
|
979
|
+
name = String(options.__file)
|
|
797
980
|
.split(/[\\/]/)
|
|
798
981
|
.pop()
|
|
799
982
|
.replace(/\.vue$/, '');
|
|
800
|
-
|
|
801
|
-
|
|
983
|
+
}
|
|
984
|
+
if (!name && vm.$root === vm) {
|
|
985
|
+
name = 'Root';
|
|
986
|
+
}
|
|
987
|
+
return name || 'Anonymous';
|
|
802
988
|
}
|
|
803
989
|
|
|
804
990
|
// Ask the Vite dev server to open the component's source file in the editor.
|
|
805
991
|
_openInEditor(file) {
|
|
806
|
-
if (!file)
|
|
992
|
+
if (!file) {
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
807
995
|
fetch('/__open-in-editor?file=' + encodeURIComponent(file)).catch(() => {});
|
|
808
996
|
}
|
|
809
997
|
|
|
810
998
|
// Scroll the component's root DOM element into view and flash the highlight.
|
|
811
999
|
_scrollToComponent(vm) {
|
|
812
1000
|
const el = vm && vm.$el;
|
|
813
|
-
if (!el || !el.scrollIntoView)
|
|
1001
|
+
if (!el || !el.scrollIntoView) {
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
814
1004
|
el.scrollIntoView({
|
|
815
1005
|
behavior: 'smooth',
|
|
816
1006
|
block: 'center',
|
|
@@ -833,17 +1023,28 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
833
1023
|
const lines = code.split('\n');
|
|
834
1024
|
let min = Infinity;
|
|
835
1025
|
for (let i = 1; i < lines.length; i++) {
|
|
836
|
-
if (!lines[i].trim())
|
|
1026
|
+
if (!lines[i].trim()) {
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
837
1029
|
const indent = lines[i].match(/^[ \t]*/)[0].length;
|
|
838
|
-
if (indent < min)
|
|
1030
|
+
if (indent < min) {
|
|
1031
|
+
min = indent;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
if (!isFinite(min) || min === 0) {
|
|
1035
|
+
return code;
|
|
839
1036
|
}
|
|
840
|
-
|
|
841
|
-
return lines.map((l, i) => (i === 0 ? l : l.slice(min))).join('\n');
|
|
1037
|
+
return lines.map((item, index) => (index === 0 ? item : item.slice(min))).join('\n');
|
|
842
1038
|
}
|
|
843
1039
|
|
|
844
1040
|
render() {
|
|
845
1041
|
return html`
|
|
846
|
-
<div
|
|
1042
|
+
<div
|
|
1043
|
+
class="entry"
|
|
1044
|
+
@pointerdown=${event => this._onFabPointerDown(event)}
|
|
1045
|
+
@pointerenter=${() => this._wakeEntry()}
|
|
1046
|
+
@pointerleave=${() => this._scheduleIdleTuck()}
|
|
1047
|
+
>
|
|
847
1048
|
<span class="fab-icon">${this._vueLogo()}</span>
|
|
848
1049
|
</div>
|
|
849
1050
|
${this.collapsed ? null : this._renderPanel()}
|
|
@@ -854,7 +1055,6 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
854
1055
|
return html`
|
|
855
1056
|
<div class="panel">
|
|
856
1057
|
<nav class="sidebar">
|
|
857
|
-
<div class="logo">${this._vueLogo()}</div>
|
|
858
1058
|
<button class="side-tab ${this.tab === 'components' ? 'active' : ''}" @click=${() => (this.tab = 'components')}>
|
|
859
1059
|
${this._icon('components')}
|
|
860
1060
|
<span class="tip">Components</span>
|
|
@@ -876,6 +1076,10 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
876
1076
|
</button>
|
|
877
1077
|
`
|
|
878
1078
|
: null}
|
|
1079
|
+
<button class="side-tab" @click=${() => this._toggleTheme()}>
|
|
1080
|
+
${this._icon(this.theme === 'dark' ? 'sun' : 'moon')}
|
|
1081
|
+
<span class="tip">${this.theme === 'dark' ? 'Light theme' : 'Dark theme'}</span>
|
|
1082
|
+
</button>
|
|
879
1083
|
<button class="side-tab side-tab--min" @click=${() => (this.collapsed = true)}>
|
|
880
1084
|
${this._icon('min')}
|
|
881
1085
|
<span class="tip">Minimize</span>
|
|
@@ -909,11 +1113,11 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
909
1113
|
type="search"
|
|
910
1114
|
placeholder="Search components…"
|
|
911
1115
|
.value=${this.query}
|
|
912
|
-
@input=${
|
|
913
|
-
@keydown=${
|
|
914
|
-
if (
|
|
1116
|
+
@input=${event => (this.query = event.target.value)}
|
|
1117
|
+
@keydown=${event => {
|
|
1118
|
+
if (event.key === 'Escape') {
|
|
915
1119
|
this.query = '';
|
|
916
|
-
|
|
1120
|
+
event.stopPropagation();
|
|
917
1121
|
}
|
|
918
1122
|
}}
|
|
919
1123
|
/>
|
|
@@ -929,8 +1133,8 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
929
1133
|
<div class="empty">No component matches</div>
|
|
930
1134
|
`
|
|
931
1135
|
: filter
|
|
932
|
-
? filter.roots.map(
|
|
933
|
-
: this.tree.map(
|
|
1136
|
+
? filter.roots.map(item => this._renderSearchNode(item, 0, filter.show))
|
|
1137
|
+
: this.tree.map(item => this._renderNode(item, 0))}
|
|
934
1138
|
</div>
|
|
935
1139
|
<div class="detail">${this._renderDetail()}</div>
|
|
936
1140
|
</div>
|
|
@@ -964,10 +1168,10 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
964
1168
|
</button>
|
|
965
1169
|
</div>
|
|
966
1170
|
${snaps.map(
|
|
967
|
-
(
|
|
968
|
-
<div class="node ${
|
|
969
|
-
<span class="mut-index">${
|
|
970
|
-
<span class="tag">${
|
|
1171
|
+
(item, index) => html`
|
|
1172
|
+
<div class="node ${index === sel ? 'selected' : ''}" @click=${() => (this.vuexSelected = index)}>
|
|
1173
|
+
<span class="mut-index">${item.base ? '' : index}</span>
|
|
1174
|
+
<span class="tag">${item.base ? 'Base State' : item.type}</span>
|
|
971
1175
|
</div>
|
|
972
1176
|
`
|
|
973
1177
|
)}
|
|
@@ -995,14 +1199,16 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
995
1199
|
// (like the vue-devtools v7 Timeline).
|
|
996
1200
|
_timelineEntries() {
|
|
997
1201
|
const entries = [];
|
|
998
|
-
for (const
|
|
999
|
-
entries.push({ id: `e${
|
|
1202
|
+
for (const event of getEvents()) {
|
|
1203
|
+
entries.push({ id: `e${event.id}`, kind: 'event', time: event.time, title: event.name, sub: `<${event.component}>`, event: event });
|
|
1000
1204
|
}
|
|
1001
|
-
getSnapshots().forEach((
|
|
1002
|
-
if (
|
|
1003
|
-
|
|
1205
|
+
getSnapshots().forEach((item, index) => {
|
|
1206
|
+
if (item.base) {
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
entries.push({ id: `m${index}`, kind: 'mutation', time: item.time || 0, title: item.type, sub: 'vuex', snap: item, index });
|
|
1004
1210
|
});
|
|
1005
|
-
entries.sort((
|
|
1211
|
+
entries.sort((first, second) => first.time - second.time);
|
|
1006
1212
|
return entries;
|
|
1007
1213
|
}
|
|
1008
1214
|
|
|
@@ -1013,7 +1219,7 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
1013
1219
|
<div class="body"><div class="empty">No timeline activity yet</div></div>
|
|
1014
1220
|
`;
|
|
1015
1221
|
}
|
|
1016
|
-
const sel = entries.find(
|
|
1222
|
+
const sel = entries.find(item => item.id === this.timelineSelected) || entries[entries.length - 1];
|
|
1017
1223
|
return html`
|
|
1018
1224
|
<div class="body">
|
|
1019
1225
|
<div class="tree">
|
|
@@ -1030,12 +1236,11 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
1030
1236
|
</button>
|
|
1031
1237
|
</div>
|
|
1032
1238
|
${entries.map(
|
|
1033
|
-
|
|
1034
|
-
<div class="node ${
|
|
1035
|
-
<span class="
|
|
1036
|
-
<span class="
|
|
1037
|
-
<span class="
|
|
1038
|
-
<span class="ev-comp">${e.sub}</span>
|
|
1239
|
+
item => html`
|
|
1240
|
+
<div class="node ${item.id === sel.id ? 'selected' : ''}" @click=${() => (this.timelineSelected = item.id)}>
|
|
1241
|
+
<span class="ev-time">${this._formatTime(item.time)}</span>
|
|
1242
|
+
<span class="tag">${item.title}</span>
|
|
1243
|
+
<span class="ev-comp">${item.sub}</span>
|
|
1039
1244
|
</div>
|
|
1040
1245
|
`
|
|
1041
1246
|
)}
|
|
@@ -1046,46 +1251,48 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
1046
1251
|
}
|
|
1047
1252
|
|
|
1048
1253
|
_renderTimelineDetail(entry) {
|
|
1049
|
-
if (!entry)
|
|
1254
|
+
if (!entry) {
|
|
1255
|
+
return null;
|
|
1256
|
+
}
|
|
1050
1257
|
if (entry.kind === 'event') {
|
|
1051
|
-
const
|
|
1258
|
+
const event = entry.event;
|
|
1052
1259
|
const argsObj = {};
|
|
1053
|
-
(
|
|
1054
|
-
argsObj[
|
|
1260
|
+
(event.args || []).forEach((item, index) => {
|
|
1261
|
+
argsObj[index] = item;
|
|
1055
1262
|
});
|
|
1056
1263
|
return html`
|
|
1057
|
-
${this._renderKvSection('event', { name:
|
|
1058
|
-
${
|
|
1264
|
+
${this._renderKvSection('event', { name: event.name, from: event.component, time: new Date(event.time).toLocaleTimeString() }, false)}
|
|
1265
|
+
${event.args && event.args.length
|
|
1059
1266
|
? this._renderKvSection('payload', argsObj, false)
|
|
1060
1267
|
: html`
|
|
1061
1268
|
<div class="empty">No payload</div>
|
|
1062
1269
|
`}
|
|
1063
1270
|
`;
|
|
1064
1271
|
}
|
|
1065
|
-
const
|
|
1272
|
+
const snap = entry.snap;
|
|
1066
1273
|
const store = getStore();
|
|
1067
|
-
const payloadObj =
|
|
1274
|
+
const payloadObj = snap.payload === undefined ? null : { payload: snap.payload };
|
|
1068
1275
|
return html`
|
|
1069
1276
|
<button class="btn on time-travel" @click=${() => travelTo(entry.index)}>⏱ Time Travel</button>
|
|
1070
|
-
${this._renderKvSection('mutation', { type:
|
|
1071
|
-
${payloadObj ? this._renderKvSection('payload', payloadObj, false) : null}
|
|
1072
|
-
${this._renderKvSection('state', s.state || {}, false)}
|
|
1277
|
+
${this._renderKvSection('mutation', { type: snap.type, time: new Date(snap.time).toLocaleTimeString() }, false)}
|
|
1278
|
+
${payloadObj ? this._renderKvSection('payload', payloadObj, false) : null} ${this._renderKvSection('state', snap.state || {}, false)}
|
|
1073
1279
|
${store ? this._renderKvSection('getters (live)', store.getters || {}, false) : null}
|
|
1074
1280
|
`;
|
|
1075
1281
|
}
|
|
1076
1282
|
|
|
1077
|
-
_formatTime(
|
|
1078
|
-
const
|
|
1079
|
-
const
|
|
1080
|
-
return `${
|
|
1283
|
+
_formatTime(time) {
|
|
1284
|
+
const date = new Date(time);
|
|
1285
|
+
const pad = (num, length = 2) => String(num).padStart(length, '0');
|
|
1286
|
+
return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}`;
|
|
1081
1287
|
}
|
|
1082
1288
|
|
|
1083
|
-
// Plugin logo
|
|
1289
|
+
// Plugin logo (Vite mark).
|
|
1084
1290
|
_vueLogo() {
|
|
1085
1291
|
return html`
|
|
1086
|
-
<svg
|
|
1292
|
+
<svg class="vlogo" viewBox="0 0 48 46" fill="none" aria-hidden="true">
|
|
1087
1293
|
<path
|
|
1088
|
-
d="
|
|
1294
|
+
d="M25.9456 44.9383C25.2821 45.7827 23.925 45.3131 23.925 44.2403V33.9369C23.925 32.6875 22.9126 31.6751 21.6631 31.6751H10.287C9.36714 31.6751 8.83075 30.6346 9.36713 29.8871L16.8464 19.4157C17.917 17.9185 16.8464 15.8376 15.0046 15.8376H1.23731C0.317479 15.8376 -0.218913 14.7972 0.317475 14.0497L10.0134 0.4741C10.2266 0.176825 10.5692 0.000183105 10.9332 0.000183105H39.8271C40.7469 0.000183105 41.2833 1.04065 40.7469 1.78814L33.2676 12.2595C32.197 13.7567 33.2676 15.8376 35.1094 15.8376H46.4856C47.4291 15.8376 47.959 16.9255 47.3753 17.6687L25.9478 44.9404L25.9456 44.9383Z"
|
|
1295
|
+
fill="#863BFF"
|
|
1089
1296
|
/>
|
|
1090
1297
|
</svg>
|
|
1091
1298
|
`;
|
|
@@ -1094,6 +1301,19 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
1094
1301
|
// Inline stroked icons (currentColor) for the sidebar / toolbar.
|
|
1095
1302
|
_icon(name) {
|
|
1096
1303
|
switch (name) {
|
|
1304
|
+
case 'moon':
|
|
1305
|
+
return html`
|
|
1306
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
1307
|
+
<path d="M21 12.8A9 9 0 1111.2 3 7 7 0 0021 12.8z" />
|
|
1308
|
+
</svg>
|
|
1309
|
+
`;
|
|
1310
|
+
case 'sun':
|
|
1311
|
+
return html`
|
|
1312
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
1313
|
+
<circle cx="12" cy="12" r="4" />
|
|
1314
|
+
<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" />
|
|
1315
|
+
</svg>
|
|
1316
|
+
`;
|
|
1097
1317
|
case 'components':
|
|
1098
1318
|
return html`
|
|
1099
1319
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
@@ -1199,6 +1419,7 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
1199
1419
|
--c-null: #b45309;
|
|
1200
1420
|
--c-fn: #2563eb;
|
|
1201
1421
|
--c-obj: #475467;
|
|
1422
|
+
--logo-fill: #2932e1;
|
|
1202
1423
|
|
|
1203
1424
|
position: fixed;
|
|
1204
1425
|
inset: 0;
|
|
@@ -1208,6 +1429,25 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
1208
1429
|
font-size: 12px;
|
|
1209
1430
|
color: var(--text);
|
|
1210
1431
|
}
|
|
1432
|
+
:host([theme='dark']) {
|
|
1433
|
+
--bg: #1e1e20;
|
|
1434
|
+
--surface: #26262a;
|
|
1435
|
+
--border: #34343a;
|
|
1436
|
+
--border-strong: #3a3a42;
|
|
1437
|
+
--field-border: #3a3a42;
|
|
1438
|
+
--text: #e4e4e7;
|
|
1439
|
+
--text-strong: #f4f4f5;
|
|
1440
|
+
--muted: #8b8b93;
|
|
1441
|
+
--muted-2: #a1a1aa;
|
|
1442
|
+
--c-key: #80cbc4;
|
|
1443
|
+
--c-num: #ffcb6b;
|
|
1444
|
+
--c-bool: #c792ea;
|
|
1445
|
+
--c-str: #c3e88d;
|
|
1446
|
+
--c-null: #f78c6c;
|
|
1447
|
+
--c-fn: #82aaff;
|
|
1448
|
+
--c-obj: #b0bec5;
|
|
1449
|
+
--logo-fill: #ffffff;
|
|
1450
|
+
}
|
|
1211
1451
|
.entry {
|
|
1212
1452
|
position: fixed;
|
|
1213
1453
|
pointer-events: auto;
|
|
@@ -1223,6 +1463,11 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
1223
1463
|
background: var(--bg);
|
|
1224
1464
|
border: 1px solid var(--border-strong);
|
|
1225
1465
|
box-shadow: 0 6px 20px rgb(16 24 40 / 0.18);
|
|
1466
|
+
transition:
|
|
1467
|
+
inset-block-start 0.2s ease,
|
|
1468
|
+
inset-block-end 0.2s ease,
|
|
1469
|
+
inset-inline-start 0.2s ease,
|
|
1470
|
+
inset-inline-end 0.2s ease;
|
|
1226
1471
|
}
|
|
1227
1472
|
.entry:active {
|
|
1228
1473
|
cursor: grabbing;
|
|
@@ -1237,6 +1482,11 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
1237
1482
|
inline-size: 16px;
|
|
1238
1483
|
block-size: 16px;
|
|
1239
1484
|
}
|
|
1485
|
+
.fab-icon .vlogo {
|
|
1486
|
+
inline-size: 18px;
|
|
1487
|
+
block-size: 18px;
|
|
1488
|
+
display: block;
|
|
1489
|
+
}
|
|
1240
1490
|
/* On the left/right edges, stack the entry's icons vertically. */
|
|
1241
1491
|
:host([dock='left']) .entry,
|
|
1242
1492
|
:host([dock='right']) .entry {
|
|
@@ -1571,7 +1821,7 @@ export class VueDevToolsPanel extends LitElement {
|
|
|
1571
1821
|
cursor: pointer;
|
|
1572
1822
|
|
|
1573
1823
|
&:hover {
|
|
1574
|
-
color:
|
|
1824
|
+
color: var(--text);
|
|
1575
1825
|
}
|
|
1576
1826
|
& .section-filter {
|
|
1577
1827
|
margin-inline-start: 6px;
|