@fr0st/ui-sortable 2.0.4 → 3.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/LICENSE CHANGED
File without changes
package/README.md CHANGED
@@ -1,37 +1,375 @@
1
- # FrostUI Sortable
2
-
3
- **FrostUI Sortable** is a free, open-source Sortable component for *JavaScript*.
4
-
5
- It is built on top of the [FrostUI](https://github.com/elusivecodes/FrostUI) library.
6
-
7
-
8
- ## Table Of Contents
9
- - [Installation](#installation)
10
- - [Usage](#usage)
11
-
12
-
13
-
14
- ## Installation
15
-
16
- **Dependencies**
17
-
18
- - [fQuery](https://github.com/elusivecodes/fQuery)
19
- - [FrostUI](https://github.com/elusivecodes/FrostUI)
20
-
21
- **CSS**
22
-
23
- ```html
24
- <link rel="stylesheet" href="/path/to/frost-ui.min.css" />
25
- ```
26
-
27
- **JS**
28
-
29
- ```html
30
- <script type="text/javascript" src="/path/to/frost-ui-bundle.min.js"></script>
31
- <script type="text/javascript" src="/path/to/frost-ui-sortable.min.js"></script>
32
- ```
33
-
34
-
35
- ## Usage
36
-
37
- Check the demo folder for examples. Full documentation and website coming soon™.
1
+ # Frost UI Sortable
2
+
3
+ [![CI](https://github.com/frost-js/ui-sortable/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/frost-js/ui-sortable/actions/workflows/ci.yml)
4
+ [![codecov](https://codecov.io/gh/frost-js/ui-sortable/branch/main/graph/badge.svg)](https://codecov.io/gh/frost-js/ui-sortable)
5
+ [![npm version](https://img.shields.io/npm/v/%40fr0st%2Fui-sortable?style=flat-square)](https://www.npmjs.com/package/@fr0st/ui-sortable)
6
+ [![npm downloads](https://img.shields.io/npm/dm/%40fr0st%2Fui-sortable?style=flat-square)](https://www.npmjs.com/package/@fr0st/ui-sortable)
7
+ [![JS gzip size](https://img.badgesize.io/frost-js/ui-sortable/main/dist/frost-ui-sortable.min.js?compression=gzip&label=JS%20gzip%20size&style=flat-square)](https://github.com/frost-js/ui-sortable/blob/main/dist/frost-ui-sortable.min.js)
8
+ [![license](https://img.shields.io/github/license/frost-js/ui-sortable?style=flat-square)](./LICENSE)
9
+
10
+ Drag-and-drop sorting for Frost UI with handles, connected containers, axis control, lifecycle events, and automatic window scrolling.
11
+
12
+ ## Highlights
13
+
14
+ - Reorder vertical, horizontal, and free-form collections
15
+ - Restrict dragging to a handle and exclude interactive controls
16
+ - Move items between connected containers, including empty lists
17
+ - Disable individual source or destination instances
18
+ - Automatic window scrolling near viewport edges
19
+ - Configurable, direction-aware item swap threshold
20
+ - Normalized item, container, and index data on lifecycle events
21
+ - Native `Sortable` class and `sortable` fQuery plugin
22
+ - Prebuilt ESM and UMD bundles with source maps
23
+ - No component-specific CSS or Sass
24
+ - JSDoc-powered IntelliSense
25
+
26
+ ## Installation
27
+
28
+ ### Browser projects / bundlers
29
+
30
+ Install Sortable with its Frost UI v4 and fQuery v5 peers:
31
+
32
+ ```bash
33
+ npm i @fr0st/ui-sortable@3 @fr0st/ui@4 @fr0st/query@5
34
+ ```
35
+
36
+ The package root resolves to the compiled ESM bundle. Import the Frost UI stylesheet and the default component export:
37
+
38
+ ```js
39
+ import '@fr0st/ui/dist/frost-ui.min.css';
40
+ import Sortable from '@fr0st/ui-sortable';
41
+
42
+ const sortable = Sortable.init(
43
+ document.querySelector('#tasks'),
44
+ {
45
+ cursor: 'grab',
46
+ handle: '.drag-handle',
47
+ },
48
+ );
49
+ ```
50
+
51
+ `@fr0st/ui` and `@fr0st/query` are peer dependencies so the component shares the application's UI and fQuery instances. The package root, `dist/*`, and `src/*` are available through package exports.
52
+
53
+ Sortable requires a browser DOM or a compatible DOM environment configured through fQuery. Server-rendered applications should load the component on the client.
54
+
55
+ ### Browser (ESM)
56
+
57
+ The ESM bundle imports `@fr0st/ui` and `@fr0st/query`. Frost UI and fQuery also require `@fr0st/core`, so map all three dependencies when loading the bundle directly in a browser:
58
+
59
+ ```html
60
+ <link
61
+ rel="stylesheet"
62
+ href="https://cdn.jsdelivr.net/npm/@fr0st/ui@4/dist/frost-ui.min.css">
63
+
64
+ <script type="importmap">
65
+ {
66
+ "imports": {
67
+ "@fr0st/core": "https://cdn.jsdelivr.net/npm/@fr0st/core@4/dist/frost-core.esm.min.js",
68
+ "@fr0st/query": "https://cdn.jsdelivr.net/npm/@fr0st/query@5/dist/fquery.esm.min.js",
69
+ "@fr0st/ui": "https://cdn.jsdelivr.net/npm/@fr0st/ui@4/dist/frost-ui.esm.min.js"
70
+ }
71
+ }
72
+ </script>
73
+ <script type="module">
74
+ import Sortable from 'https://cdn.jsdelivr.net/npm/@fr0st/ui-sortable@3/dist/frost-ui-sortable.esm.min.js';
75
+
76
+ Sortable.init(document.querySelector('#tasks'));
77
+ </script>
78
+ ```
79
+
80
+ ### Browser (UMD)
81
+
82
+ Load Frost UI's all-in-one bundle before Sortable. The UI bundle supplies both the `UI` and `fQuery` globals expected by the component:
83
+
84
+ ```html
85
+ <link
86
+ rel="stylesheet"
87
+ href="https://cdn.jsdelivr.net/npm/@fr0st/ui@4/dist/frost-ui.min.css">
88
+
89
+ <script src="https://cdn.jsdelivr.net/npm/@fr0st/ui@4/dist/frost-ui-bundle.min.js"></script>
90
+ <script src="https://cdn.jsdelivr.net/npm/@fr0st/ui-sortable@3/dist/frost-ui-sortable.min.js"></script>
91
+ <script>
92
+ const sortable = UI.Sortable.init(
93
+ document.querySelector('#tasks'),
94
+ );
95
+ </script>
96
+ ```
97
+
98
+ The UMD bundle adds `Sortable` to the existing `globalThis.UI` object. It expects `globalThis.UI` and `globalThis.fQuery` to exist before it loads. If the non-bundled Frost UI build is used instead, load fQuery, Frost UI, and Sortable in that order.
99
+
100
+ Do not load the separate fQuery script when using `frost-ui-bundle.js` or `frost-ui-bundle.min.js`.
101
+
102
+ ## Usage
103
+
104
+ Sortable targets direct `<li>` children by default:
105
+
106
+ ```html
107
+ <ul class="list-group" id="tasks">
108
+ <li class="list-group-item">Research</li>
109
+ <li class="list-group-item">Design</li>
110
+ <li class="list-group-item">Build</li>
111
+ </ul>
112
+ ```
113
+
114
+ ```js
115
+ import Sortable from '@fr0st/ui-sortable';
116
+
117
+ const sortable = Sortable.init(
118
+ document.querySelector('#tasks'),
119
+ { cursor: 'grab' },
120
+ );
121
+ ```
122
+
123
+ Calling `Sortable.init()` again for the same container returns its existing instance. Dispose the current instance before reinitializing the container with different options.
124
+
125
+ ### Handles and cancel controls
126
+
127
+ Use `handle` when only part of an item should begin a drag. Inputs, text areas, buttons, selects, and options are excluded by the default `cancel` selector, including nested content inside those controls.
128
+
129
+ Cancel checks include ancestors of the handle up to, but not including, the sortable container. A handle inside a button is therefore excluded too. Use `cancel: null` to disable these exclusions, or supply a custom selector.
130
+
131
+ ```html
132
+ <ul class="list-group" id="tasks">
133
+ <li class="list-group-item d-flex align-items-center gap-3">
134
+ <span class="drag-handle badge bg-secondary">Drag</span>
135
+ <span class="flex-grow-1">Review pull request</span>
136
+ <button class="btn btn-sm btn-outline-danger" type="button">Delete</button>
137
+ </li>
138
+ </ul>
139
+ ```
140
+
141
+ ```js
142
+ Sortable.init(document.querySelector('#tasks'), {
143
+ cursor: 'grab',
144
+ handle: '.drag-handle',
145
+ });
146
+ ```
147
+
148
+ Both `items` and `handle` accept comma-separated selectors. For example, `items: ':scope > .task, :scope > .note'` with `handle: '.drag-handle, .grip'` allows either handle inside either item type.
149
+
150
+ ### Connected containers
151
+
152
+ Initialize each container with a shared `connectWith` selector. Give empty containers a visible size so pointer proximity can select them.
153
+
154
+ ```html
155
+ <div class="row">
156
+ <div class="col">
157
+ <ul class="connected list-group" id="available">
158
+ <li class="list-group-item">Alpha</li>
159
+ <li class="list-group-item">Bravo</li>
160
+ </ul>
161
+ </div>
162
+ <div class="col">
163
+ <ul class="connected list-group" id="selected" style="min-height: 3rem"></ul>
164
+ </div>
165
+ </div>
166
+ ```
167
+
168
+ ```js
169
+ document.querySelectorAll('.connected').forEach((node) => {
170
+ Sortable.init(node, {
171
+ connectWith: '.connected',
172
+ cursor: 'grab',
173
+ });
174
+ });
175
+ ```
176
+
177
+ The receiving instance controls `dropOnEmpty` and `disabled`. Moving between containers triggers `send` on the previous container and `receive` on the new container.
178
+
179
+ Containers can use different `cursor` and `handle` settings. On transfer, Sortable restores the previous container's cursor targets before `send`, then applies the receiving container's cursor to its items or handles during `receive`.
180
+
181
+ ## Options
182
+
183
+ Options are resolved in this order:
184
+
185
+ 1. Component defaults
186
+ 2. The container's `data-ui-*` attributes
187
+ 3. Options passed to `Sortable.init()`
188
+
189
+ Resolved `instance.options` are frozen.
190
+
191
+ | Option | Type | Default | Description |
192
+ | --- | --- | --- | --- |
193
+ | `axis` | `'x' \| 'y' \| null` | `null` | Constrain item placement decisions to one axis. |
194
+ | `cancel` | `string \| null` | `'input, textarea, button, select, option'` | Prevent matching controls or their nested content from starting a sort. |
195
+ | `connectWith` | `string \| null` | `null` | Select other Sortable containers that can exchange items. |
196
+ | `cursor` | `string` | `'auto'` | Set the cursor on sortable items or configured handles. |
197
+ | `disabled` | `boolean` | `false` | Disable sorting and connected-list receiving initially. |
198
+ | `dropOnEmpty` | `boolean` | `true` | Allow this container to receive items while empty. |
199
+ | `handle` | `string \| null` | `null` | Select the drag handle inside each sortable item. |
200
+ | `helperClass` | `string \| null` | `'bg-body-tertiary'` | Add one or more classes to the active item during a drag. |
201
+ | `items` | `string` | `':scope > li'` | Select sortable items relative to the container. |
202
+ | `scroll` | `boolean` | `true` | Scroll the window while dragging near a viewport edge. |
203
+ | `scrollSensitivity` | `number` | `20` | Start auto-scrolling within this distance of an edge, in pixels. |
204
+ | `scrollSpeed` | `number` | `5` | Scroll this many pixels per animation frame. |
205
+ | `swapThreshold` | `number` | `0.25` | Swap after crossing this fraction of the next item, from `0` to `1`. The threshold is mirrored when reversing direction. |
206
+
207
+ ```js
208
+ const sortable = Sortable.init(node, {
209
+ axis: 'y',
210
+ cancel: 'input, textarea, button, select, option, [data-no-sort]',
211
+ connectWith: '.task-list',
212
+ cursor: 'grab',
213
+ disabled: false,
214
+ dropOnEmpty: true,
215
+ handle: '.drag-handle',
216
+ helperClass: 'bg-primary-subtle shadow-sm',
217
+ items: ':scope > .task',
218
+ scroll: true,
219
+ scrollSensitivity: 32,
220
+ scrollSpeed: 8,
221
+ swapThreshold: 0.25,
222
+ });
223
+ ```
224
+
225
+ Auto-scrolling updates item placement and sorting events even while the pointer remains stationary near an edge. Scroll steps are instant regardless of the page's CSS `scroll-behavior`; Sortable does not modify that style.
226
+
227
+ ## Data attributes
228
+
229
+ Every option can be supplied through a `data-ui-*` attribute. fQuery normalizes boolean, numeric, array, object, and `null` values.
230
+
231
+ ```html
232
+ <ul
233
+ id="tasks"
234
+ data-ui-toggle="sortable"
235
+ data-ui-axis="y"
236
+ data-ui-connect-with=".task-list"
237
+ data-ui-cursor="grab"
238
+ data-ui-drop-on-empty="true"
239
+ data-ui-handle=".drag-handle"
240
+ data-ui-helper-class="bg-primary-subtle shadow-sm"
241
+ data-ui-items=":scope > .task"
242
+ data-ui-scroll="true"
243
+ data-ui-scroll-sensitivity="32"
244
+ data-ui-scroll-speed="8"
245
+ data-ui-swap-threshold="0.25">
246
+ </ul>
247
+ ```
248
+
249
+ Data attributes do not initialize the component automatically. Initialize matching elements directly or through the fQuery plugin:
250
+
251
+ ```js
252
+ $('[data-ui-toggle="sortable"]').sortable();
253
+ ```
254
+
255
+ ## Methods
256
+
257
+ ### `Sortable.init(node, options?)`
258
+
259
+ Return the existing instance for `node`, or create one with the supplied options.
260
+
261
+ ### `items()`
262
+
263
+ Return a fresh array of elements matching the configured `items` selector.
264
+
265
+ ```js
266
+ const items = sortable.items();
267
+ ```
268
+
269
+ ### `disable()`
270
+
271
+ Stop the instance from starting sorts or receiving connected items.
272
+
273
+ ### `enable()`
274
+
275
+ Allow the instance to start and receive sorts.
276
+
277
+ ### `dispose()`
278
+
279
+ Detach Sortable events, restore original inline cursor declarations (including `!important` priority), release cursor style locks, cancel active auto-scrolling, clear active drag state, remove stored component data, and set `node` and `options` to `null`. Calling `dispose()` again has no effect.
280
+
281
+ ## fQuery plugin
282
+
283
+ Registration adds `sortable` to `fQuery.QuerySet.prototype`:
284
+
285
+ ```js
286
+ const sortable = $('#tasks').sortable({ cursor: 'grab' });
287
+
288
+ $('#tasks').sortable('disable');
289
+ $('#tasks').sortable('enable');
290
+ const items = $('#tasks').sortable('items');
291
+ $('#tasks').sortable('dispose');
292
+ ```
293
+
294
+ Calling the plugin on multiple elements initializes every valid element and returns the first result.
295
+
296
+ ## Events
297
+
298
+ Listen with fQuery's namespaced event API:
299
+
300
+ ```js
301
+ $.addEvent(
302
+ '#tasks',
303
+ 'sort.ui.sortable sorting.ui.sortable sorted.ui.sortable update.ui.sortable',
304
+ (event) => {
305
+ console.log({
306
+ item: event.item,
307
+ from: event.from,
308
+ to: event.to,
309
+ oldIndex: event.oldIndex,
310
+ newIndex: event.newIndex,
311
+ });
312
+ },
313
+ );
314
+ ```
315
+
316
+ | Event | Target | Description |
317
+ | --- | --- | --- |
318
+ | `sort.ui.sortable` | Source | The first movement begins an active sort. |
319
+ | `sorting.ui.sortable` | Current container | The item changes index within the same container. |
320
+ | `send.ui.sortable` | Previous container | The item moves out to a connected container. |
321
+ | `receive.ui.sortable` | New container | The item enters from a connected container. |
322
+ | `sorted.ui.sortable` | Final container | The pointer or touch is released, or the touch sequence is cancelled, after an active sort. |
323
+ | `update.ui.sortable` | Changed container(s) | The final order changes. Cross-container moves update both source and destination. |
324
+
325
+ Every event exposes the same normalized fields directly and through `event.detail`:
326
+
327
+ | Field | Type | Description |
328
+ | --- | --- | --- |
329
+ | `item` | `HTMLElement` | Item being sorted. |
330
+ | `from` | `HTMLElement` | Source for `sort`, current container for `sorting`, previous container for `send`/`receive`, and original source for `sorted`/`update`. |
331
+ | `to` | `HTMLElement` | Source for `sort`, current container for `sorting`, new container for `send`/`receive`, and final destination for `sorted`/`update`. |
332
+ | `oldIndex` | `number` | Previous index for movement events, otherwise the original index. |
333
+ | `newIndex` | `number` | New index for movement events, otherwise the final index. |
334
+ | `detail.target` | `HTMLElement` | Alias of the active `item` retained for Frost UI event compatibility. |
335
+
336
+ Indices are calculated among elements matching the container's configured `items` selector; unrelated child elements are not counted.
337
+
338
+ ## Styling and accessibility
339
+
340
+ Sortable does not ship component-specific CSS or Sass. It composes existing Frost UI classes and leaves collection layout to the application. The default helper class, `bg-body-tertiary`, comes from Frost UI and can be replaced or disabled with `helperClass: null`.
341
+
342
+ Pointer sorting does not replace keyboard-accessible ordering controls. For workflows where order is essential, provide buttons or another keyboard mechanism that performs the same move and communicates the updated position to assistive technology. A visible drag handle should have an accessible label that describes its purpose.
343
+
344
+ For touch interfaces, applying `touch-action: none` to sortable items or handles prevents browser gestures from competing with dragging:
345
+
346
+ ```css
347
+ .drag-handle {
348
+ touch-action: none;
349
+ user-select: none;
350
+ }
351
+ ```
352
+
353
+ ## Browser support
354
+
355
+ Bundles are built with Vite's `baseline-widely-available` target. The automated browser suite is configured for the Chromium, Firefox, and WebKit versions supplied by the installed Playwright release.
356
+
357
+ ## Development
358
+
359
+ Use Node.js matching `^20.19.0 || ^22.13.0 || >=24`. Install dependencies with `npm ci`, then install Playwright browsers with `npx playwright install --with-deps`.
360
+
361
+ ```bash
362
+ npm test
363
+ npm run lint
364
+ npm run build
365
+ ```
366
+
367
+ `npm test` rebuilds JavaScript, then runs the Playwright suite in Chromium, Firefox, and WebKit. `npm run test:browser` runs the suite against the existing bundles, so rebuild after changing source files.
368
+
369
+ After building, `npm run test:coverage` runs Chromium tests and writes coverage reports to `coverage/` and `test-results/coverage/`.
370
+
371
+ `npm run test:headed` and `npm run test:ui` also use the existing bundles and open headed browsers or the Playwright UI.
372
+
373
+ ## License
374
+
375
+ Frost UI Sortable is released under the [MIT License](./LICENSE).