sass-references/angular-material/material/snack-bar/snack-bar.ts

314 lines
11 KiB
TypeScript
Raw Permalink Normal View History

2024-12-06 10:42:08 +08:00
/**
* @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 {LiveAnnouncer} from '@angular/cdk/a11y';
import {BreakpointObserver, Breakpoints} from '@angular/cdk/layout';
import {ComponentType, Overlay, OverlayConfig, OverlayRef} from '@angular/cdk/overlay';
import {
ComponentRef,
EmbeddedViewRef,
Injectable,
InjectionToken,
Injector,
OnDestroy,
TemplateRef,
inject,
} from '@angular/core';
import {SimpleSnackBar, TextOnlySnackBar} from './simple-snack-bar';
import {MatSnackBarContainer} from './snack-bar-container';
import {MAT_SNACK_BAR_DATA, MatSnackBarConfig} from './snack-bar-config';
import {MatSnackBarRef} from './snack-bar-ref';
import {ComponentPortal, TemplatePortal} from '@angular/cdk/portal';
import {takeUntil} from 'rxjs/operators';
/** @docs-private */
export function MAT_SNACK_BAR_DEFAULT_OPTIONS_FACTORY(): MatSnackBarConfig {
return new MatSnackBarConfig();
}
/** Injection token that can be used to specify default snack bar. */
export const MAT_SNACK_BAR_DEFAULT_OPTIONS = new InjectionToken<MatSnackBarConfig>(
'mat-snack-bar-default-options',
{
providedIn: 'root',
factory: MAT_SNACK_BAR_DEFAULT_OPTIONS_FACTORY,
},
);
/**
* Service to dispatch Material Design snack bar messages.
*/
@Injectable({providedIn: 'root'})
export class MatSnackBar implements OnDestroy {
private _overlay = inject(Overlay);
private _live = inject(LiveAnnouncer);
private _injector = inject(Injector);
private _breakpointObserver = inject(BreakpointObserver);
private _parentSnackBar = inject(MatSnackBar, {optional: true, skipSelf: true});
private _defaultConfig = inject<MatSnackBarConfig>(MAT_SNACK_BAR_DEFAULT_OPTIONS);
/**
* Reference to the current snack bar in the view *at this level* (in the Angular injector tree).
* If there is a parent snack-bar service, all operations should delegate to that parent
* via `_openedSnackBarRef`.
*/
private _snackBarRefAtThisLevel: MatSnackBarRef<any> | null = null;
/** The component that should be rendered as the snack bar's simple component. */
simpleSnackBarComponent = SimpleSnackBar;
/** The container component that attaches the provided template or component. */
snackBarContainerComponent = MatSnackBarContainer;
/** The CSS class to apply for handset mode. */
handsetCssClass = 'mat-mdc-snack-bar-handset';
/** Reference to the currently opened snackbar at *any* level. */
get _openedSnackBarRef(): MatSnackBarRef<any> | null {
const parent = this._parentSnackBar;
return parent ? parent._openedSnackBarRef : this._snackBarRefAtThisLevel;
}
set _openedSnackBarRef(value: MatSnackBarRef<any> | null) {
if (this._parentSnackBar) {
this._parentSnackBar._openedSnackBarRef = value;
} else {
this._snackBarRefAtThisLevel = value;
}
}
constructor(...args: unknown[]);
constructor() {}
/**
* Creates and dispatches a snack bar with a custom component for the content, removing any
* currently opened snack bars.
*
* @param component Component to be instantiated.
* @param config Extra configuration for the snack bar.
*/
openFromComponent<T, D = any>(
component: ComponentType<T>,
config?: MatSnackBarConfig<D>,
): MatSnackBarRef<T> {
return this._attach(component, config) as MatSnackBarRef<T>;
}
/**
* Creates and dispatches a snack bar with a custom template for the content, removing any
* currently opened snack bars.
*
* @param template Template to be instantiated.
* @param config Extra configuration for the snack bar.
*/
openFromTemplate(
template: TemplateRef<any>,
config?: MatSnackBarConfig,
): MatSnackBarRef<EmbeddedViewRef<any>> {
return this._attach(template, config);
}
/**
* Opens a snackbar with a message and an optional action.
* @param message The message to show in the snackbar.
* @param action The label for the snackbar action.
* @param config Additional configuration options for the snackbar.
*/
open(
message: string,
action: string = '',
config?: MatSnackBarConfig,
): MatSnackBarRef<TextOnlySnackBar> {
const _config = {...this._defaultConfig, ...config};
// Since the user doesn't have access to the component, we can
// override the data to pass in our own message and action.
_config.data = {message, action};
// Since the snack bar has `role="alert"`, we don't
// want to announce the same message twice.
if (_config.announcementMessage === message) {
_config.announcementMessage = undefined;
}
return this.openFromComponent(this.simpleSnackBarComponent, _config);
}
/**
* Dismisses the currently-visible snack bar.
*/
dismiss(): void {
if (this._openedSnackBarRef) {
this._openedSnackBarRef.dismiss();
}
}
ngOnDestroy() {
// Only dismiss the snack bar at the current level on destroy.
if (this._snackBarRefAtThisLevel) {
this._snackBarRefAtThisLevel.dismiss();
}
}
/**
* Attaches the snack bar container component to the overlay.
*/
private _attachSnackBarContainer(
overlayRef: OverlayRef,
config: MatSnackBarConfig,
): MatSnackBarContainer {
const userInjector = config && config.viewContainerRef && config.viewContainerRef.injector;
const injector = Injector.create({
parent: userInjector || this._injector,
providers: [{provide: MatSnackBarConfig, useValue: config}],
});
const containerPortal = new ComponentPortal(
this.snackBarContainerComponent,
config.viewContainerRef,
injector,
);
const containerRef: ComponentRef<MatSnackBarContainer> = overlayRef.attach(containerPortal);
containerRef.instance.snackBarConfig = config;
return containerRef.instance;
}
/**
* Places a new component or a template as the content of the snack bar container.
*/
private _attach<T>(
content: ComponentType<T> | TemplateRef<T>,
userConfig?: MatSnackBarConfig,
): MatSnackBarRef<T | EmbeddedViewRef<any>> {
const config = {...new MatSnackBarConfig(), ...this._defaultConfig, ...userConfig};
const overlayRef = this._createOverlay(config);
const container = this._attachSnackBarContainer(overlayRef, config);
const snackBarRef = new MatSnackBarRef<T | EmbeddedViewRef<any>>(container, overlayRef);
if (content instanceof TemplateRef) {
const portal = new TemplatePortal(content, null!, {
$implicit: config.data,
snackBarRef,
} as any);
snackBarRef.instance = container.attachTemplatePortal(portal);
} else {
const injector = this._createInjector(config, snackBarRef);
const portal = new ComponentPortal(content, undefined, injector);
const contentRef = container.attachComponentPortal<T>(portal);
// We can't pass this via the injector, because the injector is created earlier.
snackBarRef.instance = contentRef.instance;
}
// Subscribe to the breakpoint observer and attach the mat-snack-bar-handset class as
// appropriate. This class is applied to the overlay element because the overlay must expand to
// fill the width of the screen for full width snackbars.
this._breakpointObserver
.observe(Breakpoints.HandsetPortrait)
.pipe(takeUntil(overlayRef.detachments()))
.subscribe(state => {
overlayRef.overlayElement.classList.toggle(this.handsetCssClass, state.matches);
});
if (config.announcementMessage) {
// Wait until the snack bar contents have been announced then deliver this message.
container._onAnnounce.subscribe(() => {
this._live.announce(config.announcementMessage!, config.politeness);
});
}
this._animateSnackBar(snackBarRef, config);
this._openedSnackBarRef = snackBarRef;
return this._openedSnackBarRef;
}
/** Animates the old snack bar out and the new one in. */
private _animateSnackBar(snackBarRef: MatSnackBarRef<any>, config: MatSnackBarConfig) {
// When the snackbar is dismissed, clear the reference to it.
snackBarRef.afterDismissed().subscribe(() => {
// Clear the snackbar ref if it hasn't already been replaced by a newer snackbar.
if (this._openedSnackBarRef == snackBarRef) {
this._openedSnackBarRef = null;
}
if (config.announcementMessage) {
this._live.clear();
}
});
if (this._openedSnackBarRef) {
// If a snack bar is already in view, dismiss it and enter the
// new snack bar after exit animation is complete.
this._openedSnackBarRef.afterDismissed().subscribe(() => {
snackBarRef.containerInstance.enter();
});
this._openedSnackBarRef.dismiss();
} else {
// If no snack bar is in view, enter the new snack bar.
snackBarRef.containerInstance.enter();
}
// If a dismiss timeout is provided, set up dismiss based on after the snackbar is opened.
if (config.duration && config.duration > 0) {
snackBarRef.afterOpened().subscribe(() => snackBarRef._dismissAfter(config.duration!));
}
}
/**
* Creates a new overlay and places it in the correct location.
* @param config The user-specified snack bar config.
*/
private _createOverlay(config: MatSnackBarConfig): OverlayRef {
const overlayConfig = new OverlayConfig();
overlayConfig.direction = config.direction;
let positionStrategy = this._overlay.position().global();
// Set horizontal position.
const isRtl = config.direction === 'rtl';
const isLeft =
config.horizontalPosition === 'left' ||
(config.horizontalPosition === 'start' && !isRtl) ||
(config.horizontalPosition === 'end' && isRtl);
const isRight = !isLeft && config.horizontalPosition !== 'center';
if (isLeft) {
positionStrategy.left('0');
} else if (isRight) {
positionStrategy.right('0');
} else {
positionStrategy.centerHorizontally();
}
// Set horizontal position.
if (config.verticalPosition === 'top') {
positionStrategy.top('0');
} else {
positionStrategy.bottom('0');
}
overlayConfig.positionStrategy = positionStrategy;
return this._overlay.create(overlayConfig);
}
/**
* Creates an injector to be used inside of a snack bar component.
* @param config Config that was used to create the snack bar.
* @param snackBarRef Reference to the snack bar.
*/
private _createInjector<T>(config: MatSnackBarConfig, snackBarRef: MatSnackBarRef<T>): Injector {
const userInjector = config && config.viewContainerRef && config.viewContainerRef.injector;
return Injector.create({
parent: userInjector || this._injector,
providers: [
{provide: MatSnackBarRef, useValue: snackBarRef},
{provide: MAT_SNACK_BAR_DATA, useValue: config.data},
],
});
}
}