diff options
author | 2019-10-01 21:51:34 +0200 | |
---|---|---|
committer | 2019-10-01 21:51:34 +0200 | |
commit | f8fbaad43522b91374eaed5e3df6fe47bc94862f (patch) | |
tree | e39b6301c183fe5e87c81f4f340e701faed2d3aa /src/features | |
parent | feat(Service API): Share `team`, `url` and `hasCustomIcon` with service (diff) | |
download | ferdium-app-f8fbaad43522b91374eaed5e3df6fe47bc94862f.tar.gz ferdium-app-f8fbaad43522b91374eaed5e3df6fe47bc94862f.tar.zst ferdium-app-f8fbaad43522b91374eaed5e3df6fe47bc94862f.zip |
feat(Custom Websites): Add simple browser controls
Diffstat (limited to 'src/features')
-rw-r--r-- | src/features/webControls/components/WebControls.js | 190 | ||||
-rw-r--r-- | src/features/webControls/containers/WebControlsScreen.js | 128 |
2 files changed, 318 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..03f601a17 --- /dev/null +++ b/src/features/webControls/components/WebControls.js | |||
@@ -0,0 +1,190 @@ | |||
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 | |||
7 | import { | ||
8 | mdiReload, mdiArrowRight, mdiArrowLeft, mdiHomeOutline, | ||
9 | } from '@mdi/js'; | ||
10 | |||
11 | const styles = theme => ({ | ||
12 | root: { | ||
13 | background: theme.colorBackground, | ||
14 | position: 'relative', | ||
15 | borderLeft: [1, 'solid', theme.todos.todosLayer.borderLeftColor], | ||
16 | zIndex: 300, | ||
17 | height: 50, | ||
18 | display: 'flex', | ||
19 | flexDirection: 'row', | ||
20 | alignItems: 'center', | ||
21 | padding: [0, 20], | ||
22 | |||
23 | '& + div': { | ||
24 | height: 'calc(100% - 50px)', | ||
25 | }, | ||
26 | }, | ||
27 | button: { | ||
28 | width: 30, | ||
29 | height: 50, | ||
30 | transition: 'opacity 0.25s', | ||
31 | |||
32 | '&:hover': { | ||
33 | opacity: 0.8, | ||
34 | }, | ||
35 | |||
36 | '&:disabled': { | ||
37 | opacity: 0.5, | ||
38 | }, | ||
39 | }, | ||
40 | icon: { | ||
41 | width: '20px !important', | ||
42 | height: 20, | ||
43 | marginTop: 5, | ||
44 | }, | ||
45 | input: { | ||
46 | marginBottom: 0, | ||
47 | height: 'auto', | ||
48 | marginLeft: 10, | ||
49 | flex: 1, | ||
50 | border: 0, | ||
51 | padding: [4, 10], | ||
52 | borderRadius: theme.borderRadius, | ||
53 | background: theme.inputBackground, | ||
54 | color: theme.inputColor, | ||
55 | }, | ||
56 | inputButton: { | ||
57 | color: theme.colorText, | ||
58 | }, | ||
59 | }); | ||
60 | |||
61 | @injectSheet(styles) @observer | ||
62 | class WebControls extends Component { | ||
63 | static propTypes = { | ||
64 | classes: PropTypes.object.isRequired, | ||
65 | goHome: PropTypes.func.isRequired, | ||
66 | canGoBack: PropTypes.bool.isRequired, | ||
67 | goBack: PropTypes.func.isRequired, | ||
68 | canGoForward: PropTypes.bool.isRequired, | ||
69 | goForward: PropTypes.func.isRequired, | ||
70 | reload: PropTypes.func.isRequired, | ||
71 | url: PropTypes.string.isRequired, | ||
72 | navigate: PropTypes.func.isRequired, | ||
73 | } | ||
74 | |||
75 | static getDerivedStateFromProps(props, state) { | ||
76 | const { url } = props; | ||
77 | const { editUrl } = state; | ||
78 | |||
79 | if (!editUrl) { | ||
80 | return { | ||
81 | inputUrl: url, | ||
82 | editUrl: state.editUrl, | ||
83 | }; | ||
84 | } | ||
85 | } | ||
86 | |||
87 | inputRef = React.createRef(); | ||
88 | |||
89 | state = { | ||
90 | inputUrl: '', | ||
91 | editUrl: false, | ||
92 | } | ||
93 | |||
94 | render() { | ||
95 | const { | ||
96 | classes, | ||
97 | goHome, | ||
98 | canGoBack, | ||
99 | goBack, | ||
100 | canGoForward, | ||
101 | goForward, | ||
102 | reload, | ||
103 | url, | ||
104 | navigate, | ||
105 | } = this.props; | ||
106 | |||
107 | const { | ||
108 | inputUrl, | ||
109 | editUrl, | ||
110 | } = this.state; | ||
111 | |||
112 | return ( | ||
113 | <div className={classes.root}> | ||
114 | <button | ||
115 | onClick={goHome} | ||
116 | type="button" | ||
117 | className={classes.button} | ||
118 | > | ||
119 | <Icon | ||
120 | icon={mdiHomeOutline} | ||
121 | className={classes.icon} | ||
122 | /> | ||
123 | </button> | ||
124 | <button | ||
125 | onClick={goBack} | ||
126 | type="button" | ||
127 | className={classes.button} | ||
128 | disabled={!canGoBack} | ||
129 | > | ||
130 | <Icon | ||
131 | icon={mdiArrowLeft} | ||
132 | className={classes.icon} | ||
133 | /> | ||
134 | </button> | ||
135 | <button | ||
136 | onClick={goForward} | ||
137 | type="button" | ||
138 | className={classes.button} | ||
139 | disabled={!canGoForward} | ||
140 | > | ||
141 | <Icon | ||
142 | icon={mdiArrowRight} | ||
143 | className={classes.icon} | ||
144 | /> | ||
145 | </button> | ||
146 | <button | ||
147 | onClick={reload} | ||
148 | type="button" | ||
149 | className={classes.button} | ||
150 | > | ||
151 | <Icon | ||
152 | icon={mdiReload} | ||
153 | className={classes.icon} | ||
154 | /> | ||
155 | </button> | ||
156 | <input | ||
157 | value={editUrl ? inputUrl : url} | ||
158 | className={classes.input} | ||
159 | onChange={event => this.setState({ | ||
160 | inputUrl: event.target.value, | ||
161 | })} | ||
162 | onFocus={(event) => { | ||
163 | event.target.select(); | ||
164 | this.setState({ | ||
165 | editUrl: true, | ||
166 | }); | ||
167 | }} | ||
168 | onKeyDown={(event) => { | ||
169 | if (event.key === 'Enter') { | ||
170 | this.setState({ | ||
171 | editUrl: false, | ||
172 | }); | ||
173 | navigate(inputUrl); | ||
174 | this.inputRef.current.blur(); | ||
175 | } else if (event.key === 'Escape') { | ||
176 | this.setState({ | ||
177 | editUrl: false, | ||
178 | inputUrl: url, | ||
179 | }); | ||
180 | event.target.blur(); | ||
181 | } | ||
182 | }} | ||
183 | ref={this.inputRef} | ||
184 | /> | ||
185 | </div> | ||
186 | ); | ||
187 | } | ||
188 | } | ||
189 | |||
190 | 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..1452d5a3d --- /dev/null +++ b/src/features/webControls/containers/WebControlsScreen.js | |||
@@ -0,0 +1,128 @@ | |||
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 | // 'dom-ready', | ||
13 | 'will-navigate', | ||
14 | 'did-navigate', | ||
15 | 'did-navigate-in-page', | ||
16 | ]; | ||
17 | |||
18 | @inject('stores', 'actions') @observer | ||
19 | class WebControlsScreen extends Component { | ||
20 | @observable url = ''; | ||
21 | |||
22 | @observable canGoBack = false; | ||
23 | |||
24 | @observable canGoForward = false; | ||
25 | |||
26 | webview = null; | ||
27 | |||
28 | autorunDisposer = null; | ||
29 | |||
30 | componentDidMount() { | ||
31 | const { service } = this.props; | ||
32 | |||
33 | this.autorunDisposer = autorun(() => { | ||
34 | if (service.isAttached) { | ||
35 | this.webview = service.webview; | ||
36 | |||
37 | URL_EVENTS.forEach((event) => { | ||
38 | this.webview.addEventListener(event, (e) => { | ||
39 | if (!e.isMainFrame) return; | ||
40 | |||
41 | this.url = e.url; | ||
42 | this.canGoBack = this.webview.canGoBack(); | ||
43 | this.canGoForward = this.webview.canGoForward(); | ||
44 | }); | ||
45 | }); | ||
46 | } | ||
47 | }); | ||
48 | } | ||
49 | |||
50 | componentWillUnmount() { | ||
51 | this.autorunDisposer(); | ||
52 | } | ||
53 | |||
54 | goHome() { | ||
55 | const { reloadActive } = this.props.actions.service; | ||
56 | |||
57 | if (!this.webview) return; | ||
58 | |||
59 | reloadActive(); | ||
60 | } | ||
61 | |||
62 | reload() { | ||
63 | if (!this.webview) return; | ||
64 | |||
65 | this.webview.reload(); | ||
66 | } | ||
67 | |||
68 | goBack() { | ||
69 | if (!this.webview) return; | ||
70 | |||
71 | this.webview.goBack(); | ||
72 | } | ||
73 | |||
74 | goForward() { | ||
75 | if (!this.webview) return; | ||
76 | |||
77 | this.webview.goForward(); | ||
78 | } | ||
79 | |||
80 | navigate(newUrl) { | ||
81 | if (!this.webview) return; | ||
82 | |||
83 | let url = newUrl; | ||
84 | |||
85 | try { | ||
86 | url = new URL(url).toString(); | ||
87 | } catch (err) { | ||
88 | // eslint-disable-next-line no-useless-escape | ||
89 | 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,})$/)) { | ||
90 | url = `http://${url}`; | ||
91 | } else { | ||
92 | url = `https://www.google.com/search?query=${url}`; | ||
93 | } | ||
94 | } | ||
95 | |||
96 | this.webview.loadURL(url); | ||
97 | this.url = url; | ||
98 | } | ||
99 | |||
100 | render() { | ||
101 | return ( | ||
102 | <WebControls | ||
103 | goHome={() => this.goHome()} | ||
104 | reload={() => this.reload()} | ||
105 | canGoBack={this.canGoBack} | ||
106 | goBack={() => this.goBack()} | ||
107 | canGoForward={this.canGoForward} | ||
108 | goForward={() => this.goForward()} | ||
109 | navigate={url => this.navigate(url)} | ||
110 | url={this.url} | ||
111 | /> | ||
112 | ); | ||
113 | } | ||
114 | } | ||
115 | |||
116 | export default WebControlsScreen; | ||
117 | |||
118 | WebControlsScreen.wrappedComponent.propTypes = { | ||
119 | service: PropTypes.instanceOf(Service).isRequired, | ||
120 | stores: PropTypes.shape({ | ||
121 | services: PropTypes.instanceOf(ServicesStore).isRequired, | ||
122 | }).isRequired, | ||
123 | actions: PropTypes.shape({ | ||
124 | service: PropTypes.shape({ | ||
125 | reloadActive: PropTypes.func.isRequired, | ||
126 | }).isRequired, | ||
127 | }).isRequired, | ||
128 | }; | ||