@portabletext/editor 2.21.3 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/_chunks-dts/index.d.ts +49 -209
- package/lib/_chunks-es/selector.is-at-the-start-of-block.js +103 -20
- package/lib/_chunks-es/selector.is-at-the-start-of-block.js.map +1 -1
- package/lib/_chunks-es/{util.get-text-block-text.js → util.slice-blocks.js} +73 -24
- package/lib/_chunks-es/util.slice-blocks.js.map +1 -0
- package/lib/_chunks-es/util.slice-text-block.js +13 -2
- package/lib/_chunks-es/util.slice-text-block.js.map +1 -1
- package/lib/behaviors/index.d.ts +1 -1
- package/lib/index.d.ts +2 -2
- package/lib/index.js +339 -341
- package/lib/index.js.map +1 -1
- package/lib/plugins/index.d.ts +2 -133
- package/lib/plugins/index.js +2 -796
- package/lib/plugins/index.js.map +1 -1
- package/lib/selectors/index.d.ts +2 -24
- package/lib/selectors/index.js +28 -130
- package/lib/selectors/index.js.map +1 -1
- package/lib/utils/index.d.ts +6 -4
- package/lib/utils/index.js +98 -9
- package/lib/utils/index.js.map +1 -1
- package/package.json +1 -3
- package/src/behaviors/behavior.abstract.split.ts +1 -0
- package/src/behaviors/behavior.perform-event.ts +7 -7
- package/src/converters/converter.portable-text.ts +1 -0
- package/src/converters/converter.text-html.ts +1 -0
- package/src/converters/converter.text-plain.ts +1 -0
- package/src/editor/Editable.tsx +1 -0
- package/src/editor/PortableTextEditor.tsx +0 -19
- package/src/editor/create-editor.ts +0 -3
- package/src/editor/editor-machine.ts +0 -10
- package/src/editor/event-to-change.tsx +5 -1
- package/src/editor/plugins/create-with-event-listeners.ts +30 -6
- package/src/editor/plugins/createWithObjectKeys.ts +2 -1
- package/src/editor/plugins/createWithPatches.ts +3 -3
- package/src/editor/plugins/createWithPlaceholderBlock.ts +2 -1
- package/src/editor/plugins/createWithPortableTextMarkModel.ts +2 -1
- package/src/editor/plugins/with-plugins.ts +10 -14
- package/src/editor/relay-machine.ts +0 -4
- package/src/editor/sync-machine.ts +2 -2
- package/src/editor.ts +0 -4
- package/src/history/behavior.operation.history.redo.ts +67 -0
- package/src/history/behavior.operation.history.undo.ts +71 -0
- package/src/history/event.history.undo.test.tsx +672 -0
- package/src/history/history.preserving-keys.test.tsx +112 -0
- package/src/history/remote-patches.ts +20 -0
- package/src/history/slate-plugin.history.ts +146 -0
- package/src/history/slate-plugin.redoing.ts +21 -0
- package/src/history/slate-plugin.undoing.ts +21 -0
- package/src/history/slate-plugin.without-history.ts +23 -0
- package/src/history/transform-operation.ts +245 -0
- package/src/history/undo-redo-collaboration.test.tsx +541 -0
- package/src/history/undo-redo.feature +125 -0
- package/src/history/undo-redo.test.tsx +195 -0
- package/src/history/undo-step.ts +148 -0
- package/src/index.ts +0 -1
- package/src/internal-utils/operation-to-patches.test.ts +23 -25
- package/src/internal-utils/operation-to-patches.ts +31 -22
- package/src/internal-utils/selection-text.test.ts +3 -0
- package/src/internal-utils/selection-text.ts +5 -2
- package/src/internal-utils/values.ts +23 -11
- package/src/operations/behavior.operation.block.set.ts +1 -0
- package/src/operations/behavior.operation.block.unset.ts +2 -0
- package/src/operations/behavior.operation.insert.block.ts +1 -0
- package/src/operations/behavior.operations.ts +2 -4
- package/src/plugins/index.ts +0 -3
- package/src/selectors/index.ts +0 -3
- package/src/test/vitest/step-definitions.tsx +57 -0
- package/src/test/vitest/test-editor.tsx +1 -1
- package/src/utils/parse-blocks.test.ts +296 -16
- package/src/utils/parse-blocks.ts +81 -22
- package/src/utils/util.merge-text-blocks.ts +5 -1
- package/src/utils/util.slice-blocks.ts +24 -10
- package/lib/_chunks-es/selector.get-selection-text.js +0 -92
- package/lib/_chunks-es/selector.get-selection-text.js.map +0 -1
- package/lib/_chunks-es/selector.get-text-before.js +0 -36
- package/lib/_chunks-es/selector.get-text-before.js.map +0 -1
- package/lib/_chunks-es/util.get-text-block-text.js.map +0 -1
- package/lib/_chunks-es/util.is-empty-text-block.js +0 -40
- package/lib/_chunks-es/util.is-empty-text-block.js.map +0 -1
- package/lib/_chunks-es/util.merge-text-blocks.js +0 -101
- package/lib/_chunks-es/util.merge-text-blocks.js.map +0 -1
- package/src/editor/plugins/createWithMaxBlocks.ts +0 -53
- package/src/editor/plugins/createWithUndoRedo.ts +0 -628
- package/src/editor/with-undo-step.ts +0 -37
- package/src/editor/withUndoRedo.ts +0 -34
- package/src/editor-event-listener.tsx +0 -28
- package/src/plugins/plugin.decorator-shortcut.ts +0 -238
- package/src/plugins/plugin.markdown.test.tsx +0 -42
- package/src/plugins/plugin.markdown.tsx +0 -131
- package/src/plugins/plugin.one-line.tsx +0 -123
- package/src/selectors/selector.get-list-state.test.ts +0 -189
- package/src/selectors/selector.get-list-state.ts +0 -96
- package/src/selectors/selector.get-selected-slice.ts +0 -13
- package/src/selectors/selector.get-trimmed-selection.test.ts +0 -657
- package/src/selectors/selector.get-trimmed-selection.ts +0 -189
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import {createTestKeyGenerator} from '@portabletext/test'
|
|
2
|
+
import {describe, expect, test, vi} from 'vitest'
|
|
3
|
+
import {createTestEditor} from '../test/vitest'
|
|
4
|
+
|
|
5
|
+
describe('Feature: History (Preserving Keys)', () => {
|
|
6
|
+
const keyGenerator = createTestKeyGenerator()
|
|
7
|
+
const blockAKey = keyGenerator()
|
|
8
|
+
const spanAKey = keyGenerator()
|
|
9
|
+
const blockBKey = keyGenerator()
|
|
10
|
+
const spanBKey = keyGenerator()
|
|
11
|
+
|
|
12
|
+
const initialValue = [
|
|
13
|
+
{
|
|
14
|
+
_key: blockAKey,
|
|
15
|
+
_type: 'block',
|
|
16
|
+
children: [{_key: spanAKey, _type: 'span', marks: [], text: 'Block A'}],
|
|
17
|
+
markDefs: [],
|
|
18
|
+
style: 'normal',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
_key: blockBKey,
|
|
22
|
+
_type: 'block',
|
|
23
|
+
children: [{_key: spanBKey, _type: 'span', marks: [], text: 'Block B'}],
|
|
24
|
+
markDefs: [],
|
|
25
|
+
style: 'normal',
|
|
26
|
+
},
|
|
27
|
+
]
|
|
28
|
+
const initialSelection = {
|
|
29
|
+
focus: {path: [{_key: blockBKey}, 'children', {_key: spanBKey}], offset: 7},
|
|
30
|
+
anchor: {
|
|
31
|
+
path: [{_key: blockBKey}, 'children', {_key: spanBKey}],
|
|
32
|
+
offset: 7,
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
test('Scenario: Preserving keys when undoing ', async () => {
|
|
37
|
+
const {editor} = await createTestEditor({
|
|
38
|
+
initialValue,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
editor.send({
|
|
42
|
+
type: 'delete',
|
|
43
|
+
at: initialSelection,
|
|
44
|
+
direction: 'backward',
|
|
45
|
+
unit: 'block',
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
await vi.waitFor(() => {
|
|
49
|
+
expect(editor.getSnapshot().context.value).toEqual([initialValue.at(0)])
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
editor.send({
|
|
53
|
+
type: 'history.undo',
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
await vi.waitFor(() => {
|
|
57
|
+
expect(editor.getSnapshot().context.value).toEqual(initialValue)
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('Scenario: Preserving keys when redoing ', async () => {
|
|
62
|
+
const {editor} = await createTestEditor({
|
|
63
|
+
initialValue,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const blockCKey = keyGenerator()
|
|
67
|
+
const spanCKey = keyGenerator()
|
|
68
|
+
const blockC = {
|
|
69
|
+
_key: blockCKey,
|
|
70
|
+
_type: 'block',
|
|
71
|
+
children: [{_key: spanCKey, _type: 'span', marks: [], text: 'Block C'}],
|
|
72
|
+
markDefs: [],
|
|
73
|
+
style: 'normal',
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
editor.send({
|
|
77
|
+
type: 'insert.block',
|
|
78
|
+
block: blockC,
|
|
79
|
+
at: {
|
|
80
|
+
anchor: {path: [{_key: blockBKey}], offset: 0},
|
|
81
|
+
focus: {path: [{_key: blockBKey}], offset: 0},
|
|
82
|
+
},
|
|
83
|
+
placement: 'after',
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
await vi.waitFor(() => {
|
|
87
|
+
expect(editor.getSnapshot().context.value).toEqual([
|
|
88
|
+
...initialValue,
|
|
89
|
+
blockC,
|
|
90
|
+
])
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
editor.send({
|
|
94
|
+
type: 'history.undo',
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
await vi.waitFor(() => {
|
|
98
|
+
expect(editor.getSnapshot().context.value).toEqual(initialValue)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
editor.send({
|
|
102
|
+
type: 'history.redo',
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
await vi.waitFor(() => {
|
|
106
|
+
expect(editor.getSnapshot().context.value).toEqual([
|
|
107
|
+
...initialValue,
|
|
108
|
+
blockC,
|
|
109
|
+
])
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type {Patch} from '@portabletext/patches'
|
|
2
|
+
import type {PortableTextBlock} from '@portabletext/schema'
|
|
3
|
+
import type {Editor} from 'slate'
|
|
4
|
+
|
|
5
|
+
const REMOTE_PATCHES = new WeakMap<
|
|
6
|
+
Editor,
|
|
7
|
+
{
|
|
8
|
+
patch: Patch
|
|
9
|
+
time: Date
|
|
10
|
+
snapshot: PortableTextBlock[] | undefined
|
|
11
|
+
previousSnapshot: PortableTextBlock[] | undefined
|
|
12
|
+
}[]
|
|
13
|
+
>()
|
|
14
|
+
|
|
15
|
+
export const getRemotePatches = (editor: Editor) => {
|
|
16
|
+
if (!REMOTE_PATCHES.get(editor)) {
|
|
17
|
+
REMOTE_PATCHES.set(editor, [])
|
|
18
|
+
}
|
|
19
|
+
return REMOTE_PATCHES.get(editor) ?? []
|
|
20
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This plugin will make the editor support undo/redo on the local state only.
|
|
3
|
+
* The undo/redo steps are rebased against incoming patches since the step occurred.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {PortableTextBlock} from '@portabletext/schema'
|
|
7
|
+
import type {Operation} from 'slate'
|
|
8
|
+
import type {EditorActor} from '../editor/editor-machine'
|
|
9
|
+
import {isChangingRemotely} from '../editor/withChanges'
|
|
10
|
+
import {debugWithName} from '../internal-utils/debug'
|
|
11
|
+
import {fromSlateValue} from '../internal-utils/values'
|
|
12
|
+
import type {PortableTextSlateEditor} from '../types/editor'
|
|
13
|
+
import {getRemotePatches} from './remote-patches'
|
|
14
|
+
import {isRedoing} from './slate-plugin.redoing'
|
|
15
|
+
import {isUndoing} from './slate-plugin.undoing'
|
|
16
|
+
import {isWithHistory, setWithHistory} from './slate-plugin.without-history'
|
|
17
|
+
import {createUndoSteps, getCurrentUndoStepId} from './undo-step'
|
|
18
|
+
|
|
19
|
+
const debug = debugWithName('plugin:history')
|
|
20
|
+
|
|
21
|
+
const UNDO_STEP_LIMIT = 1000
|
|
22
|
+
|
|
23
|
+
export function pluginHistory({
|
|
24
|
+
editorActor,
|
|
25
|
+
subscriptions,
|
|
26
|
+
}: {
|
|
27
|
+
editorActor: EditorActor
|
|
28
|
+
subscriptions: Array<() => () => void>
|
|
29
|
+
}): (editor: PortableTextSlateEditor) => PortableTextSlateEditor {
|
|
30
|
+
return (editor: PortableTextSlateEditor) => {
|
|
31
|
+
const remotePatches = getRemotePatches(editor)
|
|
32
|
+
let previousSnapshot: PortableTextBlock[] | undefined = fromSlateValue(
|
|
33
|
+
editor.children,
|
|
34
|
+
editorActor.getSnapshot().context.schema.block.name,
|
|
35
|
+
)
|
|
36
|
+
let previousUndoStepId = getCurrentUndoStepId(editor)
|
|
37
|
+
|
|
38
|
+
subscriptions.push(() => {
|
|
39
|
+
const subscription = editorActor.on('patches', ({patches, snapshot}) => {
|
|
40
|
+
let reset = false
|
|
41
|
+
|
|
42
|
+
for (const patch of patches) {
|
|
43
|
+
if (reset) {
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (patch.origin === 'local') {
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (patch.type === 'unset' && patch.path.length === 0) {
|
|
52
|
+
debug(
|
|
53
|
+
'Someone else cleared the content, resetting undo/redo history',
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
editor.history = {undos: [], redos: []}
|
|
57
|
+
remotePatches.splice(0, remotePatches.length)
|
|
58
|
+
setWithHistory(editor, true)
|
|
59
|
+
reset = true
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
remotePatches.push({
|
|
64
|
+
patch,
|
|
65
|
+
time: new Date(),
|
|
66
|
+
snapshot,
|
|
67
|
+
previousSnapshot,
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
previousSnapshot = snapshot
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
return () => {
|
|
75
|
+
subscription.unsubscribe()
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
editor.history = {undos: [], redos: []}
|
|
80
|
+
|
|
81
|
+
const {apply} = editor
|
|
82
|
+
|
|
83
|
+
editor.apply = (op: Operation) => {
|
|
84
|
+
if (editorActor.getSnapshot().matches({'edit mode': 'read only'})) {
|
|
85
|
+
apply(op)
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* We don't want to run any side effects when the editor is processing
|
|
91
|
+
* remote changes.
|
|
92
|
+
*/
|
|
93
|
+
if (isChangingRemotely(editor)) {
|
|
94
|
+
apply(op)
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* We don't want to run any side effects when the editor is undoing or
|
|
100
|
+
* redoing operations.
|
|
101
|
+
*/
|
|
102
|
+
if (isUndoing(editor) || isRedoing(editor)) {
|
|
103
|
+
apply(op)
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const withHistory = isWithHistory(editor)
|
|
108
|
+
const currentUndoStepId = getCurrentUndoStepId(editor)
|
|
109
|
+
|
|
110
|
+
if (!withHistory) {
|
|
111
|
+
// If we are bypassing saving undo steps, then we can just move along.
|
|
112
|
+
|
|
113
|
+
previousUndoStepId = currentUndoStepId
|
|
114
|
+
|
|
115
|
+
apply(op)
|
|
116
|
+
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (op.type !== 'set_selection') {
|
|
121
|
+
// Clear the repo steps if any actual changes are made
|
|
122
|
+
editor.history.redos = []
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
editor.history.undos = createUndoSteps({
|
|
126
|
+
steps: editor.history.undos,
|
|
127
|
+
op,
|
|
128
|
+
editor,
|
|
129
|
+
currentUndoStepId,
|
|
130
|
+
previousUndoStepId,
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
// Make sure we don't exceed the maximum number of undo steps we want
|
|
134
|
+
// to store.
|
|
135
|
+
while (editor.history.undos.length > UNDO_STEP_LIMIT) {
|
|
136
|
+
editor.history.undos.shift()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
previousUndoStepId = currentUndoStepId
|
|
140
|
+
|
|
141
|
+
apply(op)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return editor
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type {Editor} from 'slate'
|
|
2
|
+
|
|
3
|
+
const IS_REDOING: WeakMap<Editor, boolean | undefined> = new WeakMap()
|
|
4
|
+
|
|
5
|
+
export function pluginRedoing(editor: Editor, fn: () => void) {
|
|
6
|
+
const prev = isRedoing(editor)
|
|
7
|
+
|
|
8
|
+
IS_REDOING.set(editor, true)
|
|
9
|
+
|
|
10
|
+
fn()
|
|
11
|
+
|
|
12
|
+
IS_REDOING.set(editor, prev)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function isRedoing(editor: Editor) {
|
|
16
|
+
return IS_REDOING.get(editor) ?? false
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function setIsRedoing(editor: Editor, isRedoing: boolean) {
|
|
20
|
+
IS_REDOING.set(editor, isRedoing)
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type {Editor} from 'slate'
|
|
2
|
+
|
|
3
|
+
const IS_UDOING: WeakMap<Editor, boolean | undefined> = new WeakMap()
|
|
4
|
+
|
|
5
|
+
export function pluginUndoing(editor: Editor, fn: () => void) {
|
|
6
|
+
const prev = isUndoing(editor)
|
|
7
|
+
|
|
8
|
+
IS_UDOING.set(editor, true)
|
|
9
|
+
|
|
10
|
+
fn()
|
|
11
|
+
|
|
12
|
+
IS_UDOING.set(editor, prev)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function isUndoing(editor: Editor) {
|
|
16
|
+
return IS_UDOING.get(editor) ?? false
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function setIsUndoing(editor: Editor, isUndoing: boolean) {
|
|
20
|
+
IS_UDOING.set(editor, isUndoing)
|
|
21
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type {Editor} from 'slate'
|
|
2
|
+
|
|
3
|
+
const WITH_HISTORY = new WeakMap<Editor, boolean | undefined>()
|
|
4
|
+
|
|
5
|
+
export function isWithHistory(editor: Editor): boolean {
|
|
6
|
+
const withHistory = WITH_HISTORY.get(editor)
|
|
7
|
+
|
|
8
|
+
return withHistory ?? true
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function pluginWithoutHistory(editor: Editor, fn: () => void): void {
|
|
12
|
+
const withHistory = isWithHistory(editor)
|
|
13
|
+
|
|
14
|
+
WITH_HISTORY.set(editor, false)
|
|
15
|
+
|
|
16
|
+
fn()
|
|
17
|
+
|
|
18
|
+
WITH_HISTORY.set(editor, withHistory)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function setWithHistory(editor: Editor, withHistory: boolean): void {
|
|
22
|
+
WITH_HISTORY.set(editor, withHistory)
|
|
23
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import type {Patch} from '@portabletext/patches'
|
|
2
|
+
import type {PortableTextBlock} from '@portabletext/schema'
|
|
3
|
+
import {
|
|
4
|
+
DIFF_DELETE,
|
|
5
|
+
DIFF_EQUAL,
|
|
6
|
+
DIFF_INSERT,
|
|
7
|
+
parsePatch,
|
|
8
|
+
} from '@sanity/diff-match-patch'
|
|
9
|
+
import {isEqual} from 'lodash'
|
|
10
|
+
import type {Descendant, Operation} from 'slate'
|
|
11
|
+
import {debugWithName} from '../internal-utils/debug'
|
|
12
|
+
import type {PortableTextSlateEditor} from '../types/editor'
|
|
13
|
+
|
|
14
|
+
const debug = debugWithName('transformOperation')
|
|
15
|
+
const debugVerbose = debug.enabled && false
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* This will adjust the operation paths and offsets according to the
|
|
19
|
+
* remote patches by other editors since the step operations was performed.
|
|
20
|
+
*/
|
|
21
|
+
export function transformOperation(
|
|
22
|
+
editor: PortableTextSlateEditor,
|
|
23
|
+
patch: Patch,
|
|
24
|
+
operation: Operation,
|
|
25
|
+
snapshot: PortableTextBlock[] | undefined,
|
|
26
|
+
previousSnapshot: PortableTextBlock[] | undefined,
|
|
27
|
+
): Operation[] {
|
|
28
|
+
if (debugVerbose) {
|
|
29
|
+
debug(
|
|
30
|
+
`Adjusting '${operation.type}' operation paths for '${patch.type}' patch`,
|
|
31
|
+
)
|
|
32
|
+
debug(`Operation ${JSON.stringify(operation)}`)
|
|
33
|
+
debug(`Patch ${JSON.stringify(patch)}`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const transformedOperation = {...operation}
|
|
37
|
+
|
|
38
|
+
if (patch.type === 'insert' && patch.path.length === 1) {
|
|
39
|
+
const insertBlockIndex = (snapshot || []).findIndex((blk) =>
|
|
40
|
+
isEqual({_key: blk._key}, patch.path[0]),
|
|
41
|
+
)
|
|
42
|
+
debug(
|
|
43
|
+
`Adjusting block path (+${patch.items.length}) for '${transformedOperation.type}' operation and patch '${patch.type}'`,
|
|
44
|
+
)
|
|
45
|
+
return [
|
|
46
|
+
adjustBlockPath(
|
|
47
|
+
transformedOperation,
|
|
48
|
+
patch.items.length,
|
|
49
|
+
insertBlockIndex,
|
|
50
|
+
),
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (patch.type === 'unset' && patch.path.length === 1) {
|
|
55
|
+
const unsetBlockIndex = (previousSnapshot || []).findIndex((blk) =>
|
|
56
|
+
isEqual({_key: blk._key}, patch.path[0]),
|
|
57
|
+
)
|
|
58
|
+
// If this operation is targeting the same block that got removed, return empty
|
|
59
|
+
if (
|
|
60
|
+
'path' in transformedOperation &&
|
|
61
|
+
Array.isArray(transformedOperation.path) &&
|
|
62
|
+
transformedOperation.path[0] === unsetBlockIndex
|
|
63
|
+
) {
|
|
64
|
+
debug('Skipping transformation that targeted removed block')
|
|
65
|
+
return []
|
|
66
|
+
}
|
|
67
|
+
if (debugVerbose) {
|
|
68
|
+
debug(`Selection ${JSON.stringify(editor.selection)}`)
|
|
69
|
+
debug(
|
|
70
|
+
`Adjusting block path (-1) for '${transformedOperation.type}' operation and patch '${patch.type}'`,
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
return [adjustBlockPath(transformedOperation, -1, unsetBlockIndex)]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Someone reset the whole value
|
|
77
|
+
if (patch.type === 'unset' && patch.path.length === 0) {
|
|
78
|
+
debug(
|
|
79
|
+
`Adjusting selection for unset everything patch and ${operation.type} operation`,
|
|
80
|
+
)
|
|
81
|
+
return []
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (patch.type === 'diffMatchPatch') {
|
|
85
|
+
const operationTargetBlock = findOperationTargetBlock(
|
|
86
|
+
editor,
|
|
87
|
+
transformedOperation,
|
|
88
|
+
)
|
|
89
|
+
if (
|
|
90
|
+
!operationTargetBlock ||
|
|
91
|
+
!isEqual({_key: operationTargetBlock._key}, patch.path[0])
|
|
92
|
+
) {
|
|
93
|
+
return [transformedOperation]
|
|
94
|
+
}
|
|
95
|
+
const diffPatches = parsePatch(patch.value)
|
|
96
|
+
diffPatches.forEach((diffPatch) => {
|
|
97
|
+
let adjustOffsetBy = 0
|
|
98
|
+
let changedOffset = diffPatch.utf8Start1
|
|
99
|
+
const {diffs} = diffPatch
|
|
100
|
+
diffs.forEach((diff, index) => {
|
|
101
|
+
const [diffType, text] = diff
|
|
102
|
+
if (diffType === DIFF_INSERT) {
|
|
103
|
+
adjustOffsetBy += text.length
|
|
104
|
+
changedOffset += text.length
|
|
105
|
+
} else if (diffType === DIFF_DELETE) {
|
|
106
|
+
adjustOffsetBy -= text.length
|
|
107
|
+
changedOffset -= text.length
|
|
108
|
+
} else if (diffType === DIFF_EQUAL) {
|
|
109
|
+
// Only up to the point where there are no other changes
|
|
110
|
+
if (!diffs.slice(index).every(([dType]) => dType === DIFF_EQUAL)) {
|
|
111
|
+
changedOffset += text.length
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
// Adjust accordingly if someone inserted text in the same node before us
|
|
116
|
+
if (transformedOperation.type === 'insert_text') {
|
|
117
|
+
if (changedOffset < transformedOperation.offset) {
|
|
118
|
+
transformedOperation.offset += adjustOffsetBy
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Adjust accordingly if someone removed text in the same node before us
|
|
122
|
+
if (transformedOperation.type === 'remove_text') {
|
|
123
|
+
if (
|
|
124
|
+
changedOffset <=
|
|
125
|
+
transformedOperation.offset - transformedOperation.text.length
|
|
126
|
+
) {
|
|
127
|
+
transformedOperation.offset += adjustOffsetBy
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Adjust set_selection operation's points to new offset
|
|
131
|
+
if (transformedOperation.type === 'set_selection') {
|
|
132
|
+
const currentFocus = transformedOperation.properties?.focus
|
|
133
|
+
? {...transformedOperation.properties.focus}
|
|
134
|
+
: undefined
|
|
135
|
+
const currentAnchor = transformedOperation?.properties?.anchor
|
|
136
|
+
? {...transformedOperation.properties.anchor}
|
|
137
|
+
: undefined
|
|
138
|
+
const newFocus = transformedOperation?.newProperties?.focus
|
|
139
|
+
? {...transformedOperation.newProperties.focus}
|
|
140
|
+
: undefined
|
|
141
|
+
const newAnchor = transformedOperation?.newProperties?.anchor
|
|
142
|
+
? {...transformedOperation.newProperties.anchor}
|
|
143
|
+
: undefined
|
|
144
|
+
if ((currentFocus && currentAnchor) || (newFocus && newAnchor)) {
|
|
145
|
+
const points = [currentFocus, currentAnchor, newFocus, newAnchor]
|
|
146
|
+
points.forEach((point) => {
|
|
147
|
+
if (point && changedOffset < point.offset) {
|
|
148
|
+
point.offset += adjustOffsetBy
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
if (currentFocus && currentAnchor) {
|
|
152
|
+
transformedOperation.properties = {
|
|
153
|
+
focus: currentFocus,
|
|
154
|
+
anchor: currentAnchor,
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (newFocus && newAnchor) {
|
|
158
|
+
transformedOperation.newProperties = {
|
|
159
|
+
focus: newFocus,
|
|
160
|
+
anchor: newAnchor,
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
return [transformedOperation]
|
|
167
|
+
}
|
|
168
|
+
return [transformedOperation]
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Adjust the block path for a operation
|
|
172
|
+
*/
|
|
173
|
+
function adjustBlockPath(
|
|
174
|
+
operation: Operation,
|
|
175
|
+
level: number,
|
|
176
|
+
blockIndex: number,
|
|
177
|
+
): Operation {
|
|
178
|
+
const transformedOperation = {...operation}
|
|
179
|
+
if (
|
|
180
|
+
blockIndex >= 0 &&
|
|
181
|
+
transformedOperation.type !== 'set_selection' &&
|
|
182
|
+
Array.isArray(transformedOperation.path) &&
|
|
183
|
+
transformedOperation.path[0] >= blockIndex + level &&
|
|
184
|
+
transformedOperation.path[0] + level > -1
|
|
185
|
+
) {
|
|
186
|
+
const newPath = [
|
|
187
|
+
transformedOperation.path[0] + level,
|
|
188
|
+
...transformedOperation.path.slice(1),
|
|
189
|
+
]
|
|
190
|
+
transformedOperation.path = newPath
|
|
191
|
+
}
|
|
192
|
+
if (transformedOperation.type === 'set_selection') {
|
|
193
|
+
const currentFocus = transformedOperation.properties?.focus
|
|
194
|
+
? {...transformedOperation.properties.focus}
|
|
195
|
+
: undefined
|
|
196
|
+
const currentAnchor = transformedOperation?.properties?.anchor
|
|
197
|
+
? {...transformedOperation.properties.anchor}
|
|
198
|
+
: undefined
|
|
199
|
+
const newFocus = transformedOperation?.newProperties?.focus
|
|
200
|
+
? {...transformedOperation.newProperties.focus}
|
|
201
|
+
: undefined
|
|
202
|
+
const newAnchor = transformedOperation?.newProperties?.anchor
|
|
203
|
+
? {...transformedOperation.newProperties.anchor}
|
|
204
|
+
: undefined
|
|
205
|
+
if ((currentFocus && currentAnchor) || (newFocus && newAnchor)) {
|
|
206
|
+
const points = [currentFocus, currentAnchor, newFocus, newAnchor]
|
|
207
|
+
points.forEach((point) => {
|
|
208
|
+
if (
|
|
209
|
+
point &&
|
|
210
|
+
point.path[0] >= blockIndex + level &&
|
|
211
|
+
point.path[0] + level > -1
|
|
212
|
+
) {
|
|
213
|
+
point.path = [point.path[0] + level, ...point.path.slice(1)]
|
|
214
|
+
}
|
|
215
|
+
})
|
|
216
|
+
if (currentFocus && currentAnchor) {
|
|
217
|
+
transformedOperation.properties = {
|
|
218
|
+
focus: currentFocus,
|
|
219
|
+
anchor: currentAnchor,
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (newFocus && newAnchor) {
|
|
223
|
+
transformedOperation.newProperties = {
|
|
224
|
+
focus: newFocus,
|
|
225
|
+
anchor: newAnchor,
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// // Assign fresh point objects (we don't want to mutate the original ones)
|
|
231
|
+
return transformedOperation
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function findOperationTargetBlock(
|
|
235
|
+
editor: PortableTextSlateEditor,
|
|
236
|
+
operation: Operation,
|
|
237
|
+
): Descendant | undefined {
|
|
238
|
+
let block: Descendant | undefined
|
|
239
|
+
if (operation.type === 'set_selection' && editor.selection) {
|
|
240
|
+
block = editor.children[editor.selection.focus.path[0]]
|
|
241
|
+
} else if ('path' in operation) {
|
|
242
|
+
block = editor.children[operation.path[0]]
|
|
243
|
+
}
|
|
244
|
+
return block
|
|
245
|
+
}
|