dnd

Types

Configuration for drag-and-drop behavior.

This type defines how the drag-and-drop system should behave, including when to apply changes, how items should move, and what operation to perform.

Fields

  • before_update: A callback function that allows you to modify the list before the drag-and-drop operation is applied. Receives the drag index, drop index, and the updated list. Return the final list to use.
  • movement: Constrains how the ghost element can move (Free, Horizontal, or Vertical)
  • listen: When to apply list changes (OnDrag for real-time updates, OnDrop for final placement)
  • operation: What list operation to perform (InsertAfter, InsertBefore, Rotate, Swap, or Unaltered)
  • mode: Input mode for the system (MouseOnly, TouchOnly, or Auto). Default: MouseOnly
  • touch_timeout_ms: Milliseconds before touch selection auto-cancels. Default: 5000. Set to 0 to disable.
  • touch_scroll_threshold: Distance in pixels to distinguish tap from scroll. Default: 10. If the user moves more than this distance between touchstart and touchend, it’s treated as a scroll gesture and the touch selection is cancelled.
  • touch_hold_duration_ms: Minimum milliseconds to hold before touch selection activates. Default: 200. This prevents accidental selection when quickly tapping. Set to 0 to disable.
  • touch_drop_cooldown_ms: Milliseconds after pop before drops are accepted. Default: 500. This prevents accidental drops when the layout shifts and drop zones appear under the finger.

Example

// Mouse-only (default, backward compatible)
let config = dnd.Config(
  before_update: fn(_, _, list) { list },
  movement: dnd.Free,
  listen: dnd.OnDrag,
  operation: dnd.Rotate,
  mode: dnd.MouseOnly,
  touch_timeout_ms: 5000,
  touch_scroll_threshold: 10,
  touch_hold_duration_ms: 200,
  touch_drop_cooldown_ms: 500,
)

// Auto mode for hybrid devices
let touch_config = dnd.Config(
  before_update: fn(_, _, list) { list },
  movement: dnd.Vertical,
  listen: dnd.OnDrag,
  operation: dnd.Rotate,
  mode: dnd.Auto,
  touch_timeout_ms: 5000,
  touch_scroll_threshold: 10,
  touch_hold_duration_ms: 200,
  touch_drop_cooldown_ms: 500,
)
pub type Config(a) {
  Config(
    before_update: fn(Int, Int, List(a)) -> List(a),
    movement: Movement,
    listen: Listen,
    operation: Operation,
    mode: InputMode,
    touch_timeout_ms: Int,
    touch_scroll_threshold: Int,
    touch_hold_duration_ms: Int,
    touch_drop_cooldown_ms: Int,
  )
}

Constructors

  • Config(
      before_update: fn(Int, Int, List(a)) -> List(a),
      movement: Movement,
      listen: Listen,
      operation: Operation,
      mode: InputMode,
      touch_timeout_ms: Int,
      touch_scroll_threshold: Int,
      touch_hold_duration_ms: Int,
      touch_drop_cooldown_ms: Int,
    )
pub type DndMsg {
  DragStart(Int, String, Position)
  Drag(Position)
  DragOver(Int, String)
  DragEnter(Int)
  DragLeave
  DragEnd
  TouchStart(Int, String, Position)
  TouchMove(Position)
  TouchEnd(Position)
  TouchSelect(Int, String)
  TouchDropStart(Position)
  TouchDrop(Int, Position)
  TouchCancel
  TouchTimeout(Int)
  TouchDurationMet(Int)
  TouchDropCooldownComplete(Int)
}

Constructors

  • DragStart(Int, String, Position)
  • Drag(Position)
  • DragOver(Int, String)
  • DragEnter(Int)
  • DragLeave
  • DragEnd
  • TouchStart(Int, String, Position)
  • TouchMove(Position)
  • TouchEnd(Position)
  • TouchSelect(Int, String)
  • TouchDropStart(Position)
  • TouchDrop(Int, Position)
  • TouchCancel
  • TouchTimeout(Int)
  • TouchDurationMet(Int)
  • TouchDropCooldownComplete(Int)

Information about the current drag operation when an item is being dragged.

This type provides access to all relevant details about an active drag operation, including positions, indices, and element identifiers. Use system.info(model) to extract this information when a drag is in progress.

Fields

  • drag_index: List index of the item being dragged (original position)
  • drop_index: List index of the current drop target (where it would be placed)
  • drag_element_id: DOM element ID of the dragged item
  • drop_element_id: DOM element ID of the current drop target
  • start_position: Mouse coordinates where the drag began
  • current_position: Current mouse coordinates during drag

Usage

// Check if currently dragging and get info
case model.system.info(model.system.model) {
  Some(info) -> {
    // Currently dragging - can use info for styling, ghost positioning, etc.
    html.div([
      attribute.style("transform", "translate("
        <> float.to_string(info.current_position.x) <> "px, "
        <> float.to_string(info.current_position.y) <> "px)")
    ], [html.text("Ghost element")])
  }
  None -> {
    // Not currently dragging
    html.text("")
  }
}

