Skip to main content
Introducing packages.sweber.dev
Documentation menuBoard and zoom

Board and zoom

Drag to rearrange a grid or the columns of a board, and pinch, wheel and double tap zoom for images and maps.

import { board, zoom } from "@weber-development/lagrangian-pro";

Board

// A wrapping grid
const photos = board(grid, {
  onMove: ({ item, from, to }) => save(from.index, to.index),
});

// Columns: drag between lists
board([todo, doing, done], {
  canDrop: (item, list) => list !== done || isReady(item),
  onMove: ({ item, from, to }) => api.move(item.dataset.id, to.container.id, to.index),
});

photos.move(item, grid, 0); // from code

The children of each container are the items. The one you drag follows the pointer at the point you grabbed it, the others spring to their new places while you hover, and on release it settles into its slot. The slot is found in reading order, so it works for CSS grids and flex-wrap layouts, for lists of different heights and for empty columns. handle limits dragging to part of an item, lift sets how much the item grows while dragged, and the container under the pointer gets data-over="true" when there are several.

onMove runs after the drop with the item and its old and new { container, index }.

With a framework

board(columns, {
  commit: async ({ item, from, to }) => {
    setState(moveCard(state, item.dataset.id, to.container.id, to.index)); // your state
    await nextRender(); // resolve once the DOM shows the new order
  },
});

With commit the board puts the DOM back as your framework expects it, calls commit, and plays the transition after the returned promise resolves, so keys and state stay intact. In React use useBoard and let the hook handle the timing.

Keyboard and screen readers

Space or Enter picks an item up, the arrow keys move it (in a grid by the layout, between columns with left and right), Space or Enter drops it, Escape puts it back. Every step is announced. Give each column an aria-label to have its name read, and translate the texts with messages: { lifted, moved, dropped, cancelled }.

Zoom

const viewer = zoom(viewport, {
  min: 1, max: 6,
  doubleTap: 2.5,            // false to turn off
  onChange: ({ scale, x, y }) => {},
});

viewer.zoomTo(3, { x: 200, y: 150 }); // around a point in viewport pixels
viewer.panTo({ x: 800, y: 400 });     // a content point to the centre
viewer.reset();

The first child of the viewport is zoomed. Pinch with two fingers around the point between them, zoom with the wheel or a trackpad pinch around the pointer, double tap or double click to zoom in and out, drag to pan. Panning glides with friction and rubber-bands at the edges, zooming past min and max stretches and springs back, and a content that is smaller than the view stays centred. With the viewport focused +, - and 0 zoom and reset, the arrow keys pan. All of it moves the same three values (viewer.scale, viewer.x, viewer.y), so a throw, a pinch and a key press continue each other.

React

const kanban = useBoard({ commit });
<div ref={kanban.list("todo")} aria-label="To do">…</div>
<div ref={kanban.list("done")} aria-label="Done">…</div>

const [ref, viewer] = useZoom<HTMLDivElement>({ max: 8 });
<div ref={ref}><img src="map.png" alt="Map" /></div>