draggerjs
Version:
draggable made easy for you.
275 lines (252 loc) • 8.96 kB
JavaScript
export const w = window;
export const d = document;
export const root = d.documentElement;
export const defaultOptions = {
allowBoundContainer: true,
allowExactTargetDraggable: false,
autoscroll: false,
autoscrollSensitivity: 20,
eventListenerOption: false,
allowPointerEvent: true,
allowWindowBound: false
};
export const requestAnimationFrame = w.requestAnimationFrame || w.webkitRequestAnimationFrame || w.mozRequestAnimationFrame;
export const cancelAnimationFrame = w.cancelAnimationFrame || w.webkitCancelAnimationFrame || w.mozCancelAnimationFrame;
export const qs = selector => d.querySelector(selector);
export const css = (node, style) => {
Object.keys(style).forEach(key => {
if (key in node.style) {
node.style[key] = style[key];
}
});
};
export const is = {
str: a => typeof a === 'string',
fnc: a => typeof a === 'function',
node: a => a && a.nodeType === Node.ELEMENT_NODE
};
export const on = (node, event, callback, option) => {
event.split(/\s{1,10}/).forEach(eventType => {
node.addEventListener(eventType, callback, option);
});
};
export const off = (node, event, callback, option) => {
event.split(/\s{1,10}/).forEach(eventType => {
node.removeEventListener(eventType, callback, option);
});
};
export const validateType = (_data, _types, _error) => {
if (!(_types.includes(typeof _data) || _types.includes('node') && is.node(_data))) throw new Error(`${_error} must contain the following data types (${_types.map(each => each.toUpperCase()).join(', ')})`);
};
const eventSupported = ['dragstart', 'dragmove', 'dragend', 'dragenter', 'dragover', 'dragexit', 'drop'];
export const validateEvent = (type, eventType) => {
if (!eventSupported.includes(eventType)) throw new SyntaxError(`Dragger.${type} event type "${eventType}" is not recognize. supported events are ${eventSupported.join(', ')}`);
};
export class DraggerEvent {
constructor(event, options) {
Object.assign(this, options);
this.originalEvent = event;
}
preventDefault() {
this.originalEvent.preventDefault();
}
stopPropagation() {
this.originalEvent.stopPropagation();
}
}
export const applyCoordinate = (target, {
x,
y,
axis
}) => {
const position = {
position: 'absolute'
};
if (axis === 'x' || !axis) {
position.left = `${x}px`;
}
if (axis === 'y' || !axis) {
position.top = `${y}px`;
}
css(target, position);
};
export const getInitialPosition = (event, {
target,
container,
isDraggable,
allowWindowBound
}) => {
const {
clientX,
clientY
} = event.touches && event.touches[0] || event;
const {
left = 0,
top = 0,
width = 0,
height = 0
} = isDraggable ? target.getBoundingClientRect() : {};
return {
left: clientX - left,
top: clientY - top,
width: width,
height: height,
containerHeight: allowWindowBound ? window.innerHeight : container.scrollHeight,
containerWidth: allowWindowBound ? window.innerWidth : container.scrollWidth
};
};
export const getCoordinates = (event, {
container,
isDraggable,
initialPosition,
allowBoundContainer,
allowWindowBound
}) => {
let {
clientX,
clientY
} = event.touches && event.touches[0] || event;
const bound = allowWindowBound ? {
top: 0,
left: 0
} : container.getBoundingClientRect();
const containerScroll = {
x: allowWindowBound ? 0 : container.scrollLeft,
y: allowWindowBound ? 0 : container.scrollTop
};
let x = containerScroll.x + (clientX - bound.left) - (isDraggable ? initialPosition.left : 0);
let y = containerScroll.y + (clientY - bound.top) - (isDraggable ? initialPosition.top : 0);
if (allowBoundContainer) {
x = getBoundX(x, initialPosition);
y = getBoundY(y, initialPosition);
}
return {
clientX,
clientY,
x,
y
};
};
export const getDroppable = ({
target,
droppableQuery,
point: {
x,
y
}
}) => {
target.hidden = true;
const droppableTarget = d.elementFromPoint(x, y);
target.hidden = false;
const srcDroppable = droppableTarget && droppableTarget.closest(droppableQuery);
const isOverDroppable = srcDroppable && srcDroppable.matches(droppableQuery);
return {
droppableTarget,
srcDroppable,
isOverDroppable
};
};
const getBoundY = (y, initialPosition) => {
return Math.max(0, Math.min(initialPosition.containerHeight - initialPosition.height, y));
};
const getBoundX = (x, initialPosition) => {
return Math.max(0, Math.min(initialPosition.containerWidth - initialPosition.width, x));
};
const getAncestorsScrolledTree = container => {
const ancestors = [];
for (let elem = container; elem && elem !== d; elem = elem.parentNode) {
if (elem.scrollHeight > elem.clientHeight || elem.scrollWidth > elem.clientWidth) {
ancestors.push(elem);
}
}
return ancestors;
};
const getPower = (sensitivity, cut, filler) => Math.min(sensitivity, Math.min(cut, filler > 0 ? filler : cut));
export const autoScrollAlgorithm = (startDraggingObservables, {
y,
x,
clientY,
clientX,
target,
container,
sensitivity,
initialPosition
}) => {
if (startDraggingObservables.animationFrame) {
cancelAnimationFrame(startDraggingObservables.animationFrame);
}
const handleScroll = () => {
const bound = container.getBoundingClientRect();
const requestAgain = getAncestorsScrolledTree(target).reduce((result, ancestorEL, _, reduceArray) => {
const {
top,
left
} = ancestorEL.nodeName !== "HTML" ? ancestorEL.getBoundingClientRect() : {
top: 0,
left: 0
};
let {
scrollTop,
scrollLeft,
scrollHeight,
scrollWidth,
clientHeight,
clientWidth
} = ancestorEL;
const position = {};
const isContainer = ancestorEL === container;
const topBounds = top + sensitivity;
const bottomBounds = top + clientHeight - sensitivity;
const leftBounds = left + sensitivity;
const rightBounds = left + clientWidth - sensitivity;
const topSensitivityCut = topBounds - clientY;
const bottomSensitivityCut = clientY - bottomBounds;
const leftSensitivityCut = leftBounds - clientX;
const rightSensitivityCut = clientX - rightBounds;
const containerY = y + initialPosition.top;
const containerX = x + initialPosition.left;
const containerTop = bound.top;
const containerExceededTop = bound.top + bound.height;
const containerLeft = bound.left;
const containerExceededLeft = bound.left + bound.width;
const isEdgeTopScreen = clientY < topBounds && scrollTop > 0 && (isContainer ? containerY > 0 : clientY > containerTop);
const isEdgeBottomScreen = clientY > bottomBounds && scrollTop < scrollHeight - clientHeight && (isContainer ? containerY < scrollHeight : clientY < containerExceededTop);
const isEdgeLeftScreen = clientX < leftBounds && scrollLeft > 0 && (isContainer ? containerX > 0 : clientX > containerLeft);
const isEdgeRightScreen = clientX > rightBounds && scrollLeft < scrollWidth - clientWidth && (isContainer ? containerX < scrollWidth : clientX < containerExceededLeft);
const hasEdgeScreen = isEdgeTopScreen || isEdgeBottomScreen || isEdgeLeftScreen || isEdgeRightScreen;
if (isEdgeTopScreen) {
const power = getPower(sensitivity, topSensitivityCut, isContainer ? containerY - sensitivity : clientY - containerTop);
scrollTop -= power;
y -= power;
position.top = `${getBoundY(y, initialPosition)}px`;
} else if (isEdgeBottomScreen) {
const power = getPower(sensitivity, bottomSensitivityCut, isContainer ? scrollHeight - containerY : containerExceededTop - clientY);
scrollTop += power;
y += power;
position.top = `${getBoundY(y, initialPosition)}px`;
}
if (isEdgeLeftScreen) {
const power = getPower(sensitivity, leftSensitivityCut, isContainer ? containerX - sensitivity : clientX - containerLeft);
scrollLeft -= power;
x -= power;
position.left = `${getBoundX(x, initialPosition)}px`;
} else if (isEdgeRightScreen) {
const power = getPower(sensitivity, rightSensitivityCut, isContainer ? scrollWidth - containerX : containerExceededLeft - clientX);
scrollLeft += power;
x += power;
position.left = `${getBoundX(x, initialPosition)}px`;
}
if (hasEdgeScreen) {
ancestorEL.scrollTop = scrollTop;
ancestorEL.scrollLeft = scrollLeft;
if (!ancestorEL.matches('[data-draggerjs="not-relative"]')) css(target, position); // stop the reduce iteration
reduceArray.splice(1);
}
return result || hasEdgeScreen;
}, false);
if (requestAgain) {
startDraggingObservables.animationFrame = requestAnimationFrame(handleScroll);
}
};
startDraggingObservables.animationFrame = requestAnimationFrame(handleScroll);
};