@portabletext/editor 1.12.2 → 1.13.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/index.d.mts +234 -14
- package/lib/index.d.ts +234 -14
- package/lib/index.esm.js +361 -81
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +364 -83
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +361 -81
- package/lib/index.mjs.map +1 -1
- package/package.json +10 -7
- package/src/editor/Editable.tsx +60 -5
- package/src/editor/behavior/behavior.action.insert-break.ts +21 -24
- package/src/editor/behavior/behavior.actions.ts +125 -3
- package/src/editor/behavior/behavior.code-editor.ts +86 -0
- package/src/editor/behavior/behavior.core.block-objects.ts +34 -1
- package/src/editor/behavior/behavior.core.ts +2 -0
- package/src/editor/behavior/behavior.links.ts +2 -2
- package/src/editor/behavior/behavior.markdown.ts +1 -1
- package/src/editor/behavior/behavior.types.ts +37 -2
- package/src/editor/behavior/behavior.utils.ts +2 -2
- package/src/editor/editor-machine.ts +5 -2
- package/src/editor/plugins/createWithHotKeys.ts +2 -49
- package/src/index.ts +4 -0
- package/src/utils/is-hotkey.test.ts +112 -0
- package/src/utils/is-hotkey.ts +209 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@portabletext/editor",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.0",
|
|
4
4
|
"description": "Portable Text Editor made in React",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sanity",
|
|
@@ -46,7 +46,6 @@
|
|
|
46
46
|
"@xstate/react": "^5.0.0",
|
|
47
47
|
"debug": "^4.3.4",
|
|
48
48
|
"get-random-values-esm": "^1.0.2",
|
|
49
|
-
"is-hotkey-esm": "^1.0.0",
|
|
50
49
|
"lodash": "^4.17.21",
|
|
51
50
|
"lodash.startcase": "^4.4.0",
|
|
52
51
|
"react-compiler-runtime": "19.0.0-beta-df7b47d-20241124",
|
|
@@ -60,7 +59,7 @@
|
|
|
60
59
|
"@portabletext/toolkit": "^2.0.16",
|
|
61
60
|
"@sanity/block-tools": "^3.65.1",
|
|
62
61
|
"@sanity/diff-match-patch": "^3.1.1",
|
|
63
|
-
"@sanity/pkg-utils": "^6.11.
|
|
62
|
+
"@sanity/pkg-utils": "^6.11.14",
|
|
64
63
|
"@sanity/schema": "^3.65.1",
|
|
65
64
|
"@sanity/types": "^3.65.1",
|
|
66
65
|
"@testing-library/jest-dom": "^6.6.3",
|
|
@@ -70,8 +69,8 @@
|
|
|
70
69
|
"@types/lodash.startcase": "^4.4.9",
|
|
71
70
|
"@types/react": "^18.3.12",
|
|
72
71
|
"@types/react-dom": "^18.3.1",
|
|
73
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
74
|
-
"@typescript-eslint/parser": "^8.
|
|
72
|
+
"@typescript-eslint/eslint-plugin": "^8.16.0",
|
|
73
|
+
"@typescript-eslint/parser": "^8.16.0",
|
|
75
74
|
"@vitejs/plugin-react": "^4.3.4",
|
|
76
75
|
"@vitest/browser": "^2.1.5",
|
|
77
76
|
"babel-plugin-react-compiler": "19.0.0-beta-df7b47d-20241124",
|
|
@@ -87,7 +86,7 @@
|
|
|
87
86
|
"vite": "^5.4.11",
|
|
88
87
|
"vitest": "^2.1.5",
|
|
89
88
|
"vitest-browser-react": "^0.0.4",
|
|
90
|
-
"racejar": "1.0
|
|
89
|
+
"racejar": "1.1.0"
|
|
91
90
|
},
|
|
92
91
|
"peerDependencies": {
|
|
93
92
|
"@sanity/block-tools": "^3.65.1",
|
|
@@ -112,6 +111,10 @@
|
|
|
112
111
|
"dev": "pkg-utils watch",
|
|
113
112
|
"lint:fix": "biome lint --write .",
|
|
114
113
|
"test": "vitest --run",
|
|
115
|
-
"test:watch": "vitest"
|
|
114
|
+
"test:watch": "vitest",
|
|
115
|
+
"test:chromium": "vitest --run --project chromium",
|
|
116
|
+
"test:chromium:watch": "vitest --project chromium",
|
|
117
|
+
"test:unit": "vitest --run --project unit",
|
|
118
|
+
"test:unit:watch": "vitest --project unit"
|
|
116
119
|
}
|
|
117
120
|
}
|
package/src/editor/Editable.tsx
CHANGED
|
@@ -420,9 +420,19 @@ export const PortableTextEditable = forwardRef<
|
|
|
420
420
|
if (result !== undefined) {
|
|
421
421
|
event.preventDefault()
|
|
422
422
|
}
|
|
423
|
+
} else if (event.nativeEvent.clipboardData) {
|
|
424
|
+
editorActor.send({
|
|
425
|
+
type: 'behavior event',
|
|
426
|
+
behaviorEvent: {
|
|
427
|
+
type: 'copy',
|
|
428
|
+
data: event.nativeEvent.clipboardData,
|
|
429
|
+
},
|
|
430
|
+
editor: slateEditor,
|
|
431
|
+
nativeEvent: event,
|
|
432
|
+
})
|
|
423
433
|
}
|
|
424
434
|
},
|
|
425
|
-
[onCopy],
|
|
435
|
+
[onCopy, editorActor, slateEditor],
|
|
426
436
|
)
|
|
427
437
|
|
|
428
438
|
// Handle incoming pasting events in the editor
|
|
@@ -473,15 +483,14 @@ export const PortableTextEditable = forwardRef<
|
|
|
473
483
|
editorActor.send({type: 'done loading'})
|
|
474
484
|
})
|
|
475
485
|
} else if (event.nativeEvent.clipboardData) {
|
|
476
|
-
event.preventDefault()
|
|
477
|
-
|
|
478
486
|
editorActor.send({
|
|
479
487
|
type: 'behavior event',
|
|
480
488
|
behaviorEvent: {
|
|
481
489
|
type: 'paste',
|
|
482
|
-
|
|
490
|
+
data: event.nativeEvent.clipboardData,
|
|
483
491
|
},
|
|
484
492
|
editor: slateEditor,
|
|
493
|
+
nativeEvent: event,
|
|
485
494
|
})
|
|
486
495
|
}
|
|
487
496
|
|
|
@@ -659,8 +668,53 @@ export const PortableTextEditable = forwardRef<
|
|
|
659
668
|
if (!event.isDefaultPrevented()) {
|
|
660
669
|
slateEditor.pteWithHotKeys(event)
|
|
661
670
|
}
|
|
671
|
+
if (!event.isDefaultPrevented()) {
|
|
672
|
+
editorActor.send({
|
|
673
|
+
type: 'behavior event',
|
|
674
|
+
behaviorEvent: {
|
|
675
|
+
type: 'key.down',
|
|
676
|
+
keyboardEvent: {
|
|
677
|
+
key: event.key,
|
|
678
|
+
code: event.code,
|
|
679
|
+
altKey: event.altKey,
|
|
680
|
+
ctrlKey: event.ctrlKey,
|
|
681
|
+
metaKey: event.metaKey,
|
|
682
|
+
shiftKey: event.shiftKey,
|
|
683
|
+
},
|
|
684
|
+
},
|
|
685
|
+
editor: slateEditor,
|
|
686
|
+
nativeEvent: event,
|
|
687
|
+
})
|
|
688
|
+
}
|
|
689
|
+
},
|
|
690
|
+
[props, editorActor, slateEditor],
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
const handleKeyUp = useCallback(
|
|
694
|
+
(event: KeyboardEvent<HTMLDivElement>) => {
|
|
695
|
+
if (props.onKeyUp) {
|
|
696
|
+
props.onKeyUp(event)
|
|
697
|
+
}
|
|
698
|
+
if (!event.isDefaultPrevented()) {
|
|
699
|
+
editorActor.send({
|
|
700
|
+
type: 'behavior event',
|
|
701
|
+
behaviorEvent: {
|
|
702
|
+
type: 'key.up',
|
|
703
|
+
keyboardEvent: {
|
|
704
|
+
key: event.key,
|
|
705
|
+
code: event.code,
|
|
706
|
+
altKey: event.altKey,
|
|
707
|
+
ctrlKey: event.ctrlKey,
|
|
708
|
+
metaKey: event.metaKey,
|
|
709
|
+
shiftKey: event.shiftKey,
|
|
710
|
+
},
|
|
711
|
+
},
|
|
712
|
+
editor: slateEditor,
|
|
713
|
+
nativeEvent: event,
|
|
714
|
+
})
|
|
715
|
+
}
|
|
662
716
|
},
|
|
663
|
-
[props, slateEditor],
|
|
717
|
+
[props, editorActor, slateEditor],
|
|
664
718
|
)
|
|
665
719
|
|
|
666
720
|
const scrollSelectionIntoViewToSlate = useMemo(() => {
|
|
@@ -753,6 +807,7 @@ export const PortableTextEditable = forwardRef<
|
|
|
753
807
|
onDOMBeforeInput={handleOnBeforeInput}
|
|
754
808
|
onFocus={handleOnFocus}
|
|
755
809
|
onKeyDown={handleKeyDown}
|
|
810
|
+
onKeyUp={handleKeyUp}
|
|
756
811
|
onPaste={handlePaste}
|
|
757
812
|
readOnly={readOnly}
|
|
758
813
|
// We have implemented our own placeholder logic with decorations.
|
|
@@ -23,15 +23,16 @@ export const insertBreakActionImplementation: BehaviorActionImplementation<
|
|
|
23
23
|
}),
|
|
24
24
|
)[0] ?? [undefined]
|
|
25
25
|
const focusDecorators =
|
|
26
|
-
focusSpan
|
|
26
|
+
focusSpan?.marks?.filter((mark) =>
|
|
27
27
|
schema.decorators.some((decorator) => decorator.value === mark),
|
|
28
28
|
) ?? []
|
|
29
29
|
const focusAnnotations =
|
|
30
|
-
focusSpan
|
|
30
|
+
focusSpan?.marks?.filter(
|
|
31
31
|
(mark) =>
|
|
32
32
|
!schema.decorators.some((decorator) => decorator.value === mark),
|
|
33
33
|
) ?? []
|
|
34
34
|
|
|
35
|
+
const anchorBlockPath = editor.selection.anchor.path.slice(0, 1)
|
|
35
36
|
const focusBlockPath = editor.selection.focus.path.slice(0, 1)
|
|
36
37
|
const focusBlock = Node.descendant(editor, focusBlockPath) as
|
|
37
38
|
| SlateTextBlock
|
|
@@ -39,45 +40,37 @@ export const insertBreakActionImplementation: BehaviorActionImplementation<
|
|
|
39
40
|
|
|
40
41
|
if (editor.isTextBlock(focusBlock)) {
|
|
41
42
|
const [start, end] = Range.edges(editor.selection)
|
|
43
|
+
const lastFocusBlockChild =
|
|
44
|
+
focusBlock.children[focusBlock.children.length - 1]
|
|
45
|
+
const atTheEndOfBlock = isEqual(start, {
|
|
46
|
+
path: [...focusBlockPath, focusBlock.children.length - 1],
|
|
47
|
+
offset: editor.isTextSpan(lastFocusBlockChild)
|
|
48
|
+
? lastFocusBlockChild.text.length
|
|
49
|
+
: 0,
|
|
50
|
+
})
|
|
42
51
|
const atTheStartOfBlock = isEqual(end, {
|
|
43
52
|
path: [...focusBlockPath, 0],
|
|
44
53
|
offset: 0,
|
|
45
54
|
})
|
|
46
55
|
|
|
47
|
-
if (
|
|
56
|
+
if (atTheEndOfBlock && Range.isCollapsed(editor.selection)) {
|
|
48
57
|
Editor.insertNode(
|
|
49
58
|
editor,
|
|
50
59
|
editor.pteCreateTextBlock({
|
|
51
|
-
decorators:
|
|
60
|
+
decorators: [],
|
|
52
61
|
listItem: focusBlock.listItem,
|
|
53
62
|
level: focusBlock.level,
|
|
54
63
|
}),
|
|
55
64
|
)
|
|
56
65
|
|
|
57
|
-
const [nextBlockPath] = Path.next(focusBlockPath)
|
|
58
|
-
|
|
59
|
-
Transforms.select(editor, {
|
|
60
|
-
anchor: {path: [nextBlockPath, 0], offset: 0},
|
|
61
|
-
focus: {path: [nextBlockPath, 0], offset: 0},
|
|
62
|
-
})
|
|
63
|
-
|
|
64
66
|
return
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
|
|
68
|
-
focusBlock.children[focusBlock.children.length - 1]
|
|
69
|
-
const atTheEndOfBlock = isEqual(start, {
|
|
70
|
-
path: [...focusBlockPath, focusBlock.children.length - 1],
|
|
71
|
-
offset: editor.isTextSpan(lastFocusBlockChild)
|
|
72
|
-
? lastFocusBlockChild.text.length
|
|
73
|
-
: 0,
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
if (atTheEndOfBlock && Range.isCollapsed(editor.selection)) {
|
|
69
|
+
if (atTheStartOfBlock && Range.isCollapsed(editor.selection)) {
|
|
77
70
|
Editor.insertNode(
|
|
78
71
|
editor,
|
|
79
72
|
editor.pteCreateTextBlock({
|
|
80
|
-
decorators: [],
|
|
73
|
+
decorators: focusAnnotations.length === 0 ? focusDecorators : [],
|
|
81
74
|
listItem: focusBlock.listItem,
|
|
82
75
|
level: focusBlock.level,
|
|
83
76
|
}),
|
|
@@ -85,7 +78,7 @@ export const insertBreakActionImplementation: BehaviorActionImplementation<
|
|
|
85
78
|
|
|
86
79
|
const [nextBlockPath] = Path.next(focusBlockPath)
|
|
87
80
|
|
|
88
|
-
Transforms.
|
|
81
|
+
Transforms.select(editor, {
|
|
89
82
|
anchor: {path: [nextBlockPath, 0], offset: 0},
|
|
90
83
|
focus: {path: [nextBlockPath, 0], offset: 0},
|
|
91
84
|
})
|
|
@@ -93,9 +86,11 @@ export const insertBreakActionImplementation: BehaviorActionImplementation<
|
|
|
93
86
|
return
|
|
94
87
|
}
|
|
95
88
|
|
|
89
|
+
const selectionAcrossBlocks = anchorBlockPath[0] !== focusBlockPath[0]
|
|
90
|
+
|
|
96
91
|
const isInTheMiddleOfNode = !atTheStartOfBlock && !atTheEndOfBlock
|
|
97
92
|
|
|
98
|
-
if (isInTheMiddleOfNode) {
|
|
93
|
+
if (isInTheMiddleOfNode && !selectionAcrossBlocks) {
|
|
99
94
|
Editor.withoutNormalizing(editor, () => {
|
|
100
95
|
if (!editor.selection) {
|
|
101
96
|
return
|
|
@@ -202,6 +197,8 @@ export const insertBreakActionImplementation: BehaviorActionImplementation<
|
|
|
202
197
|
return
|
|
203
198
|
}
|
|
204
199
|
}
|
|
200
|
+
|
|
201
|
+
Transforms.splitNodes(editor, {always: true})
|
|
205
202
|
}
|
|
206
203
|
|
|
207
204
|
export const insertSoftBreakActionImplementation: BehaviorActionImplementation<
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
deleteBackward,
|
|
3
|
+
deleteForward,
|
|
4
|
+
insertText,
|
|
5
|
+
Path,
|
|
6
|
+
Transforms,
|
|
7
|
+
} from 'slate'
|
|
2
8
|
import {ReactEditor} from 'slate-react'
|
|
3
9
|
import type {PortableTextMemberSchemaTypes} from '../../types/editor'
|
|
4
10
|
import {toSlateRange} from '../../utils/ranges'
|
|
@@ -86,6 +92,7 @@ const behaviorActionImplementations: BehaviorActionImplementations = {
|
|
|
86
92
|
Transforms.unsetNodes(action.editor, action.props, {at})
|
|
87
93
|
}
|
|
88
94
|
},
|
|
95
|
+
'copy': () => {},
|
|
89
96
|
'delete backward': ({action}) => {
|
|
90
97
|
deleteBackward(action.editor, action.unit)
|
|
91
98
|
},
|
|
@@ -188,9 +195,54 @@ const behaviorActionImplementations: BehaviorActionImplementations = {
|
|
|
188
195
|
'effect': ({action}) => {
|
|
189
196
|
action.effect()
|
|
190
197
|
},
|
|
191
|
-
'
|
|
192
|
-
|
|
198
|
+
'key.down': () => {},
|
|
199
|
+
'key.up': () => {},
|
|
200
|
+
'move block': ({action}) => {
|
|
201
|
+
const location = toSlateRange(
|
|
202
|
+
{
|
|
203
|
+
anchor: {
|
|
204
|
+
path: action.blockPath,
|
|
205
|
+
offset: 0,
|
|
206
|
+
},
|
|
207
|
+
focus: {
|
|
208
|
+
path: action.blockPath,
|
|
209
|
+
offset: 0,
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
action.editor,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
if (!location) {
|
|
216
|
+
console.error('Unable to find Slate range from selection points')
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const newLocation = toSlateRange(
|
|
221
|
+
{
|
|
222
|
+
anchor: {
|
|
223
|
+
path: action.to,
|
|
224
|
+
offset: 0,
|
|
225
|
+
},
|
|
226
|
+
focus: {
|
|
227
|
+
path: action.to,
|
|
228
|
+
offset: 0,
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
action.editor,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
if (!newLocation) {
|
|
235
|
+
console.error('Unable to find Slate range from selection points')
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
Transforms.moveNodes(action.editor, {
|
|
240
|
+
at: location,
|
|
241
|
+
to: newLocation.anchor.path.slice(0, 1),
|
|
242
|
+
mode: 'highest',
|
|
243
|
+
})
|
|
193
244
|
},
|
|
245
|
+
'paste': () => {},
|
|
194
246
|
'select': ({action}) => {
|
|
195
247
|
const newSelection = toSlateRange(action.selection, action.editor)
|
|
196
248
|
|
|
@@ -200,6 +252,34 @@ const behaviorActionImplementations: BehaviorActionImplementations = {
|
|
|
200
252
|
Transforms.deselect(action.editor)
|
|
201
253
|
}
|
|
202
254
|
},
|
|
255
|
+
'select previous block': ({action}) => {
|
|
256
|
+
if (!action.editor.selection) {
|
|
257
|
+
console.error('Unable to select previous block without a selection')
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const blockPath = action.editor.selection.focus.path.slice(0, 1)
|
|
262
|
+
|
|
263
|
+
if (!Path.hasPrevious(blockPath)) {
|
|
264
|
+
console.error("There's no previous block to select")
|
|
265
|
+
return
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const previousBlockPath = Path.previous(blockPath)
|
|
269
|
+
|
|
270
|
+
Transforms.select(action.editor, previousBlockPath)
|
|
271
|
+
},
|
|
272
|
+
'select next block': ({action}) => {
|
|
273
|
+
if (!action.editor.selection) {
|
|
274
|
+
console.error('Unable to select next block without a selection')
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const blockPath = action.editor.selection.focus.path.slice(0, 1)
|
|
279
|
+
const nextBlockPath = [blockPath[0] + 1]
|
|
280
|
+
|
|
281
|
+
Transforms.select(action.editor, nextBlockPath)
|
|
282
|
+
},
|
|
203
283
|
'reselect': ({action}) => {
|
|
204
284
|
const selection = action.editor.selection
|
|
205
285
|
|
|
@@ -253,6 +333,13 @@ export function performAction({
|
|
|
253
333
|
})
|
|
254
334
|
break
|
|
255
335
|
}
|
|
336
|
+
case 'move block': {
|
|
337
|
+
behaviorActionImplementations['move block']({
|
|
338
|
+
context,
|
|
339
|
+
action,
|
|
340
|
+
})
|
|
341
|
+
break
|
|
342
|
+
}
|
|
256
343
|
case 'set block': {
|
|
257
344
|
behaviorActionImplementations['set block']({
|
|
258
345
|
context,
|
|
@@ -281,6 +368,20 @@ export function performAction({
|
|
|
281
368
|
})
|
|
282
369
|
break
|
|
283
370
|
}
|
|
371
|
+
case 'select previous block': {
|
|
372
|
+
behaviorActionImplementations['select previous block']({
|
|
373
|
+
context,
|
|
374
|
+
action,
|
|
375
|
+
})
|
|
376
|
+
break
|
|
377
|
+
}
|
|
378
|
+
case 'select next block': {
|
|
379
|
+
behaviorActionImplementations['select next block']({
|
|
380
|
+
context,
|
|
381
|
+
action,
|
|
382
|
+
})
|
|
383
|
+
break
|
|
384
|
+
}
|
|
284
385
|
case 'reselect': {
|
|
285
386
|
behaviorActionImplementations.reselect({
|
|
286
387
|
context,
|
|
@@ -323,6 +424,13 @@ function performDefaultAction({
|
|
|
323
424
|
})
|
|
324
425
|
break
|
|
325
426
|
}
|
|
427
|
+
case 'copy': {
|
|
428
|
+
behaviorActionImplementations.copy({
|
|
429
|
+
context,
|
|
430
|
+
action,
|
|
431
|
+
})
|
|
432
|
+
break
|
|
433
|
+
}
|
|
326
434
|
case 'decorator.add': {
|
|
327
435
|
behaviorActionImplementations['decorator.add']({
|
|
328
436
|
context,
|
|
@@ -386,6 +494,20 @@ function performDefaultAction({
|
|
|
386
494
|
})
|
|
387
495
|
break
|
|
388
496
|
}
|
|
497
|
+
case 'key.down': {
|
|
498
|
+
behaviorActionImplementations['key.down']({
|
|
499
|
+
context,
|
|
500
|
+
action,
|
|
501
|
+
})
|
|
502
|
+
break
|
|
503
|
+
}
|
|
504
|
+
case 'key.up': {
|
|
505
|
+
behaviorActionImplementations['key.up']({
|
|
506
|
+
context,
|
|
507
|
+
action,
|
|
508
|
+
})
|
|
509
|
+
break
|
|
510
|
+
}
|
|
389
511
|
default: {
|
|
390
512
|
behaviorActionImplementations.paste({
|
|
391
513
|
context,
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {isHotkey} from '../../utils/is-hotkey'
|
|
2
|
+
import {defineBehavior} from './behavior.types'
|
|
3
|
+
import {
|
|
4
|
+
getFocusBlock,
|
|
5
|
+
getNextBlock,
|
|
6
|
+
getPreviousBlock,
|
|
7
|
+
selectionIsCollapsed,
|
|
8
|
+
} from './behavior.utils'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @alpha
|
|
12
|
+
*/
|
|
13
|
+
export type CodeEditorBehaviorsConfig = {
|
|
14
|
+
moveBlockUpShortcut: string
|
|
15
|
+
moveBlockDownShortcut: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @alpha
|
|
20
|
+
*/
|
|
21
|
+
export function createCodeEditorBehaviors(config: CodeEditorBehaviorsConfig) {
|
|
22
|
+
return [
|
|
23
|
+
defineBehavior({
|
|
24
|
+
on: 'key.down',
|
|
25
|
+
guard: ({context, event}) => {
|
|
26
|
+
const isAltArrowUp = isHotkey(
|
|
27
|
+
config.moveBlockUpShortcut,
|
|
28
|
+
event.keyboardEvent,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
if (!isAltArrowUp || !selectionIsCollapsed(context)) {
|
|
32
|
+
return false
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const focusBlock = getFocusBlock(context)
|
|
36
|
+
const previousBlock = getPreviousBlock(context)
|
|
37
|
+
|
|
38
|
+
if (focusBlock && previousBlock) {
|
|
39
|
+
return {focusBlock, previousBlock}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return false
|
|
43
|
+
},
|
|
44
|
+
actions: [
|
|
45
|
+
(_, {focusBlock, previousBlock}) => [
|
|
46
|
+
{
|
|
47
|
+
type: 'move block',
|
|
48
|
+
blockPath: focusBlock.path,
|
|
49
|
+
to: previousBlock.path,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
],
|
|
53
|
+
}),
|
|
54
|
+
defineBehavior({
|
|
55
|
+
on: 'key.down',
|
|
56
|
+
guard: ({context, event}) => {
|
|
57
|
+
const isAltArrowDown = isHotkey(
|
|
58
|
+
config.moveBlockDownShortcut,
|
|
59
|
+
event.keyboardEvent,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if (!isAltArrowDown || !selectionIsCollapsed(context)) {
|
|
63
|
+
return false
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const focusBlock = getFocusBlock(context)
|
|
67
|
+
const nextBlock = getNextBlock(context)
|
|
68
|
+
|
|
69
|
+
if (focusBlock && nextBlock) {
|
|
70
|
+
return {focusBlock, nextBlock}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return false
|
|
74
|
+
},
|
|
75
|
+
actions: [
|
|
76
|
+
(_, {focusBlock, nextBlock}) => [
|
|
77
|
+
{
|
|
78
|
+
type: 'move block',
|
|
79
|
+
blockPath: focusBlock.path,
|
|
80
|
+
to: nextBlock.path,
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
],
|
|
84
|
+
}),
|
|
85
|
+
]
|
|
86
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {isPortableTextTextBlock} from '@sanity/types'
|
|
2
|
+
import {isHotkey} from '../../utils/is-hotkey'
|
|
2
3
|
import {defineBehavior} from './behavior.types'
|
|
3
4
|
import {
|
|
4
5
|
getFocusBlockObject,
|
|
@@ -9,12 +10,42 @@ import {
|
|
|
9
10
|
selectionIsCollapsed,
|
|
10
11
|
} from './behavior.utils'
|
|
11
12
|
|
|
13
|
+
const arrowDownOnLonelyBlockObject = defineBehavior({
|
|
14
|
+
on: 'key.down',
|
|
15
|
+
guard: ({context, event}) => {
|
|
16
|
+
const isArrowDown = isHotkey('ArrowDown', event.keyboardEvent)
|
|
17
|
+
const focusBlockObject = getFocusBlockObject(context)
|
|
18
|
+
const nextBlock = getNextBlock(context)
|
|
19
|
+
|
|
20
|
+
return isArrowDown && focusBlockObject && !nextBlock
|
|
21
|
+
},
|
|
22
|
+
actions: [() => [{type: 'insert text block', placement: 'after'}]],
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const arrowUpOnLonelyBlockObject = defineBehavior({
|
|
26
|
+
on: 'key.down',
|
|
27
|
+
guard: ({context, event}) => {
|
|
28
|
+
const isArrowUp = isHotkey('ArrowUp', event.keyboardEvent)
|
|
29
|
+
const focusBlockObject = getFocusBlockObject(context)
|
|
30
|
+
const previousBlock = getPreviousBlock(context)
|
|
31
|
+
|
|
32
|
+
return isArrowUp && focusBlockObject && !previousBlock
|
|
33
|
+
},
|
|
34
|
+
actions: [
|
|
35
|
+
() => [
|
|
36
|
+
{type: 'insert text block', placement: 'before'},
|
|
37
|
+
{type: 'select previous block'},
|
|
38
|
+
],
|
|
39
|
+
],
|
|
40
|
+
})
|
|
41
|
+
|
|
12
42
|
const breakingBlockObject = defineBehavior({
|
|
13
43
|
on: 'insert break',
|
|
14
44
|
guard: ({context}) => {
|
|
15
45
|
const focusBlockObject = getFocusBlockObject(context)
|
|
46
|
+
const collapsedSelection = selectionIsCollapsed(context)
|
|
16
47
|
|
|
17
|
-
return
|
|
48
|
+
return collapsedSelection && focusBlockObject !== undefined
|
|
18
49
|
},
|
|
19
50
|
actions: [() => [{type: 'insert text block', placement: 'after'}]],
|
|
20
51
|
})
|
|
@@ -94,6 +125,8 @@ const deletingEmptyTextBlockBeforeBlockObject = defineBehavior({
|
|
|
94
125
|
})
|
|
95
126
|
|
|
96
127
|
export const coreBlockObjectBehaviors = {
|
|
128
|
+
arrowDownOnLonelyBlockObject,
|
|
129
|
+
arrowUpOnLonelyBlockObject,
|
|
97
130
|
breakingBlockObject,
|
|
98
131
|
deletingEmptyTextBlockAfterBlockObject,
|
|
99
132
|
deletingEmptyTextBlockBeforeBlockObject,
|
|
@@ -16,6 +16,8 @@ export const coreBehaviors = [
|
|
|
16
16
|
coreDecoratorBehaviors.decoratorAdd,
|
|
17
17
|
coreDecoratorBehaviors.decoratorRemove,
|
|
18
18
|
coreDecoratorBehaviors.decoratorToggle,
|
|
19
|
+
coreBlockObjectBehaviors.arrowDownOnLonelyBlockObject,
|
|
20
|
+
coreBlockObjectBehaviors.arrowUpOnLonelyBlockObject,
|
|
19
21
|
coreBlockObjectBehaviors.breakingBlockObject,
|
|
20
22
|
coreBlockObjectBehaviors.deletingEmptyTextBlockAfterBlockObject,
|
|
21
23
|
coreBlockObjectBehaviors.deletingEmptyTextBlockBeforeBlockObject,
|
|
@@ -20,7 +20,7 @@ export function createLinkBehaviors(config: LinkBehaviorsConfig) {
|
|
|
20
20
|
on: 'paste',
|
|
21
21
|
guard: ({context, event}) => {
|
|
22
22
|
const selectionCollapsed = selectionIsCollapsed(context)
|
|
23
|
-
const text = event.
|
|
23
|
+
const text = event.data.getData('text/plain')
|
|
24
24
|
const url = looksLikeUrl(text) ? text : undefined
|
|
25
25
|
const annotation =
|
|
26
26
|
url !== undefined
|
|
@@ -52,7 +52,7 @@ export function createLinkBehaviors(config: LinkBehaviorsConfig) {
|
|
|
52
52
|
return false
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
const text = event.
|
|
55
|
+
const text = event.data.getData('text/plain')
|
|
56
56
|
const url = looksLikeUrl(text) ? text : undefined
|
|
57
57
|
const annotation =
|
|
58
58
|
url !== undefined
|
|
@@ -190,7 +190,7 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
|
190
190
|
const automaticHrOnPaste = defineBehavior({
|
|
191
191
|
on: 'paste',
|
|
192
192
|
guard: ({context, event}) => {
|
|
193
|
-
const text = event.
|
|
193
|
+
const text = event.data.getData('text/plain')
|
|
194
194
|
const hrRegExp = /^(---)$|(___)$|(\*\*\*)$/gm
|
|
195
195
|
const hrCharacters = text.match(hrRegExp)?.[0]
|
|
196
196
|
const hrObject = config.horizontalRuleObject?.({
|
|
@@ -45,6 +45,10 @@ export type BehaviorEvent =
|
|
|
45
45
|
value: {[prop: string]: unknown}
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
+
| {
|
|
49
|
+
type: 'copy'
|
|
50
|
+
data: DataTransfer
|
|
51
|
+
}
|
|
48
52
|
| {
|
|
49
53
|
type: 'decorator.add'
|
|
50
54
|
decorator: string
|
|
@@ -81,7 +85,21 @@ export type BehaviorEvent =
|
|
|
81
85
|
}
|
|
82
86
|
| {
|
|
83
87
|
type: 'paste'
|
|
84
|
-
|
|
88
|
+
data: DataTransfer
|
|
89
|
+
}
|
|
90
|
+
| {
|
|
91
|
+
type: 'key.down'
|
|
92
|
+
keyboardEvent: Pick<
|
|
93
|
+
KeyboardEvent,
|
|
94
|
+
'key' | 'code' | 'altKey' | 'ctrlKey' | 'metaKey' | 'shiftKey'
|
|
95
|
+
>
|
|
96
|
+
}
|
|
97
|
+
| {
|
|
98
|
+
type: 'key.up'
|
|
99
|
+
keyboardEvent: Pick<
|
|
100
|
+
KeyboardEvent,
|
|
101
|
+
'key' | 'code' | 'altKey' | 'ctrlKey' | 'metaKey' | 'shiftKey'
|
|
102
|
+
>
|
|
85
103
|
}
|
|
86
104
|
|
|
87
105
|
/**
|
|
@@ -127,6 +145,11 @@ export type BehaviorActionIntend =
|
|
|
127
145
|
children?: PortableTextTextBlock['children']
|
|
128
146
|
}
|
|
129
147
|
}
|
|
148
|
+
| {
|
|
149
|
+
type: 'move block'
|
|
150
|
+
blockPath: [KeyedSegment]
|
|
151
|
+
to: [KeyedSegment]
|
|
152
|
+
}
|
|
130
153
|
| {
|
|
131
154
|
type: 'set block'
|
|
132
155
|
paths: Array<[KeyedSegment]>
|
|
@@ -156,6 +179,12 @@ export type BehaviorActionIntend =
|
|
|
156
179
|
type: 'select'
|
|
157
180
|
selection: EditorSelection
|
|
158
181
|
}
|
|
182
|
+
| {
|
|
183
|
+
type: 'select previous block'
|
|
184
|
+
}
|
|
185
|
+
| {
|
|
186
|
+
type: 'select next block'
|
|
187
|
+
}
|
|
159
188
|
| {
|
|
160
189
|
type: 'reselect'
|
|
161
190
|
}
|
|
@@ -208,7 +237,13 @@ export type BehaviorActionIntendSet<
|
|
|
208
237
|
event: PickFromUnion<BehaviorEvent, 'type', TBehaviorEventType>
|
|
209
238
|
},
|
|
210
239
|
guardResponse: TGuardResponse,
|
|
211
|
-
) => Array<
|
|
240
|
+
) => Array<
|
|
241
|
+
OmitFromUnion<
|
|
242
|
+
BehaviorActionIntend,
|
|
243
|
+
'type',
|
|
244
|
+
'copy' | 'key.down' | 'key.up' | 'paste'
|
|
245
|
+
>
|
|
246
|
+
>
|
|
212
247
|
|
|
213
248
|
/**
|
|
214
249
|
* @alpha
|