您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. import {
  2. useCallback,
  3. useEffect,
  4. useRef,
  5. useState,
  6. } from 'react'
  7. import type { Dispatch, RefObject, SetStateAction } from 'react'
  8. import type {
  9. Klass,
  10. LexicalCommand,
  11. LexicalEditor,
  12. TextNode,
  13. } from 'lexical'
  14. import {
  15. $getNodeByKey,
  16. $getSelection,
  17. $isDecoratorNode,
  18. $isNodeSelection,
  19. COMMAND_PRIORITY_LOW,
  20. KEY_BACKSPACE_COMMAND,
  21. KEY_DELETE_COMMAND,
  22. } from 'lexical'
  23. import type { EntityMatch } from '@lexical/text'
  24. import {
  25. mergeRegister,
  26. } from '@lexical/utils'
  27. import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection'
  28. import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
  29. import { $isContextBlockNode } from './plugins/context-block/node'
  30. import { DELETE_CONTEXT_BLOCK_COMMAND } from './plugins/context-block'
  31. import { $isHistoryBlockNode } from './plugins/history-block/node'
  32. import { DELETE_HISTORY_BLOCK_COMMAND } from './plugins/history-block'
  33. import { $isQueryBlockNode } from './plugins/query-block/node'
  34. import { DELETE_QUERY_BLOCK_COMMAND } from './plugins/query-block'
  35. import type { CustomTextNode } from './plugins/custom-text/node'
  36. import { registerLexicalTextEntity } from './utils'
  37. export type UseSelectOrDeleteHandler = (nodeKey: string, command?: LexicalCommand<undefined>) => [RefObject<HTMLDivElement>, boolean]
  38. export const useSelectOrDelete: UseSelectOrDeleteHandler = (nodeKey: string, command?: LexicalCommand<undefined>) => {
  39. const ref = useRef<HTMLDivElement>(null)
  40. const [editor] = useLexicalComposerContext()
  41. const [isSelected, setSelected, clearSelection] = useLexicalNodeSelection(nodeKey)
  42. const handleDelete = useCallback(
  43. (event: KeyboardEvent) => {
  44. const selection = $getSelection()
  45. const nodes = selection?.getNodes()
  46. if (
  47. !isSelected
  48. && nodes?.length === 1
  49. && (
  50. ($isContextBlockNode(nodes[0]) && command === DELETE_CONTEXT_BLOCK_COMMAND)
  51. || ($isHistoryBlockNode(nodes[0]) && command === DELETE_HISTORY_BLOCK_COMMAND)
  52. || ($isQueryBlockNode(nodes[0]) && command === DELETE_QUERY_BLOCK_COMMAND)
  53. )
  54. )
  55. editor.dispatchCommand(command, undefined)
  56. if (isSelected && $isNodeSelection(selection)) {
  57. event.preventDefault()
  58. const node = $getNodeByKey(nodeKey)
  59. if ($isDecoratorNode(node)) {
  60. if (command)
  61. editor.dispatchCommand(command, undefined)
  62. node.remove()
  63. return true
  64. }
  65. }
  66. return false
  67. },
  68. [isSelected, nodeKey, command, editor],
  69. )
  70. const handleSelect = useCallback((e: MouseEvent) => {
  71. if (!e.metaKey && !e.ctrlKey) {
  72. e.stopPropagation()
  73. clearSelection()
  74. setSelected(true)
  75. }
  76. }, [setSelected, clearSelection])
  77. useEffect(() => {
  78. const ele = ref.current
  79. if (ele)
  80. ele.addEventListener('click', handleSelect)
  81. return () => {
  82. if (ele)
  83. ele.removeEventListener('click', handleSelect)
  84. }
  85. }, [handleSelect])
  86. useEffect(() => {
  87. return mergeRegister(
  88. editor.registerCommand(
  89. KEY_DELETE_COMMAND,
  90. handleDelete,
  91. COMMAND_PRIORITY_LOW,
  92. ),
  93. editor.registerCommand(
  94. KEY_BACKSPACE_COMMAND,
  95. handleDelete,
  96. COMMAND_PRIORITY_LOW,
  97. ),
  98. )
  99. }, [editor, clearSelection, handleDelete])
  100. return [ref, isSelected]
  101. }
  102. export type UseTriggerHandler = () => [RefObject<HTMLDivElement>, boolean, Dispatch<SetStateAction<boolean>>]
  103. export const useTrigger: UseTriggerHandler = () => {
  104. const triggerRef = useRef<HTMLDivElement>(null)
  105. const [open, setOpen] = useState(false)
  106. const handleOpen = useCallback((e: MouseEvent) => {
  107. e.stopPropagation()
  108. setOpen(v => !v)
  109. }, [])
  110. useEffect(() => {
  111. const trigger = triggerRef.current
  112. if (trigger)
  113. trigger.addEventListener('click', handleOpen)
  114. return () => {
  115. if (trigger)
  116. trigger.removeEventListener('click', handleOpen)
  117. }
  118. }, [handleOpen])
  119. return [triggerRef, open, setOpen]
  120. }
  121. export function useLexicalTextEntity<T extends TextNode>(
  122. getMatch: (text: string) => null | EntityMatch,
  123. targetNode: Klass<T>,
  124. createNode: (textNode: CustomTextNode) => T,
  125. ) {
  126. const [editor] = useLexicalComposerContext()
  127. useEffect(() => {
  128. return mergeRegister(...registerLexicalTextEntity(editor, getMatch, targetNode, createNode))
  129. }, [createNode, editor, getMatch, targetNode])
  130. }
  131. export type MenuTextMatch = {
  132. leadOffset: number
  133. matchingString: string
  134. replaceableString: string
  135. }
  136. export type TriggerFn = (
  137. text: string,
  138. editor: LexicalEditor,
  139. ) => MenuTextMatch | null
  140. export const PUNCTUATION = '\\.,\\+\\*\\?\\$\\@\\|#{}\\(\\)\\^\\-\\[\\]\\\\/!%\'"~=<>_:;'
  141. export function useBasicTypeaheadTriggerMatch(
  142. trigger: string,
  143. { minLength = 1, maxLength = 75 }: { minLength?: number; maxLength?: number },
  144. ): TriggerFn {
  145. return useCallback(
  146. (text: string) => {
  147. const validChars = `[${PUNCTUATION}\\s]`
  148. const TypeaheadTriggerRegex = new RegExp(
  149. '(.*)('
  150. + `[${trigger}]`
  151. + `((?:${validChars}){0,${maxLength}})`
  152. + ')$',
  153. )
  154. const match = TypeaheadTriggerRegex.exec(text)
  155. if (match !== null) {
  156. const maybeLeadingWhitespace = match[1]
  157. const matchingString = match[3]
  158. if (matchingString.length >= minLength) {
  159. return {
  160. leadOffset: match.index + maybeLeadingWhitespace.length,
  161. matchingString,
  162. replaceableString: match[2],
  163. }
  164. }
  165. }
  166. return null
  167. },
  168. [maxLength, minLength, trigger],
  169. )
  170. }