250 lines
7.9 KiB
TypeScript
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'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")
|
|
}
|