mosaicmap/components/tiptap-ui/heading-dropdown-menu/use-heading-dropdown-menu.ts
2025-08-12 21:25:52 +08:00

133 lines
2.9 KiB
TypeScript

"use client"
import * as React from "react"
import type { Editor } from "@tiptap/react"
// --- Hooks ---
import { useTiptapEditor } from "@/hooks/use-tiptap-editor"
// --- Icons ---
import { HeadingIcon } from "@/components/tiptap-icons/heading-icon"
// --- Tiptap UI ---
import {
headingIcons,
type Level,
isHeadingActive,
canToggle,
shouldShowButton,
} from "@/components/tiptap-ui/heading-button"
/**
* Configuration for the heading dropdown menu functionality
*/
export interface UseHeadingDropdownMenuConfig {
/**
* The Tiptap editor instance.
*/
editor?: Editor | null
/**
* Available heading levels to show in the dropdown
* @default [1, 2, 3, 4, 5, 6]
*/
levels?: Level[]
/**
* Whether the dropdown should hide when headings are not available.
* @default false
*/
hideWhenUnavailable?: boolean
}
/**
* Gets the currently active heading level from the available levels
*/
export function getActiveHeadingLevel(
editor: Editor | null,
levels: Level[] = [1, 2, 3, 4, 5, 6]
): Level | undefined {
if (!editor || !editor.isEditable) return undefined
return levels.find((level) => isHeadingActive(editor, level))
}
/**
* Custom hook that provides heading dropdown menu functionality for Tiptap editor
*
* @example
* ```tsx
* // Simple usage
* function MyHeadingDropdown() {
* const {
* isVisible,
* activeLevel,
* isAnyHeadingActive,
* canToggle,
* levels,
* } = useHeadingDropdownMenu()
*
* if (!isVisible) return null
*
* return (
* <DropdownMenu>
* // dropdown content
* </DropdownMenu>
* )
* }
*
* // Advanced usage with configuration
* function MyAdvancedHeadingDropdown() {
* const {
* isVisible,
* activeLevel,
* } = useHeadingDropdownMenu({
* editor: myEditor,
* levels: [1, 2, 3],
* hideWhenUnavailable: true,
* })
*
* // component implementation
* }
* ```
*/
export function useHeadingDropdownMenu(config?: UseHeadingDropdownMenuConfig) {
const {
editor: providedEditor,
levels = [1, 2, 3, 4, 5, 6],
hideWhenUnavailable = false,
} = config || {}
const { editor } = useTiptapEditor(providedEditor)
const [isVisible, setIsVisible] = React.useState(true)
const activeLevel = getActiveHeadingLevel(editor, levels)
const isActive = isHeadingActive(editor)
const canToggleState = canToggle(editor)
React.useEffect(() => {
if (!editor) return
const handleSelectionUpdate = () => {
setIsVisible(
shouldShowButton({ editor, hideWhenUnavailable, level: levels })
)
}
handleSelectionUpdate()
editor.on("selectionUpdate", handleSelectionUpdate)
return () => {
editor.off("selectionUpdate", handleSelectionUpdate)
}
}, [editor, hideWhenUnavailable, levels])
return {
isVisible,
activeLevel,
isActive,
canToggle: canToggleState,
levels,
label: "Heading",
Icon: activeLevel ? headingIcons[activeLevel] : HeadingIcon,
}
}