mat-ripple
Version:
Material design Ripple effect.
664 lines (648 loc) • 28.2 kB
JavaScript
'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;