@@ -28,20 +28,52 @@ public class CodeMindMapHtml
2828 margin: 0;
2929 padding: 0;
3030 }
31- /* Status indicator styles */
32- .node-completed {
33- opacity: 0.6;
31+ /* Root node: match the same padding-to-font ratio as level-1 nodes.
32+ Level-1 uses 8px/25px padding with 14px font (ratios 0.57v / 1.79h).
33+ Root has 25px font, so target: 14px vertical / 45px horizontal. */
34+ .map-container me-root me-tpc {
35+ padding: 14px 45px !important;
36+ }
37+
38+ /* Status indicator styles */
39+ /* Root (45px horizontal padding) and level-1 (25px padding) already have enough room for the icon */
40+ .map-container me-tpc {
41+ position: relative;
42+ }
43+ /* Level-2+ nodes: always reserve icon space so text never shifts when status changes. */
44+ .map-container me-children me-parent me-tpc {
45+ padding-left: 20px;
46+ box-sizing: border-box;
47+ }
48+ .map-container me-tpc[data-status=""completed""] {
3449 text-decoration: line-through;
3550 }
36- .node-in-progress::before {
37- content: '⟳ ';
38- color: #ff9800;
51+ .map-container me-tpc[data-status=""completed""] > .text {
52+ text-decoration: line-through !important;
53+ }
54+ .map-container me-tpc[data-status]::before {
55+ position: absolute;
56+ left: 2px; /* level-2+: within the 20px padding we add */
57+ top: 50%;
58+ transform: translateY(-50%);
59+ width: 14px;
60+ text-align: center;
3961 font-weight: bold;
4062 }
41- .node-completed::before {
42- content: '✓ ';
63+ /* Center icon in the existing left padding for root (45px) and level-1 (25px) */
64+ .map-container me-root me-tpc[data-status]::before {
65+ left: 16px; /* (45px - 14px) / 2 */
66+ }
67+ .map-container me-main > me-wrapper > me-parent > me-tpc[data-status]::before {
68+ left: 6px; /* (25px - 14px) / 2 */
69+ }
70+ .map-container me-tpc[data-status=""in-progress""]::before {
71+ content: '⟳';
72+ color: #ff9800;
73+ }
74+ .map-container me-tpc[data-status=""completed""]::before {
75+ content: '✓';
4376 color: #4caf50;
44- font-weight: bold;
4577 }
4678 </style>
4779</head>
@@ -52,12 +84,55 @@ public class CodeMindMapHtml
5284 import MindElixir from ""http://codemindmap.vsext/MindElixir.js"";
5385
5486 let mind, themeManager;
87+ let linkDivDebounceTimer = null;
88+ let scheduleRafHandle = null;
89+ let scheduleTimerHandle = null;
5590
5691 function initMindMap() {
5792 const options = {
5893 el: '#map',
5994 allowUndo: true,
6095 toolBar: true,
96+ contextMenu: {
97+ extend: [
98+ {
99+ name: '⟳ In Progress',
100+ onclick: () => {
101+ const node = mind.currentNode?.nodeObj;
102+ if (!node) return;
103+ node.data = node.data || {};
104+ node.data.status = 'in-progress';
105+ updateNodeStatus(node);
106+ window.chrome.webview.postMessage({ action: 'mindMapOperation', operationName: 'updateNodeStatus' });
107+ const cm = document.querySelector('.map-container > .context-menu'); if (cm) cm.hidden = true;
108+ }
109+ },
110+ {
111+ name: '✓ Completed',
112+ onclick: () => {
113+ const node = mind.currentNode?.nodeObj;
114+ if (!node) return;
115+ node.data = node.data || {};
116+ node.data.status = 'completed';
117+ updateNodeStatus(node);
118+ window.chrome.webview.postMessage({ action: 'mindMapOperation', operationName: 'updateNodeStatus' });
119+ const cm = document.querySelector('.map-container > .context-menu'); if (cm) cm.hidden = true;
120+ }
121+ },
122+ {
123+ name: '✕ Clear Status',
124+ onclick: () => {
125+ const node = mind.currentNode?.nodeObj;
126+ if (!node) return;
127+ node.data = node.data || {};
128+ delete node.data.status;
129+ updateNodeStatus(node);
130+ window.chrome.webview.postMessage({ action: 'mindMapOperation', operationName: 'updateNodeStatus' });
131+ const cm = document.querySelector('.map-container > .context-menu'); if (cm) cm.hidden = true;
132+ }
133+ },
134+ ]
135+ },
61136 view: {
62137 beforeSelect(el, node) {
63138 mind.currentNode = node;
@@ -200,23 +275,27 @@ function initMindMap() {
200275 topic: 'space - Expand/collapse nodes',
201276 id: 'bd1bb2ac4bbab458',
202277 },
203- ],
204- },
205- {
206- topic: 'C - Toggle node status (Not Started, In Progress, Completed)',
207- id: 'bd1bb2ac4bbab460',
208- children: [
209- {
210- topic: 'Press C to cycle through status states',
211- id: 'bd1bb2ac4bbab461',
212- },
213278 {
214- topic: 'Completed nodes show ✓ and appear faded',
215- id: 'bd1bb2ac4bbab462',
216- },
217- {
218- topic: 'In Progress nodes show ⟳ in orange',
219- id: 'bd1bb2ac4bbab463',
279+ topic: 'c - Cycle node status: (none) → In Progress → Completed → (none)',
280+ id: 'bd1bb2ac4bbab460',
281+ children: [
282+ {
283+ topic: 'In Progress nodes show ⟳ in orange',
284+ id: 'bd1bb2ac4bbab461',
285+ },
286+ {
287+ topic: 'Completed nodes show ✓ with strikethrough text',
288+ id: 'bd1bb2ac4bbab462',
289+ },
290+ {
291+ topic: 'Right-click a node to set status directly from the context menu',
292+ id: 'bd1bb2ac4bbab463',
293+ },
294+ {
295+ topic: 'Status is saved automatically with the diagram',
296+ id: 'bd1bb2ac4bbab464',
297+ },
298+ ],
220299 },
221300 ],
222301 },
@@ -240,26 +319,59 @@ function initMindMap() {
240319 };
241320 });
242321
322+ scheduleApplyAllStatuses();
243323 // Helper function to update node visual status
244324 function updateNodeStatus(nodeObj) {
245325 if (!nodeObj || !nodeObj.id) return;
246326 const nodeElement = MindElixir.E(nodeObj.id);
247327 if (!nodeElement) return;
248-
328+
249329 const status = nodeObj.data?.status || null;
250- const domEl = nodeElement.getEl?.();
330+ const domEl = nodeElement.getEl?.() || nodeElement ;
251331 if (!domEl) return;
252-
253- // Remove all status classes
254- domEl.classList.remove('node-completed', 'node-in-progress');
255-
256- // Apply new status class
257- if (status === 'completed') {
258- domEl.classList.add('node-completed');
259- } else if (status === 'in-progress') {
260- domEl.classList.add('node-in-progress');
332+
333+ const topicEl = (() => {
334+ if (domEl.tagName === 'ME-TPC') return domEl;
335+ const byQuery = domEl.querySelector?.('me-tpc');
336+ if (byQuery) return byQuery;
337+ const byTag = domEl.getElementsByTagName?.('me-tpc')?.[0];
338+ if (byTag) return byTag;
339+ return domEl;
340+ })();
341+
342+ if (!topicEl) return;
343+
344+ if (!status) {
345+ topicEl.removeAttribute('data-status');
346+ return;
347+ }
348+
349+ topicEl.setAttribute('data-status', status);
350+ }
351+
352+ function applyAllStatuses() {
353+ const root = mind?.nodeData;
354+ if (!root) return;
355+ const stack = [root];
356+ while (stack.length > 0) {
357+ const node = stack.pop();
358+ if (!node) continue;
359+ updateNodeStatus(node);
360+ if (Array.isArray(node.children)) {
361+ for (const child of node.children) {
362+ stack.push(child);
363+ }
364+ }
261365 }
262366 }
367+
368+ function scheduleApplyAllStatuses() {
369+ if (!mind) return;
370+ if (scheduleRafHandle !== null) cancelAnimationFrame(scheduleRafHandle);
371+ if (scheduleTimerHandle !== null) clearTimeout(scheduleTimerHandle);
372+ scheduleRafHandle = requestAnimationFrame(() => { applyAllStatuses(); scheduleRafHandle = null; });
373+ scheduleTimerHandle = setTimeout(() => { applyAllStatuses(); scheduleTimerHandle = null; }, 50);
374+ }
263375
264376 mind.bus.addListener('selectNode', node => {
265377 window.chrome.webview.postMessage({
@@ -268,13 +380,22 @@ function updateNodeStatus(nodeObj) {
268380 nodeTopic: node.topic,
269381 nodeData: node.data,
270382 });
383+ scheduleApplyAllStatuses();
384+ });
385+
386+ // Debounced linkDiv listener: MindElixir fires linkDiv after every layout pass.
387+ // Wait for 50ms of silence before applying statuses so we run after the final DOM state.
388+ mind.bus.addListener('linkDiv', () => {
389+ clearTimeout(linkDivDebounceTimer);
390+ linkDivDebounceTimer = setTimeout(applyAllStatuses, 50);
271391 });
272392
273- mind.bus.addListener('operation', operation => {
274- window.chrome.webview.postMessage({
275- action: 'mindMapOperation',
276- operationName: operation.name,
277- });
393+ mind.bus.addListener('operation', operation => {
394+ window.chrome.webview.postMessage({
395+ action: 'mindMapOperation',
396+ operationName: operation.name,
397+ });
398+ scheduleApplyAllStatuses();
278399 });
279400
280401 document.addEventListener('click', (e) => {
@@ -311,11 +432,14 @@ function updateNodeStatus(nodeObj) {
311432 }
312433 }
313434 else if ((e.key === 'c' || e.key === 'C') && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey) {
435+ // Skip if MindElixir's inline editor is open
436+ if (document.getElementById('input-box')) return;
314437 e.preventDefault();
315438 const currentNode = mind.currentNode?.nodeObj;
316- if (!currentNode || !currentNode.data) return;
317-
318- // Cycle through status: (none) -> in-progress -> completed -> (none)
439+ if (!currentNode) return;
440+ currentNode.data = currentNode.data || {};
441+
442+ // Cycle: (none) -> in-progress -> completed -> (none)
319443 const statuses = [null, 'in-progress', 'completed'];
320444 const currentStatus = currentNode.data.status || null;
321445 const currentIndex = statuses.indexOf(currentStatus);
@@ -325,9 +449,9 @@ function updateNodeStatus(nodeObj) {
325449 } else {
326450 currentNode.data.status = next;
327451 }
328-
329- // Update visual appearance
452+
330453 updateNodeStatus(currentNode);
454+ window.chrome.webview.postMessage({ action: 'mindMapOperation', operationName: 'updateNodeStatus' });
331455 }
332456 });
333457
@@ -441,10 +565,11 @@ function getThemeName(mindElixirData) {
441565 mind.refresh(mindData);
442566
443567 const dataThemeName = getThemeName(mindData);
444-
445568 if (dataThemeName != '' && themeManager.contains(dataThemeName) && dataThemeName != mind.theme?.name) {
446569 mind.changeTheme(themeManager.getTheme(dataThemeName));
447570 }
571+ // Statuses are applied via the debounced linkDiv bus listener
572+ // which fires after MindElixir's layout settles.
448573
449574 return { success: true, error: """" };
450575 } catch (e) {
0 commit comments