Availability

This information is only available during active drag operations (returns Some(Info)). When no drag is in progress, system.info() returns None.

pub type Info {
  Info(
    drag_index: Int,
    drop_index: Int,
    drag_element_id: String,
    drop_element_id: String,
    start_position: Position,
    current_position: Position,
  )
}

Constructors

  • Info(
      drag_index: Int,
      drop_index: Int,
      drag_element_id: String,
      drop_element_id: String,
      start_position: Position,
      current_position: Position,
    )

Determines which input mode the drag-and-drop system should use.

Variants

  • MouseOnly: Traditional continuous drag behavior using mouse events. This is the default and provides the classic drag-and-drop experience.
  • TouchOnly: Two-tap selection pattern optimized for touch devices. Tap to select an item, then tap a drop zone to move it.
  • Auto: Automatically detects the input type per-interaction. Uses mouse behavior for mousedown events, touch behavior for touch events. Best for hybrid devices like tablets with keyboards.
pub type InputMode {
  MouseOnly
  TouchOnly
  Auto
}

Constructors

  • MouseOnly
  • TouchOnly
  • Auto

Interaction state - tracks whether we’re idle, mouse dragging, or touch selecting. This is used internally by the Model type.

pub type InteractionState {
  Idle
  MouseDragging(State)
  TouchPending(TouchPendingState)
  TouchSelecting(TouchState)
}

Constructors

Determines when list modifications should be applied during drag operations.

This controls the timing of when your list gets updated - either continuously while dragging for real-time visual feedback, or only when the item is dropped for final placement.

Variants

  • OnDrag: Apply list changes immediately as the item is dragged over drop targets. Provides real-time visual feedback but triggers more frequent updates.
  • OnDrop: Apply list changes only when the drag operation completes. More performant for complex lists but less visual feedback during dragging.

Use Cases

  • OnDrag: Best for simple lists where you want immediate visual feedback
  • OnDrop: Better for performance-sensitive applications or when you need to validate the drop

Example

// Real-time updates while dragging
let live_config = dnd.Config(
  listen: dnd.OnDrag,
  // ... other options
)

// Updates only on final drop
let batch_config = dnd.Config(
  listen: dnd.OnDrop,
  // ... other options
)
pub type Listen {
  OnDrag
  OnDrop
}

Constructors

  • OnDrag
  • OnDrop
pub type Model {
  Model(
    interaction: InteractionState,
    touch_generation: Int,
    mouse_blocked: Bool,
  )
}

Constructors

  • Model(
      interaction: InteractionState,
      touch_generation: Int,
      mouse_blocked: Bool,
    )

    Arguments

    mouse_blocked

    Blocks synthesized mouse events after touch interactions

pub type Movement {
  Free
  Horizontal
  Vertical
}

Constructors

  • Free
  • Horizontal
  • Vertical

Defines how the list should be modified when an item is dragged and dropped.

Each operation provides a different way of rearranging items in the list, affecting how the dragged item and other items are repositioned.

Variants

  • InsertAfter: Move the dragged item to the position immediately after the drop target
  • InsertBefore: Move the dragged item to the position immediately before the drop target
  • Rotate: Perform a circular shift of all items between the drag and drop positions (includes both endpoints)
  • Swap: Exchange the positions of the dragged item and the drop target item
  • Unaltered: Do not modify the list (useful for custom logic or visual feedback only)
pub type Operation {
  InsertAfter
  InsertBefore
  Rotate
  Swap
  Unaltered
}

Constructors

  • InsertAfter
  • InsertBefore
  • Rotate
  • Swap
  • Unaltered
pub type Position {
  Position(x: Float, y: Float)
}

Constructors

  • Position(x: Float, y: Float)
pub type State {
  State(
    drag_index: Int,
    drop_index: Int,
    drag_counter: Int,
    start_position: Position,
    current_position: Position,
    drag_element_id: String,
    drop_element_id: String,
  )
}

Constructors

  • State(
      drag_index: Int,
      drop_index: Int,
      drag_counter: Int,
      start_position: Position,
      current_position: Position,
      drag_element_id: String,
      drop_element_id: String,
    )

The main drag-and-drop system containing all necessary functions and state.

This type encapsulates the complete drag-and-drop functionality and provides the interface between your application and the drag-and-drop behavior. Create it using dnd.create() and integrate it into your Lustre application.

Type Parameters

  • a: The type of items in your draggable list
  • msg: Your application’s message type

Fields

  • model: Internal drag state (opaque - use info() to access current state)
  • update: Function to handle drag messages and update both the model and your list
  • drag_events: Function to generate mouse event attributes for draggable elements
  • drop_events: Function to generate mouse event attributes for drop targets
  • ghost_styles: Function to generate CSS styling for the ghost element
  • info: Function to extract current drag information (positions, indices, etc.)

