"use client" import { memo, useCallback, useEffect, useRef, useState } from "react" import { createPortal } from "react-dom" import type { Editor } from "@tiptap/react" import { Icon } from "@iconify/react" import { ArrowDown, ArrowUp, Check, ChevronDown, Crop, MoreHorizontal, Settings2, Shapes, } from "lucide-react" import { Button } from "@/components/ui/button" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { DOCS_GRAPHIC_WRAP_MARGIN_PRESETS_MM } from "@/lib/drive/docs-graphic-layout" import { usesPageLayer } from "@/lib/drive/docs-graphic-position" import { DOCS_GRAPHIC_WRAP_LABELS, parseGraphicAttrs, type DocsGraphicFloatSide, type DocsGraphicPositionMode, type DocsGraphicWrap, } from "@/lib/drive/docs-graphic-types" import { DOCS_GRAPHIC_ADVANCED_WRAP_ACTIONS, DOCS_GRAPHIC_FLOATING_WRAP_ACTIONS, DOCS_GRAPHIC_FLOAT_SIDE_ICONS, DOCS_GRAPHIC_FLOAT_SIDE_LABELS, DOCS_GRAPHIC_LAYER_WRAP_ACTIONS, DOCS_GRAPHIC_POSITION_MODE_SHORT_LABELS, DOCS_GRAPHIC_WRAP_ICONS, } from "@/lib/drive/docs-graphic-ui" import { openDocsGraphicDrawEditor } from "@/lib/drive/docs-graphic-draw-bridge" import { readGraphicToolbarActive } from "@/components/drive/richtext/docs-graphic-toolbar-menu" import { openDocsGraphicOptionsSidebar } from "@/components/drive/richtext/docs-graphic-options-sidebar" import { applyDocsGraphicCrop, DOCS_GRAPHIC_CROP_CHANGED_EVENT, readDocsGraphicCropActive, startDocsGraphicCrop, } from "@/lib/drive/docs-graphic-crop-bridge" import { cn } from "@/lib/utils" function readSelectedGraphic(editor: Editor) { const name = editor.isActive("docsInlineGraphic") ? "docsInlineGraphic" : editor.isActive("docsGraphic") ? "docsGraphic" : null if (!name) return null return { name, attrs: parseGraphicAttrs(editor.getAttributes(name) as Record), } } function ToolbarDivider() { return } function keepEditorSelectionOnMenuClose(event: Event) { event.preventDefault() } function IconToolbarButton({ label, active, onClick, children, }: { label: string active?: boolean onClick: () => void children: React.ReactNode }) { return ( ) } function DocsGraphicFloatingToolbarInner({ editor, canvasRef, disabled, }: { editor: Editor | null canvasRef: React.RefObject disabled?: boolean }) { const [rect, setRect] = useState(null) const [cropActive, setCropActive] = useState(false) const [, setTick] = useState(0) const replaceInputRef = useRef(null) const refresh = useCallback(() => { setTick((t) => t + 1) if (!editor || !readGraphicToolbarActive(editor)) { setRect(null) return } const selected = document.querySelector( ".docs-graphic--selected, .ProseMirror-selectednode .docs-graphic" ) as HTMLElement | null if (!selected) { setRect(null) return } setRect(selected.getBoundingClientRect()) setCropActive(readDocsGraphicCropActive()) }, [editor]) useEffect(() => { if (!editor) return refresh() editor.on("selectionUpdate", refresh) editor.on("transaction", refresh) const canvas = canvasRef.current canvas?.addEventListener("scroll", refresh, { passive: true }) window.addEventListener("resize", refresh) window.addEventListener(DOCS_GRAPHIC_CROP_CHANGED_EVENT, refresh) return () => { editor.off("selectionUpdate", refresh) editor.off("transaction", refresh) canvas?.removeEventListener("scroll", refresh) window.removeEventListener("resize", refresh) window.removeEventListener(DOCS_GRAPHIC_CROP_CHANGED_EVENT, refresh) } }, [canvasRef, editor, refresh]) if (!editor || disabled || !readGraphicToolbarActive(editor) || !rect) return null const graphic = readSelectedGraphic(editor) if (!graphic) return null const { attrs } = graphic const isImage = attrs.graphicType === "image" const isGradient = attrs.graphicType === "gradient" const isDraw = attrs.graphicType === "draw" || attrs.graphicType === "shape" const moveWithText = attrs.positionMode === "move-with-text" const showInFlowWrap = moveWithText const showPageLayerActions = usesPageLayer(attrs) const showAlign = moveWithText const run = (fn: () => void) => fn() const applyWrap = (wrap: DocsGraphicWrap) => { run(() => editor.chain().setDocsGraphicWrap(wrap).run()) } const applyPositionMode = (positionMode: DocsGraphicPositionMode) => { run(() => editor.chain().setDocsGraphicPositionMode(positionMode).run()) } const applyMargin = (wrapMarginMm: number) => { run(() => editor.chain().setDocsGraphicWrapMargin(wrapMarginMm).run()) } const applyFloatSide = (floatSide: DocsGraphicFloatSide) => { run(() => editor.chain().setDocsGraphicFloatSide(floatSide).run()) } const downloadImage = () => { if (!attrs.src) return const link = document.createElement("a") link.href = attrs.src link.download = "image" link.click() } const toolbar = (
{isImage || isDraw ? ( ) : null} {showAlign ? ( <> {(Object.keys(DOCS_GRAPHIC_FLOAT_SIDE_LABELS) as DocsGraphicFloatSide[]).map( (side) => ( applyFloatSide(side)}> {DOCS_GRAPHIC_FLOAT_SIDE_LABELS[side]} {attrs.floatSide === side ? " ✓" : ""} ) )} ) : null} {showInFlowWrap ? ( <> {DOCS_GRAPHIC_FLOATING_WRAP_ACTIONS.map(({ wrap, label }) => ( applyWrap(wrap)}> {label} {attrs.wrap === wrap ? " ✓" : ""} ))} {DOCS_GRAPHIC_ADVANCED_WRAP_ACTIONS.map(({ wrap, label }) => ( applyWrap(wrap)}> {label} {attrs.wrap === wrap ? " ✓" : ""} ))} ) : null} {showPageLayerActions ? ( <> {DOCS_GRAPHIC_LAYER_WRAP_ACTIONS.map(({ wrap, label }) => ( applyWrap(wrap)} > ))} ) : null} {DOCS_GRAPHIC_WRAP_MARGIN_PRESETS_MM.map((mm) => ( applyMargin(mm)}> Marge de {mm}mm{attrs.wrapMarginMm === mm ? " ✓" : ""} ))} {(["move-with-text", "fixed-on-page"] as DocsGraphicPositionMode[]).map((mode) => ( applyPositionMode(mode)}> {DOCS_GRAPHIC_POSITION_MODE_SHORT_LABELS[mode]} {attrs.positionMode === mode ? " ✓" : ""} ))} {showPageLayerActions ? ( <> run(() => editor.chain().bringDocsGraphicForward().run())} > run(() => editor.chain().sendDocsGraphicBackward().run())} > ) : null} {isImage ? ( <> { const alt = window.prompt("Texte alternatif", attrs.alt) if (alt != null) run(() => editor.chain().updateDocsGraphic({ alt }).run()) }} > Texte alternatif… replaceInputRef.current?.click()}> Remplacer l'image… Télécharger l'image ) : null} run(() => editor.chain().focus().deleteSelection().run())} > Supprimer { const file = event.target.files?.[0] if (!file) return const reader = new FileReader() reader.onload = () => { run(() => editor.chain().focus().updateDocsGraphic({ src: reader.result as string }).run() ) } reader.readAsDataURL(file) event.target.value = "" }} />
) return createPortal(toolbar, document.body) } export const DocsGraphicFloatingToolbar = memo(DocsGraphicFloatingToolbarInner)