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 }