@portabletext/editor 1.0.19 → 1.1.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/index.d.mts +142 -67
- package/lib/index.d.ts +142 -67
- package/lib/index.esm.js +1130 -371
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +1130 -371
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +1130 -371
- package/lib/index.mjs.map +1 -1
- package/package.json +4 -18
- package/src/editor/Editable.tsx +128 -55
- package/src/editor/PortableTextEditor.tsx +66 -32
- package/src/editor/__tests__/PortableTextEditor.test.tsx +44 -18
- package/src/editor/__tests__/PortableTextEditorTester.tsx +50 -38
- package/src/editor/__tests__/RangeDecorations.test.tsx +4 -6
- package/src/editor/__tests__/handleClick.test.tsx +28 -9
- package/src/editor/__tests__/insert-block.test.tsx +24 -8
- package/src/editor/__tests__/pteWarningsSelfSolving.test.tsx +31 -63
- package/src/editor/__tests__/utils.ts +10 -4
- package/src/editor/components/DraggableBlock.tsx +36 -13
- package/src/editor/components/Element.tsx +73 -33
- package/src/editor/components/Leaf.tsx +114 -76
- package/src/editor/components/SlateContainer.tsx +14 -7
- package/src/editor/components/Synchronizer.tsx +8 -5
- package/src/editor/hooks/usePortableTextEditor.ts +3 -3
- package/src/editor/hooks/usePortableTextEditorSelection.tsx +10 -4
- package/src/editor/hooks/useSyncValue.test.tsx +9 -4
- package/src/editor/hooks/useSyncValue.ts +198 -133
- package/src/editor/nodes/DefaultAnnotation.tsx +6 -4
- package/src/editor/nodes/DefaultObject.tsx +1 -1
- package/src/editor/plugins/__tests__/createWithInsertData.test.tsx +23 -8
- package/src/editor/plugins/__tests__/withEditableAPIDelete.test.tsx +26 -9
- package/src/editor/plugins/__tests__/withEditableAPIGetFragment.test.tsx +15 -5
- package/src/editor/plugins/__tests__/withEditableAPIInsert.test.tsx +60 -19
- package/src/editor/plugins/__tests__/withEditableAPISelectionsOverlapping.test.tsx +5 -3
- package/src/editor/plugins/__tests__/withPortableTextLists.test.tsx +4 -2
- package/src/editor/plugins/__tests__/withPortableTextMarkModel.test.tsx +61 -19
- package/src/editor/plugins/__tests__/withPortableTextSelections.test.tsx +6 -3
- package/src/editor/plugins/__tests__/withUndoRedo.test.tsx +30 -13
- package/src/editor/plugins/createWithEditableAPI.ts +361 -131
- package/src/editor/plugins/createWithHotKeys.ts +46 -130
- package/src/editor/plugins/createWithInsertBreak.ts +167 -28
- package/src/editor/plugins/createWithInsertData.ts +66 -30
- package/src/editor/plugins/createWithMaxBlocks.ts +6 -3
- package/src/editor/plugins/createWithObjectKeys.ts +7 -3
- package/src/editor/plugins/createWithPatches.ts +66 -24
- package/src/editor/plugins/createWithPlaceholderBlock.ts +9 -5
- package/src/editor/plugins/createWithPortableTextBlockStyle.ts +17 -7
- package/src/editor/plugins/createWithPortableTextLists.ts +21 -9
- package/src/editor/plugins/createWithPortableTextMarkModel.ts +217 -52
- package/src/editor/plugins/createWithPortableTextSelections.ts +11 -9
- package/src/editor/plugins/createWithSchemaTypes.ts +26 -10
- package/src/editor/plugins/createWithUndoRedo.ts +106 -27
- package/src/editor/plugins/createWithUtils.ts +33 -11
- package/src/editor/plugins/index.ts +34 -13
- package/src/types/editor.ts +73 -44
- package/src/types/options.ts +7 -5
- package/src/types/slate.ts +6 -6
- package/src/utils/__tests__/dmpToOperations.test.ts +41 -16
- package/src/utils/__tests__/operationToPatches.test.ts +4 -3
- package/src/utils/__tests__/patchToOperations.test.ts +16 -5
- package/src/utils/__tests__/ranges.test.ts +9 -4
- package/src/utils/__tests__/valueNormalization.test.tsx +12 -4
- package/src/utils/__tests__/values.test.ts +0 -1
- package/src/utils/applyPatch.ts +78 -29
- package/src/utils/getPortableTextMemberSchemaTypes.ts +38 -23
- package/src/utils/operationToPatches.ts +123 -44
- package/src/utils/paths.ts +26 -9
- package/src/utils/ranges.ts +16 -10
- package/src/utils/selection.ts +21 -9
- package/src/utils/ucs2Indices.ts +2 -2
- package/src/utils/validateValue.ts +118 -45
- package/src/utils/values.ts +38 -17
- package/src/utils/weakMaps.ts +20 -10
- package/src/utils/withChanges.ts +5 -3
- package/src/utils/withUndoRedo.ts +1 -1
- package/src/utils/withoutPatching.ts +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {describe, expect, test} from '@jest/globals'
|
|
2
|
-
import
|
|
2
|
+
import type {DiffMatchPatch} from '@portabletext/patches'
|
|
3
3
|
import {makeDiff, makePatches, stringifyPatches} from '@sanity/diff-match-patch'
|
|
4
4
|
import {
|
|
5
5
|
isPortableTextSpan,
|
|
@@ -9,9 +9,8 @@ import {
|
|
|
9
9
|
type PortableTextSpan,
|
|
10
10
|
type PortableTextTextBlock,
|
|
11
11
|
} from '@sanity/types'
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
import {type PortableTextSlateEditor} from '../../types/editor'
|
|
12
|
+
import type {Descendant, Operation} from 'slate'
|
|
13
|
+
import type {PortableTextSlateEditor} from '../../types/editor'
|
|
15
14
|
import {diffMatchPatch} from '../applyPatch'
|
|
16
15
|
|
|
17
16
|
describe('operationToPatches: diffMatchPatch', () => {
|
|
@@ -23,7 +22,9 @@ describe('operationToPatches: diffMatchPatch', () => {
|
|
|
23
22
|
test('should apply the most basic additive operation correctly', () => {
|
|
24
23
|
const source = 'Hello'
|
|
25
24
|
const target = 'Hello there'
|
|
26
|
-
const patch = getPteDmpPatch(
|
|
25
|
+
const patch = getPteDmpPatch(
|
|
26
|
+
stringifyPatches(makePatches(makeDiff(source, target))),
|
|
27
|
+
)
|
|
27
28
|
const editor = getMockEditor({text: source})
|
|
28
29
|
expect(diffMatchPatch(editor, patch)).toBe(true)
|
|
29
30
|
expect(editor.getText()).toBe(target)
|
|
@@ -32,7 +33,9 @@ describe('operationToPatches: diffMatchPatch', () => {
|
|
|
32
33
|
test('should apply the most basic removal operation correctly', () => {
|
|
33
34
|
const source = 'Hello there'
|
|
34
35
|
const target = 'Hello'
|
|
35
|
-
const patch = getPteDmpPatch(
|
|
36
|
+
const patch = getPteDmpPatch(
|
|
37
|
+
stringifyPatches(makePatches(makeDiff(source, target))),
|
|
38
|
+
)
|
|
36
39
|
const editor = getMockEditor({text: source})
|
|
37
40
|
expect(diffMatchPatch(editor, patch)).toBe(true)
|
|
38
41
|
expect(editor.getText()).toBe(target)
|
|
@@ -41,7 +44,9 @@ describe('operationToPatches: diffMatchPatch', () => {
|
|
|
41
44
|
test('should treat equality as noops', () => {
|
|
42
45
|
const source = 'Hello'
|
|
43
46
|
const target = 'Hello'
|
|
44
|
-
const patch = getPteDmpPatch(
|
|
47
|
+
const patch = getPteDmpPatch(
|
|
48
|
+
stringifyPatches(makePatches(makeDiff(source, target))),
|
|
49
|
+
)
|
|
45
50
|
const editor = getMockEditor({text: source})
|
|
46
51
|
expect(diffMatchPatch(editor, patch)).toBe(true)
|
|
47
52
|
expect(editor.getText()).toBe(target)
|
|
@@ -50,7 +55,9 @@ describe('operationToPatches: diffMatchPatch', () => {
|
|
|
50
55
|
test('should apply combined add + remove operations', () => {
|
|
51
56
|
const source = 'A quick brown fox jumps over the very lazy dog'
|
|
52
57
|
const target = 'The quick brown fox jumps over the lazy dog'
|
|
53
|
-
const patch = getPteDmpPatch(
|
|
58
|
+
const patch = getPteDmpPatch(
|
|
59
|
+
stringifyPatches(makePatches(makeDiff(source, target))),
|
|
60
|
+
)
|
|
54
61
|
const editor = getMockEditor({text: source})
|
|
55
62
|
expect(diffMatchPatch(editor, patch)).toBe(true)
|
|
56
63
|
expect(editor.getText()).toBe(target)
|
|
@@ -58,8 +65,11 @@ describe('operationToPatches: diffMatchPatch', () => {
|
|
|
58
65
|
|
|
59
66
|
test('should apply combined add + remove operations', () => {
|
|
60
67
|
const source = 'Many quick brown fox jumps over the very lazy dog'
|
|
61
|
-
const target =
|
|
62
|
-
|
|
68
|
+
const target =
|
|
69
|
+
'The many, quick, brown, foxes jumps over all of the lazy dogs'
|
|
70
|
+
const patch = getPteDmpPatch(
|
|
71
|
+
stringifyPatches(makePatches(makeDiff(source, target))),
|
|
72
|
+
)
|
|
63
73
|
const editor = getMockEditor({text: source})
|
|
64
74
|
expect(diffMatchPatch(editor, patch)).toBe(true)
|
|
65
75
|
expect(editor.getText()).toBe(target)
|
|
@@ -70,7 +80,9 @@ describe('operationToPatches: diffMatchPatch', () => {
|
|
|
70
80
|
const line2 = 'But the slow green frog jumps over the wild cat'
|
|
71
81
|
const source = [line1, line2, line1, line2].join('\n')
|
|
72
82
|
const target = [line2, line1, line2, line1].join('\n')
|
|
73
|
-
const patch = getPteDmpPatch(
|
|
83
|
+
const patch = getPteDmpPatch(
|
|
84
|
+
stringifyPatches(makePatches(makeDiff(source, target))),
|
|
85
|
+
)
|
|
74
86
|
const editor = getMockEditor({text: source})
|
|
75
87
|
expect(diffMatchPatch(editor, patch)).toBe(true)
|
|
76
88
|
expect(editor.getText()).toBe(target)
|
|
@@ -79,7 +91,9 @@ describe('operationToPatches: diffMatchPatch', () => {
|
|
|
79
91
|
test('should apply larger text differences correctly', () => {
|
|
80
92
|
const source = `Portable Text is a agnostic abstraction of "rich text" that can be stringified into any markup language, for instance HTML, Markdown, SSML, XML, etc. It's designed to be efficient for collaboration, and makes it possible to enrich rich text with data structures in depth.\n\nPortable Text is built on the idea of rich text as an array of blocks, themselves arrays of children spans. Each block can have a style and a set of mark dfinitions, which describe data structures distributed on the children spans. Portable Text also allows for inserting arbitrary data objects in the array, only requiring _type-key. Portable Text also allows for custom objects in the root array, enabling rendering environments to mix rich text with custom content types.\n\nPortable Text is a combination of arrays and objects. In its simplest form it's an array of objects with an array of children. Some definitions: \n- Block: Typically recognized as a section of a text, e.g. a paragraph or a heading.\n- Span: Piece of text with a set of marks, e.g. bold or italic.\n- Mark: A mark is a data structure that can be appliad to a span, e.g. a link or a comment.\n- Mark definition: A mark definition is a structure that describes a mark, a link or a comment.`
|
|
81
93
|
const target = `Portable Text is an agnostic abstraction of rich text that can be serialized into pretty much any markup language, be it HTML, Markdown, SSML, XML, etc. It is designed to be efficient for real-time collaborative interfaces, and makes it possible to annotate rich text with additional data structures recursively.\n\nPortable Text is built on the idea of rich text as an array of blocks, themselves arrays of child spans. Each block can have a style and a set of mark definitions, which describe data structures that can be applied on the children spans. Portable Text also allows for inserting arbitrary data objects in the array, only requiring _type-key. Portable Text also allows for custom content objects in the root array, enabling editing- and rendering environments to mix rich text with custom content types.\n\nPortable Text is a recursive composition of arrays and objects. In its simplest form it's an array of objects of a type with an array of children. Some definitions: \n- Block: A block is what's typically recognized as a section of a text, e.g. a paragraph or a heading.\n- Span: A span is a piece of text with a set of marks, e.g. bold or italic.\n- Mark: A mark is a data structure that can be applied to a span, e.g. a link or a comment.\n- Mark definition: A mark definition is a data structure that describes a mark, e.g. a link or a comment.`
|
|
82
|
-
const patch = getPteDmpPatch(
|
|
94
|
+
const patch = getPteDmpPatch(
|
|
95
|
+
stringifyPatches(makePatches(makeDiff(source, target))),
|
|
96
|
+
)
|
|
83
97
|
const editor = getMockEditor({text: source})
|
|
84
98
|
expect(diffMatchPatch(editor, patch)).toBe(true)
|
|
85
99
|
expect(editor.getText()).toBe(target)
|
|
@@ -88,7 +102,9 @@ describe('operationToPatches: diffMatchPatch', () => {
|
|
|
88
102
|
test('should apply offset text differences correctly', () => {
|
|
89
103
|
const source = `This string has changes, but they occur somewhere near the end. That means we need to use an offset to get at the change, we cannot just rely on equality segaments in the generated diff.`
|
|
90
104
|
const target = `This string has changes, but they occur somewhere near the end. That means we need to use an offset to get at the change, we cannot just rely on equality segments in the generated diff.`
|
|
91
|
-
const patch = getPteDmpPatch(
|
|
105
|
+
const patch = getPteDmpPatch(
|
|
106
|
+
stringifyPatches(makePatches(makeDiff(source, target))),
|
|
107
|
+
)
|
|
92
108
|
const editor = getMockEditor({text: source})
|
|
93
109
|
expect(diffMatchPatch(editor, patch)).toBe(true)
|
|
94
110
|
expect(editor.getText()).toBe(target)
|
|
@@ -115,13 +131,20 @@ function getMockEditor(options: MockEditorOptions): Pick<
|
|
|
115
131
|
> & {
|
|
116
132
|
getText: () => string
|
|
117
133
|
} {
|
|
118
|
-
let children: PortableTextBlock[] =
|
|
134
|
+
let children: PortableTextBlock[] =
|
|
135
|
+
'children' in options ? options.children : []
|
|
119
136
|
if (!('children' in options)) {
|
|
120
137
|
children = [
|
|
121
138
|
{
|
|
122
139
|
_type: 'block',
|
|
123
140
|
_key: 'bA',
|
|
124
|
-
children: [
|
|
141
|
+
children: [
|
|
142
|
+
{
|
|
143
|
+
_type: 'span',
|
|
144
|
+
_key: 's1',
|
|
145
|
+
text: 'text' in options ? options.text : '',
|
|
146
|
+
},
|
|
147
|
+
],
|
|
125
148
|
markDefs: [],
|
|
126
149
|
},
|
|
127
150
|
]
|
|
@@ -129,7 +152,9 @@ function getMockEditor(options: MockEditorOptions): Pick<
|
|
|
129
152
|
|
|
130
153
|
function getText(blockKey?: string) {
|
|
131
154
|
return children
|
|
132
|
-
.filter((child): child is PortableTextTextBlock =>
|
|
155
|
+
.filter((child): child is PortableTextTextBlock =>
|
|
156
|
+
isPortableTextTextBlock(child),
|
|
157
|
+
)
|
|
133
158
|
.filter((child) => (blockKey ? child._key === blockKey : true))
|
|
134
159
|
.flatMap((block) =>
|
|
135
160
|
block.children
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {beforeEach, describe, expect, it} from '@jest/globals'
|
|
2
|
-
import
|
|
2
|
+
import type {PortableTextTextBlock} from '@sanity/types'
|
|
3
3
|
import {createEditor, type Descendant} from 'slate'
|
|
4
|
-
|
|
5
4
|
import {PortableTextEditor, type PortableTextEditorProps} from '../..'
|
|
6
5
|
import {schemaType} from '../../editor/__tests__/PortableTextEditorTester'
|
|
7
6
|
import {defaultKeyGenerator} from '../../editor/hooks/usePortableTextEditorKeyGenerator'
|
|
@@ -14,7 +13,9 @@ const portableTextFeatures = getPortableTextMemberSchemaTypes(schemaType)
|
|
|
14
13
|
const operationToPatches = createOperationToPatches(portableTextFeatures)
|
|
15
14
|
|
|
16
15
|
const {editor} = withPlugins(createEditor(), {
|
|
17
|
-
portableTextEditor: new PortableTextEditor({
|
|
16
|
+
portableTextEditor: new PortableTextEditor({
|
|
17
|
+
schemaType,
|
|
18
|
+
} as PortableTextEditorProps),
|
|
18
19
|
keyGenerator: defaultKeyGenerator,
|
|
19
20
|
readOnly: false,
|
|
20
21
|
})
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import {beforeEach, describe, expect, it} from '@jest/globals'
|
|
2
|
-
import
|
|
2
|
+
import type {Patch} from '@portabletext/patches'
|
|
3
3
|
import {noop} from 'lodash'
|
|
4
4
|
import {createEditor, type Descendant} from 'slate'
|
|
5
|
-
|
|
6
5
|
import {keyGenerator, PortableTextEditor} from '../..'
|
|
7
6
|
import {schemaType} from '../../editor/__tests__/PortableTextEditorTester'
|
|
8
7
|
import {withPlugins} from '../../editor/plugins'
|
|
@@ -51,7 +50,11 @@ describe('operationToPatches', () => {
|
|
|
51
50
|
it('makes the correct operations for block objects', () => {
|
|
52
51
|
editor.children = createDefaultValue()
|
|
53
52
|
const patches = [
|
|
54
|
-
{
|
|
53
|
+
{
|
|
54
|
+
type: 'unset',
|
|
55
|
+
path: [{_key: 'c01739b0d03b'}, 'hotspot'],
|
|
56
|
+
origin: 'remote',
|
|
57
|
+
},
|
|
55
58
|
{type: 'unset', path: [{_key: 'c01739b0d03b'}, 'crop'], origin: 'remote'},
|
|
56
59
|
{
|
|
57
60
|
type: 'set',
|
|
@@ -114,7 +117,11 @@ describe('operationToPatches', () => {
|
|
|
114
117
|
},
|
|
115
118
|
]
|
|
116
119
|
const patches = [
|
|
117
|
-
{
|
|
120
|
+
{
|
|
121
|
+
type: 'insert',
|
|
122
|
+
path: [{_key: 'c01739b0d03b'}, 'nestedArray'],
|
|
123
|
+
origin: 'remote',
|
|
124
|
+
},
|
|
118
125
|
] as Patch[]
|
|
119
126
|
patches.forEach((p) => {
|
|
120
127
|
patchToOperations(editor, p)
|
|
@@ -173,7 +180,11 @@ describe('operationToPatches', () => {
|
|
|
173
180
|
},
|
|
174
181
|
]
|
|
175
182
|
const patches = [
|
|
176
|
-
{
|
|
183
|
+
{
|
|
184
|
+
type: 'unset',
|
|
185
|
+
path: [{_key: 'c01739b0d03b'}, 'nestedArray', 0],
|
|
186
|
+
origin: 'remote',
|
|
187
|
+
},
|
|
177
188
|
] as Patch[]
|
|
178
189
|
patches.forEach((p) => {
|
|
179
190
|
patchToOperations(editor, p)
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import {describe, expect, it} from '@jest/globals'
|
|
2
|
-
import
|
|
3
|
-
|
|
2
|
+
import type {InsertTextOperation, Range} from 'slate'
|
|
4
3
|
import {moveRangeByOperation} from '../ranges'
|
|
5
4
|
|
|
6
5
|
describe('moveRangeByOperation', () => {
|
|
7
6
|
it('should move range when inserting text in front of it', () => {
|
|
8
|
-
const range: Range = {
|
|
7
|
+
const range: Range = {
|
|
8
|
+
anchor: {path: [0, 0], offset: 1},
|
|
9
|
+
focus: {path: [0, 0], offset: 3},
|
|
10
|
+
}
|
|
9
11
|
const operation: InsertTextOperation = {
|
|
10
12
|
type: 'insert_text',
|
|
11
13
|
path: [0, 0],
|
|
@@ -13,6 +15,9 @@ describe('moveRangeByOperation', () => {
|
|
|
13
15
|
text: 'foo',
|
|
14
16
|
}
|
|
15
17
|
const newRange = moveRangeByOperation(range, operation)
|
|
16
|
-
expect(newRange).toEqual({
|
|
18
|
+
expect(newRange).toEqual({
|
|
19
|
+
anchor: {path: [0, 0], offset: 4},
|
|
20
|
+
focus: {path: [0, 0], offset: 6},
|
|
21
|
+
})
|
|
17
22
|
})
|
|
18
23
|
})
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {describe, expect, it, jest} from '@jest/globals'
|
|
2
2
|
import {render, waitFor} from '@testing-library/react'
|
|
3
3
|
import {createRef, type RefObject} from 'react'
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
PortableTextEditorTester,
|
|
6
|
+
schemaType,
|
|
7
|
+
} from '../../editor/__tests__/PortableTextEditorTester'
|
|
6
8
|
import {PortableTextEditor} from '../../editor/PortableTextEditor'
|
|
7
9
|
|
|
8
10
|
describe('values: normalization', () => {
|
|
@@ -36,8 +38,14 @@ describe('values: normalization', () => {
|
|
|
36
38
|
if (editorRef.current) {
|
|
37
39
|
PortableTextEditor.focus(editorRef.current)
|
|
38
40
|
PortableTextEditor.select(editorRef.current, {
|
|
39
|
-
focus: {
|
|
40
|
-
|
|
41
|
+
focus: {
|
|
42
|
+
path: [{_key: '5fc57af23597'}, 'children', {_key: 'be1c67c6971a'}],
|
|
43
|
+
offset: 0,
|
|
44
|
+
},
|
|
45
|
+
anchor: {
|
|
46
|
+
path: [{_key: '5fc57af23597'}, 'children', {_key: 'be1c67c6971a'}],
|
|
47
|
+
offset: 5,
|
|
48
|
+
},
|
|
41
49
|
})
|
|
42
50
|
PortableTextEditor.toggleMark(editorRef.current, 'strong')
|
|
43
51
|
expect(PortableTextEditor.getValue(editorRef.current)).toEqual([
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {describe, expect, it} from '@jest/globals'
|
|
2
|
-
|
|
3
2
|
import {schemaType} from '../../editor/__tests__/PortableTextEditorTester'
|
|
4
3
|
import {getPortableTextMemberSchemaTypes} from '../getPortableTextMemberSchemaTypes'
|
|
5
4
|
import {fromSlateValue, toSlateValue} from '../values'
|
package/src/utils/applyPatch.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* eslint-disable max-statements */
|
|
2
1
|
import {
|
|
3
2
|
applyAll,
|
|
4
3
|
type DiffMatchPatch,
|
|
@@ -8,24 +7,33 @@ import {
|
|
|
8
7
|
type UnsetPatch,
|
|
9
8
|
} from '@portabletext/patches'
|
|
10
9
|
import {
|
|
11
|
-
applyPatches as diffMatchPatchApplyPatches,
|
|
12
10
|
cleanupEfficiency,
|
|
13
11
|
DIFF_DELETE,
|
|
14
12
|
DIFF_EQUAL,
|
|
15
13
|
DIFF_INSERT,
|
|
14
|
+
applyPatches as diffMatchPatchApplyPatches,
|
|
16
15
|
makeDiff,
|
|
17
16
|
parsePatch,
|
|
18
17
|
} from '@sanity/diff-match-patch'
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
import type {
|
|
19
|
+
KeyedSegment,
|
|
20
|
+
Path,
|
|
21
|
+
PathSegment,
|
|
22
|
+
PortableTextBlock,
|
|
23
|
+
PortableTextChild,
|
|
25
24
|
} from '@sanity/types'
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
import {
|
|
26
|
+
Element,
|
|
27
|
+
Text,
|
|
28
|
+
Transforms,
|
|
29
|
+
type Descendant,
|
|
30
|
+
type Node,
|
|
31
|
+
type Path as SlatePath,
|
|
32
|
+
} from 'slate'
|
|
33
|
+
import type {
|
|
34
|
+
PortableTextMemberSchemaTypes,
|
|
35
|
+
PortableTextSlateEditor,
|
|
36
|
+
} from '../types/editor'
|
|
29
37
|
import {debugWithName} from './debug'
|
|
30
38
|
import {toSlateValue} from './values'
|
|
31
39
|
import {KEY_TO_SLATE_ELEMENT} from './weakMaps'
|
|
@@ -41,12 +49,14 @@ export function createApplyPatch(
|
|
|
41
49
|
): (editor: PortableTextSlateEditor, patch: Patch) => boolean {
|
|
42
50
|
let previousPatch: Patch | undefined
|
|
43
51
|
|
|
44
|
-
return
|
|
52
|
+
return (editor: PortableTextSlateEditor, patch: Patch): boolean => {
|
|
45
53
|
let changed = false
|
|
46
54
|
|
|
47
55
|
// Save some CPU cycles by not stringifying unless enabled
|
|
48
56
|
if (debugVerbose) {
|
|
49
|
-
debug(
|
|
57
|
+
debug(
|
|
58
|
+
'\n\nNEW PATCH =============================================================',
|
|
59
|
+
)
|
|
50
60
|
debug(JSON.stringify(patch, null, 2))
|
|
51
61
|
}
|
|
52
62
|
|
|
@@ -91,7 +101,10 @@ export function diffMatchPatch(
|
|
|
91
101
|
>,
|
|
92
102
|
patch: DiffMatchPatch,
|
|
93
103
|
): boolean {
|
|
94
|
-
const {block, child, childPath} = findBlockAndChildFromPath(
|
|
104
|
+
const {block, child, childPath} = findBlockAndChildFromPath(
|
|
105
|
+
editor,
|
|
106
|
+
patch.path,
|
|
107
|
+
)
|
|
95
108
|
if (!block) {
|
|
96
109
|
debug('Block not found')
|
|
97
110
|
return false
|
|
@@ -112,7 +125,9 @@ export function diffMatchPatch(
|
|
|
112
125
|
}
|
|
113
126
|
|
|
114
127
|
const patches = parsePatch(patch.value)
|
|
115
|
-
const [newValue] = diffMatchPatchApplyPatches(patches, child.text, {
|
|
128
|
+
const [newValue] = diffMatchPatchApplyPatches(patches, child.text, {
|
|
129
|
+
allowExceedingIndices: true,
|
|
130
|
+
})
|
|
116
131
|
const diff = cleanupEfficiency(makeDiff(child.text, newValue), 5)
|
|
117
132
|
|
|
118
133
|
debugState(editor, 'before')
|
|
@@ -160,7 +175,8 @@ function insertPatch(
|
|
|
160
175
|
KEY_TO_SLATE_ELEMENT.get(editor),
|
|
161
176
|
) as Descendant[]
|
|
162
177
|
const targetBlockIndex = targetBlockPath[0]
|
|
163
|
-
const normalizedIdx =
|
|
178
|
+
const normalizedIdx =
|
|
179
|
+
position === 'after' ? targetBlockIndex + 1 : targetBlockIndex
|
|
164
180
|
debug(`Inserting blocks at path [${normalizedIdx}]`)
|
|
165
181
|
debugState(editor, 'before')
|
|
166
182
|
Transforms.insertNodes(editor, blocksToInsert, {at: [normalizedIdx]})
|
|
@@ -181,12 +197,15 @@ function insertPatch(
|
|
|
181
197
|
KEY_TO_SLATE_ELEMENT.get(editor),
|
|
182
198
|
)
|
|
183
199
|
const targetChildIndex = targetChildPath[1]
|
|
184
|
-
const normalizedIdx =
|
|
200
|
+
const normalizedIdx =
|
|
201
|
+
position === 'after' ? targetChildIndex + 1 : targetChildIndex
|
|
185
202
|
const childInsertPath = [targetChildPath[0], normalizedIdx]
|
|
186
203
|
debug(`Inserting children at path ${childInsertPath}`)
|
|
187
204
|
debugState(editor, 'before')
|
|
188
205
|
if (childrenToInsert && Element.isElement(childrenToInsert[0])) {
|
|
189
|
-
Transforms.insertNodes(editor, childrenToInsert[0].children, {
|
|
206
|
+
Transforms.insertNodes(editor, childrenToInsert[0].children, {
|
|
207
|
+
at: childInsertPath,
|
|
208
|
+
})
|
|
190
209
|
}
|
|
191
210
|
debugState(editor, 'after')
|
|
192
211
|
return true
|
|
@@ -198,7 +217,10 @@ function setPatch(editor: PortableTextSlateEditor, patch: SetPatch) {
|
|
|
198
217
|
value = {}
|
|
199
218
|
value[patch.path[3]] = patch.value
|
|
200
219
|
}
|
|
201
|
-
const {block, blockPath, child, childPath} = findBlockAndChildFromPath(
|
|
220
|
+
const {block, blockPath, child, childPath} = findBlockAndChildFromPath(
|
|
221
|
+
editor,
|
|
222
|
+
patch.path,
|
|
223
|
+
)
|
|
202
224
|
|
|
203
225
|
if (!block) {
|
|
204
226
|
debug('Block not found')
|
|
@@ -250,7 +272,6 @@ function setPatch(editor: PortableTextSlateEditor, patch: SetPatch) {
|
|
|
250
272
|
} else if (Element.isElement(block) && patch.path.length === 1 && blockPath) {
|
|
251
273
|
debug('Setting block property')
|
|
252
274
|
const {children, ...nextRest} = value as unknown as PortableTextBlock
|
|
253
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
|
|
254
275
|
const {children: prevChildren, ...prevRest} = block || {children: undefined}
|
|
255
276
|
// Set any block properties
|
|
256
277
|
editor.apply({
|
|
@@ -287,7 +308,11 @@ function setPatch(editor: PortableTextSlateEditor, patch: SetPatch) {
|
|
|
287
308
|
return true
|
|
288
309
|
}
|
|
289
310
|
|
|
290
|
-
function unsetPatch(
|
|
311
|
+
function unsetPatch(
|
|
312
|
+
editor: PortableTextSlateEditor,
|
|
313
|
+
patch: UnsetPatch,
|
|
314
|
+
previousPatch?: Patch,
|
|
315
|
+
) {
|
|
291
316
|
// Value
|
|
292
317
|
if (patch.path.length === 0) {
|
|
293
318
|
debug('Removing everything')
|
|
@@ -297,7 +322,7 @@ function unsetPatch(editor: PortableTextSlateEditor, patch: UnsetPatch, previous
|
|
|
297
322
|
editor.children.forEach((c, i) => {
|
|
298
323
|
Transforms.removeNodes(editor, {at: [i]})
|
|
299
324
|
})
|
|
300
|
-
Transforms.insertNodes(editor, editor.
|
|
325
|
+
Transforms.insertNodes(editor, editor.pteCreateTextBlock({decorators: []}))
|
|
301
326
|
if (previousSelection) {
|
|
302
327
|
Transforms.select(editor, {
|
|
303
328
|
anchor: {path: [0, 0], offset: 0},
|
|
@@ -309,7 +334,10 @@ function unsetPatch(editor: PortableTextSlateEditor, patch: UnsetPatch, previous
|
|
|
309
334
|
debugState(editor, 'after')
|
|
310
335
|
return true
|
|
311
336
|
}
|
|
312
|
-
const {block, blockPath, child, childPath} = findBlockAndChildFromPath(
|
|
337
|
+
const {block, blockPath, child, childPath} = findBlockAndChildFromPath(
|
|
338
|
+
editor,
|
|
339
|
+
patch.path,
|
|
340
|
+
)
|
|
313
341
|
|
|
314
342
|
// Single blocks
|
|
315
343
|
if (patch.path.length === 1) {
|
|
@@ -327,7 +355,11 @@ function unsetPatch(editor: PortableTextSlateEditor, patch: UnsetPatch, previous
|
|
|
327
355
|
}
|
|
328
356
|
|
|
329
357
|
// Unset on text block children
|
|
330
|
-
if (
|
|
358
|
+
if (
|
|
359
|
+
editor.isTextBlock(block) &&
|
|
360
|
+
patch.path[1] === 'children' &&
|
|
361
|
+
patch.path.length === 3
|
|
362
|
+
) {
|
|
331
363
|
if (!child || !childPath) {
|
|
332
364
|
debug('Child not found')
|
|
333
365
|
return false
|
|
@@ -349,7 +381,10 @@ function isKeyedSegment(segment: PathSegment): segment is KeyedSegment {
|
|
|
349
381
|
}
|
|
350
382
|
|
|
351
383
|
function debugState(
|
|
352
|
-
editor: Pick<
|
|
384
|
+
editor: Pick<
|
|
385
|
+
PortableTextSlateEditor,
|
|
386
|
+
'children' | 'isTextBlock' | 'apply' | 'selection'
|
|
387
|
+
>,
|
|
353
388
|
stateName: string,
|
|
354
389
|
) {
|
|
355
390
|
if (!debugVerbose) {
|
|
@@ -369,7 +404,9 @@ function findBlockFromPath(
|
|
|
369
404
|
): {block?: Descendant; path?: SlatePath} {
|
|
370
405
|
let blockIndex = -1
|
|
371
406
|
const block = editor.children.find((node: Descendant, index: number) => {
|
|
372
|
-
const isMatch = isKeyedSegment(path[0])
|
|
407
|
+
const isMatch = isKeyedSegment(path[0])
|
|
408
|
+
? node._key === path[0]._key
|
|
409
|
+
: index === path[0]
|
|
373
410
|
if (isMatch) {
|
|
374
411
|
blockIndex = index
|
|
375
412
|
}
|
|
@@ -387,14 +424,21 @@ function findBlockAndChildFromPath(
|
|
|
387
424
|
'children' | 'isTextBlock' | 'apply' | 'selection' | 'onChange'
|
|
388
425
|
>,
|
|
389
426
|
path: Path,
|
|
390
|
-
): {
|
|
427
|
+
): {
|
|
428
|
+
child?: Descendant
|
|
429
|
+
childPath?: SlatePath
|
|
430
|
+
block?: Descendant
|
|
431
|
+
blockPath?: SlatePath
|
|
432
|
+
} {
|
|
391
433
|
const {block, path: blockPath} = findBlockFromPath(editor, path)
|
|
392
434
|
if (!(Element.isElement(block) && path[1] === 'children')) {
|
|
393
435
|
return {block, blockPath, child: undefined, childPath: undefined}
|
|
394
436
|
}
|
|
395
437
|
let childIndex = -1
|
|
396
438
|
const child = block.children.find((node, index: number) => {
|
|
397
|
-
const isMatch = isKeyedSegment(path[2])
|
|
439
|
+
const isMatch = isKeyedSegment(path[2])
|
|
440
|
+
? node._key === path[2]._key
|
|
441
|
+
: index === path[2]
|
|
398
442
|
if (isMatch) {
|
|
399
443
|
childIndex = index
|
|
400
444
|
}
|
|
@@ -403,5 +447,10 @@ function findBlockAndChildFromPath(
|
|
|
403
447
|
if (!child) {
|
|
404
448
|
return {block, blockPath, child: undefined, childPath: undefined}
|
|
405
449
|
}
|
|
406
|
-
return {
|
|
450
|
+
return {
|
|
451
|
+
block,
|
|
452
|
+
child,
|
|
453
|
+
blockPath,
|
|
454
|
+
childPath: blockPath?.concat(childIndex) as SlatePath,
|
|
455
|
+
}
|
|
407
456
|
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import type {
|
|
2
|
+
ArraySchemaType,
|
|
3
|
+
BlockSchemaType,
|
|
4
|
+
ObjectSchemaType,
|
|
5
|
+
PortableTextBlock,
|
|
6
|
+
SchemaType,
|
|
7
|
+
SpanSchemaType,
|
|
8
8
|
} from '@sanity/types'
|
|
9
|
-
|
|
10
|
-
import {type PortableTextMemberSchemaTypes} from '../types/editor'
|
|
9
|
+
import type {PortableTextMemberSchemaTypes} from '../types/editor'
|
|
11
10
|
|
|
12
11
|
export function getPortableTextMemberSchemaTypes(
|
|
13
12
|
portableTextType: ArraySchemaType<PortableTextBlock>,
|
|
@@ -15,19 +14,23 @@ export function getPortableTextMemberSchemaTypes(
|
|
|
15
14
|
if (!portableTextType) {
|
|
16
15
|
throw new Error("Parameter 'portabletextType' missing (required)")
|
|
17
16
|
}
|
|
18
|
-
const blockType = portableTextType.of?.find(findBlockType) as
|
|
17
|
+
const blockType = portableTextType.of?.find(findBlockType) as
|
|
18
|
+
| BlockSchemaType
|
|
19
|
+
| undefined
|
|
19
20
|
if (!blockType) {
|
|
20
21
|
throw new Error('Block type is not defined in this schema (required)')
|
|
21
22
|
}
|
|
22
|
-
const childrenField = blockType.fields?.find(
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
const childrenField = blockType.fields?.find(
|
|
24
|
+
(field) => field.name === 'children',
|
|
25
|
+
) as {type: ArraySchemaType} | undefined
|
|
25
26
|
if (!childrenField) {
|
|
26
27
|
throw new Error('Children field for block type found in schema (required)')
|
|
27
28
|
}
|
|
28
29
|
const ofType = childrenField.type.of
|
|
29
30
|
if (!ofType) {
|
|
30
|
-
throw new Error(
|
|
31
|
+
throw new Error(
|
|
32
|
+
'Valid types for block children not found in schema (required)',
|
|
33
|
+
)
|
|
31
34
|
}
|
|
32
35
|
const spanType = ofType.find((memberType) => memberType.name === 'span') as
|
|
33
36
|
| ObjectSchemaType
|
|
@@ -35,10 +38,12 @@ export function getPortableTextMemberSchemaTypes(
|
|
|
35
38
|
if (!spanType) {
|
|
36
39
|
throw new Error('Span type not found in schema (required)')
|
|
37
40
|
}
|
|
38
|
-
const inlineObjectTypes = (ofType.filter(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
const inlineObjectTypes = (ofType.filter(
|
|
42
|
+
(memberType) => memberType.name !== 'span',
|
|
43
|
+
) || []) as ObjectSchemaType[]
|
|
44
|
+
const blockObjectTypes = (portableTextType.of?.filter(
|
|
45
|
+
(field) => field.name !== blockType.name,
|
|
46
|
+
) || []) as ObjectSchemaType[]
|
|
42
47
|
return {
|
|
43
48
|
styles: resolveEnabledStyles(blockType),
|
|
44
49
|
decorators: resolveEnabledDecorators(spanType),
|
|
@@ -53,13 +58,19 @@ export function getPortableTextMemberSchemaTypes(
|
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
function resolveEnabledStyles(blockType: ObjectSchemaType) {
|
|
56
|
-
const styleField = blockType.fields?.find(
|
|
61
|
+
const styleField = blockType.fields?.find(
|
|
62
|
+
(btField) => btField.name === 'style',
|
|
63
|
+
)
|
|
57
64
|
if (!styleField) {
|
|
58
|
-
throw new Error(
|
|
65
|
+
throw new Error(
|
|
66
|
+
"A field with name 'style' is not defined in the block type (required).",
|
|
67
|
+
)
|
|
59
68
|
}
|
|
60
69
|
const textStyles =
|
|
61
70
|
styleField.type.options?.list &&
|
|
62
|
-
styleField.type.options.list?.filter(
|
|
71
|
+
styleField.type.options.list?.filter(
|
|
72
|
+
(style: {value: string}) => style.value,
|
|
73
|
+
)
|
|
63
74
|
if (!textStyles || textStyles.length === 0) {
|
|
64
75
|
throw new Error(
|
|
65
76
|
'The style fields need at least one style ' +
|
|
@@ -74,9 +85,13 @@ function resolveEnabledDecorators(spanType: ObjectSchemaType) {
|
|
|
74
85
|
}
|
|
75
86
|
|
|
76
87
|
function resolveEnabledListItems(blockType: ObjectSchemaType) {
|
|
77
|
-
const listField = blockType.fields?.find(
|
|
88
|
+
const listField = blockType.fields?.find(
|
|
89
|
+
(btField) => btField.name === 'listItem',
|
|
90
|
+
)
|
|
78
91
|
if (!listField) {
|
|
79
|
-
throw new Error(
|
|
92
|
+
throw new Error(
|
|
93
|
+
"A field with name 'listItem' is not defined in the block type (required).",
|
|
94
|
+
)
|
|
80
95
|
}
|
|
81
96
|
const listItems =
|
|
82
97
|
listField.type.options?.list &&
|