101 lines
3.4 KiB
TypeScript
101 lines
3.4 KiB
TypeScript
import { Extension, type Editor } from "@tiptap/core"
|
|
import { Plugin, PluginKey } from "@tiptap/pm/state"
|
|
import type { Node as ProseMirrorNode } from "@tiptap/pm/model"
|
|
import { parseGraphicAttrs, type DocsGraphicAttrs } from "@/lib/drive/docs-graphic-types"
|
|
|
|
export const docsGraphicAnchorSyncKey = new PluginKey("docsGraphicAnchorSync")
|
|
|
|
function isGraphicNode(node: ProseMirrorNode): boolean {
|
|
return node.type.name === "docsGraphic" || node.type.name === "docsInlineGraphic"
|
|
}
|
|
|
|
function syncMoveWithTextGraphic(
|
|
node: ProseMirrorNode,
|
|
mappedAnchorPos: number
|
|
): Partial<DocsGraphicAttrs> | null {
|
|
const attrs = parseGraphicAttrs(node.attrs as Record<string, unknown>)
|
|
if (attrs.positionMode !== "move-with-text") return null
|
|
if (attrs.anchorPos < 0) return { anchorPos: mappedAnchorPos }
|
|
if (mappedAnchorPos < 0) {
|
|
return {
|
|
positionMode: "fixed-on-page",
|
|
placement: "absolute",
|
|
anchorPos: -1,
|
|
}
|
|
}
|
|
if (mappedAnchorPos !== attrs.anchorPos) {
|
|
return { anchorPos: mappedAnchorPos }
|
|
}
|
|
return null
|
|
}
|
|
|
|
/** Select a graphic under the pointer (Alt+click), including behind-text overlays. */
|
|
function selectGraphicAtPointer(editor: Editor, event: MouseEvent): boolean {
|
|
if (!event.altKey) return false
|
|
const stack = document.elementsFromPoint(event.clientX, event.clientY)
|
|
for (const el of stack) {
|
|
if (!(el instanceof HTMLElement)) continue
|
|
const graphic = el.classList.contains("docs-graphic")
|
|
? el
|
|
: (el.closest(".docs-graphic") as HTMLElement | null)
|
|
const host = graphic?.hasAttribute("data-graphic-pos")
|
|
? graphic
|
|
: (graphic?.closest("[data-graphic-pos]") as HTMLElement | null)
|
|
if (!host) continue
|
|
const posRaw = host.getAttribute("data-graphic-pos")
|
|
const pos = posRaw ? Number(posRaw) : NaN
|
|
if (!Number.isFinite(pos)) continue
|
|
editor.chain().focus().setNodeSelection(pos).run()
|
|
event.preventDefault()
|
|
event.stopPropagation()
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
/** Keeps move-with-text anchor positions mapped through document edits. */
|
|
export const DocsGraphicAnchorSync = Extension.create({
|
|
name: "docsGraphicAnchorSync",
|
|
|
|
addProseMirrorPlugins() {
|
|
const editor = this.editor
|
|
return [
|
|
new Plugin({
|
|
key: docsGraphicAnchorSyncKey,
|
|
props: {
|
|
handleDOMEvents: {
|
|
mousedown(_view, event) {
|
|
return selectGraphicAtPointer(editor, event)
|
|
},
|
|
},
|
|
},
|
|
appendTransaction(transactions, _oldState, newState) {
|
|
if (!transactions.some((tr) => tr.docChanged)) return null
|
|
|
|
let tr = newState.tr
|
|
let changed = false
|
|
|
|
newState.doc.descendants((node, pos) => {
|
|
if (!isGraphicNode(node)) return
|
|
const attrs = parseGraphicAttrs(node.attrs as Record<string, unknown>)
|
|
if (attrs.positionMode !== "move-with-text" || attrs.anchorPos < 0) return
|
|
|
|
const mapped = transactions.reduce((anchor, transaction) => {
|
|
if (!transaction.docChanged) return anchor
|
|
return transaction.mapping.map(anchor)
|
|
}, attrs.anchorPos)
|
|
|
|
const patch = syncMoveWithTextGraphic(node, mapped)
|
|
if (patch) {
|
|
tr = tr.setNodeMarkup(pos, undefined, { ...node.attrs, ...patch })
|
|
changed = true
|
|
}
|
|
})
|
|
|
|
return changed ? tr : null
|
|
},
|
|
}),
|
|
]
|
|
},
|
|
})
|