├── .gitignore ├── .npmignore ├── README.md ├── package.json └── src ├── ReactTouch.js ├── environment ├── FPSCounter.js ├── StyleKeys.js └── ZyngaScroller.js ├── interactions ├── frostedglass │ ├── FrostedGlassContainer.js │ └── helpers │ │ └── FrostedGlassViewport.js ├── leftnav │ ├── LeftNavBehaviors.js │ └── LeftNavContainer.js └── simplescroller │ └── SimpleScroller.js ├── primitives ├── AnimatableContainer.js ├── App.js ├── TouchableArea.js └── helpers │ └── StaticContainer.js ├── routing ├── RoutedLink.js └── Router.js └── thirdparty ├── ResponderEventPlugin.js ├── TapEventPlugin.js └── TouchEventUtils.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | browser-bundle.js 3 | npm-debug.log 4 | *~ 5 | lib/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-touch 2 | 3 | ![unmaintained](http://img.shields.io/badge/status-unmaintained-red.png) 4 | 5 | Component library for building intertial touch applications with React. 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-touch", 3 | "version": "0.0.6", 4 | "description": "React component library for building inertial touch applications.", 5 | "main": "lib/ReactTouch.js", 6 | "peerDependencies": { 7 | "react": "~0.9" 8 | }, 9 | "dependencies": {}, 10 | "devDependencies": { 11 | "jsxc": "~0.1.4" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/petehunt/react-touch-lib" 16 | }, 17 | "scripts": { 18 | "start": "jsxc --watch src/ lib/", 19 | "build": "jsxc src/ lib/" 20 | }, 21 | "files": ["lib/"], 22 | "author": "Pete Hunt", 23 | "license": "Apache 2" 24 | } 25 | -------------------------------------------------------------------------------- /src/ReactTouch.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | var Router = require('./routing/Router'); 4 | 5 | var ReactTouch = { 6 | start: function(componentClass, domNode, routes, useHistory) { 7 | var EventPluginHub = require('react/lib/EventPluginHub'); 8 | var ResponderEventPlugin = require('./thirdparty/ResponderEventPlugin'); 9 | var TapEventPlugin = require('./thirdparty/TapEventPlugin'); 10 | 11 | EventPluginHub.injection.injectEventPluginsByName({ 12 | ResponderEventPlugin: ResponderEventPlugin, 13 | TapEventPlugin: TapEventPlugin 14 | }); 15 | 16 | React.initializeTouchEvents(true); 17 | 18 | Router.start(componentClass, domNode, routes, useHistory); 19 | } 20 | }; 21 | 22 | module.exports = ReactTouch; -------------------------------------------------------------------------------- /src/environment/FPSCounter.js: -------------------------------------------------------------------------------- 1 | var rAF = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame; 2 | 3 | var FPSCounter = { 4 | start: function() { 5 | var stats = new Stats(); 6 | stats.setMode(0); // 0: fps, 1: ms 7 | 8 | // Align top-left 9 | stats.domElement.style.position = 'absolute'; 10 | stats.domElement.style.right = '0px'; 11 | stats.domElement.style.bottom = '0px'; 12 | 13 | document.body.appendChild(stats.domElement); 14 | 15 | function tick() { 16 | stats.update(); 17 | rAF(tick); 18 | } 19 | 20 | tick(); 21 | } 22 | }; 23 | 24 | module.exports = FPSCounter; -------------------------------------------------------------------------------- /src/environment/StyleKeys.js: -------------------------------------------------------------------------------- 1 | var TRANSFORM_KEY = typeof document.body.style.MozTransform !== 'undefined' ? 'MozTransform' : 'WebkitTransform'; 2 | var FILTER_KEY = typeof document.body.style.MozFilter !== 'undefined' ? 'MozFilter' : 'WebkitFilter'; 3 | 4 | module.exports = { 5 | TRANSFORM: TRANSFORM_KEY, 6 | FILTER: FILTER_KEY 7 | }; -------------------------------------------------------------------------------- /src/environment/ZyngaScroller.js: -------------------------------------------------------------------------------- 1 | var ZyngaScroller = window.Scroller; 2 | 3 | module.exports = ZyngaScroller; -------------------------------------------------------------------------------- /src/interactions/frostedglass/FrostedGlassContainer.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | var React = require('react'); 4 | 5 | var FrostedGlassViewport = require('./helpers/FrostedGlassViewport'); 6 | var StyleKeys = require('../../environment/StyleKeys'); 7 | 8 | function shallowCopy(x) { 9 | var y = {}; 10 | for (var z in x) { 11 | if (!x.hasOwnProperty(z)) { 12 | continue; 13 | } 14 | y[z] = x[z]; 15 | } 16 | return y; 17 | } 18 | 19 | function cloneChildren(children) { 20 | if (React.isValidComponent(children)) { 21 | return cloneComponent(children); 22 | } else if (Array.isArray(children)) { 23 | return children.map(cloneComponent); 24 | } else if (!children) { 25 | return null; 26 | } else { 27 | var r = {}; 28 | for (var k in children) { 29 | if (!children.hasOwnProperty(k)) { 30 | continue; 31 | } 32 | r[k] = cloneComponent(children[k]); 33 | } 34 | return r; 35 | } 36 | } 37 | 38 | function cloneComponent(component) { 39 | if (!React.isValidComponent(component)) { 40 | // string or something 41 | return component; 42 | } 43 | var newInstance = new component.constructor(); 44 | var newChildren = cloneChildren(component.props.children); 45 | var newProps = shallowCopy(component.props); 46 | delete newProps.children; 47 | newInstance.construct(newProps, newChildren); 48 | return newInstance; 49 | } 50 | 51 | var GlassContainer = React.createClass({ 52 | getDefaultProps: function() { 53 | return {style: {}, overlays: {}}; 54 | }, 55 | 56 | render: function() { 57 | var viewports = [ 58 | 68 | ]; 69 | 70 | for (var key in this.props.overlays) { 71 | var overlay = this.props.overlays[key]; 72 | 73 | // TODO: this is somewhat of an anti-pattern: cloneChildren() should create the 74 | // children with the correct props. But I'm too lazy to build the correct deep 75 | // merger. And this isn't that bad since this component owns the props anyway. 76 | var clonedChildren = cloneChildren(this.props.children); 77 | 78 | clonedChildren.props = shallowCopy(clonedChildren.props); 79 | clonedChildren.props.style = shallowCopy(clonedChildren.props.style || {}); 80 | clonedChildren.props.style[StyleKeys.FILTER] = 'blur(5px)'; 81 | 82 | viewports.push( 83 | 91 | {overlay.children} 92 | 93 | ); 94 | } 95 | 96 | var newProps = shallowCopy(this.props); 97 | newProps.style = newProps.style || {}; 98 | newProps.style.position = newProps.style.position || 'relative'; 99 | newProps.style.overflow = 'hidden'; 100 | 101 | return React.DOM.div(newProps, viewports); 102 | } 103 | }); 104 | 105 | module.exports = GlassContainer; -------------------------------------------------------------------------------- /src/interactions/frostedglass/helpers/FrostedGlassViewport.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | var React = require('react'); 4 | 5 | var TouchableArea = 6 | require('../../../primitives/TouchableArea'); 7 | 8 | var FrostedGlassViewport = React.createClass({ 9 | getDefaultProps: function() { 10 | return {glassStyle: {}}; 11 | }, 12 | 13 | render: function() { 14 | var style = { 15 | position: 'absolute', 16 | left: this.props.left, 17 | top: this.props.top, 18 | width: this.props.width, 19 | height: this.props.height, 20 | overflow: 'hidden' 21 | }; 22 | 23 | var glassStyle = this.props.glassStyle || {}; 24 | glassStyle.position = 'absolute'; 25 | // TODO: this won't animate well. Not sure if compositing will 26 | // make things better or worse... 27 | glassStyle.left = -this.props.left; 28 | glassStyle.top = -this.props.top; 29 | 30 | var contentStyle = { 31 | bottom: 0, 32 | left: 0, 33 | position: 'absolute', 34 | right: 0, 35 | top: 0 36 | }; 37 | 38 | return this.transferPropsTo( 39 | 40 |
41 | {this.props.glassContent} 42 |
43 |
44 | {this.props.children} 45 |
46 |
47 | ); 48 | } 49 | }); 50 | 51 | module.exports = FrostedGlassViewport; -------------------------------------------------------------------------------- /src/interactions/leftnav/LeftNavBehaviors.js: -------------------------------------------------------------------------------- 1 | var LeftNavBehaviors = { 2 | PARALLAX_FADE: { 3 | side: { 4 | translate: function(sideWidth, scrollLeft) { 5 | return { 6 | x: sideWidth - .5 * scrollLeft 7 | }; 8 | }, 9 | rotate: function() { 10 | return null; 11 | }, 12 | opacity: function(sideWidth, scrollLeft) { 13 | return .5 + .5 * (1 - scrollLeft / sideWidth); 14 | } 15 | }, 16 | top: { 17 | translate: function(sideWidth, scrollLeft) { 18 | return {x: sideWidth - scrollLeft}; 19 | }, 20 | rotate: function() { 21 | return null; 22 | }, 23 | opacity: function() { 24 | return null; 25 | } 26 | }, 27 | content: { 28 | translate: function(sideWidth, scrollLeft) { 29 | return {x: sideWidth - scrollLeft}; 30 | }, 31 | rotate: function() { 32 | return null; 33 | }, 34 | opacity: function() { 35 | return null; 36 | } 37 | } 38 | } 39 | }; 40 | 41 | module.exports = LeftNavBehaviors; -------------------------------------------------------------------------------- /src/interactions/leftnav/LeftNavContainer.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | var React = require('react'); 4 | 5 | var AnimatableContainer = require('../../primitives/AnimatableContainer'); 6 | var LeftNavBehaviors = require('./LeftNavBehaviors'); 7 | var TouchableArea = require('../../primitives/TouchableArea'); 8 | var ZyngaScroller = require('../../environment/ZyngaScroller'); 9 | 10 | var LeftNavContainer = React.createClass({ 11 | componentWillMount: function() { 12 | this.scroller = new Scroller(this._handleScroll, { 13 | bouncing: false, 14 | scrollingX: true, 15 | scrollingY: false, 16 | snapping: true 17 | }); 18 | }, 19 | 20 | componentDidMount: function() { 21 | this._measure(); 22 | }, 23 | 24 | _measure: function() { 25 | var node = this.getDOMNode(); 26 | this.scroller.setDimensions( 27 | node.clientWidth, 28 | node.clientHeight, 29 | node.clientWidth + this.props.sideWidth, 30 | node.clientHeight 31 | ); 32 | this.scroller.setSnapSize(this.props.sideWidth, node.clientHeight); 33 | this.scroller.scrollTo(this.props.sideWidth, 0); 34 | }, 35 | 36 | componentDidUpdate: function(prevProps) { 37 | if (this.props.sideWidth !== prevProps.sideWidth) { 38 | this._measure(); 39 | } 40 | }, 41 | 42 | closeNav: function() { 43 | if (this.isNavOpen()) { 44 | this.scroller.scrollTo(this.props.sideWidth, 0, true); 45 | } 46 | }, 47 | 48 | _handleScroll: function(left, top, zoom) { 49 | this.setState({scrollLeft: left}); 50 | }, 51 | 52 | getInitialState: function() { 53 | return {scrollLeft: 0}; 54 | }, 55 | 56 | getDefaultProps: function() { 57 | return { 58 | behavior: LeftNavBehaviors.PARALLAX_FADE 59 | }; 60 | }, 61 | 62 | _handleTap: function() { 63 | if (this.isNavOpen()) { 64 | this.scroller.scrollTo(this.props.sideWidth, 0, true); 65 | } else { 66 | this.scroller.scrollTo(0, 0, true); 67 | } 68 | }, 69 | 70 | _handleContentTouchTap: function(e) { 71 | if (!this.isNavOpen()) { 72 | return; 73 | } 74 | 75 | this.scroller.scrollTo(this.props.sideWidth, 0, true); 76 | e.preventDefault(); 77 | }, 78 | 79 | isNavOpen: function() { 80 | return this.state.scrollLeft !== this.props.sideWidth; 81 | }, 82 | 83 | render: function() { 84 | // props: 85 | // sideWidth 86 | // topHeight 87 | // topContent 88 | // button 89 | // sideContent 90 | // children (big content area) 91 | var sidebarX = (this.props.sideWidth - this.state.scrollLeft); 92 | 93 | var side = null; 94 | 95 | // TODO: we could do this with style calc 96 | var sideStyle = { 97 | bottom: 0, 98 | left: this.props.sideWidth * -1, 99 | position: 'absolute', 100 | top: 0, 101 | width: this.props.sideWidth 102 | }; 103 | 104 | var behavior = this.props.behavior; 105 | 106 | if (this.isNavOpen()) { 107 | side = ( 108 | 113 | {this.props.sideContent} 114 | 115 | ); 116 | } 117 | 118 | var contentTouchableAreaStyle = { 119 | bottom: 0, 120 | left: 0, 121 | position: 'absolute', 122 | right: 0, 123 | top: 0 124 | }; 125 | 126 | var topStyle = { 127 | height: this.props.topHeight, 128 | left: 0, 129 | position: 'absolute', 130 | right: 0, 131 | top: 0 132 | }; 133 | 134 | var contentStyle = { 135 | bottom: 0, 136 | left: 0, 137 | position: 'absolute', 138 | right: 0, 139 | top: this.props.topHeight 140 | }; 141 | 142 | return this.transferPropsTo( 143 |
144 | {side} 145 | 150 | 155 | {this.props.children} 156 | 157 | 158 | 163 | 166 | {this.props.button} 167 | 168 | {this.props.topContent} 169 | 170 |
171 | ); 172 | } 173 | }); 174 | 175 | module.exports = LeftNavContainer; -------------------------------------------------------------------------------- /src/interactions/simplescroller/SimpleScroller.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | var React = require('react'); 4 | 5 | var AnimatableContainer = require('../../primitives/AnimatableContainer'); 6 | var TouchableArea = require('../../primitives/TouchableArea'); 7 | var ZyngaScroller = require('../../environment/ZyngaScroller'); 8 | 9 | var ANIMATABLE_CONTAINER_STYLE = { 10 | bottom: 0, 11 | left: 0, 12 | position: 'absolute', 13 | right: 0, 14 | top: 0 15 | }; 16 | 17 | var SimpleScroller = React.createClass({ 18 | getInitialState: function() { 19 | return {left: 0, top: 0}; 20 | }, 21 | 22 | componentWillMount: function() { 23 | this.scroller = new Scroller(this.handleScroll, this.props.options); 24 | this.configured = false; 25 | }, 26 | 27 | componentDidMount: function() { 28 | this.configure(); 29 | }, 30 | 31 | componentDidUpdate: function() { 32 | this.configure(); 33 | }, 34 | 35 | configure: function() { 36 | if (this.configured) { 37 | return; 38 | } 39 | this.configured = true; 40 | var node = this.refs.content.getDOMNode(); 41 | this.scroller.setDimensions( 42 | this.getDOMNode().clientWidth, 43 | this.getDOMNode().clientHeight, 44 | node.clientWidth, 45 | node.clientHeight 46 | ); 47 | }, 48 | 49 | handleScroll: function(left, top) { 50 | // TODO: zoom 51 | this.setState({ 52 | left: left, 53 | top: top 54 | }); 55 | }, 56 | 57 | render: function() { 58 | return this.transferPropsTo( 59 | 60 | 63 |
{this.props.children}
64 |
65 |
66 | ); 67 | } 68 | }); 69 | 70 | module.exports = SimpleScroller; -------------------------------------------------------------------------------- /src/primitives/AnimatableContainer.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | var React = require('react'); 4 | 5 | var StaticContainer = require('./helpers/StaticContainer'); 6 | var StyleKeys = require('../environment/StyleKeys'); 7 | 8 | var POLL_FACTOR = .5; 9 | 10 | var AnimatableContainer = React.createClass({ 11 | getDefaultProps: function() { 12 | return { 13 | blockUpdates: true, 14 | component: React.DOM.div, 15 | contentComponent: React.DOM.span, 16 | opacity: 1, 17 | rotate: null, 18 | scale: null, 19 | timeout: 200, 20 | translate: null 21 | }; 22 | }, 23 | 24 | componentWillMount: function() { 25 | this.wasEverOnGPU = false; 26 | this.isAnimating = false; 27 | this.lastAnimationTime = 0; 28 | this.animationInterval = null; 29 | }, 30 | 31 | componentWillUnmount: function() { 32 | if (this.animationInterval) { 33 | window.clearInterval(this.animationInterval); 34 | } 35 | }, 36 | 37 | componentWillReceiveProps: function(nextProps) { 38 | var prevStyle = this.getStyle(this.props); 39 | var style = this.getStyle(nextProps); 40 | 41 | this.isAnimating = ( 42 | style['opacity'] !== prevStyle.opacity || 43 | style[StyleKeys.TRANSFORM] !== prevStyle[StyleKeys.TRANSFORM] 44 | ); 45 | 46 | if (this.isAnimating) { 47 | this.lastAnimationTime = Date.now(); 48 | if (this.props.timeout && !this.animationInterval) { 49 | this.animationInterval = window.setInterval( 50 | this.checkAnimationEnd, 51 | this.props.timeout * POLL_FACTOR 52 | ); 53 | } 54 | } 55 | }, 56 | 57 | checkAnimationEnd: function() { 58 | if (Date.now() - this.lastAnimationTime > this.props.timeout) { 59 | window.clearInterval(this.animationInterval); 60 | this.animationInterval = null; 61 | this.isAnimating = false; 62 | this.forceUpdate(); 63 | } 64 | }, 65 | 66 | getStyle: function(props) { 67 | var style = {}; 68 | 69 | if (this.props.style) { 70 | for (var key in this.props.style) { 71 | style[key] = this.props.style[key]; 72 | } 73 | } 74 | 75 | var transforms = ''; 76 | 77 | if (props.opacity !== 1) { 78 | style['opacity'] = props.opacity; 79 | } 80 | 81 | if (props.translate) { 82 | transforms += ( 83 | 'translate3d(' + (props.translate.x || 0) + 'px, ' + 84 | (props.translate.y || 0) + 'px, ' + 85 | (props.translate.z || 0) + 'px) ' 86 | ); 87 | } 88 | 89 | if (props.rotate) { 90 | transforms += ( 91 | 'rotate3d(' + (props.rotate.x || 0) + ', ' + 92 | (props.rotate.y || 0) + ', ' + 93 | (props.rotate.z || 0) + ', ' + 94 | props.rotate.deg + 'deg) ' 95 | ); 96 | } 97 | 98 | if (props.scale) { 99 | transforms += 'scale(' + props.scale + ') '; 100 | } 101 | 102 | if (transforms.length > 0) { 103 | style[StyleKeys.TRANSFORM] = transforms; 104 | this.wasEverOnGPU = true; 105 | } else { 106 | if (this.wasEverOnGPU) { 107 | // on iOS when you go from translate3d to non-translate3d you get 108 | // flicker. Let's avoid it 109 | style[StyleKeys.TRANSFORM] = 'translate3d(0, 0, 0)'; 110 | } 111 | } 112 | 113 | return style; 114 | }, 115 | 116 | render: function() { 117 | var component = this.props.component; 118 | var contentComponent = this.props.contentComponent; 119 | 120 | return ( 121 | 124 | 125 | 126 | {this.props.children} 127 | 128 | 129 | 130 | ); 131 | } 132 | }); 133 | 134 | module.exports = AnimatableContainer; -------------------------------------------------------------------------------- /src/primitives/App.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | var React = require('react'); 4 | 5 | var STYLE = { 6 | bottom: 0, 7 | left: 0, 8 | overflow: 'hidden', 9 | position: 'fixed', 10 | right: 0, 11 | top: 0 12 | }; 13 | 14 | var App = React.createClass({ 15 | handleTouch: function(e) { 16 | e.preventDefault(); 17 | }, 18 | 19 | render: function() { 20 | return this.transferPropsTo( 21 |
22 | {this.props.children} 23 |
24 | ); 25 | } 26 | }); 27 | 28 | module.exports = App; -------------------------------------------------------------------------------- /src/primitives/TouchableArea.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | var React = require('react'); 4 | 5 | var TouchableArea = React.createClass({ 6 | getDefaultProps: function() { 7 | return { 8 | component: React.DOM.div, 9 | touchable: true 10 | }; 11 | }, 12 | 13 | handleTouchStart: function(e) { 14 | if (!this.props.scroller || !this.props.touchable) { 15 | return; 16 | } 17 | 18 | this.props.scroller.doTouchStart(e.touches, e.timeStamp); 19 | e.preventDefault(); 20 | }, 21 | 22 | handleTouchMove: function(e) { 23 | if (!this.props.scroller || !this.props.touchable) { 24 | return; 25 | } 26 | 27 | this.props.scroller.doTouchMove(e.touches, e.timeStamp, e.scale); 28 | e.preventDefault(); 29 | }, 30 | 31 | handleTouchEnd: function(e) { 32 | if (!this.props.scroller || !this.props.touchable) { 33 | return; 34 | } 35 | 36 | this.props.scroller.doTouchEnd(e.timeStamp); 37 | e.preventDefault(); 38 | }, 39 | 40 | render: function() { 41 | var component = this.props.component; 42 | return this.transferPropsTo( 43 | 48 | {this.props.children} 49 | 50 | ); 51 | } 52 | }); 53 | 54 | module.exports = TouchableArea; -------------------------------------------------------------------------------- /src/primitives/helpers/StaticContainer.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | var React = require('react'); 4 | 5 | var StaticContainer = React.createClass({ 6 | getDefaultProps: function() { 7 | return {shouldUpdate: false}; 8 | }, 9 | 10 | shouldComponentUpdate: function(nextProps) { 11 | return nextProps.shouldUpdate || (this.props.staticKey !== nextProps.staticKey); 12 | }, 13 | 14 | render: function() { 15 | return this.props.children; 16 | } 17 | }); 18 | 19 | module.exports = StaticContainer; -------------------------------------------------------------------------------- /src/routing/RoutedLink.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | var React = require('react'); 4 | 5 | var Router = require('./Router'); 6 | 7 | var RoutedLink = React.createClass({ 8 | handleTap: function(e) { 9 | Router.trigger(this.props.href); 10 | 11 | if (this.props.onClick) { 12 | this.props.onClick(e); 13 | } 14 | e.preventDefault(); 15 | }, 16 | 17 | render: function() { 18 | var linkProps = {}; 19 | for (var key in this.props) { 20 | linkProps[key] = this.props[key]; 21 | } 22 | linkProps.href = 'javascript:;'; 23 | linkProps.onTouchTap = this.handleTap; 24 | return React.DOM.a(linkProps, this.props.children); 25 | } 26 | }); 27 | 28 | module.exports = RoutedLink; -------------------------------------------------------------------------------- /src/routing/Router.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | var componentClass = null; 4 | var domNode = null; 5 | var routes = null; 6 | var historyRoot = null; 7 | 8 | function getComponentForRoute(route) { 9 | for (var regexSource in routes) { 10 | var regex = new RegExp(regexSource); 11 | var result = regex.exec(route); 12 | 13 | if (result) { 14 | return componentClass({ 15 | routeName: routes[regexSource], 16 | routeParams: result 17 | }); 18 | break; 19 | } 20 | } 21 | 22 | return React.DOM.span(null, 'ReactHack: 404 not found'); 23 | } 24 | 25 | function getCurrentRouteOnClient() { 26 | if (historyRoot) { 27 | return window.location.pathname; 28 | } else { 29 | var fragment = window.location.hash.slice(1); 30 | if (fragment.length === 0) { 31 | fragment = '/'; 32 | } 33 | return fragment; 34 | } 35 | } 36 | 37 | function renderRouteOnClient() { 38 | React.renderComponent( 39 | getComponentForRoute(getCurrentRouteOnClient()), 40 | domNode 41 | ); 42 | } 43 | 44 | var Router = { 45 | start: function(componentClass_, domNode_, routes_, historyRoot_) { 46 | if (componentClass) { 47 | throw new Error('Already started Router'); 48 | } 49 | 50 | componentClass = componentClass_; 51 | domNode = domNode_; 52 | routes = routes_; 53 | historyRoot = window.history && historyRoot_; 54 | 55 | if (historyRoot) { 56 | window.addEventListener('popstate', renderRouteOnClient, false); 57 | 58 | // If we got a hash-based URL and we want to use history API 59 | // do a redirect. 60 | if (window.location.hash.length > 0) { 61 | var redirectRoute = window.location.hash; 62 | window.location.hash = ''; 63 | Router.trigger(redirectRoute.slice(1)); 64 | } else { 65 | renderRouteOnClient(); 66 | } 67 | } else { 68 | window.addEventListener('hashchange', renderRouteOnClient, false); 69 | 70 | // If we got a history-based URL and we want to use hash routing 71 | // do a redirect. 72 | if (window.location.pathname.indexOf(historyRoot) === 0 && window.location.hash.length === 0) { 73 | Router.trigger(window.location.pathname.slice(historyRoot.length)); 74 | } else { 75 | renderRouteOnClient(); 76 | } 77 | } 78 | }, 79 | 80 | trigger: function(route) { 81 | if (route.length === 0 || route[0] !== '/') { 82 | throw new Error('trigger() takes an absolute path'); 83 | } 84 | 85 | if (historyRoot) { 86 | window.history.pushState({}, document.title, route); 87 | } else { 88 | window.location.hash = route; 89 | } 90 | renderRouteOnClient(); 91 | }, 92 | 93 | getMarkupForRoute: function(route, cb) { 94 | React.renderComponentToString( 95 | getComponentForRoute(route), 96 | cb 97 | ); 98 | } 99 | }; 100 | 101 | module.exports = Router; -------------------------------------------------------------------------------- /src/thirdparty/ResponderEventPlugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013 Facebook, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @providesModule ResponderEventPlugin 17 | */ 18 | 19 | "use strict"; 20 | 21 | var EventConstants = require('react/lib/EventConstants'); 22 | var EventPluginUtils = require('react/lib/EventPluginUtils'); 23 | var EventPropagators = require('react/lib/EventPropagators'); 24 | var SyntheticEvent = require('react/lib/SyntheticEvent'); 25 | 26 | var accumulate = require('react/lib/accumulate'); 27 | var keyOf = require('react/lib/keyOf'); 28 | 29 | var isStartish = EventPluginUtils.isStartish; 30 | var isMoveish = EventPluginUtils.isMoveish; 31 | var isEndish = EventPluginUtils.isEndish; 32 | var executeDirectDispatch = EventPluginUtils.executeDirectDispatch; 33 | var hasDispatches = EventPluginUtils.hasDispatches; 34 | var executeDispatchesInOrderStopAtTrue = 35 | EventPluginUtils.executeDispatchesInOrderStopAtTrue; 36 | 37 | /** 38 | * ID of element that should respond to touch/move types of interactions, as 39 | * indicated explicitly by relevant callbacks. 40 | */ 41 | var responderID = null; 42 | var isPressing = false; 43 | 44 | var eventTypes = { 45 | /** 46 | * On a `touchStart`/`mouseDown`, is it desired that this element become the 47 | * responder? 48 | */ 49 | startShouldSetResponder: { 50 | phasedRegistrationNames: { 51 | bubbled: keyOf({onStartShouldSetResponder: null}), 52 | captured: keyOf({onStartShouldSetResponderCapture: null}) 53 | } 54 | }, 55 | 56 | /** 57 | * On a `scroll`, is it desired that this element become the responder? This 58 | * is usually not needed, but should be used to retroactively infer that a 59 | * `touchStart` had occured during momentum scroll. During a momentum scroll, 60 | * a touch start will be immediately followed by a scroll event if the view is 61 | * currently scrolling. 62 | */ 63 | scrollShouldSetResponder: { 64 | phasedRegistrationNames: { 65 | bubbled: keyOf({onScrollShouldSetResponder: null}), 66 | captured: keyOf({onScrollShouldSetResponderCapture: null}) 67 | } 68 | }, 69 | 70 | /** 71 | * On a `touchMove`/`mouseMove`, is it desired that this element become the 72 | * responder? 73 | */ 74 | moveShouldSetResponder: { 75 | phasedRegistrationNames: { 76 | bubbled: keyOf({onMoveShouldSetResponder: null}), 77 | captured: keyOf({onMoveShouldSetResponderCapture: null}) 78 | } 79 | }, 80 | 81 | /** 82 | * Direct responder events dispatched directly to responder. Do not bubble. 83 | */ 84 | responderMove: {registrationName: keyOf({onResponderMove: null})}, 85 | responderRelease: {registrationName: keyOf({onResponderRelease: null})}, 86 | responderTerminationRequest: { 87 | registrationName: keyOf({onResponderTerminationRequest: null}) 88 | }, 89 | responderGrant: {registrationName: keyOf({onResponderGrant: null})}, 90 | responderReject: {registrationName: keyOf({onResponderReject: null})}, 91 | responderTerminate: {registrationName: keyOf({onResponderTerminate: null})} 92 | }; 93 | 94 | /** 95 | * Performs negotiation between any existing/current responder, checks to see if 96 | * any new entity is interested in becoming responder, performs that handshake 97 | * and returns any events that must be emitted to notify the relevant parties. 98 | * 99 | * A note about event ordering in the `EventPluginHub`. 100 | * 101 | * Suppose plugins are injected in the following order: 102 | * 103 | * `[R, S, C]` 104 | * 105 | * To help illustrate the example, assume `S` is `SimpleEventPlugin` (for 106 | * `onClick` etc) and `R` is `ResponderEventPlugin`. 107 | * 108 | * "Deferred-Dispatched Events": 109 | * 110 | * - The current event plugin system will traverse the list of injected plugins, 111 | * in order, and extract events by collecting the plugin's return value of 112 | * `extractEvents()`. 113 | * - These events that are returned from `extractEvents` are "deferred 114 | * dispatched events". 115 | * - When returned from `extractEvents`, deferred-dispatched events contain an 116 | * "accumulation" of deferred dispatches. 117 | * - These deferred dispatches are accumulated/collected before they are 118 | * returned, but processed at a later time by the `EventPluginHub` (hence the 119 | * name deferred). 120 | * 121 | * In the process of returning their deferred-dispatched events, event plugins 122 | * themselves can dispatch events on-demand without returning them from 123 | * `extractEvents`. Plugins might want to do this, so that they can use event 124 | * dispatching as a tool that helps them decide which events should be extracted 125 | * in the first place. 126 | * 127 | * "On-Demand-Dispatched Events": 128 | * 129 | * - On-demand-dispatched events are not returned from `extractEvents`. 130 | * - On-demand-dispatched events are dispatched during the process of returning 131 | * the deferred-dispatched events. 132 | * - They should not have side effects. 133 | * - They should be avoided, and/or eventually be replaced with another 134 | * abstraction that allows event plugins to perform multiple "rounds" of event 135 | * extraction. 136 | * 137 | * Therefore, the sequence of event dispatches becomes: 138 | * 139 | * - `R`s on-demand events (if any) (dispatched by `R` on-demand) 140 | * - `S`s on-demand events (if any) (dispatched by `S` on-demand) 141 | * - `C`s on-demand events (if any) (dispatched by `C` on-demand) 142 | * - `R`s extracted events (if any) (dispatched by `EventPluginHub`) 143 | * - `S`s extracted events (if any) (dispatched by `EventPluginHub`) 144 | * - `C`s extracted events (if any) (dispatched by `EventPluginHub`) 145 | * 146 | * In the case of `ResponderEventPlugin`: If the `startShouldSetResponder` 147 | * on-demand dispatch returns `true` (and some other details are satisfied) the 148 | * `onResponderGrant` deferred dispatched event is returned from 149 | * `extractEvents`. The sequence of dispatch executions in this case 150 | * will appear as follows: 151 | * 152 | * - `startShouldSetResponder` (`ResponderEventPlugin` dispatches on-demand) 153 | * - `touchStartCapture` (`EventPluginHub` dispatches as usual) 154 | * - `touchStart` (`EventPluginHub` dispatches as usual) 155 | * - `responderGrant/Reject` (`EventPluginHub` dispatches as usual) 156 | * 157 | * @param {string} topLevelType Record from `EventConstants`. 158 | * @param {string} topLevelTargetID ID of deepest React rendered element. 159 | * @param {object} nativeEvent Native browser event. 160 | * @return {*} An accumulation of synthetic events. 161 | */ 162 | function setResponderAndExtractTransfer( 163 | topLevelType, 164 | topLevelTargetID, 165 | nativeEvent) { 166 | var shouldSetEventType = 167 | isStartish(topLevelType) ? eventTypes.startShouldSetResponder : 168 | isMoveish(topLevelType) ? eventTypes.moveShouldSetResponder : 169 | eventTypes.scrollShouldSetResponder; 170 | 171 | var bubbleShouldSetFrom = responderID || topLevelTargetID; 172 | var shouldSetEvent = SyntheticEvent.getPooled( 173 | shouldSetEventType, 174 | bubbleShouldSetFrom, 175 | nativeEvent 176 | ); 177 | EventPropagators.accumulateTwoPhaseDispatches(shouldSetEvent); 178 | var wantsResponderID = executeDispatchesInOrderStopAtTrue(shouldSetEvent); 179 | if (!shouldSetEvent.isPersistent()) { 180 | shouldSetEvent.constructor.release(shouldSetEvent); 181 | } 182 | 183 | if (!wantsResponderID || wantsResponderID === responderID) { 184 | return null; 185 | } 186 | var extracted; 187 | var grantEvent = SyntheticEvent.getPooled( 188 | eventTypes.responderGrant, 189 | wantsResponderID, 190 | nativeEvent 191 | ); 192 | 193 | EventPropagators.accumulateDirectDispatches(grantEvent); 194 | if (responderID) { 195 | var terminationRequestEvent = SyntheticEvent.getPooled( 196 | eventTypes.responderTerminationRequest, 197 | responderID, 198 | nativeEvent 199 | ); 200 | EventPropagators.accumulateDirectDispatches(terminationRequestEvent); 201 | var shouldSwitch = !hasDispatches(terminationRequestEvent) || 202 | executeDirectDispatch(terminationRequestEvent); 203 | if (!terminationRequestEvent.isPersistent()) { 204 | terminationRequestEvent.constructor.release(terminationRequestEvent); 205 | } 206 | 207 | if (shouldSwitch) { 208 | var terminateType = eventTypes.responderTerminate; 209 | var terminateEvent = SyntheticEvent.getPooled( 210 | terminateType, 211 | responderID, 212 | nativeEvent 213 | ); 214 | EventPropagators.accumulateDirectDispatches(terminateEvent); 215 | extracted = accumulate(extracted, [grantEvent, terminateEvent]); 216 | responderID = wantsResponderID; 217 | } else { 218 | var rejectEvent = SyntheticEvent.getPooled( 219 | eventTypes.responderReject, 220 | wantsResponderID, 221 | nativeEvent 222 | ); 223 | EventPropagators.accumulateDirectDispatches(rejectEvent); 224 | extracted = accumulate(extracted, rejectEvent); 225 | } 226 | } else { 227 | extracted = accumulate(extracted, grantEvent); 228 | responderID = wantsResponderID; 229 | } 230 | return extracted; 231 | } 232 | 233 | /** 234 | * A transfer is a negotiation between a currently set responder and the next 235 | * element to claim responder status. Any start event could trigger a transfer 236 | * of responderID. Any move event could trigger a transfer, so long as there is 237 | * currently a responder set (in other words as long as the user is pressing 238 | * down). 239 | * 240 | * @param {string} topLevelType Record from `EventConstants`. 241 | * @return {boolean} True if a transfer of responder could possibly occur. 242 | */ 243 | function canTriggerTransfer(topLevelType) { 244 | return topLevelType === EventConstants.topLevelTypes.topScroll || 245 | isStartish(topLevelType) || 246 | (isPressing && isMoveish(topLevelType)); 247 | } 248 | 249 | /** 250 | * Event plugin for formalizing the negotiation between claiming locks on 251 | * receiving touches. 252 | */ 253 | var ResponderEventPlugin = { 254 | 255 | getResponderID: function() { 256 | return responderID; 257 | }, 258 | 259 | eventTypes: eventTypes, 260 | 261 | /** 262 | * @param {string} topLevelType Record from `EventConstants`. 263 | * @param {DOMEventTarget} topLevelTarget The listening component root node. 264 | * @param {string} topLevelTargetID ID of `topLevelTarget`. 265 | * @param {object} nativeEvent Native browser event. 266 | * @return {*} An accumulation of synthetic events. 267 | * @see {EventPluginHub.extractEvents} 268 | */ 269 | extractEvents: function( 270 | topLevelType, 271 | topLevelTarget, 272 | topLevelTargetID, 273 | nativeEvent) { 274 | var extracted; 275 | // Must have missed an end event - reset the state here. 276 | if (responderID && isStartish(topLevelType)) { 277 | responderID = null; 278 | } 279 | if (isStartish(topLevelType)) { 280 | isPressing = true; 281 | } else if (isEndish(topLevelType)) { 282 | isPressing = false; 283 | } 284 | if (canTriggerTransfer(topLevelType)) { 285 | var transfer = setResponderAndExtractTransfer( 286 | topLevelType, 287 | topLevelTargetID, 288 | nativeEvent 289 | ); 290 | if (transfer) { 291 | extracted = accumulate(extracted, transfer); 292 | } 293 | } 294 | // Now that we know the responder is set correctly, we can dispatch 295 | // responder type events (directly to the responder). 296 | var type = isMoveish(topLevelType) ? eventTypes.responderMove : 297 | isEndish(topLevelType) ? eventTypes.responderRelease : 298 | isStartish(topLevelType) ? eventTypes.responderStart : null; 299 | if (type) { 300 | var gesture = SyntheticEvent.getPooled( 301 | type, 302 | responderID || '', 303 | nativeEvent 304 | ); 305 | EventPropagators.accumulateDirectDispatches(gesture); 306 | extracted = accumulate(extracted, gesture); 307 | } 308 | if (type === eventTypes.responderRelease) { 309 | responderID = null; 310 | } 311 | return extracted; 312 | } 313 | 314 | }; 315 | 316 | module.exports = ResponderEventPlugin; 317 | -------------------------------------------------------------------------------- /src/thirdparty/TapEventPlugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013 Facebook, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @providesModule TapEventPlugin 17 | * @typechecks static-only 18 | */ 19 | 20 | "use strict"; 21 | 22 | var EventConstants = require('react/lib/EventConstants'); 23 | var EventPluginUtils = require('react/lib/EventPluginUtils'); 24 | var EventPropagators = require('react/lib/EventPropagators'); 25 | var SyntheticUIEvent = require('react/lib/SyntheticUIEvent'); 26 | var TouchEventUtils = require('./TouchEventUtils'); 27 | var ViewportMetrics = require('react/lib/ViewportMetrics'); 28 | 29 | var keyOf = require('react/lib/keyOf'); 30 | var topLevelTypes = EventConstants.topLevelTypes; 31 | 32 | var isStartish = EventPluginUtils.isStartish; 33 | var isEndish = EventPluginUtils.isEndish; 34 | 35 | /** 36 | * Number of pixels that are tolerated in between a `touchStart` and `touchEnd` 37 | * in order to still be considered a 'tap' event. 38 | */ 39 | var tapMoveThreshold = 10; 40 | var startCoords = {x: null, y: null}; 41 | 42 | var Axis = { 43 | x: {page: 'pageX', client: 'clientX', envScroll: 'currentPageScrollLeft'}, 44 | y: {page: 'pageY', client: 'clientY', envScroll: 'currentPageScrollTop'} 45 | }; 46 | 47 | function getAxisCoordOfEvent(axis, nativeEvent) { 48 | var singleTouch = TouchEventUtils.extractSingleTouch(nativeEvent); 49 | if (singleTouch) { 50 | return singleTouch[axis.page]; 51 | } 52 | return axis.page in nativeEvent ? 53 | nativeEvent[axis.page] : 54 | nativeEvent[axis.client] + ViewportMetrics[axis.envScroll]; 55 | } 56 | 57 | function getDistance(coords, nativeEvent) { 58 | var pageX = getAxisCoordOfEvent(Axis.x, nativeEvent); 59 | var pageY = getAxisCoordOfEvent(Axis.y, nativeEvent); 60 | return Math.pow( 61 | Math.pow(pageX - coords.x, 2) + Math.pow(pageY - coords.y, 2), 62 | 0.5 63 | ); 64 | } 65 | 66 | var dependencies = [ 67 | topLevelTypes.topMouseDown, 68 | topLevelTypes.topMouseMove, 69 | topLevelTypes.topMouseUp 70 | ]; 71 | 72 | if (EventPluginUtils.useTouchEvents) { 73 | dependencies.push( 74 | topLevelTypes.topTouchCancel, 75 | topLevelTypes.topTouchEnd, 76 | topLevelTypes.topTouchStart, 77 | topLevelTypes.topTouchMove 78 | ); 79 | } 80 | 81 | var eventTypes = { 82 | touchTap: { 83 | phasedRegistrationNames: { 84 | bubbled: keyOf({onTouchTap: null}), 85 | captured: keyOf({onTouchTapCapture: null}) 86 | }, 87 | dependencies: dependencies 88 | } 89 | }; 90 | 91 | var TapEventPlugin = { 92 | 93 | tapMoveThreshold: tapMoveThreshold, 94 | 95 | eventTypes: eventTypes, 96 | 97 | /** 98 | * @param {string} topLevelType Record from `EventConstants`. 99 | * @param {DOMEventTarget} topLevelTarget The listening component root node. 100 | * @param {string} topLevelTargetID ID of `topLevelTarget`. 101 | * @param {object} nativeEvent Native browser event. 102 | * @return {*} An accumulation of synthetic events. 103 | * @see {EventPluginHub.extractEvents} 104 | */ 105 | extractEvents: function( 106 | topLevelType, 107 | topLevelTarget, 108 | topLevelTargetID, 109 | nativeEvent) { 110 | if (!isStartish(topLevelType) && !isEndish(topLevelType)) { 111 | return null; 112 | } 113 | var event = null; 114 | var distance = getDistance(startCoords, nativeEvent); 115 | if (isEndish(topLevelType) && distance < tapMoveThreshold) { 116 | event = SyntheticUIEvent.getPooled( 117 | eventTypes.touchTap, 118 | topLevelTargetID, 119 | nativeEvent 120 | ); 121 | } 122 | if (isStartish(topLevelType)) { 123 | startCoords.x = getAxisCoordOfEvent(Axis.x, nativeEvent); 124 | startCoords.y = getAxisCoordOfEvent(Axis.y, nativeEvent); 125 | } else if (isEndish(topLevelType)) { 126 | startCoords.x = 0; 127 | startCoords.y = 0; 128 | } 129 | EventPropagators.accumulateTwoPhaseDispatches(event); 130 | return event; 131 | } 132 | 133 | }; 134 | 135 | module.exports = TapEventPlugin; 136 | -------------------------------------------------------------------------------- /src/thirdparty/TouchEventUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013 Facebook, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @providesModule TouchEventUtils 17 | */ 18 | 19 | var TouchEventUtils = { 20 | /** 21 | * Utility function for common case of extracting out the primary touch from a 22 | * touch event. 23 | * - `touchEnd` events usually do not have the `touches` property. 24 | * http://stackoverflow.com/questions/3666929/ 25 | * mobile-sarai-touchend-event-not-firing-when-last-touch-is-removed 26 | * 27 | * @param {Event} nativeEvent Native event that may or may not be a touch. 28 | * @return {TouchesObject?} an object with pageX and pageY or null. 29 | */ 30 | extractSingleTouch: function(nativeEvent) { 31 | var touches = nativeEvent.touches; 32 | var changedTouches = nativeEvent.changedTouches; 33 | var hasTouches = touches && touches.length > 0; 34 | var hasChangedTouches = changedTouches && changedTouches.length > 0; 35 | 36 | return !hasTouches && hasChangedTouches ? changedTouches[0] : 37 | hasTouches ? touches[0] : 38 | nativeEvent; 39 | } 40 | }; 41 | 42 | module.exports = TouchEventUtils; 43 | --------------------------------------------------------------------------------