@portabletext/editor 1.28.0 → 1.30.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 +40 -37
- package/lib/_chunks-cjs/behavior.core.cjs.map +1 -1
- package/lib/_chunks-cjs/parse-blocks.cjs +79 -0
- package/lib/_chunks-cjs/parse-blocks.cjs.map +1 -0
- package/lib/_chunks-cjs/plugin.event-listener.cjs +55 -97
- package/lib/_chunks-cjs/plugin.event-listener.cjs.map +1 -1
- package/lib/_chunks-cjs/selector.get-selection-start-point.cjs +15 -0
- package/lib/_chunks-cjs/selector.get-selection-start-point.cjs.map +1 -0
- package/lib/_chunks-es/behavior.core.js +40 -37
- package/lib/_chunks-es/behavior.core.js.map +1 -1
- package/lib/_chunks-es/parse-blocks.js +80 -0
- package/lib/_chunks-es/parse-blocks.js.map +1 -0
- package/lib/_chunks-es/plugin.event-listener.js +57 -98
- package/lib/_chunks-es/plugin.event-listener.js.map +1 -1
- package/lib/_chunks-es/selector.get-selection-start-point.js +16 -0
- package/lib/_chunks-es/selector.get-selection-start-point.js.map +1 -0
- package/lib/behaviors/index.d.cts +196 -124
- package/lib/behaviors/index.d.ts +196 -124
- package/lib/index.d.cts +248 -0
- package/lib/index.d.ts +248 -0
- package/lib/plugins/index.cjs +249 -1
- package/lib/plugins/index.cjs.map +1 -1
- package/lib/plugins/index.d.cts +246 -1
- package/lib/plugins/index.d.ts +246 -1
- package/lib/plugins/index.js +257 -3
- package/lib/plugins/index.js.map +1 -1
- package/lib/selectors/index.cjs +28 -1
- package/lib/selectors/index.cjs.map +1 -1
- package/lib/selectors/index.d.cts +21 -0
- package/lib/selectors/index.d.ts +21 -0
- package/lib/selectors/index.js +28 -0
- package/lib/selectors/index.js.map +1 -1
- package/lib/utils/index.cjs +70 -1
- package/lib/utils/index.cjs.map +1 -1
- package/lib/utils/index.d.cts +168 -2
- package/lib/utils/index.d.ts +168 -2
- package/lib/utils/index.js +71 -1
- package/lib/utils/index.js.map +1 -1
- package/package.json +2 -2
- package/src/behavior-actions/behavior.action.delete.ts +18 -0
- package/src/behavior-actions/behavior.action.insert-break.ts +3 -8
- package/src/behavior-actions/behavior.actions.ts +9 -0
- package/src/behaviors/_exports/index.ts +1 -0
- package/src/behaviors/behavior.core.deserialize.ts +52 -38
- package/src/behaviors/behavior.core.ts +4 -11
- package/src/behaviors/behavior.types.ts +4 -0
- package/src/editor/PortableTextEditor.tsx +20 -0
- package/src/internal-utils/__tests__/patchToOperations.test.ts +19 -21
- package/src/internal-utils/applyPatch.ts +11 -3
- package/src/plugins/index.ts +2 -0
- package/src/plugins/plugin.behavior.tsx +22 -0
- package/src/plugins/plugin.one-line.tsx +225 -0
- package/src/selectors/index.ts +3 -0
- package/src/selectors/selector.get-selection-end-point.ts +17 -0
- package/src/selectors/selector.get-selection-start-point.ts +17 -0
- package/src/selectors/selector.is-overlapping-selection.ts +46 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/util.is-span.ts +12 -0
- package/src/utils/util.is-text-block.ts +12 -0
- package/src/utils/util.merge-text-blocks.ts +36 -0
- package/src/utils/util.split-text-block.ts +55 -0
|
@@ -1,46 +1,60 @@
|
|
|
1
1
|
import {defineBehavior, raise} from './behavior.types'
|
|
2
2
|
|
|
3
|
-
export const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
3
|
+
export const coreDeserializeBehaviors = {
|
|
4
|
+
'deserialize': defineBehavior({
|
|
5
|
+
on: 'deserialize',
|
|
6
|
+
guard: ({context, event}) => {
|
|
7
|
+
const deserializeEvents = context.converters.flatMap((converter) => {
|
|
8
|
+
const data = event.dataTransfer.getData(converter.mimeType)
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
if (!data) {
|
|
11
|
+
return []
|
|
12
|
+
}
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
return [
|
|
15
|
+
converter.deserialize({context, event: {type: 'deserialize', data}}),
|
|
16
|
+
]
|
|
17
|
+
})
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
const firstSuccess = deserializeEvents.find(
|
|
20
|
+
(deserializeEvent) =>
|
|
21
|
+
deserializeEvent.type === 'deserialization.success',
|
|
22
|
+
)
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
24
|
+
if (!firstSuccess) {
|
|
25
|
+
return {
|
|
26
|
+
type: 'deserialization.failure',
|
|
27
|
+
mimeType: '*/*',
|
|
28
|
+
reason: deserializeEvents
|
|
29
|
+
.map((deserializeEvent) =>
|
|
30
|
+
deserializeEvent.type === 'deserialization.failure'
|
|
31
|
+
? deserializeEvent.reason
|
|
32
|
+
: '',
|
|
33
|
+
)
|
|
34
|
+
.join(', '),
|
|
35
|
+
} as const
|
|
36
|
+
}
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
return firstSuccess
|
|
39
|
+
},
|
|
40
|
+
actions: [
|
|
41
|
+
({event}, deserializeEvent) => [
|
|
42
|
+
raise({
|
|
43
|
+
...deserializeEvent,
|
|
44
|
+
dataTransfer: event.dataTransfer,
|
|
45
|
+
}),
|
|
46
|
+
],
|
|
47
|
+
],
|
|
48
|
+
}),
|
|
49
|
+
'deserialization.success': defineBehavior({
|
|
50
|
+
on: 'deserialization.success',
|
|
51
|
+
actions: [
|
|
52
|
+
({event}) => [
|
|
53
|
+
raise({
|
|
54
|
+
type: 'insert.blocks',
|
|
55
|
+
blocks: event.data,
|
|
56
|
+
}),
|
|
57
|
+
],
|
|
44
58
|
],
|
|
45
|
-
|
|
46
|
-
}
|
|
59
|
+
}),
|
|
60
|
+
}
|
|
@@ -1,23 +1,16 @@
|
|
|
1
1
|
import {coreAnnotationBehaviors} from './behavior.core.annotations'
|
|
2
2
|
import {coreBlockObjectBehaviors} from './behavior.core.block-objects'
|
|
3
3
|
import {coreDecoratorBehaviors} from './behavior.core.decorators'
|
|
4
|
-
import {
|
|
4
|
+
import {coreDeserializeBehaviors} from './behavior.core.deserialize'
|
|
5
5
|
import {coreInsertBreakBehaviors} from './behavior.core.insert-break'
|
|
6
6
|
import {coreListBehaviors} from './behavior.core.lists'
|
|
7
7
|
import {coreSerializeBehaviors} from './behavior.core.serialize'
|
|
8
8
|
import {coreStyleBehaviors} from './behavior.core.style'
|
|
9
|
-
import {defineBehavior, raise} from './behavior.types'
|
|
10
|
-
|
|
11
|
-
const softReturn = defineBehavior({
|
|
12
|
-
on: 'insert.soft break',
|
|
13
|
-
actions: [() => [raise({type: 'insert.text', text: '\n'})]],
|
|
14
|
-
})
|
|
15
9
|
|
|
16
10
|
/**
|
|
17
11
|
* @beta
|
|
18
12
|
*/
|
|
19
13
|
export const coreBehaviors = [
|
|
20
|
-
softReturn,
|
|
21
14
|
coreAnnotationBehaviors.toggleAnnotationOff,
|
|
22
15
|
coreAnnotationBehaviors.toggleAnnotationOn,
|
|
23
16
|
coreDecoratorBehaviors.toggleDecoratorOff,
|
|
@@ -26,7 +19,8 @@ export const coreBehaviors = [
|
|
|
26
19
|
coreDecoratorBehaviors.emShortcut,
|
|
27
20
|
coreDecoratorBehaviors.underlineShortcut,
|
|
28
21
|
coreDecoratorBehaviors.codeShortcut,
|
|
29
|
-
|
|
22
|
+
coreDeserializeBehaviors.deserialize,
|
|
23
|
+
coreDeserializeBehaviors['deserialization.success'],
|
|
30
24
|
coreBlockObjectBehaviors.arrowDownOnLonelyBlockObject,
|
|
31
25
|
coreBlockObjectBehaviors.arrowUpOnLonelyBlockObject,
|
|
32
26
|
coreBlockObjectBehaviors.breakingBlockObject,
|
|
@@ -51,10 +45,9 @@ export const coreBehaviors = [
|
|
|
51
45
|
* @beta
|
|
52
46
|
*/
|
|
53
47
|
export const coreBehavior = {
|
|
54
|
-
softReturn,
|
|
55
48
|
annotation: coreAnnotationBehaviors,
|
|
56
49
|
decorators: coreDecoratorBehaviors,
|
|
57
|
-
deserialize:
|
|
50
|
+
deserialize: coreDeserializeBehaviors,
|
|
58
51
|
blockObjects: coreBlockObjectBehaviors,
|
|
59
52
|
insertBreak: coreInsertBreakBehaviors,
|
|
60
53
|
lists: coreListBehaviors,
|
|
@@ -671,6 +671,16 @@ export class PortableTextEditor extends Component<
|
|
|
671
671
|
editor.editable?.toggleMark(mark)
|
|
672
672
|
}
|
|
673
673
|
|
|
674
|
+
/**
|
|
675
|
+
* @deprecated
|
|
676
|
+
* Use built-in selectors or write your own: https://www.portabletext.org/reference/selectors/
|
|
677
|
+
*
|
|
678
|
+
* ```
|
|
679
|
+
* import * as selectors from '@portabletext/editor/selectors'
|
|
680
|
+
* const editor = useEditor()
|
|
681
|
+
* const selectedSlice = useEditorSelector(editor, selectors.getSelectedSlice)
|
|
682
|
+
* ```
|
|
683
|
+
*/
|
|
674
684
|
static getFragment = (
|
|
675
685
|
editor: PortableTextEditor,
|
|
676
686
|
): PortableTextBlock[] | undefined => {
|
|
@@ -688,6 +698,16 @@ export class PortableTextEditor extends Component<
|
|
|
688
698
|
editor.editable?.redo()
|
|
689
699
|
}
|
|
690
700
|
|
|
701
|
+
/**
|
|
702
|
+
* @deprecated
|
|
703
|
+
* Use built-in selectors or write your own: https://www.portabletext.org/reference/selectors/
|
|
704
|
+
*
|
|
705
|
+
* ```
|
|
706
|
+
* import * as selectors from '@portabletext/editor/selectors'
|
|
707
|
+
* const editor = useEditor()
|
|
708
|
+
* const isOverlapping = useEditorSelector(editor, selectors.isOverlappingSelection(selectionB))
|
|
709
|
+
* ```
|
|
710
|
+
*/
|
|
691
711
|
static isSelectionsOverlapping = (
|
|
692
712
|
editor: PortableTextEditor,
|
|
693
713
|
selectionA: EditorSelection,
|
|
@@ -75,29 +75,27 @@ describe('operationToPatches', () => {
|
|
|
75
75
|
patches.forEach((p) => {
|
|
76
76
|
patchToOperations(editor, p)
|
|
77
77
|
})
|
|
78
|
-
expect(editor.children).
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
"_type": "reference",
|
|
96
|
-
},
|
|
78
|
+
expect(editor.children).toEqual([
|
|
79
|
+
{
|
|
80
|
+
__inline: false,
|
|
81
|
+
_key: 'c01739b0d03b',
|
|
82
|
+
_type: 'image',
|
|
83
|
+
children: [
|
|
84
|
+
{
|
|
85
|
+
_key: VOID_CHILD_KEY,
|
|
86
|
+
_type: 'span',
|
|
87
|
+
marks: [],
|
|
88
|
+
text: '',
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
value: {
|
|
92
|
+
asset: {
|
|
93
|
+
_ref: 'image-b5681d9d0b2b6c922238e7c694500dd7c1349b19-256x256-jpg',
|
|
94
|
+
_type: 'reference',
|
|
97
95
|
},
|
|
98
96
|
},
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
},
|
|
98
|
+
])
|
|
101
99
|
})
|
|
102
100
|
it('will not create operations for insertion inside blocks', () => {
|
|
103
101
|
editor.children = [
|
|
@@ -298,9 +298,17 @@ function setPatch(editor: PortableTextSlateEditor, patch: SetPatch) {
|
|
|
298
298
|
})
|
|
299
299
|
}
|
|
300
300
|
} else if (block && 'value' in block) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
301
|
+
if (patch.path.length > 1 && patch.path[1] !== 'children') {
|
|
302
|
+
const newVal = applyAll(block.value, [
|
|
303
|
+
{
|
|
304
|
+
...patch,
|
|
305
|
+
path: patch.path.slice(1),
|
|
306
|
+
},
|
|
307
|
+
])
|
|
308
|
+
Transforms.setNodes(editor, {...block, value: newVal}, {at: blockPath})
|
|
309
|
+
} else {
|
|
310
|
+
return false
|
|
311
|
+
}
|
|
304
312
|
}
|
|
305
313
|
debugState(editor, 'after')
|
|
306
314
|
return true
|
package/src/plugins/index.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export {BehaviorPlugin} from './plugin.behavior'
|
|
1
2
|
export {EventListenerPlugin} from './plugin.event-listener'
|
|
2
3
|
export {EditorRefPlugin} from './plugin.editor-ref'
|
|
3
4
|
export {MarkdownPlugin, type MarkdownPluginConfig} from './plugin.markdown'
|
|
5
|
+
export {OneLinePlugin} from './plugin.one-line'
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {useEffect} from 'react'
|
|
2
|
+
import type {Behavior} from '../behaviors'
|
|
3
|
+
import {useEditor} from '../editor/editor-provider'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @beta
|
|
7
|
+
*/
|
|
8
|
+
export function BehaviorPlugin(props: {behaviors: Array<Behavior>}) {
|
|
9
|
+
const editor = useEditor()
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const unregisterBehaviors = props.behaviors.map((behavior) =>
|
|
13
|
+
editor.registerBehavior({behavior}),
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
return () => {
|
|
17
|
+
unregisterBehaviors.forEach((unregister) => unregister())
|
|
18
|
+
}
|
|
19
|
+
}, [editor, props.behaviors])
|
|
20
|
+
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import {defineBehavior, raise} from '../behaviors'
|
|
2
|
+
import * as selectors from '../selectors'
|
|
3
|
+
import * as utils from '../utils'
|
|
4
|
+
import {BehaviorPlugin} from './plugin.behavior'
|
|
5
|
+
|
|
6
|
+
const oneLineBehaviors = [
|
|
7
|
+
/**
|
|
8
|
+
* Hitting Enter on an expanded selection should just delete that selection
|
|
9
|
+
* without causing a line break.
|
|
10
|
+
*/
|
|
11
|
+
defineBehavior({
|
|
12
|
+
on: 'insert.break',
|
|
13
|
+
guard: ({context}) =>
|
|
14
|
+
context.selection && selectors.isSelectionExpanded({context})
|
|
15
|
+
? {selection: context.selection}
|
|
16
|
+
: false,
|
|
17
|
+
actions: [(_, {selection}) => [{type: 'delete', selection}]],
|
|
18
|
+
}),
|
|
19
|
+
/**
|
|
20
|
+
* All other cases of `insert.break` should be aborted.
|
|
21
|
+
*/
|
|
22
|
+
defineBehavior({
|
|
23
|
+
on: 'insert.break',
|
|
24
|
+
actions: [() => [{type: 'noop'}]],
|
|
25
|
+
}),
|
|
26
|
+
/**
|
|
27
|
+
* `insert.block` `before` or `after` is not allowed in a one-line editor.
|
|
28
|
+
*/
|
|
29
|
+
defineBehavior({
|
|
30
|
+
on: 'insert.block',
|
|
31
|
+
guard: ({event}) =>
|
|
32
|
+
event.placement === 'before' || event.placement === 'after',
|
|
33
|
+
actions: [() => [{type: 'noop'}]],
|
|
34
|
+
}),
|
|
35
|
+
/**
|
|
36
|
+
* Other cases of `insert.block` are allowed.
|
|
37
|
+
*
|
|
38
|
+
* If a text block is inserted and the focus block is fully selected, then
|
|
39
|
+
* the focus block can be replaced with the inserted block.
|
|
40
|
+
*/
|
|
41
|
+
defineBehavior({
|
|
42
|
+
on: 'insert.block',
|
|
43
|
+
guard: ({context, event}) => {
|
|
44
|
+
const focusTextBlock = selectors.getFocusTextBlock({context})
|
|
45
|
+
const selectionStartPoint = selectors.getSelectionStartPoint({context})
|
|
46
|
+
const selectionEndPoint = selectors.getSelectionEndPoint({context})
|
|
47
|
+
|
|
48
|
+
if (
|
|
49
|
+
!focusTextBlock ||
|
|
50
|
+
!utils.isTextBlock(context, event.block) ||
|
|
51
|
+
!selectionStartPoint ||
|
|
52
|
+
!selectionEndPoint
|
|
53
|
+
) {
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const blockStartPoint = utils.getBlockStartPoint(focusTextBlock)
|
|
58
|
+
const blockEndPoint = utils.getBlockEndPoint(focusTextBlock)
|
|
59
|
+
const newFocus = utils.getBlockEndPoint({
|
|
60
|
+
node: event.block,
|
|
61
|
+
path: [{_key: event.block._key}],
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
if (
|
|
65
|
+
utils.isEqualSelectionPoints(blockStartPoint, selectionStartPoint) &&
|
|
66
|
+
utils.isEqualSelectionPoints(blockEndPoint, selectionEndPoint)
|
|
67
|
+
) {
|
|
68
|
+
return {focusTextBlock, newFocus}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return false
|
|
72
|
+
},
|
|
73
|
+
actions: [
|
|
74
|
+
({event}, {focusTextBlock, newFocus}) => [
|
|
75
|
+
{type: 'delete.block', blockPath: focusTextBlock.path},
|
|
76
|
+
{type: 'insert.block', block: event.block, placement: 'auto'},
|
|
77
|
+
{
|
|
78
|
+
type: 'select',
|
|
79
|
+
selection: {
|
|
80
|
+
anchor: newFocus,
|
|
81
|
+
focus: newFocus,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
],
|
|
86
|
+
}),
|
|
87
|
+
/**
|
|
88
|
+
* An ordinary `insert.block` is acceptable if it's a text block. In that
|
|
89
|
+
* case it will get merged into the existing text block.
|
|
90
|
+
*/
|
|
91
|
+
defineBehavior({
|
|
92
|
+
on: 'insert.block',
|
|
93
|
+
guard: ({context, event}) => {
|
|
94
|
+
const focusTextBlock = selectors.getFocusTextBlock({context})
|
|
95
|
+
const selectionStartPoint = selectors.getSelectionStartPoint({context})
|
|
96
|
+
const selectionEndPoint = selectors.getSelectionEndPoint({context})
|
|
97
|
+
|
|
98
|
+
if (
|
|
99
|
+
!focusTextBlock ||
|
|
100
|
+
!utils.isTextBlock(context, event.block) ||
|
|
101
|
+
!selectionStartPoint ||
|
|
102
|
+
!selectionEndPoint
|
|
103
|
+
) {
|
|
104
|
+
return false
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const blockBeforeStartPoint = utils.splitTextBlock({
|
|
108
|
+
context,
|
|
109
|
+
block: focusTextBlock.node,
|
|
110
|
+
point: selectionStartPoint,
|
|
111
|
+
})?.before
|
|
112
|
+
const blockAfterEndPoint = utils.splitTextBlock({
|
|
113
|
+
context,
|
|
114
|
+
block: focusTextBlock.node,
|
|
115
|
+
point: selectionEndPoint,
|
|
116
|
+
})?.after
|
|
117
|
+
|
|
118
|
+
if (!blockBeforeStartPoint || !blockAfterEndPoint) {
|
|
119
|
+
return false
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const targetBlock = utils.mergeTextBlocks({
|
|
123
|
+
context,
|
|
124
|
+
targetBlock: blockBeforeStartPoint,
|
|
125
|
+
incomingBlock: event.block,
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
const newFocus = utils.getBlockEndPoint({
|
|
129
|
+
node: targetBlock,
|
|
130
|
+
path: [{_key: targetBlock._key}],
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
const mergedBlock = utils.mergeTextBlocks({
|
|
134
|
+
context,
|
|
135
|
+
targetBlock,
|
|
136
|
+
incomingBlock: blockAfterEndPoint,
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
return {focusTextBlock, mergedBlock, newFocus}
|
|
140
|
+
},
|
|
141
|
+
actions: [
|
|
142
|
+
(_, {focusTextBlock, mergedBlock, newFocus}) => [
|
|
143
|
+
{type: 'delete.block', blockPath: focusTextBlock.path},
|
|
144
|
+
{type: 'insert.block', block: mergedBlock, placement: 'auto'},
|
|
145
|
+
{
|
|
146
|
+
type: 'select',
|
|
147
|
+
selection: {
|
|
148
|
+
anchor: newFocus,
|
|
149
|
+
focus: newFocus,
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
],
|
|
154
|
+
}),
|
|
155
|
+
/**
|
|
156
|
+
* Fallback Behavior to avoid `insert.block` in case the Behaviors above all
|
|
157
|
+
* end up with a falsy guard.
|
|
158
|
+
*/
|
|
159
|
+
defineBehavior({
|
|
160
|
+
on: 'insert.block',
|
|
161
|
+
actions: [() => [{type: 'noop'}]],
|
|
162
|
+
}),
|
|
163
|
+
/**
|
|
164
|
+
* If multiple blocks are inserted, then the non-text blocks are filtered out
|
|
165
|
+
* and the text blocks are merged into one block
|
|
166
|
+
*/
|
|
167
|
+
defineBehavior({
|
|
168
|
+
on: 'insert.blocks',
|
|
169
|
+
guard: ({context, event}) => {
|
|
170
|
+
return event.blocks
|
|
171
|
+
.filter((block) => utils.isTextBlock(context, block))
|
|
172
|
+
.reduce((targetBlock, incomingBlock) => {
|
|
173
|
+
return utils.mergeTextBlocks({
|
|
174
|
+
context,
|
|
175
|
+
targetBlock,
|
|
176
|
+
incomingBlock,
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
},
|
|
180
|
+
actions: [
|
|
181
|
+
// `insert.block` is raised so the Behavior above can handle the
|
|
182
|
+
// insertion
|
|
183
|
+
(_, block) => [raise({type: 'insert.block', block, placement: 'auto'})],
|
|
184
|
+
],
|
|
185
|
+
}),
|
|
186
|
+
/**
|
|
187
|
+
* Block objects do not fit in a one-line editor
|
|
188
|
+
*/
|
|
189
|
+
defineBehavior({
|
|
190
|
+
on: 'insert.block object',
|
|
191
|
+
actions: [() => [{type: 'noop'}]],
|
|
192
|
+
}),
|
|
193
|
+
/**
|
|
194
|
+
* `insert.text block` is raised as an `insert.block` so it can be handled
|
|
195
|
+
* by the Behaviors above.
|
|
196
|
+
*/
|
|
197
|
+
defineBehavior({
|
|
198
|
+
on: 'insert.text block',
|
|
199
|
+
actions: [
|
|
200
|
+
({context, event}) => [
|
|
201
|
+
raise({
|
|
202
|
+
type: 'insert.block',
|
|
203
|
+
block: {
|
|
204
|
+
_key: context.keyGenerator(),
|
|
205
|
+
_type: context.schema.block.name,
|
|
206
|
+
children: event.textBlock?.children ?? [],
|
|
207
|
+
},
|
|
208
|
+
placement: event.placement,
|
|
209
|
+
}),
|
|
210
|
+
],
|
|
211
|
+
],
|
|
212
|
+
}),
|
|
213
|
+
]
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* @beta
|
|
217
|
+
* Restrict the editor to one line. The plugin takes care of blocking
|
|
218
|
+
* `insert.break` events and smart handling of other `insert.*` events.
|
|
219
|
+
*
|
|
220
|
+
* Place it with as high priority as possible to make sure other plugins don't
|
|
221
|
+
* overwrite `insert.*` events before this plugin gets a chance to do so.
|
|
222
|
+
*/
|
|
223
|
+
export function OneLinePlugin() {
|
|
224
|
+
return <BehaviorPlugin behaviors={oneLineBehaviors} />
|
|
225
|
+
}
|
package/src/selectors/index.ts
CHANGED
|
@@ -12,6 +12,8 @@ export {getActiveStyle} from './selector.get-active-style'
|
|
|
12
12
|
export {getSelectedSlice} from './selector.get-selected-slice'
|
|
13
13
|
export {getSelectedSpans} from './selector.get-selected-spans'
|
|
14
14
|
export {getSelection} from './selector.get-selection'
|
|
15
|
+
export {getSelectionEndPoint} from './selector.get-selection-end-point'
|
|
16
|
+
export {getSelectionStartPoint} from './selector.get-selection-start-point'
|
|
15
17
|
export {getSelectionText} from './selector.get-selection-text'
|
|
16
18
|
export {getBlockTextBefore} from './selector.get-text-before'
|
|
17
19
|
export {getValue} from './selector.get-value'
|
|
@@ -21,6 +23,7 @@ export {isActiveListItem} from './selector.is-active-list-item'
|
|
|
21
23
|
export {isActiveStyle} from './selector.is-active-style'
|
|
22
24
|
export {isAtTheEndOfBlock} from './selector.is-at-the-end-of-block'
|
|
23
25
|
export {isAtTheStartOfBlock} from './selector.is-at-the-start-of-block'
|
|
26
|
+
export {isOverlappingSelection} from './selector.is-overlapping-selection'
|
|
24
27
|
export {isPointAfterSelection} from './selector.is-point-after-selection'
|
|
25
28
|
export {isPointBeforeSelection} from './selector.is-point-before-selection'
|
|
26
29
|
export {isSelectionCollapsed} from './selector.is-selection-collapsed'
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type {EditorSelector} from '../editor/editor-selector'
|
|
2
|
+
import type {EditorSelectionPoint} from '../utils'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @public
|
|
6
|
+
*/
|
|
7
|
+
export const getSelectionEndPoint: EditorSelector<
|
|
8
|
+
EditorSelectionPoint | undefined
|
|
9
|
+
> = ({context}) => {
|
|
10
|
+
if (!context.selection) {
|
|
11
|
+
return undefined
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return context.selection.backward
|
|
15
|
+
? context.selection.anchor
|
|
16
|
+
: context.selection.focus
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type {EditorSelector} from '../editor/editor-selector'
|
|
2
|
+
import type {EditorSelectionPoint} from '../utils'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @public
|
|
6
|
+
*/
|
|
7
|
+
export const getSelectionStartPoint: EditorSelector<
|
|
8
|
+
EditorSelectionPoint | undefined
|
|
9
|
+
> = ({context}) => {
|
|
10
|
+
if (!context.selection) {
|
|
11
|
+
return undefined
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return context.selection.backward
|
|
15
|
+
? context.selection.focus
|
|
16
|
+
: context.selection.anchor
|
|
17
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type {EditorSelection} from '../types/editor'
|
|
2
|
+
import type {EditorSelector} from './../editor/editor-selector'
|
|
3
|
+
import {getSelectionEndPoint} from './selector.get-selection-end-point'
|
|
4
|
+
import {getSelectionStartPoint} from './selector.get-selection-start-point'
|
|
5
|
+
import {isPointAfterSelection} from './selector.is-point-after-selection'
|
|
6
|
+
import {isPointBeforeSelection} from './selector.is-point-before-selection'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @public
|
|
10
|
+
*/
|
|
11
|
+
export function isOverlappingSelection(
|
|
12
|
+
selection: EditorSelection,
|
|
13
|
+
): EditorSelector<boolean> {
|
|
14
|
+
return ({context}) => {
|
|
15
|
+
if (!selection || !context.selection) {
|
|
16
|
+
return false
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const selectionStartPoint = getSelectionStartPoint({
|
|
20
|
+
context: {
|
|
21
|
+
...context,
|
|
22
|
+
selection,
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
const selectionEndPoint = getSelectionEndPoint({
|
|
26
|
+
context: {
|
|
27
|
+
...context,
|
|
28
|
+
selection,
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
if (!selectionStartPoint || !selectionEndPoint) {
|
|
33
|
+
return false
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!isPointAfterSelection(selectionStartPoint)({context})) {
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!isPointBeforeSelection(selectionEndPoint)({context})) {
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return true
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -10,5 +10,9 @@ export {getTextBlockText} from './util.get-text-block-text'
|
|
|
10
10
|
export {isEmptyTextBlock} from './util.is-empty-text-block'
|
|
11
11
|
export {isEqualSelectionPoints} from './util.is-equal-selection-points'
|
|
12
12
|
export {isKeyedSegment} from './util.is-keyed-segment'
|
|
13
|
+
export {isSpan} from './util.is-span'
|
|
14
|
+
export {isTextBlock} from './util.is-text-block'
|
|
15
|
+
export {mergeTextBlocks} from './util.merge-text-blocks'
|
|
13
16
|
export {reverseSelection} from './util.reverse-selection'
|
|
14
17
|
export {sliceBlocks} from './util.slice-blocks'
|
|
18
|
+
export {splitTextBlock} from './util.split-text-block'
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type {PortableTextChild, PortableTextSpan} from '@sanity/types'
|
|
2
|
+
import type {EditorContext} from '../selectors'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @public
|
|
6
|
+
*/
|
|
7
|
+
export function isSpan(
|
|
8
|
+
context: Pick<EditorContext, 'schema'>,
|
|
9
|
+
child: PortableTextChild,
|
|
10
|
+
): child is PortableTextSpan {
|
|
11
|
+
return child._type === context.schema.span.name
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type {PortableTextBlock, PortableTextTextBlock} from '@sanity/types'
|
|
2
|
+
import type {EditorContext} from '../selectors'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @public
|
|
6
|
+
*/
|
|
7
|
+
export function isTextBlock(
|
|
8
|
+
context: Pick<EditorContext, 'schema'>,
|
|
9
|
+
block: PortableTextBlock,
|
|
10
|
+
): block is PortableTextTextBlock {
|
|
11
|
+
return block._type === context.schema.block.name
|
|
12
|
+
}
|