@portabletext/editor 1.0.13 → 1.0.15
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/index.esm.js +162 -123
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +162 -123
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +162 -123
- package/lib/index.mjs.map +1 -1
- package/package.json +12 -12
- package/src/editor/hooks/useSyncValue.ts +8 -17
- package/src/editor/plugins/createWithMaxBlocks.ts +20 -0
- package/src/editor/plugins/createWithObjectKeys.ts +22 -11
- package/src/editor/plugins/createWithPatches.ts +3 -6
- package/src/editor/plugins/createWithPlaceholderBlock.ts +20 -0
- package/src/editor/plugins/createWithPortableTextMarkModel.ts +103 -97
- package/src/editor/plugins/createWithUndoRedo.ts +5 -3
- package/src/utils/withUndoRedo.ts +34 -0
- package/src/utils/withPreserveKeys.ts +0 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@portabletext/editor",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.15",
|
|
4
4
|
"description": "Portable Text Editor made in React",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sanity",
|
|
@@ -47,22 +47,22 @@
|
|
|
47
47
|
"is-hotkey-esm": "^1.0.0",
|
|
48
48
|
"lodash": "^4.17.21",
|
|
49
49
|
"slate": "0.103.0",
|
|
50
|
-
"slate-react": "0.
|
|
50
|
+
"slate-react": "0.108.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@jest/globals": "^29.7.0",
|
|
54
54
|
"@playwright/test": "1.46.0",
|
|
55
55
|
"@portabletext/toolkit": "^2.0.15",
|
|
56
|
-
"@sanity/block-tools": "^3.
|
|
56
|
+
"@sanity/block-tools": "^3.54.0",
|
|
57
57
|
"@sanity/diff-match-patch": "^3.1.1",
|
|
58
58
|
"@sanity/eslint-config-i18n": "^1.1.0",
|
|
59
59
|
"@sanity/eslint-config-studio": "^4.0.0",
|
|
60
60
|
"@sanity/pkg-utils": "^6.10.9",
|
|
61
|
-
"@sanity/schema": "^3.
|
|
61
|
+
"@sanity/schema": "^3.54.0",
|
|
62
62
|
"@sanity/test": "0.0.1-alpha.1",
|
|
63
|
-
"@sanity/types": "^3.
|
|
63
|
+
"@sanity/types": "^3.54.0",
|
|
64
64
|
"@sanity/ui": "^2.8.8",
|
|
65
|
-
"@sanity/util": "^3.
|
|
65
|
+
"@sanity/util": "^3.54.0",
|
|
66
66
|
"@testing-library/dom": "^10.4.0",
|
|
67
67
|
"@testing-library/react": "^16.0.0",
|
|
68
68
|
"@types/debug": "^4.1.5",
|
|
@@ -74,8 +74,8 @@
|
|
|
74
74
|
"@types/react": "^18.3.3",
|
|
75
75
|
"@types/react-dom": "^18.3.0",
|
|
76
76
|
"@types/ws": "~8.5.12",
|
|
77
|
-
"@typescript-eslint/eslint-plugin": "^8.0
|
|
78
|
-
"@typescript-eslint/parser": "^8.0
|
|
77
|
+
"@typescript-eslint/eslint-plugin": "^8.1.0",
|
|
78
|
+
"@typescript-eslint/parser": "^8.1.0",
|
|
79
79
|
"@vitejs/plugin-react": "^4.3.1",
|
|
80
80
|
"dotenv": "^16.4.5",
|
|
81
81
|
"eslint": "^8.57.0",
|
|
@@ -84,10 +84,10 @@
|
|
|
84
84
|
"eslint-import-resolver-typescript": "^3.6.1",
|
|
85
85
|
"eslint-plugin-import": "^2.29.1",
|
|
86
86
|
"eslint-plugin-prettier": "^5.2.1",
|
|
87
|
-
"eslint-plugin-react-compiler": "0.0.0-experimental-
|
|
87
|
+
"eslint-plugin-react-compiler": "0.0.0-experimental-d0e920e-20240815",
|
|
88
88
|
"eslint-plugin-tsdoc": "^0.3.0",
|
|
89
89
|
"eslint-plugin-unicorn": "^55.0.0",
|
|
90
|
-
"eslint-plugin-unused-imports": "^4.
|
|
90
|
+
"eslint-plugin-unused-imports": "^4.1.3",
|
|
91
91
|
"express": "^4.19.2",
|
|
92
92
|
"express-ws": "^5.0.2",
|
|
93
93
|
"jest": "^29.7.0",
|
|
@@ -99,9 +99,9 @@
|
|
|
99
99
|
"react-dom": "^18.3.1",
|
|
100
100
|
"rxjs": "^7.8.1",
|
|
101
101
|
"styled-components": "^6.1.12",
|
|
102
|
-
"tsx": "^4.
|
|
102
|
+
"tsx": "^4.17.0",
|
|
103
103
|
"typescript": "5.5.4",
|
|
104
|
-
"vite": "^5.
|
|
104
|
+
"vite": "^5.4.1"
|
|
105
105
|
},
|
|
106
106
|
"peerDependencies": {
|
|
107
107
|
"@sanity/block-tools": "^3.47.1",
|
|
@@ -11,7 +11,6 @@ import {validateValue} from '../../utils/validateValue'
|
|
|
11
11
|
import {toSlateValue, VOID_CHILD_KEY} from '../../utils/values'
|
|
12
12
|
import {isChangingLocally, isChangingRemotely, withRemoteChanges} from '../../utils/withChanges'
|
|
13
13
|
import {withoutPatching} from '../../utils/withoutPatching'
|
|
14
|
-
import {withPreserveKeys} from '../../utils/withPreserveKeys'
|
|
15
14
|
import {withoutSaving} from '../plugins/createWithUndoRedo'
|
|
16
15
|
import {type PortableTextEditor} from '../PortableTextEditor'
|
|
17
16
|
|
|
@@ -184,10 +183,8 @@ export function useSyncValue(
|
|
|
184
183
|
currentBlock,
|
|
185
184
|
)
|
|
186
185
|
if (validation.valid || validation.resolution?.autoResolve) {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
at: [currentBlockIndex],
|
|
190
|
-
})
|
|
186
|
+
Transforms.insertNodes(slateEditor, currentBlock, {
|
|
187
|
+
at: [currentBlockIndex],
|
|
191
188
|
})
|
|
192
189
|
} else {
|
|
193
190
|
debug('Invalid', validation)
|
|
@@ -267,9 +264,7 @@ function _replaceBlock(
|
|
|
267
264
|
Transforms.deselect(slateEditor)
|
|
268
265
|
}
|
|
269
266
|
Transforms.removeNodes(slateEditor, {at: [currentBlockIndex]})
|
|
270
|
-
|
|
271
|
-
Transforms.insertNodes(slateEditor, currentBlock, {at: [currentBlockIndex]})
|
|
272
|
-
})
|
|
267
|
+
Transforms.insertNodes(slateEditor, currentBlock, {at: [currentBlockIndex]})
|
|
273
268
|
slateEditor.onChange()
|
|
274
269
|
if (selectionFocusOnBlock) {
|
|
275
270
|
Transforms.select(slateEditor, currentSelection)
|
|
@@ -350,21 +345,17 @@ function _updateBlock(
|
|
|
350
345
|
Transforms.removeNodes(slateEditor, {
|
|
351
346
|
at: [currentBlockIndex, currentBlockChildIndex],
|
|
352
347
|
})
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
at: [currentBlockIndex, currentBlockChildIndex],
|
|
356
|
-
})
|
|
348
|
+
Transforms.insertNodes(slateEditor, currentBlockChild as Node, {
|
|
349
|
+
at: [currentBlockIndex, currentBlockChildIndex],
|
|
357
350
|
})
|
|
358
351
|
slateEditor.onChange()
|
|
359
352
|
// Insert it if it didn't exist before
|
|
360
353
|
} else if (!oldBlockChild) {
|
|
361
354
|
debug('Inserting new child', currentBlockChild)
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
at: [currentBlockIndex, currentBlockChildIndex],
|
|
365
|
-
})
|
|
366
|
-
slateEditor.onChange()
|
|
355
|
+
Transforms.insertNodes(slateEditor, currentBlockChild as Node, {
|
|
356
|
+
at: [currentBlockIndex, currentBlockChildIndex],
|
|
367
357
|
})
|
|
358
|
+
slateEditor.onChange()
|
|
368
359
|
}
|
|
369
360
|
}
|
|
370
361
|
})
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import {type PortableTextSlateEditor} from '../../types/editor'
|
|
2
|
+
import {isChangingRemotely} from '../../utils/withChanges'
|
|
3
|
+
import {isRedoing, isUndoing} from '../../utils/withUndoRedo'
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* This plugin makes sure that the PTE maxBlocks prop is respected
|
|
@@ -8,6 +10,24 @@ export function createWithMaxBlocks(maxBlocks: number) {
|
|
|
8
10
|
return function withMaxBlocks(editor: PortableTextSlateEditor): PortableTextSlateEditor {
|
|
9
11
|
const {apply} = editor
|
|
10
12
|
editor.apply = (operation) => {
|
|
13
|
+
/**
|
|
14
|
+
* We don't want to run any side effects when the editor is processing
|
|
15
|
+
* remote changes.
|
|
16
|
+
*/
|
|
17
|
+
if (isChangingRemotely(editor)) {
|
|
18
|
+
apply(operation)
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* We don't want to run any side effects when the editor is undoing or
|
|
24
|
+
* redoing operations.
|
|
25
|
+
*/
|
|
26
|
+
if (isUndoing(editor) || isRedoing(editor)) {
|
|
27
|
+
apply(operation)
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
11
31
|
const rows = maxBlocks
|
|
12
32
|
if (rows > 0 && editor.children.length >= rows) {
|
|
13
33
|
if (
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import {Editor, Element, Node, Transforms} from 'slate'
|
|
2
2
|
|
|
3
3
|
import {type PortableTextMemberSchemaTypes, type PortableTextSlateEditor} from '../../types/editor'
|
|
4
|
-
import {
|
|
4
|
+
import {isChangingRemotely} from '../../utils/withChanges'
|
|
5
|
+
import {isRedoing, isUndoing} from '../../utils/withUndoRedo'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* This plugin makes sure that every new node in the editor get a new _key prop when created
|
|
@@ -12,23 +13,36 @@ export function createWithObjectKeys(
|
|
|
12
13
|
keyGenerator: () => string,
|
|
13
14
|
) {
|
|
14
15
|
return function withKeys(editor: PortableTextSlateEditor): PortableTextSlateEditor {
|
|
15
|
-
PRESERVE_KEYS.set(editor, false)
|
|
16
16
|
const {apply, normalizeNode} = editor
|
|
17
17
|
|
|
18
|
-
// The apply function can be called with a scope (withPreserveKeys) that will
|
|
19
|
-
// preserve keys for the produced nodes if they have a _key property set already.
|
|
20
18
|
// The default behavior is to always generate a new key here.
|
|
21
19
|
// For example, when undoing and redoing we want to retain the keys, but
|
|
22
20
|
// when we create a new bold span by splitting a non-bold-span we want the produced node to get a new key.
|
|
23
21
|
editor.apply = (operation) => {
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
/**
|
|
23
|
+
* We don't want to run any side effects when the editor is processing
|
|
24
|
+
* remote changes.
|
|
25
|
+
*/
|
|
26
|
+
if (isChangingRemotely(editor)) {
|
|
27
|
+
apply(operation)
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* We don't want to run any side effects when the editor is undoing or
|
|
33
|
+
* redoing operations.
|
|
34
|
+
*/
|
|
35
|
+
if (isUndoing(editor) || isRedoing(editor)) {
|
|
36
|
+
apply(operation)
|
|
37
|
+
return
|
|
38
|
+
}
|
|
26
39
|
|
|
40
|
+
if (operation.type === 'split_node') {
|
|
27
41
|
apply({
|
|
28
42
|
...operation,
|
|
29
43
|
properties: {
|
|
30
44
|
...operation.properties,
|
|
31
|
-
|
|
45
|
+
_key: keyGenerator(),
|
|
32
46
|
},
|
|
33
47
|
})
|
|
34
48
|
|
|
@@ -36,15 +50,12 @@ export function createWithObjectKeys(
|
|
|
36
50
|
}
|
|
37
51
|
|
|
38
52
|
if (operation.type === 'insert_node') {
|
|
39
|
-
// Must be given a new key or adding/removing marks while typing gets in trouble (duped keys)!
|
|
40
|
-
const withNewKey = !isPreservingKeys(editor) || !('_key' in operation.node)
|
|
41
|
-
|
|
42
53
|
if (!Editor.isEditor(operation.node)) {
|
|
43
54
|
apply({
|
|
44
55
|
...operation,
|
|
45
56
|
node: {
|
|
46
57
|
...operation.node,
|
|
47
|
-
|
|
58
|
+
_key: keyGenerator(),
|
|
48
59
|
},
|
|
49
60
|
})
|
|
50
61
|
|
|
@@ -27,7 +27,6 @@ import {fromSlateValue, isEqualToEmptyEditor} from '../../utils/values'
|
|
|
27
27
|
import {IS_PROCESSING_REMOTE_CHANGES, KEY_TO_VALUE_ELEMENT} from '../../utils/weakMaps'
|
|
28
28
|
import {withRemoteChanges} from '../../utils/withChanges'
|
|
29
29
|
import {isPatching, PATCHING, withoutPatching} from '../../utils/withoutPatching'
|
|
30
|
-
import {withPreserveKeys} from '../../utils/withPreserveKeys'
|
|
31
30
|
import {withoutSaving} from './createWithUndoRedo'
|
|
32
31
|
|
|
33
32
|
const debug = debugWithName('plugin:withPatches')
|
|
@@ -117,11 +116,9 @@ export function createWithPatches({
|
|
|
117
116
|
Editor.withoutNormalizing(editor, () => {
|
|
118
117
|
withoutPatching(editor, () => {
|
|
119
118
|
withoutSaving(editor, () => {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
changed = applyPatch(editor, patch)
|
|
124
|
-
})
|
|
119
|
+
patches.forEach((patch) => {
|
|
120
|
+
if (debug.enabled) debug(`Handling remote patch ${JSON.stringify(patch)}`)
|
|
121
|
+
changed = applyPatch(editor, patch)
|
|
125
122
|
})
|
|
126
123
|
})
|
|
127
124
|
})
|
|
@@ -3,6 +3,8 @@ import {Editor, Path} from 'slate'
|
|
|
3
3
|
import {type PortableTextSlateEditor} from '../../types/editor'
|
|
4
4
|
import {type SlateTextBlock, type VoidElement} from '../../types/slate'
|
|
5
5
|
import {debugWithName} from '../../utils/debug'
|
|
6
|
+
import {isChangingRemotely} from '../../utils/withChanges'
|
|
7
|
+
import {isRedoing, isUndoing} from '../../utils/withUndoRedo'
|
|
6
8
|
|
|
7
9
|
const debug = debugWithName('plugin:withPlaceholderBlock')
|
|
8
10
|
|
|
@@ -17,6 +19,24 @@ export function createWithPlaceholderBlock(): (
|
|
|
17
19
|
const {apply} = editor
|
|
18
20
|
|
|
19
21
|
editor.apply = (op) => {
|
|
22
|
+
/**
|
|
23
|
+
* We don't want to run any side effects when the editor is processing
|
|
24
|
+
* remote changes.
|
|
25
|
+
*/
|
|
26
|
+
if (isChangingRemotely(editor)) {
|
|
27
|
+
apply(op)
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* We don't want to run any side effects when the editor is undoing or
|
|
33
|
+
* redoing operations.
|
|
34
|
+
*/
|
|
35
|
+
if (isUndoing(editor) || isRedoing(editor)) {
|
|
36
|
+
apply(op)
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
20
40
|
if (op.type === 'remove_node') {
|
|
21
41
|
const node = op.node as SlateTextBlock | VoidElement
|
|
22
42
|
if (op.path[0] === 0 && Editor.isVoid(editor, node)) {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import {isPortableTextBlock, isPortableTextSpan} from '@portabletext/toolkit'
|
|
10
10
|
import {isEqual, uniq} from 'lodash'
|
|
11
11
|
import {type Subject} from 'rxjs'
|
|
12
|
-
import {type Descendant, Editor, Element, Path, Range, Text, Transforms} from 'slate'
|
|
12
|
+
import {type Descendant, Editor, Element, Node, Path, Range, Text, Transforms} from 'slate'
|
|
13
13
|
|
|
14
14
|
import {
|
|
15
15
|
type EditorChange,
|
|
@@ -19,7 +19,8 @@ import {
|
|
|
19
19
|
import {debugWithName} from '../../utils/debug'
|
|
20
20
|
import {toPortableTextRange} from '../../utils/ranges'
|
|
21
21
|
import {EMPTY_MARKS} from '../../utils/values'
|
|
22
|
-
import {
|
|
22
|
+
import {isChangingRemotely} from '../../utils/withChanges'
|
|
23
|
+
import {isRedoing, isUndoing} from '../../utils/withUndoRedo'
|
|
23
24
|
|
|
24
25
|
const debug = debugWithName('plugin:withPortableTextMarkModel')
|
|
25
26
|
|
|
@@ -53,29 +54,38 @@ export function createWithPortableTextMarkModel(
|
|
|
53
54
|
|
|
54
55
|
// Extend Slate's default normalization. Merge spans with same set of .marks when doing merge_node operations, and clean up markDefs / marks
|
|
55
56
|
editor.normalizeNode = (nodeEntry) => {
|
|
56
|
-
normalizeNode(nodeEntry)
|
|
57
|
-
if (
|
|
58
|
-
editor.operations.some((op) =>
|
|
59
|
-
[
|
|
60
|
-
'insert_node',
|
|
61
|
-
'insert_text',
|
|
62
|
-
'merge_node',
|
|
63
|
-
'remove_node',
|
|
64
|
-
'remove_text',
|
|
65
|
-
'set_node',
|
|
66
|
-
].includes(op.type),
|
|
67
|
-
)
|
|
68
|
-
) {
|
|
69
|
-
mergeSpans(editor)
|
|
70
|
-
}
|
|
71
57
|
const [node, path] = nodeEntry
|
|
58
|
+
|
|
72
59
|
const isSpan = Text.isText(node) && node._type === types.span.name
|
|
73
60
|
const isTextBlock = editor.isTextBlock(node)
|
|
61
|
+
|
|
62
|
+
if (editor.isTextBlock(node)) {
|
|
63
|
+
const children = Node.children(editor, path)
|
|
64
|
+
|
|
65
|
+
for (const [child, childPath] of children) {
|
|
66
|
+
const nextNode = node.children[childPath[1] + 1]
|
|
67
|
+
|
|
68
|
+
if (
|
|
69
|
+
editor.isTextSpan(child) &&
|
|
70
|
+
editor.isTextSpan(nextNode) &&
|
|
71
|
+
isEqual(child.marks, nextNode.marks)
|
|
72
|
+
) {
|
|
73
|
+
debug(
|
|
74
|
+
'Merging spans',
|
|
75
|
+
JSON.stringify(child, null, 2),
|
|
76
|
+
JSON.stringify(nextNode, null, 2),
|
|
77
|
+
)
|
|
78
|
+
Transforms.mergeNodes(editor, {at: [childPath[0], childPath[1] + 1], voids: true})
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
74
84
|
if (isSpan || isTextBlock) {
|
|
75
85
|
if (isSpan && !Array.isArray(node.marks)) {
|
|
76
86
|
debug('Adding .marks to span node')
|
|
77
87
|
Transforms.setNodes(editor, {marks: []}, {at: path})
|
|
78
|
-
|
|
88
|
+
return
|
|
79
89
|
}
|
|
80
90
|
const hasSpanMarks = isSpan && (node.marks || []).length > 0
|
|
81
91
|
if (hasSpanMarks) {
|
|
@@ -99,7 +109,7 @@ export function createWithPortableTextMarkModel(
|
|
|
99
109
|
{marks: spanMarks.filter((mark) => !orphanedMarks.includes(mark))},
|
|
100
110
|
{at: path},
|
|
101
111
|
)
|
|
102
|
-
|
|
112
|
+
return
|
|
103
113
|
}
|
|
104
114
|
}
|
|
105
115
|
}
|
|
@@ -123,7 +133,7 @@ export function createWithPortableTextMarkModel(
|
|
|
123
133
|
// eslint-disable-next-line max-depth
|
|
124
134
|
if (!isNormalized) {
|
|
125
135
|
Transforms.setNodes(editor, {markDefs: newMarkDefs}, {at: targetPath, voids: false})
|
|
126
|
-
|
|
136
|
+
return
|
|
127
137
|
}
|
|
128
138
|
}
|
|
129
139
|
}
|
|
@@ -147,7 +157,7 @@ export function createWithPortableTextMarkModel(
|
|
|
147
157
|
{markDefs: uniq([...oldDefs, ...op.properties.markDefs])},
|
|
148
158
|
{at: targetPath, voids: false},
|
|
149
159
|
)
|
|
150
|
-
|
|
160
|
+
return
|
|
151
161
|
}
|
|
152
162
|
}
|
|
153
163
|
// Make sure marks are reset, if a block is split at the end.
|
|
@@ -168,7 +178,7 @@ export function createWithPortableTextMarkModel(
|
|
|
168
178
|
child.marks.length > 0
|
|
169
179
|
) {
|
|
170
180
|
Transforms.setNodes(editor, {marks: []}, {at: childPath, voids: false})
|
|
171
|
-
|
|
181
|
+
return
|
|
172
182
|
}
|
|
173
183
|
}
|
|
174
184
|
// Make sure markDefs are reset, if a block is split at start.
|
|
@@ -191,7 +201,7 @@ export function createWithPortableTextMarkModel(
|
|
|
191
201
|
(!block.children[0].marks || block.children[0].marks.length === 0)
|
|
192
202
|
) {
|
|
193
203
|
Transforms.setNodes(editor, {markDefs: []}, {at: blockPath})
|
|
194
|
-
|
|
204
|
+
return
|
|
195
205
|
}
|
|
196
206
|
}
|
|
197
207
|
}
|
|
@@ -202,7 +212,7 @@ export function createWithPortableTextMarkModel(
|
|
|
202
212
|
(!node.marks || (node.marks.length > 0 && node.text === ''))
|
|
203
213
|
) {
|
|
204
214
|
Transforms.setNodes(editor, {marks: []}, {at: path, voids: false})
|
|
205
|
-
|
|
215
|
+
return
|
|
206
216
|
}
|
|
207
217
|
}
|
|
208
218
|
// Check consistency of markDefs (unless we are merging two nodes)
|
|
@@ -228,12 +238,32 @@ export function createWithPortableTextMarkModel(
|
|
|
228
238
|
},
|
|
229
239
|
{at: path},
|
|
230
240
|
)
|
|
231
|
-
|
|
241
|
+
return
|
|
232
242
|
}
|
|
233
243
|
}
|
|
244
|
+
|
|
245
|
+
normalizeNode(nodeEntry)
|
|
234
246
|
}
|
|
235
247
|
|
|
236
248
|
editor.apply = (op) => {
|
|
249
|
+
/**
|
|
250
|
+
* We don't want to run any side effects when the editor is processing
|
|
251
|
+
* remote changes.
|
|
252
|
+
*/
|
|
253
|
+
if (isChangingRemotely(editor)) {
|
|
254
|
+
apply(op)
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* We don't want to run any side effects when the editor is undoing or
|
|
260
|
+
* redoing operations.
|
|
261
|
+
*/
|
|
262
|
+
if (isUndoing(editor) || isRedoing(editor)) {
|
|
263
|
+
apply(op)
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
|
|
237
267
|
// Special hook before inserting text at the end of an annotation.
|
|
238
268
|
if (op.type === 'insert_text') {
|
|
239
269
|
const {selection} = editor
|
|
@@ -295,21 +325,10 @@ export function createWithPortableTextMarkModel(
|
|
|
295
325
|
const deletingFromTheEnd = op.offset + op.text.length === node.text.length
|
|
296
326
|
|
|
297
327
|
if (nodeHasAnnotations && deletingPartOfTheNode && deletingFromTheEnd) {
|
|
298
|
-
/**
|
|
299
|
-
* If all of these conditions match then override the ordinary
|
|
300
|
-
* `remove_text` operation and turn it into `split_nodes` followed
|
|
301
|
-
* by `remove_nodes`. This is so if the operation can be properly
|
|
302
|
-
* undone. Undoing a `remove_text` results in an `insert_text` and
|
|
303
|
-
* we want to bail out of that in this exact scenario to make sure
|
|
304
|
-
* the inserted text is annotated. (See custom logic regarding
|
|
305
|
-
* `insert_text`)
|
|
306
|
-
*/
|
|
307
328
|
Editor.withoutNormalizing(editor, () => {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
at: {path: op.path, offset: op.offset},
|
|
312
|
-
})
|
|
329
|
+
Transforms.splitNodes(editor, {
|
|
330
|
+
match: Text.isText,
|
|
331
|
+
at: {path: op.path, offset: op.offset},
|
|
313
332
|
})
|
|
314
333
|
Transforms.removeNodes(editor, {at: Path.next(op.path)})
|
|
315
334
|
})
|
|
@@ -317,6 +336,24 @@ export function createWithPortableTextMarkModel(
|
|
|
317
336
|
editor.onChange()
|
|
318
337
|
return
|
|
319
338
|
}
|
|
339
|
+
|
|
340
|
+
const deletingAllText = op.offset === 0 && deletingFromTheEnd
|
|
341
|
+
|
|
342
|
+
if (nodeHasAnnotations && deletingAllText) {
|
|
343
|
+
const marksWithoutAnnotationMarks: string[] = (
|
|
344
|
+
{
|
|
345
|
+
...(Editor.marks(editor) || {}),
|
|
346
|
+
}.marks || []
|
|
347
|
+
).filter((mark) => decorators.includes(mark))
|
|
348
|
+
|
|
349
|
+
Editor.withoutNormalizing(editor, () => {
|
|
350
|
+
apply(op)
|
|
351
|
+
Transforms.setNodes(editor, {marks: marksWithoutAnnotationMarks}, {at: op.path})
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
editor.onChange()
|
|
355
|
+
return
|
|
356
|
+
}
|
|
320
357
|
}
|
|
321
358
|
}
|
|
322
359
|
|
|
@@ -327,34 +364,35 @@ export function createWithPortableTextMarkModel(
|
|
|
327
364
|
editor.addMark = (mark: string) => {
|
|
328
365
|
if (editor.selection) {
|
|
329
366
|
if (Range.isExpanded(editor.selection)) {
|
|
330
|
-
// Split if needed
|
|
331
|
-
Transforms.setNodes(editor, {}, {match: Text.isText, split: true})
|
|
332
|
-
// Use new selection
|
|
333
|
-
const splitTextNodes = [
|
|
334
|
-
...Editor.nodes(editor, {at: editor.selection, match: Text.isText}),
|
|
335
|
-
]
|
|
336
|
-
const shouldRemoveMark = splitTextNodes.every((node) => node[0].marks?.includes(mark))
|
|
337
|
-
|
|
338
|
-
if (shouldRemoveMark) {
|
|
339
|
-
editor.removeMark(mark)
|
|
340
|
-
return editor
|
|
341
|
-
}
|
|
342
367
|
Editor.withoutNormalizing(editor, () => {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
)
|
|
355
|
-
}
|
|
368
|
+
// Split if needed
|
|
369
|
+
Transforms.setNodes(editor, {}, {match: Text.isText, split: true})
|
|
370
|
+
// Use new selection
|
|
371
|
+
const splitTextNodes = Range.isRange(editor.selection)
|
|
372
|
+
? [...Editor.nodes(editor, {at: editor.selection, match: Text.isText})]
|
|
373
|
+
: []
|
|
374
|
+
const shouldRemoveMark =
|
|
375
|
+
splitTextNodes.length > 1 &&
|
|
376
|
+
splitTextNodes.every((node) => node[0].marks?.includes(mark))
|
|
377
|
+
|
|
378
|
+
if (shouldRemoveMark) {
|
|
379
|
+
editor.removeMark(mark)
|
|
380
|
+
} else {
|
|
381
|
+
splitTextNodes.forEach(([node, path]) => {
|
|
382
|
+
const marks = [
|
|
383
|
+
...(Array.isArray(node.marks) ? node.marks : []).filter(
|
|
384
|
+
(eMark: string) => eMark !== mark,
|
|
385
|
+
),
|
|
386
|
+
mark,
|
|
387
|
+
]
|
|
388
|
+
Transforms.setNodes(
|
|
389
|
+
editor,
|
|
390
|
+
{marks},
|
|
391
|
+
{at: path, match: Text.isText, split: true, hanging: true},
|
|
392
|
+
)
|
|
393
|
+
})
|
|
394
|
+
}
|
|
356
395
|
})
|
|
357
|
-
Editor.normalize(editor)
|
|
358
396
|
} else {
|
|
359
397
|
const existingMarks: string[] =
|
|
360
398
|
{
|
|
@@ -460,36 +498,4 @@ export function createWithPortableTextMarkModel(
|
|
|
460
498
|
}
|
|
461
499
|
return editor
|
|
462
500
|
}
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* Normalize re-marked spans in selection
|
|
466
|
-
*/
|
|
467
|
-
function mergeSpans(editor: PortableTextSlateEditor) {
|
|
468
|
-
const {selection} = editor
|
|
469
|
-
|
|
470
|
-
if (selection) {
|
|
471
|
-
const textNodesInSelection = Array.from(
|
|
472
|
-
Editor.nodes(editor, {
|
|
473
|
-
at: Editor.range(editor, [selection.anchor.path[0]], [selection.focus.path[0]]),
|
|
474
|
-
match: Text.isText,
|
|
475
|
-
reverse: true,
|
|
476
|
-
}),
|
|
477
|
-
)
|
|
478
|
-
|
|
479
|
-
for (const [node, path] of textNodesInSelection) {
|
|
480
|
-
const [parent] = path.length > 1 ? Editor.node(editor, Path.parent(path)) : [undefined]
|
|
481
|
-
const nextPath = [path[0], path[1] + 1]
|
|
482
|
-
|
|
483
|
-
if (editor.isTextBlock(parent)) {
|
|
484
|
-
const nextNode = parent.children[nextPath[1]]
|
|
485
|
-
|
|
486
|
-
if (Text.isText(nextNode) && isEqual(nextNode.marks, node.marks)) {
|
|
487
|
-
debug('Merging spans')
|
|
488
|
-
Transforms.mergeNodes(editor, {at: nextPath, voids: true})
|
|
489
|
-
editor.onChange()
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
501
|
}
|
|
@@ -12,7 +12,7 @@ import {type Descendant, Editor, Operation, Path, type SelectionOperation, Trans
|
|
|
12
12
|
import {type PatchObservable, type PortableTextSlateEditor} from '../../types/editor'
|
|
13
13
|
import {debugWithName} from '../../utils/debug'
|
|
14
14
|
import {fromSlateValue} from '../../utils/values'
|
|
15
|
-
import {
|
|
15
|
+
import {setIsRedoing, setIsUndoing, withRedoing, withUndoing} from '../../utils/withUndoRedo'
|
|
16
16
|
|
|
17
17
|
const debug = debugWithName('plugin:withUndoRedo')
|
|
18
18
|
const debugVerbose = debug.enabled && false
|
|
@@ -150,7 +150,7 @@ export function createWithUndoRedo(
|
|
|
150
150
|
|
|
151
151
|
try {
|
|
152
152
|
Editor.withoutNormalizing(editor, () => {
|
|
153
|
-
|
|
153
|
+
withUndoing(editor, () => {
|
|
154
154
|
withoutSaving(editor, () => {
|
|
155
155
|
reversedOperations.forEach((op) => {
|
|
156
156
|
editor.apply(op)
|
|
@@ -166,6 +166,7 @@ export function createWithUndoRedo(
|
|
|
166
166
|
Transforms.deselect(editor)
|
|
167
167
|
editor.history = {undos: [], redos: []}
|
|
168
168
|
SAVING.set(editor, true)
|
|
169
|
+
setIsUndoing(editor, false)
|
|
169
170
|
editor.onChange()
|
|
170
171
|
return
|
|
171
172
|
}
|
|
@@ -195,7 +196,7 @@ export function createWithUndoRedo(
|
|
|
195
196
|
})
|
|
196
197
|
try {
|
|
197
198
|
Editor.withoutNormalizing(editor, () => {
|
|
198
|
-
|
|
199
|
+
withRedoing(editor, () => {
|
|
199
200
|
withoutSaving(editor, () => {
|
|
200
201
|
// eslint-disable-next-line max-nested-callbacks
|
|
201
202
|
transformedOperations.forEach((op) => {
|
|
@@ -212,6 +213,7 @@ export function createWithUndoRedo(
|
|
|
212
213
|
Transforms.deselect(editor)
|
|
213
214
|
editor.history = {undos: [], redos: []}
|
|
214
215
|
SAVING.set(editor, true)
|
|
216
|
+
setIsRedoing(editor, false)
|
|
215
217
|
editor.onChange()
|
|
216
218
|
return
|
|
217
219
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {type Editor} from 'slate'
|
|
2
|
+
|
|
3
|
+
const IS_UDOING: WeakMap<Editor, boolean | undefined> = new WeakMap()
|
|
4
|
+
const IS_REDOING: WeakMap<Editor, boolean | undefined> = new WeakMap()
|
|
5
|
+
|
|
6
|
+
export function withUndoing(editor: Editor, fn: () => void) {
|
|
7
|
+
const prev = isUndoing(editor)
|
|
8
|
+
IS_UDOING.set(editor, true)
|
|
9
|
+
fn()
|
|
10
|
+
IS_UDOING.set(editor, prev)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isUndoing(editor: Editor) {
|
|
14
|
+
return IS_UDOING.get(editor) ?? false
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function setIsUndoing(editor: Editor, isUndoing: boolean) {
|
|
18
|
+
IS_UDOING.set(editor, isUndoing)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function withRedoing(editor: Editor, fn: () => void) {
|
|
22
|
+
const prev = isRedoing(editor)
|
|
23
|
+
IS_REDOING.set(editor, true)
|
|
24
|
+
fn()
|
|
25
|
+
IS_REDOING.set(editor, prev)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isRedoing(editor: Editor) {
|
|
29
|
+
return IS_REDOING.get(editor) ?? false
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function setIsRedoing(editor: Editor, isRedoing: boolean) {
|
|
33
|
+
IS_REDOING.set(editor, isRedoing)
|
|
34
|
+
}
|