@portabletext/editor 0.0.0
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/LICENSE +21 -0
- package/README.md +3 -0
- package/lib/index.d.mts +911 -0
- package/lib/index.d.ts +911 -0
- package/lib/index.esm.js +4896 -0
- package/lib/index.esm.js.map +1 -0
- package/lib/index.js +4874 -0
- package/lib/index.js.map +1 -0
- package/lib/index.mjs +4896 -0
- package/lib/index.mjs.map +1 -0
- package/package.json +119 -0
- package/src/editor/Editable.tsx +683 -0
- package/src/editor/PortableTextEditor.tsx +308 -0
- package/src/editor/__tests__/PortableTextEditor.test.tsx +386 -0
- package/src/editor/__tests__/PortableTextEditorTester.tsx +116 -0
- package/src/editor/__tests__/RangeDecorations.test.tsx +115 -0
- package/src/editor/__tests__/handleClick.test.tsx +218 -0
- package/src/editor/__tests__/pteWarningsSelfSolving.test.tsx +389 -0
- package/src/editor/__tests__/utils.ts +39 -0
- package/src/editor/components/DraggableBlock.tsx +287 -0
- package/src/editor/components/Element.tsx +279 -0
- package/src/editor/components/Leaf.tsx +288 -0
- package/src/editor/components/SlateContainer.tsx +81 -0
- package/src/editor/components/Synchronizer.tsx +190 -0
- package/src/editor/hooks/usePortableTextEditor.ts +23 -0
- package/src/editor/hooks/usePortableTextEditorKeyGenerator.ts +24 -0
- package/src/editor/hooks/usePortableTextEditorSelection.ts +22 -0
- package/src/editor/hooks/usePortableTextEditorValue.ts +16 -0
- package/src/editor/hooks/usePortableTextReadOnly.ts +20 -0
- package/src/editor/hooks/useSyncValue.test.tsx +125 -0
- package/src/editor/hooks/useSyncValue.ts +372 -0
- package/src/editor/nodes/DefaultAnnotation.tsx +16 -0
- package/src/editor/nodes/DefaultObject.tsx +15 -0
- package/src/editor/nodes/index.ts +189 -0
- package/src/editor/plugins/__tests__/withEditableAPIDelete.test.tsx +244 -0
- package/src/editor/plugins/__tests__/withEditableAPIGetFragment.test.tsx +142 -0
- package/src/editor/plugins/__tests__/withEditableAPIInsert.test.tsx +346 -0
- package/src/editor/plugins/__tests__/withEditableAPISelectionsOverlapping.test.tsx +162 -0
- package/src/editor/plugins/__tests__/withHotkeys.test.tsx +212 -0
- package/src/editor/plugins/__tests__/withInsertBreak.test.tsx +204 -0
- package/src/editor/plugins/__tests__/withPlaceholderBlock.test.tsx +133 -0
- package/src/editor/plugins/__tests__/withPortableTextLists.test.tsx +65 -0
- package/src/editor/plugins/__tests__/withPortableTextMarkModel.test.tsx +1377 -0
- package/src/editor/plugins/__tests__/withPortableTextSelections.test.tsx +91 -0
- package/src/editor/plugins/__tests__/withUndoRedo.test.tsx +115 -0
- package/src/editor/plugins/createWithEditableAPI.ts +573 -0
- package/src/editor/plugins/createWithHotKeys.ts +304 -0
- package/src/editor/plugins/createWithInsertBreak.ts +45 -0
- package/src/editor/plugins/createWithInsertData.ts +359 -0
- package/src/editor/plugins/createWithMaxBlocks.ts +24 -0
- package/src/editor/plugins/createWithObjectKeys.ts +63 -0
- package/src/editor/plugins/createWithPatches.ts +274 -0
- package/src/editor/plugins/createWithPlaceholderBlock.ts +36 -0
- package/src/editor/plugins/createWithPortableTextBlockStyle.ts +91 -0
- package/src/editor/plugins/createWithPortableTextLists.ts +160 -0
- package/src/editor/plugins/createWithPortableTextMarkModel.ts +441 -0
- package/src/editor/plugins/createWithPortableTextSelections.ts +65 -0
- package/src/editor/plugins/createWithSchemaTypes.ts +76 -0
- package/src/editor/plugins/createWithUndoRedo.ts +494 -0
- package/src/editor/plugins/createWithUtils.ts +81 -0
- package/src/editor/plugins/index.ts +155 -0
- package/src/index.ts +11 -0
- package/src/patch/PatchEvent.ts +33 -0
- package/src/patch/applyPatch.ts +29 -0
- package/src/patch/array.ts +89 -0
- package/src/patch/arrayInsert.ts +27 -0
- package/src/patch/object.ts +39 -0
- package/src/patch/patches.ts +53 -0
- package/src/patch/primitive.ts +43 -0
- package/src/patch/string.ts +51 -0
- package/src/types/editor.ts +576 -0
- package/src/types/options.ts +17 -0
- package/src/types/patch.ts +65 -0
- package/src/types/slate.ts +25 -0
- package/src/utils/__tests__/dmpToOperations.test.ts +181 -0
- package/src/utils/__tests__/operationToPatches.test.ts +421 -0
- package/src/utils/__tests__/patchToOperations.test.ts +293 -0
- package/src/utils/__tests__/ranges.test.ts +18 -0
- package/src/utils/__tests__/valueNormalization.test.tsx +62 -0
- package/src/utils/__tests__/values.test.ts +253 -0
- package/src/utils/applyPatch.ts +407 -0
- package/src/utils/bufferUntil.ts +15 -0
- package/src/utils/debug.ts +12 -0
- package/src/utils/getPortableTextMemberSchemaTypes.ts +100 -0
- package/src/utils/operationToPatches.ts +357 -0
- package/src/utils/patches.ts +36 -0
- package/src/utils/paths.ts +60 -0
- package/src/utils/ranges.ts +77 -0
- package/src/utils/schema.ts +8 -0
- package/src/utils/selection.ts +65 -0
- package/src/utils/ucs2Indices.ts +67 -0
- package/src/utils/validateValue.ts +394 -0
- package/src/utils/values.ts +208 -0
- package/src/utils/weakMaps.ts +24 -0
- package/src/utils/withChanges.ts +25 -0
- package/src/utils/withPreserveKeys.ts +14 -0
- package/src/utils/withoutPatching.ts +14 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import {type Path, type PortableTextSpan, type PortableTextTextBlock} from '@sanity/types'
|
|
2
|
+
import {get, isUndefined, omitBy} from 'lodash'
|
|
3
|
+
import {
|
|
4
|
+
type Descendant,
|
|
5
|
+
type InsertNodeOperation,
|
|
6
|
+
type InsertTextOperation,
|
|
7
|
+
type MergeNodeOperation,
|
|
8
|
+
type MoveNodeOperation,
|
|
9
|
+
type RemoveNodeOperation,
|
|
10
|
+
type RemoveTextOperation,
|
|
11
|
+
type SetNodeOperation,
|
|
12
|
+
type SplitNodeOperation,
|
|
13
|
+
Text,
|
|
14
|
+
} from 'slate'
|
|
15
|
+
|
|
16
|
+
import {type PatchFunctions} from '../editor/plugins/createWithPatches'
|
|
17
|
+
import {diffMatchPatch, insert, set, setIfMissing, unset} from '../patch/PatchEvent'
|
|
18
|
+
import {type PortableTextMemberSchemaTypes, type PortableTextSlateEditor} from '../types/editor'
|
|
19
|
+
import {type InsertPosition, type Patch} from '../types/patch'
|
|
20
|
+
import {debugWithName} from './debug'
|
|
21
|
+
import {fromSlateValue} from './values'
|
|
22
|
+
|
|
23
|
+
const debug = debugWithName('operationToPatches')
|
|
24
|
+
debug.enabled = false
|
|
25
|
+
|
|
26
|
+
export function createOperationToPatches(types: PortableTextMemberSchemaTypes): PatchFunctions {
|
|
27
|
+
const textBlockName = types.block.name
|
|
28
|
+
function insertTextPatch(
|
|
29
|
+
editor: PortableTextSlateEditor,
|
|
30
|
+
operation: InsertTextOperation,
|
|
31
|
+
beforeValue: Descendant[],
|
|
32
|
+
) {
|
|
33
|
+
if (debug.enabled) {
|
|
34
|
+
debug('Operation', JSON.stringify(operation, null, 2))
|
|
35
|
+
}
|
|
36
|
+
const block =
|
|
37
|
+
editor.isTextBlock(editor.children[operation.path[0]]) && editor.children[operation.path[0]]
|
|
38
|
+
if (!block) {
|
|
39
|
+
throw new Error('Could not find block')
|
|
40
|
+
}
|
|
41
|
+
const textChild =
|
|
42
|
+
editor.isTextBlock(block) &&
|
|
43
|
+
editor.isTextSpan(block.children[operation.path[1]]) &&
|
|
44
|
+
(block.children[operation.path[1]] as PortableTextSpan)
|
|
45
|
+
if (!textChild) {
|
|
46
|
+
throw new Error('Could not find child')
|
|
47
|
+
}
|
|
48
|
+
const path: Path = [{_key: block._key}, 'children', {_key: textChild._key}, 'text']
|
|
49
|
+
const prevBlock = beforeValue[operation.path[0]]
|
|
50
|
+
const prevChild = editor.isTextBlock(prevBlock) && prevBlock.children[operation.path[1]]
|
|
51
|
+
const prevText = editor.isTextSpan(prevChild) ? prevChild.text : ''
|
|
52
|
+
const patch = diffMatchPatch(prevText, textChild.text, path)
|
|
53
|
+
return patch.value.length ? [patch] : []
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function removeTextPatch(
|
|
57
|
+
editor: PortableTextSlateEditor,
|
|
58
|
+
operation: RemoveTextOperation,
|
|
59
|
+
beforeValue: Descendant[],
|
|
60
|
+
) {
|
|
61
|
+
const block = editor && editor.children[operation.path[0]]
|
|
62
|
+
if (!block) {
|
|
63
|
+
throw new Error('Could not find block')
|
|
64
|
+
}
|
|
65
|
+
const child = (editor.isTextBlock(block) && block.children[operation.path[1]]) || undefined
|
|
66
|
+
const textChild: PortableTextSpan | undefined = editor.isTextSpan(child) ? child : undefined
|
|
67
|
+
if (child && !textChild) {
|
|
68
|
+
throw new Error('Expected span')
|
|
69
|
+
}
|
|
70
|
+
if (!textChild) {
|
|
71
|
+
throw new Error('Could not find child')
|
|
72
|
+
}
|
|
73
|
+
const path: Path = [{_key: block._key}, 'children', {_key: textChild._key}, 'text']
|
|
74
|
+
const beforeBlock = beforeValue[operation.path[0]]
|
|
75
|
+
const prevTextChild = editor.isTextBlock(beforeBlock) && beforeBlock.children[operation.path[1]]
|
|
76
|
+
const prevText = editor.isTextSpan(prevTextChild) && prevTextChild.text
|
|
77
|
+
const patch = diffMatchPatch(prevText || '', textChild.text, path)
|
|
78
|
+
return patch.value ? [patch] : []
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function setNodePatch(editor: PortableTextSlateEditor, operation: SetNodeOperation) {
|
|
82
|
+
if (operation.path.length === 1) {
|
|
83
|
+
const block = editor.children[operation.path[0]]
|
|
84
|
+
if (typeof block._key !== 'string') {
|
|
85
|
+
throw new Error('Expected block to have a _key')
|
|
86
|
+
}
|
|
87
|
+
const setNode = omitBy(
|
|
88
|
+
{...editor.children[operation.path[0]], ...operation.newProperties},
|
|
89
|
+
isUndefined,
|
|
90
|
+
) as unknown as Descendant
|
|
91
|
+
return [set(fromSlateValue([setNode], textBlockName)[0], [{_key: block._key}])]
|
|
92
|
+
} else if (operation.path.length === 2) {
|
|
93
|
+
const block = editor.children[operation.path[0]]
|
|
94
|
+
if (editor.isTextBlock(block)) {
|
|
95
|
+
const child = block.children[operation.path[1]]
|
|
96
|
+
if (child) {
|
|
97
|
+
const blockKey = block._key
|
|
98
|
+
const childKey = child._key
|
|
99
|
+
const patches: Patch[] = []
|
|
100
|
+
const keys = Object.keys(operation.newProperties)
|
|
101
|
+
keys.forEach((keyName) => {
|
|
102
|
+
// Special case for setting _key on a child. We have to target it by index and not the _key.
|
|
103
|
+
if (keys.length === 1 && keyName === '_key') {
|
|
104
|
+
const val = get(operation.newProperties, keyName)
|
|
105
|
+
patches.push(
|
|
106
|
+
set(val, [{_key: blockKey}, 'children', block.children.indexOf(child), keyName]),
|
|
107
|
+
)
|
|
108
|
+
} else {
|
|
109
|
+
const val = get(operation.newProperties, keyName)
|
|
110
|
+
patches.push(set(val, [{_key: blockKey}, 'children', {_key: childKey}, keyName]))
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
return patches
|
|
114
|
+
}
|
|
115
|
+
throw new Error('Could not find a valid child')
|
|
116
|
+
}
|
|
117
|
+
throw new Error('Could not find a valid block')
|
|
118
|
+
} else {
|
|
119
|
+
throw new Error(`Unexpected path encountered: ${JSON.stringify(operation.path)}`)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function insertNodePatch(
|
|
124
|
+
editor: PortableTextSlateEditor,
|
|
125
|
+
operation: InsertNodeOperation,
|
|
126
|
+
beforeValue: Descendant[],
|
|
127
|
+
): Patch[] {
|
|
128
|
+
const block = beforeValue[operation.path[0]]
|
|
129
|
+
const isTextBlock = editor.isTextBlock(block)
|
|
130
|
+
if (operation.path.length === 1) {
|
|
131
|
+
const position = operation.path[0] === 0 ? 'before' : 'after'
|
|
132
|
+
const beforeBlock = beforeValue[operation.path[0] - 1]
|
|
133
|
+
const targetKey = operation.path[0] === 0 ? block?._key : beforeBlock?._key
|
|
134
|
+
if (targetKey) {
|
|
135
|
+
return [
|
|
136
|
+
insert([fromSlateValue([operation.node as Descendant], textBlockName)[0]], position, [
|
|
137
|
+
{_key: targetKey},
|
|
138
|
+
]),
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
return [
|
|
142
|
+
setIfMissing(beforeValue, []),
|
|
143
|
+
insert([fromSlateValue([operation.node as Descendant], textBlockName)[0]], 'before', [
|
|
144
|
+
operation.path[0],
|
|
145
|
+
]),
|
|
146
|
+
]
|
|
147
|
+
} else if (isTextBlock && operation.path.length === 2 && editor.children[operation.path[0]]) {
|
|
148
|
+
const position =
|
|
149
|
+
block.children.length === 0 || !block.children[operation.path[1] - 1] ? 'before' : 'after'
|
|
150
|
+
const node = {...operation.node} as Descendant
|
|
151
|
+
if (!node._type && Text.isText(node)) {
|
|
152
|
+
node._type = 'span'
|
|
153
|
+
node.marks = []
|
|
154
|
+
}
|
|
155
|
+
const blk = fromSlateValue(
|
|
156
|
+
[
|
|
157
|
+
{
|
|
158
|
+
_key: 'bogus',
|
|
159
|
+
_type: textBlockName,
|
|
160
|
+
children: [node],
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
textBlockName,
|
|
164
|
+
)[0] as PortableTextTextBlock
|
|
165
|
+
const child = blk.children[0]
|
|
166
|
+
return [
|
|
167
|
+
insert([child], position, [
|
|
168
|
+
{_key: block._key},
|
|
169
|
+
'children',
|
|
170
|
+
block.children.length <= 1 || !block.children[operation.path[1] - 1]
|
|
171
|
+
? 0
|
|
172
|
+
: {_key: block.children[operation.path[1] - 1]._key},
|
|
173
|
+
]),
|
|
174
|
+
]
|
|
175
|
+
}
|
|
176
|
+
debug('Something was inserted into a void block. Not producing editor patches.')
|
|
177
|
+
return []
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function splitNodePatch(
|
|
181
|
+
editor: PortableTextSlateEditor,
|
|
182
|
+
operation: SplitNodeOperation,
|
|
183
|
+
beforeValue: Descendant[],
|
|
184
|
+
) {
|
|
185
|
+
const patches: Patch[] = []
|
|
186
|
+
const splitBlock = editor.children[operation.path[0]]
|
|
187
|
+
if (!editor.isTextBlock(splitBlock)) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`Block with path ${JSON.stringify(
|
|
190
|
+
operation.path[0],
|
|
191
|
+
)} is not a text block and can't be split`,
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
if (operation.path.length === 1) {
|
|
195
|
+
const oldBlock = beforeValue[operation.path[0]]
|
|
196
|
+
if (editor.isTextBlock(oldBlock)) {
|
|
197
|
+
const targetValue = fromSlateValue(
|
|
198
|
+
[editor.children[operation.path[0] + 1]],
|
|
199
|
+
textBlockName,
|
|
200
|
+
)[0]
|
|
201
|
+
if (targetValue) {
|
|
202
|
+
patches.push(insert([targetValue], 'after', [{_key: splitBlock._key}]))
|
|
203
|
+
const spansToUnset = oldBlock.children.slice(operation.position)
|
|
204
|
+
spansToUnset.forEach((span) => {
|
|
205
|
+
const path = [{_key: oldBlock._key}, 'children', {_key: span._key}]
|
|
206
|
+
patches.push(unset(path))
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return patches
|
|
211
|
+
}
|
|
212
|
+
if (operation.path.length === 2) {
|
|
213
|
+
const splitSpan = splitBlock.children[operation.path[1]]
|
|
214
|
+
if (editor.isTextSpan(splitSpan)) {
|
|
215
|
+
const targetSpans = (
|
|
216
|
+
fromSlateValue(
|
|
217
|
+
[
|
|
218
|
+
{
|
|
219
|
+
...splitBlock,
|
|
220
|
+
children: splitBlock.children.slice(operation.path[1] + 1, operation.path[1] + 2),
|
|
221
|
+
} as Descendant,
|
|
222
|
+
],
|
|
223
|
+
textBlockName,
|
|
224
|
+
)[0] as PortableTextTextBlock
|
|
225
|
+
).children
|
|
226
|
+
|
|
227
|
+
patches.push(
|
|
228
|
+
insert(targetSpans, 'after', [
|
|
229
|
+
{_key: splitBlock._key},
|
|
230
|
+
'children',
|
|
231
|
+
{_key: splitSpan._key},
|
|
232
|
+
]),
|
|
233
|
+
)
|
|
234
|
+
patches.push(
|
|
235
|
+
set(splitSpan.text, [
|
|
236
|
+
{_key: splitBlock._key},
|
|
237
|
+
'children',
|
|
238
|
+
{_key: splitSpan._key},
|
|
239
|
+
'text',
|
|
240
|
+
]),
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
return patches
|
|
244
|
+
}
|
|
245
|
+
return patches
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function removeNodePatch(
|
|
249
|
+
editor: PortableTextSlateEditor,
|
|
250
|
+
operation: RemoveNodeOperation,
|
|
251
|
+
beforeValue: Descendant[],
|
|
252
|
+
) {
|
|
253
|
+
const block = beforeValue[operation.path[0]]
|
|
254
|
+
if (operation.path.length === 1) {
|
|
255
|
+
// Remove a single block
|
|
256
|
+
if (block && block._key) {
|
|
257
|
+
return [unset([{_key: block._key}])]
|
|
258
|
+
}
|
|
259
|
+
throw new Error('Block not found')
|
|
260
|
+
} else if (editor.isTextBlock(block) && operation.path.length === 2) {
|
|
261
|
+
const spanToRemove =
|
|
262
|
+
editor.isTextBlock(block) && block.children && block.children[operation.path[1]]
|
|
263
|
+
if (spanToRemove) {
|
|
264
|
+
return [unset([{_key: block._key}, 'children', {_key: spanToRemove._key}])]
|
|
265
|
+
}
|
|
266
|
+
debug('Span not found in editor trying to remove node')
|
|
267
|
+
return []
|
|
268
|
+
} else {
|
|
269
|
+
debug('Not creating patch inside object block')
|
|
270
|
+
return []
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function mergeNodePatch(
|
|
275
|
+
editor: PortableTextSlateEditor,
|
|
276
|
+
operation: MergeNodeOperation,
|
|
277
|
+
beforeValue: Descendant[],
|
|
278
|
+
) {
|
|
279
|
+
const patches: Patch[] = []
|
|
280
|
+
|
|
281
|
+
const block = beforeValue[operation.path[0]]
|
|
282
|
+
const targetBlock = editor.children[operation.path[0]]
|
|
283
|
+
|
|
284
|
+
if (operation.path.length === 1) {
|
|
285
|
+
if (block?._key) {
|
|
286
|
+
const newBlock = fromSlateValue([editor.children[operation.path[0] - 1]], textBlockName)[0]
|
|
287
|
+
patches.push(set(newBlock, [{_key: newBlock._key}]))
|
|
288
|
+
patches.push(unset([{_key: block._key}]))
|
|
289
|
+
} else {
|
|
290
|
+
throw new Error('Target key not found!')
|
|
291
|
+
}
|
|
292
|
+
} else if (operation.path.length === 2 && editor.isTextBlock(targetBlock)) {
|
|
293
|
+
const mergedSpan =
|
|
294
|
+
(editor.isTextBlock(block) && block.children[operation.path[1]]) || undefined
|
|
295
|
+
const targetSpan = targetBlock.children[operation.path[1] - 1]
|
|
296
|
+
if (editor.isTextSpan(targetSpan)) {
|
|
297
|
+
// Set the merged span with it's new value
|
|
298
|
+
patches.push(
|
|
299
|
+
set(targetSpan.text, [{_key: block._key}, 'children', {_key: targetSpan._key}, 'text']),
|
|
300
|
+
)
|
|
301
|
+
if (mergedSpan) {
|
|
302
|
+
patches.push(unset([{_key: block._key}, 'children', {_key: mergedSpan._key}]))
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
} else {
|
|
306
|
+
debug("Void nodes can't be merged, not creating any patches")
|
|
307
|
+
}
|
|
308
|
+
return patches
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function moveNodePatch(
|
|
312
|
+
editor: PortableTextSlateEditor,
|
|
313
|
+
operation: MoveNodeOperation,
|
|
314
|
+
beforeValue: Descendant[],
|
|
315
|
+
) {
|
|
316
|
+
const patches: Patch[] = []
|
|
317
|
+
const block = beforeValue[operation.path[0]]
|
|
318
|
+
const targetBlock = beforeValue[operation.newPath[0]]
|
|
319
|
+
if (operation.path.length === 1) {
|
|
320
|
+
const position: InsertPosition = operation.path[0] > operation.newPath[0] ? 'before' : 'after'
|
|
321
|
+
patches.push(unset([{_key: block._key}]))
|
|
322
|
+
patches.push(
|
|
323
|
+
insert([fromSlateValue([block], textBlockName)[0]], position, [{_key: targetBlock._key}]),
|
|
324
|
+
)
|
|
325
|
+
} else if (
|
|
326
|
+
operation.path.length === 2 &&
|
|
327
|
+
editor.isTextBlock(block) &&
|
|
328
|
+
editor.isTextBlock(targetBlock)
|
|
329
|
+
) {
|
|
330
|
+
const child = block.children[operation.path[1]]
|
|
331
|
+
const targetChild = targetBlock.children[operation.newPath[1]]
|
|
332
|
+
const position = operation.newPath[1] === targetBlock.children.length ? 'after' : 'before'
|
|
333
|
+
const childToInsert = (fromSlateValue([block], textBlockName)[0] as PortableTextTextBlock)
|
|
334
|
+
.children[operation.path[1]]
|
|
335
|
+
patches.push(unset([{_key: block._key}, 'children', {_key: child._key}]))
|
|
336
|
+
patches.push(
|
|
337
|
+
insert([childToInsert], position, [
|
|
338
|
+
{_key: targetBlock._key},
|
|
339
|
+
'children',
|
|
340
|
+
{_key: targetChild._key},
|
|
341
|
+
]),
|
|
342
|
+
)
|
|
343
|
+
}
|
|
344
|
+
return patches
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
insertNodePatch,
|
|
349
|
+
insertTextPatch,
|
|
350
|
+
mergeNodePatch,
|
|
351
|
+
moveNodePatch,
|
|
352
|
+
removeNodePatch,
|
|
353
|
+
removeTextPatch,
|
|
354
|
+
setNodePatch,
|
|
355
|
+
splitNodePatch,
|
|
356
|
+
}
|
|
357
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {isEqual} from 'lodash'
|
|
2
|
+
|
|
3
|
+
import {type Patch} from '../types/patch'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Try to compact a set of patches
|
|
7
|
+
*
|
|
8
|
+
*/
|
|
9
|
+
export function compactPatches(patches: Patch[]) {
|
|
10
|
+
// If the last patch is unsetting everything, just do that
|
|
11
|
+
const lastPatch = patches.slice(-1)[0]
|
|
12
|
+
if (lastPatch && lastPatch.type === 'unset' && lastPatch.path.length === 0) {
|
|
13
|
+
return [lastPatch]
|
|
14
|
+
}
|
|
15
|
+
let finalPatches = patches
|
|
16
|
+
// Run through the patches and remove any redundant ones.
|
|
17
|
+
finalPatches = finalPatches.filter((patch, index) => {
|
|
18
|
+
if (!patch) {
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
21
|
+
const nextPatch = finalPatches[index + 1]
|
|
22
|
+
if (
|
|
23
|
+
nextPatch &&
|
|
24
|
+
nextPatch.type === 'set' &&
|
|
25
|
+
patch.type === 'set' &&
|
|
26
|
+
isEqual(patch.path, nextPatch.path)
|
|
27
|
+
) {
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
return true
|
|
31
|
+
})
|
|
32
|
+
if (finalPatches.length !== patches.length) {
|
|
33
|
+
return finalPatches
|
|
34
|
+
}
|
|
35
|
+
return patches
|
|
36
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {isKeySegment, type Path} from '@sanity/types'
|
|
2
|
+
import {isEqual} from 'lodash'
|
|
3
|
+
import {type Descendant, Editor, Element, type Path as SlatePath, type Point} from 'slate'
|
|
4
|
+
|
|
5
|
+
import {type EditorSelectionPoint, type PortableTextMemberSchemaTypes} from '../types/editor'
|
|
6
|
+
import {type ObjectWithKeyAndType} from './ranges'
|
|
7
|
+
|
|
8
|
+
export function createKeyedPath(
|
|
9
|
+
point: Point,
|
|
10
|
+
value: ObjectWithKeyAndType[] | undefined,
|
|
11
|
+
types: PortableTextMemberSchemaTypes,
|
|
12
|
+
): Path | null {
|
|
13
|
+
const blockPath = [point.path[0]]
|
|
14
|
+
if (!value) {
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
17
|
+
const block = value[blockPath[0]]
|
|
18
|
+
if (!block) {
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
const keyedBlockPath = [{_key: block._key}]
|
|
22
|
+
if (block._type !== types.block.name) {
|
|
23
|
+
return keyedBlockPath as Path
|
|
24
|
+
}
|
|
25
|
+
let keyedChildPath
|
|
26
|
+
const childPath = point.path.slice(0, 2)
|
|
27
|
+
const child = Array.isArray(block.children) && block.children[childPath[1]]
|
|
28
|
+
if (child) {
|
|
29
|
+
keyedChildPath = ['children', {_key: child._key}]
|
|
30
|
+
}
|
|
31
|
+
return (keyedChildPath ? [...keyedBlockPath, ...keyedChildPath] : keyedBlockPath) as Path
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createArrayedPath(point: EditorSelectionPoint, editor: Editor): SlatePath {
|
|
35
|
+
if (!editor) {
|
|
36
|
+
return []
|
|
37
|
+
}
|
|
38
|
+
const [block, blockPath] = Array.from(
|
|
39
|
+
Editor.nodes(editor, {
|
|
40
|
+
at: [],
|
|
41
|
+
match: (n) => isKeySegment(point.path[0]) && (n as Descendant)._key === point.path[0]._key,
|
|
42
|
+
}),
|
|
43
|
+
)[0] || [undefined, undefined]
|
|
44
|
+
if (!block || !Element.isElement(block)) {
|
|
45
|
+
return []
|
|
46
|
+
}
|
|
47
|
+
if (editor.isVoid(block)) {
|
|
48
|
+
return [blockPath[0], 0]
|
|
49
|
+
}
|
|
50
|
+
const childPath = [point.path[2]]
|
|
51
|
+
const childIndex = block.children.findIndex((child) => isEqual([{_key: child._key}], childPath))
|
|
52
|
+
if (childIndex >= 0 && block.children[childIndex]) {
|
|
53
|
+
const child = block.children[childIndex]
|
|
54
|
+
if (Element.isElement(child) && editor.isVoid(child)) {
|
|
55
|
+
return blockPath.concat(childIndex).concat(0)
|
|
56
|
+
}
|
|
57
|
+
return blockPath.concat(childIndex)
|
|
58
|
+
}
|
|
59
|
+
return blockPath
|
|
60
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/* eslint-disable complexity */
|
|
2
|
+
import {type BaseRange, type Editor, type Operation, Point, Range} from 'slate'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
type EditorSelection,
|
|
6
|
+
type EditorSelectionPoint,
|
|
7
|
+
type PortableTextMemberSchemaTypes,
|
|
8
|
+
} from '../types/editor'
|
|
9
|
+
import {createArrayedPath, createKeyedPath} from './paths'
|
|
10
|
+
|
|
11
|
+
export interface ObjectWithKeyAndType {
|
|
12
|
+
_key: string
|
|
13
|
+
_type: string
|
|
14
|
+
children?: ObjectWithKeyAndType[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function toPortableTextRange(
|
|
18
|
+
value: ObjectWithKeyAndType[] | undefined,
|
|
19
|
+
range: BaseRange | Partial<BaseRange> | null,
|
|
20
|
+
types: PortableTextMemberSchemaTypes,
|
|
21
|
+
): EditorSelection {
|
|
22
|
+
if (!range) {
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
let anchor: EditorSelectionPoint | null = null
|
|
26
|
+
let focus: EditorSelectionPoint | null = null
|
|
27
|
+
const anchorPath = range.anchor && createKeyedPath(range.anchor, value, types)
|
|
28
|
+
if (anchorPath && range.anchor) {
|
|
29
|
+
anchor = {
|
|
30
|
+
path: anchorPath,
|
|
31
|
+
offset: range.anchor.offset,
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const focusPath = range.focus && createKeyedPath(range.focus, value, types)
|
|
35
|
+
if (focusPath && range.focus) {
|
|
36
|
+
focus = {
|
|
37
|
+
path: focusPath,
|
|
38
|
+
offset: range.focus.offset,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const backward = Boolean(Range.isRange(range) ? Range.isBackward(range) : undefined)
|
|
42
|
+
return anchor && focus ? {anchor, focus, backward} : null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function toSlateRange(selection: EditorSelection, editor: Editor): Range | null {
|
|
46
|
+
if (!selection || !editor) {
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
const anchor = {
|
|
50
|
+
path: createArrayedPath(selection.anchor, editor),
|
|
51
|
+
offset: selection.anchor.offset,
|
|
52
|
+
}
|
|
53
|
+
const focus = {
|
|
54
|
+
path: createArrayedPath(selection.focus, editor),
|
|
55
|
+
offset: selection.focus.offset,
|
|
56
|
+
}
|
|
57
|
+
if (focus.path.length === 0 || anchor.path.length === 0) {
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
const range = anchor && focus ? {anchor, focus} : null
|
|
61
|
+
return range
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function moveRangeByOperation(range: Range, operation: Operation): Range | null {
|
|
65
|
+
const anchor = Point.transform(range.anchor, operation)
|
|
66
|
+
const focus = Point.transform(range.focus, operation)
|
|
67
|
+
|
|
68
|
+
if (anchor === null || focus === null) {
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (Point.equals(anchor, range.anchor) && Point.equals(focus, range.focus)) {
|
|
73
|
+
return range
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {anchor, focus}
|
|
77
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {type Path, type PortableTextBlock} from '@sanity/types'
|
|
2
|
+
import {isEqual} from 'lodash'
|
|
3
|
+
|
|
4
|
+
import {type EditorSelection, type EditorSelectionPoint} from '../types/editor'
|
|
5
|
+
|
|
6
|
+
export function normalizePoint(
|
|
7
|
+
point: EditorSelectionPoint,
|
|
8
|
+
value: PortableTextBlock[],
|
|
9
|
+
): EditorSelectionPoint | null {
|
|
10
|
+
if (!point || !value) {
|
|
11
|
+
return null
|
|
12
|
+
}
|
|
13
|
+
const newPath: Path = []
|
|
14
|
+
let newOffset: number = point.offset || 0
|
|
15
|
+
const blockKey =
|
|
16
|
+
typeof point.path[0] === 'object' && '_key' in point.path[0] && point.path[0]._key
|
|
17
|
+
const childKey =
|
|
18
|
+
typeof point.path[2] === 'object' && '_key' in point.path[2] && point.path[2]._key
|
|
19
|
+
const block: PortableTextBlock | undefined = value.find((blk) => blk._key === blockKey)
|
|
20
|
+
if (block) {
|
|
21
|
+
newPath.push({_key: block._key})
|
|
22
|
+
} else {
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
if (block && point.path[1] === 'children') {
|
|
26
|
+
if (!block.children || (Array.isArray(block.children) && block.children.length === 0)) {
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
const child =
|
|
30
|
+
Array.isArray(block.children) && block.children.find((cld) => cld._key === childKey)
|
|
31
|
+
if (child) {
|
|
32
|
+
newPath.push('children')
|
|
33
|
+
newPath.push({_key: child._key})
|
|
34
|
+
newOffset =
|
|
35
|
+
child.text && child.text.length >= point.offset
|
|
36
|
+
? point.offset
|
|
37
|
+
: (child.text && child.text.length) || 0
|
|
38
|
+
} else {
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return {path: newPath, offset: newOffset}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function normalizeSelection(
|
|
46
|
+
selection: EditorSelection,
|
|
47
|
+
value: PortableTextBlock[] | undefined,
|
|
48
|
+
): EditorSelection | null {
|
|
49
|
+
if (!selection || !value || value.length === 0) {
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
let newAnchor: EditorSelectionPoint | null = null
|
|
53
|
+
let newFocus: EditorSelectionPoint | null = null
|
|
54
|
+
const {anchor, focus} = selection
|
|
55
|
+
if (anchor && value.find((blk) => isEqual({_key: blk._key}, anchor.path[0]))) {
|
|
56
|
+
newAnchor = normalizePoint(anchor, value)
|
|
57
|
+
}
|
|
58
|
+
if (focus && value.find((blk) => isEqual({_key: blk._key}, focus.path[0]))) {
|
|
59
|
+
newFocus = normalizePoint(focus, value)
|
|
60
|
+
}
|
|
61
|
+
if (newAnchor && newFocus) {
|
|
62
|
+
return {anchor: newAnchor, focus: newFocus, backward: selection.backward}
|
|
63
|
+
}
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {type Patch} from '@sanity/diff-match-patch'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Takes a `patches` array as produced by diff-match-patch and adjusts the
|
|
5
|
+
* `start1` and `start2` properties so that they refer to UCS-2 index instead
|
|
6
|
+
* of a UTF-8 index.
|
|
7
|
+
*
|
|
8
|
+
* @param patches - The patches to adjust
|
|
9
|
+
* @param base - The base string to use for counting bytes
|
|
10
|
+
* @returns A new array of patches with adjusted indicies
|
|
11
|
+
* @beta
|
|
12
|
+
*/
|
|
13
|
+
export function adjustIndiciesToUcs2(patches: Patch[], base: string): Patch[] {
|
|
14
|
+
let byteOffset = 0
|
|
15
|
+
let idx = 0 // index into the input.
|
|
16
|
+
|
|
17
|
+
function advanceTo(target: number) {
|
|
18
|
+
for (; byteOffset < target; ) {
|
|
19
|
+
const codePoint = base.codePointAt(idx)
|
|
20
|
+
if (typeof codePoint === 'undefined') {
|
|
21
|
+
// Reached the end of the base string - the indicies won't be correct,
|
|
22
|
+
// but we also cannot advance any further to find a closer index.
|
|
23
|
+
return idx
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
byteOffset += utf8len(codePoint)
|
|
27
|
+
|
|
28
|
+
// This is encoded as a surrogate pair.
|
|
29
|
+
if (codePoint > 0xffff) {
|
|
30
|
+
idx += 2
|
|
31
|
+
} else {
|
|
32
|
+
idx += 1
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Theoretically, we should have reached target - however, due to differences in
|
|
37
|
+
// `base` from the string that the patch was originally based upon, occurences
|
|
38
|
+
// _can_ happen where we go beyond the target due to surrogate pairs or similar.
|
|
39
|
+
// In the PTE, this is okayish - best effort matching is good enough.
|
|
40
|
+
return idx
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const adjusted: Patch[] = []
|
|
44
|
+
for (const patch of patches) {
|
|
45
|
+
adjusted.push({
|
|
46
|
+
diffs: patch.diffs.map((diff) => [...diff]),
|
|
47
|
+
start1: advanceTo(patch.start1),
|
|
48
|
+
start2: advanceTo(patch.start2),
|
|
49
|
+
utf8Start1: patch.utf8Start1,
|
|
50
|
+
utf8Start2: patch.utf8Start2,
|
|
51
|
+
length1: patch.length1,
|
|
52
|
+
length2: patch.length2,
|
|
53
|
+
utf8Length1: patch.utf8Length1,
|
|
54
|
+
utf8Length2: patch.utf8Length2,
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return adjusted
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function utf8len(codePoint: number): 1 | 2 | 3 | 4 {
|
|
62
|
+
// See table at https://en.wikipedia.org/wiki/UTF-8
|
|
63
|
+
if (codePoint <= 0x007f) return 1
|
|
64
|
+
if (codePoint <= 0x07ff) return 2
|
|
65
|
+
if (codePoint <= 0xffff) return 3
|
|
66
|
+
return 4
|
|
67
|
+
}
|