Draggable objects: examples

 from Red Blob Games
DRAFT
1 Dec 2023

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.

{ TODO: should I hide the code by default, so that it’s easy to browse the page and find the example you’re most interested in? }

diagram-state-and-event-handlers.svg

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 you a list of possible behaviors like snapping to a grid or constrained movement or using a handle. In my projects the drag event handler is decoupled from the state handling, so you 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.

Here's a basic state handler that works with the event handler. The position 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.In this state handler the setter for pos clamps the x and y positions to stay within a rectangular area.In this state handler the setter for pos moves the x,y position to the nearest gray dot. This example also shows how the internal representation [x,y] can be different from the external type {x,y}.Here's the classic dragging of a <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.There's a lot of flexibility in changing what's draggable separately from how the position is updated. Here's a recreation of one example from React-Draggable[1]. The event handlers connect to the red handle, but the state handler updates the position of the white containing box.The drag events don't have to involve moving anything. This example shows how they can be used for changing the number stored in an <input> element. I use this type of interaction on my Damage Rolls page. Since it’s a standard element you 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.Here's another example of how the drag events don't have to move anything. Here we don't want to keep track of the initial offset in the pointerdown event. Draw on the canvas:Here we're not dragging any DOM element. The canvas stays in place. But I'm moving a marker that's drawn on the canvas. I had to modify the event handler recipe to update the cursor whenever anything happened.This example shows multi-state painting. It remembers the color you are painting with, and keeps using it until the pointer up event. The event handling in this example differs from the standard recipe enough that I don't know if it's really worth keeping the state separate from the events. Each square has an event handler, and we release the pointer capture so that the pointer move goes to other squares while painting. But that also means this doesn't behave properly when moving the mouse outside the painting area and then back in.State can be shared between draggables. Here, the x position is separate and the y position is shared.

 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', {
          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', {
      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.

 3  More examples#

TODO: more examples I’m considering:

  1. snap to grid, like jquery-ui and react-draggable, although this isn’t any different from the svg-snapping example in theory
  2. invisible larger drag area for mobile (from my little-details page)
  3. nested draggables[3], where both the yellow and red boxes are draggable. Modified recipe: use stopPropagation() to prevent the pointer event from going up to the parent element
  4. constrain position to a non-rectangular area, like my making-of curved-paths page
  5. accessibility - make the numbers also show up in an <input> box, or make things draggable by keyboard? - something for the vue playground
  6. .dragging class options (from little-details page)

TODO: mark which event handling recipes were modified from the ones presented on the main page; this is why it’s a recipe and not a library

I should also construct some examples with React and Svelte etc.