@bquery/bquery 1.7.0 → 1.8.2
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 +178 -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/draggable.ts
CHANGED
|
@@ -1,296 +1,296 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Make an element draggable using pointer events.
|
|
3
|
-
*
|
|
4
|
-
* Uses Pointer Events (not HTML5 Drag & Drop) for reliable
|
|
5
|
-
* cross-platform behavior including touch support.
|
|
6
|
-
*
|
|
7
|
-
* @module bquery/dnd
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type {
|
|
11
|
-
BoundsRect,
|
|
12
|
-
DragBounds,
|
|
13
|
-
DragEventData,
|
|
14
|
-
DragPosition,
|
|
15
|
-
DraggableHandle,
|
|
16
|
-
DraggableOptions,
|
|
17
|
-
} from './types';
|
|
18
|
-
|
|
19
|
-
/** Global registry of active draggable elements for drop zone detection. */
|
|
20
|
-
const activeDrags = new Map<HTMLElement, { element: HTMLElement; position: DragPosition }>();
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Returns the currently active drag state, if any.
|
|
24
|
-
* Used internally by `droppable()` to detect drag interactions.
|
|
25
|
-
* @internal
|
|
26
|
-
*/
|
|
27
|
-
export const getActiveDrag = (): { element: HTMLElement; position: DragPosition } | undefined => {
|
|
28
|
-
const entries = Array.from(activeDrags.values());
|
|
29
|
-
return entries[entries.length - 1];
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Resolves a `DragBounds` value to an absolute `BoundsRect`.
|
|
34
|
-
* @internal
|
|
35
|
-
*/
|
|
36
|
-
const resolveBounds = (el: HTMLElement, bounds: DragBounds): BoundsRect | null => {
|
|
37
|
-
if (typeof bounds === 'object') {
|
|
38
|
-
return bounds;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
let target: HTMLElement | null = null;
|
|
42
|
-
|
|
43
|
-
if (bounds === 'parent') {
|
|
44
|
-
target = el.parentElement;
|
|
45
|
-
} else {
|
|
46
|
-
target = document.querySelector(bounds) as HTMLElement | null;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (!target) return null;
|
|
50
|
-
|
|
51
|
-
const rect = target.getBoundingClientRect();
|
|
52
|
-
const elRect = el.getBoundingClientRect();
|
|
53
|
-
const rawLeft = parseFloat(el.style.left || '0');
|
|
54
|
-
const rawTop = parseFloat(el.style.top || '0');
|
|
55
|
-
const leftOffset = Number.isNaN(rawLeft) ? 0 : rawLeft;
|
|
56
|
-
const topOffset = Number.isNaN(rawTop) ? 0 : rawTop;
|
|
57
|
-
|
|
58
|
-
return {
|
|
59
|
-
left: rect.left - elRect.left + leftOffset,
|
|
60
|
-
top: rect.top - elRect.top + topOffset,
|
|
61
|
-
right: rect.right - elRect.right + leftOffset + (rect.width - elRect.width),
|
|
62
|
-
bottom: rect.bottom - elRect.bottom + topOffset + (rect.height - elRect.height),
|
|
63
|
-
};
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Clamp a position within bounds.
|
|
68
|
-
* @internal
|
|
69
|
-
*/
|
|
70
|
-
const clampPosition = (pos: DragPosition, bounds: BoundsRect | null): DragPosition => {
|
|
71
|
-
if (!bounds) return pos;
|
|
72
|
-
return {
|
|
73
|
-
x: Math.max(bounds.left, Math.min(bounds.right, pos.x)),
|
|
74
|
-
y: Math.max(bounds.top, Math.min(bounds.bottom, pos.y)),
|
|
75
|
-
};
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Makes an element draggable using pointer events.
|
|
80
|
-
*
|
|
81
|
-
* Features:
|
|
82
|
-
* - Touch and mouse support via Pointer Events
|
|
83
|
-
* - Axis locking (`x`, `y`, or `both`)
|
|
84
|
-
* - Bounds constraint (parent, selector, or explicit rect)
|
|
85
|
-
* - Optional drag handle
|
|
86
|
-
* - Ghost/clone preview during drag
|
|
87
|
-
* - Callbacks: `onDragStart`, `onDrag`, `onDragEnd`
|
|
88
|
-
*
|
|
89
|
-
* @param el - The element to make draggable
|
|
90
|
-
* @param options - Configuration options
|
|
91
|
-
* @returns A handle with `destroy()`, `disable()`, and `enable()` methods
|
|
92
|
-
*
|
|
93
|
-
* @example
|
|
94
|
-
* ```ts
|
|
95
|
-
* import { draggable } from '@bquery/bquery/dnd';
|
|
96
|
-
*
|
|
97
|
-
* const handle = draggable(document.querySelector('#box'), {
|
|
98
|
-
* axis: 'both',
|
|
99
|
-
* bounds: 'parent',
|
|
100
|
-
* onDragEnd: ({ position }) => {
|
|
101
|
-
* console.log('Dropped at', position.x, position.y);
|
|
102
|
-
* },
|
|
103
|
-
* });
|
|
104
|
-
*
|
|
105
|
-
* // Later:
|
|
106
|
-
* handle.destroy();
|
|
107
|
-
* ```
|
|
108
|
-
*/
|
|
109
|
-
export const draggable = (el: HTMLElement, options: DraggableOptions = {}): DraggableHandle => {
|
|
110
|
-
const {
|
|
111
|
-
axis = 'both',
|
|
112
|
-
bounds,
|
|
113
|
-
handle,
|
|
114
|
-
ghost = false,
|
|
115
|
-
ghostClass = 'bq-drag-ghost',
|
|
116
|
-
draggingClass = 'bq-dragging',
|
|
117
|
-
onDragStart,
|
|
118
|
-
onDrag,
|
|
119
|
-
onDragEnd,
|
|
120
|
-
} = options;
|
|
121
|
-
|
|
122
|
-
let enabled = !options.disabled;
|
|
123
|
-
let isDragging = false;
|
|
124
|
-
let startPointer: DragPosition = { x: 0, y: 0 };
|
|
125
|
-
let currentPosition: DragPosition = { x: 0, y: 0 };
|
|
126
|
-
let previousPosition: DragPosition = { x: 0, y: 0 };
|
|
127
|
-
let ghostEl: HTMLElement | null = null;
|
|
128
|
-
let ghostStartPosition: DragPosition | null = null;
|
|
129
|
-
const previousTouchAction = el.style.touchAction;
|
|
130
|
-
const previousUserSelect = el.style.userSelect;
|
|
131
|
-
|
|
132
|
-
const createEventData = (event: PointerEvent): DragEventData => ({
|
|
133
|
-
element: el,
|
|
134
|
-
position: { ...currentPosition },
|
|
135
|
-
delta: {
|
|
136
|
-
x: currentPosition.x - previousPosition.x,
|
|
137
|
-
y: currentPosition.y - previousPosition.y,
|
|
138
|
-
},
|
|
139
|
-
event,
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
const createGhost = (): HTMLElement => {
|
|
143
|
-
const clone = el.cloneNode(true) as HTMLElement;
|
|
144
|
-
const rect = el.getBoundingClientRect();
|
|
145
|
-
clone.classList.add(ghostClass);
|
|
146
|
-
clone.style.position = 'fixed';
|
|
147
|
-
clone.style.left = `${rect.left}px`;
|
|
148
|
-
clone.style.top = `${rect.top}px`;
|
|
149
|
-
clone.style.width = `${rect.width}px`;
|
|
150
|
-
clone.style.height = `${rect.height}px`;
|
|
151
|
-
clone.style.pointerEvents = 'none';
|
|
152
|
-
clone.style.zIndex = '999999';
|
|
153
|
-
clone.style.opacity = '0.7';
|
|
154
|
-
clone.style.margin = '0';
|
|
155
|
-
document.body.appendChild(clone);
|
|
156
|
-
return clone;
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
const removeGhost = (): void => {
|
|
160
|
-
if (ghostEl) {
|
|
161
|
-
ghostEl.remove();
|
|
162
|
-
ghostEl = null;
|
|
163
|
-
}
|
|
164
|
-
ghostStartPosition = null;
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
const onPointerDown = (e: PointerEvent): void => {
|
|
168
|
-
if (!enabled) return;
|
|
169
|
-
|
|
170
|
-
// Check handle constraint
|
|
171
|
-
if (handle) {
|
|
172
|
-
const target = e.target as Element;
|
|
173
|
-
if (!target.closest(handle)) return;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
e.preventDefault();
|
|
177
|
-
isDragging = true;
|
|
178
|
-
startPointer = { x: e.clientX, y: e.clientY };
|
|
179
|
-
previousPosition = { ...currentPosition };
|
|
180
|
-
|
|
181
|
-
el.classList.add(draggingClass);
|
|
182
|
-
el.setPointerCapture(e.pointerId);
|
|
183
|
-
|
|
184
|
-
if (ghost) {
|
|
185
|
-
const rect = el.getBoundingClientRect();
|
|
186
|
-
ghostStartPosition = { x: rect.left, y: rect.top };
|
|
187
|
-
ghostEl = createGhost();
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Register in global active drags
|
|
191
|
-
activeDrags.set(el, { element: el, position: currentPosition });
|
|
192
|
-
|
|
193
|
-
onDragStart?.(createEventData(e));
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
const onPointerMove = (e: PointerEvent): void => {
|
|
197
|
-
if (!isDragging) return;
|
|
198
|
-
|
|
199
|
-
e.preventDefault();
|
|
200
|
-
previousPosition = { ...currentPosition };
|
|
201
|
-
|
|
202
|
-
let newX = currentPosition.x + (e.clientX - startPointer.x);
|
|
203
|
-
let newY = currentPosition.y + (e.clientY - startPointer.y);
|
|
204
|
-
|
|
205
|
-
// Reset start pointer to current for delta calculation
|
|
206
|
-
startPointer = { x: e.clientX, y: e.clientY };
|
|
207
|
-
|
|
208
|
-
// Apply axis constraint
|
|
209
|
-
if (axis === 'x') newY = currentPosition.y;
|
|
210
|
-
if (axis === 'y') newX = currentPosition.x;
|
|
211
|
-
|
|
212
|
-
let newPos: DragPosition = { x: newX, y: newY };
|
|
213
|
-
|
|
214
|
-
// Apply bounds constraint
|
|
215
|
-
if (bounds) {
|
|
216
|
-
const resolvedBounds = resolveBounds(el, bounds);
|
|
217
|
-
newPos = clampPosition(newPos, resolvedBounds);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
currentPosition = newPos;
|
|
221
|
-
|
|
222
|
-
// Update active drag position
|
|
223
|
-
activeDrags.set(el, { element: el, position: currentPosition });
|
|
224
|
-
|
|
225
|
-
// Apply the position
|
|
226
|
-
if (ghost && ghostEl) {
|
|
227
|
-
const start = ghostStartPosition ?? {
|
|
228
|
-
x: el.getBoundingClientRect().left,
|
|
229
|
-
y: el.getBoundingClientRect().top,
|
|
230
|
-
};
|
|
231
|
-
ghostEl.style.left = `${start.x + currentPosition.x}px`;
|
|
232
|
-
ghostEl.style.top = `${start.y + currentPosition.y}px`;
|
|
233
|
-
} else {
|
|
234
|
-
el.style.transform = `translate(${currentPosition.x}px, ${currentPosition.y}px)`;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
onDrag?.(createEventData(e));
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
const onPointerUp = (e: PointerEvent): void => {
|
|
241
|
-
if (!isDragging) return;
|
|
242
|
-
|
|
243
|
-
isDragging = false;
|
|
244
|
-
el.classList.remove(draggingClass);
|
|
245
|
-
try {
|
|
246
|
-
if (
|
|
247
|
-
typeof el.releasePointerCapture === 'function' &&
|
|
248
|
-
(typeof el.hasPointerCapture !== 'function' || el.hasPointerCapture(e.pointerId))
|
|
249
|
-
) {
|
|
250
|
-
el.releasePointerCapture(e.pointerId);
|
|
251
|
-
}
|
|
252
|
-
} catch {
|
|
253
|
-
// Pointer capture may already be released in some interrupted drag flows.
|
|
254
|
-
} finally {
|
|
255
|
-
removeGhost();
|
|
256
|
-
|
|
257
|
-
// Remove from active drags
|
|
258
|
-
activeDrags.delete(el);
|
|
259
|
-
|
|
260
|
-
onDragEnd?.(createEventData(e));
|
|
261
|
-
}
|
|
262
|
-
};
|
|
263
|
-
|
|
264
|
-
// Attach listeners
|
|
265
|
-
el.addEventListener('pointerdown', onPointerDown);
|
|
266
|
-
el.addEventListener('pointermove', onPointerMove);
|
|
267
|
-
el.addEventListener('pointerup', onPointerUp);
|
|
268
|
-
el.addEventListener('pointercancel', onPointerUp);
|
|
269
|
-
|
|
270
|
-
// Prevent default drag behavior
|
|
271
|
-
el.style.touchAction = 'none';
|
|
272
|
-
el.style.userSelect = 'none';
|
|
273
|
-
|
|
274
|
-
return {
|
|
275
|
-
destroy: () => {
|
|
276
|
-
el.removeEventListener('pointerdown', onPointerDown);
|
|
277
|
-
el.removeEventListener('pointermove', onPointerMove);
|
|
278
|
-
el.removeEventListener('pointerup', onPointerUp);
|
|
279
|
-
el.removeEventListener('pointercancel', onPointerUp);
|
|
280
|
-
removeGhost();
|
|
281
|
-
activeDrags.delete(el);
|
|
282
|
-
el.style.touchAction = previousTouchAction;
|
|
283
|
-
el.style.userSelect = previousUserSelect;
|
|
284
|
-
el.classList.remove(draggingClass);
|
|
285
|
-
},
|
|
286
|
-
disable: () => {
|
|
287
|
-
enabled = false;
|
|
288
|
-
},
|
|
289
|
-
enable: () => {
|
|
290
|
-
enabled = true;
|
|
291
|
-
},
|
|
292
|
-
get enabled() {
|
|
293
|
-
return enabled;
|
|
294
|
-
},
|
|
295
|
-
};
|
|
296
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Make an element draggable using pointer events.
|
|
3
|
+
*
|
|
4
|
+
* Uses Pointer Events (not HTML5 Drag & Drop) for reliable
|
|
5
|
+
* cross-platform behavior including touch support.
|
|
6
|
+
*
|
|
7
|
+
* @module bquery/dnd
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
BoundsRect,
|
|
12
|
+
DragBounds,
|
|
13
|
+
DragEventData,
|
|
14
|
+
DragPosition,
|
|
15
|
+
DraggableHandle,
|
|
16
|
+
DraggableOptions,
|
|
17
|
+
} from './types';
|
|
18
|
+
|
|
19
|
+
/** Global registry of active draggable elements for drop zone detection. */
|
|
20
|
+
const activeDrags = new Map<HTMLElement, { element: HTMLElement; position: DragPosition }>();
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Returns the currently active drag state, if any.
|
|
24
|
+
* Used internally by `droppable()` to detect drag interactions.
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
export const getActiveDrag = (): { element: HTMLElement; position: DragPosition } | undefined => {
|
|
28
|
+
const entries = Array.from(activeDrags.values());
|
|
29
|
+
return entries[entries.length - 1];
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolves a `DragBounds` value to an absolute `BoundsRect`.
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
36
|
+
const resolveBounds = (el: HTMLElement, bounds: DragBounds): BoundsRect | null => {
|
|
37
|
+
if (typeof bounds === 'object') {
|
|
38
|
+
return bounds;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let target: HTMLElement | null = null;
|
|
42
|
+
|
|
43
|
+
if (bounds === 'parent') {
|
|
44
|
+
target = el.parentElement;
|
|
45
|
+
} else {
|
|
46
|
+
target = document.querySelector(bounds) as HTMLElement | null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!target) return null;
|
|
50
|
+
|
|
51
|
+
const rect = target.getBoundingClientRect();
|
|
52
|
+
const elRect = el.getBoundingClientRect();
|
|
53
|
+
const rawLeft = parseFloat(el.style.left || '0');
|
|
54
|
+
const rawTop = parseFloat(el.style.top || '0');
|
|
55
|
+
const leftOffset = Number.isNaN(rawLeft) ? 0 : rawLeft;
|
|
56
|
+
const topOffset = Number.isNaN(rawTop) ? 0 : rawTop;
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
left: rect.left - elRect.left + leftOffset,
|
|
60
|
+
top: rect.top - elRect.top + topOffset,
|
|
61
|
+
right: rect.right - elRect.right + leftOffset + (rect.width - elRect.width),
|
|
62
|
+
bottom: rect.bottom - elRect.bottom + topOffset + (rect.height - elRect.height),
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Clamp a position within bounds.
|
|
68
|
+
* @internal
|
|
69
|
+
*/
|
|
70
|
+
const clampPosition = (pos: DragPosition, bounds: BoundsRect | null): DragPosition => {
|
|
71
|
+
if (!bounds) return pos;
|
|
72
|
+
return {
|
|
73
|
+
x: Math.max(bounds.left, Math.min(bounds.right, pos.x)),
|
|
74
|
+
y: Math.max(bounds.top, Math.min(bounds.bottom, pos.y)),
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Makes an element draggable using pointer events.
|
|
80
|
+
*
|
|
81
|
+
* Features:
|
|
82
|
+
* - Touch and mouse support via Pointer Events
|
|
83
|
+
* - Axis locking (`x`, `y`, or `both`)
|
|
84
|
+
* - Bounds constraint (parent, selector, or explicit rect)
|
|
85
|
+
* - Optional drag handle
|
|
86
|
+
* - Ghost/clone preview during drag
|
|
87
|
+
* - Callbacks: `onDragStart`, `onDrag`, `onDragEnd`
|
|
88
|
+
*
|
|
89
|
+
* @param el - The element to make draggable
|
|
90
|
+
* @param options - Configuration options
|
|
91
|
+
* @returns A handle with `destroy()`, `disable()`, and `enable()` methods
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```ts
|
|
95
|
+
* import { draggable } from '@bquery/bquery/dnd';
|
|
96
|
+
*
|
|
97
|
+
* const handle = draggable(document.querySelector('#box'), {
|
|
98
|
+
* axis: 'both',
|
|
99
|
+
* bounds: 'parent',
|
|
100
|
+
* onDragEnd: ({ position }) => {
|
|
101
|
+
* console.log('Dropped at', position.x, position.y);
|
|
102
|
+
* },
|
|
103
|
+
* });
|
|
104
|
+
*
|
|
105
|
+
* // Later:
|
|
106
|
+
* handle.destroy();
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export const draggable = (el: HTMLElement, options: DraggableOptions = {}): DraggableHandle => {
|
|
110
|
+
const {
|
|
111
|
+
axis = 'both',
|
|
112
|
+
bounds,
|
|
113
|
+
handle,
|
|
114
|
+
ghost = false,
|
|
115
|
+
ghostClass = 'bq-drag-ghost',
|
|
116
|
+
draggingClass = 'bq-dragging',
|
|
117
|
+
onDragStart,
|
|
118
|
+
onDrag,
|
|
119
|
+
onDragEnd,
|
|
120
|
+
} = options;
|
|
121
|
+
|
|
122
|
+
let enabled = !options.disabled;
|
|
123
|
+
let isDragging = false;
|
|
124
|
+
let startPointer: DragPosition = { x: 0, y: 0 };
|
|
125
|
+
let currentPosition: DragPosition = { x: 0, y: 0 };
|
|
126
|
+
let previousPosition: DragPosition = { x: 0, y: 0 };
|
|
127
|
+
let ghostEl: HTMLElement | null = null;
|
|
128
|
+
let ghostStartPosition: DragPosition | null = null;
|
|
129
|
+
const previousTouchAction = el.style.touchAction;
|
|
130
|
+
const previousUserSelect = el.style.userSelect;
|
|
131
|
+
|
|
132
|
+
const createEventData = (event: PointerEvent): DragEventData => ({
|
|
133
|
+
element: el,
|
|
134
|
+
position: { ...currentPosition },
|
|
135
|
+
delta: {
|
|
136
|
+
x: currentPosition.x - previousPosition.x,
|
|
137
|
+
y: currentPosition.y - previousPosition.y,
|
|
138
|
+
},
|
|
139
|
+
event,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const createGhost = (): HTMLElement => {
|
|
143
|
+
const clone = el.cloneNode(true) as HTMLElement;
|
|
144
|
+
const rect = el.getBoundingClientRect();
|
|
145
|
+
clone.classList.add(ghostClass);
|
|
146
|
+
clone.style.position = 'fixed';
|
|
147
|
+
clone.style.left = `${rect.left}px`;
|
|
148
|
+
clone.style.top = `${rect.top}px`;
|
|
149
|
+
clone.style.width = `${rect.width}px`;
|
|
150
|
+
clone.style.height = `${rect.height}px`;
|
|
151
|
+
clone.style.pointerEvents = 'none';
|
|
152
|
+
clone.style.zIndex = '999999';
|
|
153
|
+
clone.style.opacity = '0.7';
|
|
154
|
+
clone.style.margin = '0';
|
|
155
|
+
document.body.appendChild(clone);
|
|
156
|
+
return clone;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const removeGhost = (): void => {
|
|
160
|
+
if (ghostEl) {
|
|
161
|
+
ghostEl.remove();
|
|
162
|
+
ghostEl = null;
|
|
163
|
+
}
|
|
164
|
+
ghostStartPosition = null;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const onPointerDown = (e: PointerEvent): void => {
|
|
168
|
+
if (!enabled) return;
|
|
169
|
+
|
|
170
|
+
// Check handle constraint
|
|
171
|
+
if (handle) {
|
|
172
|
+
const target = e.target as Element;
|
|
173
|
+
if (!target.closest(handle)) return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
e.preventDefault();
|
|
177
|
+
isDragging = true;
|
|
178
|
+
startPointer = { x: e.clientX, y: e.clientY };
|
|
179
|
+
previousPosition = { ...currentPosition };
|
|
180
|
+
|
|
181
|
+
el.classList.add(draggingClass);
|
|
182
|
+
el.setPointerCapture(e.pointerId);
|
|
183
|
+
|
|
184
|
+
if (ghost) {
|
|
185
|
+
const rect = el.getBoundingClientRect();
|
|
186
|
+
ghostStartPosition = { x: rect.left, y: rect.top };
|
|
187
|
+
ghostEl = createGhost();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Register in global active drags
|
|
191
|
+
activeDrags.set(el, { element: el, position: currentPosition });
|
|
192
|
+
|
|
193
|
+
onDragStart?.(createEventData(e));
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const onPointerMove = (e: PointerEvent): void => {
|
|
197
|
+
if (!isDragging) return;
|
|
198
|
+
|
|
199
|
+
e.preventDefault();
|
|
200
|
+
previousPosition = { ...currentPosition };
|
|
201
|
+
|
|
202
|
+
let newX = currentPosition.x + (e.clientX - startPointer.x);
|
|
203
|
+
let newY = currentPosition.y + (e.clientY - startPointer.y);
|
|
204
|
+
|
|
205
|
+
// Reset start pointer to current for delta calculation
|
|
206
|
+
startPointer = { x: e.clientX, y: e.clientY };
|
|
207
|
+
|
|
208
|
+
// Apply axis constraint
|
|
209
|
+
if (axis === 'x') newY = currentPosition.y;
|
|
210
|
+
if (axis === 'y') newX = currentPosition.x;
|
|
211
|
+
|
|
212
|
+
let newPos: DragPosition = { x: newX, y: newY };
|
|
213
|
+
|
|
214
|
+
// Apply bounds constraint
|
|
215
|
+
if (bounds) {
|
|
216
|
+
const resolvedBounds = resolveBounds(el, bounds);
|
|
217
|
+
newPos = clampPosition(newPos, resolvedBounds);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
currentPosition = newPos;
|
|
221
|
+
|
|
222
|
+
// Update active drag position
|
|
223
|
+
activeDrags.set(el, { element: el, position: currentPosition });
|
|
224
|
+
|
|
225
|
+
// Apply the position
|
|
226
|
+
if (ghost && ghostEl) {
|
|
227
|
+
const start = ghostStartPosition ?? {
|
|
228
|
+
x: el.getBoundingClientRect().left,
|
|
229
|
+
y: el.getBoundingClientRect().top,
|
|
230
|
+
};
|
|
231
|
+
ghostEl.style.left = `${start.x + currentPosition.x}px`;
|
|
232
|
+
ghostEl.style.top = `${start.y + currentPosition.y}px`;
|
|
233
|
+
} else {
|
|
234
|
+
el.style.transform = `translate(${currentPosition.x}px, ${currentPosition.y}px)`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
onDrag?.(createEventData(e));
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const onPointerUp = (e: PointerEvent): void => {
|
|
241
|
+
if (!isDragging) return;
|
|
242
|
+
|
|
243
|
+
isDragging = false;
|
|
244
|
+
el.classList.remove(draggingClass);
|
|
245
|
+
try {
|
|
246
|
+
if (
|
|
247
|
+
typeof el.releasePointerCapture === 'function' &&
|
|
248
|
+
(typeof el.hasPointerCapture !== 'function' || el.hasPointerCapture(e.pointerId))
|
|
249
|
+
) {
|
|
250
|
+
el.releasePointerCapture(e.pointerId);
|
|
251
|
+
}
|
|
252
|
+
} catch {
|
|
253
|
+
// Pointer capture may already be released in some interrupted drag flows.
|
|
254
|
+
} finally {
|
|
255
|
+
removeGhost();
|
|
256
|
+
|
|
257
|
+
// Remove from active drags
|
|
258
|
+
activeDrags.delete(el);
|
|
259
|
+
|
|
260
|
+
onDragEnd?.(createEventData(e));
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// Attach listeners
|
|
265
|
+
el.addEventListener('pointerdown', onPointerDown);
|
|
266
|
+
el.addEventListener('pointermove', onPointerMove);
|
|
267
|
+
el.addEventListener('pointerup', onPointerUp);
|
|
268
|
+
el.addEventListener('pointercancel', onPointerUp);
|
|
269
|
+
|
|
270
|
+
// Prevent default drag behavior
|
|
271
|
+
el.style.touchAction = 'none';
|
|
272
|
+
el.style.userSelect = 'none';
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
destroy: () => {
|
|
276
|
+
el.removeEventListener('pointerdown', onPointerDown);
|
|
277
|
+
el.removeEventListener('pointermove', onPointerMove);
|
|
278
|
+
el.removeEventListener('pointerup', onPointerUp);
|
|
279
|
+
el.removeEventListener('pointercancel', onPointerUp);
|
|
280
|
+
removeGhost();
|
|
281
|
+
activeDrags.delete(el);
|
|
282
|
+
el.style.touchAction = previousTouchAction;
|
|
283
|
+
el.style.userSelect = previousUserSelect;
|
|
284
|
+
el.classList.remove(draggingClass);
|
|
285
|
+
},
|
|
286
|
+
disable: () => {
|
|
287
|
+
enabled = false;
|
|
288
|
+
},
|
|
289
|
+
enable: () => {
|
|
290
|
+
enabled = true;
|
|
291
|
+
},
|
|
292
|
+
get enabled() {
|
|
293
|
+
return enabled;
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
};
|