ultisuite-client/lib/drive/docs-graphic-flow-move.ts
R3D347HR4Y 303b2b1074
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
wow
2026-06-11 01:22:40 +02:00

123 lines
4.0 KiB
TypeScript

import type { Editor } from "@tiptap/core"
import type { Node as PMNode } from "@tiptap/pm/model"
import { NodeSelection, type EditorState, type ResolvedPos, type Transaction } from "@tiptap/pm/state"
import { parseGraphicAttrs } from "./docs-graphic-types.ts"
function isGraphicNode(node: PMNode): boolean {
return node.type.name === "docsGraphic" || node.type.name === "docsInlineGraphic"
}
/** Document position immediately before child `childIndex` inside `$pos.parent`. */
export function childPosAtIndex($pos: ResolvedPos, childIndex: number): number {
const parent = $pos.parent
let pos = $pos.start($pos.depth)
for (let i = 0; i < childIndex; i++) pos += parent.child(i).nodeSize
return pos
}
function removeEmptyParagraph(tr: Transaction, paragraphPos: number): void {
const mappedPos = tr.mapping.map(paragraphPos)
const paragraph = tr.doc.nodeAt(mappedPos)
if (paragraph?.type.name === "paragraph" && paragraph.content.size === 0) {
tr.delete(mappedPos, mappedPos + paragraph.nodeSize)
}
}
function inlineToBlockNode(state: EditorState, node: PMNode): PMNode | null {
const blockType = state.schema.nodes.docsGraphic
if (!blockType) return null
const attrs = parseGraphicAttrs(node.attrs as Record<string, unknown>)
const wrap = attrs.wrap === "inline" ? "square" : attrs.wrap
return blockType.create({ ...node.attrs, placement: "block", wrap })
}
function findSelectedGraphic(
state: EditorState
): { pos: number; node: PMNode } | null {
const { selection } = state
if (!(selection instanceof NodeSelection)) return null
const node = selection.node
if (!isGraphicNode(node)) return null
return { pos: selection.from, node }
}
function moveBlockGraphic(
state: EditorState,
pos: number,
node: PMNode,
direction: "up" | "down",
dispatch?: (tr: Transaction) => void
): boolean {
const $pos = state.doc.resolve(pos)
const index = $pos.index($pos.depth)
const parent = $pos.parent
if (direction === "up") {
if (index === 0) return false
const insertPos = childPosAtIndex($pos, index - 1)
const tr = state.tr
tr.delete(pos, pos + node.nodeSize)
const mappedInsert = tr.mapping.map(insertPos)
tr.insert(mappedInsert, node)
tr.setSelection(NodeSelection.create(tr.doc, mappedInsert))
dispatch?.(tr)
return true
}
if (index >= parent.childCount - 1) return false
const insertPos = childPosAtIndex($pos, index + 2)
const tr = state.tr
tr.delete(pos, pos + node.nodeSize)
const mappedInsert = tr.mapping.map(insertPos)
tr.insert(mappedInsert, node)
tr.setSelection(NodeSelection.create(tr.doc, mappedInsert))
dispatch?.(tr)
return true
}
function moveInlineGraphic(
state: EditorState,
pos: number,
node: PMNode,
direction: "up" | "down",
dispatch?: (tr: Transaction) => void
): boolean {
const blockNode = inlineToBlockNode(state, node)
if (!blockNode) return false
const $pos = state.doc.resolve(pos)
const paragraphPos = $pos.before($pos.depth)
const $paragraph = state.doc.resolve(paragraphPos)
if (direction === "up" && $paragraph.index($paragraph.depth) === 0) return false
const paragraphEnd = $pos.after($pos.depth)
if (direction === "down" && paragraphEnd >= state.doc.content.size) return false
const tr = state.tr
tr.delete(pos, pos + node.nodeSize)
removeEmptyParagraph(tr, paragraphPos)
const insertPos =
direction === "up"
? tr.mapping.map(paragraphPos)
: tr.mapping.map(paragraphEnd)
tr.insert(insertPos, blockNode)
tr.setSelection(NodeSelection.create(tr.doc, insertPos))
dispatch?.(tr)
return true
}
/** Move a move-with-text graphic up/down in the document flow. */
export function moveInFlowGraphic(editor: Editor, direction: "up" | "down"): boolean {
const { state } = editor
const selected = findSelectedGraphic(state)
if (!selected) return false
const { pos, node } = selected
if (node.type.name === "docsGraphic") {
return moveBlockGraphic(state, pos, node, direction, editor.view.dispatch.bind(editor.view))
}
return moveInlineGraphic(state, pos, node, direction, editor.view.dispatch.bind(editor.view))
}