wix-style-react
Version:
wix-style-react
308 lines • 14.5 kB
JavaScript
import React from 'react';
import PropTypes from 'prop-types';
import { classes as theme } from './NestableListTheme.st.css';
import { classes, st } from './NestableList.st.css';
import NestableListBase from '../NestableListBase';
import TableListItem from '../TableListItem';
import Box from '../Box';
import { Arrow } from './Arrow';
import TextButton from '../TextButton';
import { Add } from '@wix/wix-ui-icons-common';
import { isFistItem, isLastItem, isRootItem, moveItemOutsideOfTheParent, moveItemToTheChildOfPrevSibling, moveItemVertically, setCollapse, VerticalMovementDirection, } from '../NestableListBase/utils';
import { DEPTH_THRESHOLD } from './constants';
/** A styled list with drag and drop and nesting capabilities. */
class NestableList extends React.PureComponent {
constructor() {
super(...arguments);
this.state = {
movedItem: null,
backup: null,
isDragging: false,
isInternalStateUpdate: false,
items: this.props.items,
};
// according to make possible use setState along with getDerivedStateFromProps
// https://stackoverflow.com/questions/51019936/why-getderivedstatefromprops-is-called-after-setstate
this._setStateDecorator = state => {
this.setState({
...state,
isInternalStateUpdate: true,
});
};
this._renderArrow = ({ isLastChild = false, isPreview = false, isPlaceholder, } = {}) => {
return (React.createElement(Box, { className: classes.arrowContainer, backgroundColor: isPlaceholder ? 'D60' : undefined, direction: "vertical" },
React.createElement("div", { className: st(classes.arrow, {
lastChild: isLastChild,
preview: isPreview,
placeholder: isPlaceholder,
}) },
React.createElement(Arrow, { className: classes.horizontalArrow }))));
};
this._renderActions = data => {
const { isRootAction, veryLastItem, item } = data;
const actions = isRootAction ? this.props.actions : item.actions;
if (!actions) {
return null;
}
const dataHook = isRootAction
? 'nestable-list-root-actions'
: item.actionsDataHook;
return (React.createElement("div", { "data-hook": dataHook, className: st(classes.item, {
rootAction: isRootAction,
dragging: this.state.isDragging,
withoutBottomBorder: !this.props.withBottomBorder &&
((veryLastItem && !actions) || isRootAction),
}, classes.actionItem) },
React.createElement(TableListItem, { onClick: e => {
if (actions.length === 1) {
actions[0].onClick(e, item);
}
}, options: actions.map((action, index, arr) => {
return {
width: `fit-content(${100 / arr.length}%)`,
value: (React.createElement(TextButton, { dataHook: "nestable-list-action", ellipsis: true, size: "small", onClick: e => {
// if one element event handler is fired on table list item
if (arr.length !== 1) {
action.onClick(e, item);
}
}, prefixIcon: action.prefixIcon }, action.label)),
};
}) })));
};
this._renderAction = data => {
const label = data.isRootAction
? this.props.addItemLabel
: data.item.addItemLabel;
if (!label || this.props.maxDepth <= data.depth) {
return;
}
return (React.createElement(Box, { direction: "vertical", className: st(classes.item, {
rootAction: data.isRootAction,
dragging: this.state.isDragging,
withoutBottomBorder: !this.props.withBottomBorder &&
((data.veryLastItem && !data.addItemLabel) || data.isRootAction),
}, classes.actionItem) },
React.createElement(TableListItem, { onClick: () => this.props.onAddItem(data.item), options: [
{
value: (React.createElement(Box, null,
React.createElement(TextButton, { size: "small", prefixIcon: React.createElement(Add, null) }, label))),
},
] })));
};
this._renderPrefix = data => {
if (data.isPreview ||
isLastItem(data.siblings, data.item) ||
isRootItem(data.depth)) {
return null;
}
return React.createElement("div", { className: classes.childArrow });
};
this._renderItem = options => {
const { isPlaceholder, depth, isPreview, isVeryLastItem, siblings, item } = options;
const { id, children, isCollapsed, draggable = true, ...rest } = item;
const isLastChild = isLastItem(siblings, item);
const focused = this.state.movedItem && id === this.state.movedItem.id;
return (React.createElement("div", { className: st(classes.itemWrapper, {
dragEnabled: draggable && !this.props.readOnly,
}), "data-hook": item.dataHook
? `${item.dataHook}-item`
: `styled-nestable-list-item-${item.id}` },
!isRootItem(depth) &&
this._renderArrow({
isLastChild,
isPreview,
isPlaceholder,
}),
React.createElement("div", { className: st(classes.item, {
root: isRootItem(depth),
firstSibling: isFistItem(siblings, item),
placeholder: isPlaceholder,
preview: isPreview,
focused,
dragging: this.state.isDragging || focused,
withoutBottomBorder: isVeryLastItem &&
!this.props.withBottomBorder &&
!this.props.addItemLabel,
}) },
React.createElement(TableListItem, { ...rest, focused: focused, onBlur: this._onBlur, onKeyUp: e => this._moveItemViaKeyboard(e, options), showDivider: false, dragging: isPreview || focused, dragDisabled: !draggable && !this.props.readOnly, draggable: !this.props.readOnly, options: rest.options ? rest.options : [] }))));
};
this._onBlur = () => {
this._releaseItems();
};
this._moveItemViaKeyboard = (e, itemOptions) => {
const left = 'ArrowLeft';
const top = 'ArrowUp';
const right = 'ArrowRight';
const bottom = 'ArrowDown';
const enter = 'Enter';
const esc = 'Escape';
// start dragend-drop
if (e.key === enter && !this.state.movedItem) {
this._setStateDecorator({
backup: this.state.items,
movedItem: itemOptions.item,
items: setCollapse(this.state.items, itemOptions.item.id, true),
});
}
if (!this.state.movedItem) {
return;
}
switch (e.key) {
case esc:
this._setStateDecorator({
items: this.state.backup,
backup: null,
movedItem: null,
});
break;
case left:
if (!this.props.preventChangeDepth) {
this._setStateDecorator({
items: moveItemOutsideOfTheParent(this.state.items, itemOptions.item),
});
}
break;
case right:
if (!this.props.preventChangeDepth) {
this._setStateDecorator({
items: moveItemToTheChildOfPrevSibling(this.state.items, itemOptions.item),
});
}
break;
case top:
this._setStateDecorator({
items: moveItemVertically(this.state.items, itemOptions.item, VerticalMovementDirection.top),
});
break;
case bottom:
this._setStateDecorator({
items: moveItemVertically(this.state.items, itemOptions.item, VerticalMovementDirection.bottom),
});
break;
default:
case enter:
this._releaseItems();
break;
}
};
this._onDragEnd = () => {
this._setStateDecorator({
isDragging: false,
});
};
this._onDragStart = () => {
this._setStateDecorator({
isDragging: true,
});
};
this._renderActionsFactory = (...args) => {
const data = args[0];
const actions = data.isRootAction ? this.props.actions : data.item.actions;
if (actions) {
return this._renderActions(...args);
}
return this._renderAction(...args);
};
}
static getDerivedStateFromProps(props, state) {
if (state.isInternalStateUpdate) {
return {
...state,
isInternalStateUpdate: false,
};
}
return {
items: props.items,
};
}
_releaseItems() {
if (this.state.movedItem) {
const releaseItems = setCollapse(this.state.items, this.state.movedItem.id, this.state.movedItem.isCollapsed);
this.props.onChange({
items: releaseItems,
item: this.state.movedItem,
});
this._setStateDecorator({
movedItem: null,
backup: null,
items: releaseItems,
});
}
}
render() {
const { dataHook, className, maxDepth, onChange, preventChangeDepth, readOnly, zIndex, } = this.props;
return (React.createElement("div", { "data-hook": dataHook, className: st(classes.root, { dragEnabled: !readOnly }, className) },
React.createElement(NestableListBase, { items: this.state.items, renderItem: this._renderItem, renderPrefix: this._renderPrefix, renderAction: this._renderActionsFactory, onDragEnd: this._onDragEnd, onDragStart: this._onDragStart, onUpdate: ({ items, item }) => {
this._setStateDecorator({
items,
});
onChange({ items, item: item.data });
}, maxDepth: maxDepth, threshold: DEPTH_THRESHOLD, theme: theme, readOnly: readOnly, preventChangeDepth: preventChangeDepth, childrenStyle: {
position: 'relative',
marginLeft: `${DEPTH_THRESHOLD}px`,
}, childrenProperty: "children", isRenderDraggingChildren: false, zIndex: zIndex }),
this._renderActionsFactory({
depth: 0,
isRootAction: true,
})));
}
}
NestableList.displayName = 'NestableList';
const actions = PropTypes.arrayOf(PropTypes.shape({
onClick: PropTypes.func,
prefixIcon: PropTypes.node,
label: PropTypes.string,
}));
const Node = {
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
actions,
dataHook: PropTypes.string,
isCollapsed: PropTypes.bool,
addItemLabel: PropTypes.string,
...TableListItem.propTypes,
};
const NodeShape = PropTypes.shape(Node);
Node.children = PropTypes.arrayOf(NodeShape);
NestableList.propTypes = {
/** Actions that will be rendered for on the root depth level */
actions,
/** Adds a bottom border (divider) to the last item. */
withBottomBorder: PropTypes.bool,
/** Prevents the list from being reordered by removing the dragging grip icons. */
readOnly: PropTypes.bool,
/** Adds a button to create a new item (to the Root or a child to the existing item). */
addItemLabel: PropTypes.string,
/** Triggers function when the "Add new ..." button is clicked. */
onAddItem: PropTypes.func,
/** Triggers function when the item’s order or nesting position is changed. */
onChange: PropTypes.func,
/** Defines a maximum depth (number of levels) for the list. */
maxDepth: PropTypes.number,
/** Allows dragging and dropping an item only on its own depth (inside the same level). */
preventChangeDepth: PropTypes.bool,
/**
* Defines each Nestable List item individually, using the following props:
* * __id__ - specifies an item’s ID.
* * __addItemLabel__ - creates the “Add new ...” button with a given label at the bottom of the item.
* * __isCollapsed__ - bool prop, defines whether to render the item’s children.
* * __children__ - defines an item’s children.
* * __draggable__ - bool prop, turns on / off dragging ability for an item.
* * All <a href="https://www.wix-style-react.com/storybook/?path=/story/components-lists-table--tablelistitem" target="_blank">`<TableListItem/>`</a> props can be used to format item’s style and content.
*/
items: PropTypes.arrayOf(NodeShape),
/** Can be applied in the tests as a data-hook HTML attribute. */
dataHook: PropTypes.string,
/** Defines a CSS class to be applied to the component's Root element. */
className: PropTypes.string,
/** Defines the zIndex of the draggable layer */
zIndex: PropTypes.number,
};
NestableList.defaultProps = {
maxDepth: 10,
withBottomBorder: false,
preventChangeDepth: false,
items: [],
onAddItem: () => { },
};
export default NestableList;
//# sourceMappingURL=NestableList.js.map