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 UseSelectOrDeleteHandler = (nodeKey: string, command?: LexicalCommand<undefined>) => [RefObject<HTMLDivElement>, boolean]
- export const useSelectOrDelete: UseSelectOrDeleteHandler = (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],
- )
- }
|