-
Notifications
You must be signed in to change notification settings - Fork 6.7k
/
scrollable.ts
204 lines (182 loc) · 7.14 KB
/
scrollable.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Directionality} from '@angular/cdk/bidi';
import {
getRtlScrollAxisType,
RtlScrollAxisType,
supportsScrollBehavior,
} from '@angular/cdk/platform';
import {Directive, ElementRef, NgZone, OnDestroy, OnInit, Optional} from '@angular/core';
import {fromEvent, Observable, Subject, Observer} from 'rxjs';
import {takeUntil} from 'rxjs/operators';
import {ScrollDispatcher} from './scroll-dispatcher';
export type _Without<T> = {[P in keyof T]?: never};
export type _XOR<T, U> = (_Without<T> & U) | (_Without<U> & T);
export type _Top = {top?: number};
export type _Bottom = {bottom?: number};
export type _Left = {left?: number};
export type _Right = {right?: number};
export type _Start = {start?: number};
export type _End = {end?: number};
export type _XAxis = _XOR<_XOR<_Left, _Right>, _XOR<_Start, _End>>;
export type _YAxis = _XOR<_Top, _Bottom>;
/**
* An extended version of ScrollToOptions that allows expressing scroll offsets relative to the
* top, bottom, left, right, start, or end of the viewport rather than just the top and left.
* Please note: the top and bottom properties are mutually exclusive, as are the left, right,
* start, and end properties.
*/
export type ExtendedScrollToOptions = _XAxis & _YAxis & ScrollOptions;
/**
* Sends an event when the directive's element is scrolled. Registers itself with the
* ScrollDispatcher service to include itself as part of its collection of scrolling events that it
* can be listened to through the service.
*/
@Directive({
selector: '[cdk-scrollable], [cdkScrollable]',
standalone: true,
})
export class CdkScrollable implements OnInit, OnDestroy {
protected readonly _destroyed = new Subject<void>();
protected _elementScrolled: Observable<Event> = new Observable((observer: Observer<Event>) =>
this.ngZone.runOutsideAngular(() =>
fromEvent(this.elementRef.nativeElement, 'scroll')
.pipe(takeUntil(this._destroyed))
.subscribe(observer),
),
);
constructor(
protected elementRef: ElementRef<HTMLElement>,
protected scrollDispatcher: ScrollDispatcher,
protected ngZone: NgZone,
@Optional() protected dir?: Directionality,
) {}
ngOnInit() {
this.scrollDispatcher.register(this);
}
ngOnDestroy() {
this.scrollDispatcher.deregister(this);
this._destroyed.next();
this._destroyed.complete();
}
/** Returns observable that emits when a scroll event is fired on the host element. */
elementScrolled(): Observable<Event> {
return this._elementScrolled;
}
/** Gets the ElementRef for the viewport. */
getElementRef(): ElementRef<HTMLElement> {
return this.elementRef;
}
/**
* Scrolls to the specified offsets. This is a normalized version of the browser's native scrollTo
* method, since browsers are not consistent about what scrollLeft means in RTL. For this method
* left and right always refer to the left and right side of the scrolling container irrespective
* of the layout direction. start and end refer to left and right in an LTR context and vice-versa
* in an RTL context.
* @param options specified the offsets to scroll to.
*/
scrollTo(options: ExtendedScrollToOptions): void {
const el = this.elementRef.nativeElement;
const isRtl = this.dir && this.dir.value == 'rtl';
// Rewrite start & end offsets as right or left offsets.
if (options.left == null) {
options.left = isRtl ? options.end : options.start;
}
if (options.right == null) {
options.right = isRtl ? options.start : options.end;
}
// Rewrite the bottom offset as a top offset.
if (options.bottom != null) {
(options as _Without<_Bottom> & _Top).top =
el.scrollHeight - el.clientHeight - options.bottom;
}
// Rewrite the right offset as a left offset.
if (isRtl && getRtlScrollAxisType() != RtlScrollAxisType.NORMAL) {
if (options.left != null) {
(options as _Without<_Left> & _Right).right =
el.scrollWidth - el.clientWidth - options.left;
}
if (getRtlScrollAxisType() == RtlScrollAxisType.INVERTED) {
options.left = options.right;
} else if (getRtlScrollAxisType() == RtlScrollAxisType.NEGATED) {
options.left = options.right ? -options.right : options.right;
}
} else {
if (options.right != null) {
(options as _Without<_Right> & _Left).left =
el.scrollWidth - el.clientWidth - options.right;
}
}
this._applyScrollToOptions(options);
}
private _applyScrollToOptions(options: ScrollToOptions): void {
const el = this.elementRef.nativeElement;
if (supportsScrollBehavior()) {
el.scrollTo(options);
} else {
if (options.top != null) {
el.scrollTop = options.top;
}
if (options.left != null) {
el.scrollLeft = options.left;
}
}
}
/**
* Measures the scroll offset relative to the specified edge of the viewport. This method can be
* used instead of directly checking scrollLeft or scrollTop, since browsers are not consistent
* about what scrollLeft means in RTL. The values returned by this method are normalized such that
* left and right always refer to the left and right side of the scrolling container irrespective
* of the layout direction. start and end refer to left and right in an LTR context and vice-versa
* in an RTL context.
* @param from The edge to measure from.
*/
measureScrollOffset(from: 'top' | 'left' | 'right' | 'bottom' | 'start' | 'end'): number {
const LEFT = 'left';
const RIGHT = 'right';
const el = this.elementRef.nativeElement;
if (from == 'top') {
return el.scrollTop;
}
if (from == 'bottom') {
return el.scrollHeight - el.clientHeight - el.scrollTop;
}
// Rewrite start & end as left or right offsets.
const isRtl = this.dir && this.dir.value == 'rtl';
if (from == 'start') {
from = isRtl ? RIGHT : LEFT;
} else if (from == 'end') {
from = isRtl ? LEFT : RIGHT;
}
if (isRtl && getRtlScrollAxisType() == RtlScrollAxisType.INVERTED) {
// For INVERTED, scrollLeft is (scrollWidth - clientWidth) when scrolled all the way left and
// 0 when scrolled all the way right.
if (from == LEFT) {
return el.scrollWidth - el.clientWidth - el.scrollLeft;
} else {
return el.scrollLeft;
}
} else if (isRtl && getRtlScrollAxisType() == RtlScrollAxisType.NEGATED) {
// For NEGATED, scrollLeft is -(scrollWidth - clientWidth) when scrolled all the way left and
// 0 when scrolled all the way right.
if (from == LEFT) {
return el.scrollLeft + el.scrollWidth - el.clientWidth;
} else {
return -el.scrollLeft;
}
} else {
// For NORMAL, as well as non-RTL contexts, scrollLeft is 0 when scrolled all the way left and
// (scrollWidth - clientWidth) when scrolled all the way right.
if (from == LEFT) {
return el.scrollLeft;
} else {
return el.scrollWidth - el.clientWidth - el.scrollLeft;
}
}
}
}