diff options
Diffstat (limited to 'src/features/webControls')
-rw-r--r-- | src/features/webControls/components/WebControls.js | 239 | ||||
-rw-r--r-- | src/features/webControls/containers/WebControlsScreen.js | 139 |
2 files changed, 378 insertions, 0 deletions
diff --git a/src/features/webControls/components/WebControls.js b/src/features/webControls/components/WebControls.js new file mode 100644 index 000000000..a39fcfe0e --- /dev/null +++ b/src/features/webControls/components/WebControls.js | |||
@@ -0,0 +1,239 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer } from 'mobx-react'; | ||
4 | import injectSheet from 'react-jss'; | ||
5 | import { Icon } from '@meetfranz/ui'; | ||
6 | import { defineMessages, intlShape } from 'react-intl'; | ||
7 | |||
8 | import { | ||
9 | mdiReload, mdiArrowRight, mdiArrowLeft, mdiHomeOutline, mdiEarth, | ||
10 | } from '@mdi/js'; | ||
11 | |||
12 | const messages = defineMessages({ | ||
13 | goHome: { | ||
14 | id: 'webControls.goHome', | ||
15 | defaultMessage: '!!!Home', | ||
16 | }, | ||
17 | openInBrowser: { | ||
18 | id: 'webControls.openInBrowser', | ||
19 | defaultMessage: '!!!Open in Browser', | ||
20 | }, | ||
21 | back: { | ||
22 | id: 'webControls.back', | ||
23 | defaultMessage: '!!!Back', | ||
24 | }, | ||
25 | forward: { | ||
26 | id: 'webControls.forward', | ||
27 | defaultMessage: '!!!Forward', | ||
28 | }, | ||
29 | reload: { | ||
30 | id: 'webControls.reload', | ||
31 | defaultMessage: '!!!Reload', | ||
32 | }, | ||
33 | }); | ||
34 | |||
35 | const styles = theme => ({ | ||
36 | root: { | ||
37 | background: theme.colorBackground, | ||
38 | position: 'relative', | ||
39 | borderLeft: [1, 'solid', theme.todos.todosLayer.borderLeftColor], | ||
40 | zIndex: 300, | ||
41 | height: 50, | ||
42 | display: 'flex', | ||
43 | flexDirection: 'row', | ||
44 | alignItems: 'center', | ||
45 | padding: [0, 10], | ||
46 | |||
47 | '& + div': { | ||
48 | height: 'calc(100% - 50px)', | ||
49 | }, | ||
50 | }, | ||
51 | button: { | ||
52 | width: 30, | ||
53 | height: 50, | ||
54 | transition: 'opacity 0.25s', | ||
55 | |||
56 | '&:hover': { | ||
57 | opacity: 0.8, | ||
58 | }, | ||
59 | |||
60 | '&:disabled': { | ||
61 | opacity: 0.5, | ||
62 | }, | ||
63 | }, | ||
64 | icon: { | ||
65 | width: '20px !important', | ||
66 | height: 20, | ||
67 | marginTop: 5, | ||
68 | }, | ||
69 | input: { | ||
70 | marginBottom: 0, | ||
71 | height: 'auto', | ||
72 | margin: [0, 10], | ||
73 | flex: 1, | ||
74 | border: 0, | ||
75 | padding: [4, 10], | ||
76 | borderRadius: theme.borderRadius, | ||
77 | background: theme.inputBackground, | ||
78 | color: theme.inputColor, | ||
79 | }, | ||
80 | inputButton: { | ||
81 | color: theme.colorText, | ||
82 | }, | ||
83 | }); | ||
84 | |||
85 | @injectSheet(styles) @observer | ||
86 | class WebControls extends Component { | ||
87 | static propTypes = { | ||
88 | classes: PropTypes.object.isRequired, | ||
89 | goHome: PropTypes.func.isRequired, | ||
90 | canGoBack: PropTypes.bool.isRequired, | ||
91 | goBack: PropTypes.func.isRequired, | ||
92 | canGoForward: PropTypes.bool.isRequired, | ||
93 | goForward: PropTypes.func.isRequired, | ||
94 | reload: PropTypes.func.isRequired, | ||
95 | openInBrowser: PropTypes.func.isRequired, | ||
96 | url: PropTypes.string.isRequired, | ||
97 | navigate: PropTypes.func.isRequired, | ||
98 | } | ||
99 | |||
100 | static contextTypes = { | ||
101 | intl: intlShape, | ||
102 | }; | ||
103 | |||
104 | static getDerivedStateFromProps(props, state) { | ||
105 | const { url } = props; | ||
106 | const { editUrl } = state; | ||
107 | |||
108 | if (!editUrl) { | ||
109 | return { | ||
110 | inputUrl: url, | ||
111 | editUrl: state.editUrl, | ||
112 | }; | ||
113 | } | ||
114 | } | ||
115 | |||
116 | inputRef = React.createRef(); | ||
117 | |||
118 | state = { | ||
119 | inputUrl: '', | ||
120 | editUrl: false, | ||
121 | } | ||
122 | |||
123 | render() { | ||
124 | const { | ||
125 | classes, | ||
126 | goHome, | ||
127 | canGoBack, | ||
128 | goBack, | ||
129 | canGoForward, | ||
130 | goForward, | ||
131 | reload, | ||
132 | openInBrowser, | ||
133 | url, | ||
134 | navigate, | ||
135 | } = this.props; | ||
136 | |||
137 | const { | ||
138 | inputUrl, | ||
139 | editUrl, | ||
140 | } = this.state; | ||
141 | |||
142 | const { intl } = this.context; | ||
143 | |||
144 | return ( | ||
145 | <div className={classes.root}> | ||
146 | <button | ||
147 | onClick={goHome} | ||
148 | type="button" | ||
149 | className={classes.button} | ||
150 | data-tip={intl.formatMessage(messages.goHome)} | ||
151 | > | ||
152 | <Icon | ||
153 | icon={mdiHomeOutline} | ||
154 | className={classes.icon} | ||
155 | /> | ||
156 | </button> | ||
157 | <button | ||
158 | onClick={goBack} | ||
159 | type="button" | ||
160 | className={classes.button} | ||
161 | disabled={!canGoBack} | ||
162 | data-tip={intl.formatMessage(messages.back)} | ||
163 | > | ||
164 | <Icon | ||
165 | icon={mdiArrowLeft} | ||
166 | className={classes.icon} | ||
167 | /> | ||
168 | </button> | ||
169 | <button | ||
170 | onClick={goForward} | ||
171 | type="button" | ||
172 | className={classes.button} | ||
173 | disabled={!canGoForward} | ||
174 | data-tip={intl.formatMessage(messages.forward)} | ||
175 | > | ||
176 | <Icon | ||
177 | icon={mdiArrowRight} | ||
178 | className={classes.icon} | ||
179 | /> | ||
180 | </button> | ||
181 | <button | ||
182 | onClick={reload} | ||
183 | type="button" | ||
184 | className={classes.button} | ||
185 | data-tip={intl.formatMessage(messages.reload)} | ||
186 | > | ||
187 | <Icon | ||
188 | icon={mdiReload} | ||
189 | className={classes.icon} | ||
190 | /> | ||
191 | </button> | ||
192 | <input | ||
193 | value={editUrl ? inputUrl : url} | ||
194 | className={classes.input} | ||
195 | onChange={event => this.setState({ | ||
196 | inputUrl: event.target.value, | ||
197 | })} | ||
198 | onFocus={(event) => { | ||
199 | event.target.select(); | ||
200 | this.setState({ | ||
201 | editUrl: true, | ||
202 | }); | ||
203 | }} | ||
204 | onKeyDown={(event) => { | ||
205 | if (event.key === 'Enter') { | ||
206 | this.setState({ | ||
207 | editUrl: false, | ||
208 | }); | ||
209 | navigate(inputUrl); | ||
210 | this.inputRef.current.blur(); | ||
211 | } else if (event.key === 'Escape') { | ||
212 | this.setState({ | ||
213 | editUrl: false, | ||
214 | inputUrl: url, | ||
215 | }); | ||
216 | event.target.blur(); | ||
217 | } | ||
218 | }} | ||
219 | ref={this.inputRef} | ||
220 | /> | ||
221 | <button | ||
222 | onClick={openInBrowser} | ||
223 | type="button" | ||
224 | className={classes.button} | ||
225 | data-tip={intl.formatMessage(messages.openInBrowser)} | ||
226 | data-place="left" | ||
227 | > | ||
228 | <Icon | ||
229 | icon={mdiEarth} | ||
230 | className={classes.icon} | ||
231 | /> | ||
232 | </button> | ||
233 | {/* <ReactTooltip place="bottom" type="dark" effect="solid" /> */} | ||
234 | </div> | ||
235 | ); | ||
236 | } | ||
237 | } | ||
238 | |||
239 | export default WebControls; | ||
diff --git a/src/features/webControls/containers/WebControlsScreen.js b/src/features/webControls/containers/WebControlsScreen.js new file mode 100644 index 000000000..cada01a6f --- /dev/null +++ b/src/features/webControls/containers/WebControlsScreen.js | |||
@@ -0,0 +1,139 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import { observer, inject } from 'mobx-react'; | ||
3 | import PropTypes from 'prop-types'; | ||
4 | |||
5 | import { autorun, observable } from 'mobx'; | ||
6 | import WebControls from '../components/WebControls'; | ||
7 | import ServicesStore from '../../../stores/ServicesStore'; | ||
8 | import Service from '../../../models/Service'; | ||
9 | |||
10 | const URL_EVENTS = [ | ||
11 | 'load-commit', | ||
12 | 'will-navigate', | ||
13 | 'did-navigate', | ||
14 | 'did-navigate-in-page', | ||
15 | ]; | ||
16 | |||
17 | @inject('stores', 'actions') @observer | ||
18 | class WebControlsScreen extends Component { | ||
19 | @observable url = ''; | ||
20 | |||
21 | @observable canGoBack = false; | ||
22 | |||
23 | @observable canGoForward = false; | ||
24 | |||
25 | webview = null; | ||
26 | |||
27 | autorunDisposer = null; | ||
28 | |||
29 | componentDidMount() { | ||
30 | const { service } = this.props; | ||
31 | |||
32 | this.autorunDisposer = autorun(() => { | ||
33 | if (service.isAttached) { | ||
34 | this.webview = service.webview; | ||
35 | |||
36 | URL_EVENTS.forEach((event) => { | ||
37 | this.webview.addEventListener(event, (e) => { | ||
38 | if (!e.isMainFrame) return; | ||
39 | |||
40 | this.url = e.url; | ||
41 | this.canGoBack = this.webview.canGoBack(); | ||
42 | this.canGoForward = this.webview.canGoForward(); | ||
43 | }); | ||
44 | }); | ||
45 | } | ||
46 | }); | ||
47 | } | ||
48 | |||
49 | componentWillUnmount() { | ||
50 | this.autorunDisposer(); | ||
51 | } | ||
52 | |||
53 | goHome() { | ||
54 | const { reloadActive } = this.props.actions.service; | ||
55 | |||
56 | if (!this.webview) return; | ||
57 | |||
58 | reloadActive(); | ||
59 | } | ||
60 | |||
61 | reload() { | ||
62 | if (!this.webview) return; | ||
63 | |||
64 | this.webview.reload(); | ||
65 | } | ||
66 | |||
67 | goBack() { | ||
68 | if (!this.webview) return; | ||
69 | |||
70 | this.webview.goBack(); | ||
71 | } | ||
72 | |||
73 | goForward() { | ||
74 | if (!this.webview) return; | ||
75 | |||
76 | this.webview.goForward(); | ||
77 | } | ||
78 | |||
79 | navigate(newUrl) { | ||
80 | if (!this.webview) return; | ||
81 | |||
82 | let url = newUrl; | ||
83 | |||
84 | try { | ||
85 | url = new URL(url).toString(); | ||
86 | } catch (err) { | ||
87 | // eslint-disable-next-line no-useless-escape | ||
88 | if (url.match(/^((?!-))(xn--)?[a-z0-9][a-z0-9-_]{0,61}[a-z0-9]{0,1}\.(xn--)?([a-z0-9\-]{1,61}|[a-z0-9-]{1,30}\.[a-z]{2,})$/)) { | ||
89 | url = `http://${url}`; | ||
90 | } else { | ||
91 | url = `https://www.google.com/search?query=${url}`; | ||
92 | } | ||
93 | } | ||
94 | |||
95 | this.webview.loadURL(url); | ||
96 | this.url = url; | ||
97 | } | ||
98 | |||
99 | openInBrowser() { | ||
100 | const { openExternalUrl } = this.props.actions.app; | ||
101 | |||
102 | if (!this.webview) return; | ||
103 | |||
104 | openExternalUrl({ url: this.url }); | ||
105 | } | ||
106 | |||
107 | render() { | ||
108 | return ( | ||
109 | <WebControls | ||
110 | goHome={() => this.goHome()} | ||
111 | reload={() => this.reload()} | ||
112 | openInBrowser={() => this.openInBrowser()} | ||
113 | canGoBack={this.canGoBack} | ||
114 | goBack={() => this.goBack()} | ||
115 | canGoForward={this.canGoForward} | ||
116 | goForward={() => this.goForward()} | ||
117 | navigate={url => this.navigate(url)} | ||
118 | url={this.url} | ||
119 | /> | ||
120 | ); | ||
121 | } | ||
122 | } | ||
123 | |||
124 | export default WebControlsScreen; | ||
125 | |||
126 | WebControlsScreen.wrappedComponent.propTypes = { | ||
127 | service: PropTypes.instanceOf(Service).isRequired, | ||
128 | stores: PropTypes.shape({ | ||
129 | services: PropTypes.instanceOf(ServicesStore).isRequired, | ||
130 | }).isRequired, | ||
131 | actions: PropTypes.shape({ | ||
132 | app: PropTypes.shape({ | ||
133 | openExternalUrl: PropTypes.func.isRequired, | ||
134 | }).isRequired, | ||
135 | service: PropTypes.shape({ | ||
136 | reloadActive: PropTypes.func.isRequired, | ||
137 | }).isRequired, | ||
138 | }).isRequired, | ||
139 | }; | ||