From 48613da57f554786b96906ad162a8678f312c330 Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Sun, 8 Jun 2025 16:30:10 +0800 Subject: [PATCH 1/4] fix(move_block): move multiple lines up/down --- .../src/plugins/move_block_updown.ts | 56 +++++++++++-------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/packages/ckeditor5/src/plugins/move_block_updown.ts b/packages/ckeditor5/src/plugins/move_block_updown.ts index e804bb426..9c33ea550 100644 --- a/packages/ckeditor5/src/plugins/move_block_updown.ts +++ b/packages/ckeditor5/src/plugins/move_block_updown.ts @@ -30,42 +30,55 @@ export default class MoveBlockUpDownPlugin extends Plugin { abstract class MoveBlockUpDownCommand extends Command { - abstract getSelectedBlocks(selection: DocumentSelection): Element[]; abstract getSibling(selectedBlock: Element): Node | null; abstract get offset(): "before" | "after"; - override refresh() { - const selection = this.editor.model.document.selection; - const selectedBlocks = this.getSelectedBlocks(selection); - - this.isEnabled = true; - for (const selectedBlock of selectedBlocks) { - if (!this.getSibling(selectedBlock)) this.isEnabled = false; - } - } - override execute() { const model = this.editor.model; const selection = model.document.selection; const selectedBlocks = this.getSelectedBlocks(selection); + const isEnabled = selectedBlocks.length > 0 + && selectedBlocks.every(block => !!this.getSibling(block)); + + if (!isEnabled) { + return; + } + + const movingBlocks = this.offset === 'before' + ? selectedBlocks + : [...selectedBlocks].reverse(); + + // Store selection offsets + const offsets = [ + model.document.selection.getFirstPosition()?.offset, + model.document.selection.getLastPosition()?.offset + ]; model.change((writer) => { - for (const selectedBlock of selectedBlocks) { - const sibling = this.getSibling(selectedBlock); + // Move blocks + for (const block of movingBlocks) { + const sibling = this.getSibling(block); if (sibling) { - const range = model.createRangeOn(selectedBlock); + const range = model.createRangeOn(block); writer.move(range, sibling, this.offset); } } - }); - } -} - -class MoveBlockUpCommand extends MoveBlockUpDownCommand { + // 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); + }); + } + getSelectedBlocks(selection: DocumentSelection) { return [...selection.getSelectedBlocks()]; } +} + +class MoveBlockUpCommand extends MoveBlockUpDownCommand { getSibling(selectedBlock: Element) { return selectedBlock.previousSibling; @@ -79,11 +92,6 @@ class MoveBlockUpCommand extends MoveBlockUpDownCommand { class MoveBlockDownCommand extends MoveBlockUpDownCommand { - /** @override */ - getSelectedBlocks(selection: DocumentSelection) { - return [...selection.getSelectedBlocks()].reverse(); - } - /** @override */ getSibling(selectedBlock: Element) { return selectedBlock.nextSibling; From be447b4139606cb82b99d3d376c2ea71c103197c Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Sun, 8 Jun 2025 16:49:02 +0800 Subject: [PATCH 2/4] fix(move_block): keep moved blocks in view --- packages/ckeditor5/src/plugins/move_block_updown.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/ckeditor5/src/plugins/move_block_updown.ts b/packages/ckeditor5/src/plugins/move_block_updown.ts index 9c33ea550..4940097c3 100644 --- a/packages/ckeditor5/src/plugins/move_block_updown.ts +++ b/packages/ckeditor5/src/plugins/move_block_updown.ts @@ -70,12 +70,21 @@ abstract class MoveBlockUpDownCommand extends Command { writer.createPositionAt( selectedBlocks[selectedBlocks.length - 1], offsets[1])); writer.setSelection(range); + + this.scrollToSelection(); }); } getSelectedBlocks(selection: DocumentSelection) { return [...selection.getSelectedBlocks()]; } + + scrollToSelection() { + // Ensure scroll happens in sync with DOM updates + requestAnimationFrame(() => { + this.editor.editing.view.scrollToTheSelection(); + }); + }; } class MoveBlockUpCommand extends MoveBlockUpDownCommand { From a23b0c5ec90b523a0daf0a1c32d7a7f6e7bcf5cb Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Mon, 9 Jun 2025 10:04:10 +0800 Subject: [PATCH 3/4] fix(move_block): Make the object movable --- .../src/plugins/move_block_updown.ts | 82 +++++++++++++------ 1 file changed, 58 insertions(+), 24 deletions(-) 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 From 3a56a16a5846e55d5ff37a34e862a66a53121f82 Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Tue, 10 Jun 2025 18:50:32 +0800 Subject: [PATCH 4/4] fix: moving tables/blockQuote --- .../src/plugins/move_block_updown.ts | 57 ++++++++++++------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/packages/ckeditor5/src/plugins/move_block_updown.ts b/packages/ckeditor5/src/plugins/move_block_updown.ts index 556076530..dc5a8958c 100644 --- a/packages/ckeditor5/src/plugins/move_block_updown.ts +++ b/packages/ckeditor5/src/plugins/move_block_updown.ts @@ -2,8 +2,7 @@ * https://github.com/TriliumNext/Notes/issues/1002 */ -import { Command, DocumentSelection, Element, Node, Plugin } from 'ckeditor5'; - +import { Command, DocumentSelection, Element, Node, Plugin, Range } from 'ckeditor5'; export default class MoveBlockUpDownPlugin extends Plugin { init() { @@ -81,36 +80,54 @@ abstract class MoveBlockUpDownCommand extends Command { } } - // Restore selection to all items if many have been moved - if ( - startOffset <= (firstBlock.maxOffset ?? Infinity) && - endOffset <= (lastBlock.maxOffset ?? Infinity) - ) { - writer.setSelection( - writer.createRange( - writer.createPositionAt(firstBlock, startOffset), - writer.createPositionAt(lastBlock, endOffset) - ) + // Restore selection + let range: Range; + const maxStart = firstBlock.maxOffset ?? startOffset; + const maxEnd = lastBlock.maxOffset ?? endOffset; + // If original offsets valid within bounds, restore partial selection + if (startOffset <= maxStart && endOffset <= maxEnd) { + const clampedStart = Math.min(startOffset, maxStart); + const clampedEnd = Math.min(endOffset, maxEnd); + range = writer.createRange( + writer.createPositionAt(firstBlock, clampedStart), + writer.createPositionAt(lastBlock, clampedEnd) + ); + } else { // Fallback: select entire moved blocks (handles tables) + range = writer.createRange( + writer.createPositionBefore(firstBlock), + writer.createPositionAfter(lastBlock) ); } + writer.setSelection(range); + this.editor.editing.view.focus(); this.scrollToSelection(); - }); + }); } getSelectedBlocks(selection: DocumentSelection) { const blocks = [...selection.getSelectedBlocks()]; + const resolved: Element[] = []; - // 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]; + // Selects elements (such as Mermaid) when there are no blocks + if (!blocks.length) { + const selectedObj = selection.getSelectedElement(); + if (selectedObj) { + return [selectedObj]; } } - return blocks; + for (const block of blocks) { + let el: Element = block; + // Traverse up until the parent is the root ($root) or there is no parent + while (el.parent && el.parent.name !== '$root') { + el = el.parent as Element; + } + resolved.push(el); + } + + // Deduplicate adjacent duplicates (e.g., nested selections resolving to same block) + return resolved.filter((blk, idx) => idx === 0 || blk !== resolved[idx - 1]); } scrollToSelection() {