@@ -15,61 +15,93 @@ export class DragScrollDirective implements AfterViewInit, OnDestroy {
1515 private startTime = signal ( 0 ) ;
1616 private moveDistance = signal ( 0 ) ;
1717
18- // Momentum scrolling variables
1918 private velocity = signal ( 0 ) ;
2019 private timestamp = signal ( 0 ) ;
2120 private frame = signal ( 0 ) ;
2221 private amplitude = signal ( 0 ) ;
2322 private target = signal ( 0 ) ;
24- private timeConstant = 325 ; // ms - adjust for scroll momentum feel
23+ private timeConstant = 500 ;
2524 private animationActive = false ;
2625
27- // Add this new property to track if we should suppress clicks
26+ private velocityFactor = 0.5 ;
27+ private momentumMultiplier = 8 ;
28+
2829 private suppressClick = signal ( false ) ;
29- private clickThreshold = 3 ; // Lower threshold to better differentiate clicks from drags
30+ private clickThreshold = 3 ;
31+
32+
33+ private startPosition = signal ( 0 ) ;
34+ private clickStartTime = signal ( 0 ) ;
3035
3136 ngAfterViewInit ( ) : void {
32- // Add passive event listener for smoother scrolling
3337 this . ngZone . runOutsideAngular ( ( ) => {
3438 this . element . nativeElement . addEventListener ( 'wheel' , ( e : WheelEvent ) => {
3539 e . preventDefault ( ) ;
3640 const scrollAmount = e . deltaY || e . deltaX ;
3741 this . element . nativeElement . scrollLeft += scrollAmount ;
3842 } , { passive : false } ) ;
3943
40- // Prevent default drag behavior on all images within the container
41- const images = this . element . nativeElement . querySelectorAll ( 'img' ) ;
42- images . forEach ( ( img : HTMLImageElement ) => {
43- img . setAttribute ( 'draggable' , 'false' ) ;
44- img . style . pointerEvents = 'none' ;
44+
45+ const observer = new MutationObserver ( ( mutations ) => {
46+ this . preventElementDrag ( ) ;
4547 } ) ;
4648
47- // Also prevent drag on app-event-header components
48- const headers = this . element . nativeElement . querySelectorAll ( 'app-event-header' ) ;
49- headers . forEach ( ( header : HTMLElement ) => {
50- header . addEventListener ( 'mousedown' , ( e : MouseEvent ) => {
51- // Only prevent default for left mouse button when we're intending to drag
52- if ( e . button === 0 ) {
53- e . preventDefault ( ) ;
54- }
55- } ) ;
56-
57- // Prevent drag start
58- header . addEventListener ( 'dragstart' , ( e : DragEvent ) => {
59- e . preventDefault ( ) ;
60- } ) ;
49+ observer . observe ( this . element . nativeElement , {
50+ childList : true ,
51+ subtree : true
6152 } ) ;
53+
54+
55+ this . preventElementDrag ( ) ;
6256 } ) ;
6357 }
6458
59+
60+ private preventElementDrag ( ) : void {
61+
62+ const images = this . element . nativeElement . querySelectorAll ( 'img' ) ;
63+ images . forEach ( ( img : HTMLImageElement ) => {
64+ img . setAttribute ( 'draggable' , 'false' ) ;
65+ img . style . pointerEvents = 'none' ;
66+ } ) ;
67+
68+
69+ const headers = this . element . nativeElement . querySelectorAll ( 'app-event-header' ) ;
70+ headers . forEach ( ( header : HTMLElement ) => {
71+ header . setAttribute ( 'draggable' , 'false' ) ;
72+
73+
74+ header . removeEventListener ( 'dragstart' , this . preventDragHandler ) ;
75+ header . addEventListener ( 'dragstart' , this . preventDragHandler ) ;
76+ } ) ;
77+ }
78+
79+
80+ private preventDragHandler = ( e : DragEvent ) => {
81+ e . preventDefault ( ) ;
82+ e . stopPropagation ( ) ;
83+ return false ;
84+ } ;
85+
6586 ngOnDestroy ( ) : void {
6687 this . cancelAnimation ( ) ;
88+
89+
90+ try {
91+ const headers = this . element . nativeElement . querySelectorAll ( 'app-event-header' ) ;
92+ headers . forEach ( ( header : HTMLElement ) => {
93+ header . removeEventListener ( 'dragstart' , this . preventDragHandler ) ;
94+ } ) ;
95+ } catch ( error ) {
96+
97+ }
6798 }
6899
69100 @HostListener ( 'mousedown' , [ '$event' ] )
70101 onMouseDown ( event : MouseEvent ) : void {
71102 this . cancelAnimation ( ) ;
72103
104+
73105 this . isDown . set ( true ) ;
74106 this . startTime . set ( Date . now ( ) ) ;
75107 this . moveDistance . set ( 0 ) ;
@@ -78,12 +110,27 @@ export class DragScrollDirective implements AfterViewInit, OnDestroy {
78110 this . timestamp . set ( Date . now ( ) ) ;
79111 this . velocity . set ( 0 ) ;
80112
113+
114+ this . startPosition . set ( event . pageX ) ;
115+ this . clickStartTime . set ( Date . now ( ) ) ;
116+
81117 this . element . nativeElement . classList . add ( 'active' ) ;
82118 this . startX . set ( event . pageX ) ;
83119 this . scrollLeft . set ( this . element . nativeElement . scrollLeft ) ;
84120
85- // Prevent default behavior for drag scrolling
86- event . preventDefault ( ) ;
121+
122+
123+ const target = event . target as HTMLElement ;
124+ const isClickable =
125+ target . tagName === 'BUTTON' ||
126+ target . tagName === 'A' ||
127+ target . closest ( 'button' ) ||
128+ target . closest ( 'a' ) ||
129+ target . closest ( 'mat-icon' ) ;
130+
131+ if ( ! isClickable ) {
132+ event . preventDefault ( ) ;
133+ }
87134 }
88135
89136 @HostListener ( 'mouseleave' )
@@ -93,10 +140,7 @@ export class DragScrollDirective implements AfterViewInit, OnDestroy {
93140 this . isDown . set ( false ) ;
94141 this . element . nativeElement . classList . remove ( 'active' ) ;
95142
96- // Start momentum scrolling if we were dragging
97- if ( this . isDragging ( ) ) {
98- this . autoScroll ( ) ;
99- }
143+ this . cancelAnimation ( ) ;
100144 }
101145
102146 @HostListener ( 'mouseup' , [ '$event' ] )
@@ -106,27 +150,37 @@ export class DragScrollDirective implements AfterViewInit, OnDestroy {
106150 this . isDown . set ( false ) ;
107151 this . element . nativeElement . classList . remove ( 'active' ) ;
108152
109- // If just a click (no significant drag) allow event to propagate
110- if ( ! this . isDragging ( ) && this . moveDistance ( ) < this . clickThreshold ) {
153+
154+ const distance = Math . abs ( event . pageX - this . startPosition ( ) ) ;
155+ const timeElapsed = Date . now ( ) - this . clickStartTime ( ) ;
156+
157+
158+ const isClick = distance < 5 && timeElapsed < 300 ;
159+
160+
161+ if ( isClick ) {
162+ this . suppressClick . set ( false ) ;
111163 return ;
112164 }
113165
114- // If we've dragged significantly, prevent the click event
166+
115167 if ( this . isDragging ( ) || this . moveDistance ( ) >= this . clickThreshold ) {
116- this . suppressClick . set ( true ) ;
117168
118- // Start momentum scrolling
119- this . autoScroll ( ) ;
169+ if ( distance > 10 ) {
170+ this . suppressClick . set ( true ) ;
171+ }
172+
173+ this . cancelAnimation ( ) ;
120174 }
121175 }
122176
123- // Add a click event handler to prevent clicks after dragging
124177 @HostListener ( 'click' , [ '$event' ] )
125178 onClick ( event : MouseEvent ) : void {
179+
126180 if ( this . suppressClick ( ) ) {
127181 event . stopPropagation ( ) ;
128182 event . preventDefault ( ) ;
129- this . suppressClick . set ( false ) ; // Reset for next interaction
183+ this . suppressClick . set ( false ) ;
130184 }
131185 }
132186
@@ -137,38 +191,31 @@ export class DragScrollDirective implements AfterViewInit, OnDestroy {
137191 const x = event . pageX ;
138192 const delta = x - this . startX ( ) ;
139193
140- // If moved more than threshold, consider it a drag
194+
141195 if ( Math . abs ( delta ) > this . clickThreshold && ! this . isDragging ( ) ) {
142196 this . isDragging . set ( true ) ;
143- event . preventDefault ( ) ; // Prevent text selection when dragging
144197 }
145198
146- // Calculate velocity for momentum scrolling
147199 const now = Date . now ( ) ;
148200 const elapsed = now - this . timestamp ( ) ;
149201
150- // Update timestamp
151202 this . timestamp . set ( now ) ;
152203
153- // Calculate movement
154- const currentScrollLeft = this . scrollLeft ( ) - delta ;
204+ const damping = 0.85 ;
205+ const currentScrollLeft = this . scrollLeft ( ) - ( delta * damping ) ;
155206 this . element . nativeElement . scrollLeft = currentScrollLeft ;
156207
157- // Update values for next move
158208 this . startX . set ( x ) ;
159209 this . scrollLeft . set ( this . element . nativeElement . scrollLeft ) ;
160210
161- // Calculate velocity (pixels/ms)
162211 if ( elapsed > 0 ) {
163- const v = 0.8 * ( 1000 * delta / ( 1 + elapsed ) ) + 0.2 * this . velocity ( ) ;
212+ const v = 0.6 * ( 1000 * delta / ( 1 + elapsed ) ) + 0.4 * this . velocity ( ) ;
164213 this . velocity . set ( v ) ;
165214 }
166215
167- // Track total distance moved for distinguishing clicks from drags
168216 this . moveDistance . set ( this . moveDistance ( ) + Math . abs ( delta ) ) ;
169217 }
170218
171- // Touch support with inertial scrolling
172219 @HostListener ( 'touchstart' , [ '$event' ] )
173220 onTouchStart ( event : TouchEvent ) : void {
174221 if ( event . touches . length !== 1 ) return ;
@@ -195,13 +242,11 @@ export class DragScrollDirective implements AfterViewInit, OnDestroy {
195242 this . isDown . set ( false ) ;
196243 this . element . nativeElement . classList . remove ( 'active' ) ;
197244
198- // If we've dragged significantly, prevent any click
199245 if ( this . isDragging ( ) || this . moveDistance ( ) >= this . clickThreshold ) {
200246 this . suppressClick . set ( true ) ;
201247 event . preventDefault ( ) ;
202248
203- // Start momentum scrolling
204- this . autoScroll ( ) ;
249+ this . cancelAnimation ( ) ;
205250 }
206251 }
207252
@@ -212,49 +257,45 @@ export class DragScrollDirective implements AfterViewInit, OnDestroy {
212257 const x = event . touches [ 0 ] . pageX ;
213258 const delta = x - this . startX ( ) ;
214259
215- // If moved more than threshold, consider it a drag
216260 if ( Math . abs ( delta ) > 5 && ! this . isDragging ( ) ) {
217261 this . isDragging . set ( true ) ;
218262 }
219263
220- // Calculate velocity for momentum scrolling
221264 const now = Date . now ( ) ;
222265 const elapsed = now - this . timestamp ( ) ;
223266
224- // Update timestamp
225267 this . timestamp . set ( now ) ;
226268
227- // Calculate movement - smoother for touch
228- const currentScrollLeft = this . scrollLeft ( ) - delta * 1.2 ; // Slightly faster for touch
269+ const touchSpeedMultiplier = 1.0 ;
270+ const currentScrollLeft = this . scrollLeft ( ) - ( delta * touchSpeedMultiplier ) ;
229271 this . element . nativeElement . scrollLeft = currentScrollLeft ;
230272
231- // Update values for next move
232273 this . startX . set ( x ) ;
233274 this . scrollLeft . set ( this . element . nativeElement . scrollLeft ) ;
234275
235- // Calculate velocity (pixels/ms)
236276 if ( elapsed > 0 ) {
237- const v = 0.8 * ( 1000 * delta / ( 1 + elapsed ) ) + 0.2 * this . velocity ( ) ;
277+ const v = 0.6 * ( 1000 * delta / ( 1 + elapsed ) ) + 0.4 * this . velocity ( ) ;
238278 this . velocity . set ( v ) ;
239279 }
240280
241- // Track total distance moved for distinguishing taps from drags
242281 this . moveDistance . set ( this . moveDistance ( ) + Math . abs ( delta ) ) ;
243282 }
244283
245- // Momentum scrolling with physics-based deceleration
246284 private autoScroll ( ) : void {
247- const amplitude = this . velocity ( ) * 0.8 ; // Adjust amplitude for desired momentum
285+ const amplitude = this . velocity ( ) * this . velocityFactor ;
248286 const initialPosition = this . element . nativeElement . scrollLeft ;
249- const targetPosition = initialPosition - amplitude * 15 ; // Adjust multiplier for momentum distance
250287
251- this . amplitude . set ( targetPosition - initialPosition ) ;
252- this . target . set ( targetPosition ) ;
288+ const targetPosition = initialPosition - amplitude * this . momentumMultiplier ;
289+
290+ const maxScrollLeft = this . element . nativeElement . scrollWidth - this . element . nativeElement . clientWidth ;
291+ const boundedTarget = Math . max ( 0 , Math . min ( targetPosition , maxScrollLeft ) ) ;
292+
293+ this . amplitude . set ( boundedTarget - initialPosition ) ;
294+ this . target . set ( boundedTarget ) ;
253295 this . timestamp . set ( Date . now ( ) ) ;
254296
255297 this . animationActive = true ;
256298
257- // Run animation outside Angular for better performance
258299 this . ngZone . runOutsideAngular ( ( ) => {
259300 cancelAnimationFrame ( this . frame ( ) ) ;
260301 this . frame . set ( requestAnimationFrame ( ( ) => this . autoScrollStep ( ) ) ) ;
@@ -279,5 +320,7 @@ export class DragScrollDirective implements AfterViewInit, OnDestroy {
279320 private cancelAnimation ( ) : void {
280321 this . animationActive = false ;
281322 cancelAnimationFrame ( this . frame ( ) ) ;
323+
324+ this . velocity . set ( 0 ) ;
282325 }
283326}
0 commit comments