136 lines
4.6 KiB
TypeScript
136 lines
4.6 KiB
TypeScript
export function createTextFragment(content: string): DocumentFragment {
|
|
const fragment = document.createDocumentFragment()
|
|
const segments = content.split("\n")
|
|
segments.forEach((segment, index) => {
|
|
if (segment) {
|
|
fragment.appendChild(document.createTextNode(segment))
|
|
} else if (segments.length > 1) {
|
|
fragment.appendChild(document.createTextNode("\u200B"))
|
|
}
|
|
if (index < segments.length - 1) {
|
|
fragment.appendChild(document.createElement("br"))
|
|
}
|
|
})
|
|
return fragment
|
|
}
|
|
|
|
export function getNodeLength(node: Node): number {
|
|
if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === "BR") return 1
|
|
return (node.textContent ?? "").replace(/\u200B/g, "").length
|
|
}
|
|
|
|
export function getTextLength(node: Node): number {
|
|
if (node.nodeType === Node.TEXT_NODE) return (node.textContent ?? "").replace(/\u200B/g, "").length
|
|
if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === "BR") return 1
|
|
let length = 0
|
|
for (const child of Array.from(node.childNodes)) {
|
|
length += getTextLength(child)
|
|
}
|
|
return length
|
|
}
|
|
|
|
export function getCursorPosition(parent: HTMLElement): number {
|
|
const selection = window.getSelection()
|
|
if (!selection || selection.rangeCount === 0) return 0
|
|
const range = selection.getRangeAt(0)
|
|
if (!parent.contains(range.startContainer)) return 0
|
|
const preCaretRange = range.cloneRange()
|
|
preCaretRange.selectNodeContents(parent)
|
|
preCaretRange.setEnd(range.startContainer, range.startOffset)
|
|
return getTextLength(preCaretRange.cloneContents())
|
|
}
|
|
|
|
export function setCursorPosition(parent: HTMLElement, position: number) {
|
|
let remaining = position
|
|
let node = parent.firstChild
|
|
while (node) {
|
|
const length = getNodeLength(node)
|
|
const isText = node.nodeType === Node.TEXT_NODE
|
|
const isPill =
|
|
node.nodeType === Node.ELEMENT_NODE &&
|
|
((node as HTMLElement).dataset.type === "file" || (node as HTMLElement).dataset.type === "agent")
|
|
const isBreak = node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === "BR"
|
|
|
|
if (isText && remaining <= length) {
|
|
const range = document.createRange()
|
|
const selection = window.getSelection()
|
|
range.setStart(node, remaining)
|
|
range.collapse(true)
|
|
selection?.removeAllRanges()
|
|
selection?.addRange(range)
|
|
return
|
|
}
|
|
|
|
if ((isPill || isBreak) && remaining <= length) {
|
|
const range = document.createRange()
|
|
const selection = window.getSelection()
|
|
if (remaining === 0) {
|
|
range.setStartBefore(node)
|
|
}
|
|
if (remaining > 0 && isPill) {
|
|
range.setStartAfter(node)
|
|
}
|
|
if (remaining > 0 && isBreak) {
|
|
const next = node.nextSibling
|
|
if (next && next.nodeType === Node.TEXT_NODE) {
|
|
range.setStart(next, 0)
|
|
}
|
|
if (!next || next.nodeType !== Node.TEXT_NODE) {
|
|
range.setStartAfter(node)
|
|
}
|
|
}
|
|
range.collapse(true)
|
|
selection?.removeAllRanges()
|
|
selection?.addRange(range)
|
|
return
|
|
}
|
|
|
|
remaining -= length
|
|
node = node.nextSibling
|
|
}
|
|
|
|
const fallbackRange = document.createRange()
|
|
const fallbackSelection = window.getSelection()
|
|
const last = parent.lastChild
|
|
if (last && last.nodeType === Node.TEXT_NODE) {
|
|
const len = last.textContent ? last.textContent.length : 0
|
|
fallbackRange.setStart(last, len)
|
|
}
|
|
if (!last || last.nodeType !== Node.TEXT_NODE) {
|
|
fallbackRange.selectNodeContents(parent)
|
|
}
|
|
fallbackRange.collapse(false)
|
|
fallbackSelection?.removeAllRanges()
|
|
fallbackSelection?.addRange(fallbackRange)
|
|
}
|
|
|
|
export function setRangeEdge(parent: HTMLElement, range: Range, edge: "start" | "end", offset: number) {
|
|
let remaining = offset
|
|
const nodes = Array.from(parent.childNodes)
|
|
|
|
for (const node of nodes) {
|
|
const length = getNodeLength(node)
|
|
const isText = node.nodeType === Node.TEXT_NODE
|
|
const isPill =
|
|
node.nodeType === Node.ELEMENT_NODE &&
|
|
((node as HTMLElement).dataset.type === "file" || (node as HTMLElement).dataset.type === "agent")
|
|
const isBreak = node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === "BR"
|
|
|
|
if (isText && remaining <= length) {
|
|
if (edge === "start") range.setStart(node, remaining)
|
|
if (edge === "end") range.setEnd(node, remaining)
|
|
return
|
|
}
|
|
|
|
if ((isPill || isBreak) && remaining <= length) {
|
|
if (edge === "start" && remaining === 0) range.setStartBefore(node)
|
|
if (edge === "start" && remaining > 0) range.setStartAfter(node)
|
|
if (edge === "end" && remaining === 0) range.setEndBefore(node)
|
|
if (edge === "end" && remaining > 0) range.setEndAfter(node)
|
|
return
|
|
}
|
|
|
|
remaining -= length
|
|
}
|
|
}
|