返回 presentation-ai
media-toolbar.tsx
根目录 / src / components / plate / ui / media-toolbar.tsx
1 "use client";
2
3 import {
4 FloatingMedia as FloatingMediaPrimitive,
5 FloatingMediaStore,
6 useFloatingMediaValue,
7 useImagePreviewValue,
8 } from "@platejs/media/react";
9 import { cva } from "class-variance-authority";
10 import { Link, Trash2Icon, X } from "lucide-react";
11 import { KEYS, type WithRequiredKey } from "platejs";
12 import {
13 useEditorRef,
14 useEditorSelector,
15 useElement,
16 useReadOnly,
17 useRemoveNodeButton,
18 useSelected,
19 } from "platejs/react";
20 import * as React from "react";
21
22 import { Button, buttonVariants } from "@/components/plate/ui/button";
23 import {
24 Popover,
25 PopoverAnchor,
26 PopoverContent,
27 } from "@/components/plate/ui/popover";
28 import { Separator } from "@/components/plate/ui/separator";
29 import { TooltipButton } from "@/components/ui/button";
30 import {
31 Select,
32 SelectContent,
33 SelectItem,
34 SelectTrigger,
35 SelectValue,
36 } from "@/components/ui/select";
37 import { CaptionButton } from "./caption";
38 import { getAllEmbedTypes } from "./media-embeds";
39
40 const inputVariants = cva(
41 "flex h-7 w-full rounded-md border-none bg-transparent px-1.5 py-1 text-base placeholder:text-muted-foreground focus-visible:ring-transparent focus-visible:outline-hidden md:text-sm",
42 );
43
44 export function MediaToolbar({
45 children,
46 plugin,
47 }: {
48 children: React.ReactNode;
49 plugin: WithRequiredKey;
50 }) {
51 const editor = useEditorRef();
52 const readOnly = useReadOnly();
53 const selected = useSelected();
54
55 const selectionCollapsed = useEditorSelector(
56 (editor) => !editor.api.isExpanded(),
57 [],
58 );
59 const isImagePreviewOpen = useImagePreviewValue("isOpen", editor.id);
60 const isOpen =
61 !readOnly && selected && selectionCollapsed && !isImagePreviewOpen;
62 const isEditing = useFloatingMediaValue("isEditing");
63
64 React.useEffect(() => {
65 if (!isOpen && isEditing) {
66 FloatingMediaStore.set("isEditing", false);
67 }
68 }, [isOpen]);
69
70 const element = useElement();
71 const { props: buttonProps } = useRemoveNodeButton({ element });
72
73 // Check if this is a MediaEmbed element
74 const isMediaEmbed = element.type === KEYS.mediaEmbed;
75
76 // Get current provider for MediaEmbed elements
77 const currentProvider = isMediaEmbed
78 ? (element as { provider?: string }).provider || ""
79 : "";
80
81 // Get all available embed types
82 const embedTypes = getAllEmbedTypes();
83
84 // Handle provider change for MediaEmbed
85 const handleProviderChange = (newProvider: string) => {
86 if (isMediaEmbed && editor && element) {
87 const path = editor.api.findPath(element);
88 if (path) {
89 editor.tf.setNodes(
90 {
91 ...element,
92 provider: newProvider,
93 url: "", // Clear URL when changing provider
94 id: "", // Clear ID when changing provider
95 },
96 { at: path },
97 );
98 }
99 }
100 };
101
102 // Handle URL removal for MediaEmbed
103 const handleRemoveUrl = () => {
104 if (isMediaEmbed && editor && element) {
105 const path = editor.api.findPath(element);
106 if (path) {
107 editor.tf.setNodes(
108 {
109 ...element,
110 url: "",
111 id: "",
112 },
113 { at: path },
114 );
115 }
116 }
117 };
118
119 if (readOnly) return <>{children}</>;
120
121 return (
122 <Popover open={isOpen} modal={false}>
123 <PopoverAnchor>{children}</PopoverAnchor>
124
125 <PopoverContent
126 className="w-auto p-1"
127 onOpenAutoFocus={(e) => e.preventDefault()}
128 >
129 {isEditing ? (
130 <div className="flex w-82.5 flex-col">
131 <div className="flex items-center">
132 <div className="flex items-center pr-1 pl-2 text-muted-foreground">
133 <Link className="size-4" />
134 </div>
135
136 <FloatingMediaPrimitive.UrlInput
137 className={inputVariants()}
138 placeholder="Paste the embed link..."
139 options={{ plugin }}
140 />
141 </div>
142 </div>
143 ) : (
144 <div className="box-content flex items-center">
145 <FloatingMediaPrimitive.EditButton
146 className={buttonVariants({ size: "sm", variant: "ghost" })}
147 >
148 Edit link
149 </FloatingMediaPrimitive.EditButton>
150
151 <CaptionButton size="sm" variant="ghost">
152 Caption
153 </CaptionButton>
154
155 {/* MediaEmbed specific options */}
156 {isMediaEmbed && (
157 <>
158 <Separator orientation="vertical" className="mx-1 h-6" />
159
160 {/* Change Provider Dropdown */}
161 <Select
162 value={currentProvider}
163 onValueChange={handleProviderChange}
164 >
165 <SelectTrigger className="flex h-8 w-auto min-w-30 items-center border-none bg-transparent px-2 py-1 text-sm">
166 <SelectValue placeholder="Provider" />
167 </SelectTrigger>
168 <SelectContent>
169 {embedTypes.map(({ type, config }) => (
170 <SelectItem key={type} value={type}>
171 {config.name}
172 </SelectItem>
173 ))}
174 </SelectContent>
175 </Select>
176
177 {/* Remove URL Button */}
178 <TooltipButton
179 size="sm"
180 tooltipText="Remove URL"
181 variant="ghost"
182 onClick={handleRemoveUrl}
183 >
184 <X className="size-4" />
185 </TooltipButton>
186 </>
187 )}
188
189 <Separator orientation="vertical" className="mx-1 h-6" />
190
191 <Button size="sm" variant="ghost" {...buttonProps}>
192 <Trash2Icon />
193 </Button>
194 </div>
195 )}
196 </PopoverContent>
197 </Popover>
198 );
199 }
200
200 lines Plain Text