Mind map nodes truncate text with ellipsis instead of showing full content #153

Open
opened 2026-05-06 08:30:52 +00:00 by AhmedHanafy725 · 3 comments
Member

Mind-map nodes have a fixed NODE_W × NODE_H (140×36) and the label is rendered with ellipsis: true, wrap: 'none', so any text longer than what fits on one line gets cut off with an ellipsis. The full content the user typed is not visible.

Expected

Each node should show its entire text content. Either the node grows in width to fit a single line up to a sensible maximum, or the text wraps onto multiple lines and the node grows in height. Layout (parent/child connections, sibling spacing) should still look right.

Actual

Long labels are truncated: e.g. "hello this is a test scenario" renders as "hello this is a test scen…".

Repro

  1. Open a board, create a mind map
  2. Double-click a node and type a longer-than-default label
  3. The node renders with the text ellipsised
Mind-map nodes have a fixed `NODE_W × NODE_H` (140×36) and the label is rendered with `ellipsis: true, wrap: 'none'`, so any text longer than what fits on one line gets cut off with an ellipsis. The full content the user typed is not visible. ## Expected Each node should show its entire text content. Either the node grows in width to fit a single line up to a sensible maximum, or the text wraps onto multiple lines and the node grows in height. Layout (parent/child connections, sibling spacing) should still look right. ## Actual Long labels are truncated: e.g. `"hello this is a test scenario"` renders as `"hello this is a test scen…"`. ## Repro 1. Open a board, create a mind map 2. Double-click a node and type a longer-than-default label 3. The node renders with the text ellipsised
5.9 KiB
Author
Member

Implementation Spec

Root cause

crates/hero_whiteboard_ui/static/web/js/whiteboard/mindmap.js:

  • L4–7: NODE_W = 140, NODE_H = 36, H_GAP = 60, V_GAP = 16 are hardcoded constants used by every layout, render, and overlay path.
  • L271–282 (drawNodes): the Konva.Text is created with width: NODE_W - 16, ellipsis: true, wrap: 'none' — that is the direct truncation.
  • layoutTree (L150–199), getLayoutBounds, drawConnections, drawNodes, editMindmapNode (L656–657 width/height), and showCommentPopup (L480 anchors at y: NODE_H) all bake the fixed sizes in.

Approach: hybrid (grow width to a soft cap, then wrap)

Short labels stay compact (look like a normal mind map); longer labels grow the width up to a cap; even longer text wraps. Avoids the failure modes of "always wrap tall" (option A) and "grow arbitrarily wide" (option B).

Files to Modify

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/mindmap.js — only file.

Implementation Plan

Step 1 — variable per-node sizing

Files: mindmap.js

