openrat-cms

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

mouse_events.min.js (15281B)


      1 import { delayBlurEvent, ensureFocus } from "../display/focus.js"
      2 import { operation } from "../display/operations.js"
      3 import { visibleLines } from "../display/update_lines.js"
      4 import { clipPos, cmp, maxPos, minPos, Pos } from "../line/pos.js"
      5 import { getLine, lineAtHeight } from "../line/utils_line.js"
      6 import { posFromMouse } from "../measurement/position_measurement.js"
      7 import { eventInWidget } from "../measurement/widgets.js"
      8 import { normalizeSelection, Range, Selection } from "../model/selection.js"
      9 import { extendRange, extendSelection, replaceOneSelection, setSelection } from "../model/selection_updates.js"
     10 import { captureRightClick, chromeOS, ie, ie_version, mac, webkit } from "../util/browser.js"
     11 import { getOrder, getBidiPartAt } from "../util/bidi.js"
     12 import { activeElt } from "../util/dom.js"
     13 import { e_button, e_defaultPrevented, e_preventDefault, e_target, hasHandler, off, on, signal, signalDOMEvent } from "../util/event.js"
     14 import { dragAndDrop } from "../util/feature_detection.js"
     15 import { bind, countColumn, findColumn, sel_mouse } from "../util/misc.js"
     16 import { addModifierNames } from "../input/keymap.js"
     17 import { Pass } from "../util/misc.js"
     18 
     19 import { dispatchKey } from "./key_events.js"
     20 import { commands } from "./commands.js"
     21 
     22 const DOUBLECLICK_DELAY = 400
     23 
     24 class PastClick {
     25   constructor(time, pos, button) {
     26     this.time = time
     27     this.pos = pos
     28     this.button = button
     29   }
     30 
     31   compare(time, pos, button) {
     32     return this.time + DOUBLECLICK_DELAY > time &&
     33       cmp(pos, this.pos) == 0 && button == this.button
     34   }
     35 }
     36 
     37 let lastClick, lastDoubleClick
     38 function clickRepeat(pos, button) {
     39   let now = +new Date
     40   if (lastDoubleClick && lastDoubleClick.compare(now, pos, button)) {
     41     lastClick = lastDoubleClick = null
     42     return "triple"
     43   } else if (lastClick && lastClick.compare(now, pos, button)) {
     44     lastDoubleClick = new PastClick(now, pos, button)
     45     lastClick = null
     46     return "double"
     47   } else {
     48     lastClick = new PastClick(now, pos, button)
     49     lastDoubleClick = null
     50     return "single"
     51   }
     52 }
     53 
     54 // A mouse down can be a single click, double click, triple click,
     55 // start of selection drag, start of text drag, new cursor
     56 // (ctrl-click), rectangle drag (alt-drag), or xwin
     57 // middle-click-paste. Or it might be a click on something we should
     58 // not interfere with, such as a scrollbar or widget.
     59 export function onMouseDown(e) {
     60   let cm = this, display = cm.display
     61   if (signalDOMEvent(cm, e) || display.activeTouch && display.input.supportsTouch()) return
     62   display.input.ensurePolled()
     63   display.shift = e.shiftKey
     64 
     65   if (eventInWidget(display, e)) {
     66     if (!webkit) {
     67       // Briefly turn off draggability, to allow widgets to do
     68       // normal dragging things.
     69       display.scroller.draggable = false
     70       setTimeout(() => display.scroller.draggable = true, 100)
     71     }
     72     return
     73   }
     74   if (clickInGutter(cm, e)) return
     75   let pos = posFromMouse(cm, e), button = e_button(e), repeat = pos ? clickRepeat(pos, button) : "single"
     76   window.focus()
     77 
     78   // #3261: make sure, that we're not starting a second selection
     79   if (button == 1 && cm.state.selectingText)
     80     cm.state.selectingText(e)
     81 
     82   if (pos && handleMappedButton(cm, button, pos, repeat, e)) return
     83 
     84   if (button == 1) {
     85     if (pos) leftButtonDown(cm, pos, repeat, e)
     86     else if (e_target(e) == display.scroller) e_preventDefault(e)
     87   } else if (button == 2) {
     88     if (pos) extendSelection(cm.doc, pos)
     89     setTimeout(() => display.input.focus(), 20)
     90   } else if (button == 3) {
     91     if (captureRightClick) onContextMenu(cm, e)
     92     else delayBlurEvent(cm)
     93   }
     94 }
     95 
     96 function handleMappedButton(cm, button, pos, repeat, event) {
     97   let name = "Click"
     98   if (repeat == "double") name = "Double" + name
     99   else if (repeat == "triple") name = "Triple" + name
    100   name = (button == 1 ? "Left" : button == 2 ? "Middle" : "Right") + name
    101 
    102   return dispatchKey(cm,  addModifierNames(name, event), event, bound => {
    103     if (typeof bound == "string") bound = commands[bound]
    104     if (!bound) return false
    105     let done = false
    106     try {
    107       if (cm.isReadOnly()) cm.state.suppressEdits = true
    108       done = bound(cm, pos) != Pass
    109     } finally {
    110       cm.state.suppressEdits = false
    111     }
    112     return done
    113   })
    114 }
    115 
    116 function configureMouse(cm, repeat, event) {
    117   let option = cm.getOption("configureMouse")
    118   let value = option ? option(cm, repeat, event) : {}
    119   if (value.unit == null) {
    120     let rect = chromeOS ? event.shiftKey && event.metaKey : event.altKey
    121     value.unit = rect ? "rectangle" : repeat == "single" ? "char" : repeat == "double" ? "word" : "line"
    122   }
    123   if (value.extend == null || cm.doc.extend) value.extend = cm.doc.extend || event.shiftKey
    124   if (value.addNew == null) value.addNew = mac ? event.metaKey : event.ctrlKey
    125   if (value.moveOnDrag == null) value.moveOnDrag = !(mac ? event.altKey : event.ctrlKey)
    126   return value
    127 }
    128 
    129 function leftButtonDown(cm, pos, repeat, event) {
    130   if (ie) setTimeout(bind(ensureFocus, cm), 0)
    131   else cm.curOp.focus = activeElt()
    132 
    133   let behavior = configureMouse(cm, repeat, event)
    134 
    135   let sel = cm.doc.sel, contained
    136   if (cm.options.dragDrop && dragAndDrop && !cm.isReadOnly() &&
    137       repeat == "single" && (contained = sel.contains(pos)) > -1 &&
    138       (cmp((contained = sel.ranges[contained]).from(), pos) < 0 || pos.xRel > 0) &&
    139       (cmp(contained.to(), pos) > 0 || pos.xRel < 0))
    140     leftButtonStartDrag(cm, event, pos, behavior)
    141   else
    142     leftButtonSelect(cm, event, pos, behavior)
    143 }
    144 
    145 // Start a text drag. When it ends, see if any dragging actually
    146 // happen, and treat as a click if it didn't.
    147 function leftButtonStartDrag(cm, event, pos, behavior) {
    148   let display = cm.display, moved = false
    149   let dragEnd = operation(cm, e => {
    150     if (webkit) display.scroller.draggable = false
    151     cm.state.draggingText = false
    152     off(document, "mouseup", dragEnd)
    153     off(document, "mousemove", mouseMove)
    154     off(display.scroller, "dragstart", dragStart)
    155     off(display.scroller, "drop", dragEnd)
    156     if (!moved) {
    157       e_preventDefault(e)
    158       if (!behavior.addNew)
    159         extendSelection(cm.doc, pos, null, null, behavior.extend)
    160       // Work around unexplainable focus problem in IE9 (#2127) and Chrome (#3081)
    161       if (webkit || ie && ie_version == 9)
    162         setTimeout(() => {document.body.focus(); display.input.focus()}, 20)
    163       else
    164         display.input.focus()
    165     }
    166   })
    167   let mouseMove = function(e2) {
    168     moved = moved || Math.abs(event.clientX - e2.clientX) + Math.abs(event.clientY - e2.clientY) >= 10
    169   }
    170   let dragStart = () => moved = true
    171   // Let the drag handler handle this.
    172   if (webkit) display.scroller.draggable = true
    173   cm.state.draggingText = dragEnd
    174   dragEnd.copy = !behavior.moveOnDrag
    175   // IE's approach to draggable
    176   if (display.scroller.dragDrop) display.scroller.dragDrop()
    177   on(document, "mouseup", dragEnd)
    178   on(document, "mousemove", mouseMove)
    179   on(display.scroller, "dragstart", dragStart)
    180   on(display.scroller, "drop", dragEnd)
    181 
    182   delayBlurEvent(cm)
    183   setTimeout(() => display.input.focus(), 20)
    184 }
    185 
    186 function rangeForUnit(cm, pos, unit) {
    187   if (unit == "char") return new Range(pos, pos)
    188   if (unit == "word") return cm.findWordAt(pos)
    189   if (unit == "line") return new Range(Pos(pos.line, 0), clipPos(cm.doc, Pos(pos.line + 1, 0)))
    190   let result = unit(cm, pos)
    191   return new Range(result.from, result.to)
    192 }
    193 
    194 // Normal selection, as opposed to text dragging.
    195 function leftButtonSelect(cm, event, start, behavior) {
    196   let display = cm.display, doc = cm.doc
    197   e_preventDefault(event)
    198 
    199   let ourRange, ourIndex, startSel = doc.sel, ranges = startSel.ranges
    200   if (behavior.addNew && !behavior.extend) {
    201     ourIndex = doc.sel.contains(start)
    202     if (ourIndex > -1)
    203       ourRange = ranges[ourIndex]
    204     else
    205       ourRange = new Range(start, start)
    206   } else {
    207     ourRange = doc.sel.primary()
    208     ourIndex = doc.sel.primIndex
    209   }
    210 
    211   if (behavior.unit == "rectangle") {
    212     if (!behavior.addNew) ourRange = new Range(start, start)
    213     start = posFromMouse(cm, event, true, true)
    214     ourIndex = -1
    215   } else {
    216     let range = rangeForUnit(cm, start, behavior.unit)
    217     if (behavior.extend)
    218       ourRange = extendRange(ourRange, range.anchor, range.head, behavior.extend)
    219     else
    220       ourRange = range
    221   }
    222 
    223   if (!behavior.addNew) {
    224     ourIndex = 0
    225     setSelection(doc, new Selection([ourRange], 0), sel_mouse)
    226     startSel = doc.sel
    227   } else if (ourIndex == -1) {
    228     ourIndex = ranges.length
    229     setSelection(doc, normalizeSelection(ranges.concat([ourRange]), ourIndex),
    230                  {scroll: false, origin: "*mouse"})
    231   } else if (ranges.length > 1 && ranges[ourIndex].empty() && behavior.unit == "char" && !behavior.extend) {
    232     setSelection(doc, normalizeSelection(ranges.slice(0, ourIndex).concat(ranges.slice(ourIndex + 1)), 0),
    233                  {scroll: false, origin: "*mouse"})
    234     startSel = doc.sel
    235   } else {
    236     replaceOneSelection(doc, ourIndex, ourRange, sel_mouse)
    237   }
    238 
    239   let lastPos = start
    240   function extendTo(pos) {
    241     if (cmp(lastPos, pos) == 0) return
    242     lastPos = pos
    243 
    244     if (behavior.unit == "rectangle") {
    245       let ranges = [], tabSize = cm.options.tabSize
    246       let startCol = countColumn(getLine(doc, start.line).text, start.ch, tabSize)
    247       let posCol = countColumn(getLine(doc, pos.line).text, pos.ch, tabSize)
    248       let left = Math.min(startCol, posCol), right = Math.max(startCol, posCol)
    249       for (let line = Math.min(start.line, pos.line), end = Math.min(cm.lastLine(), Math.max(start.line, pos.line));
    250            line <= end; line++) {
    251         let text = getLine(doc, line).text, leftPos = findColumn(text, left, tabSize)
    252         if (left == right)
    253           ranges.push(new Range(Pos(line, leftPos), Pos(line, leftPos)))
    254         else if (text.length > leftPos)
    255           ranges.push(new Range(Pos(line, leftPos), Pos(line, findColumn(text, right, tabSize))))
    256       }
    257       if (!ranges.length) ranges.push(new Range(start, start))
    258       setSelection(doc, normalizeSelection(startSel.ranges.slice(0, ourIndex).concat(ranges), ourIndex),
    259                    {origin: "*mouse", scroll: false})
    260       cm.scrollIntoView(pos)
    261     } else {
    262       let oldRange = ourRange
    263       let range = rangeForUnit(cm, pos, behavior.unit)
    264       let anchor = oldRange.anchor, head
    265       if (cmp(range.anchor, anchor) > 0) {
    266         head = range.head
    267         anchor = minPos(oldRange.from(), range.anchor)
    268       } else {
    269         head = range.anchor
    270         anchor = maxPos(oldRange.to(), range.head)
    271       }
    272       let ranges = startSel.ranges.slice(0)
    273       ranges[ourIndex] = bidiSimplify(cm, new Range(clipPos(doc, anchor), head))
    274       setSelection(doc, normalizeSelection(ranges, ourIndex), sel_mouse)
    275     }
    276   }
    277 
    278   let editorSize = display.wrapper.getBoundingClientRect()
    279   // Used to ensure timeout re-tries don't fire when another extend
    280   // happened in the meantime (clearTimeout isn't reliable -- at
    281   // least on Chrome, the timeouts still happen even when cleared,
    282   // if the clear happens after their scheduled firing time).
    283   let counter = 0
    284 
    285   function extend(e) {
    286     let curCount = ++counter
    287     let cur = posFromMouse(cm, e, true, behavior.unit == "rectangle")
    288     if (!cur) return
    289     if (cmp(cur, lastPos) != 0) {
    290       cm.curOp.focus = activeElt()
    291       extendTo(cur)
    292       let visible = visibleLines(display, doc)
    293       if (cur.line >= visible.to || cur.line < visible.from)
    294         setTimeout(operation(cm, () => {if (counter == curCount) extend(e)}), 150)
    295     } else {
    296       let outside = e.clientY < editorSize.top ? -20 : e.clientY > editorSize.bottom ? 20 : 0
    297       if (outside) setTimeout(operation(cm, () => {
    298         if (counter != curCount) return
    299         display.scroller.scrollTop += outside
    300         extend(e)
    301       }), 50)
    302     }
    303   }
    304 
    305   function done(e) {
    306     cm.state.selectingText = false
    307     counter = Infinity
    308     e_preventDefault(e)
    309     display.input.focus()
    310     off(document, "mousemove", move)
    311     off(document, "mouseup", up)
    312     doc.history.lastSelOrigin = null
    313   }
    314 
    315   let move = operation(cm, e => {
    316     if (!e_button(e)) done(e)
    317     else extend(e)
    318   })
    319   let up = operation(cm, done)
    320   cm.state.selectingText = up
    321   on(document, "mousemove", move)
    322   on(document, "mouseup", up)
    323 }
    324 
    325 // Used when mouse-selecting to adjust the anchor to the proper side
    326 // of a bidi jump depending on the visual position of the head.
    327 function bidiSimplify(cm, range) {
    328   let {anchor, head} = range, anchorLine = getLine(cm.doc, anchor.line)
    329   if (cmp(anchor, head) == 0 && anchor.sticky == head.sticky) return range
    330   let order = getOrder(anchorLine)
    331   if (!order) return range
    332   let index = getBidiPartAt(order, anchor.ch, anchor.sticky), part = order[index]
    333   if (part.from != anchor.ch && part.to != anchor.ch) return range
    334   let boundary = index + ((part.from == anchor.ch) == (part.level != 1) ? 0 : 1)
    335   if (boundary == 0 || boundary == order.length) return range
    336 
    337   // Compute the relative visual position of the head compared to the
    338   // anchor (<0 is to the left, >0 to the right)
    339   let leftSide
    340   if (head.line != anchor.line) {
    341     leftSide = (head.line - anchor.line) * (cm.doc.direction == "ltr" ? 1 : -1) > 0
    342   } else {
    343     let headIndex = getBidiPartAt(order, head.ch, head.sticky)
    344     let dir = headIndex - index || (head.ch - anchor.ch) * (part.level == 1 ? -1 : 1)
    345     if (headIndex == boundary - 1 || headIndex == boundary)
    346       leftSide = dir < 0
    347     else
    348       leftSide = dir > 0
    349   }
    350 
    351   let usePart = order[boundary + (leftSide ? -1 : 0)]
    352   let from = leftSide == (usePart.level == 1)
    353   let ch = from ? usePart.from : usePart.to, sticky = from ? "after" : "before"
    354   return anchor.ch == ch && anchor.sticky == sticky ? range : new Range(new Pos(anchor.line, ch, sticky), head)
    355 }
    356 
    357 
    358 // Determines whether an event happened in the gutter, and fires the
    359 // handlers for the corresponding event.
    360 function gutterEvent(cm, e, type, prevent) {
    361   let mX, mY
    362   if (e.touches) {
    363     mX = e.touches[0].clientX
    364     mY = e.touches[0].clientY
    365   } else {
    366     try { mX = e.clientX; mY = e.clientY }
    367     catch(e) { return false }
    368   }
    369   if (mX >= Math.floor(cm.display.gutters.getBoundingClientRect().right)) return false
    370   if (prevent) e_preventDefault(e)
    371 
    372   let display = cm.display
    373   let lineBox = display.lineDiv.getBoundingClientRect()
    374 
    375   if (mY > lineBox.bottom || !hasHandler(cm, type)) return e_defaultPrevented(e)
    376   mY -= lineBox.top - display.viewOffset
    377 
    378   for (let i = 0; i < cm.options.gutters.length; ++i) {
    379     let g = display.gutters.childNodes[i]
    380     if (g && g.getBoundingClientRect().right >= mX) {
    381       let line = lineAtHeight(cm.doc, mY)
    382       let gutter = cm.options.gutters[i]
    383       signal(cm, type, cm, line, gutter, e)
    384       return e_defaultPrevented(e)
    385     }
    386   }
    387 }
    388 
    389 export function clickInGutter(cm, e) {
    390   return gutterEvent(cm, e, "gutterClick", true)
    391 }
    392 
    393 // CONTEXT MENU HANDLING
    394 
    395 // To make the context menu work, we need to briefly unhide the
    396 // textarea (making it as unobtrusive as possible) to let the
    397 // right-click take effect on it.
    398 export function onContextMenu(cm, e) {
    399   if (eventInWidget(cm.display, e) || contextMenuInGutter(cm, e)) return
    400   if (signalDOMEvent(cm, e, "contextmenu")) return
    401   cm.display.input.onContextMenu(e)
    402 }
    403 
    404 function contextMenuInGutter(cm, e) {
    405   if (!hasHandler(cm, "gutterContextMenu")) return false
    406   return gutterEvent(cm, e, "gutterContextMenu", false)
    407 }