SurfSense/surfsense_web/components/ui/link-toolbar.tsx

179 lines
4.3 KiB
TypeScript
Raw Normal View History

2026-02-17 12:47:39 +05:30
"use client";
2026-02-20 22:44:56 -08:00
import { flip, offset, type UseVirtualFloatingOptions } from "@platejs/floating";
2026-02-17 12:47:39 +05:30
import { getLinkAttributes } from "@platejs/link";
import {
2026-02-17 12:47:39 +05:30
FloatingLinkUrlInput,
2026-02-20 22:44:56 -08:00
type LinkFloatingToolbarState,
2026-02-17 12:47:39 +05:30
useFloatingLinkEdit,
useFloatingLinkEditState,
useFloatingLinkInsert,
useFloatingLinkInsertState,
} from "@platejs/link/react";
import { cva } from "class-variance-authority";
import { ExternalLink, Link, Text, Unlink } from "lucide-react";
2026-02-20 22:44:56 -08:00
import type { TLinkElement } from "platejs";
2026-02-17 12:47:39 +05:30
import { KEYS } from "platejs";
import {
2026-02-17 12:47:39 +05:30
useEditorRef,
useEditorSelection,
useFormInputProps,
usePluginOption,
} from "platejs/react";
2026-02-20 22:44:56 -08:00
import * as React from "react";
import { Button } from "@/components/ui/button";
2026-02-17 12:47:39 +05:30
import { Separator } from "@/components/ui/separator";
const popoverVariants = cva(
"z-50 w-auto rounded-md bg-popover p-1 text-popover-foreground shadow-md outline-hidden"
);
const inputVariants = cva(
2026-02-17 12:47:39 +05:30
"flex h-[28px] w-full rounded-md border-none bg-transparent px-1.5 py-1 text-base placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-transparent md:text-sm"
);
2026-02-17 12:47:39 +05:30
export function LinkFloatingToolbar({ state }: { state?: LinkFloatingToolbarState }) {
const activeCommentId = usePluginOption({ key: KEYS.comment }, "activeId");
const activeSuggestionId = usePluginOption({ key: KEYS.suggestion }, "activeId");
const floatingOptions: UseVirtualFloatingOptions = React.useMemo(
() => ({
middleware: [
offset(8),
flip({
fallbackPlacements: ["bottom-end", "top-start", "top-end"],
padding: 12,
}),
],
placement: activeSuggestionId || activeCommentId ? "top-start" : "bottom-start",
}),
[activeCommentId, activeSuggestionId]
);
const insertState = useFloatingLinkInsertState({
...state,
floatingOptions: {
...floatingOptions,
...state?.floatingOptions,
},
});
const {
hidden,
props: insertProps,
ref: insertRef,
textInputProps,
} = useFloatingLinkInsert(insertState);
const editState = useFloatingLinkEditState({
...state,
floatingOptions: {
...floatingOptions,
...state?.floatingOptions,
},
});
const {
editButtonProps,
props: editProps,
ref: editRef,
unlinkButtonProps,
} = useFloatingLinkEdit(editState);
const inputProps = useFormInputProps({
preventDefaultOnEnterKeydown: true,
});
if (hidden) return null;
const input = (
<div className="flex w-[330px] flex-col" {...inputProps}>
<div className="flex items-center">
<div className="flex items-center pr-1 pl-2 text-muted-foreground">
<Link className="size-4" />
</div>
<FloatingLinkUrlInput
className={inputVariants()}
placeholder="Paste link"
data-plate-focus
/>
</div>
<Separator className="my-1" />
<div className="flex items-center">
<div className="flex items-center pr-1 pl-2 text-muted-foreground">
<Text className="size-4" />
</div>
<input
className={inputVariants()}
placeholder="Text to display"
data-plate-focus
{...textInputProps}
/>
</div>
</div>
);
const editContent = editState.isEditing ? (
input
) : (
<div className="box-content flex items-center">
<Button size="sm" variant="ghost" type="button" {...editButtonProps}>
2026-02-17 12:47:39 +05:30
Edit link
</Button>
2026-02-17 12:47:39 +05:30
<Separator orientation="vertical" />
<LinkOpenButton />
<Separator orientation="vertical" />
<Button size="sm" variant="ghost" type="button" {...unlinkButtonProps}>
2026-02-17 12:47:39 +05:30
<Unlink width={18} />
</Button>
2026-02-17 12:47:39 +05:30
</div>
);
return (
<>
<div ref={insertRef} className={popoverVariants()} {...insertProps}>
{input}
</div>
<div ref={editRef} className={popoverVariants()} {...editProps}>
{editContent}
</div>
</>
);
}
function LinkOpenButton() {
2026-02-17 12:47:39 +05:30
const editor = useEditorRef();
useEditorSelection();
const entry = editor.api.node<TLinkElement>({
match: { type: editor.getType(KEYS.link) },
});
const attributes = entry ? getLinkAttributes(editor, entry[0]) : {};
const href = typeof attributes.href === "string" ? attributes.href : undefined;
2026-02-17 12:47:39 +05:30
return (
<Button
size="sm"
variant="ghost"
type="button"
onClick={() => {
if (href) window.open(href, "_blank", "noopener,noreferrer");
}}
2026-02-17 12:47:39 +05:30
onMouseOver={(e) => {
e.stopPropagation();
}}
2026-03-27 03:17:05 -07:00
onFocus={(e) => {
e.stopPropagation();
}}
2026-02-17 12:47:39 +05:30
aria-label="Open link in a new tab"
disabled={!href}
2026-02-17 12:47:39 +05:30
>
<ExternalLink width={18} />
</Button>
2026-02-17 12:47:39 +05:30
);
}