Files
goTorrent/goTorrentWebUI/node_modules/react-grid-layout/build/ReactGridLayout.js.flow

646 lines
18 KiB
Plaintext

// @flow
import React from "react";
import PropTypes from "prop-types";
import isEqual from "lodash.isequal";
import classNames from "classnames";
import {
autoBindHandlers,
bottom,
childrenEqual,
cloneLayoutItem,
compact,
getLayoutItem,
moveElement,
synchronizeLayoutWithChildren,
validateLayout,
getAllCollisions,
noop
} from "./utils";
import GridItem from "./GridItem";
import type {
ChildrenArray as ReactChildrenArray,
Element as ReactElement
} from "react";
// Types
import type {
EventCallback,
CompactType,
GridResizeEvent,
GridDragEvent,
Layout,
LayoutItem
} from "./utils";
type State = {
activeDrag: ?LayoutItem,
layout: Layout,
mounted: boolean,
oldDragItem: ?LayoutItem,
oldLayout: ?Layout,
oldResizeItem: ?LayoutItem
};
export type Props = {
className: string,
style: Object,
width: number,
autoSize: boolean,
cols: number,
draggableCancel: string,
draggableHandle: string,
verticalCompact: boolean,
compactType: ?("horizontal" | "vertical"),
layout: Layout,
margin: [number, number],
containerPadding: [number, number] | null,
rowHeight: number,
maxRows: number,
isDraggable: boolean,
isResizable: boolean,
preventCollision: boolean,
useCSSTransforms: boolean,
// Callbacks
onLayoutChange: Layout => void,
onDrag: EventCallback,
onDragStart: EventCallback,
onDragStop: EventCallback,
onResize: EventCallback,
onResizeStart: EventCallback,
onResizeStop: EventCallback,
children: ReactChildrenArray<ReactElement<any>>
};
// End Types
/**
* A reactive, fluid grid layout with draggable, resizable components.
*/
export default class ReactGridLayout extends React.Component<Props, State> {
// TODO publish internal ReactClass displayName transform
static displayName = "ReactGridLayout";
static propTypes = {
//
// Basic props
//
className: PropTypes.string,
style: PropTypes.object,
// This can be set explicitly. If it is not set, it will automatically
// be set to the container width. Note that resizes will *not* cause this to adjust.
// If you need that behavior, use WidthProvider.
width: PropTypes.number,
// If true, the container height swells and contracts to fit contents
autoSize: PropTypes.bool,
// # of cols.
cols: PropTypes.number,
// A selector that will not be draggable.
draggableCancel: PropTypes.string,
// A selector for the draggable handler
draggableHandle: PropTypes.string,
// Deprecated
verticalCompact: function(props: Props) {
if (
props.verticalCompact === false &&
process.env.NODE_ENV !== "production"
) {
console.warn(
// eslint-disable-line no-console
"`verticalCompact` on <ReactGridLayout> is deprecated and will be removed soon. " +
'Use `compactType`: "horizontal" | "vertical" | null.'
);
}
},
// Choose vertical or hotizontal compaction
compactType: PropTypes.oneOf(["vertical", "horizontal"]),
// layout is an array of object with the format:
// {x: Number, y: Number, w: Number, h: Number, i: String}
layout: function(props: Props) {
var layout = props.layout;
// I hope you're setting the data-grid property on the grid items
if (layout === undefined) return;
validateLayout(layout, "layout");
},
//
// Grid Dimensions
//
// Margin between items [x, y] in px
margin: PropTypes.arrayOf(PropTypes.number),
// Padding inside the container [x, y] in px
containerPadding: PropTypes.arrayOf(PropTypes.number),
// Rows have a static height, but you can change this based on breakpoints if you like
rowHeight: PropTypes.number,
// Default Infinity, but you can specify a max here if you like.
// Note that this isn't fully fleshed out and won't error if you specify a layout that
// extends beyond the row capacity. It will, however, not allow users to drag/resize
// an item past the barrier. They can push items beyond the barrier, though.
// Intentionally not documented for this reason.
maxRows: PropTypes.number,
//
// Flags
//
isDraggable: PropTypes.bool,
isResizable: PropTypes.bool,
// If true, grid items won't change position when being dragged over.
preventCollision: PropTypes.bool,
// Use CSS transforms instead of top/left
useCSSTransforms: PropTypes.bool,
//
// Callbacks
//
// Callback so you can save the layout. Calls after each drag & resize stops.
onLayoutChange: PropTypes.func,
// Calls when drag starts. Callback is of the signature (layout, oldItem, newItem, placeholder, e, ?node).
// All callbacks below have the same signature. 'start' and 'stop' callbacks omit the 'placeholder'.
onDragStart: PropTypes.func,
// Calls on each drag movement.
onDrag: PropTypes.func,
// Calls when drag is complete.
onDragStop: PropTypes.func,
//Calls when resize starts.
onResizeStart: PropTypes.func,
// Calls when resize movement happens.
onResize: PropTypes.func,
// Calls when resize is complete.
onResizeStop: PropTypes.func,
//
// Other validations
//
// Children must not have duplicate keys.
children: function(props: Props, propName: string) {
var children = props[propName];
// Check children keys for duplicates. Throw if found.
var keys = {};
React.Children.forEach(children, function(child) {
if (keys[child.key]) {
throw new Error(
'Duplicate child key "' +
child.key +
'" found! This will cause problems in ReactGridLayout.'
);
}
keys[child.key] = true;
});
}
};
static defaultProps = {
autoSize: true,
cols: 12,
className: "",
style: {},
draggableHandle: "",
draggableCancel: "",
containerPadding: null,
rowHeight: 150,
maxRows: Infinity, // infinite vertical growth
layout: [],
margin: [10, 10],
isDraggable: true,
isResizable: true,
useCSSTransforms: true,
verticalCompact: true,
compactType: "vertical",
preventCollision: false,
onLayoutChange: noop,
onDragStart: noop,
onDrag: noop,
onDragStop: noop,
onResizeStart: noop,
onResize: noop,
onResizeStop: noop
};
state: State = {
activeDrag: null,
layout: synchronizeLayoutWithChildren(
this.props.layout,
this.props.children,
this.props.cols,
// Legacy support for verticalCompact: false
this.compactType()
),
mounted: false,
oldDragItem: null,
oldLayout: null,
oldResizeItem: null
};
constructor(props: Props, context: any): void {
super(props, context);
autoBindHandlers(this, [
"onDragStart",
"onDrag",
"onDragStop",
"onResizeStart",
"onResize",
"onResizeStop"
]);
}
componentDidMount() {
this.setState({ mounted: true });
// Possibly call back with layout on mount. This should be done after correcting the layout width
// to ensure we don't rerender with the wrong width.
this.onLayoutMaybeChanged(this.state.layout, this.props.layout);
}
componentWillReceiveProps(nextProps: Props) {
let newLayoutBase;
// Legacy support for compactType
// Allow parent to set layout directly.
if (
!isEqual(nextProps.layout, this.props.layout) ||
nextProps.compactType !== this.props.compactType
) {
newLayoutBase = nextProps.layout;
} else if (!childrenEqual(this.props.children, nextProps.children)) {
// If children change, also regenerate the layout. Use our state
// as the base in case because it may be more up to date than
// what is in props.
newLayoutBase = this.state.layout;
}
// We need to regenerate the layout.
if (newLayoutBase) {
const newLayout = synchronizeLayoutWithChildren(
newLayoutBase,
nextProps.children,
nextProps.cols,
this.compactType(nextProps)
);
const oldLayout = this.state.layout;
this.setState({ layout: newLayout });
this.onLayoutMaybeChanged(newLayout, oldLayout);
}
}
/**
* Calculates a pixel value for the container.
* @return {String} Container height in pixels.
*/
containerHeight() {
if (!this.props.autoSize) return;
const nbRow = bottom(this.state.layout);
const containerPaddingY = this.props.containerPadding
? this.props.containerPadding[1]
: this.props.margin[1];
return (
nbRow * this.props.rowHeight +
(nbRow - 1) * this.props.margin[1] +
containerPaddingY * 2 +
"px"
);
}
compactType(props: ?Object): CompactType {
if (!props) props = this.props;
return props.verticalCompact === false ? null : props.compactType;
}
/**
* When dragging starts
* @param {String} i Id of the child
* @param {Number} x X position of the move
* @param {Number} y Y position of the move
* @param {Event} e The mousedown event
* @param {Element} node The current dragging DOM element
*/
onDragStart(i: string, x: number, y: number, { e, node }: GridDragEvent) {
const { layout } = this.state;
var l = getLayoutItem(layout, i);
if (!l) return;
this.setState({
oldDragItem: cloneLayoutItem(l),
oldLayout: this.state.layout
});
return this.props.onDragStart(layout, l, l, null, e, node);
}
/**
* Each drag movement create a new dragelement and move the element to the dragged location
* @param {String} i Id of the child
* @param {Number} x X position of the move
* @param {Number} y Y position of the move
* @param {Event} e The mousedown event
* @param {Element} node The current dragging DOM element
*/
onDrag(i: string, x: number, y: number, { e, node }: GridDragEvent) {
const { oldDragItem } = this.state;
let { layout } = this.state;
const { cols } = this.props;
var l = getLayoutItem(layout, i);
if (!l) return;
// Create placeholder (display only)
var placeholder = {
w: l.w,
h: l.h,
x: l.x,
y: l.y,
placeholder: true,
i: i
};
// Move the element to the dragged location.
const isUserAction = true;
layout = moveElement(
layout,
l,
x,
y,
isUserAction,
this.props.preventCollision,
this.compactType(),
cols
);
this.props.onDrag(layout, oldDragItem, l, placeholder, e, node);
this.setState({
layout: compact(layout, this.compactType(), cols),
activeDrag: placeholder
});
}
/**
* When dragging stops, figure out which position the element is closest to and update its x and y.
* @param {String} i Index of the child.
* @param {Number} x X position of the move
* @param {Number} y Y position of the move
* @param {Event} e The mousedown event
* @param {Element} node The current dragging DOM element
*/
onDragStop(i: string, x: number, y: number, { e, node }: GridDragEvent) {
const { oldDragItem } = this.state;
let { layout } = this.state;
const { cols, preventCollision } = this.props;
const l = getLayoutItem(layout, i);
if (!l) return;
// Move the element here
const isUserAction = true;
layout = moveElement(
layout,
l,
x,
y,
isUserAction,
preventCollision,
this.compactType(),
cols
);
this.props.onDragStop(layout, oldDragItem, l, null, e, node);
// Set state
const newLayout = compact(layout, this.compactType(), cols);
const { oldLayout } = this.state;
this.setState({
activeDrag: null,
layout: newLayout,
oldDragItem: null,
oldLayout: null
});
this.onLayoutMaybeChanged(newLayout, oldLayout);
}
onLayoutMaybeChanged(newLayout: Layout, oldLayout: ?Layout) {
if (!oldLayout) oldLayout = this.state.layout;
if (!isEqual(oldLayout, newLayout)) {
this.props.onLayoutChange(newLayout);
}
}
onResizeStart(i: string, w: number, h: number, { e, node }: GridResizeEvent) {
const { layout } = this.state;
var l = getLayoutItem(layout, i);
if (!l) return;
this.setState({
oldResizeItem: cloneLayoutItem(l),
oldLayout: this.state.layout
});
this.props.onResizeStart(layout, l, l, null, e, node);
}
onResize(i: string, w: number, h: number, { e, node }: GridResizeEvent) {
const { layout, oldResizeItem } = this.state;
const { cols, preventCollision } = this.props;
const l: ?LayoutItem = getLayoutItem(layout, i);
if (!l) return;
// Something like quad tree should be used
// to find collisions faster
let hasCollisions;
if (preventCollision) {
const collisions = getAllCollisions(layout, { ...l, w, h }).filter((layoutItem) => layoutItem.i !== l.i);
hasCollisions = collisions.length > 0;
// If we're colliding, we need adjust the placeholder.
if (hasCollisions) {
// adjust w && h to maximum allowed space
let leastX = Infinity, leastY = Infinity;
collisions.forEach((layoutItem) => {
if (layoutItem.x > l.x) leastX = Math.min(leastX, layoutItem.x);
if (layoutItem.y > l.y) leastY = Math.min(leastY, layoutItem.y);
});
if (Number.isFinite(leastX)) l.w = leastX - l.x;
if (Number.isFinite(leastY)) l.h = leastY - l.y;
}
}
if (!hasCollisions) {
// Set new width and height.
l.w = w;
l.h = h;
}
// Create placeholder element (display only)
var placeholder = {
w: l.w,
h: l.h,
x: l.x,
y: l.y,
static: true,
i: i
};
this.props.onResize(layout, oldResizeItem, l, placeholder, e, node);
// Re-compact the layout and set the drag placeholder.
this.setState({
layout: compact(layout, this.compactType(), cols),
activeDrag: placeholder
});
}
onResizeStop(i: string, w: number, h: number, { e, node }: GridResizeEvent) {
const { layout, oldResizeItem } = this.state;
const { cols } = this.props;
var l = getLayoutItem(layout, i);
this.props.onResizeStop(layout, oldResizeItem, l, null, e, node);
// Set state
const newLayout = compact(layout, this.compactType(), cols);
const { oldLayout } = this.state;
this.setState({
activeDrag: null,
layout: newLayout,
oldResizeItem: null,
oldLayout: null
});
this.onLayoutMaybeChanged(newLayout, oldLayout);
}
/**
* Create a placeholder object.
* @return {Element} Placeholder div.
*/
placeholder(): ?ReactElement<any> {
const { activeDrag } = this.state;
if (!activeDrag) return null;
const {
width,
cols,
margin,
containerPadding,
rowHeight,
maxRows,
useCSSTransforms
} = this.props;
// {...this.state.activeDrag} is pretty slow, actually
return (
<GridItem
w={activeDrag.w}
h={activeDrag.h}
x={activeDrag.x}
y={activeDrag.y}
i={activeDrag.i}
className="react-grid-placeholder"
containerWidth={width}
cols={cols}
margin={margin}
containerPadding={containerPadding || margin}
maxRows={maxRows}
rowHeight={rowHeight}
isDraggable={false}
isResizable={false}
useCSSTransforms={useCSSTransforms}
>
<div />
</GridItem>
);
}
/**
* Given a grid item, set its style attributes & surround in a <Draggable>.
* @param {Element} child React element.
* @return {Element} Element wrapped in draggable and properly placed.
*/
processGridItem(child: ReactElement<any>): ?ReactElement<any> {
if (!child || !child.key) return;
const l = getLayoutItem(this.state.layout, String(child.key));
if (!l) return null;
const {
width,
cols,
margin,
containerPadding,
rowHeight,
maxRows,
isDraggable,
isResizable,
useCSSTransforms,
draggableCancel,
draggableHandle
} = this.props;
const { mounted } = this.state;
// Parse 'static'. Any properties defined directly on the grid item will take precedence.
const draggable = Boolean(
!l.static && isDraggable && (l.isDraggable || l.isDraggable == null)
);
const resizable = Boolean(
!l.static && isResizable && (l.isResizable || l.isResizable == null)
);
return (
<GridItem
containerWidth={width}
cols={cols}
margin={margin}
containerPadding={containerPadding || margin}
maxRows={maxRows}
rowHeight={rowHeight}
cancel={draggableCancel}
handle={draggableHandle}
onDragStop={this.onDragStop}
onDragStart={this.onDragStart}
onDrag={this.onDrag}
onResizeStart={this.onResizeStart}
onResize={this.onResize}
onResizeStop={this.onResizeStop}
isDraggable={draggable}
isResizable={resizable}
useCSSTransforms={useCSSTransforms && mounted}
usePercentages={!mounted}
w={l.w}
h={l.h}
x={l.x}
y={l.y}
i={l.i}
minH={l.minH}
minW={l.minW}
maxH={l.maxH}
maxW={l.maxW}
static={l.static}
>
{child}
</GridItem>
);
}
render() {
const { className, style } = this.props;
const mergedClassName = classNames("react-grid-layout", className);
const mergedStyle = {
height: this.containerHeight(),
...style
};
return (
<div className={mergedClassName} style={mergedStyle}>
{React.Children.map(this.props.children, child =>
this.processGridItem(child)
)}
{this.placeholder()}
</div>
);
}
}