Skip to content

Commit e97a9f8

Browse files
fix: invalidate node maps when nodes change in-between paints #5694. (#5746)
* fix: invalidate node maps when nodes change in-between paints #5694. * Add patch changeset * Catch additional invalide reference on Android
1 parent 0e1e4b4 commit e97a9f8

File tree

6 files changed

+76
-37
lines changed

6 files changed

+76
-37
lines changed

.changeset/bright-fishes-protect.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'slate-react': patch
3+
---
4+
5+
Invalidate node maps when nodes change until next react paint

packages/slate-react/src/components/editable.tsx

+48-35
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import {
5757
} from '../utils/environment'
5858
import Hotkeys from '../utils/hotkeys'
5959
import {
60+
IS_NODE_MAP_DIRTY,
6061
EDITOR_TO_ELEMENT,
6162
EDITOR_TO_FORCE_RENDER,
6263
EDITOR_TO_PENDING_INSERTION_MARKS,
@@ -209,6 +210,11 @@ export const Editable = forwardRef(
209210
const onDOMSelectionChange = useMemo(
210211
() =>
211212
throttle(() => {
213+
if (IS_NODE_MAP_DIRTY.get(editor)) {
214+
onDOMSelectionChange()
215+
return
216+
}
217+
212218
const el = ReactEditor.toDOMNode(editor, editor)
213219
const root = el.getRootNode()
214220

@@ -573,55 +579,62 @@ export const Editable = forwardRef(
573579
native = false
574580
}
575581

576-
// Chrome also has issues correctly editing the end of anchor elements: https://bugs.chromium.org/p/chromium/issues/detail?id=1259100
577-
// Therefore we don't allow native events to insert text at the end of anchor nodes.
578-
const { anchor } = selection
582+
// If the NODE_MAP is dirty, we can't trust the selection anchor (eg ReactEditor.toDOMPoint)
583+
if (!IS_NODE_MAP_DIRTY.get(editor)) {
584+
// Chrome also has issues correctly editing the end of anchor elements: https://bugs.chromium.org/p/chromium/issues/detail?id=1259100
585+
// Therefore we don't allow native events to insert text at the end of anchor nodes.
586+
const { anchor } = selection
579587

580-
const [node, offset] = ReactEditor.toDOMPoint(editor, anchor)
581-
const anchorNode = node.parentElement?.closest('a')
588+
const [node, offset] = ReactEditor.toDOMPoint(editor, anchor)
589+
const anchorNode = node.parentElement?.closest('a')
582590

583-
const window = ReactEditor.getWindow(editor)
584-
585-
if (
586-
native &&
587-
anchorNode &&
588-
ReactEditor.hasDOMNode(editor, anchorNode)
589-
) {
590-
// Find the last text node inside the anchor.
591-
const lastText = window?.document
592-
.createTreeWalker(anchorNode, NodeFilter.SHOW_TEXT)
593-
.lastChild() as DOMText | null
591+
const window = ReactEditor.getWindow(editor)
594592

595593
if (
596-
lastText === node &&
597-
lastText.textContent?.length === offset
594+
native &&
595+
anchorNode &&
596+
ReactEditor.hasDOMNode(editor, anchorNode)
598597
) {
599-
native = false
598+
// Find the last text node inside the anchor.
599+
const lastText = window?.document
600+
.createTreeWalker(anchorNode, NodeFilter.SHOW_TEXT)
601+
.lastChild() as DOMText | null
602+
603+
if (
604+
lastText === node &&
605+
lastText.textContent?.length === offset
606+
) {
607+
native = false
608+
}
600609
}
601-
}
602610

603-
// Chrome has issues with the presence of tab characters inside elements with whiteSpace = 'pre'
604-
// causing abnormal insert behavior: https://bugs.chromium.org/p/chromium/issues/detail?id=1219139
605-
if (
606-
native &&
607-
node.parentElement &&
608-
window?.getComputedStyle(node.parentElement)?.whiteSpace === 'pre'
609-
) {
610-
const block = Editor.above(editor, {
611-
at: anchor.path,
612-
match: n => Element.isElement(n) && Editor.isBlock(editor, n),
613-
})
611+
// Chrome has issues with the presence of tab characters inside elements with whiteSpace = 'pre'
612+
// causing abnormal insert behavior: https://bugs.chromium.org/p/chromium/issues/detail?id=1219139
613+
if (
614+
native &&
615+
node.parentElement &&
616+
window?.getComputedStyle(node.parentElement)?.whiteSpace ===
617+
'pre'
618+
) {
619+
const block = Editor.above(editor, {
620+
at: anchor.path,
621+
match: n => Element.isElement(n) && Editor.isBlock(editor, n),
622+
})
614623

615-
if (block && Node.string(block[0]).includes('\t')) {
616-
native = false
624+
if (block && Node.string(block[0]).includes('\t')) {
625+
native = false
626+
}
617627
}
618628
}
619629
}
620-
621630
// COMPAT: For the deleting forward/backward input types we don't want
622631
// to change the selection because it is the range that will be deleted,
623632
// and those commands determine that for themselves.
624-
if (!type.startsWith('delete') || type.startsWith('deleteBy')) {
633+
// If the NODE_MAP is dirty, we can't trust the selection anchor (eg ReactEditor.toDOMPoint via ReactEditor.toSlateRange)
634+
if (
635+
(!type.startsWith('delete') || type.startsWith('deleteBy')) &&
636+
!IS_NODE_MAP_DIRTY.get(editor)
637+
) {
625638
const [targetRange] = (event as any).getTargetRanges()
626639

627640
if (targetRange) {

packages/slate-react/src/hooks/android-input-manager/android-input-manager.ts

+5
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
EDITOR_TO_PLACEHOLDER_ELEMENT,
2323
EDITOR_TO_USER_MARKS,
2424
IS_COMPOSING,
25+
IS_NODE_MAP_DIRTY,
2526
} from '../../utils/weak-maps'
2627

2728
export type Action = { at?: Point | Range; run: () => void }
@@ -345,6 +346,10 @@ export function createAndroidInputManager({
345346
flushTimeoutId = null
346347
}
347348

349+
if (IS_NODE_MAP_DIRTY.get(editor)) {
350+
return
351+
}
352+
348353
const { inputType: type } = event
349354
let targetRange: Range | null = null
350355
const data: DataTransfer | string | undefined =

packages/slate-react/src/hooks/use-children.tsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import {
99
import ElementComponent from '../components/element'
1010
import TextComponent from '../components/text'
1111
import { ReactEditor } from '../plugin/react-editor'
12-
import { NODE_TO_INDEX, NODE_TO_PARENT } from '../utils/weak-maps'
12+
import {
13+
IS_NODE_MAP_DIRTY,
14+
NODE_TO_INDEX,
15+
NODE_TO_PARENT,
16+
} from '../utils/weak-maps'
1317
import { useDecorate } from './use-decorate'
1418
import { SelectedContext } from './use-selected'
1519
import { useSlateStatic } from './use-slate-static'
@@ -36,6 +40,7 @@ const useChildren = (props: {
3640
} = props
3741
const decorate = useDecorate()
3842
const editor = useSlateStatic()
43+
IS_NODE_MAP_DIRTY.set(editor as ReactEditor, false)
3944
const path = ReactEditor.findPath(editor, node)
4045
const children = []
4146
const isLeafBlock =

packages/slate-react/src/plugin/with-react.ts

+11
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
import { Key } from '../utils/key'
2626
import { findCurrentLineRange } from '../utils/lines'
2727
import {
28+
IS_NODE_MAP_DIRTY,
2829
EDITOR_TO_KEY_TO_ELEMENT,
2930
EDITOR_TO_ON_CHANGE,
3031
EDITOR_TO_PENDING_ACTION,
@@ -206,6 +207,16 @@ export const withReact = <T extends BaseEditor>(
206207

207208
apply(op)
208209

210+
switch (op.type) {
211+
case 'insert_node':
212+
case 'remove_node':
213+
case 'merge_node':
214+
case 'move_node':
215+
case 'split_node': {
216+
IS_NODE_MAP_DIRTY.set(e, true)
217+
}
218+
}
219+
209220
for (const [path, key] of matches) {
210221
const [node] = Editor.node(e, path)
211222
NODE_TO_KEY.set(node, key)

packages/slate-react/src/utils/weak-maps.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Key } from './key'
77
* Two weak maps that allow us rebuild a path given a node. They are populated
88
* at render time such that after a render occurs we can always backtrack.
99
*/
10-
10+
export const IS_NODE_MAP_DIRTY: WeakMap<Editor, boolean> = new WeakMap()
1111
export const NODE_TO_INDEX: WeakMap<Node, number> = new WeakMap()
1212
export const NODE_TO_PARENT: WeakMap<Node, Ancestor> = new WeakMap()
1313

0 commit comments

Comments
 (0)