Skip to content

Latest commit

 

History

History
1171 lines (932 loc) · 25.7 KB

File metadata and controls

1171 lines (932 loc) · 25.7 KB

InteractJS Documentation

InteractJS is a JavaScript library for drag and drop, resizing, and multi-touch gestures for modern browsers. It provides powerful options like inertia and modifiers for snapping and restricting element movements.

Official Documentation: https://interactjs.io/docs/
GitHub Repository: https://github.com/taye/interact.js

Installation

npm (Pre-bundled)

Install the pre-bundled package with all features:

npm install --save interactjs

Import in your JavaScript:

// ES6 import
import interact from 'interactjs'

// CommonJS or AMD
const interact = require('interactjs')

CDN (Pre-bundled)

<script src="https://cdn.jsdelivr.net/npm/interactjs/dist/interact.min.js"></script>
<!-- or -->
<script src="https://unpkg.com/interactjs/dist/interact.min.js"></script>

npm (Streamlined - Smaller Bundle)

Install only the features you need:

npm install --save @interactjs/interact \
  @interactjs/auto-start \
  @interactjs/actions \
  @interactjs/modifiers \
  @interactjs/dev-tools
import '@interactjs/auto-start'
import '@interactjs/actions/drag'
import '@interactjs/actions/resize'
import '@interactjs/modifiers'
import '@interactjs/dev-tools'
import interact from '@interactjs/interact'

interact('.item').draggable({
  listeners: {
    move(event) {
      console.log(event.pageX, event.pageY)
    },
  },
})

Package Reference

Package Description
@interactjs/interact (required) provides the interact() method
@interactjs/actions Drag, resize, gesture actions
@interactjs/auto-start Start actions with pointer down, move sequence
@interactjs/modifiers Snap, restrict, etc. modifiers
@interactjs/snappers Provides interact.snappers.grid() utility
@interactjs/inertia Drag and resize inertia-like throwing
@interactjs/reflow interactable.reflow(action) method
@interactjs/dev-tools Console warnings for common mistakes

CDN (Streamlined)

<script type="module">
import 'https://cdn.interactjs.io/v1.9.20/auto-start/index.js'
import 'https://cdn.interactjs.io/v1.9.20/actions/drag/index.js'
import 'https://cdn.interactjs.io/v1.9.20/actions/resize/index.js'
import 'https://cdn.interactjs.io/v1.9.20/modifiers/index.js'
import 'https://cdn.interactjs.io/v1.9.20/dev-tools/index.js'
import interact from 'https://cdn.interactjs.io/v1.9.20/interactjs/index.js'

interact('.item').draggable({
  onmove(event) {
    console.log(event.pageX, event.pageY)
  },
})
</script>

TypeScript Type Definitions

If using the library only through CDN:

npm install --save-dev @interactjs/types

Quick Start

The basic steps to setting up interactions:

  1. Create an Interactable target
  2. Configure it to enable actions and add modifiers, inertia, etc.
  3. Add event listeners to provide visual feedback and update your app's state

Simple Slider Example

// Step 1: Target elements with the "slider" class
const slider = interact('.slider')

slider
  // Step 2: Configure the draggable action
  .draggable({
    origin: 'self',           // (0, 0) will be the element's top-left
    inertia: true,            // start inertial movement if thrown
    modifiers: [
      interact.modifiers.restrict({
        restriction: 'self',  // keep the drag coords within the element
      }),
    ],
  })
  // Step 3: Add event listener
  .on('dragmove', function (event) {
    const sliderWidth = interact.getElementRect(event.target.parentNode).width
    const value = event.pageX / sliderWidth

    event.target.style.paddingLeft = (value * 100) + '%'
    event.target.setAttribute('data-value', value.toFixed(2))
  })

Features

  • 🖱️ Draggable - Move elements or draw on canvas with drag events
  • 📐 Resizable - Resize elements by dragging edges or corners
  • 👆 Gesturable - Multi-touch gestures with angle, scale, and rotation data
  • 📍 Dropzones - Define drop targets for drag and drop applications
  • Inertia - Physics-based momentum after releasing elements
  • 🎯 Snapping - Snap to grid, points, or custom targets
  • 🔒 Restriction - Constrain movement within boundaries
  • 🎛️ Modifiers - Customize event coordinates with modifiers

