From 59847759bd9a6fba6cb8f7e1b9c3883462f2227c Mon Sep 17 00:00:00 2001 From: Samuel Reed Date: Mon, 23 Nov 2015 14:47:05 -0600 Subject: [PATCH] Refactor with Flow and fix all type errors. --- .eslintrc | 5 +++ .validate.json | 6 ++++ lib/GridItem.jsx | 4 +-- lib/ReactGridLayout.jsx | 12 +++++-- lib/ResponsiveReactGridLayout.jsx | 5 ++- lib/components/ListensToWidth.jsx | 52 +++++++++++++++++++++++++++++++ lib/responsiveUtils.js | 10 +++--- lib/utils.js | 26 +++++++--------- 8 files changed, 92 insertions(+), 28 deletions(-) create mode 100644 .validate.json create mode 100644 lib/components/ListensToWidth.jsx diff --git a/.eslintrc b/.eslintrc index cf071245d..324d63652 100644 --- a/.eslintrc +++ b/.eslintrc @@ -40,5 +40,10 @@ }, "ecmaFeatures": { "jsx": true + }, + "globals": { + // For Flow + "ReactElement", + "ReactClass" } } diff --git a/.validate.json b/.validate.json new file mode 100644 index 000000000..b2f964be9 --- /dev/null +++ b/.validate.json @@ -0,0 +1,6 @@ +{ + "scripts": { + "lint": "jshint ." + }, + "pre-commit": ["lint", "validate", "test"] +} diff --git a/lib/GridItem.jsx b/lib/GridItem.jsx index e04a24f5a..69533aa2f 100644 --- a/lib/GridItem.jsx +++ b/lib/GridItem.jsx @@ -6,7 +6,6 @@ import {Resizable} from 'react-resizable'; import assign from 'object-assign'; import type {Position, Size} from './utils'; -/*global ReactElement*/ /** * An individual item within a ReactGridLayout. @@ -169,8 +168,7 @@ export default class GridItem extends Component { // This is used for server rendering. if (this.props.usePercentages) { - pos.left = perc(pos.left / this.props.containerWidth); - style.left = pos.left; + style.left = perc(pos.left / this.props.containerWidth); style.width = perc(pos.width / this.props.containerWidth); } diff --git a/lib/ReactGridLayout.jsx b/lib/ReactGridLayout.jsx index 7880deede..7db6c683c 100644 --- a/lib/ReactGridLayout.jsx +++ b/lib/ReactGridLayout.jsx @@ -6,7 +6,6 @@ import GridItem from './GridItem'; // import WidthListeningMixin from './mixins/WidthListeningMixin'; // Types -/*global ReactElement*/ import type {Layout, LayoutItem, ResizeEvent, DragEvent} from './utils'; type State = { activeDrag: ?LayoutItem, @@ -205,6 +204,7 @@ export default class ReactGridLayout extends React.Component { onDragStart(i: number, x: number, y: number, {e, element}: DragEvent) { var layout = this.state.layout; var l = getLayoutItem(layout, i); + if (!l) return; this.setState({oldDragItem: clone(l)}); @@ -222,6 +222,7 @@ export default class ReactGridLayout extends React.Component { onDrag(i: number, x: number, y: number, {e, element}: DragEvent) { var layout = this.state.layout; var l = getLayoutItem(layout, i); + if (!l) return; var oldL = this.state.oldDragItem; // Create placeholder (display only) @@ -254,6 +255,7 @@ export default class ReactGridLayout extends React.Component { onDragStop(i: number, x: number, y: number, {e, element}: DragEvent) { var layout = this.state.layout; var l = getLayoutItem(layout, i); + if (!l) return; var oldL = this.state.oldDragItem; // Move the element here @@ -272,6 +274,7 @@ export default class ReactGridLayout extends React.Component { onResizeStart(i: number, w: number, h: number, {e, element}: ResizeEvent) { var layout = this.state.layout; var l = getLayoutItem(layout, i); + if (!l) return; this.setState({oldResizeItem: clone(l)}); @@ -281,6 +284,7 @@ export default class ReactGridLayout extends React.Component { onResize(i: number, w: number, h: number, {e, element}: ResizeEvent) { var layout = this.state.layout; var l = getLayoutItem(layout, i); + if (!l) return; var oldL = this.state.oldResizeItem; // Set new width and height. @@ -348,9 +352,11 @@ export default class ReactGridLayout extends React.Component { * @param {Number} i Index of element. * @return {Element} Element wrapped in draggable and properly placed. */ - processGridItem(child: ReactElement): ReactElement { - var i = child.key; + processGridItem(child: ReactElement): ?ReactElement { + if (!child.key) return; + var i = parseInt(child.key, 10); var l = getLayoutItem(this.state.layout, i); + if (!l) return; // watchStart property tells Draggable to react to changes in the start param // Must be turned off on the item we're dragging as the changes in `activeDrag` cause rerenders diff --git a/lib/ResponsiveReactGridLayout.jsx b/lib/ResponsiveReactGridLayout.jsx index 928ec289b..b39423914 100644 --- a/lib/ResponsiveReactGridLayout.jsx +++ b/lib/ResponsiveReactGridLayout.jsx @@ -6,16 +6,15 @@ import ReactGridLayout from './ReactGridLayout'; // import WidthListeningMixin from './mixins/WidthListeningMixin'; // Types -/*global ReactElement*/ import type {Layout} from './utils'; -type ResponsiveLayout = {lg?: Layout, md?: Layout, sm?: Layout, xs?: Layout, xxs?: Layout}; +import type {ResponsiveLayout} from './responsiveUtils'; type State = { layout: Layout, layouts: ResponsiveLayout, breakpoint: string, cols: number, width: number -} +}; // End Types /** diff --git a/lib/components/ListensToWidth.jsx b/lib/components/ListensToWidth.jsx new file mode 100644 index 000000000..b28658370 --- /dev/null +++ b/lib/components/ListensToWidth.jsx @@ -0,0 +1,52 @@ +// @flow +import { PropTypes, Component } from 'react'; +import ReactDOM from 'react-dom'; + +type Props = {initialWidth: number, listenToWindowResize: boolean}; + +/** + * A simple HOC that provides facility for listening to container resizes. + */ +export default (ComposedComponent: ReactClass): ReactClass => { + return class extends Component { + static propTypes = { + // This allows setting this on the server side + initialWidth: PropTypes.number, + + // If false, you should supply width yourself. Good if you want to debounce resize events + // or reuse a handler from somewhere else. + listenToWindowResize: PropTypes.bool + }; + + static defaultProps: Props = { + initialWidth: 1280, + listenToWindowResize: true + }; + + componentDidMount() { + if (this.props.listenToWindowResize) { + window.addEventListener('resize', this.onWindowResize); + // This is intentional. Once to properly set the breakpoint and resize the elements, + // and again to compensate for any scrollbar that appeared because of the first step. + this.onWindowResize(); + this.onWindowResize(); + } + } + + componentWillUnmount() { + window.removeEventListener('resize', this.onWindowResize); + } + + /** + * On window resize, update width of child by calling listener directly. + * TODO: cleaner way to do this? + */ + onWindowResize = () => { + this.refs.child.onWidthChange(ReactDOM.findDOMNode(this).offsetWidth); + }; + + render(): ReactElement { + return ; + } + }; +}; diff --git a/lib/responsiveUtils.js b/lib/responsiveUtils.js index aa0d385dc..757726926 100644 --- a/lib/responsiveUtils.js +++ b/lib/responsiveUtils.js @@ -1,9 +1,9 @@ // @flow -import utils from './utils'; +import {compact, correctBounds} from './utils'; import type {Layout} from './utils'; -type ResponsiveLayout = {lg?: Layout, md?: Layout, sm?: Layout, xs?: Layout, xxs?: Layout}; +export type ResponsiveLayout = {lg?: Layout, md?: Layout, sm?: Layout, xs?: Layout, xxs?: Layout}; type Breakpoint = string; type Breakpoints = {lg?: number, md?: number, sm?: number, xs?: number, xxs?: number}; @@ -53,8 +53,8 @@ export function getColsFromBreakpoint(breakpoint: Breakpoint, cols: Breakpoints) * @return {Array} New layout. */ export function findOrGenerateResponsiveLayout(layouts: ResponsiveLayout, breakpoints: Breakpoints, - breakpoint: Breakpoint, - lastBreakpoint: Breakpoint, cols: number, verticalCompact: boolean) { + breakpoint: Breakpoint, lastBreakpoint: Breakpoint, + cols: number, verticalCompact: boolean): Layout { // If it already exists, just return it. if (layouts[breakpoint]) return layouts[breakpoint]; // Find or generate the next layout @@ -69,7 +69,7 @@ export function findOrGenerateResponsiveLayout(layouts: ResponsiveLayout, breakp } } layout = JSON.parse(JSON.stringify(layout || [])); // clone layout so we don't modify existing items - return utils.compact(utils.correctBounds(layout, {cols: cols}), verticalCompact); + return compact(correctBounds(layout, {cols: cols}), verticalCompact); } /** diff --git a/lib/utils.js b/lib/utils.js index 2db6fd6af..1cfc88218 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -110,7 +110,7 @@ export function compactItem(compareWith: Layout, l: LayoutItem, verticalCompact: * @param {Array} layout Layout array. * @param {Number} bounds Number of columns. */ -export function correctBounds(layout: Layout, bounds: number): Layout { +export function correctBounds(layout: Layout, bounds: {cols: number}): Layout { let collidesWith = getStatics(layout); for (let i = 0, len = layout.length; i < len; i++) { let l = layout[i]; @@ -141,10 +141,9 @@ export function correctBounds(layout: Layout, bounds: number): Layout { * @param {Number} id ID * @return {LayoutItem} Item at ID. */ -export function getLayoutItem(layout: Layout, id: number): LayoutItem { - id = "" + id; +export function getLayoutItem(layout: Layout, id: number): ?LayoutItem { for (let i = 0, len = layout.length; i < len; i++) { - if ("" + layout[i].i === id) return layout[i]; + if (layout[i].i === id) return layout[i]; } } @@ -193,16 +192,16 @@ export function getStatics(layout: Layout): Array { * @param {Boolean} [isUserAction] If true, designates that the item we're moving is * being dragged/resized by th euser. */ -export function moveElement(layout: Layout, l: LayoutItem, x: number, y: number, isUserAction: boolean): Layout { +export function moveElement(layout: Layout, l: LayoutItem, x: ?number, y: ?number, isUserAction: ?boolean): Layout { if (l.static) return layout; // Short-circuit if nothing to do. if (l.y === y && l.x === x) return layout; - let movingUp = l.y > y; + let movingUp = y && l.y > y; // This is quite a bit faster than extending the object - if (x !== undefined) l.x = x; - if (y !== undefined) l.y = y; + if (x != null) l.x = x; + if (y != null) l.y = y; l.moved = true; // If this collides with anything, move it. @@ -246,7 +245,7 @@ export function moveElement(layout: Layout, l: LayoutItem, x: number, y: number, * by the user. */ export function moveElementAwayFromCollision(layout: Layout, collidesWith: LayoutItem, - itemToMove: LayoutItem, isUserAction: boolean): Layout { + itemToMove: LayoutItem, isUserAction: ?boolean): Layout { // If there is enough space above the collision to put this element, move it there. // We only do this on the main collision as this can get funky in cascades and cause @@ -257,7 +256,8 @@ export function moveElementAwayFromCollision(layout: Layout, collidesWith: Layou x: itemToMove.x, y: itemToMove.y, w: itemToMove.w, - h: itemToMove.h + h: itemToMove.h, + i: -1 }; fakeItem.y = Math.max(collidesWith.y - itemToMove.h, 0); if (!getFirstCollision(layout, fakeItem)) { @@ -330,10 +330,8 @@ export function synchronizeLayoutWithChildren(initialLayout: Layout, children: A for (let i = 0, len = children.length; i < len; i++) { let child = children[i]; // Don't overwrite if it already exists. - let exists = getLayoutItem(initialLayout, child.key); + let exists = getLayoutItem(initialLayout, parseInt(child.key || "1" /* FIXME satisfies Flow */, 10)); if (exists) { - // Ensure 'i' is always a string - exists.i = '' + exists.i; layout.push(exists); continue; } @@ -350,7 +348,7 @@ export function synchronizeLayoutWithChildren(initialLayout: Layout, children: A } } else { // Nothing provided: ensure this is added to the bottom - layout.push({w: 1, h: 1, x: 0, y: bottom(layout), i: child.key}); + layout.push({w: 1, h: 1, x: 0, y: bottom(layout), i: parseInt(child.key || "1", 10)}); } }