UNPKG

@wix/design-system

Version:

@wix/design-system

315 lines 16 kB
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