Skip to content

Commit

Permalink
Add isBounded property so items could be constrained within grid (rea…
Browse files Browse the repository at this point in the history
…ct-grid-layout#1248)

Co-authored-by: Artem Bykov <[email protected]>
  • Loading branch information
STRML and Artem Bykov authored Jul 20, 2020
1 parent 83251e5 commit da07f0f
Show file tree
Hide file tree
Showing 12 changed files with 269 additions and 79 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ examples/*.html
examples/*.js
!examples/generate.js
!examples/vars.js
.vscode
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ test:
test-watch:
env NODE_ENV=test $(BIN)/jest --watch

test-update-snapshots:
env NODE_ENV=test $(BIN)/jest --updateSnapshot

release-patch: build lint test
@$(call release,patch)

Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ RGL is React-only and does not require jQuery.
1. [Error Case](https://strml.github.io/react-grid-layout/examples/13-error-case.html)
1. [Toolbox](https://strml.github.io/react-grid-layout/examples/14-toolbox.html)
1. [Drag From Outside](https://strml.github.io/react-grid-layout/examples/15-drag-from-outside.html)
1. [Bounded Layout](https://strml.github.io/react-grid-layout/examples/16-bounded.html)

#### Projects Using React-Grid-Layout

Expand Down Expand Up @@ -304,6 +305,7 @@ droppingItem?: { i: string, w: number, h: number }
//
isDraggable: ?boolean = true,
isResizable: ?boolean = true,
isBounded: ?boolean = false,
// Uses CSS3 translate() instead of position top/left.
// This makes about 6x faster paint performance
useCSSTransforms: ?boolean = true,
Expand Down Expand Up @@ -443,7 +445,9 @@ will be draggable, even if the item is marked `static: true`.
// If false, will not be draggable. Overrides `static`.
isDraggable: ?boolean = true,
// If false, will not be resizable. Overrides `static`.
isResizable: ?boolean = true
isResizable: ?boolean = true,
// If true and draggable, item will be moved only within grid.
isBounded: ?boolean = false
}
```
Expand Down
7 changes: 7 additions & 0 deletions examples/vars.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,5 +139,12 @@ module.exports = [
"Once you drop the item within the grid you'll get its coordinates/properties and can perform actions with " +
"it accordingly."
]
},
{
title: "Bounded",
source: "bounded",
paragraphs: [
"Try dragging the elements around. They can only be moved within the grid, the draggable placeholder will not show outside it."
]
}
];
132 changes: 73 additions & 59 deletions lib/GridItem.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ import PropTypes from "prop-types";
import { DraggableCore } from "react-draggable";
import { Resizable } from "react-resizable";
import { fastPositionEqual, perc, setTopLeft, setTransform } from "./utils";
import { calcGridItemPosition, calcXY, calcWH } from "./calculateUtils";
import {
calcGridItemPosition,
calcGridItemWHPx,
calcGridColWidth,
calcXY,
calcWH,
clamp
} from "./calculateUtils";
import classNames from "classnames";
import type { Element as ReactElement, Node as ReactNode } from "react";

Expand Down Expand Up @@ -43,6 +50,7 @@ type Props = {
maxRows: number,
isDraggable: boolean,
isResizable: boolean,
isBounded: boolean,
static?: boolean,
useCSSTransforms?: boolean,
usePercentages?: boolean,
Expand Down Expand Up @@ -139,6 +147,7 @@ export default class GridItem extends React.Component<Props, State> {
// Flags
isDraggable: PropTypes.bool.isRequired,
isResizable: PropTypes.bool.isRequired,
isBounded: PropTypes.bool.isRequired,
static: PropTypes.bool,

// Use CSS transforms instead of top/left
Expand Down Expand Up @@ -374,7 +383,8 @@ export default class GridItem extends React.Component<Props, State> {
* @param {Object} callbackData an object with node, delta and position information
*/
onDragStart = (e: Event, { node }: ReactDraggableCallbackData) => {
if (!this.props.onDragStart) return;
const { onDragStart } = this.props;
if (!onDragStart) return;

const newPosition: PartialPosition = { top: 0, left: 0 };

Expand All @@ -391,6 +401,7 @@ export default class GridItem extends React.Component<Props, State> {
newPosition.top = cTop - pTop + offsetParent.scrollTop;
this.setState({ dragging: newPosition });

// Call callback with this data
const { x, y } = calcXY(
this.getPositionParams(),
newPosition.top,
Expand All @@ -399,14 +410,11 @@ export default class GridItem extends React.Component<Props, State> {
this.props.h
);

return (
this.props.onDragStart &&
this.props.onDragStart.call(this, this.props.i, x, y, {
e,
node,
newPosition
})
);
return onDragStart.call(this, this.props.i, x, y, {
e,
node,
newPosition
});
};

/**
Expand All @@ -420,30 +428,42 @@ export default class GridItem extends React.Component<Props, State> {
deltaX /= transformScale;
deltaY /= transformScale;

const newPosition: PartialPosition = { top: 0, left: 0 };

if (!this.state.dragging)
if (!this.state.dragging) {
throw new Error("onDrag called before onDragStart.");
newPosition.left = this.state.dragging.left + deltaX;
newPosition.top = this.state.dragging.top + deltaY;
this.setState({ dragging: newPosition });
}
let top = this.state.dragging.top + deltaY;
let left = this.state.dragging.left + deltaX;

const { x, y } = calcXY(
this.getPositionParams(),
newPosition.top,
newPosition.left,
this.props.w,
this.props.h
);
const { isBounded, i, w, h, containerWidth } = this.props;
const positionParams = this.getPositionParams();

return (
onDrag &&
onDrag.call(this, this.props.i, x, y, {
e,
node,
newPosition
})
);
// Boundary calculations; keeps items within the grid
if (isBounded) {
const { offsetParent } = node;

if (offsetParent) {
const { margin, rowHeight } = this.props;
const bottomBoundary =
offsetParent.clientHeight - calcGridItemWHPx(h, rowHeight, margin[1]);
top = clamp(top, 0, bottomBoundary);

const colWidth = calcGridColWidth(positionParams);
const rightBoundary =
containerWidth - calcGridItemWHPx(w, colWidth, margin[0]);
left = clamp(left, 0, rightBoundary);
}
}

const newPosition: PartialPosition = { top, left };
this.setState({ dragging: newPosition });

// Call callback with this data
const { x, y } = calcXY(positionParams, top, left, w, h);
return onDrag.call(this, i, x, y, {
e,
node,
newPosition
});
};

/**
Expand All @@ -452,32 +472,24 @@ export default class GridItem extends React.Component<Props, State> {
* @param {Object} callbackData an object with node, delta and position information
*/
onDragStop = (e: Event, { node }: ReactDraggableCallbackData) => {
if (!this.props.onDragStop) return;
const { onDragStop } = this.props;
if (!onDragStop) return;

const newPosition: PartialPosition = { top: 0, left: 0 };

if (!this.state.dragging)
if (!this.state.dragging) {
throw new Error("onDragEnd called before onDragStart.");
newPosition.left = this.state.dragging.left;
newPosition.top = this.state.dragging.top;
}
const { w, h, i } = this.props;
const { left, top } = this.state.dragging;
const newPosition: PartialPosition = { top, left };
this.setState({ dragging: null });

const { x, y } = calcXY(
this.getPositionParams(),
newPosition.top,
newPosition.left,
this.props.w,
this.props.h
);
const { x, y } = calcXY(this.getPositionParams(), top, left, w, h);

return (
this.props.onDragStop &&
this.props.onDragStop.call(this, this.props.i, x, y, {
e,
node,
newPosition
})
);
return onDragStop.call(this, i, x, y, {
e,
node,
newPosition
});
};

/**
Expand Down Expand Up @@ -531,7 +543,8 @@ export default class GridItem extends React.Component<Props, State> {
) {
const handler = this.props[handlerName];
if (!handler) return;
const { cols, x, y, i, maxW, minW, maxH, minH } = this.props;
const { cols, x, y, i, maxH, minH } = this.props;
let { minW, maxW } = this.props;

// Get new XY
let { w, h } = calcWH(
Expand All @@ -542,14 +555,15 @@ export default class GridItem extends React.Component<Props, State> {
y
);

// Cap w at numCols
w = Math.min(w, cols - x);
// Ensure w is at least 1
w = Math.max(w, 1);
// minW should be at least 1 (TODO propTypes validation?)
minW = Math.max(minW, 1);

// maxW should be at most (cols - x)
maxW = Math.min(maxW, cols - x);

// Min/max capping
w = Math.max(Math.min(w, maxW), minW);
h = Math.max(Math.min(h, maxH), minH);
w = clamp(w, minW, maxW);
h = clamp(h, minH, maxH);

this.setState({ resizing: handlerName === "onResizeStop" ? null : size });

Expand Down
7 changes: 7 additions & 0 deletions lib/ReactGridLayout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export default class ReactGridLayout extends React.Component<Props, State> {
maxRows: Infinity, // infinite vertical growth
layout: [],
margin: [10, 10],
isBounded: false,
isDraggable: true,
isResizable: true,
isDroppable: false,
Expand Down Expand Up @@ -477,6 +478,7 @@ export default class ReactGridLayout extends React.Component<Props, State> {
rowHeight={rowHeight}
isDraggable={false}
isResizable={false}
isBounded={false}
useCSSTransforms={useCSSTransforms}
transformScale={transformScale}
>
Expand Down Expand Up @@ -506,6 +508,7 @@ export default class ReactGridLayout extends React.Component<Props, State> {
maxRows,
isDraggable,
isResizable,
isBounded,
useCSSTransforms,
transformScale,
draggableCancel,
Expand All @@ -525,6 +528,9 @@ export default class ReactGridLayout extends React.Component<Props, State> {
? l.isResizable
: !l.static && isResizable;

// isBounded set on child if set on parent, and child is not explicitly false
const bounded = draggable && isBounded && l.isBounded !== false;

return (
<GridItem
containerWidth={width}
Expand All @@ -543,6 +549,7 @@ export default class ReactGridLayout extends React.Component<Props, State> {
onResizeStop={this.onResizeStop}
isDraggable={draggable}
isResizable={resizable}
isBounded={bounded}
useCSSTransforms={useCSSTransforms && mounted}
usePercentages={!mounted}
transformScale={transformScale}
Expand Down
2 changes: 2 additions & 0 deletions lib/ReactGridLayoutPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export type Props = {|
containerPadding: [number, number] | null,
rowHeight: number,
maxRows: number,
isBounded: boolean,
isDraggable: boolean,
isResizable: boolean,
isDroppable: boolean,
Expand Down Expand Up @@ -117,6 +118,7 @@ export default {
//
// Flags
//
isBounded: PropTypes.bool,
isDraggable: PropTypes.bool,
isResizable: PropTypes.bool,
// If true, grid items won't change position when being dragged over.
Expand Down
43 changes: 27 additions & 16 deletions lib/calculateUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,22 @@ export function calcGridColWidth(positionParams: PositionParams): number {
);
}

// This can either be called:
// calcGridItemWHPx(w, colWidth, margin[0])
// or
// calcGridItemWHPx(h, rowHeight, margin[1])
export function calcGridItemWHPx(
gridUnits: number,
colOrRowSize: number,
marginPx: number
) {
// 0 * Infinity === NaN, which causes problems with resize contraints
if (!Number.isFinite(gridUnits)) return gridUnits;
return Math.round(
colOrRowSize * gridUnits + Math.max(0, gridUnits - 1) * marginPx
);
}

/**
* Return position on the page given an x, y, w, h.
* left, top, width, height are all in pixels.
Expand Down Expand Up @@ -47,17 +63,8 @@ export function calcGridItemPosition(
}
// Otherwise, calculate from grid units.
else {
// 0 * Infinity === NaN, which causes problems with resize constraints;
// Fix this if it occurs.
// Note we do it here rather than later because Math.round(Infinity) causes deopt
out.width =
w === Infinity
? w
: Math.round(colWidth * w + Math.max(0, w - 1) * margin[0]);
out.height =
h === Infinity
? h
: Math.round(rowHeight * h + Math.max(0, h - 1) * margin[1]);
out.width = calcGridItemWHPx(w, colWidth, margin[0]);
out.height = calcGridItemWHPx(h, rowHeight, margin[1]);
}

// If dragging, use the exact width and height as returned from dragging callbacks.
Expand Down Expand Up @@ -104,9 +111,8 @@ export function calcXY(
let y = Math.round((top - margin[1]) / (rowHeight + margin[1]));

// Capping
x = Math.max(Math.min(x, cols - w), 0);
y = Math.max(Math.min(y, maxRows - h), 0);

x = clamp(x, 0, cols - w);
y = clamp(y, 0, maxRows - h);
return { x, y };
}

Expand Down Expand Up @@ -136,7 +142,12 @@ export function calcWH(
let h = Math.round((height + margin[1]) / (rowHeight + margin[1]));

// Capping
w = Math.max(Math.min(w, cols - x), 0);
h = Math.max(Math.min(h, maxRows - y), 0);
w = clamp(w, 0, cols - x);
h = clamp(h, 0, maxRows - y);
return { w, h };
}

// Similar to _.clamp
export function clamp(num: number, lowerBound: number, upperBound: number) {
return Math.max(Math.min(num, upperBound), lowerBound);
}
Loading

0 comments on commit da07f0f

Please sign in to comment.