123 lines
4.0 KiB
TypeScript
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))
|
|
}
|