Touch-Friendly JavaScript Drag and Drop Sortable Library – JSort

Touch-Friendly JavaScript Drag and Drop Sortable Library – JSort
Touch-Friendly JavaScript Drag and Drop Sortable Library – JSort
JSort is a JavaScript library that enables smooth, touch-enabled, drag-and-drop sorting for HTML lists and grids.

Features:

  • Zero dependencies: Pure vanilla JS with no external libraries required.
  • Touch and mouse support: Unified pointer events API handles all input methods.
  • Smooth animations: All displaced elements animate naturally during reordering.
  • Linked groups: Drag items between multiple containers sharing a group name.
  • Swap mode: Exchange positions instead of reordering the entire list.
  • Nested sortables: Parent and child containers can both be sortable.
  • Scroll-intent detection: On touch devices, brief taps trigger clicks while drags initiate sorting.
  • Action-element protection: Automatically ignores grabs on inputs, buttons, links, and contenteditable fields.
  • Edge scrolling: Containers scroll automatically when dragging near boundaries.
  • Delegated events: Works with dynamically added items without reinitialization.
  • Custom Handles: You can restrict the drag action to a specific “handle” element.

Use Cases

  • Dashboard widget rearrangement: Users reorder analytics cards across multiple dashboard sections with linked groups.
  • Mobile-first task managers: Touch-optimized list reordering with scroll-intent prevents accidental drags during vertical scrolling.
  • E-commerce product sorting: Admin interfaces let merchants reorder products within categories using drag handles.
  • Nested content builders: Page builders with sortable rows containing sortable columns require nested sortable support.
  • Team roster management: Swap-mode lets coaches exchange player positions between offense/defense groups without full reordering.

How To Use It:

1. Install the package via NPM.

npm install @rbuljan/jsort

2. Create your HTML structure. You can use any parent tag, such as a <ul> or a <div>.

<ul id="my-list">
  <li>Item 1</li>
  <li>Item 2</li>
  <li>
    <!-- Optional drag handle -->
    <span class="handle">::</span>
    Item 3
  </li>
  <li>Item 4</li>
</ul>

3. Initialize the library in your JavaScript file.

import JSort from '@rbuljan/jsort';
// Initialize the sortable list
// The first argument is the container element
// The second argument is the configuration object
const sortable = new JSort(document.getElementById("my-list"), {
  duration: 300,
  selectorHandler: '.handle', // Optional: restricts drag to the handle
  onDrop: (data) => {
    // Log the data to see the new index
    console.log('Item dropped:', data);
  }
});

4. Available configuration options. You can pass these options to the second argument of the JSort constructor:

  • group (string): Links multiple sortable containers. Items can be dragged between containers with matching group names. Default is empty string (no linking).
  • swap (boolean): Changes drop behavior to swap positions instead of reordering. Useful for ranked lists or team rosters. Default is false.
  • duration (number): Animation duration in milliseconds for the sort animation. Default is 420.
  • easing (string): CSS easing function for animations. Default is “cubic-bezier(0.6, 0, 0.6, 1)”.
  • scale (number): Scale factor for the ghost element that follows the pointer. Default is 1.1.
  • opacity (number): Opacity value for the ghost element, ranging from 0 to 1. Default is 0.8.
  • grabTimeout (number): Delay in milliseconds before grab activates on touch devices. This allows scroll-intent detection. Default is 140. Has no effect on mouse events.
  • parentDrop (boolean): Allows dropping items directly onto the parent container instead of between items. Default is true.
  • moveThreshold (number): Distance in pixels the pointer must move before drag activates. Prevents accidental drags when clicking links or buttons. Default is 0.
  • scrollThreshold (number): Distance in pixels to consider touch movement as scrolling instead of dragging. Default is 8.
  • edgeThreshold (number): Distance in pixels from scrollable container edge to trigger auto-scroll. Default is 50.
  • scrollSpeed (number): Scroll velocity in pixels per animation frame during edge auto-scroll. Default is 10.
  • zIndex (number): Z-index value for the ghost element. Default is 2147483647 (maximum 32-bit signed integer).
  • selectorParent (string): CSS selector for parent sortable containers. Default is “.jsort”.
  • selectorItems (string): CSS selector for sortable items within the parent. Uses immediate children by default. Default is “*”.
  • selectorItemsIgnore (string): CSS selector for ignored children that should not be sortable. Default is “.jsort-ignore”.
  • selectorHandler (string): CSS selector for drag handle elements within items. Default is “.jsort-handler”.
  • selectorIgnoreTarget (string): CSS selector for item descendants that should prevent grab when clicked. Default is empty string.
  • selectorIgnoreFields (string): CSS selector for action elements that should prevent grab. Default targets inputs, buttons, links, and other interactive elements.
  • classGhost (string): Class name applied to the ghost element. Default is “is-jsort-ghost”.
  • classActive (string): Class name applied on pointer down. Default is “is-jsort-active”.
  • classTouch (string): Class name applied only on touch events. Default is “is-jsort-touch”.
  • classGrab (string): Class name applied to the grabbed item. Default is “is-jsort-grab”.
  • classTarget (string): Class name applied to the hovered target element. Default is “is-jsort-target”.
  • classAnimated (string): Class name applied to all animated elements during drop. Default is “is-jsort-animated”.
  • classAnimatedDrop (string): Class name applied to the grabbed item during drop animation. Default is “is-jsort-animated-drop”.
  • classInvalid (string): Class name applied to ghost element over invalid drop zones. Default is “is-jsort-invalid”.
  • onBeforeGrab (function): Callback invoked before grab activates. Return false to cancel the grab operation.
  • onGrab (function): Callback invoked when item grab activates.
  • onMove (function): Callback invoked during pointer movement while dragging.
  • onBeforeDrop (function): Callback invoked before drop completes. Return false to cancel the drop operation.
  • onDrop (function): Callback invoked after successful drop.
  • onAnimationEnd (function): Callback invoked when drop animation completes.
