'use client' import * as React from 'react' import type { ContentEditorSlashCommand } from '@admin/utils/editor/slash-commands' import type { SlashMenuState } from '@admin/utils/editor/slash-menu-state' import type { Editor } from '@tiptap/react' import { getSlashMenuState } from '@admin/utils/editor/get-slash-menu-state' import { isSameSlashMenuState } from '@admin/utils/editor/is-same-slash-menu-state' import { runSlashCommand } from '@admin/utils/editor/run-slash-command' import { getFilteredSlashCommands } from '@admin/utils/editor/slash-commands' export function useContentEditorSlashMenu(editorRef: React.RefObject) { const [slashMenu, setSlashMenu] = React.useState(null) const [slashSearchQuery, setSlashSearchQuery] = React.useState('') const [activeSlashIndexState, setActiveSlashIndexState] = React.useState(0) const slashMenuRef = React.useRef(null) const activeSlashIndexRef = React.useRef(0) const filteredSlashCommands = React.useMemo( () => getFilteredSlashCommands(slashSearchQuery), [slashSearchQuery] ) const filteredSlashCommandsRef = React.useRef(filteredSlashCommands) const activeSlashIndex = activeSlashIndexState >= filteredSlashCommands.length ? 0 : activeSlashIndexState slashMenuRef.current = slashMenu activeSlashIndexRef.current = activeSlashIndex filteredSlashCommandsRef.current = filteredSlashCommands const updateSlashMenu = React.useCallback((nextEditor: Editor) => { const nextSlashMenu = getSlashMenuState(nextEditor) setSlashMenu((current) => isSameSlashMenuState(current, nextSlashMenu) ? current : nextSlashMenu ) setSlashSearchQuery((current) => { const currentSlashMenu = slashMenuRef.current if (!nextSlashMenu) return '' if ( currentSlashMenu && currentSlashMenu.from === nextSlashMenu.from && currentSlashMenu.to === nextSlashMenu.to && currentSlashMenu.query === nextSlashMenu.query ) { return current } return nextSlashMenu.query }) setActiveSlashIndexState((current) => slashMenuRef.current?.query === nextSlashMenu?.query && nextSlashMenu ? current : 0 ) }, []) const closeSlashMenu = React.useCallback(() => { setSlashMenu(null) setSlashSearchQuery('') setActiveSlashIndexState(0) }, []) const setSlashSearch = React.useCallback((query: string) => { setSlashSearchQuery(query) setActiveSlashIndexState(0) }, []) const selectSlashCommand = React.useCallback( (command: ContentEditorSlashCommand) => { const currentSlashMenu = slashMenuRef.current const editor = editorRef.current if (!currentSlashMenu || !editor) return false const didRun = runSlashCommand(editor, command, currentSlashMenu) closeSlashMenu() return didRun }, [closeSlashMenu, editorRef] ) const handleSlashKeyDown = React.useCallback( (event: KeyboardEvent) => { const currentSlashMenu = slashMenuRef.current if (!currentSlashMenu) return false const commands = filteredSlashCommandsRef.current if (event.key === 'ArrowDown') { event.preventDefault() setActiveSlashIndexState((current) => commands.length > 0 ? (current + 1) % commands.length : 0 ) return true } if (event.key === 'ArrowUp') { event.preventDefault() setActiveSlashIndexState((current) => commands.length > 0 ? (current - 1 + commands.length) % commands.length : 0 ) return true } if (event.key === 'Escape') { event.preventDefault() closeSlashMenu() return true } if (event.key === 'Enter' || event.key === 'Tab') { const command = commands[activeSlashIndexRef.current] if (!command) return false event.preventDefault() return selectSlashCommand(command) } return false }, [closeSlashMenu, selectSlashCommand] ) return { activeSlashIndex, closeSlashMenu, filteredSlashCommands, handleSlashKeyDown, selectSlashCommand, setActiveSlashIndex: setActiveSlashIndexState, setSlashSearch, slashMenu, slashSearchQuery, updateSlashMenu } }