wix-style-react
Version:
314 lines (286 loc) • 8.47 kB
JavaScript
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import { DragSource } from 'react-dnd';
import noop from 'lodash/noop';
import DragLayer from './DragLayer';
import { ItemTypes } from '../types';
import { dataAttributes } from '../constants';
import { getEmptyImage } from '../DragUtils';
/* eslint-disable new-cap */
const source = {
beginDrag: ({
id,
index,
containerId,
groupName,
item,
onMoveOut,
onDragStart,
onDrop,
}) => {
if (onDragStart) {
onDragStart({
id,
index,
containerId,
groupName,
item,
});
}
/** we setup monitor.getItem() snapshot, so we will be always able to get info about item that we drag */
return {
id,
index,
containerId,
groupName,
originalIndex: index, // as index is mutable during d&d, we need another storage for original index
originalItem: item,
onMoveOut,
realTime: {
onMoveOut,
onDrop,
containerId,
},
};
},
endDrag: (
{ id, index, containerId, groupName, item, onDragEnd },
monitor,
) => {
/** if drop was called, on drop target and drag is end, then we notify parent about this */
const { onDrop } = monitor.getItem().realTime;
if (onDragEnd) {
onDragEnd({
id,
index,
containerId,
groupName,
item,
});
}
if (monitor.getDropResult()) {
const isSameGroup =
groupName &&
monitor.getItem().groupName &&
groupName === monitor.getDropResult().groupName;
const isSameContainer =
containerId === monitor.getDropResult().containerId;
if (isSameGroup || isSameContainer) {
onDrop({
payload: monitor.getItem().originalItem, // original item
removedIndex: monitor.getItem().originalIndex, // original item index
addedIndex: monitor.getItem().index, // new item index
addedToContainerId: monitor.getDropResult().containerId, // new container for item
removedFromContainerId: containerId, // original item container
});
}
}
},
canDrag: ({ id, index, containerId, groupName, item, canDrag }) => {
return canDrag
? canDrag({
id,
index,
containerId,
groupName,
item,
})
: true;
},
isDragging: ({ id, containerId, groupName }, monitor) => {
const item = monitor.getItem();
const isSameGroup =
groupName && item.groupName && groupName === item.groupName;
const isSameContainer = containerId === item.containerId;
return (isSameGroup || isSameContainer) && item.id === id;
},
};
const collect = (connect, monitor) => ({
connectDragSource: connect.dragSource(),
connectDragPreview: connect.dragPreview(),
isDragging: monitor.isDragging(),
});
class DraggableSource extends React.Component {
state = {
offsetOfHandle: { x: 0, y: 0 },
itemWidth: null,
};
rootNode = null;
componentDidMount() {
if (this.props.connectDragPreview) {
this.props.connectDragPreview(getEmptyImage(), {
captureDraggingState: true,
});
}
this.updateDiff();
this.updateItemWidth();
}
componentDidUpdate(prevProps) {
if (
prevProps.id !== this.props.id ||
prevProps.containerId !== this.props.containerId
) {
this.updateDiff();
}
if (prevProps.isDragging !== this.props.isDragging) {
this.updateItemWidth();
}
}
updateDiff() {
/* in case if we have handle, the drag will start in wrong position and we need to fix this */
if (this.props.withHandle && this.handleNode) {
this.setState({
offsetOfHandle: {
x:
this.handleNode.getBoundingClientRect().x -
this.rootNode.getBoundingClientRect().x,
y:
this.handleNode.getBoundingClientRect().y -
this.rootNode.getBoundingClientRect().y,
},
});
}
}
updateItemWidth = () => {
if (this.rootNode) {
this.setState({ itemWidth: this.rootNode.getBoundingClientRect().width });
}
};
_getWrapperStyles() {
const { shift, ignoreMouseEvents, animationDuration, animationTiming } =
this.props;
const [xShift, yShift] = shift || [0, 0];
const hasShift = xShift || yShift;
const transition = ignoreMouseEvents
? `transform ${animationDuration}ms ${animationTiming}`
: undefined;
const transform = hasShift
? `translate(${xShift}px, ${yShift}px)`
: undefined;
const willChange = hasShift ? 'transform' : undefined;
const pointerEvents = ignoreMouseEvents || hasShift ? 'none' : undefined;
return {
willChange,
transition,
transform,
pointerEvents,
};
}
_renderDraggableItem() {
const {
isDragging,
connectDragSource,
withHandle,
renderItem,
id,
item,
delayed,
withStrip,
isInitialPositionToDrop,
} = this.props;
const content = React.cloneElement(
withHandle
? renderItem({
id,
item,
isPlaceholder: isDragging,
connectHandle: handle => {
const handleWithRef = React.cloneElement(handle, {
ref: node => (this.handleNode = ReactDOM.findDOMNode(node)),
});
return connectDragSource(handleWithRef);
},
delayed,
withStrip,
isInitialPositionToDrop,
})
: connectDragSource(
renderItem({
id,
item,
isPlaceholder: isDragging,
connectHandle: noop,
delayed,
withStrip,
isInitialPositionToDrop,
}),
),
{ [dataAttributes.draggableSource]: true, [dataAttributes.id]: id },
);
return <div style={this._getWrapperStyles()}>{content}</div>;
}
_setRootNode = node => {
// Don't need to reset the values if node remains the same
if (node && this.rootNode !== node) {
this.rootNode = node;
}
};
_renderPreview = ({ previewStyles }) => {
const { renderItem, id, item, delayed } = this.props;
return renderItem({
id,
item,
previewStyles: { width: this.state.itemWidth, ...previewStyles },
isPreview: true,
connectHandle: noop,
delayed,
});
};
_renderPreviewItem() {
const { id, usePortal } = this.props;
return (
<DragLayer
offsetOfHandle={this.state.offsetOfHandle}
usePortal={usePortal}
renderPreview={this._renderPreview}
id={id}
// pass 'width' this prop to rerender element with changed width from state
width={this.state.itemWidth}
draggedType={ItemTypes.DRAGGABLE}
/>
);
}
render() {
const { connectDragSource } = this.props;
return connectDragSource ? (
<div ref={this._setRootNode}>
{this._renderDraggableItem()}
{this._renderPreviewItem()}
</div>
) : null;
}
}
DraggableSource.propTypes = {
isDragging: PropTypes.bool, // from react-dnd
connectDragSource: PropTypes.func, // from react-dnd
connectDragPreview: PropTypes.func, // from react-dnd
usePortal: PropTypes.bool,
groupName: PropTypes.string,
containerId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
renderItem: PropTypes.func,
index: PropTypes.number,
id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
item: PropTypes.object,
withHandle: PropTypes.bool,
onDrop: PropTypes.func,
onMoveOut: PropTypes.func,
onDragStart: PropTypes.func,
onDragEnd: PropTypes.func,
/** visual positioning shifting for an element (transform: translate) without moving it from its real position at DOM (left, top) */
shift: PropTypes.arrayOf(PropTypes.number),
ignoreMouseEvents: PropTypes.bool,
/** animation duration in ms, default = 0 - disabled */
animationDuration: PropTypes.number,
/** animation timing function, default = linear */
animationTiming: PropTypes.string,
/** callback that could prevent item from dragging */
canDrag: PropTypes.func,
/** Is delay timer still waiting before user can drag the item */
delayed: PropTypes.bool,
};
export default DragSource(
ItemTypes.DRAGGABLE,
source,
collect,
)(DraggableSource);