mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
feat: Add block movement with Cmd+Alt+Arrow keys (#9502)
* feat: Add block movement with Cmd+Alt+Arrow keys - Add getCurrentBlock helper function to find current block node - Implement moveBlockUp and moveBlockDown commands in Keys extension - Support Cmd+Alt+ArrowUp to move current block up - Support Cmd+Alt+ArrowDown to move current block down - Follow ProseMirror best practices for node manipulation - Maintain cursor position after block movement Fixes #9486 * feat: Add block movement shortcuts to KeyboardShortcuts component - Add Cmd+Alt+↑ shortcut for moving blocks up - Add Cmd+Alt+↓ shortcut for moving blocks down - Added to Formatting section alongside other editing shortcuts --------- Co-authored-by: codegen-sh[bot] <131295404+codegen-sh[bot]@users.noreply.github.com>
This commit is contained in:
@@ -6,8 +6,10 @@ import {
|
||||
TextSelection,
|
||||
EditorState,
|
||||
Command,
|
||||
Transaction,
|
||||
} from "prosemirror-state";
|
||||
import Extension from "@shared/editor/lib/Extension";
|
||||
import { getCurrentBlock } from "@shared/editor/queries/getCurrentBlock";
|
||||
import { isInCode } from "@shared/editor/queries/isInCode";
|
||||
|
||||
export default class Keys extends Extension {
|
||||
@@ -24,6 +26,93 @@ export default class Keys extends Extension {
|
||||
return false;
|
||||
};
|
||||
|
||||
const moveBlockUp = (
|
||||
state: EditorState,
|
||||
dispatch?: (tr: Transaction) => void
|
||||
) => {
|
||||
if (!state.selection.empty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = getCurrentBlock(state);
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [currentBlock, currentPos] = result;
|
||||
const $pos = state.doc.resolve(currentPos);
|
||||
|
||||
// Check if there's a previous sibling block
|
||||
if (!$pos.nodeBefore || !$pos.nodeBefore.isBlock) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const prevBlock = $pos.nodeBefore;
|
||||
const prevBlockPos = currentPos - prevBlock.nodeSize;
|
||||
|
||||
if (!dispatch) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { tr } = state;
|
||||
|
||||
// Move current block before the previous block
|
||||
dispatch(
|
||||
tr
|
||||
.delete(currentPos, currentPos + currentBlock.nodeSize)
|
||||
.insert(prevBlockPos, currentBlock)
|
||||
.setSelection(TextSelection.near(tr.doc.resolve(prevBlockPos + 1)))
|
||||
);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const moveBlockDown = (
|
||||
state: EditorState,
|
||||
dispatch?: (tr: Transaction) => void
|
||||
) => {
|
||||
if (!state.selection.empty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = getCurrentBlock(state);
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [currentBlock, currentPos] = result;
|
||||
const $pos = state.doc.resolve(currentPos + currentBlock.nodeSize);
|
||||
|
||||
// Check if there's a next sibling block
|
||||
if (!$pos.nodeAfter || !$pos.nodeAfter.isBlock) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const nextBlock = $pos.nodeAfter;
|
||||
const nextBlockEndPos =
|
||||
currentPos + currentBlock.nodeSize + nextBlock.nodeSize;
|
||||
|
||||
if (!dispatch) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { tr } = state;
|
||||
|
||||
// Move current block after the next block
|
||||
dispatch(
|
||||
tr
|
||||
.insert(nextBlockEndPos, currentBlock)
|
||||
.delete(currentPos, currentPos + currentBlock.nodeSize)
|
||||
.setSelection(
|
||||
TextSelection.near(
|
||||
tr.doc.resolve(nextBlockEndPos - currentBlock.nodeSize + 1)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
return {
|
||||
// Shortcuts for when editor has separate edit mode
|
||||
"Mod-Escape": onCancel,
|
||||
@@ -46,6 +135,9 @@ export default class Keys extends Extension {
|
||||
(this.editor.view.dom as HTMLElement).blur();
|
||||
return true;
|
||||
},
|
||||
// Block movement shortcuts
|
||||
"Mod-Alt-ArrowUp": moveBlockUp,
|
||||
"Mod-Alt-ArrowDown": moveBlockDown,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -260,6 +260,24 @@ function KeyboardShortcuts() {
|
||||
),
|
||||
label: t("Redo"),
|
||||
},
|
||||
{
|
||||
shortcut: (
|
||||
<>
|
||||
<Key symbol>{metaDisplay}</Key> + <Key symbol>{altDisplay}</Key>{" "}
|
||||
+ <Key symbol>↑</Key>
|
||||
</>
|
||||
),
|
||||
label: t("Move block up"),
|
||||
},
|
||||
{
|
||||
shortcut: (
|
||||
<>
|
||||
<Key symbol>{metaDisplay}</Key> + <Key symbol>{altDisplay}</Key>{" "}
|
||||
+ <Key symbol>↓</Key>
|
||||
</>
|
||||
),
|
||||
label: t("Move block down"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Node } from "prosemirror-model";
|
||||
import { EditorState } from "prosemirror-state";
|
||||
|
||||
/**
|
||||
* Gets the current block node that contains the selection
|
||||
* @param state The editor state
|
||||
* @returns The current block node and its position, or undefined if not found
|
||||
*/
|
||||
export function getCurrentBlock(
|
||||
state: EditorState
|
||||
): [Node, number] | undefined {
|
||||
const { $head } = state.selection;
|
||||
|
||||
// Walk up the tree to find the first block node
|
||||
for (let d = $head.depth; d >= 0; d--) {
|
||||
const node = $head.node(d);
|
||||
const pos = $head.before(d);
|
||||
|
||||
if (node.isBlock && d > 0) {
|
||||
// Don't return the document itself
|
||||
return [node, pos];
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
Reference in New Issue
Block a user