@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,279 @@
1
+ /* eslint-disable complexity */
2
+ /* eslint-disable max-statements */
3
+ import {
4
+ type Path,
5
+ type PortableTextChild,
6
+ type PortableTextObject,
7
+ type PortableTextTextBlock,
8
+ } from '@sanity/types'
9
+ import {type FunctionComponent, type ReactElement, useMemo, useRef} from 'react'
10
+ import {Editor, Element as SlateElement, Range} from 'slate'
11
+ import {ReactEditor, type RenderElementProps, useSelected, useSlateStatic} from 'slate-react'
12
+
13
+ import {
14
+ type BlockRenderProps,
15
+ type PortableTextMemberSchemaTypes,
16
+ type RenderBlockFunction,
17
+ type RenderChildFunction,
18
+ type RenderListItemFunction,
19
+ type RenderStyleFunction,
20
+ } from '../../types/editor'
21
+ import {debugWithName} from '../../utils/debug'
22
+ import {fromSlateValue} from '../../utils/values'
23
+ import {KEY_TO_VALUE_ELEMENT} from '../../utils/weakMaps'
24
+ import ObjectNode from '../nodes/DefaultObject'
25
+ import {DefaultBlockObject, DefaultListItem, DefaultListItemInner} from '../nodes/index'
26
+ import {DraggableBlock} from './DraggableBlock'
27
+
28
+ const debug = debugWithName('components:Element')
29
+ const debugRenders = false
30
+ const EMPTY_ANNOTATIONS: PortableTextObject[] = []
31
+
32
+ /**
33
+ * @internal
34
+ */
35
+ export interface ElementProps {
36
+ attributes: RenderElementProps['attributes']
37
+ children: ReactElement
38
+ element: SlateElement
39
+ schemaTypes: PortableTextMemberSchemaTypes
40
+ readOnly: boolean
41
+ renderBlock?: RenderBlockFunction
42
+ renderChild?: RenderChildFunction
43
+ renderListItem?: RenderListItemFunction
44
+ renderStyle?: RenderStyleFunction
45
+ spellCheck?: boolean
46
+ }
47
+
48
+ const inlineBlockStyle = {display: 'inline-block'}
49
+
50
+ /**
51
+ * Renders Portable Text block and inline object nodes in Slate
52
+ * @internal
53
+ */
54
+ export const Element: FunctionComponent<ElementProps> = ({
55
+ attributes,
56
+ children,
57
+ element,
58
+ schemaTypes,
59
+ readOnly,
60
+ renderBlock,
61
+ renderChild,
62
+ renderListItem,
63
+ renderStyle,
64
+ spellCheck,
65
+ }) => {
66
+ const editor = useSlateStatic()
67
+ const selected = useSelected()
68
+ const blockRef = useRef<HTMLDivElement | null>(null)
69
+ const inlineBlockObjectRef = useRef(null)
70
+ const focused = (selected && editor.selection && Range.isCollapsed(editor.selection)) || false
71
+
72
+ const value = useMemo(
73
+ () => fromSlateValue([element], schemaTypes.block.name, KEY_TO_VALUE_ELEMENT.get(editor))[0],
74
+ [editor, element, schemaTypes.block.name],
75
+ )
76
+
77
+ let renderedBlock = children
78
+
79
+ let className
80
+
81
+ const blockPath: Path = useMemo(() => [{_key: element._key}], [element])
82
+
83
+ if (typeof element._type !== 'string') {
84
+ throw new Error(`Expected element to have a _type property`)
85
+ }
86
+
87
+ if (typeof element._key !== 'string') {
88
+ throw new Error(`Expected element to have a _key property`)
89
+ }
90
+
91
+ // Test for inline objects first
92
+ if (editor.isInline(element)) {
93
+ const path = ReactEditor.findPath(editor, element)
94
+ const [block] = Editor.node(editor, path, {depth: 1})
95
+ const schemaType = schemaTypes.inlineObjects.find((_type) => _type.name === element._type)
96
+ if (!schemaType) {
97
+ throw new Error('Could not find type for inline block element')
98
+ }
99
+ if (SlateElement.isElement(block)) {
100
+ const elmPath: Path = [{_key: block._key}, 'children', {_key: element._key}]
101
+ if (debugRenders) {
102
+ debug(`Render ${element._key} (inline object)`)
103
+ }
104
+ return (
105
+ <span {...attributes}>
106
+ {/* Note that children must follow immediately or cut and selections will not work properly in Chrome. */}
107
+ {children}
108
+ <span
109
+ draggable={!readOnly}
110
+ className="pt-inline-object"
111
+ data-testid="pt-inline-object"
112
+ ref={inlineBlockObjectRef}
113
+ key={element._key}
114
+ style={inlineBlockStyle}
115
+ contentEditable={false}
116
+ >
117
+ {renderChild &&
118
+ renderChild({
119
+ annotations: EMPTY_ANNOTATIONS, // These inline objects currently doesn't support annotations. This is a limitation of the current PT spec/model.
120
+ children: <ObjectNode value={value} />,
121
+ editorElementRef: inlineBlockObjectRef,
122
+ focused,
123
+ path: elmPath,
124
+ schemaType,
125
+ selected,
126
+ type: schemaType,
127
+ value: value as PortableTextChild,
128
+ })}
129
+ {!renderChild && <ObjectNode value={value} />}
130
+ </span>
131
+ </span>
132
+ )
133
+ }
134
+ throw new Error('Block not found!')
135
+ }
136
+
137
+ // If not inline, it's either a block (text) or a block object (non-text)
138
+ // NOTE: text blocks aren't draggable with DraggableBlock (yet?)
139
+ if (element._type === schemaTypes.block.name) {
140
+ className = `pt-block pt-text-block`
141
+ const isListItem = 'listItem' in element
142
+ if (debugRenders) {
143
+ debug(`Render ${element._key} (text block)`)
144
+ }
145
+ const style = ('style' in element && element.style) || 'normal'
146
+ className = `pt-block pt-text-block pt-text-block-style-${style}`
147
+ const blockStyleType = schemaTypes.styles.find((item) => item.value === style)
148
+ if (renderStyle && blockStyleType) {
149
+ renderedBlock = renderStyle({
150
+ block: element as PortableTextTextBlock,
151
+ children,
152
+ focused,
153
+ selected,
154
+ value: style,
155
+ path: blockPath,
156
+ schemaType: blockStyleType,
157
+ editorElementRef: blockRef,
158
+ })
159
+ }
160
+ let level
161
+ if (isListItem) {
162
+ if (typeof element.level === 'number') {
163
+ level = element.level
164
+ }
165
+ className += ` pt-list-item pt-list-item-${element.listItem} pt-list-item-level-${level || 1}`
166
+ }
167
+ if (editor.isListBlock(value) && isListItem && element.listItem) {
168
+ const listType = schemaTypes.lists.find((item) => item.value === element.listItem)
169
+ if (renderListItem && listType) {
170
+ renderedBlock = renderListItem({
171
+ block: value,
172
+ children: renderedBlock,
173
+ focused,
174
+ selected,
175
+ value: element.listItem,
176
+ path: blockPath,
177
+ schemaType: listType,
178
+ level: value.level || 1,
179
+ editorElementRef: blockRef,
180
+ })
181
+ } else {
182
+ renderedBlock = (
183
+ <DefaultListItem
184
+ listStyle={value.listItem || schemaTypes.lists[0].value}
185
+ listLevel={value.level || 1}
186
+ >
187
+ <DefaultListItemInner>{renderedBlock}</DefaultListItemInner>
188
+ </DefaultListItem>
189
+ )
190
+ }
191
+ }
192
+ const renderProps: Omit<BlockRenderProps, 'type'> = Object.defineProperty(
193
+ {
194
+ children: renderedBlock,
195
+ editorElementRef: blockRef,
196
+ focused,
197
+ level,
198
+ listItem: isListItem ? element.listItem : undefined,
199
+ path: blockPath,
200
+ selected,
201
+ style,
202
+ schemaType: schemaTypes.block,
203
+ value,
204
+ },
205
+ 'type',
206
+ {
207
+ enumerable: false,
208
+ get() {
209
+ console.warn("Property 'type' is deprecated, use 'schemaType' instead.")
210
+ return schemaTypes.block
211
+ },
212
+ },
213
+ )
214
+
215
+ const propsOrDefaultRendered = renderBlock
216
+ ? renderBlock(renderProps as BlockRenderProps)
217
+ : children
218
+ return (
219
+ <div key={element._key} {...attributes} className={className} spellCheck={spellCheck}>
220
+ <DraggableBlock element={element} readOnly={readOnly} blockRef={blockRef}>
221
+ <div ref={blockRef}>{propsOrDefaultRendered}</div>
222
+ </DraggableBlock>
223
+ </div>
224
+ )
225
+ }
226
+ const schemaType = schemaTypes.blockObjects.find((_type) => _type.name === element._type)
227
+ if (!schemaType) {
228
+ throw new Error(`Could not find schema type for block element of _type ${element._type}`)
229
+ }
230
+ if (debugRenders) {
231
+ debug(`Render ${element._key} (object block)`)
232
+ }
233
+ className = 'pt-block pt-object-block'
234
+ const block = fromSlateValue(
235
+ [element],
236
+ schemaTypes.block.name,
237
+ KEY_TO_VALUE_ELEMENT.get(editor),
238
+ )[0]
239
+ let renderedBlockFromProps
240
+ if (renderBlock) {
241
+ const _props: Omit<BlockRenderProps, 'type'> = Object.defineProperty(
242
+ {
243
+ children: <ObjectNode value={value} />,
244
+ editorElementRef: blockRef,
245
+ focused,
246
+ path: blockPath,
247
+ schemaType,
248
+ selected,
249
+ value: block,
250
+ },
251
+ 'type',
252
+ {
253
+ enumerable: false,
254
+ get() {
255
+ console.warn("Property 'type' is deprecated, use 'schemaType' instead.")
256
+ return schemaType
257
+ },
258
+ },
259
+ )
260
+ renderedBlockFromProps = renderBlock(_props as BlockRenderProps)
261
+ }
262
+ return (
263
+ <div key={element._key} {...attributes} className={className}>
264
+ {children}
265
+ <DraggableBlock element={element} readOnly={readOnly} blockRef={blockRef}>
266
+ {renderedBlockFromProps && (
267
+ <div ref={blockRef} contentEditable={false}>
268
+ {renderedBlockFromProps}
269
+ </div>
270
+ )}
271
+ {!renderedBlockFromProps && (
272
+ <DefaultBlockObject selected={selected}>
273
+ <ObjectNode value={value} />
274
+ </DefaultBlockObject>
275
+ )}
276
+ </DraggableBlock>
277
+ </div>
278
+ )
279
+ }
@@ -0,0 +1,288 @@
1
+ import {type Path, type PortableTextObject, type PortableTextTextBlock} from '@sanity/types'
2
+ import {isEqual, uniq} from 'lodash'
3
+ import {
4
+ type ReactElement,
5
+ startTransition,
6
+ useCallback,
7
+ useEffect,
8
+ useMemo,
9
+ useRef,
10
+ useState,
11
+ } from 'react'
12
+ import {Text} from 'slate'
13
+ import {type RenderLeafProps, useSelected} from 'slate-react'
14
+
15
+ import {
16
+ type BlockAnnotationRenderProps,
17
+ type BlockChildRenderProps,
18
+ type BlockDecoratorRenderProps,
19
+ type PortableTextMemberSchemaTypes,
20
+ type RenderAnnotationFunction,
21
+ type RenderChildFunction,
22
+ type RenderDecoratorFunction,
23
+ } from '../../types/editor'
24
+ import {debugWithName} from '../../utils/debug'
25
+ import {usePortableTextEditor} from '../hooks/usePortableTextEditor'
26
+ import {DefaultAnnotation} from '../nodes/DefaultAnnotation'
27
+ import {PortableTextEditor} from '../PortableTextEditor'
28
+
29
+ const debug = debugWithName('components:Leaf')
30
+
31
+ const EMPTY_MARKS: string[] = []
32
+
33
+ /**
34
+ * @internal
35
+ */
36
+ export interface LeafProps extends RenderLeafProps {
37
+ children: ReactElement
38
+ schemaTypes: PortableTextMemberSchemaTypes
39
+ renderAnnotation?: RenderAnnotationFunction
40
+ renderChild?: RenderChildFunction
41
+ renderDecorator?: RenderDecoratorFunction
42
+ readOnly: boolean
43
+ }
44
+
45
+ /**
46
+ * Renders Portable Text span nodes in Slate
47
+ * @internal
48
+ */
49
+ export const Leaf = (props: LeafProps) => {
50
+ const {attributes, children, leaf, schemaTypes, renderChild, renderDecorator, renderAnnotation} =
51
+ props
52
+ const spanRef = useRef<HTMLElement>(null)
53
+ const portableTextEditor = usePortableTextEditor()
54
+ const blockSelected = useSelected()
55
+ const [focused, setFocused] = useState(false)
56
+ const [selected, setSelected] = useState(false)
57
+ const block = children.props.parent as PortableTextTextBlock | undefined
58
+ const path: Path = useMemo(
59
+ () => (block ? [{_key: block?._key}, 'children', {_key: leaf._key}] : []),
60
+ [block, leaf._key],
61
+ )
62
+ const decoratorValues = useMemo(
63
+ () => schemaTypes.decorators.map((dec) => dec.value),
64
+ [schemaTypes.decorators],
65
+ )
66
+ const marks: string[] = useMemo(
67
+ () => uniq((leaf.marks || EMPTY_MARKS).filter((mark) => decoratorValues.includes(mark))),
68
+ [decoratorValues, leaf.marks],
69
+ )
70
+ const annotationMarks = Array.isArray(leaf.marks) ? leaf.marks : EMPTY_MARKS
71
+ const annotations = useMemo(
72
+ () =>
73
+ annotationMarks
74
+ .map(
75
+ (mark) =>
76
+ !decoratorValues.includes(mark) && block?.markDefs?.find((def) => def._key === mark),
77
+ )
78
+ .filter(Boolean) as PortableTextObject[],
79
+ [annotationMarks, block, decoratorValues],
80
+ )
81
+
82
+ const shouldTrackSelectionAndFocus = annotations.length > 0 && blockSelected
83
+
84
+ useEffect(() => {
85
+ if (!shouldTrackSelectionAndFocus) {
86
+ setFocused(false)
87
+ return
88
+ }
89
+ const sel = PortableTextEditor.getSelection(portableTextEditor)
90
+ if (
91
+ sel &&
92
+ isEqual(sel.focus.path, path) &&
93
+ PortableTextEditor.isCollapsedSelection(portableTextEditor)
94
+ ) {
95
+ startTransition(() => {
96
+ setFocused(true)
97
+ })
98
+ }
99
+ }, [shouldTrackSelectionAndFocus, path, portableTextEditor])
100
+
101
+ // Function to check if this leaf is currently inside the user's text selection
102
+ const setSelectedFromRange = useCallback(() => {
103
+ if (!shouldTrackSelectionAndFocus) {
104
+ return
105
+ }
106
+ debug('Setting selection and focus from range')
107
+ const winSelection = window.getSelection()
108
+ if (!winSelection) {
109
+ setSelected(false)
110
+ return
111
+ }
112
+ if (winSelection && winSelection.rangeCount > 0) {
113
+ const range = winSelection.getRangeAt(0)
114
+ if (spanRef.current && range.intersectsNode(spanRef.current)) {
115
+ setSelected(true)
116
+ } else {
117
+ setSelected(false)
118
+ }
119
+ } else {
120
+ setSelected(false)
121
+ }
122
+ }, [shouldTrackSelectionAndFocus])
123
+
124
+ useEffect(() => {
125
+ if (!shouldTrackSelectionAndFocus) {
126
+ return undefined
127
+ }
128
+ const sub = portableTextEditor.change$.subscribe((next) => {
129
+ if (next.type === 'blur') {
130
+ setFocused(false)
131
+ setSelected(false)
132
+ return
133
+ }
134
+ if (next.type === 'focus') {
135
+ const sel = PortableTextEditor.getSelection(portableTextEditor)
136
+ if (
137
+ sel &&
138
+ isEqual(sel.focus.path, path) &&
139
+ PortableTextEditor.isCollapsedSelection(portableTextEditor)
140
+ ) {
141
+ setFocused(true)
142
+ }
143
+ setSelectedFromRange()
144
+ return
145
+ }
146
+ if (next.type === 'selection') {
147
+ if (
148
+ next.selection &&
149
+ isEqual(next.selection.focus.path, path) &&
150
+ PortableTextEditor.isCollapsedSelection(portableTextEditor)
151
+ ) {
152
+ setFocused(true)
153
+ } else {
154
+ setFocused(false)
155
+ }
156
+ setSelectedFromRange()
157
+ }
158
+ })
159
+ return () => {
160
+ sub.unsubscribe()
161
+ }
162
+ }, [path, portableTextEditor, setSelectedFromRange, shouldTrackSelectionAndFocus])
163
+
164
+ useEffect(() => setSelectedFromRange(), [setSelectedFromRange])
165
+
166
+ const content = useMemo(() => {
167
+ let returnedChildren = children
168
+ // Render text nodes
169
+ if (Text.isText(leaf) && leaf._type === schemaTypes.span.name) {
170
+ marks.forEach((mark) => {
171
+ const schemaType = schemaTypes.decorators.find((dec) => dec.value === mark)
172
+ if (schemaType && renderDecorator) {
173
+ const _props: Omit<BlockDecoratorRenderProps, 'type'> = Object.defineProperty(
174
+ {
175
+ children: returnedChildren,
176
+ editorElementRef: spanRef,
177
+ focused,
178
+ path,
179
+ selected,
180
+ schemaType,
181
+ value: mark,
182
+ },
183
+ 'type',
184
+ {
185
+ enumerable: false,
186
+ get() {
187
+ console.warn("Property 'type' is deprecated, use 'schemaType' instead.")
188
+ return schemaType
189
+ },
190
+ },
191
+ )
192
+ returnedChildren = renderDecorator(_props as BlockDecoratorRenderProps)
193
+ }
194
+ })
195
+
196
+ if (block && annotations.length > 0) {
197
+ annotations.forEach((annotation) => {
198
+ const schemaType = schemaTypes.annotations.find((t) => t.name === annotation._type)
199
+ if (schemaType) {
200
+ if (renderAnnotation) {
201
+ const _props: Omit<BlockAnnotationRenderProps, 'type'> = Object.defineProperty(
202
+ {
203
+ block,
204
+ children: returnedChildren,
205
+ editorElementRef: spanRef,
206
+ focused,
207
+ path,
208
+ selected,
209
+ schemaType,
210
+ value: annotation,
211
+ },
212
+ 'type',
213
+ {
214
+ enumerable: false,
215
+ get() {
216
+ console.warn("Property 'type' is deprecated, use 'schemaType' instead.")
217
+ return schemaType
218
+ },
219
+ },
220
+ )
221
+
222
+ returnedChildren = (
223
+ <span ref={spanRef}>{renderAnnotation(_props as BlockAnnotationRenderProps)}</span>
224
+ )
225
+ } else {
226
+ returnedChildren = (
227
+ <DefaultAnnotation annotation={annotation}>
228
+ <span ref={spanRef}>{returnedChildren}</span>
229
+ </DefaultAnnotation>
230
+ )
231
+ }
232
+ }
233
+ })
234
+ }
235
+ if (block && renderChild) {
236
+ const child = block.children.find((_child) => _child._key === leaf._key) // Ensure object equality
237
+ if (child) {
238
+ const defaultRendered = <>{returnedChildren}</>
239
+ const _props: Omit<BlockChildRenderProps, 'type'> = Object.defineProperty(
240
+ {
241
+ annotations,
242
+ children: defaultRendered,
243
+ editorElementRef: spanRef,
244
+ focused,
245
+ path,
246
+ schemaType: schemaTypes.span,
247
+ selected,
248
+ value: child,
249
+ },
250
+ 'type',
251
+ {
252
+ enumerable: false,
253
+ get() {
254
+ console.warn("Property 'type' is deprecated, use 'schemaType' instead.")
255
+ return schemaTypes.span
256
+ },
257
+ },
258
+ )
259
+ returnedChildren = renderChild(_props as BlockChildRenderProps)
260
+ }
261
+ }
262
+ }
263
+ return returnedChildren
264
+ }, [
265
+ annotations,
266
+ block,
267
+ children,
268
+ focused,
269
+ leaf,
270
+ marks,
271
+ path,
272
+ renderAnnotation,
273
+ renderChild,
274
+ renderDecorator,
275
+ schemaTypes.annotations,
276
+ schemaTypes.decorators,
277
+ schemaTypes.span,
278
+ selected,
279
+ ])
280
+ return useMemo(
281
+ () => (
282
+ <span key={leaf._key} {...attributes} ref={spanRef}>
283
+ {content}
284
+ </span>
285
+ ),
286
+ [leaf, attributes, content],
287
+ )
288
+ }
@@ -0,0 +1,81 @@
1
+ import {type PropsWithChildren, useEffect, useMemo, useState} from 'react'
2
+ import {createEditor} from 'slate'
3
+ import {Slate, withReact} from 'slate-react'
4
+
5
+ import {type PatchObservable} from '../../types/editor'
6
+ import {debugWithName} from '../../utils/debug'
7
+ import {KEY_TO_SLATE_ELEMENT, KEY_TO_VALUE_ELEMENT} from '../../utils/weakMaps'
8
+ import {withPlugins} from '../plugins'
9
+ import {type PortableTextEditor} from '../PortableTextEditor'
10
+
11
+ const debug = debugWithName('component:PortableTextEditor:SlateContainer')
12
+
13
+ /**
14
+ * @internal
15
+ */
16
+ export interface SlateContainerProps extends PropsWithChildren {
17
+ keyGenerator: () => string
18
+ maxBlocks: number | undefined
19
+ patches$?: PatchObservable
20
+ portableTextEditor: PortableTextEditor
21
+ readOnly: boolean
22
+ }
23
+
24
+ /**
25
+ * Sets up and encapsulates the Slate instance
26
+ * @internal
27
+ */
28
+ export function SlateContainer(props: SlateContainerProps) {
29
+ const {patches$, portableTextEditor, readOnly, maxBlocks, keyGenerator} = props
30
+
31
+ // Create the slate instance, using `useState` ensures setup is only run once, initially
32
+ const [[slateEditor, subscribe]] = useState(() => {
33
+ debug('Creating new Slate editor instance')
34
+ const {editor, subscribe: _sub} = withPlugins(withReact(createEditor()), {
35
+ keyGenerator,
36
+ maxBlocks,
37
+ patches$,
38
+ portableTextEditor,
39
+ readOnly,
40
+ })
41
+ KEY_TO_VALUE_ELEMENT.set(editor, {})
42
+ KEY_TO_SLATE_ELEMENT.set(editor, {})
43
+ return [editor, _sub] as const
44
+ })
45
+
46
+ useEffect(() => {
47
+ const unsubscribe = subscribe()
48
+ return () => {
49
+ unsubscribe()
50
+ }
51
+ }, [subscribe])
52
+
53
+ // Update the slate instance when plugin dependent props change.
54
+ useEffect(() => {
55
+ debug('Re-initializing plugin chain')
56
+ withPlugins(slateEditor, {
57
+ keyGenerator,
58
+ maxBlocks,
59
+ patches$,
60
+ portableTextEditor,
61
+ readOnly,
62
+ })
63
+ }, [keyGenerator, portableTextEditor, maxBlocks, readOnly, patches$, slateEditor])
64
+
65
+ const initialValue = useMemo(() => {
66
+ return [slateEditor.pteCreateEmptyBlock()]
67
+ }, [slateEditor])
68
+
69
+ useEffect(() => {
70
+ return () => {
71
+ debug('Destroying Slate editor')
72
+ slateEditor.destroy()
73
+ }
74
+ }, [slateEditor])
75
+
76
+ return (
77
+ <Slate editor={slateEditor} initialValue={initialValue}>
78
+ {props.children}
79
+ </Slate>
80
+ )
81
+ }