Skip to content

Commit 4ec45f8

Browse files
committed
Simplify implementation by using CSS Transforms instead of positioning.
Speeds up framerate as well but kills IE8 support.
1 parent ec17025 commit 4ec45f8

File tree

3 files changed

+96
-73
lines changed

3 files changed

+96
-73
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,17 @@ $ npm install react-draggable
1717
$ bower install react-draggable
1818
```
1919

20+
## Details
21+
22+
A `<Draggable>` element wraps an existing element and extends it with new event handlers and styles.
23+
It does not create a wrapper element in the DOM.
24+
25+
Draggable items are moved using CSS Transforms. This allows items to be dragged regardless of their current
26+
positioning (relative, absolute, or static). Elements can also be moved between drags without incident.
27+
28+
If the item you are dragging already has a CSS Transform applied, it will be overwritten by `<Draggable>`. Use
29+
an intermediate wrapper (`<Draggable><span>...</span></Draggable>`) in this case.
30+
2031
## Example
2132

2233
```js

lib/draggable.js

Lines changed: 63 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ var classNames = require('classnames');
1010
//
1111

1212
function createUIEvent(draggable) {
13-
// State changes are often (but not always!) async. We want the latest value.
14-
var state = draggable._pendingState || draggable.state;
13+
// State changes are often (but not always!) async. We want the latest value.
14+
var state = draggable._pendingState || draggable.state;
1515
return {
16-
node: draggable.getDOMNode(),
16+
node: draggable.getDOMNode(),
1717
position: {
1818
top: state.clientY,
1919
left: state.clientX
@@ -126,18 +126,20 @@ function removeEvent(el, event, handler) {
126126
}
127127

128128
function snapToGrid(draggable, clientX, clientY) {
129-
var directionX = clientX < parseInt(draggable.state.clientX, 10) ? -1 : 1;
130-
var directionY = clientY < parseInt(draggable.state.clientY, 10) ? -1 : 1;
129+
var stateX = parseInt(draggable.state.clientX, 10);
130+
var stateY = parseInt(draggable.state.clientY, 10);
131+
var directionX = clientX < stateX ? -1 : 1;
132+
var directionY = clientY < stateY ? -1 : 1;
131133

132-
clientX = Math.abs(clientX - parseInt(draggable.state.clientX, 10)) >= draggable.props.grid[0] ?
133-
(parseInt(draggable.state.clientX, 10) + (draggable.props.grid[0] * directionX)) :
134-
draggable.state.clientX;
134+
clientX = Math.abs(clientX - stateX) >= draggable.props.grid[0] ?
135+
(stateX + (draggable.props.grid[0] * directionX)) :
136+
stateX;
135137

136-
clientY = Math.abs(clientY - parseInt(draggable.state.clientY, 10)) >= draggable.props.grid[1] ?
137-
(parseInt(draggable.state.clientY, 10) + (draggable.props.grid[1] * directionY)) :
138-
draggable.state.clientY;
138+
clientY = Math.abs(clientY - stateY) >= draggable.props.grid[1] ?
139+
(stateY + (draggable.props.grid[1] * directionY)) :
140+
stateY;
139141

140-
return [clientX, clientY];
142+
return [clientX, clientY];
141143
}
142144

143145
// Useful for preventing blue highlights all over everything when dragging.
@@ -149,6 +151,21 @@ var userSelectStyle = {
149151
userSelect: 'none',
150152
};
151153

154+
function createCSSTransform(style) {
155+
if (!style.x && !style.y) return {};
156+
// Replace unitless items with px
157+
var x = style.x + 'px';
158+
var y = style.y + 'px';
159+
return {
160+
transform: 'translate(' + x + ',' + y + ')',
161+
WebkitTransform: 'translate(' + x + ',' + y + ')',
162+
OTransform: 'translate(' + x + ',' + y + ')',
163+
msTransform: 'translate(' + x + ',' + y + ')',
164+
MozTransform: 'translate(' + x + ',' + y + ')'
165+
};
166+
}
167+
168+
152169
//
153170
// End Helpers.
154171
//
@@ -315,23 +332,12 @@ module.exports = React.createClass({
315332
onStop: React.PropTypes.func,
316333

317334
/**
318-
* 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)
319-
*
335+
* A workaround option which can be passed if onMouseDown needs to be accessed,
336+
* since it'll always be blocked (due to that there's internal use of onMouseDown)
320337
*/
321338
onMouseDown: React.PropTypes.func,
322339
},
323340

324-
componentDidMount: function() {
325-
var node = this.getDOMNode();
326-
this.setState({
327-
mounted: true,
328-
startX: node.offsetLeft,
329-
startY: node.offsetTop,
330-
clientX: node.offsetLeft,
331-
clientY: node.offsetTop
332-
});
333-
},
334-
335341
componentWillUnmount: function() {
336342
// Remove any leftover event handlers
337343
removeEvent(window, dragEventFor['move'], this.handleDrag);
@@ -354,19 +360,13 @@ module.exports = React.createClass({
354360

355361
getInitialState: function () {
356362
return {
357-
// Whether or not this has been mounted
358-
mounted: false,
359-
360-
// Whether or not currently dragging
363+
// Whether or not we are currently dragging.
361364
dragging: false,
362365

363-
// Start top/left of this.getDOMNode()
364-
startX: 0, startY: 0,
365-
366-
// Offset between start top/left and mouse top/left
366+
// Offset between start top/left and mouse top/left while dragging.
367367
offsetX: 0, offsetY: 0,
368368

369-
// Current top/left of this.getDOMNode()
369+
// Current transform x and y.
370370
clientX: 0, clientY: 0
371371
};
372372
},
@@ -379,8 +379,6 @@ module.exports = React.createClass({
379379
// return
380380
// }
381381

382-
var node = this.getDOMNode();
383-
384382
// Make it possible to attach event handlers on top of this one
385383
this.props.onMouseDown(e);
386384

@@ -392,15 +390,13 @@ module.exports = React.createClass({
392390

393391
var dragPoint = getControlPosition(e);
394392

395-
// Initiate dragging
393+
// Initiate dragging. Set the current x and y as offsets
394+
// so we know how much we've moved during the drag. This allows us
395+
// to drag elements around even if they have been moved, without issue.
396396
this.setState({
397397
dragging: true,
398-
offsetX: parseInt(dragPoint.clientX, 10),
399-
offsetY: parseInt(dragPoint.clientY, 10),
400-
// Reset starting offsets. It's possible this item might have been
401-
// moved by external forces, class changes, whatever.
402-
startX: node.offsetLeft,
403-
startY: node.offsetTop
398+
offsetX: dragPoint.clientX - this.state.clientX,
399+
offsetY: dragPoint.clientY - this.state.clientY
404400
});
405401

406402
// Call event handler
@@ -433,20 +429,20 @@ module.exports = React.createClass({
433429
handleDrag: function (e) {
434430
var dragPoint = getControlPosition(e);
435431

436-
// Calculate top and left
437-
var clientX = (this.state.startX + (dragPoint.clientX - this.state.offsetX));
438-
var clientY = (this.state.startY + (dragPoint.clientY - this.state.offsetY));
432+
// Calculate X and Y
433+
var clientX = dragPoint.clientX - this.state.offsetX;
434+
var clientY = dragPoint.clientY - this.state.offsetY;
439435

440436
// Snap to grid if prop has been provided
441437
if (Array.isArray(this.props.grid)) {
442438
var coords = snapToGrid(this, clientX, clientY);
443439
clientX = coords[0], clientY = coords[1];
444440
}
445441

446-
// Update top and left
442+
// Update transform
447443
this.setState({
448-
clientX: clientX,
449-
clientY: clientY
444+
clientX: clientX,
445+
clientY: clientY
450446
});
451447

452448
// Call event handler
@@ -456,28 +452,24 @@ module.exports = React.createClass({
456452
render: function () {
457453
// Create style object. We extend from existing styles so we don't
458454
// remove anything already set (like background, color, etc).
459-
// We do completely overwrite the position.
460-
var style = this.props.children.props.style || {};
461-
if (this.state.mounted) {
462-
style = assign({}, userSelectStyle, style, {
463-
// Set top if vertical drag is enabled
464-
top: canDragY(this) ?
465-
this.state.clientY :
466-
this.state.startY,
467-
468-
// Set left if horizontal drag is enabled
469-
left: canDragX(this) ?
470-
this.state.clientX :
471-
this.state.startX,
472-
473-
position: 'absolute'
474-
});
475-
476-
// We only position with top/left. Delete bottom/right as they
477-
// will screw us up.
478-
delete style.bottom;
479-
delete style.right;
480-
}
455+
var childStyle = this.props.children.props.style || {};
456+
457+
// Add a CSS transform to move the element around. This allows us to move the element around
458+
// without worrying about whether or not it is relatively or absolutely positioned.
459+
// If the item you are dragging already has a transform set, wrap it in a <span> so <Draggable>
460+
// has a clean slate.
461+
var transform = createCSSTransform({
462+
// Set left if horizontal drag is enabled
463+
x: canDragX(this) ?
464+
this.state.clientX :
465+
0,
466+
467+
// Set top if vertical drag is enabled
468+
y: canDragY(this) ?
469+
this.state.clientY :
470+
0
471+
});
472+
var style = assign({}, userSelectStyle, childStyle, transform);
481473

482474
// Set zIndex if currently dragging and prop has been provided
483475
if (this.state.dragging && !isNaN(this.props.zIndex)) {

specs/draggable.spec.js

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ describe('react-draggable', function () {
2525
var output = renderer.getRenderOutput();
2626

2727
expect(output.props.className).toEqual('foo react-draggable');
28-
expect(output.props.style).toEqual({color: 'black'});
28+
expect(output.props.style.color).toEqual('black');
29+
// This should get added
30+
expect(output.props.style.userSelect).toEqual('none');
2931
});
3032

3133
it('should honor props', function () {
@@ -84,6 +86,25 @@ describe('react-draggable', function () {
8486
TestUtils.Simulate.mouseUp(drag.getDOMNode());
8587
expect(called).toEqual(true);
8688
});
89+
90+
it('should render with translate()', function () {
91+
var drag = TestUtils.renderIntoDocument(
92+
<Draggable>
93+
<div />
94+
</Draggable>
95+
);
96+
97+
var node = drag.getDOMNode();
98+
99+
TestUtils.Simulate.mouseDown(node, {clientX: 0, clientY: 0});
100+
// FIXME why doesn't simulate on the window work?
101+
// TestUtils.Simulate.mouseMove(window, {clientX: 100, clientY: 100});
102+
drag.handleDrag({clientX: 100, clientY:100}); // hack
103+
TestUtils.Simulate.mouseUp(node);
104+
105+
var style = node.getAttribute('style');
106+
expect(style.indexOf('transform: translate(100px, 100px);')).not.toEqual(-1);
107+
});
87108
});
88109

89110
describe('interaction', function () {
@@ -92,7 +113,6 @@ describe('react-draggable', function () {
92113

93114
TestUtils.Simulate.mouseDown(drag.getDOMNode());
94115
expect(drag.state.dragging).toEqual(true);
95-
expect(drag.state.mounted).toEqual(true);
96116
});
97117

98118
it('should only initialize dragging onmousedown of handle', function () {

0 commit comments

Comments
 (0)