@@ -137,3 +137,99 @@ export function bindEventListener<K extends keyof HTMLElementEventMap>(
137137 } ,
138138 } ;
139139}
140+
141+ export interface DragEvent {
142+ // Movement delta since the previous event.
143+ delta : { readonly x : number ; readonly y : number } ;
144+ // Absolute pointer position in client coordinates.
145+ client : { readonly x : number ; readonly y : number } ;
146+ // Pointer position at the very start of the drag in client coordinates.
147+ startClient : { readonly x : number ; readonly y : number } ;
148+ }
149+
150+ // Waits for a drag gesture to begin (pointer moved beyond `deadzone` px).
151+ // Returns an async iterable of DragEvents, or undefined if the pointer was
152+ // released before the deadzone was crossed (i.e. it was a click). The first yielded
153+ // event includes accumulated movement from the deadzone phase so callers never
154+ // lose movement that occurred before the drag was confirmed.
155+ export async function captureDrag ( attrs : {
156+ el : HTMLElement ;
157+ e : PointerEvent ;
158+ deadzone ?: number ;
159+ } ) : Promise < AsyncIterable < DragEvent > | undefined > {
160+ const { el, e, deadzone = 0 } = attrs ;
161+ const pointerId = e . pointerId ;
162+
163+ el . setPointerCapture ( pointerId ) ;
164+
165+ let resolveNext : ( ( e : PointerEvent | undefined ) => void ) | undefined ;
166+
167+ const onMove = ( e : PointerEvent ) => {
168+ if ( e . pointerId !== pointerId ) return ;
169+ resolveNext ?.( e ) ;
170+ resolveNext = undefined ;
171+ } ;
172+ const onDone = ( e : PointerEvent ) => {
173+ if ( e . pointerId !== pointerId ) return ;
174+ resolveNext ?.( undefined ) ;
175+ resolveNext = undefined ;
176+ } ;
177+
178+ el . addEventListener ( 'pointermove' , onMove ) ;
179+ el . addEventListener ( 'pointerup' , onDone ) ;
180+ el . addEventListener ( 'pointercancel' , onDone ) ;
181+
182+ const cleanup = ( ) => {
183+ el . removeEventListener ( 'pointermove' , onMove ) ;
184+ el . removeEventListener ( 'pointerup' , onDone ) ;
185+ el . removeEventListener ( 'pointercancel' , onDone ) ;
186+ } ;
187+
188+ const next = ( ) =>
189+ new Promise < PointerEvent | undefined > ( ( r ) => {
190+ resolveNext = r ;
191+ } ) ;
192+
193+ // Phase 1: wait for deadzone to be crossed, or pointerup (→ click).
194+ // Accumulate movement so the first yield includes the full delta.
195+ let accum = new Vector2D ( { x : 0 , y : 0 } ) ;
196+ const start = new Vector2D ( { x : e . clientX , y : e . clientY } ) ;
197+ let firstEvent : PointerEvent = e ;
198+ while ( deadzone > 0 ) {
199+ const ev = await next ( ) ;
200+ if ( ev === undefined ) {
201+ cleanup ( ) ;
202+ return undefined ;
203+ }
204+ firstEvent = ev ;
205+ accum = accum . add ( { x : ev . movementX , y : ev . movementY } ) ;
206+ if ( start . sub ( { x : ev . clientX , y : ev . clientY } ) . magnitude >= deadzone ) break ;
207+ }
208+
209+ const startClient = { x : e . clientX , y : e . clientY } ;
210+
211+ // Phase 2: drag confirmed — stream events to the caller, leading with the
212+ // accumulated deadzone movement as the first yield.
213+ return ( async function * ( ) {
214+ try {
215+ if ( deadzone > 0 ) {
216+ yield {
217+ delta : accum ,
218+ client : { x : firstEvent . clientX , y : firstEvent . clientY } ,
219+ startClient,
220+ } ;
221+ }
222+ while ( true ) {
223+ const ev = await next ( ) ;
224+ if ( ev === undefined ) return ;
225+ yield {
226+ delta : { x : ev . movementX , y : ev . movementY } ,
227+ client : { x : ev . clientX , y : ev . clientY } ,
228+ startClient,
229+ } ;
230+ }
231+ } finally {
232+ cleanup ( ) ;
233+ }
234+ } ) ( ) ;
235+ }
0 commit comments