@shopify/draggable
Version:
The JavaScript Drag & Drop library your grandparents warned you about.
470 lines (445 loc) • 12.1 kB
JavaScript
import { AbstractPlugin } from '../../../shared/AbstractPlugin/AbstractPlugin.mjs';
import { MirrorCreateEvent, MirrorCreatedEvent, MirrorAttachedEvent, MirrorMoveEvent, MirrorDestroyEvent, MirrorMovedEvent } from './MirrorEvent/MirrorEvent.mjs';
const onDragStart = Symbol('onDragStart');
const onDragMove = Symbol('onDragMove');
const onDragStop = Symbol('onDragStop');
const onMirrorCreated = Symbol('onMirrorCreated');
const onMirrorMove = Symbol('onMirrorMove');
const onScroll = Symbol('onScroll');
const getAppendableContainer = Symbol('getAppendableContainer');
const defaultOptions = {
constrainDimensions: false,
xAxis: true,
yAxis: true,
cursorOffsetX: null,
cursorOffsetY: null,
thresholdX: null,
thresholdY: null
};
class Mirror extends AbstractPlugin {
constructor(draggable) {
super(draggable);
this.options = {
...defaultOptions,
...this.getOptions()
};
this.scrollOffset = {
x: 0,
y: 0
};
this.initialScrollOffset = {
x: window.scrollX,
y: window.scrollY
};
this[onDragStart] = this[onDragStart].bind(this);
this[onDragMove] = this[onDragMove].bind(this);
this[onDragStop] = this[onDragStop].bind(this);
this[onMirrorCreated] = this[onMirrorCreated].bind(this);
this[onMirrorMove] = this[onMirrorMove].bind(this);
this[onScroll] = this[onScroll].bind(this);
}
attach() {
this.draggable.on('drag:start', this[onDragStart]).on('drag:move', this[onDragMove]).on('drag:stop', this[onDragStop]).on('mirror:created', this[onMirrorCreated]).on('mirror:move', this[onMirrorMove]);
}
detach() {
this.draggable.off('drag:start', this[onDragStart]).off('drag:move', this[onDragMove]).off('drag:stop', this[onDragStop]).off('mirror:created', this[onMirrorCreated]).off('mirror:move', this[onMirrorMove]);
}
getOptions() {
return this.draggable.options.mirror || {};
}
[onDragStart](dragEvent) {
if (dragEvent.canceled()) {
return;
}
if ('ontouchstart' in window) {
document.addEventListener('scroll', this[onScroll], true);
}
this.initialScrollOffset = {
x: window.scrollX,
y: window.scrollY
};
const {
source,
originalSource,
sourceContainer,
sensorEvent
} = dragEvent;
this.lastMirrorMovedClient = {
x: sensorEvent.clientX,
y: sensorEvent.clientY
};
const mirrorCreateEvent = new MirrorCreateEvent({
source,
originalSource,
sourceContainer,
sensorEvent,
dragEvent
});
this.draggable.trigger(mirrorCreateEvent);
if (isNativeDragEvent(sensorEvent) || mirrorCreateEvent.canceled()) {
return;
}
const appendableContainer = this[getAppendableContainer](source) || sourceContainer;
this.mirror = source.cloneNode(true);
const mirrorCreatedEvent = new MirrorCreatedEvent({
source,
originalSource,
sourceContainer,
sensorEvent,
dragEvent,
mirror: this.mirror
});
const mirrorAttachedEvent = new MirrorAttachedEvent({
source,
originalSource,
sourceContainer,
sensorEvent,
dragEvent,
mirror: this.mirror
});
this.draggable.trigger(mirrorCreatedEvent);
appendableContainer.appendChild(this.mirror);
this.draggable.trigger(mirrorAttachedEvent);
}
[onDragMove](dragEvent) {
if (!this.mirror || dragEvent.canceled()) {
return;
}
const {
source,
originalSource,
sourceContainer,
sensorEvent
} = dragEvent;
let passedThreshX = true;
let passedThreshY = true;
if (this.options.thresholdX || this.options.thresholdY) {
const {
x: lastX,
y: lastY
} = this.lastMirrorMovedClient;
if (Math.abs(lastX - sensorEvent.clientX) < this.options.thresholdX) {
passedThreshX = false;
} else {
this.lastMirrorMovedClient.x = sensorEvent.clientX;
}
if (Math.abs(lastY - sensorEvent.clientY) < this.options.thresholdY) {
passedThreshY = false;
} else {
this.lastMirrorMovedClient.y = sensorEvent.clientY;
}
if (!passedThreshX && !passedThreshY) {
return;
}
}
const mirrorMoveEvent = new MirrorMoveEvent({
source,
originalSource,
sourceContainer,
sensorEvent,
dragEvent,
mirror: this.mirror,
passedThreshX,
passedThreshY
});
this.draggable.trigger(mirrorMoveEvent);
}
[onDragStop](dragEvent) {
if ('ontouchstart' in window) {
document.removeEventListener('scroll', this[onScroll], true);
}
this.initialScrollOffset = {
x: 0,
y: 0
};
this.scrollOffset = {
x: 0,
y: 0
};
if (!this.mirror) {
return;
}
const {
source,
sourceContainer,
sensorEvent
} = dragEvent;
const mirrorDestroyEvent = new MirrorDestroyEvent({
source,
mirror: this.mirror,
sourceContainer,
sensorEvent,
dragEvent
});
this.draggable.trigger(mirrorDestroyEvent);
if (!mirrorDestroyEvent.canceled()) {
this.mirror.remove();
}
}
[onScroll]() {
this.scrollOffset = {
x: window.scrollX - this.initialScrollOffset.x,
y: window.scrollY - this.initialScrollOffset.y
};
}
[onMirrorCreated]({
mirror,
source,
sensorEvent
}) {
const mirrorClasses = this.draggable.getClassNamesFor('mirror');
const setState = ({
mirrorOffset,
initialX,
initialY,
...args
}) => {
this.mirrorOffset = mirrorOffset;
this.initialX = initialX;
this.initialY = initialY;
this.lastMovedX = initialX;
this.lastMovedY = initialY;
return {
mirrorOffset,
initialX,
initialY,
...args
};
};
mirror.style.display = 'none';
const initialState = {
mirror,
source,
sensorEvent,
mirrorClasses,
scrollOffset: this.scrollOffset,
options: this.options,
passedThreshX: true,
passedThreshY: true
};
return Promise.resolve(initialState)
.then(computeMirrorDimensions).then(calculateMirrorOffset).then(resetMirror).then(addMirrorClasses).then(positionMirror({
initial: true
})).then(removeMirrorID).then(setState);
}
[onMirrorMove](mirrorEvent) {
if (mirrorEvent.canceled()) {
return null;
}
const setState = ({
lastMovedX,
lastMovedY,
...args
}) => {
this.lastMovedX = lastMovedX;
this.lastMovedY = lastMovedY;
return {
lastMovedX,
lastMovedY,
...args
};
};
const triggerMoved = args => {
const mirrorMovedEvent = new MirrorMovedEvent({
source: mirrorEvent.source,
originalSource: mirrorEvent.originalSource,
sourceContainer: mirrorEvent.sourceContainer,
sensorEvent: mirrorEvent.sensorEvent,
dragEvent: mirrorEvent.dragEvent,
mirror: this.mirror,
passedThreshX: mirrorEvent.passedThreshX,
passedThreshY: mirrorEvent.passedThreshY
});
this.draggable.trigger(mirrorMovedEvent);
return args;
};
const initialState = {
mirror: mirrorEvent.mirror,
sensorEvent: mirrorEvent.sensorEvent,
mirrorOffset: this.mirrorOffset,
options: this.options,
initialX: this.initialX,
initialY: this.initialY,
scrollOffset: this.scrollOffset,
passedThreshX: mirrorEvent.passedThreshX,
passedThreshY: mirrorEvent.passedThreshY,
lastMovedX: this.lastMovedX,
lastMovedY: this.lastMovedY
};
return Promise.resolve(initialState).then(positionMirror({
raf: true
})).then(setState).then(triggerMoved);
}
[getAppendableContainer](source) {
const appendTo = this.options.appendTo;
if (typeof appendTo === 'string') {
return document.querySelector(appendTo);
} else if (appendTo instanceof HTMLElement) {
return appendTo;
} else if (typeof appendTo === 'function') {
return appendTo(source);
} else {
return source.parentNode;
}
}
}
function computeMirrorDimensions({
source,
...args
}) {
return withPromise(resolve => {
const sourceRect = source.getBoundingClientRect();
resolve({
source,
sourceRect,
...args
});
});
}
function calculateMirrorOffset({
sensorEvent,
sourceRect,
options,
...args
}) {
return withPromise(resolve => {
const top = options.cursorOffsetY === null ? sensorEvent.clientY - sourceRect.top : options.cursorOffsetY;
const left = options.cursorOffsetX === null ? sensorEvent.clientX - sourceRect.left : options.cursorOffsetX;
const mirrorOffset = {
top,
left
};
resolve({
sensorEvent,
sourceRect,
mirrorOffset,
options,
...args
});
});
}
function resetMirror({
mirror,
source,
options,
...args
}) {
return withPromise(resolve => {
let offsetHeight;
let offsetWidth;
if (options.constrainDimensions) {
const computedSourceStyles = getComputedStyle(source);
offsetHeight = computedSourceStyles.getPropertyValue('height');
offsetWidth = computedSourceStyles.getPropertyValue('width');
}
mirror.style.display = null;
mirror.style.position = 'fixed';
mirror.style.pointerEvents = 'none';
mirror.style.top = 0;
mirror.style.left = 0;
mirror.style.margin = 0;
if (options.constrainDimensions) {
mirror.style.height = offsetHeight;
mirror.style.width = offsetWidth;
}
resolve({
mirror,
source,
options,
...args
});
});
}
function addMirrorClasses({
mirror,
mirrorClasses,
...args
}) {
return withPromise(resolve => {
mirror.classList.add(...mirrorClasses);
resolve({
mirror,
mirrorClasses,
...args
});
});
}
function removeMirrorID({
mirror,
...args
}) {
return withPromise(resolve => {
mirror.removeAttribute('id');
delete mirror.id;
resolve({
mirror,
...args
});
});
}
function positionMirror({
withFrame = false,
initial = false
} = {}) {
return ({
mirror,
sensorEvent,
mirrorOffset,
initialY,
initialX,
scrollOffset,
options,
passedThreshX,
passedThreshY,
lastMovedX,
lastMovedY,
...args
}) => {
return withPromise(resolve => {
const result = {
mirror,
sensorEvent,
mirrorOffset,
options,
...args
};
if (mirrorOffset) {
const x = passedThreshX ? Math.round((sensorEvent.clientX - mirrorOffset.left - scrollOffset.x) / (options.thresholdX || 1)) * (options.thresholdX || 1) : Math.round(lastMovedX);
const y = passedThreshY ? Math.round((sensorEvent.clientY - mirrorOffset.top - scrollOffset.y) / (options.thresholdY || 1)) * (options.thresholdY || 1) : Math.round(lastMovedY);
if (options.xAxis && options.yAxis || initial) {
mirror.style.transform = `translate3d(${x}px, ${y}px, 0)`;
} else if (options.xAxis && !options.yAxis) {
mirror.style.transform = `translate3d(${x}px, ${initialY}px, 0)`;
} else if (options.yAxis && !options.xAxis) {
mirror.style.transform = `translate3d(${initialX}px, ${y}px, 0)`;
}
if (initial) {
result.initialX = x;
result.initialY = y;
}
result.lastMovedX = x;
result.lastMovedY = y;
}
resolve(result);
}, {
frame: withFrame
});
};
}
function withPromise(callback, {
raf = false
} = {}) {
return new Promise((resolve, reject) => {
if (raf) {
requestAnimationFrame(() => {
callback(resolve, reject);
});
} else {
callback(resolve, reject);
}
});
}
function isNativeDragEvent(sensorEvent) {
return /^drag/.test(sensorEvent.originalEvent.type);
}
export { Mirror as default, defaultOptions, getAppendableContainer, onDragMove, onDragStart, onDragStop, onMirrorCreated, onMirrorMove, onScroll };