editor-dom.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. export function createTextFragment(content: string): DocumentFragment {
  2. const fragment = document.createDocumentFragment()
  3. const segments = content.split("\n")
  4. segments.forEach((segment, index) => {
  5. if (segment) {
  6. fragment.appendChild(document.createTextNode(segment))
  7. } else if (segments.length > 1) {
  8. fragment.appendChild(document.createTextNode("\u200B"))
  9. }
  10. if (index < segments.length - 1) {
  11. fragment.appendChild(document.createElement("br"))
  12. }
  13. })
  14. return fragment
  15. }
  16. export function getNodeLength(node: Node): number {
  17. if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === "BR") return 1
  18. return (node.textContent ?? "").replace(/\u200B/g, "").length
  19. }
  20. export function getTextLength(node: Node): number {
  21. if (node.nodeType === Node.TEXT_NODE) return (node.textContent ?? "").replace(/\u200B/g, "").length
  22. if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === "BR") return 1
  23. let length = 0
  24. for (const child of Array.from(node.childNodes)) {
  25. length += getTextLength(child)
  26. }
  27. return length
  28. }
  29. export function getCursorPosition(parent: HTMLElement): number {
  30. const selection = window.getSelection()
  31. if (!selection || selection.rangeCount === 0) return 0
  32. const range = selection.getRangeAt(0)
  33. if (!parent.contains(range.startContainer)) return 0
  34. const preCaretRange = range.cloneRange()
  35. preCaretRange.selectNodeContents(parent)
  36. preCaretRange.setEnd(range.startContainer, range.startOffset)
  37. return getTextLength(preCaretRange.cloneContents())
  38. }
  39. export function setCursorPosition(parent: HTMLElement, position: number) {
  40. let remaining = position
  41. let node = parent.firstChild
  42. while (node) {
  43. const length = getNodeLength(node)
  44. const isText = node.nodeType === Node.TEXT_NODE
  45. const isPill =
  46. node.nodeType === Node.ELEMENT_NODE &&
  47. ((node as HTMLElement).dataset.type === "file" || (node as HTMLElement).dataset.type === "agent")
  48. const isBreak = node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === "BR"
  49. if (isText && remaining <= length) {
  50. const range = document.createRange()
  51. const selection = window.getSelection()
  52. range.setStart(node, remaining)
  53. range.collapse(true)
  54. selection?.removeAllRanges()
  55. selection?.addRange(range)
  56. return
  57. }
  58. if ((isPill || isBreak) && remaining <= length) {
  59. const range = document.createRange()
  60. const selection = window.getSelection()
  61. if (remaining === 0) {
  62. range.setStartBefore(node)
  63. }
  64. if (remaining > 0 && isPill) {
  65. range.setStartAfter(node)
  66. }
  67. if (remaining > 0 && isBreak) {
  68. const next = node.nextSibling
  69. if (next && next.nodeType === Node.TEXT_NODE) {
  70. range.setStart(next, 0)
  71. }
  72. if (!next || next.nodeType !== Node.TEXT_NODE) {
  73. range.setStartAfter(node)
  74. }
  75. }
  76. range.collapse(true)
  77. selection?.removeAllRanges()
  78. selection?.addRange(range)
  79. return
  80. }
  81. remaining -= length
  82. node = node.nextSibling
  83. }
  84. const fallbackRange = document.createRange()
  85. const fallbackSelection = window.getSelection()
  86. const last = parent.lastChild
  87. if (last && last.nodeType === Node.TEXT_NODE) {
  88. const len = last.textContent ? last.textContent.length : 0
  89. fallbackRange.setStart(last, len)
  90. }
  91. if (!last || last.nodeType !== Node.TEXT_NODE) {
  92. fallbackRange.selectNodeContents(parent)
  93. }
  94. fallbackRange.collapse(false)
  95. fallbackSelection?.removeAllRanges()
  96. fallbackSelection?.addRange(fallbackRange)
  97. }
  98. export function setRangeEdge(parent: HTMLElement, range: Range, edge: "start" | "end", offset: number) {
  99. let remaining = offset
  100. const nodes = Array.from(parent.childNodes)
  101. for (const node of nodes) {
  102. const length = getNodeLength(node)
  103. const isText = node.nodeType === Node.TEXT_NODE
  104. const isPill =
  105. node.nodeType === Node.ELEMENT_NODE &&
  106. ((node as HTMLElement).dataset.type === "file" || (node as HTMLElement).dataset.type === "agent")
  107. const isBreak = node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === "BR"
  108. if (isText && remaining <= length) {
  109. if (edge === "start") range.setStart(node, remaining)
  110. if (edge === "end") range.setEnd(node, remaining)
  111. return
  112. }
  113. if ((isPill || isBreak) && remaining <= length) {
  114. if (edge === "start" && remaining === 0) range.setStartBefore(node)
  115. if (edge === "start" && remaining > 0) range.setStartAfter(node)
  116. if (edge === "end" && remaining === 0) range.setEndBefore(node)
  117. if (edge === "end" && remaining > 0) range.setEndAfter(node)
  118. return
  119. }
  120. remaining -= length
  121. }
  122. }