utils.ts 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. import { $isAtNodeEnd } from '@lexical/selection'
  2. import type {
  3. ElementNode,
  4. Klass,
  5. LexicalEditor,
  6. LexicalNode,
  7. RangeSelection,
  8. TextNode,
  9. } from 'lexical'
  10. import {
  11. $createTextNode,
  12. $isTextNode,
  13. } from 'lexical'
  14. import type { EntityMatch } from '@lexical/text'
  15. import { CustomTextNode } from './plugins/custom-text/node'
  16. export function getSelectedNode(
  17. selection: RangeSelection,
  18. ): TextNode | ElementNode {
  19. const anchor = selection.anchor
  20. const focus = selection.focus
  21. const anchorNode = selection.anchor.getNode()
  22. const focusNode = selection.focus.getNode()
  23. if (anchorNode === focusNode)
  24. return anchorNode
  25. const isBackward = selection.isBackward()
  26. if (isBackward)
  27. return $isAtNodeEnd(focus) ? anchorNode : focusNode
  28. else
  29. return $isAtNodeEnd(anchor) ? anchorNode : focusNode
  30. }
  31. export function registerLexicalTextEntity<T extends TextNode>(
  32. editor: LexicalEditor,
  33. getMatch: (text: string) => null | EntityMatch,
  34. targetNode: Klass<T>,
  35. createNode: (textNode: TextNode) => T,
  36. ) {
  37. const isTargetNode = (node: LexicalNode | null | undefined): node is T => {
  38. return node instanceof targetNode
  39. }
  40. const replaceWithSimpleText = (node: TextNode): void => {
  41. const textNode = $createTextNode(node.getTextContent())
  42. textNode.setFormat(node.getFormat())
  43. node.replace(textNode)
  44. }
  45. const getMode = (node: TextNode): number => {
  46. return node.getLatest().__mode
  47. }
  48. const textNodeTransform = (node: TextNode) => {
  49. if (!node.isSimpleText())
  50. return
  51. const prevSibling = node.getPreviousSibling()
  52. let text = node.getTextContent()
  53. let currentNode = node
  54. let match
  55. if ($isTextNode(prevSibling)) {
  56. const previousText = prevSibling.getTextContent()
  57. const combinedText = previousText + text
  58. const prevMatch = getMatch(combinedText)
  59. if (isTargetNode(prevSibling)) {
  60. if (prevMatch === null || getMode(prevSibling) !== 0) {
  61. replaceWithSimpleText(prevSibling)
  62. return
  63. }
  64. else {
  65. const diff = prevMatch.end - previousText.length
  66. if (diff > 0) {
  67. const concatText = text.slice(0, diff)
  68. const newTextContent = previousText + concatText
  69. prevSibling.select()
  70. prevSibling.setTextContent(newTextContent)
  71. if (diff === text.length) {
  72. node.remove()
  73. }
  74. else {
  75. const remainingText = text.slice(diff)
  76. node.setTextContent(remainingText)
  77. }
  78. return
  79. }
  80. }
  81. }
  82. else if (prevMatch === null || prevMatch.start < previousText.length) {
  83. return
  84. }
  85. }
  86. while (true) {
  87. match = getMatch(text)
  88. let nextText = match === null ? '' : text.slice(match.end)
  89. text = nextText
  90. if (nextText === '') {
  91. const nextSibling = currentNode.getNextSibling()
  92. if ($isTextNode(nextSibling)) {
  93. nextText = currentNode.getTextContent() + nextSibling.getTextContent()
  94. const nextMatch = getMatch(nextText)
  95. if (nextMatch === null) {
  96. if (isTargetNode(nextSibling))
  97. replaceWithSimpleText(nextSibling)
  98. else
  99. nextSibling.markDirty()
  100. return
  101. }
  102. else if (nextMatch.start !== 0) {
  103. return
  104. }
  105. }
  106. }
  107. else {
  108. const nextMatch = getMatch(nextText)
  109. if (nextMatch !== null && nextMatch.start === 0)
  110. return
  111. }
  112. if (match === null)
  113. return
  114. if (match.start === 0 && $isTextNode(prevSibling) && prevSibling.isTextEntity())
  115. continue
  116. let nodeToReplace
  117. if (match.start === 0)
  118. [nodeToReplace, currentNode] = currentNode.splitText(match.end)
  119. else
  120. [, nodeToReplace, currentNode] = currentNode.splitText(match.start, match.end)
  121. const replacementNode = createNode(nodeToReplace)
  122. replacementNode.setFormat(nodeToReplace.getFormat())
  123. nodeToReplace.replace(replacementNode)
  124. if (currentNode == null)
  125. return
  126. }
  127. }
  128. const reverseNodeTransform = (node: T) => {
  129. const text = node.getTextContent()
  130. const match = getMatch(text)
  131. if (match === null || match.start !== 0) {
  132. replaceWithSimpleText(node)
  133. return
  134. }
  135. if (text.length > match.end) {
  136. // This will split out the rest of the text as simple text
  137. node.splitText(match.end)
  138. return
  139. }
  140. const prevSibling = node.getPreviousSibling()
  141. if ($isTextNode(prevSibling) && prevSibling.isTextEntity()) {
  142. replaceWithSimpleText(prevSibling)
  143. replaceWithSimpleText(node)
  144. }
  145. const nextSibling = node.getNextSibling()
  146. if ($isTextNode(nextSibling) && nextSibling.isTextEntity()) {
  147. replaceWithSimpleText(nextSibling) // This may have already been converted in the previous block
  148. if (isTargetNode(node))
  149. replaceWithSimpleText(node)
  150. }
  151. }
  152. const removePlainTextTransform = editor.registerNodeTransform(CustomTextNode, textNodeTransform)
  153. const removeReverseNodeTransform = editor.registerNodeTransform(targetNode, reverseNodeTransform)
  154. return [removePlainTextTransform, removeReverseNodeTransform]
  155. }
  156. export const decoratorTransform = (
  157. node: CustomTextNode,
  158. getMatch: (text: string) => null | EntityMatch,
  159. createNode: () => LexicalNode,
  160. ) => {
  161. if (!node.isSimpleText())
  162. return
  163. const prevSibling = node.getPreviousSibling()
  164. let text = node.getTextContent()
  165. let currentNode = node
  166. let match
  167. while (true) {
  168. match = getMatch(text)
  169. let nextText = match === null ? '' : text.slice(match.end)
  170. text = nextText
  171. if (nextText === '') {
  172. const nextSibling = currentNode.getNextSibling()
  173. if ($isTextNode(nextSibling)) {
  174. nextText = currentNode.getTextContent() + nextSibling.getTextContent()
  175. const nextMatch = getMatch(nextText)
  176. if (nextMatch === null) {
  177. nextSibling.markDirty()
  178. return
  179. }
  180. else if (nextMatch.start !== 0) {
  181. return
  182. }
  183. }
  184. }
  185. else {
  186. const nextMatch = getMatch(nextText)
  187. if (nextMatch !== null && nextMatch.start === 0)
  188. return
  189. }
  190. if (match === null)
  191. return
  192. if (match.start === 0 && $isTextNode(prevSibling) && prevSibling.isTextEntity())
  193. continue
  194. let nodeToReplace
  195. if (match.start === 0)
  196. [nodeToReplace, currentNode] = currentNode.splitText(match.end)
  197. else
  198. [, nodeToReplace, currentNode] = currentNode.splitText(match.start, match.end)
  199. const replacementNode = createNode()
  200. nodeToReplace.replace(replacementNode)
  201. if (currentNode == null)
  202. return
  203. }
  204. }
  205. export function textToEditorState(text: string) {
  206. const paragraph = text.split('\n')
  207. return JSON.stringify({
  208. root: {
  209. children: paragraph.map((p) => {
  210. return {
  211. children: [{
  212. detail: 0,
  213. format: 0,
  214. mode: 'normal',
  215. style: '',
  216. text: p,
  217. type: 'custom-text',
  218. version: 1,
  219. }],
  220. direction: 'ltr',
  221. format: '',
  222. indent: 0,
  223. type: 'paragraph',
  224. version: 1,
  225. }
  226. }),
  227. direction: 'ltr',
  228. format: '',
  229. indent: 0,
  230. type: 'root',
  231. version: 1,
  232. },
  233. })
  234. }