Skip to content

Instantly share code, notes, and snippets.

@Radiergummi
Last active May 2, 2023 09:51
Show Gist options
  • Save Radiergummi/5b6f9a3c59b3c11130065443c77b4f0f to your computer and use it in GitHub Desktop.
Save Radiergummi/5b6f9a3c59b3c11130065443c77b4f0f to your computer and use it in GitHub Desktop.
A Vue directive to react to something being dragged on the browser window. Port of this Stack Overflow answer: https://stackoverflow.com/a/55392885/2532203

Global dragging directive

Detect global drag status

<div v-dragging="{ enter, leave }">

What's this good for?

Imagine you'd like to build a drag-n-drop file upload, and indicate the drop target to users when they drag a file onto the window. This requires adding an event listener to the window or body, and deal with all kinds of drag and drop API shenanigans.

This directive makes it really easy to handle global drag and drop detection, even for files, which is usually restricted due to security problems.

Credit

All credits for the method go to @mpenner, who wrote the code for React originally.

<template>
<div v-dragging="{ enter, leave }">
</div>
</template>
<script>
import './dragging.ts';
export default {
methods: {
enter() {
console.log('Dragging started');
},
leave() {
console.log('Dragging ended');
}
}
}
</script>
import Vue from 'vue';
import { VNodeDirective } from 'vue/types/vnode';
declare global {
interface HTMLElement {
_draggingListeners?: Array<() => void>;
}
}
interface DraggingHandlers {
enter?: EventListener;
leave?: EventListener;
over?: EventListener;
drop?: EventListener;
}
interface DraggingDirective extends VNodeDirective {
value?: EventListener | DraggingHandlers;
}
let count = 0;
let dragging = false;
let cancelImmediate = noOp;
Vue.directive( 'dragging', {
inserted( element: HTMLElement, binding: DraggingDirective ): void {
const dragEnter: EventListener = ( event ) => {
event.preventDefault();
const handler: EventListener = (
typeof binding.value === 'function'
? binding.value
: binding.value!.enter
) as EventListener;
if ( count === 0 ) {
dragging = true;
handler( event );
}
++count;
};
const dragLeave: EventListener = ( event ) => {
event.preventDefault();
const handler: EventListener = (
typeof binding.value === 'function'
? binding.value
: binding.value!.leave
) as EventListener;
cancelImmediate = setImmediate( () => {
--count;
if ( count === 0 ) {
dragging = false;
handler( event );
}
} );
};
const dragOver: EventListener = ( event: Event ) => {
event.preventDefault();
( event as DragEvent ).dataTransfer!.dropEffect = 'copy';
const handler: EventListener | undefined =
typeof binding.value === 'function'
? binding.value
: binding.value!.leave;
if ( handler ) {
handler( event );
}
};
const drop: EventListener = ( event ) => {
event.preventDefault();
cancelImmediate();
if ( count > 0 ) {
count = 0;
dragging = false;
}
const handler: EventListener | undefined =
typeof binding.value === 'function'
? binding.value
: binding.value!.leave;
if ( handler ) {
handler( event );
}
};
element._draggingListeners = [
createEventListener( 'dragenter', dragEnter ),
createEventListener( 'dragleave', dragLeave ),
createEventListener( 'dragover', dragOver ),
createEventListener( 'drop', drop ),
];
},
unbind( element: HTMLElement ) {
if ( !element._draggingListeners ) {
return;
}
// Remove all listeners
element._draggingListeners.forEach( fn => fn() );
},
} );
function createEventListener<K extends keyof DocumentEventMap>(
type: K,
listener: ( this: Document, event: DocumentEventMap[K] ) => any, options?: boolean | AddEventListenerOptions,
) {
document.addEventListener( type, listener, options );
return () => document.removeEventListener(
type,
listener,
options,
);
}
function noOp(): void {
}
function setImmediate( callback: ( ...args: any[] ) => void, ...args: any[] ) {
let cancelled = false;
Promise.resolve().then( () => cancelled || callback( ...args ) );
return () => {
cancelled = true;
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment