@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,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
|
+
}
|