Actions

Draggable

Make elements draggable by creating an interactable and calling the draggable method.

Basic Usage

const position = { x: 0, y: 0 }

interact('.draggable').draggable({
  listeners: {
    start(event) {
      console.log(event.type, event.target)
    },
    move(event) {
      position.x += event.dx
      position.y += event.dy

      event.target.style.transform = `translate(${position.x}px, ${position.y}px)`
    },
  }
})

HTML

<div class="draggable">Draggable Element</div>

CSS (Required)

.draggable {
  touch-action: none;
  user-select: none;
}

Important: Use CSS touch-action: none to prevent browser panning on touch devices, and user-select: none to disable text selection.

Drag Event Properties

Property Description
dragEnter The dropzone this Interactable was dragged over
dragLeave The dropzone this Interactable was dragged out of

lockAxis and startAxis

// Lock the drag to the starting direction
interact(singleAxisTarget).draggable({
  startAxis: 'xy',
  lockAxis: 'start'
})

// Only drag if started horizontally
interact(horizontalTarget).draggable({
  startAxis: 'x',
  lockAxis: 'x'
})
  • startAxis: Sets the direction that initial movement must be in for action to start. Use 'x' for horizontal or 'y' for vertical.
  • lockAxis: Causes drag events to change only in the given axis. Use 'start' to lock to starting direction.

Resizable

Resize elements by dragging their edges.

Basic Usage

interact('.resizable').resizable({
  edges: { top: true, left: true, bottom: true, right: true },
  listeners: {
    move(event) {
      let { x, y } = event.target.dataset

      x = (parseFloat(x) || 0) + event.deltaRect.left
      y = (parseFloat(y) || 0) + event.deltaRect.top

      Object.assign(event.target.style, {
        width: `${event.rect.width}px`,
        height: `${event.rect.height}px`,
        transform: `translate(${x}px, ${y}px)`,
      })

      Object.assign(event.target.dataset, { x, y })
    },
  },
})

HTML

<div data-x="0" data-y="0" class="resizable">
  <!-- top-left resize handle -->
  <div class="resize-top resize-left"></div>
  <!-- bottom-right resize handle -->
  <div class="resize-bottom resize-right"></div>
</div>

CSS (Required)

.resizable {
  touch-action: none;
  user-select: none;
  box-sizing: border-box;
}

Resize Event Properties

Property Description
edges The edges of the element being changed
rect Object with new dimensions of the target
deltaRect Change in dimensions since the previous event

Edge Options

The edges property specifies which edges can be resized:

interact(target).resizable({
  edges: {
    top: true,           // Use pointer coords to check for resize
    left: false,         // Disable resizing from left edge
    bottom: '.resize-s', // Resize if pointer target matches selector
    right: handleEl      // Resize if pointer target is the given Element
  }
})

Invert Option

Controls behavior when resizing would make dimensions less than 0x0:

interact(target).resizable({
  edges: { bottom: true, right: true },
  invert: 'reposition'  // 'none' | 'negate' | 'reposition'
})
Value Description
'none' (default) Limit resize rect to minimum 0x0
'negate' Allow rect to have negative width/height
'reposition' Keep positive dimensions by swapping edges

Aspect Ratio

Maintain aspect ratio while resizing:

interact(target).resizable({
  modifiers: [
    interact.modifiers.aspectRatio({
      ratio: 2,  // width always double the height, or 'preserve'
      modifiers: [
        interact.modifiers.restrictSize({ max: 'parent' }),
      ],
    }),
  ],
})

Gesturable

Handle multi-touch gestures with two or more pointers.

Basic Usage

let angle = 0

interact('#rotate-area').gesturable({
  onmove: function (event) {
    const arrow = document.getElementById('arrow')
    angle += event.da

    arrow.style.transform = 'rotate(' + angle + 'deg)'
    document.getElementById('angle-info').textContent = angle.toFixed(2) + '°'
  },
})

