mirror of
				https://github.com/langgenius/dify.git
				synced 2025-10-31 10:53:02 +00:00 
			
		
		
		
	
		
			
	
	
		
			185 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			185 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
|   | import { | ||
|  |   useCallback, | ||
|  |   useEffect, | ||
|  |   useRef, | ||
|  |   useState, | ||
|  | } from 'react' | ||
|  | import type { Dispatch, RefObject, SetStateAction } from 'react' | ||
|  | import type { | ||
|  |   Klass, | ||
|  |   LexicalCommand, | ||
|  |   LexicalEditor, | ||
|  |   TextNode, | ||
|  | } from 'lexical' | ||
|  | import { | ||
|  |   $getNodeByKey, | ||
|  |   $getSelection, | ||
|  |   $isDecoratorNode, | ||
|  |   $isNodeSelection, | ||
|  |   COMMAND_PRIORITY_LOW, | ||
|  |   KEY_BACKSPACE_COMMAND, | ||
|  |   KEY_DELETE_COMMAND, | ||
|  | } from 'lexical' | ||
|  | import type { EntityMatch } from '@lexical/text' | ||
|  | import { | ||
|  |   mergeRegister, | ||
|  | } from '@lexical/utils' | ||
|  | import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection' | ||
|  | import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' | ||
|  | import { $isContextBlockNode } from './plugins/context-block/node' | ||
|  | import { DELETE_CONTEXT_BLOCK_COMMAND } from './plugins/context-block' | ||
|  | import { $isHistoryBlockNode } from './plugins/history-block/node' | ||
|  | import { DELETE_HISTORY_BLOCK_COMMAND } from './plugins/history-block' | ||
|  | import { $isQueryBlockNode } from './plugins/query-block/node' | ||
|  | import { DELETE_QUERY_BLOCK_COMMAND } from './plugins/query-block' | ||
|  | import type { CustomTextNode } from './plugins/custom-text/node' | ||
|  | import { registerLexicalTextEntity } from './utils' | ||
|  | 
 | ||
|  | export type UseSelectOrDeleteHanlder = (nodeKey: string, command?: LexicalCommand<undefined>) => [RefObject<HTMLDivElement>, boolean] | ||
|  | export const useSelectOrDelete: UseSelectOrDeleteHanlder = (nodeKey: string, command?: LexicalCommand<undefined>) => { | ||
|  |   const ref = useRef<HTMLDivElement>(null) | ||
|  |   const [editor] = useLexicalComposerContext() | ||
|  |   const [isSelected, setSelected, clearSelection] = useLexicalNodeSelection(nodeKey) | ||
|  | 
 | ||
|  |   const handleDelete = useCallback( | ||
|  |     (event: KeyboardEvent) => { | ||
|  |       const selection = $getSelection() | ||
|  |       const nodes = selection?.getNodes() | ||
|  |       if ( | ||
|  |         !isSelected | ||
|  |         && nodes?.length === 1 | ||
|  |         && ( | ||
|  |           ($isContextBlockNode(nodes[0]) && command === DELETE_CONTEXT_BLOCK_COMMAND) | ||
|  |           || ($isHistoryBlockNode(nodes[0]) && command === DELETE_HISTORY_BLOCK_COMMAND) | ||
|  |           || ($isQueryBlockNode(nodes[0]) && command === DELETE_QUERY_BLOCK_COMMAND) | ||
|  |         ) | ||
|  |       ) | ||
|  |         editor.dispatchCommand(command, undefined) | ||
|  | 
 | ||
|  |       if (isSelected && $isNodeSelection(selection)) { | ||
|  |         event.preventDefault() | ||
|  |         const node = $getNodeByKey(nodeKey) | ||
|  |         if ($isDecoratorNode(node)) { | ||
|  |           if (command) | ||
|  |             editor.dispatchCommand(command, undefined) | ||
|  | 
 | ||
|  |           node.remove() | ||
|  |         } | ||
|  |       } | ||
|  | 
 | ||
|  |       return false | ||
|  |     }, | ||
|  |     [isSelected, nodeKey, command, editor], | ||
|  |   ) | ||
|  | 
 | ||
|  |   const handleSelect = useCallback((e: MouseEvent) => { | ||
|  |     e.stopPropagation() | ||
|  |     clearSelection() | ||
|  |     setSelected(true) | ||
|  |   }, [setSelected, clearSelection]) | ||
|  | 
 | ||
|  |   useEffect(() => { | ||
|  |     const ele = ref.current | ||
|  | 
 | ||
|  |     if (ele) | ||
|  |       ele.addEventListener('click', handleSelect) | ||
|  | 
 | ||
|  |     return () => { | ||
|  |       if (ele) | ||
|  |         ele.removeEventListener('click', handleSelect) | ||
|  |     } | ||
|  |   }, [handleSelect]) | ||
|  |   useEffect(() => { | ||
|  |     return mergeRegister( | ||
|  |       editor.registerCommand( | ||
|  |         KEY_DELETE_COMMAND, | ||
|  |         handleDelete, | ||
|  |         COMMAND_PRIORITY_LOW, | ||
|  |       ), | ||
|  |       editor.registerCommand( | ||
|  |         KEY_BACKSPACE_COMMAND, | ||
|  |         handleDelete, | ||
|  |         COMMAND_PRIORITY_LOW, | ||
|  |       ), | ||
|  |     ) | ||
|  |   }, [editor, clearSelection, handleDelete]) | ||
|  | 
 | ||
|  |   return [ref, isSelected] | ||
|  | } | ||
|  | 
 | ||
