ledgerrb/public/app/bower_components/angular-material/angular-material.js
Gwenhael Le Moine 62fea9596e bower update
2014-09-30 12:29:53 +02:00

5059 lines
No EOL
142 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*!
* Angular Material Design
* https://github.com/angular/material
* @license MIT
* v0.0.3
*/
(function(){
angular.module('ngMaterial', [ 'ng', 'ngAnimate', 'material.services.attrBind', 'material.services.compiler', 'material.services.registry', 'material.services.throttle', 'material.decorators', 'material.services.aria', "material.components.button","material.components.card","material.components.checkbox","material.components.content","material.components.dialog","material.components.divider","material.components.icon","material.components.linearProgress","material.components.list","material.components.radioButton","material.components.sidenav","material.components.slider","material.components.switch","material.components.tabs","material.components.textField","material.components.toast","material.components.toolbar","material.components.whiteframe"]);
/*
* iterator is a list facade to easily support iteration and accessors
*
* @param items Array list which this iterator will enumerate
* @param reloop Boolean enables iterator to consider the list as an endless reloop
*/
function iterator(items, reloop) {
reloop = !!reloop;
var _items = items || [ ];
// Published API
return {
items: getItems,
count: count,
inRange: inRange,
contains: contains,
indexOf: indexOf,
itemAt: itemAt,
findBy: findBy,
add: add,
remove: remove,
first: first,
last: last,
next: next,
previous: previous,
hasPrevious:hasPrevious,
hasNext: hasNext
};
/*
* Publish copy of the enumerable set
* @returns {Array|*}
*/
function getItems() {
return [].concat(_items);
}
/*
* Determine length of the list
* @returns {Array.length|*|number}
*/
function count() {
return _items.length;
}
/*
* Is the index specified valid
* @param index
* @returns {Array.length|*|number|boolean}
*/
function inRange(index) {
return _items.length && ( index > -1 ) && (index < _items.length );
}
/*
* Can the iterator proceed to the next item in the list; relative to
* the specified item.
*
* @param tab
* @returns {Array.length|*|number|boolean}
*/
function hasNext(tab) {
return tab ? inRange(indexOf(tab) + 1) : false;
}
/*
* Can the iterator proceed to the previous item in the list; relative to
* the specified item.
*
* @param tab
* @returns {Array.length|*|number|boolean}
*/
function hasPrevious(tab) {
return tab ? inRange(indexOf(tab) - 1) : false;
}
/*
* Get item at specified index/position
* @param index
* @returns {*}
*/
function itemAt(index) {
return inRange(index) ? _items[index] : null;
}
/*
* Find all elements matching the key/value pair
* otherwise return null
*
* @param val
* @param key
*
* @return array
*/
function findBy(key, val) {
/*
* Implement of e6 Array::find()
* @param list
* @param callback
* @returns {*}
*/
function find(list, callback) {
var results = [ ];
angular.forEach(list, function (it, index) {
var val = callback.apply(null, [it, index, list]);
if (val) {
results.push(val);
}
});
return results.length ? results : null;
}
// Use iterator callback to matches element key value
// NOTE: searches full prototype chain
return find(_items, function (el) {
return ( el[key] == val ) ? el : null;
});
}
/*
* Add item to list
* @param it
* @param index
* @returns {*}
*/
function add(it, index) {
if ( !it ) return -1;
if (!angular.isDefined(index)) {
index = _items.length;
}
_items.splice(index, 0, it);
return indexOf(it);
}
/*
* Remove it from list...
* @param it
*/
function remove(it) {
if ( contains(it) ){
_items.splice(indexOf(it), 1);
}
}
/*
* Get the zero-based index of the target tab
* @param it
* @returns {*}
*/
function indexOf(it) {
return _items.indexOf(it);
}
/*
* Boolean existence check
* @param it
* @returns {boolean}
*/
function contains(it) {
return it && (indexOf(it) > -1);
}
/*
* Find the next item
* @param tab
* @returns {*}
*/
function next(it, validate) {
if (contains(it)) {
var index = indexOf(it) + 1,
found = inRange(index) ? _items[ index ] : reloop ? first() : null,
skip = found && validate && !validate(found);
return skip ? next(found, validate) : found;
}
return null;
}
/*
* Find the previous item
* @param tab
* @returns {*}
*/
function previous(it, validate) {
if (contains(it)) {
var index = indexOf(it) - 1,
found = inRange(index) ? _items[ index ] : reloop ? last() : null,
skip = found && validate && !validate(found);
return skip ? previous(found, validate) : found;
}
return null;
}
/*
* Return first item in the list
* @returns {*}
*/
function first() {
return _items.length ? _items[0] : null;
}
/*
* Return last item in the list...
* @returns {*}
*/
function last() {
return _items.length ? _items[_items.length - 1] : null;
}
}
var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g;
var Util = {
/**
* Checks to see if the element or its parents are disabled.
* @param element DOM element to start scanning for `disabled` attribute
* @param limit Number of parent levels that should be scanned; defaults to 4
* @returns {*} Boolean
*/
isDisabled : function isDisabled(element, limit) {
return Util.ancestorHasAttribute( element, 'disabled', limit );
},
/**
* Checks if the specified element has an ancestor (ancestor being parent, grandparent, etc)
* with the given attribute defined.
*
* Also pass in an optional `limit` (levels of ancestry to scan), default 8.
*/
ancestorHasAttribute: function ancestorHasAttribute(element, attrName, limit) {
limit = limit || 4;
var current = element;
while (limit-- && current.length) {
if (current[0].hasAttribute && current[0].hasAttribute(attrName)) {
return true;
}
current = current.parent();
}
return false;
},
/**
* Checks if two elements have the same parent
*/
elementIsSibling: function elementIsSibling(element, otherElement) {
return element.parent().length &&
(element.parent()[0] === otherElement.parent()[0]);
},
/**
* Converts snake_case to camelCase.
* @param name Name to normalize
*/
camelCase: function camelCase(name) {
return name.
replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) {
return offset ? letter.toUpperCase() : letter;
});
},
/**
* Selects 'n' words from a string
* for use in an HTML attribute
*/
stringFromTextBody: function stringFromTextBody(textBody, numWords) {
var string = textBody.trim();
if(string.split(/\s+/).length > numWords){
string = textBody.split(/\s+/).slice(1, (numWords + 1)).join(" ") + '...';
}
return string;
},
/**
* Spread the arguments as individual parameters to the target function.
* @param targetFn
* @param scope
* @returns {Function}
*/
spread : function ( targetFn, scope ) {
return function()
{
var params = Array.prototype.slice.call(arguments, 0);
targetFn.apply(scope, params);
};
},
/**
* Publish the iterator facade to easily support iteration and accessors
* @see iterator.js
*/
iterator : iterator,
css : {
/**
* For any positional fields, ensure that a `px` suffix
* is provided.
* @param target
* @returns {*}
*/
appendSuffix : function (target) {
var styles = 'top left right bottom ' +
'x y width height ' +
'border-width border-radius borderWidth borderRadius' +
'margin margin-top margin-bottom margin-left margin-right ' +
'padding padding-left padding-right padding-top padding-bottom'.split(' ');
angular.forEach(target, function(val, key) {
var isPositional = styles.indexOf(key) > -1;
var hasPx = String(val).indexOf('px') > -1;
if (isPositional && !hasPx) {
target[key] = val + 'px';
}
});
return target;
}
},
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
debounce: function debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
clearTimeout(timeout);
timeout = setTimeout(function() {
timeout = null;
if (!immediate) func.apply(context, args);
}, wait);
if (immediate && !timeout) func.apply(context, args);
};
}
};
/*
* Since removing jQuery from the demos, some code that uses `element.focus()` is broken.
*
* We need to add `element.focus()`, because it's testable unlike `element[0].focus`.
*
* TODO(ajoslin): This should be added in a better place later.
*/
angular.element.prototype.focus = angular.element.prototype.focus || function() {
if (this.length) {
this[0].focus();
}
return this;
};
var Constant = {
ARIA : {
ROLE : {
BUTTON : 'button',
CHECKBOX : 'checkbox',
DIALOG : 'dialog',
LIST : 'list',
LIST_ITEM : 'listitem',
RADIO : 'radio',
RADIO_GROUP : 'radiogroup',
SLIDER : 'slider',
TAB_LIST : 'tablist',
TAB : 'tab',
TAB_PANEL : 'tabpanel'
},
PROPERTY : {
CHECKED : 'aria-checked',
HIDDEN : 'aria-hidden',
EXPANDED : 'aria-expanded',
LABEL: 'aria-label',
SELECTED : 'aria-selected',
LABEL_BY : 'aria-labelledby'
},
STATE: {}
},
KEY_CODE : {
ESCAPE : 27,
SPACE : 32,
LEFT_ARROW : 37,
RIGHT_ARROW : 39,
ENTER: 13
},
EVENTS : {
SCOPE_DESTROY : '$destroy',
TABS_CHANGED : '$materialTabsChanged',
FOCUS_CHANGED : '$materialFocusChanged',
WINDOW_RESIZE : 'resize',
KEY_DOWN : 'keydown',
CLICK : 'click'
}
};
/**
* Alias shortcuts...
*/
var EVENT = Constant.EVENTS;
var KEY = Constant.KEY_CODE;
/**
* @ngdoc module
* @name material.components.animate
* @description
*
* Ink and Popup Effects
*/
angular.module('material.animations', [
'material.services.throttle'
])
.service('$materialEffects', [
'$rootElement',
'$$rAF',
'$sniffer',
'$q',
MaterialEffects
]);
/**
* @ngdoc service
* @name $materialEffects
* @module material.components.animate
*
* @description
* The `$materialEffects` service provides a simple API for various
* Material Design effects.
*
* @returns A `$materialEffects` object with the following properties:
* - `{function(element,styles,duration)}` `inkBar` - starts ink bar
* animation on specified DOM element
* - `{function(element,parentElement,clickElement)}` `popIn` - animated show of element overlayed on parent element
* - `{function(element,parentElement)}` `popOut` - animated close of popup overlay
*
*/
function MaterialEffects($rootElement, $$rAF, $sniffer, $q) {
var webkit = /webkit/i.test($sniffer.vendorPrefix);
function vendorProperty(name) {
return webkit ?
('webkit' + name.charAt(0).toUpperCase() + name.substring(1)) :
name;
}
var self;
// Publish API for effects...
return self = {
popIn: popIn,
/* Constants */
TRANSITIONEND_EVENT: 'transitionend' + (webkit ? ' webkitTransitionEnd' : ''),
ANIMATIONEND_EVENT: 'animationend' + (webkit ? ' webkitAnimationEnd' : ''),
TRANSFORM: vendorProperty('transform'),
TRANSITION: vendorProperty('transition'),
TRANSITION_DURATION: vendorProperty('transitionDuration'),
ANIMATION_PLAY_STATE: vendorProperty('animationPlayState'),
ANIMATION_DURATION: vendorProperty('animationDuration'),
ANIMATION_NAME: vendorProperty('animationName'),
ANIMATION_TIMING: vendorProperty('animationTimingFunction'),
ANIMATION_DIRECTION: vendorProperty('animationDirection')
};
// **********************************************************
// API Methods
// **********************************************************
function popIn(element, parentElement, clickElement) {
var deferred = $q.defer();
parentElement.append(element);
var startPos;
if (clickElement) {
var clickRect = clickElement[0].getBoundingClientRect();
startPos = translateString(
clickRect.left - element[0].offsetWidth,
clickRect.top - element[0].offsetHeight,
0
) + ' scale(0.2)';
} else {
startPos = 'translate3d(0,100%,0) scale(0.5)';
}
element
.css(self.TRANSFORM, startPos)
.css('opacity', 0);
$$rAF(function() {
$$rAF(function() {
element
.addClass('active')
.css(self.TRANSFORM, '')
.css('opacity', '')
.on(self.TRANSITIONEND_EVENT, finished);
});
});
function finished(ev) {
//Make sure this transitionend didn't bubble up from a child
if (ev.target === element[0]) {
element.off(self.TRANSITIONEND_EVENT, finished);
deferred.resolve();
}
}
return deferred.promise;
}
// **********************************************************
// Utility Methods
// **********************************************************
function translateString(x, y, z) {
return 'translate3d(' + Math.floor(x) + 'px,' + Math.floor(y) + 'px,' + Math.floor(z) + 'px)';
}
}
(function() {
angular.module('material.animations')
/**
* noink/nobar/nostretch directive: make any element that has one of
* these attributes be given a controller, so that other directives can
* `require:` these and see if there is a `no<xxx>` parent attribute.
*
* @usage
* <hljs lang="html">
* <parent noink>
* <child detect-no>
* </child>
* </parent>
* </hljs>
*
* <hljs lang="js">
* myApp.directive('detectNo', function() {
* return {
* require: ['^?noink', ^?nobar'],
* link: function(scope, element, attr, ctrls) {
* var noinkCtrl = ctrls[0];
* var nobarCtrl = ctrls[1];
* if (noInkCtrl) {
* alert("the noink flag has been specified on an ancestor!");
* }
* if (nobarCtrl) {
* alert("the nobar flag has been specified on an ancestor!");
* }
* }
* };
* });
* </hljs>
*/
.directive({
noink: attrNoDirective(),
nobar: attrNoDirective(),
nostretch: attrNoDirective()
});
function attrNoDirective() {
return function() {
return {
controller: angular.noop
};
};
}
})();
angular.module('material.animations')
.directive('inkRipple', [
'$materialInkRipple',
InkRippleDirective
])
.factory('$materialInkRipple', [
'$window',
'$$rAF',
'$materialEffects',
'$timeout',
InkRippleService
]);
function InkRippleDirective($materialInkRipple) {
return function(scope, element, attr) {
if (attr.inkRipple == 'checkbox') {
$materialInkRipple.attachCheckboxBehavior(element);
} else {
$materialInkRipple.attachButtonBehavior(element);
}
};
}
function InkRippleService($window, $$rAF, $materialEffects, $timeout) {
// TODO fix this. doesn't support touch AND click devices (eg chrome pixel)
var hasTouch = !!('ontouchend' in document);
var POINTERDOWN_EVENT = hasTouch ? 'touchstart' : 'mousedown';
var POINTERUP_EVENT = hasTouch ? 'touchend touchcancel' : 'mouseup mouseleave';
return {
attachButtonBehavior: attachButtonBehavior,
attachCheckboxBehavior: attachCheckboxBehavior,
attach: attach
};
function attachButtonBehavior(element) {
return attach(element, {
mousedown: true,
center: false,
animationDuration: 350,
mousedownPauseTime: 175,
animationName: 'inkRippleButton',
animationTimingFunction: 'linear'
});
}
function attachCheckboxBehavior(element) {
return attach(element, {
mousedown: true,
center: true,
animationDuration: 300,
mousedownPauseTime: 180,
animationName: 'inkRippleCheckbox',
animationTimingFunction: 'linear'
});
}
function attach(element, options) {
options = angular.extend({
mousedown: true,
hover: true,
focus: true,
center: false,
animationDuration: 300,
mousedownPauseTime: 150,
animationName: '',
animationTimingFunction: 'linear'
}, options || {});
var rippleContainer;
var node = element[0];
if (options.mousedown) {
enableMousedown();
}
function rippleIsAllowed() {
return !element.controller('noink') && !Util.isDisabled(element);
}
function enableMousedown() {
element.on(POINTERDOWN_EVENT, onPointerDown);
function onPointerDown(ev) {
if (!rippleIsAllowed()) return;
var rippleEl = createRippleFromEvent(ev);
var ripplePauseTimeout = $timeout(pauseRipple, options.mousedownPauseTime, false);
rippleEl.on('$destroy', cancelRipplePause);
// Stop listening to pointer down for now, until the user lifts their finger/mouse
element.off(POINTERDOWN_EVENT, onPointerDown);
element.on(POINTERUP_EVENT, onPointerUp);
function onPointerUp() {
cancelRipplePause();
rippleEl.css($materialEffects.ANIMATION_PLAY_STATE, 'running');
element.off(POINTERUP_EVENT, onPointerUp);
element.on(POINTERDOWN_EVENT, onPointerDown);
}
function pauseRipple() {
rippleEl.css($materialEffects.ANIMATION_PLAY_STATE, 'paused');
}
function cancelRipplePause() {
$timeout.cancel(ripplePauseTimeout);
}
}
}
function createRippleFromEvent(ev) {
ev = ev.touches ? ev.touches[0] : ev;
return createRipple(ev.pageX, ev.pageY, true);
}
function createRipple(left, top, positionsAreAbsolute) {
var rippleEl = angular.element('<div class="material-ripple">')
.css($materialEffects.ANIMATION_DURATION, options.animationDuration + 'ms')
.css($materialEffects.ANIMATION_NAME, options.animationName)
.css($materialEffects.ANIMATION_TIMING, options.animationTimingFunction)
.on($materialEffects.ANIMATIONEND_EVENT, function() {
rippleEl.remove();
});
if (!rippleContainer) {
rippleContainer = angular.element('<div class="material-ripple-container">');
element.append(rippleContainer);
}
rippleContainer.append(rippleEl);
var containerWidth = rippleContainer.prop('offsetWidth');
if (options.center) {
left = containerWidth / 2;
top = rippleContainer.prop('offsetHeight') / 2;
} else if (positionsAreAbsolute) {
var elementRect = node.getBoundingClientRect();
left -= elementRect.left;
top -= elementRect.top;
}
var css = {
'background-color': $window.getComputedStyle(rippleEl[0]).color ||
$window.getComputedStyle(node).color,
'border-radius': (containerWidth / 2) + 'px',
left: (left - containerWidth / 2) + 'px',
width: containerWidth + 'px',
top: (top - containerWidth / 2) + 'px',
height: containerWidth + 'px'
};
css[$materialEffects.ANIMATION_DURATION] = options.fadeoutDuration + 'ms';
rippleEl.css(css);
return rippleEl;
}
}
}
/**
* @ngdoc module
* @name material.components.buttons
* @description
*
* Button
*/
angular.module('material.components.button', [
'material.animations',
'material.services.aria'
])
.directive('materialButton', [
'ngHrefDirective',
'$materialInkRipple',
'$aria',
MaterialButtonDirective
]);
/**
* @ngdoc directive
* @name materialButton
* @order 0
*
* @restrict E
*
* @description
* `<material-button>` is a button directive with optional ink ripples (default enabled).
*
* @param {boolean=} noink Flag indicates use of ripple ink effects
* @param {boolean=} disabled Flag indicates if the tab is disabled: not selectable with no ink effects
* @param {string=} type Optional attribute to specific button types (useful for forms); such as 'submit', etc.
* @param {string=} ng-href Optional attribute to support both ARIA and link navigation
* @param {string=} href Optional attribute to support both ARIA and link navigation
* @param {string=} ariaLabel Publish the button label used by screen-readers for accessibility. Defaults to the radio button's text.
*
* @usage
* <hljs lang="html">
* <material-button>Button</material-button>
* <br/>
* <material-button noink class="material-button-colored">
* Button (noInk)
* </material-button>
* <br/>
* <material-button disabled class="material-button-colored">
* Colored (disabled)
* </material-button>
* </hljs>
*/
function MaterialButtonDirective(ngHrefDirectives, $materialInkRipple, $aria ) {
var ngHrefDirective = ngHrefDirectives[0];
return {
restrict: 'E',
compile: function(element, attr) {
var innerElement;
var attributesToCopy;
// Add an inner anchor if the element has a `href` or `ngHref` attribute,
// so this element can be clicked like a normal `<a>`.
if (attr.ngHref || attr.href) {
innerElement = angular.element('<a>');
attributesToCopy = ['ng-href', 'href', 'rel', 'target'];
// Otherwise, just add an inner button element (for form submission etc)
} else {
innerElement = angular.element('<button>');
attributesToCopy = ['type', 'disabled', 'ng-disabled', 'form'];
}
angular.forEach(attributesToCopy, function(name) {
var camelCaseName = Util.camelCase(name);
if (attr.hasOwnProperty(camelCaseName)) {
innerElement.attr(name, attr[camelCaseName]);
}
});
innerElement
.addClass('material-button-inner')
.append(element.contents())
// Since we're always passing focus to the inner element,
// add a focus class to the outer element so we can still style
// it with focus.
.on('focus', function() {
element.addClass('focus');
})
.on('blur', function() {
element.removeClass('focus');
});
element.
append(innerElement)
.attr('tabIndex', -1)
//Always pass focus to innerElement
.on('focus', function() {
innerElement.focus();
});
return function postLink(scope, element, attr) {
$aria.expect(element, 'aria-label', element.text());
$materialInkRipple.attachButtonBehavior(element);
};
}
};
}
/**
* @ngdoc module
* @name material.components.card
*
* @description
* Card components.
*/
angular.module('material.components.card', [
])
.directive('materialCard', [
materialCardDirective
]);
/**
* @ngdoc directive
* @name materialCard
* @module material.components.card
*
* @restrict E
*
* @description
* The `<material-card>` directive is a container element used within `<material-content>` containers.
*
* Cards have constant width and variable heights; where the maximum height is limited to what can
* fit within a single view on a platform, but it can temporarily expand as needed
*
* @usage
* <hljs lang="html">
* <material-card>
* <img src="/img/washedout.png" class="material-card-image">
* <h2>Paracosm</h2>
* <p>
* The titles of Washed Out's breakthrough song and the first single from Paracosm share the * two most important words in Ernest Greene's musical language: feel it. It's a simple request, as well...
* </p>
* </material-card>
* </hljs>
*
*/
function materialCardDirective() {
return {
restrict: 'E',
link: function($scope, $element, $attr) {
}
};
}
/**
* @ngdoc module
* @name material.components.checkbox
* @description Checkbox module!
*/
angular.module('material.components.checkbox', [
'material.animations',
'material.services.aria'
])
.directive('materialCheckbox', [
'inputDirective',
'$materialInkRipple',
'$aria',
MaterialCheckboxDirective
]);
/**
* @ngdoc directive
* @name materialCheckbox
* @module material.components.checkbox
* @restrict E
*
* @description
* The checkbox directive is used like the normal [angular checkbox](https://docs.angularjs.org/api/ng/input/input%5Bcheckbox%5D).
*
* @param {string} ngModel Assignable angular expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {expression=} ngTrueValue The value to which the expression should be set when selected.
* @param {expression=} ngFalseValue The value to which the expression should be set when not selected.
* @param {string=} ngChange Angular expression to be executed when input changes due to user interaction with the input element.
* @param {boolean=} noink Use of attribute indicates use of ripple ink effects
* @param {boolean=} disabled Use of attribute indicates the switch is disabled: no ink effects and not selectable
* @param {string=} ariaLabel Publish the button label used by screen-readers for accessibility. Defaults to the checkbox's text.
*
* @usage
* <hljs lang="html">
* <material-checkbox ng-model="isChecked" aria-label="Finished?">
* Finished ?
* </material-checkbox>
*
* <material-checkbox noink ng-model="hasInk" aria-label="No Ink Effects">
* No Ink Effects
* </material-checkbox>
*
* <material-checkbox disabled ng-model="isDisabled" aria-label="Disabled">
* Disabled
* </material-checkbox>
*
* </hljs>
*
*/
function MaterialCheckboxDirective(inputDirectives, $materialInkRipple, $aria) {
var inputDirective = inputDirectives[0];
var CHECKED_CSS = 'material-checked';
return {
restrict: 'E',
transclude: true,
require: '?ngModel',
template:
'<div class="material-container" ink-ripple="checkbox">' +
'<div class="material-icon"></div>' +
'</div>' +
'<div ng-transclude class="material-label"></div>',
link: postLink
};
// **********************************************************
// Private Methods
// **********************************************************
function postLink(scope, element, attr, ngModelCtrl) {
var checked = false;
// Create a mock ngModel if the user doesn't provide one
ngModelCtrl = ngModelCtrl || {
$setViewValue: function(value) {
this.$viewValue = value;
},
$parsers: [],
$formatters: []
};
// Reuse the original input[type=checkbox] directive from Angular core.
// This is a bit hacky as we need our own event listener and own render
// function.
attr.type = 'checkbox';
attr.tabIndex = 0;
inputDirective.link(scope, {
on: angular.noop,
0: {}
}, attr, [ngModelCtrl]);
// We can't chain element.attr here because of a bug with jqLite
element.attr(Constant.ARIA.PROPERTY.CHECKED, checked);
element.attr('role', attr.type);
element.attr('tabIndex', attr.tabIndex);
element.on('click', listener);
element.on('keypress', keypressHandler);
ngModelCtrl.$render = render;
$aria.expect(element, Constant.ARIA.PROPERTY.LABEL, element.text());
function keypressHandler(ev) {
if(ev.which === Constant.KEY_CODE.SPACE) {
ev.preventDefault();
listener(ev);
}
}
function listener(ev) {
if (element[0].hasAttribute('disabled')) return;
scope.$apply(function() {
checked = !checked;
ngModelCtrl.$setViewValue(checked, ev && ev.type);
ngModelCtrl.$render();
});
}
function render() {
checked = ngModelCtrl.$viewValue;
element.attr(Constant.ARIA.PROPERTY.CHECKED, checked);
if(checked) {
element.addClass(CHECKED_CSS);
} else {
element.removeClass(CHECKED_CSS);
}
}
}
}
/**
* @ngdoc module
* @name material.components.content
*
* @description
* Scrollable content
*/
angular.module('material.components.content', [
'material.services.registry'
])
.directive('materialContent', [
materialContentDirective
]);
/**
* @ngdoc directive
* @name materialContent
* @module material.components.content
*
* @restrict E
*
* @description
* The `<material-content>` directive is a container element useful for scrollable content
*
* @usage
* <hljs lang="html">
* <material-content class="material-content-padding">
* Lorem ipsum dolor sit amet, ne quod novum mei.
* </material-content>
* </hljs>
*
*/
function materialContentDirective() {
return {
restrict: 'E',
controller: angular.noop,
link: function($scope, $element, $attr) {
$scope.$broadcast('$materialContentLoaded', $element);
}
};
}
/**
* @ngdoc module
* @name material.components.dialog
*/
angular.module('material.components.dialog', [
'material.animations',
'material.services.compiler',
'material.services.aria'
])
.directive('materialDialog', [
'$$rAF',
MaterialDialogDirective
])
.factory('$materialDialog', [
'$timeout',
'$materialCompiler',
'$rootElement',
'$rootScope',
'$materialEffects',
'$animate',
'$aria',
MaterialDialogService
]);
function MaterialDialogDirective($$rAF) {
return {
restrict: 'E',
link: function(scope, element, attr) {
$$rAF(function() {
var content = element[0].querySelector('.dialog-content');
if (content && content.scrollHeight > content.clientHeight) {
element.addClass('dialog-content-overflow');
}
});
}
};
}
/**
* @ngdoc service
* @name $materialDialog
* @module material.components.dialog
*
* @description
*
* The $materialDialog service opens a dialog over top of the app.
*
* The `$materialDialog` service can be used as a function, which when called will open a
* dialog. Note: the dialog is always given an isolate scope.
*
* It takes one argument, `options`, which is defined below.
*
* Note: the dialog's template must have an outer `<material-dialog>` element.
* Inside, use an element with class `dialog-content` for the dialog's content, and use
* an element with class `dialog-actions` for the dialog's actions.
*
* When opened, the `dialog-actions` area will attempt to focus the first button found with
* class `dialog-close`. If no button with `dialog-close` class is found, it will focus the
* last button in the `dialog-actions` area.
*
* @usage
* <hljs lang="html">
* <div ng-controller="MyController">
* <material-button ng-click="openDialog($event)">
* Open a Dialog from this button!
* </material-button>
* </div>
* </hljs>
* <hljs lang="js">
* var app = angular.module('app', ['ngMaterial']);
* app.controller('MyController', function($scope, $materialDialog) {
* $scope.openDialog = function($event) {
* var hideDialog = $materialDialog({
* template: '<material-dialog>Hello!</material-dialog>',
* targetEvent: $event
* });
* };
* });
* </hljs>
*
* @returns {function} `hideDialog` - A function that hides the dialog.
*
* @paramType Options
* @param {string=} templateUrl The url of a template that will be used as the content
* of the dialog.
* @param {string=} template Same as templateUrl, except this is an actual template string.
* @param {DOMClickEvent=} targetEvent A click's event object. When passed in as an option,
* the location of the click will be used as the starting point for the opening animation
* of the the dialog.
* @param {boolean=} hasBackdrop Whether there should be an opaque backdrop behind the dialog.
* Default true.
* @param {boolean=} clickOutsideToClose Whether the user can click outside the dialog to
* close it. Default true.
* @param {boolean=} escapeToClose Whether the user can press escape to close the dialog.
* Default true.
* @param {string=} controller The controller to associate with the dialog. The controller
* will be injected with the local `$hideDialog`, which is a function used to hide the dialog.
* @param {object=} locals An object containing key/value pairs. The keys will be used as names
* of values to inject into the controller. For example, `locals: {three: 3}` would inject
* `three` into the controller, with the value 3.
* @param {object=} resolve Similar to locals, except it takes promises as values, and the
* toast will not open until all of the promises resolve.
* @param {string=} controllerAs An alias to assign the controller to on the scope.
* @param {element=} appendTo The element to append the dialog to. Defaults to appending
* to the root element of the application.
*/
function MaterialDialogService($timeout, $materialCompiler, $rootElement, $rootScope, $materialEffects, $animate, $aria) {
var recentDialog;
var dialogParent = $rootElement.find('body');
if ( !dialogParent.length ) {
dialogParent = $rootElement;
}
return showDialog;
function showDialog(options) {
options = angular.extend({
appendTo: dialogParent,
hasBackdrop: true, // should have an opaque backdrop
clickOutsideToClose: true, // should have a clickable backdrop to close
escapeToClose: true,
// targetEvent: used to find the location to start the dialog from
targetEvent: null,
transformTemplate: function(template) {
return '<div class="material-dialog-container">' + template + '</div>';
}
// Also supports all options from $materialCompiler.compile
}, options || {});
// Incase the user provides a raw dom element, always wrap it in jqLite
options.appendTo = angular.element(options.appendTo);
// Close the old dialog
recentDialog && recentDialog.then(function(destroyDialog) {
destroyDialog();
});
recentDialog = $materialCompiler.compile(options).then(function(compileData) {
// Controller will be passed a `$hideDialog` function
compileData.locals.$hideDialog = destroyDialog;
var scope = $rootScope.$new(true);
var element = compileData.link(scope);
var popInTarget = options.targetEvent && options.targetEvent.target &&
angular.element(options.targetEvent.target);
var closeButton = findCloseButton();
var backdrop;
configureAria(element.find('material-dialog'));
if (options.hasBackdrop) {
backdrop = angular.element('<material-backdrop class="opaque ng-enter">');
$animate.enter(backdrop, options.appendTo, null);
}
$materialEffects.popIn(element, options.appendTo, popInTarget)
.then(function() {
if (options.escapeToClose) {
$rootElement.on('keyup', onRootElementKeyup);
}
if (options.clickOutsideToClose) {
element.on('click', dialogClickOutside);
}
closeButton.focus();
});
return destroyDialog;
function findCloseButton() {
//If no element with class dialog-close, try to find the last
//button child in dialog-actions and assume it is a close button
var closeButton = element[0].querySelector('.dialog-close');
if (!closeButton) {
var actionButtons = element[0].querySelectorAll('.dialog-actions button');
closeButton = actionButtons[ actionButtons.length - 1 ];
}
return angular.element(closeButton);
}
function destroyDialog() {
if (destroyDialog.called) return;
destroyDialog.called = true;
if (backdrop) {
$animate.leave(backdrop);
}
if (options.escapeToClose) {
$rootElement.off('keyup', onRootElementKeyup);
}
if (options.clickOutsideToClose) {
element.off('click', dialogClickOutside);
}
$animate.leave(element).then(function() {
element.remove();
scope.$destroy();
scope = null;
element = null;
if(popInTarget !== null) {
popInTarget.focus();
}
});
}
function onRootElementKeyup(e) {
if (e.keyCode === Constant.KEY_CODE.ESCAPE) {
$timeout(destroyDialog);
}
}
function dialogClickOutside(e) {
// Only close if we click the flex container outside the backdrop
if (e.target === element[0]) {
$timeout(destroyDialog);
}
}
});
/**
* Inject ARIA-specific attributes appropriate for Dialogs
*/
function configureAria(element) {
var ROLE = Constant.ARIA.ROLE;
$aria.update(element, {
'role': ROLE.DIALOG
});
var dialogContent = element.find('.dialog-content');
if(dialogContent.length === 0){
dialogContent = element;
}
var defaultText = Util.stringFromTextBody(dialogContent.text(), 3);
$aria.expect(element, 'aria-label', defaultText);
}
return recentDialog;
}
}
/**
* @ngdoc module
* @name material.components.textField
* @description
* Form
*/
angular.module('material.components.textField', [])
.directive('materialInputGroup', [
materialInputGroupDirective
])
.directive('materialInput', [
materialInputDirective
]);
/**
* @ngdoc directive
* @name materialInputGroup
* @module material.components.textField
* @restrict E
* @description
* Use the `<material-input-group>` directive as the grouping parent of a `<material-input>` element.
*
* @usage
* <hljs lang="html">
* <material-input-group>
* <material-input type="text" ng-model="myText"></material-input>
* </material-input-group>
* </hljs>
*/
function materialInputGroupDirective() {
return {
restrict: 'CE',
controller: ['$element', function($element) {
this.setFocused = function(isFocused) {
$element.toggleClass('material-input-focused', !!isFocused);
};
this.setHasValue = function(hasValue) {
$element.toggleClass('material-input-has-value', !!hasValue);
};
}]
};
}
/**
* @ngdoc directive
* @name materialInput
* @module material.components.textField
*
* @restrict E
*
* @description
* Use the `<material-input>` directive as elements within a `<material-input-group>` container
*
* @usage
* <hljs lang="html">
* <material-input-group>
* <material-input type="text" ng-model="user.fullName"></material-input>
* <material-input type="text" ng-model="user.email"></material-input>
* </material-input-group>
* </hljs>
*/
function materialInputDirective() {
return {
restrict: 'E',
replace: true,
template: '<input>',
require: ['^?materialInputGroup', '?ngModel'],
link: function(scope, element, attr, ctrls) {
var inputGroupCtrl = ctrls[0];
var ngModelCtrl = ctrls[1];
if (!inputGroupCtrl) {
return;
}
// When the input value changes, check if it "has" a value, and
// set the appropriate class on the input group
if (ngModelCtrl) {
//Add a $formatter so we don't use up the render function
ngModelCtrl.$formatters.push(function(value) {
inputGroupCtrl.setHasValue(!!value);
return value;
});
}
element.on('input', function() {
inputGroupCtrl.setHasValue(!!element.val());
});
// When the input focuses, add the focused class to the group
element.on('focus', function(e) {
inputGroupCtrl.setFocused(true);
});
// When the input blurs, remove the focused class from the group
element.on('blur', function(e) {
inputGroupCtrl.setFocused(false);
});
scope.$on('$destroy', function() {
inputGroupCtrl.setFocused(false);
inputGroupCtrl.setHasValue(false);
});
}
};
}
/**
* @ngdoc module
* @name material.components.icon
* @description
* Icon
*/
angular.module('material.components.icon', [])
.directive('materialIcon', [
materialIconDirective
]);
/**
* @ngdoc directive
* @name materialIcon
* @module material.components.icon
*
* @restrict E
*
* @description
* The `<material-icon>` directive is an element useful for SVG icons
*
* @usage
* <hljs lang="html">
* <material-icon icon="/img/icons/ic_access_time_24px.svg">
* </material-icon>
* </hljs>
*
*/
function materialIconDirective() {
return {
restrict: 'E',
template: '<object class="material-icon"></object>',
compile: function(element, attr) {
var object = angular.element(element[0].children[0]);
if(angular.isDefined(attr.icon)) {
object.attr('data', attr.icon);
}
}
};
}
/**
* @ngdoc module
* @name material.components.list
* @description
* List module
*/
angular.module('material.components.list', [])
.directive('materialList', [
materialListDirective
])
.directive('materialItem', [
materialItemDirective
]);
/**
* @ngdoc directive
* @name materialList
* @module material.components.list
*
* @restrict E
*
* @description
* The `<material-list>` directive is a list container for 1..n `<material-item>` tags.
*
* @usage
* <hljs lang="html">
* <material-list>
* <material-item ng-repeat="item in todos">
* <div class="material-tile-left">
* <img ng-src="{{item.face}}" class="face" alt="{{item.who}}">
* </div>
* <div class="material-tile-content">
* <h2>{{item.what}}</h2>
* <h3>{{item.who}}</h3>
* <p>
* {{item.notes}}
* </p>
* </div>
*
* </material-item>
* </material-list>
* </hljs>
*
*/
function materialListDirective() {
return {
restrict: 'E',
link: function($scope, $element, $attr) {
$element.attr({
'role' : Constant.ARIA.ROLE.LIST
});
}
};
}
/**
* @ngdoc directive
* @name materialItem
* @module material.components.list
*
* @restrict E
*
* @description
* The `<material-item>` directive is a container intended for row items in a `<material-list>` container.
*
* @usage
* <hljs lang="html">
* <material-list>
* <material-item>
* Item content in list
* </material-item>
* </material-list>
* </hljs>
*
*/
function materialItemDirective() {
return {
restrict: 'E',
link: function($scope, $element, $attr) {
$element.attr({
'role' : Constant.ARIA.ROLE.LIST_ITEM
});
}
};
}
/**
* @ngdoc module
* @name material.components.radioButton
* @description radioButton module!
*/
angular.module('material.components.radioButton', [
'material.animations',
'material.services.aria'
])
.directive('materialRadioGroup', [
materialRadioGroupDirective
])
.directive('materialRadioButton', [
'$aria',
materialRadioButtonDirective
]);
/**
* @ngdoc directive
* @module material.components.radioButton
* @name materialRadioGroup
*
* @order 0
* @restrict E
*
* @description
* The `<material-radio-group>` directive identifies a grouping
* container for the 1..n grouped material radio buttons; specified using nested
* `<material-radio-button>` tags.
*
* @param {string} ngModel Assignable angular expression to data-bind to.
* @param {boolean=} noink Use of attribute indicates flag to disable ink ripple effects.
*
* @usage
* <hljs lang="html">
* <material-radio-group ng-model="selected">
*
* <material-radio-button
* ng-repeat="d in colorOptions"
* ng-value="d.value" aria-label="{{ d.label }}">
*
* {{ d.label }}
*
* </material-radio-button>
*
* </material-radio-group>
* </hljs>
*
*/
function materialRadioGroupDirective() {
RadioGroupController.prototype = createRadioGroupControllerProto();
return {
restrict: 'E',
controller: RadioGroupController,
require: ['materialRadioGroup', '?ngModel'],
link: link
};
function link(scope, element, attr, ctrls) {
var rgCtrl = ctrls[0],
ngModelCtrl = ctrls[1] || {
$setViewValue: angular.noop
};
function keydownListener(ev) {
if (ev.which === Constant.KEY_CODE.LEFT_ARROW) {
ev.preventDefault();
rgCtrl.selectPrevious(element);
}
else if (ev.which === Constant.KEY_CODE.RIGHT_ARROW) {
ev.preventDefault();
rgCtrl.selectNext(element);
}
}
rgCtrl.init(ngModelCtrl);
element.attr({
'role': Constant.ARIA.ROLE.RADIO_GROUP,
'tabIndex': '0'
})
.on('keydown', keydownListener);
}
function RadioGroupController() {
this._radioButtonRenderFns = [];
}
function createRadioGroupControllerProto() {
return {
init: function(ngModelCtrl) {
this._ngModelCtrl = ngModelCtrl;
this._ngModelCtrl.$render = angular.bind(this, this.render);
},
add: function(rbRender) {
this._radioButtonRenderFns.push(rbRender);
},
remove: function(rbRender) {
var index = this._radioButtonRenderFns.indexOf(rbRender);
if (index !== -1) {
this._radioButtonRenderFns.splice(index, 1);
}
},
render: function() {
this._radioButtonRenderFns.forEach(function(rbRender) {
rbRender();
});
},
setViewValue: function(value, eventType) {
this._ngModelCtrl.$setViewValue(value, eventType);
// update the other radio buttons as well
this.render();
},
getViewValue: function() {
return this._ngModelCtrl.$viewValue;
},
selectNext: function(element) {
return selectButton('next', element);
},
selectPrevious : function(element) {
return selectButton('previous', element);
}
};
}
/**
* Select the grouping parent's next or previous radio/checkbox button.
* NOTE: this uses the iterator.js utility function...
*/
function selectButton( direction, parent, loop ) {
loop = angular.isUndefined(loop) ? true : !!loop;
var buttons = Util.iterator( findAllButtons(parent), loop );
if ( buttons.count() ) {
var selected = findSelectedButton(parent);
var target = !selected ? buttons.first() :
(direction =='previous') ? buttons.previous( selected ) : buttons.next( selected );
if ( target ) {
// Activate radioButton's click listener (triggerHandler won't send an actual click event)
angular.element(target).triggerHandler('click');
}
}
}
/**
* Find all button children for specified element
* NOTE: This guarantees giving us every radio, even grandchildren, and
* us getting them in the proper order.
*/
function findAllButtons(element) {
return Array.prototype.slice.call(
element[0].querySelectorAll('material-radio-button')
);
}
/**
* Find the currently selected button element (if any)
*/
function findSelectedButton(element) {
return element[0].querySelector('material-radio-button.material-checked');
}
}
/**
* @ngdoc directive
* @module material.components.radioButton
* @name materialRadioButton
*
* @order 1
* @restrict E
*
* @description
* The `<material-radio-button>`directive is the child directive required to be used within `<material-radioo-group>` elements.
*
* While similar to the `<input type="radio" ng-model="" value="">` directive,
* the `<material-radio-button>` directive provides material ink effects, ARIA support, and
* supports use within named radio groups.
*
* @param {string} ngModel Assignable angular expression to data-bind to.
* @param {string=} ngChange Angular expression to be executed when input changes due to user
* interaction with the input element.
* @param {string} ngValue Angular expression which sets the value to which the expression should
* be set when selected.*
* @param {string} value The value to which the expression should be set when selected.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} ariaLabel Publish the button label used by screen-readers for accessibility. Defaults to the radio button's text.
*
* @usage
* <hljs lang="html">
*
* <material-radio-button value="1" aria-label="Label 1">
* Label 1
* </material-radio-button>
*
* <material-radio-button ng-model="color" ng-value="specialValue" aria-label="Green">
* Green
* </material-radio-button>
*
* </hljs>
*
*/
function materialRadioButtonDirective($aria) {
var CHECKED_CSS = 'material-checked';
return {
restrict: 'E',
require: '^materialRadioGroup',
transclude: true,
template: '<div class="material-container" ink-ripple="checkbox">' +
'<div class="material-off"></div>' +
'<div class="material-on"></div>' +
'</div>' +
'<div ng-transclude class="material-label"></div>',
link: link
};
function link(scope, element, attr, rgCtrl) {
var lastChecked;
rgCtrl.add(render);
attr.$observe('value', render);
element
.on('click', listener)
.on('$destroy', function() {
rgCtrl.remove(render);
})
.attr('role', Constant.ARIA.ROLE.RADIO);
$aria.expect(element, Constant.ARIA.PROPERTY.LABEL, element.text());
function listener(ev) {
if (element[0].hasAttribute('disabled')) return;
scope.$apply(function() {
rgCtrl.setViewValue(attr.value, ev && ev.type);
});
}
function render() {
var checked = (rgCtrl.getViewValue() === attr.value);
if (checked === lastChecked) {
return;
}
lastChecked = checked;
element.attr(Constant.ARIA.PROPERTY.CHECKED, checked);
if (checked) {
element.addClass(CHECKED_CSS);
} else {
element.removeClass(CHECKED_CSS);
}
}
}
}
/**
* @ngdoc module
* @name material.components.sidenav
*
* @description
* A Sidenav QP component.
*/
angular.module('material.components.sidenav', [
'material.services.registry'
])
.factory('$materialSidenav', [
'$materialComponentRegistry',
materialSidenavService
])
.directive('materialSidenav', [
'$timeout',
materialSidenavDirective
])
.controller('$materialSidenavController', [
'$scope',
'$element',
'$attrs',
'$timeout',
'$materialSidenav',
'$materialComponentRegistry',
materialSidenavController
]);
/**
* @private
* @ngdoc object
* @name materialSidenavController
* @module material.components.sidenav
*
* @description
* The controller for materialSidenav components.
*/
function materialSidenavController($scope, $element, $attrs, $timeout, $materialSidenav, $materialComponentRegistry) {
var self = this;
$materialComponentRegistry.register(this, $attrs.componentId);
this.isOpen = function() {
return !!$scope.isOpen;
};
/**
* Toggle the side menu to open or close depending on its current state.
*/
this.toggle = function() {
$scope.isOpen = !$scope.isOpen;
};
/**
* Open the side menu
*/
this.open = function() {
$scope.isOpen = true;
};
/**
* Close the side menu
*/
this.close = function() {
$scope.isOpen = false;
};
}
/**
* @private
* @ngdoc service
* @name $materialSidenav
* @module material.components.sidenav
*
* @description
* $materialSidenav makes it easy to interact with multiple sidenavs
* in an app.
*
* @usage
*
* ```javascript
* // Toggle the given sidenav
* $materialSidenav(componentId).toggle();
*
* // Open the given sidenav
* $materialSidenav(componentId).open();
*
* // Close the given sidenav
* $materialSidenav(componentId).close();
* ```
*/
function materialSidenavService($materialComponentRegistry) {
return function(handle) {
var instance = $materialComponentRegistry.get(handle);
if(!instance) {
$materialComponentRegistry.notFoundError(handle);
}
return {
isOpen: function() {
if (!instance) { return; }
return instance.isOpen();
},
/**
* Toggle the given sidenav
* @param handle the specific sidenav to toggle
*/
toggle: function() {
if(!instance) { return; }
instance.toggle();
},
/**
* Open the given sidenav
* @param handle the specific sidenav to open
*/
open: function(handle) {
if(!instance) { return; }
instance.open();
},
/**
* Close the given sidenav
* @param handle the specific sidenav to close
*/
close: function(handle) {
if(!instance) { return; }
instance.close();
}
};
};
}
/**
* @ngdoc directive
* @name materialSidenav
* @module material.components.sidenav
* @restrict E
*
* @description
*
* A Sidenav component that can be opened and closed programatically.
*
* When used properly with a layout, it will seamleslly stay open on medium
* and larger screens, while being hidden by default on mobile devices.
*
* @usage
* <hljs lang="html">
* <div layout="horizontal" ng-controller="MyController">
* <material-sidenav class="material-sidenav-left">
* Left Nav!
* </material-sidenav>
*
* <material-content>
* Center Content
* <material-button ng-click="openLeftMenu()">
* Open Left Menu
* </material-button>
* </material-content>
*
* <material-sidenav class="material-sidenav-right">
* Right Nav!
* </material-sidenav>
* </div>
* </hljs>
*
* <hljs lang="js">
* var app = angular.module('myApp', ['ngMaterial']);
* app.controller('MainController', function($scope, $materialSidenav) {
* $scope.openLeftMenu = function() {
* $materialSidenav('left').toggle();
* };
* });
* </hljs>
*/
function materialSidenavDirective($timeout) {
return {
restrict: 'E',
scope: {},
controller: '$materialSidenavController',
link: function($scope, $element, $attr, sidenavCtrl) {
var backdrop = angular.element('<material-backdrop class="material-sidenav-backdrop">');
$scope.$watch('isOpen', onShowHideSide);
/**
* Toggle the SideNav view and attach/detach listeners
* @param isOpen
*/
function onShowHideSide(isOpen) {
var parent = $element.parent();
$element.toggleClass('open', !!isOpen);
if (isOpen) {
parent.append(backdrop);
backdrop.on( EVENT.CLICK, onBackdropClick );
parent.on( EVENT.KEY_DOWN, onKeyDown );
} else {
backdrop.remove();
backdrop.off( EVENT.CLICK, onBackdropClick);
parent.off( EVENT.KEY_DOWN, onKeyDown );
}
}
/**
* Auto-close the sideNav when the backdrop mask is clicked
*/
function onBackdropClick() {
close();
}
/**
* Auto-close sideNav when the `escape` key is pressed.
* @param evt
*/
function onKeyDown(evt) {
if(evt.which === Constant.KEY_CODE.ESCAPE){
close();
evt.preventDefault();
evt.stopPropagation();
}
}
/**
* With backdrop `clicks` or `escape` key-press, immediately
* apply the CSS close transition... Then notify the controller
* to close() and perform its own actions.
*/
function close() {
onShowHideSide( false );
$timeout(function(){
sidenavCtrl.close();
});
}
}
};
}
/**
* @ngdoc module
* @name material.components.slider
*/
angular.module('material.components.slider', [
'material.animations',
'material.services.aria'
])
.directive('materialSlider', [
SliderDirective
]);
/**
* @ngdoc directive
* @name materialSlider
* @module material.components.slider
* @restrict E
* @description
* The `<material-slider>` component allows the user to choose from a range of
* values.
*
* It has two modes: 'normal' mode, where the user slides between a wide range
* of values, and 'discrete' mode, where the user slides between only a few
* select values.
*
* To enable discrete mode, add the `discrete` attribute to a slider,
* and use the `step` attribute to change the distance between
* values the user is allowed to pick.
*
* @usage
* <h4>Normal Mode</h4>
* <hljs lang="html">
* <material-slider ng-model="myValue" min="5" max="500">
* </material-slider>
* </hljs>
* <h4>Discrete Mode</h4>
* <hljs lang="html">
* <material-slider discrete ng-model="myDiscreteValue" step="10" min="10" max="130">
* </material-slider>
* </hljs>
*
* @param {boolean=} discrete Whether to enable discrete mode.
* @param {number=} step The distance between values the user is allowed to pick. Default 1.
* @param {number=} min The minimum value the user is allowed to pick. Default 0.
* @param {number=} max The maximum value the user is allowed to pick. Default 100.
*/
function SliderDirective() {
return {
scope: {},
require: ['?ngModel', 'materialSlider'],
controller: [
'$scope',
'$element',
'$attrs',
'$$rAF',
'$timeout',
'$window',
'$materialEffects',
'$aria',
SliderController
],
template:
'<div class="slider-track-container">' +
'<div class="slider-track"></div>' +
'<div class="slider-track slider-track-fill"></div>' +
'<div class="slider-track-ticks"></div>' +
'</div>' +
'<div class="slider-thumb-container">' +
'<div class="slider-thumb"></div>' +
'<div class="slider-focus-thumb"></div>' +
'<div class="slider-focus-ring"></div>' +
'<div class="slider-sign">' +
'<span class="slider-thumb-text" ng-bind="modelValue"></span>' +
'</div>' +
'<div class="slider-disabled-thumb"></div>' +
'</div>',
link: postLink
};
function postLink(scope, element, attr, ctrls) {
var ngModelCtrl = ctrls[0] || {
// Mock ngModelController if it doesn't exist to give us
// the minimum functionality needed
$setViewValue: function(val) {
this.$viewValue = val;
this.$viewChangeListeners.forEach(function(cb) { cb(); });
},
$parsers: [],
$formatters: [],
$viewChangeListeners: []
};
var sliderCtrl = ctrls[1];
sliderCtrl.init(ngModelCtrl);
}
}
/**
* We use a controller for all the logic so that we can expose a few
* things to unit tests
*/
function SliderController(scope, element, attr, $$rAF, $timeout, $window, $materialEffects, $aria) {
this.init = function init(ngModelCtrl) {
var thumb = angular.element(element[0].querySelector('.slider-thumb'));
var thumbContainer = thumb.parent();
var trackContainer = angular.element(element[0].querySelector('.slider-track-container'));
var activeTrack = angular.element(element[0].querySelector('.slider-track-fill'));
var tickContainer = angular.element(element[0].querySelector('.slider-track-ticks'));
// Default values, overridable by attrs
attr.min ? attr.$observe('min', updateMin) : updateMin(0);
attr.max ? attr.$observe('max', updateMax) : updateMax(100);
attr.step ? attr.$observe('step', updateStep) : updateStep(1);
// We have to manually stop the $watch on ngDisabled because it exists
// on the parent scope, and won't be automatically destroyed when
// the component is destroyed.
var stopDisabledWatch = angular.noop;
if (attr.ngDisabled) {
stopDisabledWatch = scope.$parent.$watch(attr.ngDisabled, updateAriaDisabled);
} else {
updateAriaDisabled(!!attr.disabled);
}
$aria.expect(element, 'aria-label');
element.attr('tabIndex', 0);
element.attr('role', Constant.ARIA.ROLE.SLIDER);
element.on('keydown', keydownListener);
var hammertime = new Hammer(element[0], {
recognizers: [
[Hammer.Pan, { direction: Hammer.DIRECTION_HORIZONTAL }]
]
});
hammertime.on('hammer.input', onInput);
hammertime.on('panstart', onPanStart);
hammertime.on('pan', onPan);
// On resize, recalculate the slider's dimensions and re-render
var updateAll = $$rAF.debounce(function() {
refreshSliderDimensions();
ngModelRender();
redrawTicks();
});
updateAll();
angular.element($window).on('resize', updateAll);
scope.$on('$destroy', function() {
angular.element($window).off('resize', updateAll);
hammertime.destroy();
stopDisabledWatch();
});
ngModelCtrl.$render = ngModelRender;
ngModelCtrl.$viewChangeListeners.push(ngModelRender);
ngModelCtrl.$formatters.push(minMaxValidator);
ngModelCtrl.$formatters.push(stepValidator);
/**
* Attributes
*/
var min;
var max;
var step;
function updateMin(value) {
min = parseFloat(value);
element.attr('aria-valuemin', value);
}
function updateMax(value) {
max = parseFloat(value);
element.attr('aria-valuemax', value);
}
function updateStep(value) {
step = parseFloat(value);
redrawTicks();
}
function updateAriaDisabled(isDisabled) {
element.attr('aria-disabled', !!isDisabled);
}
// Draw the ticks with canvas.
// The alternative to drawing ticks with canvas is to draw one element for each tick,
// which could quickly become a performance bottleneck.
var tickCanvas, tickCtx;
function redrawTicks() {
if (!angular.isDefined(attr.discrete)) return;
var numSteps = Math.floor( (max - min) / step );
if (!tickCanvas) {
tickCanvas = angular.element('<canvas style="position:absolute;">');
tickCtx = tickCanvas[0].getContext('2d');
tickCtx.fillStyle = 'black';
tickContainer.append(tickCanvas);
}
var dimensions = getSliderDimensions();
tickCanvas[0].width = dimensions.width;
tickCanvas[0].height = dimensions.height;
var distance;
for (var i = 0; i <= numSteps; i++) {
distance = Math.floor(dimensions.width * (i / numSteps));
tickCtx.fillRect(distance - 1, 0, 2, dimensions.height);
}
}
/**
* Refreshing Dimensions
*/
var sliderDimensions = {};
var debouncedRefreshDimensions = Util.debounce(refreshSliderDimensions, 5000);
refreshSliderDimensions();
function refreshSliderDimensions() {
sliderDimensions = trackContainer[0].getBoundingClientRect();
}
function getSliderDimensions() {
debouncedRefreshDimensions();
return sliderDimensions;
}
/**
* left/right arrow listener
*/
function keydownListener(ev) {
var changeAmount;
if (ev.which === Constant.KEY_CODE.LEFT_ARROW) {
changeAmount = -step;
} else if (ev.which === Constant.KEY_CODE.RIGHT_ARROW) {
changeAmount = step;
}
if (changeAmount) {
if (ev.metaKey || ev.ctrlKey || ev.altKey) {
changeAmount *= 4;
}
ev.preventDefault();
ev.stopPropagation();
scope.$evalAsync(function() {
setModelValue(ngModelCtrl.$viewValue + changeAmount);
});
}
}
/**
* ngModel setters and validators
*/
function setModelValue(value) {
ngModelCtrl.$setViewValue( minMaxValidator(stepValidator(value)) );
}
function ngModelRender() {
var percent = (ngModelCtrl.$viewValue - min) / (max - min);
scope.modelValue = ngModelCtrl.$viewValue;
element.attr('aria-valuenow', ngModelCtrl.$viewValue);
setSliderPercent(percent);
}
function minMaxValidator(value) {
if (angular.isNumber(value)) {
return Math.max(min, Math.min(max, value));
}
}
function stepValidator(value) {
if (angular.isNumber(value)) {
return Math.round(value / step) * step;
}
}
/**
* @param percent 0-1
*/
function setSliderPercent(percent) {
activeTrack.css('width', (percent * 100) + '%');
thumbContainer.css(
$materialEffects.TRANSFORM,
'translate3d(' + getSliderDimensions().width * percent + 'px,0,0)'
);
element.toggleClass('slider-min', percent === 0);
}
/**
* Slide listeners
*/
var isSliding = false;
function onInput(ev) {
if (!isSliding && ev.eventType === Hammer.INPUT_START &&
!element[0].hasAttribute('disabled')) {
isSliding = true;
element.addClass('active');
element[0].focus();
refreshSliderDimensions();
doSlide(ev.center.x);
} else if (isSliding && ev.eventType === Hammer.INPUT_END) {
isSliding = false;
element.removeClass('panning active');
}
}
function onPanStart() {
if (!isSliding) return;
element.addClass('panning');
}
function onPan(ev) {
if (!isSliding) return;
doSlide(ev.center.x);
ev.preventDefault();
}
/**
* Expose for testing
*/
this._onInput = onInput;
this._onPanStart = onPanStart;
this._onPan = onPan;
function doSlide(x) {
var percent = (x - sliderDimensions.left) / (sliderDimensions.width);
scope.$evalAsync(function() { setModelValue(min + percent * (max - min)); });
}
};
}
/**
* @ngdoc module
* @name material.components.switch
*/
angular.module('material.components.switch', [
'material.components.checkbox',
'material.components.radioButton'
])
.directive('materialSwitch', [
'materialCheckboxDirective',
'materialRadioButtonDirective',
MaterialSwitch
]);
/**
* @ngdoc directive
* @module material.components.switch
* @name materialSwitch
* @restrict E
*
* The switch directive is used very much like the normal [angular checkbox](https://docs.angularjs.org/api/ng/input/input%5Bcheckbox%5D).
*
* @param {string} ngModel Assignable angular expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {expression=} ngTrueValue The value to which the expression should be set when selected.
* @param {expression=} ngFalseValue The value to which the expression should be set when not selected.
* @param {string=} ngChange Angular expression to be executed when input changes due to user interaction with the input element.
* @param {boolean=} noink Use of attribute indicates use of ripple ink effects.
* @param {boolean=} disabled Use of attribute indicates the switch is disabled: no ink effects and not selectable
* @param {string=} ariaLabel Publish the button label used by screen-readers for accessibility. Defaults to the switch's text.
*
* @usage
* <hljs lang="html">
* <material-switch ng-model="isActive" aria-label="Finished?">
* Finished ?
* </material-switch>
*
* <material-switch noink ng-model="hasInk" aria-label="No Ink Effects">
* No Ink Effects
* </material-switch>
*
* <material-switch disabled ng-model="isDisabled" aria-label="Disabled">
* Disabled
* </material-switch>
*
* </hljs>
*/
function MaterialSwitch(checkboxDirectives, radioButtonDirectives) {
var checkboxDirective = checkboxDirectives[0];
var radioButtonDirective = radioButtonDirectives[0];
return {
restrict: 'E',
transclude: true,
template:
'<div class="material-switch-bar"></div>' +
'<div class="material-switch-thumb">' +
radioButtonDirective.template +
'</div>',
require: '?ngModel',
compile: compile
};
function compile(element, attr) {
var thumb = angular.element(element[0].querySelector('.material-switch-thumb'));
//Copy down disabled attributes for checkboxDirective to use
thumb.attr('disabled', attr.disabled);
thumb.attr('ngDisabled', attr.ngDisabled);
return function postLink(scope, element, attr, ngModelCtrl) {
checkboxDirective.link(scope, thumb, attr, ngModelCtrl);
};
}
}
/**
* @ngdoc module
* @name material.components.tabs
* @description
*
* Tabs
*/
angular.module('material.components.tabs', [
'material.animations',
'material.services.attrBind',
'material.services.registry'
]);
/**
* Conditionally configure ink bar animations when the
* tab selection changes. If `nobar` then do not show the
* bar nor animate.
*/
function linkTabInk(scope, element, tabsCtrl, $q, $materialEffects) {
// TODO scope.nostretch
if ( scope.nobar ) return;
// Single inkBar is used for all tabs
var tabsHeader = findNode('.tabs-header-items-container', element); // excludes paginators
var inkBar = findNode("material-ink-bar", element);
var lastLeft = 0;
// Immediately place the ink bar
updateInkBar(true);
// Delay inkBar updates 1-frame until pagination updates...
return updateInkBar;
/**
* Update the position and size of the ink bar based on the
* specified tab DOM element. If all tabs have been removed, then
* hide the inkBar.
*
* @param tab
* @param skipAnimation
*/
function updateInkBar( immediate ) {
var selButton = tabsCtrl.selectedElement();
var showInk = selButton && selButton.length && angular.isDefined(inkBar);
var isHiding = selButton && selButton.hasClass('pagination-hide');
var styles = { display : 'none', width : '0px' };
var left = 0, width = 0;
if ( !showInk || isHiding ) {
// no animation
inkBar.toggleClass('animate', (immediate !== true))
.css({
display : 'none',
width : '0px'
});
} else {
// Just a linear animation...
width = selButton.prop('offsetWidth');
left = tabsHeader.prop('offsetLeft') + (scope.pagingOffset || 0) + selButton.prop('offsetLeft');
styles = {
display : width > 0 ? 'block' : 'none',
width: width + 'px'
};
styles[$materialEffects.TRANSFORM] = 'translate3d(' + left + 'px,0,0)';
// Before we update the CSS to create a linear slide effect,
// let's add/remove `animate` class for transition & duration
inkBar
.toggleClass('animate', (immediate !== true) )
.css(styles)
}
// Listen for CSS transition completion and announce
var dfd = $q.defer();
inkBar.one( $materialEffects.TRANSITIONEND_EVENT, function() {
dfd.resolve({ width: width, left:left });
});
return dfd.promise;
}
}
/**
* Configure pagination and add listeners for tab changes
* and Tabs width changes...
*
* @returns {updatePagination}
*/
function linkTabPagination(scope, element, tabsCtrl, $q, $materialEffects ) {
// TODO allow configuration of TAB_MIN_WIDTH
var TAB_MIN_WIDTH = 8 * 12; // Must match tab min-width rule in _tabs.scss
var PAGINATORS_WIDTH = (8 * 4) * 2; // Must match (2 * width of paginators) in scss
var tabsHeader = findNode('.tabs-header-items-container', element); // excludes paginators
var buttonBar = findNode('.tabs-header-items', element);
var pagination = scope.pagination = {
page : 0,
next: function() {
// selectPageAt(pagination.page + 1);
tabsCtrl.selectAt( pagination.endIndex + 1 );
},
prev: function() {
// selectPageAt(pagination.page - 1);
tabsCtrl.selectAt( pagination.startIndex - 1 );
}
};
scope.$on( EVENT.FOCUS_CHANGED, function() {
});
return updatePagination;
/**
* When the window resizes [`resize`] or the tabs are added/removed
* [$materialTabsChanged], then calculate pagination-width and
* update both the current page (if needed) and the tab headers width...
*
* @returns Promise that is resolved when the pagination transition finishes
*/
function updatePagination() {
var dfd = $q.defer();
var tabs = buttonBar.children();
var tabsWidth = element.prop('clientWidth') - PAGINATORS_WIDTH;
var needPagination = (tabsWidth > 0) && ((TAB_MIN_WIDTH * tabs.length) > tabsWidth);
var paginationToggled = (needPagination !== pagination.active);
if (tabsWidth <= 0) {
//tabsWidth is 0 on initial load. Just instantly resolve the promise if it's 0
return $q.when();
}
pagination.active = needPagination;
if (needPagination) {
pagination.pagesCount = Math.ceil((TAB_MIN_WIDTH * tabs.length) / tabsWidth);
pagination.itemsPerPage = Math.max(1, Math.floor(tabs.length / pagination.pagesCount));
pagination.tabWidth = tabsWidth / pagination.itemsPerPage;
// If we just activated pagination, go to page 0 and watch the
// selected tab index to be sure we're on the same page
var pageIndex = getPageAtTabIndex(scope.$selIndex);
var pageChange = (pagination.page != pageIndex);
// Manually set width of page...
buttonBar.css('width', pagination.tabWidth * tabs.length + 'px');
selectPageAt( pageIndex );
// If pagination.page changed, we need to wait for the transition to complete
// before we announce status [and potentially update focus]
if ( pageChange ) {
tabsHeader.one($materialEffects.TRANSITIONEND_EVENT, function() {
dfd.resolve(pageIndex);
});
} else {
dfd.resolve(pageIndex);
}
} else {
if (paginationToggled) {
// Release buttonBar to be self-adjust to size of all tab buttons
// Slide tab buttons to show all buttons (starting at first)
buttonBar.css('width', '');
selectPageAt( 0 );
}
dfd.resolve(0);
}
return dfd.promise;
}
/**
* Select the specified page in the page group and
* also change the selected the tab if the current
* tab selected is **not** within the new page range.
*
* @param page
*/
function selectPageAt(page, updateTabSelection) {
var lastPage = pagination.pagesCount - 1;
var lastTab = buttonBar.children().length - 1;
if ( page < 0 ) page = 0;
if ( page > lastPage ) page = lastPage;
pagination.page = page;
pagination.startIndex = !pagination.active ? 0 : page * pagination.itemsPerPage;
pagination.endIndex = !pagination.active ? lastTab : pagination.startIndex + pagination.itemsPerPage - 1;
pagination.hasPrev = !pagination.active ? false : page > 0;
pagination.hasNext = !pagination.active ? false : (page + 1) < pagination.pagesCount;
slideTabButtons( -page * pagination.itemsPerPage * pagination.tabWidth );
}
/**
* Determine the associated page for the specified tab index
* @param tabIndex
*/
function getPageAtTabIndex( tabIndex ) {
var numPages = pagination.pagesCount;
var lastTab = (pagination.itemsPerPage * pagination.pagesCount) - 1;
var lastPage = pagination.pagesCount - 1;
return (numPages < 1) ? -1 :
(tabIndex < 0) ? 0 :
(tabIndex > lastTab) ? lastPage : Math.floor(tabIndex / pagination.itemsPerPage);
}
/**
* Perform animated CSS translation of the tab buttons container
* @param xOffset
*/
function slideTabButtons( xOffset ) {
if ( scope.pagingOffset == xOffset ) return;
if ( isNaN(xOffset) ) xOffset = 0;
scope.pagingOffset = xOffset;
buttonBar.css( $materialEffects.TRANSFORM, 'translate3d(' + xOffset + 'px,0,0)');
}
/**
* Is the specified tabIndex with the tab range allowed
* for the current page/pagination?
*
* @param tabIndex
* @returns {boolean}
*/
function isTabInRange( tabIndex ){
return (tabIndex >= pagination.startIndex) &&
(tabIndex <= pagination.endIndex);
}
}
angular.module('material.components.tabs')
.directive('materialTab', [
'$attrBind',
'$aria',
'$materialInkRipple',
TabDirective
]);
/**
* @ngdoc directive
* @name materialTab
* @module material.components.tabs
* @order 1
*
* @restrict E
*
* @description
* `<material-tab>` is the nested directive used [within `<material-tabs>`] to specify each tab with a **label** and optional *view content*
*
* If the `label` attribute is not specified, then an optional `<material-tab-label>` tag can be used to specified more
* complex tab header markup. If neither the **label** nor the **material-tab-label** are specified, then the nested
* markup of the `<material-tab>` is used as the tab header markup.
*
* If a tab **label** has been identified, then any **non-**`<material-tab-label>` markup
* will be considered tab content and will be transcluded to the internal `<div class="tabs-content">` container.
*
* This container is used by the TabsController to show/hide the active tab's content view. This synchronization is
* automatically managed by the internal TabsController whenever the tab selection changes. Selection changes can
* be initiated via data binding changes, programmatic invocation, or user gestures.
*
* @param {string=} label Optional attribute to specify a simple string as the tab label
* @param {boolean=} active Flag indicates if the tab is currently selected; normally the `<material-tabs selected="">`; attribute is used instead.
* @param {boolean=} ngDisabled Flag indicates if the tab is disabled: not selectable with no ink effects
* @param {expression=} deselected Expression to be evaluated after the tab has been de-selected.
* @param {expression=} selected Expression to be evaluated after the tab has been selected.
*
*
* @usage
*
* <hljs lang="html">
* <material-tab label="" disabled="" selected="" deselected="" >
* <h3>My Tab content</h3>
* </material-tab>
*
* <material-tab >
* <material-tab-label>
* <h3>My Tab content</h3>
* </material-tab-label>
* <p>
* Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium,
* totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae
* dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit,
* sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.
* </p>
* </material-tab>
* </hljs>
*
*/
function TabDirective( $attrBind, $aria, $materialInkRipple) {
var noop = angular.noop;
return {
restrict: 'E',
replace: false,
require: "^materialTabs",
transclude: 'true',
scope: true,
link: linkTab,
template:
'<material-tab-label></material-tab-label>'
};
function linkTab(scope, element, attrs, tabsCtrl, $transclude) {
var defaults = { active: false, disabled: false, deselected: noop, selected: noop };
// Since using scope=true for inherited new scope,
// then manually scan element attributes for forced local mappings...
$attrBind(scope, attrs, {
label: '@?',
active: '=?',
disabled: '=?ngDisabled',
deselected: '&onDeselect',
selected: '&onSelect'
}, defaults);
scope.$watch('active', function(isActive) {
element.toggleClass('active', isActive);
});
$materialInkRipple.attachButtonBehavior(element);
configureWatchers();
updateTabContent(scope);
// Update ARIA values for each tab element
configureAria(element, scope);
element.on('click', function onRequestSelect()
{
// Click support for entire <material-tab /> element
if ( !scope.disabled ) tabsCtrl.select(scope);
else tabsCtrl.focusSelected();
})
.on('keydown', function onRequestSelect(event)
{
if (event.which == Constant.KEY_CODE.SPACE ) tabsCtrl.select(scope);
else if (event.which === Constant.KEY_CODE.LEFT_ARROW) tabsCtrl.previous(scope);
else if (event.which === Constant.KEY_CODE.RIGHT_ARROW) tabsCtrl.next(scope);
});
tabsCtrl.add(scope, element);
// **********************************************************
// Private Methods
// **********************************************************
/**
* Inject ARIA-specific attributes appropriate for each Tab button
*/
function configureAria( element, scope ){
var ROLE = Constant.ARIA.ROLE;
scope.ariaId = buildAriaID();
$aria.update( element, {
'id' : scope.ariaId,
'role' : ROLE.TAB,
'aria-selected' : false,
'aria-controls' : "content_" + scope.ariaId
});
/**
* Build a unique ID for each Tab that will be used for WAI-ARIA.
* Preserve existing ID if already specified.
* @returns {*|string}
*/
function buildAriaID() {
return attrs.id || ( ROLE.TAB + "_" + tabsCtrl.$scope.$id + "_" + scope.$id );
}
}
/**
* Auto select the next tab if the current tab is active and
* has been disabled.
*
* Set tab index for the current tab (0), with all other tabs
* outside of the tab order (-1)
*
*/
function configureWatchers() {
var unwatch = scope.$watch('disabled', function (isDisabled) {
if (scope.active && isDisabled) {
tabsCtrl.next(scope);
}
});
scope.$watch('active', function (isActive) {
$aria.update( element, {
'aria-selected' : isActive,
'tabIndex' : isActive === true ? 0 : -1
});
});
scope.$on("$destroy", function () {
unwatch();
tabsCtrl.remove(scope);
});
}
/**
* Transpose the optional `label` attribute value or materialTabHeader or `content` body
* into the body of the materialTabButton... all other content is saved in scope.content
* and used by TabsController to inject into the `tabs-content` container.
*/
function updateTabContent(scope) {
var tab = scope;
// Check to override label attribute with the content of the <material-tab-header> node,
// If a materialTabHeader is not specified, then the node will be considered
// a <material-view> content element...
$transclude(function ( contents ) {
// Transient references...
tab.content = [ ];
angular.forEach(contents, function (node) {
if (!isNodeEmpty(node)) {
if (isNodeType(node, 'material-tab-label')) {
// Simulate use of `label` attribute
tab.label = node.childNodes;
} else {
// Transient references...
//
// Attach to scope for future transclusion into materialView(s)
// We need the bound scope for the content elements; which is NOT
// the scope of tab or material-view container...
tab.content.push(node);
}
}
});
});
// Prepare to assign the materialTabButton content
// Use the label attribute or fallback to TabHeader content
var cntr = angular.element(element[0].querySelector('material-tab-label'));
if (angular.isDefined(scope.label)) {
// The `label` attribute is the default source
cntr.append(scope.label);
delete scope.label;
} else {
// NOTE: If not specified, all markup and content is assumed
// to be used for the tab label.
angular.forEach(scope.content, function (node) {
cntr.append(node);
});
delete scope.content;
}
}
}
}
angular.module('material.components.tabs')
.factory('$materialTabs', [
'$materialComponentRegistry',
TabsService
])
.controller('materialTabsController', [
'$scope',
'$attrs',
'$materialComponentRegistry',
'$timeout',
TabsController
]);
/**
* @private
* @ngdoc service
* @name $materialTabs
* @module material.components.tabs
*
* @description
* $materialTabs makes it easy to programmatically interact with a specific Tabs group
* in an app.
*
* @usage
*
* ```javascript
* // Toggle the given sidenav
* $materialTabs(tabsID).select(0);
* ```
*/
function TabsService($materialComponentRegistry) {
return function(handle) {
var instance = $materialComponentRegistry.get(handle);
if(!instance) {
$materialComponentRegistry.notFoundError(handle);
}
return {
/**
* Select the tab at the specified index
* @param index
* @returns {*}
*/
select: function(index) {
return instance && instance.selectAt(index);
}
};
};
}
/**
* @ngdoc object
* @name materialTabsController
* @module material.components.tabs
* @description Controller used within `<material-tabs>` to manage tab selection and iteration
*
* @private
*/
function TabsController($scope, $attrs, $materialComponentRegistry, $timeout ) {
var list = Util.iterator([], false),
componentID = "tabs" + $scope.$id,
elements = { },
selected = null,
self = this;
// Property for child access
self.noink = !!$scope.noink;
self.nobar = !!$scope.nobar;
self.scope = $scope;
// Special internal accessor to access scopes and tab `content`
// Used by TabsDirective::buildContentItems()
self.$scope = $scope;
self.$$tabs = findTabs;
self.$$hash = "";
// Methods used by <material-tab> and children
self.add = addTab;
self.remove = removeTab;
self.select = selectTab;
self.selectAt = selectTabAt;
self.next = selectNext;
self.previous = selectPrevious;
self.focusSelected = focusSelected;
self.focusNext = focusNext;
self.focusPrevious = focusPrevious;
self.selectedElement = selectedElement;
$materialComponentRegistry.register( self, $attrs.componentId || componentID );
/**
* Accessor to look up the associated
* @returns {*}
*/
function selectedElement() {
return findElementFor( selected );
};
/**
* When the selected tab changes, broadcast notification
*/
function onSelectedChange() {
if (onSelectedChange.queued) return;
onSelectedChange.queued = true;
$scope.$evalAsync(function() {
$scope.$broadcast(EVENT.TABS_CHANGED, selected);
onSelectedChange.queued = false;
});
}
/**
* Make sure the currently selected tab is
* focused. Do not! announce focus changes..
*
* NOTE: this is primarily used within pagination/ink updates after
* tab click handlers. @see tabsDirective.js
* @returns {*}
*/
function focusSelected() {
return focusOn('current');
}
/**
* Focus on the next enabled tab relative to `from`
* Announce focus change with new focusIndex if appropriate
* @param from
*/
function focusNext(from) {
var focusIndex = focusOn('next', from);
if ( focusIndex != list.indexOf(selected)) {
// Announce focus change
$scope.$broadcast(EVENT.FOCUS_CHANGED, focusIndex);
}
return focusIndex;
}
/**
* Focus on the previous enabled tab relative to `from`
* Announce focus change with new focusIndex if appropriate
* @param from
*/
function focusPrevious(from) {
var focusIndex = focusOn('previous', from );
if ( focusIndex != list.indexOf(selected)) {
// Announce focus change
$scope.$broadcast(EVENT.FOCUS_CHANGED, focusIndex);
}
return focusIndex;
}
/**
* Find the DOM element associated with the tab/scope
* @param tab
* @returns {*}
*/
function findElementFor(tab) {
if ( angular.isUndefined(tab) ) {
tab = selected;
}
return tab ? elements[ tab.$id ] : undefined;
}
/**
* Publish array of tab scope items
* NOTE: Tabs are not required to have `contents` and the
* node may be undefined.
* @returns {*} Array
*/
function findTabs(filterBy) {
return list.items().filter(filterBy || angular.identity);
}
/**
* Create unique hashKey representing all available
* tabs.
*/
function updateHash() {
self.$$hash = list.items()
.map(function (it) {
return it.$id;
})
.join(',');
}
/**
* Select specified tab; deselect all others (if any selected)
* @param tab
*/
function selectTab(tab, noUpdate) {
if ( tab == selected ) return;
var activate = makeActivator(true),
deactivate = makeActivator(false);
// Turn off all tabs (if current active)
angular.forEach(list.items(), deactivate);
if ( tab != null ) {
// Activate the specified tab (or next available)
selected = activate(tab.disabled ? list.next(tab, isEnabled) : tab);
// update external models and trigger databinding watchers
$scope.$selIndex = selected ? String(selected.$index || list.indexOf(selected)) : -1;
// update the tabs ink to indicate the selected tab
if (!noUpdate) {
onSelectedChange();
}
}
return selected;
}
/**
* Select tab based on its index position
* @param index
*/
function selectTabAt(index, noUpdate) {
if (list.inRange(index)) {
var matches = list.findBy("$index", index),
it = matches ? matches[0] : null;
if (it != selected) {
// Tab must be selectable...
if ( !isEnabled(it) ) {
it = selectNext(it);
}
selectTab( it || list.first(), noUpdate );
}
}
}
/**
* Add tab to list and auto-select; default adds item to end of list
* @param tab
*/
function addTab(tab, element) {
if (angular.isUndefined(tab.$index)) {
tab.$index = list.count();
}
// cache materialTab DOM element; these are not materialView elements
elements[ tab.$id ] = element;
if (!list.contains(tab)) {
var pos = list.add(tab, tab.$index);
// Should we auto-select it?
if ($scope.$selIndex == pos || tab.active) {
selectTab(tab);
} else {
onSelectedChange();
}
}
updateHash();
return tab.$index;
}
/**
* Remove the specified tab from the list
* Auto select the next tab or the previous tab (if last)
* @param tab
*/
function removeTab(tab) {
if (list.contains(tab)) {
selectTab( list.next(tab, isEnabled) || list.previous(tab, isEnabled) );
list.remove(tab);
onSelectedChange();
// another tab was removed, make sure to update ink bar
$timeout(function(){
delete elements[tab.$id];
},300);
}
updateHash();
}
/**
* Focus on the specified tab (if available)
* @returns {*} Tab
*/
function focusOn(which, from) {
var tab = (which === 'current' ) ? selected :
(which === 'next') ? list.next(from || selected, isEnabled) :
(which === 'previous') ? list.previous(from || selected, isEnabled) : null;
var el = findElementFor( tab );
if ( el ) el[0].focus();
return list.indexOf(tab);
}
/**
* Select the next tab in the list or the
* @returns {*} Tab
*/
function selectNext(target) {
return selectTab( list.next(target, isEnabled) || target );
}
/**
* Select the previous tab
* @returns {*} Tab
*/
function selectPrevious(target) {
return selectTab( list.previous(target, isEnabled) || target );
}
/**
* Validation criteria for list iterator when List::next() or List::previous() is used..:
* In this case, the list iterator should skip items that are disabled.
* @param tab
* @returns {boolean}
*/
function isEnabled(tab) {
return tab && !tab.disabled;
}
/**
* Partial application to build function that will
* mark the specified tab as active or not. This also
* allows the `updateStatus` function to be used as an iterator.
*
* @param active
*/
function makeActivator(active) {
return function updateState(tab) {
if (tab && (active != tab.active)) {
tab.active = active;
if (active) {
selected = tab;
tab.selected();
} else {
if (selected == tab) {
selected = null;
}
tab.deselected();
}
return tab;
}
return null;
};
}
}
/* Disable Tab Pagination */
/**
* @ngdoc module
* @name material.components.tabs
* @description
*
* Tabs
*/
angular.module('material.components.tabs')
.directive('materialTabs', [
'$q',
'$window',
'$timeout',
'$compile',
'$materialEffects',
'$$rAF',
'$aria',
TabsDirective
]);
/**
* @ngdoc directive
* @name materialTabs
* @module material.components.tabs
* @order 0
*
* @restrict E
*
* @description
* The `<material-tabs>` directive serves as the container for 1..n `<material-tab>` child directives to produces a Tabs components.
* In turn, the nested `<material-tab>` directive is used to specify a tab label for the **header button** and a [optional] tab view
* content that will be associated with each tab button.
*
* Below is the markup for its simplest usage:
*
* <hljs lang="html">
* <material-tabs>
* <material-tab label="Tab #1"></material-tab>
* <material-tab label="Tab #2"></material-tab>
* <material-tab label="Tab #3"></material-tab>
* <material-tabs>
* </hljs>
*
* Tabs supports three (3) usage scenarios:
*
* 1. Tabs (buttons only)
* 2. Tabs with internal view content
* 3. Tabs with external view content
*
* **Tab-only** support is useful when tab buttons are used for custom navigation regardless of any other components, content, or views.
* **Tabs with internal views** are the traditional usages where each tab has associated view content and the view switching is managed internally by the Tabs component.
* **Tabs with external view content** is often useful when content associated with each tab is independently managed and data-binding notifications announce tab selection changes.
*
* > As a performance bonus, if the tab content is managed internally then the non-active (non-visible) tab contents are temporarily disconnected from the `$scope.$digest()` processes; which restricts and optimizes DOM updates to only the currently active tab.
*
* Additional features also include:
*
* * Content can include any markup.
* * If a tab is disabled while active/selected, then the next tab will be auto-selected.
* * If the currently active tab is the last tab, then next() action will select the first tab.
* * Any markup (other than **`<material-tab>`** tags) will be transcluded into the tab header area BEFORE the tab buttons.
*
* @param {integer=} selected Index of the active/selected tab
* @param {boolean=} noink Flag indicates use of ripple ink effects
* @param {boolean=} nobar Flag indicates use of ink bar effects
* @param {boolean=} nostretch Flag indicates use of elastic animation for inkBar width and position changes
* @param {string=} align-tabs Attribute to indicate position of tab buttons: bottom or top; default is `top`
*
* @usage
* <hljs lang="html">
* <material-tabs selected="selectedIndex" >
* <img ng-src="/img/angular.png" class="centered">
*
* <material-tab
* ng-repeat="tab in tabs | orderBy:predicate:reversed"
* on-select="onTabSelected(tab)"
* on-deselect="announceDeselected(tab)"
* disabled="tab.disabled" >
*
* <material-tab-label>
* {{tab.title}}
* <img src="/img/removeTab.png"
* ng-click="removeTab(tab)"
* class="delete" >
* </material-tab-label>
*
* {{tab.content}}
*
* </material-tab>
*
* </material-tabs>
* </hljs>
*
*/
function TabsDirective($q, $window, $timeout, $compile, $materialEffects, $$rAF, $aria) {
return {
restrict: 'E',
replace: false,
transclude: 'true',
scope: {
$selIndex: '=?selected'
},
compile: compileTabsFn,
controller: [ '$scope', '$attrs', '$materialComponentRegistry', '$timeout', '$$rAF', TabsController ],
template:
'<div class="tabs-header" ng-class="{\'tab-paginating\': pagination.active}">' +
' <div class="tab-paginator prev" ng-if="pagination.active" ng-click="pagination.hasPrev && pagination.prev()" ng-class="{active: pagination.hasPrev}">' +
' </div>' +
' <div class="tabs-header-items-container">' +
' <div class="tabs-header-items"></div>' +
' </div>' +
' <div class="tab-paginator next" ng-if="pagination.active" ng-click="pagination.hasNext && pagination.next()" ng-class="{active: pagination.hasNext}">' +
' </div>' +
' <material-ink-bar></material-ink-bar>' +
'</div>'+
'<div class="tabs-content ng-hide"></div>'
};
/**
* Use prelink to configure inherited scope attributes: noink, nobar, and nostretch;
* do this before the child elements are linked.
*
* @param element
* @param attr
* @returns {{pre: materialTabsLink}}
*/
function compileTabsFn() {
return {
pre: function tabsPreLink(scope, element, attrs, tabsCtrl) {
// These attributes do not have values; but their presence defaults to value == true.
scope.noink = angular.isDefined(attrs.noink);
scope.nobar = angular.isDefined(attrs.nobar);
scope.nostretch = angular.isDefined(attrs.nostretch);
// Publish for access by nested `<material-tab>` elements
tabsCtrl.noink = scope.noink;
scope.$watch('$selIndex', function (index) {
tabsCtrl.selectAt(index);
});
// Remove the `inkBar` element if `nobar` is defined
var elBar = findNode("material-ink-bar",element);
if ( elBar && scope.nobar ) {
elBar.remove();
}
},
post: function tabsPostLink(scope, element, attrs, tabsCtrl, $transclude) {
var cache = {
length: 0,
contains: function (tab) {
return !angular.isUndefined(cache[tab.$id]);
}
};
var allowFocus = 0; // do not auto-focus on default tab selection
var updateInk = linkTabInk(scope, element, tabsCtrl, $q, $materialEffects) || angular.noop;
var updatePagination = linkTabPagination( scope, element, tabsCtrl, $q, $materialEffects );
var updateAll = function(event) {
scope.$evalAsync(function() {
updatePagination().then( function(){
// Make sure the ink positioning is correct
$timeout( function() {
updateInk();
// Key focus synced with tab selection...
if ( (event.name == EVENT.TABS_CHANGED) && allowFocus++) {
tabsCtrl.focusSelected();
}
},60);
});
// Make sure ink changes start just after pagination transitions have started...
$$rAF( updateInk );
});
};
var onWindowResize = $$rAF.debounce( updateAll );
var onWindowRelease = function() {
angular.element($window).off('resize', onWindowResize);
};
$$rAF(updateAll);
angular.element($window).on( EVENT.WINDOW_RESIZE, onWindowResize);
scope.$on( EVENT.TABS_CHANGED, updateAll );
scope.$on( EVENT.SCOPE_DESTROY, onWindowRelease );
transcludeHeaderItems();
transcludeContentItems();
configureAria(); // Update ARIA values for the Tab group (Tabs)
alignTabButtons();
selectDefaultTab();
// **********************************************************
// Private Methods
// **********************************************************
/**
* Inject ARIA-specific attributes appropriate for Tab Groups
*/
function configureAria() {
var ROLE = Constant.ARIA.ROLE;
$aria.update( element, {
'id': buildAriaID(),
'role': ROLE.TAB_LIST
});
/**
* Build a unique Tabs ID for WAI-ARIA; preserve the existing ID if already
* specified.
* @returns {*|string}
*/
function buildAriaID() {
return attrs.id || ("tabs" + "_" + scope.$id);
}
}
/**
* Change the positioning of the tab header and buttons.
* If the tabs-align attribute is 'bottom', then the tabs-content
* container is transposed with the tabs-header
*/
function alignTabButtons() {
var align = attrs.tabsAlign || "top";
var container = findNode('.tabs-content', element);
if (align == "bottom") {
element.prepend(container);
}
}
/**
* If an initial tab selection has not been specified, then
* select the first tab by default
*/
function selectDefaultTab() {
var tabs = tabsCtrl.$$tabs();
if ( tabs.length && angular.isUndefined(scope.$selIndex)) {
tabsCtrl.select(tabs[0]);
}
}
/**
* Transclude the materialTab items into the tabsHeaderItems container
*
*/
function transcludeHeaderItems() {
$transclude( function (content) {
var header = findNode('.tabs-header-items', element);
var parent = angular.element(element[0]);
angular.forEach(content, function (node) {
var intoHeader = isNodeType(node, 'material-tab') || isNgRepeat(node);
if (intoHeader) {
header.append(node);
} else {
parent.prepend(node);
}
});
});
}
/**
* Transclude the materialTab view/body contents into materialView containers; which
* are stored in the tabsContent area...
*/
function transcludeContentItems() {
var cntr = findNode('.tabs-content', element),
materialViewTmpl = '<div class="material-view" ng-show="active"></div>';
scope.$watch(getTabsHash, function buildContentItems() {
var tabs = tabsCtrl.$$tabs(notInCache),
views = tabs.map(extractContent);
// At least 1 tab must have valid content to build; otherwise
// we hide/remove the tabs-content container...
if (views.some(notEmpty)) {
angular.forEach(views, function (content, j) {
var tab = tabs[j++],
materialView = $compile(materialViewTmpl)(tab);
// For ARIA, link the tab content container with the tab button...
configureAria( materialView, tab );
// Allow dynamic $digest() disconnect/reconnect of tab content's scope
enableDisconnect(tab, content.scope);
// Do we have content DOM nodes ?
// If transcluded content is not undefined then add all nodes to the materialView
if (content.nodes) {
angular.forEach(content.nodes, function (node) {
if ( !isNodeEmpty(node) ) {
materialView.append(node);
}
});
}
cntr.append(materialView);
addToCache(cache, { tab:tab, element: materialView });
});
// We have some new content just added...
showTabContent();
} else {
showTabContent(false);
}
/**
* Add class to hide or show the container for the materialView(s)
* NOTE: the `<div.tabs-content>` is **hidden** by default.
* @param visible Boolean a value `true` will remove the `class="ng-hide"` setting
*/
function showTabContent(visible) {
cntr.toggleClass('ng-hide', !!visible);
}
/**
* Configure ARIA attributes to link tab content back to their respective
* 'owning' tab buttons.
*/
function configureAria( cntr, tab ) {
$aria.update( cntr, {
'id' : "content_" + tab.ariaId,
'role' : Constant.ARIA.ROLE.TAB_PANEL,
'aria-labelledby' : tab.ariaId
});
}
});
/**
* Allow tabs to disconnect or reconnect their content from the $digest() processes
* when unselected or selected (respectively).
*
* @param content Special content scope which is a direct child of a `tab` scope
*/
function enableDisconnect(tab, content) {
if ( !content ) return;
var selectedFn = angular.bind(tab, tab.selected),
deselectedFn = angular.bind(tab, tab.deselected);
addDigestConnector(content);
// 1) Tail-hook deselected()
tab.deselected = function() {
deselectedFn();
tab.$$postDigest(function(){
content.$disconnect();
});
};
// 2) Head-hook selected()
tab.selected = function() {
content.$reconnect();
selectedFn();
};
// Immediate disconnect all non-actives
if ( !tab.active ) {
tab.$$postDigest(function(){
content.$disconnect();
});
}
}
/**
* Add tab scope/DOM node to the cache and configure
* to auto-remove when the scope is destroyed.
* @param cache
* @param item
*/
function addToCache(cache, item) {
var scope = item.tab;
cache[ scope.$id ] = item;
cache.length = cache.length + 1;
// When the tab is removed, remove its associated material-view Node...
scope.$on("$destroy", function () {
angular.element(item.element).remove();
delete cache[ scope.$id];
cache.length = cache.length - 1;
});
}
function getTabsHash() {
return tabsCtrl.$$hash;
}
/**
* Special function to extract transient data regarding transcluded
* tab content. Data includes dynamic lookup of bound scope for the transcluded content.
*
* @see TabDirective::updateTabContent()
*
* @param tab
* @returns {{nodes: *, scope: *}}
*/
function extractContent(tab) {
var content = hasContent(tab) ? tab.content : undefined;
var scope = (content && content.length) ? angular.element(content[0]).scope() : null;
// release immediately...
delete tab.content;
return { nodes:content, scope:scope };
}
function hasContent(tab) {
return tab.content && tab.content.length;
}
function notEmpty(view) {
var hasContent = false;
if (angular.isDefined(view.nodes)) {
angular.forEach(view.nodes, function(node) {
hasContent = hasContent || !isNodeEmpty(node);
});
}
return hasContent;
}
function notInCache(tab) {
return !cache.contains(tab);
}
}
}
};
}
}
/**
* Determine if the DOM element is of a certain tag type
* or has the specified attribute type
*
* @param node
* @returns {*|boolean}
*/
var isNodeType = function (node, type) {
return node.tagName && (
node.hasAttribute(type) ||
node.hasAttribute('data-' + type) ||
node.tagName.toLowerCase() === type ||
node.tagName.toLowerCase() === 'data-' + type
);
};
var isNgRepeat = function (node) {
var COMMENT_NODE = 8;
return node.nodeType == COMMENT_NODE && node.nodeValue.indexOf('ngRepeat') > -1;
};
/**
* Is the an empty text string
* @param node
* @returns {boolean}
*/
var isNodeEmpty = function (node) {
var TEXT_NODE = 3,
COMMENT_NODE = 8;
return (node.nodeType == COMMENT_NODE) ||
(node.nodeType == TEXT_NODE && !(node.nodeValue || '').trim());
};
function findNode(selector, element) {
var parentNode = element[0];
return angular.element(parentNode.querySelector(selector));
}
/*
* This function() provides scope-relative features to disconnect and reconnect to the $digest() processes
* NOTE: this is essentially a reversible $destroy() for scopes.
*
* Detaching the scope would mean:
*
* Detaching the scope from the scope's current parent so that watchers no
* longer fire when the scope's current parent's $digest is called
*
* On re-attaching to a DOM element (as a child):
*
* It would be attached as he child scope of the DOM element. This is useful
* for optimizations such as not running watchers on hidden DOM (that could be detached).
*
* @see https://github.com/angular/angular.js/issues/5301
*
*/
function addDigestConnector (scope) {
var disconnect = function () {
// we can't destroy the root scope or a scope that has been already destroyed
if (this.$root === this) return;
if (this.$$destroyed ) return;
var parent = this.$parent;
this.$$disconnected = true;
// See Scope.$destroy
if (parent.$$childHead === this) parent.$$childHead = this.$$nextSibling;
if (parent.$$childTail === this) parent.$$childTail = this.$$prevSibling;
if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling;
if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling;
this.$$nextSibling = this.$$prevSibling = null;
};
var reconnect = function () {
// we can't disconnect the root node or scope already disconnected
if (this.$root === this) return;
if (!this.$$disconnected) return;
var child = this;
var parent = child.$parent;
child.$$disconnected = false;
// See Scope.$new for this logic...
child.$$prevSibling = parent.$$childTail;
if (parent.$$childHead) {
parent.$$childTail.$$nextSibling = child;
parent.$$childTail = child;
} else {
parent.$$childHead = parent.$$childTail = child;
}
};
scope.$disconnect = angular.bind( scope, disconnect );
scope.$reconnect = angular.bind( scope, reconnect );
return scope;
}
/**
* @ngdoc module
* @name material.components.toast
* @description
* Toast
*/
angular.module('material.components.toast', ['material.services.compiler'])
.directive('materialToast', [
QpToastDirective
])
.factory('$materialToast', [
'$timeout',
'$rootScope',
'$materialCompiler',
'$rootElement',
'$animate',
QpToastService
]);
function QpToastDirective() {
return {
restrict: 'E'
};
}
/**
* @ngdoc service
* @name $materialToast
* @module material.components.toast
*
* @description
* Open a toast notification on any position on the screen, with an optional
* duration.
*
* Only one toast notification may ever be active at any time. If a new toast is
* shown while a different toast is active, the old toast will be automatically
* hidden.
*
* `$materialToast` takes one argument, options, which is defined below.
*
* @usage
* <hljs lang="html">
* <div ng-controller="MyController">
* <material-button ng-click="openToast()">
* Open a Toast!
* </material-button>
* </div>
* </hljs>
* <hljs lang="js">
* var app = angular.module('app', ['ngMaterial']);
* app.controller('MyController', function($scope, $materialToast) {
* $scope.openToast = function($event) {
* var hideToast = $materialToast({
* template: '<material-toast>Hello!</material-toast>',
* duration: 3000
* });
* };
* });
* </hljs>
*
* @returns {function} `hideToast` - A function that hides the toast.
*
* @paramType Options
* @param {string=} templateUrl The url of an html template file that will
* be used as the content of the toast. Restrictions: the template must
* have an outer `material-toast` element.
* @param {string=} template Same as templateUrl, except this is an actual
* template string.
* @param {number=} duration How many milliseconds the toast should stay
* active before automatically closing. Set to 0 to disable duration.
* Default: 3000.
* @param {string=} position Where to place the toast. Available: any combination
* of 'bottom', 'left', 'top', 'right', 'fit'. Default: 'bottom left'.
* @param {string=} controller The controller to associate with this toast.
* The controller will be injected the local `$hideToast`, which is a function
* used to hide the toast.
* @param {string=} locals An object containing key/value pairs. The keys will
* be used as names of values to inject into the controller. For example,
* `locals: {three: 3}` would inject `three` into the controller with the value
* of 3.
* @param {object=} resolve Similar to locals, except it takes promises as values
* and the toast will not open until the promises resolve.
* @param {string=} controllerAs An alias to assign the controller to on the scope.
*/
function QpToastService($timeout, $rootScope, $materialCompiler, $rootElement, $animate) {
var recentToast;
function toastOpenClass(position) {
return 'material-toast-open-' +
(position.indexOf('top') > -1 ? 'top' : 'bottom');
}
// If the $rootElement is the document (<html> element), be sure to append it to the
// body instead.
var toastParent = $rootElement.find('body');
if ( !toastParent.length ) {
toastParent = $rootElement;
}
return showToast;
/**
* TODO fully document this
* Supports all options from $materialPopup, in addition to `duration` and `position`
*/
function showToast(options) {
options = angular.extend({
// How long to keep the toast up, milliseconds
duration: 3000,
// [unimplemented] Whether to disable swiping
swipeDisabled: false,
// Supports any combination of these class names: 'bottom top left right fit'.
// Default: 'bottom left'
position: 'bottom left'
}, options || {});
recentToast && recentToast.then(function(destroy) { destroy(); });
recentToast = $materialCompiler.compile(options).then(function(compileData) {
// Controller will be passed a `$hideToast` function
compileData.locals.$hideToast = destroy;
var scope = $rootScope.$new();
var element = compileData.link(scope);
var toastParentClass = toastOpenClass(options.position);
element.addClass(options.position);
toastParent.addClass(toastParentClass);
var delayTimeout;
$animate.enter(element, toastParent).then(function() {
if (options.duration) {
delayTimeout = $timeout(destroy, options.duration);
}
});
var hammertime = new Hammer(element[0], {
recognizers: [
[Hammer.Swipe, { direction: Hammer.DIRECTION_HORIZONTAL }]
]
});
hammertime.on('swipeleft swiperight', onSwipe);
function onSwipe(ev) {
//Add swipeleft/swiperight class to element so it can animate correctly
element.addClass(ev.type);
$timeout(destroy);
}
return destroy;
function destroy() {
if (destroy.called) return;
destroy.called = true;
hammertime.destroy();
toastParent.removeClass(toastParentClass);
$timeout.cancel(delayTimeout);
$animate.leave(element).then(function() {
scope.$destroy();
});
}
});
return recentToast;
}
}
/**
* @ngdoc module
* @name material.components.toolbar
*/
angular.module('material.components.toolbar', [
'material.components.content',
'material.animations'
])
.directive('materialToolbar', [
'$$rAF',
'$materialEffects',
materialToolbarDirective
]);
/**
* @ngdoc directive
* @name materialToolbar
* @restrict E
* @description
* `material-toolbar` is used to place a toolbar in your app.
*
* Toolbars are usually used above a content area to display the title of the
* current page, and show relevant action buttons for that page.
*
* You can change the height of the toolbar by adding either the
* `material-medium-tall` or `material-tall` class to the toolbar.
*
* @usage
* <hljs lang="html">
* <div layout="vertical" layout-fill>
* <material-toolbar>
*
* <div class="material-toolbar-tools">
* <span>My App's Title</span>
*
* <!-- fill up the space between left and right area -->
* <span flex></span>
*
* <material-button>
* Right Bar Button
* </material-button>
* </div>
*
* </material-toolbar>
* <material-content>
* Hello!
* </material-content>
* </div>
* </hljs>
*
* @param {boolean=} scrollShrink Whether the header should shrink away as
* the user scrolls down, and reveal itself as the user scrolls up.
* Note: for scrollShrink to work, the toolbar must be a sibling of a
* `material-content` element, placed before it. See the scroll shrink demo.
*
*
* @param {number=} shrinkSpeedFactor How much to change the speed of the toolbar's
* shrinking by. For example, if 0.25 is given then the toolbar will shrink
* at one fourth the rate at which the user scrolls down. Default 0.5.
*/
function materialToolbarDirective($$rAF, $materialEffects) {
return {
restrict: 'E',
controller: angular.noop,
link: function(scope, element, attr) {
if (angular.isDefined(attr.scrollShrink)) {
setupScrollShrink();
}
function setupScrollShrink() {
// Current "y" position of scroll
var y = 0;
// Store the last scroll top position
var prevScrollTop = 0;
var shrinkSpeedFactor = attr.shrinkSpeedFactor || 0.5;
var toolbarHeight;
var contentElement;
var debouncedContentScroll = $$rAF.debounce(onContentScroll);
var debouncedUpdateHeight = Util.debounce(updateToolbarHeight, 5 * 1000);
// Wait for $materialContentLoaded event from materialContent directive.
// If the materialContent element is a sibling of our toolbar, hook it up
// to scroll events.
scope.$on('$materialContentLoaded', onMaterialContentLoad);
function onMaterialContentLoad($event, newContentEl) {
if (Util.elementIsSibling(element, newContentEl)) {
// unhook old content event listener if exists
if (contentElement) {
contentElement.off('scroll', debouncedContentScroll);
}
newContentEl.on('scroll', debouncedContentScroll);
newContentEl.attr('scroll-shrink', 'true');
contentElement = newContentEl;
$$rAF(updateToolbarHeight);
}
}
function updateToolbarHeight() {
toolbarHeight = element.prop('offsetHeight');
// Add a negative margin-top the size of the toolbar to the content el.
// The content will start transformed down the toolbarHeight amount,
// so everything looks normal.
//
// As the user scrolls down, the content will be transformed up slowly
// to put the content underneath where the toolbar was.
contentElement.css(
'margin-top',
(-toolbarHeight * shrinkSpeedFactor) + 'px'
);
onContentScroll();
}
function onContentScroll(e) {
var scrollTop = e ? e.target.scrollTop : prevScrollTop;
debouncedUpdateHeight();
y = Math.min(
toolbarHeight / shrinkSpeedFactor,
Math.max(0, y + scrollTop - prevScrollTop)
);
element.css(
$materialEffects.TRANSFORM,
'translate3d(0,' + (-y * shrinkSpeedFactor) + 'px,0)'
);
contentElement.css(
$materialEffects.TRANSFORM,
'translate3d(0,' + ((toolbarHeight - y) * shrinkSpeedFactor) + 'px,0)'
);
prevScrollTop = scrollTop;
}
}
}
};
}
angular.module('material.components.whiteframe', []);
/**
* @ngdoc module
* @name material.components.divider
* @description Divider module!
*/
angular.module('material.components.divider', [
'material.animations',
'material.services.aria'
])
.directive('materialDivider', MaterialDividerDirective);
function MaterialDividerController(){}
/**
* @ngdoc directive
* @name materialDivider
* @module material.components.divider
* @restrict E
*
* @description
* Dividers group and separate content within lists and page layouts using strong visual and spatial distinctions. This divider is a thin rule, lightweight enough to not distract the user from content.
*
* @param {boolean=} inset Add this attribute to activate the inset divider style.
* @usage
* <hljs lang="html">
* <material-divider></material-divider>
*
* <material-divider inset></material-divider>
* </hljs>
*
*/
function MaterialDividerDirective() {
return {
restrict: 'E',
controller: [MaterialDividerController]
};
}
/**
* @ngdoc module
* @name material.components.linearProgress
* @description Linear Progress module!
*/
angular.module('material.components.linearProgress', [
'material.animations',
'material.services.aria'
])
.directive('materialLinearProgress', ['$timeout', MaterialLinearProgressDirective]);
/**
* @ngdoc directive
* @name materialLinearProgress
* @module material.components.linearProgress
* @restrict E
*
* @description
* The linear progress directive is used to make loading content in your app as delightful and painless as possible by minimizing the amount of visual change a user sees before they can view and interact with content. Each operation should only be represented by one activity indicator—for example, one refresh operation should not display both a refresh bar and an activity circle.
*
* For operations where the percentage of the operation completed can be determined, use a determinate indicator. They give users a quick sense of how long an operation will take.
*
* For operations where the user is asked to wait a moment while something finishes up, and its not necessary to expose what's happening behind the scenes and how long it will take, use an indeterminate indicator.
*
* @param {string} mode Select from one of four modes: determinate, indeterminate, buffer or query.
* @param {number=} value In determinate and buffer modes, this number represents the percentage of the primary progress bar. Default: 0
* @param {number=} secondaryValue In the buffer mode, this number represents the precentage of the secondary progress bar. Default: 0
*
* @usage
* <hljs lang="html">
* <material-linear-progress mode="determinate" value="..."></material-linear-progress>
*
* <material-linear-progress mode="determinate" ng-value="..."></material-linear-progress>
*
* <material-linear-progress mode="indeterminate"></material-linear-progress>
*
* <material-linear-progress mode="buffer" value="..." secondaryValue="..."></material-linear-progress>
*
* <material-linear-progress mode="query"></material-linear-progress>
* </hljs>
*/
function MaterialLinearProgressDirective($timeout) {
return {
restrict: 'E',
template: '<div class="container">' +
'<div class="dashed"></div>' +
'<div class="bar bar1"></div>' +
'<div class="bar bar2"></div>' +
'</div>',
link: function(scope, element, attr) {
var bar1 = angular.element(element[0].querySelector('.bar1')),
bar2 = angular.element(element[0].querySelector('.bar2')),
container = angular.element(element[0].querySelector('.container'));
attr.$observe('value', function(value) {
bar2.css('width', clamp(value).toString() + '%');
});
attr.$observe('secondaryvalue', function(value) {
bar1.css('width', clamp(value).toString() + '%');
});
$timeout(function() {
container.addClass('ready');
});
}
};
}
// **********************************************************
// Private Methods
// **********************************************************
function clamp(value) {
if (value > 100) {
return 100;
}
if (value < 0) {
return 0;
}
return value || 0;
}
angular.module('material.decorators', [])
.config(['$provide', function($provide) {
$provide.decorator('$$rAF', ['$delegate', '$rootScope', rAFDecorator]);
function rAFDecorator($$rAF, $rootScope) {
/**
* Use this to debounce events that come in often.
* The debounced function will always use the *last* invocation before the
* coming frame.
*
* For example, window resize events that fire many times a second:
* If we set to use an raf-debounced callback on window resize, then
* our callback will only be fired once per frame, with the last resize
* event that happened before that frame.
*
* @param {function} callback function to debounce
*/
$$rAF.debounce = function(cb) {
var queueArgs, alreadyQueued, queueCb, context;
return function debounced() {
queueArgs = arguments;
context = this;
queueCb = cb;
if (!alreadyQueued) {
alreadyQueued = true;
$$rAF(function() {
queueCb.apply(context, queueArgs);
alreadyQueued = false;
});
}
};
};
return $$rAF;
}
}]);
angular.module('material.services.aria', [])
.service('$aria', [
'$log',
AriaService
]);
function AriaService($log) {
var messageTemplate = 'ARIA: Attribute "%s", required for accessibility, is missing on "%s"!';
var defaultValueTemplate = 'Default value was set: %s="%s".';
return {
expect : expectAttribute,
update : assignAttributes
};
/**
* Assign 1..n ARIA values to the target element
* @param element
* @param options
*/
function assignAttributes(element, options )
{
angular.forEach(options, Util.spread(function( attrValue, attrName ) {
element.attr(attrName, attrValue);
}));
return element;
}
/**
* Check if expected ARIA has been specified on the target element
* @param element
* @param attrName
* @param defaultValue
*/
function expectAttribute(element, attrName, defaultValue) {
var node = element[0];
if (!node.hasAttribute(attrName)) {
var hasDefault = angular.isDefined(defaultValue);
if (hasDefault) {
defaultValue = String(defaultValue).trim();
// $log.warn(messageTemplate + ' ' + defaultValueTemplate,
// attrName, getTagString(node), attrName, defaultValue);
element.attr(attrName, defaultValue);
} else {
// $log.warn(messageTemplate, attrName, getTagString(node));
}
}
}
/**
* Gets the tag definition from a node's outerHTML
* @example getTagDefinition(
* '<material-button foo="bar">Hello</material-button>'
* ) // => '<material-button foo="bar">'
*/
function getTagString(node) {
var html = node.outerHTML;
var closingIndex = html.indexOf('>');
return html.substring(0, closingIndex + 1);
}
}
angular.module('material.services.attrBind', [
])
.factory('$attrBind', [
'$parse',
'$interpolate',
MaterialAttrBind
]);
/**
* This service allows directives to easily databind attributes to private scope properties.
*
* @private
*/
function MaterialAttrBind($parse, $interpolate) {
var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/;
return function (scope, attrs, bindDefinition, bindDefaults) {
angular.forEach(bindDefinition || {}, function (definition, scopeName) {
//Adapted from angular.js $compile
var match = definition.match(LOCAL_REGEXP) || [],
attrName = match[3] || scopeName,
mode = match[1], // @, =, or &
parentGet,
unWatchFn;
switch (mode) {
case '@': // One-way binding from attribute into scope
attrs.$observe(attrName, function (value) {
scope[scopeName] = value;
});
attrs.$$observers[attrName].$$scope = scope;
if (!bypassWithDefaults(attrName, scopeName)) {
// we trigger an interpolation to ensure
// the value is there for use immediately
scope[scopeName] = $interpolate(attrs[attrName])(scope);
}
break;
case '=': // Two-way binding...
if (!bypassWithDefaults(attrName, scopeName)) {
// Immediate evaluation
scope[scopeName] = (attrs[attrName] === "") ? true : scope.$eval(attrs[attrName]);
// Data-bind attribute to scope (incoming) and
// auto-release watcher when scope is destroyed
unWatchFn = scope.$watch(attrs[attrName], function (value) {
scope[scopeName] = value;
});
scope.$on('$destroy', unWatchFn);
}
break;
case '&': // execute an attribute-defined expression in the context of the parent scope
if (!bypassWithDefaults(attrName, scopeName, angular.noop)) {
/* jshint -W044 */
if (attrs[attrName] && attrs[attrName].match(RegExp(scopeName + '\(.*?\)'))) {
throw new Error('& expression binding "' + scopeName + '" looks like it will recursively call "' +
attrs[attrName] + '" and cause a stack overflow! Please choose a different scopeName.');
}
parentGet = $parse(attrs[attrName]);
scope[scopeName] = function (locals) {
return parentGet(scope, locals);
};
}
break;
}
});
/**
* Optional fallback value if attribute is not specified on element
* @param scopeName
*/
function bypassWithDefaults(attrName, scopeName, defaultVal) {
if (!angular.isDefined(attrs[attrName])) {
var hasDefault = bindDefaults && bindDefaults.hasOwnProperty(scopeName);
scope[scopeName] = hasDefault ? bindDefaults[scopeName] : defaultVal;
return true;
}
return false;
}
};
}
angular.module('material.services.compiler', [
])
.service('$materialCompiler', [
'$q',
'$http',
'$injector',
'$compile',
'$controller',
'$templateCache',
materialCompilerService
]);
function materialCompilerService($q, $http, $injector, $compile, $controller, $templateCache) {
/**
* @ngdoc service
* @name $materialCompiler
* @module material.services.compiler
*
* @description
* The $materialCompiler service is an abstraction of angular's compiler, that allows the developer
* to easily compile an element with a templateUrl, controller, and locals.
*/
/**
* @ngdoc method
* @name $materialCompiler#compile
* @param {object} options An options object, with the following properties:
*
* - `controller` – `{(string=|function()=}` – Controller fn that should be associated with
* newly created scope or the name of a {@link angular.Module#controller registered
* controller} if passed as a string.
* - `controllerAs` – `{string=}` – A controller alias name. If present the controller will be
* published to scope under the `controllerAs` name.
* - `template` – `{string=}` – html template as a string or a function that
* returns an html template as a string which should be used by {@link
* ngRoute.directive:ngView ngView} or {@link ng.directive:ngInclude ngInclude} directives.
* This property takes precedence over `templateUrl`.
*
* - `templateUrl` – `{string=}` – path or function that returns a path to an html
* template that should be used by {@link ngRoute.directive:ngView ngView}.
*
* - `transformTemplate` – `{function=} – a function which can be used to transform
* the templateUrl or template provided after it is fetched. It will be given one
* parameter, the template, and should return a transformed template.
*
* - `resolve` - `{Object.<string, function>=}` - An optional map of dependencies which should
* be injected into the controller. If any of these dependencies are promises, the compiler
* will wait for them all to be resolved or one to be rejected before the controller is
* instantiated.
*
* - `key` – `{string}`: a name of a dependency to be injected into the controller.
* - `factory` - `{string|function}`: If `string` then it is an alias for a service.
* Otherwise if function, then it is {@link api/AUTO.$injector#invoke injected}
* and the return value is treated as the dependency. If the result is a promise, it is
* resolved before its value is injected into the controller.
*
* @returns {object=} promise A promsie which will be resolved with a `compileData` object,
* with the following properties:
*
* - `{element}` – `element` – an uncompiled angular element compiled using the provided template.
*
* - `{function(scope)}` – `link` – A link function, which, when called, will compile
* the elmeent and instantiate options.controller.
*
* - `{object}` – `locals` – The locals which will be passed into the controller once `link` is
* called.
*
* @usage
* $materialCompiler.compile({
* templateUrl: 'modal.html',
* controller: 'ModalCtrl',
* locals: {
* modal: myModalInstance;
* }
* }).then(function(compileData) {
* compileData.element; // modal.html's template in an element
* compileData.link(myScope); //attach controller & scope to element
* });
*/
this.compile = function(options) {
var templateUrl = options.templateUrl;
var template = options.template || '';
var controller = options.controller;
var controllerAs = options.controllerAs;
var resolve = options.resolve || {};
var locals = options.locals || {};
var transformTemplate = options.transformTemplate || angular.identity;
// Take resolve values and invoke them.
// Resolves can either be a string (value: 'MyRegisteredAngularConst'),
// or an invokable 'factory' of sorts: (value: function ValueGetter($dependency) {})
angular.forEach(resolve, function(value, key) {
if (angular.isString(value)) {
resolve[key] = $injector.get(value);
} else {
resolve[key] = $injector.invoke(value);
}
});
//Add the locals, which are just straight values to inject
//eg locals: { three: 3 }, will inject three into the controller
angular.extend(resolve, locals);
if (templateUrl) {
resolve.$template = $http.get(templateUrl, {cache: $templateCache})
.then(function(response) {
return response.data;
});
} else {
resolve.$template = $q.when(template);
}
// Wait for all the resolves to finish if they are promises
return $q.all(resolve).then(function(locals) {
var template = transformTemplate(locals.$template);
var element = angular.element('<div>').html(template).contents();
var linkFn = $compile(element);
//Return a linking function that can be used later when the element is ready
return {
locals: locals,
element: element,
link: function link(scope) {
locals.$scope = scope;
//Instantiate controller if it exists, because we have scope
if (controller) {
var ctrl = $controller(controller, locals);
//See angular-route source for this logic
element.data('$ngControllerController', ctrl);
element.children().data('$ngControllerController', ctrl);
if (controllerAs) {
scope[controllerAs] = ctrl;
}
}
return linkFn(scope);
}
};
});
};
}
/**
* @ngdoc overview
* @name material.services.registry
*
* @description
* A component registry system for accessing various component instances in an app.
*/
angular.module('material.services.registry', [
])
.factory('$materialComponentRegistry', [
'$log',
materialComponentRegistry
]);
/**
* @ngdoc service
* @name material.services.registry.service:$materialComponentRegistry
*
* @description
* $materialComponentRegistry enables the user to interact with multiple instances of
* certain complex components in a running app.
*/
function materialComponentRegistry($log) {
var instances = [];
return {
/**
* Used to print an error when an instance for a handle isn't found.
*/
notFoundError: function(handle) {
$log.error('No instance found for handle', handle);
},
/**
* Return all registered instances as an array.
*/
getInstances: function() {
return instances;
},
/**
* Get a registered instance.
* @param handle the String handle to look up for a registered instance.
*/
get: function(handle) {
var i, j, instance;
for(i = 0, j = instances.length; i < j; i++) {
instance = instances[i];
if(instance.$$materialHandle === handle) {
return instance;
}
}
return null;
},
/**
* Register an instance.
* @param instance the instance to register
* @param handle the handle to identify the instance under.
*/
register: function(instance, handle) {
instance.$$materialHandle = handle;
instances.push(instance);
return function deregister() {
var index = instances.indexOf(instance);
if (index !== -1) {
instances.splice(index, 1);
}
};
}
}
}
angular.module('material.services.throttle', [ ])
.factory('$throttle', [
'$timeout',
'$$q',
'$log',
MaterialThrottleService
]);
/**
* var ripple, watchMouse,
* parent = element.parent(),
* makeRipple = $throttle({
* start : function() {
* ripple = ripple || $materialEffects.inkRipple( element[0], options );
* watchMouse = watchMouse || buildMouseWatcher(parent, makeRipple);
* // Ripples start with mouseDow (or taps)
* parent.on('mousedown', makeRipple);
* },
* throttle : function(e, done) {
* if ( effectAllowed() )
* {
* switch(e.type)
* {
* case 'mousedown' :
* watchMouse(true);
* ripple.createAt( options.forceToCenter ? null : localToCanvas(e) );
* break;
* default:
* watchMouse(false);
* ripple.draw( localToCanvas(e) );
* break;
* }
* } else {
* done();
* }
* },
* end : function() {
* watchMouse(false);
* }
* });
*
* makeRipple();
*
*/
function MaterialThrottleService($timeout, $$q, $log) {
var STATE_READY= 0, STATE_START=1, STATE_THROTTLE=2, STATE_END=3;
return function( config ){
return function( done, otherwise ){
return buildInstance( angular.extend({}, config), done || angular.noop, otherwise || angular.noop );
};
};
function buildInstance( phases, done, otherwise ) {
var pendingActions = [ ],
cancel = angular.noop,
state = STATE_READY;
// Defer the call to the start function ... so `throttle` reference can be returned...
$timeout(function(){
start().then(function(){
if ( !phases.throttle ) {
end();
}
});
},0,false);
return throttle;
/**
* Facade function that validates throttler
* state BEFORE processing the `throttle` request.
*/
function throttle( data, done ) {
if ( state != STATE_THROTTLE ) {
cacheRquest();
}
switch( state )
{
case STATE_READY :
start();
break;
case STATE_START:
break;
// Proxy throttle call to custom, user-defined throttle handler
case STATE_THROTTLE:
invokeThrottleHandler(data, done);
break;
case STATE_END :
restart();
break;
}
// **********************************************************
// Internal Methods
// **********************************************************
/**
* Cache for later submission to 'throttle()'
*/
function cacheRquest() {
pendingActions.push({ data:data, done:done });
}
/**
* Delegate to the custom throttle function...
* When CTF reports complete, then proceed to the `end` state
*
* @param data Data to be delegated to the throttle function
* @param done Callback when all throttle actions have completed
*/
function invokeThrottleHandler(data, done) {
if ( angular.isFunction(phases.throttle) ) {
done = done || angular.noop;
try {
phases.throttle.apply( null, [data, function(response) {
done.apply( null, [response] );
end();
}]);
} catch( error ) {
// Report error... and end()
otherwise(error);
end();
}
} else {
end();
}
}
}
/**
* Initiate the async `start` phase of the Throttler
* @returns {*} promise
*/
function start() {
return gotoState.apply(null, [ STATE_START, phases.start ] )
.then( feedPendingActions, otherwise );
/**
* Process all pending actions (if any)
*/
function feedPendingActions( response ) {
logResponse(response);
state = STATE_THROTTLE;
angular.forEach(pendingActions, function (it) {
throttle( it.data, function(response) {
logResponse(response);
if ( angular.isFunction(it.done) ) {
it.done(response);
}
});
});
pendingActions = [ ];
}
}
/**
* Initiate the async `end` phase of the Throttler
* @returns {*} promise
*/
function end() {
return gotoState.apply(null,[ STATE_END, phases.end ])
.then( finish, otherwise );
/**
* Mark throttle as ready to start... and announce completion
* of the current activity cycle
*/
function finish( response ) {
logResponse(response);
if ( state == STATE_END ){
state = STATE_READY;
done();
}
}
}
/**
* Cancel any `end` process and restart state machine processes
*/
function restart() {
try {
if ( !angular.isFunction(cancel) ) {
cancel = angular.noop;
}
cancel();
state = STATE_READY;
} finally {
start();
}
}
/**
* Change to next state and call the state function associated with that state...
* @param nextState
* @param targetFn
* @returns {*}
*/
function gotoState( nextState , targetFn )
{
var dfd = $$q.defer(),
hasFn = angular.isFunction(targetFn),
goNext = hasFn && (targetFn.length < 1),
fn = hasFn ? targetFn : resolved;
try {
state = nextState;
cancel = fn.apply( null, [
goNext ? resolved(dfd) :
hasFn ? callbackToResolve(dfd) : dfd
]);
} catch( error ) {
dfd.reject( error );
}
return dfd.promise;
}
}
// **********************************************************
// Internal Methods
// **********************************************************
/**
* Create callback function that will resolve the specified deferred.
* @param dfd
* @returns {Function}
*/
function callbackToResolve( dfd )
{
return function(response){
dfd.resolve.apply(null, [response ]);
};
}
/**
* Prepare fallback promise for start, end, throttle phases of the Throttler
* @param dfd
* @returns {*}
*/
function resolved(dfd)
{
dfd = dfd || $$q.defer();
dfd.resolve.apply(null, arguments.length > 1 ? [].slice.call(arguments,1) : [ ]);
return dfd.promise;
}
function logResponse(response)
{
if ( angular.isDefined(response) ) {
$log.debug(response);
}
}
}
})();