lucid-ui
Version:
A UI component library from AppNexus.
147 lines (146 loc) • 6.15 kB
JavaScript
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 };