angular-material-npfixed
Version:
The Angular Material project is an implementation of Material Design in Angular.js. This project provides a set of reusable, well-tested, and accessible Material Design UI components. Angular Material is supported internally at Google by the Angular.js, M
462 lines (403 loc) • 13.8 kB
JavaScript
/**
* @ngdoc module
* @name material.core.ripple
* @description
* Ripple
*/
angular.module('material.core')
.provider('$mdInkRipple', InkRippleProvider)
.directive('mdInkRipple', InkRippleDirective)
.directive('mdNoInk', attrNoDirective)
.directive('mdNoBar', attrNoDirective)
.directive('mdNoStretch', attrNoDirective);
var DURATION = 450;
/**
* @ngdoc directive
* @name mdInkRipple
* @module material.core.ripple
*
* @description
* The `md-ink-ripple` directive allows you to specify the ripple color or id a ripple is allowed.
*
* @param {string|boolean} md-ink-ripple A color string `#FF0000` or boolean (`false` or `0`) for preventing ripple
*
* @usage
* ### String values
* <hljs lang="html">
* <ANY md-ink-ripple="#FF0000">
* Ripples in red
* </ANY>
*
* <ANY md-ink-ripple="false">
* Not rippling
* </ANY>
* </hljs>
*
* ### Interpolated values
* <hljs lang="html">
* <ANY md-ink-ripple="{{ randomColor() }}">
* Ripples with the return value of 'randomColor' function
* </ANY>
*
* <ANY md-ink-ripple="{{ canRipple() }}">
* Ripples if 'canRipple' function return value is not 'false' or '0'
* </ANY>
* </hljs>
*/
function InkRippleDirective ($mdButtonInkRipple, $mdCheckboxInkRipple) {
return {
controller: angular.noop,
link: function (scope, element, attr) {
attr.hasOwnProperty('mdInkRippleCheckbox')
? $mdCheckboxInkRipple.attach(scope, element)
: $mdButtonInkRipple.attach(scope, element);
}
};
}
/**
* @ngdoc service
* @name $mdInkRipple
* @module material.core.ripple
*
* @description
* `$mdInkRipple` is a service for adding ripples to any element
*
* @usage
* <hljs lang="js">
* app.factory('$myElementInkRipple', function($mdInkRipple) {
* return {
* attach: function (scope, element, options) {
* return $mdInkRipple.attach(scope, element, angular.extend({
* center: false,
* dimBackground: true
* }, options));
* }
* };
* });
*
* app.controller('myController', function ($scope, $element, $myElementInkRipple) {
* $scope.onClick = function (ev) {
* $myElementInkRipple.attach($scope, angular.element(ev.target), { center: true });
* }
* });
* </hljs>
*
* ### Disabling ripples globally
* If you want to disable ink ripples globally, for all components, you can call the
* `disableInkRipple` method in your app's config.
*
* <hljs lang="js">
* app.config(function ($mdInkRippleProvider) {
* $mdInkRippleProvider.disableInkRipple();
* });
*/
function InkRippleProvider () {
var isDisabledGlobally = false;
return {
disableInkRipple: disableInkRipple,
$get: function($injector) {
return { attach: attach };
/**
* @ngdoc method
* @name $mdInkRipple#attach
*
* @description
* Attaching given scope, element and options to inkRipple controller
*
* @param {object=} scope Scope within the current context
* @param {object=} element The element the ripple effect should be applied to
* @param {object=} options (Optional) Configuration options to override the defaultRipple configuration
* * `center` - Whether the ripple should start from the center of the container element
* * `dimBackground` - Whether the background should be dimmed with the ripple color
* * `colorElement` - The element the ripple should take its color from, defined by css property `color`
* * `fitRipple` - Whether the ripple should fill the element
*/
function attach (scope, element, options) {
if (isDisabledGlobally || element.controller('mdNoInk')) return angular.noop;
return $injector.instantiate(InkRippleCtrl, {
$scope: scope,
$element: element,
rippleOptions: options
});
}
}
};
/**
* @ngdoc method
* @name $mdInkRipple#disableInkRipple
*
* @description
* A config-time method that, when called, disables ripples globally.
*/
function disableInkRipple () {
isDisabledGlobally = true;
}
}
/**
* Controller used by the ripple service in order to apply ripples
* @ngInject
*/
function InkRippleCtrl ($scope, $element, rippleOptions, $window, $timeout, $mdUtil, $mdColorUtil) {
this.$window = $window;
this.$timeout = $timeout;
this.$mdUtil = $mdUtil;
this.$mdColorUtil = $mdColorUtil;
this.$scope = $scope;
this.$element = $element;
this.options = rippleOptions;
this.mousedown = false;
this.ripples = [];
this.timeout = null; // Stores a reference to the most-recent ripple timeout
this.lastRipple = null;
$mdUtil.valueOnUse(this, 'container', this.createContainer);
this.$element.addClass('md-ink-ripple');
// attach method for unit tests
($element.controller('mdInkRipple') || {}).createRipple = angular.bind(this, this.createRipple);
($element.controller('mdInkRipple') || {}).setColor = angular.bind(this, this.color);
this.bindEvents();
}
/**
* Either remove or unlock any remaining ripples when the user mouses off of the element (either by
* mouseup or mouseleave event)
*/
function autoCleanup (self, cleanupFn) {
if ( self.mousedown || self.lastRipple ) {
self.mousedown = false;
self.$mdUtil.nextTick( angular.bind(self, cleanupFn), false);
}
}
/**
* Returns the color that the ripple should be (either based on CSS or hard-coded)
* @returns {string}
*/
InkRippleCtrl.prototype.color = function (value) {
var self = this;
// If assigning a color value, apply it to background and the ripple color
if (angular.isDefined(value)) {
self._color = self._parseColor(value);
}
// If color lookup, use assigned, defined, or inherited
return self._color || self._parseColor( self.inkRipple() ) || self._parseColor( getElementColor() );
/**
* Finds the color element and returns its text color for use as default ripple color
* @returns {string}
*/
function getElementColor () {
var items = self.options && self.options.colorElement ? self.options.colorElement : [];
var elem = items.length ? items[ 0 ] : self.$element[ 0 ];
return elem ? self.$window.getComputedStyle(elem).color : 'rgb(0,0,0)';
}
};
/**
* Updating the ripple colors based on the current inkRipple value
* or the element's computed style color
*/
InkRippleCtrl.prototype.calculateColor = function () {
return this.color();
};
/**
* Takes a string color and converts it to RGBA format
* @param color {string}
* @param [multiplier] {int}
* @returns {string}
*/
InkRippleCtrl.prototype._parseColor = function parseColor (color, multiplier) {
multiplier = multiplier || 1;
var colorUtil = this.$mdColorUtil;
if (!color) return;
if (color.indexOf('rgba') === 0) return color.replace(/\d?\.?\d*\s*\)\s*$/, (0.1 * multiplier).toString() + ')');
if (color.indexOf('rgb') === 0) return colorUtil.rgbToRgba(color);
if (color.indexOf('#') === 0) return colorUtil.hexToRgba(color);
};
/**
* Binds events to the root element for
*/
InkRippleCtrl.prototype.bindEvents = function () {
this.$element.on('mousedown', angular.bind(this, this.handleMousedown));
this.$element.on('mouseup touchend', angular.bind(this, this.handleMouseup));
this.$element.on('mouseleave', angular.bind(this, this.handleMouseup));
this.$element.on('touchmove', angular.bind(this, this.handleTouchmove));
};
/**
* Create a new ripple on every mousedown event from the root element
* @param event {MouseEvent}
*/
InkRippleCtrl.prototype.handleMousedown = function (event) {
if ( this.mousedown ) return;
// When jQuery is loaded, we have to get the original event
if (event.hasOwnProperty('originalEvent')) event = event.originalEvent;
this.mousedown = true;
if (this.options.center) {
this.createRipple(this.container.prop('clientWidth') / 2, this.container.prop('clientWidth') / 2);
} else {
// We need to calculate the relative coordinates if the target is a sublayer of the ripple element
if (event.srcElement !== this.$element[0]) {
var layerRect = this.$element[0].getBoundingClientRect();
var layerX = event.clientX - layerRect.left;
var layerY = event.clientY - layerRect.top;
this.createRipple(layerX, layerY);
} else {
this.createRipple(event.offsetX, event.offsetY);
}
}
};
/**
* Either remove or unlock any remaining ripples when the user mouses off of the element (either by
* mouseup, touchend or mouseleave event)
*/
InkRippleCtrl.prototype.handleMouseup = function () {
autoCleanup(this, this.clearRipples);
};
/**
* Either remove or unlock any remaining ripples when the user mouses off of the element (by
* touchmove)
*/
InkRippleCtrl.prototype.handleTouchmove = function () {
autoCleanup(this, this.deleteRipples);
};
/**
* Cycles through all ripples and attempts to remove them.
*/
InkRippleCtrl.prototype.deleteRipples = function () {
for (var i = 0; i < this.ripples.length; i++) {
this.ripples[ i ].remove();
}
};
/**
* Cycles through all ripples and attempts to remove them with fade.
* Depending on logic within `fadeInComplete`, some removals will be postponed.
*/
InkRippleCtrl.prototype.clearRipples = function () {
for (var i = 0; i < this.ripples.length; i++) {
this.fadeInComplete(this.ripples[ i ]);
}
};
/**
* Creates the ripple container element
* @returns {*}
*/
InkRippleCtrl.prototype.createContainer = function () {
var container = angular.element('<div class="md-ripple-container"></div>');
this.$element.append(container);
return container;
};
InkRippleCtrl.prototype.clearTimeout = function () {
if (this.timeout) {
this.$timeout.cancel(this.timeout);
this.timeout = null;
}
};
InkRippleCtrl.prototype.isRippleAllowed = function () {
var element = this.$element[0];
do {
if (!element.tagName || element.tagName === 'BODY') break;
if (element && angular.isFunction(element.hasAttribute)) {
if (element.hasAttribute('disabled')) return false;
if (this.inkRipple() === 'false' || this.inkRipple() === '0') return false;
}
} while (element = element.parentNode);
return true;
};
/**
* The attribute `md-ink-ripple` may be a static or interpolated
* color value OR a boolean indicator (used to disable ripples)
*/
InkRippleCtrl.prototype.inkRipple = function () {
return this.$element.attr('md-ink-ripple');
};
/**
* Creates a new ripple and adds it to the container. Also tracks ripple in `this.ripples`.
* @param left
* @param top
*/
InkRippleCtrl.prototype.createRipple = function (left, top) {
if (!this.isRippleAllowed()) return;
var ctrl = this;
var colorUtil = ctrl.$mdColorUtil;
var ripple = angular.element('<div class="md-ripple"></div>');
var width = this.$element.prop('clientWidth');
var height = this.$element.prop('clientHeight');
var x = Math.max(Math.abs(width - left), left) * 2;
var y = Math.max(Math.abs(height - top), top) * 2;
var size = getSize(this.options.fitRipple, x, y);
var color = this.calculateColor();
ripple.css({
left: left + 'px',
top: top + 'px',
background: 'black',
width: size + 'px',
height: size + 'px',
backgroundColor: colorUtil.rgbaToRgb(color),
borderColor: colorUtil.rgbaToRgb(color)
});
this.lastRipple = ripple;
// we only want one timeout to be running at a time
this.clearTimeout();
this.timeout = this.$timeout(function () {
ctrl.clearTimeout();
if (!ctrl.mousedown) ctrl.fadeInComplete(ripple);
}, DURATION * 0.35, false);
if (this.options.dimBackground) this.container.css({ backgroundColor: color });
this.container.append(ripple);
this.ripples.push(ripple);
ripple.addClass('md-ripple-placed');
this.$mdUtil.nextTick(function () {
ripple.addClass('md-ripple-scaled md-ripple-active');
ctrl.$timeout(function () {
ctrl.clearRipples();
}, DURATION, false);
}, false);
function getSize (fit, x, y) {
return fit
? Math.max(x, y)
: Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
}
};
/**
* After fadeIn finishes, either kicks off the fade-out animation or queues the element for removal on mouseup
* @param ripple
*/
InkRippleCtrl.prototype.fadeInComplete = function (ripple) {
if (this.lastRipple === ripple) {
if (!this.timeout && !this.mousedown) {
this.removeRipple(ripple);
}
} else {
this.removeRipple(ripple);
}
};
/**
* Kicks off the animation for removing a ripple
* @param ripple {Element}
*/
InkRippleCtrl.prototype.removeRipple = function (ripple) {
var ctrl = this;
var index = this.ripples.indexOf(ripple);
if (index < 0) return;
this.ripples.splice(this.ripples.indexOf(ripple), 1);
ripple.removeClass('md-ripple-active');
ripple.addClass('md-ripple-remove');
if (this.ripples.length === 0) this.container.css({ backgroundColor: '' });
// use a 2-second timeout in order to allow for the animation to finish
// we don't actually care how long the animation takes
this.$timeout(function () {
ctrl.fadeOutComplete(ripple);
}, DURATION, false);
};
/**
* Removes the provided ripple from the DOM
* @param ripple
*/
InkRippleCtrl.prototype.fadeOutComplete = function (ripple) {
ripple.remove();
this.lastRipple = null;
};
/**
* Used to create an empty directive. This is used to track flag-directives whose children may have
* functionality based on them.
*
* Example: `md-no-ink` will potentially be used by all child directives.
*/
function attrNoDirective () {
return { controller: angular.noop };
}