@portabletext/editor 1.48.15 → 1.49.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 (30) hide show
  1. package/lib/_chunks-cjs/editor-provider.cjs +32 -17
  2. package/lib/_chunks-cjs/editor-provider.cjs.map +1 -1
  3. package/lib/_chunks-cjs/util.slice-blocks.cjs +2 -0
  4. package/lib/_chunks-cjs/util.slice-blocks.cjs.map +1 -1
  5. package/lib/_chunks-es/editor-provider.js +32 -17
  6. package/lib/_chunks-es/editor-provider.js.map +1 -1
  7. package/lib/_chunks-es/util.slice-blocks.js +2 -0
  8. package/lib/_chunks-es/util.slice-blocks.js.map +1 -1
  9. package/lib/index.cjs +346 -273
  10. package/lib/index.cjs.map +1 -1
  11. package/lib/index.js +353 -280
  12. package/lib/index.js.map +1 -1
  13. package/package.json +1 -1
  14. package/src/behaviors/behavior.core.block-element.ts +108 -0
  15. package/src/converters/converter.portable-text.ts +4 -1
  16. package/src/converters/converter.text-html.ts +4 -1
  17. package/src/converters/converter.text-plain.ts +4 -1
  18. package/src/editor/Editable.tsx +2 -4
  19. package/src/editor/__tests__/PortableTextEditor.test.tsx +6 -0
  20. package/src/editor/components/Leaf.tsx +8 -1
  21. package/src/editor/components/render-block-object.tsx +90 -0
  22. package/src/editor/components/render-default-object.tsx +21 -0
  23. package/src/editor/components/render-element.tsx +140 -0
  24. package/src/editor/components/render-inline-object.tsx +93 -0
  25. package/src/editor/components/render-text-block.tsx +148 -0
  26. package/src/editor/components/use-core-block-element-behaviors.ts +39 -0
  27. package/src/internal-utils/parse-blocks.ts +2 -2
  28. package/src/internal-utils/slate-utils.ts +1 -1
  29. package/src/editor/components/DefaultObject.tsx +0 -21
  30. package/src/editor/components/Element.tsx +0 -461
