1 | import React from 'react';
|
2 | import { connect } from 'react-redux';
|
3 | import { createSelector } from 'reselect';
|
4 | import { isEqual, isEmpty } from 'lodash/lang';
|
5 |
|
6 | import * as navigationActions from './actions';
|
7 | import { METHODS } from './router';
|
8 | import { extractQuery, createQuery } from './pageUtils';
|
9 | import shouldGoBack from './shouldGoBack';
|
10 |
|
11 | const T = React.PropTypes;
|
12 |
|
13 | const isNewTabClick = (e) => e.metaKey || e.ctrlKey || e.button === 1 || e.button === 4;
|
14 |
|
15 | const findLinkParent = el => {
|
16 | if (el.tagName === 'A') {
|
17 | return el;
|
18 | }
|
19 |
|
20 | if (el.parentNode) {
|
21 | return findLinkParent(el.parentNode);
|
22 | }
|
23 | };
|
24 |
|
25 |
|
26 | export class _Anchor extends React.Component {
|
27 | static propTypes = {
|
28 | href: T.string,
|
29 | noop: T.bool,
|
30 | className: T.string,
|
31 | style: T.object,
|
32 | navigateToPage: T.func,
|
33 | onClick: T.func,
|
34 | };
|
35 |
|
36 | static defaultProps = {
|
37 | href: '#',
|
38 | noop: false,
|
39 | navigateToPage: () => {},
|
40 | onClick: () => {},
|
41 | };
|
42 |
|
43 | handleClick = e => {
|
44 | if (isNewTabClick(e)) { return; }
|
45 |
|
46 | this.props.onClick(e);
|
47 | if (e.defaultPrevented) { return; }
|
48 |
|
49 | e.stopPropagation();
|
50 | e.preventDefault();
|
51 |
|
52 | const url = this.props.href.split('?')[0];
|
53 | const queryParams = extractQuery(this.props.href);
|
54 |
|
55 | this.props.navigateToPage(url, queryParams);
|
56 | }
|
57 |
|
58 | render() {
|
59 | const { href, className, style, children } = this.props;
|
60 |
|
61 | return (
|
62 | <a
|
63 | href={ href }
|
64 | className={ className }
|
65 | style={ style }
|
66 | onClick={ this.handleClick }
|
67 | >
|
68 | { children }
|
69 | </a>
|
70 | );
|
71 | }
|
72 | }
|
73 |
|
74 | export class _BackAnchor extends React.Component {
|
75 | static propTypes = {
|
76 | href: T.string,
|
77 | backupHref: T.string,
|
78 | noop: T.bool,
|
79 | className: T.string,
|
80 | style: T.object,
|
81 | referrer: T.string,
|
82 | navigateToPage: T.func,
|
83 | onClick: T.func,
|
84 | };
|
85 |
|
86 | static defaultProps = {
|
87 | href: '#',
|
88 | backupHref: '#',
|
89 | noop: false,
|
90 | referrer: '',
|
91 | navigateToPage: () => {},
|
92 | onClick: () => {},
|
93 | };
|
94 |
|
95 | static AUTO_ROUTE = '__backanchor-auto-route';
|
96 |
|
97 | handleClick = e => {
|
98 | if (isNewTabClick(e)) { return; }
|
99 |
|
100 | this.props.onClick(e);
|
101 | if (e.defaultPrevented) { return; }
|
102 |
|
103 | e.stopPropagation();
|
104 | e.preventDefault();
|
105 |
|
106 | const { urlHistory, currentIndex, href, referrer, backupHref } = this.props;
|
107 |
|
108 | const unParsedUrl = href === _BackAnchor.AUTO_ROUTE ? referrer || backupHref : href;
|
109 | const url = unParsedUrl.split('?')[0];
|
110 | const queryParams = extractQuery(unParsedUrl);
|
111 |
|
112 | if (shouldGoBack(urlHistory, currentIndex, url, queryParams)) {
|
113 | history.back();
|
114 | } else {
|
115 | this.props.navigateToPage(url, queryParams);
|
116 | }
|
117 | }
|
118 |
|
119 | render() {
|
120 | const { href, className, style, children, referrer, backupHref } = this.props;
|
121 | const renderHref = href === _BackAnchor.AUTO_ROUTE
|
122 | ? referrer || backupHref
|
123 | : href;
|
124 |
|
125 | return (
|
126 | <a
|
127 | href={ renderHref }
|
128 | className={ className }
|
129 | style={ style }
|
130 | onClick={ this.handleClick }
|
131 | >
|
132 | { children }
|
133 | </a>
|
134 | );
|
135 | }
|
136 | }
|
137 |
|
138 | const anchorSelector = createSelector(
|
139 | state => state.platform.history,
|
140 | state => state.platform.currentPageIndex,
|
141 | state => state.platform.currentPage.referrer,
|
142 | (urlHistory, currentIndex, referrer) => ({ urlHistory, currentIndex, referrer })
|
143 | );
|
144 |
|
145 | const anchorDispatcher = dispatch => ({
|
146 | navigateToPage: (url, queryParams) => dispatch(
|
147 | navigationActions.navigateToUrl(METHODS.GET, url, { queryParams })
|
148 | ),
|
149 | });
|
150 |
|
151 | export const Anchor = connect(null, anchorDispatcher)(_Anchor);
|
152 | export const BackAnchor = connect(anchorSelector, anchorDispatcher)(_BackAnchor);
|
153 | BackAnchor.AUTO_ROUTE = _BackAnchor.AUTO_ROUTE;
|
154 |
|
155 |
|
156 |
|
157 |
|
158 |
|
159 |
|
160 |
|
161 |
|
162 |
|
163 |
|
164 |
|
165 | export class _LinkHijacker extends React.Component {
|
166 | static propTypes = {
|
167 | children: T.element.isRequired,
|
168 | onLinkClick: T.func,
|
169 |
|
170 |
|
171 |
|
172 | urlRegexp: T.instanceOf(RegExp),
|
173 |
|
174 |
|
175 |
|
176 | navigateToPage: T.func.isRequired,
|
177 | }
|
178 |
|
179 | static defaultProps = {
|
180 | navigateToPage: () => {},
|
181 | onLinkClick: () => {},
|
182 | };
|
183 |
|
184 | extractValidPath($link) {
|
185 | const href = $link.getAttribute('href');
|
186 | if (!href) { return; }
|
187 |
|
188 |
|
189 | if (href.indexOf('//') === -1) {
|
190 | return href;
|
191 | }
|
192 |
|
193 |
|
194 | const { urlRegexp }= this.props;
|
195 | if (urlRegexp) {
|
196 | const match = href.match(urlRegexp);
|
197 | if (match && match[1]) {
|
198 | return match[1];
|
199 | }
|
200 | }
|
201 | }
|
202 |
|
203 | onClick = e => {
|
204 |
|
205 |
|
206 | const { children: child } = this.props;
|
207 | if (child && child.props.onClick) {
|
208 | child.props.onClick(e);
|
209 | if (e.defaultPrevented) { return; }
|
210 | }
|
211 |
|
212 | const $link = findLinkParent(e.target);
|
213 | if (!$link) { return; }
|
214 |
|
215 | if (isNewTabClick(e)) { return; }
|
216 |
|
217 | const path = this.extractValidPath($link);
|
218 | if (!path) { return; }
|
219 |
|
220 | this.props.onLinkClick(path, e, $link);
|
221 | if (e.defaultPrevented) { return; }
|
222 |
|
223 | e.stopPropagation();
|
224 | e.preventDefault();
|
225 |
|
226 | const url = path.split('?')[0];
|
227 | const queryParams = extractQuery(path);
|
228 |
|
229 | this.props.navigateToPage(url, queryParams);
|
230 | }
|
231 |
|
232 | render() {
|
233 | const { children: child } = this.props;
|
234 |
|
235 | return React.cloneElement(React.Children.only(child), {
|
236 | onClick: this.onClick,
|
237 | });
|
238 | }
|
239 | }
|
240 |
|
241 | export const LinkHijacker = connect(null, anchorDispatcher)(_LinkHijacker);
|
242 |
|
243 |
|
244 | const getValues = form => {
|
245 | if (!form || form.nodeName.toLowerCase() !== 'form') {
|
246 | return {};
|
247 | }
|
248 |
|
249 | return Array.from(form.elements).reduce((values, el) => {
|
250 | if (el.name) {
|
251 | switch (el.type) {
|
252 | case 'checkbox': {
|
253 | if (!values[el.name]) { values[el.name] = []; }
|
254 | if (el.checked) { values[el.name].push(el.value); }
|
255 | break;
|
256 | }
|
257 | case 'select-multiple': {
|
258 | values[el.name] = Array.from(el.options).map(o => o.value);
|
259 | break;
|
260 | }
|
261 | case 'radio': {
|
262 | if (el.checked) { values[el.name] = el.value; }
|
263 | break;
|
264 | }
|
265 | default: {
|
266 | values[el.name] = el.value;
|
267 | break;
|
268 | }
|
269 | }
|
270 | }
|
271 | return values;
|
272 | }, {});
|
273 | };
|
274 |
|
275 | const preventFormSubmit = e => {
|
276 | e.preventDefault();
|
277 |
|
278 |
|
279 |
|
280 |
|
281 | if (document.activeElement) {
|
282 | document.activeElement.blur();
|
283 | }
|
284 | };
|
285 |
|
286 | export class _Form extends React.Component {
|
287 | static propTypes = {
|
288 | action: T.string.isRequired,
|
289 | method: T.oneOf([METHODS.POST, METHODS.GET]),
|
290 | className: T.string,
|
291 | style: T.object,
|
292 | onSubmit: T.func,
|
293 | };
|
294 |
|
295 | static defaultProps = {
|
296 | method: METHODS.POST,
|
297 | onSubmit: () => {},
|
298 | };
|
299 |
|
300 | handleSubmit = e => {
|
301 | preventFormSubmit(e);
|
302 |
|
303 | const form = e.target;
|
304 | this.props.onSubmit(this.props.action, this.props.method, getValues(form));
|
305 | }
|
306 |
|
307 | render() {
|
308 | const { className, action, method, style, children } = this.props;
|
309 |
|
310 | return (
|
311 | <form
|
312 | className={ className }
|
313 | action={ action }
|
314 | method={ method }
|
315 | style={ style }
|
316 | onSubmit={ this.handleSubmit }
|
317 | >
|
318 | { children }
|
319 | </form>
|
320 | );
|
321 | }
|
322 | }
|
323 |
|
324 | const formDispatcher = dispatch => ({
|
325 | onSubmit: (url, method, bodyParams) => dispatch(
|
326 | navigationActions.navigateToUrl(method, url, { bodyParams })
|
327 | ),
|
328 | });
|
329 |
|
330 | export const Form = connect(null, formDispatcher)(_Form);
|
331 |
|
332 |
|
333 | export class JSForm extends React.Component {
|
334 | static propTypes = {
|
335 | onSubmit: T.func.isRequired,
|
336 | className: T.string,
|
337 | style: T.object,
|
338 | };
|
339 |
|
340 | handleSubmit = e => {
|
341 | preventFormSubmit(e);
|
342 | const form = e.target;
|
343 | this.props.onSubmit(getValues(form));
|
344 | }
|
345 |
|
346 | render() {
|
347 | const { className, style, children } = this.props;
|
348 |
|
349 | return (
|
350 | <form className={ className } style={ style } onSubmit={ this.handleSubmit }>
|
351 | { children }
|
352 | </form>
|
353 | );
|
354 | }
|
355 | }
|
356 |
|
357 |
|
358 |
|
359 | export class _UrlSync extends React.Component {
|
360 | static propTypes = {
|
361 | pageIndex: T.number.isRequired,
|
362 | history: T.array.isRequired,
|
363 | gotoPageIndex: T.func,
|
364 | navigateToPage: T.func,
|
365 | };
|
366 |
|
367 | static defaultProps = {
|
368 | gotoPageIndex: () => {},
|
369 | navigateToPage: () => {},
|
370 | };
|
371 |
|
372 | componentDidMount() {
|
373 | const handlePopstate = () => {
|
374 | const pathname = self.location.pathname;
|
375 | const currentQuery = extractQuery(self.location.search);
|
376 | const currentHash = {};
|
377 | let pageIndex = -1;
|
378 | let hist = {};
|
379 |
|
380 | for (let i = this.props.history.length - 1; i >= 0; i--) {
|
381 | hist = this.props.history[i];
|
382 | if (hist.url === pathname && isEqual(hist.queryParams, currentQuery)) {
|
383 | pageIndex = i;
|
384 | break;
|
385 | }
|
386 | }
|
387 |
|
388 | if (pageIndex > -1) {
|
389 | const { url, queryParams, hashParams, urlParams, referrer } = hist;
|
390 | this.props.gotoPageIndex(pageIndex, url, { queryParams, hashParams, urlParams, referrer });
|
391 | } else {
|
392 |
|
393 | this.props.navigateToPage(pathname, currentQuery, currentHash);
|
394 | }
|
395 | };
|
396 |
|
397 | self.addEventListener('popstate', handlePopstate);
|
398 | self.addEventListener('hashchange', handlePopstate);
|
399 | }
|
400 |
|
401 | componentWillUpdate(nextProps) {
|
402 | const currentQuery = extractQuery(self.location.search);
|
403 | const { pageIndex, history } = nextProps;
|
404 | const page = history[pageIndex];
|
405 | const newUrl = page.url;
|
406 | const newQuery = page.queryParams;
|
407 |
|
408 | if ((self.location.pathname !== newUrl) || (!isEqual(currentQuery, newQuery))) {
|
409 | if (self.history && self.history.pushState) {
|
410 | let newHref = newUrl;
|
411 | if (!isEmpty(newQuery)) { newHref += createQuery(newQuery); }
|
412 | self.history.pushState({}, '', newHref);
|
413 | } else {
|
414 | self.location = newUrl;
|
415 | }
|
416 | }
|
417 | }
|
418 |
|
419 | render() {
|
420 | return false;
|
421 | }
|
422 | }
|
423 |
|
424 | const urlSelector = createSelector(
|
425 | state => state.platform.currentPageIndex,
|
426 | state => state.platform.history,
|
427 | (pageIndex, history) => ({ pageIndex, history })
|
428 | );
|
429 |
|
430 | const urlDispatcher = dispatch => ({
|
431 | gotoPageIndex: (index, url, data) => dispatch(navigationActions.gotoPageIndex(index, url, data)),
|
432 | navigateToPage: (url, queryParams, hashParams) => dispatch(
|
433 | navigationActions.navigateToUrl(METHODS.GET, url, { queryParams, hashParams })
|
434 | ),
|
435 | });
|
436 |
|
437 | export const UrlSync = connect(urlSelector, urlDispatcher)(_UrlSync);
|