File
Blob: src/client/components/editor/lib/block-actions.ts
| 1 | import type { Editor } from "@tiptap/core"; |
| 2 | import { NodeSelection, TextSelection, type Transaction } from "@tiptap/pm/state"; |
| 3 | import { scheduleMovedTextblockSelectionFinalization } from "./moved-textblock-selection"; |
| 4 | import { findTopLevelBlockByBid, getTopLevelBlocks } from "./top-level-blocks"; |
| 5 | |
| 6 | type MoveDirection = -1 | 1; |
| 7 | |
| 8 | export 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 | |
| 17 | export 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 | |
| 39 | export 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 | |
| 59 | export 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 | |
| 68 | export 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 | |
| 76 | export function getCurrentTopLevelBlock(editor: Editor, bid: string | null) { |
| 77 | return findTopLevelBlockByBid(editor.state.doc, bid); |
| 78 | } |