FormKit Drag and Drop

repository·main·Indexed 23 days ago

https://github.com/formkit/drag-and-drop

A lightweight (~4Kb gzipped), framework-agnostic library for data-first drag and drop sorting and list-to-list transfers. It ensures the application state array remains the source of truth during reordering. The library provides first-class wrappers for React, Vue, and Solid, as well as support for plain JS/TS. Key features include multi-drag, animations, insert indicators, drop zones, and drag handles.

Tokens
9K
Snippets
16
Records
48
Agent score
83%

What's inside @formkit/drag-and-drop

  1. Overview of FormKit Drag and Drop

    main

    FormKit's Drag and Drop is a tiny (~4Kb gzipped), framework-agnostic library designed for data-first drag and drop sorting and list-to-list transfers.

    Key features include:

    • Data-first approach: The array is the source of truth; reordering items updates the data directly.
    • Framework support: First-class wrappers for React, Vue, and Solid, as well as support for plain JS/TS.
    • Built-in capabilities: Multi-drag, animations, insert indicators, drop zones, drag handles, and list-to-list transfers.
  2. Quick start with React using useDragAndDrop

    main

    FormKit's Drag and Drop is a data-first library, meaning your array is the source of truth. When you drag an item, the underlying data reorders automatically without manual DOM manipulation.

    To use it in React, import useDragAndDrop from @formkit/drag-and-drop/react. The hook returns a ref to be attached to the container element and the updated array of items.

    import { useDragAndDrop } from "@formkit/drag-and-drop/react";
    
    export function Tapes() {
      const [ref, tapes] = useDragAndDrop([
        "Depeche Mode",
        "Duran Duran",
        "Pet Shop Boys",
        "Kraftwerk",
      ]);
    
      return (
        <ul ref={ref}>
          {tapes.map((tape) => (
            <li key={tape}>{tape}</li>
          ))}
        </ul>
      );
    }
  3. How the `insert` plugin handles insertion logic

    main

    The insert plugin operates using a state-driven model to manage the visual and logical aspects of dragging:

    1. Range Detection: The plugin calculates 'ranges' (vertical or horizontal) around existing nodes. When the cursor enters a range, an insertPoint is positioned.
    2. Insert Point: A visual element is created and moved to the calculated position to show the user where the drop will occur.
    3. Validation: The plugin checks isValidDropTarget to ensure the dragged node is allowed to enter a specific parent (checking group compatibility or the accepts callback).
    4. Execution: Upon drop, the plugin updates the underlying data via setParentValues. It distinguishes between:
      • Sorting: Reordering nodes within the same parent.
      • Transferring: Moving nodes from one parent to another.
      • Inserting into Parent: Adding nodes to a parent that was previously empty (using draggedOverParent).
  4. Extend functionality with DNDPlugin

    main

    You can extend the drag-and-drop behavior by providing an array of DNDPlugin functions in the ParentConfig.plugins option.

    A plugin is a function that takes the parent HTMLElement and returns an optional DNDPluginData object containing lifecycle hooks.

    DNDPluginData hooks include:

    • setup: Called when the parent is set up.
    • tearDown: Called when the parent is torn down.
    • setupNode: Called when a node is set up.
    • tearDownNode: Called when a node is torn down.
    • setupNodeRemap: Called when nodes are mutated/remapped.
    • tearDownNodeRemap: Called when nodes are mutated/remapped.
    • remapFinished: Called when all nodes have finished remapping for a parent.
    export type DNDPlugin = (parent: HTMLElement) => DNDPluginData | undefined;
    
    export interface DNDPluginData {
      setup?: () => void;
      tearDown?: () => void;
      setupNode?: SetupNode;
      tearDownNode?: TearDownNode;
      setupNodeRemap?: SetupNode;
      tearDownNodeRemap?: TearDownNode;
      remapFinished?: () => void;
    }
  5. Use the dropOrSwap plugin

    main

    The dropOrSwap plugin enables advanced drag-and-drop interactions, allowing nodes to be either dropped into a new parent or swapped between parents.

    To use it, call dropOrSwap(config) with your desired configuration. This returns a function that accepts a parent HTMLElement and returns a setup object containing a setup() method. You should call this setup() method within your component's lifecycle (e.g., in a Vue setup() hook) to initialize the plugin for that parent element.

    Note that the plugin automatically intercepts and extends the handleEnd lifecycle event to perform the actual data movement (dropping or swapping) based on your configuration.

  6. Configure the animations plugin

    main

    The animations plugin accepts a Partial<AnimationsConfig> object to customize the sliding behavior and timing.

    Available configuration keys:

    • duration: The length of the animation in milliseconds. Defaults to 150.
    • easing: The CSS easing function string (e.g., 'ease-in-out'). Defaults to 'ease-in-out'.
    • xScale: The percentage value used for horizontal sliding translations. Defaults to 50.
    • yScale: The percentage value used for vertical sliding translations. Defaults to 50.
  7. Configure the `insert` plugin via `InsertConfig`

    main

    The insert plugin is configured using an InsertConfig<T> object. Key configuration options include:

    • insertPoint: A function that returns an HTMLElement to be used as the visual indicator for where a node will be inserted.
    • dynamicValues: A function used to determine the values being inserted, allowing for custom data transformation during transfer.
    • insertEvent: A callback triggered when an insertion is completed.
    • handleNodeDragover, handleParentPointerover, handleNodePointerover, handleParentDragover: Custom event handlers that can override default drag-and-drop behavior.

    Note: The plugin also utilizes standard parent configuration keys like dropZoneClass, dragPlaceholderClass, onSort, and onTransfer to manage visual states and lifecycle events.

  8. Configure Parent behavior with ParentConfig

    main

    The ParentConfig<T> interface allows you to customize how a specific parent container handles drag-and-drop operations.

    Common configuration options include:

    • accepts: A predicate (targetParentData, initialParentData, currentParentData, state) => boolean to determine if a parent accepts a specific node.
    • dragHandle: A CSS selector for a specific element within a node that acts as the drag trigger.
    • draggable: A function (child: HTMLElement) => boolean to determine if a specific element is draggable.
    • draggableValue: A function (values: T) => boolean to determine if a specific value is draggable.
    • group: A string used to group parents together, allowing nodes to be transferred between parents in the same group.
    • sortable: A boolean flag to enable or disable sorting within the parent.
    • dropZone: A boolean flag indicating if the parent itself acts as a drop zone.
    • dragEffectAllowed & dragDropEffect: Set the NativeDragEffects ("link" | "none" | "copy" | "move") for the operation.
    • draggingClass, dragPlaceholderClass, dropZoneClass, dropZoneParentClass: CSS classes applied during various stages of the drag lifecycle.
    • longPress & longPressDuration: Configuration for touch-based long-press interactions.
    • nativeDrag: If false, the library bypasses the native HTML5 Drag and Drop API in favor of synthetic pointer events.
    export interface ParentConfig<T> {
      accepts?: (
        targetParentData: ParentRecord<T>,
        initialParentData: ParentRecord<T>,
        currentParentData: ParentRecord<T>,
        state: BaseDragState<T>
      ) => boolean;
      dragEffectAllowed: NativeDragEffects;
      dragDropEffect: NativeDragEffects;
      dragImage?: (data: NodeDragEventData<T>, draggedNodes: Array<NodeRecord<T>>) => HTMLElement;
      disabled?: boolean;
      dragHandle?: string;
      externalDragHandle?: {
        el: HTMLElement;
        callback: () => HTMLElement;
      };
      draggable?: (child: HTMLElement) => boolean;
      draggableValue?: (values: T) => boolean;
      draggedNodes: (pointerDown: { parent: ParentRecord<T>; node: NodeRecord<T>; }) => Array<NodeRecord<T>>;
      draggingClass?: string;
      dragstartClasses: (node: NodeRecord<T>, nodes: Array<NodeRecord<T>>, config: ParentConfig<T>, isSynthDrag?: boolean) => void;
      dragPlaceholderClass?: string;
      dropSwapConfig?: DropSwapConfig<T>;
      dropZoneClass?: string;
      dropZoneParentClass?: string;
      dropZone?: boolean;
      group?: string;
      handleParentFocus: (data: ParentEventData<T>, state: BaseDragState<T>) => void;
      handleNodeKeydown: (data: NodeEventData<T>, state: DragState<T>) => void;
      handleDragend: (data: NodeDragEventData<T>, state: DragState<T>) => void;
      handleDragstart: (data: NodeDragEventData<T>, state: DragState<T>) => void;
      handleEnd: (state: DragState<T> | SynthDragState<T>) => void;
      handleNodeDrop: (data: NodeDragEventData<T>, state: DragState<T>) => void;
      handleNodePointerup: (data: NodePointerEventData<T>, state: DragState<T>) => void;
      handleParentScroll: (data: ParentEventData<T>, state: DragState<T> | BaseDragState<T> | SynthDragState<T>) => void;
      handleNodeDragenter: (data: NodeDragEventData<T>, state: DragState<T>) => void;
      handleNodeBlur: (data: NodeEventData<T>, state: DragState<T>) => void;
      handleNodeFocus: (data: NodeEventData<T>, state: DragState<T>) => void;
      handleNodeDragleave: (data: NodeEventData<T>, state: DragState<T>) => void;
      handleParentDragover: (data: ParentDragEventData<T>, state: DragState<T>) => void;
      handleParentDrop: (data: ParentDragEventData<T>, state: DragState<T>) => void;
      handleNodeDragover: (data: NodeDragEventData<T>, state: DragState<T>) => void;
      handlePointercancel: (data: NodeDragEventData<T> | NodePointerEventData<T>, state: DragState<T> | SynthDragState<T> | BaseDragState<T>) => void;
      handleNodePointerdown: (data: NodePointerEventData<T>, state: DragState<T>) => void;
      handleNodePointerover: (e: PointeroverNodeEvent<T>, state: SynthDragState<T>) => void;
      handleParentPointerover: (e: PointeroverParentEvent<T>, state: SynthDragState<T>) => void;
      insertConfig?: InsertConfig<T>;
      longPress?: boolean;
      longPressClass?: string;
      longPressDuration?: number;
      name?: string;
      multiDrag?: boolean;
      nativeDrag?: boolean;
      performSort: ({ parent, draggedNodes, targetNodes }: { parent: ParentRecord<T>; draggedNodes: Array<NodeRecord<T>>; targetNodes: Array<NodeRecord<T>>; }) => void;
      performTransfer: ({ currentParent, targetParent, initialParent, draggedNodes, initialIndex, state, targetNodes }: { currentParent: ParentRecord<T>; targetParent: ParentRecord<T>; initialParent: ParentRecord<T>; draggedNodes: Array<NodeRecord<T>>; initialIndex: number; state: BaseDragState<T> | DragState<T> | SynthDragState<T>; targetNodes: Array<NodeRecord<T>>; }) => void;
      plugins?: Array<DNDPlugin>;
      reapplyDragClasses: (node: Node, parentData: ParentData<T>) => void;
      remapFinished: (data: ParentData<T>) => void;
      root: Document | ShadowRoot;
      selectedClass?: string;
      setupNode: SetupNode;
      setupNodeRemap: SetupNode;
      tearDownNode: TearDownNode;
      tearDownNodeRemap: TearDownNode;
      threshold: { horizontal: number; vertical: number; };
      synthDraggingClass?: string;
      synthDragPlaceholderClass?: string;
      synthDropZoneClass?: string;
      sortable?: boolean;
      synthDropZoneParentClass?: string;
      synthDragImage?: (node: NodeRecord<T>, parent: ParentRecord<T>, e: PointerEvent, draggedNodes: Array<NodeRecord<T>>) => { dragImage: HTMLElement; offsetX?: number; offsetY?: number; };
      onSort?: SortEvent<T>;
      onTransfer?: TransferEvent<T>;
      onDragstart?: DragstartEvent<T>;
      onDragend?: DragendEvent<T>;
    }
  9. Configure animation settings with AnimationsConfig

    main

    When using the animations plugin, you can provide an animationsConfig object within your parent configuration to control the behavior of sort and transfer animations.

    Available options:

    • duration (number): The duration of the animation in milliseconds.
    • easing (string): The CSS easing function for sort/transfer animations. This accepts any value compatible with the Web Animations API (e.g., "ease-in" or "cubic-bezier(0.22, 1, 0.36, 1)"). Defaults to "ease-in-out".
    • remapFinished (function): A callback function that executes when the remapping process is finished.
    • yScale (number): A scale factor for the Y-axis during animations.
    • xScale (number): A scale factor for the X-axis during animations.
  10. Configure the dropOrSwap plugin

    main

    The DropSwapConfig<T> object allows you to customize how drag interactions are handled and how data is updated.

    Key configuration options include:

    • shouldSwap: A function that determines if a swap should occur instead of a drop. It receives sourceParent, targetParent, draggedNodes, targetNodes, and the current state.
    • onSort: A callback triggered when nodes are reordered within the same parent. It provides details about the parent, previousValues, nodes, values, draggedNodes, previousPosition, position, targetNodes, and state.
    • onTransfer: A callback triggered when nodes are moved from one parent to another. It provides sourceParent, targetParent, initialParent, draggedNodes, targetIndex, state, and targetNodes.
    • handleNodeDragover, handleParentDragover, handleNodePointerover, handleParentPointerover: Custom handlers for drag and pointer events that can override the default plugin behavior.
  11. Use the useDragAndDrop hook in SolidJS

    main

    The useDragAndDrop hook is the primary way to integrate drag-and-drop functionality into a SolidJS application. It manages the lifecycle of the drag-and-drop instance, handles cleanup when components unmount, and provides a reactive store for your list data.

    To use it, pass an initial array of values and an optional configuration object. The hook returns a tuple containing:

    1. A Setter to bind the parent element to the hook.
    2. An Accessor to access the reactive store of values.
    3. The Setter for the store to allow manual updates.
    4. An updateConfig function to dynamically change drag-and-drop options.

    You must call the returned setParent function (usually via a ref) to attach the drag-and-drop logic to a DOM element.