ripple-BT3tzh6F.mjs 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639
  1. import { normalizePassiveListenerOptions, _getEventTarget, Platform } from '@angular/cdk/platform';
  2. import * as i0 from '@angular/core';
  3. import { Component, ChangeDetectionStrategy, ViewEncapsulation, InjectionToken, inject, ElementRef, ANIMATION_MODULE_TYPE, NgZone, Injector, Directive, Input } from '@angular/core';
  4. import { isFakeMousedownFromScreenReader, isFakeTouchstartFromScreenReader } from '@angular/cdk/a11y';
  5. import { coerceElement } from '@angular/cdk/coercion';
  6. import { _CdkPrivateStyleLoader } from '@angular/cdk/private';
  7. /** Possible states for a ripple element. */
  8. var RippleState;
  9. (function (RippleState) {
  10. RippleState[RippleState["FADING_IN"] = 0] = "FADING_IN";
  11. RippleState[RippleState["VISIBLE"] = 1] = "VISIBLE";
  12. RippleState[RippleState["FADING_OUT"] = 2] = "FADING_OUT";
  13. RippleState[RippleState["HIDDEN"] = 3] = "HIDDEN";
  14. })(RippleState || (RippleState = {}));
  15. /**
  16. * Reference to a previously launched ripple element.
  17. */
  18. class RippleRef {
  19. _renderer;
  20. element;
  21. config;
  22. _animationForciblyDisabledThroughCss;
  23. /** Current state of the ripple. */
  24. state = RippleState.HIDDEN;
  25. constructor(_renderer,
  26. /** Reference to the ripple HTML element. */
  27. element,
  28. /** Ripple configuration used for the ripple. */
  29. config,
  30. /* Whether animations are forcibly disabled for ripples through CSS. */
  31. _animationForciblyDisabledThroughCss = false) {
  32. this._renderer = _renderer;
  33. this.element = element;
  34. this.config = config;
  35. this._animationForciblyDisabledThroughCss = _animationForciblyDisabledThroughCss;
  36. }
  37. /** Fades out the ripple element. */
  38. fadeOut() {
  39. this._renderer.fadeOutRipple(this);
  40. }
  41. }
  42. /** Options used to bind a passive capturing event. */
  43. const passiveCapturingEventOptions$1 = normalizePassiveListenerOptions({
  44. passive: true,
  45. capture: true,
  46. });
  47. /** Manages events through delegation so that as few event handlers as possible are bound. */
  48. class RippleEventManager {
  49. _events = new Map();
  50. /** Adds an event handler. */
  51. addHandler(ngZone, name, element, handler) {
  52. const handlersForEvent = this._events.get(name);
  53. if (handlersForEvent) {
  54. const handlersForElement = handlersForEvent.get(element);
  55. if (handlersForElement) {
  56. handlersForElement.add(handler);
  57. }
  58. else {
  59. handlersForEvent.set(element, new Set([handler]));
  60. }
  61. }
  62. else {
  63. this._events.set(name, new Map([[element, new Set([handler])]]));
  64. ngZone.runOutsideAngular(() => {
  65. document.addEventListener(name, this._delegateEventHandler, passiveCapturingEventOptions$1);
  66. });
  67. }
  68. }
  69. /** Removes an event handler. */
  70. removeHandler(name, element, handler) {
  71. const handlersForEvent = this._events.get(name);
  72. if (!handlersForEvent) {
  73. return;
  74. }
  75. const handlersForElement = handlersForEvent.get(element);
  76. if (!handlersForElement) {
  77. return;
  78. }
  79. handlersForElement.delete(handler);
  80. if (handlersForElement.size === 0) {
  81. handlersForEvent.delete(element);
  82. }
  83. if (handlersForEvent.size === 0) {
  84. this._events.delete(name);
  85. document.removeEventListener(name, this._delegateEventHandler, passiveCapturingEventOptions$1);
  86. }
  87. }
  88. /** Event handler that is bound and which dispatches the events to the different targets. */
  89. _delegateEventHandler = (event) => {
  90. const target = _getEventTarget(event);
  91. if (target) {
  92. this._events.get(event.type)?.forEach((handlers, element) => {
  93. if (element === target || element.contains(target)) {
  94. handlers.forEach(handler => handler.handleEvent(event));
  95. }
  96. });
  97. }
  98. };
  99. }
  100. /**
  101. * Default ripple animation configuration for ripples without an explicit
  102. * animation config specified.
  103. */
  104. const defaultRippleAnimationConfig = {
  105. enterDuration: 225,
  106. exitDuration: 150,
  107. };
  108. /**
  109. * Timeout for ignoring mouse events. Mouse events will be temporary ignored after touch
  110. * events to avoid synthetic mouse events.
  111. */
  112. const ignoreMouseEventsTimeout = 800;
  113. /** Options used to bind a passive capturing event. */
  114. const passiveCapturingEventOptions = normalizePassiveListenerOptions({
  115. passive: true,
  116. capture: true,
  117. });
  118. /** Events that signal that the pointer is down. */
  119. const pointerDownEvents = ['mousedown', 'touchstart'];
  120. /** Events that signal that the pointer is up. */
  121. const pointerUpEvents = ['mouseup', 'mouseleave', 'touchend', 'touchcancel'];
  122. class _MatRippleStylesLoader {
  123. static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: _MatRippleStylesLoader, deps: [], target: i0.ɵɵFactoryTarget.Component });
  124. static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.6", type: _MatRippleStylesLoader, isStandalone: true, selector: "ng-component", host: { attributes: { "mat-ripple-style-loader": "" } }, ngImport: i0, template: '', isInline: true, styles: [".mat-ripple{overflow:hidden;position:relative}.mat-ripple:not(:empty){transform:translateZ(0)}.mat-ripple.mat-ripple-unbounded{overflow:visible}.mat-ripple-element{position:absolute;border-radius:50%;pointer-events:none;transition:opacity,transform 0ms cubic-bezier(0, 0, 0.2, 1);transform:scale3d(0, 0, 0);background-color:var(--mat-ripple-color, color-mix(in srgb, var(--mat-sys-on-surface) 10%, transparent))}@media(forced-colors: active){.mat-ripple-element{display:none}}.cdk-drag-preview .mat-ripple-element,.cdk-drag-placeholder .mat-ripple-element{display:none}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
  125. }
  126. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: _MatRippleStylesLoader, decorators: [{
  127. type: Component,
  128. args: [{ template: '', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: { 'mat-ripple-style-loader': '' }, styles: [".mat-ripple{overflow:hidden;position:relative}.mat-ripple:not(:empty){transform:translateZ(0)}.mat-ripple.mat-ripple-unbounded{overflow:visible}.mat-ripple-element{position:absolute;border-radius:50%;pointer-events:none;transition:opacity,transform 0ms cubic-bezier(0, 0, 0.2, 1);transform:scale3d(0, 0, 0);background-color:var(--mat-ripple-color, color-mix(in srgb, var(--mat-sys-on-surface) 10%, transparent))}@media(forced-colors: active){.mat-ripple-element{display:none}}.cdk-drag-preview .mat-ripple-element,.cdk-drag-placeholder .mat-ripple-element{display:none}\n"] }]
  129. }] });
  130. /**
  131. * Helper service that performs DOM manipulations. Not intended to be used outside this module.
  132. * The constructor takes a reference to the ripple directive's host element and a map of DOM
  133. * event handlers to be installed on the element that triggers ripple animations.
  134. * This will eventually become a custom renderer once Angular support exists.
  135. * @docs-private
  136. */
  137. class RippleRenderer {
  138. _target;
  139. _ngZone;
  140. _platform;
  141. /** Element where the ripples are being added to. */
  142. _containerElement;
  143. /** Element which triggers the ripple elements on mouse events. */
  144. _triggerElement;
  145. /** Whether the pointer is currently down or not. */
  146. _isPointerDown = false;
  147. /**
  148. * Map of currently active ripple references.
  149. * The ripple reference is mapped to its element event listeners.
  150. * The reason why `| null` is used is that event listeners are added only
  151. * when the condition is truthy (see the `_startFadeOutTransition` method).
  152. */
  153. _activeRipples = new Map();
  154. /** Latest non-persistent ripple that was triggered. */
  155. _mostRecentTransientRipple;
  156. /** Time in milliseconds when the last touchstart event happened. */
  157. _lastTouchStartEvent;
  158. /** Whether pointer-up event listeners have been registered. */
  159. _pointerUpEventsRegistered = false;
  160. /**
  161. * Cached dimensions of the ripple container. Set when the first
  162. * ripple is shown and cleared once no more ripples are visible.
  163. */
  164. _containerRect;
  165. static _eventManager = new RippleEventManager();
  166. constructor(_target, _ngZone, elementOrElementRef, _platform, injector) {
  167. this._target = _target;
  168. this._ngZone = _ngZone;
  169. this._platform = _platform;
  170. // Only do anything if we're on the browser.
  171. if (_platform.isBrowser) {
  172. this._containerElement = coerceElement(elementOrElementRef);
  173. }
  174. if (injector) {
  175. injector.get(_CdkPrivateStyleLoader).load(_MatRippleStylesLoader);
  176. }
  177. }
  178. /**
  179. * Fades in a ripple at the given coordinates.
  180. * @param x Coordinate within the element, along the X axis at which to start the ripple.
  181. * @param y Coordinate within the element, along the Y axis at which to start the ripple.
  182. * @param config Extra ripple options.
  183. */
  184. fadeInRipple(x, y, config = {}) {
  185. const containerRect = (this._containerRect =
  186. this._containerRect || this._containerElement.getBoundingClientRect());
  187. const animationConfig = { ...defaultRippleAnimationConfig, ...config.animation };
  188. if (config.centered) {
  189. x = containerRect.left + containerRect.width / 2;
  190. y = containerRect.top + containerRect.height / 2;
  191. }
  192. const radius = config.radius || distanceToFurthestCorner(x, y, containerRect);
  193. const offsetX = x - containerRect.left;
  194. const offsetY = y - containerRect.top;
  195. const enterDuration = animationConfig.enterDuration;
  196. const ripple = document.createElement('div');
  197. ripple.classList.add('mat-ripple-element');
  198. ripple.style.left = `${offsetX - radius}px`;
  199. ripple.style.top = `${offsetY - radius}px`;
  200. ripple.style.height = `${radius * 2}px`;
  201. ripple.style.width = `${radius * 2}px`;
  202. // If a custom color has been specified, set it as inline style. If no color is
  203. // set, the default color will be applied through the ripple theme styles.
  204. if (config.color != null) {
  205. ripple.style.backgroundColor = config.color;
  206. }
  207. ripple.style.transitionDuration = `${enterDuration}ms`;
  208. this._containerElement.appendChild(ripple);
  209. // By default the browser does not recalculate the styles of dynamically created
  210. // ripple elements. This is critical to ensure that the `scale` animates properly.
  211. // We enforce a style recalculation by calling `getComputedStyle` and *accessing* a property.
  212. // See: https://gist.github.com/paulirish/5d52fb081b3570c81e3a
  213. const computedStyles = window.getComputedStyle(ripple);
  214. const userTransitionProperty = computedStyles.transitionProperty;
  215. const userTransitionDuration = computedStyles.transitionDuration;
  216. // Note: We detect whether animation is forcibly disabled through CSS (e.g. through
  217. // `transition: none` or `display: none`). This is technically unexpected since animations are
  218. // controlled through the animation config, but this exists for backwards compatibility. This
  219. // logic does not need to be super accurate since it covers some edge cases which can be easily
  220. // avoided by users.
  221. const animationForciblyDisabledThroughCss = userTransitionProperty === 'none' ||
  222. // Note: The canonical unit for serialized CSS `<time>` properties is seconds. Additionally
  223. // some browsers expand the duration for every property (in our case `opacity` and `transform`).
  224. userTransitionDuration === '0s' ||
  225. userTransitionDuration === '0s, 0s' ||
  226. // If the container is 0x0, it's likely `display: none`.
  227. (containerRect.width === 0 && containerRect.height === 0);
  228. // Exposed reference to the ripple that will be returned.
  229. const rippleRef = new RippleRef(this, ripple, config, animationForciblyDisabledThroughCss);
  230. // Start the enter animation by setting the transform/scale to 100%. The animation will
  231. // execute as part of this statement because we forced a style recalculation before.
  232. // Note: We use a 3d transform here in order to avoid an issue in Safari where
  233. // the ripples aren't clipped when inside the shadow DOM (see #24028).
  234. ripple.style.transform = 'scale3d(1, 1, 1)';
  235. rippleRef.state = RippleState.FADING_IN;
  236. if (!config.persistent) {
  237. this._mostRecentTransientRipple = rippleRef;
  238. }
  239. let eventListeners = null;
  240. // Do not register the `transition` event listener if fade-in and fade-out duration
  241. // are set to zero. The events won't fire anyway and we can save resources here.
  242. if (!animationForciblyDisabledThroughCss && (enterDuration || animationConfig.exitDuration)) {
  243. this._ngZone.runOutsideAngular(() => {
  244. const onTransitionEnd = () => {
  245. // Clear the fallback timer since the transition fired correctly.
  246. if (eventListeners) {
  247. eventListeners.fallbackTimer = null;
  248. }
  249. clearTimeout(fallbackTimer);
  250. this._finishRippleTransition(rippleRef);
  251. };
  252. const onTransitionCancel = () => this._destroyRipple(rippleRef);
  253. // In some cases where there's a higher load on the browser, it can choose not to dispatch
  254. // neither `transitionend` nor `transitioncancel` (see b/227356674). This timer serves as a
  255. // fallback for such cases so that the ripple doesn't become stuck. We add a 100ms buffer
  256. // because timers aren't precise. Note that another approach can be to transition the ripple
  257. // to the `VISIBLE` state immediately above and to `FADING_IN` afterwards inside
  258. // `transitionstart`. We go with the timer because it's one less event listener and
  259. // it's less likely to break existing tests.
  260. const fallbackTimer = setTimeout(onTransitionCancel, enterDuration + 100);
  261. ripple.addEventListener('transitionend', onTransitionEnd);
  262. // If the transition is cancelled (e.g. due to DOM removal), we destroy the ripple
  263. // directly as otherwise we would keep it part of the ripple container forever.
  264. // https://www.w3.org/TR/css-transitions-1/#:~:text=no%20longer%20in%20the%20document.
  265. ripple.addEventListener('transitioncancel', onTransitionCancel);
  266. eventListeners = { onTransitionEnd, onTransitionCancel, fallbackTimer };
  267. });
  268. }
  269. // Add the ripple reference to the list of all active ripples.
  270. this._activeRipples.set(rippleRef, eventListeners);
  271. // In case there is no fade-in transition duration, we need to manually call the transition
  272. // end listener because `transitionend` doesn't fire if there is no transition.
  273. if (animationForciblyDisabledThroughCss || !enterDuration) {
  274. this._finishRippleTransition(rippleRef);
  275. }
  276. return rippleRef;
  277. }
  278. /** Fades out a ripple reference. */
  279. fadeOutRipple(rippleRef) {
  280. // For ripples already fading out or hidden, this should be a noop.
  281. if (rippleRef.state === RippleState.FADING_OUT || rippleRef.state === RippleState.HIDDEN) {
  282. return;
  283. }
  284. const rippleEl = rippleRef.element;
  285. const animationConfig = { ...defaultRippleAnimationConfig, ...rippleRef.config.animation };
  286. // This starts the fade-out transition and will fire the transition end listener that
  287. // removes the ripple element from the DOM.
  288. rippleEl.style.transitionDuration = `${animationConfig.exitDuration}ms`;
  289. rippleEl.style.opacity = '0';
  290. rippleRef.state = RippleState.FADING_OUT;
  291. // In case there is no fade-out transition duration, we need to manually call the
  292. // transition end listener because `transitionend` doesn't fire if there is no transition.
  293. if (rippleRef._animationForciblyDisabledThroughCss || !animationConfig.exitDuration) {
  294. this._finishRippleTransition(rippleRef);
  295. }
  296. }
  297. /** Fades out all currently active ripples. */
  298. fadeOutAll() {
  299. this._getActiveRipples().forEach(ripple => ripple.fadeOut());
  300. }
  301. /** Fades out all currently active non-persistent ripples. */
  302. fadeOutAllNonPersistent() {
  303. this._getActiveRipples().forEach(ripple => {
  304. if (!ripple.config.persistent) {
  305. ripple.fadeOut();
  306. }
  307. });
  308. }
  309. /** Sets up the trigger event listeners */
  310. setupTriggerEvents(elementOrElementRef) {
  311. const element = coerceElement(elementOrElementRef);
  312. if (!this._platform.isBrowser || !element || element === this._triggerElement) {
  313. return;
  314. }
  315. // Remove all previously registered event listeners from the trigger element.
  316. this._removeTriggerEvents();
  317. this._triggerElement = element;
  318. // Use event delegation for the trigger events since they're
  319. // set up during creation and are performance-sensitive.
  320. pointerDownEvents.forEach(type => {
  321. RippleRenderer._eventManager.addHandler(this._ngZone, type, element, this);
  322. });
  323. }
  324. /**
  325. * Handles all registered events.
  326. * @docs-private
  327. */
  328. handleEvent(event) {
  329. if (event.type === 'mousedown') {
  330. this._onMousedown(event);
  331. }
  332. else if (event.type === 'touchstart') {
  333. this._onTouchStart(event);
  334. }
  335. else {
  336. this._onPointerUp();
  337. }
  338. // If pointer-up events haven't been registered yet, do so now.
  339. // We do this on-demand in order to reduce the total number of event listeners
  340. // registered by the ripples, which speeds up the rendering time for large UIs.
  341. if (!this._pointerUpEventsRegistered) {
  342. // The events for hiding the ripple are bound directly on the trigger, because:
  343. // 1. Some of them occur frequently (e.g. `mouseleave`) and any advantage we get from
  344. // delegation will be diminished by having to look through all the data structures often.
  345. // 2. They aren't as performance-sensitive, because they're bound only after the user
  346. // has interacted with an element.
  347. this._ngZone.runOutsideAngular(() => {
  348. pointerUpEvents.forEach(type => {
  349. this._triggerElement.addEventListener(type, this, passiveCapturingEventOptions);
  350. });
  351. });
  352. this._pointerUpEventsRegistered = true;
  353. }
  354. }
  355. /** Method that will be called if the fade-in or fade-in transition completed. */
  356. _finishRippleTransition(rippleRef) {
  357. if (rippleRef.state === RippleState.FADING_IN) {
  358. this._startFadeOutTransition(rippleRef);
  359. }
  360. else if (rippleRef.state === RippleState.FADING_OUT) {
  361. this._destroyRipple(rippleRef);
  362. }
  363. }
  364. /**
  365. * Starts the fade-out transition of the given ripple if it's not persistent and the pointer
  366. * is not held down anymore.
  367. */
  368. _startFadeOutTransition(rippleRef) {
  369. const isMostRecentTransientRipple = rippleRef === this._mostRecentTransientRipple;
  370. const { persistent } = rippleRef.config;
  371. rippleRef.state = RippleState.VISIBLE;
  372. // When the timer runs out while the user has kept their pointer down, we want to
  373. // keep only the persistent ripples and the latest transient ripple. We do this,
  374. // because we don't want stacked transient ripples to appear after their enter
  375. // animation has finished.
  376. if (!persistent && (!isMostRecentTransientRipple || !this._isPointerDown)) {
  377. rippleRef.fadeOut();
  378. }
  379. }
  380. /** Destroys the given ripple by removing it from the DOM and updating its state. */
  381. _destroyRipple(rippleRef) {
  382. const eventListeners = this._activeRipples.get(rippleRef) ?? null;
  383. this._activeRipples.delete(rippleRef);
  384. // Clear out the cached bounding rect if we have no more ripples.
  385. if (!this._activeRipples.size) {
  386. this._containerRect = null;
  387. }
  388. // If the current ref is the most recent transient ripple, unset it
  389. // avoid memory leaks.
  390. if (rippleRef === this._mostRecentTransientRipple) {
  391. this._mostRecentTransientRipple = null;
  392. }
  393. rippleRef.state = RippleState.HIDDEN;
  394. if (eventListeners !== null) {
  395. rippleRef.element.removeEventListener('transitionend', eventListeners.onTransitionEnd);
  396. rippleRef.element.removeEventListener('transitioncancel', eventListeners.onTransitionCancel);
  397. if (eventListeners.fallbackTimer !== null) {
  398. clearTimeout(eventListeners.fallbackTimer);
  399. }
  400. }
  401. rippleRef.element.remove();
  402. }
  403. /** Function being called whenever the trigger is being pressed using mouse. */
  404. _onMousedown(event) {
  405. // Screen readers will fire fake mouse events for space/enter. Skip launching a
  406. // ripple in this case for consistency with the non-screen-reader experience.
  407. const isFakeMousedown = isFakeMousedownFromScreenReader(event);
  408. const isSyntheticEvent = this._lastTouchStartEvent &&
  409. Date.now() < this._lastTouchStartEvent + ignoreMouseEventsTimeout;
  410. if (!this._target.rippleDisabled && !isFakeMousedown && !isSyntheticEvent) {
  411. this._isPointerDown = true;
  412. this.fadeInRipple(event.clientX, event.clientY, this._target.rippleConfig);
  413. }
  414. }
  415. /** Function being called whenever the trigger is being pressed using touch. */
  416. _onTouchStart(event) {
  417. if (!this._target.rippleDisabled && !isFakeTouchstartFromScreenReader(event)) {
  418. // Some browsers fire mouse events after a `touchstart` event. Those synthetic mouse
  419. // events will launch a second ripple if we don't ignore mouse events for a specific
  420. // time after a touchstart event.
  421. this._lastTouchStartEvent = Date.now();
  422. this._isPointerDown = true;
  423. // Use `changedTouches` so we skip any touches where the user put
  424. // their finger down, but used another finger to tap the element again.
  425. const touches = event.changedTouches;
  426. // According to the typings the touches should always be defined, but in some cases
  427. // the browser appears to not assign them in tests which leads to flakes.
  428. if (touches) {
  429. for (let i = 0; i < touches.length; i++) {
  430. this.fadeInRipple(touches[i].clientX, touches[i].clientY, this._target.rippleConfig);
  431. }
  432. }
  433. }
  434. }
  435. /** Function being called whenever the trigger is being released. */
  436. _onPointerUp() {
  437. if (!this._isPointerDown) {
  438. return;
  439. }
  440. this._isPointerDown = false;
  441. // Fade-out all ripples that are visible and not persistent.
  442. this._getActiveRipples().forEach(ripple => {
  443. // By default, only ripples that are completely visible will fade out on pointer release.
  444. // If the `terminateOnPointerUp` option is set, ripples that still fade in will also fade out.
  445. const isVisible = ripple.state === RippleState.VISIBLE ||
  446. (ripple.config.terminateOnPointerUp && ripple.state === RippleState.FADING_IN);
  447. if (!ripple.config.persistent && isVisible) {
  448. ripple.fadeOut();
  449. }
  450. });
  451. }
  452. _getActiveRipples() {
  453. return Array.from(this._activeRipples.keys());
  454. }
  455. /** Removes previously registered event listeners from the trigger element. */
  456. _removeTriggerEvents() {
  457. const trigger = this._triggerElement;
  458. if (trigger) {
  459. pointerDownEvents.forEach(type => RippleRenderer._eventManager.removeHandler(type, trigger, this));
  460. if (this._pointerUpEventsRegistered) {
  461. pointerUpEvents.forEach(type => trigger.removeEventListener(type, this, passiveCapturingEventOptions));
  462. this._pointerUpEventsRegistered = false;
  463. }
  464. }
  465. }
  466. }
  467. /**
  468. * Returns the distance from the point (x, y) to the furthest corner of a rectangle.
  469. */
  470. function distanceToFurthestCorner(x, y, rect) {
  471. const distX = Math.max(Math.abs(x - rect.left), Math.abs(x - rect.right));
  472. const distY = Math.max(Math.abs(y - rect.top), Math.abs(y - rect.bottom));
  473. return Math.sqrt(distX * distX + distY * distY);
  474. }
  475. /** Injection token that can be used to specify the global ripple options. */
  476. const MAT_RIPPLE_GLOBAL_OPTIONS = new InjectionToken('mat-ripple-global-options');
  477. class MatRipple {
  478. _elementRef = inject(ElementRef);
  479. _animationMode = inject(ANIMATION_MODULE_TYPE, { optional: true });
  480. /** Custom color for all ripples. */
  481. color;
  482. /** Whether the ripples should be visible outside the component's bounds. */
  483. unbounded;
  484. /**
  485. * Whether the ripple always originates from the center of the host element's bounds, rather
  486. * than originating from the location of the click event.
  487. */
  488. centered;
  489. /**
  490. * If set, the radius in pixels of foreground ripples when fully expanded. If unset, the radius
  491. * will be the distance from the center of the ripple to the furthest corner of the host element's
  492. * bounding rectangle.
  493. */
  494. radius = 0;
  495. /**
  496. * Configuration for the ripple animation. Allows modifying the enter and exit animation
  497. * duration of the ripples. The animation durations will be overwritten if the
  498. * `NoopAnimationsModule` is being used.
  499. */
  500. animation;
  501. /**
  502. * Whether click events will not trigger the ripple. Ripples can be still launched manually
  503. * by using the `launch()` method.
  504. */
  505. get disabled() {
  506. return this._disabled;
  507. }
  508. set disabled(value) {
  509. if (value) {
  510. this.fadeOutAllNonPersistent();
  511. }
  512. this._disabled = value;
  513. this._setupTriggerEventsIfEnabled();
  514. }
  515. _disabled = false;
  516. /**
  517. * The element that triggers the ripple when click events are received.
  518. * Defaults to the directive's host element.
  519. */
  520. get trigger() {
  521. return this._trigger || this._elementRef.nativeElement;
  522. }
  523. set trigger(trigger) {
  524. this._trigger = trigger;
  525. this._setupTriggerEventsIfEnabled();
  526. }
  527. _trigger;
  528. /** Renderer for the ripple DOM manipulations. */
  529. _rippleRenderer;
  530. /** Options that are set globally for all ripples. */
  531. _globalOptions;
  532. /** @docs-private Whether ripple directive is initialized and the input bindings are set. */
  533. _isInitialized = false;
  534. constructor() {
  535. const ngZone = inject(NgZone);
  536. const platform = inject(Platform);
  537. const globalOptions = inject(MAT_RIPPLE_GLOBAL_OPTIONS, { optional: true });
  538. const injector = inject(Injector);
  539. // Note: cannot use `inject()` here, because this class
  540. // gets instantiated manually in the ripple loader.
  541. this._globalOptions = globalOptions || {};
  542. this._rippleRenderer = new RippleRenderer(this, ngZone, this._elementRef, platform, injector);
  543. }
  544. ngOnInit() {
  545. this._isInitialized = true;
  546. this._setupTriggerEventsIfEnabled();
  547. }
  548. ngOnDestroy() {
  549. this._rippleRenderer._removeTriggerEvents();
  550. }
  551. /** Fades out all currently showing ripple elements. */
  552. fadeOutAll() {
  553. this._rippleRenderer.fadeOutAll();
  554. }
  555. /** Fades out all currently showing non-persistent ripple elements. */
  556. fadeOutAllNonPersistent() {
  557. this._rippleRenderer.fadeOutAllNonPersistent();
  558. }
  559. /**
  560. * Ripple configuration from the directive's input values.
  561. * @docs-private Implemented as part of RippleTarget
  562. */
  563. get rippleConfig() {
  564. return {
  565. centered: this.centered,
  566. radius: this.radius,
  567. color: this.color,
  568. animation: {
  569. ...this._globalOptions.animation,
  570. ...(this._animationMode === 'NoopAnimations' ? { enterDuration: 0, exitDuration: 0 } : {}),
  571. ...this.animation,
  572. },
  573. terminateOnPointerUp: this._globalOptions.terminateOnPointerUp,
  574. };
  575. }
  576. /**
  577. * Whether ripples on pointer-down are disabled or not.
  578. * @docs-private Implemented as part of RippleTarget
  579. */
  580. get rippleDisabled() {
  581. return this.disabled || !!this._globalOptions.disabled;
  582. }
  583. /** Sets up the trigger event listeners if ripples are enabled. */
  584. _setupTriggerEventsIfEnabled() {
  585. if (!this.disabled && this._isInitialized) {
  586. this._rippleRenderer.setupTriggerEvents(this.trigger);
  587. }
  588. }
  589. /** Launches a manual ripple at the specified coordinated or just by the ripple config. */
  590. launch(configOrX, y = 0, config) {
  591. if (typeof configOrX === 'number') {
  592. return this._rippleRenderer.fadeInRipple(configOrX, y, { ...this.rippleConfig, ...config });
  593. }
  594. else {
  595. return this._rippleRenderer.fadeInRipple(0, 0, { ...this.rippleConfig, ...configOrX });
  596. }
  597. }
  598. static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: MatRipple, deps: [], target: i0.ɵɵFactoryTarget.Directive });
  599. static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.6", type: MatRipple, isStandalone: true, selector: "[mat-ripple], [matRipple]", inputs: { color: ["matRippleColor", "color"], unbounded: ["matRippleUnbounded", "unbounded"], centered: ["matRippleCentered", "centered"], radius: ["matRippleRadius", "radius"], animation: ["matRippleAnimation", "animation"], disabled: ["matRippleDisabled", "disabled"], trigger: ["matRippleTrigger", "trigger"] }, host: { properties: { "class.mat-ripple-unbounded": "unbounded" }, classAttribute: "mat-ripple" }, exportAs: ["matRipple"], ngImport: i0 });
  600. }
  601. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: MatRipple, decorators: [{
  602. type: Directive,
  603. args: [{
  604. selector: '[mat-ripple], [matRipple]',
  605. exportAs: 'matRipple',
  606. host: {
  607. 'class': 'mat-ripple',
  608. '[class.mat-ripple-unbounded]': 'unbounded',
  609. },
  610. }]
  611. }], ctorParameters: () => [], propDecorators: { color: [{
  612. type: Input,
  613. args: ['matRippleColor']
  614. }], unbounded: [{
  615. type: Input,
  616. args: ['matRippleUnbounded']
  617. }], centered: [{
  618. type: Input,
  619. args: ['matRippleCentered']
  620. }], radius: [{
  621. type: Input,
  622. args: ['matRippleRadius']
  623. }], animation: [{
  624. type: Input,
  625. args: ['matRippleAnimation']
  626. }], disabled: [{
  627. type: Input,
  628. args: ['matRippleDisabled']
  629. }], trigger: [{
  630. type: Input,
  631. args: ['matRippleTrigger']
  632. }] } });
  633. export { MatRipple as M, RippleRenderer as R, MAT_RIPPLE_GLOBAL_OPTIONS as a, RippleState as b, RippleRef as c, defaultRippleAnimationConfig as d };
  634. //# sourceMappingURL=ripple-BT3tzh6F.mjs.map