Skip to content

Commit ca2058a

Browse files
committed
Add bounds options.
Fixes #36, #35, #31.
1 parent b28566a commit ca2058a

File tree

4 files changed

+141
-5
lines changed

4 files changed

+141
-5
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ var App = React.createClass({
7474
//
7575
// `grid` specifies the x and y that dragging should snap to.
7676
//
77+
// `bounds` specifies movement boundaries. Pass:
78+
// - 'parent' restricts movement within the node's offsetParent
79+
// (nearest node with position relative or absolute), or
80+
// - An object with left, top, right, and bottom properties.
81+
//
7782
// `zIndex` specifies the zIndex to use while dragging.
7883
//
7984
// `onStart` is called when dragging starts.

example/index.html

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,14 +108,26 @@ <h1>React Draggable</h1>
108108
<Draggable grid={[50, 50]}>
109109
<div className="box">I snap to a 50 x 50 grid</div>
110110
</Draggable>
111-
<Draggable>
112-
<div className="box" style={{position: 'absolute', top: '400px', left: '700px'}}>
113-
I already have left and top properties assigned.
114-
</div>
111+
<Draggable bounds={{top: 0, left: 0, right: 300, bottom: 300}} zIndex={5}>
112+
<div className="box">I can only be moved within 300px bounds.</div>
115113
</Draggable>
114+
<div className="box" style={{height: '500px', width: '500px', position: 'relative'}}>
115+
<Draggable bounds="parent">
116+
<div className="box">
117+
I can only be moved within my offsetParent.<br /><br />
118+
Both parent padding and child margin work properly.
119+
</div>
120+
</Draggable>
121+
<Draggable bounds="parent">
122+
<div className="box">
123+
I also can only be moved within my offsetParent.<br /><br />
124+
Both parent padding and child margin work properly.
125+
</div>
126+
</Draggable>
127+
</div>
116128
<Draggable>
117129
<div className="box" style={{position: 'absolute', bottom: '100px', right: '100px'}}>
118-
I already have bottom and right properties assigned.
130+
I already have an absolute position.
119131
</div>
120132
</Draggable>
121133
</div>

lib/draggable.js

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,82 @@ function removeEvent(el, event, handler) {
125125
}
126126
}
127127

128+
function outerHeight(node) {
129+
// This is deliberately excluding margin for our calculations, since we are using
130+
// offsetTop which is including margin. See getBoundPosition
131+
var height = node.clientHeight;
132+
var computedStyle = window.getComputedStyle(node);
133+
height += int(computedStyle.borderTopWidth);
134+
height += int(computedStyle.borderBottomWidth);
135+
return height;
136+
}
137+
138+
function outerWidth(node) {
139+
// This is deliberately excluding margin for our calculations, since we are using
140+
// offsetLeft which is including margin. See getBoundPosition
141+
var width = node.clientWidth;
142+
var computedStyle = window.getComputedStyle(node);
143+
width += int(computedStyle.borderLeftWidth);
144+
width += int(computedStyle.borderRightWidth);
145+
return width;
146+
}
147+
function innerHeight(node) {
148+
var height = node.clientHeight;
149+
var computedStyle = window.getComputedStyle(node);
150+
height -= int(computedStyle.paddingTop);
151+
height -= int(computedStyle.paddingBottom);
152+
return height;
153+
}
154+
155+
function innerWidth(node) {
156+
var width = node.clientWidth;
157+
var computedStyle = window.getComputedStyle(node);
158+
width -= int(computedStyle.paddingLeft);
159+
width -= int(computedStyle.paddingRight);
160+
return width;
161+
}
162+
163+
function isNum(num) {
164+
return typeof num === 'number' && !isNaN(num);
165+
}
166+
167+
function int(a) {
168+
return parseInt(a, 10);
169+
}
170+
171+
function getBoundPosition(draggable, clientX, clientY) {
172+
var bounds = JSON.parse(JSON.stringify(draggable.props.bounds));
173+
var node = draggable.getDOMNode();
174+
var parent = node.parentNode;
175+
176+
if (bounds === 'parent') {
177+
var nodeStyle = window.getComputedStyle(node);
178+
var parentStyle = window.getComputedStyle(parent);
179+
// Compute bounds. This is a pain with padding and offsets but this gets it exactly right.
180+
bounds = {
181+
left: -node.offsetLeft + int(parentStyle.paddingLeft) +
182+
int(nodeStyle.borderLeftWidth) + int(nodeStyle.marginLeft),
183+
top: -node.offsetTop + int(parentStyle.paddingTop) +
184+
int(nodeStyle.borderTopWidth) + int(nodeStyle.marginTop),
185+
right: innerWidth(parent) - outerWidth(node) - node.offsetLeft,
186+
bottom: innerHeight(parent) - outerHeight(node) - node.offsetTop
187+
};
188+
} else {
189+
if (isNum(bounds.right)) bounds.right -= outerWidth(node);
190+
if (isNum(bounds.bottom)) bounds.bottom -= outerHeight(node);
191+
}
192+
193+
// Keep x and y below right and bottom limits...
194+
if (isNum(bounds.right)) clientX = Math.min(clientX, bounds.right);
195+
if (isNum(bounds.bottom)) clientY = Math.min(clientY, bounds.bottom);
196+
197+
// But above left and top limits.
198+
if (isNum(bounds.left)) clientX = Math.max(clientX, bounds.left);
199+
if (isNum(bounds.top)) clientY = Math.max(clientY, bounds.top);
200+
201+
return [clientX, clientY];
202+
}
203+
128204
function snapToGrid(grid, pendingX, pendingY) {
129205
var x = Math.round(pendingX / grid[0]) * grid[0];
130206
var y = Math.round(pendingY / grid[1]) * grid[1];
@@ -185,6 +261,42 @@ module.exports = React.createClass({
185261
*/
186262
axis: React.PropTypes.oneOf(['both', 'x', 'y']),
187263

264+
/**
265+
* `bounds` determines the range of movement available to the element.
266+
* Available values are:
267+
*
268+
* 'parent' restricts movement within the Draggable's parent node.
269+
*
270+
* Alternatively, pass an object with the following properties, all of which are optional:
271+
*
272+
* {left: LEFT_BOUND, right: RIGHT_BOUND, bottom: BOTTOM_BOUND, top: TOP_BOUND}
273+
*
274+
* All values are in px.
275+
*
276+
* Example:
277+
*
278+
* ```jsx
279+
* var App = React.createClass({
280+
* render: function () {
281+
* return (
282+
* <Draggable bounds={{right: 300, bottom: 300}}>
283+
* <div>Content</div>
284+
* </Draggable>
285+
* );
286+
* }
287+
* });
288+
* ```
289+
*/
290+
bounds: React.PropTypes.oneOfType([
291+
React.PropTypes.shape({
292+
left: React.PropTypes.Number,
293+
right: React.PropTypes.Number,
294+
top: React.PropTypes.Number,
295+
bottom: React.PropTypes.Number
296+
}),
297+
React.PropTypes.oneOf(['parent', false])
298+
]),
299+
188300
/**
189301
* By default, we add 'user-select:none' attributes to the document body
190302
* to prevent ugly text selection during drag. If this is causing problems
@@ -353,6 +465,7 @@ module.exports = React.createClass({
353465
getDefaultProps: function () {
354466
return {
355467
axis: 'both',
468+
bounds: false,
356469
handle: null,
357470
cancel: null,
358471
grid: null,
@@ -454,6 +567,11 @@ module.exports = React.createClass({
454567
clientX = coords[0], clientY = coords[1];
455568
}
456569

570+
if (this.props.bounds) {
571+
var pos = getBoundPosition(this, clientX, clientY);
572+
clientX = pos[0], clientY = pos[1];
573+
}
574+
457575
// Call event handler. If it returns explicit false, cancel.
458576
var shouldUpdate = this.props.onDrag(e, createUIEvent(this));
459577
if (shouldUpdate === false) return this.handleDragEnd();

specs/draggable.spec.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ describe('react-draggable', function () {
1717
expect(drag.props.axis).toEqual('both');
1818
expect(drag.props.handle).toEqual(null);
1919
expect(drag.props.cancel).toEqual(null);
20+
expect(drag.props.bounds).toBeFalsy();
2021
expect(isNaN(drag.props.zIndex)).toEqual(true);
2122
expect(typeof drag.props.onStart).toEqual('function');
2223
expect(typeof drag.props.onDrag).toEqual('function');

0 commit comments

Comments
 (0)