HTML

<div id="rotate-area">
  <div id="angle-info">0&deg;</div>
  <svg id="arrow" viewBox="0 0 100 100">
    <polygon points="50,0 75,25 62.5,25 62.5,100 37.5,100 37.5,25 25,25" fill="#29e"></polygon>
  </svg>
</div>

CSS (Required)

#rotate-area {
  touch-action: none;
  user-select: none;
}

Gesture Event Properties

Property Description
distance Distance between the first two touches
angle Angle of the line made by the two touches
da Change in angle since previous event
scale Ratio of start distance to current distance
ds Change in scale since previous event
box Box enclosing all touch points

Dropzone

Define elements that draggable targets can be dropped into.

Basic Usage

interact(dropTarget)
  .dropzone({
    ondrop: function (event) {
      alert(event.relatedTarget.id + ' was dropped into ' + event.target.id)
    }
  })
  .on('dropactivate', function (event) {
    event.target.classList.add('drop-activated')
  })

Dropzone Events

Event Description
dropactivate A draggable started that can be dropped here
dropdeactivate A draggable ended
dragenter A draggable entered the dropzone
dragleave A draggable left the dropzone
dropmove A draggable moved within the dropzone
drop A draggable was dropped in the dropzone

Dropzone Event Properties

Property Description
target The dropzone element
dropzone The dropzone Interactable
relatedTarget The element being dragged
draggable The Interactable being dragged
dragEvent The related drag event (dragstart, dragmove, dragend)
timeStamp Time of the event
type The event type

Accept Option

Specify which draggable elements can be dropped:

interact('.dropzone').dropzone({
  accept: '.drag0, .drag1',
})

Overlap Option

Controls how drops are checked:

interact(target).dropzone({
  overlap: 0.25  // 'pointer' | 'center' | number (0-1)
})
Value Description
'pointer' (default) Pointer must be over the dropzone
'center' Draggable element's center must be over the dropzone
0-1 (intersection area) / (draggable area) required for drop

Checker Function

Custom drop validation:

interact(target).dropzone({
  checker: function (
    dragEvent,         // related dragmove or dragend
    event,             // Touch, Pointer or Mouse Event
    dropped,           // bool default checker result
    dropzone,          // dropzone Interactable
    dropzoneElement,   // dropzone element
    draggable,         // draggable Interactable
    draggableElement   // draggable element
  ) {
    // only allow drops into empty dropzone elements
    return dropped && !dropzoneElement.hasChildNodes()
  }
})

Events

InteractEvent Types

Action Events
Draggable dragstart, dragmove, draginertiastart, dragend
Resizable resizestart, resizemove, resizeinertiastart, resizeend
Gesturable gesturestart, gesturemove, gestureend

Adding Event Listeners

function listener(event) {
  event.target.textContent = `${event.type} at ${event.pageX}, ${event.pageY}`
}

interact(target)
  .on('dragstart', listener)
  .on('dragmove dragend', listener)
  .on(['resizemove', 'resizeend'], listener)
  .on({
    gesturestart: listener,
    gestureend: listener,
  })

// Or using action options
interact(target).draggable({
  onstart: listener,
  onmove: listener,
  onend: listener,
})

interact(target).resizable({
  listeners: [
    {
      start: function (event) {
        console.log(event.type, event.pageX, event.pageY)
      },
    },
  ],
})

Common InteractEvent Properties

Property Description
target The element being interacted with
interactable The Interactable being interacted with
interaction The Interaction the event belongs to
x0, y0 Page x and y coordinates of the starting event
clientX0, clientY0 Client x and y coordinates of the starting event
dx, dy Change in coordinates of the mouse/touch
velocityX, velocityY The velocity of the pointer
speed The speed of the pointer
timeStamp The time of creation of the event object

Pointer Events

interact(target).on('hold', function (event) {
  console.log(event.type, event.target)
})

