201 lines
6.9 KiB
Plaintext
201 lines
6.9 KiB
Plaintext
// @flow
|
|
import React from 'react';
|
|
import PropTypes from 'prop-types';
|
|
import isEqual from 'lodash.isequal';
|
|
|
|
import {cloneLayout, synchronizeLayoutWithChildren, validateLayout} from './utils';
|
|
import {getBreakpointFromWidth, getColsFromBreakpoint, findOrGenerateResponsiveLayout} from './responsiveUtils';
|
|
import ReactGridLayout from './ReactGridLayout';
|
|
import type {Props as RGLProps} from './ReactGridLayout';
|
|
import type {Layout} from './utils';
|
|
|
|
const noop = function(){};
|
|
const type = (obj) => Object.prototype.toString.call(obj);
|
|
|
|
type State = {
|
|
layout: Layout,
|
|
breakpoint: string,
|
|
cols: number
|
|
};
|
|
|
|
type Props<Breakpoint: string = string> = {
|
|
...$Exact<RGLProps>,
|
|
|
|
// Responsive config
|
|
breakpoint: Breakpoint,
|
|
breakpoints: {[key: Breakpoint]: number},
|
|
cols: {[key: Breakpoint]: number},
|
|
layouts: {[key: Breakpoint]: Layout},
|
|
width: number,
|
|
|
|
// Callbacks
|
|
onBreakpointChange: (Breakpoint, cols: number) => void,
|
|
onLayoutChange: (Layout, {[key: Breakpoint]: Layout}) => void,
|
|
onWidthChange:
|
|
(containerWidth: number, margin: [number, number], cols: number, containerPadding: [number, number]) => void
|
|
};
|
|
|
|
export default class ResponsiveReactGridLayout extends React.Component<Props<>, State> {
|
|
|
|
// This should only include propTypes needed in this code; RGL itself
|
|
// will do validation of the rest props passed to it.
|
|
static propTypes = {
|
|
|
|
//
|
|
// Basic props
|
|
//
|
|
|
|
// Optional, but if you are managing width yourself you may want to set the breakpoint
|
|
// yourself as well.
|
|
breakpoint: PropTypes.string,
|
|
|
|
// {name: pxVal}, e.g. {lg: 1200, md: 996, sm: 768, xs: 480}
|
|
breakpoints: PropTypes.object,
|
|
|
|
// # of cols. This is a breakpoint -> cols map
|
|
cols: PropTypes.object,
|
|
|
|
// layouts is an object mapping breakpoints to layouts.
|
|
// e.g. {lg: Layout, md: Layout, ...}
|
|
layouts(props, propName) {
|
|
if (type(props[propName]) !== '[object Object]') {
|
|
throw new Error('Layout property must be an object. Received: ' + type(props[propName]));
|
|
}
|
|
Object.keys(props[propName]).forEach((key) => {
|
|
if (!(key in props.breakpoints)) {
|
|
throw new Error('Each key in layouts must align with a key in breakpoints.');
|
|
}
|
|
validateLayout(props.layouts[key], 'layouts.' + key);
|
|
});
|
|
},
|
|
|
|
// The width of this component.
|
|
// Required in this propTypes stanza because generateInitialState() will fail without it.
|
|
width: PropTypes.number.isRequired,
|
|
|
|
//
|
|
// Callbacks
|
|
//
|
|
|
|
// Calls back with breakpoint and new # cols
|
|
onBreakpointChange: PropTypes.func,
|
|
|
|
// Callback so you can save the layout.
|
|
// Calls back with (currentLayout, allLayouts). allLayouts are keyed by breakpoint.
|
|
onLayoutChange: PropTypes.func,
|
|
|
|
// Calls back with (containerWidth, margin, cols, containerPadding)
|
|
onWidthChange: PropTypes.func
|
|
};
|
|
|
|
static defaultProps = {
|
|
breakpoints: {lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0},
|
|
cols: {lg: 12, md: 10, sm: 6, xs: 4, xxs: 2},
|
|
layouts: {},
|
|
onBreakpointChange: noop,
|
|
onLayoutChange: noop,
|
|
onWidthChange: noop,
|
|
};
|
|
|
|
state = this.generateInitialState();
|
|
|
|
generateInitialState(): State {
|
|
const {width, breakpoints, layouts, cols} = this.props;
|
|
const breakpoint = getBreakpointFromWidth(breakpoints, width);
|
|
const colNo = getColsFromBreakpoint(breakpoint, cols);
|
|
// verticalCompact compatibility, now deprecated
|
|
const compactType = this.props.verticalCompact === false ? null : this.props.compactType;
|
|
// Get the initial layout. This can tricky; we try to generate one however possible if one doesn't exist
|
|
// for this layout.
|
|
const initialLayout = findOrGenerateResponsiveLayout(layouts, breakpoints, breakpoint,
|
|
breakpoint, colNo, compactType);
|
|
|
|
return {
|
|
layout: initialLayout,
|
|
breakpoint: breakpoint,
|
|
cols: colNo
|
|
};
|
|
}
|
|
|
|
componentWillReceiveProps(nextProps: Props<*>) {
|
|
|
|
// Allow parent to set width or breakpoint directly.
|
|
if (
|
|
nextProps.width != this.props.width
|
|
|| nextProps.breakpoint !== this.props.breakpoint
|
|
|| !isEqual(nextProps.breakpoints, this.props.breakpoints)
|
|
|| !isEqual(nextProps.cols, this.props.cols)
|
|
) {
|
|
this.onWidthChange(nextProps);
|
|
}
|
|
|
|
// Allow parent to set layouts directly.
|
|
else if (!isEqual(nextProps.layouts, this.props.layouts)) {
|
|
const {breakpoint, cols} = this.state;
|
|
|
|
// Since we're setting an entirely new layout object, we must generate a new responsive layout
|
|
// if one does not exist.
|
|
const newLayout = findOrGenerateResponsiveLayout(
|
|
nextProps.layouts, nextProps.breakpoints,
|
|
breakpoint, breakpoint, cols, nextProps.compactType
|
|
);
|
|
this.setState({layout: newLayout});
|
|
}
|
|
}
|
|
|
|
// wrap layouts so we do not need to pass layouts to child
|
|
onLayoutChange = (layout: Layout) => {
|
|
this.props.onLayoutChange(layout, {...this.props.layouts, [this.state.breakpoint]: layout});
|
|
};
|
|
|
|
/**
|
|
* When the width changes work through breakpoints and reset state with the new width & breakpoint.
|
|
* Width changes are necessary to figure out the widget widths.
|
|
*/
|
|
onWidthChange(nextProps: Props<*>) {
|
|
const {breakpoints, cols, layouts, compactType} = nextProps;
|
|
const newBreakpoint = nextProps.breakpoint || getBreakpointFromWidth(nextProps.breakpoints, nextProps.width);
|
|
|
|
const lastBreakpoint = this.state.breakpoint;
|
|
|
|
// Breakpoint change
|
|
if (lastBreakpoint !== newBreakpoint || this.props.breakpoints !== breakpoints || this.props.cols !== cols) {
|
|
// Preserve the current layout if the current breakpoint is not present in the next layouts.
|
|
if (!(lastBreakpoint in layouts)) layouts[lastBreakpoint] = cloneLayout(this.state.layout);
|
|
|
|
// Find or generate a new layout.
|
|
const newCols: number = getColsFromBreakpoint(newBreakpoint, cols);
|
|
let layout = findOrGenerateResponsiveLayout(layouts, breakpoints, newBreakpoint,
|
|
lastBreakpoint, newCols, compactType);
|
|
|
|
// This adds missing items.
|
|
layout = synchronizeLayoutWithChildren(layout, nextProps.children, newCols, compactType);
|
|
|
|
// Store the new layout.
|
|
layouts[newBreakpoint] = layout;
|
|
|
|
// callbacks
|
|
this.props.onLayoutChange(layout, layouts);
|
|
this.props.onBreakpointChange(newBreakpoint, newCols);
|
|
this.props.onWidthChange(nextProps.width, nextProps.margin, newCols, nextProps.containerPadding);
|
|
|
|
this.setState({breakpoint: newBreakpoint, layout: layout, cols: newCols});
|
|
}
|
|
}
|
|
|
|
render() {
|
|
// eslint-disable-next-line no-unused-vars
|
|
const {breakpoint, breakpoints, cols, layouts, onBreakpointChange, onLayoutChange, onWidthChange,
|
|
...other} = this.props;
|
|
|
|
return (
|
|
<ReactGridLayout
|
|
{...other}
|
|
onLayoutChange={this.onLayoutChange}
|
|
layout={this.state.layout}
|
|
cols={this.state.cols}
|
|
/>
|
|
);
|
|
}
|
|
}
|