Skip to content

fix(clipboard): use ProseMirror selection state for Shadow DOM compatibility#1

Merged
wielinde merged 1 commit intomainfrom
fix/shadow-dom-clipboard-selection
Apr 25, 2026
Merged

fix(clipboard): use ProseMirror selection state for Shadow DOM compatibility#1
wielinde merged 1 commit intomainfrom
fix/shadow-dom-clipboard-selection

Conversation

@wielinde
Copy link
Copy Markdown
Member

Problem

BlockNote embeds ProseMirror inside editors that may be mounted in a Shadow DOM (e.g. OpenProject uses attachShadow({ mode: 'open' }) to isolate the editor from its Angular host). In this setup, window.getSelection() returns null or a collapsed selection even when text is visually selected — this affects Firefox (all versions), Safari ≤16.3, and Chromium edge cases.

checkIfSelectionInNonEditableBlock used window.getSelection() as its primary empty-selection guard. Because getSelection() misfires in Shadow DOM, the guard always returned true, causing both the copy and cut handlers to bail out early without writing to the clipboard. The browser's default copy then ran, which uses ProseMirror's DOMSerializer without BlockNote's semantic wrappers — losing list formatting, headings, and bold/italic on paste into external apps.

Fix

Use view.state.selection.empty as the primary guard. ProseMirror's internal selection state is updated via its own reconciliation loop and is always accurate regardless of DOM mode.

The non-editable-block DOM walk (which does need window.getSelection()) is kept as a secondary guard, but only runs when getSelection() actually returns a non-collapsed selection — so it's a no-op in Shadow DOM environments where getSelection() is unreliable.

Test

Verified in OpenProject (Shadow DOM setup) with a bullet list: the clipboard now contains all three expected types — blocknote/html, text/html (with <ul>/<li>), and text/plain (markdown * item).

…ibility

OpenProject embeds BlockNote inside a Shadow DOM (attachShadow({ mode: 'open' }))
to isolate it from the host Angular application. In this setup,
window.getSelection() returns null or a collapsed selection even when text is
selected (Firefox all versions, Safari ≤16.3, Chromium edge cases), causing
checkIfSelectionInNonEditableBlock to always return true and skip the
clipboard write entirely. The browser's default copy then fires, which uses
ProseMirror's DOMSerializer without semantic wrappers — so list formatting,
headings, and bold/italic are lost on paste into external apps.

Fix: use view.state.selection.empty as the primary empty-selection guard.
ProseMirror's internal state is always accurate regardless of DOM mode. The
DOM-level non-editable-island check is kept as a secondary guard, but only
when window.getSelection() actually returns a non-collapsed selection.

Fixes copy/cut for editors mounted inside attachShadow({ mode: 'open' }).
@wielinde wielinde merged commit a9992bc into main Apr 25, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant