domcoords.ts (23712B)
1 import {EditorState} from "prosemirror-state" 2 import {nodeSize, textRange, parentNode, caretFromPoint} from "./dom" 3 import * as browser from "./browser" 4 import {EditorView} from "./index" 5 6 export type Rect = {left: number, right: number, top: number, bottom: number} 7 8 function windowRect(doc: Document): Rect { 9 let vp = doc.defaultView && doc.defaultView.visualViewport 10 if (vp) return { 11 left: 0, right: vp.width, 12 top: 0, bottom: vp.height 13 } 14 return {left: 0, right: doc.documentElement.clientWidth, 15 top: 0, bottom: doc.documentElement.clientHeight} 16 } 17 18 function getSide(value: number | Rect, side: keyof Rect): number { 19 return typeof value == "number" ? value : value[side] 20 } 21 22 function clientRect(node: HTMLElement): Rect { 23 let rect = node.getBoundingClientRect() 24 // Adjust for elements with style "transform: scale()" 25 let scaleX = (rect.width / node.offsetWidth) || 1 26 let scaleY = (rect.height / node.offsetHeight) || 1 27 // Make sure scrollbar width isn't included in the rectangle 28 return {left: rect.left, right: rect.left + node.clientWidth * scaleX, 29 top: rect.top, bottom: rect.top + node.clientHeight * scaleY} 30 } 31 32 export function scrollRectIntoView(view: EditorView, rect: Rect, startDOM: Node) { 33 let scrollThreshold = view.someProp("scrollThreshold") || 0, scrollMargin = view.someProp("scrollMargin") || 5 34 let doc = view.dom.ownerDocument 35 for (let parent: Node | null = startDOM || view.dom;;) { 36 if (!parent) break 37 if (parent.nodeType != 1) { parent = parentNode(parent); continue } 38 let elt = parent as HTMLElement 39 let atTop = elt == doc.body 40 let bounding = atTop ? windowRect(doc) : clientRect(elt as HTMLElement) 41 let moveX = 0, moveY = 0 42 if (rect.top < bounding.top + getSide(scrollThreshold, "top")) 43 moveY = -(bounding.top - rect.top + getSide(scrollMargin, "top")) 44 else if (rect.bottom > bounding.bottom - getSide(scrollThreshold, "bottom")) 45 moveY = rect.bottom - rect.top > bounding.bottom - bounding.top 46 ? rect.top + getSide(scrollMargin, "top") - bounding.top 47 : rect.bottom - bounding.bottom + getSide(scrollMargin, "bottom") 48 if (rect.left < bounding.left + getSide(scrollThreshold, "left")) 49 moveX = -(bounding.left - rect.left + getSide(scrollMargin, "left")) 50 else if (rect.right > bounding.right - getSide(scrollThreshold, "right")) 51 moveX = rect.right - bounding.right + getSide(scrollMargin, "right") 52 if (moveX || moveY) { 53 if (atTop) { 54 doc.defaultView!.scrollBy(moveX, moveY) 55 } else { 56 let startX = elt.scrollLeft, startY = elt.scrollTop 57 if (moveY) elt.scrollTop += moveY 58 if (moveX) elt.scrollLeft += moveX 59 let dX = elt.scrollLeft - startX, dY = elt.scrollTop - startY 60 rect = {left: rect.left - dX, top: rect.top - dY, right: rect.right - dX, bottom: rect.bottom - dY} 61 } 62 } 63 let pos: string = atTop ? "fixed" : getComputedStyle(parent as HTMLElement).position 64 if (/^(fixed|sticky)$/.test(pos)) break 65 parent = pos == "absolute" ? (parent as HTMLElement).offsetParent : parentNode(parent) 66 } 67 } 68 69 // Store the scroll position of the editor's parent nodes, along with 70 // the top position of an element near the top of the editor, which 71 // will be used to make sure the visible viewport remains stable even 72 // when the size of the content above changes. 73 export function storeScrollPos(view: EditorView): { 74 refDOM: HTMLElement, 75 refTop: number, 76 stack: {dom: HTMLElement, top: number, left: number}[] 77 } { 78 let rect = view.dom.getBoundingClientRect(), startY = Math.max(0, rect.top) 79 let refDOM: HTMLElement, refTop: number 80 for (let x = (rect.left + rect.right) / 2, y = startY + 1; 81 y < Math.min(innerHeight, rect.bottom); y += 5) { 82 let dom = view.root.elementFromPoint(x, y) 83 if (!dom || dom == view.dom || !view.dom.contains(dom)) continue 84 let localRect = (dom as HTMLElement).getBoundingClientRect() 85 if (localRect.top >= startY - 20) { 86 refDOM = dom as HTMLElement 87 refTop = localRect.top 88 break 89 } 90 } 91 return {refDOM: refDOM!, refTop: refTop!, stack: scrollStack(view.dom)} 92 } 93 94 function scrollStack(dom: Node): {dom: HTMLElement, top: number, left: number}[] { 95 let stack = [], doc = dom.ownerDocument 96 for (let cur: Node | null = dom; cur; cur = parentNode(cur)) { 97 stack.push({dom: cur as HTMLElement, top: (cur as HTMLElement).scrollTop, left: (cur as HTMLElement).scrollLeft}) 98 if (dom == doc) break 99 } 100 return stack 101 } 102 103 // Reset the scroll position of the editor's parent nodes to that what 104 // it was before, when storeScrollPos was called. 105 export function resetScrollPos({refDOM, refTop, stack}: { 106 refDOM: HTMLElement, 107 refTop: number, 108 stack: {dom: HTMLElement, top: number, left: number}[] 109 }) { 110 let newRefTop = refDOM ? refDOM.getBoundingClientRect().top : 0 111 restoreScrollStack(stack, newRefTop == 0 ? 0 : newRefTop - refTop) 112 } 113 114 function restoreScrollStack(stack: {dom: HTMLElement, top: number, left: number}[], dTop: number) { 115 for (let i = 0; i < stack.length; i++) { 116 let {dom, top, left} = stack[i] 117 if (dom.scrollTop != top + dTop) dom.scrollTop = top + dTop 118 if (dom.scrollLeft != left) dom.scrollLeft = left 119 } 120 } 121 122 let preventScrollSupported: false | null | {preventScroll: boolean} = null 123 // Feature-detects support for .focus({preventScroll: true}), and uses 124 // a fallback kludge when not supported. 125 export function focusPreventScroll(dom: HTMLElement) { 126 if ((dom as any).setActive) return (dom as any).setActive() // in IE 127 if (preventScrollSupported) return dom.focus(preventScrollSupported) 128 129 let stored = scrollStack(dom) 130 dom.focus(preventScrollSupported == null ? { 131 get preventScroll() { 132 preventScrollSupported = {preventScroll: true} 133 return true 134 } 135 } : undefined) 136 if (!preventScrollSupported) { 137 preventScrollSupported = false 138 restoreScrollStack(stored, 0) 139 } 140 } 141 142 function findOffsetInNode(node: HTMLElement, coords: {top: number, left: number}): {node: Node, offset: number} { 143 let closest, dxClosest = 2e8, coordsClosest: {left: number, top: number} | undefined, offset = 0 144 let rowBot = coords.top, rowTop = coords.top 145 let firstBelow: Node | undefined, coordsBelow: {left: number, top: number} | undefined 146 for (let child = node.firstChild, childIndex = 0; child; child = child.nextSibling, childIndex++) { 147 let rects 148 if (child.nodeType == 1) rects = (child as HTMLElement).getClientRects() 149 else if (child.nodeType == 3) rects = textRange(child as Text).getClientRects() 150 else continue 151 152 for (let i = 0; i < rects.length; i++) { 153 let rect = rects[i] 154 if (rect.top <= rowBot && rect.bottom >= rowTop) { 155 rowBot = Math.max(rect.bottom, rowBot) 156 rowTop = Math.min(rect.top, rowTop) 157 let dx = rect.left > coords.left ? rect.left - coords.left 158 : rect.right < coords.left ? coords.left - rect.right : 0 159 if (dx < dxClosest) { 160 closest = child 161 dxClosest = dx 162 coordsClosest = dx && closest.nodeType == 3 ? { 163 left: rect.right < coords.left ? rect.right : rect.left, 164 top: coords.top 165 } : coords 166 if (child.nodeType == 1 && dx) 167 offset = childIndex + (coords.left >= (rect.left + rect.right) / 2 ? 1 : 0) 168 continue 169 } 170 } else if (rect.top > coords.top && !firstBelow && rect.left <= coords.left && rect.right >= coords.left) { 171 firstBelow = child 172 coordsBelow = {left: Math.max(rect.left, Math.min(rect.right, coords.left)), top: rect.top} 173 } 174 if (!closest && (coords.left >= rect.right && coords.top >= rect.top || 175 coords.left >= rect.left && coords.top >= rect.bottom)) 176 offset = childIndex + 1 177 } 178 } 179 if (!closest && firstBelow) { closest = firstBelow; coordsClosest = coordsBelow; dxClosest = 0 } 180 if (closest && closest.nodeType == 3) return findOffsetInText(closest as Text, coordsClosest!) 181 if (!closest || (dxClosest && closest.nodeType == 1)) return {node, offset} 182 return findOffsetInNode(closest as HTMLElement, coordsClosest!) 183 } 184 185 function findOffsetInText(node: Text, coords: {top: number, left: number}) { 186 let len = node.nodeValue!.length 187 let range = document.createRange(), result: {node: Node, offset: number} | undefined 188 for (let i = 0; i < len; i++) { 189 range.setEnd(node, i + 1) 190 range.setStart(node, i) 191 let rect = singleRect(range, 1) 192 if (rect.top == rect.bottom) continue 193 if (inRect(coords, rect)) { 194 result = {node, offset: i + (coords.left >= (rect.left + rect.right) / 2 ? 1 : 0)} 195 break 196 } 197 } 198 range.detach() 199 return result || {node, offset: 0} 200 } 201 202 function inRect(coords: {top: number, left: number}, rect: Rect) { 203 return coords.left >= rect.left - 1 && coords.left <= rect.right + 1&& 204 coords.top >= rect.top - 1 && coords.top <= rect.bottom + 1 205 } 206 207 function targetKludge(dom: HTMLElement, coords: {top: number, left: number}) { 208 let parent = dom.parentNode 209 if (parent && /^li$/i.test(parent.nodeName) && coords.left < dom.getBoundingClientRect().left) 210 return parent as HTMLElement 211 return dom 212 } 213 214 function posFromElement(view: EditorView, elt: HTMLElement, coords: {top: number, left: number}) { 215 let {node, offset} = findOffsetInNode(elt, coords), bias = -1 216 if (node.nodeType == 1 && !node.firstChild) { 217 let rect = (node as HTMLElement).getBoundingClientRect() 218 bias = rect.left != rect.right && coords.left > (rect.left + rect.right) / 2 ? 1 : -1 219 } 220 return view.docView.posFromDOM(node, offset, bias) 221 } 222 223 function posFromCaret(view: EditorView, node: Node, offset: number, coords: {top: number, left: number}) { 224 // Browser (in caretPosition/RangeFromPoint) will agressively 225 // normalize towards nearby inline nodes. Since we are interested in 226 // positions between block nodes too, we first walk up the hierarchy 227 // of nodes to see if there are block nodes that the coordinates 228 // fall outside of. If so, we take the position before/after that 229 // block. If not, we call `posFromDOM` on the raw node/offset. 230 let outsideBlock = -1 231 for (let cur = node, sawBlock = false;;) { 232 if (cur == view.dom) break 233 let desc = view.docView.nearestDesc(cur, true), rect 234 if (!desc) return null 235 if (desc.dom.nodeType == 1 && (desc.node.isBlock && desc.parent || !desc.contentDOM) && 236 // Ignore elements with zero-size bounding rectangles 237 ((rect = (desc.dom as HTMLElement).getBoundingClientRect()).width || rect.height)) { 238 if (desc.node.isBlock && desc.parent && !/^T(R|BODY|HEAD|FOOT)$/.test(desc.dom!.nodeName)) { 239 // Only apply the horizontal test to the innermost block. Vertical for any parent. 240 if (!sawBlock && rect.left > coords.left || rect.top > coords.top) outsideBlock = desc.posBefore 241 else if (!sawBlock && rect.right < coords.left || rect.bottom < coords.top) outsideBlock = desc.posAfter 242 sawBlock = true 243 } 244 if (!desc.contentDOM && outsideBlock < 0 && !desc.node.isText) { 245 // If we are inside a leaf, return the side of the leaf closer to the coords 246 let before = desc.node.isBlock ? coords.top < (rect.top + rect.bottom) / 2 247 : coords.left < (rect.left + rect.right) / 2 248 return before ? desc.posBefore : desc.posAfter 249 } 250 } 251 cur = desc.dom.parentNode! 252 } 253 return outsideBlock > -1 ? outsideBlock : view.docView.posFromDOM(node, offset, -1) 254 } 255 256 function elementFromPoint(element: HTMLElement, coords: {top: number, left: number}, box: Rect): HTMLElement { 257 let len = element.childNodes.length 258 if (len && box.top < box.bottom) { 259 for (let startI = Math.max(0, Math.min(len - 1, Math.floor(len * (coords.top - box.top) / (box.bottom - box.top)) - 2)), i = startI;;) { 260 let child = element.childNodes[i] 261 if (child.nodeType == 1) { 262 let rects = (child as HTMLElement).getClientRects() 263 for (let j = 0; j < rects.length; j++) { 264 let rect = rects[j] 265 if (inRect(coords, rect)) return elementFromPoint(child as HTMLElement, coords, rect) 266 } 267 } 268 if ((i = (i + 1) % len) == startI) break 269 } 270 } 271 return element 272 } 273 274 // Given an x,y position on the editor, get the position in the document. 275 export function posAtCoords(view: EditorView, coords: {top: number, left: number}) { 276 let doc = view.dom.ownerDocument, node: Node | undefined, offset = 0 277 let caret = caretFromPoint(doc, coords.left, coords.top) 278 if (caret) ({node, offset} = caret) 279 280 let elt = ((view.root as any).elementFromPoint ? view.root : doc) 281 .elementFromPoint(coords.left, coords.top) as HTMLElement 282 let pos 283 if (!elt || !view.dom.contains(elt.nodeType != 1 ? elt.parentNode : elt)) { 284 let box = view.dom.getBoundingClientRect() 285 if (!inRect(coords, box)) return null 286 elt = elementFromPoint(view.dom, coords, box) 287 if (!elt) return null 288 } 289 // Safari's caretRangeFromPoint returns nonsense when on a draggable element 290 if (browser.safari) { 291 for (let p: Node | null = elt; node && p; p = parentNode(p)) 292 if ((p as HTMLElement).draggable) node = undefined 293 } 294 elt = targetKludge(elt, coords) 295 if (node) { 296 if (browser.gecko && node.nodeType == 1) { 297 // Firefox will sometimes return offsets into <input> nodes, which 298 // have no actual children, from caretPositionFromPoint (#953) 299 offset = Math.min(offset, node.childNodes.length) 300 // It'll also move the returned position before image nodes, 301 // even if those are behind it. 302 if (offset < node.childNodes.length) { 303 let next = node.childNodes[offset], box 304 if (next.nodeName == "IMG" && (box = (next as HTMLElement).getBoundingClientRect()).right <= coords.left && 305 box.bottom > coords.top) 306 offset++ 307 } 308 } 309 let prev 310 // When clicking above the right side of an uneditable node, Chrome will report a cursor position after that node. 311 if (browser.webkit && offset && node.nodeType == 1 && (prev = node.childNodes[offset - 1]).nodeType == 1 && 312 (prev as HTMLElement).contentEditable == "false" && (prev as HTMLElement).getBoundingClientRect().top >= coords.top) 313 offset-- 314 // Suspiciously specific kludge to work around caret*FromPoint 315 // never returning a position at the end of the document 316 if (node == view.dom && offset == node.childNodes.length - 1 && node.lastChild!.nodeType == 1 && 317 coords.top > (node.lastChild as HTMLElement).getBoundingClientRect().bottom) 318 pos = view.state.doc.content.size 319 // Ignore positions directly after a BR, since caret*FromPoint 320 // 'round up' positions that would be more accurately placed 321 // before the BR node. 322 else if (offset == 0 || node.nodeType != 1 || node.childNodes[offset - 1].nodeName != "BR") 323 pos = posFromCaret(view, node, offset, coords) 324 } 325 if (pos == null) pos = posFromElement(view, elt, coords) 326 327 let desc = view.docView.nearestDesc(elt, true) 328 return {pos, inside: desc ? desc.posAtStart - desc.border : -1} 329 } 330 331 function nonZero(rect: DOMRect) { 332 return rect.top < rect.bottom || rect.left < rect.right 333 } 334 335 function singleRect(target: HTMLElement | Range, bias: number): DOMRect { 336 let rects = target.getClientRects() 337 if (rects.length) { 338 let first = rects[bias < 0 ? 0 : rects.length - 1] 339 if (nonZero(first)) return first 340 } 341 return Array.prototype.find.call(rects, nonZero) || target.getBoundingClientRect() 342 } 343 344 const BIDI = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/ 345 346 // Given a position in the document model, get a bounding box of the 347 // character at that position, relative to the window. 348 export function coordsAtPos(view: EditorView, pos: number, side: number): Rect { 349 let {node, offset, atom} = view.docView.domFromPos(pos, side < 0 ? -1 : 1) 350 351 let supportEmptyRange = browser.webkit || browser.gecko 352 if (node.nodeType == 3) { 353 // These browsers support querying empty text ranges. Prefer that in 354 // bidi context or when at the end of a node. 355 if (supportEmptyRange && (BIDI.test(node.nodeValue!) || (side < 0 ? !offset : offset == node.nodeValue!.length))) { 356 let rect = singleRect(textRange(node as Text, offset, offset), side) 357 // Firefox returns bad results (the position before the space) 358 // when querying a position directly after line-broken 359 // whitespace. Detect this situation and and kludge around it 360 if (browser.gecko && offset && /\s/.test(node.nodeValue![offset - 1]) && offset < node.nodeValue!.length) { 361 let rectBefore = singleRect(textRange(node as Text, offset - 1, offset - 1), -1) 362 if (rectBefore.top == rect.top) { 363 let rectAfter = singleRect(textRange(node as Text, offset, offset + 1), -1) 364 if (rectAfter.top != rect.top) 365 return flattenV(rectAfter, rectAfter.left < rectBefore.left) 366 } 367 } 368 return rect 369 } else { 370 let from = offset, to = offset, takeSide = side < 0 ? 1 : -1 371 if (side < 0 && !offset) { to++; takeSide = -1 } 372 else if (side >= 0 && offset == node.nodeValue!.length) { from--; takeSide = 1 } 373 else if (side < 0) { from-- } 374 else { to ++ } 375 return flattenV(singleRect(textRange(node as Text, from, to), takeSide), takeSide < 0) 376 } 377 } 378 379 let $dom = view.state.doc.resolve(pos - (atom || 0)) 380 // Return a horizontal line in block context 381 if (!$dom.parent.inlineContent) { 382 if (atom == null && offset && (side < 0 || offset == nodeSize(node))) { 383 let before = node.childNodes[offset - 1] 384 if (before.nodeType == 1) return flattenH((before as HTMLElement).getBoundingClientRect(), false) 385 } 386 if (atom == null && offset < nodeSize(node)) { 387 let after = node.childNodes[offset] 388 if (after.nodeType == 1) return flattenH((after as HTMLElement).getBoundingClientRect(), true) 389 } 390 return flattenH((node as HTMLElement).getBoundingClientRect(), side >= 0) 391 } 392 393 // Inline, not in text node (this is not Bidi-safe) 394 if (atom == null && offset && (side < 0 || offset == nodeSize(node))) { 395 let before = node.childNodes[offset - 1] 396 let target = before.nodeType == 3 ? textRange(before as Text, nodeSize(before) - (supportEmptyRange ? 0 : 1)) 397 // BR nodes tend to only return the rectangle before them. 398 // Only use them if they are the last element in their parent 399 : before.nodeType == 1 && (before.nodeName != "BR" || !before.nextSibling) ? before : null 400 if (target) return flattenV(singleRect(target as Range | HTMLElement, 1), false) 401 } 402 if (atom == null && offset < nodeSize(node)) { 403 let after = node.childNodes[offset] 404 while (after.pmViewDesc && after.pmViewDesc.ignoreForCoords) after = after.nextSibling! 405 let target = !after ? null : after.nodeType == 3 ? textRange(after as Text, 0, (supportEmptyRange ? 0 : 1)) 406 : after.nodeType == 1 ? after : null 407 if (target) return flattenV(singleRect(target as Range | HTMLElement, -1), true) 408 } 409 // All else failed, just try to get a rectangle for the target node 410 return flattenV(singleRect(node.nodeType == 3 ? textRange(node as Text) : node as HTMLElement, -side), side >= 0) 411 } 412 413 function flattenV(rect: DOMRect, left: boolean) { 414 if (rect.width == 0) return rect 415 let x = left ? rect.left : rect.right 416 return {top: rect.top, bottom: rect.bottom, left: x, right: x} 417 } 418 419 function flattenH(rect: DOMRect, top: boolean) { 420 if (rect.height == 0) return rect 421 let y = top ? rect.top : rect.bottom 422 return {top: y, bottom: y, left: rect.left, right: rect.right} 423 } 424 425 function withFlushedState<T>(view: EditorView, state: EditorState, f: () => T): T { 426 let viewState = view.state, active = view.root.activeElement as HTMLElement 427 if (viewState != state) view.updateState(state) 428 if (active != view.dom) view.focus() 429 try { 430 return f() 431 } finally { 432 if (viewState != state) view.updateState(viewState) 433 if (active != view.dom && active) active.focus() 434 } 435 } 436 437 // Whether vertical position motion in a given direction 438 // from a position would leave a text block. 439 function endOfTextblockVertical(view: EditorView, state: EditorState, dir: "up" | "down") { 440 let sel = state.selection 441 let $pos = dir == "up" ? sel.$from : sel.$to 442 return withFlushedState(view, state, () => { 443 let {node: dom} = view.docView.domFromPos($pos.pos, dir == "up" ? -1 : 1) 444 for (;;) { 445 let nearest = view.docView.nearestDesc(dom, true) 446 if (!nearest) break 447 if (nearest.node.isBlock) { dom = nearest.contentDOM || nearest.dom; break } 448 dom = nearest.dom.parentNode! 449 } 450 let coords = coordsAtPos(view, $pos.pos, 1) 451 for (let child = dom.firstChild; child; child = child.nextSibling) { 452 let boxes 453 if (child.nodeType == 1) boxes = (child as HTMLElement).getClientRects() 454 else if (child.nodeType == 3) boxes = textRange(child as Text, 0, child.nodeValue!.length).getClientRects() 455 else continue 456 for (let i = 0; i < boxes.length; i++) { 457 let box = boxes[i] 458 if (box.bottom > box.top + 1 && 459 (dir == "up" ? coords.top - box.top > (box.bottom - coords.top) * 2 460 : box.bottom - coords.bottom > (coords.bottom - box.top) * 2)) 461 return false 462 } 463 } 464 return true 465 }) 466 } 467 468 const maybeRTL = /[\u0590-\u08ac]/ 469 470 function endOfTextblockHorizontal(view: EditorView, state: EditorState, dir: "left" | "right" | "forward" | "backward") { 471 let {$head} = state.selection 472 if (!$head.parent.isTextblock) return false 473 let offset = $head.parentOffset, atStart = !offset, atEnd = offset == $head.parent.content.size 474 let sel: Selection = view.domSelection()! 475 if (!sel) return $head.pos == $head.start() || $head.pos == $head.end() 476 // If the textblock is all LTR, or the browser doesn't support 477 // Selection.modify (Edge), fall back to a primitive approach 478 if (!maybeRTL.test($head.parent.textContent) || !(sel as any).modify) 479 return dir == "left" || dir == "backward" ? atStart : atEnd 480 481 return withFlushedState(view, state, () => { 482 // This is a huge hack, but appears to be the best we can 483 // currently do: use `Selection.modify` to move the selection by 484 // one character, and see if that moves the cursor out of the 485 // textblock (or doesn't move it at all, when at the start/end of 486 // the document). 487 let {focusNode: oldNode, focusOffset: oldOff, anchorNode, anchorOffset} = view.domSelectionRange() 488 let oldBidiLevel = (sel as any).caretBidiLevel // Only for Firefox 489 ;(sel as any).modify("move", dir, "character") 490 let parentDOM = $head.depth ? view.docView.domAfterPos($head.before()) : view.dom 491 let {focusNode: newNode, focusOffset: newOff} = view.domSelectionRange() 492 let result = newNode && !parentDOM.contains(newNode.nodeType == 1 ? newNode : newNode.parentNode) || 493 (oldNode == newNode && oldOff == newOff) 494 // Restore the previous selection 495 try { 496 sel.collapse(anchorNode, anchorOffset) 497 if (oldNode && (oldNode != anchorNode || oldOff != anchorOffset) && sel.extend) sel.extend(oldNode, oldOff) 498 } catch (_) {} 499 if (oldBidiLevel != null) (sel as any).caretBidiLevel = oldBidiLevel 500 return result 501 }) 502 } 503 504 export type TextblockDir = "up" | "down" | "left" | "right" | "forward" | "backward" 505 506 let cachedState: EditorState | null = null 507 let cachedDir: TextblockDir | null = null 508 let cachedResult: boolean = false 509 export function endOfTextblock(view: EditorView, state: EditorState, dir: TextblockDir) { 510 if (cachedState == state && cachedDir == dir) return cachedResult 511 cachedState = state; cachedDir = dir 512 return cachedResult = dir == "up" || dir == "down" 513 ? endOfTextblockVertical(view, state, dir) 514 : endOfTextblockHorizontal(view, state, dir) 515 }