const sortable: new JSort(document.getElementById("my-list"), {
  elGrabParent: el,
  group: "",
  swap: false,
  duration: 420,
  easing: "cubic-bezier(0.6, 0, 0.6, 1)",
  scale: 1.1,
  opacity: 0.8,
  grabTimeout: 140,
  parentDrop: true,
  moveThreshold: 0,
  scrollThreshold: 8,
  edgeThreshold: 50,
  scrollSpeed: 10,
  zIndex: 2147483647 // 0x7FFFFFFF,
  selectorParent: ".jsort",
  selectorItems: "*",
  selectorItemsIgnore: ".jsort-ignore",
  selectorHandler: ".jsort-handler",
  selectorIgnoreTarget: "",
  selectorIgnoreFields: `:is(input, select, textarea, button, label, [contenteditable=""], [contenteditable="true"], [tabindex]:not([tabindex^="-"]), a[href]:not(a[href=""]), area[href]):not(:disabled)`,
  classGhost: "is-jsort-ghost",
  classActive: "is-jsort-active",
  classTouch: "is-jsort-touch",
  classGrab: "is-jsort-grab",
  classTarget: "is-jsort-target",
  classAnimated: "is-jsort-animated",
  classAnimatedDrop: "is-jsort-animated-drop",
  classInvalid: "is-jsort-invalid",
  onBeforeGrab: () => { },
  onGrab: () => { },
  onMove: () => { },
  onBeforeDrop: () => { },
  onDrop: () => { },
  onAnimationEnd: () => { },
}),

5. Or define them directly in HTML using the data-jsort attribute as follows:

The data-jsort format uses semicolons to separate options. Use colons to separate option names from values. The library automatically parses numbers and booleans from string values.

<div id="playerRoster" data-jsort="
  group: team-a;
  selectorItems: .player;
  selectorHandler: .handle;
  swap: true;
  duration: 300;
  easing: ease-out;
  zIndex: 999;
  parentDrop: false;
">
  <div class="player">
    <div class="handle">⋮⋮</div>
    <span>Player One</span>
  </div>
  <div class="player">
    <div class="handle">⋮⋮</div>
    <span>Player Two</span>
  </div>
</div>
new JSort(document.querySelector("#playerRoster"));

6. JSort provides methods to control the sortable instance programmatically:

// Re-initialize with new options
sortableInstance.init({
  duration: 500,
  swap: true
});

// Clean up and remove event listeners
sortableInstance.destroy();

// Programmatically insert an item
const newItem = document.createElement('div');
newItem.textContent = 'New Task';
sortableInstance.insert(newItem, targetElement);

