ledgerrb/public/app/bower_components/angular-material/angular-material.js

5059 lines
142 KiB
JavaScript
Raw Normal View History

2014-09-30 12:29:53 +02:00
/*!
* 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 indicatorfor 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);
}
}
}
})();