UNPKG

@vdnd/v3

Version:

Easy used vue drag and drop component library.

1,707 lines (1,640 loc) 91.5 kB
'use strict'; var vue = require('vue'); class Plugin { constructor(dnd) { this.dnd = dnd; } } /** * All events fired by draggable inherit this class. You can call `cancel()` to * cancel a specific event or you can check if an event has been canceled by * calling `canceled()`. */ class AbstractEvent { static type = 'event'; /** * Event cancelable */ static cancelable = false; /** * Private instance variable to track canceled state */ _canceled = false; /** * Read-only cancelable */ get cancelable() { return this.constructor.cancelable; } get type() { return this.constructor.type; } /** * Cancels the event instance */ cancel() { if (this.cancelable) { this._canceled = true; } } /** * Check if event has been canceled */ canceled() { return this._canceled; } } class BaseMirrorEvent extends AbstractEvent { constructor(source, mirror, container, dndEvent) { super(); this.source = source; this.mirror = mirror; this.container = container; this.dndEvent = dndEvent; } } class MirrorCreatedEvent extends BaseMirrorEvent { static type = 'mirror:created'; } class MirrorAttachedEvent extends BaseMirrorEvent { static type = 'mirror:attached'; } class MirrorMoveEvent extends BaseMirrorEvent { static type = 'mirror:move'; } class MirrorDetachedEvent extends BaseMirrorEvent { static type = 'mirror:detached'; } class Emitter { callbacks = {}; onceCallbacks = new Set(); /** * Registers callback by event name */ on(type, callback) { if (!this.callbacks[type]) { this.callbacks[type] = []; } this.callbacks[type].push(callback); return this; } /** * Registers disposable callback by event name */ once(type, callback) { if (!this.callbacks[type]) { this.callbacks[type] = []; } this.callbacks[type].push(callback); this.onceCallbacks.add(callback); return this; } /** * Unregisters callback by event name */ off(type, callback) { if (!this.callbacks[type]) { return this; } const callbacks = this.callbacks[type]; const index = callbacks.indexOf(callback); if (index >= 0) { callbacks.splice(index, 1); } this.onceCallbacks.delete(callback); return this; } /** * Emits event callbacks by event object */ emit(type, payload) { if (!this.callbacks[type]) { return this; } const callbacks = [...this.callbacks[type]]; const caughtErrors = []; for (let i = 0; i <= callbacks.length - 1; i++) { const callback = callbacks[i]; try { callback(payload); } catch (error) { caughtErrors.push(error); } if (this.onceCallbacks.has(callback)) { this.off(type, callback); } } if (caughtErrors.length) { console.error(`[vdnd error]: caught errors while emitting '${type}':`); caughtErrors.forEach(error => console.error(error)); } return this; } destroy() { this.callbacks = {}; this.onceCallbacks.clear(); } } const emptyFn = () => {}; function ensureArray(value) { return Array.isArray(value) ? value : [value]; } function isDef(value) { return value !== null && value !== undefined; } function isPrimitive(v) { return typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || typeof v === 'symbol' || typeof v === 'bigint'; } function $typeof(v) { if (v === null) return 'null'; if (typeof v === 'undefined') return 'undefined'; if (typeof v === 'string') return 'string'; if (typeof v === 'number') return 'number'; if (typeof v === 'boolean') return 'boolean'; if (typeof v === 'symbol') return 'symbol'; if (typeof v === 'bigint') return 'bigint'; const raw = Object.prototype.toString.call(v); return raw.slice(8, raw.length - 1); } const defaultMirrorCreator = params => params.source.cloneNode(true); const defaultMirrorAppendTo = params => { return params.source.parentNode || document.body; }; const defaultOptions = { className: 'dnd-mirror', create: defaultMirrorCreator, appendTo: defaultMirrorAppendTo, constrainDimensions: false }; /** * Mirror plugin which controls the mirror positioning while dragging */ class Mirror extends Plugin { /** * Scroll offset for touch devices because the mirror is positioned fixed */ scrollOffset = { x: 0, y: 0 }; /** * Initial scroll offset for touch devices because the mirror is positioned fixed */ initialScrollOffset = { x: window.scrollX, y: window.scrollY }; lastMovedX = 0; lastMovedY = 0; mirrorOffset = { top: 0, left: 0 }; initialX = 0; initialY = 0; mirror = null; constructor(dnd) { super(dnd); this.options = { ...defaultOptions, ...this.getOptions() }; } attach() { this.dnd.on('drag:start', this.onDragStart).on('drag:move', this.onDragMove).on('drag:end', this.onDragEnd).on('mirror:created', this.onMirrorCreated).on('mirror:move', this.onMirrorMove); } detach() { this.dnd.off('drag:start', this.onDragStart).off('drag:move', this.onDragMove).off('drag:end', this.onDragEnd).off('mirror:created', this.onMirrorCreated).off('mirror:move', this.onMirrorMove); } /** * Returns options passed through draggable */ getOptions() { return this.dnd.options.mirror || {}; } onDragStart = async dragEvent => { if ('ontouchstart' in window) { document.addEventListener('scroll', this.onScroll, true); } this.initialScrollOffset = { x: window.scrollX, y: window.scrollY }; const { source, container } = dragEvent; const appendableContainer = this.getAppendableContainer({ source, event: dragEvent }) || document.body; const createMirror = this.options.create; this.mirror = createMirror({ source, event: dragEvent }); const mirrorCreatedEvent = new MirrorCreatedEvent(source, this.mirror, container, dragEvent); this.dnd.emit('mirror:created', mirrorCreatedEvent); const mirrorAttachedEvent = new MirrorAttachedEvent(source, this.mirror, container, dragEvent); appendableContainer.appendChild(this.mirror); this.dnd.emit('mirror:attached', mirrorAttachedEvent); }; onDragMove = dragEvent => { if (!this.mirror) { return; } const { source, container } = dragEvent; const mirrorMoveEvent = new MirrorMoveEvent(source, this.mirror, container, dragEvent); this.dnd.emit('mirror:move', mirrorMoveEvent); }; onDragEnd = 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, container } = dragEvent; const mirrorDetachedEvent = new MirrorDetachedEvent(source, this.mirror, container, dragEvent); this.mirror.remove(); this.dnd.emit('mirror:detached', mirrorDetachedEvent); }; onScroll = () => { this.scrollOffset = { x: window.scrollX - this.initialScrollOffset.x, y: window.scrollY - this.initialScrollOffset.y }; }; onMirrorCreated = event => { const { mirror, source, dndEvent: dragEvent } = event; const mirrorClasses = ensureArray(this.options.className || ''); 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 }; }; if ('style' in mirror && typeof mirror.style === 'object') { mirror.style.display = 'none'; } const initialState = { mirror, source, dragEvent, mirrorClasses, scrollOffset: this.scrollOffset, options: this.options }; Promise.resolve(initialState) // Fix reflow here .then(computeMirrorDimensions).then(calculateMirrorOffset).then(resetMirror).then(addMirrorClasses).then(positionMirror({ initial: true })).then(removeMirrorID).then(setState); }; onMirrorMove = event => { const setState = ({ lastMovedX, lastMovedY, ...args }) => { this.lastMovedX = lastMovedX; this.lastMovedY = lastMovedY; return { lastMovedX, lastMovedY, ...args }; }; const initialState = { mirror: event.mirror, dragEvent: event.dndEvent, mirrorOffset: this.mirrorOffset, options: this.options, initialX: this.initialX, initialY: this.initialY, scrollOffset: this.scrollOffset, lastMovedX: this.lastMovedX, lastMovedY: this.lastMovedY }; return Promise.resolve(initialState).then(positionMirror({ raf: true })).then(setState); }; /** * Returns appendable container for mirror based on the appendTo option */ getAppendableContainer({ source, event }) { 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, event }); } else { return source.parentNode; } } } /** * Computes mirror dimensions based on the source element * Adds sourceRect to state * @param {Object} state * @param {HTMLElement} state.source */ function computeMirrorDimensions(state) { const { source, ...args } = state; return withPromise(resolve => { const sourceRect = source.getBoundingClientRect(); resolve({ source, sourceRect, ...args }); }); } /** * Calculates mirror offset * Adds mirrorOffset to state */ function calculateMirrorOffset({ dragEvent, sourceRect, options, ...args }) { return withPromise(resolve => { const top = typeof options.cursorOffsetY !== 'number' ? dragEvent.clientY - sourceRect.top : options.cursorOffsetY; const left = typeof options.cursorOffsetX !== 'number' ? dragEvent.clientX - sourceRect.left : options.cursorOffsetX; const mirrorOffset = { top, left }; resolve({ dragEvent, sourceRect, mirrorOffset, options, ...args }); }); } /** * Applys mirror styles * @param {Object} state * @param {HTMLElement} state.mirror * @param {HTMLElement} state.source * @param {Object} state.options */ 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 }); }); } /** * Applys mirror class on mirror element * @param {Object} state * @param {HTMLElement} state.mirror * @param {String[]} state.mirrorClasses */ function addMirrorClasses({ mirror, mirrorClasses, ...args }) { return withPromise(resolve => { mirror.classList.add(...mirrorClasses); resolve({ mirror, mirrorClasses, ...args }); }); } /** * Removes source ID from cloned mirror element * @param {Object} state * @param {HTMLElement} state.mirror */ function removeMirrorID({ mirror, source, ...args }) { return withPromise(resolve => { if (source.getAttribute('id') === mirror.getAttribute('id')) { mirror.removeAttribute('id'); delete mirror.id; } resolve({ mirror, ...args }); }); } /** * Positions mirror with translate3d */ function positionMirror(state) { const { raf = false, initial = false } = state; return ({ mirror, dragEvent, mirrorOffset, initialY, initialX, scrollOffset, options, lastMovedX, lastMovedY, ...args }) => { return withPromise(resolve => { const result = { mirror, dragEvent, mirrorOffset, options, ...args }; if (mirrorOffset) { const x = Math.round(dragEvent.clientX - mirrorOffset.left - scrollOffset.x); const y = Math.round(dragEvent.clientY - mirrorOffset.top - scrollOffset.y); mirror.style.transform = `translate3d(${x}px, ${y}px, 0)`; if (initial) { // @ts-expect-error result.initialX = x; // @ts-expect-error result.initialY = y; } // @ts-expect-error result.lastMovedX = x; // @ts-expect-error result.lastMovedY = y; } resolve(result); }, { raf }); }; } /** * Wraps functions in promise with potential animation frame option */ function withPromise(callback, { raf = false } = {}) { return new Promise((resolve, reject) => { if (raf) { requestAnimationFrame(() => { callback(resolve, reject); }); } else { callback(resolve, reject); } }); } /** * Get the closest parent element of a given element that matches the given matcher */ function closest(element, matcher) { if (matcher instanceof Node) { return closest(element, [matcher]); } if (matcher instanceof NodeList) { return closest(element, Array.from(matcher)); } function isExpected(currentElement) { if (isClassName(matcher)) { return currentElement.classList.contains(matcher); } if (isCallbackMatcher(matcher)) { return matcher(currentElement); } if (isNodes(matcher)) { return matcher.includes(currentElement); } } let current = element; do { if (isExpected(current)) { return current; } current = current?.parentElement || null; } while (current != null && current !== document.body); return null; } function isClassName(matcher) { return typeof matcher === 'string'; } function isNodes(matcher) { return Array.isArray(matcher); } function isCallbackMatcher(matcher) { return typeof matcher === 'function'; } /** * Returns all handles where 'options.isHandle' is true in a source node. */ function findHandles(root, options) { const { isHandle, isSource } = options; const handles = []; for (let i = 0, element; i < root.children.length; i++) { element = root.children.item(i); if (!(element instanceof Element) || isSource(element)) { continue; } if (isHandle(element)) { handles.push(element); continue; } handles.push(...findHandles(element, options)); } return handles; } /** * Simulator picks up native browser events and dictates drag operations. */ class Simulator { startEvent = null; source = null; constructor(env, container, options, onDragStrat, onDragMove, onDragEnd) { this.env = env; this.container = container; this.options = options; this.onDragStrat = onDragStrat; this.onDragMove = onDragMove; this.onDragEnd = onDragEnd; } getSource(startEvent) { if (!(startEvent.target instanceof Element) || !closest(startEvent.target, this.container)) return null; const source = closest(startEvent.target, this.options.source); if (!source) return null; if (this.options.handle) { const handles = findHandles(source, { isSource: target => target.classList.contains(this.options.source), isHandle: target => target.classList.contains(this.options.handle) }); if (handles.length > 0 && handles.every(handle => !closest(startEvent.target, handle))) { return null; } } return source; } get dragging() { return this.source !== null; } /** Stops moving */ /** * Attaches simulators event listeners to the DOM */ /** * Detaches simulators event listeners to the DOM */ } class BaseSimulatorEvent extends AbstractEvent { constructor(target, source, container, clientX, clientY, originalEvent) { super(); this.target = target; this.source = source; this.container = container; this.clientX = clientX; this.clientY = clientY; this.originalEvent = originalEvent; } } class DragStartSimulatorEvent extends BaseSimulatorEvent { static type = 'drag:start'; static cancelable = true; } class DragMoveSimulatorEvent extends BaseSimulatorEvent { static type = 'drag:move'; } class DragEndSimulatorEvent extends BaseSimulatorEvent { static type = 'drag:end'; } /** * This simulator picks up native browser mouse events and dictates drag operations */ class MouseSimulator extends Simulator { attach() { this.env.addEventListener('mousedown', this.onMouseDown, true); return this; } detach() { this.env.removeEventListener('mousedown', this.onMouseDown, true); this.cancel(); return this; } onMouseDown = event => { if (event.button !== 0 || event.altKey || this.dragging) { return; } const source = this.getSource(event); if (source) { this.source = source; this.startEvent = event; this.env.addEventListener('mouseup', this.onMouseUp); this.env.addEventListener('dragstart', preventNativeDragStart); this.env.addEventListener('mousemove', this.onFirstMouseMove); } }; startDrag() { const dragStartEvent = new DragStartSimulatorEvent(this.startEvent.target, this.source, this.container, this.startEvent.clientX, this.startEvent.clientY, this.startEvent); this.onDragStrat(dragStartEvent); if (!dragStartEvent.canceled()) { this.env.addEventListener('keydown', this.onKeyDown); this.env.addEventListener('contextmenu', this.onContextMenuWhileDragging, true); this.env.addEventListener('mousemove', this.onMouseMove); } else { this.cancel(); } } onKeyDown = event => { if (this.dragging && event.target && !event.isComposing && event.code?.toLowerCase() === 'escape') { const { source, container } = this; this.cancel(); this.onDragEnd(new DragEndSimulatorEvent(event.target, source, container, 0, 0, event)); } }; onFirstMouseMove = () => { this.env.removeEventListener('mousemove', this.onFirstMouseMove); this.startDrag(); }; onMouseMove = event => { if (!(event.target instanceof Element)) return; const dragMoveEvent = new DragMoveSimulatorEvent(event.target, this.source, this.container, event.clientX, event.clientY, event); this.onDragMove(dragMoveEvent); }; onMouseUp = event => { if (event.button !== 0 || !(event.target instanceof Element)) { return; } const { source } = this; this.cancel(); if (source) { const dragEndEvent = new DragEndSimulatorEvent(event.target, source, this.container, event.clientX, event.clientY, event); this.onDragEnd(dragEndEvent); } }; cancel() { this.source = null; this.startEvent = null; this.env.removeEventListener('mouseup', this.onMouseUp); this.env.removeEventListener('dragstart', preventNativeDragStart); this.env.removeEventListener('mousemove', this.onFirstMouseMove); this.env.removeEventListener('keydown', this.onKeyDown); this.env.removeEventListener('contextmenu', this.onContextMenuWhileDragging, true); this.env.removeEventListener('mousemove', this.onMouseMove); } onContextMenuWhileDragging = event => { event.preventDefault(); }; } function preventNativeDragStart(event) { event.preventDefault(); } function touchCoords(event) { const { touches, changedTouches } = event; return touches && touches[0] || changedTouches && changedTouches[0]; } /** * Prevents scrolling when set to true */ let preventScrolling = false; // WebKit requires cancelable `touchmove` events to be added as early as possible window.addEventListener('touchmove', event => { if (!preventScrolling) { return; } // Prevent scrolling event.preventDefault(); }, { passive: false }); /** * This simulator picks up native browser touch events and dictates drag operations */ class TouchSimulator extends Simulator { onTouchStartAt = 0; attach() { this.env.addEventListener('touchstart', this.onTouchStart); return this; } detach() { this.env.removeEventListener('touchstart', this.onTouchStart); this.cancel(); return this; } get delay() { return this.options.delay ?? 100; } /** * Touch start handler */ onTouchStart = event => { if (this.dragging || !(event.target instanceof Element)) return; const source = this.getSource(event); if (source) { this.source = source; this.startEvent = event; this.onTouchStartAt = Date.now(); this.env.addEventListener('touchend', this.onTouchEnd); this.env.addEventListener('touchcancel', this.onTouchEnd); this.env.addEventListener('touchmove', this.checkElapsedTime); this.env.addEventListener('contextmenu', onContextMenu); } }; /** * Start the drag */ startDrag = () => { const touch = touchCoords(this.startEvent); const dragStartEvent = new DragStartSimulatorEvent(this.startEvent.target, this.source, this.container, touch.pageX, touch.pageY, this.startEvent); this.onDragStrat(dragStartEvent); if (dragStartEvent.canceled()) { this.cancel(); } else { preventScrolling = true; this.env.addEventListener('touchmove', this.onTouchMove); } }; checkElapsedTime = () => { this.env.removeEventListener('touchmove', this.checkElapsedTime); const timeElapsed = Date.now() - this.onTouchStartAt; if (timeElapsed >= this.delay) { this.startDrag(); } }; onTouchMove = event => { const { pageX, pageY } = touchCoords(event); const target = document.elementFromPoint(pageX - window.scrollX, pageY - window.scrollY); if (!target) return; const dragMoveEvent = new DragMoveSimulatorEvent(target, this.source, this.container, pageX, pageY, event); this.onDragMove(dragMoveEvent); }; onTouchEnd = event => { const { source } = this; this.cancel(); if (source) { event.preventDefault(); const { pageX, pageY } = touchCoords(event); const target = document.elementFromPoint(pageX - window.scrollX, pageY - window.scrollY); if (!target) return; const dragEndEvent = new DragEndSimulatorEvent(target, source, this.container, pageX, pageY, event); this.onDragEnd(dragEndEvent); } }; cancel() { preventScrolling = false; this.source = null; this.env.removeEventListener('touchend', this.onTouchEnd); this.env.removeEventListener('touchcancel', this.onTouchEnd); this.env.removeEventListener('touchmove', this.checkElapsedTime); this.env.removeEventListener('contextmenu', onContextMenu); this.env.removeEventListener('touchmove', this.onTouchMove); } } function onContextMenu(event) { event.preventDefault(); event.stopPropagation(); } class AbstractDnd extends Emitter { source = null; currentOver = null; constructor(container, /** @readonly */ options) { super(); this.container = container; this.options = options; } isDraggable(source) { return this.options.isDraggable?.(source) ?? true; } isDroppable(dropzone) { return this.options.isDroppable?.(dropzone) ?? true; } get dragging() { return this.source !== null; } isDragging(target) { if (!this.dragging) return false; return target ? this.source === target : this.dragging; } } class DragEvent extends AbstractEvent { static type = 'drag'; constructor(source, over, container, originalEvent) { super(); this.source = source; this.over = over; this.container = container; this.originalEvent = originalEvent; } } class DragStartEvent extends AbstractEvent { static type = 'drag:start'; constructor(source, container, originalEvent, clientX, clientY) { super(); this.source = source; this.container = container; this.originalEvent = originalEvent; this.clientX = clientX; this.clientY = clientY; } } class DragPreventEvent extends AbstractEvent { static type = 'drag:prevent'; constructor(source, container, originalEvent) { super(); this.source = source; this.container = container; this.originalEvent = originalEvent; } } class DragEnterEvent extends AbstractEvent { static type = 'drag:enter'; constructor(source, enter, container, originalEvent) { super(); this.source = source; this.enter = enter; this.container = container; this.originalEvent = originalEvent; } } /** * Only for SimulatedDnd */ class DragMoveEvent extends AbstractEvent { static type = 'drag:move'; constructor(source, over, container, originalEvent, clientX, clientY) { super(); this.source = source; this.over = over; this.container = container; this.originalEvent = originalEvent; this.clientX = clientX; this.clientY = clientY; } } class DragOverEvent extends AbstractEvent { static type = 'drag:over'; constructor(source, over, container, originalEvent) { super(); this.source = source; this.over = over; this.container = container; this.originalEvent = originalEvent; } } class DragLeaveEvent extends AbstractEvent { static type = 'drag:leave'; constructor(source, leave, container, originalEvent) { super(); this.source = source; this.leave = leave; this.container = container; this.originalEvent = originalEvent; } } class DropEvent extends AbstractEvent { static type = 'drop'; constructor(source, dropzone, container, originalEvent) { super(); this.source = source; this.dropzone = dropzone; this.container = container; this.originalEvent = originalEvent; } } class DragEndEvent extends AbstractEvent { static type = 'drag:end'; constructor(source, over, container, originalEvent) { super(); this.source = source; this.over = over; this.container = container; this.originalEvent = originalEvent; } } class ListenerProxy { destoryed = false; constructor(type, listener, options) { this.type = type; this.listener = listener; this.options = options; if (this.options.signal) { if (this.options.signal.aborted) { this.destroy(); } else { this.options.signal.addEventListener('abort', this.onSignalAborted); } } } is(listenr) { return this.listener === listenr; } call(e) { if (this.destoryed || e.type !== this.type) return; try { if (this.options.passive) { e.preventDefault = () => {}; } this.listener(e); } finally { if (this.options.once) { this.destroy(); } } } destroy() { this.destoryed = true; this.options.signal?.removeEventListener('abort', this.onSignalAborted); } onSignalAborted = () => { this.destroy(); }; } function findListenerProxy(proxys, listener) { for (const proxy of proxys) { if (proxy.is(listener)) { return proxy; } } return null; } class SpecificEventSuppressor { scopeToAliveListeners = new Map(); constructor(type, behavior, isEnabled, isTrustedEvent) { this.type = type; this.behavior = behavior; this.isEnabled = isEnabled; this.isTrustedEvent = isTrustedEvent; document.addEventListener(type, this.suppress, { capture: true }); } findAliveListener(scope, listener) { const aliveListeners = this.scopeToAliveListeners.get(scope); if (!aliveListeners) return null; return findListenerProxy(aliveListeners.values(), listener); } suppress = e => { // we do not suppress the event dispatched by 'dispatchEvent' if (!this.isEnabled() || !this.isTrustedEvent(e)) return; if (this.behavior.preventDefault) { e.preventDefault(); } if (this.behavior.stopPropagation) { e.stopPropagation(); } if (this.behavior.stopImmediatePropagation) { e.stopImmediatePropagation(); } for (const aliveListeners of this.scopeToAliveListeners.values()) { for (const proxy of aliveListeners) { if (proxy.options.capture) proxy.call(e); } for (const proxy of aliveListeners) { if (!proxy.options.capture) proxy.call(e); } } }; addAliveEventListener(scope, type, listener, options = {}) { if (this.type !== type) return; const proxy = new ListenerProxy(type, listener, options); proxy.call = proxy.call.bind(proxy); document.addEventListener(type, proxy.call, options); let aliveListeners = this.scopeToAliveListeners.get(scope); if (!aliveListeners) { aliveListeners = new Set(); this.scopeToAliveListeners.set(scope, aliveListeners); } aliveListeners.add(proxy); } removeAliveEventListener(scope, type, listener, options = {}) { if (this.type !== type) return; const proxy = this.findAliveListener(scope, listener); if (proxy) { this.scopeToAliveListeners.get(scope).delete(proxy); document.removeEventListener(type, proxy.call, options); } } clear(scope) { const aliveListeners = this.scopeToAliveListeners.get(scope); if (!aliveListeners) return; for (const proxy of aliveListeners) { proxy.destroy(); document.removeEventListener(proxy.type, proxy.call, proxy.options); } this.scopeToAliveListeners.delete(scope); } destroy() { for (const aliveListeners of this.scopeToAliveListeners.values()) { for (const proxy of aliveListeners) { proxy.destroy(); document.removeEventListener(proxy.type, proxy.call, proxy.options); } } this.scopeToAliveListeners.clear(); document.removeEventListener(this.type, this.suppress, { capture: true }); } } // Not directly using 'EventSuppressorEnvironment.isTrustedEvent' is for testing purposes. function isTrustedEvent(e) { return EventSuppressorEnvironment.isTrustedEvent(e); } // It must be initiated as early as possible, before the application runs. class EventSuppressorEnvironment { static isTrustedEvent = e => e.isTrusted; // Will not suppress events, if false. enabled = false; typeToSpecificEventSuppressor = new Map(); constructor(configs) { const types = Object.keys(configs); const isEnabled = () => this.enabled; for (const type of types) { const suppressor = new SpecificEventSuppressor(type, configs[type], isEnabled, isTrustedEvent); this.typeToSpecificEventSuppressor.set(type, suppressor); } } enable() { this.enabled = true; } disable() { this.enabled = false; } getSpecificEventSuppressor(type) { return this.typeToSpecificEventSuppressor.get(type) || null; } getAllSpecificEventSuppressors() { return this.typeToSpecificEventSuppressor.values(); } destroy() { const suppressors = this.getAllSpecificEventSuppressors(); for (const suppressor of suppressors) { suppressor.destroy(); } } } /** * Suppress events dispatched by the 'user action' in a document * * In theory, if an event is completely suppressed, * its default behavior will be canceled and all related listeners will not be called. * * But this is only a simulation, events cannot be suppressed successfully in certain cases, * such as: ctrl+t(create a new tab), ctrl+w(close the current the tab), * or add listeners before the instance starts work. */ class EventSuppressor { nativeListenerProxys = new Set(); constructor(env, scope) { this.env = env; this.scope = scope; } enable() { this.env.enable(); } disable() { this.env.disable(); } /** Adds an event listener that will not be supprressed. */ addAliveEventListener(type, listener, options = {}) { const suppressor = this.env.getSpecificEventSuppressor(type); if (suppressor) { suppressor.addAliveEventListener(this.scope, type, listener, options); } else { this.addNativeEventListener(type, listener, options); } } /** Removes an event listener that added by `addAliveEventListener`. */ removeAliveEventListener(type, listener, options = {}) { const suppressor = this.env.getSpecificEventSuppressor(type); if (suppressor) { suppressor.removeAliveEventListener(this.scope, type, listener, options); } else { this.removeNativeEventListener(type, listener, options); } } addNativeEventListener(type, listener, options = {}) { const proxy = new ListenerProxy(type, listener, options); proxy.call = proxy.call.bind(proxy); document.addEventListener(type, proxy.call, options); this.nativeListenerProxys.add(proxy); } removeNativeEventListener(type, listener, options = {}) { const proxys = this.nativeListenerProxys.values(); const proxy = findListenerProxy(proxys, listener); if (proxy) { document.removeEventListener(type, proxy.call, options); this.nativeListenerProxys.delete(proxy); } } destroy() { const suppressors = this.env.getAllSpecificEventSuppressors(); for (const suppressor of suppressors) { suppressor.clear(this.scope); } const proxys = this.nativeListenerProxys.values(); for (const proxy of proxys) { document.removeEventListener(proxy.type, proxy.call, proxy.options); } this.nativeListenerProxys.clear(); } } function checkHandleIsDraggable(handle, host) { let base = handle; while (base !== host && !base.draggable) { if (!base.parentElement) { break; } base = base.parentElement; } return base !== host; } function correctPreDropEffect(dataTransfer, droppable) { if (droppable) { dataTransfer.dropEffect = getEffectiveDropEffect(dataTransfer.effectAllowed); } else { dataTransfer.dropEffect = 'none'; } } function correctPostDropEffect(dataTransfer, droppable) { if (droppable) { if (dataTransfer.dropEffect === 'none') { dataTransfer.dropEffect = getEffectiveDropEffect(dataTransfer.effectAllowed); console.warn("[vdnd warn]: Do you set the 'dataTransfer.dropEffect' to 'none'? " + "The 'dataTransfer.dropEffect' for a droppable dropzone must not be 'none', " + "vdnd will force it to be set to not 'none'."); } } else { if (dataTransfer.dropEffect !== 'none') { dataTransfer.dropEffect = 'none'; console.warn("[vdnd warn]: Do you set the 'dataTransfer.dropEffect' to not 'none'? " + "The 'dataTransfer.dropEffect' for a not droppable dropzone must be 'none', " + "vdnd will force it to be set to 'none'."); } } } let NativeDnd$1 = class NativeDnd extends AbstractDnd { shouldSkipDragLeaveHandler = false; temporaryPreviousOver = null; temporaryLeave = null; // source or handle trigger = null; constructor(container, /** @readonly */ options) { super(container, options); this.container = container; this.options = options; document.addEventListener('drag', this.onDrag); document.addEventListener('dragstart', this.onDragStart); document.addEventListener('dragenter', this.onDragEnter); document.addEventListener('dragover', this.onDragOver); document.addEventListener('dragleave', this.onDragLeave); document.addEventListener('dragend', this.onDragEnd); document.addEventListener('drop', this.onDrop); } destroy() { if (this.dragging) return; document.removeEventListener('drag', this.onDrag); document.removeEventListener('dragstart', this.onDragStart); document.removeEventListener('dragenter', this.onDragEnter); document.removeEventListener('dragover', this.onDragOver); document.removeEventListener('dragleave', this.onDragLeave); document.removeEventListener('dragend', this.onDragEnd); document.removeEventListener('drop', this.onDrop); super.destroy(); } onDrag = e => { if (!this.dragging || !(e.target instanceof Element) || !e.dataTransfer || !closest(e.target, this.trigger)) return; const event = new DragEvent(this.source, this.currentOver, this.container, e); this.emit('drag', event); }; onDragStart = e => { if (this.dragging || !e.dataTransfer) return; // only HTMLElement is draggable if (!closest(e.target, this.container)) return null; const source = closest(e.target, this.options.source); if (!source) return; let handles = []; let currentHandle; if (this.options.handle) { handles = findHandles(source, { isSource: element => element.classList.contains(this.options.source), isHandle: element => element.classList.contains(this.options.handle) }); if (handles.length) { currentHandle = handles.find(handle => !!closest(e.target, handle)); if (!currentHandle) { e.preventDefault(); return; } else { if (!checkHandleIsDraggable(currentHandle, source)) { console.error('[vdnd error]: NativeDnd requires the handle must be draggable, the illegal handle is ', currentHandle); return; } } } } if (!this.isDraggable(source)) { e.preventDefault(); const dragPreventEvent = new DragPreventEvent(source, this.container, e); this.emit('drag:prevent', dragPreventEvent); return; } this.source = source; this.trigger = currentHandle || source; if (currentHandle && e.dataTransfer) { const { top, left } = this.source.getBoundingClientRect(); e.dataTransfer.setDragImage(this.source, e.clientX - left, e.clientY - top); } const dragStartEvent = new DragStartEvent(source, this.container, e); this.emit('drag:start', dragStartEvent); }; onDragEnter = e => { if (!this.dragging || !(e.target instanceof Element) || !e.dataTransfer) return; const dropzone = closest(e.target, this.container) && closest(e.target, this.options.dropzone); if (!dropzone) { return; } e.preventDefault(); correctPreDropEffect(e.dataTransfer, this.isDroppable(dropzone)); this.temporaryLeave = null; if (dropzone === this.currentOver) { this.shouldSkipDragLeaveHandler = true; return; } this.temporaryPreviousOver = this.currentOver; this.currentOver = dropzone; const dragEnterEvent = new DragEnterEvent(this.source, dropzone, this.container, e); this.emit('drag:enter', dragEnterEvent); correctPostDropEffect(e.dataTransfer, this.isDroppable(dropzone)); }; onDragLeave = e => { if (!this.dragging || this.shouldSkipDragLeaveHandler || !e.dataTransfer || e.target !== this.temporaryPreviousOver && e.target !== this.currentOver) { this.shouldSkipDragLeaveHandler = false; return; } this.shouldSkipDragLeaveHandler = false; const leave = this.temporaryPreviousOver || this.currentOver; this.temporaryPreviousOver = null; // Established when the following conditions are met: // 1、terminates interaction by pressing 'esc'. // 2、ends iteraction by releasing the mouse and dataTranser.dropEffect is 'none' // —— this means the current drop target is not droppable. if (e.relatedTarget === null) { this.temporaryLeave = leave; } if (leave === this.currentOver) { this.currentOver = null; } const event = new DragLeaveEvent(this.source, leave, this.container, e); this.emit('drag:leave', event); }; onDragOver = e => { if (!this.dragging || !this.currentOver || !(e.target instanceof Element) || !e.dataTransfer || !closest(e.target, this.currentOver)) return; const droppable = this.isDroppable(this.currentOver); if (droppable) { e.preventDefault(); } correctPreDropEffect(e.dataTransfer, droppable); const event = new DragOverEvent(this.source, this.currentOver, this.container, e); this.emit('drag:over', event); correctPostDropEffect(e.dataTransfer, this.isDroppable(this.currentOver)); }; onDrop = e => { if (!this.dragging || !this.currentOver || !(e.target instanceof Element) || !e.dataTransfer || !closest(e.target, this.currentOver) || !this.isDroppable(this.currentOver)) { return; } const dropEvent = new DropEvent(this.source, this.currentOver, this.container, e); this.emit('drop', dropEvent); }; onDragEnd = e => { if (!this.dragging || !(e.target instanceof Element) || !e.dataTransfer || !closest(e.target, this.trigger)) return; const dragEndEvent = new DragEndEvent(this.source, this.currentOver || this.temporaryLeave, this.container, e); this.emit('drag:end', dragEndEvent); this.trigger = null; this.source = null; this.currentOver = null; this.temporaryPreviousOver = null; this.temporaryLeave = null; this.shouldSkipDragLeaveHandler = false; }; }; function getEffectiveDropEffect(effectAllowed) { switch (effectAllowed) { case 'all': case 'copy': case 'none': case 'copyLink': case 'copyMove': case 'uninitialized': return 'copy'; case 'link': case 'linkMove': return 'link'; case 'move': return 'move'; } } class SimulatedDnd extends AbstractDnd { static defaultPlugins = [Mirror]; static eventTriggeringInterval = { drag: 50, dragover: 50 }; plugins = []; simulator = null; /** * This is a mock: browser will suppress ui events while dragging. */ eventSuppressor = null; /** * Triggers 'drag' event at regular interval */ clearIntervalTaskForDrag = emptyFn; /** * Triggers 'drag:over' event at regular interval */ clearIntervalTaskForDragOver = emptyFn; constructor(container, /** @readonly */ options) { super(container, options); this.container = container; this.options = options; this.addPlugin(...SimulatedDnd.defaultPlugins); this.eventSuppressor = options.eventSuppressor || null; this.setSimulator(this.options.simulator); this.initIntervalTasks(); } /** * Destroys dnd instance. This removes all internal event listeners and * deactivates simulator and plugins */ destroy() { if (this.dragging) return; this.removePlugin(...this.plugins.map(plugin => plugin.constructor)); this.setSimulator(null); this.eventSuppressor?.destroy(); super.destroy(); } /** * Adds plugin to this dnd instance. This will end up calling the attach method of the plugin */ addPlugin(...plugins) { const activePlugins = plugins.map(Plugin => new Plugin(this)); activePlugins.forEach(plugin => plugin.attach()); this.plugins = [...this.plugins, ...activePlugins]; return this; } /** * Removes plugins that are already attached to this dnd instance. This will end up calling * the detach method of the plugin */ removePlugin(...plugins) { const removedPlugins = this.plugins.filter(plugin => plugins.some(Plugin => plugin instanceof Plugin)); removedPlugins.forEach(plugin => plugin.detach()); this.plugins = this.plugins.filter(plugin => plugins.some(Plugin => plugin instanceof Plugin)); return this; } initIntervalTasks() { this.runIntervalTaskForDrag = e => { const timer = setInterval(() => { if (!e.canceled() && this.dragging) { const event = new DragEvent(this.source, this.currentOver, this.container, null); this.emit('drag', event); } }, SimulatedDnd.eventTriggeringInterval.drag); this.clearIntervalTaskForDrag = () => { clearInterval(timer); this.clearIntervalTaskForDrag = emptyFn; }; }; this.on('drag:start', this.runIntervalTaskForDrag); this.on('drag:end', () => this.clearIntervalTaskForDrag()); this.runIntervalTaskForDragOver = () => { const timer = setInterval(() => { if (!this.dragging || !this.currentOver) return; const dragOverEvent = new DragOverEvent(this.source, this.currentOver, this.container, null); this.emit('drag:over', dragOverEvent); }, SimulatedDnd.eventTriggeringInterval.dragover); this.clearIntervalTaskForDragOver = () => { clearInterval(timer); this.clearIntervalTaskForDragOver = emptyFn; }; }; this.on('drag:enter', this.runIntervalTaskForDragOver); this.on('drag:leave', () => this.clearIntervalTaskForDragOver()); } /** * Adds simulator to this dnd instance. * This will end up calling the attach method of the simulator and binding drag events */ setSimulator(Simulator) { if (this.dragging) { return; } if (!Simulator) { this.simulator?.detach(); this.simulator = null; return; } this.simulator?.detach(); const env = this.eventSuppressor ? { addEventListener: this.eventSuppressor.addAliveEventListener.bind(this.eventSuppressor), removeEventListener: this.eventSuppressor.removeAliveEventListener.bind(this.eventSuppressor) } : document; this.simulator = new Simulator(env, this.container, this.options, this.onDragStart.bind(this), this.onDragMove.bind(this), this.onDragEnd.bind(this)); this.simulator.attach(); return this; } onDragStart(event) { if (this.dragging) return; if (!this.isDraggable(event.source)) { const dragPreventEvent = new DragPreventEvent(event.source, this.container, event.originalEvent); this.emit('drag:prevent', dragPreventEvent); event.cancel(); return; } this.source = event.source; this.eventSuppressor?.enable(); applyUserSelect(document.body, 'none'); const dragStartEvent = new DragStartEvent(this.source, this.container, event.originalEvent, event.clientX, event.clientY); this.emit('drag:start', dragStartEvent); if (closest(this.source, this.options.dropzone)) { this.currentOver = this.source; const dragEnterEvent = new DragEnterEvent(this.source, this.source, this.container, event.originalEvent); this.emit('drag:enter', dragEnterEvent); } } onDragMove(event) { if (!this.dragging) return; const dropzone = closest(event.target, this.container) && closest(event.target, this.options.dropzone); const isLeavingCurrentDropzone = dropzone ? !!this.currentOver && dropzone !== this.currentOver : !!this.currentOver; const isEnterNewDropzone = dropzone && this.currentOver !== dropzone; let nextOver = null; if (isEnterNewDropzone) { nextOver = dropzone; const dragEnterEvent = new DragEnterEvent(this.source, dropzone, this.container, event.originalEvent); this.emit('drag:enter', dragEnterEvent); } if (isLeavingCurrentDropzone) { const dragLeaveEvent = new DragLeaveEvent(this.source, this.currentOver, this.container, event.originalEvent); this.emit('drag:leave', dragLeaveEvent); this.currentOver = null; } if (nextOver) { this.currentOver = nextOver; } const dragMoveEvent = new DragMoveEvent(this.source, this.currentOver, this.container, event.originalEvent, event.clientX, event.clientY); this.emit('drag:move', dragMoveEvent); } onDragEnd(event) { if (!this.dragging) return; this.eventSuppressor?.disable(); applyUserSelect(document.body, ''); if (this.currentOver) { const isEsc = event.originalEvent instanceof KeyboardEvent; if (!isEsc && this.isDroppable(this.currentOver)) { const dropEvent = new DropEvent(this.source, this.currentOver, this.container, event.originalEvent); this.emit('drop', dropEvent); } else { const dragLeaveEvent = new DragLeaveEvent(this.source, this.currentOver, this.container, event.originalEvent); this.emit('drag:leave', dragLeaveEvent); } } const dragEndEvent = new DragEndEvent(this.source, this.currentOver, this.container, event.originalEvent); this.emit('drag:end', dragEndEvent); this.source = null; this.currentOver = null; } } function applyUserSelect(element, value) { element.style.webkitUserSelect = value; // @ts-expect-error element.style.mozUserSelect = value; // @ts-expect-error element.style.msUserSelect = value; // @ts-expect-error element.style.oUserSelect = value; element.style.userSelect = value; } const DndContextSymbol = Symbol('DndContextSymbol'); const DndParentElementSymbol = Symbol('DndParentElementSymbol'); const defaultDndClasses = { 'source:dragging': 'dnd-source--dragging', 'source:draggable': 'dnd-source--draggable', 'source:disabled': 'dnd-source--disabled', 'dropzone:over': 'dnd-dropzone--over', 'dropzone:droppable': 'dnd-dropzone--droppable', 'dropzone:disabled': 'dnd-dropzone--disabled' }; const defaultMirrorOptions = { create: ({ source }) => source.nativeEl.cloneNode(true), appendTo: ({ source }) => source.nativeEl.parentNode || document.body }; function useInternalDndOptions(instance) { const type = instance.type; const options = vue.shallowRef(); function updateInternalOptionsBy(_options) { const internalOptions = { ..._options }; internalOptions.debug ??= false; internalOptions.strict ??= false; internalOptions.source