Skip to content

Commit

Permalink
feat(portable-text-editor): preserve keys on undo/redo (#5805)
Browse files Browse the repository at this point in the history
* refactor(portable-text-editor): preserve keys on undo/redo

* fix(portable-text-editor): add forgotten static class functions for undo and redo

* test(portable-text-editor): add tests for undo/redo preserve keys
  • Loading branch information
skogsmaskin committed Feb 28, 2024
1 parent 6e551b0 commit f83e8e4
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -284,4 +284,12 @@ export class PortableTextEditor extends Component<PortableTextEditorProps> {
debug(`Host getting fragment`)
return editor.editable?.getFragment()
}
static undo = (editor: PortableTextEditor): void => {
debug('Host undoing')
editor.editable?.undo()
}
static redo = (editor: PortableTextEditor): void => {
debug('Host redoing')
editor.editable?.redo()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import {describe, expect, it, jest} from '@jest/globals'
import {render, waitFor} from '@testing-library/react'
import {createRef, type RefObject} from 'react'

import {PortableTextEditorTester, schemaType} from '../../__tests__/PortableTextEditorTester'
import {PortableTextEditor} from '../../PortableTextEditor'

const initialValue = [
{
_key: 'a',
_type: 'myTestBlockType',
children: [
{
_key: 'a1',
_type: 'span',
marks: [],
text: 'Block A',
},
],
markDefs: [],
style: 'normal',
},
{
_key: 'b',
_type: 'myTestBlockType',
children: [
{
_key: 'b1',
_type: 'span',
marks: [],
text: 'Block B',
},
],
markDefs: [],
style: 'normal',
},
]

const initialSelection = {
focus: {path: [{_key: 'b'}, 'children', {_key: 'b1'}], offset: 7},
anchor: {path: [{_key: 'b'}, 'children', {_key: 'b1'}], offset: 7},
}

describe('plugin:withUndoRedo', () => {
it('preserves the keys when undoing ', async () => {
const editorRef: RefObject<PortableTextEditor> = createRef()
const onChange = jest.fn()
render(
<PortableTextEditorTester
onChange={onChange}
ref={editorRef}
schemaType={schemaType}
value={initialValue}
/>,
)
await waitFor(() => {
if (editorRef.current) {
PortableTextEditor.focus(editorRef.current)
PortableTextEditor.select(editorRef.current, initialSelection)
PortableTextEditor.delete(
editorRef.current,
PortableTextEditor.getSelection(editorRef.current),
{mode: 'blocks'},
)
expect(PortableTextEditor.getValue(editorRef.current)).toMatchInlineSnapshot(`
Array [
Object {
"_key": "a",
"_type": "myTestBlockType",
"children": Array [
Object {
"_key": "a1",
"_type": "span",
"marks": Array [],
"text": "Block A",
},
],
"markDefs": Array [],
"style": "normal",
},
]
`)
PortableTextEditor.undo(editorRef.current)
expect(PortableTextEditor.getValue(editorRef.current)).toEqual(initialValue)
}
})
})
it('preserves the keys when redoing ', async () => {
const editorRef: RefObject<PortableTextEditor> = createRef()
const onChange = jest.fn()
render(
<PortableTextEditorTester
onChange={onChange}
ref={editorRef}
schemaType={schemaType}
value={initialValue}
/>,
)
await waitFor(() => {
if (editorRef.current) {
PortableTextEditor.focus(editorRef.current)
PortableTextEditor.select(editorRef.current, initialSelection)
PortableTextEditor.insertBlock(editorRef.current, editorRef.current.schemaTypes.block, {
children: [{_key: 'c1', _type: 'span', marks: [], text: 'Block C'}],
})
const producedKey = PortableTextEditor.getValue(editorRef.current)?.slice(-1)[0]?._key
PortableTextEditor.undo(editorRef.current)
PortableTextEditor.redo(editorRef.current)
expect(PortableTextEditor.getValue(editorRef.current)?.slice(-1)[0]?._key).toEqual(
producedKey,
)
}
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,18 @@ export function createWithObjectKeys(
return function withKeys(editor: PortableTextSlateEditor): PortableTextSlateEditor {
PRESERVE_KEYS.set(editor, false)
const {apply, normalizeNode} = editor

// The apply function can be called with a scope (withPreserveKeys) that will
// preserve keys for the produced nodes if they have a _key property set already.
// The default behavior is to always generate a new key here.
// For example, when undoing and redoing we want to retain the keys, but
// when we create a new bold span by splitting a non-bold-span we want the produced node to get a new key.
editor.apply = (operation) => {
if (operation.type === 'split_node') {
const withNewKey = !isPreservingKeys(editor) || !('_key' in operation.properties)
operation.properties = {
...operation.properties,
_key: keyGenerator(),
...(withNewKey ? {_key: keyGenerator()} : {}),
}
}
if (operation.type === 'insert_node') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {type PatchObservable, type PortableTextSlateEditor} from '../../types/ed
import {type Patch} from '../../types/patch'
import {debugWithName} from '../../utils/debug'
import {fromSlateValue} from '../../utils/values'
import {withPreserveKeys} from '../../utils/withPreserveKeys'

const debug = debugWithName('plugin:withUndoRedo')
const debugVerbose = debug.enabled && false
Expand Down Expand Up @@ -147,13 +148,16 @@ export function createWithUndoRedo(
})
try {
Editor.withoutNormalizing(editor, () => {
withoutSaving(editor, () => {
transformedOperations
.map(Operation.inverse)
.reverse()
.forEach((op) => {
editor.apply(op)
})
withPreserveKeys(editor, () => {
withoutSaving(editor, () => {
transformedOperations
.map(Operation.inverse)
.reverse()
// eslint-disable-next-line max-nested-callbacks
.forEach((op) => {
editor.apply(op)
})
})
})
})
editor.normalize()
Expand Down Expand Up @@ -193,9 +197,12 @@ export function createWithUndoRedo(
})
try {
Editor.withoutNormalizing(editor, () => {
withoutSaving(editor, () => {
transformedOperations.forEach((op) => {
editor.apply(op)
withPreserveKeys(editor, () => {
withoutSaving(editor, () => {
// eslint-disable-next-line max-nested-callbacks
transformedOperations.forEach((op) => {
editor.apply(op)
})
})
})
})
Expand Down

0 comments on commit f83e8e4

Please sign in to comment.