Skip to content

Commit

Permalink
feat(react-grid): column reordering animation (#169)
Browse files Browse the repository at this point in the history
  • Loading branch information
kvet authored Jun 29, 2017
1 parent 43d9da1 commit d5e808b
Show file tree
Hide file tree
Showing 10 changed files with 170 additions and 15 deletions.
23 changes: 23 additions & 0 deletions packages/dx-core/src/easings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export const easeInQuad = t => t * t;
export const easeOutQuad = t => t * (2 - t);
export const easeInOutQuad = t => (t < 0.5
? 2 * t * t
: -1 + ((4 - (2 * t)) * t));

export const easeInCubic = t => t * t * t;
export const easeOutCubic = t => ((t - 1) * (t - 1) * (t - 1)) + 1;
export const easeInOutCubic = t => (t < 0.5
? 4 * t * t * t
: ((t - 1) * ((2 * t) - 2) * ((2 * t) - 2)) + 1);

export const easeInQuart = t => t * t * t * t;
export const easeOutQuart = t => 1 - ((t - 1) * (t - 1) * (t - 1) * (t - 1));
export const easeInOutQuart = t => (t < 0.5
? 8 * t * t * t * t
: 1 - (8 * (t - 1) * (t - 1) * (t - 1) * (t - 1)));

export const easeInQuint = t => t * t * t * t * t;
export const easeOutQuint = t => 1 + ((t - 1) * (t - 1) * (t - 1) * (t - 1) * (t - 1));
export const easeInOutQuint = t => (t < 0.5
? 16 * t * t * t * t * t
: 1 + (16 * (t - 1) * (t - 1) * (t - 1) * (t - 1) * (t - 1)));
1 change: 1 addition & 0 deletions packages/dx-core/src/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { PluginHost } from './plugin-host';
export { EventEmitter } from './event-emitter';
export * from './easings';
4 changes: 4 additions & 0 deletions packages/dx-grid-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"output": "../../shippable/testresults/dx-grid-core.xml"
},
"devDependencies": {
"@devexpress/dx-core": "1.0.0-alpha.4",
"babel-cli": "^6.24.1",
"babel-core": "^6.25.0",
"babel-jest": "^20.0.3",
Expand All @@ -68,5 +69,8 @@
"rollup-plugin-babel": "^2.7.1",
"rollup-plugin-license": "^0.4.0",
"rollup-watch": "^4.0.0"
},
"peerDependencies": {
"@devexpress/dx-core": "1.0.0-alpha.4"
}
}
3 changes: 3 additions & 0 deletions packages/dx-grid-core/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,7 @@ export {
findTableCellTarget,
getTableColumnGeometries,
getTableTargetColumnIndex,
getAnimations,
filterActiveAnimations,
evalAnimations,
} from './utils/table';
60 changes: 60 additions & 0 deletions packages/dx-grid-core/src/utils/table.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { easeOutCubic } from '@devexpress/dx-core';

import { querySelectorAll } from './dom-utils';

const getTableKeyGetter = (getIntrinsicKey, object, index) => {
Expand Down Expand Up @@ -84,3 +86,61 @@ export const getTableTargetColumnIndex = (columnGeometries, sourceIndex, offset)
})
.findIndex(({ left, right }) => offset > left && offset < right);
};

const ANIMATION_DURATION = 200;

const getAnimationProgress = animation =>
(new Date().getTime() - animation.startTime) / ANIMATION_DURATION;

export const getAnimations = (
prevColumns, nextColumns, tableWidth, draggingColumnKey, prevAnimations,
) => {
const prevColumnGeometries = new Map(getTableColumnGeometries(prevColumns, tableWidth)
.map((geometry, index) => [tableColumnKeyGetter(prevColumns[index], index), geometry])
.map(([key, geometry]) => {
const animation = prevAnimations.get(key);
if (!animation) return [key, geometry];
const progress = easeOutCubic(getAnimationProgress(animation));
const left = ((animation.left.to - animation.left.from) * progress) + animation.left.from;
return [key, {
left,
right: geometry.right - (geometry.left - left),
}];
}));

const nextColumnGeometries = new Map(getTableColumnGeometries(nextColumns, tableWidth)
.map((geometry, index) => [tableColumnKeyGetter(nextColumns[index], index), geometry]));

return new Map([...nextColumnGeometries.keys()]
.map((key) => {
const prev = prevColumnGeometries.get(key);
const next = nextColumnGeometries.get(key);

const result = { startTime: new Date().getTime(), style: {} };
if (Math.abs(prev.left - next.left) > 1) {
result.left = { from: prev.left, to: next.left };
}
if (draggingColumnKey === key) {
result.style = {
zIndex: 100,
position: 'relative',
};
}
return [key, result];
})
.filter(animation => animation[1].left));
};

