@rbuljan/jsort
Version:
A JavaScript library for sorting and reordering grid or list items.
387 lines (311 loc) • 16.1 kB
Markdown

# JSort
Small yet powerful drag and drop sortable library with touch support, smooth animations, for a great UX.
[](https://www.npmjs.com/package/@rbuljan/jsort)
[](https://github.com/rokobuljan/jsort)
[](https://github.com/rokobuljan/jsort)
[](https://rokobuljan.github.io/jsort/)
<br>
NPM: [@rbuljan/jsort](https://www.npmjs.com/package/@rbuljan/jsort)
Demo & examples: [JSort — Homepage](https://rokobuljan.github.io/jsort/)
## 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-jsort` HTML 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 <sup>beta</sup>
## Installation
```bash
npm install @rbuljan/jsort
```
## Syntax
```js
const JSortInstance = new JSort(HTMLElement, { /* Options */ });
```
## Usage
Add class `class="jsort"` to your HTML element, and instantiate `JSort` with the desired Options
```html
<ul class="jsort" 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,
onGrab(data) {
console.log(this, data);
}
onMove(data) {
console.log(this, data);
}
onDrop(data) {
console.log(this, data);
}
});
</script>
```
## 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:
```css
/* 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.
### Example
```javascript
const sortable = new JSort(document.querySelector('.my-list'), {
group: 'shared-group',
duration: 300,
scale: 1.05,
onDrop: (data) => {
console.log(this, data); // JSort, data{}
}
});
```
***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`
```html
<ul class="jsort" data-jsort="
group: a;
selectorItems: .item;
selectorHandler: .my-handler;
swap: true;
duration: 300;
easing: ease-out;
zIndex: 999;
parentDrop: false;
">
<li class="item"><div class="my-handler">✥</div>Item 1</li>
<li class="item"><div class="my-handler">✥</div>Item 2</li>
</ul>
```
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
```js
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() {
console.log(`Grabbed index ${this.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() {
console.log(`Dropped index ${this.indexGrab} into ${this.indexDrop}`);
}
})
```
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
```html
<div class="jsort group-a">
<div>A 1</div>
<div>A 2</div>
<div>A 3</div>
</div>
<div class="jsort group-a">
<div>B 1</div>
<div>B 2</div>
</div>
<script type="module">
import JSort from '@rbuljan/jsort';
document.querySelectorAll(".group-a").forEach((el) => {
new JSort(el, {
group: "a"
});
});
</script>
```
or by adding the `data-jsort` attribute in HTML:
<details>
<summary>View code</summary>
```html
<div class="jsort" data-jsort="group:a">
<div>A 1</div>
<div>A 2</div>
<div>A 3</div>
</div>
<div class="jsort" data-jsort="group:a">
<div>B 1</div>
<div>B 2</div>
</div>
<script type="module">
import JSort from '@rbuljan/jsort';
document.querySelectorAll(".jsort").forEach((el) => new JSort(el));
</script>
```
</details>
## 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:
```html
<div class="jsort">
<div>Mark</div>
<div>Jack</div>
<div>Theo</div>
</div>
<div class="jsort">
<div>Luke</div>
<div>John</div>
<div>Roko</div>
</div>
<script type="module">
import JSort from '@rbuljan/jsort';
document.querySelectorAll(".jsort").forEach((el) => {
new JSort(el, {
group: "players",
swap: true,
})
});
</script>
```
PS: Instead of using the constructor options, you can use the data-jsort:
<details>
<summary>View code</summary>
```html
<div class="jsort" data-jsort="group:players; swap:true">
<div>Mark</div>
<div>Jack</div>
<div>Theo</div>
</div>
<div class="jsort" data-jsort="group:players; swap:true">
<div>Luke</div>
<div>John</div>
<div>Roko</div>
</div>
<script type="module">
import JSort from '@rbuljan/jsort';
document.querySelectorAll(".jsort").forEach((el) => new JSort(el));
</script>
```
</details>
## 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:
```css
/* 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](https://rokobuljan.github.io/jsort/) 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](https://github.com/rokobuljan/jsort)