返回 presentation-ai
floating-toolbar.tsx
根目录 / src / components / plate / ui / floating-toolbar.tsx
1 "use client";
2
3 import { flip, offset, type FloatingToolbarState } from "@platejs/floating";
4 import { BlockSelectionPlugin } from "@platejs/selection/react";
5 import { TablePlugin } from "@platejs/table/react";
6 import { KEYS } from "platejs";
7 import {
8 useComposedRef,
9 useEditorId,
10 useEditorRef,
11 useEditorSelector,
12 useEventEditorValue,
13 usePluginOption,
14 } from "platejs/react";
15 import * as React from "react";
16
17 import { getDirectLayoutToolbarTargetEntry } from "@/components/notebook/presentation/editor/lib";
18 import { cn } from "@/lib/utils";
19 import { type MyEditor } from "../editor-kit";
20 import {
21 useFloatingToolbar,
22 useFloatingToolbarState,
23 } from "../hooks/use-floating-toolbar";
24 import { Toolbar } from "./toolbar";
25
26 export function FloatingToolbar({
27 children,
28 className,
29 state,
30 ...props
31 }: React.ComponentProps<typeof Toolbar> & {
32 state?: FloatingToolbarState;
33 }) {
34 const editorId = useEditorId();
35 const editor = useEditorRef<MyEditor>();
36 const focusedEditorId = useEventEditorValue("focus");
37 const isFloatingLinkOpen = !!usePluginOption({ key: KEYS.link }, "mode");
38 const isAIChatOpen = usePluginOption({ key: KEYS.aiChat }, "open");
39 const selectedCells = usePluginOption(TablePlugin, "selectedCells");
40 const hasSelectedTableCells = (selectedCells?.length ?? 0) > 0;
41 const isSelectionInTable = useEditorSelector(
42 (currentEditor) => currentEditor.api.some({ match: { type: KEYS.table } }),
43 [],
44 );
45
46 // Check if any blocks are selected
47 const selectedIds = usePluginOption(BlockSelectionPlugin, "selectedIds");
48 const hasBlockSelection = selectedIds && selectedIds.size > 0;
49
50 // Presentation layout blocks have their own toolbar. Native Plate blocks
51 // should still use the basic floating toolbar on block selection.
52 const isLayoutBlockSelected = React.useMemo(() => {
53 if (!hasBlockSelection || !selectedIds) return false;
54
55 for (const blockId of selectedIds) {
56 if (getDirectLayoutToolbarTargetEntry(editor, String(blockId))) {
57 return true;
58 }
59 }
60
61 return false;
62 }, [hasBlockSelection, selectedIds, editor]);
63 const isTableSelectionActive = isSelectionInTable || hasSelectedTableCells;
64
65 const floatingToolbarState = useFloatingToolbarState({
66 editorId,
67 focusedEditorId,
68 hideToolbar:
69 isLayoutBlockSelected ||
70 isTableSelectionActive ||
71 isFloatingLinkOpen ||
72 isAIChatOpen,
73 enableBlockSelection: true,
74 ...state,
75 floatingOptions: {
76 middleware: [
77 offset(12),
78 flip({
79 fallbackPlacements: [
80 "top-start",
81 "top-end",
82 "bottom-start",
83 "bottom-end",
84 ],
85 padding: 12,
86 }),
87 ],
88 placement: "top",
89 strategy: "fixed",
90 ...state?.floatingOptions,
91 },
92 });
93
94 const {
95 clickOutsideRef,
96 hidden,
97 props: rootProps,
98 ref: floatingRef,
99 } = useFloatingToolbar(floatingToolbarState);
100
101 const ref = useComposedRef<HTMLDivElement>(props.ref, floatingRef);
102
103 if (hidden) return null;
104
105 return (
106 <div ref={clickOutsideRef}>
107 <Toolbar
108 {...props}
109 {...rootProps}
110 ref={ref}
111 className={cn(
112 "z-999999 scrollbar-hide overflow-x-auto rounded-md border bg-popover p-1 whitespace-nowrap opacity-100 shadow-md print:hidden",
113 "max-w-[80vw]",
114 className,
115 )}
116 >
117 {children}
118 </Toolbar>
119 </div>
120 );
121 }
122
122 lines Plain Text