22 * https://github.com/TriliumNext/Notes/issues/1002
33 */
44
5- import { Command , DocumentSelection , Element , Node , Plugin } from 'ckeditor5' ;
6-
5+ import { Command , DocumentSelection , Element , Node , Plugin , Range } from 'ckeditor5' ;
76export default class MoveBlockUpDownPlugin extends Plugin {
87
98 init ( ) {
109 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- } ) ;
1710
1811 editor . commands . add ( 'moveBlockUp' , new MoveBlockUpCommand ( editor ) ) ;
1912 editor . commands . add ( 'moveBlockDown' , new MoveBlockDownCommand ( editor ) ) ;
2013
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 ) ;
2717 }
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+ }
2844
2945}
3046
3147abstract class MoveBlockUpDownCommand extends Command {
3248
33- abstract getSelectedBlocks ( selection : DocumentSelection ) : Element [ ] ;
3449 abstract getSibling ( selectedBlock : Element ) : Node | null ;
3550 abstract get offset ( ) : "before" | "after" ;
3651
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-
4752 override execute ( ) {
4853 const model = this . editor . model ;
4954 const selection = model . document . selection ;
5055 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 ;
5172
5273 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 ) ;
5577 if ( sibling ) {
56- const range = model . createRangeOn ( selectedBlock ) ;
78+ const range = model . createRangeOn ( block ) ;
5779 writer . move ( range , sibling , this . offset ) ;
5880 }
5981 }
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 ( ) ;
60105 } ) ;
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 ] ) ;
61131 }
132+
133+ scrollToSelection ( ) {
134+ // Ensure scroll happens in sync with DOM updates
135+ requestAnimationFrame ( ( ) => {
136+ this . editor . editing . view . scrollToTheSelection ( ) ;
137+ } ) ;
138+ } ;
62139}
63140
64141class MoveBlockUpCommand extends MoveBlockUpDownCommand {
65142
66- getSelectedBlocks ( selection : DocumentSelection ) {
67- return [ ...selection . getSelectedBlocks ( ) ] ;
68- }
69-
70143 getSibling ( selectedBlock : Element ) {
71144 return selectedBlock . previousSibling ;
72145 }
@@ -79,11 +152,6 @@ class MoveBlockUpCommand extends MoveBlockUpDownCommand {
79152
80153class MoveBlockDownCommand extends MoveBlockUpDownCommand {
81154
82- /** @override */
83- getSelectedBlocks ( selection : DocumentSelection ) {
84- return [ ...selection . getSelectedBlocks ( ) ] . reverse ( ) ;
85- }
86-
87155 /** @override */
88156 getSibling ( selectedBlock : Element ) {
89157 return selectedBlock . nextSibling ;
0 commit comments