@electerm/electerm-react 5.5.6 → 5.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -44,12 +44,23 @@ export const contextMenuMixin = {
44
44
  disabled: !hasSelection
45
45
  },
46
46
  {
47
-
48
47
  key: 'onSelectAll',
49
48
  icon: <iconsMap.CheckSquareOutlined />,
50
49
  label: e('selectall'),
51
50
  extra: selectAllShortcut
52
51
  },
52
+ // Touch only: a selection dragged out on the canvas can only ever be an
53
+ // approximation of a native one, so offer the overlay where the OS does
54
+ // the selecting (see term-touch.js / terminal-select-text.jsx).
55
+ ...(
56
+ this.isTouchMode()
57
+ ? [{
58
+ key: 'onSelectTextMode',
59
+ icon: <iconsMap.SelectOutlined />,
60
+ label: e('selectText')
61
+ }]
62
+ : []
63
+ ),
53
64
  ...(
54
65
  isAIDisabled()
55
66
  ? []
@@ -124,11 +135,25 @@ export const contextMenuMixin = {
124
135
  ) {
125
136
  return false
126
137
  }
138
+ // While a touch drag is still building the selection we copy once on
139
+ // release instead — otherwise every cell the finger crosses would write
140
+ // the clipboard and raise a toast.
141
+ if (this.dragSelect) {
142
+ return false
143
+ }
127
144
  this.copySelectionToClipboard()
128
145
  },
129
146
 
