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