diff --git a/packages/ckeditor5/src/plugins/move_block_updown.ts b/packages/ckeditor5/src/plugins/move_block_updown.ts index 4940097c3..556076530 100644 --- a/packages/ckeditor5/src/plugins/move_block_updown.ts +++ b/packages/ckeditor5/src/plugins/move_block_updown.ts @@ -8,23 +8,40 @@ export default class MoveBlockUpDownPlugin extends Plugin { init() { const editor = this.editor; - editor.config.define('moveBlockUp', { - keystroke: ['ctrl+arrowup', 'alt+arrowup'], - }); - editor.config.define('moveBlockDown', { - keystroke: ['ctrl+arrowdown', 'alt+arrowdown'], - }); editor.commands.add('moveBlockUp', new MoveBlockUpCommand(editor)); editor.commands.add('moveBlockDown', new MoveBlockDownCommand(editor)); - for (const keystroke of editor.config.get('moveBlockUp.keystroke') ?? []) { - editor.keystrokes.set(keystroke, 'moveBlockUp'); - } - for (const keystroke of editor.config.get('moveBlockDown.keystroke') ?? []) { - editor.keystrokes.set(keystroke, 'moveBlockDown'); - } + // Use native DOM capturing to intercept Ctrl/Alt + ↑/↓, + // as plugin-level keystroke handling may fail when the selection is near an object. + this.bindMoveBlockShortcuts(editor); } + + bindMoveBlockShortcuts(editor: any) { + editor.editing.view.once('render', () => { + const domRoot = editor.editing.view.getDomRoot(); + if (!domRoot) return; + + const handleKeydown = (e: KeyboardEvent) => { + const keyMap = { + ArrowUp: 'moveBlockUp', + ArrowDown: 'moveBlockDown' + }; + + const command = keyMap[e.key]; + const isCtrl = e.ctrlKey || e.metaKey; + const hasModifier = (isCtrl || e.altKey) && !(isCtrl && e.altKey); + + if (command && hasModifier) { + e.preventDefault(); + e.stopImmediatePropagation(); + editor.execute(command); + } + }; + + domRoot.addEventListener('keydown', handleKeydown, { capture: true }); + }); + } } @@ -49,10 +66,10 @@ abstract class MoveBlockUpDownCommand extends Command { : [...selectedBlocks].reverse(); // Store selection offsets - const offsets = [ - model.document.selection.getFirstPosition()?.offset, - model.document.selection.getLastPosition()?.offset - ]; + const firstBlock = selectedBlocks[0]; + const lastBlock = selectedBlocks[selectedBlocks.length - 1]; + const startOffset = model.document.selection.getFirstPosition()?.offset ?? 0; + const endOffset = model.document.selection.getLastPosition()?.offset ?? 0; model.change((writer) => { // Move blocks @@ -65,19 +82,36 @@ abstract class MoveBlockUpDownCommand extends Command { } // Restore selection to all items if many have been moved - const range = writer.createRange( - writer.createPositionAt(selectedBlocks[0], offsets[0]), - writer.createPositionAt( - selectedBlocks[selectedBlocks.length - 1], offsets[1])); - writer.setSelection(range); + if ( + startOffset <= (firstBlock.maxOffset ?? Infinity) && + endOffset <= (lastBlock.maxOffset ?? Infinity) + ) { + writer.setSelection( + writer.createRange( + writer.createPositionAt(firstBlock, startOffset), + writer.createPositionAt(lastBlock, endOffset) + ) + ); + } this.scrollToSelection(); }); } - getSelectedBlocks(selection: DocumentSelection) { - return [...selection.getSelectedBlocks()]; - } + getSelectedBlocks(selection: DocumentSelection) { + const blocks = [...selection.getSelectedBlocks()]; + + // If the selected block is an object, such as a block quote or admonition, return the entire block. + if (blocks.length === 1) { + const block = blocks[0]; + const parent = block.parent; + if (!parent?.name?.startsWith('$')) { + return [parent as Element]; + } + } + + return blocks; + } scrollToSelection() { // Ensure scroll happens in sync with DOM updates