@rbuljan/jsort
v1.10.3
Published
A JavaScript library for sorting and reordering grid or list items.
Maintainers
Readme

JSort
Small yet powerful drag and drop sortable library with touch support, smooth animations, for a great UX.
NPM: @rbuljan/jsort
Demo & examples: JSort — Homepage
Features
- [ ] Multiple items select (coming soon)
- [x] No external dependencies
- [x] Smooth animations for a better UE/UX
- [x] Touch & Mouse support (with Pointer events)
- [x] Dual customization options (via instance argument or
data-jsortHTML attribute) - [x] Grab handler
- [x] Linked groups
- [x] Nested groups
- [x] Swap group items
- [x] Scroll-intent on touch devices
- [x] Dynamic items support (delegated events)
- [x] Scroll parent on near-edge drag-move
- [x] Respects selection on inner action elements
- [x] In-place animated sorting beta
Installation
npm install @rbuljan/jsortSyntax
const JSortInstance = new JSort(HTMLElement, { /* Options */ });Usage example
<ul id="list">
<li>1</li>
<li>2</li>
<li>
<div class="jsort-handler">✥</div>
3 Grab me by the handler
</li>
<li>4</li>
</ul>
<script type="module">
import JSort from '@rbuljan/jsort';
const jsortList = new JSort(document.querySelector("#list"), {
duration: 360,
// Other Options here
onGrab(data) {
console.log(this, data);
}
onMove(data) {
console.log(this, data);
}
onDrop(data) {
console.log(this, data);
}
});
</script>Tip:
Works with any parent Tag
Options
JSort(HTMLElement, options)
The second parameter accepts an options object to customize the sorting behavior and appearance.
| Option | Type | Default | Description |
| ---------------------- | ---------- | -------------------------- | ----------------------------------------------- |
| Event Callbacks | | | |
| onBeforeGrab(data) | function | | Called before item grab |
| onGrab(data) | function | | Called on item grab |
| onMove(data) | function | | Called on item move |
| onBeforeDrop(data) | function | | Called before itme drop |
| onDrop(data) | function | | Called on drop |
| onAnimationEnd() | function | | Called on drop animation end |
| Behavior | | | |
| group | string | "" | Group sortable parents |
| swap | boolean | false | Swap elements (instead of reordering) |
| parentDrop | boolean | true | Allow drop onto parent container |
| moveThreshold | number | 0 | Px before it's considered a pointer move |
| grabTimeout | number | 140 | Grab delay in ms (touch devices only) |
| Selectors | | | |
| selectorParent | string | ".jsort" | Parent container |
| selectorItems | string | "*" | Sortable items (parent's immediate children |
| selectorItemsIgnore | string | ".jsort-ignore" | Ignored sortable's immediate children |
| selectorIgnoreTarget | string | "" | Prevent grab on item descendant selectors |
| selectorIgnoreFields | string | Read Tip below | Prevent grab on item descendant action elements |
| selectorHandler | string | ".jsort-handler" | Drag handle selector |
| CSS Classes | | | |
| classGrab | string | "is-jsort-grab" | Grabbed item |
| classGhost | string | "is-jsort-ghost" | Ghost element |
| classTarget | string | "is-jsort-target" | Hovered target element |
| classAnimated | string | "is-jsort-animated" | All affected and animated items |
| classAnimatedDrop | string | "is-jsort-animated-drop" | Grabbed item during drop animation |
| classInvalid | string | "is-jsort-invalid" | Ghost element over invalid drop zones |
| classTouch | string | "is-jsort-touch" | Grabbed item if Event is touchstart (touch) |
| Animation | | | |
| duration | number | 420 | Sort animation duration in ms |
| easing | string | "cubic-bezier()" | Animation easing function |
| Ghost Styles | | | |
| scale | number | 1.1 | Ghost element scale |
| opacity | number | 0.8 | Ghost element opacity (0-1) |
| zIndex | number | 2147483647 | Ghost element z-index |
| Scroll | | | |
| scrollThreshold | number | 8 | Px before considering auto-scroll |
| edgeThreshold | number | 50 | Autoscroll distance to edge |
| scrollSpeed | number | 10 | Auto-scroll speed in pixels per step |
Tip:
By defafult, the selectorIgnoreFields provides a good sane selector in order to prevent grabbing an item if the direct target is an action element, like: Anchor, Button, Input, Contenteditable, etc:
/* DO NOT GRAB ITEM IF POINTER TARGET IS AN ACTION FIELD: */
:is(input, select, textarea, button, label, [contenteditable=""], [contenteditable="true"], [tabindex]:not([tabindex^="-"]), a[href]:not(a[href=""]), area[href]):not(:disabled)Tip:
Internally, the Options selectors will be concatenated like selectorParent > selectorItems:not( selectorItemsIgnore ) into i.e: .jsort > *:not(.jsort-ignore) (or like: .jsort > * if you pass an empty string to the selectorItemsIgnore). The > immediate child selector will always be injected by default.
Options example
const sortable = new JSort(document.querySelector("#mySortable"), {
selectorItems: ".item",
group: "shared-group",
swap: true,
duration: 300,
scale: 1.05,
onDrop: (data) => {
console.log(this, data); // JSort, data{}
},
// More options here...
});Tip:
Options (that are not callbacks) can be assigned directly in your HTML markup using the data-jsort attribute in this format option: value; option: value
<div id="mySortable" data-jsort="
group: a;
selectorItems: .item;
selectorHandler: .my-handler;
swap: true;
duration: 300;
easing: ease-out;
zIndex: 999;
parentDrop: false;
">
<div class="item"><div class="my-handler">✥</div>Item 1</div>
<div class="item"><div class="my-handler">✥</div>Item 2</div>
</div>just remember that data-jsort options will override any instance options you passed to the constructor (analogue to stylesheet vs. style="" attribute). Having that in consideration, you can define some JSort global, shared options from JavaScript, and customize specific elements using the data-jsort="" attribute, if needed.
Custom validation
To manually abort some actions depending on a condition, you can use the onBeforeGrab() and onBeforeDrop() callbacks
new JSort(myListElement, {
onBeforeGrab(data) {
if (data.indexGrab === 0) {
console.error("Cannot grab first item");
return false;
}
if (data.elGrab.closest(".no-grab")) {
console.error("Grabbed an invalid item");
return false;
}
},
onGrab(data) {
console.log(`Grabbed index ${data.indexGrab}`);
},
onBeforeDrop(data) {
if (data.indexDrop === 0) {
console.error("Cannot drop into first item");
return false;
}
if (data.elDrop.closest(".no-drop")) {
console.error("Cannot drop here");
return false;
}
},
onDrop(data) {
console.log(`Dropped index ${data.indexGrab} into ${data.indexDrop}`, this);
}
})If you returned false from one of the callbacks, the respective onGrab or onDrop actions will not be called.
Methods
| Method | Description |
| ------------------------- | ----------------------------------------------------- |
| init({/*options*/}) | Re-initialize the instance with updated Options |
| destroy() | Destroys the instance and removes the event listeners |
| insert(Element, Target) | Insert item at Target (parent or item) |
| sort(fn) | (Beta) In-place sort parent items (with animation) |
Properties
JSort
| Property | Type | Default | Description |
| ------------------ | --------------- | ------- | ------------------------------------------ |
| indexGrab | number | -1 | The index of the grabbed item |
| indexDrop | number | -1 | The new index on drop |
| elGrab | HTMLElement | null | The grabbed item |
| elGrabParent | HTMLElement | null | The grabbed item's parent |
| elGhost | HTMLElement | null | Element that follows the pointer |
| elTarget | HTMLElement | null | The hovered target (item or parent) |
| elDrop | HTMLElement | null | Same as elTarget but on drop |
| elDropParent | HTMLElement | null | The drop (target) item's parent on drop |
| affectedElements | HTMLElement[] | [] | Array of drop-affected (animated) elements |
Static Properties
JSort
| Property | Type | Description |
| --------- | -------- | ----------------------- |
| version | string | Current library version |
Linked Groups
JSort allows to drag & drop into a linked group by defining a group property
<div class="shared">
<div>A 1</div>
<div>A 2</div>
<div>A 3</div>
</div>
<div class="shared">
<div>B 1</div>
<div>B 2</div>
</div>
<script type="module">
import JSort from '@rbuljan/jsort';
document.querySelectorAll(".shared").forEach((el) => {
new JSort(el, {
group: "shared"
});
});
</script>or by adding the data-jsort attribute in HTML:
<div class="shared" data-jsort="group:shared">
<div>A 1</div>
<div>A 2</div>
<div>A 3</div>
</div>
<div class="shared" data-jsort="group:shared">
<div>B 1</div>
<div>B 2</div>
</div>
<script type="module">
import JSort from '@rbuljan/jsort';
document.querySelectorAll(".shared").forEach((el) => new JSort(el));
</script>Swap items
By default JSort reorders the items on drop. If instead you want to swap, you can set the swap option to true to your element or grouped elements:
<div class="players">
<div>Mark</div>
<div>Jack</div>
<div>Theo</div>
</div>
<div class="players">
<div>Luke</div>
<div>John</div>
<div>Roko</div>
</div>
<script type="module">
import JSort from '@rbuljan/jsort';
document.querySelectorAll(".players").forEach((el) => {
new JSort(el, {
group: "team",
swap: true,
})
});
</script>PS: Instead of using the constructor options, you can use the data-jsort:
<div class="players" data-jsort="group:team; swap:true">
<div>Mark</div>
<div>Jack</div>
<div>Theo</div>
</div>
<div class="players" data-jsort="group:team; swap:true">
<div>Luke</div>
<div>John</div>
<div>Roko</div>
</div>
<script type="module">
import JSort from '@rbuljan/jsort';
document.querySelectorAll(".players").forEach((el) => new JSort(el));
</script>Find more examples on JSort Homepage.
Styling
Is there a minimal CSS styling I might want to use to get started?
Yes! This is the minimal CSS styling you might want to use to get the best from JSort defaults:
/* JSort — Minimal suggested styles */
.is-jsort-active.is-jsort-touch {
outline: 2px solid currentColor; /* Useful hint on touch devices */
}
.is-jsort-grab {
opacity: 0; /* Dim the original grabbed element */
}
.is-jsort-target {
z-index: 1;
outline: 2px dashed currentColor;
}
.is-jsort-invalid {
outline: 2px solid red;
}The above was not hardcoded into the library since everyone wants to style their UI differently, i.e: set the grabbed element's opacity at a different value, change the targeted element styles, etc.
For custom styling JSort provides several classes you could use in your CSS to further style your UI: (See: Options → CSS Classes)
See the JSort Homepage for examples and inspiration.
Motivation
I needed a sortable library. After reviewing some popular ones like SortableJS, Dragula, jQueryUI/Sortable, and others, I found that they do not work the way I want, fast, smoothly, touch/mobile friendly. An important factor was also to minimize motion, only when necessary (on drop) and to animate all affected elements naturally and smoothly to support visual cognitive feedback and make the experience overall more natural and pleasant.
JSort was born to fill this necessity.
Licence: MIT
