From 3bc4fcf199bab75025dcfe6ede32522b3e75a6a0 Mon Sep 17 00:00:00 2001 From: 01zulfi <85733202+01zulfi@users.noreply.github.com> Date: Mon, 9 Dec 2024 20:55:30 +0500 Subject: [PATCH] editor: add alt arrow keyboard shortcut * implementation from https://discuss.prosemirror.net/t/keymap-to-move-a-line/3645/5 * only supports moving paragraphs * TODO: add tests Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com> --- .../editor/src/extensions/key-map/key-map.ts | 89 ++++++++++++++++++- 1 file changed, 88 insertions(+), 1 deletion(-) diff --git a/packages/editor/src/extensions/key-map/key-map.ts b/packages/editor/src/extensions/key-map/key-map.ts index 148005836d..d8b071fd6c 100644 --- a/packages/editor/src/extensions/key-map/key-map.ts +++ b/packages/editor/src/extensions/key-map/key-map.ts @@ -17,11 +17,92 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -import { Extension } from "@tiptap/core"; +import { Editor, Extension } from "@tiptap/core"; import { isInTable } from "@tiptap/pm/tables"; import { CodeBlock } from "../code-block/index.js"; import { showLinkPopup } from "../../toolbar/popups/link-popup.js"; import { isListActive } from "../../utils/list.js"; +import { findParentNodeOfType } from "../../utils/prosemirror.js"; +import { Fragment, Node, Slice } from "@tiptap/pm/model"; +import { ReplaceStep } from "@tiptap/pm/transform"; +import { Selection } from "@tiptap/pm/state"; +import { Callout } from "../callout/callout.js"; + +function mapChildren( + node: Node | Fragment, + callback: (child: Node, index: number, frag: Fragment) => T +): T[] { + const array = []; + for (let i = 0; i < node.childCount; i++) { + array.push( + callback(node.child(i), i, node instanceof Fragment ? node : node.content) + ); + } + return array; +} + +/** + * implementation from https://discuss.prosemirror.net/t/keymap-to-move-a-line/3645/5 + */ +function moveNode(editor: Editor, dir: "up" | "down") { + const isDown = dir === "down"; + const { state } = editor; + if (!state.selection.empty) { + return false; + } + + const { $from } = state.selection; + const type = $from.node().type; + if (!type) { + return false; + } + + const currentResolved = findParentNodeOfType(type)(state.selection); + if (!currentResolved) { + return false; + } + + const { node: currentNode } = currentResolved; + const parentDepth = currentResolved.depth - 1; + const parent = $from.node(parentDepth); + const parentPos = $from.start(parentDepth); + + if (currentNode.type !== type) { + return false; + } + + let arr = mapChildren(parent, (node) => node); + let index = arr.indexOf(currentNode); + let swapWith = isDown ? index + 1 : index - 1; + if (swapWith >= arr.length || swapWith < 0) { + return false; + } + if (swapWith === 0 && parent.type.name === Callout.name) { + return false; + } + + const swapWithNodeSize = arr[swapWith].nodeSize; + + [arr[index], arr[swapWith]] = [arr[swapWith], arr[index]]; + + let tr = state.tr; + let replaceStart = parentPos; + let replaceEnd = $from.end(parentDepth); + + const slice = new Slice(Fragment.fromArray(arr), 0, 0); + + tr = tr.step(new ReplaceStep(replaceStart, replaceEnd, slice, false)); + tr = tr.setSelection( + Selection.near( + tr.doc.resolve( + isDown ? $from.pos + swapWithNodeSize : $from.pos - swapWithNodeSize + ) + ) + ); + tr.scrollIntoView(); + editor.view.dispatch(tr); + return true; +} export const KeyMap = Extension.create({ name: "key-map", @@ -70,6 +151,12 @@ export const KeyMap = Extension.create({ "Mod-k": ({ editor }) => { showLinkPopup(editor); return true; + }, + "Alt-ArrowUp": ({ editor }) => { + return moveNode(editor, "up"); + }, + "Alt-ArrowDown": ({ editor }) => { + return moveNode(editor, "down"); } }; }