89
app/components/FloatGrid/Col.js
Executable file
89
app/components/FloatGrid/Col.js
Executable file
@@ -0,0 +1,89 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import _ from 'lodash';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Col as BootstrapCol } from 'reactstrap';
|
||||
|
||||
// Twice Smaller than Bootstrap breakpoints
|
||||
const breakPoints = [
|
||||
{ id: 'xl', min: 600 },
|
||||
{ id: 'lg', min: 496, max: 600 },
|
||||
{ id: 'md', min: 384, max: 496 },
|
||||
{ id: 'sm', min: 288, max: 384 },
|
||||
{ id: 'xs', max: 288 }
|
||||
];
|
||||
|
||||
const getCurrentbreakPoint = (width, breakPoints) => {
|
||||
let output = 'xl';
|
||||
for (let bp of breakPoints) {
|
||||
if (
|
||||
(_.isUndefined(bp.min) || bp.min <= width) &&
|
||||
(_.isUndefined(bp.max) || bp.max > width)
|
||||
) {
|
||||
output = bp.id;
|
||||
}
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
export class Col extends React.Component {
|
||||
static propTypes = {
|
||||
active: PropTypes.bool,
|
||||
|
||||
lg: PropTypes.number,
|
||||
md: PropTypes.number,
|
||||
sm: PropTypes.number,
|
||||
xs: PropTypes.number,
|
||||
xl: PropTypes.number,
|
||||
|
||||
xlH: PropTypes.number,
|
||||
lgH: PropTypes.number,
|
||||
mdH: PropTypes.number,
|
||||
smH: PropTypes.number,
|
||||
xsH: PropTypes.number,
|
||||
|
||||
xlX: PropTypes.number,
|
||||
lgX: PropTypes.number,
|
||||
mdX: PropTypes.number,
|
||||
smX: PropTypes.number,
|
||||
xsX: PropTypes.number,
|
||||
|
||||
xlY: PropTypes.number,
|
||||
lgY: PropTypes.number,
|
||||
mdY: PropTypes.number,
|
||||
smY: PropTypes.number,
|
||||
xsY: PropTypes.number,
|
||||
|
||||
trueSize: PropTypes.object,
|
||||
children: PropTypes.node,
|
||||
className: PropTypes.string
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
active: true
|
||||
}
|
||||
|
||||
render() {
|
||||
const { active, children, className, trueSize } = this.props;
|
||||
const bsColumnProps = _.pick(this.props, ['xl', 'lg', 'md', 'sm', 'xs']);
|
||||
const otherProps = _.omit(this.props, [..._.keys(Col.propTypes),
|
||||
'minW', 'maxW', 'minH', 'maxH', 'moved', 'static', 'isDraggable', 'isResizable']);
|
||||
const floatColBpId = trueSize ? getCurrentbreakPoint(trueSize.wPx, breakPoints) : 'xl';
|
||||
const floatColClasses = classNames(className, 'float-col',
|
||||
'float-column', `float-column--size-${floatColBpId}`);
|
||||
|
||||
return active ? (
|
||||
<div { ...otherProps } className={ floatColClasses }>
|
||||
{ children }
|
||||
</div>
|
||||
) : (
|
||||
<BootstrapCol
|
||||
{ ...(_.extend(bsColumnProps, otherProps)) }
|
||||
className={ classNames(className, 'pb-3') }
|
||||
>
|
||||
{ children }
|
||||
</BootstrapCol>
|
||||
);
|
||||
}
|
||||
}
|
122
app/components/FloatGrid/Grid.js
Executable file
122
app/components/FloatGrid/Grid.js
Executable file
@@ -0,0 +1,122 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Container } from 'reactstrap';
|
||||
import { FloatGridContext } from './floatGridContext';
|
||||
import './../../styles/components/float-grid.scss';
|
||||
|
||||
export class Grid extends React.Component {
|
||||
static propTypes = {
|
||||
active: PropTypes.bool,
|
||||
children: PropTypes.node,
|
||||
fluid: PropTypes.bool,
|
||||
rowHeight: PropTypes.number,
|
||||
className: PropTypes.string
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
active: true,
|
||||
fluid: false,
|
||||
rowHeight: 100
|
||||
}
|
||||
|
||||
state = {
|
||||
gridSize: { w: 0, h: 0 },
|
||||
gridReady: false,
|
||||
}
|
||||
_gridRef = React.createRef();
|
||||
_resizeDebounceTimeout = 0;
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({
|
||||
gridSize: {
|
||||
w: this._gridRef.current.clientWidth,
|
||||
h: this._gridRef.current.clientHeight
|
||||
}
|
||||
});
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('resize', this._resizeHandler);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(nextProps) {
|
||||
// HACK
|
||||
if (
|
||||
typeof window !== 'undefined' &&
|
||||
nextProps.fluid !== this.props.fluid
|
||||
) {
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.removeEventListener('resize', this._resizeHandler);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { active, children, fluid, className, rowHeight, ...otherProps } = this.props;
|
||||
const { gridSize } = this.state;
|
||||
const modifiedChildren = React.Children.map(children, child => (
|
||||
React.cloneElement(child, {
|
||||
...otherProps,
|
||||
active,
|
||||
gridSize
|
||||
})
|
||||
));
|
||||
|
||||
const floatWrapClasses = classNames({
|
||||
['float-grid-parent__static']: !fluid
|
||||
}, className, 'float-grid-parent');
|
||||
|
||||
return(
|
||||
<FloatGridContext.Provider
|
||||
value={{
|
||||
gridUnitsToPx: (w, h) => {
|
||||
return {
|
||||
wPx: w / 12 * gridSize.w,
|
||||
hPx: h * rowHeight
|
||||
}
|
||||
},
|
||||
active,
|
||||
gridReady: this.state.gridReady,
|
||||
setGridReady: () => { this.setState({ gridReady: true }) }
|
||||
}}
|
||||
>
|
||||
{
|
||||
active ? (
|
||||
<div
|
||||
className={ floatWrapClasses }
|
||||
ref={ this._gridRef }
|
||||
>
|
||||
{ modifiedChildren }
|
||||
</div>
|
||||
) : (
|
||||
<div ref={ this._gridRef }>
|
||||
<Container fluid={ fluid } className={ className } { ...otherProps }>
|
||||
{ modifiedChildren }
|
||||
</Container>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
</FloatGridContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
_resizeHandler = () => {
|
||||
clearInterval(this._resizeDebounceTimeout);
|
||||
|
||||
this._resizeDebounceTimeout = setTimeout(() => {
|
||||
this.setState({
|
||||
gridSize: {
|
||||
w: this._gridRef.current.clientWidth,
|
||||
h: this._gridRef.current.clientHeight
|
||||
}
|
||||
});
|
||||
}, 1000 / 60 * 10); //Every 10 frames debounce
|
||||
}
|
||||
}
|
14
app/components/FloatGrid/Ready.js
Executable file
14
app/components/FloatGrid/Ready.js
Executable file
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FloatGridContext } from './floatGridContext';
|
||||
|
||||
export const Ready = ({ children }) => (
|
||||
<FloatGridContext.Consumer>
|
||||
{
|
||||
({ gridReady }) => gridReady ? children : null
|
||||
}
|
||||
</FloatGridContext.Consumer>
|
||||
);
|
||||
Ready.propTypes = {
|
||||
children: PropTypes.node
|
||||
}
|
207
app/components/FloatGrid/Row.js
Executable file
207
app/components/FloatGrid/Row.js
Executable file
@@ -0,0 +1,207 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import _ from 'lodash';
|
||||
import {
|
||||
WidthProvider,
|
||||
Responsive
|
||||
} from 'react-grid-layout';
|
||||
import { Row as BSRow } from 'reactstrap';
|
||||
|
||||
import { FloatGridContext } from './floatGridContext';
|
||||
|
||||
const ResponsiveGrid = WidthProvider(Responsive);
|
||||
const responsiveBreakpoints = {
|
||||
xl: 1139, lg: 959, md: 719, sm: 539, xs: 0
|
||||
//xl: Number.MAX_VALUE, lg: 1199, md: 991, sm: 767, xs: 576
|
||||
};
|
||||
const breakPointSteps = _.keys(responsiveBreakpoints);
|
||||
const TOTAL_ROW = 12;
|
||||
|
||||
const simplifyChildrenArray = (reactChildren) => _.map(reactChildren, child => (
|
||||
{ ...child, key: child.key.replace(/\.\$/g, '') }
|
||||
));
|
||||
|
||||
export class Row extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
columns: PropTypes.object,
|
||||
onLayoutChange: PropTypes.func,
|
||||
rowHeight: PropTypes.number,
|
||||
gridSize: PropTypes.object
|
||||
};
|
||||
|
||||
static contextType = FloatGridContext;
|
||||
|
||||
_lastLayouts = { };
|
||||
state = {
|
||||
trueColSizes: { },
|
||||
activeLayout: 'xl'
|
||||
}
|
||||
initialDebounceTimeout = false;
|
||||
|
||||
componentDidUpdate(nextProps) {
|
||||
if (!_.isEqual(nextProps.gridSize, this.props.gridSize)) {
|
||||
if (!_.isEmpty(this._lastLayouts)) {
|
||||
this._updateTrueColSizes(this._lastLayouts[this.state.activeLayout]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { children, rowHeight, onLayoutChange, ...otherProps } = this.props;
|
||||
const { trueColSizes } = this.state;
|
||||
|
||||
if (this.context.active) {
|
||||
const layouts = this._lastLayouts = this._calculateLayouts(children);
|
||||
const adjustedChildren = simplifyChildrenArray(
|
||||
React.Children.map(children, (child) =>
|
||||
React.cloneElement(child, { trueSize: trueColSizes[child.props.i] })));
|
||||
|
||||
return (
|
||||
<ResponsiveGrid
|
||||
cols={{ xl: 12, lg: 12, md: 12, sm: 12, xs: 12 }}
|
||||
breakpoints={ responsiveBreakpoints }
|
||||
layouts={ layouts }
|
||||
padding={ [ 0, 0 ] }
|
||||
margin={ [ 0, 0 ] }
|
||||
rowHeight={ rowHeight }
|
||||
onLayoutChange={(currentLayout, allLayouts) => {
|
||||
// Notify the parent
|
||||
onLayoutChange(this._transformForChangeHandler(allLayouts));
|
||||
// Recalculate true sizes
|
||||
this._updateTrueColSizes(currentLayout);
|
||||
|
||||
clearTimeout(this.initialDebounceTimeout);
|
||||
this.initialDebounceTimeout = setTimeout(() => {
|
||||
this.context.setGridReady();
|
||||
}, 0);
|
||||
}}
|
||||
onBreakpointChange={(activeLayout) => {
|
||||
this.setState({ activeLayout });
|
||||
}}
|
||||
onResize={
|
||||
(layout, oldItem, newItem) => {
|
||||
this.setState({
|
||||
trueColSizes: {
|
||||
...trueColSizes,
|
||||
[newItem.i]: this.context.gridUnitsToPx(newItem.w, newItem.h)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
{ ...otherProps }
|
||||
>
|
||||
{ adjustedChildren }
|
||||
</ResponsiveGrid>
|
||||
);
|
||||
} else {
|
||||
const adjustedChildren = React.Children.map(children, (child) =>
|
||||
React.cloneElement(child, { active: false }));
|
||||
|
||||
return (
|
||||
<BSRow>
|
||||
{ adjustedChildren }
|
||||
</BSRow>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_updateTrueColSizes = (layout) => {
|
||||
const { trueColSizes } = this.state;
|
||||
for (let child of layout) {
|
||||
trueColSizes[child.i] = this.context.gridUnitsToPx(child.w, child.h);
|
||||
}
|
||||
this.setState({ trueColSizes });
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the nearest breakpoint relative to the one provided in the
|
||||
* first param. For example - when the `definition` param contains
|
||||
* such bps - { md: 6, xs: 8 }, for `breakpoint` - xl/md will return 6
|
||||
*/
|
||||
_findClosestBreakpoint = (breakpoint, definition) => {
|
||||
let found = 12;
|
||||
for (let bp of _.drop(breakPointSteps, _.indexOf(breakPointSteps, breakpoint))) {
|
||||
if (!_.isUndefined(definition[bp])) {
|
||||
found = definition[bp];
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
_calculateLayouts = (children) => {
|
||||
let output = { };
|
||||
const childrenArray = React.Children.toArray(children);
|
||||
for (let breakPoint of breakPointSteps) {
|
||||
let rowChildren = [];
|
||||
let rowCounter = 0;
|
||||
let y = 0;
|
||||
for (let child of childrenArray) {
|
||||
let bpData = { };
|
||||
// Save the props for current child and breakpoint
|
||||
const config = _.pick(child.props, [
|
||||
'i',
|
||||
'h', 'minH', 'maxH',
|
||||
'minW', 'maxW',
|
||||
breakPoint, `${breakPoint}MinW`, `${breakPoint}MaxW`,
|
||||
'moved', 'static', 'isResizable', 'isDraggable'
|
||||
]);
|
||||
// Calculate the needed definition
|
||||
bpData = Object.assign(bpData, {
|
||||
...config,
|
||||
// Add default heights when none provided
|
||||
...{
|
||||
// Set the x to the calculated value or take from the
|
||||
// props if defined for controlled type
|
||||
x: _.isUndefined(child.props[`${breakPoint}X`]) ?
|
||||
rowCounter : child.props[`${breakPoint}X`],
|
||||
h: child.props[`${breakPoint}H`] || config.h || 1,
|
||||
minH: config.minH || (config.h || 1),
|
||||
maxH: config.maxH || (config.h || 1),
|
||||
},
|
||||
w: config[breakPoint] ||
|
||||
this._findClosestBreakpoint(breakPoint, child.props),
|
||||
// Set the y to the calculated value or take from the
|
||||
// props if defined for controlled type
|
||||
y: _.isUndefined(child.props[`${breakPoint}Y`]) ?
|
||||
y : child.props[`${breakPoint}Y`]
|
||||
});
|
||||
rowChildren = [...rowChildren, bpData];
|
||||
rowCounter += bpData.w;
|
||||
if (rowCounter + bpData.x > TOTAL_ROW) {
|
||||
// Increase by the largest found height
|
||||
y += _.max(_.map(rowChildren, 'h'));
|
||||
rowCounter = 0;
|
||||
rowChildren = [];
|
||||
}
|
||||
output = {
|
||||
...output,
|
||||
[breakPoint]: [...(output[breakPoint] || []), bpData]
|
||||
}
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the calculated layout to a structure
|
||||
* which is provided by `layouts` props
|
||||
*/
|
||||
_transformForChangeHandler = (layouts) => {
|
||||
let output = { };
|
||||
for (let breakPoint of breakPointSteps) {
|
||||
const bpLayout = layouts[breakPoint];
|
||||
for (let element of bpLayout) {
|
||||
output[element.i] = {
|
||||
...(output[element.i]),
|
||||
...(element),
|
||||
[breakPoint]: element.w,
|
||||
[`${breakPoint}X`]: element.x,
|
||||
[`${breakPoint}Y`]: element.y,
|
||||
[`${breakPoint}H`]: element.h
|
||||
}
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
}
|
3
app/components/FloatGrid/floatGridContext.js
Executable file
3
app/components/FloatGrid/floatGridContext.js
Executable file
@@ -0,0 +1,3 @@
|
||||
import React from 'react';
|
||||
|
||||
export const FloatGridContext = React.createContext();
|
16
app/components/FloatGrid/index.js
Executable file
16
app/components/FloatGrid/index.js
Executable file
@@ -0,0 +1,16 @@
|
||||
import { Col } from './Col';
|
||||
import { Row } from './Row';
|
||||
import { Grid } from './Grid';
|
||||
import { Ready } from './Ready';
|
||||
|
||||
Grid.Col = Col;
|
||||
Grid.Row = Row;
|
||||
Grid.Ready = Ready;
|
||||
|
||||
export const applyColumn = (columnId, layouts) => ({
|
||||
...layouts[columnId],
|
||||
i: columnId,
|
||||
key: columnId
|
||||
});
|
||||
|
||||
export default Grid;
|
Reference in New Issue
Block a user