/*! * Angular Material Design * https://github.com/angular/material * @license MIT * v0.7.0-rc3 */ (function() { 'use strict'; /** * Initialization function that validates environment * requirements. */ angular.module('material.core', ['material.core.theming']) .run(MdCoreInitialize) .config(MdCoreConfigure); function MdCoreInitialize() { if (typeof Hammer === 'undefined') { throw new Error( 'ngMaterial requires HammerJS to be preloaded.' ); } // By default, Hammer disables user selection on desktop if swipe is enabled. // We don't want this, so we make sure Hammer doesn't set a user-select: none. Hammer.defaults.cssProps.userSelect = ''; } function MdCoreConfigure($provide, $mdThemingProvider) { $provide.decorator('$$rAF', ['$delegate', '$rootScope', rAFDecorator]); $mdThemingProvider.theme('default') .primaryColor('blue') .accentColor('green') .warnColor('red') .backgroundColor('grey'); 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", "$mdThemingProvider"]; })(); (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') }, MEDIA: { 'sm': '(max-width: 600px)', 'gt-sm': '(min-width: 600px)', 'md': '(min-width: 600px) and (max-width: 960px)', 'gt-md': '(min-width: 960px)', 'lg': '(min-width: 960px) and (max-width: 1200px)', 'gt-lg': '(min-width: 1200px)' } }; } MdConstantFactory.$inject = ["$$rAF", "$sniffer"]; })(); (function(){ angular .module('material.core') .config( ["$provide", function($provide){ $provide.decorator('$mdUtil', ['$delegate', function ($delegate){ /** * Inject the iterator facade to easily support iteration and accessors * @see iterator below */ $delegate.iterator = Iterator; return $delegate; } ]); }]); /** * 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: angular.bind(null, findSubsequentItem, false), previous: angular.bind(null, findSubsequentItem, true), 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); } /** * 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; } /** * 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 function * @param {optional} recursion limit * @returns {*} */ function findSubsequentItem(backwards, item, validate, limit) { validate = validate || trueFn; var curIndex = indexOf(item); if (!inRange(curIndex)) { return null; } var nextIndex = curIndex + (backwards ? -1 : 1); var foundItem = null; if (inRange(nextIndex)) { foundItem = _items[nextIndex]; } else if (reloop) { foundItem = backwards ? last() : first(); nextIndex = indexOf(foundItem); } if ((foundItem === null) || (nextIndex === limit)) { return null; } if (angular.isUndefined(limit)) { limit = nextIndex; } return validate(foundItem) ? foundItem : findSubsequentItem(backwards, foundItem, validate, limit); } } })(); angular.module('material.core') .factory('$mdMedia', mdMediaFactory); /** * 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($mdConstant, $mdUtil, $rootScope, $window) { var queriesCache = $mdUtil.cacheFactory('$mdMedia:queries', {capacity: 15}); var resultsCache = $mdUtil.cacheFactory('$mdMedia:results', {capacity: 15}); angular.element($window).on('resize', updateAll); return $mdMedia; function $mdMedia(query) { var validated = queriesCache.get(query); if (angular.isUndefined(validated)) { validated = queriesCache.put(query, validate(query)); } var result = resultsCache.get(validated); if (angular.isUndefined(result)) { result = add(validated); } return result; } function validate(query) { return $mdConstant.MEDIA[query] || ((query.charAt(0) !== '(') ? ('(' + query + ')') : query); } function add(query) { return resultsCache.put(query, !!$window.matchMedia(query).matches); } function updateAll() { var keys = resultsCache.keys(); var len = keys.length; if (len) { for (var i = 0; i < len; i++) { add(keys[i]); } // Trigger a $digest() if not already in progress $rootScope.$evalAsync(); } } } mdMediaFactory.$inject = ["$mdConstant", "$mdUtil", "$rootScope", "$window"]; (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", "$document", "$timeout", function($cacheFactory, $document, $timeout) { var Util; return Util = { now: window.performance ? angular.bind(window.performance, window.performance.now) : Date.now, attachDragBehavior: attachDragBehavior, elementRect: function(element, offsetParent) { var node = element[0]; offsetParent = offsetParent || node.offsetParent || document.body; offsetParent = offsetParent[0] || offsetParent; var nodeRect = node.getBoundingClientRect(); var parentRect = offsetParent.getBoundingClientRect(); return { left: nodeRect.left - parentRect.left + offsetParent.scrollLeft, top: nodeRect.top - parentRect.top + offsetParent.scrollTop, width: nodeRect.width, height: nodeRect.height }; }, fakeNgModel: function() { return { $setViewValue: function(value) { this.$viewValue = value; this.$render(value); this.$viewChangeListeners.forEach(function(cb) { cb(); }); }, $parsers: [], $formatters: [], $viewChangeListeners: [], $render: angular.noop }; }, /** * @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. // @param wait Integer value of msecs to delay (since last debounce reset); default value 10 msecs // @param invokeApply should the $timeout trigger $digest() dirty checking debounce: function (func, wait, scope, invokeApply) { var timer; return function debounced() { var context = scope, args = Array.prototype.slice.call(arguments); $timeout.cancel(timer); timer = $timeout(function() { timer = undefined; func.apply(context, args); }, wait || 10, invokeApply ); }; }, // 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 || (now - recent > 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; } }, /* * getClosest replicates jQuery.closest() to walk up the DOM tree until it finds a matching nodeName * * @param el Element to start walking the DOM from * @param tagName Tag name to find closest to el, such as 'form' */ getClosest: function getClosest(el, tagName) { tagName = tagName.toUpperCase(); do { if (el.nodeName === tagName) { return el; } } while (el = el.parentNode); return null; } }; function attachDragBehavior(scope, element, options) { // The state of the current drag & previous drag var drag; var previousDrag; // Whether the pointer is currently down on this element. var pointerIsDown; var START_EVENTS = 'mousedown touchstart pointerdown'; var MOVE_EVENTS = 'mousemove touchmove pointermove'; var END_EVENTS = 'mouseup mouseleave touchend touchcancel pointerup pointercancel'; // Listen to move and end events on document. End events especially could have bubbled up // from the child. element.on(START_EVENTS, startDrag); $document.on(MOVE_EVENTS, doDrag) .on(END_EVENTS, endDrag); scope.$on('$destroy', cleanup); return cleanup; function cleanup() { if (cleanup.called) return; cleanup.called = true; element.off(START_EVENTS, startDrag); $document.off(MOVE_EVENTS, doDrag) .off(END_EVENTS, endDrag); drag = pointerIsDown = false; } function startDrag(ev) { var eventType = ev.type.charAt(0); var now = Util.now(); // iOS & old android bug: after a touch event, iOS sends a click event 350 ms later. // Don't allow a drag of a different pointerType than the previous drag if it has been // less than 400ms. if (previousDrag && previousDrag.pointerType !== eventType && (now - previousDrag.endTime < 400)) { return; } if (pointerIsDown) return; pointerIsDown = true; drag = { // Restrict this drag to whatever started it: if a mousedown started the drag, // don't let anything but mouse events continue it. pointerType: eventType, startX: getPosition(ev), startTime: now }; element.one('$md.dragstart', function(ev) { // Allow user to cancel by preventing default if (ev.defaultPrevented) drag = null; }); element.triggerHandler('$md.dragstart', drag); } function doDrag(ev) { if (!drag || !isProperEventType(ev, drag)) return; if (drag.pointerType === 't' || drag.pointerType === 'p') { // No scrolling for touch/pointer events ev.preventDefault(); } updateDragState(ev); element.triggerHandler('$md.drag', drag); } function endDrag(ev) { pointerIsDown = false; if (!drag || !isProperEventType(ev, drag)) return; drag.endTime = Util.now(); updateDragState(ev); element.triggerHandler('$md.dragend', drag); previousDrag = drag; drag = null; } function updateDragState(ev) { var x = getPosition(ev); drag.distance = drag.startX - x; drag.direction = drag.distance > 0 ? 'left' : (drag.distance < 0 ? 'right' : ''); drag.duration = drag.startTime - Util.now(); drag.velocity = Math.abs(drag.duration) / drag.time; } function getPosition(ev) { ev = ev.originalEvent || ev; //support jQuery events var point = (ev.touches && ev.touches[0]) || (ev.changedTouches && ev.changedTouches[0]) || ev; return point.pageX; } function isProperEventType(ev, drag) { return drag && ev && (ev.type || '').charAt(0) === drag.pointerType; } } /* * Inject a 'keys()' method into Angular's $cacheFactory. Then * head-hook all other methods * */ 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._removeAll = cache.removeAll; cache.removeAll = function() { keys = {}; return cache._removeAll(); }; cache._destroy = cache.destroy; cache.destroy = function() { keys = {}; return cache._destroy(); }; 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 * $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 * }); * */ /* * @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.=}` - 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('
').html(template.trim()).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", "$interpolate", "$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 EXPOSED_METHODS = ['onHide', 'onShow', 'onRemove']; 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', '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 || []).concat(EXPOSED_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.concat(EXPOSED_METHODS), optionsFactory: definition.options, argOption: definition.argOption }; 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; }; }); // Create shortcut method for one-linear methods if (definition.argOption) { var methodName = 'show' + name.charAt(0).toUpperCase() + name.slice(1); publicService[methodName] = function(arg) { var config = publicService[name](arg); return publicService.show(config); }; } // eg $mdDialog.alert() will return a new alert preset publicService[name] = function(arg) { // If argOption is supplied, eg `argOption: 'content'`, then we assume // if the argument is not an options object then it is the `argOption` option. // // @example `$mdToast.simple('hello')` // sets options.content to hello // // because argOption === 'content' if (arguments.length && definition.argOption && !angular.isObject(arg) && !angular.isArray(arg)) { return (new Preset())[definition.argOption](arg); } else { return new Preset(arg); } }; }); 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 if (opts && opts._options) opts = opts._options; return interimElementService.show( angular.extend({}, defaultOptions, 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, $interpolate, $mdCompiler, $mdTheming ) { var startSymbol = $interpolate.startSymbol(), endSymbol = $interpolate.endSymbol(), usesStandardSymbols = ((startSymbol === '{{') && (endSymbol === '}}')), processTemplate = usesStandardSymbols ? angular.identity : replaceInterpolationSymbols; 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 }; /* * @ngdoc method * @name $$interimElement.$service#show * @kind function * * @description * Adds the `$interimElement` to the DOM and returns a promise that will be resolved or rejected * with hide or cancel, respectively. * * @param {*} options is hashMap of settings * @returns a Promise * */ 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 a Promise that will be resolved after the element has been removed. * */ function hide(response) { var interimElement = stack.shift(); interimElement && interimElement.remove().then(function() { interimElement.deferred.resolve(response); }); return interimElement ? interimElement.deferred.promise : $q.when(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 Promise that will be rejected after the element has been removed. * */ function cancel(reason) { var interimElement = stack.shift(); interimElement && interimElement.remove().then(function() { interimElement.deferred.reject(reason); }); return interimElement ? interimElement.deferred.promise : $q.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); if (options.template) { options.template = processTemplate(options.template); } 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(); }); } }; } }; /* * Replace `{{` and `}}` in a string (usually a template) with the actual start-/endSymbols used * for interpolation. This allows pre-defined templates (for components such as dialog, toast etc) * to continue to work in apps that use custom interpolation start-/endSymbols. * * @param {string} text The text in which to replace `{{` / `}}` * @returns {string} The modified string using the actual interpolation start-/endSymbols */ function replaceInterpolationSymbols(text) { if (!text || !angular.isString(text)) return text; return text.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol); } } } })(); (function() { 'use strict'; /** * @ngdoc module * @name material.core.componentRegistry * * @description * A component instance registration service. * Note: currently this as a private service in the SideNav component. */ angular.module('material.core') .factory('$mdComponentRegistry', ComponentRegistry); /* * @private * @ngdoc factory * @name ComponentRegistry * @module material.core.componentRegistry * */ function ComponentRegistry($log, $q) { var self; var instances = [ ]; var pendings = { }; return self = { /** * 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) { if ( !isValidID(handle) ) return null; 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) { if ( !handle ) return angular.noop; instance.$$mdHandle = handle; instances.push(instance); resolveWhen(); return deregister; /** * Remove registration for an instance */ function deregister() { var index = instances.indexOf(instance); if (index !== -1) { instances.splice(index, 1); } } /** * Resolve any pending promises for this instance */ function resolveWhen() { var dfd = pendings[handle]; if ( dfd ) { dfd.resolve( instance ); delete pendings[handle]; } } }, /** * Async accessor to registered component instance * If not available then a promise is created to notify * all listeners when the instance is registered. */ when : function(handle) { if ( isValidID(handle) ) { var deferred = $q.defer(); var instance = self.get(handle); if ( instance ) { deferred.resolve( instance ); } else { pendings[handle] = deferred; } return deferred.promise; } return $q.reject("Invalid `md-component-id` value."); } }; function isValidID(handle){ return handle && (handle !== ""); } } ComponentRegistry.$inject = ["$log", "$q"]; })(); (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)'); // expose onInput for ripple testing scope._onInput = onInput; 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.replace(/\d?\.?\d*\s*\)\s*$/, '0.1)'); 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 if (elem) { 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) { color = parseColor(element.attr('md-ink-ripple')) || parseColor($window.getComputedStyle(options.colorElement[0]).color || 'rgb(0, 0, 0)'); 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('
'); 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 = angular.element('
'); rippleContainer = 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; var grandparent = parent && parent.parentNode; var ancestor = grandparent && grandparent.parentNode; return !isDisabled(node) && !isDisabled(parent) && !isDisabled(grandparent) && !isDisabled(ancestor); function isDisabled (elem) { return elem && elem.hasAttribute && elem.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` parent attribute. * * @usage * * * * * * * * * 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!"); * } * } * }; * }); * */ function attrNoDirective() { return function() { return { controller: angular.noop }; }; } })(); (function() { 'use strict'; angular.module('material.core.theming.palette', []) .constant('$mdColorPalette', { 'red': { '50': '#ffebee', '100': '#ffcdd2', '200': '#ef9a9a', '300': '#e57373', '400': '#ef5350', '500': '#f44336', '600': '#e53935', '700': '#d32f2f', '800': '#c62828', '900': '#b71c1c', 'A100': '#ff8a80', 'A200': '#ff5252', 'A400': '#ff1744', 'A700': '#d50000', 'contrastDefaultColor': 'light', 'contrastDarkColors': '50 100 200 300 400 A100' }, 'pink': { '50': '#fce4ec', '100': '#f8bbd0', '200': '#f48fb1', '300': '#f06292', '400': '#ec407a', '500': '#e91e63', '600': '#d81b60', '700': '#c2185b', '800': '#ad1457', '900': '#880e4f', 'A100': '#ff80ab', 'A200': '#ff4081', 'A400': '#f50057', 'A700': '#c51162', 'contrastDefaultColor': 'light', 'contrastDarkColors': '50 100 200 300 400 A100' }, 'purple': { '50': '#f3e5f5', '100': '#e1bee7', '200': '#ce93d8', '300': '#ba68c8', '400': '#ab47bc', '500': '#9c27b0', '600': '#8e24aa', '700': '#7b1fa2', '800': '#6a1b9a', '900': '#4a148c', 'A100': '#ea80fc', 'A200': '#e040fb', 'A400': '#d500f9', 'A700': '#aa00ff', 'contrastDefaultColor': 'light', 'contrastDarkColors': '50 100 200 A100' }, 'deep-purple': { '50': '#ede7f6', '100': '#d1c4e9', '200': '#b39ddb', '300': '#9575cd', '400': '#7e57c2', '500': '#673ab7', '600': '#5e35b1', '700': '#512da8', '800': '#4527a0', '900': '#311b92', 'A100': '#b388ff', 'A200': '#7c4dff', 'A400': '#651fff', 'A700': '#6200ea', 'contrastDefaultColor': 'light', 'contrastDarkColors': '50 100 200 A100' }, 'indigo': { '50': '#e8eaf6', '100': '#c5cae9', '200': '#9fa8da', '300': '#7986cb', '400': '#5c6bc0', '500': '#3f51b5', '600': '#3949ab', '700': '#303f9f', '800': '#283593', '900': '#1a237e', 'A100': '#8c9eff', 'A200': '#536dfe', 'A400': '#3d5afe', 'A700': '#304ffe', 'contrastDefaultColor': 'light', 'contrastDarkColors': '50 100 200 A100' }, 'blue': { '50': '#e3f2fd', '100': '#bbdefb', '200': '#90caf9', '300': '#64b5f6', '400': '#42a5f5', '500': '#2196f3', '600': '#1e88e5', '700': '#1976d2', '800': '#1565c0', '900': '#0d47a1', 'A100': '#82b1ff', 'A200': '#448aff', 'A400': '#2979ff', 'A700': '#2962ff', 'contrastDefaultColor': 'light', 'contrastDarkColors': '100 200 300 400 A100' }, 'light-blue': { '50': '#e1f5fe', '100': '#b3e5fc', '200': '#81d4fa', '300': '#4fc3f7', '400': '#29b6f6', '500': '#03a9f4', '600': '#039be5', '700': '#0288d1', '800': '#0277bd', '900': '#01579b', 'A100': '#80d8ff', 'A200': '#40c4ff', 'A400': '#00b0ff', 'A700': '#0091ea', 'contrastDefaultColor': 'dark', 'contrastLightColors': '500 600 700 800 900 A700' }, 'cyan': { '50': '#e0f7fa', '100': '#b2ebf2', '200': '#80deea', '300': '#4dd0e1', '400': '#26c6da', '500': '#00bcd4', '600': '#00acc1', '700': '#0097a7', '800': '#00838f', '900': '#006064', 'A100': '#84ffff', 'A200': '#18ffff', 'A400': '#00e5ff', 'A700': '#00b8d4', 'contrastDefaultColor': 'dark', 'contrastLightColors': '500 600 700 800 900' }, 'teal': { '50': '#e0f2f1', '100': '#b2dfdb', '200': '#80cbc4', '300': '#4db6ac', '400': '#26a69a', '500': '#009688', '600': '#00897b', '700': '#00796b', '800': '#00695c', '900': '#004d40', 'A100': '#a7ffeb', 'A200': '#64ffda', 'A400': '#1de9b6', 'A700': '#00bfa5', 'contrastDefaultColor': 'dark', 'contrastLightColors': '500 600 700 800 900' }, 'green': { '50': '#e8f5e9', '100': '#c8e6c9', '200': '#a5d6a7', '300': '#81c784', '400': '#66bb6a', '500': '#4caf50', '600': '#43a047', '700': '#388e3c', '800': '#2e7d32', '900': '#1b5e20', 'A100': '#b9f6ca', 'A200': '#69f0ae', 'A400': '#00e676', 'A700': '#00c853', 'contrastDefaultColor': 'dark', 'contrastLightColors': '500 600 700 800 900' }, 'light-green': { '50': '#f1f8e9', '100': '#dcedc8', '200': '#c5e1a5', '300': '#aed581', '400': '#9ccc65', '500': '#8bc34a', '600': '#7cb342', '700': '#689f38', '800': '#558b2f', '900': '#33691e', 'A100': '#ccff90', 'A200': '#b2ff59', 'A400': '#76ff03', 'A700': '#64dd17', 'contrastDefaultColor': 'dark', 'contrastLightColors': '800 900' }, 'lime': { '50': '#f9fbe7', '100': '#f0f4c3', '200': '#e6ee9c', '300': '#dce775', '400': '#d4e157', '500': '#cddc39', '600': '#c0ca33', '700': '#afb42b', '800': '#9e9d24', '900': '#827717', 'A100': '#f4ff81', 'A200': '#eeff41', 'A400': '#c6ff00', 'A700': '#aeea00', 'contrastDefaultColor': 'dark', 'contrastLightColors': '900' }, 'yellow': { '50': '#fffde7', '100': '#fff9c4', '200': '#fff59d', '300': '#fff176', '400': '#ffee58', '500': '#ffeb3b', '600': '#fdd835', '700': '#fbc02d', '800': '#f9a825', '900': '#f57f17', 'A100': '#ffff8d', 'A200': '#ffff00', 'A400': '#ffea00', 'A700': '#ffd600', 'contrastDefaultColor': 'dark' }, 'amber': { '50': '#fff8e1', '100': '#ffecb3', '200': '#ffe082', '300': '#ffd54f', '400': '#ffca28', '500': '#ffc107', '600': '#ffb300', '700': '#ffa000', '800': '#ff8f00', '900': '#ff6f00', 'A100': '#ffe57f', 'A200': '#ffd740', 'A400': '#ffc400', 'A700': '#ffab00', 'contrastDefaultColor': 'dark' }, 'orange': { '50': '#fff3e0', '100': '#ffe0b2', '200': '#ffcc80', '300': '#ffb74d', '400': '#ffa726', '500': '#ff9800', '600': '#fb8c00', '700': '#f57c00', '800': '#ef6c00', '900': '#e65100', 'A100': '#ffd180', 'A200': '#ffab40', 'A400': '#ff9100', 'A700': '#ff6d00', 'contrastDefaultColor': 'dark', 'contrastLightColors': '800 900' }, 'deep-orange': { '50': '#fbe9e7', '100': '#ffccbc', '200': '#ffab91', '300': '#ff8a65', '400': '#ff7043', '500': '#ff5722', '600': '#f4511e', '700': '#e64a19', '800': '#d84315', '900': '#bf360c', 'A100': '#ff9e80', 'A200': '#ff6e40', 'A400': '#ff3d00', 'A700': '#dd2c00', 'contrastDefaultColor': 'light', 'contrastDarkColors': '50 100 200 300 400 A100 A200' }, 'brown': { '50': '#efebe9', '100': '#d7ccc8', '200': '#bcaaa4', '300': '#a1887f', '400': '#8d6e63', '500': '#795548', '600': '#6d4c41', '700': '#5d4037', '800': '#4e342e', '900': '#3e2723', 'A100': '#d7ccc8', 'A200': '#bcaaa4', 'A400': '#8d6e63', 'A700': '#5d4037', 'contrastDefaultColor': 'light', 'contrastDarkColors': '50 100 200' }, 'grey': { '0': '#ffffff', '50': '#fafafa', '100': '#f5f5f5', '200': '#eeeeee', '300': '#e0e0e0', '400': '#bdbdbd', '500': '#9e9e9e', '600': '#757575', '700': '#616161', '800': '#424242', '900': '#212121', '1000': '#000000', 'A100': '#ffffff', 'A200': '#eeeeee', 'A400': '#bdbdbd', 'A700': '#616161', 'contrastDefaultColor': 'dark', 'contrastLightColors': '600 700 800 900' }, 'blue-grey': { '50': '#eceff1', '100': '#cfd8dc', '200': '#b0bec5', '300': '#90a4ae', '400': '#78909c', '500': '#607d8b', '600': '#546e7a', '700': '#455a64', '800': '#37474f', '900': '#263238', 'A100': '#cfd8dc', 'A200': '#b0bec5', 'A400': '#78909c', 'A700': '#455a64', 'contrastDefaultColor': 'light', 'contrastDarkColors': '50 100 200 300' } }); })(); (function() { 'use strict'; angular.module('material.core.theming', ['material.core.theming.palette']) .directive('mdTheme', ThemingDirective) .directive('mdThemable', ThemableDirective) .provider('$mdTheming', ThemingProvider) .run(generateThemes); /** * @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. */ // In memory storage of defined themes and color palettes (both loaded by CSS, and user specified) var PALETTES; var THEMES; var themingProvider; var generationIsDone; var DARK_FOREGROUND = { name: 'dark', '1': 'rgba(0,0,0,0.87)', '2': 'rgba(0,0,0,0.54)', '3': 'rgba(0,0,0,0.26)', '4': 'rgba(0,0,0,0.12)' }; var LIGHT_FOREGROUND = { name: 'light', '1': 'rgba(255,255,255,1.0)', '2': 'rgba(255,255,255,0.7)', '3': 'rgba(255,255,255,0.3)', '4': 'rgba(255,255,255,0.12)' }; var DARK_SHADOW = '1px 1px 0px rgba(0,0,0,0.4), -1px -1px 0px rgba(0,0,0,0.4)'; var LIGHT_SHADOW = ''; var DARK_CONTRAST_COLOR = colorToRgbaArray('rgba(0,0,0,0.87)'); var LIGHT_CONTRAST_COLOR = colorToRgbaArray('rgb(255,255,255)'); var THEME_COLOR_TYPES = ['primary', 'accent', 'warn', 'background']; var DEFAULT_COLOR_TYPE = 'primary'; // A color in a theme will use these hues by default, if not specified by user. var LIGHT_DEFAULT_HUES = { 'accent': { 'default': 'A700', 'hue-1': 'A200', 'hue-2': 'A400', 'hue-3': 'A100' } }; var DARK_DEFAULT_HUES = { 'background': { 'default': '500', 'hue-1': '300', 'hue-2': '600', 'hue-3': '800' } }; THEME_COLOR_TYPES.forEach(function(colorType) { // Color types with unspecified default hues will use these default hue values var defaultDefaultHues = { 'default': '500', 'hue-1': '300', 'hue-2': '800', 'hue-3': 'A100' }; if (!LIGHT_DEFAULT_HUES[colorType]) LIGHT_DEFAULT_HUES[colorType] = defaultDefaultHues; if (!DARK_DEFAULT_HUES[colorType]) DARK_DEFAULT_HUES[colorType] = defaultDefaultHues; }); var VALID_HUE_VALUES = [ '50', '100', '200', '300', '400', '500', '600', '700', '800', '900', 'A100', 'A200', 'A400', 'A700' ]; function ThemingProvider($mdColorPalette) { PALETTES = {}; THEMES = {}; var defaultTheme = 'default'; var alwaysWatchTheme = false; // Load JS Defined Palettes angular.extend(PALETTES, $mdColorPalette); // Default theme defined in core.js ThemingService.$inject = ["$rootScope"]; return themingProvider = { definePalette: definePalette, extendPalette: extendPalette, theme: registerTheme, setDefaultTheme: function(theme) { defaultTheme = theme; }, alwaysWatchTheme: function(alwaysWatch) { alwaysWatchTheme = alwaysWatch; }, $get: ThemingService, _LIGHT_DEFAULT_HUES: LIGHT_DEFAULT_HUES, _DARK_DEFAULT_HUES: DARK_DEFAULT_HUES, _PALETTES: PALETTES, _THEMES: THEMES, _parseRules: parseRules, _rgba: rgba }; // Example: $mdThemingProvider.definePalette('neonRed', { 50: '#f5fafa', ... }); function definePalette(name, map) { map = map || {}; PALETTES[name] = checkPaletteValid(name, map); return themingProvider; } // Returns an new object which is a copy of a given palette `name` with variables from // `map` overwritten // Example: var neonRedMap = $mdThemingProvider.extendPalette('red', { 50: '#f5fafafa' }); function extendPalette(name, map) { return checkPaletteValid(name, angular.extend({}, PALETTES[name] || {}, map) ); } // Make sure that palette has all required hues function checkPaletteValid(name, map) { var missingColors = VALID_HUE_VALUES.filter(function(field) { return !map[field]; }); if (missingColors.length) { throw new Error("Missing colors %1 in palette %2!" .replace('%1', missingColors.join(', ')) .replace('%2', name)); } return map; } // Register a theme (which is a collection of color palettes to use with various states // ie. warn, accent, primary ) // Optionally inherit from an existing theme // $mdThemingProvider.theme('custom-theme').primaryColor('red'); function registerTheme(name, inheritFrom) { inheritFrom = inheritFrom || 'default'; if (THEMES[name]) return THEMES[name]; var parentTheme = typeof inheritFrom === 'string' ? THEMES[inheritFrom] : inheritFrom; var theme = new Theme(name); if (parentTheme) { angular.forEach(parentTheme.colors, function(color, colorType) { theme.colors[colorType] = { name: color.name, // Make sure a COPY of the hues is given to the child color, // not the same reference. hues: angular.extend({}, color.hues) }; }); } THEMES[name] = theme; return theme; } function Theme(name) { var self = this; self.name = name; self.colors = {}; self.dark = setDark; setDark(false); function setDark(isDark) { isDark = arguments.length === 0 ? true : !!isDark; // If no change, abort if (isDark === self.isDark) return; self.isDark = isDark; self.foregroundPalette = self.isDark ? LIGHT_FOREGROUND : DARK_FOREGROUND; self.foregroundShadow = self.isDark ? DARK_SHADOW : LIGHT_SHADOW; // Light and dark themes have different default hues. // Go through each existing color type for this theme, and for every // hue value that is still the default hue value from the previous light/dark setting, // set it to the default hue value from the new light/dark setting. var newDefaultHues = self.isDark ? DARK_DEFAULT_HUES : LIGHT_DEFAULT_HUES; var oldDefaultHues = self.isDark ? LIGHT_DEFAULT_HUES : DARK_DEFAULT_HUES; angular.forEach(newDefaultHues, function(newDefaults, colorType) { var color = self.colors[colorType]; var oldDefaults = oldDefaultHues[colorType]; if (color) { for (var hueName in color.hues) { if (color.hues[hueName] === oldDefaults[hueName]) { color.hues[hueName] = newDefaults[hueName]; } } } }); return self; } THEME_COLOR_TYPES.forEach(function(colorType) { var defaultHues = (self.isDark ? DARK_DEFAULT_HUES : LIGHT_DEFAULT_HUES)[colorType]; self[colorType + 'Color'] = function setColorType(paletteName, hues) { var color = self.colors[colorType] = { name: paletteName, hues: angular.extend({}, defaultHues, hues) }; Object.keys(color.hues).forEach(function(name) { if (!defaultHues[name]) { throw new Error("Invalid hue name '%1' in theme %2's %3 color %4. Available hue names: %4" .replace('%1', name) .replace('%2', self.name) .replace('%3', paletteName) .replace('%4', Object.keys(defaultHues).join(', ')) ); } }); Object.keys(color.hues).map(function(key) { return color.hues[key]; }).forEach(function(hueValue) { if (VALID_HUE_VALUES.indexOf(hueValue) == -1) { throw new Error("Invalid hue value '%1' in theme %2's %3 color %4. Available hue values: %5" .replace('%1', hueValue) .replace('%2', self.name) .replace('%3', colorType) .replace('%4', paletteName) .replace('%5', VALID_HUE_VALUES.join(', ')) ); } }); return self; }; }); } /** * @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 */ /* @ngInject */ 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); } } } ThemingProvider.$inject = ["$mdColorPalette"]; 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 parseRules(theme, colorType, rules) { checkValidPalette(theme, colorType); rules = rules.replace(/THEME_NAME/g, theme.name); var generatedRules = []; var color = theme.colors[colorType]; var themeNameRegex = new RegExp('.md-' + theme.name + '-theme', 'g'); // Matches '{{ primary-color }}', etc var hueRegex = new RegExp('(\'|")?{{\\s*(' + colorType + ')-(color|contrast)-?(\\d\\.?\\d*)?\\s*}}(\"|\')?','g'); var simpleVariableRegex = /'?"?\{\{\s*([a-zA-Z]+)-(A?\d+|hue\-[0-3]|shadow)-?(\d\.?\d*)?\s*\}\}'?"?/g; var palette = PALETTES[color.name]; // find and replace simple variables where we use a specific hue, not angentire palette // eg. "{{primary-100}}" //\(' + THEME_COLOR_TYPES.join('\|') + '\)' rules = rules.replace(simpleVariableRegex, function(match, colorType, hue, opacity) { if (colorType === 'foreground') { if (hue == 'shadow') { return theme.foregroundShadow; } else { return theme.foregroundPalette[hue] || theme.foregroundPalette['1']; } } if (hue.indexOf('hue') === 0) { hue = theme.colors[colorType].hues[hue]; } return rgba( (PALETTES[ theme.colors[colorType].name ][hue] || '').value, opacity ); }); // For each type, generate rules for each hue (ie. default, md-hue-1, md-hue-2, md-hue-3) angular.forEach(color.hues, function(hueValue, hueName) { var newRule = rules .replace(hueRegex, function(match, _, colorType, hueType, opacity) { return rgba(palette[hueValue][hueType === 'color' ? 'value' : 'contrast'], opacity); }); if (hueName !== 'default') { newRule = newRule.replace(themeNameRegex, '.md-' + theme.name + '-theme.md-' + hueName); } generatedRules.push(newRule); }); return generatedRules.join(''); } // Generate our themes at run time given the state of THEMES and PALETTES function generateThemes($injector) { var themeCss = $injector.has('$MD_THEME_CSS') ? $injector.get('$MD_THEME_CSS') : ''; // MD_THEME_CSS is a string generated by the build process that includes all the themable // components as templates // Expose contrast colors for palettes to ensure that text is always readable angular.forEach(PALETTES, sanitizePalette); // Break the CSS into individual rules var rules = themeCss.split(/\}(?!(\}|'|"|;))/) .filter(function(rule) { return rule && rule.length; }) .map(function(rule) { return rule.trim() + '}'; }); var rulesByType = {}; THEME_COLOR_TYPES.forEach(function(type) { rulesByType[type] = ''; }); var ruleMatchRegex = new RegExp('md-(' + THEME_COLOR_TYPES.join('|') + ')', 'g'); // Sort the rules based on type, allowing us to do color substitution on a per-type basis rules.forEach(function(rule) { var match = rule.match(ruleMatchRegex); // First: test that if the rule has '.md-accent', it goes into the accent set of rules for (var i = 0, type; type = THEME_COLOR_TYPES[i]; i++) { if (rule.indexOf('.md-' + type) > -1) { return rulesByType[type] += rule; } } // If no eg 'md-accent' class is found, try to just find 'accent' in the rule and guess from // there for (i = 0; type = THEME_COLOR_TYPES[i]; i++) { if (rule.indexOf(type) > -1) { return rulesByType[type] += rule; } } // Default to the primary array return rulesByType[DEFAULT_COLOR_TYPE] += rule; }); var styleString = ''; // For each theme, use the color palettes specified for `primary`, `warn` and `accent` // to generate CSS rules. angular.forEach(THEMES, function(theme) { THEME_COLOR_TYPES.forEach(function(colorType) { styleString += parseRules(theme, colorType, rulesByType[colorType] + ''); }); }); // Insert our newly minted styles into the DOM if (!generationIsDone) { var style = document.createElement('style'); style.innerHTML = styleString; var head = document.getElementsByTagName('head')[0]; head.insertBefore(style, head.firstElementChild); generationIsDone = true; } // The user specifies a 'default' contrast color as either light or dark, // then explicitly lists which hues are the opposite contrast (eg. A100 has dark, A200 has light) function sanitizePalette(palette) { var defaultContrast = palette.contrastDefaultColor; var lightColors = palette.contrastLightColors || []; var darkColors = palette.contrastDarkColors || []; // Sass provides these colors as space-separated lists if (typeof lightColors === 'string') lightColors = lightColors.split(' '); if (typeof darkColors === 'string') darkColors = darkColors.split(' '); // Cleanup after ourselves delete palette.contrastDefaultColor; delete palette.contrastLightColors; delete palette.contrastDarkColors; // Change { 'A100': '#fffeee' } to { 'A100': { value: '#fffeee', contrast:DARK_CONTRAST_COLOR } angular.forEach(palette, function(hueValue, hueName) { if (angular.isObject(hueValue)) return; // Already converted // Map everything to rgb colors var rgbValue = colorToRgbaArray(hueValue); if (!rgbValue) { throw new Error("Color %1, in palette %2's hue %3, is invalid. Hex or rgb(a) color expected." .replace('%1', hueValue) .replace('%2', palette.name) .replace('%3', hueName)); } palette[hueName] = { value: rgbValue, contrast: getContrastColor() }; function getContrastColor() { if (defaultContrast === 'light') { return darkColors.indexOf(hueName) > -1 ? DARK_CONTRAST_COLOR : LIGHT_CONTRAST_COLOR; } else { return lightColors.indexOf(hueName) > -1 ? LIGHT_CONTRAST_COLOR : DARK_CONTRAST_COLOR; } } }); } } generateThemes.$inject = ["$injector"]; function checkValidPalette(theme, colorType) { // If theme attempts to use a palette that doesnt exist, throw error if (!PALETTES[ (theme.colors[colorType] || {}).name ]) { throw new Error( "You supplied an invalid color palette for theme %1's %2 palette. Available palettes: %3" .replace('%1', theme.name) .replace('%2', colorType) .replace('%3', Object.keys(PALETTES).join(', ')) ); } } function colorToRgbaArray(clr) { if (angular.isArray(clr) && clr.length == 3) return clr; if (/^rgb/.test(clr)) { return clr.replace(/(^\s*rgba?\(|\)\s*$)/g, '').split(',').map(function(value) { return parseInt(value, 10); }); } if (clr.charAt(0) == '#') clr = clr.substring(1); if (!/^([a-fA-F0-9]{3}){1,2}$/g.test(clr)) return; var dig = clr.length / 3; var red = clr.substr(0, dig); var grn = clr.substr(dig, dig); var blu = clr.substr(dig * 2); if (dig === 1) { red += red; grn += grn; blu += blu; } return [parseInt(red, 16), parseInt(grn, 16), parseInt(blu, 16)]; } function rgba(rgbArray, opacity) { if (rgbArray.length == 4) opacity = rgbArray.pop(); return opacity && opacity.length ? 'rgba(' + rgbArray.join(',') + ',' + opacity + ')' : 'rgb(' + rgbArray.join(',') + ')'; } })();