UNPKG

@shopify/draggable

Version:

The JavaScript Drag & Drop library your grandparents warned you about.

470 lines (445 loc) 12.1 kB
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 };