mirror of
https://github.com/gwenhael-le-moine/ledgerrb.git
synced 2025-01-10 05:24:24 +01:00
5059 lines
No EOL
142 KiB
JavaScript
5059 lines
No EOL
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);
|
||
}
|
||
}
|
||
}
|
||
|
||
})(); |