Skip to content

feat: Add hide completed tasks filter#3

Draft
DisciplinedSoftware wants to merge 15 commits intofeature/node-completion-statusfrom
feature/hide-completed-tasks
Draft

feat: Add hide completed tasks filter#3
DisciplinedSoftware wants to merge 15 commits intofeature/node-completion-statusfrom
feature/hide-completed-tasks

Conversation

@DisciplinedSoftware
Copy link
Copy Markdown
Owner

Overview

This stacked PR adds a filter to hide completed tasks and their descendants on top of the node completion status feature.

Stack

Base branch: feature/node-completion-status
Related upstream PR: OlegIGalkin#14

Changes

  • Add a toggle to hide completed tasks and descendant nodes
  • Hide matching SVG branch lines along with filtered nodes
  • Persist filter state across sessions in VS Code
  • Port the filter behavior to the Visual Studio extension
  • Prevent initial flash of unfiltered content during load/import

Testing

  • Rebases cleanly onto feature/node-completion-status
  • VS Code extension bundles successfully with npm run bundle

Pierre-Luc Gagné added 15 commits March 23, 2026 22:45
- Add 🙈 toolbar toggle button that hides/shows all completed nodes
  and their entire subtrees
- Completed node + descendant DOM wrappers get .mm-node-hidden
  (display: none) when the filter is active; the class is removed
  when the filter is toggled off
- For level-1 nodes the me-wrapper is hidden (no blank connector
  stub left behind); for deeper nodes me-parent is hidden
- applyFilter() is called from applyAllStatuses() so the filter
  stays consistent after every status change or layout refresh
- Button shows an active/pressed style (.mm-btn-active) while the
  filter is on
- Added help-text node to the default diagram describing the button
applyFilter and getHideTargetEl were defined inside initMindMap(), so
they were out of scope when the button handler in setupUI() called
scheduleApplyAllStatuses() — causing a silent ReferenceError that
prevented any hiding from happening.

Fixes:
- Move getHideTargetEl and applyFilter to module level (accessible
  from anywhere in the script)
- Simplify getHideTargetEl: use document.getElementById(id) directly
  to get me-parent, then check if its parentElement is me-wrapper
  (one straightforward traversal, no intermediate me-tpc lookup)
- In the button click handler call applyFilter() directly instead of
  scheduleApplyAllStatuses(), which was out of scope
document.getElementById(id) always returned null because MindElixir
does not use the node id as the HTML element id. Instead it renders
nodes as me-parent[data-nodeid="me<id>"]. Use querySelector with
the correct attribute selector.
The JS template literal inside the TS template string caused a parse
error. Use string concatenation instead.
When a node was hidden with display:none, the SVG connector lines
drawn by MindElixir remained visible because they are rendered on
separate SVG overlay elements, not inside the node's own DOM subtree.

Fix:
- After hiding/showing me-wrapper elements, applyFilter() now also
  syncs SVG path visibility:

  Main branches (root → level-1):
    The .lines SVG has one <path> per me-main>me-wrapper in DOM order.
    Paths for hidden wrappers are set to display:none.

  Sub-branches (level-2+):
    Each level-1 me-wrapper owns a subLines SVG (its last child) that
    holds ALL branch paths for the entire subtree, in the same DFS
    pre-order used by MindElixir's internal Ie() function.
    collectSubLineOrder() mirrors that traversal so paths[i] maps
    to nodesInOrder[i], and paths for hidden nodes are hidden.

- getHideTargetEl() simplified: every me-parent[data-nodeid] is
  always the first child of its own me-wrapper, so parentElement
  is always me-wrapper regardless of depth.

- Button click also calls mind.linkDiv() so the layout recompacts
  around hidden nodes; the debounced applyAllStatuses → applyFilter
  then cleans up any stale paths drawn by that linkDiv pass.
getHideTargetEl was querying [data-nodeid] which MindElixir sets on
me-tpc, not me-parent. So .parentElement returned me-parent, while
the SVG path-hiding code checks classList on me-wrapper — a mismatch
that meant mm-node-hidden was never found on the queried wrappers.

Fix: go up two levels (me-tpc → me-parent → me-wrapper) so the class
lands on me-wrapper, matching what querySelectorAll('me-main>me-wrapper')
returns for path index correlation.

Also reorder button handler: call mind.linkDiv() before applyFilter()
so paths are hidden after linkDiv redraws them, not before.
Calling mind.linkDiv() while nodes are display:none causes linkDiv to
query offsetLeft/offsetTop of hidden elements (which return 0), drawing
all their SVG paths to (0,0). This permanently corrupts the layout even
after filtering is turned off.

Fix: just call applyFilter() — the existing paths drawn in the correct
full layout are hidden/shown in place. No relayout, no corruption.
- Use mind.lines and mind.map.querySelectorAll (same references linkDiv
  uses internally) instead of document.querySelector/querySelectorAll.
- Use subSvg.querySelectorAll('path') instead of .children so only
  <path> elements are counted (avoids any stray non-path children).
- Set both setAttribute('display') and style.display for maximum SVG
  compatibility with Chromium's rendering of SVG presentation attrs.
display:none removes hidden nodes from layout flow, causing sibling nodes
to shift position. SVG paths (drawn at fixed coordinates by linkDiv) then
point to the old positions of the shifted nodes, causing the visible
branches and labels to appear misaligned.

visibility:hidden keeps all nodes in layout (no shift), so SVG path
coordinates remain correct for all visible nodes. The hidden nodes are
invisible but still occupy their original space, eliminating misalignment.
Restore hideCompleted from vscode.getState() before initMindMap() so the
filter is active immediately on load (applyAllStatuses -> applyFilter picks
it up). Save the new value to vscode.setState() on every toggle, spreading
any other existing state keys so nothing is overwritten.
… close)

vscode.getState/setState only persists while the webview is alive (hide/show).
When the panel is closed and reopened, a new webview is created and getState()
returns null, so the previous approach lost the setting.

Fix: store hideCompleted in ExtensionContext.workspaceState on the extension
host, which survives panel close/reopen and VS Code restarts (workspace-scoped).

- _getHtmlForWebview reads workspaceState and injects the value directly into
  the script as 'let hideCompleted = <true|false>' - no messaging or timing
  issues, the correct value is present from the very first applyFilter() call.
- On toggle, webview posts setHideCompleted to extension which persists it.
… unfiltered content

scheduleApplyAllStatuses() defers via RAF + 50ms, causing a visible frame
of the full unfiltered graph before filtering is applied. Calling applyFilter()
immediately after mind.init() runs before the first paint, so hidden nodes
are never visible to the user when hideCompleted is active.
…iltered content

mind.refresh() calls linkDiv() which fires the linkDiv bus event, but our
listener debounces it by 50ms. During that window the full unfiltered diagram
is visible. Calling applyFilter() immediately after mind.refresh() hides
completed nodes before the browser paints the imported data.
…nfiltered content

Start #container with opacity:0. After mind.init() or mind.refresh() calls
applyFilter() synchronously, set opacity:1 with a 150ms ease-in transition.
The filter is applied while the diagram is invisible, so completed nodes are
never seen by the user regardless of load timing.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant