diff --git a/lib/draggable.js b/lib/draggable.js index 69ad0db4..44736fb8 100644 --- a/lib/draggable.js +++ b/lib/draggable.js @@ -3,13 +3,6 @@ var React = require('react/addons'); var emptyFunction = function () {}; -// for accessing browser globals -var root = typeof window !== 'undefined' ? window : this; -var bodyElement; -if (typeof document !== 'undefined' && 'body' in document) { - bodyElement = document.body; -} - function updateBoundState (state, bound) { if (!bound) return state; bound = String(bound); @@ -72,10 +65,6 @@ function matchesSelector(el, selector) { return el[method].call(el, selector); } -// @credits: http://stackoverflow.com/questions/4817029/whats-the-best-way-to-detect-a-touch-screen-device-using-javascript/4819886#4819886 -var isTouchDevice = 'ontouchstart' in root // works on most browsers - || 'onmsgesturechange' in root; // works on ie10 on ms surface - // look ::handleDragStart //function isMultiTouch(e) { // return e.touches && Array.isArray(e.touches) && e.touches.length > 1 @@ -84,21 +73,18 @@ var isTouchDevice = 'ontouchstart' in root // works on most browsers /** * simple abstraction for dragging events names * */ -var dragEventFor = (function () { - var eventsFor = { - touch: { - start: 'touchstart', - move: 'touchmove', - end: 'touchend' - }, - mouse: { - start: 'mousedown', - move: 'mousemove', - end: 'mouseup' - } - }; - return eventsFor[isTouchDevice ? 'touch' : 'mouse']; -})(); + var dragEventsFor = { + touch: { + start: 'touchstart', + move: 'touchmove', + end: 'touchend' + }, + mouse: { + start: 'mousedown', + move: 'mousemove', + end: 'mouseup' + } +}; /** * get {clientX, clientY} positions of control @@ -380,10 +366,20 @@ module.exports = React.createClass({ onStop: React.PropTypes.func, /** - * A workaround option which can be passed if onMouseDown needs to be accessed, since it'll always be blocked (due to that there's internal use of onMouseDown) + * A workaround option which can be passed if these event handlers need to be accessed, since they're always handled internally. * */ - onMouseDown: React.PropTypes.func + onMouseDown: React.PropTypes.func, + onTouchStart: React.PropTypes.func, + onMouseUp: React.PropTypes.func, + onTouchEnd: React.PropTypes.func, + }, + + componentWillMount: function() { + this._dragHandlers = { + mouse: {start: this.handleMouseDown, move: this.handleMouseMove, end: this.handleMouseUp}, + touch: {start: this.handleTouchStart, move: this.handleTouchMove, end: this.handleTouchEnd} + }; }, getDefaultProps: function () { @@ -399,7 +395,10 @@ module.exports = React.createClass({ onStart: emptyFunction, onDrag: emptyFunction, onStop: emptyFunction, - onMouseDown: emptyFunction + onMouseDown: emptyFunction, + onMouseUp: emptyFunction, + onTouchStart: emptyFunction, + onTouchEnd: emptyFunction, }; }, @@ -435,11 +434,53 @@ module.exports = React.createClass({ componentWillUnmount: function() { // Remove any leftover event handlers - removeEvent(bodyElement, dragEventFor['move'], this.handleDrag); - removeEvent(bodyElement, dragEventFor['end'], this.handleDragEnd); + var bodyElement = this.getDOMNode().ownerDocument.body; + removeEvent(bodyElement, dragEventsFor.mouse.move, this._dragHandlers.mouse.move); + removeEvent(bodyElement, dragEventsFor.mouse.end, this._dragHandlers.mouse.end); + removeEvent(bodyElement, dragEventsFor.touch.move, this._dragHandlers.touch.move); + removeEvent(bodyElement, dragEventsFor.touch.end, this._dragHandlers.touch.end); + }, + + handleMouseDown: function(e) { + if (typeof this.props.onMouseDown === 'function') + this.props.onMouseDown(e); + if (!e.defaultPrevented) + this.handleDragStart(e, 'mouse'); + }, + + handleMouseMove: function(e) { + this.handleDrag(e, 'mouse'); + }, + + handleMouseUp: function(e) { + if (typeof this.props.onMouseUp === 'function') + this.props.onMouseUp(e); + if (!e.defaultPrevented) + this.handleDragEnd(e, 'mouse'); }, - handleDragStart: function (e) { + handleTouchStart: function(e) { + if (typeof this.props.onTouchStart === 'function') + this.props.onTouchStart(e); + if (!e.defaultPrevented) + this.handleDragStart(e, 'touch'); + }, + + handleTouchMove: function(e) { + this.handleDrag(e, 'touch'); + }, + + handleTouchEnd: function(e) { + if (typeof this.props.onTouchEnd === 'function') + this.props.onTouchEnd(e); + if (!e.defaultPrevented) + this.handleDragEnd(e, 'touch'); + }, + + handleDragStart: function (e, device) { + if (this.state.dragging) + return; + // todo: write right implementation to prevent multitouch drag // prevent multi-touch events // if (isMultiTouch(e)) { @@ -447,20 +488,19 @@ module.exports = React.createClass({ // return // } - // Make it possible to attach event handlers on top of this one - this.props.onMouseDown(e); - // Short circuit if handle or cancel prop was provided and selector doesn't match if ((this.props.handle && !matchesSelector(e.target, this.props.handle)) || (this.props.cancel && matchesSelector(e.target, this.props.cancel))) { return; } + e.preventDefault(); + var dragPoint = getControlPosition(e); // Initiate dragging this.setState({ - dragging: true, + dragging: device, clientX: dragPoint.clientX, clientY: dragPoint.clientY }); @@ -468,20 +508,24 @@ module.exports = React.createClass({ // Call event handler this.props.onStart(e, createUIEvent(this)); + var bodyElement = this.getDOMNode().ownerDocument.body; + // Add event handlers - addEvent(bodyElement, dragEventFor['move'], this.handleDrag); - addEvent(bodyElement, dragEventFor['end'], this.handleDragEnd); + addEvent(bodyElement, dragEventsFor[device].move, this._dragHandlers[device].move); + addEvent(bodyElement, dragEventsFor[device].end, this._dragHandlers[device].end); // Add dragging class to body element if (bodyElement) bodyElement.className += ' react-draggable-dragging'; }, - handleDragEnd: function (e) { + handleDragEnd: function (e, device) { // Short circuit if not currently dragging - if (!this.state.dragging) { + if (!this.state.dragging || (this.state.dragging !== device)) { return; } + e.preventDefault(); + // Turn off dragging this.setState({ dragging: false @@ -490,9 +534,13 @@ module.exports = React.createClass({ // Call event handler this.props.onStop(e, createUIEvent(this)); + + var bodyElement = this.getDOMNode().ownerDocument.body; + // Remove event handlers - removeEvent(root, dragEventFor['move'], this.handleDrag); - removeEvent(root, dragEventFor['end'], this.handleDragEnd); + removeEvent(bodyElement, dragEventsFor[device].move, this._dragHandlers[device].move); + removeEvent(bodyElement, dragEventsFor[device].end, this._dragHandlers[device].end); + // Remove dragging class from body element if (bodyElement) { @@ -502,7 +550,7 @@ module.exports = React.createClass({ } }, - handleDrag: function (e) { + handleDrag: function (e, device) { var dragPoint = getControlPosition(e); var offsetLeft = this._toPixels(this.state.offsetLeft); var offsetTop = this._toPixels(this.state.offsetTop); @@ -615,11 +663,6 @@ module.exports = React.createClass({ this.props.onDrag(e, createUIEvent(this)); }, - onTouchStart: function (e) { - e.preventDefault(); // prevent for scroll - return this.handleDragStart.apply(this, arguments); - }, - render: function () { var style = { top: this.state.offsetTop, @@ -635,11 +678,10 @@ module.exports = React.createClass({ style: style, className: 'react-draggable', - onMouseDown: this.handleDragStart, - onTouchStart: this.onTouchStart, - - onMouseUp: this.handleDragEnd, - onTouchEnd: this.handleDragEnd + onMouseDown: this._dragHandlers.mouse.start, + onTouchStart: this._dragHandlers.touch.start, + onMouseUp: this._dragHandlers.mouse.end, + onTouchEnd: this._dragHandlers.touch.end, }; // Reuse the child provided diff --git a/lib/styles.css b/lib/styles.css index 29fd9710..8dc82eaa 100644 --- a/lib/styles.css +++ b/lib/styles.css @@ -1,4 +1,4 @@ -.react-draggable, .react-draggable-dragging { +.react-draggable strong, .react-draggable-dragging strong { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; diff --git a/test/draggable_test.jsx b/test/draggable_test.jsx index 51a9e67a..63e6f804 100644 --- a/test/draggable_test.jsx +++ b/test/draggable_test.jsx @@ -2,6 +2,7 @@ var React = require('react/addons'); var TestUtils = React.addons.TestUtils; var Draggable = require('../lib/draggable'); +React.initializeTouchEvents(true); describe('react-draggable', function () { describe('props', function () { @@ -86,6 +87,32 @@ describe('react-draggable', function () { expect(called).toEqual(true); }); + it('should call onStart when touch dragging begins', function () { + var called = false; + var drag = TestUtils.renderIntoDocument( + +
+ + ); + + TestUtils.Simulate.touchStart(drag.getDOMNode()); + expect(called).toEqual(true); + }); + + it('should call onStop when touch dragging ends', function () { + var called = false; + var drag = TestUtils.renderIntoDocument( + +
+ + ); + + TestUtils.Simulate.touchStart(drag.getDOMNode()); + TestUtils.Simulate.touchEnd(drag.getDOMNode()); + expect(called).toEqual(true); + }); + + it('should add react-draggable-dragging CSS class to body element when dragging', function () { var drag = TestUtils.renderIntoDocument( @@ -99,12 +126,12 @@ describe('react-draggable', function () { }); }); - describe('interaction', function () { + describe('mouse interaction', function () { it('should initialize dragging onmousedown', function () { var drag = TestUtils.renderIntoDocument(
); TestUtils.Simulate.mouseDown(drag.getDOMNode()); - expect(drag.state.dragging).toEqual(true); + expect(drag.state.dragging).toEqual('mouse'); }); it('should only initialize dragging onmousedown of handle', function () { @@ -121,7 +148,7 @@ describe('react-draggable', function () { expect(drag.state.dragging).toEqual(false); TestUtils.Simulate.mouseDown(drag.getDOMNode().querySelector('.handle')); - expect(drag.state.dragging).toEqual(true); + expect(drag.state.dragging).toEqual('mouse'); }); it('should not initialize dragging onmousedown of cancel', function () { @@ -138,19 +165,72 @@ describe('react-draggable', function () { expect(drag.state.dragging).toEqual(false); TestUtils.Simulate.mouseDown(drag.getDOMNode().querySelector('.content')); - expect(drag.state.dragging).toEqual(true); + expect(drag.state.dragging).toEqual('mouse'); }); it('should discontinue dragging onmouseup', function () { var drag = TestUtils.renderIntoDocument(
); TestUtils.Simulate.mouseDown(drag.getDOMNode()); - expect(drag.state.dragging).toEqual(true); + expect(drag.state.dragging).toEqual('mouse'); TestUtils.Simulate.mouseUp(drag.getDOMNode()); expect(drag.state.dragging).toEqual(false); }); }); + + describe('touch interaction', function () { + it('should initialize dragging ontouchstart', function () { + var drag = TestUtils.renderIntoDocument(
); + + TestUtils.Simulate.touchStart(drag.getDOMNode()); + expect(drag.state.dragging).toEqual('touch'); + }); + + it('should only initialize dragging ontouchstart of handle', function () { + var drag = TestUtils.renderIntoDocument( + +
+
Handle
+
Lorem ipsum...
+
+
+ ); + + TestUtils.Simulate.touchStart(drag.getDOMNode().querySelector('.content')); + expect(drag.state.dragging).toEqual(false); + + TestUtils.Simulate.touchStart(drag.getDOMNode().querySelector('.handle')); + expect(drag.state.dragging).toEqual('touch'); + }); + + it('should not initialize dragging ontouchstart of cancel', function () { + var drag = TestUtils.renderIntoDocument( + +
+
Cancel
+
Lorem ipsum...
+
+
+ ); + + TestUtils.Simulate.touchStart(drag.getDOMNode().querySelector('.cancel')); + expect(drag.state.dragging).toEqual(false); + + TestUtils.Simulate.touchStart(drag.getDOMNode().querySelector('.content')); + expect(drag.state.dragging).toEqual('touch'); + }); + + it('should discontinue dragging ontouchend', function () { + var drag = TestUtils.renderIntoDocument(
); + + TestUtils.Simulate.touchStart(drag.getDOMNode()); + expect(drag.state.dragging).toEqual('touch'); + + TestUtils.Simulate.touchEnd(drag.getDOMNode()); + expect(drag.state.dragging).toEqual(false); + }); + }); describe('validation', function () { it('should result with invariant when there isn\'t any children', function () { @@ -179,4 +259,60 @@ describe('react-draggable', function () { expect(error).toEqual(true); }); }); + + describe('mouse events', function () { + it('should pass through onMouseDown', function () { + var called = false; + + var drag = TestUtils.renderIntoDocument(
); + + TestUtils.Simulate.mouseDown(drag.getDOMNode()); + expect(called).toEqual(true); + }); + + it('should not drag if onMouseDown calls preventDefault', function () { + var drag = TestUtils.renderIntoDocument(
); + + TestUtils.Simulate.mouseDown(drag.getDOMNode()); + expect(drag.state.dragging).toEqual(false); + }); + + it('should pass through onMouseUp', function () { + var called = false; + + var drag = TestUtils.renderIntoDocument(
); + + TestUtils.Simulate.mouseUp(drag.getDOMNode()); + expect(called).toEqual(true); + }); + }); + + describe('touch events', function() { + it('should pass through onTouchStart', function () { + var called = false; + + var drag = TestUtils.renderIntoDocument(
); + + TestUtils.Simulate.touchStart(drag.getDOMNode()); + expect(called).toEqual(true); + }); + + it('should not drag if onTouchStart calls preventDefault', function () { + var drag = TestUtils.renderIntoDocument(
); + + TestUtils.Simulate.touchStart(drag.getDOMNode()); + expect(drag.state.dragging).toEqual(false); + }); + + + it('should pass through onTouchEnd', function () { + var called = false; + + var drag = TestUtils.renderIntoDocument(
); + + TestUtils.Simulate.touchEnd(drag.getDOMNode()); + expect(called).toEqual(true); + }); + }); + });