返回 presentation-ai
highlight-toolbar-button.tsx
根目录 / src / components / plate / ui / highlight-toolbar-button.tsx
1 "use client";
2
3 import { BlockSelectionPlugin } from "@platejs/selection/react";
4 import { HighlighterIcon } from "lucide-react";
5 import { KEYS } from "platejs";
6 import { useEditorRef, useEditorSelector } from "platejs/react";
7 import * as React from "react";
8
9 import {
10 DropdownMenu,
11 DropdownMenuContent,
12 DropdownMenuTrigger,
13 } from "@/components/plate/ui/dropdown-menu";
14 import {
15 Tooltip,
16 TooltipContent,
17 TooltipProvider,
18 TooltipTrigger,
19 } from "@/components/plate/ui/tooltip";
20 import { cn } from "@/lib/utils";
21 import {
22 ToolbarSplitButton,
23 ToolbarSplitButtonPrimary,
24 ToolbarSplitButtonSecondary,
25 } from "./toolbar";
26
27 // Default yellow highlight color
28 const DEFAULT_HIGHLIGHT_COLOR = "#FEFF00";
29
30 // Highlight color options - lighter/pastel colors suitable for highlighting
31 const HIGHLIGHT_COLORS = [
32 {
33 isBrightColor: true,
34 name: "yellow",
35 value: "#FEFF00",
36 },
37 {
38 isBrightColor: true,
39 name: "light yellow",
40 value: "#FFF2CC",
41 },
42 {
43 isBrightColor: true,
44 name: "light orange",
45 value: "#FCE4CD",
46 },
47 {
48 isBrightColor: true,
49 name: "light green",
50 value: "#D9EAD3",
51 },
52 {
53 isBrightColor: true,
54 name: "light cyan",
55 value: "#D0DFE3",
56 },
57 {
58 isBrightColor: true,
59 name: "light blue",
60 value: "#CFE1F3",
61 },
62 {
63 isBrightColor: true,
64 name: "light purple",
65 value: "#D9D2E9",
66 },
67 {
68 isBrightColor: true,
69 name: "light pink",
70 value: "#EAD1DB",
71 },
72 {
73 isBrightColor: false,
74 name: "orange",
75 value: "#F9CB9C",
76 },
77 {
78 isBrightColor: false,
79 name: "green",
80 value: "#B7D6A8",
81 },
82 {
83 isBrightColor: false,
84 name: "cyan",
85 value: "#A1C4C9",
86 },
87 {
88 isBrightColor: false,
89 name: "blue",
90 value: "#A4C2F4",
91 },
92 {
93 isBrightColor: false,
94 name: "purple",
95 value: "#B5A7D5",
96 },
97 {
98 isBrightColor: false,
99 name: "pink",
100 value: "#D5A6BD",
101 },
102 {
103 isBrightColor: false,
104 name: "red",
105 value: "#EA9999",
106 },
107 {
108 isBrightColor: false,
109 name: "light red",
110 value: "#F4CCCC",
111 },
112 ];
113
114 export function HighlightToolbarButton() {
115 const editor = useEditorRef();
116 const [open, setOpen] = React.useState(false);
117
118 const currentColor = useEditorSelector(
119 (editor) => editor.api.mark(KEYS.backgroundColor) as string | undefined,
120 [],
121 );
122
123 const isHighlighted = !!currentColor;
124
125 const applyHighlight = React.useCallback(
126 (color: string) => {
127 const blockSelectionApi =
128 editor.getApi(BlockSelectionPlugin).blockSelection;
129 const selectedBlocks = blockSelectionApi
130 ? blockSelectionApi.getNodes()
131 : [];
132
133 if (selectedBlocks.length > 0) {
134 selectedBlocks.forEach(([_, path]) => {
135 editor.tf.setNodes(
136 { [KEYS.backgroundColor]: color },
137 { at: path, match: (n) => editor.api.isText(n), mode: "all" },
138 );
139 });
140 return;
141 }
142
143 if (editor.selection) {
144 editor.tf.select(editor.selection);
145 editor.tf.focus();
146 editor.tf.addMarks({ [KEYS.backgroundColor]: color });
147 }
148 },
149 [editor],
150 );
151
152 const removeHighlight = React.useCallback(() => {
153 const blockSelectionApi =
154 editor.getApi(BlockSelectionPlugin).blockSelection;
155 const selectedBlocks = blockSelectionApi
156 ? blockSelectionApi.getNodes()
157 : [];
158
159 if (selectedBlocks.length > 0) {
160 selectedBlocks.forEach(([_, path]) => {
161 editor.tf.unsetNodes(KEYS.backgroundColor, {
162 at: path,
163 match: (n) => editor.api.isText(n),
164 mode: "all",
165 });
166 });
167 return;
168 }
169
170 if (editor.selection) {
171 editor.tf.select(editor.selection);
172 editor.tf.focus();
173 editor.tf.removeMarks(KEYS.backgroundColor);
174 }
175 }, [editor]);
176
177 const handlePrimaryClick = React.useCallback(() => {
178 if (isHighlighted) {
179 removeHighlight();
180 } else {
181 applyHighlight(DEFAULT_HIGHLIGHT_COLOR);
182 }
183 }, [isHighlighted, applyHighlight, removeHighlight]);
184
185 const handleColorSelect = React.useCallback(
186 (color: string) => {
187 applyHighlight(color);
188 setOpen(false);
189 },
190 [applyHighlight],
191 );
192
193 return (
194 <ToolbarSplitButton pressed={open}>
195 <Tooltip>
196 <TooltipTrigger asChild>
197 <ToolbarSplitButtonPrimary
198 className="data-[state=on]:bg-accent data-[state=on]:text-accent-foreground"
199 onClick={handlePrimaryClick}
200 data-state={isHighlighted ? "on" : "off"}
201 >
202 <HighlighterIcon className="size-4" />
203 </ToolbarSplitButtonPrimary>
204 </TooltipTrigger>
205 <TooltipContent>Highlight</TooltipContent>
206 </Tooltip>
207
208 <DropdownMenu open={open} onOpenChange={setOpen} modal={false}>
209 <DropdownMenuTrigger asChild>
210 <ToolbarSplitButtonSecondary />
211 </DropdownMenuTrigger>
212
213 <DropdownMenuContent
214 className="ignore-click-outside/toolbar p-2"
215 align="start"
216 alignOffset={-32}
217 >
218 <TooltipProvider>
219 <div className="grid grid-cols-4 gap-1">
220 {HIGHLIGHT_COLORS.map((color) => (
221 <Tooltip key={color.value}>
222 <TooltipTrigger asChild>
223 <button
224 type="button"
225 className={cn(
226 "size-6 rounded border transition-transform hover:scale-110",
227 color.isBrightColor
228 ? "border-muted"
229 : "border-transparent",
230 currentColor === color.value &&
231 "ring-2 ring-primary ring-offset-1",
232 )}
233 style={{ backgroundColor: color.value }}
234 onClick={() => handleColorSelect(color.value)}
235 >
236 <span className="sr-only">{color.name}</span>
237 </button>
238 </TooltipTrigger>
239 <TooltipContent className="capitalize">
240 {color.name}
241 </TooltipContent>
242 </Tooltip>
243 ))}
244 </div>
245 {currentColor && (
246 <button
247 type="button"
248 className="mt-2 flex w-full items-center justify-center gap-1 rounded border border-muted px-2 py-1 text-xs hover:bg-muted"
249 onClick={() => {
250 removeHighlight();
251 setOpen(false);
252 }}
253 >
254 Remove highlight
255 </button>
256 )}
257 </TooltipProvider>
258 </DropdownMenuContent>
259 </DropdownMenu>
260 </ToolbarSplitButton>
261 );
262 }
263
263 lines Plain Text