Document element: cannot scroll long documents and element auto-resizes to fit content on edit #73

Open
opened 2026-04-23 13:35:59 +00:00 by eslamnawara · 4 comments
Member

Summary

Two related issues on the Document element, the document renders at the full height of its
content instead of clipping to the element's bounds with an internal
scrollbar:

  1. No internal scroll. When a document's content exceeds the element's
    visible area, scrolling inside the element does not reveal the hidden
    content. The only workaround is to resize the element taller.
  2. Auto-resize on edit. When the user updates the document, the element
    automatically grows vertically to fit the new content length. The size
    the user chose is not respected.

Combined, these make the document element behave like a non-scrollable,
content-sized container rather than a fixed-size scrollable region.

## Summary Two related issues on the Document element, the document renders at the full height of its content instead of clipping to the element's bounds with an internal scrollbar: 1. **No internal scroll.** When a document's content exceeds the element's visible area, scrolling inside the element does not reveal the hidden content. The only workaround is to resize the element taller. 2. **Auto-resize on edit.** When the user updates the document, the element automatically grows vertically to fit the new content length. The size the user chose is not respected. Combined, these make the document element behave like a non-scrollable, content-sized container rather than a fixed-size scrollable region.
Member

Implementation Spec for Issue #73

Objective

Make the Document whiteboard element behave as a fixed-size, scrollable container:

  1. When content exceeds the element's visible area, clip it and allow the user to scroll vertically to reveal hidden content.
  2. Respect the size the user chose — editing must not auto-grow the element.

Requirements

  • Konva-only architecture (no HTML overlay). Scroll is implemented via a _scrollY offset on the group and a Konva scrollbar on the right edge.
  • Content children (md-line-*) are offset vertically by -_scrollY; clipping is already done by the existing clipFunc.
  • A Konva scrollbar appears only when content exceeds visible height; hides otherwise. Dragging the thumb scrolls.
  • Wheel over the document scrolls the document internally; does not pan/zoom the stage.
  • Transformer-resize still works; re-clamp _scrollY after resize so no blank space appears.
  • Editing (dblclick textarea, Properties panel "Content", remote sync text update) never mutates bg.width/bg.height.
  • Scroll offset is transient per-client view state — not serialized.

Files to Modify

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/objects.js — primary fix: drop auto-grow, add scroll state + scrollbar + wheel handling.

No schema or server changes.

Implementation Plan

Step 1 — Drop auto-grow from renderDocumentContent

Files: objects.js

  • In renderDocumentContent, remove the branch that mutates bg.height() to neededHeight. After this, the bg size only changes via applyTransform (user-initiated transformer-resize). The skipAutoGrow option name can be kept as a no-op for external callers, or removed.

Dependencies: none.

Step 2 — Scroll state + scrollbar rendering in renderDocumentContent

Files: objects.js

  • In createDocument, initialize group._scrollY = 0, group._contentHeight = 0.
  • In renderDocumentContent:
    • After the markdown renders its children, record group._contentHeight = contentHeight.
    • Give each md-line-* child a _baseY = child.y() so setScrollY can reposition without re-reading the markdown.
    • Destroy any previous .scrollbar-track / .scrollbar-thumb and rebuild them:
      • Track: 6px wide, right-edge of bg, listening: false, dim color, name scrollbar-track.
      • Thumb: 6px wide, draggable y-only via dragBoundFunc; height = max(20, visibleH * visibleH / contentH); name scrollbar-thumb.
    • Hide both when maxScroll <= 0.
    • On thumb dragmove: compute _scrollY from thumb y, call setScrollY.

Step 3 — setScrollY / clampScroll helpers on the group

Files: objects.js

  • Attach group._setScrollY(y) and group._clampScroll() as closures (or plain functions called via group reference).
  • setScrollY(y): clamp to [0, maxScroll], store on group._scrollY, iterate group children whose name starts with md-line- and set child.y(child._baseY - _scrollY), update thumb y, batchDraw.
  • clampScroll(): re-run setScrollY(_scrollY) — this naturally clips to the new max after a resize.

Step 4 — Wheel interception

