@wix/design-system
Version:
@wix/design-system
315 lines • 16 kB
JavaScript
import React from 'react';
import { classes as theme } from './NestableListTheme.st.css.js';
import { classes, st } from './NestableList.st.css.js';
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 { DEFAULT_INDENT_SIZE, indentSizes } from './constants';
import { getDepthThreshold, normalizeIndentSize } from './utils';
import { IconThemeContext } from '../WixDesignSystemIconThemeProvider/IconThemeContext';
/** 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,
normalizedIndentSize: DEFAULT_INDENT_SIZE,
};
// 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: st(classes.arrowContainer, {
indent: this.state.normalizedIndentSize,
}), direction: "vertical" },
React.createElement("div", { className: st(classes.arrow, {
lastChild: isLastChild,
preview: isPreview,
placeholder: isPlaceholder,
offset: this.state.normalizedIndentSize,
}) },
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,
last: veryLastItem,
dragging: this.state.isDragging,
withoutDividers: this.props.dividers === false,
withoutBottomBorder: !this.props.withBottomBorder && (veryLastItem || isRootAction),
indent: this.state.normalizedIndentSize,
}, 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, AddIcon = Add) => {
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,
last: data.veryLastItem,
dragging: this.state.isDragging,
withoutDividers: this.props.dividers === false,
withoutBottomBorder: !this.props.withBottomBorder &&
((data.veryLastItem && !data.addItemLabel) || data.isRootAction),
indent: this.state.normalizedIndentSize,
}, 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(AddIcon, null) }, label))),
},
] })));
};
this._renderPrefix = data => {
if (data.isPreview ||
isLastItem(data.siblings, data.item) ||
isRootItem(data.depth)) {
return null;
}
return (React.createElement("div", { className: st(classes.childArrow, {
offset: this.state.normalizedIndentSize,
}) }));
};
this._renderItem = options => {
const { isPlaceholder, depth, isPreview, isVeryLastItem, siblings, item } = options;
const { id, children, isCollapsed, draggable = true, dragDisabled, hideDragHandle, ...rest } = item;
const isLastChild = isLastItem(siblings, item);
const focused = this.state.movedItem && id === this.state.movedItem.id;
const hasExpandableSiblings = siblings?.some(sibling => sibling.expandable);
const hasDraggableSiblings = siblings?.some(sibling => !sibling.hideDragHandle) &&
!this.props.readOnly;
const isStatic = (draggable === false && hideDragHandle === true) || this.props.readOnly;
const isDraggableWithDisabledHandle = !isStatic && (draggable === false || dragDisabled === true);
const isDraggableWithoutHandle = !isStatic && hideDragHandle === true;
const onBlur = () => {
this._onBlur();
rest.onBlur?.();
};
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}` },
this.props.hierarchyIndicator &&
!isRootItem(depth) &&
this._renderArrow({
isLastChild,
isPreview,
isPlaceholder,
}),
React.createElement("div", { className: st(classes.item, {
root: isRootItem(depth),
firstSibling: isFistItem(siblings, item),
last: isVeryLastItem,
placeholder: isPlaceholder,
preview: isPreview,
focused,
dragging: this.state.isDragging || focused,
withoutDividers: this.props.dividers === false && !focused && !isPreview,
withoutBottomBorder: isVeryLastItem &&
!this.props.withBottomBorder &&
!this.props.addItemLabel,
}) },
React.createElement(TableListItem, { ...rest, className: st(rest.className, {}, classes.tableListItem), reserveExpandHandleSpace: hasExpandableSiblings, reserveDragHandleSpace: hasDraggableSiblings, focused: focused, onBlur: onBlur, onKeyUp: e => this._moveItemViaKeyboard(e, options), showDivider: false, dragging: isPreview || focused, draggable: !isStatic, dragDisabled: isDraggableWithDisabledHandle, hideDragHandle: isDraggableWithoutHandle, options: rest.options ? rest.options : [], dragHandleSize: this.props.dragHandleSize, showDragHandleOnHover: this.props.showDragHandleOnHover }))));
};
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({
items: this.state.items,
item: itemOptions.item,
step: VerticalMovementDirection.top,
preventChangeParent: this.props.preventChangeParent,
enforcePinnedOrder: this.props.enforcePinnedOrder,
}),
});
break;
case bottom:
this._setStateDecorator({
items: moveItemVertically({
items: this.state.items,
item: itemOptions.item,
step: VerticalMovementDirection.bottom,
preventChangeParent: this.props.preventChangeParent,
enforcePinnedOrder: this.props.enforcePinnedOrder,
}),
});
break;
default:
case enter:
this._releaseItems();
break;
}
};
this._onDragEnd = () => {
this.props.onDragEnd?.();
this._setStateDecorator({
isDragging: false,
});
};
this._onDragStart = () => {
this.props.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);
};
// Stable function reference passed to NestableListBase as renderAction.
// _currentAddIcon is set synchronously each render so calls always pick up
// the latest icon without creating a new function on every render.
this._currentAddIcon = Add;
this._stableRenderAction = data => this._renderActionsFactory(data, this._currentAddIcon);
}
static getDerivedStateFromProps(props, state) {
if (state.isInternalStateUpdate) {
return {
...state,
isInternalStateUpdate: false,
};
}
return {
items: props.items,
normalizedIndentSize: normalizeIndentSize(props.indentSize),
};
}
_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, preventChangeParent, enforcePinnedOrder, canDrop, readOnly, zIndex, hierarchyIndicator, virtualized, itemHeight, viewportHeight, offscreenRowCount, } = this.props;
const depthThreshold = getDepthThreshold(this.state.normalizedIndentSize);
return (React.createElement(IconThemeContext.Consumer, null, ({ icons = {} }) => {
const { NestableList: nestableListIcons = {} } = icons;
const { Add: AddIcon = Add } = nestableListIcons;
this._currentAddIcon = AddIcon;
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: hierarchyIndicator ? this._renderPrefix : undefined, renderAction: this._stableRenderAction, onDragEnd: this._onDragEnd, onDragStart: this._onDragStart, onUpdate: ({ items, item }) => {
this._setStateDecorator({
items,
});
onChange({ items, item: item.data });
}, maxDepth: maxDepth, threshold: depthThreshold, theme: theme, readOnly: readOnly, preventChangeDepth: preventChangeDepth, preventChangeParent: preventChangeParent, enforcePinnedOrder: enforcePinnedOrder, canDrop: canDrop, childrenStyle: {
position: 'relative',
marginLeft: `${depthThreshold}px`,
}, childrenProperty: "children", isRenderDraggingChildren: false, zIndex: zIndex, virtualized: virtualized, itemHeight: itemHeight, viewportHeight: viewportHeight, offscreenRowCount: offscreenRowCount, pinnedItemId: this.state.movedItem?.id }),
this._renderActionsFactory({
depth: 0,
isRootAction: true,
}, AddIcon)));
}));
}
}
NestableList.displayName = 'NestableList';
NestableList.defaultProps = {
maxDepth: 10,
withBottomBorder: false,
preventChangeDepth: false,
preventChangeParent: false,
dragHandleSize: 'large',
showDragHandleOnHover: false,
enforcePinnedOrder: false,
hierarchyIndicator: true,
items: [],
onAddItem: () => { },
};
export default NestableList;
//# sourceMappingURL=NestableList.js.map