Available pointer events:

  • down - Pointer pressed down
  • move - Pointer moved
  • up - Pointer released
  • cancel - Interaction cancelled
  • tap - Quick press and release
  • doubletap - Two taps in quick succession
  • hold - Pointer held for ~600ms

Configuring Pointer Events

interact(target).pointerEvents({
  holdDuration: 1000,
  ignoreFrom: '[no-pointer]',
  allowFrom: '.handle',
  origin: 'self',
})

Fast Click

Use tap for fast clicks without mobile delay:

interact('a[href]').on('tap', function (event) {
  window.location.href = event.currentTarget.href
  event.preventDefault()
})

Modifiers

Modifiers change the coordinates of action events. Apply them using the modifiers array in action options.

Basic Usage

// Create modifiers
const restrictToParent = interact.modifiers.restrict({
  restriction: 'parent',
  elementRect: { left: 0, right: 0, top: 1, bottom: 1 },
})

const snap100x100 = interact.modifiers.snap({
  targets: [interact.snappers.grid({ x: 100, y: 100 })],
  relativePoints: [{ x: 0.5, y: 0.5 }],
})

// Apply to action
interact(target)
  .draggable({
    modifiers: [restrictToParent, snap100x100],
  })
  .on('dragmove', event => console.log(event.pageX, event.pageY))

Note: Modifiers are applied sequentially; their order may affect the final result.

endOnly Option

Apply modifier only to the last move event:

const snapAtEnd = interact.modifiers.snap({
  endOnly: true,
  targets: [/* ... */],
})

Snapping

snap()

Snap pointer coordinates to specified targets:

const mySnap = interact.modifiers.snap({
  targets: [
    { x: 200, y: 200 },
    { x: 250, y: 350 },
  ],
})

interact(element).draggable({
  modifiers: [mySnap]
})

relativePoints

Specify points on the element for snapping:

interact(element).draggable({
  modifiers: [
    interact.modifiers.snap({
      targets: [{ x: 300, y: 300 }],
      relativePoints: [
        { x: 0, y: 0 },     // snap relative to top-left
        { x: 0.5, y: 0.5 }, // to the center
        { x: 1, y: 1 }      // to the bottom-right
      ]
    })
  ]
})

offset

Shift target coordinates:

interact(element).draggable({
  modifiers: [
    interact.modifiers.snap({
      targets: [{ x: 300, y: 300 }],
      offset: { x: 20, y: 20 }  // or 'startCoords' | 'self' | 'parent'
    })
  ]
})

snapSize()

Snap dimensions when resizing:

interact(target).resizable({
  edges: { top: true, left: true },
  modifiers: [
    interact.modifiers.snapSize({
      targets: [
        { width: 100 },
        interact.snappers.grid({ width: 100, height: 100 }),
      ],
    }),
  ],
})

snapEdges()

Snap edges when resizing:

interact(target).resizable({
  edges: { top: true, left: true },
  modifiers: [
    interact.modifiers.snapEdges({
      targets: [
        interact.snappers.grid({ top: 100, left: 100 }),
      ],
    }),
  ],
})

Snap Grid

Create a grid snapping target:

const gridTarget = interact.snappers.grid({
  x: 50,              // horizontal grid spacing
  y: 50,              // vertical grid spacing
  range: 10,          // optional: snap range
  offset: { x: 5, y: 10 },  // optional: offset grid lines
  limits: {           // optional: grid boundaries
    top: 0, left: 0, bottom: 500, right: 500
  }
})

interact(element).draggable({
  modifiers: [interact.modifiers.snap({ targets: [gridTarget] })]
})

Custom Target Function

interact.modifiers.snap({
  targets: [
    function (x, y, interaction, offset, index) {
      return {
        x: x,
        y: 75 + 50 * Math.sin(x * 0.04),
        range: 40,
      }
    },
  ],
})

Restriction

restrict()

Restrict pointer coordinates to an area:

interact(target).draggable({
  modifiers: [
    interact.modifiers.restrict({
      restriction: 'parent',
      endOnly: true
    })
  ]
})

