UNPKG

wix-style-react

Version:
391 lines (365 loc) 10.9 kB
import React from 'react'; import PropTypes from 'prop-types'; import { classes as theme } from './NestableListTheme.st.css'; import { classes, st } from './StyledNestableList.st.css'; import NestableList from '../NestableList'; import TableListItem from '../TableListItem'; import Box from '../Box'; import { Arrow } from './Arrow'; import TextButton from '../TextButton'; import Add from 'wix-ui-icons-common/Add'; import { isFistItem, isLastItem, isRootItem, moveItemOutsideOfTheParent, moveItemToTheChildOfPrevSibling, moveItemVertically, setCollapse, VerticalMovementDirection, } from '../NestableList/utils'; import { DEPTH_THRESHOLD } from './constants'; /** A styled list with drag and drop and nesting capabilities. */ class StyledNestableList extends React.PureComponent { 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 _setStateDecorator = state => { this.setState({ ...state, isInternalStateUpdate: true, }); }; static getDerivedStateFromProps(props, state) { if (state.isInternalStateUpdate) { return { ...state, isInternalStateUpdate: false, }; } return { items: props.items, }; } _renderArrow = ({ isLastChild = false, isPreview = false, isPlaceholder, } = {}) => { return ( <Box className={classes.arrowContainer} backgroundColor={isPlaceholder ? 'D60' : undefined} direction={'vertical'} > <div className={st(classes.arrow, { lastChild: isLastChild, preview: isPreview, placeholder: isPlaceholder, })} > <Arrow className={classes.horizontalArrow} /> </div> </Box> ); }; _renderAction = data => { const label = data.isRootAction ? this.props.addItemLabel : data.item.addItemLabel; if (!label || this.props.maxDepth <= data.depth) { return; } return ( <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, )} > <TableListItem onClick={() => this.props.onAddItem(data.item)} options={[ { value: ( <Box> <TextButton size={'small'} prefixIcon={<Add />}> {label} </TextButton> </Box> ), }, ]} /> </Box> ); }; _renderPrefix = data => { if ( data.isPreview || isLastItem(data.siblings, data.item) || isRootItem(data.depth) ) { return null; } return <div className={classes.childArrow} />; }; _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 ( <div className={st(classes.itemWrapper, { dragEnabled: draggable && !this.props.readOnly, })} data-hook={`styled-nestable-list-item-${item.id}`} > {!isRootItem(depth) && this._renderArrow({ isLastChild, isPreview, isPlaceholder, })} <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, })} > <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 : []} /> </div> </div> ); }; _onBlur = () => { this._releaseItems(); }; _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; } }; _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, }); } } _onDragEnd = () => { this._setStateDecorator({ isDragging: false, }); }; _onDragStart = () => { this._setStateDecorator({ isDragging: true, }); }; render() { const { dataHook, className, maxDepth, onChange, preventChangeDepth, readOnly, } = this.props; return ( <div data-hook={dataHook} className={st(classes.root, { dragEnabled: !readOnly }, className)} > <NestableList items={this.state.items} renderItem={this._renderItem} renderPrefix={this._renderPrefix} renderAction={this._renderAction} onDragEnd={this._onDragEnd} onDragStart={this._onDragStart} onUpdate={({ items, item }) => { this._setStateDecorator({ items: 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} /> {this.props.addItemLabel && this._renderAction({ depth: 0, isRootAction: true, })} </div> ); } } StyledNestableList.displayName = 'StyledNestableList'; const Node = { id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, isCollapsed: PropTypes.bool, addItemLabel: PropTypes.string, ...TableListItem.propTypes, }; const NodeShape = PropTypes.shape(Node); Node.children = PropTypes.arrayOf(NodeShape); StyledNestableList.propTypes = { /** 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/?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, }; StyledNestableList.defaultProps = { maxDepth: 10, withBottomBorder: false, preventChangeDepth: false, items: [], onAddItem: () => {}, }; export default StyledNestableList;