@bquery/bquery 1.7.0 → 1.8.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/README.md +760 -716
- package/dist/{a11y-C5QOVvRn.js → a11y-DVBCy09c.js} +3 -3
- package/dist/a11y-DVBCy09c.js.map +1 -0
- package/dist/a11y.es.mjs +1 -1
- package/dist/component/library.d.ts.map +1 -1
- package/dist/{component-CuuTijA6.js → component-L3-JfOFz.js} +5 -5
- package/dist/component-L3-JfOFz.js.map +1 -0
- package/dist/component.es.mjs +1 -1
- package/dist/{config-BW35FKuA.js → config-DhT9auRm.js} +1 -1
- package/dist/{config-BW35FKuA.js.map → config-DhT9auRm.js.map} +1 -1
- package/dist/{constraints-3lV9yyBw.js → constraints-D5RHQLmP.js} +1 -1
- package/dist/constraints-D5RHQLmP.js.map +1 -0
- package/dist/core/collection.d.ts +86 -0
- package/dist/core/collection.d.ts.map +1 -1
- package/dist/core/element.d.ts +28 -0
- package/dist/core/element.d.ts.map +1 -1
- package/dist/core/shared.d.ts +6 -0
- package/dist/core/shared.d.ts.map +1 -1
- package/dist/core-DdtZHzsS.js +168 -0
- package/dist/core-DdtZHzsS.js.map +1 -0
- package/dist/{core-Cjl7GUu8.js → core-EMYSLzaT.js} +289 -259
- package/dist/core-EMYSLzaT.js.map +1 -0
- package/dist/core.es.mjs +48 -47
- package/dist/{custom-directives-7wAShnnd.js → custom-directives-Dr4C5lVV.js} +1 -1
- package/dist/custom-directives-Dr4C5lVV.js.map +1 -0
- package/dist/{devtools-D2fQLhDN.js → devtools-BhB2iDPT.js} +2 -2
- package/dist/devtools-BhB2iDPT.js.map +1 -0
- package/dist/devtools.es.mjs +1 -1
- package/dist/{dnd-B8EgyzaI.js → dnd-NwZBYh4l.js} +1 -1
- package/dist/dnd-NwZBYh4l.js.map +1 -0
- package/dist/dnd.es.mjs +1 -1
- package/dist/{env-NeVmr4Gf.js → env-CTdvLaH2.js} +1 -1
- package/dist/env-CTdvLaH2.js.map +1 -0
- package/dist/forms/create-form.d.ts.map +1 -1
- package/dist/forms/index.d.ts +3 -2
- package/dist/forms/index.d.ts.map +1 -1
- package/dist/forms/types.d.ts +46 -0
- package/dist/forms/types.d.ts.map +1 -1
- package/dist/forms/use-field.d.ts +34 -0
- package/dist/forms/use-field.d.ts.map +1 -0
- package/dist/forms/validators.d.ts +25 -0
- package/dist/forms/validators.d.ts.map +1 -1
- package/dist/forms-UcRHsYxC.js +227 -0
- package/dist/forms-UcRHsYxC.js.map +1 -0
- package/dist/forms.es.mjs +14 -12
- package/dist/full.d.ts +17 -26
- package/dist/full.d.ts.map +1 -1
- package/dist/full.es.mjs +206 -181
- package/dist/full.iife.js +33 -33
- package/dist/full.iife.js.map +1 -1
- package/dist/full.umd.js +33 -33
- package/dist/full.umd.js.map +1 -1
- package/dist/function-Cybd57JV.js +33 -0
- package/dist/function-Cybd57JV.js.map +1 -0
- package/dist/{i18n-BnnhTFOS.js → i18n-kuF6Ekj6.js} +3 -3
- package/dist/i18n-kuF6Ekj6.js.map +1 -0
- package/dist/i18n.es.mjs +1 -1
- package/dist/index.es.mjs +251 -228
- package/dist/media/breakpoints.d.ts.map +1 -1
- package/dist/media/types.d.ts +2 -2
- package/dist/media/types.d.ts.map +1 -1
- package/dist/{media-Di2Ta22s.js → media-i-fB5WxI.js} +3 -3
- package/dist/media-i-fB5WxI.js.map +1 -0
- package/dist/media.es.mjs +1 -1
- package/dist/{motion-qPj_TYGv.js → motion-BJsAuULb.js} +2 -2
- package/dist/motion-BJsAuULb.js.map +1 -0
- package/dist/motion.es.mjs +1 -1
- package/dist/{mount-SM07RUa6.js → mount-B4Y8bk8Z.js} +5 -5
- package/dist/mount-B4Y8bk8Z.js.map +1 -0
- package/dist/{platform-CPbCprb6.js → platform-Dw2gE3zI.js} +3 -3
- package/dist/{platform-CPbCprb6.js.map → platform-Dw2gE3zI.js.map} +1 -1
- package/dist/platform.es.mjs +2 -2
- package/dist/plugin/registry.d.ts.map +1 -1
- package/dist/{plugin-cPoOHFLY.js → plugin-C2WuC8SF.js} +20 -18
- package/dist/plugin-C2WuC8SF.js.map +1 -0
- package/dist/plugin.es.mjs +1 -1
- package/dist/reactive/async-data.d.ts +28 -3
- package/dist/reactive/async-data.d.ts.map +1 -1
- package/dist/reactive/computed.d.ts +3 -0
- package/dist/reactive/computed.d.ts.map +1 -1
- package/dist/reactive/effect.d.ts +3 -0
- package/dist/reactive/effect.d.ts.map +1 -1
- package/dist/reactive/http.d.ts +194 -0
- package/dist/reactive/http.d.ts.map +1 -0
- package/dist/reactive/index.d.ts +2 -2
- package/dist/reactive/index.d.ts.map +1 -1
- package/dist/reactive/pagination.d.ts +126 -0
- package/dist/reactive/pagination.d.ts.map +1 -0
- package/dist/reactive/polling.d.ts +55 -0
- package/dist/reactive/polling.d.ts.map +1 -0
- package/dist/reactive/readonly.d.ts +20 -1
- package/dist/reactive/readonly.d.ts.map +1 -1
- package/dist/reactive/rest.d.ts +293 -0
- package/dist/reactive/rest.d.ts.map +1 -0
- package/dist/reactive/scope.d.ts +140 -0
- package/dist/reactive/scope.d.ts.map +1 -0
- package/dist/reactive/signal.d.ts +16 -2
- package/dist/reactive/signal.d.ts.map +1 -1
- package/dist/reactive/to-value.d.ts +57 -0
- package/dist/reactive/to-value.d.ts.map +1 -0
- package/dist/reactive/websocket.d.ts +285 -0
- package/dist/reactive/websocket.d.ts.map +1 -0
- package/dist/reactive-DwkhUJfP.js +1148 -0
- package/dist/reactive-DwkhUJfP.js.map +1 -0
- package/dist/reactive.es.mjs +38 -19
- package/dist/{registry-CWf368tT.js → registry-B08iilIh.js} +1 -1
- package/dist/{registry-CWf368tT.js.map → registry-B08iilIh.js.map} +1 -1
- package/dist/router/constraints.d.ts.map +1 -1
- package/dist/router/index.d.ts +1 -1
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/router.d.ts.map +1 -1
- package/dist/router/state.d.ts +25 -2
- package/dist/router/state.d.ts.map +1 -1
- package/dist/router-CQikC9Ed.js +492 -0
- package/dist/router-CQikC9Ed.js.map +1 -0
- package/dist/router.es.mjs +9 -8
- package/dist/ssr/hydrate.d.ts.map +1 -1
- package/dist/{ssr-B2qd_WBB.js → ssr-_dAcGdzu.js} +4 -4
- package/dist/ssr-_dAcGdzu.js.map +1 -0
- package/dist/ssr.es.mjs +1 -1
- package/dist/store/persisted.d.ts.map +1 -1
- package/dist/{store-DWpyH6p5.js → store-Cb3gPRve.js} +7 -7
- package/dist/store-Cb3gPRve.js.map +1 -0
- package/dist/store.es.mjs +2 -2
- package/dist/storybook.es.mjs.map +1 -1
- package/dist/{testing-CsqjNUyy.js → testing-C5Sjfsna.js} +8 -8
- package/dist/testing-C5Sjfsna.js.map +1 -0
- package/dist/testing.es.mjs +1 -1
- package/dist/{type-guards-Do9DWgNp.js → type-guards-BMX2c0LP.js} +1 -1
- package/dist/{type-guards-Do9DWgNp.js.map → type-guards-BMX2c0LP.js.map} +1 -1
- package/dist/untrack-D0fnO5k2.js +36 -0
- package/dist/untrack-D0fnO5k2.js.map +1 -0
- package/dist/view/custom-directives.d.ts.map +1 -1
- package/dist/view.es.mjs +4 -4
- package/package.json +177 -177
- package/src/a11y/announce.ts +131 -131
- package/src/a11y/audit.ts +314 -314
- package/src/a11y/index.ts +68 -68
- package/src/a11y/media-preferences.ts +255 -255
- package/src/a11y/roving-tab-index.ts +164 -164
- package/src/a11y/skip-link.ts +255 -255
- package/src/a11y/trap-focus.ts +184 -184
- package/src/a11y/types.ts +183 -183
- package/src/component/component.ts +599 -599
- package/src/component/html.ts +153 -153
- package/src/component/index.ts +52 -52
- package/src/component/library.ts +540 -542
- package/src/component/scope.ts +212 -212
- package/src/component/types.ts +310 -310
- package/src/core/collection.ts +876 -707
- package/src/core/element.ts +1015 -981
- package/src/core/env.ts +60 -60
- package/src/core/index.ts +49 -49
- package/src/core/shared.ts +77 -62
- package/src/core/utils/index.ts +148 -148
- package/src/devtools/devtools.ts +410 -410
- package/src/devtools/index.ts +48 -48
- package/src/devtools/types.ts +104 -104
- package/src/dnd/draggable.ts +296 -296
- package/src/dnd/droppable.ts +228 -228
- package/src/dnd/index.ts +62 -62
- package/src/dnd/sortable.ts +307 -307
- package/src/dnd/types.ts +293 -293
- package/src/forms/create-form.ts +320 -278
- package/src/forms/index.ts +70 -65
- package/src/forms/types.ts +203 -154
- package/src/forms/use-field.ts +231 -0
- package/src/forms/validators.ts +294 -265
- package/src/full.ts +554 -480
- package/src/i18n/formatting.ts +67 -67
- package/src/i18n/i18n.ts +200 -200
- package/src/i18n/index.ts +67 -67
- package/src/i18n/translate.ts +182 -182
- package/src/i18n/types.ts +171 -171
- package/src/index.ts +108 -108
- package/src/media/battery.ts +116 -116
- package/src/media/breakpoints.ts +129 -131
- package/src/media/clipboard.ts +80 -80
- package/src/media/device-sensors.ts +158 -158
- package/src/media/geolocation.ts +119 -119
- package/src/media/index.ts +76 -76
- package/src/media/media-query.ts +92 -92
- package/src/media/network.ts +115 -115
- package/src/media/types.ts +177 -177
- package/src/media/viewport.ts +84 -84
- package/src/motion/index.ts +57 -57
- package/src/motion/morph.ts +151 -151
- package/src/motion/parallax.ts +120 -120
- package/src/motion/reduced-motion.ts +66 -66
- package/src/motion/types.ts +271 -271
- package/src/motion/typewriter.ts +164 -164
- package/src/plugin/index.ts +37 -37
- package/src/plugin/registry.ts +284 -269
- package/src/plugin/types.ts +137 -137
- package/src/reactive/async-data.ts +250 -29
- package/src/reactive/computed.ts +144 -130
- package/src/reactive/effect.ts +29 -6
- package/src/reactive/http.ts +790 -0
- package/src/reactive/index.ts +60 -0
- package/src/reactive/pagination.ts +317 -0
- package/src/reactive/polling.ts +179 -0
- package/src/reactive/readonly.ts +52 -8
- package/src/reactive/rest.ts +859 -0
- package/src/reactive/scope.ts +276 -0
- package/src/reactive/signal.ts +61 -1
- package/src/reactive/to-value.ts +71 -0
- package/src/reactive/websocket.ts +849 -0
- package/src/router/bq-link.ts +279 -279
- package/src/router/constraints.ts +204 -201
- package/src/router/index.ts +49 -49
- package/src/router/match.ts +312 -312
- package/src/router/path-pattern.ts +52 -52
- package/src/router/query.ts +38 -38
- package/src/router/router.ts +421 -402
- package/src/router/state.ts +51 -3
- package/src/router/types.ts +139 -139
- package/src/router/use-route.ts +68 -68
- package/src/router/utils.ts +157 -157
- package/src/security/index.ts +12 -12
- package/src/ssr/hydrate.ts +84 -82
- package/src/ssr/index.ts +70 -70
- package/src/ssr/render.ts +508 -508
- package/src/ssr/serialize.ts +296 -296
- package/src/ssr/types.ts +81 -81
- package/src/store/create-store.ts +467 -467
- package/src/store/index.ts +27 -27
- package/src/store/persisted.ts +245 -249
- package/src/store/types.ts +247 -247
- package/src/store/utils.ts +135 -135
- package/src/storybook/index.ts +480 -480
- package/src/testing/index.ts +42 -42
- package/src/testing/testing.ts +593 -593
- package/src/testing/types.ts +170 -170
- package/src/view/custom-directives.ts +28 -30
- package/src/view/evaluate.ts +292 -292
- package/src/view/process.ts +108 -108
- package/dist/a11y-C5QOVvRn.js.map +0 -1
- package/dist/component-CuuTijA6.js.map +0 -1
- package/dist/constraints-3lV9yyBw.js.map +0 -1
- package/dist/core-Cjl7GUu8.js.map +0 -1
- package/dist/core-DnlyjbF2.js +0 -112
- package/dist/core-DnlyjbF2.js.map +0 -1
- package/dist/custom-directives-7wAShnnd.js.map +0 -1
- package/dist/devtools-D2fQLhDN.js.map +0 -1
- package/dist/dnd-B8EgyzaI.js.map +0 -1
- package/dist/env-NeVmr4Gf.js.map +0 -1
- package/dist/forms-C3yovgH9.js +0 -141
- package/dist/forms-C3yovgH9.js.map +0 -1
- package/dist/i18n-BnnhTFOS.js.map +0 -1
- package/dist/media-Di2Ta22s.js.map +0 -1
- package/dist/motion-qPj_TYGv.js.map +0 -1
- package/dist/mount-SM07RUa6.js.map +0 -1
- package/dist/plugin-cPoOHFLY.js.map +0 -1
- package/dist/reactive-Cfv0RK6x.js +0 -233
- package/dist/reactive-Cfv0RK6x.js.map +0 -1
- package/dist/router-BrthaP_z.js +0 -473
- package/dist/router-BrthaP_z.js.map +0 -1
- package/dist/ssr-B2qd_WBB.js.map +0 -1
- package/dist/store-DWpyH6p5.js.map +0 -1
- package/dist/testing-CsqjNUyy.js.map +0 -1
- package/dist/untrack-DJVQQ2WM.js +0 -33
- package/dist/untrack-DJVQQ2WM.js.map +0 -1
package/src/dnd/sortable.ts
CHANGED
|
@@ -1,307 +1,307 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sortable list with animated reordering via pointer events.
|
|
3
|
-
*
|
|
4
|
-
* Makes children of a container sortable by dragging. Items are
|
|
5
|
-
* rearranged in the DOM with optional CSS animation.
|
|
6
|
-
*
|
|
7
|
-
* @module bquery/dnd
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type { SortEventData, SortableHandle, SortableOptions } from './types';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Gets the sortable items within a container.
|
|
14
|
-
* @internal
|
|
15
|
-
*/
|
|
16
|
-
const getItems = (container: HTMLElement, selector: string): HTMLElement[] => {
|
|
17
|
-
return Array.from(container.querySelectorAll(selector)) as HTMLElement[];
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Finds the closest sortable item to a given Y (or X) position.
|
|
22
|
-
* @internal
|
|
23
|
-
*/
|
|
24
|
-
const getClosestItem = (
|
|
25
|
-
items: HTMLElement[],
|
|
26
|
-
clientPos: number,
|
|
27
|
-
axis: 'x' | 'y',
|
|
28
|
-
dragged: HTMLElement
|
|
29
|
-
): { element: HTMLElement; index: number } | null => {
|
|
30
|
-
let closest: { element: HTMLElement; index: number; distance: number } | null = null;
|
|
31
|
-
|
|
32
|
-
for (let i = 0; i < items.length; i++) {
|
|
33
|
-
const item = items[i];
|
|
34
|
-
if (item === dragged) continue;
|
|
35
|
-
|
|
36
|
-
const rect = item.getBoundingClientRect();
|
|
37
|
-
const mid = axis === 'y' ? rect.top + rect.height / 2 : rect.left + rect.width / 2;
|
|
38
|
-
const distance = clientPos - mid;
|
|
39
|
-
|
|
40
|
-
if (
|
|
41
|
-
closest === null ||
|
|
42
|
-
(distance < 0 && distance > closest.distance) ||
|
|
43
|
-
(closest.distance >= 0 && distance < 0 && Math.abs(distance) < Math.abs(closest.distance))
|
|
44
|
-
) {
|
|
45
|
-
// Find the item we're just before
|
|
46
|
-
if (distance < 0) {
|
|
47
|
-
closest = { element: item, index: i, distance };
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return closest ? { element: closest.element, index: closest.index } : null;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Makes the children of a container sortable by dragging.
|
|
57
|
-
*
|
|
58
|
-
* Features:
|
|
59
|
-
* - Pointer event based (touch + mouse)
|
|
60
|
-
* - Animated reordering with configurable duration
|
|
61
|
-
* - Axis constraint (vertical or horizontal)
|
|
62
|
-
* - Optional drag handle
|
|
63
|
-
* - Placeholder element during sort
|
|
64
|
-
* - Callbacks: `onSortStart`, `onSortMove`, `onSortEnd`
|
|
65
|
-
*
|
|
66
|
-
* @param container - The container element whose children will be sortable
|
|
67
|
-
* @param options - Configuration options
|
|
68
|
-
* @returns A handle with `destroy()`, `disable()`, and `enable()` methods
|
|
69
|
-
*
|
|
70
|
-
* @example
|
|
71
|
-
* ```ts
|
|
72
|
-
* import { sortable } from '@bquery/bquery/dnd';
|
|
73
|
-
*
|
|
74
|
-
* const handle = sortable(document.querySelector('#list'), {
|
|
75
|
-
* items: 'li',
|
|
76
|
-
* axis: 'y',
|
|
77
|
-
* animationDuration: 200,
|
|
78
|
-
* onSortEnd: ({ oldIndex, newIndex }) => {
|
|
79
|
-
* console.log(`Moved from ${oldIndex} to ${newIndex}`);
|
|
80
|
-
* },
|
|
81
|
-
* });
|
|
82
|
-
*
|
|
83
|
-
* // Later:
|
|
84
|
-
* handle.destroy();
|
|
85
|
-
* ```
|
|
86
|
-
*/
|
|
87
|
-
export const sortable = (container: HTMLElement, options: SortableOptions = {}): SortableHandle => {
|
|
88
|
-
const {
|
|
89
|
-
items: itemSelector = ':scope > *',
|
|
90
|
-
axis = 'y',
|
|
91
|
-
handle,
|
|
92
|
-
placeholderClass = 'bq-sort-placeholder',
|
|
93
|
-
sortingClass = 'bq-sorting',
|
|
94
|
-
animationDuration = 200,
|
|
95
|
-
onSortStart,
|
|
96
|
-
onSortMove,
|
|
97
|
-
onSortEnd,
|
|
98
|
-
} = options;
|
|
99
|
-
|
|
100
|
-
let enabled = !options.disabled;
|
|
101
|
-
let isDragging = false;
|
|
102
|
-
let dragItem: HTMLElement | null = null;
|
|
103
|
-
let placeholder: HTMLElement | null = null;
|
|
104
|
-
let startIndex = -1;
|
|
105
|
-
let startPointerY = 0;
|
|
106
|
-
let startPointerX = 0;
|
|
107
|
-
let itemStartTop = 0;
|
|
108
|
-
let itemStartLeft = 0;
|
|
109
|
-
|
|
110
|
-
const createEventData = (item: HTMLElement, oldIdx: number, newIdx: number): SortEventData => ({
|
|
111
|
-
container,
|
|
112
|
-
item,
|
|
113
|
-
oldIndex: oldIdx,
|
|
114
|
-
newIndex: newIdx,
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
const onPointerDown = (e: PointerEvent): void => {
|
|
118
|
-
if (!enabled) return;
|
|
119
|
-
|
|
120
|
-
const target = e.target as HTMLElement;
|
|
121
|
-
|
|
122
|
-
// Find the item being dragged
|
|
123
|
-
const items = getItems(container, itemSelector);
|
|
124
|
-
let item: HTMLElement | null = null;
|
|
125
|
-
|
|
126
|
-
for (const it of items) {
|
|
127
|
-
if (it.contains(target)) {
|
|
128
|
-
item = it;
|
|
129
|
-
break;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (!item) return;
|
|
134
|
-
|
|
135
|
-
// Check handle constraint
|
|
136
|
-
if (handle && !target.closest(handle)) return;
|
|
137
|
-
|
|
138
|
-
e.preventDefault();
|
|
139
|
-
|
|
140
|
-
isDragging = true;
|
|
141
|
-
dragItem = item;
|
|
142
|
-
startIndex = items.indexOf(item);
|
|
143
|
-
startPointerY = e.clientY;
|
|
144
|
-
startPointerX = e.clientX;
|
|
145
|
-
|
|
146
|
-
const rect = item.getBoundingClientRect();
|
|
147
|
-
itemStartTop = rect.top;
|
|
148
|
-
itemStartLeft = rect.left;
|
|
149
|
-
|
|
150
|
-
// Create placeholder
|
|
151
|
-
placeholder = document.createElement('div');
|
|
152
|
-
placeholder.classList.add(placeholderClass);
|
|
153
|
-
placeholder.style.width = `${rect.width}px`;
|
|
154
|
-
placeholder.style.height = `${rect.height}px`;
|
|
155
|
-
placeholder.style.boxSizing = 'border-box';
|
|
156
|
-
|
|
157
|
-
// Style the dragged item
|
|
158
|
-
item.classList.add(sortingClass);
|
|
159
|
-
item.style.position = 'fixed';
|
|
160
|
-
item.style.width = `${rect.width}px`;
|
|
161
|
-
item.style.height = `${rect.height}px`;
|
|
162
|
-
item.style.left = `${rect.left}px`;
|
|
163
|
-
item.style.top = `${rect.top}px`;
|
|
164
|
-
item.style.zIndex = '999999';
|
|
165
|
-
item.style.pointerEvents = 'none';
|
|
166
|
-
item.style.margin = '0';
|
|
167
|
-
|
|
168
|
-
// Insert placeholder where the item was
|
|
169
|
-
item.parentNode?.insertBefore(placeholder, item);
|
|
170
|
-
|
|
171
|
-
container.setPointerCapture(e.pointerId);
|
|
172
|
-
|
|
173
|
-
onSortStart?.(createEventData(item, startIndex, startIndex));
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
const onPointerMove = (e: PointerEvent): void => {
|
|
177
|
-
if (!isDragging || !dragItem || !placeholder) return;
|
|
178
|
-
|
|
179
|
-
e.preventDefault();
|
|
180
|
-
|
|
181
|
-
const deltaX = e.clientX - startPointerX;
|
|
182
|
-
const deltaY = e.clientY - startPointerY;
|
|
183
|
-
|
|
184
|
-
// Move the dragged item
|
|
185
|
-
if (axis === 'y') {
|
|
186
|
-
dragItem.style.top = `${itemStartTop + deltaY}px`;
|
|
187
|
-
} else {
|
|
188
|
-
dragItem.style.left = `${itemStartLeft + deltaX}px`;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Find the closest item to determine insertion point
|
|
192
|
-
const items = getItems(container, itemSelector);
|
|
193
|
-
const clientPos = axis === 'y' ? e.clientY : e.clientX;
|
|
194
|
-
const closest = getClosestItem(items, clientPos, axis, dragItem);
|
|
195
|
-
|
|
196
|
-
if (closest) {
|
|
197
|
-
// Move placeholder before the closest element
|
|
198
|
-
container.insertBefore(placeholder, closest.element);
|
|
199
|
-
} else {
|
|
200
|
-
// Append to end
|
|
201
|
-
container.appendChild(placeholder);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const currentIndex = Array.from(container.children).indexOf(placeholder);
|
|
205
|
-
onSortMove?.(createEventData(dragItem, startIndex, currentIndex));
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
const onPointerUp = (e: PointerEvent): void => {
|
|
209
|
-
if (!isDragging || !dragItem || !placeholder) return;
|
|
210
|
-
|
|
211
|
-
isDragging = false;
|
|
212
|
-
const draggedItem = dragItem;
|
|
213
|
-
|
|
214
|
-
// Get final index
|
|
215
|
-
const newIndex = Array.from(container.children).indexOf(placeholder);
|
|
216
|
-
|
|
217
|
-
// Animate the item back to the placeholder position
|
|
218
|
-
const placeholderRect = placeholder.getBoundingClientRect();
|
|
219
|
-
const itemRect = draggedItem.getBoundingClientRect();
|
|
220
|
-
|
|
221
|
-
if (animationDuration > 0) {
|
|
222
|
-
const deltaX = placeholderRect.left - itemRect.left;
|
|
223
|
-
const deltaY = placeholderRect.top - itemRect.top;
|
|
224
|
-
|
|
225
|
-
draggedItem.style.transition = `transform ${animationDuration}ms ease`;
|
|
226
|
-
draggedItem.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
|
|
227
|
-
|
|
228
|
-
let finalized = false;
|
|
229
|
-
let timeoutId: number | null = null;
|
|
230
|
-
const finalize = (): void => {
|
|
231
|
-
if (finalized) return;
|
|
232
|
-
finalized = true;
|
|
233
|
-
if (timeoutId !== null) {
|
|
234
|
-
window.clearTimeout(timeoutId);
|
|
235
|
-
timeoutId = null;
|
|
236
|
-
}
|
|
237
|
-
resetDragItem();
|
|
238
|
-
onSortEnd?.(createEventData(draggedItem, startIndex, newIndex));
|
|
239
|
-
};
|
|
240
|
-
timeoutId = window.setTimeout(() => {
|
|
241
|
-
finalize();
|
|
242
|
-
}, animationDuration + 50);
|
|
243
|
-
|
|
244
|
-
draggedItem.addEventListener('transitionend', finalize, { once: true });
|
|
245
|
-
} else {
|
|
246
|
-
resetDragItem();
|
|
247
|
-
onSortEnd?.(createEventData(draggedItem, startIndex, newIndex));
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
container.releasePointerCapture(e.pointerId);
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
const resetDragItem = (): void => {
|
|
254
|
-
if (!dragItem || !placeholder) return;
|
|
255
|
-
|
|
256
|
-
// Insert the real item where the placeholder is
|
|
257
|
-
placeholder.parentNode?.insertBefore(dragItem, placeholder);
|
|
258
|
-
placeholder.remove();
|
|
259
|
-
placeholder = null;
|
|
260
|
-
|
|
261
|
-
// Reset styles
|
|
262
|
-
dragItem.classList.remove(sortingClass);
|
|
263
|
-
dragItem.style.position = '';
|
|
264
|
-
dragItem.style.width = '';
|
|
265
|
-
dragItem.style.height = '';
|
|
266
|
-
dragItem.style.left = '';
|
|
267
|
-
dragItem.style.top = '';
|
|
268
|
-
dragItem.style.zIndex = '';
|
|
269
|
-
dragItem.style.pointerEvents = '';
|
|
270
|
-
dragItem.style.margin = '';
|
|
271
|
-
dragItem.style.transition = '';
|
|
272
|
-
dragItem.style.transform = '';
|
|
273
|
-
|
|
274
|
-
dragItem = null;
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
container.addEventListener('pointerdown', onPointerDown);
|
|
278
|
-
container.addEventListener('pointermove', onPointerMove);
|
|
279
|
-
container.addEventListener('pointerup', onPointerUp);
|
|
280
|
-
container.addEventListener('pointercancel', onPointerUp);
|
|
281
|
-
|
|
282
|
-
// Prevent default touch behavior on container
|
|
283
|
-
container.style.touchAction = 'none';
|
|
284
|
-
|
|
285
|
-
return {
|
|
286
|
-
destroy: () => {
|
|
287
|
-
container.removeEventListener('pointerdown', onPointerDown);
|
|
288
|
-
container.removeEventListener('pointermove', onPointerMove);
|
|
289
|
-
container.removeEventListener('pointerup', onPointerUp);
|
|
290
|
-
container.removeEventListener('pointercancel', onPointerUp);
|
|
291
|
-
container.style.touchAction = '';
|
|
292
|
-
|
|
293
|
-
if (isDragging) {
|
|
294
|
-
resetDragItem();
|
|
295
|
-
}
|
|
296
|
-
},
|
|
297
|
-
disable: () => {
|
|
298
|
-
enabled = false;
|
|
299
|
-
},
|
|
300
|
-
enable: () => {
|
|
301
|
-
enabled = true;
|
|
302
|
-
},
|
|
303
|
-
get enabled() {
|
|
304
|
-
return enabled;
|
|
305
|
-
},
|
|
306
|
-
};
|
|
307
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Sortable list with animated reordering via pointer events.
|
|
3
|
+
*
|
|
4
|
+
* Makes children of a container sortable by dragging. Items are
|
|
5
|
+
* rearranged in the DOM with optional CSS animation.
|
|
6
|
+
*
|
|
7
|
+
* @module bquery/dnd
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { SortEventData, SortableHandle, SortableOptions } from './types';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Gets the sortable items within a container.
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
const getItems = (container: HTMLElement, selector: string): HTMLElement[] => {
|
|
17
|
+
return Array.from(container.querySelectorAll(selector)) as HTMLElement[];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Finds the closest sortable item to a given Y (or X) position.
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
const getClosestItem = (
|
|
25
|
+
items: HTMLElement[],
|
|
26
|
+
clientPos: number,
|
|
27
|
+
axis: 'x' | 'y',
|
|
28
|
+
dragged: HTMLElement
|
|
29
|
+
): { element: HTMLElement; index: number } | null => {
|
|
30
|
+
let closest: { element: HTMLElement; index: number; distance: number } | null = null;
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < items.length; i++) {
|
|
33
|
+
const item = items[i];
|
|
34
|
+
if (item === dragged) continue;
|
|
35
|
+
|
|
36
|
+
const rect = item.getBoundingClientRect();
|
|
37
|
+
const mid = axis === 'y' ? rect.top + rect.height / 2 : rect.left + rect.width / 2;
|
|
38
|
+
const distance = clientPos - mid;
|
|
39
|
+
|
|
40
|
+
if (
|
|
41
|
+
closest === null ||
|
|
42
|
+
(distance < 0 && distance > closest.distance) ||
|
|
43
|
+
(closest.distance >= 0 && distance < 0 && Math.abs(distance) < Math.abs(closest.distance))
|
|
44
|
+
) {
|
|
45
|
+
// Find the item we're just before
|
|
46
|
+
if (distance < 0) {
|
|
47
|
+
closest = { element: item, index: i, distance };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return closest ? { element: closest.element, index: closest.index } : null;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Makes the children of a container sortable by dragging.
|
|
57
|
+
*
|
|
58
|
+
* Features:
|
|
59
|
+
* - Pointer event based (touch + mouse)
|
|
60
|
+
* - Animated reordering with configurable duration
|
|
61
|
+
* - Axis constraint (vertical or horizontal)
|
|
62
|
+
* - Optional drag handle
|
|
63
|
+
* - Placeholder element during sort
|
|
64
|
+
* - Callbacks: `onSortStart`, `onSortMove`, `onSortEnd`
|
|
65
|
+
*
|
|
66
|
+
* @param container - The container element whose children will be sortable
|
|
67
|
+
* @param options - Configuration options
|
|
68
|
+
* @returns A handle with `destroy()`, `disable()`, and `enable()` methods
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```ts
|
|
72
|
+
* import { sortable } from '@bquery/bquery/dnd';
|
|
73
|
+
*
|
|
74
|
+
* const handle = sortable(document.querySelector('#list'), {
|
|
75
|
+
* items: 'li',
|
|
76
|
+
* axis: 'y',
|
|
77
|
+
* animationDuration: 200,
|
|
78
|
+
* onSortEnd: ({ oldIndex, newIndex }) => {
|
|
79
|
+
* console.log(`Moved from ${oldIndex} to ${newIndex}`);
|
|
80
|
+
* },
|
|
81
|
+
* });
|
|
82
|
+
*
|
|
83
|
+
* // Later:
|
|
84
|
+
* handle.destroy();
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export const sortable = (container: HTMLElement, options: SortableOptions = {}): SortableHandle => {
|
|
88
|
+
const {
|
|
89
|
+
items: itemSelector = ':scope > *',
|
|
90
|
+
axis = 'y',
|
|
91
|
+
handle,
|
|
92
|
+
placeholderClass = 'bq-sort-placeholder',
|
|
93
|
+
sortingClass = 'bq-sorting',
|
|
94
|
+
animationDuration = 200,
|
|
95
|
+
onSortStart,
|
|
96
|
+
onSortMove,
|
|
97
|
+
onSortEnd,
|
|
98
|
+
} = options;
|
|
99
|
+
|
|
100
|
+
let enabled = !options.disabled;
|
|
101
|
+
let isDragging = false;
|
|
102
|
+
let dragItem: HTMLElement | null = null;
|
|
103
|
+
let placeholder: HTMLElement | null = null;
|
|
104
|
+
let startIndex = -1;
|
|
105
|
+
let startPointerY = 0;
|
|
106
|
+
let startPointerX = 0;
|
|
107
|
+
let itemStartTop = 0;
|
|
108
|
+
let itemStartLeft = 0;
|
|
109
|
+
|
|
110
|
+
const createEventData = (item: HTMLElement, oldIdx: number, newIdx: number): SortEventData => ({
|
|
111
|
+
container,
|
|
112
|
+
item,
|
|
113
|
+
oldIndex: oldIdx,
|
|
114
|
+
newIndex: newIdx,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const onPointerDown = (e: PointerEvent): void => {
|
|
118
|
+
if (!enabled) return;
|
|
119
|
+
|
|
120
|
+
const target = e.target as HTMLElement;
|
|
121
|
+
|
|
122
|
+
// Find the item being dragged
|
|
123
|
+
const items = getItems(container, itemSelector);
|
|
124
|
+
let item: HTMLElement | null = null;
|
|
125
|
+
|
|
126
|
+
for (const it of items) {
|
|
127
|
+
if (it.contains(target)) {
|
|
128
|
+
item = it;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!item) return;
|
|
134
|
+
|
|
135
|
+
// Check handle constraint
|
|
136
|
+
if (handle && !target.closest(handle)) return;
|
|
137
|
+
|
|
138
|
+
e.preventDefault();
|
|
139
|
+
|
|
140
|
+
isDragging = true;
|
|
141
|
+
dragItem = item;
|
|
142
|
+
startIndex = items.indexOf(item);
|
|
143
|
+
startPointerY = e.clientY;
|
|
144
|
+
startPointerX = e.clientX;
|
|
145
|
+
|
|
146
|
+
const rect = item.getBoundingClientRect();
|
|
147
|
+
itemStartTop = rect.top;
|
|
148
|
+
itemStartLeft = rect.left;
|
|
149
|
+
|
|
150
|
+
// Create placeholder
|
|
151
|
+
placeholder = document.createElement('div');
|
|
152
|
+
placeholder.classList.add(placeholderClass);
|
|
153
|
+
placeholder.style.width = `${rect.width}px`;
|
|
154
|
+
placeholder.style.height = `${rect.height}px`;
|
|
155
|
+
placeholder.style.boxSizing = 'border-box';
|
|
156
|
+
|
|
157
|
+
// Style the dragged item
|
|
158
|
+
item.classList.add(sortingClass);
|
|
159
|
+
item.style.position = 'fixed';
|
|
160
|
+
item.style.width = `${rect.width}px`;
|
|
161
|
+
item.style.height = `${rect.height}px`;
|
|
162
|
+
item.style.left = `${rect.left}px`;
|
|
163
|
+
item.style.top = `${rect.top}px`;
|
|
164
|
+
item.style.zIndex = '999999';
|
|
165
|
+
item.style.pointerEvents = 'none';
|
|
166
|
+
item.style.margin = '0';
|
|
167
|
+
|
|
168
|
+
// Insert placeholder where the item was
|
|
169
|
+
item.parentNode?.insertBefore(placeholder, item);
|
|
170
|
+
|
|
171
|
+
container.setPointerCapture(e.pointerId);
|
|
172
|
+
|
|
173
|
+
onSortStart?.(createEventData(item, startIndex, startIndex));
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const onPointerMove = (e: PointerEvent): void => {
|
|
177
|
+
if (!isDragging || !dragItem || !placeholder) return;
|
|
178
|
+
|
|
179
|
+
e.preventDefault();
|
|
180
|
+
|
|
181
|
+
const deltaX = e.clientX - startPointerX;
|
|
182
|
+
const deltaY = e.clientY - startPointerY;
|
|
183
|
+
|
|
184
|
+
// Move the dragged item
|
|
185
|
+
if (axis === 'y') {
|
|
186
|
+
dragItem.style.top = `${itemStartTop + deltaY}px`;
|
|
187
|
+
} else {
|
|
188
|
+
dragItem.style.left = `${itemStartLeft + deltaX}px`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Find the closest item to determine insertion point
|
|
192
|
+
const items = getItems(container, itemSelector);
|
|
193
|
+
const clientPos = axis === 'y' ? e.clientY : e.clientX;
|
|
194
|
+
const closest = getClosestItem(items, clientPos, axis, dragItem);
|
|
195
|
+
|
|
196
|
+
if (closest) {
|
|
197
|
+
// Move placeholder before the closest element
|
|
198
|
+
container.insertBefore(placeholder, closest.element);
|
|
199
|
+
} else {
|
|
200
|
+
// Append to end
|
|
201
|
+
container.appendChild(placeholder);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const currentIndex = Array.from(container.children).indexOf(placeholder);
|
|
205
|
+
onSortMove?.(createEventData(dragItem, startIndex, currentIndex));
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const onPointerUp = (e: PointerEvent): void => {
|
|
209
|
+
if (!isDragging || !dragItem || !placeholder) return;
|
|
210
|
+
|
|
211
|
+
isDragging = false;
|
|
212
|
+
const draggedItem = dragItem;
|
|
213
|
+
|
|
214
|
+
// Get final index
|
|
215
|
+
const newIndex = Array.from(container.children).indexOf(placeholder);
|
|
216
|
+
|
|
217
|
+
// Animate the item back to the placeholder position
|
|
218
|
+
const placeholderRect = placeholder.getBoundingClientRect();
|
|
219
|
+
const itemRect = draggedItem.getBoundingClientRect();
|
|
220
|
+
|
|
221
|
+
if (animationDuration > 0) {
|
|
222
|
+
const deltaX = placeholderRect.left - itemRect.left;
|
|
223
|
+
const deltaY = placeholderRect.top - itemRect.top;
|
|
224
|
+
|
|
225
|
+
draggedItem.style.transition = `transform ${animationDuration}ms ease`;
|
|
226
|
+
draggedItem.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
|
|
227
|
+
|
|
228
|
+
let finalized = false;
|
|
229
|
+
let timeoutId: number | null = null;
|
|
230
|
+
const finalize = (): void => {
|
|
231
|
+
if (finalized) return;
|
|
232
|
+
finalized = true;
|
|
233
|
+
if (timeoutId !== null) {
|
|
234
|
+
window.clearTimeout(timeoutId);
|
|
235
|
+
timeoutId = null;
|
|
236
|
+
}
|
|
237
|
+
resetDragItem();
|
|
238
|
+
onSortEnd?.(createEventData(draggedItem, startIndex, newIndex));
|
|
239
|
+
};
|
|
240
|
+
timeoutId = window.setTimeout(() => {
|
|
241
|
+
finalize();
|
|
242
|
+
}, animationDuration + 50);
|
|
243
|
+
|
|
244
|
+
draggedItem.addEventListener('transitionend', finalize, { once: true });
|
|
245
|
+
} else {
|
|
246
|
+
resetDragItem();
|
|
247
|
+
onSortEnd?.(createEventData(draggedItem, startIndex, newIndex));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
container.releasePointerCapture(e.pointerId);
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const resetDragItem = (): void => {
|
|
254
|
+
if (!dragItem || !placeholder) return;
|
|
255
|
+
|
|
256
|
+
// Insert the real item where the placeholder is
|
|
257
|
+
placeholder.parentNode?.insertBefore(dragItem, placeholder);
|
|
258
|
+
placeholder.remove();
|
|
259
|
+
placeholder = null;
|
|
260
|
+
|
|
261
|
+
// Reset styles
|
|
262
|
+
dragItem.classList.remove(sortingClass);
|
|
263
|
+
dragItem.style.position = '';
|
|
264
|
+
dragItem.style.width = '';
|
|
265
|
+
dragItem.style.height = '';
|
|
266
|
+
dragItem.style.left = '';
|
|
267
|
+
dragItem.style.top = '';
|
|
268
|
+
dragItem.style.zIndex = '';
|
|
269
|
+
dragItem.style.pointerEvents = '';
|
|
270
|
+
dragItem.style.margin = '';
|
|
271
|
+
dragItem.style.transition = '';
|
|
272
|
+
dragItem.style.transform = '';
|
|
273
|
+
|
|
274
|
+
dragItem = null;
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
container.addEventListener('pointerdown', onPointerDown);
|
|
278
|
+
container.addEventListener('pointermove', onPointerMove);
|
|
279
|
+
container.addEventListener('pointerup', onPointerUp);
|
|
280
|
+
container.addEventListener('pointercancel', onPointerUp);
|
|
281
|
+
|
|
282
|
+
// Prevent default touch behavior on container
|
|
283
|
+
container.style.touchAction = 'none';
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
destroy: () => {
|
|
287
|
+
container.removeEventListener('pointerdown', onPointerDown);
|
|
288
|
+
container.removeEventListener('pointermove', onPointerMove);
|
|
289
|
+
container.removeEventListener('pointerup', onPointerUp);
|
|
290
|
+
container.removeEventListener('pointercancel', onPointerUp);
|
|
291
|
+
container.style.touchAction = '';
|
|
292
|
+
|
|
293
|
+
if (isDragging) {
|
|
294
|
+
resetDragItem();
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
disable: () => {
|
|
298
|
+
enabled = false;
|
|
299
|
+
},
|
|
300
|
+
enable: () => {
|
|
301
|
+
enabled = true;
|
|
302
|
+
},
|
|
303
|
+
get enabled() {
|
|
304
|
+
return enabled;
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
};
|