wix-style-react
Version:
331 lines (296 loc) • 9.65 kB
JavaScript
import React from 'react';
import PropTypes from 'prop-types';
import AddSmallIcon from 'wix-ui-icons-common/AddSmall';
import cloneDeep from 'lodash/cloneDeep';
import uniqueId from 'lodash/uniqueId';
import { stVars } from '../Foundation/stylable/easing.st.css';
import { FontUpgradeContext } from '../FontUpgrade/context';
import { DataHook, DataAttribute } from './constants';
import { st, classes } from './TimeTable.st.css';
import SortableList from '../SortableList';
import DragDropContextProvider from '../DragDropContextProvider';
import AddItem from '../AddItem';
import Item from './components/Item';
const getSortableListItems = columns =>
columns.map(({ items, disabled: columnDisabled = false }) =>
items.map(({ content, disabled, draggable }) => {
const isDisabled = disabled !== undefined ? disabled : columnDisabled;
const isDraggable = draggable !== undefined ? draggable : !isDisabled;
return {
id: uniqueId(),
content,
draggable: !!isDraggable,
disabled: !!isDisabled,
};
}),
);
class TimeTable extends React.PureComponent {
static displayName = 'TimeTable';
static propTypes = {
/** Hook for testing purposes. */
dataHook: PropTypes.string,
/**
* Event triggered on column data change:
* `onChange(columns, { addedToColumnIndex, removedFromColumnIndex, addedItemIndex, removedItemIndex })`
*/
onChange: PropTypes.func,
/**
* Column data configuration. Item content is provided as a simple node or a
* render function with `content` property. When render function is used the
* signature is:
* `({ Item, draggable, disabled }) => {}`:
* - `Item` - component used to provide default item visual representation.
* You should render `<Item draggable={draggable} disabled={disabled}>...</Item>`
* - `draggable` - item is draggable.
* - `disabled` - item is disabled.
*/
columns: PropTypes.arrayOf(
PropTypes.shape({
title: PropTypes.string.isRequired,
subtitle: PropTypes.string.isRequired,
items: PropTypes.arrayOf(
PropTypes.shape({
content: PropTypes.oneOfType([PropTypes.node, PropTypes.func])
.isRequired,
draggable: PropTypes.bool,
disabled: PropTypes.bool,
}),
).isRequired,
disabled: PropTypes.bool,
droppable: PropTypes.bool,
active: PropTypes.bool,
}),
),
/**
* Event triggered on add button click: `onAdd(columnIndex)`.
* When not provided the button will be hidden.
*/
onAdd: PropTypes.func,
/** Title of add button. */
addItemButtonLabel: PropTypes.node,
/**
* Position where dragged items will be inserted. Using `any` value will
* allow the items to be re-ordered within the same column.
*/
insertPosition: PropTypes.oneOf(['start', 'end', 'any']),
/** Custom table height. */
height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
};
static defaultProps = {
columns: [],
insertPosition: 'any',
height: '283px',
};
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.columns === prevState.columns) {
return null;
}
return {
...prevState,
columns: nextProps.columns,
sortableListItems: getSortableListItems(nextProps.columns),
};
}
state = {
addButtonColumnIndex: null,
dragging: false,
columns: [],
sortableListItems: [],
groupName: uniqueId('time-table'),
};
_renderHeader = () => (
<div className={classes.headerContainer}>
{this.props.columns.map(
(
{
active = false,
disabled = false,
title,
subtitle,
droppable = !disabled,
},
index,
) => (
<div
key={index}
className={st(classes.header, { active, disabled, droppable })}
>
<div
data-hook={DataHook.Title}
className={st(classes.title, { active })}
>
{title}
</div>
<div
data-hook={DataHook.Subtitle}
className={st(classes.subtitle, { active })}
>
{subtitle}
</div>
</div>
),
)}
</div>
);
_renderContent = () => {
const { columns, insertPosition, onAdd } = this.props;
const { sortableListItems, addButtonColumnIndex, groupName } = this.state;
return (
<div className={classes.contentContainer}>
<DragDropContextProvider>
{columns.map(
({ active, disabled, droppable = !disabled }, columnIndex) => {
const addItemEnabled = !columns[columnIndex].disabled && !!onAdd;
const addItemVisible =
addItemEnabled && addButtonColumnIndex === columnIndex;
return (
<div
key={columnIndex}
data-hook={DataHook.Column}
onMouseEnter={() => this._handleMouseEnter(columnIndex)}
onMouseLeave={this._handleMouseLeave}
className={st(classes.column, {
addItemEnabled,
addItemVisible,
droppable,
})}
{...{
[DataAttribute.ColumnActive]: !!active,
[DataAttribute.ColumnDisabled]: !!disabled,
[DataAttribute.ColumnDroppable]: droppable,
}}
>
<SortableList
usePortal
className={classes.content}
groupName={groupName}
onDragStart={this._handleDragStart}
onDragEnd={this._handleDragEnd}
containerId={String(columnIndex)}
items={sortableListItems[columnIndex]}
droppable={droppable}
onDrop={this._handleDrop}
canDrag={this._handleCanDrag}
renderItem={this._renderItem}
insertPosition={insertPosition}
animationDuration={500}
animationTiming={stVars['ease-7']}
/>
{addItemVisible && this._renderAddItemButton(columnIndex)}
</div>
);
},
)}
</DragDropContextProvider>
</div>
);
};
render() {
const { dataHook, height } = this.props;
const { dragging } = this.state;
return (
<FontUpgradeContext.Consumer>
{({ active: isMadefor }) => (
<div
className={st(classes.root, { isMadefor, dragging })}
style={{ height }}
data-hook={dataHook}
>
{this._renderHeader()}
{this._renderContent()}
</div>
)}
</FontUpgradeContext.Consumer>
);
}
_renderItem = ({
isPlaceholder,
previewStyles,
item: { content, disabled, draggable },
}) => (
<div
data-hook={DataHook.Item}
className={st(classes.item, { isPlaceholder })}
style={previewStyles}
{...{
[DataAttribute.ItemDraggable]: draggable,
[DataAttribute.ItemDisabled]: disabled,
}}
>
{typeof content === 'function' ? (
content({ Item, disabled, draggable })
) : (
<Item disabled={disabled} draggable={draggable}>
{content}
</Item>
)}
</div>
);
_renderAddItemButton = columnIndex => (
<div>
<AddItem
className={classes.addItemButton}
dataHook={DataHook.AddItemButton}
showIcon={false}
onClick={() => this.props.onAdd(columnIndex)}
>
<span className={classes.addItemLabel}>
<AddSmallIcon className={classes.addItemIcon} size="18px" />
{this.props.addItemButtonLabel}
</span>
</AddItem>
</div>
);
_handleDrop = ({
addedIndex: addedItemIndex,
removedIndex: removedItemIndex,
addedToContainerId,
removedFromContainerId,
}) => {
const { columns } = this.props;
const addedToColumnIndex = Number(addedToContainerId);
const removedFromColumnIndex = Number(removedFromContainerId);
this.setState({ addButtonColumnIndex: addedToColumnIndex });
if (
addedToColumnIndex === removedFromColumnIndex &&
addedItemIndex === removedItemIndex
) {
return;
}
const newColumns = cloneDeep(columns);
const removedItem = columns[removedFromColumnIndex].items[removedItemIndex];
newColumns[removedFromColumnIndex].items.splice(removedItemIndex, 1);
newColumns[addedToColumnIndex].items.splice(addedItemIndex, 0, removedItem);
if (this.props.onChange) {
this.props.onChange(newColumns, {
addedToColumnIndex,
removedFromColumnIndex,
addedItemIndex,
removedItemIndex,
});
}
};
_handleMouseEnter = columnIndex => {
if (!this.state.dragging) {
this.setState({ addButtonColumnIndex: columnIndex });
}
};
_handleMouseLeave = () => {
if (!this.state.dragging) {
this.setState({ addButtonColumnIndex: null });
}
};
_handleDragStart = () => {
this.setState({
dragging: true,
addButtonColumnIndex: null,
});
};
_handleDragEnd = () => {
this.setState({
dragging: false,
});
};
_handleCanDrag = ({ item }) => !!item.draggable;
}
export default TimeTable;