返回 presentation-ai
turn-into-toolbar-button.tsx
根目录 / src / components / plate / ui / turn-into-toolbar-button.tsx
1 "use client";
2
3 import { BlockSelectionPlugin } from "@platejs/selection/react";
4 import {
5 DropdownMenuItemIndicator,
6 type DropdownMenuProps,
7 } from "@radix-ui/react-dropdown-menu";
8 import {
9 CheckIcon,
10 ChevronRightIcon,
11 Columns3Icon,
12 FileCodeIcon,
13 Heading1Icon,
14 Heading2Icon,
15 Heading3Icon,
16 Heading4Icon,
17 Heading5Icon,
18 Heading6Icon,
19 ListIcon,
20 ListOrderedIcon,
21 PilcrowIcon,
22 QuoteIcon,
23 SquareIcon,
24 } from "lucide-react";
25 import { KEYS, type TElement } from "platejs";
26 import {
27 useEditorRef,
28 useEditorSelector,
29 usePluginOption,
30 useSelectionFragmentProp,
31 } from "platejs/react";
32 import * as React from "react";
33
34 import { PRESENTATION_TITLE_ELEMENT } from "@/components/notebook/presentation/editor/lib";
35 import {
36 DropdownMenu,
37 DropdownMenuContent,
38 DropdownMenuRadioItem,
39 DropdownMenuTrigger,
40 } from "@/components/plate/ui/dropdown-menu";
41 import {
42 getBlockType,
43 setBlockType,
44 } from "@/components/plate/utils/transforms";
45 import { ToolbarButton, ToolbarMenuGroup } from "./toolbar";
46
47 const turnIntoItems = [
48 {
49 icon: <Heading1Icon />,
50 keywords: ["title", "!"],
51 label: "Title",
52 props: { variant: "title" },
53 type: PRESENTATION_TITLE_ELEMENT,
54 value: PRESENTATION_TITLE_ELEMENT,
55 },
56 {
57 icon: <Heading2Icon />,
58 keywords: ["display", "!!"],
59 label: "Display",
60 props: { variant: "display" },
61 type: PRESENTATION_TITLE_ELEMENT,
62 value: "presentation-title-display",
63 },
64 {
65 icon: <Heading3Icon />,
66 keywords: ["humongous", "!!!"],
67 label: "Humongous",
68 props: { variant: "humongous" },
69 type: PRESENTATION_TITLE_ELEMENT,
70 value: "presentation-title-humongous",
71 },
72 {
73 icon: <PilcrowIcon />,
74 keywords: ["paragraph"],
75 label: "Text",
76 type: KEYS.p,
77 value: KEYS.p,
78 },
79 {
80 icon: <Heading1Icon />,
81 keywords: ["title", "h1"],
82 label: "Heading 1",
83 type: "h1",
84 value: "h1",
85 },
86 {
87 icon: <Heading2Icon />,
88 keywords: ["subtitle", "h2"],
89 label: "Heading 2",
90 type: "h2",
91 value: "h2",
92 },
93 {
94 icon: <Heading3Icon />,
95 keywords: ["subtitle", "h3"],
96 label: "Heading 3",
97 type: "h3",
98 value: "h3",
99 },
100 {
101 icon: <Heading4Icon />,
102 keywords: ["subtitle", "h4"],
103 label: "Heading 4",
104 type: "h4",
105 value: "h4",
106 },
107 {
108 icon: <Heading5Icon />,
109 keywords: ["subtitle", "h5"],
110 label: "Heading 5",
111 type: "h5",
112 value: "h5",
113 },
114 {
115 icon: <Heading6Icon />,
116 keywords: ["subtitle", "h6"],
117 label: "Heading 6",
118 type: "h6",
119 value: "h6",
120 },
121 {
122 icon: <ListIcon />,
123 keywords: ["unordered", "ul", "-"],
124 label: "Bulleted list",
125 type: KEYS.ul,
126 value: KEYS.ul,
127 },
128 {
129 icon: <ListOrderedIcon />,
130 keywords: ["ordered", "ol", "1"],
131 label: "Numbered list",
132 type: KEYS.ol,
133 value: KEYS.ol,
134 },
135 {
136 icon: <SquareIcon />,
137 keywords: ["checklist", "task", "checkbox", "[]"],
138 label: "To-do list",
139 type: KEYS.listTodo,
140 value: KEYS.listTodo,
141 },
142 {
143 icon: <ChevronRightIcon />,
144 keywords: ["collapsible", "expandable"],
145 label: "Toggle list",
146 type: KEYS.toggle,
147 value: KEYS.toggle,
148 },
149 {
150 icon: <FileCodeIcon />,
151 keywords: ["```"],
152 label: "Code",
153 type: KEYS.codeBlock,
154 value: KEYS.codeBlock,
155 },
156 {
157 icon: <QuoteIcon />,
158 keywords: ["citation", "blockquote", ">"],
159 label: "Quote",
160 type: KEYS.blockquote,
161 value: KEYS.blockquote,
162 },
163 {
164 icon: <Columns3Icon />,
165 label: "3 columns",
166 type: "action_three_columns",
167 value: "action_three_columns",
168 },
169 ];
170
171 function getTurnIntoValue(block: TElement) {
172 if (getBlockType(block) !== PRESENTATION_TITLE_ELEMENT) {
173 return getBlockType(block);
174 }
175
176 if (block.variant === "humongous") return "presentation-title-humongous";
177 if (block.variant === "display") return "presentation-title-display";
178 return PRESENTATION_TITLE_ELEMENT;
179 }
180
181 export function TurnIntoToolbarButton(props: DropdownMenuProps) {
182 const editor = useEditorRef();
183 const [open, setOpen] = React.useState(false);
184 const selectedIds = usePluginOption(BlockSelectionPlugin, "selectedIds");
185
186 const selectionValue = useSelectionFragmentProp({
187 defaultValue: KEYS.p,
188 getProp: (node) => getTurnIntoValue(node as TElement),
189 });
190 const activeBlockValue = useEditorSelector((currentEditor) => {
191 const activeBlock = currentEditor.api.block();
192
193 return activeBlock
194 ? getTurnIntoValue(activeBlock[0] as TElement)
195 : undefined;
196 }, []);
197 const selectedBlockValue = React.useMemo(() => {
198 if (!selectedIds || selectedIds.size === 0) return undefined;
199
200 const selectedValues = Array.from(selectedIds)
201 .map((id) => editor.api.node({ id, at: [] })?.[0])
202 .filter((node): node is TElement => Boolean(node))
203 .map((node) => getTurnIntoValue(node));
204
205 const [firstValue] = selectedValues;
206 if (!firstValue) return undefined;
207
208 return selectedValues.every((itemValue) => itemValue === firstValue)
209 ? firstValue
210 : undefined;
211 }, [editor, selectedIds]);
212 const value = selectedBlockValue ?? activeBlockValue ?? selectionValue;
213 const selectedItem = React.useMemo(
214 () =>
215 turnIntoItems.find((item) => item.value === (value ?? KEYS.p)) ??
216 turnIntoItems[0],
217 [value],
218 );
219
220 return (
221 <DropdownMenu open={open} onOpenChange={setOpen} modal={false} {...props}>
222 <DropdownMenuTrigger asChild>
223 <ToolbarButton
224 className="min-w-31.25"
225 pressed={open}
226 tooltip="Turn into"
227 isDropdown
228 >
229 {selectedItem!.label}
230 </ToolbarButton>
231 </DropdownMenuTrigger>
232
233 <DropdownMenuContent
234 className="ignore-click-outside/toolbar min-w-0"
235 onCloseAutoFocus={(e) => {
236 e.preventDefault();
237 editor.tf.focus();
238 }}
239 align="start"
240 >
241 <ToolbarMenuGroup
242 value={value}
243 onValueChange={(value) => {
244 const item = turnIntoItems.find(
245 (turnIntoItem) => turnIntoItem.value === value,
246 );
247 if (!item) return;
248
249 setBlockType(editor, item.type, {
250 props: item.props,
251 });
252 }}
253 label="Turn into"
254 >
255 {turnIntoItems.map(({ icon, label, value: itemValue }) => (
256 <DropdownMenuRadioItem
257 key={itemValue}
258 className="min-w-45 pl-2 [span]:first:*:hidden"
259 value={itemValue}
260 >
261 <span className="pointer-events-none absolute right-2 flex size-3.5 items-center justify-center">
262 <DropdownMenuItemIndicator>
263 <CheckIcon />
264 </DropdownMenuItemIndicator>
265 </span>
266 {icon}
267 {label}
268 </DropdownMenuRadioItem>
269 ))}
270 </ToolbarMenuGroup>
271 </DropdownMenuContent>
272 </DropdownMenu>
273 );
274 }
275
275 lines Plain Text