206 lines
4.6 KiB
TypeScript
Executable File
206 lines
4.6 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 { BlockQuoteIcon } from "@/components/tiptap-icons/block-quote-icon"
|
|
|
|
// --- Lib ---
|
|
import { isNodeInSchema } from "@/lib/tiptap-utils"
|
|
|
|
// --- UI Primitives ---
|
|
import type { ButtonProps } from "@/components/tiptap-ui-primitive/button"
|
|
import { Button } from "@/components/tiptap-ui-primitive/button"
|
|
|
|
export interface BlockquoteButtonProps extends Omit<ButtonProps, "type"> {
|
|
/**
|
|
* The TipTap editor instance.
|
|
*/
|
|
editor?: Editor | null
|
|
/**
|
|
* Optional text to display alongside the icon.
|
|
*/
|
|
text?: string
|
|
/**
|
|
* Whether the button should hide when the node is not available.
|
|
* @default false
|
|
*/
|
|
hideWhenUnavailable?: boolean
|
|
}
|
|
|
|
export function canToggleBlockquote(editor: Editor | null): boolean {
|
|
if (!editor) return false
|
|
|
|
try {
|
|
return editor.can().toggleWrap("blockquote")
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
export function isBlockquoteActive(editor: Editor | null): boolean {
|
|
if (!editor) return false
|
|
return editor.isActive("blockquote")
|
|
}
|
|
|
|
export function toggleBlockquote(editor: Editor | null): boolean {
|
|
if (!editor) return false
|
|
return editor.chain().focus().toggleWrap("blockquote").run()
|
|
}
|
|
|
|
export function isBlockquoteButtonDisabled(
|
|
editor: Editor | null,
|
|
canToggle: boolean,
|
|
userDisabled: boolean = false
|
|
): boolean {
|
|
if (!editor) return true
|
|
if (userDisabled) return true
|
|
if (!canToggle) return true
|
|
return false
|
|
}
|
|
|
|
export function shouldShowBlockquoteButton(params: {
|
|
editor: Editor | null
|
|
hideWhenUnavailable: boolean
|
|
nodeInSchema: boolean
|
|
canToggle: boolean
|
|
}): boolean {
|
|
const { editor, hideWhenUnavailable, nodeInSchema, canToggle } = params
|
|
|
|
if (!nodeInSchema || !editor) {
|
|
return false
|
|
}
|
|
|
|
if (hideWhenUnavailable) {
|
|
if (isNodeSelection(editor.state.selection) || !canToggle) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return Boolean(editor?.isEditable)
|
|
}
|
|
|
|
export function useBlockquoteState(
|
|
editor: Editor | null,
|
|
disabled: boolean = false,
|
|
hideWhenUnavailable: boolean = false
|
|
) {
|
|
const nodeInSchema = isNodeInSchema("blockquote", editor)
|
|
|
|
const canToggle = canToggleBlockquote(editor)
|
|
const isDisabled = isBlockquoteButtonDisabled(editor, canToggle, disabled)
|
|
const isActive = isBlockquoteActive(editor)
|
|
|
|
const shouldShow = React.useMemo(
|
|
() =>
|
|
shouldShowBlockquoteButton({
|
|
editor,
|
|
hideWhenUnavailable,
|
|
nodeInSchema,
|
|
canToggle,
|
|
}),
|
|
[editor, hideWhenUnavailable, nodeInSchema, canToggle]
|
|
)
|
|
|
|
const handleToggle = React.useCallback(() => {
|
|
if (!isDisabled && editor) {
|
|
return toggleBlockquote(editor)
|
|
}
|
|
return false
|
|
}, [editor, isDisabled])
|
|
|
|
const shortcutKey = "Ctrl-Shift-b"
|
|
const label = "Blockquote"
|
|
|
|
return {
|
|
nodeInSchema,
|
|
canToggle,
|
|
isDisabled,
|
|
isActive,
|
|
shouldShow,
|
|
handleToggle,
|
|
shortcutKey,
|
|
label,
|
|
}
|
|
}
|
|
|
|
export const BlockquoteButton = React.forwardRef<
|
|
HTMLButtonElement,
|
|
BlockquoteButtonProps
|
|
>(
|
|
(
|
|
{
|
|
editor: providedEditor,
|
|
text,
|
|
hideWhenUnavailable = false,
|
|
className = "",
|
|
disabled,
|
|
onClick,
|
|
children,
|
|
...buttonProps
|
|
},
|
|
ref
|
|
) => {
|
|
const editor = useTiptapEditor(providedEditor)
|
|
|
|
const {
|
|
isDisabled,
|
|
isActive,
|
|
shouldShow,
|
|
handleToggle,
|
|
shortcutKey,
|
|
label,
|
|
} = useBlockquoteState(editor, disabled, hideWhenUnavailable)
|
|
|
|
const handleClick = React.useCallback(
|
|
(e: React.MouseEvent<HTMLButtonElement>) => {
|
|
onClick?.(e)
|
|
|
|
if (!e.defaultPrevented && !isDisabled) {
|
|
handleToggle()
|
|
}
|
|
},
|
|
[onClick, isDisabled, handleToggle]
|
|
)
|
|
|
|
if (!shouldShow || !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="blockquote"
|
|
aria-pressed={isActive}
|
|
tooltip={label}
|
|
shortcutKeys={shortcutKey}
|
|
onClick={handleClick}
|
|
{...buttonProps}
|
|
ref={ref}
|
|
>
|
|
{children || (
|
|
<>
|
|
<BlockQuoteIcon className="tiptap-button-icon" />
|
|
{text && <span className="tiptap-button-text">{text}</span>}
|
|
</>
|
|
)}
|
|
</Button>
|
|
)
|
|
}
|
|
)
|
|
|
|
BlockquoteButton.displayName = "BlockquoteButton"
|
|
|
|
export default BlockquoteButton
|