aboutsummaryrefslogtreecommitdiffstats
path: root/src/features/quickSwitch/Component.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/features/quickSwitch/Component.js')
-rw-r--r--src/features/quickSwitch/Component.js294
1 files changed, 294 insertions, 0 deletions
diff --git a/src/features/quickSwitch/Component.js b/src/features/quickSwitch/Component.js
new file mode 100644
index 000000000..ddbdbe304
--- /dev/null
+++ b/src/features/quickSwitch/Component.js
@@ -0,0 +1,294 @@
1import React, { Component, createRef } from 'react';
2import { remote } from 'electron';
3import PropTypes from 'prop-types';
4import { observer, inject } from 'mobx-react';
5import { reaction } from 'mobx';
6import injectSheet from 'react-jss';
7import { defineMessages, intlShape } from 'react-intl';
8import { Input } from '@meetfranz/forms';
9
10import Modal from '../../components/ui/Modal';
11import { state as ModalState } from '.';
12import ServicesStore from '../../stores/ServicesStore';
13
14const messages = defineMessages({
15 search: {
16 id: 'feature.quickSwitch.search',
17 defaultMessage: '!!!Search...',
18 },
19 info: {
20 id: 'feature.quickSwitch.info',
21 defaultMessage: '!!!Select a service with TAB, ↑ and ↓. Open a service with ENTER.',
22 },
23});
24
25const styles = theme => ({
26 modal: {
27 width: '80%',
28 maxWidth: 600,
29 background: theme.styleTypes.primary.contrast,
30 color: theme.styleTypes.primary.accent,
31 paddingTop: 30,
32 },
33 services: {
34 width: '100%',
35 marginTop: 30,
36 },
37 service: {
38 background: theme.styleTypes.primary.contrast,
39 color: theme.colorText,
40 borderColor: theme.styleTypes.primary.accent,
41 borderStyle: 'solid',
42 borderWidth: 1,
43 borderRadius: 6,
44 padding: '3px 25px',
45 marginBottom: 10,
46 display: 'flex',
47 alignItems: 'center',
48 '&:hover': {
49 background: theme.styleTypes.primary.accent,
50 color: theme.styleTypes.primary.contrast,
51 cursor: 'pointer',
52 },
53 },
54 activeService: {
55 background: theme.styleTypes.primary.accent,
56 color: theme.styleTypes.primary.contrast,
57 cursor: 'pointer',
58 },
59 serviceIcon: {
60 width: 50,
61 height: 50,
62 paddingRight: 20,
63 objectFit: 'contain',
64 },
65});
66
67export default @injectSheet(styles) @inject('stores', 'actions') @observer class QuickSwitchModal extends Component {
68 static propTypes = {
69 classes: PropTypes.object.isRequired,
70 };
71
72 static contextTypes = {
73 intl: intlShape,
74 };
75
76 state = {
77 selected: 0,
78 search: '',
79 wasPrevVisible: false,
80 }
81
82 ARROW_DOWN = 40;
83
84 ARROW_UP = 38;
85
86 ENTER = 13;
87
88 TAB = 9;
89
90 inputRef = createRef();
91
92 constructor(props) {
93 super(props);
94
95 this._handleKeyDown = this._handleKeyDown.bind(this);
96 this._handleSearchUpdate = this._handleSearchUpdate.bind(this);
97 this._handleVisibilityChange = this._handleVisibilityChange.bind(this);
98 this.openService = this.openService.bind(this);
99
100 reaction(
101 () => ModalState.isModalVisible,
102 this._handleVisibilityChange,
103 );
104 }
105
106 // Add global keydown listener when component mounts
107 componentDidMount() {
108 document.addEventListener('keydown', this._handleKeyDown);
109 }
110
111 // Remove global keydown listener when component unmounts
112 componentWillUnmount() {
113 document.removeEventListener('keydown', this._handleKeyDown);
114 }
115
116 // Get currently shown services
117 services() {
118 let services = this.props.stores.services.allDisplayed;
119 if (this.state.search) {
120 // Apply simple search algorythm
121 services = services.filter(service => service.name.toLowerCase().includes(this.state.search.toLowerCase()));
122 }
123
124 return services;
125 }
126
127 openService(index) {
128 // Open service
129 const service = this.services()[index];
130 this.props.actions.service.setActive({ serviceId: service.id });
131
132 // Reset and close modal
133 this.setState({
134 search: '',
135 });
136 this.close();
137 }
138
139 // Change the selected service
140 // factor should be -1 or 1
141 changeSelected(factor) {
142 this.setState((state) => {
143 let newSelected = state.selected + factor;
144 const services = this.services().length;
145
146 // Roll around when on edge of list
147 if (state.selected < 1 && factor === -1) {
148 newSelected = services - 1;
149 } else if ((state.selected >= (services - 1)) && factor === 1) {
150 newSelected = 0;
151 }
152
153 return {
154 selected: newSelected,
155 };
156 });
157 }
158
159 // Handle global key presses to change the selection
160 _handleKeyDown(event) {
161 if (ModalState.isModalVisible) {
162 switch (event.keyCode) {
163 case this.ARROW_DOWN:
164 this.changeSelected(1);
165 break;
166 case this.TAB:
167 this.changeSelected(1);
168 break;
169 case this.ARROW_UP:
170 this.changeSelected(-1);
171 break;
172 case this.ENTER:
173 this.openService(this.state.selected);
174 break;
175 default:
176 break;
177 }
178 }
179 }
180
181 // Handle update of the search query
182 _handleSearchUpdate(evt) {
183 this.setState({
184 search: evt.target.value,
185 });
186 }
187
188 _handleVisibilityChange() {
189 const { isModalVisible } = ModalState;
190
191 if (isModalVisible && !this.state.wasPrevVisible) {
192 // Set focus back on current window if its in a service
193 // TODO: Find a way to gain back focus
194 remote.getCurrentWindow().blurWebView();
195 remote.getCurrentWindow().webContents.focus();
196
197 // The input "focus" attribute will only work on first modal open
198 // Manually add focus to the input element
199 // Wrapped inside timeout to let the modal render first
200 setTimeout(() => {
201 if (this.inputRef.current) {
202 this.inputRef.current.getElementsByTagName('input')[0].focus();
203 }
204 }, 10);
205
206 this.setState({
207 wasPrevVisible: true,
208 });
209 } else if (!isModalVisible && this.state.wasPrevVisible) {
210 // Manually blur focus from the input element to prevent
211 // search query change when modal not visible
212 setTimeout(() => {
213 if (this.inputRef.current) {
214 this.inputRef.current.getElementsByTagName('input')[0].blur();
215 }
216 }, 100);
217
218 this.setState({
219 wasPrevVisible: false,
220 });
221 }
222 }
223
224 // Close this modal
225 close() {
226 ModalState.isModalVisible = false;
227 }
228
229 render() {
230 const { isModalVisible } = ModalState;
231
232 const {
233 openService,
234 } = this;
235
236 const {
237 classes,
238 } = this.props;
239
240 const services = this.services();
241
242 const { intl } = this.context;
243
244 return (
245 <Modal
246 isOpen={isModalVisible}
247 className={classes.modal}
248 shouldCloseOnOverlayClick
249 close={this.close.bind(this)}
250 >
251 <div ref={this.inputRef}>
252 <Input
253 placeholder={intl.formatMessage(messages.search)}
254 focus
255 value={this.state.search}
256 onChange={this._handleSearchUpdate}
257 />
258 </div>
259
260 <div className={classes.services}>
261 { services.map((service, index) => (
262 <div
263 className={`${classes.service} ${this.state.selected === index ? classes.activeService : ''}`}
264 onClick={() => openService(index)}
265 key={service.id}
266 >
267 <img
268 src={service.icon}
269 className={classes.serviceIcon}
270 alt={service.recipe.name}
271 />
272 <div>
273 { service.name }
274 </div>
275 </div>
276 ))}
277 </div>
278
279 <p>{intl.formatMessage(messages.info)}</p>
280 </Modal>
281 );
282 }
283}
284
285QuickSwitchModal.wrappedComponent.propTypes = {
286 stores: PropTypes.shape({
287 services: PropTypes.instanceOf(ServicesStore).isRequired,
288 }).isRequired,
289 actions: PropTypes.shape({
290 service: PropTypes.shape({
291 setActive: PropTypes.func.isRequired,
292 }).isRequired,
293 }).isRequired,
294};