UNPKG

lucid-ui

Version:

A UI component library from AppNexus.

147 lines (146 loc) 6.15 kB
import _ from 'lodash'; import React, { useRef } from 'react'; import PropTypes from 'react-peek/prop-types'; import { lucidClassNames } from '../../util/style-helpers'; import { buildModernHybridComponent } from '../../util/state-management'; import DotsIcon from '../Icon/DotsIcon/DotsIcon'; import * as reducers from './DraggableList.reducers'; import { findTypes, omitProps, } from '../../util/component-types'; const cx = lucidClassNames.bind('&-DraggableList'); const { bool, func, object, number, string } = PropTypes; const DraggableListItem = (_props) => null; DraggableListItem.displayName = 'DraggableList.Item'; DraggableListItem.peek = { description: ` Renders a \`<div>\` that acts as an item in the list `, }; DraggableListItem.propName = 'Item'; DraggableListItem.propTypes = { children: PropTypes.node ``, }; /** Verifies its ok to drop an item given the current drag indexes and * provides a typeguard that dragIndex and dragOverIndex aren't undefined */ const isValidDropIndex = (dragIndexes) => { const { dragOverIndex, dragIndex } = dragIndexes; return (_.isNumber(dragOverIndex) && _.isNumber(dragIndex) && (dragOverIndex < dragIndex || dragOverIndex > dragIndex + 1)); }; const DraggableList = (props) => { const { style, className, dragIndex, dragOverIndex, hasDragHandle = true, onDragStart = _.noop, onDragEnd = _.noop, onDragOver = _.noop, onDrop = _.noop, ...passThroughs } = props; const lastItemEl = useRef(null); //This object helps handle 'undefined' indexes in a way that makes typescript happy const dragIndexes = { dragIndex, dragOverIndex }; const handleDragStart = (index) => { return (event) => { const { dataTransfer } = event; dataTransfer.effectAllowed = 'move'; dataTransfer.dropEffect = 'move'; dataTransfer.setData('drag', 'drag'); onDragStart(index, { event, props }); }; }; const handleDragEnd = (event) => { onDragEnd({ event, props }); if (isValidDropIndex(dragIndexes)) { onDrop({ oldIndex: dragIndex, newIndex: dragIndexes.dragOverIndex > dragIndexes.dragIndex ? dragIndexes.dragOverIndex - 1 : dragOverIndex, }, { event, props }); } }; const handleDragOver = (index) => { return (event) => { event.preventDefault(); if (dragOverIndex !== index) { onDragOver(index, { event, props }); } }; }; const handleDragLeave = (event) => { const childCount = findTypes(props, DraggableList.Item).length; const currentLastItemEl = lastItemEl.current; if (currentLastItemEl !== null) { //@ts-ignore const { bottom } = currentLastItemEl.getBoundingClientRect(); if (_.isFinite(dragIndex) && event.clientY > bottom) { onDragOver(childCount, { event, props: props }); } } }; const itemChildProps = _.map(findTypes(props, DraggableList.Item), 'props'); const dividerIndex = isValidDropIndex(dragIndexes) ? dragIndexes.dragOverIndex : -1; return (React.createElement("div", Object.assign({}, omitProps(passThroughs, undefined, _.keys(DraggableList.propTypes)), { className: cx('&', { '&-is-dragging': _.isNumber(dragIndex), }, className), style: style, onDragLeave: handleDragLeave }), _.map(itemChildProps, (itemChildProp, index) => { return (React.createElement("div", { key: index }, React.createElement("hr", { className: cx('&-Divider', { '&-Divider-is-visible': dividerIndex === index, }) }), React.createElement("div", { className: cx('&-Item', { '&-Item-is-dragging': dragIndex === index, '&-Item-is-drag-over': dragOverIndex === index, }, itemChildProp.className), draggable: true, onDragStart: handleDragStart(index), onDragEnd: handleDragEnd, onDragOver: handleDragOver(index) }, React.createElement("div", Object.assign({}, itemChildProp, { className: cx('&-Item-content'), ref: index === itemChildProps.length - 1 ? lastItemEl : null })), hasDragHandle && (React.createElement("span", { className: cx('&-Item-handle') }, React.createElement(DotsIcon, { size: 8 }), React.createElement(DotsIcon, { size: 8 })))))); }), React.createElement("hr", { key: 'divider', className: cx('&-Divider', { '&-Divider-is-visible': dividerIndex >= itemChildProps.length, }) }))); }; DraggableList.Item = DraggableListItem; DraggableList.displayName = 'DraggableList'; DraggableList.peek = { description: ` This is a container that renders divs in a list that can be drag and drop reordered. `, categories: ['controls'], }; DraggableList.propTypes = { className: string ` Appended to the component-specific class names set on the root element. `, style: object ` Passed through to the root element. `, hasDragHandle: bool ` Render a drag handle on list items `, dragIndex: number ` Index of the item the drag was started on `, dragOverIndex: number ` Index of the item the dragged item is hovered over `, onDragStart: func ` Called when the user starts to drag an item. Signature: \`(dragIndex, { event, props }) => {}\` `, onDragEnd: func ` Called when the user stops to dragging an item. Signature: \`({ event, props }) => {}\` `, onDragOver: func ` Called when the user drags an item over another item. Signature: \`(dragOverIndex, { event, props }) => {}\` `, onDrop: func ` Called when the user drops an item in the list Signature: \`({oldIndex, newIndex}, { event, props }) => {}\` `, Item: PropTypes.any ` Props for DraggableList.Item `, }; export default buildModernHybridComponent(DraggableList, { reducers }); export { DraggableList as DraggableListDumb };