trello-smooth-dnd
Version:
Drag and Drop library to support react-trello project
480 lines (429 loc) • 14.7 kB
JavaScript
import './polyfills';
import * as Utils from './utils';
import * as constants from './constants';
import { addStyleToHead, addCursorStyleToBody, removeStyle } from './styles';
import dragScroller from './dragscroller';
const grabEvents = ['mousedown', 'touchstart'];
const moveEvents = ['mousemove', 'touchmove'];
const releaseEvents = ['mouseup', 'touchend'];
let dragListeningContainers = null;
let grabbedElement = null;
let ghostInfo = null;
let draggableInfo = null;
let containers = [];
let isDragging = false;
let removedElement = null;
let handleDrag = null;
let handleScroll = null;
let sourceContainer = null;
let sourceContainerLockAxis = null;
let cursorStyleElement = null;
// Utils.addClass(document.body, 'clearfix');
const isMobile = Utils.isMobile();
function listenEvents() {
if (typeof window !== 'undefined') {
addGrabListeners();
}
}
function addGrabListeners() {
grabEvents.forEach(e => {
global.document.addEventListener(e, onMouseDown, { passive: false });
});
}
function addMoveListeners() {
moveEvents.forEach(e => {
global.document.addEventListener(e, onMouseMove, { passive: false });
});
}
function removeMoveListeners() {
moveEvents.forEach(e => {
global.document.removeEventListener(e, onMouseMove, { passive: false });
});
}
function addReleaseListeners() {
releaseEvents.forEach(e => {
global.document.addEventListener(e, onMouseUp, { passive: false });
});
}
function removeReleaseListeners() {
releaseEvents.forEach(e => {
global.document.removeEventListener(e, onMouseUp, { passive: false });
});
}
function getGhostParent() {
if (draggableInfo.ghostParent) {
return draggableInfo.ghostParent;
}
if (grabbedElement) {
return grabbedElement.parentElement || global.document.body;
} else {
return global.document.body;
}
}
function getGhostElement(wrapperElement, { x, y }, container, cursor) {
const { scaleX = 1, scaleY = 1 } = container.getScale();
const { left, top, right, bottom } = wrapperElement.getBoundingClientRect();
const midX = left + (right - left) / 2;
const midY = top + (bottom - top) / 2;
const ghost = wrapperElement.cloneNode(true);
ghost.style.zIndex = 1000;
ghost.style.boxSizing = 'border-box';
ghost.style.position = 'fixed';
ghost.style.left = left + 'px';
ghost.style.top = top + 'px';
ghost.style.width = right - left + 'px';
ghost.style.height = bottom - top + 'px';
ghost.style.overflow = 'visible';
ghost.style.transition = null;
ghost.style.removeProperty('transition');
ghost.style.pointerEvents = 'none';
if (container.getOptions().dragClass) {
setTimeout(() => {
Utils.addClass(ghost.firstElementChild, container.getOptions().dragClass);
const dragCursor = global.getComputedStyle(ghost.firstElementChild).cursor;
cursorStyleElement = addCursorStyleToBody(dragCursor);
});
} else {
cursorStyleElement = addCursorStyleToBody(cursor);
}
Utils.addClass(ghost, container.getOptions().orientation);
Utils.addClass(ghost, constants.ghostClass);
return {
ghost: ghost,
centerDelta: { x: midX - x, y: midY - y },
positionDelta: { left: left - x, top: top - y }
};
}
function getDraggableInfo(draggableElement) {
const container = containers.filter(p => draggableElement.parentElement === p.element)[0];
const draggableIndex = container.draggables.indexOf(draggableElement);
const getGhostParent = container.getOptions().getGhostParent;
return {
container,
element: draggableElement,
elementIndex: draggableIndex,
payload: container.getOptions().getChildPayload
? container.getOptions().getChildPayload(draggableIndex)
: undefined,
targetElement: null,
position: { x: 0, y: 0 },
groupName: container.getOptions().groupName,
ghostParent: getGhostParent ? getGhostParent() : null,
};
}
function handleDropAnimation(callback) {
function endDrop() {
Utils.removeClass(ghostInfo.ghost, 'animated');
ghostInfo.ghost.style.transitionDuration = null;
getGhostParent().removeChild(ghostInfo.ghost);
callback();
}
function animateGhostToPosition({ top, left }, duration, dropClass) {
Utils.addClass(ghostInfo.ghost, 'animated');
if (dropClass) {
Utils.addClass(ghostInfo.ghost.firstElementChild, dropClass);
}
ghostInfo.ghost.style.transitionDuration = duration + 'ms';
ghostInfo.ghost.style.left = left + 'px';
ghostInfo.ghost.style.top = top + 'px';
setTimeout(function() {
endDrop();
}, duration + 20);
}
function shouldAnimateDrop(options) {
return options.shouldAnimateDrop
? options.shouldAnimateDrop(draggableInfo.container.getOptions(), draggableInfo.payload)
: true;
}
if (draggableInfo.targetElement) {
const container = containers.filter(p => p.element === draggableInfo.targetElement)[0];
if (shouldAnimateDrop(container.getOptions())) {
const dragResult = container.getDragResult();
animateGhostToPosition(
dragResult.shadowBeginEnd.rect,
Math.max(150, container.getOptions().animationDuration / 2),
container.getOptions().dropClass
);
} else {
endDrop();
}
} else {
const container = containers.filter(p => p === draggableInfo.container)[0];
const { behaviour, removeOnDropOut } = container.getOptions();
if (behaviour === 'move' && !removeOnDropOut && container.getDragResult()) {
const { removedIndex, elementSize } = container.getDragResult();
const layout = container.layout;
// drag ghost to back
container.getTranslateCalculator({
dragResult: {
removedIndex,
addedIndex: removedIndex,
elementSize
}
});
const prevDraggableEnd =
removedIndex > 0
? layout.getBeginEnd(container.draggables[removedIndex - 1]).end
: layout.getBeginEndOfContainer().begin;
animateGhostToPosition(
layout.getTopLeftOfElementBegin(prevDraggableEnd),
container.getOptions().animationDuration,
container.getOptions().dropClass
);
} else {
Utils.addClass(ghostInfo.ghost, 'animated');
ghostInfo.ghost.style.transitionDuration = container.getOptions().animationDuration + 'ms';
ghostInfo.ghost.style.opacity = '0';
ghostInfo.ghost.style.transform = 'scale(0.90)';
setTimeout(function() {
endDrop();
}, container.getOptions().animationDuration);
}
}
}
const handleDragStartConditions = (function handleDragStartConditions() {
let startEvent;
let delay;
let clb;
let timer = null;
const moveThreshold = 1;
const maxMoveInDelay = 5;
function onMove(event) {
const { clientX: currentX, clientY: currentY } = getPointerEvent(event);
if (!delay) {
if (
Math.abs(startEvent.clientX - currentX) > moveThreshold ||
Math.abs(startEvent.clientY - currentY) > moveThreshold
) {
return callCallback();
}
} else {
if (
Math.abs(startEvent.clientX - currentX) > maxMoveInDelay ||
Math.abs(startEvent.clientY - currentY) > maxMoveInDelay
) {
deregisterEvent();
}
}
}
function onUp() {
deregisterEvent();
}
function onHTMLDrag() {
deregisterEvent();
}
function registerEvents() {
if (delay) {
timer = setTimeout(callCallback, delay);
}
moveEvents.forEach(e => global.document.addEventListener(e, onMove), {
passive: false
});
releaseEvents.forEach(e => global.document.addEventListener(e, onUp), {
passive: false
});
global.document.addEventListener('drag', onHTMLDrag, {
passive: false
});
}
function deregisterEvent() {
clearTimeout(timer);
moveEvents.forEach(e => global.document.removeEventListener(e, onMove), {
passive: false
});
releaseEvents.forEach(e => global.document.removeEventListener(e, onUp), {
passive: false
});
global.document.removeEventListener('drag', onHTMLDrag, {
passive: false
});
}
function callCallback() {
clearTimeout(timer);
deregisterEvent();
clb();
}
return function(_startEvent, _delay, _clb) {
startEvent = getPointerEvent(_startEvent);
delay = (typeof _delay === 'number') ? _delay : (isMobile ? 200 : 0);
clb = _clb;
registerEvents();
};
})();
function onMouseDown(event) {
const e = getPointerEvent(event);
if (!isDragging && (e.button === undefined || e.button === 0)) {
grabbedElement = Utils.getParent(e.target, '.' + constants.wrapperClass);
if (grabbedElement) {
const containerElement = Utils.getParent(grabbedElement, '.' + constants.containerClass);
const container = containers.filter(p => p.element === containerElement)[0];
const dragHandleSelector = container.getOptions().dragHandleSelector;
const nonDragAreaSelector = container.getOptions().nonDragAreaSelector;
let startDrag = true;
if (dragHandleSelector && !Utils.getParent(e.target, dragHandleSelector)) {
startDrag = false;
}
if (nonDragAreaSelector && Utils.getParent(e.target, nonDragAreaSelector)) {
startDrag = false;
}
if (startDrag) {
handleDragStartConditions(e, container.getOptions().dragBeginDelay, () => {
Utils.clearSelection();
initiateDrag(e, Utils.getElementCursor(event.target));
addMoveListeners();
addReleaseListeners();
});
}
}
}
}
function onMouseUp() {
removeMoveListeners();
removeReleaseListeners();
handleScroll({ reset: true });
if (cursorStyleElement) {
removeStyle(cursorStyleElement);
cursorStyleElement = null;
}
if (draggableInfo) {
handleDropAnimation(() => {
Utils.removeClass(global.document.body, constants.disbaleTouchActions);
Utils.removeClass(global.document.body, constants.noUserSelectClass);
fireOnDragStartEnd(false);
(dragListeningContainers || []).forEach(p => {
p.handleDrop(draggableInfo);
});
dragListeningContainers = null;
grabbedElement = null;
ghostInfo = null;
draggableInfo = null;
isDragging = false;
sourceContainer = null;
sourceContainerLockAxis = null;
handleDrag = null;
});
}
}
function getPointerEvent(e) {
return e.touches ? e.touches[0] : e;
}
function dragHandler(dragListeningContainers) {
let targetContainers = dragListeningContainers;
return function(draggableInfo) {
let containerBoxChanged = false;
targetContainers.forEach(p => {
const dragResult = p.handleDrag(draggableInfo);
containerBoxChanged |= dragResult.containerBoxChanged || false;
dragResult.containerBoxChanged = false;
});
handleScroll({ draggableInfo });
if (containerBoxChanged) {
containerBoxChanged = false;
setTimeout(() => {
containers.forEach(p => {
p.layout.invalidateRects();
p.onTranslated();
});
}, 10);
}
};
}
function getScrollHandler(container, dragListeningContainers) {
if (container.getOptions().autoScrollEnabled) {
return dragScroller(dragListeningContainers);
} else {
return () => null;
}
}
function fireOnDragStartEnd(isStart) {
containers.forEach(p => {
const fn = isStart ? p.getOptions().onDragStart : p.getOptions().onDragEnd;
if (fn) {
const options = {
isSource: p === draggableInfo.container,
payload: draggableInfo.payload
};
if (p.isDragRelevant(draggableInfo.container, draggableInfo.payload)) {
options.willAcceptDrop = true;
} else {
options.willAcceptDrop = false;
}
fn(options);
}
});
}
function initiateDrag(position, cursor) {
isDragging = true;
const container = containers.filter(p => grabbedElement.parentElement === p.element)[0];
container.setDraggables();
sourceContainer = container;
sourceContainerLockAxis = container.getOptions().lockAxis ? container.getOptions().lockAxis.toLowerCase() : null;
draggableInfo = getDraggableInfo(grabbedElement);
ghostInfo = getGhostElement(
grabbedElement,
{ x: position.clientX, y: position.clientY },
draggableInfo.container,
cursor
);
draggableInfo.position = {
x: position.clientX + ghostInfo.centerDelta.x,
y: position.clientY + ghostInfo.centerDelta.y
};
draggableInfo.mousePosition = {
x: position.clientX,
y: position.clientY
};
Utils.addClass(global.document.body, constants.disbaleTouchActions);
Utils.addClass(global.document.body, constants.noUserSelectClass);
dragListeningContainers = containers.filter(p => p.isDragRelevant(container, draggableInfo.payload));
handleDrag = dragHandler(dragListeningContainers);
if (handleScroll) {
handleScroll({ reset: true });
}
handleScroll = getScrollHandler(container, dragListeningContainers);
dragListeningContainers.forEach(p => p.prepareDrag(p, dragListeningContainers));
fireOnDragStartEnd(true);
handleDrag(draggableInfo);
getGhostParent().appendChild(ghostInfo.ghost);
}
function onMouseMove(event) {
event.preventDefault();
const e = getPointerEvent(event);
if (!draggableInfo) {
initiateDrag(e, Utils.getElementCursor(event.target));
} else {
// just update ghost position && draggableInfo position
if (sourceContainerLockAxis) {
if (sourceContainerLockAxis === 'y') {
ghostInfo.ghost.style.top = `${e.clientY + ghostInfo.positionDelta.top}px`;
draggableInfo.position.y = e.clientY + ghostInfo.centerDelta.y;
draggableInfo.mousePosition.y = e.clientY;
} else if (sourceContainerLockAxis === 'x') {
ghostInfo.ghost.style.left = `${e.clientX + ghostInfo.positionDelta.left}px`;
draggableInfo.position.x = e.clientX + ghostInfo.centerDelta.x;
draggableInfo.mousePosition.x = e.clientX;
}
} else {
ghostInfo.ghost.style.left = `${e.clientX + ghostInfo.positionDelta.left}px`;
ghostInfo.ghost.style.top = `${e.clientY + ghostInfo.positionDelta.top}px`;
draggableInfo.position.x = e.clientX + ghostInfo.centerDelta.x;
draggableInfo.position.y = e.clientY + ghostInfo.centerDelta.y;
draggableInfo.mousePosition.x = e.clientX;
draggableInfo.mousePosition.y = e.clientY;
}
handleDrag(draggableInfo);
}
}
function Mediator() {
listenEvents();
return {
register: function(container) {
containers.push(container);
},
unregister: function(container) {
containers.splice(containers.indexOf(container), 1);
}
};
}
addStyleToHead();
export default Mediator();