@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.
Files changed (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -0
  3. package/lib/index.d.mts +911 -0
  4. package/lib/index.d.ts +911 -0
  5. package/lib/index.esm.js +4896 -0
  6. package/lib/index.esm.js.map +1 -0
  7. package/lib/index.js +4874 -0
  8. package/lib/index.js.map +1 -0
  9. package/lib/index.mjs +4896 -0
  10. package/lib/index.mjs.map +1 -0
  11. package/package.json +119 -0
  12. package/src/editor/Editable.tsx +683 -0
  13. package/src/editor/PortableTextEditor.tsx +308 -0
  14. package/src/editor/__tests__/PortableTextEditor.test.tsx +386 -0
  15. package/src/editor/__tests__/PortableTextEditorTester.tsx +116 -0
  16. package/src/editor/__tests__/RangeDecorations.test.tsx +115 -0
  17. package/src/editor/__tests__/handleClick.test.tsx +218 -0
  18. package/src/editor/__tests__/pteWarningsSelfSolving.test.tsx +389 -0
  19. package/src/editor/__tests__/utils.ts +39 -0
  20. package/src/editor/components/DraggableBlock.tsx +287 -0
  21. package/src/editor/components/Element.tsx +279 -0
  22. package/src/editor/components/Leaf.tsx +288 -0
  23. package/src/editor/components/SlateContainer.tsx +81 -0
  24. package/src/editor/components/Synchronizer.tsx +190 -0
  25. package/src/editor/hooks/usePortableTextEditor.ts +23 -0
  26. package/src/editor/hooks/usePortableTextEditorKeyGenerator.ts +24 -0
  27. package/src/editor/hooks/usePortableTextEditorSelection.ts +22 -0
  28. package/src/editor/hooks/usePortableTextEditorValue.ts +16 -0
  29. package/src/editor/hooks/usePortableTextReadOnly.ts +20 -0
  30. package/src/editor/hooks/useSyncValue.test.tsx +125 -0
  31. package/src/editor/hooks/useSyncValue.ts +372 -0
  32. package/src/editor/nodes/DefaultAnnotation.tsx +16 -0
  33. package/src/editor/nodes/DefaultObject.tsx +15 -0
  34. package/src/editor/nodes/index.ts +189 -0
  35. package/src/editor/plugins/__tests__/withEditableAPIDelete.test.tsx +244 -0
  36. package/src/editor/plugins/__tests__/withEditableAPIGetFragment.test.tsx +142 -0
  37. package/src/editor/plugins/__tests__/withEditableAPIInsert.test.tsx +346 -0
  38. package/src/editor/plugins/__tests__/withEditableAPISelectionsOverlapping.test.tsx +162 -0
  39. package/src/editor/plugins/__tests__/withHotkeys.test.tsx +212 -0
  40. package/src/editor/plugins/__tests__/withInsertBreak.test.tsx +204 -0
  41. package/src/editor/plugins/__tests__/withPlaceholderBlock.test.tsx +133 -0
  42. package/src/editor/plugins/__tests__/withPortableTextLists.test.tsx +65 -0
  43. package/src/editor/plugins/__tests__/withPortableTextMarkModel.test.tsx +1377 -0
  44. package/src/editor/plugins/__tests__/withPortableTextSelections.test.tsx +91 -0
  45. package/src/editor/plugins/__tests__/withUndoRedo.test.tsx +115 -0
  46. package/src/editor/plugins/createWithEditableAPI.ts +573 -0
  47. package/src/editor/plugins/createWithHotKeys.ts +304 -0
  48. package/src/editor/plugins/createWithInsertBreak.ts +45 -0
  49. package/src/editor/plugins/createWithInsertData.ts +359 -0
  50. package/src/editor/plugins/createWithMaxBlocks.ts +24 -0
  51. package/src/editor/plugins/createWithObjectKeys.ts +63 -0
  52. package/src/editor/plugins/createWithPatches.ts +274 -0
  53. package/src/editor/plugins/createWithPlaceholderBlock.ts +36 -0
  54. package/src/editor/plugins/createWithPortableTextBlockStyle.ts +91 -0
  55. package/src/editor/plugins/createWithPortableTextLists.ts +160 -0
  56. package/src/editor/plugins/createWithPortableTextMarkModel.ts +441 -0
  57. package/src/editor/plugins/createWithPortableTextSelections.ts +65 -0
  58. package/src/editor/plugins/createWithSchemaTypes.ts +76 -0
  59. package/src/editor/plugins/createWithUndoRedo.ts +494 -0
  60. package/src/editor/plugins/createWithUtils.ts +81 -0
  61. package/src/editor/plugins/index.ts +155 -0
  62. package/src/index.ts +11 -0
  63. package/src/patch/PatchEvent.ts +33 -0
  64. package/src/patch/applyPatch.ts +29 -0
  65. package/src/patch/array.ts +89 -0
  66. package/src/patch/arrayInsert.ts +27 -0
  67. package/src/patch/object.ts +39 -0
  68. package/src/patch/patches.ts +53 -0
  69. package/src/patch/primitive.ts +43 -0
  70. package/src/patch/string.ts +51 -0
  71. package/src/types/editor.ts +576 -0
  72. package/src/types/options.ts +17 -0
  73. package/src/types/patch.ts +65 -0
  74. package/src/types/slate.ts +25 -0
  75. package/src/utils/__tests__/dmpToOperations.test.ts +181 -0
  76. package/src/utils/__tests__/operationToPatches.test.ts +421 -0
  77. package/src/utils/__tests__/patchToOperations.test.ts +293 -0
  78. package/src/utils/__tests__/ranges.test.ts +18 -0
  79. package/src/utils/__tests__/valueNormalization.test.tsx +62 -0
  80. package/src/utils/__tests__/values.test.ts +253 -0
  81. package/src/utils/applyPatch.ts +407 -0
  82. package/src/utils/bufferUntil.ts +15 -0
  83. package/src/utils/debug.ts +12 -0
  84. package/src/utils/getPortableTextMemberSchemaTypes.ts +100 -0
  85. package/src/utils/operationToPatches.ts +357 -0
  86. package/src/utils/patches.ts +36 -0
  87. package/src/utils/paths.ts +60 -0
  88. package/src/utils/ranges.ts +77 -0
  89. package/src/utils/schema.ts +8 -0
  90. package/src/utils/selection.ts +65 -0
  91. package/src/utils/ucs2Indices.ts +67 -0
  92. package/src/utils/validateValue.ts +394 -0
  93. package/src/utils/values.ts +208 -0
  94. package/src/utils/weakMaps.ts +24 -0
  95. package/src/utils/withChanges.ts +25 -0
  96. package/src/utils/withPreserveKeys.ts +14 -0
  97. 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
+ }