ff-stream-web

git clone git://archive.git.mtrnord.blog/MTRNord/ff-stream-web.git
Log | Files | Refs | README | LICENSE

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:
Mpackage.json | 1+
Msrc/app.js | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Msrc/components/Footer/Footer.js | 2+-
Msrc/components/Header/Header.js | 2+-
Asrc/components/Link/Link.js | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/Link/package.json | 6++++++
Msrc/components/Navigation/Navigation.js | 2+-
Msrc/constants/ActionTypes.js | 2+-
Msrc/core/Location.js | 23+++--------------------
Asrc/utils/DOMUtils.js | 17+++++++++++++++++
Dsrc/utils/Link.js | 29-----------------------------
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 };