On the main page I show the event handler recipe I use for dragging objects around in my interactive diagrams. That’s only handles the input. It doesn’t draw or move anything. The event handler needs to be connected to a state handler . The state handler handles the output.
This structure gives me the flexibility I want for my projects. The state handler isn’t limited to dragging, or a fixed set of behaviors. This is unlike the design of libraries like jquery-ui-draggable or react-draggable. Those libraries give me a list of predefined behaviors like snapping to a grid or constrained movement or using a handle, but the behavior I want isn’t always in that list. In my projects I decouple the drag event handler from the state handler, so I can implement any of those, and more, such as scrubble numbers or a painting program. On this page I’ll give examples with code you can use.
1 Vanilla JS examples#
These examples are starting points using global variables. You’ll have to adapt the recipe to fit into your own project.
pos
has a setter on it that also updates the circle’s position whenever the position variable is changed. The event handler sets the dragging
CSS class, which changes the color of the circle while it’s being dragged.pos
clamps the x and y positions to stay within a rectangular area.pos
snaps the x,y position to the nearest gray dot. On the left is snap-to-grid as seen in other libraries but the right side shows the flexibility of being able to write my own state handler.<div>
by setting its transform
CSS property. In this example eventToCoordinates
doesn’t need to transform the coordinate system. The position is constrained to the container <div>
. To switch to absolute positioning, the set pos()
function would change, but the event handler code would not.<input>
element. I use this type of interaction on my Damage Rolls page. Since it’s a standard element I can change it with keyboard/mouse in addition to dragging. However, the standard input event has styling and text selection that may interfere with dragging, so some people use a <span>
instead.[x,y]
can be different from the external type {x,y}
.2 Vue component#
I use Vue in many of my projects. The event handlers can be put into a Vue component that wraps a draggable element. This example is an SVG <g> wrapper, but the code would be similar for a <div> wrapper.
To use this component, create position variables (data
in Options API or ref
in Composition API) of type {x, y}
, and then draw the desired shape as the child slot of the draggable component. In this example, I have redCircle
and blueSquare
positions:
<svg viewBox="-100 -100 200 200" style="background: gray"> <Draggable v-model="redCircle"> <circle r="10" fill="red" /> </Draggable> <Draggable v-model="blueSquare"> <rect x="-10" y="-10" width="20" height="20" fill="blue" /> </Draggable> </svg>
Run it on the Vue Playground[2], with either the Options API or Composition API components. Or browse the component source code here:
<Draggable> component, Options API:
<template> <g v-bind:transform="`translate(${modelValue.x},${modelValue.y})`" v-bind:class="{dragging}" v-on:pointerdown.left="start" v-on:pointerup="end" v-on:pointercancel="end" v-on:pointermove="dragging ? move($event) : null" v-on:touchstart.prevent="" v-on:dragstart.prevent=""> <slot /> </g> </template> <script> import {eventToSvgCoordinates} from './svgcoords.js'; export default { props: {modelValue: Object}, // should be {x, y} data() { return { dragging: false }; }, methods: { start(event) { if (event.ctrlKey) return; let {x, y} = eventToSvgCoordinates(event); this.dragging = {dx: this.modelValue.x - x, dy: this.modelValue.y - y}; event.currentTarget.setPointerCapture(event.pointerId); }, end(_event) { this.dragging = null; }, move(event) { let {x, y} = eventToSvgCoordinates(event); this.$emit('update:modelValue', { ...this.modelValue, x: x + this.dragging.dx, y: y + this.dragging.dy, }); }, } } </script> <style scoped> g { cursor: grab; } g.dragging { user-select: none; cursor: grabbing; } </style>
<Draggable> component, Composition API:
<template> <g v-bind:transform="`translate(${modelValue.x},${modelValue.y})`" v-bind:class="{dragging}" v-on:pointerdown.left="start" v-on:pointerup="end" v-on:pointercancel="end" v-on:pointermove="dragging ? move($event) : null" v-on:touchstart.prevent="" v-on:dragstart.prevent=""> <slot /> </g> </template> <script setup> import {ref} from 'vue'; import {eventToSvgCoordinates} from './svgcoords.js'; const props = defineProps({ modelValue: Object // should be {x, y} }); const emit = defineEmits(['update:modelValue']) const dragging = ref(false); function start(event) { if (event.ctrlKey) return; let {x, y} = eventToSvgCoordinates(event); dragging.value = {dx: props.modelValue.x - x, dy: props.modelValue.y - y}; event.currentTarget.setPointerCapture(event.pointerId); } function end(event) { dragging.value = null; } function move(event) { let {x, y} = eventToSvgCoordinates(event); emit('update:modelValue', { ...props.modelValue, x: x + dragging.value.dx, y: y + dragging.value.dy, }); } </script> <style scoped> g { cursor: grab; } g.dragging { user-select: none; cursor: grabbing; } </style>
If you’re using Vue v2 (whether Options API or Composition API), you’ll need to change the v-model prop from modelValue
to value
and the event from update:modelValue
to input
.
Add computed setters on the model value to apply constraints like bounds checking or grid snapping. In this Vue playground example[3] the blue square uses a computed setter to stay in orbit around the red circle, with the orbit distance snapping to one of three values.
In Vue, multiple elements can point to the same reactive data. In this Vue Playground example[4] there’s an <input> element with v-model bound to the same state to make the position editable by keyboard.
3 More#
I feel like I should make the event handlers into a library but so far I’m treating it as a mere “recipe” that I copy and paste. This is in part because I have needed to modify it for the various examples on this page, and it seemed simpler to present something as something to copy and modify than to try to make one piece of code that handles all situations.
TODO: more examples I’m considering:
- invisible larger drag area for mobile (from my “little things” page)
- constrain position to a non-rectangular area, like my making-of curved-paths page
.dragging
class options (from “little things” page)- maybe a React or Svelte example
- drop constraints, such as detecting which box something is dropped into, or dropped between two items
Although I think of the state handling and the event handling as separate concepts, I will sometimes “inline” the state handling into the event handling code.
Other libraries to consider