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