Skip to content

Commit 15d1535

Browse files
Pierre-Luc GagnéCopilot
andcommitted
Align Visual Studio extension status feature with VS Code extension
- Replace class-based status approach with data-status attribute - Add CSS icons (::before) with correct positioning per node level - Add applyAllStatuses() + debounced linkDiv listener for load/refresh - Add context menu items (In Progress, Completed, Clear Status) - Add input-box guard to c key handler - Trigger autosave on status change via c key and context menu - Move status tutorial node inside Mind Map group, update descriptions - scheduleApplyAllStatuses() called on selectNode and operation events Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 6c93bea commit 15d1535

1 file changed

Lines changed: 176 additions & 46 deletions

File tree

Visual Studio/CodeMindMap/CodeMindMapHtml.cs

Lines changed: 176 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,52 @@ public class CodeMindMapHtml
2929
margin: 0;
3030
padding: 0;
3131
}
32-
/* Status indicator styles */
33-
.node-completed {
34-
opacity: 0.6;
32+
/* Root node: match the same padding-to-font ratio as level-1 nodes.
33+
Level-1 uses 8px/25px padding with 14px font (ratios 0.57v / 1.79h).
34+
Root has 25px font, so target: 14px vertical / 45px horizontal. */
35+
.map-container me-root me-tpc {
36+
padding: 14px 45px !important;
37+
}
38+
39+
/* Status indicator styles */
40+
/* Root (45px horizontal padding) and level-1 (25px padding) already have enough room for the icon */
41+
.map-container me-tpc {
42+
position: relative;
43+
}
44+
/* Level-2+ nodes: always reserve icon space so text never shifts when status changes. */
45+
.map-container me-children me-parent me-tpc {
46+
padding-left: 20px;
47+
box-sizing: border-box;
48+
}
49+
.map-container me-tpc[data-status=""completed""] {
3550
text-decoration: line-through;
3651
}
37-
.node-in-progress::before {
38-
content: '⟳ ';
39-
color: #ff9800;
52+
.map-container me-tpc[data-status=""completed""] > .text {
53+
text-decoration: line-through !important;
54+
}
55+
.map-container me-tpc[data-status]::before {
56+
position: absolute;
57+
left: 2px; /* level-2+: within the 20px padding we add */
58+
top: 50%;
59+
transform: translateY(-50%);
60+
width: 14px;
61+
text-align: center;
4062
font-weight: bold;
4163
}
42-
.node-completed::before {
43-
content: '✓ ';
64+
/* Center icon in the existing left padding for root (45px) and level-1 (25px) */
65+
.map-container me-root me-tpc[data-status]::before {
66+
left: 16px; /* (45px - 14px) / 2 */
67+
}
68+
.map-container me-main > me-wrapper > me-parent > me-tpc[data-status]::before {
69+
left: 6px; /* (25px - 14px) / 2 */
70+
}
71+
.map-container me-tpc[data-status=""in-progress""]::before {
72+
content: '⟳';
73+
color: #ff9800;
74+
}
75+
.map-container me-tpc[data-status=""completed""]::before {
76+
content: '✓';
4477
color: #4caf50;
45-
font-weight: bold;
4678
}
4779
</style>
4880
</head>
@@ -53,12 +85,61 @@ public class CodeMindMapHtml
5385
import MindElixir from ""http://codemindmap.vsext/MindElixir.js"";
5486
5587
let mind, themeManager, lastSelectedNode;
88+
let linkDivDebounceTimer = null;
89+
let scheduleRafHandle = null;
90+
let scheduleTimerHandle = null;
5691
5792
function initMindMap() {
5893
const options = {
5994
el: '#map',
6095
allowUndo: true,
6196
toolBar: true,
97+
contextMenu: {
98+
extend: [
99+
{
100+
name: '⟳ In Progress',
101+
onclick: () => {
102+
const node = mind.currentNode?.nodeObj;
103+
if (!node) return;
104+
node.data = node.data || {};
105+
node.data.status = 'in-progress';
106+
updateNodeStatus(node);
107+
window.chrome.webview.postMessage({ action: 'mindMapOperation', operationName: 'updateNodeStatus' });
108+
const cm = document.querySelector('.map-container > .context-menu'); if (cm) cm.hidden = true;
109+
}
110+
},
111+
{
112+
name: '✓ Completed',
113+
onclick: () => {
114+
const node = mind.currentNode?.nodeObj;
115+
if (!node) return;
116+
node.data = node.data || {};
117+
node.data.status = 'completed';
118+
updateNodeStatus(node);
119+
window.chrome.webview.postMessage({ action: 'mindMapOperation', operationName: 'updateNodeStatus' });
120+
const cm = document.querySelector('.map-container > .context-menu'); if (cm) cm.hidden = true;
121+
}
122+
},
123+
{
124+
name: '✕ Clear Status',
125+
onclick: () => {
126+
const node = mind.currentNode?.nodeObj;
127+
if (!node) return;
128+
node.data = node.data || {};
129+
delete node.data.status;
130+
updateNodeStatus(node);
131+
window.chrome.webview.postMessage({ action: 'mindMapOperation', operationName: 'updateNodeStatus' });
132+
const cm = document.querySelector('.map-container > .context-menu'); if (cm) cm.hidden = true;
133+
}
134+
},
135+
]
136+
},
137+
view: {
138+
beforeSelect(el, node) {
139+
mind.currentNode = node;
140+
return true;
141+
}
142+
}
62143
};
63144
64145
const LIGHT_THEME = {
@@ -195,23 +276,27 @@ function initMindMap() {
195276
topic: 'Space - Expand/collapse nodes',
196277
id: 'bd1bb2ac4bbab458',
197278
},
198-
],
199-
},
200-
{
201-
topic: 'C - Toggle node status (Not Started, In Progress, Completed)',
202-
id: 'bd1bb2ac4bbab460',
203-
children: [
204279
{
205-
topic: 'Press C to cycle through status states',
206-
id: 'bd1bb2ac4bbab461',
207-
},
208-
{
209-
topic: 'Completed nodes show ✓ and appear faded',
210-
id: 'bd1bb2ac4bbab462',
211-
},
212-
{
213-
topic: 'In Progress nodes show ⟳ in orange',
214-
id: 'bd1bb2ac4bbab463',
280+
topic: 'c - Cycle node status: (none) → In Progress → Completed → (none)',
281+
id: 'bd1bb2ac4bbab460',
282+
children: [
283+
{
284+
topic: 'In Progress nodes show ⟳ in orange',
285+
id: 'bd1bb2ac4bbab461',
286+
},
287+
{
288+
topic: 'Completed nodes show ✓ with strikethrough text',
289+
id: 'bd1bb2ac4bbab462',
290+
},
291+
{
292+
topic: 'Right-click a node to set status directly from the context menu',
293+
id: 'bd1bb2ac4bbab463',
294+
},
295+
{
296+
topic: 'Status is saved automatically with the diagram',
297+
id: 'bd1bb2ac4bbab464',
298+
},
299+
],
215300
},
216301
],
217302
},
@@ -235,27 +320,60 @@ function initMindMap() {
235320
};
236321
});
237322
323+
scheduleApplyAllStatuses();
238324
// Helper function to update node visual status
239325
function updateNodeStatus(nodeObj) {
240326
if (!nodeObj || !nodeObj.id) return;
241327
const nodeElement = MindElixir.E(nodeObj.id);
242328
if (!nodeElement) return;
243-
329+
244330
const status = nodeObj.data?.status || null;
245-
const domEl = nodeElement.getEl?.();
331+
const domEl = nodeElement.getEl?.() || nodeElement;
246332
if (!domEl) return;
247-
248-
// Remove all status classes
249-
domEl.classList.remove('node-completed', 'node-in-progress');
250-
251-
// Apply new status class
252-
if (status === 'completed') {
253-
domEl.classList.add('node-completed');
254-
} else if (status === 'in-progress') {
255-
domEl.classList.add('node-in-progress');
333+
334+
const topicEl = (() => {
335+
if (domEl.tagName === 'ME-TPC') return domEl;
336+
const byQuery = domEl.querySelector?.('me-tpc');
337+
if (byQuery) return byQuery;
338+
const byTag = domEl.getElementsByTagName?.('me-tpc')?.[0];
339+
if (byTag) return byTag;
340+
return domEl;
341+
})();
342+
343+
if (!topicEl) return;
344+
345+
if (!status) {
346+
topicEl.removeAttribute('data-status');
347+
return;
348+
}
349+
350+
topicEl.setAttribute('data-status', status);
351+
}
352+
353+
function applyAllStatuses() {
354+
const root = mind?.nodeData;
355+
if (!root) return;
356+
const stack = [root];
357+
while (stack.length > 0) {
358+
const node = stack.pop();
359+
if (!node) continue;
360+
updateNodeStatus(node);
361+
if (Array.isArray(node.children)) {
362+
for (const child of node.children) {
363+
stack.push(child);
364+
}
365+
}
256366
}
257367
}
258368
369+
function scheduleApplyAllStatuses() {
370+
if (!mind) return;
371+
if (scheduleRafHandle !== null) cancelAnimationFrame(scheduleRafHandle);
372+
if (scheduleTimerHandle !== null) clearTimeout(scheduleTimerHandle);
373+
scheduleRafHandle = requestAnimationFrame(() => { applyAllStatuses(); scheduleRafHandle = null; });
374+
scheduleTimerHandle = setTimeout(() => { applyAllStatuses(); scheduleTimerHandle = null; }, 50);
375+
}
376+
259377
mind.bus.addListener('selectNodes', nodes => {
260378
const node = nodes.at(-1);
261379
if (!node) return;
@@ -266,18 +384,27 @@ function updateNodeStatus(nodeObj) {
266384
nodeTopic: node.topic,
267385
nodeData: node.data,
268386
});
387+
scheduleApplyAllStatuses();
269388
});
270389
271390
mind.bus.addListener('selectNewNode', () => {
272391
// Mirror the same focus behavior for newly created nodes.
273392
mind.map?.focus();
274393
});
275394
276-
mind.bus.addListener('operation', operation => {
277-
window.chrome.webview.postMessage({
278-
action: 'mindMapOperation',
279-
operationName: operation.name,
280-
});
395+
// Debounced linkDiv listener: MindElixir fires linkDiv after every layout pass.
396+
// Wait for 50ms of silence before applying statuses so we run after the final DOM state.
397+
mind.bus.addListener('linkDiv', () => {
398+
clearTimeout(linkDivDebounceTimer);
399+
linkDivDebounceTimer = setTimeout(applyAllStatuses, 50);
400+
});
401+
402+
mind.bus.addListener('operation', operation => {
403+
window.chrome.webview.postMessage({
404+
action: 'mindMapOperation',
405+
operationName: operation.name,
406+
});
407+
scheduleApplyAllStatuses();
281408
});
282409
283410
document.addEventListener('click', (e) => {
@@ -314,13 +441,15 @@ function updateNodeStatus(nodeObj) {
314441
}
315442
}
316443
else if ((e.key === 'c' || e.key === 'C') && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey) {
444+
// Skip if MindElixir's inline editor is open
445+
if (document.getElementById('input-box')) return;
317446
e.preventDefault();
318447
const currentNode = mind.currentNode?.nodeObj;
319448
if (!currentNode) return;
320449
321450
currentNode.data = currentNode.data || {};
322-
323-
// Cycle through status: (none) -> in-progress -> completed -> (none)
451+
452+
// Cycle: (none) -> in-progress -> completed -> (none)
324453
const statuses = [null, 'in-progress', 'completed'];
325454
const currentStatus = currentNode.data.status || null;
326455
const currentIndex = statuses.indexOf(currentStatus);
@@ -330,9 +459,9 @@ function updateNodeStatus(nodeObj) {
330459
} else {
331460
currentNode.data.status = next;
332461
}
333-
334-
// Update visual appearance
462+
335463
updateNodeStatus(currentNode);
464+
window.chrome.webview.postMessage({ action: 'mindMapOperation', operationName: 'updateNodeStatus' });
336465
}
337466
});
338467
@@ -438,10 +567,11 @@ function getThemeName(mindElixirData) {
438567
mind.clearHistory();
439568
440569
const dataThemeName = getThemeName(mindData);
441-
442570
if (dataThemeName != '' && themeManager.contains(dataThemeName) && dataThemeName != mind.theme?.name) {
443571
mind.changeTheme(themeManager.getTheme(dataThemeName));
444572
}
573+
// Statuses are applied via the debounced linkDiv bus listener
574+
// which fires after MindElixir's layout settles.
445575
446576
return { success: true, error: """" };
447577
} catch (e) {

0 commit comments

Comments
 (0)