UNPKG

mat-ripple

Version:
664 lines (648 loc) 28.2 kB
'use strict'; /*! ***************************************************************************** Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. ***************************************************************************** */ /* global Reflect, Promise */ var extendStatics = function(d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return extendStatics(d, b); }; function __extends(d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); } var __assign = function() { __assign = Object.assign || function __assign(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; /** * This shim allows elements written in, or compiled to, ES5 to work on native * implementations of Custom Elements v1. It sets new.target to the value of * `this.constructor` so that the native HTMLElement constructor can access the * current under-construction element's definition. */ var NATIVE_SHIM = (function () { var _window = window; if ( // No Reflect, no classes, no need for shim because native custom elements // require ES2015 classes or Reflect. _window.Reflect === undefined || _window.customElements === undefined) { return; } var BuiltInHTMLElement = HTMLElement; /** * With jscompiler's RECOMMENDED_FLAGS the function name will be optimized away. * However, if we declare the function as a property on an object literal, and * use quotes for the property name, then closure will leave that much intact, * which is enough for the JS VM to correctly set Function.prototype.name. */ var wrapperForTheName = { HTMLElement: /** @this {!Object} */ function HTMLElement() { return Reflect.construct(BuiltInHTMLElement, [], /** @type {!Function} */ (this.constructor)); } }; _window.HTMLElement = wrapperForTheName['HTMLElement']; HTMLElement.prototype = BuiltInHTMLElement.prototype; HTMLElement.prototype.constructor = HTMLElement; Object.setPrototypeOf(HTMLElement, BuiltInHTMLElement); })(); /** Possible states for a ripple element. */ var RippleState; (function (RippleState) { /** Ripple is still fading in */ RippleState[RippleState["FADING_IN"] = 0] = "FADING_IN"; /** Ripple faded in and completely visible */ RippleState[RippleState["VISIBLE"] = 1] = "VISIBLE"; /** Ripple is fading out */ RippleState[RippleState["FADING_OUT"] = 2] = "FADING_OUT"; /** Ripple faded out and completely hidden */ RippleState[RippleState["HIDDEN"] = 3] = "HIDDEN"; })(RippleState || (RippleState = {})); /** * Reference to a previously launched ripple element. */ var RippleRef = /** @class */ (function () { function RippleRef(_renderer, /** Reference to the ripple HTML element. */ element, /** Ripple configuration used for the ripple. */ config) { this._renderer = _renderer; this.element = element; this.config = config; /** Current state of the ripple. */ this.state = RippleState.HIDDEN; } /** Fades out the ripple element. */ RippleRef.prototype.fadeOut = function () { this._renderer.fadeOutRipple(this); }; return RippleRef; }()); /** Cached result of whether the user's browser supports passive event listeners. */ var supportsPassiveEvents; /** * Checks whether the user's browser supports passive event listeners. * See: https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md */ function supportsPassiveEventListeners() { if (supportsPassiveEvents == null && typeof window !== 'undefined') { try { window.addEventListener('test', null, Object.defineProperty({}, 'passive', { get: function () { return (supportsPassiveEvents = true); } })); } finally { supportsPassiveEvents = supportsPassiveEvents || false; } } return supportsPassiveEvents; } /** * Normalizes an `AddEventListener` object to something that can be passed * to `addEventListener` on any browser, no matter whether it supports the * `options` parameter. * @param options Object to be normalized. */ function normalizePassiveListenerOptions(options) { return supportsPassiveEventListeners() ? options : !!options.capture; } /** * Screen readers will often fire fake mousedown events when a focusable element * is activated using the keyboard. We can typically distinguish between these faked * mousedown events and real mousedown events using the "buttons" property. While * real mousedown will indicate the mouse button that was pressed (e.g. `1` for * the left mouse button), faked mousedown will usually set the property value to 0. */ function isFakeMousedownFromScreenReader(event) { return event.buttons === 0; } /** Return style property of a DOM element. */ function getStyle(element, styleProperty) { return window .getComputedStyle(element) .getPropertyValue(styleProperty || 'opacity'); } /** Enforces a style recalculation of a DOM element by computing its styles. */ function enforceStyleRecalculation(element) { /** * Enforce a style recalculation by calling `getComputedStyle` and accessing any property. * Calling `getPropertyValue` is important to let optimizer know that this is not a noop. * See: `https://gist.github.com/paulirish/5d52fb081b3570c81e3a` */ getStyle(element); } /** * Returns the distance from the point (x, y) to the furthest corner of a rectangle. */ function distanceToFurthestCorner(x, y, rect) { var distX = Math.max(Math.abs(x - rect.left), Math.abs(x - rect.right)); var distY = Math.max(Math.abs(y - rect.top), Math.abs(y - rect.bottom)); return Math.sqrt(distX * distX + distY * distY); } /** * Default ripple animation configuration for ripples without an explicit * animation config specified. */ var defaultRippleAnimationConfig = { enterDuration: 450, exitDuration: 400 }; /** * Timeout for ignoring mouse events. Mouse events will be temporary ignored after touch * events to avoid synthetic mouse events. */ var ignoreMouseEventsTimeout = 800; /** Options that apply to all the event listeners that are bound by the ripple renderer. */ var passiveEventOptions = normalizePassiveListenerOptions({ passive: true }); /** * Helper service that performs DOM manipulations. Not intended to be used outside this module. * The constructor takes a reference to the ripple host element and a map of DOM * event handlers to be installed on the element that triggers ripple animations. */ var RippleRenderer = /** @class */ (function () { function RippleRenderer(_target, elementRef, pathElement) { this._target = _target; /** Whether the pointer is currently down or not. */ this._isPointerDown = false; /** Events to be registered on the trigger element. */ this._triggerEvents = new Map(); /** Set of currently active ripple references. */ this._activeRipples = new Set(); this._containerElement = elementRef; this._pathElement = pathElement; // Specify events which need to be registered on the trigger. this._triggerEvents .set('mousedown', this._onMousedown.bind(this)) .set('mouseup', this._onPointerUp.bind(this)) .set('mouseleave', this._onPointerUp.bind(this)) .set('touchstart', this._onTouchStart.bind(this)) .set('touchend', this._onPointerUp.bind(this)) .set('touchcancel', this._onPointerUp.bind(this)); } /** * Fades in a ripple at the given coordinates. * @param x Coordinate within the element, along the X axis at which to start the ripple. * @param y Coordinate within the element, along the Y axis at which to start the ripple. * @param config Extra ripple options. */ RippleRenderer.prototype.fadeInRipple = function (x, y, config) { var _this = this; if (config === void 0) { config = {}; } var containerRect = (this._containerRect = this._containerRect || this._containerElement.getBoundingClientRect()); var animationConfig = __assign({}, defaultRippleAnimationConfig, config.animation); if (config.centered) { x = containerRect.left + containerRect.width / 2; y = containerRect.top + containerRect.height / 2; } var radius = config.radius || distanceToFurthestCorner(x, y, containerRect); var offsetX = x - containerRect.left; var offsetY = y - containerRect.top; var duration = animationConfig.enterDuration; var ripple = document.createElement('div'); ripple.classList.add('mat-ripple-element'); ripple.style.left = offsetX - radius + "px"; ripple.style.top = offsetY - radius + "px"; ripple.style.height = radius * 2 + "px"; ripple.style.width = radius * 2 + "px"; // If the color is not set, the default CSS color will be used. ripple.style.background = config.color || null; ripple.style.transitionDuration = duration + "ms"; this._pathElement.appendChild(ripple); /** * By default the browser does not recalculate the styles of dynamically created * ripple elements. This is critical because then the `scale` would not animate properly. */ enforceStyleRecalculation(ripple); ripple.style.transform = ' translate3d(0,0,0) scale(1)'; // Exposed reference to the ripple that will be returned. var rippleRef = new RippleRef(this, ripple, config); rippleRef.state = RippleState.FADING_IN; // Add the ripple reference to the list of all active ripples. this._activeRipples.add(rippleRef); if (!config.persistent) { this._mostRecentTransientRipple = rippleRef; } /** * Wait for the ripple element to be completely faded in. * Once it's faded in, the ripple can be hidden immediately if the mouse is released. */ setTimeout(function () { var isMostRecentTransientRipple = rippleRef === _this._mostRecentTransientRipple; rippleRef.state = RippleState.VISIBLE; /** * When the timer runs out while the user has kept their pointer down, we want to * keep only the persistent ripples and the latest transient ripple. We do this, * because we don't want stacked transient ripples to appear after their enter * animation has finished. */ if (!config.persistent && (!isMostRecentTransientRipple || !_this._isPointerDown)) { rippleRef.fadeOut(); } }, duration); return rippleRef; }; /** Fades out a ripple reference. */ RippleRenderer.prototype.fadeOutRipple = function (rippleRef) { var wasActive = this._activeRipples.delete(rippleRef); if (rippleRef === this._mostRecentTransientRipple) { this._mostRecentTransientRipple = null; } // Clear out the cached bounding rect if we have no more ripples. if (!this._activeRipples.size) { this._containerRect = null; } // For ripples that are not active anymore, don't re-run the fade-out animation. if (!wasActive) return; var rippleEl = rippleRef.element; var animationConfig = __assign({}, defaultRippleAnimationConfig, rippleRef.config.animation); rippleEl.style.transitionDuration = animationConfig.exitDuration + "ms"; rippleEl.style.opacity = "0"; rippleRef.state = RippleState.FADING_OUT; // Once the ripple faded out, the ripple can be safely removed from the DOM. setTimeout(function () { rippleRef.state = RippleState.HIDDEN; rippleEl.parentNode.removeChild(rippleEl); }, animationConfig.exitDuration); }; /** Fades out all currently active ripples. */ RippleRenderer.prototype.fadeOutAll = function () { this._activeRipples.forEach(function (ripple) { return ripple.fadeOut(); }); }; /** Sets up the trigger event listeners */ RippleRenderer.prototype.setupTriggerEvents = function (element) { if (!element || element === this._triggerElement) { return; } // Remove all previously registered event listeners from the trigger element. this.removeTriggerEvents(); this._triggerEvents.forEach(function (fn, type) { element.addEventListener(type, fn, passiveEventOptions); }); this._triggerElement = element; }; /** Removes previously registered event listeners from the trigger element. */ RippleRenderer.prototype.removeTriggerEvents = function () { var _this = this; if (this._triggerElement) { this._triggerEvents.forEach(function (fn, type) { _this._triggerElement.removeEventListener(type, fn, passiveEventOptions); }); } }; /** Function being called whenever the trigger is being pressed using mouse. */ RippleRenderer.prototype._onMousedown = function (event) { /** * Screen readers will fire fake mouse events for space/enter. Skip launching a * ripple in this case for consistency with the non-screen-reader experience. */ var isFakeMousedown = isFakeMousedownFromScreenReader(event); var isSyntheticEvent = this._lastTouchStartEvent && Date.now() < this._lastTouchStartEvent + ignoreMouseEventsTimeout; if (!this._target.rippleDisabled && !isFakeMousedown && !isSyntheticEvent) { this._isPointerDown = true; this.fadeInRipple(event.clientX, event.clientY, this._target.rippleConfig); } }; /** Function being called whenever the trigger is being pressed using touch. */ RippleRenderer.prototype._onTouchStart = function (event) { if (!this._target.rippleDisabled) { /** * Some browsers fire mouse events after a `touchstart` event. Those synthetic mouse * events will launch a second ripple if we don't ignore mouse events for a specific * time after a touchstart event. */ this._lastTouchStartEvent = Date.now(); this._isPointerDown = true; /** * Use `changedTouches` so we skip any touches where the user put * their finger down, but used another finger to tap the element again. */ var touches = event.changedTouches; for (var _i = 0, touches_1 = touches; _i < touches_1.length; _i++) { var touch = touches_1[_i]; this.fadeInRipple(touch.clientX, touch.clientY, this._target.rippleConfig); } } }; /** Function being called whenever the trigger is being released. */ RippleRenderer.prototype._onPointerUp = function () { if (!this._isPointerDown) return; this._isPointerDown = false; // Fade-out all ripples that are visible and not persistent. this._activeRipples.forEach(function (ripple) { /** * By default, only ripples that are completely visible will fade out on pointer release. * If the `terminateOnPointerUp` option is set, ripples that still fade in will also fade out. */ var isVisible = ripple.state === RippleState.VISIBLE || (ripple.config.terminateOnPointerUp && ripple.state === RippleState.FADING_IN); if (!ripple.config.persistent && isVisible) { ripple.fadeOut(); } }); }; return RippleRenderer; }()); var Ripple = /** @class */ (function (_super) { __extends(Ripple, _super); function Ripple(globalOptions) { var _this = _super.call(this) || this; _this._color = "rgba(0,0,0,.2)"; _this._unbounded = false; _this._centered = false; _this._radius = 0; _this._disabled = false; /** Whether ripple directive is initialized and the input bindings are set. */ _this._isInitialized = false; _this._globalOptions = globalOptions || {}; return _this; } Object.defineProperty(Ripple, "observedAttributes", { /** * Return an array containing the names of the attributes to be observed. */ get: function () { return ['color', 'unbounded', 'centered', 'radius', 'disabled']; }, enumerable: true, configurable: true }); Object.defineProperty(Ripple.prototype, "color", { /** Get custom color for all ripples. */ get: function () { return this._color; }, /** Set custom color for all ripples. */ set: function (val) { if (val) { this._color = val; } else { this._color = "rgba(0,0,0,.2)"; } }, enumerable: true, configurable: true }); Object.defineProperty(Ripple.prototype, "unbounded", { /** Get whether the ripples should be visible outside the component's bounds. */ get: function () { return this._unbounded; }, /** Set whether the ripples should be visible outside the component's bounds. */ set: function (val) { this._unbounded = val; if (val) { if (this.hasAttribute('unbounded')) return; this.setAttribute('unbounded', ''); } else { this.removeAttribute('unbounded'); } }, enumerable: true, configurable: true }); Object.defineProperty(Ripple.prototype, "centered", { /** * Get whether the ripple always originates from the center of the host element's bounds, rather * than originating from the location of the click event. */ get: function () { return this._centered; }, /** * Set whether the ripple always originates from the center of the host element's bounds, rather * than originating from the location of the click event. */ set: function (val) { this._centered = val; }, enumerable: true, configurable: true }); Object.defineProperty(Ripple.prototype, "radius", { // If set, this will return the radius in pixels of foreground ripples when fully expanded. get: function () { return this._radius; }, /** * If set, the radius in pixels of foreground ripples when fully expanded. If unset, the radius * will be the distance from the center of the ripple to the furthest corner of the host element's * bounding rectangle. */ set: function (val) { this._radius = val; }, enumerable: true, configurable: true }); Object.defineProperty(Ripple.prototype, "animation", { /** Returns the enter and exit animation duration of the ripples. */ get: function () { return this._animation; }, /** * Configuration for the ripple animation. Allows modifying the enter and exit animation * duration of the ripples. */ set: function (val) { this._animation = val; }, enumerable: true, configurable: true }); Object.defineProperty(Ripple.prototype, "disabled", { /** Get whether click events will not trigger the ripple. */ get: function () { return this._disabled; }, /** Set whether click events will not trigger the ripple. */ set: function (value) { this._disabled = value; this._setupTriggerEventsIfEnabled(); }, enumerable: true, configurable: true }); Object.defineProperty(Ripple.prototype, "trigger", { get: function () { return this._trigger || this._elementRef; }, /** The element that triggers the ripple when click events are received. */ set: function (trigger) { this._trigger = trigger; this._setupTriggerEventsIfEnabled(); }, enumerable: true, configurable: true }); Object.defineProperty(Ripple.prototype, "rippleConfig", { /** * Ripple configuration values. * Implemented as part of RippleTarget */ get: function () { return { centered: this.centered, radius: this.radius, color: this.color, animation: __assign({}, this._globalOptions.animation, this.animation), terminateOnPointerUp: this._globalOptions.terminateOnPointerUp }; }, enumerable: true, configurable: true }); Object.defineProperty(Ripple.prototype, "rippleDisabled", { /** * Whether ripples on pointer-down are disabled or not. * Implemented as part of RippleTarget */ get: function () { return this.disabled || !!this._globalOptions.disabled; }, enumerable: true, configurable: true }); /** Callback to fire when an attribute changes. */ Ripple.prototype.attributeChangedCallback = function (name, oldValue, newValue) { switch (name) { case 'color': if (oldValue !== newValue) { this.color = newValue; } break; case 'unbounded': if (this.hasAttribute('unbounded')) { this.unbounded = true; } else { this.unbounded = false; } break; case 'centered': if (this.hasAttribute('centered')) { this.centered = true; } else { this.centered = false; } break; case 'radius': if (oldValue !== newValue) { this.radius = JSON.parse(newValue); } break; case 'disabled': if (this.hasAttribute('disabled')) { this.disabled = true; } else { this.disabled = false; } break; default: break; } }; /** Function invoked each time the custom element is appended into a document-connected element */ Ripple.prototype.connectedCallback = function () { this._isInitialized = true; this._setup(); this._setupTriggerEventsIfEnabled(); }; /** Function is invoked each time the custom element is disconnected from the document's DOM. */ Ripple.prototype.disconnectedCallback = function () { this._rippleRenderer.removeTriggerEvents(); }; /** Fades out all currently showing ripple elements. */ Ripple.prototype.fadeOutAll = function () { this._rippleRenderer.fadeOutAll(); }; /** Launches a manual ripple at the specified coordinated or just by the ripple config. */ Ripple.prototype.launch = function (configOrX, y, config) { if (y === void 0) { y = 0; } if (typeof configOrX === 'number') { return this._rippleRenderer.fadeInRipple(configOrX, y, __assign({}, this.rippleConfig, config)); } else { return this._rippleRenderer.fadeInRipple(0, 0, __assign({}, this.rippleConfig, configOrX)); } }; /** Sets up the trigger event listeners if ripples are enabled. */ Ripple.prototype._setupTriggerEventsIfEnabled = function () { if (!this.disabled && this._isInitialized) { this._rippleRenderer.setupTriggerEvents(this.trigger); } }; /** * Function to creat the `template` for the Ripple and * attaching the shadow DOM to the root */ Ripple.prototype._setup = function () { var tmp = document.createElement('template'); tmp.innerHTML = "\n\t\t\t<style>\n :host{\n\t\t\t\t\tposition: absolute !important;\n\t\t\t\t\tborder-radius: inherit;\n top: 0;\n left: 0;\n bottom: 0;\n\t\t\t\t\tright: 0;\n\t\t\t\t\toverflow: hidden;\n\t\t\t\t\tpointer-events: none\n }\n\n\t\t\t\t.mat-ripple-element {\n\t\t\t\t\tposition: absolute;\n\t\t\t\t\tborder-radius: 50%;\n\t\t\t\t\tpointer-events: none;\n\t\t\t\t\ttransition: opacity, transform 0ms cubic-bezier(0, 0, 0.2, 1);\n\t\t\t\t\ttransform: translate3d(0,0,0) scale(0);\n\t\t\t\t\twill-change: transform, opacity;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t:host([unbounded]) {\n\t\t\t\t\toverflow: visible;\n\t\t\t\t}\n\t\t\t\t\n </style>\n <slot></slot>\n "; var shadowRoot = this.attachShadow({ mode: 'open' }); shadowRoot.appendChild(tmp.content.cloneNode(true)); this._elementRef = this.parentElement; this._rippleRenderer = new RippleRenderer(this, this._elementRef, this.shadowRoot); /** Parent style */ var parentElementPositionStyle = getStyle(this.parentElement, 'position'); if (parentElementPositionStyle === 'static') { this.parentElement.style.position = 'relative'; } }; return Ripple; }(HTMLElement)); /** * Main class to export. can be used to define custom element. * It can also be extended to add more functionality or * modify any default configuration. */ var MatRipple = /** @class */ (function (_super) { __extends(MatRipple, _super); function MatRipple(globalOptions) { return _super.call(this, globalOptions) || this; } return MatRipple; }(Ripple)); /** * Define `mat-ripple` as a custom element using * the `MatRipple` class. */ customElements.define('mat-ripple', MatRipple); module.exports = MatRipple;