Skip to content

Commit

Permalink
Merge pull request #6 from williaster/chris--dnd-robustness
Browse files Browse the repository at this point in the history
[dnd] refactor drop position logic
  • Loading branch information
williaster authored Feb 22, 2018
2 parents c9276a9 + 60a1ef2 commit 4c60f67
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 123 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import isValidChild from '../../util/isValidChild';
import shouldWrapChildInRow from '../../util/shouldWrapChildInRow';
import getDropPosition, { DROP_TOP, DROP_RIGHT, DROP_BOTTOM, DROP_LEFT } from '../../util/getDropPosition';

export default function handleDrop(props, monitor, Component) {
// this may happen due to throttling
if (!Component.mounted) return undefined;

Component.setState(() => ({ dropIndicator: null }));
const dropPosition = getDropPosition(monitor, Component);

if (!dropPosition) {
return undefined;
}

const {
parentComponent,
Expand All @@ -16,30 +21,10 @@ export default function handleDrop(props, monitor, Component) {

const draggingItem = monitor.getItem();

// if dropped self on self, do nothing
if (!draggingItem || draggingItem.draggableId === component.id) {
return undefined;
}

// append to self, or parent
const validChild = isValidChild({
parentType: component.type,
childType: draggingItem.type,
});

const validSibling = isValidChild({
parentType: parentComponent && parentComponent.type,
childType: draggingItem.type,
});

const shouldWrapSibling = shouldWrapChildInRow({
parentType: parentComponent && parentComponent.type,
childType: draggingItem.type,
});

if (!validChild && !validSibling) {
return undefined;
}
const dropAsChildOrSibling =
(orientation === 'row' && (dropPosition === DROP_TOP || dropPosition === DROP_BOTTOM)) ||
(orientation === 'column' && (dropPosition === DROP_LEFT || dropPosition === DROP_RIGHT))
? 'sibling' : 'child';

const dropResult = {
source: draggingItem.parentId ? {
Expand All @@ -49,33 +34,21 @@ export default function handleDrop(props, monitor, Component) {
draggableId: draggingItem.draggableId,
};

if (validChild && (!validSibling || shouldWrapSibling)) { // append it to component.children
// simplest case, append as child
if (dropAsChildOrSibling === 'child') {
dropResult.destination = {
droppableId: component.id,
index: component.children.length,
};
} else { // insert as sibling
} else {
// if the item is in the same list with a smaller index, you must account for the
// "missing" index upon movement within the list
const sameList =
draggingItem.parentId && parentComponent && draggingItem.parentId === parentComponent.id;
const sameListLowerIndex = sameList && draggingItem.index < componentIndex;

let nextIndex = sameListLowerIndex ? componentIndex - 1 : componentIndex;
const refBoundingRect = Component.ref.getBoundingClientRect();
const clientOffset = monitor.getClientOffset();

const sameParent = parentComponent && draggingItem.parentId === parentComponent.id;
const sameParentLowerIndex = sameParent && draggingItem.index < componentIndex;

if (clientOffset) {
if (orientation === 'row') {
const refMiddleY =
refBoundingRect.top + ((refBoundingRect.bottom - refBoundingRect.top) / 2);
nextIndex += clientOffset.y >= refMiddleY ? 1 : 0;
} else {
const refMiddleX =
refBoundingRect.left + ((refBoundingRect.right - refBoundingRect.left) / 2);
nextIndex += clientOffset.x >= refMiddleX ? 1 : 0;
}
let nextIndex = sameParentLowerIndex ? componentIndex - 1 : componentIndex;
if (dropPosition === DROP_BOTTOM || dropPosition === DROP_RIGHT) {
nextIndex += 1;
}

dropResult.destination = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,92 +1,36 @@
import throttle from 'lodash.throttle';
import isValidChild from '../../util/isValidChild';
import shouldWrapChildInRow from '../../util/shouldWrapChildInRow';
import getDropPosition, { DROP_TOP, DROP_RIGHT, DROP_BOTTOM, DROP_LEFT } from '../../util/getDropPosition';

const HOVER_THROTTLE_MS = 200;

function handleHover(props, monitor, Component) {
// this may happen due to throttling
if (!Component.mounted) return;

const {
component,
parentComponent,
orientation,
isDraggingOverShallow,
} = Component.props;
const dropPosition = getDropPosition(monitor, Component);

const draggingItem = monitor.getItem();

if (!draggingItem || draggingItem.draggableId === component.id) {
Component.setState(() => ({ dropIndicator: null }));
return;
}

const validChild = isValidChild({
parentType: component.type,
childType: draggingItem.type,
});

const validSibling = isValidChild({
parentType: parentComponent && parentComponent.type,
childType: draggingItem.type,
});

const shouldWrapSibling = shouldWrapChildInRow({
parentType: parentComponent && parentComponent.type,
childType: draggingItem.type,
});

if ((validChild && !isDraggingOverShallow) || (!validChild && !validSibling)) {
if (!dropPosition) {
Component.setState(() => ({ dropIndicator: null }));
return;
}

if (validChild && (!validSibling || shouldWrapSibling)) { // indicate drop in container
const indicatorOrientation = orientation === 'row' ? 'column' : 'row';

Component.setState(() => ({
dropIndicator: {
top: 0,
right: component.children.length ? 8 : null,
height: indicatorOrientation === 'column' ? '100%' : 3,
width: indicatorOrientation === 'column' ? 3 : '100%',
minHeight: indicatorOrientation === 'column' ? 16 : null,
minWidth: indicatorOrientation === 'column' ? null : 16,
margin: 'auto',
backgroundColor: '#44C0FF',
position: 'absolute',
zIndex: 10,
},
}));
} else { // indicate drop near parent
const refBoundingRect = Component.ref.getBoundingClientRect();
const clientOffset = monitor.getClientOffset();

if (clientOffset) {
let dropOffset;
if (orientation === 'row') {
const refMiddleY =
refBoundingRect.top + ((refBoundingRect.bottom - refBoundingRect.top) / 2);
dropOffset = clientOffset.y < refMiddleY ? 0 : refBoundingRect.height;
} else {
const refMiddleX =
refBoundingRect.left + ((refBoundingRect.right - refBoundingRect.left) / 2);
dropOffset = clientOffset.x < refMiddleX ? 0 : refBoundingRect.width;
}

Component.setState(() => ({
dropIndicator: {
top: orientation === 'column' ? 0 : dropOffset,
left: orientation === 'column' ? dropOffset : 0,
height: orientation === 'column' ? '100%' : 3,
width: orientation === 'column' ? 3 : '100%',
backgroundColor: '#44C0FF',
position: 'absolute',
zIndex: 10,
},
}));
}
}
// @TODO
// drop-indicator
// drop-indicator--top/right/bottom/left
Component.setState(() => ({
dropIndicator: {
top: dropPosition === DROP_BOTTOM ? '100%' : 0,
left: dropPosition === DROP_RIGHT ? '100%' : 0,
height: dropPosition === DROP_LEFT || dropPosition === DROP_RIGHT ? '100%' : 3,
width: dropPosition === DROP_TOP || dropPosition === DROP_BOTTOM ? '100%' : 3,
minHeight: dropPosition === DROP_LEFT || dropPosition === DROP_RIGHT ? 16 : null,
minWidth: dropPosition === DROP_TOP || dropPosition === DROP_BOTTOM ? 16 : null,
margin: 'auto',
backgroundColor: '#44C0FF',
position: 'absolute',
zIndex: 10,
},
}));
}

// this is called very frequently by react-dnd
Expand Down
88 changes: 88 additions & 0 deletions superset/assets/javascripts/dashboard/v2/util/getDropPosition.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import isValidChild from './isValidChild';

export const DROP_TOP = 'DROP_TOP';
export const DROP_RIGHT = 'DROP_RIGHT';
export const DROP_BOTTOM = 'DROP_BOTTOM';
export const DROP_LEFT = 'DROP_LEFT';

const SIBLING_DROP_THRESHOLD = 10;

export default function getDropPosition(monitor, Component) {
const {
parentComponent,
component,
orientation,
isDraggingOverShallow,
} = Component.props;

const draggingItem = monitor.getItem();

// if dropped self on self, do nothing
if (!draggingItem || draggingItem.draggableId === component.id || !isDraggingOverShallow) {
return null;
}

const validChild = isValidChild({
parentType: component.type,
childType: draggingItem.type,
});

const validSibling = isValidChild({
parentType: parentComponent && parentComponent.type,
childType: draggingItem.type,
});

if (!validChild && !validSibling) {
return null;
}

const hasChildren = component.children.length > 0;
const childDropOrientation = orientation === 'row' ? 'vertical' : 'horizontal';
const siblingDropOrientation = orientation === 'row' ? 'horizontal' : 'vertical';

if (validChild && !validSibling) { // easiest case, insert as child
if (childDropOrientation === 'vertical') {
return hasChildren ? DROP_RIGHT : DROP_LEFT;
}
return hasChildren ? DROP_BOTTOM : DROP_TOP;
}

const refBoundingRect = Component.ref.getBoundingClientRect();
const clientOffset = monitor.getClientOffset();

// Drop based on mouse position relative to component center
if (validSibling && !validChild) {
if (siblingDropOrientation === 'vertical') {
const refMiddleX =
refBoundingRect.left + ((refBoundingRect.right - refBoundingRect.left) / 2);
return clientOffset.x < refMiddleX ? DROP_LEFT : DROP_RIGHT;
}
const refMiddleY = refBoundingRect.top + ((refBoundingRect.bottom - refBoundingRect.top) / 2);
return clientOffset.y < refMiddleY ? DROP_TOP : DROP_BOTTOM;
}

// either is valid, so choose location based on boundary deltas
if (validSibling && validChild) {
const deltaTop = Math.abs(clientOffset.y - refBoundingRect.top);
const deltaBottom = Math.abs(clientOffset.y - refBoundingRect.bottom);
const deltaLeft = Math.abs(clientOffset.x - refBoundingRect.left);
const deltaRight = Math.abs(clientOffset.x - refBoundingRect.right);

// if near enough to a sibling boundary, drop there
if (siblingDropOrientation === 'vertical') {
if (deltaLeft < SIBLING_DROP_THRESHOLD) return DROP_LEFT;
if (deltaRight < SIBLING_DROP_THRESHOLD) return DROP_RIGHT;
} else {
if (deltaTop < SIBLING_DROP_THRESHOLD) return DROP_TOP;
if (deltaBottom < SIBLING_DROP_THRESHOLD) return DROP_BOTTOM;
}

// drop as child
if (childDropOrientation === 'vertical') {
return hasChildren ? DROP_RIGHT : DROP_LEFT;
}
return hasChildren ? DROP_BOTTOM : DROP_TOP;
}

return null;
}

0 comments on commit 4c60f67

Please sign in to comment.