@@ -0,0 +1,148 @@
1
+ import type {PortableTextTextBlock} from '@sanity/types'
2
+ import {useSelector} from '@xstate/react'
3
+ import {useContext, useRef, useState, type ReactElement} from 'react'
4
+ import {Range, type Element as SlateElement} from 'slate'
5
+ import {useSelected, useSlateStatic, type RenderElementProps} from 'slate-react'
6
+ import type {EventPositionBlock} from '../../internal-utils/event-position'
7
+ import type {
8
+ RenderBlockFunction,
9
+ RenderListItemFunction,
10
+ RenderStyleFunction,
11
+ } from '../../types/editor'
12
+ import {EditorActorContext} from '../editor-actor-context'
13
+ import {DropIndicator} from './drop-indicator'
14
+ import {useCoreBlockElementBehaviors} from './use-core-block-element-behaviors'
15
+
16
+ export function RenderTextBlock(props: {
17
+ attributes: RenderElementProps['attributes']
18
+ children: ReactElement
19
+ element: SlateElement
20
+ readOnly: boolean
21
+ renderBlock?: RenderBlockFunction
22
+ renderListItem?: RenderListItemFunction
23
+ renderStyle?: RenderStyleFunction
24
+ spellCheck?: boolean
25
+ textBlock: PortableTextTextBlock
26
+ }) {
27
+ const [dragPositionBlock, setDragPositionBlock] =
28
+ useState<EventPositionBlock>()
29
+ const blockRef = useRef<HTMLDivElement>(null)
30
+
31
+ const slateEditor = useSlateStatic()
32
+ const selected = useSelected()
33
+
34
+ const editorActor = useContext(EditorActorContext)
35
+
36
+ useCoreBlockElementBehaviors({
37
+ key: props.element._key,
38
+ onSetDragPositionBlock: setDragPositionBlock,
39
+ })
40
+
41
+ const legacySchema = useSelector(editorActor, (s) =>
42
+ s.context.getLegacySchema(),
43
+ )
44
+
45
+ const focused =
46
+ selected &&
47
+ slateEditor.selection !== null &&
48
+ Range.isCollapsed(slateEditor.selection)
49
+
50
+ let children = props.children
51
+
52
+ const legacyBlockSchemaType = legacySchema.block
53
+
54
+ if (props.renderStyle && props.textBlock.style) {
55
+ const legacyStyleSchemaType =
56
+ props.textBlock.style !== undefined
57
+ ? legacySchema.styles.find(
58
+ (style) => style.value === props.textBlock.style,
59
+ )
60
+ : undefined
61
+
62
+ if (legacyStyleSchemaType) {
63
+ children = props.renderStyle({
64
+ block: props.textBlock,
65
+ children,
66
+ editorElementRef: blockRef,
67
+ focused,
68
+ path: [{_key: props.textBlock._key}],
69
+ schemaType: legacyStyleSchemaType,
70
+ selected,
71
+ value: props.textBlock.style,
72
+ })
73
+ } else {
74
+ console.error(
75
+ `Unable to find Schema type for text block style ${props.textBlock.style}`,
76
+ )
77
+ }
78
+ }
79
+
80
+ if (props.renderListItem && props.textBlock.listItem) {
81
+ const legacyListItemSchemaType = legacySchema.lists.find(
82
+ (list) => list.value === props.textBlock.listItem,
83
+ )
84
+
85
+ if (legacyListItemSchemaType) {
86
+ children = props.renderListItem({
87
+ block: props.textBlock,
88
+ children,
89
+ editorElementRef: blockRef,
90
+ focused,
91
+ level: props.textBlock.level ?? 1,
92
+ path: [{_key: props.textBlock._key}],
93
+ selected,
94
+ value: props.textBlock.listItem,
95
+ schemaType: legacyListItemSchemaType,
96
+ })
97
+ } else {
98
+ console.error(
99
+ `Unable to find Schema type for text block list item ${props.textBlock.listItem}`,
100
+ )
101
+ }
102
+ }
103
+
104
+ return (
105
+ <div
106
+ key={props.element._key}
107
+ {...props.attributes}
108
+ className={[
109
+ 'pt-block',
110
+ 'pt-text-block',
111
+ ...(props.textBlock.style
112
+ ? [`pt-text-block-style-${props.textBlock.style}`]
113
+ : []),
114
+ ...(props.textBlock.listItem
115
+ ? [
116
+ 'pt-list-item',
117
+ `pt-list-item-${props.textBlock.listItem}`,
118
+ `pt-list-item-level-${props.textBlock.level ?? 1}`,
119
+ ]
120
+ : []),
121
+ ].join(' ')}
122
+ spellCheck={props.spellCheck}
123
+ data-block-key={props.textBlock._key}
124
+ data-block-name={props.textBlock._type}
125
+ data-block-type="text"
126
+ >
127
+ {dragPositionBlock === 'start' ? <DropIndicator /> : null}
128
+ <div ref={blockRef}>
129
+ {props.renderBlock
130
+ ? props.renderBlock({
131
+ children,
132
+ editorElementRef: blockRef,
133
+ focused,
134
+ level: props.textBlock.level,
135
+ listItem: props.textBlock.listItem,
136
+ path: [{_key: props.textBlock._key}],
137
+ selected,
138
+ schemaType: legacyBlockSchemaType,
139
+ style: props.textBlock.style,
140
+ type: legacyBlockSchemaType,
141
+ value: props.textBlock,
142
+ })
143
+ : props.children}
144
+ </div>
145
+ {dragPositionBlock === 'end' ? <DropIndicator /> : null}
146
+ </div>
147
+ )
148
+ }
@@ -0,0 +1,39 @@
1
+ import {useContext, useEffect} from 'react'
2
+ import {createCoreBlockElementBehaviorsConfig} from '../../behaviors/behavior.core.block-element'
3
+ import type {EventPositionBlock} from '../../internal-utils/event-position'
4
+ import {EditorActorContext} from '../editor-actor-context'
5
+
6
+ export function useCoreBlockElementBehaviors({
7
+ key,
8
+ onSetDragPositionBlock,
9
+ }: {
10
+ key: string
11
+ onSetDragPositionBlock: (
12
+ eventPositionBlock: EventPositionBlock | undefined,
13
+ ) => void
14
+ }) {
15
+ const editorActor = useContext(EditorActorContext)
16
+
17
+ useEffect(() => {
18
+ const behaviorConfigs = createCoreBlockElementBehaviorsConfig({
19
+ key,
20
+ onSetDragPositionBlock,
21
+ })
22
+
23
+ for (const behaviorConfig of behaviorConfigs) {
24
+ editorActor.send({
25
+ type: 'add behavior',
26
+ behaviorConfig,
27
+ })
28
+ }
29
+
30
+ return () => {
31
+ for (const behaviorConfig of behaviorConfigs) {
32
+ editorActor.send({
33
+ type: 'remove behavior',
34
+ behaviorConfig,
35
+ })
36
+ }
37
+ }
38
+ }, [editorActor, key, onSetDragPositionBlock])
39
+ }
@@ -49,7 +49,7 @@ export function parseBlock({
49
49
  )
50
50
  }
