diff --git a/README.md b/README.md index 82c555a..08f1828 100644 --- a/README.md +++ b/README.md @@ -23,4 +23,4 @@ If you desire to require minified AngularMaterial files, add the following: ## Versioning -Current version of AngularMaterial - 1.1.0-rc1 +Current version of AngularMaterial - 1.1.3 diff --git a/lib/rails-angular-material/version.rb b/lib/rails-angular-material/version.rb index ca92d30..aff895f 100644 --- a/lib/rails-angular-material/version.rb +++ b/lib/rails-angular-material/version.rb @@ -1,3 +1,3 @@ module AngularMaterialRails - VERSION = "1.1.0-rc1" + VERSION = "1.1.3" end diff --git a/vendor/assets/javascripts/angular-material.js b/vendor/assets/javascripts/angular-material.js index 641025a..5936252 100644 --- a/vendor/assets/javascripts/angular-material.js +++ b/vendor/assets/javascripts/angular-material.js @@ -2,7 +2,7 @@ * Angular Material Design * https://github.com/angular/material * @license MIT - * v1.1.0-rc1 + * v1.1.3 */ (function( window, angular, undefined ){ "use strict"; @@ -10,7 +10,7 @@ (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.fabActions","material.components.fabShared","material.components.fabSpeedDial","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"]); +angular.module('ngMaterial', ["ng","ngAnimate","ngAria","material.core","material.core.gestures","material.core.interaction","material.core.layout","material.core.meta","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.colors","material.components.content","material.components.datepicker","material.components.dialog","material.components.divider","material.components.fabActions","material.components.fabShared","material.components.fabSpeedDial","material.components.fabToolbar","material.components.gridList","material.components.icon","material.components.input","material.components.list","material.components.menu","material.components.menuBar","material.components.panel","material.components.navBar","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.toast","material.components.tabs","material.components.toolbar","material.components.tooltip","material.components.truncate","material.components.virtualRepeat","material.components.whiteframe"]); })(); (function(){ "use strict"; @@ -19,11 +19,16 @@ angular.module('ngMaterial', ["ng","ngAnimate","ngAria","material.core","materia * Initialization function that validates environment * requirements. */ +DetectNgTouch.$inject = ["$log", "$injector"]; +MdCoreConfigure.$inject = ["$provide", "$mdThemingProvider"]; +rAFDecorator.$inject = ["$delegate"]; +qDecorator.$inject = ["$delegate"]; angular .module('material.core', [ 'ngAnimate', 'material.core.animate', 'material.core.layout', + 'material.core.interaction', 'material.core.gestures', 'material.core.theming' ]) @@ -34,6 +39,7 @@ angular /** * Detect if the ng-Touch module is also being used. * Warn if detected. + * @ngInject */ function DetectNgTouch($log, $injector) { if ( $injector.has('$swipe') ) { @@ -44,12 +50,14 @@ function DetectNgTouch($log, $injector) { $log.warn(msg); } } -DetectNgTouch.$inject = ["$log", "$injector"]; - +/** + * @ngInject + */ function MdCoreConfigure($provide, $mdThemingProvider) { - $provide.decorator('$$rAF', ["$delegate", rAFDecorator]); + $provide.decorator('$$rAF', ['$delegate', rAFDecorator]); + $provide.decorator('$q', ['$delegate', qDecorator]); $mdThemingProvider.theme('default') .primaryPalette('indigo') @@ -57,8 +65,10 @@ function MdCoreConfigure($provide, $mdThemingProvider) { .warnPalette('deep-orange') .backgroundPalette('grey'); } -MdCoreConfigure.$inject = ["$provide", "$mdThemingProvider"]; +/** + * @ngInject + */ function rAFDecorator($delegate) { /** * Use this to throttle events that come in often. @@ -90,11 +100,30 @@ function rAFDecorator($delegate) { return $delegate; } +/** + * @ngInject + */ +function qDecorator($delegate) { + /** + * Adds a shim for $q.resolve for Angular version that don't have it, + * so we don't have to think about it. + * + * via https://github.com/angular/angular.js/pull/11987 + */ + + // TODO(crisbeto): this won't be necessary once we drop Angular 1.3 + if (!$delegate.resolve) { + $delegate.resolve = $delegate.when; + } + return $delegate; +} + })(); (function(){ "use strict"; -angular.module('material.core') + +MdAutofocusDirective.$inject = ["$parse"];angular.module('material.core') .directive('mdAutofocus', MdAutofocusDirective) // Support the deprecated md-auto-focus and md-sidenav-focus as well @@ -109,7 +138,7 @@ angular.module('material.core') * @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. + * `$mdBottomSheet`, `$mdMenu` 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 @@ -186,21 +215,120 @@ angular.module('material.core') * * **/ -function MdAutofocusDirective() { +function MdAutofocusDirective($parse) { return { restrict: 'A', + link: { + pre: preLink + } + }; - link: postLink + function preLink(scope, element, attr) { + var attrExp = attr.mdAutoFocus || attr.mdAutofocus || attr.mdSidenavFocus; + + // Initially update the expression by manually parsing the expression as per $watch source. + updateExpression($parse(attrExp)(scope)); + + // Only watch the expression if it is not empty. + if (attrExp) { + scope.$watch(attrExp, updateExpression); + } + + /** + * Updates the autofocus class which is used to determine whether the attribute + * expression evaluates to true or false. + * @param {string|boolean} value Attribute Value + */ + function updateExpression(value) { + + // Rather than passing undefined to the jqLite toggle class function we explicitly set the + // value to true. Otherwise the class will be just toggled instead of being forced. + if (angular.isUndefined(value)) { + value = true; + } + + element.toggleClass('md-autofocus', !!value); + } } + } -function postLink(scope, element, attrs) { - var attr = attrs.mdAutoFocus || attrs.mdAutofocus || attrs.mdSidenavFocus; +})(); +(function(){ +"use strict"; - // 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); - }); +/** + * @ngdoc module + * @name material.core.colorUtil + * @description + * Color Util + */ +angular + .module('material.core') + .factory('$mdColorUtil', ColorUtilFactory); + +function ColorUtilFactory() { + /** + * 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 rgba value to hex string + * @param color {string} + * @returns {string} + */ + function rgbaToHex(color) { + color = color.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i); + + var hex = (color && color.length === 4) ? "#" + + ("0" + parseInt(color[1],10).toString(16)).slice(-2) + + ("0" + parseInt(color[2],10).toString(16)).slice(-2) + + ("0" + parseInt(color[3],10).toString(16)).slice(-2) : ''; + + return hex.toUpperCase(); + } + + /** + * Converts an RGB color to RGBA + * @param color {string} + * @returns {string} + */ + function rgbToRgba (color) { + return color.replace(')', ', 0.1)').replace('(', 'a('); + } + + /** + * Converts an RGBA color to RGB + * @param color {string} + * @returns {string} + */ + function rgbaToRgb (color) { + return color + ? color.replace('rgba', 'rgb').replace(/,[^\),]+\)/, ')') + : 'rgb(0,0,0)'; + } + + return { + rgbaToHex: rgbaToHex, + hexToRgba: hexToRgba, + rgbToRgba: rgbToRgba, + rgbaToRgb: rgbaToRgb + }; } })(); @@ -214,14 +342,69 @@ angular.module('material.core') * Factory function that creates the grab-bag $mdConstant service. * @ngInject */ -function MdConstantFactory($sniffer) { +function MdConstantFactory() { + + var prefixTestEl = document.createElement('div'); + var vendorPrefix = getVendorPrefix(prefixTestEl); + var isWebkit = /webkit/i.test(vendorPrefix); + var SPECIAL_CHARS_REGEXP = /([:\-_]+(.))/g; - var webkit = /webkit/i.test($sniffer.vendorPrefix); function vendorProperty(name) { - return webkit ? ('webkit' + name.charAt(0).toUpperCase() + name.substring(1)) : name; + // Add a dash between the prefix and name, to be able to transform the string into camelcase. + var prefixedName = vendorPrefix + '-' + name; + var ucPrefix = camelCase(prefixedName); + var lcPrefix = ucPrefix.charAt(0).toLowerCase() + ucPrefix.substring(1); + + return hasStyleProperty(prefixTestEl, name) ? name : // The current browser supports the un-prefixed property + hasStyleProperty(prefixTestEl, ucPrefix) ? ucPrefix : // The current browser only supports the prefixed property. + hasStyleProperty(prefixTestEl, lcPrefix) ? lcPrefix : name; // Some browsers are only supporting the prefix in lowercase. } - return { + function hasStyleProperty(testElement, property) { + return angular.isDefined(testElement.style[property]); + } + + function camelCase(input) { + return input.replace(SPECIAL_CHARS_REGEXP, function(matches, separator, letter, offset) { + return offset ? letter.toUpperCase() : letter; + }); + } + + function getVendorPrefix(testElement) { + var prop, match; + var vendorRegex = /^(Moz|webkit|ms)(?=[A-Z])/; + + for (prop in testElement.style) { + if (match = vendorRegex.exec(prop)) { + return match[0]; + } + } + } + + var self = { + isInputKey : function(e) { return (e.keyCode >= 31 && e.keyCode <= 90); }, + isNumPadKey : function (e){ return (3 === e.location && e.keyCode >= 97 && e.keyCode <= 105); }, + isNavigationKey : function(e) { + var kc = self.KEY_CODE, NAVIGATION_KEYS = [kc.SPACE, kc.ENTER, kc.UP_ARROW, kc.DOWN_ARROW]; + return (NAVIGATION_KEYS.indexOf(e.keyCode) != -1); + }, + + /** + * Maximum size, in pixels, that can be explicitly set to an element. The actual value varies + * between browsers, but IE11 has the very lowest size at a mere 1,533,917px. Ideally we could + * compute this value, but Firefox always reports an element to have a size of zero if it + * goes over the max, meaning that we'd have to binary search for the value. + */ + ELEMENT_MAX_PIXELS: 1533917, + + /** + * Priority for a directive that should run before the directives from ngAria. + */ + BEFORE_NG_ARIA: 210, + + /** + * Common Keyboard actions and their associated keycode. + */ KEY_CODE: { COMMA: 188, SEMICOLON : 186, @@ -240,10 +423,15 @@ function MdConstantFactory($sniffer) { BACKSPACE: 8, DELETE: 46 }, + + /** + * Vendor prefixed CSS properties to be used to support the given functionality in older browsers + * as well. + */ CSS: { /* Constants */ - TRANSITIONEND: 'transitionend' + (webkit ? ' webkitTransitionEnd' : ''), - ANIMATIONEND: 'animationend' + (webkit ? ' webkitAnimationEnd' : ''), + TRANSITIONEND: 'transitionend' + (isWebkit ? ' webkitTransitionEnd' : ''), + ANIMATIONEND: 'animationend' + (isWebkit ? ' webkitAnimationEnd' : ''), TRANSFORM: vendorProperty('transform'), TRANSFORM_ORIGIN: vendorProperty('transformOrigin'), @@ -255,6 +443,7 @@ function MdConstantFactory($sniffer) { ANIMATION_TIMING: vendorProperty('animationTimingFunction'), ANIMATION_DIRECTION: vendorProperty('animationDirection') }, + /** * As defined in core/style/variables.scss * @@ -265,17 +454,20 @@ function MdConstantFactory($sniffer) { * */ 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)' , + '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)' , + 'landscape' : '(orientation: landscape)' , + 'portrait' : '(orientation: portrait)' , 'print' : 'print' }, + MEDIA_PRIORITY: [ 'xl', 'gt-lg', @@ -286,11 +478,14 @@ function MdConstantFactory($sniffer) { 'sm', 'gt-xs', 'xs', + 'landscape', + 'portrait', 'print' ] }; + + return self; } -MdConstantFactory.$inject = ["$sniffer"]; })(); (function(){ @@ -528,7 +723,8 @@ MdConstantFactory.$inject = ["$sniffer"]; (function(){ "use strict"; -angular.module('material.core') + +mdMediaFactory.$inject = ["$mdConstant", "$rootScope", "$window"];angular.module('material.core') .factory('$mdMedia', mdMediaFactory); /** @@ -588,13 +784,21 @@ angular.module('material.core') * (min-width: 1920px) * * + * landscape + * landscape + * + * + * portrait + * portrait + * + * * print * print * * * * - * See Material Design's Layout - Adaptive UI for more details. + * See Material Design's Layout - Adaptive UI for more details. * * * @@ -616,6 +820,7 @@ angular.module('material.core') * */ +/* @ngInject */ function mdMediaFactory($mdConstant, $rootScope, $window) { var queries = {}; var mqls = {}; @@ -703,7 +908,7 @@ function mdMediaFactory($mdConstant, $rootScope, $window) { }); return function unwatch() { - unwatchFns.forEach(function(fn) { fn(); }) + unwatchFns.forEach(function(fn) { fn(); }); }; } @@ -713,7 +918,107 @@ function mdMediaFactory($mdConstant, $rootScope, $window) { (normalizeCache[attrName] = attrs.$normalize(attrName)); } } -mdMediaFactory.$inject = ["$mdConstant", "$rootScope", "$window"]; + +})(); +(function(){ +"use strict"; + +angular + .module('material.core') + .config( ["$provide", function($provide) { + $provide.decorator('$mdUtil', ['$delegate', function ($delegate) { + + // Inject the prefixer into our original $mdUtil service. + $delegate.prefixer = MdPrefixer; + + return $delegate; + }]); + }]); + +function MdPrefixer(initialAttributes, buildSelector) { + var PREFIXES = ['data', 'x']; + + if (initialAttributes) { + // The prefixer also accepts attributes as a parameter, and immediately builds a list or selector for + // the specified attributes. + return buildSelector ? _buildSelector(initialAttributes) : _buildList(initialAttributes); + } + + return { + buildList: _buildList, + buildSelector: _buildSelector, + hasAttribute: _hasAttribute, + removeAttribute: _removeAttribute + }; + + function _buildList(attributes) { + attributes = angular.isArray(attributes) ? attributes : [attributes]; + + attributes.forEach(function(item) { + PREFIXES.forEach(function(prefix) { + attributes.push(prefix + '-' + item); + }); + }); + + return attributes; + } + + function _buildSelector(attributes) { + attributes = angular.isArray(attributes) ? attributes : [attributes]; + + return _buildList(attributes) + .map(function(item) { + return '[' + item + ']'; + }) + .join(','); + } + + function _hasAttribute(element, attribute) { + element = _getNativeElement(element); + + if (!element) { + return false; + } + + var prefixedAttrs = _buildList(attribute); + + for (var i = 0; i < prefixedAttrs.length; i++) { + if (element.hasAttribute(prefixedAttrs[i])) { + return true; + } + } + + return false; + } + + function _removeAttribute(element, attribute) { + element = _getNativeElement(element); + + if (!element) { + return; + } + + _buildList(attribute).forEach(function(prefixedAttribute) { + element.removeAttribute(prefixedAttribute); + }); + } + + /** + * Transforms a jqLite or DOM element into a HTML element. + * This is useful when supporting jqLite elements and DOM elements at + * same time. + * @param element {JQLite|Element} Element to be parsed + * @returns {HTMLElement} Parsed HTMLElement + */ + function _getNativeElement(element) { + element = element[0] || element; + + if (element.nodeType) { + return element; + } + } + +} })(); (function(){ @@ -725,6 +1030,7 @@ mdMediaFactory.$inject = ["$mdConstant", "$rootScope", "$window"]; * will create its own instance of this array and the app's IDs * will not be unique. */ +UtilFactory.$inject = ["$document", "$timeout", "$compile", "$rootScope", "$$mdAnimate", "$interpolate", "$log", "$rootElement", "$window", "$$rAF"]; var nextUniqueId = 0; /** @@ -737,7 +1043,10 @@ angular .module('material.core') .factory('$mdUtil', UtilFactory); -function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $interpolate, $log, $rootElement, $window) { +/** + * @ngInject + */ +function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $interpolate, $log, $rootElement, $window, $$rAF) { // Setup some core variables for the processTemplate method var startSymbol = $interpolate.startSymbol(), endSymbol = $interpolate.endSymbol(), @@ -761,13 +1070,46 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in return hasValue; }; + function validateCssValue(value) { + return !value ? '0' : + hasPx(value) || hasPercent(value) ? value : value + 'px'; + } + + function hasPx(value) { + return String(value).indexOf('px') > -1; + } + + function hasPercent(value) { + return String(value).indexOf('%') > -1; + + } + var $mdUtil = { dom: {}, - now: window.performance ? + now: window.performance && window.performance.now ? angular.bind(window.performance, window.performance.now) : Date.now || function() { return new Date().getTime(); }, + /** + * Cross-version compatibility method to retrieve an option of a ngModel controller, + * which supports the breaking changes in the AngularJS snapshot (SHA 87a2ff76af5d0a9268d8eb84db5755077d27c84c). + * @param {!angular.ngModelCtrl} ngModelCtrl + * @param {!string} optionName + * @returns {Object|undefined} + */ + getModelOption: function (ngModelCtrl, optionName) { + if (!ngModelCtrl.$options) { + return; + } + + var $options = ngModelCtrl.$options; + + // The newer versions of Angular introduced a `getOption function and made the option values no longer + // visible on the $options object. + return $options.getOption ? $options.getOption(optionName) : $options[optionName] + }, + /** * Bi-directional accessor/mutator used to easily update an element's * property based on the current 'dir'ectional value. @@ -779,22 +1121,29 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in if ( arguments.length == 0 ) return ltr ? 'ltr' : 'rtl'; // If mutator + var elem = angular.element(element); + if ( ltr && angular.isDefined(lValue)) { - angular.element(element).css(property, validate(lValue)); + elem.css(property, validateCssValue(lValue)); } else if ( !ltr && angular.isDefined(rValue)) { - angular.element(element).css(property, validate(rValue) ); + elem.css(property, validateCssValue(rValue) ); } + }, - // Internal utils + bidiProperty: function (element, lProperty, rProperty, value) { + var ltr = !($document[0].dir == 'rtl' || $document[0].body.dir == 'rtl'); - function validate(value) { - return !value ? '0' : - hasPx(value) ? value : value + 'px'; - } - function hasPx(value) { - return String(value).indexOf('px') > -1; - } + var elem = angular.element(element); + + if ( ltr && angular.isDefined(lProperty)) { + elem.css(lProperty, validateCssValue(value)); + elem.css(rProperty, ''); + } + else if ( !ltr && angular.isDefined(rProperty)) { + elem.css(rProperty, validateCssValue(value) ); + elem.css(lProperty, ''); + } }, clientRect: function(element, offsetParent, isOffsetRect) { @@ -830,18 +1179,12 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in }, /** - * Calculate the positive scroll offset - * TODO: Check with pinch-zoom in IE/Chrome; - * https://code.google.com/p/chromium/issues/detail?id=496285 + * Determines the absolute position of the viewport. + * Useful when making client rectangles absolute. + * @returns {number} */ - 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); + getViewportTop: function() { + return window.scrollY || window.pageYOffset || 0; }, /** @@ -852,14 +1195,14 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in * @returns {*} */ findFocusTarget: function(containerEl, attributeVal) { - var AUTO_FOCUS = '[md-autofocus]'; + var AUTO_FOCUS = this.prefixer('md-autofocus', true); var elToFocus; elToFocus = scanForFocusable(containerEl, attributeVal || AUTO_FOCUS); if ( !elToFocus && attributeVal != AUTO_FOCUS) { // Scan for deprecated attribute - elToFocus = scanForFocusable(containerEl, '[md-auto-focus]'); + elToFocus = scanForFocusable(containerEl, this.prefixer('md-auto-focus', true)); if ( !elToFocus ) { // Scan for fallback to 'universal' API @@ -881,9 +1224,9 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in items.length && angular.forEach(items, function(it) { it = angular.element(it); - // Check the element for the _md-autofocus class to ensure any associated expression + // Check the element for the md-autofocus class to ensure any associated expression // evaluated to true. - var isFocusable = it.hasClass('_md-autofocus'); + var isFocusable = it.hasClass('md-autofocus'); if (isFocusable) elFound = it; }); } @@ -891,103 +1234,120 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in } }, - // 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) { + /** + * Disables scroll around the passed parent element. + * @param element Unused + * @param {!Element|!angular.JQLite} parent Element to disable scrolling within. + * Defaults to body if none supplied. + * @param options Object of options to modify functionality + * - disableScrollMask Boolean of whether or not to create a scroll mask element or + * use the passed parent element. + */ + disableScrollAround: function(element, parent, options) { + options = options || {}; + + $mdUtil.disableScrollAround._count = Math.max(0, $mdUtil.disableScrollAround._count || 0); + $mdUtil.disableScrollAround._count++; + + if ($mdUtil.disableScrollAround._restoreScroll) { + return $mdUtil.disableScrollAround._restoreScroll; + } + + var body = $document[0].body; + var restoreBody = disableBodyScroll(); + var restoreElement = disableElementScroll(parent); + + return $mdUtil.disableScrollAround._restoreScroll = function() { + if (--$mdUtil.disableScrollAround._count <= 0) { restoreBody(); restoreElement(); - delete $mdUtil.disableScrollAround._enableScrolling; + delete $mdUtil.disableScrollAround._restoreScroll; } }; - // Creates a virtual scrolling mask to absorb touchmove, keyboard, scrollbar clicking, and wheel events + /** + * Creates a virtual scrolling mask to prevent 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]); + element = angular.element(element || body); + + var scrollMask; + + if (options.disableScrollMask) { + scrollMask = element; + } else { + scrollMask = angular.element( + '
' + + '
' + + '
'); + element.append(scrollMask); + } scrollMask.on('wheel', preventDefault); scrollMask.on('touchmove', preventDefault); - $document.on('keydown', disableKeyNav); - return function restoreScroll() { + return function restoreElementScroll() { 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(); - //} - } + if (!options.disableScrollMask) { + scrollMask[0].parentNode.removeChild(scrollMask[0]); + } + }; function preventDefault(e) { e.preventDefault(); } } - // Converts the body to a position fixed block and translate it to the proper scroll - // position + // 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 documentElement = $document[0].documentElement; + + var prevDocumentStyle = documentElement.style.cssText || ''; + var prevBodyStyle = body.style.cssText || ''; + + var viewportTop = $mdUtil.getViewportTop(); var clientWidth = body.clientWidth; + var hasVerticalScrollbar = body.scrollHeight > body.clientHeight + 1; - if (body.scrollHeight > body.clientHeight + 1) { - applyStyles(body, { + if (hasVerticalScrollbar) { + angular.element(body).css({ position: 'fixed', width: '100%', - top: -scrollOffset + 'px' + top: -viewportTop + 'px' }); + } - applyStyles(htmlNode, { - overflowY: 'scroll' - }); + if (body.clientWidth < clientWidth) { + body.style.overflow = 'hidden'; } - if (body.clientWidth < clientWidth) applyStyles(body, {overflow: 'hidden'}); + // This should be applied after the manipulation to the body, because + // adding a scrollbar can potentially resize it, causing the measurement + // to change. + if (hasVerticalScrollbar) { + documentElement.style.overflowY = 'scroll'; + } return function restoreScroll() { - body.style.cssText = restoreBodyStyle; - htmlNode.style.cssText = restoreHtmlStyle; - body.scrollTop = scrollOffset; - htmlNode.scrollTop = scrollOffset; + // Reset the inline style CSS to the previous. + body.style.cssText = prevBodyStyle; + documentElement.style.cssText = prevDocumentStyle; + + // The body loses its scroll position while being fixed. + body.scrollTop = viewportTop; }; } - function applyStyles(el, styles) { - for (var key in styles) { - el.style[key] = styles[key]; - } - } }, + enableScrolling: function() { - var method = this.disableScrollAround._enableScrolling; - method && method(); + var restoreFn = this.disableScrollAround._restoreScroll; + restoreFn && restoreFn(); }, + floatingScrollbars: function() { if (this.floatingScrollbars.cached === undefined) { var tempNode = angular.element('
').css({ @@ -1202,19 +1562,29 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in * 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 check Either a string or a function. If a string is passed, it will be evaluated against + * each of the parent nodes' tag name. If a function is passed, the loop will call it with each of + * the parents and will use the return value to determine whether the node is a match. * @param onlyParent Only start checking from the parent element, not `el`. */ - getClosest: function getClosest(el, tagName, onlyParent) { + getClosest: function getClosest(el, validateWith, onlyParent) { + if ( angular.isString(validateWith) ) { + var tagName = validateWith.toUpperCase(); + validateWith = function(el) { + return el.nodeName.toUpperCase() === tagName; + }; + } + 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) { + if (validateWith(el)) { return el; } } while (el = el.parentNode); + return null; }, @@ -1318,7 +1688,7 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in var queue = nextTick.queue || []; //-- add callback to the queue - queue.push(callback); + queue.push({scope: scope, callback: callback}); //-- set default value for digest if (digest == null) digest = true; @@ -1337,16 +1707,18 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in * Trigger digest if necessary */ function processQueue() { - var skip = scope && scope.$$destroyed; - var queue = !skip ? nextTick.queue : []; - var digest = !skip ? nextTick.digest : null; + var queue = nextTick.queue; + var digest = nextTick.digest; nextTick.queue = []; nextTick.timeout = null; nextTick.digest = false; - queue.forEach(function(callback) { - callback(); + queue.forEach(function(queueItem) { + var skip = queueItem.scope && queueItem.scope.$$destroyed; + if (!skip) { + queueItem.callback(); + } }); if (digest) $rootScope.$digest(); @@ -1393,21 +1765,129 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in }, /** - * Parses an attribute value, mostly a string. - * By default checks for negated values and returns `false“ if present. - * Negated values are: (native falsy) and negative strings like: - * `false` or `0`. - * @param value Attribute value which should be parsed. - * @param negatedCheck When set to false, won't check for negated values. - * @returns {boolean} + * Checks if the current browser is natively supporting the `sticky` position. + * @returns {string} supported sticky property name */ - parseAttributeBoolean: function(value, negatedCheck) { - return value === '' || !!value && (negatedCheck === false || value !== 'false' && value !== '0'); + checkStickySupport: function() { + var stickyProp; + var testEl = angular.element('
'); + $document[0].body.appendChild(testEl[0]); + + var stickyProps = ['sticky', '-webkit-sticky']; + for (var i = 0; i < stickyProps.length; ++i) { + testEl.css({ + position: stickyProps[i], + top: 0, + 'z-index': 2 + }); + + if (testEl.css('position') == stickyProps[i]) { + stickyProp = stickyProps[i]; + break; + } + } + + testEl.remove(); + + return stickyProp; + }, + + /** + * Parses an attribute value, mostly a string. + * By default checks for negated values and returns `false“ if present. + * Negated values are: (native falsy) and negative strings like: + * `false` or `0`. + * @param value Attribute value which should be parsed. + * @param negatedCheck When set to false, won't check for negated values. + * @returns {boolean} + */ + parseAttributeBoolean: function(value, negatedCheck) { + return value === '' || !!value && (negatedCheck === false || value !== 'false' && value !== '0'); + }, + + hasComputedStyle: hasComputedStyle, + + /** + * Returns true if the parent form of the element has been submitted. + * + * @param element An Angular or HTML5 element. + * + * @returns {boolean} + */ + isParentFormSubmitted: function(element) { + var parent = $mdUtil.getClosest(element, 'form'); + var form = parent ? angular.element(parent).controller('form') : null; + + return form ? form.$submitted : false; }, - hasComputedStyle: hasComputedStyle + /** + * Animate the requested element's scrollTop to the requested scrollPosition with basic easing. + * + * @param {!HTMLElement} element The element to scroll. + * @param {number} scrollEnd The new/final scroll position. + * @param {number=} duration Duration of the scroll. Default is 1000ms. + */ + animateScrollTo: function(element, scrollEnd, duration) { + var scrollStart = element.scrollTop; + var scrollChange = scrollEnd - scrollStart; + var scrollingDown = scrollStart < scrollEnd; + var startTime = $mdUtil.now(); + + $$rAF(scrollChunk); + + function scrollChunk() { + var newPosition = calculateNewPosition(); + + element.scrollTop = newPosition; + + if (scrollingDown ? newPosition < scrollEnd : newPosition > scrollEnd) { + $$rAF(scrollChunk); + } + } + + function calculateNewPosition() { + var easeDuration = duration || 1000; + var currentTime = $mdUtil.now() - startTime; + + return ease(currentTime, scrollStart, scrollChange, easeDuration); + } + + function ease(currentTime, start, change, duration) { + // If the duration has passed (which can occur if our app loses focus due to $$rAF), jump + // straight to the proper position + if (currentTime > duration) { + return start + change; + } + + var ts = (currentTime /= duration) * currentTime; + var tc = ts * currentTime; + + return start + change * (-2 * tc + 3 * ts); + } + }, + + /** + * Provides an easy mechanism for removing duplicates from an array. + * + * var myArray = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]; + * + * $mdUtil.uniq(myArray) => [1, 2, 3, 4] + * + * @param {array} array The array whose unique values should be returned. + * + * @returns {array} A copy of the array containing only unique values. + */ + uniq: function(array) { + if (!array) { return; } + + return array.filter(function(value, index, self) { + return self.indexOf(value) === index; + }); + } }; + // Instantiate other namespace utility methods $mdUtil.dom.animator = $$mdAnimate($mdUtil); @@ -1419,7 +1899,6 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in } } -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. @@ -1439,24 +1918,415 @@ angular.element.prototype.blur = angular.element.prototype.blur || function() { return this; }; +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.core.compiler + * @description + * Angular Material template and element compiler. + */ +MdCompilerService.$inject = ["$q", "$templateRequest", "$injector", "$compile", "$controller"]; +angular + .module('material.core') + .service('$mdCompiler', MdCompilerService); + +/** + * @ngdoc service + * @name $mdCompiler + * @module material.core.compiler + * @description + * The $mdCompiler service is an abstraction of Angular's compiler, that allows developers + * to easily compile an element with options like in a Directive Definition Object. + * + * > The compiler powers a lot of components inside of Angular Material. + * > Like the `$mdPanel` or `$mdDialog`. + * + * @usage + * + * Basic Usage with a template + * + * + * $mdCompiler.compile({ + * templateUrl: 'modal.html', + * controller: 'ModalCtrl', + * locals: { + * modal: myModalInstance; + * } + * }).then(function (compileData) { + * compileData.element; // Compiled DOM element + * compileData.link(myScope); // Instantiate controller and link element to scope. + * }); + * + * + * Example with a content element + * + * + * + * // Create a virtual element and link it manually. + * // The compiler doesn't need to recompile the element each time. + * var myElement = $compile('Test')(myScope); + * + * $mdCompiler.compile({ + * contentElement: myElement + * }).then(function (compileData) { + * compileData.element // Content Element (same as above) + * compileData.link // This does nothing when using a contentElement. + * }); + * + * + * > Content Element is a significant performance improvement when the developer already knows that the + * > compiled element will be always the same and the scope will not change either. + * + * The `contentElement` option also supports DOM elements which will be temporary removed and restored + * at its old position. + * + * + * var domElement = document.querySelector('#myElement'); + * + * $mdCompiler.compile({ + * contentElement: myElement + * }).then(function (compileData) { + * compileData.element // Content Element (same as above) + * compileData.link // This does nothing when using a contentElement. + * }); + * + * + * The `$mdCompiler` can also query for the element in the DOM itself. + * + * + * $mdCompiler.compile({ + * contentElement: '#myElement' + * }).then(function (compileData) { + * compileData.element // Content Element (same as above) + * compileData.link // This does nothing when using a contentElement. + * }); + * + * + */ +function MdCompilerService($q, $templateRequest, $injector, $compile, $controller) { + /** @private @const {!angular.$q} */ + this.$q = $q; + + /** @private @const {!angular.$templateRequest} */ + this.$templateRequest = $templateRequest; + + /** @private @const {!angular.$injector} */ + this.$injector = $injector; + + /** @private @const {!angular.$compile} */ + this.$compile = $compile; + + /** @private @const {!angular.$controller} */ + this.$controller = $controller; +} + +/** + * @ngdoc method + * @name $mdCompiler#compile + * @description + * + * A method to compile a HTML template with the Angular compiler. + * The `$mdCompiler` is wrapper around the Angular compiler and provides extra functionality + * like controller instantiation or async resolves. + * + * @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. + * - `contentElement` - `{string|Element}`: Instead of using a template, which will be + * compiled each time, you can also use a DOM element.
+ * - `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 + * + */ +MdCompilerService.prototype.compile = function(options) { + + if (options.contentElement) { + return this._prepareContentElement(options); + } else { + return this._compileTemplate(options); + } + +}; + +/** + * Instead of compiling any template, the compiler just fetches an existing HTML element from the DOM and + * provides a restore function to put the element back it old DOM position. + * @param {!Object} options Options to be used for the compiler. + * @private + */ +MdCompilerService.prototype._prepareContentElement = function(options) { + + var contentElement = this._fetchContentElement(options); + + return this.$q.resolve({ + element: contentElement.element, + cleanup: contentElement.restore, + locals: {}, + link: function() { + return contentElement.element; + } + }); + +}; + +/** + * Compiles a template by considering all options and waiting for all resolves to be ready. + * @param {!Object} options Compile options + * @returns {!Object} Compile data with link function. + * @private + */ +MdCompilerService.prototype._compileTemplate = function(options) { + + var self = this; + var templateUrl = options.templateUrl; + var template = options.template || ''; + var resolve = angular.extend({}, options.resolve); + var locals = angular.extend({}, options.locals); + var transformTemplate = options.transformTemplate || angular.identity; + + // Take resolve values and invoke them. + // Resolves can either be a string (value: 'MyRegisteredAngularConst'), + // or an invokable 'factory' of sorts: (value: function ValueGetter($dependency) {}) + angular.forEach(resolve, function(value, key) { + if (angular.isString(value)) { + resolve[key] = self.$injector.get(value); + } else { + resolve[key] = self.$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.$$ngTemplate = this.$templateRequest(templateUrl); + } else { + resolve.$$ngTemplate = this.$q.when(template); + } + + + // Wait for all the resolves to finish if they are promises + return this.$q.all(resolve).then(function(locals) { + + var template = transformTemplate(locals.$$ngTemplate, options); + var element = options.element || angular.element('
').html(template.trim()).contents(); + + return self._compileElement(locals, element, options); + }); + +}; + +/** + * Method to compile an element with the given options. + * @param {!Object} locals Locals to be injected to the controller if present + * @param {!JQLite} element Element to be compiled and linked + * @param {!Object} options Options to be used for linking. + * @returns {!Object} Compile data with link function. + * @private + */ +MdCompilerService.prototype._compileElement = function(locals, element, options) { + var self = this; + var ngLinkFn = this.$compile(element); + + var compileData = { + element: element, + cleanup: element.remove.bind(element), + locals: locals, + link: linkFn + }; + + function linkFn(scope) { + locals.$scope = scope; + + // Instantiate controller if the developer provided one. + if (options.controller) { + + var injectLocals = angular.extend(locals, { + $element: element + }); + + var invokeCtrl = self.$controller(options.controller, injectLocals, true, options.controllerAs); + + if (options.bindToController) { + angular.extend(invokeCtrl.instance, locals); + } + + var ctrl = invokeCtrl(); + + // Unique identifier for Angular Route ngView controllers. + element.data('$ngControllerController', ctrl); + element.children().data('$ngControllerController', ctrl); + + // Expose the instantiated controller to the compile data + compileData.controller = ctrl; + } + + // Invoke the Angular $compile link function. + return ngLinkFn(scope); + } + + return compileData; + +}; + +/** + * Fetches an element removing it from the DOM and using it temporary for the compiler. + * Elements which were fetched will be restored after use. + * @param {!Object} options Options to be used for the compilation. + * @returns {{element: !JQLite, restore: !Function}} + * @private + */ +MdCompilerService.prototype._fetchContentElement = function(options) { + + var contentEl = options.contentElement; + var restoreFn = null; + + if (angular.isString(contentEl)) { + contentEl = document.querySelector(contentEl); + restoreFn = createRestoreFn(contentEl); + } else { + contentEl = contentEl[0] || contentEl; + + // When the element is visible in the DOM, then we restore it at close of the dialog. + // Otherwise it will be removed from the DOM after close. + if (document.contains(contentEl)) { + restoreFn = createRestoreFn(contentEl); + } else { + restoreFn = function() { + if (contentEl.parentNode) { + contentEl.parentNode.removeChild(contentEl); + } + } + } + } + + return { + element: angular.element(contentEl), + restore: restoreFn + }; + + function createRestoreFn(element) { + var parent = element.parentNode; + var nextSibling = element.nextElementSibling; + + return function() { + if (!nextSibling) { + // When the element didn't had any sibling, then it can be simply appended to the + // parent, because it plays no role, which index it had before. + parent.appendChild(element); + } else { + // When the element had a sibling, which marks the previous position of the element + // in the DOM, we insert it correctly before the sibling, to have the same index as + // before. + parent.insertBefore(element, nextSibling); + } + } + } +}; + })(); (function(){ "use strict"; +/** + * @ngdoc module + * @name material.core.aria + * @description + * Aria Expectations for ngMaterial components. + */ +MdAriaService.$inject = ["$$rAF", "$log", "$window", "$interpolate"]; +angular + .module('material.core') + .provider('$mdAria', MdAriaProvider); -angular.module('material.core') - .service('$mdAria', AriaService); +/** + * @ngdoc service + * @name $mdAriaProvider + * @module material.core.aria + * + * @description + * + * Modify options of the `$mdAria` service, which will be used by most of the Angular Material + * components. + * + * You are able to disable `$mdAria` warnings, by using the following markup. + * + * + * app.config(function($mdAriaProvider) { + * // Globally disables all ARIA warnings. + * $mdAriaProvider.disableWarnings(); + * }); + * + * + */ +function MdAriaProvider() { + + var config = { + /** Whether we should show ARIA warnings in the console if labels are missing on the element */ + showWarnings: true + }; + + return { + disableWarnings: disableWarnings, + $get: ["$$rAF", "$log", "$window", "$interpolate", function($$rAF, $log, $window, $interpolate) { + return MdAriaService.apply(config, arguments); + }] + }; + + /** + * @ngdoc method + * @name $mdAriaProvider#disableWarnings + * @description Disables all ARIA warnings generated by Angular Material. + */ + function disableWarnings() { + config.showWarnings = false; + } +} /* * @ngInject */ -function AriaService($$rAF, $log, $window, $interpolate) { +function MdAriaService($$rAF, $log, $window, $interpolate) { + + // Load the showWarnings option from the current context and store it inside of a scope variable, + // because the context will be probably lost in some function calls. + var showWarnings = this.showWarnings; return { expect: expect, expectAsync: expectAsync, - expectWithText: expectWithText + expectWithText: expectWithText, + expectWithoutText: expectWithoutText, + getText: getText }; /** @@ -1478,7 +2348,7 @@ function AriaService($$rAF, $log, $window, $interpolate) { defaultValue = angular.isString(defaultValue) ? defaultValue.trim() : ''; if (defaultValue.length) { element.attr(attrName, defaultValue); - } else { + } else if (showWarnings) { $log.warn('ARIA: Attribute "', attrName, '", required for accessibility, is missing on node:', node); } @@ -1496,9 +2366,9 @@ function AriaService($$rAF, $log, $window, $interpolate) { function expectWithText(element, attrName) { var content = getText(element) || ""; - var hasBinding = content.indexOf($interpolate.startSymbol())>-1; + var hasBinding = content.indexOf($interpolate.startSymbol()) > -1; - if ( hasBinding ) { + if (hasBinding) { expectAsync(element, attrName, function() { return getText(element); }); @@ -1507,8 +2377,36 @@ function AriaService($$rAF, $log, $window, $interpolate) { } } + function expectWithoutText(element, attrName) { + var content = getText(element); + var hasBinding = content.indexOf($interpolate.startSymbol()) > -1; + + if ( !hasBinding && !content) { + expect(element, attrName, content); + } + } + function getText(element) { - return (element.text() || "").trim(); + element = element[0] || element; + var walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false); + var text = ''; + + var node; + while (node = walker.nextNode()) { + if (!isAriaHiddenNode(node)) { + text += node.textContent; + } + } + + return text.trim() || ''; + + function isAriaHiddenNode(node) { + while (node.parentNode && (node = node.parentNode) !== element) { + if (node.getAttribute && node.getAttribute('aria-hidden') === 'true') { + return true; + } + } + } } function childHasAttribute(node, attrName) { @@ -1520,179 +2418,36 @@ function AriaService($$rAF, $log, $window, $interpolate) { return (style.display === 'none'); } - if(hasChildren) { + 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 = {}; +MdGesture.$inject = ["$$MdGestureHandler", "$$rAF", "$timeout"]; +attachToDocument.$inject = ["$mdGesture", "$$MdGestureHandler"];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; /** @@ -1760,6 +2515,7 @@ 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 touchActionProperty = getTouchAction(); var hasJQuery = (typeof window.jQuery !== 'undefined') && (angular.element === window.jQuery); var self = { @@ -1795,7 +2551,7 @@ function MdGesture($$MdGestureHandler, $$rAF, $timeout) { return (element.getAttribute('tabindex') != '-1') && !element.hasAttribute('DISABLED') && - (element.hasAttribute('tabindex') || element.hasAttribute('href') || + (element.hasAttribute('tabindex') || element.hasAttribute('href') || element.isContentEditable || (focusableElements.indexOf(element.nodeName) != -1)); } } @@ -1856,7 +2612,7 @@ function MdGesture($$MdGestureHandler, $$rAF, $timeout) { * 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 + * @see GestureHandler */ return self /* @@ -1902,7 +2658,7 @@ function MdGesture($$MdGestureHandler, $$rAF, $timeout) { // 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 (!touchActionProperty && ev.type === 'touchmove') ev.preventDefault(); // If the user moves greater than pixels, stop the hold timer // set in onStart @@ -1921,7 +2677,7 @@ function MdGesture($$MdGestureHandler, $$rAF, $timeout) { * 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 * + * the perpendicular 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', { @@ -1930,6 +2686,18 @@ function MdGesture($$MdGestureHandler, $$rAF, $timeout) { horizontal: true, cancelMultiplier: 1.5 }, + onSetup: function(element, options) { + if (touchActionProperty) { + // We check for horizontal to be false, because otherwise we would overwrite the default opts. + this.oldTouchAction = element[0].style[touchActionProperty]; + element[0].style[touchActionProperty] = options.horizontal ? 'pan-y' : 'pan-x'; + } + }, + onCleanup: function(element) { + if (this.oldTouchAction) { + element[0].style[touchActionProperty] = this.oldTouchAction; + } + }, onStart: function (ev) { // For drag, require a parent to be registered with $mdGesture.register() if (!this.state.registeredParent) this.cancel(); @@ -1940,7 +2708,7 @@ function MdGesture($$MdGestureHandler, $$rAF, $timeout) { // 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 (!touchActionProperty && ev.type === 'touchmove') ev.preventDefault(); if (!this.state.dragPointer) { if (this.state.options.horizontal) { @@ -1964,7 +2732,7 @@ function MdGesture($$MdGestureHandler, $$rAF, $timeout) { this.dispatchDragMove(ev); } }, - // Only dispatch dragmove events every frame; any more is unnecessray + // Only dispatch dragmove events every frame; any more is unnecessary dispatchDragMove: $$rAF.throttle(function (ev) { // Make sure the drag didn't stop while waiting for the next frame if (this.state.isRunning) { @@ -2005,8 +2773,20 @@ function MdGesture($$MdGestureHandler, $$rAF, $timeout) { } }); + function getTouchAction() { + var testEl = document.createElement('div'); + var vendorPrefixes = ['', 'webkit', 'Moz', 'MS', 'ms', 'o']; + + for (var i = 0; i < vendorPrefixes.length; i++) { + var prefix = vendorPrefixes[i]; + var property = prefix ? prefix + 'TouchAction' : 'touchAction'; + if (angular.isDefined(testEl.style[property])) { + return property; + } + } + } + } -MdGesture.$inject = ["$$MdGestureHandler", "$$rAF", "$timeout"]; /** * MdGestureHandler @@ -2034,6 +2814,8 @@ function MdGestureHandler() { dispatchEvent: hasJQuery ? jQueryDispatchEvent : nativeDispatchEvent, // These are overridden by the registered handler + onSetup: angular.noop, + onCleanup: angular.noop, onStart: angular.noop, onMove: angular.noop, onEnd: angular.noop, @@ -2083,7 +2865,7 @@ function MdGestureHandler() { return null; }, - // Called from $mdGesture.register when an element reigsters itself with a handler. + // Called from $mdGesture.register when an element registers 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) { @@ -2092,11 +2874,15 @@ function MdGestureHandler() { element[0].$mdGesture[this.name] = options || {}; element.on('$destroy', onDestroy); + self.onSetup(element, options || {}); + return onDestroy; function onDestroy() { delete element[0].$mdGesture[self.name]; element.off('$destroy', onDestroy); + + self.onCleanup(element, options || {}); } } }; @@ -2184,7 +2970,7 @@ function attachToDocument( $mdGesture, $$MdGestureHandler ) { * 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 @@ -2306,7 +3092,6 @@ function attachToDocument( $mdGesture, $$MdGestureHandler ) { } } -attachToDocument.$inject = ["$mdGesture", "$$MdGestureHandler"]; // ******************** // Module Functions @@ -2424,8 +3209,8 @@ angular.module('material.core') */ function InterimElementProvider() { + InterimElementFactory.$inject = ["$document", "$q", "$rootScope", "$timeout", "$rootElement", "$animate", "$mdUtil", "$mdCompiler", "$mdTheming", "$injector", "$exceptionHandler"]; createInterimElementProvider.$get = InterimElementFactory; - InterimElementFactory.$inject = ["$document", "$q", "$$q", "$rootScope", "$timeout", "$rootElement", "$animate", "$mdUtil", "$mdCompiler", "$mdTheming", "$injector"]; return createInterimElementProvider; /** @@ -2434,6 +3219,7 @@ function InterimElementProvider() { * as well as configuration of 'preset' methods (eg dialog.basic(): basic is a preset method) */ function createInterimElementProvider(interimFactoryName) { + factory.$inject = ["$$interimElement", "$injector"]; var EXPOSED_METHODS = ['onHide', 'onShow', 'onRemove']; var customMethods = {}; @@ -2452,11 +3238,10 @@ function InterimElementProvider() { * all interim elements will come with the 'build' preset */ provider.addPreset('build', { - methods: ['controller', 'controllerAs', 'resolve', - 'template', 'templateUrl', 'themable', 'transformTemplate', 'parent'] + methods: ['controller', 'controllerAs', 'resolve', 'multiple', + 'template', 'templateUrl', 'themable', 'transformTemplate', 'parent', 'contentElement'] }); - factory.$inject = ["$$interimElement", "$injector"]; return provider; /** @@ -2644,8 +3429,8 @@ function InterimElementProvider() { } /* @ngInject */ - function InterimElementFactory($document, $q, $$q, $rootScope, $timeout, $rootElement, $animate, - $mdUtil, $mdCompiler, $mdTheming, $injector ) { + function InterimElementFactory($document, $q, $rootScope, $timeout, $rootElement, $animate, + $mdUtil, $mdCompiler, $mdTheming, $injector, $exceptionHandler) { return function createInterimElementService() { var SHOW_CANCELLED = false; @@ -2657,15 +3442,20 @@ function InterimElementProvider() { * A service used to control inserting and removing an element into the DOM. * */ - var service, stack = []; + + var service; + + var showPromises = []; // Promises for the interim's which are currently opening. + var hidePromises = []; // Promises for the interim's which are currently hiding. + var showingInterims = []; // Interim elements which are currently showing up. // Publish instance $$interimElement service; // ... used as $mdDialog, $mdToast, $mdMenu, and $mdSelect return service = { show: show, - hide: hide, - cancel: cancel, + hide: waitForInterim(hide), + cancel: waitForInterim(cancel), destroy : destroy, $injector_: $injector }; @@ -2686,26 +3476,47 @@ function InterimElementProvider() { 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) + // When an interim element is currently showing, we have to cancel it. + // Just hiding it, will resolve the InterimElement's promise, the promise should be + // rejected instead. + var hideAction = options.multiple ? $q.resolve() : $q.all(showPromises); + + if (!options.multiple) { + // Wait for all opening interim's to finish their transition. + hideAction = hideAction.then(function() { + // Wait for all closing and showing interim's to be completely closed. + var promiseArray = hidePromises.concat(showingInterims.map(service.cancel)); + return $q.all(promiseArray); + }); + } - hideExisting.finally(function() { + var showAction = hideAction.then(function() { - stack.push(interimElement); - interimElement + return interimElement .show() - .catch(function( reason ) { - //$log.error("InterimElement.show() error: " + reason ); - return reason; + .catch(function(reason) { return reason; }) + .finally(function() { + showPromises.splice(showPromises.indexOf(showAction), 1); + showingInterims.push(interimElement); }); }); + showPromises.push(showAction); + + // In Angular 1.6+, exceptions inside promises will cause a rejection. We need to handle + // the rejection and only log it if it's an error. + interimElement.deferred.promise.catch(function(fault) { + if (fault instanceof Error) { + $exceptionHandler(fault); + } + + return fault; + }); + // Return a promise that will be resolved when the interim // element is hidden or cancelled... - return interimElement.deferred.promise; } @@ -2722,27 +3533,30 @@ function InterimElementProvider() { * */ 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; + // We have to make a shallow copy of the array, because otherwise the map will break. + return $q.all(showingInterims.slice().reverse().map(closeElement)); } else if (options.closeTo !== undefined) { - return $q.all(stack.splice(options.closeTo).map(closeElement)); - } else { - var interim = stack.pop(); - return closeElement(interim); + return $q.all(showingInterims.slice(options.closeTo).map(closeElement)); } + // Hide the latest showing interim element. + return closeElement(showingInterims[showingInterims.length - 1]); + function closeElement(interim) { - interim + + var hideAction = interim .remove(reason, false, options || { }) - .catch(function( reason ) { - //$log.error("InterimElement.hide() error: " + reason ); - return reason; + .catch(function(reason) { return reason; }) + .finally(function() { + hidePromises.splice(hidePromises.indexOf(hideAction), 1); }); + + showingInterims.splice(showingInterims.indexOf(interim), 1); + hidePromises.push(hideAction); + return interim.deferred.promise; } } @@ -2760,43 +3574,76 @@ function InterimElementProvider() { * */ function cancel(reason, options) { - var interim = stack.pop(); - if ( !interim ) return $q.when(reason); - - interim - .remove(reason, true, options || { }) - .catch(function( reason ) { - //$log.error("InterimElement.cancel() error: " + reason ); - return reason; + var interim = showingInterims.pop(); + if (!interim) { + return $q.when(reason); + } + + var cancelAction = interim + .remove(reason, true, options || {}) + .catch(function(reason) { return reason; }) + .finally(function() { + hidePromises.splice(hidePromises.indexOf(cancelAction), 1); }); - return interim.deferred.promise; + hidePromises.push(cancelAction); + + // Since Angular 1.6.7, promises will be logged to $exceptionHandler when the promise + // is not handling the rejection. We create a pseudo catch handler, which will prevent the + // promise from being logged to the $exceptionHandler. + return interim.deferred.promise.catch(angular.noop); + } + + /** + * Creates a function to wait for at least one interim element to be available. + * @param callbackFn Function to be used as callback + * @returns {Function} + */ + function waitForInterim(callbackFn) { + return function() { + var fnArguments = arguments; + + if (!showingInterims.length) { + // When there are still interim's opening, then wait for the first interim element to + // finish its open animation. + if (showPromises.length) { + return showPromises[0].finally(function () { + return callbackFn.apply(service, fnArguments); + }); + } + + return $q.when("No interim elements currently showing up."); + } + + return callbackFn.apply(service, fnArguments); + }; } /* * 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); - }); + function destroy(targetEl) { + var interim = !targetEl ? showingInterims.shift() : null; - // 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); - } + var parentEl = angular.element(targetEl).length && angular.element(targetEl)[0].parentNode; + + if (parentEl) { + // Try to find the interim in the stack which corresponds to the supplied DOM element. + var filtered = showingInterims.filter(function(entry) { + return entry.options.element[0] === parentEl; + }); + + // Note: This function might be called when the element already has been removed, + // in which case we won't find any matches. + if (filtered.length) { + interim = filtered[0]; + showingInterims.splice(showingInterims.indexOf(interim), 1); + } } - return interim ? interim.remove(SHOW_CANCELLED, false, {'$destroy':true}) : $q.when(SHOW_CANCELLED); + return interim ? interim.remove(SHOW_CANCELLED, false, { '$destroy': true }) : + $q.when(SHOW_CANCELLED); } /* @@ -2820,16 +3667,22 @@ function InterimElementProvider() { * Use optional autoHided and transition-in effects */ function createAndTransitionIn() { - return $q(function(resolve, reject){ + return $q(function(resolve, reject) { + + // Trigger onCompiling callback before the compilation starts. + // This is useful, when modifying options, which can be influenced by developers. + options.onCompiling && options.onCompiling(options); compileElement(options) .then(function( compiledData ) { element = linkElement( compiledData, options ); - showAction = showElement(element, options, compiledData.controller) - .then(resolve, rejectAll ); + // Expose the cleanup function from the compiler. + options.cleanupElement = compiledData.cleanup; - }, rejectAll); + showAction = showElement(element, options, compiledData.controller) + .then(resolve, rejectAll); + }).catch(rejectAll); function rejectAll(fault) { // Force the '$md.show()' promise to reject @@ -2863,15 +3716,11 @@ function InterimElementProvider() { }); } else { - - $q.when(showAction) - .finally(function() { - hideElement(options.element, options).then(function() { - - (isCancelled && rejectAll(response)) || resolveAll(response); - - }, rejectAll); - }); + $q.when(showAction).finally(function() { + hideElement(options.element, options).then(function() { + isCancelled ? rejectAll(response) : resolveAll(response); + }, rejectAll); + }); return self.deferred.promise; } @@ -3002,14 +3851,14 @@ function InterimElementProvider() { autoHideTimer = $timeout(service.hide, options.hideDelay) ; cancelAutoHide = function() { $timeout.cancel(autoHideTimer); - } + }; } // Cache for subsequent use options.cancelAutoHide = function() { cancelAutoHide(); options.cancelAutoHide = undefined; - } + }; } /** @@ -3022,7 +3871,12 @@ function InterimElementProvider() { // Trigger onComplete callback when the `show()` finishes var notifyComplete = options.onComplete || angular.noop; - notifyShowing(options.scope, element, options, controller); + // Necessary for consistency between Angular 1.5 and 1.6. + try { + notifyShowing(options.scope, element, options, controller); + } catch (e) { + return $q.reject(e); + } return $q(function (resolve, reject) { try { @@ -3033,10 +3887,9 @@ function InterimElementProvider() { startAutoHide(); resolve(element); + }, reject); - }, reject ); - - } catch(e) { + } catch (e) { reject(e.message); } }); @@ -3045,34 +3898,33 @@ function InterimElementProvider() { function hideElement(element, options) { var announceRemoving = options.onRemoving || angular.noop; - return $$q(function (resolve, reject) { + return $q(function (resolve, reject) { try { // Start transitionIn - var action = $$q.when( options.onRemove(options.scope, element, options) || true ); + 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 ) { - + if (options.$destroy) { // For $destroy, onRemove should be synchronous resolve(element); + if (!options.preserveScope && options.scope ) { + // scope destroy should still be be done after the current digest is done + action.then( function() { options.scope.$destroy(); }); + } } else { - // Wait until transition-out is done action.then(function () { - if (!options.preserveScope && options.scope ) { options.scope.$destroy(); } resolve(element); - - }, reject ); + }, reject); } - - } catch(e) { + } catch (e) { reject(e.message); } }); @@ -3089,6 +3941,161 @@ function InterimElementProvider() { (function(){ "use strict"; +/** + * @ngdoc module + * @name material.core.interaction + * @description + * User interaction detection to provide proper accessibility. + */ +MdInteractionService.$inject = ["$timeout", "$mdUtil"]; +angular + .module('material.core.interaction', []) + .service('$mdInteraction', MdInteractionService); + + +/** + * @ngdoc service + * @name $mdInteraction + * @module material.core.interaction + * + * @description + * + * Service which keeps track of the last interaction type and validates them for several browsers. + * The service hooks into the document's body and listens for touch, mouse and keyboard events. + * + * The most recent interaction type can be retrieved by calling the `getLastInteractionType` method. + * + * Here is an example markup for using the interaction service. + * + * + * var lastType = $mdInteraction.getLastInteractionType(); + * + * if (lastType === 'keyboard') { + * // We only restore the focus for keyboard users. + * restoreFocus(); + * } + * + * + */ +function MdInteractionService($timeout, $mdUtil) { + this.$timeout = $timeout; + this.$mdUtil = $mdUtil; + + this.bodyElement = angular.element(document.body); + this.isBuffering = false; + this.bufferTimeout = null; + this.lastInteractionType = null; + this.lastInteractionTime = null; + + // Type Mappings for the different events + // There will be three three interaction types + // `keyboard`, `mouse` and `touch` + // type `pointer` will be evaluated in `pointerMap` for IE Browser events + this.inputEventMap = { + 'keydown': 'keyboard', + 'mousedown': 'mouse', + 'mouseenter': 'mouse', + 'touchstart': 'touch', + 'pointerdown': 'pointer', + 'MSPointerDown': 'pointer' + }; + + // IE PointerDown events will be validated in `touch` or `mouse` + // Index numbers referenced here: https://msdn.microsoft.com/library/windows/apps/hh466130.aspx + this.iePointerMap = { + 2: 'touch', + 3: 'touch', + 4: 'mouse' + }; + + this.initializeEvents(); +} + +/** + * Initializes the interaction service, by registering all interaction events to the + * body element. + */ +MdInteractionService.prototype.initializeEvents = function() { + // IE browsers can also trigger pointer events, which also leads to an interaction. + var pointerEvent = 'MSPointerEvent' in window ? 'MSPointerDown' : 'PointerEvent' in window ? 'pointerdown' : null; + + this.bodyElement.on('keydown mousedown', this.onInputEvent.bind(this)); + + if ('ontouchstart' in document.documentElement) { + this.bodyElement.on('touchstart', this.onBufferInputEvent.bind(this)); + } + + if (pointerEvent) { + this.bodyElement.on(pointerEvent, this.onInputEvent.bind(this)); + } + +}; + +/** + * Event listener for normal interaction events, which should be tracked. + * @param event {MouseEvent|KeyboardEvent|PointerEvent|TouchEvent} + */ +MdInteractionService.prototype.onInputEvent = function(event) { + if (this.isBuffering) { + return; + } + + var type = this.inputEventMap[event.type]; + + if (type === 'pointer') { + type = this.iePointerMap[event.pointerType] || event.pointerType; + } + + this.lastInteractionType = type; + this.lastInteractionTime = this.$mdUtil.now(); +}; + +/** + * Event listener for interaction events which should be buffered (touch events). + * @param event {TouchEvent} + */ +MdInteractionService.prototype.onBufferInputEvent = function(event) { + this.$timeout.cancel(this.bufferTimeout); + + this.onInputEvent(event); + this.isBuffering = true; + + // The timeout of 650ms is needed to delay the touchstart, because otherwise the touch will call + // the `onInput` function multiple times. + this.bufferTimeout = this.$timeout(function() { + this.isBuffering = false; + }.bind(this), 650, false); + +}; + +/** + * @ngdoc method + * @name $mdInteraction#getLastInteractionType + * @description Retrieves the last interaction type triggered in body. + * @returns {string|null} Last interaction type. + */ +MdInteractionService.prototype.getLastInteractionType = function() { + return this.lastInteractionType; +}; + +/** + * @ngdoc method + * @name $mdInteraction#isUserInvoked + * @description Method to detect whether any interaction happened recently or not. + * @param {number=} checkDelay Time to check for any interaction to have been triggered. + * @returns {boolean} Whether there was any interaction or not. + */ +MdInteractionService.prototype.isUserInvoked = function(checkDelay) { + var delay = angular.isNumber(checkDelay) ? checkDelay : 15; + + // Check for any interaction to be within the specified check time. + return this.lastInteractionTime >= this.$mdUtil.now() - delay; +}; + +})(); +(function(){ +"use strict"; + (function() { 'use strict'; @@ -3192,8 +4199,25 @@ function InterimElementProvider() { // Register other, special directive functions for the Layout features: module - .directive('mdLayoutCss' , disableLayoutDirective ) - .directive('ngCloak' , buildCloakInterceptor('ng-cloak')) + + .provider('$$mdLayout' , function() { + // Publish internal service for Layouts + return { + $get : angular.noop, + validateAttributeValue : validateAttributeValue, + validateAttributeUsage : validateAttributeUsage, + /** + * Easy way to disable/enable the Layout API. + * When disabled, this stops all attribute-to-classname generations + */ + disableLayouts : function(isDisabled) { + config.enabled = (isDisabled !== true); + } + }; + }) + + .directive('mdLayoutCss' , disableLayoutDirective ) + .directive('ngCloak' , buildCloakInterceptor('ng-cloak')) .directive('layoutWrap' , attributeWithoutValue('layout-wrap')) .directive('layoutNowrap' , attributeWithoutValue('layout-nowrap')) @@ -3217,7 +4241,10 @@ function InterimElementProvider() { .directive('hideLtMd' , warnAttrNotSupported('hide-lt-md')) .directive('hideLtLg' , warnAttrNotSupported('hide-lt-lg')) .directive('showLtMd' , warnAttrNotSupported('show-lt-md')) - .directive('showLtLg' , warnAttrNotSupported('show-lt-lg')); + .directive('showLtLg' , warnAttrNotSupported('show-lt-lg')) + + // Determine if + .config( detectDisabledLayouts ); /** * Converts snake_case to camelCase. @@ -3234,6 +4261,21 @@ function InterimElementProvider() { } + + /** + * Detect if any of the HTML tags has a [md-layouts-disabled] attribute; + * If yes, then immediately disable all layout API features + * + * Note: this attribute should be specified on either the HTML or BODY tags + */ + /** + * @ngInject + */ + function detectDisabledLayouts() { + var isDisabled = !!document.querySelector('[md-layouts-disabled]'); + config.enabled = !isDisabled; + } + /** * Special directive that will disable ALL Layout conversions of layout * attribute(s) to classname(s). @@ -3255,19 +4297,18 @@ function InterimElementProvider() { * */ function disableLayoutDirective() { + // Return a 1x-only, first-match attribute directive + config.enabled = false; + return { restrict : 'A', - priority : '900', - compile : function(element, attr) { - config.enabled = false; - return angular.noop; - } + priority : '900' }; } /** * Tail-hook ngCloak to delay the uncloaking while Layout transformers - * finish processing. Eliminates flicker with Material.Layoouts + * finish processing. Eliminates flicker with Material.Layouts */ function buildCloakInterceptor(className) { return [ '$timeout', function($timeout){ @@ -3347,7 +4388,7 @@ function InterimElementProvider() { var unwatch = attrs.$observe(attrs.$normalize(className), updateFn); updateFn(getNormalizedAttrValue(className, attrs, "")); - scope.$on("$destroy", function() { unwatch() }); + scope.$on("$destroy", function() { unwatch(); }); } } @@ -3560,7 +4601,7 @@ function InterimElementProvider() { attrValue = (attrValue || ""); - if ( attrValue.indexOf("-") == 0 || attrValue.indexOf(" ") == 0) { + if ( attrValue.indexOf("-") === 0 || attrValue.indexOf(" ") === 0) { // For missing main-axis values attrValue = "none" + attrValue; } @@ -3587,39 +4628,258 @@ function InterimElementProvider() { (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); +/** + * @ngdoc module + * @name material.core.liveannouncer + * @description + * Angular Material Live Announcer to provide accessibility for Voice Readers. + */ +MdLiveAnnouncer.$inject = ["$timeout"]; +angular + .module('material.core') + .service('$mdLiveAnnouncer', MdLiveAnnouncer); + +/** + * @ngdoc service + * @name $mdLiveAnnouncer + * @module material.core.liveannouncer + * + * @description + * + * Service to announce messages to supported screenreaders. + * + * > The `$mdLiveAnnouncer` service is internally used for components to provide proper accessibility. + * + * + * module.controller('AppCtrl', function($mdLiveAnnouncer) { + * // Basic announcement (Polite Mode) + * $mdLiveAnnouncer.announce('Hey Google'); + * + * // Custom announcement (Assertive Mode) + * $mdLiveAnnouncer.announce('Hey Google', 'assertive'); + * }); + * + * + */ +function MdLiveAnnouncer($timeout) { + /** @private @const @type {!angular.$timeout} */ + this._$timeout = $timeout; + + /** @private @const @type {!HTMLElement} */ + this._liveElement = this._createLiveElement(); + + /** @private @const @type {!number} */ + this._announceTimeout = 100; +} + +/** + * @ngdoc method + * @name $mdLiveAnnouncer#announce + * @description Announces messages to supported screenreaders. + * @param {string} message Message to be announced to the screenreader + * @param {'off'|'polite'|'assertive'} politeness The politeness of the announcer element. + */ +MdLiveAnnouncer.prototype.announce = function(message, politeness) { + if (!politeness) { + politeness = 'polite'; + } + + var self = this; + + self._liveElement.textContent = ''; + self._liveElement.setAttribute('aria-live', politeness); + + // This 100ms timeout is necessary for some browser + screen-reader combinations: + // - Both JAWS and NVDA over IE11 will not announce anything without a non-zero timeout. + // - With Chrome and IE11 with NVDA or JAWS, a repeated (identical) message won't be read a + // second time without clearing and then using a non-zero delay. + // (using JAWS 17 at time of this writing). + self._$timeout(function() { + self._liveElement.textContent = message; + }, self._announceTimeout, false); +}; + +/** + * Creates a live announcer element, which listens for DOM changes and announces them + * to the screenreaders. + * @returns {!HTMLElement} + * @private + */ +MdLiveAnnouncer.prototype._createLiveElement = function() { + var liveEl = document.createElement('div'); + + liveEl.classList.add('md-visually-hidden'); + liveEl.setAttribute('role', 'status'); + liveEl.setAttribute('aria-atomic', 'true'); + liveEl.setAttribute('aria-live', 'polite'); + + document.body.appendChild(liveEl); + + return liveEl; +}; - /* - * @private - * @ngdoc factory - * @name ComponentRegistry - * @module material.core.componentRegistry - * - */ - function ComponentRegistry($log, $q) { +})(); +(function(){ +"use strict"; - var self; - var instances = [ ]; - var pendings = { }; +/** + * @ngdoc service + * @name $$mdMeta + * @module material.core.meta + * + * @description + * + * A provider and a service that simplifies meta tags access + * + * Note: This is intended only for use with dynamic meta tags such as browser color and title. + * Tags that are only processed when the page is rendered (such as `charset`, and `http-equiv`) + * will not work since `$$mdMeta` adds the tags after the page has already been loaded. + * + * ```js + * app.config(function($$mdMetaProvider) { + * var removeMeta = $$mdMetaProvider.setMeta('meta-name', 'content'); + * var metaValue = $$mdMetaProvider.getMeta('meta-name'); // -> 'content' + * + * removeMeta(); + * }); + * + * app.controller('myController', function($$mdMeta) { + * var removeMeta = $$mdMeta.setMeta('meta-name', 'content'); + * var metaValue = $$mdMeta.getMeta('meta-name'); // -> 'content' + * + * removeMeta(); + * }); + * ``` + * + * @returns {$$mdMeta.$service} + * + */ +angular.module('material.core.meta', []) + .provider('$$mdMeta', function () { + var head = angular.element(document.head); + var metaElements = {}; - 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. + /** + * Checks if the requested element was written manually and maps it + * + * @param {string} name meta tag 'name' attribute value + * @returns {boolean} returns true if there is an element with the requested name + */ + function mapExistingElement(name) { + if (metaElements[name]) { + return true; + } + + var element = document.getElementsByName(name)[0]; + + if (!element) { + return false; + } + + metaElements[name] = angular.element(element); + + return true; + } + + /** + * @ngdoc method + * @name $$mdMeta#setMeta + * + * @description + * Creates meta element with the 'name' and 'content' attributes, + * if the meta tag is already created than we replace the 'content' value + * + * @param {string} name meta tag 'name' attribute value + * @param {string} content meta tag 'content' attribute value + * @returns {function} remove function + * + */ + function setMeta(name, content) { + mapExistingElement(name); + + if (!metaElements[name]) { + var newMeta = angular.element(''); + head.append(newMeta); + metaElements[name] = newMeta; + } + else { + metaElements[name].attr('content', content); + } + + return function () { + metaElements[name].attr('content', ''); + metaElements[name].remove(); + delete metaElements[name]; + }; + } + + /** + * @ngdoc method + * @name $$mdMeta#getMeta + * + * @description + * Gets the 'content' attribute value of the wanted meta element + * + * @param {string} name meta tag 'name' attribute value + * @returns {string} content attribute value + */ + function getMeta(name) { + if (!mapExistingElement(name)) { + throw Error('$$mdMeta: could not find a meta tag with the name \'' + name + '\''); + } + + return metaElements[name].attr('content'); + } + + var module = { + setMeta: setMeta, + getMeta: getMeta + }; + + return angular.extend({}, module, { + $get: function () { + return module; + } + }); + }); +})(); +(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. + */ + ComponentRegistry.$inject = ["$log", "$q"]; + 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, msgContext) { + $log.error( (msgContext || "") + 'No instance found for handle', handle); + }, + /** + * Return all registered instances as an array. */ getInstances: function() { return instances; @@ -3672,7 +4932,9 @@ function InterimElementProvider() { function resolveWhen() { var dfd = pendings[handle]; if ( dfd ) { - dfd.resolve( instance ); + dfd.forEach(function (promise) { + promise.resolve(instance); + }); delete pendings[handle]; } } @@ -3691,7 +4953,10 @@ function InterimElementProvider() { if ( instance ) { deferred.resolve( instance ); } else { - pendings[handle] = deferred; + if (pendings[handle] === undefined) { + pendings[handle] = []; + } + pendings[handle].push(deferred); } return deferred.promise; @@ -3706,7 +4971,6 @@ function InterimElementProvider() { } } - ComponentRegistry.$inject = ["$log", "$q"]; })(); (function(){ @@ -3728,6 +4992,7 @@ function InterimElementProvider() { * @param {object=} options (Optional) Configuration options to override the default ripple configuration */ + MdButtonInkRipple.$inject = ["$mdInkRipple"]; angular.module('material.core') .factory('$mdButtonInkRipple', MdButtonInkRipple); @@ -3751,11 +5016,10 @@ function InterimElementProvider() { return { isMenuItem: element.hasClass('md-menu-item'), dimBackground: true - } + }; } - }; + } } - MdButtonInkRipple.$inject = ["$mdInkRipple"];; })(); })(); @@ -3778,6 +5042,7 @@ function InterimElementProvider() { * @param {object=} options (Optional) Configuration options to override the defaultripple configuration */ + MdCheckboxInkRipple.$inject = ["$mdInkRipple"]; angular.module('material.core') .factory('$mdCheckboxInkRipple', MdCheckboxInkRipple); @@ -3792,9 +5057,8 @@ function InterimElementProvider() { dimBackground: false, fitRipple: true }, options)); - }; + } } - MdCheckboxInkRipple.$inject = ["$mdInkRipple"];; })(); })(); @@ -3817,6 +5081,7 @@ function InterimElementProvider() { * @param {object=} options (Optional) Configuration options to override the defaultripple configuration */ + MdListInkRipple.$inject = ["$mdInkRipple"]; angular.module('material.core') .factory('$mdListInkRipple', MdListInkRipple); @@ -3832,9 +5097,8 @@ function InterimElementProvider() { outline: false, rippleSize: 'full' }, options)); - }; + } } - MdListInkRipple.$inject = ["$mdInkRipple"];; })(); })(); @@ -3847,8 +5111,10 @@ function InterimElementProvider() { * @description * Ripple */ +InkRippleCtrl.$inject = ["$scope", "$element", "rippleOptions", "$window", "$timeout", "$mdUtil", "$mdColorUtil"]; +InkRippleDirective.$inject = ["$mdButtonInkRipple", "$mdCheckboxInkRipple"]; angular.module('material.core') - .factory('$mdInkRipple', InkRippleService) + .provider('$mdInkRipple', InkRippleProvider) .directive('mdInkRipple', InkRippleDirective) .directive('mdNoInk', attrNoDirective) .directive('mdNoBar', attrNoDirective) @@ -3899,7 +5165,6 @@ function InkRippleDirective ($mdButtonInkRipple, $mdCheckboxInkRipple) { } }; } -InkRippleDirective.$inject = ["$mdButtonInkRipple", "$mdCheckboxInkRipple"]; /** * @ngdoc service @@ -3928,44 +5193,72 @@ InkRippleDirective.$inject = ["$mdButtonInkRipple", "$mdCheckboxInkRipple"]; * } * }); * + * + * ### Disabling ripples globally + * If you want to disable ink ripples globally, for all components, you can call the + * `disableInkRipple` method in your app's config. + * + * + * app.config(function ($mdInkRippleProvider) { + * $mdInkRippleProvider.disableInkRipple(); + * }); */ -/** - * @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 - }); +function InkRippleProvider () { + var isDisabledGlobally = false; + + return { + disableInkRipple: disableInkRipple, + $get: ["$injector", function($injector) { + return { attach: attach }; + + /** + * @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 attach (scope, element, options) { + if (isDisabledGlobally || element.controller('mdNoInk')) return angular.noop; + return $injector.instantiate(InkRippleCtrl, { + $scope: scope, + $element: element, + rippleOptions: options + }); + } + }] + }; + + /** + * @ngdoc method + * @name $mdInkRipple#disableInkRipple + * + * @description + * A config-time method that, when called, disables ripples globally. + */ + function disableInkRipple () { + isDisabledGlobally = true; } } -InkRippleService.$inject = ["$injector"]; /** * Controller used by the ripple service in order to apply ripples * @ngInject */ -function InkRippleCtrl ($scope, $element, rippleOptions, $window, $timeout, $mdUtil) { +function InkRippleCtrl ($scope, $element, rippleOptions, $window, $timeout, $mdUtil, $mdColorUtil) { this.$window = $window; this.$timeout = $timeout; this.$mdUtil = $mdUtil; + this.$mdColorUtil = $mdColorUtil; this.$scope = $scope; this.$element = $element; this.options = rippleOptions; @@ -3984,7 +5277,6 @@ function InkRippleCtrl ($scope, $element, rippleOptions, $window, $timeout, $mdU this.bindEvents(); } -InkRippleCtrl.$inject = ["$scope", "$element", "rippleOptions", "$window", "$timeout", "$mdUtil"]; /** @@ -4046,39 +5338,12 @@ InkRippleCtrl.prototype.calculateColor = function () { InkRippleCtrl.prototype._parseColor = function parseColor (color, multiplier) { multiplier = multiplier || 1; + var colorUtil = this.$mdColorUtil; 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('); - } + if (color.indexOf('rgb') === 0) return colorUtil.rgbToRgba(color); + if (color.indexOf('#') === 0) return colorUtil.hexToRgba(color); }; @@ -4202,6 +5467,7 @@ InkRippleCtrl.prototype.createRipple = function (left, top) { if (!this.isRippleAllowed()) return; var ctrl = this; + var colorUtil = ctrl.$mdColorUtil; var ripple = angular.element('
'); var width = this.$element.prop('clientWidth'); var height = this.$element.prop('clientHeight'); @@ -4216,8 +5482,8 @@ InkRippleCtrl.prototype.createRipple = function (left, top) { background: 'black', width: size + 'px', height: size + 'px', - backgroundColor: rgbaToRGB(color), - borderColor: rgbaToRGB(color) + backgroundColor: colorUtil.rgbaToRgb(color), + borderColor: colorUtil.rgbaToRgb(color) }); this.lastRipple = ripple; @@ -4242,12 +5508,6 @@ InkRippleCtrl.prototype.createRipple = function (left, top) { }, 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) @@ -4281,6 +5541,7 @@ InkRippleCtrl.prototype.removeRipple = function (ripple) { if (index < 0) return; this.ripples.splice(this.ripples.indexOf(ripple), 1); ripple.removeClass('md-ripple-active'); + ripple.addClass('md-ripple-remove'); 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 @@ -4328,6 +5589,7 @@ function attrNoDirective () { * @param {object=} options (Optional) Configuration options to override the defaultripple configuration */ + MdTabInkRipple.$inject = ["$mdInkRipple"]; angular.module('material.core') .factory('$mdTabInkRipple', MdTabInkRipple); @@ -4343,9 +5605,8 @@ function attrNoDirective () { outline: false, rippleSize: 'full' }, options)); - }; + } } - MdTabInkRipple.$inject = ["$mdInkRipple"];; })(); })(); @@ -4670,7 +5931,7 @@ angular.module('material.core.theming.palette', []) 'A400': '#8d6e63', 'A700': '#5d4037', 'contrastDefaultColor': 'light', - 'contrastDarkColors': '50 100 200', + 'contrastDarkColors': '50 100 200 A100 A200', 'contrastStrongLightColors': '300 400' }, 'grey': { @@ -4684,13 +5945,12 @@ angular.module('material.core.theming.palette', []) '700': '#616161', '800': '#424242', '900': '#212121', - '1000': '#000000', 'A100': '#ffffff', - 'A200': '#eeeeee', + 'A200': '#000000', 'A400': '#303030', 'A700': '#616161', 'contrastDefaultColor': 'dark', - 'contrastLightColors': '600 700 800 900' + 'contrastLightColors': '600 700 800 900 A200 A400 A700' }, 'blue-grey': { '50': '#eceff1', @@ -4708,7 +5968,7 @@ angular.module('material.core.theming.palette', []) 'A400': '#78909c', 'A700': '#455a64', 'contrastDefaultColor': 'light', - 'contrastDarkColors': '50 100 200 300 700', + 'contrastDarkColors': '50 100 200 300 A100 A200', 'contrastStrongLightColors': '400 500 700' } }); @@ -4717,25 +5977,141 @@ angular.module('material.core.theming.palette', []) (function(){ "use strict"; -angular.module('material.core.theming', ['material.core.theming.palette']) +(function(angular) { + 'use strict'; +/** + * @ngdoc module + * @name material.core.theming + * @description + * Theming + */ +detectDisabledThemes.$inject = ["$mdThemingProvider"]; +ThemingDirective.$inject = ["$mdTheming", "$interpolate", "$parse", "$mdUtil", "$q", "$log"]; +ThemableDirective.$inject = ["$mdTheming"]; +ThemingProvider.$inject = ["$mdColorPalette", "$$mdMetaProvider"]; +generateAllThemes.$inject = ["$injector", "$mdTheming"]; +angular.module('material.core.theming', ['material.core.theming.palette', 'material.core.meta']) .directive('mdTheme', ThemingDirective) .directive('mdThemable', ThemableDirective) + .directive('mdThemesDisabled', disableThemesDirective ) .provider('$mdTheming', ThemingProvider) + .config( detectDisabledThemes ) .run(generateAllThemes); +/** + * Detect if the HTML or the BODY tags has a [md-themes-disabled] attribute + * If yes, then immediately disable all theme stylesheet generation and DOM injection + */ +/** + * @ngInject + */ +function detectDisabledThemes($mdThemingProvider) { + var isDisabled = !!document.querySelector('[md-themes-disabled]'); + $mdThemingProvider.disableTheming(isDisabled); +} + /** * @ngdoc service * @name $mdThemingProvider * @module material.core.theming * * @description Provider to configure the `$mdTheming` service. + * + * ### Default Theme + * The `$mdThemingProvider` uses by default the following theme configuration: + * + * - Primary Palette: `Blue` + * - Accent Palette: `Pink` + * - Warn Palette: `Deep-Orange` + * - Background Palette: `Grey` + * + * If you don't want to use the `md-theme` directive on the elements itself, you may want to overwrite + * the default theme.
+ * This can be done by using the following markup. + * + * + * myAppModule.config(function($mdThemingProvider) { + * $mdThemingProvider + * .theme('default') + * .primaryPalette('blue') + * .accentPalette('teal') + * .warnPalette('red') + * .backgroundPalette('grey'); + * }); + * + * + + * ### Dynamic Themes + * + * By default, if you change a theme at runtime, the `$mdTheming` service will not detect those changes.
+ * If you have an application, which changes its theme on runtime, you have to enable theme watching. + * + * + * myAppModule.config(function($mdThemingProvider) { + * // Enable theme watching. + * $mdThemingProvider.alwaysWatchTheme(true); + * }); + * + * + * ### Custom Theme Styles + * + * Sometimes you may want to use your own theme styles for some custom components.
+ * You are able to register your own styles by using the following markup. + * + * + * myAppModule.config(function($mdThemingProvider) { + * // Register our custom stylesheet into the theming provider. + * $mdThemingProvider.registerStyles(STYLESHEET); + * }); + * + * + * The `registerStyles` method only accepts strings as value, so you're actually not able to load an external + * stylesheet file into the `$mdThemingProvider`. + * + * If it's necessary to load an external stylesheet, we suggest using a bundler, which supports including raw content, + * like [raw-loader](https://github.com/webpack/raw-loader) for `webpack`. + * + * + * myAppModule.config(function($mdThemingProvider) { + * // Register your custom stylesheet into the theming provider. + * $mdThemingProvider.registerStyles(require('../styles/my-component.theme.css')); + * }); + * + * + * ### Browser color + * + * Enables browser header coloring + * for more info please visit: + * https://developers.google.com/web/fundamentals/design-and-ui/browser-customization/theme-color + * + * Options parameter:
+ * `theme` - A defined theme via `$mdThemeProvider` to use the palettes from. Default is `default` theme.
+ * `palette` - Can be any one of the basic material design palettes, extended defined palettes and 'primary', + * 'accent', 'background' and 'warn'. Default is `primary`.
+ * `hue` - The hue from the selected palette. Default is `800`
+ * + * + * myAppModule.config(function($mdThemingProvider) { + * // Enable browser color + * $mdThemingProvider.enableBrowserColor({ + * theme: 'myTheme', // Default is 'default' + * palette: 'accent', // Default is 'primary', any basic material palette and extended palettes are available + * hue: '200' // Default is '800' + * }); + * }); + * */ +/** + * @ngdoc method + * @name $mdThemingProvider#registerStyles + * @param {string} styles The styles to be appended to Angular Material's built in theme css. + */ /** * @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. + * Setting a value allows the use of CSP policy without using the unsafe-inline directive. */ /** @@ -4751,6 +6127,17 @@ angular.module('material.core.theming', ['material.core.theming.palette']) * classes when they change. Default is `false`. Enabling can reduce performance. */ +/** + * @ngdoc method + * @name $mdThemingProvider#enableBrowserColor + * @param {Object=} options Options object for the browser color
+ * `theme` - A defined theme via `$mdThemeProvider` to use the palettes from. Default is `default` theme.
+ * `palette` - Can be any one of the basic material design palettes, extended defined palettes and 'primary', + * 'accent', 'background' and 'warn'. Default is `primary`.
+ * `hue` - The hue from the selected palette. Default is `800`
+ * @returns {Function} remove function of the browser color + */ + /* Some Example Valid Theming Expressions * ======================================= * @@ -4780,20 +6167,21 @@ var GENERATED = { }; // In memory storage of defined themes and color palettes (both loaded by CSS, and user specified) var PALETTES; -var THEMES; +// Text Colors on light and dark backgrounds +// @see https://www.google.com/design/spec/style/color.html#color-text-background-colors 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)', + '3': 'rgba(0,0,0,0.38)', '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)', + '3': 'rgba(255,255,255,0.5)', '4': 'rgba(255,255,255,0.12)' }; @@ -4828,7 +6216,7 @@ var DARK_DEFAULT_HUES = { 'default': 'A400', 'hue-1': '800', 'hue-2': '900', - 'hue-3': '1000' + 'hue-3': 'A200' } }; THEME_COLOR_TYPES.forEach(function(colorType) { @@ -4848,43 +6236,125 @@ var VALID_HUE_VALUES = [ '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; +var themeConfig = { + disableTheming : false, // Generate our themes at run time; also disable stylesheet DOM injection + generateOnDemand : false, // Whether or not themes are to be generated on-demand (vs. eagerly). + registeredStyles : [], // Custom styles registered to be used in the theming of custom components. + nonce : null // Nonce to be added as an attribute to the generated themes style tags. +}; -function ThemingProvider($mdColorPalette) { +/** + * + */ +function ThemingProvider($mdColorPalette, $$mdMetaProvider) { + ThemingService.$inject = ["$rootScope", "$mdUtil", "$q", "$log"]; PALETTES = { }; - THEMES = { }; + var THEMES = { }; var themingProvider; - var defaultTheme = 'default'; + var alwaysWatchTheme = false; + var defaultTheme = 'default'; // Load JS Defined Palettes angular.extend(PALETTES, $mdColorPalette); // Default theme defined in core.js - ThemingService.$inject = ["$rootScope", "$log"]; + /** + * Adds `theme-color` and `msapplication-navbutton-color` meta tags with the color parameter + * @param {string} color Hex value of the wanted browser color + * @returns {Function} Remove function of the meta tags + */ + var setBrowserColor = function (color) { + // Chrome, Firefox OS and Opera + var removeChrome = $$mdMetaProvider.setMeta('theme-color', color); + // Windows Phone + var removeWindows = $$mdMetaProvider.setMeta('msapplication-navbutton-color', color); + + return function () { + removeChrome(); + removeWindows(); + }; + }; + + /** + * Enables browser header coloring + * for more info please visit: + * https://developers.google.com/web/fundamentals/design-and-ui/browser-customization/theme-color + * + * The default color is `800` from `primary` palette of the `default` theme + * + * options are: + * `theme` - A defined theme via `$mdThemeProvider` to use the palettes from. Default is `default` theme + * `palette` - Can be any one of the basic material design palettes, extended defined palettes and 'primary', + * 'accent', 'background' and 'warn'. Default is `primary` + * `hue` - The hue from the selected palette. Default is `800` + * + * @param {Object=} options Options object for the browser color + * @returns {Function} remove function of the browser color + */ + var enableBrowserColor = function (options) { + options = angular.isObject(options) ? options : {}; + + var theme = options.theme || 'default'; + var hue = options.hue || '800'; + + var palette = PALETTES[options.palette] || + PALETTES[THEMES[theme].colors[options.palette || 'primary'].name]; + + var color = angular.isObject(palette[hue]) ? palette[hue].hex : palette[hue]; + + return setBrowserColor(color); + }; + return themingProvider = { definePalette: definePalette, extendPalette: extendPalette, theme: registerTheme, + /** + * return a read-only clone of the current theme configuration + */ + configuration : function() { + return angular.extend( { }, themeConfig, { + defaultTheme : defaultTheme, + alwaysWatchTheme : alwaysWatchTheme, + registeredStyles : [].concat(themeConfig.registeredStyles) + }); + }, + + /** + * Easy way to disable theming without having to use + * `.constant("$MD_THEME_CSS","");` This disables + * all dynamic theme style sheet generations and injections... + */ + disableTheming: function(isDisabled) { + themeConfig.disableTheming = angular.isUndefined(isDisabled) || !!isDisabled; + }, + + registerStyles: function(styles) { + themeConfig.registeredStyles.push(styles); + }, + setNonce: function(nonceValue) { - nonce = nonceValue; + themeConfig.nonce = nonceValue; + }, + + generateThemesOnDemand: function(onDemand) { + themeConfig.generateOnDemand = onDemand; }, + setDefaultTheme: function(theme) { defaultTheme = theme; }, + alwaysWatchTheme: function(alwaysWatch) { alwaysWatchTheme = alwaysWatch; }, - generateThemesOnDemand: function(onDemand) { - generateOnDemand = onDemand; - }, + + enableBrowserColor: enableBrowserColor, + $get: ThemingService, _LIGHT_DEFAULT_HUES: LIGHT_DEFAULT_HUES, _DARK_DEFAULT_HUES: DARK_DEFAULT_HUES, @@ -5035,12 +6505,13 @@ function ThemingProvider($mdColorPalette) { /** * @ngdoc service * @name $mdTheming + * @module material.core.theming * * @description * - * Service that makes an element apply theming related classes to itself. + * Service that makes an element apply theming related classes to itself. * - * ```js + * * app.directive('myFancyDirective', function($mdTheming) { * return { * restrict: 'e', @@ -5049,32 +6520,185 @@ function ThemingProvider($mdColorPalette) { * } * }; * }); - * ``` - * @param {el=} element to apply theming to + * + * @param {element=} element to apply theming to */ - /* @ngInject */ - function ThemingService($rootScope, $log) { - applyTheme.inherit = function(el, parent) { - var ctrl = parent.controller('mdTheme'); + /** + * @ngdoc property + * @name $mdTheming#THEMES + * @description + * Property to get all the themes defined + * @returns {Object} All the themes defined with their properties + */ - 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); - } + /** + * @ngdoc property + * @name $mdTheming#PALETTES + * @description + * Property to get all the palettes defined + * @returns {Object} All the palettes defined with their colors + */ - function changeTheme(theme) { - if (!theme) return; + /** + * @ngdoc method + * @name $mdTheming#registered + * @description + * Determine is specified theme name is a valid, registered theme + * @param {string} themeName the theme to check if registered + * @returns {boolean} whether the theme is registered or not + */ + + /** + * @ngdoc method + * @name $mdTheming#defaultTheme + * @description + * Returns the default theme + * @returns {string} The default theme + */ + + /** + * @ngdoc method + * @name $mdTheming#generateTheme + * @description + * Lazy generate themes - by default, every theme is generated when defined. + * You can disable this in the configuration section using the + * `$mdThemingProvider.generateThemesOnDemand(true);` + * + * The theme name that is passed in must match the name of the theme that was defined as part of the configuration block. + * + * @param name {string} theme name to generate + */ + + /** + * @ngdoc method + * @name $mdTheming#setBrowserColor + * @description + * Sets browser header coloring + * for more info please visit: + * https://developers.google.com/web/fundamentals/design-and-ui/browser-customization/theme-color + * + * The default color is `800` from `primary` palette of the `default` theme + * + * options are:
+ * `theme` - A defined theme via `$mdThemeProvider` to use the palettes from. Default is `default` theme.
+ * `palette` - Can be any one of the basic material design palettes, extended defined palettes and 'primary', + * 'accent', 'background' and 'warn'. Default is `primary`
+ * `hue` - The hue from the selected palette. Default is `800` + * + * @param {Object} options Options object for the browser color + * @returns {Function} remove function of the browser color + */ + + /* @ngInject */ + function ThemingService($rootScope, $mdUtil, $q, $log) { + // Allow us to be invoked via a linking function signature. + var applyTheme = function (scope, el) { + if (el === undefined) { el = scope; scope = undefined; } + if (scope === undefined) { scope = $rootScope; } + applyTheme.inherit(el, el); + }; + + Object.defineProperty(applyTheme, 'THEMES', { + get: function () { + return angular.extend({}, THEMES); + } + }); + Object.defineProperty(applyTheme, 'PALETTES', { + get: function () { + return angular.extend({}, PALETTES); + } + }); + Object.defineProperty(applyTheme, 'ALWAYS_WATCH', { + get: function () { + return alwaysWatchTheme; + } + }); + applyTheme.inherit = inheritTheme; + applyTheme.registered = registered; + applyTheme.defaultTheme = function() { return defaultTheme; }; + applyTheme.generateTheme = function(name) { generateTheme(THEMES[name], name, themeConfig.nonce); }; + applyTheme.defineTheme = function(name, options) { + options = options || {}; + + var theme = registerTheme(name); + + if (options.primary) { + theme.primaryPalette(options.primary); + } + if (options.accent) { + theme.accentPalette(options.accent); + } + if (options.warn) { + theme.warnPalette(options.warn); + } + if (options.background) { + theme.backgroundPalette(options.background); + } + if (options.dark){ + theme.dark(); + } + + this.generateTheme(name); + + return $q.resolve(name); + }; + applyTheme.setBrowserColor = enableBrowserColor; + + return applyTheme; + + /** + * Determine is specified theme name is a valid, registered theme + */ + function registered(themeName) { + if (themeName === undefined || themeName === '') return true; + return applyTheme.THEMES[themeName] !== undefined; + } + + /** + * Get theme name for the element, then update with Theme CSS class + */ + function inheritTheme (el, parent) { + var ctrl = parent.controller('mdTheme') || el.data('$mdThemeController'); + + updateThemeClass(lookupThemeName()); + + if (ctrl) { + var watchTheme = alwaysWatchTheme || + ctrl.$shouldWatch || + $mdUtil.parseAttributeBoolean(el.attr('md-theme-watch')); + + var unwatch = ctrl.registerChanges(function (name) { + updateThemeClass(name); + + if (!watchTheme) { + unwatch(); + } + else { + el.on('$destroy', unwatch); + } + }); + } + + /** + * Find the theme name from the parent controller or element data + */ + function lookupThemeName() { + // As a few components (dialog) add their controllers later, we should also watch for a controller init. + return ctrl && ctrl.$mdTheme || (defaultTheme == 'default' ? '' : defaultTheme); + } + + /** + * Remove old theme class and apply a new one + * NOTE: if not a valid theme name, then the current name is not changed + */ + function updateThemeClass(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'); @@ -5083,61 +6707,135 @@ function ThemingProvider($mdColorPalette) { 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) { +function ThemingDirective($mdTheming, $interpolate, $parse, $mdUtil, $q, $log) { return { - priority: 100, + priority: 101, // has to be more than 100 to be before interpolation (issue on IE) link: { pre: function(scope, el, attrs) { + var registeredCallbacks = []; + + var startSymbol = $interpolate.startSymbol(); + var endSymbol = $interpolate.endSymbol(); + + var theme = attrs.mdTheme.trim(); + + var hasInterpolation = + theme.substr(0, startSymbol.length) === startSymbol && + theme.lastIndexOf(endSymbol) === theme.length - endSymbol.length; + + var oneTimeOperator = '::'; + var oneTimeBind = attrs.mdTheme + .split(startSymbol).join('') + .split(endSymbol).join('') + .trim() + .substr(0, oneTimeOperator.length) === oneTimeOperator; + var ctrl = { - $setTheme: function(theme) { + registerChanges: function (cb, context) { + if (context) { + cb = angular.bind(context, cb); + } + + registeredCallbacks.push(cb); + + return function () { + var index = registeredCallbacks.indexOf(cb); + + if (index > -1) { + registeredCallbacks.splice(index, 1); + } + }; + }, + $setTheme: function (theme) { if (!$mdTheming.registered(theme)) { $log.warn('attempted to use unregistered theme \'' + theme + '\''); } + ctrl.$mdTheme = theme; - } + + // Iterating backwards to support unregistering during iteration + // http://stackoverflow.com/a/9882349/890293 + // we don't use `reverse()` of array because it mutates the array and we don't want it to get re-indexed + for (var i = registeredCallbacks.length; i--;) { + registeredCallbacks[i](theme); + } + }, + $shouldWatch: $mdUtil.parseAttributeBoolean(el.attr('md-theme-watch')) || + $mdTheming.ALWAYS_WATCH || + (hasInterpolation && !oneTimeBind) }; + el.data('$mdThemeController', ctrl); - ctrl.$setTheme($interpolate(attrs.mdTheme)(scope)); - attrs.$observe('mdTheme', ctrl.$setTheme); + + var getTheme = function () { + var interpolation = $interpolate(attrs.mdTheme)(scope); + return $parse(interpolation)(scope) || interpolation; + }; + + var setParsedTheme = function (theme) { + if (typeof theme === 'string') { + return ctrl.$setTheme(theme); + } + + $q.when( angular.isFunction(theme) ? theme() : theme ) + .then(function(name){ + ctrl.$setTheme(name); + }); + }; + + setParsedTheme(getTheme()); + + var unwatch = scope.$watch(getTheme, function(theme) { + if (theme) { + setParsedTheme(theme); + + if (!ctrl.$shouldWatch) { + unwatch(); + } + } + }); } } }; } -ThemingDirective.$inject = ["$mdTheming", "$interpolate", "$log"]; + +/** + * Special directive that will disable ALL runtime Theme style generation and DOM injection + * + * + * + * + * + * ... + * + * + * Note: Using md-themes-css directive requires the developer to load external + * theme stylesheets; e.g. custom themes from Material-Tools: + * + * `angular-material.themes.css` + * + * Another option is to use the ThemingProvider to configure and disable the attribute + * conversions; this would obviate the use of the `md-themes-css` directive + * + */ +function disableThemesDirective() { + themeConfig.disableTheming = true; + + // Return a 1x-only, first-match attribute directive + return { + restrict : 'A', + priority : '900' + }; +} function ThemableDirective($mdTheming) { return $mdTheming; } -ThemableDirective.$inject = ["$mdTheming"]; function parseRules(theme, colorType, rules) { checkValidPalette(theme, colorType); @@ -5146,10 +6844,10 @@ function parseRules(theme, colorType, rules) { var generatedRules = []; var color = theme.colors[colorType]; - var themeNameRegex = new RegExp('.md-' + theme.name + '-theme', 'g'); + 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 simpleVariableRegex = /'?"?\{\{\s*([a-zA-Z]+)-(A?\d+|hue\-[0-3]|shadow|default)-?(\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 @@ -5163,9 +6861,13 @@ function parseRules(theme, colorType, rules) { return theme.foregroundPalette[hue] || theme.foregroundPalette['1']; } } - if (hue.indexOf('hue') === 0) { + + // `default` is also accepted as a hue-value, because the background palettes are + // using it as a name for the default hue. + if (hue.indexOf('hue') === 0 || hue === 'default') { hue = theme.colors[colorType].hues[hue]; } + return rgba( (PALETTES[ theme.colors[colorType].name ][hue] || '')[contrast ? 'contrast' : 'value'], opacity ); }); @@ -5182,9 +6884,10 @@ function parseRules(theme, colorType, rules) { // 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; + var themeRuleRegex = /((?:\s|>|\.|\w|-|:|\(|\)|\[|\]|"|'|=)*)\.md-default-theme((?:\s|>|\.|\w|-|:|\(|\)|\[|\]|"|'|=)*)/g; + + newRule = newRule.replace(themeRuleRegex, function(match, start, end) { + return match + ', ' + start + end; }); } generatedRules.push(newRule); @@ -5196,10 +6899,13 @@ function parseRules(theme, colorType, rules) { var rulesByType = {}; // Generate our themes at run time given the state of THEMES and PALETTES -function generateAllThemes($injector) { +function generateAllThemes($injector, $mdTheming) { var head = document.head; var firstChild = head ? head.firstElementChild : null; - var themeCss = $injector.has('$MD_THEME_CSS') ? $injector.get('$MD_THEME_CSS') : ''; + var themeCss = !themeConfig.disableTheming && $injector.has('$MD_THEME_CSS') ? $injector.get('$MD_THEME_CSS') : ''; + + // Append our custom registered styles to the theme stylesheet. + themeCss += themeConfig.registeredStyles.join(''); if ( !firstChild ) return; if (themeCss.length === 0) return; // no rules, so no point in running this expensive task @@ -5213,7 +6919,7 @@ function generateAllThemes($injector) { // Break the CSS into individual rules var rules = themeCss .split(/\}(?!(\}|'|"|;))/) - .filter(function(rule) { return rule && rule.length; }) + .filter(function(rule) { return rule && rule.trim().length; }) .map(function(rule) { return rule.trim() + '}'; }); @@ -5248,11 +6954,11 @@ function generateAllThemes($injector) { // 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; + if (themeConfig.generateOnDemand) return; - angular.forEach(THEMES, function(theme) { - if (!GENERATED[theme.name]) { - generateTheme(theme.name, nonce); + angular.forEach($mdTheming.THEMES, function(theme) { + if (!GENERATED[theme.name] && !($mdTheming.defaultTheme() !== 'default' && theme.name === 'default')) { + generateTheme(theme, theme.name, themeConfig.nonce); } }); @@ -5263,7 +6969,7 @@ function generateAllThemes($injector) { // 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) { + function sanitizePalette(palette, name) { var defaultContrast = palette.contrastDefaultColor; var lightColors = palette.contrastLightColors || []; var strongLightColors = palette.contrastStrongLightColors || []; @@ -5293,6 +6999,7 @@ function generateAllThemes($injector) { } palette[hueName] = { + hex: palette[hueName], value: rgbValue, contrast: getContrastColor() }; @@ -5316,10 +7023,8 @@ function generateAllThemes($injector) { }); } } -generateAllThemes.$inject = ["$injector"]; -function generateTheme(name, nonce) { - var theme = THEMES[name]; +function generateTheme(theme, name, nonce) { var head = document.head; var firstChild = head ? head.firstElementChild : null; @@ -5342,12 +7047,6 @@ function generateTheme(name, nonce) { } }); - - 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; } @@ -5400,6 +7099,9 @@ function rgba(rgbArray, opacity) { 'rgb(' + rgbArray.join(',') + ')'; } + +})(window.angular); + })(); (function(){ "use strict"; @@ -5427,10 +7129,12 @@ function AnimateDomUtils($mdUtil, $q, $timeout, $mdConstant, $animateCss) { * */ translate3d : function( target, from, to, options ) { - return $animateCss(target,{ - from:from, - to:to, - addClass:options.transitionInClass + return $animateCss(target, { + from: from, + to: to, + addClass: options.transitionInClass, + removeClass: options.transitionOutClass, + duration: options.duration }) .start() .then(function(){ @@ -5445,77 +7149,83 @@ function AnimateDomUtils($mdUtil, $q, $timeout, $mdConstant, $animateCss) { return $animateCss(target, { to: newFrom || from, addClass: options.transitionOutClass, - removeClass: options.transitionInClass + removeClass: options.transitionInClass, + duration: options.duration }).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 + var TIMEOUT = 3000; // fallback is 3 secs - return $q(function(resolve, reject){ - opts = opts || { }; + return $q(function(resolve, reject){ + opts = opts || { }; - var timer = $timeout(finished, opts.timeout || TIMEOUT); - element.on($mdConstant.CSS.TRANSITIONEND, finished); + // If there is no transition is found, resolve immediately + // + // NOTE: using $mdUtil.nextTick() causes delays/issues + if (noTransitionFound(opts.cachedTransitionStyles)) { + TIMEOUT = 0; + } - /** - * 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; + var timer = $timeout(finished, opts.timeout || TIMEOUT); + element.on($mdConstant.CSS.TRANSITIONEND, finished); - if ( ev ) $timeout.cancel(timer); - element.off($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; - // Never reject since ngAnimate may cause timeouts due missed transitionEnd events - resolve(); + 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) { + /** + * Checks whether or not there is a transition. + * + * @param styles The cached styles to use for the calculation. If null, getComputedStyle() + * will be used. + * + * @returns {boolean} True if there is no transition/duration; false otherwise. + */ + function noTransitionFound(styles) { + styles = styles || window.getComputedStyle(element[0]); + + return styles.transitionDuration == '0s' || (!styles.transition && !styles.transitionProperty); + } + + }); + }, + + calculateTransformValues: 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({ + return { 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 - }); + 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; + return {centerX: 0, centerY: 0, scaleX: 0.5, scaleY: 0.5}; /** * This is a fallback if the origin information is no longer valid, then the @@ -5529,6 +7239,33 @@ function AnimateDomUtils($mdUtil, $q, $timeout, $mdConstant, $animateCss) { } }, + /** + * 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 zoomTemplate = "translate3d( {centerX}px, {centerY}px, 0 ) scale( {scaleX}, {scaleY} )"; + var buildZoom = angular.bind(null, $mdUtil.supplant, zoomTemplate); + + return buildZoom(self.calculateTransformValues(element, originator)); + }, + + /** + * Calculate the slide transform from panel to origin. + * NOTE: all values are rounded to the nearest integer + */ + calculateSlideToOrigin: function (element, originator) { + var slideTemplate = "translate3d( {centerX}px, {centerY}px, 0 )"; + var buildSlide = angular.bind(null, $mdUtil.supplant, slideTemplate); + + return buildSlide(self.calculateTransformValues(element, originator)); + }, + /** * Enhance raw values to represent valid css stylings... */ @@ -5552,6 +7289,9 @@ function AnimateDomUtils($mdUtil, $q, $timeout, $mdConstant, $animateCss) { case 'transformOrigin': convertToVendor(key, $mdConstant.CSS.TRANSFORM_ORIGIN, value); break; + case 'font-size': + css['font-size'] = value; // font sizes aren't always in px + break; } } }); @@ -5576,7 +7316,7 @@ function AnimateDomUtils($mdUtil, $q, $timeout, $mdConstant, $animateCss) { if (addTransition) { transition = transition || "all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) !important"; - css['transition'] = transition; + css.transition = transition; } return css; @@ -5591,7 +7331,7 @@ function AnimateDomUtils($mdUtil, $q, $timeout, $mdConstant, $animateCss) { destination = destination || {}; angular.forEach('left top right bottom width height'.split(' '), function (key) { - destination[key] = Math.round(source[key]) + destination[key] = Math.round(source[key]); }); destination.width = destination.width || (destination.right - destination.left); @@ -5624,19 +7364,18 @@ function AnimateDomUtils($mdUtil, $q, $timeout, $mdConstant, $animateCss) { } }; -}; +} })(); (function(){ "use strict"; -"use strict"; - if (angular.version.minor >= 4) { angular.module('material.core.animate', []); } else { (function() { + "use strict"; var forEach = angular.forEach; @@ -5651,7 +7390,7 @@ if (angular.version.minor >= 4) { var $$ForceReflowFactory = ['$document', function($document) { return function() { return $document[0].body.clientWidth + 1; - } + }; }]; var $$rAFMutexFactory = ['$$rAF', function($$rAF) { @@ -5765,6 +7504,22 @@ if (angular.version.minor >= 4) { } }; + // Polyfill AnimateRunner.all which is used by input animations + AnimateRunner.all = function(runners, callback) { + var count = 0; + var status = true; + forEach(runners, function(runner) { + runner.done(onProgress); + }); + + function onProgress(response) { + status = status && response; + if (++count === runners.length) { + callback(status); + } + } + }; + return AnimateRunner; }]; @@ -5773,36 +7528,44 @@ if (angular.version.minor >= 4) { .factory('$$forceReflow', $$ForceReflowFactory) .factory('$$AnimateRunner', $$AnimateRunnerFactory) .factory('$$rAFMutex', $$rAFMutexFactory) - .factory('$animateCss', ['$window', '$$rAF', '$$AnimateRunner', '$$forceReflow', '$$jqLite', '$timeout', - function($window, $$rAF, $$AnimateRunner, $$forceReflow, $$jqLite, $timeout) { + .factory('$animateCss', ['$window', '$$rAF', '$$AnimateRunner', '$$forceReflow', '$$jqLite', '$timeout', '$animate', + function($window, $$rAF, $$AnimateRunner, $$forceReflow, $$jqLite, $timeout, $animate) { function init(element, options) { var temporaryStyles = []; var node = getDomNode(element); + var areAnimationsAllowed = node && $animate.enabled(); - if (options.transitionStyle) { - temporaryStyles.push([PREFIX + 'transition', options.transitionStyle]); - } + var hasCompleteStyles = false; + var hasCompleteClasses = false; - if (options.keyframeStyle) { - temporaryStyles.push([PREFIX + 'animation', options.keyframeStyle]); - } + if (areAnimationsAllowed) { + if (options.transitionStyle) { + temporaryStyles.push([PREFIX + 'transition', options.transitionStyle]); + } - if (options.delay) { - temporaryStyles.push([PREFIX + 'transition-delay', options.delay + 's']); - } + 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']); + } + + hasCompleteStyles = options.keyframeStyle || + (options.to && (options.duration > 0 || options.transitionStyle)); + hasCompleteClasses = !!options.addClass || !!options.removeClass; - if (options.duration) { - temporaryStyles.push([PREFIX + 'transition-duration', options.duration + 's']); + blockTransition(element, true); } - var hasCompleteStyles = options.keyframeStyle || - (options.to && (options.duration > 0 || options.transitionStyle)); - var hasCompleteClasses = !!options.addClass || !!options.removeClass; - var hasCompleteAnimation = hasCompleteStyles || hasCompleteClasses; + var hasCompleteAnimation = areAnimationsAllowed && (hasCompleteStyles || hasCompleteClasses); - blockTransition(element, true); applyAnimationFromStyles(element, options); var animationClosed = false; @@ -5905,7 +7668,7 @@ if (angular.version.minor >= 4) { return runner; } } - } + }; } function applyClasses(element, options) { @@ -5921,7 +7684,7 @@ if (angular.version.minor >= 4) { function computeTimings(element) { var node = getDomNode(element); - var cs = $window.getComputedStyle(node) + var cs = $window.getComputedStyle(node); var tdr = parseMaxTime(cs[prop('transitionDuration')]); var adr = parseMaxTime(cs[prop('animationDuration')]); var tdy = parseMaxTime(cs[prop('transitionDelay')]); @@ -6076,8 +7839,8 @@ angular.module('material.components.autocomplete', [ 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."; + .directive('mdBackdrop', ["$mdTheming", "$mdUtil", "$animate", "$rootElement", "$window", "$log", "$$rAF", "$document", function BackdropDirective($mdTheming, $mdUtil, $animate, $rootElement, $window, $log, $$rAF, $document) { + var ERROR_CSS_POSITION = ' may not work properly in a scrolled, static-positioned parent container.'; return { restrict: 'E', @@ -6085,44 +7848,56 @@ angular }; 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 () { + var bodyStyles; + + $$rAF(function() { + // If body scrolling has been disabled using mdUtil.disableBodyScroll(), + // adjust the 'backdrop' height to account for the fixed 'body' top offset. + // Note that this can be pretty expensive and is better done inside the $$rAF. + bodyStyles = $window.getComputedStyle($document[0].body); + + if (bodyStyles.position === 'fixed') { + var resizeHandler = $mdUtil.debounce(function(){ + bodyStyles = $window.getComputedStyle($document[0].body); + resize(); + }, 60, null, false); + + resize(); + angular.element($window).on('resize', resizeHandler); + + scope.$on('$destroy', function() { + angular.element($window).off('resize', resizeHandler); + }); + } // 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) { + var parent = element.parent(); - if ( parent.nodeName == 'BODY' ) { - element.css({position : 'fixed'}); + if (parent.length) { + if (parent[0].nodeName === 'BODY') { + element.css('position', 'fixed'); } - var styles = $window.getComputedStyle(parent); - if (styles.position == 'static') { + var styles = $window.getComputedStyle(parent[0]); + + if (styles.position === 'static') { // backdrop uses position:absolute and will not work properly with parent position:static (default) $log.warn(ERROR_CSS_POSITION); } - } - // Only inherit the parent if the backdrop has a parent. - if (element.parent().length) { - $mdTheming.inherit(element, element.parent()); + // Only inherit the parent if the backdrop has a parent. + $mdTheming.inherit(element, parent); } }); + function resize() { + var viewportHeight = parseInt(bodyStyles.height, 10) + Math.abs(parseInt(bodyStyles.top, 10)); + element.css('height', viewportHeight + 'px'); + } } }]); @@ -6137,6 +7912,8 @@ angular * @description * BottomSheet */ +MdBottomSheetDirective.$inject = ["$mdBottomSheet"]; +MdBottomSheetProvider.$inject = ["$$interimElementProvider"]; angular .module('material.components.bottomSheet', [ 'material.core', @@ -6149,7 +7926,9 @@ angular function MdBottomSheetDirective($mdBottomSheet) { return { restrict: 'E', - link : function postLink(scope, element, attr) { + link : function postLink(scope, element) { + element.addClass('_md'); // private md component indicator for styling + // When navigation force destroys an interimElement, then // listen and $destroy() that interim instance... scope.$on('$destroy', function() { @@ -6158,7 +7937,6 @@ function MdBottomSheetDirective($mdBottomSheet) { } }; } -MdBottomSheetDirective.$inject = ["$mdBottomSheet"]; /** @@ -6219,6 +7997,7 @@ MdBottomSheetDirective.$inject = ["$mdBottomSheet"]; * of 3. * - `clickOutsideToClose` - `{boolean=}`: Whether the user can click outside the bottom sheet to * close it. Default true. + * - `bindToController` - `{boolean=}`: When set to true, the locals will be bound to the controller instance. * - `disableBackdrop` - `{boolean=}`: When set to true, the bottomsheet will not show a backdrop. * - `escapeToClose` - `{boolean=}`: Whether the user can press escape to close the bottom sheet. * Default true. @@ -6261,10 +8040,10 @@ MdBottomSheetDirective.$inject = ["$mdBottomSheet"]; function MdBottomSheetProvider($$interimElementProvider) { // how fast we need to flick down to close the sheet, pixels/ms + bottomSheetDefaults.$inject = ["$animate", "$mdConstant", "$mdUtil", "$mdTheming", "$mdBottomSheet", "$rootElement", "$mdGesture", "$log"]; 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'], @@ -6272,7 +8051,8 @@ function MdBottomSheetProvider($$interimElementProvider) { }); /* @ngInject */ - function bottomSheetDefaults($animate, $mdConstant, $mdUtil, $mdTheming, $mdBottomSheet, $rootElement, $mdGesture) { + function bottomSheetDefaults($animate, $mdConstant, $mdUtil, $mdTheming, $mdBottomSheet, $rootElement, + $mdGesture, $log) { var backdrop; return { @@ -6293,9 +8073,16 @@ function MdBottomSheetProvider($$interimElementProvider) { // prevent tab focus or click focus on the bottom-sheet container element.attr('tabindex',"-1"); + // Once the md-bottom-sheet has `ng-cloak` applied on his template the opening animation will not work properly. + // This is a very common problem, so we have to notify the developer about this. + if (element.hasClass('ng-cloak')) { + var message = '$mdBottomSheet: using `` will affect the bottom-sheet opening animations.'; + $log.warn( message, element[0] ); + } + if (!options.disableBackdrop) { // Add a backdrop that will close on click - backdrop = $mdUtil.createBackdrop(scope, "_md-bottom-sheet-backdrop md-opaque"); + backdrop = $mdUtil.createBackdrop(scope, "md-bottom-sheet-backdrop md-opaque"); // Prevent mouse focus on backdrop; ONLY programatic focus allowed. // This allows clicks on backdrop to propogate to the $rootElement and @@ -6328,7 +8115,7 @@ function MdBottomSheetProvider($$interimElementProvider) { var focusable = $mdUtil.findFocusTarget(element) || angular.element( element[0].querySelector('button') || element[0].querySelector('a') || - element[0].querySelector('[ng-click]') + element[0].querySelector($mdUtil.prefixer('ng-click', true)) ) || backdrop; if (options.escapeToClose) { @@ -6410,7 +8197,6 @@ function MdBottomSheetProvider($$interimElementProvider) { } } -MdBottomSheetProvider.$inject = ["$$interimElementProvider"]; })(); (function(){ @@ -6423,27 +8209,81 @@ MdBottomSheetProvider.$inject = ["$$interimElementProvider"]; * * Button */ +MdButtonDirective.$inject = ["$mdButtonInkRipple", "$mdTheming", "$mdAria", "$mdInteraction"]; +MdAnchorDirective.$inject = ["$mdTheming"]; angular .module('material.components.button', [ 'material.core' ]) - .directive('mdButton', MdButtonDirective); + .directive('mdButton', MdButtonDirective) + .directive('a', MdAnchorDirective); + /** - * @ngdoc directive - * @name mdButton - * @module material.components.button - * + * @private * @restrict E * * @description - * `` is a button directive with optional ink ripples (default enabled). + * `a` is an anchor directive used to inherit theme colors for md-primary, md-accent, etc. * - * If you supply a `href` or `ng-href` attribute, it will become an `
` element. Otherwise, it will - * become a `' + + * '
' + + * ' ' + + * ' ' + + * '
' + + * '
', + * panelClass: 'menu-panel-container', + * focusOnOpen: false, + * zIndex: 100, + * propagateContainerEvents: true, + * groupName: 'menus' + * }); + * } + * + * function PanelProviderCtrl($mdPanel) { + * this.navigation = { + * name: 'navigation', + * items: [ + * 'Home', + * 'About', + * 'Contact' + * ] + * }; + * this.favorites = { + * name: 'favorites', + * items: [ + * 'Add to Favorites' + * ] + * }; + * this.more = { + * name: 'more', + * items: [ + * 'Account', + * 'Sign Out' + * ] + * }; + * + * $mdPanel.newPanelGroup('menus', { + * maxOpen: 2 + * }); + * + * this.showMenu = function($event, menu) { + * $mdPanel.open('demoPreset', { + * id: 'menu_' + menu.name, + * position: $mdPanel.newPanelPosition() + * .relativeTo($event.srcElement) + * .addPanelPosition( + * $mdPanel.xPosition.ALIGN_START, + * $mdPanel.yPosition.BELOW + * ), + * locals: { + * items: menu.items + * }, + * openFrom: $event + * }); + * }; + * } + * + * function PanelMenuCtrl(mdPanelRef) { + * this.closeMenu = function() { + * mdPanelRef && mdPanelRef.close(); + * }; + * } + * })(angular); + * + */ + +/** + * @ngdoc method + * @name $mdPanelProvider#definePreset + * @description + * Takes the passed in preset name and preset configuration object and adds it + * to the `_presets` object of the provider. This `_presets` object is then + * passed along to the `$mdPanel` service. + * + * @param {string} name Preset name. + * @param {!Object} preset Specific configuration object that can contain any + * and all of the parameters avaialble within the `$mdPanel.create` method. + * However, parameters that pertain to id, position, animation, and user + * interaction are not allowed and will be removed from the preset + * configuration. + */ + + +/***************************************************************************** + * MdPanel Service * + *****************************************************************************/ + + +/** + * @ngdoc service + * @name $mdPanel + * @module material.components.panel + * + * @description + * `$mdPanel` is a robust, low-level service for creating floating panels on + * the screen. It can be used to implement tooltips, dialogs, pop-ups, etc. + * + * @usage + * + * (function(angular, undefined) { + * 'use strict'; + * + * angular + * .module('demoApp', ['ngMaterial']) + * .controller('DemoDialogController', DialogController); + * + * var panelRef; + * + * function showPanel($event) { + * var panelPosition = $mdPanel.newPanelPosition() + * .absolute() + * .top('50%') + * .left('50%'); + * + * var panelAnimation = $mdPanel.newPanelAnimation() + * .targetEvent($event) + * .defaultAnimation('md-panel-animate-fly') + * .closeTo('.show-button'); + * + * var config = { + * attachTo: angular.element(document.body), + * controller: DialogController, + * controllerAs: 'ctrl', + * position: panelPosition, + * animation: panelAnimation, + * targetEvent: $event, + * templateUrl: 'dialog-template.html', + * clickOutsideToClose: true, + * escapeToClose: true, + * focusOnOpen: true + * } + * + * $mdPanel.open(config) + * .then(function(result) { + * panelRef = result; + * }); + * } + * + * function DialogController(MdPanelRef) { + * function closeDialog() { + * if (MdPanelRef) MdPanelRef.close(); + * } + * } + * })(angular); + * + */ + +/** + * @ngdoc method + * @name $mdPanel#create + * @description + * Creates a panel with the specified options. + * + * @param config {!Object=} Specific configuration object that may contain the + * following properties: + * + * - `id` - `{string=}`: An ID to track the panel by. When an ID is provided, + * the created panel is added to a tracked panels object. Any subsequent + * requests made to create a panel with that ID are ignored. This is useful + * in having the panel service not open multiple panels from the same user + * interaction when there is no backdrop and events are propagated. Defaults + * to an arbitrary string that is not tracked. + * - `template` - `{string=}`: HTML template to show in the panel. This + * **must** be trusted HTML with respect to Angular’s + * [$sce service](https://docs.angularjs.org/api/ng/service/$sce). + * - `templateUrl` - `{string=}`: The URL that will be used as the content of + * the panel. + * - `contentElement` - `{(string|!angular.JQLite|!Element)=}`: Pre-compiled + * element to be used as the panel's content. + * - `controller` - `{(function|string)=}`: The controller to associate with + * the panel. The controller can inject a reference to the returned + * panelRef, which allows the panel to be closed, hidden, and shown. Any + * fields passed in through locals or resolve will be bound to the + * controller. + * - `controllerAs` - `{string=}`: An alias to assign the controller to on + * the scope. + * - `bindToController` - `{boolean=}`: Binds locals to the controller + * instead of passing them in. Defaults to true, as this is a best + * practice. + * - `locals` - `{Object=}`: An object containing key/value pairs. The keys + * will be used as names of values to inject into the controller. For + * example, `locals: {three: 3}` would inject `three` into the controller, + * with the value 3. + * - `resolve` - `{Object=}`: Similar to locals, except it takes promises as + * values. The panel will not open until all of the promises resolve. + * - `attachTo` - `{(string|!angular.JQLite|!Element)=}`: The element to + * attach the panel to. Defaults to appending to the root element of the + * application. + * - `propagateContainerEvents` - `{boolean=}`: Whether pointer or touch + * events should be allowed to propagate 'go through' the container, aka the + * wrapper, of the panel. Defaults to false. + * - `panelClass` - `{string=}`: A css class to apply to the panel element. + * This class should define any borders, box-shadow, etc. for the panel. + * - `zIndex` - `{number=}`: The z-index to place the panel at. + * Defaults to 80. + * - `position` - `{MdPanelPosition=}`: An MdPanelPosition object that + * specifies the alignment of the panel. For more information, see + * `MdPanelPosition`. + * - `clickOutsideToClose` - `{boolean=}`: Whether the user can click + * outside the panel to close it. Defaults to false. + * - `escapeToClose` - `{boolean=}`: Whether the user can press escape to + * close the panel. Defaults to false. + * - `onCloseSuccess` - `{function(!panelRef, string)=}`: Function that is + * called after the close successfully finishes. The first parameter passed + * into this function is the current panelRef and the 2nd is an optional + * string explaining the close reason. The currently supported closeReasons + * can be found in the MdPanelRef.closeReasons enum. These are by default + * passed along by the panel. + * - `trapFocus` - `{boolean=}`: Whether focus should be trapped within the + * panel. If `trapFocus` is true, the user will not be able to interact + * with the rest of the page until the panel is dismissed. Defaults to + * false. + * - `focusOnOpen` - `{boolean=}`: An option to override focus behavior on + * open. Only disable if focusing some other way, as focus management is + * required for panels to be accessible. Defaults to true. + * - `fullscreen` - `{boolean=}`: Whether the panel should be full screen. + * Applies the class `._md-panel-fullscreen` to the panel on open. Defaults + * to false. + * - `animation` - `{MdPanelAnimation=}`: An MdPanelAnimation object that + * specifies the animation of the panel. For more information, see + * `MdPanelAnimation`. + * - `hasBackdrop` - `{boolean=}`: Whether there should be an opaque backdrop + * behind the panel. Defaults to false. + * - `disableParentScroll` - `{boolean=}`: Whether the user can scroll the + * page behind the panel. Defaults to false. + * - `onDomAdded` - `{function=}`: Callback function used to announce when + * the panel is added to the DOM. + * - `onOpenComplete` - `{function=}`: Callback function used to announce + * when the open() action is finished. + * - `onRemoving` - `{function=}`: Callback function used to announce the + * close/hide() action is starting. + * - `onDomRemoved` - `{function=}`: Callback function used to announce when + * the panel is removed from the DOM. + * - `origin` - `{(string|!angular.JQLite|!Element)=}`: The element to focus + * on when the panel closes. This is commonly the element which triggered + * the opening of the panel. If you do not use `origin`, you need to control + * the focus manually. + * - `groupName` - `{(string|!Array)=}`: A group name or an array of + * group names. The group name is used for creating a group of panels. The + * group is used for configuring the number of open panels and identifying + * specific behaviors for groups. For instance, all tooltips could be + * identified using the same groupName. + * + * @returns {!MdPanelRef} panelRef + */ + +/** + * @ngdoc method + * @name $mdPanel#open + * @description + * Calls the create method above, then opens the panel. This is a shortcut for + * creating and then calling open manually. If custom methods need to be + * called when the panel is added to the DOM or opened, do not use this method. + * Instead create the panel, chain promises on the domAdded and openComplete + * methods, and call open from the returned panelRef. + * + * @param {!Object=} config Specific configuration object that may contain + * the properties defined in `$mdPanel.create`. + * @returns {!angular.$q.Promise} panelRef A promise that resolves + * to an instance of the panel. + */ + +/** + * @ngdoc method + * @name $mdPanel#newPanelPosition + * @description + * Returns a new instance of the MdPanelPosition object. Use this to create + * the position config object. + * + * @returns {!MdPanelPosition} panelPosition + */ + +/** + * @ngdoc method + * @name $mdPanel#newPanelAnimation + * @description + * Returns a new instance of the MdPanelAnimation object. Use this to create + * the animation config object. + * + * @returns {!MdPanelAnimation} panelAnimation + */ + +/** + * @ngdoc method + * @name $mdPanel#newPanelGroup + * @description + * Creates a panel group and adds it to a tracked list of panel groups. + * + * @param {string} groupName Name of the group to create. + * @param {!Object=} config Specific configuration object that may contain the + * following properties: + * + * - `maxOpen` - `{number=}`: The maximum number of panels that are allowed to + * be open within a defined panel group. + * + * @returns {!Object, + * openPanels: !Array, + * maxOpen: number}>} panelGroup + */ + +/** + * @ngdoc method + * @name $mdPanel#setGroupMaxOpen + * @description + * Sets the maximum number of panels in a group that can be opened at a given + * time. + * + * @param {string} groupName The name of the group to configure. + * @param {number} maxOpen The maximum number of panels that can be + * opened. Infinity can be passed in to remove the maxOpen limit. + */ + + +/***************************************************************************** + * MdPanelRef * + *****************************************************************************/ + + +/** + * @ngdoc type + * @name MdPanelRef + * @module material.components.panel + * @description + * A reference to a created panel. This reference contains a unique id for the + * panel, along with the following properties: + * + * - `id` - `{string}`: The unique id for the panel. This id is used to track + * when a panel was interacted with. + * - `config` - `{!Object=}`: The entire config object that was used in + * create. + * - `isAttached` - `{boolean}`: Whether the panel is attached to the DOM. + * Visibility to the user does not factor into isAttached. + * - `panelContainer` - `{angular.JQLite}`: The wrapper element containing the + * panel. This property is added in order to have access to the `addClass`, + * `removeClass`, `toggleClass`, etc methods. + * - `panelEl` - `{angular.JQLite}`: The panel element. This property is added + * in order to have access to the `addClass`, `removeClass`, `toggleClass`, + * etc methods. + */ + +/** + * @ngdoc method + * @name MdPanelRef#open + * @description + * Attaches and shows the panel. + * + * @returns {!angular.$q.Promise} A promise that is resolved when the panel is + * opened. + */ + +/** + * @ngdoc method + * @name MdPanelRef#close + * @description + * Hides and detaches the panel. Note that this will **not** destroy the panel. + * If you don't intend on using the panel again, call the {@link #destroy + * destroy} method afterwards. + * + * @returns {!angular.$q.Promise} A promise that is resolved when the panel is + * closed. + */ + +/** + * @ngdoc method + * @name MdPanelRef#attach + * @description + * Create the panel elements and attach them to the DOM. The panel will be + * hidden by default. + * + * @returns {!angular.$q.Promise} A promise that is resolved when the panel is + * attached. + */ + +/** + * @ngdoc method + * @name MdPanelRef#detach + * @description + * Removes the panel from the DOM. This will NOT hide the panel before removing + * it. + * + * @returns {!angular.$q.Promise} A promise that is resolved when the panel is + * detached. + */ + +/** + * @ngdoc method + * @name MdPanelRef#show + * @description + * Shows the panel. + * + * @returns {!angular.$q.Promise} A promise that is resolved when the panel has + * shown and animations are completed. + */ + +/** + * @ngdoc method + * @name MdPanelRef#hide + * @description + * Hides the panel. + * + * @returns {!angular.$q.Promise} A promise that is resolved when the panel has + * hidden and animations are completed. + */ + +/** + * @ngdoc method + * @name MdPanelRef#destroy + * @description + * Destroys the panel. The panel cannot be opened again after this is called. + */ + +/** + * @ngdoc method + * @name MdPanelRef#addClass + * @deprecated + * This method is in the process of being deprecated in favor of using the panel + * and container JQLite elements that are referenced in the MdPanelRef object. + * Full deprecation is scheduled for material 1.2. + * @description + * Adds a class to the panel. DO NOT use this hide/show the panel. + * + * @param {string} newClass class to be added. + * @param {boolean} toElement Whether or not to add the class to the panel + * element instead of the container. + */ + +/** + * @ngdoc method + * @name MdPanelRef#removeClass + * @deprecated + * This method is in the process of being deprecated in favor of using the panel + * and container JQLite elements that are referenced in the MdPanelRef object. + * Full deprecation is scheduled for material 1.2. + * @description + * Removes a class from the panel. DO NOT use this to hide/show the panel. + * + * @param {string} oldClass Class to be removed. + * @param {boolean} fromElement Whether or not to remove the class from the + * panel element instead of the container. + */ + +/** + * @ngdoc method + * @name MdPanelRef#toggleClass + * @deprecated + * This method is in the process of being deprecated in favor of using the panel + * and container JQLite elements that are referenced in the MdPanelRef object. + * Full deprecation is scheduled for material 1.2. + * @description + * Toggles a class on the panel. DO NOT use this to hide/show the panel. + * + * @param {string} toggleClass Class to be toggled. + * @param {boolean} onElement Whether or not to remove the class from the panel + * element instead of the container. + */ + +/** + * @ngdoc method + * @name MdPanelRef#updatePosition + * @description + * Updates the position configuration of a panel. Use this to update the + * position of a panel that is open, without having to close and re-open the + * panel. + * + * @param {!MdPanelPosition} position + */ + +/** + * @ngdoc method + * @name MdPanelRef#addToGroup + * @description + * Adds a panel to a group if the panel does not exist within the group already. + * A panel can only exist within a single group. + * + * @param {string} groupName The name of the group to add the panel to. + */ + +/** + * @ngdoc method + * @name MdPanelRef#removeFromGroup + * @description + * Removes a panel from a group if the panel exists within that group. The group + * must be created ahead of time. + * + * @param {string} groupName The name of the group. + */ + +/** + * @ngdoc method + * @name MdPanelRef#registerInterceptor + * @description + * Registers an interceptor with the panel. The callback should return a promise, + * which will allow the action to continue when it gets resolved, or will + * prevent an action if it is rejected. The interceptors are called sequentially + * and it reverse order. `type` must be one of the following + * values available on `$mdPanel.interceptorTypes`: + * * `CLOSE` - Gets called before the panel begins closing. + * + * @param {string} type Type of interceptor. + * @param {!angular.$q.Promise} callback Callback to be registered. + * @returns {!MdPanelRef} + */ + +/** + * @ngdoc method + * @name MdPanelRef#removeInterceptor + * @description + * Removes a registered interceptor. + * + * @param {string} type Type of interceptor to be removed. + * @param {function(): !angular.$q.Promise} callback Interceptor to be removed. + * @returns {!MdPanelRef} + */ + +/** + * @ngdoc method + * @name MdPanelRef#removeAllInterceptors + * @description + * Removes all interceptors. If a type is supplied, only the + * interceptors of that type will be cleared. + * + * @param {string=} type Type of interceptors to be removed. + * @returns {!MdPanelRef} + */ + +/** + * @ngdoc method + * @name MdPanelRef#updateAnimation + * @description + * Updates the animation configuration for a panel. You can use this to change + * the panel's animation without having to re-create it. + * + * @param {!MdPanelAnimation} animation + */ + + +/***************************************************************************** + * MdPanelPosition * + *****************************************************************************/ + + +/** + * @ngdoc type + * @name MdPanelPosition + * @module material.components.panel + * @description + * + * Object for configuring the position of the panel. + * + * @usage + * + * #### Centering the panel + * + * + * new MdPanelPosition().absolute().center(); + * + * + * #### Overlapping the panel with an element + * + * + * new MdPanelPosition() + * .relativeTo(someElement) + * .addPanelPosition( + * $mdPanel.xPosition.ALIGN_START, + * $mdPanel.yPosition.ALIGN_TOPS + * ); + * + * + * #### Aligning the panel with the bottom of an element + * + * + * new MdPanelPosition() + * .relativeTo(someElement) + * .addPanelPosition($mdPanel.xPosition.CENTER, $mdPanel.yPosition.BELOW); + * + */ + +/** + * @ngdoc method + * @name MdPanelPosition#absolute + * @description + * Positions the panel absolutely relative to the parent element. If the parent + * is document.body, this is equivalent to positioning the panel absolutely + * within the viewport. + * + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#relativeTo + * @description + * Positions the panel relative to a specific element. + * + * @param {string|!Element|!angular.JQLite} element Query selector, DOM element, + * or angular element to position the panel with respect to. + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#top + * @description + * Sets the value of `top` for the panel. Clears any previously set vertical + * position. + * + * @param {string=} top Value of `top`. Defaults to '0'. + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#bottom + * @description + * Sets the value of `bottom` for the panel. Clears any previously set vertical + * position. + * + * @param {string=} bottom Value of `bottom`. Defaults to '0'. + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#start + * @description + * Sets the panel to the start of the page - `left` if `ltr` or `right` for + * `rtl`. Clears any previously set horizontal position. + * + * @param {string=} start Value of position. Defaults to '0'. + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#end + * @description + * Sets the panel to the end of the page - `right` if `ltr` or `left` for `rtl`. + * Clears any previously set horizontal position. + * + * @param {string=} end Value of position. Defaults to '0'. + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#left + * @description + * Sets the value of `left` for the panel. Clears any previously set + * horizontal position. + * + * @param {string=} left Value of `left`. Defaults to '0'. + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#right + * @description + * Sets the value of `right` for the panel. Clears any previously set + * horizontal position. + * + * @param {string=} right Value of `right`. Defaults to '0'. + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#centerHorizontally + * @description + * Centers the panel horizontally in the viewport. Clears any previously set + * horizontal position. + * + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#centerVertically + * @description + * Centers the panel vertically in the viewport. Clears any previously set + * vertical position. + * + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#center + * @description + * Centers the panel horizontally and vertically in the viewport. This is + * equivalent to calling both `centerHorizontally` and `centerVertically`. + * Clears any previously set horizontal and vertical positions. + * + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#addPanelPosition + * @description + * Sets the x and y position for the panel relative to another element. Can be + * called multiple times to specify an ordered list of panel positions. The + * first position which allows the panel to be completely on-screen will be + * chosen; the last position will be chose whether it is on-screen or not. + * + * xPosition must be one of the following values available on + * $mdPanel.xPosition: + * + * + * CENTER | ALIGN_START | ALIGN_END | OFFSET_START | OFFSET_END + * + *
+ *    *************
+ *    *           *
+ *    *   PANEL   *
+ *    *           *
+ *    *************
+ *   A B    C    D E
+ *
+ * A: OFFSET_START (for LTR displays)
+ * B: ALIGN_START (for LTR displays)
+ * C: CENTER
+ * D: ALIGN_END (for LTR displays)
+ * E: OFFSET_END (for LTR displays)
+ * 
+ * + * yPosition must be one of the following values available on + * $mdPanel.yPosition: + * + * CENTER | ALIGN_TOPS | ALIGN_BOTTOMS | ABOVE | BELOW + * + *
+ *   F
+ *   G *************
+ *     *           *
+ *   H *   PANEL   *
+ *     *           *
+ *   I *************
+ *   J
+ *
+ * F: BELOW
+ * G: ALIGN_TOPS
+ * H: CENTER
+ * I: ALIGN_BOTTOMS
+ * J: ABOVE
+ * 
+ * + * @param {string} xPosition + * @param {string} yPosition + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#withOffsetX + * @description + * Sets the value of the offset in the x-direction. + * + * @param {string} offsetX + * @returns {!MdPanelPosition} + */ + +/** + * @ngdoc method + * @name MdPanelPosition#withOffsetY + * @description + * Sets the value of the offset in the y-direction. + * + * @param {string} offsetY + * @returns {!MdPanelPosition} + */ + + +/***************************************************************************** + * MdPanelAnimation * + *****************************************************************************/ + + +/** + * @ngdoc type + * @name MdPanelAnimation + * @module material.components.panel + * @description + * Animation configuration object. To use, create an MdPanelAnimation with the + * desired properties, then pass the object as part of $mdPanel creation. + * + * @usage + * + * + * var panelAnimation = new MdPanelAnimation() + * .openFrom(myButtonEl) + * .duration(1337) + * .closeTo('.my-button') + * .withAnimation($mdPanel.animation.SCALE); + * + * $mdPanel.create({ + * animation: panelAnimation + * }); + * + */ + +/** + * @ngdoc method + * @name MdPanelAnimation#openFrom + * @description + * Specifies where to start the open animation. `openFrom` accepts a + * click event object, query selector, DOM element, or a Rect object that + * is used to determine the bounds. When passed a click event, the location + * of the click will be used as the position to start the animation. + * + * @param {string|!Element|!Event|{top: number, left: number}} + * @returns {!MdPanelAnimation} + */ + +/** + * @ngdoc method + * @name MdPanelAnimation#closeTo + * @description + * Specifies where to animate the panel close. `closeTo` accepts a + * query selector, DOM element, or a Rect object that is used to determine + * the bounds. + * + * @param {string|!Element|{top: number, left: number}} + * @returns {!MdPanelAnimation} + */ + +/** + * @ngdoc method + * @name MdPanelAnimation#withAnimation + * @description + * Specifies the animation class. + * + * There are several default animations that can be used: + * ($mdPanel.animation) + * SLIDE: The panel slides in and out from the specified + * elements. It will not fade in or out. + * SCALE: The panel scales in and out. Slide and fade are + * included in this animation. + * FADE: The panel fades in and out. + * + * Custom classes will by default fade in and out unless + * "transition: opacity 1ms" is added to the to custom class. + * + * @param {string|{open: string, close: string}} cssClass + * @returns {!MdPanelAnimation} + */ + +/** + * @ngdoc method + * @name MdPanelAnimation#duration + * @description + * Specifies the duration of the animation in milliseconds. The `duration` + * method accepts either a number or an object with separate open and close + * durations. + * + * @param {number|{open: number, close: number}} duration + * @returns {!MdPanelAnimation} + */ + + +/***************************************************************************** + * PUBLIC DOCUMENTATION * + *****************************************************************************/ + + +var MD_PANEL_Z_INDEX = 80; +var MD_PANEL_HIDDEN = '_md-panel-hidden'; +var FOCUS_TRAP_TEMPLATE = angular.element( + '
'); + +var _presets = {}; + + +/** + * A provider that is used for creating presets for the panel API. + * @final @constructor @ngInject + */ +function MdPanelProvider() { + return { + 'definePreset': definePreset, + 'getAllPresets': getAllPresets, + 'clearPresets': clearPresets, + '$get': $getProvider() + }; +} + + +/** + * Takes the passed in panel configuration object and adds it to the `_presets` + * object at the specified name. + * @param {string} name Name of the preset to set. + * @param {!Object} preset Specific configuration object that can contain any + * and all of the parameters avaialble within the `$mdPanel.create` method. + * However, parameters that pertain to id, position, animation, and user + * interaction are not allowed and will be removed from the preset + * configuration. + */ +function definePreset(name, preset) { + if (!name || !preset) { + throw new Error('mdPanelProvider: The panel preset definition is ' + + 'malformed. The name and preset object are required.'); + } else if (_presets.hasOwnProperty(name)) { + throw new Error('mdPanelProvider: The panel preset you have requested ' + + 'has already been defined.'); + } + + // Delete any property on the preset that is not allowed. + delete preset.id; + delete preset.position; + delete preset.animation; + + _presets[name] = preset; +} + + +/** + * Gets a clone of the `_presets`. + * @return {!Object} + */ +function getAllPresets() { + return angular.copy(_presets); +} + + +/** + * Clears all of the stored presets. + */ +function clearPresets() { + _presets = {}; +} + + +/** + * Represents the `$get` method of the Angular provider. From here, a new + * reference to the MdPanelService is returned where the needed arguments are + * passed in including the MdPanelProvider `_presets`. + * @param {!Object} _presets + * @param {!angular.JQLite} $rootElement + * @param {!angular.Scope} $rootScope + * @param {!angular.$injector} $injector + * @param {!angular.$window} $window + */ +function $getProvider() { + return [ + '$rootElement', '$rootScope', '$injector', '$window', + function($rootElement, $rootScope, $injector, $window) { + return new MdPanelService(_presets, $rootElement, $rootScope, + $injector, $window); + } + ]; +} + + +/***************************************************************************** + * MdPanel Service * + *****************************************************************************/ + + +/** + * A service that is used for controlling/displaying panels on the screen. + * @param {!Object} presets + * @param {!angular.JQLite} $rootElement + * @param {!angular.Scope} $rootScope + * @param {!angular.$injector} $injector + * @param {!angular.$window} $window + * @final @constructor @ngInject + */ +function MdPanelService(presets, $rootElement, $rootScope, $injector, $window) { + /** + * Default config options for the panel. + * Anything angular related needs to be done later. Therefore + * scope: $rootScope.$new(true), + * attachTo: $rootElement, + * are added later. + * @private {!Object} */ - CalendarMonthCtrl.prototype.buildDateCell = function(opt_date) { - var calendarCtrl = this.calendarCtrl; + this._defaultConfigOptions = { + bindToController: true, + clickOutsideToClose: false, + disableParentScroll: false, + escapeToClose: false, + focusOnOpen: true, + fullscreen: false, + hasBackdrop: false, + propagateContainerEvents: false, + transformTemplate: angular.bind(this, this._wrapTemplate), + trapFocus: false, + zIndex: MD_PANEL_Z_INDEX + }; + + /** @private {!Object} */ + this._config = {}; + + /** @private {!Object} */ + this._presets = presets; + + /** @private @const */ + this._$rootElement = $rootElement; + + /** @private @const */ + this._$rootScope = $rootScope; + + /** @private @const */ + this._$injector = $injector; + + /** @private @const */ + this._$window = $window; + + /** @private @const */ + this._$mdUtil = this._$injector.get('$mdUtil'); + + /** @private {!Object} */ + this._trackedPanels = {}; + + /** + * @private {!Object, + * openPanels: !Array, + * maxOpen: number}>} + */ + this._groups = Object.create(null); + + /** + * Default animations that can be used within the panel. + * @type {enum} + */ + this.animation = MdPanelAnimation.animation; + + /** + * Possible values of xPosition for positioning the panel relative to + * another element. + * @type {enum} + */ + this.xPosition = MdPanelPosition.xPosition; + + /** + * Possible values of yPosition for positioning the panel relative to + * another element. + * @type {enum} + */ + this.yPosition = MdPanelPosition.yPosition; + + /** + * Possible values for the interceptors that can be registered on a panel. + * @type {enum} + */ + this.interceptorTypes = MdPanelRef.interceptorTypes; + + /** + * Possible values for closing of a panel. + * @type {enum} + */ + this.closeReasons = MdPanelRef.closeReasons; + + /** + * Possible values of absolute position. + * @type {enum} + */ + this.absPosition = MdPanelPosition.absPosition; +} + + +/** + * Creates a panel with the specified options. + * @param {string=} preset Name of a preset configuration that can be used to + * extend the panel configuration. + * @param {!Object=} config Configuration object for the panel. + * @returns {!MdPanelRef} + */ +MdPanelService.prototype.create = function(preset, config) { + if (typeof preset === 'string') { + preset = this._getPresetByName(preset); + } else if (typeof preset === 'object' && + (angular.isUndefined(config) || !config)) { + config = preset; + preset = {}; + } + + preset = preset || {}; + config = config || {}; + + // If the passed-in config contains an ID and the ID is within _trackedPanels, + // return the tracked panel after updating its config with the passed-in + // config. + if (angular.isDefined(config.id) && this._trackedPanels[config.id]) { + var trackedPanel = this._trackedPanels[config.id]; + angular.extend(trackedPanel.config, config); + return trackedPanel; + } + + // Combine the passed-in config, the _defaultConfigOptions, and the preset + // configuration into the `_config`. + this._config = angular.extend({ + // If no ID is set within the passed-in config, then create an arbitrary ID. + id: config.id || 'panel_' + this._$mdUtil.nextUid(), + scope: this._$rootScope.$new(true), + attachTo: this._$rootElement + }, this._defaultConfigOptions, config, preset); + + // Create the panelRef and add it to the `_trackedPanels` object. + var panelRef = new MdPanelRef(this._config, this._$injector); + this._trackedPanels[config.id] = panelRef; + + // Add the panel to each of its requested groups. + if (this._config.groupName) { + if (angular.isString(this._config.groupName)) { + this._config.groupName = [this._config.groupName]; + } + angular.forEach(this._config.groupName, function(group) { + panelRef.addToGroup(group); + }); + } + + this._config.scope.$on('$destroy', angular.bind(panelRef, panelRef.detach)); + + return panelRef; +}; + + +/** + * Creates and opens a panel with the specified options. + * @param {string=} preset Name of a preset configuration that can be used to + * extend the panel configuration. + * @param {!Object=} config Configuration object for the panel. + * @returns {!angular.$q.Promise} The panel created from create. + */ +MdPanelService.prototype.open = function(preset, config) { + var panelRef = this.create(preset, config); + return panelRef.open().then(function() { + return panelRef; + }); +}; + + +/** + * Gets a specific preset configuration object saved within `_presets`. + * @param {string} preset Name of the preset to search for. + * @returns {!Object} The preset configuration object. + */ +MdPanelService.prototype._getPresetByName = function(preset) { + if (!this._presets[preset]) { + throw new Error('mdPanel: The panel preset configuration that you ' + + 'requested does not exist. Use the $mdPanelProvider to create a ' + + 'preset before requesting one.'); + } + return this._presets[preset]; +}; + + +/** + * Returns a new instance of the MdPanelPosition. Use this to create the + * positioning object. + * @returns {!MdPanelPosition} + */ +MdPanelService.prototype.newPanelPosition = function() { + return new MdPanelPosition(this._$injector); +}; + + +/** + * Returns a new instance of the MdPanelAnimation. Use this to create the + * animation object. + * @returns {!MdPanelAnimation} + */ +MdPanelService.prototype.newPanelAnimation = function() { + return new MdPanelAnimation(this._$injector); +}; + + +/** + * Creates a panel group and adds it to a tracked list of panel groups. + * @param groupName {string} Name of the group to create. + * @param config {!Object=} Specific configuration object that may contain the + * following properties: + * + * - `maxOpen` - `{number=}`: The maximum number of panels that are allowed + * open within a defined panel group. + * + * @returns {!Object, + * openPanels: !Array, + * maxOpen: number}>} panelGroup + */ +MdPanelService.prototype.newPanelGroup = function(groupName, config) { + if (!this._groups[groupName]) { + config = config || {}; + var group = { + panels: [], + openPanels: [], + maxOpen: config.maxOpen > 0 ? config.maxOpen : Infinity + }; + this._groups[groupName] = group; + } + return this._groups[groupName]; +}; + + +/** + * Sets the maximum number of panels in a group that can be opened at a given + * time. + * @param {string} groupName The name of the group to configure. + * @param {number} maxOpen The maximum number of panels that can be + * opened. Infinity can be passed in to remove the maxOpen limit. + */ +MdPanelService.prototype.setGroupMaxOpen = function(groupName, maxOpen) { + if (this._groups[groupName]) { + this._groups[groupName].maxOpen = maxOpen; + } else { + throw new Error('mdPanel: Group does not exist yet. Call newPanelGroup().'); + } +}; - // 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); +/** + * Determines if the current number of open panels within a group exceeds the + * limit of allowed open panels. + * @param {string} groupName The name of the group to check. + * @returns {boolean} true if open count does exceed maxOpen and false if not. + * @private + */ +MdPanelService.prototype._openCountExceedsMaxOpen = function(groupName) { + if (this._groups[groupName]) { + var group = this._groups[groupName]; + return group.maxOpen > 0 && group.openPanels.length > group.maxOpen; + } + return false; +}; - // 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); - } +/** + * Closes the first open panel within a specific group. + * @param {string} groupName The name of the group. + * @private + */ +MdPanelService.prototype._closeFirstOpenedPanel = function(groupName) { + this._groups[groupName].openPanels[0].close(); +}; - 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()]; +/** + * Wraps the users template in two elements, md-panel-outer-wrapper, which + * covers the entire attachTo element, and md-panel, which contains only the + * template. This allows the panel control over positioning, animations, + * and similar properties. + * @param {string} origTemplate The original template. + * @returns {string} The wrapped template. + * @private + */ +MdPanelService.prototype._wrapTemplate = function(origTemplate) { + var template = origTemplate || ''; + + // The panel should be initially rendered offscreen so we can calculate + // height and width for positioning. + return '' + + '
' + + '
' + template + '
' + + '
'; +}; - 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); +/** + * Wraps a content element in a md-panel-outer wrapper and + * positions it off-screen. Allows for proper control over positoning + * and animations. + * @param {!angular.JQLite} contentElement Element to be wrapped. + * @return {!angular.JQLite} Wrapper element. + * @private + */ +MdPanelService.prototype._wrapContentElement = function(contentElement) { + var wrapper = angular.element('
'); - if (calendarCtrl.focusDate && this.dateUtil.isSameDay(opt_date, calendarCtrl.focusDate)) { - this.focusAfterAppend = cell; - } - } else { - cell.classList.add('md-calendar-date-disabled'); - cell.textContent = cellText; - } - } + contentElement.addClass('md-panel').css('left', '-9999px'); + wrapper.append(contentElement); - return cell; - }; - + return wrapper; +}; + + +/***************************************************************************** + * MdPanelRef * + *****************************************************************************/ + + +/** + * A reference to a created panel. This reference contains a unique id for the + * panel, along with properties/functions used to control the panel. + * @param {!Object} config + * @param {!angular.$injector} $injector + * @final @constructor + */ +function MdPanelRef(config, $injector) { + // Injected variables. + /** @private @const {!angular.$q} */ + this._$q = $injector.get('$q'); + + /** @private @const {!angular.$mdCompiler} */ + this._$mdCompiler = $injector.get('$mdCompiler'); + + /** @private @const {!angular.$mdConstant} */ + this._$mdConstant = $injector.get('$mdConstant'); + + /** @private @const {!angular.$mdUtil} */ + this._$mdUtil = $injector.get('$mdUtil'); + + /** @private @const {!angular.$mdTheming} */ + this._$mdTheming = $injector.get('$mdTheming'); + + /** @private @const {!angular.Scope} */ + this._$rootScope = $injector.get('$rootScope'); + + /** @private @const {!angular.$animate} */ + this._$animate = $injector.get('$animate'); + + /** @private @const {!MdPanelRef} */ + this._$mdPanel = $injector.get('$mdPanel'); + + /** @private @const {!angular.$log} */ + this._$log = $injector.get('$log'); + + /** @private @const {!angular.$window} */ + this._$window = $injector.get('$window'); + + /** @private @const {!Function} */ + this._$$rAF = $injector.get('$$rAF'); + + // Public variables. /** - * Check whether date is in range and enabled - * @param {Date=} opt_date - * @return {boolean} Whether the date is enabled. + * Unique id for the panelRef. + * @type {string} */ - 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)); - } - + this.id = config.id; + + /** @type {!Object} */ + this.config = config; + + /** @type {!angular.JQLite|undefined} */ + this.panelContainer; + + /** @type {!angular.JQLite|undefined} */ + this.panelEl; + /** - * Builds a `tr` element for the calendar grid. - * @param rowNumber The week number within the month. - * @returns {HTMLElement} + * Whether the panel is attached. This is synchronous. When attach is called, + * isAttached is set to true. When detach is called, isAttached is set to + * false. + * @type {boolean} */ - CalendarMonthCtrl.prototype.buildDateRow = function(rowNumber) { - var row = document.createElement('tr'); - row.setAttribute('role', 'row'); + this.isAttached = false; - // 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)); + // Private variables. + /** @private {Array} */ + this._removeListeners = []; - return row; - }; + /** @private {!angular.JQLite|undefined} */ + this._topFocusTrap; + + /** @private {!angular.JQLite|undefined} */ + this._bottomFocusTrap; + + /** @private {!$mdPanel|undefined} */ + this._backdropRef; + + /** @private {Function?} */ + this._restoreScroll = null; /** - * Builds the content for the given date's month. - * @param {Date=} opt_dateInMonth - * @returns {DocumentFragment} A document fragment containing the elements. + * Keeps track of all the panel interceptors. + * @private {!Object} */ - CalendarMonthCtrl.prototype.buildCalendarForMonth = function(opt_dateInMonth) { - var date = this.dateUtil.isValidDate(opt_dateInMonth) ? opt_dateInMonth : new Date(); + this._interceptors = Object.create(null); - var firstDayOfMonth = this.dateUtil.getFirstDateOfMonth(date); - var firstDayOfTheWeek = this.getLocaleDay_(firstDayOfMonth); - var numberOfDaysInMonth = this.dateUtil.getNumberOfDaysInMonth(date); + /** + * Cleanup function, provided by `$mdCompiler` and assigned after the element + * has been compiled. When `contentElement` is used, the function is used to + * restore the element to it's proper place in the DOM. + * @private {!Function} + */ + this._compilerCleanup = null; + + /** + * Cache for saving and restoring element inline styles, CSS classes etc. + * @type {{styles: string, classes: string}} + */ + this._restoreCache = { + styles: '', + classes: '' + }; +} + + +MdPanelRef.interceptorTypes = { + CLOSE: 'onClose' +}; + + +/** + * Opens an already created and configured panel. If the panel is already + * visible, does nothing. + * @returns {!angular.$q.Promise} A promise that is resolved when + * the panel is opened and animations finish. + */ +MdPanelRef.prototype.open = function() { + var self = this; + return this._$q(function(resolve, reject) { + var done = self._done(resolve, self); + var show = self._simpleBind(self.show, self); + var checkGroupMaxOpen = function() { + if (self.config.groupName) { + angular.forEach(self.config.groupName, function(group) { + if (self._$mdPanel._openCountExceedsMaxOpen(group)) { + self._$mdPanel._closeFirstOpenedPanel(group); + } + }); + } + }; + + self.attach() + .then(show) + .then(checkGroupMaxOpen) + .then(done) + .catch(reject); + }); +}; + + +/** + * Closes the panel. + * @param {string} closeReason The event type that triggered the close. + * @returns {!angular.$q.Promise} A promise that is resolved when + * the panel is closed and animations finish. + */ +MdPanelRef.prototype.close = function(closeReason) { + var self = this; + + return this._$q(function(resolve, reject) { + self._callInterceptors(MdPanelRef.interceptorTypes.CLOSE).then(function() { + var done = self._done(resolve, self); + var detach = self._simpleBind(self.detach, self); + var onCloseSuccess = self.config['onCloseSuccess'] || angular.noop; + onCloseSuccess = angular.bind(self, onCloseSuccess, self, closeReason); + + self.hide() + .then(detach) + .then(done) + .then(onCloseSuccess) + .catch(reject); + }, reject); + }); +}; + + +/** + * Attaches the panel. The panel will be hidden afterwards. + * @returns {!angular.$q.Promise} A promise that is resolved when + * the panel is attached. + */ +MdPanelRef.prototype.attach = function() { + if (this.isAttached && this.panelEl) { + return this._$q.when(this); + } + + var self = this; + return this._$q(function(resolve, reject) { + var done = self._done(resolve, self); + var onDomAdded = self.config['onDomAdded'] || angular.noop; + var addListeners = function(response) { + self.isAttached = true; + self._addEventListeners(); + return response; + }; + + self._$q.all([ + self._createBackdrop(), + self._createPanel() + .then(addListeners) + .catch(reject) + ]).then(onDomAdded) + .then(done) + .catch(reject); + }); +}; + + +/** + * Only detaches the panel. Will NOT hide the panel first. + * @returns {!angular.$q.Promise} A promise that is resolved when + * the panel is detached. + */ +MdPanelRef.prototype.detach = function() { + if (!this.isAttached) { + return this._$q.when(this); + } + + var self = this; + var onDomRemoved = self.config['onDomRemoved'] || angular.noop; + + var detachFn = function() { + self._removeEventListeners(); + + // Remove the focus traps that we added earlier for keeping focus within + // the panel. + if (self._topFocusTrap && self._topFocusTrap.parentNode) { + self._topFocusTrap.parentNode.removeChild(self._topFocusTrap); + } + + if (self._bottomFocusTrap && self._bottomFocusTrap.parentNode) { + self._bottomFocusTrap.parentNode.removeChild(self._bottomFocusTrap); + } + + if (self._restoreCache.classes) { + self.panelEl[0].className = self._restoreCache.classes; + } + + // Either restore the saved styles or clear the ones set by mdPanel. + self.panelEl[0].style.cssText = self._restoreCache.styles || ''; + + self._compilerCleanup(); + self.panelContainer.remove(); + self.isAttached = false; + return self._$q.when(self); + }; + + if (this._restoreScroll) { + this._restoreScroll(); + this._restoreScroll = null; + } + + return this._$q(function(resolve, reject) { + var done = self._done(resolve, self); + + self._$q.all([ + detachFn(), + self._backdropRef ? self._backdropRef.detach() : true + ]).then(onDomRemoved) + .then(done) + .catch(reject); + }); +}; + + +/** + * Destroys the panel. The Panel cannot be opened again after this. + */ +MdPanelRef.prototype.destroy = function() { + var self = this; + if (this.config.groupName) { + angular.forEach(this.config.groupName, function(group) { + self.removeFromGroup(group); + }); + } + this.config.scope.$destroy(); + this.config.locals = null; + this._interceptors = null; +}; + + +/** + * Shows the panel. + * @returns {!angular.$q.Promise} A promise that is resolved when + * the panel has shown and animations finish. + */ +MdPanelRef.prototype.show = function() { + if (!this.panelContainer) { + return this._$q(function(resolve, reject) { + reject('mdPanel: Panel does not exist yet. Call open() or attach().'); + }); + } + + if (!this.panelContainer.hasClass(MD_PANEL_HIDDEN)) { + return this._$q.when(this); + } + + var self = this; + var animatePromise = function() { + self.panelContainer.removeClass(MD_PANEL_HIDDEN); + return self._animateOpen(); + }; + + return this._$q(function(resolve, reject) { + var done = self._done(resolve, self); + var onOpenComplete = self.config['onOpenComplete'] || angular.noop; + var addToGroupOpen = function() { + if (self.config.groupName) { + angular.forEach(self.config.groupName, function(group) { + self._$mdPanel._groups[group].openPanels.push(self); + }); + } + }; + + self._$q.all([ + self._backdropRef ? self._backdropRef.show() : self, + animatePromise().then(function() { self._focusOnOpen(); }, reject) + ]).then(onOpenComplete) + .then(addToGroupOpen) + .then(done) + .catch(reject); + }); +}; + + +/** + * Hides the panel. + * @returns {!angular.$q.Promise} A promise that is resolved when + * the panel has hidden and animations finish. + */ +MdPanelRef.prototype.hide = function() { + if (!this.panelContainer) { + return this._$q(function(resolve, reject) { + reject('mdPanel: Panel does not exist yet. Call open() or attach().'); + }); + } + + if (this.panelContainer.hasClass(MD_PANEL_HIDDEN)) { + return this._$q.when(this); + } + + var self = this; + + return this._$q(function(resolve, reject) { + var done = self._done(resolve, self); + var onRemoving = self.config['onRemoving'] || angular.noop; + var hidePanel = function() { + self.panelContainer.addClass(MD_PANEL_HIDDEN); + }; + var removeFromGroupOpen = function() { + if (self.config.groupName) { + var group, index; + angular.forEach(self.config.groupName, function(group) { + group = self._$mdPanel._groups[group]; + index = group.openPanels.indexOf(self); + if (index > -1) { + group.openPanels.splice(index, 1); + } + }); + } + }; + var focusOnOrigin = function() { + var origin = self.config['origin']; + if (origin) { + getElement(origin).focus(); + } + }; + + self._$q.all([ + self._backdropRef ? self._backdropRef.hide() : self, + self._animateClose() + .then(onRemoving) + .then(hidePanel) + .then(removeFromGroupOpen) + .then(focusOnOrigin) + .catch(reject) + ]).then(done, reject); + }); +}; + + +/** + * Add a class to the panel. DO NOT use this to hide/show the panel. + * @deprecated + * This method is in the process of being deprecated in favor of using the panel + * and container JQLite elements that are referenced in the MdPanelRef object. + * Full deprecation is scheduled for material 1.2. + * + * @param {string} newClass Class to be added. + * @param {boolean} toElement Whether or not to add the class to the panel + * element instead of the container. + */ +MdPanelRef.prototype.addClass = function(newClass, toElement) { + this._$log.warn( + 'mdPanel: The addClass method is in the process of being deprecated. ' + + 'Full deprecation is scheduled for the Angular Material 1.2 release. ' + + 'To achieve the same results, use the panelContainer or panelEl ' + + 'JQLite elements that are referenced in MdPanelRef.'); + + if (!this.panelContainer) { + throw new Error( + 'mdPanel: Panel does not exist yet. Call open() or attach().'); + } + + if (!toElement && !this.panelContainer.hasClass(newClass)) { + this.panelContainer.addClass(newClass); + } else if (toElement && !this.panelEl.hasClass(newClass)) { + this.panelEl.addClass(newClass); + } +}; + + +/** + * Remove a class from the panel. DO NOT use this to hide/show the panel. + * @deprecated + * This method is in the process of being deprecated in favor of using the panel + * and container JQLite elements that are referenced in the MdPanelRef object. + * Full deprecation is scheduled for material 1.2. + * + * @param {string} oldClass Class to be removed. + * @param {boolean} fromElement Whether or not to remove the class from the + * panel element instead of the container. + */ +MdPanelRef.prototype.removeClass = function(oldClass, fromElement) { + this._$log.warn( + 'mdPanel: The removeClass method is in the process of being deprecated. ' + + 'Full deprecation is scheduled for the Angular Material 1.2 release. ' + + 'To achieve the same results, use the panelContainer or panelEl ' + + 'JQLite elements that are referenced in MdPanelRef.'); + + if (!this.panelContainer) { + throw new Error( + 'mdPanel: Panel does not exist yet. Call open() or attach().'); + } + + if (!fromElement && this.panelContainer.hasClass(oldClass)) { + this.panelContainer.removeClass(oldClass); + } else if (fromElement && this.panelEl.hasClass(oldClass)) { + this.panelEl.removeClass(oldClass); + } +}; + + +/** + * Toggle a class on the panel. DO NOT use this to hide/show the panel. + * @deprecated + * This method is in the process of being deprecated in favor of using the panel + * and container JQLite elements that are referenced in the MdPanelRef object. + * Full deprecation is scheduled for material 1.2. + * + * @param {string} toggleClass The class to toggle. + * @param {boolean} onElement Whether or not to toggle the class on the panel + * element instead of the container. + */ +MdPanelRef.prototype.toggleClass = function(toggleClass, onElement) { + this._$log.warn( + 'mdPanel: The toggleClass method is in the process of being deprecated. ' + + 'Full deprecation is scheduled for the Angular Material 1.2 release. ' + + 'To achieve the same results, use the panelContainer or panelEl ' + + 'JQLite elements that are referenced in MdPanelRef.'); + + if (!this.panelContainer) { + throw new Error( + 'mdPanel: Panel does not exist yet. Call open() or attach().'); + } + + if (!onElement) { + this.panelContainer.toggleClass(toggleClass); + } else { + this.panelEl.toggleClass(toggleClass); + } +}; - // 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); +/** + * Compiles the panel, according to the passed in config and appends it to + * the DOM. Helps normalize differences in the compilation process between + * using a string template and a content element. + * @returns {!angular.$q.Promise} Promise that is resolved when + * the element has been compiled and added to the DOM. + * @private + */ +MdPanelRef.prototype._compile = function() { + var self = this; - // 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; + // Compile the element via $mdCompiler. Note that when using a + // contentElement, the element isn't actually being compiled, rather the + // compiler saves it's place in the DOM and provides a way of restoring it. + return self._$mdCompiler.compile(self.config).then(function(compileData) { + var config = self.config; - // 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'); + if (config.contentElement) { + var panelEl = compileData.element; - var monthLabelRow = this.buildDateRow(); - monthLabelRow.appendChild(monthLabelCell); - monthBody.insertBefore(monthLabelRow, row); + // Since mdPanel modifies the inline styles and CSS classes, we need + // to save them in order to be able to restore on close. + self._restoreCache.styles = panelEl[0].style.cssText; + self._restoreCache.classes = panelEl[0].className; - if (isFinalMonth) { - return monthBody; - } + self.panelContainer = self._$mdPanel._wrapContentElement(panelEl); + self.panelEl = panelEl; } else { - blankCellOffset = 2; - monthLabelCell.setAttribute('colspan', '2'); - row.appendChild(monthLabelCell); + self.panelContainer = compileData.link(config['scope']); + self.panelEl = angular.element( + self.panelContainer[0].querySelector('.md-panel') + ); } - // 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()); - } + // Save a reference to the cleanup function from the compiler. + self._compilerCleanup = compileData.cleanup; - // 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); - } + // Attach the panel to the proper place in the DOM. + getElement(self.config['attachTo']).append(self.panelContainer); - iterationDate.setDate(d); - var cell = this.buildDateCell(iterationDate); - row.appendChild(cell); + return self; + }); +}; - dayOfWeek++; - } - // Ensure that the last row of the month has 7 cells. - while (row.childNodes.length < 7) { - row.appendChild(this.buildDateCell()); +/** + * Creates a panel and adds it to the dom. + * @returns {!angular.$q.Promise} A promise that is resolved when the panel is + * created. + * @private + */ +MdPanelRef.prototype._createPanel = function() { + var self = this; + + return this._$q(function(resolve, reject) { + if (!self.config.locals) { + self.config.locals = {}; } - // 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()); + self.config.locals.mdPanelRef = self; + + self._compile().then(function() { + if (self.config['disableParentScroll']) { + self._restoreScroll = self._$mdUtil.disableScrollAround( + null, + self.panelContainer, + { disableScrollMask: true } + ); } - monthBody.appendChild(whitespaceRow); - } - return monthBody; - }; + // Add a custom CSS class to the panel element. + if (self.config['panelClass']) { + self.panelEl.addClass(self.config['panelClass']); + } - /** - * 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 - }; -})(); + // Handle click and touch events for the panel container. + if (self.config['propagateContainerEvents']) { + self.panelContainer.css('pointer-events', 'none'); + } -})(); -(function(){ -"use strict"; + // Panel may be outside the $rootElement, tell ngAnimate to animate + // regardless. + if (self._$animate.pin) { + self._$animate.pin( + self.panelContainer, + getElement(self.config['attachTo']) + ); + } -(function() { - 'use strict'; + self._configureTrapFocus(); + self._addStyles().then(function() { + resolve(self); + }, reject); + }, reject); - /** - * @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 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; +/** + * Adds the styles for the panel, such as positioning and z-index. Also, + * themes the panel element and panel container using `$mdTheming`. + * @returns {!angular.$q.Promise} + * @private + */ +MdPanelRef.prototype._addStyles = function() { + var self = this; + return this._$q(function(resolve) { + self.panelContainer.css('z-index', self.config['zIndex']); + self.panelEl.css('z-index', self.config['zIndex'] + 1); - /** Array of abbreviated month names. E.g., ['Jan', 'Feb', ...] */ - this.shortMonths = null; + var hideAndResolve = function() { + // Theme the element and container. + self._setTheming(); - /** Array of full day of the week names. E.g., ['Monday', 'Tuesday', ...] */ - this.days = null; + // Remove left: -9999px and add hidden class. + self.panelEl.css('left', ''); + self.panelContainer.addClass(MD_PANEL_HIDDEN); - /** Array of abbreviated dat of the week names. E.g., ['M', 'T', ...] */ - this.shortDays = null; + resolve(self); + }; - /** Array of dates of a month (1 - 31). Characters might be different in some locales. */ - this.dates = null; + if (self.config['fullscreen']) { + self.panelEl.addClass('_md-panel-fullscreen'); + hideAndResolve(); + return; // Don't setup positioning. + } - /** Index of the first day of the week. 0 = Sunday, 1 = Monday, etc. */ - this.firstDayOfWeek = 0; + var positionConfig = self.config['position']; + if (!positionConfig) { + hideAndResolve(); + return; // Don't setup positioning. + } - /** - * Function that converts the date portion of a Date to a string. - * @type {(function(Date): string)} - */ - this.formatDate = null; + // Wait for angular to finish processing the template + self._$rootScope['$$postDigest'](function() { + // Position it correctly. This is necessary so that the panel will have a + // defined height and width. + self._updatePosition(true); - /** - * Function that converts a date string to a Date object (the date portion) - * @type {function(string): Date} - */ - this.parseDate = null; + // Theme the element and container. + self._setTheming(); - /** - * Function that formats a Date into a month header string. - * @type {function(Date): string} - */ - this.monthHeaderFormatter = null; + resolve(self); + }); + }); +}; - /** - * 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; +/** + * Sets the `$mdTheming` classes on the `panelContainer` and `panelEl`. + * @private + */ +MdPanelRef.prototype._setTheming = function() { + this._$mdTheming(this.panelEl); + this._$mdTheming(this.panelContainer); +}; - /** - * 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 = ''; - } +/** + * Updates the position configuration of a panel + * @param {!MdPanelPosition} position + */ +MdPanelRef.prototype.updatePosition = function(position) { + if (!this.panelContainer) { + throw new Error( + 'mdPanel: Panel does not exist yet. Call open() or attach().'); + } - /** - * 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 ''; - } + this.config['position'] = position; + this._updatePosition(); +}; + + +/** + * Calculates and updates the position of the panel. + * @param {boolean=} init + * @private + */ +MdPanelRef.prototype._updatePosition = function(init) { + var positionConfig = this.config['position']; - // 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); - } + if (positionConfig) { + positionConfig._setPanelPosition(this.panelEl); - return formatDate.toLocaleDateString(); - } + // Hide the panel now that position is known. + if (init) { + this.panelContainer.addClass(MD_PANEL_HIDDEN); + } - /** - * Default string-to-date parsing function. - * @param {string} dateString - * @returns {!Date} - */ - function defaultParseDate(dateString) { - return new Date(dateString); - } + this.panelEl.css( + MdPanelPosition.absPosition.TOP, + positionConfig.getTop() + ); + this.panelEl.css( + MdPanelPosition.absPosition.BOTTOM, + positionConfig.getBottom() + ); + this.panelEl.css( + MdPanelPosition.absPosition.LEFT, + positionConfig.getLeft() + ); + this.panelEl.css( + MdPanelPosition.absPosition.RIGHT, + positionConfig.getRight() + ); + } +}; - /** - * 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); - } +/** + * Focuses on the panel or the first focus target. + * @private + */ +MdPanelRef.prototype._focusOnOpen = function() { + if (this.config['focusOnOpen']) { + // Wait for the template to finish rendering to guarantee md-autofocus has + // finished adding the class md-autofocus, otherwise the focusable element + // isn't available to focus. + var self = this; + this._$rootScope['$$postDigest'](function() { + var target = self._$mdUtil.findFocusTarget(self.panelEl) || + self.panelEl; + target.focus(); + }); + } +}; - /** - * 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; - } +/** + * Shows the backdrop. + * @returns {!angular.$q.Promise} A promise that is resolved when the backdrop + * is created and attached. + * @private + */ +MdPanelRef.prototype._createBackdrop = function() { + if (this.config.hasBackdrop) { + if (!this._backdropRef) { + var backdropAnimation = this._$mdPanel.newPanelAnimation() + .openFrom(this.config.attachTo) + .withAnimation({ + open: '_md-opaque-enter', + close: '_md-opaque-leave' + }); - /** - * 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(' '); + if (this.config.animation) { + backdropAnimation.duration(this.config.animation._rawDuration); } - // 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]; - }); + var backdropConfig = { + animation: backdropAnimation, + attachTo: this.config.attachTo, + focusOnOpen: false, + panelClass: '_md-panel-backdrop', + zIndex: this.config.zIndex - 1 + }; - // The default dates are simply the numbers 1 through 31. - var defaultDates = Array(32); - for (var i = 1; i <= 31; i++) { - defaultDates[i] = i; - } + this._backdropRef = this._$mdPanel.create(backdropConfig); + } + if (!this._backdropRef.isAttached) { + return this._backdropRef.attach(); + } + } +}; - // 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 - }; +/** + * Listen for escape keys and outside clicks to auto close. + * @private + */ +MdPanelRef.prototype._addEventListeners = function() { + this._configureEscapeToClose(); + this._configureClickOutsideToClose(); + this._configureScrollListener(); +}; - return service; - }; - DateLocaleProvider.prototype.$get.$inject = ["$locale"]; - $provide.provider('$mdDateLocale', new DateLocaleProvider()); - }]); -})(); +/** + * Remove event listeners added in _addEventListeners. + * @private + */ +MdPanelRef.prototype._removeEventListeners = function() { + this._removeListeners && this._removeListeners.forEach(function(removeFn) { + removeFn(); + }); + this._removeListeners = []; +}; -})(); -(function(){ -"use strict"; -(function() { - 'use strict'; +/** + * Setup the escapeToClose event listeners. + * @private + */ +MdPanelRef.prototype._configureEscapeToClose = function() { + if (this.config['escapeToClose']) { + var parentTarget = getElement(this.config['attachTo']); + var self = this; - // 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 + var keyHandlerFn = function(ev) { + if (ev.keyCode === self._$mdConstant.KEY_CODE.ESCAPE) { + ev.stopPropagation(); + ev.preventDefault(); + self.close(MdPanelRef.closeReasons.ESCAPE); + } + }; - angular.module('material.components.datepicker') - .directive('mdDatepicker', datePickerDirective); + // Add keydown listeners + this.panelContainer.on('keydown', keyHandlerFn); + parentTarget.on('keydown', keyHandlerFn); - /** - * @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. - '' + - '
' + - '' + - '' + - '
' + - '
' + - '
' + + // Queue remove listeners function + this._removeListeners.push(function() { + self.panelContainer.off('keydown', keyHandlerFn); + parentTarget.off('keydown', keyHandlerFn); + }); + } +}; - // 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.'); +/** + * Setup the clickOutsideToClose event listeners. + * @private + */ +MdPanelRef.prototype._configureClickOutsideToClose = function() { + if (this.config['clickOutsideToClose']) { + var target = this.config['propagateContainerEvents'] ? + angular.element(document.body) : + this.panelContainer; + var sourceEl; + + // Keep track of the element on which the mouse originally went down + // so that we can only close the backdrop when the 'click' started on it. + // A simple 'click' handler does not work, it sets the target object as the + // element the mouse went down on. + var mousedownHandler = function(ev) { + sourceEl = ev.target; + }; + + // We check if our original element and the target is the backdrop + // because if the original was the backdrop and the target was inside the + // panel we don't want to panel to close. + var self = this; + var mouseupHandler = function(ev) { + if (self.config['propagateContainerEvents']) { + + // We check if the sourceEl of the event is the panel element or one + // of it's children. If it is not, then close the panel. + if (sourceEl !== self.panelEl[0] && !self.panelEl[0].contains(sourceEl)) { + self.close(); } - mdDatePickerCtrl.configureNgModel(ngModelCtrl); + } else if (sourceEl === target[0] && ev.target === target[0]) { + ev.stopPropagation(); + ev.preventDefault(); + + self.close(MdPanelRef.closeReasons.CLICK_OUTSIDE); } }; - } - - /** 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'; + // Add listeners + target.on('mousedown', mousedownHandler); + target.on('mouseup', mouseupHandler); - /** Default time in ms to debounce input event by. */ - var DEFAULT_DEBOUNCE_INTERVAL = 500; + // Queue remove listeners function + this._removeListeners.push(function() { + target.off('mousedown', mousedownHandler); + target.off('mouseup', mouseupHandler); + }); + } +}; - /** - * 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; +/** + * Configures the listeners for updating the panel position on scroll. + * @private +*/ +MdPanelRef.prototype._configureScrollListener = function() { + // No need to bind the event if scrolling is disabled. + if (!this.config['disableParentScroll']) { + var updatePosition = angular.bind(this, this._updatePosition); + var debouncedUpdatePosition = this._$$rAF.throttle(updatePosition); + var self = this; - /** - * Controller for md-datepicker. - * - * @ngInject @constructor - */ - function DatePickerCtrl($scope, $element, $attrs, $compile, $timeout, $window, - $mdConstant, $mdTheming, $mdUtil, $mdDateLocale, $$mdDateUtil, $$rAF) { - /** @final */ - this.$compile = $compile; + var onScroll = function() { + debouncedUpdatePosition(); + }; - /** @final */ - this.$timeout = $timeout; + // Add listeners. + this._$window.addEventListener('scroll', onScroll, true); - /** @final */ - this.$window = $window; + // Queue remove listeners function. + this._removeListeners.push(function() { + self._$window.removeEventListener('scroll', onScroll, true); + }); + } +}; - /** @final */ - this.dateLocale = $mdDateLocale; - /** @final */ - this.dateUtil = $$mdDateUtil; +/** + * Setup the focus traps. These traps will wrap focus when tabbing past the + * panel. When shift-tabbing, the focus will stick in place. + * @private + */ +MdPanelRef.prototype._configureTrapFocus = function() { + // Focus doesn't remain inside of the panel without this. + this.panelEl.attr('tabIndex', '-1'); + if (this.config['trapFocus']) { + var element = this.panelEl; + // Set up elements before and after the panel to capture focus and + // redirect back into the panel. + this._topFocusTrap = FOCUS_TRAP_TEMPLATE.clone()[0]; + this._bottomFocusTrap = FOCUS_TRAP_TEMPLATE.clone()[0]; + + // When focus is about to move out of the panel, we want to intercept it + // and redirect it back to the panel element. + var focusHandler = function() { + element.focus(); + }; + this._topFocusTrap.addEventListener('focus', focusHandler); + this._bottomFocusTrap.addEventListener('focus', focusHandler); - /** @final */ - this.$mdConstant = $mdConstant; + // Queue remove listeners function + this._removeListeners.push(this._simpleBind(function() { + this._topFocusTrap.removeEventListener('focus', focusHandler); + this._bottomFocusTrap.removeEventListener('focus', focusHandler); + }, this)); - /* @final */ - this.$mdUtil = $mdUtil; + // The top focus trap inserted immediately before the md-panel element (as + // a sibling). The bottom focus trap inserted immediately after the + // md-panel element (as a sibling). + element[0].parentNode.insertBefore(this._topFocusTrap, element[0]); + element.after(this._bottomFocusTrap); + } +}; - /** @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); +/** + * Updates the animation of a panel. + * @param {!MdPanelAnimation} animation + */ +MdPanelRef.prototype.updateAnimation = function(animation) { + this.config['animation'] = animation; - /** @type {!angular.NgModelController} */ - this.ngModelCtrl = null; + if (this._backdropRef) { + this._backdropRef.config.animation.duration(animation._rawDuration); + } +}; - /** @type {HTMLInputElement} */ - this.inputElement = $element[0].querySelector('input'); - /** @final {!angular.JQLite} */ - this.ngInputElement = angular.element(this.inputElement); +/** + * Animate the panel opening. + * @returns {!angular.$q.Promise} A promise that is resolved when the panel has + * animated open. + * @private + */ +MdPanelRef.prototype._animateOpen = function() { + this.panelContainer.addClass('md-panel-is-showing'); + var animationConfig = this.config['animation']; + if (!animationConfig) { + // Promise is in progress, return it. + this.panelContainer.addClass('_md-panel-shown'); + return this._$q.when(this); + } - /** @type {HTMLElement} */ - this.inputContainer = $element[0].querySelector('.md-datepicker-input-container'); + var self = this; + return this._$q(function(resolve) { + var done = self._done(resolve, self); + var warnAndOpen = function() { + self._$log.warn( + 'mdPanel: MdPanel Animations failed. ' + + 'Showing panel without animating.'); + done(); + }; - /** @type {HTMLElement} Floating calendar pane. */ - this.calendarPane = $element[0].querySelector('.md-datepicker-calendar-pane'); + animationConfig.animateOpen(self.panelEl) + .then(done, warnAndOpen); + }); +}; - /** @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'); +/** + * Animate the panel closing. + * @returns {!angular.$q.Promise} A promise that is resolved when the panel has + * animated closed. + * @private + */ +MdPanelRef.prototype._animateClose = function() { + var animationConfig = this.config['animation']; + if (!animationConfig) { + this.panelContainer.removeClass('md-panel-is-showing'); + this.panelContainer.removeClass('_md-panel-shown'); + return this._$q.when(this); + } - /** @final {!angular.JQLite} */ - this.$element = $element; + var self = this; + return this._$q(function(resolve) { + var done = function() { + self.panelContainer.removeClass('md-panel-is-showing'); + resolve(self); + }; + var warnAndClose = function() { + self._$log.warn( + 'mdPanel: MdPanel Animations failed. ' + + 'Hiding panel without animating.'); + done(); + }; - /** @final {!angular.Attributes} */ - this.$attrs = $attrs; + animationConfig.animateClose(self.panelEl) + .then(done, warnAndClose); + }); +}; - /** @final {!angular.Scope} */ - this.$scope = $scope; - /** @type {Date} */ - this.date = null; +/** + * Registers a interceptor with the panel. The callback should return a promise, + * which will allow the action to continue when it gets resolved, or will + * prevent an action if it is rejected. + * @param {string} type Type of interceptor. + * @param {!angular.$q.Promise} callback Callback to be registered. + * @returns {!MdPanelRef} + */ +MdPanelRef.prototype.registerInterceptor = function(type, callback) { + var error = null; - /** @type {boolean} */ - this.isFocused = false; + if (!angular.isString(type)) { + error = 'Interceptor type must be a string, instead got ' + typeof type; + } else if (!angular.isFunction(callback)) { + error = 'Interceptor callback must be a function, instead got ' + typeof callback; + } - /** @type {boolean} */ - this.isDisabled; - this.setDisabled($element[0].disabled || angular.isString($attrs['disabled'])); + if (error) { + throw new Error('MdPanel: ' + error); + } - /** @type {boolean} Whether the date-picker's calendar pane is open. */ - this.isCalendarOpen = false; + var interceptors = this._interceptors[type] = this._interceptors[type] || []; - /** - * 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; + if (interceptors.indexOf(callback) === -1) { + interceptors.push(callback); + } - this.calendarPane.id = 'md-date-pane' + $mdUtil.nextUid(); + return this; +}; - $mdTheming($element); - /** Pre-bound click handler is saved so that the event listener can be removed. */ - this.bodyClickHandler = angular.bind(this, this.handleBodyClick); +/** + * Removes a registered interceptor. + * @param {string} type Type of interceptor to be removed. + * @param {Function} callback Interceptor to be removed. + * @returns {!MdPanelRef} + */ +MdPanelRef.prototype.removeInterceptor = function(type, callback) { + var index = this._interceptors[type] ? + this._interceptors[type].indexOf(callback) : -1; - /** Pre-bound resize handler so that the event listener can be removed. */ - this.windowResizeHandler = $mdUtil.debounce(angular.bind(this, this.closeCalendarPane), 100); + if (index > -1) { + this._interceptors[type].splice(index, 1); + } - // 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'); - } + return this; +}; - this.installPropertyInterceptors(); - this.attachChangeListeners(); - this.attachInteractionListeners(); - var self = this; - $scope.$on('$destroy', function() { - self.detachCalendarPane(); - }); +/** + * Removes all interceptors. + * @param {string=} type Type of interceptors to be removed. + * If ommited, all interceptors types will be removed. + * @returns {!MdPanelRef} + */ +MdPanelRef.prototype.removeAllInterceptors = function(type) { + if (type) { + this._interceptors[type] = []; + } else { + this._interceptors = Object.create(null); } - 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; + return this; +}; - 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)); - } +/** + * Invokes all the interceptors of a certain type sequantially in + * reverse order. Works in a similar way to `$q.all`, except it + * respects the order of the functions. + * @param {string} type Type of interceptors to be invoked. + * @returns {!angular.$q.Promise} + * @private + */ +MdPanelRef.prototype._callInterceptors = function(type) { + var self = this; + var $q = self._$q; + var interceptors = self._interceptors && self._interceptors[type] || []; - self.date = value; - self.inputElement.value = self.dateLocale.formatDate(value); - self.resizeInputElement(); - self.updateErrorState(); - }; - }; + return interceptors.reduceRight(function(promise, interceptor) { + var isPromiseLike = interceptor && angular.isFunction(interceptor.then); + var response = isPromiseLike ? interceptor : null; - /** - * 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; + /** + * For interceptors to reject/cancel subsequent portions of the chain, simply + * return a `$q.reject()` + */ + return promise.then(function() { + if (!response) { + try { + response = interceptor(self); + } catch(e) { + response = $q.reject(e); + } + } - 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(); + return response; }); + }, $q.resolve(self)); +}; - 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; +/** + * Faster, more basic than angular.bind + * http://jsperf.com/angular-bind-vs-custom-vs-native + * @param {function} callback + * @param {!Object} self + * @return {function} Callback function with a bound self. + */ +MdPanelRef.prototype._simpleBind = function(callback, self) { + return function(value) { + return callback.apply(self, value); + }; +}; - // 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(); - }); +/** + * @param {function} callback + * @param {!Object} self + * @return {function} Callback function with a self param. + */ +MdPanelRef.prototype._done = function(callback, self) { + return function() { + callback(self); }; +}; - /** - * 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; +/** + * Adds a panel to a group if the panel does not exist within the group already. + * A panel can only exist within a single group. + * @param {string} groupName The name of the group. + */ +MdPanelRef.prototype.addToGroup = function(groupName) { + if (!this._$mdPanel._groups[groupName]) { + this._$mdPanel.newPanelGroup(groupName); + } - if (scope) { - scope.$watch(this.$attrs['ngDisabled'], function(isDisabled) { - self.setDisabled(isDisabled); - }); - } - } + var group = this._$mdPanel._groups[groupName]; + var index = group.panels.indexOf(this); - Object.defineProperty(this, 'placeholder', { - get: function() { return self.inputElement.placeholder; }, - set: function(value) { self.inputElement.placeholder = value || ''; } - }); - }; + if (index < 0) { + group.panels.push(this); + } +}; - /** - * 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; +/** + * Removes a panel from a group if the panel exists within that group. The group + * must be created ahead of time. + * @param {string} groupName The name of the group. + */ +MdPanelRef.prototype.removeFromGroup = function(groupName) { + if (!this._$mdPanel._groups[groupName]) { + throw new Error('mdPanel: The group ' + groupName + ' does not exist.'); + } - // Clear any existing errors to get rid of anything that's no longer relevant. - this.clearErrorState(); + var group = this._$mdPanel._groups[groupName]; + var index = group.panels.indexOf(this); - if (this.dateUtil.isValidDate(date)) { - // Force all dates to midnight in order to ignore the time portion. - date = this.dateUtil.createDateAtMidnight(date); + if (index > -1) { + group.panels.splice(index, 1); + } +}; - 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); - } +/** + * Possible default closeReasons for the close function. + * @enum {string} + */ +MdPanelRef.closeReasons = { + CLICK_OUTSIDE: 'clickOutsideToClose', + ESCAPE: 'escapeToClose', +}; + + +/***************************************************************************** + * MdPanelPosition * + *****************************************************************************/ + + +/** + * Position configuration object. To use, create an MdPanelPosition with the + * desired properties, then pass the object as part of $mdPanel creation. + * + * Example: + * + * var panelPosition = new MdPanelPosition() + * .relativeTo(myButtonEl) + * .addPanelPosition( + * $mdPanel.xPosition.CENTER, + * $mdPanel.yPosition.ALIGN_TOPS + * ); + * + * $mdPanel.create({ + * position: panelPosition + * }); + * + * @param {!angular.$injector} $injector + * @final @constructor + */ +function MdPanelPosition($injector) { + /** @private @const {!angular.$window} */ + this._$window = $injector.get('$window'); - // 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); - } - }; + /** @private {boolean} */ + this._isRTL = $injector.get('$mdUtil').bidi() === 'rtl'; - /** 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); - }; + /** @private @const {!angular.$mdConstant} */ + this._$mdConstant = $injector.get('$mdConstant'); - /** 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; - }; + /** @private {boolean} */ + this._absolute = false; - /** - * 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); + /** @private {!angular.JQLite} */ + this._relativeToEl; - // 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) - ); + /** @private {string} */ + this._top = ''; - // The datepicker's model is only updated when there is a valid input. - if (isValidInput) { - this.ngModelCtrl.$setViewValue(parsedDate); - this.date = parsedDate; - } + /** @private {string} */ + this._bottom = ''; - 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'); + /** @private {string} */ + this._left = ''; - var elementRect = this.inputContainer.getBoundingClientRect(); - var bodyRect = document.body.getBoundingClientRect(); + /** @private {string} */ + this._right = ''; - // 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; + /** @private {!Array} */ + this._translateX = []; - // 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; + /** @private {!Array} */ + this._translateY = []; - var viewportLeft = (bodyRect.left < 0 && document.body.scrollLeft == 0) ? - -bodyRect.left : - document.body.scrollLeft; + /** @private {!Array<{x:string, y:string}>} */ + this._positions = []; - var viewportBottom = viewportTop + this.$window.innerHeight; - var viewportRight = viewportLeft + this.$window.innerWidth; + /** @private {?{x:string, y:string}} */ + this._actualPosition; +} - // 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'); - } +/** + * Possible values of xPosition. + * @enum {string} + */ +MdPanelPosition.xPosition = { + CENTER: 'center', + ALIGN_START: 'align-start', + ALIGN_END: 'align-end', + OFFSET_START: 'offset-start', + OFFSET_END: 'offset-end' +}; - // 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); +/** + * Possible values of yPosition. + * @enum {string} + */ +MdPanelPosition.yPosition = { + CENTER: 'center', + ALIGN_TOPS: 'align-tops', + ALIGN_BOTTOMS: 'align-bottoms', + ABOVE: 'above', + BELOW: 'below' +}; - // 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'); - }); - }; +/** + * Possible values of absolute position. + * @enum {string} + */ +MdPanelPosition.absPosition = { + TOP: 'top', + RIGHT: 'right', + BOTTOM: 'bottom', + LEFT: 'left' +}; - /** 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'); +/** + * Margin between the edges of a panel and the viewport. + * @const {number} + */ +MdPanelPosition.viewportMargin = 8; - 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; +/** + * Sets absolute positioning for the panel. + * @return {!MdPanelPosition} + */ +MdPanelPosition.prototype.absolute = function() { + this._absolute = true; + return this; +}; - // 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 `', - ' ', - ' ', - ' ', - ' ', - ' {{ dialog.cancel }}', - ' ', - ' ', - ' {{ dialog.ok }}', - ' ', - ' ', - '' - ].join('').replace(/\s\s+/g, ''), - controller: function mdDialogCtrl() { - this.hide = function() { - $mdDialog.hide(this.$type === 'prompt' ? this.result : true); - }; - this.abort = function() { - $mdDialog.cancel(); - }; - this.keypress = function($event) { - if ($event.keyCode === $mdConstant.KEY_CODE.ENTER) { - $mdDialog.hide(this.result) - } - } - }, - controllerAs: 'dialog', - bindToController: true, - theme: $mdTheming.defaultTheme() - }; - } - - /* @ngInject */ - function dialogDefaultOptions($mdDialog, $mdAria, $mdUtil, $mdConstant, $animate, $document, $window, $rootElement, $log, $injector) { - return { - hasBackdrop: true, - isolateScope: true, - onShow: onShow, - onShowing: beforeShow, - onRemove: onRemove, - clickOutsideToClose: false, - escapeToClose: true, - targetEvent: null, - closeTo: null, - openFrom: null, - focusOnOpen: true, - disableParentScroll: true, - autoWrap: true, - fullscreen: false, - transformTemplate: function(template, options) { - // Make the dialog container focusable, because otherwise the focus will be always redirected to - // an element outside of the container, and the focus trap won't work probably.. - // Also the tabindex is needed for the `escapeToClose` functionality, because - // the keyDown event can't be triggered when the focus is outside of the container. - return '
' + validatedTemplate(template) + '
'; - - /** - * The specified template should contain a wrapper element.... - */ - function validatedTemplate(template) { - if (options.autoWrap && !/<\/md-dialog>/g.test(template)) { - return '' + (template || '') + ''; - } else { - return template || ''; - } - } - } - }; - - function beforeShow(scope, element, options, controller) { - if (controller) { - controller.mdHtmlContent = controller.htmlContent || options.htmlContent || ''; - controller.mdTextContent = controller.textContent || options.textContent || - controller.content || options.content || ''; - if (controller.mdHtmlContent && !$injector.has('$sanitize')) { - throw Error('The ngSanitize module must be loaded in order to use htmlContent.'); - } +/***************************************************************************** + * IMPLEMENTATION * + *****************************************************************************/ - if (controller.mdHtmlContent && controller.mdTextContent) { - throw Error('md-dialog cannot have both `htmlContent` and `textContent`'); - } +function MdNavBar($mdAria, $mdTheming) { + return { + restrict: 'E', + transclude: true, + controller: MdNavBarController, + controllerAs: 'ctrl', + bindToController: true, + scope: { + 'mdSelectedNavItem': '=?', + 'mdNoInkBar': '=?', + 'navBarAriaLabel': '@?', + }, + template: + '
' + + '' + + '' + + '
', + link: function(scope, element, attrs, ctrl) { + $mdTheming(element); + if (!ctrl.navBarAriaLabel) { + $mdAria.expectAsync(element, 'aria-label', angular.noop); } - } + }, + }; +} - /** Show method for dialogs */ - function onShow(scope, element, options, controller) { - angular.element($document[0].body).addClass('md-dialog-is-showing'); +/** + * Controller for the nav-bar component. + * + * Accessibility functionality is implemented as a site navigator with a + * listbox, according to + * https://www.w3.org/TR/wai-aria-practices/#Site_Navigator_Tabbed_Style + * @param {!angular.JQLite} $element + * @param {!angular.Scope} $scope + * @param {!angular.Timeout} $timeout + * @param {!Object} $mdConstant + * @constructor + * @final + * @ngInject + */ +function MdNavBarController($element, $scope, $timeout, $mdConstant) { + // Injected variables + /** @private @const {!angular.Timeout} */ + this._$timeout = $timeout; - captureParentAndFromToElements(options); - configureAria(element.find('md-dialog'), options); - showBackdrop(scope, element, options); + /** @private @const {!angular.Scope} */ + this._$scope = $scope; - return dialogPopIn(element, options) - .then(function() { - activateListeners(element, options); - lockScreenReader(element, options); - warnDeprecatedActions(); - focusOnOpen(); - }); + /** @private @const {!Object} */ + this._$mdConstant = $mdConstant; - /** - * Check to see if they used the deprecated .md-actions class and log a warning - */ - function warnDeprecatedActions() { - var badActions = element[0].querySelectorAll('.md-actions'); + // Data-bound variables. + /** @type {string} */ + this.mdSelectedNavItem; - if (badActions.length > 0) { - $log.warn('Using a class of md-actions is deprected, please use .'); - } - } + /** @type {string} */ + this.navBarAriaLabel; - /** - * For alerts, focus on content... otherwise focus on - * the close button (or equivalent) - */ - function focusOnOpen() { - if (options.focusOnOpen) { - var target = $mdUtil.findFocusTarget(element) || findCloseButton(); - target.focus(); - } + // State variables. - /** - * If no element with class dialog-close, try to find the last - * button child in md-actions and assume it is a close button. - * - * If we find no actions at all, log a warning to the console. - */ - function findCloseButton() { - var closeButton = element[0].querySelector('.dialog-close'); - if (!closeButton) { - var actionButtons = element[0].querySelectorAll('.md-actions button, md-dialog-actions button'); - closeButton = actionButtons[actionButtons.length - 1]; - } - return angular.element(closeButton); - } - } + /** @type {?angular.JQLite} */ + this._navBarEl = $element[0]; + + /** @type {?angular.JQLite} */ + this._inkbar; + + var self = this; + // need to wait for transcluded content to be available + var deregisterTabWatch = this._$scope.$watch(function() { + return self._navBarEl.querySelectorAll('._md-nav-button').length; + }, + function(newLength) { + if (newLength > 0) { + self._initTabs(); + deregisterTabWatch(); } + }); +} - /** - * Remove function for all dialogs - */ - function onRemove(scope, element, options) { - options.deactivateListeners(); - options.unlockScreenReader(); - options.hideBackdrop(options.$destroy); - // Remove the focus traps that we added earlier for keeping focus within the dialog. - if (topFocusTrap && topFocusTrap.parentNode) { - topFocusTrap.parentNode.removeChild(topFocusTrap); - } - if (bottomFocusTrap && bottomFocusTrap.parentNode) { - bottomFocusTrap.parentNode.removeChild(bottomFocusTrap); - } +/** + * Initializes the tab components once they exist. + * @private + */ +MdNavBarController.prototype._initTabs = function() { + this._inkbar = angular.element(this._navBarEl.querySelector('md-nav-ink-bar')); - // For navigation $destroy events, do a quick, non-animated removal, - // but for normal closes (from clicks, etc) animate the removal - return !!options.$destroy ? detachAndClean() : animateRemoval().then( detachAndClean ); + var self = this; + this._$timeout(function() { + self._updateTabs(self.mdSelectedNavItem, undefined); + }); - /** - * For normal closes, animate the removal. - * For forced closes (like $destroy events), skip the animations - */ - function animateRemoval() { - return dialogPopOut(element, options); - } + this._$scope.$watch('ctrl.mdSelectedNavItem', function(newValue, oldValue) { + // Wait a digest before update tabs for products doing + // anything dynamic in the template. + self._$timeout(function() { + self._updateTabs(newValue, oldValue); + }); + }); +}; - /** - * Detach the element - */ - function detachAndClean() { - angular.element($document[0].body).removeClass('md-dialog-is-showing'); - element.remove(); +/** + * Set the current tab to be selected. + * @param {string|undefined} newValue New current tab name. + * @param {string|undefined} oldValue Previous tab name. + * @private + */ +MdNavBarController.prototype._updateTabs = function(newValue, oldValue) { + var self = this; + var tabs = this._getTabs(); - if (!options.$destroy) options.origin.focus(); - } - } + // this._getTabs can return null if nav-bar has not yet been initialized + if(!tabs) + return; - /** - * Capture originator/trigger/from/to element information (if available) - * and the parent container for the dialog; defaults to the $rootElement - * unless overridden in the options.parent - */ - function captureParentAndFromToElements(options) { - options.origin = angular.extend({ - element: null, - bounds: null, - focus: angular.noop - }, options.origin || {}); + var oldIndex = -1; + var newIndex = -1; + var newTab = this._getTabByName(newValue); + var oldTab = this._getTabByName(oldValue); - options.parent = getDomElement(options.parent, $rootElement); - options.closeTo = getBoundingClientRect(getDomElement(options.closeTo)); - options.openFrom = getBoundingClientRect(getDomElement(options.openFrom)); + if (oldTab) { + oldTab.setSelected(false); + oldIndex = tabs.indexOf(oldTab); + } - if ( options.targetEvent ) { - options.origin = getBoundingClientRect(options.targetEvent.target, options.origin); - } + if (newTab) { + newTab.setSelected(true); + newIndex = tabs.indexOf(newTab); + } - /** - * Identify the bounding RECT for the target element - * - */ - function getBoundingClientRect (element, orig) { - var source = angular.element((element || {})); - if (source && source.length) { - // Compute and save the target element's bounding rect, so that if the - // element is hidden when the dialog closes, we can shrink the dialog - // back to the same position it expanded from. - // - // Checking if the source is a rect object or a DOM element - var bounds = {top:0,left:0,height:0,width:0}; - var hasFn = angular.isFunction(source[0].getBoundingClientRect); + this._$timeout(function() { + self._updateInkBarStyles(newTab, newIndex, oldIndex); + }); +}; - return angular.extend(orig || {}, { - element : hasFn ? source : undefined, - bounds : hasFn ? source[0].getBoundingClientRect() : angular.extend({}, bounds, source[0]), - focus : angular.bind(source, source.focus), - }); - } - } +/** + * Repositions the ink bar to the selected tab. + * @private + */ +MdNavBarController.prototype._updateInkBarStyles = function(tab, newIndex, oldIndex) { + this._inkbar.toggleClass('_md-left', newIndex < oldIndex) + .toggleClass('_md-right', newIndex > oldIndex); - /** - * If the specifier is a simple string selector, then query for - * the DOM element. - */ - function getDomElement(element, defaultElement) { - if (angular.isString(element)) { - var simpleSelector = element, - container = $document[0].querySelectorAll(simpleSelector); - element = container.length ? container[0] : null; - } + this._inkbar.css({display: newIndex < 0 ? 'none' : ''}); - // If we have a reference to a raw dom element, always wrap it in jqLite - return angular.element(element || defaultElement); - } + if (tab) { + var tabEl = tab.getButtonEl(); + var left = tabEl.offsetLeft; - } + this._inkbar.css({left: left + 'px', width: tabEl.offsetWidth + 'px'}); + } +}; - /** - * Listen for escape keys and outside clicks to auto close - */ - function activateListeners(element, options) { - var window = angular.element($window); - var onWindowResize = $mdUtil.debounce(function(){ - stretchDialogContainerToViewport(element, options); - }, 60); +/** + * Returns an array of the current tabs. + * @return {!Array} + * @private + */ +MdNavBarController.prototype._getTabs = function() { + var controllers = Array.prototype.slice.call( + this._navBarEl.querySelectorAll('.md-nav-item')) + .map(function(el) { + return angular.element(el).controller('mdNavItem') + }); + return controllers.indexOf(undefined) ? controllers : null; +}; - var removeListeners = []; - var smartClose = function() { - // Only 'confirm' dialogs have a cancel button... escape/clickOutside will - // cancel or fallback to hide. - var closeFn = ( options.$type == 'alert' ) ? $mdDialog.hide : $mdDialog.cancel; - $mdUtil.nextTick(closeFn, true); - }; +/** + * Returns the tab with the specified name. + * @param {string} name The name of the tab, found in its name attribute. + * @return {!NavItemController|undefined} + * @private + */ +MdNavBarController.prototype._getTabByName = function(name) { + return this._findTab(function(tab) { + return tab.getName() == name; + }); +}; - if (options.escapeToClose) { - var parentTarget = options.parent; - var keyHandlerFn = function(ev) { - if (ev.keyCode === $mdConstant.KEY_CODE.ESCAPE) { - ev.stopPropagation(); - ev.preventDefault(); +/** + * Returns the selected tab. + * @return {!NavItemController|undefined} + * @private + */ +MdNavBarController.prototype._getSelectedTab = function() { + return this._findTab(function(tab) { + return tab.isSelected(); + }); +}; + +/** + * Returns the focused tab. + * @return {!NavItemController|undefined} + */ +MdNavBarController.prototype.getFocusedTab = function() { + return this._findTab(function(tab) { + return tab.hasFocus(); + }); +}; + +/** + * Find a tab that matches the specified function. + * @private + */ +MdNavBarController.prototype._findTab = function(fn) { + var tabs = this._getTabs(); + for (var i = 0; i < tabs.length; i++) { + if (fn(tabs[i])) { + return tabs[i]; + } + } + + return null; +}; + +/** + * Direct focus to the selected tab when focus enters the nav bar. + */ +MdNavBarController.prototype.onFocus = function() { + var tab = this._getSelectedTab(); + if (tab) { + tab.setFocused(true); + } +}; + +/** + * Move focus from oldTab to newTab. + * @param {!NavItemController} oldTab + * @param {!NavItemController} newTab + * @private + */ +MdNavBarController.prototype._moveFocus = function(oldTab, newTab) { + oldTab.setFocused(false); + newTab.setFocused(true); +}; + +/** + * Responds to keypress events. + * @param {!Event} e + */ +MdNavBarController.prototype.onKeydown = function(e) { + var keyCodes = this._$mdConstant.KEY_CODE; + var tabs = this._getTabs(); + var focusedTab = this.getFocusedTab(); + if (!focusedTab) return; + + var focusedTabIndex = tabs.indexOf(focusedTab); + + // use arrow keys to navigate between tabs + switch (e.keyCode) { + case keyCodes.UP_ARROW: + case keyCodes.LEFT_ARROW: + if (focusedTabIndex > 0) { + this._moveFocus(focusedTab, tabs[focusedTabIndex - 1]); + } + break; + case keyCodes.DOWN_ARROW: + case keyCodes.RIGHT_ARROW: + if (focusedTabIndex < tabs.length - 1) { + this._moveFocus(focusedTab, tabs[focusedTabIndex + 1]); + } + break; + case keyCodes.SPACE: + case keyCodes.ENTER: + // timeout to avoid a "digest already in progress" console error + this._$timeout(function() { + focusedTab.getButtonEl().click(); + }); + break; + } +}; + +/** + * @ngInject + */ +function MdNavItem($mdAria, $$rAF) { + return { + restrict: 'E', + require: ['mdNavItem', '^mdNavBar'], + controller: MdNavItemController, + bindToController: true, + controllerAs: 'ctrl', + replace: true, + transclude: true, + template: function(tElement, tAttrs) { + var hasNavClick = tAttrs.mdNavClick; + var hasNavHref = tAttrs.mdNavHref; + var hasNavSref = tAttrs.mdNavSref; + var hasSrefOpts = tAttrs.srefOpts; + var navigationAttribute; + var navigationOptions; + var buttonTemplate; + + // Cannot specify more than one nav attribute + if ((hasNavClick ? 1:0) + (hasNavHref ? 1:0) + (hasNavSref ? 1:0) > 1) { + throw Error( + 'Must not specify more than one of the md-nav-click, md-nav-href, ' + + 'or md-nav-sref attributes per nav-item directive.' + ); + } - smartClose(); - } - }; + if (hasNavClick) { + navigationAttribute = 'ng-click="ctrl.mdNavClick()"'; + } else if (hasNavHref) { + navigationAttribute = 'ng-href="{{ctrl.mdNavHref}}"'; + } else if (hasNavSref) { + navigationAttribute = 'ui-sref="{{ctrl.mdNavSref}}"'; + } - // Add keydown listeners - element.on('keydown', keyHandlerFn); - parentTarget.on('keydown', keyHandlerFn); + navigationOptions = hasSrefOpts ? 'ui-sref-opts="{{ctrl.srefOpts}}" ' : ''; - // Queue remove listeners function - removeListeners.push(function() { + if (navigationAttribute) { + buttonTemplate = '' + + '' + + '' + + ''; + } - element.off('keydown', keyHandlerFn); - parentTarget.off('keydown', keyHandlerFn); + return '' + + '
  • ' + + (buttonTemplate || '') + + '
  • '; + }, + scope: { + 'mdNavClick': '&?', + 'mdNavHref': '@?', + 'mdNavSref': '@?', + 'srefOpts': '=?', + 'name': '@', + }, + link: function(scope, element, attrs, controllers) { + // When accessing the element's contents synchronously, they + // may not be defined yet because of transclusion. There is a higher + // chance that it will be accessible if we wait one frame. + $$rAF(function() { + var mdNavItem = controllers[0]; + var mdNavBar = controllers[1]; + var navButton = angular.element(element[0].querySelector('._md-nav-button')); - }); - } + if (!mdNavItem.name) { + mdNavItem.name = angular.element(element[0] + .querySelector('._md-nav-button-text')).text().trim(); + } - // Register listener to update dialog on window resize - window.on('resize', onWindowResize); + navButton.on('click', function() { + mdNavBar.mdSelectedNavItem = mdNavItem.name; + scope.$apply(); + }); - removeListeners.push(function() { - window.off('resize', onWindowResize); + $mdAria.expectWithText(element, 'aria-label'); }); + } + }; +} - if (options.clickOutsideToClose) { - var target = element; - var sourceElem; - - // Keep track of the element on which the mouse originally went down - // so that we can only close the backdrop when the 'click' started on it. - // A simple 'click' handler does not work, - // it sets the target object as the element the mouse went down on. - var mousedownHandler = function(ev) { - sourceElem = ev.target; - }; +/** + * Controller for the nav-item component. + * @param {!angular.JQLite} $element + * @constructor + * @final + * @ngInject + */ +function MdNavItemController($element) { - // We check if our original element and the target is the backdrop - // because if the original was the backdrop and the target was inside the dialog - // we don't want to dialog to close. - var mouseupHandler = function(ev) { - if (sourceElem === target[0] && ev.target === target[0]) { - ev.stopPropagation(); - ev.preventDefault(); + /** @private @const {!angular.JQLite} */ + this._$element = $element; - smartClose(); - } - }; + // Data-bound variables - // Add listeners - target.on('mousedown', mousedownHandler); - target.on('mouseup', mouseupHandler); + /** @const {?Function} */ + this.mdNavClick; - // Queue remove listeners function - removeListeners.push(function() { - target.off('mousedown', mousedownHandler); - target.off('mouseup', mouseupHandler); - }); - } + /** @const {?string} */ + this.mdNavHref; - // Attach specific `remove` listener handler - options.deactivateListeners = function() { - removeListeners.forEach(function(removeFn) { - removeFn(); - }); - options.deactivateListeners = null; - }; - } + /** @const {?string} */ + this.mdNavSref; + /** @const {?Object} */ + this.srefOpts; + /** @const {?string} */ + this.name; - /** - * Show modal backdrop element... - */ - function showBackdrop(scope, element, options) { + // State variables + /** @private {boolean} */ + this._selected = false; - if (options.disableParentScroll) { - // !! DO this before creating the backdrop; since disableScrollAround() - // configures the scroll offset; which is used by mdBackDrop postLink() - options.restoreScroll = $mdUtil.disableScrollAround(element, options.parent); - } + /** @private {boolean} */ + this._focused = false; +} - if (options.hasBackdrop) { - options.backdrop = $mdUtil.createBackdrop(scope, "_md-dialog-backdrop md-opaque"); - $animate.enter(options.backdrop, options.parent); - } +/** + * Returns a map of class names and values for use by ng-class. + * @return {!Object} + */ +MdNavItemController.prototype.getNgClassMap = function() { + return { + 'md-active': this._selected, + 'md-primary': this._selected, + 'md-unselected': !this._selected, + 'md-focused': this._focused, + }; +}; - /** - * Hide modal backdrop element... - */ - options.hideBackdrop = function hideBackdrop($destroy) { - if (options.backdrop) { - if ( !!$destroy ) options.backdrop.remove(); - else $animate.leave(options.backdrop); - } +/** + * Get the name attribute of the tab. + * @return {string} + */ +MdNavItemController.prototype.getName = function() { + return this.name; +}; - if (options.disableParentScroll) { - options.restoreScroll(); - delete options.restoreScroll; - } +/** + * Get the button element associated with the tab. + * @return {!Element} + */ +MdNavItemController.prototype.getButtonEl = function() { + return this._$element[0].querySelector('._md-nav-button'); +}; - options.hideBackdrop = null; - } - } +/** + * Set the selected state of the tab. + * @param {boolean} isSelected + */ +MdNavItemController.prototype.setSelected = function(isSelected) { + this._selected = isSelected; +}; - /** - * Inject ARIA-specific attributes appropriate for Dialogs - */ - function configureAria(element, options) { +/** + * @return {boolean} + */ +MdNavItemController.prototype.isSelected = function() { + return this._selected; +}; - var role = (options.$type === 'alert') ? 'alertdialog' : 'dialog'; - var dialogContent = element.find('md-dialog-content'); - var dialogContentId = 'dialogContent_' + (element.attr('id') || $mdUtil.nextUid()); +/** + * Set the focused state of the tab. + * @param {boolean} isFocused + */ +MdNavItemController.prototype.setFocused = function(isFocused) { + this._focused = isFocused; - element.attr({ - 'role': role, - 'tabIndex': '-1' - }); + if (isFocused) { + this.getButtonEl().focus(); + } +}; - if (dialogContent.length === 0) { - dialogContent = element; - } +/** + * @return {boolean} + */ +MdNavItemController.prototype.hasFocus = function() { + return this._focused; +}; - dialogContent.attr('id', dialogContentId); - element.attr('aria-describedby', dialogContentId); +})(); +(function(){ +"use strict"; - if (options.ariaLabel) { - $mdAria.expect(element, 'aria-label', options.ariaLabel); - } - else { - $mdAria.expectAsync(element, 'aria-label', function() { - var words = dialogContent.text().split(/\s+/); - if (words.length > 3) words = words.slice(0, 3).concat('...'); - return words.join(' '); - }); - } +/** + * @ngdoc module + * @name material.components.progressCircular + * @description Module for a circular progressbar + */ - // Set up elements before and after the dialog content to capture focus and - // redirect back into the dialog. - topFocusTrap = document.createElement('div'); - topFocusTrap.classList.add('_md-dialog-focus-trap'); - topFocusTrap.tabIndex = 0; +angular.module('material.components.progressCircular', ['material.core']); - bottomFocusTrap = topFocusTrap.cloneNode(false); +})(); +(function(){ +"use strict"; - // When focus is about to move out of the dialog, we want to intercept it and redirect it - // back to the dialog element. - var focusHandler = function() { - element.focus(); - }; - topFocusTrap.addEventListener('focus', focusHandler); - bottomFocusTrap.addEventListener('focus', focusHandler); +/** + * @ngdoc module + * @name material.components.progressLinear + * @description Linear Progress module! + */ +MdProgressLinearDirective.$inject = ["$mdTheming", "$mdUtil", "$log"]; +angular.module('material.components.progressLinear', [ + 'material.core' +]) + .directive('mdProgressLinear', MdProgressLinearDirective); - // The top focus trap inserted immeidately before the md-dialog element (as a sibling). - // The bottom focus trap is inserted at the very end of the md-dialog element (as a child). - element[0].parentNode.insertBefore(topFocusTrap, element[0]); - element.after(bottomFocusTrap); - } +/** + * @ngdoc directive + * @name mdProgressLinear + * @module material.components.progressLinear + * @restrict E + * + * @description + * The linear progress directive is used to make loading content + * in your app as delightful and painless as possible by minimizing + * the amount of visual change a user sees before they can view + * and interact with content. + * + * Each operation should only be represented by one activity indicator + * For example: one refresh operation should not display both a + * refresh bar and an activity circle. + * + * For operations where the percentage of the operation completed + * can be determined, use a determinate indicator. They give users + * a quick sense of how long an operation will take. + * + * For operations where the user is asked to wait a moment while + * something finishes up, and it’s not necessary to expose what's + * happening behind the scenes and how long it will take, use an + * indeterminate indicator. + * + * @param {string} md-mode Select from one of four modes: determinate, indeterminate, buffer or query. + * + * Note: if the `md-mode` value is set as undefined or specified as 1 of the four (4) valid modes, then `indeterminate` + * will be auto-applied as the mode. + * + * Note: if not configured, the `md-mode="indeterminate"` will be auto injected as an attribute. If `value=""` is also specified, however, + * then `md-mode="determinate"` would be auto-injected instead. + * @param {number=} value In determinate and buffer modes, this number represents the percentage of the primary progress bar. Default: 0 + * @param {number=} md-buffer-value In the buffer mode, this number represents the percentage of the secondary progress bar. Default: 0 + * @param {boolean=} ng-disabled Determines whether to disable the progress element. + * + * @usage + * + * + * + * + * + * + * + * + * + * + * + */ +function MdProgressLinearDirective($mdTheming, $mdUtil, $log) { + var MODE_DETERMINATE = "determinate"; + var MODE_INDETERMINATE = "indeterminate"; + var MODE_BUFFER = "buffer"; + var MODE_QUERY = "query"; + var DISABLED_CLASS = "_md-progress-linear-disabled"; - /** - * Prevents screen reader interaction behind modal window - * on swipe interfaces - */ - function lockScreenReader(element, options) { - var isHidden = true; + return { + restrict: 'E', + template: '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ', + compile: compile + }; - // get raw DOM node - walkDOM(element[0]); + function compile(tElement, tAttrs, transclude) { + tElement.attr('aria-valuemin', 0); + tElement.attr('aria-valuemax', 100); + tElement.attr('role', 'progressbar'); - options.unlockScreenReader = function() { - isHidden = false; - walkDOM(element[0]); + return postLink; + } + function postLink(scope, element, attr) { + $mdTheming(element); - options.unlockScreenReader = null; - }; + var lastMode; + var isDisabled = attr.hasOwnProperty('disabled'); + var toVendorCSS = $mdUtil.dom.animator.toCss; + var bar1 = angular.element(element[0].querySelector('.md-bar1')); + var bar2 = angular.element(element[0].querySelector('.md-bar2')); + var container = angular.element(element[0].querySelector('.md-container')); - /** - * Walk DOM to apply or remove aria-hidden on sibling nodes - * and parent sibling nodes - * - */ - function walkDOM(element) { - while (element.parentNode) { - if (element === document.body) { - return; - } - var children = element.parentNode.children; - for (var i = 0; i < children.length; i++) { - // skip over child if it is an ascendant of the dialog - // or a script or style tag - if (element !== children[i] && !isNodeOneOf(children[i], ['SCRIPT', 'STYLE'])) { - children[i].setAttribute('aria-hidden', isHidden); - } - } + element + .attr('md-mode', mode()) + .toggleClass(DISABLED_CLASS, isDisabled); - walkDOM(element = element.parentNode); - } - } - } + validateMode(); + watchAttributes(); /** - * Ensure the dialog container fill-stretches to the viewport + * Watch the value, md-buffer-value, and md-mode attributes */ - function stretchDialogContainerToViewport(container, options) { - var isFixed = $window.getComputedStyle($document[0].body).position == 'fixed'; - var backdrop = options.backdrop ? $window.getComputedStyle(options.backdrop[0]) : null; - var height = backdrop ? Math.min($document[0].body.clientHeight, Math.ceil(Math.abs(parseInt(backdrop.height, 10)))) : 0; + function watchAttributes() { + attr.$observe('value', function(value) { + var percentValue = clamp(value); + element.attr('aria-valuenow', percentValue); - container.css({ - top: (isFixed ? $mdUtil.scrollTop(options.parent) : 0) + 'px', - height: height ? height + 'px' : '100%' + if (mode() != MODE_QUERY) animateIndicator(bar2, percentValue); }); - return container; - } - - /** - * Dialog open and pop-in animation - */ - function dialogPopIn(container, options) { - // Add the `md-dialog-container` to the DOM - options.parent.append(container); - stretchDialogContainerToViewport(container, options); + attr.$observe('mdBufferValue', function(value) { + animateIndicator(bar1, clamp(value)); + }); - var dialogEl = container.find('md-dialog'); - var animator = $mdUtil.dom.animator; - var buildTranslateToOrigin = animator.calculateZoomToOrigin; - var translateOptions = {transitionInClass: '_md-transition-in', transitionOutClass: '_md-transition-out'}; - var from = animator.toTransformCss(buildTranslateToOrigin(dialogEl, options.openFrom || options.origin)); - var to = animator.toTransformCss(""); // defaults to center display (or parent or $rootElement) + attr.$observe('disabled', function(value) { + if (value === true || value === false) { + isDisabled = !!value; + } else { + isDisabled = angular.isDefined(value); + } - if (options.fullscreen) { - dialogEl.addClass('md-dialog-fullscreen'); - } + element.toggleClass(DISABLED_CLASS, isDisabled); + container.toggleClass(lastMode, !isDisabled); + }); - return animator - .translate3d(dialogEl, from, to, translateOptions) - .then(function(animateReversal) { - // Build a reversal translate function synched to this translation... - options.reverseAnimate = function() { - delete options.reverseAnimate; + attr.$observe('mdMode', function(mode) { + if (lastMode) container.removeClass( lastMode ); - if (options.closeTo) { - // Using the opposite classes to create a close animation to the closeTo element - translateOptions = {transitionInClass: '_md-transition-out', transitionOutClass: '_md-transition-in'}; - from = to; - to = animator.toTransformCss(buildTranslateToOrigin(dialogEl, options.closeTo)); + switch( mode ) { + case MODE_QUERY: + case MODE_BUFFER: + case MODE_DETERMINATE: + case MODE_INDETERMINATE: + container.addClass( lastMode = "md-mode-" + mode ); + break; + default: + container.addClass( lastMode = "md-mode-" + MODE_INDETERMINATE ); + break; + } + }); + } - return animator - .translate3d(dialogEl, from, to,translateOptions); - } + /** + * Auto-defaults the mode to either `determinate` or `indeterminate` mode; if not specified + */ + function validateMode() { + if ( angular.isUndefined(attr.mdMode) ) { + var hasValue = angular.isDefined(attr.value); + var mode = hasValue ? MODE_DETERMINATE : MODE_INDETERMINATE; + var info = "Auto-adding the missing md-mode='{0}' to the ProgressLinear element"; - return animateReversal( - animator.toTransformCss( - // in case the origin element has moved or is hidden, - // let's recalculate the translateCSS - buildTranslateToOrigin(dialogEl, options.origin) - ) - ); + //$log.debug( $mdUtil.supplant(info, [mode]) ); - }; - return true; - }); + element.attr("md-mode", mode); + attr.mdMode = mode; + } } /** - * Dialog close and pop-out animation + * Is the md-mode a valid option? */ - function dialogPopOut(container, options) { - return options.reverseAnimate(); + function mode() { + var value = (attr.mdMode || "").trim(); + if ( value ) { + switch(value) { + case MODE_DETERMINATE: + case MODE_INDETERMINATE: + case MODE_BUFFER: + case MODE_QUERY: + break; + default: + value = MODE_INDETERMINATE; + break; + } + } + return value; } /** - * Utility function to filter out raw DOM nodes + * Manually set CSS to animate the Determinate indicator based on the specified + * percentage value (0-100). */ - function isNodeOneOf(elem, nodeTypeArray) { - if (nodeTypeArray.indexOf(elem.nodeName) !== -1) { - return true; - } + function animateIndicator(target, value) { + if ( isDisabled || !mode() ) return; + + var to = $mdUtil.supplant("translateX({0}%) scale({1},1)", [ (value-100)/2, value/100 ]); + var styles = toVendorCSS({ transform : to }); + angular.element(target).css( styles ); } + } + /** + * Clamps the value to be between 0 and 100. + * @param {number} value The value to clamp. + * @returns {number} + */ + function clamp(value) { + return Math.max(0, Math.min(value || 0, 100)); } } -MdDialogProvider.$inject = ["$$interimElementProvider"]; + })(); (function(){ @@ -10044,1668 +18273,2044 @@ MdDialogProvider.$inject = ["$$interimElementProvider"]; /** * @ngdoc module - * @name material.components.divider - * @description Divider module! + * @name material.components.radioButton + * @description radioButton module! */ -angular.module('material.components.divider', [ +mdRadioGroupDirective.$inject = ["$mdUtil", "$mdConstant", "$mdTheming", "$timeout"]; +mdRadioButtonDirective.$inject = ["$mdAria", "$mdUtil", "$mdTheming"]; +angular.module('material.components.radioButton', [ 'material.core' ]) - .directive('mdDivider', MdDividerDirective); + .directive('mdRadioGroup', mdRadioGroupDirective) + .directive('mdRadioButton', mdRadioButtonDirective); /** * @ngdoc directive - * @name mdDivider - * @module material.components.divider + * @module material.components.radioButton + * @name mdRadioGroup + * * @restrict E * * @description - * Dividers group and separate content within lists and page layouts using strong visual and spatial distinctions. This divider is a thin rule, lightweight enough to not distract the user from content. + * The `` directive identifies a grouping + * container for the 1..n grouped radio buttons; specified using nested + * `` tags. + * + * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-ui-color-application) + * the radio button is in the accent color by default. The primary color palette may be used with + * the `md-primary` class. + * + * Note: `` and `` handle tabindex differently + * than the native `` controls. Whereas the native controls + * force the user to tab through all the radio buttons, `` + * is focusable, and by default the ``s are not. + * + * @param {string} ng-model Assignable angular expression to data-bind to. + * @param {boolean=} md-no-ink Use of attribute indicates flag to disable ink ripple effects. * - * @param {boolean=} md-inset Add this attribute to activate the inset divider style. * @usage * - * + * * - * + * + * + * {{ d.label }} + * + * + * + * * * */ -function MdDividerDirective($mdTheming) { - return { - restrict: 'E', - link: $mdTheming - }; -} -MdDividerDirective.$inject = ["$mdTheming"]; - -})(); -(function(){ -"use strict"; - -(function() { - 'use strict'; - - /** - * @ngdoc module - * @name material.components.fabActions - */ - angular - .module('material.components.fabActions', ['material.core']) - .directive('mdFabActions', MdFabActionsDirective); - - /** - * @ngdoc directive - * @name mdFabActions - * @module material.components.fabActions - * - * @restrict E - * - * @description - * The `` directive is used inside of a `` or - * `` directive to mark an element (or elements) as the actions and setup the - * proper event listeners. - * - * @usage - * See the `` or `` directives for example usage. - */ - function MdFabActionsDirective() { - return { - restrict: 'E', - - require: ['^?mdFabSpeedDial', '^?mdFabToolbar'], - - compile: function(element, attributes) { - var children = element.children(); - - var hasNgRepeat = false; - - angular.forEach(['', 'data-', 'x-'], function(prefix) { - hasNgRepeat = hasNgRepeat || (children.attr(prefix + 'ng-repeat') ? true : false); - }); - - // Support both ng-repeat and static content - if (hasNgRepeat) { - children.addClass('md-fab-action-item'); - } else { - // Wrap every child in a new div and add a class that we can scale/fling independently - children.wrap('
    '); - } - } - } - } - -})(); - -})(); -(function(){ -"use strict"; +function mdRadioGroupDirective($mdUtil, $mdConstant, $mdTheming, $timeout) { + RadioGroupController.prototype = createRadioGroupControllerProto(); -(function() { - 'use strict'; + return { + restrict: 'E', + controller: ['$element', RadioGroupController], + require: ['mdRadioGroup', '?ngModel'], + link: { pre: linkRadioGroup } + }; - angular.module('material.components.fabShared', ['material.core']) - .controller('MdFabController', MdFabController); + function linkRadioGroup(scope, element, attr, ctrls) { + element.addClass('_md'); // private md component indicator for styling + $mdTheming(element); - function MdFabController($scope, $element, $animate, $mdUtil, $mdConstant, $timeout) { - var vm = this; + var rgCtrl = ctrls[0]; + var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel(); - // NOTE: We use async eval(s) below to avoid conflicts with any existing digest loops + rgCtrl.init(ngModelCtrl); - vm.open = function() { - $scope.$evalAsync("vm.isOpen = true"); - }; + scope.mouseActive = false; - vm.close = function() { - // Async eval to avoid conflicts with existing digest loops - $scope.$evalAsync("vm.isOpen = false"); + element + .attr({ + 'role': 'radiogroup', + 'tabIndex': element.attr('tabindex') || '0' + }) + .on('keydown', keydownListener) + .on('mousedown', function(event) { + scope.mouseActive = true; + $timeout(function() { + scope.mouseActive = false; + }, 100); + }) + .on('focus', function() { + if(scope.mouseActive === false) { + rgCtrl.$element.addClass('md-focused'); + } + }) + .on('blur', function() { + rgCtrl.$element.removeClass('md-focused'); + }); - // Focus the trigger when the element closes so users can still tab to the next item - $element.find('md-fab-trigger')[0].focus(); - }; + /** + * + */ + function setFocus() { + if (!element.hasClass('md-focused')) { element.addClass('md-focused'); } + } - // Toggle the open/close state when the trigger is clicked - vm.toggle = function() { - $scope.$evalAsync("vm.isOpen = !vm.isOpen"); - }; + /** + * + */ + function keydownListener(ev) { + var keyCode = ev.which || ev.keyCode; - setupDefaults(); - setupListeners(); - setupWatchers(); + // Only listen to events that we originated ourselves + // so that we don't trigger on things like arrow keys in + // inputs. - var initialAnimationAttempts = 0; - fireInitialAnimations(); + if (keyCode != $mdConstant.KEY_CODE.ENTER && + ev.currentTarget != ev.target) { + return; + } - function setupDefaults() { - // Set the default direction to 'down' if none is specified - vm.direction = vm.direction || 'down'; + switch (keyCode) { + case $mdConstant.KEY_CODE.LEFT_ARROW: + case $mdConstant.KEY_CODE.UP_ARROW: + ev.preventDefault(); + rgCtrl.selectPrevious(); + setFocus(); + break; - // Set the default to be closed - vm.isOpen = vm.isOpen || false; + case $mdConstant.KEY_CODE.RIGHT_ARROW: + case $mdConstant.KEY_CODE.DOWN_ARROW: + ev.preventDefault(); + rgCtrl.selectNext(); + setFocus(); + break; - // Start the keyboard interaction at the first action - resetActionIndex(); + case $mdConstant.KEY_CODE.ENTER: + var form = angular.element($mdUtil.getClosest(element[0], 'form')); + if (form.length > 0) { + form.triggerHandler('submit'); + } + break; + } - // Add an animations waiting class so we know not to run - $element.addClass('_md-animations-waiting'); } + } - function setupListeners() { - var eventTypes = [ - 'click', 'focusin', 'focusout' - ]; - - // Add our listeners - angular.forEach(eventTypes, function(eventType) { - $element.on(eventType, parseEvents); - }); + function RadioGroupController($element) { + this._radioButtonRenderFns = []; + this.$element = $element; + } - // Remove our listeners when destroyed - $scope.$on('$destroy', function() { - angular.forEach(eventTypes, function(eventType) { - $element.off(eventType, parseEvents); + function createRadioGroupControllerProto() { + return { + init: function(ngModelCtrl) { + this._ngModelCtrl = ngModelCtrl; + this._ngModelCtrl.$render = angular.bind(this, this.render); + }, + add: function(rbRender) { + this._radioButtonRenderFns.push(rbRender); + }, + remove: function(rbRender) { + var index = this._radioButtonRenderFns.indexOf(rbRender); + if (index !== -1) { + this._radioButtonRenderFns.splice(index, 1); + } + }, + render: function() { + this._radioButtonRenderFns.forEach(function(rbRender) { + rbRender(); }); - - // remove any attached keyboard handlers in case element is removed while - // speed dial is open - disableKeyboard(); - }); - } - - var closeTimeout; - function parseEvents(event) { - // If the event is a click, just handle it - if (event.type == 'click') { - handleItemClick(event); + }, + setViewValue: function(value, eventType) { + this._ngModelCtrl.$setViewValue(value, eventType); + // update the other radio buttons as well + this.render(); + }, + getViewValue: function() { + return this._ngModelCtrl.$viewValue; + }, + selectNext: function() { + return changeSelectedButton(this.$element, 1); + }, + selectPrevious: function() { + return changeSelectedButton(this.$element, -1); + }, + setActiveDescendant: function (radioId) { + this.$element.attr('aria-activedescendant', radioId); + }, + isDisabled: function() { + return this.$element[0].hasAttribute('disabled'); } + }; + } + /** + * Change the radio group's selected button by a given increment. + * If no button is selected, select the first button. + */ + function changeSelectedButton(parent, increment) { + // Coerce all child radio buttons into an array, then wrap then in an iterator + var buttons = $mdUtil.iterator(parent[0].querySelectorAll('md-radio-button'), true); - // If we focusout, set a timeout to close the element - if (event.type == 'focusout' && !closeTimeout) { - closeTimeout = $timeout(function() { - vm.close(); - }, 100, false); - } + if (buttons.count()) { + var validate = function (button) { + // If disabled, then NOT valid + return !angular.element(button).attr("disabled"); + }; - // If we see a focusin and there is a timeout about to run, cancel it so we stay open - if (event.type == 'focusin' && closeTimeout) { - $timeout.cancel(closeTimeout); - closeTimeout = null; - } - } + var selected = parent[0].querySelector('md-radio-button.md-checked'); + var target = buttons[increment < 0 ? 'previous' : 'next'](selected, validate) || buttons.first(); - function resetActionIndex() { - vm.currentActionIndex = -1; + // Activate radioButton's click listener (triggerHandler won't create a real click event) + angular.element(target).triggerHandler('click'); } + } - function setupWatchers() { - // Watch for changes to the direction and update classes/attributes - $scope.$watch('vm.direction', function(newDir, oldDir) { - // Add the appropriate classes so we can target the direction in the CSS - $animate.removeClass($element, 'md-' + oldDir); - $animate.addClass($element, 'md-' + newDir); - - // Reset the action index since it may have changed - resetActionIndex(); - }); - - var trigger, actions; +} - // Watch for changes to md-open - $scope.$watch('vm.isOpen', function(isOpen) { - // Reset the action index since it may have changed - resetActionIndex(); +/** + * @ngdoc directive + * @module material.components.radioButton + * @name mdRadioButton + * + * @restrict E + * + * @description + * The ``directive is the child directive required to be used within `` elements. + * + * While similar to the `` directive, + * the `` directive provides ink effects, ARIA support, and + * supports use within named radio groups. + * + * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string=} ngChange Angular expression to be executed when input changes due to user + * interaction with the input element. + * @param {string} ngValue Angular expression which sets the value to which the expression should + * be set when selected. + * @param {string} value The value to which the expression should be set when selected. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} aria-label Adds label to radio button for accessibility. + * Defaults to radio button's text. If no text content is available, a warning will be logged. + * + * @usage + * + * + * + * Label 1 + * + * + * + * Green + * + * + * + * + */ +function mdRadioButtonDirective($mdAria, $mdUtil, $mdTheming) { - // We can't get the trigger/actions outside of the watch because the component hasn't been - // linked yet, so we wait until the first watch fires to cache them. - if (!trigger || !actions) { - trigger = getTriggerElement(); - actions = getActionsElement(); - } + var CHECKED_CSS = 'md-checked'; - if (isOpen) { - enableKeyboard(); - } else { - disableKeyboard(); - } + return { + restrict: 'E', + require: '^mdRadioGroup', + transclude: true, + template: '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ', + link: link + }; - var toAdd = isOpen ? 'md-is-open' : ''; - var toRemove = isOpen ? '' : 'md-is-open'; + function link(scope, element, attr, rgCtrl) { + var lastChecked; - // Set the proper ARIA attributes - trigger.attr('aria-haspopup', true); - trigger.attr('aria-expanded', isOpen); - actions.attr('aria-hidden', !isOpen); + $mdTheming(element); + configureAria(element, scope); - // Animate the CSS classes - $animate.setClass($element, toAdd, toRemove); - }); + // ngAria overwrites the aria-checked inside a $watch for ngValue. + // We should defer the initialization until all the watches have fired. + // This can also be fixed by removing the `lastChecked` check, but that'll + // cause more DOM manipulation on each digest. + if (attr.ngValue) { + $mdUtil.nextTick(initialize, false); + } else { + initialize(); } - function fireInitialAnimations() { - // If the element is actually visible on the screen - if ($element[0].scrollHeight > 0) { - // Fire our animation - $animate.addClass($element, '_md-animations-ready').then(function() { - // Remove the waiting class - $element.removeClass('_md-animations-waiting'); - }); + /** + * Initializes the component. + */ + function initialize() { + if (!rgCtrl) { + throw 'RadioButton: No RadioGroupController could be found.'; } - // Otherwise, try for up to 1 second before giving up - else if (initialAnimationAttempts < 10) { - $timeout(fireInitialAnimations, 100); + rgCtrl.add(render); + attr.$observe('value', render); - // Increment our counter - initialAnimationAttempts = initialAnimationAttempts + 1; - } + element + .on('click', listener) + .on('$destroy', function() { + rgCtrl.remove(render); + }); } - function enableKeyboard() { - $element.on('keydown', keyPressed); + /** + * On click functionality. + */ + function listener(ev) { + if (element[0].hasAttribute('disabled') || rgCtrl.isDisabled()) return; - // On the next tick, setup a check for outside clicks; we do this on the next tick to avoid - // clicks/touches that result in the isOpen attribute changing (e.g. a bound radio button) - $mdUtil.nextTick(function() { - angular.element(document).on('click touchend', checkForOutsideClick); + scope.$apply(function() { + rgCtrl.setViewValue(attr.value, ev && ev.type); }); - - // TODO: On desktop, we should be able to reset the indexes so you cannot tab through, but - // this breaks accessibility, especially on mobile, since you have no arrow keys to press - //resetActionTabIndexes(); } - function disableKeyboard() { - $element.off('keydown', keyPressed); - angular.element(document).off('click touchend', checkForOutsideClick); - } + /** + * Add or remove the `.md-checked` class from the RadioButton (and conditionally its parent). + * Update the `aria-activedescendant` attribute. + */ + function render() { + var checked = rgCtrl.getViewValue() == attr.value; - function checkForOutsideClick(event) { - if (event.target) { - var closestTrigger = $mdUtil.getClosest(event.target, 'md-fab-trigger'); - var closestActions = $mdUtil.getClosest(event.target, 'md-fab-actions'); + if (checked === lastChecked) return; - if (!closestTrigger && !closestActions) { - vm.close(); - } + if (element[0].parentNode.nodeName.toLowerCase() !== 'md-radio-group') { + // If the radioButton is inside a div, then add class so highlighting will work + element.parent().toggleClass(CHECKED_CSS, checked); } - } - function keyPressed(event) { - switch (event.which) { - case $mdConstant.KEY_CODE.ESCAPE: vm.close(); event.preventDefault(); return false; - case $mdConstant.KEY_CODE.LEFT_ARROW: doKeyLeft(event); return false; - case $mdConstant.KEY_CODE.UP_ARROW: doKeyUp(event); return false; - case $mdConstant.KEY_CODE.RIGHT_ARROW: doKeyRight(event); return false; - case $mdConstant.KEY_CODE.DOWN_ARROW: doKeyDown(event); return false; + if (checked) { + rgCtrl.setActiveDescendant(element.attr('id')); } - } - function doActionPrev(event) { - focusAction(event, -1); + lastChecked = checked; + + element + .attr('aria-checked', checked) + .toggleClass(CHECKED_CSS, checked); } - function doActionNext(event) { - focusAction(event, 1); + /** + * Inject ARIA-specific attributes appropriate for each radio button + */ + function configureAria(element, scope){ + element.attr({ + id: attr.id || 'radio_' + $mdUtil.nextUid(), + role: 'radio', + 'aria-checked': 'false' + }); + + $mdAria.expectWithText(element, 'aria-label'); } + } +} - function focusAction(event, direction) { - var actions = resetActionTabIndexes(); +})(); +(function(){ +"use strict"; - // Increment/decrement the counter with restrictions - vm.currentActionIndex = vm.currentActionIndex + direction; - vm.currentActionIndex = Math.min(actions.length - 1, vm.currentActionIndex); - vm.currentActionIndex = Math.max(0, vm.currentActionIndex); +/** + * @ngdoc module + * @name material.components.select + */ - // Focus the element - var focusElement = angular.element(actions[vm.currentActionIndex]).children()[0]; - angular.element(focusElement).attr('tabindex', 0); - focusElement.focus(); +/*************************************************** - // Make sure the event doesn't bubble and cause something else - event.preventDefault(); - event.stopImmediatePropagation(); - } + ### TODO - POST RC1 ### + - [ ] Abstract placement logic in $mdSelect service to $mdMenu service - function resetActionTabIndexes() { - // Grab all of the actions - var actions = getActionsElement()[0].querySelectorAll('.md-fab-action-item'); + ***************************************************/ - // Disable all other actions for tabbing - angular.forEach(actions, function(action) { - angular.element(angular.element(action).children()[0]).attr('tabindex', -1); - }); +SelectDirective.$inject = ["$mdSelect", "$mdUtil", "$mdConstant", "$mdTheming", "$mdAria", "$parse", "$sce", "$injector"]; +SelectMenuDirective.$inject = ["$parse", "$mdUtil", "$mdConstant", "$mdTheming"]; +OptionDirective.$inject = ["$mdButtonInkRipple", "$mdUtil"]; +SelectProvider.$inject = ["$$interimElementProvider"]; +var SELECT_EDGE_MARGIN = 8; +var selectNextId = 0; +var CHECKBOX_SELECTION_INDICATOR = + angular.element('
    '); - return actions; - } +angular.module('material.components.select', [ + 'material.core', + 'material.components.backdrop' + ]) + .directive('mdSelect', SelectDirective) + .directive('mdSelectMenu', SelectMenuDirective) + .directive('mdOption', OptionDirective) + .directive('mdOptgroup', OptgroupDirective) + .directive('mdSelectHeader', SelectHeaderDirective) + .provider('$mdSelect', SelectProvider); - function doKeyLeft(event) { - if (vm.direction === 'left') { - doActionNext(event); - } else { - doActionPrev(event); - } - } +/** + * @ngdoc directive + * @name mdSelect + * @restrict E + * @module material.components.select + * + * @description Displays a select box, bound to an ng-model. + * + * When the select is required and uses a floating label, then the label will automatically contain + * an asterisk (`*`). This behavior can be disabled by using the `md-no-asterisk` attribute. + * + * By default, the select will display with an underline to match other form elements. This can be + * disabled by applying the `md-no-underline` CSS class. + * + * ### Option Params + * + * When applied, `md-option-empty` will mark the option as "empty" allowing the option to clear the + * select and put it back in it's default state. You may supply this attribute on any option you + * wish, however, it is automatically applied to an option whose `value` or `ng-value` are not + * defined. + * + * **Automatically Applied** + * + * - `` + * - `` + * - `` + * - `` + * - `` + * + * **NOT Automatically Applied** + * + * - `` + * - `` + * - `` + * - `` (this evaluates to the string `"undefined"`) + * - <md-option ng-value="{{someValueThatMightBeUndefined}}"> + * + * **Note:** A value of `undefined` ***is considered a valid value*** (and does not auto-apply this + * attribute) since you may wish this to be your "Not Available" or "None" option. + * + * **Note:** Using the `value` attribute (as opposed to `ng-value`) always evaluates to a string, so + * `value="null"` will require the test `ng-if="myValue != 'null'"` rather than `ng-if="!myValue"`. + * + * @param {expression} ng-model The model! + * @param {boolean=} multiple When set to true, allows for more than one option to be selected. The model is an array with the selected choices. + * @param {expression=} md-on-close Expression to be evaluated when the select is closed. + * @param {expression=} md-on-open Expression to be evaluated when opening the select. + * Will hide the select options and show a spinner until the evaluated promise resolves. + * @param {expression=} md-selected-text Expression to be evaluated that will return a string + * to be displayed as a placeholder in the select input box when it is closed. The value + * will be treated as *text* (not html). + * @param {expression=} md-selected-html Expression to be evaluated that will return a string + * to be displayed as a placeholder in the select input box when it is closed. The value + * will be treated as *html*. The value must either be explicitly marked as trustedHtml or + * the ngSanitize module must be loaded. + * @param {string=} placeholder Placeholder hint text. + * @param md-no-asterisk {boolean=} When set to true, an asterisk will not be appended to the + * floating label. **Note:** This attribute is only evaluated once; it is not watched. + * @param {string=} aria-label Optional label for accessibility. Only necessary if no placeholder or + * explicit label is present. + * @param {string=} md-container-class Class list to get applied to the `.md-select-menu-container` + * element (for custom styling). + * + * @usage + * With a placeholder (label and aria-label are added dynamically) + * + * + * + * {{ opt }} + * + * + * + * + * With an explicit label + * + * + * + * + * {{ opt }} + * + * + * + * + * With a select-header + * + * When a developer needs to put more than just a text label in the + * md-select-menu, they should use the md-select-header. + * The user can put custom HTML inside of the header and style it to their liking. + * One common use case of this would be a sticky search bar. + * + * When using the md-select-header the labels that would previously be added to the + * OptGroupDirective are ignored. + * + * + * + * + * + * Neighborhoods - + * + * {{ opt }} + * + * + * + * + * ## Selects and object equality + * When using a `md-select` to pick from a list of objects, it is important to realize how javascript handles + * equality. Consider the following example: + * + * angular.controller('MyCtrl', function($scope) { + * $scope.users = [ + * { id: 1, name: 'Bob' }, + * { id: 2, name: 'Alice' }, + * { id: 3, name: 'Steve' } + * ]; + * $scope.selectedUser = { id: 1, name: 'Bob' }; + * }); + * + * + *
    + * + * {{ user.name }} + * + *
    + *
    + * + * At first one might expect that the select should be populated with "Bob" as the selected user. However, + * this is not true. To determine whether something is selected, + * `ngModelController` is looking at whether `$scope.selectedUser == (any user in $scope.users);`; + * + * Javascript's `==` operator does not check for deep equality (ie. that all properties + * on the object are the same), but instead whether the objects are *the same object in memory*. + * In this case, we have two instances of identical objects, but they exist in memory as unique + * entities. Because of this, the select will have no value populated for a selected user. + * + * To get around this, `ngModelController` provides a `track by` option that allows us to specify a different + * expression which will be used for the equality operator. As such, we can update our `html` to + * make use of this by specifying the `ng-model-options="{trackBy: '$value.id'}"` on the `md-select` + * element. This converts our equality expression to be + * `$scope.selectedUser.id == (any id in $scope.users.map(function(u) { return u.id; }));` + * which results in Bob being selected as desired. + * + * Working HTML: + * + *
    + * + * {{ user.name }} + * + *
    + *
    + */ +function SelectDirective($mdSelect, $mdUtil, $mdConstant, $mdTheming, $mdAria, $parse, $sce, + $injector) { + var keyCodes = $mdConstant.KEY_CODE; + var NAVIGATION_KEYS = [keyCodes.SPACE, keyCodes.ENTER, keyCodes.UP_ARROW, keyCodes.DOWN_ARROW]; - function doKeyUp(event) { - if (vm.direction === 'down') { - doActionPrev(event); - } else { - doActionNext(event); - } - } + return { + restrict: 'E', + require: ['^?mdInputContainer', 'mdSelect', 'ngModel', '?^form'], + compile: compile, + controller: function() { + } // empty placeholder controller to be initialized in link + }; - function doKeyRight(event) { - if (vm.direction === 'left') { - doActionPrev(event); - } else { - doActionNext(event); - } + function compile(element, attr) { + // add the select value that will hold our placeholder or selected option value + var valueEl = angular.element(''); + valueEl.append(''); + valueEl.addClass('md-select-value'); + if (!valueEl[0].hasAttribute('id')) { + valueEl.attr('id', 'select_value_label_' + $mdUtil.nextUid()); } - function doKeyDown(event) { - if (vm.direction === 'up') { - doActionPrev(event); - } else { - doActionNext(event); - } + // There's got to be an md-content inside. If there's not one, let's add it. + if (!element.find('md-content').length) { + element.append(angular.element('').append(element.contents())); } - function isTrigger(element) { - return $mdUtil.getClosest(element, 'md-fab-trigger'); - } - function isAction(element) { - return $mdUtil.getClosest(element, 'md-fab-actions'); - } + // Add progress spinner for md-options-loading + if (attr.mdOnOpen) { - function handleItemClick(event) { - if (isTrigger(event.target)) { - vm.toggle(); - } + // Show progress indicator while loading async + // Use ng-hide for `display:none` so the indicator does not interfere with the options list + element + .find('md-content') + .prepend(angular.element( + '
    ' + + ' ' + + '
    ' + )); - if (isAction(event.target)) { - vm.close(); - } + // Hide list [of item options] while loading async + element + .find('md-option') + .attr('ng-show', '$$loadingAsyncDone'); } - function getTriggerElement() { - return $element.find('md-fab-trigger'); - } + if (attr.name) { + var autofillClone = angular.element(', - * - * - * - * - *

    When disabling floating labels

    - * - * - * - * - * + * @ngdoc module + * @name material.components.sidenav * - * + * @description + * A Sidenav QP component. */ -function mdInputContainerDirective($mdTheming, $parse) { +SidenavService.$inject = ["$mdComponentRegistry", "$mdUtil", "$q", "$log"]; +SidenavDirective.$inject = ["$mdMedia", "$mdUtil", "$mdConstant", "$mdTheming", "$mdInteraction", "$animate", "$compile", "$parse", "$log", "$q", "$document", "$window", "$$rAF"]; +SidenavController.$inject = ["$scope", "$attrs", "$mdComponentRegistry", "$q", "$interpolate"]; +angular + .module('material.components.sidenav', [ + 'material.core', + 'material.components.backdrop' + ]) + .factory('$mdSidenav', SidenavService ) + .directive('mdSidenav', SidenavDirective) + .directive('mdSidenavFocus', SidenavFocusDirective) + .controller('$mdSidenavController', SidenavController); - var INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT', 'MD-SELECT']; - var LEFT_SELECTORS = INPUT_TAGS.reduce(function(selectors, isel) { - return selectors.concat(['md-icon ~ ' + isel, '.md-icon ~ ' + isel]); - }, []).join(","); +/** + * @ngdoc service + * @name $mdSidenav + * @module material.components.sidenav + * + * @description + * `$mdSidenav` makes it easy to interact with multiple sidenavs + * in an app. When looking up a sidenav instance, you can either look + * it up synchronously or wait for it to be initializied asynchronously. + * This is done by passing the second argument to `$mdSidenav`. + * + * @usage + * + * // Async lookup for sidenav instance; will resolve when the instance is available + * $mdSidenav(componentId, true).then(function(instance) { + * $log.debug( componentId + "is now ready" ); + * }); + * // Sync lookup for sidenav instance; this will resolve immediately. + * $mdSidenav(componentId).then(function(instance) { + * $log.debug( componentId + "is now ready" ); + * }); + * // Async toggle the given sidenav; + * // when instance is known ready and lazy lookup is not needed. + * $mdSidenav(componentId) + * .toggle() + * .then(function(){ + * $log.debug('toggled'); + * }); + * // Async open the given sidenav + * $mdSidenav(componentId) + * .open() + * .then(function(){ + * $log.debug('opened'); + * }); + * // Async close the given sidenav + * $mdSidenav(componentId) + * .close() + * .then(function(){ + * $log.debug('closed'); + * }); + * // Sync check to see if the specified sidenav is set to be open + * $mdSidenav(componentId).isOpen(); + * // Sync check to whether given sidenav is locked open + * // If this is true, the sidenav will be open regardless of close() + * $mdSidenav(componentId).isLockedOpen(); + * // On close callback to handle close, backdrop click or escape key pressed + * // Callback happens BEFORE the close action occurs. + * $mdSidenav(componentId).onClose(function () { + * $log.debug('closing'); + * }); + * + */ +function SidenavService($mdComponentRegistry, $mdUtil, $q, $log) { + var errorMsg = "SideNav '{0}' is not available! Did you use md-component-id='{0}'?"; + var service = { + find : findInstance, // sync - returns proxy API + waitFor : waitForInstance // async - returns promise + }; - var RIGHT_SELECTORS = INPUT_TAGS.reduce(function(selectors, isel) { - return selectors.concat([isel + ' ~ md-icon', isel + ' ~ .md-icon']); - }, []).join(","); + /** + * Service API that supports three (3) usages: + * $mdSidenav().find("left") // sync (must already exist) or returns undefined + * $mdSidenav("left").toggle(); // sync (must already exist) or returns reject promise; + * $mdSidenav("left",true).then( function(left){ // async returns instance when available + * left.toggle(); + * }); + */ + return function(handle, enableWait) { + if ( angular.isUndefined(handle) ) return service; - ContainerCtrl.$inject = ["$scope", "$element", "$attrs", "$animate"]; - return { - restrict: 'E', - link: postLink, - controller: ContainerCtrl + var shouldWait = enableWait === true; + var instance = service.find(handle, shouldWait); + return !instance && shouldWait ? service.waitFor(handle) : + !instance && angular.isUndefined(enableWait) ? addLegacyAPI(service, handle) : instance; }; - function postLink(scope, element) { - $mdTheming(element); - - // Check for both a left & right icon - var leftIcon = element[0].querySelector(LEFT_SELECTORS); - var rightIcon = element[0].querySelector(RIGHT_SELECTORS); + /** + * For failed instance/handle lookups, older-clients expect an response object with noops + * that include `rejected promise APIs` + */ + function addLegacyAPI(service, handle) { + var falseFn = function() { return false; }; + var rejectFn = function() { + return $q.when($mdUtil.supplant(errorMsg, [handle || ""])); + }; - if (leftIcon) { element.addClass('md-icon-left'); } - if (rightIcon) { element.addClass('md-icon-right'); } - } + return angular.extend({ + isLockedOpen : falseFn, + isOpen : falseFn, + toggle : rejectFn, + open : rejectFn, + close : rejectFn, + onClose : angular.noop, + then : function(callback) { + return waitForInstance(handle) + .then(callback || angular.noop); + } + }, service); + } + /** + * Synchronously lookup the controller instance for the specified sidNav instance which has been + * registered with the markup `md-component-id` + */ + function findInstance(handle, shouldWait) { + var instance = $mdComponentRegistry.get(handle); - function ContainerCtrl($scope, $element, $attrs, $animate) { - var self = this; + if (!instance && !shouldWait) { - self.isErrorGetter = $attrs.mdIsError && $parse($attrs.mdIsError); + // Report missing instance + $log.error( $mdUtil.supplant(errorMsg, [handle || ""]) ); - 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')); + // The component has not registered itself... most like NOT yet created + // return null to indicate that the Sidenav is not in the DOM + return undefined; } - }); - } -} -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; + return instance; + } - containerCtrl.label = element; - scope.$on('$destroy', function() { - containerCtrl.label = null; - }); + /** + * Asynchronously wait for the component instantiation, + * Deferred lookup of component instance using $component registry + */ + function waitForInstance(handle) { + return $mdComponentRegistry.when(handle).catch($log.error); } - }; } - /** * @ngdoc directive - * @name mdInput - * @restrict E - * @module material.components.input + * @name mdSidenavFocus + * @module material.components.sidenav * - * @description - * You can use any `` or ` - *
    - *
    This is required!
    - *
    That's too long!
    - *
    - * - * - * - * - * - * - * - * + * + *
    + * + * + * + * + *
    + *
    + *
    * * - *

    Notes

    + * + * var app = angular.module('myApp', ['ngMaterial']); + * app.controller('MyController', function($scope, $mdSidenav) { + * $scope.openLeftMenu = function() { + * $mdSidenav('left').toggle(); + * }; + * }); + * * - * - Requires [ngMessages](https://docs.angularjs.org/api/ngMessages). - * - Behaves like the [AngularJS input directive](https://docs.angularjs.org/api/ng/directive/input). + * @param {expression=} md-is-open A model bound to whether the sidenav is opened. + * @param {boolean=} md-disable-backdrop When present in the markup, the sidenav will not show a backdrop. + * @param {string=} md-component-id componentId to use with $mdSidenav service. + * @param {expression=} md-is-locked-open When this expression evaluates to true, + * the sidenav 'locks open': it falls into the content's flow instead + * of appearing over it. This overrides the `md-is-open` attribute. + * @param {string=} md-disable-scroll-target Selector, pointing to an element, whose scrolling will + * be disabled when the sidenav is opened. By default this is the sidenav's direct parent. * - * 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. +* The $mdMedia() service is exposed to the is-locked-open attribute, which + * can be given a media query or one of the `sm`, `gt-sm`, `md`, `gt-md`, `lg` or `gt-lg` presets. + * Examples: * + * - `` + * - `` + * - `` (locks open on small screens) */ - -function inputTextareaDirective($mdUtil, $window, $mdAria) { +function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $mdInteraction, $animate, + $compile, $parse, $log, $q, $document, $window, $$rAF) { return { restrict: 'E', - require: ['^?mdInputContainer', '?ngModel'], - link: postLink + scope: { + isOpen: '=?mdIsOpen' + }, + controller: '$mdSidenavController', + compile: function(element) { + element.addClass('md-closed').attr('tabIndex', '-1'); + return postLink; + } }; - function postLink(scope, element, attr, ctrls) { + /** + * Directive Post Link function... + */ + function postLink(scope, element, attr, sidenavCtrl) { + var lastParentOverFlow; + var backdrop; + var disableScrollTarget = null; + var triggeringInteractionType; + var triggeringElement = null; + var previousContainerStyles; + var promise = $q.when(true); + var isLockedOpenParsed = $parse(attr.mdIsLockedOpen); + var ngWindow = angular.element($window); + var isLocked = function() { + return isLockedOpenParsed(scope.$parent, { + $media: function(arg) { + $log.warn("$media is deprecated for is-locked-open. Use $mdMedia instead."); + return $mdMedia(arg); + }, + $mdMedia: $mdMedia + }); + }; - var containerCtrl = ctrls[0]; - var hasNgModel = !!ctrls[1]; - var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel(); - var isReadonly = angular.isDefined(attr.readonly); - var mdNoAsterisk = $mdUtil.parseAttributeBoolean(attr.mdNoAsterisk); + if (attr.mdDisableScrollTarget) { + disableScrollTarget = $document[0].querySelector(attr.mdDisableScrollTarget); + if (disableScrollTarget) { + disableScrollTarget = angular.element(disableScrollTarget); + } else { + $log.warn($mdUtil.supplant('mdSidenav: couldn\'t find element matching ' + + 'selector "{selector}". Falling back to parent.', { selector: attr.mdDisableScrollTarget })); + } + } - if (!containerCtrl) return; - if (containerCtrl.input) { - throw new Error(" can only have *one* , - * - * - * - */ -function mdSelectOnFocusDirective() { + // Draw the ticks with canvas. + // The alternative to drawing ticks with canvas is to draw one element for each tick, + // which could quickly become a performance bottleneck. + var tickCanvas, tickCtx; + function redrawTicks() { + if (!discrete || isDisabled()) return; + if ( angular.isUndefined(step) ) return; - return { - restrict: 'A', - link: postLink - }; + if ( step <= 0 ) { + var msg = 'Slider step value must be greater than zero when in discrete mode'; + $log.error(msg); + throw new Error(msg); + } - function postLink(scope, element, attr) { - if (element[0].nodeName !== 'INPUT' && element[0].nodeName !== "TEXTAREA") return; + var numSteps = Math.floor( (max - min) / step ); + if (!tickCanvas) { + tickCanvas = angular.element('').css('position', 'absolute'); + tickContainer.append(tickCanvas); - element.on('focus', onFocus); + tickCtx = tickCanvas[0].getContext('2d'); + } - scope.$on('$destroy', function() { - element.off('focus', onFocus); - }); + var dimensions = getSliderDimensions(); - function onFocus() { - // Use HTMLInputElement#select to fix firefox select issues - element[0].select(); - } - } -} + // If `dimensions` doesn't have height and width it might be the first attempt so we will refresh dimensions + if (dimensions && !dimensions.height && !dimensions.width) { + refreshSliderDimensions(); + dimensions = sliderDimensions; + } -var visibilityDirectives = ['ngIf', 'ngShow', 'ngHide', 'ngSwitchWhen', 'ngSwitchDefault']; -function ngMessagesDirective() { - return { - restrict: 'EA', - link: postLink, + tickCanvas[0].width = dimensions.width; + tickCanvas[0].height = dimensions.height; - // This is optional because we don't want target *all* ngMessage instances, just those inside of - // mdInputContainer. - require: '^^?mdInputContainer' - }; + var distance; + for (var i = 0; i <= numSteps; i++) { + var trackTicksStyle = $window.getComputedStyle(tickContainer[0]); + tickCtx.fillStyle = trackTicksStyle.color || 'black'; - function postLink(scope, element, attrs, inputContainer) { - // If we are not a child of an input container, don't do anything - if (!inputContainer) return; + distance = Math.floor((vertical ? dimensions.height : dimensions.width) * (i / numSteps)); - // Add our animation class - element.toggleClass('md-input-messages-animation', true); + tickCtx.fillRect(vertical ? 0 : distance - 1, + vertical ? distance - 1 : 0, + vertical ? dimensions.width : 2, + vertical ? 2 : dimensions.height); + } + } - // Add our md-auto-hide class to automatically hide/show messages when container is invalid - element.toggleClass('md-auto-hide', true); + function clearTicks() { + if(tickCanvas && tickCtx) { + var dimensions = getSliderDimensions(); + tickCtx.clearRect(0, 0, dimensions.width, dimensions.height); + } + } - // If we see some known visibility directives, remove the md-auto-hide class - if (attrs.mdAutoHide == 'false' || hasVisibiltyDirective(attrs)) { - element.toggleClass('md-auto-hide', false); + /** + * Refreshing Dimensions + */ + var sliderDimensions = {}; + refreshSliderDimensions(); + function refreshSliderDimensions() { + sliderDimensions = trackContainer[0].getBoundingClientRect(); + } + function getSliderDimensions() { + throttledRefreshDimensions(); + return sliderDimensions; } - } - function hasVisibiltyDirective(attrs) { - return visibilityDirectives.some(function(attr) { - return attrs[attr]; - }); - } -} + /** + * left/right/up/down arrow listener + */ + function keydownListener(ev) { + if (isDisabled()) return; -function ngMessageDirective($mdUtil) { - return { - restrict: 'EA', - compile: compile, - priority: 100 - }; + var changeAmount; + if (vertical ? ev.keyCode === $mdConstant.KEY_CODE.DOWN_ARROW : ev.keyCode === $mdConstant.KEY_CODE.LEFT_ARROW) { + changeAmount = -step; + } else if (vertical ? ev.keyCode === $mdConstant.KEY_CODE.UP_ARROW : ev.keyCode === $mdConstant.KEY_CODE.RIGHT_ARROW) { + changeAmount = step; + } + changeAmount = invert ? -changeAmount : changeAmount; + if (changeAmount) { + if (ev.metaKey || ev.ctrlKey || ev.altKey) { + changeAmount *= 4; + } + ev.preventDefault(); + ev.stopPropagation(); + scope.$evalAsync(function() { + setModelValue(ngModelCtrl.$viewValue + changeAmount); + }); + } + } - function compile(element) { - var inputContainer = $mdUtil.getClosest(element, "md-input-container"); + function mouseDownListener() { + redrawTicks(); - // If we are not a child of an input container, don't do anything - if (!inputContainer) return; + scope.mouseActive = true; + wrapper.removeClass('md-focused'); + + $timeout(function() { + scope.mouseActive = false; + }, 100); + } + + function focusListener() { + if (scope.mouseActive === false) { + wrapper.addClass('md-focused'); + } + } - // Add our animation class - element.toggleClass('md-input-message-animation', true); + function blurListener() { + wrapper.removeClass('md-focused'); + element.removeClass('md-active'); + clearTicks(); + } - return {}; - } -} -ngMessageDirective.$inject = ["$mdUtil"]; + /** + * ngModel setters and validators + */ + function setModelValue(value) { + ngModelCtrl.$setViewValue( minMaxValidator(stepValidator(value)) ); + } + function ngModelRender() { + if (isNaN(ngModelCtrl.$viewValue)) { + ngModelCtrl.$viewValue = ngModelCtrl.$modelValue; + } -function mdInputInvalidMessagesAnimation($q, $animateCss) { - return { - addClass: function(element, className, done) { - var messages = getMessagesElement(element); + ngModelCtrl.$viewValue = minMaxValidator(ngModelCtrl.$viewValue); - if (className == "md-input-invalid" && messages.hasClass('md-auto-hide')) { - showInputMessages(element, $animateCss, $q).finally(done); - } else { - done(); - } + var percent = valueToPercent(ngModelCtrl.$viewValue); + scope.modelValue = ngModelCtrl.$viewValue; + element.attr('aria-valuenow', ngModelCtrl.$viewValue); + setSliderPercent(percent); + thumbText.text( ngModelCtrl.$viewValue ); } - // NOTE: We do not need the removeClass method, because the message ng-leave animation will fire - } -} -mdInputInvalidMessagesAnimation.$inject = ["$q", "$animateCss"]; + function minMaxValidator(value, minValue, maxValue) { + if (angular.isNumber(value)) { + minValue = angular.isNumber(minValue) ? minValue : min; + maxValue = angular.isNumber(maxValue) ? maxValue : max; -function ngMessagesAnimation($q, $animateCss) { - return { - enter: function(element, done) { - showInputMessages(element, $animateCss, $q).finally(done); - }, + return Math.max(minValue, Math.min(maxValue, value)); + } + } - leave: function(element, done) { - hideInputMessages(element, $animateCss, $q).finally(done); - }, + function stepValidator(value) { + if (angular.isNumber(value)) { + var formattedValue = (Math.round((value - min) / step) * step + min); + formattedValue = (Math.round(formattedValue * Math.pow(10, round)) / Math.pow(10, round)); - addClass: function(element, className, done) { - if (className == "ng-hide") { - hideInputMessages(element, $animateCss, $q).finally(done); - } else { - done(); - } - }, + if (containerCtrl && containerCtrl.fitInputWidthToTextLength){ + $mdUtil.debounce(function () { + containerCtrl.fitInputWidthToTextLength(formattedValue.toString().length); + }, 100)(); + } - removeClass: function(element, className, done) { - if (className == "ng-hide") { - showInputMessages(element, $animateCss, $q).finally(done); - } else { - done(); + return formattedValue; } } - } -} -ngMessagesAnimation.$inject = ["$q", "$animateCss"]; -function ngMessageAnimation($animateCss) { - return { - enter: function(element, done) { - var messages = getMessagesElement(element); + /** + * @param percent 0-1 + */ + function setSliderPercent(percent) { - // If we have the md-auto-hide class, the md-input-invalid animation will fire, so we can skip - if (messages.hasClass('md-auto-hide')) { - done(); - return; + percent = clamp(percent); + + var thumbPosition = (percent * 100) + '%'; + var activeTrackPercent = invert ? (1 - percent) * 100 + '%' : thumbPosition; + + if (vertical) { + thumbContainer.css('bottom', thumbPosition); + } + else { + $mdUtil.bidiProperty(thumbContainer, 'left', 'right', thumbPosition); } - return showMessage(element, $animateCss); - }, + + activeTrack.css(vertical ? 'height' : 'width', activeTrackPercent); - leave: function(element, done) { - return hideMessage(element, $animateCss); + element.toggleClass((invert ? 'md-max' : 'md-min'), percent === 0); + element.toggleClass((invert ? 'md-min' : 'md-max'), percent === 1); } - } -} -ngMessageAnimation.$inject = ["$animateCss"]; -function showInputMessages(element, $animateCss, $q) { - var animators = [], animator; - var messages = getMessagesElement(element); + /** + * Slide listeners + */ + var isDragging = false; - angular.forEach(messages.children(), function(child) { - animator = showMessage(angular.element(child), $animateCss); + function onPressDown(ev) { + if (isDisabled()) return; - animators.push(animator.start()); - }); + element.addClass('md-active'); + element[0].focus(); + refreshSliderDimensions(); - return $q.all(animators); -} + var exactVal = percentToValue( positionToPercent( vertical ? ev.pointer.y : ev.pointer.x )); + var closestVal = minMaxValidator( stepValidator(exactVal) ); + scope.$apply(function() { + setModelValue( closestVal ); + setSliderPercent( valueToPercent(closestVal)); + }); + } + function onPressUp(ev) { + if (isDisabled()) return; -function hideInputMessages(element, $animateCss, $q) { - var animators = [], animator; - var messages = getMessagesElement(element); + element.removeClass('md-dragging'); - angular.forEach(messages.children(), function(child) { - animator = hideMessage(angular.element(child), $animateCss); + var exactVal = percentToValue( positionToPercent( vertical ? ev.pointer.y : ev.pointer.x )); + var closestVal = minMaxValidator( stepValidator(exactVal) ); + scope.$apply(function() { + setModelValue(closestVal); + ngModelRender(); + }); + } + function onDragStart(ev) { + if (isDisabled()) return; + isDragging = true; - animators.push(animator.start()); - }); + ev.stopPropagation(); - return $q.all(animators); -} + element.addClass('md-dragging'); + setSliderFromEvent(ev); + } + function onDrag(ev) { + if (!isDragging) return; + ev.stopPropagation(); + setSliderFromEvent(ev); + } + function onDragEnd(ev) { + if (!isDragging) return; + ev.stopPropagation(); + isDragging = false; + } -function showMessage(element, $animateCss) { - var height = element[0].offsetHeight; + function setSliderFromEvent(ev) { + // While panning discrete, update only the + // visual positioning but not the model value. + if ( discrete ) adjustThumbPosition( vertical ? ev.pointer.y : ev.pointer.x ); + else doSlide( vertical ? ev.pointer.y : ev.pointer.x ); + } - return $animateCss(element, { - event: 'enter', - structural: true, - from: {"opacity": 0, "margin-top": -height + "px"}, - to: {"opacity": 1, "margin-top": "0"}, - duration: 0.3 - }); -} + /** + * Slide the UI by changing the model value + * @param x + */ + function doSlide( x ) { + scope.$evalAsync( function() { + setModelValue( percentToValue( positionToPercent(x) )); + }); + } -function hideMessage(element, $animateCss) { - var height = element[0].offsetHeight; - var styles = window.getComputedStyle(element[0]); + /** + * Slide the UI without changing the model (while dragging/panning) + * @param x + */ + function adjustThumbPosition( x ) { + var exactVal = percentToValue( positionToPercent( x )); + var closestVal = minMaxValidator( stepValidator(exactVal) ); + setSliderPercent( positionToPercent(x) ); + thumbText.text( closestVal ); + } - // If we are already hidden, just return an empty animation - if (styles.opacity == 0) { - return $animateCss(element, {}); - } + /** + * Clamps the value to be between 0 and 1. + * @param {number} value The value to clamp. + * @returns {number} + */ + function clamp(value) { + return Math.max(0, Math.min(value || 0, 1)); + } - // Otherwise, animate - return $animateCss(element, { - event: 'leave', - structural: true, - from: {"opacity": 1, "margin-top": 0}, - to: {"opacity": 0, "margin-top": -height + "px"}, - duration: 0.3 - }); -} + /** + * Convert position on slider to percentage value of offset from beginning... + * @param position + * @returns {number} + */ + function positionToPercent( position ) { + var offset = vertical ? sliderDimensions.top : sliderDimensions.left; + var size = vertical ? sliderDimensions.height : sliderDimensions.width; + var calc = (position - offset) / size; -function getInputElement(element) { - var inputContainer = element.controller('mdInputContainer'); + if (!vertical && $mdUtil.bidi() === 'rtl') { + calc = 1 - calc; + } - return inputContainer.element; -} + return Math.max(0, Math.min(1, vertical ? 1 - calc : calc)); + } -function getMessagesElement(element) { - var input = getInputElement(element); + /** + * Convert percentage offset on slide to equivalent model value + * @param percent + * @returns {*} + */ + function percentToValue( percent ) { + var adjustedPercent = invert ? (1 - percent) : percent; + return (min + adjustedPercent * (max - min)); + } - return angular.element(input[0].querySelector('.md-input-messages-animation')); + function valueToPercent( val ) { + var percent = (val - min) / (max - min); + return invert ? (1 - percent) : percent; + } + } } })(); @@ -12568,395 +21526,483 @@ function getMessagesElement(element) { /** * @ngdoc module - * @name material.components.list - * @description - * List module - */ -angular.module('material.components.list', [ - 'material.core' -]) - .controller('MdListController', MdListController) - .directive('mdList', mdListDirective) - .directive('mdListItem', mdListItemDirective); - -/** - * @ngdoc directive - * @name mdList - * @module material.components.list - * - * @restrict E - * + * @name material.components.sticky * @description - * The `` directive is a list container for 1..n `` tags. + * Sticky effects for md * - * @usage - * - * - * - * - *
    - *

    {{item.title}}

    - *

    {{item.description}}

    - *
    - *
    - *
    - *
    */ +MdSticky.$inject = ["$mdConstant", "$$rAF", "$mdUtil", "$compile"]; +angular + .module('material.components.sticky', [ + 'material.core', + 'material.components.content' + ]) + .factory('$mdSticky', MdSticky); -function mdListDirective($mdTheming) { - return { - restrict: 'E', - compile: function(tEl) { - tEl[0].setAttribute('role', 'list'); - return $mdTheming; - } - }; -} -mdListDirective.$inject = ["$mdTheming"]; /** - * @ngdoc directive - * @name mdListItem - * @module material.components.list - * - * @restrict E + * @ngdoc service + * @name $mdSticky + * @module material.components.sticky * * @description - * The `` directive is a container intended for row items in a `` container. - * The `md-2-line` and `md-3-line` classes can be added to a `` - * to increase the height with 22px and 40px respectively. + * The `$mdSticky`service provides a mixin to make elements sticky. * - * ## CSS - * `.md-avatar` - class for image avatars + * Whenever the current browser supports stickiness natively, the `$mdSticky` service will just + * use the native browser stickiness. + * + * By default the `$mdSticky` service compiles the cloned element, when not specified through the `elementClone` + * parameter, in the same scope as the actual element lives. * - * `.md-avatar-icon` - class for icon avatars * - * `.md-offset` - on content without an avatar + *

    Notes

    + * When using an element which is containing a compiled directive, which changed its DOM structure during compilation, + * you should compile the clone yourself using the plain template.

    + * See the right usage below: + * + * angular.module('myModule') + * .directive('stickySelect', function($mdSticky, $compile) { + * var SELECT_TEMPLATE = + * '' + + * 'Option 1' + + * ''; + * + * return { + * restrict: 'E', + * replace: true, + * template: SELECT_TEMPLATE, + * link: function(scope,element) { + * $mdSticky(scope, element, $compile(SELECT_TEMPLATE)(scope)); + * } + * }; + * }); + * * * @usage - * - * - * - * - * Item content in list - * - * - * - * Item content in list - * - * + * + * angular.module('myModule') + * .directive('stickyText', function($mdSticky, $compile) { + * return { + * restrict: 'E', + * template: 'Sticky Text', + * link: function(scope,element) { + * $mdSticky(scope, element); + * } + * }; + * }); * * - * _**Note:** We automatically apply special styling when the inner contents are wrapped inside - * of a `` tag. This styling is automatically ignored for `class="md-secondary"` buttons - * and you can include a class of `class="md-exclude"` if you need to use a non-secondary button - * that is inside the list, but does not wrap the contents._ + * @returns A `$mdSticky` function that takes three arguments: + * - `scope` + * - `element`: The element that will be 'sticky' + * - `elementClone`: A clone of the element, that will be shown + * when the user starts scrolling past the original element. + * If not provided, it will use the result of `element.clone()` and compiles it in the given scope. */ -function mdListItemDirective($mdAria, $mdConstant, $mdUtil, $timeout) { - var proxiedTypes = ['md-checkbox', 'md-switch']; - return { - restrict: 'E', - controller: 'MdListController', - compile: function(tEl, tAttrs) { - // Check for proxy controls (no ng-click on parent, and a control inside) - var secondaryItems = tEl[0].querySelectorAll('.md-secondary'); - var hasProxiedElement; - var proxyElement; +function MdSticky($mdConstant, $$rAF, $mdUtil, $compile) { - tEl[0].setAttribute('role', 'listitem'); + var browserStickySupport = $mdUtil.checkStickySupport(); - if (tAttrs.ngClick || tAttrs.ngHref || tAttrs.href || tAttrs.uiSref || tAttrs.ngAttrUiSref) { - wrapIn('button'); - } else { - for (var i = 0, type; type = proxiedTypes[i]; ++i) { - if (proxyElement = tEl[0].querySelector(type)) { - hasProxiedElement = true; - break; - } - } - if (hasProxiedElement) { - wrapIn('div'); - } else if (!tEl[0].querySelector('md-button:not(.md-secondary):not(.md-exclude)')) { - tEl.addClass('_md-no-proxy'); - } + /** + * Registers an element as sticky, used internally by directives to register themselves + */ + return function registerStickyElement(scope, element, stickyClone) { + var contentCtrl = element.controller('mdContent'); + if (!contentCtrl) return; + + if (browserStickySupport) { + element.css({ + position: browserStickySupport, + top: 0, + 'z-index': 2 + }); + } else { + var $$sticky = contentCtrl.$element.data('$$sticky'); + if (!$$sticky) { + $$sticky = setupSticky(contentCtrl); + contentCtrl.$element.data('$$sticky', $$sticky); } - wrapSecondaryItems(); - setupToggleAria(); + // Compile our cloned element, when cloned in this service, into the given scope. + var cloneElement = stickyClone || $compile(element.clone())(scope); - function setupToggleAria() { - var toggleTypes = ['md-switch', 'md-checkbox']; - var toggle; + var deregister = $$sticky.add(element, cloneElement); + scope.$on('$destroy', deregister); + } + }; - for (var i = 0, toggleType; toggleType = toggleTypes[i]; ++i) { - if (toggle = tEl.find(toggleType)[0]) { - if (!toggle.hasAttribute('aria-label')) { - var p = tEl.find('p')[0]; - if (!p) return; - toggle.setAttribute('aria-label', 'Toggle ' + p.textContent); - } + function setupSticky(contentCtrl) { + var contentEl = contentCtrl.$element; + + // Refresh elements is very expensive, so we use the debounced + // version when possible. + var debouncedRefreshElements = $$rAF.throttle(refreshElements); + + // setupAugmentedScrollEvents gives us `$scrollstart` and `$scroll`, + // more reliable than `scroll` on android. + setupAugmentedScrollEvents(contentEl); + contentEl.on('$scrollstart', debouncedRefreshElements); + contentEl.on('$scroll', onScroll); + + var self; + return self = { + prev: null, + current: null, //the currently stickied item + next: null, + items: [], + add: add, + refreshElements: refreshElements + }; + + /*************** + * Public + ***************/ + // Add an element and its sticky clone to this content's sticky collection + function add(element, stickyClone) { + stickyClone.addClass('md-sticky-clone'); + + var item = { + element: element, + clone: stickyClone + }; + self.items.push(item); + + $mdUtil.nextTick(function() { + contentEl.prepend(item.clone); + }); + + debouncedRefreshElements(); + + return function remove() { + self.items.forEach(function(item, index) { + if (item.element[0] === element[0]) { + self.items.splice(index, 1); + item.clone.remove(); } + }); + debouncedRefreshElements(); + }; + } + + function refreshElements() { + // Sort our collection of elements by their current position in the DOM. + // We need to do this because our elements' order of being added may not + // be the same as their order of display. + self.items.forEach(refreshPosition); + self.items = self.items.sort(function(a, b) { + return a.top < b.top ? -1 : 1; + }); + + // Find which item in the list should be active, + // based upon the content's current scroll position + var item; + var currentScrollTop = contentEl.prop('scrollTop'); + for (var i = self.items.length - 1; i >= 0; i--) { + if (currentScrollTop > self.items[i].top) { + item = self.items[i]; + break; } } + setCurrentItem(item); + } - function wrapIn(type) { - var container; - if (type == 'div') { - container = angular.element('
    '); - container.append(tEl.contents()); - tEl.addClass('_md-proxy-focus'); - } else { - // Element which holds the default list-item content. - container = angular.element( - '' - ); + /*************** + * Private + ***************/ - // Button which shows ripple and executes primary action. - var buttonWrap = angular.element( - '' - ); + // Find the `top` of an item relative to the content element, + // and also the height. + function refreshPosition(item) { + // Find the top of an item by adding to the offsetHeight until we reach the + // content element. + var current = item.element[0]; + item.top = 0; + item.left = 0; + item.right = 0; + while (current && current !== contentEl[0]) { + item.top += current.offsetTop; + item.left += current.offsetLeft; + if ( current.offsetParent ){ + item.right += current.offsetParent.offsetWidth - current.offsetWidth - current.offsetLeft; //Compute offsetRight + } + current = current.offsetParent; + } + item.height = item.element.prop('offsetHeight'); - buttonWrap[0].setAttribute('aria-label', tEl[0].textContent); - copyAttributes(tEl[0], buttonWrap[0]); + var defaultVal = $mdUtil.floatingScrollbars() ? '0' : undefined; + $mdUtil.bidi(item.clone, 'margin-left', item.left, defaultVal); + $mdUtil.bidi(item.clone, 'margin-right', defaultVal, item.right); + } - // Append the button wrap before our list-item content, because it will overlay in relative. - container.prepend(buttonWrap); - container.children().eq(1).append(tEl.contents()); - - tEl.addClass('_md-button-wrap'); - } + // As we scroll, push in and select the correct sticky element. + function onScroll() { + var scrollTop = contentEl.prop('scrollTop'); + var isScrollingDown = scrollTop > (onScroll.prevScrollTop || 0); - tEl[0].setAttribute('tabindex', '-1'); - tEl.append(container); + // Store the previous scroll so we know which direction we are scrolling + onScroll.prevScrollTop = scrollTop; + + // + // AT TOP (not scrolling) + // + if (scrollTop === 0) { + // If we're at the top, just clear the current item and return + setCurrentItem(null); + return; } - function wrapSecondaryItems() { - if (secondaryItems.length === 1) { - wrapSecondaryItem(secondaryItems[0], tEl); - } else if (secondaryItems.length > 1) { - var secondaryItemsWrapper = angular.element('
    '); - angular.forEach(secondaryItems, function(secondaryItem) { - wrapSecondaryItem(secondaryItem, secondaryItemsWrapper, true); - }); - tEl.append(secondaryItemsWrapper); + // + // SCROLLING DOWN (going towards the next item) + // + if (isScrollingDown) { + + // If we've scrolled down past the next item's position, sticky it and return + if (self.next && self.next.top <= scrollTop) { + setCurrentItem(self.next); + return; + } + + // If the next item is close to the current one, push the current one up out of the way + if (self.current && self.next && self.next.top - scrollTop <= self.next.height) { + translate(self.current, scrollTop + (self.next.top - self.next.height - scrollTop)); + return; } } - function wrapSecondaryItem(secondaryItem, container, hasSecondaryItemsWrapper) { - if (secondaryItem && !isButton(secondaryItem) && secondaryItem.hasAttribute('ng-click')) { - $mdAria.expect(secondaryItem, 'aria-label'); - var buttonWrapper; - if (hasSecondaryItemsWrapper) { - buttonWrapper = angular.element(''); - } else { - buttonWrapper = angular.element(''); - } - copyAttributes(secondaryItem, buttonWrapper[0]); - secondaryItem.setAttribute('tabindex', '-1'); - secondaryItem.classList.remove('md-secondary'); - buttonWrapper.append(secondaryItem); - secondaryItem = buttonWrapper[0]; + // + // SCROLLING UP (not at the top & not scrolling down; must be scrolling up) + // + if (!isScrollingDown) { + + // If we've scrolled up past the previous item's position, sticky it and return + if (self.current && self.prev && scrollTop < self.current.top) { + setCurrentItem(self.prev); + return; } - // Check for a secondary item and move it outside - if ( secondaryItem && ( - secondaryItem.hasAttribute('ng-click') || - ( tAttrs.ngClick && - isProxiedElement(secondaryItem) ) - )) { - // When using multiple secondary items we need to remove their secondary class to be - // orderd correctly in the list-item - if (hasSecondaryItemsWrapper) { - secondaryItem.classList.remove('md-secondary'); - } - tEl.addClass('md-with-secondary'); - container.append(secondaryItem); + // If the next item is close to the current one, pull the current one down into view + if (self.next && self.current && (scrollTop >= (self.next.top - self.current.height))) { + translate(self.current, scrollTop + (self.next.top - scrollTop - self.current.height)); + return; } } - function copyAttributes(item, wrapper) { - var copiedAttrs = ['ng-if', 'ng-click', 'aria-label', 'ng-disabled', - 'ui-sref', 'href', 'ng-href', 'ng-attr-ui-sref']; - angular.forEach(copiedAttrs, function(attr) { - if (item.hasAttribute(attr)) { - wrapper.setAttribute(attr, item.getAttribute(attr)); - item.removeAttribute(attr); - } - }); + // + // Otherwise, just move the current item to the proper place (scrolling up or down) + // + if (self.current) { + translate(self.current, scrollTop); } + } - function isProxiedElement(el) { - return proxiedTypes.indexOf(el.nodeName.toLowerCase()) != -1; + function setCurrentItem(item) { + if (self.current === item) return; + // Deactivate currently active item + if (self.current) { + translate(self.current, null); + setStickyState(self.current, null); } - function isButton(el) { - var nodeName = el.nodeName.toUpperCase(); - - return nodeName == "MD-BUTTON" || nodeName == "BUTTON"; + // Activate new item if given + if (item) { + setStickyState(item, 'active'); } - return postLink; - - function postLink($scope, $element, $attr, ctrl) { - - var proxies = [], - firstChild = $element[0].firstElementChild, - hasClick = firstChild && firstChild.firstElementChild && - hasClickEvent(firstChild.firstElementChild); - - computeProxies(); - computeClickable(); - - if ($element.hasClass('_md-proxy-focus') && proxies.length) { - angular.forEach(proxies, function(proxy) { - proxy = angular.element(proxy); - - $scope.mouseActive = false; - proxy.on('mousedown', function() { - $scope.mouseActive = true; - $timeout(function(){ - $scope.mouseActive = false; - }, 100); - }) - .on('focus', function() { - if ($scope.mouseActive === false) { $element.addClass('md-focused'); } - proxy.on('blur', function proxyOnBlur() { - $element.removeClass('md-focused'); - proxy.off('blur', proxyOnBlur); - }); - }); - }); - } - - function hasClickEvent (element) { - var attr = element.attributes; - for (var i = 0; i < attr.length; i++) { - if ($attr.$normalize(attr[i].name) === 'ngClick') return true; - } - return false; - } + self.current = item; + var index = self.items.indexOf(item); + // If index === -1, index + 1 = 0. It works out. + self.next = self.items[index + 1]; + self.prev = self.items[index - 1]; + setStickyState(self.next, 'next'); + setStickyState(self.prev, 'prev'); + } - function computeProxies() { - var children = $element.children(); - if (children.length && !children[0].hasAttribute('ng-click')) { - angular.forEach(proxiedTypes, function(type) { - angular.forEach(firstChild.querySelectorAll(type), function(child) { - proxies.push(child); - }); - }); - } - } - function computeClickable() { - if (proxies.length == 1 || hasClick) { - $element.addClass('md-clickable'); + function setStickyState(item, state) { + if (!item || item.state === state) return; + if (item.state) { + item.clone.attr('sticky-prev-state', item.state); + item.element.attr('sticky-prev-state', item.state); + } + item.clone.attr('sticky-state', state); + item.element.attr('sticky-state', state); + item.state = state; + } - if (!hasClick) { - ctrl.attachRipple($scope, angular.element($element[0].querySelector('._md-no-style'))); - } - } + function translate(item, amount) { + if (!item) return; + if (amount === null || amount === undefined) { + if (item.translateY) { + item.translateY = null; + item.clone.css($mdConstant.CSS.TRANSFORM, ''); } + } else { + item.translateY = amount; - var firstChildKeypressListener = function(e) { - if (e.target.nodeName != 'INPUT' && e.target.nodeName != 'TEXTAREA' && !e.target.isContentEditable) { - var keyCode = e.which || e.keyCode; - if (keyCode == $mdConstant.KEY_CODE.SPACE) { - if (firstChild) { - firstChild.click(); - e.preventDefault(); - e.stopPropagation(); - } - } - } - }; - - if (!hasClick && !proxies.length) { - firstChild && firstChild.addEventListener('keypress', firstChildKeypressListener); - } + $mdUtil.bidi( item.clone, $mdConstant.CSS.TRANSFORM, + 'translate3d(' + item.left + 'px,' + amount + 'px,0)', + 'translateY(' + amount + 'px)' + ); + } + } + } - $element.off('click'); - $element.off('keypress'); - if (proxies.length == 1 && firstChild) { - $element.children().eq(0).on('click', function(e) { - var parentButton = $mdUtil.getClosest(e.target, 'BUTTON'); - if (!parentButton && firstChild.contains(e.target)) { - angular.forEach(proxies, function(proxy) { - if (e.target !== proxy && !proxy.contains(e.target)) { - angular.element(proxy).triggerHandler('click'); - } - }); - } - }); - } + // Android 4.4 don't accurately give scroll events. + // To fix this problem, we setup a fake scroll event. We say: + // > If a scroll or touchmove event has happened in the last DELAY milliseconds, + // then send a `$scroll` event every animationFrame. + // Additionally, we add $scrollstart and $scrollend events. + function setupAugmentedScrollEvents(element) { + var SCROLL_END_DELAY = 200; + var isScrolling; + var lastScrollTime; + element.on('scroll touchmove', function() { + if (!isScrolling) { + isScrolling = true; + $$rAF.throttle(loopScrollEvent); + element.triggerHandler('$scrollstart'); + } + element.triggerHandler('$scroll'); + lastScrollTime = +$mdUtil.now(); + }); - $scope.$on('$destroy', function () { - firstChild && firstChild.removeEventListener('keypress', firstChildKeypressListener); - }); + function loopScrollEvent() { + if (+$mdUtil.now() - lastScrollTime > SCROLL_END_DELAY) { + isScrolling = false; + element.triggerHandler('$scrollend'); + } else { + element.triggerHandler('$scroll'); + $$rAF.throttle(loopScrollEvent); } } - }; -} -mdListItemDirective.$inject = ["$mdAria", "$mdConstant", "$mdUtil", "$timeout"]; - -/* - * @private - * @ngdoc controller - * @name MdListController - * @module material.components.list - * - */ -function MdListController($scope, $element, $mdListInkRipple) { - var ctrl = this; - ctrl.attachRipple = attachRipple; - - function attachRipple (scope, element) { - var options = {}; - $mdListInkRipple.attach(scope, element, options); } -} -MdListController.$inject = ["$scope", "$element", "$mdListInkRipple"]; +} })(); (function(){ "use strict"; -/** - * @ngdoc module - * @name material.components.menu - */ +/** + * @ngdoc module + * @name material.components.subheader + * @description + * SubHeader module + * + * Subheaders are special list tiles that delineate distinct sections of a + * list or grid list and are typically related to the current filtering or + * sorting criteria. Subheader tiles are either displayed inline with tiles or + * can be associated with content, for example, in an adjacent column. + * + * Upon scrolling, subheaders remain pinned to the top of the screen and remain + * pinned until pushed on or off screen by the next subheader. @see [Material + * Design Specifications](https://www.google.com/design/spec/components/subheaders.html) + * + * > To improve the visual grouping of content, use the system color for your subheaders. + * + */ +MdSubheaderDirective.$inject = ["$mdSticky", "$compile", "$mdTheming", "$mdUtil", "$mdAria"]; +angular + .module('material.components.subheader', [ + 'material.core', + 'material.components.sticky' + ]) + .directive('mdSubheader', MdSubheaderDirective); + +/** + * @ngdoc directive + * @name mdSubheader + * @module material.components.subheader + * + * @restrict E + * + * @description + * The `md-subheader` directive creates a sticky subheader for a section. + * + * Developers are able to disable the stickiness of the subheader by using the following markup + * + * + * Not Sticky + * + * + * ### Notes + * - The `md-subheader` directive uses the $mdSticky service + * to make the subheader sticky. + * + * > Whenever the current browser doesn't support stickiness natively, the subheader + * will be compiled twice to create a sticky clone of the subheader. + * + * @usage + * + * Online Friends + * + */ + +function MdSubheaderDirective($mdSticky, $compile, $mdTheming, $mdUtil, $mdAria) { + return { + restrict: 'E', + replace: true, + transclude: true, + template: ( + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + ), + link: function postLink(scope, element, attr, controllers, transclude) { + $mdTheming(element); + element.addClass('_md'); + + // Remove the ngRepeat attribute from the root element, because we don't want to compile + // the ngRepeat for the sticky clone again. + $mdUtil.prefixer().removeAttribute(element, 'ng-repeat'); -angular.module('material.components.menu', [ - 'material.core', - 'material.components.backdrop' -]); + var outerHTML = element[0].outerHTML; -})(); -(function(){ -"use strict"; + function getContent(el) { + return angular.element(el[0].querySelector('.md-subheader-content')); + } -/** - * @ngdoc module - * @name material.components.menu-bar - */ + // Set the ARIA attributes on the original element since it keeps it's original place in + // the DOM, whereas the clones are in reverse order. Should be done after the outerHTML, + // in order to avoid having multiple element be marked as headers. + attr.$set('role', 'heading'); + $mdAria.expect(element, 'aria-level', '2'); -angular.module('material.components.menuBar', [ - 'material.core', - 'material.components.menu' -]); + // Transclude the user-given contents of the subheader + // the conventional way. + transclude(scope, function(clone) { + getContent(element).append(clone); + }); -})(); -(function(){ -"use strict"; + // Create another clone, that uses the outer and inner contents + // of the element, that will be 'stickied' as the user scrolls. + if (!element.hasClass('md-no-sticky')) { + transclude(scope, function(clone) { + // If the user adds an ng-if or ng-repeat directly to the md-subheader element, the + // compiled clone below will only be a comment tag (since they replace their elements with + // a comment) which cannot be properly passed to the $mdSticky; so we wrap it in our own + // DIV to ensure we have something $mdSticky can use + var wrapper = $compile('')(scope); -/** - * @ngdoc module - * @name material.components.progressCircular - * @description Module for a circular progressbar - */ + // Delay initialization until after any `ng-if`/`ng-repeat`/etc has finished before + // attempting to create the clone + $mdUtil.nextTick(function() { + // Append our transcluded clone into the wrapper. + // We don't have to recompile the element again, because the clone is already + // compiled in it's transclusion scope. If we recompile the outerHTML of the new clone, we would lose + // our ngIf's and other previous registered bindings / properties. + getContent(wrapper).append(clone); + }); -angular.module('material.components.progressCircular', ['material.core']); + // Make the element sticky and provide the stickyClone our self, to avoid recompilation of the subheader + // element. + $mdSticky(scope, element, wrapper); + }); + } + } + }; +} })(); (function(){ @@ -12964,189 +22010,101 @@ angular.module('material.components.progressCircular', ['material.core']); /** * @ngdoc module - * @name material.components.progressLinear - * @description Linear Progress module! + * @name material.components.swipe + * @description Swipe module! */ -angular.module('material.components.progressLinear', [ - 'material.core' -]) - .directive('mdProgressLinear', MdProgressLinearDirective); - /** * @ngdoc directive - * @name mdProgressLinear - * @module material.components.progressLinear - * @restrict E + * @module material.components.swipe + * @name mdSwipeLeft + * + * @restrict A * * @description - * The linear progress directive is used to make loading content - * in your app as delightful and painless as possible by minimizing - * the amount of visual change a user sees before they can view - * and interact with content. + * The md-swipe-left directive allows you to specify custom behavior when an element is swiped + * left. * - * Each operation should only be represented by one activity indicator - * For example: one refresh operation should not display both a - * refresh bar and an activity circle. + * @usage + * + *
    Swipe me left!
    + *
    + */ +/** + * @ngdoc directive + * @module material.components.swipe + * @name mdSwipeRight * - * For operations where the percentage of the operation completed - * can be determined, use a determinate indicator. They give users - * a quick sense of how long an operation will take. + * @restrict A * - * For operations where the user is asked to wait a moment while - * something finishes up, and it’s not necessary to expose what's - * happening behind the scenes and how long it will take, use an - * indeterminate indicator. + * @description + * The md-swipe-right directive allows you to specify custom behavior when an element is swiped + * right. * - * @param {string} md-mode Select from one of four modes: determinate, indeterminate, buffer or query. + * @usage + * + *
    Swipe me right!
    + *
    + */ +/** + * @ngdoc directive + * @module material.components.swipe + * @name mdSwipeUp * - * Note: if the `md-mode` value is set as undefined or specified as 1 of the four (4) valid modes, then `indeterminate` - * will be auto-applied as the mode. + * @restrict A * - * Note: if not configured, the `md-mode="indeterminate"` will be auto injected as an attribute. If `value=""` is also specified, however, - * then `md-mode="determinate"` would be auto-injected instead. - * @param {number=} value In determinate and buffer modes, this number represents the percentage of the primary progress bar. Default: 0 - * @param {number=} md-buffer-value In the buffer mode, this number represents the percentage of the secondary progress bar. Default: 0 + * @description + * The md-swipe-up directive allows you to specify custom behavior when an element is swiped + * up. * * @usage * - * - * - * + *
    Swipe me up!
    + *
    + */ +/** + * @ngdoc directive + * @module material.components.swipe + * @name mdSwipeDown * - * + * @restrict A * - * + * @description + * The md-swipe-down directive allows you to specify custom behavior when an element is swiped + * down. * - * + * @usage + * + *
    Swipe me down!
    *
    */ -function MdProgressLinearDirective($mdTheming, $mdUtil, $log) { - var MODE_DETERMINATE = "determinate", - MODE_INDETERMINATE = "indeterminate", - MODE_BUFFER = "buffer", - MODE_QUERY = "query"; - - return { - restrict: 'E', - template: '
    ' + - '
    ' + - '
    ' + - '
    ' + - '
    ', - compile: compile - }; - - function compile(tElement, tAttrs, transclude) { - tElement.attr('aria-valuemin', 0); - tElement.attr('aria-valuemax', 100); - tElement.attr('role', 'progressbar'); - - return postLink; - } - function postLink(scope, element, attr) { - $mdTheming(element); - - var lastMode, toVendorCSS = $mdUtil.dom.animator.toCss; - var bar1 = angular.element(element[0].querySelector('._md-bar1')), - bar2 = angular.element(element[0].querySelector('._md-bar2')), - container = angular.element(element[0].querySelector('._md-container')); - - element.attr('md-mode', mode()); - - validateMode(); - watchAttributes(); - - /** - * Watch the value, md-buffer-value, and md-mode attributes - */ - function watchAttributes() { - attr.$observe('value', function(value) { - var percentValue = clamp(value); - element.attr('aria-valuenow', percentValue); - - if (mode() != MODE_QUERY) animateIndicator(bar2, percentValue); - }); - - attr.$observe('mdBufferValue', function(value) { - animateIndicator(bar1, clamp(value)); - }); - - attr.$observe('mdMode',function(mode){ - if (lastMode) container.removeClass( lastMode ); - switch( mode ) { - case MODE_QUERY: - case MODE_BUFFER: - case MODE_DETERMINATE: - case MODE_INDETERMINATE: - container.addClass( lastMode = "_md-mode-" + mode ); - break; - default: - container.addClass( lastMode = "_md-mode-" + MODE_INDETERMINATE ); - break; - } - }); - } +angular.module('material.components.swipe', ['material.core']) + .directive('mdSwipeLeft', getDirective('SwipeLeft')) + .directive('mdSwipeRight', getDirective('SwipeRight')) + .directive('mdSwipeUp', getDirective('SwipeUp')) + .directive('mdSwipeDown', getDirective('SwipeDown')); - /** - * Auto-defaults the mode to either `determinate` or `indeterminate` mode; if not specified - */ - function validateMode() { - if ( angular.isUndefined(attr.mdMode) ) { - var hasValue = angular.isDefined(attr.value); - var mode = hasValue ? MODE_DETERMINATE : MODE_INDETERMINATE; - var info = "Auto-adding the missing md-mode='{0}' to the ProgressLinear element"; +function getDirective(name) { + DirectiveFactory.$inject = ["$parse"]; + var directiveName = 'md' + name; + var eventName = '$md.' + name.toLowerCase(); - $log.debug( $mdUtil.supplant(info, [mode]) ); + return DirectiveFactory; - element.attr("md-mode",mode); - attr['mdMode'] = mode; - } - } + /* @ngInject */ + function DirectiveFactory($parse) { + return { restrict: 'A', link: postLink }; + function postLink(scope, element, attr) { + element.css('touch-action', 'none'); - /** - * Is the md-mode a valid option? - */ - function mode() { - var value = (attr.mdMode || "").trim(); - if ( value ) { - switch(value) { - case MODE_DETERMINATE: - case MODE_INDETERMINATE: - case MODE_BUFFER: - case MODE_QUERY: - break; - default: - value = MODE_INDETERMINATE; - break; - } + var fn = $parse(attr[directiveName]); + element.on(eventName, function(ev) { + scope.$applyAsync(function() { fn(scope, { $event: ev }); }); + }); } - return value; - } - - /** - * Manually set CSS to animate the Determinate indicator based on the specified - * percentage value (0-100). - */ - function animateIndicator(target, value) { - if ( !mode() ) return; - - var to = $mdUtil.supplant("translateX({0}%) scale({1},1)", [ (value-100)/2, value/100 ]); - var styles = toVendorCSS({ transform : to }); - angular.element(target).css( styles ); - } - } - - /** - * Clamps the value to be between 0 and 100. - * @param {number} value The value to clamp. - * @returns {number} - */ - function clamp(value) { - return Math.max(0, Math.min(value || 0, 100)); - } + } } -MdProgressLinearDirective.$inject = ["$mdTheming", "$mdUtil", "$log"]; + })(); @@ -13155,360 +22113,667 @@ MdProgressLinearDirective.$inject = ["$mdTheming", "$mdUtil", "$log"]; /** * @ngdoc module - * @name material.components.radioButton - * @description radioButton module! + * @name material.components.switch */ -angular.module('material.components.radioButton', [ - 'material.core' + +MdSwitch.$inject = ["mdCheckboxDirective", "$mdUtil", "$mdConstant", "$parse", "$$rAF", "$mdGesture", "$timeout"]; +angular.module('material.components.switch', [ + 'material.core', + 'material.components.checkbox' ]) - .directive('mdRadioGroup', mdRadioGroupDirective) - .directive('mdRadioButton', mdRadioButtonDirective); + .directive('mdSwitch', MdSwitch); /** * @ngdoc directive - * @module material.components.radioButton - * @name mdRadioGroup - * + * @module material.components.switch + * @name mdSwitch * @restrict E * - * @description - * The `` directive identifies a grouping - * container for the 1..n grouped radio buttons; specified using nested - * `` tags. + * The switch directive is used very much 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 radio button is in the accent color by default. The primary color palette may be used with + * the switch is in the accent color by default. The primary color palette may be used with * the `md-primary` class. * - * Note: `` and `` handle tabindex differently - * than the native `` controls. Whereas the native controls - * force the user to tab through all the radio buttons, `` - * is focusable, and by default the ``s are not. - * * @param {string} ng-model Assignable angular expression to data-bind to. - * @param {boolean=} md-no-ink Use of attribute indicates flag to disable ink ripple effects. + * @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 {expression=} ng-disabled En/Disable based on the expression. + * @param {boolean=} md-no-ink Use of attribute indicates use of ripple ink effects. + * @param {string=} aria-label Publish the button label used by screen-readers for accessibility. Defaults to the switch's text. + * @param {boolean=} md-invert When set to true, the switch will be inverted. * * @usage * - * - * - * + * + * Finished ? + * * - * {{ d.label }} + * + * No Ink Effects + * * - * + * + * Disabled + * * - * * - * */ -function mdRadioGroupDirective($mdUtil, $mdConstant, $mdTheming, $timeout) { - RadioGroupController.prototype = createRadioGroupControllerProto(); +function MdSwitch(mdCheckboxDirective, $mdUtil, $mdConstant, $parse, $$rAF, $mdGesture, $timeout) { + var checkboxDirective = mdCheckboxDirective[0]; return { restrict: 'E', - controller: ['$element', RadioGroupController], - require: ['mdRadioGroup', '?ngModel'], - link: { pre: linkRadioGroup } + priority: $mdConstant.BEFORE_NG_ARIA, + transclude: true, + template: + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    '+ + '
    ' + + '
    ', + require: ['^?mdInputContainer', '?ngModel', '?^form'], + compile: mdSwitchCompile }; - function linkRadioGroup(scope, element, attr, ctrls) { - $mdTheming(element); - var rgCtrl = ctrls[0]; - var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel(); + function mdSwitchCompile(element, attr) { + var checkboxLink = checkboxDirective.compile(element, attr).post; + // No transition on initial load. + element.addClass('md-dragging'); - rgCtrl.init(ngModelCtrl); + return function (scope, element, attr, ctrls) { + var containerCtrl = ctrls[0]; + var ngModel = ctrls[1] || $mdUtil.fakeNgModel(); + var formCtrl = ctrls[2]; - scope.mouseActive = false; - element.attr({ - 'role': 'radiogroup', - 'tabIndex': element.attr('tabindex') || '0' - }) - .on('keydown', keydownListener) - .on('mousedown', function(event) { - scope.mouseActive = true; - $timeout(function() { - scope.mouseActive = false; - }, 100); - }) - .on('focus', function() { - if(scope.mouseActive === false) { rgCtrl.$element.addClass('md-focused'); } - }) - .on('blur', function() { rgCtrl.$element.removeClass('md-focused'); }); + var disabledGetter = null; + if (attr.disabled != null) { + disabledGetter = function() { return true; }; + } else if (attr.ngDisabled) { + disabledGetter = $parse(attr.ngDisabled); + } - /** - * - */ - function setFocus() { - if (!element.hasClass('md-focused')) { element.addClass('md-focused'); } - } + var thumbContainer = angular.element(element[0].querySelector('.md-thumb-container')); + var switchContainer = angular.element(element[0].querySelector('.md-container')); + var labelContainer = angular.element(element[0].querySelector('.md-label')); - /** - * - */ - function keydownListener(ev) { - var keyCode = ev.which || ev.keyCode; + // no transition on initial load + $$rAF(function() { + element.removeClass('md-dragging'); + }); - // Only listen to events that we originated ourselves - // so that we don't trigger on things like arrow keys in - // inputs. + checkboxLink(scope, element, attr, ctrls); + + if (disabledGetter) { + scope.$watch(disabledGetter, function(isDisabled) { + element.attr('tabindex', isDisabled ? -1 : 0); + }); + } + + attr.$observe('mdInvert', function(newValue) { + var isInverted = $mdUtil.parseAttributeBoolean(newValue); + + isInverted ? element.prepend(labelContainer) : element.prepend(switchContainer); + + // Toggle a CSS class to update the margin. + element.toggleClass('md-inverted', isInverted); + }); + + // These events are triggered by setup drag + $mdGesture.register(switchContainer, 'drag'); + switchContainer + .on('$md.dragstart', onDragStart) + .on('$md.drag', onDrag) + .on('$md.dragend', onDragEnd); + + var drag; + function onDragStart(ev) { + // Don't go if the switch is disabled. + if (disabledGetter && disabledGetter(scope)) return; + ev.stopPropagation(); + + element.addClass('md-dragging'); + drag = {width: thumbContainer.prop('offsetWidth')}; + } + + function onDrag(ev) { + if (!drag) return; + ev.stopPropagation(); + ev.srcEvent && ev.srcEvent.preventDefault(); + + var percent = ev.pointer.distanceX / drag.width; + + //if checked, start from right. else, start from left + var translate = ngModel.$viewValue ? 1 + percent : percent; + // Make sure the switch stays inside its bounds, 0-1% + translate = Math.max(0, Math.min(1, translate)); + + thumbContainer.css($mdConstant.CSS.TRANSFORM, 'translate3d(' + (100*translate) + '%,0,0)'); + drag.translate = translate; + } + + function onDragEnd(ev) { + if (!drag) return; + ev.stopPropagation(); + + element.removeClass('md-dragging'); + thumbContainer.css($mdConstant.CSS.TRANSFORM, ''); + + // We changed if there is no distance (this is a click a click), + // or if the drag distance is >50% of the total. + var isChanged = ngModel.$viewValue ? drag.translate < 0.5 : drag.translate > 0.5; + if (isChanged) { + applyModelValue(!ngModel.$viewValue); + } + drag = null; + + // Wait for incoming mouse click + scope.skipToggle = true; + $timeout(function() { + scope.skipToggle = false; + }, 1); + } + + function applyModelValue(newValue) { + scope.$apply(function() { + ngModel.$setViewValue(newValue); + ngModel.$render(); + }); + } + + }; + } + + +} + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.toast + * @description + * Toast + */ +MdToastDirective.$inject = ["$mdToast"]; +MdToastProvider.$inject = ["$$interimElementProvider"]; +angular.module('material.components.toast', [ + 'material.core', + 'material.components.button' +]) + .directive('mdToast', MdToastDirective) + .provider('$mdToast', MdToastProvider); + +/* @ngInject */ +function MdToastDirective($mdToast) { + return { + restrict: 'E', + link: function postLink(scope, element) { + element.addClass('_md'); // private md component indicator for styling + + // When navigation force destroys an interimElement, then + // listen and $destroy() that interim instance... + scope.$on('$destroy', function() { + $mdToast.destroy(); + }); + } + }; +} + +/** + * @ngdoc service + * @name $mdToast + * @module material.components.toast + * + * @description + * `$mdToast` is a service to build a toast notification on any position + * on the screen with an optional duration, and provides a simple promise API. + * + * The toast will be always positioned at the `bottom`, when the screen size is + * between `600px` and `959px` (`sm` breakpoint) + * + * ## Restrictions on custom toasts + * - The toast's template must have an outer `` element. + * - For a toast action, use element with class `md-action`. + * - Add the class `md-capsule` for curved corners. + * + * ### Custom Presets + * Developers are also able to create their own preset, which can be easily used without repeating + * their options each time. + * + * + * $mdToastProvider.addPreset('testPreset', { + * options: function() { + * return { + * template: + * '' + + * '
    ' + + * 'This is a custom preset' + + * '
    ' + + * '
    ', + * controllerAs: 'toast', + * bindToController: true + * }; + * } + * }); + *
    + * + * After you created your preset at config phase, you can easily access it. + * + * + * $mdToast.show( + * $mdToast.testPreset() + * ); + * + * + * ## Parent container notes + * + * The toast is positioned using absolute positioning relative to its first non-static parent + * container. Thus, if the requested parent container uses static positioning, we will temporarily + * set its positioning to `relative` while the toast is visible and reset it when the toast is + * hidden. + * + * Because of this, it is usually best to ensure that the parent container has a fixed height and + * prevents scrolling by setting the `overflow: hidden;` style. Since the position is based off of + * the parent's height, the toast may be mispositioned if you allow the parent to scroll. + * + * You can, however, have a scrollable element inside of the container; just make sure the + * container itself does not scroll. + * + * + *
    + * + * I can have lots of content and scroll! + * + *
    + *
    + * + * @usage + * + *
    + * + * Open a Toast! + * + *
    + *
    + * + * + * var app = angular.module('app', ['ngMaterial']); + * app.controller('MyController', function($scope, $mdToast) { + * $scope.openToast = function($event) { + * $mdToast.show($mdToast.simple().textContent('Hello!')); + * // Could also do $mdToast.showSimple('Hello'); + * }; + * }); + * + */ - if (keyCode != $mdConstant.KEY_CODE.ENTER && - ev.currentTarget != ev.target) { - return; - } +/** + * @ngdoc method + * @name $mdToast#showSimple + * + * @param {string} message The message to display inside the toast + * @description + * Convenience method which builds and shows a simple toast. + * + * @returns {promise} A promise that can be resolved with `$mdToast.hide()` or + * rejected with `$mdToast.cancel()`. + * + */ - switch (keyCode) { - case $mdConstant.KEY_CODE.LEFT_ARROW: - case $mdConstant.KEY_CODE.UP_ARROW: - ev.preventDefault(); - rgCtrl.selectPrevious(); - setFocus(); - break; + /** + * @ngdoc method + * @name $mdToast#simple + * + * @description + * Builds a preconfigured toast. + * + * @returns {obj} a `$mdToastPreset` with the following chainable configuration methods. + * + * _**Note:** These configuration methods are provided in addition to the methods provided by + * the `build()` and `show()` methods below._ + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    MethodDescription
    `.textContent(string)`Sets the toast content to the specified string
    `.action(string)` + * Adds an action button.
    + * If clicked, the promise (returned from `show()`) + * will resolve with the value `'ok'`; otherwise, it is resolved with `true` after a `hideDelay` + * timeout + *
    `.highlightAction(boolean)` + * Whether or not the action button will have an additional highlight class.
    + * By default the `accent` color will be applied to the action button. + *
    `.highlightClass(string)` + * If set, the given class will be applied to the highlighted action button.
    + * This allows you to specify the highlight color easily. Highlight classes are `md-primary`, `md-warn` + * and `md-accent` + *
    `.capsule(boolean)`Whether or not to add the `md-capsule` class to the toast to provide rounded corners
    `.theme(string)`Sets the theme on the toast to the requested theme. Default is `$mdThemingProvider`'s default.
    `.toastClass(string)`Sets a class on the toast element
    + * + */ - case $mdConstant.KEY_CODE.RIGHT_ARROW: - case $mdConstant.KEY_CODE.DOWN_ARROW: - ev.preventDefault(); - rgCtrl.selectNext(); - setFocus(); - break; +/** + * @ngdoc method + * @name $mdToast#updateTextContent + * + * @description + * Updates the content of an existing toast. Useful for updating things like counts, etc. + * + */ - case $mdConstant.KEY_CODE.ENTER: - var form = angular.element($mdUtil.getClosest(element[0], 'form')); - if (form.length > 0) { - form.triggerHandler('submit'); - } - break; - } + /** + * @ngdoc method + * @name $mdToast#build + * + * @description + * Creates a custom `$mdToastPreset` that you can configure. + * + * @returns {obj} a `$mdToastPreset` with the chainable configuration methods for shows' options (see below). + */ - } - } + /** + * @ngdoc method + * @name $mdToast#show + * + * @description Shows the toast. + * + * @param {object} optionsOrPreset Either provide an `$mdToastPreset` returned from `simple()` + * and `build()`, or an options object with the following properties: + * + * - `templateUrl` - `{string=}`: The url of an html template file that will + * be used as the content of the toast. Restrictions: the template must + * have an outer `md-toast` element. + * - `template` - `{string=}`: Same as templateUrl, except this is an actual + * template string. + * - `autoWrap` - `{boolean=}`: Whether or not to automatically wrap the template content with a + * `
    ` if one is not provided. Defaults to true. Can be disabled if you provide a + * custom toast directive. + * - `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 toast is removed unless `preserveScope` is set to true. + * - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false + * - `hideDelay` - `{number=}`: How many milliseconds the toast should stay + * active before automatically closing. Set to 0 or false to have the toast stay open until + * closed manually. Default: 3000. + * - `position` - `{string=}`: Sets the position of the toast.
    + * Available: any combination of `'bottom'`, `'left'`, `'top'`, `'right'`, `'end'` and `'start'`. + * The properties `'end'` and `'start'` are dynamic and can be used for RTL support.
    + * Default combination: `'bottom left'`. + * - `toastClass` - `{string=}`: A class to set on the toast element. + * - `controller` - `{string=}`: The controller to associate with this toast. + * The controller will be injected the local `$mdToast.hide( )`, which is a function + * used to hide the toast. + * - `locals` - `{string=}`: An object containing key/value pairs. The keys will + * be used as names of values to inject into the controller. For example, + * `locals: {three: 3}` would inject `three` into the controller with the value + * of 3. + * - `bindToController` - `bool`: bind the locals to the controller, instead of passing them in. + * - `resolve` - `{object=}`: Similar to locals, except it takes promises as values + * and the toast will not open until the promises resolve. + * - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope. + * - `parent` - `{element=}`: The element to append the toast to. Defaults to appending + * to the root element of the application. + * + * @returns {promise} A promise that can be resolved with `$mdToast.hide()` or + * rejected with `$mdToast.cancel()`. `$mdToast.hide()` will resolve either with a Boolean + * value == 'true' or the value passed as an argument to `$mdToast.hide()`. + * And `$mdToast.cancel()` will resolve the promise with a Boolean value == 'false' + */ - function RadioGroupController($element) { - this._radioButtonRenderFns = []; - this.$element = $element; - } +/** + * @ngdoc method + * @name $mdToast#hide + * + * @description + * Hide an existing toast and resolve the promise returned from `$mdToast.show()`. + * + * @param {*=} response An argument for the resolved promise. + * + * @returns {promise} a promise that is called when the existing element is removed from the DOM. + * The promise is resolved with either a Boolean value == 'true' or the value passed as the + * argument to `.hide()`. + * + */ - function createRadioGroupControllerProto() { - return { - init: function(ngModelCtrl) { - this._ngModelCtrl = ngModelCtrl; - this._ngModelCtrl.$render = angular.bind(this, this.render); - }, - add: function(rbRender) { - this._radioButtonRenderFns.push(rbRender); - }, - remove: function(rbRender) { - var index = this._radioButtonRenderFns.indexOf(rbRender); - if (index !== -1) { - this._radioButtonRenderFns.splice(index, 1); - } - }, - render: function() { - this._radioButtonRenderFns.forEach(function(rbRender) { - rbRender(); - }); - }, - setViewValue: function(value, eventType) { - this._ngModelCtrl.$setViewValue(value, eventType); - // update the other radio buttons as well - this.render(); - }, - getViewValue: function() { - return this._ngModelCtrl.$viewValue; - }, - selectNext: function() { - return changeSelectedButton(this.$element, 1); - }, - selectPrevious: function() { - return changeSelectedButton(this.$element, -1); - }, - setActiveDescendant: function (radioId) { - this.$element.attr('aria-activedescendant', radioId); - } - }; - } - /** - * Change the radio group's selected button by a given increment. - * If no button is selected, select the first button. - */ - function changeSelectedButton(parent, increment) { - // Coerce all child radio buttons into an array, then wrap then in an iterator - var buttons = $mdUtil.iterator(parent[0].querySelectorAll('md-radio-button'), true); +/** + * @ngdoc method + * @name $mdToast#cancel + * + * @description + * `DEPRECATED` - The promise returned from opening a toast is used only to notify about the closing of the toast. + * As such, there isn't any reason to also allow that promise to be rejected, + * since it's not clear what the difference between resolve and reject would be. + * + * Hide the existing toast and reject the promise returned from + * `$mdToast.show()`. + * + * @param {*=} response An argument for the rejected promise. + * + * @returns {promise} a promise that is called when the existing element is removed from the DOM + * The promise is resolved with a Boolean value == 'false'. + * + */ - if (buttons.count()) { - var validate = function (button) { - // If disabled, then NOT valid - return !angular.element(button).attr("disabled"); - }; +function MdToastProvider($$interimElementProvider) { + // Differentiate promise resolves: hide timeout (value == true) and hide action clicks (value == ok). + toastDefaultOptions.$inject = ["$animate", "$mdToast", "$mdUtil", "$mdMedia"]; + var ACTION_RESOLVE = 'ok'; - var selected = parent[0].querySelector('md-radio-button.md-checked'); - var target = buttons[increment < 0 ? 'previous' : 'next'](selected, validate) || buttons.first(); + var activeToastContent; + var $mdToast = $$interimElementProvider('$mdToast') + .setDefaults({ + methods: ['position', 'hideDelay', 'capsule', 'parent', 'position', 'toastClass'], + options: toastDefaultOptions + }) + .addPreset('simple', { + argOption: 'textContent', + methods: ['textContent', 'content', 'action', 'highlightAction', 'highlightClass', 'theme', 'parent' ], + options: /* @ngInject */ ["$mdToast", "$mdTheming", function($mdToast, $mdTheming) { + return { + template: + '' + + '
    ' + + ' ' + + ' {{ toast.content }}' + + ' ' + + ' ' + + ' {{ toast.action }}' + + ' ' + + '
    ' + + '
    ', + controller: /* @ngInject */ ["$scope", function mdToastCtrl($scope) { + var self = this; - // Activate radioButton's click listener (triggerHandler won't create a real click event) - angular.element(target).triggerHandler('click'); + if (self.highlightAction) { + $scope.highlightClasses = [ + 'md-highlight', + self.highlightClass + ] + } + + $scope.$watch(function() { return activeToastContent; }, function() { + self.content = activeToastContent; + }); + this.resolve = function() { + $mdToast.hide( ACTION_RESOLVE ); + }; + }], + theme: $mdTheming.defaultTheme(), + controllerAs: 'toast', + bindToController: true + }; + }] + }) + .addMethod('updateTextContent', updateTextContent) + .addMethod('updateContent', updateTextContent); + function updateTextContent(newContent) { + activeToastContent = newContent; } - } -} -mdRadioGroupDirective.$inject = ["$mdUtil", "$mdConstant", "$mdTheming", "$timeout"]; + return $mdToast; -/** - * @ngdoc directive - * @module material.components.radioButton - * @name mdRadioButton - * - * @restrict E - * - * @description - * The ``directive is the child directive required to be used within `` elements. - * - * While similar to the `` directive, - * the `` directive provides ink effects, ARIA support, and - * supports use within named radio groups. - * - * @param {string} ngModel Assignable angular expression to data-bind to. - * @param {string=} ngChange Angular expression to be executed when input changes due to user - * interaction with the input element. - * @param {string} ngValue Angular expression which sets the value to which the expression should - * be set when selected. - * @param {string} value The value to which the expression should be set when selected. - * @param {string=} name Property name of the form under which the control is published. - * @param {string=} aria-label Adds label to radio button for accessibility. - * Defaults to radio button's text. If no text content is available, a warning will be logged. - * - * @usage - * - * - * - * Label 1 - * - * - * - * Green - * - * - * - * - */ -function mdRadioButtonDirective($mdAria, $mdUtil, $mdTheming) { + /* @ngInject */ + function toastDefaultOptions($animate, $mdToast, $mdUtil, $mdMedia) { + var SWIPE_EVENTS = '$md.swipeleft $md.swiperight $md.swipeup $md.swipedown'; + return { + onShow: onShow, + onRemove: onRemove, + toastClass: '', + position: 'bottom left', + themable: true, + hideDelay: 3000, + autoWrap: true, + transformTemplate: function(template, options) { + var shouldAddWrapper = options.autoWrap && template && !/md-toast-content/g.test(template); - var CHECKED_CSS = 'md-checked'; + if (shouldAddWrapper) { + // Root element of template will be . We need to wrap all of its content inside of + // of
    . All templates provided here should be static, developer-controlled + // content (meaning we're not attempting to guard against XSS). + var templateRoot = document.createElement('md-template'); + templateRoot.innerHTML = template; - return { - restrict: 'E', - require: '^mdRadioGroup', - transclude: true, - template: '
    ' + - '
    ' + - '
    ' + - '
    ' + - '
    ', - link: link - }; + // Iterate through all root children, to detect possible md-toast directives. + for (var i = 0; i < templateRoot.children.length; i++) { + if (templateRoot.children[i].nodeName === 'MD-TOAST') { + var wrapper = angular.element('
    '); - function link(scope, element, attr, rgCtrl) { - var lastChecked; + // Wrap the children of the `md-toast` directive in jqLite, to be able to append multiple + // nodes with the same execution. + wrapper.append(angular.element(templateRoot.children[i].childNodes)); - $mdTheming(element); - configureAria(element, scope); + // Append the new wrapped element to the `md-toast` directive. + templateRoot.children[i].appendChild(wrapper[0]); + } + } - initialize(); + // We have to return the innerHTMl, because we do not want to have the `md-template` element to be + // the root element of our interimElement. + return templateRoot.innerHTML; + } - /** - * - */ - function initialize(controller) { - if ( !rgCtrl ) { - throw 'RadioGroupController not found.'; + return template || ''; } + }; - rgCtrl.add(render); - attr.$observe('value', render); - - element - .on('click', listener) - .on('$destroy', function() { - rgCtrl.remove(render); - }); - } + function onShow(scope, element, options) { + activeToastContent = options.textContent || options.content; // support deprecated #content method - /** - * - */ - function listener(ev) { - if (element[0].hasAttribute('disabled')) return; + var isSmScreen = !$mdMedia('gt-sm'); - scope.$apply(function() { - rgCtrl.setViewValue(attr.value, ev && ev.type); - }); - } + element = $mdUtil.extractElementByName(element, 'md-toast', true); + options.element = element; - /** - * Add or remove the `.md-checked` class from the RadioButton (and conditionally its parent). - * Update the `aria-activedescendant` attribute. - */ - function render() { - var checked = (rgCtrl.getViewValue() == attr.value); - if (checked === lastChecked) { - return; - } + options.onSwipe = function(ev, gesture) { + //Add the relevant swipe class to the element so it can animate correctly + var swipe = ev.type.replace('$md.',''); + var direction = swipe.replace('swipe', ''); - lastChecked = checked; - element.attr('aria-checked', checked); + // If the swipe direction is down/up but the toast came from top/bottom don't fade away + // Unless the screen is small, then the toast always on bottom + if ((direction === 'down' && options.position.indexOf('top') != -1 && !isSmScreen) || + (direction === 'up' && (options.position.indexOf('bottom') != -1 || isSmScreen))) { + return; + } - if (checked) { - markParentAsChecked(true); - element.addClass(CHECKED_CSS); + if ((direction === 'left' || direction === 'right') && isSmScreen) { + return; + } - rgCtrl.setActiveDescendant(element.attr('id')); + element.addClass('md-' + swipe); + $mdUtil.nextTick($mdToast.cancel); + }; + options.openClass = toastOpenClass(options.position); - } else { - markParentAsChecked(false); - element.removeClass(CHECKED_CSS); - } + element.addClass(options.toastClass); - /** - * If the radioButton is inside a div, then add class so highlighting will work... - */ - function markParentAsChecked(addClass ) { - if ( element.parent()[0].nodeName != "MD-RADIO-GROUP") { - element.parent()[ !!addClass ? 'addClass' : 'removeClass'](CHECKED_CSS); - } + // 'top left' -> 'md-top md-left' + options.parent.addClass(options.openClass); + // static is the default position + if ($mdUtil.hasComputedStyle(options.parent, 'position', 'static')) { + options.parent.css('position', 'relative'); } - } - /** - * Inject ARIA-specific attributes appropriate for each radio button - */ - function configureAria( element, scope ){ - scope.ariaId = buildAriaID(); + element.on(SWIPE_EVENTS, options.onSwipe); + element.addClass(isSmScreen ? 'md-bottom' : options.position.split(' ').map(function(pos) { + return 'md-' + pos; + }).join(' ')); - element.attr({ - 'id' : scope.ariaId, - 'role' : 'radio', - 'aria-checked' : 'false' + if (options.parent) options.parent.addClass('md-toast-animating'); + return $animate.enter(element, options.parent).then(function() { + if (options.parent) options.parent.removeClass('md-toast-animating'); }); + } - $mdAria.expectWithText(element, 'aria-label'); + function onRemove(scope, element, options) { + element.off(SWIPE_EVENTS, options.onSwipe); + if (options.parent) options.parent.addClass('md-toast-animating'); + if (options.openClass) options.parent.removeClass(options.openClass); - /** - * Build a unique ID for each radio button that will be used with aria-activedescendant. - * Preserve existing ID if already specified. - * @returns {*|string} - */ - function buildAriaID() { - return attr.id || ( 'radio' + "_" + $mdUtil.nextUid() ); + return ((options.$destroy == true) ? element.remove() : $animate.leave(element)) + .then(function () { + if (options.parent) options.parent.removeClass('md-toast-animating'); + if ($mdUtil.hasComputedStyle(options.parent, 'position', 'static')) { + options.parent.css('position', ''); + } + }); + } + + function toastOpenClass(position) { + // For mobile, always open full-width on bottom + if (!$mdMedia('gt-xs')) { + return 'md-toast-open-bottom'; } + + return 'md-toast-open-' + + (position.indexOf('top') > -1 ? 'top' : 'bottom'); } } + } -mdRadioButtonDirective.$inject = ["$mdAria", "$mdUtil", "$mdTheming"]; })(); (function(){ @@ -13516,7699 +22781,8494 @@ mdRadioButtonDirective.$inject = ["$mdAria", "$mdUtil", "$mdTheming"]; /** * @ngdoc module - * @name material.components.select + * @name material.components.tabs + * @description + * + * Tabs, created with the `` directive provide *tabbed* navigation with different styles. + * The Tabs component consists of clickable tabs that are aligned horizontally side-by-side. + * + * Features include support for: + * + * - static or dynamic tabs, + * - responsive designs, + * - accessibility support (ARIA), + * - tab pagination, + * - external or internal tab content, + * - focus indicators and arrow-key navigations, + * - programmatic lookup and access to tab controllers, and + * - dynamic transitions through different tab contents. + * + */ +/* + * @see js folder for tabs implementation + */ +angular.module('material.components.tabs', [ + 'material.core', + 'material.components.icon' +]); + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.toolbar */ - -/*************************************************** - - ### TODO - POST RC1 ### - - [ ] Abstract placement logic in $mdSelect service to $mdMenu service - - ***************************************************/ - -var SELECT_EDGE_MARGIN = 8; -var selectNextId = 0; - -angular.module('material.components.select', [ - 'material.core', - 'material.components.backdrop' - ]) - .directive('mdSelect', SelectDirective) - .directive('mdSelectMenu', SelectMenuDirective) - .directive('mdOption', OptionDirective) - .directive('mdOptgroup', OptgroupDirective) - .provider('$mdSelect', SelectProvider); +mdToolbarDirective.$inject = ["$$rAF", "$mdConstant", "$mdUtil", "$mdTheming", "$animate"]; +angular.module('material.components.toolbar', [ + 'material.core', + 'material.components.content' +]) + .directive('mdToolbar', mdToolbarDirective); /** * @ngdoc directive - * @name mdSelect + * @name mdToolbar + * @module material.components.toolbar * @restrict E - * @module material.components.select + * @description + * `md-toolbar` is used to place a toolbar in your app. * - * @description Displays a select box, bound to an ng-model. + * Toolbars are usually used above a content area to display the title of the + * current page, and show relevant action buttons for that page. * - * @param {expression} ng-model The model! - * @param {boolean=} multiple Whether it's multiple. - * @param {expression=} md-on-close Expression to be evaluated when the select is closed. - * @param {expression=} md-on-open Expression to be evaluated when opening the select. - * Will hide the select options and show a spinner until the evaluated promise resolves. - * @param {string=} placeholder Placeholder hint text. - * @param {string=} aria-label Optional label for accessibility. Only necessary if no placeholder or - * explicit label is present. - * @param {string=} md-container-class Class list to get applied to the `._md-select-menu-container` - * element (for custom styling). + * You can change the height of the toolbar by adding either the + * `md-medium-tall` or `md-tall` class to the toolbar. * * @usage - * With a placeholder (label and aria-label are added dynamically) * - * - * - * {{ opt }} - * - * - * + *
    + * * - * With an explicit label - * - * - * - * - * {{ opt }} - * - * - * + *
    + *

    My App's Title

    * - * ## Selects and object equality - * When using a `md-select` to pick from a list of objects, it is important to realize how javascript handles - * equality. Consider the following example: - * - * angular.controller('MyCtrl', function($scope) { - * $scope.users = [ - * { id: 1, name: 'Bob' }, - * { id: 2, name: 'Alice' }, - * { id: 3, name: 'Steve' } - * ]; - * $scope.selectedUser = { id: 1, name: 'Bob' }; - * }); - * - * - *
    - * - * {{ user.name }} - * + * + * Right Bar Button + * + *
    + * + * + * + * Hello! + * *
    * * - * At first one might expect that the select should be populated with "Bob" as the selected user. However, - * this is not true. To determine whether something is selected, - * `ngModelController` is looking at whether `$scope.selectedUser == (any user in $scope.users);`; - * - * Javascript's `==` operator does not check for deep equality (ie. that all properties - * on the object are the same), but instead whether the objects are *the same object in memory*. - * In this case, we have two instances of identical objects, but they exist in memory as unique - * entities. Because of this, the select will have no value populated for a selected user. + * Note: The code above shows usage with the `md-truncate` component which provides an + * ellipsis if the title is longer than the width of the Toolbar. * - * To get around this, `ngModelController` provides a `track by` option that allows us to specify a different - * expression which will be used for the equality operator. As such, we can update our `html` to - * make use of this by specifying the `ng-model-options="{trackBy: '$value.id'}"` on the `md-select` - * element. This converts our equality expression to be - * `$scope.selectedUser.id == (any id in $scope.users.map(function(u) { return u.id; }));` - * which results in Bob being selected as desired. + * ## CSS & Styles * - * Working HTML: - * - *
    - * - * {{ user.name }} - * + * The `` provides a few custom CSS classes that you may use to enhance the + * functionality of your toolbar. + * + *
    + * + * + * + * The `md-toolbar-tools` class provides quite a bit of automatic styling for your toolbar + * buttons and text. When applied, it will center the buttons and text vertically for you. + * + * + * *
    - * + * + * ### Private Classes + * + * Currently, the only private class is the `md-toolbar-transitions` class. All other classes are + * considered public. + * + * @param {boolean=} md-scroll-shrink Whether the header should shrink away as + * the user scrolls down, and reveal itself as the user scrolls up. + * + * _**Note (1):** for scrollShrink to work, the toolbar must be a sibling of a + * `md-content` element, placed before it. See the scroll shrink demo._ + * + * _**Note (2):** The `md-scroll-shrink` attribute is only parsed on component + * initialization, it does not watch for scope changes._ + * + * + * @param {number=} md-shrink-speed-factor How much to change the speed of the toolbar's + * shrinking by. For example, if 0.25 is given then the toolbar will shrink + * at one fourth the rate at which the user scrolls down. Default 0.5. + * */ -function SelectDirective($mdSelect, $mdUtil, $mdTheming, $mdAria, $compile, $parse) { + +function mdToolbarDirective($$rAF, $mdConstant, $mdUtil, $mdTheming, $animate) { + var translateY = angular.bind(null, $mdUtil.supplant, 'translate3d(0,{0}px,0)'); + return { + template: '', restrict: 'E', - require: ['^?mdInputContainer', 'mdSelect', 'ngModel', '?^form'], - compile: compile, - controller: function() { - } // empty placeholder controller to be initialized in link - }; - - function compile(element, attr) { - // add the select value that will hold our placeholder or selected option value - var valueEl = angular.element(''); - valueEl.append(''); - valueEl.addClass('_md-select-value'); - if (!valueEl[0].hasAttribute('id')) { - valueEl.attr('id', 'select_value_label_' + $mdUtil.nextUid()); - } - // There's got to be an md-content inside. If there's not one, let's add it. - if (!element.find('md-content').length) { - element.append(angular.element('').append(element.contents())); - } + link: function(scope, element, attr) { + element.addClass('_md'); // private md component indicator for styling + $mdTheming(element); - // Add progress spinner for md-options-loading - if (attr.mdOnOpen) { + $mdUtil.nextTick(function () { + element.addClass('_md-toolbar-transitions'); // adding toolbar transitions after digest + }, false); - // Show progress indicator while loading async - // Use ng-hide for `display:none` so the indicator does not interfere with the options list - element - .find('md-content') - .prepend(angular.element( - '
    ' + - ' ' + - '
    ' - )); + if (angular.isDefined(attr.mdScrollShrink)) { + setupScrollShrink(); + } - // Hide list [of item options] while loading async - element - .find('md-option') - .attr('ng-show', '$$loadingAsyncDone'); - } + function setupScrollShrink() { - if (attr.name) { - var autofillClone = angular.element(',