@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,407 @@
|
|
|
1
|
+
/* eslint-disable max-statements */
|
|
2
|
+
import {
|
|
3
|
+
applyPatches as diffMatchPatchApplyPatches,
|
|
4
|
+
cleanupEfficiency,
|
|
5
|
+
DIFF_DELETE,
|
|
6
|
+
DIFF_EQUAL,
|
|
7
|
+
DIFF_INSERT,
|
|
8
|
+
makeDiff,
|
|
9
|
+
parsePatch,
|
|
10
|
+
} from '@sanity/diff-match-patch'
|
|
11
|
+
import {
|
|
12
|
+
type KeyedSegment,
|
|
13
|
+
type Path,
|
|
14
|
+
type PathSegment,
|
|
15
|
+
type PortableTextBlock,
|
|
16
|
+
type PortableTextChild,
|
|
17
|
+
} from '@sanity/types'
|
|
18
|
+
import {type Descendant, Element, type Node, type Path as SlatePath, Text, Transforms} from 'slate'
|
|
19
|
+
|
|
20
|
+
import {applyAll} from '../patch/applyPatch'
|
|
21
|
+
import {type PortableTextMemberSchemaTypes, type PortableTextSlateEditor} from '../types/editor'
|
|
22
|
+
import {
|
|
23
|
+
type DiffMatchPatch,
|
|
24
|
+
type InsertPatch,
|
|
25
|
+
type Patch,
|
|
26
|
+
type SetPatch,
|
|
27
|
+
type UnsetPatch,
|
|
28
|
+
} from '../types/patch'
|
|
29
|
+
import {debugWithName} from './debug'
|
|
30
|
+
import {toSlateValue} from './values'
|
|
31
|
+
import {KEY_TO_SLATE_ELEMENT} from './weakMaps'
|
|
32
|
+
|
|
33
|
+
const debug = debugWithName('applyPatches')
|
|
34
|
+
const debugVerbose = debug.enabled && true
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Creates a function that can apply a patch onto a PortableTextSlateEditor.
|
|
38
|
+
*/
|
|
39
|
+
export function createApplyPatch(
|
|
40
|
+
schemaTypes: PortableTextMemberSchemaTypes,
|
|
41
|
+
): (editor: PortableTextSlateEditor, patch: Patch) => boolean {
|
|
42
|
+
let previousPatch: Patch | undefined
|
|
43
|
+
|
|
44
|
+
return function (editor: PortableTextSlateEditor, patch: Patch): boolean {
|
|
45
|
+
let changed = false
|
|
46
|
+
|
|
47
|
+
// Save some CPU cycles by not stringifying unless enabled
|
|
48
|
+
if (debugVerbose) {
|
|
49
|
+
debug('\n\nNEW PATCH =============================================================')
|
|
50
|
+
debug(JSON.stringify(patch, null, 2))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
switch (patch.type) {
|
|
55
|
+
case 'insert':
|
|
56
|
+
changed = insertPatch(editor, patch, schemaTypes)
|
|
57
|
+
break
|
|
58
|
+
case 'unset':
|
|
59
|
+
changed = unsetPatch(editor, patch, previousPatch)
|
|
60
|
+
break
|
|
61
|
+
case 'set':
|
|
62
|
+
changed = setPatch(editor, patch)
|
|
63
|
+
break
|
|
64
|
+
case 'diffMatchPatch':
|
|
65
|
+
changed = diffMatchPatch(editor, patch)
|
|
66
|
+
break
|
|
67
|
+
default:
|
|
68
|
+
debug('Unhandled patch', patch.type)
|
|
69
|
+
}
|
|
70
|
+
} catch (err) {
|
|
71
|
+
console.error(err)
|
|
72
|
+
}
|
|
73
|
+
previousPatch = patch
|
|
74
|
+
return changed
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Apply a remote diff match patch to the current PTE instance.
|
|
80
|
+
* Note meant for external consumption, only exported for testing purposes.
|
|
81
|
+
*
|
|
82
|
+
* @param editor - Portable text slate editor instance
|
|
83
|
+
* @param patch - The PTE diff match patch operation to apply
|
|
84
|
+
* @returns true if the patch was applied, false otherwise
|
|
85
|
+
* @internal
|
|
86
|
+
*/
|
|
87
|
+
export function diffMatchPatch(
|
|
88
|
+
editor: Pick<
|
|
89
|
+
PortableTextSlateEditor,
|
|
90
|
+
'children' | 'isTextBlock' | 'apply' | 'selection' | 'onChange'
|
|
91
|
+
>,
|
|
92
|
+
patch: DiffMatchPatch,
|
|
93
|
+
): boolean {
|
|
94
|
+
const {block, child, childPath} = findBlockAndChildFromPath(editor, patch.path)
|
|
95
|
+
if (!block) {
|
|
96
|
+
debug('Block not found')
|
|
97
|
+
return false
|
|
98
|
+
}
|
|
99
|
+
if (!child || !childPath) {
|
|
100
|
+
debug('Child not found')
|
|
101
|
+
return false
|
|
102
|
+
}
|
|
103
|
+
const isSpanTextDiffMatchPatch =
|
|
104
|
+
block &&
|
|
105
|
+
editor.isTextBlock(block) &&
|
|
106
|
+
patch.path.length === 4 &&
|
|
107
|
+
patch.path[1] === 'children' &&
|
|
108
|
+
patch.path[3] === 'text'
|
|
109
|
+
|
|
110
|
+
if (!isSpanTextDiffMatchPatch || !Text.isText(child)) {
|
|
111
|
+
return false
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const patches = parsePatch(patch.value)
|
|
115
|
+
const [newValue] = diffMatchPatchApplyPatches(patches, child.text, {allowExceedingIndices: true})
|
|
116
|
+
const diff = cleanupEfficiency(makeDiff(child.text, newValue), 5)
|
|
117
|
+
|
|
118
|
+
debugState(editor, 'before')
|
|
119
|
+
let offset = 0
|
|
120
|
+
for (const [op, text] of diff) {
|
|
121
|
+
if (op === DIFF_INSERT) {
|
|
122
|
+
editor.apply({type: 'insert_text', path: childPath, offset, text})
|
|
123
|
+
offset += text.length
|
|
124
|
+
} else if (op === DIFF_DELETE) {
|
|
125
|
+
editor.apply({type: 'remove_text', path: childPath, offset: offset, text})
|
|
126
|
+
} else if (op === DIFF_EQUAL) {
|
|
127
|
+
offset += text.length
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
debugState(editor, 'after')
|
|
131
|
+
|
|
132
|
+
return true
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function insertPatch(
|
|
136
|
+
editor: PortableTextSlateEditor,
|
|
137
|
+
patch: InsertPatch,
|
|
138
|
+
schemaTypes: PortableTextMemberSchemaTypes,
|
|
139
|
+
) {
|
|
140
|
+
const {
|
|
141
|
+
block: targetBlock,
|
|
142
|
+
child: targetChild,
|
|
143
|
+
blockPath: targetBlockPath,
|
|
144
|
+
childPath: targetChildPath,
|
|
145
|
+
} = findBlockAndChildFromPath(editor, patch.path)
|
|
146
|
+
if (!targetBlock || !targetBlockPath) {
|
|
147
|
+
debug('Block not found')
|
|
148
|
+
return false
|
|
149
|
+
}
|
|
150
|
+
if (patch.path.length > 1 && patch.path[1] !== 'children') {
|
|
151
|
+
debug('Ignoring patch targeting void value')
|
|
152
|
+
return false
|
|
153
|
+
}
|
|
154
|
+
// Insert blocks
|
|
155
|
+
if (patch.path.length === 1) {
|
|
156
|
+
const {items, position} = patch
|
|
157
|
+
const blocksToInsert = toSlateValue(
|
|
158
|
+
items as PortableTextBlock[],
|
|
159
|
+
{schemaTypes},
|
|
160
|
+
KEY_TO_SLATE_ELEMENT.get(editor),
|
|
161
|
+
) as Descendant[]
|
|
162
|
+
const targetBlockIndex = targetBlockPath[0]
|
|
163
|
+
const normalizedIdx = position === 'after' ? targetBlockIndex + 1 : targetBlockIndex
|
|
164
|
+
debug(`Inserting blocks at path [${normalizedIdx}]`)
|
|
165
|
+
debugState(editor, 'before')
|
|
166
|
+
Transforms.insertNodes(editor, blocksToInsert, {at: [normalizedIdx]})
|
|
167
|
+
debugState(editor, 'after')
|
|
168
|
+
return true
|
|
169
|
+
}
|
|
170
|
+
// Insert children
|
|
171
|
+
const {items, position} = patch
|
|
172
|
+
if (!targetChild || !targetChildPath) {
|
|
173
|
+
debug('Child not found')
|
|
174
|
+
return false
|
|
175
|
+
}
|
|
176
|
+
const childrenToInsert =
|
|
177
|
+
targetBlock &&
|
|
178
|
+
toSlateValue(
|
|
179
|
+
[{...targetBlock, children: items as PortableTextChild[]}],
|
|
180
|
+
{schemaTypes},
|
|
181
|
+
KEY_TO_SLATE_ELEMENT.get(editor),
|
|
182
|
+
)
|
|
183
|
+
const targetChildIndex = targetChildPath[1]
|
|
184
|
+
const normalizedIdx = position === 'after' ? targetChildIndex + 1 : targetChildIndex
|
|
185
|
+
const childInsertPath = [targetChildPath[0], normalizedIdx]
|
|
186
|
+
debug(`Inserting children at path ${childInsertPath}`)
|
|
187
|
+
debugState(editor, 'before')
|
|
188
|
+
if (childrenToInsert && Element.isElement(childrenToInsert[0])) {
|
|
189
|
+
Transforms.insertNodes(editor, childrenToInsert[0].children, {at: childInsertPath})
|
|
190
|
+
}
|
|
191
|
+
debugState(editor, 'after')
|
|
192
|
+
return true
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function setPatch(editor: PortableTextSlateEditor, patch: SetPatch) {
|
|
196
|
+
let value = patch.value
|
|
197
|
+
if (typeof patch.path[3] === 'string') {
|
|
198
|
+
value = {}
|
|
199
|
+
value[patch.path[3]] = patch.value
|
|
200
|
+
}
|
|
201
|
+
const {block, blockPath, child, childPath} = findBlockAndChildFromPath(editor, patch.path)
|
|
202
|
+
|
|
203
|
+
if (!block) {
|
|
204
|
+
debug('Block not found')
|
|
205
|
+
return false
|
|
206
|
+
}
|
|
207
|
+
const isTextBlock = editor.isTextBlock(block)
|
|
208
|
+
|
|
209
|
+
// Ignore patches targeting nested void data, like 'markDefs'
|
|
210
|
+
if (isTextBlock && patch.path.length > 1 && patch.path[1] !== 'children') {
|
|
211
|
+
debug('Ignoring setting void value')
|
|
212
|
+
return false
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
debugState(editor, 'before')
|
|
216
|
+
|
|
217
|
+
// If this is targeting a text block child
|
|
218
|
+
if (isTextBlock && child && childPath) {
|
|
219
|
+
if (Text.isText(value) && Text.isText(child)) {
|
|
220
|
+
const newText = child.text
|
|
221
|
+
const oldText = value.text
|
|
222
|
+
if (oldText !== newText) {
|
|
223
|
+
debug('Setting text property')
|
|
224
|
+
editor.apply({
|
|
225
|
+
type: 'remove_text',
|
|
226
|
+
path: childPath,
|
|
227
|
+
offset: 0,
|
|
228
|
+
text: newText,
|
|
229
|
+
})
|
|
230
|
+
editor.apply({
|
|
231
|
+
type: 'insert_text',
|
|
232
|
+
path: childPath,
|
|
233
|
+
offset: 0,
|
|
234
|
+
text: value.text,
|
|
235
|
+
})
|
|
236
|
+
// call OnChange here to emit the new selection
|
|
237
|
+
// the user's selection might be interfering with
|
|
238
|
+
editor.onChange()
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
debug('Setting non-text property')
|
|
242
|
+
editor.apply({
|
|
243
|
+
type: 'set_node',
|
|
244
|
+
path: childPath,
|
|
245
|
+
properties: {},
|
|
246
|
+
newProperties: value as Partial<Node>,
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
return true
|
|
250
|
+
} else if (Element.isElement(block) && patch.path.length === 1 && blockPath) {
|
|
251
|
+
debug('Setting block property')
|
|
252
|
+
const {children, ...nextRest} = value as unknown as PortableTextBlock
|
|
253
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
|
|
254
|
+
const {children: prevChildren, ...prevRest} = block || {children: undefined}
|
|
255
|
+
// Set any block properties
|
|
256
|
+
editor.apply({
|
|
257
|
+
type: 'set_node',
|
|
258
|
+
path: blockPath,
|
|
259
|
+
properties: {...prevRest},
|
|
260
|
+
newProperties: nextRest,
|
|
261
|
+
})
|
|
262
|
+
// Replace the children in the block
|
|
263
|
+
// Note that children must be explicitly inserted, and can't be set with set_node
|
|
264
|
+
debug('Setting children')
|
|
265
|
+
block.children.forEach((c, cIndex) => {
|
|
266
|
+
editor.apply({
|
|
267
|
+
type: 'remove_node',
|
|
268
|
+
path: blockPath.concat(block.children.length - 1 - cIndex),
|
|
269
|
+
node: c,
|
|
270
|
+
})
|
|
271
|
+
})
|
|
272
|
+
if (Array.isArray(children)) {
|
|
273
|
+
children.forEach((c, cIndex) => {
|
|
274
|
+
editor.apply({
|
|
275
|
+
type: 'insert_node',
|
|
276
|
+
path: blockPath.concat(cIndex),
|
|
277
|
+
node: c,
|
|
278
|
+
})
|
|
279
|
+
})
|
|
280
|
+
}
|
|
281
|
+
} else if (block && 'value' in block) {
|
|
282
|
+
const newVal = applyAll([block.value], [patch])[0]
|
|
283
|
+
Transforms.setNodes(editor, {...block, value: newVal}, {at: blockPath})
|
|
284
|
+
return true
|
|
285
|
+
}
|
|
286
|
+
debugState(editor, 'after')
|
|
287
|
+
return true
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function unsetPatch(editor: PortableTextSlateEditor, patch: UnsetPatch, previousPatch?: Patch) {
|
|
291
|
+
// Value
|
|
292
|
+
if (patch.path.length === 0) {
|
|
293
|
+
debug('Removing everything')
|
|
294
|
+
debugState(editor, 'before')
|
|
295
|
+
const previousSelection = editor.selection
|
|
296
|
+
Transforms.deselect(editor)
|
|
297
|
+
editor.children.forEach((c, i) => {
|
|
298
|
+
Transforms.removeNodes(editor, {at: [i]})
|
|
299
|
+
})
|
|
300
|
+
Transforms.insertNodes(editor, editor.pteCreateEmptyBlock())
|
|
301
|
+
if (previousSelection) {
|
|
302
|
+
Transforms.select(editor, {
|
|
303
|
+
anchor: {path: [0, 0], offset: 0},
|
|
304
|
+
focus: {path: [0, 0], offset: 0},
|
|
305
|
+
})
|
|
306
|
+
}
|
|
307
|
+
// call OnChange here to emit the new selection
|
|
308
|
+
editor.onChange()
|
|
309
|
+
debugState(editor, 'after')
|
|
310
|
+
return true
|
|
311
|
+
}
|
|
312
|
+
const {block, blockPath, child, childPath} = findBlockAndChildFromPath(editor, patch.path)
|
|
313
|
+
|
|
314
|
+
// Single blocks
|
|
315
|
+
if (patch.path.length === 1) {
|
|
316
|
+
if (!block || !blockPath) {
|
|
317
|
+
debug('Block not found')
|
|
318
|
+
return false
|
|
319
|
+
}
|
|
320
|
+
const blockIndex = blockPath[0]
|
|
321
|
+
debug(`Removing block at path [${blockIndex}]`)
|
|
322
|
+
debugState(editor, 'before')
|
|
323
|
+
|
|
324
|
+
Transforms.removeNodes(editor, {at: [blockIndex]})
|
|
325
|
+
debugState(editor, 'after')
|
|
326
|
+
return true
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Unset on text block children
|
|
330
|
+
if (editor.isTextBlock(block) && patch.path[1] === 'children' && patch.path.length === 3) {
|
|
331
|
+
if (!child || !childPath) {
|
|
332
|
+
debug('Child not found')
|
|
333
|
+
return false
|
|
334
|
+
}
|
|
335
|
+
debug(`Unsetting child at path ${JSON.stringify(childPath)}`)
|
|
336
|
+
debugState(editor, 'before')
|
|
337
|
+
if (debugVerbose) {
|
|
338
|
+
debug(`Removing child at path ${JSON.stringify(childPath)}`)
|
|
339
|
+
}
|
|
340
|
+
Transforms.removeNodes(editor, {at: childPath})
|
|
341
|
+
debugState(editor, 'after')
|
|
342
|
+
return true
|
|
343
|
+
}
|
|
344
|
+
return false
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function isKeyedSegment(segment: PathSegment): segment is KeyedSegment {
|
|
348
|
+
return typeof segment === 'object' && '_key' in segment
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function debugState(
|
|
352
|
+
editor: Pick<PortableTextSlateEditor, 'children' | 'isTextBlock' | 'apply' | 'selection'>,
|
|
353
|
+
stateName: string,
|
|
354
|
+
) {
|
|
355
|
+
if (!debugVerbose) {
|
|
356
|
+
return
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
debug(`Children ${stateName}:`, JSON.stringify(editor.children, null, 2))
|
|
360
|
+
debug(`Selection ${stateName}: `, JSON.stringify(editor.selection, null, 2))
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function findBlockFromPath(
|
|
364
|
+
editor: Pick<
|
|
365
|
+
PortableTextSlateEditor,
|
|
366
|
+
'children' | 'isTextBlock' | 'apply' | 'selection' | 'onChange'
|
|
367
|
+
>,
|
|
368
|
+
path: Path,
|
|
369
|
+
): {block?: Descendant; path?: SlatePath} {
|
|
370
|
+
let blockIndex = -1
|
|
371
|
+
const block = editor.children.find((node: Descendant, index: number) => {
|
|
372
|
+
const isMatch = isKeyedSegment(path[0]) ? node._key === path[0]._key : index === path[0]
|
|
373
|
+
if (isMatch) {
|
|
374
|
+
blockIndex = index
|
|
375
|
+
}
|
|
376
|
+
return isMatch
|
|
377
|
+
})
|
|
378
|
+
if (!block) {
|
|
379
|
+
return {}
|
|
380
|
+
}
|
|
381
|
+
return {block, path: [blockIndex] as SlatePath}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function findBlockAndChildFromPath(
|
|
385
|
+
editor: Pick<
|
|
386
|
+
PortableTextSlateEditor,
|
|
387
|
+
'children' | 'isTextBlock' | 'apply' | 'selection' | 'onChange'
|
|
388
|
+
>,
|
|
389
|
+
path: Path,
|
|
390
|
+
): {child?: Descendant; childPath?: SlatePath; block?: Descendant; blockPath?: SlatePath} {
|
|
391
|
+
const {block, path: blockPath} = findBlockFromPath(editor, path)
|
|
392
|
+
if (!(Element.isElement(block) && path[1] === 'children')) {
|
|
393
|
+
return {block, blockPath, child: undefined, childPath: undefined}
|
|
394
|
+
}
|
|
395
|
+
let childIndex = -1
|
|
396
|
+
const child = block.children.find((node, index: number) => {
|
|
397
|
+
const isMatch = isKeyedSegment(path[2]) ? node._key === path[2]._key : index === path[2]
|
|
398
|
+
if (isMatch) {
|
|
399
|
+
childIndex = index
|
|
400
|
+
}
|
|
401
|
+
return isMatch
|
|
402
|
+
})
|
|
403
|
+
if (!child) {
|
|
404
|
+
return {block, blockPath, child: undefined, childPath: undefined}
|
|
405
|
+
}
|
|
406
|
+
return {block, child, blockPath, childPath: blockPath?.concat(childIndex) as SlatePath}
|
|
407
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {defer, EMPTY, type Observable, of, type OperatorFunction, switchMap, tap} from 'rxjs'
|
|
2
|
+
|
|
3
|
+
export function bufferUntil<T>(
|
|
4
|
+
emitWhen: (currentBuffer: T[]) => boolean,
|
|
5
|
+
): OperatorFunction<T, T[]> {
|
|
6
|
+
return (source: Observable<T>) =>
|
|
7
|
+
defer(() => {
|
|
8
|
+
let buffer: T[] = [] // custom buffer
|
|
9
|
+
return source.pipe(
|
|
10
|
+
tap((v) => buffer.push(v)), // add values to buffer
|
|
11
|
+
switchMap(() => (emitWhen(buffer) ? of(buffer) : EMPTY)), // emit the buffer when the condition is met
|
|
12
|
+
tap(() => (buffer = [])), // clear the buffer
|
|
13
|
+
)
|
|
14
|
+
})
|
|
15
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import debug from 'debug'
|
|
2
|
+
|
|
3
|
+
const rootName = 'sanity-pte:'
|
|
4
|
+
|
|
5
|
+
export default debug(rootName)
|
|
6
|
+
export function debugWithName(name: string): debug.Debugger {
|
|
7
|
+
const namespace = `${rootName}${name}`
|
|
8
|
+
if (debug && debug.enabled(namespace)) {
|
|
9
|
+
return debug(namespace)
|
|
10
|
+
}
|
|
11
|
+
return debug(rootName)
|
|
12
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ArraySchemaType,
|
|
3
|
+
type BlockSchemaType,
|
|
4
|
+
type ObjectSchemaType,
|
|
5
|
+
type PortableTextBlock,
|
|
6
|
+
type SchemaType,
|
|
7
|
+
type SpanSchemaType,
|
|
8
|
+
} from '@sanity/types'
|
|
9
|
+
|
|
10
|
+
import {type PortableTextMemberSchemaTypes} from '../types/editor'
|
|
11
|
+
|
|
12
|
+
export function getPortableTextMemberSchemaTypes(
|
|
13
|
+
portableTextType: ArraySchemaType<PortableTextBlock>,
|
|
14
|
+
): PortableTextMemberSchemaTypes {
|
|
15
|
+
if (!portableTextType) {
|
|
16
|
+
throw new Error("Parameter 'portabletextType' missing (required)")
|
|
17
|
+
}
|
|
18
|
+
const blockType = portableTextType.of?.find(findBlockType) as BlockSchemaType | undefined
|
|
19
|
+
if (!blockType) {
|
|
20
|
+
throw new Error('Block type is not defined in this schema (required)')
|
|
21
|
+
}
|
|
22
|
+
const childrenField = blockType.fields?.find((field) => field.name === 'children') as
|
|
23
|
+
| {type: ArraySchemaType}
|
|
24
|
+
| undefined
|
|
25
|
+
if (!childrenField) {
|
|
26
|
+
throw new Error('Children field for block type found in schema (required)')
|
|
27
|
+
}
|
|
28
|
+
const ofType = childrenField.type.of
|
|
29
|
+
if (!ofType) {
|
|
30
|
+
throw new Error('Valid types for block children not found in schema (required)')
|
|
31
|
+
}
|
|
32
|
+
const spanType = ofType.find((memberType) => memberType.name === 'span') as
|
|
33
|
+
| ObjectSchemaType
|
|
34
|
+
| undefined
|
|
35
|
+
if (!spanType) {
|
|
36
|
+
throw new Error('Span type not found in schema (required)')
|
|
37
|
+
}
|
|
38
|
+
const inlineObjectTypes = (ofType.filter((memberType) => memberType.name !== 'span') ||
|
|
39
|
+
[]) as ObjectSchemaType[]
|
|
40
|
+
const blockObjectTypes = (portableTextType.of?.filter((field) => field.name !== blockType.name) ||
|
|
41
|
+
[]) as ObjectSchemaType[]
|
|
42
|
+
return {
|
|
43
|
+
styles: resolveEnabledStyles(blockType),
|
|
44
|
+
decorators: resolveEnabledDecorators(spanType),
|
|
45
|
+
lists: resolveEnabledListItems(blockType),
|
|
46
|
+
block: blockType,
|
|
47
|
+
span: spanType,
|
|
48
|
+
portableText: portableTextType,
|
|
49
|
+
inlineObjects: inlineObjectTypes,
|
|
50
|
+
blockObjects: blockObjectTypes,
|
|
51
|
+
annotations: (spanType as SpanSchemaType).annotations,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function resolveEnabledStyles(blockType: ObjectSchemaType) {
|
|
56
|
+
const styleField = blockType.fields?.find((btField) => btField.name === 'style')
|
|
57
|
+
if (!styleField) {
|
|
58
|
+
throw new Error("A field with name 'style' is not defined in the block type (required).")
|
|
59
|
+
}
|
|
60
|
+
const textStyles =
|
|
61
|
+
styleField.type.options?.list &&
|
|
62
|
+
styleField.type.options.list?.filter((style: {value: string}) => style.value)
|
|
63
|
+
if (!textStyles || textStyles.length === 0) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
'The style fields need at least one style ' +
|
|
66
|
+
"defined. I.e: {title: 'Normal', value: 'normal'}.",
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
return textStyles
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function resolveEnabledDecorators(spanType: ObjectSchemaType) {
|
|
73
|
+
return (spanType as any).decorators
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function resolveEnabledListItems(blockType: ObjectSchemaType) {
|
|
77
|
+
const listField = blockType.fields?.find((btField) => btField.name === 'listItem')
|
|
78
|
+
if (!listField) {
|
|
79
|
+
throw new Error("A field with name 'listItem' is not defined in the block type (required).")
|
|
80
|
+
}
|
|
81
|
+
const listItems =
|
|
82
|
+
listField.type.options?.list &&
|
|
83
|
+
listField.type.options.list.filter((list: {value: string}) => list.value)
|
|
84
|
+
if (!listItems) {
|
|
85
|
+
throw new Error('The list field need at least to be an empty array')
|
|
86
|
+
}
|
|
87
|
+
return listItems
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function findBlockType(type: SchemaType): BlockSchemaType | null {
|
|
91
|
+
if (type.type) {
|
|
92
|
+
return findBlockType(type.type)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (type.name === 'block') {
|
|
96
|
+
return type as BlockSchemaType
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return null
|
|
100
|
+
}
|