51
51
 
52
- function parseBlockObject({
52
+ export function parseBlockObject({
53
53
  blockObject,
54
54
  context,
55
55
  options,
@@ -104,7 +104,7 @@ export function isTextBlock(
104
104
  )
105
105
  }
106
106
 
107
- function parseTextBlock({
107
+ export function parseTextBlock({
108
108
  block,
109
109
  context,
110
110
  options,
@@ -73,7 +73,7 @@ export function getSelectionEndBlock({
73
73
  return getPointBlock({editor, point: selectionEndPoint})
74
74
  }
75
75
 
76
- function getPointBlock({
76
+ export function getPointBlock({
77
77
  editor,
78
78
  point,
79
79
  }: {
@@ -1,21 +0,0 @@
1
- import type {PortableTextBlock, PortableTextChild} from '@sanity/types'
2
-
3
- export function DefaultBlockObject(props: {
4
- value: PortableTextBlock | PortableTextChild
5
- }) {
6
- return (
7
- <div style={{userSelect: 'none'}}>
8
- [{props.value._type}: {props.value._key}]
9
- </div>
10
- )
11
- }
12
-
13
- export function DefaultInlineObject(props: {
14
- value: PortableTextBlock | PortableTextChild
15
- }) {
16
- return (
17
- <span style={{userSelect: 'none'}}>
18
- [{props.value._type}: {props.value._key}]
19
- </span>
20
- )
21
- }
@@ -1,461 +0,0 @@
1
- import type {
2
- Path,
3
- PortableTextChild,
4
- PortableTextObject,
5
- PortableTextTextBlock,
6
- } from '@sanity/types'
7
- import {
8
- useContext,
9
- useEffect,
10
- useMemo,
11
- useRef,
12
- useState,
13
- type FunctionComponent,
14
- type JSX,
15
- type ReactElement,
16
- } from 'react'
17
- import {Editor, Range, Element as SlateElement} from 'slate'
18
- import {
19
- ReactEditor,
20
- useSelected,
21
- useSlateStatic,
22
- type RenderElementProps,
23
- } from 'slate-react'
24
- import {defineBehavior, forward} from '../../behaviors'
25
- import {debugWithName} from '../../internal-utils/debug'
26
- import type {EventPositionBlock} from '../../internal-utils/event-position'
27
- import {fromSlateValue} from '../../internal-utils/values'
28
- import {KEY_TO_VALUE_ELEMENT} from '../../internal-utils/weakMaps'
29
- import {corePriority} from '../../priority/priority.core'
30
- import {createEditorPriority} from '../../priority/priority.types'
31
- import * as selectors from '../../selectors'
32
- import type {
33
- BlockRenderProps,
34
- PortableTextMemberSchemaTypes,
35
- RenderBlockFunction,
36
- RenderChildFunction,
37
- RenderListItemFunction,
38
- RenderStyleFunction,
39
- } from '../../types/editor'
40
- import {EditorActorContext} from '../editor-actor-context'
41
- import {DefaultBlockObject, DefaultInlineObject} from './DefaultObject'
42
- import {DropIndicator} from './drop-indicator'
43
-
44
- const debug = debugWithName('components:Element')
45
- const debugRenders = false
46
- const EMPTY_ANNOTATIONS: PortableTextObject[] = []
47
-
48
- /**
49
- * @internal
50
- */
51
- export interface ElementProps {
52
- attributes: RenderElementProps['attributes']
53
- children: ReactElement<any>
54
- element: SlateElement
55
- schemaTypes: PortableTextMemberSchemaTypes
56
- readOnly: boolean
57
- renderBlock?: RenderBlockFunction
58
- renderChild?: RenderChildFunction
59
- renderListItem?: RenderListItemFunction
60
- renderStyle?: RenderStyleFunction
61
- spellCheck?: boolean
62
- }
63
-
64
- const inlineBlockStyle = {display: 'inline-block'}
65
-
66
- /**
67
- * Renders Portable Text block and inline object nodes in Slate
68
- * @internal
69
- */
70
- export const Element: FunctionComponent<ElementProps> = ({
71
- attributes,
72
- children,
73
- element,
74
- schemaTypes,
75
- readOnly,
76
- renderBlock,
77
- renderChild,
78
- renderListItem,
79
- renderStyle,
80
- spellCheck,
81
- }) => {
82
- const editorActor = useContext(EditorActorContext)
83
- const slateEditor = useSlateStatic()
84
- const selected = useSelected()
85
- const blockRef = useRef<HTMLDivElement | null>(null)
86
- const inlineBlockObjectRef = useRef(null)
87
- const focused =
88
- (selected &&
89
- slateEditor.selection &&
90
- Range.isCollapsed(slateEditor.selection)) ||
91
- false
92
- const [dragPositionBlock, setDragPositionBlock] =
93
- useState<EventPositionBlock>()
94
-
95
- useEffect(() => {
96
- const behavior = defineBehavior({
97
- on: 'drag.dragover',
98
- guard: ({snapshot, event}) => {
99
- const dropFocusBlock = selectors.getFocusBlock({
100
- ...snapshot,
101
- context: {
102
- ...snapshot.context,
103
- selection: event.position.selection,
104
- },
105
- })
106
-
107
- if (!dropFocusBlock || dropFocusBlock.node._key !== element._key) {
108
- return false
109
- }
110
-
111
- const dragOrigin = snapshot.beta.internalDrag?.origin
112
-
113
- if (!dragOrigin) {
114
- return false
115
- }
116
-
117
- const draggedBlocks = selectors.getSelectedBlocks({
118
- ...snapshot,
119
- context: {
120
- ...snapshot.context,
121
- selection: dragOrigin.selection,
122
- },
123
- })
124
-
125
- if (
126
- draggedBlocks.some(
127
- (draggedBlock) => draggedBlock.node._key === element._key,
128
- )
129
- ) {
130
- return false
131
- }
132
-
133
- const draggingEntireBlocks = selectors.isSelectingEntireBlocks({
134
- ...snapshot,
135
- context: {
136
- ...snapshot.context,
137
- selection: dragOrigin.selection,
138
- },
139
- })
140
-
141
- return draggingEntireBlocks
142
- },
143
- actions: [
144
- ({event}) => [
145
- {
146
- type: 'effect',
147
- effect: () => {
148
- setDragPositionBlock(event.position.block)
149
- },
150
- },
151
- ],
152
- ],
153
- })
154
-
155
- const priority = createEditorPriority({
156
- reference: {
157
- priority: corePriority,
158
- importance: 'lower',
159
- },
160
- })
161
-
162
- const behaviorConfig = {
163
- behavior,
164
- priority,
165
- }
166
-
167
- editorActor.send({
168
- type: 'add behavior',
169
- behaviorConfig,
170
- })
171
-
172
- return () => {
173
- editorActor.send({
174
- type: 'remove behavior',
175
- behaviorConfig,
176
- })
177
- }
178
- }, [editorActor, element._key])
179
-
180
- useEffect(() => {
181
- const behavior = defineBehavior({
182
- on: 'drag.*',
183
- guard: ({event}) => {
184
- return event.type !== 'drag.dragover'
185
- },
186
- actions: [
187
- ({event}) => [
188
- {
189
- type: 'effect',
190
- effect: () => {
191
- setDragPositionBlock(undefined)
192
- },
193
- },
194
- forward(event),
195
- ],
196
- ],
197
- })
198
-
199
- const priority = createEditorPriority({
200
- reference: {
201
- priority: corePriority,
202
- importance: 'lower',
203
- },
204
- })
205
-
206
- const behaviorConfig = {
207
- behavior,
208
- priority,
209
- }
210
-
211
- editorActor.send({
212
- type: 'add behavior',
213
- behaviorConfig,
214
- })
215
-
216
- return () => {
217
- editorActor.send({
218
- type: 'remove behavior',
219
- behaviorConfig,
220
- })
221
- }
222
- }, [editorActor])
223
-
224
- const value = useMemo(
225
- () =>
226
- fromSlateValue(
227
- [element],
228
- schemaTypes.block.name,
229
- KEY_TO_VALUE_ELEMENT.get(slateEditor),
230
- )[0],
231
- [slateEditor, element, schemaTypes.block.name],
232
- )
233
-
234
- let renderedBlock = children
235
-
236
- let className: string | undefined
237
-
238
- const blockPath: Path = useMemo(() => [{_key: element._key}], [element])
239
-
240
- if (typeof element._type !== 'string') {
241
- throw new Error(`Expected element to have a _type property`)
242
- }
243
-
244
- if (typeof element._key !== 'string') {
245
- throw new Error(`Expected element to have a _key property`)
246
- }
247
-
248
- // Test for inline objects first
249
- if (slateEditor.isInline(element)) {
250
- const path = ReactEditor.findPath(slateEditor, element)
251
- const [block] = Editor.node(slateEditor, path, {depth: 1})
252
- const schemaType = schemaTypes.inlineObjects.find(
253
- (_type) => _type.name === element._type,
254
- )
255
- if (!schemaType) {
256
- throw new Error('Could not find type for inline block element')
257
- }
258
- if (SlateElement.isElement(block)) {
259
- const elmPath: Path = [
260
- {_key: block._key},
261
- 'children',
262
- {_key: element._key},
263
- ]
264
- if (debugRenders) {
265
- debug(`Render ${element._key} (inline object)`)
266
- }
267
- return (
268
- <span {...attributes}>
269
- {/* Note that children must follow immediately or cut and selections will not work properly in Chrome. */}
270
- {children}
271
- <span
272
- draggable={!readOnly}
273
- className="pt-inline-object"
274
- data-testid="pt-inline-object"
275
- ref={inlineBlockObjectRef}
276
- key={element._key}
277
- style={inlineBlockStyle}
278
- contentEditable={false}
279
- >
280
- {renderChild &&
281
- renderChild({
282
- annotations: EMPTY_ANNOTATIONS, // These inline objects currently doesn't support annotations. This is a limitation of the current PT spec/model.
283
- children: <DefaultInlineObject value={value} />,
284
- editorElementRef: inlineBlockObjectRef,
285
- focused,
286
- path: elmPath,
287
- schemaType,
288
- selected,
289
- type: schemaType,
290
- value: value as PortableTextChild,
291
- })}
292
- {!renderChild && <DefaultInlineObject value={value} />}
293
- </span>
294
- </span>
295
- )
296
- }
297
- throw new Error('Block not found!')
298
- }
299
-
300
- // If not inline, it's either a block (text) or a block object (non-text)
301
- // NOTE: text blocks aren't draggable with DraggableBlock (yet?)
302
- if (element._type === schemaTypes.block.name) {
303
- className = `pt-block pt-text-block`
304
- const isListItem = 'listItem' in element
305
- if (debugRenders) {
306
- debug(`Render ${element._key} (text block)`)
307
- }
308
- const style = ('style' in element && element.style) || 'normal'
309
- className = `pt-block pt-text-block pt-text-block-style-${style}`
310
- const blockStyleType = schemaTypes.styles.find(
311
- (item) => item.value === style,
312
- )
313
- if (renderStyle && blockStyleType) {
314
- renderedBlock = renderStyle({
315
- block: element as PortableTextTextBlock,
316
- children,
317
- focused,
318
- selected,
319
- value: style,
320
- path: blockPath,
321
- schemaType: blockStyleType,
322
- editorElementRef: blockRef,
323
- })
324
- }
325
- let level: number | undefined
326
-
327
- if (isListItem) {
328
- if (typeof element.level === 'number') {
329
- level = element.level
330
- }
331
- className += ` pt-list-item pt-list-item-${element.listItem} pt-list-item-level-${level || 1}`
332
- }
333
-
334
- if (slateEditor.isListBlock(value) && isListItem && element.listItem) {
335
- const listType = schemaTypes.lists.find(
336
- (item) => item.value === element.listItem,
337
- )
338
- if (renderListItem && listType) {
339
- renderedBlock = renderListItem({
340
- block: value,
341
- children: renderedBlock,
342
- focused,
343
- selected,
344
- value: element.listItem,
345
- path: blockPath,
346
- schemaType: listType,
347
- level: value.level || 1,
348
- editorElementRef: blockRef,
349
- })
350
- }
351
- }
352
-
353
- const renderProps: Omit<BlockRenderProps, 'type'> = Object.defineProperty(
354
- {
355
- children: renderedBlock,
356
- editorElementRef: blockRef,
357
- focused,
358
- level,
359
- listItem: isListItem ? element.listItem : undefined,
360
- path: blockPath,
361
- selected,
362
- style,
363
- schemaType: schemaTypes.block,
364
- value,
365
- },
366
- 'type',
367
- {
368
- enumerable: false,
369
- get() {
370
- console.warn(
371
- "Property 'type' is deprecated, use 'schemaType' instead.",
372
- )
373
- return schemaTypes.block
374
- },
375
- },
376
- )
377
-
378
- const propsOrDefaultRendered = renderBlock
379
- ? renderBlock(renderProps as BlockRenderProps)
380
- : children
381
-
382
- return (
383
- <div
384
- key={element._key}
385
- {...attributes}
386
- className={className}
387
- spellCheck={spellCheck}
388
- >
389
- {dragPositionBlock === 'start' ? <DropIndicator /> : null}
390
- <div ref={blockRef}>{propsOrDefaultRendered}</div>
391
- {dragPositionBlock === 'end' ? <DropIndicator /> : null}
392
- </div>
393
- )
394
- }
395
-
396
- const schemaType = schemaTypes.blockObjects.find(
397
- (_type) => _type.name === element._type,
398
- )
399
-
400
- if (!schemaType) {
401
- throw new Error(
402
- `Could not find schema type for block element of _type ${element._type}`,
403
- )
404
- }
405
-
406
- if (debugRenders) {
407
- debug(`Render ${element._key} (object block)`)
408
- }
409
-
410
- className = 'pt-block pt-object-block'
411
-
412
- const block = fromSlateValue(
413
- [element],
414
- schemaTypes.block.name,
415
- KEY_TO_VALUE_ELEMENT.get(slateEditor),
416
- )[0]
417
-
418
- let renderedBlockFromProps: JSX.Element | undefined
419
-
420
- if (renderBlock) {
421
- const _props: Omit<BlockRenderProps, 'type'> = Object.defineProperty(
422
- {
423
- children: <DefaultBlockObject value={value} />,
424
- editorElementRef: blockRef,
425
- focused,
426
- path: blockPath,
427
- schemaType,
428
- selected,
429
- value: block,
430
- },
431
- 'type',
432
- {
433
- enumerable: false,
434
- get() {
435
- console.warn(
436
- "Property 'type' is deprecated, use 'schemaType' instead.",
437
- )
438
- return schemaType
439
- },
440
- },
441
- )
442
- renderedBlockFromProps = renderBlock(_props as BlockRenderProps)
443
- }
444
-
445
- return (
446
- <div key={element._key} {...attributes} className={className}>
447
- {dragPositionBlock === 'start' ? <DropIndicator /> : null}
448
- {children}
449
- <div ref={blockRef} contentEditable={false} draggable={!readOnly}>
450
- {renderedBlockFromProps ? (
451
- renderedBlockFromProps
452
- ) : (
453
- <DefaultBlockObject value={value} />
454
- )}
455
- </div>
456
- {dragPositionBlock === 'end' ? <DropIndicator /> : null}
457
- </div>
458
- )
459
- }
460
-
461
- Element.displayName = 'Element'