@@ -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+
128204function 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 ( ) ;
0 commit comments