@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,394 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isPortableTextTextBlock,
|
|
3
|
+
type PortableTextBlock,
|
|
4
|
+
type PortableTextSpan,
|
|
5
|
+
type PortableTextTextBlock,
|
|
6
|
+
} from '@sanity/types'
|
|
7
|
+
import {flatten, isPlainObject, uniq} from 'lodash'
|
|
8
|
+
|
|
9
|
+
import {insert, set, setIfMissing, unset} from '../patch/PatchEvent'
|
|
10
|
+
import {type InvalidValueResolution, type PortableTextMemberSchemaTypes} from '../types/editor'
|
|
11
|
+
import {EMPTY_MARKDEFS} from './values'
|
|
12
|
+
|
|
13
|
+
export interface Validation {
|
|
14
|
+
valid: boolean
|
|
15
|
+
resolution: InvalidValueResolution | null
|
|
16
|
+
value: PortableTextBlock[] | undefined
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function validateValue(
|
|
20
|
+
value: PortableTextBlock[] | undefined,
|
|
21
|
+
types: PortableTextMemberSchemaTypes,
|
|
22
|
+
keyGenerator: () => string,
|
|
23
|
+
): Validation {
|
|
24
|
+
let resolution: InvalidValueResolution | null = null
|
|
25
|
+
let valid = true
|
|
26
|
+
const validChildTypes = [types.span.name, ...types.inlineObjects.map((t) => t.name)]
|
|
27
|
+
const validBlockTypes = [types.block.name, ...types.blockObjects.map((t) => t.name)]
|
|
28
|
+
|
|
29
|
+
// Undefined is allowed
|
|
30
|
+
if (value === undefined) {
|
|
31
|
+
return {valid: true, resolution: null, value}
|
|
32
|
+
}
|
|
33
|
+
// Only lengthy arrays are allowed in the editor.
|
|
34
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
35
|
+
return {
|
|
36
|
+
valid: false,
|
|
37
|
+
resolution: {
|
|
38
|
+
patches: [unset([])],
|
|
39
|
+
description: 'Editor value must be an array of Portable Text blocks, or undefined.',
|
|
40
|
+
action: 'Unset the value',
|
|
41
|
+
item: value,
|
|
42
|
+
|
|
43
|
+
i18n: {
|
|
44
|
+
description: 'inputs.portable-text.invalid-value.not-an-array.description',
|
|
45
|
+
action: 'inputs.portable-text.invalid-value.not-an-array.action',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
value,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (
|
|
52
|
+
value.some((blk: PortableTextBlock, index: number): boolean => {
|
|
53
|
+
// Is the block an object?
|
|
54
|
+
if (!isPlainObject(blk)) {
|
|
55
|
+
resolution = {
|
|
56
|
+
patches: [unset([index])],
|
|
57
|
+
description: `Block must be an object, got ${String(blk)}`,
|
|
58
|
+
action: `Unset invalid item`,
|
|
59
|
+
item: blk,
|
|
60
|
+
|
|
61
|
+
i18n: {
|
|
62
|
+
description: 'inputs.portable-text.invalid-value.not-an-object.description',
|
|
63
|
+
action: 'inputs.portable-text.invalid-value.not-an-object.action',
|
|
64
|
+
values: {index},
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
return true
|
|
68
|
+
}
|
|
69
|
+
// Test that every block has a _key prop
|
|
70
|
+
if (!blk._key || typeof blk._key !== 'string') {
|
|
71
|
+
resolution = {
|
|
72
|
+
patches: [set({...blk, _key: keyGenerator()}, [index])],
|
|
73
|
+
description: `Block at index ${index} is missing required _key.`,
|
|
74
|
+
action: 'Set the block with a random _key value',
|
|
75
|
+
item: blk,
|
|
76
|
+
|
|
77
|
+
i18n: {
|
|
78
|
+
description: 'inputs.portable-text.invalid-value.missing-key.description',
|
|
79
|
+
action: 'inputs.portable-text.invalid-value.missing-key.action',
|
|
80
|
+
values: {index},
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
return true
|
|
84
|
+
}
|
|
85
|
+
// Test that every block has valid _type
|
|
86
|
+
if (!blk._type || !validBlockTypes.includes(blk._type)) {
|
|
87
|
+
// Special case where block type is set to default 'block', but the block type is named something else according to the schema.
|
|
88
|
+
if (blk._type === 'block') {
|
|
89
|
+
const currentBlockTypeName = types.block.name
|
|
90
|
+
resolution = {
|
|
91
|
+
patches: [set({...blk, _type: currentBlockTypeName}, [{_key: blk._key}])],
|
|
92
|
+
description: `Block with _key '${blk._key}' has invalid type name '${blk._type}'. According to the schema, the block type name is '${currentBlockTypeName}'`,
|
|
93
|
+
action: `Use type '${currentBlockTypeName}'`,
|
|
94
|
+
item: blk,
|
|
95
|
+
|
|
96
|
+
i18n: {
|
|
97
|
+
description: 'inputs.portable-text.invalid-value.incorrect-block-type.description',
|
|
98
|
+
action: 'inputs.portable-text.invalid-value.incorrect-block-type.action',
|
|
99
|
+
values: {key: blk._key, expectedTypeName: currentBlockTypeName},
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
return true
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// If the block has no `_type`, but aside from that is a valid Portable Text block
|
|
106
|
+
if (!blk._type && isPortableTextTextBlock({...blk, _type: types.block.name})) {
|
|
107
|
+
resolution = {
|
|
108
|
+
patches: [set({...blk, _type: types.block.name}, [{_key: blk._key}])],
|
|
109
|
+
description: `Block with _key '${blk._key}' is missing a type name. According to the schema, the block type name is '${types.block.name}'`,
|
|
110
|
+
action: `Use type '${types.block.name}'`,
|
|
111
|
+
item: blk,
|
|
112
|
+
|
|
113
|
+
i18n: {
|
|
114
|
+
description: 'inputs.portable-text.invalid-value.missing-block-type.description',
|
|
115
|
+
action: 'inputs.portable-text.invalid-value.missing-block-type.action',
|
|
116
|
+
values: {key: blk._key, expectedTypeName: types.block.name},
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
return true
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!blk._type) {
|
|
123
|
+
resolution = {
|
|
124
|
+
patches: [unset([{_key: blk._key}])],
|
|
125
|
+
description: `Block with _key '${blk._key}' is missing an _type property`,
|
|
126
|
+
action: 'Remove the block',
|
|
127
|
+
item: blk,
|
|
128
|
+
|
|
129
|
+
i18n: {
|
|
130
|
+
description: 'inputs.portable-text.invalid-value.missing-type.description',
|
|
131
|
+
action: 'inputs.portable-text.invalid-value.missing-type.action',
|
|
132
|
+
values: {key: blk._key},
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
return true
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
resolution = {
|
|
139
|
+
patches: [unset([{_key: blk._key}])],
|
|
140
|
+
description: `Block with _key '${blk._key}' has invalid _type '${blk._type}'`,
|
|
141
|
+
action: 'Remove the block',
|
|
142
|
+
item: blk,
|
|
143
|
+
|
|
144
|
+
i18n: {
|
|
145
|
+
description: 'inputs.portable-text.invalid-value.disallowed-type.description',
|
|
146
|
+
action: 'inputs.portable-text.invalid-value.disallowed-type.action',
|
|
147
|
+
values: {key: blk._key, typeName: blk._type},
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
return true
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Test regular text blocks
|
|
154
|
+
if (blk._type === types.block.name) {
|
|
155
|
+
const textBlock = blk as PortableTextTextBlock
|
|
156
|
+
// Test that it has a valid children property (array)
|
|
157
|
+
if (textBlock.children && !Array.isArray(textBlock.children)) {
|
|
158
|
+
resolution = {
|
|
159
|
+
patches: [set({children: []}, [{_key: textBlock._key}])],
|
|
160
|
+
description: `Text block with _key '${textBlock._key}' has a invalid required property 'children'.`,
|
|
161
|
+
action: 'Reset the children property',
|
|
162
|
+
item: textBlock,
|
|
163
|
+
|
|
164
|
+
i18n: {
|
|
165
|
+
description:
|
|
166
|
+
'inputs.portable-text.invalid-value.missing-or-invalid-children.description',
|
|
167
|
+
action: 'inputs.portable-text.invalid-value.missing-or-invalid-children.action',
|
|
168
|
+
values: {key: textBlock._key},
|
|
169
|
+
},
|
|
170
|
+
}
|
|
171
|
+
return true
|
|
172
|
+
}
|
|
173
|
+
// Test that children is set and lengthy
|
|
174
|
+
if (
|
|
175
|
+
textBlock.children === undefined ||
|
|
176
|
+
(Array.isArray(textBlock.children) && textBlock.children.length === 0)
|
|
177
|
+
) {
|
|
178
|
+
const newSpan = {
|
|
179
|
+
_type: types.span.name,
|
|
180
|
+
_key: keyGenerator(),
|
|
181
|
+
text: '',
|
|
182
|
+
marks: [],
|
|
183
|
+
}
|
|
184
|
+
resolution = {
|
|
185
|
+
autoResolve: true,
|
|
186
|
+
patches: [
|
|
187
|
+
setIfMissing([], [{_key: blk._key}, 'children']),
|
|
188
|
+
insert([newSpan], 'after', [{_key: blk._key}, 'children', 0]),
|
|
189
|
+
],
|
|
190
|
+
description: `Children for text block with _key '${blk._key}' is empty.`,
|
|
191
|
+
action: 'Insert an empty text',
|
|
192
|
+
item: blk,
|
|
193
|
+
|
|
194
|
+
i18n: {
|
|
195
|
+
description: 'inputs.portable-text.invalid-value.empty-children.description',
|
|
196
|
+
action: 'inputs.portable-text.invalid-value.empty-children.action',
|
|
197
|
+
values: {key: blk._key},
|
|
198
|
+
},
|
|
199
|
+
}
|
|
200
|
+
return true
|
|
201
|
+
}
|
|
202
|
+
// Test that markDefs are valid if they exists
|
|
203
|
+
if (blk.markDefs && !Array.isArray(blk.markDefs)) {
|
|
204
|
+
resolution = {
|
|
205
|
+
patches: [set({...textBlock, markDefs: EMPTY_MARKDEFS}, [{_key: textBlock._key}])],
|
|
206
|
+
description: `Block has invalid required property 'markDefs'.`,
|
|
207
|
+
action: 'Add empty markDefs array',
|
|
208
|
+
item: textBlock,
|
|
209
|
+
|
|
210
|
+
i18n: {
|
|
211
|
+
description:
|
|
212
|
+
'inputs.portable-text.invalid-value.missing-or-invalid-markdefs.description',
|
|
213
|
+
action: 'inputs.portable-text.invalid-value.missing-or-invalid-markdefs.action',
|
|
214
|
+
values: {key: textBlock._key},
|
|
215
|
+
},
|
|
216
|
+
}
|
|
217
|
+
return true
|
|
218
|
+
}
|
|
219
|
+
const allUsedMarks = uniq(
|
|
220
|
+
flatten(
|
|
221
|
+
textBlock.children
|
|
222
|
+
.filter((cld) => cld._type === types.span.name)
|
|
223
|
+
.map((cld) => cld.marks || []),
|
|
224
|
+
) as string[],
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
// Test that all markDefs are in use (remove orphaned markDefs)
|
|
228
|
+
if (Array.isArray(blk.markDefs) && blk.markDefs.length > 0) {
|
|
229
|
+
const unusedMarkDefs: string[] = uniq(
|
|
230
|
+
blk.markDefs.map((def) => def._key).filter((key) => !allUsedMarks.includes(key)),
|
|
231
|
+
)
|
|
232
|
+
if (unusedMarkDefs.length > 0) {
|
|
233
|
+
resolution = {
|
|
234
|
+
autoResolve: true,
|
|
235
|
+
patches: unusedMarkDefs.map((markDefKey) =>
|
|
236
|
+
unset([{_key: blk._key}, 'markDefs', {_key: markDefKey}]),
|
|
237
|
+
),
|
|
238
|
+
description: `Block contains orphaned data (unused mark definitions): ${unusedMarkDefs.join(
|
|
239
|
+
', ',
|
|
240
|
+
)}.`,
|
|
241
|
+
action: 'Remove unused mark definition item',
|
|
242
|
+
item: blk,
|
|
243
|
+
i18n: {
|
|
244
|
+
description: 'inputs.portable-text.invalid-value.orphaned-mark-defs.description',
|
|
245
|
+
action: 'inputs.portable-text.invalid-value.orphaned-mark-defs.action',
|
|
246
|
+
values: {key: blk._key, unusedMarkDefs: unusedMarkDefs.map((m) => m.toString())},
|
|
247
|
+
},
|
|
248
|
+
}
|
|
249
|
+
return true
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Test that every annotation mark used has a definition
|
|
254
|
+
const annotationMarks = allUsedMarks.filter(
|
|
255
|
+
(mark) => !types.decorators.map((dec) => dec.value).includes(mark),
|
|
256
|
+
)
|
|
257
|
+
const orphanedMarks = annotationMarks.filter(
|
|
258
|
+
(mark) =>
|
|
259
|
+
textBlock.markDefs === undefined ||
|
|
260
|
+
!textBlock.markDefs.find((def) => def._key === mark),
|
|
261
|
+
)
|
|
262
|
+
if (orphanedMarks.length > 0) {
|
|
263
|
+
const spanChildren = textBlock.children.filter(
|
|
264
|
+
(cld) =>
|
|
265
|
+
cld._type === types.span.name &&
|
|
266
|
+
Array.isArray(cld.marks) &&
|
|
267
|
+
cld.marks.some((mark) => orphanedMarks.includes(mark)),
|
|
268
|
+
) as PortableTextSpan[]
|
|
269
|
+
if (spanChildren) {
|
|
270
|
+
const orphaned = orphanedMarks.join(', ')
|
|
271
|
+
resolution = {
|
|
272
|
+
autoResolve: true,
|
|
273
|
+
patches: spanChildren.map((child) => {
|
|
274
|
+
return set(
|
|
275
|
+
(child.marks || []).filter((cMrk) => !orphanedMarks.includes(cMrk)),
|
|
276
|
+
[{_key: blk._key}, 'children', {_key: child._key}, 'marks'],
|
|
277
|
+
)
|
|
278
|
+
}),
|
|
279
|
+
description: `Block with _key '${blk._key}' contains marks (${orphaned}) not supported by the current content model.`,
|
|
280
|
+
action: 'Remove invalid marks',
|
|
281
|
+
item: blk,
|
|
282
|
+
|
|
283
|
+
i18n: {
|
|
284
|
+
description: 'inputs.portable-text.invalid-value.orphaned-marks.description',
|
|
285
|
+
action: 'inputs.portable-text.invalid-value.orphaned-marks.action',
|
|
286
|
+
values: {key: blk._key, orphanedMarks: orphanedMarks.map((m) => m.toString())},
|
|
287
|
+
},
|
|
288
|
+
}
|
|
289
|
+
return true
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Test every child
|
|
294
|
+
if (
|
|
295
|
+
textBlock.children.some((child, cIndex: number) => {
|
|
296
|
+
if (!isPlainObject(child)) {
|
|
297
|
+
resolution = {
|
|
298
|
+
patches: [unset([{_key: blk._key}, 'children', cIndex])],
|
|
299
|
+
description: `Child at index '${cIndex}' in block with key '${blk._key}' is not an object.`,
|
|
300
|
+
action: 'Remove the item',
|
|
301
|
+
item: blk,
|
|
302
|
+
|
|
303
|
+
i18n: {
|
|
304
|
+
description: 'inputs.portable-text.invalid-value.non-object-child.description',
|
|
305
|
+
action: 'inputs.portable-text.invalid-value.non-object-child.action',
|
|
306
|
+
values: {key: blk._key, index: cIndex},
|
|
307
|
+
},
|
|
308
|
+
}
|
|
309
|
+
return true
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (!child._key || typeof child._key !== 'string') {
|
|
313
|
+
const newChild = {...child, _key: keyGenerator()}
|
|
314
|
+
resolution = {
|
|
315
|
+
autoResolve: true,
|
|
316
|
+
patches: [set(newChild, [{_key: blk._key}, 'children', cIndex])],
|
|
317
|
+
description: `Child at index ${cIndex} is missing required _key in block with _key ${blk._key}.`,
|
|
318
|
+
action: 'Set a new random _key on the object',
|
|
319
|
+
item: blk,
|
|
320
|
+
|
|
321
|
+
i18n: {
|
|
322
|
+
description: 'inputs.portable-text.invalid-value.missing-child-key.description',
|
|
323
|
+
action: 'inputs.portable-text.invalid-value.missing-child-key.action',
|
|
324
|
+
values: {key: blk._key, index: cIndex},
|
|
325
|
+
},
|
|
326
|
+
}
|
|
327
|
+
return true
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Verify that children have valid types
|
|
331
|
+
if (!child._type) {
|
|
332
|
+
resolution = {
|
|
333
|
+
patches: [unset([{_key: blk._key}, 'children', {_key: child._key}])],
|
|
334
|
+
description: `Child with _key '${child._key}' in block with key '${blk._key}' is missing '_type' property.`,
|
|
335
|
+
action: 'Remove the object',
|
|
336
|
+
item: blk,
|
|
337
|
+
|
|
338
|
+
i18n: {
|
|
339
|
+
description: 'inputs.portable-text.invalid-value.missing-child-type.description',
|
|
340
|
+
action: 'inputs.portable-text.invalid-value.missing-child-type.action',
|
|
341
|
+
values: {key: blk._key, childKey: child._key},
|
|
342
|
+
},
|
|
343
|
+
}
|
|
344
|
+
return true
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (!validChildTypes.includes(child._type)) {
|
|
348
|
+
resolution = {
|
|
349
|
+
patches: [unset([{_key: blk._key}, 'children', {_key: child._key}])],
|
|
350
|
+
description: `Child with _key '${child._key}' in block with key '${blk._key}' has invalid '_type' property (${child._type}).`,
|
|
351
|
+
action: 'Remove the object',
|
|
352
|
+
item: blk,
|
|
353
|
+
|
|
354
|
+
i18n: {
|
|
355
|
+
description:
|
|
356
|
+
'inputs.portable-text.invalid-value.disallowed-child-type.description',
|
|
357
|
+
action: 'inputs.portable-text.invalid-value.disallowed-child-type.action',
|
|
358
|
+
values: {key: blk._key, childKey: child._key, childType: child._type},
|
|
359
|
+
},
|
|
360
|
+
}
|
|
361
|
+
return true
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Verify that spans have .text property that is a string
|
|
365
|
+
if (child._type === types.span.name && typeof child.text !== 'string') {
|
|
366
|
+
resolution = {
|
|
367
|
+
patches: [
|
|
368
|
+
set({...child, text: ''}, [{_key: blk._key}, 'children', {_key: child._key}]),
|
|
369
|
+
],
|
|
370
|
+
description: `Child with _key '${child._key}' in block with key '${blk._key}' has missing or invalid text property!`,
|
|
371
|
+
action: `Write an empty text property to the object`,
|
|
372
|
+
item: blk,
|
|
373
|
+
|
|
374
|
+
i18n: {
|
|
375
|
+
description: 'inputs.portable-text.invalid-value.invalid-span-text.description',
|
|
376
|
+
action: 'inputs.portable-text.invalid-value.invalid-span-text.action',
|
|
377
|
+
values: {key: blk._key, childKey: child._key},
|
|
378
|
+
},
|
|
379
|
+
}
|
|
380
|
+
return true
|
|
381
|
+
}
|
|
382
|
+
return false
|
|
383
|
+
})
|
|
384
|
+
) {
|
|
385
|
+
valid = false
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return false
|
|
389
|
+
})
|
|
390
|
+
) {
|
|
391
|
+
valid = false
|
|
392
|
+
}
|
|
393
|
+
return {valid, resolution, value}
|
|
394
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type PathSegment,
|
|
3
|
+
type PortableTextBlock,
|
|
4
|
+
type PortableTextChild,
|
|
5
|
+
type PortableTextObject,
|
|
6
|
+
type PortableTextTextBlock,
|
|
7
|
+
} from '@sanity/types'
|
|
8
|
+
import {isEqual} from 'lodash'
|
|
9
|
+
import {type Descendant, Element, type Node, Text} from 'slate'
|
|
10
|
+
|
|
11
|
+
import {type PortableTextMemberSchemaTypes} from '../types/editor'
|
|
12
|
+
|
|
13
|
+
export const EMPTY_MARKDEFS: PortableTextObject[] = []
|
|
14
|
+
export const EMPTY_MARKS: string[] = []
|
|
15
|
+
|
|
16
|
+
export const VOID_CHILD_KEY = 'void-child'
|
|
17
|
+
|
|
18
|
+
type Partial<T> = {
|
|
19
|
+
[P in keyof T]?: T[P]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function keepObjectEquality(
|
|
23
|
+
object: PortableTextBlock | PortableTextChild,
|
|
24
|
+
keyMap: Record<string, PortableTextBlock | PortableTextChild>,
|
|
25
|
+
) {
|
|
26
|
+
const value = keyMap[object._key]
|
|
27
|
+
if (value && isEqual(object, value)) {
|
|
28
|
+
return value
|
|
29
|
+
}
|
|
30
|
+
keyMap[object._key] = object
|
|
31
|
+
return object
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function toSlateValue(
|
|
35
|
+
value: PortableTextBlock[] | undefined,
|
|
36
|
+
{schemaTypes}: {schemaTypes: PortableTextMemberSchemaTypes},
|
|
37
|
+
keyMap: Record<string, any> = {},
|
|
38
|
+
): Descendant[] {
|
|
39
|
+
if (value && Array.isArray(value)) {
|
|
40
|
+
return value.map((block) => {
|
|
41
|
+
const {_type, _key, ...rest} = block
|
|
42
|
+
const voidChildren = [{_key: VOID_CHILD_KEY, _type: 'span', text: '', marks: []}]
|
|
43
|
+
const isPortableText = block && block._type === schemaTypes.block.name
|
|
44
|
+
if (isPortableText) {
|
|
45
|
+
const textBlock = block as PortableTextTextBlock
|
|
46
|
+
let hasInlines = false
|
|
47
|
+
const hasMissingStyle = typeof textBlock.style === 'undefined'
|
|
48
|
+
const hasMissingMarkDefs = typeof textBlock.markDefs === 'undefined'
|
|
49
|
+
const hasMissingChildren = typeof textBlock.children === 'undefined'
|
|
50
|
+
|
|
51
|
+
const children = (textBlock.children || []).map((child) => {
|
|
52
|
+
const {_type: cType, _key: cKey, ...cRest} = child
|
|
53
|
+
// Return 'slate' version of inline object where the actual
|
|
54
|
+
// value is stored in the `value` property.
|
|
55
|
+
// In slate, inline objects are represented as regular
|
|
56
|
+
// children with actual text node in order to be able to
|
|
57
|
+
// be selected the same way as the rest of the (text) content.
|
|
58
|
+
if (cType !== 'span') {
|
|
59
|
+
hasInlines = true
|
|
60
|
+
return keepObjectEquality(
|
|
61
|
+
{
|
|
62
|
+
_type: cType,
|
|
63
|
+
_key: cKey,
|
|
64
|
+
children: voidChildren,
|
|
65
|
+
value: cRest,
|
|
66
|
+
__inline: true,
|
|
67
|
+
},
|
|
68
|
+
keyMap,
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
// Original child object (span)
|
|
72
|
+
return child
|
|
73
|
+
})
|
|
74
|
+
// Return original block
|
|
75
|
+
if (
|
|
76
|
+
!hasMissingStyle &&
|
|
77
|
+
!hasMissingMarkDefs &&
|
|
78
|
+
!hasMissingChildren &&
|
|
79
|
+
!hasInlines &&
|
|
80
|
+
Element.isElement(block)
|
|
81
|
+
) {
|
|
82
|
+
// Original object
|
|
83
|
+
return block
|
|
84
|
+
}
|
|
85
|
+
// TODO: remove this when we have a better way to handle missing style
|
|
86
|
+
if (hasMissingStyle) {
|
|
87
|
+
rest.style = schemaTypes.styles[0].value
|
|
88
|
+
}
|
|
89
|
+
return keepObjectEquality({_type, _key, ...rest, children}, keyMap)
|
|
90
|
+
}
|
|
91
|
+
return keepObjectEquality(
|
|
92
|
+
{
|
|
93
|
+
_type,
|
|
94
|
+
_key,
|
|
95
|
+
children: voidChildren,
|
|
96
|
+
value: rest,
|
|
97
|
+
},
|
|
98
|
+
keyMap,
|
|
99
|
+
)
|
|
100
|
+
}) as Descendant[]
|
|
101
|
+
}
|
|
102
|
+
return []
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function fromSlateValue(
|
|
106
|
+
value: Descendant[],
|
|
107
|
+
textBlockType: string,
|
|
108
|
+
keyMap: Record<string, PortableTextBlock | PortableTextChild> = {},
|
|
109
|
+
): PortableTextBlock[] {
|
|
110
|
+
return value.map((block) => {
|
|
111
|
+
const {_key, _type} = block
|
|
112
|
+
if (!_key || !_type) {
|
|
113
|
+
throw new Error('Not a valid block')
|
|
114
|
+
}
|
|
115
|
+
if (_type === textBlockType && 'children' in block && Array.isArray(block.children) && _key) {
|
|
116
|
+
let hasInlines = false
|
|
117
|
+
const children = block.children.map((child) => {
|
|
118
|
+
const {_type: _cType} = child
|
|
119
|
+
if ('value' in child && _cType !== 'span') {
|
|
120
|
+
hasInlines = true
|
|
121
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
122
|
+
const {value: v, _key: k, _type: t, __inline: _i, children: _c, ...rest} = child
|
|
123
|
+
return keepObjectEquality({...rest, ...v, _key: k as string, _type: t as string}, keyMap)
|
|
124
|
+
}
|
|
125
|
+
return child
|
|
126
|
+
})
|
|
127
|
+
if (!hasInlines) {
|
|
128
|
+
return block as PortableTextBlock // Original object
|
|
129
|
+
}
|
|
130
|
+
return keepObjectEquality({...block, children, _key, _type}, keyMap) as PortableTextBlock
|
|
131
|
+
}
|
|
132
|
+
const blockValue = 'value' in block && block.value
|
|
133
|
+
return keepObjectEquality(
|
|
134
|
+
{_key, _type, ...(typeof blockValue === 'object' ? blockValue : {})},
|
|
135
|
+
keyMap,
|
|
136
|
+
) as PortableTextBlock
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function isEqualToEmptyEditor(
|
|
141
|
+
children: Descendant[] | PortableTextBlock[],
|
|
142
|
+
schemaTypes: PortableTextMemberSchemaTypes,
|
|
143
|
+
): boolean {
|
|
144
|
+
return (
|
|
145
|
+
children === undefined ||
|
|
146
|
+
(children && Array.isArray(children) && children.length === 0) ||
|
|
147
|
+
(children &&
|
|
148
|
+
Array.isArray(children) &&
|
|
149
|
+
children.length === 1 &&
|
|
150
|
+
Element.isElement(children[0]) &&
|
|
151
|
+
children[0]._type === schemaTypes.block.name &&
|
|
152
|
+
'style' in children[0] &&
|
|
153
|
+
children[0].style === schemaTypes.styles[0].value &&
|
|
154
|
+
!('listItem' in children[0]) &&
|
|
155
|
+
Array.isArray(children[0].children) &&
|
|
156
|
+
children[0].children.length === 1 &&
|
|
157
|
+
Text.isText(children[0].children[0]) &&
|
|
158
|
+
children[0].children[0]._type === 'span' &&
|
|
159
|
+
!children[0].children[0].marks?.join('') &&
|
|
160
|
+
children[0].children[0].text === '')
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function findBlockAndIndexFromPath(
|
|
165
|
+
firstPathSegment: PathSegment,
|
|
166
|
+
children: (Node | Partial<Node>)[],
|
|
167
|
+
): [Element | undefined, number | undefined] {
|
|
168
|
+
let blockIndex = -1
|
|
169
|
+
const isNumber = Number.isInteger(Number(firstPathSegment))
|
|
170
|
+
if (isNumber) {
|
|
171
|
+
blockIndex = Number(firstPathSegment)
|
|
172
|
+
} else if (children) {
|
|
173
|
+
blockIndex = children.findIndex(
|
|
174
|
+
(blk) => Element.isElement(blk) && isEqual({_key: blk._key}, firstPathSegment),
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
if (blockIndex > -1) {
|
|
178
|
+
return [children[blockIndex] as Element, blockIndex]
|
|
179
|
+
}
|
|
180
|
+
return [undefined, -1]
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function findChildAndIndexFromPath(
|
|
184
|
+
secondPathSegment: PathSegment,
|
|
185
|
+
block: Element,
|
|
186
|
+
): [Element | Text | undefined, number] {
|
|
187
|
+
let childIndex = -1
|
|
188
|
+
const isNumber = Number.isInteger(Number(secondPathSegment))
|
|
189
|
+
if (isNumber) {
|
|
190
|
+
childIndex = Number(secondPathSegment)
|
|
191
|
+
} else {
|
|
192
|
+
childIndex = block.children.findIndex((child) => isEqual({_key: child._key}, secondPathSegment))
|
|
193
|
+
}
|
|
194
|
+
if (childIndex > -1) {
|
|
195
|
+
return [block.children[childIndex] as Element | Text, childIndex]
|
|
196
|
+
}
|
|
197
|
+
return [undefined, -1]
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function getValueOrInitialValue(
|
|
201
|
+
value: unknown,
|
|
202
|
+
initialValue: PortableTextBlock[],
|
|
203
|
+
): PortableTextBlock[] | undefined {
|
|
204
|
+
if (value && Array.isArray(value) && value.length > 0) {
|
|
205
|
+
return value
|
|
206
|
+
}
|
|
207
|
+
return initialValue
|
|
208
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {type Editor, type Element, type Range} from 'slate'
|
|
2
|
+
|
|
3
|
+
import {type EditorSelection} from '..'
|
|
4
|
+
|
|
5
|
+
// Is the editor currently receiving remote changes that are being applied to the content?
|
|
6
|
+
export const IS_PROCESSING_REMOTE_CHANGES: WeakMap<Editor, boolean> = new WeakMap()
|
|
7
|
+
// Is the editor currently producing local changes that are not yet submitted?
|
|
8
|
+
export const IS_PROCESSING_LOCAL_CHANGES: WeakMap<Editor, boolean> = new WeakMap()
|
|
9
|
+
|
|
10
|
+
// Is the editor dragging something?
|
|
11
|
+
export const IS_DRAGGING: WeakMap<Editor, boolean> = new WeakMap()
|
|
12
|
+
// Is the editor dragging a element?
|
|
13
|
+
export const IS_DRAGGING_BLOCK_ELEMENT: WeakMap<Editor, Element> = new WeakMap()
|
|
14
|
+
|
|
15
|
+
// When dragging elements, this will be the target element
|
|
16
|
+
export const IS_DRAGGING_ELEMENT_TARGET: WeakMap<Editor, Element> = new WeakMap()
|
|
17
|
+
// Target position for dragging over a block
|
|
18
|
+
export const IS_DRAGGING_BLOCK_TARGET_POSITION: WeakMap<Editor, 'top' | 'bottom'> = new WeakMap()
|
|
19
|
+
|
|
20
|
+
export const KEY_TO_SLATE_ELEMENT: WeakMap<Editor, any | undefined> = new WeakMap()
|
|
21
|
+
export const KEY_TO_VALUE_ELEMENT: WeakMap<Editor, any | undefined> = new WeakMap()
|
|
22
|
+
|
|
23
|
+
// Keep object relation to slate range in the portable-text-range
|
|
24
|
+
export const SLATE_TO_PORTABLE_TEXT_RANGE = new WeakMap<Range, EditorSelection>()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {type Editor} from 'slate'
|
|
2
|
+
|
|
3
|
+
import {IS_PROCESSING_LOCAL_CHANGES, IS_PROCESSING_REMOTE_CHANGES} from './weakMaps'
|
|
4
|
+
|
|
5
|
+
export function withRemoteChanges(editor: Editor, fn: () => void): void {
|
|
6
|
+
const prev = isChangingRemotely(editor) || false
|
|
7
|
+
IS_PROCESSING_REMOTE_CHANGES.set(editor, true)
|
|
8
|
+
fn()
|
|
9
|
+
IS_PROCESSING_REMOTE_CHANGES.set(editor, prev)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isChangingRemotely(editor: Editor): boolean | undefined {
|
|
13
|
+
return IS_PROCESSING_REMOTE_CHANGES.get(editor)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function withLocalChanges(editor: Editor, fn: () => void): void {
|
|
17
|
+
const prev = isChangingLocally(editor) || false
|
|
18
|
+
IS_PROCESSING_LOCAL_CHANGES.set(editor, true)
|
|
19
|
+
fn()
|
|
20
|
+
IS_PROCESSING_LOCAL_CHANGES.set(editor, prev)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isChangingLocally(editor: Editor): boolean | undefined {
|
|
24
|
+
return IS_PROCESSING_LOCAL_CHANGES.get(editor)
|
|
25
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {type Editor} from 'slate'
|
|
2
|
+
|
|
3
|
+
export const PRESERVE_KEYS: WeakMap<Editor, boolean | undefined> = new WeakMap()
|
|
4
|
+
|
|
5
|
+
export function withPreserveKeys(editor: Editor, fn: () => void): void {
|
|
6
|
+
const prev = isPreservingKeys(editor)
|
|
7
|
+
PRESERVE_KEYS.set(editor, true)
|
|
8
|
+
fn()
|
|
9
|
+
PRESERVE_KEYS.set(editor, prev)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isPreservingKeys(editor: Editor): boolean | undefined {
|
|
13
|
+
return PRESERVE_KEYS.get(editor)
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {type Editor} from 'slate'
|
|
2
|
+
|
|
3
|
+
export const PATCHING: WeakMap<Editor, boolean | undefined> = new WeakMap()
|
|
4
|
+
|
|
5
|
+
export function withoutPatching(editor: Editor, fn: () => void): void {
|
|
6
|
+
const prev = isPatching(editor)
|
|
7
|
+
PATCHING.set(editor, false)
|
|
8
|
+
fn()
|
|
9
|
+
PATCHING.set(editor, prev)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isPatching(editor: Editor): boolean | undefined {
|
|
13
|
+
return PATCHING.get(editor)
|
|
14
|
+
}
|