@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.
- package/client/common/default-setting.js +1 -0
- package/client/components/footer/cmd-history.jsx +132 -45
- package/client/components/footer/cmd-history.styl +42 -4
- package/client/components/footer/multi-tab-run-modal.jsx +83 -0
- package/client/components/footer/tab-select.jsx +34 -17
- package/client/components/quick-commands/quick-command-create-modal.jsx +60 -0
- package/client/components/quick-commands/quick-commands-form-elem.jsx +10 -4
- package/client/components/setting-panel/setting-terminal.jsx +1 -0
- package/client/components/sftp/sftp-entry.jsx +4 -0
- package/client/components/sidebar/side-panel.jsx +1 -1
- package/client/components/terminal/mixins/term-content.js +38 -0
- package/client/components/terminal/mixins/term-context-menu.jsx +35 -3
- package/client/components/terminal/mixins/term-socket.js +5 -5
- package/client/components/terminal/mixins/term-touch.js +444 -57
- package/client/components/terminal/shortcut-bar-defs.js +30 -4
- package/client/components/terminal/shortcut-bar-edit.jsx +40 -9
- package/client/components/terminal/shortcut-bar.jsx +13 -4
- package/client/components/terminal/ssh-reload-state.js +41 -1
- package/client/components/terminal/startup-queue.js +67 -13
- package/client/components/terminal/terminal-select-text.jsx +69 -0
- package/client/components/terminal/terminal.jsx +72 -4
- package/client/components/terminal/terminal.styl +61 -0
- package/client/css/mobile.styl +20 -7
- package/client/store/tab.js +5 -4
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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 (
|
|
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
|
-
|
|
21
|
+
createTerminalReloadState,
|
|
22
22
|
getAlternateBufferSnapshot,
|
|
23
|
-
|
|
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
|
|
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 (!
|
|
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
|
|
416
|
+
return createTerminalReloadState({ cwd, screen })
|
|
417
417
|
},
|
|
418
418
|
|
|
419
419
|
restoreReloadScreen (term) {
|
|
@@ -1,15 +1,90 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Mobile touch support.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
this.
|
|
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
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
69
|
-
//
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
254
|
+
if (!anchor) {
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
this.dragSelect = {
|
|
258
|
+
anchor,
|
|
259
|
+
moved: false,
|
|
80
260
|
clientX,
|
|
81
|
-
clientY
|
|
82
|
-
|
|
83
|
-
|
|
261
|
+
clientY,
|
|
262
|
+
target
|
|
263
|
+
}
|
|
264
|
+
this.lastDragTouch = { clientX, clientY }
|
|
84
265
|
},
|
|
85
266
|
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
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,
|
|
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
|
}
|