ultisuite-client/components/drive/richtext/docs-graphic-toolbar-menu.tsx
R3D347HR4Y 303b2b1074
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
wow
2026-06-11 01:22:40 +02:00

250 lines
7.9 KiB
TypeScript

"use client"
import { useRef, useState } from "react"
import type { Editor } from "@tiptap/react"
import { Icon } from "@iconify/react"
import { ImageIcon, Pencil, Sparkles } from "lucide-react"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Button } from "@/components/ui/button"
import { DocsDriveImagePickerDialog } from "@/components/drive/richtext/docs-drive-image-picker-dialog"
import { openDocsGraphicDrawModal } from "@/components/drive/richtext/docs-graphic-draw-modal"
import {
buildImageInsertGraphicAttrs,
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"
import { suitePublicAsset } from "@/lib/suite/suite-public-asset"
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)
const [drivePickerOpen, setDrivePickerOpen] = useState(false)
if (!editor) return null
const insertImageSrc = (
src: string,
options?: { wrap?: DocsGraphicWrap; placement?: DocsGraphicPlacement; alt?: string }
) => {
void buildImageInsertGraphicAttrs({
src,
alt: options?.alt ?? "",
wrap: options?.wrap ?? "square",
placement: options?.placement ?? "block",
}).then((attrs) => {
editor.chain().focus().insertDocsGraphic(attrs).run()
})
}
const insertImageFile = (
file: File,
options?: { wrap?: DocsGraphicWrap; placement?: DocsGraphicPlacement }
) => {
const reader = new FileReader()
reader.onload = () => {
const src = reader.result as string
insertImageSrc(src, options)
}
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-64">
<DropdownMenuItem onClick={() => setDrivePickerOpen(true)}>
<img
src={suitePublicAsset("/ultidrive-mark.svg")}
alt=""
className="size-4 shrink-0"
aria-hidden
/>
Image depuis Drive
</DropdownMenuItem>
<DropdownMenuItem onClick={() => imageInputRef.current?.click()}>
<ImageIcon className="size-4 shrink-0" />
Image depuis l&apos;ordinateur
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
editor
.chain()
.focus()
.insertDocsGraphic(
buildInsertGraphicAttrs("draw", { width: 320, height: 240 })
)
.run()
openDocsGraphicDrawModal()
}}
>
<Pencil className="size-4 shrink-0" />
Dessin
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
editor
.chain()
.focus()
.insertDocsGraphic(buildInsertGraphicAttrs("gradient"))
.run()
}
>
<Sparkles className="size-4 shrink-0" />
Dégradé
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<DocsDriveImagePickerDialog
open={drivePickerOpen}
onOpenChange={setDrivePickerOpen}
onPickImage={(src, file) => {
insertImageSrc(src, { alt: file.name })
}}
/>
<input
ref={imageInputRef}
type="file"
accept="image/*"
className="hidden"
onChange={(event) => {
const file = event.target.files?.[0]
if (file) insertImageFile(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().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"
onCloseAutoFocus={(event) => event.preventDefault()}
>
<DropdownMenuSub>
<DropdownMenuSubTrigger>Habillage texte</DropdownMenuSubTrigger>
<DropdownMenuSubContent onCloseAutoFocus={(event) => event.preventDefault()}>
{(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")
}