mirror of
https://github.com/langgenius/dify.git
synced 2024-11-16 19:59:50 +08:00
186 lines
5.4 KiB
TypeScript
186 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 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],
|
|
)
|
|
}
|