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 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
+ }
@@ -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 }