@data-navigator/inspector 1.0.3 → 1.1.0
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/package.json +4 -2
- package/src/console-menu.js +85 -0
- package/src/force-graph.js +4 -2
- package/src/inspector.js +56 -9
- package/src/menu-events.js +26 -0
- package/src/menu-sections.js +751 -0
- package/src/menu-state.js +89 -0
- package/src/svg-highlight.js +457 -0
- package/src/tree-graph.js +4 -1
- package/style.css +295 -0
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builders for each section of the console menu.
|
|
3
|
+
* Each function returns an HTMLElement to be appended to the menu.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { EVENTS, dispatch } from './menu-events.js';
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Utility: collapsible section with caret indicator
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a <details> with a compact caret-styled <summary>.
|
|
14
|
+
*/
|
|
15
|
+
function makeDetails(summaryText, opts = {}) {
|
|
16
|
+
const details = document.createElement('details');
|
|
17
|
+
if (opts.className) details.className = opts.className;
|
|
18
|
+
if (opts.open) details.open = true;
|
|
19
|
+
|
|
20
|
+
const summary = document.createElement('summary');
|
|
21
|
+
summary.className = 'dn-menu-summary' + (opts.summaryClass ? ' ' + opts.summaryClass : '');
|
|
22
|
+
summary.textContent = summaryText;
|
|
23
|
+
details.appendChild(summary);
|
|
24
|
+
|
|
25
|
+
return details;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create a <details> with a group checkbox in the <summary>.
|
|
30
|
+
* The checkbox checks/unchecks all child items and syncs bidirectionally.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} summaryText - Label text for the summary
|
|
33
|
+
* @param {Array<{type: string, id: string}>} childItems - Items this group controls
|
|
34
|
+
* @param {Object} state - The menu state object
|
|
35
|
+
* @param {HTMLElement} container - Container for event dispatching
|
|
36
|
+
* @param {Object} [opts] - Options passed to details (className, open, summaryClass)
|
|
37
|
+
* @returns {HTMLDetailsElement}
|
|
38
|
+
*/
|
|
39
|
+
function makeGroupDetails(summaryText, childItems, state, container, opts = {}) {
|
|
40
|
+
const details = document.createElement('details');
|
|
41
|
+
if (opts.className) details.className = opts.className;
|
|
42
|
+
if (opts.open) details.open = true;
|
|
43
|
+
|
|
44
|
+
const summary = document.createElement('summary');
|
|
45
|
+
summary.className = 'dn-menu-summary' + (opts.summaryClass ? ' ' + opts.summaryClass : '');
|
|
46
|
+
|
|
47
|
+
const checkbox = document.createElement('input');
|
|
48
|
+
checkbox.type = 'checkbox';
|
|
49
|
+
checkbox.className = 'dn-menu-group-checkbox';
|
|
50
|
+
checkbox.addEventListener('click', (e) => {
|
|
51
|
+
e.stopPropagation(); // Don't toggle <details> when clicking checkbox
|
|
52
|
+
});
|
|
53
|
+
checkbox.addEventListener('change', () => {
|
|
54
|
+
const shouldCheck = checkbox.checked;
|
|
55
|
+
childItems.forEach(item => {
|
|
56
|
+
if (shouldCheck) {
|
|
57
|
+
state.check(item.type, item.id);
|
|
58
|
+
} else {
|
|
59
|
+
state.uncheck(item.type, item.id);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
const allChecked = state.getChecked();
|
|
63
|
+
dispatch(container, EVENTS.SELECTION_CHANGE, { checked: allChecked });
|
|
64
|
+
});
|
|
65
|
+
summary.appendChild(checkbox);
|
|
66
|
+
|
|
67
|
+
const labelSpan = document.createElement('span');
|
|
68
|
+
labelSpan.textContent = summaryText;
|
|
69
|
+
summary.appendChild(labelSpan);
|
|
70
|
+
|
|
71
|
+
details.appendChild(summary);
|
|
72
|
+
|
|
73
|
+
// Sync checkbox state when child items change
|
|
74
|
+
function syncGroupCheckbox() {
|
|
75
|
+
const checkedCount = childItems.filter(item => state.isChecked(item.type, item.id)).length;
|
|
76
|
+
checkbox.checked = checkedCount === childItems.length && childItems.length > 0;
|
|
77
|
+
checkbox.indeterminate = checkedCount > 0 && checkedCount < childItems.length;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const unsub = state.subscribe((changeType) => {
|
|
81
|
+
if (changeType === 'check' || changeType === 'uncheck') {
|
|
82
|
+
syncGroupCheckbox();
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
details._unsub = unsub;
|
|
86
|
+
|
|
87
|
+
return details;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Utility: inline expandable array "label: [...](N)"
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Create an inline expandable array display.
|
|
96
|
+
* Collapsed: "label: [...](N)"
|
|
97
|
+
* Expanded: "label: [item1, item2, item3]" (inline, no new lines)
|
|
98
|
+
* Each item is interactive (hoverable via state).
|
|
99
|
+
*/
|
|
100
|
+
function buildInlineArray({ label, items, state, container, structure }) {
|
|
101
|
+
const wrapper = document.createElement('div');
|
|
102
|
+
wrapper.className = 'dn-menu-inline-array';
|
|
103
|
+
|
|
104
|
+
const toggle = document.createElement('span');
|
|
105
|
+
toggle.className = 'dn-menu-array-toggle';
|
|
106
|
+
toggle.textContent = label + ': ';
|
|
107
|
+
wrapper.appendChild(toggle);
|
|
108
|
+
|
|
109
|
+
const collapsed = document.createElement('span');
|
|
110
|
+
collapsed.className = 'dn-menu-array-collapsed';
|
|
111
|
+
collapsed.textContent = '[...](' + items.length + ')';
|
|
112
|
+
wrapper.appendChild(collapsed);
|
|
113
|
+
|
|
114
|
+
const expanded = document.createElement('span');
|
|
115
|
+
expanded.className = 'dn-menu-array-expanded dn-menu-hidden';
|
|
116
|
+
expanded.textContent = '[';
|
|
117
|
+
|
|
118
|
+
items.forEach((item, i) => {
|
|
119
|
+
const chip = document.createElement('span');
|
|
120
|
+
chip.className = 'dn-menu-array-chip';
|
|
121
|
+
chip.textContent = item.label;
|
|
122
|
+
chip.dataset.type = item.type;
|
|
123
|
+
chip.dataset.id = item.id;
|
|
124
|
+
|
|
125
|
+
chip.addEventListener('mouseenter', () => {
|
|
126
|
+
state.setHover(item.type, item.id);
|
|
127
|
+
dispatch(container, EVENTS.ITEM_HOVER, {
|
|
128
|
+
type: item.type, id: item.id,
|
|
129
|
+
sourceData: getSourceData(item.type, item.id, structure)
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
chip.addEventListener('mouseleave', () => {
|
|
133
|
+
state.clearHover();
|
|
134
|
+
dispatch(container, EVENTS.ITEM_UNHOVER, { type: item.type, id: item.id });
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
expanded.appendChild(chip);
|
|
138
|
+
if (i < items.length - 1) {
|
|
139
|
+
expanded.appendChild(document.createTextNode(', '));
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expanded.appendChild(document.createTextNode(']'));
|
|
144
|
+
wrapper.appendChild(expanded);
|
|
145
|
+
|
|
146
|
+
const toggleFn = () => {
|
|
147
|
+
const isCollapsed = !collapsed.classList.contains('dn-menu-hidden');
|
|
148
|
+
collapsed.classList.toggle('dn-menu-hidden', isCollapsed);
|
|
149
|
+
expanded.classList.toggle('dn-menu-hidden', !isCollapsed);
|
|
150
|
+
};
|
|
151
|
+
collapsed.addEventListener('click', toggleFn);
|
|
152
|
+
toggle.addEventListener('click', toggleFn);
|
|
153
|
+
toggle.style.cursor = 'pointer';
|
|
154
|
+
collapsed.style.cursor = 'pointer';
|
|
155
|
+
|
|
156
|
+
return wrapper;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Shared helper: buildMenuItem
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Creates a menu item row with checkbox, label, and optional "log" button.
|
|
165
|
+
*/
|
|
166
|
+
export function buildMenuItem({ type, id, label, state, container, showLog, logFn, consoleListEl, structure }) {
|
|
167
|
+
const row = document.createElement('div');
|
|
168
|
+
row.className = 'dn-menu-item';
|
|
169
|
+
row.dataset.type = type;
|
|
170
|
+
row.dataset.id = id;
|
|
171
|
+
|
|
172
|
+
const checkbox = document.createElement('input');
|
|
173
|
+
checkbox.type = 'checkbox';
|
|
174
|
+
checkbox.checked = state.isChecked(type, id);
|
|
175
|
+
checkbox.addEventListener('change', () => {
|
|
176
|
+
state.toggle(type, id);
|
|
177
|
+
const allChecked = state.getChecked();
|
|
178
|
+
if (state.isChecked(type, id)) {
|
|
179
|
+
dispatch(container, EVENTS.ITEM_CHECK, { type, id, allChecked });
|
|
180
|
+
} else {
|
|
181
|
+
dispatch(container, EVENTS.ITEM_UNCHECK, { type, id, allChecked });
|
|
182
|
+
}
|
|
183
|
+
dispatch(container, EVENTS.SELECTION_CHANGE, { checked: allChecked });
|
|
184
|
+
});
|
|
185
|
+
row.appendChild(checkbox);
|
|
186
|
+
|
|
187
|
+
const labelSpan = document.createElement('span');
|
|
188
|
+
labelSpan.className = 'dn-menu-item-label';
|
|
189
|
+
labelSpan.textContent = label;
|
|
190
|
+
row.appendChild(labelSpan);
|
|
191
|
+
|
|
192
|
+
if (showLog && logFn) {
|
|
193
|
+
const logBtn = document.createElement('button');
|
|
194
|
+
logBtn.className = 'dn-menu-log-btn';
|
|
195
|
+
logBtn.textContent = 'log';
|
|
196
|
+
logBtn.addEventListener('click', (e) => {
|
|
197
|
+
e.stopPropagation();
|
|
198
|
+
const result = logFn();
|
|
199
|
+
if (result && consoleListEl) {
|
|
200
|
+
// Open the Console section and all ancestor <details> so it's visible
|
|
201
|
+
let el = consoleListEl;
|
|
202
|
+
while (el) {
|
|
203
|
+
if (el.tagName === 'DETAILS') el.open = true;
|
|
204
|
+
el = el.parentElement;
|
|
205
|
+
}
|
|
206
|
+
appendConsoleLogGroup(result, state, container, consoleListEl, structure);
|
|
207
|
+
dispatch(container, EVENTS.ITEM_LOG, {
|
|
208
|
+
type: result.mainEntry.type,
|
|
209
|
+
id: result.mainEntry.id,
|
|
210
|
+
loggedData: result.mainEntry.data
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
row.appendChild(logBtn);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
row.addEventListener('mouseenter', () => {
|
|
218
|
+
state.setHover(type, id);
|
|
219
|
+
dispatch(container, EVENTS.ITEM_HOVER, {
|
|
220
|
+
type, id, sourceData: getSourceData(type, id, structure)
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
row.addEventListener('mouseleave', () => {
|
|
224
|
+
state.clearHover();
|
|
225
|
+
dispatch(container, EVENTS.ITEM_UNHOVER, { type, id });
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const unsub = state.subscribe((changeType, payload) => {
|
|
229
|
+
if ((changeType === 'check' || changeType === 'uncheck') &&
|
|
230
|
+
payload.type === type && payload.id === id) {
|
|
231
|
+
checkbox.checked = state.isChecked(type, id);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
row._unsub = unsub;
|
|
235
|
+
|
|
236
|
+
return row;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function getSourceData(type, id, structure) {
|
|
240
|
+
if (!structure) return null;
|
|
241
|
+
if (type === 'node') return structure.nodes[id] || null;
|
|
242
|
+
if (type === 'edge') return structure.edges[id] || null;
|
|
243
|
+
if (type === 'rule') return structure.navigationRules?.[id] || null;
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
// Console log rendering (with inline expandable arrays)
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
function appendConsoleLogGroup(result, state, container, consoleListEl, structure) {
|
|
252
|
+
const { mainEntry, relatedArrays } = result;
|
|
253
|
+
|
|
254
|
+
const mainRow = buildMenuItem({
|
|
255
|
+
type: mainEntry.type,
|
|
256
|
+
id: mainEntry.id,
|
|
257
|
+
label: mainEntry.label,
|
|
258
|
+
state, container, showLog: false, structure
|
|
259
|
+
});
|
|
260
|
+
mainRow.classList.add('dn-menu-console-entry');
|
|
261
|
+
consoleListEl.appendChild(mainRow);
|
|
262
|
+
|
|
263
|
+
state.addLogEntry(mainEntry);
|
|
264
|
+
|
|
265
|
+
if (relatedArrays) {
|
|
266
|
+
relatedArrays.forEach(arr => {
|
|
267
|
+
const inlineEl = buildInlineArray({
|
|
268
|
+
label: arr.label,
|
|
269
|
+
items: arr.items,
|
|
270
|
+
state, container, structure
|
|
271
|
+
});
|
|
272
|
+
inlineEl.classList.add('dn-menu-console-entry');
|
|
273
|
+
inlineEl.style.paddingLeft = '24px';
|
|
274
|
+
consoleListEl.appendChild(inlineEl);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
consoleListEl.scrollTop = consoleListEl.scrollHeight;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
// Log functions: return { mainEntry, relatedArrays }
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
function buildNodeLogResult(nodeId, structure) {
|
|
286
|
+
const node = structure.nodes[nodeId];
|
|
287
|
+
if (!node) return null;
|
|
288
|
+
|
|
289
|
+
const edgeItems = (node.edges || [])
|
|
290
|
+
.map(eId => {
|
|
291
|
+
const edge = structure.edges[eId];
|
|
292
|
+
if (!edge) return null;
|
|
293
|
+
const src = typeof edge.source === 'function' ? '?' : edge.source;
|
|
294
|
+
const tgt = typeof edge.target === 'function' ? '?' : edge.target;
|
|
295
|
+
return { type: 'edge', id: eId, label: src + '\u2192' + tgt };
|
|
296
|
+
})
|
|
297
|
+
.filter(Boolean);
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
mainEntry: {
|
|
301
|
+
type: 'node', id: nodeId,
|
|
302
|
+
label: 'node: ' + nodeId,
|
|
303
|
+
data: node, timestamp: Date.now()
|
|
304
|
+
},
|
|
305
|
+
relatedArrays: edgeItems.length > 0 ? [{
|
|
306
|
+
label: 'edges',
|
|
307
|
+
items: edgeItems
|
|
308
|
+
}] : []
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function buildEdgeLogResult(edgeKey, structure) {
|
|
313
|
+
const edge = structure.edges[edgeKey];
|
|
314
|
+
if (!edge) return null;
|
|
315
|
+
|
|
316
|
+
const src = typeof edge.source === 'function' ? null : edge.source;
|
|
317
|
+
const tgt = typeof edge.target === 'function' ? null : edge.target;
|
|
318
|
+
|
|
319
|
+
const relatedArrays = [];
|
|
320
|
+
if (src && structure.nodes[src]) {
|
|
321
|
+
relatedArrays.push({ label: 'source', items: [{ type: 'node', id: src, label: src }] });
|
|
322
|
+
}
|
|
323
|
+
if (tgt && structure.nodes[tgt]) {
|
|
324
|
+
relatedArrays.push({ label: 'target', items: [{ type: 'node', id: tgt, label: tgt }] });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
mainEntry: {
|
|
329
|
+
type: 'edge', id: edgeKey,
|
|
330
|
+
label: 'edge: ' + (src || '?') + ' \u2192 ' + (tgt || '?'),
|
|
331
|
+
data: edge, timestamp: Date.now()
|
|
332
|
+
},
|
|
333
|
+
relatedArrays
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function buildNavRuleLogResult(ruleId, structure) {
|
|
338
|
+
const rule = structure.navigationRules?.[ruleId];
|
|
339
|
+
|
|
340
|
+
const matchingEdgeItems = [];
|
|
341
|
+
const matchingNodeIds = new Set();
|
|
342
|
+
|
|
343
|
+
Object.keys(structure.edges).forEach(edgeKey => {
|
|
344
|
+
const edge = structure.edges[edgeKey];
|
|
345
|
+
if (edge.navigationRules && edge.navigationRules.includes(ruleId)) {
|
|
346
|
+
const src = typeof edge.source === 'function' ? '?' : edge.source;
|
|
347
|
+
const tgt = typeof edge.target === 'function' ? '?' : edge.target;
|
|
348
|
+
matchingEdgeItems.push({ type: 'edge', id: edgeKey, label: src + '\u2192' + tgt });
|
|
349
|
+
if (typeof edge.source === 'string') matchingNodeIds.add(edge.source);
|
|
350
|
+
if (typeof edge.target === 'string') matchingNodeIds.add(edge.target);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
const matchingNodeItems = [...matchingNodeIds]
|
|
355
|
+
.filter(nid => structure.nodes[nid])
|
|
356
|
+
.map(nid => ({ type: 'node', id: nid, label: nid }));
|
|
357
|
+
|
|
358
|
+
const relatedArrays = [];
|
|
359
|
+
if (matchingEdgeItems.length > 0) {
|
|
360
|
+
relatedArrays.push({ label: 'edges', items: matchingEdgeItems });
|
|
361
|
+
}
|
|
362
|
+
if (matchingNodeItems.length > 0) {
|
|
363
|
+
relatedArrays.push({ label: 'nodes', items: matchingNodeItems });
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
mainEntry: {
|
|
368
|
+
type: 'rule', id: ruleId,
|
|
369
|
+
label: 'rule: ' + ruleId,
|
|
370
|
+
data: rule || ruleId, timestamp: Date.now()
|
|
371
|
+
},
|
|
372
|
+
relatedArrays
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ---------------------------------------------------------------------------
|
|
377
|
+
// Helper: build an edge menu item
|
|
378
|
+
// ---------------------------------------------------------------------------
|
|
379
|
+
|
|
380
|
+
function buildEdgeMenuItem(edgeKey, structure, state, container, consoleListEl) {
|
|
381
|
+
const edge = structure.edges[edgeKey];
|
|
382
|
+
const src = typeof edge.source === 'function' ? '(fn)' : edge.source;
|
|
383
|
+
const tgt = typeof edge.target === 'function' ? '(fn)' : edge.target;
|
|
384
|
+
const rules = (edge.navigationRules || []).join(',');
|
|
385
|
+
const label = src + ' \u2192 ' + tgt + (rules ? ' [' + rules + ']' : '');
|
|
386
|
+
return buildMenuItem({
|
|
387
|
+
type: 'edge', id: edgeKey, label,
|
|
388
|
+
state, container, showLog: true,
|
|
389
|
+
logFn: () => buildEdgeLogResult(edgeKey, structure),
|
|
390
|
+
consoleListEl, structure
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ---------------------------------------------------------------------------
|
|
395
|
+
// Section builders
|
|
396
|
+
// ---------------------------------------------------------------------------
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Console section (collapsed by default, sits at top of menu).
|
|
400
|
+
* Includes a "clear" button on the summary row.
|
|
401
|
+
*/
|
|
402
|
+
export function buildConsoleSection() {
|
|
403
|
+
const details = document.createElement('details');
|
|
404
|
+
details.className = 'dn-menu-console';
|
|
405
|
+
|
|
406
|
+
const summary = document.createElement('summary');
|
|
407
|
+
summary.className = 'dn-menu-summary dn-menu-summary-top';
|
|
408
|
+
summary.textContent = 'Console';
|
|
409
|
+
details.appendChild(summary);
|
|
410
|
+
|
|
411
|
+
// Clear button (appended after the summary text, inside summary)
|
|
412
|
+
const clearBtn = document.createElement('button');
|
|
413
|
+
clearBtn.className = 'dn-menu-log-btn dn-menu-clear-btn';
|
|
414
|
+
clearBtn.textContent = 'clear';
|
|
415
|
+
clearBtn.addEventListener('click', (e) => {
|
|
416
|
+
e.stopPropagation();
|
|
417
|
+
e.preventDefault();
|
|
418
|
+
listEl.innerHTML = '';
|
|
419
|
+
});
|
|
420
|
+
summary.appendChild(clearBtn);
|
|
421
|
+
|
|
422
|
+
const listEl = document.createElement('div');
|
|
423
|
+
listEl.className = 'dn-menu-console-list';
|
|
424
|
+
details.appendChild(listEl);
|
|
425
|
+
|
|
426
|
+
return { element: details, listEl };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Rendered Elements section with grouped Nodes and grouped Edges.
|
|
431
|
+
*
|
|
432
|
+
* Nodes are grouped by dimension > division, then "All Nodes" flat list.
|
|
433
|
+
* Edges are grouped by navigation rule, then "All Edges" flat list.
|
|
434
|
+
*/
|
|
435
|
+
export function buildRenderedElementsSection(structure, state, container, consoleListEl, buildLabelFn) {
|
|
436
|
+
const details = makeDetails('Rendered Elements', { summaryClass: 'dn-menu-summary-top' });
|
|
437
|
+
|
|
438
|
+
// --- Nodes ---
|
|
439
|
+
const allNodeKeys = Object.keys(structure.nodes || {});
|
|
440
|
+
const nodesDetails = makeDetails('Nodes (' + allNodeKeys.length + ')');
|
|
441
|
+
|
|
442
|
+
// Group nodes by dimension > division
|
|
443
|
+
if (structure.dimensions) {
|
|
444
|
+
const dimKeys = Object.keys(structure.dimensions);
|
|
445
|
+
dimKeys.forEach(dimKey => {
|
|
446
|
+
const dim = structure.dimensions[dimKey];
|
|
447
|
+
const dimNode = structure.nodes[dim.nodeId];
|
|
448
|
+
const divIds = Object.keys(dim.divisions || {});
|
|
449
|
+
|
|
450
|
+
// Collect all node IDs in this dimension for the group checkbox
|
|
451
|
+
const dimChildItems = [];
|
|
452
|
+
if (dimNode) dimChildItems.push({ type: 'node', id: dim.nodeId });
|
|
453
|
+
divIds.forEach(divId => {
|
|
454
|
+
const div = dim.divisions[divId];
|
|
455
|
+
const divNodeId = findDivisionNodeId(structure, dimKey, divId);
|
|
456
|
+
if (divNodeId) dimChildItems.push({ type: 'node', id: divNodeId });
|
|
457
|
+
Object.keys(div.values || {}).forEach(leafId => {
|
|
458
|
+
if (structure.nodes[leafId]) dimChildItems.push({ type: 'node', id: leafId });
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// Dimension-level details with group checkbox
|
|
463
|
+
const dimDetails = makeGroupDetails(
|
|
464
|
+
dimKey + ' (' + dim.type + ', ' + divIds.length + ' div' + (divIds.length !== 1 ? 's' : '') + ')',
|
|
465
|
+
dimChildItems, state, container
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
// The dimension node itself
|
|
469
|
+
if (dimNode) {
|
|
470
|
+
dimDetails.appendChild(buildMenuItem({
|
|
471
|
+
type: 'node', id: dim.nodeId,
|
|
472
|
+
label: dim.nodeId + ' (dimension)',
|
|
473
|
+
state, container, showLog: true,
|
|
474
|
+
logFn: () => buildNodeLogResult(dim.nodeId, structure),
|
|
475
|
+
consoleListEl, structure
|
|
476
|
+
}));
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Each division
|
|
480
|
+
divIds.forEach(divId => {
|
|
481
|
+
const div = dim.divisions[divId];
|
|
482
|
+
const leafIds = Object.keys(div.values || {});
|
|
483
|
+
|
|
484
|
+
// Collect node IDs in this division for the group checkbox
|
|
485
|
+
const divChildItems = [];
|
|
486
|
+
const divNodeId = findDivisionNodeId(structure, dimKey, divId);
|
|
487
|
+
if (divNodeId) divChildItems.push({ type: 'node', id: divNodeId });
|
|
488
|
+
leafIds.forEach(leafId => {
|
|
489
|
+
if (structure.nodes[leafId]) divChildItems.push({ type: 'node', id: leafId });
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
const divDetails = makeGroupDetails(
|
|
493
|
+
divId + ' (' + leafIds.length + ' item' + (leafIds.length !== 1 ? 's' : '') + ')',
|
|
494
|
+
divChildItems, state, container
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
// Division node itself
|
|
498
|
+
if (divNodeId) {
|
|
499
|
+
divDetails.appendChild(buildMenuItem({
|
|
500
|
+
type: 'node', id: divNodeId,
|
|
501
|
+
label: divNodeId + ' (division)',
|
|
502
|
+
state, container, showLog: true,
|
|
503
|
+
logFn: () => buildNodeLogResult(divNodeId, structure),
|
|
504
|
+
consoleListEl, structure
|
|
505
|
+
}));
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Leaf nodes in this division
|
|
509
|
+
leafIds.forEach(leafId => {
|
|
510
|
+
const node = structure.nodes[leafId];
|
|
511
|
+
if (!node) return;
|
|
512
|
+
const desc = buildLabelFn ? buildLabelFn(node) : leafId;
|
|
513
|
+
const label = leafId + (desc !== leafId ? ' \u2014 ' + truncate(desc, 30) : '');
|
|
514
|
+
divDetails.appendChild(buildMenuItem({
|
|
515
|
+
type: 'node', id: leafId, label,
|
|
516
|
+
state, container, showLog: true,
|
|
517
|
+
logFn: () => buildNodeLogResult(leafId, structure),
|
|
518
|
+
consoleListEl, structure
|
|
519
|
+
}));
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
dimDetails.appendChild(divDetails);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
nodesDetails.appendChild(dimDetails);
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// All Nodes flat list
|
|
530
|
+
const allNodesDetails = makeDetails('All Nodes (' + allNodeKeys.length + ')');
|
|
531
|
+
allNodeKeys.forEach(nodeId => {
|
|
532
|
+
const node = structure.nodes[nodeId];
|
|
533
|
+
const desc = buildLabelFn ? buildLabelFn(node) : nodeId;
|
|
534
|
+
const label = nodeId + (desc !== nodeId ? ' \u2014 ' + truncate(desc, 30) : '');
|
|
535
|
+
allNodesDetails.appendChild(buildMenuItem({
|
|
536
|
+
type: 'node', id: nodeId, label,
|
|
537
|
+
state, container, showLog: true,
|
|
538
|
+
logFn: () => buildNodeLogResult(nodeId, structure),
|
|
539
|
+
consoleListEl, structure
|
|
540
|
+
}));
|
|
541
|
+
});
|
|
542
|
+
nodesDetails.appendChild(allNodesDetails);
|
|
543
|
+
|
|
544
|
+
details.appendChild(nodesDetails);
|
|
545
|
+
|
|
546
|
+
// --- Edges ---
|
|
547
|
+
const allEdgeKeys = Object.keys(structure.edges || {});
|
|
548
|
+
const edgesDetails = makeDetails('Edges (' + allEdgeKeys.length + ')');
|
|
549
|
+
|
|
550
|
+
// Group edges by navigation rule
|
|
551
|
+
const ruleToEdges = new Map();
|
|
552
|
+
allEdgeKeys.forEach(edgeKey => {
|
|
553
|
+
const edge = structure.edges[edgeKey];
|
|
554
|
+
const rules = edge.navigationRules || [];
|
|
555
|
+
rules.forEach(rule => {
|
|
556
|
+
if (!ruleToEdges.has(rule)) ruleToEdges.set(rule, []);
|
|
557
|
+
ruleToEdges.get(rule).push(edgeKey);
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
// Sort rule names for consistent ordering
|
|
562
|
+
const sortedRules = [...ruleToEdges.keys()].sort();
|
|
563
|
+
sortedRules.forEach(rule => {
|
|
564
|
+
const edgeKeysForRule = ruleToEdges.get(rule);
|
|
565
|
+
const ruleChildItems = edgeKeysForRule.map(edgeKey => ({ type: 'edge', id: edgeKey }));
|
|
566
|
+
const ruleDetails = makeGroupDetails(
|
|
567
|
+
rule + ' (' + edgeKeysForRule.length + ')',
|
|
568
|
+
ruleChildItems, state, container
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
edgeKeysForRule.forEach(edgeKey => {
|
|
572
|
+
ruleDetails.appendChild(buildEdgeMenuItem(edgeKey, structure, state, container, consoleListEl));
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
edgesDetails.appendChild(ruleDetails);
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
// All Edges flat list
|
|
579
|
+
const allEdgesDetails = makeDetails('All Edges (' + allEdgeKeys.length + ')');
|
|
580
|
+
allEdgeKeys.forEach(edgeKey => {
|
|
581
|
+
allEdgesDetails.appendChild(buildEdgeMenuItem(edgeKey, structure, state, container, consoleListEl));
|
|
582
|
+
});
|
|
583
|
+
edgesDetails.appendChild(allEdgesDetails);
|
|
584
|
+
|
|
585
|
+
details.appendChild(edgesDetails);
|
|
586
|
+
|
|
587
|
+
return details;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Find the node ID for a division node given dimension key and division ID.
|
|
592
|
+
*/
|
|
593
|
+
function findDivisionNodeId(structure, dimKey, divId) {
|
|
594
|
+
// Division nodes have derivedNode === dimKey and data[dimKey] matching
|
|
595
|
+
const nodeKeys = Object.keys(structure.nodes);
|
|
596
|
+
for (let i = 0; i < nodeKeys.length; i++) {
|
|
597
|
+
const node = structure.nodes[nodeKeys[i]];
|
|
598
|
+
if (node.derivedNode === dimKey && node.dimensionLevel === 2) {
|
|
599
|
+
// Check if this node's data value matches divId
|
|
600
|
+
if (node.data && node.data[dimKey] !== undefined && String(node.data[dimKey]) === String(divId)) {
|
|
601
|
+
return nodeKeys[i];
|
|
602
|
+
}
|
|
603
|
+
// Also check by ID pattern — sometimes the node ID is the divId itself
|
|
604
|
+
if (nodeKeys[i] === divId) {
|
|
605
|
+
return divId;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Source Input section: wraps Data, Props, Dimensions, Divisions.
|
|
614
|
+
* (Console is now separate at top; nav rules are grouped under Edges)
|
|
615
|
+
*/
|
|
616
|
+
export function buildSourceInputSection(structure, showConsoleMenu) {
|
|
617
|
+
const details = makeDetails('Source Input', { summaryClass: 'dn-menu-summary-top' });
|
|
618
|
+
|
|
619
|
+
// Data
|
|
620
|
+
if (showConsoleMenu.data) {
|
|
621
|
+
details.appendChild(buildDataSection(showConsoleMenu.data));
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Props
|
|
625
|
+
const propsEl = buildPropsSection({
|
|
626
|
+
structure: showConsoleMenu.structure,
|
|
627
|
+
input: showConsoleMenu.input,
|
|
628
|
+
rendering: showConsoleMenu.rendering
|
|
629
|
+
});
|
|
630
|
+
if (propsEl) details.appendChild(propsEl);
|
|
631
|
+
|
|
632
|
+
// Dimensions
|
|
633
|
+
details.appendChild(buildDimensionsSection(structure.dimensions));
|
|
634
|
+
|
|
635
|
+
// Divisions
|
|
636
|
+
details.appendChild(buildDivisionsSection(structure.dimensions));
|
|
637
|
+
|
|
638
|
+
return details;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// ---------------------------------------------------------------------------
|
|
642
|
+
// Simple section builders (Data, Props, Dimensions, Divisions)
|
|
643
|
+
// ---------------------------------------------------------------------------
|
|
644
|
+
|
|
645
|
+
function buildDataSection(data) {
|
|
646
|
+
const count = Array.isArray(data) ? data.length + ' items' : 'object';
|
|
647
|
+
const details = makeDetails('Data (' + count + ')');
|
|
648
|
+
|
|
649
|
+
const pre = document.createElement('pre');
|
|
650
|
+
pre.className = 'dn-menu-pre';
|
|
651
|
+
const code = document.createElement('code');
|
|
652
|
+
code.textContent = JSON.stringify(data, null, 2);
|
|
653
|
+
pre.appendChild(code);
|
|
654
|
+
details.appendChild(pre);
|
|
655
|
+
|
|
656
|
+
return details;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function buildPropsSection(propsConfig) {
|
|
660
|
+
const { structure: s, input: i, rendering: r } = propsConfig;
|
|
661
|
+
if (!s && !i && !r) return null;
|
|
662
|
+
|
|
663
|
+
const details = makeDetails('Props');
|
|
664
|
+
|
|
665
|
+
if (s) {
|
|
666
|
+
const sub = makeDetails('structure');
|
|
667
|
+
const pre = document.createElement('pre');
|
|
668
|
+
pre.className = 'dn-menu-pre';
|
|
669
|
+
pre.textContent = JSON.stringify(s, null, 2);
|
|
670
|
+
sub.appendChild(pre);
|
|
671
|
+
details.appendChild(sub);
|
|
672
|
+
}
|
|
673
|
+
if (i) {
|
|
674
|
+
const sub = makeDetails('input');
|
|
675
|
+
const pre = document.createElement('pre');
|
|
676
|
+
pre.className = 'dn-menu-pre';
|
|
677
|
+
pre.textContent = JSON.stringify(i, null, 2);
|
|
678
|
+
sub.appendChild(pre);
|
|
679
|
+
details.appendChild(sub);
|
|
680
|
+
}
|
|
681
|
+
if (r) {
|
|
682
|
+
const sub = makeDetails('rendering');
|
|
683
|
+
const pre = document.createElement('pre');
|
|
684
|
+
pre.className = 'dn-menu-pre';
|
|
685
|
+
pre.textContent = JSON.stringify(r, null, 2);
|
|
686
|
+
sub.appendChild(pre);
|
|
687
|
+
details.appendChild(sub);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return details;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function buildDimensionsSection(dimensions) {
|
|
694
|
+
const dimKeys = dimensions ? Object.keys(dimensions) : [];
|
|
695
|
+
const details = makeDetails('Dimensions (' + dimKeys.length + ')');
|
|
696
|
+
|
|
697
|
+
dimKeys.forEach(key => {
|
|
698
|
+
const dim = dimensions[key];
|
|
699
|
+
const divCount = Object.keys(dim.divisions || {}).length;
|
|
700
|
+
const item = document.createElement('div');
|
|
701
|
+
item.className = 'dn-menu-info';
|
|
702
|
+
item.textContent = key + ' (' + dim.type + ', ' + divCount + ' div' + (divCount !== 1 ? 's' : '') + ')';
|
|
703
|
+
details.appendChild(item);
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
if (dimKeys.length === 0) {
|
|
707
|
+
const empty = document.createElement('div');
|
|
708
|
+
empty.className = 'dn-menu-info dn-menu-empty';
|
|
709
|
+
empty.textContent = '(none)';
|
|
710
|
+
details.appendChild(empty);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
return details;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function buildDivisionsSection(dimensions) {
|
|
717
|
+
const allDivisions = [];
|
|
718
|
+
if (dimensions) {
|
|
719
|
+
Object.keys(dimensions).forEach(dimKey => {
|
|
720
|
+
const dim = dimensions[dimKey];
|
|
721
|
+
Object.keys(dim.divisions || {}).forEach(divId => {
|
|
722
|
+
const div = dim.divisions[divId];
|
|
723
|
+
const count = Object.keys(div.values || {}).length;
|
|
724
|
+
allDivisions.push({ dimKey, divId, count });
|
|
725
|
+
});
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const details = makeDetails('Divisions (' + allDivisions.length + ')');
|
|
730
|
+
|
|
731
|
+
allDivisions.forEach(({ dimKey, divId, count }) => {
|
|
732
|
+
const item = document.createElement('div');
|
|
733
|
+
item.className = 'dn-menu-info';
|
|
734
|
+
item.textContent = dimKey + ': ' + divId + ' (' + count + ')';
|
|
735
|
+
details.appendChild(item);
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
if (allDivisions.length === 0) {
|
|
739
|
+
const empty = document.createElement('div');
|
|
740
|
+
empty.className = 'dn-menu-info dn-menu-empty';
|
|
741
|
+
empty.textContent = '(none)';
|
|
742
|
+
details.appendChild(empty);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
return details;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function truncate(str, max) {
|
|
749
|
+
if (str.length <= max) return str;
|
|
750
|
+
return str.slice(0, max - 1) + '\u2026';
|
|
751
|
+
}
|