-
Notifications
You must be signed in to change notification settings - Fork 6.8k
/
Copy pathtab-body.ts
286 lines (245 loc) · 10.2 KB
/
tab-body.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
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
/**
* @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 {
Component,
ChangeDetectorRef,
Input,
Inject,
Output,
EventEmitter,
OnDestroy,
OnInit,
ElementRef,
Directive,
Optional,
ViewEncapsulation,
ChangeDetectionStrategy,
ComponentFactoryResolver,
ViewContainerRef,
forwardRef,
ViewChild,
OnChanges,
SimpleChanges,
} from '@angular/core';
import {AnimationEvent} from '@angular/animations';
import {TemplatePortal, CdkPortalOutlet, PortalHostDirective} from '@angular/cdk/portal';
import {Directionality, Direction} from '@angular/cdk/bidi';
import {Subscription, Subject} from 'rxjs';
import {matTabsAnimations} from './tabs-animations';
import {startWith, distinctUntilChanged} from 'rxjs/operators';
/**
* These position states are used internally as animation states for the tab body. Setting the
* position state to left, right, or center will transition the tab body from its current
* position to its respective state. If there is not current position (void, in the case of a new
* tab body), then there will be no transition animation to its state.
*
* In the case of a new tab body that should immediately be centered with an animating transition,
* then left-origin-center or right-origin-center can be used, which will use left or right as its
* psuedo-prior state.
*/
export type MatTabBodyPositionState =
'left' | 'center' | 'right' | 'left-origin-center' | 'right-origin-center';
/**
* The origin state is an internally used state that is set on a new tab body indicating if it
* began to the left or right of the prior selected index. For example, if the selected index was
* set to 1, and a new tab is created and selected at index 2, then the tab body would have an
* origin of right because its index was greater than the prior selected index.
*/
export type MatTabBodyOriginState = 'left' | 'right';
/**
* The portal host directive for the contents of the tab.
* @docs-private
*/
@Directive({
selector: '[matTabBodyHost]'
})
export class MatTabBodyPortal extends CdkPortalOutlet implements OnInit, OnDestroy {
/** Subscription to events for when the tab body begins centering. */
private _centeringSub = Subscription.EMPTY;
/** Subscription to events for when the tab body finishes leaving from center position. */
private _leavingSub = Subscription.EMPTY;
constructor(
componentFactoryResolver: ComponentFactoryResolver,
viewContainerRef: ViewContainerRef,
@Inject(forwardRef(() => MatTabBody)) private _host: MatTabBody) {
super(componentFactoryResolver, viewContainerRef);
}
/** Set initial visibility or set up subscription for changing visibility. */
ngOnInit(): void {
super.ngOnInit();
this._centeringSub = this._host._beforeCentering
.pipe(startWith(this._host._isCenterPosition(this._host._position)))
.subscribe((isCentering: boolean) => {
if (isCentering && !this.hasAttached()) {
this.attach(this._host._content);
this._host._restoreScrollPosition();
}
});
this._leavingSub = this._host._afterLeavingCenter.subscribe(() => {
this.detach();
});
}
/** Clean up centering subscription. */
ngOnDestroy(): void {
super.ngOnDestroy();
this._centeringSub.unsubscribe();
this._leavingSub.unsubscribe();
}
}
/**
* Wrapper for the contents of a tab.
* @docs-private
*/
@Component({
moduleId: module.id,
selector: 'mat-tab-body',
templateUrl: 'tab-body.html',
styleUrls: ['tab-body.css'],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [matTabsAnimations.translateTab],
host: {
'class': 'mat-tab-body',
'[class.mat-tab-body-active]': 'active',
},
})
export class MatTabBody implements OnInit, OnChanges, OnDestroy {
/** Current position of the tab-body in the tab-group. Zero means that the tab is visible. */
private _positionIndex: number;
/** Subscription to the directionality change observable. */
private _dirChangeSubscription = Subscription.EMPTY;
/** Scroll position of the tab before the user switched away. */
private _lastScrollPosition = 0;
/** Tab body position state. Used by the animation trigger for the current state. */
_position: MatTabBodyPositionState;
/** Emits when an animation on the tab is complete. */
_translateTabComplete = new Subject<AnimationEvent>();
/** Element wrapping the tab's content. */
@ViewChild('content') _contentElement: ElementRef;
/** Event emitted when the tab begins to animate towards the center as the active tab. */
@Output() readonly _onCentering: EventEmitter<number> = new EventEmitter<number>();
/** Event emitted before the centering of the tab begins. */
@Output() readonly _beforeCentering: EventEmitter<boolean> = new EventEmitter<boolean>();
/** Event emitted before the centering of the tab begins. */
@Output() readonly _afterLeavingCenter: EventEmitter<boolean> = new EventEmitter<boolean>();
/** Event emitted when the tab completes its animation towards the center. */
@Output() readonly _onCentered: EventEmitter<void> = new EventEmitter<void>(true);
/** The portal host inside of this container into which the tab body content will be loaded. */
@ViewChild(PortalHostDirective) _portalHost: PortalHostDirective;
/** The tab body content to display. */
@Input('content') _content: TemplatePortal;
/** Position that will be used when the tab is immediately becoming visible after creation. */
@Input() origin: number;
// Note that the default value will always be overwritten by `MatTabBody`, but we need one
// anyway to prevent the animations module from throwing an error if the body is used on its own.
/** Duration for the tab's animation. */
@Input() animationDuration: string = '500ms';
/** Whether the tab is currently active. */
@Input() active: boolean;
/** The shifted index position of the tab body, where zero represents the active center tab. */
@Input()
set position(position: number) {
this._positionIndex = position;
this._computePositionAnimationState();
}
constructor(private _elementRef: ElementRef<HTMLElement>,
@Optional() private _dir: Directionality,
/**
* @breaking-change 8.0.0 changeDetectorRef to be made required.
*/
changeDetectorRef?: ChangeDetectorRef) {
if (this._dir && changeDetectorRef) {
this._dirChangeSubscription = this._dir.change.subscribe((dir: Direction) => {
this._computePositionAnimationState(dir);
changeDetectorRef.markForCheck();
});
}
// Ensure that we get unique animation events, because the `.done` callback can get
// invoked twice in some browsers. See https://github.com/angular/angular/issues/24084.
this._translateTabComplete.pipe(distinctUntilChanged((x, y) => {
return x.fromState === y.fromState && x.toState === y.toState;
})).subscribe(event => {
// If the transition to the center is complete, emit an event.
if (this._isCenterPosition(event.toState) && this._isCenterPosition(this._position)) {
this._onCentered.emit();
}
if (this._isCenterPosition(event.fromState) && !this._isCenterPosition(this._position)) {
this._afterLeavingCenter.emit();
}
});
}
/**
* After initialized, check if the content is centered and has an origin. If so, set the
* special position states that transition the tab from the left or right before centering.
*/
ngOnInit() {
if (this._position == 'center' && this.origin != null) {
this._position = this._computePositionFromOrigin();
}
}
ngOnDestroy() {
this._dirChangeSubscription.unsubscribe();
this._translateTabComplete.complete();
}
_onTranslateTabStarted(event: AnimationEvent): void {
const isCentering = this._isCenterPosition(event.toState);
this._beforeCentering.emit(isCentering);
if (isCentering) {
this._onCentering.emit(this._elementRef.nativeElement.clientHeight);
}
}
ngOnChanges(changes: SimpleChanges) {
// Cache the scroll position before moving away from the tab. Note that this has to be done
// through change detection and as early as possible, because some browsers (namely Safari)
// will reset the scroll position when we switch from an absolute to a relative position.
if (changes.active && changes.active.previousValue) {
this._lastScrollPosition = this._elementRef.nativeElement.scrollTop ||
this._contentElement.nativeElement.scrollTop;
}
}
/** The text direction of the containing app. */
_getLayoutDirection(): Direction {
return this._dir && this._dir.value === 'rtl' ? 'rtl' : 'ltr';
}
/** Whether the provided position state is considered center, regardless of origin. */
_isCenterPosition(position: MatTabBodyPositionState | string): boolean {
return position == 'center' ||
position == 'left-origin-center' ||
position == 'right-origin-center';
}
_restoreScrollPosition() {
if (this._lastScrollPosition) {
// Depending on the browser, the scrollable element can end up being
// either the host element or the element with all the content.
this._contentElement.nativeElement.scrollTop =
this._elementRef.nativeElement.scrollTop =
this._lastScrollPosition;
}
}
/** Computes the position state that will be used for the tab-body animation trigger. */
private _computePositionAnimationState(dir: Direction = this._getLayoutDirection()) {
if (this._positionIndex < 0) {
this._position = dir == 'ltr' ? 'left' : 'right';
} else if (this._positionIndex > 0) {
this._position = dir == 'ltr' ? 'right' : 'left';
} else {
this._position = 'center';
}
}
/**
* Computes the position state based on the specified origin position. This is used if the
* tab is becoming visible immediately after creation.
*/
private _computePositionFromOrigin(): MatTabBodyPositionState {
const dir = this._getLayoutDirection();
if ((dir == 'ltr' && this.origin <= 0) || (dir == 'rtl' && this.origin > 0)) {
return 'left-origin-center';
}
return 'right-origin-center';
}
}