// Sort items with animation (Beta feature)
sortableInstance.sort((a, b) => {
  // Sort alphabetically by text content
  return a.textContent.localeCompare(b.textContent);
});

7. Event handlers.

new JSort(document.querySelector("#myList"), {

  // Called before grab activates (return false to cancel)
  onBeforeGrab(data) {
    console.log('Before grab:', data.elGrab, data.indexGrab);
    console.log('Event:', data.event);
    // Access instance properties via 'this'
    console.log('Parent element:', this.elGrabParent);
  },

  // Called when grab activates
  onGrab(data) {
    console.log('Grabbed element:', data.elGrab);
    console.log('From parent:', data.elGrabParent);
    console.log('Original index:', data.indexGrab);
  },

  // Called during pointer movement
  onMove(data) {
    console.log('Moving over:', data.elTarget);
    console.log('Valid target:', data.isValidTarget);
    console.log('Ghost element:', data.elGhost);
  },

  // Called before drop completes (return false to cancel)
  onBeforeDrop(data) {
    console.log('Attempting drop at index:', data.indexDrop);
    console.log('Is valid:', data.isValidTarget);
    console.log('Same parent:', data.isSameParent);
  },

  // Called after successful drop
  onDrop(data) {
    console.log('Moved from', data.indexGrab, 'to', data.indexDrop);
    console.log('Dropped element:', data.elDrop);
    console.log('New parent:', data.elDropParent);
    console.log('Affected elements:', this.affectedElements);
  },

  // Called when animation completes
  onAnimationEnd() {
    console.log('Animation finished');
    // All affected elements have reached final positions
  }
  
});

8. Access instance properties to read the current drag state or affected elements.

const sortable = new JSort(document.querySelector("#list"));

// During drag operations:
console.log(sortable.indexGrab);        // Index of grabbed item (-1 when not dragging)
console.log(sortable.indexDrop);        // Target drop index (-1 when not dragging)
console.log(sortable.elGrab);           // Grabbed element reference
console.log(sortable.elGrabParent);     // Grabbed item's parent container
console.log(sortable.elGhost);          // Ghost element that follows pointer
console.log(sortable.elTarget);         // Currently hovered target
console.log(sortable.elDrop);           // Final drop target element
console.log(sortable.elDropParent);     // Drop target's parent container
console.log(sortable.affectedElements); // Array of elements moved by the drop

9. JSort works without CSS but benefits from minimal styling for visual feedback. Add these styles to improve the user experience.

/* Highlight active item on touch devices */
.is-jsort-active.is-jsort-touch {
  outline: 2px solid currentColor;
}

/* Hide the original grabbed element */
.is-jsort-grab {
  opacity: 0;
}

/* Highlight valid drop targets */
.is-jsort-target {
  z-index: 1;
  outline: 2px dashed currentColor;
}

/* Show invalid drop zones */
.is-jsort-invalid {
  outline: 2px solid red;
}

Alternatives

  • Sortable: The most popular drag-and-drop library with multi-drag and plugin system.
  • Dragula: Simple drag-and-drop library with minimal configuration.
  • Shopify Draggable: A modular drag-and-drop library.

FAQs:

Q: How do I prevent users from dragging specific items?
A: Add the selectorItemsIgnore class to items you want to exclude. Configure this through the options object or use a custom class name. You can also use the onBeforeGrab callback to conditionally prevent grabs based on element properties or state.

Q: Can I use JSort with dynamically added items?
A: Yes. JSort uses event delegation on the parent container. Items added to the DOM after initialization work automatically without re-initialization.

Q: How do I persist the new order after users reorder items?
A: Use the onDrop callback to capture the new order. Query all items and extract their IDs or data attributes, then send this data to your server.

Q: Why isn’t my list scrolling when I drag to the edge?
A: The parent container must have a defined height and overflow: auto or overflow: scroll in your CSS. JSort detects the scrollable parent automatically.

The post Touch-Friendly JavaScript Drag and Drop Sortable Library – JSort appeared first on CSS Script.


Discover more from RSS Feeds Cloud

Subscribe to get the latest posts sent to your email.

Discover more from RSS Feeds Cloud

Subscribe now to keep reading and get access to the full archive.

Continue reading