mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-07-31 20:40:19 +02:00
Fix edge-cases when text leaf nodes are deleted (#5325)
This commit is contained in:
5
.changeset/android-removed-leaf.md
Normal file
5
.changeset/android-removed-leaf.md
Normal file
@@ -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.
|
@@ -2,6 +2,7 @@ import { DebouncedFunc } from 'lodash'
|
|||||||
import { Editor, Node, Path, Point, Range, Text, Transforms } from 'slate'
|
import { Editor, Node, Path, Point, Range, Text, Transforms } from 'slate'
|
||||||
import { ReactEditor } from '../../plugin/react-editor'
|
import { ReactEditor } from '../../plugin/react-editor'
|
||||||
import {
|
import {
|
||||||
|
applyStringDiff,
|
||||||
mergeStringDiffs,
|
mergeStringDiffs,
|
||||||
normalizePoint,
|
normalizePoint,
|
||||||
normalizeRange,
|
normalizeRange,
|
||||||
@@ -153,7 +154,7 @@ export function createAndroidInputManager({
|
|||||||
EDITOR_TO_PENDING_DIFFS.get(editor)
|
EDITOR_TO_PENDING_DIFFS.get(editor)
|
||||||
)
|
)
|
||||||
|
|
||||||
let scheduleSelectionChange = !!EDITOR_TO_PENDING_DIFFS.get(editor)?.length
|
let scheduleSelectionChange = hasPendingDiffs()
|
||||||
|
|
||||||
let diff: TextDiff | undefined
|
let diff: TextDiff | undefined
|
||||||
while ((diff = EDITOR_TO_PENDING_DIFFS.get(editor)?.[0])) {
|
while ((diff = EDITOR_TO_PENDING_DIFFS.get(editor)?.[0])) {
|
||||||
@@ -377,38 +378,75 @@ export function createAndroidInputManager({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Range.isExpanded(targetRange) && type.startsWith('delete')) {
|
// By default, the input manager tries to store text diffs so that we can
|
||||||
const [start, end] = Range.edges(targetRange)
|
// defer flushing them at a later point in time. We don't want to flush
|
||||||
const leaf = Node.leaf(editor, start.path)
|
// 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 (leaf.text.length === start.offset && end.offset === 0) {
|
if (type.startsWith('delete')) {
|
||||||
const next = Editor.next(editor, { at: start.path, match: Text.isText })
|
if (Range.isExpanded(targetRange)) {
|
||||||
if (next && Path.equals(next[1], end.path)) {
|
|
||||||
targetRange = { anchor: end, focus: end }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Range.isExpanded(targetRange) && type.startsWith('delete')) {
|
|
||||||
if (Path.equals(targetRange.anchor.path, targetRange.focus.path)) {
|
|
||||||
const [start, end] = Range.edges(targetRange)
|
const [start, end] = Range.edges(targetRange)
|
||||||
|
const leaf = Node.leaf(editor, start.path)
|
||||||
|
|
||||||
const point = { path: targetRange.anchor.path, offset: start.offset }
|
if (leaf.text.length === start.offset && end.offset === 0) {
|
||||||
const range = Editor.range(editor, point, point)
|
const next = Editor.next(editor, {
|
||||||
handleUserSelect(range)
|
at: start.path,
|
||||||
|
match: Text.isText,
|
||||||
return storeDiff(targetRange.anchor.path, {
|
})
|
||||||
text: '',
|
if (next && Path.equals(next[1], end.path)) {
|
||||||
end: end.offset,
|
targetRange = { anchor: end, focus: end }
|
||||||
start: start.offset,
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const direction = type.endsWith('Backward') ? 'backward' : 'forward'
|
const direction = type.endsWith('Backward') ? 'backward' : 'forward'
|
||||||
return scheduleAction(
|
const [start, end] = Range.edges(targetRange)
|
||||||
() => Editor.deleteFragment(editor, { direction }),
|
const [leaf, path] = Editor.leaf(editor, start.path)
|
||||||
{ at: targetRange }
|
|
||||||
|
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) {
|
switch (type) {
|
||||||
@@ -423,7 +461,7 @@ export function createAndroidInputManager({
|
|||||||
case 'deleteContent':
|
case 'deleteContent':
|
||||||
case 'deleteContentForward': {
|
case 'deleteContentForward': {
|
||||||
const { anchor } = targetRange
|
const { anchor } = targetRange
|
||||||
if (Range.isCollapsed(targetRange)) {
|
if (canStoreDiff && Range.isCollapsed(targetRange)) {
|
||||||
const targetNode = Node.leaf(editor, anchor.path)
|
const targetNode = Node.leaf(editor, anchor.path)
|
||||||
|
|
||||||
if (anchor.offset < targetNode.text.length) {
|
if (anchor.offset < targetNode.text.length) {
|
||||||
@@ -451,6 +489,7 @@ export function createAndroidInputManager({
|
|||||||
: !!nativeTargetRange?.collapsed
|
: !!nativeTargetRange?.collapsed
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
canStoreDiff &&
|
||||||
nativeCollapsed &&
|
nativeCollapsed &&
|
||||||
Range.isCollapsed(targetRange) &&
|
Range.isCollapsed(targetRange) &&
|
||||||
anchor.offset > 0
|
anchor.offset > 0
|
||||||
@@ -610,8 +649,10 @@ export function createAndroidInputManager({
|
|||||||
insertPositionHint = false
|
insertPositionHint = false
|
||||||
}
|
}
|
||||||
|
|
||||||
storeDiff(start.path, diff)
|
if (canStoreDiff) {
|
||||||
return
|
storeDiff(start.path, diff)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return scheduleAction(() => Editor.insertText(editor, text), {
|
return scheduleAction(() => Editor.insertText(editor, text), {
|
||||||
|
@@ -52,7 +52,7 @@ export function verifyDiffState(editor: Editor, textDiff: TextDiff): boolean {
|
|||||||
return Text.isText(nextNode) && nextNode.text.startsWith(diff.text)
|
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(
|
return diffs.reduce(
|
||||||
(text, diff) =>
|
(text, diff) =>
|
||||||
text.slice(0, diff.start) + diff.text + text.slice(diff.end),
|
text.slice(0, diff.start) + diff.text + text.slice(diff.end),
|
||||||
|
Reference in New Issue
Block a user