tor-browser

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

capturekeys.ts (13414B)


      1 import {Selection, NodeSelection, TextSelection, AllSelection, EditorState} from "prosemirror-state"
      2 import {EditorView} from "./index"
      3 import * as browser from "./browser"
      4 import {domIndex, selectionCollapsed, hasBlockDesc} from "./dom"
      5 import {selectionToDOM} from "./selection"
      6 
      7 function moveSelectionBlock(state: EditorState, dir: number) {
      8  let {$anchor, $head} = state.selection
      9  let $side = dir > 0 ? $anchor.max($head) : $anchor.min($head)
     10  let $start = !$side.parent.inlineContent ? $side : $side.depth ? state.doc.resolve(dir > 0 ? $side.after() : $side.before()) : null
     11  return $start && Selection.findFrom($start, dir)
     12 }
     13 
     14 function apply(view: EditorView, sel: Selection) {
     15  view.dispatch(view.state.tr.setSelection(sel).scrollIntoView())
     16  return true
     17 }
     18 
     19 function selectHorizontally(view: EditorView, dir: number, mods: string) {
     20  let sel = view.state.selection
     21  if (sel instanceof TextSelection) {
     22    if (mods.indexOf("s") > -1) {
     23      let {$head} = sel, node = $head.textOffset ? null : dir < 0 ? $head.nodeBefore : $head.nodeAfter
     24      if (!node || node.isText || !node.isLeaf) return false
     25      let $newHead = view.state.doc.resolve($head.pos + node.nodeSize * (dir < 0 ? -1 : 1))
     26      return apply(view, new TextSelection(sel.$anchor, $newHead))
     27    } else if (!sel.empty) {
     28      return false
     29    } else if (view.endOfTextblock(dir > 0 ? "forward" : "backward")) {
     30      let next = moveSelectionBlock(view.state, dir)
     31      if (next && (next instanceof NodeSelection)) return apply(view, next)
     32      return false
     33    } else if (!(browser.mac && mods.indexOf("m") > -1)) {
     34      let $head = sel.$head, node = $head.textOffset ? null : dir < 0 ? $head.nodeBefore : $head.nodeAfter, desc
     35      if (!node || node.isText) return false
     36      let nodePos = dir < 0 ? $head.pos - node.nodeSize : $head.pos
     37      if (!(node.isAtom || (desc = view.docView.descAt(nodePos)) && !desc.contentDOM)) return false
     38      if (NodeSelection.isSelectable(node)) {
     39        return apply(view, new NodeSelection(dir < 0 ? view.state.doc.resolve($head.pos - node.nodeSize) : $head))
     40      } else if (browser.webkit) {
     41        // Chrome and Safari will introduce extra pointless cursor
     42        // positions around inline uneditable nodes, so we have to
     43        // take over and move the cursor past them (#937)
     44        return apply(view, new TextSelection(view.state.doc.resolve(dir < 0 ? nodePos : nodePos + node.nodeSize)))
     45      } else {
     46        return false
     47      }
     48    }
     49  } else if (sel instanceof NodeSelection && sel.node.isInline) {
     50    return apply(view, new TextSelection(dir > 0 ? sel.$to : sel.$from))
     51  } else {
     52    let next = moveSelectionBlock(view.state, dir)
     53    if (next) return apply(view, next)
     54    return false
     55  }
     56 }
     57 
     58 function nodeLen(node: Node) {
     59  return node.nodeType == 3 ? node.nodeValue!.length : node.childNodes.length
     60 }
     61 
     62 function isIgnorable(dom: Node, dir: number) {
     63  let desc = dom.pmViewDesc
     64  return desc && desc.size == 0 && (dir < 0 || dom.nextSibling || dom.nodeName != "BR")
     65 }
     66 
     67 function skipIgnoredNodes(view: EditorView, dir: number) {
     68  return dir < 0 ? skipIgnoredNodesBefore(view) : skipIgnoredNodesAfter(view)
     69 }
     70 
     71 // Make sure the cursor isn't directly after one or more ignored
     72 // nodes, which will confuse the browser's cursor motion logic.
     73 function skipIgnoredNodesBefore(view: EditorView) {
     74  let sel = view.domSelectionRange()
     75  let node = sel.focusNode!, offset = sel.focusOffset
     76  if (!node) return
     77  let moveNode, moveOffset: number | undefined, force = false
     78  // Gecko will do odd things when the selection is directly in front
     79  // of a non-editable node, so in that case, move it into the next
     80  // node if possible. Issue prosemirror/prosemirror#832.
     81  if (browser.gecko && node.nodeType == 1 && offset < nodeLen(node) && isIgnorable(node.childNodes[offset], -1)) force = true
     82  for (;;) {
     83    if (offset > 0) {
     84      if (node.nodeType != 1) {
     85        break
     86      } else {
     87        let before = node.childNodes[offset - 1]
     88        if (isIgnorable(before, -1)) {
     89          moveNode = node
     90          moveOffset = --offset
     91        } else if (before.nodeType == 3) {
     92          node = before
     93          offset = node.nodeValue!.length
     94        } else break
     95      }
     96    } else if (isBlockNode(node)) {
     97      break
     98    } else {
     99      let prev = node.previousSibling
    100      while (prev && isIgnorable(prev, -1)) {
    101        moveNode = node.parentNode
    102        moveOffset = domIndex(prev)
    103        prev = prev.previousSibling
    104      }
    105      if (!prev) {
    106        node = node.parentNode!
    107        if (node == view.dom) break
    108        offset = 0
    109      } else {
    110        node = prev
    111        offset = nodeLen(node)
    112      }
    113    }
    114  }
    115  if (force) setSelFocus(view, node, offset)
    116  else if (moveNode) setSelFocus(view, moveNode, moveOffset!)
    117 }
    118 
    119 // Make sure the cursor isn't directly before one or more ignored
    120 // nodes.
    121 function skipIgnoredNodesAfter(view: EditorView) {
    122  let sel = view.domSelectionRange()
    123  let node = sel.focusNode!, offset = sel.focusOffset
    124  if (!node) return
    125  let len = nodeLen(node)
    126  let moveNode, moveOffset: number | undefined
    127  for (;;) {
    128    if (offset < len) {
    129      if (node.nodeType != 1) break
    130      let after = node.childNodes[offset]
    131      if (isIgnorable(after, 1)) {
    132        moveNode = node
    133        moveOffset = ++offset
    134      }
    135      else break
    136    } else if (isBlockNode(node)) {
    137      break
    138    } else {
    139      let next = node.nextSibling
    140      while (next && isIgnorable(next, 1)) {
    141        moveNode = next.parentNode
    142        moveOffset = domIndex(next) + 1
    143        next = next.nextSibling
    144      }
    145      if (!next) {
    146        node = node.parentNode!
    147        if (node == view.dom) break
    148        offset = len = 0
    149      } else {
    150        node = next
    151        offset = 0
    152        len = nodeLen(node)
    153      }
    154    }
    155  }
    156  if (moveNode) setSelFocus(view, moveNode, moveOffset!)
    157 }
    158 
    159 function isBlockNode(dom: Node) {
    160  let desc = dom.pmViewDesc
    161  return desc && desc.node && desc.node.isBlock
    162 }
    163 
    164 function textNodeAfter(node: Node | null, offset: number): Text | undefined {
    165  while (node && offset == node.childNodes.length && !hasBlockDesc(node)) {
    166    offset = domIndex(node) + 1
    167    node = node.parentNode
    168  }
    169  while (node && offset < node.childNodes.length) {
    170    let next = node.childNodes[offset]
    171    if (next.nodeType == 3) return next as Text
    172    if (next.nodeType == 1 && (next as HTMLElement).contentEditable == "false") break
    173    node = next
    174    offset = 0
    175  }
    176 }
    177 
    178 function textNodeBefore(node: Node | null, offset: number): Text | undefined {
    179  while (node && !offset && !hasBlockDesc(node)) {
    180    offset = domIndex(node)
    181    node = node.parentNode
    182  }
    183  while (node && offset) {
    184    let next = node.childNodes[offset - 1]
    185    if (next.nodeType == 3) return next as Text
    186    if (next.nodeType == 1 && (next as HTMLElement).contentEditable == "false") break
    187    node = next
    188    offset = node.childNodes.length
    189  }
    190 }
    191 
    192 function setSelFocus(view: EditorView, node: Node, offset: number) {
    193  if (node.nodeType != 3) {
    194    let before, after
    195    if (after = textNodeAfter(node, offset)) {
    196      node = after
    197      offset = 0
    198    } else if (before = textNodeBefore(node, offset)) {
    199      node = before
    200      offset = before.nodeValue!.length
    201    }
    202  }
    203 
    204  let sel = view.domSelection()
    205  if (!sel) return
    206  if (selectionCollapsed(sel)) {
    207    let range = document.createRange()
    208    range.setEnd(node, offset)
    209    range.setStart(node, offset)
    210    sel.removeAllRanges()
    211    sel.addRange(range)
    212  } else if (sel.extend) {
    213    sel.extend(node, offset)
    214  }
    215  view.domObserver.setCurSelection()
    216  let {state} = view
    217  // If no state update ends up happening, reset the selection.
    218  setTimeout(() => {
    219    if (view.state == state) selectionToDOM(view)
    220  }, 50)
    221 }
    222 
    223 function findDirection(view: EditorView, pos: number): "rtl" | "ltr" {
    224  let $pos = view.state.doc.resolve(pos)
    225  if (!(browser.chrome || browser.windows) && $pos.parent.inlineContent) {
    226    let coords = view.coordsAtPos(pos)
    227    if (pos > $pos.start()) {
    228      let before = view.coordsAtPos(pos - 1)
    229      let mid = (before.top + before.bottom) / 2
    230      if (mid > coords.top && mid < coords.bottom && Math.abs(before.left - coords.left) > 1)
    231        return before.left < coords.left ? "ltr" : "rtl"
    232    }
    233    if (pos < $pos.end()) {
    234      let after = view.coordsAtPos(pos + 1)
    235      let mid = (after.top + after.bottom) / 2
    236      if (mid > coords.top && mid < coords.bottom && Math.abs(after.left - coords.left) > 1)
    237        return after.left > coords.left ? "ltr" : "rtl"
    238    }
    239  }
    240  let computed = getComputedStyle(view.dom).direction
    241  return computed == "rtl" ? "rtl" : "ltr"
    242 }
    243 
    244 // Check whether vertical selection motion would involve node
    245 // selections. If so, apply it (if not, the result is left to the
    246 // browser)
    247 function selectVertically(view: EditorView, dir: number, mods: string) {
    248  let sel = view.state.selection
    249  if (sel instanceof TextSelection && !sel.empty || mods.indexOf("s") > -1) return false
    250  if (browser.mac && mods.indexOf("m") > -1) return false
    251  let {$from, $to} = sel
    252 
    253  if (!$from.parent.inlineContent || view.endOfTextblock(dir < 0 ? "up" : "down")) {
    254    let next = moveSelectionBlock(view.state, dir)
    255    if (next && (next instanceof NodeSelection))
    256      return apply(view, next)
    257  }
    258  if (!$from.parent.inlineContent) {
    259    let side = dir < 0 ? $from : $to
    260    let beyond = sel instanceof AllSelection ? Selection.near(side, dir) : Selection.findFrom(side, dir)
    261    return beyond ? apply(view, beyond) : false
    262  }
    263  return false
    264 }
    265 
    266 function stopNativeHorizontalDelete(view: EditorView, dir: number) {
    267  if (!(view.state.selection instanceof TextSelection)) return true
    268  let {$head, $anchor, empty} = view.state.selection
    269  if (!$head.sameParent($anchor)) return true
    270  if (!empty) return false
    271  if (view.endOfTextblock(dir > 0 ? "forward" : "backward")) return true
    272  let nextNode = !$head.textOffset && (dir < 0 ? $head.nodeBefore : $head.nodeAfter)
    273  if (nextNode && !nextNode.isText) {
    274    let tr = view.state.tr
    275    if (dir < 0) tr.delete($head.pos - nextNode.nodeSize, $head.pos)
    276    else tr.delete($head.pos, $head.pos + nextNode.nodeSize)
    277    view.dispatch(tr)
    278    return true
    279  }
    280  return false
    281 }
    282 
    283 function switchEditable(view: EditorView, node: HTMLElement, state: string) {
    284  view.domObserver.stop()
    285  node.contentEditable = state
    286  view.domObserver.start()
    287 }
    288 
    289 // Issue #867 / #1090 / https://bugs.chromium.org/p/chromium/issues/detail?id=903821
    290 // In which Safari (and at some point in the past, Chrome) does really
    291 // wrong things when the down arrow is pressed when the cursor is
    292 // directly at the start of a textblock and has an uneditable node
    293 // after it
    294 function safariDownArrowBug(view: EditorView) {
    295  if (!browser.safari || view.state.selection.$head.parentOffset > 0) return false
    296  let {focusNode, focusOffset} = view.domSelectionRange()
    297  if (focusNode && focusNode.nodeType == 1 && focusOffset == 0 &&
    298      focusNode.firstChild && (focusNode.firstChild as HTMLElement).contentEditable == "false") {
    299    let child = focusNode.firstChild as HTMLElement
    300    switchEditable(view, child, "true")
    301    setTimeout(() => switchEditable(view, child, "false"), 20)
    302  }
    303  return false
    304 }
    305 
    306 // A backdrop key mapping used to make sure we always suppress keys
    307 // that have a dangerous default effect, even if the commands they are
    308 // bound to return false, and to make sure that cursor-motion keys
    309 // find a cursor (as opposed to a node selection) when pressed. For
    310 // cursor-motion keys, the code in the handlers also takes care of
    311 // block selections.
    312 
    313 function getMods(event: KeyboardEvent) {
    314  let result = ""
    315  if (event.ctrlKey) result += "c"
    316  if (event.metaKey) result += "m"
    317  if (event.altKey) result += "a"
    318  if (event.shiftKey) result += "s"
    319  return result
    320 }
    321 
    322 export function captureKeyDown(view: EditorView, event: KeyboardEvent) {
    323  let code = event.keyCode, mods = getMods(event)
    324  if (code == 8 || (browser.mac && code == 72 && mods == "c")) { // Backspace, Ctrl-h on Mac
    325    return stopNativeHorizontalDelete(view, -1) || skipIgnoredNodes(view, -1)
    326  } else if ((code == 46 && !event.shiftKey) || (browser.mac && code == 68 && mods == "c")) { // Delete, Ctrl-d on Mac
    327    return stopNativeHorizontalDelete(view, 1) || skipIgnoredNodes(view, 1)
    328  } else if (code == 13 || code == 27) { // Enter, Esc
    329    return true
    330  } else if (code == 37 || (browser.mac && code == 66 && mods == "c")) { // Left arrow, Ctrl-b on Mac
    331    let dir = code == 37 ? (findDirection(view, view.state.selection.from) == "ltr" ? -1 : 1) : -1
    332    return selectHorizontally(view, dir, mods) || skipIgnoredNodes(view, dir)
    333  } else if (code == 39 || (browser.mac && code == 70 && mods == "c")) { // Right arrow, Ctrl-f on Mac
    334    let dir = code == 39 ? (findDirection(view, view.state.selection.from) == "ltr" ? 1 : -1) : 1
    335    return selectHorizontally(view, dir, mods) || skipIgnoredNodes(view, dir)
    336  } else if (code == 38 || (browser.mac && code == 80 && mods == "c")) { // Up arrow, Ctrl-p on Mac
    337    return selectVertically(view, -1, mods) || skipIgnoredNodes(view, -1)
    338  } else if (code == 40 || (browser.mac && code == 78 && mods == "c")) { // Down arrow, Ctrl-n on Mac
    339    return safariDownArrowBug(view) || selectVertically(view, 1, mods) || skipIgnoredNodes(view, 1)
    340  } else if (mods == (browser.mac ? "m" : "c") &&
    341             (code == 66 || code == 73 || code == 89 || code == 90)) { // Mod-[biyz]
    342    return true
    343  }
    344  return false
    345 }