Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/nodeview-leaf-selection-mutations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tiptap/core': patch
---

Node views without a `contentDOM` no longer ignore selection mutations, so ProseMirror moves the caret back to a valid position when the browser places it inside the node view.
53 changes: 53 additions & 0 deletions packages/core/src/NodeView.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ViewMutationRecord } from '@tiptap/pm/view'
import { afterEach, describe, expect, it, vi } from 'vite-plus/test'

import { createViewMutationTestContext } from '../tests/utils/createViewMutationTestContext.js'
Expand Down Expand Up @@ -102,3 +103,55 @@ describe('NodeView ignoreMutation on iOS', () => {
expect(nodeView.ignoreMutation(childListMutation(contentDOM, [edited]))).toBe(false)
})
})

function createLeafNodeView() {
const wrapper = document.createElement('span')

wrapper.contentEditable = 'false'
wrapper.append(document.createTextNode('@mention'))
document.body.append(wrapper)

class LeafNodeView extends NodeView<null> {
get dom() {
return wrapper
}
}

const nodeView = new LeafNodeView(null, {
editor: {},
extension: {},
node: { isLeaf: true, isAtom: true },
decorations: [],
innerDecorations: {},
view: {},
HTMLAttributes: {},
getPos: () => 0,
} as unknown as NodeViewRendererProps)

return { nodeView, wrapper }
}

describe('NodeView ignoreMutation without contentDOM', () => {
afterEach(() => {
document.body.replaceChildren()
})

it('lets ProseMirror handle selection changes inside the node view', () => {
const { nodeView, wrapper } = createLeafNodeView()
const mutation = { type: 'selection', target: wrapper } as unknown as ViewMutationRecord

expect(nodeView.ignoreMutation(mutation)).toBe(false)
})

it('still ignores DOM mutations inside the node view', () => {
const { nodeView, wrapper } = createLeafNodeView()
const mutation = {
type: 'childList',
target: wrapper,
addedNodes: [],
removedNodes: [],
} as unknown as ViewMutationRecord

expect(nodeView.ignoreMutation(mutation)).toBe(true)
})
})
8 changes: 7 additions & 1 deletion packages/core/src/NodeView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,10 +269,16 @@ export class NodeView<
* @return `true` if it can safely be ignored.
*/
ignoreMutation(mutation: ViewMutationRecord) {
if (!this.dom || !this.contentDOM) {
if (!this.dom) {
return true
}

if (!this.contentDOM) {
// ProseMirror has to see selection changes inside a leaf node view, otherwise
// the browser caret can get stuck inside its non-editable DOM and typed text is lost
return mutation.type !== 'selection'
}

if (typeof this.options.ignoreMutation === 'function') {
return this.options.ignoreMutation({ mutation })
}
Expand Down
Loading