Skip to content
File

Blob: src/client/components/editor/lib/block-actions.ts

typescript79 lines
1import type { Editor } from "@tiptap/core";
2import { NodeSelection, TextSelection, type Transaction } from "@tiptap/pm/state";
3import { scheduleMovedTextblockSelectionFinalization } from "./moved-textblock-selection";
4import { findTopLevelBlockByBid, getTopLevelBlocks } from "./top-level-blocks";
5 
6type MoveDirection = -1 | 1;
7 
8export function canMoveTopLevelBlock(doc: Transaction["doc"], bid: string | null, direction: MoveDirection): boolean {
9 const blocks = getTopLevelBlocks(doc);
10 const block = blocks.find((candidate) => candidate.bid === bid) ?? null;
11 if (!block) return false;
12 
13 const targetIndex = block.index + direction;
14 return targetIndex >= 0 && targetIndex < blocks.length;
15}
16 
17export function applyMoveTopLevelBlock(tr: Transaction, bid: string | null, direction: MoveDirection): boolean {
18 const blocks = getTopLevelBlocks(tr.doc);
19 const source = blocks.find((candidate) => candidate.bid === bid) ?? null;
20 if (!source) return false;
21 
22 const targetIndex = source.index + direction;
23 if (targetIndex < 0 || targetIndex >= blocks.length) return false;
24 
25 const target = blocks[targetIndex];
26 if (!target) return false;
27 
28 const sourceStart = source.pos;
29 const sourceEnd = sourceStart + source.node.nodeSize;
30 const insertAnchor = direction < 0 ? target.pos : target.pos + target.node.nodeSize;
31 
32 tr.delete(sourceStart, sourceEnd);
33 const insertPos = tr.mapping.map(insertAnchor, direction < 0 ? -1 : 1);
34 tr.insert(insertPos, source.node);
35 tr.setSelection(NodeSelection.create(tr.doc, insertPos));
36 return true;
37}
38 
39export function applyDeleteTopLevelBlock(tr: Transaction, bid: string | null): boolean {
40 const blocks = getTopLevelBlocks(tr.doc);
41 const block = blocks.find((candidate) => candidate.bid === bid) ?? null;
42 if (!block) return false;
43 
44 if (blocks.length === 1) {
45 const paragraph = tr.doc.type.schema.nodes.paragraph;
46 if (!paragraph) return false;
47 
48 tr.replaceWith(block.pos, block.pos + block.node.nodeSize, paragraph.create());
49 tr.setSelection(TextSelection.create(tr.doc, 1));
50 return true;
51 }
52 
53 tr.delete(block.pos, block.pos + block.node.nodeSize);
54 const nextPos = Math.min(block.pos, tr.doc.content.size);
55 tr.setSelection(TextSelection.near(tr.doc.resolve(nextPos)));
56 return true;
57}
58 
59export function moveTopLevelBlock(editor: Editor, bid: string | null, direction: MoveDirection): boolean {
60 const tr = editor.state.tr;
61 if (!applyMoveTopLevelBlock(tr, bid, direction)) return false;
62 editor.view.dispatch(tr);
63 editor.view.focus();
64 scheduleMovedTextblockSelectionFinalization(editor.view);
65 return true;
66}
67 
68export function deleteTopLevelBlock(editor: Editor, bid: string | null): boolean {
69 const tr = editor.state.tr;
70 if (!applyDeleteTopLevelBlock(tr, bid)) return false;
71 editor.view.dispatch(tr);
72 editor.view.focus();
73 return true;
74}
75 
76export function getCurrentTopLevelBlock(editor: Editor, bid: string | null) {
77 return findTopLevelBlockByBid(editor.state.doc, bid);
78}