@vdnd/v3
Version:
Easy used vue drag and drop component library.
1,707 lines (1,640 loc) • 91.5 kB
JavaScript
'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