Utilities

Draggable

A box dragged anywhere in its container with the pointer or the arrow keys, kept inside it, with on_move(x, y).

#utility#drag#keyboard#interactive

Demo

Preview
Buy milk
Call Ana
Drag to move, or use the arrow keys; Shift moves further.
Drag me by the grip.
Drag to move, or use the arrow keys; Shift moves further.

The note is at 24, 24. Drag it, or Tab to it and use the arrow keys.

Get it

It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/draggable.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/draggable.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/draggable.qwe
component DraggableDemoLive() {    state at = [24, 24]    <div.demo-stack>        <div id="draggable-demo-board" style="position: relative; height: 260px; border: 1px dashed var(--ui-strong); border-radius: var(--ui-radius); background: var(--ui-subtle)">            <Draggable x={24} y={24} label="Sticky note" on_move={(x, y) => { at = [x, y] }}>                <div style="width: 150px; padding: 14px; border-radius: var(--ui-radius-sm); background: var(--ui-warning-soft); border: 1px solid var(--ui-border)">Buy milk<br/>Call Ana</div>            </Draggable>            <Draggable x={260} y={120} handle={true} label="Panel">                <div style="width: 180px; padding: 10px 14px; background: var(--ui-surface); border: 1px solid var(--ui-border); border-top: 0; border-radius: 0 0 var(--ui-radius-sm) var(--ui-radius-sm)">Drag me by the grip.</div>            </Draggable>        </div>        <p.demo-note>The note is at {at[0]}, {at[1]}. Drag it, or Tab to it and use the arrow keys.</p>    </div>}view DraggableDemo() =    <div.demo-stack>        <DraggableDemoLive/>    </div>

Source

ui/draggable.qwe
// Draggable: a box that can be dragged anywhere inside its container, with the// pointer or with the arrow keys, and says where it ended up. For a sticker on// a canvas, a note on a board, a crop's handle, a floating panel.////   <div style="position: relative; height: 300px">//       <Draggable x={40} y={20} label="Note" on_move={(x, y) => { pos = [x, y] }}><Card>Buy milk</Card></Draggable>//   </div>//// The container is the nearest element around it with a position (relative),// which needs a size; the box stays inside it. x, y: where it starts, in// pixels from the container's top left corner. step: how far an arrow key// moves it (10; Shift moves ten times as far). label: what screen readers call// it, and they are told how to move it (give each on a page its own label). handle: only a grip bar along its top// starts a drag (false: anywhere on it). disabled: it stays put. on_move(x, y):// inside one of your components, called with the new place when a drag ends or// a key moves it. children: what is dragged.import ui/theme.qwe use ui_clslet draggable_css = css`.ui-drag { position: absolute; z-index: 1; touch-action: none; user-select: none; border-radius: var(--ui-radius-sm); outline: none; }.ui-drag:not(.ui-drag-handled):not(.ui-drag-off) { cursor: grab; }.ui-drag:focus-visible { box-shadow: var(--ui-focus); }.ui-drag-on { z-index: 2; cursor: grabbing !important; box-shadow: var(--ui-shadow-lg); }.ui-drag-grip { display: flex; justify-content: center; align-items: center; height: 16px; cursor: grab; color: var(--ui-muted);  border-radius: var(--ui-radius-sm) var(--ui-radius-sm) 0 0; background: var(--ui-subtle); }.ui-drag-grip::before { content: ""; width: 24px; height: 4px; border-top: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor; }.ui-drag-on .ui-drag-grip { cursor: grabbing; }`component Draggable(x: num = 0, y: num = 0, step: int = 10, label: str = "Draggable item", handle: bool = false,                    disabled: bool = false, on_move = null, class: str? = null, children) {    style global draggable_css    state px = x    state py = y    state grab = null       // { dx, dy } from the pointer to the box's corner, while dragging    state el = null    let help = `ui-drag-help-${re.replace(lower(label), "[^a-z0-9]+", "-")}`    let place = (nx: num, ny: num) => {        let box = el.offsetParent        px = round(max([0, min([nx, box.clientWidth - el.offsetWidth])]))        py = round(max([0, min([ny, box.clientHeight - el.offsetHeight])]))    }    <div class={ui_cls(["ui-drag", grab != null && "ui-drag-on", handle && "ui-drag-handled", disabled && "ui-drag-off", class])}         style={`left: ${px}px; top: ${py}px`} tabindex={disabled ? null : "0"} role="group" aria-roledescription="draggable"         aria-label={label} aria-describedby={help}         @mount={(me) => {             el = me             let d = js("document")             let moving = (e) => {                 if grab != null {                     let r = el.offsetParent.getBoundingClientRect()                     place(e.clientX - r.left - el.offsetParent.clientLeft - grab.dx, e.clientY - r.top - el.offsetParent.clientTop - grab.dy)                 }             }             let stop = (_e = null) => {                 if grab != null {                     grab = null                     on_move && on_move(px, py)                 }             }             d.addEventListener("pointermove", moving)             d.addEventListener("pointerup", stop)             d.addEventListener("pointercancel", stop)             () => {                 d.removeEventListener("pointermove", moving)                 d.removeEventListener("pointerup", stop)                 d.removeEventListener("pointercancel", stop)             }         }}         @pointerdown={(e) => {             let on_grip = !handle || e.target.closest(".ui-drag-grip") != null             if !disabled && e.button == 0 && on_grip {                 e.preventDefault()                 el.focus()                 let r = el.getBoundingClientRect()                 grab = { dx: e.clientX - r.left, dy: e.clientY - r.top }             }         }}         @keydown={(e) => {             let by = e.shiftKey ? step * 10 : step             let to = match e.key {                 "ArrowLeft" => [px - by, py]                 "ArrowRight" => [px + by, py]                 "ArrowUp" => [px, py - by]                 "ArrowDown" => [px, py + by]                 _ => null             }             if to != null && !disabled {                 e.preventDefault()                 place(to[0], to[1])                 on_move && on_move(px, py)             }         }}>        {handle && <div.ui-drag-grip aria-hidden="true"></div>}        {children}        <span.ui-sr id={help}>Drag to move, or use the arrow keys; Shift moves further.</span>    </div>}
Thread
87Useful? Like it, or add to its thread below.

Thread 0 posts

On Draggable: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.