Files: objects.js

  • In createDocument, add group.on('wheel', function(e) {...}):
    • If maxScroll <= 0, do not preventDefault (let stage pan).
    • Otherwise, e.evt.preventDefault(); e.cancelBubble = true; group._setScrollY(_scrollY + e.evt.deltaY);.

Step 5 — Re-clamp on resize

Files: objects.js

  • In applyTransform for type === 'document', after renderDocumentContent(node, { skipAutoGrow: true }), call node._clampScroll && node._clampScroll().

Acceptance Criteria

  • Editing a document's text never changes bg.width/bg.height.
  • Wheel over the document scrolls the document only; stage does not pan/zoom.
  • Scrollbar appears when content overflows; hides when it fits; is draggable.
  • Content outside the bg is not visible (clipped).
  • Resize smaller keeps content scrollable; resize taller re-clamps to 0.
  • Remote edits don't auto-resize.
  • Reload preserves width/height; _scrollY resets to 0 (intentional).
  • No regressions on other object types.

Notes

Architecture: Konva-only. The document is a Konva.Group with a .bg Rect and md-line-* children rendered by WhiteboardMarkdown.renderMarkdown. Clipping is already in place via group.clipFunc. A DOM textarea is used only during edit.

Root cause 1 (no scroll): No scroll handling exists. Children are at absolute y-positions; clipFunc hides overflow but nothing offsets. Stage wheel panners trigger instead.

Root cause 2 (auto-resize): renderDocumentContent actively grows bg.height() to fit content whenever !skipAutoGrow; editMarkdownText and rerenderDocument both call without skipAutoGrow. Dropping the auto-grow branch entirely fixes both edit and sync paths — user's size is already authoritative via the sync serializer.

Scope: all changes in objects.js. No schema changes.

## Implementation Spec for Issue #73 ### Objective Make the Document whiteboard element behave as a fixed-size, scrollable container: 1. When content exceeds the element's visible area, clip it and allow the user to scroll vertically to reveal hidden content. 2. Respect the size the user chose — editing must not auto-grow the element. ### Requirements - Konva-only architecture (no HTML overlay). Scroll is implemented via a `_scrollY` offset on the group and a Konva scrollbar on the right edge. - Content children (`md-line-*`) are offset vertically by `-_scrollY`; clipping is already done by the existing `clipFunc`. - A Konva scrollbar appears only when content exceeds visible height; hides otherwise. Dragging the thumb scrolls. - Wheel over the document scrolls the document internally; does not pan/zoom the stage. - Transformer-resize still works; re-clamp `_scrollY` after resize so no blank space appears. - Editing (dblclick textarea, Properties panel "Content", remote sync text update) never mutates `bg.width/bg.height`. - Scroll offset is transient per-client view state — not serialized. ### Files to Modify - `crates/hero_whiteboard_ui/static/web/js/whiteboard/objects.js` — primary fix: drop auto-grow, add scroll state + scrollbar + wheel handling. No schema or server changes. ### Implementation Plan #### Step 1 — Drop auto-grow from `renderDocumentContent` Files: `objects.js` - In `renderDocumentContent`, remove the branch that mutates `bg.height()` to `neededHeight`. After this, the bg size only changes via `applyTransform` (user-initiated transformer-resize). The `skipAutoGrow` option name can be kept as a no-op for external callers, or removed. Dependencies: none. #### Step 2 — Scroll state + scrollbar rendering in `renderDocumentContent` Files: `objects.js` - In `createDocument`, initialize `group._scrollY = 0`, `group._contentHeight = 0`. - In `renderDocumentContent`: - After the markdown renders its children, record `group._contentHeight = contentHeight`. - Give each `md-line-*` child a `_baseY = child.y()` so `setScrollY` can reposition without re-reading the markdown. - Destroy any previous `.scrollbar-track` / `.scrollbar-thumb` and rebuild them: - Track: 6px wide, right-edge of bg, `listening: false`, dim color, name `scrollbar-track`. - Thumb: 6px wide, draggable y-only via `dragBoundFunc`; height = `max(20, visibleH * visibleH / contentH)`; name `scrollbar-thumb`. - Hide both when `maxScroll <= 0`. - On thumb `dragmove`: compute `_scrollY` from thumb y, call `setScrollY`. #### Step 3 — `setScrollY` / `clampScroll` helpers on the group Files: `objects.js` - Attach `group._setScrollY(y)` and `group._clampScroll()` as closures (or plain functions called via `group` reference). - `setScrollY(y)`: clamp to `[0, maxScroll]`, store on `group._scrollY`, iterate group children whose `name` starts with `md-line-` and set `child.y(child._baseY - _scrollY)`, update thumb y, batchDraw. - `clampScroll()`: re-run `setScrollY(_scrollY)` — this naturally clips to the new max after a resize. #### Step 4 — Wheel interception Files: `objects.js` - In `createDocument`, add `group.on('wheel', function(e) {...})`: - If `maxScroll <= 0`, do not preventDefault (let stage pan). - Otherwise, `e.evt.preventDefault(); e.cancelBubble = true; group._setScrollY(_scrollY + e.evt.deltaY);`. #### Step 5 — Re-clamp on resize Files: `objects.js` - In `applyTransform` for `type === 'document'`, after `renderDocumentContent(node, { skipAutoGrow: true })`, call `node._clampScroll && node._clampScroll()`. ### Acceptance Criteria - [ ] Editing a document's text never changes `bg.width/bg.height`. - [ ] Wheel over the document scrolls the document only; stage does not pan/zoom. - [ ] Scrollbar appears when content overflows; hides when it fits; is draggable. - [ ] Content outside the bg is not visible (clipped). - [ ] Resize smaller keeps content scrollable; resize taller re-clamps to 0. - [ ] Remote edits don't auto-resize. - [ ] Reload preserves width/height; `_scrollY` resets to 0 (intentional). - [ ] No regressions on other object types. ### Notes **Architecture**: Konva-only. The document is a `Konva.Group` with a `.bg` Rect and `md-line-*` children rendered by `WhiteboardMarkdown.renderMarkdown`. Clipping is already in place via `group.clipFunc`. A DOM textarea is used only during edit. **Root cause 1 (no scroll)**: No scroll handling exists. Children are at absolute y-positions; `clipFunc` hides overflow but nothing offsets. Stage wheel panners trigger instead. **Root cause 2 (auto-resize)**: `renderDocumentContent` actively grows `bg.height()` to fit content whenever `!skipAutoGrow`; `editMarkdownText` and `rerenderDocument` both call without `skipAutoGrow`. Dropping the auto-grow branch entirely fixes both edit and sync paths — user's size is already authoritative via the sync serializer. **Scope**: all changes in `objects.js`. No schema changes.
Member

