UNPKG

wix-style-react

Version:
308 lines 14.5 kB
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