返回 presentation-ai
equation-node.tsx
根目录 / src / components / plate / ui / equation-node.tsx
1 "use client";
2
3 import { useEquationElement, useEquationInput } from "@platejs/math/react";
4 import { BlockSelectionPlugin } from "@platejs/selection/react";
5 import { CornerDownLeftIcon, RadicalIcon } from "lucide-react";
6 import { type TEquationElement } from "platejs";
7 import {
8 createPrimitiveComponent,
9 PlateElement,
10 useEditorRef,
11 useEditorSelector,
12 useElement,
13 useReadOnly,
14 useSelected,
15 type PlateElementProps,
16 } from "platejs/react";
17 import * as React from "react";
18 import TextareaAutosize, {
19 type TextareaAutosizeProps,
20 } from "react-textarea-autosize";
21
22 import { Button } from "@/components/plate/ui/button";
23 import {
24 Popover,
25 PopoverContent,
26 PopoverTrigger,
27 } from "@/components/plate/ui/popover";
28 import { cn } from "@/lib/utils";
29
30 export function EquationElement(props: PlateElementProps<TEquationElement>) {
31 const selected = useSelected();
32 const [open, setOpen] = React.useState(selected);
33 const katexRef = React.useRef<HTMLDivElement | null>(null);
34
35 useEquationElement({
36 element: props.element,
37 katexRef: katexRef,
38 options: {
39 displayMode: true,
40 errorColor: "#cc0000",
41 fleqn: false,
42 leqno: false,
43 macros: { "\\f": "#1f(#2)" },
44 output: "htmlAndMathml",
45 strict: "warn",
46 throwOnError: false,
47 trust: false,
48 },
49 });
50
51 return (
52 <PlateElement className="my-1" {...props}>
53 <Popover open={open} onOpenChange={setOpen} modal={false}>
54 <PopoverTrigger asChild>
55 <button
56 type="button"
57 className={cn(
58 "group flex cursor-pointer items-center justify-center rounded-sm select-none hover:bg-primary/10 data-[selected=true]:bg-primary/10",
59 props.element.texExpression.length === 0
60 ? "bg-muted p-3 pr-9"
61 : "px-2 py-1",
62 )}
63 data-selected={selected}
64 contentEditable={false}
65 >
66 {props.element.texExpression.length > 0 ? (
67 <span ref={katexRef} />
68 ) : (
69 <div className="flex h-7 w-full items-center gap-2 text-sm whitespace-nowrap text-muted-foreground">
70 <RadicalIcon className="size-6 text-muted-foreground/80" />
71 <div>Add a Tex equation</div>
72 </div>
73 )}
74 </button>
75 </PopoverTrigger>
76
77 <EquationPopoverContent
78 open={open}
79 placeholder={`f(x) = \\begin{cases}\n x^2, &\\quad x > 0 \\\\\n 0, &\\quad x = 0 \\\\\n -x^2, &\\quad x < 0\n\\end{cases}`}
80 isInline={false}
81 setOpen={setOpen}
82 />
83 </Popover>
84
85 {props.children}
86 </PlateElement>
87 );
88 }
89
90 export function InlineEquationElement(
91 props: PlateElementProps<TEquationElement>,
92 ) {
93 const element = props.element;
94 const katexRef = React.useRef<HTMLDivElement | null>(null);
95 const selected = useSelected();
96 const isCollapsed = useEditorSelector(
97 (editor) => editor.api.isCollapsed(),
98 [],
99 );
100 const [open, setOpen] = React.useState(selected && isCollapsed);
101
102 React.useEffect(() => {
103 if (selected && isCollapsed) {
104 setOpen(true);
105 }
106 }, [selected, isCollapsed]);
107
108 useEquationElement({
109 element,
110 katexRef: katexRef,
111 options: {
112 displayMode: true,
113 errorColor: "#cc0000",
114 fleqn: false,
115 leqno: false,
116 macros: { "\\f": "#1f(#2)" },
117 output: "htmlAndMathml",
118 strict: "warn",
119 throwOnError: false,
120 trust: false,
121 },
122 });
123
124 return (
125 <PlateElement
126 {...props}
127 className={cn(
128 "mx-1 inline-block rounded-sm select-none [&_.katex-display]:my-0!",
129 )}
130 >
131 <Popover open={open} onOpenChange={setOpen} modal={false}>
132 <PopoverTrigger asChild>
133 <div
134 className={cn(
135 'after:absolute after:inset-0 after:-top-0.5 after:-left-1 after:z-1 after:h-[calc(100%)+4px] after:w-[calc(100%+8px)] after:rounded-sm after:content-[""]',
136 "h-6",
137 ((element.texExpression.length > 0 && open) || selected) &&
138 "after:bg-brand/15",
139 element.texExpression.length === 0 &&
140 "text-muted-foreground after:bg-neutral-500/10",
141 )}
142 contentEditable={false}
143 >
144 <span
145 ref={katexRef}
146 className={cn(
147 element.texExpression.length === 0 && "hidden",
148 "font-mono leading-none",
149 )}
150 />
151 {element.texExpression.length === 0 && (
152 <span>
153 <RadicalIcon className="mr-1 inline-block h-4.75 w-4 py-[1.5px] align-text-bottom" />
154 New equation
155 </span>
156 )}
157 </div>
158 </PopoverTrigger>
159
160 <EquationPopoverContent
161 className="my-auto"
162 open={open}
163 placeholder="E = mc^2"
164 setOpen={setOpen}
165 isInline
166 />
167 </Popover>
168
169 {props.children}
170 </PlateElement>
171 );
172 }
173
174 const EquationInput = createPrimitiveComponent(TextareaAutosize)({
175 propsHook: useEquationInput,
176 });
177
178 const EquationPopoverContent = ({
179 className,
180 isInline,
181 open,
182 setOpen,
183 ...props
184 }: {
185 isInline: boolean;
186 open: boolean;
187 setOpen: (open: boolean) => void;
188 } & TextareaAutosizeProps) => {
189 const editor = useEditorRef();
190 const readOnly = useReadOnly();
191 const element = useElement<TEquationElement>();
192
193 React.useEffect(() => {
194 if (isInline && open) {
195 setOpen(true);
196 }
197 }, [isInline, open, setOpen]);
198
199 if (readOnly) return null;
200
201 const onClose = () => {
202 setOpen(false);
203
204 if (isInline) {
205 editor.tf.select(element, { focus: true, next: true });
206 } else {
207 editor
208 .getApi(BlockSelectionPlugin)
209 .blockSelection.set(element.id as string);
210 }
211 };
212
213 return (
214 <PopoverContent
215 className="flex gap-2"
216 onEscapeKeyDown={(e) => {
217 e.preventDefault();
218 }}
219 contentEditable={false}
220 >
221 <EquationInput
222 className={cn("max-h-[50vh] grow resize-none p-2 text-sm", className)}
223 state={{ isInline, open, onClose }}
224 autoFocus
225 {...props}
226 />
227
228 <Button variant="secondary" className="px-3" onClick={onClose}>
229 Done <CornerDownLeftIcon className="size-3.5" />
230 </Button>
231 </PopoverContent>
232 );
233 };
234
234 lines Plain Text