226 lines
6.9 KiB
TypeScript
226 lines
6.9 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 {DOCUMENT} from '@angular/common';
|
||
|
|
import {
|
||
|
|
ANIMATION_MODULE_TYPE,
|
||
|
|
Injectable,
|
||
|
|
Injector,
|
||
|
|
NgZone,
|
||
|
|
OnDestroy,
|
||
|
|
inject,
|
||
|
|
} from '@angular/core';
|
||
|
|
import {
|
||
|
|
MAT_RIPPLE_GLOBAL_OPTIONS,
|
||
|
|
RippleRenderer,
|
||
|
|
RippleTarget,
|
||
|
|
defaultRippleAnimationConfig,
|
||
|
|
} from '../ripple';
|
||
|
|
import {Platform, _getEventTarget} from '@angular/cdk/platform';
|
||
|
|
import {_CdkPrivateStyleLoader} from '@angular/cdk/private';
|
||
|
|
|
||
|
|
/** The options for the MatRippleLoader's event listeners. */
|
||
|
|
const eventListenerOptions = {capture: true};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The events that should trigger the initialization of the ripple.
|
||
|
|
* Note that we use `mousedown`, rather than `click`, for mouse devices because
|
||
|
|
* we can't rely on `mouseenter` in the shadow DOM and `click` happens too late.
|
||
|
|
*/
|
||
|
|
const rippleInteractionEvents = ['focus', 'mousedown', 'mouseenter', 'touchstart'];
|
||
|
|
|
||
|
|
/** The attribute attached to a component whose ripple has not yet been initialized. */
|
||
|
|
const matRippleUninitialized = 'mat-ripple-loader-uninitialized';
|
||
|
|
|
||
|
|
/** Additional classes that should be added to the ripple when it is rendered. */
|
||
|
|
const matRippleClassName = 'mat-ripple-loader-class-name';
|
||
|
|
|
||
|
|
/** Whether the ripple should be centered. */
|
||
|
|
const matRippleCentered = 'mat-ripple-loader-centered';
|
||
|
|
|
||
|
|
/** Whether the ripple should be disabled. */
|
||
|
|
const matRippleDisabled = 'mat-ripple-loader-disabled';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handles attaching ripples on demand.
|
||
|
|
*
|
||
|
|
* This service allows us to avoid eagerly creating & attaching MatRipples.
|
||
|
|
* It works by creating & attaching a ripple only when a component is first interacted with.
|
||
|
|
*
|
||
|
|
* @docs-private
|
||
|
|
*/
|
||
|
|
@Injectable({providedIn: 'root'})
|
||
|
|
export class MatRippleLoader implements OnDestroy {
|
||
|
|
private _document = inject(DOCUMENT, {optional: true});
|
||
|
|
private _animationMode = inject(ANIMATION_MODULE_TYPE, {optional: true});
|
||
|
|
private _globalRippleOptions = inject(MAT_RIPPLE_GLOBAL_OPTIONS, {optional: true});
|
||
|
|
private _platform = inject(Platform);
|
||
|
|
private _ngZone = inject(NgZone);
|
||
|
|
private _injector = inject(Injector);
|
||
|
|
private _hosts = new Map<
|
||
|
|
HTMLElement,
|
||
|
|
{renderer: RippleRenderer; target: RippleTarget; hasSetUpEvents: boolean}
|
||
|
|
>();
|
||
|
|
|
||
|
|
constructor() {
|
||
|
|
this._ngZone.runOutsideAngular(() => {
|
||
|
|
for (const event of rippleInteractionEvents) {
|
||
|
|
this._document?.addEventListener(event, this._onInteraction, eventListenerOptions);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
ngOnDestroy(): void {
|
||
|
|
const hosts = this._hosts.keys();
|
||
|
|
|
||
|
|
for (const host of hosts) {
|
||
|
|
this.destroyRipple(host);
|
||
|
|
}
|
||
|
|
|
||
|
|
for (const event of rippleInteractionEvents) {
|
||
|
|
this._document?.removeEventListener(event, this._onInteraction, eventListenerOptions);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Configures the ripple that will be rendered by the ripple loader.
|
||
|
|
*
|
||
|
|
* Stores the given information about how the ripple should be configured on the host
|
||
|
|
* element so that it can later be retrived & used when the ripple is actually created.
|
||
|
|
*/
|
||
|
|
configureRipple(
|
||
|
|
host: HTMLElement,
|
||
|
|
config: {
|
||
|
|
className?: string;
|
||
|
|
centered?: boolean;
|
||
|
|
disabled?: boolean;
|
||
|
|
},
|
||
|
|
): void {
|
||
|
|
// Indicates that the ripple has not yet been rendered for this component.
|
||
|
|
host.setAttribute(matRippleUninitialized, this._globalRippleOptions?.namespace ?? '');
|
||
|
|
|
||
|
|
// Store the additional class name(s) that should be added to the ripple element.
|
||
|
|
if (config.className || !host.hasAttribute(matRippleClassName)) {
|
||
|
|
host.setAttribute(matRippleClassName, config.className || '');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Store whether the ripple should be centered.
|
||
|
|
if (config.centered) {
|
||
|
|
host.setAttribute(matRippleCentered, '');
|
||
|
|
}
|
||
|
|
|
||
|
|
if (config.disabled) {
|
||
|
|
host.setAttribute(matRippleDisabled, '');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Sets the disabled state on the ripple instance corresponding to the given host element. */
|
||
|
|
setDisabled(host: HTMLElement, disabled: boolean): void {
|
||
|
|
const ripple = this._hosts.get(host);
|
||
|
|
|
||
|
|
// If the ripple has already been instantiated, just disable it.
|
||
|
|
if (ripple) {
|
||
|
|
ripple.target.rippleDisabled = disabled;
|
||
|
|
|
||
|
|
if (!disabled && !ripple.hasSetUpEvents) {
|
||
|
|
ripple.hasSetUpEvents = true;
|
||
|
|
ripple.renderer.setupTriggerEvents(host);
|
||
|
|
}
|
||
|
|
} else if (disabled) {
|
||
|
|
// Otherwise, set an attribute so we know what the
|
||
|
|
// disabled state should be when the ripple is initialized.
|
||
|
|
host.setAttribute(matRippleDisabled, '');
|
||
|
|
} else {
|
||
|
|
host.removeAttribute(matRippleDisabled);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handles creating and attaching component internals
|
||
|
|
* when a component is initially interacted with.
|
||
|
|
*/
|
||
|
|
private _onInteraction = (event: Event) => {
|
||
|
|
const eventTarget = _getEventTarget(event);
|
||
|
|
|
||
|
|
if (eventTarget instanceof HTMLElement) {
|
||
|
|
// TODO(wagnermaciel): Consider batching these events to improve runtime performance.
|
||
|
|
const element = eventTarget.closest(
|
||
|
|
`[${matRippleUninitialized}="${this._globalRippleOptions?.namespace ?? ''}"]`,
|
||
|
|
);
|
||
|
|
|
||
|
|
if (element) {
|
||
|
|
this._createRipple(element as HTMLElement);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
/** Creates a MatRipple and appends it to the given element. */
|
||
|
|
private _createRipple(host: HTMLElement): void {
|
||
|
|
if (!this._document || this._hosts.has(host)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create the ripple element.
|
||
|
|
host.querySelector('.mat-ripple')?.remove();
|
||
|
|
const rippleEl = this._document.createElement('span');
|
||
|
|
rippleEl.classList.add('mat-ripple', host.getAttribute(matRippleClassName)!);
|
||
|
|
host.append(rippleEl);
|
||
|
|
|
||
|
|
const isNoopAnimations = this._animationMode === 'NoopAnimations';
|
||
|
|
const globalOptions = this._globalRippleOptions;
|
||
|
|
const enterDuration = isNoopAnimations
|
||
|
|
? 0
|
||
|
|
: globalOptions?.animation?.enterDuration ?? defaultRippleAnimationConfig.enterDuration;
|
||
|
|
const exitDuration = isNoopAnimations
|
||
|
|
? 0
|
||
|
|
: globalOptions?.animation?.exitDuration ?? defaultRippleAnimationConfig.exitDuration;
|
||
|
|
const target: RippleTarget = {
|
||
|
|
rippleDisabled:
|
||
|
|
isNoopAnimations || globalOptions?.disabled || host.hasAttribute(matRippleDisabled),
|
||
|
|
rippleConfig: {
|
||
|
|
centered: host.hasAttribute(matRippleCentered),
|
||
|
|
terminateOnPointerUp: globalOptions?.terminateOnPointerUp,
|
||
|
|
animation: {
|
||
|
|
enterDuration,
|
||
|
|
exitDuration,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
const renderer = new RippleRenderer(
|
||
|
|
target,
|
||
|
|
this._ngZone,
|
||
|
|
rippleEl,
|
||
|
|
this._platform,
|
||
|
|
this._injector,
|
||
|
|
);
|
||
|
|
const hasSetUpEvents = !target.rippleDisabled;
|
||
|
|
|
||
|
|
if (hasSetUpEvents) {
|
||
|
|
renderer.setupTriggerEvents(host);
|
||
|
|
}
|
||
|
|
|
||
|
|
this._hosts.set(host, {
|
||
|
|
target,
|
||
|
|
renderer,
|
||
|
|
hasSetUpEvents,
|
||
|
|
});
|
||
|
|
|
||
|
|
host.removeAttribute(matRippleUninitialized);
|
||
|
|
}
|
||
|
|
|
||
|
|
destroyRipple(host: HTMLElement): void {
|
||
|
|
const ripple = this._hosts.get(host);
|
||
|
|
|
||
|
|
if (ripple) {
|
||
|
|
ripple.renderer._removeTriggerEvents();
|
||
|
|
this._hosts.delete(host);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|