@portabletext/editor 1.21.6 → 1.23.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/lib/_chunks-cjs/behavior.core.cjs.map +1 -1
- package/lib/_chunks-cjs/selector.get-text-before.cjs +4 -4
- package/lib/_chunks-cjs/selector.get-text-before.cjs.map +1 -1
- package/lib/_chunks-cjs/{util.get-block-start-point.cjs → util.reverse-selection.cjs} +12 -12
- package/lib/_chunks-cjs/util.reverse-selection.cjs.map +1 -0
- package/lib/_chunks-cjs/util.slice-blocks.cjs +75 -0
- package/lib/_chunks-cjs/util.slice-blocks.cjs.map +1 -0
- package/lib/_chunks-es/behavior.core.js.map +1 -1
- package/lib/_chunks-es/selector.get-text-before.js +1 -1
- package/lib/_chunks-es/{util.get-block-start-point.js → util.reverse-selection.js} +12 -12
- package/lib/_chunks-es/util.reverse-selection.js.map +1 -0
- package/lib/_chunks-es/util.slice-blocks.js +76 -0
- package/lib/_chunks-es/util.slice-blocks.js.map +1 -0
- package/lib/behaviors/index.cjs.map +1 -1
- package/lib/behaviors/index.d.cts +57 -0
- package/lib/behaviors/index.d.ts +57 -0
- package/lib/behaviors/index.js.map +1 -1
- package/lib/index.cjs +33 -12
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.cts +418 -0
- package/lib/index.d.ts +418 -0
- package/lib/index.js +33 -12
- package/lib/index.js.map +1 -1
- package/lib/selectors/index.cjs +10 -4
- package/lib/selectors/index.cjs.map +1 -1
- package/lib/selectors/index.d.cts +5 -0
- package/lib/selectors/index.d.ts +5 -0
- package/lib/selectors/index.js +9 -2
- package/lib/selectors/index.js.map +1 -1
- package/lib/utils/index.cjs +4 -3
- package/lib/utils/index.cjs.map +1 -1
- package/lib/utils/index.d.cts +11 -0
- package/lib/utils/index.d.ts +11 -0
- package/lib/utils/index.js +3 -1
- package/lib/utils/index.js.map +1 -1
- package/package.json +16 -12
- package/src/behaviors/behavior.markdown.ts +42 -0
- package/src/behaviors/behavior.types.ts +15 -0
- package/src/editor/Editable.tsx +17 -0
- package/src/editor/create-editor.ts +1 -0
- package/src/editor/define-schema.ts +24 -1
- package/src/editor/editor-event-listener.tsx +45 -0
- package/src/editor/editor-machine.ts +13 -26
- package/src/editor/editor-provider.tsx +27 -0
- package/src/editor/editor-selector.ts +21 -0
- package/src/editor/editor-snapshot.ts +37 -1
- package/src/selectors/index.ts +1 -0
- package/src/selectors/selector.get-selected-slice.ts +12 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/util.slice-blocks.test.ts +257 -0
- package/src/utils/util.slice-blocks.ts +153 -0
- package/lib/_chunks-cjs/util.get-block-start-point.cjs.map +0 -1
- package/lib/_chunks-es/util.get-block-start-point.js.map +0 -1
|
@@ -5,6 +5,51 @@ import {useEditor} from './editor-provider'
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* @public
|
|
8
|
+
* Listen for events emitted by the editor. Must be used inside `EditorProvider`. Events available include:
|
|
9
|
+
* - 'blurred'
|
|
10
|
+
* - 'done loading'
|
|
11
|
+
* - 'editable'
|
|
12
|
+
* - 'error'
|
|
13
|
+
* - 'focused'
|
|
14
|
+
* - 'invalid value'
|
|
15
|
+
* - 'loading'
|
|
16
|
+
* - 'mutation'
|
|
17
|
+
* - 'patch'
|
|
18
|
+
* - 'read only'
|
|
19
|
+
* - 'ready'
|
|
20
|
+
* - 'selection'
|
|
21
|
+
* - 'value changed'
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* Listen and log events.
|
|
25
|
+
* ```tsx
|
|
26
|
+
* import {EditorEventListener, EditorProvider} from '@portabletext/editor'
|
|
27
|
+
*
|
|
28
|
+
* function MyComponent() {
|
|
29
|
+
* return (
|
|
30
|
+
* <EditorProvider>
|
|
31
|
+
* <EditorEventListener
|
|
32
|
+
* on={(event) => {
|
|
33
|
+
* console.log(event)
|
|
34
|
+
* }
|
|
35
|
+
* } />
|
|
36
|
+
* { ... }
|
|
37
|
+
* </EditorProvider>
|
|
38
|
+
* )
|
|
39
|
+
* }
|
|
40
|
+
* ```
|
|
41
|
+
* @example
|
|
42
|
+
* Handle events when there is a mutation.
|
|
43
|
+
* ```tsx
|
|
44
|
+
* <EditorEventListener
|
|
45
|
+
* on={(event) => {
|
|
46
|
+
* if (event.type === 'mutation') {
|
|
47
|
+
* console.log('Value changed:', event.snapshot)
|
|
48
|
+
* }
|
|
49
|
+
* }}
|
|
50
|
+
* />
|
|
51
|
+
* ```
|
|
52
|
+
* @group Components
|
|
8
53
|
*/
|
|
9
54
|
export function EditorEventListener(props: {
|
|
10
55
|
on: (event: EditorEmittedEvent) => void
|
|
@@ -20,9 +20,6 @@ import {
|
|
|
20
20
|
type NativeBehaviorEvent,
|
|
21
21
|
type SyntheticBehaviorEvent,
|
|
22
22
|
} from '../behaviors/behavior.types'
|
|
23
|
-
import {toPortableTextRange} from '../internal-utils/ranges'
|
|
24
|
-
import {fromSlateValue} from '../internal-utils/values'
|
|
25
|
-
import {KEY_TO_VALUE_ELEMENT} from '../internal-utils/weakMaps'
|
|
26
23
|
import type {OmitFromUnion, PickFromUnion} from '../type-utils'
|
|
27
24
|
import type {
|
|
28
25
|
EditorSelection,
|
|
@@ -30,8 +27,7 @@ import type {
|
|
|
30
27
|
PortableTextSlateEditor,
|
|
31
28
|
} from '../types/editor'
|
|
32
29
|
import type {EditorSchema} from './define-schema'
|
|
33
|
-
import
|
|
34
|
-
import {getActiveDecorators} from './get-active-decorators'
|
|
30
|
+
import {createEditorSnapshot} from './editor-snapshot'
|
|
35
31
|
import {withApplyingBehaviorActions} from './with-applying-behavior-actions'
|
|
36
32
|
|
|
37
33
|
export * from 'xstate/guards'
|
|
@@ -109,6 +105,10 @@ export type InternalEditorEvent =
|
|
|
109
105
|
type: 'update behaviors'
|
|
110
106
|
behaviors: Array<Behavior>
|
|
111
107
|
}
|
|
108
|
+
| {
|
|
109
|
+
type: 'update key generator'
|
|
110
|
+
keyGenerator: () => string
|
|
111
|
+
}
|
|
112
112
|
| {
|
|
113
113
|
type: 'update value'
|
|
114
114
|
value: Array<PortableTextBlock> | undefined
|
|
@@ -342,27 +342,11 @@ export const editorMachine = setup({
|
|
|
342
342
|
return
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
-
const
|
|
346
|
-
event.editor
|
|
347
|
-
context.schema.block.name,
|
|
348
|
-
KEY_TO_VALUE_ELEMENT.get(event.editor),
|
|
349
|
-
)
|
|
350
|
-
const selection = toPortableTextRange(
|
|
351
|
-
value,
|
|
352
|
-
event.editor.selection,
|
|
353
|
-
context.schema,
|
|
354
|
-
)
|
|
355
|
-
|
|
356
|
-
const editorContext = {
|
|
357
|
-
activeDecorators: getActiveDecorators({
|
|
358
|
-
schema: context.schema,
|
|
359
|
-
slateEditorInstance: event.editor,
|
|
360
|
-
}),
|
|
345
|
+
const editorSnapshot = createEditorSnapshot({
|
|
346
|
+
editor: event.editor,
|
|
361
347
|
keyGenerator: context.keyGenerator,
|
|
362
348
|
schema: context.schema,
|
|
363
|
-
|
|
364
|
-
value,
|
|
365
|
-
} satisfies EditorContext
|
|
349
|
+
})
|
|
366
350
|
|
|
367
351
|
let behaviorOverwritten = false
|
|
368
352
|
|
|
@@ -370,7 +354,7 @@ export const editorMachine = setup({
|
|
|
370
354
|
const shouldRun =
|
|
371
355
|
eventBehavior.guard === undefined ||
|
|
372
356
|
eventBehavior.guard({
|
|
373
|
-
context:
|
|
357
|
+
context: editorSnapshot.context,
|
|
374
358
|
event: event.behaviorEvent,
|
|
375
359
|
})
|
|
376
360
|
|
|
@@ -380,7 +364,7 @@ export const editorMachine = setup({
|
|
|
380
364
|
|
|
381
365
|
const actionIntendSets = eventBehavior.actions.map((actionSet) =>
|
|
382
366
|
actionSet(
|
|
383
|
-
{context:
|
|
367
|
+
{context: editorSnapshot.context, event: event.behaviorEvent},
|
|
384
368
|
shouldRun,
|
|
385
369
|
),
|
|
386
370
|
)
|
|
@@ -513,6 +497,9 @@ export const editorMachine = setup({
|
|
|
513
497
|
'patches': {actions: emit(({event}) => event)},
|
|
514
498
|
'done loading': {actions: emit({type: 'done loading'})},
|
|
515
499
|
'update behaviors': {actions: 'assign behaviors'},
|
|
500
|
+
'update key generator': {
|
|
501
|
+
actions: assign({keyGenerator: ({event}) => event.keyGenerator}),
|
|
502
|
+
},
|
|
516
503
|
'update schema': {actions: 'assign schema'},
|
|
517
504
|
'update value': {actions: assign({value: ({event}) => event.value})},
|
|
518
505
|
'update maxBlocks': {
|
|
@@ -23,6 +23,21 @@ export type EditorProviderProps = {
|
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* @public
|
|
26
|
+
* The EditorProvider component is used to set up the editor context and configure the Portable Text Editor.
|
|
27
|
+
* @example
|
|
28
|
+
* ```tsx
|
|
29
|
+
* import {EditorProvider} from '@portabletext/editor'
|
|
30
|
+
*
|
|
31
|
+
* function App() {
|
|
32
|
+
* return (
|
|
33
|
+
* <EditorProvider initialConfig={{ ... }} >
|
|
34
|
+
* ...
|
|
35
|
+
* </EditorProvider>
|
|
36
|
+
* )
|
|
37
|
+
* }
|
|
38
|
+
*
|
|
39
|
+
* ```
|
|
40
|
+
* @group Components
|
|
26
41
|
*/
|
|
27
42
|
export function EditorProvider(props: EditorProviderProps) {
|
|
28
43
|
const editor = useCreateEditor(props.initialConfig)
|
|
@@ -66,6 +81,18 @@ export function EditorProvider(props: EditorProviderProps) {
|
|
|
66
81
|
|
|
67
82
|
/**
|
|
68
83
|
* @public
|
|
84
|
+
* Get the current editor context from the `EditorProvider`.
|
|
85
|
+
* Must be used inside the `EditorProvider` component.
|
|
86
|
+
* @returns The current editor object.
|
|
87
|
+
* @example
|
|
88
|
+
* ```tsx
|
|
89
|
+
* import { useEditor } from '@portabletext/editor'
|
|
90
|
+
*
|
|
91
|
+
* function MyComponent() {
|
|
92
|
+
* const editor = useEditor()
|
|
93
|
+
* }
|
|
94
|
+
* ```
|
|
95
|
+
* @group Hooks
|
|
69
96
|
*/
|
|
70
97
|
export function useEditor() {
|
|
71
98
|
const editor = React.useContext(EditorContext)
|
|
@@ -17,6 +17,27 @@ export type EditorSelector<TSelected> = (snapshot: EditorSnapshot) => TSelected
|
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* @public
|
|
20
|
+
* Hook to select a value from the editor state.
|
|
21
|
+
* @example
|
|
22
|
+
* Pass a selector as the second argument
|
|
23
|
+
* ```tsx
|
|
24
|
+
* import { useEditorSelector } from '@portabletext/editor'
|
|
25
|
+
*
|
|
26
|
+
* function MyComponent(editor) {
|
|
27
|
+
* const value = useEditorSelector(editor, selector)
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
* @example
|
|
31
|
+
* Pass an inline selector as the second argument.
|
|
32
|
+
* In this case, use the editor context to obtain the schema.
|
|
33
|
+
* ```tsx
|
|
34
|
+
* import { useEditorSelector } from '@portabletext/editor'
|
|
35
|
+
*
|
|
36
|
+
* function MyComponent(editor) {
|
|
37
|
+
* const schema = useEditorSelector(editor, (snapshot) => snapshot.context.schema)
|
|
38
|
+
* }
|
|
39
|
+
* ```
|
|
40
|
+
* @group Hooks
|
|
20
41
|
*/
|
|
21
42
|
export function useEditorSelector<TSelected>(
|
|
22
43
|
editor: Editor,
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type {PortableTextBlock} from '@sanity/types'
|
|
2
|
-
import
|
|
2
|
+
import {toPortableTextRange} from '../internal-utils/ranges'
|
|
3
|
+
import {fromSlateValue} from '../internal-utils/values'
|
|
4
|
+
import {KEY_TO_VALUE_ELEMENT} from '../internal-utils/weakMaps'
|
|
5
|
+
import type {EditorSelection, PortableTextSlateEditor} from '../types/editor'
|
|
3
6
|
import type {EditorSchema} from './define-schema'
|
|
7
|
+
import {getActiveDecorators} from './get-active-decorators'
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
10
|
* @public
|
|
@@ -19,3 +23,35 @@ export type EditorContext = {
|
|
|
19
23
|
export type EditorSnapshot = {
|
|
20
24
|
context: EditorContext
|
|
21
25
|
}
|
|
26
|
+
|
|
27
|
+
export function createEditorSnapshot({
|
|
28
|
+
editor,
|
|
29
|
+
keyGenerator,
|
|
30
|
+
schema,
|
|
31
|
+
}: {
|
|
32
|
+
editor: PortableTextSlateEditor
|
|
33
|
+
keyGenerator: () => string
|
|
34
|
+
schema: EditorSchema
|
|
35
|
+
}) {
|
|
36
|
+
const value = fromSlateValue(
|
|
37
|
+
editor.children,
|
|
38
|
+
schema.block.name,
|
|
39
|
+
KEY_TO_VALUE_ELEMENT.get(editor),
|
|
40
|
+
)
|
|
41
|
+
const selection = toPortableTextRange(value, editor.selection, schema)
|
|
42
|
+
|
|
43
|
+
const context = {
|
|
44
|
+
activeDecorators: getActiveDecorators({
|
|
45
|
+
schema,
|
|
46
|
+
slateEditorInstance: editor,
|
|
47
|
+
}),
|
|
48
|
+
keyGenerator,
|
|
49
|
+
schema,
|
|
50
|
+
selection,
|
|
51
|
+
value,
|
|
52
|
+
} satisfies EditorContext
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
context,
|
|
56
|
+
} satisfies EditorSnapshot
|
|
57
|
+
}
|
package/src/selectors/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ export type {
|
|
|
9
9
|
export type {EditorSchema} from '../editor/define-schema'
|
|
10
10
|
export {getActiveListItem} from './selector.get-active-list-item'
|
|
11
11
|
export {getActiveStyle} from './selector.get-active-style'
|
|
12
|
+
export {getSelectedSlice} from './selector.get-selected-slice'
|
|
12
13
|
export {getSelectedSpans} from './selector.get-selected-spans'
|
|
13
14
|
export {getSelectionText} from './selector.get-selection-text'
|
|
14
15
|
export {getBlockTextBefore} from './selector.get-text-before'
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type {PortableTextBlock} from '@sanity/types'
|
|
2
|
+
import type {EditorSelector} from '../editor/editor-selector'
|
|
3
|
+
import {sliceBlocks} from '../utils'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @public
|
|
7
|
+
*/
|
|
8
|
+
export const getSelectedSlice: EditorSelector<Array<PortableTextBlock>> = ({
|
|
9
|
+
context,
|
|
10
|
+
}) => {
|
|
11
|
+
return sliceBlocks({blocks: context.value, selection: context.selection})
|
|
12
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -9,3 +9,4 @@ export {getTextBlockText} from './util.get-text-block-text'
|
|
|
9
9
|
export {isEmptyTextBlock} from './util.is-empty-text-block'
|
|
10
10
|
export {isKeyedSegment} from './util.is-keyed-segment'
|
|
11
11
|
export {reverseSelection} from './util.reverse-selection'
|
|
12
|
+
export {sliceBlocks} from './util.slice-blocks'
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import type {PortableTextBlock, PortableTextTextBlock} from '@sanity/types'
|
|
2
|
+
import {describe, expect, test} from 'vitest'
|
|
3
|
+
import {sliceBlocks} from './util.slice-blocks'
|
|
4
|
+
|
|
5
|
+
const b1: PortableTextTextBlock = {
|
|
6
|
+
_type: 'block',
|
|
7
|
+
_key: 'b1',
|
|
8
|
+
children: [
|
|
9
|
+
{
|
|
10
|
+
_type: 'span',
|
|
11
|
+
_key: 'b1c1',
|
|
12
|
+
text: 'foo',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
_type: 'span',
|
|
16
|
+
_key: 'b1c2',
|
|
17
|
+
text: 'bar',
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
}
|
|
21
|
+
const b2: PortableTextBlock = {
|
|
22
|
+
_type: 'image',
|
|
23
|
+
_key: 'b2',
|
|
24
|
+
src: 'https://example.com/image.jpg',
|
|
25
|
+
alt: 'Example',
|
|
26
|
+
}
|
|
27
|
+
const b3: PortableTextTextBlock = {
|
|
28
|
+
_type: 'block',
|
|
29
|
+
_key: 'b3',
|
|
30
|
+
children: [
|
|
31
|
+
{
|
|
32
|
+
_type: 'span',
|
|
33
|
+
_key: 'b3c1',
|
|
34
|
+
text: 'baz',
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
}
|
|
38
|
+
const b4: PortableTextTextBlock = {
|
|
39
|
+
_type: 'block',
|
|
40
|
+
_key: 'b4',
|
|
41
|
+
children: [
|
|
42
|
+
{
|
|
43
|
+
_type: 'span',
|
|
44
|
+
_key: 'b4c1',
|
|
45
|
+
text: 'fizz',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
_type: 'stock-ticker',
|
|
49
|
+
_key: 'b4c2',
|
|
50
|
+
symbol: 'AAPL',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
_type: 'span',
|
|
54
|
+
_key: 'b4c3',
|
|
55
|
+
text: 'buzz',
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const blocks: Array<PortableTextBlock> = [b1, b2, b3, b4]
|
|
61
|
+
|
|
62
|
+
describe(sliceBlocks.name, () => {
|
|
63
|
+
test('sensible defaults', () => {
|
|
64
|
+
expect(sliceBlocks({blocks: [], selection: null})).toEqual([])
|
|
65
|
+
expect(sliceBlocks({blocks, selection: null})).toEqual([])
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('slicing a single block', () => {
|
|
69
|
+
expect(
|
|
70
|
+
sliceBlocks({
|
|
71
|
+
blocks,
|
|
72
|
+
selection: {
|
|
73
|
+
anchor: {
|
|
74
|
+
path: [{_key: b1._key}, 'children', {_key: b1.children[0]._key}],
|
|
75
|
+
offset: 0,
|
|
76
|
+
},
|
|
77
|
+
focus: {
|
|
78
|
+
path: [{_key: b1._key}, 'children', {_key: b1.children[0]._key}],
|
|
79
|
+
offset: 3,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
}),
|
|
83
|
+
).toEqual([
|
|
84
|
+
{
|
|
85
|
+
...b1,
|
|
86
|
+
children: [b1.children[0]],
|
|
87
|
+
},
|
|
88
|
+
])
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('slicing a single span', () => {
|
|
92
|
+
expect(
|
|
93
|
+
sliceBlocks({
|
|
94
|
+
blocks,
|
|
95
|
+
selection: {
|
|
96
|
+
anchor: {
|
|
97
|
+
path: [{_key: b1._key}, 'children', {_key: b1.children[0]._key}],
|
|
98
|
+
offset: 1,
|
|
99
|
+
},
|
|
100
|
+
focus: {
|
|
101
|
+
path: [{_key: b1._key}, 'children', {_key: b1.children[0]._key}],
|
|
102
|
+
offset: 2,
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
}),
|
|
106
|
+
).toEqual([
|
|
107
|
+
{
|
|
108
|
+
...b1,
|
|
109
|
+
children: [
|
|
110
|
+
{
|
|
111
|
+
...b1.children[0],
|
|
112
|
+
text: 'o',
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
])
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('starting and ending selection on a block object', () => {
|
|
120
|
+
expect(
|
|
121
|
+
sliceBlocks({
|
|
122
|
+
blocks,
|
|
123
|
+
selection: {
|
|
124
|
+
anchor: {
|
|
125
|
+
path: [{_key: b2._key}],
|
|
126
|
+
offset: 0,
|
|
127
|
+
},
|
|
128
|
+
focus: {
|
|
129
|
+
path: [{_key: b2._key}],
|
|
130
|
+
offset: 0,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
}),
|
|
134
|
+
).toEqual([b2])
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('ending selection on a block object', () => {
|
|
138
|
+
expect(
|
|
139
|
+
sliceBlocks({
|
|
140
|
+
blocks,
|
|
141
|
+
selection: {
|
|
142
|
+
anchor: {
|
|
143
|
+
path: [{_key: b1._key}, 'children', {_key: b1.children[0]._key}],
|
|
144
|
+
offset: 3,
|
|
145
|
+
},
|
|
146
|
+
focus: {
|
|
147
|
+
path: [{_key: b2._key}],
|
|
148
|
+
offset: 0,
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
}),
|
|
152
|
+
).toEqual([
|
|
153
|
+
{
|
|
154
|
+
...b1,
|
|
155
|
+
children: [
|
|
156
|
+
{
|
|
157
|
+
...b1.children[0],
|
|
158
|
+
text: '',
|
|
159
|
+
},
|
|
160
|
+
...b1.children.slice(1),
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
blocks[1],
|
|
164
|
+
])
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
test('slicing across block object', () => {
|
|
168
|
+
expect(
|
|
169
|
+
sliceBlocks({
|
|
170
|
+
blocks,
|
|
171
|
+
selection: {
|
|
172
|
+
anchor: {
|
|
173
|
+
path: [{_key: b1._key}, 'children', {_key: b1.children[0]._key}],
|
|
174
|
+
offset: 0,
|
|
175
|
+
},
|
|
176
|
+
focus: {
|
|
177
|
+
path: [{_key: b3._key}, 'children', {_key: b3.children[0]._key}],
|
|
178
|
+
offset: 3,
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
}),
|
|
182
|
+
).toEqual([b1, b2, b3])
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test('starting and ending mid-span', () => {
|
|
186
|
+
expect(
|
|
187
|
+
sliceBlocks({
|
|
188
|
+
blocks,
|
|
189
|
+
selection: {
|
|
190
|
+
anchor: {
|
|
191
|
+
path: [{_key: b3._key}, 'children', {_key: b3.children[0]._key}],
|
|
192
|
+
offset: 2,
|
|
193
|
+
},
|
|
194
|
+
focus: {
|
|
195
|
+
path: [{_key: b4._key}, 'children', {_key: b4.children[0]._key}],
|
|
196
|
+
offset: 1,
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
}),
|
|
200
|
+
).toEqual([
|
|
201
|
+
{
|
|
202
|
+
...b3,
|
|
203
|
+
children: [
|
|
204
|
+
{
|
|
205
|
+
...b3.children[0],
|
|
206
|
+
text: 'z',
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
...b4,
|
|
212
|
+
children: [
|
|
213
|
+
{
|
|
214
|
+
...b4.children[0],
|
|
215
|
+
text: 'f',
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
},
|
|
219
|
+
])
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
test('starting mid-span and ending end-span', () => {
|
|
223
|
+
expect(
|
|
224
|
+
sliceBlocks({
|
|
225
|
+
blocks,
|
|
226
|
+
selection: {
|
|
227
|
+
anchor: {
|
|
228
|
+
path: [{_key: b3._key}, 'children', {_key: b3.children[0]._key}],
|
|
229
|
+
offset: 2,
|
|
230
|
+
},
|
|
231
|
+
focus: {
|
|
232
|
+
path: [{_key: b4._key}, 'children', {_key: b4.children[0]._key}],
|
|
233
|
+
offset: 4,
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
}),
|
|
237
|
+
).toEqual([
|
|
238
|
+
{
|
|
239
|
+
...b3,
|
|
240
|
+
children: [
|
|
241
|
+
{
|
|
242
|
+
...b3.children[0],
|
|
243
|
+
text: 'z',
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
...b4,
|
|
249
|
+
children: [
|
|
250
|
+
{
|
|
251
|
+
...b4.children[0],
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
},
|
|
255
|
+
])
|
|
256
|
+
})
|
|
257
|
+
})
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isKeySegment,
|
|
3
|
+
isPortableTextSpan,
|
|
4
|
+
isPortableTextTextBlock,
|
|
5
|
+
type PortableTextBlock,
|
|
6
|
+
} from '@sanity/types'
|
|
7
|
+
import type {EditorSelection} from '../selectors'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @public
|
|
11
|
+
*/
|
|
12
|
+
export function sliceBlocks({
|
|
13
|
+
blocks,
|
|
14
|
+
selection,
|
|
15
|
+
}: {
|
|
16
|
+
blocks: Array<PortableTextBlock>
|
|
17
|
+
selection: EditorSelection
|
|
18
|
+
}): Array<PortableTextBlock> {
|
|
19
|
+
const slice: Array<PortableTextBlock> = []
|
|
20
|
+
|
|
21
|
+
if (!selection) {
|
|
22
|
+
return slice
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let startBlock: PortableTextBlock | undefined
|
|
26
|
+
const middleBlocks: PortableTextBlock[] = []
|
|
27
|
+
let endBlock: PortableTextBlock | undefined
|
|
28
|
+
|
|
29
|
+
const startPoint = selection.backward ? selection.focus : selection.anchor
|
|
30
|
+
const endPoint = selection.backward ? selection.anchor : selection.focus
|
|
31
|
+
|
|
32
|
+
const startBlockKey = isKeySegment(startPoint.path[0])
|
|
33
|
+
? startPoint.path[0]._key
|
|
34
|
+
: undefined
|
|
35
|
+
const endBlockKey = isKeySegment(endPoint.path[0])
|
|
36
|
+
? endPoint.path[0]._key
|
|
37
|
+
: undefined
|
|
38
|
+
const startChildKey = isKeySegment(startPoint.path[2])
|
|
39
|
+
? startPoint.path[2]._key
|
|
40
|
+
: undefined
|
|
41
|
+
const endChildKey = isKeySegment(endPoint.path[2])
|
|
42
|
+
? endPoint.path[2]._key
|
|
43
|
+
: undefined
|
|
44
|
+
|
|
45
|
+
if (!startBlockKey || !endBlockKey) {
|
|
46
|
+
return slice
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const block of blocks) {
|
|
50
|
+
if (block._key === startBlockKey) {
|
|
51
|
+
if (isPortableTextTextBlock(block) && startChildKey) {
|
|
52
|
+
for (const child of block.children) {
|
|
53
|
+
if (child._key === startChildKey) {
|
|
54
|
+
if (isPortableTextSpan(child)) {
|
|
55
|
+
const text =
|
|
56
|
+
child._key === endChildKey
|
|
57
|
+
? child.text.slice(startPoint.offset, endPoint.offset)
|
|
58
|
+
: child.text.slice(startPoint.offset)
|
|
59
|
+
|
|
60
|
+
startBlock = {
|
|
61
|
+
...block,
|
|
62
|
+
children: [
|
|
63
|
+
{
|
|
64
|
+
...child,
|
|
65
|
+
text,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
}
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
startBlock = {
|
|
73
|
+
...block,
|
|
74
|
+
children: [child],
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (startChildKey === endChildKey) {
|
|
79
|
+
break
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (startBlock && isPortableTextTextBlock(startBlock)) {
|
|
83
|
+
startBlock.children.push(child)
|
|
84
|
+
|
|
85
|
+
if (
|
|
86
|
+
block._key === endBlockKey &&
|
|
87
|
+
endChildKey &&
|
|
88
|
+
child._key === endChildKey
|
|
89
|
+
) {
|
|
90
|
+
break
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (startBlockKey === endBlockKey) {
|
|
96
|
+
break
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
startBlock = block
|
|
103
|
+
|
|
104
|
+
if (startBlockKey === endBlockKey) {
|
|
105
|
+
break
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (block._key === endBlockKey) {
|
|
110
|
+
if (isPortableTextTextBlock(block) && endBlockKey) {
|
|
111
|
+
endBlock = {
|
|
112
|
+
...block,
|
|
113
|
+
children: [],
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for (const child of block.children) {
|
|
117
|
+
if (endBlock && isPortableTextTextBlock(endBlock)) {
|
|
118
|
+
if (child._key === endChildKey && isPortableTextSpan(child)) {
|
|
119
|
+
endBlock.children.push({
|
|
120
|
+
...child,
|
|
121
|
+
text: child.text.slice(0, endPoint.offset),
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
break
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
endBlock.children.push(child)
|
|
128
|
+
|
|
129
|
+
if (endChildKey && child._key === endChildKey) {
|
|
130
|
+
break
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
break
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
endBlock = block
|
|
139
|
+
|
|
140
|
+
break
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (startBlock) {
|
|
144
|
+
middleBlocks.push(block)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return [
|
|
149
|
+
...(startBlock ? [startBlock] : []),
|
|
150
|
+
...middleBlocks,
|
|
151
|
+
...(endBlock ? [endBlock] : []),
|
|
152
|
+
]
|
|
153
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"util.get-block-start-point.cjs","sources":["../../src/utils/util.reverse-selection.ts","../../src/utils/util.get-block-start-point.ts"],"sourcesContent":["import type {EditorSelection} from '../types/editor'\n\n/**\n * @public\n */\nexport function reverseSelection(\n selection: NonNullable<EditorSelection>,\n): NonNullable<EditorSelection> {\n if (selection.backward) {\n return {\n anchor: selection.focus,\n focus: selection.anchor,\n backward: false,\n }\n }\n\n return {\n anchor: selection.focus,\n focus: selection.anchor,\n backward: true,\n }\n}\n","import {\n isPortableTextTextBlock,\n type KeyedSegment,\n type PortableTextBlock,\n} from '@sanity/types'\nimport type {EditorSelectionPoint} from '../types/editor'\n\n/**\n * @public\n */\nexport function getBlockStartPoint({\n node,\n path,\n}: {\n node: PortableTextBlock\n path: [KeyedSegment]\n}): EditorSelectionPoint {\n if (isPortableTextTextBlock(node)) {\n return {\n path: [...path, 'children', {_key: node.children[0]._key}],\n offset: 0,\n }\n }\n\n return {\n path,\n offset: 0,\n }\n}\n"],"names":["reverseSelection","selection","backward","anchor","focus","getBlockStartPoint","node","path","isPortableTextTextBlock","_key","children","offset"],"mappings":";;AAKO,SAASA,iBACdC,WAC8B;AAC9B,SAAIA,UAAUC,WACL;AAAA,IACLC,QAAQF,UAAUG;AAAAA,IAClBA,OAAOH,UAAUE;AAAAA,IACjBD,UAAU;AAAA,EAAA,IAIP;AAAA,IACLC,QAAQF,UAAUG;AAAAA,IAClBA,OAAOH,UAAUE;AAAAA,IACjBD,UAAU;AAAA,EACZ;AACF;ACXO,SAASG,mBAAmB;AAAA,EACjCC;AAAAA,EACAC;AAIF,GAAyB;AACnBC,SAAAA,MAAAA,wBAAwBF,IAAI,IACvB;AAAA,IACLC,MAAM,CAAC,GAAGA,MAAM,YAAY;AAAA,MAACE,MAAMH,KAAKI,SAAS,CAAC,EAAED;AAAAA,IAAAA,CAAK;AAAA,IACzDE,QAAQ;AAAA,EAAA,IAIL;AAAA,IACLJ;AAAAA,IACAI,QAAQ;AAAA,EACV;AACF;;;"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"util.get-block-start-point.js","sources":["../../src/utils/util.reverse-selection.ts","../../src/utils/util.get-block-start-point.ts"],"sourcesContent":["import type {EditorSelection} from '../types/editor'\n\n/**\n * @public\n */\nexport function reverseSelection(\n selection: NonNullable<EditorSelection>,\n): NonNullable<EditorSelection> {\n if (selection.backward) {\n return {\n anchor: selection.focus,\n focus: selection.anchor,\n backward: false,\n }\n }\n\n return {\n anchor: selection.focus,\n focus: selection.anchor,\n backward: true,\n }\n}\n","import {\n isPortableTextTextBlock,\n type KeyedSegment,\n type PortableTextBlock,\n} from '@sanity/types'\nimport type {EditorSelectionPoint} from '../types/editor'\n\n/**\n * @public\n */\nexport function getBlockStartPoint({\n node,\n path,\n}: {\n node: PortableTextBlock\n path: [KeyedSegment]\n}): EditorSelectionPoint {\n if (isPortableTextTextBlock(node)) {\n return {\n path: [...path, 'children', {_key: node.children[0]._key}],\n offset: 0,\n }\n }\n\n return {\n path,\n offset: 0,\n }\n}\n"],"names":["reverseSelection","selection","backward","anchor","focus","getBlockStartPoint","node","path","isPortableTextTextBlock","_key","children","offset"],"mappings":";AAKO,SAASA,iBACdC,WAC8B;AAC9B,SAAIA,UAAUC,WACL;AAAA,IACLC,QAAQF,UAAUG;AAAAA,IAClBA,OAAOH,UAAUE;AAAAA,IACjBD,UAAU;AAAA,EAAA,IAIP;AAAA,IACLC,QAAQF,UAAUG;AAAAA,IAClBA,OAAOH,UAAUE;AAAAA,IACjBD,UAAU;AAAA,EACZ;AACF;ACXO,SAASG,mBAAmB;AAAA,EACjCC;AAAAA,EACAC;AAIF,GAAyB;AACnBC,SAAAA,wBAAwBF,IAAI,IACvB;AAAA,IACLC,MAAM,CAAC,GAAGA,MAAM,YAAY;AAAA,MAACE,MAAMH,KAAKI,SAAS,CAAC,EAAED;AAAAA,IAAAA,CAAK;AAAA,IACzDE,QAAQ;AAAA,EAAA,IAIL;AAAA,IACLJ;AAAAA,IACAI,QAAQ;AAAA,EACV;AACF;"}
|