├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── CHANGES.md ├── LICENSE ├── README.md ├── bower.json ├── dist ├── react-tappable.js └── react-tappable.min.js ├── example ├── dist │ ├── bundle.js │ ├── common.js │ ├── example.css │ ├── example.js │ ├── index.html │ ├── pinch.html │ └── pinch.js └── src │ ├── example.js │ ├── example.less │ ├── index.html │ ├── pinch.html │ └── pinch.js ├── gulpfile.js ├── lib ├── Pinchable.js ├── PinchableBaseMixin.js ├── PinchableMixin.js ├── TapAndPinchable.js ├── Tappable.js ├── TappableMixin.js ├── getComponent.js └── touchStyles.js ├── package.json └── src ├── Pinchable.js ├── PinchableBaseMixin.js ├── PinchableMixin.js ├── TapAndPinchable.js ├── Tappable.js ├── TappableMixin.js ├── getComponent.js └── touchStyles.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | indent_style = tab 11 | 12 | [*.json] 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.yml] 17 | indent_style = space 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/* 2 | lib/* 3 | example/dist/* 4 | node_modules/* 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true, 5 | "node": true 6 | }, 7 | "plugins": [ 8 | "react" 9 | ], 10 | "rules": { 11 | "curly": [2, "multi-line"], 12 | "no-shadow": 0, 13 | "no-underscore-dangle": 0, 14 | "no-unused-expressions": 0, 15 | "quotes": [2, "single", "avoid-escape"], 16 | "react/jsx-uses-react": 1, 17 | "react/jsx-uses-vars": 1, 18 | "react/react-in-jsx-scope": 1, 19 | "semi": 2, 20 | "strict": 0 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Coverage tools 11 | lib-cov 12 | coverage 13 | 14 | # Compiled binary addons (http://nodejs.org/api/addons.html) 15 | build/Release 16 | 17 | # Dependency directory 18 | node_modules 19 | 20 | # Deploy directory 21 | .publish 22 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | bower.json 2 | dist/ 3 | example 4 | gulpfile.js 5 | src/ 6 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # React-Tappable Changelog 2 | 3 | ## v0.8.4 / 2016-08-24 4 | 5 | * fixed; Actually include React 15.2+ compatability patches this time! 6 | 7 | ## v0.8.3 / 2016-08-07 8 | 9 | * fixed; React 15.2+ compatiblity patches from the last version were lost during build, correctly included in this version 10 | 11 | ## v0.8.2 / 2016-07-30 12 | 13 | * fixed; warnings from React 15.2+ about invalid dom attributes have been resolved, thanks [Olivier Tassinari](https://github.com/oliviertassinari) 14 | * fixed; pinch angle calculations have been fixed, thanks [Yusuke Shibata](https://github.com/yusukeshibata) 15 | * fixed; `detectScroll()` didn't work properly on Android, thanks [Fangzhou Li](https://github.com/riophae) 16 | * fixed; `ReactDOM` global is now used in the `dist` build 17 | 18 | ## v0.8.1 / 2016-03-20 19 | 20 | Updated to allow compatiblity with React 15.x 21 | 22 | ## v0.8.0 / 2015-12-28 23 | 24 | Tappable now supports keyboard events; a `keyDown` event with the `space` or `enter` keys followed by a `keyUp` event will fire the `onTap` handler. 25 | 26 | New props `onKeyDown` and `onKeyUp` have also been added; return `false` from `onKeyDown` to prevent event handling. 27 | 28 | Thanks to [Will Binns-Smith](https://github.com/wbinnssmith) for this update. 29 | 30 | ## v0.7.2 / 2015-12-13 31 | 32 | * added; new `classes` prop (`Object`) adds support for defining the complete className applied with the component is `active` or `inactive`. Handy for use with [css-modules](https://github.com/css-modules/css-modules), thanks [Rudin Swagerman](https://github.com/rudin). 33 | 34 | ## v0.7.1 / 2015-10-16 35 | 36 | * fixed; use `react-dom` for `findDOMNode`, thanks [Daniel Cousens](https://github.com/dcousens) 37 | 38 | ## v0.7.0 / 2015-10-13 39 | 40 | Tappable is updated for React 0.14. If you're still using React 0.13, please continue to use `react-tappable@0.6.x`. There are no functional differences between v0.7.0 and v0.6.0. 41 | 42 | ## v0.6.0 / 2015-07-31 43 | 44 | This release contains a major refactor that makes `react-tappable` more modular, thanks to [Naman Goel](https://github.com/nmn) 45 | 46 | You can now use _just_ the `Tappable` component, or choose to use the `TapAndPinchable` (default export). Instructions will be added to the Readme with more information soon. 47 | 48 | ## v0.5.7 / 2015-07-30 49 | 50 | * fixed; removed former hacky attempts to handle the React eventpooling problem 51 | * added; support for React `0.14.0-beta1` 52 | 53 | ## v0.5.6 / 2015-07-29 54 | 55 | * fixed; regression introduced in `v0.5.5` where errors would occur in certain conditions 56 | 57 | ## v0.5.5 / 2015-07-29 58 | 59 | * fixed; `afterEndTouch` is now called synchronously, which means the SyntheticTouch event behaves as expected. See [#39](https://github.com/JedWatson/react-tappable/issues/39) and [#47](https://github.com/JedWatson/react-tappable/pull/47) for more information. 60 | 61 | ## v0.5.4 / 2015-07-25 62 | 63 | * fixed; removed `React.initializeTouchEvents`, no longer needed and breaks in React 0.14 64 | 65 | ## v0.5.3 / 2015-07-24 66 | 67 | * fixed; `preventDefault` issue on iOS 68 | 69 | ## v0.5.2 / 2015-06-23 70 | 71 | * Added `activeDelay` prop, delays adding the `-active` class by the provided milliseconds for situations when you don't want to hilight a tap immediately (e.g. iOS Scrollable Lists) 72 | 73 | ## v0.5.1 / 2015-06-17 74 | 75 | * Fixed issue where halting momentum scrolling would incorrectly fire a tap event 76 | * `onTap` now fires after the tappable's `setState` is complete, resolves some animation edge-case issues 77 | 78 | ## v0.5.0 / 2015-06-16 79 | 80 | * Using Babel's polyfill for Object.assign 81 | * `lib` build (via Babel) is provided for use without further transpilation 82 | * `preventDefault` is called to clock the click event firing after a touch has been detected 83 | * React has been changed to a dev/peerDependency 84 | * Added pinch events - `onPinchStart`, `onPinchMove`, `onPinchEnd` 85 | * Older single touch based events don't fire when dealing with multi-touch 86 | * Refactored the way props are passed to component. You can now pass in custom properties for the target component that are not meant for React-Tappable 87 | 88 | ## v0.4.0 / 2015-03-12 89 | 90 | ### Updated 91 | 92 | - Now works with React 0.13, backwards compatible with 0.12 93 | 94 | ## v0.3.3 / 2015-02-19 95 | 96 | ### Added 97 | 98 | - Support for `data-` and `aria-` props on the Component, thanks [Tom Hicks](https://github.com/tomhicks-bsf) 99 | 100 | ## v0.3.2 / 2015-02-19 101 | 102 | ### Fixed 103 | 104 | - Cleanup around removal of Reactify, build-examples is working again 105 | 106 | ## v0.3.1 / 2015-02-18 107 | 108 | ### Fixed 109 | 110 | - Reactify is no longer included as a Browserify transform, thanks [Naman Goel](https://github.com/nmn) 111 | 112 | ## v0.3.0 / 2015-02-07 113 | 114 | This release restructured the code so that most methods are now on a Mixin, which is used by the Component (`module.exports`); 115 | 116 | You can now mix `react-tappable` into your own Components by using the Mixin directly: 117 | 118 | ``` 119 | var Tappable = require('react-tappable'); 120 | 121 | var MyComponent = React.createComponent({ 122 | mixins: [Tappable.Mixin], 123 | /* ... */ 124 | }); 125 | ``` 126 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jed Watson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | React-Tappable 2 | ============== 3 | 4 | Tappable component for React. Abstracts touch events to implement `onTap`, `onPress`, and pinch events. 5 | 6 | The events mimic their native equivalents as closely as possible: 7 | 8 | * the baseClass (default: `Tappable`) has `-active` or `-inactive` added to it to enable pressed-state styling 9 | * the pressed state is visually cancelled if the touch moves too far away from the element, but resumes if the touch comes back again 10 | * when you start scrolling a parent element, the touch event is cancelled 11 | * if the `onPress` property is set, it will cancel the touch event after the press happens 12 | 13 | When touch events are not supported, it will fall back to mouse events. Keyboard events are also supported, emulating the behaviour of native button controls. 14 | 15 | 16 | ## Demo & Examples 17 | 18 | Live demo: [jedwatson.github.io/react-tappable](http://jedwatson.github.io/react-tappable/) 19 | 20 | To build the examples locally, run: 21 | 22 | ``` 23 | npm install 24 | gulp dev 25 | ``` 26 | 27 | Then open [`localhost:8000`](http://localhost:8000) in a browser. 28 | 29 | 30 | ## Installation 31 | 32 | The easiest way to use React-tappable is to install it from npm. 33 | 34 | ``` 35 | npm install react-tappable --save 36 | ``` 37 | 38 | Ensure to include it in your own React build process (using [Browserify](http://browserify.org), etc). 39 | 40 | You could also use the standalone build by including `dist/react-tappable.js` in your page; but, if you do this, make sure you have already included React, and that it is available globally. 41 | 42 | 43 | ## Usage 44 | 45 | React-tappable generates a React component (defaults to ``) and binds touch events to it. 46 | 47 | To disable default event handling (e.g. scrolling) set the `preventDefault` prop. 48 | 49 | ```jsx 50 | import Tappable from 'react-tappable'; 51 | 52 | Tap me 53 | ``` 54 | 55 | For a lighter component, you can opt-in to just the features you need: 56 | 57 | ```jsx 58 | import Tappable from 'react-tappable/lib/Tappable'; 59 | import Pinchable from 'react-tappable/lib/Pinchable'; 60 | import TapAndPinchable from 'react-tappable/lib/TapAndPinchable'; 61 | 62 | I respond to Tap events 63 | I respond to Pinch events 64 | In respond to both! 65 | ``` 66 | 67 | The `TapAndPinchable` component is the default one you get when you just import `react-tappable`. 68 | 69 | ### Properties 70 | 71 | * `activeDelay` ms delay before the `-active` class is added, defaults to `0` 72 | * `component` component to render, defaults to `'span'` 73 | * `classes` optional object containing `active` and `inactive` class names to apply to the component; useful with [css-modules](https://github.com/css-modules/css-modules) 74 | * `classBase` base to use for the active/inactive classes 75 | * `className` optional class name for the component 76 | * `moveThreshold` px to allow movement before cancelling a tap; defaults to `100` 77 | * `pressDelay` ms delay before a press event is detected, defaults to `1000` 78 | * `pressMoveThreshold` px to allow movement before ignoring long presses; defaults to `5` 79 | * `preventDefault` (boolean) automatically call preventDefault on all events 80 | * `stopPropagation` (boolean) automatically call stopPropagation on all events 81 | * `style` (object) styles to apply to the component 82 | 83 | ### Special Events 84 | 85 | These are the special events implemented by `Tappable`. 86 | 87 | * `onTap` fired when touchStart or mouseDown is followed by touchEnd or mouseUp within the moveThreshold 88 | * `onPress` fired when a touch is held for the specified ms 89 | * `onPinchStart` fired when two fingers land on the screen 90 | * `onPinchMove` fired on any movement while two fingers are on screen 91 | * `onPinchEnd` fired when less than two fingers are left on the screen, onTouchStart is triggerred, if one touch remains 92 | 93 | #### Pinch Events 94 | 95 | Pinch events come with a special object with additional data to actually be more useful than the native events: 96 | 97 | * `touches`: Array of Objects - {identifier, pageX, pageY} - raw data from the event 98 | * `center`: Object - {x, y} - Calculated center between the two touch points 99 | * `angle`: Degrees - angle of the line connecting the two touch points to the X-axis 100 | * `distance`: Number of pixels - beween the two touch points 101 | * `displacement`: Object {x, y} - offset of the center since the pinch began 102 | * `displacementVelocity`: Object {x, y} : Pixels/ms - Immediate velocity of the displacement 103 | * `rotation`: degrees - delta rotation since the beginning of the gesture 104 | * `rotationVelocity`: degrees/millisecond - immediate rotational velocity 105 | * `zoom`: Number - Zoom factor - ratio between distance between the two touch points now over initial 106 | * `zoomVelocity`: zoomFactor/millisecond - immediate velocity of zooming 107 | * `time`: milliseconds since epoch - Timestamp 108 | 109 | #### Known Issues 110 | 111 | * The pinch implementation has not been thoroughly tested 112 | * Any touch event with 3 three or more touches is completely ignored. 113 | 114 | ### Native Events 115 | 116 | The following native event handlers can also be specified. 117 | 118 | * `onKeyDown` 119 | * `onKeyUp` 120 | * `onTouchStart` 121 | * `onTouchMove` 122 | * `onTouchEnd` 123 | * `onMouseDown` 124 | * `onMouseUp` 125 | * `onMouseMove` 126 | * `onMouseOut` 127 | 128 | Returning `false` from `onKeyDown`, `onMouseDown`, or `onTouchStart` handlers will prevent `Tappable` from handling the event. 129 | 130 | ## Changelog 131 | 132 | See [CHANGES.md](https://github.com/JedWatson/react-tappable/blob/master/CHANGES.md) 133 | 134 | ## License 135 | 136 | Copyright (c) 2017 Jed Watson. [MIT](LICENSE) 137 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-tappable", 3 | "main": "dist/react-tappable.min.js", 4 | "version": "1.0.0", 5 | "homepage": "https://github.com/JedWatson/react-tappable", 6 | "authors": [ 7 | "Jed Watson" 8 | ], 9 | "description": "Touch / Tappable Event Handling Component for React", 10 | "moduleType": [ 11 | "amd", 12 | "globals", 13 | "node" 14 | ], 15 | "keywords": [ 16 | "react", 17 | "react-component", 18 | "tap", 19 | "tappable", 20 | "touch", 21 | "mobile" 22 | ], 23 | "license": "MIT", 24 | "ignore": [ 25 | ".editorconfig", 26 | ".gitignore", 27 | "package.json", 28 | "src", 29 | "node_modules", 30 | "example", 31 | "test" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /dist/react-tappable.min.js: -------------------------------------------------------------------------------- 1 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var t;t="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,t.Tappable=e()}}(function(){return function e(t,o,s){function n(c,h){if(!o[c]){if(!t[c]){var r="function"==typeof require&&require;if(!h&&r)return r(c,!0);if(i)return i(c,!0);var u=new Error("Cannot find module '"+c+"'");throw u.code="MODULE_NOT_FOUND",u}var l=o[c]={exports:{}};t[c][0].call(l.exports,function(e){var o=t[c][1][e];return n(o?o:e)},l,l.exports,e,t,o,s)}return o[c].exports}for(var i="function"==typeof require&&require,c=0;c0?this._activeTimeout=setTimeout(this.makeActive,this.props.activeDelay):this.makeActive()):this.onPinchStart&&(this.props.onPinchStart||this.props.onPinchMove||this.props.onPinchEnd)&&2===e.touches.length&&this.onPinchStart(e))},makeActive:function(){this.isMounted&&(this.clearActiveTimeout(),this.setState({isActive:!0}))},clearActiveTimeout:function(){clearTimeout(this._activeTimeout),this._activeTimeout=!1},initScrollDetection:function(){this._scrollPos={top:0,left:0},this._scrollParents=[],this._scrollParentPos=[];for(var e=n.findDOMNode(this);e;)(e.scrollHeight>e.offsetHeight||e.scrollWidth>e.offsetWidth)&&(this._scrollParents.push(e),this._scrollParentPos.push(e.scrollTop+e.scrollLeft),this._scrollPos.top+=e.scrollTop,this._scrollPos.left+=e.scrollLeft),e=e.parentNode},initTouchmoveDetection:function(){this._touchmoveTriggeredTimes=0},cancelTouchmoveDetection:function(){this._touchmoveDetectionTimeout&&(clearTimeout(this._touchmoveDetectionTimeout),this._touchmoveDetectionTimeout=null,this._touchmoveTriggeredTimes=0)},calculateMovement:function(e){return{x:Math.abs(e.clientX-this._initialTouch.clientX),y:Math.abs(e.clientY-this._initialTouch.clientY)}},detectScroll:function(){for(var e={top:0,left:0},t=0;tthis.props.pressMoveThreshold||t.y>this.props.pressMoveThreshold)&&this.cancelPressDetection(),t.x>this.props.moveThreshold||t.y>this.props.moveThreshold?this.state.isActive?this.setState({isActive:!1}):this._activeTimeout&&this.clearActiveTimeout():this.state.isActive||this._activeTimeout||this.setState({isActive:!0})}else this._initialPinch&&2===e.touches.length&&this.onPinchMove&&(this.onPinchMove(e),e.preventDefault())},onTouchEnd:function(e){var t=this;if(this._initialTouch){this.processEvent(e);var o,s=this.calculateMovement(this._lastTouch);s.x<=this.props.moveThreshold&&s.y<=this.props.moveThreshold&&this.props.onTap&&(e.preventDefault(),o=function(){var o=t._scrollParents.map(function(e){return e.scrollTop+e.scrollLeft}),s=t._scrollParentPos.some(function(e,t){return e!==o[t]});s||t.props.onTap(e)}),this.endTouch(e,o)}else this.onPinchEnd&&this._initialPinch&&e.touches.length+e.changedTouches.length===2&&(this.onPinchEnd(e),e.preventDefault())},endTouch:function(e,t){this.cancelTouchmoveDetection(),this.cancelPressDetection(),this.clearActiveTimeout(),e&&this.props.onTouchEnd&&this.props.onTouchEnd(e),this._initialTouch=null,this._lastTouch=null,t&&t(),this.state.isActive&&this.setState({isActive:!1})},onMouseDown:function(e){return window._blockMouseEvents?void(window._blockMouseEvents=!1):void(this.props.onMouseDown&&this.props.onMouseDown(e)===!1||(this.processEvent(e),this.initPressDetection(e,this.endMouseEvent),this._mouseDown=!0,this.setState({isActive:!0})))},onMouseMove:function(e){!window._blockMouseEvents&&this._mouseDown&&(this.processEvent(e),this.props.onMouseMove&&this.props.onMouseMove(e))},onMouseUp:function(e){!window._blockMouseEvents&&this._mouseDown&&(this.processEvent(e),this.props.onMouseUp&&this.props.onMouseUp(e),this.props.onTap&&this.props.onTap(e),this.endMouseEvent())},onMouseOut:function(e){!window._blockMouseEvents&&this._mouseDown&&(this.processEvent(e),this.props.onMouseOut&&this.props.onMouseOut(e),this.endMouseEvent())},endMouseEvent:function(){this.cancelPressDetection(),this._mouseDown=!1,this.setState({isActive:!1})},onKeyUp:function(e){this._keyDown&&(this.processEvent(e),this.props.onKeyUp&&this.props.onKeyUp(e),this.props.onTap&&this.props.onTap(e),this._keyDown=!1,this.cancelPressDetection(),this.setState({isActive:!1}))},onKeyDown:function(e){this.props.onKeyDown&&this.props.onKeyDown(e)===!1||e.which!==i&&e.which!==c||this._keyDown||(this.initPressDetection(e,this.endKeyEvent),this.processEvent(e),this._keyDown=!0,this.setState({isActive:!0}))},endKeyEvent:function(){this.cancelPressDetection(),this._keyDown=!1,this.setState({isActive:!1})},cancelTap:function(){this.endTouch(),this._mouseDown=!1},handlers:function(){return{onTouchStart:this.onTouchStart,onTouchMove:this.onTouchMove,onTouchEnd:this.onTouchEnd,onMouseDown:this.onMouseDown,onMouseUp:this.onMouseUp,onMouseMove:this.onMouseMove,onMouseOut:this.onMouseOut,onKeyDown:this.onKeyDown,onKeyUp:this.onKeyUp}}};t.exports=h}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],3:[function(e,t,o){(function(o){"use strict";var s=Object.assign||function(e){for(var t=1;tHello World; 180 | * } 181 | * }); 182 | * 183 | * The class specification supports a specific protocol of methods that have 184 | * special meaning (e.g. `render`). See `ReactClassInterface` for 185 | * more the comprehensive protocol. Any other properties and methods in the 186 | * class specification will be available on the prototype. 187 | * 188 | * @interface ReactClassInterface 189 | * @internal 190 | */ 191 | var ReactClassInterface = { 192 | /** 193 | * An array of Mixin objects to include when defining your component. 194 | * 195 | * @type {array} 196 | * @optional 197 | */ 198 | mixins: 'DEFINE_MANY', 199 | 200 | /** 201 | * An object containing properties and methods that should be defined on 202 | * the component's constructor instead of its prototype (static methods). 203 | * 204 | * @type {object} 205 | * @optional 206 | */ 207 | statics: 'DEFINE_MANY', 208 | 209 | /** 210 | * Definition of prop types for this component. 211 | * 212 | * @type {object} 213 | * @optional 214 | */ 215 | propTypes: 'DEFINE_MANY', 216 | 217 | /** 218 | * Definition of context types for this component. 219 | * 220 | * @type {object} 221 | * @optional 222 | */ 223 | contextTypes: 'DEFINE_MANY', 224 | 225 | /** 226 | * Definition of context types this component sets for its children. 227 | * 228 | * @type {object} 229 | * @optional 230 | */ 231 | childContextTypes: 'DEFINE_MANY', 232 | 233 | // ==== Definition methods ==== 234 | 235 | /** 236 | * Invoked when the component is mounted. Values in the mapping will be set on 237 | * `this.props` if that prop is not specified (i.e. using an `in` check). 238 | * 239 | * This method is invoked before `getInitialState` and therefore cannot rely 240 | * on `this.state` or use `this.setState`. 241 | * 242 | * @return {object} 243 | * @optional 244 | */ 245 | getDefaultProps: 'DEFINE_MANY_MERGED', 246 | 247 | /** 248 | * Invoked once before the component is mounted. The return value will be used 249 | * as the initial value of `this.state`. 250 | * 251 | * getInitialState: function() { 252 | * return { 253 | * isOn: false, 254 | * fooBaz: new BazFoo() 255 | * } 256 | * } 257 | * 258 | * @return {object} 259 | * @optional 260 | */ 261 | getInitialState: 'DEFINE_MANY_MERGED', 262 | 263 | /** 264 | * @return {object} 265 | * @optional 266 | */ 267 | getChildContext: 'DEFINE_MANY_MERGED', 268 | 269 | /** 270 | * Uses props from `this.props` and state from `this.state` to render the 271 | * structure of the component. 272 | * 273 | * No guarantees are made about when or how often this method is invoked, so 274 | * it must not have side effects. 275 | * 276 | * render: function() { 277 | * var name = this.props.name; 278 | * return
Hello, {name}!
; 279 | * } 280 | * 281 | * @return {ReactComponent} 282 | * @required 283 | */ 284 | render: 'DEFINE_ONCE', 285 | 286 | // ==== Delegate methods ==== 287 | 288 | /** 289 | * Invoked when the component is initially created and about to be mounted. 290 | * This may have side effects, but any external subscriptions or data created 291 | * by this method must be cleaned up in `componentWillUnmount`. 292 | * 293 | * @optional 294 | */ 295 | componentWillMount: 'DEFINE_MANY', 296 | 297 | /** 298 | * Invoked when the component has been mounted and has a DOM representation. 299 | * However, there is no guarantee that the DOM node is in the document. 300 | * 301 | * Use this as an opportunity to operate on the DOM when the component has 302 | * been mounted (initialized and rendered) for the first time. 303 | * 304 | * @param {DOMElement} rootNode DOM element representing the component. 305 | * @optional 306 | */ 307 | componentDidMount: 'DEFINE_MANY', 308 | 309 | /** 310 | * Invoked before the component receives new props. 311 | * 312 | * Use this as an opportunity to react to a prop transition by updating the 313 | * state using `this.setState`. Current props are accessed via `this.props`. 314 | * 315 | * componentWillReceiveProps: function(nextProps, nextContext) { 316 | * this.setState({ 317 | * likesIncreasing: nextProps.likeCount > this.props.likeCount 318 | * }); 319 | * } 320 | * 321 | * NOTE: There is no equivalent `componentWillReceiveState`. An incoming prop 322 | * transition may cause a state change, but the opposite is not true. If you 323 | * need it, you are probably looking for `componentWillUpdate`. 324 | * 325 | * @param {object} nextProps 326 | * @optional 327 | */ 328 | componentWillReceiveProps: 'DEFINE_MANY', 329 | 330 | /** 331 | * Invoked while deciding if the component should be updated as a result of 332 | * receiving new props, state and/or context. 333 | * 334 | * Use this as an opportunity to `return false` when you're certain that the 335 | * transition to the new props/state/context will not require a component 336 | * update. 337 | * 338 | * shouldComponentUpdate: function(nextProps, nextState, nextContext) { 339 | * return !equal(nextProps, this.props) || 340 | * !equal(nextState, this.state) || 341 | * !equal(nextContext, this.context); 342 | * } 343 | * 344 | * @param {object} nextProps 345 | * @param {?object} nextState 346 | * @param {?object} nextContext 347 | * @return {boolean} True if the component should update. 348 | * @optional 349 | */ 350 | shouldComponentUpdate: 'DEFINE_ONCE', 351 | 352 | /** 353 | * Invoked when the component is about to update due to a transition from 354 | * `this.props`, `this.state` and `this.context` to `nextProps`, `nextState` 355 | * and `nextContext`. 356 | * 357 | * Use this as an opportunity to perform preparation before an update occurs. 358 | * 359 | * NOTE: You **cannot** use `this.setState()` in this method. 360 | * 361 | * @param {object} nextProps 362 | * @param {?object} nextState 363 | * @param {?object} nextContext 364 | * @param {ReactReconcileTransaction} transaction 365 | * @optional 366 | */ 367 | componentWillUpdate: 'DEFINE_MANY', 368 | 369 | /** 370 | * Invoked when the component's DOM representation has been updated. 371 | * 372 | * Use this as an opportunity to operate on the DOM when the component has 373 | * been updated. 374 | * 375 | * @param {object} prevProps 376 | * @param {?object} prevState 377 | * @param {?object} prevContext 378 | * @param {DOMElement} rootNode DOM element representing the component. 379 | * @optional 380 | */ 381 | componentDidUpdate: 'DEFINE_MANY', 382 | 383 | /** 384 | * Invoked when the component is about to be removed from its parent and have 385 | * its DOM representation destroyed. 386 | * 387 | * Use this as an opportunity to deallocate any external resources. 388 | * 389 | * NOTE: There is no `componentDidUnmount` since your component will have been 390 | * destroyed by that point. 391 | * 392 | * @optional 393 | */ 394 | componentWillUnmount: 'DEFINE_MANY', 395 | 396 | // ==== Advanced methods ==== 397 | 398 | /** 399 | * Updates the component's currently mounted DOM representation. 400 | * 401 | * By default, this implements React's rendering and reconciliation algorithm. 402 | * Sophisticated clients may wish to override this. 403 | * 404 | * @param {ReactReconcileTransaction} transaction 405 | * @internal 406 | * @overridable 407 | */ 408 | updateComponent: 'OVERRIDE_BASE' 409 | }; 410 | 411 | /** 412 | * Mapping from class specification keys to special processing functions. 413 | * 414 | * Although these are declared like instance properties in the specification 415 | * when defining classes using `React.createClass`, they are actually static 416 | * and are accessible on the constructor instead of the prototype. Despite 417 | * being static, they must be defined outside of the "statics" key under 418 | * which all other static methods are defined. 419 | */ 420 | var RESERVED_SPEC_KEYS = { 421 | displayName: function(Constructor, displayName) { 422 | Constructor.displayName = displayName; 423 | }, 424 | mixins: function(Constructor, mixins) { 425 | if (mixins) { 426 | for (var i = 0; i < mixins.length; i++) { 427 | mixSpecIntoComponent(Constructor, mixins[i]); 428 | } 429 | } 430 | }, 431 | childContextTypes: function(Constructor, childContextTypes) { 432 | if ("production" !== 'production') { 433 | validateTypeDef(Constructor, childContextTypes, 'childContext'); 434 | } 435 | Constructor.childContextTypes = _assign( 436 | {}, 437 | Constructor.childContextTypes, 438 | childContextTypes 439 | ); 440 | }, 441 | contextTypes: function(Constructor, contextTypes) { 442 | if ("production" !== 'production') { 443 | validateTypeDef(Constructor, contextTypes, 'context'); 444 | } 445 | Constructor.contextTypes = _assign( 446 | {}, 447 | Constructor.contextTypes, 448 | contextTypes 449 | ); 450 | }, 451 | /** 452 | * Special case getDefaultProps which should move into statics but requires 453 | * automatic merging. 454 | */ 455 | getDefaultProps: function(Constructor, getDefaultProps) { 456 | if (Constructor.getDefaultProps) { 457 | Constructor.getDefaultProps = createMergedResultFunction( 458 | Constructor.getDefaultProps, 459 | getDefaultProps 460 | ); 461 | } else { 462 | Constructor.getDefaultProps = getDefaultProps; 463 | } 464 | }, 465 | propTypes: function(Constructor, propTypes) { 466 | if ("production" !== 'production') { 467 | validateTypeDef(Constructor, propTypes, 'prop'); 468 | } 469 | Constructor.propTypes = _assign({}, Constructor.propTypes, propTypes); 470 | }, 471 | statics: function(Constructor, statics) { 472 | mixStaticSpecIntoComponent(Constructor, statics); 473 | }, 474 | autobind: function() {} 475 | }; 476 | 477 | function validateTypeDef(Constructor, typeDef, location) { 478 | for (var propName in typeDef) { 479 | if (typeDef.hasOwnProperty(propName)) { 480 | // use a warning instead of an _invariant so components 481 | // don't show up in prod but only in __DEV__ 482 | if ("production" !== 'production') { 483 | warning( 484 | typeof typeDef[propName] === 'function', 485 | '%s: %s type `%s` is invalid; it must be a function, usually from ' + 486 | 'React.PropTypes.', 487 | Constructor.displayName || 'ReactClass', 488 | ReactPropTypeLocationNames[location], 489 | propName 490 | ); 491 | } 492 | } 493 | } 494 | } 495 | 496 | function validateMethodOverride(isAlreadyDefined, name) { 497 | var specPolicy = ReactClassInterface.hasOwnProperty(name) 498 | ? ReactClassInterface[name] 499 | : null; 500 | 501 | // Disallow overriding of base class methods unless explicitly allowed. 502 | if (ReactClassMixin.hasOwnProperty(name)) { 503 | _invariant( 504 | specPolicy === 'OVERRIDE_BASE', 505 | 'ReactClassInterface: You are attempting to override ' + 506 | '`%s` from your class specification. Ensure that your method names ' + 507 | 'do not overlap with React methods.', 508 | name 509 | ); 510 | } 511 | 512 | // Disallow defining methods more than once unless explicitly allowed. 513 | if (isAlreadyDefined) { 514 | _invariant( 515 | specPolicy === 'DEFINE_MANY' || specPolicy === 'DEFINE_MANY_MERGED', 516 | 'ReactClassInterface: You are attempting to define ' + 517 | '`%s` on your component more than once. This conflict may be due ' + 518 | 'to a mixin.', 519 | name 520 | ); 521 | } 522 | } 523 | 524 | /** 525 | * Mixin helper which handles policy validation and reserved 526 | * specification keys when building React classes. 527 | */ 528 | function mixSpecIntoComponent(Constructor, spec) { 529 | if (!spec) { 530 | if ("production" !== 'production') { 531 | var typeofSpec = typeof spec; 532 | var isMixinValid = typeofSpec === 'object' && spec !== null; 533 | 534 | if ("production" !== 'production') { 535 | warning( 536 | isMixinValid, 537 | "%s: You're attempting to include a mixin that is either null " + 538 | 'or not an object. Check the mixins included by the component, ' + 539 | 'as well as any mixins they include themselves. ' + 540 | 'Expected object but got %s.', 541 | Constructor.displayName || 'ReactClass', 542 | spec === null ? null : typeofSpec 543 | ); 544 | } 545 | } 546 | 547 | return; 548 | } 549 | 550 | _invariant( 551 | typeof spec !== 'function', 552 | "ReactClass: You're attempting to " + 553 | 'use a component class or function as a mixin. Instead, just use a ' + 554 | 'regular object.' 555 | ); 556 | _invariant( 557 | !isValidElement(spec), 558 | "ReactClass: You're attempting to " + 559 | 'use a component as a mixin. Instead, just use a regular object.' 560 | ); 561 | 562 | var proto = Constructor.prototype; 563 | var autoBindPairs = proto.__reactAutoBindPairs; 564 | 565 | // By handling mixins before any other properties, we ensure the same 566 | // chaining order is applied to methods with DEFINE_MANY policy, whether 567 | // mixins are listed before or after these methods in the spec. 568 | if (spec.hasOwnProperty(MIXINS_KEY)) { 569 | RESERVED_SPEC_KEYS.mixins(Constructor, spec.mixins); 570 | } 571 | 572 | for (var name in spec) { 573 | if (!spec.hasOwnProperty(name)) { 574 | continue; 575 | } 576 | 577 | if (name === MIXINS_KEY) { 578 | // We have already handled mixins in a special case above. 579 | continue; 580 | } 581 | 582 | var property = spec[name]; 583 | var isAlreadyDefined = proto.hasOwnProperty(name); 584 | validateMethodOverride(isAlreadyDefined, name); 585 | 586 | if (RESERVED_SPEC_KEYS.hasOwnProperty(name)) { 587 | RESERVED_SPEC_KEYS[name](Constructor, property); 588 | } else { 589 | // Setup methods on prototype: 590 | // The following member methods should not be automatically bound: 591 | // 1. Expected ReactClass methods (in the "interface"). 592 | // 2. Overridden methods (that were mixed in). 593 | var isReactClassMethod = ReactClassInterface.hasOwnProperty(name); 594 | var isFunction = typeof property === 'function'; 595 | var shouldAutoBind = 596 | isFunction && 597 | !isReactClassMethod && 598 | !isAlreadyDefined && 599 | spec.autobind !== false; 600 | 601 | if (shouldAutoBind) { 602 | autoBindPairs.push(name, property); 603 | proto[name] = property; 604 | } else { 605 | if (isAlreadyDefined) { 606 | var specPolicy = ReactClassInterface[name]; 607 | 608 | // These cases should already be caught by validateMethodOverride. 609 | _invariant( 610 | isReactClassMethod && 611 | (specPolicy === 'DEFINE_MANY_MERGED' || 612 | specPolicy === 'DEFINE_MANY'), 613 | 'ReactClass: Unexpected spec policy %s for key %s ' + 614 | 'when mixing in component specs.', 615 | specPolicy, 616 | name 617 | ); 618 | 619 | // For methods which are defined more than once, call the existing 620 | // methods before calling the new property, merging if appropriate. 621 | if (specPolicy === 'DEFINE_MANY_MERGED') { 622 | proto[name] = createMergedResultFunction(proto[name], property); 623 | } else if (specPolicy === 'DEFINE_MANY') { 624 | proto[name] = createChainedFunction(proto[name], property); 625 | } 626 | } else { 627 | proto[name] = property; 628 | if ("production" !== 'production') { 629 | // Add verbose displayName to the function, which helps when looking 630 | // at profiling tools. 631 | if (typeof property === 'function' && spec.displayName) { 632 | proto[name].displayName = spec.displayName + '_' + name; 633 | } 634 | } 635 | } 636 | } 637 | } 638 | } 639 | } 640 | 641 | function mixStaticSpecIntoComponent(Constructor, statics) { 642 | if (!statics) { 643 | return; 644 | } 645 | for (var name in statics) { 646 | var property = statics[name]; 647 | if (!statics.hasOwnProperty(name)) { 648 | continue; 649 | } 650 | 651 | var isReserved = name in RESERVED_SPEC_KEYS; 652 | _invariant( 653 | !isReserved, 654 | 'ReactClass: You are attempting to define a reserved ' + 655 | 'property, `%s`, that shouldn\'t be on the "statics" key. Define it ' + 656 | 'as an instance property instead; it will still be accessible on the ' + 657 | 'constructor.', 658 | name 659 | ); 660 | 661 | var isInherited = name in Constructor; 662 | _invariant( 663 | !isInherited, 664 | 'ReactClass: You are attempting to define ' + 665 | '`%s` on your component more than once. This conflict may be ' + 666 | 'due to a mixin.', 667 | name 668 | ); 669 | Constructor[name] = property; 670 | } 671 | } 672 | 673 | /** 674 | * Merge two objects, but throw if both contain the same key. 675 | * 676 | * @param {object} one The first object, which is mutated. 677 | * @param {object} two The second object 678 | * @return {object} one after it has been mutated to contain everything in two. 679 | */ 680 | function mergeIntoWithNoDuplicateKeys(one, two) { 681 | _invariant( 682 | one && two && typeof one === 'object' && typeof two === 'object', 683 | 'mergeIntoWithNoDuplicateKeys(): Cannot merge non-objects.' 684 | ); 685 | 686 | for (var key in two) { 687 | if (two.hasOwnProperty(key)) { 688 | _invariant( 689 | one[key] === undefined, 690 | 'mergeIntoWithNoDuplicateKeys(): ' + 691 | 'Tried to merge two objects with the same key: `%s`. This conflict ' + 692 | 'may be due to a mixin; in particular, this may be caused by two ' + 693 | 'getInitialState() or getDefaultProps() methods returning objects ' + 694 | 'with clashing keys.', 695 | key 696 | ); 697 | one[key] = two[key]; 698 | } 699 | } 700 | return one; 701 | } 702 | 703 | /** 704 | * Creates a function that invokes two functions and merges their return values. 705 | * 706 | * @param {function} one Function to invoke first. 707 | * @param {function} two Function to invoke second. 708 | * @return {function} Function that invokes the two argument functions. 709 | * @private 710 | */ 711 | function createMergedResultFunction(one, two) { 712 | return function mergedResult() { 713 | var a = one.apply(this, arguments); 714 | var b = two.apply(this, arguments); 715 | if (a == null) { 716 | return b; 717 | } else if (b == null) { 718 | return a; 719 | } 720 | var c = {}; 721 | mergeIntoWithNoDuplicateKeys(c, a); 722 | mergeIntoWithNoDuplicateKeys(c, b); 723 | return c; 724 | }; 725 | } 726 | 727 | /** 728 | * Creates a function that invokes two functions and ignores their return vales. 729 | * 730 | * @param {function} one Function to invoke first. 731 | * @param {function} two Function to invoke second. 732 | * @return {function} Function that invokes the two argument functions. 733 | * @private 734 | */ 735 | function createChainedFunction(one, two) { 736 | return function chainedFunction() { 737 | one.apply(this, arguments); 738 | two.apply(this, arguments); 739 | }; 740 | } 741 | 742 | /** 743 | * Binds a method to the component. 744 | * 745 | * @param {object} component Component whose method is going to be bound. 746 | * @param {function} method Method to be bound. 747 | * @return {function} The bound method. 748 | */ 749 | function bindAutoBindMethod(component, method) { 750 | var boundMethod = method.bind(component); 751 | if ("production" !== 'production') { 752 | boundMethod.__reactBoundContext = component; 753 | boundMethod.__reactBoundMethod = method; 754 | boundMethod.__reactBoundArguments = null; 755 | var componentName = component.constructor.displayName; 756 | var _bind = boundMethod.bind; 757 | boundMethod.bind = function(newThis) { 758 | for ( 759 | var _len = arguments.length, 760 | args = Array(_len > 1 ? _len - 1 : 0), 761 | _key = 1; 762 | _key < _len; 763 | _key++ 764 | ) { 765 | args[_key - 1] = arguments[_key]; 766 | } 767 | 768 | // User is trying to bind() an autobound method; we effectively will 769 | // ignore the value of "this" that the user is trying to use, so 770 | // let's warn. 771 | if (newThis !== component && newThis !== null) { 772 | if ("production" !== 'production') { 773 | warning( 774 | false, 775 | 'bind(): React component methods may only be bound to the ' + 776 | 'component instance. See %s', 777 | componentName 778 | ); 779 | } 780 | } else if (!args.length) { 781 | if ("production" !== 'production') { 782 | warning( 783 | false, 784 | 'bind(): You are binding a component method to the component. ' + 785 | 'React does this for you automatically in a high-performance ' + 786 | 'way, so you can safely remove this call. See %s', 787 | componentName 788 | ); 789 | } 790 | return boundMethod; 791 | } 792 | var reboundMethod = _bind.apply(boundMethod, arguments); 793 | reboundMethod.__reactBoundContext = component; 794 | reboundMethod.__reactBoundMethod = method; 795 | reboundMethod.__reactBoundArguments = args; 796 | return reboundMethod; 797 | }; 798 | } 799 | return boundMethod; 800 | } 801 | 802 | /** 803 | * Binds all auto-bound methods in a component. 804 | * 805 | * @param {object} component Component whose method is going to be bound. 806 | */ 807 | function bindAutoBindMethods(component) { 808 | var pairs = component.__reactAutoBindPairs; 809 | for (var i = 0; i < pairs.length; i += 2) { 810 | var autoBindKey = pairs[i]; 811 | var method = pairs[i + 1]; 812 | component[autoBindKey] = bindAutoBindMethod(component, method); 813 | } 814 | } 815 | 816 | var IsMountedPreMixin = { 817 | componentDidMount: function() { 818 | this.__isMounted = true; 819 | } 820 | }; 821 | 822 | var IsMountedPostMixin = { 823 | componentWillUnmount: function() { 824 | this.__isMounted = false; 825 | } 826 | }; 827 | 828 | /** 829 | * Add more to the ReactClass base class. These are all legacy features and 830 | * therefore not already part of the modern ReactComponent. 831 | */ 832 | var ReactClassMixin = { 833 | /** 834 | * TODO: This will be deprecated because state should always keep a consistent 835 | * type signature and the only use case for this, is to avoid that. 836 | */ 837 | replaceState: function(newState, callback) { 838 | this.updater.enqueueReplaceState(this, newState, callback); 839 | }, 840 | 841 | /** 842 | * Checks whether or not this composite component is mounted. 843 | * @return {boolean} True if mounted, false otherwise. 844 | * @protected 845 | * @final 846 | */ 847 | isMounted: function() { 848 | if ("production" !== 'production') { 849 | warning( 850 | this.__didWarnIsMounted, 851 | '%s: isMounted is deprecated. Instead, make sure to clean up ' + 852 | 'subscriptions and pending requests in componentWillUnmount to ' + 853 | 'prevent memory leaks.', 854 | (this.constructor && this.constructor.displayName) || 855 | this.name || 856 | 'Component' 857 | ); 858 | this.__didWarnIsMounted = true; 859 | } 860 | return !!this.__isMounted; 861 | } 862 | }; 863 | 864 | var ReactClassComponent = function() {}; 865 | _assign( 866 | ReactClassComponent.prototype, 867 | ReactComponent.prototype, 868 | ReactClassMixin 869 | ); 870 | 871 | /** 872 | * Creates a composite component class given a class specification. 873 | * See https://facebook.github.io/react/docs/top-level-api.html#react.createclass 874 | * 875 | * @param {object} spec Class specification (which must define `render`). 876 | * @return {function} Component constructor function. 877 | * @public 878 | */ 879 | function createClass(spec) { 880 | // To keep our warnings more understandable, we'll use a little hack here to 881 | // ensure that Constructor.name !== 'Constructor'. This makes sure we don't 882 | // unnecessarily identify a class without displayName as 'Constructor'. 883 | var Constructor = identity(function(props, context, updater) { 884 | // This constructor gets overridden by mocks. The argument is used 885 | // by mocks to assert on what gets mounted. 886 | 887 | if ("production" !== 'production') { 888 | warning( 889 | this instanceof Constructor, 890 | 'Something is calling a React component directly. Use a factory or ' + 891 | 'JSX instead. See: https://fb.me/react-legacyfactory' 892 | ); 893 | } 894 | 895 | // Wire up auto-binding 896 | if (this.__reactAutoBindPairs.length) { 897 | bindAutoBindMethods(this); 898 | } 899 | 900 | this.props = props; 901 | this.context = context; 902 | this.refs = emptyObject; 903 | this.updater = updater || ReactNoopUpdateQueue; 904 | 905 | this.state = null; 906 | 907 | // ReactClasses doesn't have constructors. Instead, they use the 908 | // getInitialState and componentWillMount methods for initialization. 909 | 910 | var initialState = this.getInitialState ? this.getInitialState() : null; 911 | if ("production" !== 'production') { 912 | // We allow auto-mocks to proceed as if they're returning null. 913 | if ( 914 | initialState === undefined && 915 | this.getInitialState._isMockFunction 916 | ) { 917 | // This is probably bad practice. Consider warning here and 918 | // deprecating this convenience. 919 | initialState = null; 920 | } 921 | } 922 | _invariant( 923 | typeof initialState === 'object' && !Array.isArray(initialState), 924 | '%s.getInitialState(): must return an object or null', 925 | Constructor.displayName || 'ReactCompositeComponent' 926 | ); 927 | 928 | this.state = initialState; 929 | }); 930 | Constructor.prototype = new ReactClassComponent(); 931 | Constructor.prototype.constructor = Constructor; 932 | Constructor.prototype.__reactAutoBindPairs = []; 933 | 934 | injectedMixins.forEach(mixSpecIntoComponent.bind(null, Constructor)); 935 | 936 | mixSpecIntoComponent(Constructor, IsMountedPreMixin); 937 | mixSpecIntoComponent(Constructor, spec); 938 | mixSpecIntoComponent(Constructor, IsMountedPostMixin); 939 | 940 | // Initialize the defaultProps property after all mixins have been merged. 941 | if (Constructor.getDefaultProps) { 942 | Constructor.defaultProps = Constructor.getDefaultProps(); 943 | } 944 | 945 | if ("production" !== 'production') { 946 | // This is a tag to indicate that the use of these method names is ok, 947 | // since it's used with createClass. If it's not, then it's likely a 948 | // mistake so we'll warn you to use the static property, property 949 | // initializer or constructor respectively. 950 | if (Constructor.getDefaultProps) { 951 | Constructor.getDefaultProps.isReactClassApproved = {}; 952 | } 953 | if (Constructor.prototype.getInitialState) { 954 | Constructor.prototype.getInitialState.isReactClassApproved = {}; 955 | } 956 | } 957 | 958 | _invariant( 959 | Constructor.prototype.render, 960 | 'createClass(...): Class specification must implement a `render` method.' 961 | ); 962 | 963 | if ("production" !== 'production') { 964 | warning( 965 | !Constructor.prototype.componentShouldUpdate, 966 | '%s has a method called ' + 967 | 'componentShouldUpdate(). Did you mean shouldComponentUpdate()? ' + 968 | 'The name is phrased as a question because the function is ' + 969 | 'expected to return a value.', 970 | spec.displayName || 'A component' 971 | ); 972 | warning( 973 | !Constructor.prototype.componentWillRecieveProps, 974 | '%s has a method called ' + 975 | 'componentWillRecieveProps(). Did you mean componentWillReceiveProps()?', 976 | spec.displayName || 'A component' 977 | ); 978 | } 979 | 980 | // Reduce time spent doing lookups by setting these on the prototype. 981 | for (var methodName in ReactClassInterface) { 982 | if (!Constructor.prototype[methodName]) { 983 | Constructor.prototype[methodName] = null; 984 | } 985 | } 986 | 987 | return Constructor; 988 | } 989 | 990 | return createClass; 991 | } 992 | 993 | module.exports = factory; 994 | 995 | },{"fbjs/lib/emptyObject":5,"fbjs/lib/invariant":6,"fbjs/lib/warning":7,"object-assign":8}],3:[function(require,module,exports){ 996 | /** 997 | * Copyright 2013-present, Facebook, Inc. 998 | * All rights reserved. 999 | * 1000 | * This source code is licensed under the BSD-style license found in the 1001 | * LICENSE file in the root directory of this source tree. An additional grant 1002 | * of patent rights can be found in the PATENTS file in the same directory. 1003 | * 1004 | */ 1005 | 1006 | 'use strict'; 1007 | 1008 | var React = require('react'); 1009 | var factory = require('./factory'); 1010 | 1011 | if (typeof React === 'undefined') { 1012 | throw Error( 1013 | 'create-react-class could not find the React object. If you are using script tags, ' + 1014 | 'make sure that React is being loaded before create-react-class.' 1015 | ); 1016 | } 1017 | 1018 | // Hack to grab NoopUpdateQueue from isomorphic React 1019 | var ReactNoopUpdateQueue = new React.Component().updater; 1020 | 1021 | module.exports = factory( 1022 | React.Component, 1023 | React.isValidElement, 1024 | ReactNoopUpdateQueue 1025 | ); 1026 | 1027 | },{"./factory":2,"react":undefined}],4:[function(require,module,exports){ 1028 | "use strict"; 1029 | 1030 | /** 1031 | * Copyright (c) 2013-present, Facebook, Inc. 1032 | * All rights reserved. 1033 | * 1034 | * This source code is licensed under the BSD-style license found in the 1035 | * LICENSE file in the root directory of this source tree. An additional grant 1036 | * of patent rights can be found in the PATENTS file in the same directory. 1037 | * 1038 | * 1039 | */ 1040 | 1041 | function makeEmptyFunction(arg) { 1042 | return function () { 1043 | return arg; 1044 | }; 1045 | } 1046 | 1047 | /** 1048 | * This function accepts and discards inputs; it has no side effects. This is 1049 | * primarily useful idiomatically for overridable function endpoints which 1050 | * always need to be callable, since JS lacks a null-call idiom ala Cocoa. 1051 | */ 1052 | var emptyFunction = function emptyFunction() {}; 1053 | 1054 | emptyFunction.thatReturns = makeEmptyFunction; 1055 | emptyFunction.thatReturnsFalse = makeEmptyFunction(false); 1056 | emptyFunction.thatReturnsTrue = makeEmptyFunction(true); 1057 | emptyFunction.thatReturnsNull = makeEmptyFunction(null); 1058 | emptyFunction.thatReturnsThis = function () { 1059 | return this; 1060 | }; 1061 | emptyFunction.thatReturnsArgument = function (arg) { 1062 | return arg; 1063 | }; 1064 | 1065 | module.exports = emptyFunction; 1066 | },{}],5:[function(require,module,exports){ 1067 | /** 1068 | * Copyright (c) 2013-present, Facebook, Inc. 1069 | * All rights reserved. 1070 | * 1071 | * This source code is licensed under the BSD-style license found in the 1072 | * LICENSE file in the root directory of this source tree. An additional grant 1073 | * of patent rights can be found in the PATENTS file in the same directory. 1074 | * 1075 | */ 1076 | 1077 | 'use strict'; 1078 | 1079 | var emptyObject = {}; 1080 | 1081 | if ("production" !== 'production') { 1082 | Object.freeze(emptyObject); 1083 | } 1084 | 1085 | module.exports = emptyObject; 1086 | },{}],6:[function(require,module,exports){ 1087 | /** 1088 | * Copyright (c) 2013-present, Facebook, Inc. 1089 | * All rights reserved. 1090 | * 1091 | * This source code is licensed under the BSD-style license found in the 1092 | * LICENSE file in the root directory of this source tree. An additional grant 1093 | * of patent rights can be found in the PATENTS file in the same directory. 1094 | * 1095 | */ 1096 | 1097 | 'use strict'; 1098 | 1099 | /** 1100 | * Use invariant() to assert state which your program assumes to be true. 1101 | * 1102 | * Provide sprintf-style format (only %s is supported) and arguments 1103 | * to provide information about what broke and what you were 1104 | * expecting. 1105 | * 1106 | * The invariant message will be stripped in production, but the invariant 1107 | * will remain to ensure logic does not differ in production. 1108 | */ 1109 | 1110 | var validateFormat = function validateFormat(format) {}; 1111 | 1112 | if ("production" !== 'production') { 1113 | validateFormat = function validateFormat(format) { 1114 | if (format === undefined) { 1115 | throw new Error('invariant requires an error message argument'); 1116 | } 1117 | }; 1118 | } 1119 | 1120 | function invariant(condition, format, a, b, c, d, e, f) { 1121 | validateFormat(format); 1122 | 1123 | if (!condition) { 1124 | var error; 1125 | if (format === undefined) { 1126 | error = new Error('Minified exception occurred; use the non-minified dev environment ' + 'for the full error message and additional helpful warnings.'); 1127 | } else { 1128 | var args = [a, b, c, d, e, f]; 1129 | var argIndex = 0; 1130 | error = new Error(format.replace(/%s/g, function () { 1131 | return args[argIndex++]; 1132 | })); 1133 | error.name = 'Invariant Violation'; 1134 | } 1135 | 1136 | error.framesToPop = 1; // we don't care about invariant's own frame 1137 | throw error; 1138 | } 1139 | } 1140 | 1141 | module.exports = invariant; 1142 | },{}],7:[function(require,module,exports){ 1143 | /** 1144 | * Copyright 2014-2015, Facebook, Inc. 1145 | * All rights reserved. 1146 | * 1147 | * This source code is licensed under the BSD-style license found in the 1148 | * LICENSE file in the root directory of this source tree. An additional grant 1149 | * of patent rights can be found in the PATENTS file in the same directory. 1150 | * 1151 | */ 1152 | 1153 | 'use strict'; 1154 | 1155 | var emptyFunction = require('./emptyFunction'); 1156 | 1157 | /** 1158 | * Similar to invariant but only logs a warning if the condition is not met. 1159 | * This can be used to log issues in development environments in critical 1160 | * paths. Removing the logging code for production environments will keep the 1161 | * same logic and follow the same code paths. 1162 | */ 1163 | 1164 | var warning = emptyFunction; 1165 | 1166 | if ("production" !== 'production') { 1167 | var printWarning = function printWarning(format) { 1168 | for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { 1169 | args[_key - 1] = arguments[_key]; 1170 | } 1171 | 1172 | var argIndex = 0; 1173 | var message = 'Warning: ' + format.replace(/%s/g, function () { 1174 | return args[argIndex++]; 1175 | }); 1176 | if (typeof console !== 'undefined') { 1177 | console.error(message); 1178 | } 1179 | try { 1180 | // --- Welcome to debugging React --- 1181 | // This error was thrown as a convenience so that you can use this stack 1182 | // to find the callsite that caused this warning to fire. 1183 | throw new Error(message); 1184 | } catch (x) {} 1185 | }; 1186 | 1187 | warning = function warning(condition, format) { 1188 | if (format === undefined) { 1189 | throw new Error('`warning(condition, format, ...args)` requires a warning ' + 'message argument'); 1190 | } 1191 | 1192 | if (format.indexOf('Failed Composite propType: ') === 0) { 1193 | return; // Ignore CompositeComponent proptype check. 1194 | } 1195 | 1196 | if (!condition) { 1197 | for (var _len2 = arguments.length, args = Array(_len2 > 2 ? _len2 - 2 : 0), _key2 = 2; _key2 < _len2; _key2++) { 1198 | args[_key2 - 2] = arguments[_key2]; 1199 | } 1200 | 1201 | printWarning.apply(undefined, [format].concat(args)); 1202 | } 1203 | }; 1204 | } 1205 | 1206 | module.exports = warning; 1207 | },{"./emptyFunction":4}],8:[function(require,module,exports){ 1208 | /* 1209 | object-assign 1210 | (c) Sindre Sorhus 1211 | @license MIT 1212 | */ 1213 | 1214 | 'use strict'; 1215 | /* eslint-disable no-unused-vars */ 1216 | var getOwnPropertySymbols = Object.getOwnPropertySymbols; 1217 | var hasOwnProperty = Object.prototype.hasOwnProperty; 1218 | var propIsEnumerable = Object.prototype.propertyIsEnumerable; 1219 | 1220 | function toObject(val) { 1221 | if (val === null || val === undefined) { 1222 | throw new TypeError('Object.assign cannot be called with null or undefined'); 1223 | } 1224 | 1225 | return Object(val); 1226 | } 1227 | 1228 | function shouldUseNative() { 1229 | try { 1230 | if (!Object.assign) { 1231 | return false; 1232 | } 1233 | 1234 | // Detect buggy property enumeration order in older V8 versions. 1235 | 1236 | // https://bugs.chromium.org/p/v8/issues/detail?id=4118 1237 | var test1 = new String('abc'); // eslint-disable-line no-new-wrappers 1238 | test1[5] = 'de'; 1239 | if (Object.getOwnPropertyNames(test1)[0] === '5') { 1240 | return false; 1241 | } 1242 | 1243 | // https://bugs.chromium.org/p/v8/issues/detail?id=3056 1244 | var test2 = {}; 1245 | for (var i = 0; i < 10; i++) { 1246 | test2['_' + String.fromCharCode(i)] = i; 1247 | } 1248 | var order2 = Object.getOwnPropertyNames(test2).map(function (n) { 1249 | return test2[n]; 1250 | }); 1251 | if (order2.join('') !== '0123456789') { 1252 | return false; 1253 | } 1254 | 1255 | // https://bugs.chromium.org/p/v8/issues/detail?id=3056 1256 | var test3 = {}; 1257 | 'abcdefghijklmnopqrst'.split('').forEach(function (letter) { 1258 | test3[letter] = letter; 1259 | }); 1260 | if (Object.keys(Object.assign({}, test3)).join('') !== 1261 | 'abcdefghijklmnopqrst') { 1262 | return false; 1263 | } 1264 | 1265 | return true; 1266 | } catch (err) { 1267 | // We don't expect any of the above to throw, but better to be safe. 1268 | return false; 1269 | } 1270 | } 1271 | 1272 | module.exports = shouldUseNative() ? Object.assign : function (target, source) { 1273 | var from; 1274 | var to = toObject(target); 1275 | var symbols; 1276 | 1277 | for (var s = 1; s < arguments.length; s++) { 1278 | from = Object(arguments[s]); 1279 | 1280 | for (var key in from) { 1281 | if (hasOwnProperty.call(from, key)) { 1282 | to[key] = from[key]; 1283 | } 1284 | } 1285 | 1286 | if (getOwnPropertySymbols) { 1287 | symbols = getOwnPropertySymbols(from); 1288 | for (var i = 0; i < symbols.length; i++) { 1289 | if (propIsEnumerable.call(from, symbols[i])) { 1290 | to[symbols[i]] = from[symbols[i]]; 1291 | } 1292 | } 1293 | } 1294 | } 1295 | 1296 | return to; 1297 | }; 1298 | 1299 | },{}]},{},[1]); 1300 | -------------------------------------------------------------------------------- /example/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | React-Tappable Example 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |

React Tappable

15 |

View project on GitHub

16 |
17 |
18 | Events in the area on the left will be logged on the right. 19 | Toggle scrolling to test event cancel on scroll (touch devices only). 20 |
21 | 24 |
25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /example/dist/pinch.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | React-Tappable Example 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |

React Tappable

15 |

View project on GitHub

16 |
17 |
18 | Events in the area on the left will be logged on the right. 19 | Toggle scrolling to test event cancel on scroll (touch devices only). 20 |
21 | 24 |
25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /example/dist/pinch.js: -------------------------------------------------------------------------------- 1 | require=(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;oHello World; 141 | * } 142 | * }); 143 | * 144 | * The class specification supports a specific protocol of methods that have 145 | * special meaning (e.g. `render`). See `ReactClassInterface` for 146 | * more the comprehensive protocol. Any other properties and methods in the 147 | * class specification will be available on the prototype. 148 | * 149 | * @interface ReactClassInterface 150 | * @internal 151 | */ 152 | var ReactClassInterface = { 153 | /** 154 | * An array of Mixin objects to include when defining your component. 155 | * 156 | * @type {array} 157 | * @optional 158 | */ 159 | mixins: 'DEFINE_MANY', 160 | 161 | /** 162 | * An object containing properties and methods that should be defined on 163 | * the component's constructor instead of its prototype (static methods). 164 | * 165 | * @type {object} 166 | * @optional 167 | */ 168 | statics: 'DEFINE_MANY', 169 | 170 | /** 171 | * Definition of prop types for this component. 172 | * 173 | * @type {object} 174 | * @optional 175 | */ 176 | propTypes: 'DEFINE_MANY', 177 | 178 | /** 179 | * Definition of context types for this component. 180 | * 181 | * @type {object} 182 | * @optional 183 | */ 184 | contextTypes: 'DEFINE_MANY', 185 | 186 | /** 187 | * Definition of context types this component sets for its children. 188 | * 189 | * @type {object} 190 | * @optional 191 | */ 192 | childContextTypes: 'DEFINE_MANY', 193 | 194 | // ==== Definition methods ==== 195 | 196 | /** 197 | * Invoked when the component is mounted. Values in the mapping will be set on 198 | * `this.props` if that prop is not specified (i.e. using an `in` check). 199 | * 200 | * This method is invoked before `getInitialState` and therefore cannot rely 201 | * on `this.state` or use `this.setState`. 202 | * 203 | * @return {object} 204 | * @optional 205 | */ 206 | getDefaultProps: 'DEFINE_MANY_MERGED', 207 | 208 | /** 209 | * Invoked once before the component is mounted. The return value will be used 210 | * as the initial value of `this.state`. 211 | * 212 | * getInitialState: function() { 213 | * return { 214 | * isOn: false, 215 | * fooBaz: new BazFoo() 216 | * } 217 | * } 218 | * 219 | * @return {object} 220 | * @optional 221 | */ 222 | getInitialState: 'DEFINE_MANY_MERGED', 223 | 224 | /** 225 | * @return {object} 226 | * @optional 227 | */ 228 | getChildContext: 'DEFINE_MANY_MERGED', 229 | 230 | /** 231 | * Uses props from `this.props` and state from `this.state` to render the 232 | * structure of the component. 233 | * 234 | * No guarantees are made about when or how often this method is invoked, so 235 | * it must not have side effects. 236 | * 237 | * render: function() { 238 | * var name = this.props.name; 239 | * return
Hello, {name}!
; 240 | * } 241 | * 242 | * @return {ReactComponent} 243 | * @required 244 | */ 245 | render: 'DEFINE_ONCE', 246 | 247 | // ==== Delegate methods ==== 248 | 249 | /** 250 | * Invoked when the component is initially created and about to be mounted. 251 | * This may have side effects, but any external subscriptions or data created 252 | * by this method must be cleaned up in `componentWillUnmount`. 253 | * 254 | * @optional 255 | */ 256 | componentWillMount: 'DEFINE_MANY', 257 | 258 | /** 259 | * Invoked when the component has been mounted and has a DOM representation. 260 | * However, there is no guarantee that the DOM node is in the document. 261 | * 262 | * Use this as an opportunity to operate on the DOM when the component has 263 | * been mounted (initialized and rendered) for the first time. 264 | * 265 | * @param {DOMElement} rootNode DOM element representing the component. 266 | * @optional 267 | */ 268 | componentDidMount: 'DEFINE_MANY', 269 | 270 | /** 271 | * Invoked before the component receives new props. 272 | * 273 | * Use this as an opportunity to react to a prop transition by updating the 274 | * state using `this.setState`. Current props are accessed via `this.props`. 275 | * 276 | * componentWillReceiveProps: function(nextProps, nextContext) { 277 | * this.setState({ 278 | * likesIncreasing: nextProps.likeCount > this.props.likeCount 279 | * }); 280 | * } 281 | * 282 | * NOTE: There is no equivalent `componentWillReceiveState`. An incoming prop 283 | * transition may cause a state change, but the opposite is not true. If you 284 | * need it, you are probably looking for `componentWillUpdate`. 285 | * 286 | * @param {object} nextProps 287 | * @optional 288 | */ 289 | componentWillReceiveProps: 'DEFINE_MANY', 290 | 291 | /** 292 | * Invoked while deciding if the component should be updated as a result of 293 | * receiving new props, state and/or context. 294 | * 295 | * Use this as an opportunity to `return false` when you're certain that the 296 | * transition to the new props/state/context will not require a component 297 | * update. 298 | * 299 | * shouldComponentUpdate: function(nextProps, nextState, nextContext) { 300 | * return !equal(nextProps, this.props) || 301 | * !equal(nextState, this.state) || 302 | * !equal(nextContext, this.context); 303 | * } 304 | * 305 | * @param {object} nextProps 306 | * @param {?object} nextState 307 | * @param {?object} nextContext 308 | * @return {boolean} True if the component should update. 309 | * @optional 310 | */ 311 | shouldComponentUpdate: 'DEFINE_ONCE', 312 | 313 | /** 314 | * Invoked when the component is about to update due to a transition from 315 | * `this.props`, `this.state` and `this.context` to `nextProps`, `nextState` 316 | * and `nextContext`. 317 | * 318 | * Use this as an opportunity to perform preparation before an update occurs. 319 | * 320 | * NOTE: You **cannot** use `this.setState()` in this method. 321 | * 322 | * @param {object} nextProps 323 | * @param {?object} nextState 324 | * @param {?object} nextContext 325 | * @param {ReactReconcileTransaction} transaction 326 | * @optional 327 | */ 328 | componentWillUpdate: 'DEFINE_MANY', 329 | 330 | /** 331 | * Invoked when the component's DOM representation has been updated. 332 | * 333 | * Use this as an opportunity to operate on the DOM when the component has 334 | * been updated. 335 | * 336 | * @param {object} prevProps 337 | * @param {?object} prevState 338 | * @param {?object} prevContext 339 | * @param {DOMElement} rootNode DOM element representing the component. 340 | * @optional 341 | */ 342 | componentDidUpdate: 'DEFINE_MANY', 343 | 344 | /** 345 | * Invoked when the component is about to be removed from its parent and have 346 | * its DOM representation destroyed. 347 | * 348 | * Use this as an opportunity to deallocate any external resources. 349 | * 350 | * NOTE: There is no `componentDidUnmount` since your component will have been 351 | * destroyed by that point. 352 | * 353 | * @optional 354 | */ 355 | componentWillUnmount: 'DEFINE_MANY', 356 | 357 | // ==== Advanced methods ==== 358 | 359 | /** 360 | * Updates the component's currently mounted DOM representation. 361 | * 362 | * By default, this implements React's rendering and reconciliation algorithm. 363 | * Sophisticated clients may wish to override this. 364 | * 365 | * @param {ReactReconcileTransaction} transaction 366 | * @internal 367 | * @overridable 368 | */ 369 | updateComponent: 'OVERRIDE_BASE' 370 | }; 371 | 372 | /** 373 | * Mapping from class specification keys to special processing functions. 374 | * 375 | * Although these are declared like instance properties in the specification 376 | * when defining classes using `React.createClass`, they are actually static 377 | * and are accessible on the constructor instead of the prototype. Despite 378 | * being static, they must be defined outside of the "statics" key under 379 | * which all other static methods are defined. 380 | */ 381 | var RESERVED_SPEC_KEYS = { 382 | displayName: function(Constructor, displayName) { 383 | Constructor.displayName = displayName; 384 | }, 385 | mixins: function(Constructor, mixins) { 386 | if (mixins) { 387 | for (var i = 0; i < mixins.length; i++) { 388 | mixSpecIntoComponent(Constructor, mixins[i]); 389 | } 390 | } 391 | }, 392 | childContextTypes: function(Constructor, childContextTypes) { 393 | if ("production" !== 'production') { 394 | validateTypeDef(Constructor, childContextTypes, 'childContext'); 395 | } 396 | Constructor.childContextTypes = _assign( 397 | {}, 398 | Constructor.childContextTypes, 399 | childContextTypes 400 | ); 401 | }, 402 | contextTypes: function(Constructor, contextTypes) { 403 | if ("production" !== 'production') { 404 | validateTypeDef(Constructor, contextTypes, 'context'); 405 | } 406 | Constructor.contextTypes = _assign( 407 | {}, 408 | Constructor.contextTypes, 409 | contextTypes 410 | ); 411 | }, 412 | /** 413 | * Special case getDefaultProps which should move into statics but requires 414 | * automatic merging. 415 | */ 416 | getDefaultProps: function(Constructor, getDefaultProps) { 417 | if (Constructor.getDefaultProps) { 418 | Constructor.getDefaultProps = createMergedResultFunction( 419 | Constructor.getDefaultProps, 420 | getDefaultProps 421 | ); 422 | } else { 423 | Constructor.getDefaultProps = getDefaultProps; 424 | } 425 | }, 426 | propTypes: function(Constructor, propTypes) { 427 | if ("production" !== 'production') { 428 | validateTypeDef(Constructor, propTypes, 'prop'); 429 | } 430 | Constructor.propTypes = _assign({}, Constructor.propTypes, propTypes); 431 | }, 432 | statics: function(Constructor, statics) { 433 | mixStaticSpecIntoComponent(Constructor, statics); 434 | }, 435 | autobind: function() {} 436 | }; 437 | 438 | function validateTypeDef(Constructor, typeDef, location) { 439 | for (var propName in typeDef) { 440 | if (typeDef.hasOwnProperty(propName)) { 441 | // use a warning instead of an _invariant so components 442 | // don't show up in prod but only in __DEV__ 443 | if ("production" !== 'production') { 444 | warning( 445 | typeof typeDef[propName] === 'function', 446 | '%s: %s type `%s` is invalid; it must be a function, usually from ' + 447 | 'React.PropTypes.', 448 | Constructor.displayName || 'ReactClass', 449 | ReactPropTypeLocationNames[location], 450 | propName 451 | ); 452 | } 453 | } 454 | } 455 | } 456 | 457 | function validateMethodOverride(isAlreadyDefined, name) { 458 | var specPolicy = ReactClassInterface.hasOwnProperty(name) 459 | ? ReactClassInterface[name] 460 | : null; 461 | 462 | // Disallow overriding of base class methods unless explicitly allowed. 463 | if (ReactClassMixin.hasOwnProperty(name)) { 464 | _invariant( 465 | specPolicy === 'OVERRIDE_BASE', 466 | 'ReactClassInterface: You are attempting to override ' + 467 | '`%s` from your class specification. Ensure that your method names ' + 468 | 'do not overlap with React methods.', 469 | name 470 | ); 471 | } 472 | 473 | // Disallow defining methods more than once unless explicitly allowed. 474 | if (isAlreadyDefined) { 475 | _invariant( 476 | specPolicy === 'DEFINE_MANY' || specPolicy === 'DEFINE_MANY_MERGED', 477 | 'ReactClassInterface: You are attempting to define ' + 478 | '`%s` on your component more than once. This conflict may be due ' + 479 | 'to a mixin.', 480 | name 481 | ); 482 | } 483 | } 484 | 485 | /** 486 | * Mixin helper which handles policy validation and reserved 487 | * specification keys when building React classes. 488 | */ 489 | function mixSpecIntoComponent(Constructor, spec) { 490 | if (!spec) { 491 | if ("production" !== 'production') { 492 | var typeofSpec = typeof spec; 493 | var isMixinValid = typeofSpec === 'object' && spec !== null; 494 | 495 | if ("production" !== 'production') { 496 | warning( 497 | isMixinValid, 498 | "%s: You're attempting to include a mixin that is either null " + 499 | 'or not an object. Check the mixins included by the component, ' + 500 | 'as well as any mixins they include themselves. ' + 501 | 'Expected object but got %s.', 502 | Constructor.displayName || 'ReactClass', 503 | spec === null ? null : typeofSpec 504 | ); 505 | } 506 | } 507 | 508 | return; 509 | } 510 | 511 | _invariant( 512 | typeof spec !== 'function', 513 | "ReactClass: You're attempting to " + 514 | 'use a component class or function as a mixin. Instead, just use a ' + 515 | 'regular object.' 516 | ); 517 | _invariant( 518 | !isValidElement(spec), 519 | "ReactClass: You're attempting to " + 520 | 'use a component as a mixin. Instead, just use a regular object.' 521 | ); 522 | 523 | var proto = Constructor.prototype; 524 | var autoBindPairs = proto.__reactAutoBindPairs; 525 | 526 | // By handling mixins before any other properties, we ensure the same 527 | // chaining order is applied to methods with DEFINE_MANY policy, whether 528 | // mixins are listed before or after these methods in the spec. 529 | if (spec.hasOwnProperty(MIXINS_KEY)) { 530 | RESERVED_SPEC_KEYS.mixins(Constructor, spec.mixins); 531 | } 532 | 533 | for (var name in spec) { 534 | if (!spec.hasOwnProperty(name)) { 535 | continue; 536 | } 537 | 538 | if (name === MIXINS_KEY) { 539 | // We have already handled mixins in a special case above. 540 | continue; 541 | } 542 | 543 | var property = spec[name]; 544 | var isAlreadyDefined = proto.hasOwnProperty(name); 545 | validateMethodOverride(isAlreadyDefined, name); 546 | 547 | if (RESERVED_SPEC_KEYS.hasOwnProperty(name)) { 548 | RESERVED_SPEC_KEYS[name](Constructor, property); 549 | } else { 550 | // Setup methods on prototype: 551 | // The following member methods should not be automatically bound: 552 | // 1. Expected ReactClass methods (in the "interface"). 553 | // 2. Overridden methods (that were mixed in). 554 | var isReactClassMethod = ReactClassInterface.hasOwnProperty(name); 555 | var isFunction = typeof property === 'function'; 556 | var shouldAutoBind = 557 | isFunction && 558 | !isReactClassMethod && 559 | !isAlreadyDefined && 560 | spec.autobind !== false; 561 | 562 | if (shouldAutoBind) { 563 | autoBindPairs.push(name, property); 564 | proto[name] = property; 565 | } else { 566 | if (isAlreadyDefined) { 567 | var specPolicy = ReactClassInterface[name]; 568 | 569 | // These cases should already be caught by validateMethodOverride. 570 | _invariant( 571 | isReactClassMethod && 572 | (specPolicy === 'DEFINE_MANY_MERGED' || 573 | specPolicy === 'DEFINE_MANY'), 574 | 'ReactClass: Unexpected spec policy %s for key %s ' + 575 | 'when mixing in component specs.', 576 | specPolicy, 577 | name 578 | ); 579 | 580 | // For methods which are defined more than once, call the existing 581 | // methods before calling the new property, merging if appropriate. 582 | if (specPolicy === 'DEFINE_MANY_MERGED') { 583 | proto[name] = createMergedResultFunction(proto[name], property); 584 | } else if (specPolicy === 'DEFINE_MANY') { 585 | proto[name] = createChainedFunction(proto[name], property); 586 | } 587 | } else { 588 | proto[name] = property; 589 | if ("production" !== 'production') { 590 | // Add verbose displayName to the function, which helps when looking 591 | // at profiling tools. 592 | if (typeof property === 'function' && spec.displayName) { 593 | proto[name].displayName = spec.displayName + '_' + name; 594 | } 595 | } 596 | } 597 | } 598 | } 599 | } 600 | } 601 | 602 | function mixStaticSpecIntoComponent(Constructor, statics) { 603 | if (!statics) { 604 | return; 605 | } 606 | for (var name in statics) { 607 | var property = statics[name]; 608 | if (!statics.hasOwnProperty(name)) { 609 | continue; 610 | } 611 | 612 | var isReserved = name in RESERVED_SPEC_KEYS; 613 | _invariant( 614 | !isReserved, 615 | 'ReactClass: You are attempting to define a reserved ' + 616 | 'property, `%s`, that shouldn\'t be on the "statics" key. Define it ' + 617 | 'as an instance property instead; it will still be accessible on the ' + 618 | 'constructor.', 619 | name 620 | ); 621 | 622 | var isInherited = name in Constructor; 623 | _invariant( 624 | !isInherited, 625 | 'ReactClass: You are attempting to define ' + 626 | '`%s` on your component more than once. This conflict may be ' + 627 | 'due to a mixin.', 628 | name 629 | ); 630 | Constructor[name] = property; 631 | } 632 | } 633 | 634 | /** 635 | * Merge two objects, but throw if both contain the same key. 636 | * 637 | * @param {object} one The first object, which is mutated. 638 | * @param {object} two The second object 639 | * @return {object} one after it has been mutated to contain everything in two. 640 | */ 641 | function mergeIntoWithNoDuplicateKeys(one, two) { 642 | _invariant( 643 | one && two && typeof one === 'object' && typeof two === 'object', 644 | 'mergeIntoWithNoDuplicateKeys(): Cannot merge non-objects.' 645 | ); 646 | 647 | for (var key in two) { 648 | if (two.hasOwnProperty(key)) { 649 | _invariant( 650 | one[key] === undefined, 651 | 'mergeIntoWithNoDuplicateKeys(): ' + 652 | 'Tried to merge two objects with the same key: `%s`. This conflict ' + 653 | 'may be due to a mixin; in particular, this may be caused by two ' + 654 | 'getInitialState() or getDefaultProps() methods returning objects ' + 655 | 'with clashing keys.', 656 | key 657 | ); 658 | one[key] = two[key]; 659 | } 660 | } 661 | return one; 662 | } 663 | 664 | /** 665 | * Creates a function that invokes two functions and merges their return values. 666 | * 667 | * @param {function} one Function to invoke first. 668 | * @param {function} two Function to invoke second. 669 | * @return {function} Function that invokes the two argument functions. 670 | * @private 671 | */ 672 | function createMergedResultFunction(one, two) { 673 | return function mergedResult() { 674 | var a = one.apply(this, arguments); 675 | var b = two.apply(this, arguments); 676 | if (a == null) { 677 | return b; 678 | } else if (b == null) { 679 | return a; 680 | } 681 | var c = {}; 682 | mergeIntoWithNoDuplicateKeys(c, a); 683 | mergeIntoWithNoDuplicateKeys(c, b); 684 | return c; 685 | }; 686 | } 687 | 688 | /** 689 | * Creates a function that invokes two functions and ignores their return vales. 690 | * 691 | * @param {function} one Function to invoke first. 692 | * @param {function} two Function to invoke second. 693 | * @return {function} Function that invokes the two argument functions. 694 | * @private 695 | */ 696 | function createChainedFunction(one, two) { 697 | return function chainedFunction() { 698 | one.apply(this, arguments); 699 | two.apply(this, arguments); 700 | }; 701 | } 702 | 703 | /** 704 | * Binds a method to the component. 705 | * 706 | * @param {object} component Component whose method is going to be bound. 707 | * @param {function} method Method to be bound. 708 | * @return {function} The bound method. 709 | */ 710 | function bindAutoBindMethod(component, method) { 711 | var boundMethod = method.bind(component); 712 | if ("production" !== 'production') { 713 | boundMethod.__reactBoundContext = component; 714 | boundMethod.__reactBoundMethod = method; 715 | boundMethod.__reactBoundArguments = null; 716 | var componentName = component.constructor.displayName; 717 | var _bind = boundMethod.bind; 718 | boundMethod.bind = function(newThis) { 719 | for ( 720 | var _len = arguments.length, 721 | args = Array(_len > 1 ? _len - 1 : 0), 722 | _key = 1; 723 | _key < _len; 724 | _key++ 725 | ) { 726 | args[_key - 1] = arguments[_key]; 727 | } 728 | 729 | // User is trying to bind() an autobound method; we effectively will 730 | // ignore the value of "this" that the user is trying to use, so 731 | // let's warn. 732 | if (newThis !== component && newThis !== null) { 733 | if ("production" !== 'production') { 734 | warning( 735 | false, 736 | 'bind(): React component methods may only be bound to the ' + 737 | 'component instance. See %s', 738 | componentName 739 | ); 740 | } 741 | } else if (!args.length) { 742 | if ("production" !== 'production') { 743 | warning( 744 | false, 745 | 'bind(): You are binding a component method to the component. ' + 746 | 'React does this for you automatically in a high-performance ' + 747 | 'way, so you can safely remove this call. See %s', 748 | componentName 749 | ); 750 | } 751 | return boundMethod; 752 | } 753 | var reboundMethod = _bind.apply(boundMethod, arguments); 754 | reboundMethod.__reactBoundContext = component; 755 | reboundMethod.__reactBoundMethod = method; 756 | reboundMethod.__reactBoundArguments = args; 757 | return reboundMethod; 758 | }; 759 | } 760 | return boundMethod; 761 | } 762 | 763 | /** 764 | * Binds all auto-bound methods in a component. 765 | * 766 | * @param {object} component Component whose method is going to be bound. 767 | */ 768 | function bindAutoBindMethods(component) { 769 | var pairs = component.__reactAutoBindPairs; 770 | for (var i = 0; i < pairs.length; i += 2) { 771 | var autoBindKey = pairs[i]; 772 | var method = pairs[i + 1]; 773 | component[autoBindKey] = bindAutoBindMethod(component, method); 774 | } 775 | } 776 | 777 | var IsMountedPreMixin = { 778 | componentDidMount: function() { 779 | this.__isMounted = true; 780 | } 781 | }; 782 | 783 | var IsMountedPostMixin = { 784 | componentWillUnmount: function() { 785 | this.__isMounted = false; 786 | } 787 | }; 788 | 789 | /** 790 | * Add more to the ReactClass base class. These are all legacy features and 791 | * therefore not already part of the modern ReactComponent. 792 | */ 793 | var ReactClassMixin = { 794 | /** 795 | * TODO: This will be deprecated because state should always keep a consistent 796 | * type signature and the only use case for this, is to avoid that. 797 | */ 798 | replaceState: function(newState, callback) { 799 | this.updater.enqueueReplaceState(this, newState, callback); 800 | }, 801 | 802 | /** 803 | * Checks whether or not this composite component is mounted. 804 | * @return {boolean} True if mounted, false otherwise. 805 | * @protected 806 | * @final 807 | */ 808 | isMounted: function() { 809 | if ("production" !== 'production') { 810 | warning( 811 | this.__didWarnIsMounted, 812 | '%s: isMounted is deprecated. Instead, make sure to clean up ' + 813 | 'subscriptions and pending requests in componentWillUnmount to ' + 814 | 'prevent memory leaks.', 815 | (this.constructor && this.constructor.displayName) || 816 | this.name || 817 | 'Component' 818 | ); 819 | this.__didWarnIsMounted = true; 820 | } 821 | return !!this.__isMounted; 822 | } 823 | }; 824 | 825 | var ReactClassComponent = function() {}; 826 | _assign( 827 | ReactClassComponent.prototype, 828 | ReactComponent.prototype, 829 | ReactClassMixin 830 | ); 831 | 832 | /** 833 | * Creates a composite component class given a class specification. 834 | * See https://facebook.github.io/react/docs/top-level-api.html#react.createclass 835 | * 836 | * @param {object} spec Class specification (which must define `render`). 837 | * @return {function} Component constructor function. 838 | * @public 839 | */ 840 | function createClass(spec) { 841 | // To keep our warnings more understandable, we'll use a little hack here to 842 | // ensure that Constructor.name !== 'Constructor'. This makes sure we don't 843 | // unnecessarily identify a class without displayName as 'Constructor'. 844 | var Constructor = identity(function(props, context, updater) { 845 | // This constructor gets overridden by mocks. The argument is used 846 | // by mocks to assert on what gets mounted. 847 | 848 | if ("production" !== 'production') { 849 | warning( 850 | this instanceof Constructor, 851 | 'Something is calling a React component directly. Use a factory or ' + 852 | 'JSX instead. See: https://fb.me/react-legacyfactory' 853 | ); 854 | } 855 | 856 | // Wire up auto-binding 857 | if (this.__reactAutoBindPairs.length) { 858 | bindAutoBindMethods(this); 859 | } 860 | 861 | this.props = props; 862 | this.context = context; 863 | this.refs = emptyObject; 864 | this.updater = updater || ReactNoopUpdateQueue; 865 | 866 | this.state = null; 867 | 868 | // ReactClasses doesn't have constructors. Instead, they use the 869 | // getInitialState and componentWillMount methods for initialization. 870 | 871 | var initialState = this.getInitialState ? this.getInitialState() : null; 872 | if ("production" !== 'production') { 873 | // We allow auto-mocks to proceed as if they're returning null. 874 | if ( 875 | initialState === undefined && 876 | this.getInitialState._isMockFunction 877 | ) { 878 | // This is probably bad practice. Consider warning here and 879 | // deprecating this convenience. 880 | initialState = null; 881 | } 882 | } 883 | _invariant( 884 | typeof initialState === 'object' && !Array.isArray(initialState), 885 | '%s.getInitialState(): must return an object or null', 886 | Constructor.displayName || 'ReactCompositeComponent' 887 | ); 888 | 889 | this.state = initialState; 890 | }); 891 | Constructor.prototype = new ReactClassComponent(); 892 | Constructor.prototype.constructor = Constructor; 893 | Constructor.prototype.__reactAutoBindPairs = []; 894 | 895 | injectedMixins.forEach(mixSpecIntoComponent.bind(null, Constructor)); 896 | 897 | mixSpecIntoComponent(Constructor, IsMountedPreMixin); 898 | mixSpecIntoComponent(Constructor, spec); 899 | mixSpecIntoComponent(Constructor, IsMountedPostMixin); 900 | 901 | // Initialize the defaultProps property after all mixins have been merged. 902 | if (Constructor.getDefaultProps) { 903 | Constructor.defaultProps = Constructor.getDefaultProps(); 904 | } 905 | 906 | if ("production" !== 'production') { 907 | // This is a tag to indicate that the use of these method names is ok, 908 | // since it's used with createClass. If it's not, then it's likely a 909 | // mistake so we'll warn you to use the static property, property 910 | // initializer or constructor respectively. 911 | if (Constructor.getDefaultProps) { 912 | Constructor.getDefaultProps.isReactClassApproved = {}; 913 | } 914 | if (Constructor.prototype.getInitialState) { 915 | Constructor.prototype.getInitialState.isReactClassApproved = {}; 916 | } 917 | } 918 | 919 | _invariant( 920 | Constructor.prototype.render, 921 | 'createClass(...): Class specification must implement a `render` method.' 922 | ); 923 | 924 | if ("production" !== 'production') { 925 | warning( 926 | !Constructor.prototype.componentShouldUpdate, 927 | '%s has a method called ' + 928 | 'componentShouldUpdate(). Did you mean shouldComponentUpdate()? ' + 929 | 'The name is phrased as a question because the function is ' + 930 | 'expected to return a value.', 931 | spec.displayName || 'A component' 932 | ); 933 | warning( 934 | !Constructor.prototype.componentWillRecieveProps, 935 | '%s has a method called ' + 936 | 'componentWillRecieveProps(). Did you mean componentWillReceiveProps()?', 937 | spec.displayName || 'A component' 938 | ); 939 | } 940 | 941 | // Reduce time spent doing lookups by setting these on the prototype. 942 | for (var methodName in ReactClassInterface) { 943 | if (!Constructor.prototype[methodName]) { 944 | Constructor.prototype[methodName] = null; 945 | } 946 | } 947 | 948 | return Constructor; 949 | } 950 | 951 | return createClass; 952 | } 953 | 954 | module.exports = factory; 955 | 956 | },{"fbjs/lib/emptyObject":5,"fbjs/lib/invariant":6,"fbjs/lib/warning":7,"object-assign":8}],3:[function(require,module,exports){ 957 | /** 958 | * Copyright 2013-present, Facebook, Inc. 959 | * All rights reserved. 960 | * 961 | * This source code is licensed under the BSD-style license found in the 962 | * LICENSE file in the root directory of this source tree. An additional grant 963 | * of patent rights can be found in the PATENTS file in the same directory. 964 | * 965 | */ 966 | 967 | 'use strict'; 968 | 969 | var React = require('react'); 970 | var factory = require('./factory'); 971 | 972 | if (typeof React === 'undefined') { 973 | throw Error( 974 | 'create-react-class could not find the React object. If you are using script tags, ' + 975 | 'make sure that React is being loaded before create-react-class.' 976 | ); 977 | } 978 | 979 | // Hack to grab NoopUpdateQueue from isomorphic React 980 | var ReactNoopUpdateQueue = new React.Component().updater; 981 | 982 | module.exports = factory( 983 | React.Component, 984 | React.isValidElement, 985 | ReactNoopUpdateQueue 986 | ); 987 | 988 | },{"./factory":2,"react":undefined}],4:[function(require,module,exports){ 989 | "use strict"; 990 | 991 | /** 992 | * Copyright (c) 2013-present, Facebook, Inc. 993 | * All rights reserved. 994 | * 995 | * This source code is licensed under the BSD-style license found in the 996 | * LICENSE file in the root directory of this source tree. An additional grant 997 | * of patent rights can be found in the PATENTS file in the same directory. 998 | * 999 | * 1000 | */ 1001 | 1002 | function makeEmptyFunction(arg) { 1003 | return function () { 1004 | return arg; 1005 | }; 1006 | } 1007 | 1008 | /** 1009 | * This function accepts and discards inputs; it has no side effects. This is 1010 | * primarily useful idiomatically for overridable function endpoints which 1011 | * always need to be callable, since JS lacks a null-call idiom ala Cocoa. 1012 | */ 1013 | var emptyFunction = function emptyFunction() {}; 1014 | 1015 | emptyFunction.thatReturns = makeEmptyFunction; 1016 | emptyFunction.thatReturnsFalse = makeEmptyFunction(false); 1017 | emptyFunction.thatReturnsTrue = makeEmptyFunction(true); 1018 | emptyFunction.thatReturnsNull = makeEmptyFunction(null); 1019 | emptyFunction.thatReturnsThis = function () { 1020 | return this; 1021 | }; 1022 | emptyFunction.thatReturnsArgument = function (arg) { 1023 | return arg; 1024 | }; 1025 | 1026 | module.exports = emptyFunction; 1027 | },{}],5:[function(require,module,exports){ 1028 | /** 1029 | * Copyright (c) 2013-present, Facebook, Inc. 1030 | * All rights reserved. 1031 | * 1032 | * This source code is licensed under the BSD-style license found in the 1033 | * LICENSE file in the root directory of this source tree. An additional grant 1034 | * of patent rights can be found in the PATENTS file in the same directory. 1035 | * 1036 | */ 1037 | 1038 | 'use strict'; 1039 | 1040 | var emptyObject = {}; 1041 | 1042 | if ("production" !== 'production') { 1043 | Object.freeze(emptyObject); 1044 | } 1045 | 1046 | module.exports = emptyObject; 1047 | },{}],6:[function(require,module,exports){ 1048 | /** 1049 | * Copyright (c) 2013-present, Facebook, Inc. 1050 | * All rights reserved. 1051 | * 1052 | * This source code is licensed under the BSD-style license found in the 1053 | * LICENSE file in the root directory of this source tree. An additional grant 1054 | * of patent rights can be found in the PATENTS file in the same directory. 1055 | * 1056 | */ 1057 | 1058 | 'use strict'; 1059 | 1060 | /** 1061 | * Use invariant() to assert state which your program assumes to be true. 1062 | * 1063 | * Provide sprintf-style format (only %s is supported) and arguments 1064 | * to provide information about what broke and what you were 1065 | * expecting. 1066 | * 1067 | * The invariant message will be stripped in production, but the invariant 1068 | * will remain to ensure logic does not differ in production. 1069 | */ 1070 | 1071 | var validateFormat = function validateFormat(format) {}; 1072 | 1073 | if ("production" !== 'production') { 1074 | validateFormat = function validateFormat(format) { 1075 | if (format === undefined) { 1076 | throw new Error('invariant requires an error message argument'); 1077 | } 1078 | }; 1079 | } 1080 | 1081 | function invariant(condition, format, a, b, c, d, e, f) { 1082 | validateFormat(format); 1083 | 1084 | if (!condition) { 1085 | var error; 1086 | if (format === undefined) { 1087 | error = new Error('Minified exception occurred; use the non-minified dev environment ' + 'for the full error message and additional helpful warnings.'); 1088 | } else { 1089 | var args = [a, b, c, d, e, f]; 1090 | var argIndex = 0; 1091 | error = new Error(format.replace(/%s/g, function () { 1092 | return args[argIndex++]; 1093 | })); 1094 | error.name = 'Invariant Violation'; 1095 | } 1096 | 1097 | error.framesToPop = 1; // we don't care about invariant's own frame 1098 | throw error; 1099 | } 1100 | } 1101 | 1102 | module.exports = invariant; 1103 | },{}],7:[function(require,module,exports){ 1104 | /** 1105 | * Copyright 2014-2015, Facebook, Inc. 1106 | * All rights reserved. 1107 | * 1108 | * This source code is licensed under the BSD-style license found in the 1109 | * LICENSE file in the root directory of this source tree. An additional grant 1110 | * of patent rights can be found in the PATENTS file in the same directory. 1111 | * 1112 | */ 1113 | 1114 | 'use strict'; 1115 | 1116 | var emptyFunction = require('./emptyFunction'); 1117 | 1118 | /** 1119 | * Similar to invariant but only logs a warning if the condition is not met. 1120 | * This can be used to log issues in development environments in critical 1121 | * paths. Removing the logging code for production environments will keep the 1122 | * same logic and follow the same code paths. 1123 | */ 1124 | 1125 | var warning = emptyFunction; 1126 | 1127 | if ("production" !== 'production') { 1128 | var printWarning = function printWarning(format) { 1129 | for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { 1130 | args[_key - 1] = arguments[_key]; 1131 | } 1132 | 1133 | var argIndex = 0; 1134 | var message = 'Warning: ' + format.replace(/%s/g, function () { 1135 | return args[argIndex++]; 1136 | }); 1137 | if (typeof console !== 'undefined') { 1138 | console.error(message); 1139 | } 1140 | try { 1141 | // --- Welcome to debugging React --- 1142 | // This error was thrown as a convenience so that you can use this stack 1143 | // to find the callsite that caused this warning to fire. 1144 | throw new Error(message); 1145 | } catch (x) {} 1146 | }; 1147 | 1148 | warning = function warning(condition, format) { 1149 | if (format === undefined) { 1150 | throw new Error('`warning(condition, format, ...args)` requires a warning ' + 'message argument'); 1151 | } 1152 | 1153 | if (format.indexOf('Failed Composite propType: ') === 0) { 1154 | return; // Ignore CompositeComponent proptype check. 1155 | } 1156 | 1157 | if (!condition) { 1158 | for (var _len2 = arguments.length, args = Array(_len2 > 2 ? _len2 - 2 : 0), _key2 = 2; _key2 < _len2; _key2++) { 1159 | args[_key2 - 2] = arguments[_key2]; 1160 | } 1161 | 1162 | printWarning.apply(undefined, [format].concat(args)); 1163 | } 1164 | }; 1165 | } 1166 | 1167 | module.exports = warning; 1168 | },{"./emptyFunction":4}],8:[function(require,module,exports){ 1169 | /* 1170 | object-assign 1171 | (c) Sindre Sorhus 1172 | @license MIT 1173 | */ 1174 | 1175 | 'use strict'; 1176 | /* eslint-disable no-unused-vars */ 1177 | var getOwnPropertySymbols = Object.getOwnPropertySymbols; 1178 | var hasOwnProperty = Object.prototype.hasOwnProperty; 1179 | var propIsEnumerable = Object.prototype.propertyIsEnumerable; 1180 | 1181 | function toObject(val) { 1182 | if (val === null || val === undefined) { 1183 | throw new TypeError('Object.assign cannot be called with null or undefined'); 1184 | } 1185 | 1186 | return Object(val); 1187 | } 1188 | 1189 | function shouldUseNative() { 1190 | try { 1191 | if (!Object.assign) { 1192 | return false; 1193 | } 1194 | 1195 | // Detect buggy property enumeration order in older V8 versions. 1196 | 1197 | // https://bugs.chromium.org/p/v8/issues/detail?id=4118 1198 | var test1 = new String('abc'); // eslint-disable-line no-new-wrappers 1199 | test1[5] = 'de'; 1200 | if (Object.getOwnPropertyNames(test1)[0] === '5') { 1201 | return false; 1202 | } 1203 | 1204 | // https://bugs.chromium.org/p/v8/issues/detail?id=3056 1205 | var test2 = {}; 1206 | for (var i = 0; i < 10; i++) { 1207 | test2['_' + String.fromCharCode(i)] = i; 1208 | } 1209 | var order2 = Object.getOwnPropertyNames(test2).map(function (n) { 1210 | return test2[n]; 1211 | }); 1212 | if (order2.join('') !== '0123456789') { 1213 | return false; 1214 | } 1215 | 1216 | // https://bugs.chromium.org/p/v8/issues/detail?id=3056 1217 | var test3 = {}; 1218 | 'abcdefghijklmnopqrst'.split('').forEach(function (letter) { 1219 | test3[letter] = letter; 1220 | }); 1221 | if (Object.keys(Object.assign({}, test3)).join('') !== 1222 | 'abcdefghijklmnopqrst') { 1223 | return false; 1224 | } 1225 | 1226 | return true; 1227 | } catch (err) { 1228 | // We don't expect any of the above to throw, but better to be safe. 1229 | return false; 1230 | } 1231 | } 1232 | 1233 | module.exports = shouldUseNative() ? Object.assign : function (target, source) { 1234 | var from; 1235 | var to = toObject(target); 1236 | var symbols; 1237 | 1238 | for (var s = 1; s < arguments.length; s++) { 1239 | from = Object(arguments[s]); 1240 | 1241 | for (var key in from) { 1242 | if (hasOwnProperty.call(from, key)) { 1243 | to[key] = from[key]; 1244 | } 1245 | } 1246 | 1247 | if (getOwnPropertySymbols) { 1248 | symbols = getOwnPropertySymbols(from); 1249 | for (var i = 0; i < symbols.length; i++) { 1250 | if (propIsEnumerable.call(from, symbols[i])) { 1251 | to[symbols[i]] = from[symbols[i]]; 1252 | } 1253 | } 1254 | } 1255 | } 1256 | 1257 | return to; 1258 | }; 1259 | 1260 | },{}]},{},[1]); 1261 | -------------------------------------------------------------------------------- /example/src/example.js: -------------------------------------------------------------------------------- 1 | var createReactClass = require('create-react-class'); 2 | var React = require('react'); 3 | var ReactDOM = require('react-dom'); 4 | var Tappable = require('react-tappable'); 5 | 6 | var App = createReactClass({ 7 | getInitialState: function () { 8 | return { 9 | scrolling: false, 10 | events: [] 11 | }; 12 | }, 13 | componentDidUpdate: function () { 14 | var log = this.refs.eventLog; 15 | log.scrollTop = log.scrollHeight; 16 | }, 17 | handleEvent: function (name/*, event*/) { 18 | var events = this.state.events; 19 | events.push(name); 20 | this.setState({ 21 | events: events 22 | }); 23 | }, 24 | toggleScrolling: function () { 25 | console.log('scrolling: ' + !this.state.scrolling); 26 | this.setState({ 27 | scrolling: !this.state.scrolling 28 | }); 29 | }, 30 | render: function () { 31 | var events = { 32 | onTap: this.handleEvent.bind(this, 'tap'), 33 | onPress: this.handleEvent.bind(this, 'press'), 34 | onTouchStart: this.handleEvent.bind(this, 'touchStart'), 35 | // onTouchMove: this.handleEvent.bind(this, 'touchMove'), 36 | onTouchEnd: this.handleEvent.bind(this, 'touchEnd'), 37 | onMouseDown: this.handleEvent.bind(this, 'mouseDown'), 38 | // onMouseMove: this.handleEvent.bind(this, 'mouseMove'), 39 | onMouseUp: this.handleEvent.bind(this, 'mouseUp'), 40 | onMouseOut: this.handleEvent.bind(this, 'mouseOut'), 41 | onKeyDown: this.handleEvent.bind(this, 'keyDown'), 42 | onKeyUp: this.handleEvent.bind(this, 'keyUp') 43 | }; 44 | var nestedEvents = { 45 | onTap: this.handleEvent.bind(this, 'tap (nested)') 46 | }; 47 | var toggleClass = this.state.scrolling ? 'scrolling-enabled' : 'scrolling-disabled'; 48 | return ( 49 |
50 |
51 | 52 | Toggle Scrolling: {this.state.scrolling ? 'on' : 'off'} 53 | 54 |
55 |
56 |

Tappable area:

57 | Tappable Button 58 | 59 | Touch me 60 | Nested Tappable 61 | 62 |
63 |
64 |

Event log:

65 |
66 | {this.state.events.map(function (ev, i) { 67 | return
{ev}
; 68 | })} 69 |
70 |
71 |
72 | ); 73 | } 74 | }); 75 | 76 | ReactDOM.render(, document.getElementById('app')); 77 | -------------------------------------------------------------------------------- /example/src/example.less: -------------------------------------------------------------------------------- 1 | /* 2 | // Examples Stylesheet 3 | // ------------------- 4 | */ 5 | 6 | body { 7 | font-family: Helvetica Neue, Helvetica, Arial, sans-serif; 8 | font-size: 14px; 9 | color: #333; 10 | margin: 0; 11 | padding: 0; 12 | } 13 | 14 | a { 15 | color: #08c; 16 | text-decoration: none; 17 | } 18 | 19 | a:hover { 20 | text-decoration: underline; 21 | } 22 | 23 | button { 24 | border-radius: 5px; 25 | background: #eee; 26 | border: 1px solid #666; 27 | margin-bottom: 10px; 28 | } 29 | 30 | .container { 31 | margin-left: auto; 32 | margin-right: auto; 33 | max-width: 400px; 34 | padding: 1em; 35 | } 36 | 37 | .footer { 38 | margin-top: 50px; 39 | border-top: 1px solid #eee; 40 | padding: 20px 0; 41 | font-size: 12px; 42 | color: #999; 43 | } 44 | 45 | h1, h2, h3, h4, h5, h6 { 46 | color: #222; 47 | font-weight: 100; 48 | margin: 0.5em 0; 49 | } 50 | 51 | .example { 52 | overflow: hidden; 53 | } 54 | 55 | .scrolling { 56 | margin: 5px 0; 57 | 58 | .link { 59 | text-decoration: underline; 60 | } 61 | 62 | .Tappable-active { 63 | opacity: 0.8; 64 | background: white 65 | } 66 | } 67 | 68 | .left { 69 | float: left; 70 | width: 120px; 71 | height: 200px; 72 | overflow: scroll; 73 | } 74 | 75 | .tappable-area { 76 | width: 80px; 77 | height: 460px; 78 | padding: 20px; 79 | text-align: center; 80 | background: #eee; 81 | cursor: default; 82 | } 83 | 84 | .nested-tappable { 85 | display: block; 86 | margin: 15px 5px; 87 | padding: 5px; 88 | border: 1px solid #ccc; 89 | background: #ddd; 90 | } 91 | 92 | .Tappable-active { 93 | background: #5cc2f8; 94 | } 95 | 96 | .right { 97 | float: left; 98 | width: 180px; 99 | padding-left: 20px; 100 | } 101 | 102 | .event-log { 103 | height: 180px; 104 | overflow: auto; 105 | } 106 | 107 | .hint { 108 | margin: 15px 0; 109 | font-style: italic; 110 | color: #999; 111 | } 112 | -------------------------------------------------------------------------------- /example/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | React-Tappable Example 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |

React Tappable

15 |

View project on GitHub

16 |
17 |
18 | Events in the area on the left will be logged on the right. 19 | Toggle scrolling to test event cancel on scroll (touch devices only). 20 |
21 | 24 |
25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /example/src/pinch.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | React-Tappable Example 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |

React Tappable

15 |

View project on GitHub

16 |
17 |
18 | Events in the area on the left will be logged on the right. 19 | Toggle scrolling to test event cancel on scroll (touch devices only). 20 |
21 | 24 |
25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /example/src/pinch.js: -------------------------------------------------------------------------------- 1 | var createReactClass = require('create-react-class'); 2 | var React = require('react'); 3 | var ReactDOM = require('react-dom'); 4 | var Tappable = require('react-tappable'); 5 | 6 | var App = createReactClass({ 7 | getInitialState: function () { 8 | return { 9 | events: [] 10 | }; 11 | }, 12 | componentDidUpdate: function () { 13 | var log = this.refs.eventLog; 14 | log.scrollTop = log.scrollHeight; 15 | }, 16 | handleEvent: function (name/*, event*/) { 17 | var events = this.state.events; 18 | events.push(name); 19 | this.setState({ 20 | events: events 21 | }); 22 | }, 23 | render: function () { 24 | var nestedEvents = { 25 | onPinchStart: this.handleEvent.bind(this, 'pinch start'), 26 | onPinchMove: this.handleEvent.bind(this, 'pinch move'), 27 | onPinchEnd: this.handleEvent.bind(this, 'pinch end') 28 | }; 29 | return ( 30 |
31 |
32 |

Tappable area:

33 |
34 | Touch me 35 | Nested Pinchable 36 |
37 |
38 |
39 |

Event log:

40 |
41 | {this.state.events.map(function (ev, i) { 42 | return
{ev}
; 43 | })} 44 |
45 |
46 |
47 | ); 48 | } 49 | }); 50 | 51 | ReactDOM.render(, document.getElementById('app')); 52 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var initGulpTasks = require('react-component-gulp-tasks'); 3 | 4 | var taskConfig = { 5 | 6 | component: { 7 | name: 'Tappable', 8 | dependencies: [ 9 | 'react', 10 | 'react-dom' 11 | ] 12 | }, 13 | 14 | example: { 15 | src: 'example/src', 16 | dist: 'example/dist', 17 | files: [ 18 | 'index.html', 19 | 'pinch.html', 20 | '.gitignore' 21 | ], 22 | scripts: [ 23 | 'example.js', 24 | 'pinch.js' 25 | ], 26 | less: [ 27 | 'example.less' 28 | ] 29 | } 30 | 31 | }; 32 | 33 | initGulpTasks(gulp, taskConfig); 34 | -------------------------------------------------------------------------------- /lib/Pinchable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var PinchableBaseMixin = require('./PinchableBaseMixin'); 4 | var PinchableMixin = require('./PinchableMixin'); 5 | var getComponent = require('./getComponent'); 6 | var touchStyles = require('./touchStyles'); 7 | 8 | var Component = getComponent([PinchableBaseMixin, PinchableMixin]); 9 | 10 | module.exports = Component; 11 | module.exports.touchStyles = touchStyles; -------------------------------------------------------------------------------- /lib/PinchableBaseMixin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var PropTypes = require('prop-types'); 4 | var React = require('react'); 5 | 6 | var Mixin = { 7 | propTypes: { 8 | preventDefault: PropTypes.bool, // whether to preventDefault on all events 9 | stopPropagation: PropTypes.bool, // whether to stopPropagation on all events 10 | 11 | onTouchStart: PropTypes.func, // pass-through touch event 12 | onTouchMove: PropTypes.func, // pass-through touch event 13 | onTouchEnd: PropTypes.func // pass-through touch event 14 | }, 15 | 16 | getInitialState: function getInitialState() { 17 | return { 18 | isActive: false 19 | }; 20 | }, 21 | 22 | processEvent: function processEvent(event) { 23 | if (this.props.preventDefault) event.preventDefault(); 24 | if (this.props.stopPropagation) event.stopPropagation(); 25 | }, 26 | 27 | onTouchStart: function onTouchStart(event) { 28 | if (this.props.onTouchStart && this.props.onTouchStart(event) === false) return; 29 | this.processEvent(event); 30 | 31 | if (this.onPinchStart && (this.props.onPinchStart || this.props.onPinchMove || this.props.onPinchEnd) && event.touches.length === 2) { 32 | this.onPinchStart && this.onPinchStart(event); 33 | } 34 | }, 35 | 36 | onTouchMove: function onTouchMove(event) { 37 | if (this._initialPinch && event.touches.length === 2 && this.onPinchMove) { 38 | this.onPinchMove(event); 39 | event.preventDefault(); 40 | } 41 | }, 42 | 43 | onTouchEnd: function onTouchEnd(event) { 44 | if (this.onPinchEnd && this._initialPinch && event.touches.length + event.changedTouches.length === 2) { 45 | this.onPinchEnd(event); 46 | event.preventDefault(); 47 | } 48 | }, 49 | 50 | endTouch: function endTouch(event, callback) { 51 | this._initialTouch = null; 52 | this._lastTouch = null; 53 | if (this.state.isActive) { 54 | this.setState({ 55 | isActive: false 56 | }, callback); 57 | } else if (callback) { 58 | callback(); 59 | } 60 | }, 61 | 62 | handlers: function handlers() { 63 | return { 64 | onTouchStart: this.onTouchStart, 65 | onTouchMove: this.onTouchMove, 66 | onTouchEnd: this.onTouchEnd, 67 | onMouseDown: this.onMouseDown, 68 | onMouseUp: this.onMouseUp, 69 | onMouseMove: this.onMouseMove, 70 | onMouseOut: this.onMouseOut 71 | }; 72 | } 73 | }; 74 | 75 | module.exports = Mixin; -------------------------------------------------------------------------------- /lib/PinchableMixin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var PropTypes = require('prop-types'); 4 | var React = require('react'); 5 | 6 | function getPinchProps(touches) { 7 | return { 8 | touches: Array.prototype.map.call(touches, function copyTouch(touch) { 9 | return { identifier: touch.identifier, pageX: touch.pageX, pageY: touch.pageY }; 10 | }), 11 | center: { x: (touches[0].pageX + touches[1].pageX) / 2, y: (touches[0].pageY + touches[1].pageY) / 2 }, 12 | angle: Math.atan() * (touches[1].pageY - touches[0].pageY) / (touches[1].pageX - touches[0].pageX) * 180 / Math.PI, 13 | distance: Math.sqrt(Math.pow(Math.abs(touches[1].pageX - touches[0].pageX), 2) + Math.pow(Math.abs(touches[1].pageY - touches[0].pageY), 2)) 14 | }; 15 | } 16 | 17 | var Mixin = { 18 | propTypes: { 19 | onPinchStart: PropTypes.func, // fires when a pinch gesture is started 20 | onPinchMove: PropTypes.func, // fires on every touch-move when a pinch action is active 21 | onPinchEnd: PropTypes.func // fires when a pinch action ends 22 | }, 23 | 24 | onPinchStart: function onPinchStart(event) { 25 | // in case the two touches didn't start exactly at the same time 26 | if (this._initialTouch) { 27 | this.endTouch(); 28 | } 29 | var touches = event.touches; 30 | this._initialPinch = getPinchProps(touches); 31 | this._initialPinch = Object.assign(this._initialPinch, { 32 | displacement: { x: 0, y: 0 }, 33 | displacementVelocity: { x: 0, y: 0 }, 34 | rotation: 0, 35 | rotationVelocity: 0, 36 | zoom: 1, 37 | zoomVelocity: 0, 38 | time: Date.now() 39 | }); 40 | this._lastPinch = this._initialPinch; 41 | this.props.onPinchStart && this.props.onPinchStart(this._initialPinch, event); 42 | }, 43 | 44 | onPinchMove: function onPinchMove(event) { 45 | if (this._initialTouch) { 46 | this.endTouch(); 47 | } 48 | var touches = event.touches; 49 | if (touches.length !== 2) { 50 | return this.onPinchEnd(event); // bail out before disaster 51 | } 52 | 53 | var currentPinch = touches[0].identifier === this._initialPinch.touches[0].identifier && touches[1].identifier === this._initialPinch.touches[1].identifier ? getPinchProps(touches) // the touches are in the correct order 54 | : touches[1].identifier === this._initialPinch.touches[0].identifier && touches[0].identifier === this._initialPinch.touches[1].identifier ? getPinchProps(touches.reverse()) // the touches have somehow changed order 55 | : getPinchProps(touches); // something is wrong, but we still have two touch-points, so we try not to fail 56 | 57 | currentPinch.displacement = { 58 | x: currentPinch.center.x - this._initialPinch.center.x, 59 | y: currentPinch.center.y - this._initialPinch.center.y 60 | }; 61 | 62 | currentPinch.time = Date.now(); 63 | var timeSinceLastPinch = currentPinch.time - this._lastPinch.time; 64 | 65 | currentPinch.displacementVelocity = { 66 | x: (currentPinch.displacement.x - this._lastPinch.displacement.x) / timeSinceLastPinch, 67 | y: (currentPinch.displacement.y - this._lastPinch.displacement.y) / timeSinceLastPinch 68 | }; 69 | 70 | currentPinch.rotation = currentPinch.angle - this._initialPinch.angle; 71 | currentPinch.rotationVelocity = currentPinch.rotation - this._lastPinch.rotation / timeSinceLastPinch; 72 | 73 | currentPinch.zoom = currentPinch.distance / this._initialPinch.distance; 74 | currentPinch.zoomVelocity = (currentPinch.zoom - this._lastPinch.zoom) / timeSinceLastPinch; 75 | 76 | this.props.onPinchMove && this.props.onPinchMove(currentPinch, event); 77 | 78 | this._lastPinch = currentPinch; 79 | }, 80 | 81 | onPinchEnd: function onPinchEnd(event) { 82 | // TODO use helper to order touches by identifier and use actual values on touchEnd. 83 | var currentPinch = Object.assign({}, this._lastPinch); 84 | currentPinch.time = Date.now(); 85 | 86 | if (currentPinch.time - this._lastPinch.time > 16) { 87 | currentPinch.displacementVelocity = 0; 88 | currentPinch.rotationVelocity = 0; 89 | currentPinch.zoomVelocity = 0; 90 | } 91 | 92 | this.props.onPinchEnd && this.props.onPinchEnd(currentPinch, event); 93 | 94 | this._initialPinch = this._lastPinch = null; 95 | 96 | // If one finger is still on screen, it should start a new touch event for swiping etc 97 | // But it should never fire an onTap or onPress event. 98 | // Since there is no support swipes yet, this should be disregarded for now 99 | // if (event.touches.length === 1) { 100 | // this.onTouchStart(event); 101 | // } 102 | } 103 | }; 104 | 105 | module.exports = Mixin; -------------------------------------------------------------------------------- /lib/TapAndPinchable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var TappableMixin = require('./TappableMixin'); 4 | var PinchableMixin = require('./PinchableMixin'); 5 | var getComponent = require('./getComponent'); 6 | var touchStyles = require('./touchStyles'); 7 | 8 | var Component = getComponent([TappableMixin, PinchableMixin]); 9 | 10 | module.exports = Component; 11 | module.exports.touchStyles = touchStyles; 12 | module.exports.Mixin = Object.assign({}, TappableMixin, { 13 | onPinchStart: PinchableMixin.onPinchStart, 14 | onPinchMove: PinchableMixin.onPinchMove, 15 | onPinchEnd: PinchableMixin.onPinchEnd 16 | }); -------------------------------------------------------------------------------- /lib/Tappable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var TappableMixin = require('./TappableMixin'); 4 | var getComponent = require('./getComponent'); 5 | var touchStyles = require('./touchStyles'); 6 | 7 | var Component = getComponent([TappableMixin]); 8 | 9 | module.exports = Component; 10 | module.exports.touchStyles = touchStyles; 11 | module.exports.Mixin = TappableMixin; -------------------------------------------------------------------------------- /lib/TappableMixin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var PropTypes = require('prop-types'); 4 | var React = require('react'); 5 | var ReactDOM = require('react-dom'); 6 | 7 | var SPACE_KEY = 32; 8 | var ENTER_KEY = 13; 9 | 10 | function getTouchProps(touch) { 11 | if (!touch) return {}; 12 | return { 13 | pageX: touch.pageX, 14 | pageY: touch.pageY, 15 | clientX: touch.clientX, 16 | clientY: touch.clientY 17 | }; 18 | } 19 | 20 | var Mixin = { 21 | propTypes: { 22 | moveThreshold: PropTypes.number, // pixels to move before cancelling tap 23 | moveXThreshold: PropTypes.number, // pixels on the x axis to move before cancelling tap (overrides moveThreshold) 24 | moveYThreshold: PropTypes.number, // pixels on the y axis to move before cancelling tap (overrides moveThreshold) 25 | allowReactivation: PropTypes.bool, // after moving outside of the moveThreshold will you allow 26 | // reactivation by moving back within the moveThreshold? 27 | activeDelay: PropTypes.number, // ms to wait before adding the `-active` class 28 | pressDelay: PropTypes.number, // ms to wait before detecting a press 29 | pressMoveThreshold: PropTypes.number, // pixels to move before cancelling press 30 | preventDefault: PropTypes.bool, // whether to preventDefault on all events 31 | stopPropagation: PropTypes.bool, // whether to stopPropagation on all events 32 | 33 | onTap: PropTypes.func, // fires when a tap is detected 34 | onPress: PropTypes.func, // fires when a press is detected 35 | onTouchStart: PropTypes.func, // pass-through touch event 36 | onTouchMove: PropTypes.func, // pass-through touch event 37 | onTouchEnd: PropTypes.func, // pass-through touch event 38 | onMouseDown: PropTypes.func, // pass-through mouse event 39 | onMouseUp: PropTypes.func, // pass-through mouse event 40 | onMouseMove: PropTypes.func, // pass-through mouse event 41 | onMouseOut: PropTypes.func, // pass-through mouse event 42 | onKeyDown: PropTypes.func, // pass-through key event 43 | onKeyUp: PropTypes.func // pass-through key event 44 | }, 45 | 46 | getDefaultProps: function getDefaultProps() { 47 | return { 48 | activeDelay: 0, 49 | allowReactivation: true, 50 | moveThreshold: 100, 51 | pressDelay: 1000, 52 | pressMoveThreshold: 5 53 | }; 54 | }, 55 | 56 | getInitialState: function getInitialState() { 57 | return { 58 | isActive: false, 59 | touchActive: false, 60 | pinchActive: false 61 | }; 62 | }, 63 | 64 | componentDidMount: function componentDidMount() { 65 | this.isMounted = true; 66 | }, 67 | 68 | componentWillUnmount: function componentWillUnmount() { 69 | this.isMounted = false; 70 | this.cleanupScrollDetection(); 71 | this.cancelPressDetection(); 72 | this.clearActiveTimeout(); 73 | }, 74 | 75 | componentWillUpdate: function componentWillUpdate(nextProps, nextState) { 76 | if (this.state.isActive && !nextState.isActive) { 77 | this.props.onDeactivate && this.props.onDeactivate(); 78 | } else if (!this.state.isActive && nextState.isActive) { 79 | this.props.onReactivate && this.props.onReactivate(); 80 | } 81 | }, 82 | 83 | processEvent: function processEvent(event) { 84 | if (this.props.preventDefault) event.preventDefault(); 85 | if (this.props.stopPropagation) event.stopPropagation(); 86 | }, 87 | 88 | onTouchStart: function onTouchStart(event) { 89 | if (this.props.onTouchStart && this.props.onTouchStart(event) === false) return; 90 | this.processEvent(event); 91 | window._blockMouseEvents = true; 92 | if (event.touches.length === 1) { 93 | this._initialTouch = this._lastTouch = getTouchProps(event.touches[0]); 94 | this.initScrollDetection(); 95 | this.initPressDetection(event, this.endTouch); 96 | this.initTouchmoveDetection(); 97 | if (this.props.activeDelay > 0) { 98 | this._activeTimeout = setTimeout(this.makeActive, this.props.activeDelay); 99 | } else { 100 | this.makeActive(); 101 | } 102 | } else if (this.onPinchStart && (this.props.onPinchStart || this.props.onPinchMove || this.props.onPinchEnd) && event.touches.length === 2) { 103 | this.onPinchStart(event); 104 | } 105 | }, 106 | 107 | makeActive: function makeActive() { 108 | if (!this.isMounted) return; 109 | this.clearActiveTimeout(); 110 | this.setState({ 111 | isActive: true 112 | }); 113 | }, 114 | 115 | clearActiveTimeout: function clearActiveTimeout() { 116 | clearTimeout(this._activeTimeout); 117 | this._activeTimeout = false; 118 | }, 119 | 120 | initScrollDetection: function initScrollDetection() { 121 | this._scrollPos = { top: 0, left: 0 }; 122 | this._scrollParents = []; 123 | this._scrollParentPos = []; 124 | var node = ReactDOM.findDOMNode(this); 125 | 126 | while (node) { 127 | if (node.scrollHeight > node.offsetHeight || node.scrollWidth > node.offsetWidth) { 128 | this._scrollParents.push(node); 129 | this._scrollParentPos.push(node.scrollTop + node.scrollLeft); 130 | this._scrollPos.top += node.scrollTop; 131 | this._scrollPos.left += node.scrollLeft; 132 | } 133 | 134 | node = node.parentNode; 135 | } 136 | }, 137 | 138 | initTouchmoveDetection: function initTouchmoveDetection() { 139 | this._touchmoveTriggeredTimes = 0; 140 | }, 141 | 142 | cancelTouchmoveDetection: function cancelTouchmoveDetection() { 143 | if (this._touchmoveDetectionTimeout) { 144 | clearTimeout(this._touchmoveDetectionTimeout); 145 | this._touchmoveDetectionTimeout = null; 146 | this._touchmoveTriggeredTimes = 0; 147 | } 148 | }, 149 | 150 | calculateMovement: function calculateMovement(touch) { 151 | return { 152 | x: Math.abs(touch.clientX - this._initialTouch.clientX), 153 | y: Math.abs(touch.clientY - this._initialTouch.clientY) 154 | }; 155 | }, 156 | 157 | detectScroll: function detectScroll() { 158 | var currentScrollPos = { top: 0, left: 0 }; 159 | for (var i = 0; i < this._scrollParents.length; i++) { 160 | currentScrollPos.top += this._scrollParents[i].scrollTop; 161 | currentScrollPos.left += this._scrollParents[i].scrollLeft; 162 | } 163 | return !(currentScrollPos.top === this._scrollPos.top && currentScrollPos.left === this._scrollPos.left); 164 | }, 165 | 166 | cleanupScrollDetection: function cleanupScrollDetection() { 167 | this._scrollParents = undefined; 168 | this._scrollPos = undefined; 169 | }, 170 | 171 | initPressDetection: function initPressDetection(event, callback) { 172 | if (!this.props.onPress) return; 173 | 174 | // SyntheticEvent objects are pooled, so persist the event so it can be referenced asynchronously 175 | event.persist(); 176 | 177 | this._pressTimeout = setTimeout(function () { 178 | this.props.onPress(event); 179 | callback(); 180 | }.bind(this), this.props.pressDelay); 181 | }, 182 | 183 | cancelPressDetection: function cancelPressDetection() { 184 | clearTimeout(this._pressTimeout); 185 | }, 186 | 187 | onTouchMove: function onTouchMove(event) { 188 | if (this._initialTouch) { 189 | this.processEvent(event); 190 | 191 | if (this.detectScroll()) { 192 | return this.endTouch(event); 193 | } else { 194 | if (this._touchmoveTriggeredTimes++ === 0) { 195 | this._touchmoveDetectionTimeout = setTimeout(function () { 196 | if (this._touchmoveTriggeredTimes === 1) { 197 | this.endTouch(event); 198 | } 199 | }.bind(this), 64); 200 | } 201 | } 202 | 203 | this.props.onTouchMove && this.props.onTouchMove(event); 204 | this._lastTouch = getTouchProps(event.touches[0]); 205 | var movement = this.calculateMovement(this._lastTouch); 206 | if (movement.x > this.props.pressMoveThreshold || movement.y > this.props.pressMoveThreshold) { 207 | this.cancelPressDetection(); 208 | } 209 | if (movement.x > (this.props.moveXThreshold || this.props.moveThreshold) || movement.y > (this.props.moveYThreshold || this.props.moveThreshold)) { 210 | if (this.state.isActive) { 211 | if (this.props.allowReactivation) { 212 | this.setState({ 213 | isActive: false 214 | }); 215 | } else { 216 | return this.endTouch(event); 217 | } 218 | } else if (this._activeTimeout) { 219 | this.clearActiveTimeout(); 220 | } 221 | } else { 222 | if (!this.state.isActive && !this._activeTimeout) { 223 | this.setState({ 224 | isActive: true 225 | }); 226 | } 227 | } 228 | } else if (this._initialPinch && event.touches.length === 2 && this.onPinchMove) { 229 | this.onPinchMove(event); 230 | event.preventDefault(); 231 | } 232 | }, 233 | 234 | onTouchEnd: function onTouchEnd(event) { 235 | var _this = this; 236 | 237 | if (this._initialTouch) { 238 | this.processEvent(event); 239 | var afterEndTouch; 240 | var movement = this.calculateMovement(this._lastTouch); 241 | if (movement.x <= (this.props.moveXThreshold || this.props.moveThreshold) && movement.y <= (this.props.moveYThreshold || this.props.moveThreshold) && this.props.onTap) { 242 | event.preventDefault(); 243 | afterEndTouch = function afterEndTouch() { 244 | var finalParentScrollPos = _this._scrollParents.map(function (node) { 245 | return node.scrollTop + node.scrollLeft; 246 | }); 247 | var stoppedMomentumScroll = _this._scrollParentPos.some(function (end, i) { 248 | return end !== finalParentScrollPos[i]; 249 | }); 250 | if (!stoppedMomentumScroll) { 251 | _this.props.onTap(event); 252 | } 253 | }; 254 | } 255 | this.endTouch(event, afterEndTouch); 256 | } else if (this.onPinchEnd && this._initialPinch && event.touches.length + event.changedTouches.length === 2) { 257 | this.onPinchEnd(event); 258 | event.preventDefault(); 259 | } 260 | }, 261 | 262 | endTouch: function endTouch(event, callback) { 263 | this.cancelTouchmoveDetection(); 264 | this.cancelPressDetection(); 265 | this.clearActiveTimeout(); 266 | if (event && this.props.onTouchEnd) { 267 | this.props.onTouchEnd(event); 268 | } 269 | this._initialTouch = null; 270 | this._lastTouch = null; 271 | if (callback) { 272 | callback(); 273 | } 274 | if (this.state.isActive) { 275 | this.setState({ 276 | isActive: false 277 | }); 278 | } 279 | }, 280 | 281 | onMouseDown: function onMouseDown(event) { 282 | if (window._blockMouseEvents) { 283 | window._blockMouseEvents = false; 284 | return; 285 | } 286 | if (this.props.onMouseDown && this.props.onMouseDown(event) === false) return; 287 | this.processEvent(event); 288 | this.initPressDetection(event, this.endMouseEvent); 289 | this._mouseDown = true; 290 | this.setState({ 291 | isActive: true 292 | }); 293 | }, 294 | 295 | onMouseMove: function onMouseMove(event) { 296 | if (window._blockMouseEvents || !this._mouseDown) return; 297 | this.processEvent(event); 298 | this.props.onMouseMove && this.props.onMouseMove(event); 299 | }, 300 | 301 | onMouseUp: function onMouseUp(event) { 302 | if (window._blockMouseEvents || !this._mouseDown) return; 303 | this.processEvent(event); 304 | this.props.onMouseUp && this.props.onMouseUp(event); 305 | this.props.onTap && this.props.onTap(event); 306 | this.endMouseEvent(); 307 | }, 308 | 309 | onMouseOut: function onMouseOut(event) { 310 | if (window._blockMouseEvents || !this._mouseDown) return; 311 | this.processEvent(event); 312 | this.props.onMouseOut && this.props.onMouseOut(event); 313 | this.endMouseEvent(); 314 | }, 315 | 316 | endMouseEvent: function endMouseEvent() { 317 | this.cancelPressDetection(); 318 | this._mouseDown = false; 319 | this.setState({ 320 | isActive: false 321 | }); 322 | }, 323 | 324 | onKeyUp: function onKeyUp(event) { 325 | if (!this._keyDown) return; 326 | this.processEvent(event); 327 | this.props.onKeyUp && this.props.onKeyUp(event); 328 | this.props.onTap && this.props.onTap(event); 329 | this._keyDown = false; 330 | this.cancelPressDetection(); 331 | this.setState({ 332 | isActive: false 333 | }); 334 | }, 335 | 336 | onKeyDown: function onKeyDown(event) { 337 | if (this.props.onKeyDown && this.props.onKeyDown(event) === false) return; 338 | if (event.which !== SPACE_KEY && event.which !== ENTER_KEY) return; 339 | if (this._keyDown) return; 340 | this.initPressDetection(event, this.endKeyEvent); 341 | this.processEvent(event); 342 | this._keyDown = true; 343 | this.setState({ 344 | isActive: true 345 | }); 346 | }, 347 | 348 | endKeyEvent: function endKeyEvent() { 349 | this.cancelPressDetection(); 350 | this._keyDown = false; 351 | this.setState({ 352 | isActive: false 353 | }); 354 | }, 355 | 356 | cancelTap: function cancelTap() { 357 | this.endTouch(); 358 | this._mouseDown = false; 359 | }, 360 | 361 | handlers: function handlers() { 362 | return { 363 | onTouchStart: this.onTouchStart, 364 | onTouchMove: this.onTouchMove, 365 | onTouchEnd: this.onTouchEnd, 366 | onMouseDown: this.onMouseDown, 367 | onMouseUp: this.onMouseUp, 368 | onMouseMove: this.onMouseMove, 369 | onMouseOut: this.onMouseOut, 370 | onKeyDown: this.onKeyDown, 371 | onKeyUp: this.onKeyUp 372 | }; 373 | } 374 | }; 375 | 376 | module.exports = Mixin; -------------------------------------------------------------------------------- /lib/getComponent.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var createReactClass = require('create-react-class'); 4 | var PropTypes = require('prop-types'); 5 | var React = require('react'); 6 | var touchStyles = require('./touchStyles'); 7 | 8 | /** 9 | * Tappable Component 10 | * ================== 11 | */ 12 | module.exports = function (mixins) { 13 | return createReactClass({ 14 | displayName: 'Tappable', 15 | 16 | mixins: mixins, 17 | 18 | propTypes: { 19 | component: PropTypes.any, // component to create 20 | className: PropTypes.string, // optional className 21 | classBase: PropTypes.string, // base for generated classNames 22 | classes: PropTypes.object, // object containing the active and inactive class names 23 | style: PropTypes.object, // additional style properties for the component 24 | disabled: PropTypes.bool // only applies to buttons 25 | }, 26 | 27 | getDefaultProps: function getDefaultProps() { 28 | return { 29 | component: 'span', 30 | classBase: 'Tappable' 31 | }; 32 | }, 33 | 34 | render: function render() { 35 | var props = this.props; 36 | var className = props.classBase + (this.state.isActive ? '-active' : '-inactive'); 37 | 38 | if (props.className) { 39 | className += ' ' + props.className; 40 | } 41 | 42 | if (props.classes) { 43 | className += ' ' + (this.state.isActive ? props.classes.active : props.classes.inactive); 44 | } 45 | 46 | var style = {}; 47 | Object.assign(style, touchStyles, props.style); 48 | 49 | var newComponentProps = Object.assign({}, props, { 50 | style: style, 51 | className: className, 52 | disabled: props.disabled, 53 | handlers: this.handlers 54 | }, this.handlers()); 55 | 56 | delete newComponentProps.activeDelay; 57 | delete newComponentProps.allowReactivation; 58 | delete newComponentProps.classBase; 59 | delete newComponentProps.classes; 60 | delete newComponentProps.handlers; 61 | delete newComponentProps.onTap; 62 | delete newComponentProps.onPress; 63 | delete newComponentProps.onPinchStart; 64 | delete newComponentProps.onPinchMove; 65 | delete newComponentProps.onPinchEnd; 66 | delete newComponentProps.onDeactivate; 67 | delete newComponentProps.onReactivate; 68 | delete newComponentProps.moveThreshold; 69 | delete newComponentProps.moveXThreshold; 70 | delete newComponentProps.moveYThreshold; 71 | delete newComponentProps.pressDelay; 72 | delete newComponentProps.pressMoveThreshold; 73 | delete newComponentProps.preventDefault; 74 | delete newComponentProps.stopPropagation; 75 | delete newComponentProps.component; 76 | 77 | return React.createElement(props.component, newComponentProps, props.children); 78 | } 79 | }); 80 | }; -------------------------------------------------------------------------------- /lib/touchStyles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var touchStyles = { 4 | WebkitTapHighlightColor: 'rgba(0,0,0,0)', 5 | WebkitTouchCallout: 'none', 6 | WebkitUserSelect: 'none', 7 | KhtmlUserSelect: 'none', 8 | MozUserSelect: 'none', 9 | msUserSelect: 'none', 10 | userSelect: 'none', 11 | cursor: 'pointer' 12 | }; 13 | 14 | module.exports = touchStyles; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-tappable", 3 | "version": "1.0.4", 4 | "description": "Touch / Tappable Event Handling Component for React", 5 | "main": "lib/TapAndPinchable.js", 6 | "author": "Jed Watson", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/JedWatson/react-tappable.git" 11 | }, 12 | "peerDependencies": { 13 | "create-react-class": "^15.5.2", 14 | "prop-types": "^15.5.8", 15 | "react": "^0.14 || ^15.0.0-rc || ^15.0.0 || ^16.0.0", 16 | "react-dom": "^0.14 || ^15.0.0-rc || ^15.0.0 || ^16.0.0" 17 | }, 18 | "devDependencies": { 19 | "babel-cli": "^6.26.0", 20 | "babel-core": "^6.26.0", 21 | "babel-eslint": "^4.1.3", 22 | "babel-preset-es2015": "^6.24.1", 23 | "babel-preset-react": "^6.24.1", 24 | "babelify": "^8.0.0", 25 | "create-react-class": "^15.5.2", 26 | "eslint": "^1.6.0", 27 | "eslint-plugin-react": "^3.5.1", 28 | "gulp": "^3.9.1", 29 | "prop-types": "^15.5.8", 30 | "react": "^0.14 || ^15.0.0-rc || ^15.0.0 || ^16.0.0", 31 | "react-component-gulp-tasks": "^0.7.7", 32 | "react-dom": "^0.14 || ^15.0.0-rc || ^15.0.0 || ^16.0.0" 33 | }, 34 | "babel": { 35 | "presets": [ 36 | "es2015", 37 | "react" 38 | ] 39 | }, 40 | "browserify-shim": { 41 | "create-react-class": "global:createReactClass", 42 | "prop-types": "global:PropTypes", 43 | "react": "global:React", 44 | "react-dom": "global:ReactDOM" 45 | }, 46 | "scripts": { 47 | "build": "babel src --out-dir lib", 48 | "dist": "browserify src/TapAndPinchable.js --standalone ReactTappable --transform [ babelify ] > dist/react-tappable.js", 49 | "start": "gulp dev", 50 | "examples": "gulp dev:server", 51 | "lint": "eslint ./; true", 52 | "publish:site": "NODE_ENV=production gulp publish:examples", 53 | "release": "NODE_ENV=production gulp release", 54 | "test": "echo \"no tests yet\" && exit 0", 55 | "watch": "gulp watch:lib" 56 | }, 57 | "keywords": [ 58 | "react", 59 | "react-component", 60 | "tap", 61 | "tappable", 62 | "touch", 63 | "mobile" 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /src/Pinchable.js: -------------------------------------------------------------------------------- 1 | var PinchableBaseMixin = require('./PinchableBaseMixin'); 2 | var PinchableMixin = require('./PinchableMixin'); 3 | var getComponent = require('./getComponent'); 4 | var touchStyles = require('./touchStyles'); 5 | 6 | var Component = getComponent([PinchableBaseMixin, PinchableMixin]); 7 | 8 | module.exports = Component; 9 | module.exports.touchStyles = touchStyles; 10 | -------------------------------------------------------------------------------- /src/PinchableBaseMixin.js: -------------------------------------------------------------------------------- 1 | var PropTypes = require('prop-types'); 2 | var React = require('react'); 3 | 4 | var Mixin = { 5 | propTypes: { 6 | preventDefault: PropTypes.bool, // whether to preventDefault on all events 7 | stopPropagation: PropTypes.bool, // whether to stopPropagation on all events 8 | 9 | onTouchStart: PropTypes.func, // pass-through touch event 10 | onTouchMove: PropTypes.func, // pass-through touch event 11 | onTouchEnd: PropTypes.func // pass-through touch event 12 | }, 13 | 14 | getInitialState: function () { 15 | return { 16 | isActive: false 17 | }; 18 | }, 19 | 20 | processEvent: function (event) { 21 | if (this.props.preventDefault) event.preventDefault(); 22 | if (this.props.stopPropagation) event.stopPropagation(); 23 | }, 24 | 25 | onTouchStart: function (event) { 26 | if (this.props.onTouchStart && this.props.onTouchStart(event) === false) return; 27 | this.processEvent(event); 28 | 29 | if (this.onPinchStart && 30 | (this.props.onPinchStart || this.props.onPinchMove || this.props.onPinchEnd) && 31 | event.touches.length === 2) { 32 | this.onPinchStart && this.onPinchStart(event); 33 | } 34 | }, 35 | 36 | onTouchMove: function (event) { 37 | if (this._initialPinch && event.touches.length === 2 && this.onPinchMove) { 38 | this.onPinchMove(event); 39 | event.preventDefault(); 40 | } 41 | }, 42 | 43 | onTouchEnd: function (event) { 44 | if (this.onPinchEnd && this._initialPinch && (event.touches.length + event.changedTouches.length) === 2) { 45 | this.onPinchEnd(event); 46 | event.preventDefault(); 47 | } 48 | }, 49 | 50 | endTouch: function (event, callback) { 51 | this._initialTouch = null; 52 | this._lastTouch = null; 53 | if (this.state.isActive) { 54 | this.setState({ 55 | isActive: false 56 | }, callback); 57 | } else if (callback) { 58 | callback(); 59 | } 60 | }, 61 | 62 | handlers: function () { 63 | return { 64 | onTouchStart: this.onTouchStart, 65 | onTouchMove: this.onTouchMove, 66 | onTouchEnd: this.onTouchEnd, 67 | onMouseDown: this.onMouseDown, 68 | onMouseUp: this.onMouseUp, 69 | onMouseMove: this.onMouseMove, 70 | onMouseOut: this.onMouseOut 71 | }; 72 | } 73 | }; 74 | 75 | module.exports = Mixin; 76 | -------------------------------------------------------------------------------- /src/PinchableMixin.js: -------------------------------------------------------------------------------- 1 | var PropTypes = require('prop-types'); 2 | var React = require('react'); 3 | 4 | function getPinchProps (touches) { 5 | return { 6 | touches: Array.prototype.map.call(touches, function copyTouch (touch) { 7 | return { identifier: touch.identifier, pageX: touch.pageX, pageY: touch.pageY }; 8 | }), 9 | center: {x: (touches[0].pageX + touches[1].pageX) / 2, y: (touches[0].pageY + touches[1].pageY) / 2 }, 10 | angle: Math.atan() * (touches[1].pageY - touches[0].pageY) / (touches[1].pageX - touches[0].pageX) * 180 / Math.PI, 11 | distance: Math.sqrt(Math.pow(Math.abs(touches[1].pageX - touches[0].pageX), 2) + Math.pow(Math.abs(touches[1].pageY - touches[0].pageY), 2)) 12 | }; 13 | } 14 | 15 | var Mixin = { 16 | propTypes: { 17 | onPinchStart: PropTypes.func, // fires when a pinch gesture is started 18 | onPinchMove: PropTypes.func, // fires on every touch-move when a pinch action is active 19 | onPinchEnd: PropTypes.func // fires when a pinch action ends 20 | }, 21 | 22 | onPinchStart: function (event) { 23 | // in case the two touches didn't start exactly at the same time 24 | if (this._initialTouch) { 25 | this.endTouch(); 26 | } 27 | var touches = event.touches; 28 | this._initialPinch = getPinchProps(touches); 29 | this._initialPinch = Object.assign(this._initialPinch, { 30 | displacement: { x: 0, y: 0 }, 31 | displacementVelocity: { x: 0, y: 0 }, 32 | rotation: 0, 33 | rotationVelocity: 0, 34 | zoom: 1, 35 | zoomVelocity: 0, 36 | time: Date.now() 37 | }); 38 | this._lastPinch = this._initialPinch; 39 | this.props.onPinchStart && this.props.onPinchStart(this._initialPinch, event); 40 | }, 41 | 42 | onPinchMove: function (event) { 43 | if (this._initialTouch) { 44 | this.endTouch(); 45 | } 46 | var touches = event.touches; 47 | if (touches.length !== 2) { 48 | return this.onPinchEnd(event); // bail out before disaster 49 | } 50 | 51 | var currentPinch = 52 | touches[0].identifier === this._initialPinch.touches[0].identifier && touches[1].identifier === this._initialPinch.touches[1].identifier ? 53 | getPinchProps(touches) // the touches are in the correct order 54 | : touches[1].identifier === this._initialPinch.touches[0].identifier && touches[0].identifier === this._initialPinch.touches[1].identifier ? 55 | getPinchProps(touches.reverse()) // the touches have somehow changed order 56 | : getPinchProps(touches); // something is wrong, but we still have two touch-points, so we try not to fail 57 | 58 | currentPinch.displacement = { 59 | x: currentPinch.center.x - this._initialPinch.center.x, 60 | y: currentPinch.center.y - this._initialPinch.center.y 61 | }; 62 | 63 | currentPinch.time = Date.now(); 64 | var timeSinceLastPinch = currentPinch.time - this._lastPinch.time; 65 | 66 | currentPinch.displacementVelocity = { 67 | x: (currentPinch.displacement.x - this._lastPinch.displacement.x) / timeSinceLastPinch, 68 | y: (currentPinch.displacement.y - this._lastPinch.displacement.y) / timeSinceLastPinch 69 | }; 70 | 71 | currentPinch.rotation = currentPinch.angle - this._initialPinch.angle; 72 | currentPinch.rotationVelocity = currentPinch.rotation - this._lastPinch.rotation / timeSinceLastPinch; 73 | 74 | currentPinch.zoom = currentPinch.distance / this._initialPinch.distance; 75 | currentPinch.zoomVelocity = (currentPinch.zoom - this._lastPinch.zoom) / timeSinceLastPinch; 76 | 77 | this.props.onPinchMove && this.props.onPinchMove(currentPinch, event); 78 | 79 | this._lastPinch = currentPinch; 80 | }, 81 | 82 | onPinchEnd: function (event) { 83 | // TODO use helper to order touches by identifier and use actual values on touchEnd. 84 | var currentPinch = Object.assign({}, this._lastPinch); 85 | currentPinch.time = Date.now(); 86 | 87 | if (currentPinch.time - this._lastPinch.time > 16) { 88 | currentPinch.displacementVelocity = 0; 89 | currentPinch.rotationVelocity = 0; 90 | currentPinch.zoomVelocity = 0; 91 | } 92 | 93 | this.props.onPinchEnd && this.props.onPinchEnd(currentPinch, event); 94 | 95 | this._initialPinch = this._lastPinch = null; 96 | 97 | // If one finger is still on screen, it should start a new touch event for swiping etc 98 | // But it should never fire an onTap or onPress event. 99 | // Since there is no support swipes yet, this should be disregarded for now 100 | // if (event.touches.length === 1) { 101 | // this.onTouchStart(event); 102 | // } 103 | } 104 | }; 105 | 106 | module.exports = Mixin; 107 | -------------------------------------------------------------------------------- /src/TapAndPinchable.js: -------------------------------------------------------------------------------- 1 | var TappableMixin = require('./TappableMixin'); 2 | var PinchableMixin = require('./PinchableMixin'); 3 | var getComponent = require('./getComponent'); 4 | var touchStyles = require('./touchStyles'); 5 | 6 | var Component = getComponent([TappableMixin, PinchableMixin]); 7 | 8 | module.exports = Component; 9 | module.exports.touchStyles = touchStyles; 10 | module.exports.Mixin = Object.assign({}, TappableMixin, { 11 | onPinchStart: PinchableMixin.onPinchStart, 12 | onPinchMove: PinchableMixin.onPinchMove, 13 | onPinchEnd: PinchableMixin.onPinchEnd 14 | }); 15 | -------------------------------------------------------------------------------- /src/Tappable.js: -------------------------------------------------------------------------------- 1 | var TappableMixin = require('./TappableMixin'); 2 | var getComponent = require('./getComponent'); 3 | var touchStyles = require('./touchStyles'); 4 | 5 | var Component = getComponent([TappableMixin]); 6 | 7 | module.exports = Component; 8 | module.exports.touchStyles = touchStyles; 9 | module.exports.Mixin = TappableMixin; 10 | -------------------------------------------------------------------------------- /src/TappableMixin.js: -------------------------------------------------------------------------------- 1 | var PropTypes = require('prop-types'); 2 | var React = require('react'); 3 | var ReactDOM = require('react-dom'); 4 | 5 | const SPACE_KEY = 32; 6 | const ENTER_KEY = 13; 7 | 8 | function getTouchProps (touch) { 9 | if (!touch) return {}; 10 | return { 11 | pageX: touch.pageX, 12 | pageY: touch.pageY, 13 | clientX: touch.clientX, 14 | clientY: touch.clientY 15 | }; 16 | } 17 | 18 | var Mixin = { 19 | propTypes: { 20 | moveThreshold: PropTypes.number, // pixels to move before cancelling tap 21 | moveXThreshold: PropTypes.number, // pixels on the x axis to move before cancelling tap (overrides moveThreshold) 22 | moveYThreshold: PropTypes.number, // pixels on the y axis to move before cancelling tap (overrides moveThreshold) 23 | allowReactivation: PropTypes.bool, // after moving outside of the moveThreshold will you allow 24 | // reactivation by moving back within the moveThreshold? 25 | activeDelay: PropTypes.number, // ms to wait before adding the `-active` class 26 | pressDelay: PropTypes.number, // ms to wait before detecting a press 27 | pressMoveThreshold: PropTypes.number, // pixels to move before cancelling press 28 | preventDefault: PropTypes.bool, // whether to preventDefault on all events 29 | stopPropagation: PropTypes.bool, // whether to stopPropagation on all events 30 | 31 | onTap: PropTypes.func, // fires when a tap is detected 32 | onPress: PropTypes.func, // fires when a press is detected 33 | onTouchStart: PropTypes.func, // pass-through touch event 34 | onTouchMove: PropTypes.func, // pass-through touch event 35 | onTouchEnd: PropTypes.func, // pass-through touch event 36 | onMouseDown: PropTypes.func, // pass-through mouse event 37 | onMouseUp: PropTypes.func, // pass-through mouse event 38 | onMouseMove: PropTypes.func, // pass-through mouse event 39 | onMouseOut: PropTypes.func, // pass-through mouse event 40 | onKeyDown: PropTypes.func, // pass-through key event 41 | onKeyUp: PropTypes.func, // pass-through key event 42 | }, 43 | 44 | getDefaultProps: function () { 45 | return { 46 | activeDelay: 0, 47 | allowReactivation: true, 48 | moveThreshold: 100, 49 | pressDelay: 1000, 50 | pressMoveThreshold: 5 51 | }; 52 | }, 53 | 54 | getInitialState: function () { 55 | return { 56 | isActive: false, 57 | touchActive: false, 58 | pinchActive: false 59 | }; 60 | }, 61 | 62 | componentDidMount: function () { 63 | this.isMounted = true; 64 | }, 65 | 66 | componentWillUnmount: function () { 67 | this.isMounted = false; 68 | this.cleanupScrollDetection(); 69 | this.cancelPressDetection(); 70 | this.clearActiveTimeout(); 71 | }, 72 | 73 | componentWillUpdate: function(nextProps, nextState) { 74 | if (this.state.isActive && !nextState.isActive) { 75 | this.props.onDeactivate && this.props.onDeactivate(); 76 | } else if (!this.state.isActive && nextState.isActive) { 77 | this.props.onReactivate && this.props.onReactivate(); 78 | } 79 | }, 80 | 81 | processEvent: function (event) { 82 | if (this.props.preventDefault) event.preventDefault(); 83 | if (this.props.stopPropagation) event.stopPropagation(); 84 | }, 85 | 86 | onTouchStart: function (event) { 87 | if (this.props.onTouchStart && this.props.onTouchStart(event) === false) return; 88 | this.processEvent(event); 89 | window._blockMouseEvents = true; 90 | if (event.touches.length === 1) { 91 | this._initialTouch = this._lastTouch = getTouchProps(event.touches[0]); 92 | this.initScrollDetection(); 93 | this.initPressDetection(event, this.endTouch); 94 | this.initTouchmoveDetection(); 95 | if (this.props.activeDelay > 0) { 96 | this._activeTimeout = setTimeout(this.makeActive, this.props.activeDelay); 97 | } else { 98 | this.makeActive(); 99 | } 100 | } else if (this.onPinchStart && 101 | (this.props.onPinchStart || this.props.onPinchMove || this.props.onPinchEnd) && 102 | event.touches.length === 2) { 103 | this.onPinchStart(event); 104 | } 105 | }, 106 | 107 | makeActive: function () { 108 | if (!this.isMounted) return; 109 | this.clearActiveTimeout(); 110 | this.setState({ 111 | isActive: true 112 | }); 113 | }, 114 | 115 | clearActiveTimeout: function () { 116 | clearTimeout(this._activeTimeout); 117 | this._activeTimeout = false; 118 | }, 119 | 120 | initScrollDetection: function () { 121 | this._scrollPos = { top: 0, left: 0 }; 122 | this._scrollParents = []; 123 | this._scrollParentPos = []; 124 | var node = ReactDOM.findDOMNode(this); 125 | 126 | while (node) { 127 | if (node.scrollHeight > node.offsetHeight || node.scrollWidth > node.offsetWidth) { 128 | this._scrollParents.push(node); 129 | this._scrollParentPos.push(node.scrollTop + node.scrollLeft); 130 | this._scrollPos.top += node.scrollTop; 131 | this._scrollPos.left += node.scrollLeft; 132 | } 133 | 134 | node = node.parentNode; 135 | } 136 | }, 137 | 138 | initTouchmoveDetection: function () { 139 | this._touchmoveTriggeredTimes = 0; 140 | }, 141 | 142 | cancelTouchmoveDetection: function () { 143 | if (this._touchmoveDetectionTimeout) { 144 | clearTimeout(this._touchmoveDetectionTimeout); 145 | this._touchmoveDetectionTimeout = null; 146 | this._touchmoveTriggeredTimes = 0; 147 | } 148 | }, 149 | 150 | calculateMovement: function (touch) { 151 | return { 152 | x: Math.abs(touch.clientX - this._initialTouch.clientX), 153 | y: Math.abs(touch.clientY - this._initialTouch.clientY) 154 | }; 155 | }, 156 | 157 | detectScroll: function () { 158 | var currentScrollPos = { top: 0, left: 0 }; 159 | for (var i = 0; i < this._scrollParents.length; i++) { 160 | currentScrollPos.top += this._scrollParents[i].scrollTop; 161 | currentScrollPos.left += this._scrollParents[i].scrollLeft; 162 | } 163 | return !(currentScrollPos.top === this._scrollPos.top && currentScrollPos.left === this._scrollPos.left); 164 | }, 165 | 166 | cleanupScrollDetection: function () { 167 | this._scrollParents = undefined; 168 | this._scrollPos = undefined; 169 | }, 170 | 171 | initPressDetection: function (event, callback) { 172 | if (!this.props.onPress) return; 173 | 174 | // SyntheticEvent objects are pooled, so persist the event so it can be referenced asynchronously 175 | event.persist(); 176 | 177 | this._pressTimeout = setTimeout(function () { 178 | this.props.onPress(event); 179 | callback(); 180 | }.bind(this), this.props.pressDelay); 181 | }, 182 | 183 | cancelPressDetection: function () { 184 | clearTimeout(this._pressTimeout); 185 | }, 186 | 187 | onTouchMove: function (event) { 188 | if (this._initialTouch) { 189 | this.processEvent(event); 190 | 191 | if (this.detectScroll()) { 192 | return this.endTouch(event); 193 | } else { 194 | if ((this._touchmoveTriggeredTimes)++ === 0) { 195 | this._touchmoveDetectionTimeout = setTimeout(function() { 196 | if (this._touchmoveTriggeredTimes === 1) { 197 | this.endTouch(event); 198 | } 199 | }.bind(this), 64); 200 | } 201 | } 202 | 203 | this.props.onTouchMove && this.props.onTouchMove(event); 204 | this._lastTouch = getTouchProps(event.touches[0]); 205 | var movement = this.calculateMovement(this._lastTouch); 206 | if (movement.x > this.props.pressMoveThreshold || movement.y > this.props.pressMoveThreshold) { 207 | this.cancelPressDetection(); 208 | } 209 | if (movement.x > (this.props.moveXThreshold || this.props.moveThreshold) || 210 | movement.y > (this.props.moveYThreshold || this.props.moveThreshold)) { 211 | if (this.state.isActive) { 212 | if (this.props.allowReactivation) { 213 | this.setState({ 214 | isActive: false 215 | }); 216 | } else { 217 | return this.endTouch(event); 218 | } 219 | } else if (this._activeTimeout) { 220 | this.clearActiveTimeout(); 221 | } 222 | } else { 223 | if (!this.state.isActive && !this._activeTimeout) { 224 | this.setState({ 225 | isActive: true 226 | }); 227 | } 228 | } 229 | } else if (this._initialPinch && event.touches.length === 2 && this.onPinchMove) { 230 | this.onPinchMove(event); 231 | event.preventDefault(); 232 | } 233 | }, 234 | 235 | onTouchEnd: function (event) { 236 | if (this._initialTouch) { 237 | this.processEvent(event); 238 | var afterEndTouch; 239 | var movement = this.calculateMovement(this._lastTouch); 240 | if (movement.x <= (this.props.moveXThreshold || this.props.moveThreshold) && 241 | movement.y <= (this.props.moveYThreshold || this.props.moveThreshold) && 242 | this.props.onTap) { 243 | event.preventDefault(); 244 | afterEndTouch = () => { 245 | var finalParentScrollPos = this._scrollParents.map(node => node.scrollTop + node.scrollLeft); 246 | var stoppedMomentumScroll = this._scrollParentPos.some((end, i) => { 247 | return end !== finalParentScrollPos[i]; 248 | }); 249 | if (!stoppedMomentumScroll) { 250 | this.props.onTap(event); 251 | } 252 | }; 253 | } 254 | this.endTouch(event, afterEndTouch); 255 | } else if (this.onPinchEnd && this._initialPinch && (event.touches.length + event.changedTouches.length) === 2) { 256 | this.onPinchEnd(event); 257 | event.preventDefault(); 258 | } 259 | }, 260 | 261 | endTouch: function (event, callback) { 262 | this.cancelTouchmoveDetection(); 263 | this.cancelPressDetection(); 264 | this.clearActiveTimeout(); 265 | if (event && this.props.onTouchEnd) { 266 | this.props.onTouchEnd(event); 267 | } 268 | this._initialTouch = null; 269 | this._lastTouch = null; 270 | if (callback) { 271 | callback(); 272 | } 273 | if (this.state.isActive) { 274 | this.setState({ 275 | isActive: false 276 | }); 277 | } 278 | }, 279 | 280 | onMouseDown: function (event) { 281 | if (window._blockMouseEvents) { 282 | window._blockMouseEvents = false; 283 | return; 284 | } 285 | if (this.props.onMouseDown && this.props.onMouseDown(event) === false) return; 286 | this.processEvent(event); 287 | this.initPressDetection(event, this.endMouseEvent); 288 | this._mouseDown = true; 289 | this.setState({ 290 | isActive: true 291 | }); 292 | }, 293 | 294 | onMouseMove: function (event) { 295 | if (window._blockMouseEvents || !this._mouseDown) return; 296 | this.processEvent(event); 297 | this.props.onMouseMove && this.props.onMouseMove(event); 298 | }, 299 | 300 | onMouseUp: function (event) { 301 | if (window._blockMouseEvents || !this._mouseDown) return; 302 | this.processEvent(event); 303 | this.props.onMouseUp && this.props.onMouseUp(event); 304 | this.props.onTap && this.props.onTap(event); 305 | this.endMouseEvent(); 306 | }, 307 | 308 | onMouseOut: function (event) { 309 | if (window._blockMouseEvents || !this._mouseDown) return; 310 | this.processEvent(event); 311 | this.props.onMouseOut && this.props.onMouseOut(event); 312 | this.endMouseEvent(); 313 | }, 314 | 315 | endMouseEvent: function () { 316 | this.cancelPressDetection(); 317 | this._mouseDown = false; 318 | this.setState({ 319 | isActive: false 320 | }); 321 | }, 322 | 323 | onKeyUp: function (event) { 324 | if (!this._keyDown) return; 325 | this.processEvent(event); 326 | this.props.onKeyUp && this.props.onKeyUp(event); 327 | this.props.onTap && this.props.onTap(event); 328 | this._keyDown = false; 329 | this.cancelPressDetection(); 330 | this.setState({ 331 | isActive: false 332 | }); 333 | }, 334 | 335 | onKeyDown: function (event) { 336 | if (this.props.onKeyDown && this.props.onKeyDown(event) === false) return; 337 | if (event.which !== SPACE_KEY && event.which !== ENTER_KEY) return; 338 | if (this._keyDown) return; 339 | this.initPressDetection(event, this.endKeyEvent); 340 | this.processEvent(event); 341 | this._keyDown = true; 342 | this.setState({ 343 | isActive: true 344 | }); 345 | }, 346 | 347 | endKeyEvent: function () { 348 | this.cancelPressDetection(); 349 | this._keyDown = false; 350 | this.setState({ 351 | isActive: false 352 | }); 353 | }, 354 | 355 | cancelTap: function () { 356 | this.endTouch(); 357 | this._mouseDown = false; 358 | }, 359 | 360 | handlers: function () { 361 | return { 362 | onTouchStart: this.onTouchStart, 363 | onTouchMove: this.onTouchMove, 364 | onTouchEnd: this.onTouchEnd, 365 | onMouseDown: this.onMouseDown, 366 | onMouseUp: this.onMouseUp, 367 | onMouseMove: this.onMouseMove, 368 | onMouseOut: this.onMouseOut, 369 | onKeyDown: this.onKeyDown, 370 | onKeyUp: this.onKeyUp, 371 | }; 372 | } 373 | }; 374 | 375 | module.exports = Mixin; 376 | -------------------------------------------------------------------------------- /src/getComponent.js: -------------------------------------------------------------------------------- 1 | var createReactClass = require('create-react-class'); 2 | var PropTypes = require('prop-types'); 3 | var React = require('react'); 4 | var touchStyles = require('./touchStyles'); 5 | 6 | /** 7 | * Tappable Component 8 | * ================== 9 | */ 10 | module.exports = function (mixins) { 11 | return createReactClass({ 12 | displayName: 'Tappable', 13 | 14 | mixins: mixins, 15 | 16 | propTypes: { 17 | component: PropTypes.any, // component to create 18 | className: PropTypes.string, // optional className 19 | classBase: PropTypes.string, // base for generated classNames 20 | classes: PropTypes.object, // object containing the active and inactive class names 21 | style: PropTypes.object, // additional style properties for the component 22 | disabled: PropTypes.bool // only applies to buttons 23 | }, 24 | 25 | getDefaultProps: function () { 26 | return { 27 | component: 'span', 28 | classBase: 'Tappable' 29 | }; 30 | }, 31 | 32 | render: function () { 33 | var props = this.props; 34 | var className = props.classBase + (this.state.isActive ? '-active' : '-inactive'); 35 | 36 | if (props.className) { 37 | className += ' ' + props.className; 38 | } 39 | 40 | if (props.classes) { 41 | className += ' ' + (this.state.isActive ? props.classes.active : props.classes.inactive); 42 | } 43 | 44 | var style = {}; 45 | Object.assign(style, touchStyles, props.style); 46 | 47 | var newComponentProps = Object.assign({}, props, { 48 | style: style, 49 | className: className, 50 | disabled: props.disabled, 51 | handlers: this.handlers 52 | }, this.handlers()); 53 | 54 | delete newComponentProps.activeDelay; 55 | delete newComponentProps.allowReactivation; 56 | delete newComponentProps.classBase; 57 | delete newComponentProps.classes; 58 | delete newComponentProps.handlers; 59 | delete newComponentProps.onTap; 60 | delete newComponentProps.onPress; 61 | delete newComponentProps.onPinchStart; 62 | delete newComponentProps.onPinchMove; 63 | delete newComponentProps.onPinchEnd; 64 | delete newComponentProps.onDeactivate; 65 | delete newComponentProps.onReactivate; 66 | delete newComponentProps.moveThreshold; 67 | delete newComponentProps.moveXThreshold; 68 | delete newComponentProps.moveYThreshold; 69 | delete newComponentProps.pressDelay; 70 | delete newComponentProps.pressMoveThreshold; 71 | delete newComponentProps.preventDefault; 72 | delete newComponentProps.stopPropagation; 73 | delete newComponentProps.component; 74 | 75 | return React.createElement(props.component, newComponentProps, props.children); 76 | } 77 | }); 78 | }; 79 | -------------------------------------------------------------------------------- /src/touchStyles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var touchStyles = { 4 | WebkitTapHighlightColor: 'rgba(0,0,0,0)', 5 | WebkitTouchCallout: 'none', 6 | WebkitUserSelect: 'none', 7 | KhtmlUserSelect: 'none', 8 | MozUserSelect: 'none', 9 | msUserSelect: 'none', 10 | userSelect: 'none', 11 | cursor: 'pointer' 12 | }; 13 | 14 | module.exports = touchStyles; 15 | --------------------------------------------------------------------------------