Usage

// In your update function:
DndMsg(dnd_msg) -> {
  let #(new_dnd, new_items) =
    model.system.update(dnd_msg, model.system.model, model.items)
  let updated_system = dnd.System(..model.system, model: new_dnd)
  Model(system: updated_system, items: new_items)
}

// In your view function:
html.div(
  [..model.system.drag_events(index, element_id)],
  [html.text(item)]
)

Touch Support (new)

For touch-enabled modes (TouchOnly or Auto), use these additional fields:

  • update_with_effect: Like update, but returns an Effect for timeout scheduling
  • touch_grip_events: Click handler for entering touch selection mode
  • touch_drop_zone_events: Click handler for drop zones in touch mode
  • cancel_events: Event handler for canceling touch selection
  • is_touch_selecting: Check if currently in touch selection mode
  • selected_index: Get the index of the selected item (if any)
  • touch_selected_styles: Returns pop animation styles for the selected item
pub type System(a, msg) {
  System(
    model: Model,
    update: fn(DndMsg, Model, List(a)) -> #(Model, List(a)),
    drag_events: fn(Int, String) -> List(attribute.Attribute(msg)),
    drop_events: fn(Int, String) -> List(attribute.Attribute(msg)),
    ghost_styles: fn(Model) -> List(attribute.Attribute(msg)),
    info: fn(Model) -> option.Option(Info),
    update_with_effect: fn(DndMsg, Model, List(a)) -> #(
      Model,
      List(a),
      effect.Effect(msg),
    ),
    touch_grip_events: fn(Int, String) -> List(
      attribute.Attribute(msg),
    ),
    touch_drop_zone_events: fn(Int) -> List(
      attribute.Attribute(msg),
    ),
    cancel_events: fn() -> List(attribute.Attribute(msg)),
    is_touch_selecting: fn(Model) -> Bool,
    selected_index: fn(Model) -> option.Option(Int),
    touch_selected_styles: fn(Model, Int) -> List(
      attribute.Attribute(msg),
    ),
  )
}

Constructors

Touch pending state - awaiting touchend to determine if tap or scroll. The duration_met field tracks whether the minimum hold duration has elapsed.

pub type TouchPendingState {
  TouchPendingState(
    index: Int,
    element_id: String,
    start_position: Position,
    duration_met: Bool,
  )
}

Constructors

  • TouchPendingState(
      index: Int,
      element_id: String,
      start_position: Position,
      duration_met: Bool,
    )

Touch selection state for the two-tap pattern. The scroll_start field tracks when user begins a potential scroll gesture while in selection mode, allowing us to reset the timeout on scroll.

pub type TouchState {
  TouchState(
    selected_index: Int,
    selected_id: String,
    scroll_start: option.Option(Position),
    drop_enabled: Bool,
    drop_touch_start: option.Option(Position),
  )
}

Constructors

Values

pub fn after_backward(i: Int, j: Int, list: List(a)) -> List(a)
pub fn after_forward(i: Int, j: Int, list: List(a)) -> List(a)
pub fn before_backward(i: Int, j: Int, list: List(a)) -> List(a)
pub fn before_forward(i: Int, j: Int, list: List(a)) -> List(a)
pub fn create(
  config: Config(a),
  step_msg: fn(DndMsg) -> msg,
) -> System(a, msg)
pub fn get_info(model: Model) -> option.Option(Info)
pub fn get_selected_index(model: Model) -> option.Option(Int)

Get the index of the currently selected item in touch mode. Returns None if not in touch selection mode.

pub fn insert_after(
  drag_index: Int,
  drop_index: Int,
  list: List(a),
) -> List(a)
pub fn insert_before(
  drag_index: Int,
  drop_index: Int,
  list: List(a),
) -> List(a)
pub fn is_touch_selecting(model: Model) -> Bool

Check if the system is currently in touch selection mode.

pub fn rotate_items(
  drag_index: Int,
  drop_index: Int,
  list: List(a),
) -> List(a)
pub fn split_at(index: Int, list: List(a)) -> #(List(a), List(a))
pub fn swap_at(i: Int, j: Int, list: List(a)) -> List(a)
pub fn swap_items(
  drag_index: Int,
  drop_index: Int,
  list: List(a),
) -> List(a)
pub fn touch_selected_styles(
  model: Model,
  index: Int,
) -> List(attribute.Attribute(msg))

Returns pop animation styles for the selected item in touch mode. Apply these styles to the item element at the given index. Returns empty list if the index doesn’t match the selected item.

The animation uses CSS keyframes which must be defined in your stylesheet:

@keyframes dnd-pop {
  50% { transform: scale(1.2); }
}
Search Document