fix(collapsible): allow multi-paragraph drag-selection in Chromium#10584
Conversation
Chromium caps a native mouse drag-selection to a single block when the body blocks are direct children of a <details> inside a contenteditable: its ::details-content slot acts as a hard selection boundary. Keyboard and programmatic selection cross it fine, only the drag algorithm doesn't, and Firefox is unaffected. It reproduces on Chromium 150 (ahead of stable), and no CSS works around it (issue #10570). Wrap the body blocks in a <div class="trilium-collapsible-content"> in the editing view via elementToStructure + slots; one wrapping container removes the boundary. <summary> stays a direct child so native collapse keeps it visible, so only the non-summary blocks are wrapped. The wrapper is editing-only: the data downcast stays flat (<details><summary>...</summary><blocks>) so the saved HTML signature is unchanged and existing notes load identically. elementToStructure reconverts the <details> on every body-block change, rebuilding it closed and collapsing a block mid-edit. Track open state in a WeakMap (fed by the arrow toggle, setDetailsOpen and auto-open) that the downcast view() reads back to re-apply the `open` attribute — a post-render fix-up can't, because the view's "render" event fires before the DOM is reconciled. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces an editing-only wrapper (.trilium-collapsible-content) around the body blocks of collapsible elements (<details>) in CKEditor 5. This wrapper resolves a Chromium limitation where native mouse drag-selection is restricted to a single block when blocks are direct children of a <details> element. Additionally, the PR introduces a WeakMap (detailsOpenState) to track and preserve the open/collapsed state of the collapsible elements during reconversions triggered by editing. Tests and CSS rules have been updated accordingly. I have no feedback to provide as there are no review comments.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
Greptile SummaryThis PR fixes a Chromium-specific bug where mouse drag-selection inside a
Confidence Score: 4/5Safe to merge after a quick manual smoke-test of collapse/expand while editing body content — the unit tests cover the core state-preservation path but cannot exercise the full editor rendering pipeline. The detailsOpenState WeakMap correctly threads open state through CKEditor's reconversion cycle across all write paths (setDetailsOpen, toggle event, auto-open). The data downcast and saved HTML are untouched so no migration risk exists. The only gaps are hiddenBodyPostFixer reading DOM directly instead of isDetailsOpen() (harmless today but creates future drift risk) and a missing test for the Ctrl+Enter reconversion flow. Both are non-blocking quality items. packages/ckeditor5-collapsible/src/collapsible-editing.ts — specifically the hiddenBodyPostFixer method and the onDomKeydown Ctrl+Enter handler, which are the two places that interact with open state outside the new isDetailsOpen/setDetailsOpen abstraction. Important Files Changed
|
| it("preserves the open state across the reconversion a body-block change triggers", () => { | ||
| editor.setData("<details><summary>Title</summary><p>First</p></details>"); | ||
| const root = editor.editing.view.getDomRoot(); | ||
| const selector = "details.trilium-collapsible"; | ||
| const detailsBefore = root?.querySelector<HTMLDetailsElement>(selector); | ||
| // Simulate the user opening the block: flip `open` and fire the native | ||
| // `toggle` the plugin listens for (the arrow's click handler does the same). | ||
| if (detailsBefore) { | ||
| detailsBefore.open = true; | ||
| detailsBefore.dispatchEvent(new Event("toggle")); | ||
| } | ||
|
|
||
| // Add a second body paragraph — this changes the <details> children and | ||
| // makes elementToStructure rebuild its DOM (fresh, hence closed). | ||
| editor.model.change((writer) => { | ||
| const details = editor.model.document.getRoot()?.getChild(0); | ||
| if (details?.is("element", "details")) { | ||
| writer.insertElement("paragraph", details, "end"); | ||
| } | ||
| }); | ||
|
|
||
| const detailsAfter = root?.querySelector<HTMLDetailsElement>(selector); | ||
| expect(detailsAfter?.open).toBe(true); | ||
| }); |
There was a problem hiding this comment.
No test coverage for Ctrl+Enter → reconversion open-state path
The new test covers the arrow-toggle → reconversion path (setting open directly + dispatching toggle). A symmetric scenario is missing: Ctrl+Enter in onDomKeydown sets dom.open = !dom.open directly rather than calling setDetailsOpen(), relying on the native toggle event propagating to onDetailsToggle to update detailsOpenState. This chain works correctly in practice, but a test that fires a synthetic keydown with Ctrl+Enter and then adds a body block would guard against a regression if the Ctrl+Enter handler is ever refactored to not emit a toggle event.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Bundle ReportChanges will increase total bundle size by 738 bytes (0.0%) ⬆️. This is within the configured threshold ✅ Detailed changes
Affected Assets, Files, and Routes:view changes for bundle: client-esmAssets Changed:
|
Problem
In the editor, dragging the mouse to select text inside a collapsible block (
<details>) is capped to a single paragraph on Chromium/Electron. Firefox is unaffected.Root-caused as a Chromium bug: inside a
contenteditable, native mouse drag-selection cannot cross the::details-contentslot boundary when the body blocks are direct children of<details>. Keyboard selection (Shift+↓) and programmatic selection cross it fine — only the drag algorithm doesn't, which is why the model/cache never saw a problem. It reproduces on Chromium 150 (well ahead of stable), and no CSS (user-select,::details-content { … },display,content-visibility, …) works around it.Fix
Wrap the body blocks in a
<div class="trilium-collapsible-content">in the editing view (elementToStructure+ slots). A single wrapping container removes the selection boundary.<summary>stays a direct child of<details>(native collapse must keep it visible), so only the non-summary blocks are wrapped.<details><summary>…</summary><p>…</p></details>. The wrapper exists only in the editing view; nothing new is saved, no migration, existing notes load identically.elementToStructurereconverts the<details>on every body-block change, which rebuilds it closed and would collapse a block mid-edit. AWeakMapof open state (fed by the arrow toggle,setDetailsOpen, and auto-open) is read back in the downcastview()to re-apply theopenattribute. A post-render fix-up doesn't work here because CKEditor fires itsrenderevent before the DOM is reconciled — setting the attribute inview()lets the renderer apply it correctly.Validation
<details>boundary works too.Note: validated at the unit + isolated-browser level, not a full running-editor build — worth a quick manual smoke test that collapse/expand still feels right while editing a block's body.
🤖 Generated with Claude Code