tor-browser

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

selection.ts (8979B)


      1 import {TextSelection, NodeSelection, Selection} from "prosemirror-state"
      2 import {ResolvedPos} from "prosemirror-model"
      3 
      4 import * as browser from "./browser"
      5 import {isEquivalentPosition, domIndex, isOnEdge, selectionCollapsed} from "./dom"
      6 import {EditorView} from "./index"
      7 import {NodeViewDesc} from "./viewdesc"
      8 
      9 export function selectionFromDOM(view: EditorView, origin: string | null = null) {
     10  let domSel = view.domSelectionRange(), doc = view.state.doc
     11  if (!domSel.focusNode) return null
     12  let nearestDesc = view.docView.nearestDesc(domSel.focusNode), inWidget = nearestDesc && nearestDesc.size == 0
     13  let head = view.docView.posFromDOM(domSel.focusNode, domSel.focusOffset, 1)
     14  if (head < 0) return null
     15  let $head = doc.resolve(head), anchor, selection
     16  if (selectionCollapsed(domSel)) {
     17    anchor = head
     18    while (nearestDesc && !nearestDesc.node) nearestDesc = nearestDesc.parent
     19    let nearestDescNode = (nearestDesc as NodeViewDesc).node
     20    if (nearestDesc && nearestDescNode.isAtom && NodeSelection.isSelectable(nearestDescNode) && nearestDesc.parent
     21        && !(nearestDescNode.isInline && isOnEdge(domSel.focusNode, domSel.focusOffset, nearestDesc.dom))) {
     22      let pos = nearestDesc.posBefore
     23      selection = new NodeSelection(head == pos ? $head : doc.resolve(pos))
     24    }
     25  } else {
     26    if (domSel instanceof view.dom.ownerDocument.defaultView!.Selection && domSel.rangeCount > 1) {
     27      let min = head, max = head
     28      for (let i = 0; i < domSel.rangeCount; i++) {
     29        let range = domSel.getRangeAt(i)
     30        min = Math.min(min, view.docView.posFromDOM(range.startContainer, range.startOffset, 1))
     31        max = Math.max(max, view.docView.posFromDOM(range.endContainer, range.endOffset, -1))
     32      }
     33      if (min < 0) return null
     34      ;[anchor, head] = max == view.state.selection.anchor ? [max, min] : [min, max]
     35      $head = doc.resolve(head)
     36    } else {
     37      anchor = view.docView.posFromDOM(domSel.anchorNode!, domSel.anchorOffset, 1)
     38    }
     39    if (anchor < 0) return null
     40  }
     41  let $anchor = doc.resolve(anchor)
     42 
     43  if (!selection) {
     44    let bias = origin == "pointer" || (view.state.selection.head < $head.pos && !inWidget) ? 1 : -1
     45    selection = selectionBetween(view, $anchor, $head, bias)
     46  }
     47  return selection
     48 }
     49 
     50 function editorOwnsSelection(view: EditorView) {
     51  return view.editable ? view.hasFocus() :
     52    hasSelection(view) && document.activeElement && document.activeElement.contains(view.dom)
     53 }
     54 
     55 export function selectionToDOM(view: EditorView, force = false) {
     56  let sel = view.state.selection
     57  syncNodeSelection(view, sel)
     58 
     59  if (!editorOwnsSelection(view)) return
     60 
     61  // The delayed drag selection causes issues with Cell Selections
     62  // in Safari. And the drag selection delay is to workarond issues
     63  // which only present in Chrome.
     64  if (!force && view.input.mouseDown && view.input.mouseDown.allowDefault && browser.chrome) {
     65    let domSel = view.domSelectionRange(), curSel = view.domObserver.currentSelection
     66    if (domSel.anchorNode && curSel.anchorNode &&
     67        isEquivalentPosition(domSel.anchorNode, domSel.anchorOffset,
     68                             curSel.anchorNode, curSel.anchorOffset)) {
     69      view.input.mouseDown.delayedSelectionSync = true
     70      view.domObserver.setCurSelection()
     71      return
     72    }
     73  }
     74 
     75  view.domObserver.disconnectSelection()
     76 
     77  if (view.cursorWrapper) {
     78    selectCursorWrapper(view)
     79  } else {
     80    let {anchor, head} = sel, resetEditableFrom, resetEditableTo
     81    if (brokenSelectBetweenUneditable && !(sel instanceof TextSelection)) {
     82      if (!sel.$from.parent.inlineContent)
     83        resetEditableFrom = temporarilyEditableNear(view, sel.from)
     84      if (!sel.empty && !sel.$from.parent.inlineContent)
     85        resetEditableTo = temporarilyEditableNear(view, sel.to)
     86    }
     87    view.docView.setSelection(anchor, head, view, force)
     88    if (brokenSelectBetweenUneditable) {
     89      if (resetEditableFrom) resetEditable(resetEditableFrom)
     90      if (resetEditableTo) resetEditable(resetEditableTo)
     91    }
     92    if (sel.visible) {
     93      view.dom.classList.remove("ProseMirror-hideselection")
     94    } else {
     95      view.dom.classList.add("ProseMirror-hideselection")
     96      if ("onselectionchange" in document) removeClassOnSelectionChange(view)
     97    }
     98  }
     99 
    100  view.domObserver.setCurSelection()
    101  view.domObserver.connectSelection()
    102 }
    103 
    104 // Kludge to work around Webkit not allowing a selection to start/end
    105 // between non-editable block nodes. We briefly make something
    106 // editable, set the selection, then set it uneditable again.
    107 
    108 const brokenSelectBetweenUneditable = browser.safari || browser.chrome && browser.chrome_version < 63
    109 
    110 function temporarilyEditableNear(view: EditorView, pos: number) {
    111  let {node, offset} = view.docView.domFromPos(pos, 0)
    112  let after = offset < node.childNodes.length ? node.childNodes[offset] : null
    113  let before = offset ? node.childNodes[offset - 1] : null
    114  if (browser.safari && after && (after as HTMLElement).contentEditable == "false") return setEditable(after as HTMLElement)
    115  if ((!after || (after as HTMLElement).contentEditable == "false") &&
    116      (!before || (before as HTMLElement).contentEditable == "false")) {
    117    if (after) return setEditable(after as HTMLElement)
    118    else if (before) return setEditable(before as HTMLElement)
    119  }
    120 }
    121 
    122 function setEditable(element: HTMLElement) {
    123  element.contentEditable = "true"
    124  if (browser.safari && element.draggable) { element.draggable = false; (element as any).wasDraggable = true }
    125  return element
    126 }
    127 
    128 function resetEditable(element: HTMLElement) {
    129  element.contentEditable = "false"
    130  if ((element as any).wasDraggable) { element.draggable = true; (element as any).wasDraggable = null }
    131 }
    132 
    133 function removeClassOnSelectionChange(view: EditorView) {
    134  let doc = view.dom.ownerDocument
    135  doc.removeEventListener("selectionchange", view.input.hideSelectionGuard!)
    136  let domSel = view.domSelectionRange()
    137  let node = domSel.anchorNode, offset = domSel.anchorOffset
    138  doc.addEventListener("selectionchange", view.input.hideSelectionGuard = () => {
    139    if (domSel.anchorNode != node || domSel.anchorOffset != offset) {
    140      doc.removeEventListener("selectionchange", view.input.hideSelectionGuard!)
    141      setTimeout(() => {
    142        if (!editorOwnsSelection(view) || view.state.selection.visible)
    143          view.dom.classList.remove("ProseMirror-hideselection")
    144      }, 20)
    145    }
    146  })
    147 }
    148 
    149 function selectCursorWrapper(view: EditorView) {
    150  let domSel = view.domSelection()
    151  if (!domSel) return
    152  let node = view.cursorWrapper!.dom, img = node.nodeName == "IMG"
    153  if (img) domSel.collapse(node.parentNode!, domIndex(node) + 1)
    154  else domSel.collapse(node, 0)
    155  // Kludge to kill 'control selection' in IE11 when selecting an
    156  // invisible cursor wrapper, since that would result in those weird
    157  // resize handles and a selection that considers the absolutely
    158  // positioned wrapper, rather than the root editable node, the
    159  // focused element.
    160  if (!img && !view.state.selection.visible && browser.ie && browser.ie_version <= 11) {
    161    ;(node as any).disabled = true
    162    ;(node as any).disabled = false
    163  }
    164 }
    165 
    166 export function syncNodeSelection(view: EditorView, sel: Selection) {
    167  if (sel instanceof NodeSelection) {
    168    let desc = view.docView.descAt(sel.from)
    169    if (desc != view.lastSelectedViewDesc) {
    170      clearNodeSelection(view)
    171      if (desc) (desc as NodeViewDesc).selectNode()
    172      view.lastSelectedViewDesc = desc
    173    }
    174  } else {
    175    clearNodeSelection(view)
    176  }
    177 }
    178 
    179 // Clear all DOM statefulness of the last node selection.
    180 function clearNodeSelection(view: EditorView) {
    181  if (view.lastSelectedViewDesc) {
    182    if (view.lastSelectedViewDesc.parent)
    183      (view.lastSelectedViewDesc as NodeViewDesc).deselectNode()
    184    view.lastSelectedViewDesc = undefined
    185  }
    186 }
    187 
    188 export function selectionBetween(view: EditorView, $anchor: ResolvedPos, $head: ResolvedPos, bias?: number) {
    189  return view.someProp("createSelectionBetween", f => f(view, $anchor, $head))
    190    || TextSelection.between($anchor, $head, bias)
    191 }
    192 
    193 export function hasFocusAndSelection(view: EditorView) {
    194  if (view.editable && !view.hasFocus()) return false
    195  return hasSelection(view)
    196 }
    197 
    198 export function hasSelection(view: EditorView) {
    199  let sel = view.domSelectionRange()
    200  if (!sel.anchorNode) return false
    201  try {
    202    // Firefox will raise 'permission denied' errors when accessing
    203    // properties of `sel.anchorNode` when it's in a generated CSS
    204    // element.
    205    return view.dom.contains(sel.anchorNode.nodeType == 3 ? sel.anchorNode.parentNode : sel.anchorNode) &&
    206      (view.editable || view.dom.contains(sel.focusNode!.nodeType == 3 ? sel.focusNode!.parentNode : sel.focusNode))
    207  } catch(_) {
    208    return false
    209  }
    210 }
    211 
    212 export function anchorInRightPlace(view: EditorView) {
    213  let anchorDOM = view.docView.domFromPos(view.state.selection.anchor, 0)
    214  let domSel = view.domSelectionRange()
    215  return isEquivalentPosition(anchorDOM.node, anchorDOM.offset, domSel.anchorNode!, domSel.anchorOffset)
    216 }