257 lines
8.7 KiB
TypeScript
257 lines
8.7 KiB
TypeScript
|
|
/**
|
||
|
|
* @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.dev/license
|
||
|
|
*/
|
||
|
|
|
||
|
|
import {AnimationEvent} from '@angular/animations';
|
||
|
|
import {Direction, Directionality} from '@angular/cdk/bidi';
|
||
|
|
import {CdkPortalOutlet, TemplatePortal} from '@angular/cdk/portal';
|
||
|
|
import {CdkScrollable} from '@angular/cdk/scrolling';
|
||
|
|
import {
|
||
|
|
ChangeDetectionStrategy,
|
||
|
|
ChangeDetectorRef,
|
||
|
|
Component,
|
||
|
|
Directive,
|
||
|
|
ElementRef,
|
||
|
|
EventEmitter,
|
||
|
|
Input,
|
||
|
|
OnDestroy,
|
||
|
|
OnInit,
|
||
|
|
Output,
|
||
|
|
ViewChild,
|
||
|
|
ViewEncapsulation,
|
||
|
|
inject,
|
||
|
|
} from '@angular/core';
|
||
|
|
import {Subject, Subscription} from 'rxjs';
|
||
|
|
import {startWith} from 'rxjs/operators';
|
||
|
|
import {matTabsAnimations} from './tabs-animations';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The portal host directive for the contents of the tab.
|
||
|
|
* @docs-private
|
||
|
|
*/
|
||
|
|
@Directive({
|
||
|
|
selector: '[matTabBodyHost]',
|
||
|
|
})
|
||
|
|
export class MatTabBodyPortal extends CdkPortalOutlet implements OnInit, OnDestroy {
|
||
|
|
private _host = inject(MatTabBody);
|
||
|
|
|
||
|
|
/** 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(...args: unknown[]);
|
||
|
|
|
||
|
|
constructor() {
|
||
|
|
super();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Set initial visibility or set up subscription for changing visibility. */
|
||
|
|
override ngOnInit(): void {
|
||
|
|
super.ngOnInit();
|
||
|
|
|
||
|
|
this._centeringSub = this._host._beforeCentering
|
||
|
|
.pipe(startWith(this._host._isCenterPosition(this._host._position)))
|
||
|
|
.subscribe((isCentering: boolean) => {
|
||
|
|
if (this._host._content && isCentering && !this.hasAttached()) {
|
||
|
|
this.attach(this._host._content);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
this._leavingSub = this._host._afterLeavingCenter.subscribe(() => {
|
||
|
|
if (!this._host.preserveContent) {
|
||
|
|
this.detach();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Clean up centering subscription. */
|
||
|
|
override ngOnDestroy(): void {
|
||
|
|
super.ngOnDestroy();
|
||
|
|
this._centeringSub.unsubscribe();
|
||
|
|
this._leavingSub.unsubscribe();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 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
|
||
|
|
* pseudo-prior state.
|
||
|
|
*/
|
||
|
|
export type MatTabBodyPositionState =
|
||
|
|
| 'left'
|
||
|
|
| 'center'
|
||
|
|
| 'right'
|
||
|
|
| 'left-origin-center'
|
||
|
|
| 'right-origin-center';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Wrapper for the contents of a tab.
|
||
|
|
* @docs-private
|
||
|
|
*/
|
||
|
|
@Component({
|
||
|
|
selector: 'mat-tab-body',
|
||
|
|
templateUrl: 'tab-body.html',
|
||
|
|
styleUrl: 'tab-body.css',
|
||
|
|
encapsulation: ViewEncapsulation.None,
|
||
|
|
// tslint:disable-next-line:validate-decorators
|
||
|
|
changeDetection: ChangeDetectionStrategy.Default,
|
||
|
|
animations: [matTabsAnimations.translateTab],
|
||
|
|
host: {
|
||
|
|
'class': 'mat-mdc-tab-body',
|
||
|
|
},
|
||
|
|
imports: [MatTabBodyPortal, CdkScrollable],
|
||
|
|
})
|
||
|
|
export class MatTabBody implements OnInit, OnDestroy {
|
||
|
|
private _elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
|
||
|
|
private _dir = inject(Directionality, {optional: true});
|
||
|
|
|
||
|
|
/** 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;
|
||
|
|
|
||
|
|
/** Tab body position state. Used by the animation trigger for the current state. */
|
||
|
|
_position: MatTabBodyPositionState;
|
||
|
|
|
||
|
|
/** Emits when an animation on the tab is complete. */
|
||
|
|
readonly _translateTabComplete = new Subject<AnimationEvent>();
|
||
|
|
|
||
|
|
/** 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<void> = new EventEmitter<void>();
|
||
|
|
|
||
|
|
/** 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(CdkPortalOutlet) _portalHost: CdkPortalOutlet;
|
||
|
|
|
||
|
|
/** 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 | null;
|
||
|
|
|
||
|
|
// 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's content should be kept in the DOM while it's off-screen. */
|
||
|
|
@Input() preserveContent: boolean = false;
|
||
|
|
|
||
|
|
/** 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(...args: unknown[]);
|
||
|
|
|
||
|
|
constructor() {
|
||
|
|
if (this._dir) {
|
||
|
|
const changeDetectorRef = inject(ChangeDetectorRef);
|
||
|
|
this._dirChangeSubscription = this._dir.change.subscribe((dir: Direction) => {
|
||
|
|
this._computePositionAnimationState(dir);
|
||
|
|
changeDetectorRef.markForCheck();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
this._translateTabComplete.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(this.origin);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 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'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 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(origin: number): MatTabBodyPositionState {
|
||
|
|
const dir = this._getLayoutDirection();
|
||
|
|
|
||
|
|
if ((dir == 'ltr' && origin <= 0) || (dir == 'rtl' && origin > 0)) {
|
||
|
|
return 'left-origin-center';
|
||
|
|
}
|
||
|
|
|
||
|
|
return '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';
|