Test Results

JavaScript-only change in objects.js; Rust workspace validated for regressions.

Check Result
cargo check --workspace pass
cargo clippy --workspace -- -D warnings pass
cargo fmt --check pass

Manual verification required: add a long markdown document, confirm a scrollbar appears on the right edge, scroll with wheel or by dragging the thumb, edit via the textarea or Properties panel and confirm the document does NOT grow vertically, resize via the transformer and confirm content still scrolls inside the new bounds.

## Test Results JavaScript-only change in `objects.js`; Rust workspace validated for regressions. | Check | Result | |---|---| | `cargo check --workspace` | pass | | `cargo clippy --workspace -- -D warnings` | pass | | `cargo fmt --check` | pass | Manual verification required: add a long markdown document, confirm a scrollbar appears on the right edge, scroll with wheel or by dragging the thumb, edit via the textarea or Properties panel and confirm the document does NOT grow vertically, resize via the transformer and confirm content still scrolls inside the new bounds.
Member

Implementation Summary

Document elements now scroll internally when content overflows, and editing no longer auto-grows the element.

Files changed

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/objects.js (+111 / -16)

Changes

  • Auto-grow removed. renderDocumentContent no longer mutates bg.height() to fit content. The user's chosen size (via transformer resize or creation default) is now authoritative. Every edit path (editMarkdownText, rerenderDocument, remote sync) flows through the same render without growing the bg.
  • Scroll state on the group. createDocument initializes group._scrollY = 0 and group._contentHeight = 0, and attaches three helpers: _docMaxScroll(), _docSetScrollY(y), _docClampScroll(). _docSetScrollY clamps, repositions every md-line-* child by _baseY − _scrollY, updates the scrollbar thumb y, and batchDraws.
  • Wheel interception. group.on('wheel', ...) sets e.cancelBubble and consumes e.evt.deltaY when maxScroll > 0, so the stage pan/zoom is suppressed over documents. When content fits, the wheel passes through untouched.
  • Konva scrollbar. renderDocumentContent now rebuilds a .scrollbar-track + .scrollbar-thumb on the right edge whenever maxScroll > 0. The thumb is draggable with a dragBoundFunc that constrains it to the track; dragmove converts thumb position back into _scrollY via the helper. Hover highlights the thumb.
  • Child base-Y stamping. After the markdown renderer creates its children, each md-line-* is stamped with _baseY = c.y() so _docSetScrollY can cheaply shift it to _baseY − _scrollY without re-running the markdown renderer.
  • Resize re-clamp. applyTransform for documents now drops the skipAutoGrow option (the flag became a no-op once auto-grow was removed) and calls node._docClampScroll() after re-render so shrinking reveals hidden content and enlarging past the content pulls scroll back to 0.

