├── .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 | 
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 |
--------------------------------------------------------------------------------