|  | export type UseTriggerHandler = () => [RefObject<HTMLDivElement>, boolean, Dispatch<SetStateAction<boolean>>] | ||
|  | export const useTrigger: UseTriggerHandler = () => { | ||
|  |   const triggerRef = useRef<HTMLDivElement>(null) | ||
|  |   const [open, setOpen] = useState(false) | ||
|  |   const handleOpen = useCallback((e: MouseEvent) => { | ||
|  |     e.stopPropagation() | ||
|  |     setOpen(v => !v) | ||
|  |   }, []) | ||
|  | 
 | ||
|  |   useEffect(() => { | ||
|  |     const trigger = triggerRef.current | ||
|  |     if (trigger) | ||
|  |       trigger.addEventListener('click', handleOpen) | ||
|  | 
 | ||
|  |     return () => { | ||
|  |       if (trigger) | ||
|  |         trigger.removeEventListener('click', handleOpen) | ||
|  |     } | ||
|  |   }, [handleOpen]) | ||
|  | 
 | ||
|  |   return [triggerRef, open, setOpen] | ||
|  | } | ||
|  | 
 | ||
|  | export function useLexicalTextEntity<T extends TextNode>( | ||
|  |   getMatch: (text: string) => null | EntityMatch, | ||
|  |   targetNode: Klass<T>, | ||
|  |   createNode: (textNode: CustomTextNode) => T, | ||
|  | ) { | ||
|  |   const [editor] = useLexicalComposerContext() | ||
|  | 
 | ||
|  |   useEffect(() => { | ||
|  |     return mergeRegister(...registerLexicalTextEntity(editor, getMatch, targetNode, createNode)) | ||
|  |   }, [createNode, editor, getMatch, targetNode]) | ||
|  | } | ||
|  | 
 | ||
|  | export type MenuTextMatch = { | ||
|  |   leadOffset: number | ||
|  |   matchingString: string | ||
|  |   replaceableString: string | ||
|  | } | ||
|  | export type TriggerFn = ( | ||
|  |   text: string, | ||
|  |   editor: LexicalEditor, | ||
|  | ) => MenuTextMatch | null | ||
|  | export const PUNCTUATION = '\\.,\\+\\*\\?\\$\\@\\|#{}\\(\\)\\^\\-\\[\\]\\\\/!%\'"~=<>_:;' | ||
|  | export function useBasicTypeaheadTriggerMatch( | ||
|  |   trigger: string, | ||
|  |   { minLength = 1, maxLength = 75 }: { minLength?: number; maxLength?: number }, | ||
|  | ): TriggerFn { | ||
|  |   return useCallback( | ||
|  |     (text: string) => { | ||
|  |       const validChars = `[^${trigger}${PUNCTUATION}\\s]` | ||
|  |       const TypeaheadTriggerRegex = new RegExp( | ||
|  |         `([^${trigger}]|^)(` | ||
|  |           + `[${trigger}]` | ||
|  |           + `((?:${validChars}){0,${maxLength}})` | ||
|  |           + ')$', | ||
|  |       ) | ||
|  |       const match = TypeaheadTriggerRegex.exec(text) | ||
|  |       if (match !== null) { | ||
|  |         const maybeLeadingWhitespace = match[1] | ||
|  |         const matchingString = match[3] | ||
|  |         if (matchingString.length >= minLength) { | ||
|  |           return { | ||
|  |             leadOffset: match.index + maybeLeadingWhitespace.length, | ||
|  |             matchingString, | ||
|  |             replaceableString: match[2], | ||
|  |           } | ||
|  |         } | ||
|  |       } | ||
|  |       return null | ||
|  |     }, | ||
|  |     [maxLength, minLength, trigger], | ||
|  |   ) | ||
|  | } |