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);
+ });
+ });
+
});