New constants (replace NODE_W / NODE_H literals with these where used as the node's minimum size):

MIN_NODE_W = 140
MAX_NODE_W = 280
MIN_NODE_H = 36
H_PADDING  = 16   // total
V_PADDING  = 16   // total

Text-measure helper (Konva pattern — detached Text node, .destroy() after read):

function measureNodeText(text, fontSize, fontStyle) {
    var probe = new Konva.Text({ text: text || '', fontSize: fontSize, fontStyle: fontStyle || 'normal', wrap: 'none', listening: false });
    var singleLineW = probe.width();
    var w = Math.max(MIN_NODE_W, Math.min(singleLineW + H_PADDING, MAX_NODE_W));
    probe.width(w - H_PADDING);
    probe.wrap('word');
    var h = Math.max(MIN_NODE_H, probe.height() + V_PADDING);
    probe.destroy();
    return { width: w, height: h };
}

layoutTree — measure each node up front, store on the layout result, use per-depth max for cross-axis spacing so all nodes at depth d still share a row (vertical) / column (horizontal). Two-pass:

  1. Recurse to compute nodeW/nodeH for every layout entry.
  2. Walk the tree to compute maxW[depth] (horizontal) or maxH[depth] (vertical), then use cumsum(max[0..d-1] + gap) for the depth-axis coordinate. Keeps siblings of different parents aligned.
  • Sibling-stack centering uses each layout's own nodeW/nodeH (no longer the constants).

getLayoutBounds — replace + NODE_W / + NODE_H with + layout.nodeW / + layout.nodeH.

drawConnectionsfrom/to edge points use parent.nodeW / parent.nodeH and child.nodeW / child.nodeH instead of the constants.

drawNodes:

  • Rect: width: layout.nodeW, height: layout.nodeH.
  • Label: x: x + 8, y: y + 8, width: layout.nodeW - H_PADDING, wrap: 'word'. Remove ellipsis: true and change wrap: 'none'wrap: 'word'.
  • Collapse +/ indicator: x + layout.nodeW - 18, y + layout.nodeH - 14 (vertical).
  • Add-child + button: addX = x + layout.nodeW + 4 (horizontal), or addX = x + layout.nodeW/2 - 4, addY = y + layout.nodeH + 4 (vertical).
  • Comment 💬 indicator: stays at x + 2, y + 2.

editMindmapNode — read size from the rect itself: nodeRect.width() * absScale, nodeRect.height() * absScale (no extra params).

showCommentPopup — anchor below the actual node: transform.point({ x: 0, y: nodeRect.height() }).

Dependencies: none

Acceptance Criteria

  • "hello this is a test scenario" is fully visible — no ellipsis — on both vertical and horizontal mind maps.
  • Short labels ("Leaf 1.1") keep the previous compact look (≈140×36).
  • Labels longer than ~280px wide wrap onto a second line; node height grows accordingly.
  • Connections start/end on the actual edge of each node — no gaps, no overlap.
  • +/ collapse, add-child +, comment 💬 stay at the correct corners on resized nodes.
  • Double-click opens the inline editor exactly over the rendered rect (variable size).
  • Comment popover anchors directly under the node, even when the node is taller.
  • Direction flip, collapse/expand, add-child, delete-node, and selection transformer all work with variable node sizes.
  • Edits round-trip through WhiteboardSync.onUpdate.

Notes / regressions to watch

  • Per-depth max: chosen over parent-relative spacing so siblings of different parents at the same depth stay aligned in a row/column (preserves the canonical mind-map look).
  • Long single words: Konva's wrap: 'word' doesn't break inside a word; an extreme single token can still push past MAX_NODE_W. Acceptable for v1.
  • Performance: helper creates/destroys one detached Konva.Text per node per render. Fine for typical mindmaps; cache by (text, fontSize, fontStyle) only if profiling shows it matters.
  • Recent commits: don't regress b7d21f1 (in-place edit) or 8fa6da3 (shared comment popover styling).
## Implementation Spec ### Root cause `crates/hero_whiteboard_ui/static/web/js/whiteboard/mindmap.js`: - L4–7: `NODE_W = 140`, `NODE_H = 36`, `H_GAP = 60`, `V_GAP = 16` are hardcoded constants used by every layout, render, and overlay path. - L271–282 (`drawNodes`): the `Konva.Text` is created with `width: NODE_W - 16, ellipsis: true, wrap: 'none'` — that is the direct truncation. - `layoutTree` (L150–199), `getLayoutBounds`, `drawConnections`, `drawNodes`, `editMindmapNode` (L656–657 width/height), and `showCommentPopup` (L480 anchors at `y: NODE_H`) all bake the fixed sizes in. ### Approach: hybrid (grow width to a soft cap, then wrap) Short labels stay compact (look like a normal mind map); longer labels grow the width up to a cap; even longer text wraps. Avoids the failure modes of "always wrap tall" (option A) and "grow arbitrarily wide" (option B). ### Files to Modify - `crates/hero_whiteboard_ui/static/web/js/whiteboard/mindmap.js` — only file. ### Implementation Plan #### Step 1 — variable per-node sizing Files: `mindmap.js` **New constants** (replace `NODE_W` / `NODE_H` literals with these where used as the node's *minimum* size): ``` MIN_NODE_W = 140 MAX_NODE_W = 280 MIN_NODE_H = 36 H_PADDING = 16 // total V_PADDING = 16 // total ``` **Text-measure helper** (Konva pattern — detached Text node, `.destroy()` after read): ``` function measureNodeText(text, fontSize, fontStyle) { var probe = new Konva.Text({ text: text || '', fontSize: fontSize, fontStyle: fontStyle || 'normal', wrap: 'none', listening: false }); var singleLineW = probe.width(); var w = Math.max(MIN_NODE_W, Math.min(singleLineW + H_PADDING, MAX_NODE_W)); probe.width(w - H_PADDING); probe.wrap('word'); var h = Math.max(MIN_NODE_H, probe.height() + V_PADDING); probe.destroy(); return { width: w, height: h }; } ``` **`layoutTree`** — measure each node up front, store on the layout result, use **per-depth max** for cross-axis spacing so all nodes at depth `d` still share a row (vertical) / column (horizontal). Two-pass: 1. Recurse to compute `nodeW`/`nodeH` for every layout entry. 2. Walk the tree to compute `maxW[depth]` (horizontal) or `maxH[depth]` (vertical), then use `cumsum(max[0..d-1] + gap)` for the depth-axis coordinate. Keeps siblings of different parents aligned. - Sibling-stack centering uses each layout's own `nodeW`/`nodeH` (no longer the constants). **`getLayoutBounds`** — replace `+ NODE_W` / `+ NODE_H` with `+ layout.nodeW` / `+ layout.nodeH`. **`drawConnections`** — `from`/`to` edge points use `parent.nodeW` / `parent.nodeH` and `child.nodeW` / `child.nodeH` instead of the constants. **`drawNodes`**: - Rect: `width: layout.nodeW, height: layout.nodeH`. - Label: `x: x + 8, y: y + 8, width: layout.nodeW - H_PADDING, wrap: 'word'`. **Remove `ellipsis: true`** and change `wrap: 'none'` → `wrap: 'word'`. - Collapse `+`/`−` indicator: `x + layout.nodeW - 18`, `y + layout.nodeH - 14` (vertical). - Add-child `+` button: `addX = x + layout.nodeW + 4` (horizontal), or `addX = x + layout.nodeW/2 - 4, addY = y + layout.nodeH + 4` (vertical). - Comment 💬 indicator: stays at `x + 2, y + 2`. **`editMindmapNode`** — read size from the rect itself: `nodeRect.width() * absScale`, `nodeRect.height() * absScale` (no extra params). **`showCommentPopup`** — anchor below the actual node: `transform.point({ x: 0, y: nodeRect.height() })`. Dependencies: none ### Acceptance Criteria - [ ] `"hello this is a test scenario"` is fully visible — no ellipsis — on both vertical and horizontal mind maps. - [ ] Short labels (`"Leaf 1.1"`) keep the previous compact look (≈140×36). - [ ] Labels longer than ~280px wide wrap onto a second line; node height grows accordingly. - [ ] Connections start/end on the actual edge of each node — no gaps, no overlap. - [ ] `+`/`−` collapse, add-child `+`, comment 💬 stay at the correct corners on resized nodes. - [ ] Double-click opens the inline editor exactly over the rendered rect (variable size). - [ ] Comment popover anchors directly under the node, even when the node is taller. - [ ] Direction flip, collapse/expand, add-child, delete-node, and selection transformer all work with variable node sizes. - [ ] Edits round-trip through `WhiteboardSync.onUpdate`. ### Notes / regressions to watch - **Per-depth max**: chosen over parent-relative spacing so siblings of different parents at the same depth stay aligned in a row/column (preserves the canonical mind-map look). - **Long single words**: Konva's `wrap: 'word'` doesn't break inside a word; an extreme single token can still push past `MAX_NODE_W`. Acceptable for v1. - **Performance**: helper creates/destroys one detached `Konva.Text` per node per render. Fine for typical mindmaps; cache by `(text, fontSize, fontStyle)` only if profiling shows it matters. - **Recent commits**: don't regress `b7d21f1` (in-place edit) or `8fa6da3` (shared comment popover styling).
Author
Member

Validation

Check Result
Files changed 1 file (mindmap.js, +97/-60)
cargo check --workspace pass
cargo test --workspace --lib pass
## Validation | Check | Result | |---|---| | Files changed | 1 file (mindmap.js, +97/-60) | | `cargo check --workspace` | pass | | `cargo test --workspace --lib` | pass |
Author
Member

Implementation summary

Changes

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/mindmap.js — every node now has a measured per-node size instead of the fixed 140×36 box.
    • New constants: MIN_NODE_W=140, MAX_NODE_W=280, MIN_NODE_H=36, H_PADDING=16, V_PADDING=16. NODE_W / NODE_H removed entirely.
    • New measureNodeText(text, fontSize, fontStyle) helper using a detached Konva.Text probe — measures unwrapped width, clamps to [MIN_NODE_W, MAX_NODE_W] plus padding, then re-measures with wrap: 'word' for the wrapped height.
    • layoutTree measures each node up front and stores nodeW/nodeH/depth on the layout entry.
    • New assignDepthAxis(layout, dir) second pass computes per-depth max sizes and writes cumulative depth-axis coordinates so siblings of different parents at the same depth still share a row (vertical) or column (horizontal).
    • getLayoutBounds, drawConnections, drawNodes all use layout.nodeW / layout.nodeH instead of the constants. The +/ collapse toggle, the add-child + button, and the comment 💬 indicator anchor to per-node corners.
    • drawNodes label: wrap: 'word', ellipsis: true removed.
    • editMindmapNode: input width/height now from nodeRect.width() / nodeRect.height() × abs scale — inline editor tracks the variable node size automatically.
    • showCommentPopup: anchors at transform.point({ x: 0, y: nodeRect.height() }) so the popover sits below the actual bottom of the (possibly taller) node.

Validation

  • cargo check --workspace: pass
  • cargo test --workspace --lib: pass
  • Diff scope: 1 file (mindmap.js, +97/-60)

Notes

UI-only change. Verify visually: a node labelled "hello this is a test scenario" should now render fully (no ellipsis) on both vertical and horizontal mind maps, short labels still look compact, very long labels wrap onto multiple lines and the node grows in height. Connectors, collapse toggle, add-child, comment indicator, inline editor, and comment popover all track the new variable size.

## Implementation summary ### Changes - `crates/hero_whiteboard_ui/static/web/js/whiteboard/mindmap.js` — every node now has a measured per-node size instead of the fixed 140×36 box. - New constants: `MIN_NODE_W=140`, `MAX_NODE_W=280`, `MIN_NODE_H=36`, `H_PADDING=16`, `V_PADDING=16`. `NODE_W` / `NODE_H` removed entirely. - New `measureNodeText(text, fontSize, fontStyle)` helper using a detached `Konva.Text` probe — measures unwrapped width, clamps to `[MIN_NODE_W, MAX_NODE_W]` plus padding, then re-measures with `wrap: 'word'` for the wrapped height. - `layoutTree` measures each node up front and stores `nodeW`/`nodeH`/`depth` on the layout entry. - New `assignDepthAxis(layout, dir)` second pass computes per-depth max sizes and writes cumulative depth-axis coordinates so siblings of different parents at the same depth still share a row (vertical) or column (horizontal). - `getLayoutBounds`, `drawConnections`, `drawNodes` all use `layout.nodeW` / `layout.nodeH` instead of the constants. The `+`/`−` collapse toggle, the add-child `+` button, and the comment 💬 indicator anchor to per-node corners. - `drawNodes` label: `wrap: 'word'`, `ellipsis: true` removed. - `editMindmapNode`: input width/height now from `nodeRect.width()` / `nodeRect.height()` × abs scale — inline editor tracks the variable node size automatically. - `showCommentPopup`: anchors at `transform.point({ x: 0, y: nodeRect.height() })` so the popover sits below the actual bottom of the (possibly taller) node. ### Validation - `cargo check --workspace`: pass - `cargo test --workspace --lib`: pass - Diff scope: 1 file (mindmap.js, +97/-60) ### Notes UI-only change. Verify visually: a node labelled `"hello this is a test scenario"` should now render fully (no ellipsis) on both vertical and horizontal mind maps, short labels still look compact, very long labels wrap onto multiple lines and the node grows in height. Connectors, collapse toggle, add-child, comment indicator, inline editor, and comment popover all track the new variable size.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_whiteboard#153
No description provided.