Enhancement

Swappable

Drag one item onto another to exchange their places

Import
@parallelogram-js/core/components/Swappable
Selector
[data-swappable]
Stylesheet
@parallelogram-js/core/styles/draggable.css

Playground

Tiles

Drag a tile onto another to swap them.

One
Two
Three
Four
<div class="swappable-grid" data-swappable>
  <div class="card">One</div>
  <div class="card">Two</div>
  <div class="card">Three</div>
  <div class="card">Four</div>
</div>

About

Works as Sortable does, with a mouse, a finger or the keyboard, except that an item dropped on another swaps the two rather than shifting the ones between: the right shape for a grid of tiles or a set of slots, where every other item should stay put.

The container's children are the items, or the elements data-swappable-items selects, and data-swappable-handle limits where a drag can start. From the keyboard, Space or Enter picks up the focused item, the arrow keys swap it with the previous or next one, Space or Enter drops it and Escape undoes the swap. Containers with the same data-swappable-group swap with each other. swappable:change fires once a drop has swapped two items.

Usage

Register the component and the framework loads it the first time a page contains [data-swappable].

import { Parallelogram } from '@parallelogram-js/core';

const app = Parallelogram.create();
app.components.add('[data-swappable]', () => import('@parallelogram-js/core/components/Swappable'));
app.run();

The framework is optional. Swappable.enhanceAll() mounts the component on every matching element by itself, for a page that wants one enhancement rather than a framework:

import Swappable from '@parallelogram-js/core/components/Swappable';

Swappable.enhanceAll();

It takes a selector and the component's options, and returns the instance it mounted them with. What it leaves out is what the framework adds: loading a component only on the pages that use it, mounting again after a page swap, and ordering with dependsOn.

Its styles come from the package stylesheet:

@import '@parallelogram-js/core/styles/draggable.css';

Acknowledgements

Swappable is built on Shopify Draggable, used under the MIT licence. Copyright (c) 2018-present Shopify.

The drag engine, its Mirror, Scrollable and Focusable plugins, and the Sortable, Swappable and Droppable behaviour are ported from Draggable 1.2. A single Pointer Events sensor replaces its mouse, touch and force-touch sensors, the keyboard sensor is new, and state is written to data attributes rather than classes. Thank you to Shopify and Draggable's contributors for the work this builds on.

Attributes

NameTypeDefaultDescription
data-swappablerequiredflagnoneMarks the container
data-swappable-groupstringnoneContainers with the same group share one drag, so items move between them; the first one mounted sets the options For every instance, Swappable.defaults.group.
data-swappable-itemsselectornoneThe elements that can be dragged; the container's children when not set For every instance, Swappable.defaults.items.
data-swappable-handleselectornoneThe part of an item that starts a drag, such as a grip; give it touch-action: none so a touch on it drags rather than scrolls For every instance, Swappable.defaults.handle.
data-swappable-distancenumber4Pixels a mouse or pen moves before a drag starts, so a click on a link or button inside an item still clicks it For every instance, Swappable.defaults.distance.
data-swappable-touch-delaynumber100Milliseconds a finger rests on an item before a drag starts; moving sooner scrolls the page instead For every instance, Swappable.defaults.touchDelay.
data-swappable-axisboth | x | ybothWhich way the copy under the pointer may move For every instance, Swappable.defaults.axis.
data-swappable-disabledbooleanfalseStops new drags from starting in this container while set
data-swappable-picked-upstringPicked up {item}.Announced when an item is picked up; {item} is its label For every instance, Swappable.defaults.pickedUp.
data-swappable-keyboard-hintstringArrow keys move it, Space drops it, Escape cancels.Announced after the pick-up message when the keyboard picked the item up For every instance, Swappable.defaults.keyboardHint.
data-swappable-movedstringSwapped with {other}.Announced after each move; {item} and {other}, the item it swapped with For every instance, Swappable.defaults.moved.
data-swappable-droppedstringDropped {item}.Announced when an item is dropped For every instance, Swappable.defaults.dropped.
data-swappable-cancelledstringCancelled. {item} is back where it started.Announced when a drag is cancelled with Escape For every instance, Swappable.defaults.cancelled.
data-swappable-stateset by the componentidle | active | overnoneactive while a drag in its group is in progress, over while the item is over this container
data-swappable-item-stateset by the componentplaceholder | mirror | placednoneOn the placeholder showing where the item will land, the copy following the pointer, and an item just dropped On item.

Methods

NameSignatureDescription
cancel(element: HTMLElement) => voidCancel a drag in progress in the element’s group, putting the item back

Events

NameDetailDescription
swappable:start{ element: HTMLElement; item: HTMLElement; placeholder: HTMLElement; reverted: boolean; pointerType: string | null }An item is being picked up; preventDefault() leaves it where it is DOM event and event bus Cancelable.
swappable:swap{ element: HTMLElement; item: HTMLElement; placeholder: HTMLElement; reverted: boolean; pointerType: string | null; over: HTMLElement }The item is about to swap with another; preventDefault() stops it DOM event and event bus Cancelable.
swappable:swappedswappedElement: HTMLElementTwo items have swapped places; the drop is still to come DOM event and event bus
swappable:stop{ element: HTMLElement; item: HTMLElement; placeholder: HTMLElement; reverted: boolean; pointerType: string | null; swappedElement: HTMLElement | null; changed: boolean; items: HTMLElement[] }The drag has ended and the item is in place, dispatched on each container it changed; items are that container’s items in order DOM event and event bus
swappable:change{ element: HTMLElement; item: HTMLElement; placeholder: HTMLElement; reverted: boolean; pointerType: string | null; swappedElement: HTMLElement | null; changed: boolean; items: HTMLElement[] }As swappable:stop, only when two items swapped: the one to save on DOM event and event bus

CSS custom properties

NameDefaultDescription
--drag-over-bgvar(--color-hover)Background of a container the item is over
--drag-placeholder-opacity0.4Opacity of the placeholder where the item will land
--drag-placeholder-outline2px dashed var(--color-accent)Outline of the placeholder
--drag-mirror-shadowvar(--surface-dropdown-shadow)Shadow of the copy following the pointer
--drag-mirror-opacity0.95Opacity of the copy following the pointer
--drag-mirror-z-index2147483000Stacking of the copy following the pointer
--drag-placed-colorrgba(37, 99, 235, 0.15)Colour a dropped item fades from; none under prefers-reduced-motion
--drag-placed-duration0.6sHow long a dropped item takes to fade back