import {getControlPosition, createCoreData, snapToGrid, setTransform, setTopLeft} from './utils/PositionFns'; import {addEvent, getTouchIdentifier, matchesSelectorAndParentsTo, removeEvent} from './utils/domFns'; import Vue, {DirectiveOptions, VNode, VNodeDirective} from 'vue'; import {setBindingArrayValue, setBindingModifier, setBindingValue, setClassValue} from './utils/bindingHelper'; export interface DraggableOptions { useCSSTransforms?: boolean; noSelection?: boolean; noTouchAction?: boolean; touchAction?: string; allowAnyClick?: boolean; cancel?: string | null; disabled?: boolean; grid: [number, number]; handle?: string | null; className?: string; emitStartName?: string; emitDragName?: string; emitStopName?: string; onStart?: ((event: Event, coreEvent) => void) | null; onDrag?: ((event: Event, coreEvent) => void) | null; onStop?: ((event: Event, coreEvent) => void) | null; } export interface DraggableModifiers { callbackBase?: boolean; methodBase?: boolean; eventBase?: boolean; styleBase?: boolean; } export interface DraggableData { element: HTMLElement; binding: VNodeDirective; vnode: VNode; dragEventFor: 'string' | null; dragging: boolean; lastX: number | null; lastY: number | null; grid: [number, number]; touchIdentifier: any; draggingPosition: any; componentInstance?: Vue | null; } export interface DraggableState { data: DraggableData; options: DraggableOptions; modifiers: DraggableModifiers; onMouseDown: any; onMouseUp: any; onTouchStart: any; onTouchStop: any; handleDrag: any; handleDragStop: any; } export interface DragEventForInterface { start?: string; move?: string; stop?: string; } export interface EventForInterface { touch?: DragEventForInterface; mouse?: DragEventForInterface; } const eventsFor: EventForInterface = { touch: { start: 'touchstart', move: 'touchmove', stop: 'touchend', }, mouse: { start: 'mousedown', move: 'mousemove', stop: 'mouseup', }, }; const initialOptions: DraggableOptions = { useCSSTransforms: true, noSelection: true, noTouchAction: true, touchAction: 'none', allowAnyClick: false, cancel: null, disabled: false, grid: [1, 1], handle: null, className: 'vue-draggable', emitStartName: 'onStart', emitDragName: 'onDrag', emitStopName: 'onStop', onStart: null, onDrag: null, onStop: null, }; const initialModifiers: DraggableModifiers = { callbackBase: false, methodBase: false, eventBase: false, styleBase: false, }; const setBindingValues = (binding: VNodeDirective, data) => { if (binding.value) { setBindingValue(binding, data.options, initialOptions, 'useCSSTransforms', 'useCSSTransforms'); setBindingValue(binding, data.options, initialOptions, 'noSelection', 'noSelection'); setBindingValue(binding, data.options, initialOptions, 'noTouchAction', 'noTouchAction'); setBindingValue(binding, data.options, initialOptions, 'touchAction', 'touchAction'); setBindingValue(binding, data.options, initialOptions, 'allowAnyClick', 'allowAnyClick'); setBindingValue(binding, data.options, initialOptions, 'cancel', 'cancel'); setBindingValue(binding, data.options, initialOptions, 'disabled', 'disabled'); setBindingArrayValue(binding, data.options, initialOptions, 'grid', 'grid'); setBindingValue(binding, data.options, initialOptions, 'handle', 'handle'); setBindingValue(binding, data.options, initialOptions, 'emitStartName', 'emitStartName'); setBindingValue(binding, data.options, initialOptions, 'emitDragName', 'emitDragName'); setBindingValue(binding, data.options, initialOptions, 'emitStopName', 'emitStopName'); setBindingValue(binding, data.options, initialOptions, 'onStart', 'onStart'); setBindingValue(binding, data.options, initialOptions, 'onStop', 'onStop'); setBindingValue(binding, data.options, initialOptions, 'onDrag', 'onDrag'); } else { data.options = { ...initialOptions, }; } }; const setBindingModifiers = (binding: VNodeDirective, data) => { if (binding.modifiers) { setBindingModifier(binding, data.modifiers, initialModifiers, 'callbackBase', 'callback'); setBindingModifier(binding, data.modifiers, initialModifiers, 'methodBase', 'method'); setBindingModifier(binding, data.modifiers, initialModifiers, 'eventBase', 'event'); setBindingModifier(binding, data.modifiers, initialModifiers, 'styleBase', 'style'); } else { data.modifiers = { ...initialModifiers, }; } }; const setClass = (binding: VNodeDirective, data) => { setClassValue(data.data.element, binding, data.options, initialOptions, 'className', 'className'); }; const setStyles = (data) => { if (data.options.noSelection === true) { data.data.element.style['user-select'] = 'none'; } else { data.data.element.style['user-select'] = ''; } data.data.element.style.position = 'absolute'; }; const setNoTouchAction = (data, value: string | null = null) => { if (value === null) { value = data.options.touchAction; } if (data.options.noTouchAction) { data.data.element.style['touch-action'] = value; } else { data.data.element.style['touch-action'] = ''; } }; const emit = (name: string, event: Event, coreEvent, data) => { if (data.modifiers.callbackBase) { if (data.options.onStart && data.options.onDrag && data.options.onStop) { switch (name) { case data.options.emitStartName: data.options.onStart(event, coreEvent); break; case data.options.emitDragName: data.options.onDrag(event, coreEvent); break; case data.options.emitStopName: data.options.onStop(event, coreEvent); break; } } else { console.error('[v-draggable-core] You need to provide onStart, onDrag and onStop option ' + 'in order to use callback modifier'); } } if (data.modifiers.methodBase && data.componentInstance[name]) { data.componentInstance[name](event, coreEvent); } if (data.modifiers.eventBase && data.componentInstance) { data.componentInstance.$emit(name, event, coreEvent); } if (data.modifiers.styleBase && data.data.element) { onDragHandler(name, event, coreEvent, data); } }; const emitOnStart = (event: Event, coreEvent, data) => { emit(data.options.emitStartName, event, coreEvent, data); }; const emitOnDrag = (event: Event, coreEvent, data) => { emit(data.options.emitDragName, event, coreEvent, data); }; const emitOnStop = (event: Event, coreEvent, data) => { emit(data.options.emitStopName, event, coreEvent, data); }; const onDragHandler = (handlerName, e, { node, deltaX, deltaY }, data) => { const newPosition = {left: node.offsetLeft, top: node.offsetTop}; switch (handlerName) { case data.options.emitStartName: { const offsetParent = node.offsetParent; if (!offsetParent) { return; } const parentRect = offsetParent.getBoundingClientRect(); const clientRect = node.getBoundingClientRect(); newPosition.left = clientRect.left - parentRect.left + offsetParent.scrollLeft; newPosition.top = clientRect.top - parentRect.top + offsetParent.scrollTop; data.data.draggingPosition = { left: newPosition.left, top: newPosition.top }; break; } case data.options.emitDragName: if (!data.data.draggingPosition) { throw new Error('onDrag called before onDragStart.'); } newPosition.left = data.data.draggingPosition.left + deltaX; newPosition.top = data.data.draggingPosition.top + deltaY; data.data.draggingPosition = { left: newPosition.left, top: newPosition.top }; break; case data.options.emitStopName: if (!data.data.draggingPosition) { throw new Error('onDragEnd called before onDragStart.'); } newPosition.left = data.data.draggingPosition.left; newPosition.top = data.data.draggingPosition.top; data.data.draggingPosition = null; break; default: throw new Error( 'onDragHandler called with unrecognized handlerName: ' + handlerName, ); } const x = Math.round(newPosition.left); const y = Math.round(newPosition.top); // Not working if there is more elements in parent container // Then use useCSSTransforms = false if (data.options.useCSSTransforms) { setTransform(data.data.element, y, x); } else { setTopLeft(data.data.element, y, x); } }; const handleDragStart = (event: any, data) => { if (!data.data.allowAnyClick && typeof event.button === 'number' && event.button !== 0) { return false; } const thisNode = data.data.element; if (!thisNode || !thisNode.ownerDocument || !thisNode.ownerDocument.body) { throw new Error(' not mounted on DragStart!'); } if (data.options.disabled || (data.options.handle && !matchesSelectorAndParentsTo(event.target, data.options.handle, thisNode)) || (data.options.cancel && matchesSelectorAndParentsTo(event.target, data.options.cancel, thisNode))) { return; } setNoTouchAction(data); const {ownerDocument} = thisNode; data.data.touchIdentifier = getTouchIdentifier(event); const position = getControlPosition(event, data.data.touchIdentifier); if (position === null) { return; } const {x, y} = position; // Create an event object with all the data parents need to make a decision here. const coreEvent = createCoreData(data.data.element, x, y, data.data.lastX, data.data.lastY); emitOnStart(event, coreEvent, data); data.data.dragging = true; data.data.lastX = x; data.data.lastY = y; addEvent(ownerDocument, data.data.dragEventFor.move, data.handleDrag = (e) => { handleDrag(e, data); }); addEvent(ownerDocument, data.data.dragEventFor.stop, data.handleDragStop = (e) => { handleDragStop(e, data); }); }; const handleDrag = (event: any, data) => { if (event.type === 'touchmove') { event.preventDefault(); } const position = getControlPosition(event, data.data.touchIdentifier); if (position === null) { return; } let {x, y} = position; // Snap to grid if prop has been provided if (Array.isArray(data.data.grid)) { let deltaX = x - data.data.lastX; let deltaY = y - data.data.lastY; [deltaX, deltaY] = snapToGrid(data.data.grid, deltaX, deltaY); // skip useless drag if (!deltaX && !deltaY) { return; } x = data.data.lastX + deltaX; y = data.data.lastY + deltaY; } const coreEvent = createCoreData(data.data.element, x, y, data.data.lastX, data.data.lastY); emitOnDrag(event, coreEvent, data); data.data.lastX = x; data.data.lastY = y; }; const handleDragStop = (event: any, data) => { if (!data.data.dragging) { return; } const position = getControlPosition(event, data.data.touchIdentifier); if (position === null) { return; } const {x, y} = position; const coreEvent = createCoreData(data.data.element, x, y, data.data.lastX, data.data.lastY); data.data.dragging = false; data.data.lastX = NaN; data.data.lastY = NaN; setNoTouchAction(data, ''); emitOnStop(event, coreEvent, data); const {ownerDocument} = data.data.element; if (ownerDocument) { removeEvent(ownerDocument, data.data.dragEventFor.move, handleDrag); removeEvent(ownerDocument, data.data.dragEventFor.stop, handleDrag); } }; const onMouseDown = (event: MouseEvent, data) => { data.data.dragEventFor = eventsFor.mouse; // on touchscreen laptops we could switch back to mouse return handleDragStart(event, data); }; const onMouseUp = (event: MouseEvent, data) => { data.data.dragEventFor = eventsFor.mouse; return handleDragStop(event, data); }; const onTouchStart = (event: TouchEvent, data) => { data.data.dragEventFor = eventsFor.touch; // on touchscreen laptops we could switch back to mouse return handleDragStart(event, data); }; const onTouchStop = (event: TouchEvent, data) => { data.data.dragEventFor = eventsFor.touch; return handleDragStop(event, data); }; const Draggable: DirectiveOptions = (() => { const state = new WeakMap(); return { bind(el: HTMLElement, binding: VNodeDirective, vnode: VNode): void { const data: DraggableState = { data: { element: el, binding, vnode, dragEventFor: null, dragging: false, lastX: null, lastY: null, grid: initialOptions.grid, touchIdentifier: null, draggingPosition: null, componentInstance: vnode.componentInstance, }, options: { ...initialOptions, }, modifiers: { ...initialModifiers, }, onMouseDown: null, onMouseUp: null, onTouchStart: null, onTouchStop: null, handleDrag: null, handleDragStop: null, }; state.set(el, data); setBindingValues(binding, data); setBindingModifiers(binding, data); setStyles(data); setClass(binding, data); addEvent(el, 'mousedown', data.onMouseDown = (e) => { onMouseDown(e, data); }); addEvent(el, 'mouseup', data.onMouseUp = (e) => { onMouseUp(e, data); }); addEvent(el, 'touchstart', data.onTouchStart = (e) => { onTouchStart(e, data); }); addEvent(el, 'touchstop', data.onTouchStop = (e) => { onTouchStop(e, data); }); }, update(el: HTMLElement, binding: VNodeDirective, vnode: VNode): void { const data = state.get(el); setBindingValues(binding, data); setBindingModifiers(binding, data); setStyles(data); setClass(binding, data); }, unbind(el: HTMLElement, binding: VNodeDirective, vnode: VNode): void { const data = state.get(el); removeEvent(el, 'mousedown', data.onMouseDown); removeEvent(el, 'mouseup', data.onMouseUp); removeEvent(el, 'touchstart', data.onTouchStart); removeEvent(el, 'touchstop', data.onTouchStop); state.delete(el); }, }; })(); export default Draggable;