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