unsortable 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.js +202 -0
- package/lib/utils.js +51 -0
- package/package.json +52 -0
- package/svelte/index.js +12 -0
- package/vue/index.js +26 -0
package/lib/index.js
ADDED
@@ -0,0 +1,202 @@
|
|
1
|
+
import { DragDropManager, Draggable, Droppable } from '@dnd-kit/dom'
|
2
|
+
import { getNearestParentElementFromMap, hasValue, move, toArrayAccessors, toItemAccessor } from './utils'
|
3
|
+
|
4
|
+
/**
|
5
|
+
* @typedef {Object} UnsortableOptions
|
6
|
+
* @property {boolean} [autoAttach=true] - Whether to automatically attach the Unsortable instance to the drag manager.
|
7
|
+
* @property {import('@dnd-kit/dom').DragDropManagerInput} [managerOptions={}] - Options for the DragDropManager instance.
|
8
|
+
* @property {import('@dnd-kit/dom').DragDropManager} [manager=undefined] - An instance of DragDropManager to use. If not provided, a new instance will be created.
|
9
|
+
*/
|
10
|
+
|
11
|
+
/**
|
12
|
+
* @type {UnsortableOptions}
|
13
|
+
*/
|
14
|
+
const defaultOptions = {
|
15
|
+
autoAttach: true,
|
16
|
+
}
|
17
|
+
|
18
|
+
const containerMap = new WeakMap()
|
19
|
+
const itemMap = new WeakMap()
|
20
|
+
|
21
|
+
export class Unsortable {
|
22
|
+
/**
|
23
|
+
* @param {UnsortableOptions=} options - Configuration options for Unsortable.
|
24
|
+
*/
|
25
|
+
constructor(options) {
|
26
|
+
this.options = { ...defaultOptions, ...options }
|
27
|
+
this.manager = options?.manager || new DragDropManager(options?.managerOptions)
|
28
|
+
this.addDraggable = this.addDraggable.bind(this)
|
29
|
+
this.addDroppable = this.addDroppable.bind(this)
|
30
|
+
this.addHandle = this.addHandle.bind(this)
|
31
|
+
this._onDragOver = this._onDragOver.bind(this)
|
32
|
+
|
33
|
+
if (this.options.autoAttach) this.attach()
|
34
|
+
}
|
35
|
+
|
36
|
+
attach() {
|
37
|
+
console.debug('Unsortable: attaching to drag manager')
|
38
|
+
this.manager.monitor.addEventListener('dragover', this._onDragOver)
|
39
|
+
}
|
40
|
+
|
41
|
+
destroy() {
|
42
|
+
console.debug('Unsortable: destroying')
|
43
|
+
this.manager.destroy()
|
44
|
+
}
|
45
|
+
|
46
|
+
_onDragOver(event) {
|
47
|
+
if (!event.operation.target) return
|
48
|
+
console.debug('Unsortable: drag over event', event)
|
49
|
+
event.operation.source.element['style'].display = 'block'
|
50
|
+
|
51
|
+
if (event.operation.target.data.isContainer) return this.handleMove(event)
|
52
|
+
else return this.handleSort(event)
|
53
|
+
}
|
54
|
+
|
55
|
+
handleMove(event) {
|
56
|
+
console.debug('Unsortable: handling move', event, this.manager)
|
57
|
+
const source = getNearestParentElementFromMap(event.operation.source.element, containerMap)
|
58
|
+
|
59
|
+
const sourceItem = event.operation.source.data.item()
|
60
|
+
const sourceItems = source.items.get()
|
61
|
+
const setSourceItems = source.items.set
|
62
|
+
|
63
|
+
const targetItems = event.operation.target.data.items.get()
|
64
|
+
const setTargetItems = event.operation.target.data.items.set
|
65
|
+
|
66
|
+
const isAlreadyInTargetItems = targetItems.includes(sourceItem)
|
67
|
+
if (isAlreadyInTargetItems) return
|
68
|
+
|
69
|
+
// update the source items
|
70
|
+
setSourceItems(sourceItems.filter((item) => item !== sourceItem))
|
71
|
+
setTargetItems([...targetItems, sourceItem])
|
72
|
+
}
|
73
|
+
|
74
|
+
handleSort(event) {
|
75
|
+
console.debug('Unsortable: handling sort', event)
|
76
|
+
const source = getNearestParentElementFromMap(event.operation.source.element, containerMap)
|
77
|
+
const target = getNearestParentElementFromMap(event.operation.target.element, containerMap)
|
78
|
+
|
79
|
+
const sourceItem = event.operation.source.data.item()
|
80
|
+
const sourceItems = source.items.get()
|
81
|
+
const targetItem = event.operation.target.data.item()
|
82
|
+
const targetItems = target.items.get()
|
83
|
+
|
84
|
+
if (sourceItem === targetItem) {
|
85
|
+
console.debug('Unsortable: source item is the same as target item, no action taken')
|
86
|
+
return
|
87
|
+
}
|
88
|
+
const isSameList = sourceItems === targetItems
|
89
|
+
|
90
|
+
const oldIndex = sourceItems.indexOf(sourceItem)
|
91
|
+
const newIndex = targetItems.indexOf(targetItem)
|
92
|
+
|
93
|
+
console.debug('Unsortable: oldIndex', oldIndex, 'newIndex', newIndex)
|
94
|
+
|
95
|
+
if (isSameList) {
|
96
|
+
source.items.set([...move([...sourceItems], oldIndex, newIndex)])
|
97
|
+
return
|
98
|
+
}
|
99
|
+
|
100
|
+
// If the items are in different lists, we need to remove the item from the old list and add it to the new list
|
101
|
+
// check we're not moving the item into its own child
|
102
|
+
if (newIndex !== -1 && !hasValue(sourceItem, targetItems)) {
|
103
|
+
source.items.set(sourceItems.filter((item) => item !== sourceItem))
|
104
|
+
const _targetItems = [...targetItems]
|
105
|
+
_targetItems.splice(newIndex, 0, sourceItem)
|
106
|
+
target.items.set(_targetItems)
|
107
|
+
}
|
108
|
+
}
|
109
|
+
|
110
|
+
/**
|
111
|
+
* Adds a draggable element to the Unsortable instance.
|
112
|
+
* @template T
|
113
|
+
* @param {HTMLElement} element
|
114
|
+
* @param {Object} options
|
115
|
+
* @param {import('@dnd-kit/dom').DraggableInput['type']=} [options.type] - The type of the draggable element.
|
116
|
+
* @param {import('@dnd-kit/dom').DroppableInput['accept']=} [options.accept] - The accepted types for the droppable element.
|
117
|
+
* @param {import('@dnd-kit/dom').DraggableInput=} options.draggableOptions - Options for the Draggable instance.
|
118
|
+
* @param {import('@dnd-kit/dom').DroppableInput=} options.droppableOptions - Options for the Droppable instance.
|
119
|
+
* @param {T | (()=>T) } options.item - An accessor for the item associated with the draggable.
|
120
|
+
*/
|
121
|
+
addDraggable(element, options) {
|
122
|
+
options.item = toItemAccessor(options.item)
|
123
|
+
|
124
|
+
console.debug('unsortable: draggable options', options)
|
125
|
+
const draggable = new Draggable(
|
126
|
+
{
|
127
|
+
type: options.type,
|
128
|
+
...options?.draggableOptions,
|
129
|
+
id: options.item(),
|
130
|
+
element,
|
131
|
+
data: { ...options, ...options?.draggableOptions?.data, isContainer: false },
|
132
|
+
},
|
133
|
+
this.manager,
|
134
|
+
)
|
135
|
+
|
136
|
+
const droppable = new Droppable(
|
137
|
+
{
|
138
|
+
accept: options.accept || options.type,
|
139
|
+
...options?.droppableOptions,
|
140
|
+
id: options.item(),
|
141
|
+
element,
|
142
|
+
data: { ...options, ...options?.droppableOptions?.data, isContainer: false },
|
143
|
+
},
|
144
|
+
this.manager,
|
145
|
+
)
|
146
|
+
|
147
|
+
itemMap.set(element, { ...options, draggable, droppable })
|
148
|
+
|
149
|
+
return {
|
150
|
+
draggable,
|
151
|
+
droppable,
|
152
|
+
options,
|
153
|
+
destroy() {
|
154
|
+
draggable.destroy()
|
155
|
+
droppable.destroy()
|
156
|
+
},
|
157
|
+
}
|
158
|
+
}
|
159
|
+
|
160
|
+
/**
|
161
|
+
* Adds a draggable element to the Unsortable instance.
|
162
|
+
* @template {any[]} T
|
163
|
+
* @param {HTMLElement} element
|
164
|
+
* @param {Object} options
|
165
|
+
* @param {import('@dnd-kit/dom').DroppableInput['accept']=} [options.accept] - The accepted types for the droppable element.
|
166
|
+
* @param {import('@dnd-kit/dom').DroppableInput=} options.droppableOptions - Options for the Droppable instance.
|
167
|
+
* @param {T | (() => T) | { set: ((T) => void), get: (() => T)} } options.items - An accessor for the items associated with the droppable.
|
168
|
+
* @param {(T) => void=} [options.setItems] - A function to set the items in the droppable.
|
169
|
+
*/
|
170
|
+
addDroppable(element, options) {
|
171
|
+
options.items = toArrayAccessors(options.items)
|
172
|
+
options.items.set = options.setItems || options.items.set
|
173
|
+
|
174
|
+
const droppable = new Droppable(
|
175
|
+
{
|
176
|
+
...options?.droppableOptions,
|
177
|
+
id: options.items.get(),
|
178
|
+
element,
|
179
|
+
accept: options.accept,
|
180
|
+
data: { ...options, ...options?.droppableOptions?.data, isContainer: true },
|
181
|
+
},
|
182
|
+
this.manager,
|
183
|
+
)
|
184
|
+
|
185
|
+
containerMap.set(element, options)
|
186
|
+
|
187
|
+
return {
|
188
|
+
droppable,
|
189
|
+
options,
|
190
|
+
destroy() {
|
191
|
+
droppable.destroy()
|
192
|
+
},
|
193
|
+
}
|
194
|
+
}
|
195
|
+
|
196
|
+
addHandle(element) {
|
197
|
+
requestAnimationFrame(() => {
|
198
|
+
const nearest = getNearestParentElementFromMap(element, itemMap)
|
199
|
+
nearest.draggable.handle = element
|
200
|
+
})
|
201
|
+
}
|
202
|
+
}
|
package/lib/utils.js
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
/**
|
2
|
+
* Moves an element within an array from one index to another.
|
3
|
+
*
|
4
|
+
* @template {any[]} T
|
5
|
+
* @param {T} arr - The array to modify.
|
6
|
+
* @param {number} from - The index of the element to move.
|
7
|
+
* @param {number} to - The index to move the element to.
|
8
|
+
* @returns {T} The modified array with the element moved.
|
9
|
+
*/
|
10
|
+
export const move = (arr, from, to) => {
|
11
|
+
arr.splice(to, 0, arr.splice(from, 1)[0])
|
12
|
+
return arr
|
13
|
+
}
|
14
|
+
|
15
|
+
/**
|
16
|
+
* Checks if a target value exists within an object or its nested structures.
|
17
|
+
*
|
18
|
+
* @param {any} obj - The object to search within.
|
19
|
+
* @param {any} target - The value to search for.
|
20
|
+
* @returns {boolean} True if the target value is found, otherwise false.
|
21
|
+
*/
|
22
|
+
export const hasValue = (obj, target) => {
|
23
|
+
if (obj === target) return true
|
24
|
+
if (Array.isArray(obj)) return obj.some((v) => hasValue(v, target))
|
25
|
+
if (obj && typeof obj === 'object') return Object.values(obj).some((v) => hasValue(v, target))
|
26
|
+
return false
|
27
|
+
}
|
28
|
+
|
29
|
+
export const getNearestParentElementFromMap = (element, map) => {
|
30
|
+
let parent = element.parentElement
|
31
|
+
while (parent) {
|
32
|
+
if (map.has(parent)) {
|
33
|
+
return map.get(parent)
|
34
|
+
}
|
35
|
+
parent = parent.parentElement
|
36
|
+
}
|
37
|
+
return null
|
38
|
+
}
|
39
|
+
|
40
|
+
export const toArrayAccessors = (arr) => {
|
41
|
+
const mappedArr = Array.isArray(arr)
|
42
|
+
? { get: () => arr, set: (items) => arr.splice(0, items.length, ...items) }
|
43
|
+
: typeof arr === 'function'
|
44
|
+
? { get: arr, set: (items) => arr().splice(0, items.length, ...items) }
|
45
|
+
: arr
|
46
|
+
return mappedArr
|
47
|
+
}
|
48
|
+
|
49
|
+
export function toItemAccessor(item) {
|
50
|
+
return typeof item === 'function' ? item : () => item
|
51
|
+
}
|
package/package.json
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
{
|
2
|
+
"name": "unsortable",
|
3
|
+
"version": "0.0.1",
|
4
|
+
"description": "Headless drag-and-drop library for nested sorting using state instead of DOM mutations.",
|
5
|
+
"main": "lib/index.js",
|
6
|
+
"type": "module",
|
7
|
+
"private": false,
|
8
|
+
"publishConfig": {
|
9
|
+
"access": "public"
|
10
|
+
},
|
11
|
+
"exports": {
|
12
|
+
".": "./lib/index.js",
|
13
|
+
"./svelte": "./svelte/index.js",
|
14
|
+
"./vue": "./vue/index.js"
|
15
|
+
},
|
16
|
+
"keywords": [
|
17
|
+
"drag-and-drop",
|
18
|
+
"sortable",
|
19
|
+
"nested",
|
20
|
+
"headless",
|
21
|
+
"state-driven",
|
22
|
+
"reactive",
|
23
|
+
"sort",
|
24
|
+
"reorder",
|
25
|
+
"javascript",
|
26
|
+
"library",
|
27
|
+
"UI logic",
|
28
|
+
"Svelte",
|
29
|
+
"SvelteKit",
|
30
|
+
"agnostic",
|
31
|
+
"vanilla js"
|
32
|
+
],
|
33
|
+
"author": "jakobrosenberg@gmail.com",
|
34
|
+
"license": "ISC",
|
35
|
+
"dependencies": {
|
36
|
+
"@dnd-kit/dom": "^0.1.18"
|
37
|
+
},
|
38
|
+
"optionalDependencies": {
|
39
|
+
"svelte": "^5.0.0",
|
40
|
+
"vue": "^3.0.0"
|
41
|
+
},
|
42
|
+
"release-it": {
|
43
|
+
"git": false,
|
44
|
+
"plugins": {
|
45
|
+
"release-it-pnpm": {}
|
46
|
+
}
|
47
|
+
},
|
48
|
+
"scripts": {
|
49
|
+
"release": "release-it",
|
50
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
51
|
+
}
|
52
|
+
}
|
package/svelte/index.js
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
import { Unsortable } from '../lib'
|
2
|
+
import { onDestroy } from 'svelte'
|
3
|
+
|
4
|
+
class UnsortableSvelte extends Unsortable {
|
5
|
+
/** @param {ConstructorParameters<typeof Unsortable>[0]=} options */
|
6
|
+
constructor(options) {
|
7
|
+
super(options)
|
8
|
+
onDestroy(() => this.destroy())
|
9
|
+
}
|
10
|
+
}
|
11
|
+
|
12
|
+
export { UnsortableSvelte as Unsortable }
|
package/vue/index.js
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
import { Unsortable } from '../lib'
|
2
|
+
import { onUnmounted } from 'vue'
|
3
|
+
|
4
|
+
/**
|
5
|
+
* @typedef {Object} UnsortableVueOptions
|
6
|
+
* @property {boolean} [autoAttach=true] - Whether to automatically attach the Unsortable instance to the drag manager.
|
7
|
+
*/
|
8
|
+
|
9
|
+
/**
|
10
|
+
* @type {UnsortableVueOptions}
|
11
|
+
*/
|
12
|
+
const defaultOptions = {
|
13
|
+
autoAttach: true,
|
14
|
+
}
|
15
|
+
|
16
|
+
class UnsortableSvelte extends Unsortable {
|
17
|
+
/**
|
18
|
+
* @param {UnsortableVueOptions=} options - Configuration options for Unsortable.
|
19
|
+
*/
|
20
|
+
constructor(options) {
|
21
|
+
super({ ...defaultOptions, ...options })
|
22
|
+
onUnmounted(() => this.destroy())
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
26
|
+
export { UnsortableSvelte as Unsortable }
|