@electerm/electerm-react 2.3.18 → 2.3.30

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.
@@ -0,0 +1,10 @@
1
+ export default function hasActiveInput (className = 'ant-input-search') {
2
+ const activeElement = document.activeElement
3
+ const isInput = activeElement && (
4
+ activeElement.tagName === 'INPUT' ||
5
+ activeElement.tagName === 'TEXTAREA'
6
+ )
7
+ const hasClass = className ? activeElement.classList.contains(className) : true
8
+ const hasInputDropDown = document.querySelector('.ant-dropdown:not(.ant-dropdown-hidden)')
9
+ return (isInput && hasClass) || hasInputDropDown
10
+ }
@@ -33,7 +33,8 @@
33
33
  display flex
34
34
  padding 8px 5px
35
35
  &:hover
36
- background var(--main-darker)
36
+ background var(--primary)
37
+ color var(--primary-contrast)
37
38
  .sftp-transport .transfer-control-icon:hover
38
39
  color var(--primary)
39
40
 
@@ -119,7 +119,7 @@ export default class FileSection extends React.Component {
119
119
 
120
120
  onCopy = (targetFiles, isCut) => {
121
121
  const { file } = this.state
122
- const selected = this.isSelected(file)
122
+ const selected = this.isSelected(file.id)
123
123
  const files = targetFiles ||
124
124
  (
125
125
  selected
@@ -138,7 +138,7 @@ export default class FileSection extends React.Component {
138
138
 
139
139
  onCopyPath = (targetFiles) => {
140
140
  const { file } = this.state
141
- const selected = this.isSelected(file)
141
+ const selected = this.isSelected(file.id)
142
142
  const files = targetFiles ||
143
143
  (
144
144
  selected
@@ -339,7 +339,7 @@ export default class FileSection extends React.Component {
339
339
  }
340
340
 
341
341
  transferDrop = (fromFiles, toFile, operation) => {
342
- const files = this.isSelected(fromFiles[0])
342
+ const files = this.isSelected(fromFiles[0]?.id)
343
343
  ? this.props.getSelectedFiles()
344
344
  : fromFiles
345
345
  return this.doTransferSelected(
@@ -351,8 +351,8 @@ export default class FileSection extends React.Component {
351
351
  )
352
352
  }
353
353
 
354
- isSelected = file => {
355
- return this.props.selectedFiles.has(file.id)
354
+ isSelected = (fileId = '') => {
355
+ return this.props.selectedFiles.has(fileId)
356
356
  }
357
357
 
358
358
  doRename = () => {
@@ -872,7 +872,7 @@ export default class FileSection extends React.Component {
872
872
  }
873
873
 
874
874
  handleContextMenuCapture = (e) => {
875
- if (!this.isSelected(this.state.file)) {
875
+ if (!this.isSelected(this.state.file.id)) {
876
876
  this.onClick(e)
877
877
  }
878
878
  this.contextMenuPosition = {
@@ -27,7 +27,7 @@ import fs from '../../common/fs'
27
27
  import ListTable from './list-table-ui'
28
28
  import deepCopy from 'json-deep-copy'
29
29
  import isValidPath from '../../common/is-valid-path'
30
-
30
+ import { LoadingOutlined } from '@ant-design/icons'
31
31
  import * as owner from './owner-list'
32
32
  import AddressBar from './address-bar'
33
33
  import getProxy from '../../common/get-proxy'
@@ -47,7 +47,8 @@ export default class Sftp extends Component {
47
47
  onEditFile: false,
48
48
  ...this.defaultState(),
49
49
  loadingSftp: false,
50
- inited: false
50
+ inited: false,
51
+ ready: false
51
52
  }
52
53
  this.retryCount = 0
53
54
  }
@@ -58,6 +59,11 @@ export default class Sftp extends Component {
58
59
  if (this.props.isFtp) {
59
60
  this.initFtpData()
60
61
  }
62
+ this.timer = setTimeout(() => {
63
+ this.setState({
64
+ ready: true
65
+ })
66
+ }, 100)
61
67
  }
62
68
 
63
69
  componentDidUpdate (prevProps, prevState) {
@@ -1261,10 +1267,18 @@ export default class Sftp extends Component {
1261
1267
  }
1262
1268
 
1263
1269
  render () {
1264
- const { height } = this.props
1265
1270
  const {
1266
- id
1271
+ id,
1272
+ ready
1267
1273
  } = this.state
1274
+ if (!ready) {
1275
+ return (
1276
+ <div className='pd3 aligncenter'>
1277
+ <LoadingOutlined />
1278
+ </div>
1279
+ )
1280
+ }
1281
+ const { height } = this.props
1268
1282
  const all = {
1269
1283
  className: 'sftp-wrap overhide relative',
1270
1284
  id: `id-${id}`,
@@ -33,7 +33,7 @@
33
33
  box-shadow none
34
34
  border none
35
35
  height 0
36
- background main
36
+ background var(--main)
37
37
  overflow hidden
38
38
  border-radius 3px
39
39
  &.focused
@@ -46,7 +46,8 @@
46
46
  cursor pointer
47
47
  border-bottom 1px solid var(--main-darker)
48
48
  &:hover
49
- background var(--main-darker)
49
+ background var(--primary)
50
+ color var(--primary-contrast)
50
51
 
51
52
  .sftp-item-title
52
53
  width 100%
@@ -115,7 +116,7 @@
115
116
  height 28px
116
117
  line-height 25px
117
118
  padding 0 5px
118
- background main
119
+ background var(--main)
119
120
  overflow hidden
120
121
  white-space nowrap
121
122
  text-overflow ellipsis
@@ -20,6 +20,7 @@ import {
20
20
  } from '../../common/constants'
21
21
  import SideIcon from './side-icon'
22
22
  import SidePanel from './side-panel'
23
+ import hasActiveInput from '../../common/has-active-input'
23
24
  import './sidebar.styl'
24
25
 
25
26
  const e = window.translate
@@ -50,6 +51,11 @@ export default memo(function Sidebar (props) {
50
51
  if (pinned) {
51
52
  return false
52
53
  }
54
+
55
+ if (hasActiveInput()) {
56
+ return false
57
+ }
58
+
53
59
  handler.current = setTimeout(
54
60
  () => store.setOpenedSideBar(''),
55
61
  400
@@ -21,7 +21,7 @@
21
21
  width 36px
22
22
  z-index 100
23
23
  bottom 0
24
- background var(--main-darker)
24
+ background var(--main-dark)
25
25
 
26
26
  .item-list
27
27
  padding-right 0
@@ -86,7 +86,7 @@
86
86
  color var(--success)
87
87
  //btns
88
88
  .btns
89
- background var(--main-darker)
89
+ background var(--main-dark)
90
90
  border-color var(--primary)
91
91
  .open-about-icon
92
92
  color var(--text-dark)
@@ -1,5 +1,5 @@
1
1
  import { PureComponent } from 'react'
2
- import { createTitleWithTag } from '../../common/create-title'
2
+ import createTitle from '../../common/create-title'
3
3
 
4
4
  export default class TabsSubMenuChild extends PureComponent {
5
5
  handleClick = () => {
@@ -8,7 +8,7 @@ export default class TabsSubMenuChild extends PureComponent {
8
8
 
9
9
  render () {
10
10
  const { item } = this.props
11
- const title = createTitleWithTag(item)
11
+ const title = createTitle(item)
12
12
  return (
13
13
  <div
14
14
  className='sub-context-menu-item'
@@ -42,16 +42,18 @@
42
42
  left 100%
43
43
  top 0
44
44
  background var(--main)
45
- box-shadow 0px 0px 3px 3px var(--main-lighter)
45
+ box-shadow 0px 0px 3px 3px var(--main-darker)
46
46
  max-height 300px
47
47
  overflow-y scroll
48
+ color var(--text)
48
49
  .with-sub-menu
49
50
  &:hover
50
51
  .sub-context-menu
51
52
  display block
53
+
52
54
  .sub-context-menu-item
53
55
  cursor pointer
54
- padding 10px 16px
56
+ padding 5px 16px
55
57
  overflow hidden
56
58
  white-space nowrap
57
59
  text-overflow ellipsis
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Add button component for tabs
3
+ */
4
+
5
+ import React, { Component } from 'react'
6
+ import { createPortal } from 'react-dom'
7
+ import {
8
+ CodeFilled,
9
+ PlusOutlined,
10
+ RightSquareFilled
11
+ } from '@ant-design/icons'
12
+ import BookmarksList from '../sidebar/bookmark-select'
13
+ import classNames from 'classnames'
14
+ import hasActiveInput from '../../common/has-active-input'
15
+ import './add-btn.styl'
16
+
17
+ const e = window.translate
18
+
19
+ export default class AddBtn extends Component {
20
+ constructor (props) {
21
+ super(props)
22
+ this.state = {
23
+ open: false,
24
+ menuPosition: 'right',
25
+ menuTop: 0,
26
+ menuLeft: 0
27
+ }
28
+ this.addBtnRef = React.createRef()
29
+ this.menuRef = React.createRef()
30
+ this.hideTimeout = null
31
+ this.portalContainer = null
32
+ }
33
+
34
+ getPortalContainer = () => {
35
+ if (!this.portalContainer) {
36
+ this.portalContainer = document.createElement('div')
37
+ this.portalContainer.className = 'add-btn-menu-portal'
38
+ document.body.appendChild(this.portalContainer)
39
+ }
40
+ return this.portalContainer
41
+ }
42
+
43
+ componentDidMount () {
44
+ document.addEventListener('click', this.handleDocumentClick)
45
+ // Listen for dropdown events to prevent menu closing
46
+ document.addEventListener('ant-dropdown-show', this.handleDropdownShow)
47
+ document.addEventListener('ant-dropdown-hide', this.handleDropdownHide)
48
+ }
49
+
50
+ componentWillUnmount () {
51
+ document.removeEventListener('click', this.handleDocumentClick)
52
+ document.removeEventListener('ant-dropdown-show', this.handleDropdownShow)
53
+ document.removeEventListener('ant-dropdown-hide', this.handleDropdownHide)
54
+ if (this.hideTimeout) {
55
+ clearTimeout(this.hideTimeout)
56
+ }
57
+ // Clean up portal container
58
+ if (this.portalContainer) {
59
+ document.body.removeChild(this.portalContainer)
60
+ this.portalContainer = null
61
+ }
62
+ }
63
+
64
+ handleDropdownShow = () => {
65
+ // Cancel any pending hide timeout when dropdown shows
66
+ if (this.hideTimeout) {
67
+ clearTimeout(this.hideTimeout)
68
+ this.hideTimeout = null
69
+ }
70
+ }
71
+
72
+ handleDropdownHide = () => {
73
+ // Small delay after dropdown hides before allowing menu to close
74
+ // This prevents flicker when dropdown closes
75
+ }
76
+
77
+ handleDocumentClick = (e) => {
78
+ // Don't close menu when clicking inside menu or add button
79
+ if (this.menuRef.current && this.menuRef.current.contains(e.target)) {
80
+ return
81
+ }
82
+ if (this.addBtnRef.current && this.addBtnRef.current.contains(e.target)) {
83
+ return
84
+ }
85
+
86
+ // Don't close if clicking on dropdown or active input elements
87
+ if (hasActiveInput()) {
88
+ return
89
+ }
90
+
91
+ this.setState({ open: false })
92
+ }
93
+
94
+ handleMouseEnter = () => {
95
+ if (this.hideTimeout) {
96
+ clearTimeout(this.hideTimeout)
97
+ this.hideTimeout = null
98
+ }
99
+
100
+ // Calculate menu position
101
+ if (this.addBtnRef.current) {
102
+ const rect = this.addBtnRef.current.getBoundingClientRect()
103
+ const windowWidth = window.innerWidth
104
+ const windowHeight = window.innerHeight
105
+
106
+ // Estimate menu width and height
107
+ const estimatedMenuWidth = Math.min(300, windowWidth - 40) // Responsive width
108
+ const estimatedMenuHeight = 400 // Rough estimate
109
+
110
+ // Calculate fixed position coordinates
111
+ let menuTop = rect.bottom + 4 // 4px margin
112
+ let menuLeft = rect.left
113
+ let menuPosition = 'right'
114
+
115
+ // Check if menu would overflow bottom of screen
116
+ if (menuTop + estimatedMenuHeight > windowHeight - 20) {
117
+ menuTop = rect.top - estimatedMenuHeight - 4 // Show above button
118
+ }
119
+
120
+ // If aligning right would cause overflow, align left instead
121
+ if (rect.left + estimatedMenuWidth > windowWidth - 20) {
122
+ menuPosition = 'left'
123
+ menuLeft = rect.right - estimatedMenuWidth
124
+ }
125
+
126
+ // If aligning left would cause overflow (negative position), force right
127
+ if (menuLeft < 20) {
128
+ menuPosition = 'right'
129
+ menuLeft = Math.max(20, rect.left) // Ensure at least 20px from edge
130
+ }
131
+
132
+ // Final check to ensure menu doesn't go off screen
133
+ if (menuLeft + estimatedMenuWidth > windowWidth - 20) {
134
+ menuLeft = windowWidth - estimatedMenuWidth - 20
135
+ }
136
+
137
+ this.setState({
138
+ open: true,
139
+ menuPosition,
140
+ menuTop,
141
+ menuLeft
142
+ })
143
+
144
+ if (!this.state.open) {
145
+ window.openTabBatch = this.props.batch
146
+ }
147
+ }
148
+ }
149
+
150
+ handleMouseLeave = () => {
151
+ this.hideTimeout = setTimeout(() => {
152
+ this.setState({ open: false })
153
+ }, 200)
154
+ }
155
+
156
+ handleMenuMouseEnter = () => {
157
+ if (this.hideTimeout) {
158
+ clearTimeout(this.hideTimeout)
159
+ this.hideTimeout = null
160
+ }
161
+ }
162
+
163
+ handleMenuMouseLeave = () => {
164
+ // Don't close if there's an active input or dropdown
165
+ if (hasActiveInput()) {
166
+ return
167
+ }
168
+
169
+ this.hideTimeout = setTimeout(() => {
170
+ this.setState({ open: false })
171
+ }, 200)
172
+ }
173
+
174
+ handleMenuScroll = (e) => {
175
+ // Prevent scroll events from bubbling up
176
+ e.stopPropagation()
177
+ }
178
+
179
+ handleTabAdd = () => {
180
+ if (!window.store.hasNodePty) {
181
+ window.store.onNewSsh()
182
+ return
183
+ }
184
+ window.store.addTab(
185
+ undefined, undefined,
186
+ this.props.batch
187
+ )
188
+ }
189
+
190
+ renderMenus = () => {
191
+ const { onNewSsh } = window.store
192
+ const cls = 'pd2x pd1y context-item pointer'
193
+ const addTabBtn = window.store.hasNodePty
194
+ ? (
195
+ <div
196
+ className={cls}
197
+ onClick={this.handleTabAdd}
198
+ >
199
+ <RightSquareFilled /> {e('newTab')}
200
+ </div>
201
+ )
202
+ : null
203
+
204
+ const { menuPosition, menuTop, menuLeft } = this.state
205
+
206
+ return (
207
+ <div
208
+ ref={this.menuRef}
209
+ className={`add-menu-wrap add-menu-${menuPosition}`}
210
+ style={{
211
+ maxHeight: window.innerHeight - menuTop - 50,
212
+ top: menuTop,
213
+ left: menuLeft
214
+ }}
215
+ onMouseEnter={this.handleMenuMouseEnter}
216
+ onMouseLeave={this.handleMenuMouseLeave}
217
+ onScroll={this.handleMenuScroll}
218
+ >
219
+ <div
220
+ className={cls}
221
+ onClick={onNewSsh}
222
+ >
223
+ <CodeFilled /> {e('newBookmark')}
224
+ </div>
225
+ {addTabBtn}
226
+ <BookmarksList
227
+ store={window.store}
228
+ />
229
+ </div>
230
+ )
231
+ }
232
+
233
+ render () {
234
+ const { empty, className } = this.props
235
+ const { open } = this.state
236
+ const cls = classNames(
237
+ 'tabs-add-btn pointer',
238
+ className,
239
+ {
240
+ empty
241
+ }
242
+ )
243
+
244
+ return (
245
+ <>
246
+ <PlusOutlined
247
+ title={e('openNewTerm')}
248
+ className={cls}
249
+ onClick={this.handleTabAdd}
250
+ onMouseEnter={this.handleMouseEnter}
251
+ onMouseLeave={this.handleMouseLeave}
252
+ ref={this.addBtnRef}
253
+ />
254
+ {open && createPortal(this.renderMenus(), this.getPortalContainer())}
255
+ </>
256
+ )
257
+ }
258
+ }
@@ -0,0 +1,12 @@
1
+
2
+ .add-menu-wrap
3
+ position fixed
4
+ z-index 1000
5
+ background var(--main)
6
+ border 1px solid var(--main-darker)
7
+ border-radius 6px
8
+ min-width 160px
9
+ max-width 300px
10
+ overflow-y auto
11
+ margin-top 4px
12
+ padding 4px 15px
@@ -8,12 +8,9 @@ import runIdle from '../../common/run-idle'
8
8
  import { throttle } from 'lodash-es'
9
9
  import TabTitle from './tab-title'
10
10
  import {
11
- CodeFilled,
12
11
  DownOutlined,
13
12
  LeftOutlined,
14
- PlusOutlined,
15
- RightOutlined,
16
- RightSquareFilled
13
+ RightOutlined
17
14
  } from '@ant-design/icons'
18
15
  import {
19
16
  SingleIcon,
@@ -25,7 +22,7 @@ import {
25
22
  TwoRowsRightIcon,
26
23
  TwoColumnsBottomIcon
27
24
  } from '../icons/split-icons'
28
- import { Dropdown, Popover } from 'antd'
25
+ import { Dropdown } from 'antd'
29
26
  import Tab from './tab'
30
27
  import './tabs.styl'
31
28
  import {
@@ -37,7 +34,7 @@ import {
37
34
  splitMapDesc
38
35
  } from '../../common/constants'
39
36
  import WindowControl from './window-control'
40
- import BookmarksList from '../sidebar/bookmark-select'
37
+ import AddBtn from './add-btn'
41
38
  import AppDrag from './app-drag'
42
39
  import NoSession from './no-session'
43
40
  import classNames from 'classnames'
@@ -190,45 +187,6 @@ export default class Tabs extends Component {
190
187
  window.store.setLayout(key)
191
188
  }
192
189
 
193
- handleOpenChange = (open) => {
194
- if (open) {
195
- window.openTabBatch = this.props.batch
196
- }
197
- }
198
-
199
- renderMenus () {
200
- const { onNewSsh } = window.store
201
- const cls = 'pd2x pd1y context-item pointer'
202
- const addTabBtn = window.store.hasNodePty
203
- ? (
204
- <div
205
- className={cls}
206
- onClick={this.handleTabAdd}
207
- >
208
- <RightSquareFilled /> {e('newTab')}
209
- </div>
210
- )
211
- : null
212
- return (
213
- <div
214
- className='add-menu-wrap' style={{
215
- maxHeight: window.innerHeight - 200
216
- }}
217
- >
218
- <div
219
- className={cls}
220
- onClick={onNewSsh}
221
- >
222
- <CodeFilled /> {e('newBookmark')}
223
- </div>
224
- {addTabBtn}
225
- <BookmarksList
226
- store={window.store}
227
- />
228
- </div>
229
- )
230
- }
231
-
232
190
  renderAddBtn = () => {
233
191
  const cls = classNames(
234
192
  'pointer tabs-add-btn font16',
@@ -237,17 +195,11 @@ export default class Tabs extends Component {
237
195
  }
238
196
  )
239
197
  return (
240
- <Popover
241
- content={this.renderMenus()}
242
- onOpenChange={this.handleOpenChange}
243
- placement='bottomRight'
244
- >
245
- <PlusOutlined
246
- title={e('openNewTerm')}
247
- className={cls}
248
- onClick={this.handleTabAdd}
249
- />
250
- </Popover>
198
+ <AddBtn
199
+ className={cls}
200
+ empty={!this.props.tabs?.length}
201
+ batch={this.props.batch}
202
+ />
251
203
  )
252
204
  }
253
205
 
@@ -41,7 +41,7 @@
41
41
  line-height 36px
42
42
  margin 0 1px 0 0
43
43
  border-radius 3px 3px 0 0
44
- background var(--main-darker)
44
+ background var(--main-dark)
45
45
  text-align center
46
46
  color var(--text-dark)
47
47
  &.tab-last
@@ -121,8 +121,8 @@
121
121
  right 5px
122
122
  cursor pointer
123
123
  top 50%
124
+ background var(--main)
124
125
  margin-top -8px
125
- background var(--text-dark)
126
126
  border-radius 100%
127
127
  color var(--text)
128
128
  height 16px
@@ -131,7 +131,8 @@
131
131
  line-height 16px
132
132
  font-size 10px
133
133
  &:hover
134
- color var(--text-light)
134
+ background var(--primary)
135
+ color var(--primary-contast)
135
136
 
136
137
  .tabs-add-btn
137
138
  display inline-block
@@ -206,7 +207,7 @@
206
207
  .window-control-minimize
207
208
  .window-control-maximize
208
209
  &:hover
209
- background var(--main-darker)
210
+ background var(--main-dark)
210
211
  .window-control-close:hover
211
212
  background var(--error)
212
213
 
@@ -4,10 +4,12 @@ import {
4
4
  } from '@ant-design/icons'
5
5
 
6
6
  export default function TreeExpander (props) {
7
- function onExpand () {
7
+ function onExpand (e) {
8
+ e.stopPropagation()
8
9
  props.onExpand(group)
9
10
  }
10
- function onUnExpand () {
11
+ function onUnExpand (e) {
12
+ e.stopPropagation()
11
13
  props.onUnExpand(group)
12
14
  }
13
15
  const { group } = props
@@ -3,7 +3,7 @@
3
3
 
4
4
  body
5
5
  overflow hidden
6
- background main
6
+ background var(--main)
7
7
  font-size 12px
8
8
  line-height 1.5715
9
9
  font-family -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "2.3.18",
3
+ "version": "2.3.30",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",