commit d8b96de8b2dd58beb7237ecdf1346f18780b4eaa
parent 9457b36f462c105b3856af0c1f4cb98b9e380db9
Author: Konstantin Tarkus <hello@tarkus.me>
Date: Thu, 27 Aug 2015 01:51:46 +0300
Update Location component and navigation logic
Diffstat:
11 files changed, 148 insertions(+), 76 deletions(-)
diff --git a/package.json b/package.json
@@ -13,6 +13,7 @@
"fbjs": "0.1.0-alpha.7",
"flux": "2.0.3",
"front-matter": "1.0.0",
+ "history": "1.8.0",
"jade": "1.11.0",
"lodash": "3.10.1",
"normalize.css": "3.0.3",
diff --git a/src/app.js b/src/app.js
@@ -7,6 +7,7 @@ import Dispatcher from './core/Dispatcher';
import Router from './Router';
import Location from './core/Location';
import ActionTypes from './constants/ActionTypes';
+import { addEventListener, removeEventListener } from './utils/DOMUtils';
const container = document.getElementById('app');
const context = {
@@ -27,37 +28,74 @@ const context = {
}
};
-function run() {
- FastClick.attach(document.body);
+function cleanUp() {
+ let done = false;
+ if (!done) {
+ // Remove the pre-rendered CSS because it's no longer used
+ // after the React app is launched
+ const css = document.getElementById('css');
+ if (css) {
+ css.parentNode.removeChild(css);
+ done = true;
+ }
+ }
+}
- Router.dispatch({ path: window.location.pathname, context }, (state, component) => {
+function render(state) {
+ Router.dispatch(state, (_, component) => {
ReactDOM.render(component, container, () => {
- let css = document.getElementById('css');
- css.parentNode.removeChild(css);
+ // Restore the scroll position if it was saved into the state
+ if (state.scrollY !== undefined) {
+ window.scrollTo(state.scrollX, state.scrollY);
+ } else {
+ window.scrollTo(0, 0);
+ }
+ cleanUp();
});
});
+}
- Dispatcher.register(action => {
- if (action.type === ActionTypes.CHANGE_LOCATION) {
- Router.dispatch({ path: action.path, context }, (state, component) => {
- ReactDOM.render(component, container);
- });
- }
+function run() {
+ let currentLocation = null;
+ let currentState = null;
+
+ // Make taps on links and buttons work fast on mobiles
+ FastClick.attach(document.body);
+
+ // Re-render the app when window.location changes
+ const unlisten = Location.listen(location => {
+ currentLocation = location;
+ currentState = Object.assign({}, location.state, {
+ path: location.pathname,
+ query: location.query,
+ state: location.state,
+ context
+ });
+ render(currentState);
});
-}
-function handlePopState(event) {
- Location.navigateTo(window.location.pathname, { replace: !!event.state });
+ // Save the page scroll position into the current location's state
+ var supportPageOffset = window.pageXOffset !== undefined;
+ var isCSS1Compat = ((document.compatMode || '') === 'CSS1Compat');
+ const setPageOffset = () => {
+ currentLocation.state = currentLocation.state || Object.create(null);
+ currentLocation.state.scrollX = supportPageOffset ? window.pageXOffset : isCSS1Compat ?
+ document.documentElement.scrollLeft : document.body.scrollLeft;
+ currentLocation.state.scrollY = supportPageOffset ? window.pageYOffset : isCSS1Compat ?
+ document.documentElement.scrollTop : document.body.scrollTop;
+ };
+
+ addEventListener(window, 'scroll', setPageOffset);
+ addEventListener(window, 'pagehide', () => {
+ removeEventListener(window, 'scroll', setPageOffset);
+ unlisten();
+ });
}
// Run the application when both DOM is ready
// and page content is loaded
-new Promise(resolve => {
- if (window.addEventListener) {
- window.addEventListener('DOMContentLoaded', resolve);
- window.addEventListener('popstate', handlePopState);
- } else {
- window.attachEvent('onload', resolve);
- window.attachEvent('popstate', handlePopState);
- }
-}).then(run);
+if (window.addEventListener) {
+ window.addEventListener('DOMContentLoaded', run);
+} else {
+ window.attachEvent('onload', run);
+}
diff --git a/src/components/Footer/Footer.js b/src/components/Footer/Footer.js
@@ -4,7 +4,7 @@ import React, { PropTypes } from 'react';
import styles from './Footer.css';
import withViewport from '../../decorators/withViewport';
import withStyles from '../../decorators/withStyles';
-import Link from '../../utils/Link';
+import Link from '../Link';
@withViewport
@withStyles(styles)
diff --git a/src/components/Header/Header.js b/src/components/Header/Header.js
@@ -3,7 +3,7 @@
import React from 'react';
import styles from './Header.css';
import withStyles from '../../decorators/withStyles';
-import Link from '../../utils/Link';
+import Link from '../Link';
import Navigation from '../Navigation';
@withStyles(styles)
diff --git a/src/components/Link/Link.js b/src/components/Link/Link.js
@@ -0,0 +1,56 @@
+/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */
+
+import React, { PropTypes } from 'react';
+import Location from '../../core/Location';
+
+function isLeftClickEvent(event) {
+ return event.button === 0;
+}
+
+function isModifiedEvent(event) {
+ return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
+}
+
+class Link {
+
+ static propTypes = {
+ to: PropTypes.string.isRequired,
+ children: PropTypes.element.isRequired,
+ state: PropTypes.object,
+ onClick: PropTypes.func
+ };
+
+ static handleClick = event => {
+ var allowTransition = true;
+ var clickResult;
+
+ if (this.props && this.props.onClick) {
+ clickResult = this.props.onClick(event);
+ }
+
+ if (isModifiedEvent(event) || !isLeftClickEvent(event)) {
+ return;
+ }
+
+ if (clickResult === false || event.defaultPrevented === true) {
+ allowTransition = false;
+ }
+
+ event.preventDefault();
+
+ if (allowTransition) {
+ const link = event.currentTarget;
+ Location.pushState(
+ this.props && this.props.state || null,
+ this.props && this.props.to || (link.pathname + link.search));
+ }
+ };
+
+ render() {
+ const { to, children, ...props } = this.props;
+ return <a onClick={Link.handleClick.bind(this)} {...props}>{children}</a>;
+ }
+
+}
+
+export default Link;
diff --git a/src/components/Link/package.json b/src/components/Link/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "Link",
+ "version": "0.0.0",
+ "private": true,
+ "main": "./Link.js"
+}
diff --git a/src/components/Navigation/Navigation.js b/src/components/Navigation/Navigation.js
@@ -4,7 +4,7 @@ import React, { PropTypes } from 'react';
import classNames from 'classnames';
import styles from './Navigation.css';
import withStyles from '../../decorators/withStyles';
-import Link from '../../utils/Link';
+import Link from '../Link';
@withStyles(styles)
class Navigation {
diff --git a/src/constants/ActionTypes.js b/src/constants/ActionTypes.js
@@ -3,5 +3,5 @@
import keyMirror from 'fbjs/lib/keyMirror';
export default keyMirror({
- CHANGE_LOCATION: null
+
});
diff --git a/src/core/Location.js b/src/core/Location.js
@@ -1,26 +1,9 @@
/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */
import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment';
-import Dispatcher from '../core/Dispatcher';
-import ActionTypes from '../constants/ActionTypes';
+import createHistory from 'history/lib/createBrowserHistory';
+import useQueries from 'history/lib/useQueries';
-const location = {
-
- navigateTo(path, options) {
- if (canUseDOM) {
- if (options && options.replace) {
- window.history.replaceState({}, document.title, path);
- } else {
- window.history.pushState({}, document.title, path);
- }
- }
-
- Dispatcher.dispatch({
- type: ActionTypes.CHANGE_LOCATION,
- path
- });
- }
-
-};
+const location = canUseDOM ? useQueries(createHistory)({}) : {};
export default location;
diff --git a/src/utils/DOMUtils.js b/src/utils/DOMUtils.js
@@ -0,0 +1,17 @@
+/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */
+
+export function addEventListener(node, event, listener) {
+ if (node.addEventListener) {
+ node.addEventListener(event, listener, false);
+ } else {
+ node.attachEvent('on' + event, listener);
+ }
+}
+
+export function removeEventListener(node, event, listener) {
+ if (node.removeEventListener) {
+ node.removeEventListener(event, listener, false);
+ } else {
+ node.detachEvent('on' + event, listener);
+ }
+}
diff --git a/src/utils/Link.js b/src/utils/Link.js
@@ -1,29 +0,0 @@
-/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */
-
-import invariant from 'fbjs/lib/invariant';
-import Location from '../core/Location';
-
-function handleClick(event) {
-
- // If not left mouse click
- if (event.button !== 0) {
- return;
- }
-
- // If modified event
- if (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) {
- return;
- }
-
- var el = event.currentTarget;
-
- invariant(el && el.nodeName === 'A', 'The target element must be a link.');
-
- // Rebuild path
- var path = el.pathname + el.search + (el.hash || '');
-
- event.preventDefault();
- Location.navigateTo(path);
-}
-
-export default { handleClick };