• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IgniteUI / igniteui-angular / 13331632524

14 Feb 2025 02:51PM CUT coverage: 22.015% (-69.6%) from 91.622%
13331632524

Pull #15372

github

web-flow
Merge d52d57714 into bcb78ae0a
Pull Request #15372: chore(*): test ci passing

1990 of 15592 branches covered (12.76%)

431 of 964 new or added lines in 18 files covered. (44.71%)

19956 existing lines in 307 files now uncovered.

6452 of 29307 relevant lines covered (22.02%)

249.17 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

13.27
/projects/igniteui-angular/src/lib/directives/scroll-inertia/scroll_inertia.directive.ts
1
import { Directive, Input, ElementRef, NgZone, OnInit, OnDestroy } from '@angular/core';
2

3
/**
4
 * @hidden
5
 */
6
@Directive({
7
    selector: '[igxScrollInertia]',
8
    standalone: true
9
})
10
export class IgxScrollInertiaDirective implements OnInit, OnDestroy {
2✔
11

12
    @Input()
13
    public IgxScrollInertiaDirection: string;
14

15
    @Input()
16
    public IgxScrollInertiaScrollContainer: any;
17

18
    @Input()
19
    public wheelStep = 50;
486✔
20

21
    @Input()
22
    public inertiaStep = 1.5;
486✔
23

24
    @Input()
25
    public smoothingStep = 1.5;
486✔
26

27
    @Input()
28
    public smoothingDuration = 0.5;
486✔
29

30
    @Input()
31
    public swipeToleranceX = 20;
486✔
32

33
    @Input()
34
    public inertiaDeltaY = 3;
486✔
35

36
    @Input()
37
    public inertiaDeltaX = 2;
486✔
38

39
    @Input()
40
    public inertiaDuration = 0.5;
486✔
41

42
    private _touchInertiaAnimID;
43
    private _startX;
44
    private _startY;
45
    private _touchStartX;
46
    private _touchStartY;
47
    private _lastTouchEnd;
48
    private _lastTouchX;
49
    private _lastTouchY;
50
    private _savedSpeedsX = [];
486✔
51
    private _savedSpeedsY;
52
    private _totalMovedX;
53
    private _offsetRecorded;
54
    private _offsetDirection;
55
    private _lastMovedX;
56
    private _lastMovedY;
57
    private _nextX;
58
    private _nextY;
59
    private parentElement;
60
    private baseDeltaMultiplier = 1 / 120;
486✔
61
    private firefoxDeltaMultiplier = 1 / 30;
486✔
62

63
    constructor(private element: ElementRef, private _zone: NgZone) { }
486✔
64

65
    public ngOnInit(): void {
66
        this._zone.runOutsideAngular(() => {
486✔
67
            this.parentElement = this.element.nativeElement.parentElement || this.element.nativeElement.parentNode;
486!
68
            if (!this.parentElement) {
486!
69
                return;
×
70
            }
71
            const targetElem = this.parentElement;
486✔
72
            targetElem.addEventListener('wheel', this.onWheel.bind(this), { passive: false });
486✔
73
            targetElem.addEventListener('touchstart', this.onTouchStart.bind(this), { passive: false });
486✔
74
            targetElem.addEventListener('touchmove', this.onTouchMove.bind(this), { passive: false });
486✔
75
            targetElem.addEventListener('touchend', this.onTouchEnd.bind(this), { passive: false });
486✔
76
        });
77
    }
78

79
    public ngOnDestroy() {
80
        this._zone.runOutsideAngular(() => {
486✔
81
            const targetElem = this.parentElement;
486✔
82
            if (!targetElem) {
486!
83
                return;
×
84
            }
85
            targetElem.removeEventListener('wheel', this.onWheel);
486✔
86
            targetElem.removeEventListener('touchstart', this.onTouchStart);
486✔
87
            targetElem.removeEventListener('touchmove', this.onTouchMove);
486✔
88
            targetElem.removeEventListener('touchend', this.onTouchEnd);
486✔
89
        });
90
    }
91

92
    /**
93
     * @hidden
94
     * Function that is called when scrolling with the mouse wheel or using touchpad
95
     */
96
    protected onWheel(evt) {
97
        // if no scrollbar return
UNCOV
98
        if (!this.IgxScrollInertiaScrollContainer) {
×
UNCOV
99
            return;
×
100
        }
101
        // if ctrl key is pressed and the user want to zoom in/out the page
UNCOV
102
        if (evt.ctrlKey) {
×
103
            return;
×
104
        }
105
        let scrollDeltaX;
106
        let scrollDeltaY;
UNCOV
107
        const scrollStep = this.wheelStep;
×
UNCOV
108
        const minWheelStep = 1 / this.wheelStep;
×
UNCOV
109
        const smoothing = this.smoothingDuration !== 0;
×
110

UNCOV
111
        this._startX = this.IgxScrollInertiaScrollContainer.scrollLeft;
×
UNCOV
112
        this._startY = this.IgxScrollInertiaScrollContainer.scrollTop;
×
113

UNCOV
114
        if (evt.wheelDeltaX) {
×
115
            /* Option supported on Chrome, Safari, Opera.
116
            /* 120 is default for mousewheel on these browsers. Other values are for trackpads */
UNCOV
117
            scrollDeltaX = -evt.wheelDeltaX * this.baseDeltaMultiplier;
×
118

UNCOV
119
            if (-minWheelStep < scrollDeltaX && scrollDeltaX < minWheelStep) {
×
120
                scrollDeltaX = Math.sign(scrollDeltaX) * minWheelStep;
×
121
            }
UNCOV
122
        } else if (evt.deltaX) {
×
123
            /* For other browsers that don't provide wheelDelta, use the deltaY to determine direction and pass default values. */
UNCOV
124
            const deltaScaledX = evt.deltaX * (evt.deltaMode === 0 ? this.firefoxDeltaMultiplier : 1);
×
UNCOV
125
            scrollDeltaX = this.calcAxisCoords(deltaScaledX, -1, 1);
×
126
        }
127

128
        /** Get delta for the Y axis */
UNCOV
129
        if (evt.wheelDeltaY) {
×
130
            /* Option supported on Chrome, Safari, Opera.
131
            /* 120 is default for mousewheel on these browsers. Other values are for trackpads */
UNCOV
132
            scrollDeltaY = -evt.wheelDeltaY * this.baseDeltaMultiplier;
×
133

UNCOV
134
            if (-minWheelStep < scrollDeltaY && scrollDeltaY < minWheelStep) {
×
135
                scrollDeltaY = Math.sign(scrollDeltaY) * minWheelStep;
×
136
            }
UNCOV
137
        } else if (evt.deltaY) {
×
138
            /* For other browsers that don't provide wheelDelta, use the deltaY to determine direction and pass default values. */
UNCOV
139
            const deltaScaledY = evt.deltaY * (evt.deltaMode === 0 ? this.firefoxDeltaMultiplier : 1);
×
UNCOV
140
            scrollDeltaY = this.calcAxisCoords(deltaScaledY, -1, 1);
×
141
        }
142

UNCOV
143
        if (evt.composedPath && this.didChildScroll(evt, scrollDeltaX, scrollDeltaY)) {
×
144
            return;
×
145
        }
146

UNCOV
147
        if (scrollDeltaX && this.IgxScrollInertiaDirection === 'horizontal') {
×
UNCOV
148
            const nextLeft = this._startX + scrollDeltaX * scrollStep;
×
UNCOV
149
            if (!smoothing) {
×
UNCOV
150
                this._scrollToX(nextLeft);
×
151
            } else {
152
                this._smoothWheelScroll(scrollDeltaX);
×
153
            }
UNCOV
154
            const maxScrollLeft = parseInt(this.IgxScrollInertiaScrollContainer.children[0].style.width, 10);
×
UNCOV
155
            if (0 < nextLeft && nextLeft < maxScrollLeft) {
×
156
                // Prevent navigating through pages when scrolling on Mac
157
                evt.preventDefault();
×
158
            }
UNCOV
159
        } else if (evt.shiftKey && scrollDeltaY && this.IgxScrollInertiaDirection === 'horizontal') {
×
UNCOV
160
            if (!smoothing) {
×
UNCOV
161
                const step = this._startX + scrollDeltaY * scrollStep;
×
UNCOV
162
                this._scrollToX(step);
×
163
            } else {
164
                this._smoothWheelScroll(scrollDeltaY);
×
165
            }
UNCOV
166
        } else if (!evt.shiftKey && scrollDeltaY && this.IgxScrollInertiaDirection === 'vertical') {
×
UNCOV
167
            const nextTop = this._startY + scrollDeltaY * scrollStep;
×
UNCOV
168
            if (!smoothing) {
×
UNCOV
169
                this._scrollToY(nextTop);
×
170
            } else {
UNCOV
171
                this._smoothWheelScroll(scrollDeltaY);
×
172
            }
UNCOV
173
            this.preventParentScroll(evt, true, nextTop);
×
174
        }
175
    }
176

177
    /**
178
     * @hidden
179
     * When there is still room to scroll up/down prevent the parent elements from scrolling too.
180
     */
181
    protected preventParentScroll(evt, preventDefault, nextTop = 0) {
×
UNCOV
182
        const curScrollTop = nextTop === 0 ? this.IgxScrollInertiaScrollContainer.scrollTop : nextTop;
×
UNCOV
183
        const maxScrollTop = this.IgxScrollInertiaScrollContainer.children[0].scrollHeight -
×
184
            this.IgxScrollInertiaScrollContainer.offsetHeight;
UNCOV
185
        if (0 < curScrollTop && curScrollTop < maxScrollTop) {
×
UNCOV
186
            if (preventDefault) {
×
UNCOV
187
                evt.preventDefault();
×
188
            }
UNCOV
189
            if (evt.stopPropagation) {
×
UNCOV
190
                evt.stopPropagation();
×
191
            }
192
        }
193
    }
194

195
    /**
196
     * @hidden
197
     * Checks if the wheel event would have scrolled an element under the display container
198
     * in DOM tree so that it can correctly be ignored until that element can no longer be scrolled.
199
     */
200
    protected didChildScroll(evt, scrollDeltaX, scrollDeltaY): boolean {
UNCOV
201
        const path = evt.composedPath();
×
UNCOV
202
        let i = 0;
×
UNCOV
203
        while (i < path.length && path[i].localName !== 'igx-display-container') {
×
204
            const e = path[i++];
×
205
            if (e.scrollHeight > e.clientHeight) {
×
206
                const overflowY = window.getComputedStyle(e)['overflow-y'];
×
207
                if (overflowY === 'auto' || overflowY === 'scroll') {
×
208
                    if (scrollDeltaY > 0 && e.scrollHeight - Math.abs(Math.round(e.scrollTop)) !== e.clientHeight) {
×
209
                        return true;
×
210
                    }
211
                    if (scrollDeltaY < 0 && e.scrollTop !== 0) {
×
212
                        return true;
×
213
                    }
214
                }
215
            }
216
            if (e.scrollWidth > e.clientWidth) {
×
217
                const overflowX = window.getComputedStyle(e)['overflow-x'];
×
218
                if (overflowX === 'auto' || overflowX === 'scroll') {
×
219
                    if (scrollDeltaX > 0 && e.scrollWidth - Math.abs(Math.round(e.scrollLeft)) !== e.clientWidth) {
×
220
                        return true;
×
221
                    }
222
                    if (scrollDeltaX < 0 && e.scrollLeft !== 0) {
×
223
                        return true;
×
224
                    }
225
                }
226
            }
227
        }
UNCOV
228
        return false;
×
229
    }
230

231
    /**
232
     * @hidden
233
     * Function that is called the first moment we start interacting with the content on a touch device
234
     */
235
    protected onTouchStart(event) {
UNCOV
236
        if (!this.IgxScrollInertiaScrollContainer) {
×
UNCOV
237
            return false;
×
238
        }
239

240
        // stops any current ongoing inertia
UNCOV
241
        cancelAnimationFrame(this._touchInertiaAnimID);
×
242

UNCOV
243
        const touch = event.touches[0];
×
244

UNCOV
245
        this._startX = this.IgxScrollInertiaScrollContainer.scrollLeft;
×
246

UNCOV
247
        this._startY = this.IgxScrollInertiaScrollContainer.scrollTop;
×
248

UNCOV
249
        this._touchStartX = touch.pageX;
×
UNCOV
250
        this._touchStartY = touch.pageY;
×
251

UNCOV
252
        this._lastTouchEnd = new Date().getTime();
×
UNCOV
253
        this._lastTouchX = touch.pageX;
×
UNCOV
254
        this._lastTouchY = touch.pageY;
×
UNCOV
255
        this._savedSpeedsX = [];
×
UNCOV
256
        this._savedSpeedsY = [];
×
257

258
        // Vars regarding swipe offset
UNCOV
259
        this._totalMovedX = 0;
×
UNCOV
260
        this._offsetRecorded = false;
×
UNCOV
261
        this._offsetDirection = 0;
×
262

UNCOV
263
        if (this.IgxScrollInertiaDirection === 'vertical') {
×
UNCOV
264
            this.preventParentScroll(event, false);
×
265
        }
266
    }
267

268
    /**
269
     * @hidden
270
     * Function that is called when we need to scroll the content based on touch interactions
271
     */
272
    protected onTouchMove(event) {
UNCOV
273
        if (!this.IgxScrollInertiaScrollContainer) {
×
UNCOV
274
            return;
×
275
        }
276

UNCOV
277
        const touch = event.touches[0];
×
UNCOV
278
        const destX = this._startX + (this._touchStartX - touch.pageX) * Math.sign(this.inertiaStep);
×
UNCOV
279
        const destY = this._startY + (this._touchStartY - touch.pageY) * Math.sign(this.inertiaStep);
×
280

281
        /* Handle complex touchmoves when swipe stops but the toch doesn't end and then a swipe is initiated again */
282
        /* **********************************************************/
283

284

UNCOV
285
        const timeFromLastTouch = (new Date().getTime()) - this._lastTouchEnd;
×
UNCOV
286
        if (timeFromLastTouch !== 0 && timeFromLastTouch < 100) {
×
UNCOV
287
            const speedX = (this._lastTouchX - touch.pageX) / timeFromLastTouch;
×
UNCOV
288
            const speedY = (this._lastTouchY - touch.pageY) / timeFromLastTouch;
×
289

290
            // Save the last 5 speeds between two touchmoves on X axis
UNCOV
291
            if (this._savedSpeedsX.length < 5) {
×
UNCOV
292
                this._savedSpeedsX.push(speedX);
×
293
            } else {
294
                this._savedSpeedsX.shift();
×
295
                this._savedSpeedsX.push(speedX);
×
296
            }
297

298
            // Save the last 5 speeds between two touchmoves on Y axis
UNCOV
299
            if (this._savedSpeedsY.length < 5) {
×
UNCOV
300
                this._savedSpeedsY.push(speedY);
×
301
            } else {
302
                this._savedSpeedsY.shift();
×
303
                this._savedSpeedsY.push(speedY);
×
304
            }
305
        }
UNCOV
306
        this._lastTouchEnd = new Date().getTime();
×
UNCOV
307
        this._lastMovedX = this._lastTouchX - touch.pageX;
×
UNCOV
308
        this._lastMovedY = this._lastTouchY - touch.pageY;
×
UNCOV
309
        this._lastTouchX = touch.pageX;
×
UNCOV
310
        this._lastTouchY = touch.pageY;
×
311

UNCOV
312
        this._totalMovedX += this._lastMovedX;
×
313

314
        /*        Do not scroll using touch untill out of the swipeToleranceX bounds */
UNCOV
315
        if (Math.abs(this._totalMovedX) < this.swipeToleranceX && !this._offsetRecorded) {
×
UNCOV
316
            this._scrollTo(this._startX, destY);
×
317
        } else {
318
            /*        Record the direction the first time we are out of the swipeToleranceX bounds.
319
            *        That way we know which direction we apply the offset so it doesn't hickup when moving out of the swipeToleranceX bounds */
UNCOV
320
            if (!this._offsetRecorded) {
×
UNCOV
321
                this._offsetDirection = Math.sign(destX - this._startX);
×
UNCOV
322
                this._offsetRecorded = true;
×
323
            }
324

325
            /*        Scroll with offset ammout of swipeToleranceX in the direction we have exited the bounds and
326
            don't change it after that ever until touchend and again touchstart */
UNCOV
327
            this._scrollTo(destX - this._offsetDirection * this.swipeToleranceX, destY);
×
328
        }
329

330
        // On Safari preventing the touchmove would prevent default page scroll behaviour even if there is the element doesn't have overflow
UNCOV
331
        if (this.IgxScrollInertiaDirection === 'vertical') {
×
UNCOV
332
            this.preventParentScroll(event, true);
×
333
        }
334
    }
335

336
    protected onTouchEnd(event) {
UNCOV
337
        let speedX = 0;
×
UNCOV
338
        let speedY = 0;
×
339

340
        // savedSpeedsX and savedSpeedsY have same length
UNCOV
341
        for (let i = 0; i < this._savedSpeedsX.length; i++) {
×
UNCOV
342
            speedX += this._savedSpeedsX[i];
×
UNCOV
343
            speedY += this._savedSpeedsY[i];
×
344
        }
UNCOV
345
        speedX = this._savedSpeedsX.length ? speedX / this._savedSpeedsX.length : 0;
×
UNCOV
346
        speedY = this._savedSpeedsX.length ? speedY / this._savedSpeedsY.length : 0;
×
347

348
        // Use the lastMovedX and lastMovedY to determine if the swipe stops without lifting the finger so we don't start inertia
UNCOV
349
        if ((Math.abs(speedX) > 0.1 || Math.abs(speedY) > 0.1) &&
×
350
            (Math.abs(this._lastMovedX) > 2 || Math.abs(this._lastMovedY) > 2)) {
UNCOV
351
            this._inertiaInit(speedX, speedY);
×
352
        }
UNCOV
353
        if (this.IgxScrollInertiaDirection === 'vertical') {
×
354
            this.preventParentScroll(event, false);
×
355
        }
356
    }
357

358
    protected _smoothWheelScroll(delta) {
UNCOV
359
        this._nextY = this.IgxScrollInertiaScrollContainer.scrollTop;
×
UNCOV
360
        this._nextX = this.IgxScrollInertiaScrollContainer.scrollLeft;
×
UNCOV
361
        let x = -1;
×
UNCOV
362
        let wheelInertialAnimation = null;
×
UNCOV
363
        const inertiaWheelStep = () => {
×
UNCOV
364
            if (x > 1) {
×
UNCOV
365
                cancelAnimationFrame(wheelInertialAnimation);
×
UNCOV
366
                return;
×
367
            }
UNCOV
368
            const nextScroll = ((-3 * x * x + 3) * delta * 2) * this.smoothingStep;
×
UNCOV
369
            if (this.IgxScrollInertiaDirection === 'vertical') {
×
UNCOV
370
                this._nextY += nextScroll;
×
UNCOV
371
                this._scrollToY(this._nextY);
×
372
            } else {
373
                this._nextX += nextScroll;
×
374
                this._scrollToX(this._nextX);
×
375
            }
376
            //continue the inertia
UNCOV
377
            x += 0.08 * (1 / this.smoothingDuration);
×
UNCOV
378
            wheelInertialAnimation = requestAnimationFrame(inertiaWheelStep);
×
379
        };
UNCOV
380
        wheelInertialAnimation = requestAnimationFrame(inertiaWheelStep);
×
381
    }
382

383
    protected _inertiaInit(speedX, speedY) {
UNCOV
384
        const stepModifer = this.inertiaStep;
×
UNCOV
385
        const inertiaDuration = this.inertiaDuration;
×
UNCOV
386
        let x = 0;
×
UNCOV
387
        this._nextX = this.IgxScrollInertiaScrollContainer.scrollLeft;
×
UNCOV
388
        this._nextY = this.IgxScrollInertiaScrollContainer.scrollTop;
×
389

390
        // Sets timeout until executing next movement iteration of the inertia
UNCOV
391
        const inertiaStep = () => {
×
UNCOV
392
            if (x > 6) {
×
393
                cancelAnimationFrame(this._touchInertiaAnimID);
×
394
                return;
×
395
            }
396

UNCOV
397
            if (Math.abs(speedX) > Math.abs(speedY)) {
×
UNCOV
398
                x += 0.05 / (1 * inertiaDuration);
×
399
            } else {
UNCOV
400
                x += 0.05 / (1 * inertiaDuration);
×
401
            }
402

UNCOV
403
            if (x <= 1) {
×
404
                // We use constant quation to determine the offset without speed falloff befor x reaches 1
UNCOV
405
                if (Math.abs(speedY) <= Math.abs(speedX) * this.inertiaDeltaY) {
×
UNCOV
406
                    this._nextX += 1 * speedX * 15 * stepModifer;
×
407
                }
UNCOV
408
                if (Math.abs(speedY) >= Math.abs(speedX) * this.inertiaDeltaX) {
×
UNCOV
409
                    this._nextY += 1 * speedY * 15 * stepModifer;
×
410
                }
411
            } else {
412
                // We use the quation "y = 2 / (x + 0.55) - 0.3" to determine the offset
UNCOV
413
                if (Math.abs(speedY) <= Math.abs(speedX) * this.inertiaDeltaY) {
×
UNCOV
414
                    this._nextX += Math.abs(2 / (x + 0.55) - 0.3) * speedX * 15 * stepModifer;
×
415
                }
UNCOV
416
                if (Math.abs(speedY) >= Math.abs(speedX) * this.inertiaDeltaX) {
×
UNCOV
417
                    this._nextY += Math.abs(2 / (x + 0.55) - 0.3) * speedY * 15 * stepModifer;
×
418
                }
419
            }
420

421
            // If we have mixed environment we use the default behaviour. i.e. touchscreen + mouse
UNCOV
422
            this._scrollTo(this._nextX, this._nextY);
×
423

UNCOV
424
            this._touchInertiaAnimID = requestAnimationFrame(inertiaStep);
×
425
        };
426

427
        // Start inertia and continue it recursively
UNCOV
428
        this._touchInertiaAnimID = requestAnimationFrame(inertiaStep);
×
429
    }
430

431
    private calcAxisCoords(target, min, max) {
UNCOV
432
        if (target === undefined || target < min) {
×
433
            target = min;
×
UNCOV
434
        } else if (target > max) {
×
435
            target = max;
×
436
        }
437

UNCOV
438
        return target;
×
439
    }
440

441
    private _scrollTo(destX, destY) {
442
        // TODO Trigger scrolling event?
UNCOV
443
        const scrolledX = this._scrollToX(destX);
×
UNCOV
444
        const scrolledY = this._scrollToY(destY);
×
445

UNCOV
446
        return { x: scrolledX, y: scrolledY };
×
447
    }
448
    private _scrollToX(dest) {
UNCOV
449
        this.IgxScrollInertiaScrollContainer.scrollLeft = dest;
×
450
    }
451
    private _scrollToY(dest) {
UNCOV
452
        this.IgxScrollInertiaScrollContainer.scrollTop = dest;
×
453
    }
454
}
455

456
/**
457
 * @hidden
458
 */
459

460

STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc