@electerm/electerm-react 5.0.8 → 5.1.7
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/cache.js +2 -1
- package/client/common/constants.js +2 -0
- package/client/common/default-setting.js +10 -1
- package/client/common/left-sidebar-icon-defs.js +36 -0
- package/client/common/parse-json-safe.js +7 -3
- package/client/components/ai/ai-config-props.js +8 -0
- package/client/components/bookmark-form/common/fields.jsx +3 -2
- package/client/components/bookmark-form/common/ssh-agent.jsx +3 -2
- package/client/components/bookmark-form/common/x11.jsx +2 -2
- package/client/components/common/switch.jsx +63 -0
- package/client/components/common/switch.styl +7 -0
- package/client/components/footer/cmd-history.jsx +4 -8
- package/client/components/layout/layout.jsx +8 -5
- package/client/components/main/main.jsx +7 -2
- package/client/components/profile/profile-form-elem.jsx +2 -2
- package/client/components/quick-commands/quick-commands-form-elem.jsx +2 -2
- package/client/components/rdp/rdp-session.jsx +3 -4
- package/client/components/setting-panel/setting-common.jsx +9 -5
- package/client/components/setting-panel/setting-left-sidebar-icons.jsx +54 -0
- package/client/components/setting-panel/setting-terminal.jsx +3 -4
- package/client/components/setting-sync/data-import.jsx +3 -4
- package/client/components/setting-sync/setting-sync-form.jsx +3 -2
- package/client/components/sftp/file-compare-modal.jsx +1 -1
- package/client/components/sidebar/history.jsx +3 -8
- package/client/components/sidebar/index.jsx +22 -71
- package/client/components/sidebar/left-sidebar-icons.jsx +148 -0
- package/client/components/sidebar/transfer-list.jsx +1 -1
- package/client/components/spice/spice-session.jsx +4 -5
- package/client/components/terminal/shortcut-bar-defs.js +131 -0
- package/client/components/terminal/shortcut-bar-edit.jsx +205 -0
- package/client/components/terminal/shortcut-bar-entry.jsx +34 -0
- package/client/components/terminal/shortcut-bar.jsx +150 -0
- package/client/components/terminal/shortcut-bar.styl +186 -0
- package/client/components/terminal/terminal-color-query.mjs +47 -0
- package/client/components/terminal/terminal.jsx +39 -1
- package/client/components/terminal/terminal.styl +8 -0
- package/client/components/terminal-info/base.jsx +5 -7
- package/client/components/widgets/widget-form.jsx +3 -2
- package/client/store/init-state.js +3 -0
- package/client/store/mcp-handler.js +29 -1
- package/package.json +1 -1
|
@@ -1,27 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
|
-
BookOutlined,
|
|
3
|
-
CloudSyncOutlined,
|
|
4
2
|
InfoCircleOutlined,
|
|
5
|
-
PictureOutlined,
|
|
6
|
-
PlusCircleOutlined,
|
|
7
|
-
SettingOutlined,
|
|
8
3
|
UpCircleOutlined,
|
|
9
|
-
AppstoreOutlined,
|
|
10
|
-
ThunderboltOutlined,
|
|
11
4
|
AimOutlined,
|
|
12
5
|
MenuFoldOutlined
|
|
13
6
|
} from '@ant-design/icons'
|
|
14
|
-
import { Tooltip
|
|
7
|
+
import { Tooltip } from 'antd'
|
|
15
8
|
import SideBarPanel from './sidebar-panel'
|
|
16
|
-
import TransferList from './transfer-list'
|
|
17
9
|
import MenuBtn from '../sys-menu/menu-btn'
|
|
18
|
-
import QuickConnect from '../tabs/quick-connect'
|
|
19
10
|
import {
|
|
20
11
|
settingMap,
|
|
21
12
|
modals
|
|
22
13
|
} from '../../common/constants'
|
|
23
14
|
import SideIcon from './side-icon'
|
|
24
15
|
import SidePanel from './side-panel'
|
|
16
|
+
import LeftSidebarIcons from './left-sidebar-icons'
|
|
17
|
+
import TransferList from './transfer-list'
|
|
25
18
|
import hasActiveInput from '../../common/has-active-input'
|
|
26
19
|
import './sidebar.styl'
|
|
27
20
|
|
|
@@ -47,7 +40,9 @@ export default function Sidebar (props) {
|
|
|
47
40
|
showInfoModal,
|
|
48
41
|
sidebarPanelTab,
|
|
49
42
|
openWidgetsModal,
|
|
50
|
-
zoom
|
|
43
|
+
zoom,
|
|
44
|
+
leftSideBarIcons,
|
|
45
|
+
widgetInstancesLength
|
|
51
46
|
} = props
|
|
52
47
|
|
|
53
48
|
const { store } = window
|
|
@@ -146,67 +141,23 @@ export default function Sidebar (props) {
|
|
|
146
141
|
<div className='control-icon-wrap'>
|
|
147
142
|
<MenuBtn store={store} config={store.config} />
|
|
148
143
|
</div>
|
|
149
|
-
<
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
/>
|
|
166
|
-
</div>
|
|
167
|
-
</Popover>
|
|
168
|
-
<SideIcon
|
|
169
|
-
title={e(settingMap.bookmarks)}
|
|
170
|
-
active={bookmarksActive}
|
|
171
|
-
>
|
|
172
|
-
<BookOutlined
|
|
173
|
-
onClick={handleClickBookmark}
|
|
174
|
-
className='font20 iblock control-icon'
|
|
175
|
-
/>
|
|
176
|
-
</SideIcon>
|
|
144
|
+
<LeftSidebarIcons
|
|
145
|
+
iconIds={leftSideBarIcons}
|
|
146
|
+
handleClickBookmark={handleClickBookmark}
|
|
147
|
+
onNewSsh={onNewSsh}
|
|
148
|
+
openSetting={openSetting}
|
|
149
|
+
openSettingSync={openSettingSync}
|
|
150
|
+
openTerminalThemes={openTerminalThemes}
|
|
151
|
+
openWidgetsModal={openWidgetsModal}
|
|
152
|
+
bookmarksActive={bookmarksActive}
|
|
153
|
+
themeActive={themeActive}
|
|
154
|
+
settingActive={settingActive}
|
|
155
|
+
syncActive={syncActive}
|
|
156
|
+
widgetsActive={widgetsActive}
|
|
157
|
+
isSyncingSetting={isSyncingSetting}
|
|
158
|
+
widgetInstancesLength={widgetInstancesLength}
|
|
159
|
+
/>
|
|
177
160
|
<TransferList {...transferProps} />
|
|
178
|
-
<SideIcon
|
|
179
|
-
title={e(settingMap.terminalThemes)}
|
|
180
|
-
active={themeActive}
|
|
181
|
-
>
|
|
182
|
-
<PictureOutlined
|
|
183
|
-
className='font20 iblock pointer control-icon'
|
|
184
|
-
onClick={openTerminalThemes}
|
|
185
|
-
/>
|
|
186
|
-
</SideIcon>
|
|
187
|
-
<SideIcon
|
|
188
|
-
title={e(settingMap.setting)}
|
|
189
|
-
active={settingActive}
|
|
190
|
-
>
|
|
191
|
-
<SettingOutlined className='iblock font20 control-icon' onClick={openSetting} />
|
|
192
|
-
</SideIcon>
|
|
193
|
-
<SideIcon
|
|
194
|
-
title={e('settingSync')}
|
|
195
|
-
active={syncActive}
|
|
196
|
-
>
|
|
197
|
-
<CloudSyncOutlined
|
|
198
|
-
className='iblock font20 control-icon'
|
|
199
|
-
onClick={openSettingSync}
|
|
200
|
-
spin={isSyncingSetting}
|
|
201
|
-
/>
|
|
202
|
-
</SideIcon>
|
|
203
|
-
<SideIcon
|
|
204
|
-
title={e('widgets')}
|
|
205
|
-
active={widgetsActive}
|
|
206
|
-
>
|
|
207
|
-
<AppstoreOutlined className='iblock font20 control-icon' onClick={openWidgetsModal} />
|
|
208
|
-
</SideIcon>
|
|
209
|
-
|
|
210
161
|
<SideIcon
|
|
211
162
|
title={e('about')}
|
|
212
163
|
active={showInfoModal}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* renders the customizable far-left sidebar icons.
|
|
3
|
+
*
|
|
4
|
+
* which icons appear, and in what order, is driven by `iconIds`
|
|
5
|
+
* (from config.leftSideBarIcons). The "about", "hide sidebar" and
|
|
6
|
+
* "file transfer" icons are not customizable — they are rendered
|
|
7
|
+
* directly by the parent (sidebar/index.jsx).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
AppstoreOutlined,
|
|
12
|
+
BookOutlined,
|
|
13
|
+
CloudSyncOutlined,
|
|
14
|
+
PictureOutlined,
|
|
15
|
+
PlusCircleOutlined,
|
|
16
|
+
SettingOutlined,
|
|
17
|
+
ThunderboltOutlined
|
|
18
|
+
} from '@ant-design/icons'
|
|
19
|
+
import { Badge, Popover } from 'antd'
|
|
20
|
+
import SideIcon from './side-icon'
|
|
21
|
+
import QuickConnect from '../tabs/quick-connect'
|
|
22
|
+
import { settingMap } from '../../common/constants'
|
|
23
|
+
import { defaultLeftSideBarIcons } from '../../common/left-sidebar-icon-defs'
|
|
24
|
+
|
|
25
|
+
const e = window.translate
|
|
26
|
+
|
|
27
|
+
export default function LeftSidebarIcons (props) {
|
|
28
|
+
const {
|
|
29
|
+
iconIds,
|
|
30
|
+
handleClickBookmark,
|
|
31
|
+
onNewSsh,
|
|
32
|
+
openSetting,
|
|
33
|
+
openSettingSync,
|
|
34
|
+
openTerminalThemes,
|
|
35
|
+
openWidgetsModal,
|
|
36
|
+
bookmarksActive,
|
|
37
|
+
themeActive,
|
|
38
|
+
settingActive,
|
|
39
|
+
syncActive,
|
|
40
|
+
widgetsActive,
|
|
41
|
+
isSyncingSetting,
|
|
42
|
+
widgetInstancesLength
|
|
43
|
+
} = props
|
|
44
|
+
|
|
45
|
+
const ids = Array.isArray(iconIds) && iconIds.length
|
|
46
|
+
? iconIds
|
|
47
|
+
: defaultLeftSideBarIcons
|
|
48
|
+
|
|
49
|
+
const registry = {
|
|
50
|
+
newBookmark: (
|
|
51
|
+
<SideIcon
|
|
52
|
+
key='newBookmark'
|
|
53
|
+
title={e('newBookmark')}
|
|
54
|
+
>
|
|
55
|
+
<PlusCircleOutlined
|
|
56
|
+
className='font22 iblock control-icon'
|
|
57
|
+
onClick={onNewSsh}
|
|
58
|
+
/>
|
|
59
|
+
</SideIcon>
|
|
60
|
+
),
|
|
61
|
+
quickConnect: (
|
|
62
|
+
<Popover
|
|
63
|
+
key='quickConnect'
|
|
64
|
+
content={<QuickConnect inputOnly />}
|
|
65
|
+
trigger='click'
|
|
66
|
+
placement='right'
|
|
67
|
+
>
|
|
68
|
+
<div
|
|
69
|
+
className='control-icon-wrap'
|
|
70
|
+
title={e('quickConnect')}
|
|
71
|
+
>
|
|
72
|
+
<ThunderboltOutlined className='font20 iblock control-icon' />
|
|
73
|
+
</div>
|
|
74
|
+
</Popover>
|
|
75
|
+
),
|
|
76
|
+
bookmarks: (
|
|
77
|
+
<SideIcon
|
|
78
|
+
key='bookmarks'
|
|
79
|
+
title={e(settingMap.bookmarks)}
|
|
80
|
+
active={bookmarksActive}
|
|
81
|
+
>
|
|
82
|
+
<BookOutlined
|
|
83
|
+
onClick={handleClickBookmark}
|
|
84
|
+
className='font20 iblock control-icon'
|
|
85
|
+
/>
|
|
86
|
+
</SideIcon>
|
|
87
|
+
),
|
|
88
|
+
terminalThemes: (
|
|
89
|
+
<SideIcon
|
|
90
|
+
key='terminalThemes'
|
|
91
|
+
title={e(settingMap.terminalThemes)}
|
|
92
|
+
active={themeActive}
|
|
93
|
+
>
|
|
94
|
+
<PictureOutlined
|
|
95
|
+
className='font20 iblock pointer control-icon'
|
|
96
|
+
onClick={openTerminalThemes}
|
|
97
|
+
/>
|
|
98
|
+
</SideIcon>
|
|
99
|
+
),
|
|
100
|
+
setting: (
|
|
101
|
+
<SideIcon
|
|
102
|
+
key='setting'
|
|
103
|
+
title={e(settingMap.setting)}
|
|
104
|
+
active={settingActive}
|
|
105
|
+
>
|
|
106
|
+
<SettingOutlined
|
|
107
|
+
className='iblock font20 control-icon'
|
|
108
|
+
onClick={openSetting}
|
|
109
|
+
/>
|
|
110
|
+
</SideIcon>
|
|
111
|
+
),
|
|
112
|
+
settingSync: (
|
|
113
|
+
<SideIcon
|
|
114
|
+
key='settingSync'
|
|
115
|
+
title={e('settingSync')}
|
|
116
|
+
active={syncActive}
|
|
117
|
+
>
|
|
118
|
+
<CloudSyncOutlined
|
|
119
|
+
className='iblock font20 control-icon'
|
|
120
|
+
onClick={openSettingSync}
|
|
121
|
+
spin={isSyncingSetting}
|
|
122
|
+
/>
|
|
123
|
+
</SideIcon>
|
|
124
|
+
),
|
|
125
|
+
widgets: (
|
|
126
|
+
<SideIcon
|
|
127
|
+
key='widgets'
|
|
128
|
+
title={e('widgets')}
|
|
129
|
+
active={widgetsActive}
|
|
130
|
+
>
|
|
131
|
+
<Badge
|
|
132
|
+
count={widgetInstancesLength}
|
|
133
|
+
size='small'
|
|
134
|
+
offset={[0, 0]}
|
|
135
|
+
color='green'
|
|
136
|
+
overflowCount={99}
|
|
137
|
+
>
|
|
138
|
+
<AppstoreOutlined
|
|
139
|
+
className='iblock font20 control-icon'
|
|
140
|
+
onClick={openWidgetsModal}
|
|
141
|
+
/>
|
|
142
|
+
</Badge>
|
|
143
|
+
</SideIcon>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return ids.map(id => registry[id] || null)
|
|
148
|
+
}
|
|
@@ -10,11 +10,11 @@ import {
|
|
|
10
10
|
ReloadOutlined
|
|
11
11
|
} from '@ant-design/icons'
|
|
12
12
|
import {
|
|
13
|
-
Spin
|
|
14
|
-
Switch
|
|
13
|
+
Spin
|
|
15
14
|
} from 'antd'
|
|
16
15
|
import * as ls from '../../common/safe-local-storage'
|
|
17
16
|
import RemoteFloatControl from '../common/remote-float-control'
|
|
17
|
+
import SwitchLabel from '../common/switch'
|
|
18
18
|
import './spice.styl'
|
|
19
19
|
|
|
20
20
|
async function loadSpiceModule () {
|
|
@@ -132,8 +132,7 @@ export default class SpiceSession extends PureComponent {
|
|
|
132
132
|
const scaleProps = {
|
|
133
133
|
checked: this.state.scaleViewport,
|
|
134
134
|
onChange: this.handleScaleViewChange,
|
|
135
|
-
|
|
136
|
-
checkedChildren: window.translate('scaleViewport'),
|
|
135
|
+
label: window.translate('scaleViewport'),
|
|
137
136
|
className: 'mg1l'
|
|
138
137
|
}
|
|
139
138
|
return (
|
|
@@ -144,7 +143,7 @@ export default class SpiceSession extends PureComponent {
|
|
|
144
143
|
className='mg2r mg1l pointer'
|
|
145
144
|
/>
|
|
146
145
|
{this.renderInfo()}
|
|
147
|
-
<
|
|
146
|
+
<SwitchLabel
|
|
148
147
|
{...scaleProps}
|
|
149
148
|
/>
|
|
150
149
|
</div>
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* shortcut-bar data: the candidate button library, the default selection, and
|
|
3
|
+
* localStorage persistence. Pure data (no React) so it can be shared by the
|
|
4
|
+
* bar (shortcut-bar.jsx) and the edit modal (shortcut-bar-edit.jsx).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as ls from '../../common/safe-local-storage'
|
|
8
|
+
import { shortcutBarLsKey } from '../../common/constants'
|
|
9
|
+
|
|
10
|
+
// build control bytes at runtime — keeps source free of literal control chars
|
|
11
|
+
const ESC = String.fromCharCode(0x1b)
|
|
12
|
+
const DEL = String.fromCharCode(0x7f)
|
|
13
|
+
|
|
14
|
+
// --- candidate library -----------------------------------------------------
|
|
15
|
+
|
|
16
|
+
const ctrlChars = 'abcdefghijklmnopqrstuvwxyz'.split('')
|
|
17
|
+
const ctrlButtons = ctrlChars.map(ch => ({
|
|
18
|
+
id: 'ctrl+' + ch,
|
|
19
|
+
label: 'Ctrl+' + ch.toUpperCase(),
|
|
20
|
+
data: String.fromCharCode(ch.charCodeAt(0) - 96)
|
|
21
|
+
}))
|
|
22
|
+
|
|
23
|
+
const fKeys = [
|
|
24
|
+
['f1', 'F1', ESC + 'OP'],
|
|
25
|
+
['f2', 'F2', ESC + 'OQ'],
|
|
26
|
+
['f3', 'F3', ESC + 'OR'],
|
|
27
|
+
['f4', 'F4', ESC + 'OS'],
|
|
28
|
+
['f5', 'F5', ESC + '[15~'],
|
|
29
|
+
['f6', 'F6', ESC + '[17~'],
|
|
30
|
+
['f7', 'F7', ESC + '[18~'],
|
|
31
|
+
['f8', 'F8', ESC + '[19~'],
|
|
32
|
+
['f9', 'F9', ESC + '[20~'],
|
|
33
|
+
['f10', 'F10', ESC + '[21~'],
|
|
34
|
+
['f11', 'F11', ESC + '[23~'],
|
|
35
|
+
['f12', 'F12', ESC + '[24~']
|
|
36
|
+
].map(([id, label, data]) => ({ id, label, data }))
|
|
37
|
+
|
|
38
|
+
const navButtons = [
|
|
39
|
+
{ id: 'esc', label: 'Esc', data: ESC },
|
|
40
|
+
{ id: 'tab', label: 'Tab', data: '\t' },
|
|
41
|
+
{ id: 'enter', label: 'Enter', data: '\r' },
|
|
42
|
+
{ id: 'space', label: 'Space', data: ' ' },
|
|
43
|
+
{ id: 'backspace', label: 'Bksp', data: DEL },
|
|
44
|
+
{ id: 'arrow-up', label: '↑', data: ESC + '[A' },
|
|
45
|
+
{ id: 'arrow-down', label: '↓', data: ESC + '[B' },
|
|
46
|
+
{ id: 'arrow-right', label: '→', data: ESC + '[C' },
|
|
47
|
+
{ id: 'arrow-left', label: '←', data: ESC + '[D' },
|
|
48
|
+
{ id: 'home', label: 'Home', data: ESC + '[H' },
|
|
49
|
+
{ id: 'end', label: 'End', data: ESC + '[F' },
|
|
50
|
+
{ id: 'pageup', label: 'PgUp', data: ESC + '[5~' },
|
|
51
|
+
{ id: 'pagedown', label: 'PgDn', data: ESC + '[6~' },
|
|
52
|
+
{ id: 'insert', label: 'Ins', data: ESC + '[2~' },
|
|
53
|
+
{ id: 'delete', label: 'Del', data: ESC + '[3~' }
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
const punctChars = ':;/~|\\`\'".,(){}[]<>=+-_*&^%$#@!?'
|
|
57
|
+
const punctButtons = punctChars.split('').map(ch => ({
|
|
58
|
+
id: 'char-' + ch.charCodeAt(0),
|
|
59
|
+
label: ch,
|
|
60
|
+
data: ch
|
|
61
|
+
}))
|
|
62
|
+
|
|
63
|
+
// full library, de-duplicated by id
|
|
64
|
+
const candidates = []
|
|
65
|
+
const seenIds = new Set()
|
|
66
|
+
;[...navButtons, ...ctrlButtons, ...fKeys, ...punctButtons].forEach(b => {
|
|
67
|
+
if (!seenIds.has(b.id)) {
|
|
68
|
+
seenIds.add(b.id)
|
|
69
|
+
candidates.push(b)
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// ids selected by default — mirrors the keys requested in the feature spec
|
|
74
|
+
const DEFAULT_ACTIVE_IDS = [
|
|
75
|
+
'esc',
|
|
76
|
+
'tab',
|
|
77
|
+
'ctrl+c',
|
|
78
|
+
'ctrl+v',
|
|
79
|
+
'ctrl+z',
|
|
80
|
+
'ctrl+y',
|
|
81
|
+
'ctrl+a',
|
|
82
|
+
'ctrl+x',
|
|
83
|
+
'ctrl+s',
|
|
84
|
+
'ctrl+f',
|
|
85
|
+
'ctrl+r',
|
|
86
|
+
'ctrl+g',
|
|
87
|
+
'ctrl+h',
|
|
88
|
+
'ctrl+n',
|
|
89
|
+
'ctrl+p',
|
|
90
|
+
'ctrl+l',
|
|
91
|
+
'enter',
|
|
92
|
+
'char-58',
|
|
93
|
+
'arrow-up',
|
|
94
|
+
'arrow-down',
|
|
95
|
+
'arrow-left',
|
|
96
|
+
'arrow-right',
|
|
97
|
+
'f1',
|
|
98
|
+
'f2',
|
|
99
|
+
'f3',
|
|
100
|
+
'f4',
|
|
101
|
+
'f5',
|
|
102
|
+
'f6',
|
|
103
|
+
'f7',
|
|
104
|
+
'f8',
|
|
105
|
+
'f9',
|
|
106
|
+
'f10',
|
|
107
|
+
'f11',
|
|
108
|
+
'f12'
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
export function defaultActive () {
|
|
112
|
+
const byId = new Map(candidates.map(c => [c.id, c]))
|
|
113
|
+
return DEFAULT_ACTIVE_IDS
|
|
114
|
+
.map(id => byId.get(id))
|
|
115
|
+
.filter(Boolean)
|
|
116
|
+
.map(b => ({ ...b }))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function loadActive () {
|
|
120
|
+
const saved = ls.getItemJSON(shortcutBarLsKey, null)
|
|
121
|
+
if (Array.isArray(saved) && saved.length) {
|
|
122
|
+
return saved
|
|
123
|
+
}
|
|
124
|
+
return defaultActive()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function saveActive (arr) {
|
|
128
|
+
ls.setItemJSON(shortcutBarLsKey, arr)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export { candidates }
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* edit modal for the shortcut bar.
|
|
3
|
+
*
|
|
4
|
+
* - tap a candidate to append it to the active list (order = click order)
|
|
5
|
+
* - tap the × on an active item to remove it
|
|
6
|
+
* - build a custom button (label + literal text to send)
|
|
7
|
+
* - search filters the candidate library
|
|
8
|
+
*
|
|
9
|
+
* Rendered through a portal to document.body so it isn't trapped inside the
|
|
10
|
+
* bar's (z-index 400, 44px tall) stacking context — otherwise higher-z app
|
|
11
|
+
* layers would paint over it on mobile.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useState } from 'react'
|
|
15
|
+
import { createPortal } from 'react-dom'
|
|
16
|
+
import { Input } from 'antd'
|
|
17
|
+
import {
|
|
18
|
+
CloseOutlined,
|
|
19
|
+
PlusOutlined,
|
|
20
|
+
ReloadOutlined,
|
|
21
|
+
SearchOutlined
|
|
22
|
+
} from '@ant-design/icons'
|
|
23
|
+
import classnames from 'classnames'
|
|
24
|
+
import Modal from '../common/modal'
|
|
25
|
+
import { defaultActive } from './shortcut-bar-defs'
|
|
26
|
+
|
|
27
|
+
const e = window.translate
|
|
28
|
+
|
|
29
|
+
export default function ShortcutBarEdit (props) {
|
|
30
|
+
const {
|
|
31
|
+
active,
|
|
32
|
+
candidates: allCandidates,
|
|
33
|
+
isMobile,
|
|
34
|
+
onCancel,
|
|
35
|
+
onSave
|
|
36
|
+
} = props
|
|
37
|
+
|
|
38
|
+
const [draft, setDraft] = useState(() => active.map(b => ({ ...b })))
|
|
39
|
+
const [keyword, setKeyword] = useState('')
|
|
40
|
+
const [customLabel, setCustomLabel] = useState('')
|
|
41
|
+
const [customData, setCustomData] = useState('')
|
|
42
|
+
|
|
43
|
+
const activeIds = new Set(draft.map(b => b.id))
|
|
44
|
+
|
|
45
|
+
function handleAddCandidate (btn) {
|
|
46
|
+
if (activeIds.has(btn.id)) {
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
setDraft(prev => [...prev, { ...btn }])
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function handleRemoveActive (id) {
|
|
53
|
+
setDraft(prev => prev.filter(b => b.id !== id))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function handleAddCustom () {
|
|
57
|
+
const label = customLabel.trim()
|
|
58
|
+
const data = customData
|
|
59
|
+
if (!label) {
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
const id = 'custom-' + label + '-' + Date.now()
|
|
63
|
+
setDraft(prev => [...prev, { id, label, data, custom: true }])
|
|
64
|
+
setCustomLabel('')
|
|
65
|
+
setCustomData('')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function handleReset () {
|
|
69
|
+
setDraft(defaultActive())
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function handleSave () {
|
|
73
|
+
onSave(draft)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const kw = keyword.trim().toLowerCase()
|
|
77
|
+
const filtered = allCandidates.filter(b => {
|
|
78
|
+
if (!kw) {
|
|
79
|
+
return true
|
|
80
|
+
}
|
|
81
|
+
return b.label.toLowerCase().includes(kw) || b.id.includes(kw)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
function renderActive (b) {
|
|
85
|
+
return (
|
|
86
|
+
<div
|
|
87
|
+
key={b.id}
|
|
88
|
+
className='shortcut-active-item'
|
|
89
|
+
>
|
|
90
|
+
<span className='shortcut-active-label'>{b.label}</span>
|
|
91
|
+
<button
|
|
92
|
+
type='button'
|
|
93
|
+
className='shortcut-active-remove'
|
|
94
|
+
onClick={() => handleRemoveActive(b.id)}
|
|
95
|
+
>
|
|
96
|
+
<CloseOutlined />
|
|
97
|
+
</button>
|
|
98
|
+
</div>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function renderCandidate (b) {
|
|
103
|
+
const added = activeIds.has(b.id)
|
|
104
|
+
return (
|
|
105
|
+
<button
|
|
106
|
+
type='button'
|
|
107
|
+
key={b.id}
|
|
108
|
+
className={classnames('shortcut-candidate-item', { added })}
|
|
109
|
+
onClick={() => handleAddCandidate(b)}
|
|
110
|
+
>
|
|
111
|
+
{b.label}
|
|
112
|
+
</button>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const footer = (
|
|
117
|
+
<div className='custom-modal-footer-buttons shortcut-edit-footer'>
|
|
118
|
+
<button
|
|
119
|
+
type='button'
|
|
120
|
+
className='custom-modal-cancel-btn'
|
|
121
|
+
onClick={handleReset}
|
|
122
|
+
>
|
|
123
|
+
<ReloadOutlined /> {e('reset')}
|
|
124
|
+
</button>
|
|
125
|
+
<button
|
|
126
|
+
type='button'
|
|
127
|
+
className='custom-modal-cancel-btn'
|
|
128
|
+
onClick={onCancel}
|
|
129
|
+
>
|
|
130
|
+
{e('cancel')}
|
|
131
|
+
</button>
|
|
132
|
+
<button
|
|
133
|
+
type='button'
|
|
134
|
+
className='custom-modal-ok-btn'
|
|
135
|
+
onClick={handleSave}
|
|
136
|
+
>
|
|
137
|
+
{e('save')}
|
|
138
|
+
</button>
|
|
139
|
+
</div>
|
|
140
|
+
)
|
|
141
|
+
const sst = e('keyboardShortcuts')
|
|
142
|
+
return createPortal(
|
|
143
|
+
<Modal
|
|
144
|
+
open
|
|
145
|
+
title={e('edit')}
|
|
146
|
+
onCancel={onCancel}
|
|
147
|
+
footer={footer}
|
|
148
|
+
width={560}
|
|
149
|
+
zIndex={1100}
|
|
150
|
+
wrapClassName={classnames('shortcut-edit-wrap', { 'is-mobile': isMobile })}
|
|
151
|
+
>
|
|
152
|
+
<div className='shortcut-edit-section'>
|
|
153
|
+
<div className='shortcut-edit-section-title'>{sst}</div>
|
|
154
|
+
<div className='shortcut-active-list'>
|
|
155
|
+
{draft.length === 0 && (
|
|
156
|
+
<div className='shortcut-active-empty'>0 {sst}</div>
|
|
157
|
+
)}
|
|
158
|
+
{draft.map(renderActive)}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<div className='shortcut-edit-section'>
|
|
163
|
+
<div className='shortcut-custom-form'>
|
|
164
|
+
<Input
|
|
165
|
+
prefix={<SearchOutlined />}
|
|
166
|
+
value={keyword}
|
|
167
|
+
onChange={ev => setKeyword(ev.target.value)}
|
|
168
|
+
placeholder={e('search')}
|
|
169
|
+
className='shortcut-search-input'
|
|
170
|
+
allowClear
|
|
171
|
+
/>
|
|
172
|
+
</div>
|
|
173
|
+
<div className='shortcut-candidate-list'>
|
|
174
|
+
{filtered.map(renderCandidate)}
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<div className='shortcut-edit-section'>
|
|
179
|
+
<div className='shortcut-edit-section-title'>+ {sst}</div>
|
|
180
|
+
<div className='shortcut-custom-form'>
|
|
181
|
+
<Input
|
|
182
|
+
value={customLabel}
|
|
183
|
+
onChange={ev => setCustomLabel(ev.target.value)}
|
|
184
|
+
placeholder={e('label')}
|
|
185
|
+
className='shortcut-custom-input'
|
|
186
|
+
/>
|
|
187
|
+
<Input
|
|
188
|
+
value={customData}
|
|
189
|
+
onChange={ev => setCustomData(ev.target.value)}
|
|
190
|
+
placeholder={e('edit')}
|
|
191
|
+
className='shortcut-custom-input'
|
|
192
|
+
/>
|
|
193
|
+
<button
|
|
194
|
+
type='button'
|
|
195
|
+
className='custom-modal-ok-btn shortcut-custom-add'
|
|
196
|
+
onClick={handleAddCustom}
|
|
197
|
+
>
|
|
198
|
+
<PlusOutlined /> {e('add')}
|
|
199
|
+
</button>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
</Modal>,
|
|
203
|
+
document.body
|
|
204
|
+
)
|
|
205
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lazy entry for shortcut-bar.
|
|
3
|
+
*
|
|
4
|
+
* The bar only makes sense on touch devices (it exists to compensate for the
|
|
5
|
+
* lack of a physical keyboard when the system input panel is shown). On
|
|
6
|
+
* non-touch devices we render nothing AND never import the real component, so
|
|
7
|
+
* its code (and the antd/icon deps it pulls in) stays out of the main bundle.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { lazy, Suspense } from 'react'
|
|
11
|
+
|
|
12
|
+
const ShortcutBar = lazy(() => import('./shortcut-bar'))
|
|
13
|
+
|
|
14
|
+
export function isTouchDevice () {
|
|
15
|
+
if (typeof window === 'undefined' || typeof navigator === 'undefined') {
|
|
16
|
+
return false
|
|
17
|
+
}
|
|
18
|
+
const coarse = typeof window.matchMedia === 'function' &&
|
|
19
|
+
window.matchMedia('(pointer: coarse)').matches
|
|
20
|
+
const hasTouch = (navigator.maxTouchPoints || 0) > 0 ||
|
|
21
|
+
('ontouchstart' in window)
|
|
22
|
+
return Boolean(coarse || hasTouch)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default function ShortcutBarEntry (props) {
|
|
26
|
+
if (!isTouchDevice()) {
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
return (
|
|
30
|
+
<Suspense fallback={null}>
|
|
31
|
+
<ShortcutBar {...props} />
|
|
32
|
+
</Suspense>
|
|
33
|
+
)
|
|
34
|
+
}
|