3de05d63ad6f456e077def803f4cce76bd0f96a0914b9f70bb1a6940b02a62d7e03ae406e9de5a11c1286924040e93468d14e82dbfff6a8f922b334d311804 16 KB

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