| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185 | 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 | nullexport 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],  )}
 |