/*! * Angular Material Design * https://github.com/angular/material * @license MIT * v0.9.8 */ (function( window, angular, undefined ){ "use strict"; (function(){ "use strict"; angular.module('ngMaterial', ["ng","ngAnimate","ngAria","material.core","material.core.gestures","material.core.theming.palette","material.core.theming","material.components.autocomplete","material.components.backdrop","material.components.bottomSheet","material.components.button","material.components.card","material.components.checkbox","material.components.chips","material.components.content","material.components.dialog","material.components.divider","material.components.gridList","material.components.icon","material.components.input","material.components.list","material.components.progressCircular","material.components.progressLinear","material.components.radioButton","material.components.select","material.components.sidenav","material.components.slider","material.components.sticky","material.components.subheader","material.components.swipe","material.components.switch","material.components.tabs","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', [ 'material.core.gestures', 'material.core.theming' ]) .config( MdCoreConfigure ); function MdCoreConfigure($provide, $mdThemingProvider) { $provide.decorator('$$rAF', ["$delegate", rAFDecorator]); $mdThemingProvider.theme('default') .primaryPalette('indigo') .accentPalette('pink') .warnPalette('red') .backgroundPalette('grey'); } MdCoreConfigure.$inject = ["$provide", "$mdThemingProvider"]; function rAFDecorator( $delegate ) { /** * Use this to throttle events that come in often. * The throttled 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-throttled 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 */ $delegate.throttle = function(cb) { var queueArgs, alreadyQueued, queueCb, context; return function debounced() { queueArgs = arguments; context = this; queueCb = cb; if (!alreadyQueued) { alreadyQueued = true; $delegate(function() { queueCb.apply(context, queueArgs); alreadyQueued = false; }); } }; }; return $delegate; } })(); (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, TAB : 9, BACKSPACE: 8, DELETE: 46 }, CSS: { /* Constants */ TRANSITIONEND: 'transitionend' + (webkit ? ' webkitTransitionEnd' : ''), ANIMATIONEND: 'animationend' + (webkit ? ' webkitAnimationEnd' : ''), TRANSFORM: vendorProperty('transform'), TRANSFORM_ORIGIN: vendorProperty('transformOrigin'), 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)' }, MEDIA_PRIORITY: [ 'gt-lg', 'lg', 'gt-md', 'md', 'gt-sm', 'sm' ] }; } MdConstantFactory.$inject = ["$$rAF", "$sniffer"]; })(); (function(){ "use strict"; 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 = MdIterator; 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 MdIterator(items, reloop) { var trueFn = function() { return true; }; if (items && !angular.isArray(items)) { items = Array.prototype.slice.call(items); } 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 to determine whether the next item * is valid. If not valid, it will try to find the next item again. * * @param {boolean} backwards Specifies the direction of searching (forwards/backwards) * @param {*} item The item whose subsequent item we are looking for * @param {Function=} validate The `validate` function * @param {integer=} limit The recursion limit * * @returns {*} The subsequent item or null */ function findSubsequentItem(backwards, item, validate, limit) { validate = validate || trueFn; var curIndex = indexOf(item); while (true) { 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 (validate(foundItem)) return foundItem; if (angular.isUndefined(limit)) limit = nextIndex; curIndex = nextIndex; } } } })(); (function(){ "use strict"; angular.module('material.core') .factory('$mdMedia', mdMediaFactory); /** * @ngdoc service * @name $mdMedia * @module material.core * * @description * `$mdMedia` is used to evaluate whether a given media query is true or false given the * current device's screen / window size. The media query will be re-evaluated on resize, allowing * you to register a watch. * * `$mdMedia` also has pre-programmed support for media queries that match the layout breakpoints. * (`sm`, `gt-sm`, `md`, `gt-md`, `lg`, `gt-lg`). * * @returns {boolean} a boolean representing whether or not the given media query is true or false. * * @usage * * app.controller('MyController', function($mdMedia, $scope) { * $scope.$watch(function() { return $mdMedia('lg'); }, function(big) { * $scope.bigScreen = big; * }); * * $scope.screenIsSmall = $mdMedia('sm'); * $scope.customQuery = $mdMedia('(min-width: 1234px)'); * $scope.anotherCustom = $mdMedia('max-width: 300px'); * }); * */ function mdMediaFactory($mdConstant, $rootScope, $window) { var queries = {}; var mqls = {}; var results = {}; var normalizeCache = {}; $mdMedia.getResponsiveAttribute = getResponsiveAttribute; $mdMedia.getQuery = getQuery; $mdMedia.watchResponsiveAttributes = watchResponsiveAttributes; return $mdMedia; function $mdMedia(query) { var validated = queries[query]; if (angular.isUndefined(validated)) { validated = queries[query] = validate(query); } var result = results[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) { var result = mqls[query] = $window.matchMedia(query); result.addListener(onQueryChange); return (results[result.media] = !!result.matches); } function onQueryChange(query) { $rootScope.$evalAsync(function() { results[query.media] = !!query.matches; }); } function getQuery(name) { return mqls[name]; } function getResponsiveAttribute(attrs, attrName) { for (var i = 0; i < $mdConstant.MEDIA_PRIORITY.length; i++) { var mediaName = $mdConstant.MEDIA_PRIORITY[i]; if (!mqls[queries[mediaName]].matches) { continue; } var normalizedName = getNormalizedName(attrs, attrName + '-' + mediaName); if (attrs[normalizedName]) { return attrs[normalizedName]; } } // fallback on unprefixed return attrs[getNormalizedName(attrs, attrName)]; } function watchResponsiveAttributes(attrNames, attrs, watchFn) { var unwatchFns = []; attrNames.forEach(function(attrName) { var normalizedName = getNormalizedName(attrs, attrName); if (attrs[normalizedName]) { unwatchFns.push( attrs.$observe(normalizedName, angular.bind(void 0, watchFn, null))); } for (var mediaName in $mdConstant.MEDIA) { normalizedName = getNormalizedName(attrs, attrName + '-' + mediaName); if (!attrs[normalizedName]) { return; } unwatchFns.push(attrs.$observe(normalizedName, angular.bind(void 0, watchFn, mediaName))); } }); return function unwatch() { unwatchFns.forEach(function(fn) { fn(); }) }; } // Improves performance dramatically function getNormalizedName(attrs, attrName) { return normalizeCache[attrName] || (normalizeCache[attrName] = attrs.$normalize(attrName)); } } mdMediaFactory.$inject = ["$mdConstant", "$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; angular.module('material.core') .factory('$mdUtil', ["$cacheFactory", "$document", "$timeout", "$q", "$window", "$mdConstant", function($cacheFactory, $document, $timeout, $q, $window, $mdConstant) { var Util; function getNode(el) { return el[0] || el; } return Util = { now: window.performance ? angular.bind(window.performance, window.performance.now) : Date.now, clientRect: function(element, offsetParent, isOffsetRect) { var node = getNode(element); offsetParent = getNode(offsetParent || node.offsetParent || document.body); var nodeRect = node.getBoundingClientRect(); // The user can ask for an offsetRect: a rect relative to the offsetParent, // or a clientRect: a rect relative to the page var offsetRect = isOffsetRect ? offsetParent.getBoundingClientRect() : { left: 0, top: 0, width: 0, height: 0 }; return { left: nodeRect.left - offsetRect.left, top: nodeRect.top - offsetRect.top, width: nodeRect.width, height: nodeRect.height }; }, offsetRect: function(element, offsetParent) { return Util.clientRect(element, offsetParent, true); }, // Disables scroll around the passed element. Goes up the DOM to find a // disableTarget (a md-content that is scrolling, or the body as a fallback) // and uses CSS/JS to prevent it from scrolling disableScrollAround: function(element) { element = element instanceof angular.element ? element[0] : element; var parentEl = element; var disableTarget; // Find the highest level scrolling md-content while (parentEl = this.getClosest(parentEl, 'MD-CONTENT', true)) { if (isScrolling(parentEl)) { disableTarget = angular.element(parentEl)[0]; } } // Default to the body if no scrolling md-content if (!disableTarget) { disableTarget = $document[0].body; if (!isScrolling(disableTarget)) return angular.noop; } if (disableTarget.nodeName == 'BODY') { return disableBodyScroll(); } else { return disableElementScroll(); } // Creates a virtual scrolling mask to absorb touchmove, keyboard, scrollbar clicking, and wheel events function disableElementScroll() { var scrollMask = angular.element('
'); var computedStyle = $window.getComputedStyle(disableTarget); var disableRect = disableTarget.getBoundingClientRect(); var scrollWidth = disableRect.width - disableTarget.clientWidth; applyStyles(scrollMask[0], { zIndex: computedStyle.zIndex == 'auto' ? 2 : computedStyle.zIndex + 1, width: disableRect.width + 'px', height: disableRect.height + 'px', top: disableRect.top + 'px', left: disableRect.left + 'px' }); scrollMask[0].firstElementChild.style.width = scrollWidth + 'px'; $document[0].body.appendChild(scrollMask[0]); scrollMask.on('wheel', preventDefault); scrollMask.on('touchmove', preventDefault); $document.on('keydown', disableKeyNav); return function restoreScroll() { scrollMask.off('wheel'); scrollMask.off('touchmove'); scrollMask[0].parentNode.removeChild(scrollMask[0]); $document.off('keydown', disableKeyNav); }; // Prevent keypresses from elements inside the disableTarget // used to stop the keypresses that could cause the page to scroll // (arrow keys, spacebar, tab, etc). function disableKeyNav(e) { if (disableTarget.contains(e.target)) { e.preventDefault(); e.stopImmediatePropagation(); } } function preventDefault(e) { e.preventDefault(); } } // Converts the disableTarget (body) to a position fixed block and translate it to the propper scroll position function disableBodyScroll() { var restoreStyle = disableTarget.getAttribute('style') || ''; var scrollOffset = disableTarget.scrollTop; applyStyles(disableTarget, { position: 'fixed', width: '100%', overflowY: 'scroll', top: -scrollOffset + 'px' }); return function restoreScroll() { disableTarget.setAttribute('style', restoreStyle); disableTarget.scrollTop = scrollOffset; }; } function applyStyles (el, styles) { for (var key in styles) { el.style[key] = styles[key]; } } function isScrolling(el) { if (el instanceof angular.element) el = el[0]; return el.scrollHeight > el.offsetHeight; } }, floatingScrollbars: function() { if (this.floatingScrollbars.cached === undefined) { var tempNode = angular.element('
'); $document[0].body.appendChild(tempNode[0]); this.floatingScrollbars.cached = (tempNode[0].offsetWidth == tempNode[0].childNodes[0].offsetWidth); tempNode.remove(); } return this.floatingScrollbars.cached; }, // Mobile safari only allows you to set focus in click event listeners... forceFocus: function(element) { var node = element[0] || element; document.addEventListener('click', function focusOnClick(ev) { if (ev.target === node && ev.$focus) { node.focus(); ev.stopImmediatePropagation(); ev.preventDefault(); node.removeEventListener('click', focusOnClick); } }, true); var newEvent = document.createEvent('MouseEvents'); newEvent.initMouseEvent('click', false, true, window, {}, 0, 0, 0, 0, false, false, false, false, 0, null); newEvent.$material = true; newEvent.$focus = true; node.dispatchEvent(newEvent); }, transitionEndPromise: function(element, opts) { opts = opts || {}; var deferred = $q.defer(); element.on($mdConstant.CSS.TRANSITIONEND, finished); function finished(ev) { // Make sure this transitionend didn't bubble up from a child if (!ev || ev.target === element[0]) { element.off($mdConstant.CSS.TRANSITIONEND, finished); deferred.resolve(); } } if (opts.timeout) $timeout(finished, opts.timeout); return deferred.promise; }, fakeNgModel: function() { return { $fake: true, $setTouched: angular.noop, $setViewValue: function(value) { this.$viewValue = value; this.$render(value); this.$viewChangeListeners.forEach(function(cb) { cb(); }); }, $isEmpty: function(value) { return ('' + value).length === 0; }, $parsers: [], $formatters: [], $viewChangeListeners: [], $render: angular.noop }; }, // 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; } }; }, /** * Measures the number of milliseconds taken to run the provided callback * function. Uses a high-precision timer if available. */ time: function time(cb) { var start = Util.now(); cb(); return Util.now() - start; }, /** * Get a unique ID. * * @returns {string} an unique numeric string */ nextUid: function() { return '' + nextUniqueId++; }, // 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, onlyParent) { if (el instanceof angular.element) el = el[0]; tagName = tagName.toUpperCase(); if (onlyParent) el = el.parentNode; if (!el) return null; do { if (el.nodeName === tagName) { return el; } } while (el = el.parentNode); return null; }, /** * Functional equivalent for $element.filter(‘md-bottom-sheet’) * useful with interimElements where the element and its container are important... */ extractElementByName: function (element, nodeName) { for (var i = 0, len = element.length; i < len; i++) { if (element[i].nodeName.toLowerCase() === nodeName){ return angular.element(element[i]); } } return element; }, /** * Give optional properties with no value a boolean true by default */ initOptionalProperties: function (scope, attr, defaults ) { defaults = defaults || { }; angular.forEach(scope.$$isolateBindings, function (binding, key) { if (binding.optional && angular.isUndefined(scope[key])) { var hasKey = attr.hasOwnProperty(attr.$normalize(binding.attrName)); scope[key] = angular.isDefined(defaults[key]) ? defaults[key] : hasKey; } }); } }; }]); /* * 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); /* * @ngInject */ 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] || element; // if node exists and neither it nor its children have the attribute if (node && ((!node.hasAttribute(attrName) || node.getAttribute(attrName).length === 0) && !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 getText(element); }); } function getText(element) { 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 = options.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 invokeCtrl = $controller(controller, locals, true); if (bindToController) { angular.extend(invokeCtrl.instance, locals); } var ctrl = invokeCtrl(); //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"; var HANDLERS = {}; /* The state of the current 'pointer' * The pointer represents the state of the current touch. * It contains normalized x and y coordinates from DOM events, * as well as other information abstracted from the DOM. */ var pointer, lastPointer, forceSkipClickHijack = false; // Used to attach event listeners once when multiple ng-apps are running. var isInitialized = false; angular .module('material.core.gestures', [ ]) .provider('$mdGesture', MdGestureProvider) .factory('$$MdGestureHandler', MdGestureHandler) .run( attachToDocument ); /** * @ngdoc service * @name $mdGestureProvider * @module material.core.gestures * * @description * In some scenarios on Mobile devices (without jQuery), the click events should NOT be hijacked. * `$mdGestureProvider` is used to configure the Gesture module to ignore or skip click hijacking on mobile * devices. * * * app.config(function($mdGestureProvider) { * * // For mobile devices without jQuery loaded, do not * // intercept click events during the capture phase. * $mdGestureProvider.skipClickHijack(); * * }); * * */ function MdGestureProvider() { } MdGestureProvider.prototype = { // Publish access to setter to configure a variable BEFORE the // $mdGesture service is instantiated... skipClickHijack: function() { return forceSkipClickHijack = true; }, /** * $get is used to build an instance of $mdGesture * @ngInject */ $get : ["$$MdGestureHandler", "$$rAF", "$timeout", function($$MdGestureHandler, $$rAF, $timeout) { return new MdGesture($$MdGestureHandler, $$rAF, $timeout); }] }; /** * MdGesture factory construction function * @ngInject */ function MdGesture($$MdGestureHandler, $$rAF, $timeout) { var userAgent = navigator.userAgent || navigator.vendor || window.opera; var isIos = userAgent.match(/ipad|iphone|ipod/i); var isAndroid = userAgent.match(/android/i); var hasJQuery = (typeof window.jQuery !== 'undefined') && (angular.element === window.jQuery); var self = { handler: addHandler, register: register, // On mobile w/out jQuery, we normally intercept clicks. Should we skip that? isHijackingClicks: (isIos || isAndroid) && !hasJQuery && !forceSkipClickHijack }; if (self.isHijackingClicks) { self.handler('click', { options: { maxDistance: 6 }, onEnd: function (ev, pointer) { if (pointer.distance < this.state.options.maxDistance) { this.dispatchEvent(ev, 'click'); } } }); } /* * Register an element to listen for a handler. * This allows an element to override the default options for a handler. * Additionally, some handlers like drag and hold only dispatch events if * the domEvent happens inside an element that's registered to listen for these events. * * @see GestureHandler for how overriding of default options works. * @example $mdGesture.register(myElement, 'drag', { minDistance: 20, horziontal: false }) */ function register(element, handlerName, options) { var handler = HANDLERS[handlerName.replace(/^\$md./, '')]; if (!handler) { throw new Error('Failed to register element with handler ' + handlerName + '. ' + 'Available handlers: ' + Object.keys(HANDLERS).join(', ')); } return handler.registerElement(element, options); } /* * add a handler to $mdGesture. see below. */ function addHandler(name, definition) { var handler = new $$MdGestureHandler(name); angular.extend(handler, definition); HANDLERS[name] = handler; return self; } /* * Register handlers. These listen to touch/start/move events, interpret them, * and dispatch gesture events depending on options & conditions. These are all * instances of GestureHandler. * @see GestureHandler */ return self /* * The press handler dispatches an event on touchdown/touchend. * It's a simple abstraction of touch/mouse/pointer start and end. */ .handler('press', { onStart: function (ev, pointer) { this.dispatchEvent(ev, '$md.pressdown'); }, onEnd: function (ev, pointer) { this.dispatchEvent(ev, '$md.pressup'); } }) /* * The hold handler dispatches an event if the user keeps their finger within * the same area for ms. * The hold handler will only run if a parent of the touch target is registered * to listen for hold events through $mdGesture.register() */ .handler('hold', { options: { maxDistance: 6, delay: 500 }, onCancel: function () { $timeout.cancel(this.state.timeout); }, onStart: function (ev, pointer) { // For hold, require a parent to be registered with $mdGesture.register() // Because we prevent scroll events, this is necessary. if (!this.state.registeredParent) return this.cancel(); this.state.pos = {x: pointer.x, y: pointer.y}; this.state.timeout = $timeout(angular.bind(this, function holdDelayFn() { this.dispatchEvent(ev, '$md.hold'); this.cancel(); //we're done! }), this.state.options.delay, false); }, onMove: function (ev, pointer) { // Don't scroll while waiting for hold. // If we don't preventDefault touchmove events here, Android will assume we don't // want to listen to anymore touch events. It will start scrolling and stop sending // touchmove events. ev.preventDefault(); // If the user moves greater than pixels, stop the hold timer // set in onStart var dx = this.state.pos.x - pointer.x; var dy = this.state.pos.y - pointer.y; if (Math.sqrt(dx * dx + dy * dy) > this.options.maxDistance) { this.cancel(); } }, onEnd: function () { this.onCancel(); } }) /* * The drag handler dispatches a drag event if the user holds and moves his finger greater than * px in the x or y direction, depending on options.horizontal. * The drag will be cancelled if the user moves his finger greater than * in * the perpindicular direction. Eg if the drag is horizontal and the user moves his finger * * pixels vertically, this handler won't consider the move part of a drag. */ .handler('drag', { options: { minDistance: 6, horizontal: true, cancelMultiplier: 1.5 }, onStart: function (ev) { // For drag, require a parent to be registered with $mdGesture.register() if (!this.state.registeredParent) this.cancel(); }, onMove: function (ev, pointer) { var shouldStartDrag, shouldCancel; // Don't scroll while deciding if this touchmove qualifies as a drag event. // If we don't preventDefault touchmove events here, Android will assume we don't // want to listen to anymore touch events. It will start scrolling and stop sending // touchmove events. ev.preventDefault(); if (!this.state.dragPointer) { if (this.state.options.horizontal) { shouldStartDrag = Math.abs(pointer.distanceX) > this.state.options.minDistance; shouldCancel = Math.abs(pointer.distanceY) > this.state.options.minDistance * this.state.options.cancelMultiplier; } else { shouldStartDrag = Math.abs(pointer.distanceY) > this.state.options.minDistance; shouldCancel = Math.abs(pointer.distanceX) > this.state.options.minDistance * this.state.options.cancelMultiplier; } if (shouldStartDrag) { // Create a new pointer representing this drag, starting at this point where the drag started. this.state.dragPointer = makeStartPointer(ev); updatePointerState(ev, this.state.dragPointer); this.dispatchEvent(ev, '$md.dragstart', this.state.dragPointer); } else if (shouldCancel) { this.cancel(); } } else { this.dispatchDragMove(ev); } }, // Only dispatch dragmove events every frame; any more is unnecessray dispatchDragMove: $$rAF.throttle(function (ev) { // Make sure the drag didn't stop while waiting for the next frame if (this.state.isRunning) { updatePointerState(ev, this.state.dragPointer); this.dispatchEvent(ev, '$md.drag', this.state.dragPointer); } }), onEnd: function (ev, pointer) { if (this.state.dragPointer) { updatePointerState(ev, this.state.dragPointer); this.dispatchEvent(ev, '$md.dragend', this.state.dragPointer); } } }) /* * The swipe handler will dispatch a swipe event if, on the end of a touch, * the velocity and distance were high enough. * TODO: add vertical swiping with a `horizontal` option similar to the drag handler. */ .handler('swipe', { options: { minVelocity: 0.65, minDistance: 10 }, onEnd: function (ev, pointer) { if (Math.abs(pointer.velocityX) > this.state.options.minVelocity && Math.abs(pointer.distanceX) > this.state.options.minDistance) { var eventType = pointer.directionX == 'left' ? '$md.swipeleft' : '$md.swiperight'; this.dispatchEvent(ev, eventType); } } }); } MdGesture.$inject = ["$$MdGestureHandler", "$$rAF", "$timeout"]; /** * MdGestureHandler * A GestureHandler is an object which is able to dispatch custom dom events * based on native dom {touch,pointer,mouse}{start,move,end} events. * * A gesture will manage its lifecycle through the start,move,end, and cancel * functions, which are called by native dom events. * * A gesture has the concept of 'options' (eg a swipe's required velocity), which can be * overridden by elements registering through $mdGesture.register() */ function GestureHandler (name) { this.name = name; this.state = {}; } function MdGestureHandler() { var hasJQuery = (typeof window.jQuery !== 'undefined') && (angular.element === window.jQuery); GestureHandler.prototype = { options: {}, // jQuery listeners don't work with custom DOMEvents, so we have to dispatch events // differently when jQuery is loaded dispatchEvent: hasJQuery ? jQueryDispatchEvent : nativeDispatchEvent, // These are overridden by the registered handler onStart: angular.noop, onMove: angular.noop, onEnd: angular.noop, onCancel: angular.noop, // onStart sets up a new state for the handler, which includes options from the // nearest registered parent element of ev.target. start: function (ev, pointer) { if (this.state.isRunning) return; var parentTarget = this.getNearestParent(ev.target); // Get the options from the nearest registered parent var parentTargetOptions = parentTarget && parentTarget.$mdGesture[this.name] || {}; this.state = { isRunning: true, // Override the default options with the nearest registered parent's options options: angular.extend({}, this.options, parentTargetOptions), // Pass in the registered parent node to the state so the onStart listener can use registeredParent: parentTarget }; this.onStart(ev, pointer); }, move: function (ev, pointer) { if (!this.state.isRunning) return; this.onMove(ev, pointer); }, end: function (ev, pointer) { if (!this.state.isRunning) return; this.onEnd(ev, pointer); this.state.isRunning = false; }, cancel: function (ev, pointer) { this.onCancel(ev, pointer); this.state = {}; }, // Find and return the nearest parent element that has been registered to // listen for this handler via $mdGesture.register(element, 'handlerName'). getNearestParent: function (node) { var current = node; while (current) { if ((current.$mdGesture || {})[this.name]) { return current; } current = current.parentNode; } return null; }, // Called from $mdGesture.register when an element reigsters itself with a handler. // Store the options the user gave on the DOMElement itself. These options will // be retrieved with getNearestParent when the handler starts. registerElement: function (element, options) { var self = this; element[0].$mdGesture = element[0].$mdGesture || {}; element[0].$mdGesture[this.name] = options || {}; element.on('$destroy', onDestroy); return onDestroy; function onDestroy() { delete element[0].$mdGesture[self.name]; element.off('$destroy', onDestroy); } } }; return GestureHandler; /* * Dispatch an event with jQuery * TODO: Make sure this sends bubbling events * * @param srcEvent the original DOM touch event that started this. * @param eventType the name of the custom event to send (eg 'click' or '$md.drag') * @param eventPointer the pointer object that matches this event. */ function jQueryDispatchEvent(srcEvent, eventType, eventPointer) { eventPointer = eventPointer || pointer; var eventObj = new angular.element.Event(eventType); eventObj.$material = true; eventObj.pointer = eventPointer; eventObj.srcEvent = srcEvent; angular.extend(eventObj, { clientX: eventPointer.x, clientY: eventPointer.y, screenX: eventPointer.x, screenY: eventPointer.y, pageX: eventPointer.x, pageY: eventPointer.y, ctrlKey: srcEvent.ctrlKey, altKey: srcEvent.altKey, shiftKey: srcEvent.shiftKey, metaKey: srcEvent.metaKey }); angular.element(eventPointer.target).trigger(eventObj); } /* * NOTE: nativeDispatchEvent is very performance sensitive. * @param srcEvent the original DOM touch event that started this. * @param eventType the name of the custom event to send (eg 'click' or '$md.drag') * @param eventPointer the pointer object that matches this event. */ function nativeDispatchEvent(srcEvent, eventType, eventPointer) { eventPointer = eventPointer || pointer; var eventObj; if (eventType === 'click') { eventObj = document.createEvent('MouseEvents'); eventObj.initMouseEvent( 'click', true, true, window, srcEvent.detail, eventPointer.x, eventPointer.y, eventPointer.x, eventPointer.y, srcEvent.ctrlKey, srcEvent.altKey, srcEvent.shiftKey, srcEvent.metaKey, srcEvent.button, srcEvent.relatedTarget || null ); } else { eventObj = document.createEvent('CustomEvent'); eventObj.initCustomEvent(eventType, true, true, {}); } eventObj.$material = true; eventObj.pointer = eventPointer; eventObj.srcEvent = srcEvent; eventPointer.target.dispatchEvent(eventObj); } } /** * Attach Gestures: hook document and check shouldHijack clicks * @ngInject */ function attachToDocument( $mdGesture, $$MdGestureHandler ) { // Polyfill document.contains for IE11. // TODO: move to util document.contains || (document.contains = function (node) { return document.body.contains(node); }); if (!isInitialized && $mdGesture.isHijackingClicks ) { /* * If hijack clicks is true, we preventDefault any click that wasn't * sent by ngMaterial. This is because on older Android & iOS, a false, or 'ghost', * click event will be sent ~400ms after a touchend event happens. * The only way to know if this click is real is to prevent any normal * click events, and add a flag to events sent by material so we know not to prevent those. * * Two exceptions to click events that should be prevented are: * - click events sent by the keyboard (eg form submit) * - events that originate from an Ionic app */ document.addEventListener('click', function clickHijacker(ev) { var isKeyClick = ev.clientX === 0 && ev.clientY === 0; if (!isKeyClick && !ev.$material && !ev.isIonicTap) { ev.preventDefault(); ev.stopPropagation(); } }, true); isInitialized = true; } // Listen to all events to cover all platforms. var START_EVENTS = 'mousedown touchstart pointerdown'; var MOVE_EVENTS = 'mousemove touchmove pointermove'; var END_EVENTS = 'mouseup mouseleave touchend touchcancel pointerup pointercancel'; angular.element(document) .on(START_EVENTS, gestureStart) .on(MOVE_EVENTS, gestureMove) .on(END_EVENTS, gestureEnd) // For testing .on('$$mdGestureReset', function gestureClearCache () { lastPointer = pointer = null; }); /* * When a DOM event happens, run all registered gesture handlers' lifecycle * methods which match the DOM event. * Eg when a 'touchstart' event happens, runHandlers('start') will call and * run `handler.cancel()` and `handler.start()` on all registered handlers. */ function runHandlers(handlerEvent, event) { var handler; for (var name in HANDLERS) { handler = HANDLERS[name]; if( handler instanceof $$MdGestureHandler ) { if (handlerEvent === 'start') { // Run cancel to reset any handlers' state handler.cancel(); } handler[handlerEvent](event, pointer); } } } /* * gestureStart vets if a start event is legitimate (and not part of a 'ghost click' from iOS/Android) * If it is legitimate, we initiate the pointer state and mark the current pointer's type * For example, for a touchstart event, mark the current pointer as a 'touch' pointer, so mouse events * won't effect it. */ function gestureStart(ev) { // If we're already touched down, abort if (pointer) return; var now = +Date.now(); // iOS & old android bug: after a touch event, a click event is sent 350 ms later. // If <400ms have passed, don't allow an event of a different type than the previous event if (lastPointer && !typesMatch(ev, lastPointer) && (now - lastPointer.endTime < 1500)) { return; } pointer = makeStartPointer(ev); runHandlers('start', ev); } /* * If a move event happens of the right type, update the pointer and run all the move handlers. * "of the right type": if a mousemove happens but our pointer started with a touch event, do nothing. */ function gestureMove(ev) { if (!pointer || !typesMatch(ev, pointer)) return; updatePointerState(ev, pointer); runHandlers('move', ev); } /* * If an end event happens of the right type, update the pointer, run endHandlers, and save the pointer as 'lastPointer' */ function gestureEnd(ev) { if (!pointer || !typesMatch(ev, pointer)) return; updatePointerState(ev, pointer); pointer.endTime = +Date.now(); runHandlers('end', ev); lastPointer = pointer; pointer = null; } } attachToDocument.$inject = ["$mdGesture", "$$MdGestureHandler"]; // ******************** // Module Functions // ******************** /* * Initiate the pointer. x, y, and the pointer's type. */ function makeStartPointer(ev) { var point = getEventPoint(ev); var startPointer = { startTime: +Date.now(), target: ev.target, // 'p' for pointer events, 'm' for mouse, 't' for touch type: ev.type.charAt(0) }; startPointer.startX = startPointer.x = point.pageX; startPointer.startY = startPointer.y = point.pageY; return startPointer; } /* * return whether the pointer's type matches the event's type. * Eg if a touch event happens but the pointer has a mouse type, return false. */ function typesMatch(ev, pointer) { return ev && pointer && ev.type.charAt(0) === pointer.type; } /* * Update the given pointer based upon the given DOMEvent. * Distance, velocity, direction, duration, etc */ function updatePointerState(ev, pointer) { var point = getEventPoint(ev); var x = pointer.x = point.pageX; var y = pointer.y = point.pageY; pointer.distanceX = x - pointer.startX; pointer.distanceY = y - pointer.startY; pointer.distance = Math.sqrt( pointer.distanceX * pointer.distanceX + pointer.distanceY * pointer.distanceY ); pointer.directionX = pointer.distanceX > 0 ? 'right' : pointer.distanceX < 0 ? 'left' : ''; pointer.directionY = pointer.distanceY > 0 ? 'up' : pointer.distanceY < 0 ? 'down' : ''; pointer.duration = +Date.now() - pointer.startTime; pointer.velocityX = pointer.distanceX / pointer.duration; pointer.velocityY = pointer.distanceY / pointer.duration; } /* * Normalize the point where the DOM event happened whether it's touch or mouse. * @returns point event obj with pageX and pageY on it. */ function getEventPoint(ev) { ev = ev.originalEvent || ev; // support jQuery events return (ev.touches && ev.touches[0]) || (ev.changedTouches && ev.changedTouches[0]) || ev; } })(); (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 customMethods = {}; var providerConfig = { presets: {} }; var provider = { setDefaults: setDefaults, addPreset: addPreset, addMethod: addMethod, $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; } /** * Add a method to the factory that isn't specific to any interim element operations */ function addMethod(name, fn) { customMethods[name] = fn; 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, {}); // Copy over the simple custom methods angular.forEach(customMethods, function(fn, name) { publicService[name] = fn; }); 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) { return service.cancel().then(function() { return show(options); }); } else { 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(); return 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 Promise that will be resolved after the element has been removed. * */ function cancel(reason) { var interimElement = stack.shift(); return $q.when(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, showDone, removeDone; options = options || {}; options = angular.extend({ preserveScope: false, 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() { var compilePromise; if (options.skipCompile) { compilePromise = $q(function(resolve) { resolve({ locals: {}, link: function() { return options.element; } }); }); } else { compilePromise = $mdCompiler.compile(options); } return showDone = compilePromise.then(function(compileData) { angular.extend(compileData.locals, self.options); element = compileData.link(options.scope); // Search for parent at insertion time, if not specified if (angular.isFunction(options.parent)) { options.parent = options.parent(options.scope, element, options); } else if (angular.isString(options.parent)) { options.parent = angular.element($document[0].querySelector(options.parent)); } // If parent querySelector/getter function fails, or it's just null, // find a default. if (!(options.parent || {}).length) { var el; if ($rootElement[0] && $rootElement[0].querySelector) { el = $rootElement[0].querySelector(':not(svg) > body'); } if (!el) el = $rootElement[0]; if (el.nodeName == '#comment') { el = $document[0].body; } options.parent = angular.element(el); } 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) ; } } }, function(reason) { showDone = true; self.deferred.reject(reason); }); }, cancelTimeout: function() { if (hideTimeout) { $timeout.cancel(hideTimeout); hideTimeout = undefined; } }, remove: function() { self.cancelTimeout(); return removeDone = $q.when(showDone).then(function() { var ret = element ? options.onRemove(options.scope, element, options) : true; return $q.when(ret).then(function() { if (!options.preserveScope) options.scope.$destroy(); removeDone = true; }); }); } }; } }; /* * 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"; (function() { 'use strict'; /** * @ngdoc service * @name $mdButtonInkRipple * @module material.core * * @description * Provides ripple effects for md-button. See $mdInkRipple service for all possible configuration options. * * @param {object=} scope Scope within the current context * @param {object=} element The element the ripple effect should be applied to * @param {object=} options (Optional) Configuration options to override the defaultripple configuration */ angular.module('material.core') .factory('$mdButtonInkRipple', MdButtonInkRipple); function MdButtonInkRipple($mdInkRipple) { return { attach: attach }; function attach(scope, element, options) { var elementOptions = optionsForElement(element); return $mdInkRipple.attach(scope, element, angular.extend(elementOptions, options)); }; function optionsForElement(element) { if (element.hasClass('md-icon-button')) { return { isMenuItem: element.hasClass('md-menu-item'), fitRipple: true, center: true }; } else { return { isMenuItem: element.hasClass('md-menu-item'), dimBackground: true } } }; } MdButtonInkRipple.$inject = ["$mdInkRipple"];; })(); })(); (function(){ "use strict"; (function() { 'use strict'; /** * @ngdoc service * @name $mdCheckboxInkRipple * @module material.core * * @description * Provides ripple effects for md-checkbox. See $mdInkRipple service for all possible configuration options. * * @param {object=} scope Scope within the current context * @param {object=} element The element the ripple effect should be applied to * @param {object=} options (Optional) Configuration options to override the defaultripple configuration */ angular.module('material.core') .factory('$mdCheckboxInkRipple', MdCheckboxInkRipple); function MdCheckboxInkRipple($mdInkRipple) { return { attach: attach }; function attach(scope, element, options) { return $mdInkRipple.attach(scope, element, angular.extend({ center: true, dimBackground: false, fitRipple: true }, options)); }; } MdCheckboxInkRipple.$inject = ["$mdInkRipple"];; })(); })(); (function(){ "use strict"; (function() { 'use strict'; /** * @ngdoc service * @name $mdListInkRipple * @module material.core * * @description * Provides ripple effects for md-list. See $mdInkRipple service for all possible configuration options. * * @param {object=} scope Scope within the current context * @param {object=} element The element the ripple effect should be applied to * @param {object=} options (Optional) Configuration options to override the defaultripple configuration */ angular.module('material.core') .factory('$mdListInkRipple', MdListInkRipple); function MdListInkRipple($mdInkRipple) { return { attach: attach }; function attach(scope, element, options) { return $mdInkRipple.attach(scope, element, angular.extend({ center: false, dimBackground: true, outline: false, rippleSize: 'full' }, options)); }; } MdListInkRipple.$inject = ["$mdInkRipple"];; })(); })(); (function(){ "use strict"; angular.module('material.core') .factory('$mdInkRipple', InkRippleService) .directive('mdInkRipple', InkRippleDirective) .directive('mdNoInk', attrNoDirective()) .directive('mdNoBar', attrNoDirective()) .directive('mdNoStretch', attrNoDirective()); function InkRippleDirective($mdButtonInkRipple, $mdCheckboxInkRipple) { return { controller: angular.noop, link: function (scope, element, attr) { if (attr.hasOwnProperty('mdInkRippleCheckbox')) { $mdCheckboxInkRipple.attach(scope, element); } else { $mdButtonInkRipple.attach(scope, element); } } }; } InkRippleDirective.$inject = ["$mdButtonInkRipple", "$mdCheckboxInkRipple"]; function InkRippleService($window, $timeout) { return { attach: attach }; 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, fullRipple: true, isMenuItem: false, fitRipple: false }, options); var rippleSize, controller = element.controller('mdInkRipple') || {}, counter = 0, ripples = [], states = [], isActiveExpr = element.attr('md-highlight'), isActive = false, isHeld = false, node = element[0], rippleSizeSetting = element.attr('md-ripple-size'), color = parseColor(element.attr('md-ink-ripple')) || parseColor(options.colorElement.length && $window.getComputedStyle(options.colorElement[0]).color || 'rgb(0, 0, 0)'); switch (rippleSizeSetting) { case 'full': options.fullRipple = true; break; case 'partial': options.fullRipple = false; break; } // expose onInput for ripple testing if (options.mousedown) { element.on('$md.pressdown', onPressDown) .on('$md.pressup', onPressUp); } 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() { element.off('$md.pressdown', onPressDown) .off('$md.pressup', onPressUp); getRippleContainer().remove(); }; /** * 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() { var container = element.data('$mdRippleContainer'); if (container) return container; container = angular.element('
'); element.append(container); element.data('$mdRippleContainer', container); return container; } 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) { getRippleContainer().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.fullRipple ? 1.1 : 0.8; size = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2)) * multiplier; if (options.fitRipple) { size = Math.min(height, width, size); } } 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: string, borderColor: string, width: string, height: string}} */ function getRippleCss(size, left, top) { var rect = node.getBoundingClientRect(), 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 { 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(/,[^\),]+\)/, ')'); } } } /** * Handles user input start and stop events * */ function onPressDown(ev) { if (!isRippleAllowed()) return; createRipple(ev.pointer.x, ev.pointer.y); isHeld = true; } function onPressUp() { isHeld = false; var ripple = ripples[ ripples.length - 1 ]; $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"; (function() { 'use strict'; /** * @ngdoc service * @name $mdTabInkRipple * @module material.core * * @description * Provides ripple effects for md-tabs. See $mdInkRipple service for all possible configuration options. * * @param {object=} scope Scope within the current context * @param {object=} element The element the ripple effect should be applied to * @param {object=} options (Optional) Configuration options to override the defaultripple configuration */ angular.module('material.core') .factory('$mdTabInkRipple', MdTabInkRipple); function MdTabInkRipple($mdInkRipple) { return { attach: attach }; function attach(scope, element, options) { return $mdInkRipple.attach(scope, element, angular.extend({ center: false, dimBackground: true, outline: false, rippleSize: 'full' }, options)); }; } MdTabInkRipple.$inject = ["$mdInkRipple"];; })(); })(); (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', 'contrastStrongLightColors': '500 600 700 A200 A400 A700' }, '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', 'contrastStrongLightColors': '500 600 A200 A400 A700' }, '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', 'contrastStrongLightColors': '300 400 A200 A400 A700' }, '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', 'contrastStrongLightColors': '300 400 A200' }, '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', 'contrastStrongLightColors': '300 400 A200 A400' }, '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', 'contrastStrongLightColors': '500 600 700 A200 A400 A700' }, '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', 'contrastStrongLightColors': '500 600 700 800 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', 'contrastStrongLightColors': '500 600 700 800' }, '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', 'contrastStrongLightColors': '500 600 700' }, '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', 'contrastStrongLightColors': '500 600 700' }, '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', 'contrastStrongLightColors': '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', 'contrastStrongLightColors': '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', 'contrastStrongLightColors': '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', 'contrastStrongLightColors': '500 600 700 800 900 A400 A700' }, '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', 'contrastStrongLightColors': '300 400' }, 'grey': { '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', 'contrastStrongLightColors': '400 500' } }); })(); (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. */ /* Some Example Valid Theming Expressions * ======================================= * * Intention group expansion: (valid for primary, accent, warn, background) * * {{primary-100}} - grab shade 100 from the primary palette * {{primary-100-0.7}} - grab shade 100, apply opacity of 0.7 * {{primary-hue-1}} - grab the shade assigned to hue-1 from the primary palette * {{primary-hue-1-0.7}} - apply 0.7 opacity to primary-hue-1 * {{primary-color}} - Generates .md-hue-1, .md-hue-2, .md-hue-3 with configured shades set for each hue * {{primary-color-0.7}} - Apply 0.7 opacity to each of the above rules * {{primary-contrast}} - Generates .md-hue-1, .md-hue-2, .md-hue-3 with configured contrast (ie. text) color shades set for each hue * {{primary-contrast-0.7}} - Apply 0.7 opacity to each of the above rules * * Foreground expansion: Applies rgba to black/white foreground text * * {{foreground-1}} - used for primary text * {{foreground-2}} - used for secondary text/divider * {{foreground-3}} - used for disabled text * {{foreground-4}} - used for dividers * */ // In memory generated CSS rules; registered by theme.name var GENERATED = { }; // In memory storage of defined themes and color palettes (both loaded by CSS, and user specified) var PALETTES; var THEMES; 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('rgba(255,255,255,0.87'); var STRONG_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': 'A200', 'hue-1': 'A100', 'hue-2': 'A400', 'hue-3': 'A700' }, 'background': { 'default': 'A100', 'hue-1': '300', 'hue-2': '800', 'hue-3': '900' } }; var DARK_DEFAULT_HUES = { 'background': { 'default': '800', 'hue-1': '300', 'hue-2': '600', 'hue-3': '900' } }; 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 themingProvider; var defaultTheme = 'default'; var alwaysWatchTheme = false; // Load JS Defined Palettes angular.extend(PALETTES, $mdColorPalette); // Default theme defined in core.js ThemingService.$inject = ["$rootScope", "$log"]; 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').primaryPalette('red'); function registerTheme(name, inheritFrom) { if (THEMES[name]) return THEMES[name]; inheritFrom = inheritFrom || 'default'; 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 + 'Palette'] = function setPaletteType(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; }; self[colorType + 'Color'] = function() { var args = Array.prototype.slice.call(arguments); console.warn('$mdThemingProviderTheme.' + colorType + 'Color() has been deprecated. ' + 'Use $mdThemingProviderTheme.' + colorType + 'Palette() instead.'); return self[colorType + 'Palette'].apply(self, args); }; }); } /** * @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, $log) { 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) { if (!registered(theme)) { $log.warn('Attempted to use unregistered theme \'' + theme + '\'. ' + 'Register it with $mdThemingProvider.theme().'); } var oldTheme = el.data('$mdThemeName'); if (oldTheme) el.removeClass('md-' + oldTheme +'-theme'); el.addClass('md-' + theme + '-theme'); el.data('$mdThemeName', theme); } }; applyTheme.THEMES = angular.extend({}, THEMES); applyTheme.defaultTheme = function() { return defaultTheme; }; applyTheme.registered = registered; return applyTheme; function registered(themeName) { if (themeName === undefined || themeName === '') return true; return applyTheme.THEMES[themeName] !== undefined; } 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($mdTheming, $interpolate, $log) { return { priority: 100, link: { pre: function(scope, el, attrs) { var ctrl = { $setTheme: function(theme) { if (!$mdTheming.registered(theme)) { $log.warn('attempted to use unregistered theme \'' + theme + '\''); } ctrl.$mdTheme = theme; } }; el.data('$mdThemeController', ctrl); ctrl.$setTheme($interpolate(attrs.mdTheme)(scope)); attrs.$observe('mdTheme', ctrl.$setTheme); } } }; } ThemingDirective.$inject = ["$mdTheming", "$interpolate", "$log"]; 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 an entire 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); } // Don't apply a selector rule to the default theme, making it easier to override // styles of the base-component if (theme.name == 'default') { newRule = newRule.replace(/\.md-default-theme/g, ''); } generatedRules.push(newRule); }); return generatedRules; } // Generate our themes at run time given the state of THEMES and PALETTES function generateThemes($injector) { var head = document.getElementsByTagName('head')[0]; var firstChild = head ? head.firstElementChild : null; var themeCss = $injector.has('$MD_THEME_CSS') ? $injector.get('$MD_THEME_CSS') : ''; if ( !firstChild ) return; if (themeCss.length === 0) return; // no rules, so no point in running this expensive task // Expose contrast colors for palettes to ensure that text is always readable angular.forEach(PALETTES, sanitizePalette); // MD_THEME_CSS is a string generated by the build process that includes all the themable // components as templates // Break the CSS into individual rules var rulesByType = {}; var rules = themeCss .split(/\}(?!(\}|'|"|;))/) .filter(function(rule) { return rule && rule.length; }) .map(function(rule) { return rule.trim() + '}'; }); var ruleMatchRegex = new RegExp('md-(' + THEME_COLOR_TYPES.join('|') + ')', 'g'); THEME_COLOR_TYPES.forEach(function(type) { rulesByType[type] = ''; }); // 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; }); // For each theme, use the color palettes specified for // `primary`, `warn` and `accent` to generate CSS rules. angular.forEach(THEMES, function(theme) { if ( !GENERATED[theme.name] ) { THEME_COLOR_TYPES.forEach(function(colorType) { var styleStrings = parseRules(theme, colorType, rulesByType[colorType]); while (styleStrings.length) { var style = document.createElement('style'); style.setAttribute('type', 'text/css'); style.appendChild(document.createTextNode(styleStrings.shift())); head.insertBefore(style, firstChild); } }); if (theme.colors.primary.name == theme.colors.accent.name) { console.warn("$mdThemingProvider: Using the same palette for primary and" + " accent. This violates the material design spec."); } GENERATED[theme.name] = true; } }); // ************************* // Internal functions // ************************* // 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 strongLightColors = palette.contrastStrongLightColors || []; var darkColors = palette.contrastDarkColors || []; // These colors are provided as space-separated lists if (typeof lightColors === 'string') lightColors = lightColors.split(' '); if (typeof strongLightColors === 'string') strongLightColors = strongLightColors.split(' '); if (typeof darkColors === 'string') darkColors = darkColors.split(' '); // Cleanup after ourselves delete palette.contrastDefaultColor; delete palette.contrastLightColors; delete palette.contrastStrongLightColors; 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') { if (darkColors.indexOf(hueName) > -1) { return DARK_CONTRAST_COLOR; } else { return strongLightColors.indexOf(hueName) > -1 ? STRONG_LIGHT_CONTRAST_COLOR : LIGHT_CONTRAST_COLOR; } } else { if (lightColors.indexOf(hueName) > -1) { return strongLightColors.indexOf(hueName) > -1 ? STRONG_LIGHT_CONTRAST_COLOR : LIGHT_CONTRAST_COLOR; } else { return 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, i) { return i == 3 ? parseFloat(value, 10) : 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 ) return "rgb('0,0,0')"; if (rgbArray.length == 4) { rgbArray = angular.copy(rgbArray); opacity ? rgbArray.pop() : opacity = rgbArray.pop(); } return opacity && (typeof opacity == 'number' || (typeof opacity == 'string' && opacity.length)) ? 'rgba(' + rgbArray.join(',') + ',' + opacity + ')' : 'rgb(' + rgbArray.join(',') + ')'; } })(); (function(){ "use strict"; /** * @ngdoc module * @name material.components.autocomplete */ /* * @see js folder for autocomplete implementation */ angular.module('material.components.autocomplete', [ 'material.core', 'material.components.icon' ]); })(); (function(){ "use strict"; /* * @ngdoc module * @name material.components.backdrop * @description Backdrop */ /** * @ngdoc directive * @name mdBackdrop * @module material.components.backdrop * * @restrict E * * @description * `` is a backdrop element used by other components, 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 `` 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 * *
* * Open a Bottom Sheet! * *
*
* * var app = angular.module('app', ['ngMaterial']); * app.controller('MyController', function($scope, $mdBottomSheet) { * $scope.openBottomSheet = function() { * $mdBottomSheet.show({ * template: 'Hello!' * }); * }; * }); * */ /** * @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. * - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified, it will create a new child scope. * This scope will be destroyed when the bottom sheet is removed unless `preserveScope` is set to true. * - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false * - `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. * - `parent` - `{element=}`: The element to append the bottom sheet to. The `parent` may be a `function`, `string`, * `object`, or null. Defaults to appending to the body of the root element (or the root element) of the application. * e.g. angular.element(document.getElementById('content')) or "#content" * - `disableParentScroll` - `{boolean=}`: Whether to disable scrolling while the bottom sheet is open. * Default true. * * @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()`. This call will close the most recently opened/current bottomsheet (if any). * * @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) { // how fast we need to flick down to close the sheet, pixels/ms var CLOSING_VELOCITY = 0.5; var PADDING = 80; // same as css bottomSheetDefaults.$inject = ["$animate", "$mdConstant", "$mdUtil", "$timeout", "$compile", "$mdTheming", "$mdBottomSheet", "$rootElement", "$mdGesture"]; return $$interimElementProvider('$mdBottomSheet') .setDefaults({ methods: ['disableParentScroll', 'escapeToClose', 'targetEvent'], options: bottomSheetDefaults }); /* @ngInject */ function bottomSheetDefaults($animate, $mdConstant, $mdUtil, $timeout, $compile, $mdTheming, $mdBottomSheet, $rootElement, $mdGesture) { var backdrop; return { themable: true, targetEvent: null, onShow: onShow, onRemove: onRemove, escapeToClose: true, disableParentScroll: true }; function onShow(scope, element, options) { element = $mdUtil.extractElementByName(element, 'md-bottom-sheet'); // Add a backdrop that will close on click backdrop = $compile('')(scope); backdrop.on('click', function() { $timeout($mdBottomSheet.cancel); }); $mdTheming.inherit(backdrop, options.parent); $animate.enter(backdrop, options.parent, null); var bottomSheet = new BottomSheet(element, options.parent); options.bottomSheet = bottomSheet; // Give up focus on calling item options.targetEvent && angular.element(options.targetEvent.target).blur(); $mdTheming.inherit(bottomSheet.element, options.parent); if (options.disableParentScroll) { options.lastOverflow = options.parent.css('overflow'); options.parent.css('overflow', 'hidden'); } return $animate.enter(bottomSheet.element, options.parent) .then(function() { var focusable = angular.element( element[0].querySelector('button') || element[0].querySelector('a') || element[0].querySelector('[ng-click]') ); focusable.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() { if (options.disableParentScroll) { options.parent.css('overflow', options.lastOverflow); delete options.lastOverflow; } 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, parent) { var deregister = $mdGesture.register(parent, 'drag', { horizontal: false }); parent.on('$md.dragstart', onDragStart) .on('$md.drag', onDrag) .on('$md.dragend', onDragEnd); return { element: element, cleanup: function cleanup() { deregister(); parent.off('$md.dragstart', onDragStart) .off('$md.drag', onDrag) .off('$md.dragend', onDragEnd); } }; function onDragStart(ev) { // Disable transitions on transform so that it feels fast element.css($mdConstant.CSS.TRANSITION_DURATION, '0ms'); } function onDrag(ev) { var transform = ev.pointer.distanceY; if (transform < 5) { // Slow down drag when trying to drag up, and stop after PADDING transform = Math.max(-PADDING, transform / 2); } element.css($mdConstant.CSS.TRANSFORM, 'translate3d(0,' + (PADDING + transform) + 'px,0)'); } function onDragEnd(ev) { if (ev.pointer.distanceY > 0 && (ev.pointer.distanceY > 20 || Math.abs(ev.pointer.velocityY) > CLOSING_VELOCITY)) { var distanceRemaining = element.prop('offsetHeight') - ev.pointer.distanceY; var transitionDuration = Math.min(distanceRemaining / ev.pointer.velocityY * 0.75, 500); element.css($mdConstant.CSS.TRANSITION_DURATION, transitionDuration + 'ms'); $timeout($mdBottomSheet.cancel); } else { element.css($mdConstant.CSS.TRANSITION_DURATION, ''); element.css($mdConstant.CSS.TRANSFORM, ''); } } } } } 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 * `` is a button directive with optional ink ripples (default enabled). * * If you supply a `href` or `ng-href` attribute, it will become an `` element. Otherwise, it will * become a `'; } function postLink(scope, element, attr) { var node = element[0]; $mdTheming(element); $mdButtonInkRipple.attach(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) && angular.isDefined(attr.ngDisabled) ) { scope.$watch(attr.ngDisabled, function(isDisabled) { element.attr('tabindex', isDisabled ? -1 : 0); }); } // disabling click event when disabled is true element.on('click', function(e){ if (attr.disabled === true) { e.preventDefault(); e.stopImmediatePropagation(); } }); // restrict focus styles to the keyboard scope.mouseActive = false; element.on('mousedown', function() { scope.mouseActive = true; $timeout(function(){ scope.mouseActive = false; }, 100); }) .on('focus', function() { if(scope.mouseActive === false) { element.addClass('md-focused'); } }) .on('blur', function() { element.removeClass('md-focused'); }); } } MdButtonDirective.$inject = ["$mdButtonInkRipple", "$mdTheming", "$mdAria", "$timeout"]; })(); (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 `` directive is a container element used within `` containers. * * An image included as a direct descendant will fill the card's width, while the `` * container will wrap text content and provide padding. An `` element can be * optionally included to put content flush against the bottom edge of the card. * * Action buttons can be included in an element with the `.md-actions` class, also used in `md-dialog`. * You can then position buttons using layout attributes. * * 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 * ###Card with optional footer * * * image caption * *

Card headline

*

Card content

*
* * Card footer * *
*
* * ###Card with actions * * * image caption * *

Card headline

*

Card content

*
*
* Action 1 * Action 2 *
*
*
* */ 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). * * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-ui-color-application) * the checkbox is in the accent color by default. The primary color palette may be used with * the `md-primary` class. * * @param {string} ng-model Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @param {expression=} ng-true-value The value to which the expression should be set when selected. * @param {expression=} ng-false-value The value to which the expression should be set when not selected. * @param {string=} ng-change Angular expression to be executed when input changes due to user interaction with the input element. * @param {boolean=} md-no-ink Use of attribute indicates use of ripple ink effects * @param {string=} aria-label Adds label to checkbox for accessibility. * Defaults to checkbox's text. If no default text is found, a warning will be logged. * * @usage * * * Finished ? * * * * No Ink Effects * * * * Disabled * * * * */ function MdCheckboxDirective(inputDirective, $mdInkRipple, $mdAria, $mdConstant, $mdTheming, $mdUtil, $timeout) { inputDirective = inputDirective[0]; var CHECKED_CSS = 'md-checked'; return { restrict: 'E', transclude: true, require: '?ngModel', priority:210, // Run before ngAria template: '
' + '
' + '
' + '
', compile: compile }; // ********************************************************** // Private Methods // ********************************************************** function compile (tElement, tAttrs) { tAttrs.type = 'checkbox'; tAttrs.tabindex = tAttrs.tabindex || '0'; tElement.attr('role', tAttrs.type); return function postLink(scope, element, attr, ngModelCtrl) { ngModelCtrl = ngModelCtrl || $mdUtil.fakeNgModel(); $mdTheming(element); if (attr.ngChecked) { scope.$watch( scope.$eval.bind(scope, attr.ngChecked), ngModelCtrl.$setViewValue.bind(ngModelCtrl) ); } $$watchExpr('ngDisabled', 'tabindex', { true: '-1', false: attr.tabindex }); $mdAria.expectWithText(element, '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]); scope.mouseActive = false; element.on('click', listener) .on('keypress', keypressHandler) .on('mousedown', function() { scope.mouseActive = true; $timeout(function(){ scope.mouseActive = false; }, 100); }) .on('focus', function() { if(scope.mouseActive === false) { element.addClass('md-focused'); } }) .on('blur', function() { element.removeClass('md-focused'); }); ngModelCtrl.$render = render; function $$watchExpr(expr, htmlAttr, valueOpts) { if (attr[expr]) { scope.$watch(attr[expr], function(val) { if (valueOpts[val]) { element.attr(htmlAttr, valueOpts[val]); } }); } } function keypressHandler(ev) { var keyCode = ev.which || ev.keyCode; if (keyCode === $mdConstant.KEY_CODE.SPACE || keyCode === $mdConstant.KEY_CODE.ENTER) { ev.preventDefault(); if (!element.hasClass('md-focused')) { element.addClass('md-focused'); } listener(ev); } } function listener(ev) { if (element[0].hasAttribute('disabled')) return; scope.$apply(function() { // Toggle the checkbox value... var viewValue = attr.ngChecked ? attr.checked : !ngModelCtrl.$viewValue; ngModelCtrl.$setViewValue( viewValue, ev && ev.type); ngModelCtrl.$render(); }); } function render() { if(ngModelCtrl.$viewValue) { element.addClass(CHECKED_CSS); } else { element.removeClass(CHECKED_CSS); } } }; } } MdCheckboxDirective.$inject = ["inputDirective", "$mdInkRipple", "$mdAria", "$mdConstant", "$mdTheming", "$mdUtil", "$timeout"]; })(); (function(){ "use strict"; /** * @ngdoc module * @name material.components.chips */ /* * @see js folder for chips implementation */ angular.module('material.components.chips', [ 'material.core', 'material.components.autocomplete' ]); })(); (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 `` directive is a container element useful for scrollable content * * @usage * * - Add the `[layout-padding]` attribute to make the content padded. * * * * Lorem ipsum dolor sit amet, ne quod novum mei. * * * */ function mdContentDirective($mdTheming) { return { restrict: 'E', controller: ['$scope', '$element', ContentController], link: function(scope, element, attr) { var node = element[0]; $mdTheming(element); scope.$broadcast('$mdContentLoaded', element); iosScrollFix(element[0]); } }; function ContentController($scope, $element) { this.$scope = $scope; this.$element = $element; } } mdContentDirective.$inject = ["$mdTheming"]; function iosScrollFix(node) { // IOS FIX: // If we scroll where there is no more room for the webview to scroll, // by default the webview itself will scroll up and down, this looks really // bad. So if we are scrolling to the very top or bottom, add/subtract one angular.element(node).on('$md.pressdown', function(ev) { // Only touch events if (ev.pointer.type !== 't') return; // Don't let a child content's touchstart ruin it for us. if (ev.$materialScrollFixed) return; ev.$materialScrollFixed = true; if (node.scrollTop === 0) { node.scrollTop = 1; } else if (node.scrollHeight === node.scrollTop + node.offsetHeight) { node.scrollTop -= 1; } }); } })(); (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-dialog-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 to inform users about critical information or require * them to make decisions. There are two approaches for setup: a simple promise API * and regular object syntax. * * ## Restrictions * * - The dialog is always given an isolate scope. * - The dialog's template must have an outer `` element. * Inside, use an `` element for the dialog's content, and use * an element with class `md-actions` for the dialog's actions. * - Dialogs must cover the entire application to keep interactions inside of them. * Use the `parent` option to change where dialogs are appended. * * ## Sizing * - Complex dialogs can be sized with `flex="percentage"`, i.e. `flex="66"`. * - Default max-width is 80% of the `rootElement` or `parent`. * * @usage * *
*
* * Employee Alert! * *
*
* * Custom Dialog * *
*
* * Close Alert * *
*
* * Greet Employee * *
*
*
* * ### JavaScript: object syntax * * (function(angular, undefined){ * "use strict"; * * angular * .module('demoApp', ['ngMaterial']) * .controller('AppCtrl', AppController); * * function AppController($scope, $mdDialog) { * var alert; * $scope.showAlert = showAlert; * $scope.showDialog = showDialog; * $scope.items = [1, 2, 3]; * * // Internal method * function showAlert() { * alert = $mdDialog.alert({ * title: 'Attention', * content: 'This is an example of how easy dialogs can be!', * ok: 'Close' * }); * * $mdDialog * .show( alert ) * .finally(function() { * alert = undefined; * }); * } * * function showDialog($event) { * var parentEl = angular.element(document.body); * $mdDialog.show({ * parent: parentEl, * targetEvent: $event, * template: * '' + * ' '+ * ' '+ * ' '+ * '

Number {{item}}

' + * ' '+ * '
'+ * '
' + * '
' + * ' ' + * ' Close Dialog' + * ' ' + * '
' + * '
', * locals: { * items: $scope.items * }, * controller: DialogController * }); * function DialogController($scope, $mdDialog, items) { * $scope.items = items; * $scope.closeDialog = function() { * $mdDialog.hide(); * } * } * } * } * })(angular); *
* * ### JavaScript: promise API syntax, custom dialog template * * (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: * '' + * * ' Hello {{ employee }}!' + * * '
' + * ' ' + * ' Close Greeting' + * ' ' + * '
' + * '
', * 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. * } * } * * // Dialog #3 - Demonstrate use of ControllerAs and passing $scope to dialog * // Here we used ng-controller="GreetingController as vm" and * // $scope.vm === * * function showCustomGreeting() { * * $mdDialog.show({ * clickOutsideToClose: true, * * scope: $scope, // use parent scope in template * preserveScope: true, // do not forget this if use parent scope * // Since GreetingController is instantiated with ControllerAs syntax * // AND we are passing the parent '$scope' to the dialog, we MUST * // use 'vm.' in the template markup * * template: '' + * ' ' + * ' Hi There {{vm.employee}}' + * ' ' + * '', * * controller: function DialogController($scope, $mdDialog) { * $scope.closeDialog = function() { * $mdDialog.hide(); * } * } * }); * } * * } * * // Greeting controller used with the more complex 'showCustomGreeting()' custom dialog * * function GreetingController($scope, $mdDialog, employee) { * // Assigned from construction locals options... * $scope.employee = employee; * * $scope.closeDialog = function() { * // Easily hides most recent dialog shown... * // no specific instance reference is needed. * $mdDialog.hide(); * }; * } * * })(angular); *
*/ /** * @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 * - $mdDialogPreset#theme(string) - sets the theme of the dialog * */ /** * @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 * - $mdDialogPreset#theme(string) - sets the theme of the dialog * */ /** * @ngdoc method * @name $mdDialog#show * * @description * Show a dialog with the specified options. * * @param {object} optionsOrPreset Either provide an `$mdDialogPreset` returned from `alert()`, and * `confirm()`, 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. * - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified, * it will create a new isolate scope. * This scope will be destroyed when the dialog is removed unless `preserveScope` is set to true. * - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false * - `disableParentScroll` - `{boolean=}`: Whether to disable scrolling while the dialog is open. * Default true. * - `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 false. * - `escapeToClose` - `{boolean=}`: Whether the user can press escape to close the dialog. * Default true. * - `focusOnOpen` - `{boolean=}`: An option to override focus behavior on open. Only disable if * focusing some other way, as focus management is required for dialogs to be accessible. * Defaults to true. * - `controller` - `{string=}`: The controller to associate with the dialog. The controller * will be injected with the local `$mdDialog`, which passes along a scope for 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 * copied to the controller instead. * - `bindToController` - `bool`: bind the locals to the controller, instead of passing them in. * These values will not be available until after initialization. * - `resolve` - `{object=}`: Similar to locals, except it takes promises as values, and the * dialog 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. * * @returns {promise} A promise that is resolved when the dialog has been closed. */ /** * @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. * * @returns {promise} A promise that is resolved when the dialog has been closed. */ function MdDialogProvider($$interimElementProvider) { var alertDialogMethods = ['title', 'content', 'ariaLabel', 'ok']; advancedDialogOptions.$inject = ["$mdDialog", "$mdTheming"]; dialogDefaultOptions.$inject = ["$mdAria", "$document", "$mdUtil", "$mdConstant", "$mdTheming", "$mdDialog", "$timeout", "$rootElement", "$animate", "$$rAF", "$q"]; return $$interimElementProvider('$mdDialog') .setDefaults({ methods: ['disableParentScroll', 'hasBackdrop', 'clickOutsideToClose', 'escapeToClose', 'targetEvent', 'parent'], options: dialogDefaultOptions }) .addPreset('alert', { methods: ['title', 'content', 'ariaLabel', 'ok', 'theme'], options: advancedDialogOptions }) .addPreset('confirm', { methods: ['title', 'content', 'ariaLabel', 'ok', 'cancel', 'theme'], options: advancedDialogOptions }); /* @ngInject */ function advancedDialogOptions($mdDialog, $mdTheming) { return { template: [ '', '', '

{{ dialog.title }}

', '

{{ dialog.content }}

', '
', '
', '', '{{ dialog.cancel }}', '', '', '{{ dialog.ok }}', '', '
', '
' ].join(''), controller: function mdDialogCtrl() { this.hide = function() { $mdDialog.hide(true); }; this.abort = function() { $mdDialog.cancel(); }; }, controllerAs: 'dialog', bindToController: true, theme: $mdTheming.defaultTheme() }; } /* @ngInject */ function dialogDefaultOptions($mdAria, $document, $mdUtil, $mdConstant, $mdTheming, $mdDialog, $timeout, $rootElement, $animate, $$rAF, $q) { return { hasBackdrop: true, isolateScope: true, onShow: onShow, onRemove: onRemove, clickOutsideToClose: false, escapeToClose: true, targetEvent: null, focusOnOpen: true, disableParentScroll: true, transformTemplate: function(template) { return '
' + template + '
'; } }; function trapFocus(ev) { var dialog = document.querySelector('md-dialog'); if (dialog && !dialog.contains(ev.target)) { ev.stopImmediatePropagation(); dialog.focus(); } } // On show method for dialogs function onShow(scope, element, options) { angular.element($document[0].body).addClass('md-dialog-is-showing'); element = $mdUtil.extractElementByName(element, 'md-dialog'); // 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(); if (options.hasBackdrop) { // Fix for IE 10 var computeFrom = (options.parent[0] == $document[0].body && $document[0].documentElement && $document[0].documentElement.scrollTop) ? angular.element($document[0].documentElement) : options.parent; var parentOffset = computeFrom.prop('scrollTop'); options.backdrop = angular.element(''); options.backdrop.css('top', parentOffset +'px'); $mdTheming.inherit(options.backdrop, options.parent); $animate.enter(options.backdrop, options.parent); element.css('top', parentOffset +'px'); } var role = 'dialog', elementToFocus = closeButton; if (options.$type === 'alert') { role = 'alertdialog'; elementToFocus = element.find('md-dialog-content'); } configureAria(element.find('md-dialog'), role, options); document.addEventListener('focus', trapFocus, true); if (options.disableParentScroll) { options.lastOverflow = options.parent.css('overflow'); options.parent.css('overflow', 'hidden'); } return dialogPopIn( element, options.parent, options.popInTarget && options.popInTarget.length && options.popInTarget ) .then(function() { applyAriaToSiblings(element, true); 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(ev) { // Only close if we click the flex container outside the backdrop if (ev.target === element[0]) { $timeout($mdDialog.cancel); } }; element.on('click', options.dialogClickOutsideCallback); } if (options.focusOnOpen) { elementToFocus.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) { angular.element($document[0].body).removeClass('md-dialog-is-showing'); if (options.backdrop) { $animate.leave(options.backdrop); } if (options.disableParentScroll) { options.parent.css('overflow', options.lastOverflow); delete options.lastOverflow; } if (options.escapeToClose) { $rootElement.off('keyup', options.rootElementKeyupCallback); } if (options.clickOutsideToClose) { element.off('click', options.dialogClickOutsideCallback); } applyAriaToSiblings(element, false); document.removeEventListener('focus', trapFocus, true); return dialogPopOut( element, options.parent, options.popInTarget && options.popInTarget.length && options.popInTarget ).then(function() { element.remove(); options.popInTarget && options.popInTarget.focus(); }); } /** * Inject ARIA-specific attributes appropriate for Dialogs */ function configureAria(element, role, options) { element.attr({ 'role': role, 'tabIndex': '-1' }); var dialogContent = element.find('md-dialog-content'); if (dialogContent.length === 0){ dialogContent = element; } var dialogId = element.attr('id') || ('dialog_' + $mdUtil.nextUid()); dialogContent.attr('id', dialogId); element.attr('aria-describedby', dialogId); if (options.ariaLabel) { $mdAria.expect(element, 'aria-label', options.ariaLabel); } else { $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(' '); }); } } /** * Utility function to filter out raw DOM nodes */ function isNodeOneOf(elem, nodeTypeArray) { if (nodeTypeArray.indexOf(elem.nodeName) !== -1) { return true; } } /** * Walk DOM to apply or remove aria-hidden on sibling nodes * and parent sibling nodes * * Prevents screen reader interaction behind modal window * on swipe interfaces */ function applyAriaToSiblings(element, value) { var attribute = 'aria-hidden'; // get raw DOM node element = element[0]; function walkDOM(element) { while (element.parentNode) { if (element === document.body) { return; } var children = element.parentNode.children; for (var i = 0; i < children.length; i++) { // skip over child if it is an ascendant of the dialog // or a script or style tag if (element !== children[i] && !isNodeOneOf(children[i], ['SCRIPT', 'STYLE'])) { children[i].setAttribute(attribute, value); } } walkDOM(element = element.parentNode); } } walkDOM(element); } function dialogPopIn(container, parentElement, clickElement) { var dialogEl = container.find('md-dialog'); parentElement.append(container); transformToClickElement(dialogEl, clickElement); $$rAF(function() { dialogEl.addClass('transition-in') .css($mdConstant.CSS.TRANSFORM, ''); }); return $mdUtil.transitionEndPromise(dialogEl); } function dialogPopOut(container, parentElement, clickElement) { var dialogEl = container.find('md-dialog'); dialogEl.addClass('transition-out').removeClass('transition-in'); transformToClickElement(dialogEl, clickElement); return $mdUtil.transitionEndPromise(dialogEl); } function transformToClickElement(dialogEl, clickElement) { if (clickElement) { var clickRect = clickElement[0].getBoundingClientRect(); var dialogRect = dialogEl[0].getBoundingClientRect(); var scaleX = Math.min(0.5, clickRect.width / dialogRect.width); var scaleY = Math.min(0.5, clickRect.height / dialogRect.height); dialogEl.css($mdConstant.CSS.TRANSFORM, 'translate3d(' + (-dialogRect.left + clickRect.left + clickRect.width/2 - dialogRect.width/2) + 'px,' + (-dialogRect.top + clickRect.top + clickRect.height/2 - dialogRect.height/2) + 'px,' + '0) scale(' + scaleX + ',' + scaleY + ')' ); } } function dialogTransitionEnd(dialogEl) { var deferred = $q.defer(); dialogEl.on($mdConstant.CSS.TRANSITIONEND, finished); function finished(ev) { //Make sure this transitionend didn't bubble up from a child if (ev.target === dialogEl[0]) { dialogEl.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); /** * @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=} md-inset Add this attribute to activate the inset divider style. * @usage * * * * * * */ function MdDividerDirective($mdTheming) { return { restrict: 'E', link: $mdTheming }; } MdDividerDirective.$inject = ["$mdTheming"]; })(); (function(){ "use strict"; /** * @ngdoc module * @name material.components.gridList */ angular.module('material.components.gridList', ['material.core']) .directive('mdGridList', GridListDirective) .directive('mdGridTile', GridTileDirective) .directive('mdGridTileFooter', GridTileCaptionDirective) .directive('mdGridTileHeader', GridTileCaptionDirective) .factory('$mdGridLayout', GridLayoutFactory); /** * @ngdoc directive * @name mdGridList * @module material.components.gridList * @restrict E * @description * Grid lists are an alternative to standard list views. Grid lists are distinct * from grids used for layouts and other visual presentations. * * A grid list is best suited to presenting a homogenous data type, typically * images, and is optimized for visual comprehension and differentiating between * like data types. * * A grid list is a continuous element consisting of tessellated, regular * subdivisions called cells that contain tiles (`md-grid-tile`). * * Concept of grid explained visually * Grid concepts legend * * Cells are arrayed vertically and horizontally within the grid. * * Tiles hold content and can span one or more cells vertically or horizontally. * * ### Responsive Attributes * * The `md-grid-list` directive supports "responsive" attributes, which allow * different `md-cols`, `md-gutter` and `md-row-height` values depending on the * currently matching media query (as defined in `$mdConstant.MEDIA`). * * In order to set a responsive attribute, first define the fallback value with * the standard attribute name, then add additional attributes with the * following convention: `{base-attribute-name}-{media-query-name}="{value}"` * (ie. `md-cols-lg="8"`) * * @param {number} md-cols Number of columns in the grid. * @param {string} md-row-height One of *
    *
  • CSS length - Fixed height rows (eg. `8px` or `1rem`)
  • *
  • `{width}:{height}` - Ratio of width to height (eg. * `md-row-height="16:9"`)
  • *
  • `"fit"` - Height will be determined by subdividing the available * height by the number of rows
  • *
* @param {string=} md-gutter The amount of space between tiles in CSS units * (default 1px) * @param {expression=} md-on-layout Expression to evaluate after layout. Event * object is available as `$event`, and contains performance information. * * @usage * Basic: * * * * * * * Fixed-height rows: * * * * * * * Fit rows: * * * * * * * Using responsive attributes: * * * * * */ function GridListDirective($interpolate, $mdConstant, $mdGridLayout, $mdMedia) { return { restrict: 'E', controller: GridListController, scope: { mdOnLayout: '&' }, link: postLink }; function postLink(scope, element, attrs, ctrl) { // Apply semantics element.attr('role', 'list'); // Provide the controller with a way to trigger layouts. ctrl.layoutDelegate = layoutDelegate; var invalidateLayout = angular.bind(ctrl, ctrl.invalidateLayout), unwatchAttrs = watchMedia(); scope.$on('$destroy', unwatchMedia); /** * Watches for changes in media, invalidating layout as necessary. */ function watchMedia() { for (var mediaName in $mdConstant.MEDIA) { $mdMedia(mediaName); // initialize $mdMedia.getQuery($mdConstant.MEDIA[mediaName]) .addListener(invalidateLayout); } return $mdMedia.watchResponsiveAttributes( ['md-cols', 'md-row-height'], attrs, layoutIfMediaMatch); } function unwatchMedia() { ctrl.layoutDelegate = angular.noop; unwatchAttrs(); for (var mediaName in $mdConstant.MEDIA) { $mdMedia.getQuery($mdConstant.MEDIA[mediaName]) .removeListener(invalidateLayout); } } /** * Performs grid layout if the provided mediaName matches the currently * active media type. */ function layoutIfMediaMatch(mediaName) { if (mediaName == null) { // TODO(shyndman): It would be nice to only layout if we have // instances of attributes using this media type ctrl.invalidateLayout(); } else if ($mdMedia(mediaName)) { ctrl.invalidateLayout(); } } var lastLayoutProps; /** * Invokes the layout engine, and uses its results to lay out our * tile elements. * * @param {boolean} tilesInvalidated Whether tiles have been * added/removed/moved since the last layout. This is to avoid situations * where tiles are replaced with properties identical to their removed * counterparts. */ function layoutDelegate(tilesInvalidated) { var tiles = getTileElements(); var props = { tileSpans: getTileSpans(tiles), colCount: getColumnCount(), rowMode: getRowMode(), rowHeight: getRowHeight(), gutter: getGutter() }; if (!tilesInvalidated && angular.equals(props, lastLayoutProps)) { return; } var performance = $mdGridLayout(props.colCount, props.tileSpans, tiles) .map(function(tilePositions, rowCount) { return { grid: { element: element, style: getGridStyle(props.colCount, rowCount, props.gutter, props.rowMode, props.rowHeight) }, tiles: tilePositions.map(function(ps, i) { return { element: angular.element(tiles[i]), style: getTileStyle(ps.position, ps.spans, props.colCount, props.rowCount, props.gutter, props.rowMode, props.rowHeight) } }) } }) .reflow() .performance(); // Report layout scope.mdOnLayout({ $event: { performance: performance } }); lastLayoutProps = props; } // Use $interpolate to do some simple string interpolation as a convenience. var startSymbol = $interpolate.startSymbol(); var endSymbol = $interpolate.endSymbol(); // Returns an expression wrapped in the interpolator's start and end symbols. function expr(exprStr) { return startSymbol + exprStr + endSymbol; } // The amount of space a single 1x1 tile would take up (either width or height), used as // a basis for other calculations. This consists of taking the base size percent (as would be // if evenly dividing the size between cells), and then subtracting the size of one gutter. // However, since there are no gutters on the edges, each tile only uses a fration // (gutterShare = numGutters / numCells) of the gutter size. (Imagine having one gutter per // tile, and then breaking up the extra gutter on the edge evenly among the cells). var UNIT = $interpolate(expr('share') + '% - (' + expr('gutter') + ' * ' + expr('gutterShare') + ')'); // The horizontal or vertical position of a tile, e.g., the 'top' or 'left' property value. // The position comes the size of a 1x1 tile plus gutter for each previous tile in the // row/column (offset). var POSITION = $interpolate('calc((' + expr('unit') + ' + ' + expr('gutter') + ') * ' + expr('offset') + ')'); // The actual size of a tile, e.g., width or height, taking rowSpan or colSpan into account. // This is computed by multiplying the base unit by the rowSpan/colSpan, and then adding back // in the space that the gutter would normally have used (which was already accounted for in // the base unit calculation). var DIMENSION = $interpolate('calc((' + expr('unit') + ') * ' + expr('span') + ' + (' + expr('span') + ' - 1) * ' + expr('gutter') + ')'); /** * Gets the styles applied to a tile element described by the given parameters. * @param {{row: number, col: number}} position The row and column indices of the tile. * @param {{row: number, col: number}} spans The rowSpan and colSpan of the tile. * @param {number} colCount The number of columns. * @param {number} rowCount The number of rows. * @param {string} gutter The amount of space between tiles. This will be something like * '5px' or '2em'. * @param {string} rowMode The row height mode. Can be one of: * 'fixed': all rows have a fixed size, given by rowHeight, * 'ratio': row height defined as a ratio to width, or * 'fit': fit to the grid-list element height, divinding evenly among rows. * @param {string|number} rowHeight The height of a row. This is only used for 'fixed' mode and * for 'ratio' mode. For 'ratio' mode, this is the *ratio* of width-to-height (e.g., 0.75). * @returns {Object} Map of CSS properties to be applied to the style element. Will define * values for top, left, width, height, marginTop, and paddingTop. */ function getTileStyle(position, spans, colCount, rowCount, gutter, rowMode, rowHeight) { // TODO(shyndman): There are style caching opportunities here. // Percent of the available horizontal space that one column takes up. var hShare = (1 / colCount) * 100; // Fraction of the gutter size that each column takes up. var hGutterShare = (colCount - 1) / colCount; // Base horizontal size of a column. var hUnit = UNIT({share: hShare, gutterShare: hGutterShare, gutter: gutter}); // The width and horizontal position of each tile is always calculated the same way, but the // height and vertical position depends on the rowMode. var style = { left: POSITION({ unit: hUnit, offset: position.col, gutter: gutter }), width: DIMENSION({ unit: hUnit, span: spans.col, gutter: gutter }), // resets paddingTop: '', marginTop: '', top: '', height: '' }; switch (rowMode) { case 'fixed': // In fixed mode, simply use the given rowHeight. style.top = POSITION({ unit: rowHeight, offset: position.row, gutter: gutter }); style.height = DIMENSION({ unit: rowHeight, span: spans.row, gutter: gutter }); break; case 'ratio': // Percent of the available vertical space that one row takes up. Here, rowHeight holds // the ratio value. For example, if the width:height ratio is 4:3, rowHeight = 1.333. var vShare = hShare / rowHeight; // Base veritcal size of a row. var vUnit = UNIT({ share: vShare, gutterShare: hGutterShare, gutter: gutter }); // padidngTop and marginTop are used to maintain the given aspect ratio, as // a percentage-based value for these properties is applied to the *width* of the // containing block. See http://www.w3.org/TR/CSS2/box.html#margin-properties style.paddingTop = DIMENSION({ unit: vUnit, span: spans.row, gutter: gutter}); style.marginTop = POSITION({ unit: vUnit, offset: position.row, gutter: gutter }); break; case 'fit': // Fraction of the gutter size that each column takes up. var vGutterShare = (rowCount - 1) / rowCount; // Percent of the available vertical space that one row takes up. var vShare = (1 / rowCount) * 100; // Base vertical size of a row. var vUnit = UNIT({share: vShare, gutterShare: vGutterShare, gutter: gutter}); style.top = POSITION({unit: vUnit, offset: position.row, gutter: gutter}); style.height = DIMENSION({unit: vUnit, span: spans.row, gutter: gutter}); break; } return style; } function getGridStyle(colCount, rowCount, gutter, rowMode, rowHeight) { var style = { height: '', paddingBottom: '' }; switch(rowMode) { case 'fixed': style.height = DIMENSION({ unit: rowHeight, span: rowCount, gutter: gutter }); break; case 'ratio': // rowHeight is width / height var hGutterShare = colCount === 1 ? 0 : (colCount - 1) / colCount, hShare = (1 / colCount) * 100, vShare = hShare * (1 / rowHeight), vUnit = UNIT({ share: vShare, gutterShare: hGutterShare, gutter: gutter }); style.paddingBottom = DIMENSION({ unit: vUnit, span: rowCount, gutter: gutter}); break; case 'fit': // noop, as the height is user set break; } return style; } function getTileElements() { return [].filter.call(element.children(), function(ele) { return ele.tagName == 'MD-GRID-TILE'; }); } /** * Gets an array of objects containing the rowspan and colspan for each tile. * @returns {Array<{row: number, col: number}>} */ function getTileSpans(tileElements) { return [].map.call(tileElements, function(ele) { var ctrl = angular.element(ele).controller('mdGridTile'); return { row: parseInt( $mdMedia.getResponsiveAttribute(ctrl.$attrs, 'md-rowspan'), 10) || 1, col: parseInt( $mdMedia.getResponsiveAttribute(ctrl.$attrs, 'md-colspan'), 10) || 1 }; }); } function getColumnCount() { var colCount = parseInt($mdMedia.getResponsiveAttribute(attrs, 'md-cols'), 10); if (isNaN(colCount)) { throw 'md-grid-list: md-cols attribute was not found, or contained a non-numeric value'; } return colCount; } function getGutter() { return applyDefaultUnit($mdMedia.getResponsiveAttribute(attrs, 'md-gutter') || 1); } function getRowHeight() { var rowHeight = $mdMedia.getResponsiveAttribute(attrs, 'md-row-height'); switch (getRowMode()) { case 'fixed': return applyDefaultUnit(rowHeight); case 'ratio': var whRatio = rowHeight.split(':'); return parseFloat(whRatio[0]) / parseFloat(whRatio[1]); case 'fit': return 0; // N/A } } function getRowMode() { var rowHeight = $mdMedia.getResponsiveAttribute(attrs, 'md-row-height'); if (rowHeight == 'fit') { return 'fit'; } else if (rowHeight.indexOf(':') !== -1) { return 'ratio'; } else { return 'fixed'; } } function applyDefaultUnit(val) { return /\D$/.test(val) ? val : val + 'px'; } } } GridListDirective.$inject = ["$interpolate", "$mdConstant", "$mdGridLayout", "$mdMedia"]; /* @ngInject */ function GridListController($timeout) { this.layoutInvalidated = false; this.tilesInvalidated = false; this.$timeout_ = $timeout; this.layoutDelegate = angular.noop; } GridListController.$inject = ["$timeout"]; GridListController.prototype = { invalidateTiles: function() { this.tilesInvalidated = true; this.invalidateLayout(); }, invalidateLayout: function() { if (this.layoutInvalidated) { return; } this.layoutInvalidated = true; this.$timeout_(angular.bind(this, this.layout)); }, layout: function() { try { this.layoutDelegate(this.tilesInvalidated); } finally { this.layoutInvalidated = false; this.tilesInvalidated = false; } } }; /* @ngInject */ function GridLayoutFactory($mdUtil) { var defaultAnimator = GridTileAnimator; /** * Set the reflow animator callback */ GridLayout.animateWith = function(customAnimator) { defaultAnimator = !angular.isFunction(customAnimator) ? GridTileAnimator : customAnimator; }; return GridLayout; /** * Publish layout function */ function GridLayout(colCount, tileSpans) { var self, layoutInfo, gridStyles, layoutTime, mapTime, reflowTime; layoutTime = $mdUtil.time(function() { layoutInfo = calculateGridFor(colCount, tileSpans); }); return self = { /** * An array of objects describing each tile's position in the grid. */ layoutInfo: function() { return layoutInfo; }, /** * Maps grid positioning to an element and a set of styles using the * provided updateFn. */ map: function(updateFn) { mapTime = $mdUtil.time(function() { var info = self.layoutInfo(); gridStyles = updateFn(info.positioning, info.rowCount); }); return self; }, /** * Default animator simply sets the element.css( ). An alternate * animator can be provided as an argument. The function has the following * signature: * * function({grid: {element: JQLite, style: Object}, tiles: Array<{element: JQLite, style: Object}>) */ reflow: function(animatorFn) { reflowTime = $mdUtil.time(function() { var animator = animatorFn || defaultAnimator; animator(gridStyles.grid, gridStyles.tiles); }); return self; }, /** * Timing for the most recent layout run. */ performance: function() { return { tileCount: tileSpans.length, layoutTime: layoutTime, mapTime: mapTime, reflowTime: reflowTime, totalTime: layoutTime + mapTime + reflowTime }; } }; } /** * Default Gridlist animator simple sets the css for each element; * NOTE: any transitions effects must be manually set in the CSS. * e.g. * * md-grid-tile { * transition: all 700ms ease-out 50ms; * } * */ function GridTileAnimator(grid, tiles) { grid.element.css(grid.style); tiles.forEach(function(t) { t.element.css(t.style); }) } /** * Calculates the positions of tiles. * * The algorithm works as follows: * An Array with length colCount (spaceTracker) keeps track of * available tiling positions, where elements of value 0 represents an * empty position. Space for a tile is reserved by finding a sequence of * 0s with length <= than the tile's colspan. When such a space has been * found, the occupied tile positions are incremented by the tile's * rowspan value, as these positions have become unavailable for that * many rows. * * If the end of a row has been reached without finding space for the * tile, spaceTracker's elements are each decremented by 1 to a minimum * of 0. Rows are searched in this fashion until space is found. */ function calculateGridFor(colCount, tileSpans) { var curCol = 0, curRow = 0, spaceTracker = newSpaceTracker(); return { positioning: tileSpans.map(function(spans, i) { return { spans: spans, position: reserveSpace(spans, i) }; }), rowCount: curRow + Math.max.apply(Math, spaceTracker) }; function reserveSpace(spans, i) { if (spans.col > colCount) { throw 'md-grid-list: Tile at position ' + i + ' has a colspan ' + '(' + spans.col + ') that exceeds the column count ' + '(' + colCount + ')'; } var start = 0, end = 0; // TODO(shyndman): This loop isn't strictly necessary if you can // determine the minimum number of rows before a space opens up. To do // this, recognize that you've iterated across an entire row looking for // space, and if so fast-forward by the minimum rowSpan count. Repeat // until the required space opens up. while (end - start < spans.col) { if (curCol >= colCount) { nextRow(); continue; } start = spaceTracker.indexOf(0, curCol); if (start === -1 || (end = findEnd(start + 1)) === -1) { start = end = 0; nextRow(); continue; } curCol = end + 1; } adjustRow(start, spans.col, spans.row); curCol = start + spans.col; return { col: start, row: curRow }; } function nextRow() { curCol = 0; curRow++; adjustRow(0, colCount, -1); // Decrement row spans by one } function adjustRow(from, cols, by) { for (var i = from; i < from + cols; i++) { spaceTracker[i] = Math.max(spaceTracker[i] + by, 0); } } function findEnd(start) { var i; for (i = start; i < spaceTracker.length; i++) { if (spaceTracker[i] !== 0) { return i; } } if (i === spaceTracker.length) { return i; } } function newSpaceTracker() { var tracker = []; for (var i = 0; i < colCount; i++) { tracker.push(0); } return tracker; } } } GridLayoutFactory.$inject = ["$mdUtil"]; /** * @ngdoc directive * @name mdGridTile * @module material.components.gridList * @restrict E * @description * Tiles contain the content of an `md-grid-list`. They span one or more grid * cells vertically or horizontally, and use `md-grid-tile-{footer,header}` to * display secondary content. * * ### Responsive Attributes * * The `md-grid-tile` directive supports "responsive" attributes, which allow * different `md-rowspan` and `md-colspan` values depending on the currently * matching media query (as defined in `$mdConstant.MEDIA`). * * In order to set a responsive attribute, first define the fallback value with * the standard attribute name, then add additional attributes with the * following convention: `{base-attribute-name}-{media-query-name}="{value}"` * (ie. `md-colspan-sm="4"`) * * @param {number=} md-colspan The number of columns to span (default 1). Cannot * exceed the number of columns in the grid. Supports interpolation. * @param {number=} md-rowspan The number of rows to span (default 1). Supports * interpolation. * * @usage * With header: * * * *

This is a header

*
*
*
* * With footer: * * * *

This is a footer

*
*
*
* * Spanning multiple rows/columns: * * * * * * Responsive attributes: * * * * */ function GridTileDirective($mdMedia) { return { restrict: 'E', require: '^mdGridList', template: '
', transclude: true, scope: {}, // Simple controller that exposes attributes to the grid directive controller: ["$attrs", function($attrs) { this.$attrs = $attrs; }], link: postLink }; function postLink(scope, element, attrs, gridCtrl) { // Apply semantics element.attr('role', 'listitem'); // If our colspan or rowspan changes, trigger a layout var unwatchAttrs = $mdMedia.watchResponsiveAttributes(['md-colspan', 'md-rowspan'], attrs, angular.bind(gridCtrl, gridCtrl.invalidateLayout)); // Tile registration/deregistration gridCtrl.invalidateTiles(); scope.$on('$destroy', function() { unwatchAttrs(); gridCtrl.invalidateLayout(); }); if (angular.isDefined(scope.$parent.$index)) { scope.$watch(function() { return scope.$parent.$index; }, function indexChanged(newIdx, oldIdx) { if (newIdx === oldIdx) { return; } gridCtrl.invalidateTiles(); }); } } } GridTileDirective.$inject = ["$mdMedia"]; function GridTileCaptionDirective() { return { template: '
', transclude: true }; } })(); (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 `` directive is an markup element useful for showing an icon based on a font-icon * or a SVG. Icons are view-only elements that should not be used directly as buttons; instead nest a `` * inside a `md-button` to add hover and click features. * * When using SVGs, both external SVGs (via URLs) or sets of SVGs [from icon sets] can be * easily loaded and used.When use font-icons, developers must following three (3) simple steps: * *
    *
  1. Load the font library. e.g.
    * <link href="https://fonts.googleapis.com/icon?family=Material+Icons" * rel="stylesheet"> *
  2. *
  3. Use either (a) font-icon class names or (b) font ligatures to render the font glyph by using its textual name
  4. *
  5. Use <md-icon md-font-icon="classname" /> or
    * use <md-icon md-font-set="font library classname or alias"> textual_name </md-icon> or
    * use <md-icon md-font-set="font library classname or alias"> numerical_character_reference </md-icon> *
  6. *
* * Full details for these steps can be found: * *
    *
  • http://google.github.io/material-design-icons/
  • *
  • http://google.github.io/material-design-icons/#icon-font-for-the-web
  • *
* * The Material Design icon style .material-icons and the icon font references are published in * Material Design Icons: * *
    *
  • http://www.google.com/design/icons/
  • *
  • https://www.google.com/design/icons/#ic_accessibility
  • *
* *

Material Design Icons

* Using the Material Design Icon-Selector, developers can easily and quickly search for a Material Design font-icon and * determine its textual name and character reference code. Click on any icon to see the slide-up information * panel with details regarding a SVG download or information on the font-icon usage. * * * * * * * Click on the image above to link to the * Material Design Icon-Selector. * * * @param {string} md-font-icon Name of CSS icon associated with the font-face will be used * to render the icon. Requires the fonts and the named CSS styles to be preloaded. * @param {string} md-font-set CSS style name associated with the font library; which will be assigned as * the class for the font-icon ligature. This value may also be an alias that is used to lookup the classname; * internally use `$mdIconProvider.fontSet()` to determine the style name. * @param {string} md-svg-src URL [or expression ] used to load, cache, and display an external SVG. * @param {string} md-svg-icon Name used for lookup of the icon from the internal cache; interpolated strings or * expressions may also be used. Specific set names can be used with the syntax `:`.

* To use icon sets, developers are required to pre-register the sets using the `$mdIconProvider` service. * @param {string=} aria-label Labels icon for accessibility. If an empty string is provided, icon * will be hidden from accessibility layer with `aria-hidden="true"`. If there's no aria-label on the icon * nor a label on the parent element, a warning will be logged to the console. * * @usage * When using SVGs: * * * * * * * * * * * * Use the $mdIconProvider to configure your application with * svg iconsets. * * * angular.module('appSvgIconSets', ['ngMaterial']) * .controller('DemoCtrl', function($scope) {}) * .config(function($mdIconProvider) { * $mdIconProvider * .iconSet('social', 'img/icons/sets/social-icons.svg', 24) * .defaultIconSet('img/icons/sets/core-icons.svg', 24); * }); * * * * When using Font Icons with classnames: * * * * * * * * When using Material Font Icons with ligatures: * * * * face * face * face * #xE87C; * * * When using other Font-Icon libraries: * * * // Specify a font-icon style alias * angular.config(function($mdIconProvider) { * $mdIconProvider.fontSet('fa', 'fontawesome'); * }); * * * * email * * */ function mdIconDirective($mdIcon, $mdTheming, $mdAria, $interpolate ) { return { scope: { fontSet : '@mdFontSet', fontIcon: '@mdFontIcon', svgIcon : '@mdSvgIcon', svgSrc : '@mdSvgSrc' }, restrict: 'E', transclude:true, template: getTemplate, link: postLink }; function getTemplate(element, attr) { var isEmptyAttr = function(key) { return angular.isDefined(attr[key]) ? attr[key].length == 0 : false }, hasAttrValue = function(key) { return attr[key] && attr[key].length > 0; }, attrValue = function(key) { return hasAttrValue(key) ? attr[key] : '' }; // If using the deprecated md-font-icon API // If using ligature-based font-icons, transclude the ligature or NRCs var tmplFontIcon = ''; var tmplFontSet = ''; var tmpl = hasAttrValue('mdSvgIcon') ? '' : hasAttrValue('mdSvgSrc') ? '' : isEmptyAttr('mdFontIcon') ? '' : hasAttrValue('mdFontIcon') ? tmplFontIcon : tmplFontSet; // If available, lookup the fontSet style and add to the list of classnames // NOTE: Material Icons expects classnames like `.material-icons.md-48` instead of `.material-icons .md-48` var names = (tmpl == tmplFontSet) ? $mdIcon.fontSet(attrValue('mdFontSet')) + ' ' : ''; names = (names + attrValue('class')).trim(); return $interpolate( tmpl )({ classNames: names }); } /** * Directive postLink * Supports embedded SVGs, font-icons, & external SVGs */ function postLink(scope, element, attr) { $mdTheming(element); // If using a font-icon, then the textual name of the icon itself // provides the aria-label. var label = attr.alt || scope.fontIcon || scope.svgIcon || element.text(); var attrName = attr.$normalize(attr.$attr.mdSvgIcon || attr.$attr.mdSvgSrc || ''); if ( !attr['aria-label'] ) { if (label != '' && !parentsHaveText() ) { $mdAria.expect(element, 'aria-label', label); $mdAria.expect(element, 'role', 'img'); } else if ( !element.text() ) { // If not a font-icon with ligature, then // hide from the accessibility layer. $mdAria.expect(element, 'aria-hidden', 'true'); } } if (attrName) { // Use either pre-configured SVG or URL source, respectively. attr.$observe(attrName, function(attrVal) { element.empty(); if (attrVal) { $mdIcon(attrVal).then(function(svg) { element.append(svg); }); } }); } function parentsHaveText() { var parent = element.parent(); if (parent.attr('aria-label') || parent.text()) { return true; } else if(parent.parent().attr('aria-label') || parent.parent().text()) { return true; } return false; } } } mdIconDirective.$inject = ["$mdIcon", "$mdTheming", "$mdAria", "$interpolate"]; })(); (function(){ "use strict"; angular .module('material.components.icon' ) .provider('$mdIcon', MdIconProvider); /** * @ngdoc service * @name $mdIconProvider * @module material.components.icon * * @description * `$mdIconProvider` is used only to register icon IDs with URLs. These configuration features allow * icons and icon sets to be pre-registered and associated with source URLs **before** the `` * directives are compiled. * * If using font-icons, the developer is repsonsible for loading the fonts. * * If using SVGs, loading of the actual svg files are deferred to on-demand requests and are loaded * internally by the `$mdIcon` service using the `$http` service. When an SVG is requested by name/ID, * the `$mdIcon` service searches its registry for the associated source URL; * that URL is used to on-demand load and parse the SVG dynamically. * * @usage * * app.config(function($mdIconProvider) { * * // Configure URLs for icons specified by [set:]id. * * $mdIconProvider * .defaultFontSet( 'fontawesome' ) * .defaultIconSet('my/app/icons.svg') // Register a default set of SVG icons * .iconSet('social', 'my/app/social.svg') // Register a named icon set of SVGs * .icon('android', 'my/app/android.svg') // Register a specific icon (by name) * .icon('work:chair', 'my/app/chair.svg'); // Register icon in a specific set * }); * * * SVG icons and icon sets can be easily pre-loaded and cached using either (a) a build process or (b) a runtime * **startup** process (shown below): * * * app.config(function($mdIconProvider) { * * // Register a default set of SVG icon definitions * $mdIconProvider.defaultIconSet('my/app/icons.svg') * * }) * .run(function($http, $templateCache){ * * // Pre-fetch icons sources by URL and cache in the $templateCache... * // subsequent $http calls will look there first. * * var urls = [ 'imy/app/icons.svg', 'img/icons/android.svg']; * * angular.forEach(urls, function(url) { * $http.get(url, {cache: $templateCache}); * }); * * }); * * * * NOTE: the loaded SVG data is subsequently cached internally for future requests. * */ /** * @ngdoc method * @name $mdIconProvider#icon * * @description * Register a source URL for a specific icon name; the name may include optional 'icon set' name prefix. * These icons will later be retrieved from the cache using `$mdIcon( )` * * @param {string} id Icon name/id used to register the icon * @param {string} url specifies the external location for the data file. Used internally by `$http` to load the * data or as part of the lookup in `$templateCache` if pre-loading was configured. * @param {string=} iconSize Number indicating the width and height of the icons in the set. All icons * in the icon set must be the same size. Default size is 24. * * @returns {obj} an `$mdIconProvider` reference; used to support method call chains for the API * * @usage * * app.config(function($mdIconProvider) { * * // Configure URLs for icons specified by [set:]id. * * $mdIconProvider * .icon('android', 'my/app/android.svg') // Register a specific icon (by name) * .icon('work:chair', 'my/app/chair.svg'); // Register icon in a specific set * }); * * */ /** * @ngdoc method * @name $mdIconProvider#iconSet * * @description * Register a source URL for a 'named' set of icons; group of SVG definitions where each definition * has an icon id. Individual icons can be subsequently retrieved from this cached set using * `$mdIcon(:)` * * @param {string} id Icon name/id used to register the iconset * @param {string} url specifies the external location for the data file. Used internally by `$http` to load the * data or as part of the lookup in `$templateCache` if pre-loading was configured. * @param {string=} iconSize Number indicating the width and height of the icons in the set. All icons * in the icon set must be the same size. Default size is 24. * * @returns {obj} an `$mdIconProvider` reference; used to support method call chains for the API * * * @usage * * app.config(function($mdIconProvider) { * * // Configure URLs for icons specified by [set:]id. * * $mdIconProvider * .iconSet('social', 'my/app/social.svg') // Register a named icon set * }); * * */ /** * @ngdoc method * @name $mdIconProvider#defaultIconSet * * @description * Register a source URL for the default 'named' set of icons. Unless explicitly registered, * subsequent lookups of icons will failover to search this 'default' icon set. * Icon can be retrieved from this cached, default set using `$mdIcon()` * * @param {string} url specifies the external location for the data file. Used internally by `$http` to load the * data or as part of the lookup in `$templateCache` if pre-loading was configured. * @param {string=} iconSize Number indicating the width and height of the icons in the set. All icons * in the icon set must be the same size. Default size is 24. * * @returns {obj} an `$mdIconProvider` reference; used to support method call chains for the API * * @usage * * app.config(function($mdIconProvider) { * * // Configure URLs for icons specified by [set:]id. * * $mdIconProvider * .defaultIconSet( 'my/app/social.svg' ) // Register a default icon set * }); * * */ /** * @ngdoc method * @name $mdIconProvider#defaultFontSet * * @description * When using Font-Icons, Angular Material assumes the the Material Design icons will be used and automatically * configures the default font-set == 'material-icons'. Note that the font-set references the font-icon library * class style that should be applied to the ``. * * Configuring the default means that the attributes * `md-font-set="material-icons"` or `class="material-icons"` do not need to be explicitly declared on the * `` markup. For example: * * ` face ` * will render as * ` face `, and * * ` face ` * will render as * ` face ` * * @param {string} name of the font-library style that should be applied to the md-icon DOM element * * @usage * * app.config(function($mdIconProvider) { * $mdIconProvider.defaultFontSet( 'fontawesome' ); * }); * * */ /** * @ngdoc method * @name $mdIconProvider#defaultIconSize * * @description * While `` markup can also be style with sizing CSS, this method configures * the default width **and** height used for all icons; unless overridden by specific CSS. * The default sizing is (24px, 24px). * * @param {string} iconSize Number indicating the width and height of the icons in the set. All icons * in the icon set must be the same size. Default size is 24. * * @returns {obj} an `$mdIconProvider` reference; used to support method call chains for the API * * @usage * * app.config(function($mdIconProvider) { * * // Configure URLs for icons specified by [set:]id. * * $mdIconProvider * .defaultIconSize(36) // Register a default icon size (width == height) * }); * * */ var config = { defaultIconSize: 24, defaultFontSet: 'material-icons', fontSets : [ ] }; function MdIconProvider() { } MdIconProvider.prototype = { icon : function icon(id, url, iconSize) { if ( id.indexOf(':') == -1 ) id = '$default:' + id; config[id] = new ConfigurationItem(url, iconSize ); return this; }, iconSet : function iconSet(id, url, iconSize) { config[id] = new ConfigurationItem(url, iconSize ); return this; }, defaultIconSet : function defaultIconSet(url, iconSize) { var setName = '$default'; if ( !config[setName] ) { config[setName] = new ConfigurationItem(url, iconSize ); } config[setName].iconSize = iconSize || config.defaultIconSize; return this; }, /** * Register an alias name associated with a font-icon library style ; */ fontSet : function fontSet(alias, className) { config.fontSets.push({ alias : alias, fontSet : className || alias }); }, /** * Specify a default style name associated with a font-icon library * fallback to Material Icons. * */ defaultFontSet : function defaultFontSet(className) { config.defaultFontSet = !className ? '' : className; return this; }, defaultIconSize : function defaultIconSize(iconSize) { config.defaultIconSize = iconSize; return this; }, preloadIcons: function ($templateCache) { var iconProvider = this; var svgRegistry = [ { id : 'md-tabs-arrow', url: 'md-tabs-arrow.svg', svg: '' }, { id : 'md-close', url: 'md-close.svg', svg: '' }, { id: 'md-cancel', url: 'md-cancel.svg', svg: '' }, { id: 'md-menu', url: 'md-menu.svg', svg: '' }, { id: 'md-toggle-arrow', url: 'md-toggle-arrow-svg', svg: '' } ]; svgRegistry.forEach(function(asset){ iconProvider.icon(asset.id, asset.url); $templateCache.put(asset.url, asset.svg); }); }, $get : ['$http', '$q', '$log', '$templateCache', function($http, $q, $log, $templateCache) { this.preloadIcons($templateCache); return MdIconService(config, $http, $q, $log, $templateCache); }] }; /** * Configuration item stored in the Icon registry; used for lookups * to load if not already cached in the `loaded` cache */ function ConfigurationItem(url, iconSize) { this.url = url; this.iconSize = iconSize || config.defaultIconSize; } /** * @ngdoc service * @name $mdIcon * @module material.components.icon * * @description * The `$mdIcon` service is a function used to lookup SVG icons. * * @param {string} id Query value for a unique Id or URL. If the argument is a URL, then the service will retrieve the icon element * from its internal cache or load the icon and cache it first. If the value is not a URL-type string, then an ID lookup is * performed. The Id may be a unique icon ID or may include an iconSet ID prefix. * * For the **id** query to work properly, this means that all id-to-URL mappings must have been previously configured * using the `$mdIconProvider`. * * @returns {obj} Clone of the initial SVG DOM element; which was created from the SVG markup in the SVG data file. * * @usage * * function SomeDirective($mdIcon) { * * // See if the icon has already been loaded, if not * // then lookup the icon from the registry cache, load and cache * // it for future requests. * // NOTE: ID queries require configuration with $mdIconProvider * * $mdIcon('android').then(function(iconEl) { element.append(iconEl); }); * $mdIcon('work:chair').then(function(iconEl) { element.append(iconEl); }); * * // Load and cache the external SVG using a URL * * $mdIcon('img/icons/android.svg').then(function(iconEl) { * element.append(iconEl); * }); * }; * * * NOTE: The ` ` directive internally uses the `$mdIcon` service to query, loaded, and instantiate * SVG DOM elements. */ function MdIconService(config, $http, $q, $log, $templateCache) { var iconCache = {}; var urlRegex = /[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?/i; Icon.prototype = { clone : cloneSVG, prepare: prepareAndStyle }; getIcon.fontSet = findRegisteredFontSet; // Publish service... return getIcon; /** * Actual $mdIcon service is essentially a lookup function */ function getIcon(id) { id = id || ''; // If already loaded and cached, use a clone of the cached icon. // Otherwise either load by URL, or lookup in the registry and then load by URL, and cache. if ( iconCache[id] ) return $q.when( iconCache[id].clone() ); if ( urlRegex.test(id) ) return loadByURL(id).then( cacheIcon(id) ); if ( id.indexOf(':') == -1 ) id = '$default:' + id; return loadByID(id) .catch(loadFromIconSet) .catch(announceIdNotFound) .catch(announceNotFound) .then( cacheIcon(id) ); } /** * Lookup registered fontSet style using its alias... * If not found, */ function findRegisteredFontSet(alias) { var useDefault = angular.isUndefined(alias) || !(alias && alias.length); if ( useDefault ) return config.defaultFontSet; var result = alias; angular.forEach(config.fontSets, function(it){ if ( it.alias == alias ) result = it.fontSet || result; }); return result; } /** * Prepare and cache the loaded icon for the specified `id` */ function cacheIcon( id ) { return function updateCache( icon ) { iconCache[id] = isIcon(icon) ? icon : new Icon(icon, config[id]); return iconCache[id].clone(); }; } /** * Lookup the configuration in the registry, if !registered throw an error * otherwise load the icon [on-demand] using the registered URL. * */ function loadByID(id) { var iconConfig = config[id]; return !iconConfig ? $q.reject(id) : loadByURL(iconConfig.url).then(function(icon) { return new Icon(icon, iconConfig); }); } /** * Loads the file as XML and uses querySelector( ) to find * the desired node... */ function loadFromIconSet(id) { var setName = id.substring(0, id.lastIndexOf(':')) || '$default'; var iconSetConfig = config[setName]; return !iconSetConfig ? $q.reject(id) : loadByURL(iconSetConfig.url).then(extractFromSet); function extractFromSet(set) { var iconName = id.slice(id.lastIndexOf(':') + 1); var icon = set.querySelector('#' + iconName); return !icon ? $q.reject(id) : new Icon(icon, iconSetConfig); } } /** * Load the icon by URL (may use the $templateCache). * Extract the data for later conversion to Icon */ function loadByURL(url) { return $http .get(url, { cache: $templateCache }) .then(function(response) { return angular.element('
').append(response.data).find('svg')[0]; }); } /** * User did not specify a URL and the ID has not been registered with the $mdIcon * registry */ function announceIdNotFound(id) { var msg; if (angular.isString(id)) { msg = 'icon ' + id + ' not found'; $log.warn(msg); } return $q.reject(msg || id); } /** * Catch HTTP or generic errors not related to incorrect icon IDs. */ function announceNotFound(err) { var msg = angular.isString(err) ? err : (err.message || err.data || err.statusText); $log.warn(msg); return $q.reject(msg); } /** * Check target signature to see if it is an Icon instance. */ function isIcon(target) { return angular.isDefined(target.element) && angular.isDefined(target.config); } /** * Define the Icon class */ function Icon(el, config) { if (el.tagName != 'svg') { el = angular.element('').append(el)[0]; } // Inject the namespace if not available... if ( !el.getAttribute('xmlns') ) { el.setAttribute('xmlns', "http://www.w3.org/2000/svg"); } this.element = el; this.config = config; this.prepare(); } /** * Prepare the DOM element that will be cached in the * loaded iconCache store. */ function prepareAndStyle() { var iconSize = this.config ? this.config.iconSize : config.defaultIconSize; angular.forEach({ 'fit' : '', 'height': '100%', 'width' : '100%', 'preserveAspectRatio': 'xMidYMid meet', 'viewBox' : this.element.getAttribute('viewBox') || ('0 0 ' + iconSize + ' ' + iconSize) }, function(val, attr) { this.element.setAttribute(attr, val); }, this); angular.forEach({ 'pointer-events' : 'none', 'display' : 'block' }, function(val, style) { this.element.style[style] = val; }, this); } /** * Clone the Icon DOM element. */ function cloneSVG(){ return this.element.cloneNode(true); } } })(); (function(){ "use strict"; /** * @ngdoc module * @name material.components.input */ angular.module('material.components.input', [ 'material.core' ]) .directive('mdInputContainer', mdInputContainerDirective) .directive('label', labelDirective) .directive('input', inputTextareaDirective) .directive('textarea', inputTextareaDirective) .directive('mdMaxlength', mdMaxlengthDirective) .directive('placeholder', placeholderDirective); /** * @ngdoc directive * @name mdInputContainer * @module material.components.input * * @restrict E * * @description * `` is the parent of any input or textarea element. * * Input and textarea elements will not behave properly unless the md-input-container * parent is provided. * * @param md-is-error {expression=} When the given expression evaluates to true, the input container will go into error state. Defaults to erroring if the input has been touched and is invalid. * @param md-no-float {boolean=} When present, placeholders will not be converted to floating labels * * @usage * * * * * * * * * * * * */ function mdInputContainerDirective($mdTheming, $parse) { ContainerCtrl.$inject = ["$scope", "$element", "$attrs"]; return { restrict: 'E', link: postLink, controller: ContainerCtrl }; function postLink(scope, element, attr) { $mdTheming(element); } function ContainerCtrl($scope, $element, $attrs) { var self = this; self.isErrorGetter = $attrs.mdIsError && $parse($attrs.mdIsError); self.delegateClick = function() { self.input.focus(); }; self.element = $element; self.setFocused = function(isFocused) { $element.toggleClass('md-input-focused', !!isFocused); }; self.setHasValue = function(hasValue) { $element.toggleClass('md-input-has-value', !!hasValue); }; self.setInvalid = function(isInvalid) { $element.toggleClass('md-input-invalid', !!isInvalid); }; $scope.$watch(function() { return self.label && self.input; }, function(hasLabelAndInput) { if (hasLabelAndInput && !self.label.attr('for')) { self.label.attr('for', self.input.attr('id')); } }); } } mdInputContainerDirective.$inject = ["$mdTheming", "$parse"]; function labelDirective() { return { restrict: 'E', require: '^?mdInputContainer', link: function(scope, element, attr, containerCtrl) { if (!containerCtrl || attr.mdNoFloat) return; containerCtrl.label = element; scope.$on('$destroy', function() { containerCtrl.label = null; }); } }; } /** * @ngdoc directive * @name mdInput * @restrict E * @module material.components.input * * @description * Use the `

* The purpose of **`md-maxlength`** is exactly to show the max length counter text. If you don't want the counter text and only need "plain" validation, you can use the "simple" `ng-maxlength` or maxlength attributes. * @param {string=} aria-label Aria-label is required when no label is present. A warning message will be logged in the console if not present. * @param {string=} placeholder An alternative approach to using aria-label when the label is not present. The placeholder text is copied to the aria-label attribute. * * @usage * * * * * * *

With Errors

* * *
* * * *
*
This is required!
*
That's too long!
*
That's too short!
*
*
* * * *
*
This is required!
*
That's too long!
*
*
* * * * * * *
*
* * Requires [ngMessages](https://docs.angularjs.org/api/ngMessages). * Behaves like the [AngularJS input directive](https://docs.angularjs.org/api/ng/directive/input). * */ function inputTextareaDirective($mdUtil, $window, $mdAria) { return { restrict: 'E', require: ['^?mdInputContainer', '?ngModel'], link: postLink }; function postLink(scope, element, attr, ctrls) { var containerCtrl = ctrls[0]; var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel(); var isReadonly = angular.isDefined(attr.readonly); if ( !containerCtrl ) return; if (containerCtrl.input) { throw new Error(" can only have *one* or