130
147
  copySelectionToClipboard () {
131
- const txt = this.term.getSelection()
148
+ this.copyToClipboard(this.term.getSelection())
149
+ },
150
+
151
+ /**
152
+ * Shared clipboard write. Lives here rather than in term-touch.js so that
153
+ * the touch module stays free of imports (it is loaded directly by a
154
+ * node:test unit spec), and so every copy raises the same `copied` toast.
155
+ */
156
+ copyToClipboard (txt) {
132
157
  if (txt) {
133
158
  copy(txt)
134
159
  }
@@ -152,6 +177,9 @@ export const contextMenuMixin = {
152
177
  },
153
178
 
154
179
  pasteTextTooLong () {
180
+ if (this.props.config.disableConfirmForLargeClipboardContent) {
181
+ return false
182
+ }
155
183
  if (window.et.isWebApp) {
156
184
  return false
157
185
  }
@@ -180,7 +208,11 @@ export const contextMenuMixin = {
180
208
 
181
209
  async onPaste (skipTextLengthCheck) {
182
210
  let selected = await readClipboardAsync()
183
- if (!skipTextLengthCheck && selected.length > 500) {
211
+ if (
212
+ !skipTextLengthCheck &&
213
+ !this.props.config.disableConfirmForLargeClipboardContent &&
214
+ selected.length > 500
215
+ ) {
184
216
  return this.askUserConfirm()
185
217
  }
186
218
  if (isWin && this.isRemote()) {
@@ -18,9 +18,9 @@ import { XmodemClient } from '../xmodem-client.js'
18
18
  import { loadWebLinksAddon } from '../xterm-loader.js'
19
19
  import { KeywordHighlighterAddon } from '../highlight-addon.js'
20
20
  import {
21
- createSshReloadState,
21
+ createTerminalReloadState,
22
22
  getAlternateBufferSnapshot,
23
- shouldCaptureSshReloadState
23
+ shouldCaptureTerminalReloadState
24
24
  } from '../ssh-reload-state.js'
25
25
 
26
26
  // Expands \n \t \r \\ and \xHH hex byte escapes, used to let users type
@@ -58,7 +58,7 @@ function expandCloseSequence (text) {
58
58
  /**
59
59
  * Session lifecycle: opening the pty/ssh session over its websocket, error
60
60
  * and reconnect handling, input broadcast, and the screen snapshot used to
61
- * restore an ssh session after a reload.
61
+ * restore a shell session (ssh, local, telnet, serial) after a reload.
62
62
  */
63
63
  export const socketMixin = {
64
64
  buildWsUrl (port) {
@@ -392,7 +392,7 @@ export const socketMixin = {
392
392
  },
393
393
 
394
394
  getReloadState () {
395
- if (!shouldCaptureSshReloadState(this.props.tab, this.props.config)) {
395
+ if (!shouldCaptureTerminalReloadState(this.props.tab, this.props.config)) {
396
396
  return undefined
397
397
  }
398
398
  let screen = ''
@@ -413,7 +413,7 @@ export const socketMixin = {
413
413
  console.warn('Failed to serialize terminal before reload', e)
414
414
  }
415
415
  const cwd = this.cmdAddon?.getCwd() || this.props.tab._reloadState?.cwd || ''
416
- return createSshReloadState({ cwd, screen })
416
+ return createTerminalReloadState({ cwd, screen })
417
417
  },
418
418
 
419
419
  restoreReloadScreen (term) {
@@ -1,15 +1,90 @@
1
1
  /**
2
- * Mobile touch support.
3
- * On touch devices, long-press should (1) select the word under the finger
4
- * and (2) open the context menu — mirroring desktop right-click behaviour.
5
- * xterm.js only uses touch events for scrolling, so we add explicit
6
- * long-press detection here.
2
+ * Mobile touch support — active only while `store.isTouchDevice` is true.
3
+ *
4
+ * `store.isTouchDevice` is flipped by main.jsx from the real pointer input
5
+ * (mouse → false, touch/pen → true), and a pointerdown always precedes the
6
+ * touch event it stands for, so by the time a touchstart reaches us the flag
7
+ * already reflects the device the user is actually operating. A mouse-driven
8
+ * desktop therefore never enters any of this code, and the whole gesture
9
+ * layer is gated at `onTouchStart` alone: every other handler needs the
10
+ * gesture state that only a touch start can create, so a stray event cannot
11
+ * strand a half-finished gesture when the flag flips mid-gesture.
12
+ *
13
+ * Why this exists at all: xterm.js has **no** touch text selection. Its touch
14
+ * layer maps gestures to scrolling only (`-xterm-gesturestart/-change`, see
15
+ * @xterm/xterm), and `-xterm-gesturetap` has no consumer in core. Every touch
16
+ * terminal therefore has to build selection itself; the previous version here
17
+ * could only ever select a single word, which is what made copying painful.
18
+ *
19
+ * Gesture map (touch mode only):
20
+ * tap → clear any selection, focus (opens the soft keyboard)
21
+ * double tap → select the word under the finger
22
+ * triple tap → select the whole line
23
+ * long press → select the word, then KEEP DRAGGING to extend it,
24
+ * auto-scrolling while the finger is past an edge
25
+ * release, dragged → copy (copyWhenSelect on) or open the menu
26
+ * release, no drag → open the menu, exactly as before
27
+ * two fingers → left alone: xterm's own pinch/scroll handling
28
+ *
29
+ * Dragging out a selection is only ever an approximation of what a phone user
30
+ * expects, so the menu also offers "select text" (see onSelectTextMode): it
31
+ * opens the whole buffer in a full-screen read-only textarea where the OS
32
+ * supplies real selection handles. That overlay is an ordinary DOM surface, so
33
+ * `onTouchStart` bails out of it — see isInsideSelectText().
34
+ *
35
+ * Two things that are easy to get wrong:
36
+ * - `term.select()` takes a BUFFER-ABSOLUTE row, not a viewport row. Passing
37
+ * the raw viewport row selects the wrong text as soon as the user has
38
+ * scrolled back — that was a real bug in the single-word version.
39
+ * - xterm registers its touch listeners on `document`, while React's root
40
+ * container sits below it, so our `onTouchMove` runs first and a plain
41
+ * `stopPropagation()` is enough to veto xterm's scroll for the duration of
42
+ * the drag. `preventDefault()` is not an option: React listens to
43
+ * `touchmove` passively, so it is ignored (with a console warning).
7
44
  */
45
+
46
+ const DEFAULT_WORD_SEPARATOR = './\\()"\'-:,.;<>~!@#$%^&*|+=[]{}`~ ?'
47
+
48
+ // the select-text overlay; its own native selection must stay out of the
49
+ // terminal gesture layer
50
+ const SELECT_TEXT_CLASS = '.terminal-select-text'
51
+
52
+ // tap detection
53
+ const DOUBLE_TAP_MS = 350
54
+ const DOUBLE_TAP_PX = 24
55
+
56
+ // edge auto-scroll: a short delay before it kicks in, then one scroll step
57
+ // per interval
58
+ const AUTOSCROLL_DELAY = 260
59
+ const AUTOSCROLL_INTERVAL = 60
60
+
8
61
  export const touchMixin = {
62
+ isTouchMode () {
63
+ const store = window.store
64
+ return !!(store && store.isTouchDevice)
65
+ },
66
+
67
+ // ---- gesture entry points, wired up in terminal.jsx ----
68
+
9
69
  onTouchStart (e) {
70
+ // the one and only touch-mode gate — see the file header
71
+ if (!this.isTouchMode()) {
72
+ return
73
+ }
74
+ // the select-text overlay is a normal DOM surface: its native selection
75
+ // gestures must not be recycled into terminal gestures. Only a touchstart
76
+ // is checked, which is enough — the other handlers need gesture state that
77
+ // only a touchstart can create.
78
+ if (this.isInsideSelectText(e)) {
79
+ return
80
+ }
81
+ // two or more fingers mean zoom/scroll for xterm; abort whatever we were
82
+ // doing and stay out of the way
10
83
  if (e.touches.length !== 1) {
84
+ this.cancelTouchGesture()
11
85
  return
12
86
  }
87
+ this.cancelTouchGesture()
13
88
  const touch = e.touches[0]
14
89
  this.touchStartPos = {
15
90
  clientX: touch.clientX,
@@ -24,34 +99,138 @@ export const touchMixin = {
24
99
  },
25
100
 
26
101
  onTouchMove (e) {
27
- if (!this.touchStartPos) {
102
+ // nothing in flight → not our gesture (this also covers touch mode being
103
+ // off, since only a gated onTouchStart can put state in flight)
104
+ if (!this.dragSelect && !this.touchStartPos) {
28
105
  return
29
106
  }
30
107
  const touch = e.touches[0]
108
+ if (!touch) {
109
+ return
110
+ }
111
+ if (this.dragSelect) {
112
+ // veto xterm's document-level gesture handler so the drag selects
113
+ // instead of scrolling
114
+ e.stopPropagation()
115
+ this.lastDragTouch = {
116
+ clientX: touch.clientX,
117
+ clientY: touch.clientY
118
+ }
119
+ this.dragSelect.moved = true
120
+ this.scheduleDragUpdate()
121
+ return
122
+ }
31
123
  const dx = touch.clientX - this.touchStartPos.clientX
32
124
  const dy = touch.clientY - this.touchStartPos.clientY
33
125
  if (Math.sqrt(dx * dx + dy * dy) > this.longPressMoveTolerance) {
34
- clearTimeout(this.longPressTimer)
35
- this.longPressTimer = null
36
- this.touchStartPos = null
126
+ // it is a scroll/pan, not a long press — hand it back to xterm
127
+ this.cancelLongPress()
37
128
  }
38
129
  },
39
130
 
40
- onTouchEnd () {
41
- const wasTap = this.touchStartPos && !this.longPressFired
131
+ onTouchEnd (e) {
132
+ const gesture = this.dragSelect
133
+ const startPos = this.touchStartPos
134
+ const longPressed = this.longPressFired
135
+
42
136
  clearTimeout(this.longPressTimer)
43
137
  this.longPressTimer = null
138
+
139
+ // touchmove is coalesced to one update per frame, so flush the pending
140
+ // frame here to make the final selection match where the finger lifted
141
+ if (gesture && gesture.moved && this.lastDragTouch) {
142
+ const coords = this.toBufferCoords(
143
+ this.lastDragTouch.clientX,
144
+ this.lastDragTouch.clientY
145
+ )
146
+ if (coords) {
147
+ this.applySelection(gesture.anchor, coords)
148
+ }
149
+ }
150
+ window.cancelAnimationFrame(this.dragFrame)
151
+ this.dragFrame = 0
152
+ this.stopAutoScroll()
153
+ this.dragSelect = null
154
+ this.lastDragTouch = null
44
155
  this.touchStartPos = null
45
- // xterm's own touch (Gesture) handler calls preventDefault()/stopPropagation()
46
- // on touchstart/touchend, which suppresses the synthesised mousedown xterm
47
- // relies on to focus its hidden helper textarea. As a result a tap never
48
- // focuses the terminal on touch devices, so the soft keyboard never opens
49
- // and you cannot type. Focus explicitly on a clean tap (not a long-press,
50
- // not a scroll) so mobile input works. The focus() runs synchronously inside
51
- // this user-gesture handler, so iOS/Android will show the keyboard.
52
- if (wasTap && this.term) {
156
+ this.longPressFired = false
157
+
158
+ if (gesture) {
159
+ if (gesture.moved && this.props.config.copyWhenSelect) {
160
+ // mobile has no Ctrl+C — copy on release and skip the menu; the
161
+ // selection stays visible so the user can still see what was copied
162
+ this.copySelectionToClipboard()
163
+ return
164
+ }
165
+ // no drag, or auto-copy off: the menu is the way to act on the
166
+ // selection, and its Copy item now covers a real multi-line range
167
+ // instead of a single word. Anchor it under the finger that lifted,
168
+ // which for a long drag is where the user is actually looking.
169
+ const endTouch = gesture.moved && e.changedTouches && e.changedTouches[0]
170
+ this.openContextMenuAt(
171
+ endTouch ? endTouch.clientX : gesture.clientX,
172
+ endTouch ? endTouch.clientY : gesture.clientY,
173
+ gesture.target
174
+ )
175
+ return
176
+ }
177
+ if (longPressed) {
178
+ // long pressed on a separator/blank cell, so nothing got selected —
179
+ // still offer the menu, as before
180
+ if (startPos) {
181
+ this.openContextMenuAt(startPos.clientX, startPos.clientY, startPos.target)
182
+ }
183
+ return
184
+ }
185
+ if (!startPos) {
186
+ // the gesture turned into a scroll, which is xterm's job
187
+ return
188
+ }
189
+ this.handleTap(e.changedTouches[0])
190
+ },
191
+
192
+ /**
193
+ * A tap that never became a drag: single → focus + drop the selection,
194
+ * double → select the word, triple → select the line.
195
+ */
196
+ handleTap (touch) {
197
+ if (!touch || !this.term) {
198
+ return
199
+ }
200
+ const { clientX, clientY } = touch
201
+ const now = Date.now()
202
+ const near = this.lastTapPos &&
203
+ Math.abs(clientX - this.lastTapPos.clientX) < DOUBLE_TAP_PX &&
204
+ Math.abs(clientY - this.lastTapPos.clientY) < DOUBLE_TAP_PX
205
+ this.tapCount = (now - this.lastTapTime < DOUBLE_TAP_MS && near)
206
+ ? this.tapCount + 1
207
+ : 1
208
+ this.lastTapTime = now
209
+ this.lastTapPos = { clientX, clientY }
210
+
211
+ if (this.tapCount === 2 && this.selectWordAt(clientX, clientY)) {
53
212
  this.term.focus()
213
+ return
214
+ }
215
+ if (this.tapCount >= 3) {
216
+ this.tapCount = 0
217
+ const coords = this.toBufferCoords(clientX, clientY)
218
+ if (coords) {
219
+ // selectLines() takes buffer-absolute line indices, same as select()
220
+ this.term.selectLines(coords.absRow, coords.absRow)
221
+ this.term.focus()
222
+ return
223
+ }
224
+ }
225
+ if (this.term.hasSelection()) {
226
+ this.term.clearSelection()
54
227
  }
228
+ // xterm's own touch handler calls preventDefault()/stopPropagation() on
229
+ // touchstart/touchend, which suppresses the synthesised mousedown xterm
230
+ // relies on to focus its hidden helper textarea — so a tap never focuses
231
+ // the terminal by itself and the soft keyboard would never open. Focusing
232
+ // here runs synchronously inside the user gesture, so iOS/Android show it.
233
+ this.term.focus()
55
234
  },
56
235
 
57
236
  handleLongPress () {
@@ -59,78 +238,286 @@ export const touchMixin = {
59
238
  return
60
239
  }
61
240
  this.longPressFired = true
241
+ // a long press is not part of any tap sequence
242
+ this.tapCount = 0
243
+ this.lastTapPos = null
62
244
  const { clientX, clientY, target } = this.touchStartPos
63
245
 
64
- // Select the word at the touch position (same as desktop right-click word
65
- // select) so the user can immediately copy or act on it.
66
- this.selectWordAt(clientX, clientY)
246
+ const anchor = this.selectWordAt(clientX, clientY)
67
247
 
68
- // Respect pasteWhenContextMenu: when enabled, long-press pastes instead
69
- // of showing the menu (same as desktop right-click).
248
+ // pasteWhenContextMenu means "long press pastes" — honour that contract
249
+ // and never take over the gesture with a drag
70
250
  if (this.props.config.pasteWhenContextMenu) {
71
251
  this.onPaste()
72
252
  return
73
253
  }
74
-
75
- // Dispatch a synthetic contextmenu event so antd Dropdown's contextMenu
76
- // trigger opens the menu at the exact finger position.
77
- const event = new MouseEvent('contextmenu', {
78
- bubbles: true,
79
- cancelable: true,
254
+ if (!anchor) {
255
+ return
256
+ }
257
+ this.dragSelect = {
258
+ anchor,
259
+ moved: false,
80
260
  clientX,
81
- clientY
82
- })
83
- target.dispatchEvent(event)
261
+ clientY,
262
+ target
263
+ }
264
+ this.lastDragTouch = { clientX, clientY }
84
265
  },
85
266
 
86
- selectWordAt (clientX, clientY) {
87
- if (!this.term) {
267
+ isInsideSelectText (e) {
268
+ const target = e.target
269
+ return !!(target && target.closest && target.closest(SELECT_TEXT_CLASS))
270
+ },
271
+
272
+ // ---- select-text overlay ----
273
+ // The full-screen read-only textarea behind the menu's "select text" item.
274
+ // Dragging a selection out on the canvas is only ever an approximation of a
275
+ // native one, so this hands the whole buffer to the platform instead; the
276
+ // component itself is ./terminal-select-text.jsx.
277
+
278
+ onSelectTextMode () {
279
+ const text = this.getSelectableBufferText()
280
+ if (!text) {
88
281
  return
89
282
  }
90
- const termElement = this.term.element
91
- if (!termElement) {
283
+ this.setState({
284
+ selectTextVisible: true,
285
+ selectTextContent: text
286
+ })
287
+ },
288
+
289
+ handleSelectTextClose () {
290
+ this.setState({
291
+ selectTextVisible: false,
292
+ selectTextContent: ''
293
+ })
294
+ },
295
+
296
+ handleSelectTextAll () {
297
+ const el = this.selectTextRef.current
298
+ if (!el) {
92
299
  return
93
300
  }
94
- const rect = termElement.getBoundingClientRect()
95
- const x = clientX - rect.left
96
- const y = clientY - rect.top
97
- if (x < 0 || y < 0 || x > rect.width || y > rect.height) {
301
+ // readOnly, so this does not raise the soft keyboard; the element still
302
+ // has to be focused or browsers will not paint the selection
303
+ el.focus({ preventScroll: true })
304
+ el.setSelectionRange(0, el.value.length)
305
+ },
306
+
307
+ handleSelectTextCopy () {
308
+ const el = this.selectTextRef.current
309
+ if (!el) {
98
310
  return
99
311
  }
312
+ const { value, selectionStart, selectionEnd } = el
313
+ // nothing selected by hand → "copy" means the whole buffer, which is what
314
+ // a phone user means after opening this panel
315
+ this.copyToClipboard(
316
+ selectionEnd > selectionStart
317
+ ? value.slice(selectionStart, selectionEnd)
318
+ : value
319
+ )
320
+ },
100
321
 
101
- const cellWidth = rect.width / this.term.cols
102
- const cellHeight = rect.height / this.term.rows
103
- const col = Math.floor(x / cellWidth)
104
- const row = Math.floor(y / cellHeight)
322
+ // ---- selection plumbing ----
105
323
 
106
- const buffer = this.term.buffer.active
107
- const line = buffer.getLine(row)
324
+ /**
325
+ * Selects the word under the point and returns the anchor a following drag
326
+ * extends from. The anchor is the word start, so a drag always keeps the
327
+ * whole word. Returns null when there is no usable coordinate mapping.
328
+ */
329
+ selectWordAt (clientX, clientY) {
330
+ if (!this.term) {
331
+ return null
332
+ }
333
+ const coords = this.toBufferCoords(clientX, clientY)
334
+ if (!coords) {
335
+ return null
336
+ }
337
+ const { col, absRow } = coords
338
+ const anchor = { col, absRow }
339
+ const line = this.term.buffer.active.getLine(absRow)
108
340
  if (!line) {
109
- return
341
+ return anchor
110
342
  }
111
-
112
343
  const text = line.translateToString(true)
113
344
  const wordSeparator = this.props.config.terminalWordSeparator ||
114
- ' ./\\()"\'-:,.;<>~!@#$%^&*|+=[]{}`~ ?'
345
+ DEFAULT_WORD_SEPARATOR
115
346
 
116
- // If the touched cell is empty or a separator, nothing to select
117
- if (col >= text.length || wordSeparator.includes(text[col])) {
118
- return
347
+ // touched a blank/separator cell: nothing to expand, but it is still a
348
+ // valid place to start dragging from
349
+ const char = col < text.length ? text[col] : ''
350
+ if (!char || wordSeparator.includes(char)) {
351
+ return anchor
119
352
  }
120
353
 
121
- // Find word start
122
354
  let start = col
123
355
  while (start > 0 && !wordSeparator.includes(text[start - 1])) {
124
356
  start--
125
357
  }
126
- // Find word end
127
358
  let end = col
128
359
  while (end < text.length && !wordSeparator.includes(text[end])) {
129
360
  end++
130
361
  }
131
-
132
362
  if (end > start) {
133
- this.term.select(start, row, end - start)
363
+ this.term.select(start, absRow, end - start)
364
+ }
365
+ return { col: start, absRow }
366
+ },
367
+
368
+ /**
369
+ * Client point → buffer cell. `beyond` is -1/1 while the finger is above or
370
+ * below the screen area, which is what drives edge auto-scrolling.
371
+ */
372
+ toBufferCoords (clientX, clientY) {
373
+ const term = this.term
374
+ if (!term || !term.element || !term.cols || !term.rows) {
375
+ return null
376
+ }
377
+ // measure against the screen element (exactly cols × rows cells) rather
378
+ // than the whole `.xterm` box, whose padding and scrollbar would skew the
379
+ // mapping by a growing amount the further the finger moves
380
+ const screen = term.element.querySelector('.xterm-screen') || term.element
381
+ const rect = screen.getBoundingClientRect()
382
+ if (!rect.width || !rect.height) {
383
+ return null
384
+ }
385
+ const cellWidth = rect.width / term.cols
386
+ const cellHeight = rect.height / term.rows
387
+ const col = Math.min(
388
+ term.cols - 1,
389
+ Math.max(0, Math.floor((clientX - rect.left) / cellWidth))
390
+ )
391
+ const viewportRow = Math.min(
392
+ term.rows - 1,
393
+ Math.max(0, Math.floor((clientY - rect.top) / cellHeight))
394
+ )
395
+ return {
396
+ col,
397
+ beyond: clientY < rect.top ? -1 : (clientY > rect.bottom ? 1 : 0),
398
+ // the selection model stores buffer-absolute rows, not viewport rows
399
+ absRow: term.buffer.active.viewportY + viewportRow
400
+ }
401
+ },
402
+
403
+ /**
404
+ * Selects from `a` to `b`, in either direction.
405
+ *
406
+ * `term.select()` only accepts "start + length" — it clears any end point,
407
+ * so a backwards drag has to be normalised to min/max. The length is a
408
+ * linear character count across buffer rows, matching xterm's own
409
+ * `toSelectionLength = cols * (endY - startY) + (endX - startX + 1)` and the
410
+ * inverse in `finalSelectionEnd`, which is why a multi-line drag needs no
411
+ * private API.
412
+ */
413
+ applySelection (a, b) {
414
+ if (!this.term || !a || !b) {
415
+ return
416
+ }
417
+ const forward = a.absRow < b.absRow ||
418
+ (a.absRow === b.absRow && a.col <= b.col)
419
+ const start = forward ? a : b
420
+ const end = forward ? b : a
421
+ const length = this.term.cols * (end.absRow - start.absRow) +
422
+ (end.col - start.col + 1)
423
+ this.term.select(start.col, start.absRow, Math.max(1, length))
424
+ },
425
+
426
+ /**
427
+ * Coalesces touchmove to one update per frame: every `term.select()` fires
428
+ * `onSelectionChange`, and the terminal re-renders on that, so a raw
429
+ * per-event update makes the drag stutter on slower phones.
430
+ */
431
+ scheduleDragUpdate () {
432
+ if (this.dragFrame) {
433
+ return
434
+ }
435
+ this.dragFrame = window.requestAnimationFrame(() => {
436
+ this.dragFrame = 0
437
+ const gesture = this.dragSelect
438
+ const touch = this.lastDragTouch
439
+ if (!gesture || !touch) {
440
+ return
441
+ }
442
+ const coords = this.toBufferCoords(touch.clientX, touch.clientY)
443
+ if (!coords) {
444
+ return
445
+ }
446
+ this.applySelection(gesture.anchor, coords)
447
+ this.autoScrollFrom(coords.beyond)
448
+ })
449
+ },
450
+
451
+ /**
452
+ * Keeps scrolling while the finger is held past the top/bottom edge, and
453
+ * re-extends the selection as the buffer moves under it. The anchor is in
454
+ * absolute coordinates, so it stays put while the viewport scrolls.
455
+ */
456
+ autoScrollFrom (beyond) {
457
+ if (!beyond) {
458
+ this.stopAutoScroll()
459
+ return
460
+ }
461
+ if (this.autoScrollDir === beyond) {
462
+ return
463
+ }
464
+ this.stopAutoScroll()
465
+ this.autoScrollDir = beyond
466
+ const tick = () => {
467
+ if (!this.autoScrollDir || !this.dragSelect) {
468
+ return
469
+ }
470
+ this.term.scrollLines(this.autoScrollDir)
471
+ const touch = this.lastDragTouch
472
+ if (touch) {
473
+ const coords = this.toBufferCoords(touch.clientX, touch.clientY)
474
+ if (coords) {
475
+ this.applySelection(this.dragSelect.anchor, coords)
476
+ }
477
+ }
478
+ this.timers.touchAutoScroll = setTimeout(tick, AUTOSCROLL_INTERVAL)
479
+ }
480
+ this.timers.touchAutoScroll = setTimeout(tick, AUTOSCROLL_DELAY)
481
+ },
482
+
483
+ stopAutoScroll () {
484
+ this.autoScrollDir = 0
485
+ clearTimeout(this.timers.touchAutoScroll)
486
+ this.timers.touchAutoScroll = null
487
+ },
488
+
489
+ /**
490
+ * Opens the antd context-menu dropdown at the finger position by
491
+ * dispatching a synthetic `contextmenu` event, so the `contextMenu` trigger
492
+ * (and its positioning) is handled by antd exactly like a desktop
493
+ * right-click.
494
+ */
495
+ openContextMenuAt (clientX, clientY, target) {
496
+ if (!target || this.props.config.pasteWhenContextMenu) {
497
+ return
134
498
  }
499
+ const event = new MouseEvent('contextmenu', {
500
+ bubbles: true,
501
+ cancelable: true,
502
+ clientX,
503
+ clientY
504
+ })
505
+ target.dispatchEvent(event)
506
+ },
507
+
508
+ cancelLongPress () {
509
+ clearTimeout(this.longPressTimer)
510
+ this.longPressTimer = null
511
+ this.touchStartPos = null
512
+ this.longPressFired = false
513
+ },
514
+
515
+ cancelTouchGesture () {
516
+ this.cancelLongPress()
517
+ this.stopAutoScroll()
518
+ window.cancelAnimationFrame(this.dragFrame)
519
+ this.dragFrame = 0
520
+ this.dragSelect = null
521
+ this.lastDragTouch = null
135
522
  }
136
523
  }