220 lines
5.0 KiB
TypeScript
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,
|
|
}
|
|
}
|