Affected Packages
@tiptap/vue-3 (maybe vue-2 and react affected too, can't check)
Version(s)
3.20.4
Bug Description
After updating from tiptap 2 to tiptap 3 my custom notion-like paragraph extensions using custom node view started behaving strange on command-A selection. After some digging and looking at commit history, I found cause. It was added to VueNodeViewRenderer class handleSelectionUpdate handler, which forcefully trigger selectNode method, so instead of selected text, we now have selected node, resulting unexpected visual and behaviour. I suggest at least add checking selectable prop of node extension for this handler (right now it is ignored).
Browser Used
Chrome
Code Example URL
No response
Expected Behavior
Return to selectText behaviour or at least control this with selectable prop
Additional Context (Optional)
wrote this extension fixing this problem for me (I extended in my project with custom view nodes in CUSTOM_NODE_TYPES var):
/* eslint-disable @typescript-eslint/naming-convention */
import { Extension } from '@tiptap/vue-3';
import { NodeSelection, Plugin, PluginKey, Selection, TextSelection } from 'prosemirror-state';
const PLUGIN_KEY = new PluginKey('selectAllTextSelection');
const META_APPLIED = 'selectAllTextSelectionApplied';
const CUSTOM_NODE_TYPES = ['paragraph', 'heading', 'bulletList', 'orderedList'];
/**
* TipTap 3's VueNodeViewRenderer.handleSelectionUpdate() adds the
* `ProseMirror-selectednode` CSS class to every node view whose range is
* encompassed by the current selection — even for TextSelection / AllSelection.
* This is a behaviour change from TipTap 2 that causes nodes to *look*
* node-selected when the user simply Cmd+A-selects all text.
*
* Three-layer fix:
* 1. handleKeyDown – intercepts Mod-a and creates a TextSelection instead
* of the default AllSelection, which already reduces the blast radius.
* 2. appendTransaction – converts any real NodeSelection on our custom-view
* node types back to a TextSelection within the node.
* 3. onSelectionUpdate + queueMicrotask – removes the ProseMirror-selectednode
* class that handleSelectionUpdate adds for non-NodeSelection selections.
* The microtask runs after all synchronous selectionUpdate listeners
* (including VueNodeView's) but before the browser paints.
*/
export const SelectAllTextSelection = Extension.create({
name: 'selectAllTextSelection',
onSelectionUpdate() {
const { editor } = this;
const { selection } = editor.state;
if (selection instanceof NodeSelection) {
return;
}
queueMicrotask(() => {
if (editor.isDestroyed) {
return;
}
editor.view.dom.querySelectorAll('.ProseMirror-selectednode').forEach((el) => {
el.classList.remove('ProseMirror-selectednode');
});
});
},
addProseMirrorPlugins() {
return [
new Plugin({
key: PLUGIN_KEY,
props: {
handleKeyDown(view, event) {
if ((event.metaKey || event.ctrlKey) && !event.shiftKey && event.code === 'KeyA') {
const { state } = view;
const { doc } = state;
const from = Selection.atStart(doc).from;
const to = Selection.atEnd(doc).to;
const selection = TextSelection.create(doc, from, to);
const tr = state.tr.setSelection(selection).setMeta(META_APPLIED, true);
view.dispatch(tr);
event.preventDefault();
return true;
}
return false;
},
},
appendTransaction(transactions, _oldState, newState) {
if (transactions.some((tr) => tr.getMeta(META_APPLIED))) {
return null;
}
const { selection } = newState;
if (selection instanceof NodeSelection && CUSTOM_NODE_TYPES.includes(selection.node.type.name)) {
const from = selection.from + 1;
const to = selection.from + selection.node.nodeSize - 1;
return newState.tr
.setSelection(TextSelection.create(newState.doc, from, to))
.setMeta(META_APPLIED, true);
}
return null;
},
}),
];
},
});
Dependency Updates
Affected Packages
@tiptap/vue-3 (maybe vue-2 and react affected too, can't check)
Version(s)
3.20.4
Bug Description
After updating from tiptap 2 to tiptap 3 my custom notion-like paragraph extensions using custom node view started behaving strange on command-A selection. After some digging and looking at commit history, I found cause. It was added to VueNodeViewRenderer class handleSelectionUpdate handler, which forcefully trigger selectNode method, so instead of selected text, we now have selected node, resulting unexpected visual and behaviour. I suggest at least add checking selectable prop of node extension for this handler (right now it is ignored).
Browser Used
Chrome
Code Example URL
No response
Expected Behavior
Return to selectText behaviour or at least control this with selectable prop
Additional Context (Optional)
wrote this extension fixing this problem for me (I extended in my project with custom view nodes in CUSTOM_NODE_TYPES var):
Dependency Updates