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

220 lines
5.0 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 { ListIcon } from "@/components/tiptap-icons/list-icon"
import { ListOrderedIcon } from "@/components/tiptap-icons/list-ordered-icon"
import { ListTodoIcon } from "@/components/tiptap-icons/list-todo-icon"
// --- Lib ---
import { isNodeInSchema } from "@/lib/tiptap-utils"
// --- Tiptap UI ---
import {
canToggleList,
isListActive,
listIcons,
type ListType,
} from "@/components/tiptap-ui/list-button"
/**
* Configuration for the list dropdown menu functionality
*/
export interface UseListDropdownMenuConfig {
/**
* The Tiptap editor instance.
*/
editor?: Editor | null
/**
* The list types to display in the dropdown.
* @default ["bulletList", "orderedList", "taskList"]
*/
types?: ListType[]
/**
* Whether the dropdown should be hidden when no list types are available
* @default false
*/
hideWhenUnavailable?: boolean
}
export interface ListOption {
label: string
type: ListType
icon: React.ElementType
}
export const listOptions: ListOption[] = [
{
label: "Bullet List",
type: "bulletList",
icon: ListIcon,
},
{
label: "Ordered List",
type: "orderedList",
icon: ListOrderedIcon,
},
{
label: "Task List",
type: "taskList",
icon: ListTodoIcon,
},
]
export function canToggleAnyList(
editor: Editor | null,
listTypes: ListType[]
): boolean {
if (!editor || !editor.isEditable) return false
return listTypes.some((type) => canToggleList(editor, type))
}
export function isAnyListActive(
editor: Editor | null,
listTypes: ListType[]
): boolean {
if (!editor || !editor.isEditable) return false
return listTypes.some((type) => isListActive(editor, type))
}
export function getFilteredListOptions(
availableTypes: ListType[]
): typeof listOptions {
return listOptions.filter(
(option) => !option.type || availableTypes.includes(option.type)
)
}
export function shouldShowListDropdown(params: {
editor: Editor | null
listTypes: ListType[]
hideWhenUnavailable: boolean
listInSchema: boolean
canToggleAny: boolean
}): boolean {
const { editor, hideWhenUnavailable, listInSchema, canToggleAny } = params
if (!listInSchema || !editor) {
return false
}
if (hideWhenUnavailable && !editor.isActive("code")) {
return canToggleAny
}
return true
}
/**
* Gets the currently active list type from the available types
*/
export function getActiveListType(
editor: Editor | null,
availableTypes: ListType[]
): ListType | undefined {
if (!editor || !editor.isEditable) return undefined
return availableTypes.find((type) => isListActive(editor, type))
}
/**
* Custom hook that provides list dropdown menu functionality for Tiptap editor
*
* @example
* ```tsx
* // Simple usage
* function MyListDropdown() {
* const {
* isVisible,
* activeType,
* isAnyActive,
* canToggleAny,
* filteredLists,
* } = useListDropdownMenu()
*
* if (!isVisible) return null
*
* return (
* <DropdownMenu>
* // dropdown content
* </DropdownMenu>
* )
* }
*
* // Advanced usage with configuration
* function MyAdvancedListDropdown() {
* const {
* isVisible,
* activeType,
* } = useListDropdownMenu({
* editor: myEditor,
* types: ["bulletList", "orderedList"],
* hideWhenUnavailable: true,
* })
*
* // component implementation
* }
* ```
*/
export function useListDropdownMenu(config?: UseListDropdownMenuConfig) {
const {
editor: providedEditor,
types = ["bulletList", "orderedList", "taskList"],
hideWhenUnavailable = false,
} = config || {}
const { editor } = useTiptapEditor(providedEditor)
const [isVisible, setIsVisible] = React.useState(false)
const listInSchema = types.some((type) => isNodeInSchema(type, editor))
const filteredLists = React.useMemo(
() => getFilteredListOptions(types),
[types]
)
const canToggleAny = canToggleAnyList(editor, types)
const isAnyActive = isAnyListActive(editor, types)
const activeType = getActiveListType(editor, types)
const activeList = filteredLists.find((option) => option.type === activeType)
React.useEffect(() => {
if (!editor) return
const handleSelectionUpdate = () => {
setIsVisible(
shouldShowListDropdown({
editor,
listTypes: types,
hideWhenUnavailable,
listInSchema,
canToggleAny,
})
)
}
handleSelectionUpdate()
editor.on("selectionUpdate", handleSelectionUpdate)
return () => {
editor.off("selectionUpdate", handleSelectionUpdate)
}
}, [canToggleAny, editor, hideWhenUnavailable, listInSchema, types])
return {
isVisible,
activeType,
isActive: isAnyActive,
canToggle: canToggleAny,
types,
filteredLists,
label: "List",
Icon: activeList ? listIcons[activeList.type] : ListIcon,
}
}