/*! * Angular Material Design * https://github.com/angular/material * @license MIT * v1.0.4 */ (function( window, angular, undefined ){ "use strict"; (function(){ "use strict"; angular.module('ngMaterial', ["ng","ngAnimate","ngAria","material.core","material.core.gestures","material.core.layout","material.core.theming.palette","material.core.theming","material.core.animate","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.datepicker","material.components.dialog","material.components.divider","material.components.fabShared","material.components.fabSpeedDial","material.components.fabActions","material.components.fabToolbar","material.components.fabTrigger","material.components.gridList","material.components.icon","material.components.input","material.components.list","material.components.menu","material.components.menuBar","material.components.progressCircular","material.components.progressLinear","material.components.radioButton","material.components.select","material.components.showHide","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.virtualRepeat","material.components.whiteframe"]); })(); (function(){ "use strict"; /** * Initialization function that validates environment * requirements. */ angular .module('material.core', [ 'ngAnimate', 'material.core.animate', 'material.core.layout', 'material.core.gestures', 'material.core.theming' ]) .config(MdCoreConfigure) .run(DetectNgTouch); /** * Detect if the ng-Touch module is also being used. * Warn if detected. */ function DetectNgTouch($log, $injector) { if ( $injector.has('$swipe') ) { var msg = "" + "You are using the ngTouch module. \n" + "Angular Material already has mobile click, tap, and swipe support... \n" + "ngTouch is not supported with Angular Material!"; $log.warn(msg); } } DetectNgTouch.$inject = ["$log", "$injector"]; function MdCoreConfigure($provide, $mdThemingProvider) { $provide.decorator('$$rAF', ["$delegate", rAFDecorator]); $mdThemingProvider.theme('default') .primaryPalette('indigo') .accentPalette('pink') .warnPalette('deep-orange') .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 queuedArgs, alreadyQueued, queueCb, context; return function debounced() { queuedArgs = arguments; context = this; queueCb = cb; if (!alreadyQueued) { alreadyQueued = true; $delegate(function() { queueCb.apply(context, Array.prototype.slice.call(queuedArgs)); alreadyQueued = false; }); } }; }; return $delegate; } })(); (function(){ "use strict"; angular.module('material.core') .directive('mdAutofocus', MdAutofocusDirective) // Support the deprecated md-auto-focus and md-sidenav-focus as well .directive('mdAutoFocus', MdAutofocusDirective) .directive('mdSidenavFocus', MdAutofocusDirective); /** * @ngdoc directive * @name mdAutofocus * @module material.core.util * * @description * * `[md-autofocus]` provides an optional way to identify the focused element when a `$mdDialog`, * `$mdBottomSheet`, or `$mdSidenav` opens or upon page load for input-like elements. * * When one of these opens, it will find the first nested element with the `[md-autofocus]` * attribute directive and optional expression. An expression may be specified as the directive * value to enable conditional activation of the autofocus. * * @usage * * ### Dialog * * *
* * * * *
*
*
* * ### Bottomsheet * * * Comment Actions * * * * * * {{ item.name }} * * * * * * * * ### Autocomplete * * * {{item.display}} * * * * ### Sidenav * *
* * Left Nav! * * * * Center Content * * Open Left Menu * * * * *
* * * * *
*
*
*
**/ function MdAutofocusDirective() { return { restrict: 'A', link: postLink } } function postLink(scope, element, attrs) { var attr = attrs.mdAutoFocus || attrs.mdAutofocus || attrs.mdSidenavFocus; // Setup a watcher on the proper attribute to update a class we can check for in $mdUtil scope.$watch(attr, function(canAutofocus) { element.toggleClass('_md-autofocus', canAutofocus); }); } })(); (function(){ "use strict"; angular.module('material.core') .factory('$mdConstant', MdConstantFactory); /** * Factory function that creates the grab-bag $mdConstant service. * @ngInject */ function MdConstantFactory($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: { COMMA: 188, ENTER: 13, ESCAPE: 27, SPACE: 32, PAGE_UP: 33, PAGE_DOWN: 34, END: 35, HOME: 36, 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') }, /** * As defined in core/style/variables.scss * * $layout-breakpoint-xs: 600px !default; * $layout-breakpoint-sm: 960px !default; * $layout-breakpoint-md: 1280px !default; * $layout-breakpoint-lg: 1920px !default; * */ MEDIA: { 'xs' : '(max-width: 599px)' , 'gt-xs' : '(min-width: 600px)' , 'sm' : '(min-width: 600px) and (max-width: 959px)' , 'gt-sm' : '(min-width: 960px)' , 'md' : '(min-width: 960px) and (max-width: 1279px)' , 'gt-md' : '(min-width: 1280px)' , 'lg' : '(min-width: 1280px) and (max-width: 1919px)', 'gt-lg' : '(min-width: 1920px)' , 'xl' : '(min-width: 1920px)' }, MEDIA_PRIORITY: [ 'xl', 'gt-lg', 'lg', 'gt-md', 'md', 'gt-sm', 'sm', 'gt-xs', 'xs' ] }; } MdConstantFactory.$inject = ["$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: * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
BreakpointmediaQuery
xs(max-width: 599px)
gt-xs(min-width: 600px)
sm(min-width: 600px) and (max-width: 959px)
gt-sm(min-width: 960px)
md(min-width: 960px) and (max-width: 1279px)
gt-md(min-width: 1280px)
lg(min-width: 1280px) and (max-width: 1919px)
gt-lg(min-width: 1920px)
xl(min-width: 1920px)
* * See Material Design's Layout - Adaptive UI for more details. * * * * * * @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]; if ( !result ) { 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 (angular.isDefined(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 (angular.isDefined(attrs[normalizedName])) { 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; /** * @ngdoc module * @name material.core.util * @description * Util */ angular .module('material.core') .factory('$mdUtil', UtilFactory); function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $interpolate, $log, $rootElement, $window) { // Setup some core variables for the processTemplate method var startSymbol = $interpolate.startSymbol(), endSymbol = $interpolate.endSymbol(), usesStandardSymbols = ((startSymbol === '{{') && (endSymbol === '}}')); /** * Checks if the target element has the requested style by key * @param {DOMElement|JQLite} target Target element * @param {string} key Style key * @param {string=} expectedVal Optional expected value * @returns {boolean} Whether the target element has the style or not */ var hasComputedStyle = function (target, key, expectedVal) { var hasValue = false; if ( target && target.length ) { var computedStyles = $window.getComputedStyle(target[0]); hasValue = angular.isDefined(computedStyles[key]) && (expectedVal ? computedStyles[key] == expectedVal : true); } return hasValue; }; var $mdUtil = { dom: {}, now: window.performance ? angular.bind(window.performance, window.performance.now) : Date.now || function() { return new Date().getTime(); }, 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 $mdUtil.clientRect(element, offsetParent, true); }, // Annoying method to copy nodes to an array, thanks to IE nodesToArray: function(nodes) { nodes = nodes || []; var results = []; for (var i = 0; i < nodes.length; ++i) { results.push(nodes.item(i)); } return results; }, /** * Calculate the positive scroll offset * TODO: Check with pinch-zoom in IE/Chrome; * https://code.google.com/p/chromium/issues/detail?id=496285 */ scrollTop: function(element) { element = angular.element(element || $document[0].body); var body = (element[0] == $document[0].body) ? $document[0].body : undefined; var scrollTop = body ? body.scrollTop + body.parentElement.scrollTop : 0; // Calculate the positive scroll offset return scrollTop || Math.abs(element[0].getBoundingClientRect().top); }, /** * Finds the proper focus target by searching the DOM. * * @param containerEl * @param attributeVal * @returns {*} */ findFocusTarget: function(containerEl, attributeVal) { var AUTO_FOCUS = '[md-autofocus]'; var elToFocus; elToFocus = scanForFocusable(containerEl, attributeVal || AUTO_FOCUS); if ( !elToFocus && attributeVal != AUTO_FOCUS) { // Scan for deprecated attribute elToFocus = scanForFocusable(containerEl, '[md-auto-focus]'); if ( !elToFocus ) { // Scan for fallback to 'universal' API elToFocus = scanForFocusable(containerEl, AUTO_FOCUS); } } return elToFocus; /** * Can target and nested children for specified Selector (attribute) * whose value may be an expression that evaluates to True/False. */ function scanForFocusable(target, selector) { var elFound, items = target[0].querySelectorAll(selector); // Find the last child element with the focus attribute if ( items && items.length ){ items.length && angular.forEach(items, function(it) { it = angular.element(it); // Check the element for the _md-autofocus class to ensure any associated expression // evaluated to true. var isFocusable = it.hasClass('_md-autofocus'); if (isFocusable) elFound = it; }); } return elFound; } }, // Disables scroll around the passed element. disableScrollAround: function(element, parent) { $mdUtil.disableScrollAround._count = $mdUtil.disableScrollAround._count || 0; ++$mdUtil.disableScrollAround._count; if ($mdUtil.disableScrollAround._enableScrolling) return $mdUtil.disableScrollAround._enableScrolling; element = angular.element(element); var body = $document[0].body, restoreBody = disableBodyScroll(), restoreElement = disableElementScroll(parent); return $mdUtil.disableScrollAround._enableScrolling = function() { if (!--$mdUtil.disableScrollAround._count) { restoreBody(); restoreElement(); delete $mdUtil.disableScrollAround._enableScrolling; } }; // Creates a virtual scrolling mask to absorb touchmove, keyboard, scrollbar clicking, and wheel events function disableElementScroll(element) { element = angular.element(element || body)[0]; var zIndex = 50; var scrollMask = angular.element( '
' + '
' + '
').css('z-index', zIndex); element.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); delete $mdUtil.disableScrollAround._enableScrolling; }; // Prevent keypresses from elements inside the body // used to stop the keypresses that could cause the page to scroll // (arrow keys, spacebar, tab, etc). function disableKeyNav(e) { //-- temporarily removed this logic, will possibly re-add at a later date //if (!element[0].contains(e.target)) { // e.preventDefault(); // e.stopImmediatePropagation(); //} } function preventDefault(e) { e.preventDefault(); } } // Converts the body to a position fixed block and translate it to the proper scroll // position function disableBodyScroll() { var htmlNode = body.parentNode; var restoreHtmlStyle = htmlNode.style.cssText || ''; var restoreBodyStyle = body.style.cssText || ''; var scrollOffset = $mdUtil.scrollTop(body); var clientWidth = body.clientWidth; if (body.scrollHeight > body.clientHeight + 1) { applyStyles(body, { position: 'fixed', width: '100%', top: -scrollOffset + 'px' }); applyStyles(htmlNode, { overflowY: 'scroll' }); } if (body.clientWidth < clientWidth) applyStyles(body, {overflow: 'hidden'}); return function restoreScroll() { body.style.cssText = restoreBodyStyle; htmlNode.style.cssText = restoreHtmlStyle; body.scrollTop = scrollOffset; htmlNode.scrollTop = scrollOffset; }; } function applyStyles(el, styles) { for (var key in styles) { el.style[key] = styles[key]; } } }, enableScrolling: function() { var method = this.disableScrollAround._enableScrolling; method && method(); }, floatingScrollbars: function() { if (this.floatingScrollbars.cached === undefined) { var tempNode = angular.element('
').css({ width: '100%', 'z-index': -1, position: 'absolute', height: '35px', 'overflow-y': 'scroll' }); tempNode.children().css('height', '60px'); $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); }, /** * facade to build md-backdrop element with desired styles * NOTE: Use $compile to trigger backdrop postLink function */ createBackdrop: function(scope, addClass) { return $compile($mdUtil.supplant('', [addClass]))(scope); }, /** * supplant() method from Crockford's `Remedial Javascript` * Equivalent to use of $interpolate; without dependency on * interpolation symbols and scope. Note: the '{}' can * be property names, property chains, or array indices. */ supplant: function(template, values, pattern) { pattern = pattern || /\{([^\{\}]*)\}/g; return template.replace(pattern, function(a, b) { var p = b.split('.'), r = values; try { for (var s in p) { if (p.hasOwnProperty(s) ) { r = r[p[s]]; } } } catch (e) { r = a; } return (typeof r === 'string' || typeof r === 'number') ? r : a; }); }, 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 = $mdUtil.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 = $mdUtil.now(); cb(); return $mdUtil.now() - start; }, /** * Create an implicit getter that caches its `getter()` * lookup value */ valueOnUse : function (scope, key, getter) { var value = null, args = Array.prototype.slice.call(arguments); var params = (args.length > 3) ? args.slice(3) : [ ]; Object.defineProperty(scope, key, { get: function () { if (value === null) value = getter.apply(scope, params); return value; } }); }, /** * 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' * @param onlyParent Only start checking from the parent element, not `el`. */ 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; }, /** * Build polyfill for the Node.contains feature (if needed) */ elementContains: function(node, child) { var hasContains = (window.Node && window.Node.prototype && Node.prototype.contains); var findFn = hasContains ? angular.bind(node, node.contains) : angular.bind(node, function(arg) { // compares the positions of two nodes and returns a bitmask return (node === child) || !!(this.compareDocumentPosition(arg) & 16) }); return findFn(child); }, /** * Functional equivalent for $element.filter(‘md-bottom-sheet’) * useful with interimElements where the element and its container are important... * * @param {[]} elements to scan * @param {string} name of node to find (e.g. 'md-dialog') * @param {boolean=} optional flag to allow deep scans; defaults to 'false'. * @param {boolean=} optional flag to enable log warnings; defaults to false */ extractElementByName: function(element, nodeName, scanDeep, warnNotFound) { var found = scanTree(element); if (!found && !!warnNotFound) { $log.warn( $mdUtil.supplant("Unable to find node '{0}' in element '{1}'.",[nodeName, element[0].outerHTML]) ); } return angular.element(found || element); /** * Breadth-First tree scan for element with matching `nodeName` */ function scanTree(element) { return scanLevel(element) || (!!scanDeep ? scanChildren(element) : null); } /** * Case-insensitive scan of current elements only (do not descend). */ function scanLevel(element) { if ( element ) { for (var i = 0, len = element.length; i < len; i++) { if (element[i].nodeName.toLowerCase() === nodeName) { return element[i]; } } } return null; } /** * Scan children of specified node */ function scanChildren(element) { var found; if ( element ) { for (var i = 0, len = element.length; i < len; i++) { var target = element[i]; if ( !found ) { for (var j = 0, numChild = target.childNodes.length; j < numChild; j++) { found = found || scanTree([target.childNodes[j]]); } } } } return found; } }, /** * Give optional properties with no value a boolean true if attr provided or false otherwise */ initOptionalProperties: function(scope, attr, defaults) { defaults = defaults || {}; angular.forEach(scope.$$isolateBindings, function(binding, key) { if (binding.optional && angular.isUndefined(scope[key])) { var attrIsDefined = angular.isDefined(attr[binding.attrName]); scope[key] = angular.isDefined(defaults[key]) ? defaults[key] : attrIsDefined; } }); }, /** * Alternative to $timeout calls with 0 delay. * nextTick() coalesces all calls within a single frame * to minimize $digest thrashing * * @param callback * @param digest * @returns {*} */ nextTick: function(callback, digest, scope) { //-- grab function reference for storing state details var nextTick = $mdUtil.nextTick; var timeout = nextTick.timeout; var queue = nextTick.queue || []; //-- add callback to the queue queue.push(callback); //-- set default value for digest if (digest == null) digest = true; //-- store updated digest/queue values nextTick.digest = nextTick.digest || digest; nextTick.queue = queue; //-- either return existing timeout or create a new one return timeout || (nextTick.timeout = $timeout(processQueue, 0, false)); /** * Grab a copy of the current queue * Clear the queue for future use * Process the existing queue * Trigger digest if necessary */ function processQueue() { var skip = scope && scope.$$destroyed; var queue = !skip ? nextTick.queue : []; var digest = !skip ? nextTick.digest : null; nextTick.queue = []; nextTick.timeout = null; nextTick.digest = false; queue.forEach(function(callback) { callback(); }); if (digest) $rootScope.$digest(); } }, /** * Processes a template and replaces the start/end symbols if the application has * overriden them. * * @param template The template to process whose start/end tags may be replaced. * @returns {*} */ processTemplate: function(template) { if (usesStandardSymbols) { return template; } else { if (!template || !angular.isString(template)) return template; return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol); } }, /** * Scan up dom hierarchy for enabled parent; */ getParentWithPointerEvents: function (element) { var parent = element.parent(); // jqLite might return a non-null, but still empty, parent; so check for parent and length while (hasComputedStyle(parent, 'pointer-events', 'none')) { parent = parent.parent(); } return parent; }, getNearestContentElement: function (element) { var current = element.parent()[0]; // Look for the nearest parent md-content, stopping at the rootElement. while (current && current !== $rootElement[0] && current !== document.body && current.nodeName.toUpperCase() !== 'MD-CONTENT') { current = current.parentNode; } return current; }, hasComputedStyle: hasComputedStyle }; // Instantiate other namespace utility methods $mdUtil.dom.animator = $$mdAnimate($mdUtil); return $mdUtil; function getNode(el) { return el[0] || el; } } UtilFactory.$inject = ["$document", "$timeout", "$compile", "$rootScope", "$$mdAnimate", "$interpolate", "$log", "$rootElement", "$window"]; /* * 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`. */ 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, $interpolate) { 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 = angular.element(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) { var content = getText(element) || ""; var hasBinding = content.indexOf($interpolate.startSymbol())>-1; if ( hasBinding ) { expectAsync(element, attrName, function() { return getText(element); }); } else { expect(element, attrName, content); } } 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 = angular.extend({}, options.resolve || {}); var locals = angular.extend({}, 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 compiledData; var template = transformTemplate(locals.$template, options); 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 compiledData = { 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; } // Publish reference to this controller compiledData.controller = 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; /** * The position of the most recent click if that click was on a label element. * @type {{x: number, y: number}?} */ var lastLabelClickPos = null; // 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) { var maxClickDistance = 6; self.handler('click', { options: { maxDistance: maxClickDistance }, onEnd: checkDistanceAndEmit('click') }); self.handler('focus', { options: { maxDistance: maxClickDistance }, onEnd: function(ev, pointer) { if (pointer.distance < this.state.options.maxDistance) { if (canFocus(ev.target)) { this.dispatchEvent(ev, 'focus', pointer); ev.target.focus(); } } function canFocus(element) { var focusableElements = ['INPUT', 'SELECT', 'BUTTON', 'TEXTAREA', 'VIDEO', 'AUDIO']; return (element.getAttribute('tabindex') != '-1') && !element.hasAttribute('DISABLED') && (element.hasAttribute('tabindex') || element.hasAttribute('href') || (focusableElements.indexOf(element.nodeName) != -1)); } } }); self.handler('mouseup', { options: { maxDistance: maxClickDistance }, onEnd: checkDistanceAndEmit('mouseup') }); self.handler('mousedown', { onStart: function(ev) { this.dispatchEvent(ev, 'mousedown'); } }); } function checkDistanceAndEmit(eventName) { return function(ev, pointer) { if (pointer.distance < this.state.options.maxDistance) { this.dispatchEvent(ev, eventName, pointer); } }; } /* * 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. */ .handler('swipe', { options: { minVelocity: 0.65, minDistance: 10 }, onEnd: function (ev, pointer) { var eventType; if (Math.abs(pointer.velocityX) > this.state.options.minVelocity && Math.abs(pointer.distanceX) > this.state.options.minDistance) { eventType = pointer.directionX == 'left' ? '$md.swipeleft' : '$md.swiperight'; this.dispatchEvent(ev, eventType); } else if (Math.abs(pointer.velocityY) > this.state.options.minVelocity && Math.abs(pointer.distanceY) > this.state.options.minDistance) { eventType = pointer.directionY == 'up' ? '$md.swipeup' : '$md.swipedown'; 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' || eventType == 'mouseup' || eventType == 'mousedown' ) { eventObj = document.createEvent('MouseEvents'); eventObj.initMouseEvent( eventType, 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' , clickHijacker , true); document.addEventListener('mouseup' , mouseInputHijacker, true); document.addEventListener('mousedown', mouseInputHijacker, true); document.addEventListener('focus' , mouseInputHijacker, true); isInitialized = true; } function mouseInputHijacker(ev) { var isKeyClick = !ev.clientX && !ev.clientY; if (!isKeyClick && !ev.$material && !ev.isIonicTap && !isInputEventFromLabelClick(ev)) { ev.preventDefault(); ev.stopPropagation(); } } function clickHijacker(ev) { var isKeyClick = ev.clientX === 0 && ev.clientY === 0; if (!isKeyClick && !ev.$material && !ev.isIonicTap && !isInputEventFromLabelClick(ev)) { ev.preventDefault(); ev.stopPropagation(); lastLabelClickPos = null; } else { lastLabelClickPos = null; if (ev.target.tagName.toLowerCase() == 'label') { lastLabelClickPos = {x: ev.x, y: ev.y}; } } } // 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; } /** * Gets whether the given event is an input event that was caused by clicking on an * associated label element. * * This is necessary because the browser will, upon clicking on a label element, fire an * *extra* click event on its associated input (if any). mdGesture is able to flag the label * click as with `$material` correctly, but not the second input click. * * In order to determine whether an input event is from a label click, we compare the (x, y) for * the event to the (x, y) for the most recent label click (which is cleared whenever a non-label * click occurs). Unfortunately, there are no event properties that tie the input and the label * together (such as relatedTarget). * * @param {MouseEvent} event * @returns {boolean} */ function isInputEventFromLabelClick(event) { return lastLabelClickPos && lastLabelClickPos.x == event.x && lastLabelClickPos.y == event.y; } /* * 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 ? 'down' : pointer.distanceY < 0 ? 'up' : ''; 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", "$$q", "$rootScope", "$timeout", "$rootElement", "$animate", "$mdUtil", "$mdCompiler", "$mdTheming", "$injector"]; 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", "$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; } function addPresetMethod(presetName, methodName, method) { providerConfig.presets[presetName][methodName] = method; } /** * Create a factory that has the given methods & defaults implementing interimElement */ /* @ngInject */ function factory($$interimElement, $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, // Special internal method to destroy an interim element without animations // used when navigation changes causes a $scope.$destroy() action destroy : destroyInterimElement }; 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 interimElement.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 opts = opts || { }; if (opts._options) opts = opts._options; return interimElementService.show( angular.extend({}, defaultOptions, opts) ); } /** * Special method to hide and destroy an interimElement WITHOUT * any 'leave` or hide animations ( an immediate force hide/remove ) * * NOTE: This calls the onRemove() subclass method for each component... * which must have code to respond to `options.$destroy == true` */ function destroyInterimElement(opts) { return interimElementService.destroy(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, $$q, $rootScope, $timeout, $rootElement, $animate, $mdUtil, $mdCompiler, $mdTheming, $injector ) { return function createInterimElementService() { var SHOW_CANCELLED = false; /* * @ngdoc service * @name $$interimElement.$service * * @description * A service used to control inserting and removing an element into the DOM. * */ var service, stack = []; // Publish instance $$interimElement service; // ... used as $mdDialog, $mdToast, $mdMenu, and $mdSelect return service = { show: show, hide: hide, cancel: cancel, destroy : destroy, $injector_: $injector }; /* * @ngdoc method * @name $$interimElement.$service#show * @kind function * * @description * Adds the `$interimElement` to the DOM and returns a special promise that will be resolved or rejected * with hide or cancel, respectively. To external cancel/hide, developers should use the * * @param {*} options is hashMap of settings * @returns a Promise * */ function show(options) { options = options || {}; var interimElement = new InterimElement(options || {}); var hideExisting = !options.skipHide && stack.length ? service.hide() : $q.when(true); // This hide()s only the current interim element before showing the next, new one // NOTE: this is not reversible (e.g. interim elements are not stackable) hideExisting.finally(function() { stack.push(interimElement); interimElement .show() .catch(function( reason ) { //$log.error("InterimElement.show() error: " + reason ); return reason; }); }); // Return a promise that will be resolved when the interim // element is hidden or cancelled... 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(reason, options) { if ( !stack.length ) return $q.when(reason); options = options || {}; if (options.closeAll) { var promise = $q.all(stack.reverse().map(closeElement)); stack = []; return promise; } else if (options.closeTo !== undefined) { return $q.all(stack.splice(options.closeTo).map(closeElement)); } else { var interim = stack.pop(); return closeElement(interim); } function closeElement(interim) { interim .remove(reason, false, options || { }) .catch(function( reason ) { //$log.error("InterimElement.hide() error: " + reason ); return reason; }); return interim.deferred.promise; } } /* * @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, options) { var interim = stack.shift(); if ( !interim ) return $q.when(reason); interim .remove(reason, true, options || { }) .catch(function( reason ) { //$log.error("InterimElement.cancel() error: " + reason ); return reason; }); return interim.deferred.promise; } /* * Special method to quick-remove the interim element without animations * Note: interim elements are in "interim containers" */ function destroy(target) { var interim = !target ? stack.shift() : null; var cntr = angular.element(target).length ? angular.element(target)[0].parentNode : null; if (cntr) { // Try to find the interim element in the stack which corresponds to the supplied DOM element. var filtered = stack.filter(function(entry) { var currNode = entry.options.element[0]; return (currNode === cntr); }); // Note: this function might be called when the element already has been removed, in which // case we won't find any matches. That's ok. if (filtered.length > 0) { interim = filtered[0]; stack.splice(stack.indexOf(interim), 1); } } return interim ? interim.remove(SHOW_CANCELLED, false, {'$destroy':true}) : $q.when(SHOW_CANCELLED); } /* * Internal Interim Element Object * Used internally to manage the DOM element and related data */ function InterimElement(options) { var self, element, showAction = $q.when(true); options = configureScopeAndTransitions(options); return self = { options : options, deferred: $q.defer(), show : createAndTransitionIn, remove : transitionOutAndRemove }; /** * Compile, link, and show this interim element * Use optional autoHided and transition-in effects */ function createAndTransitionIn() { return $q(function(resolve, reject){ compileElement(options) .then(function( compiledData ) { element = linkElement( compiledData, options ); showAction = showElement(element, options, compiledData.controller) .then(resolve, rejectAll ); }, rejectAll); function rejectAll(fault) { // Force the '$md.show()' promise to reject self.deferred.reject(fault); // Continue rejection propagation reject(fault); } }); } /** * After the show process has finished/rejected: * - announce 'removing', * - perform the transition-out, and * - perform optional clean up scope. */ function transitionOutAndRemove(response, isCancelled, opts) { // abort if the show() and compile failed if ( !element ) return $q.when(false); options = angular.extend(options || {}, opts || {}); options.cancelAutoHide && options.cancelAutoHide(); options.element.triggerHandler('$mdInterimElementRemove'); if ( options.$destroy === true ) { return hideElement(options.element, options).then(function(){ (isCancelled && rejectAll(response)) || resolveAll(response); }); } else { $q.when(showAction) .finally(function() { hideElement(options.element, options).then(function() { (isCancelled && rejectAll(response)) || resolveAll(response); }, rejectAll); }); return self.deferred.promise; } /** * The `show()` returns a promise that will be resolved when the interim * element is hidden or cancelled... */ function resolveAll(response) { self.deferred.resolve(response); } /** * Force the '$md.show()' promise to reject */ function rejectAll(fault) { self.deferred.reject(fault); } } /** * Prepare optional isolated scope and prepare $animate with default enter and leave * transitions for the new element instance. */ function configureScopeAndTransitions(options) { options = options || { }; if ( options.template ) { options.template = $mdUtil.processTemplate(options.template); } return angular.extend({ preserveScope: false, cancelAutoHide : angular.noop, scope: options.scope || $rootScope.$new(options.isolateScope), /** * Default usage to enable $animate to transition-in; can be easily overridden via 'options' */ onShow: function transitionIn(scope, element, options) { return $animate.enter(element, options.parent); }, /** * Default usage to enable $animate to transition-out; can be easily overridden via 'options' */ onRemove: function transitionOut(scope, element) { // Element could be undefined if a new element is shown before // the old one finishes compiling. return element && $animate.leave(element) || $q.when(); } }, options ); } /** * Compile an element with a templateUrl, controller, and locals */ function compileElement(options) { var compiled = !options.skipCompile ? $mdCompiler.compile(options) : null; return compiled || $q(function (resolve) { resolve({ locals: {}, link: function () { return options.element; } }); }); } /** * Link an element with compiled configuration */ function linkElement(compileData, options){ angular.extend(compileData.locals, options); var element = compileData.link(options.scope); // Search for parent at insertion time, if not specified options.element = element; options.parent = findParent(element, options); if (options.themable) $mdTheming(element); return element; } /** * Search for parent at insertion time, if not specified */ function findParent(element, options) { var parent = options.parent; // Search for parent at insertion time, if not specified if (angular.isFunction(parent)) { parent = parent(options.scope, element, options); } else if (angular.isString(parent)) { parent = angular.element($document[0].querySelector(parent)); } else { parent = angular.element(parent); } // If parent querySelector/getter function fails, or it's just null, // find a default. if (!(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; } return angular.element(el); } return parent; } /** * If auto-hide is enabled, start timer and prepare cancel function */ function startAutoHide() { var autoHideTimer, cancelAutoHide = angular.noop; if (options.hideDelay) { autoHideTimer = $timeout(service.hide, options.hideDelay) ; cancelAutoHide = function() { $timeout.cancel(autoHideTimer); } } // Cache for subsequent use options.cancelAutoHide = function() { cancelAutoHide(); options.cancelAutoHide = undefined; } } /** * Show the element ( with transitions), notify complete and start * optional auto-Hide */ function showElement(element, options, controller) { // Trigger onShowing callback before the `show()` starts var notifyShowing = options.onShowing || angular.noop; // Trigger onComplete callback when the `show()` finishes var notifyComplete = options.onComplete || angular.noop; notifyShowing(options.scope, element, options, controller); return $q(function (resolve, reject) { try { // Start transitionIn $q.when(options.onShow(options.scope, element, options, controller)) .then(function () { notifyComplete(options.scope, element, options); startAutoHide(); resolve(element); }, reject ); } catch(e) { reject(e.message); } }); } function hideElement(element, options) { var announceRemoving = options.onRemoving || angular.noop; return $$q(function (resolve, reject) { try { // Start transitionIn var action = $$q.when( options.onRemove(options.scope, element, options) || true ); // Trigger callback *before* the remove operation starts announceRemoving(element, action); if ( options.$destroy == true ) { // For $destroy, onRemove should be synchronous resolve(element); } else { // Wait until transition-out is done action.then(function () { if (!options.preserveScope && options.scope ) { options.scope.$destroy(); } resolve(element); }, reject ); } } catch(e) { reject(e.message); } }); } } }; } } })(); (function(){ "use strict"; (function() { 'use strict'; var $mdUtil, $interpolate, $log; var SUFFIXES = /(-gt)?-(sm|md|lg)/g; var WHITESPACE = /\s+/g; var FLEX_OPTIONS = ['grow', 'initial', 'auto', 'none', 'noshrink', 'nogrow' ]; var LAYOUT_OPTIONS = ['row', 'column']; var ALIGNMENT_MAIN_AXIS= [ "", "start", "center", "end", "stretch", "space-around", "space-between" ]; var ALIGNMENT_CROSS_AXIS= [ "", "start", "center", "end", "stretch" ]; var config = { /** * Enable directive attribute-to-class conversions * Developers can use `` to quickly * disable the Layout directives and prohibit the injection of Layout classNames */ enabled: true, /** * List of mediaQuery breakpoints and associated suffixes * * [ * { suffix: "sm", mediaQuery: "screen and (max-width: 599px)" }, * { suffix: "md", mediaQuery: "screen and (min-width: 600px) and (max-width: 959px)" } * ] */ breakpoints: [] }; registerLayoutAPI( angular.module('material.core.layout', ['ng']) ); /** * registerLayoutAPI() * * The original ngMaterial Layout solution used attribute selectors and CSS. * * ```html *
My Content
* ``` * * ```css * [layout] { * box-sizing: border-box; * display:flex; * } * [layout=column] { * flex-direction : column * } * ``` * * Use of attribute selectors creates significant performance impacts in some * browsers... mainly IE. * * This module registers directives that allow the same layout attributes to be * interpreted and converted to class selectors. The directive will add equivalent classes to each element that * contains a Layout directive. * * ```html *
My Content
*``` * * ```css * .layout { * box-sizing: border-box; * display:flex; * } * .layout-column { * flex-direction : column * } * ``` */ function registerLayoutAPI(module){ var PREFIX_REGEXP = /^((?:x|data)[\:\-_])/i; var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g; // NOTE: these are also defined in constants::MEDIA_PRIORITY and constants::MEDIA var BREAKPOINTS = [ "", "xs", "gt-xs", "sm", "gt-sm", "md", "gt-md", "lg", "gt-lg", "xl" ]; var API_WITH_VALUES = [ "layout", "flex", "flex-order", "flex-offset", "layout-align" ]; var API_NO_VALUES = [ "show", "hide", "layout-padding", "layout-margin" ]; // Build directive registration functions for the standard Layout API... for all breakpoints. angular.forEach(BREAKPOINTS, function(mqb) { // Attribute directives with expected, observable value(s) angular.forEach( API_WITH_VALUES, function(name){ var fullName = mqb ? name + "-" + mqb : name; module.directive( directiveNormalize(fullName), attributeWithObserve(fullName)); }); // Attribute directives with no expected value(s) angular.forEach( API_NO_VALUES, function(name){ var fullName = mqb ? name + "-" + mqb : name; module.directive( directiveNormalize(fullName), attributeWithoutValue(fullName)); }); }); // Register other, special directive functions for the Layout features: module .directive('mdLayoutCss' , disableLayoutDirective ) .directive('ngCloak' , buildCloakInterceptor('ng-cloak')) .directive('layoutWrap' , attributeWithoutValue('layout-wrap')) .directive('layoutNoWrap' , attributeWithoutValue('layout-no-wrap')) .directive('layoutFill' , attributeWithoutValue('layout-fill')) // !! Deprecated attributes: use the `-lt` (aka less-than) notations .directive('layoutLtMd' , warnAttrNotSupported('layout-lt-md', true)) .directive('layoutLtLg' , warnAttrNotSupported('layout-lt-lg', true)) .directive('flexLtMd' , warnAttrNotSupported('flex-lt-md', true)) .directive('flexLtLg' , warnAttrNotSupported('flex-lt-lg', true)) .directive('layoutAlignLtMd', warnAttrNotSupported('layout-align-lt-md')) .directive('layoutAlignLtLg', warnAttrNotSupported('layout-align-lt-lg')) .directive('flexOrderLtMd' , warnAttrNotSupported('flex-order-lt-md')) .directive('flexOrderLtLg' , warnAttrNotSupported('flex-order-lt-lg')) .directive('offsetLtMd' , warnAttrNotSupported('flex-offset-lt-md')) .directive('offsetLtLg' , warnAttrNotSupported('flex-offset-lt-lg')) .directive('hideLtMd' , warnAttrNotSupported('hide-lt-md')) .directive('hideLtLg' , warnAttrNotSupported('hide-lt-lg')) .directive('showLtMd' , warnAttrNotSupported('show-lt-md')) .directive('showLtLg' , warnAttrNotSupported('show-lt-lg')); /** * Converts snake_case to camelCase. * Also there is special case for Moz prefix starting with upper case letter. * @param name Name to normalize */ function directiveNormalize(name) { return name .replace(PREFIX_REGEXP, '') .replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) { return offset ? letter.toUpperCase() : letter; }); } } /** * Special directive that will disable ALL Layout conversions of layout * attribute(s) to classname(s). * * * * * * ... * * * Note: Using md-layout-css directive requires the developer to load the Material * Layout Attribute stylesheet (which only uses attribute selectors): * * `angular-material.layout.css` * * Another option is to use the LayoutProvider to configure and disable the attribute * conversions; this would obviate the use of the `md-layout-css` directive * */ function disableLayoutDirective() { return { restrict : 'A', priority : '900', compile : function(element, attr) { config.enabled = false; return angular.noop; } }; } /** * Tail-hook ngCloak to delay the uncloaking while Layout transformers * finish processing. Eliminates flicker with Material.Layoouts */ function buildCloakInterceptor(className) { return [ '$timeout', function($timeout){ return { restrict : 'A', priority : -10, // run after normal ng-cloak compile : function( element ) { if (!config.enabled) return angular.noop; // Re-add the cloak element.addClass(className); return function( scope, element ) { // Wait while layout injectors configure, then uncloak // NOTE: $rAF does not delay enough... and this is a 1x-only event, // $timeout is acceptable. $timeout( function(){ element.removeClass(className); }, 10, false); }; } }; }]; } // ********************************************************************************* // // These functions create registration functions for ngMaterial Layout attribute directives // This provides easy translation to switch ngMaterial attribute selectors to // CLASS selectors and directives; which has huge performance implications // for IE Browsers // // ********************************************************************************* /** * Creates a directive registration function where a possible dynamic attribute * value will be observed/watched. * @param {string} className attribute name; eg `layout-gt-md` with value ="row" */ function attributeWithObserve(className) { return ['$mdUtil', '$interpolate', "$log", function(_$mdUtil_, _$interpolate_, _$log_) { $mdUtil = _$mdUtil_; $interpolate = _$interpolate_; $log = _$log_; return { restrict: 'A', compile: function(element, attr) { var linkFn; if (config.enabled) { // immediately replace static (non-interpolated) invalid values... validateAttributeUsage(className, attr, element, $log); validateAttributeValue( className, getNormalizedAttrValue(className, attr, ""), buildUpdateFn(element, className, attr) ); linkFn = translateWithValueToCssClass; } // Use for postLink to account for transforms after ng-transclude. return linkFn || angular.noop; } }; }]; /** * Add as transformed class selector(s), then * remove the deprecated attribute selector */ function translateWithValueToCssClass(scope, element, attrs) { var updateFn = updateClassWithValue(element, className, attrs); var unwatch = attrs.$observe(attrs.$normalize(className), updateFn); updateFn(getNormalizedAttrValue(className, attrs, "")); scope.$on("$destroy", function() { unwatch() }); } } /** * Creates a registration function for ngMaterial Layout attribute directive. * This is a `simple` transpose of attribute usage to class usage; where we ignore * any attribute value */ function attributeWithoutValue(className) { return ['$mdUtil', '$interpolate', "$log", function(_$mdUtil_, _$interpolate_, _$log_) { $mdUtil = _$mdUtil_; $interpolate = _$interpolate_; $log = _$log_; return { restrict: 'A', compile: function(element, attr) { var linkFn; if (config.enabled) { // immediately replace static (non-interpolated) invalid values... validateAttributeValue( className, getNormalizedAttrValue(className, attr, ""), buildUpdateFn(element, className, attr) ); translateToCssClass(null, element); // Use for postLink to account for transforms after ng-transclude. linkFn = translateToCssClass; } return linkFn || angular.noop; } }; }]; /** * Add as transformed class selector, then * remove the deprecated attribute selector */ function translateToCssClass(scope, element) { element.addClass(className); } } /** * After link-phase, do NOT remove deprecated layout attribute selector. * Instead watch the attribute so interpolated data-bindings to layout * selectors will continue to be supported. * * $observe() the className and update with new class (after removing the last one) * * e.g. `layout="{{layoutDemo.direction}}"` will update... * * NOTE: The value must match one of the specified styles in the CSS. * For example `flex-gt-md="{{size}}` where `scope.size == 47` will NOT work since * only breakpoints for 0, 5, 10, 15... 100, 33, 34, 66, 67 are defined. * */ function updateClassWithValue(element, className) { var lastClass; return function updateClassFn(newValue) { var value = validateAttributeValue(className, newValue || ""); if ( angular.isDefined(value) ) { if (lastClass) element.removeClass(lastClass); lastClass = !value ? className : className + "-" + value.replace(WHITESPACE, "-"); element.addClass(lastClass); } }; } /** * Provide console warning that this layout attribute has been deprecated * */ function warnAttrNotSupported(className) { var parts = className.split("-"); return ["$log", function($log) { $log.warn(className + "has been deprecated. Please use a `" + parts[0] + "-gt-` variant."); return angular.noop; }]; } /** * Centralize warnings for known flexbox issues (especially IE-related issues) */ function validateAttributeUsage(className, attr, element, $log){ var message, usage, url; var nodeName = element[0].nodeName.toLowerCase(); switch(className.replace(SUFFIXES,"")) { case "flex": if ((nodeName == "md-button") || (nodeName == "fieldset")){ // @see https://github.com/philipwalton/flexbugs#9-some-html-elements-cant-be-flex-containers // Use
wrapper inside (preferred) or outside usage = "<" + nodeName + " " + className + ">"; url = "https://github.com/philipwalton/flexbugs#9-some-html-elements-cant-be-flex-containers"; message = "Markup '{0}' may not work as expected in IE Browsers. Consult '{1}' for details."; $log.warn( $mdUtil.supplant(message, [usage, url]) ); } } } /** * For the Layout attribute value, validate or replace with default * fallback value */ function validateAttributeValue(className, value, updateFn) { var origValue = value; if (!needsInterpolation(value)) { switch (className.replace(SUFFIXES,"")) { case 'layout' : if ( !findIn(value, LAYOUT_OPTIONS) ) { value = LAYOUT_OPTIONS[0]; // 'row'; } break; case 'flex' : if (!findIn(value, FLEX_OPTIONS)) { if (isNaN(value)) { value = ''; } } break; case 'flex-offset' : case 'flex-order' : if (!value || isNaN(+value)) { value = '0'; } break; case 'layout-align' : var axis = extractAlignAxis(value); value = $mdUtil.supplant("{main}-{cross}",axis); break; case 'layout-padding' : case 'layout-margin' : case 'layout-fill' : case 'layout-wrap' : case 'layout-no-wrap' : value = ''; break; } if (value != origValue) { (updateFn || angular.noop)(value); } } return value; } /** * Replace current attribute value with fallback value */ function buildUpdateFn(element, className, attrs) { return function updateAttrValue(fallback) { if (!needsInterpolation(fallback)) { // Do not modify the element's attribute value; so // uses '' will not // be affected. Just update the attrs value. attrs[attrs.$normalize(className)] = fallback; } }; } /** * See if the original value has interpolation symbols: * e.g. flex-gt-md="{{triggerPoint}}" */ function needsInterpolation(value) { return (value || "").indexOf($interpolate.startSymbol()) > -1; } function getNormalizedAttrValue(className, attrs, defaultVal) { var normalizedAttr = attrs.$normalize(className); return attrs[normalizedAttr] ? attrs[normalizedAttr].replace(WHITESPACE, "-") : defaultVal || null; } function findIn(item, list, replaceWith) { item = replaceWith && item ? item.replace(WHITESPACE, replaceWith) : item; var found = false; if (item) { list.forEach(function(it) { it = replaceWith ? it.replace(WHITESPACE, replaceWith) : it; found = found || (it === item); }); } return found; } function extractAlignAxis(attrValue) { var axis = { main : "start", cross: "stretch" }, values; attrValue = (attrValue || ""); if ( attrValue.indexOf("-") == 0 || attrValue.indexOf(" ") == 0) { // For missing main-axis values attrValue = "none" + attrValue; } values = attrValue.toLowerCase().trim().replace(WHITESPACE, "-").split("-"); if ( values.length && (values[0] === "space") ) { // for main-axis values of "space-around" or "space-between" values = [ values[0]+"-"+values[1],values[2] ]; } if ( values.length > 0 ) axis.main = values[0] || axis.main; if ( values.length > 1 ) axis.cross = values[1] || axis.cross; if ( ALIGNMENT_MAIN_AXIS.indexOf(axis.main) < 0 ) axis.main = "start"; if ( ALIGNMENT_CROSS_AXIS.indexOf(axis.cross) < 0 ) axis.cross = "stretch"; return axis; } })(); })(); (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 default ripple configuration */ angular.module('material.core') .factory('$mdButtonInkRipple', MdButtonInkRipple); function MdButtonInkRipple($mdInkRipple) { return { attach: function attachRipple(scope, element, options) { options = angular.extend(optionsForElement(element), options); return $mdInkRipple.attach(scope, element, 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"; /** * @ngdoc module * @name material.core.ripple * @description * Ripple */ angular.module('material.core') .factory('$mdInkRipple', InkRippleService) .directive('mdInkRipple', InkRippleDirective) .directive('mdNoInk', attrNoDirective) .directive('mdNoBar', attrNoDirective) .directive('mdNoStretch', attrNoDirective); var DURATION = 450; /** * @ngdoc directive * @name mdInkRipple * @module material.core.ripple * * @description * The `md-ink-ripple` directive allows you to specify the ripple color or id a ripple is allowed. * * @param {string|boolean} md-ink-ripple A color string `#FF0000` or boolean (`false` or `0`) for preventing ripple * * @usage * ### String values * * * Ripples in red * * * * Not rippling * * * * ### Interpolated values * * * Ripples with the return value of 'randomColor' function * * * * Ripples if 'canRipple' function return value is not 'false' or '0' * * */ function InkRippleDirective ($mdButtonInkRipple, $mdCheckboxInkRipple) { return { controller: angular.noop, link: function (scope, element, attr) { attr.hasOwnProperty('mdInkRippleCheckbox') ? $mdCheckboxInkRipple.attach(scope, element) : $mdButtonInkRipple.attach(scope, element); } }; } InkRippleDirective.$inject = ["$mdButtonInkRipple", "$mdCheckboxInkRipple"]; /** * @ngdoc service * @name $mdInkRipple * @module material.core.ripple * * @description * `$mdInkRipple` is a service for adding ripples to any element * * @usage * * app.factory('$myElementInkRipple', function($mdInkRipple) { * return { * attach: function (scope, element, options) { * return $mdInkRipple.attach(scope, element, angular.extend({ * center: false, * dimBackground: true * }, options)); * } * }; * }); * * app.controller('myController', function ($scope, $element, $myElementInkRipple) { * $scope.onClick = function (ev) { * $myElementInkRipple.attach($scope, angular.element(ev.target), { center: true }); * } * }); * */ /** * @ngdoc method * @name $mdInkRipple#attach * * @description * Attaching given scope, element and options to inkRipple controller * * @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 * * `center` - Whether the ripple should start from the center of the container element * * `dimBackground` - Whether the background should be dimmed with the ripple color * * `colorElement` - The element the ripple should take its color from, defined by css property `color` * * `fitRipple` - Whether the ripple should fill the element */ function InkRippleService ($injector) { return { attach: attach }; function attach (scope, element, options) { if (element.controller('mdNoInk')) return angular.noop; return $injector.instantiate(InkRippleCtrl, { $scope: scope, $element: element, rippleOptions: options }); } } InkRippleService.$inject = ["$injector"]; /** * Controller used by the ripple service in order to apply ripples * @ngInject */ function InkRippleCtrl ($scope, $element, rippleOptions, $window, $timeout, $mdUtil) { this.$window = $window; this.$timeout = $timeout; this.$mdUtil = $mdUtil; this.$scope = $scope; this.$element = $element; this.options = rippleOptions; this.mousedown = false; this.ripples = []; this.timeout = null; // Stores a reference to the most-recent ripple timeout this.lastRipple = null; $mdUtil.valueOnUse(this, 'container', this.createContainer); this.$element.addClass('md-ink-ripple'); // attach method for unit tests ($element.controller('mdInkRipple') || {}).createRipple = angular.bind(this, this.createRipple); ($element.controller('mdInkRipple') || {}).setColor = angular.bind(this, this.color); this.bindEvents(); } InkRippleCtrl.$inject = ["$scope", "$element", "rippleOptions", "$window", "$timeout", "$mdUtil"]; /** * Either remove or unlock any remaining ripples when the user mouses off of the element (either by * mouseup or mouseleave event) */ function autoCleanup (self, cleanupFn) { if ( self.mousedown || self.lastRipple ) { self.mousedown = false; self.$mdUtil.nextTick( angular.bind(self, cleanupFn), false); } } /** * Returns the color that the ripple should be (either based on CSS or hard-coded) * @returns {string} */ InkRippleCtrl.prototype.color = function (value) { var self = this; // If assigning a color value, apply it to background and the ripple color if (angular.isDefined(value)) { self._color = self._parseColor(value); } // If color lookup, use assigned, defined, or inherited return self._color || self._parseColor( self.inkRipple() ) || self._parseColor( getElementColor() ); /** * Finds the color element and returns its text color for use as default ripple color * @returns {string} */ function getElementColor () { var items = self.options && self.options.colorElement ? self.options.colorElement : []; var elem = items.length ? items[ 0 ] : self.$element[ 0 ]; return elem ? self.$window.getComputedStyle(elem).color : 'rgb(0,0,0)'; } }; /** * Updating the ripple colors based on the current inkRipple value * or the element's computed style color */ InkRippleCtrl.prototype.calculateColor = function () { return this.color(); }; /** * Takes a string color and converts it to RGBA format * @param color {string} * @param [multiplier] {int} * @returns {string} */ InkRippleCtrl.prototype._parseColor = function parseColor (color, multiplier) { multiplier = multiplier || 1; if (!color) return; if (color.indexOf('rgba') === 0) return color.replace(/\d?\.?\d*\s*\)\s*$/, (0.1 * multiplier).toString() + ')'); if (color.indexOf('rgb') === 0) return rgbToRGBA(color); if (color.indexOf('#') === 0) return hexToRGBA(color); /** * Converts hex value to RGBA string * @param color {string} * @returns {string} */ function hexToRGBA (color) { var hex = color[ 0 ] === '#' ? color.substr(1) : color, dig = hex.length / 3, red = hex.substr(0, dig), green = hex.substr(dig, dig), blue = hex.substr(dig * 2); if (dig === 1) { red += red; green += green; blue += blue; } return 'rgba(' + parseInt(red, 16) + ',' + parseInt(green, 16) + ',' + parseInt(blue, 16) + ',0.1)'; } /** * Converts an RGB color to RGBA * @param color {string} * @returns {string} */ function rgbToRGBA (color) { return color.replace(')', ', 0.1)').replace('(', 'a('); } }; /** * Binds events to the root element for */ InkRippleCtrl.prototype.bindEvents = function () { this.$element.on('mousedown', angular.bind(this, this.handleMousedown)); this.$element.on('mouseup touchend', angular.bind(this, this.handleMouseup)); this.$element.on('mouseleave', angular.bind(this, this.handleMouseup)); this.$element.on('touchmove', angular.bind(this, this.handleTouchmove)); }; /** * Create a new ripple on every mousedown event from the root element * @param event {MouseEvent} */ InkRippleCtrl.prototype.handleMousedown = function (event) { if ( this.mousedown ) return; // When jQuery is loaded, we have to get the original event if (event.hasOwnProperty('originalEvent')) event = event.originalEvent; this.mousedown = true; if (this.options.center) { this.createRipple(this.container.prop('clientWidth') / 2, this.container.prop('clientWidth') / 2); } else { // We need to calculate the relative coordinates if the target is a sublayer of the ripple element if (event.srcElement !== this.$element[0]) { var layerRect = this.$element[0].getBoundingClientRect(); var layerX = event.clientX - layerRect.left; var layerY = event.clientY - layerRect.top; this.createRipple(layerX, layerY); } else { this.createRipple(event.offsetX, event.offsetY); } } }; /** * Either remove or unlock any remaining ripples when the user mouses off of the element (either by * mouseup, touchend or mouseleave event) */ InkRippleCtrl.prototype.handleMouseup = function () { autoCleanup(this, this.clearRipples); }; /** * Either remove or unlock any remaining ripples when the user mouses off of the element (by * touchmove) */ InkRippleCtrl.prototype.handleTouchmove = function () { autoCleanup(this, this.deleteRipples); }; /** * Cycles through all ripples and attempts to remove them. */ InkRippleCtrl.prototype.deleteRipples = function () { for (var i = 0; i < this.ripples.length; i++) { this.ripples[ i ].remove(); } }; /** * Cycles through all ripples and attempts to remove them with fade. * Depending on logic within `fadeInComplete`, some removals will be postponed. */ InkRippleCtrl.prototype.clearRipples = function () { for (var i = 0; i < this.ripples.length; i++) { this.fadeInComplete(this.ripples[ i ]); } }; /** * Creates the ripple container element * @returns {*} */ InkRippleCtrl.prototype.createContainer = function () { var container = angular.element('
'); this.$element.append(container); return container; }; InkRippleCtrl.prototype.clearTimeout = function () { if (this.timeout) { this.$timeout.cancel(this.timeout); this.timeout = null; } }; InkRippleCtrl.prototype.isRippleAllowed = function () { var element = this.$element[0]; do { if (!element.tagName || element.tagName === 'BODY') break; if (element && angular.isFunction(element.hasAttribute)) { if (element.hasAttribute('disabled')) return false; if (this.inkRipple() === 'false' || this.inkRipple() === '0') return false; } } while (element = element.parentNode); return true; }; /** * The attribute `md-ink-ripple` may be a static or interpolated * color value OR a boolean indicator (used to disable ripples) */ InkRippleCtrl.prototype.inkRipple = function () { return this.$element.attr('md-ink-ripple'); }; /** * Creates a new ripple and adds it to the container. Also tracks ripple in `this.ripples`. * @param left * @param top */ InkRippleCtrl.prototype.createRipple = function (left, top) { if (!this.isRippleAllowed()) return; var ctrl = this; var ripple = angular.element('
'); var width = this.$element.prop('clientWidth'); var height = this.$element.prop('clientHeight'); var x = Math.max(Math.abs(width - left), left) * 2; var y = Math.max(Math.abs(height - top), top) * 2; var size = getSize(this.options.fitRipple, x, y); var color = this.calculateColor(); ripple.css({ left: left + 'px', top: top + 'px', background: 'black', width: size + 'px', height: size + 'px', backgroundColor: rgbaToRGB(color), borderColor: rgbaToRGB(color) }); this.lastRipple = ripple; // we only want one timeout to be running at a time this.clearTimeout(); this.timeout = this.$timeout(function () { ctrl.clearTimeout(); if (!ctrl.mousedown) ctrl.fadeInComplete(ripple); }, DURATION * 0.35, false); if (this.options.dimBackground) this.container.css({ backgroundColor: color }); this.container.append(ripple); this.ripples.push(ripple); ripple.addClass('md-ripple-placed'); this.$mdUtil.nextTick(function () { ripple.addClass('md-ripple-scaled md-ripple-active'); ctrl.$timeout(function () { ctrl.clearRipples(); }, DURATION, false); }, false); function rgbaToRGB (color) { return color ? color.replace('rgba', 'rgb').replace(/,[^\),]+\)/, ')') : 'rgb(0,0,0)'; } function getSize (fit, x, y) { return fit ? Math.max(x, y) : Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)); } }; /** * After fadeIn finishes, either kicks off the fade-out animation or queues the element for removal on mouseup * @param ripple */ InkRippleCtrl.prototype.fadeInComplete = function (ripple) { if (this.lastRipple === ripple) { if (!this.timeout && !this.mousedown) { this.removeRipple(ripple); } } else { this.removeRipple(ripple); } }; /** * Kicks off the animation for removing a ripple * @param ripple {Element} */ InkRippleCtrl.prototype.removeRipple = function (ripple) { var ctrl = this; var index = this.ripples.indexOf(ripple); if (index < 0) return; this.ripples.splice(this.ripples.indexOf(ripple), 1); ripple.removeClass('md-ripple-active'); if (this.ripples.length === 0) this.container.css({ backgroundColor: '' }); // use a 2-second timeout in order to allow for the animation to finish // we don't actually care how long the animation takes this.$timeout(function () { ctrl.fadeOutComplete(ripple); }, DURATION, false); }; /** * Removes the provided ripple from the DOM * @param ripple */ InkRippleCtrl.prototype.fadeOutComplete = function (ripple) { ripple.remove(); this.lastRipple = null; }; /** * Used to create an empty directive. This is used to track flag-directives whose children may have * functionality based on them. * * Example: `md-no-ink` will potentially be used by all child directives. */ function attrNoDirective () { 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 A100', 'contrastStrongLightColors': '400 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 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': '50 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': '600 700 800 900 A700', 'contrastStrongLightColors': '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': '700 800 900', 'contrastStrongLightColors': '700 800 900' }, 'teal': { '50': '#e0f2f1', '100': '#b2dfdb', '200': '#80cbc4', '300': '#4db6ac', '400': '#26a69a', '500': '#009688', '600': '#00897b', '700': '#00796b', '800': '#00695c', '900': '#004d40', 'A100': '#a7ffeb', 'A200': '#64ffda', 'A400': '#1de9b6', 'A700': '#00bfa5', 'contrastDefaultColor': 'dark', 'contrastLightColors': '500 600 700 800 900', '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': '600 700 800 900', 'contrastStrongLightColors': '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': '700 800 900', 'contrastStrongLightColors': '700 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(generateAllThemes); /** * @ngdoc service * @name $mdThemingProvider * @module material.core.theming * * @description Provider to configure the `$mdTheming` service. */ /** * @ngdoc method * @name $mdThemingProvider#setNonce * @param {string} nonceValue The nonce to be added as an attribute to the theme style tags. * Setting a value allows the use CSP policy without using the unsafe-inline directive. */ /** * @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-100-contrast}} - grab shade 100's contrast color * {{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': '600', 'hue-2': '300', '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' ]; // Whether or not themes are to be generated on-demand (vs. eagerly). var generateOnDemand = false; // Nonce to be added as an attribute to the generated themes style tags. var nonce = null; 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, setNonce: function(nonceValue) { nonce = nonceValue; }, setDefaultTheme: function(theme) { defaultTheme = theme; }, alwaysWatchTheme: function(alwaysWatch) { alwaysWatchTheme = alwaysWatch; }, generateThemesOnDemand: function(onDemand) { generateOnDemand = onDemand; }, $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 == 'default' ? '' : defaultTheme); }, changeTheme); el.on('$destroy', deregisterWatch); } else { var theme = ctrl && ctrl.$mdTheme || (defaultTheme == 'default' ? '' : defaultTheme); changeTheme(theme); } function changeTheme(theme) { if (!theme) return; 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); if (ctrl) { el.data('$mdThemeController', ctrl); } } }; applyTheme.THEMES = angular.extend({}, THEMES); applyTheme.defaultTheme = function() { return defaultTheme; }; applyTheme.registered = registered; applyTheme.generateTheme = function(name) { generateTheme(name, nonce); }; 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*)?(contrast)?\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, contrast) { 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] || '')[contrast ? 'contrast' : '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') { var themeRuleRegex = /((?:(?:(?: |>|\.|\w|-|:|\(|\)|\[|\]|"|'|=)+) )?)((?:(?:\w|\.|-)+)?)\.md-default-theme((?: |>|\.|\w|-|:|\(|\)|\[|\]|"|'|=)*)/g; newRule = newRule.replace(themeRuleRegex, function(match, prefix, target, suffix) { return match + ', ' + prefix + target + suffix; }); } generatedRules.push(newRule); }); return generatedRules; } var rulesByType = {}; // Generate our themes at run time given the state of THEMES and PALETTES function generateAllThemes($injector) { var head = document.head; 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 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; }); // If themes are being generated on-demand, quit here. The user will later manually // call generateTheme to do this on a theme-by-theme basis. if (generateOnDemand) return; angular.forEach(THEMES, function(theme) { if (!GENERATED[theme.name]) { generateTheme(theme.name, nonce); } }); // ************************* // 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; } } } }); } } generateAllThemes.$inject = ["$injector"]; function generateTheme(name, nonce) { var theme = THEMES[name]; var head = document.head; var firstChild = head ? head.firstElementChild : null; if (!GENERATED[name]) { // For each theme, use the color palettes specified for // `primary`, `warn` and `accent` to generate CSS rules. THEME_COLOR_TYPES.forEach(function(colorType) { var styleStrings = parseRules(theme, colorType, rulesByType[colorType]); while (styleStrings.length) { var styleContent = styleStrings.shift(); if (styleContent) { var style = document.createElement('style'); style.setAttribute('md-theme-style', ''); if (nonce) { style.setAttribute('nonce', nonce); } style.appendChild(document.createTextNode(styleContent)); 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; } } 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"; // Polyfill angular < 1.4 (provide $animateCss) angular .module('material.core') .factory('$$mdAnimate', ["$q", "$timeout", "$mdConstant", "$animateCss", function($q, $timeout, $mdConstant, $animateCss){ // Since $$mdAnimate is injected into $mdUtil... use a wrapper function // to subsequently inject $mdUtil as an argument to the AnimateDomUtils return function($mdUtil) { return AnimateDomUtils( $mdUtil, $q, $timeout, $mdConstant, $animateCss); }; }]); /** * Factory function that requires special injections */ function AnimateDomUtils($mdUtil, $q, $timeout, $mdConstant, $animateCss) { var self; return self = { /** * */ translate3d : function( target, from, to, options ) { return $animateCss(target,{ from:from, to:to, addClass:options.transitionInClass }) .start() .then(function(){ // Resolve with reverser function... return reverseTranslate; }); /** * Specific reversal of the request translate animation above... */ function reverseTranslate (newFrom) { return $animateCss(target, { to: newFrom || from, addClass: options.transitionOutClass, removeClass: options.transitionInClass }).start(); } }, /** * Listen for transitionEnd event (with optional timeout) * Announce completion or failure via promise handlers */ waitTransitionEnd: function (element, opts) { var TIMEOUT = 3000; // fallback is 3 secs return $q(function(resolve, reject){ opts = opts || { }; var timer = $timeout(finished, opts.timeout || TIMEOUT); element.on($mdConstant.CSS.TRANSITIONEND, finished); /** * Upon timeout or transitionEnd, reject or resolve (respectively) this promise. * NOTE: Make sure this transitionEnd didn't bubble up from a child */ function finished(ev) { if ( ev && ev.target !== element[0]) return; if ( ev ) $timeout.cancel(timer); element.off($mdConstant.CSS.TRANSITIONEND, finished); // Never reject since ngAnimate may cause timeouts due missed transitionEnd events resolve(); } }); }, /** * Calculate the zoom transform from dialog to origin. * * We use this to set the dialog position immediately; * then the md-transition-in actually translates back to * `translate3d(0,0,0) scale(1.0)`... * * NOTE: all values are rounded to the nearest integer */ calculateZoomToOrigin: function (element, originator) { var origin = originator.element; var bounds = originator.bounds; var zoomTemplate = "translate3d( {centerX}px, {centerY}px, 0 ) scale( {scaleX}, {scaleY} )"; var buildZoom = angular.bind(null, $mdUtil.supplant, zoomTemplate); var zoomStyle = buildZoom({centerX: 0, centerY: 0, scaleX: 0.5, scaleY: 0.5}); if (origin || bounds) { var originBnds = origin ? self.clientRect(origin) || currentBounds() : self.copyRect(bounds); var dialogRect = self.copyRect(element[0].getBoundingClientRect()); var dialogCenterPt = self.centerPointFor(dialogRect); var originCenterPt = self.centerPointFor(originBnds); // Build the transform to zoom from the dialog center to the origin center zoomStyle = buildZoom({ centerX: originCenterPt.x - dialogCenterPt.x, centerY: originCenterPt.y - dialogCenterPt.y, scaleX: Math.round(100 * Math.min(0.5, originBnds.width / dialogRect.width))/100, scaleY: Math.round(100 * Math.min(0.5, originBnds.height / dialogRect.height))/100 }); } return zoomStyle; /** * This is a fallback if the origin information is no longer valid, then the * origin bounds simply becomes the current bounds for the dialogContainer's parent */ function currentBounds() { var cntr = element ? element.parent() : null; var parent = cntr ? cntr.parent() : null; return parent ? self.clientRect(parent) : null; } }, /** * Enhance raw values to represent valid css stylings... */ toCss : function( raw ) { var css = { }; var lookups = 'left top right bottom width height x y min-width min-height max-width max-height'; angular.forEach(raw, function(value,key) { if ( angular.isUndefined(value) ) return; if ( lookups.indexOf(key) >= 0 ) { css[key] = value + 'px'; } else { switch (key) { case 'transition': convertToVendor(key, $mdConstant.CSS.TRANSITION, value); break; case 'transform': convertToVendor(key, $mdConstant.CSS.TRANSFORM, value); break; case 'transformOrigin': convertToVendor(key, $mdConstant.CSS.TRANSFORM_ORIGIN, value); break; } } }); return css; function convertToVendor(key, vendor, value) { angular.forEach(vendor.split(' '), function (key) { css[key] = value; }); } }, /** * Convert the translate CSS value to key/value pair(s). */ toTransformCss: function (transform, addTransition, transition) { var css = {}; angular.forEach($mdConstant.CSS.TRANSFORM.split(' '), function (key) { css[key] = transform; }); if (addTransition) { transition = transition || "all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) !important"; css['transition'] = transition; } return css; }, /** * Clone the Rect and calculate the height/width if needed */ copyRect: function (source, destination) { if (!source) return null; destination = destination || {}; angular.forEach('left top right bottom width height'.split(' '), function (key) { destination[key] = Math.round(source[key]) }); destination.width = destination.width || (destination.right - destination.left); destination.height = destination.height || (destination.bottom - destination.top); return destination; }, /** * Calculate ClientRect of element; return null if hidden or zero size */ clientRect: function (element) { var bounds = angular.element(element)[0].getBoundingClientRect(); var isPositiveSizeClientRect = function (rect) { return rect && (rect.width > 0) && (rect.height > 0); }; // If the event origin element has zero size, it has probably been hidden. return isPositiveSizeClientRect(bounds) ? self.copyRect(bounds) : null; }, /** * Calculate 'rounded' center point of Rect */ centerPointFor: function (targetRect) { return targetRect ? { x: Math.round(targetRect.left + (targetRect.width / 2)), y: Math.round(targetRect.top + (targetRect.height / 2)) } : { x : 0, y : 0 }; } }; }; })(); (function(){ "use strict"; "use strict"; if (angular.version.minor >= 4) { angular.module('material.core.animate', []); } else { (function() { var forEach = angular.forEach; var WEBKIT = angular.isDefined(document.documentElement.style.WebkitAppearance); var TRANSITION_PROP = WEBKIT ? 'WebkitTransition' : 'transition'; var ANIMATION_PROP = WEBKIT ? 'WebkitAnimation' : 'animation'; var PREFIX = WEBKIT ? '-webkit-' : ''; var TRANSITION_EVENTS = (WEBKIT ? 'webkitTransitionEnd ' : '') + 'transitionend'; var ANIMATION_EVENTS = (WEBKIT ? 'webkitAnimationEnd ' : '') + 'animationend'; var $$ForceReflowFactory = ['$document', function($document) { return function() { return $document[0].body.clientWidth + 1; } }]; var $$rAFMutexFactory = ['$$rAF', function($$rAF) { return function() { var passed = false; $$rAF(function() { passed = true; }); return function(fn) { passed ? fn() : $$rAF(fn); }; }; }]; var $$AnimateRunnerFactory = ['$q', '$$rAFMutex', function($q, $$rAFMutex) { var INITIAL_STATE = 0; var DONE_PENDING_STATE = 1; var DONE_COMPLETE_STATE = 2; function AnimateRunner(host) { this.setHost(host); this._doneCallbacks = []; this._runInAnimationFrame = $$rAFMutex(); this._state = 0; } AnimateRunner.prototype = { setHost: function(host) { this.host = host || {}; }, done: function(fn) { if (this._state === DONE_COMPLETE_STATE) { fn(); } else { this._doneCallbacks.push(fn); } }, progress: angular.noop, getPromise: function() { if (!this.promise) { var self = this; this.promise = $q(function(resolve, reject) { self.done(function(status) { status === false ? reject() : resolve(); }); }); } return this.promise; }, then: function(resolveHandler, rejectHandler) { return this.getPromise().then(resolveHandler, rejectHandler); }, 'catch': function(handler) { return this.getPromise()['catch'](handler); }, 'finally': function(handler) { return this.getPromise()['finally'](handler); }, pause: function() { if (this.host.pause) { this.host.pause(); } }, resume: function() { if (this.host.resume) { this.host.resume(); } }, end: function() { if (this.host.end) { this.host.end(); } this._resolve(true); }, cancel: function() { if (this.host.cancel) { this.host.cancel(); } this._resolve(false); }, complete: function(response) { var self = this; if (self._state === INITIAL_STATE) { self._state = DONE_PENDING_STATE; self._runInAnimationFrame(function() { self._resolve(response); }); } }, _resolve: function(response) { if (this._state !== DONE_COMPLETE_STATE) { forEach(this._doneCallbacks, function(fn) { fn(response); }); this._doneCallbacks.length = 0; this._state = DONE_COMPLETE_STATE; } } }; return AnimateRunner; }]; angular .module('material.core.animate', []) .factory('$$forceReflow', $$ForceReflowFactory) .factory('$$AnimateRunner', $$AnimateRunnerFactory) .factory('$$rAFMutex', $$rAFMutexFactory) .factory('$animateCss', ['$window', '$$rAF', '$$AnimateRunner', '$$forceReflow', '$$jqLite', '$timeout', function($window, $$rAF, $$AnimateRunner, $$forceReflow, $$jqLite, $timeout) { function init(element, options) { var temporaryStyles = []; var node = getDomNode(element); if (options.transitionStyle) { temporaryStyles.push([PREFIX + 'transition', options.transitionStyle]); } if (options.keyframeStyle) { temporaryStyles.push([PREFIX + 'animation', options.keyframeStyle]); } if (options.delay) { temporaryStyles.push([PREFIX + 'transition-delay', options.delay + 's']); } if (options.duration) { temporaryStyles.push([PREFIX + 'transition-duration', options.duration + 's']); } var hasCompleteStyles = options.keyframeStyle || (options.to && (options.duration > 0 || options.transitionStyle)); var hasCompleteClasses = !!options.addClass || !!options.removeClass; var hasCompleteAnimation = hasCompleteStyles || hasCompleteClasses; blockTransition(element, true); applyAnimationFromStyles(element, options); var animationClosed = false; var events, eventFn; return { close: $window.close, start: function() { var runner = new $$AnimateRunner(); waitUntilQuiet(function() { blockTransition(element, false); if (!hasCompleteAnimation) { return close(); } forEach(temporaryStyles, function(entry) { var key = entry[0]; var value = entry[1]; node.style[camelCase(key)] = value; }); applyClasses(element, options); var timings = computeTimings(element); if (timings.duration === 0) { return close(); } var moreStyles = []; if (options.easing) { if (timings.transitionDuration) { moreStyles.push([PREFIX + 'transition-timing-function', options.easing]); } if (timings.animationDuration) { moreStyles.push([PREFIX + 'animation-timing-function', options.easing]); } } if (options.delay && timings.animationDelay) { moreStyles.push([PREFIX + 'animation-delay', options.delay + 's']); } if (options.duration && timings.animationDuration) { moreStyles.push([PREFIX + 'animation-duration', options.duration + 's']); } forEach(moreStyles, function(entry) { var key = entry[0]; var value = entry[1]; node.style[camelCase(key)] = value; temporaryStyles.push(entry); }); var maxDelay = timings.delay; var maxDelayTime = maxDelay * 1000; var maxDuration = timings.duration; var maxDurationTime = maxDuration * 1000; var startTime = Date.now(); events = []; if (timings.transitionDuration) { events.push(TRANSITION_EVENTS); } if (timings.animationDuration) { events.push(ANIMATION_EVENTS); } events = events.join(' '); eventFn = function(event) { event.stopPropagation(); var ev = event.originalEvent || event; var timeStamp = ev.timeStamp || Date.now(); var elapsedTime = parseFloat(ev.elapsedTime.toFixed(3)); if (Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) { close(); } }; element.on(events, eventFn); applyAnimationToStyles(element, options); $timeout(close, maxDelayTime + maxDurationTime * 1.5, false); }); return runner; function close() { if (animationClosed) return; animationClosed = true; if (events && eventFn) { element.off(events, eventFn); } applyClasses(element, options); applyAnimationStyles(element, options); forEach(temporaryStyles, function(entry) { node.style[camelCase(entry[0])] = ''; }); runner.complete(true); return runner; } } } } function applyClasses(element, options) { if (options.addClass) { $$jqLite.addClass(element, options.addClass); options.addClass = null; } if (options.removeClass) { $$jqLite.removeClass(element, options.removeClass); options.removeClass = null; } } function computeTimings(element) { var node = getDomNode(element); var cs = $window.getComputedStyle(node) var tdr = parseMaxTime(cs[prop('transitionDuration')]); var adr = parseMaxTime(cs[prop('animationDuration')]); var tdy = parseMaxTime(cs[prop('transitionDelay')]); var ady = parseMaxTime(cs[prop('animationDelay')]); adr *= (parseInt(cs[prop('animationIterationCount')], 10) || 1); var duration = Math.max(adr, tdr); var delay = Math.max(ady, tdy); return { duration: duration, delay: delay, animationDuration: adr, transitionDuration: tdr, animationDelay: ady, transitionDelay: tdy }; function prop(key) { return WEBKIT ? 'Webkit' + key.charAt(0).toUpperCase() + key.substr(1) : key; } } function parseMaxTime(str) { var maxValue = 0; var values = (str || "").split(/\s*,\s*/); forEach(values, function(value) { // it's always safe to consider only second values and omit `ms` values since // getComputedStyle will always handle the conversion for us if (value.charAt(value.length - 1) == 's') { value = value.substring(0, value.length - 1); } value = parseFloat(value) || 0; maxValue = maxValue ? Math.max(value, maxValue) : value; }); return maxValue; } var cancelLastRAFRequest; var rafWaitQueue = []; function waitUntilQuiet(callback) { if (cancelLastRAFRequest) { cancelLastRAFRequest(); //cancels the request } rafWaitQueue.push(callback); cancelLastRAFRequest = $$rAF(function() { cancelLastRAFRequest = null; // DO NOT REMOVE THIS LINE OR REFACTOR OUT THE `pageWidth` variable. // PLEASE EXAMINE THE `$$forceReflow` service to understand why. var pageWidth = $$forceReflow(); // we use a for loop to ensure that if the queue is changed // during this looping then it will consider new requests for (var i = 0; i < rafWaitQueue.length; i++) { rafWaitQueue[i](pageWidth); } rafWaitQueue.length = 0; }); } function applyAnimationStyles(element, options) { applyAnimationFromStyles(element, options); applyAnimationToStyles(element, options); } function applyAnimationFromStyles(element, options) { if (options.from) { element.css(options.from); options.from = null; } } function applyAnimationToStyles(element, options) { if (options.to) { element.css(options.to); options.to = null; } } function getDomNode(element) { for (var i = 0; i < element.length; i++) { if (element[i].nodeType === 1) return element[i]; } } function blockTransition(element, bool) { var node = getDomNode(element); var key = camelCase(PREFIX + 'transition-delay'); node.style[key] = bool ? '-9999s' : ''; } return init; }]); /** * Older browsers [FF31] expect camelCase * property keys. * e.g. * animation-duration --> animationDuration */ function camelCase(str) { return str.replace(/-[a-z]/g, function(str) { return str.charAt(1).toUpperCase(); }); } })(); } })(); (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', 'material.components.virtualRepeat' ]); })(); (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', ["$mdTheming", "$animate", "$rootElement", "$window", "$log", "$$rAF", "$document", function BackdropDirective($mdTheming, $animate, $rootElement, $window, $log, $$rAF, $document) { var ERROR_CSS_POSITION = " may not work properly in a scrolled, static-positioned parent container."; return { restrict: 'E', link: postLink }; function postLink(scope, element, attrs) { // If body scrolling has been disabled using mdUtil.disableBodyScroll(), // adjust the 'backdrop' height to account for the fixed 'body' top offset var body = $window.getComputedStyle($document[0].body); if (body.position == 'fixed') { var hViewport = parseInt(body.height, 10) + Math.abs(parseInt(body.top, 10)); element.css({ height: hViewport + 'px' }); } // backdrop may be outside the $rootElement, tell ngAnimate to animate regardless if ($animate.pin) $animate.pin(element, $rootElement); $$rAF(function () { // Often $animate.enter() is used to append the backDrop element // so let's wait until $animate is done... var parent = element.parent()[0]; if (parent) { if ( parent.nodeName == 'BODY' ) { element.css({position : 'fixed'}); } var styles = $window.getComputedStyle(parent); if (styles.position == 'static') { // backdrop uses position:absolute and will not work properly with parent position:static (default) $log.warn(ERROR_CSS_POSITION); } } $mdTheming.inherit(element, element.parent()); }); } }]); })(); (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); /* @ngInject */ function MdBottomSheetDirective($mdBottomSheet) { return { restrict: 'E', link : function postLink(scope, element, attr) { // When navigation force destroys an interimElement, then // listen and $destroy() that interim instance... scope.$on('$destroy', function() { $mdBottomSheet.destroy(); }); } }; } MdBottomSheetDirective.$inject = ["$mdBottomSheet"]; /** * @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. * - `clickOutsideToClose` - `{boolean=}`: Whether the user can click outside the bottom sheet to * close it. Default true. * - `escapeToClose` - `{boolean=}`: Whether the user can press escape to close the bottom sheet. * Default true. * - `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", "$mdTheming", "$mdBottomSheet", "$rootElement", "$mdGesture"]; return $$interimElementProvider('$mdBottomSheet') .setDefaults({ methods: ['disableParentScroll', 'escapeToClose', 'clickOutsideToClose'], options: bottomSheetDefaults }); /* @ngInject */ function bottomSheetDefaults($animate, $mdConstant, $mdUtil, $mdTheming, $mdBottomSheet, $rootElement, $mdGesture) { var backdrop; return { themable: true, onShow: onShow, onRemove: onRemove, escapeToClose: true, clickOutsideToClose: true, disableParentScroll: true }; function onShow(scope, element, options, controller) { element = $mdUtil.extractElementByName(element, 'md-bottom-sheet'); // Add a backdrop that will close on click backdrop = $mdUtil.createBackdrop(scope, "md-bottom-sheet-backdrop md-opaque"); if (options.clickOutsideToClose) { backdrop.on('click', function() { $mdUtil.nextTick($mdBottomSheet.cancel,true); }); } $mdTheming.inherit(backdrop, options.parent); $animate.enter(backdrop, options.parent, null); var bottomSheet = new BottomSheet(element, options.parent); options.bottomSheet = bottomSheet; $mdTheming.inherit(bottomSheet.element, options.parent); if (options.disableParentScroll) { options.restoreScroll = $mdUtil.disableScrollAround(bottomSheet.element, options.parent); } return $animate.enter(bottomSheet.element, options.parent) .then(function() { var focusable = $mdUtil.findFocusTarget(element) || 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) { $mdUtil.nextTick($mdBottomSheet.cancel,true); } }; $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.restoreScroll(); delete options.restoreScroll; } bottomSheet.cleanup(); }); } /** * 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); parent.off('$md.drag', onDrag); parent.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'); $mdUtil.nextTick($mdBottomSheet.cancel,true); } 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) { $mdTheming(element); $mdButtonInkRipple.attach(scope, element); // Use async expect to support possible bindings in the button label $mdAria.expectWithText(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(ev) { 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, similar to ``. * You can then position buttons using layout attributes. * * Card is built with: * * `` - Header for the card, holds avatar, text and squared image * - `` - Card avatar * - `md-user-avatar` - Class for user image * - `` * - `` - Contains elements for the card description * - `md-title` - Class for the card title * - `md-subhead` - Class for the card sub header * * `` - Image for the card * * `` - Card content title * - `` * - `md-headline` - Class for the card content title * - `md-subhead` - Class for the card content sub header * - `` - Squared image within the title * - `md-media-sm` - Class for small image * - `md-media-md` - Class for medium image * - `md-media-lg` - Class for large image * * `` - Card content * - `md-media-xl` - Class for extra large image * * `` - Card actions * - `` - Icon actions * * 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 * *
*
* * ### Card with header, image, title actions and content * * * * * * * * Title * Sub header * * * image caption * * * Card headline * Card subheader * * * * Action 1 * Action 2 * * * * * * * *

* Card content *

*
*
*
*/ function mdCardDirective($mdTheming) { return { restrict: 'E', link: function ($scope, $element) { $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, $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); // Attach a click handler in compile in order to immediately stop propagation // (especially for ng-click) when the checkbox is disabled. tElement.on('click', function(event) { if (this.hasAttribute('disabled')) { event.stopImmediatePropagation(); } }); 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", "$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"; (function() { 'use strict'; /** * @ngdoc module * @name material.components.datepicker * @description Datepicker */ angular.module('material.components.datepicker', [ 'material.core', 'material.components.icon', 'material.components.virtualRepeat' ]).directive('mdCalendar', calendarDirective); // POST RELEASE // TODO(jelbourn): Mac Cmd + left / right == Home / End // TODO(jelbourn): Clicking on the month label opens the month-picker. // TODO(jelbourn): Minimum and maximum date // TODO(jelbourn): Refactor month element creation to use cloneNode (performance). // TODO(jelbourn): Define virtual scrolling constants (compactness) users can override. // TODO(jelbourn): Animated month transition on ng-model change (virtual-repeat) // TODO(jelbourn): Scroll snapping (virtual repeat) // TODO(jelbourn): Remove superfluous row from short months (virtual-repeat) // TODO(jelbourn): Month headers stick to top when scrolling. // TODO(jelbourn): Previous month opacity is lowered when partially scrolled out of view. // TODO(jelbourn): Support md-calendar standalone on a page (as a tabstop w/ aria-live // announcement and key handling). // Read-only calendar (not just date-picker). /** * Height of one calendar month tbody. This must be made known to the virtual-repeat and is * subsequently used for scrolling to specific months. */ var TBODY_HEIGHT = 265; /** * Height of a calendar month with a single row. This is needed to calculate the offset for * rendering an extra month in virtual-repeat that only contains one row. */ var TBODY_SINGLE_ROW_HEIGHT = 45; function calendarDirective() { return { template: '' + '
' + '' + '' + '' + '
' + '
' + '
', scope: { minDate: '=mdMinDate', maxDate: '=mdMaxDate', dateFilter: '=mdDateFilter', }, require: ['ngModel', 'mdCalendar'], controller: CalendarCtrl, controllerAs: 'ctrl', bindToController: true, link: function(scope, element, attrs, controllers) { var ngModelCtrl = controllers[0]; var mdCalendarCtrl = controllers[1]; mdCalendarCtrl.configureNgModel(ngModelCtrl); } }; } /** Class applied to the selected date cell/. */ var SELECTED_DATE_CLASS = 'md-calendar-selected-date'; /** Class applied to the focused date cell/. */ var FOCUSED_DATE_CLASS = 'md-focus'; /** Next identifier for calendar instance. */ var nextUniqueId = 0; /** The first renderable date in the virtual-scrolling calendar (for all instances). */ var firstRenderableDate = null; /** * Controller for the mdCalendar component. * @ngInject @constructor */ function CalendarCtrl($element, $attrs, $scope, $animate, $q, $mdConstant, $mdTheming, $$mdDateUtil, $mdDateLocale, $mdInkRipple, $mdUtil) { $mdTheming($element); /** * Dummy array-like object for virtual-repeat to iterate over. The length is the total * number of months that can be viewed. This is shorter than ideal because of (potential) * Firefox bug https://bugzilla.mozilla.org/show_bug.cgi?id=1181658. */ this.items = {length: 2000}; if (this.maxDate && this.minDate) { // Limit the number of months if min and max dates are set. var numMonths = $$mdDateUtil.getMonthDistance(this.minDate, this.maxDate) + 1; numMonths = Math.max(numMonths, 1); // Add an additional month as the final dummy month for rendering purposes. numMonths += 1; this.items.length = numMonths; } /** @final {!angular.$animate} */ this.$animate = $animate; /** @final {!angular.$q} */ this.$q = $q; /** @final */ this.$mdInkRipple = $mdInkRipple; /** @final */ this.$mdUtil = $mdUtil; /** @final */ this.keyCode = $mdConstant.KEY_CODE; /** @final */ this.dateUtil = $$mdDateUtil; /** @final */ this.dateLocale = $mdDateLocale; /** @final {!angular.JQLite} */ this.$element = $element; /** @final {!angular.Scope} */ this.$scope = $scope; /** @final {HTMLElement} */ this.calendarElement = $element[0].querySelector('.md-calendar'); /** @final {HTMLElement} */ this.calendarScroller = $element[0].querySelector('.md-virtual-repeat-scroller'); /** @final {Date} */ this.today = this.dateUtil.createDateAtMidnight(); /** @type {Date} */ this.firstRenderableDate = this.dateUtil.incrementMonths(this.today, -this.items.length / 2); if (this.minDate && this.minDate > this.firstRenderableDate) { this.firstRenderableDate = this.minDate; } else if (this.maxDate) { // Calculate the difference between the start date and max date. // Subtract 1 because it's an inclusive difference and 1 for the final dummy month. // var monthDifference = this.items.length - 2; this.firstRenderableDate = this.dateUtil.incrementMonths(this.maxDate, -(this.items.length - 2)); } /** @final {number} Unique ID for this calendar instance. */ this.id = nextUniqueId++; /** @type {!angular.NgModelController} */ this.ngModelCtrl = null; /** * The selected date. Keep track of this separately from the ng-model value so that we * can know, when the ng-model value changes, what the previous value was before its updated * in the component's UI. * * @type {Date} */ this.selectedDate = null; /** * The date that is currently focused or showing in the calendar. This will initially be set * to the ng-model value if set, otherwise to today. It will be updated as the user navigates * to other months. The cell corresponding to the displayDate does not necesarily always have * focus in the document (such as for cases when the user is scrolling the calendar). * @type {Date} */ this.displayDate = null; /** * The date that has or should have focus. * @type {Date} */ this.focusDate = null; /** @type {boolean} */ this.isInitialized = false; /** @type {boolean} */ this.isMonthTransitionInProgress = false; // Unless the user specifies so, the calendar should not be a tab stop. // This is necessary because ngAria might add a tabindex to anything with an ng-model // (based on whether or not the user has turned that particular feature on/off). if (!$attrs['tabindex']) { $element.attr('tabindex', '-1'); } var self = this; /** * Handles a click event on a date cell. * Created here so that every cell can use the same function instance. * @this {HTMLTableCellElement} The cell that was clicked. */ this.cellClickHandler = function() { var cellElement = this; if (this.hasAttribute('data-timestamp')) { $scope.$apply(function() { var timestamp = Number(cellElement.getAttribute('data-timestamp')); self.setNgModelValue(self.dateUtil.createDateAtMidnight(timestamp)); }); } }; this.attachCalendarEventListeners(); } CalendarCtrl.$inject = ["$element", "$attrs", "$scope", "$animate", "$q", "$mdConstant", "$mdTheming", "$$mdDateUtil", "$mdDateLocale", "$mdInkRipple", "$mdUtil"]; /*** Initialization ***/ /** * Sets up the controller's reference to ngModelController. * @param {!angular.NgModelController} ngModelCtrl */ CalendarCtrl.prototype.configureNgModel = function(ngModelCtrl) { this.ngModelCtrl = ngModelCtrl; var self = this; ngModelCtrl.$render = function() { self.changeSelectedDate(self.ngModelCtrl.$viewValue); }; }; /** * Initialize the calendar by building the months that are initially visible. * Initialization should occur after the ngModel value is known. */ CalendarCtrl.prototype.buildInitialCalendarDisplay = function() { this.buildWeekHeader(); this.hideVerticalScrollbar(); this.displayDate = this.selectedDate || this.today; this.isInitialized = true; }; /** * Hides the vertical scrollbar on the calendar scroller by setting the width on the * calendar scroller and the `overflow: hidden` wrapper around the scroller, and then setting * a padding-right on the scroller equal to the width of the browser's scrollbar. * * This will cause a reflow. */ CalendarCtrl.prototype.hideVerticalScrollbar = function() { var element = this.$element[0]; var scrollMask = element.querySelector('.md-calendar-scroll-mask'); var scroller = this.calendarScroller; var headerWidth = element.querySelector('.md-calendar-day-header').clientWidth; var scrollbarWidth = scroller.offsetWidth - scroller.clientWidth; scrollMask.style.width = headerWidth + 'px'; scroller.style.width = (headerWidth + scrollbarWidth) + 'px'; scroller.style.paddingRight = scrollbarWidth + 'px'; }; /** Attach event listeners for the calendar. */ CalendarCtrl.prototype.attachCalendarEventListeners = function() { // Keyboard interaction. this.$element.on('keydown', angular.bind(this, this.handleKeyEvent)); }; /*** User input handling ***/ /** * Handles a key event in the calendar with the appropriate action. The action will either * be to select the focused date or to navigate to focus a new date. * @param {KeyboardEvent} event */ CalendarCtrl.prototype.handleKeyEvent = function(event) { var self = this; this.$scope.$apply(function() { // Capture escape and emit back up so that a wrapping component // (such as a date-picker) can decide to close. if (event.which == self.keyCode.ESCAPE || event.which == self.keyCode.TAB) { self.$scope.$emit('md-calendar-close'); if (event.which == self.keyCode.TAB) { event.preventDefault(); } return; } // Remaining key events fall into two categories: selection and navigation. // Start by checking if this is a selection event. if (event.which === self.keyCode.ENTER) { self.setNgModelValue(self.displayDate); event.preventDefault(); return; } // Selection isn't occuring, so the key event is either navigation or nothing. var date = self.getFocusDateFromKeyEvent(event); if (date) { date = self.boundDateByMinAndMax(date); event.preventDefault(); event.stopPropagation(); // Since this is a keyboard interaction, actually give the newly focused date keyboard // focus after the been brought into view. self.changeDisplayDate(date).then(function () { self.focus(date); }); } }); }; /** * Gets the date to focus as the result of a key event. * @param {KeyboardEvent} event * @returns {Date} Date to navigate to, or null if the key does not match a calendar shortcut. */ CalendarCtrl.prototype.getFocusDateFromKeyEvent = function(event) { var dateUtil = this.dateUtil; var keyCode = this.keyCode; switch (event.which) { case keyCode.RIGHT_ARROW: return dateUtil.incrementDays(this.displayDate, 1); case keyCode.LEFT_ARROW: return dateUtil.incrementDays(this.displayDate, -1); case keyCode.DOWN_ARROW: return event.metaKey ? dateUtil.incrementMonths(this.displayDate, 1) : dateUtil.incrementDays(this.displayDate, 7); case keyCode.UP_ARROW: return event.metaKey ? dateUtil.incrementMonths(this.displayDate, -1) : dateUtil.incrementDays(this.displayDate, -7); case keyCode.PAGE_DOWN: return dateUtil.incrementMonths(this.displayDate, 1); case keyCode.PAGE_UP: return dateUtil.incrementMonths(this.displayDate, -1); case keyCode.HOME: return dateUtil.getFirstDateOfMonth(this.displayDate); case keyCode.END: return dateUtil.getLastDateOfMonth(this.displayDate); default: return null; } }; /** * Gets the "index" of the currently selected date as it would be in the virtual-repeat. * @returns {number} */ CalendarCtrl.prototype.getSelectedMonthIndex = function() { return this.dateUtil.getMonthDistance(this.firstRenderableDate, this.selectedDate || this.today); }; /** * Scrolls to the month of the given date. * @param {Date} date */ CalendarCtrl.prototype.scrollToMonth = function(date) { if (!this.dateUtil.isValidDate(date)) { return; } var monthDistance = this.dateUtil.getMonthDistance(this.firstRenderableDate, date); this.calendarScroller.scrollTop = monthDistance * TBODY_HEIGHT; }; /** * Sets the ng-model value for the calendar and emits a change event. * @param {Date} date */ CalendarCtrl.prototype.setNgModelValue = function(date) { this.$scope.$emit('md-calendar-change', date); this.ngModelCtrl.$setViewValue(date); this.ngModelCtrl.$render(); }; /** * Focus the cell corresponding to the given date. * @param {Date=} opt_date */ CalendarCtrl.prototype.focus = function(opt_date) { var date = opt_date || this.selectedDate || this.today; var previousFocus = this.calendarElement.querySelector('.md-focus'); if (previousFocus) { previousFocus.classList.remove(FOCUSED_DATE_CLASS); } var cellId = this.getDateId(date); var cell = document.getElementById(cellId); if (cell) { cell.classList.add(FOCUSED_DATE_CLASS); cell.focus(); } else { this.focusDate = date; } }; /** * If a date exceeds minDate or maxDate, returns date matching minDate or maxDate, respectively. * Otherwise, returns the date. * @param {Date} date * @return {Date} */ CalendarCtrl.prototype.boundDateByMinAndMax = function(date) { var boundDate = date; if (this.minDate && date < this.minDate) { boundDate = new Date(this.minDate.getTime()); } if (this.maxDate && date > this.maxDate) { boundDate = new Date(this.maxDate.getTime()); } return boundDate; }; /*** Updating the displayed / selected date ***/ /** * Change the selected date in the calendar (ngModel value has already been changed). * @param {Date} date */ CalendarCtrl.prototype.changeSelectedDate = function(date) { var self = this; var previousSelectedDate = this.selectedDate; this.selectedDate = date; this.changeDisplayDate(date).then(function() { // Remove the selected class from the previously selected date, if any. if (previousSelectedDate) { var prevDateCell = document.getElementById(self.getDateId(previousSelectedDate)); if (prevDateCell) { prevDateCell.classList.remove(SELECTED_DATE_CLASS); prevDateCell.setAttribute('aria-selected', 'false'); } } // Apply the select class to the new selected date if it is set. if (date) { var dateCell = document.getElementById(self.getDateId(date)); if (dateCell) { dateCell.classList.add(SELECTED_DATE_CLASS); dateCell.setAttribute('aria-selected', 'true'); } } }); }; /** * Change the date that is being shown in the calendar. If the given date is in a different * month, the displayed month will be transitioned. * @param {Date} date */ CalendarCtrl.prototype.changeDisplayDate = function(date) { // Initialization is deferred until this function is called because we want to reflect // the starting value of ngModel. if (!this.isInitialized) { this.buildInitialCalendarDisplay(); return this.$q.when(); } // If trying to show an invalid date or a transition is in progress, do nothing. if (!this.dateUtil.isValidDate(date) || this.isMonthTransitionInProgress) { return this.$q.when(); } this.isMonthTransitionInProgress = true; var animationPromise = this.animateDateChange(date); this.displayDate = date; var self = this; animationPromise.then(function() { self.isMonthTransitionInProgress = false; }); return animationPromise; }; /** * Animates the transition from the calendar's current month to the given month. * @param {Date} date * @returns {angular.$q.Promise} The animation promise. */ CalendarCtrl.prototype.animateDateChange = function(date) { this.scrollToMonth(date); return this.$q.when(); }; /*** Constructing the calendar table ***/ /** * Builds and appends a day-of-the-week header to the calendar. * This should only need to be called once during initialization. */ CalendarCtrl.prototype.buildWeekHeader = function() { var firstDayOfWeek = this.dateLocale.firstDayOfWeek; var shortDays = this.dateLocale.shortDays; var row = document.createElement('tr'); for (var i = 0; i < 7; i++) { var th = document.createElement('th'); th.textContent = shortDays[(i + firstDayOfWeek) % 7]; row.appendChild(th); } this.$element.find('thead').append(row); }; /** * Gets an identifier for a date unique to the calendar instance for internal * purposes. Not to be displayed. * @param {Date} date * @returns {string} */ CalendarCtrl.prototype.getDateId = function(date) { return [ 'md', this.id, date.getFullYear(), date.getMonth(), date.getDate() ].join('-'); }; })(); })(); (function(){ "use strict"; (function() { 'use strict'; angular.module('material.components.datepicker') .directive('mdCalendarMonth', mdCalendarMonthDirective); /** * Private directive consumed by md-calendar. Having this directive lets the calender use * md-virtual-repeat and also cleanly separates the month DOM construction functions from * the rest of the calendar controller logic. */ function mdCalendarMonthDirective() { return { require: ['^^mdCalendar', 'mdCalendarMonth'], scope: {offset: '=mdMonthOffset'}, controller: CalendarMonthCtrl, controllerAs: 'mdMonthCtrl', bindToController: true, link: function(scope, element, attrs, controllers) { var calendarCtrl = controllers[0]; var monthCtrl = controllers[1]; monthCtrl.calendarCtrl = calendarCtrl; monthCtrl.generateContent(); // The virtual-repeat re-uses the same DOM elements, so there are only a limited number // of repeated items that are linked, and then those elements have their bindings updataed. // Since the months are not generated by bindings, we simply regenerate the entire thing // when the binding (offset) changes. scope.$watch(function() { return monthCtrl.offset; }, function(offset, oldOffset) { if (offset != oldOffset) { monthCtrl.generateContent(); } }); } }; } /** Class applied to the cell for today. */ var TODAY_CLASS = 'md-calendar-date-today'; /** Class applied to the selected date cell/. */ var SELECTED_DATE_CLASS = 'md-calendar-selected-date'; /** Class applied to the focused date cell/. */ var FOCUSED_DATE_CLASS = 'md-focus'; /** * Controller for a single calendar month. * @ngInject @constructor */ function CalendarMonthCtrl($element, $$mdDateUtil, $mdDateLocale) { this.dateUtil = $$mdDateUtil; this.dateLocale = $mdDateLocale; this.$element = $element; this.calendarCtrl = null; /** * Number of months from the start of the month "items" that the currently rendered month * occurs. Set via angular data binding. * @type {number} */ this.offset; /** * Date cell to focus after appending the month to the document. * @type {HTMLElement} */ this.focusAfterAppend = null; } CalendarMonthCtrl.$inject = ["$element", "$$mdDateUtil", "$mdDateLocale"]; /** Generate and append the content for this month to the directive element. */ CalendarMonthCtrl.prototype.generateContent = function() { var calendarCtrl = this.calendarCtrl; var date = this.dateUtil.incrementMonths(calendarCtrl.firstRenderableDate, this.offset); this.$element.empty(); this.$element.append(this.buildCalendarForMonth(date)); if (this.focusAfterAppend) { this.focusAfterAppend.classList.add(FOCUSED_DATE_CLASS); this.focusAfterAppend.focus(); this.focusAfterAppend = null; } }; /** * Creates a single cell to contain a date in the calendar with all appropriate * attributes and classes added. If a date is given, the cell content will be set * based on the date. * @param {Date=} opt_date * @returns {HTMLElement} */ CalendarMonthCtrl.prototype.buildDateCell = function(opt_date) { var calendarCtrl = this.calendarCtrl; // TODO(jelbourn): cloneNode is likely a faster way of doing this. var cell = document.createElement('td'); cell.tabIndex = -1; cell.classList.add('md-calendar-date'); cell.setAttribute('role', 'gridcell'); if (opt_date) { cell.setAttribute('tabindex', '-1'); cell.setAttribute('aria-label', this.dateLocale.longDateFormatter(opt_date)); cell.id = calendarCtrl.getDateId(opt_date); // Use `data-timestamp` attribute because IE10 does not support the `dataset` property. cell.setAttribute('data-timestamp', opt_date.getTime()); // TODO(jelourn): Doing these comparisons for class addition during generation might be slow. // It may be better to finish the construction and then query the node and add the class. if (this.dateUtil.isSameDay(opt_date, calendarCtrl.today)) { cell.classList.add(TODAY_CLASS); } if (this.dateUtil.isValidDate(calendarCtrl.selectedDate) && this.dateUtil.isSameDay(opt_date, calendarCtrl.selectedDate)) { cell.classList.add(SELECTED_DATE_CLASS); cell.setAttribute('aria-selected', 'true'); } var cellText = this.dateLocale.dates[opt_date.getDate()]; if (this.isDateEnabled(opt_date)) { // Add a indicator for select, hover, and focus states. var selectionIndicator = document.createElement('span'); cell.appendChild(selectionIndicator); selectionIndicator.classList.add('md-calendar-date-selection-indicator'); selectionIndicator.textContent = cellText; cell.addEventListener('click', calendarCtrl.cellClickHandler); if (calendarCtrl.focusDate && this.dateUtil.isSameDay(opt_date, calendarCtrl.focusDate)) { this.focusAfterAppend = cell; } } else { cell.classList.add('md-calendar-date-disabled'); cell.textContent = cellText; } } return cell; }; /** * Check whether date is in range and enabled * @param {Date=} opt_date * @return {boolean} Whether the date is enabled. */ CalendarMonthCtrl.prototype.isDateEnabled = function(opt_date) { return this.dateUtil.isDateWithinRange(opt_date, this.calendarCtrl.minDate, this.calendarCtrl.maxDate) && (!angular.isFunction(this.calendarCtrl.dateFilter) || this.calendarCtrl.dateFilter(opt_date)); } /** * Builds a `tr` element for the calendar grid. * @param rowNumber The week number within the month. * @returns {HTMLElement} */ CalendarMonthCtrl.prototype.buildDateRow = function(rowNumber) { var row = document.createElement('tr'); row.setAttribute('role', 'row'); // Because of an NVDA bug (with Firefox), the row needs an aria-label in order // to prevent the entire row being read aloud when the user moves between rows. // See http://community.nvda-project.org/ticket/4643. row.setAttribute('aria-label', this.dateLocale.weekNumberFormatter(rowNumber)); return row; }; /** * Builds the content for the given date's month. * @param {Date=} opt_dateInMonth * @returns {DocumentFragment} A document fragment containing the elements. */ CalendarMonthCtrl.prototype.buildCalendarForMonth = function(opt_dateInMonth) { var date = this.dateUtil.isValidDate(opt_dateInMonth) ? opt_dateInMonth : new Date(); var firstDayOfMonth = this.dateUtil.getFirstDateOfMonth(date); var firstDayOfTheWeek = this.getLocaleDay_(firstDayOfMonth); var numberOfDaysInMonth = this.dateUtil.getNumberOfDaysInMonth(date); // Store rows for the month in a document fragment so that we can append them all at once. var monthBody = document.createDocumentFragment(); var rowNumber = 1; var row = this.buildDateRow(rowNumber); monthBody.appendChild(row); // If this is the final month in the list of items, only the first week should render, // so we should return immediately after the first row is complete and has been // attached to the body. var isFinalMonth = this.offset === this.calendarCtrl.items.length - 1; // Add a label for the month. If the month starts on a Sun/Mon/Tues, the month label // goes on a row above the first of the month. Otherwise, the month label takes up the first // two cells of the first row. var blankCellOffset = 0; var monthLabelCell = document.createElement('td'); monthLabelCell.classList.add('md-calendar-month-label'); // If the entire month is after the max date, render the label as a disabled state. if (this.calendarCtrl.maxDate && firstDayOfMonth > this.calendarCtrl.maxDate) { monthLabelCell.classList.add('md-calendar-month-label-disabled'); } monthLabelCell.textContent = this.dateLocale.monthHeaderFormatter(date); if (firstDayOfTheWeek <= 2) { monthLabelCell.setAttribute('colspan', '7'); var monthLabelRow = this.buildDateRow(); monthLabelRow.appendChild(monthLabelCell); monthBody.insertBefore(monthLabelRow, row); if (isFinalMonth) { return monthBody; } } else { blankCellOffset = 2; monthLabelCell.setAttribute('colspan', '2'); row.appendChild(monthLabelCell); } // Add a blank cell for each day of the week that occurs before the first of the month. // For example, if the first day of the month is a Tuesday, add blank cells for Sun and Mon. // The blankCellOffset is needed in cases where the first N cells are used by the month label. for (var i = blankCellOffset; i < firstDayOfTheWeek; i++) { row.appendChild(this.buildDateCell()); } // Add a cell for each day of the month, keeping track of the day of the week so that // we know when to start a new row. var dayOfWeek = firstDayOfTheWeek; var iterationDate = firstDayOfMonth; for (var d = 1; d <= numberOfDaysInMonth; d++) { // If we've reached the end of the week, start a new row. if (dayOfWeek === 7) { // We've finished the first row, so we're done if this is the final month. if (isFinalMonth) { return monthBody; } dayOfWeek = 0; rowNumber++; row = this.buildDateRow(rowNumber); monthBody.appendChild(row); } iterationDate.setDate(d); var cell = this.buildDateCell(iterationDate); row.appendChild(cell); dayOfWeek++; } // Ensure that the last row of the month has 7 cells. while (row.childNodes.length < 7) { row.appendChild(this.buildDateCell()); } // Ensure that all months have 6 rows. This is necessary for now because the virtual-repeat // requires that all items have exactly the same height. while (monthBody.childNodes.length < 6) { var whitespaceRow = this.buildDateRow(); for (var i = 0; i < 7; i++) { whitespaceRow.appendChild(this.buildDateCell()); } monthBody.appendChild(whitespaceRow); } return monthBody; }; /** * Gets the day-of-the-week index for a date for the current locale. * @private * @param {Date} date * @returns {number} The column index of the date in the calendar. */ CalendarMonthCtrl.prototype.getLocaleDay_ = function(date) { return (date.getDay() + (7 - this.dateLocale.firstDayOfWeek)) % 7 }; })(); })(); (function(){ "use strict"; (function() { 'use strict'; /** * @ngdoc service * @name $mdDateLocaleProvider * @module material.components.datepicker * * @description * The `$mdDateLocaleProvider` is the provider that creates the `$mdDateLocale` service. * This provider that allows the user to specify messages, formatters, and parsers for date * internationalization. The `$mdDateLocale` service itself is consumed by Angular Material * components that that deal with dates. * * @property {(Array)=} months Array of month names (in order). * @property {(Array)=} shortMonths Array of abbreviated month names. * @property {(Array)=} days Array of the days of the week (in order). * @property {(Array)=} shortDays Array of abbreviated dayes of the week. * @property {(Array)=} dates Array of dates of the month. Only necessary for locales * using a numeral system other than [1, 2, 3...]. * @property {(Array)=} firstDayOfWeek The first day of the week. Sunday = 0, Monday = 1, * etc. * @property {(function(string): Date)=} parseDate Function to parse a date object from a string. * @property {(function(Date): string)=} formatDate Function to format a date object to a string. * @property {(function(Date): string)=} monthHeaderFormatter Function that returns the label for * a month given a date. * @property {(function(number): string)=} weekNumberFormatter Function that returns a label for * a week given the week number. * @property {(string)=} msgCalendar Translation of the label "Calendar" for the current locale. * @property {(string)=} msgOpenCalendar Translation of the button label "Open calendar" for the * current locale. * * @usage * * myAppModule.config(function($mdDateLocaleProvider) { * * // Example of a French localization. * $mdDateLocaleProvider.months = ['janvier', 'février', 'mars', ...]; * $mdDateLocaleProvider.shortMonths = ['janv', 'févr', 'mars', ...]; * $mdDateLocaleProvider.days = ['dimanche', 'lundi', 'mardi', ...]; * $mdDateLocaleProvider.shortDays = ['Di', 'Lu', 'Ma', ...]; * * // Can change week display to start on Monday. * $mdDateLocaleProvider.firstDayOfWeek = 1; * * // Optional. * $mdDateLocaleProvider.dates = [1, 2, 3, 4, 5, 6, ...]; * * // Example uses moment.js to parse and format dates. * $mdDateLocaleProvider.parseDate = function(dateString) { * var m = moment(dateString, 'L', true); * return m.isValid() ? m.toDate() : new Date(NaN); * }; * * $mdDateLocaleProvider.formatDate = function(date) { * return moment(date).format('L'); * }; * * $mdDateLocaleProvider.monthHeaderFormatter = function(date) { * return myShortMonths[date.getMonth()] + ' ' + date.getFullYear(); * }; * * // In addition to date display, date components also need localized messages * // for aria-labels for screen-reader users. * * $mdDateLocaleProvider.weekNumberFormatter = function(weekNumber) { * return 'Semaine ' + weekNumber; * }; * * $mdDateLocaleProvider.msgCalendar = 'Calendrier'; * $mdDateLocaleProvider.msgOpenCalendar = 'Ouvrir le calendrier'; * * }); * * */ angular.module('material.components.datepicker').config(["$provide", function($provide) { // TODO(jelbourn): Assert provided values are correctly formatted. Need assertions. /** @constructor */ function DateLocaleProvider() { /** Array of full month names. E.g., ['January', 'Febuary', ...] */ this.months = null; /** Array of abbreviated month names. E.g., ['Jan', 'Feb', ...] */ this.shortMonths = null; /** Array of full day of the week names. E.g., ['Monday', 'Tuesday', ...] */ this.days = null; /** Array of abbreviated dat of the week names. E.g., ['M', 'T', ...] */ this.shortDays = null; /** Array of dates of a month (1 - 31). Characters might be different in some locales. */ this.dates = null; /** Index of the first day of the week. 0 = Sunday, 1 = Monday, etc. */ this.firstDayOfWeek = 0; /** * Function that converts the date portion of a Date to a string. * @type {(function(Date): string)} */ this.formatDate = null; /** * Function that converts a date string to a Date object (the date portion) * @type {function(string): Date} */ this.parseDate = null; /** * Function that formats a Date into a month header string. * @type {function(Date): string} */ this.monthHeaderFormatter = null; /** * Function that formats a week number into a label for the week. * @type {function(number): string} */ this.weekNumberFormatter = null; /** * Function that formats a date into a long aria-label that is read * when the focused date changes. * @type {function(Date): string} */ this.longDateFormatter = null; /** * ARIA label for the calendar "dialog" used in the datepicker. * @type {string} */ this.msgCalendar = ''; /** * ARIA label for the datepicker's "Open calendar" buttons. * @type {string} */ this.msgOpenCalendar = ''; } /** * Factory function that returns an instance of the dateLocale service. * @ngInject * @param $locale * @returns {DateLocale} */ DateLocaleProvider.prototype.$get = function($locale) { /** * Default date-to-string formatting function. * @param {!Date} date * @returns {string} */ function defaultFormatDate(date) { if (!date) { return ''; } // All of the dates created through ng-material *should* be set to midnight. // If we encounter a date where the localeTime shows at 11pm instead of midnight, // we have run into an issue with DST where we need to increment the hour by one: // var d = new Date(1992, 9, 8, 0, 0, 0); // d.toLocaleString(); // == "10/7/1992, 11:00:00 PM" var localeTime = date.toLocaleTimeString(); var formatDate = date; if (date.getHours() == 0 && (localeTime.indexOf('11:') !== -1 || localeTime.indexOf('23:') !== -1)) { formatDate = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 1, 0, 0); } return formatDate.toLocaleDateString(); } /** * Default string-to-date parsing function. * @param {string} dateString * @returns {!Date} */ function defaultParseDate(dateString) { return new Date(dateString); } /** * Default function to determine whether a string makes sense to be * parsed to a Date object. * * This is very permissive and is just a basic sanity check to ensure that * things like single integers aren't able to be parsed into dates. * @param {string} dateString * @returns {boolean} */ function defaultIsDateComplete(dateString) { dateString = dateString.trim(); // Looks for three chunks of content (either numbers or text) separated // by delimiters. var re = /^(([a-zA-Z]{3,}|[0-9]{1,4})([ \.,]+|[\/\-])){2}([a-zA-Z]{3,}|[0-9]{1,4})$/; return re.test(dateString); } /** * Default date-to-string formatter to get a month header. * @param {!Date} date * @returns {string} */ function defaultMonthHeaderFormatter(date) { return service.shortMonths[date.getMonth()] + ' ' + date.getFullYear(); } /** * Default week number formatter. * @param number * @returns {string} */ function defaultWeekNumberFormatter(number) { return 'Week ' + number; } /** * Default formatter for date cell aria-labels. * @param {!Date} date * @returns {string} */ function defaultLongDateFormatter(date) { // Example: 'Thursday June 18 2015' return [ service.days[date.getDay()], service.months[date.getMonth()], service.dates[date.getDate()], date.getFullYear() ].join(' '); } // The default "short" day strings are the first character of each day, // e.g., "Monday" => "M". var defaultShortDays = $locale.DATETIME_FORMATS.DAY.map(function(day) { return day[0]; }); // The default dates are simply the numbers 1 through 31. var defaultDates = Array(32); for (var i = 1; i <= 31; i++) { defaultDates[i] = i; } // Default ARIA messages are in English (US). var defaultMsgCalendar = 'Calendar'; var defaultMsgOpenCalendar = 'Open calendar'; var service = { months: this.months || $locale.DATETIME_FORMATS.MONTH, shortMonths: this.shortMonths || $locale.DATETIME_FORMATS.SHORTMONTH, days: this.days || $locale.DATETIME_FORMATS.DAY, shortDays: this.shortDays || defaultShortDays, dates: this.dates || defaultDates, firstDayOfWeek: this.firstDayOfWeek || 0, formatDate: this.formatDate || defaultFormatDate, parseDate: this.parseDate || defaultParseDate, isDateComplete: this.isDateComplete || defaultIsDateComplete, monthHeaderFormatter: this.monthHeaderFormatter || defaultMonthHeaderFormatter, weekNumberFormatter: this.weekNumberFormatter || defaultWeekNumberFormatter, longDateFormatter: this.longDateFormatter || defaultLongDateFormatter, msgCalendar: this.msgCalendar || defaultMsgCalendar, msgOpenCalendar: this.msgOpenCalendar || defaultMsgOpenCalendar }; return service; }; DateLocaleProvider.prototype.$get.$inject = ["$locale"]; $provide.provider('$mdDateLocale', new DateLocaleProvider()); }]); })(); })(); (function(){ "use strict"; (function() { 'use strict'; // POST RELEASE // TODO(jelbourn): Demo that uses moment.js // TODO(jelbourn): make sure this plays well with validation and ngMessages. // TODO(jelbourn): calendar pane doesn't open up outside of visible viewport. // TODO(jelbourn): forward more attributes to the internal input (required, autofocus, etc.) // TODO(jelbourn): something better for mobile (calendar panel takes up entire screen?) // TODO(jelbourn): input behavior (masking? auto-complete?) // TODO(jelbourn): UTC mode // TODO(jelbourn): RTL angular.module('material.components.datepicker') .directive('mdDatepicker', datePickerDirective); /** * @ngdoc directive * @name mdDatepicker * @module material.components.datepicker * * @param {Date} ng-model The component's model. Expects a JavaScript Date object. * @param {expression=} ng-change Expression evaluated when the model value changes. * @param {Date=} md-min-date Expression representing a min date (inclusive). * @param {Date=} md-max-date Expression representing a max date (inclusive). * @param {(function(Date): boolean)=} md-date-filter Function expecting a date and returning a boolean whether it can be selected or not. * @param {String=} md-placeholder The date input placeholder value. * @param {boolean=} ng-disabled Whether the datepicker is disabled. * @param {boolean=} ng-required Whether a value is required for the datepicker. * * @description * `` is a component used to select a single date. * For information on how to configure internationalization for the date picker, * see `$mdDateLocaleProvider`. * * This component supports [ngMessages](https://docs.angularjs.org/api/ngMessages/directive/ngMessages). * Supported attributes are: * * `required`: whether a required date is not set. * * `mindate`: whether the selected date is before the minimum allowed date. * * `maxdate`: whether the selected date is after the maximum allowed date. * * @usage * * * * */ function datePickerDirective() { return { template: // Buttons are not in the tab order because users can open the calendar via keyboard // interaction on the text input, and multiple tab stops for one component (picker) // may be confusing. '' + '
' + '' + '' + '
' + '
' + '
' + // This pane will be detached from here and re-attached to the document body. '
' + '
' + '
' + '
' + '
' + '' + '' + '
' + '
', require: ['ngModel', 'mdDatepicker', '?^mdInputContainer'], scope: { minDate: '=mdMinDate', maxDate: '=mdMaxDate', placeholder: '@mdPlaceholder', dateFilter: '=mdDateFilter' }, controller: DatePickerCtrl, controllerAs: 'ctrl', bindToController: true, link: function(scope, element, attr, controllers) { var ngModelCtrl = controllers[0]; var mdDatePickerCtrl = controllers[1]; var mdInputContainer = controllers[2]; if (mdInputContainer) { throw Error('md-datepicker should not be placed inside md-input-container.'); } mdDatePickerCtrl.configureNgModel(ngModelCtrl); } }; } /** Additional offset for the input's `size` attribute, which is updated based on its content. */ var EXTRA_INPUT_SIZE = 3; /** Class applied to the container if the date is invalid. */ var INVALID_CLASS = 'md-datepicker-invalid'; /** Default time in ms to debounce input event by. */ var DEFAULT_DEBOUNCE_INTERVAL = 500; /** * Height of the calendar pane used to check if the pane is going outside the boundary of * the viewport. See calendar.scss for how $md-calendar-height is computed; an extra 20px is * also added to space the pane away from the exact edge of the screen. * * This is computed statically now, but can be changed to be measured if the circumstances * of calendar sizing are changed. */ var CALENDAR_PANE_HEIGHT = 368; /** * Width of the calendar pane used to check if the pane is going outside the boundary of * the viewport. See calendar.scss for how $md-calendar-width is computed; an extra 20px is * also added to space the pane away from the exact edge of the screen. * * This is computed statically now, but can be changed to be measured if the circumstances * of calendar sizing are changed. */ var CALENDAR_PANE_WIDTH = 360; /** * Controller for md-datepicker. * * @ngInject @constructor */ function DatePickerCtrl($scope, $element, $attrs, $compile, $timeout, $window, $mdConstant, $mdTheming, $mdUtil, $mdDateLocale, $$mdDateUtil, $$rAF) { /** @final */ this.$compile = $compile; /** @final */ this.$timeout = $timeout; /** @final */ this.$window = $window; /** @final */ this.dateLocale = $mdDateLocale; /** @final */ this.dateUtil = $$mdDateUtil; /** @final */ this.$mdConstant = $mdConstant; /* @final */ this.$mdUtil = $mdUtil; /** @final */ this.$$rAF = $$rAF; /** * The root document element. This is used for attaching a top-level click handler to * close the calendar panel when a click outside said panel occurs. We use `documentElement` * instead of body because, when scrolling is disabled, some browsers consider the body element * to be completely off the screen and propagate events directly to the html element. * @type {!angular.JQLite} */ this.documentElement = angular.element(document.documentElement); /** @type {!angular.NgModelController} */ this.ngModelCtrl = null; /** @type {HTMLInputElement} */ this.inputElement = $element[0].querySelector('input'); /** @final {!angular.JQLite} */ this.ngInputElement = angular.element(this.inputElement); /** @type {HTMLElement} */ this.inputContainer = $element[0].querySelector('.md-datepicker-input-container'); /** @type {HTMLElement} Floating calendar pane. */ this.calendarPane = $element[0].querySelector('.md-datepicker-calendar-pane'); /** @type {HTMLElement} Calendar icon button. */ this.calendarButton = $element[0].querySelector('.md-datepicker-button'); /** * Element covering everything but the input in the top of the floating calendar pane. * @type {HTMLElement} */ this.inputMask = $element[0].querySelector('.md-datepicker-input-mask-opaque'); /** @final {!angular.JQLite} */ this.$element = $element; /** @final {!angular.Attributes} */ this.$attrs = $attrs; /** @final {!angular.Scope} */ this.$scope = $scope; /** @type {Date} */ this.date = null; /** @type {boolean} */ this.isFocused = false; /** @type {boolean} */ this.isDisabled; this.setDisabled($element[0].disabled || angular.isString($attrs['disabled'])); /** @type {boolean} Whether the date-picker's calendar pane is open. */ this.isCalendarOpen = false; /** * Element from which the calendar pane was opened. Keep track of this so that we can return * focus to it when the pane is closed. * @type {HTMLElement} */ this.calendarPaneOpenedFrom = null; this.calendarPane.id = 'md-date-pane' + $mdUtil.nextUid(); $mdTheming($element); /** Pre-bound click handler is saved so that the event listener can be removed. */ this.bodyClickHandler = angular.bind(this, this.handleBodyClick); /** Pre-bound resize handler so that the event listener can be removed. */ this.windowResizeHandler = $mdUtil.debounce(angular.bind(this, this.closeCalendarPane), 100); // Unless the user specifies so, the datepicker should not be a tab stop. // This is necessary because ngAria might add a tabindex to anything with an ng-model // (based on whether or not the user has turned that particular feature on/off). if (!$attrs['tabindex']) { $element.attr('tabindex', '-1'); } this.installPropertyInterceptors(); this.attachChangeListeners(); this.attachInteractionListeners(); var self = this; $scope.$on('$destroy', function() { self.detachCalendarPane(); }); } DatePickerCtrl.$inject = ["$scope", "$element", "$attrs", "$compile", "$timeout", "$window", "$mdConstant", "$mdTheming", "$mdUtil", "$mdDateLocale", "$$mdDateUtil", "$$rAF"]; /** * Sets up the controller's reference to ngModelController. * @param {!angular.NgModelController} ngModelCtrl */ DatePickerCtrl.prototype.configureNgModel = function(ngModelCtrl) { this.ngModelCtrl = ngModelCtrl; var self = this; ngModelCtrl.$render = function() { var value = self.ngModelCtrl.$viewValue; if (value && !(value instanceof Date)) { throw Error('The ng-model for md-datepicker must be a Date instance. ' + 'Currently the model is a: ' + (typeof value)); } self.date = value; self.inputElement.value = self.dateLocale.formatDate(value); self.resizeInputElement(); self.updateErrorState(); }; }; /** * Attach event listeners for both the text input and the md-calendar. * Events are used instead of ng-model so that updates don't infinitely update the other * on a change. This should also be more performant than using a $watch. */ DatePickerCtrl.prototype.attachChangeListeners = function() { var self = this; self.$scope.$on('md-calendar-change', function(event, date) { self.ngModelCtrl.$setViewValue(date); self.date = date; self.inputElement.value = self.dateLocale.formatDate(date); self.closeCalendarPane(); self.resizeInputElement(); self.updateErrorState(); }); self.ngInputElement.on('input', angular.bind(self, self.resizeInputElement)); // TODO(chenmike): Add ability for users to specify this interval. self.ngInputElement.on('input', self.$mdUtil.debounce(self.handleInputEvent, DEFAULT_DEBOUNCE_INTERVAL, self)); }; /** Attach event listeners for user interaction. */ DatePickerCtrl.prototype.attachInteractionListeners = function() { var self = this; var $scope = this.$scope; var keyCodes = this.$mdConstant.KEY_CODE; // Add event listener through angular so that we can triggerHandler in unit tests. self.ngInputElement.on('keydown', function(event) { if (event.altKey && event.keyCode == keyCodes.DOWN_ARROW) { self.openCalendarPane(event); $scope.$digest(); } }); $scope.$on('md-calendar-close', function() { self.closeCalendarPane(); }); }; /** * Capture properties set to the date-picker and imperitively handle internal changes. * This is done to avoid setting up additional $watches. */ DatePickerCtrl.prototype.installPropertyInterceptors = function() { var self = this; if (this.$attrs['ngDisabled']) { // The expression is to be evaluated against the directive element's scope and not // the directive's isolate scope. var scope = this.$scope.$parent; if (scope) { scope.$watch(this.$attrs['ngDisabled'], function(isDisabled) { self.setDisabled(isDisabled); }); } } Object.defineProperty(this, 'placeholder', { get: function() { return self.inputElement.placeholder; }, set: function(value) { self.inputElement.placeholder = value || ''; } }); }; /** * Sets whether the date-picker is disabled. * @param {boolean} isDisabled */ DatePickerCtrl.prototype.setDisabled = function(isDisabled) { this.isDisabled = isDisabled; this.inputElement.disabled = isDisabled; this.calendarButton.disabled = isDisabled; }; /** * Sets the custom ngModel.$error flags to be consumed by ngMessages. Flags are: * - mindate: whether the selected date is before the minimum date. * - maxdate: whether the selected flag is after the maximum date. * - filtered: whether the selected date is allowed by the custom filtering function. * - valid: whether the entered text input is a valid date * * The 'required' flag is handled automatically by ngModel. * * @param {Date=} opt_date Date to check. If not given, defaults to the datepicker's model value. */ DatePickerCtrl.prototype.updateErrorState = function(opt_date) { var date = opt_date || this.date; // Clear any existing errors to get rid of anything that's no longer relevant. this.clearErrorState(); if (this.dateUtil.isValidDate(date)) { // Force all dates to midnight in order to ignore the time portion. date = this.dateUtil.createDateAtMidnight(date); if (this.dateUtil.isValidDate(this.minDate)) { var minDate = this.dateUtil.createDateAtMidnight(this.minDate); this.ngModelCtrl.$setValidity('mindate', date >= minDate); } if (this.dateUtil.isValidDate(this.maxDate)) { var maxDate = this.dateUtil.createDateAtMidnight(this.maxDate); this.ngModelCtrl.$setValidity('maxdate', date <= maxDate); } if (angular.isFunction(this.dateFilter)) { this.ngModelCtrl.$setValidity('filtered', this.dateFilter(date)); } } else { // The date is seen as "not a valid date" if there is *something* set // (i.e.., not null or undefined), but that something isn't a valid date. this.ngModelCtrl.$setValidity('valid', date == null); } // TODO(jelbourn): Change this to classList.toggle when we stop using PhantomJS in unit tests // because it doesn't conform to the DOMTokenList spec. // See https://github.com/ariya/phantomjs/issues/12782. if (!this.ngModelCtrl.$valid) { this.inputContainer.classList.add(INVALID_CLASS); } }; /** Clears any error flags set by `updateErrorState`. */ DatePickerCtrl.prototype.clearErrorState = function() { this.inputContainer.classList.remove(INVALID_CLASS); ['mindate', 'maxdate', 'filtered', 'valid'].forEach(function(field) { this.ngModelCtrl.$setValidity(field, true); }, this); }; /** Resizes the input element based on the size of its content. */ DatePickerCtrl.prototype.resizeInputElement = function() { this.inputElement.size = this.inputElement.value.length + EXTRA_INPUT_SIZE; }; /** * Sets the model value if the user input is a valid date. * Adds an invalid class to the input element if not. */ DatePickerCtrl.prototype.handleInputEvent = function() { var inputString = this.inputElement.value; var parsedDate = inputString ? this.dateLocale.parseDate(inputString) : null; this.dateUtil.setDateTimeToMidnight(parsedDate); // An input string is valid if it is either empty (representing no date) // or if it parses to a valid date that the user is allowed to select. var isValidInput = inputString == '' || ( this.dateUtil.isValidDate(parsedDate) && this.dateLocale.isDateComplete(inputString) && this.isDateEnabled(parsedDate) ); // The datepicker's model is only updated when there is a valid input. if (isValidInput) { this.ngModelCtrl.$setViewValue(parsedDate); this.date = parsedDate; } this.updateErrorState(parsedDate); }; /** * Check whether date is in range and enabled * @param {Date=} opt_date * @return {boolean} Whether the date is enabled. */ DatePickerCtrl.prototype.isDateEnabled = function(opt_date) { return this.dateUtil.isDateWithinRange(opt_date, this.minDate, this.maxDate) && (!angular.isFunction(this.dateFilter) || this.dateFilter(opt_date)); }; /** Position and attach the floating calendar to the document. */ DatePickerCtrl.prototype.attachCalendarPane = function() { var calendarPane = this.calendarPane; calendarPane.style.transform = ''; this.$element.addClass('md-datepicker-open'); var elementRect = this.inputContainer.getBoundingClientRect(); var bodyRect = document.body.getBoundingClientRect(); // Check to see if the calendar pane would go off the screen. If so, adjust position // accordingly to keep it within the viewport. var paneTop = elementRect.top - bodyRect.top; var paneLeft = elementRect.left - bodyRect.left; // If ng-material has disabled body scrolling (for example, if a dialog is open), // then it's possible that the already-scrolled body has a negative top/left. In this case, // we want to treat the "real" top as (0 - bodyRect.top). In a normal scrolling situation, // though, the top of the viewport should just be the body's scroll position. var viewportTop = (bodyRect.top < 0 && document.body.scrollTop == 0) ? -bodyRect.top : document.body.scrollTop; var viewportLeft = (bodyRect.left < 0 && document.body.scrollLeft == 0) ? -bodyRect.left : document.body.scrollLeft; var viewportBottom = viewportTop + this.$window.innerHeight; var viewportRight = viewportLeft + this.$window.innerWidth; // If the right edge of the pane would be off the screen and shifting it left by the // difference would not go past the left edge of the screen. If the calendar pane is too // big to fit on the screen at all, move it to the left of the screen and scale the entire // element down to fit. if (paneLeft + CALENDAR_PANE_WIDTH > viewportRight) { if (viewportRight - CALENDAR_PANE_WIDTH > 0) { paneLeft = viewportRight - CALENDAR_PANE_WIDTH; } else { paneLeft = viewportLeft; var scale = this.$window.innerWidth / CALENDAR_PANE_WIDTH; calendarPane.style.transform = 'scale(' + scale + ')'; } calendarPane.classList.add('md-datepicker-pos-adjusted'); } // If the bottom edge of the pane would be off the screen and shifting it up by the // difference would not go past the top edge of the screen. if (paneTop + CALENDAR_PANE_HEIGHT > viewportBottom && viewportBottom - CALENDAR_PANE_HEIGHT > viewportTop) { paneTop = viewportBottom - CALENDAR_PANE_HEIGHT; calendarPane.classList.add('md-datepicker-pos-adjusted'); } calendarPane.style.left = paneLeft + 'px'; calendarPane.style.top = paneTop + 'px'; document.body.appendChild(calendarPane); // The top of the calendar pane is a transparent box that shows the text input underneath. // Since the pane is floating, though, the page underneath the pane *adjacent* to the input is // also shown unless we cover it up. The inputMask does this by filling up the remaining space // based on the width of the input. this.inputMask.style.left = elementRect.width + 'px'; // Add CSS class after one frame to trigger open animation. this.$$rAF(function() { calendarPane.classList.add('md-pane-open'); }); }; /** Detach the floating calendar pane from the document. */ DatePickerCtrl.prototype.detachCalendarPane = function() { this.$element.removeClass('md-datepicker-open'); this.calendarPane.classList.remove('md-pane-open'); this.calendarPane.classList.remove('md-datepicker-pos-adjusted'); if (this.calendarPane.parentNode) { // Use native DOM removal because we do not want any of the angular state of this element // to be disposed. this.calendarPane.parentNode.removeChild(this.calendarPane); } }; /** * Open the floating calendar pane. * @param {Event} event */ DatePickerCtrl.prototype.openCalendarPane = function(event) { if (!this.isCalendarOpen && !this.isDisabled) { this.isCalendarOpen = true; this.calendarPaneOpenedFrom = event.target; // Because the calendar pane is attached directly to the body, it is possible that the // rest of the component (input, etc) is in a different scrolling container, such as // an md-content. This means that, if the container is scrolled, the pane would remain // stationary. To remedy this, we disable scrolling while the calendar pane is open, which // also matches the native behavior for things like ` * * * * * * * * */ function mdInputContainerDirective($mdTheming, $parse) { ContainerCtrl.$inject = ["$scope", "$element", "$attrs", "$animate"]; return { restrict: 'E', link: postLink, controller: ContainerCtrl }; function postLink(scope, element) { $mdTheming(element); var iconElements = element.find('md-icon'); var icons = iconElements.length ? iconElements : element[0].getElementsByClassName('md-icon'); // Incase there's one icon we want to identify where the icon is (right or left) and apply the related class if (icons.length == 1) { var next = icons[0].nextElementSibling; var previous = icons[0].previousElementSibling; element.addClass(next && next.tagName === 'INPUT' ? 'md-icon-left' : previous && previous.tagName === 'INPUT' ? 'md-icon-right' : ''); } // In case there are two icons we apply both icon classes else if (icons.length == 2) { element.addClass('md-icon-left md-icon-right'); } } function ContainerCtrl($scope, $element, $attrs, $animate) { 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.setHasPlaceholder = function(hasPlaceholder) { $element.toggleClass('md-input-has-placeholder', !!hasPlaceholder); }; self.setInvalid = function(isInvalid) { if (isInvalid) { $animate.addClass($element, 'md-input-invalid'); } else { $animate.removeClass($element, 'md-input-invalid'); } }; $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 || element.hasClass('md-container-ignore')) return; containerCtrl.label = element; scope.$on('$destroy', function() { containerCtrl.label = null; }); } }; } /** * @ngdoc directive * @name mdInput * @restrict E * @module material.components.input * * @description * You can use any `` or ` *
*
This is required!
*
That's too long!
*
* * * * * * * * * * *

Notes

* * - Requires [ngMessages](https://docs.angularjs.org/api/ngMessages). * - Behaves like the [AngularJS input directive](https://docs.angularjs.org/api/ng/directive/input). * * The `md-input` and `md-input-container` directives use very specific positioning to achieve the * error animation effects. Therefore, it is *not* advised to use the Layout system inside of the * `` tags. Instead, use relative or absolute positioning. * */ function inputTextareaDirective($mdUtil, $window, $mdAria) { return { restrict: 'E', require: ['^?mdInputContainer', '?ngModel'], link: postLink }; function postLink(scope, element, attr, ctrls) { var containerCtrl = ctrls[0]; var hasNgModel = !!ctrls[1]; 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* ,