@electerm/electerm-react 3.15.175 → 3.15.186
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/bookmark-schemas.js +2 -0
- package/client/common/drop-position.js +59 -0
- package/client/components/bookmark-form/bookmark-schema.js +2 -0
- package/client/components/bookmark-form/common/connection-hopping.jsx +33 -7
- package/client/components/bookmark-form/common/ssh-tunnel-form.jsx +25 -7
- package/client/components/bookmark-form/common/ssh-tunnels.jsx +1 -1
- package/client/components/bookmark-form/config/serial.js +14 -0
- package/client/components/main/term-fullscreen.styl +5 -0
- package/client/components/quick-commands/on-drop.js +6 -1
- package/client/components/quick-commands/quick-commands-box.jsx +18 -2
- package/client/components/quick-commands/quick-commands-list-form.jsx +15 -4
- package/client/components/quick-commands/quick-commands-list.jsx +18 -2
- package/client/components/session/session-control.jsx +331 -0
- package/client/components/session/session-control.styl +38 -0
- package/client/components/session/session.jsx +41 -246
- package/client/components/session/session.styl +1 -0
- package/client/components/sftp/address-bookmark-item.jsx +15 -2
- package/client/components/sftp/code-compare-utils.js +260 -0
- package/client/components/sftp/code-compare.jsx +183 -0
- package/client/components/sftp/code-compare.styl +13 -0
- package/client/components/sftp/file-compare-modal.jsx +87 -42
- package/client/components/shortcuts/shortcut-handler.js +20 -0
- package/client/components/sidebar/transport-ui.jsx +21 -14
- package/client/components/tabs/index.jsx +18 -3
- package/client/components/tabs/tab.jsx +27 -8
- package/client/components/tabs/tabs.styl +8 -0
- package/client/components/terminal/attach-addon-custom.js +147 -36
- package/client/components/terminal/highlight-addon.js +26 -8
- package/client/components/terminal/term-search.jsx +1 -1
- package/client/components/terminal/terminal.jsx +193 -2
- package/client/components/terminal/terminal.styl +0 -2
- package/client/components/terminal-info/run-cmd.jsx +1 -1
- package/client/components/tree-list/tree-list.jsx +57 -39
- package/client/css/basic.styl +14 -0
- package/client/css/mobile.styl +31 -9
- package/client/store/item.js +10 -6
- package/package.json +1 -1
|
@@ -21,7 +21,8 @@ import {
|
|
|
21
21
|
rendererTypes,
|
|
22
22
|
isMac,
|
|
23
23
|
isMacJs,
|
|
24
|
-
connectionMap
|
|
24
|
+
connectionMap,
|
|
25
|
+
terminalSerialType
|
|
25
26
|
} from '../../common/constants.js'
|
|
26
27
|
import deepCopy from 'json-deep-copy'
|
|
27
28
|
import { readClipboardAsync, readClipboard, copy } from '../../common/clipboard.js'
|
|
@@ -74,6 +75,38 @@ import {
|
|
|
74
75
|
|
|
75
76
|
const e = window.translate
|
|
76
77
|
|
|
78
|
+
// Expands \n \t \r \\ and \xHH hex byte escapes, used to let users type
|
|
79
|
+
// control bytes (e.g. \x01 = Ctrl+A) in the serial "closeSequence" field.
|
|
80
|
+
function expandCloseSequence (text) {
|
|
81
|
+
let result = ''
|
|
82
|
+
for (let i = 0; i < text.length; i++) {
|
|
83
|
+
if (text[i] === '\\' && i + 1 < text.length) {
|
|
84
|
+
const next = text[i + 1]
|
|
85
|
+
if (next === 'n') {
|
|
86
|
+
result += '\n'
|
|
87
|
+
i++
|
|
88
|
+
} else if (next === 't') {
|
|
89
|
+
result += '\t'
|
|
90
|
+
i++
|
|
91
|
+
} else if (next === 'r') {
|
|
92
|
+
result += '\r'
|
|
93
|
+
i++
|
|
94
|
+
} else if (next === '\\') {
|
|
95
|
+
result += '\\'
|
|
96
|
+
i++
|
|
97
|
+
} else if (next === 'x' && /^[0-9a-fA-F]{2}$/.test(text.slice(i + 2, i + 4))) {
|
|
98
|
+
result += String.fromCharCode(parseInt(text.slice(i + 2, i + 4), 16))
|
|
99
|
+
i += 3
|
|
100
|
+
} else {
|
|
101
|
+
result += text[i]
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
result += text[i]
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return result
|
|
108
|
+
}
|
|
109
|
+
|
|
77
110
|
class Term extends Component {
|
|
78
111
|
constructor (props) {
|
|
79
112
|
super(props)
|
|
@@ -161,6 +194,9 @@ class Term extends Component {
|
|
|
161
194
|
|
|
162
195
|
componentWillUnmount () {
|
|
163
196
|
refs.remove(this.id)
|
|
197
|
+
clearTimeout(this.longPressTimer)
|
|
198
|
+
this.longPressTimer = null
|
|
199
|
+
this.touchStartPos = null
|
|
164
200
|
if (window.store.activeTerminalId === this.props.tab.id) {
|
|
165
201
|
window.store.activeTerminalId = ''
|
|
166
202
|
}
|
|
@@ -625,6 +661,131 @@ class Term extends Component {
|
|
|
625
661
|
}
|
|
626
662
|
}
|
|
627
663
|
|
|
664
|
+
// ---- Mobile touch support ----
|
|
665
|
+
// On touch devices, long-press should (1) select the word under the finger
|
|
666
|
+
// and (2) open the context menu — mirroring desktop right-click behaviour.
|
|
667
|
+
// xterm.js only uses touch events for scrolling, so we add explicit
|
|
668
|
+
// long-press detection here.
|
|
669
|
+
longPressTimer = null
|
|
670
|
+
touchStartPos = null
|
|
671
|
+
longPressThreshold = 500 // ms
|
|
672
|
+
longPressMoveTolerance = 10 // px
|
|
673
|
+
|
|
674
|
+
onTouchStart = (e) => {
|
|
675
|
+
if (e.touches.length !== 1) {
|
|
676
|
+
return
|
|
677
|
+
}
|
|
678
|
+
const touch = e.touches[0]
|
|
679
|
+
this.touchStartPos = {
|
|
680
|
+
clientX: touch.clientX,
|
|
681
|
+
clientY: touch.clientY,
|
|
682
|
+
target: e.currentTarget
|
|
683
|
+
}
|
|
684
|
+
clearTimeout(this.longPressTimer)
|
|
685
|
+
this.longPressTimer = setTimeout(() => {
|
|
686
|
+
this.handleLongPress()
|
|
687
|
+
}, this.longPressThreshold)
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
onTouchMove = (e) => {
|
|
691
|
+
if (!this.touchStartPos) {
|
|
692
|
+
return
|
|
693
|
+
}
|
|
694
|
+
const touch = e.touches[0]
|
|
695
|
+
const dx = touch.clientX - this.touchStartPos.clientX
|
|
696
|
+
const dy = touch.clientY - this.touchStartPos.clientY
|
|
697
|
+
if (Math.sqrt(dx * dx + dy * dy) > this.longPressMoveTolerance) {
|
|
698
|
+
clearTimeout(this.longPressTimer)
|
|
699
|
+
this.longPressTimer = null
|
|
700
|
+
this.touchStartPos = null
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
onTouchEnd = () => {
|
|
705
|
+
clearTimeout(this.longPressTimer)
|
|
706
|
+
this.longPressTimer = null
|
|
707
|
+
this.touchStartPos = null
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
handleLongPress = () => {
|
|
711
|
+
if (!this.touchStartPos || this.state.loading) {
|
|
712
|
+
return
|
|
713
|
+
}
|
|
714
|
+
const { clientX, clientY, target } = this.touchStartPos
|
|
715
|
+
|
|
716
|
+
// Select the word at the touch position (same as desktop right-click word
|
|
717
|
+
// select) so the user can immediately copy or act on it.
|
|
718
|
+
this.selectWordAt(clientX, clientY)
|
|
719
|
+
|
|
720
|
+
// Respect pasteWhenContextMenu: when enabled, long-press pastes instead
|
|
721
|
+
// of showing the menu (same as desktop right-click).
|
|
722
|
+
if (this.props.config.pasteWhenContextMenu) {
|
|
723
|
+
this.onPaste()
|
|
724
|
+
return
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Dispatch a synthetic contextmenu event so antd Dropdown's contextMenu
|
|
728
|
+
// trigger opens the menu at the exact finger position.
|
|
729
|
+
const event = new MouseEvent('contextmenu', {
|
|
730
|
+
bubbles: true,
|
|
731
|
+
cancelable: true,
|
|
732
|
+
clientX,
|
|
733
|
+
clientY
|
|
734
|
+
})
|
|
735
|
+
target.dispatchEvent(event)
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
selectWordAt = (clientX, clientY) => {
|
|
739
|
+
if (!this.term) {
|
|
740
|
+
return
|
|
741
|
+
}
|
|
742
|
+
const termElement = this.term.element
|
|
743
|
+
if (!termElement) {
|
|
744
|
+
return
|
|
745
|
+
}
|
|
746
|
+
const rect = termElement.getBoundingClientRect()
|
|
747
|
+
const x = clientX - rect.left
|
|
748
|
+
const y = clientY - rect.top
|
|
749
|
+
if (x < 0 || y < 0 || x > rect.width || y > rect.height) {
|
|
750
|
+
return
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const cellWidth = rect.width / this.term.cols
|
|
754
|
+
const cellHeight = rect.height / this.term.rows
|
|
755
|
+
const col = Math.floor(x / cellWidth)
|
|
756
|
+
const row = Math.floor(y / cellHeight)
|
|
757
|
+
|
|
758
|
+
const buffer = this.term.buffer.active
|
|
759
|
+
const line = buffer.getLine(row)
|
|
760
|
+
if (!line) {
|
|
761
|
+
return
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const text = line.translateToString(true)
|
|
765
|
+
const wordSeparator = this.props.config.terminalWordSeparator ||
|
|
766
|
+
' ./\\()"\'-:,.;<>~!@#$%^&*|+=[]{}`~ ?'
|
|
767
|
+
|
|
768
|
+
// If the touched cell is empty or a separator, nothing to select
|
|
769
|
+
if (col >= text.length || wordSeparator.includes(text[col])) {
|
|
770
|
+
return
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Find word start
|
|
774
|
+
let start = col
|
|
775
|
+
while (start > 0 && !wordSeparator.includes(text[start - 1])) {
|
|
776
|
+
start--
|
|
777
|
+
}
|
|
778
|
+
// Find word end
|
|
779
|
+
let end = col
|
|
780
|
+
while (end < text.length && !wordSeparator.includes(text[end])) {
|
|
781
|
+
end++
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if (end > start) {
|
|
785
|
+
this.term.select(start, row, end - start)
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
628
789
|
onContextMenuInner = e => {
|
|
629
790
|
e.preventDefault()
|
|
630
791
|
if (this.state.loading) {
|
|
@@ -1856,6 +2017,33 @@ class Term extends Component {
|
|
|
1856
2017
|
this.props.delTab(id)
|
|
1857
2018
|
}
|
|
1858
2019
|
|
|
2020
|
+
/**
|
|
2021
|
+
* Manually triggered from the "exit gracefully" control in
|
|
2022
|
+
* session-control.jsx (serial tabs only). Writes the configured key
|
|
2023
|
+
* sequence (default \x01ky = Ctrl+A, k, y to kill a GNU screen window) to
|
|
2024
|
+
* the socket, waits a bit so the remote end (e.g. a Bluetooth console
|
|
2025
|
+
* adapter) can release cleanly, then closes the tab.
|
|
2026
|
+
*/
|
|
2027
|
+
exitGracefully = async () => {
|
|
2028
|
+
const { tab } = this.props
|
|
2029
|
+
if (tab.type !== terminalSerialType) {
|
|
2030
|
+
return
|
|
2031
|
+
}
|
|
2032
|
+
if (!this.onClose && this.attachAddon?._sendData) {
|
|
2033
|
+
try {
|
|
2034
|
+
const data = expandCloseSequence(tab.closeSequence || '\\x01ky')
|
|
2035
|
+
if (data) {
|
|
2036
|
+
this.attachAddon._sendData(data)
|
|
2037
|
+
}
|
|
2038
|
+
} catch (err) {
|
|
2039
|
+
console.error('send close sequence failed', err)
|
|
2040
|
+
}
|
|
2041
|
+
const delay = Number(tab.closeSequenceDelay)
|
|
2042
|
+
await new Promise(resolve => setTimeout(resolve, Number.isFinite(delay) && delay >= 0 ? delay : 500))
|
|
2043
|
+
}
|
|
2044
|
+
this.props.delTab(tab.id)
|
|
2045
|
+
}
|
|
2046
|
+
|
|
1859
2047
|
handleShowInfo = () => {
|
|
1860
2048
|
const { logName, tab } = this.props
|
|
1861
2049
|
const infoProps = {
|
|
@@ -1926,7 +2114,10 @@ class Term extends Component {
|
|
|
1926
2114
|
zIndex: 10
|
|
1927
2115
|
},
|
|
1928
2116
|
onDrop: this.onDrop,
|
|
1929
|
-
onContextMenu: this.onContextMenuInner
|
|
2117
|
+
onContextMenu: this.onContextMenuInner,
|
|
2118
|
+
onTouchStart: this.onTouchStart,
|
|
2119
|
+
onTouchMove: this.onTouchMove,
|
|
2120
|
+
onTouchEnd: this.onTouchEnd
|
|
1930
2121
|
}
|
|
1931
2122
|
// const fileProps = {
|
|
1932
2123
|
// type: 'file',
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from '../../common/constants'
|
|
15
15
|
import { getItemJSON, setItemJSON } from '../../common/safe-local-storage'
|
|
16
16
|
import findParentBySel from '../../common/find-parent'
|
|
17
|
+
import { isDropAfterHalf, setDropIndicator, clearDropIndicator } from '../../common/drop-position'
|
|
17
18
|
import copy from 'json-deep-copy'
|
|
18
19
|
import NewButtonsGroup from './bookmark-toolbar'
|
|
19
20
|
import findBookmarkGroupId from '../../common/find-bookmark-group-id'
|
|
@@ -479,8 +480,10 @@ export default class ItemListTree extends Component {
|
|
|
479
480
|
)
|
|
480
481
|
}
|
|
481
482
|
|
|
482
|
-
|
|
483
|
-
|
|
483
|
+
// resolve the .tree-item under the pointer and show a directional
|
|
484
|
+
// drop indicator (top edge = before, bottom edge = after).
|
|
485
|
+
// NOTE: preventDefault on dragover is required for onDrop to fire.
|
|
486
|
+
updateDropIndicator = e => {
|
|
484
487
|
let {
|
|
485
488
|
target
|
|
486
489
|
} = e
|
|
@@ -488,7 +491,12 @@ export default class ItemListTree extends Component {
|
|
|
488
491
|
if (tar) {
|
|
489
492
|
target = tar
|
|
490
493
|
}
|
|
491
|
-
target
|
|
494
|
+
setDropIndicator(target, isDropAfterHalf(e, target))
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
onDragEnter = e => {
|
|
498
|
+
e.preventDefault()
|
|
499
|
+
this.updateDropIndicator(e)
|
|
492
500
|
}
|
|
493
501
|
|
|
494
502
|
onDragLeave = e => {
|
|
@@ -500,25 +508,21 @@ export default class ItemListTree extends Component {
|
|
|
500
508
|
if (tar) {
|
|
501
509
|
target = tar
|
|
502
510
|
}
|
|
503
|
-
target
|
|
511
|
+
clearDropIndicator(target)
|
|
504
512
|
}
|
|
505
513
|
|
|
506
514
|
onDragOver = e => {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
} = e
|
|
510
|
-
const tar = findParentBySel(target, '.tree-item')
|
|
511
|
-
if (tar) {
|
|
512
|
-
target = tar
|
|
513
|
-
}
|
|
514
|
-
target.classList.add('item-dragover-top')
|
|
515
|
+
e.preventDefault()
|
|
516
|
+
this.updateDropIndicator(e)
|
|
515
517
|
}
|
|
516
518
|
|
|
517
519
|
onDrop = action(e => {
|
|
518
520
|
e.preventDefault()
|
|
519
|
-
const elems = document.querySelectorAll(
|
|
521
|
+
const elems = document.querySelectorAll(
|
|
522
|
+
'.tree-item.dnd-before, .tree-item.dnd-after'
|
|
523
|
+
)
|
|
520
524
|
elems.forEach(elem => {
|
|
521
|
-
elem
|
|
525
|
+
clearDropIndicator(elem)
|
|
522
526
|
})
|
|
523
527
|
let {
|
|
524
528
|
target
|
|
@@ -537,6 +541,9 @@ export default class ItemListTree extends Component {
|
|
|
537
541
|
const pidDrops = target.getAttribute('data-parent-id') || ''
|
|
538
542
|
const pidDropsArr = pidDrops.split('#')
|
|
539
543
|
const pidDrop = pidDropsArr[pidDropsArr.length - 1]
|
|
544
|
+
// bottom half of the target row => insert after it, so the last
|
|
545
|
+
// item/group in a list can receive a drop (append to the end).
|
|
546
|
+
const insertAfter = isDropAfterHalf(e, target)
|
|
540
547
|
// can not drag item to its own children
|
|
541
548
|
if (
|
|
542
549
|
(idDragged === 'default' &&
|
|
@@ -566,8 +573,9 @@ export default class ItemListTree extends Component {
|
|
|
566
573
|
if (indexDrop < 0) {
|
|
567
574
|
return
|
|
568
575
|
}
|
|
576
|
+
const insertIndex = insertAfter ? indexDrop + 1 : indexDrop
|
|
569
577
|
bookmarkGroups.splice(
|
|
570
|
-
|
|
578
|
+
insertIndex,
|
|
571
579
|
0,
|
|
572
580
|
dragItem
|
|
573
581
|
)
|
|
@@ -580,9 +588,13 @@ export default class ItemListTree extends Component {
|
|
|
580
588
|
)
|
|
581
589
|
: false
|
|
582
590
|
if (parentDrag) {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
591
|
+
// remove from old parent IN PLACE: manate only reacts to in-place
|
|
592
|
+
// array mutations (the delete inside splice), not reassignment.
|
|
593
|
+
const fromArr = parentDrag.bookmarkGroupIds || []
|
|
594
|
+
const fromIdx = fromArr.indexOf(idDragged)
|
|
595
|
+
if (fromIdx >= 0) {
|
|
596
|
+
fromArr.splice(fromIdx, 1)
|
|
597
|
+
}
|
|
586
598
|
}
|
|
587
599
|
const parentDrop = pidDrop
|
|
588
600
|
? bookmarkGroups.find(
|
|
@@ -594,20 +606,18 @@ export default class ItemListTree extends Component {
|
|
|
594
606
|
if (!parentDrop) {
|
|
595
607
|
return
|
|
596
608
|
}
|
|
609
|
+
const dropArr = parentDrop.bookmarkGroupIds || []
|
|
597
610
|
if (!pidDrop) {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
idDragged
|
|
602
|
-
]
|
|
603
|
-
)
|
|
611
|
+
if (!dropArr.includes(idDragged)) {
|
|
612
|
+
dropArr.push(idDragged)
|
|
613
|
+
}
|
|
604
614
|
} else {
|
|
605
|
-
|
|
606
|
-
let index = arr.findIndex(item => item === idDrop)
|
|
615
|
+
let index = dropArr.findIndex(item => item === idDrop)
|
|
607
616
|
if (index < 0) {
|
|
608
617
|
index = 0
|
|
609
618
|
}
|
|
610
|
-
|
|
619
|
+
const insertIndex = insertAfter ? index + 1 : index
|
|
620
|
+
dropArr.splice(insertIndex, 0, idDragged)
|
|
611
621
|
}
|
|
612
622
|
} else {
|
|
613
623
|
const parentDrag = bookmarkGroups.find(
|
|
@@ -616,9 +626,13 @@ export default class ItemListTree extends Component {
|
|
|
616
626
|
if (!parentDrag) {
|
|
617
627
|
return
|
|
618
628
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
629
|
+
// remove from old parent IN PLACE: manate only reacts to in-place
|
|
630
|
+
// array mutations (the delete inside splice), not reassignment.
|
|
631
|
+
const fromArr = parentDrag.bookmarkIds || []
|
|
632
|
+
const fromIdx = fromArr.indexOf(idDragged)
|
|
633
|
+
if (fromIdx >= 0) {
|
|
634
|
+
fromArr.splice(fromIdx, 1)
|
|
635
|
+
}
|
|
622
636
|
const parentDrop = isGroupDrop
|
|
623
637
|
? bookmarkGroups.find(
|
|
624
638
|
item => item.id === idDrop
|
|
@@ -629,21 +643,25 @@ export default class ItemListTree extends Component {
|
|
|
629
643
|
if (!parentDrop) {
|
|
630
644
|
return
|
|
631
645
|
}
|
|
646
|
+
const dropArr = parentDrop.bookmarkIds || []
|
|
632
647
|
if (isGroupDrop) {
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
idDragged
|
|
637
|
-
]
|
|
638
|
-
)
|
|
648
|
+
if (!dropArr.includes(idDragged)) {
|
|
649
|
+
dropArr.push(idDragged)
|
|
650
|
+
}
|
|
639
651
|
} else {
|
|
640
|
-
|
|
641
|
-
let index = arr.findIndex(item => item === idDrop)
|
|
652
|
+
let index = dropArr.findIndex(item => item === idDrop)
|
|
642
653
|
if (index < 0) {
|
|
643
654
|
index = 0
|
|
644
655
|
}
|
|
645
|
-
|
|
656
|
+
const insertIndex = insertAfter ? index + 1 : index
|
|
657
|
+
dropArr.splice(insertIndex, 0, idDragged)
|
|
646
658
|
}
|
|
659
|
+
// a manual bookmark reorder overrides any active sort, otherwise
|
|
660
|
+
// buildVisibleTreeRows would re-sort bookmarks and hide this order.
|
|
661
|
+
setItemJSON(treeSortLsKey, null)
|
|
662
|
+
this.setState({
|
|
663
|
+
sort: null
|
|
664
|
+
})
|
|
647
665
|
}
|
|
648
666
|
if (
|
|
649
667
|
isGroupDrag &&
|
package/client/css/basic.styl
CHANGED
|
@@ -42,6 +42,20 @@ a
|
|
|
42
42
|
.cap
|
|
43
43
|
text-transform capitalize
|
|
44
44
|
|
|
45
|
+
// shared drag-and-drop reorder indicators (vertical lists)
|
|
46
|
+
// first half of the target => insert before => top edge
|
|
47
|
+
// second half of the target => insert after => bottom edge
|
|
48
|
+
// (tabs use their own horizontal left/right line indicators)
|
|
49
|
+
.dnd-before
|
|
50
|
+
box-shadow inset 0 2px 0 0 var(--primary)
|
|
51
|
+
.dnd-after
|
|
52
|
+
box-shadow inset 0 -2px 0 0 var(--primary)
|
|
53
|
+
// antd table rows: box-shadow on <tr> is not rendered, apply on cells
|
|
54
|
+
tr.dnd-before > td
|
|
55
|
+
box-shadow inset 0 2px 0 0 var(--primary)
|
|
56
|
+
tr.dnd-after > td
|
|
57
|
+
box-shadow inset 0 -2px 0 0 var(--primary)
|
|
58
|
+
|
|
45
59
|
.sync-data-compare
|
|
46
60
|
.sync-suggest-action
|
|
47
61
|
color var(--success)
|
package/client/css/mobile.styl
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
// Session pane tab label: show full name by default, short label on mobile
|
|
2
|
-
.type-tab-short
|
|
3
|
-
display none
|
|
4
|
-
|
|
5
1
|
// ---------------------------------------------------------------------------
|
|
6
2
|
// Mobile UI driven by store.isMobile (is-mobile class on root)
|
|
7
3
|
// Breakpoint: 600px (see mobileBreakpoint in constants.js)
|
|
@@ -33,6 +29,24 @@
|
|
|
33
29
|
// close icon always visible on mobile
|
|
34
30
|
.tab-close
|
|
35
31
|
display block
|
|
32
|
+
// keep a left drag grip for the custom title bar (not webapp, not system-ui),
|
|
33
|
+
// so the empty area left of the tab is draggable. mac needs extra room for
|
|
34
|
+
// the traffic-light window controls in the top-left.
|
|
35
|
+
&.not-system-ui.not-webapp
|
|
36
|
+
.mobile-tabs-inner
|
|
37
|
+
margin-left 20px !important
|
|
38
|
+
&.is-mac
|
|
39
|
+
.mobile-tabs-inner
|
|
40
|
+
margin-left 72px !important
|
|
41
|
+
// AppDrag wraps .mobile-tabs-inner under a custom title bar; make it the flex
|
|
42
|
+
// item so the tab layout is unchanged and the inner's left drag grip stays
|
|
43
|
+
// inside the draggable .app-drag box.
|
|
44
|
+
.tabs.mobile-tabs > .app-drag
|
|
45
|
+
flex 1
|
|
46
|
+
min-width 0
|
|
47
|
+
display flex
|
|
48
|
+
align-items center
|
|
49
|
+
overflow hidden
|
|
36
50
|
.mobile-tabs-extra
|
|
37
51
|
flex-shrink 0
|
|
38
52
|
display flex
|
|
@@ -147,11 +161,19 @@
|
|
|
147
161
|
.upgrade-panel
|
|
148
162
|
max-width calc(100vw - 20px)
|
|
149
163
|
|
|
150
|
-
// ---- 9.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
164
|
+
// ---- 9. Terminal touch support: prevent the browser's native long-press
|
|
165
|
+
// callout (iOS "Copy/Look Up" menu) and text-selection gesture from
|
|
166
|
+
// interfering with xterm's own touch handling and our custom
|
|
167
|
+
// long-press context-menu detection. xterm.js already sets
|
|
168
|
+
// user-select:none on .xterm; we additionally suppress the iOS
|
|
169
|
+
// callout and set touch-action so the browser does not compete with
|
|
170
|
+
// xterm for touch gesture interpretation.
|
|
171
|
+
.term-wrap
|
|
172
|
+
-webkit-touch-callout none
|
|
173
|
+
-webkit-tap-highlight-color transparent
|
|
174
|
+
.xterm
|
|
175
|
+
-webkit-touch-callout none
|
|
176
|
+
touch-action none
|
|
155
177
|
|
|
156
178
|
// ---------------------------------------------------------------------------
|
|
157
179
|
// Touch-primary devices only (feature-detected via pointer media query, not
|
package/client/store/item.js
CHANGED
|
@@ -82,18 +82,22 @@ export default Store => {
|
|
|
82
82
|
window.store[type] = arr
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
Store.prototype.adjustOrder = function (type, fromId, toId) {
|
|
85
|
+
Store.prototype.adjustOrder = function (type, fromId, toId, insertAfter = false) {
|
|
86
86
|
const { store } = window
|
|
87
87
|
const items = store.getItems(type)
|
|
88
88
|
const fromIndex = items.findIndex(t => t.id === fromId)
|
|
89
|
-
|
|
89
|
+
const toIndex = items.findIndex(t => t.id === toId)
|
|
90
90
|
if (fromIndex < 0 || toIndex < 0 || fromIndex === toIndex) {
|
|
91
91
|
return
|
|
92
92
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
// insertAfter lets the item land after the target, so the last
|
|
94
|
+
// item can receive a drop (insertIndex can reach items.length).
|
|
95
|
+
let insertIndex = insertAfter ? toIndex + 1 : toIndex
|
|
96
96
|
const [removed] = items.splice(fromIndex, 1)
|
|
97
|
-
|
|
97
|
+
// account for the index shift caused by the removal above
|
|
98
|
+
if (fromIndex < insertIndex) {
|
|
99
|
+
insertIndex = insertIndex - 1
|
|
100
|
+
}
|
|
101
|
+
items.splice(insertIndex, 0, removed)
|
|
98
102
|
}
|
|
99
103
|
}
|