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

223 lines
7.0 KiB
TypeScript

"use client"
import { useRef } from "react"
import type { Editor } from "@tiptap/react"
import { Icon } from "@iconify/react"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Button } from "@/components/ui/button"
import { buildInsertGraphicAttrs } from "@/lib/drive/extensions/docs-graphic"
import {
DOCS_GRAPHIC_PLACEMENT_LABELS,
DOCS_GRAPHIC_WRAP_LABELS,
type DocsGraphicFloatSide,
type DocsGraphicPlacement,
type DocsGraphicWrap,
parseGraphicAttrs,
} from "@/lib/drive/docs-graphic-types"
function readSelectedGraphicAttrs(editor: Editor) {
if (editor.isActive("docsGraphic")) {
return parseGraphicAttrs(editor.getAttributes("docsGraphic") as Record<string, unknown>)
}
if (editor.isActive("docsInlineGraphic")) {
return parseGraphicAttrs(editor.getAttributes("docsInlineGraphic") as Record<string, unknown>)
}
return null
}
export function DocsGraphicInsertMenu({
editor,
disabled,
}: {
editor: Editor | null
disabled?: boolean
}) {
const imageInputRef = useRef<HTMLInputElement>(null)
if (!editor) return null
const insertImage = (file: File, options?: { wrap?: DocsGraphicWrap; placement?: DocsGraphicPlacement }) => {
const reader = new FileReader()
reader.onload = () => {
const src = reader.result as string
editor
.chain()
.focus()
.insertDocsGraphic(
buildInsertGraphicAttrs("image", {
src,
wrap: options?.wrap ?? "square",
placement: options?.placement ?? "block",
width: 280,
height: 180,
})
)
.run()
}
reader.readAsDataURL(file)
}
return (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
className="docs-toolbar-btn size-7 shrink-0"
disabled={disabled}
aria-label="Insérer un élément graphique"
>
<Icon icon="material-symbols:image-outline" className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="min-w-56">
<DropdownMenuItem onClick={() => imageInputRef.current?.click()}>
Image
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>Forme</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
{(["rect", "ellipse", "line", "arrow"] as const).map((shapeType) => (
<DropdownMenuItem
key={shapeType}
onClick={() =>
editor
.chain()
.focus()
.insertDocsGraphic(buildInsertGraphicAttrs("shape", { shapeType }))
.run()
}
>
{shapeType === "rect"
? "Rectangle"
: shapeType === "ellipse"
? "Ellipse"
: shapeType === "line"
? "Ligne"
: "Flèche"}
</DropdownMenuItem>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuItem
onClick={() =>
editor
.chain()
.focus()
.insertDocsGraphic(buildInsertGraphicAttrs("gradient"))
.run()
}
>
Dégradé
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<input
ref={imageInputRef}
type="file"
accept="image/*"
className="hidden"
onChange={(event) => {
const file = event.target.files?.[0]
if (file) insertImage(file)
event.target.value = ""
}}
/>
</>
)
}
export function DocsGraphicLayoutMenu({
editor,
disabled,
}: {
editor: Editor | null
disabled?: boolean
}) {
if (!editor) return null
const attrs = readSelectedGraphicAttrs(editor)
if (!attrs) return null
const applyWrap = (wrap: DocsGraphicWrap) => {
editor.chain().focus().setDocsGraphicWrap(wrap).run()
}
const applyPlacement = (placement: DocsGraphicPlacement) => {
editor.chain().focus().setDocsGraphicPlacement(placement).run()
}
const applyFloatSide = (floatSide: DocsGraphicFloatSide) => {
editor.chain().focus().setDocsGraphicFloatSide(floatSide).run()
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
className="docs-toolbar-btn docs-toolbar-btn--active size-7 shrink-0"
disabled={disabled}
aria-label="Disposition de l'élément graphique"
>
<Icon icon="material-symbols:layers-outline" className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="min-w-64">
<DropdownMenuSub>
<DropdownMenuSubTrigger>Habillage texte</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
{(Object.keys(DOCS_GRAPHIC_WRAP_LABELS) as DocsGraphicWrap[]).map((wrap) => (
<DropdownMenuItem key={wrap} onClick={() => applyWrap(wrap)}>
{DOCS_GRAPHIC_WRAP_LABELS[wrap]}
{attrs.wrap === wrap ? " ✓" : ""}
</DropdownMenuItem>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger>Placement</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
{(Object.keys(DOCS_GRAPHIC_PLACEMENT_LABELS) as DocsGraphicPlacement[]).map(
(placement) => (
<DropdownMenuItem key={placement} onClick={() => applyPlacement(placement)}>
{DOCS_GRAPHIC_PLACEMENT_LABELS[placement]}
{attrs.placement === placement ? " ✓" : ""}
</DropdownMenuItem>
)
)}
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger>Côté du flottement</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
{(["left", "right", "center"] as const).map((side) => (
<DropdownMenuItem key={side} onClick={() => applyFloatSide(side)}>
{side === "left" ? "Gauche" : side === "right" ? "Droite" : "Centre"}
{attrs.floatSide === side ? " ✓" : ""}
</DropdownMenuItem>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuContent>
</DropdownMenu>
)
}
export function readGraphicToolbarActive(editor: Editor | null): boolean {
if (!editor) return false
return editor.isActive("docsGraphic") || editor.isActive("docsInlineGraphic")
}