import { mergeAttributes, Node } from "@tiptap/core" import { ReactNodeViewRenderer } from "@tiptap/react" import { DocsGraphicNodeView } from "@/components/drive/richtext/docs-graphic-node-view" import { buildGradientCss, DOCS_GRAPHIC_DEFAULTS, type DocsGraphicAttrs, type DocsGraphicFloatSide, type DocsGraphicPlacement, type DocsGraphicType, type DocsGraphicWrap, type DocsShapeType, parseGraphicAttrs, } from "@/lib/drive/docs-graphic-types" declare module "@tiptap/core" { interface Commands { docsGraphic: { insertDocsGraphic: (attrs: Partial) => ReturnType updateDocsGraphic: (attrs: Partial) => ReturnType setDocsGraphicWrap: (wrap: DocsGraphicWrap) => ReturnType setDocsGraphicPlacement: (placement: DocsGraphicPlacement) => ReturnType setDocsGraphicFloatSide: (floatSide: DocsGraphicFloatSide) => ReturnType bringDocsGraphicForward: () => ReturnType sendDocsGraphicBackward: () => ReturnType } } } const graphicAttributes = { graphicType: { default: DOCS_GRAPHIC_DEFAULTS.graphicType }, src: { default: null as string | null }, alt: { default: "" }, shapeType: { default: DOCS_GRAPHIC_DEFAULTS.shapeType }, fill: { default: DOCS_GRAPHIC_DEFAULTS.fill }, stroke: { default: DOCS_GRAPHIC_DEFAULTS.stroke }, strokeWidth: { default: DOCS_GRAPHIC_DEFAULTS.strokeWidth }, gradientCss: { default: "" }, gradientAngle: { default: DOCS_GRAPHIC_DEFAULTS.gradientAngle }, gradientColor1: { default: DOCS_GRAPHIC_DEFAULTS.gradientColor1 }, gradientColor2: { default: DOCS_GRAPHIC_DEFAULTS.gradientColor2 }, width: { default: DOCS_GRAPHIC_DEFAULTS.width }, height: { default: DOCS_GRAPHIC_DEFAULTS.height }, placement: { default: DOCS_GRAPHIC_DEFAULTS.placement }, wrap: { default: DOCS_GRAPHIC_DEFAULTS.wrap }, floatSide: { default: DOCS_GRAPHIC_DEFAULTS.floatSide }, x: { default: 0 }, y: { default: 0 }, rotationDeg: { default: 0 }, zIndex: { default: 0 }, cropX: { default: 0 }, cropY: { default: 0 }, cropWidth: { default: 1 }, cropHeight: { default: 1 }, cropShape: { default: "rect" }, assetId: { default: null as string | null }, opacity: { default: 1 }, shadow: { default: "" }, } function mergeGraphicAttrs(partial: Partial): DocsGraphicAttrs { const merged = parseGraphicAttrs({ ...DOCS_GRAPHIC_DEFAULTS, ...partial }) if (merged.graphicType === "gradient" && !partial.gradientCss) { merged.gradientCss = buildGradientCss( merged.gradientAngle, merged.gradientColor1, merged.gradientColor2 ) } return merged } function graphicCommands() { return { insertDocsGraphic: (partial: Partial) => ({ chain }: { chain: () => { insertContent: (content: unknown) => { run: () => boolean } } }) => { const attrs = mergeGraphicAttrs(partial) const type = attrs.placement === "inline" || attrs.wrap === "inline" ? "docsInlineGraphic" : "docsGraphic" return chain() .insertContent({ type, attrs }) .run() }, updateDocsGraphic: (partial: Partial) => ({ chain, editor, }: { chain: () => { updateAttributes: (name: string, attrs: Partial) => { run: () => boolean } } editor: { isActive: (name: string) => boolean } }) => { const name = editor.isActive("docsInlineGraphic") ? "docsInlineGraphic" : "docsGraphic" return chain().updateAttributes(name, partial).run() }, setDocsGraphicWrap: (wrap: DocsGraphicWrap) => ({ chain, editor, }: { chain: () => { updateAttributes: (name: string, attrs: { wrap: DocsGraphicWrap }) => { run: () => boolean } } editor: { isActive: (name: string) => boolean } }) => { const name = editor.isActive("docsInlineGraphic") ? "docsInlineGraphic" : "docsGraphic" return chain().updateAttributes(name, { wrap }).run() }, setDocsGraphicPlacement: (placement: DocsGraphicPlacement) => ({ chain, editor, }: { chain: () => { updateAttributes: (name: string, attrs: { placement: DocsGraphicPlacement }) => { run: () => boolean } } editor: { isActive: (name: string) => boolean } }) => { const name = editor.isActive("docsInlineGraphic") ? "docsInlineGraphic" : "docsGraphic" return chain().updateAttributes(name, { placement }).run() }, setDocsGraphicFloatSide: (floatSide: DocsGraphicFloatSide) => ({ chain, editor, }: { chain: () => { updateAttributes: (name: string, attrs: { floatSide: DocsGraphicFloatSide }) => { run: () => boolean } } editor: { isActive: (name: string) => boolean } }) => { const name = editor.isActive("docsInlineGraphic") ? "docsInlineGraphic" : "docsGraphic" return chain().updateAttributes(name, { floatSide }).run() }, bringDocsGraphicForward: () => ({ chain, editor, }: { chain: () => { updateAttributes: (name: string, attrs: { zIndex: number }) => { run: () => boolean } } editor: { isActive: (name: string) => boolean; getAttributes: (name: string) => Record } }) => { const name = editor.isActive("docsInlineGraphic") ? "docsInlineGraphic" : "docsGraphic" const z = Number(editor.getAttributes(name).zIndex ?? 0) return chain().updateAttributes(name, { zIndex: z + 1 }).run() }, sendDocsGraphicBackward: () => ({ chain, editor, }: { chain: () => { updateAttributes: (name: string, attrs: { zIndex: number }) => { run: () => boolean } } editor: { isActive: (name: string) => boolean; getAttributes: (name: string) => Record } }) => { const name = editor.isActive("docsInlineGraphic") ? "docsInlineGraphic" : "docsGraphic" const z = Number(editor.getAttributes(name).zIndex ?? 0) return chain().updateAttributes(name, { zIndex: Math.max(0, z - 1) }).run() }, } } export const DocsGraphic = Node.create({ name: "docsGraphic", group: "block", atom: true, draggable: true, selectable: true, addAttributes() { return graphicAttributes }, parseHTML() { return [{ tag: 'div[data-type="docs-graphic"]' }] }, renderHTML({ HTMLAttributes }) { return ["div", mergeAttributes(HTMLAttributes, { "data-type": "docs-graphic" })] }, addNodeView() { return ReactNodeViewRenderer(DocsGraphicNodeView) }, addCommands() { return graphicCommands() }, }) export const DocsInlineGraphic = Node.create({ name: "docsInlineGraphic", group: "inline", inline: true, atom: true, draggable: true, selectable: true, addAttributes() { return graphicAttributes }, parseHTML() { return [{ tag: 'span[data-type="docs-inline-graphic"]' }] }, renderHTML({ HTMLAttributes }) { return ["span", mergeAttributes(HTMLAttributes, { "data-type": "docs-inline-graphic" })] }, addNodeView() { return ReactNodeViewRenderer(DocsGraphicNodeView) }, }) export function buildInsertGraphicAttrs( graphicType: DocsGraphicType, partial: Partial = {} ): DocsGraphicAttrs { return mergeGraphicAttrs({ ...DOCS_GRAPHIC_DEFAULTS, graphicType, ...partial }) } export { mergeGraphicAttrs }