Architecture note

Konva-only — no HTML overlay. Clipping relies on the existing clipFunc on the document group (which already clips rounded corners). Scroll offset is transient per-client view state; it is not serialized to the server or to history.

Test results

  • cargo check --workspace: pass
  • cargo clippy --workspace -- -D warnings: pass
  • cargo fmt --check: pass

Manual QA required: add a long document, scroll via wheel and thumb drag, edit to confirm no auto-grow, resize via transformer and confirm scroll re-clamps.

## Implementation Summary Document elements now scroll internally when content overflows, and editing no longer auto-grows the element. ### Files changed - `crates/hero_whiteboard_ui/static/web/js/whiteboard/objects.js` (+111 / -16) ### Changes - **Auto-grow removed.** `renderDocumentContent` no longer mutates `bg.height()` to fit content. The user's chosen size (via transformer resize or creation default) is now authoritative. Every edit path (`editMarkdownText`, `rerenderDocument`, remote sync) flows through the same render without growing the bg. - **Scroll state on the group.** `createDocument` initializes `group._scrollY = 0` and `group._contentHeight = 0`, and attaches three helpers: `_docMaxScroll()`, `_docSetScrollY(y)`, `_docClampScroll()`. `_docSetScrollY` clamps, repositions every `md-line-*` child by `_baseY − _scrollY`, updates the scrollbar thumb y, and batchDraws. - **Wheel interception.** `group.on('wheel', ...)` sets `e.cancelBubble` and consumes `e.evt.deltaY` when `maxScroll > 0`, so the stage pan/zoom is suppressed over documents. When content fits, the wheel passes through untouched. - **Konva scrollbar.** `renderDocumentContent` now rebuilds a `.scrollbar-track` + `.scrollbar-thumb` on the right edge whenever `maxScroll > 0`. The thumb is draggable with a `dragBoundFunc` that constrains it to the track; `dragmove` converts thumb position back into `_scrollY` via the helper. Hover highlights the thumb. - **Child base-Y stamping.** After the markdown renderer creates its children, each `md-line-*` is stamped with `_baseY = c.y()` so `_docSetScrollY` can cheaply shift it to `_baseY − _scrollY` without re-running the markdown renderer. - **Resize re-clamp.** `applyTransform` for documents now drops the `skipAutoGrow` option (the flag became a no-op once auto-grow was removed) and calls `node._docClampScroll()` after re-render so shrinking reveals hidden content and enlarging past the content pulls scroll back to 0. ### Architecture note Konva-only — no HTML overlay. Clipping relies on the existing `clipFunc` on the document group (which already clips rounded corners). Scroll offset is transient per-client view state; it is not serialized to the server or to history. ### Test results - `cargo check --workspace`: pass - `cargo clippy --workspace -- -D warnings`: pass - `cargo fmt --check`: pass Manual QA required: add a long document, scroll via wheel and thumb drag, edit to confirm no auto-grow, resize via transformer and confirm scroll re-clamps.
Member

Pull request opened: #75

This PR implements the changes discussed in this issue.

Pull request opened: https://forge.ourworld.tf/lhumina_code/hero_whiteboard/pulls/75 This PR implements the changes discussed in this issue.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
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#73
No description provided.