Skip to content

Commit

Permalink
fix(editor): allow undoing part-deletion of annotated text
Browse files Browse the repository at this point in the history
Before this change, the editor would crash if you tried to undo deleting part
of an annotated text (from behind). This is because undoing the `remove_text`
operation results in an `insert_text` operation. And we have special logic in
place to make sure that `insert_text` right after annotated text results in a
new node without annotations. This not only results in an invalid state (we
didn't want to the node to be split), but it also results in a subsequent crash
when the selection stored on the undo stack would try to select inside the
unsplit node.
  • Loading branch information
christianhg committed Aug 9, 2024
1 parent c91d54b commit 42c2cdf
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*
*/

import {isPortableTextBlock, isPortableTextSpan} from '@portabletext/toolkit'
import {isEqual, uniq} from 'lodash'
import {type Subject} from 'rxjs'
import {type Descendant, Editor, Element, Path, Range, Text, Transforms} from 'slate'
Expand All @@ -18,6 +19,7 @@ import {
import {debugWithName} from '../../utils/debug'
import {toPortableTextRange} from '../../utils/ranges'
import {EMPTY_MARKS} from '../../utils/values'
import {withoutPreserveKeys} from '../../utils/withPreserveKeys'

const debug = debugWithName('plugin:withPortableTextMarkModel')

Expand Down Expand Up @@ -230,8 +232,8 @@ export function createWithPortableTextMarkModel(
}
}

// Special hook before inserting text at the end of an annotation.
editor.apply = (op) => {
// Special hook before inserting text at the end of an annotation.
if (op.type === 'insert_text') {
const {selection} = editor
if (
Expand Down Expand Up @@ -273,6 +275,54 @@ export function createWithPortableTextMarkModel(
}
}
}

if (op.type === 'remove_text') {
const nodeEntry = Array.from(
Editor.nodes(editor, {
mode: 'lowest',
at: {path: op.path, offset: op.offset},
match: (n) => n._type === types.span.name,
voids: false,
}),
)[0]
const node = nodeEntry[0]
const blockEntry = Editor.node(editor, Path.parent(op.path))
const block = blockEntry[0]

if (node && isPortableTextSpan(node) && block && isPortableTextBlock(block)) {
const markDefs = block.markDefs ?? []
const nodeHasAnnotations = (node.marks ?? []).some((mark) =>
markDefs.find((markDef) => markDef._key === mark),
)
const deletingPartOfTheNode = op.offset !== 0
const deletingFromTheEnd = op.offset + op.text.length === node.text.length

if (nodeHasAnnotations && deletingPartOfTheNode && deletingFromTheEnd) {
/**
* If all of these conditions match then override the ordinary
* `remove_text` operation and turn it into `split_nodes` followed
* by `remove_nodes`. This is so if the operation can be properly
* undone. Undoing a `remove_text` results in an `insert_text` and
* we want to bail out of that in this exact scenario to make sure
* the inserted text is annotated. (See custom logic regarding
* `insert_text`)
*/
Editor.withoutNormalizing(editor, () => {
withoutPreserveKeys(editor, () => {
Transforms.splitNodes(editor, {
match: Text.isText,
at: {path: op.path, offset: op.offset},
})
})
Transforms.removeNodes(editor, {at: Path.next(op.path)})
})

editor.onChange()
return
}
}
}

apply(op)
}

Expand Down
7 changes: 7 additions & 0 deletions packages/editor/src/utils/withPreserveKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ export function withPreserveKeys(editor: Editor, fn: () => void): void {
PRESERVE_KEYS.set(editor, prev)
}

export function withoutPreserveKeys(editor: Editor, fn: () => void): void {
const prev = isPreservingKeys(editor)
PRESERVE_KEYS.set(editor, false)
fn()
PRESERVE_KEYS.set(editor, prev)
}

export function isPreservingKeys(editor: Editor): boolean | undefined {
return PRESERVE_KEYS.get(editor)
}

0 comments on commit 42c2cdf

Please sign in to comment.