tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 }