ultisuite-client/lib/drive/extensions/docs-graphic.ts
R3D347HR4Y 2a7c153748
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
wrap page
2026-06-10 12:48:27 +02:00

241 lines
7.5 KiB
TypeScript

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<ReturnType> {
docsGraphic: {
insertDocsGraphic: (attrs: Partial<DocsGraphicAttrs>) => ReturnType
updateDocsGraphic: (attrs: Partial<DocsGraphicAttrs>) => 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>): 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<DocsGraphicAttrs>) =>
({ 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<DocsGraphicAttrs>) =>
({
chain,
editor,
}: {
chain: () => {
updateAttributes: (name: string, attrs: Partial<DocsGraphicAttrs>) => { 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<string, unknown> }
}) => {
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<string, unknown> }
}) => {
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> = {}
): DocsGraphicAttrs {
return mergeGraphicAttrs({ ...DOCS_GRAPHIC_DEFAULTS, graphicType, ...partial })
}
export { mergeGraphicAttrs }