"use client" import * as React from "react" import { isNodeSelection, type Editor } from "@tiptap/react" // --- Hooks --- import { useTiptapEditor } from "@/hooks/use-tiptap-editor" // --- Icons --- import { CornerDownLeftIcon } from "@/components/tiptap-icons/corner-down-left-icon" import { ExternalLinkIcon } from "@/components/tiptap-icons/external-link-icon" import { LinkIcon } from "@/components/tiptap-icons/link-icon" import { TrashIcon } from "@/components/tiptap-icons/trash-icon" // --- Lib --- import { isMarkInSchema, sanitizeUrl } from "@/lib/tiptap-utils" // --- UI Primitives --- import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" import { Button } from "@/components/tiptap-ui-primitive/button" import { Popover, PopoverContent, PopoverTrigger, } from "@/components/tiptap-ui-primitive/popover" import { Separator } from "@/components/tiptap-ui-primitive/separator" // --- Styles --- import "@/components/tiptap-ui/link-popover/link-popover.scss" export interface LinkHandlerProps { editor: Editor | null onSetLink?: () => void onLinkActive?: () => void } export interface LinkMainProps { url: string setUrl: React.Dispatch> setLink: () => void removeLink: () => void isActive: boolean } export const useLinkHandler = (props: LinkHandlerProps) => { const { editor, onSetLink, onLinkActive } = props const [url, setUrl] = React.useState(null) React.useEffect(() => { if (!editor) return // Get URL immediately on mount const { href } = editor.getAttributes("link") if (editor.isActive("link") && url === null) { setUrl(href || "") onLinkActive?.() } }, [editor, onLinkActive, url]) React.useEffect(() => { if (!editor) return const updateLinkState = () => { const { href } = editor.getAttributes("link") setUrl(href || "") if (editor.isActive("link") && url !== null) { onLinkActive?.() } } editor.on("selectionUpdate", updateLinkState) return () => { editor.off("selectionUpdate", updateLinkState) } }, [editor, onLinkActive, url]) const setLink = React.useCallback(() => { if (!url || !editor) return editor.chain().focus().extendMarkRange("link").setLink({ href: url }).run() setUrl(null) onSetLink?.() }, [editor, onSetLink, url]) const removeLink = React.useCallback(() => { if (!editor) return editor .chain() .focus() .extendMarkRange("link") .unsetLink() .setMeta("preventAutolink", true) .run() setUrl("") }, [editor]) return { url: url || "", setUrl, setLink, removeLink, isActive: editor?.isActive("link") || false, } } export const LinkButton = React.forwardRef( ({ className, children, ...props }, ref) => { return ( ) } ) export const LinkContent: React.FC<{ editor?: Editor | null }> = ({ editor: providedEditor }) => { const editor = useTiptapEditor(providedEditor) const linkHandler = useLinkHandler({ editor: editor, }) return } const LinkMain: React.FC = ({ url, setUrl, setLink, removeLink, isActive, }) => { const handleKeyDown = (event: React.KeyboardEvent) => { if (event.key === "Enter") { event.preventDefault() setLink() } } const handleOpenLink = () => { if (!url) return const safeUrl = sanitizeUrl(url, window.location.href) if (safeUrl !== "#") { window.open(safeUrl, "_blank", "noopener,noreferrer") } } return ( <> setUrl(e.target.value)} onKeyDown={handleKeyDown} autoComplete="off" autoCorrect="off" autoCapitalize="off" className="tiptap-input tiptap-input-clamp" />
) } export interface LinkPopoverProps extends Omit { /** * The TipTap editor instance. */ editor?: Editor | null /** * Whether to hide the link popover. * @default false */ hideWhenUnavailable?: boolean /** * Callback for when the popover opens or closes. */ onOpenChange?: (isOpen: boolean) => void /** * Whether to automatically open the popover when a link is active. * @default true */ autoOpenOnLinkActive?: boolean } export function LinkPopover({ editor: providedEditor, hideWhenUnavailable = false, onOpenChange, autoOpenOnLinkActive = true, ...props }: LinkPopoverProps) { const editor = useTiptapEditor(providedEditor) const linkInSchema = isMarkInSchema("link", editor) const [isOpen, setIsOpen] = React.useState(false) const onSetLink = () => { setIsOpen(false) } const onLinkActive = () => setIsOpen(autoOpenOnLinkActive) const linkHandler = useLinkHandler({ editor: editor, onSetLink, onLinkActive, }) const isDisabled = React.useMemo(() => { if (!editor) return true if (editor.isActive("codeBlock")) return true return !editor.can().setLink?.({ href: "" }) }, [editor]) const canSetLink = React.useMemo(() => { if (!editor) return false try { return editor.can().setMark("link") } catch { return false } }, [editor]) const isActive = editor?.isActive("link") ?? false const handleOnOpenChange = React.useCallback( (nextIsOpen: boolean) => { setIsOpen(nextIsOpen) onOpenChange?.(nextIsOpen) }, [onOpenChange] ) const show = React.useMemo(() => { if (!linkInSchema || !editor) { return false } if (hideWhenUnavailable) { if (isNodeSelection(editor.state.selection) || !canSetLink) { return false } } return true }, [linkInSchema, hideWhenUnavailable, editor, canSetLink]) if (!show || !editor || !editor.isEditable) { return null } return ( ) } LinkButton.displayName = "LinkButton"