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/tough-snails-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@wangeditor-next/core': patch
---

fix beforeinput target range mapping after divider undo to tolerate stale DOM nodes and avoid "Cannot resolve a Slate node from DOM node" errors when typing
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,62 @@ describe('handleBeforeInput', () => {
expect(Editor.insertText).toHaveBeenCalledWith(editor, 'same')
})

it('skips reselection when target range cannot be mapped to Slate', () => {
const editor = {
selection: createSelection(false),
getConfig: () => ({ readOnly: false }),
insertData: vi.fn(),
} as any
const targetRange = { startContainer: document.createTextNode('x') }
const event = {
inputType: 'insertText',
data: 'hello',
dataTransfer: null,
target: {},
preventDefault: vi.fn(),
getTargetRanges: () => [targetRange],
} as any

vi.spyOn(helpers, 'hasEditableTarget').mockReturnValue(true)
vi.spyOn(DomEditor, 'toSlateRange').mockReturnValue(null as any)
vi.spyOn(Transforms, 'select').mockImplementation(() => {})
vi.spyOn(Editor, 'insertText').mockImplementation(() => {})

expect(() => handleBeforeInput(event, {} as any, editor)).not.toThrow()
expect(DomEditor.toSlateRange).toHaveBeenCalledWith(editor, targetRange, {
exactMatch: false,
suppressThrow: true,
})
expect(Transforms.select).not.toHaveBeenCalled()
expect(Editor.insertText).toHaveBeenCalledWith(editor, 'hello')
})

it('does not throw when target range points to stale DOM nodes', () => {
const editor = {
selection: createSelection(false),
getConfig: () => ({ readOnly: false }),
insertData: vi.fn(),
} as any
const targetRange = { startContainer: document.createTextNode('x') }
const event = {
inputType: 'insertText',
data: 'hello',
dataTransfer: null,
target: {},
preventDefault: vi.fn(),
getTargetRanges: () => [targetRange],
} as any

vi.spyOn(helpers, 'hasEditableTarget').mockReturnValue(true)
vi.spyOn(DomEditor, 'toSlateRange').mockImplementation(() => {
throw new Error('Cannot resolve a Slate node from DOM node')
})
vi.spyOn(Editor, 'insertText').mockImplementation(() => {})

expect(() => handleBeforeInput(event, {} as any, editor)).not.toThrow()
expect(Editor.insertText).toHaveBeenCalledWith(editor, 'hello')
})

it('does not delete when selection partially covers a table', () => {
const editor = {
selection: createSelection(true),
Expand Down
18 changes: 13 additions & 5 deletions packages/core/src/text-area/event-handlers/beforeInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,20 @@ function handleBeforeInput(e: Event, textarea: TextArea, editor: IDomEditor) {
const [targetRange] = event.getTargetRanges()

if (targetRange) {
const range = DomEditor.toSlateRange(editor, targetRange, {
exactMatch: false,
suppressThrow: false,
})
let range: Range | null = null

try {
range = DomEditor.toSlateRange(editor, targetRange, {
exactMatch: false,
suppressThrow: true,
})
} catch {
// Undo/redo or IME transitions can leave beforeinput target ranges
// pointing to stale DOM nodes for a short time.
range = null
}

if (!selection || !Range.equals(selection, range)) {
if (range && (!selection || !Range.equals(selection, range))) {
Transforms.select(editor, range)
}
}
Expand Down