diff --git a/.changeset/android-removed-leaf.md b/.changeset/android-removed-leaf.md new file mode 100644 index 0000000000..3a2228cd64 --- /dev/null +++ b/.changeset/android-removed-leaf.md @@ -0,0 +1,5 @@ +--- +'slate-react': patch +--- + +Fix edge-cases in the Android input manager when text leaf nodes are deleted, such as when deleting text leaf nodes adjacent to inline void nodes. diff --git a/packages/slate-react/src/hooks/android-input-manager/android-input-manager.ts b/packages/slate-react/src/hooks/android-input-manager/android-input-manager.ts index d50943f3a3..371c57c806 100644 --- a/packages/slate-react/src/hooks/android-input-manager/android-input-manager.ts +++ b/packages/slate-react/src/hooks/android-input-manager/android-input-manager.ts @@ -2,6 +2,7 @@ import { DebouncedFunc } from 'lodash' import { Editor, Node, Path, Point, Range, Text, Transforms } from 'slate' import { ReactEditor } from '../../plugin/react-editor' import { + applyStringDiff, mergeStringDiffs, normalizePoint, normalizeRange, @@ -153,7 +154,7 @@ export function createAndroidInputManager({ EDITOR_TO_PENDING_DIFFS.get(editor) ) - let scheduleSelectionChange = !!EDITOR_TO_PENDING_DIFFS.get(editor)?.length + let scheduleSelectionChange = hasPendingDiffs() let diff: TextDiff | undefined while ((diff = EDITOR_TO_PENDING_DIFFS.get(editor)?.[0])) { @@ -377,38 +378,75 @@ export function createAndroidInputManager({ return } - if (Range.isExpanded(targetRange) && type.startsWith('delete')) { - const [start, end] = Range.edges(targetRange) - const leaf = Node.leaf(editor, start.path) - - if (leaf.text.length === start.offset && end.offset === 0) { - const next = Editor.next(editor, { at: start.path, match: Text.isText }) - if (next && Path.equals(next[1], end.path)) { - targetRange = { anchor: end, focus: end } - } - } - } + // By default, the input manager tries to store text diffs so that we can + // defer flushing them at a later point in time. We don't want to flush + // for every input event as this can be expensive. However, there are some + // scenarios where we cannot safely store the text diff and must instead + // schedule an action to let Slate normalize the editor state. + let canStoreDiff = true - if (Range.isExpanded(targetRange) && type.startsWith('delete')) { - if (Path.equals(targetRange.anchor.path, targetRange.focus.path)) { + if (type.startsWith('delete')) { + if (Range.isExpanded(targetRange)) { const [start, end] = Range.edges(targetRange) + const leaf = Node.leaf(editor, start.path) - const point = { path: targetRange.anchor.path, offset: start.offset } - const range = Editor.range(editor, point, point) - handleUserSelect(range) - - return storeDiff(targetRange.anchor.path, { - text: '', - end: end.offset, - start: start.offset, - }) + if (leaf.text.length === start.offset && end.offset === 0) { + const next = Editor.next(editor, { + at: start.path, + match: Text.isText, + }) + if (next && Path.equals(next[1], end.path)) { + targetRange = { anchor: end, focus: end } + } + } } const direction = type.endsWith('Backward') ? 'backward' : 'forward' - return scheduleAction( - () => Editor.deleteFragment(editor, { direction }), - { at: targetRange } + const [start, end] = Range.edges(targetRange) + const [leaf, path] = Editor.leaf(editor, start.path) + + const diff = { + text: '', + start: start.offset, + end: end.offset, + } + const pendingDiffs = EDITOR_TO_PENDING_DIFFS.get(editor) + const relevantPendingDiffs = pendingDiffs?.find(change => + Path.equals(change.path, path) ) + const diffs = relevantPendingDiffs + ? [relevantPendingDiffs.diff, diff] + : [diff] + const text = applyStringDiff(leaf.text, ...diffs) + + if (text.length === 0) { + // Text leaf will be removed, so we need to schedule an + // action to remove it so that Slate can normalize instead + // of storing as a diff + canStoreDiff = false + } + + if (Range.isExpanded(targetRange)) { + if ( + canStoreDiff && + Path.equals(targetRange.anchor.path, targetRange.focus.path) + ) { + const point = { path: targetRange.anchor.path, offset: start.offset } + const range = Editor.range(editor, point, point) + handleUserSelect(range) + + return storeDiff(targetRange.anchor.path, { + text: '', + end: end.offset, + start: start.offset, + }) + } + + return scheduleAction( + () => Editor.deleteFragment(editor, { direction }), + { at: targetRange } + ) + } } switch (type) { @@ -423,7 +461,7 @@ export function createAndroidInputManager({ case 'deleteContent': case 'deleteContentForward': { const { anchor } = targetRange - if (Range.isCollapsed(targetRange)) { + if (canStoreDiff && Range.isCollapsed(targetRange)) { const targetNode = Node.leaf(editor, anchor.path) if (anchor.offset < targetNode.text.length) { @@ -451,6 +489,7 @@ export function createAndroidInputManager({ : !!nativeTargetRange?.collapsed if ( + canStoreDiff && nativeCollapsed && Range.isCollapsed(targetRange) && anchor.offset > 0 @@ -610,8 +649,10 @@ export function createAndroidInputManager({ insertPositionHint = false } - storeDiff(start.path, diff) - return + if (canStoreDiff) { + storeDiff(start.path, diff) + return + } } return scheduleAction(() => Editor.insertText(editor, text), { diff --git a/packages/slate-react/src/utils/diff-text.ts b/packages/slate-react/src/utils/diff-text.ts index ad72be4b9e..eeefde6c42 100644 --- a/packages/slate-react/src/utils/diff-text.ts +++ b/packages/slate-react/src/utils/diff-text.ts @@ -52,7 +52,7 @@ export function verifyDiffState(editor: Editor, textDiff: TextDiff): boolean { return Text.isText(nextNode) && nextNode.text.startsWith(diff.text) } -function applyStringDiff(text: string, ...diffs: StringDiff[]) { +export function applyStringDiff(text: string, ...diffs: StringDiff[]) { return diffs.reduce( (text, diff) => text.slice(0, diff.start) + diff.text + text.slice(diff.end),