diff --git a/README.md b/README.md index ede1202..c8101e3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Selectable items for React -Allows individual or group selection of items using the mouse. +Allows individual or group selection of items using the mouse or touch. ## Demo [Try it out](http://unclecheese.github.io/react-selectable/example) @@ -58,3 +58,6 @@ The `` component accepts a few optional props: * `component` (String) The component to render. Defaults to `div`. * `fixedPosition` (Boolean) Whether the `` container is a fixed/absolute position element or the grandchild of one. Note: if you get an error that `Value must be omitted for boolean attributes` when you try ``, simply use Javascript's boolean object function: ``. +## Extended Features + +If you need extended features such as item click selection (without dragging), not clearing the previous selection at the start of a drag, or a function callback during selection, check out [react-selectable-extended](https://github.com/leopoldjoy/react-selectable-extended). \ No newline at end of file diff --git a/dist/react-selectable.js b/dist/react-selectable.js index 55a4e97..60802d9 100644 --- a/dist/react-selectable.js +++ b/dist/react-selectable.js @@ -80,12 +80,12 @@ return /******/ (function(modules) { // webpackBootstrap 'use strict'; - var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - Object.defineProperty(exports, "__esModule", { value: true }); + var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + var _react = __webpack_require__(2); var _react2 = _interopRequireDefault(_react); @@ -131,12 +131,19 @@ return /******/ (function(modules) { // webpackBootstrap _this._mouseDownData = null; _this._registry = []; + // Used to prevent actions from firing twice on devices that are both click and touch enabled + _this._mouseDownStarted = false; + _this._mouseMoveStarted = false; + _this._mouseUpStarted = false; + _this._openSelector = _this._openSelector.bind(_this); _this._mouseDown = _this._mouseDown.bind(_this); _this._mouseUp = _this._mouseUp.bind(_this); _this._selectElements = _this._selectElements.bind(_this); _this._registerSelectable = _this._registerSelectable.bind(_this); _this._unregisterSelectable = _this._unregisterSelectable.bind(_this); + _this._desktopEventCoords = _this._desktopEventCoords.bind(_this); + return _this; } @@ -154,6 +161,7 @@ return /******/ (function(modules) { // webpackBootstrap key: 'componentDidMount', value: function componentDidMount() { _reactDom2.default.findDOMNode(this).addEventListener('mousedown', this._mouseDown); + _reactDom2.default.findDOMNode(this).addEventListener('touchstart', this._mouseDown); } /** @@ -164,6 +172,7 @@ return /******/ (function(modules) { // webpackBootstrap key: 'componentWillUnmount', value: function componentWillUnmount() { _reactDom2.default.findDOMNode(this).removeEventListener('mousedown', this._mouseDown); + _reactDom2.default.findDOMNode(this).removeEventListener('touchstart', this._mouseDown); } }, { key: '_registerSelectable', @@ -186,6 +195,13 @@ return /******/ (function(modules) { // webpackBootstrap }, { key: '_openSelector', value: function _openSelector(e) { + var _this2 = this; + + if (this._mouseMoveStarted) return; + this._mouseMoveStarted = true; + + e = this._desktopEventCoords(e); + var w = Math.abs(this._mouseDownData.initialW - e.pageX); var h = Math.abs(this._mouseDownData.initialH - e.pageY); @@ -195,6 +211,8 @@ return /******/ (function(modules) { // webpackBootstrap boxHeight: h, boxLeft: Math.min(e.pageX, this._mouseDownData.initialW), boxTop: Math.min(e.pageY, this._mouseDownData.initialH) + }, function () { + _this2._mouseMoveStarted = false; }); } @@ -206,11 +224,18 @@ return /******/ (function(modules) { // webpackBootstrap }, { key: '_mouseDown', value: function _mouseDown(e) { + if (this._mouseDownStarted) return; + this._mouseDownStarted = true; + this._mouseUpStarted = false; + + e = this._desktopEventCoords(e); + var node = _reactDom2.default.findDOMNode(this); var collides = undefined, offsetData = undefined, distanceData = undefined; _reactDom2.default.findDOMNode(this).addEventListener('mouseup', this._mouseUp); + _reactDom2.default.findDOMNode(this).addEventListener('touchend', this._mouseUp); // Right clicks if (e.which === 3 || e.button === 2) return; @@ -241,6 +266,7 @@ return /******/ (function(modules) { // webpackBootstrap e.preventDefault(); _reactDom2.default.findDOMNode(this).addEventListener('mousemove', this._openSelector); + _reactDom2.default.findDOMNode(this).addEventListener('touchmove', this._openSelector); } /** @@ -250,8 +276,14 @@ return /******/ (function(modules) { // webpackBootstrap }, { key: '_mouseUp', value: function _mouseUp(e) { + if (this._mouseUpStarted) return; + this._mouseUpStarted = true; + this._mouseDownStarted = false; + _reactDom2.default.findDOMNode(this).removeEventListener('mousemove', this._openSelector); _reactDom2.default.findDOMNode(this).removeEventListener('mouseup', this._mouseUp); + _reactDom2.default.findDOMNode(this).removeEventListener('touchmove', this._openSelector); + _reactDom2.default.findDOMNode(this).removeEventListener('touchend', this._mouseUp); if (!this._mouseDownData) return; @@ -270,6 +302,7 @@ return /******/ (function(modules) { // webpackBootstrap var selectbox = _reactDom2.default.findDOMNode(this.refs.selectbox); var tolerance = this.props.tolerance; + if (!selectbox) return; this._registry.forEach(function (itemData) { @@ -287,6 +320,22 @@ return /******/ (function(modules) { // webpackBootstrap this.props.onSelection(currentItems); } + /** + * Used to return event object with desktop (non-touch) format of event + * coordinates, regardless of whether the action is from mobile or desktop. + */ + + }, { + key: '_desktopEventCoords', + value: function _desktopEventCoords(e) { + if (e.pageX == undefined || e.pageY == undefined) { + // Touch-device + e.pageX = e.targetTouches[0].pageX; + e.pageY = e.targetTouches[0].pageY; + } + return e; + } + /** * Renders the component * @return {ReactComponent} @@ -496,12 +545,12 @@ return /******/ (function(modules) { // webpackBootstrap 'use strict'; - var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - Object.defineProperty(exports, "__esModule", { value: true }); + var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + var _react = __webpack_require__(2); var _react2 = _interopRequireDefault(_react); diff --git a/src/selectable-group.js b/src/selectable-group.js index aa5ee0e..9ec9dae 100644 --- a/src/selectable-group.js +++ b/src/selectable-group.js @@ -13,18 +13,25 @@ class SelectableGroup extends React.Component { this.state = { isBoxSelecting: false, boxWidth: 0, - boxHeight: 0 + boxHeight: 0 } this._mouseDownData = null; this._registry = []; + // Used to prevent actions from firing twice on devices that are both click and touch enabled + this._mouseDownStarted = false; + this._mouseMoveStarted = false; + this._mouseUpStarted = false; + this._openSelector = this._openSelector.bind(this); this._mouseDown = this._mouseDown.bind(this); this._mouseUp = this._mouseUp.bind(this); this._selectElements = this._selectElements.bind(this); this._registerSelectable = this._registerSelectable.bind(this); this._unregisterSelectable = this._unregisterSelectable.bind(this); + this._desktopEventCoords = this._desktopEventCoords.bind(this); + } @@ -39,7 +46,8 @@ class SelectableGroup extends React.Component { componentDidMount () { - ReactDOM.findDOMNode(this).addEventListener('mousedown', this._mouseDown); + ReactDOM.findDOMNode(this).addEventListener('mousedown', this._mouseDown); + ReactDOM.findDOMNode(this).addEventListener('touchstart', this._mouseDown); } @@ -47,7 +55,8 @@ class SelectableGroup extends React.Component { * Remove global event listeners */ componentWillUnmount () { - ReactDOM.findDOMNode(this).removeEventListener('mousedown', this._mouseDown); + ReactDOM.findDOMNode(this).removeEventListener('mousedown', this._mouseDown); + ReactDOM.findDOMNode(this).removeEventListener('touchstart', this._mouseDown); } @@ -65,7 +74,12 @@ class SelectableGroup extends React.Component { * Called while moving the mouse with the button down. Changes the boundaries * of the selection box */ - _openSelector (e) { + _openSelector (e) { + if(this._mouseMoveStarted) return; + this._mouseMoveStarted = true; + + e = this._desktopEventCoords(e); + const w = Math.abs(this._mouseDownData.initialW - e.pageX); const h = Math.abs(this._mouseDownData.initialH - e.pageY); @@ -75,6 +89,8 @@ class SelectableGroup extends React.Component { boxHeight: h, boxLeft: Math.min(e.pageX, this._mouseDownData.initialW), boxTop: Math.min(e.pageY, this._mouseDownData.initialH) + }, () => { + this._mouseMoveStarted = false; }); } @@ -84,9 +100,16 @@ class SelectableGroup extends React.Component { * be added, and if so, attach event listeners */ _mouseDown (e) { + if(this._mouseDownStarted) return; + this._mouseDownStarted = true; + this._mouseUpStarted = false; + + e = this._desktopEventCoords(e); + const node = ReactDOM.findDOMNode(this); let collides, offsetData, distanceData; ReactDOM.findDOMNode(this).addEventListener('mouseup', this._mouseUp); + ReactDOM.findDOMNode(this).addEventListener('touchend', this._mouseUp); // Right clicks if(e.which === 3 || e.button === 2) return; @@ -120,6 +143,7 @@ class SelectableGroup extends React.Component { e.preventDefault(); ReactDOM.findDOMNode(this).addEventListener('mousemove', this._openSelector); + ReactDOM.findDOMNode(this).addEventListener('touchmove', this._openSelector); } @@ -127,8 +151,14 @@ class SelectableGroup extends React.Component { * Called when the user has completed selection */ _mouseUp (e) { + if(this._mouseUpStarted) return; + this._mouseUpStarted = true; + this._mouseDownStarted = false; + ReactDOM.findDOMNode(this).removeEventListener('mousemove', this._openSelector); ReactDOM.findDOMNode(this).removeEventListener('mouseup', this._mouseUp); + ReactDOM.findDOMNode(this).removeEventListener('touchmove', this._openSelector); + ReactDOM.findDOMNode(this).removeEventListener('touchend', this._mouseUp); if(!this._mouseDownData) return; @@ -162,6 +192,17 @@ class SelectableGroup extends React.Component { this.props.onSelection(currentItems); } + /** + * Used to return event object with desktop (non-touch) format of event + * coordinates, regardless of whether the action is from mobile or desktop. + */ + _desktopEventCoords (e){ + if(e.pageX==undefined || e.pageY==undefined){ // Touch-device + e.pageX = e.targetTouches[0].pageX; + e.pageY = e.targetTouches[0].pageY; + } + return e; + } /** * Renders the component