casualroom/apps/fenghuo/web/components/tiptap-ui/mark-button/mark-button.tsx

238 lines
5.5 KiB
TypeScript
Executable File

"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<ButtonProps, "type"> {
/**
* 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<Record<Mark, string>> = {
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<HTMLButtonElement, MarkButtonProps>(
(
{
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<HTMLButtonElement>) => {
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 (
<Button
type="button"
className={className.trim()}
disabled={isDisabled}
data-style="ghost"
data-active-state={isActive ? "on" : "off"}
data-disabled={isDisabled}
role="button"
tabIndex={-1}
aria-label={type}
aria-pressed={isActive}
tooltip={formattedName}
shortcutKeys={shortcutKey}
onClick={handleClick}
{...buttonProps}
ref={ref}
>
{children || (
<>
<Icon className="tiptap-button-icon" />
{text && <span className="tiptap-button-text">{text}</span>}
</>
)}
</Button>
)
}
)
MarkButton.displayName = "MarkButton"
export default MarkButton