import { BlockFlavorKeys, Protocol } from '@toeverything/datasource/db-service'; import React, { useCallback, useEffect, useMemo, useRef, useState, } from 'react'; import style9 from 'style9'; import { MuiClickAwayListener } from '@toeverything/components/ui'; import { Virgo, HookType, PluginHooks } from '@toeverything/framework/virgo'; import { CommandMenuContainer } from './Container'; import { CommandMenuCategories, commandMenuHandlerMap, commonCommandMenuHandler, menuItemsMap, } from './config'; import { QueryBlocks, QueryResult } from '../../search'; export type CommandMenuProps = { editor: Virgo; hooks: PluginHooks; style?: { left: number; top: number }; }; type CommandMenuPosition = { left: number; top: number | 'initial'; bottom: number | 'initial'; }; export const CommandMenu = ({ editor, hooks, style }: CommandMenuProps) => { const [is_show, set_is_show] = useState(false); const [block_id, set_block_id] = useState(); const [commandMenuPosition, setCommandMenuPosition] = useState({ left: 0, top: 0, bottom: 0, }); const [search_text, set_search_text] = useState(''); const [search_blocks, set_search_blocks] = useState([]); const commandMenuContentRef = useRef(); // TODO: Two-way link to be developed // useEffect(() => { // QueryBlocks(editor, search_text, result => set_search_blocks(result)); // }, [editor, search_text]); const [types, categories] = useMemo(() => { const types: Array = []; const categories: Array = []; if (search_blocks.length) { Object.values(search_blocks).forEach(({ id }) => types.push(id)); categories.push(CommandMenuCategories.pages); } Object.entries(menuItemsMap).forEach(([category, itemInfoList]) => { itemInfoList.forEach(info => { if ( !search_text || info.text.toLowerCase().includes(search_text.toLowerCase()) ) { types.push(info.type); } if ( !categories.includes(category as CommandMenuCategories) || types.includes(info.type) ) { categories.push(category as CommandMenuCategories); } }); }); return [types, categories]; }, [search_blocks, search_text]); const check_if_show_command_menu = useCallback( async (event: React.KeyboardEvent) => { const { type, anchorNode } = editor.selection.currentSelectInfo; if (event.key === '/' && type === 'Range') { if (anchorNode) { const text = editor.blockHelper.getBlockTextBeforeSelection( anchorNode.id ); if (text.endsWith('/')) { set_block_id(anchorNode.id); editor.blockHelper.removeSearchSlash(block_id); setTimeout(() => { const textSelection = editor.blockHelper.selectionToSlateRange( anchorNode.id, editor.selection.currentSelectInfo .browserSelection ); if (textSelection) { const { anchor } = textSelection; editor.blockHelper.setSearchSlash( anchorNode.id, anchor ); } }); set_search_text(''); set_is_show(true); const rect = editor.selection.currentSelectInfo?.browserSelection ?.getRangeAt(0) ?.getBoundingClientRect(); if (rect) { let top = rect.top; let clientHeight = document.documentElement.clientHeight; const COMMAND_MENU_HEIGHT = window.innerHeight * 0.4; if (clientHeight - top <= COMMAND_MENU_HEIGHT) { top = clientHeight - top + 10; setCommandMenuPosition({ left: rect.left, bottom: top, top: 'initial', }); } else { top += 24; setCommandMenuPosition({ left: rect.left, top: top, bottom: 'initial', }); } } } } } }, [editor, block_id] ); const handle_click_others = useCallback( (event: React.KeyboardEvent) => { if (is_show) { const { anchorNode } = editor.selection.currentSelectInfo; if (anchorNode.id !== block_id) { set_is_show(false); return; } setTimeout(() => { const searchText = editor.blockHelper.getSearchSlashText(block_id); // check if has search text if (searchText && searchText.startsWith('/')) { set_search_text(searchText.slice(1)); } else { set_is_show(false); } if (searchText.length > 6 && !types.length) { set_is_show(false); } }); } }, [editor, is_show, block_id, types] ); const handle_keyup = useCallback( (event: React.KeyboardEvent) => { check_if_show_command_menu(event); handle_click_others(event); }, [check_if_show_command_menu, handle_click_others] ); const handle_key_down = useCallback( (event: React.KeyboardEvent) => { if (event.code === 'Escape') { set_is_show(false); } }, [] ); useEffect(() => { hooks.addHook(HookType.ON_ROOT_NODE_KEYUP, handle_keyup); hooks.addHook(HookType.ON_ROOT_NODE_KEYDOWN_CAPTURE, handle_key_down); return () => { hooks.removeHook(HookType.ON_ROOT_NODE_KEYUP, handle_keyup); hooks.removeHook( HookType.ON_ROOT_NODE_KEYDOWN_CAPTURE, handle_key_down ); }; }, [handle_keyup, handle_key_down, hooks]); const handle_click_away = () => { set_is_show(false); }; const handle_selected = async (type: BlockFlavorKeys | string) => { const text = await editor.commands.textCommands.getBlockText(block_id); editor.blockHelper.removeSearchSlash(block_id, true); if (type.startsWith('Virgo')) { const handler = commandMenuHandlerMap[Protocol.Block.Type.reference]; handler(block_id, type, editor); } else if (text.length > 1) { const handler = commandMenuHandlerMap[type]; if (handler) { await handler(block_id, type, editor); } else { await commonCommandMenuHandler(block_id, type, editor); } const block = await editor.getBlockById(block_id); block.remove(); } else { if (Protocol.Block.Type[type as BlockFlavorKeys]) { const block = await editor.commands.blockCommands.convertBlock( block_id, type as BlockFlavorKeys ); block.firstCreateFlag = true; } } set_is_show(false); }; const handle_close = () => { editor.blockHelper.removeSearchSlash(block_id); }; return (
{/* MuiClickAwayListener 渲染子节点问题*/}
); }; const styles = style9.create({ commandMenu: { position: 'absolute', zIndex: 1, }, });