Skip to content

Instantly share code, notes, and snippets.

@eyyub
Created December 4, 2014 13:41
Show Gist options
  • Save eyyub/7202e9c4ddf12960e333 to your computer and use it in GitHub Desktop.
Save eyyub/7202e9c4ddf12960e333 to your computer and use it in GitHub Desktop.
/*!
* Angular Material Design
* https://github.com/angular/material
* @license MIT
* v0.6.0-rc3
*/
angular.module('ngMaterial', ["ng","ngAnimate","ngAria","material.core","material.components.backdrop","material.components.bottomSheet","material.components.button","material.components.card","material.components.checkbox","material.components.content","material.components.dialog","material.components.divider","material.components.icon","material.components.list","material.components.progressCircular","material.components.progressLinear","material.components.radioButton","material.components.sidenav","material.components.slider","material.components.sticky","material.components.subheader","material.components.swipe","material.components.switch","material.components.tabs","material.components.textField","material.components.toast","material.components.toolbar","material.components.tooltip","material.components.whiteframe"]);
(function() {
'use strict';
/**
* Initialization function that validates environment
* requirements.
*/
angular.module('material.core', [])
.run(MdCoreInitialize)
.config(MdCoreConfigure);
function MdCoreInitialize() {
if (typeof Hammer === 'undefined') {
throw new Error(
'ngMaterial requires HammerJS to be preloaded.'
);
}
}
function MdCoreConfigure($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;
}
}
MdCoreConfigure.$inject = ["$provide"];
})();
(function() {
'use strict';
angular.module('material.core')
.factory('$mdConstant', MdConstantFactory);
function MdConstantFactory($$rAF, $sniffer) {
var webkit = /webkit/i.test($sniffer.vendorPrefix);
function vendorProperty(name) {
return webkit ? ('webkit' + name.charAt(0).toUpperCase() + name.substring(1)) : name;
}
return {
KEY_CODE: {
ENTER: 13,
ESCAPE: 27,
SPACE: 32,
LEFT_ARROW : 37,
UP_ARROW : 38,
RIGHT_ARROW : 39,
DOWN_ARROW : 40
},
CSS: {
/* Constants */
TRANSITIONEND: 'transitionend' + (webkit ? ' webkitTransitionEnd' : ''),
ANIMATIONEND: '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')
}
};
}
MdConstantFactory.$inject = ["$$rAF", "$sniffer"];
})();
(function() {
'use strict';
/*
* This var has to be outside the angular factory, otherwise when
* there are multiple material apps on the same page, each app
* will create its own instance of this array and the app's IDs
* will not be unique.
*/
var nextUniqueId = ['0','0','0'];
angular.module('material.core')
.factory('$mdUtil', ['$cacheFactory', function($cacheFactory) {
var Util;
return Util = {
now: window.performance ? angular.bind(window.performance, window.performance.now) : Date.now,
/**
* Publish the iterator facade to easily support iteration and accessors
* @see iterator below
*/
iterator: iterator,
/**
* @see cacheFactory below
*/
cacheFactory: cacheFactory,
// 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 debounced() {
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);
};
},
// Returns a function that can only be triggered every `delay` milliseconds.
// In other words, the function will not be called unless it has been more
// than `delay` milliseconds since the last call.
throttle: function throttle(func, delay) {
var recent;
return function throttled() {
var context = this;
var args = arguments;
var now = Util.now();
if (!recent || recent - now > delay) {
func.apply(context, args);
recent = now;
}
};
},
/**
* nextUid, from angular.js.
* A consistent way of creating unique IDs in angular. The ID is a sequence of alpha numeric
* characters such as '012ABC'. The reason why we are not using simply a number counter is that
* the number string gets longer over time, and it can also overflow, where as the nextId
* will grow much slower, it is a string, and it will never overflow.
*
* @returns an unique alpha-numeric string
*/
nextUid: function() {
var index = nextUniqueId.length;
var digit;
while(index) {
index--;
digit = nextUniqueId[index].charCodeAt(0);
if (digit == 57 /*'9'*/) {
nextUniqueId[index] = 'A';
return nextUniqueId.join('');
}
if (digit == 90 /*'Z'*/) {
nextUniqueId[index] = '0';
} else {
nextUniqueId[index] = String.fromCharCode(digit + 1);
return nextUniqueId.join('');
}
}
nextUniqueId.unshift('0');
return nextUniqueId.join('');
},
// Stop watchers and events from firing on a scope without destroying it,
// by disconnecting it from its parent and its siblings' linked lists.
disconnectScope: function disconnectScope(scope) {
if (!scope) return;
// we can't destroy the root scope or a scope that has been already destroyed
if (scope.$root === scope) return;
if (scope.$$destroyed ) return;
var parent = scope.$parent;
scope.$$disconnected = true;
// See Scope.$destroy
if (parent.$$childHead === scope) parent.$$childHead = scope.$$nextSibling;
if (parent.$$childTail === scope) parent.$$childTail = scope.$$prevSibling;
if (scope.$$prevSibling) scope.$$prevSibling.$$nextSibling = scope.$$nextSibling;
if (scope.$$nextSibling) scope.$$nextSibling.$$prevSibling = scope.$$prevSibling;
scope.$$nextSibling = scope.$$prevSibling = null;
},
// Undo the effects of disconnectScope above.
reconnectScope: function reconnectScope(scope) {
if (!scope) return;
// we can't disconnect the root node or scope already disconnected
if (scope.$root === scope) return;
if (!scope.$$disconnected) return;
var child = scope;
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;
}
}
};
/*
* 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) {
var trueFn = function() { return true; };
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 item
* @returns {Array.length|*|number|boolean}
*/
function hasNext(item) {
return item ? inRange(indexOf(item) + 1) : false;
}
/*
* Can the iterator proceed to the previous item in the list; relative to
* the specified item.
*
* @param item
* @returns {Array.length|*|number|boolean}
*/
function hasPrevious(item) {
return item ? inRange(indexOf(item) - 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) {
return _items.filter(function(item) {
return item[key] === val;
});
}
/*
* Add item to list
* @param item
* @param index
* @returns {*}
*/
function add(item, index) {
if ( !item ) return -1;
if (!angular.isNumber(index)) {
index = _items.length;
}
_items.splice(index, 0, item);
return indexOf(item);
}
/*
* Remove item from list...
* @param item
*/
function remove(item) {
if ( contains(item) ){
_items.splice(indexOf(item), 1);
}
}
/*
* Get the zero-based index of the target item
* @param item
* @returns {*}
*/
function indexOf(item) {
return _items.indexOf(item);
}
/*
* Boolean existence check
* @param item
* @returns {boolean}
*/
function contains(item) {
return item && (indexOf(item) > -1);
}
/*
* Find the next item. If reloop is true and at the end of the list, it will
* go back to the first item. If given ,the `validate` callback will be used
* determine whether the next item is valid. If not valid, it will try to find the
* next item again.
* @param item
* @param {optional} validate
* @returns {*}
*/
function next(item, validate) {
validate = validate || trueFn;
if (contains(item)) {
var index = indexOf(item) + 1,
found = inRange(index) ? _items[ index ] : (reloop ? first() : null);
return validate(found) ? found : next(found, validate);
}
return null;
}
/*
* Find the previous item. If reloop is true and at the beginning of the list, it will
* go back to the last item. If given ,the `validate` callback will be used
* determine whether the previous item is valid. If not valid, it will try to find the
* previous item again.
* @param item
* @param {optional} validate
* @returns {*}
*/
function previous(item, validate) {
validate = validate || trueFn;
if (contains(item)) {
var index = indexOf(item) - 1,
found = inRange(index) ? _items[ index ] : (reloop ? last() : null);
return validate(found) ? found : previous(found, validate);
}
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;
}
}
/*
* Angular's $cacheFactory doesn't have a keys() method,
* so we add one ourself.
*/
function cacheFactory(id, options) {
var cache = $cacheFactory(id, options);
var keys = {};
cache._put = cache.put;
cache.put = function(k,v) {
keys[k] = true;
return cache._put(k, v);
};
cache._remove = cache.remove;
cache.remove = function(k) {
delete keys[k];
return cache._remove(k);
};
cache.keys = function() {
return Object.keys(keys);
};
return cache;
}
}]);
/*
* 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;
};
angular.element.prototype.blur = angular.element.prototype.blur || function() {
if (this.length) {
this[0].blur();
}
return this;
};
})();
(function() {
'use strict';
angular.module('material.core')
.service('$mdAria', AriaService);
function AriaService($$rAF, $log, $window) {
return {
expect: expect,
expectAsync: expectAsync,
expectWithText: expectWithText
};
/**
* Check if expected attribute has been specified on the target element or child
* @param element
* @param attrName
* @param {optional} defaultValue What to set the attr to if no value is found
*/
function expect(element, attrName, defaultValue) {
var node = element[0];
if (!node.hasAttribute(attrName) && !childHasAttribute(node, attrName)) {
defaultValue = angular.isString(defaultValue) && defaultValue.trim() || '';
if (defaultValue.length) {
element.attr(attrName, defaultValue);
} else {
$log.warn('ARIA: Attribute "', attrName, '", required for accessibility, is missing on node:', node);
}
}
}
function expectAsync(element, attrName, defaultValueGetter) {
// Problem: when retrieving the element's contents synchronously to find the label,
// the text may not be defined yet in the case of a binding.
// There is a higher chance that a binding will be defined if we wait one frame.
$$rAF(function() {
expect(element, attrName, defaultValueGetter());
});
}
function expectWithText(element, attrName) {
expectAsync(element, attrName, function() {
return element.text().trim();
});
}
function childHasAttribute(node, attrName) {
var hasChildren = node.hasChildNodes(),
hasAttr = false;
function isHidden(el) {
var style = el.currentStyle ? el.currentStyle : $window.getComputedStyle(el);
return (style.display === 'none');
}
if(hasChildren) {
var children = node.childNodes;
for(var i=0; i<children.length; i++){
var child = children[i];
if(child.nodeType === 1 && child.hasAttribute(attrName)) {
if(!isHidden(child)){
hasAttr = true;
}
}
}
}
return hasAttr;
}
}
AriaService.$inject = ["$$rAF", "$log", "$window"];
})();
(function() {
'use strict';
angular.module('material.core')
.service('$mdCompiler', mdCompilerService);
function mdCompilerService($q, $http, $injector, $compile, $controller, $templateCache) {
/*
* @ngdoc service
* @name $mdCompiler
* @module material.core
* @description
* The $mdCompiler service is an abstraction of angular's compiler, that allows the developer
* to easily compile an element with a templateUrl, controller, and locals.
*
* @usage
* <hljs lang="js">
* $mdCompiler.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
* });
* </hljs>
*/
/*
* @ngdoc method
* @name $mdCompiler#compile
* @description A helper to compile an HTML template/templateUrl with a given controller,
* locals, and scope.
* @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 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=}` An html template as a string.
* - `templateUrl` - `{string=}` A path to an html template.
* - `transformTemplate` - `{function(template)=}` A function which transforms the template after
* it is loaded. It will be given the template string as a parameter, and should
* return a a new string representing the 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 if one is rejected before the controller is
* instantiated `compile()` will fail..
* * `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 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 promise, which will be resolved with a `compileData` object.
* `compileData` has the following properties:
*
* - `element` - `{element}`: an uncompiled element matching the provided template.
* - `link` - `{function(scope)}`: A link function, which, when called, will compile
* the element and instantiate the provided controller (if given).
* - `locals` - `{object}`: The locals which will be passed into the controller once `link` is
* called. If `bindToController` is true, they will be coppied to the ctrl instead
* - `bindToController` - `bool`: bind the locals to the controller, instead of passing them in
*/
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;
var bindToController = options.bindToController;
// 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);
if (bindToController) {
angular.extend(ctrl, 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);
}
};
});
};
}
mdCompilerService.$inject = ["$q", "$http", "$injector", "$compile", "$controller", "$templateCache"];
})();
(function() {
'use strict';
angular.module('material.core')
.provider('$$interimElement', InterimElementProvider);
/*
* @ngdoc service
* @name $$interimElement
* @module material.core
*
* @description
*
* Factory that contructs `$$interimElement.$service` services.
* Used internally in material design for elements that appear on screen temporarily.
* The service provides a promise-like API for interacting with the temporary
* elements.
*
* ```js
* app.service('$mdToast', function($$interimElement) {
* var $mdToast = $$interimElement(toastDefaultOptions);
* return $mdToast;
* });
* ```
* @param {object=} defaultOptions Options used by default for the `show` method on the service.
*
* @returns {$$interimElement.$service}
*
*/
function InterimElementProvider() {
createInterimElementProvider.$get = InterimElementFactory;
InterimElementFactory.$inject = ["$document", "$q", "$rootScope", "$timeout", "$rootElement", "$animate", "$mdCompiler", "$mdTheming"];
return createInterimElementProvider;
/**
* Returns a new provider which allows configuration of a new interimElement
* service. Allows configuration of default options & methods for options,
* as well as configuration of 'preset' methods (eg dialog.basic(): basic is a preset method)
*/
function createInterimElementProvider(interimFactoryName) {
var providerConfig = {
presets: {}
};
var provider = {
setDefaults: setDefaults,
addPreset: addPreset,
$get: factory
};
/**
* all interim elements will come with the 'build' preset
*/
provider.addPreset('build', {
methods: ['controller', 'controllerAs', 'onRemove', 'onShow', 'resolve',
'template', 'templateUrl', 'themable', 'transformTemplate', 'parent']
});
factory.$inject = ["$$interimElement", "$animate", "$injector"];
return provider;
/**
* Save the configured defaults to be used when the factory is instantiated
*/
function setDefaults(definition) {
providerConfig.optionsFactory = definition.options;
providerConfig.methods = definition.methods;
return provider;
}
/**
* Save the configured preset to be used when the factory is instantiated
*/
function addPreset(name, definition) {
definition = definition || {};
definition.methods = definition.methods || [];
definition.options = definition.options || function() { return {}; };
if (/^cancel|hide|show$/.test(name)) {
throw new Error("Preset '" + name + "' in " + interimFactoryName + " is reserved!");
}
if (definition.methods.indexOf('_options') > -1) {
throw new Error("Method '_options' in " + interimFactoryName + " is reserved!");
}
providerConfig.presets[name] = {
methods: definition.methods,
optionsFactory: definition.options
};
return provider;
}
/**
* Create a factory that has the given methods & defaults implementing interimElement
*/
/* @ngInject */
function factory($$interimElement, $animate, $injector) {
var defaultMethods;
var defaultOptions;
var interimElementService = $$interimElement();
/*
* publicService is what the developer will be using.
* It has methods hide(), cancel(), show(), build(), and any other
* presets which were set during the config phase.
*/
var publicService = {
hide: interimElementService.hide,
cancel: interimElementService.cancel,
show: showInterimElement
};
defaultMethods = providerConfig.methods || [];
// This must be invoked after the publicService is initialized
defaultOptions = invokeFactory(providerConfig.optionsFactory, {});
angular.forEach(providerConfig.presets, function(definition, name) {
var presetDefaults = invokeFactory(definition.optionsFactory, {});
var presetMethods = (definition.methods || []).concat(defaultMethods);
// Every interimElement built with a preset has a field called `$type`,
// which matches the name of the preset.
// Eg in preset 'confirm', options.$type === 'confirm'
angular.extend(presetDefaults, { $type: name });
// This creates a preset class which has setter methods for every
// method given in the `.addPreset()` function, as well as every
// method given in the `.setDefaults()` function.
//
// @example
// .setDefaults({
// methods: ['hasBackdrop', 'clickOutsideToClose', 'escapeToClose', 'targetEvent'],
// options: dialogDefaultOptions
// })
// .addPreset('alert', {
// methods: ['title', 'ok'],
// options: alertDialogOptions
// })
//
// Set values will be passed to the options when interimElemnt.show() is called.
function Preset(opts) {
this._options = angular.extend({}, presetDefaults, opts);
}
angular.forEach(presetMethods, function(name) {
Preset.prototype[name] = function(value) {
this._options[name] = value;
return this;
};
});
// eg $mdDialog.alert() will return a new alert preset
publicService[name] = function(options) {
return new Preset(options);
};
});
return publicService;
function showInterimElement(opts) {
// opts is either a preset which stores its options on an _options field,
// or just an object made up of options
return interimElementService.show(
angular.extend({}, defaultOptions, (opts || {})._options || opts)
);
}
/**
* Helper to call $injector.invoke with a local of the factory name for
* this provider.
* If an $mdDialog is providing options for a dialog and tries to inject
* $mdDialog, a circular dependency error will happen.
* We get around that by manually injecting $mdDialog as a local.
*/
function invokeFactory(factory, defaultVal) {
var locals = {};
locals[interimFactoryName] = publicService;
return $injector.invoke(factory || function() { return defaultVal; }, {}, locals);
}
}
}
/* @ngInject */
function InterimElementFactory($document, $q, $rootScope, $timeout, $rootElement, $animate, $mdCompiler, $mdTheming) {
return function createInterimElementService() {
/*
* @ngdoc service
* @name $$interimElement.$service
*
* @description
* A service used to control inserting and removing an element into the DOM.
*
*/
var stack = [];
var service;
return service = {
show: show,
hide: hide,
cancel: cancel,
};
function show(options) {
if (stack.length) {
service.cancel();
}
var interimElement = new InterimElement(options);
stack.push(interimElement);
return interimElement.show().then(function() {
return interimElement.deferred.promise;
});
}
/*
* @ngdoc method
* @name $$interimElement.$service#hide
* @kind function
*
* @description
* Removes the `$interimElement` from the DOM and resolves the promise returned from `show`
*
* @param {*} resolveParam Data to resolve the promise with
*
* @returns undefined data that resolves after the element has been removed.
*
*/
function hide(response) {
var interimElement = stack.shift();
interimElement && interimElement.remove().then(function() {
interimElement.deferred.resolve(response);
});
}
/*
* @ngdoc method
* @name $$interimElement.$service#cancel
* @kind function
*
* @description
* Removes the `$interimElement` from the DOM and rejects the promise returned from `show`
*
* @param {*} reason Data to reject the promise with
*
* @returns undefined
*
*/
function cancel(reason) {
var interimElement = stack.shift();
interimElement && interimElement.remove().then(function() {
interimElement.deferred.reject(reason);
});
}
/*
* Internal Interim Element Object
* Used internally to manage the DOM element and related data
*/
function InterimElement(options) {
var self;
var hideTimeout, element;
options = options || {};
options = angular.extend({
scope: options.scope || $rootScope.$new(options.isolateScope),
onShow: function(scope, element, options) {
return $animate.enter(element, options.parent);
},
onRemove: function(scope, element, options) {
// Element could be undefined if a new element is shown before
// the old one finishes compiling.
return element && $animate.leave(element) || $q.when();
}
}, options);
return self = {
options: options,
deferred: $q.defer(),
show: function() {
return $mdCompiler.compile(options).then(function(compileData) {
angular.extend(compileData.locals, self.options);
// Search for parent at insertion time, if not specified
if (angular.isString(options.parent)) {
options.parent = angular.element($document[0].querySelector(options.parent));
} else if (!options.parent) {
options.parent = $rootElement.find('body');
if (!options.parent.length) options.parent = $rootElement;
}
element = compileData.link(options.scope);
if (options.themable) $mdTheming(element);
var ret = options.onShow(options.scope, element, options);
return $q.when(ret)
.then(function(){
// Issue onComplete callback when the `show()` finishes
(options.onComplete || angular.noop)(options.scope, element, options);
startHideTimeout();
});
function startHideTimeout() {
if (options.hideDelay) {
hideTimeout = $timeout(service.cancel, options.hideDelay) ;
}
}
});
},
cancelTimeout: function() {
if (hideTimeout) {
$timeout.cancel(hideTimeout);
hideTimeout = undefined;
}
},
remove: function() {
self.cancelTimeout();
var ret = options.onRemove(options.scope, element, options);
return $q.when(ret).then(function() {
options.scope.$destroy();
});
}
};
}
};
}
}
})();
(function() {
'use strict';
angular.module('material.core')
.factory('$mdInkRipple', InkRippleService)
.directive('mdInkRipple', InkRippleDirective)
.directive('mdNoInk', attrNoDirective())
.directive('mdNoBar', attrNoDirective())
.directive('mdNoStretch', attrNoDirective());
function InkRippleDirective($mdInkRipple) {
return {
controller: angular.noop,
link: function (scope, element, attr) {
if (attr.hasOwnProperty('mdInkRippleCheckbox')) {
$mdInkRipple.attachCheckboxBehavior(scope, element);
} else {
$mdInkRipple.attachButtonBehavior(scope, element);
}
}
};
}
InkRippleDirective.$inject = ["$mdInkRipple"];
function InkRippleService($window, $timeout) {
return {
attachButtonBehavior: attachButtonBehavior,
attachCheckboxBehavior: attachCheckboxBehavior,
attachTabBehavior: attachTabBehavior,
attach: attach
};
function attachButtonBehavior(scope, element, options) {
return attach(scope, element, angular.extend({
isFAB: element.hasClass('md-fab'),
isMenuItem: element.hasClass('md-menu-item'),
center: false,
dimBackground: true
}, options));
}
function attachCheckboxBehavior(scope, element, options) {
return attach(scope, element, angular.extend({
center: true,
dimBackground: false
}, options));
}
function attachTabBehavior(scope, element, options) {
return attach(scope, element, angular.extend({
center: false,
dimBackground: true,
outline: true
}, options));
}
function attach(scope, element, options) {
if (element.controller('mdNoInk')) return angular.noop;
options = angular.extend({
colorElement: element,
mousedown: true,
hover: true,
focus: true,
center: false,
mousedownPauseTime: 150,
dimBackground: false,
outline: false,
isFAB: false,
isMenuItem: false
}, options);
var rippleContainer, rippleSize,
controller = element.controller('mdInkRipple') || {},
counter = 0,
ripples = [],
states = [],
isActiveExpr = element.attr('md-highlight'),
isActive = false,
isHeld = false,
node = element[0],
hammertime = new Hammer(node),
color = parseColor(element.attr('md-ink-ripple')) || parseColor($window.getComputedStyle(options.colorElement[0]).color || 'rgb(0, 0, 0)');
options.mousedown && hammertime.on('hammer.input', onInput);
controller.createRipple = createRipple;
if (isActiveExpr) {
scope.$watch(isActiveExpr, function watchActive(newValue) {
isActive = newValue;
if (isActive && !ripples.length) {
$timeout(function () {
createRipple(0, 0);
}, 0, false);
}
angular.forEach(ripples, updateElement);
});
}
// Publish self-detach method if desired...
return function detach() {
hammertime.destroy();
rippleContainer && rippleContainer.remove();
};
function parseColor(color) {
if (!color) return;
if (color.indexOf('rgba') === 0) return color;
if (color.indexOf('rgb') === 0) return rgbToRGBA(color);
if (color.indexOf('#') === 0) return hexToRGBA(color);
/**
* Converts a hex value to an rgba string
*
* @param {string} hex value (3 or 6 digits) to be converted
*
* @returns {string} rgba color with 0.1 alpha
*/
function hexToRGBA(color) {
var hex = color.charAt(0) === '#' ? color.substr(1) : color,
dig = hex.length / 3,
red = hex.substr(0, dig),
grn = hex.substr(dig, dig),
blu = hex.substr(dig * 2);
if (dig === 1) {
red += red;
grn += grn;
blu += blu;
}
return 'rgba(' + parseInt(red, 16) + ',' + parseInt(grn, 16) + ',' + parseInt(blu, 16) + ',0.1)';
}
/**
* Converts rgb value to rgba string
*
* @param {string} rgb color string
*
* @returns {string} rgba color with 0.1 alpha
*/
function rgbToRGBA(color) {
return color.replace(')', ', 0.1)').replace('(', 'a(')
}
}
function removeElement(elem, wait) {
ripples.splice(ripples.indexOf(elem), 1);
if (ripples.length === 0) {
rippleContainer && rippleContainer.css({ backgroundColor: '' });
}
$timeout(function () { elem.remove(); }, wait, false);
}
function updateElement(elem) {
var index = ripples.indexOf(elem),
state = states[index] || {},
elemIsActive = ripples.length > 1 ? false : isActive,
elemIsHeld = ripples.length > 1 ? false : isHeld;
if (elemIsActive || state.animating || elemIsHeld) {
elem.addClass('md-ripple-visible');
} else {
elem.removeClass('md-ripple-visible');
if (options.outline) {
elem.css({
width: rippleSize + 'px',
height: rippleSize + 'px',
marginLeft: (rippleSize * -1) + 'px',
marginTop: (rippleSize * -1) + 'px'
});
}
removeElement(elem, options.outline ? 450 : 650);
}
}
/**
* Creates a ripple at the provided coordinates
*
* @param {number} left cursor position
* @param {number} top cursor position
*
* @returns {angular.element} the generated ripple element
*/
function createRipple(left, top) {
var container = getRippleContainer(),
size = getRippleSize(left, top),
css = getRippleCss(size, left, top),
elem = getRippleElement(css),
index = ripples.indexOf(elem),
state = states[index] || {};
rippleSize = size;
state.animating = true;
$timeout(function () {
if (options.dimBackground) {
container.css({ backgroundColor: color });
}
elem.addClass('md-ripple-placed md-ripple-scaled');
if (options.outline) {
elem.css({
borderWidth: (size * 0.5) + 'px',
marginLeft: (size * -0.5) + 'px',
marginTop: (size * -0.5) + 'px'
});
} else {
elem.css({ left: '50%', top: '50%' });
}
updateElement(elem);
$timeout(function () {
state.animating = false;
updateElement(elem);
}, (options.outline ? 450 : 225), false);
}, 0, false);
return elem;
/**
* Creates the ripple element with the provided css
*
* @param {object} css properties to be applied
*
* @returns {angular.element} the generated ripple element
*/
function getRippleElement(css) {
var elem = angular.element('<div class="md-ripple" data-counter="' + counter++ + '">');
ripples.unshift(elem);
states.unshift({ animating: true });
container.append(elem);
css && elem.css(css);
return elem;
}
/**
* Calculate the ripple size
*
* @returns {number} calculated ripple diameter
*/
function getRippleSize(left, top) {
var width = container.prop('offsetWidth'),
height = container.prop('offsetHeight'),
multiplier, size, rect;
if (options.isMenuItem) {
size = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2));
} else if (options.outline) {
rect = node.getBoundingClientRect();
left -= rect.left;
top -= rect.top;
width = Math.max(left, width - left);
height = Math.max(top, height - top);
size = 2 * Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2));
} else {
multiplier = options.isFAB ? 1.1 : 0.8;
size = Math.max(width, height) * multiplier;
}
return size;
}
/**
* Generates the ripple css
*
* @param {number} the diameter of the ripple
* @param {number} the left cursor offset
* @param {number} the top cursor offset
*
* @returns {{backgroundColor: *, width: string, height: string, marginLeft: string, marginTop: string}}
*/
function getRippleCss(size, left, top) {
var rect,
css = {
backgroundColor: rgbaToRGB(color),
borderColor: rgbaToRGB(color),
width: size + 'px',
height: size + 'px'
};
if (options.outline) {
css.width = 0;
css.height = 0;
} else {
css.marginLeft = css.marginTop = (size * -0.5) + 'px';
}
if (options.center) {
css.left = css.top = '50%';
} else {
rect = node.getBoundingClientRect();
css.left = Math.round((left - rect.left) / container.prop('offsetWidth') * 100) + '%';
css.top = Math.round((top - rect.top) / container.prop('offsetHeight') * 100) + '%';
}
return css;
/**
* Converts rgba string to rgb, removing the alpha value
*
* @param {string} rgba color
*
* @returns {string} rgb color
*/
function rgbaToRGB(color) {
return color.replace('rgba', 'rgb').replace(/,[^\)\,]+\)/, ')');
}
}
/**
* Gets the current ripple container
* If there is no ripple container, it creates one and returns it
*
* @returns {angular.element} ripple container element
*/
function getRippleContainer() {
if (rippleContainer) return rippleContainer;
var container = rippleContainer = angular.element('<div class="md-ripple-container">');
element.append(container);
return container;
}
}
/**
* Handles user input start and stop events
*
* @param {event} event fired by hammer.js
*/
function onInput(ev) {
var ripple, index;
if (ev.eventType === Hammer.INPUT_START && ev.isFirst && isRippleAllowed()) {
ripple = createRipple(ev.center.x, ev.center.y);
isHeld = true;
} else if (ev.eventType === Hammer.INPUT_END && ev.isFinal) {
isHeld = false;
index = ripples.length - 1;
ripple = ripples[index];
$timeout(function () {
updateElement(ripple);
}, 0, false);
}
/**
* Determines if the ripple is allowed
*
* @returns {boolean} true if the ripple is allowed, false if not
*/
function isRippleAllowed() {
var parent = node.parentNode;
return !node.hasAttribute('disabled') && !(parent && parent.hasAttribute('disabled'));
}
}
}
}
InkRippleService.$inject = ["$window", "$timeout"];
/**
* 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 md-no-ink>
* <child detect-no>
* </child>
* </parent>
* </hljs>
*
* <hljs lang="js">
* myApp.directive('detectNo', function() {
* return {
* require: ['^?mdNoInk', ^?mdNoBar'],
* link: function(scope, element, attr, ctrls) {
* var noinkCtrl = ctrls[0];
* var nobarCtrl = ctrls[1];
* if (noInkCtrl) {
* alert("the md-no-ink flag has been specified on an ancestor!");
* }
* if (nobarCtrl) {
* alert("the md-no-bar flag has been specified on an ancestor!");
* }
* }
* };
* });
* </hljs>
*/
function attrNoDirective() {
return function() {
return {
controller: angular.noop
};
};
}
})();
(function() {
'use strict';
angular.module('material.core')
.directive('mdTheme', ThemingDirective)
.directive('mdThemable', ThemableDirective)
.provider('$mdTheming', ThemingProvider);
/**
* @ngdoc provider
* @name $mdThemingProvider
* @module material.core
*
* @description Provider to configure the `$mdTheming` service.
*/
/**
* @ngdoc method
* @name $mdThemingProvider#setDefaultTheme
* @param {string} themeName Default theme name to be applied to elements. Default value is `default`.
*/
/**
* @ngdoc method
* @name $mdThemingProvider#alwaysWatchTheme
* @param {boolean} watch Whether or not to always watch themes for changes and re-apply
* classes when they change. Default is `false`. Enabling can reduce performance.
*/
function ThemingProvider() {
var defaultTheme = 'default';
var alwaysWatchTheme = false;
return {
setDefaultTheme: function(theme) {
defaultTheme = theme;
},
alwaysWatchTheme: function(alwaysWatch) {
alwaysWatchTheme = alwaysWatch;
},
$get: ['$rootScope', ThemingService]
};
/**
* @ngdoc service
* @name $mdTheming
*
* @description
*
* Service that makes an element apply theming related classes to itself.
*
* ```js
* app.directive('myFancyDirective', function($mdTheming) {
* return {
* restrict: 'e',
* link: function(scope, el, attrs) {
* $mdTheming(el);
* }
* };
* });
* ```
* @param {el=} element to apply theming to
*/
function ThemingService($rootScope) {
applyTheme.inherit = function(el, parent) {
var ctrl = parent.controller('mdTheme');
var attrThemeValue = el.attr('md-theme-watch');
if ( (alwaysWatchTheme || angular.isDefined(attrThemeValue)) && attrThemeValue != 'false') {
var deregisterWatch = $rootScope.$watch(function() {
return ctrl && ctrl.$mdTheme || defaultTheme;
}, changeTheme);
el.on('$destroy', deregisterWatch);
} else {
var theme = ctrl && ctrl.$mdTheme || defaultTheme;
changeTheme(theme);
}
function changeTheme(theme) {
var oldTheme = el.data('$mdThemeName');
if (oldTheme) el.removeClass('md-' + oldTheme +'-theme');
el.addClass('md-' + theme + '-theme');
el.data('$mdThemeName', theme);
}
};
return applyTheme;
function applyTheme(scope, el) {
// Allow us to be invoked via a linking function signature.
if (el === undefined) {
el = scope;
scope = undefined;
}
if (scope === undefined) {
scope = $rootScope;
}
applyTheme.inherit(el, el);
}
}
}
function ThemingDirective($interpolate) {
return {
priority: 100,
link: {
pre: function(scope, el, attrs) {
var ctrl = {
$setTheme: function(theme) {
ctrl.$mdTheme = theme;
}
};
el.data('$mdThemeController', ctrl);
ctrl.$setTheme($interpolate(attrs.mdTheme)(scope));
attrs.$observe('mdTheme', ctrl.$setTheme);
}
}
};
}
ThemingDirective.$inject = ["$interpolate"];
function ThemableDirective($mdTheming) {
return $mdTheming;
}
ThemableDirective.$inject = ["$mdTheming"];
})();
(function() {
'use strict';
/*
* @ngdoc module
* @name material.components.backdrop
* @description Backdrop
*/
/**
* @ngdoc directive
* @name mdBackdrop
* @module material.components.backdrop
*
* @restrict E
*
* @description
* `<md-backdrop>` is a backdrop element used by other coponents, such as dialog and bottom sheet.
* Apply class `opaque` to make the backdrop use the theme backdrop color.
*
*/
angular.module('material.components.backdrop', [
'material.core'
])
.directive('mdBackdrop', BackdropDirective);
function BackdropDirective($mdTheming) {
return $mdTheming;
}
BackdropDirective.$inject = ["$mdTheming"];
})();
(function() {
'use strict';
/**
* @ngdoc module
* @name material.components.bottomSheet
* @description
* BottomSheet
*/
angular.module('material.components.bottomSheet', [
'material.core',
'material.components.backdrop'
])
.directive('mdBottomSheet', MdBottomSheetDirective)
.provider('$mdBottomSheet', MdBottomSheetProvider);
function MdBottomSheetDirective() {
return {
restrict: 'E'
};
}
/**
* @ngdoc service
* @name $mdBottomSheet
* @module material.components.bottomSheet
*
* @description
* `$mdBottomSheet` opens a bottom sheet over the app and provides a simple promise API.
*
* ### Restrictions
*
* - The bottom sheet's template must have an outer `<md-bottom-sheet>` element.
* - Add the `md-grid` class to the bottom sheet for a grid layout.
* - Add the `md-list` class to the bottom sheet for a list layout.
*
* @usage
* <hljs lang="html">
* <div ng-controller="MyController">
* <md-button ng-click="openBottomSheet()">
* Open a Bottom Sheet!
* </md-button>
* </div>
* </hljs>
* <hljs lang="js">
* var app = angular.module('app', ['ngMaterial']);
* app.controller('MyController', function($scope, $mdBottomSheet) {
* $scope.openBottomSheet = function() {
* $mdBottomSheet.show({
* template: '<md-bottom-sheet>Hello!</md-bottom-sheet>'
* });
* };
* });
* </hljs>
*/
/**
* @ngdoc method
* @name $mdBottomSheet#show
*
* @description
* Show a bottom sheet with the specified options.
*
* @param {object} options An options object, with the following properties:
*
* - `templateUrl` - `{string=}`: The url of an html template file that will
* be used as the content of the bottom sheet. Restrictions: the template must
* have an outer `md-bottom-sheet` element.
* - `template` - `{string=}`: Same as templateUrl, except this is an actual
* template string.
* - `controller` - `{string=}`: The controller to associate with this bottom sheet.
* - `locals` - `{string=}`: 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.
* - `targetEvent` - `{DOMClickEvent=}`: 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.
* - `resolve` - `{object=}`: Similar to locals, except it takes promises as values
* and the bottom sheet will not open until the promises resolve.
* - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope.
*
* @returns {promise} A promise that can be resolved with `$mdBottomSheet.hide()` or
* rejected with `$mdBottomSheet.cancel()`.
*/
/**
* @ngdoc method
* @name $mdBottomSheet#hide
*
* @description
* Hide the existing bottom sheet and resolve the promise returned from
* `$mdBottomSheet.show()`.
*
* @param {*=} response An argument for the resolved promise.
*
*/
/**
* @ngdoc method
* @name $mdBottomSheet#cancel
*
* @description
* Hide the existing bottom sheet and reject the promise returned from
* `$mdBottomSheet.show()`.
*
* @param {*=} response An argument for the rejected promise.
*
*/
function MdBottomSheetProvider($$interimElementProvider) {
bottomSheetDefaults.$inject = ["$animate", "$mdConstant", "$timeout", "$$rAF", "$compile", "$mdTheming", "$mdBottomSheet", "$rootElement"];
return $$interimElementProvider('$mdBottomSheet')
.setDefaults({
options: bottomSheetDefaults
});
/* @ngInject */
function bottomSheetDefaults($animate, $mdConstant, $timeout, $$rAF, $compile, $mdTheming,
$mdBottomSheet, $rootElement) {
var backdrop;
return {
themable: true,
targetEvent: null,
onShow: onShow,
onRemove: onRemove,
escapeToClose: true
};
function onShow(scope, element, options) {
// Add a backdrop that will close on click
backdrop = $compile('<md-backdrop class="md-opaque ng-enter">')(scope);
backdrop.on('click touchstart', function() {
$timeout($mdBottomSheet.cancel);
});
$mdTheming.inherit(backdrop, options.parent);
$animate.enter(backdrop, options.parent, null);
var bottomSheet = new BottomSheet(element);
options.bottomSheet = bottomSheet;
// Give up focus on calling item
options.targetEvent && angular.element(options.targetEvent.target).blur();
$mdTheming.inherit(bottomSheet.element, options.parent);
return $animate.enter(bottomSheet.element, options.parent)
.then(function() {
var focusableItems = angular.element(
element[0].querySelector('button') ||
element[0].querySelector('a') ||
element[0].querySelector('[ng-click]')
);
focusableItems.eq(0).focus();
if (options.escapeToClose) {
options.rootElementKeyupCallback = function(e) {
if (e.keyCode === $mdConstant.KEY_CODE.ESCAPE) {
$timeout($mdBottomSheet.cancel);
}
};
$rootElement.on('keyup', options.rootElementKeyupCallback);
}
});
}
function onRemove(scope, element, options) {
var bottomSheet = options.bottomSheet;
$animate.leave(backdrop);
return $animate.leave(bottomSheet.element).then(function() {
bottomSheet.cleanup();
// Restore focus
options.targetEvent && angular.element(options.targetEvent.target).focus();
});
}
/**
* BottomSheet class to apply bottom-sheet behavior to an element
*/
function BottomSheet(element) {
var MAX_OFFSET = 80; // amount past the bottom of the element that we can drag down, this is same as in _bottomSheet.scss
var WIGGLE_AMOUNT = 20; // point where it starts to get "harder" to drag
var CLOSING_VELOCITY = 10; // how fast we need to flick down to close the sheet
var startY, lastY, velocity, transitionDelay, startTarget;
// coercion incase $mdCompiler returns multiple elements
element = element.eq(0);
element.on('touchstart', onTouchStart)
.on('touchmove', onTouchMove)
.on('touchend', onTouchEnd);
return {
element: element,
cleanup: function cleanup() {
element.off('touchstart', onTouchStart)
.off('touchmove', onTouchMove)
.off('touchend', onTouchEnd);
}
};
function onTouchStart(e) {
e.preventDefault();
startTarget = e.target;
startY = getY(e);
// Disable transitions on transform so that it feels fast
transitionDelay = element.css($mdConstant.CSS.TRANSITION_DURATION);
element.css($mdConstant.CSS.TRANSITION_DURATION, '0s');
}
function onTouchEnd(e) {
// Re-enable the transitions on transforms
element.css($mdConstant.CSS.TRANSITION_DURATION, transitionDelay);
var currentY = getY(e);
// If we didn't scroll much, and we didn't change targets, assume its a click
if ( Math.abs(currentY - startY) < 5 && e.target == startTarget) {
angular.element(e.target).triggerHandler('click');
} else {
// If they went fast enough, trigger a close.
if (velocity > CLOSING_VELOCITY) {
$timeout($mdBottomSheet.cancel);
// Otherwise, untransform so that we go back to our normal position
} else {
setTransformY(undefined);
}
}
}
function onTouchMove(e) {
var currentY = getY(e);
var delta = currentY - startY;
velocity = currentY - lastY;
lastY = currentY;
// Do some conversion on delta to get a friction-like effect
delta = adjustedDelta(delta);
setTransformY(delta + MAX_OFFSET);
}
/**
* Helper function to find the Y aspect of various touch events.
**/
function getY(e) {
var touch = e.touches && e.touches.length ? e.touches[0] : e.changedTouches[0];
return touch.clientY;
}
/**
* Transform the element along the y-axis
**/
function setTransformY(amt) {
if (amt === null || amt === undefined) {
element.css($mdConstant.CSS.TRANSFORM, '');
} else {
element.css($mdConstant.CSS.TRANSFORM, 'translate3d(0, ' + amt + 'px, 0)');
}
}
// Returns a new value for delta that will never exceed MAX_OFFSET_AMOUNT
// Will get harder to exceed it as you get closer to it
function adjustedDelta(delta) {
if ( delta < 0 && delta < -MAX_OFFSET + WIGGLE_AMOUNT) {
delta = -delta;
var base = MAX_OFFSET - WIGGLE_AMOUNT;
delta = Math.max(-MAX_OFFSET, -Math.min(MAX_OFFSET - 5, base + ( WIGGLE_AMOUNT * (delta - base)) / MAX_OFFSET) - delta / 50);
}
return delta;
}
}
}
}
MdBottomSheetProvider.$inject = ["$$interimElementProvider"];
})();
(function() {
'use strict';
/**
* @ngdoc module
* @name material.components.button
* @description
*
* Button
*/
angular.module('material.components.button', [
'material.core'
])
.directive('mdButton', MdButtonDirective);
/**
* @ngdoc directive
* @name mdButton
* @module material.components.button
*
* @restrict E
*
* @description
* `<md-button>` is a button directive with optional ink ripples (default enabled).
*
* If you supply a `href` or `ng-href` attribute, it will become an `<a>` element. Otherwise, it will
* become a `<button>` element.
*
* @param {boolean=} mdNoInk If present, disable ripple ink effects.
* @param {expression=} ngDisabled En/Disable based on the expre
* @param {string=} ariaLabel Publish the button label used by screen-readers for accessibility. Defaults to the button's text.
*
* @usage
* <hljs lang="html">
* <md-button>
* Button
* </md-button>
* <md-button href="http://google.com" class="md-button-colored">
* I'm a link
* </md-button>
* <md-button ng-disabled="true" class="md-colored">
* I'm a disabled button
* </md-button>
* </hljs>
*/
function MdButtonDirective($mdInkRipple, $mdTheming, $mdAria) {
return {
restrict: 'E',
replace: true,
transclude: true,
template: getTemplate,
link: postLink
};
function isAnchor(attr) {
return angular.isDefined(attr.href) || angular.isDefined(attr.ngHref);
}
function getTemplate(element, attr) {
if (isAnchor(attr)) {
return '<a class="md-button" ng-transclude></a>';
} else {
return '<button class="md-button" ng-transclude></button>';
}
}
function postLink(scope, element, attr) {
var node = element[0];
$mdTheming(element);
$mdInkRipple.attachButtonBehavior(scope, element);
var elementHasText = node.textContent.trim();
if (!elementHasText) {
$mdAria.expect(element, 'aria-label');
}
// For anchor elements, we have to set tabindex manually when the
// element is disabled
if (isAnchor(attr)) {
scope.$watch(attr.ngDisabled, function(isDisabled) {
element.attr('tabindex', isDisabled ? -1 : 0);
});
}
}
}
MdButtonDirective.$inject = ["$mdInkRipple", "$mdTheming", "$mdAria"];
})();
(function() {
'use strict';
/**
* @ngdoc module
* @name material.components.card
*
* @description
* Card components.
*/
angular.module('material.components.card', [
'material.core'
])
.directive('mdCard', mdCardDirective);
/**
* @ngdoc directive
* @name mdCard
* @module material.components.card
*
* @restrict E
*
* @description
* The `<md-card>` directive is a container element used within `<md-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">
* <md-card>
* <img src="img/washedout.png" class="md-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>
* </md-card>
* </hljs>
*
*/
function mdCardDirective($mdTheming) {
return {
restrict: 'E',
link: function($scope, $element, $attr) {
$mdTheming($element);
}
};
}
mdCardDirective.$inject = ["$mdTheming"];
})();
(function() {
'use strict';
/**
* @ngdoc module
* @name material.components.checkbox
* @description Checkbox module!
*/
angular.module('material.components.checkbox', [
'material.core'
])
.directive('mdCheckbox', MdCheckboxDirective);
/**
* @ngdoc directive
* @name mdCheckbox
* @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=} mdNoInk Use of attribute indicates use of ripple ink effects
* @param {string=} ariaLabel Publish the button label used by screen-readers for accessibility. Defaults to the checkbox's text.
*
* @usage
* <hljs lang="html">
* <md-checkbox ng-model="isChecked" aria-label="Finished?">
* Finished ?
* </md-checkbox>
*
* <md-checkbox md-no-ink ng-model="hasInk" aria-label="No Ink Effects">
* No Ink Effects
* </md-checkbox>
*
* <md-checkbox ng-disabled="true" ng-model="isDisabled" aria-label="Disabled">
* Disabled
* </md-checkbox>
*
* </hljs>
*
*/
function MdCheckboxDirective(inputDirective, $mdInkRipple, $mdAria, $mdConstant, $mdTheming) {
inputDirective = inputDirective[0];
var CHECKED_CSS = 'md-checked';
return {
restrict: 'E',
transclude: true,
require: '?ngModel',
template:
'<div class="md-container" md-ink-ripple md-ink-ripple-checkbox>' +
'<div class="md-icon"></div>' +
'</div>' +
'<div ng-transclude class="md-label"></div>',
compile: compile
};
// **********************************************************
// Private Methods
// **********************************************************
function compile (tElement, tAttrs) {
tAttrs.type = 'checkbox';
tAttrs.tabIndex = 0;
tElement.attr('role', tAttrs.type);
return function postLink(scope, element, attr, ngModelCtrl) {
var checked = false;
$mdTheming(element);
// Create a mock ngModel if the user doesn't provide one
ngModelCtrl = ngModelCtrl || {
$setViewValue: function(value) {
this.$viewValue = value;
},
$parsers: [],
$formatters: []
};
$mdAria.expectWithText(tElement, 'aria-label');
// 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.
inputDirective.link.pre(scope, {
on: angular.noop,
0: {}
}, attr, [ngModelCtrl]);
element.on('click', listener);
element.on('keypress', keypressHandler);
ngModelCtrl.$render = render;
function keypressHandler(ev) {
if(ev.which === $mdConstant.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;
if(checked) {
element.addClass(CHECKED_CSS);
} else {
element.removeClass(CHECKED_CSS);
}
}
};
}
}
MdCheckboxDirective.$inject = ["inputDirective", "$mdInkRipple", "$mdAria", "$mdConstant", "$mdTheming"];
})();
(function() {
'use strict';
/**
* @ngdoc module
* @name material.components.content
*
* @description
* Scrollable content
*/
angular.module('material.components.content', [
'material.core'
])
.directive('mdContent', mdContentDirective);
/**
* @ngdoc directive
* @name mdContent
* @module material.components.content
*
* @restrict E
*
* @description
* The `<md-content>` directive is a container element useful for scrollable content
*
* ### Restrictions
*
* - Add the `md-padding` class to make the content padded.
*
* @usage
* <hljs lang="html">
* <md-content class="md-padding">
* Lorem ipsum dolor sit amet, ne quod novum mei.
* </md-content>
* </hljs>
*
*/
function mdContentDirective($mdTheming) {
return {
restrict: 'E',
controller: ['$scope', '$element', ContentController],
link: function($scope, $element, $attr) {
$mdTheming($element);
$scope.$broadcast('$mdContentLoaded', $element);
}
};
function ContentController($scope, $element) {
this.$scope = $scope;
this.$element = $element;
}
}
mdContentDirective.$inject = ["$mdTheming"];
})();
(function() {
'use strict';
/**
* @ngdoc module
* @name material.components.dialog
*/
angular.module('material.components.dialog', [
'material.core',
'material.components.backdrop'
])
.directive('mdDialog', MdDialogDirective)
.provider('$mdDialog', MdDialogProvider);
function MdDialogDirective($$rAF, $mdTheming) {
return {
restrict: 'E',
link: function(scope, element, attr) {
$mdTheming(element);
$$rAF(function() {
var content = element[0].querySelector('md-content');
if (content && content.scrollHeight > content.clientHeight) {
element.addClass('md-content-overflow');
}
});
}
};
}
MdDialogDirective.$inject = ["$$rAF", "$mdTheming"];
/**
* @ngdoc service
* @name $mdDialog
* @module material.components.dialog
*
* @description
* `$mdDialog` opens a dialog over the app and provides a simple promise API.
*
* ### Restrictions
*
* - The dialog is always given an isolate scope.
* - The dialog's template must have an outer `<md-dialog>` element.
* Inside, use an `<md-content>` element for the dialog's content, and use
* an element with class `md-actions` for the dialog's actions.
*
* @usage
* ##### HTML
*
* <hljs lang="html">
* <div ng-app="demoApp" ng-controller="EmployeeController">
* <md-button ng-click="showAlert()" class="md-raised md-warn">
* Employee Alert!
* </md-button>
* <md-button ng-click="closeAlert()" ng-disabled="!hasAlert()" class="md-raised">
* Close Alert
* </md-button>
* <md-button ng-click="showGreeting($event)" class="md-raised md-primary" >
* Greet Employee
* </md-button>
* </div>
* </hljs>
*
* ##### JavaScript
*
* <hljs lang="js">
* (function(angular, undefined){
* "use strict";
*
* angular
* .module('demoApp', ['ngMaterial'])
* .controller('EmployeeController', EmployeeEditor)
* .controller('GreetingController', GreetingController);
*
* // Fictitious Employee Editor to show how to use simple and complex dialogs.
*
* function EmployeeEditor($scope, $mdDialog) {
* var alert;
*
* $scope.showAlert = showAlert;
* $scope.closeAlert = closeAlert;
* $scope.showGreeting = showCustomGreeting;
*
* $scope.hasAlert = function() { return !!alert };
* $scope.userName = $scope.userName || 'Bobby';
*
* // Dialog #1 - Show simple alert dialog and cache
* // reference to dialog instance
*
* function showAlert() {
* alert = $mdDialog.alert()
* .title('Attention, ' + $scope.userName)
* .content('This is an example of how easy dialogs can be!')
* .ok('Close');
*
* $mdDialog
* .show( alert )
* .finally(function() {
* alert = undefined;
* });
* }
*
* // Close the specified dialog instance and resolve with 'finished' flag
* // Normally this is not needed, just use '$mdDialog.hide()' to close
* // the most recent dialog popup.
*
* function closeAlert() {
* $mdDialog.hide( alert, "finished" );
* alert = undefined;
* }
*
* // Dialog #2 - Demonstrate more complex dialogs construction and popup.
*
* function showCustomGreeting($event) {
* $mdDialog.show({
* targetEvent: $event,
* template:
* '<md-dialog>' +
*
* ' <md-content>Hello {{ employee }}!</md-content>' +
*
* ' <div class="md-actions">' +
* ' <md-button ng-click="closeDialog()">' +
* ' Close Greeting' +
*
* ' </md-button>' +
* ' </div>' +
* '</md-dialog>',
* controller: 'GreetingController',
* onComplete: afterShowAnimation,
* locals: { employee: $scope.userName }
* });
*
* // When the 'enter' animation finishes...
*
* function afterShowAnimation(scope, element, options) {
* // post-show code here: DOM element focus, etc.
* }
* }
* }
*
* // Greeting controller used with the more complex 'showCustomGreeting()' custom dialog
*
* function GreetingController($scope, $mdDialog, employee) {
* // Assigned from construction <code>locals</code> options...
* $scope.employee = employee;
*
* $scope.closeDialog = function() {
* // Easily hides most recent dialog shown...
* // no specific instance reference is needed.
* $mdDialog.hide();
* };
* }
*
* })(angular);
* </hljs>
*/
/**
* @ngdoc method
* @name $mdDialog#alert
*
* @description
* Builds a preconfigured dialog with the specified message.
*
* @returns {obj} an `$mdDialogPreset` with the chainable configuration methods:
*
* - $mdDialogPreset#title(string) - sets title to string
* - $mdDialogPreset#content(string) - sets content / message to string
* - $mdDialogPreset#ok(string) - sets okay button text to string
*
*/
/**
* @ngdoc method
* @name $mdDialog#confirm
*
* @description
* Builds a preconfigured dialog with the specified message. You can call show and the promise returned
* will be resolved only if the user clicks the confirm action on the dialog.
*
* @returns {obj} an `$mdDialogPreset` with the chainable configuration methods:
*
* Additionally, it supports the following methods:
*
* - $mdDialogPreset#title(string) - sets title to string
* - $mdDialogPreset#content(string) - sets content / message to string
* - $mdDialogPreset#ok(string) - sets okay button text to string
* - $mdDialogPreset#cancel(string) - sets cancel button text to string
*
*/
/**
* @ngdoc method
* @name $mdDialog#show
*
* @description
* Show a dialog with the specified options.
*
* @param {object} optionsOrPreset Either provide an `$mdToastPreset` returned from `alert()`,
* `confirm()`, and `build()`, or an options object with the following properties:
* - `templateUrl` - `{string=}`: The url of a template that will be used as the content
* of the dialog.
* - `template` - `{string=}`: Same as templateUrl, except this is an actual template string.
* - `targetEvent` - `{DOMClickEvent=}`: 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.
* - `hasBackdrop` - `{boolean=}`: Whether there should be an opaque backdrop behind the dialog.
* Default true.
* - `clickOutsideToClose` - `{boolean=}`: Whether the user can click outside the dialog to
* close it. Default true.
* - `escapeToClose` - `{boolean=}`: Whether the user can press escape to close the dialog.
* Default true.
* - `controller` - `{string=}`: 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.
* - `locals` - `{object=}`: 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. If `bindToController` is true, they will be
* coppied to the controller instead.
* - `bindToController` - `bool`: bind the locals to the controller, instead of passing them in
* - `resolve` - `{object=}`: Similar to locals, except it takes promises as values, and the
* toast will not open until all of the promises resolve.
* - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope.
* - `parent` - `{element=}`: The element to append the dialog to. Defaults to appending
* to the root element of the application.
* - `onComplete` `{function=}`: Callback function used to announce when the show() action is
* finished.
*
* @returns {promise} A promise that can be resolved with `$mdDialog.hide()` or
* rejected with `mdDialog.cancel()`.
*/
/**
* @ngdoc method
* @name $mdDialog#hide
*
* @description
* Hide an existing dialog and resolve the promise returned from `$mdDialog.show()`.
*
* @param {*=} response An argument for the resolved promise.
*/
/**
* @ngdoc method
* @name $mdDialog#cancel
*
* @description
* Hide an existing dialog and reject the promise returned from `$mdDialog.show()`.
*
* @param {*=} response An argument for the rejected promise.
*/
function MdDialogProvider($$interimElementProvider) {
var alertDialogMethods = ['title', 'content', 'ariaLabel', 'ok'];
advancedDialogOptions.$inject = ["$mdDialog"];
dialogDefaultOptions.$inject = ["$timeout", "$rootElement", "$compile", "$animate", "$mdAria", "$mdUtil", "$mdConstant", "$mdTheming", "$$rAF", "$q", "$mdDialog"];
return $$interimElementProvider('$mdDialog')
.setDefaults({
methods: ['hasBackdrop', 'clickOutsideToClose', 'escapeToClose', 'targetEvent'],
options: dialogDefaultOptions
})
.addPreset('alert', {
methods: alertDialogMethods,
options: advancedDialogOptions
})
.addPreset('confirm', {
methods: alertDialogMethods.concat('cancel'),
options: advancedDialogOptions
});
/* @ngInject */
function advancedDialogOptions($mdDialog) {
return {
template: [
'<md-dialog aria-label="{{dialog.label}}">',
'<md-content>',
'<h2>{{ dialog.title }}</h2>',
'<p>{{ dialog.content }}</p>',
'</md-content>',
'<div class="md-actions">',
'<md-button ng-if="dialog.$type == \'confirm\'" ng-click="dialog.abort()">',
'{{ dialog.cancel }}',
'</md-button>',
'<md-button ng-click="dialog.hide()" class="md-primary">',
'{{ dialog.ok }}',
'</md-button>',
'</div>',
'</md-dialog>'
].join(''),
controller: function mdDialogCtrl() {
this.hide = function() {
$mdDialog.hide(true);
};
this.abort = function() {
$mdDialog.cancel();
};
},
controllerAs: 'dialog',
bindToController: true
};
}
/* @ngInject */
function dialogDefaultOptions($timeout, $rootElement, $compile, $animate, $mdAria,
$mdUtil, $mdConstant, $mdTheming, $$rAF, $q, $mdDialog) {
return {
hasBackdrop: true,
isolateScope: true,
onShow: onShow,
onRemove: onRemove,
clickOutsideToClose: true,
escapeToClose: true,
targetEvent: null,
transformTemplate: function(template) {
return '<div class="md-dialog-container">' + template + '</div>';
}
};
// On show method for dialogs
function onShow(scope, element, options) {
// Incase the user provides a raw dom element, always wrap it in jqLite
options.parent = angular.element(options.parent);
options.popInTarget = angular.element((options.targetEvent || {}).target);
var closeButton = findCloseButton();
configureAria(element.find('md-dialog'));
if (options.hasBackdrop) {
options.backdrop = $compile('<md-backdrop class="md-opaque ng-enter">')(scope);
$mdTheming.inherit(options.backdrop, options.parent);
$animate.enter(options.backdrop, options.parent, null);
}
return dialogPopIn(
element,
options.parent,
options.popInTarget.length && options.popInTarget
)
.then(function() {
if (options.escapeToClose) {
options.rootElementKeyupCallback = function(e) {
if (e.keyCode === $mdConstant.KEY_CODE.ESCAPE) {
$timeout($mdDialog.cancel);
}
};
$rootElement.on('keyup', options.rootElementKeyupCallback);
}
if (options.clickOutsideToClose) {
options.dialogClickOutsideCallback = function(e) {
// Only close if we click the flex container outside the backdrop
if (e.target === element[0]) {
$timeout($mdDialog.cancel);
}
};
element.on('click', options.dialogClickOutsideCallback);
}
closeButton.focus();
});
function findCloseButton() {
//If no element with class dialog-close, try to find the last
//button child in md-actions and assume it is a close button
var closeButton = element[0].querySelector('.dialog-close');
if (!closeButton) {
var actionButtons = element[0].querySelectorAll('.md-actions button');
closeButton = actionButtons[ actionButtons.length - 1 ];
}
return angular.element(closeButton);
}
}
// On remove function for all dialogs
function onRemove(scope, element, options) {
if (options.backdrop) {
$animate.leave(options.backdrop);
element.data('backdrop', undefined);
}
if (options.escapeToClose) {
$rootElement.off('keyup', options.rootElementKeyupCallback);
}
if (options.clickOutsideToClose) {
element.off('click', options.dialogClickOutsideCallback);
}
return $animate.leave(element).then(function() {
element.remove();
options.popInTarget && options.popInTarget.focus();
});
}
/**
* Inject ARIA-specific attributes appropriate for Dialogs
*/
function configureAria(element) {
element.attr({
'role': 'dialog'
});
var dialogContent = element.find('md-content');
if (dialogContent.length === 0){
dialogContent = element;
}
$mdAria.expectAsync(element, 'aria-label', function() {
var words = dialogContent.text().split(/\s+/);
if (words.length > 3) words = words.slice(0,3).concat('...');
return words.join(' ');
});
}
function dialogPopIn(element, parentElement, clickElement) {
var deferred = $q.defer();
parentElement.append(element);
var startPos;
if (clickElement) {
var clickRect = clickElement[0].getBoundingClientRect();
startPos = 'translate3d(' +
(clickRect.left - element[0].offsetWidth / 2) + 'px,' +
(clickRect.top - element[0].offsetHeight / 2) + 'px,' +
'0) scale(0.2)';
} else {
startPos = 'translate3d(0,100%,0) scale(0.5)';
}
element
.css($mdConstant.CSS.TRANSFORM, startPos)
.css('opacity', 0);
$$rAF(function() {
$$rAF(function() {
element
.addClass('md-active')
.css($mdConstant.CSS.TRANSFORM, '')
.css('opacity', '')
.on($mdConstant.CSS.TRANSITIONEND, finished);
});
});
function finished(ev) {
//Make sure this transitionend didn't bubble up from a child
if (ev.target === element[0]) {
element.off($mdConstant.CSS.TRANSITIONEND, finished);
deferred.resolve();
}
}
return deferred.promise;
}
}
}
MdDialogProvider.$inject = ["$$interimElementProvider"];
})();
(function() {
'use strict';
/**
* @ngdoc module
* @name material.components.divider
* @description Divider module!
*/
angular.module('material.components.divider', [
'material.core'
])
.directive('mdDivider', MdDividerDirective);
function MdDividerController(){}
/**
* @ngdoc directive
* @name mdDivider
* @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=} mdInset Add this attribute to activate the inset divider style.
* @usage
* <hljs lang="html">
* <md-divider></md-divider>
*
* <md-divider md-inset></md-divider>
* </hljs>
*
*/
function MdDividerDirective($mdTheming) {
return {
restrict: 'E',
link: $mdTheming,
controller: [MdDividerController]
};
}
MdDividerDirective.$inject = ["$mdTheming"];
})();
(function() {
'use strict';
/*
* @ngdoc module
* @name material.components.icon
* @description
* Icon
*/
angular.module('material.components.icon', [
'material.core'
])
.directive('mdIcon', mdIconDirective);
/*
* @ngdoc directive
* @name mdIcon
* @module material.components.icon
*
* @restrict E
*
* @description
* The `<md-icon>` directive is an element useful for SVG icons
*
* @usage
* <hljs lang="html">
* <md-icon icon="/img/icons/ic_access_time_24px.svg">
* </md-icon>
* </hljs>
*
*/
function mdIconDirective() {
return {
restrict: 'E',
template: '<object class="md-icon"></object>',
compile: function(element, attr) {
var object = angular.element(element[0].children[0]);
if(angular.isDefined(attr.icon)) {
object.attr('data', attr.icon);
}
}
};
}
})();
(function() {
'use strict';
/**
* @ngdoc module
* @name material.components.list
* @description
* List module
*/
angular.module('material.components.list', [
'material.core'
])
.directive('mdList', mdListDirective)
.directive('mdItem', mdItemDirective);
/**
* @ngdoc directive
* @name mdList
* @module material.components.list
*
* @restrict E
*
* @description
* The `<md-list>` directive is a list container for 1..n `<md-item>` tags.
*
* @usage
* <hljs lang="html">
* <md-list>
* <md-item ng-repeat="item in todos">
* <div class="md-tile-left">
* <img ng-src="{{item.face}}" class="face" alt="{{item.who}}">
* </div>
* <div class="md-tile-content">
* <h3>{{item.what}}</h3>
* <h4>{{item.who}}</h4>
* <p>
* {{item.notes}}
* </p>
* </div>
*
* </md-item>
* </md-list>
* </hljs>
*
*/
function mdListDirective() {
return {
restrict: 'E',
link: function($scope, $element, $attr) {
$element.attr({
'role' : 'list'
});
}
};
}
/**
* @ngdoc directive
* @name mdItem
* @module material.components.list
*
* @restrict E
*
* @description
* The `<md-item>` directive is a container intended for row items in a `<md-list>` container.
*
* @usage
* <hljs lang="html">
* <md-list>
* <md-item>
* Item content in list
* </md-item>
* </md-list>
* </hljs>
*
*/
function mdItemDirective() {
return {
restrict: 'E',
link: function($scope, $element, $attr) {
$element.attr({
'role' : 'listitem'
});
}
};
}
})();
(function() {
'use strict';
/**
* @ngdoc module
* @name material.components.progressCircular
* @description Circular Progress module!
*/
angular.module('material.components.progressCircular', [
'material.core'
])
.directive('mdProgressCircular', MdProgressCircularDirective);
/**
* @ngdoc directive
* @name mdProgressCircular
* @module material.components.progressCircular
* @restrict E
*
* @description
* The circular 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.
*
* 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} mdMode Select from one of two modes: determinate and indeterminate.
* @param {number=} value In determinate mode, this number represents the percentage of the circular progress. Default: 0
* @param {number=} mdDiameter This specifies the diamter of the circular progress. Default: 48
*
* @usage
* <hljs lang="html">
* <md-progress-circular md-mode="determinate" value="..."></md-progress-circular>
*
* <md-progress-circular md-mode="determinate" ng-value="..."></md-progress-circular>
*
* <md-progress-circular md-mode="determinate" value="..." diameter="100"></md-progress-circular>
*
* <md-progress-circular md-mode="indeterminate"></md-progress-circular>
* </hljs>
*/
function MdProgressCircularDirective($$rAF, $mdConstant, $mdTheming) {
var fillRotations = new Array(101),
fixRotations = new Array(101);
for (var i = 0; i < 101; i++) {
var percent = i / 100;
var rotation = Math.floor(percent * 180);
fillRotations[i] = 'rotate(' + rotation.toString() + 'deg)';
fixRotations[i] = 'rotate(' + (rotation * 2).toString() + 'deg)';
}
return {
restrict: 'E',
template:
'<div class="md-wrapper1"><div class="md-wrapper2"><div class="md-circle">' +
'<div class="md-mask md-full">' +
'<div class="md-fill"></div>' +
'</div>' +
'<div class="md-mask md-half">' +
'<div class="md-fill"></div>' +
'<div class="md-fill md-fix"></div>' +
'</div>' +
'<div class="md-shadow"></div>' +
'</div>' +
'<div class="md-inset"></div></div></div>',
compile: compile
};
function compile(tElement, tAttrs, transclude) {
tElement.attr('aria-valuemin', 0);
tElement.attr('aria-valuemax', 100);
tElement.attr('role', 'progressbar');
return postLink;
}
function postLink(scope, element, attr) {
$mdTheming(element);
var circle = element[0],
fill = circle.querySelectorAll('.md-fill, .md-mask.md-full'),
fix = circle.querySelectorAll('.md-fill.md-fix'),
i, clamped, fillRotation, fixRotation;
var diameter = attr.mdDiameter || 48;
var scale = diameter/48;
circle.style[$mdConstant.CSS.TRANSFORM] = 'scale(' + scale.toString() + ')';
attr.$observe('value', function(value) {
clamped = clamp(value);
fillRotation = fillRotations[clamped];
fixRotation = fixRotations[clamped];
element.attr('aria-valuenow', clamped);
for (i = 0; i < fill.length; i++) {
fill[i].style[$mdConstant.CSS.TRANSFORM] = fillRotation;
}
for (i = 0; i < fix.length; i++) {
fix[i].style[$mdConstant.CSS.TRANSFORM] = fixRotation;
}
});
}
function clamp(value) {
if (value > 100) {
return 100;
}
if (value < 0) {
return 0;
}
return Math.ceil(value || 0);
}
}
MdProgressCircularDirective.$inject = ["$$rAF", "$mdConstant", "$mdTheming"];
})();
(function() {
'use strict';
/**
* @ngdoc module
* @name material.components.progressLinear
* @description Linear Progress module!
*/
angular.module('material.components.progressLinear', [
'material.core'
])
.directive('mdProgressLinear', MdProgressLinearDirective);
/**
* @ngdoc directive
* @name mdProgressLinear
* @module material.components.progressLinear
* @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} mdMode 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=} mdBufferValue In the buffer mode, this number represents the precentage of the secondary progress bar. Default: 0
*
* @usage
* <hljs lang="html">
* <md-progress-linear md-mode="determinate" value="..."></md-progress-linear>
*
* <md-progress-linear md-mode="determinate" ng-value="..."></md-progress-linear>
*
* <md-progress-linear md-mode="indeterminate"></md-progress-linear>
*
* <md-progress-linear md-mode="buffer" value="..." md-buffer-value="..."></md-progress-linear>
*
* <md-progress-linear md-mode="query"></md-progress-linear>
* </hljs>
*/
function MdProgressLinearDirective($$rAF, $mdConstant, $mdTheming) {
return {
restrict: 'E',
template: '<div class="md-container">' +
'<div class="md-dashed"></div>' +
'<div class="md-bar md-bar1"></div>' +
'<div class="md-bar md-bar2"></div>' +
'</div>',
compile: compile
};
function compile(tElement, tAttrs, transclude) {
tElement.attr('aria-valuemin', 0);
tElement.attr('aria-valuemax', 100);
tElement.attr('role', 'progressbar');
return postLink;
}
function postLink(scope, element, attr) {
$mdTheming(element);
var bar1Style = element[0].querySelector('.md-bar1').style,
bar2Style = element[0].querySelector('.md-bar2').style,
container = angular.element(element[0].querySelector('.md-container'));
attr.$observe('value', function(value) {
if (attr.mdMode == 'query') {
return;
}
var clamped = clamp(value);
element.attr('aria-valuenow', clamped);
bar2Style[$mdConstant.CSS.TRANSFORM] = transforms[clamped];
});
attr.$observe('mdBufferValue', function(value) {
bar1Style[$mdConstant.CSS.TRANSFORM] = transforms[clamp(value)];
});
$$rAF(function() {
container.addClass('md-ready');
});
}
function clamp(value) {
if (value > 100) {
return 100;
}
if (value < 0) {
return 0;
}
return Math.ceil(value || 0);
}
}
MdProgressLinearDirective.$inject = ["$$rAF", "$mdConstant", "$mdTheming"];
// **********************************************************
// Private Methods
// **********************************************************
var transforms = (function() {
var values = new Array(101);
for(var i = 0; i < 101; i++){
values[i] = makeTransform(i);
}
return values;
function makeTransform(value){
var scale = value/100;
var translateX = (value-100)/2;
return 'translateX(' + translateX.toString() + '%) scale(' + scale.toString() + ', 1)';
}
})();
})();
(function() {
'use strict';
/**
* @ngdoc module
* @name material.components.radioButton
* @description radioButton module!
*/
angular.module('material.components.radioButton', [
'material.core'
])
.directive('mdRadioGroup', mdRadioGroupDirective)
.directive('mdRadioButton', mdRadioButtonDirective);
/**
* @ngdoc directive
* @module material.components.radioButton
* @name mdRadioGroup
*
* @restrict E
*
* @description
* The `<md-radio-group>` directive identifies a grouping
* container for the 1..n grouped radio buttons; specified using nested
* `<md-radio-button>` tags.
*
* Note: `<md-radio-group>` and `<md-radio-button>` handle tabindex differently
* than the native `<input type='radio'>` controls. Whereas the native controls
* force the user to tab through all the radio buttons, `<md-radio-group>`
* is focusable, and by default the `<md-radio-button>`s are not.
*
* @param {string} ngModel Assignable angular expression to data-bind to.
* @param {boolean=} mdNoInk Use of attribute indicates flag to disable ink ripple effects.
*
* @usage
* <hljs lang="html">
* <md-radio-group ng-model="selected">
*
* <md-radio-button
* ng-repeat="d in colorOptions"
* ng-value="d.value" aria-label="{{ d.label }}">
*
* {{ d.label }}
*
* </md-radio-button>
*
* </md-radio-group>
* </hljs>
*
*/
function mdRadioGroupDirective($mdUtil, $mdConstant, $mdTheming) {
RadioGroupController.prototype = createRadioGroupControllerProto();
return {
restrict: 'E',
controller: ['$element', RadioGroupController],
require: ['mdRadioGroup', '?ngModel'],
link: linkRadioGroup
};
function linkRadioGroup(scope, element, attr, ctrls) {
$mdTheming(element);
var rgCtrl = ctrls[0],
ngModelCtrl = ctrls[1] || {
$setViewValue: angular.noop
};
function keydownListener(ev) {
if (ev.keyCode === $mdConstant.KEY_CODE.LEFT_ARROW || ev.keyCode === $mdConstant.KEY_CODE.UP_ARROW) {
ev.preventDefault();
rgCtrl.selectPrevious();
}
else if (ev.keyCode === $mdConstant.KEY_CODE.RIGHT_ARROW || ev.keyCode === $mdConstant.KEY_CODE.DOWN_ARROW) {
ev.preventDefault();
rgCtrl.selectNext();
}
}
rgCtrl.init(ngModelCtrl);
element.attr({
'role': 'radiogroup',
'tabIndex': element.attr('tabindex') || '0'
})
.on('keydown', keydownListener);
}
function RadioGroupController($element) {
this._radioButtonRenderFns = [];
this.$element = $element;
}
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() {
return changeSelectedButton(this.$element, 1);
},
selectPrevious : function() {
return changeSelectedButton(this.$element, -1);
},
setActiveDescendant: function (radioId) {
this.$element.attr('aria-activedescendant', radioId);
}
};
}
/**
* Change the radio group's selected button by a given increment.
* If no button is selected, select the first button.
*/
function changeSelectedButton(parent, increment) {
// Coerce all child radio buttons into an array, then wrap then in an iterator
var buttons = $mdUtil.iterator(
Array.prototype.slice.call(parent[0].querySelectorAll('md-radio-button')),
true
);
if (buttons.count()) {
var selected = parent[0].querySelector('md-radio-button.md-checked');
var target = buttons[increment < 0 ? 'previous' : 'next'](selected) || buttons.first();
// Activate radioButton's click listener (triggerHandler won't create a real click event)
angular.element(target).triggerHandler('click');
}
}
}
mdRadioGroupDirective.$inject = ["$mdUtil", "$mdConstant", "$mdTheming"];
/**
* @ngdoc directive
* @module material.components.radioButton
* @name mdRadioButton
*
* @restrict E
*
* @description
* The `<md-radio-button>`directive is the child directive required to be used within `<md-radioo-group>` elements.
*
* While similar to the `<input type="radio" ng-model="" value="">` directive,
* the `<md-radio-button>` directive provides 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">
*
* <md-radio-button value="1" aria-label="Label 1">
* Label 1
* </md-radio-button>
*
* <md-radio-button ng-model="color" ng-value="specialValue" aria-label="Green">
* Green
* </md-radio-button>
*
* </hljs>
*
*/
function mdRadioButtonDirective($mdAria, $mdUtil, $mdTheming) {
var CHECKED_CSS = 'md-checked';
return {
restrict: 'E',
require: '^mdRadioGroup',
transclude: true,
template: '<div class="md-container" md-ink-ripple md-ink-ripple-checkbox>' +
'<div class="md-off"></div>' +
'<div class="md-on"></div>' +
'</div>' +
'<div ng-transclude class="md-label"></div>',
link: link
};
function link(scope, element, attr, rgCtrl) {
var lastChecked;
$mdTheming(element);
configureAria(element, scope);
rgCtrl.add(render);
attr.$observe('value', render);
element
.on('click', listener)
.on('$destroy', function() {
rgCtrl.remove(render);
});
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('aria-checked', checked);
if (checked) {
element.addClass(CHECKED_CSS);
rgCtrl.setActiveDescendant(element.attr('id'));
} else {
element.removeClass(CHECKED_CSS);
}
}
/**
* Inject ARIA-specific attributes appropriate for each radio button
*/
function configureAria( element, scope ){
scope.ariaId = buildAriaID();
element.attr({
'id' : scope.ariaId,
'role' : 'radio',
'aria-checked' : 'false'
});
$mdAria.expectWithText(element, 'aria-label');
/**
* Build a unique ID for each radio button that will be used with aria-activedescendant.
* Preserve existing ID if already specified.
* @returns {*|string}
*/
function buildAriaID() {
return attr.id || ( 'radio' + "_" + $mdUtil.nextUid() );
}
}
}
}
mdRadioButtonDirective.$inject = ["$mdAria", "$mdUtil", "$mdTheming"];
})();
(function() {
'use strict';
/**
* @ngdoc module
* @name material.components.sidenav
*
* @description
* A Sidenav QP component.
*/
angular.module('material.components.sidenav', [
'material.core',
'material.components.backdrop'
])
.factory('$mdSidenav', mdSidenavService )
.directive('mdSidenav', mdSidenavDirective)
.controller('$mdSidenavController', mdSidenavController)
.factory('$mdMedia', mdMediaFactory)
.factory('$mdComponentRegistry', mdComponentRegistry);
/*
* @private
* @ngdoc object
* @name mdSidenavController
* @module material.components.sidenav
*
* @description
* The controller for mdSidenav components.
*/
function mdSidenavController($scope, $element, $attrs, $timeout, $mdSidenav, $mdComponentRegistry) {
var self = this;
this.destroy = $mdComponentRegistry.register(this, $attrs.mdComponentId);
this.isOpen = function() {
return !!$scope.isOpen;
};
this.toggle = function() {
$scope.isOpen = !$scope.isOpen;
};
this.open = function() {
$scope.isOpen = true;
};
this.close = function() {
$scope.isOpen = false;
};
}
mdSidenavController.$inject = ["$scope", "$element", "$attrs", "$timeout", "$mdSidenav", "$mdComponentRegistry"];
/*
* @private
* @ngdoc service
* @name $mdSidenav
* @module material.components.sidenav
*
* @description
* $mdSidenav makes it easy to interact with multiple sidenavs
* in an app.
*
* @usage
*
* ```javascript
* // Toggle the given sidenav
* $mdSidenav(componentId).toggle();
*
* // Open the given sidenav
* $mdSidenav(componentId).open();
*
* // Close the given sidenav
* $mdSidenav(componentId).close();
* ```
*/
function mdSidenavService($mdComponentRegistry) {
return function(handle) {
var instance = $mdComponentRegistry.get(handle);
if(!instance) {
$mdComponentRegistry.notFoundError(handle);
}
return {
isOpen: function() {
return instance && instance.isOpen();
},
toggle: function() {
instance && instance.toggle();
},
open: function() {
instance && instance.open();
},
close: function() {
instance && instance.close();
}
};
};
}
mdSidenavService.$inject = ["$mdComponentRegistry"];
/**
* @ngdoc directive
* @name mdSidenav
* @module material.components.sidenav
* @restrict E
*
* @description
*
* A Sidenav component that can be opened and closed programatically.
*
* By default, upon opening it will slide out on top of the main content area.
*
* @usage
* <hljs lang="html">
* <div layout="row" ng-controller="MyController">
* <md-sidenav md-component-id="left" class="md-sidenav-left">
* Left Nav!
* </md-sidenav>
*
* <md-content>
* Center Content
* <md-button ng-click="openLeftMenu()">
* Open Left Menu
* </md-button>
* </md-content>
*
* <md-sidenav md-component-id="right"
* md-is-locked-open="$media('min-width: 333px')"
* class="md-sidenav-right">
* Right Nav!
* </md-sidenav>
* </div>
* </hljs>
*
* <hljs lang="js">
* var app = angular.module('myApp', ['ngMaterial']);
* app.controller('MyController', function($scope, $mdSidenav) {
* $scope.openLeftMenu = function() {
* $mdSidenav('left').toggle();
* };
* });
* </hljs>
*
* @param {expression=} mdIsOpen A model bound to whether the sidenav is opened.
* @param {string=} mdComponentId componentId to use with $mdSidenav service.
* @param {expression=} mdIsLockedOpen When this expression evalutes to true,
* the sidenav 'locks open': it falls into the content's flow instead
* of appearing over it. This overrides the `is-open` attribute.
*
* A $media() function is exposed to the is-locked-open attribute, which
* can be given a media query or one of the `sm`, `md` or `lg` presets.
* Examples:
*
* - `<md-sidenav md-is-locked-open="shouldLockOpen"></md-sidenav>`
* - `<md-sidenav md-is-locked-open="$media('min-width: 1000px')"></md-sidenav>`
* - `<md-sidenav md-is-locked-open="$media('sm')"></md-sidenav>` <!-- locks open on small screens !-->
*/
function mdSidenavDirective($timeout, $animate, $parse, $mdMedia, $mdConstant, $compile, $mdTheming) {
return {
restrict: 'E',
scope: {
isOpen: '=?mdIsOpen'
},
controller: '$mdSidenavController',
compile: function(element) {
element.addClass('md-closed');
element.attr('tabIndex', '-1');
return postLink;
}
};
function postLink(scope, element, attr, sidenavCtrl) {
var isLockedOpenParsed = $parse(attr.mdIsLockedOpen);
var backdrop = $compile(
'<md-backdrop class="md-sidenav-backdrop md-opaque">'
)(scope);
$mdTheming.inherit(backdrop, element);
element.on('$destroy', sidenavCtrl.destroy);
scope.$watch('isOpen', setOpen);
scope.$watch(function() {
return isLockedOpenParsed(scope.$parent, {
$media: $mdMedia
});
}, function(isLocked) {
element.toggleClass('md-locked-open', !!isLocked);
backdrop.toggleClass('md-locked-open', !!isLocked);
});
/**
* Toggle the SideNav view and attach/detach listeners
* @param isOpen
*/
function setOpen(isOpen) {
var parent = element.parent();
parent[isOpen ? 'on' : 'off']('keydown', onKeyDown);
$animate[isOpen ? 'enter' : 'leave'](backdrop, parent);
backdrop[isOpen ? 'on' : 'off']('click', close);
$animate[isOpen ? 'removeClass' : 'addClass'](element, 'md-closed').then(function() {
// If we opened, and haven't closed again before the animation finished
if (scope.isOpen) {
element.focus();
}
});
}
/**
* Auto-close sideNav when the `escape` key is pressed.
* @param evt
*/
function onKeyDown(ev) {
if (ev.keyCode === $mdConstant.KEY_CODE.ESCAPE) {
close();
ev.preventDefault();
ev.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() {
$timeout(function(){
sidenavCtrl.close();
});
}
}
}
mdSidenavDirective.$inject = ["$timeout", "$animate", "$parse", "$mdMedia", "$mdConstant", "$compile", "$mdTheming"];
/**
* Exposes a function on the '$mdMedia' service which will return true or false,
* whether the given media query matches. Re-evaluates on resize. Allows presets
* for 'sm', 'md', 'lg'.
*
* @example $mdMedia('sm') == true if device-width <= sm
* @example $mdMedia('(min-width: 1200px)') == true if device-width >= 1200px
* @example $mdMedia('max-width: 300px') == true if device-width <= 300px (sanitizes input, adding parens)
*/
function mdMediaFactory($window, $mdUtil, $timeout) {
var cache = $mdUtil.cacheFactory('$mdMedia', { capacity: 15 });
var presets = {
sm: '(min-width: 600px)',
md: '(min-width: 960px)',
lg: '(min-width: 1200px)'
};
angular.element($window).on('resize', updateAll);
return $mdMedia;
function $mdMedia(query) {
query = validate(query);
var result;
if ( !angular.isDefined(result = cache.get(query)) ) {
return add(query);
}
return result;
}
function validate(query) {
return presets[query] || (
query.charAt(0) != '(' ? ('(' + query + ')') : query
);
}
function add(query) {
return cache.put(query, !!$window.matchMedia(query).matches);
}
function updateAll() {
var keys = cache.keys();
if (keys.length) {
for (var i = 0, ii = keys.length; i < ii; i++) {
cache.put(keys[i], !!$window.matchMedia(keys[i]).matches);
}
// trigger a $digest()
$timeout(angular.noop);
}
}
}
mdMediaFactory.$inject = ["$window", "$mdUtil", "$timeout"];
function mdComponentRegistry($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.$$mdHandle === 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.$$mdHandle = handle;
instances.push(instance);
return function deregister() {
var index = instances.indexOf(instance);
if (index !== -1) {
instances.splice(index, 1);
}
};
}
};
}
mdComponentRegistry.$inject = ["$log"];
})();
(function() {
'use strict';
/**
* @ngdoc module
* @name material.components.slider
*/
angular.module('material.components.slider', [
'material.core'
])
.directive('mdSlider', SliderDirective);
/**
* @ngdoc directive
* @name mdSlider
* @module material.components.slider
* @restrict E
* @description
* The `<md-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 `md-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">
* <md-slider ng-model="myValue" min="5" max="500">
* </md-slider>
* </hljs>
* <h4>Discrete Mode</h4>
* <hljs lang="html">
* <md-slider md-discrete ng-model="myDiscreteValue" step="10" min="10" max="130">
* </md-slider>
* </hljs>
*
* @param {boolean=} mdDiscrete 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($mdTheming) {
return {
scope: {},
require: ['?ngModel', 'mdSlider'],
controller: SliderController,
template:
'<div class="md-track-container">' +
'<div class="md-track"></div>' +
'<div class="md-track md-track-fill"></div>' +
'<div class="md-track-ticks"></div>' +
'</div>' +
'<div class="md-thumb-container">' +
'<div class="md-thumb"></div>' +
'<div class="md-focus-thumb"></div>' +
'<div class="md-focus-ring"></div>' +
'<div class="md-sign">' +
'<span class="md-thumb-text"></span>' +
'</div>' +
'<div class="md-disabled-thumb"></div>' +
'</div>',
link: postLink
};
function postLink(scope, element, attr, ctrls) {
$mdTheming(element);
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);
}
}
SliderDirective.$inject = ["$mdTheming"];
/**
* We use a controller for all the logic so that we can expose a few
* things to unit tests
*/
function SliderController($scope, $element, $attrs, $$rAF, $window, $mdAria, $mdUtil, $mdConstant) {
this.init = function init(ngModelCtrl) {
var thumb = angular.element($element[0].querySelector('.md-thumb'));
var thumbText = angular.element($element[0].querySelector('.md-thumb-text'));
var thumbContainer = thumb.parent();
var trackContainer = angular.element($element[0].querySelector('.md-track-container'));
var activeTrack = angular.element($element[0].querySelector('.md-track-fill'));
var tickContainer = angular.element($element[0].querySelector('.md-track-ticks'));
var throttledRefreshDimensions = $mdUtil.throttle(refreshSliderDimensions, 5000);
// Default values, overridable by $attrss
$attrs.min ? $attrs.$observe('min', updateMin) : updateMin(0);
$attrs.max ? $attrs.$observe('max', updateMax) : updateMax(100);
$attrs.step ? $attrs.$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 ($attrs.ngDisabled) {
stopDisabledWatch = $scope.$parent.$watch($attrs.ngDisabled, updateAriaDisabled);
}
$mdAria.expect($element, 'aria-label');
$element.attr('tabIndex', 0);
$element.attr('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);
hammertime.on('panend', onPanEnd);
// On resize, recalculate the slider's dimensions and re-render
function updateAll() {
refreshSliderDimensions();
ngModelRender();
redrawTicks();
}
setTimeout(updateAll);
var debouncedUpdateAll = $$rAF.debounce(updateAll);
angular.element($window).on('resize', debouncedUpdateAll);
$scope.$on('$destroy', function() {
angular.element($window).off('resize', debouncedUpdateAll);
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($attrs.mdDiscrete)) 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 = {};
refreshSliderDimensions();
function refreshSliderDimensions() {
sliderDimensions = trackContainer[0].getBoundingClientRect();
}
function getSliderDimensions() {
throttledRefreshDimensions();
return sliderDimensions;
}
/**
* left/right arrow listener
*/
function keydownListener(ev) {
if($element[0].hasAttribute('disabled')) {
return;
}
var changeAmount;
if (ev.keyCode === $mdConstant.KEY_CODE.LEFT_ARROW) {
changeAmount = -step;
} else if (ev.keyCode === $mdConstant.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() {
if (isNaN(ngModelCtrl.$viewValue)) {
ngModelCtrl.$viewValue = ngModelCtrl.$modelValue;
}
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(
$mdConstant.CSS.TRANSFORM,
'translate3d(' + getSliderDimensions().width * percent + 'px,0,0)'
);
$element.toggleClass('md-min', percent === 0);
}
/**
* Slide listeners
*/
var isSliding = false;
var isDiscrete = angular.isDefined($attrs.mdDiscrete);
function onInput(ev) {
if (!isSliding && ev.eventType === Hammer.INPUT_START &&
!$element[0].hasAttribute('disabled')) {
isSliding = true;
$element.addClass('active');
$element[0].focus();
refreshSliderDimensions();
onPan(ev);
ev.srcEvent.stopPropagation();
} else if (isSliding && ev.eventType === Hammer.INPUT_END) {
if ( isSliding && isDiscrete ) onPanEnd(ev);
isSliding = false;
$element.removeClass('panning active');
}
}
function onPanStart() {
if (!isSliding) return;
$element.addClass('panning');
}
function onPan(ev) {
if (!isSliding) return;
// While panning discrete, update only the
// visual positioning but not the model value.
if ( isDiscrete ) adjustThumbPosition( ev.center.x );
else doSlide( ev.center.x );
ev.preventDefault();
ev.srcEvent.stopPropagation();
}
function onPanEnd(ev) {
if ( isDiscrete && !$element[0].hasAttribute('disabled') ) {
// Convert exact to closest discrete value.
// Slide animate the thumb... and then update the model value.
var exactVal = percentToValue( positionToPercent( ev.center.x ));
var closestVal = minMaxValidator( stepValidator(exactVal) );
setSliderPercent( valueToPercent(closestVal));
$$rAF(function(){
setModelValue( closestVal );
});
ev.preventDefault();
ev.srcEvent.stopPropagation();
}
}
/**
* Expose for testing
*/
this._onInput = onInput;
this._onPanStart = onPanStart;
this._onPan = onPan;
/**
* Slide the UI by changing the model value
* @param x
*/
function doSlide( x ) {
$scope.$evalAsync( function() {
setModelValue( percentToValue( positionToPercent(x) ));
});
}
/**
* Slide the UI without changing the model (while dragging/panning)
* @param x
*/
function adjustThumbPosition( x ) {
var exactVal = percentToValue( positionToPercent( x ));
var closestVal = minMaxValidator( stepValidator(exactVal) );
setSliderPercent( positionToPercent(x) );
thumbText.text( closestVal );
}
/**
* Convert horizontal position on slider to percentage value of offset from beginning...
* @param x
* @returns {number}
*/
function positionToPercent( x ) {
return Math.max(0, Math.min(1, (x - sliderDimensions.left) / (sliderDimensions.width)));
}
/**
* Convert percentage offset on slide to equivalent model value
* @param percent
* @returns {*}
*/
function percentToValue( percent ) {
return (min + percent * (max - min));
}
function valueToPercent( val ) {
return (val - min)/(max - min);
}
};
}
SliderController.$inject = ["$scope", "$element", "$attrs", "$$rAF", "$window", "$mdAria", "$mdUtil", "$mdConstant"];
})();
(function() {
'use strict';
/*
* @ngdoc module
* @name material.components.sticky
* @description
*
* Sticky effects for md
*/
angular.module('material.components.sticky', [
'material.core',
'material.components.content'
])
.factory('$mdSticky', MdSticky);
/*
* @ngdoc service
* @name $mdSticky
* @module material.components.sticky
*
* @description
* The `$mdSticky`service provides a mixin to make elements sticky.
*
* @returns A `$mdSticky` function that takes three arguments:
* - `scope`
* - `element`: The element that will be 'sticky'
* - `elementClone`: A clone of the element, that will be shown
* when the user starts scrolling past the original element.
* If not provided, it will use the result of `element.clone()`.
*/
function MdSticky($document, $mdConstant, $compile, $$rAF, $mdUtil) {
var browserStickySupport = checkStickySupport();
/**
* Registers an element as sticky, used internally by directives to register themselves
*/
return function registerStickyElement(scope, element, stickyClone) {
var contentCtrl = element.controller('mdContent');
if (!contentCtrl) return;
if (browserStickySupport) {
element.css({
position: browserStickySupport,
top: 0,
'z-index': 2
});
} else {
var $$sticky = contentCtrl.$element.data('$$sticky');
if (!$$sticky) {
$$sticky = setupSticky(contentCtrl);
contentCtrl.$element.data('$$sticky', $$sticky);
}
var deregister = $$sticky.add(element, stickyClone || element.clone());
scope.$on('$destroy', deregister);
}
};
function setupSticky(contentCtrl) {
var contentEl = contentCtrl.$element;
// Refresh elements is very expensive, so we use the debounced
// version when possible.
var debouncedRefreshElements = $$rAF.debounce(refreshElements);
// setupAugmentedScrollEvents gives us `$scrollstart` and `$scroll`,
// more reliable than `scroll` on android.
setupAugmentedScrollEvents(contentEl);
contentEl.on('$scrollstart', debouncedRefreshElements);
contentEl.on('$scroll', onScroll);
var self;
return self = {
prev: null,
current: null, //the currently stickied item
next: null,
items: [],
add: add,
refreshElements: refreshElements
};
/***************
* Public
***************/
// Add an element and its sticky clone to this content's sticky collection
function add(element, stickyClone) {
stickyClone.addClass('md-sticky-clone');
var item = {
element: element,
clone: stickyClone
};
self.items.push(item);
contentEl.parent().prepend(item.clone);
debouncedRefreshElements();
return function remove() {
self.items.forEach(function(item, index) {
if (item.element[0] === element[0]) {
self.items.splice(index, 1);
item.clone.remove();
}
});
debouncedRefreshElements();
};
}
function refreshElements() {
// Sort our collection of elements by their current position in the DOM.
// We need to do this because our elements' order of being added may not
// be the same as their order of display.
self.items.forEach(refreshPosition);
self.items = self.items.sort(function(a, b) {
return a.top < b.top ? -1 : 1;
});
// Find which item in the list should be active,
// based upon the content's current scroll position
var item;
var currentScrollTop = contentEl.prop('scrollTop');
for (var i = self.items.length - 1; i >= 0; i--) {
if (currentScrollTop > self.items[i].top) {
item = self.items[i];
break;
}
}
setCurrentItem(item);
}
/***************
* Private
***************/
// Find the `top` of an item relative to the content element,
// and also the height.
function refreshPosition(item) {
// Find the top of an item by adding to the offsetHeight until we reach the
// content element.
var current = item.element[0];
item.top = 0;
item.left = 0;
while (current && current !== contentEl[0]) {
item.top += current.offsetTop;
item.left += current.offsetLeft;
current = current.offsetParent;
}
item.height = item.element.prop('offsetHeight');
item.clone.css('margin-left', item.left + 'px');
}
// As we scroll, push in and select the correct sticky element.
function onScroll() {
var scrollTop = contentEl.prop('scrollTop');
var isScrollingDown = scrollTop > (onScroll.prevScrollTop || 0);
onScroll.prevScrollTop = scrollTop;
// At the top?
if (scrollTop === 0) {
setCurrentItem(null);
// Going to next item?
} else if (isScrollingDown && self.next) {
if (self.next.top - scrollTop <= 0) {
// Sticky the next item if we've scrolled past its position.
setCurrentItem(self.next);
} else if (self.current) {
// Push the current item up when we're almost at the next item.
if (self.next.top - scrollTop <= self.next.height) {
translate(self.current, self.next.top - self.next.height - scrollTop);
} else {
translate(self.current, null);
}
}
// Scrolling up with a current sticky item?
} else if (!isScrollingDown && self.current) {
if (scrollTop < self.current.top) {
// Sticky the previous item if we've scrolled up past
// the original position of the currently stickied item.
setCurrentItem(self.prev);
}
// Scrolling up, and just bumping into the item above (just set to current)?
// If we have a next item bumping into the current item, translate
// the current item up from the top as it scrolls into view.
if (self.current && self.next) {
if (scrollTop >= self.next.top - self.current.height) {
translate(self.current, self.next.top - scrollTop - self.current.height);
} else {
translate(self.current, null);
}
}
}
}
function setCurrentItem(item) {
if (self.current === item) return;
// Deactivate currently active item
if (self.current) {
translate(self.current, null);
setStickyState(self.current, null);
}
// Activate new item if given
if (item) {
setStickyState(item, 'active');
}
self.current = item;
var index = self.items.indexOf(item);
// If index === -1, index + 1 = 0. It works out.
self.next = self.items[index + 1];
self.prev = self.items[index - 1];
setStickyState(self.next, 'next');
setStickyState(self.prev, 'prev');
}
function setStickyState(item, state) {
if (!item || item.state === state) return;
if (item.state) {
item.clone.attr('sticky-prev-state', item.state);
item.element.attr('sticky-prev-state', item.state);
}
item.clone.attr('sticky-state', state);
item.element.attr('sticky-state', state);
item.state = state;
}
function translate(item, amount) {
if (!item) return;
if (amount === null || amount === undefined) {
if (item.translateY) {
item.translateY = null;
item.clone.css($mdConstant.CSS.TRANSFORM, '');
}
} else {
item.translateY = amount;
item.clone.css(
$mdConstant.CSS.TRANSFORM,
'translate3d(' + item.left + 'px,' + amount + 'px,0)'
);
}
}
}
// Function to check for browser sticky support
function checkStickySupport($el) {
var stickyProp;
var testEl = angular.element('<div>');
$document[0].body.appendChild(testEl[0]);
var stickyProps = ['sticky', '-webkit-sticky'];
for (var i = 0; i < stickyProps.length; ++i) {
testEl.css({position: stickyProps[i], top: 0, 'z-index': 2});
if (testEl.css('position') == stickyProps[i]) {
stickyProp = stickyProps[i];
break;
}
}
testEl.remove();
return stickyProp;
}
// Android 4.4 don't accurately give scroll events.
// To fix this problem, we setup a fake scroll event. We say:
// > If a scroll or touchmove event has happened in the last DELAY milliseconds,
// then send a `$scroll` event every animationFrame.
// Additionally, we add $scrollstart and $scrollend events.
function setupAugmentedScrollEvents(element) {
var SCROLL_END_DELAY = 200;
var isScrolling;
var lastScrollTime;
element.on('scroll touchmove', function() {
if (!isScrolling) {
isScrolling = true;
$$rAF(loopScrollEvent);
element.triggerHandler('$scrollstart');
}
element.triggerHandler('$scroll');
lastScrollTime = +$mdUtil.now();
});
function loopScrollEvent() {
if (+$mdUtil.now() - lastScrollTime > SCROLL_END_DELAY) {
isScrolling = false;
element.triggerHandler('$scrollend');
} else {
element.triggerHandler('$scroll');
$$rAF(loopScrollEvent);
}
}
}
}
MdSticky.$inject = ["$document", "$mdConstant", "$compile", "$$rAF", "$mdUtil"];
})();
(function() {
'use strict';
/**
* @ngdoc module
* @name material.components.subheader
* @description
* SubHeader module
*
* Subheaders are special list tiles that delineate distinct sections of a
* list or grid list and are typically related to the current filtering or
* sorting criteria. Subheader tiles are either displayed inline with tiles or
* can be associated with content, for example, in an adjacent column.
*
* Upon scrolling, subheaders remain pinned to the top of the screen and remain
* pinned until pushed on or off screen by the next subheader. @see [Material
* Design Specifications](https://www.google.com/design/spec/components/subheaders.html)
*
* > To improve the visual grouping of content, use the system color for your subheaders.
*
*/
angular.module('material.components.subheader', [
'material.core',
'material.components.sticky'
])
.directive('mdSubheader', MdSubheaderDirective);
/**
* @ngdoc directive
* @name mdSubheader
* @module material.components.subheader
*
* @restrict E
*
* @description
* The `<md-subheader>` directive is a subheader for a section
*
* @usage
* <hljs lang="html">
* <md-subheader>Online Friends</md-subheader>
* </hljs>
*/
function MdSubheaderDirective($mdSticky, $compile, $mdTheming) {
return {
restrict: 'E',
replace: true,
transclude: true,
template:
'<h2 class="md-subheader">' +
'<span class="md-subheader-content"></span>' +
'</h2>',
compile: function(element, attr, transclude) {
var outerHTML = element[0].outerHTML;
return function postLink(scope, element, attr) {
$mdTheming(element);
function getContent(el) {
return angular.element(el[0].querySelector('.md-subheader-content'));
}
// Transclude the user-given contents of the subheader
// the conventional way.
transclude(scope, function(clone) {
getContent(element).append(clone);
});
// Create another clone, that uses the outer and inner contents
// of the element, that will be 'stickied' as the user scrolls.
transclude(scope, function(clone) {
var stickyClone = $compile(angular.element(outerHTML))(scope);
$mdTheming(stickyClone);
getContent(stickyClone).append(clone);
$mdSticky(scope, element, stickyClone);
});
};
}
};
}
MdSubheaderDirective.$inject = ["$mdSticky", "$compile", "$mdTheming"];
})();
(function() {
'use strict';
/**
* @ngdoc module
* @name material.components.swipe
* @description Swipe module!
*/
angular.module('material.components.swipe',[])
.factory('$mdSwipe', MdSwipeFactory)
.directive('mdSwipeLeft', MdSwipeLeftDirective)
.directive('mdSwipeRight', MdSwipeRightDirective);
/*
* @ngdoc service
* @module material.components.swipe
* @name $mdSwipe
* @description
* This service allows directives to easily attach swipe and pan listeners to
* the specified element.
*/
function MdSwipeFactory() {
// match expected API functionality
var attachNoop = function(){ return angular.noop; };
/**
* SwipeService constructor pre-captures scope and customized event types
*
* @param scope
* @param eventTypes
* @returns {*}
* @constructor
*/
return function SwipeService(scope, eventTypes) {
if ( !eventTypes ) eventTypes = "swipeleft swiperight";
// publish configureFor() method for specific element instance
return function configureFor(element, onSwipeCallback, attachLater ) {
var hammertime = new Hammer(element[0], {
recognizers : addRecognizers([], eventTypes )
});
// Attach swipe listeners now
if ( !attachLater ) attachSwipe();
// auto-disconnect during destroy
scope.$on('$destroy', function() {
hammertime.destroy();
});
return attachSwipe;
// **********************
// Internal methods
// **********************
/**
* Delegate swipe event to callback function
* and ensure $digest is triggered.
*
* @param ev HammerEvent
*/
function swipeHandler(ev) {
// Prevent triggering parent hammer listeners
ev.srcEvent.stopPropagation();
if ( angular.isFunction(onSwipeCallback) ) {
scope.$apply(function() {
onSwipeCallback(ev);
});
}
}
/**
* Enable listeners and return detach() fn
*/
function attachSwipe() {
hammertime.on(eventTypes, swipeHandler );
return function detachSwipe() {
hammertime.off( eventTypes );
};
}
/**
* Add optional recognizers such as panleft, panright
*/
function addRecognizers(list, events) {
var hasPanning = (events.indexOf("pan") > -1);
var hasSwipe = (events.indexOf("swipe") > -1);
if (hasPanning) {
list.push([ Hammer.Pan, { direction: Hammer.DIRECTION_HORIZONTAL } ]);
}
if (hasSwipe) {
list.push([ Hammer.Swipe, { direction: Hammer.DIRECTION_HORIZONTAL } ]);
}
return list;
}
};
};
}
/**
* @ngdoc directive
* @module material.components.swipe
* @name mdSwipeLeft
*
* @restrict A
*
* @description
* The `<div md-swipe-left="expression">` directive identifies an element on which
* HammerJS horizontal swipe left and pan left support will be active. The swipe/pan action
* can result in custom activity trigger by evaluating `expression`.
*
* @param {boolean=} mdNoPan Use of attribute indicates flag to disable detection of `panleft` activity
*
* @usage
* <hljs lang="html">
*
* <div class="animate-switch-container"
* ng-switch on="data.selectedIndex"
* md-swipe-left="data.selectedIndex+=1;"
* md-swipe-right="data.selectedIndex-=1;" >
*
* </div>
* </hljs>
*
*/
function MdSwipeLeftDirective($parse, $mdSwipe) {
return {
restrict: 'A',
link : swipePostLink( $parse, $mdSwipe, "SwipeLeft" )
};
}
MdSwipeLeftDirective.$inject = ["$parse", "$mdSwipe"];
/**
* @ngdoc directive
* @module material.components.swipe
* @name mdSwipeRight
*
* @restrict A
*
* @description
* The `<div md-swipe-right="expression">` directive identifies functionality
* that attaches HammerJS horizontal swipe right and pan right support to an element. The swipe/pan action
* can result in activity trigger by evaluating `expression`
*
* @param {boolean=} mdNoPan Use of attribute indicates flag to disable detection of `panright` activity
*
* @usage
* <hljs lang="html">
*
* <div class="animate-switch-container"
* ng-switch on="data.selectedIndex"
* md-swipe-left="data.selectedIndex+=1;"
* md-swipe-right="data.selectedIndex-=1;" >
*
* </div>
* </hljs>
*
*/
function MdSwipeRightDirective($parse, $mdSwipe) {
return {
restrict: 'A',
link : swipePostLink( $parse, $mdSwipe, "SwipeRight" )
};
}
MdSwipeRightDirective.$inject = ["$parse", "$mdSwipe"];
/**
* Factory to build PostLink function specific to Swipe or Pan direction
*
* @param $parse
* @param $mdSwipe
* @param name
* @returns {Function}
*/
function swipePostLink($parse, $mdSwipe, name ) {
return function(scope, element, attrs) {
var direction = name.toLowerCase();
var directiveName= "md" + name;
var parentGetter = $parse(attrs[directiveName]) || angular.noop;
var configureSwipe = $mdSwipe(scope, direction);
var requestSwipe = function(locals) {
// build function to request scope-specific swipe response
parentGetter(scope, locals);
};
configureSwipe( element, function onHandleSwipe(ev) {
if ( ev.type == direction ) {
requestSwipe();
}
});
};
}
})();
(function() {
'use strict';
/**
* @private
* @ngdoc module
* @name material.components.switch
*/
angular.module('material.components.switch', [
'material.core',
'material.components.checkbox',
'material.components.radioButton'
])
.directive('mdSwitch', MdSwitch);
/**
* @private
* @ngdoc directive
* @module material.components.switch
* @name mdSwitch
* @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=} mdNoInk Use of attribute indicates use of ripple ink effects.
* @param {string=} ariaLabel Publish the button label used by screen-readers for accessibility. Defaults to the switch's text.
*
* @usage
* <hljs lang="html">
* <md-switch ng-model="isActive" aria-label="Finished?">
* Finished ?
* </md-switch>
*
* <md-switch md-no-ink ng-model="hasInk" aria-label="No Ink Effects">
* No Ink Effects
* </md-switch>
*
* <md-switch ng-disabled="true" ng-model="isDisabled" aria-label="Disabled">
* Disabled
* </md-switch>
*
* </hljs>
*/
function MdSwitch(mdCheckboxDirective, mdRadioButtonDirective, $mdTheming) {
var checkboxDirective = mdCheckboxDirective[0];
var radioButtonDirective = mdRadioButtonDirective[0];
return {
restrict: 'E',
transclude: true,
template:
'<div class="md-switch-bar"></div>' +
'<div class="md-switch-thumb">' +
radioButtonDirective.template +
'</div>',
require: '?ngModel',
compile: compile
};
function compile(element, attr) {
var thumb = angular.element(element[0].querySelector('.md-switch-thumb'));
var checkboxLink = checkboxDirective.compile(thumb, attr);
return function (scope, element, attr, ngModelCtrl) {
$mdTheming(element);
return checkboxLink(scope, thumb, attr, ngModelCtrl);
};
}
}
MdSwitch.$inject = ["mdCheckboxDirective", "mdRadioButtonDirective", "$mdTheming"];
})();
(function() {
'use strict';
/**
* @ngdoc module
* @name material.components.tabs
* @description
*
* Tabs, created with the `<md-tabs>` directive provide *tabbed* navigation with different styles.
* The Tabs component consists of clickable tabs that are aligned horizontally side-by-side.
*
* Features include support for:
*
* - static or dynamic tabs,
* - responsive designs,
* - accessibility support (ARIA),
* - tab pagination,
* - external or internal tab content,
* - focus indicators and arrow-key navigations,
* - programmatic lookup and access to tab controllers, and
* - dynamic transitions through different tab contents.
*
*/
/*
* @see js folder for tabs implementation
*/
angular.module('material.components.tabs', [
'material.core'
]);
})();
(function() {
'use strict';
/**
* @ngdoc module
* @name material.components.textField
* @description
* Form
*/
angular.module('material.components.textField', [
'material.core'
])
.directive('mdInputGroup', mdInputGroupDirective)
.directive('mdInput', mdInputDirective)
.directive('mdTextFloat', mdTextFloatDirective);
/**
* @ngdoc directive
* @name mdTextFloat
* @module material.components.textField
*
* @restrict E
*
* @description
* Use the `<md-text-float>` directive to quickly construct `Floating Label` text fields
*
* @param {string} mdFid Attribute used for accessibility link pairing between the Label and Input elements
* @param {string=} type Optional value to define the type of input field. Defaults to string.
* @param {string} label Attribute to specify the input text field hint.
* @param {string=} ng-model Optional value to assign as existing input text string
*
* @usage
* <hljs lang="html">
* <md-text-float label="LastName" ng-model="user.lastName" > </md-text-float>
*
* <!-- Specify a read-only input field by using the `disabled` attribute -->
* <md-text-float label="Company" ng-model="user.company" ng-disabled="true" > </md-text-float>
*
* <!-- Specify an input type if desired. -->
* <md-text-float label="eMail" ng-model="user.email" type="email" ></md-text-float>
* </hljs>
*/
function mdTextFloatDirective($mdTheming, $mdUtil, $parse) {
return {
restrict: 'E',
replace: true,
scope : {
fid : '@?mdFid',
label : '@?',
value : '=ngModel'
},
compile : function(element, attr) {
if ( angular.isUndefined(attr.mdFid) ) {
attr.mdFid = $mdUtil.nextUid();
}
return {
pre : function(scope, element, attrs) {
var disabledParsed = $parse(attrs.ngDisabled);
scope.isDisabled = function() {
return disabledParsed(scope.$parent);
};
scope.inputType = attrs.type || "text";
},
post: $mdTheming
};
},
template:
'<md-input-group tabindex="-1">' +
' <label for="{{fid}}" >{{label}}</label>' +
' <md-input id="{{fid}}" ng-disabled="isDisabled()" ng-model="value" type="{{inputType}}"></md-input>' +
'</md-input-group>'
};
}
mdTextFloatDirective.$inject = ["$mdTheming", "$mdUtil", "$parse"];
/*
* @private
*
* @ngdoc directive
* @name mdInputGroup
* @module material.components.textField
* @restrict E
* @description
* Use the `<md-input-group>` directive as the grouping parent of a `<md-input>` element.
*
* @usage
* <hljs lang="html">
* <md-input-group ng-disabled="isDisabled">
* <label for="{{fid}}">{{someLabel}}</label>
* <md-input id="{{fid}}" type="text" ng-model="someText"></md-input>
* </md-input-group>
* </hljs>
*/
function mdInputGroupDirective() {
return {
restrict: 'CE',
controller: ['$element', function($element) {
this.setFocused = function(isFocused) {
$element.toggleClass('md-input-focused', !!isFocused);
};
this.setHasValue = function(hasValue) {
$element.toggleClass('md-input-has-value', hasValue );
};
}]
};
}
/*
* @private
*
* @ngdoc directive
* @name mdInput
* @module material.components.textField
*
* @restrict E
*
* @description
* Use the `<md-input>` directive as elements within a `<md-input-group>` container
*
* @usage
* <hljs lang="html">
* <md-input-group ng-disabled="user.isLocked">
* <label for="i1">FirstName</label>
* <md-input id="i1" ng-model="user.firstName"></md-input>
* </md-input-group>
* </hljs>
*/
function mdInputDirective($mdUtil) {
return {
restrict: 'E',
replace: true,
template: '<input >',
require: ['^?mdInputGroup', '?ngModel'],
link: function(scope, element, attr, ctrls) {
if ( !ctrls[0] ) return;
var inputGroupCtrl = ctrls[0];
var ngModelCtrl = ctrls[1];
scope.$watch(scope.isDisabled, function(isDisabled) {
element.attr('aria-disabled', !!isDisabled);
element.attr('tabindex', !!isDisabled);
});
element.attr('type', attr.type || element.parent().attr('type') || "text");
// 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( isNotEmpty(value) );
return value;
});
}
element
.on('input', function() {
inputGroupCtrl.setHasValue( isNotEmpty() );
})
.on('focus', function(e) {
// When the input focuses, add the focused class to the group
inputGroupCtrl.setFocused(true);
})
.on('blur', function(e) {
// When the input blurs, remove the focused class from the group
inputGroupCtrl.setFocused(false);
inputGroupCtrl.setHasValue( isNotEmpty() );
});
scope.$on('$destroy', function() {
inputGroupCtrl.setFocused(false);
inputGroupCtrl.setHasValue(false);
});
function isNotEmpty(value) {
value = angular.isUndefined(value) ? element.val() : value;
return (angular.isDefined(value) && (value!==null) &&
(value.toString().trim() !== ""));
}
}
};
}
mdInputDirective.$inject = ["$mdUtil"];
})();
(function() {
'use strict';
/**
* @ngdoc module
* @name material.components.toast
* @description
* Toast
*/
angular.module('material.components.toast', [
'material.core',
'material.components.swipe',
'material.components.button'
])
.directive('mdToast', MdToastDirective)
.provider('$mdToast', MdToastProvider);
function MdToastDirective() {
return {
restrict: 'E'
};
}
/**
* @ngdoc service
* @name $mdToast
* @module material.components.toast
*
* @description
* `$mdToast` is a service to butild a toast nofication on any position
* on the screen with an optional duration, and provides a simple promise API.
*
*
* ### Restrictions on custom toasts
* - The toast's template must have an outer `<md-toast>` element.
* - For a toast action, use element with class `md-action`.
* - Add the class `md-capsule` for curved corners.
*
* @usage
* <hljs lang="html">
* <div ng-controller="MyController">
* <md-button ng-click="openToast()">
* Open a Toast!
* </md-button>
* </div>
* </hljs>
*
* <hljs lang="js">
* var app = angular.module('app', ['ngMaterial']);
* app.controller('MyController', function($scope, $mdToast) {
* $scope.openToast = function($event) {
* $mdToast.show($mdToast.simple().content('Hello!'));
* };
* });
* </hljs>
*/
/**
* @ngdoc method
* @name $mdToast#simple
*
* @description
* Builds a preconfigured toast.
*
* @returns {obj} a `$mdToastPreset` with the chainable configuration methods:
*
* - $mdToastPreset#content(string) - sets toast content to string
* - $mdToastPreset#action(string) - adds an action button, which resolves the promise returned from `show()` if clicked.
* - $mdToastPreset#highlightAction(boolean) - sets action button to be highlighted
* - $mdToastPreset#capsule(boolean) - adds 'md-capsule' class to the toast (curved corners)
*/
/**
* @ngdoc method
* @name $mdToast#build
*
* @description
* Creates a custom `$mdToastPreset` that you can configure.
*
* @returns {obj} a `$mdToastPreset` with the chainable configuration methods for shows' options (see below).
*/
/**
* @ngdoc method
* @name $mdToast#show
*
* @description Shows the toast.
*
* @param {object} optionsOrPreset Either provide an `$mdToastPreset` returned from `simple()`
* and `build()`, or an options object with the following properties:
*
* - `templateUrl` - `{string=}`: The url of an html template file that will
* be used as the content of the toast. Restrictions: the template must
* have an outer `md-toast` element.
* - `template` - `{string=}`: Same as templateUrl, except this is an actual
* template string.
* - `hideDelay` - `{number=}`: How many milliseconds the toast should stay
* active before automatically closing. Set to 0 or false to have the toast stay open until
* closed manually. Default: 3000.
* - `position` - `{string=}`: Where to place the toast. Available: any combination
* of 'bottom', 'left', 'top', 'right', 'fit'. Default: 'bottom left'.
* - `controller` - `{string=}`: The controller to associate with this toast.
* The controller will be injected the local `$hideToast`, which is a function
* used to hide the toast.
* - `locals` - `{string=}`: 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.
* - `resolve` - `{object=}`: Similar to locals, except it takes promises as values
* and the toast will not open until the promises resolve.
* - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope.
*
* @returns {promise} A promise that can be resolved with `$mdToast.hide()` or
* rejected with `$mdToast.cancel()`.
*/
/**
* @ngdoc method
* @name $mdToast#hide
*
* @description
* Hide an existing toast and resolve the promise returned from `$mdToast.show()`.
*
* @param {*=} response An argument for the resolved promise.
*
*/
/**
* @ngdoc method
* @name $mdToast#cancel
*
* @description
* Hide the existing toast and reject the promise returned from
* `$mdToast.show()`.
*
* @param {*=} response An argument for the rejected promise.
*
*/
function MdToastProvider($$interimElementProvider) {
toastDefaultOptions.$inject = ["$timeout", "$animate", "$mdSwipe", "$mdTheming", "$mdToast"];
return $$interimElementProvider('$mdToast')
.setDefaults({
methods: ['position', 'hideDelay', 'capsule'],
options: toastDefaultOptions
})
.addPreset('simple', {
methods: ['content', 'action', 'highlightAction'],
options: /* @ngInject */ ["$mdToast", function($mdToast) {
return {
template: [
'<md-toast ng-class="{\'md-capsule\': toast.capsule}">',
'<span flex>{{ toast.content }}</span>',
'<md-button ng-if="toast.action" ng-click="toast.resolve()" ng-class="{\'md-action\': toast.highlightAction}">',
'{{toast.action}}',
'</md-button>',
'</md-toast>'
].join(''),
controller: function mdToastCtrl() {
this.resolve = function() {
$mdToast.hide();
};
},
controllerAs: 'toast',
bindToController: true
};
}]
});
/* @ngInject */
function toastDefaultOptions($timeout, $animate, $mdSwipe, $mdTheming, $mdToast) {
return {
onShow: onShow,
onRemove: onRemove,
position: 'bottom left',
themable: true,
hideDelay: 3000
};
function onShow(scope, element, options) {
// 'top left' -> 'md-top md-left'
element.addClass(options.position.split(' ').map(function(pos) {
return 'md-' + pos;
}).join(' '));
options.parent.addClass(toastOpenClass(options.position));
var configureSwipe = $mdSwipe(scope, 'swipeleft swiperight');
options.detachSwipe = configureSwipe(element, function(ev) {
//Add swipeleft/swiperight class to element so it can animate correctly
element.addClass('md-' + ev.type);
$timeout($mdToast.cancel);
});
return $animate.enter(element, options.parent);
}
function onRemove(scope, element, options) {
options.detachSwipe();
options.parent.removeClass(toastOpenClass(options.position));
return $animate.leave(element);
}
function toastOpenClass(position) {
return 'md-toast-open-' +
(position.indexOf('top') > -1 ? 'top' : 'bottom');
}
}
}
MdToastProvider.$inject = ["$$interimElementProvider"];
})();
(function() {
'use strict';
/**
* @ngdoc module
* @name material.components.toolbar
*/
angular.module('material.components.toolbar', [
'material.core',
'material.components.content'
])
.directive('mdToolbar', mdToolbarDirective);
/**
* @ngdoc directive
* @name mdToolbar
* @module material.components.toolbar
* @restrict E
* @description
* `md-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
* `md-medium-tall` or `md-tall` class to the toolbar.
*
* @usage
* <hljs lang="html">
* <div layout="column" layout-fill>
* <md-toolbar>
*
* <div class="md-toolbar-tools">
* <span>My App's Title</span>
*
* <!-- fill up the space between left and right area -->
* <span flex></span>
*
* <md-button>
* Right Bar Button
* </md-button>
* </div>
*
* </md-toolbar>
* <md-content>
* Hello!
* </md-content>
* </div>
* </hljs>
*
* @param {boolean=} mdScrollShrink 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
* `md-content` element, placed before it. See the scroll shrink demo.
*
*
* @param {number=} mdShrinkSpeedFactor 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 mdToolbarDirective($$rAF, $mdConstant, $mdUtil, $mdTheming) {
return {
restrict: 'E',
controller: angular.noop,
link: function(scope, element, attr) {
$mdTheming(element);
if (angular.isDefined(attr.mdScrollShrink)) {
setupScrollShrink();
}
function setupScrollShrink() {
// Current "y" position of scroll
var y = 0;
// Store the last scroll top position
var prevScrollTop = 0;
var shrinkSpeedFactor = attr.mdShrinkSpeedFactor || 0.5;
var toolbarHeight;
var contentElement;
var debouncedContentScroll = $$rAF.debounce(onContentScroll);
var debouncedUpdateHeight = $mdUtil.debounce(updateToolbarHeight, 5 * 1000);
// Wait for $mdContentLoaded event from mdContent directive.
// If the mdContent element is a sibling of our toolbar, hook it up
// to scroll events.
scope.$on('$mdContentLoaded', onMdContentLoad);
function onMdContentLoad($event, newContentEl) {
// Toolbar and content must be siblings
if (element.parent()[0] === newContentEl.parent()[0]) {
// 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(
$mdConstant.CSS.TRANSFORM,
'translate3d(0,' + (-y * shrinkSpeedFactor) + 'px,0)'
);
contentElement.css(
$mdConstant.CSS.TRANSFORM,
'translate3d(0,' + ((toolbarHeight - y) * shrinkSpeedFactor) + 'px,0)'
);
prevScrollTop = scrollTop;
}
}
}
};
}
mdToolbarDirective.$inject = ["$$rAF", "$mdConstant", "$mdUtil", "$mdTheming"];
})();
(function() {
'use strict';
/**
* @ngdoc module
* @name material.components.tooltip
*/
angular.module('material.components.tooltip', [
'material.core'
])
.directive('mdTooltip', MdTooltipDirective);
/**
* @ngdoc directive
* @name mdTooltip
* @module material.components.tooltip
* @description
* Tooltips are used to describe elements that are interactive and primarily graphical (not textual).
*
* Place a `<md-tooltip>` as a child of the element it describes.
*
* A tooltip will activate when the user focuses, hovers over, or touches the parent.
*
* @usage
* <hljs lang="html">
* <md-icon icon="/img/icons/ic_play_arrow_24px.svg">
* <md-tooltip>
* Play Music
* </md-tooltip>
* </md-icon>
* </hljs>
*
* @param {expression=} mdVisible Boolean bound to whether the tooltip is
* currently visible.
*/
function MdTooltipDirective($timeout, $window, $$rAF, $document, $mdUtil, $mdTheming) {
var TOOLTIP_SHOW_DELAY = 400;
var TOOLTIP_WINDOW_EDGE_SPACE = 8;
// We have to append tooltips to the body, because we use
// getBoundingClientRect() to find where to append the tooltip.
var tooltipParent = angular.element(document.body);
return {
restrict: 'E',
transclude: true,
require: '^?mdContent',
template:
'<div class="md-background"></div>' +
'<div class="md-content" ng-transclude></div>',
scope: {
visible: '=?mdVisible'
},
link: postLink
};
function postLink(scope, element, attr, contentCtrl) {
$mdTheming(element);
var parent = element.parent();
// We will re-attach tooltip when visible
element.detach();
element.attr('role', 'tooltip');
element.attr('id', attr.id || ('tooltip_' + $mdUtil.nextUid()));
parent.on('focus mouseenter touchstart', function() {
setVisible(true);
});
parent.on('blur mouseleave touchend touchcancel', function() {
// Don't hide the tooltip if the parent is still focused.
if ($document.activeElement === parent[0]) return;
setVisible(false);
});
scope.$watch('visible', function(isVisible) {
if (isVisible) showTooltip();
else hideTooltip();
});
var debouncedOnResize = $$rAF.debounce(function windowResize() {
// Reposition on resize
if (scope.visible) positionTooltip();
});
angular.element($window).on('resize', debouncedOnResize);
// Be sure to completely cleanup the element on destroy
scope.$on('$destroy', function() {
scope.visible = false;
element.remove();
angular.element($window).off('resize', debouncedOnResize);
});
// *******
// Methods
// *******
// If setting visible to true, debounce to TOOLTIP_SHOW_DELAY ms
// If setting visible to false and no timeout is active, instantly hide the tooltip.
function setVisible(value) {
setVisible.value = !!value;
if (!setVisible.queued) {
if (value) {
setVisible.queued = true;
$timeout(function() {
scope.visible = setVisible.value;
setVisible.queued = false;
}, TOOLTIP_SHOW_DELAY);
} else {
$timeout(function() { scope.visible = false; });
}
}
}
function showTooltip() {
// Insert the element before positioning it, so we can get position
// (tooltip is hidden by default)
element.removeClass('md-hide');
parent.attr('aria-describedby', element.attr('id'));
tooltipParent.append(element);
// Wait until the element has been in the dom for two frames before
// fading it in.
// Additionally, we position the tooltip twice to avoid positioning bugs
//positionTooltip();
$$rAF(function() {
$$rAF(function() {
positionTooltip();
if (!scope.visible) return;
element.addClass('md-show');
});
});
}
function hideTooltip() {
element.removeClass('md-show').addClass('md-hide');
parent.removeAttr('aria-describedby');
$timeout(function() {
if (scope.visible) return;
element.detach();
}, 200, false);
}
function positionTooltip() {
var tipRect = element[0].getBoundingClientRect();
var parentRect = parent[0].getBoundingClientRect();
if (contentCtrl) {
parentRect.top += contentCtrl.$element.prop('scrollTop');
parentRect.left += contentCtrl.$element.prop('scrollLeft');
}
// Default to bottom position if possible
var tipDirection = 'bottom';
var newPosition = {
left: parentRect.left + parentRect.width / 2 - tipRect.width / 2,
top: parentRect.top + parentRect.height
};
// If element bleeds over left/right of the window, place it on the edge of the window.
newPosition.left = Math.min(
newPosition.left,
$window.innerWidth - tipRect.width - TOOLTIP_WINDOW_EDGE_SPACE
);
newPosition.left = Math.max(newPosition.left, TOOLTIP_WINDOW_EDGE_SPACE);
// If element bleeds over the bottom of the window, place it above the parent.
if (newPosition.top + tipRect.height > $window.innerHeight) {
newPosition.top = parentRect.top - tipRect.height;
tipDirection = 'top';
}
element.css({top: newPosition.top + 'px', left: newPosition.left + 'px'});
// Tell the CSS the size of this tooltip, as a multiple of 32.
element.attr('width-32', Math.ceil(tipRect.width / 32));
element.attr('md-direction', tipDirection);
}
}
}
MdTooltipDirective.$inject = ["$timeout", "$window", "$$rAF", "$document", "$mdUtil", "$mdTheming"];
})();
(function() {
'use strict';
/**
* @ngdoc module
* @name material.components.whiteframe
*/
angular.module('material.components.whiteframe', []);
})();
(function() {
'use strict';
/**
* Conditionally configure ink bar animations when the
* tab selection changes. If `mdNoBar` then do not show the
* bar nor animate.
*/
angular.module('material.components.tabs')
.directive('mdTabsInkBar', MdTabInkDirective);
function MdTabInkDirective($mdConstant, $window, $$rAF, $timeout) {
return {
restrict: 'E',
require: ['^?mdNoBar', '^mdTabs'],
link: postLink
};
function postLink(scope, element, attr, ctrls) {
var nobar = ctrls[0],
tabsCtrl = ctrls[1],
timeout;
if (nobar) return;
tabsCtrl.inkBarElement = element;
scope.$watch(tabsCtrl.selected, updateBar);
scope.$on('$mdTabsChanged', updateBar);
function updateBar() {
var selected = tabsCtrl.selected();
var hideInkBar = !selected || tabsCtrl.count() < 2 ||
(scope.pagination || {}).itemsPerPage === 1;
element.css('display', hideInkBar ? 'none' : 'block');
if (!hideInkBar) {
var count = tabsCtrl.count();
var scale = 1 / count;
var left = tabsCtrl.indexOf(selected);
element.css($mdConstant.CSS.TRANSFORM, 'scaleX(' + scale + ') ' +
'translate3d(' + left * 100 + '%,0,0)');
element.addClass('md-ink-bar-grow');
if (timeout) $timeout.cancel(timeout);
timeout = $timeout(function () {
element.removeClass('md-ink-bar-grow');
}, 250, false);
}
}
}
}
MdTabInkDirective.$inject = ["$mdConstant", "$window", "$$rAF", "$timeout"];
})();
(function() {
'use strict';
angular.module('material.components.tabs')
.directive('mdTabsPagination', TabPaginationDirective);
function TabPaginationDirective($mdConstant, $window, $$rAF, $$q, $timeout) {
// TODO allow configuration of TAB_MIN_WIDTH
// Must match tab min-width rule in _tabs.scss
var TAB_MIN_WIDTH = 8 * 12;
// Must match (2 * width of paginators) in scss
var PAGINATORS_WIDTH = (8 * 4) * 2;
return {
restrict: 'A',
require: '^mdTabs',
link: postLink
};
function postLink(scope, element, attr, tabsCtrl) {
var tabsParent = element.children();
var state = scope.pagination = {
page: -1,
active: false,
clickNext: function() { userChangePage(+1); },
clickPrevious: function() { userChangePage(-1); }
};
updatePagination();
var debouncedUpdatePagination = $$rAF.debounce(updatePagination);
scope.$on('$mdTabsChanged', debouncedUpdatePagination);
angular.element($window).on('resize', debouncedUpdatePagination);
// Listen to focus events bubbling up from md-tab elements
tabsParent.on('focusin', onTabsFocusIn);
scope.$on('$destroy', function() {
angular.element($window).off('resize', debouncedUpdatePagination);
tabsParent.off('focusin', onTabsFocusIn);
});
scope.$watch(tabsCtrl.selected, onSelectedTabChange);
// Allows pagination through focus change.
function onTabsFocusIn(ev) {
if (!state.active) return;
var tab = angular.element(ev.target).controller('mdTab');
var pageIndex = getPageForTab(tab);
if (pageIndex !== state.page) {
// If the focused element is on a new page, don't focus yet.
tab.element.blur();
// Go to the new page, wait for the page transition to end, then focus.
setPage(pageIndex).then(function() {
tab.element.focus();
});
}
}
function onSelectedTabChange(selectedTab) {
if (!selectedTab) return;
if (state.active) {
var selectedTabPage = getPageForTab(selectedTab);
setPage(selectedTabPage);
} else {
debouncedUpdatePagination();
}
}
// Called when page is changed by a user action (click)
function userChangePage(increment) {
var newPage = state.page + increment;
var newTab;
if (!tabsCtrl.selected() || getPageForTab(tabsCtrl.selected()) !== newPage) {
var startIndex;
if (increment < 0) {
// If going backward, select the previous available tab, starting from
// the first item on the page after newPage.
startIndex = (newPage + 1) * state.itemsPerPage;
newTab = tabsCtrl.previous( tabsCtrl.itemAt(startIndex) );
} else {
// If going forward, select the next available tab, starting with the
// last item before newPage.
startIndex = (newPage * state.itemsPerPage) - 1;
newTab = tabsCtrl.next( tabsCtrl.itemAt(startIndex) );
}
}
setPage(newPage).then(function() {
newTab && newTab.element.focus();
});
newTab && tabsCtrl.select(newTab);
}
function updatePagination() {
var tabs = element.find('md-tab');
var tabsWidth = element.parent().prop('clientWidth') - PAGINATORS_WIDTH;
var needPagination = tabsWidth && TAB_MIN_WIDTH * tabsCtrl.count() > tabsWidth;
var paginationToggled = needPagination !== state.active;
// If the md-tabs element is not displayed, then do nothing.
if ( tabsWidth <= 0 ) {
needPagination = false;
paginationToggled = true;
}
state.active = needPagination;
if (needPagination) {
state.pagesCount = Math.ceil((TAB_MIN_WIDTH * tabsCtrl.count()) / tabsWidth);
state.itemsPerPage = Math.max(1, Math.floor(tabsCtrl.count() / state.pagesCount));
state.tabWidth = tabsWidth / state.itemsPerPage;
tabsParent.css('width', state.tabWidth * tabsCtrl.count() + 'px');
tabs.css('width', state.tabWidth + 'px');
var selectedTabPage = getPageForTab(tabsCtrl.selected());
setPage(selectedTabPage);
} else {
if (paginationToggled) {
$timeout(function() {
tabsParent.css('width', '');
tabs.css('width', '');
slideTabButtons(0);
state.page = -1;
});
}
}
}
function slideTabButtons(x) {
if (tabsCtrl.pagingOffset === x) {
// Resolve instantly if no change
return $$q.when();
}
var deferred = $$q.defer();
tabsCtrl.$$pagingOffset = x;
tabsParent.css($mdConstant.CSS.TRANSFORM, 'translate3d(' + x + 'px,0,0)');
tabsParent.on($mdConstant.CSS.TRANSITIONEND, onTabsParentTransitionEnd);
return deferred.promise;
function onTabsParentTransitionEnd(ev) {
// Make sure this event didn't bubble up from an animation in a child element.
if (ev.target === tabsParent[0]) {
tabsParent.off($mdConstant.CSS.TRANSITIONEND, onTabsParentTransitionEnd);
deferred.resolve();
}
}
}
function getPageForTab(tab) {
var tabIndex = tabsCtrl.indexOf(tab);
if (tabIndex === -1) return 0;
return Math.floor(tabIndex / state.itemsPerPage);
}
function setPage(page) {
if (page === state.page) return;
var lastPage = state.pagesCount;
if (page < 0) page = 0;
if (page > lastPage) page = lastPage;
state.hasPrev = page > 0;
state.hasNext = ((page + 1) * state.itemsPerPage) < tabsCtrl.count();
state.page = page;
$timeout(function() {
scope.$broadcast('$mdTabsPaginationChanged');
});
return slideTabButtons(-page * state.itemsPerPage * state.tabWidth);
}
}
}
TabPaginationDirective.$inject = ["$mdConstant", "$window", "$$rAF", "$$q", "$timeout"];
})();
(function() {
'use strict';
angular.module('material.components.tabs')
.controller('$mdTab', TabItemController);
function TabItemController($scope, $element, $attrs, $compile, $animate, $mdUtil, $parse) {
var self = this;
// Properties
self.contentContainer = angular.element('<div class="md-tab-content ng-hide">');
self.hammertime = new Hammer(self.contentContainer[0]);
self.element = $element;
// Methods
self.isDisabled = isDisabled;
self.onAdd = onAdd;
self.onRemove = onRemove;
self.onSelect = onSelect;
self.onDeselect = onDeselect;
var disabledParsed = $parse($attrs.ngDisabled);
function isDisabled() {
return disabledParsed($scope.$parent);
}
/**
* Add the tab's content to the DOM container area in the tabs,
* @param contentArea the contentArea to add the content of the tab to
*/
function onAdd(contentArea) {
if (self.content.length) {
self.contentContainer.append(self.content);
self.contentScope = $scope.$parent.$new();
contentArea.append(self.contentContainer);
$compile(self.contentContainer)(self.contentScope);
$mdUtil.disconnectScope(self.contentScope);
}
}
function onRemove() {
self.hammertime.destroy();
$animate.leave(self.contentContainer).then(function() {
self.contentScope && self.contentScope.$destroy();
self.contentScope = null;
});
}
function onSelect() {
// Resume watchers and events firing when tab is selected
$mdUtil.reconnectScope(self.contentScope);
self.hammertime.on('swipeleft swiperight', $scope.onSwipe);
$element.addClass('active');
$element.attr('aria-selected', true);
$element.attr('tabIndex', 0);
$animate.removeClass(self.contentContainer, 'ng-hide');
$scope.onSelect();
}
function onDeselect() {
// Stop watchers & events from firing while tab is deselected
$mdUtil.disconnectScope(self.contentScope);
self.hammertime.off('swipeleft swiperight', $scope.onSwipe);
$element.removeClass('active');
$element.attr('aria-selected', false);
// Only allow tabbing to the active tab
$element.attr('tabIndex', -1);
$animate.addClass(self.contentContainer, 'ng-hide');
$scope.onDeselect();
}
}
TabItemController.$inject = ["$scope", "$element", "$attrs", "$compile", "$animate", "$mdUtil", "$parse"];
})();
(function() {
'use strict';
angular.module('material.components.tabs')
.directive('mdTab', MdTabDirective);
/**
* @ngdoc directive
* @name mdTab
* @module material.components.tabs
*
* @restrict E
*
* @description
* `<md-tab>` is the nested directive used [within `<md-tabs>`] to specify each tab with a **label** and optional *view content*.
*
* If the `label` attribute is not specified, then an optional `<md-tab-label>` tag can be used to specified more
* complex tab header markup. If neither the **label** nor the **md-tab-label** are specified, then the nested
* markup of the `<md-tab>` is used as the tab header markup.
*
* If a tab **label** has been identified, then any **non-**`<md-tab-label>` markup
* will be considered tab content and will be transcluded to the internal `<div class="md-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=} mdActive When evaluteing to true, selects the tab.
* @param {boolean=} disabled If present, disabled tab selection.
* @param {expression=} mdOnDeselect Expression to be evaluated after the tab has been de-selected.
* @param {expression=} mdOnSelect Expression to be evaluated after the tab has been selected.
*
*
* @usage
*
* <hljs lang="html">
* <md-tab label="" disabled="" md-on-select="" md-on-deselect="" >
* <h3>My Tab content</h3>
* </md-tab>
*
* <md-tab >
* <md-tab-label>
* <h3>My Tab content</h3>
* </md-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>
* </md-tab>
* </hljs>
*
*/
function MdTabDirective($mdInkRipple, $compile, $mdAria, $mdUtil, $mdConstant) {
return {
restrict: 'E',
require: ['mdTab', '^mdTabs'],
controller: '$mdTab',
scope: {
onSelect: '&mdOnSelect',
onDeselect: '&mdOnDeselect',
label: '@'
},
compile: compile
};
function compile(element, attr) {
var tabLabel = element.find('md-tab-label');
if (tabLabel.length) {
// If a tab label element is found, remove it for later re-use.
tabLabel.remove();
} else if (angular.isDefined(attr.label)) {
// Otherwise, try to use attr.label as the label
tabLabel = angular.element('<md-tab-label>').html(attr.label);
} else {
// If nothing is found, use the tab's content as the label
tabLabel = angular.element('<md-tab-label>')
.append(element.contents().remove());
}
// Everything that's left as a child is the tab's content.
var tabContent = element.contents().remove();
return function postLink(scope, element, attr, ctrls) {
var tabItemCtrl = ctrls[0]; // Controller for THIS tabItemCtrl
var tabsCtrl = ctrls[1]; // Controller for ALL tabs
transcludeTabContent();
configureAria();
var detachRippleFn = $mdInkRipple.attachTabBehavior(scope, element, {
colorElement: tabsCtrl.inkBarElement
});
tabsCtrl.add(tabItemCtrl);
scope.$on('$destroy', function() {
detachRippleFn();
tabsCtrl.remove(tabItemCtrl);
});
if (!angular.isDefined(attr.ngClick)) {
element.on('click', defaultClickListener);
}
element.on('keydown', keydownListener);
scope.onSwipe = onSwipe;
if (angular.isNumber(scope.$parent.$index)) {
watchNgRepeatIndex();
}
if (angular.isDefined(attr.mdActive)) {
watchActiveAttribute();
}
watchDisabled();
function transcludeTabContent() {
// Clone the label we found earlier, and $compile and append it
var label = tabLabel.clone();
element.append(label);
$compile(label)(scope.$parent);
// Clone the content we found earlier, and mark it for later placement into
// the proper content area.
tabItemCtrl.content = tabContent.clone();
}
//defaultClickListener isn't applied if the user provides an ngClick expression.
function defaultClickListener() {
scope.$apply(function() {
tabsCtrl.select(tabItemCtrl);
tabItemCtrl.element.focus();
});
}
function keydownListener(ev) {
if (ev.keyCode == $mdConstant.KEY_CODE.SPACE || ev.keyCode == $mdConstant.KEY_CODE.ENTER ) {
// Fire the click handler to do normal selection if space is pressed
element.triggerHandler('click');
ev.preventDefault();
} else if (ev.keyCode === $mdConstant.KEY_CODE.LEFT_ARROW) {
var previous = tabsCtrl.previous(tabItemCtrl);
previous && previous.element.focus();
} else if (ev.keyCode === $mdConstant.KEY_CODE.RIGHT_ARROW) {
var next = tabsCtrl.next(tabItemCtrl);
next && next.element.focus();
}
}
function onSwipe(ev) {
scope.$apply(function() {
if (ev.type === 'swipeleft') {
tabsCtrl.select(tabsCtrl.next());
} else {
tabsCtrl.select(tabsCtrl.previous());
}
});
}
// If tabItemCtrl is part of an ngRepeat, move the tabItemCtrl in our internal array
// when its $index changes
function watchNgRepeatIndex() {
// The tabItemCtrl has an isolate scope, so we watch the $index on the parent.
scope.$watch('$parent.$index', function $indexWatchAction(newIndex) {
tabsCtrl.move(tabItemCtrl, newIndex);
});
}
function watchActiveAttribute() {
var unwatch = scope.$parent.$watch('!!(' + attr.mdActive + ')', activeWatchAction);
scope.$on('$destroy', unwatch);
function activeWatchAction(isActive) {
var isSelected = tabsCtrl.selected() === tabItemCtrl;
if (isActive && !isSelected) {
tabsCtrl.select(tabItemCtrl);
} else if (!isActive && isSelected) {
tabsCtrl.deselect(tabItemCtrl);
}
}
}
function watchDisabled() {
scope.$watch(tabItemCtrl.isDisabled, disabledWatchAction);
function disabledWatchAction(isDisabled) {
element.attr('aria-disabled', isDisabled);
// Auto select `next` tab when disabled
var isSelected = (tabsCtrl.selected() === tabItemCtrl);
if (isSelected && isDisabled) {
tabsCtrl.select(tabsCtrl.next() || tabsCtrl.previous());
}
}
}
function configureAria() {
// Link together the content area and tabItemCtrl with an id
var tabId = attr.id || ('tab_' + $mdUtil.nextUid());
element.attr({
id: tabId,
role: 'tab',
tabIndex: -1 //this is also set on select/deselect in tabItemCtrl
});
// Only setup the contentContainer's aria attributes if tab content is provided
if (tabContent.length) {
var tabContentId = 'content_' + tabId;
if (!element.attr('aria-controls')) {
element.attr('aria-controls', tabContentId);
}
tabItemCtrl.contentContainer.attr({
id: tabContentId,
role: 'tabpanel',
'aria-labelledby': tabId
});
}
}
};
}
}
MdTabDirective.$inject = ["$mdInkRipple", "$compile", "$mdAria", "$mdUtil", "$mdConstant"];
})();
(function() {
'use strict';
angular.module('material.components.tabs')
.controller('$mdTabs', MdTabsController);
function MdTabsController($scope, $element, $mdUtil) {
var tabsList = $mdUtil.iterator([], false);
var self = this;
// Properties
self.$element = $element;
// The section containing the tab content $elements
self.contentArea = angular.element($element[0].querySelector('.md-tabs-content'));
// Methods from iterator
self.inRange = tabsList.inRange;
self.indexOf = tabsList.indexOf;
self.itemAt = tabsList.itemAt;
self.count = tabsList.count;
self.selected = selected;
self.add = add;
self.remove = remove;
self.move = move;
self.select = select;
self.deselect = deselect;
self.next = next;
self.previous = previous;
$scope.$on('$destroy', function() {
self.deselect(self.selected());
for (var i = tabsList.count() - 1; i >= 0; i--) {
self.remove(tabsList[i], true);
}
});
// Get the selected tab
function selected() {
return self.itemAt($scope.selectedIndex);
}
// Add a new tab.
// Returns a method to remove the tab from the list.
function add(tab, index) {
tabsList.add(tab, index);
tab.onAdd(self.contentArea);
// Select the new tab if we don't have a selectedIndex, or if the
// selectedIndex we've been waiting for is this tab
if ($scope.selectedIndex === -1 || !angular.isNumber($scope.selectedIndex) ||
$scope.selectedIndex === self.indexOf(tab)) {
self.select(tab);
}
$scope.$broadcast('$mdTabsChanged');
}
function remove(tab, noReselect) {
if (!tabsList.contains(tab)) return;
if (noReselect) {
// do nothing
} else if (self.selected() === tab) {
if (tabsList.count() > 1) {
self.select(self.previous() || self.next());
} else {
self.deselect(tab);
}
}
tabsList.remove(tab);
tab.onRemove();
$scope.$broadcast('$mdTabsChanged');
}
// Move a tab (used when ng-repeat order changes)
function move(tab, toIndex) {
var isSelected = self.selected() === tab;
tabsList.remove(tab);
tabsList.add(tab, toIndex);
if (isSelected) self.select(tab);
$scope.$broadcast('$mdTabsChanged');
}
function select(tab) {
if (!tab || tab.isSelected || tab.isDisabled()) return;
if (!tabsList.contains(tab)) return;
self.deselect(self.selected());
$scope.selectedIndex = self.indexOf(tab);
tab.isSelected = true;
tab.onSelect();
}
function deselect(tab) {
if (!tab || !tab.isSelected) return;
if (!tabsList.contains(tab)) return;
$scope.selectedIndex = -1;
tab.isSelected = false;
tab.onDeselect();
}
function next(tab, filterFn) {
return tabsList.next(tab || self.selected(), filterFn || isTabEnabled);
}
function previous(tab, filterFn) {
return tabsList.previous(tab || self.selected(), filterFn || isTabEnabled);
}
function isTabEnabled(tab) {
return tab && !tab.isDisabled();
}
}
MdTabsController.$inject = ["$scope", "$element", "$mdUtil"];
})();
(function() {
'use strict';
angular.module('material.components.tabs')
.directive('mdTabs', TabsDirective);
/**
* @ngdoc directive
* @name mdTabs
* @module material.components.tabs
*
* @restrict E
*
* @description
* The `<md-tabs>` directive serves as the container for 1..n `<md-tab>` child directives to produces a Tabs components.
* In turn, the nested `<md-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">
* <md-tabs>
* <md-tab label="Tab #1"></md-tab>
* <md-tab label="Tab #2"></md-tab>
* <md-tab label="Tab #3"></md-tab>
* <md-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 **`<md-tab>`** tags) will be transcluded into the tab header area BEFORE the tab buttons.
*
* @param {integer=} mdSelected Index of the active/selected tab
* @param {boolean=} mdNoInk If present, disables ink ripple effects.
* @param {boolean=} mdNoBar If present, disables the selection ink bar.
* @param {string=} mdAlignTabs Attribute to indicate position of tab buttons: bottom or top; default is `top`
*
* @usage
* <hljs lang="html">
* <md-tabs md-selected="selectedIndex" >
* <img ng-src="img/angular.png" class="centered">
*
* <md-tab
* ng-repeat="tab in tabs | orderBy:predicate:reversed"
* md-on-select="onTabSelected(tab)"
* md-on-deselect="announceDeselected(tab)"
* disabled="tab.disabled" >
*
* <md-tab-label>
* {{tab.title}}
* <img src="img/removeTab.png"
* ng-click="removeTab(tab)"
* class="delete" >
* </md-tab-label>
*
* {{tab.content}}
*
* </md-tab>
*
* </md-tabs>
* </hljs>
*
*/
function TabsDirective($parse, $mdTheming) {
return {
restrict: 'E',
controller: '$mdTabs',
require: 'mdTabs',
transclude: true,
scope: {
selectedIndex: '=?mdSelected'
},
template:
'<section class="md-header" ' +
'ng-class="{\'md-paginating\': pagination.active}">' +
'<button class="md-paginator md-prev" ' +
'ng-if="pagination.active && pagination.hasPrev" ' +
'ng-click="pagination.clickPrevious()" ' +
'aria-hidden="true">' +
'</button>' +
// overflow: hidden container when paginating
'<div class="md-header-items-container" md-tabs-pagination>' +
// flex container for <md-tab> elements
'<div class="md-header-items">' +
'<md-tabs-ink-bar></md-tabs-ink-bar>' +
'<md-tabs-ink-bar class="md-ink-bar-delayed"></md-tabs-ink-bar>' +
'</div>' +
'</div>' +
'<button class="md-paginator md-next" ' +
'ng-if="pagination.active && pagination.hasNext" ' +
'ng-click="pagination.clickNext()" ' +
'aria-hidden="true">' +
'</button>' +
'</section>' +
'<section class="md-tabs-content"></section>',
link: postLink
};
function postLink(scope, element, attr, tabsCtrl, transclude) {
$mdTheming(element);
configureAria();
watchSelected();
transclude(scope.$parent, function(clone) {
angular.element(element[0].querySelector('.md-header-items')).append(clone);
});
function configureAria() {
element.attr({
role: 'tablist'
});
}
function watchSelected() {
scope.$watch('selectedIndex', function watchSelectedIndex(newIndex, oldIndex) {
// Note: if the user provides an invalid newIndex, all tabs will be deselected
// and the associated view will be hidden.
tabsCtrl.deselect( tabsCtrl.itemAt(oldIndex) );
if (tabsCtrl.inRange(newIndex)) {
var newTab = tabsCtrl.itemAt(newIndex);
// If the newTab is disabled, find an enabled one to go to.
if (newTab && newTab.isDisabled()) {
newTab = newIndex > oldIndex ?
tabsCtrl.next(newTab) :
tabsCtrl.previous(newTab);
}
tabsCtrl.select(newTab);
}
});
}
}
}
TabsDirective.$inject = ["$parse", "$mdTheming"];
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment