Spaces:
Paused
Paused
| 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 true | |
| } | |
| } | |
| 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 = `[${PUNCTUATION}\\s]` | |
| const TypeaheadTriggerRegex = new RegExp( | |
| '(.*)(' | |
| + `[${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], | |
| ) | |
| } | |