zero-query 0.7.5 → 0.8.7
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 +39 -30
- package/cli/commands/build.js +110 -1
- package/cli/commands/bundle.js +127 -50
- package/cli/commands/create.js +1 -1
- package/cli/commands/dev/devtools/index.js +56 -0
- package/cli/commands/dev/devtools/js/components.js +49 -0
- package/cli/commands/dev/devtools/js/core.js +409 -0
- package/cli/commands/dev/devtools/js/elements.js +413 -0
- package/cli/commands/dev/devtools/js/network.js +166 -0
- package/cli/commands/dev/devtools/js/performance.js +73 -0
- package/cli/commands/dev/devtools/js/router.js +105 -0
- package/cli/commands/dev/devtools/js/source.js +132 -0
- package/cli/commands/dev/devtools/js/stats.js +35 -0
- package/cli/commands/dev/devtools/js/tabs.js +79 -0
- package/cli/commands/dev/devtools/panel.html +95 -0
- package/cli/commands/dev/devtools/styles.css +244 -0
- package/cli/commands/dev/index.js +28 -3
- package/cli/commands/dev/logger.js +6 -1
- package/cli/commands/dev/overlay.js +377 -0
- package/cli/commands/dev/server.js +8 -0
- package/cli/commands/dev/watcher.js +26 -1
- package/cli/help.js +8 -5
- package/cli/scaffold/{scripts → app}/app.js +1 -1
- package/cli/scaffold/{scripts → app}/components/about.js +4 -4
- package/cli/scaffold/{scripts → app}/components/api-demo.js +1 -1
- package/cli/scaffold/app/components/home.js +137 -0
- package/cli/scaffold/{scripts → app}/routes.js +1 -1
- package/cli/scaffold/{scripts → app}/store.js +6 -6
- package/cli/scaffold/assets/.gitkeep +0 -0
- package/cli/scaffold/{styles/styles.css → global.css} +3 -2
- package/cli/scaffold/index.html +11 -11
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +740 -226
- package/dist/zquery.min.js +2 -2
- package/index.d.ts +11 -11
- package/index.js +15 -10
- package/package.json +3 -2
- package/src/component.js +154 -139
- package/src/core.js +57 -11
- package/src/diff.js +256 -58
- package/src/expression.js +33 -3
- package/src/reactive.js +37 -5
- package/src/router.js +196 -7
- package/src/ssr.js +1 -1
- package/tests/component.test.js +582 -0
- package/tests/core.test.js +251 -0
- package/tests/diff.test.js +333 -2
- package/tests/expression.test.js +148 -0
- package/tests/http.test.js +108 -0
- package/tests/reactive.test.js +148 -0
- package/tests/router.test.js +317 -0
- package/tests/store.test.js +126 -0
- package/tests/utils.test.js +161 -2
- package/types/collection.d.ts +17 -2
- package/types/component.d.ts +10 -34
- package/types/misc.d.ts +13 -0
- package/types/router.d.ts +30 -1
- package/cli/commands/dev.old.js +0 -520
- package/cli/scaffold/scripts/components/home.js +0 -137
- /package/cli/scaffold/{scripts → app}/components/contacts/contacts.css +0 -0
- /package/cli/scaffold/{scripts → app}/components/contacts/contacts.html +0 -0
- /package/cli/scaffold/{scripts → app}/components/contacts/contacts.js +0 -0
- /package/cli/scaffold/{scripts → app}/components/counter.js +0 -0
- /package/cli/scaffold/{scripts → app}/components/not-found.js +0 -0
- /package/cli/scaffold/{scripts → app}/components/todos.js +0 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
// ===================================================================
|
|
2
|
+
// DOM Tree
|
|
3
|
+
// ===================================================================
|
|
4
|
+
function getNodePath(node) {
|
|
5
|
+
var parts = [];
|
|
6
|
+
var cur = node;
|
|
7
|
+
while (cur && cur.nodeType === 1) {
|
|
8
|
+
var tag = cur.tagName.toLowerCase();
|
|
9
|
+
var idx = 0;
|
|
10
|
+
var sib = cur.previousElementSibling;
|
|
11
|
+
while (sib) { if (sib.tagName === cur.tagName) idx++; sib = sib.previousElementSibling; }
|
|
12
|
+
parts.unshift(tag + (idx ? ':' + idx : ''));
|
|
13
|
+
cur = cur.parentElement;
|
|
14
|
+
}
|
|
15
|
+
return parts.join('>');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function refreshComponentNames() {
|
|
19
|
+
componentNames = {};
|
|
20
|
+
try {
|
|
21
|
+
if (targetWin && targetWin.$ && typeof targetWin.$.components === 'function') {
|
|
22
|
+
var reg = targetWin.$.components();
|
|
23
|
+
var keys = Object.keys(reg);
|
|
24
|
+
for (var i = 0; i < keys.length; i++) componentNames[keys[i].toLowerCase()] = true;
|
|
25
|
+
}
|
|
26
|
+
} catch(e) {}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function buildDOMTree() {
|
|
30
|
+
refreshComponentNames();
|
|
31
|
+
var tree = document.getElementById('dom-tree');
|
|
32
|
+
tree.innerHTML = '';
|
|
33
|
+
if (!targetDoc) return;
|
|
34
|
+
var root = targetDoc.documentElement;
|
|
35
|
+
tree.appendChild(buildNode(root, 0));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function buildNode(node, depth) {
|
|
39
|
+
var wrap = document.createElement('div');
|
|
40
|
+
wrap.className = 'tree-node';
|
|
41
|
+
|
|
42
|
+
if (node.nodeType === 3) {
|
|
43
|
+
var text = node.textContent.trim();
|
|
44
|
+
if (!text) return wrap;
|
|
45
|
+
var row = document.createElement('div');
|
|
46
|
+
row.className = 'tree-row';
|
|
47
|
+
var truncated = text.length > 60;
|
|
48
|
+
row.innerHTML = '<span class="tree-toggle leaf"></span><span class="tree-text">' +
|
|
49
|
+
esc(truncated ? text.slice(0, 60) + '...' : text) + '</span>' +
|
|
50
|
+
(truncated ? '<span class="tree-peek" title="View full text">👁</span>' : '');
|
|
51
|
+
if (truncated) {
|
|
52
|
+
row.__sourceText = text;
|
|
53
|
+
row.__sourceLabel = 'Text content';
|
|
54
|
+
}
|
|
55
|
+
wrap.appendChild(row);
|
|
56
|
+
return wrap;
|
|
57
|
+
}
|
|
58
|
+
if (node.nodeType === 8) {
|
|
59
|
+
var row = document.createElement('div');
|
|
60
|
+
row.className = 'tree-row';
|
|
61
|
+
row.innerHTML = '<span class="tree-toggle leaf"></span><span class="tree-comment"><!-- ' +
|
|
62
|
+
esc(node.textContent.trim().slice(0, 40)) + ' --></span>';
|
|
63
|
+
wrap.appendChild(row);
|
|
64
|
+
return wrap;
|
|
65
|
+
}
|
|
66
|
+
if (node.nodeType !== 1) return wrap;
|
|
67
|
+
|
|
68
|
+
// Skip devtools-injected overlay
|
|
69
|
+
if (node.id === '__zq_error_overlay' || node.id === '__zq_devbar') return wrap;
|
|
70
|
+
|
|
71
|
+
var tag = node.tagName.toLowerCase();
|
|
72
|
+
var nodePath = getNodePath(node);
|
|
73
|
+
var hasChildren = false;
|
|
74
|
+
var childNodes = node.childNodes;
|
|
75
|
+
// style/script content is shown inline via the peek button — treat as leaf
|
|
76
|
+
if (tag !== 'style' && tag !== 'script') {
|
|
77
|
+
for (var i = 0; i < childNodes.length; i++) {
|
|
78
|
+
var cn = childNodes[i];
|
|
79
|
+
if (cn.nodeType === 1 || (cn.nodeType === 3 && cn.textContent.trim())) { hasChildren = true; break; }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Detect special nodes
|
|
84
|
+
var isScopedStyle = tag === 'style' && node.hasAttribute('data-zq-scope');
|
|
85
|
+
var isComponent = !isScopedStyle && (componentNames[tag] || node.hasAttribute('data-zq-component'));
|
|
86
|
+
var isRouterView = tag === 'div' && (node.id === 'app' || node.hasAttribute('data-zq-router'));
|
|
87
|
+
var isEntryPoint = tag === 'body' || isRouterView;
|
|
88
|
+
|
|
89
|
+
// Build row
|
|
90
|
+
var row = document.createElement('div');
|
|
91
|
+
row.className = 'tree-row';
|
|
92
|
+
row.__targetNode = node;
|
|
93
|
+
|
|
94
|
+
var toggleCls = hasChildren ? 'tree-toggle' : 'tree-toggle leaf';
|
|
95
|
+
var html = '<span class="' + toggleCls + '">▶</span>';
|
|
96
|
+
html += '<span class="tree-tag"><' + tag + '</span>';
|
|
97
|
+
|
|
98
|
+
// Show key attributes inline (skip internal zq attrs on scoped styles)
|
|
99
|
+
var attrs = node.attributes;
|
|
100
|
+
var shown = 0;
|
|
101
|
+
for (var i = 0; i < attrs.length && shown < 4; i++) {
|
|
102
|
+
var a = attrs[i];
|
|
103
|
+
if (a.name === 'style' || a.name === 'class' && a.value.length > 40) continue;
|
|
104
|
+
if (isScopedStyle && /^data-zq-/.test(a.name)) continue;
|
|
105
|
+
html += ' <span class="tree-attr-name">' + esc(a.name) + '</span>';
|
|
106
|
+
if (a.value) html += '=<span class="tree-attr-val">"' + esc(a.value.length > 30 ? a.value.slice(0, 30) + '...' : a.value) + '"</span>';
|
|
107
|
+
shown++;
|
|
108
|
+
}
|
|
109
|
+
html += '<span class="tree-tag">></span>';
|
|
110
|
+
|
|
111
|
+
// Badges for components, router, entry-point, scoped css
|
|
112
|
+
if (isScopedStyle) {
|
|
113
|
+
var scopeComp = node.getAttribute('data-zq-component');
|
|
114
|
+
html += '<span class="tree-badge scoped">scoped css' + (scopeComp ? ' · ' + esc(scopeComp) : '') + '</span>';
|
|
115
|
+
} else if (isComponent) html += '<span class="tree-badge comp">component</span>';
|
|
116
|
+
if (isRouterView) html += '<span class="tree-badge router">router</span>';
|
|
117
|
+
else if (isEntryPoint) html += '<span class="tree-badge entry">entry</span>';
|
|
118
|
+
|
|
119
|
+
// Expand / collapse action buttons (visible on hover)
|
|
120
|
+
if (hasChildren) {
|
|
121
|
+
html += '<span class="tree-actions">';
|
|
122
|
+
html += '<button class="tree-action" data-act="expand" title="Expand all">+</button>';
|
|
123
|
+
html += '<button class="tree-action" data-act="collapse" title="Collapse all">−</button>';
|
|
124
|
+
html += '</span>';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Inline text for leaf elements; add peek button for style/script with any content, or long text
|
|
128
|
+
var hasInlineContent = false;
|
|
129
|
+
if (tag === 'style' || tag === 'script') {
|
|
130
|
+
var fullText = node.textContent.trim();
|
|
131
|
+
if (fullText) {
|
|
132
|
+
var preview = fullText.length > 50 ? fullText.slice(0, 50) + '...' : fullText;
|
|
133
|
+
html += '<span class="tree-text">' + esc(preview) + '</span>';
|
|
134
|
+
html += '<span class="tree-peek" title="View full source">👁</span>';
|
|
135
|
+
hasInlineContent = true;
|
|
136
|
+
}
|
|
137
|
+
} else if (!hasChildren || (childNodes.length === 1 && childNodes[0].nodeType === 3)) {
|
|
138
|
+
var text = node.textContent.trim();
|
|
139
|
+
if (text && text.length < 60) {
|
|
140
|
+
html += '<span class="tree-text">' + esc(text) + '</span>';
|
|
141
|
+
html += '<span class="tree-tag"></' + tag + '></span>';
|
|
142
|
+
} else if (text && text.length >= 60) {
|
|
143
|
+
html += '<span class="tree-text">' + esc(text.slice(0, 50) + '...') + '</span>';
|
|
144
|
+
html += '<span class="tree-peek" title="View full text">👁</span>';
|
|
145
|
+
hasInlineContent = true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
row.innerHTML = html;
|
|
150
|
+
// Attach source data for peek overlay
|
|
151
|
+
if (hasInlineContent) {
|
|
152
|
+
row.__sourceText = node.textContent.trim();
|
|
153
|
+
row.__sourceIsCSS = (tag === 'style');
|
|
154
|
+
if (tag === 'style') {
|
|
155
|
+
var scopeComp = node.getAttribute('data-zq-component');
|
|
156
|
+
row.__sourceLabel = '<span class="tree-tag"><style></span>' + (scopeComp ? ' <span class="tree-badge scoped">scoped css \u00b7 ' + esc(scopeComp) + '</span>' : '');
|
|
157
|
+
} else if (tag === 'script') {
|
|
158
|
+
row.__sourceLabel = '<span class="tree-tag"><script></span>';
|
|
159
|
+
} else {
|
|
160
|
+
row.__sourceLabel = '<span class="tree-tag"><' + tag + '></span> text content';
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
wrap.appendChild(row);
|
|
164
|
+
|
|
165
|
+
// Children container
|
|
166
|
+
if (hasChildren) {
|
|
167
|
+
var childDiv = document.createElement('div');
|
|
168
|
+
childDiv.className = 'tree-children';
|
|
169
|
+
|
|
170
|
+
// Restore expanded state or auto-expand; entry/router always expand, changed paths auto-expand (capped at depth 5)
|
|
171
|
+
var alwaysOpen = isEntryPoint || isRouterView;
|
|
172
|
+
var changedNow = changedPaths[nodePath] && depth < 6;
|
|
173
|
+
var isExpanded = alwaysOpen || (expandedPaths.hasOwnProperty(nodePath) ? expandedPaths[nodePath] : (depth < 3 || changedNow));
|
|
174
|
+
if (isExpanded) {
|
|
175
|
+
childDiv.classList.add('open');
|
|
176
|
+
row.querySelector('.tree-toggle').classList.add('open');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Flash only actual mutation targets, not ancestors
|
|
180
|
+
if (mutatedPaths[nodePath]) {
|
|
181
|
+
row.classList.add('morph-changed');
|
|
182
|
+
row.setAttribute('data-changed', '1');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
for (var i = 0; i < childNodes.length; i++) {
|
|
186
|
+
var child = buildNode(childNodes[i], depth + 1);
|
|
187
|
+
if (child.innerHTML) childDiv.appendChild(child);
|
|
188
|
+
}
|
|
189
|
+
wrap.appendChild(childDiv);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Toggle click (on the arrow only)
|
|
193
|
+
var toggleEl = row.querySelector('.tree-toggle');
|
|
194
|
+
if (hasChildren) {
|
|
195
|
+
toggleEl.addEventListener('click', function(e) {
|
|
196
|
+
e.stopPropagation();
|
|
197
|
+
var children = row.nextElementSibling;
|
|
198
|
+
if (children && children.classList.contains('tree-children')) {
|
|
199
|
+
var opening = !children.classList.contains('open');
|
|
200
|
+
children.classList.toggle('open');
|
|
201
|
+
toggleEl.classList.toggle('open');
|
|
202
|
+
expandedPaths[nodePath] = opening;
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Expand / collapse action buttons
|
|
208
|
+
row.addEventListener('click', function(e) {
|
|
209
|
+
var actBtn = e.target.closest('.tree-action');
|
|
210
|
+
if (!actBtn) return;
|
|
211
|
+
e.stopPropagation();
|
|
212
|
+
var childContainer = row.nextElementSibling;
|
|
213
|
+
if (!childContainer || !childContainer.classList.contains('tree-children')) return;
|
|
214
|
+
var opening = actBtn.dataset.act === 'expand';
|
|
215
|
+
if (opening) { childContainer.classList.add('open'); toggleEl.classList.add('open'); }
|
|
216
|
+
else { childContainer.classList.remove('open'); toggleEl.classList.remove('open'); }
|
|
217
|
+
expandedPaths[nodePath] = opening;
|
|
218
|
+
var maxDepth = opening ? 4 : Infinity;
|
|
219
|
+
function toggleNested(container, level) {
|
|
220
|
+
if (level >= maxDepth) return;
|
|
221
|
+
var children = container.children;
|
|
222
|
+
for (var j = 0; j < children.length; j++) {
|
|
223
|
+
var nested = children[j].querySelector(':scope > .tree-children');
|
|
224
|
+
if (nested) {
|
|
225
|
+
if (opening) nested.classList.add('open');
|
|
226
|
+
else nested.classList.remove('open');
|
|
227
|
+
var arrow = nested.previousElementSibling ? nested.previousElementSibling.querySelector('.tree-toggle') : null;
|
|
228
|
+
if (arrow) { if (opening) arrow.classList.add('open'); else arrow.classList.remove('open'); }
|
|
229
|
+
toggleNested(nested, level + 1);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
toggleNested(childContainer, 0);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Row click — select element (not toggle)
|
|
237
|
+
row.addEventListener('click', function(e) {
|
|
238
|
+
// Don't select when clicking toggle arrow, badge, action buttons, or peek
|
|
239
|
+
if (e.target.closest('.tree-toggle') || e.target.closest('.tree-badge') || e.target.closest('.tree-action') || e.target.closest('.tree-peek')) return;
|
|
240
|
+
document.querySelectorAll('.tree-row.selected').forEach(function(r) { r.classList.remove('selected'); });
|
|
241
|
+
row.classList.add('selected');
|
|
242
|
+
selectedEl = node;
|
|
243
|
+
showDetail(node);
|
|
244
|
+
try { highlightElement(node); } catch(err) {}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Double-click to expand/collapse
|
|
248
|
+
row.addEventListener('dblclick', function(e) {
|
|
249
|
+
if (!hasChildren) return;
|
|
250
|
+
e.preventDefault();
|
|
251
|
+
var children = row.nextElementSibling;
|
|
252
|
+
if (children && children.classList.contains('tree-children')) {
|
|
253
|
+
var opening = !children.classList.contains('open');
|
|
254
|
+
children.classList.toggle('open');
|
|
255
|
+
toggleEl.classList.toggle('open');
|
|
256
|
+
expandedPaths[nodePath] = opening;
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
return wrap;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ===================================================================
|
|
264
|
+
// Element highlighting
|
|
265
|
+
// ===================================================================
|
|
266
|
+
function highlightElement(el) {
|
|
267
|
+
if (!targetWin || !targetDoc) return;
|
|
268
|
+
// Remove previous highlight
|
|
269
|
+
var prev = targetDoc.getElementById('__zq_highlight');
|
|
270
|
+
if (prev) prev.remove();
|
|
271
|
+
|
|
272
|
+
var rect = el.getBoundingClientRect();
|
|
273
|
+
if (!rect.width && !rect.height) return;
|
|
274
|
+
|
|
275
|
+
var box = targetDoc.createElement('div');
|
|
276
|
+
box.id = '__zq_highlight';
|
|
277
|
+
box.style.cssText = 'position:fixed;z-index:2147483645;pointer-events:none;' +
|
|
278
|
+
'border:2px solid rgba(88,166,255,0.8);background:rgba(88,166,255,0.1);' +
|
|
279
|
+
'transition:all .15s ease;' +
|
|
280
|
+
'top:' + rect.top + 'px;left:' + rect.left + 'px;' +
|
|
281
|
+
'width:' + rect.width + 'px;height:' + rect.height + 'px;';
|
|
282
|
+
targetDoc.body.appendChild(box);
|
|
283
|
+
setTimeout(function() { if (box.parentNode) box.remove(); }, 2000);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ===================================================================
|
|
287
|
+
// Detail panel
|
|
288
|
+
// ===================================================================
|
|
289
|
+
function showDetail(node) {
|
|
290
|
+
var detail = document.getElementById('dom-detail');
|
|
291
|
+
detail.style.display = 'block';
|
|
292
|
+
var html = '<button class="detail-close" id="detail-close" title="Close">×</button>';
|
|
293
|
+
|
|
294
|
+
// Tag and ID
|
|
295
|
+
html += '<div class="detail-section"><h4>Element</h4>';
|
|
296
|
+
html += '<div class="detail-row"><span class="detail-key">Tag</span><span class="detail-val">' + node.tagName.toLowerCase() + '</span></div>';
|
|
297
|
+
if (node.id) html += '<div class="detail-row"><span class="detail-key">ID</span><span class="detail-val">' + esc(node.id) + '</span></div>';
|
|
298
|
+
if (node.className) html += '<div class="detail-row"><span class="detail-key">Classes</span><span class="detail-val">' + esc(node.className) + '</span></div>';
|
|
299
|
+
html += '</div>';
|
|
300
|
+
|
|
301
|
+
// Attributes
|
|
302
|
+
if (node.attributes.length) {
|
|
303
|
+
html += '<div class="detail-section"><h4>Attributes</h4>';
|
|
304
|
+
for (var i = 0; i < node.attributes.length; i++) {
|
|
305
|
+
var a = node.attributes[i];
|
|
306
|
+
var val = esc(a.value);
|
|
307
|
+
if (/^(src|href|action|data-src|poster|srcset)$/i.test(a.name) && a.value) {
|
|
308
|
+
val = '<a href="' + esc(a.value) + '" target="_blank" rel="noopener" title="Open in new tab">' + val + '</a>';
|
|
309
|
+
}
|
|
310
|
+
html += '<div class="detail-row"><span class="detail-key">' + esc(a.name) + '</span><span class="detail-val">' + val + '</span></div>';
|
|
311
|
+
}
|
|
312
|
+
html += '</div>';
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Dimensions
|
|
316
|
+
var rect = node.getBoundingClientRect();
|
|
317
|
+
html += '<div class="detail-section"><h4>Box Model</h4>';
|
|
318
|
+
html += '<div class="detail-row"><span class="detail-key">Size</span><span class="detail-val">' + Math.round(rect.width) + ' \u00d7 ' + Math.round(rect.height) + '</span></div>';
|
|
319
|
+
html += '<div class="detail-row"><span class="detail-key">Position</span><span class="detail-val">(' + Math.round(rect.left) + ', ' + Math.round(rect.top) + ')</span></div>';
|
|
320
|
+
html += '</div>';
|
|
321
|
+
|
|
322
|
+
// Component state
|
|
323
|
+
try {
|
|
324
|
+
if (targetWin.$ && targetWin.$.getInstance) {
|
|
325
|
+
var inst = targetWin.$.getInstance(node);
|
|
326
|
+
if (inst && inst.state) {
|
|
327
|
+
html += '<div class="detail-section"><h4>Component State</h4>';
|
|
328
|
+
var stateKeys = Object.keys(inst.state);
|
|
329
|
+
for (var i = 0; i < stateKeys.length; i++) {
|
|
330
|
+
var k = stateKeys[i];
|
|
331
|
+
var v = inst.state[k];
|
|
332
|
+
var display = typeof v === 'object' ? JSON.stringify(v, null, 1) : String(v);
|
|
333
|
+
html += '<div class="detail-row"><span class="detail-key">' + esc(k) + '</span><span class="detail-val">' + esc(display) + '</span></div>';
|
|
334
|
+
}
|
|
335
|
+
html += '</div>';
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
} catch(err) {}
|
|
339
|
+
|
|
340
|
+
detail.innerHTML = html;
|
|
341
|
+
document.getElementById('detail-close').addEventListener('click', function() {
|
|
342
|
+
detail.style.display = 'none';
|
|
343
|
+
document.querySelectorAll('.tree-row.selected').forEach(function(r) { r.classList.remove('selected'); });
|
|
344
|
+
selectedEl = null;
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ===================================================================
|
|
349
|
+
// MutationObserver — watch target document for live DOM changes
|
|
350
|
+
// ===================================================================
|
|
351
|
+
function startObserver() {
|
|
352
|
+
if (!targetDoc || observer) return;
|
|
353
|
+
observer = new MutationObserver(function(mutations) {
|
|
354
|
+
// Collect paths of mutated nodes so we can auto-expand + highlight them
|
|
355
|
+
var dominated = false;
|
|
356
|
+
for (var i = 0; i < mutations.length; i++) {
|
|
357
|
+
var m = mutations[i];
|
|
358
|
+
var target = m.target.nodeType === 1 ? m.target : m.target.parentElement;
|
|
359
|
+
if (!target) continue;
|
|
360
|
+
// Skip mutations caused by the devtools highlight overlay
|
|
361
|
+
if (target.id === '__zq_highlight' || target.id === '__zq_error_overlay' || target.id === '__zq_devbar') continue;
|
|
362
|
+
var isHighlightMutation = false;
|
|
363
|
+
if (m.addedNodes) { for (var k = 0; k < m.addedNodes.length; k++) { if (m.addedNodes[k].id === '__zq_highlight') { isHighlightMutation = true; break; } } }
|
|
364
|
+
if (!isHighlightMutation && m.removedNodes) { for (var k = 0; k < m.removedNodes.length; k++) { if (m.removedNodes[k].id === '__zq_highlight') { isHighlightMutation = true; break; } } }
|
|
365
|
+
if (isHighlightMutation) continue;
|
|
366
|
+
dominated = true;
|
|
367
|
+
var tp = getNodePath(target);
|
|
368
|
+
changedPaths[tp] = true;
|
|
369
|
+
mutatedPaths[tp] = true;
|
|
370
|
+
// Also mark added nodes
|
|
371
|
+
if (m.addedNodes) {
|
|
372
|
+
for (var j = 0; j < m.addedNodes.length; j++) {
|
|
373
|
+
if (m.addedNodes[j].nodeType === 1) {
|
|
374
|
+
var ap = getNodePath(m.addedNodes[j]);
|
|
375
|
+
changedPaths[ap] = true;
|
|
376
|
+
mutatedPaths[ap] = true;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
// Mark ancestor paths so they auto-expand to reveal changes
|
|
381
|
+
var cur = target.parentElement;
|
|
382
|
+
while (cur && cur.nodeType === 1) {
|
|
383
|
+
var p = getNodePath(cur);
|
|
384
|
+
if (!expandedPaths.hasOwnProperty(p)) changedPaths[p] = true;
|
|
385
|
+
cur = cur.parentElement;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (!dominated) return; // All mutations were from devtools highlight — skip rebuild
|
|
390
|
+
|
|
391
|
+
// Debounce tree rebuild
|
|
392
|
+
clearTimeout(startObserver._timer);
|
|
393
|
+
startObserver._timer = setTimeout(function() {
|
|
394
|
+
buildDOMTree();
|
|
395
|
+
updateStats();
|
|
396
|
+
|
|
397
|
+
// Scroll to deepest (last in DOM order) changed row
|
|
398
|
+
var allChanged = document.querySelectorAll('.tree-row[data-changed]');
|
|
399
|
+
if (allChanged.length) {
|
|
400
|
+
allChanged[allChanged.length - 1].scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
401
|
+
}
|
|
402
|
+
// Clear changed paths after applying
|
|
403
|
+
changedPaths = {};
|
|
404
|
+
mutatedPaths = {};
|
|
405
|
+
}, 150);
|
|
406
|
+
});
|
|
407
|
+
observer.observe(targetDoc.documentElement, {
|
|
408
|
+
childList: true,
|
|
409
|
+
subtree: true,
|
|
410
|
+
attributes: true,
|
|
411
|
+
characterData: true
|
|
412
|
+
});
|
|
413
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// ===================================================================
|
|
2
|
+
// Network log
|
|
3
|
+
// ===================================================================
|
|
4
|
+
function renderNetwork() {
|
|
5
|
+
var tbody = document.getElementById('net-body');
|
|
6
|
+
var html = '';
|
|
7
|
+
for (var i = requests.length - 1; i >= 0; i--) {
|
|
8
|
+
var r = requests[i];
|
|
9
|
+
var statusCls = r.status < 300 ? 'ok' : r.status < 400 ? 'redirect' : 'err';
|
|
10
|
+
var preview = '';
|
|
11
|
+
if (r.bodyPreview) {
|
|
12
|
+
try { preview = JSON.stringify(JSON.parse(r.bodyPreview), null, 0); } catch(e) { preview = r.bodyPreview; }
|
|
13
|
+
if (preview.length > 120) preview = preview.slice(0, 120) + '...';
|
|
14
|
+
}
|
|
15
|
+
html += '<tr data-idx="' + i + '">';
|
|
16
|
+
html += '<td><span class="net-method ' + r.method + '">' + r.method + '</span></td>';
|
|
17
|
+
html += '<td><span class="net-status ' + statusCls + '">' + r.status + '</span></td>';
|
|
18
|
+
html += '<td class="net-url" title="' + esc(r.url) + '">' + esc(r.url) + '</td>';
|
|
19
|
+
html += '<td class="net-time">' + r.elapsed + 'ms</td>';
|
|
20
|
+
html += '<td class="timestamp">' + formatTime(r.timestamp) + '</td>';
|
|
21
|
+
html += '<td style="color:var(--text2);font-size:10px;max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc(preview) + '</td>';
|
|
22
|
+
html += '</tr>';
|
|
23
|
+
}
|
|
24
|
+
if (!html) html = '<tr><td colspan="6" style="text-align:center;padding:24px;color:var(--text2)">No requests yet</td></tr>';
|
|
25
|
+
tbody.innerHTML = html;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Click to expand/collapse (attached once, outside renderNetwork)
|
|
29
|
+
document.getElementById('net-body').addEventListener('click', function(e) {
|
|
30
|
+
var tr = e.target.closest('tr');
|
|
31
|
+
if (!tr || !tr.dataset.idx) return;
|
|
32
|
+
var idx = parseInt(tr.dataset.idx);
|
|
33
|
+
var existing = tr.nextElementSibling;
|
|
34
|
+
if (existing && existing.classList.contains('net-body-row')) {
|
|
35
|
+
existing.remove();
|
|
36
|
+
tr.classList.remove('expanded');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
tr.classList.add('expanded');
|
|
40
|
+
var r = requests[idx];
|
|
41
|
+
if (!r) return;
|
|
42
|
+
var detail = document.createElement('tr');
|
|
43
|
+
detail.className = 'net-body-row';
|
|
44
|
+
var td = document.createElement('td');
|
|
45
|
+
td.colSpan = 6;
|
|
46
|
+
var body = r.bodyPreview || '';
|
|
47
|
+
var parsed = null;
|
|
48
|
+
try { parsed = JSON.parse(body); } catch(e2) {}
|
|
49
|
+
if (parsed !== null && typeof parsed === 'object') {
|
|
50
|
+
var treeWrap = document.createElement('div');
|
|
51
|
+
treeWrap.className = 'json-tree';
|
|
52
|
+
treeWrap.appendChild(buildJsonNode(parsed, true));
|
|
53
|
+
td.appendChild(treeWrap);
|
|
54
|
+
} else {
|
|
55
|
+
var pre = document.createElement('pre');
|
|
56
|
+
pre.style.cssText = 'margin:0;max-height:240px;overflow:auto';
|
|
57
|
+
pre.textContent = body;
|
|
58
|
+
td.appendChild(pre);
|
|
59
|
+
}
|
|
60
|
+
detail.appendChild(td);
|
|
61
|
+
tr.after(detail);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
function buildJsonNode(val, isRoot) {
|
|
65
|
+
var frag = document.createDocumentFragment();
|
|
66
|
+
if (val === null) {
|
|
67
|
+
var s = document.createElement('span');
|
|
68
|
+
s.className = 'json-null';
|
|
69
|
+
s.textContent = 'null';
|
|
70
|
+
frag.appendChild(s);
|
|
71
|
+
return frag;
|
|
72
|
+
}
|
|
73
|
+
if (typeof val === 'string') {
|
|
74
|
+
var s = document.createElement('span');
|
|
75
|
+
s.className = 'json-str';
|
|
76
|
+
s.textContent = '"' + (val.length > 300 ? val.slice(0, 300) + '...' : val) + '"';
|
|
77
|
+
frag.appendChild(s);
|
|
78
|
+
return frag;
|
|
79
|
+
}
|
|
80
|
+
if (typeof val === 'number') {
|
|
81
|
+
var s = document.createElement('span');
|
|
82
|
+
s.className = 'json-num';
|
|
83
|
+
s.textContent = String(val);
|
|
84
|
+
frag.appendChild(s);
|
|
85
|
+
return frag;
|
|
86
|
+
}
|
|
87
|
+
if (typeof val === 'boolean') {
|
|
88
|
+
var s = document.createElement('span');
|
|
89
|
+
s.className = 'json-bool';
|
|
90
|
+
s.textContent = String(val);
|
|
91
|
+
frag.appendChild(s);
|
|
92
|
+
return frag;
|
|
93
|
+
}
|
|
94
|
+
var isArr = Array.isArray(val);
|
|
95
|
+
var keys = isArr ? val : Object.keys(val);
|
|
96
|
+
var len = isArr ? val.length : keys.length;
|
|
97
|
+
var open = isArr ? '[' : '{';
|
|
98
|
+
var close = isArr ? ']' : '}';
|
|
99
|
+
|
|
100
|
+
// Toggle arrow + opening bracket
|
|
101
|
+
var toggle = document.createElement('span');
|
|
102
|
+
toggle.className = 'json-toggle';
|
|
103
|
+
toggle.textContent = '\u25BE ';
|
|
104
|
+
frag.appendChild(toggle);
|
|
105
|
+
|
|
106
|
+
var bracket = document.createElement('span');
|
|
107
|
+
bracket.className = 'json-bracket';
|
|
108
|
+
bracket.textContent = open;
|
|
109
|
+
frag.appendChild(bracket);
|
|
110
|
+
|
|
111
|
+
var count = document.createElement('span');
|
|
112
|
+
count.className = 'json-count';
|
|
113
|
+
count.textContent = len + (isArr ? ' items' : ' keys');
|
|
114
|
+
count.style.display = 'none';
|
|
115
|
+
frag.appendChild(count);
|
|
116
|
+
|
|
117
|
+
// Children
|
|
118
|
+
var children = document.createElement('div');
|
|
119
|
+
children.className = 'json-children';
|
|
120
|
+
var entries = isArr ? val : keys;
|
|
121
|
+
for (var i = 0; i < entries.length; i++) {
|
|
122
|
+
var line = document.createElement('div');
|
|
123
|
+
if (!isArr) {
|
|
124
|
+
var k = document.createElement('span');
|
|
125
|
+
k.className = 'json-key';
|
|
126
|
+
k.textContent = '"' + entries[i] + '"';
|
|
127
|
+
line.appendChild(k);
|
|
128
|
+
var colon = document.createTextNode(': ');
|
|
129
|
+
line.appendChild(colon);
|
|
130
|
+
line.appendChild(buildJsonNode(val[entries[i]], false));
|
|
131
|
+
} else {
|
|
132
|
+
line.appendChild(buildJsonNode(entries[i], false));
|
|
133
|
+
}
|
|
134
|
+
if (i < entries.length - 1) {
|
|
135
|
+
var comma = document.createElement('span');
|
|
136
|
+
comma.className = 'json-comma';
|
|
137
|
+
comma.textContent = ',';
|
|
138
|
+
line.appendChild(comma);
|
|
139
|
+
}
|
|
140
|
+
children.appendChild(line);
|
|
141
|
+
}
|
|
142
|
+
frag.appendChild(children);
|
|
143
|
+
|
|
144
|
+
var closeBracket = document.createElement('span');
|
|
145
|
+
closeBracket.className = 'json-bracket';
|
|
146
|
+
closeBracket.textContent = close;
|
|
147
|
+
frag.appendChild(closeBracket);
|
|
148
|
+
|
|
149
|
+
// Collapse large nodes by default (except root)
|
|
150
|
+
if (!isRoot && len > 5) {
|
|
151
|
+
children.classList.add('collapsed');
|
|
152
|
+
toggle.textContent = '\u25B8 ';
|
|
153
|
+
count.style.display = '';
|
|
154
|
+
closeBracket.style.display = 'none';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
toggle.addEventListener('click', function() {
|
|
158
|
+
var collapsed = children.classList.contains('collapsed');
|
|
159
|
+
children.classList.toggle('collapsed');
|
|
160
|
+
toggle.textContent = collapsed ? '\u25BE ' : '\u25B8 ';
|
|
161
|
+
count.style.display = collapsed ? 'none' : '';
|
|
162
|
+
closeBracket.style.display = collapsed ? '' : 'none';
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
return frag;
|
|
166
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// ===================================================================
|
|
2
|
+
// Performance
|
|
3
|
+
// ===================================================================
|
|
4
|
+
function recordMorphEvent(data) {
|
|
5
|
+
if (!data.timestamp) data.timestamp = Date.now();
|
|
6
|
+
morphEvents.push(data);
|
|
7
|
+
if (morphEvents.length > 200) morphEvents.shift();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function renderPerf() {
|
|
11
|
+
var content = document.getElementById('perf-content');
|
|
12
|
+
var html = '';
|
|
13
|
+
|
|
14
|
+
// Summary
|
|
15
|
+
html += '<div class="perf-card">';
|
|
16
|
+
html += '<div class="perf-title">Render Operations</div>';
|
|
17
|
+
html += '<div class="detail-row"><span class="detail-key">Total</span><span class="detail-val">' + morphCount + '</span></div>';
|
|
18
|
+
|
|
19
|
+
if (morphEvents.length) {
|
|
20
|
+
var times = morphEvents.map(function(e) { return e.elapsed || 0; });
|
|
21
|
+
var avg = times.reduce(function(a, b) { return a + b; }, 0) / times.length;
|
|
22
|
+
var max = Math.max.apply(null, times);
|
|
23
|
+
var min = Math.min.apply(null, times);
|
|
24
|
+
html += '<div class="detail-row"><span class="detail-key">Avg time</span><span class="detail-val">' + avg.toFixed(2) + 'ms</span></div>';
|
|
25
|
+
html += '<div class="detail-row"><span class="detail-key">Min / Max</span><span class="detail-val">' + min.toFixed(2) + 'ms / ' + max.toFixed(2) + 'ms</span></div>';
|
|
26
|
+
}
|
|
27
|
+
html += '</div>';
|
|
28
|
+
|
|
29
|
+
// DOM stats
|
|
30
|
+
if (targetDoc) {
|
|
31
|
+
var allEls = targetDoc.querySelectorAll('*').length;
|
|
32
|
+
html += '<div class="perf-card">';
|
|
33
|
+
html += '<div class="perf-title">DOM Statistics</div>';
|
|
34
|
+
html += '<div class="detail-row"><span class="detail-key">Elements</span><span class="detail-val">' + allEls + '</span></div>';
|
|
35
|
+
html += '<div class="detail-row"><span class="detail-key">Tree depth</span><span class="detail-val">' + measureDepth(targetDoc.body) + '</span></div>';
|
|
36
|
+
|
|
37
|
+
// Component count
|
|
38
|
+
if (targetWin.$ && typeof targetWin.$.components === 'function') {
|
|
39
|
+
html += '<div class="detail-row"><span class="detail-key">Components</span><span class="detail-val">' + Object.keys(targetWin.$.components()).length + ' registered</span></div>';
|
|
40
|
+
}
|
|
41
|
+
html += '</div>';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Recent morph events
|
|
45
|
+
if (morphEvents.length) {
|
|
46
|
+
html += '<div class="perf-card">';
|
|
47
|
+
html += '<div class="perf-title">Recent Renders</div>';
|
|
48
|
+
for (var i = morphEvents.length - 1; i >= Math.max(0, morphEvents.length - 20); i--) {
|
|
49
|
+
var ev = morphEvents[i];
|
|
50
|
+
var kind = ev.kind || 'morph';
|
|
51
|
+
var badge = kind === 'route' ? '<span style="color:#f5c542;font-weight:600;margin-right:6px">[route]</span>'
|
|
52
|
+
: kind === 'mount' ? '<span style="color:#66d9ef;font-weight:600;margin-right:6px">[mount]</span>'
|
|
53
|
+
: '<span style="color:#c792ea;font-weight:600;margin-right:6px">[morph]</span>';
|
|
54
|
+
var pct = Math.min(100, (ev.elapsed / 16.67) * 100);
|
|
55
|
+
var color = pct < 50 ? 'var(--green)' : pct < 100 ? 'var(--yellow)' : 'var(--red)';
|
|
56
|
+
html += '<div class="perf-label"><span>' + badge + esc(ev.target || '?') + '</span><span class="timestamp">' + formatTime(ev.timestamp) + '</span><span style="min-width:60px;text-align:right">' + (ev.elapsed || 0).toFixed(2) + 'ms</span></div>';
|
|
57
|
+
html += '<div class="perf-bar"><div class="perf-fill" style="width:' + Math.max(2, pct) + '%;background:' + color + '"></div></div>';
|
|
58
|
+
}
|
|
59
|
+
html += '</div>';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
content.innerHTML = html;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function measureDepth(el) {
|
|
66
|
+
if (!el || !el.children || !el.children.length) return 0;
|
|
67
|
+
var max = 0;
|
|
68
|
+
for (var i = 0; i < el.children.length; i++) {
|
|
69
|
+
var d = measureDepth(el.children[i]);
|
|
70
|
+
if (d > max) max = d;
|
|
71
|
+
}
|
|
72
|
+
return max + 1;
|
|
73
|
+
}
|