restriction values:

  • Rect object: { top, left, bottom, right } or { x, y, width, height }
  • Element: Use element's dimensions as restriction area
  • Function: (x, y, element) => rect or element
  • String: 'self', 'parent', or CSS selector

restrictRect()

Restrict element edges (not just pointer):

interact(target).draggable({
  modifiers: [
    interact.modifiers.restrictRect({
      restriction: 'parent'
    })
  ]
})

elementRect

Specify which part of element to consider as its edges:

// Allow quarter of element to hang over restriction edges
interact(target).draggable({
  modifiers: [
    interact.modifiers.restrictRect({
      restriction: 'parent',
      elementRect: { top: 0.25, left: 0.25, bottom: 0.75, right: 0.75 }
    })
  ]
})

restrictSize()

Set minimum and maximum dimensions when resizing:

interact(target).resizable({
  modifiers: [
    interact.modifiers.restrictSize({
      min: { width: 100, height: 100 },
      max: { width: 500, height: 500 }
    })
  ]
})

restrictEdges()

Set inner and outer edge boundaries when resizing:

interact(target).resizable({
  modifiers: [
    interact.modifiers.restrictEdges({
      inner: {
        left: 100,   // left edge must be <= 100
        right: 200   // right edge must be >= 200
      },
      outer: {
        left: 0,     // left edge must be >= 0
        right: 300   // right edge must be <= 300
      }
    })
  ]
})

Inertia

Enable physics-based momentum after releasing elements:

interact(target)
  .draggable({
    inertia: true
  })
  .resizable({
    inertia: {
      resistance: 30,
      minSpeed: 200,
      endSpeed: 100
    }
  })

Inertia Options

Option Description
resistance Rate at which action slows down (higher = slower)
endSpeed Speed (px/s) at which action is considered stopped
allowResume Allow user to resume action during inertia phase
smoothEndDuration Duration (ms) of interpolated movement to endOnly snap/restrict coords

Common Action Options

Options available for draggable(), resizable(), and gesturable():

max / maxPerElement

interact(target).draggable({
  max: 3,           // max concurrent interactions on this interactable
  maxPerElement: 2  // max on same interactable+element combination (default: 1)
})

manualStart

Disable auto-start and require manual Interaction#start:

interact(target)
  .draggable({
    manualStart: true,
  })
  .on('doubletap', function (event) {
    var interaction = event.interaction
    if (!interaction.interacting()) {
      interaction.start(
        { name: 'drag' },
        event.interactable,
        event.currentTarget,
      )
    }
  })

hold

Start action after pointer held down for specified milliseconds:

interact(target).draggable({
  hold: 500  // 500ms delay before drag starts
})

autoScroll

Scroll container when dragging near edges:

interact(element)
  .draggable({
    autoScroll: true,
  })
  .resizable({
    autoScroll: {
      container: document.body,
      margin: 50,
      distance: 5,
      interval: 10,
      speed: 300,
    }
  })

allowFrom (Handle)

Require action to start from specific child element:

<div class="movable-box">
  <div class="drag-handle"></div>
  Content
  <div class="resize-handle"></div>
</div>
interact('.movable-box')
  .draggable({
    allowFrom: '.drag-handle',
  })
  .resizable({
    allowFrom: '.resize-handle',
  })

ignoreFrom

Prevent action from starting on specific child elements:

<div id="movable-box">
  <p class="content">Selectable text</p>
  <div no-pointer-event>Should not fire events</div>
</div>
interact('#movable-box')
  .draggable({
    ignoreFrom: '.content',
  })
  .pointerEvents({
    ignoreFrom: '[no-pointer-event]',
  })

styleCursor

Disable automatic cursor styling:

interact(target).styleCursor(false)

cursorChecker

Custom cursor for each action:

interact(target).resizable({
  edges: { left: true, right: true },
  cursorChecker(action, interactable, element, interacting) {
    if (action.edges.left) return 'w-resize'
    if (action.edges.right) return 'e-resize'
  },
})

enabled

Enable or disable the action:

