"use client" import * as React from "react" import { isNodeSelection, type Editor } from "@tiptap/react" // --- Hooks --- import { useTiptapEditor } from "@/hooks/use-tiptap-editor" // --- Icons --- import { BoldIcon } from "@/components/tiptap-icons/bold-icon" import { Code2Icon } from "@/components/tiptap-icons/code2-icon" import { ItalicIcon } from "@/components/tiptap-icons/italic-icon" import { StrikeIcon } from "@/components/tiptap-icons/strike-icon" import { SubscriptIcon } from "@/components/tiptap-icons/subscript-icon" import { SuperscriptIcon } from "@/components/tiptap-icons/superscript-icon" import { UnderlineIcon } from "@/components/tiptap-icons/underline-icon" // --- Lib --- import { isMarkInSchema } from "@/lib/tiptap-utils" // --- UI Primitives --- import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" import { Button } from "@/components/tiptap-ui-primitive/button" export type Mark = | "bold" | "italic" | "strike" | "code" | "underline" | "superscript" | "subscript" export interface MarkButtonProps extends Omit { /** * The type of mark to toggle */ type: Mark /** * Optional editor instance. If not provided, will use editor from context */ editor?: Editor | null /** * Display text for the button (optional) */ text?: string /** * Whether this button should be hidden when the mark is not available */ hideWhenUnavailable?: boolean } export const markIcons = { bold: BoldIcon, italic: ItalicIcon, underline: UnderlineIcon, strike: StrikeIcon, code: Code2Icon, superscript: SuperscriptIcon, subscript: SubscriptIcon, } export const markShortcutKeys: Partial> = { bold: "Ctrl-b", italic: "Ctrl-i", underline: "Ctrl-u", strike: "Ctrl-Shift-s", code: "Ctrl-e", superscript: "Ctrl-.", subscript: "Ctrl-,", } export function canToggleMark(editor: Editor | null, type: Mark): boolean { if (!editor) return false try { return editor.can().toggleMark(type) } catch { return false } } export function isMarkActive(editor: Editor | null, type: Mark): boolean { if (!editor) return false return editor.isActive(type) } export function toggleMark(editor: Editor | null, type: Mark): void { if (!editor) return editor.chain().focus().toggleMark(type).run() } export function isMarkButtonDisabled( editor: Editor | null, type: Mark, userDisabled: boolean = false ): boolean { if (!editor) return true if (userDisabled) return true if (editor.isActive("codeBlock")) return true if (!canToggleMark(editor, type)) return true return false } export function shouldShowMarkButton(params: { editor: Editor | null type: Mark hideWhenUnavailable: boolean markInSchema: boolean }): boolean { const { editor, type, hideWhenUnavailable, markInSchema } = params if (!markInSchema || !editor) { return false } if (hideWhenUnavailable) { if ( isNodeSelection(editor.state.selection) || !canToggleMark(editor, type) ) { return false } } return true } export function getFormattedMarkName(type: Mark): string { return type.charAt(0).toUpperCase() + type.slice(1) } export function useMarkState( editor: Editor | null, type: Mark, disabled: boolean = false ) { const markInSchema = isMarkInSchema(type, editor) const isDisabled = isMarkButtonDisabled(editor, type, disabled) const isActive = isMarkActive(editor, type) const Icon = markIcons[type] const shortcutKey = markShortcutKeys[type] const formattedName = getFormattedMarkName(type) return { markInSchema, isDisabled, isActive, Icon, shortcutKey, formattedName, } } export const MarkButton = React.forwardRef( ( { editor: providedEditor, type, text, hideWhenUnavailable = false, className = "", disabled, onClick, children, ...buttonProps }, ref ) => { const editor = useTiptapEditor(providedEditor) const { markInSchema, isDisabled, isActive, Icon, shortcutKey, formattedName, } = useMarkState(editor, type, disabled) const handleClick = React.useCallback( (e: React.MouseEvent) => { onClick?.(e) if (!e.defaultPrevented && !isDisabled && editor) { toggleMark(editor, type) } }, [onClick, isDisabled, editor, type] ) const show = React.useMemo(() => { return shouldShowMarkButton({ editor, type, hideWhenUnavailable, markInSchema, }) }, [editor, type, hideWhenUnavailable, markInSchema]) if (!show || !editor || !editor.isEditable) { return null } return ( ) } ) MarkButton.displayName = "MarkButton" export default MarkButton