wix-style-react
Version:
wix-style-react
374 lines • 16 kB
JavaScript
import React from 'react';
import { findDOMNode } from 'react-dom';
import { DragSource, DropTarget } from 'react-dnd';
import itemTypes from './itemTypes';
import { getValuesByKey, hoverAboveItself } from './utils';
import { NestableListBaseContext } from './NestableListBaseContext';
import { getEmptyImage } from '../DragAndDrop/Draggable/DragUtils';
import classNames from 'classnames';
import { dataAttributes } from '../DragAndDrop/Draggable/constants';
// keep track of horizontal mouse movement
const mouse = {
lastX: 0,
};
function increaseHorizontalLevel(prevPosition, prevIndex) {
const nextPosition = prevPosition.slice(0, -1);
// append to prevSibling's children
nextPosition.push(prevIndex - 1, -1);
return nextPosition;
}
function decreaseHorizontalLevel(prevPosition) {
const nextPosition = prevPosition.slice(0, -1);
nextPosition[nextPosition.length - 1] += 1;
return nextPosition;
}
function calculateHandleOffset(handleRect, containerRect) {
return {
x: handleRect.x - containerRect.x,
y: handleRect.y - containerRect.y,
};
}
const cardSource = {
isDragging(props, monitor) {
const ids = getValuesByKey(monitor.getItem().data, 'id', 'children');
return ids.indexOf(props.id) > -1;
},
beginDrag(props, monitor, component) {
props.onDragStart && props.onDragStart(props);
const node = findDOMNode(component);
const clientRect = node.getBoundingClientRect();
let handleOffset = { x: 0, y: 0 };
// needed to fix dnd drag offset data
if (component.handleNode) {
handleOffset = calculateHandleOffset(component.handleNode.getBoundingClientRect(), clientRect);
}
return {
id: props.id,
dragged: false, // needed for workaround of immediately fired dragend event after dragstart
index: props.index,
position: props.position,
data: props.item,
groupName: props.groupName,
depth: props.depth,
// rect for entire component including children
clientRect,
handleOffset,
};
},
endDrag: (props, monitor) => {
mouse.lastX = 0;
props.dropItem(monitor.getItem());
props.onDragEnd && props.onDragEnd(props);
},
};
const determineHorizontalPosition = ({ monitor, props, hoverNode }) => {
const item = monitor.getItem();
// the item being dragged
const { position: prevPosition, depth: dragDepth, index: prevIndex } = item;
// props for component underneath drag
const { position: hoverPosition, siblings: hoverSiblings, maxDepth, threshold, } = props;
// determine mouse position
const clientOffset = monitor.getClientOffset() || { x: 0, y: 0 };
const initialClientOffset = monitor.getInitialClientOffset() || {
x: 0,
y: 0,
};
// rect for entire component including children
const hoverClientRect = hoverNode.getBoundingClientRect();
const isOverSelf = hoverAboveItself(prevPosition, hoverPosition);
// set mouse.lastX if it isn't set yet (first hover event)
mouse.lastX = mouse.lastX || initialClientOffset.x;
const currMouseX = clientOffset.x;
const mouseDistanceX = currMouseX - mouse.lastX;
const nearLeftEdge = currMouseX < hoverClientRect.left + 10;
const currentIndex = props.index;
// nextPosition will be overwritten when moving horizontally
let nextPosition = hoverPosition;
// disable horizonal movement is previous sibling has lockDropArea=true prop
if (isOverSelf &&
currentIndex !== 0 &&
!!hoverSiblings[currentIndex - 1].lockDropArea &&
mouseDistanceX > 0) {
return nextPosition;
}
// disable horizonal movement is next sibling has lockDropArea=true prop
if (isOverSelf &&
currentIndex < hoverSiblings.length - 2 &&
!!hoverSiblings[currentIndex + 1].lockDropArea &&
mouseDistanceX < 0) {
return nextPosition;
}
// moving horizontally
if (isOverSelf && (nearLeftEdge || Math.abs(mouseDistanceX) >= threshold)) {
// reset lastX for new phase
mouse.lastX = currMouseX;
// increase horizontal level
if (mouseDistanceX > 0 &&
// has previous sibling
prevIndex - 1 >= 0 &&
// isn't at max depth
prevPosition.length + dragDepth - 1 !== maxDepth) {
nextPosition = increaseHorizontalLevel(prevPosition, prevIndex);
}
// decrease horizontal level
if (mouseDistanceX < 0 &&
// is nested
prevPosition.length > 1 &&
// is last item in array
prevIndex === hoverSiblings.length - 1) {
nextPosition = decreaseHorizontalLevel(prevPosition);
}
}
if (props.preventChangeDepth &&
nextPosition.length - prevPosition.length === -1 // means that new parent is suitable for current dragged item
) {
const isSameParent = nextPosition.every((position, depth) => {
return prevPosition[depth] === position;
});
if (!isSameParent) {
nextPosition = prevPosition.map((position, depth) => {
return nextPosition[depth] !== undefined ? nextPosition[depth] : 0;
});
}
}
return nextPosition;
};
const allowItemMove = ({ prevPosition, nextPosition, monitor, hoverNode, props, }) => {
// don't replace items with themselves
if (hoverAboveItself(prevPosition, nextPosition)) {
return;
}
// prevent drop if preventChangeDepth and depth is changed
if (props.preventChangeDepth && nextPosition.length !== prevPosition.length) {
return;
}
const { position: hoverPosition } = props;
const isOverSelf = hoverAboveItself(prevPosition, hoverPosition);
const clientOffset = monitor.getClientOffset() || { x: 0, y: 0 };
// rect for entire component including children
const hoverClientRect = hoverNode.getBoundingClientRect();
// rect for item without children
const hoverItemClientRect = hoverNode.children[0].getBoundingClientRect();
// get vertical middle
const hoverMiddleY = (hoverClientRect.bottom - hoverClientRect.top) / 2;
// get pixels to the top
const hoverClientY = clientOffset.y - hoverClientRect.top;
// dragging child item to another position with same parent
if (nextPosition.length === prevPosition.length) {
const last = nextPosition.length - 1;
const previousIndex = prevPosition[last];
const nextIndex = nextPosition[last];
// only perform the move when the mouse has crossed half of the items height
// when dragging downwards, only move when the cursor is below 50%
// when dragging upwards, only move when the cursor is above 50%
// dragging downwards
if (previousIndex < nextIndex && hoverClientY < hoverMiddleY) {
return;
}
// dragging upwards
if (previousIndex > nextIndex && hoverClientY > hoverMiddleY) {
return;
}
}
else if (
// dragging child item over parent item
nextPosition.length < prevPosition.length &&
nextPosition[nextPosition.length - 1] ===
prevPosition[prevPosition.length - 2]) {
const hoverItemMiddleY = (hoverItemClientRect.bottom - hoverItemClientRect.top) / 2;
// cancel if hovering in lower half of parent item
if (hoverClientY > hoverItemMiddleY) {
return;
}
}
else if (!isOverSelf && clientOffset.y > hoverItemClientRect.bottom) {
// cancel if over a nested target that isn't its own child
return;
}
return true;
};
const cardTarget = {
hover(props, monitor, component) {
// prevent drag and drop between different groups.
// currently drag and drop between multiple nestable lists is not supported
if (monitor.getItem().groupName !== props.groupName) {
return;
}
const item = monitor.getItem();
// the item being dragged
const { position: prevPosition, data: dragItem, depth: dragDepth } = item;
// props for component underneath drag
const { position: hoverPosition, maxDepth } = props;
const hoverDepth = hoverPosition.length - 1;
const totalDepth = hoverDepth + dragDepth;
// don't exceed max depth
if (totalDepth > maxDepth) {
return;
}
const hoverNode = findDOMNode(component);
const nextPosition = determineHorizontalPosition({
monitor,
props,
hoverNode,
});
if (!allowItemMove({ prevPosition, nextPosition, monitor, hoverNode, props })) {
return;
}
// this is where the actual move happens
const nextPos = props.moveItem({
dragItem,
prevPosition,
nextPosition,
});
item.prevPosition = prevPosition;
item.prevIndex = item.index;
item.dragged = true;
// note: we're mutating the monitor item here!
// generally it's better to avoid mutations,
// but it's good here for the sake of performance
// to avoid expensive index searches
item.position = nextPos;
item.index = nextPos[nextPos.length - 1];
},
};
class Item extends React.PureComponent {
constructor() {
super(...arguments);
this.state = {
shouldRenderChildren: true,
};
this.unmounted = false;
}
componentWillUnmount() {
this.unmounted = true;
}
componentDidMount() {
// use empty image as a drag preview so browsers don't draw it
// and we can draw whatever we want on the custom drag layer instead.
this.props.connectDragPreview(getEmptyImage(), {
// IE fallback: specify that we'd rather screenshot the node
// when it already knows it's being dragged so we can hide it with CSS.
captureDraggingState: true,
});
this.updateShouldRenderChildren();
}
componentDidUpdate() {
this.updateShouldRenderChildren();
}
updateShouldRenderChildren() {
const { isPlaceholder, isRenderDraggingChildren } = this.props;
const shouldRenderChildren = !isPlaceholder || isRenderDraggingChildren;
// start workaround of immediately fired dragend event after dragstart
// https://github.com/react-dnd/react-dnd/issues/766#issuecomment-748255082
if (shouldRenderChildren !== this.state.shouldRenderChildren) {
if (!this.props.dragged &&
this.state.shouldRenderChildren !== shouldRenderChildren) {
setTimeout(() => {
if (!this.unmounted) {
this.setState({
shouldRenderChildren,
});
}
}, 0);
}
else {
this.setState({
shouldRenderChildren,
});
}
}
// end workaround of immediately fired dragend event after dragstart
}
render() {
const { item, position, children, isPlaceholder, connectDragSource, connectDropTarget, useDragHandle, renderItem, renderPrefix, theme, readOnly, isVeryLastItem, siblings, } = this.props;
// params passed to renderItem callback
const renderParams = {
item,
siblings,
isVeryLastItem,
isPlaceholder,
isPreview: false,
connectDragSource: () => { },
depth: position.length,
};
const draggableTargetDataProps = {
[dataAttributes.draggableTarget]: true,
'data-hook': 'nestable-item',
};
const renderItemWithDataAttributes = params => React.cloneElement(renderItem(params), {
[dataAttributes.draggableSource]: true,
[dataAttributes.depth]: position.length - 1,
[dataAttributes.id]: item.id,
});
const classes = classNames('nestable-item', theme && theme.item);
const { draggable = true, lockDropArea = false, isParentLocked = false, } = item;
if (!draggable || readOnly) {
if (lockDropArea) {
return (React.createElement("div", { className: classes, ...draggableTargetDataProps },
renderPrefix(renderParams),
renderItemWithDataAttributes(renderParams),
this.state.shouldRenderChildren && children));
}
if (isParentLocked && draggable && !readOnly) {
return connectDragSource(React.createElement("div", { className: classes, ...draggableTargetDataProps },
renderPrefix(renderParams),
renderItemWithDataAttributes(renderParams),
this.state.shouldRenderChildren && children));
}
return (React.createElement("div", { className: classes, ...draggableTargetDataProps },
renderPrefix(renderParams),
renderItemWithDataAttributes(renderParams),
this.state.shouldRenderChildren && children));
}
if (useDragHandle) {
renderParams.connectDragSource = handle => {
const handleWithRef = React.cloneElement(handle, {
ref: node => (this.handleNode = findDOMNode(node)),
});
return connectDragSource(handleWithRef);
};
if (lockDropArea) {
return connectDropTarget(React.createElement("div", { className: classes, ...draggableTargetDataProps },
renderPrefix(renderParams),
renderItemWithDataAttributes(renderParams),
this.state.shouldRenderChildren && children));
}
if (isParentLocked) {
return connectDragSource(React.createElement("div", { className: classes, ...draggableTargetDataProps },
renderPrefix(renderParams),
renderItemWithDataAttributes(renderParams),
this.state.shouldRenderChildren && children));
}
return connectDropTarget(React.createElement("div", { className: classes, ...draggableTargetDataProps },
renderPrefix(renderParams),
renderItemWithDataAttributes(renderParams),
this.state.shouldRenderChildren && children));
}
if (isParentLocked) {
return connectDragSource(React.createElement("div", { className: classes, ...draggableTargetDataProps },
renderPrefix(renderParams),
renderItemWithDataAttributes(renderParams),
this.state.shouldRenderChildren && children));
}
return connectDropTarget(connectDragSource(React.createElement("div", { className: classes, ...draggableTargetDataProps },
renderPrefix(renderParams),
renderItemWithDataAttributes(renderParams),
this.state.shouldRenderChildren && children)));
}
}
export const DragItemSource = DragSource(itemTypes.nestedItem, cardSource, (connect, monitor) => ({
connectDragSource: connect.dragSource(),
connectDragPreview: connect.dragPreview(),
isPlaceholder: monitor.isDragging(),
dragged: monitor.getItem() && monitor.getItem().dragged,
}))(Item);
export const DropItemTarget = DropTarget(itemTypes.nestedItem, cardTarget, connect => ({
connectDropTarget: connect.dropTarget(),
}))(DragItemSource);
class ItemWithContext extends React.PureComponent {
render() {
return (React.createElement(NestableListBaseContext.Consumer, null, context => React.createElement(DropItemTarget, { ...this.props, ...context })));
}
}
export default ItemWithContext;
//# sourceMappingURL=Item.js.map