export const filterActiveAnimations = animations => new Map([...animations.entries()]
.filter(([, animation]) => getAnimationProgress(animation) < 1));

export const evalAnimations = animations => new Map([...animations.entries()]
.map(([key, animation]) => {
const progress = easeOutCubic(getAnimationProgress(animation));
const result = { ...animation.style };
if (animation.left) {
const offset = (animation.left.to - animation.left.from) * (progress - 1);
result.transform = `translateX(${offset}px)`;
}
return [key, result];
}));
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,13 @@ const styleSheet = createStyleSheet('HighlightedCell', theme => ({
},
}));

const HighlightedCellBase = ({ align, value, classes }) => (
const HighlightedCellBase = ({ align, value, classes, style }) => (
<TableCell
className={classes.highlightedCell}
style={{
color: getColor(value),
textAlign: align,
...style,
}}
>
${value}
Expand All @@ -40,6 +41,10 @@ HighlightedCellBase.propTypes = {
value: PropTypes.number.isRequired,
classes: PropTypes.object.isRequired,
align: PropTypes.string.isRequired,
style: PropTypes.object,
};
HighlightedCellBase.defaultProps = {
style: {},
};

export const HighlightedCell = withStyles(styleSheet)(HighlightedCellBase);
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ const styleSheet = createStyleSheet('ProgressBarCell', theme => ({
},
}));

export const ProgressBarCellBase = ({ value, classes }) => (
<TableCell className={classes.progressBarCell}>
export const ProgressBarCellBase = ({ value, classes, style }) => (
<TableCell
className={classes.progressBarCell}
style={style}
>
<div
className={classes.progressBar}
style={{ width: `${value}%` }}
Expand All @@ -28,6 +31,10 @@ export const ProgressBarCellBase = ({ value, classes }) => (
ProgressBarCellBase.propTypes = {
value: PropTypes.number.isRequired,
classes: PropTypes.object.isRequired,
style: PropTypes.object,
};
ProgressBarCellBase.defaultProps = {
style: {},
};

export const ProgressBarCell = withStyles(styleSheet)(ProgressBarCellBase);
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,14 @@ export class UncontrolledModeDemo extends React.PureComponent {
<DragDropContext />

<TableView
tableCellTemplate={({ row, column }) => {
tableCellTemplate={({ row, column, style }) => {
if (column.name === 'discount') {
return (
<ProgressBarCell value={row.discount * 100} />
<ProgressBarCell value={row.discount * 100} style={style} />
);
} else if (column.name === 'amount') {
return (
<HighlightedCell align={column.align} value={row.amount} />
<HighlightedCell align={column.align} value={row.amount} style={style} />
);
}
return undefined;
Expand Down
64 changes: 56 additions & 8 deletions packages/dx-react-grid/src/components/table-layout.jsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* globals requestAnimationFrame */

import React from 'react';
import PropTypes from 'prop-types';
import { findDOMNode } from 'react-dom';
Expand All @@ -13,31 +15,36 @@ import {
findTableCellTarget,
getTableColumnGeometries,
getTableTargetColumnIndex,
getAnimations,
filterActiveAnimations,
evalAnimations,
} from '@devexpress/dx-grid-core';

const FLEX_TYPE = 'flex';

const getColumnStyle = ({ column }) => ({
const getColumnStyle = ({ column, animationState = {} }) => ({
width: column.width !== undefined ? `${column.width}px` : undefined,
...animationState,
});

const getRowStyle = ({ row }) => ({
height: row.height !== undefined ? `${row.height}px` : undefined,
});

const renderRowCells = ({ row, columns, cellTemplate }) =>
const renderRowCells = ({ row, columns, cellTemplate, animationState }) =>
columns
.filter((column, columnIndex) => !getTableCellInfo({ row, columns, columnIndex }).skip)
.map((column, columnIndex) => {
const key = tableColumnKeyGetter(column, columnIndex);
const colspan = getTableCellInfo({ row, columns, columnIndex }).colspan;
return React.cloneElement(
cellTemplate({
row,
column,
style: getColumnStyle({ column }),
...colspan ? { colspan } : null,
style: getColumnStyle({ column, animationState: animationState.get(key) }),
}),
{ key: tableColumnKeyGetter(column, columnIndex) },
{ key },
);
});

Expand All @@ -47,13 +54,14 @@ const renderRows = ({
columns,
rowTemplate,
cellTemplate,
animationState,
}) =>
rows
.map((row, rowIndex) => React.cloneElement(
rowTemplate({
row,
style: getRowStyle({ row }),
children: renderRowCells({ row, columns, cellTemplate }),
children: renderRowCells({ row, columns, cellTemplate, animationState }),
}),
{ key: tableRowKeyGetter(getRowId, row, rowIndex) },
));
Expand All @@ -66,6 +74,7 @@ const renderRowsBlock = ({
rowTemplate,
cellTemplate,
onClick,
animationState,
}) => blockTemplate({
onClick: (e) => {
const { rowIndex, columnIndex } = findTableCellTarget(e);
Expand All @@ -78,6 +87,7 @@ const renderRowsBlock = ({
columns,
rowTemplate,
cellTemplate,
animationState,
}),
});

Expand All @@ -88,9 +98,15 @@ export class TableLayout extends React.PureComponent {
this.state = {
sourceColumnIndex: -1,
targetColumnIndex: -1,

animationState: new Map(),
};

this.animations = new Map();

this.tableNode = null;
// eslint-disable-next-line react/no-find-dom-node
this.tableRect = () => findDOMNode(this.tableNode).getBoundingClientRect();

this.getColumns = () => {
const { columns } = this.props;
Expand All @@ -115,16 +131,17 @@ export class TableLayout extends React.PureComponent {
};
this.onOver = ({ payload, clientOffset }) => {
const sourceColumnName = payload[0].columnName;
// eslint-disable-next-line react/no-find-dom-node
const tableRect = findDOMNode(this.tableNode).getBoundingClientRect();
const tableRect = this.tableRect();
const columns = this.getColumns();
const columnGeometries = getTableColumnGeometries(columns, tableRect.width);
const targetColumnIndex = getTableTargetColumnIndex(
columnGeometries,
columns.findIndex(c => c.name === sourceColumnName),
clientOffset.x - tableRect.left);

if (targetColumnIndex === -1 || columns[targetColumnIndex].type) return;
if (targetColumnIndex === -1 ||
columns[targetColumnIndex].type ||
targetColumnIndex === this.state.targetColumnIndex) return;

const { sourceColumnIndex } = this.state;
this.setState({
Expand All @@ -133,12 +150,25 @@ export class TableLayout extends React.PureComponent {
: sourceColumnIndex,
targetColumnIndex,
});

this.animations = getAnimations(columns, this.getColumns(), tableRect.width,
tableColumnKeyGetter(this.props.columns[this.state.sourceColumnIndex]), this.animations);
this.processAnimationFrame();
};
this.onLeave = () => {
const columns = this.getColumns();
const tableRect = this.tableRect();

const sourceColumnIndex = this.state.sourceColumnIndex;

this.setState({
sourceColumnIndex: -1,
targetColumnIndex: -1,
});

this.animations = getAnimations(columns, this.getColumns(), tableRect.width,
tableColumnKeyGetter(this.props.columns[sourceColumnIndex]), this.animations);
this.processAnimationFrame();
};
this.onDrop = () => {
const { sourceColumnIndex, targetColumnIndex } = this.state;
Expand All @@ -162,6 +192,21 @@ export class TableLayout extends React.PureComponent {
});
}
}
processAnimationFrame() {
this.animations = filterActiveAnimations(this.animations);

if (!this.animations.size) {
if (this.state.animationState.size) {
this.setState({ animationState: new Map() });
}
return;
}

const animationState = evalAnimations(this.animations);
this.setState({ animationState });

requestAnimationFrame(this.processAnimationFrame.bind(this));
}
render() {
const {
headerRows,
Expand All @@ -178,6 +223,7 @@ export class TableLayout extends React.PureComponent {
className,
style,
} = this.props;
const { animationState } = this.state;
const columns = this.getColumns();
const minWidth = columns
.map(column => column.width || (column.type === FLEX_TYPE ? 0 : minColumnWidth))
Expand All @@ -199,6 +245,7 @@ export class TableLayout extends React.PureComponent {
rowTemplate,
cellTemplate,
onClick,
animationState,
}),
{ key: 'head' },
),
Expand All @@ -211,6 +258,7 @@ export class TableLayout extends React.PureComponent {
rowTemplate,
cellTemplate,
onClick,
animationState,
}),
{ key: 'body' },
),
Expand Down
Loading

0 comments on commit d5e808b

Please sign in to comment.