2
2
* https://github.com/TriliumNext/Notes/issues/1002
3
3
*/
4
4
5
- import { Command , DocumentSelection , Element , Node , Plugin } from 'ckeditor5' ;
6
-
5
+ import { Command , DocumentSelection , Element , Node , Plugin , Range } from 'ckeditor5' ;
7
6
export default class MoveBlockUpDownPlugin extends Plugin {
8
7
9
8
init ( ) {
10
9
const editor = this . editor ;
11
- editor . config . define ( 'moveBlockUp' , {
12
- keystroke : [ 'ctrl+arrowup' , 'alt+arrowup' ] ,
13
- } ) ;
14
- editor . config . define ( 'moveBlockDown' , {
15
- keystroke : [ 'ctrl+arrowdown' , 'alt+arrowdown' ] ,
16
- } ) ;
17
10
18
11
editor . commands . add ( 'moveBlockUp' , new MoveBlockUpCommand ( editor ) ) ;
19
12
editor . commands . add ( 'moveBlockDown' , new MoveBlockDownCommand ( editor ) ) ;
20
13
21
- for ( const keystroke of editor . config . get ( 'moveBlockUp.keystroke' ) ?? [ ] ) {
22
- editor . keystrokes . set ( keystroke , 'moveBlockUp' ) ;
23
- }
24
- for ( const keystroke of editor . config . get ( 'moveBlockDown.keystroke' ) ?? [ ] ) {
25
- editor . keystrokes . set ( keystroke , 'moveBlockDown' ) ;
26
- }
14
+ // Use native DOM capturing to intercept Ctrl/Alt + ↑/↓,
15
+ // as plugin-level keystroke handling may fail when the selection is near an object.
16
+ this . bindMoveBlockShortcuts ( editor ) ;
27
17
}
18
+
19
+ bindMoveBlockShortcuts ( editor : any ) {
20
+ editor . editing . view . once ( 'render' , ( ) => {
21
+ const domRoot = editor . editing . view . getDomRoot ( ) ;
22
+ if ( ! domRoot ) return ;
23
+
24
+ const handleKeydown = ( e : KeyboardEvent ) => {
25
+ const keyMap = {
26
+ ArrowUp : 'moveBlockUp' ,
27
+ ArrowDown : 'moveBlockDown'
28
+ } ;
29
+
30
+ const command = keyMap [ e . key ] ;
31
+ const isCtrl = e . ctrlKey || e . metaKey ;
32
+ const hasModifier = ( isCtrl || e . altKey ) && ! ( isCtrl && e . altKey ) ;
33
+
34
+ if ( command && hasModifier ) {
35
+ e . preventDefault ( ) ;
36
+ e . stopImmediatePropagation ( ) ;
37
+ editor . execute ( command ) ;
38
+ }
39
+ } ;
40
+
41
+ domRoot . addEventListener ( 'keydown' , handleKeydown , { capture : true } ) ;
42
+ } ) ;
43
+ }
28
44
29
45
}
30
46
31
47
abstract class MoveBlockUpDownCommand extends Command {
32
48
33
- abstract getSelectedBlocks ( selection : DocumentSelection ) : Element [ ] ;
34
49
abstract getSibling ( selectedBlock : Element ) : Node | null ;
35
50
abstract get offset ( ) : "before" | "after" ;
36
51
37
- override refresh ( ) {
38
- const selection = this . editor . model . document . selection ;
39
- const selectedBlocks = this . getSelectedBlocks ( selection ) ;
40
-
41
- this . isEnabled = true ;
42
- for ( const selectedBlock of selectedBlocks ) {
43
- if ( ! this . getSibling ( selectedBlock ) ) this . isEnabled = false ;
44
- }
45
- }
46
-
47
52
override execute ( ) {
48
53
const model = this . editor . model ;
49
54
const selection = model . document . selection ;
50
55
const selectedBlocks = this . getSelectedBlocks ( selection ) ;
56
+ const isEnabled = selectedBlocks . length > 0
57
+ && selectedBlocks . every ( block => ! ! this . getSibling ( block ) ) ;
58
+
59
+ if ( ! isEnabled ) {
60
+ return ;
61
+ }
62
+
63
+ const movingBlocks = this . offset === 'before'
64
+ ? selectedBlocks
65
+ : [ ...selectedBlocks ] . reverse ( ) ;
66
+
67
+ // Store selection offsets
68
+ const firstBlock = selectedBlocks [ 0 ] ;
69
+ const lastBlock = selectedBlocks [ selectedBlocks . length - 1 ] ;
70
+ const startOffset = model . document . selection . getFirstPosition ( ) ?. offset ?? 0 ;
71
+ const endOffset = model . document . selection . getLastPosition ( ) ?. offset ?? 0 ;
51
72
52
73
model . change ( ( writer ) => {
53
- for ( const selectedBlock of selectedBlocks ) {
54
- const sibling = this . getSibling ( selectedBlock ) ;
74
+ // Move blocks
75
+ for ( const block of movingBlocks ) {
76
+ const sibling = this . getSibling ( block ) ;
55
77
if ( sibling ) {
56
- const range = model . createRangeOn ( selectedBlock ) ;
78
+ const range = model . createRangeOn ( block ) ;
57
79
writer . move ( range , sibling , this . offset ) ;
58
80
}
59
81
}
82
+
83
+ // Restore selection
84
+ let range : Range ;
85
+ const maxStart = firstBlock . maxOffset ?? startOffset ;
86
+ const maxEnd = lastBlock . maxOffset ?? endOffset ;
87
+ // If original offsets valid within bounds, restore partial selection
88
+ if ( startOffset <= maxStart && endOffset <= maxEnd ) {
89
+ const clampedStart = Math . min ( startOffset , maxStart ) ;
90
+ const clampedEnd = Math . min ( endOffset , maxEnd ) ;
91
+ range = writer . createRange (
92
+ writer . createPositionAt ( firstBlock , clampedStart ) ,
93
+ writer . createPositionAt ( lastBlock , clampedEnd )
94
+ ) ;
95
+ } else { // Fallback: select entire moved blocks (handles tables)
96
+ range = writer . createRange (
97
+ writer . createPositionBefore ( firstBlock ) ,
98
+ writer . createPositionAfter ( lastBlock )
99
+ ) ;
100
+ }
101
+ writer . setSelection ( range ) ;
102
+ this . editor . editing . view . focus ( ) ;
103
+
104
+ this . scrollToSelection ( ) ;
60
105
} ) ;
106
+ }
107
+
108
+ getSelectedBlocks ( selection : DocumentSelection ) {
109
+ const blocks = [ ...selection . getSelectedBlocks ( ) ] ;
110
+ const resolved : Element [ ] = [ ] ;
111
+
112
+ // Selects elements (such as Mermaid) when there are no blocks
113
+ if ( ! blocks . length ) {
114
+ const selectedObj = selection . getSelectedElement ( ) ;
115
+ if ( selectedObj ) {
116
+ return [ selectedObj ] ;
117
+ }
118
+ }
119
+
120
+ for ( const block of blocks ) {
121
+ let el : Element = block ;
122
+ // Traverse up until the parent is the root ($root) or there is no parent
123
+ while ( el . parent && el . parent . name !== '$root' ) {
124
+ el = el . parent as Element ;
125
+ }
126
+ resolved . push ( el ) ;
127
+ }
128
+
129
+ // Deduplicate adjacent duplicates (e.g., nested selections resolving to same block)
130
+ return resolved . filter ( ( blk , idx ) => idx === 0 || blk !== resolved [ idx - 1 ] ) ;
61
131
}
132
+
133
+ scrollToSelection ( ) {
134
+ // Ensure scroll happens in sync with DOM updates
135
+ requestAnimationFrame ( ( ) => {
136
+ this . editor . editing . view . scrollToTheSelection ( ) ;
137
+ } ) ;
138
+ } ;
62
139
}
63
140
64
141
class MoveBlockUpCommand extends MoveBlockUpDownCommand {
65
142
66
- getSelectedBlocks ( selection : DocumentSelection ) {
67
- return [ ...selection . getSelectedBlocks ( ) ] ;
68
- }
69
-
70
143
getSibling ( selectedBlock : Element ) {
71
144
return selectedBlock . previousSibling ;
72
145
}
@@ -79,11 +152,6 @@ class MoveBlockUpCommand extends MoveBlockUpDownCommand {
79
152
80
153
class MoveBlockDownCommand extends MoveBlockUpDownCommand {
81
154
82
- /** @override */
83
- getSelectedBlocks ( selection : DocumentSelection ) {
84
- return [ ...selection . getSelectedBlocks ( ) ] . reverse ( ) ;
85
- }
86
-
87
155
/** @override */
88
156
getSibling ( selectedBlock : Element ) {
89
157
return selectedBlock . nextSibling ;
0 commit comments