@urbanstudio/ua-sortable 1.0.0
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/CHANGELOG.md +27 -0
- package/LICENSE +21 -0
- package/README.md +254 -0
- package/dist/ua-sortable.js +356 -0
- package/package.json +50 -0
- package/src/Sortable.js +583 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@urbanstudio/ua-sortable` will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
6
|
+
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## [1.0.0] — 2026-06-05
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Initial release
|
|
14
|
+
- `UA_Sortable` class with full API (options, callbacks, instance + static methods)
|
|
15
|
+
- Pointer Events based drag (mouse, touch, stylus — no separate handling)
|
|
16
|
+
- `direction: "auto"` — detects `flex-direction` via `getComputedStyle`
|
|
17
|
+
- Cross-list drag via `group` option
|
|
18
|
+
- `confirm` callback for cross-list drops with async support
|
|
19
|
+
- `onSort`, `onMove`, `onDragStart`, `onDragEnd` callbacks
|
|
20
|
+
- `handle`, `filter`, `delay`, `delayOnTouchOnly` options
|
|
21
|
+
- `UA_Sortable.get()`, `getGroup()`, `snapshot()` static methods
|
|
22
|
+
- `UA_Sortable.initAll()` and `UA_Sortable.observe()` for declarative HTML API
|
|
23
|
+
- `data-ua-sortable` attribute + `data-sortable-*` attributes for HTML init
|
|
24
|
+
- `uaMakeSortable()` shorthand function
|
|
25
|
+
- Auto-injected minimal CSS (`ua-sortable-ghost`, `ua-drag-handle`, etc.)
|
|
26
|
+
- MutationObserver on container for auto-refresh on child changes
|
|
27
|
+
- ESM build (`src/Sortable.js`) + IIFE/CJS build (`dist/ua-sortable.js`)
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marian Feiler, urbanstudio GmbH (https://urbanstudio.de)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# ua-sortable
|
|
2
|
+
|
|
3
|
+
**Pointer-Events-based drag-and-drop sorting for lists and grids.**
|
|
4
|
+
No dependencies. No jQuery. No CDN required. Pure browser APIs.
|
|
5
|
+
|
|
6
|
+
[](https://www.npmjs.com/package/@urbanstudio/ua-sortable)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](https://cdn.jsdelivr.net/npm/@urbanstudio/ua-sortable/src/Sortable.js)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## What it does
|
|
13
|
+
|
|
14
|
+
`UA_Sortable` makes any list or grid container sortable by drag-and-drop using native [Pointer Events](https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events). Works with mouse, touch, and stylus — no special handling needed.
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
ua-sortable run myList
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
That is: one line to get a sortable list, one callback to persist the order, zero dependencies.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
**npm:**
|
|
27
|
+
```sh
|
|
28
|
+
npm install @urbanstudio/ua-sortable
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**CDN (jsDelivr — ESM):**
|
|
32
|
+
```html
|
|
33
|
+
<script type="module">
|
|
34
|
+
import { UA_Sortable } from "https://cdn.jsdelivr.net/npm/@urbanstudio/ua-sortable/src/Sortable.js";
|
|
35
|
+
</script>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**CDN (jsDelivr — browser global `<script>`):**
|
|
39
|
+
```html
|
|
40
|
+
<script src="https://cdn.jsdelivr.net/npm/@urbanstudio/ua-sortable/dist/ua-sortable.js"></script>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**CDN (unpkg):**
|
|
44
|
+
```html
|
|
45
|
+
<script src="https://unpkg.com/@urbanstudio/ua-sortable/dist/ua-sortable.js"></script>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Quick start
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
import { UA_Sortable } from "@urbanstudio/ua-sortable";
|
|
54
|
+
|
|
55
|
+
new UA_Sortable(document.querySelector("ul"), {
|
|
56
|
+
handle: ".drag-handle",
|
|
57
|
+
animation: 150,
|
|
58
|
+
onSort: (ids) => console.log("new order:", ids),
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Or with the shorthand:
|
|
63
|
+
```js
|
|
64
|
+
import { uaMakeSortable } from "@urbanstudio/ua-sortable";
|
|
65
|
+
|
|
66
|
+
const sortable = uaMakeSortable(document.querySelector("ul"), {
|
|
67
|
+
handle: ".drag-handle",
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Or declarative HTML — auto-initialized by `UA_Sortable.observe()`:
|
|
72
|
+
```html
|
|
73
|
+
<ul data-ua-sortable data-sortable-handle=".drag-handle" data-sortable-group="tasks">
|
|
74
|
+
<li data-id="1">Item 1 <span class="drag-handle">⠿</span></li>
|
|
75
|
+
<li data-id="2">Item 2 <span class="drag-handle">⠿</span></li>
|
|
76
|
+
</ul>
|
|
77
|
+
|
|
78
|
+
<script type="module">
|
|
79
|
+
import { UA_Sortable } from "@urbanstudio/ua-sortable";
|
|
80
|
+
UA_Sortable.observe(document.body);
|
|
81
|
+
</script>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Options
|
|
87
|
+
|
|
88
|
+
| Option | Type | Default | Description |
|
|
89
|
+
|--------|------|---------|-------------|
|
|
90
|
+
| `handle` | `string\|null` | `null` | CSS selector for drag handle. `null` = whole item is draggable. |
|
|
91
|
+
| `filter` | `string\|null` | `null` | CSS selector for items excluded from drag. |
|
|
92
|
+
| `group` | `string\|null` | `null` | Group name for cross-list drag. |
|
|
93
|
+
| `direction` | `string` | `"auto"` | `"vertical"`, `"horizontal"`, or `"auto"` (reads `flex-direction`). |
|
|
94
|
+
| `animation` | `number` | `150` | Ghost transition duration in ms. `0` = disabled. |
|
|
95
|
+
| `dataIdAttr` | `string` | `"data-id"` | Attribute used to identify items in callbacks. |
|
|
96
|
+
| `delay` | `number` | `0` | ms before drag starts after pointerdown. |
|
|
97
|
+
| `delayOnTouchOnly` | `boolean` | `false` | Apply `delay` only for touch input. |
|
|
98
|
+
| `disabled` | `boolean` | `false` | Disable drag on this instance. |
|
|
99
|
+
| `ghostClass` | `string` | `"ua-sortable-ghost"` | Class added to the ghost clone. |
|
|
100
|
+
| `dragClass` | `string` | `"ua-sortable-drag"` | Class added to the dragged item. |
|
|
101
|
+
| `confirm` | `function\|null` | `null` | `(movedId, from, to) => Promise<bool>` — called before cross-list drop. Return `false` to cancel. |
|
|
102
|
+
| `onDragStart` | `function\|null` | `null` | `(el)` — drag begins. |
|
|
103
|
+
| `onDragEnd` | `function\|null` | `null` | `(el, didMove: bool)` — drag ends. |
|
|
104
|
+
| `onSort` | `function\|null` | `null` | `(ids[], container)` — order changed within same list. |
|
|
105
|
+
| `onMove` | `function\|null` | `null` | `(movedId, from, to, fromIds[], toIds[]) => bool\|Promise<bool>` — item moved to other list. |
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Instance methods
|
|
110
|
+
|
|
111
|
+
| Method | Description |
|
|
112
|
+
|--------|-------------|
|
|
113
|
+
| `toArray()` | IDs of all draggable children in current DOM order. |
|
|
114
|
+
| `enable()` | Enable drag. |
|
|
115
|
+
| `disable()` | Disable drag. |
|
|
116
|
+
| `refresh()` | Re-scan draggable children (after manual DOM changes). |
|
|
117
|
+
| `destroy()` | Remove all listeners, disconnect observers, unregister. |
|
|
118
|
+
| `option(name)` | Get option value. |
|
|
119
|
+
| `option(name, value)` | Set option value at runtime. |
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Static methods
|
|
124
|
+
|
|
125
|
+
| Method | Description |
|
|
126
|
+
|--------|-------------|
|
|
127
|
+
| `UA_Sortable.get(el)` | Instance for a container element. |
|
|
128
|
+
| `UA_Sortable.getGroup(name)` | All instances with this group name. |
|
|
129
|
+
| `UA_Sortable.snapshot(group?)` | `[{container, ids, group}]` — current order of all grouped instances. |
|
|
130
|
+
| `UA_Sortable.initAll(root?)` | Initialize all `[data-ua-sortable]` in `root`. |
|
|
131
|
+
| `UA_Sortable.observe(root)` | MutationObserver — auto-init new `[data-ua-sortable]` containers. |
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Cross-list drag
|
|
136
|
+
|
|
137
|
+
Containers sharing the same `group` name accept each other's items:
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
const opts = {
|
|
141
|
+
group: "tasks",
|
|
142
|
+
onMove: (id, from, to, fromIds, toIds) => {
|
|
143
|
+
api.moveTask(id, to.dataset.listId);
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
new UA_Sortable(document.querySelector("#todo"), opts);
|
|
147
|
+
new UA_Sortable(document.querySelector("#doing"), opts);
|
|
148
|
+
new UA_Sortable(document.querySelector("#done"), opts);
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
With a confirmation dialog before cross-list drop:
|
|
152
|
+
```js
|
|
153
|
+
new UA_Sortable(el, {
|
|
154
|
+
group: "rooms",
|
|
155
|
+
confirm: async (id, from, to) => confirm(`Move item to "${to.dataset.label}"?`),
|
|
156
|
+
onMove: (id, from, to) => api.moveToSection(id, to.dataset.sectionId),
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Snapshot
|
|
163
|
+
|
|
164
|
+
```js
|
|
165
|
+
// Read current order of all containers in a group
|
|
166
|
+
const state = UA_Sortable.snapshot("tasks");
|
|
167
|
+
// [{ container: HTMLElement, ids: ["id1", "id2"], group: "tasks" }, ...]
|
|
168
|
+
|
|
169
|
+
state.forEach(({ container, ids }) => {
|
|
170
|
+
api.saveOrder(container.dataset.listId, ids);
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## CSS
|
|
177
|
+
|
|
178
|
+
Minimal styles are **injected automatically** on first use — no stylesheet to include.
|
|
179
|
+
|
|
180
|
+
```css
|
|
181
|
+
.ua-sortable-ghost { opacity: .4; }
|
|
182
|
+
.ua-sortable-drag { opacity: .4; }
|
|
183
|
+
.ua-drag-handle { cursor: grab; touch-action: none; }
|
|
184
|
+
.ua-drag-handle:active { cursor: grabbing; }
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The drop indicator uses `--accent` (CSS custom property) with fallback `#2563eb`.
|
|
188
|
+
Override any of these in your own stylesheet.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## HTML attributes
|
|
193
|
+
|
|
194
|
+
When using `UA_Sortable.observe()` or `UA_Sortable.initAll()`:
|
|
195
|
+
|
|
196
|
+
| Attribute | Option |
|
|
197
|
+
|-----------|--------|
|
|
198
|
+
| `data-ua-sortable` | triggers init |
|
|
199
|
+
| `data-sortable-handle` | `handle` |
|
|
200
|
+
| `data-sortable-filter` | `filter` |
|
|
201
|
+
| `data-sortable-group` | `group` |
|
|
202
|
+
| `data-sortable-direction` | `direction` |
|
|
203
|
+
| `data-sortable-animation` | `animation` |
|
|
204
|
+
| `data-sortable-delay` | `delay` |
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Browser support
|
|
209
|
+
|
|
210
|
+
Pointer Events: Chrome 55+, Firefox 59+, Safari 13+, Edge 79+.
|
|
211
|
+
No polyfills required.
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Design principles
|
|
216
|
+
|
|
217
|
+
`ua-sortable` tries to stay small and predictable.
|
|
218
|
+
|
|
219
|
+
It does not try to replace SortableJS.
|
|
220
|
+
It does not try to support IE11.
|
|
221
|
+
It does not ship a bundler, a build step, or a runtime dependency.
|
|
222
|
+
|
|
223
|
+
It simply:
|
|
224
|
+
1. listens for pointer events
|
|
225
|
+
2. moves a placeholder element as you drag
|
|
226
|
+
3. calls your callback with the new order
|
|
227
|
+
|
|
228
|
+
For everything else, use the `onMove` / `onSort` callbacks.
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## License
|
|
233
|
+
|
|
234
|
+
MIT License
|
|
235
|
+
|
|
236
|
+
Copyright (c) 2026 Marian Feiler, [urbanstudio GmbH](https://urbanstudio.de)
|
|
237
|
+
|
|
238
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
239
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
240
|
+
in the Software without restriction, including without limitation the rights
|
|
241
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
242
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
243
|
+
furnished to do so, subject to the following conditions:
|
|
244
|
+
|
|
245
|
+
The above copyright notice and this permission notice shall be included in all
|
|
246
|
+
copies or substantial portions of the Software.
|
|
247
|
+
|
|
248
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
249
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
250
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
251
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
252
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
253
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
254
|
+
SOFTWARE.
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UA_Sortable v1.0.0 — IIFE build (browser global)
|
|
3
|
+
* Pointer-Events-based drag-and-drop sorting. No dependencies.
|
|
4
|
+
*
|
|
5
|
+
* @author Marian Feiler <mf@urbanstudio.de>
|
|
6
|
+
* @company urbanstudio GmbH — https://urbanstudio.de
|
|
7
|
+
* @license MIT
|
|
8
|
+
* @see https://github.com/urbanstudioGmbH/ua-sortable
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* <script src="ua-sortable.js"></script>
|
|
12
|
+
* <script>
|
|
13
|
+
* new UA_Sortable(document.querySelector("ul"), { handle: ".drag-handle" });
|
|
14
|
+
* // or shorthand:
|
|
15
|
+
* uaMakeSortable(document.querySelector("ul"), { handle: ".drag-handle" });
|
|
16
|
+
* </script>
|
|
17
|
+
*/
|
|
18
|
+
(function (global, factory) {
|
|
19
|
+
"use strict";
|
|
20
|
+
if (typeof module !== "undefined" && module.exports) {
|
|
21
|
+
// CommonJS / Node (no DOM, only class export)
|
|
22
|
+
module.exports = factory();
|
|
23
|
+
} else {
|
|
24
|
+
// Browser global
|
|
25
|
+
const exports = factory();
|
|
26
|
+
global.UA_Sortable = exports.UA_Sortable;
|
|
27
|
+
global.uaMakeSortable = exports.uaMakeSortable;
|
|
28
|
+
}
|
|
29
|
+
})(typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : this, function () {
|
|
30
|
+
"use strict";
|
|
31
|
+
|
|
32
|
+
class UA_Sortable {
|
|
33
|
+
|
|
34
|
+
static #instanceRegistry = new WeakMap();
|
|
35
|
+
static #groupRegistry = new Map();
|
|
36
|
+
static #globalObserver = null;
|
|
37
|
+
|
|
38
|
+
#containerElement = null;
|
|
39
|
+
#options = {};
|
|
40
|
+
#draggableElements = [];
|
|
41
|
+
#ghostElement = null;
|
|
42
|
+
#draggedElement = null;
|
|
43
|
+
#placeholderElement = null;
|
|
44
|
+
#sourceContainerElement = null;
|
|
45
|
+
#currentContainerElement = null;
|
|
46
|
+
#pointerStartX = 0;
|
|
47
|
+
#pointerStartY = 0;
|
|
48
|
+
#delayTimer = null;
|
|
49
|
+
#isDragging = false;
|
|
50
|
+
#childObserver = null;
|
|
51
|
+
#boundHandlePointerDown = null;
|
|
52
|
+
#boundHandlePointerMove = null;
|
|
53
|
+
#boundHandlePointerUp = null;
|
|
54
|
+
#boundHandlePointerCancel = null;
|
|
55
|
+
|
|
56
|
+
constructor(containerElement, options = {}) {
|
|
57
|
+
if (!(containerElement instanceof HTMLElement)) {
|
|
58
|
+
throw new Error("UA_Sortable: first argument must be an HTMLElement");
|
|
59
|
+
}
|
|
60
|
+
if (UA_Sortable.#instanceRegistry.has(containerElement)) {
|
|
61
|
+
UA_Sortable.#instanceRegistry.get(containerElement).destroy();
|
|
62
|
+
}
|
|
63
|
+
this.#containerElement = containerElement;
|
|
64
|
+
this.#options = {
|
|
65
|
+
handle: null, filter: null, group: null, direction: "auto",
|
|
66
|
+
animation: 150, dataIdAttr: "data-id", delay: 0,
|
|
67
|
+
delayOnTouchOnly: false, disabled: false,
|
|
68
|
+
ghostClass: "ua-sortable-ghost", dragClass: "ua-sortable-drag",
|
|
69
|
+
confirm: null, onDragStart: null, onDragEnd: null,
|
|
70
|
+
onSort: null, onMove: null,
|
|
71
|
+
...options,
|
|
72
|
+
};
|
|
73
|
+
this.#boundHandlePointerDown = this.#handlePointerDown.bind(this);
|
|
74
|
+
this.#boundHandlePointerMove = this.#handlePointerMove.bind(this);
|
|
75
|
+
this.#boundHandlePointerUp = this.#handlePointerUp.bind(this);
|
|
76
|
+
this.#boundHandlePointerCancel = this.#handlePointerCancel.bind(this);
|
|
77
|
+
this.#registerInGlobals();
|
|
78
|
+
this.#attachContainerListeners();
|
|
79
|
+
this.#observeChildChanges();
|
|
80
|
+
this.#refreshDraggableList();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
toArray() {
|
|
84
|
+
return this.#getDraggableChildren().map(el => el.getAttribute(this.#options.dataIdAttr) ?? "");
|
|
85
|
+
}
|
|
86
|
+
enable() { this.option("disabled", false); }
|
|
87
|
+
disable() { this.option("disabled", true); }
|
|
88
|
+
refresh() { this.#refreshDraggableList(); }
|
|
89
|
+
destroy() {
|
|
90
|
+
this.#detachContainerListeners();
|
|
91
|
+
this.#childObserver?.disconnect();
|
|
92
|
+
this.#childObserver = null;
|
|
93
|
+
this.#cleanupDragState();
|
|
94
|
+
this.#unregisterFromGlobals();
|
|
95
|
+
}
|
|
96
|
+
option(name, value = undefined) {
|
|
97
|
+
if (value === undefined) return this.#options[name];
|
|
98
|
+
this.#options[name] = value;
|
|
99
|
+
if (name === "filter" || name === "handle") this.#refreshDraggableList();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
static get(containerElement) {
|
|
103
|
+
return UA_Sortable.#instanceRegistry.get(containerElement) ?? null;
|
|
104
|
+
}
|
|
105
|
+
static getGroup(groupName) {
|
|
106
|
+
return [...(UA_Sortable.#groupRegistry.get(groupName) ?? [])];
|
|
107
|
+
}
|
|
108
|
+
static snapshot(groupName = null) {
|
|
109
|
+
let instances;
|
|
110
|
+
if (groupName !== null) {
|
|
111
|
+
instances = UA_Sortable.getGroup(groupName);
|
|
112
|
+
} else {
|
|
113
|
+
instances = [];
|
|
114
|
+
UA_Sortable.#groupRegistry.forEach(g => instances.push(...g));
|
|
115
|
+
}
|
|
116
|
+
return instances.map(i => ({ container: i.#containerElement, ids: i.toArray(), group: i.#options.group }));
|
|
117
|
+
}
|
|
118
|
+
static initAll(root = document) {
|
|
119
|
+
root.querySelectorAll("[data-ua-sortable]").forEach(el => {
|
|
120
|
+
if (!UA_Sortable.#instanceRegistry.has(el)) UA_Sortable.#initFromDataAttributes(el);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
static observe(root) {
|
|
124
|
+
if (UA_Sortable.#globalObserver) UA_Sortable.#globalObserver.disconnect();
|
|
125
|
+
UA_Sortable.#globalObserver = new MutationObserver(mutations => {
|
|
126
|
+
for (const m of mutations) {
|
|
127
|
+
for (const node of m.addedNodes) {
|
|
128
|
+
if (!(node instanceof HTMLElement)) continue;
|
|
129
|
+
if (node.hasAttribute("data-ua-sortable") && !UA_Sortable.#instanceRegistry.has(node)) {
|
|
130
|
+
UA_Sortable.#initFromDataAttributes(node);
|
|
131
|
+
}
|
|
132
|
+
node.querySelectorAll("[data-ua-sortable]").forEach(el => {
|
|
133
|
+
if (!UA_Sortable.#instanceRegistry.has(el)) UA_Sortable.#initFromDataAttributes(el);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
UA_Sortable.#globalObserver.observe(root, { childList: true, subtree: true });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
static #initFromDataAttributes(el) {
|
|
142
|
+
const d = el.dataset, o = {};
|
|
143
|
+
if (d.sortableHandle !== undefined) o.handle = d.sortableHandle;
|
|
144
|
+
if (d.sortableFilter !== undefined) o.filter = d.sortableFilter;
|
|
145
|
+
if (d.sortableGroup !== undefined) o.group = d.sortableGroup;
|
|
146
|
+
if (d.sortableDirection !== undefined) o.direction = d.sortableDirection;
|
|
147
|
+
if (d.sortableAnimation !== undefined) o.animation = parseInt(d.sortableAnimation, 10);
|
|
148
|
+
if (d.sortableDataIdAttr!== undefined) o.dataIdAttr = d.sortableDataIdAttr;
|
|
149
|
+
if (d.sortableDelay !== undefined) o.delay = parseInt(d.sortableDelay, 10);
|
|
150
|
+
new UA_Sortable(el, o);
|
|
151
|
+
}
|
|
152
|
+
#registerInGlobals() {
|
|
153
|
+
UA_Sortable.#instanceRegistry.set(this.#containerElement, this);
|
|
154
|
+
const g = this.#options.group;
|
|
155
|
+
if (g) {
|
|
156
|
+
if (!UA_Sortable.#groupRegistry.has(g)) UA_Sortable.#groupRegistry.set(g, new Set());
|
|
157
|
+
UA_Sortable.#groupRegistry.get(g).add(this);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
#unregisterFromGlobals() {
|
|
161
|
+
UA_Sortable.#instanceRegistry.delete(this.#containerElement);
|
|
162
|
+
const g = this.#options.group;
|
|
163
|
+
if (g && UA_Sortable.#groupRegistry.has(g)) UA_Sortable.#groupRegistry.get(g).delete(this);
|
|
164
|
+
}
|
|
165
|
+
#attachContainerListeners() {
|
|
166
|
+
this.#containerElement.addEventListener("pointerdown", this.#boundHandlePointerDown);
|
|
167
|
+
}
|
|
168
|
+
#detachContainerListeners() {
|
|
169
|
+
this.#containerElement.removeEventListener("pointerdown", this.#boundHandlePointerDown);
|
|
170
|
+
document.removeEventListener("pointermove", this.#boundHandlePointerMove);
|
|
171
|
+
document.removeEventListener("pointerup", this.#boundHandlePointerUp);
|
|
172
|
+
document.removeEventListener("pointercancel", this.#boundHandlePointerCancel);
|
|
173
|
+
}
|
|
174
|
+
#observeChildChanges() {
|
|
175
|
+
this.#childObserver = new MutationObserver(() => this.#refreshDraggableList());
|
|
176
|
+
this.#childObserver.observe(this.#containerElement, { childList: true });
|
|
177
|
+
}
|
|
178
|
+
#refreshDraggableList() { this.#draggableElements = this.#getDraggableChildren(); }
|
|
179
|
+
#getDraggableChildren() {
|
|
180
|
+
const c = [...this.#containerElement.children];
|
|
181
|
+
return this.#options.filter ? c.filter(el => !el.matches(this.#options.filter)) : c;
|
|
182
|
+
}
|
|
183
|
+
#handlePointerDown(e) {
|
|
184
|
+
if (this.#options.disabled) return;
|
|
185
|
+
if (e.button !== 0 && e.pointerType === "mouse") return;
|
|
186
|
+
const dragged = this.#findDraggableParent(e.target);
|
|
187
|
+
if (!dragged) return;
|
|
188
|
+
if (this.#options.filter && dragged.matches(this.#options.filter)) return;
|
|
189
|
+
if (this.#options.handle) {
|
|
190
|
+
const h = e.target.closest(this.#options.handle);
|
|
191
|
+
if (!h || !dragged.contains(h)) return;
|
|
192
|
+
}
|
|
193
|
+
this.#pointerStartX = e.clientX;
|
|
194
|
+
this.#pointerStartY = e.clientY;
|
|
195
|
+
const delay = this.#options.delay > 0 && (!this.#options.delayOnTouchOnly || e.pointerType === "touch");
|
|
196
|
+
if (delay) {
|
|
197
|
+
this.#delayTimer = setTimeout(() => this.#startDrag(e, dragged), this.#options.delay);
|
|
198
|
+
const cancel = mv => {
|
|
199
|
+
if (Math.hypot(mv.clientX - this.#pointerStartX, mv.clientY - this.#pointerStartY) > 5) {
|
|
200
|
+
clearTimeout(this.#delayTimer);
|
|
201
|
+
document.removeEventListener("pointermove", cancel);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
document.addEventListener("pointermove", cancel);
|
|
205
|
+
} else {
|
|
206
|
+
this.#startDrag(e, dragged);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
#startDrag(e, dragged) {
|
|
210
|
+
this.#isDragging = true;
|
|
211
|
+
this.#draggedElement = dragged;
|
|
212
|
+
this.#sourceContainerElement = this.#containerElement;
|
|
213
|
+
this.#currentContainerElement = this.#containerElement;
|
|
214
|
+
const r = dragged.getBoundingClientRect();
|
|
215
|
+
this.#ghostElement = dragged.cloneNode(true);
|
|
216
|
+
this.#ghostElement.classList.add(this.#options.ghostClass);
|
|
217
|
+
this.#ghostElement.style.cssText = `position:fixed;left:${r.left}px;top:${r.top}px;width:${r.width}px;height:${r.height}px;margin:0;pointer-events:none;z-index:9999;`;
|
|
218
|
+
document.body.appendChild(this.#ghostElement);
|
|
219
|
+
this.#placeholderElement = document.createElement(dragged.tagName);
|
|
220
|
+
this.#placeholderElement.style.cssText = `width:${r.width}px;height:${r.height}px;opacity:0;pointer-events:none;`;
|
|
221
|
+
dragged.parentNode.insertBefore(this.#placeholderElement, dragged);
|
|
222
|
+
dragged.classList.add(this.#options.dragClass);
|
|
223
|
+
dragged.style.opacity = "0.001";
|
|
224
|
+
this.#containerElement.classList.add("ua-sortable-active");
|
|
225
|
+
try { dragged.setPointerCapture(e.pointerId); } catch (_) {}
|
|
226
|
+
document.addEventListener("pointermove", this.#boundHandlePointerMove);
|
|
227
|
+
document.addEventListener("pointerup", this.#boundHandlePointerUp);
|
|
228
|
+
document.addEventListener("pointercancel", this.#boundHandlePointerCancel);
|
|
229
|
+
this.#options.onDragStart?.(dragged);
|
|
230
|
+
}
|
|
231
|
+
#handlePointerMove(e) {
|
|
232
|
+
if (!this.#isDragging) return;
|
|
233
|
+
const dx = e.clientX - this.#pointerStartX, dy = e.clientY - this.#pointerStartY;
|
|
234
|
+
const or = this.#draggedElement.getBoundingClientRect();
|
|
235
|
+
this.#ghostElement.style.left = `${or.left + dx}px`;
|
|
236
|
+
this.#ghostElement.style.top = `${or.top + dy}px`;
|
|
237
|
+
const tc = this.#findTargetContainer(e.clientX, e.clientY);
|
|
238
|
+
if (tc && tc !== this.#currentContainerElement) {
|
|
239
|
+
this.#currentContainerElement.classList.remove("ua-sortable-active");
|
|
240
|
+
tc.classList.add("ua-sortable-active");
|
|
241
|
+
this.#currentContainerElement = tc;
|
|
242
|
+
}
|
|
243
|
+
this.#updatePlaceholderPosition(e.clientX, e.clientY);
|
|
244
|
+
}
|
|
245
|
+
#handlePointerUp() { if (this.#isDragging) this.#finalizeDrop(); }
|
|
246
|
+
#handlePointerCancel() { if (this.#isDragging) this.#revertDrag(); }
|
|
247
|
+
async #finalizeDrop() {
|
|
248
|
+
const dragged = this.#draggedElement;
|
|
249
|
+
const src = this.#sourceContainerElement;
|
|
250
|
+
const tgt = this.#currentContainerElement;
|
|
251
|
+
const moved = src !== tgt;
|
|
252
|
+
if (moved && this.#options.confirm) {
|
|
253
|
+
const ok = await Promise.resolve(this.#options.confirm(dragged.getAttribute(this.#options.dataIdAttr), src, tgt));
|
|
254
|
+
if (!ok) { this.#revertDrag(); return; }
|
|
255
|
+
}
|
|
256
|
+
tgt.insertBefore(dragged, this.#placeholderElement);
|
|
257
|
+
this.#cleanupDragState();
|
|
258
|
+
const id = dragged.getAttribute(this.#options.dataIdAttr);
|
|
259
|
+
const ids = UA_Sortable.get(tgt)?.toArray() ?? [...tgt.children]
|
|
260
|
+
.filter(el => el !== this.#placeholderElement && (!this.#options.filter || !el.matches(this.#options.filter)))
|
|
261
|
+
.map(el => el.getAttribute(this.#options.dataIdAttr) ?? "");
|
|
262
|
+
if (moved) {
|
|
263
|
+
const srcIds = UA_Sortable.get(src)?.toArray() ?? [];
|
|
264
|
+
const ok = await this.#invokeOnMove(id, src, tgt, srcIds, ids);
|
|
265
|
+
if (ok === false) { src.appendChild(dragged); this.#options.onDragEnd?.(dragged, false); return; }
|
|
266
|
+
} else {
|
|
267
|
+
this.#options.onSort?.(ids, tgt);
|
|
268
|
+
}
|
|
269
|
+
this.#options.onDragEnd?.(dragged, true);
|
|
270
|
+
}
|
|
271
|
+
#revertDrag() {
|
|
272
|
+
if (this.#placeholderElement?.parentNode) {
|
|
273
|
+
this.#placeholderElement.parentNode.insertBefore(this.#draggedElement, this.#placeholderElement);
|
|
274
|
+
}
|
|
275
|
+
const d = this.#draggedElement;
|
|
276
|
+
this.#cleanupDragState();
|
|
277
|
+
this.#options.onDragEnd?.(d, false);
|
|
278
|
+
}
|
|
279
|
+
async #invokeOnMove(id, from, to, fromIds, toIds) {
|
|
280
|
+
if (!this.#options.onMove) return true;
|
|
281
|
+
return await Promise.resolve(this.#options.onMove(id, from, to, fromIds, toIds)) !== false;
|
|
282
|
+
}
|
|
283
|
+
#cleanupDragState() {
|
|
284
|
+
this.#ghostElement?.remove();
|
|
285
|
+
this.#placeholderElement?.remove();
|
|
286
|
+
if (this.#draggedElement) {
|
|
287
|
+
this.#draggedElement.classList.remove(this.#options.dragClass);
|
|
288
|
+
this.#draggedElement.style.opacity = "";
|
|
289
|
+
}
|
|
290
|
+
this.#containerElement.classList.remove("ua-sortable-active");
|
|
291
|
+
if (this.#currentContainerElement && this.#currentContainerElement !== this.#containerElement) {
|
|
292
|
+
this.#currentContainerElement.classList.remove("ua-sortable-active");
|
|
293
|
+
}
|
|
294
|
+
clearTimeout(this.#delayTimer);
|
|
295
|
+
document.removeEventListener("pointermove", this.#boundHandlePointerMove);
|
|
296
|
+
document.removeEventListener("pointerup", this.#boundHandlePointerUp);
|
|
297
|
+
document.removeEventListener("pointercancel", this.#boundHandlePointerCancel);
|
|
298
|
+
this.#ghostElement = this.#placeholderElement = this.#draggedElement =
|
|
299
|
+
this.#sourceContainerElement = this.#currentContainerElement = this.#delayTimer = null;
|
|
300
|
+
this.#isDragging = false;
|
|
301
|
+
}
|
|
302
|
+
#updatePlaceholderPosition(px, py) {
|
|
303
|
+
const tc = this.#currentContainerElement;
|
|
304
|
+
const children = [...tc.children].filter(c =>
|
|
305
|
+
c !== this.#draggedElement && c !== this.#ghostElement &&
|
|
306
|
+
c !== this.#placeholderElement &&
|
|
307
|
+
(!this.#options.filter || !c.matches(this.#options.filter))
|
|
308
|
+
);
|
|
309
|
+
const dir = this.#resolveDirection(tc);
|
|
310
|
+
let before = null;
|
|
311
|
+
for (const s of children) {
|
|
312
|
+
const sr = s.getBoundingClientRect();
|
|
313
|
+
const mid = dir === "horizontal" ? sr.left + sr.width / 2 : sr.top + sr.height / 2;
|
|
314
|
+
if ((dir === "horizontal" ? px : py) < mid) { before = s; break; }
|
|
315
|
+
}
|
|
316
|
+
before ? tc.insertBefore(this.#placeholderElement, before) : tc.appendChild(this.#placeholderElement);
|
|
317
|
+
}
|
|
318
|
+
#resolveDirection(el) {
|
|
319
|
+
if (this.#options.direction !== "auto") return this.#options.direction;
|
|
320
|
+
const fd = getComputedStyle(el).flexDirection;
|
|
321
|
+
return (fd === "row" || fd === "row-reverse") ? "horizontal" : "vertical";
|
|
322
|
+
}
|
|
323
|
+
#findDraggableParent(el) {
|
|
324
|
+
let c = el;
|
|
325
|
+
while (c && c !== this.#containerElement) {
|
|
326
|
+
if (c.parentElement === this.#containerElement) return c;
|
|
327
|
+
c = c.parentElement;
|
|
328
|
+
}
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
#findTargetContainer(px, py) {
|
|
332
|
+
const g = this.#options.group;
|
|
333
|
+
if (!g) return this.#containerElement;
|
|
334
|
+
for (const inst of UA_Sortable.getGroup(g)) {
|
|
335
|
+
if (inst === this || inst.#options.disabled) continue;
|
|
336
|
+
const r = inst.#containerElement.getBoundingClientRect();
|
|
337
|
+
if (px >= r.left && px <= r.right && py >= r.top && py <= r.bottom) return inst.#containerElement;
|
|
338
|
+
}
|
|
339
|
+
return this.#containerElement;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Inject minimal CSS
|
|
344
|
+
if (typeof document !== "undefined" && !document.getElementById("ua-sortable-styles")) {
|
|
345
|
+
const s = document.createElement("style");
|
|
346
|
+
s.id = "ua-sortable-styles";
|
|
347
|
+
s.textContent = ".ua-sortable-ghost{opacity:.4;}.ua-sortable-drag{opacity:.4;}.ua-sortable-active>.ua-sortable-over{border-top:2px solid var(--accent,#2563eb);}.ua-drag-handle{cursor:grab;touch-action:none;}.ua-drag-handle:active{cursor:grabbing;}";
|
|
348
|
+
document.head.appendChild(s);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function uaMakeSortable(el, options = {}) {
|
|
352
|
+
return new UA_Sortable(el, options);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return { UA_Sortable, uaMakeSortable };
|
|
356
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@urbanstudio/ua-sortable",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Pointer-Events-based drag-and-drop sorting for lists and grids. No dependencies.",
|
|
5
|
+
"author": "Marian Feiler <mf@urbanstudio.de> (https://urbanstudio.de)",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://github.com/urbanstudioGmbH/ua-sortable#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/urbanstudioGmbH/ua-sortable.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/urbanstudioGmbH/ua-sortable/issues"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./dist/ua-sortable.js",
|
|
17
|
+
"module": "./src/Sortable.js",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"import": "./src/Sortable.js",
|
|
21
|
+
"require": "./dist/ua-sortable.js"
|
|
22
|
+
},
|
|
23
|
+
"./dist": "./dist/ua-sortable.js"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"src/",
|
|
27
|
+
"dist/",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE",
|
|
30
|
+
"CHANGELOG.md"
|
|
31
|
+
],
|
|
32
|
+
"keywords": [
|
|
33
|
+
"drag-and-drop",
|
|
34
|
+
"sortable",
|
|
35
|
+
"sort",
|
|
36
|
+
"reorder",
|
|
37
|
+
"drag",
|
|
38
|
+
"pointer-events",
|
|
39
|
+
"touch",
|
|
40
|
+
"es6-module",
|
|
41
|
+
"zero-dependency",
|
|
42
|
+
"vanilla-js",
|
|
43
|
+
"list",
|
|
44
|
+
"grid"
|
|
45
|
+
],
|
|
46
|
+
"sideEffects": false,
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=14"
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/Sortable.js
ADDED
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* UA_Sortable — Pointer-Events-based drag-and-drop sorting for lists and grids.
|
|
5
|
+
* No dependencies — no jQuery, no CDN, no external frameworks.
|
|
6
|
+
* Supports: simple sorting, cross-list groups, auto-direction, filter, delay.
|
|
7
|
+
*
|
|
8
|
+
* @author Marian Feiler <mf@urbanstudio.de>
|
|
9
|
+
* @company urbanstudio GmbH — https://urbanstudio.de
|
|
10
|
+
* @license MIT
|
|
11
|
+
* @see https://github.com/urbanstudioGmbH/ua-sortable
|
|
12
|
+
*/
|
|
13
|
+
export class UA_Sortable {
|
|
14
|
+
|
|
15
|
+
// --- Global registries ---
|
|
16
|
+
static #instanceRegistry = new WeakMap(); // containerElement → UA_Sortable
|
|
17
|
+
static #groupRegistry = new Map(); // groupName → Set<UA_Sortable>
|
|
18
|
+
static #globalObserver = null;
|
|
19
|
+
|
|
20
|
+
// --- Internal drag state ---
|
|
21
|
+
#containerElement = null;
|
|
22
|
+
#options = {};
|
|
23
|
+
#draggableElements = [];
|
|
24
|
+
#ghostElement = null;
|
|
25
|
+
#draggedElement = null;
|
|
26
|
+
#placeholderElement = null;
|
|
27
|
+
#sourceContainerElement = null;
|
|
28
|
+
#currentContainerElement = null;
|
|
29
|
+
#pointerStartX = 0;
|
|
30
|
+
#pointerStartY = 0;
|
|
31
|
+
#delayTimer = null;
|
|
32
|
+
#isDragging = false;
|
|
33
|
+
#childObserver = null;
|
|
34
|
+
|
|
35
|
+
// Bound handler references for clean removeEventListener
|
|
36
|
+
#boundHandlePointerDown = null;
|
|
37
|
+
#boundHandlePointerMove = null;
|
|
38
|
+
#boundHandlePointerUp = null;
|
|
39
|
+
#boundHandlePointerCancel = null;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {HTMLElement} containerElement
|
|
43
|
+
* @param {object} options
|
|
44
|
+
*/
|
|
45
|
+
constructor(containerElement, options = {}) {
|
|
46
|
+
if (!(containerElement instanceof HTMLElement)) {
|
|
47
|
+
throw new Error("UA_Sortable: first argument must be an HTMLElement");
|
|
48
|
+
}
|
|
49
|
+
if (UA_Sortable.#instanceRegistry.has(containerElement)) {
|
|
50
|
+
UA_Sortable.#instanceRegistry.get(containerElement).destroy();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
this.#containerElement = containerElement;
|
|
54
|
+
this.#options = {
|
|
55
|
+
handle: null,
|
|
56
|
+
filter: null,
|
|
57
|
+
group: null,
|
|
58
|
+
direction: "auto",
|
|
59
|
+
animation: 150,
|
|
60
|
+
dataIdAttr: "data-id",
|
|
61
|
+
delay: 0,
|
|
62
|
+
delayOnTouchOnly: false,
|
|
63
|
+
disabled: false,
|
|
64
|
+
ghostClass: "ua-sortable-ghost",
|
|
65
|
+
dragClass: "ua-sortable-drag",
|
|
66
|
+
confirm: null,
|
|
67
|
+
onDragStart: null,
|
|
68
|
+
onDragEnd: null,
|
|
69
|
+
onSort: null,
|
|
70
|
+
onMove: null,
|
|
71
|
+
...options,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
this.#boundHandlePointerDown = this.#handlePointerDown.bind(this);
|
|
75
|
+
this.#boundHandlePointerMove = this.#handlePointerMove.bind(this);
|
|
76
|
+
this.#boundHandlePointerUp = this.#handlePointerUp.bind(this);
|
|
77
|
+
this.#boundHandlePointerCancel = this.#handlePointerCancel.bind(this);
|
|
78
|
+
|
|
79
|
+
this.#registerInGlobals();
|
|
80
|
+
this.#attachContainerListeners();
|
|
81
|
+
this.#observeChildChanges();
|
|
82
|
+
this.#refreshDraggableList();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// =========================================================================
|
|
86
|
+
// PUBLIC API — Instance methods
|
|
87
|
+
// =========================================================================
|
|
88
|
+
|
|
89
|
+
/** Returns IDs of all draggable children in current DOM order. */
|
|
90
|
+
toArray() {
|
|
91
|
+
return this.#getDraggableChildren().map(el => el.getAttribute(this.#options.dataIdAttr) ?? "");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Enable dragging. */
|
|
95
|
+
enable() {
|
|
96
|
+
this.option("disabled", false);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Disable dragging. */
|
|
100
|
+
disable() {
|
|
101
|
+
this.option("disabled", true);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Re-scan draggable children and filtered elements. */
|
|
105
|
+
refresh() {
|
|
106
|
+
this.#refreshDraggableList();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Remove all listeners and observers, delete from global registry. */
|
|
110
|
+
destroy() {
|
|
111
|
+
this.#detachContainerListeners();
|
|
112
|
+
this.#childObserver?.disconnect();
|
|
113
|
+
this.#childObserver = null;
|
|
114
|
+
this.#cleanupDragState();
|
|
115
|
+
this.#unregisterFromGlobals();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get or set an option at runtime.
|
|
120
|
+
* @param {string} name
|
|
121
|
+
* @param {*} [value]
|
|
122
|
+
*/
|
|
123
|
+
option(name, value = undefined) {
|
|
124
|
+
if (value === undefined) return this.#options[name];
|
|
125
|
+
this.#options[name] = value;
|
|
126
|
+
if (name === "filter" || name === "handle") this.#refreshDraggableList();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// =========================================================================
|
|
130
|
+
// PUBLIC API — Static methods
|
|
131
|
+
// =========================================================================
|
|
132
|
+
|
|
133
|
+
/** Returns the UA_Sortable instance for a container element. */
|
|
134
|
+
static get(containerElement) {
|
|
135
|
+
return UA_Sortable.#instanceRegistry.get(containerElement) ?? null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Returns all instances registered under a group name. */
|
|
139
|
+
static getGroup(groupName) {
|
|
140
|
+
return [...(UA_Sortable.#groupRegistry.get(groupName) ?? [])];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Returns a snapshot of current order for all grouped instances.
|
|
145
|
+
* @param {string} [groupName] — if omitted, all grouped instances
|
|
146
|
+
* @returns {{ container: HTMLElement, ids: string[], group: string|null }[]}
|
|
147
|
+
*/
|
|
148
|
+
static snapshot(groupName = null) {
|
|
149
|
+
let instances;
|
|
150
|
+
if (groupName !== null) {
|
|
151
|
+
instances = UA_Sortable.getGroup(groupName);
|
|
152
|
+
} else {
|
|
153
|
+
instances = [];
|
|
154
|
+
UA_Sortable.#groupRegistry.forEach(groupSet => instances.push(...groupSet));
|
|
155
|
+
}
|
|
156
|
+
return instances.map(instance => ({
|
|
157
|
+
container: instance.#containerElement,
|
|
158
|
+
ids: instance.toArray(),
|
|
159
|
+
group: instance.#options.group,
|
|
160
|
+
}));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Initialize all [data-ua-sortable] containers within root.
|
|
165
|
+
* @param {HTMLElement|Document} [root=document]
|
|
166
|
+
*/
|
|
167
|
+
static initAll(root = document) {
|
|
168
|
+
root.querySelectorAll("[data-ua-sortable]").forEach(containerElement => {
|
|
169
|
+
if (!UA_Sortable.#instanceRegistry.has(containerElement)) {
|
|
170
|
+
UA_Sortable.#initFromDataAttributes(containerElement);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Start a MutationObserver that auto-initializes new [data-ua-sortable] containers.
|
|
177
|
+
* @param {HTMLElement|Document} root
|
|
178
|
+
*/
|
|
179
|
+
static observe(root) {
|
|
180
|
+
if (UA_Sortable.#globalObserver) UA_Sortable.#globalObserver.disconnect();
|
|
181
|
+
UA_Sortable.#globalObserver = new MutationObserver(mutationList => {
|
|
182
|
+
for (const mutation of mutationList) {
|
|
183
|
+
for (const addedNode of mutation.addedNodes) {
|
|
184
|
+
if (!(addedNode instanceof HTMLElement)) continue;
|
|
185
|
+
if (addedNode.hasAttribute("data-ua-sortable")) {
|
|
186
|
+
UA_Sortable.#initFromDataAttributes(addedNode);
|
|
187
|
+
}
|
|
188
|
+
addedNode.querySelectorAll("[data-ua-sortable]").forEach(el => {
|
|
189
|
+
if (!UA_Sortable.#instanceRegistry.has(el)) {
|
|
190
|
+
UA_Sortable.#initFromDataAttributes(el);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
UA_Sortable.#globalObserver.observe(root, { childList: true, subtree: true });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// =========================================================================
|
|
200
|
+
// PRIVATE — Initialization
|
|
201
|
+
// =========================================================================
|
|
202
|
+
|
|
203
|
+
static #initFromDataAttributes(containerElement) {
|
|
204
|
+
const dataset = containerElement.dataset;
|
|
205
|
+
const options = {};
|
|
206
|
+
if (dataset.sortableHandle !== undefined) options.handle = dataset.sortableHandle;
|
|
207
|
+
if (dataset.sortableFilter !== undefined) options.filter = dataset.sortableFilter;
|
|
208
|
+
if (dataset.sortableGroup !== undefined) options.group = dataset.sortableGroup;
|
|
209
|
+
if (dataset.sortableDirection !== undefined) options.direction = dataset.sortableDirection;
|
|
210
|
+
if (dataset.sortableAnimation !== undefined) options.animation = parseInt(dataset.sortableAnimation, 10);
|
|
211
|
+
if (dataset.sortableDataIdAttr!== undefined) options.dataIdAttr = dataset.sortableDataIdAttr;
|
|
212
|
+
if (dataset.sortableDelay !== undefined) options.delay = parseInt(dataset.sortableDelay, 10);
|
|
213
|
+
new UA_Sortable(containerElement, options);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
#registerInGlobals() {
|
|
217
|
+
UA_Sortable.#instanceRegistry.set(this.#containerElement, this);
|
|
218
|
+
const groupName = this.#options.group;
|
|
219
|
+
if (groupName) {
|
|
220
|
+
if (!UA_Sortable.#groupRegistry.has(groupName)) {
|
|
221
|
+
UA_Sortable.#groupRegistry.set(groupName, new Set());
|
|
222
|
+
}
|
|
223
|
+
UA_Sortable.#groupRegistry.get(groupName).add(this);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
#unregisterFromGlobals() {
|
|
228
|
+
UA_Sortable.#instanceRegistry.delete(this.#containerElement);
|
|
229
|
+
const groupName = this.#options.group;
|
|
230
|
+
if (groupName && UA_Sortable.#groupRegistry.has(groupName)) {
|
|
231
|
+
UA_Sortable.#groupRegistry.get(groupName).delete(this);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
#attachContainerListeners() {
|
|
236
|
+
this.#containerElement.addEventListener("pointerdown", this.#boundHandlePointerDown);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
#detachContainerListeners() {
|
|
240
|
+
this.#containerElement.removeEventListener("pointerdown", this.#boundHandlePointerDown);
|
|
241
|
+
document.removeEventListener("pointermove", this.#boundHandlePointerMove);
|
|
242
|
+
document.removeEventListener("pointerup", this.#boundHandlePointerUp);
|
|
243
|
+
document.removeEventListener("pointercancel", this.#boundHandlePointerCancel);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
#observeChildChanges() {
|
|
247
|
+
this.#childObserver = new MutationObserver(() => this.#refreshDraggableList());
|
|
248
|
+
this.#childObserver.observe(this.#containerElement, { childList: true });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
#refreshDraggableList() {
|
|
252
|
+
this.#draggableElements = this.#getDraggableChildren();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
#getDraggableChildren() {
|
|
256
|
+
const children = [...this.#containerElement.children];
|
|
257
|
+
if (!this.#options.filter) return children;
|
|
258
|
+
return children.filter(child => !child.matches(this.#options.filter));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// =========================================================================
|
|
262
|
+
// PRIVATE — Pointer event handlers
|
|
263
|
+
// =========================================================================
|
|
264
|
+
|
|
265
|
+
#handlePointerDown(pointerEvent) {
|
|
266
|
+
if (this.#options.disabled) return;
|
|
267
|
+
if (pointerEvent.button !== 0 && pointerEvent.pointerType === "mouse") return;
|
|
268
|
+
|
|
269
|
+
const draggedElement = this.#findDraggableParent(pointerEvent.target);
|
|
270
|
+
if (!draggedElement) return;
|
|
271
|
+
|
|
272
|
+
if (this.#options.filter && draggedElement.matches(this.#options.filter)) return;
|
|
273
|
+
|
|
274
|
+
if (this.#options.handle) {
|
|
275
|
+
const handleElement = pointerEvent.target.closest(this.#options.handle);
|
|
276
|
+
if (!handleElement || !draggedElement.contains(handleElement)) return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
this.#pointerStartX = pointerEvent.clientX;
|
|
280
|
+
this.#pointerStartY = pointerEvent.clientY;
|
|
281
|
+
|
|
282
|
+
const shouldDelay = this.#options.delay > 0 &&
|
|
283
|
+
(!this.#options.delayOnTouchOnly || pointerEvent.pointerType === "touch");
|
|
284
|
+
|
|
285
|
+
if (shouldDelay) {
|
|
286
|
+
this.#delayTimer = setTimeout(() => {
|
|
287
|
+
this.#startDrag(pointerEvent, draggedElement);
|
|
288
|
+
}, this.#options.delay);
|
|
289
|
+
const cancelDelay = (moveEvent) => {
|
|
290
|
+
const distance = Math.hypot(
|
|
291
|
+
moveEvent.clientX - this.#pointerStartX,
|
|
292
|
+
moveEvent.clientY - this.#pointerStartY
|
|
293
|
+
);
|
|
294
|
+
if (distance > 5) {
|
|
295
|
+
clearTimeout(this.#delayTimer);
|
|
296
|
+
document.removeEventListener("pointermove", cancelDelay);
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
document.addEventListener("pointermove", cancelDelay, { once: false });
|
|
300
|
+
} else {
|
|
301
|
+
this.#startDrag(pointerEvent, draggedElement);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
#startDrag(pointerEvent, draggedElement) {
|
|
306
|
+
this.#isDragging = true;
|
|
307
|
+
this.#draggedElement = draggedElement;
|
|
308
|
+
this.#sourceContainerElement = this.#containerElement;
|
|
309
|
+
this.#currentContainerElement = this.#containerElement;
|
|
310
|
+
|
|
311
|
+
const boundingRect = draggedElement.getBoundingClientRect();
|
|
312
|
+
this.#ghostElement = draggedElement.cloneNode(true);
|
|
313
|
+
this.#ghostElement.classList.add(this.#options.ghostClass);
|
|
314
|
+
this.#ghostElement.style.cssText = [
|
|
315
|
+
"position:fixed",
|
|
316
|
+
`left:${boundingRect.left}px`,
|
|
317
|
+
`top:${boundingRect.top}px`,
|
|
318
|
+
`width:${boundingRect.width}px`,
|
|
319
|
+
`height:${boundingRect.height}px`,
|
|
320
|
+
"margin:0",
|
|
321
|
+
"pointer-events:none",
|
|
322
|
+
"z-index:9999",
|
|
323
|
+
].join(";");
|
|
324
|
+
document.body.appendChild(this.#ghostElement);
|
|
325
|
+
|
|
326
|
+
this.#placeholderElement = document.createElement(draggedElement.tagName);
|
|
327
|
+
this.#placeholderElement.style.cssText = [
|
|
328
|
+
`width:${boundingRect.width}px`,
|
|
329
|
+
`height:${boundingRect.height}px`,
|
|
330
|
+
"opacity:0",
|
|
331
|
+
"pointer-events:none",
|
|
332
|
+
].join(";");
|
|
333
|
+
draggedElement.parentNode.insertBefore(this.#placeholderElement, draggedElement);
|
|
334
|
+
|
|
335
|
+
draggedElement.classList.add(this.#options.dragClass);
|
|
336
|
+
draggedElement.style.opacity = "0.001";
|
|
337
|
+
|
|
338
|
+
this.#containerElement.classList.add("ua-sortable-active");
|
|
339
|
+
|
|
340
|
+
try { draggedElement.setPointerCapture(pointerEvent.pointerId); } catch (_) {}
|
|
341
|
+
|
|
342
|
+
document.addEventListener("pointermove", this.#boundHandlePointerMove);
|
|
343
|
+
document.addEventListener("pointerup", this.#boundHandlePointerUp);
|
|
344
|
+
document.addEventListener("pointercancel", this.#boundHandlePointerCancel);
|
|
345
|
+
|
|
346
|
+
this.#options.onDragStart?.(draggedElement);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
#handlePointerMove(pointerEvent) {
|
|
350
|
+
if (!this.#isDragging) return;
|
|
351
|
+
|
|
352
|
+
const deltaX = pointerEvent.clientX - this.#pointerStartX;
|
|
353
|
+
const deltaY = pointerEvent.clientY - this.#pointerStartY;
|
|
354
|
+
|
|
355
|
+
const originalRect = this.#draggedElement.getBoundingClientRect();
|
|
356
|
+
this.#ghostElement.style.left = `${originalRect.left + deltaX}px`;
|
|
357
|
+
this.#ghostElement.style.top = `${originalRect.top + deltaY}px`;
|
|
358
|
+
|
|
359
|
+
const targetContainer = this.#findTargetContainer(pointerEvent.clientX, pointerEvent.clientY);
|
|
360
|
+
if (targetContainer && targetContainer !== this.#currentContainerElement) {
|
|
361
|
+
this.#currentContainerElement.classList.remove("ua-sortable-active");
|
|
362
|
+
targetContainer.classList.add("ua-sortable-active");
|
|
363
|
+
this.#currentContainerElement = targetContainer;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
this.#updatePlaceholderPosition(pointerEvent.clientX, pointerEvent.clientY);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
#handlePointerUp() {
|
|
370
|
+
if (!this.#isDragging) return;
|
|
371
|
+
this.#finalizeDrop();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
#handlePointerCancel() {
|
|
375
|
+
if (!this.#isDragging) return;
|
|
376
|
+
this.#revertDrag();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// =========================================================================
|
|
380
|
+
// PRIVATE — Drop logic
|
|
381
|
+
// =========================================================================
|
|
382
|
+
|
|
383
|
+
async #finalizeDrop() {
|
|
384
|
+
const draggedElement = this.#draggedElement;
|
|
385
|
+
const sourceContainerElement = this.#sourceContainerElement;
|
|
386
|
+
const targetContainerElement = this.#currentContainerElement;
|
|
387
|
+
const didChangeContainer = sourceContainerElement !== targetContainerElement;
|
|
388
|
+
|
|
389
|
+
if (didChangeContainer && this.#options.confirm !== null) {
|
|
390
|
+
const confirmFunction = this.#options.confirm;
|
|
391
|
+
const confirmed = await Promise.resolve(
|
|
392
|
+
confirmFunction(
|
|
393
|
+
draggedElement.getAttribute(this.#options.dataIdAttr),
|
|
394
|
+
sourceContainerElement,
|
|
395
|
+
targetContainerElement
|
|
396
|
+
)
|
|
397
|
+
);
|
|
398
|
+
if (!confirmed) {
|
|
399
|
+
this.#revertDrag();
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
targetContainerElement.insertBefore(draggedElement, this.#placeholderElement);
|
|
405
|
+
this.#cleanupDragState();
|
|
406
|
+
|
|
407
|
+
const movedId = draggedElement.getAttribute(this.#options.dataIdAttr);
|
|
408
|
+
const orderedTargetIds = UA_Sortable.get(targetContainerElement)?.toArray()
|
|
409
|
+
?? [...targetContainerElement.children]
|
|
410
|
+
.filter(el => el !== this.#placeholderElement && (!this.#options.filter || !el.matches(this.#options.filter)))
|
|
411
|
+
.map(el => el.getAttribute(this.#options.dataIdAttr) ?? "");
|
|
412
|
+
|
|
413
|
+
if (didChangeContainer) {
|
|
414
|
+
const sourceInstance = UA_Sortable.get(sourceContainerElement);
|
|
415
|
+
const orderedSourceIds = sourceInstance?.toArray() ?? [];
|
|
416
|
+
|
|
417
|
+
const shouldProceed = await this.#invokeOnMove(
|
|
418
|
+
movedId,
|
|
419
|
+
sourceContainerElement,
|
|
420
|
+
targetContainerElement,
|
|
421
|
+
orderedSourceIds,
|
|
422
|
+
orderedTargetIds
|
|
423
|
+
);
|
|
424
|
+
if (shouldProceed === false) {
|
|
425
|
+
sourceContainerElement.appendChild(draggedElement);
|
|
426
|
+
this.#options.onDragEnd?.(draggedElement, false);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
} else {
|
|
430
|
+
this.#options.onSort?.(orderedTargetIds, targetContainerElement);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
this.#options.onDragEnd?.(draggedElement, true);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
#revertDrag() {
|
|
437
|
+
if (this.#placeholderElement?.parentNode) {
|
|
438
|
+
this.#placeholderElement.parentNode.insertBefore(
|
|
439
|
+
this.#draggedElement,
|
|
440
|
+
this.#placeholderElement
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
const draggedElement = this.#draggedElement;
|
|
444
|
+
this.#cleanupDragState();
|
|
445
|
+
this.#options.onDragEnd?.(draggedElement, false);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
async #invokeOnMove(movedId, fromContainer, toContainer, orderedFromIds, orderedToIds) {
|
|
449
|
+
if (!this.#options.onMove) return true;
|
|
450
|
+
const result = await Promise.resolve(
|
|
451
|
+
this.#options.onMove(movedId, fromContainer, toContainer, orderedFromIds, orderedToIds)
|
|
452
|
+
);
|
|
453
|
+
return result !== false;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
#cleanupDragState() {
|
|
457
|
+
this.#ghostElement?.remove();
|
|
458
|
+
this.#placeholderElement?.remove();
|
|
459
|
+
|
|
460
|
+
if (this.#draggedElement) {
|
|
461
|
+
this.#draggedElement.classList.remove(this.#options.dragClass);
|
|
462
|
+
this.#draggedElement.style.opacity = "";
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
this.#containerElement.classList.remove("ua-sortable-active");
|
|
466
|
+
if (this.#currentContainerElement && this.#currentContainerElement !== this.#containerElement) {
|
|
467
|
+
this.#currentContainerElement.classList.remove("ua-sortable-active");
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
clearTimeout(this.#delayTimer);
|
|
471
|
+
document.removeEventListener("pointermove", this.#boundHandlePointerMove);
|
|
472
|
+
document.removeEventListener("pointerup", this.#boundHandlePointerUp);
|
|
473
|
+
document.removeEventListener("pointercancel", this.#boundHandlePointerCancel);
|
|
474
|
+
|
|
475
|
+
this.#ghostElement = null;
|
|
476
|
+
this.#placeholderElement = null;
|
|
477
|
+
this.#draggedElement = null;
|
|
478
|
+
this.#sourceContainerElement = null;
|
|
479
|
+
this.#currentContainerElement = null;
|
|
480
|
+
this.#isDragging = false;
|
|
481
|
+
this.#delayTimer = null;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// =========================================================================
|
|
485
|
+
// PRIVATE — Position & direction calculation
|
|
486
|
+
// =========================================================================
|
|
487
|
+
|
|
488
|
+
#updatePlaceholderPosition(pointerX, pointerY) {
|
|
489
|
+
const targetContainer = this.#currentContainerElement;
|
|
490
|
+
const children = [...targetContainer.children].filter(child =>
|
|
491
|
+
child !== this.#draggedElement &&
|
|
492
|
+
child !== this.#ghostElement &&
|
|
493
|
+
child !== this.#placeholderElement &&
|
|
494
|
+
(!this.#options.filter || !child.matches(this.#options.filter))
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
const direction = this.#resolveDirection(targetContainer);
|
|
498
|
+
let insertBeforeElement = null;
|
|
499
|
+
|
|
500
|
+
for (const sibling of children) {
|
|
501
|
+
const siblingRect = sibling.getBoundingClientRect();
|
|
502
|
+
const siblingMidpoint = direction === "horizontal"
|
|
503
|
+
? siblingRect.left + siblingRect.width / 2
|
|
504
|
+
: siblingRect.top + siblingRect.height / 2;
|
|
505
|
+
const pointerPosition = direction === "horizontal" ? pointerX : pointerY;
|
|
506
|
+
|
|
507
|
+
if (pointerPosition < siblingMidpoint) {
|
|
508
|
+
insertBeforeElement = sibling;
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (insertBeforeElement) {
|
|
514
|
+
targetContainer.insertBefore(this.#placeholderElement, insertBeforeElement);
|
|
515
|
+
} else {
|
|
516
|
+
targetContainer.appendChild(this.#placeholderElement);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
#resolveDirection(containerElement) {
|
|
521
|
+
if (this.#options.direction !== "auto") return this.#options.direction;
|
|
522
|
+
const flexDirection = getComputedStyle(containerElement).flexDirection;
|
|
523
|
+
return (flexDirection === "row" || flexDirection === "row-reverse")
|
|
524
|
+
? "horizontal"
|
|
525
|
+
: "vertical";
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
#findDraggableParent(targetElement) {
|
|
529
|
+
let current = targetElement;
|
|
530
|
+
while (current && current !== this.#containerElement) {
|
|
531
|
+
if (current.parentElement === this.#containerElement) return current;
|
|
532
|
+
current = current.parentElement;
|
|
533
|
+
}
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
#findTargetContainer(pointerX, pointerY) {
|
|
538
|
+
const groupName = this.#options.group;
|
|
539
|
+
if (!groupName) return this.#containerElement;
|
|
540
|
+
|
|
541
|
+
const groupInstances = UA_Sortable.getGroup(groupName);
|
|
542
|
+
for (const instance of groupInstances) {
|
|
543
|
+
if (instance === this) continue;
|
|
544
|
+
if (instance.#options.disabled) continue;
|
|
545
|
+
const containerRect = instance.#containerElement.getBoundingClientRect();
|
|
546
|
+
if (
|
|
547
|
+
pointerX >= containerRect.left &&
|
|
548
|
+
pointerX <= containerRect.right &&
|
|
549
|
+
pointerY >= containerRect.top &&
|
|
550
|
+
pointerY <= containerRect.bottom
|
|
551
|
+
) {
|
|
552
|
+
return instance.#containerElement;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return this.#containerElement;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// --- Inject minimal CSS once ---
|
|
560
|
+
(function injectUASortableCSS() {
|
|
561
|
+
if (typeof document === "undefined") return;
|
|
562
|
+
if (document.getElementById("ua-sortable-styles")) return;
|
|
563
|
+
const style = document.createElement("style");
|
|
564
|
+
style.id = "ua-sortable-styles";
|
|
565
|
+
style.textContent = [
|
|
566
|
+
".ua-sortable-ghost{opacity:.4;}",
|
|
567
|
+
".ua-sortable-drag{opacity:.4;}",
|
|
568
|
+
".ua-sortable-active>.ua-sortable-over{border-top:2px solid var(--accent,#2563eb);}",
|
|
569
|
+
".ua-drag-handle{cursor:grab;touch-action:none;}",
|
|
570
|
+
".ua-drag-handle:active{cursor:grabbing;}",
|
|
571
|
+
].join("");
|
|
572
|
+
document.head.appendChild(style);
|
|
573
|
+
})();
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Shorthand: make a container element sortable.
|
|
577
|
+
* @param {HTMLElement} el
|
|
578
|
+
* @param {object} [options]
|
|
579
|
+
* @returns {UA_Sortable}
|
|
580
|
+
*/
|
|
581
|
+
export function uaMakeSortable(el, options = {}) {
|
|
582
|
+
return new UA_Sortable(el, options);
|
|
583
|
+
}
|