interact(target).draggable({
  enabled: false  // disable dragging
})

Complete Example

Drag and Drop with Snapping

<!DOCTYPE html>
<html>
<head>
  <style>
    .draggable {
      width: 100px;
      height: 100px;
      background: #29e;
      color: white;
      display: flex;
      align-items: center;
      justify-content: center;
      touch-action: none;
      user-select: none;
      cursor: move;
    }
    
    .dropzone {
      width: 300px;
      height: 300px;
      border: 2px dashed #666;
      display: flex;
      align-items: center;
      justify-content: center;
    }
    
    .dropzone.active {
      border-color: #29e;
    }
    
    .dropzone.hover {
      background: rgba(41, 158, 238, 0.1);
    }
  </style>
</head>
<body>
  <div class="draggable" id="drag-1">Drag me</div>
  <div class="dropzone" id="drop-1">Drop here</div>

  <script src="https://cdn.jsdelivr.net/npm/interactjs/dist/interact.min.js"></script>
  <script>
    const position = { x: 0, y: 0 }

    interact('.draggable')
      .draggable({
        inertia: true,
        modifiers: [
          interact.modifiers.snap({
            targets: [interact.snappers.grid({ x: 30, y: 30 })],
            range: Infinity,
            relativePoints: [{ x: 0.5, y: 0.5 }]
          }),
          interact.modifiers.restrict({
            restriction: 'body',
            elementRect: { top: 0, left: 0, bottom: 1, right: 1 },
            endOnly: true
          })
        ],
        autoScroll: true,
        listeners: {
          move(event) {
            position.x += event.dx
            position.y += event.dy
            event.target.style.transform = `translate(${position.x}px, ${position.y}px)`
          },
        }
      })

    interact('.dropzone')
      .dropzone({
        accept: '.draggable',
        overlap: 0.5,
        ondropactivate(event) {
          event.target.classList.add('active')
        },
        ondragenter(event) {
          event.target.classList.add('hover')
        },
        ondragleave(event) {
          event.target.classList.remove('hover')
        },
        ondrop(event) {
          console.log(event.relatedTarget.id + ' dropped into ' + event.target.id)
        },
        ondropdeactivate(event) {
          event.target.classList.remove('active', 'hover')
        }
      })
  </script>
</body>
</html>

Resizable Element

<!DOCTYPE html>
<html>
<head>
  <style>
    .resizable {
      width: 200px;
      height: 200px;
      background: #29e;
      color: white;
      padding: 20px;
      box-sizing: border-box;
      touch-action: none;
      user-select: none;
    }
  </style>
</head>
<body>
  <div class="resizable">Resize from any edge or corner</div>

  <script src="https://cdn.jsdelivr.net/npm/interactjs/dist/interact.min.js"></script>
  <script>
    interact('.resizable')
      .resizable({
        edges: { left: true, right: true, bottom: true, top: true },
        listeners: {
          move(event) {
            const target = event.target
            let x = (parseFloat(target.getAttribute('data-x')) || 0)
            let y = (parseFloat(target.getAttribute('data-y')) || 0)

            // update the element's style
            target.style.width = event.rect.width + 'px'
            target.style.height = event.rect.height + 'px'

            // translate when resizing from top or left edges
            x += event.deltaRect.left
            y += event.deltaRect.top

            target.style.transform = 'translate(' + x + 'px,' + y + 'px)'

            target.setAttribute('data-x', x)
            target.setAttribute('data-y', y)
            target.textContent = Math.round(event.rect.width) + '×' + Math.round(event.rect.height)
          }
        },
        modifiers: [
          interact.modifiers.restrictEdges({
            outer: 'parent'
          }),
          interact.modifiers.restrictSize({
            min: { width: 100, height: 50 }
          })
        ],
        inertia: true
      })
  </script>
</body>
</html>

Browser Support

InteractJS supports modern browsers including Chrome, Firefox, Safari, Edge, and mobile browsers. It handles both mouse and touch inputs.

License

MIT License - Copyright (c) 2012-present Taye Adeyemi