Mercurial
comparison .cms/lib/codemirror/src/edit/mouse_events.js @ 0:78edf6b517a0 draft
24.10
author | Coffee CMS <info@coffee-cms.ru> |
---|---|
date | Fri, 11 Oct 2024 22:40:23 +0000 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:78edf6b517a0 |
---|---|
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 | |
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 win(cm).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) cm.display.input.onContextMenu(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(root(cm)) | |
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 if (cm.state.delayingBlurEvent) { | |
153 if (cm.hasFocus()) cm.state.delayingBlurEvent = false | |
154 else delayBlurEvent(cm) | |
155 } | |
156 off(display.wrapper.ownerDocument, "mouseup", dragEnd) | |
157 off(display.wrapper.ownerDocument, "mousemove", mouseMove) | |
158 off(display.scroller, "dragstart", dragStart) | |
159 off(display.scroller, "drop", dragEnd) | |
160 if (!moved) { | |
161 e_preventDefault(e) | |
162 if (!behavior.addNew) | |
163 extendSelection(cm.doc, pos, null, null, behavior.extend) | |
164 // Work around unexplainable focus problem in IE9 (#2127) and Chrome (#3081) | |
165 if ((webkit && !safari) || ie && ie_version == 9) | |
166 setTimeout(() => {display.wrapper.ownerDocument.body.focus({preventScroll: true}); display.input.focus()}, 20) | |
167 else | |
168 display.input.focus() | |
169 } | |
170 }) | |
171 let mouseMove = function(e2) { | |
172 moved = moved || Math.abs(event.clientX - e2.clientX) + Math.abs(event.clientY - e2.clientY) >= 10 | |
173 } | |
174 let dragStart = () => moved = true | |
175 // Let the drag handler handle this. | |
176 if (webkit) display.scroller.draggable = true | |
177 cm.state.draggingText = dragEnd | |
178 dragEnd.copy = !behavior.moveOnDrag | |
179 on(display.wrapper.ownerDocument, "mouseup", dragEnd) | |
180 on(display.wrapper.ownerDocument, "mousemove", mouseMove) | |
181 on(display.scroller, "dragstart", dragStart) | |
182 on(display.scroller, "drop", dragEnd) | |
183 | |
184 cm.state.delayingBlurEvent = true | |
185 setTimeout(() => display.input.focus(), 20) | |
186 // IE's approach to draggable | |
187 if (display.scroller.dragDrop) display.scroller.dragDrop() | |
188 } | |
189 | |
190 function rangeForUnit(cm, pos, unit) { | |
191 if (unit == "char") return new Range(pos, pos) | |
192 if (unit == "word") return cm.findWordAt(pos) | |
193 if (unit == "line") return new Range(Pos(pos.line, 0), clipPos(cm.doc, Pos(pos.line + 1, 0))) | |
194 let result = unit(cm, pos) | |
195 return new Range(result.from, result.to) | |
196 } | |
197 | |
198 // Normal selection, as opposed to text dragging. | |
199 function leftButtonSelect(cm, event, start, behavior) { | |
200 if (ie) delayBlurEvent(cm) | |
201 let display = cm.display, doc = cm.doc | |
202 e_preventDefault(event) | |
203 | |
204 let ourRange, ourIndex, startSel = doc.sel, ranges = startSel.ranges | |
205 if (behavior.addNew && !behavior.extend) { | |
206 ourIndex = doc.sel.contains(start) | |
207 if (ourIndex > -1) | |
208 ourRange = ranges[ourIndex] | |
209 else | |
210 ourRange = new Range(start, start) | |
211 } else { | |
212 ourRange = doc.sel.primary() | |
213 ourIndex = doc.sel.primIndex | |
214 } | |
215 | |
216 if (behavior.unit == "rectangle") { | |
217 if (!behavior.addNew) ourRange = new Range(start, start) | |
218 start = posFromMouse(cm, event, true, true) | |
219 ourIndex = -1 | |
220 } else { | |
221 let range = rangeForUnit(cm, start, behavior.unit) | |
222 if (behavior.extend) | |
223 ourRange = extendRange(ourRange, range.anchor, range.head, behavior.extend) | |
224 else | |
225 ourRange = range | |
226 } | |
227 | |
228 if (!behavior.addNew) { | |
229 ourIndex = 0 | |
230 setSelection(doc, new Selection([ourRange], 0), sel_mouse) | |
231 startSel = doc.sel | |
232 } else if (ourIndex == -1) { | |
233 ourIndex = ranges.length | |
234 setSelection(doc, normalizeSelection(cm, ranges.concat([ourRange]), ourIndex), | |
235 {scroll: false, origin: "*mouse"}) | |
236 } else if (ranges.length > 1 && ranges[ourIndex].empty() && behavior.unit == "char" && !behavior.extend) { | |
237 setSelection(doc, normalizeSelection(cm, ranges.slice(0, ourIndex).concat(ranges.slice(ourIndex + 1)), 0), | |
238 {scroll: false, origin: "*mouse"}) | |
239 startSel = doc.sel | |
240 } else { | |
241 replaceOneSelection(doc, ourIndex, ourRange, sel_mouse) | |
242 } | |
243 | |
244 let lastPos = start | |
245 function extendTo(pos) { | |
246 if (cmp(lastPos, pos) == 0) return | |
247 lastPos = pos | |
248 | |
249 if (behavior.unit == "rectangle") { | |
250 let ranges = [], tabSize = cm.options.tabSize | |
251 let startCol = countColumn(getLine(doc, start.line).text, start.ch, tabSize) | |
252 let posCol = countColumn(getLine(doc, pos.line).text, pos.ch, tabSize) | |
253 let left = Math.min(startCol, posCol), right = Math.max(startCol, posCol) | |
254 for (let line = Math.min(start.line, pos.line), end = Math.min(cm.lastLine(), Math.max(start.line, pos.line)); | |
255 line <= end; line++) { | |
256 let text = getLine(doc, line).text, leftPos = findColumn(text, left, tabSize) | |
257 if (left == right) | |
258 ranges.push(new Range(Pos(line, leftPos), Pos(line, leftPos))) | |
259 else if (text.length > leftPos) | |
260 ranges.push(new Range(Pos(line, leftPos), Pos(line, findColumn(text, right, tabSize)))) | |
261 } | |
262 if (!ranges.length) ranges.push(new Range(start, start)) | |
263 setSelection(doc, normalizeSelection(cm, startSel.ranges.slice(0, ourIndex).concat(ranges), ourIndex), | |
264 {origin: "*mouse", scroll: false}) | |
265 cm.scrollIntoView(pos) | |
266 } else { | |
267 let oldRange = ourRange | |
268 let range = rangeForUnit(cm, pos, behavior.unit) | |
269 let anchor = oldRange.anchor, head | |
270 if (cmp(range.anchor, anchor) > 0) { | |
271 head = range.head | |
272 anchor = minPos(oldRange.from(), range.anchor) | |
273 } else { | |
274 head = range.anchor | |
275 anchor = maxPos(oldRange.to(), range.head) | |
276 } | |
277 let ranges = startSel.ranges.slice(0) | |
278 ranges[ourIndex] = bidiSimplify(cm, new Range(clipPos(doc, anchor), head)) | |
279 setSelection(doc, normalizeSelection(cm, ranges, ourIndex), sel_mouse) | |
280 } | |
281 } | |
282 | |
283 let editorSize = display.wrapper.getBoundingClientRect() | |
284 // Used to ensure timeout re-tries don't fire when another extend | |
285 // happened in the meantime (clearTimeout isn't reliable -- at | |
286 // least on Chrome, the timeouts still happen even when cleared, | |
287 // if the clear happens after their scheduled firing time). | |
288 let counter = 0 | |
289 | |
290 function extend(e) { | |
291 let curCount = ++counter | |
292 let cur = posFromMouse(cm, e, true, behavior.unit == "rectangle") | |
293 if (!cur) return | |
294 if (cmp(cur, lastPos) != 0) { | |
295 cm.curOp.focus = activeElt(root(cm)) | |
296 extendTo(cur) | |
297 let visible = visibleLines(display, doc) | |
298 if (cur.line >= visible.to || cur.line < visible.from) | |
299 setTimeout(operation(cm, () => {if (counter == curCount) extend(e)}), 150) | |
300 } else { | |
301 let outside = e.clientY < editorSize.top ? -20 : e.clientY > editorSize.bottom ? 20 : 0 | |
302 if (outside) setTimeout(operation(cm, () => { | |
303 if (counter != curCount) return | |
304 display.scroller.scrollTop += outside | |
305 extend(e) | |
306 }), 50) | |
307 } | |
308 } | |
309 | |
310 function done(e) { | |
311 cm.state.selectingText = false | |
312 counter = Infinity | |
313 // If e is null or undefined we interpret this as someone trying | |
314 // to explicitly cancel the selection rather than the user | |
315 // letting go of the mouse button. | |
316 if (e) { | |
317 e_preventDefault(e) | |
318 display.input.focus() | |
319 } | |
320 off(display.wrapper.ownerDocument, "mousemove", move) | |
321 off(display.wrapper.ownerDocument, "mouseup", up) | |
322 doc.history.lastSelOrigin = null | |
323 } | |
324 | |
325 let move = operation(cm, e => { | |
326 if (e.buttons === 0 || !e_button(e)) done(e) | |
327 else extend(e) | |
328 }) | |
329 let up = operation(cm, done) | |
330 cm.state.selectingText = up | |
331 on(display.wrapper.ownerDocument, "mousemove", move) | |
332 on(display.wrapper.ownerDocument, "mouseup", up) | |
333 } | |
334 | |
335 // Used when mouse-selecting to adjust the anchor to the proper side | |
336 // of a bidi jump depending on the visual position of the head. | |
337 function bidiSimplify(cm, range) { | |
338 let {anchor, head} = range, anchorLine = getLine(cm.doc, anchor.line) | |
339 if (cmp(anchor, head) == 0 && anchor.sticky == head.sticky) return range | |
340 let order = getOrder(anchorLine) | |
341 if (!order) return range | |
342 let index = getBidiPartAt(order, anchor.ch, anchor.sticky), part = order[index] | |
343 if (part.from != anchor.ch && part.to != anchor.ch) return range | |
344 let boundary = index + ((part.from == anchor.ch) == (part.level != 1) ? 0 : 1) | |
345 if (boundary == 0 || boundary == order.length) return range | |
346 | |
347 // Compute the relative visual position of the head compared to the | |
348 // anchor (<0 is to the left, >0 to the right) | |
349 let leftSide | |
350 if (head.line != anchor.line) { | |
351 leftSide = (head.line - anchor.line) * (cm.doc.direction == "ltr" ? 1 : -1) > 0 | |
352 } else { | |
353 let headIndex = getBidiPartAt(order, head.ch, head.sticky) | |
354 let dir = headIndex - index || (head.ch - anchor.ch) * (part.level == 1 ? -1 : 1) | |
355 if (headIndex == boundary - 1 || headIndex == boundary) | |
356 leftSide = dir < 0 | |
357 else | |
358 leftSide = dir > 0 | |
359 } | |
360 | |
361 let usePart = order[boundary + (leftSide ? -1 : 0)] | |
362 let from = leftSide == (usePart.level == 1) | |
363 let ch = from ? usePart.from : usePart.to, sticky = from ? "after" : "before" | |
364 return anchor.ch == ch && anchor.sticky == sticky ? range : new Range(new Pos(anchor.line, ch, sticky), head) | |
365 } | |
366 | |
367 | |
368 // Determines whether an event happened in the gutter, and fires the | |
369 // handlers for the corresponding event. | |
370 function gutterEvent(cm, e, type, prevent) { | |
371 let mX, mY | |
372 if (e.touches) { | |
373 mX = e.touches[0].clientX | |
374 mY = e.touches[0].clientY | |
375 } else { | |
376 try { mX = e.clientX; mY = e.clientY } | |
377 catch(e) { return false } | |
378 } | |
379 if (mX >= Math.floor(cm.display.gutters.getBoundingClientRect().right)) return false | |
380 if (prevent) e_preventDefault(e) | |
381 | |
382 let display = cm.display | |
383 let lineBox = display.lineDiv.getBoundingClientRect() | |
384 | |
385 if (mY > lineBox.bottom || !hasHandler(cm, type)) return e_defaultPrevented(e) | |
386 mY -= lineBox.top - display.viewOffset | |
387 | |
388 for (let i = 0; i < cm.display.gutterSpecs.length; ++i) { | |
389 let g = display.gutters.childNodes[i] | |
390 if (g && g.getBoundingClientRect().right >= mX) { | |
391 let line = lineAtHeight(cm.doc, mY) | |
392 let gutter = cm.display.gutterSpecs[i] | |
393 signal(cm, type, cm, line, gutter.className, e) | |
394 return e_defaultPrevented(e) | |
395 } | |
396 } | |
397 } | |
398 | |
399 export function clickInGutter(cm, e) { | |
400 return gutterEvent(cm, e, "gutterClick", true) | |
401 } | |
402 | |
403 // CONTEXT MENU HANDLING | |
404 | |
405 // To make the context menu work, we need to briefly unhide the | |
406 // textarea (making it as unobtrusive as possible) to let the | |
407 // right-click take effect on it. | |
408 export function onContextMenu(cm, e) { | |
409 if (eventInWidget(cm.display, e) || contextMenuInGutter(cm, e)) return | |
410 if (signalDOMEvent(cm, e, "contextmenu")) return | |
411 if (!captureRightClick) cm.display.input.onContextMenu(e) | |
412 } | |
413 | |
414 function contextMenuInGutter(cm, e) { | |
415 if (!hasHandler(cm, "gutterContextMenu")) return false | |
416 return gutterEvent(cm, e, "gutterContextMenu", false) | |
417 } |