返回 presentation-ai
font-size-toolbar-button.tsx
根目录 / src / components / plate / ui / font-size-toolbar-button.tsx
1 "use client";
2
3 import { toUnitLess } from "@platejs/basic-styles";
4 import { FontSizePlugin } from "@platejs/basic-styles/react";
5 import { Minus, Plus } from "lucide-react";
6 import { KEYS, type NodeEntry, type TElement } from "platejs";
7 import { useEditorPlugin, useEditorSelector } from "platejs/react";
8 import * as React from "react";
9
10 import {
11 Popover,
12 PopoverContent,
13 PopoverTrigger,
14 } from "@/components/plate/ui/popover";
15 import { cn } from "@/lib/utils";
16 import { ToolbarButton } from "./toolbar";
17
18 const DEFAULT_FONT_SIZE = "16";
19
20 const FONT_SIZE_MAP = {
21 h1: "36",
22 h2: "24",
23 h3: "20",
24 } as const;
25
26 const FONT_SIZES = [
27 "8",
28 "9",
29 "10",
30 "12",
31 "14",
32 "16",
33 "18",
34 "24",
35 "30",
36 "36",
37 "48",
38 "60",
39 "72",
40 "96",
41 ] as const;
42
43 export function FontSizeToolbarButton() {
44 const [inputValue, setInputValue] = React.useState(DEFAULT_FONT_SIZE);
45 const [isFocused, setIsFocused] = React.useState(false);
46 const { editor, tf } = useEditorPlugin(FontSizePlugin);
47
48 const cursorFontSize = useEditorSelector((editor) => {
49 const fontSize = editor.api.marks()?.[KEYS.fontSize];
50
51 if (fontSize) {
52 return toUnitLess(fontSize as string);
53 }
54
55 const blockEntry = editor.api.block() as NodeEntry<TElement> | undefined;
56 const block = blockEntry?.[0];
57
58 if (!block?.type) return DEFAULT_FONT_SIZE;
59
60 return block.type in FONT_SIZE_MAP
61 ? FONT_SIZE_MAP[block.type as keyof typeof FONT_SIZE_MAP]
62 : DEFAULT_FONT_SIZE;
63 }, []);
64
65 const handleInputChange = () => {
66 const newSize = toUnitLess(inputValue);
67
68 if (
69 Number.parseInt(newSize, 10) < 1 ||
70 Number.parseInt(newSize, 10) > 100
71 ) {
72 editor.tf.focus();
73
74 return;
75 }
76 if (newSize !== toUnitLess(cursorFontSize)) {
77 tf.fontSize.addMark(`${newSize}px`);
78 }
79
80 editor.tf.focus();
81 };
82
83 const handleFontSizeChange = (delta: number) => {
84 const newSize = Number(displayValue) + delta;
85 tf.fontSize.addMark(`${newSize}px`);
86 editor.tf.focus();
87 };
88
89 const displayValue = isFocused ? inputValue : cursorFontSize;
90
91 return (
92 <div className="flex h-7 items-center gap-1 rounded-md bg-muted/60 p-0">
93 <ToolbarButton onClick={() => handleFontSizeChange(-1)}>
94 <Minus />
95 </ToolbarButton>
96
97 <Popover open={isFocused} modal={false}>
98 <PopoverTrigger asChild>
99 <input
100 aria-label="font size toolbar button control"
101 className={cn(
102 "h-full w-10 shrink-0 bg-transparent px-1 text-center text-sm hover:bg-muted",
103 )}
104 value={displayValue}
105 onBlur={() => {
106 setIsFocused(false);
107 handleInputChange();
108 }}
109 onChange={(e) => setInputValue(e.target.value)}
110 onFocus={() => {
111 setIsFocused(true);
112 setInputValue(toUnitLess(cursorFontSize));
113 }}
114 onKeyDown={(e) => {
115 if (e.key === "Enter") {
116 e.preventDefault();
117 handleInputChange();
118 }
119 }}
120 data-plate-focus="true"
121 type="text"
122 />
123 </PopoverTrigger>
124 <PopoverContent
125 className="w-10 px-px py-1"
126 onOpenAutoFocus={(e) => e.preventDefault()}
127 >
128 {FONT_SIZES.map((size) => (
129 <button
130 key={size}
131 className={cn(
132 "flex h-8 w-full items-center justify-center text-sm hover:bg-accent data-[highlighted=true]:bg-accent",
133 )}
134 onClick={() => {
135 tf.fontSize.addMark(`${size}px`);
136 setIsFocused(false);
137 }}
138 data-highlighted={size === displayValue}
139 type="button"
140 >
141 {size}
142 </button>
143 ))}
144 </PopoverContent>
145 </Popover>
146
147 <ToolbarButton onClick={() => handleFontSizeChange(1)}>
148 <Plus />
149 </ToolbarButton>
150 </div>
151 );
152 }
153
153 lines Plain Text