From ec1221bceca4b99a261a5ac9b19bae7c3ecebb96 Mon Sep 17 00:00:00 2001 From: eef Date: Mon, 27 Jun 2016 12:15:57 -0500 Subject: [PATCH 1/5] Updated to 1.1.0-rc5 --- lib/rails-angular-material/version.rb | 2 +- vendor/assets/javascripts/angular-material.js | 24417 ++++++++++------ .../javascripts/angular-material.min.js | 22 +- .../assets/stylesheets/angular-material.css | 12395 ++------ .../stylesheets/angular-material.min.css | 4 +- 5 files changed, 16765 insertions(+), 20075 deletions(-) diff --git a/lib/rails-angular-material/version.rb b/lib/rails-angular-material/version.rb index ca92d30..e22eced 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.0-rc5" end diff --git a/vendor/assets/javascripts/angular-material.js b/vendor/assets/javascripts/angular-material.js index 641025a..dd8de52 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.0-rc.5 */ (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.layout","material.core.theming.palette","material.core.theming","material.core.animate","material.components.autocomplete","material.components.backdrop","material.components.button","material.components.bottomSheet","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.fabTrigger","material.components.gridList","material.components.icon","material.components.list","material.components.input","material.components.menuBar","material.components.menu","material.components.navBar","material.components.panel","material.components.progressCircular","material.components.progressLinear","material.components.radioButton","material.components.sidenav","material.components.select","material.components.slider","material.components.showHide","material.components.swipe","material.components.sticky","material.components.switch","material.components.subheader","material.components.tabs","material.components.toast","material.components.toolbar","material.components.tooltip","material.components.virtualRepeat","material.components.whiteframe"]); })(); (function(){ "use strict"; @@ -34,6 +34,7 @@ angular /** * Detect if the ng-Touch module is also being used. * Warn if detected. + * @ngInject */ function DetectNgTouch($log, $injector) { if ( $injector.has('$swipe') ) { @@ -46,7 +47,9 @@ function DetectNgTouch($log, $injector) { } DetectNgTouch.$inject = ["$log", "$injector"]; - +/** + * @ngInject + */ function MdCoreConfigure($provide, $mdThemingProvider) { $provide.decorator('$$rAF', ["$delegate", rAFDecorator]); @@ -59,6 +62,9 @@ function MdCoreConfigure($provide, $mdThemingProvider) { } MdCoreConfigure.$inject = ["$provide", "$mdThemingProvider"]; +/** + * @ngInject + */ function rAFDecorator($delegate) { /** * Use this to throttle events that come in often. @@ -89,6 +95,7 @@ function rAFDecorator($delegate) { }; return $delegate; } +rAFDecorator.$inject = ["$delegate"]; })(); (function(){ @@ -109,7 +116,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 @@ -207,6 +214,83 @@ function postLink(scope, element, attrs) { (function(){ "use strict"; +/** + * @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 + } +} +})(); +(function(){ +"use strict"; + angular.module('material.core') .factory('$mdConstant', MdConstantFactory); @@ -216,9 +300,30 @@ angular.module('material.core') */ function MdConstantFactory($sniffer) { - var webkit = /webkit/i.test($sniffer.vendorPrefix); + var vendorPrefix = $sniffer.vendorPrefix; + var isWebkit = /webkit/i.test(vendorPrefix); + var SPECIAL_CHARS_REGEXP = /([:\-_]+(.))/g; + var prefixTestEl = document.createElement('div'); + 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(name) ? name : // The current browser supports the un-prefixed property + hasStyleProperty(ucPrefix) ? ucPrefix : // The current browser only supports the prefixed property. + hasStyleProperty(lcPrefix) ? lcPrefix : name; // Some browsers are only supporting the prefix in lowercase. + } + + function hasStyleProperty(property) { + return angular.isDefined(prefixTestEl.style[property]); + } + + function camelCase(input) { + return input.replace(SPECIAL_CHARS_REGEXP, function(matches, separator, letter, offset) { + return offset ? letter.toUpperCase() : letter; + }); } return { @@ -242,8 +347,8 @@ function MdConstantFactory($sniffer) { }, 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'), @@ -265,15 +370,17 @@ 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: [ @@ -286,6 +393,8 @@ function MdConstantFactory($sniffer) { 'sm', 'gt-xs', 'xs', + 'landscape', + 'portrait', 'print' ] }; @@ -588,6 +697,14 @@ angular.module('material.core') * (min-width: 1920px) * * + * landscape + * landscape + * + * + * portrait + * portrait + * + * * print * print * @@ -616,6 +733,7 @@ angular.module('material.core') * */ +/* @ngInject */ function mdMediaFactory($mdConstant, $rootScope, $window) { var queries = {}; var mqls = {}; @@ -719,6 +837,73 @@ 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 + }; + + 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 = element[0] || element; + + var prefixedAttrs = _buildList(attribute); + + for (var i = 0; i < prefixedAttrs.length; i++) { + if (element.hasAttribute(prefixedAttrs[i])) { + return true; + } + } + + return false; + } +} +})(); +(function(){ +"use strict"; + /* * This var has to be outside the angular factory, otherwise when * there are multiple material apps on the same page, each app @@ -737,6 +922,9 @@ angular .module('material.core') .factory('$mdUtil', UtilFactory); +/** + * @ngInject + */ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $interpolate, $log, $rootElement, $window) { // Setup some core variables for the processTemplate method var startSymbol = $interpolate.startSymbol(), @@ -852,14 +1040,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 @@ -891,12 +1079,16 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in } }, - // Disables scroll around the passed element. + /** + * 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. + */ 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); @@ -912,36 +1104,22 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in // Creates a virtual scrolling mask to absorb touchmove, keyboard, scrollbar clicking, and wheel events function disableElementScroll(element) { element = angular.element(element || body)[0]; - var zIndex = 50; var scrollMask = angular.element( '
' + '
' + - '
').css('z-index', zIndex); + ''); element.appendChild(scrollMask[0]); scrollMask.on('wheel', preventDefault); scrollMask.on('touchmove', preventDefault); - $document.on('keydown', disableKeyNav); return function restoreScroll() { scrollMask.off('wheel'); scrollMask.off('touchmove'); scrollMask[0].parentNode.removeChild(scrollMask[0]); - $document.off('keydown', disableKeyNav); delete $mdUtil.disableScrollAround._enableScrolling; }; - // Prevent keypresses from elements inside the body - // used to stop the keypresses that could cause the page to scroll - // (arrow keys, spacebar, tab, etc). - function disableKeyNav(e) { - //-- temporarily removed this logic, will possibly re-add at a later date - //if (!element[0].contains(e.target)) { - // e.preventDefault(); - // e.stopImmediatePropagation(); - //} - } - function preventDefault(e) { e.preventDefault(); } @@ -963,9 +1141,7 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in top: -scrollOffset + 'px' }); - applyStyles(htmlNode, { - overflowY: 'scroll' - }); + htmlNode.style.overflowY = 'scroll'; } if (body.clientWidth < clientWidth) applyStyles(body, {overflow: 'hidden'}); @@ -1318,7 +1494,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 +1513,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(); @@ -1408,6 +1586,7 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in hasComputedStyle: hasComputedStyle }; + // Instantiate other namespace utility methods $mdUtil.dom.animator = $$mdAnimate($mdUtil); @@ -1496,7 +1675,7 @@ 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 ) { expectAsync(element, attrName, function() { @@ -1508,7 +1687,26 @@ function AriaService($$rAF, $log, $window, $interpolate) { } 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,17 +1718,18 @@ function AriaService($$rAF, $log, $window, $interpolate) { return (style.display === 'none'); } - if(hasChildren) { + if (hasChildren) { var children = node.childNodes; - for(var i=0; i + * + * ### 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 +4222,7 @@ function InkRippleCtrl ($scope, $element, rippleOptions, $window, $timeout, $mdU this.bindEvents(); } -InkRippleCtrl.$inject = ["$scope", "$element", "rippleOptions", "$window", "$timeout", "$mdUtil"]; +InkRippleCtrl.$inject = ["$scope", "$element", "rippleOptions", "$window", "$timeout", "$mdUtil", "$mdColorUtil"]; /** @@ -4046,39 +4284,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 +4413,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 +4428,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 +4454,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) @@ -4670,7 +4876,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 +4890,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 +4913,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,6 +4922,12 @@ angular.module('material.core.theming.palette', []) (function(){ "use strict"; +/** + * @ngdoc module + * @name material.core.theming + * @description + * Theming + */ angular.module('material.core.theming', ['material.core.theming.palette']) .directive('mdTheme', ThemingDirective) .directive('mdThemable', ThemableDirective) @@ -4780,20 +4991,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 +5040,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) { @@ -4853,10 +5065,11 @@ var generateOnDemand = false; // Nonce to be added as an attribute to the generated themes style tags. var nonce = null; +var disableTheming = false; function ThemingProvider($mdColorPalette) { PALETTES = { }; - THEMES = { }; + var THEMES = { }; var themingProvider; var defaultTheme = 'default'; @@ -4873,6 +5086,15 @@ function ThemingProvider($mdColorPalette) { extendPalette: extendPalette, theme: registerTheme, + /** + * 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() { + disableTheming = true; + }, + setNonce: function(nonceValue) { nonce = nonceValue; }, @@ -5054,27 +5276,62 @@ function ThemingProvider($mdColorPalette) { */ /* @ngInject */ function ThemingService($rootScope, $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); + }; - applyTheme.inherit = function(el, parent) { - var ctrl = parent.controller('mdTheme'); + applyTheme.THEMES = angular.extend({}, THEMES); + applyTheme.PALETTES = angular.extend({}, PALETTES); + applyTheme.inherit = inheritTheme; + applyTheme.registered = registered; + applyTheme.defaultTheme = function() { return defaultTheme; }; + applyTheme.generateTheme = function(name) { generateTheme(THEMES[name], name, nonce); }; + + 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'); 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); + var watchTheme = (alwaysWatchTheme || angular.isDefined(attrThemeValue)) && attrThemeValue != 'false'; + + updateThemeClass(lookupThemeName()); + + el.on('$destroy', watchTheme ? $rootScope.$watch(lookupThemeName, updateThemeClass) : angular.noop ); + + /** + * 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. + ctrl = parent.controller('mdTheme') || el.data('$mdThemeController'); + return ctrl && ctrl.$mdTheme || (defaultTheme == 'default' ? '' : defaultTheme); } - function changeTheme(theme) { + /** + * 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,31 +5340,8 @@ 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"]; @@ -5117,12 +5351,32 @@ function ThemingDirective($mdTheming, $interpolate, $log) { priority: 100, link: { pre: function(scope, el, attrs) { + var registeredCallbacks = []; 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; + + registeredCallbacks.forEach(function (cb) { + cb(); + }) } }; el.data('$mdThemeController', ctrl); @@ -5149,7 +5403,7 @@ function parseRules(theme, colorType, rules) { 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 +5417,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 ); }); @@ -5196,10 +5454,10 @@ 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 = !disableTheming && $injector.has('$MD_THEME_CSS') ? $injector.get('$MD_THEME_CSS') : ''; if ( !firstChild ) return; if (themeCss.length === 0) return; // no rules, so no point in running this expensive task @@ -5250,9 +5508,9 @@ function generateAllThemes($injector) { // call generateTheme to do this on a theme-by-theme basis. if (generateOnDemand) return; - angular.forEach(THEMES, function(theme) { - if (!GENERATED[theme.name]) { - generateTheme(theme.name, nonce); + angular.forEach($mdTheming.THEMES, function(theme) { + if (!GENERATED[theme.name] && !($mdTheming.defaultTheme() !== 'default' && theme.name === 'default')) { + generateTheme(theme, theme.name, nonce); } }); @@ -5263,7 +5521,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 || []; @@ -5316,10 +5574,9 @@ function generateAllThemes($injector) { }); } } -generateAllThemes.$inject = ["$injector"]; +generateAllThemes.$inject = ["$injector", "$mdTheming"]; -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 +5599,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; } @@ -5430,7 +5681,8 @@ function AnimateDomUtils($mdUtil, $q, $timeout, $mdConstant, $animateCss) { return $animateCss(target,{ from:from, to:to, - addClass:options.transitionInClass + addClass:options.transitionInClass, + removeClass:options.transitionOutClass }) .start() .then(function(){ @@ -5449,73 +5701,78 @@ function AnimateDomUtils($mdUtil, $q, $timeout, $mdConstant, $animateCss) { }).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 || { }; - - var timer = $timeout(finished, opts.timeout || TIMEOUT); - element.on($mdConstant.CSS.TRANSITIONEND, finished); + return $q(function(resolve, reject){ + opts = opts || { }; - /** - * Upon timeout or transitionEnd, reject or resolve (respectively) this promise. - * NOTE: Make sure this transitionEnd didn't bubble up from a child - */ - function finished(ev) { - if ( ev && ev.target !== element[0]) return; + // If there is no transition is found, resolve immediately + // + // NOTE: using $mdUtil.nextTick() causes delays/issues + if (noTransitionFound(opts.cachedTransitionStyles)) { + TIMEOUT = 0; + } - if ( ev ) $timeout.cancel(timer); - element.off($mdConstant.CSS.TRANSITIONEND, finished); + var timer = $timeout(finished, opts.timeout || TIMEOUT); + element.on($mdConstant.CSS.TRANSITIONEND, finished); - // Never reject since ngAnimate may cause timeouts due missed transitionEnd events - resolve(); + /** + * Upon timeout or transitionEnd, reject or resolve (respectively) this promise. + * NOTE: Make sure this transitionEnd didn't bubble up from a child + */ + function finished(ev) { + if ( ev && ev.target !== element[0]) return; - } + if ( ev ) $timeout.cancel(timer); + element.off($mdConstant.CSS.TRANSITIONEND, finished); - }); - }, + // Never reject since ngAnimate may cause timeouts due missed transitionEnd events + resolve(); - /** - * Calculate the zoom transform from dialog to origin. - * - * We use this to set the dialog position immediately; - * then the md-transition-in actually translates back to - * `translate3d(0,0,0) scale(1.0)`... - * - * NOTE: all values are rounded to the nearest integer - */ - calculateZoomToOrigin: function (element, originator) { + } + + /** + * 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 +5786,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... */ @@ -5773,36 +6057,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']); + 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; + + 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; @@ -6076,7 +6368,7 @@ 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) { + .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 { @@ -6085,21 +6377,20 @@ 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 () { + // 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. + 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' + }); + } // Often $animate.enter() is used to append the backDrop element // so let's wait until $animate is done... @@ -6123,6 +6414,13 @@ angular } }); + function resize() { + var hViewport = parseInt(body.height, 10) + Math.abs(parseInt(body.top, 10)); + element.css({ + height: hViewport + 'px' + }); + } + } }]); @@ -6131,6 +6429,174 @@ angular (function(){ "use strict"; +/** + * @ngdoc module + * @name material.components.button + * @description + * + * Button + */ +angular + .module('material.components.button', [ 'material.core' ]) + .directive('mdButton', MdButtonDirective) + .directive('a', MdAnchorDirective); + + +/** + * @private + * @restrict E + * + * @description + * `a` is an anchor directive used to inherit theme colors for md-primary, md-accent, etc. + * + * @usage + * + * + * + * + * + * + */ +function MdAnchorDirective($mdTheming) { + return { + restrict : 'E', + link : function postLink(scope, element) { + // Make sure to inherit theme so stand-alone anchors + // support theme colors for md-primary, md-accent, etc. + $mdTheming(element); + } + }; +} +MdAnchorDirective.$inject = ["$mdTheming"]; + + +/** + * @ngdoc directive + * @name mdButton + * @module material.components.button + * + * @restrict E + * + * @description + * `` is a button directive with optional ink ripples (default enabled). + * + * If you supply a `href` or `ng-href` attribute, it will become an `` element. Otherwise, it will + * become a `'; + } + } + + function postLink(scope, element, attr) { + $mdTheming(element); + $mdButtonInkRipple.attach(scope, element); + + // Use async expect to support possible bindings in the button label + $mdAria.expectWithText(element, 'aria-label'); + + // For anchor elements, we have to set tabindex manually when the + // element is disabled + if (isAnchor(attr) && angular.isDefined(attr.ngDisabled) ) { + scope.$watch(attr.ngDisabled, function(isDisabled) { + element.attr('tabindex', isDisabled ? -1 : 0); + }); + } + + // disabling click event when disabled is true + element.on('click', function(e){ + if (attr.disabled === true) { + e.preventDefault(); + e.stopImmediatePropagation(); + } + }); + + if (!angular.isDefined(attr.mdNoFocusStyle)) { + // restrict focus styles to the keyboard + scope.mouseActive = false; + element.on('mousedown', function() { + scope.mouseActive = true; + $timeout(function(){ + scope.mouseActive = false; + }, 100); + }) + .on('focus', function() { + if (scope.mouseActive === false) { + element.addClass('md-focused'); + } + }) + .on('blur', function(ev) { + element.removeClass('md-focused'); + }); + } + } + +} +MdButtonDirective.$inject = ["$mdButtonInkRipple", "$mdTheming", "$mdAria", "$timeout"]; + +})(); +(function(){ +"use strict"; + /** * @ngdoc module * @name material.components.bottomSheet @@ -6149,7 +6615,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() { @@ -6328,7 +6796,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) { @@ -6418,153 +6886,15 @@ MdBottomSheetProvider.$inject = ["$$interimElementProvider"]; /** * @ngdoc module - * @name material.components.button - * @description + * @name material.components.card * - * Button + * @description + * Card components. */ -angular - .module('material.components.button', [ 'material.core' ]) - .directive('mdButton', MdButtonDirective); - -/** - * @ngdoc directive - * @name mdButton - * @module material.components.button - * - * @restrict E - * - * @description - * `` is a button directive with optional ink ripples (default enabled). - * - * If you supply a `href` or `ng-href` attribute, it will become an `` element. Otherwise, it will - * become a `'; - } - } - - function postLink(scope, element, attr) { - $mdTheming(element); - $mdButtonInkRipple.attach(scope, element); - - // Use async expect to support possible bindings in the button label - $mdAria.expectWithText(element, 'aria-label'); - - // For anchor elements, we have to set tabindex manually when the - // element is disabled - if (isAnchor(attr) && angular.isDefined(attr.ngDisabled) ) { - scope.$watch(attr.ngDisabled, function(isDisabled) { - element.attr('tabindex', isDisabled ? -1 : 0); - }); - } - - // disabling click event when disabled is true - element.on('click', function(e){ - if (attr.disabled === true) { - e.preventDefault(); - e.stopImmediatePropagation(); - } - }); - - if (!angular.isDefined(attr.mdNoFocusStyle)) { - // restrict focus styles to the keyboard - scope.mouseActive = false; - element.on('mousedown', function() { - scope.mouseActive = true; - $timeout(function(){ - scope.mouseActive = false; - }, 100); - }) - .on('focus', function() { - if (scope.mouseActive === false) { - element.addClass('md-focused'); - } - }) - .on('blur', function(ev) { - element.removeClass('md-focused'); - }); - } - } - -} -MdButtonDirective.$inject = ["$mdButtonInkRipple", "$mdTheming", "$mdAria", "$timeout"]; - -})(); -(function(){ -"use strict"; - -/** - * @ngdoc module - * @name material.components.card - * - * @description - * Card components. - */ -angular.module('material.components.card', [ - 'material.core' - ]) - .directive('mdCard', mdCardDirective); +angular.module('material.components.card', [ + 'material.core' + ]) + .directive('mdCard', mdCardDirective); /** @@ -6678,7 +7008,8 @@ angular.module('material.components.card', [ function mdCardDirective($mdTheming) { return { restrict: 'E', - link: function ($scope, $element) { + link: function ($scope, $element, attr) { + $element.addClass('_md'); // private md component indicator for styling $mdTheming($element); } }; @@ -6718,7 +7049,15 @@ angular * @param {string=} ng-change Angular expression to be executed when input changes due to user interaction with the input element. * @param {boolean=} md-no-ink Use of attribute indicates use of ripple ink effects * @param {string=} aria-label Adds label to checkbox for accessibility. - * Defaults to checkbox's text. If no default text is found, a warning will be logged. + * Defaults to checkbox's text. If no default text is found, a warning will be logged. + * @param {expression=} md-indeterminate This determines when the checkbox should be rendered as 'indeterminate'. + * If a truthy expression or no value is passed in the checkbox renders in the md-indeterminate state. + * If falsy expression is passed in it just looks like a normal unchecked checkbox. + * The indeterminate, checked, and unchecked states are mutually exclusive. A box cannot be in any two states at the same time. + * Adding the 'md-indeterminate' attribute overrides any checked/unchecked rendering logic. + * When using the 'md-indeterminate' attribute use 'ng-checked' to define rendering logic instead of using 'ng-model'. + * @param {expression=} ng-checked If this expression evaluates as truthy, the 'md-checked' css class is added to the checkbox and it + * will appear checked. * * @usage * @@ -6746,7 +7085,7 @@ function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $ transclude: true, require: '?ngModel', priority: 210, // Run before ngAria - template: + template: '
' + '
' + '
' + @@ -6759,10 +7098,12 @@ function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $ // ********************************************************** function compile (tElement, tAttrs) { + var container = tElement.children(); + var mdIndeterminateStateEnabled = $mdUtil.parseAttributeBoolean(tAttrs.mdIndeterminate); - tAttrs.type = 'checkbox'; - tAttrs.tabindex = tAttrs.tabindex || '0'; - tElement.attr('role', tAttrs.type); + tAttrs.$set('tabindex', tAttrs.tabindex || '0'); + tAttrs.$set('type', 'checkbox'); + tAttrs.$set('role', tAttrs.type); // Attach a click handler in compile in order to immediately stop propagation // (especially for ng-click) when the checkbox is disabled. @@ -6772,9 +7113,20 @@ function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $ } }); + // Redirect focus events to the root element, because IE11 is always focusing the container element instead + // of the md-checkbox element. This causes issues when using ngModelOptions: `updateOnBlur` + container.on('focus', function() { + tElement.focus(); + }); + return function postLink(scope, element, attr, ngModelCtrl) { + var isIndeterminate; ngModelCtrl = ngModelCtrl || $mdUtil.fakeNgModel(); $mdTheming(element); + if (mdIndeterminateStateEnabled) { + setIndeterminateState(); + scope.$watch(attr.mdIndeterminate, setIndeterminateState); + } if (attr.ngChecked) { scope.$watch( @@ -6840,6 +7192,7 @@ function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $ listener(ev); } } + function listener(ev) { if (element[0].hasAttribute('disabled')) { return; @@ -6855,12 +7208,20 @@ function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $ } function render() { - if(ngModelCtrl.$viewValue) { + if(ngModelCtrl.$viewValue && !isIndeterminate) { element.addClass(CHECKED_CSS); } else { element.removeClass(CHECKED_CSS); } } + + function setIndeterminateState(newValue) { + isIndeterminate = newValue !== false; + if (isIndeterminate) { + element.attr('aria-checked', 'mixed'); + } + element.toggleClass('md-indeterminate', isIndeterminate); + } }; } } @@ -6886,922 +7247,1172 @@ angular.module('material.components.chips', [ (function(){ "use strict"; -/** - * @ngdoc module - * @name material.components.content - * - * @description - * Scrollable content - */ -angular.module('material.components.content', [ - 'material.core' -]) - .directive('mdContent', mdContentDirective); +(function () { + "use strict"; -/** - * @ngdoc directive - * @name mdContent - * @module material.components.content - * - * @restrict E - * - * @description - * - * The `` directive is a container element useful for scrollable content. It achieves - * this by setting the CSS `overflow` property to `auto` so that content can properly scroll. - * - * In general, `` components are not designed to be nested inside one another. If - * possible, it is better to make them siblings. This often results in a better user experience as - * having nested scrollbars may confuse the user. - * - * ## Troubleshooting - * - * In some cases, you may wish to apply the `md-no-momentum` class to ensure that Safari's - * momentum scrolling is disabled. Momentum scrolling can cause flickering issues while scrolling - * SVG icons and some other components. - * - * @usage - * - * Add the `[layout-padding]` attribute to make the content padded. - * - * - * - * Lorem ipsum dolor sit amet, ne quod novum mei. - * - * - * - */ + /** + * Use a RegExp to check if the `md-colors=""` is static string + * or one that should be observed and dynamically interpolated. + */ + var STATIC_COLOR_EXPRESSION = /^{((\s|,)*?["'a-zA-Z-]+?\s*?:\s*?('|")[a-zA-Z0-9-.]*('|"))+\s*}$/; + var colorPalettes = undefined; -function mdContentDirective($mdTheming) { - return { - restrict: 'E', - controller: ['$scope', '$element', ContentController], - link: function(scope, element, attr) { - var node = element[0]; + /** + * @ngdoc module + * @name material.components.colors + * + * @description + * Define $mdColors service and a `md-colors=""` attribute directive + */ + angular + .module('material.components.colors', ['material.core']) + .directive('mdColors', MdColorsDirective) + .service('$mdColors', MdColorsService); - $mdTheming(element); - scope.$broadcast('$mdContentLoaded', element); + /** + * @ngdoc service + * @name $mdColors + * @module material.components.colors + * + * @description + * With only defining themes, one couldn't get non ngMaterial elements colored with Material colors, + * `$mdColors` service is used by the md-color directive to convert the 1..n color expressions to RGBA values and will apply + * those values to element as CSS property values. + * + * @usage + * + * angular.controller('myCtrl', function ($mdColors) { + * var color = $mdColors.getThemeColor('myTheme-red-200-0.5'); + * ... + * }); + * + * + */ + function MdColorsService($mdTheming, $mdUtil, $log) { + colorPalettes = colorPalettes || Object.keys($mdTheming.PALETTES); - iosScrollFix(element[0]); - } - }; + // Publish service instance + return { + applyThemeColors: applyThemeColors, + getThemeColor: getThemeColor, + hasTheme: hasTheme + }; - function ContentController($scope, $element) { - this.$scope = $scope; - this.$element = $element; - } -} -mdContentDirective.$inject = ["$mdTheming"]; + // ******************************************** + // Internal Methods + // ******************************************** -function iosScrollFix(node) { - // IOS FIX: - // If we scroll where there is no more room for the webview to scroll, - // by default the webview itself will scroll up and down, this looks really - // bad. So if we are scrolling to the very top or bottom, add/subtract one - angular.element(node).on('$md.pressdown', function(ev) { - // Only touch events - if (ev.pointer.type !== 't') return; - // Don't let a child content's touchstart ruin it for us. - if (ev.$materialScrollFixed) return; - ev.$materialScrollFixed = true; + /** + * @ngdoc method + * @name $mdColors#applyThemeColors + * + * @description + * Gets a color json object, keys are css properties and values are string of the wanted color + * Then calculate the rgba() values based on the theme color parts + * + * @param {DOMElement} element the element to apply the styles on. + * @param {object} colorExpression json object, keys are css properties and values are string of the wanted color, + * for example: `{color: 'red-A200-0.3'}`. + * + * @usage + * + * app.directive('myDirective', function($mdColors) { + * return { + * ... + * link: function (scope, elem) { + * $mdColors.applyThemeColors(elem, {color: 'red'}); + * } + * } + * }); + * + */ + function applyThemeColors(element, colorExpression) { + try { + // Assign the calculate RGBA color values directly as inline CSS + element.css(interpolateColors(colorExpression)); + } catch (e) { + $log.error(e.message); + } - if (node.scrollTop === 0) { - node.scrollTop = 1; - } else if (node.scrollHeight === node.scrollTop + node.offsetHeight) { - node.scrollTop -= 1; } - }); -} -})(); -(function(){ -"use strict"; - -(function() { - 'use strict'; - - /** - * @ngdoc module - * @name material.components.datepicker - * @description Datepicker - */ - angular.module('material.components.datepicker', [ - 'material.core', - 'material.components.icon', - 'material.components.virtualRepeat' - ]).directive('mdCalendar', calendarDirective); - - - // POST RELEASE - // TODO(jelbourn): Mac Cmd + left / right == Home / End - // TODO(jelbourn): Clicking on the month label opens the month-picker. - // TODO(jelbourn): Minimum and maximum date - // TODO(jelbourn): Refactor month element creation to use cloneNode (performance). - // TODO(jelbourn): Define virtual scrolling constants (compactness) users can override. - // TODO(jelbourn): Animated month transition on ng-model change (virtual-repeat) - // TODO(jelbourn): Scroll snapping (virtual repeat) - // TODO(jelbourn): Remove superfluous row from short months (virtual-repeat) - // TODO(jelbourn): Month headers stick to top when scrolling. - // TODO(jelbourn): Previous month opacity is lowered when partially scrolled out of view. - // TODO(jelbourn): Support md-calendar standalone on a page (as a tabstop w/ aria-live - // announcement and key handling). - // Read-only calendar (not just date-picker). - - /** - * Height of one calendar month tbody. This must be made known to the virtual-repeat and is - * subsequently used for scrolling to specific months. - */ - var TBODY_HEIGHT = 265; - - /** - * Height of a calendar month with a single row. This is needed to calculate the offset for - * rendering an extra month in virtual-repeat that only contains one row. - */ - var TBODY_SINGLE_ROW_HEIGHT = 45; - - function calendarDirective() { - return { - template: - '' + - '
' + - '' + - '' + - '' + - '
' + - '
' + - '
', - scope: { - minDate: '=mdMinDate', - maxDate: '=mdMaxDate', - dateFilter: '=mdDateFilter', - }, - require: ['ngModel', 'mdCalendar'], - controller: CalendarCtrl, - controllerAs: 'ctrl', - bindToController: true, - link: function(scope, element, attrs, controllers) { - var ngModelCtrl = controllers[0]; - var mdCalendarCtrl = controllers[1]; - mdCalendarCtrl.configureNgModel(ngModelCtrl); - } - }; - } - - /** Class applied to the selected date cell/. */ - var SELECTED_DATE_CLASS = 'md-calendar-selected-date'; - - /** Class applied to the focused date cell/. */ - var FOCUSED_DATE_CLASS = 'md-focus'; - - /** Next identifier for calendar instance. */ - var nextUniqueId = 0; - - /** The first renderable date in the virtual-scrolling calendar (for all instances). */ - var firstRenderableDate = null; - - /** - * Controller for the mdCalendar component. - * @ngInject @constructor - */ - function CalendarCtrl($element, $attrs, $scope, $animate, $q, $mdConstant, - $mdTheming, $$mdDateUtil, $mdDateLocale, $mdInkRipple, $mdUtil) { - $mdTheming($element); /** - * Dummy array-like object for virtual-repeat to iterate over. The length is the total - * number of months that can be viewed. This is shorter than ideal because of (potential) - * Firefox bug https://bugzilla.mozilla.org/show_bug.cgi?id=1181658. + * @ngdoc method + * @name $mdColors#getThemeColor + * + * @description + * Get parsed color from expression + * + * @param {string} expression string of a color expression (for instance `'red-700-0.8'`) + * + * @returns {string} a css color expression (for instance `rgba(211, 47, 47, 0.8)`) + * + * @usage + * + * angular.controller('myCtrl', function ($mdColors) { + * var color = $mdColors.getThemeColor('myTheme-red-200-0.5'); + * ... + * }); + * */ - this.items = {length: 2000}; + function getThemeColor(expression) { + var color = extractColorOptions(expression); - if (this.maxDate && this.minDate) { - // Limit the number of months if min and max dates are set. - var numMonths = $$mdDateUtil.getMonthDistance(this.minDate, this.maxDate) + 1; - numMonths = Math.max(numMonths, 1); - // Add an additional month as the final dummy month for rendering purposes. - numMonths += 1; - this.items.length = numMonths; + return parseColor(color); } - /** @final {!angular.$animate} */ - this.$animate = $animate; - - /** @final {!angular.$q} */ - this.$q = $q; - - /** @final */ - this.$mdInkRipple = $mdInkRipple; - - /** @final */ - this.$mdUtil = $mdUtil; - - /** @final */ - this.keyCode = $mdConstant.KEY_CODE; - - /** @final */ - this.dateUtil = $$mdDateUtil; - - /** @final */ - this.dateLocale = $mdDateLocale; + /** + * Return the parsed color + * @param color hashmap of color definitions + * @param contrast whether use contrast color for foreground + * @returns rgba color string + */ + function parseColor(color, contrast) { + contrast = contrast || false; + var rgbValues = $mdTheming.PALETTES[color.palette][color.hue]; - /** @final {!angular.JQLite} */ - this.$element = $element; + rgbValues = contrast ? rgbValues.contrast : rgbValues.value; - /** @final {!angular.Scope} */ - this.$scope = $scope; + return $mdUtil.supplant('rgba( {0}, {1}, {2}, {3} )', + [rgbValues[0], rgbValues[1], rgbValues[2], rgbValues[3] || color.opacity] + ); + } - /** @final {HTMLElement} */ - this.calendarElement = $element[0].querySelector('.md-calendar'); + /** + * Convert the color expression into an object with scope-interpolated values + * Then calculate the rgba() values based on the theme color parts + * + * @results Hashmap of CSS properties with associated `rgba( )` string vales + * + * + */ + function interpolateColors(themeColors) { + var rgbColors = {}; - /** @final {HTMLElement} */ - this.calendarScroller = $element[0].querySelector('.md-virtual-repeat-scroller'); + var hasColorProperty = themeColors.hasOwnProperty('color'); - /** @final {Date} */ - this.today = this.dateUtil.createDateAtMidnight(); + angular.forEach(themeColors, function (value, key) { + var color = extractColorOptions(value); + var hasBackground = key.indexOf('background') > -1; - /** @type {Date} */ - this.firstRenderableDate = this.dateUtil.incrementMonths(this.today, -this.items.length / 2); + rgbColors[key] = parseColor(color); + if (hasBackground && !hasColorProperty) { + rgbColors['color'] = parseColor(color, true); + } + }); - if (this.minDate && this.minDate > this.firstRenderableDate) { - this.firstRenderableDate = this.minDate; - } else if (this.maxDate) { - // Calculate the difference between the start date and max date. - // Subtract 1 because it's an inclusive difference and 1 for the final dummy month. - // - var monthDifference = this.items.length - 2; - this.firstRenderableDate = this.dateUtil.incrementMonths(this.maxDate, -(this.items.length - 2)); + return rgbColors; } - - /** @final {number} Unique ID for this calendar instance. */ - this.id = nextUniqueId++; - - /** @type {!angular.NgModelController} */ - this.ngModelCtrl = null; - /** - * The selected date. Keep track of this separately from the ng-model value so that we - * can know, when the ng-model value changes, what the previous value was before it's updated - * in the component's UI. - * - * @type {Date} + * Check if expression has defined theme + * e.g. + * 'myTheme-primary' => true + * 'red-800' => false */ - this.selectedDate = null; + function hasTheme(expression) { + return angular.isDefined($mdTheming.THEMES[expression.split('-')[0]]); + } /** - * The date that is currently focused or showing in the calendar. This will initially be set - * to the ng-model value if set, otherwise to today. It will be updated as the user navigates - * to other months. The cell corresponding to the displayDate does not necesarily always have - * focus in the document (such as for cases when the user is scrolling the calendar). - * @type {Date} + * For the evaluated expression, extract the color parts into a hash map */ - this.displayDate = null; + function extractColorOptions(expression) { + var parts = expression.split('-'); + var hasTheme = angular.isDefined($mdTheming.THEMES[parts[0]]); + var theme = hasTheme ? parts.splice(0, 1)[0] : $mdTheming.defaultTheme(); + + return { + theme: theme, + palette: extractPalette(parts, theme), + hue: extractHue(parts, theme), + opacity: parts[2] || 1 + }; + } /** - * The date that has or should have focus. - * @type {Date} + * Calculate the theme palette name */ - this.focusDate = null; + function extractPalette(parts, theme) { + // If the next section is one of the palettes we assume it's a two word palette + // Two word palette can be also written in camelCase, forming camelCase to dash-case - /** @type {boolean} */ - this.isInitialized = false; + var isTwoWord = parts.length > 1 && colorPalettes.indexOf(parts[1]) !== -1; + var palette = parts[0].replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); - /** @type {boolean} */ - this.isMonthTransitionInProgress = false; + if (isTwoWord) palette = parts[0] + '-' + parts.splice(1, 1); - // Unless the user specifies so, the calendar should not be a tab stop. - // This is necessary because ngAria might add a tabindex to anything with an ng-model - // (based on whether or not the user has turned that particular feature on/off). - if (!$attrs['tabindex']) { - $element.attr('tabindex', '-1'); + if (colorPalettes.indexOf(palette) === -1) { + // If the palette is not in the palette list it's one of primary/accent/warn/background + var scheme = $mdTheming.THEMES[theme].colors[palette]; + if (!scheme) { + throw new Error($mdUtil.supplant('mdColors: couldn\'t find \'{palette}\' in the palettes.', {palette: palette})); + } + palette = scheme.name; + } + + return palette; } - var self = this; + function extractHue(parts, theme) { + var themeColors = $mdTheming.THEMES[theme].colors; - /** - * Handles a click event on a date cell. - * Created here so that every cell can use the same function instance. - * @this {HTMLTableCellElement} The cell that was clicked. - */ - this.cellClickHandler = function() { - var cellElement = this; - if (this.hasAttribute('data-timestamp')) { - $scope.$apply(function() { - var timestamp = Number(cellElement.getAttribute('data-timestamp')); - self.setNgModelValue(self.dateUtil.createDateAtMidnight(timestamp)); - }); - } - }; + if (parts[1] === 'hue') { + var hueNumber = parseInt(parts.splice(2, 1)[0], 10); - this.attachCalendarEventListeners(); - } - CalendarCtrl.$inject = ["$element", "$attrs", "$scope", "$animate", "$q", "$mdConstant", "$mdTheming", "$$mdDateUtil", "$mdDateLocale", "$mdInkRipple", "$mdUtil"]; + if (hueNumber < 1 || hueNumber > 3) { + throw new Error($mdUtil.supplant('mdColors: \'hue-{hueNumber}\' is not a valid hue, can be only \'hue-1\', \'hue-2\' and \'hue-3\'', {hueNumber: hueNumber})); + } + parts[1] = 'hue-' + hueNumber; + if (!(parts[0] in themeColors)) { + throw new Error($mdUtil.supplant('mdColors: \'hue-x\' can only be used with [{availableThemes}], but was used with \'{usedTheme}\'', { + availableThemes: Object.keys(themeColors).join(', '), + usedTheme: parts[0] + })); + } - /*** Initialization ***/ + return themeColors[parts[0]].hues[parts[1]]; + } + + return parts[1] || themeColors[parts[0] in themeColors ? parts[0] : 'primary'].hues['default']; + } + } + MdColorsService.$inject = ["$mdTheming", "$mdUtil", "$log"]; /** - * Sets up the controller's reference to ngModelController. - * @param {!angular.NgModelController} ngModelCtrl + * @ngdoc directive + * @name mdColors + * @module material.components.colors + * + * @restrict A + * + * @description + * `mdColors` directive will apply the theme-based color expression as RGBA CSS style values. + * + * The format will be similar to our color defining in the scss files: + * + * ## `[?theme]-[palette]-[?hue]-[?opacity]` + * - [theme] - default value is the default theme + * - [palette] - can be either palette name or primary/accent/warn/background + * - [hue] - default is 500 (hue-x can be used with primary/accent/warn/background) + * - [opacity] - default is 1 + * + * > `?` indicates optional parameter + * + * @usage + * + *
+ *
+ * Color demo + *
+ *
+ *
+ * + * `mdColors` directive will automatically watch for changes in the expression if it recognizes an interpolation + * expression or a function. For performance options, you can use `::` prefix to the `md-colors` expression + * to indicate a one-time data binding. + * + * + * + * + * */ - CalendarCtrl.prototype.configureNgModel = function(ngModelCtrl) { - this.ngModelCtrl = ngModelCtrl; + function MdColorsDirective($mdColors, $mdUtil, $log, $parse) { + return { + restrict: 'A', + require: ['^?mdTheme'], + compile: function (tElem, tAttrs) { + var shouldWatch = shouldColorsWatch(); - var self = this; - ngModelCtrl.$render = function() { - self.changeSelectedDate(self.ngModelCtrl.$viewValue); - }; - }; + return function (scope, element, attrs, ctrl) { + var mdThemeController = ctrl[0]; - /** - * Initialize the calendar by building the months that are initially visible. - * Initialization should occur after the ngModel value is known. - */ - CalendarCtrl.prototype.buildInitialCalendarDisplay = function() { - this.buildWeekHeader(); - this.hideVerticalScrollbar(); + var parseColors = function (theme) { + /** + * Json.parse() does not work because the keys are not quoted; + * use $parse to convert to a hash map + */ + var colors = $parse(attrs.mdColors)(scope); - this.displayDate = this.selectedDate || this.today; - this.isInitialized = true; - }; + /** + * If mdTheme is defined up the DOM tree + * we add mdTheme theme to colors who doesn't specified a theme + * + * # example + * + *
+ *
+ * Color demo + *
+ *
+ *
+ * + * 'primary-600' will be 'myTheme-primary-600', + * but 'mySecondTheme-accent-200' will stay the same cause it has a theme prefix + */ + if (mdThemeController) { + Object.keys(colors).forEach(function (prop) { + var color = colors[prop]; + if (!$mdColors.hasTheme(color)) { + colors[prop] = (theme || mdThemeController.$mdTheme) + '-' + color; + } + }); + } - /** - * Hides the vertical scrollbar on the calendar scroller by setting the width on the - * calendar scroller and the `overflow: hidden` wrapper around the scroller, and then setting - * a padding-right on the scroller equal to the width of the browser's scrollbar. - * - * This will cause a reflow. - */ - CalendarCtrl.prototype.hideVerticalScrollbar = function() { - var element = this.$element[0]; + return colors; + }; - var scrollMask = element.querySelector('.md-calendar-scroll-mask'); - var scroller = this.calendarScroller; + /** + * Registering for mgTheme changes and asking mdTheme controller run our callback whenever a theme changes + */ + var unregisterChanges = angular.noop; - var headerWidth = element.querySelector('.md-calendar-day-header').clientWidth; - var scrollbarWidth = scroller.offsetWidth - scroller.clientWidth; + if (mdThemeController) { + unregisterChanges = mdThemeController.registerChanges(function (theme) { + $mdColors.applyThemeColors(element, parseColors(theme)); + }); + } - scrollMask.style.width = headerWidth + 'px'; - scroller.style.width = (headerWidth + scrollbarWidth) + 'px'; - scroller.style.paddingRight = scrollbarWidth + 'px'; - }; + scope.$on('destroy', function () { + unregisterChanges(); + }); + try { + if (shouldWatch) { + scope.$watch(parseColors, angular.bind(this, + $mdColors.applyThemeColors, element + ), true); + } + else { + $mdColors.applyThemeColors(element, parseColors()); + } - /** Attach event listeners for the calendar. */ - CalendarCtrl.prototype.attachCalendarEventListeners = function() { - // Keyboard interaction. - this.$element.on('keydown', angular.bind(this, this.handleKeyEvent)); - }; + } + catch (e) { + $log.error(e.message); + } - /*** User input handling ***/ + }; - /** - * Handles a key event in the calendar with the appropriate action. The action will either - * be to select the focused date or to navigate to focus a new date. - * @param {KeyboardEvent} event - */ - CalendarCtrl.prototype.handleKeyEvent = function(event) { - var self = this; - this.$scope.$apply(function() { - // Capture escape and emit back up so that a wrapping component - // (such as a date-picker) can decide to close. - if (event.which == self.keyCode.ESCAPE || event.which == self.keyCode.TAB) { - self.$scope.$emit('md-calendar-close'); + function shouldColorsWatch() { + // Simulate 1x binding and mark mdColorsWatch == false + var rawColorExpression = tAttrs.mdColors; + var bindOnce = rawColorExpression.indexOf('::') > -1; + var isStatic = bindOnce ? true : STATIC_COLOR_EXPRESSION.test(tAttrs.mdColors); - if (event.which == self.keyCode.TAB) { - event.preventDefault(); - } + // Remove it for the postLink... + tAttrs.mdColors = rawColorExpression.replace('::', ''); - return; - } + var hasWatchAttr = angular.isDefined(tAttrs.mdColorsWatch); - // Remaining key events fall into two categories: selection and navigation. - // Start by checking if this is a selection event. - if (event.which === self.keyCode.ENTER) { - self.setNgModelValue(self.displayDate); - event.preventDefault(); - return; + return (bindOnce || isStatic) ? false : + hasWatchAttr ? $mdUtil.parseAttributeBoolean(tAttrs.mdColorsWatch) : true; + } } + }; - // Selection isn't occuring, so the key event is either navigation or nothing. - var date = self.getFocusDateFromKeyEvent(event); - if (date) { - date = self.boundDateByMinAndMax(date); - event.preventDefault(); - event.stopPropagation(); + } + MdColorsDirective.$inject = ["$mdColors", "$mdUtil", "$log", "$parse"]; - // Since this is a keyboard interaction, actually give the newly focused date keyboard - // focus after the been brought into view. - self.changeDisplayDate(date).then(function () { - self.focus(date); - }); - } - }); - }; - /** - * Gets the date to focus as the result of a key event. - * @param {KeyboardEvent} event - * @returns {Date} Date to navigate to, or null if the key does not match a calendar shortcut. - */ - CalendarCtrl.prototype.getFocusDateFromKeyEvent = function(event) { - var dateUtil = this.dateUtil; - var keyCode = this.keyCode; +})(); - switch (event.which) { - case keyCode.RIGHT_ARROW: return dateUtil.incrementDays(this.displayDate, 1); - case keyCode.LEFT_ARROW: return dateUtil.incrementDays(this.displayDate, -1); - case keyCode.DOWN_ARROW: - return event.metaKey ? - dateUtil.incrementMonths(this.displayDate, 1) : - dateUtil.incrementDays(this.displayDate, 7); - case keyCode.UP_ARROW: - return event.metaKey ? - dateUtil.incrementMonths(this.displayDate, -1) : - dateUtil.incrementDays(this.displayDate, -7); - case keyCode.PAGE_DOWN: return dateUtil.incrementMonths(this.displayDate, 1); - case keyCode.PAGE_UP: return dateUtil.incrementMonths(this.displayDate, -1); - case keyCode.HOME: return dateUtil.getFirstDateOfMonth(this.displayDate); - case keyCode.END: return dateUtil.getLastDateOfMonth(this.displayDate); - default: return null; +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.content + * + * @description + * Scrollable content + */ +angular.module('material.components.content', [ + 'material.core' +]) + .directive('mdContent', mdContentDirective); + +/** + * @ngdoc directive + * @name mdContent + * @module material.components.content + * + * @restrict E + * + * @description + * + * The `` directive is a container element useful for scrollable content. It achieves + * this by setting the CSS `overflow` property to `auto` so that content can properly scroll. + * + * In general, `` components are not designed to be nested inside one another. If + * possible, it is better to make them siblings. This often results in a better user experience as + * having nested scrollbars may confuse the user. + * + * ## Troubleshooting + * + * In some cases, you may wish to apply the `md-no-momentum` class to ensure that Safari's + * momentum scrolling is disabled. Momentum scrolling can cause flickering issues while scrolling + * SVG icons and some other components. + * + * @usage + * + * Add the `[layout-padding]` attribute to make the content padded. + * + * + * + * Lorem ipsum dolor sit amet, ne quod novum mei. + * + * + * + */ + +function mdContentDirective($mdTheming) { + return { + restrict: 'E', + controller: ['$scope', '$element', ContentController], + link: function(scope, element) { + element.addClass('_md'); // private md component indicator for styling + + $mdTheming(element); + scope.$broadcast('$mdContentLoaded', element); + + iosScrollFix(element[0]); } }; - /** - * Gets the "index" of the currently selected date as it would be in the virtual-repeat. - * @returns {number} - */ - CalendarCtrl.prototype.getSelectedMonthIndex = function() { - return this.dateUtil.getMonthDistance(this.firstRenderableDate, - this.selectedDate || this.today); - }; + function ContentController($scope, $element) { + this.$scope = $scope; + this.$element = $element; + } +} +mdContentDirective.$inject = ["$mdTheming"]; - /** - * Scrolls to the month of the given date. - * @param {Date} date - */ - CalendarCtrl.prototype.scrollToMonth = function(date) { - if (!this.dateUtil.isValidDate(date)) { - return; +function iosScrollFix(node) { + // IOS FIX: + // If we scroll where there is no more room for the webview to scroll, + // by default the webview itself will scroll up and down, this looks really + // bad. So if we are scrolling to the very top or bottom, add/subtract one + angular.element(node).on('$md.pressdown', function(ev) { + // Only touch events + if (ev.pointer.type !== 't') return; + // Don't let a child content's touchstart ruin it for us. + if (ev.$materialScrollFixed) return; + ev.$materialScrollFixed = true; + + if (node.scrollTop === 0) { + node.scrollTop = 1; + } else if (node.scrollHeight === node.scrollTop + node.offsetHeight) { + node.scrollTop -= 1; } + }); +} - var monthDistance = this.dateUtil.getMonthDistance(this.firstRenderableDate, date); - this.calendarScroller.scrollTop = monthDistance * TBODY_HEIGHT; - }; +})(); +(function(){ +"use strict"; - /** - * Sets the ng-model value for the calendar and emits a change event. - * @param {Date} date - */ - CalendarCtrl.prototype.setNgModelValue = function(date) { - this.$scope.$emit('md-calendar-change', date); - this.ngModelCtrl.$setViewValue(date); - this.ngModelCtrl.$render(); - }; +(function() { + 'use strict'; /** - * Focus the cell corresponding to the given date. - * @param {Date=} opt_date + * @ngdoc module + * @name material.components.calendar + * @description Calendar */ - CalendarCtrl.prototype.focus = function(opt_date) { - var date = opt_date || this.selectedDate || this.today; + angular.module('material.components.datepicker', [ + 'material.core', + 'material.components.icon', + 'material.components.virtualRepeat' + ]).directive('mdCalendar', calendarDirective); - var previousFocus = this.calendarElement.querySelector('.md-focus'); - if (previousFocus) { - previousFocus.classList.remove(FOCUSED_DATE_CLASS); - } - var cellId = this.getDateId(date); - var cell = document.getElementById(cellId); - if (cell) { - cell.classList.add(FOCUSED_DATE_CLASS); - cell.focus(); - } else { - this.focusDate = date; - } - }; + // POST RELEASE + // TODO(jelbourn): Mac Cmd + left / right == Home / End + // TODO(jelbourn): Refactor month element creation to use cloneNode (performance). + // TODO(jelbourn): Define virtual scrolling constants (compactness) users can override. + // TODO(jelbourn): Animated month transition on ng-model change (virtual-repeat) + // TODO(jelbourn): Scroll snapping (virtual repeat) + // TODO(jelbourn): Remove superfluous row from short months (virtual-repeat) + // TODO(jelbourn): Month headers stick to top when scrolling. + // TODO(jelbourn): Previous month opacity is lowered when partially scrolled out of view. + // TODO(jelbourn): Support md-calendar standalone on a page (as a tabstop w/ aria-live + // announcement and key handling). + // Read-only calendar (not just date-picker). + + function calendarDirective() { + return { + template: function(tElement, tAttr) { + // TODO(crisbeto): This is a workaround that allows the calendar to work, without + // a datepicker, until issue #8585 gets resolved. It can safely be removed + // afterwards. This ensures that the virtual repeater scrolls to the proper place on load by + // deferring the execution until the next digest. It's necessary only if the calendar is used + // without a datepicker, otherwise it's already wrapped in an ngIf. + var extraAttrs = tAttr.hasOwnProperty('ngIf') ? '' : 'ng-if="calendarCtrl.isInitialized"'; + var template = '' + + '
' + + '' + + '' + + '
'; + + return template; + }, + scope: { + minDate: '=mdMinDate', + maxDate: '=mdMaxDate', + dateFilter: '=mdDateFilter' + }, + require: ['ngModel', 'mdCalendar'], + controller: CalendarCtrl, + controllerAs: 'calendarCtrl', + bindToController: true, + link: function(scope, element, attrs, controllers) { + var ngModelCtrl = controllers[0]; + var mdCalendarCtrl = controllers[1]; + mdCalendarCtrl.configureNgModel(ngModelCtrl); + } + }; + } /** - * If a date exceeds minDate or maxDate, returns date matching minDate or maxDate, respectively. - * Otherwise, returns the date. - * @param {Date} date - * @return {Date} + * Occasionally the hideVerticalScrollbar method might read an element's + * width as 0, because it hasn't been laid out yet. This value will be used + * as a fallback, in order to prevent scenarios where the element's width + * would otherwise have been set to 0. This value is the "usual" width of a + * calendar within a floating calendar pane. */ - CalendarCtrl.prototype.boundDateByMinAndMax = function(date) { - var boundDate = date; - if (this.minDate && date < this.minDate) { - boundDate = new Date(this.minDate.getTime()); - } - if (this.maxDate && date > this.maxDate) { - boundDate = new Date(this.maxDate.getTime()); - } - return boundDate; - }; + var FALLBACK_WIDTH = 340; - /*** Updating the displayed / selected date ***/ + /** Next identifier for calendar instance. */ + var nextUniqueId = 0; /** - * Change the selected date in the calendar (ngModel value has already been changed). - * @param {Date} date + * Controller for the mdCalendar component. + * @ngInject @constructor */ - CalendarCtrl.prototype.changeSelectedDate = function(date) { - var self = this; - var previousSelectedDate = this.selectedDate; - this.selectedDate = date; - this.changeDisplayDate(date).then(function() { + function CalendarCtrl($element, $scope, $$mdDateUtil, $mdUtil, + $mdConstant, $mdTheming, $$rAF, $attrs) { - // Remove the selected class from the previously selected date, if any. - if (previousSelectedDate) { - var prevDateCell = - document.getElementById(self.getDateId(previousSelectedDate)); - if (prevDateCell) { - prevDateCell.classList.remove(SELECTED_DATE_CLASS); - prevDateCell.setAttribute('aria-selected', 'false'); - } - } + $mdTheming($element); - // Apply the select class to the new selected date if it is set. - if (date) { - var dateCell = document.getElementById(self.getDateId(date)); - if (dateCell) { - dateCell.classList.add(SELECTED_DATE_CLASS); - dateCell.setAttribute('aria-selected', 'true'); - } - } - }); - }; + /** @final {!angular.JQLite} */ + this.$element = $element; + /** @final {!angular.Scope} */ + this.$scope = $scope; - /** - * Change the date that is being shown in the calendar. If the given date is in a different - * month, the displayed month will be transitioned. - * @param {Date} date - */ - CalendarCtrl.prototype.changeDisplayDate = function(date) { - // Initialization is deferred until this function is called because we want to reflect - // the starting value of ngModel. - if (!this.isInitialized) { - this.buildInitialCalendarDisplay(); - return this.$q.when(); - } + /** @final */ + this.dateUtil = $$mdDateUtil; - // If trying to show an invalid date or a transition is in progress, do nothing. - if (!this.dateUtil.isValidDate(date) || this.isMonthTransitionInProgress) { - return this.$q.when(); - } + /** @final */ + this.$mdUtil = $mdUtil; - this.isMonthTransitionInProgress = true; - var animationPromise = this.animateDateChange(date); + /** @final */ + this.keyCode = $mdConstant.KEY_CODE; - this.displayDate = date; + /** @final */ + this.$$rAF = $$rAF; - var self = this; - animationPromise.then(function() { - self.isMonthTransitionInProgress = false; - }); + /** @final {Date} */ + this.today = this.dateUtil.createDateAtMidnight(); - return animationPromise; - }; + /** @type {!angular.NgModelController} */ + this.ngModelCtrl = null; - /** - * Animates the transition from the calendar's current month to the given month. - * @param {Date} date - * @returns {angular.$q.Promise} The animation promise. - */ - CalendarCtrl.prototype.animateDateChange = function(date) { - this.scrollToMonth(date); - return this.$q.when(); - }; + /** @type {String} The currently visible calendar view. */ + this.currentView = 'month'; - /*** Constructing the calendar table ***/ + /** @type {String} Class applied to the selected date cell. */ + this.SELECTED_DATE_CLASS = 'md-calendar-selected-date'; - /** - * Builds and appends a day-of-the-week header to the calendar. - * This should only need to be called once during initialization. - */ - CalendarCtrl.prototype.buildWeekHeader = function() { - var firstDayOfWeek = this.dateLocale.firstDayOfWeek; - var shortDays = this.dateLocale.shortDays; + /** @type {String} Class applied to the cell for today. */ + this.TODAY_CLASS = 'md-calendar-date-today'; - var row = document.createElement('tr'); - for (var i = 0; i < 7; i++) { - var th = document.createElement('th'); - th.textContent = shortDays[(i + firstDayOfWeek) % 7]; - row.appendChild(th); - } + /** @type {String} Class applied to the focused cell. */ + this.FOCUSED_DATE_CLASS = 'md-focus'; - this.$element.find('thead').append(row); - }; + /** @final {number} Unique ID for this calendar instance. */ + this.id = nextUniqueId++; /** - * Gets an identifier for a date unique to the calendar instance for internal - * purposes. Not to be displayed. - * @param {Date} date - * @returns {string} - */ - CalendarCtrl.prototype.getDateId = function(date) { - return [ - 'md', - this.id, - date.getFullYear(), - date.getMonth(), - date.getDate() - ].join('-'); - }; -})(); + * The date that is currently focused or showing in the calendar. This will initially be set + * to the ng-model value if set, otherwise to today. It will be updated as the user navigates + * to other months. The cell corresponding to the displayDate does not necesarily always have + * focus in the document (such as for cases when the user is scrolling the calendar). + * @type {Date} + */ + this.displayDate = null; -})(); -(function(){ -"use strict"; + /** + * The selected date. Keep track of this separately from the ng-model value so that we + * can know, when the ng-model value changes, what the previous value was before it's updated + * in the component's UI. + * + * @type {Date} + */ + this.selectedDate = null; -(function() { - 'use strict'; + /** + * Used to toggle initialize the root element in the next digest. + * @type {Boolean} + */ + this.isInitialized = false; + /** + * Cache for the width of the element without a scrollbar. Used to hide the scrollbar later on + * and to avoid extra reflows when switching between views. + * @type {Number} + */ + this.width = 0; - angular.module('material.components.datepicker') - .directive('mdCalendarMonth', mdCalendarMonthDirective); + /** + * Caches the width of the scrollbar in order to be used when hiding it and to avoid extra reflows. + * @type {Number} + */ + this.scrollbarWidth = 0; + + // Unless the user specifies so, the calendar should not be a tab stop. + // This is necessary because ngAria might add a tabindex to anything with an ng-model + // (based on whether or not the user has turned that particular feature on/off). + if (!$attrs.tabindex) { + $element.attr('tabindex', '-1'); + } + $element.on('keydown', angular.bind(this, this.handleKeyEvent)); + } + CalendarCtrl.$inject = ["$element", "$scope", "$$mdDateUtil", "$mdUtil", "$mdConstant", "$mdTheming", "$$rAF", "$attrs"]; /** - * Private directive consumed by md-calendar. Having this directive lets the calender use - * md-virtual-repeat and also cleanly separates the month DOM construction functions from - * the rest of the calendar controller logic. + * Sets up the controller's reference to ngModelController. + * @param {!angular.NgModelController} ngModelCtrl */ - function mdCalendarMonthDirective() { - return { - require: ['^^mdCalendar', 'mdCalendarMonth'], - scope: {offset: '=mdMonthOffset'}, - controller: CalendarMonthCtrl, - controllerAs: 'mdMonthCtrl', - bindToController: true, - link: function(scope, element, attrs, controllers) { - var calendarCtrl = controllers[0]; - var monthCtrl = controllers[1]; + CalendarCtrl.prototype.configureNgModel = function(ngModelCtrl) { + var self = this; - monthCtrl.calendarCtrl = calendarCtrl; - monthCtrl.generateContent(); + self.ngModelCtrl = ngModelCtrl; - // The virtual-repeat re-uses the same DOM elements, so there are only a limited number - // of repeated items that are linked, and then those elements have their bindings updataed. - // Since the months are not generated by bindings, we simply regenerate the entire thing - // when the binding (offset) changes. - scope.$watch(function() { return monthCtrl.offset; }, function(offset, oldOffset) { - if (offset != oldOffset) { - monthCtrl.generateContent(); - } - }); - } - }; - } + self.$mdUtil.nextTick(function() { + self.isInitialized = true; + }); + + ngModelCtrl.$render = function() { + var value = this.$viewValue; - /** Class applied to the cell for today. */ - var TODAY_CLASS = 'md-calendar-date-today'; + // Notify the child scopes of any changes. + self.$scope.$broadcast('md-calendar-parent-changed', value); - /** Class applied to the selected date cell/. */ - var SELECTED_DATE_CLASS = 'md-calendar-selected-date'; + // Set up the selectedDate if it hasn't been already. + if (!self.selectedDate) { + self.selectedDate = value; + } - /** Class applied to the focused date cell/. */ - var FOCUSED_DATE_CLASS = 'md-focus'; + // Also set up the displayDate. + if (!self.displayDate) { + self.displayDate = self.selectedDate || self.today; + } + }; + }; /** - * Controller for a single calendar month. - * @ngInject @constructor + * Sets the ng-model value for the calendar and emits a change event. + * @param {Date} date */ - function CalendarMonthCtrl($element, $$mdDateUtil, $mdDateLocale) { - this.dateUtil = $$mdDateUtil; - this.dateLocale = $mdDateLocale; - this.$element = $element; - this.calendarCtrl = null; + CalendarCtrl.prototype.setNgModelValue = function(date) { + var value = this.dateUtil.createDateAtMidnight(date); + this.focus(value); + this.$scope.$emit('md-calendar-change', value); + this.ngModelCtrl.$setViewValue(value); + this.ngModelCtrl.$render(); + return value; + }; - /** - * Number of months from the start of the month "items" that the currently rendered month - * occurs. Set via angular data binding. - * @type {number} - */ - this.offset; + /** + * Sets the current view that should be visible in the calendar + * @param {string} newView View name to be set. + * @param {number|Date} time Date object or a timestamp for the new display date. + */ + CalendarCtrl.prototype.setCurrentView = function(newView, time) { + var self = this; - /** - * Date cell to focus after appending the month to the document. - * @type {HTMLElement} - */ - this.focusAfterAppend = null; - } - CalendarMonthCtrl.$inject = ["$element", "$$mdDateUtil", "$mdDateLocale"]; + self.$mdUtil.nextTick(function() { + self.currentView = newView; - /** Generate and append the content for this month to the directive element. */ - CalendarMonthCtrl.prototype.generateContent = function() { - var calendarCtrl = this.calendarCtrl; - var date = this.dateUtil.incrementMonths(calendarCtrl.firstRenderableDate, this.offset); + if (time) { + self.displayDate = angular.isDate(time) ? time : new Date(time); + } + }); + }; - this.$element.empty(); - this.$element.append(this.buildCalendarForMonth(date)); + /** + * Focus the cell corresponding to the given date. + * @param {Date} date The date to be focused. + */ + CalendarCtrl.prototype.focus = function(date) { + if (this.dateUtil.isValidDate(date)) { + var previousFocus = this.$element[0].querySelector('.md-focus'); + if (previousFocus) { + previousFocus.classList.remove(this.FOCUSED_DATE_CLASS); + } - if (this.focusAfterAppend) { - this.focusAfterAppend.classList.add(FOCUSED_DATE_CLASS); - this.focusAfterAppend.focus(); - this.focusAfterAppend = null; + var cellId = this.getDateId(date, this.currentView); + var cell = document.getElementById(cellId); + if (cell) { + cell.classList.add(this.FOCUSED_DATE_CLASS); + cell.focus(); + this.displayDate = date; + } + } else { + var rootElement = this.$element[0].querySelector('[ng-switch]'); + + if (rootElement) { + rootElement.focus(); + } } }; /** - * Creates a single cell to contain a date in the calendar with all appropriate - * attributes and classes added. If a date is given, the cell content will be set - * based on the date. - * @param {Date=} opt_date - * @returns {HTMLElement} + * Normalizes the key event into an action name. The action will be broadcast + * to the child controllers. + * @param {KeyboardEvent} event + * @returns {String} The action that should be taken, or null if the key + * does not match a calendar shortcut. */ - CalendarMonthCtrl.prototype.buildDateCell = function(opt_date) { - var calendarCtrl = this.calendarCtrl; + CalendarCtrl.prototype.getActionFromKeyEvent = function(event) { + var keyCode = this.keyCode; - // 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'); + switch (event.which) { + case keyCode.ENTER: return 'select'; - if (opt_date) { - cell.setAttribute('tabindex', '-1'); - cell.setAttribute('aria-label', this.dateLocale.longDateFormatter(opt_date)); - cell.id = calendarCtrl.getDateId(opt_date); + case keyCode.RIGHT_ARROW: return 'move-right'; + case keyCode.LEFT_ARROW: return 'move-left'; - // Use `data-timestamp` attribute because IE10 does not support the `dataset` property. - cell.setAttribute('data-timestamp', opt_date.getTime()); + // TODO(crisbeto): Might want to reconsider using metaKey, because it maps + // to the "Windows" key on PC, which opens the start menu or resizes the browser. + case keyCode.DOWN_ARROW: return event.metaKey ? 'move-page-down' : 'move-row-down'; + case keyCode.UP_ARROW: return event.metaKey ? 'move-page-up' : 'move-row-up'; - // 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); - } + case keyCode.PAGE_DOWN: return 'move-page-down'; + case keyCode.PAGE_UP: return 'move-page-up'; - if (this.dateUtil.isValidDate(calendarCtrl.selectedDate) && - this.dateUtil.isSameDay(opt_date, calendarCtrl.selectedDate)) { - cell.classList.add(SELECTED_DATE_CLASS); - cell.setAttribute('aria-selected', 'true'); - } + case keyCode.HOME: return 'start'; + case keyCode.END: return 'end'; - var cellText = this.dateLocale.dates[opt_date.getDate()]; + default: return null; + } + }; - 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; + /** + * Handles a key event in the calendar with the appropriate action. The action will either + * be to select the focused date or to navigate to focus a new date. + * @param {KeyboardEvent} event + */ + CalendarCtrl.prototype.handleKeyEvent = function(event) { + var self = this; - cell.addEventListener('click', calendarCtrl.cellClickHandler); + this.$scope.$apply(function() { + // Capture escape and emit back up so that a wrapping component + // (such as a date-picker) can decide to close. + if (event.which == self.keyCode.ESCAPE || event.which == self.keyCode.TAB) { + self.$scope.$emit('md-calendar-close'); - if (calendarCtrl.focusDate && this.dateUtil.isSameDay(opt_date, calendarCtrl.focusDate)) { - this.focusAfterAppend = cell; + if (event.which == self.keyCode.TAB) { + event.preventDefault(); } - } else { - cell.classList.add('md-calendar-date-disabled'); - cell.textContent = cellText; + + return; } - } - return cell; + // Broadcast the action that any child controllers should take. + var action = self.getActionFromKeyEvent(event); + if (action) { + event.preventDefault(); + event.stopPropagation(); + self.$scope.$broadcast('md-calendar-parent-action', action); + } + }); }; - - /** - * Check whether date is in range and enabled - * @param {Date=} opt_date - * @return {boolean} Whether the date is enabled. - */ - CalendarMonthCtrl.prototype.isDateEnabled = function(opt_date) { - return this.dateUtil.isDateWithinRange(opt_date, - this.calendarCtrl.minDate, this.calendarCtrl.maxDate) && - (!angular.isFunction(this.calendarCtrl.dateFilter) - || this.calendarCtrl.dateFilter(opt_date)); - } - + /** - * Builds a `tr` element for the calendar grid. - * @param rowNumber The week number within the month. - * @returns {HTMLElement} + * Hides the vertical scrollbar on the calendar scroller of a child controller by + * setting the width on the calendar scroller and the `overflow: hidden` wrapper + * around the scroller, and then setting a padding-right on the scroller equal + * to the width of the browser's scrollbar. + * + * This will cause a reflow. + * + * @param {object} childCtrl The child controller whose scrollbar should be hidden. */ - CalendarMonthCtrl.prototype.buildDateRow = function(rowNumber) { - var row = document.createElement('tr'); - row.setAttribute('role', 'row'); + CalendarCtrl.prototype.hideVerticalScrollbar = function(childCtrl) { + var self = this; + var element = childCtrl.$element[0]; + var scrollMask = element.querySelector('.md-calendar-scroll-mask'); - // 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)); + if (self.width > 0) { + setWidth(); + } else { + self.$$rAF(function() { + var scroller = childCtrl.calendarScroller; - return row; + self.scrollbarWidth = scroller.offsetWidth - scroller.clientWidth; + self.width = element.querySelector('table').offsetWidth; + setWidth(); + }); + } + + function setWidth() { + var width = self.width || FALLBACK_WIDTH; + var scrollbarWidth = self.scrollbarWidth; + var scroller = childCtrl.calendarScroller; + + scrollMask.style.width = width + 'px'; + scroller.style.width = (width + scrollbarWidth) + 'px'; + scroller.style.paddingRight = scrollbarWidth + 'px'; + } }; /** - * Builds the content for the given date's month. - * @param {Date=} opt_dateInMonth - * @returns {DocumentFragment} A document fragment containing the elements. + * Gets an identifier for a date unique to the calendar instance for internal + * purposes. Not to be displayed. + * @param {Date} date The date for which the id is being generated + * @param {string} namespace Namespace for the id. (month, year etc.) + * @returns {string} */ - CalendarMonthCtrl.prototype.buildCalendarForMonth = function(opt_dateInMonth) { - var date = this.dateUtil.isValidDate(opt_dateInMonth) ? opt_dateInMonth : new Date(); + CalendarCtrl.prototype.getDateId = function(date, namespace) { + if (!namespace) { + throw new Error('A namespace for the date id has to be specified.'); + } - var firstDayOfMonth = this.dateUtil.getFirstDateOfMonth(date); - var firstDayOfTheWeek = this.getLocaleDay_(firstDayOfMonth); - var numberOfDaysInMonth = this.dateUtil.getNumberOfDaysInMonth(date); + return [ + 'md', + this.id, + namespace, + date.getFullYear(), + date.getMonth(), + date.getDate() + ].join('-'); + }; +})(); - // Store rows for the month in a document fragment so that we can append them all at once. - var monthBody = document.createDocumentFragment(); +})(); +(function(){ +"use strict"; - var rowNumber = 1; - var row = this.buildDateRow(rowNumber); - monthBody.appendChild(row); +(function() { + 'use strict'; - // 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; + angular.module('material.components.datepicker') + .directive('mdCalendarMonth', calendarDirective); - // 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'); + /** + * Height of one calendar month tbody. This must be made known to the virtual-repeat and is + * subsequently used for scrolling to specific months. + */ + var TBODY_HEIGHT = 265; - var monthLabelRow = this.buildDateRow(); - monthLabelRow.appendChild(monthLabelCell); - monthBody.insertBefore(monthLabelRow, row); + /** + * Height of a calendar month with a single row. This is needed to calculate the offset for + * rendering an extra month in virtual-repeat that only contains one row. + */ + var TBODY_SINGLE_ROW_HEIGHT = 45; - if (isFinalMonth) { - return monthBody; + /** Private directive that represents a list of months inside the calendar. */ + function calendarDirective() { + return { + template: + '' + + '
' + + '' + + '' + + '' + + '
' + + '
' + + '
', + require: ['^^mdCalendar', 'mdCalendarMonth'], + controller: CalendarMonthCtrl, + controllerAs: 'monthCtrl', + bindToController: true, + link: function(scope, element, attrs, controllers) { + var calendarCtrl = controllers[0]; + var monthCtrl = controllers[1]; + monthCtrl.initialize(calendarCtrl); } - } else { - blankCellOffset = 2; - monthLabelCell.setAttribute('colspan', '2'); - row.appendChild(monthLabelCell); + }; + } + + /** + * Controller for the calendar month component. + * @ngInject @constructor + */ + function CalendarMonthCtrl($element, $scope, $animate, $q, + $$mdDateUtil, $mdDateLocale) { + + /** @final {!angular.JQLite} */ + this.$element = $element; + + /** @final {!angular.Scope} */ + this.$scope = $scope; + + /** @final {!angular.$animate} */ + this.$animate = $animate; + + /** @final {!angular.$q} */ + this.$q = $q; + + /** @final */ + this.dateUtil = $$mdDateUtil; + + /** @final */ + this.dateLocale = $mdDateLocale; + + /** @final {HTMLElement} */ + this.calendarScroller = $element[0].querySelector('.md-virtual-repeat-scroller'); + + /** @type {Date} */ + this.firstRenderableDate = null; + + /** @type {boolean} */ + this.isInitialized = false; + + /** @type {boolean} */ + this.isMonthTransitionInProgress = false; + + var self = this; + + /** + * Handles a click event on a date cell. + * Created here so that every cell can use the same function instance. + * @this {HTMLTableCellElement} The cell that was clicked. + */ + this.cellClickHandler = function() { + var timestamp = $$mdDateUtil.getTimestampFromNode(this); + self.$scope.$apply(function() { + self.calendarCtrl.setNgModelValue(timestamp); + }); + }; + + /** + * Handles click events on the month headers. Switches + * the calendar to the year view. + * @this {HTMLTableCellElement} The cell that was clicked. + */ + this.headerClickHandler = function() { + self.calendarCtrl.setCurrentView('year', $$mdDateUtil.getTimestampFromNode(this)); + }; + } + CalendarMonthCtrl.$inject = ["$element", "$scope", "$animate", "$q", "$$mdDateUtil", "$mdDateLocale"]; + + /*** Initialization ***/ + + /** + * Initialize the controller by saving a reference to the calendar and + * setting up the object that will be iterated by the virtual repeater. + */ + CalendarMonthCtrl.prototype.initialize = function(calendarCtrl) { + var minDate = calendarCtrl.minDate; + var maxDate = calendarCtrl.maxDate; + this.calendarCtrl = calendarCtrl; + + /** + * Dummy array-like object for virtual-repeat to iterate over. The length is the total + * number of months that can be viewed. This is shorter than ideal because of (potential) + * Firefox bug https://bugzilla.mozilla.org/show_bug.cgi?id=1181658. + */ + this.items = { length: 2000 }; + + if (maxDate && minDate) { + // Limit the number of months if min and max dates are set. + var numMonths = this.dateUtil.getMonthDistance(minDate, maxDate) + 1; + numMonths = Math.max(numMonths, 1); + // Add an additional month as the final dummy month for rendering purposes. + numMonths += 1; + this.items.length = numMonths; } - // 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()); + this.firstRenderableDate = this.dateUtil.incrementMonths(calendarCtrl.today, -this.items.length / 2); + + if (minDate && minDate > this.firstRenderableDate) { + this.firstRenderableDate = minDate; + } else if (maxDate) { + // Calculate the difference between the start date and max date. + // Subtract 1 because it's an inclusive difference and 1 for the final dummy month. + var monthDifference = this.items.length - 2; + this.firstRenderableDate = this.dateUtil.incrementMonths(maxDate, -(this.items.length - 2)); } - // 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; + this.attachScopeListeners(); + + // Fire the initial render, since we might have missed it the first time it fired. + calendarCtrl.ngModelCtrl && calendarCtrl.ngModelCtrl.$render(); + }; + + /** + * Gets the "index" of the currently selected date as it would be in the virtual-repeat. + * @returns {number} + */ + CalendarMonthCtrl.prototype.getSelectedMonthIndex = function() { + var calendarCtrl = this.calendarCtrl; + return this.dateUtil.getMonthDistance(this.firstRenderableDate, + calendarCtrl.displayDate || calendarCtrl.selectedDate || calendarCtrl.today); + }; + + /** + * Change the selected date in the calendar (ngModel value has already been changed). + * @param {Date} date + */ + CalendarMonthCtrl.prototype.changeSelectedDate = function(date) { + var self = this; + var calendarCtrl = self.calendarCtrl; + var previousSelectedDate = calendarCtrl.selectedDate; + calendarCtrl.selectedDate = date; + + this.changeDisplayDate(date).then(function() { + var selectedDateClass = calendarCtrl.SELECTED_DATE_CLASS; + var namespace = 'month'; + + // Remove the selected class from the previously selected date, if any. + if (previousSelectedDate) { + var prevDateCell = document.getElementById(calendarCtrl.getDateId(previousSelectedDate, namespace)); + if (prevDateCell) { + prevDateCell.classList.remove(selectedDateClass); + prevDateCell.setAttribute('aria-selected', 'false'); } - dayOfWeek = 0; - rowNumber++; - row = this.buildDateRow(rowNumber); - monthBody.appendChild(row); } - iterationDate.setDate(d); - var cell = this.buildDateCell(iterationDate); - row.appendChild(cell); + // Apply the select class to the new selected date if it is set. + if (date) { + var dateCell = document.getElementById(calendarCtrl.getDateId(date, namespace)); + if (dateCell) { + dateCell.classList.add(selectedDateClass); + dateCell.setAttribute('aria-selected', 'true'); + } + } + }); + }; - dayOfWeek++; + /** + * Change the date that is being shown in the calendar. If the given date is in a different + * month, the displayed month will be transitioned. + * @param {Date} date + */ + CalendarMonthCtrl.prototype.changeDisplayDate = function(date) { + // Initialization is deferred until this function is called because we want to reflect + // the starting value of ngModel. + if (!this.isInitialized) { + this.buildWeekHeader(); + this.calendarCtrl.hideVerticalScrollbar(this); + this.isInitialized = true; + return this.$q.when(); } - // Ensure that the last row of the month has 7 cells. - while (row.childNodes.length < 7) { - row.appendChild(this.buildDateCell()); + // If trying to show an invalid date or a transition is in progress, do nothing. + if (!this.dateUtil.isValidDate(date) || this.isMonthTransitionInProgress) { + return this.$q.when(); } - // Ensure that all months have 6 rows. This is necessary for now because the virtual-repeat - // requires that all items have exactly the same height. - while (monthBody.childNodes.length < 6) { - var whitespaceRow = this.buildDateRow(); - for (var i = 0; i < 7; i++) { - whitespaceRow.appendChild(this.buildDateCell()); - } - monthBody.appendChild(whitespaceRow); - } + this.isMonthTransitionInProgress = true; + var animationPromise = this.animateDateChange(date); - return monthBody; + this.calendarCtrl.displayDate = date; + + var self = this; + animationPromise.then(function() { + self.isMonthTransitionInProgress = false; + }); + + return animationPromise; }; /** - * Gets the day-of-the-week index for a date for the current locale. - * @private + * Animates the transition from the calendar's current month to the given month. * @param {Date} date - * @returns {number} The column index of the date in the calendar. + * @returns {angular.$q.Promise} The animation promise. + */ + CalendarMonthCtrl.prototype.animateDateChange = function(date) { + if (this.dateUtil.isValidDate(date)) { + var monthDistance = this.dateUtil.getMonthDistance(this.firstRenderableDate, date); + this.calendarScroller.scrollTop = monthDistance * TBODY_HEIGHT; + } + + return this.$q.when(); + }; + + /** + * Builds and appends a day-of-the-week header to the calendar. + * This should only need to be called once during initialization. + */ + CalendarMonthCtrl.prototype.buildWeekHeader = function() { + var firstDayOfWeek = this.dateLocale.firstDayOfWeek; + var shortDays = this.dateLocale.shortDays; + + var row = document.createElement('tr'); + for (var i = 0; i < 7; i++) { + var th = document.createElement('th'); + th.textContent = shortDays[(i + firstDayOfWeek) % 7]; + row.appendChild(th); + } + + this.$element.find('thead').append(row); + }; + + /** + * Attaches listeners for the scope events that are broadcast by the calendar. + */ + CalendarMonthCtrl.prototype.attachScopeListeners = function() { + var self = this; + + self.$scope.$on('md-calendar-parent-changed', function(event, value) { + self.changeSelectedDate(value); + }); + + self.$scope.$on('md-calendar-parent-action', angular.bind(this, this.handleKeyEvent)); + }; + + /** + * Handles the month-specific keyboard interactions. + * @param {Object} event Scope event object passed by the calendar. + * @param {String} action Action, corresponding to the key that was pressed. */ - CalendarMonthCtrl.prototype.getLocaleDay_ = function(date) { - return (date.getDay() + (7 - this.dateLocale.firstDayOfWeek)) % 7 + CalendarMonthCtrl.prototype.handleKeyEvent = function(event, action) { + var calendarCtrl = this.calendarCtrl; + var displayDate = calendarCtrl.displayDate; + + if (action === 'select') { + calendarCtrl.setNgModelValue(displayDate); + } else { + var date = null; + var dateUtil = this.dateUtil; + + switch (action) { + case 'move-right': date = dateUtil.incrementDays(displayDate, 1); break; + case 'move-left': date = dateUtil.incrementDays(displayDate, -1); break; + + case 'move-page-down': date = dateUtil.incrementMonths(displayDate, 1); break; + case 'move-page-up': date = dateUtil.incrementMonths(displayDate, -1); break; + + case 'move-row-down': date = dateUtil.incrementDays(displayDate, 7); break; + case 'move-row-up': date = dateUtil.incrementDays(displayDate, -7); break; + + case 'start': date = dateUtil.getFirstDateOfMonth(displayDate); break; + case 'end': date = dateUtil.getLastDateOfMonth(displayDate); break; + } + + if (date) { + date = this.dateUtil.clampDate(date, calendarCtrl.minDate, calendarCtrl.maxDate); + + this.changeDisplayDate(date).then(function() { + calendarCtrl.focus(date); + }); + } + } }; })(); @@ -7812,278 +8423,294 @@ function iosScrollFix(node) { (function() { 'use strict'; + angular.module('material.components.datepicker') + .directive('mdCalendarMonthBody', mdCalendarMonthBodyDirective); + /** - * @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'; - * - * }); - * - * + * Private directive consumed by md-calendar-month. Having this directive lets the calender use + * md-virtual-repeat and also cleanly separates the month DOM construction functions from + * the rest of the calendar controller logic. */ + function mdCalendarMonthBodyDirective() { + return { + require: ['^^mdCalendar', '^^mdCalendarMonth', 'mdCalendarMonthBody'], + scope: { offset: '=mdMonthOffset' }, + controller: CalendarMonthBodyCtrl, + controllerAs: 'mdMonthBodyCtrl', + bindToController: true, + link: function(scope, element, attrs, controllers) { + var calendarCtrl = controllers[0]; + var monthCtrl = controllers[1]; + var monthBodyCtrl = controllers[2]; - angular.module('material.components.datepicker').config(["$provide", function($provide) { - // TODO(jelbourn): Assert provided values are correctly formatted. Need assertions. + monthBodyCtrl.calendarCtrl = calendarCtrl; + monthBodyCtrl.monthCtrl = monthCtrl; + monthBodyCtrl.generateContent(); - /** @constructor */ - function DateLocaleProvider() { - /** Array of full month names. E.g., ['January', 'Febuary', ...] */ - this.months = null; + // The virtual-repeat re-uses the same DOM elements, so there are only a limited number + // of repeated items that are linked, and then those elements have their bindings updataed. + // Since the months are not generated by bindings, we simply regenerate the entire thing + // when the binding (offset) changes. + scope.$watch(function() { return monthBodyCtrl.offset; }, function(offset, oldOffset) { + if (offset != oldOffset) { + monthBodyCtrl.generateContent(); + } + }); + } + }; + } - /** Array of abbreviated month names. E.g., ['Jan', 'Feb', ...] */ - this.shortMonths = null; + /** + * Controller for a single calendar month. + * @ngInject @constructor + */ + function CalendarMonthBodyCtrl($element, $$mdDateUtil, $mdDateLocale) { + /** @final {!angular.JQLite} */ + this.$element = $element; - /** Array of full day of the week names. E.g., ['Monday', 'Tuesday', ...] */ - this.days = null; + /** @final */ + this.dateUtil = $$mdDateUtil; - /** Array of abbreviated dat of the week names. E.g., ['M', 'T', ...] */ - this.shortDays = null; + /** @final */ + this.dateLocale = $mdDateLocale; - /** Array of dates of a month (1 - 31). Characters might be different in some locales. */ - this.dates = null; + /** @type {Object} Reference to the month view. */ + this.monthCtrl = null; - /** Index of the first day of the week. 0 = Sunday, 1 = Monday, etc. */ - this.firstDayOfWeek = 0; + /** @type {Object} Reference to the calendar. */ + this.calendarCtrl = null; - /** - * Function that converts the date portion of a Date to a string. - * @type {(function(Date): string)} - */ - this.formatDate = null; + /** + * Number of months from the start of the month "items" that the currently rendered month + * occurs. Set via angular data binding. + * @type {number} + */ + this.offset = null; - /** - * Function that converts a date string to a Date object (the date portion) - * @type {function(string): Date} - */ - this.parseDate = null; + /** + * Date cell to focus after appending the month to the document. + * @type {HTMLElement} + */ + this.focusAfterAppend = null; + } + CalendarMonthBodyCtrl.$inject = ["$element", "$$mdDateUtil", "$mdDateLocale"]; - /** - * Function that formats a Date into a month header string. - * @type {function(Date): string} - */ - this.monthHeaderFormatter = null; + /** Generate and append the content for this month to the directive element. */ + CalendarMonthBodyCtrl.prototype.generateContent = function() { + var date = this.dateUtil.incrementMonths(this.monthCtrl.firstRenderableDate, this.offset); - /** - * Function that formats a week number into a label for the week. - * @type {function(number): string} - */ - this.weekNumberFormatter = null; + this.$element.empty(); + this.$element.append(this.buildCalendarForMonth(date)); - /** - * 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; + if (this.focusAfterAppend) { + this.focusAfterAppend.classList.add(this.calendarCtrl.FOCUSED_DATE_CLASS); + this.focusAfterAppend.focus(); + this.focusAfterAppend = null; + } + }; - /** - * ARIA label for the calendar "dialog" used in the datepicker. - * @type {string} - */ - this.msgCalendar = ''; + /** + * Creates a single cell to contain a date in the calendar with all appropriate + * attributes and classes added. If a date is given, the cell content will be set + * based on the date. + * @param {Date=} opt_date + * @returns {HTMLElement} + */ + CalendarMonthBodyCtrl.prototype.buildDateCell = function(opt_date) { + var monthCtrl = this.monthCtrl; + var calendarCtrl = this.calendarCtrl; - /** - * ARIA label for the datepicker's "Open calendar" buttons. - * @type {string} - */ - this.msgOpenCalendar = ''; - } + // 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'); - /** - * 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 ''; - } + if (opt_date) { + cell.setAttribute('tabindex', '-1'); + cell.setAttribute('aria-label', this.dateLocale.longDateFormatter(opt_date)); + cell.id = calendarCtrl.getDateId(opt_date, 'month'); - // 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); - } + // Use `data-timestamp` attribute because IE10 does not support the `dataset` property. + cell.setAttribute('data-timestamp', opt_date.getTime()); - return formatDate.toLocaleDateString(); + // 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(calendarCtrl.TODAY_CLASS); } - /** - * Default string-to-date parsing function. - * @param {string} dateString - * @returns {!Date} - */ - function defaultParseDate(dateString) { - return new Date(dateString); + if (this.dateUtil.isValidDate(calendarCtrl.selectedDate) && + this.dateUtil.isSameDay(opt_date, calendarCtrl.selectedDate)) { + cell.classList.add(calendarCtrl.SELECTED_DATE_CLASS); + cell.setAttribute('aria-selected', 'true'); } - /** - * 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(); + var cellText = this.dateLocale.dates[opt_date.getDate()]; - // 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); - } + if (this.isDateEnabled(opt_date)) { + // Add a indicator for select, hover, and focus states. + var selectionIndicator = document.createElement('span'); + selectionIndicator.classList.add('md-calendar-date-selection-indicator'); + selectionIndicator.textContent = cellText; + cell.appendChild(selectionIndicator); + cell.addEventListener('click', monthCtrl.cellClickHandler); - /** - * 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(); + if (calendarCtrl.displayDate && this.dateUtil.isSameDay(opt_date, calendarCtrl.displayDate)) { + this.focusAfterAppend = cell; + } + } else { + cell.classList.add('md-calendar-date-disabled'); + cell.textContent = cellText; } + } - /** - * Default week number formatter. - * @param number - * @returns {string} - */ - function defaultWeekNumberFormatter(number) { - return 'Week ' + number; - } + return cell; + }; - /** - * 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(' '); + /** + * Check whether date is in range and enabled + * @param {Date=} opt_date + * @return {boolean} Whether the date is enabled. + */ + CalendarMonthBodyCtrl.prototype.isDateEnabled = function(opt_date) { + return this.dateUtil.isDateWithinRange(opt_date, + this.calendarCtrl.minDate, this.calendarCtrl.maxDate) && + (!angular.isFunction(this.calendarCtrl.dateFilter) + || this.calendarCtrl.dateFilter(opt_date)); + }; + + /** + * Builds a `tr` element for the calendar grid. + * @param rowNumber The week number within the month. + * @returns {HTMLElement} + */ + CalendarMonthBodyCtrl.prototype.buildDateRow = function(rowNumber) { + var row = document.createElement('tr'); + row.setAttribute('role', 'row'); + + // Because of an NVDA bug (with Firefox), the row needs an aria-label in order + // to prevent the entire row being read aloud when the user moves between rows. + // See http://community.nvda-project.org/ticket/4643. + row.setAttribute('aria-label', this.dateLocale.weekNumberFormatter(rowNumber)); + + return row; + }; + + /** + * Builds the content for the given date's month. + * @param {Date=} opt_dateInMonth + * @returns {DocumentFragment} A document fragment containing the elements. + */ + CalendarMonthBodyCtrl.prototype.buildCalendarForMonth = function(opt_dateInMonth) { + var date = this.dateUtil.isValidDate(opt_dateInMonth) ? opt_dateInMonth : new Date(); + + var firstDayOfMonth = this.dateUtil.getFirstDateOfMonth(date); + var firstDayOfTheWeek = this.getLocaleDay_(firstDayOfMonth); + var numberOfDaysInMonth = this.dateUtil.getNumberOfDaysInMonth(date); + + // Store rows for the month in a document fragment so that we can append them all at once. + var monthBody = document.createDocumentFragment(); + + var rowNumber = 1; + var row = this.buildDateRow(rowNumber); + monthBody.appendChild(row); + + // If this is the final month in the list of items, only the first week should render, + // so we should return immediately after the first row is complete and has been + // attached to the body. + var isFinalMonth = this.offset === this.monthCtrl.items.length - 1; + + // Add a label for the month. If the month starts on a Sun/Mon/Tues, the month label + // goes on a row above the first of the month. Otherwise, the month label takes up the first + // two cells of the first row. + var blankCellOffset = 0; + var monthLabelCell = document.createElement('td'); + monthLabelCell.textContent = this.dateLocale.monthHeaderFormatter(date); + 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'); + } else { + monthLabelCell.addEventListener('click', this.monthCtrl.headerClickHandler); + monthLabelCell.setAttribute('data-timestamp', firstDayOfMonth.getTime()); + monthLabelCell.setAttribute('aria-label', this.dateLocale.monthFormatter(date)); + } + + if (firstDayOfTheWeek <= 2) { + monthLabelCell.setAttribute('colspan', '7'); + + var monthLabelRow = this.buildDateRow(); + monthLabelRow.appendChild(monthLabelCell); + monthBody.insertBefore(monthLabelRow, row); + + if (isFinalMonth) { + return monthBody; } + } else { + blankCellOffset = 2; + monthLabelCell.setAttribute('colspan', '2'); + row.appendChild(monthLabelCell); + } - // 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]; - }); + // 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()); + } - // The default dates are simply the numbers 1 through 31. - var defaultDates = Array(32); - for (var i = 1; i <= 31; i++) { - defaultDates[i] = i; + // 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); } - // Default ARIA messages are in English (US). - var defaultMsgCalendar = 'Calendar'; - var defaultMsgOpenCalendar = 'Open calendar'; + iterationDate.setDate(d); + var cell = this.buildDateCell(iterationDate); + row.appendChild(cell); - 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 - }; + dayOfWeek++; + } - return service; - }; - DateLocaleProvider.prototype.$get.$inject = ["$locale"]; + // Ensure that the last row of the month has 7 cells. + while (row.childNodes.length < 7) { + row.appendChild(this.buildDateCell()); + } - $provide.provider('$mdDateLocale', new DateLocaleProvider()); - }]); + // 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 j = 0; j < 7; j++) { + whitespaceRow.appendChild(this.buildDateCell()); + } + monthBody.appendChild(whitespaceRow); + } + + return monthBody; + }; + + /** + * Gets the day-of-the-week index for a date for the current locale. + * @private + * @param {Date} date + * @returns {number} The column index of the date in the calendar. + */ + CalendarMonthBodyCtrl.prototype.getLocaleDay_ = function(date) { + return (date.getDay() + (7 - this.dateLocale.firstDayOfWeek)) % 7; + }; })(); })(); @@ -8093,631 +8720,423 @@ function iosScrollFix(node) { (function() { 'use strict'; - // POST RELEASE - // TODO(jelbourn): Demo that uses moment.js - // TODO(jelbourn): make sure this plays well with validation and ngMessages. - // TODO(jelbourn): calendar pane doesn't open up outside of visible viewport. - // TODO(jelbourn): forward more attributes to the internal input (required, autofocus, etc.) - // TODO(jelbourn): something better for mobile (calendar panel takes up entire screen?) - // TODO(jelbourn): input behavior (masking? auto-complete?) - // TODO(jelbourn): UTC mode - // TODO(jelbourn): RTL - - angular.module('material.components.datepicker') - .directive('mdDatepicker', datePickerDirective); + .directive('mdCalendarYear', calendarDirective); /** - * @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 - * - * - * - * + * Height of one calendar year tbody. This must be made known to the virtual-repeat and is + * subsequently used for scrolling to specific years. */ - function datePickerDirective() { + var TBODY_HEIGHT = 88; + + /** Private component, representing a list of years in the calendar. */ + function calendarDirective() { return { template: - // Buttons are not in the tab order because users can open the calendar via keyboard - // interaction on the text input, and multiple tab stops for one component (picker) - // may be confusing. - '' + - '
' + - '' + - '' + - '
' + - '
' + - '
' + - - // This pane will be detached from here and re-attached to the document body. - '
' + - '
' + - '
' + - '
' + - '
' + - '' + - '' + - '
' + - '
', - require: ['ngModel', 'mdDatepicker', '?^mdInputContainer'], - scope: { - minDate: '=mdMinDate', - maxDate: '=mdMaxDate', - placeholder: '@mdPlaceholder', - dateFilter: '=mdDateFilter' - }, - controller: DatePickerCtrl, - controllerAs: 'ctrl', - bindToController: true, - link: function(scope, element, attr, controllers) { - var ngModelCtrl = controllers[0]; - var mdDatePickerCtrl = controllers[1]; - - var mdInputContainer = controllers[2]; - if (mdInputContainer) { - throw Error('md-datepicker should not be placed inside md-input-container.'); - } - - mdDatePickerCtrl.configureNgModel(ngModelCtrl); - } - }; - } - - /** Additional offset for the input's `size` attribute, which is updated based on its content. */ - var EXTRA_INPUT_SIZE = 3; - - /** Class applied to the container if the date is invalid. */ - var INVALID_CLASS = 'md-datepicker-invalid'; - - /** Default time in ms to debounce input event by. */ - var DEFAULT_DEBOUNCE_INTERVAL = 500; - - /** - * Height of the calendar pane used to check if the pane is going outside the boundary of - * the viewport. See calendar.scss for how $md-calendar-height is computed; an extra 20px is - * also added to space the pane away from the exact edge of the screen. - * - * This is computed statically now, but can be changed to be measured if the circumstances - * of calendar sizing are changed. - */ - var CALENDAR_PANE_HEIGHT = 368; - - /** - * Width of the calendar pane used to check if the pane is going outside the boundary of - * the viewport. See calendar.scss for how $md-calendar-width is computed; an extra 20px is - * also added to space the pane away from the exact edge of the screen. - * - * This is computed statically now, but can be changed to be measured if the circumstances - * of calendar sizing are changed. - */ - var CALENDAR_PANE_WIDTH = 360; + '
' + + '' + + '' + + '' + + '
' + + '
' + + '
', + require: ['^^mdCalendar', 'mdCalendarYear'], + controller: CalendarYearCtrl, + controllerAs: 'yearCtrl', + bindToController: true, + link: function(scope, element, attrs, controllers) { + var calendarCtrl = controllers[0]; + var yearCtrl = controllers[1]; + yearCtrl.initialize(calendarCtrl); + } + }; + } /** - * Controller for md-datepicker. - * + * Controller for the mdCalendar component. * @ngInject @constructor */ - function DatePickerCtrl($scope, $element, $attrs, $compile, $timeout, $window, - $mdConstant, $mdTheming, $mdUtil, $mdDateLocale, $$mdDateUtil, $$rAF) { - /** @final */ - this.$compile = $compile; + function CalendarYearCtrl($element, $scope, $animate, $q, $$mdDateUtil, $timeout) { - /** @final */ - this.$timeout = $timeout; + /** @final {!angular.JQLite} */ + this.$element = $element; - /** @final */ - this.$window = $window; + /** @final {!angular.Scope} */ + this.$scope = $scope; - /** @final */ - this.dateLocale = $mdDateLocale; + /** @final {!angular.$animate} */ + this.$animate = $animate; - /** @final */ - this.dateUtil = $$mdDateUtil; + /** @final {!angular.$q} */ + this.$q = $q; /** @final */ - this.$mdConstant = $mdConstant; - - /* @final */ - this.$mdUtil = $mdUtil; + this.dateUtil = $$mdDateUtil; /** @final */ - this.$$rAF = $$rAF; - - /** - * The root document element. This is used for attaching a top-level click handler to - * close the calendar panel when a click outside said panel occurs. We use `documentElement` - * instead of body because, when scrolling is disabled, some browsers consider the body element - * to be completely off the screen and propagate events directly to the html element. - * @type {!angular.JQLite} - */ - this.documentElement = angular.element(document.documentElement); - - /** @type {!angular.NgModelController} */ - this.ngModelCtrl = null; - - /** @type {HTMLInputElement} */ - this.inputElement = $element[0].querySelector('input'); - - /** @final {!angular.JQLite} */ - this.ngInputElement = angular.element(this.inputElement); - - /** @type {HTMLElement} */ - this.inputContainer = $element[0].querySelector('.md-datepicker-input-container'); - - /** @type {HTMLElement} Floating calendar pane. */ - this.calendarPane = $element[0].querySelector('.md-datepicker-calendar-pane'); - - /** @type {HTMLElement} Calendar icon button. */ - this.calendarButton = $element[0].querySelector('.md-datepicker-button'); - - /** - * Element covering everything but the input in the top of the floating calendar pane. - * @type {HTMLElement} - */ - this.inputMask = $element[0].querySelector('.md-datepicker-input-mask-opaque'); - - /** @final {!angular.JQLite} */ - this.$element = $element; - - /** @final {!angular.Attributes} */ - this.$attrs = $attrs; + this.$timeout = $timeout; - /** @final {!angular.Scope} */ - this.$scope = $scope; + /** @final {HTMLElement} */ + this.calendarScroller = $element[0].querySelector('.md-virtual-repeat-scroller'); /** @type {Date} */ - this.date = null; + this.firstRenderableDate = null; /** @type {boolean} */ - this.isFocused = false; + this.isInitialized = false; /** @type {boolean} */ - this.isDisabled; - this.setDisabled($element[0].disabled || angular.isString($attrs['disabled'])); + this.isMonthTransitionInProgress = false; - /** @type {boolean} Whether the date-picker's calendar pane is open. */ - this.isCalendarOpen = false; + var self = this; /** - * 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} + * Handles a click event on a date cell. + * Created here so that every cell can use the same function instance. + * @this {HTMLTableCellElement} The cell that was clicked. */ - this.calendarPaneOpenedFrom = null; + this.cellClickHandler = function() { + self.calendarCtrl.setCurrentView('month', $$mdDateUtil.getTimestampFromNode(this)); + }; + } + CalendarYearCtrl.$inject = ["$element", "$scope", "$animate", "$q", "$$mdDateUtil", "$timeout"]; - this.calendarPane.id = 'md-date-pane' + $mdUtil.nextUid(); + /** + * Initialize the controller by saving a reference to the calendar and + * setting up the object that will be iterated by the virtual repeater. + */ + CalendarYearCtrl.prototype.initialize = function(calendarCtrl) { + var minDate = calendarCtrl.minDate; + var maxDate = calendarCtrl.maxDate; + this.calendarCtrl = calendarCtrl; - $mdTheming($element); + /** + * Dummy array-like object for virtual-repeat to iterate over. The length is the total + * number of months that can be viewed. This is shorter than ideal because of (potential) + * Firefox bug https://bugzilla.mozilla.org/show_bug.cgi?id=1181658. + */ + this.items = { length: 400 }; - /** Pre-bound click handler is saved so that the event listener can be removed. */ - this.bodyClickHandler = angular.bind(this, this.handleBodyClick); + if (maxDate && minDate) { + // Limit the number of years if min and max dates are set. + var numYears = this.dateUtil.getYearDistance(minDate, maxDate) + 1; + this.items.length = Math.max(numYears, 1); + } - /** Pre-bound resize handler so that the event listener can be removed. */ - this.windowResizeHandler = $mdUtil.debounce(angular.bind(this, this.closeCalendarPane), 100); + this.firstRenderableDate = this.dateUtil.incrementYears(calendarCtrl.today, - (this.items.length / 2)); - // 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'); + if (minDate && minDate > this.firstRenderableDate) { + this.firstRenderableDate = minDate; + } else if (maxDate) { + // Calculate the year difference between the start date and max date. + // Subtract 1 because it's an inclusive difference. + this.firstRenderableDate = this.dateUtil.incrementMonths(maxDate, - (this.items.length - 1)); } - this.installPropertyInterceptors(); - this.attachChangeListeners(); - this.attachInteractionListeners(); + // Trigger an extra digest to ensure that the virtual repeater has updated. This + // is necessary, because the virtual repeater doesn't update the $index the first + // time around since the content isn't in place yet. The case, in which this is an + // issues, is when the repeater has less than a page of content (e.g. there's a min + // and max date). + if (minDate || maxDate) this.$timeout(); + this.attachScopeListeners(); - var self = this; - $scope.$on('$destroy', function() { - self.detachCalendarPane(); - }); - } - DatePickerCtrl.$inject = ["$scope", "$element", "$attrs", "$compile", "$timeout", "$window", "$mdConstant", "$mdTheming", "$mdUtil", "$mdDateLocale", "$$mdDateUtil", "$$rAF"]; + // Fire the initial render, since we might have missed it the first time it fired. + calendarCtrl.ngModelCtrl && calendarCtrl.ngModelCtrl.$render(); + }; /** - * Sets up the controller's reference to ngModelController. - * @param {!angular.NgModelController} ngModelCtrl + * Gets the "index" of the currently selected date as it would be in the virtual-repeat. + * @returns {number} */ - DatePickerCtrl.prototype.configureNgModel = function(ngModelCtrl) { - this.ngModelCtrl = ngModelCtrl; + CalendarYearCtrl.prototype.getFocusedYearIndex = function() { + var calendarCtrl = this.calendarCtrl; + return this.dateUtil.getYearDistance(this.firstRenderableDate, + calendarCtrl.displayDate || calendarCtrl.selectedDate || calendarCtrl.today); + }; - var self = this; - ngModelCtrl.$render = function() { - var value = self.ngModelCtrl.$viewValue; + /** + * Change the date that is highlighted in the calendar. + * @param {Date} date + */ + CalendarYearCtrl.prototype.changeDate = function(date) { + // Initialization is deferred until this function is called because we want to reflect + // the starting value of ngModel. + if (!this.isInitialized) { + this.calendarCtrl.hideVerticalScrollbar(this); + this.isInitialized = true; + return this.$q.when(); + } else if (this.dateUtil.isValidDate(date) && !this.isMonthTransitionInProgress) { + var self = this; + var animationPromise = this.animateDateChange(date); - if (value && !(value instanceof Date)) { - throw Error('The ng-model for md-datepicker must be a Date instance. ' + - 'Currently the model is a: ' + (typeof value)); - } + self.isMonthTransitionInProgress = true; + self.calendarCtrl.displayDate = date; - self.date = value; - self.inputElement.value = self.dateLocale.formatDate(value); - self.resizeInputElement(); - self.updateErrorState(); - }; + return animationPromise.then(function() { + self.isMonthTransitionInProgress = false; + }); + } }; /** - * 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. + * Animates the transition from the calendar's current month to the given month. + * @param {Date} date + * @returns {angular.$q.Promise} The animation promise. */ - DatePickerCtrl.prototype.attachChangeListeners = function() { - var self = this; - - self.$scope.$on('md-calendar-change', function(event, date) { - self.ngModelCtrl.$setViewValue(date); - self.date = date; - self.inputElement.value = self.dateLocale.formatDate(date); - self.closeCalendarPane(); - self.resizeInputElement(); - self.updateErrorState(); - }); + CalendarYearCtrl.prototype.animateDateChange = function(date) { + if (this.dateUtil.isValidDate(date)) { + var monthDistance = this.dateUtil.getYearDistance(this.firstRenderableDate, date); + this.calendarScroller.scrollTop = monthDistance * TBODY_HEIGHT; + } - 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)); + return this.$q.when(); }; - /** Attach event listeners for user interaction. */ - DatePickerCtrl.prototype.attachInteractionListeners = function() { - var self = this; - var $scope = this.$scope; - var keyCodes = this.$mdConstant.KEY_CODE; + /** + * Handles the year-view-specific keyboard interactions. + * @param {Object} event Scope event object passed by the calendar. + * @param {String} action Action, corresponding to the key that was pressed. + */ + CalendarYearCtrl.prototype.handleKeyEvent = function(event, action) { + var calendarCtrl = this.calendarCtrl; + var displayDate = calendarCtrl.displayDate; - // 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(); + if (action === 'select') { + this.changeDate(displayDate).then(function() { + calendarCtrl.setCurrentView('month', displayDate); + calendarCtrl.focus(displayDate); + }); + } else { + var date = null; + var dateUtil = this.dateUtil; + + switch (action) { + case 'move-right': date = dateUtil.incrementMonths(displayDate, 1); break; + case 'move-left': date = dateUtil.incrementMonths(displayDate, -1); break; + + case 'move-row-down': date = dateUtil.incrementMonths(displayDate, 6); break; + case 'move-row-up': date = dateUtil.incrementMonths(displayDate, -6); break; } - }); - $scope.$on('md-calendar-close', function() { - self.closeCalendarPane(); - }); + if (date) { + var min = calendarCtrl.minDate ? dateUtil.incrementMonths(dateUtil.getFirstDateOfMonth(calendarCtrl.minDate), 1) : null; + var max = calendarCtrl.maxDate ? dateUtil.getFirstDateOfMonth(calendarCtrl.maxDate) : null; + date = dateUtil.getFirstDateOfMonth(this.dateUtil.clampDate(date, min, max)); + + this.changeDate(date).then(function() { + calendarCtrl.focus(date); + }); + } + } }; /** - * Capture properties set to the date-picker and imperitively handle internal changes. - * This is done to avoid setting up additional $watches. + * Attaches listeners for the scope events that are broadcast by the calendar. */ - DatePickerCtrl.prototype.installPropertyInterceptors = function() { + CalendarYearCtrl.prototype.attachScopeListeners = 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; + self.$scope.$on('md-calendar-parent-changed', function(event, value) { + self.changeDate(value); + }); - if (scope) { - scope.$watch(this.$attrs['ngDisabled'], function(isDisabled) { - self.setDisabled(isDisabled); - }); - } - } - - Object.defineProperty(this, 'placeholder', { - get: function() { return self.inputElement.placeholder; }, - set: function(value) { self.inputElement.placeholder = value || ''; } - }); + self.$scope.$on('md-calendar-parent-action', angular.bind(self, self.handleKeyEvent)); }; +})(); + +})(); +(function(){ +"use strict"; + +(function() { + 'use strict'; + + angular.module('material.components.datepicker') + .directive('mdCalendarYearBody', mdCalendarYearDirective); /** - * Sets whether the date-picker is disabled. - * @param {boolean} isDisabled + * Private component, consumed by the md-calendar-year, which separates the DOM construction logic + * and allows for the year view to use md-virtual-repeat. */ - DatePickerCtrl.prototype.setDisabled = function(isDisabled) { - this.isDisabled = isDisabled; - this.inputElement.disabled = isDisabled; - this.calendarButton.disabled = isDisabled; - }; + function mdCalendarYearDirective() { + return { + require: ['^^mdCalendar', '^^mdCalendarYear', 'mdCalendarYearBody'], + scope: { offset: '=mdYearOffset' }, + controller: CalendarYearBodyCtrl, + controllerAs: 'mdYearBodyCtrl', + bindToController: true, + link: function(scope, element, attrs, controllers) { + var calendarCtrl = controllers[0]; + var yearCtrl = controllers[1]; + var yearBodyCtrl = controllers[2]; + + yearBodyCtrl.calendarCtrl = calendarCtrl; + yearBodyCtrl.yearCtrl = yearCtrl; + yearBodyCtrl.generateContent(); + + scope.$watch(function() { return yearBodyCtrl.offset; }, function(offset, oldOffset) { + if (offset != oldOffset) { + yearBodyCtrl.generateContent(); + } + }); + } + }; + } /** - * 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. + * Controller for a single year. + * @ngInject @constructor */ - DatePickerCtrl.prototype.updateErrorState = function(opt_date) { - var date = opt_date || this.date; - - // Clear any existing errors to get rid of anything that's no longer relevant. - this.clearErrorState(); + function CalendarYearBodyCtrl($element, $$mdDateUtil, $mdDateLocale) { + /** @final {!angular.JQLite} */ + this.$element = $element; - if (this.dateUtil.isValidDate(date)) { - // Force all dates to midnight in order to ignore the time portion. - date = this.dateUtil.createDateAtMidnight(date); + /** @final */ + this.dateUtil = $$mdDateUtil; - if (this.dateUtil.isValidDate(this.minDate)) { - var minDate = this.dateUtil.createDateAtMidnight(this.minDate); - this.ngModelCtrl.$setValidity('mindate', date >= minDate); - } + /** @final */ + this.dateLocale = $mdDateLocale; - 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); - } + /** @type {Object} Reference to the calendar. */ + this.calendarCtrl = null; - // TODO(jelbourn): Change this to classList.toggle when we stop using PhantomJS in unit tests - // because it doesn't conform to the DOMTokenList spec. - // See https://github.com/ariya/phantomjs/issues/12782. - if (!this.ngModelCtrl.$valid) { - this.inputContainer.classList.add(INVALID_CLASS); - } - }; + /** @type {Object} Reference to the year view. */ + this.yearCtrl = null; - /** 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); - }; + /** + * Number of months from the start of the month "items" that the currently rendered month + * occurs. Set via angular data binding. + * @type {number} + */ + this.offset = null; - /** 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; - }; + /** + * Date cell to focus after appending the month to the document. + * @type {HTMLElement} + */ + this.focusAfterAppend = null; + } + CalendarYearBodyCtrl.$inject = ["$element", "$$mdDateUtil", "$mdDateLocale"]; - /** - * 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); + /** Generate and append the content for this year to the directive element. */ + CalendarYearBodyCtrl.prototype.generateContent = function() { + var date = this.dateUtil.incrementYears(this.yearCtrl.firstRenderableDate, this.offset); - // 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) - ); + this.$element.empty(); + this.$element.append(this.buildCalendarForYear(date)); - // The datepicker's model is only updated when there is a valid input. - if (isValidInput) { - this.ngModelCtrl.$setViewValue(parsedDate); - this.date = parsedDate; + if (this.focusAfterAppend) { + this.focusAfterAppend.classList.add(this.calendarCtrl.FOCUSED_DATE_CLASS); + this.focusAfterAppend.focus(); + this.focusAfterAppend = null; } - - this.updateErrorState(parsedDate); }; - + /** - * Check whether date is in range and enabled - * @param {Date=} opt_date - * @return {boolean} Whether the date is enabled. + * Creates a single cell to contain a year in the calendar. + * @param {number} opt_year Four-digit year. + * @param {number} opt_month Zero-indexed month. + * @returns {HTMLElement} */ - DatePickerCtrl.prototype.isDateEnabled = function(opt_date) { - return this.dateUtil.isDateWithinRange(opt_date, this.minDate, this.maxDate) && - (!angular.isFunction(this.dateFilter) || this.dateFilter(opt_date)); - }; - - /** Position and attach the floating calendar to the document. */ - DatePickerCtrl.prototype.attachCalendarPane = function() { - var calendarPane = this.calendarPane; - calendarPane.style.transform = ''; - this.$element.addClass('md-datepicker-open'); - - var elementRect = this.inputContainer.getBoundingClientRect(); - var bodyRect = document.body.getBoundingClientRect(); - - // Check to see if the calendar pane would go off the screen. If so, adjust position - // accordingly to keep it within the viewport. - var paneTop = elementRect.top - bodyRect.top; - var paneLeft = elementRect.left - bodyRect.left; - - // If ng-material has disabled body scrolling (for example, if a dialog is open), - // then it's possible that the already-scrolled body has a negative top/left. In this case, - // we want to treat the "real" top as (0 - bodyRect.top). In a normal scrolling situation, - // though, the top of the viewport should just be the body's scroll position. - var viewportTop = (bodyRect.top < 0 && document.body.scrollTop == 0) ? - -bodyRect.top : - document.body.scrollTop; - - var viewportLeft = (bodyRect.left < 0 && document.body.scrollLeft == 0) ? - -bodyRect.left : - document.body.scrollLeft; + CalendarYearBodyCtrl.prototype.buildMonthCell = function(year, month) { + var calendarCtrl = this.calendarCtrl; + var yearCtrl = this.yearCtrl; + var cell = this.buildBlankCell(); - var viewportBottom = viewportTop + this.$window.innerHeight; - var viewportRight = viewportLeft + this.$window.innerWidth; + // Represent this month/year as a date. + var firstOfMonth = new Date(year, month, 1); + cell.setAttribute('aria-label', this.dateLocale.monthFormatter(firstOfMonth)); + cell.id = calendarCtrl.getDateId(firstOfMonth, 'year'); - // 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 + ')'; - } + // Use `data-timestamp` attribute because IE10 does not support the `dataset` property. + cell.setAttribute('data-timestamp', firstOfMonth.getTime()); - calendarPane.classList.add('md-datepicker-pos-adjusted'); + if (this.dateUtil.isSameMonthAndYear(firstOfMonth, calendarCtrl.today)) { + cell.classList.add(calendarCtrl.TODAY_CLASS); } - // 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'); + if (this.dateUtil.isValidDate(calendarCtrl.selectedDate) && + this.dateUtil.isSameMonthAndYear(firstOfMonth, calendarCtrl.selectedDate)) { + cell.classList.add(calendarCtrl.SELECTED_DATE_CLASS); + cell.setAttribute('aria-selected', 'true'); } - calendarPane.style.left = paneLeft + 'px'; - calendarPane.style.top = paneTop + 'px'; - document.body.appendChild(calendarPane); + var cellText = this.dateLocale.shortMonths[month]; - // 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'; + if (this.dateUtil.isDateWithinRange(firstOfMonth, + calendarCtrl.minDate, calendarCtrl.maxDate)) { + var selectionIndicator = document.createElement('span'); + selectionIndicator.classList.add('md-calendar-date-selection-indicator'); + selectionIndicator.textContent = cellText; + cell.appendChild(selectionIndicator); + cell.addEventListener('click', yearCtrl.cellClickHandler); - // Add CSS class after one frame to trigger open animation. - this.$$rAF(function() { - calendarPane.classList.add('md-pane-open'); - }); + if (calendarCtrl.displayDate && this.dateUtil.isSameMonthAndYear(firstOfMonth, calendarCtrl.displayDate)) { + this.focusAfterAppend = cell; + } + } else { + cell.classList.add('md-calendar-date-disabled'); + cell.textContent = cellText; + } + + return cell; }; - /** 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'); + /** + * Builds a blank cell. + * @return {HTMLTableCellElement} + */ + CalendarYearBodyCtrl.prototype.buildBlankCell = function() { + var cell = document.createElement('td'); + cell.tabIndex = -1; + cell.classList.add('md-calendar-date'); + cell.setAttribute('role', 'gridcell'); - 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); - } + cell.setAttribute('tabindex', '-1'); + return cell; }; /** - * Open the floating calendar pane. - * @param {Event} event + * Builds the content for the given year. + * @param {Date} date Date for which the content should be built. + * @returns {DocumentFragment} A document fragment containing the months within the year. */ - DatePickerCtrl.prototype.openCalendarPane = function(event) { - if (!this.isCalendarOpen && !this.isDisabled) { - this.isCalendarOpen = true; - this.calendarPaneOpenedFrom = event.target; - - // Because the calendar pane is attached directly to the body, it is possible that the - // rest of the component (input, etc) is in a different scrolling container, such as - // an md-content. This means that, if the container is scrolled, the pane would remain - // stationary. To remedy this, we disable scrolling while the calendar pane is open, which - // also matches the native behavior for things like `' + + '' + + '
' + + '
' + + '' + + + // This pane will be detached from here and re-attached to the document body. + '
' + + '
' + + '
' + + '
' + + '
' + + '' + + '' + + '
' + + '
', + require: ['ngModel', 'mdDatepicker', '?^mdInputContainer'], + scope: { + minDate: '=mdMinDate', + maxDate: '=mdMaxDate', + placeholder: '@mdPlaceholder', + dateFilter: '=mdDateFilter' + }, + controller: DatePickerCtrl, + controllerAs: 'ctrl', + bindToController: true, + link: function(scope, element, attr, controllers) { + var ngModelCtrl = controllers[0]; + var mdDatePickerCtrl = controllers[1]; + + var mdInputContainer = controllers[2]; + if (mdInputContainer) { + throw Error('md-datepicker should not be placed inside md-input-container.'); } + mdDatePickerCtrl.configureNgModel(ngModelCtrl); + } + }; + } + datePickerDirective.$inject = ["$$mdSvgRegistry"]; - }); - } - }; -} -MdDialogDirective.$inject = ["$$rAF", "$mdTheming", "$mdDialog"]; + /** Additional offset for the input's `size` attribute, which is updated based on its content. */ + var EXTRA_INPUT_SIZE = 3; -/** - * @ngdoc service - * @name $mdDialog - * @module material.components.dialog - * - * @description - * `$mdDialog` opens a dialog over the app to inform users about critical information or require - * them to make decisions. There are two approaches for setup: a simple promise API - * and regular object syntax. - * - * ## Restrictions - * - * - The dialog is always given an isolate scope. - * - The dialog's template must have an outer `` element. - * Inside, use an `` element for the dialog's content, and use - * an `` element for the dialog's actions. - * - Dialogs must cover the entire application to keep interactions inside of them. - * Use the `parent` option to change where dialogs are appended. - * - * ## Sizing - * - Complex dialogs can be sized with `flex="percentage"`, i.e. `flex="66"`. - * - Default max-width is 80% of the `rootElement` or `parent`. - * - * ## CSS - * - `.md-dialog-content` - class that sets the padding on the content as the spec file - * - * @usage - * - *
- *
- * - * Employee Alert! - * - *
- *
- * - * Custom Dialog - * - *
- *
- * - * Close Alert - * - *
- *
- * - * Greet Employee - * - *
- *
- *
- * - * ### JavaScript: object syntax - * - * (function(angular, undefined){ - * "use strict"; - * - * angular - * .module('demoApp', ['ngMaterial']) - * .controller('AppCtrl', AppController); - * - * function AppController($scope, $mdDialog) { - * var alert; - * $scope.showAlert = showAlert; - * $scope.showDialog = showDialog; - * $scope.items = [1, 2, 3]; - * - * // Internal method - * function showAlert() { - * alert = $mdDialog.alert({ - * title: 'Attention', - * textContent: 'This is an example of how easy dialogs can be!', - * ok: 'Close' - * }); - * - * $mdDialog - * .show( alert ) - * .finally(function() { - * alert = undefined; - * }); - * } - * - * function showDialog($event) { - * var parentEl = angular.element(document.body); - * $mdDialog.show({ - * parent: parentEl, - * targetEvent: $event, - * template: - * '' + + /** Class applied to the container if the date is invalid. */ + var INVALID_CLASS = 'md-datepicker-invalid'; + + /** Default time in ms to debounce input event by. */ + var DEFAULT_DEBOUNCE_INTERVAL = 500; + + /** + * Height of the calendar pane used to check if the pane is going outside the boundary of + * the viewport. See calendar.scss for how $md-calendar-height is computed; an extra 20px is + * also added to space the pane away from the exact edge of the screen. + * + * This is computed statically now, but can be changed to be measured if the circumstances + * of calendar sizing are changed. + */ + var CALENDAR_PANE_HEIGHT = 368; + + /** + * Width of the calendar pane used to check if the pane is going outside the boundary of + * the viewport. See calendar.scss for how $md-calendar-width is computed; an extra 20px is + * also added to space the pane away from the exact edge of the screen. + * + * This is computed statically now, but can be changed to be measured if the circumstances + * of calendar sizing are changed. + */ + var CALENDAR_PANE_WIDTH = 360; + + /** + * Controller for md-datepicker. + * + * @ngInject @constructor + */ + function DatePickerCtrl($scope, $element, $attrs, $compile, $timeout, $window, + $mdConstant, $mdTheming, $mdUtil, $mdDateLocale, $$mdDateUtil, $$rAF) { + /** @final */ + this.$compile = $compile; + + /** @final */ + this.$timeout = $timeout; + + /** @final */ + this.$window = $window; + + /** @final */ + this.dateLocale = $mdDateLocale; + + /** @final */ + this.dateUtil = $$mdDateUtil; + + /** @final */ + this.$mdConstant = $mdConstant; + + /* @final */ + this.$mdUtil = $mdUtil; + + /** @final */ + this.$$rAF = $$rAF; + + /** + * The root document element. This is used for attaching a top-level click handler to + * close the calendar panel when a click outside said panel occurs. We use `documentElement` + * instead of body because, when scrolling is disabled, some browsers consider the body element + * to be completely off the screen and propagate events directly to the html element. + * @type {!angular.JQLite} + */ + this.documentElement = angular.element(document.documentElement); + + /** @type {!angular.NgModelController} */ + this.ngModelCtrl = null; + + /** @type {HTMLInputElement} */ + this.inputElement = $element[0].querySelector('input'); + + /** @final {!angular.JQLite} */ + this.ngInputElement = angular.element(this.inputElement); + + /** @type {HTMLElement} */ + this.inputContainer = $element[0].querySelector('.md-datepicker-input-container'); + + /** @type {HTMLElement} Floating calendar pane. */ + this.calendarPane = $element[0].querySelector('.md-datepicker-calendar-pane'); + + /** @type {HTMLElement} Calendar icon button. */ + this.calendarButton = $element[0].querySelector('.md-datepicker-button'); + + /** + * Element covering everything but the input in the top of the floating calendar pane. + * @type {HTMLElement} + */ + this.inputMask = $element[0].querySelector('.md-datepicker-input-mask-opaque'); + + /** @final {!angular.JQLite} */ + this.$element = $element; + + /** @final {!angular.Attributes} */ + this.$attrs = $attrs; + + /** @final {!angular.Scope} */ + this.$scope = $scope; + + /** @type {Date} */ + this.date = null; + + /** @type {boolean} */ + this.isFocused = false; + + /** @type {boolean} */ + this.isDisabled; + this.setDisabled($element[0].disabled || angular.isString($attrs.disabled)); + + /** @type {boolean} Whether the date-picker's calendar pane is open. */ + this.isCalendarOpen = false; + + /** @type {boolean} Whether the calendar should open when the input is focused. */ + this.openOnFocus = $attrs.hasOwnProperty('mdOpenOnFocus'); + + /** + * Element from which the calendar pane was opened. Keep track of this so that we can return + * focus to it when the pane is closed. + * @type {HTMLElement} + */ + this.calendarPaneOpenedFrom = null; + + this.calendarPane.id = 'md-date-pane' + $mdUtil.nextUid(); + + $mdTheming($element); + + /** Pre-bound click handler is saved so that the event listener can be removed. */ + this.bodyClickHandler = angular.bind(this, this.handleBodyClick); + + /** Pre-bound resize handler so that the event listener can be removed. */ + this.windowResizeHandler = $mdUtil.debounce(angular.bind(this, this.closeCalendarPane), 100); + + // Unless the user specifies so, the datepicker should not be a tab stop. + // This is necessary because ngAria might add a tabindex to anything with an ng-model + // (based on whether or not the user has turned that particular feature on/off). + if (!$attrs.tabindex) { + $element.attr('tabindex', '-1'); + } + + this.installPropertyInterceptors(); + this.attachChangeListeners(); + this.attachInteractionListeners(); + + var self = this; + $scope.$on('$destroy', function() { + self.detachCalendarPane(); + }); + } + DatePickerCtrl.$inject = ["$scope", "$element", "$attrs", "$compile", "$timeout", "$window", "$mdConstant", "$mdTheming", "$mdUtil", "$mdDateLocale", "$$mdDateUtil", "$$rAF"]; + + /** + * Sets up the controller's reference to ngModelController. + * @param {!angular.NgModelController} ngModelCtrl + */ + DatePickerCtrl.prototype.configureNgModel = function(ngModelCtrl) { + this.ngModelCtrl = ngModelCtrl; + + var self = this; + ngModelCtrl.$render = function() { + var value = self.ngModelCtrl.$viewValue; + + if (value && !(value instanceof Date)) { + throw Error('The ng-model for md-datepicker must be a Date instance. ' + + 'Currently the model is a: ' + (typeof value)); + } + + self.date = value; + self.inputElement.value = self.dateLocale.formatDate(value); + self.resizeInputElement(); + self.updateErrorState(); + }; + }; + + /** + * Attach event listeners for both the text input and the md-calendar. + * Events are used instead of ng-model so that updates don't infinitely update the other + * on a change. This should also be more performant than using a $watch. + */ + DatePickerCtrl.prototype.attachChangeListeners = function() { + var self = this; + + self.$scope.$on('md-calendar-change', function(event, date) { + self.ngModelCtrl.$setViewValue(date); + self.date = date; + self.inputElement.value = self.dateLocale.formatDate(date); + self.closeCalendarPane(); + self.resizeInputElement(); + self.updateErrorState(); + }); + + self.ngInputElement.on('input', angular.bind(self, self.resizeInputElement)); + // TODO(chenmike): Add ability for users to specify this interval. + self.ngInputElement.on('input', self.$mdUtil.debounce(self.handleInputEvent, + DEFAULT_DEBOUNCE_INTERVAL, self)); + }; + + /** Attach event listeners for user interaction. */ + DatePickerCtrl.prototype.attachInteractionListeners = function() { + var self = this; + var $scope = this.$scope; + var keyCodes = this.$mdConstant.KEY_CODE; + + // Add event listener through angular so that we can triggerHandler in unit tests. + self.ngInputElement.on('keydown', function(event) { + if (event.altKey && event.keyCode == keyCodes.DOWN_ARROW) { + self.openCalendarPane(event); + $scope.$digest(); + } + }); + + if (self.openOnFocus) { + self.ngInputElement.on('focus', angular.bind(self, self.openCalendarPane)); + } + + $scope.$on('md-calendar-close', function() { + self.closeCalendarPane(); + }); + }; + + /** + * Capture properties set to the date-picker and imperitively handle internal changes. + * This is done to avoid setting up additional $watches. + */ + DatePickerCtrl.prototype.installPropertyInterceptors = function() { + var self = this; + + if (this.$attrs.ngDisabled) { + // The expression is to be evaluated against the directive element's scope and not + // the directive's isolate scope. + var scope = this.$scope.$parent; + + if (scope) { + scope.$watch(this.$attrs.ngDisabled, function(isDisabled) { + self.setDisabled(isDisabled); + }); + } + } + + Object.defineProperty(this, 'placeholder', { + get: function() { return self.inputElement.placeholder; }, + set: function(value) { self.inputElement.placeholder = value || ''; } + }); + }; + + /** + * Sets whether the date-picker is disabled. + * @param {boolean} isDisabled + */ + DatePickerCtrl.prototype.setDisabled = function(isDisabled) { + this.isDisabled = isDisabled; + this.inputElement.disabled = isDisabled; + this.calendarButton.disabled = isDisabled; + }; + + /** + * Sets the custom ngModel.$error flags to be consumed by ngMessages. Flags are: + * - mindate: whether the selected date is before the minimum date. + * - maxdate: whether the selected flag is after the maximum date. + * - filtered: whether the selected date is allowed by the custom filtering function. + * - valid: whether the entered text input is a valid date + * + * The 'required' flag is handled automatically by ngModel. + * + * @param {Date=} opt_date Date to check. If not given, defaults to the datepicker's model value. + */ + DatePickerCtrl.prototype.updateErrorState = function(opt_date) { + var date = opt_date || this.date; + + // Clear any existing errors to get rid of anything that's no longer relevant. + this.clearErrorState(); + + if (this.dateUtil.isValidDate(date)) { + // Force all dates to midnight in order to ignore the time portion. + date = this.dateUtil.createDateAtMidnight(date); + + if (this.dateUtil.isValidDate(this.minDate)) { + var minDate = this.dateUtil.createDateAtMidnight(this.minDate); + this.ngModelCtrl.$setValidity('mindate', date >= minDate); + } + + if (this.dateUtil.isValidDate(this.maxDate)) { + var maxDate = this.dateUtil.createDateAtMidnight(this.maxDate); + this.ngModelCtrl.$setValidity('maxdate', date <= maxDate); + } + + if (angular.isFunction(this.dateFilter)) { + this.ngModelCtrl.$setValidity('filtered', this.dateFilter(date)); + } + } else { + // The date is seen as "not a valid date" if there is *something* set + // (i.e.., not null or undefined), but that something isn't a valid date. + this.ngModelCtrl.$setValidity('valid', date == null); + } + + // TODO(jelbourn): Change this to classList.toggle when we stop using PhantomJS in unit tests + // because it doesn't conform to the DOMTokenList spec. + // See https://github.com/ariya/phantomjs/issues/12782. + if (!this.ngModelCtrl.$valid) { + this.inputContainer.classList.add(INVALID_CLASS); + } + }; + + /** Clears any error flags set by `updateErrorState`. */ + DatePickerCtrl.prototype.clearErrorState = function() { + this.inputContainer.classList.remove(INVALID_CLASS); + ['mindate', 'maxdate', 'filtered', 'valid'].forEach(function(field) { + this.ngModelCtrl.$setValidity(field, true); + }, this); + }; + + /** Resizes the input element based on the size of its content. */ + DatePickerCtrl.prototype.resizeInputElement = function() { + this.inputElement.size = this.inputElement.value.length + EXTRA_INPUT_SIZE; + }; + + /** + * Sets the model value if the user input is a valid date. + * Adds an invalid class to the input element if not. + */ + DatePickerCtrl.prototype.handleInputEvent = function() { + var inputString = this.inputElement.value; + var parsedDate = inputString ? this.dateLocale.parseDate(inputString) : null; + this.dateUtil.setDateTimeToMidnight(parsedDate); + + // An input string is valid if it is either empty (representing no date) + // or if it parses to a valid date that the user is allowed to select. + var isValidInput = inputString == '' || ( + this.dateUtil.isValidDate(parsedDate) && + this.dateLocale.isDateComplete(inputString) && + this.isDateEnabled(parsedDate) + ); + + // The datepicker's model is only updated when there is a valid input. + if (isValidInput) { + this.ngModelCtrl.$setViewValue(parsedDate); + this.date = parsedDate; + } + + this.updateErrorState(parsedDate); + }; + + /** + * Check whether date is in range and enabled + * @param {Date=} opt_date + * @return {boolean} Whether the date is enabled. + */ + DatePickerCtrl.prototype.isDateEnabled = function(opt_date) { + return this.dateUtil.isDateWithinRange(opt_date, this.minDate, this.maxDate) && + (!angular.isFunction(this.dateFilter) || this.dateFilter(opt_date)); + }; + + /** Position and attach the floating calendar to the document. */ + DatePickerCtrl.prototype.attachCalendarPane = function() { + var calendarPane = this.calendarPane; + var body = document.body; + + calendarPane.style.transform = ''; + this.$element.addClass('md-datepicker-open'); + angular.element(body).addClass('md-datepicker-is-showing'); + + var elementRect = this.inputContainer.getBoundingClientRect(); + var bodyRect = body.getBoundingClientRect(); + + // Check to see if the calendar pane would go off the screen. If so, adjust position + // accordingly to keep it within the viewport. + var paneTop = elementRect.top - bodyRect.top; + var paneLeft = elementRect.left - bodyRect.left; + + // If ng-material has disabled body scrolling (for example, if a dialog is open), + // then it's possible that the already-scrolled body has a negative top/left. In this case, + // we want to treat the "real" top as (0 - bodyRect.top). In a normal scrolling situation, + // though, the top of the viewport should just be the body's scroll position. + var viewportTop = (bodyRect.top < 0 && document.body.scrollTop == 0) ? + -bodyRect.top : + document.body.scrollTop; + + var viewportLeft = (bodyRect.left < 0 && document.body.scrollLeft == 0) ? + -bodyRect.left : + document.body.scrollLeft; + + var viewportBottom = viewportTop + this.$window.innerHeight; + var viewportRight = viewportLeft + this.$window.innerWidth; + + // If the right edge of the pane would be off the screen and shifting it left by the + // difference would not go past the left edge of the screen. If the calendar pane is too + // big to fit on the screen at all, move it to the left of the screen and scale the entire + // element down to fit. + if (paneLeft + CALENDAR_PANE_WIDTH > viewportRight) { + if (viewportRight - CALENDAR_PANE_WIDTH > 0) { + paneLeft = viewportRight - CALENDAR_PANE_WIDTH; + } else { + paneLeft = viewportLeft; + var scale = this.$window.innerWidth / CALENDAR_PANE_WIDTH; + calendarPane.style.transform = 'scale(' + scale + ')'; + } + + calendarPane.classList.add('md-datepicker-pos-adjusted'); + } + + // If the bottom edge of the pane would be off the screen and shifting it up by the + // difference would not go past the top edge of the screen. + if (paneTop + CALENDAR_PANE_HEIGHT > viewportBottom && + viewportBottom - CALENDAR_PANE_HEIGHT > viewportTop) { + paneTop = viewportBottom - CALENDAR_PANE_HEIGHT; + calendarPane.classList.add('md-datepicker-pos-adjusted'); + } + + calendarPane.style.left = paneLeft + 'px'; + calendarPane.style.top = paneTop + 'px'; + document.body.appendChild(calendarPane); + + // The top of the calendar pane is a transparent box that shows the text input underneath. + // Since the pane is floating, though, the page underneath the pane *adjacent* to the input is + // also shown unless we cover it up. The inputMask does this by filling up the remaining space + // based on the width of the input. + this.inputMask.style.left = elementRect.width + 'px'; + + // Add CSS class after one frame to trigger open animation. + this.$$rAF(function() { + calendarPane.classList.add('md-pane-open'); + }); + }; + + /** Detach the floating calendar pane from the document. */ + DatePickerCtrl.prototype.detachCalendarPane = function() { + this.$element.removeClass('md-datepicker-open'); + angular.element(document.body).removeClass('md-datepicker-is-showing'); + this.calendarPane.classList.remove('md-pane-open'); + this.calendarPane.classList.remove('md-datepicker-pos-adjusted'); + + if (this.isCalendarOpen) { + this.$mdUtil.enableScrolling(); + } + + if (this.calendarPane.parentNode) { + // Use native DOM removal because we do not want any of the angular state of this element + // to be disposed. + this.calendarPane.parentNode.removeChild(this.calendarPane); + } + }; + + /** + * Open the floating calendar pane. + * @param {Event} event + */ + DatePickerCtrl.prototype.openCalendarPane = function(event) { + if (!this.isCalendarOpen && !this.isDisabled) { + this.isCalendarOpen = true; + this.calendarPaneOpenedFrom = event.target; + + // Because the calendar pane is attached directly to the body, it is possible that the + // rest of the component (input, etc) is in a different scrolling container, such as + // an md-content. This means that, if the container is scrolled, the pane would remain + // stationary. To remedy this, we disable scrolling while the calendar pane is open, which + // also matches the native behavior for things like `', + ' ', + '
', + ' ', + ' ', + ' {{ dialog.cancel }}', + ' ', + ' ', + ' {{ dialog.ok }}', + ' ', + ' ', + '
' + ].join('').replace(/\s\s+/g, ''), + controller: function mdDialogCtrl() { + var isPrompt = this.$type == 'prompt'; + + if (isPrompt && this.initialValue) { + this.result = this.initialValue; + } + + this.hide = function() { + $mdDialog.hide(isPrompt ? 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, + contentElement: 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.'); + } + + if (controller.mdHtmlContent && controller.mdTextContent) { + throw Error('md-dialog cannot have both `htmlContent` and `textContent`'); + } + } + } + + /** Show method for dialogs */ + function onShow(scope, element, options, controller) { + angular.element($document[0].body).addClass('md-dialog-is-showing'); + + if (options.contentElement) { + var contentEl = options.contentElement; + + if (angular.isString(contentEl)) { + contentEl = document.querySelector(contentEl); + options.elementInsertionSibling = contentEl.nextElementSibling; + options.elementInsertionParent = contentEl.parentNode; + } else { + contentEl = contentEl[0] || contentEl; + // When the element is not visible in the DOM, then we can treat is as same + // as a normal dialog would do. Removing it at close etc. + // --- + // When the element is visible in the DOM, then we restore it at close of the dialog. + if (document.contains(contentEl)) { + options.elementInsertionSibling = contentEl.nextElementSibling; + options.elementInsertionParent = contentEl.parentNode; + } + } + + options.elementInsertionEntry = contentEl; + element = angular.element(contentEl); + } + + captureParentAndFromToElements(options); + configureAria(element.find('md-dialog'), options); + showBackdrop(scope, element, options); + + return dialogPopIn(element, options) + .then(function() { + activateListeners(element, options); + lockScreenReader(element, options); + warnDeprecatedActions(); + focusOnOpen(); + }); + + /** + * Check to see if they used the deprecated .md-actions class and log a warning + */ + function warnDeprecatedActions() { + if (element[0].querySelector('.md-actions')) { + $log.warn('Using a class of md-actions is deprecated, please use .'); + } + } + + /** + * 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(); + } + + /** + * 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); + } + } + } + + /** + * 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); + } + + // 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 ); + + /** + * For normal closes, animate the removal. + * For forced closes (like $destroy events), skip the animations + */ + function animateRemoval() { + return dialogPopOut(element, options); + } + + function removeContentElement() { + if (!options.contentElement) return; + + options.reverseContainerStretch(); + + if (!options.elementInsertionParent) { + // When the contentElement has no parent, then it's a virtual DOM element, which should + // be removed at close, as same as normal templates inside of a dialog. + options.elementInsertionEntry.parentNode.removeChild(options.elementInsertionEntry); + } else if (!options.elementInsertionSibling) { + // When the contentElement doesn't have any sibling, then it can be simply appended to the + // parent, because it plays no role, which index it had before. + options.elementInsertionParent.appendChild(options.elementInsertionEntry); + } else { + // When the contentElement has a sibling, which marks the previous position of the contentElement + // in the DOM, we insert it correctly before the sibling, to have the same index as before. + options.elementInsertionParent.insertBefore(options.elementInsertionEntry, options.elementInsertionSibling); + } + } + + /** + * Detach the element + */ + function detachAndClean() { + angular.element($document[0].body).removeClass('md-dialog-is-showing'); + // Only remove the element, if it's not provided through the contentElement option. + if (!options.contentElement) { + element.remove(); + } else { + removeContentElement(); + } + + if (!options.$destroy) options.origin.focus(); + } + } + + /** + * 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 || {}); + + options.parent = getDomElement(options.parent, $rootElement); + options.closeTo = getBoundingClientRect(getDomElement(options.closeTo)); + options.openFrom = getBoundingClientRect(getDomElement(options.openFrom)); + + if ( options.targetEvent ) { + options.origin = getBoundingClientRect(options.targetEvent.target, options.origin); + } + + /** + * 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); + + return angular.extend(orig || {}, { + element : hasFn ? source : undefined, + bounds : hasFn ? source[0].getBoundingClientRect() : angular.extend({}, bounds, source[0]), + focus : angular.bind(source, source.focus), + }); + } + } + + /** + * If the specifier is a simple string selector, then query for + * the DOM element. + */ + function getDomElement(element, defaultElement) { + if (angular.isString(element)) { + element = $document[0].querySelector(element); + } + + // If we have a reference to a raw dom element, always wrap it in jqLite + return angular.element(element || defaultElement); + } + + } + + /** + * 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); + + 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); + }; + + if (options.escapeToClose) { + var parentTarget = options.parent; + var keyHandlerFn = function(ev) { + if (ev.keyCode === $mdConstant.KEY_CODE.ESCAPE) { + ev.stopPropagation(); + ev.preventDefault(); + + smartClose(); + } + }; + + // Add keydown listeners + element.on('keydown', keyHandlerFn); + parentTarget.on('keydown', keyHandlerFn); + + // Queue remove listeners function + removeListeners.push(function() { + + element.off('keydown', keyHandlerFn); + parentTarget.off('keydown', keyHandlerFn); + + }); + } + + // Register listener to update dialog on window resize + window.on('resize', onWindowResize); + + removeListeners.push(function() { + window.off('resize', onWindowResize); + }); + + 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; + }; + + // 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(); + + smartClose(); + } + }; + + // Add listeners + target.on('mousedown', mousedownHandler); + target.on('mouseup', mouseupHandler); + + // Queue remove listeners function + removeListeners.push(function() { + target.off('mousedown', mousedownHandler); + target.off('mouseup', mouseupHandler); + }); + } + + // Attach specific `remove` listener handler + options.deactivateListeners = function() { + removeListeners.forEach(function(removeFn) { + removeFn(); + }); + options.deactivateListeners = null; + }; + } + + /** + * Show modal backdrop element... + */ + function showBackdrop(scope, element, options) { + + 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); + } + + if (options.hasBackdrop) { + options.backdrop = $mdUtil.createBackdrop(scope, "_md-dialog-backdrop md-opaque"); + $animate.enter(options.backdrop, options.parent); + } + + /** + * Hide modal backdrop element... + */ + options.hideBackdrop = function hideBackdrop($destroy) { + if (options.backdrop) { + if ( !!$destroy ) options.backdrop.remove(); + else $animate.leave(options.backdrop); + } + + if (options.disableParentScroll) { + options.restoreScroll(); + delete options.restoreScroll; + } + + options.hideBackdrop = null; + } + } + + /** + * Inject ARIA-specific attributes appropriate for Dialogs + */ + function configureAria(element, options) { + + var role = (options.$type === 'alert') ? 'alertdialog' : 'dialog'; + var dialogContent = element.find('md-dialog-content'); + var existingDialogId = element.attr('id'); + var dialogContentId = 'dialogContent_' + (existingDialogId || $mdUtil.nextUid()); + + element.attr({ + 'role': role, + 'tabIndex': '-1' + }); + + if (dialogContent.length === 0) { + dialogContent = element; + // If the dialog element already had an ID, don't clobber it. + if (existingDialogId) { + dialogContentId = existingDialogId; + } + } + + dialogContent.attr('id', dialogContentId); + element.attr('aria-describedby', dialogContentId); + + 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(' '); + }); + } + + // 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; + + bottomFocusTrap = topFocusTrap.cloneNode(false); + + // 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); + + // 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); + } + + /** + * Prevents screen reader interaction behind modal window + * on swipe interfaces + */ + function lockScreenReader(element, options) { + var isHidden = true; + + // get raw DOM node + walkDOM(element[0]); + + options.unlockScreenReader = function() { + isHidden = false; + walkDOM(element[0]); + + options.unlockScreenReader = null; + }; + + /** + * 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); + } + } + + walkDOM(element = element.parentNode); + } + } + } + + /** + * Ensure the dialog container fill-stretches to the viewport + */ + 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; + + var previousStyles = { + top: container.css('top'), + height: container.css('height') + }; + + container.css({ + top: (isFixed ? $mdUtil.scrollTop(options.parent) : 0) + 'px', + height: height ? height + 'px' : '100%' + }); + + return function() { + // Reverts the modified styles back to the previous values. + // This is needed for contentElements, which should have the same styles after close + // as before. + container.css(previousStyles); + }; + } + + /** + * Dialog open and pop-in animation + */ + function dialogPopIn(container, options) { + // Add the `md-dialog-container` to the DOM + options.parent.append(container); + options.reverseContainerStretch = stretchDialogContainerToViewport(container, options); + + 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) + + if (options.fullscreen) { + dialogEl.addClass('md-dialog-fullscreen'); + } + + return animator + .translate3d(dialogEl, from, to, translateOptions) + .then(function(animateReversal) { + // Build a reversal translate function synced to this translation... + options.reverseAnimate = function() { + delete options.reverseAnimate; + + 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)); + + return animator + .translate3d(dialogEl, from, to,translateOptions); + } + + return animateReversal( + to = animator.toTransformCss( + // in case the origin element has moved or is hidden, + // let's recalculate the translateCSS + buildTranslateToOrigin(dialogEl, options.origin) + ) + ); + + }; + + // Builds a function, which clears the animations / transforms of the dialog element. + // Required for contentElements, which should not have the the animation styling after + // the dialog is closed. + options.clearAnimate = function() { + delete options.clearAnimate; + return animator + .translate3d(dialogEl, to, animator.toTransformCss(''), {}); + }; + + return true; + }); + } + + /** + * Dialog close and pop-out animation + */ + function dialogPopOut(container, options) { + return options.reverseAnimate().then(function() { + if (options.contentElement) { + // When we use a contentElement, we want the element to be the same as before. + // That means, that we have to clear all the animation properties, like transform. + options.clearAnimate(); + } + }); + } + + /** + * Utility function to filter out raw DOM nodes + */ + function isNodeOneOf(elem, nodeTypeArray) { + if (nodeTypeArray.indexOf(elem.nodeName) !== -1) { + return true; + } + } + + } +} +MdDialogProvider.$inject = ["$$interimElementProvider"]; + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.divider + * @description Divider module! + */ +angular.module('material.components.divider', [ + 'material.core' +]) + .directive('mdDivider', MdDividerDirective); + +/** + * @ngdoc directive + * @name mdDivider + * @module material.components.divider + * @restrict E * - * $scope.showAlert = showAlert; - * $scope.closeAlert = closeAlert; - * $scope.showGreeting = showCustomGreeting; + * @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. * - * $scope.hasAlert = function() { return !!alert }; - * $scope.userName = $scope.userName || 'Bobby'; + * @param {boolean=} md-inset Add this attribute to activate the inset divider style. + * @usage + * + * * - * // Dialog #1 - Show simple alert dialog and cache - * // reference to dialog instance + * + * * - * function showAlert() { - * alert = $mdDialog.alert() - * .title('Attention, ' + $scope.userName) - * .textContent('This is an example of how easy dialogs can be!') - * .ok('Close'); + */ +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($mdUtil) { + return { + restrict: 'E', + + require: ['^?mdFabSpeedDial', '^?mdFabToolbar'], + + compile: function(element, attributes) { + var children = element.children(); + + var hasNgRepeat = $mdUtil.prefixer().hasAttribute(children, 'ng-repeat'); + + // 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('
'); + } + } + } + } + MdFabActionsDirective.$inject = ["$mdUtil"]; + +})(); + +})(); +(function(){ +"use strict"; + +(function() { + 'use strict'; + + angular.module('material.components.fabShared', ['material.core']) + .controller('MdFabController', MdFabController); + + function MdFabController($scope, $element, $animate, $mdUtil, $mdConstant, $timeout) { + var vm = this; + + // NOTE: We use async eval(s) below to avoid conflicts with any existing digest loops + + vm.open = function() { + $scope.$evalAsync("vm.isOpen = true"); + }; + + vm.close = function() { + // Async eval to avoid conflicts with existing digest loops + $scope.$evalAsync("vm.isOpen = false"); + + // Focus the trigger when the element closes so users can still tab to the next item + $element.find('md-fab-trigger')[0].focus(); + }; + + // Toggle the open/close state when the trigger is clicked + vm.toggle = function() { + $scope.$evalAsync("vm.isOpen = !vm.isOpen"); + }; + + setupDefaults(); + setupListeners(); + setupWatchers(); + + var initialAnimationAttempts = 0; + fireInitialAnimations(); + + function setupDefaults() { + // Set the default direction to 'down' if none is specified + vm.direction = vm.direction || 'down'; + + // Set the default to be closed + vm.isOpen = vm.isOpen || false; + + // Start the keyboard interaction at the first action + resetActionIndex(); + + // 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); + }); + + // Remove our listeners when destroyed + $scope.$on('$destroy', function() { + angular.forEach(eventTypes, function(eventType) { + $element.off(eventType, parseEvents); + }); + + // 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); + } + + // If we focusout, set a timeout to close the element + if (event.type == 'focusout' && !closeTimeout) { + closeTimeout = $timeout(function() { + vm.close(); + }, 100, false); + } + + // 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; + } + } + + function resetActionIndex() { + vm.currentActionIndex = -1; + } + + 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(); + + // 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(); + } + + if (isOpen) { + enableKeyboard(); + } else { + disableKeyboard(); + } + + var toAdd = isOpen ? 'md-is-open' : ''; + var toRemove = isOpen ? '' : 'md-is-open'; + + // Set the proper ARIA attributes + trigger.attr('aria-haspopup', true); + trigger.attr('aria-expanded', isOpen); + actions.attr('aria-hidden', !isOpen); + + // Animate the CSS classes + $animate.setClass($element, toAdd, toRemove); + }); + } + + 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'); + }); + } + + // Otherwise, try for up to 1 second before giving up + else if (initialAnimationAttempts < 10) { + $timeout(fireInitialAnimations, 100); + + // Increment our counter + initialAnimationAttempts = initialAnimationAttempts + 1; + } + } + + function enableKeyboard() { + $element.on('keydown', keyPressed); + + // 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); + }); + + // 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); + } + + 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 (!closestTrigger && !closestActions) { + vm.close(); + } + } + } + + 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; + } + } + + function doActionPrev(event) { + focusAction(event, -1); + } + + function doActionNext(event) { + focusAction(event, 1); + } + + function focusAction(event, direction) { + var actions = resetActionTabIndexes(); + + // 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); + + // 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(); + } + + 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); + }); + + return actions; + } + + function doKeyLeft(event) { + if (vm.direction === 'left') { + doActionNext(event); + } else { + doActionPrev(event); + } + } + + function doKeyUp(event) { + if (vm.direction === 'down') { + doActionPrev(event); + } else { + doActionNext(event); + } + } + + function doKeyRight(event) { + if (vm.direction === 'left') { + doActionPrev(event); + } else { + doActionNext(event); + } + } + + function doKeyDown(event) { + if (vm.direction === 'up') { + doActionPrev(event); + } else { + doActionNext(event); + } + } + + function isTrigger(element) { + return $mdUtil.getClosest(element, 'md-fab-trigger'); + } + + function isAction(element) { + return $mdUtil.getClosest(element, 'md-fab-actions'); + } + + function handleItemClick(event) { + if (isTrigger(event.target)) { + vm.toggle(); + } + + if (isAction(event.target)) { + vm.close(); + } + } + + function getTriggerElement() { + return $element.find('md-fab-trigger'); + } + + function getActionsElement() { + return $element.find('md-fab-actions'); + } + } + MdFabController.$inject = ["$scope", "$element", "$animate", "$mdUtil", "$mdConstant", "$timeout"]; +})(); + +})(); +(function(){ +"use strict"; + +(function() { + 'use strict'; + + /** + * The duration of the CSS animation in milliseconds. + * + * @type {number} + */ + var cssAnimationDuration = 300; + + /** + * @ngdoc module + * @name material.components.fabSpeedDial + */ + angular + // Declare our module + .module('material.components.fabSpeedDial', [ + 'material.core', + 'material.components.fabShared', + 'material.components.fabTrigger', + 'material.components.fabActions' + ]) + + // Register our directive + .directive('mdFabSpeedDial', MdFabSpeedDialDirective) + + // Register our custom animations + .animation('.md-fling', MdFabSpeedDialFlingAnimation) + .animation('.md-scale', MdFabSpeedDialScaleAnimation) + + // Register a service for each animation so that we can easily inject them into unit tests + .service('mdFabSpeedDialFlingAnimation', MdFabSpeedDialFlingAnimation) + .service('mdFabSpeedDialScaleAnimation', MdFabSpeedDialScaleAnimation); + + /** + * @ngdoc directive + * @name mdFabSpeedDial + * @module material.components.fabSpeedDial + * + * @restrict E + * + * @description + * The `` directive is used to present a series of popup elements (usually + * ``s) for quick access to common actions. + * + * There are currently two animations available by applying one of the following classes to + * the component: + * + * - `md-fling` - The speed dial items appear from underneath the trigger and move into their + * appropriate positions. + * - `md-scale` - The speed dial items appear in their proper places by scaling from 0% to 100%. + * + * You may also easily position the trigger by applying one one of the following classes to the + * `` element: + * - `md-fab-top-left` + * - `md-fab-top-right` + * - `md-fab-bottom-left` + * - `md-fab-bottom-right` + * + * These CSS classes use `position: absolute`, so you need to ensure that the container element + * also uses `position: absolute` or `position: relative` in order for them to work. + * + * Additionally, you may use the standard `ng-mouseenter` and `ng-mouseleave` directives to + * open or close the speed dial. However, if you wish to allow users to hover over the empty + * space where the actions will appear, you must also add the `md-hover-full` class to the speed + * dial element. Without this, the hover effect will only occur on top of the trigger. + * + * See the demos for more information. + * + * ## Troubleshooting + * + * If your speed dial shows the closing animation upon launch, you may need to use `ng-cloak` on + * the parent container to ensure that it is only visible once ready. We have plans to remove this + * necessity in the future. + * + * @usage + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * @param {string} md-direction From which direction you would like the speed dial to appear + * relative to the trigger element. + * @param {expression=} md-open Programmatically control whether or not the speed-dial is visible. + */ + function MdFabSpeedDialDirective() { + return { + restrict: 'E', + + scope: { + direction: '@?mdDirection', + isOpen: '=?mdOpen' + }, + + bindToController: true, + controller: 'MdFabController', + controllerAs: 'vm', + + link: FabSpeedDialLink + }; + + function FabSpeedDialLink(scope, element) { + // Prepend an element to hold our CSS variables so we can use them in the animations below + element.prepend('
'); + } + } + + function MdFabSpeedDialFlingAnimation($timeout) { + function delayDone(done) { $timeout(done, cssAnimationDuration, false); } + + function runAnimation(element) { + // Don't run if we are still waiting and we are not ready + if (element.hasClass('_md-animations-waiting') && !element.hasClass('_md-animations-ready')) { + return; + } + + var el = element[0]; + var ctrl = element.controller('mdFabSpeedDial'); + var items = el.querySelectorAll('.md-fab-action-item'); + + // Grab our trigger element + var triggerElement = el.querySelector('md-fab-trigger'); + + // Grab our element which stores CSS variables + var variablesElement = el.querySelector('._md-css-variables'); + + // Setup JS variables based on our CSS variables + var startZIndex = parseInt(window.getComputedStyle(variablesElement).zIndex); + + // Always reset the items to their natural position/state + angular.forEach(items, function(item, index) { + var styles = item.style; + + styles.transform = styles.webkitTransform = ''; + styles.transitionDelay = ''; + styles.opacity = 1; + + // Make the items closest to the trigger have the highest z-index + styles.zIndex = (items.length - index) + startZIndex; + }); + + // Set the trigger to be above all of the actions so they disappear behind it. + triggerElement.style.zIndex = startZIndex + items.length + 1; + + // If the control is closed, hide the items behind the trigger + if (!ctrl.isOpen) { + angular.forEach(items, function(item, index) { + var newPosition, axis; + var styles = item.style; + + // Make sure to account for differences in the dimensions of the trigger verses the items + // so that we can properly center everything; this helps hide the item's shadows behind + // the trigger. + var triggerItemHeightOffset = (triggerElement.clientHeight - item.clientHeight) / 2; + var triggerItemWidthOffset = (triggerElement.clientWidth - item.clientWidth) / 2; + + switch (ctrl.direction) { + case 'up': + newPosition = (item.scrollHeight * (index + 1) + triggerItemHeightOffset); + axis = 'Y'; + break; + case 'down': + newPosition = -(item.scrollHeight * (index + 1) + triggerItemHeightOffset); + axis = 'Y'; + break; + case 'left': + newPosition = (item.scrollWidth * (index + 1) + triggerItemWidthOffset); + axis = 'X'; + break; + case 'right': + newPosition = -(item.scrollWidth * (index + 1) + triggerItemWidthOffset); + axis = 'X'; + break; + } + + var newTranslate = 'translate' + axis + '(' + newPosition + 'px)'; + + styles.transform = styles.webkitTransform = newTranslate; + }); + } + } + + return { + addClass: function(element, className, done) { + if (element.hasClass('md-fling')) { + runAnimation(element); + delayDone(done); + } else { + done(); + } + }, + removeClass: function(element, className, done) { + runAnimation(element); + delayDone(done); + } + } + } + MdFabSpeedDialFlingAnimation.$inject = ["$timeout"]; + + function MdFabSpeedDialScaleAnimation($timeout) { + function delayDone(done) { $timeout(done, cssAnimationDuration, false); } + + var delay = 65; + + function runAnimation(element) { + var el = element[0]; + var ctrl = element.controller('mdFabSpeedDial'); + var items = el.querySelectorAll('.md-fab-action-item'); + + // Grab our element which stores CSS variables + var variablesElement = el.querySelector('._md-css-variables'); + + // Setup JS variables based on our CSS variables + var startZIndex = parseInt(window.getComputedStyle(variablesElement).zIndex); + + // Always reset the items to their natural position/state + angular.forEach(items, function(item, index) { + var styles = item.style, + offsetDelay = index * delay; + + styles.opacity = ctrl.isOpen ? 1 : 0; + styles.transform = styles.webkitTransform = ctrl.isOpen ? 'scale(1)' : 'scale(0)'; + styles.transitionDelay = (ctrl.isOpen ? offsetDelay : (items.length - offsetDelay)) + 'ms'; + + // Make the items closest to the trigger have the highest z-index + styles.zIndex = (items.length - index) + startZIndex; + }); + } + + return { + addClass: function(element, className, done) { + runAnimation(element); + delayDone(done); + }, + + removeClass: function(element, className, done) { + runAnimation(element); + delayDone(done); + } + } + } + MdFabSpeedDialScaleAnimation.$inject = ["$timeout"]; +})(); + +})(); +(function(){ +"use strict"; + +(function() { + 'use strict'; + + /** + * @ngdoc module + * @name material.components.fabToolbar + */ + angular + // Declare our module + .module('material.components.fabToolbar', [ + 'material.core', + 'material.components.fabShared', + 'material.components.fabTrigger', + 'material.components.fabActions' + ]) + + // Register our directive + .directive('mdFabToolbar', MdFabToolbarDirective) + + // Register our custom animations + .animation('.md-fab-toolbar', MdFabToolbarAnimation) + + // Register a service for the animation so that we can easily inject it into unit tests + .service('mdFabToolbarAnimation', MdFabToolbarAnimation); + + /** + * @ngdoc directive + * @name mdFabToolbar + * @module material.components.fabToolbar + * + * @restrict E + * + * @description + * + * The `` directive is used present a toolbar of elements (usually ``s) + * for quick access to common actions when a floating action button is activated (via click or + * keyboard navigation). + * + * You may also easily position the trigger by applying one one of the following classes to the + * `` element: + * - `md-fab-top-left` + * - `md-fab-top-right` + * - `md-fab-bottom-left` + * - `md-fab-bottom-right` + * + * These CSS classes use `position: absolute`, so you need to ensure that the container element + * also uses `position: absolute` or `position: relative` in order for them to work. + * + * @usage + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * @param {string} md-direction From which direction you would like the toolbar items to appear + * relative to the trigger element. Supports `left` and `right` directions. + * @param {expression=} md-open Programmatically control whether or not the toolbar is visible. + */ + function MdFabToolbarDirective() { + return { + restrict: 'E', + transclude: true, + template: '
' + + '
' + + '
', + + scope: { + direction: '@?mdDirection', + isOpen: '=?mdOpen' + }, + + bindToController: true, + controller: 'MdFabController', + controllerAs: 'vm', + + link: link + }; + + function link(scope, element, attributes) { + // Add the base class for animations + element.addClass('md-fab-toolbar'); + + // Prepend the background element to the trigger's button + element.find('md-fab-trigger').find('button') + .prepend('
'); + } + } + + function MdFabToolbarAnimation() { + + function runAnimation(element, className, done) { + // If no className was specified, don't do anything + if (!className) { + return; + } + + var el = element[0]; + var ctrl = element.controller('mdFabToolbar'); + + // Grab the relevant child elements + var backgroundElement = el.querySelector('._md-fab-toolbar-background'); + var triggerElement = el.querySelector('md-fab-trigger button'); + var toolbarElement = el.querySelector('md-toolbar'); + var iconElement = el.querySelector('md-fab-trigger button md-icon'); + var actions = element.find('md-fab-actions').children(); + + // If we have both elements, use them to position the new background + if (triggerElement && backgroundElement) { + // Get our variables + var color = window.getComputedStyle(triggerElement).getPropertyValue('background-color'); + var width = el.offsetWidth; + var height = el.offsetHeight; + + // Make it twice as big as it should be since we scale from the center + var scale = 2 * (width / triggerElement.offsetWidth); + + // Set some basic styles no matter what animation we're doing + backgroundElement.style.backgroundColor = color; + backgroundElement.style.borderRadius = width + 'px'; + + // If we're open + if (ctrl.isOpen) { + // Turn on toolbar pointer events when closed + toolbarElement.style.pointerEvents = 'inherit'; + + backgroundElement.style.width = triggerElement.offsetWidth + 'px'; + backgroundElement.style.height = triggerElement.offsetHeight + 'px'; + backgroundElement.style.transform = 'scale(' + scale + ')'; + + // Set the next close animation to have the proper delays + backgroundElement.style.transitionDelay = '0ms'; + iconElement && (iconElement.style.transitionDelay = '.3s'); + + // Apply a transition delay to actions + angular.forEach(actions, function(action, index) { + action.style.transitionDelay = (actions.length - index) * 25 + 'ms'; + }); + } else { + // Turn off toolbar pointer events when closed + toolbarElement.style.pointerEvents = 'none'; + + // Scale it back down to the trigger's size + backgroundElement.style.transform = 'scale(1)'; + + // Reset the position + backgroundElement.style.top = '0'; + + if (element.hasClass('md-right')) { + backgroundElement.style.left = '0'; + backgroundElement.style.right = null; + } + + if (element.hasClass('md-left')) { + backgroundElement.style.right = '0'; + backgroundElement.style.left = null; + } + + // Set the next open animation to have the proper delays + backgroundElement.style.transitionDelay = '200ms'; + iconElement && (iconElement.style.transitionDelay = '0ms'); + + // Apply a transition delay to actions + angular.forEach(actions, function(action, index) { + action.style.transitionDelay = 200 + (index * 25) + 'ms'; + }); + } + } + } + + return { + addClass: function(element, className, done) { + runAnimation(element, className, done); + done(); + }, + + removeClass: function(element, className, done) { + runAnimation(element, className, done); + done(); + } + } + } +})(); + +})(); +(function(){ +"use strict"; + +(function() { + 'use strict'; + + /** + * @ngdoc module + * @name material.components.fabTrigger + */ + angular + .module('material.components.fabTrigger', ['material.core']) + .directive('mdFabTrigger', MdFabTriggerDirective); + + /** + * @ngdoc directive + * @name mdFabTrigger + * @module material.components.fabSpeedDial + * + * @restrict E + * + * @description + * The `` directive is used inside of a `` or + * `` directive to mark an element (or elements) as the trigger and setup the + * proper event listeners. + * + * @usage + * See the `` or `` directives for example usage. + */ + function MdFabTriggerDirective() { + // TODO: Remove this completely? + return { + restrict: 'E', + + require: ['^?mdFabSpeedDial', '^?mdFabToolbar'] + }; + } +})(); + + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.gridList + */ +angular.module('material.components.gridList', ['material.core']) + .directive('mdGridList', GridListDirective) + .directive('mdGridTile', GridTileDirective) + .directive('mdGridTileFooter', GridTileCaptionDirective) + .directive('mdGridTileHeader', GridTileCaptionDirective) + .factory('$mdGridLayout', GridLayoutFactory); + +/** + * @ngdoc directive + * @name mdGridList + * @module material.components.gridList + * @restrict E + * @description + * Grid lists are an alternative to standard list views. Grid lists are distinct + * from grids used for layouts and other visual presentations. * - * $mdDialog - * .show( alert ) - * .finally(function() { - * alert = undefined; - * }); - * } + * A grid list is best suited to presenting a homogenous data type, typically + * images, and is optimized for visual comprehension and differentiating between + * like data types. * - * // Close the specified dialog instance and resolve with 'finished' flag - * // Normally this is not needed, just use '$mdDialog.hide()' to close - * // the most recent dialog popup. + * A grid list is a continuous element consisting of tessellated, regular + * subdivisions called cells that contain tiles (`md-grid-tile`). * - * function closeAlert() { - * $mdDialog.hide( alert, "finished" ); - * alert = undefined; - * } + * Concept of grid explained visually + * Grid concepts legend * - * // Dialog #2 - Demonstrate more complex dialogs construction and popup. + * Cells are arrayed vertically and horizontally within the grid. * - * function showCustomGreeting($event) { - * $mdDialog.show({ - * targetEvent: $event, - * template: - * '' + + * Tiles hold content and can span one or more cells vertically or horizontally. * - * ' Hello {{ employee }}!' + + * ### Responsive Attributes * - * ' ' + - * ' ' + - * ' Close Greeting' + - * ' ' + - * ' ' + - * '', - * controller: 'GreetingController', - * onComplete: afterShowAnimation, - * locals: { employee: $scope.userName } - * }); + * The `md-grid-list` directive supports "responsive" attributes, which allow + * different `md-cols`, `md-gutter` and `md-row-height` values depending on the + * currently matching media query. * - * // When the 'enter' animation finishes... + * In order to set a responsive attribute, first define the fallback value with + * the standard attribute name, then add additional attributes with the + * following convention: `{base-attribute-name}-{media-query-name}="{value}"` + * (ie. `md-cols-lg="8"`) * - * function afterShowAnimation(scope, element, options) { - * // post-show code here: DOM element focus, etc. - * } - * } + * @param {number} md-cols Number of columns in the grid. + * @param {string} md-row-height One of + *
    + *
  • CSS length - Fixed height rows (eg. `8px` or `1rem`)
  • + *
  • `{width}:{height}` - Ratio of width to height (eg. + * `md-row-height="16:9"`)
  • + *
  • `"fit"` - Height will be determined by subdividing the available + * height by the number of rows
  • + *
+ * @param {string=} md-gutter The amount of space between tiles in CSS units + * (default 1px) + * @param {expression=} md-on-layout Expression to evaluate after layout. Event + * object is available as `$event`, and contains performance information. * - * // Dialog #3 - Demonstrate use of ControllerAs and passing $scope to dialog - * // Here we used ng-controller="GreetingController as vm" and - * // $scope.vm === + * @usage + * Basic: + * + * + * + * + * * - * function showCustomGreeting() { + * Fixed-height rows: + * + * + * + * + * * - * $mdDialog.show({ - * clickOutsideToClose: true, + * Fit rows: + * + * + * + * + * * - * scope: $scope, // use parent scope in template - * preserveScope: true, // do not forget this if use parent scope + * Using responsive attributes: + * + * + * + * + * + */ +function GridListDirective($interpolate, $mdConstant, $mdGridLayout, $mdMedia) { + return { + restrict: 'E', + controller: GridListController, + scope: { + mdOnLayout: '&' + }, + link: postLink + }; + + function postLink(scope, element, attrs, ctrl) { + element.addClass('_md'); // private md component indicator for styling + + // Apply semantics + element.attr('role', 'list'); + + // Provide the controller with a way to trigger layouts. + ctrl.layoutDelegate = layoutDelegate; + + var invalidateLayout = angular.bind(ctrl, ctrl.invalidateLayout), + unwatchAttrs = watchMedia(); + scope.$on('$destroy', unwatchMedia); + + /** + * Watches for changes in media, invalidating layout as necessary. + */ + function watchMedia() { + for (var mediaName in $mdConstant.MEDIA) { + $mdMedia(mediaName); // initialize + $mdMedia.getQuery($mdConstant.MEDIA[mediaName]) + .addListener(invalidateLayout); + } + return $mdMedia.watchResponsiveAttributes( + ['md-cols', 'md-row-height', 'md-gutter'], attrs, layoutIfMediaMatch); + } + + function unwatchMedia() { + ctrl.layoutDelegate = angular.noop; + + unwatchAttrs(); + for (var mediaName in $mdConstant.MEDIA) { + $mdMedia.getQuery($mdConstant.MEDIA[mediaName]) + .removeListener(invalidateLayout); + } + } + + /** + * Performs grid layout if the provided mediaName matches the currently + * active media type. + */ + function layoutIfMediaMatch(mediaName) { + if (mediaName == null) { + // TODO(shyndman): It would be nice to only layout if we have + // instances of attributes using this media type + ctrl.invalidateLayout(); + } else if ($mdMedia(mediaName)) { + ctrl.invalidateLayout(); + } + } + + var lastLayoutProps; + + /** + * Invokes the layout engine, and uses its results to lay out our + * tile elements. + * + * @param {boolean} tilesInvalidated Whether tiles have been + * added/removed/moved since the last layout. This is to avoid situations + * where tiles are replaced with properties identical to their removed + * counterparts. + */ + function layoutDelegate(tilesInvalidated) { + var tiles = getTileElements(); + var props = { + tileSpans: getTileSpans(tiles), + colCount: getColumnCount(), + rowMode: getRowMode(), + rowHeight: getRowHeight(), + gutter: getGutter() + }; + + if (!tilesInvalidated && angular.equals(props, lastLayoutProps)) { + return; + } + + var performance = + $mdGridLayout(props.colCount, props.tileSpans, tiles) + .map(function(tilePositions, rowCount) { + return { + grid: { + element: element, + style: getGridStyle(props.colCount, rowCount, + props.gutter, props.rowMode, props.rowHeight) + }, + tiles: tilePositions.map(function(ps, i) { + return { + element: angular.element(tiles[i]), + style: getTileStyle(ps.position, ps.spans, + props.colCount, rowCount, + props.gutter, props.rowMode, props.rowHeight) + } + }) + } + }) + .reflow() + .performance(); + + // Report layout + scope.mdOnLayout({ + $event: { + performance: performance + } + }); + + lastLayoutProps = props; + } + + // Use $interpolate to do some simple string interpolation as a convenience. + + var startSymbol = $interpolate.startSymbol(); + var endSymbol = $interpolate.endSymbol(); + + // Returns an expression wrapped in the interpolator's start and end symbols. + function expr(exprStr) { + return startSymbol + exprStr + endSymbol; + } + + // The amount of space a single 1x1 tile would take up (either width or height), used as + // a basis for other calculations. This consists of taking the base size percent (as would be + // if evenly dividing the size between cells), and then subtracting the size of one gutter. + // However, since there are no gutters on the edges, each tile only uses a fration + // (gutterShare = numGutters / numCells) of the gutter size. (Imagine having one gutter per + // tile, and then breaking up the extra gutter on the edge evenly among the cells). + var UNIT = $interpolate(expr('share') + '% - (' + expr('gutter') + ' * ' + expr('gutterShare') + ')'); + + // The horizontal or vertical position of a tile, e.g., the 'top' or 'left' property value. + // The position comes the size of a 1x1 tile plus gutter for each previous tile in the + // row/column (offset). + var POSITION = $interpolate('calc((' + expr('unit') + ' + ' + expr('gutter') + ') * ' + expr('offset') + ')'); + + // The actual size of a tile, e.g., width or height, taking rowSpan or colSpan into account. + // This is computed by multiplying the base unit by the rowSpan/colSpan, and then adding back + // in the space that the gutter would normally have used (which was already accounted for in + // the base unit calculation). + var DIMENSION = $interpolate('calc((' + expr('unit') + ') * ' + expr('span') + ' + (' + expr('span') + ' - 1) * ' + expr('gutter') + ')'); + + /** + * Gets the styles applied to a tile element described by the given parameters. + * @param {{row: number, col: number}} position The row and column indices of the tile. + * @param {{row: number, col: number}} spans The rowSpan and colSpan of the tile. + * @param {number} colCount The number of columns. + * @param {number} rowCount The number of rows. + * @param {string} gutter The amount of space between tiles. This will be something like + * '5px' or '2em'. + * @param {string} rowMode The row height mode. Can be one of: + * 'fixed': all rows have a fixed size, given by rowHeight, + * 'ratio': row height defined as a ratio to width, or + * 'fit': fit to the grid-list element height, divinding evenly among rows. + * @param {string|number} rowHeight The height of a row. This is only used for 'fixed' mode and + * for 'ratio' mode. For 'ratio' mode, this is the *ratio* of width-to-height (e.g., 0.75). + * @returns {Object} Map of CSS properties to be applied to the style element. Will define + * values for top, left, width, height, marginTop, and paddingTop. + */ + function getTileStyle(position, spans, colCount, rowCount, gutter, rowMode, rowHeight) { + // TODO(shyndman): There are style caching opportunities here. + + // Percent of the available horizontal space that one column takes up. + var hShare = (1 / colCount) * 100; + + // Fraction of the gutter size that each column takes up. + var hGutterShare = (colCount - 1) / colCount; + + // Base horizontal size of a column. + var hUnit = UNIT({share: hShare, gutterShare: hGutterShare, gutter: gutter}); + + // The width and horizontal position of each tile is always calculated the same way, but the + // height and vertical position depends on the rowMode. + var style = { + left: POSITION({ unit: hUnit, offset: position.col, gutter: gutter }), + width: DIMENSION({ unit: hUnit, span: spans.col, gutter: gutter }), + // resets + paddingTop: '', + marginTop: '', + top: '', + height: '' + }; + + switch (rowMode) { + case 'fixed': + // In fixed mode, simply use the given rowHeight. + style.top = POSITION({ unit: rowHeight, offset: position.row, gutter: gutter }); + style.height = DIMENSION({ unit: rowHeight, span: spans.row, gutter: gutter }); + break; + + case 'ratio': + // Percent of the available vertical space that one row takes up. Here, rowHeight holds + // the ratio value. For example, if the width:height ratio is 4:3, rowHeight = 1.333. + var vShare = hShare / rowHeight; - * // Since GreetingController is instantiated with ControllerAs syntax - * // AND we are passing the parent '$scope' to the dialog, we MUST - * // use 'vm.' in the template markup - * - * template: '' + - * ' ' + - * ' Hi There {{vm.employee}}' + - * ' ' + - * '', - * - * controller: function DialogController($scope, $mdDialog) { - * $scope.closeDialog = function() { - * $mdDialog.hide(); - * } - * } - * }); - * } - * - * } - * - * // Greeting controller used with the more complex 'showCustomGreeting()' custom dialog - * - * function GreetingController($scope, $mdDialog, employee) { - * // Assigned from construction locals options... - * $scope.employee = employee; - * - * $scope.closeDialog = function() { - * // Easily hides most recent dialog shown... - * // no specific instance reference is needed. - * $mdDialog.hide(); - * }; - * } - * - * })(angular); - * - */ + // Base veritcal size of a row. + var vUnit = UNIT({ share: vShare, gutterShare: hGutterShare, gutter: gutter }); -/** - * @ngdoc method - * @name $mdDialog#alert - * - * @description - * Builds a preconfigured dialog with the specified message. - * - * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods: - * - * - $mdDialogPreset#title(string) - Sets the alert title. - * - $mdDialogPreset#textContent(string) - Sets the alert message. - * - $mdDialogPreset#htmlContent(string) - Sets the alert message as HTML. Requires ngSanitize - * module to be loaded. HTML is not run through Angular's compiler. - * - $mdDialogPreset#ok(string) - Sets the alert "Okay" button text. - * - $mdDialogPreset#theme(string) - Sets the theme of the alert dialog. - * - $mdDialogPreset#targetEvent(DOMClickEvent=) - A click's event object. When passed in as an option, - * the location of the click will be used as the starting point for the opening animation - * of the the dialog. - * - */ + // padidngTop and marginTop are used to maintain the given aspect ratio, as + // a percentage-based value for these properties is applied to the *width* of the + // containing block. See http://www.w3.org/TR/CSS2/box.html#margin-properties + style.paddingTop = DIMENSION({ unit: vUnit, span: spans.row, gutter: gutter}); + style.marginTop = POSITION({ unit: vUnit, offset: position.row, gutter: gutter }); + break; -/** - * @ngdoc method - * @name $mdDialog#confirm - * - * @description - * Builds a preconfigured dialog with the specified message. You can call show and the promise returned - * will be resolved only if the user clicks the confirm action on the dialog. - * - * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods: - * - * Additionally, it supports the following methods: - * - * - $mdDialogPreset#title(string) - Sets the confirm title. - * - $mdDialogPreset#textContent(string) - Sets the confirm message. - * - $mdDialogPreset#htmlContent(string) - Sets the confirm message as HTML. Requires ngSanitize - * module to be loaded. HTML is not run through Angular's compiler. - * - $mdDialogPreset#ok(string) - Sets the confirm "Okay" button text. - * - $mdDialogPreset#cancel(string) - Sets the confirm "Cancel" button text. - * - $mdDialogPreset#theme(string) - Sets the theme of the confirm dialog. - * - $mdDialogPreset#targetEvent(DOMClickEvent=) - A click's event object. When passed in as an option, - * the location of the click will be used as the starting point for the opening animation - * of the the dialog. - * - */ + case 'fit': + // Fraction of the gutter size that each column takes up. + var vGutterShare = (rowCount - 1) / rowCount; + + // Percent of the available vertical space that one row takes up. + var vShare = (1 / rowCount) * 100; + + // Base vertical size of a row. + var vUnit = UNIT({share: vShare, gutterShare: vGutterShare, gutter: gutter}); + + style.top = POSITION({unit: vUnit, offset: position.row, gutter: gutter}); + style.height = DIMENSION({unit: vUnit, span: spans.row, gutter: gutter}); + break; + } + + return style; + } + + function getGridStyle(colCount, rowCount, gutter, rowMode, rowHeight) { + var style = {}; + + switch(rowMode) { + case 'fixed': + style.height = DIMENSION({ unit: rowHeight, span: rowCount, gutter: gutter }); + style.paddingBottom = ''; + break; + + case 'ratio': + // rowHeight is width / height + var hGutterShare = colCount === 1 ? 0 : (colCount - 1) / colCount, + hShare = (1 / colCount) * 100, + vShare = hShare * (1 / rowHeight), + vUnit = UNIT({ share: vShare, gutterShare: hGutterShare, gutter: gutter }); + + style.height = ''; + style.paddingBottom = DIMENSION({ unit: vUnit, span: rowCount, gutter: gutter}); + break; + + case 'fit': + // noop, as the height is user set + break; + } + + return style; + } + + function getTileElements() { + return [].filter.call(element.children(), function(ele) { + return ele.tagName == 'MD-GRID-TILE' && !ele.$$mdDestroyed; + }); + } + + /** + * Gets an array of objects containing the rowspan and colspan for each tile. + * @returns {Array<{row: number, col: number}>} + */ + function getTileSpans(tileElements) { + return [].map.call(tileElements, function(ele) { + var ctrl = angular.element(ele).controller('mdGridTile'); + return { + row: parseInt( + $mdMedia.getResponsiveAttribute(ctrl.$attrs, 'md-rowspan'), 10) || 1, + col: parseInt( + $mdMedia.getResponsiveAttribute(ctrl.$attrs, 'md-colspan'), 10) || 1 + }; + }); + } + + function getColumnCount() { + var colCount = parseInt($mdMedia.getResponsiveAttribute(attrs, 'md-cols'), 10); + if (isNaN(colCount)) { + throw 'md-grid-list: md-cols attribute was not found, or contained a non-numeric value'; + } + return colCount; + } + + function getGutter() { + return applyDefaultUnit($mdMedia.getResponsiveAttribute(attrs, 'md-gutter') || 1); + } + + function getRowHeight() { + var rowHeight = $mdMedia.getResponsiveAttribute(attrs, 'md-row-height'); + if (!rowHeight) { + throw 'md-grid-list: md-row-height attribute was not found'; + } + + switch (getRowMode()) { + case 'fixed': + return applyDefaultUnit(rowHeight); + case 'ratio': + var whRatio = rowHeight.split(':'); + return parseFloat(whRatio[0]) / parseFloat(whRatio[1]); + case 'fit': + return 0; // N/A + } + } + + function getRowMode() { + var rowHeight = $mdMedia.getResponsiveAttribute(attrs, 'md-row-height'); + if (!rowHeight) { + throw 'md-grid-list: md-row-height attribute was not found'; + } + + if (rowHeight == 'fit') { + return 'fit'; + } else if (rowHeight.indexOf(':') !== -1) { + return 'ratio'; + } else { + return 'fixed'; + } + } + + function applyDefaultUnit(val) { + return /\D$/.test(val) ? val : val + 'px'; + } + } +} +GridListDirective.$inject = ["$interpolate", "$mdConstant", "$mdGridLayout", "$mdMedia"]; + +/* @ngInject */ +function GridListController($mdUtil) { + this.layoutInvalidated = false; + this.tilesInvalidated = false; + this.$timeout_ = $mdUtil.nextTick; + this.layoutDelegate = angular.noop; +} +GridListController.$inject = ["$mdUtil"]; + +GridListController.prototype = { + invalidateTiles: function() { + this.tilesInvalidated = true; + this.invalidateLayout(); + }, + + invalidateLayout: function() { + if (this.layoutInvalidated) { + return; + } + this.layoutInvalidated = true; + this.$timeout_(angular.bind(this, this.layout)); + }, + + layout: function() { + try { + this.layoutDelegate(this.tilesInvalidated); + } finally { + this.layoutInvalidated = false; + this.tilesInvalidated = false; + } + } +}; + + +/* @ngInject */ +function GridLayoutFactory($mdUtil) { + var defaultAnimator = GridTileAnimator; + + /** + * Set the reflow animator callback + */ + GridLayout.animateWith = function(customAnimator) { + defaultAnimator = !angular.isFunction(customAnimator) ? GridTileAnimator : customAnimator; + }; + + return GridLayout; -/** - * @ngdoc method - * @name $mdDialog#prompt - * - * @description - * Builds a preconfigured dialog with the specified message and input box. You can call show and the promise returned - * will be resolved only if the user clicks the prompt action on the dialog, passing the input value as the first argument. - * - * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods: - * - * Additionally, it supports the following methods: - * - * - $mdDialogPreset#title(string) - Sets the prompt title. - * - $mdDialogPreset#textContent(string) - Sets the prompt message. - * - $mdDialogPreset#htmlContent(string) - Sets the prompt message as HTML. Requires ngSanitize - * module to be loaded. HTML is not run through Angular's compiler. - * - $mdDialogPreset#placeholder(string) - Sets the placeholder text for the input. - * - $mdDialogPreset#ok(string) - Sets the prompt "Okay" button text. - * - $mdDialogPreset#cancel(string) - Sets the prompt "Cancel" button text. - * - $mdDialogPreset#theme(string) - Sets the theme of the prompt dialog. - * - $mdDialogPreset#targetEvent(DOMClickEvent=) - A click's event object. When passed in as an option, - * the location of the click will be used as the starting point for the opening animation - * of the the dialog. - * - */ + /** + * Publish layout function + */ + function GridLayout(colCount, tileSpans) { + var self, layoutInfo, gridStyles, layoutTime, mapTime, reflowTime; -/** - * @ngdoc method - * @name $mdDialog#show - * - * @description - * Show a dialog with the specified options. - * - * @param {object} optionsOrPreset Either provide an `$mdDialogPreset` returned from `alert()`, and - * `confirm()`, or an options object with the following properties: - * - `templateUrl` - `{string=}`: The url of a template that will be used as the content - * of the dialog. - * - `template` - `{string=}`: HTML template to show in the dialog. This **must** be trusted HTML - * with respect to Angular's [$sce service](https://docs.angularjs.org/api/ng/service/$sce). - * This template should **never** be constructed with any kind of user input or user data. - * - `autoWrap` - `{boolean=}`: Whether or not to automatically wrap the template with a - * `` tag if one is not provided. Defaults to true. Can be disabled if you provide a - * custom dialog directive. - * - `targetEvent` - `{DOMClickEvent=}`: A click's event object. When passed in as an option, - * the location of the click will be used as the starting point for the opening animation - * of the the dialog. - * - `openFrom` - `{string|Element|object}`: The query selector, DOM element or the Rect object - * that is used to determine the bounds (top, left, height, width) from which the Dialog will - * originate. - * - `closeTo` - `{string|Element|object}`: The query selector, DOM element or the Rect object - * that is used to determine the bounds (top, left, height, width) to which the Dialog will - * target. - * - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified, - * it will create a new isolate scope. - * This scope will be destroyed when the dialog is removed unless `preserveScope` is set to true. - * - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false - * - `disableParentScroll` - `{boolean=}`: Whether to disable scrolling while the dialog is open. - * Default true. - * - `hasBackdrop` - `{boolean=}`: Whether there should be an opaque backdrop behind the dialog. - * Default true. - * - `clickOutsideToClose` - `{boolean=}`: Whether the user can click outside the dialog to - * close it. Default false. - * - `escapeToClose` - `{boolean=}`: Whether the user can press escape to close the dialog. - * Default true. - * - `focusOnOpen` - `{boolean=}`: An option to override focus behavior on open. Only disable if - * focusing some other way, as focus management is required for dialogs to be accessible. - * Defaults to true. - * - `controller` - `{function|string=}`: The controller to associate with the dialog. The controller - * will be injected with the local `$mdDialog`, which passes along a scope for the dialog. - * - `locals` - `{object=}`: An object containing key/value pairs. The keys will be used as names - * of values to inject into the controller. For example, `locals: {three: 3}` would inject - * `three` into the controller, with the value 3. If `bindToController` is true, they will be - * copied to the controller instead. - * - `bindToController` - `bool`: bind the locals to the controller, instead of passing them in. - * - `resolve` - `{object=}`: Similar to locals, except it takes promises as values, and the - * dialog will not open until all of the promises resolve. - * - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope. - * - `parent` - `{element=}`: The element to append the dialog to. Defaults to appending - * to the root element of the application. - * - `onShowing` `{function=} Callback function used to announce the show() action is - * starting. - * - `onComplete` `{function=}`: Callback function used to announce when the show() action is - * finished. - * - `onRemoving` `{function=}`: Callback function used to announce the close/hide() action is - * starting. This allows developers to run custom animations in parallel the close animations. - * - `fullscreen` `{boolean=}`: An option to apply `.md-dialog-fullscreen` class on open. - * @returns {promise} A promise that can be resolved with `$mdDialog.hide()` or - * rejected with `$mdDialog.cancel()`. - */ + layoutTime = $mdUtil.time(function() { + layoutInfo = calculateGridFor(colCount, tileSpans); + }); -/** - * @ngdoc method - * @name $mdDialog#hide - * - * @description - * Hide an existing dialog and resolve the promise returned from `$mdDialog.show()`. - * - * @param {*=} response An argument for the resolved promise. - * - * @returns {promise} A promise that is resolved when the dialog has been closed. - */ + return self = { -/** - * @ngdoc method - * @name $mdDialog#cancel - * - * @description - * Hide an existing dialog and reject the promise returned from `$mdDialog.show()`. - * - * @param {*=} response An argument for the rejected promise. - * - * @returns {promise} A promise that is resolved when the dialog has been closed. - */ + /** + * An array of objects describing each tile's position in the grid. + */ + layoutInfo: function() { + return layoutInfo; + }, -function MdDialogProvider($$interimElementProvider) { - // Elements to capture and redirect focus when the user presses tab at the dialog boundary. - var topFocusTrap, bottomFocusTrap; + /** + * Maps grid positioning to an element and a set of styles using the + * provided updateFn. + */ + map: function(updateFn) { + mapTime = $mdUtil.time(function() { + var info = self.layoutInfo(); + gridStyles = updateFn(info.positioning, info.rowCount); + }); + return self; + }, - advancedDialogOptions.$inject = ["$mdDialog", "$mdTheming", "$mdConstant"]; - dialogDefaultOptions.$inject = ["$mdDialog", "$mdAria", "$mdUtil", "$mdConstant", "$animate", "$document", "$window", "$rootElement", "$log", "$injector"]; - return $$interimElementProvider('$mdDialog') - .setDefaults({ - methods: ['disableParentScroll', 'hasBackdrop', 'clickOutsideToClose', 'escapeToClose', - 'targetEvent', 'closeTo', 'openFrom', 'parent', 'fullscreen'], - options: dialogDefaultOptions - }) - .addPreset('alert', { - methods: ['title', 'htmlContent', 'textContent', 'content', 'ariaLabel', 'ok', 'theme', - 'css'], - options: advancedDialogOptions - }) - .addPreset('confirm', { - methods: ['title', 'htmlContent', 'textContent', 'content', 'ariaLabel', 'ok', 'cancel', - 'theme', 'css'], - options: advancedDialogOptions - }) - .addPreset('prompt', { - methods: ['title', 'htmlContent', 'textContent', 'content', 'placeholder', 'ariaLabel', - 'ok', 'cancel', 'theme', 'css'], - options: advancedDialogOptions - }); + /** + * Default animator simply sets the element.css( ). An alternate + * animator can be provided as an argument. The function has the following + * signature: + * + * function({grid: {element: JQLite, style: Object}, tiles: Array<{element: JQLite, style: Object}>) + */ + reflow: function(animatorFn) { + reflowTime = $mdUtil.time(function() { + var animator = animatorFn || defaultAnimator; + animator(gridStyles.grid, gridStyles.tiles); + }); + return self; + }, - /* @ngInject */ - function advancedDialogOptions($mdDialog, $mdTheming, $mdConstant) { - return { - template: [ - '', - ' ', - '

{{ dialog.title }}

', - '
', - '
', - '

{{::dialog.mdTextContent}}

', - '
', - ' ', - ' ', - ' ', - '
', - ' ', - ' ', - ' {{ 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) - } + /** + * Timing for the most recent layout run. + */ + performance: function() { + return { + tileCount: tileSpans.length, + layoutTime: layoutTime, + mapTime: mapTime, + reflowTime: reflowTime, + totalTime: layoutTime + mapTime + reflowTime + }; } - }, - controllerAs: 'dialog', - bindToController: true, - theme: $mdTheming.defaultTheme() - }; + }; + } + + /** + * Default Gridlist animator simple sets the css for each element; + * NOTE: any transitions effects must be manually set in the CSS. + * e.g. + * + * md-grid-tile { + * transition: all 700ms ease-out 50ms; + * } + * + */ + function GridTileAnimator(grid, tiles) { + grid.element.css(grid.style); + tiles.forEach(function(t) { + t.element.css(t.style); + }) } - /* @ngInject */ - function dialogDefaultOptions($mdDialog, $mdAria, $mdUtil, $mdConstant, $animate, $document, $window, $rootElement, $log, $injector) { + /** + * Calculates the positions of tiles. + * + * The algorithm works as follows: + * An Array with length colCount (spaceTracker) keeps track of + * available tiling positions, where elements of value 0 represents an + * empty position. Space for a tile is reserved by finding a sequence of + * 0s with length <= than the tile's colspan. When such a space has been + * found, the occupied tile positions are incremented by the tile's + * rowspan value, as these positions have become unavailable for that + * many rows. + * + * If the end of a row has been reached without finding space for the + * tile, spaceTracker's elements are each decremented by 1 to a minimum + * of 0. Rows are searched in this fashion until space is found. + */ + function calculateGridFor(colCount, tileSpans) { + var curCol = 0, + curRow = 0, + spaceTracker = newSpaceTracker(); + return { - 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) + '
'; + positioning: tileSpans.map(function(spans, i) { + return { + spans: spans, + position: reserveSpace(spans, i) + }; + }), + rowCount: curRow + Math.max.apply(Math, spaceTracker) + }; - /** - * 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 reserveSpace(spans, i) { + if (spans.col > colCount) { + throw 'md-grid-list: Tile at position ' + i + ' has a colspan ' + + '(' + spans.col + ') that exceeds the column count ' + + '(' + colCount + ')'; } - }; - function beforeShow(scope, element, options, controller) { - if (controller) { - controller.mdHtmlContent = controller.htmlContent || options.htmlContent || ''; - controller.mdTextContent = controller.textContent || options.textContent || - controller.content || options.content || ''; + var start = 0, + end = 0; - if (controller.mdHtmlContent && !$injector.has('$sanitize')) { - throw Error('The ngSanitize module must be loaded in order to use htmlContent.'); + // TODO(shyndman): This loop isn't strictly necessary if you can + // determine the minimum number of rows before a space opens up. To do + // this, recognize that you've iterated across an entire row looking for + // space, and if so fast-forward by the minimum rowSpan count. Repeat + // until the required space opens up. + while (end - start < spans.col) { + if (curCol >= colCount) { + nextRow(); + continue; } - if (controller.mdHtmlContent && controller.mdTextContent) { - throw Error('md-dialog cannot have both `htmlContent` and `textContent`'); + start = spaceTracker.indexOf(0, curCol); + if (start === -1 || (end = findEnd(start + 1)) === -1) { + start = end = 0; + nextRow(); + continue; } + + curCol = end + 1; } - } - /** Show method for dialogs */ - function onShow(scope, element, options, controller) { - angular.element($document[0].body).addClass('md-dialog-is-showing'); + adjustRow(start, spans.col, spans.row); + curCol = start + spans.col; - captureParentAndFromToElements(options); - configureAria(element.find('md-dialog'), options); - showBackdrop(scope, element, options); + return { + col: start, + row: curRow + }; + } - return dialogPopIn(element, options) - .then(function() { - activateListeners(element, options); - lockScreenReader(element, options); - warnDeprecatedActions(); - focusOnOpen(); - }); + function nextRow() { + curCol = 0; + curRow++; + adjustRow(0, colCount, -1); // Decrement row spans by one + } - /** - * Check to see if they used the deprecated .md-actions class and log a warning - */ - function warnDeprecatedActions() { - var badActions = element[0].querySelectorAll('.md-actions'); + function adjustRow(from, cols, by) { + for (var i = from; i < from + cols; i++) { + spaceTracker[i] = Math.max(spaceTracker[i] + by, 0); + } + } - if (badActions.length > 0) { - $log.warn('Using a class of md-actions is deprected, please use .'); + function findEnd(start) { + var i; + for (i = start; i < spaceTracker.length; i++) { + if (spaceTracker[i] !== 0) { + return i; } } - /** - * 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(); - } + if (i === spaceTracker.length) { + return i; + } + } - /** - * 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); - } + function newSpaceTracker() { + var tracker = []; + for (var i = 0; i < colCount; i++) { + tracker.push(0); } + return tracker; + } + } +} +GridLayoutFactory.$inject = ["$mdUtil"]; + +/** + * @ngdoc directive + * @name mdGridTile + * @module material.components.gridList + * @restrict E + * @description + * Tiles contain the content of an `md-grid-list`. They span one or more grid + * cells vertically or horizontally, and use `md-grid-tile-{footer,header}` to + * display secondary content. + * + * ### Responsive Attributes + * + * The `md-grid-tile` directive supports "responsive" attributes, which allow + * different `md-rowspan` and `md-colspan` values depending on the currently + * matching media query. + * + * In order to set a responsive attribute, first define the fallback value with + * the standard attribute name, then add additional attributes with the + * following convention: `{base-attribute-name}-{media-query-name}="{value}"` + * (ie. `md-colspan-sm="4"`) + * + * @param {number=} md-colspan The number of columns to span (default 1). Cannot + * exceed the number of columns in the grid. Supports interpolation. + * @param {number=} md-rowspan The number of rows to span (default 1). Supports + * interpolation. + * + * @usage + * With header: + * + * + * + *

This is a header

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

This is a footer

+ *
+ *
+ *
+ * + * Spanning multiple rows/columns: + * + * + * + * + * + * Responsive attributes: + * + * + * + * + */ +function GridTileDirective($mdMedia) { + return { + restrict: 'E', + require: '^mdGridList', + template: '
', + transclude: true, + scope: {}, + // Simple controller that exposes attributes to the grid directive + controller: ["$attrs", function($attrs) { + this.$attrs = $attrs; + }], + link: postLink + }; + + function postLink(scope, element, attrs, gridCtrl) { + // Apply semantics + element.attr('role', 'listitem'); + + // If our colspan or rowspan changes, trigger a layout + var unwatchAttrs = $mdMedia.watchResponsiveAttributes(['md-colspan', 'md-rowspan'], + attrs, angular.bind(gridCtrl, gridCtrl.invalidateLayout)); + + // Tile registration/deregistration + gridCtrl.invalidateTiles(); + scope.$on('$destroy', function() { + // Mark the tile as destroyed so it is no longer considered in layout, + // even if the DOM element sticks around (like during a leave animation) + element[0].$$mdDestroyed = true; + unwatchAttrs(); + gridCtrl.invalidateLayout(); + }); + + if (angular.isDefined(scope.$parent.$index)) { + scope.$watch(function() { return scope.$parent.$index; }, + function indexChanged(newIdx, oldIdx) { + if (newIdx === oldIdx) { + return; + } + gridCtrl.invalidateTiles(); + }); } + } +} +GridTileDirective.$inject = ["$mdMedia"]; - /** - * 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); - } +function GridTileCaptionDirective() { + return { + template: '
', + transclude: true + }; +} - if (bottomFocusTrap && bottomFocusTrap.parentNode) { - bottomFocusTrap.parentNode.removeChild(bottomFocusTrap); - } +})(); +(function(){ +"use strict"; - // 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 ); +/** + * @ngdoc module + * @name material.components.icon + * @description + * Icon + */ +angular.module('material.components.icon', ['material.core']); - /** - * For normal closes, animate the removal. - * For forced closes (like $destroy events), skip the animations - */ - function animateRemoval() { - return dialogPopOut(element, options); - } +})(); +(function(){ +"use strict"; - /** - * Detach the element - */ - function detachAndClean() { - angular.element($document[0].body).removeClass('md-dialog-is-showing'); - element.remove(); +/** + * @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); - if (!options.$destroy) options.origin.focus(); - } +/** + * @ngdoc directive + * @name mdList + * @module material.components.list + * + * @restrict E + * + * @description + * The `` directive is a list container for 1..n `` tags. + * + * @usage + * + * + * + * + *
+ *

{{item.title}}

+ *

{{item.description}}

+ *
+ *
+ *
+ *
+ */ + +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 + * + * @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. + * + * ## CSS + * `.md-avatar` - class for image avatars + * + * `.md-avatar-icon` - class for icon avatars + * + * `.md-offset` - on content without an avatar + * + * @usage + * + * + * + * + * Item content in list + * + * + * + * Item content in list + * + * + * + * + * _**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._ + */ +function mdListItemDirective($mdAria, $mdConstant, $mdUtil, $timeout) { + var proxiedTypes = ['md-checkbox', 'md-switch']; + return { + restrict: 'E', + controller: 'MdListController', + compile: function(tEl, tAttrs) { - /** - * 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 || {}); + // Check for proxy controls (no ng-click on parent, and a control inside) + var secondaryItems = tEl[0].querySelectorAll('.md-secondary'); + var hasProxiedElement; + var proxyElement; + var itemContainer = tEl; - options.parent = getDomElement(options.parent, $rootElement); - options.closeTo = getBoundingClientRect(getDomElement(options.closeTo)); - options.openFrom = getBoundingClientRect(getDomElement(options.openFrom)); + tEl[0].setAttribute('role', 'listitem'); - if ( options.targetEvent ) { - options.origin = getBoundingClientRect(options.targetEvent.target, options.origin); + if (tAttrs.ngClick || tAttrs.ngDblclick || 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'); + } + } + wrapSecondaryItems(); + setupToggleAria(); - /** - * 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); - return angular.extend(orig || {}, { - element : hasFn ? source : undefined, - bounds : hasFn ? source[0].getBoundingClientRect() : angular.extend({}, bounds, source[0]), - focus : angular.bind(source, source.focus), - }); + function setupToggleAria() { + var toggleTypes = ['md-switch', 'md-checkbox']; + var toggle; + + 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); } } + } + } - /** - * 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; - } + function wrapIn(type) { + if (type == 'div') { + itemContainer = angular.element('
'); + itemContainer.append(tEl.contents()); + tEl.addClass('_md-proxy-focus'); + } else { + // Element which holds the default list-item content. + itemContainer = angular.element( + '
'+ + '
'+ + '
' + ); - // If we have a reference to a raw dom element, always wrap it in jqLite - return angular.element(element || defaultElement); - } + // Button which shows ripple and executes primary action. + var buttonWrap = angular.element( + '' + ); + + buttonWrap[0].setAttribute('aria-label', tEl[0].textContent); + copyAttributes(tEl[0], buttonWrap[0]); + // Append the button wrap before our list-item content, because it will overlay in relative. + itemContainer.prepend(buttonWrap); + itemContainer.children().eq(1).append(tEl.contents()); + + tEl.addClass('_md-button-wrap'); } - /** - * 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); + tEl[0].setAttribute('tabindex', '-1'); + tEl.append(itemContainer); + } + + function wrapSecondaryItems() { + var secondaryItemsWrapper = angular.element('
'); + + angular.forEach(secondaryItems, function(secondaryItem) { + wrapSecondaryItem(secondaryItem, secondaryItemsWrapper); + }); + + itemContainer.append(secondaryItemsWrapper); + } + + function wrapSecondaryItem(secondaryItem, container) { + if (secondaryItem && !isButton(secondaryItem) && secondaryItem.hasAttribute('ng-click')) { + $mdAria.expect(secondaryItem, 'aria-label'); + var buttonWrapper = angular.element(''); + copyAttributes(secondaryItem, buttonWrapper[0]); + secondaryItem.setAttribute('tabindex', '-1'); + buttonWrapper.append(secondaryItem); + secondaryItem = buttonWrapper[0]; + } - 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); - }; + if (secondaryItem && (!hasClickEvent(secondaryItem) || (!tAttrs.ngClick && isProxiedElement(secondaryItem)))) { + // In this case we remove the secondary class, so we can identify it later, when we searching for the + // proxy items. + angular.element(secondaryItem).removeClass('md-secondary'); + } - if (options.escapeToClose) { - var parentTarget = options.parent; - var keyHandlerFn = function(ev) { - if (ev.keyCode === $mdConstant.KEY_CODE.ESCAPE) { - ev.stopPropagation(); - ev.preventDefault(); + tEl.addClass('md-with-secondary'); + container.append(secondaryItem); + } - smartClose(); + function copyAttributes(item, wrapper) { + var copiedAttrs = $mdUtil.prefixer([ + 'ng-if', 'ng-click', 'ng-dblclick', 'aria-label', 'ng-disabled', 'ui-sref', + 'href', 'ng-href', 'target', 'ng-attr-ui-sref', 'ui-sref-opts' + ]); + + angular.forEach(copiedAttrs, function(attr) { + if (item.hasAttribute(attr)) { + wrapper.setAttribute(attr, item.getAttribute(attr)); + item.removeAttribute(attr); } - }; + }); + } - // Add keydown listeners - element.on('keydown', keyHandlerFn); - parentTarget.on('keydown', keyHandlerFn); + function isProxiedElement(el) { + return proxiedTypes.indexOf(el.nodeName.toLowerCase()) != -1; + } - // Queue remove listeners function - removeListeners.push(function() { + function isButton(el) { + var nodeName = el.nodeName.toUpperCase(); - element.off('keydown', keyHandlerFn); - parentTarget.off('keydown', keyHandlerFn); + return nodeName == "MD-BUTTON" || nodeName == "BUTTON"; + } - }); + function hasClickEvent (element) { + var attr = element.attributes; + for (var i = 0; i < attr.length; i++) { + if (tAttrs.$normalize(attr[i].name) === 'ngClick') return true; + } + return false; } - // Register listener to update dialog on window resize - window.on('resize', onWindowResize); + return postLink; - removeListeners.push(function() { - window.off('resize', onWindowResize); - }); + function postLink($scope, $element, $attr, ctrl) { + $element.addClass('_md'); // private md component indicator for styling + + var proxies = [], + firstElement = $element[0].firstElementChild, + isButtonWrap = $element.hasClass('_md-button-wrap'), + clickChild = isButtonWrap ? firstElement.firstElementChild : firstElement, + hasClick = clickChild && hasClickEvent(clickChild); - if (options.clickOutsideToClose) { - var target = element; - var sourceElem; + computeProxies(); + computeClickable(); - // 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; - }; + if ($element.hasClass('_md-proxy-focus') && proxies.length) { + angular.forEach(proxies, function(proxy) { + proxy = angular.element(proxy); - // 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(); + $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); + }); + }); + }); + } - smartClose(); - } - }; - // Add listeners - target.on('mousedown', mousedownHandler); - target.on('mouseup', mouseupHandler); + function computeProxies() { + if (firstElement && firstElement.children && !hasClick) { - // Queue remove listeners function - removeListeners.push(function() { - target.off('mousedown', mousedownHandler); - target.off('mouseup', mouseupHandler); - }); - } + angular.forEach(proxiedTypes, function(type) { - // Attach specific `remove` listener handler - options.deactivateListeners = function() { - removeListeners.forEach(function(removeFn) { - removeFn(); - }); - options.deactivateListeners = null; - }; - } + // All elements which are not capable for being used a proxy have the .md-secondary class + // applied. These items had been sorted out in the secondary wrap function. + angular.forEach(firstElement.querySelectorAll(type + ':not(.md-secondary)'), function(child) { + proxies.push(child); + }); + }); - /** - * Show modal backdrop element... - */ - function showBackdrop(scope, element, options) { + } + } + function computeClickable() { + if (proxies.length == 1 || hasClick) { + $element.addClass('md-clickable'); - 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); - } + if (!hasClick) { + ctrl.attachRipple($scope, angular.element($element[0].querySelector('._md-no-style'))); + } + } + } - if (options.hasBackdrop) { - options.backdrop = $mdUtil.createBackdrop(scope, "_md-dialog-backdrop md-opaque"); - $animate.enter(options.backdrop, options.parent); - } + var clickChildKeypressListener = 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 (clickChild) { + clickChild.click(); + e.preventDefault(); + e.stopPropagation(); + } + } + } + }; - /** - * Hide modal backdrop element... - */ - options.hideBackdrop = function hideBackdrop($destroy) { - if (options.backdrop) { - if ( !!$destroy ) options.backdrop.remove(); - else $animate.leave(options.backdrop); + if (!hasClick && !proxies.length) { + clickChild && clickChild.addEventListener('keypress', clickChildKeypressListener); } - if (options.disableParentScroll) { - options.restoreScroll(); - delete options.restoreScroll; + $element.off('click'); + $element.off('keypress'); + + if (proxies.length == 1 && clickChild) { + $element.children().eq(0).on('click', function(e) { + var parentButton = $mdUtil.getClosest(e.target, 'BUTTON'); + if (!parentButton && clickChild.contains(e.target)) { + angular.forEach(proxies, function(proxy) { + if (e.target !== proxy && !proxy.contains(e.target)) { + angular.element(proxy).triggerHandler('click'); + } + }); + } + }); } - options.hideBackdrop = null; + $scope.$on('$destroy', function () { + clickChild && clickChild.removeEventListener('keypress', clickChildKeypressListener); + }); } } + }; +} +mdListItemDirective.$inject = ["$mdAria", "$mdConstant", "$mdUtil", "$timeout"]; - /** - * Inject ARIA-specific attributes appropriate for Dialogs - */ - function configureAria(element, options) { +/* + * @private + * @ngdoc controller + * @name MdListController + * @module material.components.list + * + */ +function MdListController($scope, $element, $mdListInkRipple) { + var ctrl = this; + ctrl.attachRipple = attachRipple; - var role = (options.$type === 'alert') ? 'alertdialog' : 'dialog'; - var dialogContent = element.find('md-dialog-content'); - var dialogContentId = 'dialogContent_' + (element.attr('id') || $mdUtil.nextUid()); + function attachRipple (scope, element) { + var options = {}; + $mdListInkRipple.attach(scope, element, options); + } +} +MdListController.$inject = ["$scope", "$element", "$mdListInkRipple"]; - element.attr({ - 'role': role, - 'tabIndex': '-1' - }); +})(); +(function(){ +"use strict"; - if (dialogContent.length === 0) { - dialogContent = element; - } +/** + * @ngdoc module + * @name material.components.input + */ - dialogContent.attr('id', dialogContentId); - element.attr('aria-describedby', dialogContentId); +angular.module('material.components.input', [ + 'material.core' + ]) + .directive('mdInputContainer', mdInputContainerDirective) + .directive('label', labelDirective) + .directive('input', inputTextareaDirective) + .directive('textarea', inputTextareaDirective) + .directive('mdMaxlength', mdMaxlengthDirective) + .directive('placeholder', placeholderDirective) + .directive('ngMessages', ngMessagesDirective) + .directive('ngMessage', ngMessageDirective) + .directive('ngMessageExp', ngMessageDirective) + .directive('mdSelectOnFocus', mdSelectOnFocusDirective) - 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(' '); - }); - } + .animation('.md-input-invalid', mdInputInvalidMessagesAnimation) + .animation('.md-input-messages-animation', ngMessagesAnimation) + .animation('.md-input-message-animation', ngMessageAnimation); + +/** + * @ngdoc directive + * @name mdInputContainer + * @module material.components.input + * + * @restrict E + * + * @description + * `` is the parent of any input or textarea element. + * + * Input and textarea elements will not behave properly unless the md-input-container + * parent is provided. + * + * A single `` should contain only one `` element, otherwise it will throw an error. + * + * Exception: Hidden inputs (``) are ignored and will not throw an error, so + * you may combine these with other inputs. + * + * @param md-is-error {expression=} When the given expression evaluates to true, the input container + * will go into error state. Defaults to erroring if the input has been touched and is invalid. + * @param md-no-float {boolean=} When present, `placeholder` attributes on the input will not be converted to floating + * labels. + * + * @usage + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *

When disabling floating labels

+ * + * + * + * + * + * + * + */ +function mdInputContainerDirective($mdTheming, $parse) { - // 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; + var INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT', 'MD-SELECT']; - bottomFocusTrap = topFocusTrap.cloneNode(false); + var LEFT_SELECTORS = INPUT_TAGS.reduce(function(selectors, isel) { + return selectors.concat(['md-icon ~ ' + isel, '.md-icon ~ ' + isel]); + }, []).join(","); - // 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); + var RIGHT_SELECTORS = INPUT_TAGS.reduce(function(selectors, isel) { + return selectors.concat([isel + ' ~ md-icon', isel + ' ~ .md-icon']); + }, []).join(","); - // 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); - } + ContainerCtrl.$inject = ["$scope", "$element", "$attrs", "$animate"]; + return { + restrict: 'E', + link: postLink, + controller: ContainerCtrl + }; - /** - * Prevents screen reader interaction behind modal window - * on swipe interfaces - */ - function lockScreenReader(element, options) { - var isHidden = true; + function postLink(scope, element) { + $mdTheming(element); - // get raw DOM node - walkDOM(element[0]); + // Check for both a left & right icon + var leftIcon = element[0].querySelector(LEFT_SELECTORS); + var rightIcon = element[0].querySelector(RIGHT_SELECTORS); - options.unlockScreenReader = function() { - isHidden = false; - walkDOM(element[0]); + if (leftIcon) { element.addClass('md-icon-left'); } + if (rightIcon) { element.addClass('md-icon-right'); } + } - options.unlockScreenReader = null; - }; + function ContainerCtrl($scope, $element, $attrs, $animate) { + var self = this; - /** - * 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); - } - } + self.isErrorGetter = $attrs.mdIsError && $parse($attrs.mdIsError); - walkDOM(element = element.parentNode); - } + self.delegateClick = function() { + self.input.focus(); + }; + self.element = $element; + self.setFocused = function(isFocused) { + $element.toggleClass('md-input-focused', !!isFocused); + }; + self.setHasValue = function(hasValue) { + $element.toggleClass('md-input-has-value', !!hasValue); + }; + self.setHasPlaceholder = function(hasPlaceholder) { + $element.toggleClass('md-input-has-placeholder', !!hasPlaceholder); + }; + self.setInvalid = function(isInvalid) { + if (isInvalid) { + $animate.addClass($element, 'md-input-invalid'); + } else { + $animate.removeClass($element, 'md-input-invalid'); } - } + }; + $scope.$watch(function() { + return self.label && self.input; + }, function(hasLabelAndInput) { + if (hasLabelAndInput && !self.label.attr('for')) { + self.label.attr('for', self.input.attr('id')); + } + }); + } +} +mdInputContainerDirective.$inject = ["$mdTheming", "$parse"]; - /** - * Ensure the dialog container fill-stretches to the viewport - */ - 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 labelDirective() { + return { + restrict: 'E', + require: '^?mdInputContainer', + link: function(scope, element, attr, containerCtrl) { + if (!containerCtrl || attr.mdNoFloat || element.hasClass('_md-container-ignore')) return; - container.css({ - top: (isFixed ? $mdUtil.scrollTop(options.parent) : 0) + 'px', - height: height ? height + 'px' : '100%' + containerCtrl.label = element; + scope.$on('$destroy', function() { + containerCtrl.label = null; }); - - return container; } + }; +} + +/** + * @ngdoc directive + * @name mdInput + * @restrict E + * @module material.components.input + * + * @description + * You can use any `` or ` + *
+ *
This is required!
+ *
That's too long!
+ *
+ *
+ * + * + * + * + * + * + * + * + * + *

Notes

+ * + * - Requires [ngMessages](https://docs.angularjs.org/api/ngMessages). + * - Behaves like the [AngularJS input directive](https://docs.angularjs.org/api/ng/directive/input). + * + * The `md-input` and `md-input-container` directives use very specific positioning to achieve the + * error animation effects. Therefore, it is *not* advised to use the Layout system inside of the + * `` tags. Instead, use relative or absolute positioning. + * + * + *

Textarea directive

+ * The `textarea` element within a `md-input-container` has the following specific behavior: + * - By default the `textarea` grows as the user types. This can be disabled via the `md-no-autogrow` + * attribute. + * - If a `textarea` has the `rows` attribute, it will treat the `rows` as the minimum height and will + * continue growing as the user types. For example a textarea with `rows="3"` will be 3 lines of text + * high initially. If no rows are specified, the directive defaults to 1. + * - If you wan't a `textarea` to stop growing at a certain point, you can specify the `max-rows` attribute. + * - The textarea's bottom border acts as a handle which users can drag, in order to resize the element vertically. + * Once the user has resized a `textarea`, the autogrowing functionality becomes disabled. If you don't want a + * `textarea` to be resizeable by the user, you can add the `md-no-resize` attribute. + */ - /** - * 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); +function inputTextareaDirective($mdUtil, $window, $mdAria, $timeout, $mdGesture) { + return { + restrict: 'E', + require: ['^?mdInputContainer', '?ngModel'], + link: postLink + }; - 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) + function postLink(scope, element, attr, ctrls) { - if (options.fullscreen) { - dialogEl.addClass('md-dialog-fullscreen'); - } + 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); + var tagName = element[0].tagName.toLowerCase(); - 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; - 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)); + if (!containerCtrl) return; + if (attr.type === 'hidden') { + element.attr('aria-hidden', 'true'); + return; + } else if (containerCtrl.input) { + throw new Error(" can only have *one* , + * + * + * + */ +function mdSelectOnFocusDirective($timeout) { - function doKeyUp(event) { - if (vm.direction === 'down') { - doActionPrev(event); - } else { - doActionNext(event); - } - } + return { + restrict: 'A', + link: postLink + }; - function doKeyRight(event) { - if (vm.direction === 'left') { - doActionPrev(event); - } else { - doActionNext(event); - } - } + function postLink(scope, element, attr) { + if (element[0].nodeName !== 'INPUT' && element[0].nodeName !== "TEXTAREA") return; - function doKeyDown(event) { - if (vm.direction === 'up') { - doActionPrev(event); - } else { - doActionNext(event); - } - } + var preventMouseUp = false; - function isTrigger(element) { - return $mdUtil.getClosest(element, 'md-fab-trigger'); - } + element + .on('focus', onFocus) + .on('mouseup', onMouseUp); - function isAction(element) { - return $mdUtil.getClosest(element, 'md-fab-actions'); - } + scope.$on('$destroy', function() { + element + .off('focus', onFocus) + .off('mouseup', onMouseUp); + }); - function handleItemClick(event) { - if (isTrigger(event.target)) { - vm.toggle(); - } + function onFocus() { + preventMouseUp = true; - if (isAction(event.target)) { - vm.close(); + $timeout(function() { + // Use HTMLInputElement#select to fix firefox select issues. + // The debounce is here for Edge's sake, otherwise the selection doesn't work. + element[0].select(); + + // This should be reset from inside the `focus`, because the event might + // have originated from something different than a click, e.g. a keyboard event. + preventMouseUp = false; + }, 1, false); + } + + // Prevents the default action of the first `mouseup` after a focus. + // This is necessary, because browsers fire a `mouseup` right after the element + // has been focused. In some browsers (Firefox in particular) this can clear the + // selection. There are examples of the problem in issue #7487. + function onMouseUp(event) { + if (preventMouseUp) { + event.preventDefault(); } } - - function getTriggerElement() { - return $element.find('md-fab-trigger'); - } - - function getActionsElement() { - return $element.find('md-fab-actions'); - } } - MdFabController.$inject = ["$scope", "$element", "$animate", "$mdUtil", "$mdConstant", "$timeout"]; -})(); +} +mdSelectOnFocusDirective.$inject = ["$timeout"]; -})(); -(function(){ -"use strict"; +var visibilityDirectives = ['ngIf', 'ngShow', 'ngHide', 'ngSwitchWhen', 'ngSwitchDefault']; +function ngMessagesDirective() { + return { + restrict: 'EA', + link: postLink, -(function() { - 'use strict'; + // This is optional because we don't want target *all* ngMessage instances, just those inside of + // mdInputContainer. + require: '^^?mdInputContainer' + }; - /** - * The duration of the CSS animation in milliseconds. - * - * @type {number} - */ - var cssAnimationDuration = 300; + function postLink(scope, element, attrs, inputContainer) { + // If we are not a child of an input container, don't do anything + if (!inputContainer) return; - /** - * @ngdoc module - * @name material.components.fabSpeedDial - */ - angular - // Declare our module - .module('material.components.fabSpeedDial', [ - 'material.core', - 'material.components.fabShared', - 'material.components.fabTrigger', - 'material.components.fabActions' - ]) + // Add our animation class + element.toggleClass('md-input-messages-animation', true); - // Register our directive - .directive('mdFabSpeedDial', MdFabSpeedDialDirective) + // Add our md-auto-hide class to automatically hide/show messages when container is invalid + element.toggleClass('md-auto-hide', true); - // Register our custom animations - .animation('.md-fling', MdFabSpeedDialFlingAnimation) - .animation('.md-scale', MdFabSpeedDialScaleAnimation) + // 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); + } + } - // Register a service for each animation so that we can easily inject them into unit tests - .service('mdFabSpeedDialFlingAnimation', MdFabSpeedDialFlingAnimation) - .service('mdFabSpeedDialScaleAnimation', MdFabSpeedDialScaleAnimation); + function hasVisibiltyDirective(attrs) { + return visibilityDirectives.some(function(attr) { + return attrs[attr]; + }); + } +} - /** - * @ngdoc directive - * @name mdFabSpeedDial - * @module material.components.fabSpeedDial - * - * @restrict E - * - * @description - * The `` directive is used to present a series of popup elements (usually - * ``s) for quick access to common actions. - * - * There are currently two animations available by applying one of the following classes to - * the component: - * - * - `md-fling` - The speed dial items appear from underneath the trigger and move into their - * appropriate positions. - * - `md-scale` - The speed dial items appear in their proper places by scaling from 0% to 100%. - * - * You may also easily position the trigger by applying one one of the following classes to the - * `` element: - * - `md-fab-top-left` - * - `md-fab-top-right` - * - `md-fab-bottom-left` - * - `md-fab-bottom-right` - * - * These CSS classes use `position: absolute`, so you need to ensure that the container element - * also uses `position: absolute` or `position: relative` in order for them to work. - * - * Additionally, you may use the standard `ng-mouseenter` and `ng-mouseleave` directives to - * open or close the speed dial. However, if you wish to allow users to hover over the empty - * space where the actions will appear, you must also add the `md-hover-full` class to the speed - * dial element. Without this, the hover effect will only occur on top of the trigger. - * - * See the demos for more information. - * - * ## Troubleshooting - * - * If your speed dial shows the closing animation upon launch, you may need to use `ng-cloak` on - * the parent container to ensure that it is only visible once ready. We have plans to remove this - * necessity in the future. - * - * @usage - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * @param {string} md-direction From which direction you would like the speed dial to appear - * relative to the trigger element. - * @param {expression=} md-open Programmatically control whether or not the speed-dial is visible. - */ - function MdFabSpeedDialDirective() { - return { - restrict: 'E', +function ngMessageDirective($mdUtil) { + return { + restrict: 'EA', + compile: compile, + priority: 100 + }; - scope: { - direction: '@?mdDirection', - isOpen: '=?mdOpen' - }, + function compile(tElement) { + if (!isInsideInputContainer(tElement)) { + + // When the current element is inside of a document fragment, then we need to check for an input-container + // in the postLink, because the element will be later added to the DOM and is currently just in a temporary + // fragment, which causes the input-container check to fail. + if (isInsideFragment()) { + return function (scope, element) { + if (isInsideInputContainer(element)) { + // Inside of the postLink function, a ngMessage directive will be a comment element, because it's + // currently hidden. To access the shown element, we need to use the element from the compile function. + initMessageElement(tElement); + } + }; + } + } else { + initMessageElement(tElement); + } - bindToController: true, - controller: 'MdFabController', - controllerAs: 'vm', + function isInsideFragment() { + var nextNode = tElement[0]; + while (nextNode = nextNode.parentNode) { + if (nextNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { + return true; + } + } + return false; + } - link: FabSpeedDialLink - }; + function isInsideInputContainer(element) { + return !!$mdUtil.getClosest(element, "md-input-container"); + } - function FabSpeedDialLink(scope, element) { - // Prepend an element to hold our CSS variables so we can use them in the animations below - element.prepend('
'); + function initMessageElement(element) { + // Add our animation class + element.toggleClass('md-input-message-animation', true); } } +} +ngMessageDirective.$inject = ["$mdUtil"]; - function MdFabSpeedDialFlingAnimation($timeout) { - function delayDone(done) { $timeout(done, cssAnimationDuration, false); } +function mdInputInvalidMessagesAnimation($q, $animateCss) { + return { + addClass: function(element, className, done) { + var messages = getMessagesElement(element); - function runAnimation(element) { - // Don't run if we are still waiting and we are not ready - if (element.hasClass('_md-animations-waiting') && !element.hasClass('_md-animations-ready')) { + if (className == "md-input-invalid" && messages.hasClass('md-auto-hide')) { + showInputMessages(element, $animateCss, $q).finally(done); + } else { + done(); + } + } + + // NOTE: We do not need the removeClass method, because the message ng-leave animation will fire + }; +} +mdInputInvalidMessagesAnimation.$inject = ["$q", "$animateCss"]; + +function ngMessagesAnimation($q, $animateCss) { + return { + enter: function(element, done) { + showInputMessages(element, $animateCss, $q).finally(done); + }, + + leave: function(element, done) { + hideInputMessages(element, $animateCss, $q).finally(done); + }, + + addClass: function(element, className, done) { + if (className == "ng-hide") { + hideInputMessages(element, $animateCss, $q).finally(done); + } else { + done(); + } + }, + + removeClass: function(element, className, done) { + if (className == "ng-hide") { + showInputMessages(element, $animateCss, $q).finally(done); + } else { + done(); + } + } + } +} +ngMessagesAnimation.$inject = ["$q", "$animateCss"]; + +function ngMessageAnimation($animateCss) { + return { + enter: function(element, done) { + var messages = getMessagesElement(element); + + // 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; } - var el = element[0]; - var ctrl = element.controller('mdFabSpeedDial'); - var items = el.querySelectorAll('.md-fab-action-item'); + return showMessage(element, $animateCss); + }, - // Grab our trigger element - var triggerElement = el.querySelector('md-fab-trigger'); + leave: function(element, done) { + return hideMessage(element, $animateCss); + } + } +} +ngMessageAnimation.$inject = ["$animateCss"]; - // Grab our element which stores CSS variables - var variablesElement = el.querySelector('._md-css-variables'); +function showInputMessages(element, $animateCss, $q) { + var animators = [], animator; + var messages = getMessagesElement(element); - // Setup JS variables based on our CSS variables - var startZIndex = parseInt(window.getComputedStyle(variablesElement).zIndex); + angular.forEach(messages.children(), function(child) { + animator = showMessage(angular.element(child), $animateCss); - // Always reset the items to their natural position/state - angular.forEach(items, function(item, index) { - var styles = item.style; + animators.push(animator.start()); + }); - styles.transform = styles.webkitTransform = ''; - styles.transitionDelay = ''; - styles.opacity = 1; + return $q.all(animators); +} - // Make the items closest to the trigger have the highest z-index - styles.zIndex = (items.length - index) + startZIndex; - }); +function hideInputMessages(element, $animateCss, $q) { + var animators = [], animator; + var messages = getMessagesElement(element); - // Set the trigger to be above all of the actions so they disappear behind it. - triggerElement.style.zIndex = startZIndex + items.length + 1; + angular.forEach(messages.children(), function(child) { + animator = hideMessage(angular.element(child), $animateCss); - // If the control is closed, hide the items behind the trigger - if (!ctrl.isOpen) { - angular.forEach(items, function(item, index) { - var newPosition, axis; - var styles = item.style; + animators.push(animator.start()); + }); - // Make sure to account for differences in the dimensions of the trigger verses the items - // so that we can properly center everything; this helps hide the item's shadows behind - // the trigger. - var triggerItemHeightOffset = (triggerElement.clientHeight - item.clientHeight) / 2; - var triggerItemWidthOffset = (triggerElement.clientWidth - item.clientWidth) / 2; + return $q.all(animators); +} - switch (ctrl.direction) { - case 'up': - newPosition = (item.scrollHeight * (index + 1) + triggerItemHeightOffset); - axis = 'Y'; - break; - case 'down': - newPosition = -(item.scrollHeight * (index + 1) + triggerItemHeightOffset); - axis = 'Y'; - break; - case 'left': - newPosition = (item.scrollWidth * (index + 1) + triggerItemWidthOffset); - axis = 'X'; - break; - case 'right': - newPosition = -(item.scrollWidth * (index + 1) + triggerItemWidthOffset); - axis = 'X'; - break; - } +function showMessage(element, $animateCss) { + var height = element[0].offsetHeight; - var newTranslate = 'translate' + axis + '(' + newPosition + 'px)'; + return $animateCss(element, { + event: 'enter', + structural: true, + from: {"opacity": 0, "margin-top": -height + "px"}, + to: {"opacity": 1, "margin-top": "0"}, + duration: 0.3 + }); +} - styles.transform = styles.webkitTransform = newTranslate; - }); - } - } +function hideMessage(element, $animateCss) { + var height = element[0].offsetHeight; + var styles = window.getComputedStyle(element[0]); - return { - addClass: function(element, className, done) { - if (element.hasClass('md-fling')) { - runAnimation(element); - delayDone(done); - } else { - done(); - } - }, - removeClass: function(element, className, done) { - runAnimation(element); - delayDone(done); - } - } + // If we are already hidden, just return an empty animation + if (styles.opacity == 0) { + return $animateCss(element, {}); } - MdFabSpeedDialFlingAnimation.$inject = ["$timeout"]; - function MdFabSpeedDialScaleAnimation($timeout) { - function delayDone(done) { $timeout(done, cssAnimationDuration, false); } + // 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 + }); +} + +function getInputElement(element) { + var inputContainer = element.controller('mdInputContainer'); + + return inputContainer.element; +} + +function getMessagesElement(element) { + var input = getInputElement(element); + + return angular.element(input[0].querySelector('.md-input-messages-animation')); +} + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.menu-bar + */ + +angular.module('material.components.menuBar', [ + 'material.core', + 'material.components.menu' +]); + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.menu + */ + +angular.module('material.components.menu', [ + 'material.core', + 'material.components.backdrop' +]); + +})(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.navBar + */ + + +angular.module('material.components.navBar', ['material.core']) + .controller('MdNavBarController', MdNavBarController) + .directive('mdNavBar', MdNavBar) + .controller('MdNavItemController', MdNavItemController) + .directive('mdNavItem', MdNavItem); - var delay = 65; - function runAnimation(element) { - var el = element[0]; - var ctrl = element.controller('mdFabSpeedDial'); - var items = el.querySelectorAll('.md-fab-action-item'); +/***************************************************************************** + * PUBLIC DOCUMENTATION * + *****************************************************************************/ +/** + * @ngdoc directive + * @name mdNavBar + * @module material.components.navBar + * + * @restrict E + * + * @description + * The `` directive renders a list of material tabs that can be used + * for top-level page navigation. Unlike ``, it has no concept of a tab + * body and no bar pagination. + * + * Because it deals with page navigation, certain routing concepts are built-in. + * Route changes via via ng-href, ui-sref, or ng-click events are supported. + * Alternatively, the user could simply watch currentNavItem for changes. + * + * 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 {string=} mdSelectedNavItem The name of the current tab; this must + * match the name attribute of `` + * @param {string=} navBarAriaLabel An aria-label for the nav-bar + * + * @usage + * + * + * Page One + * Page Two + * Page Three + * + * + * + * (function() { + * ‘use strict’; + * + * $rootScope.$on('$routeChangeSuccess', function(event, current) { + * $scope.currentLink = getCurrentLinkFromRoute(current); + * }); + * }); + * + */ - // Grab our element which stores CSS variables - var variablesElement = el.querySelector('._md-css-variables'); +/***************************************************************************** + * mdNavItem + *****************************************************************************/ +/** + * @ngdoc directive + * @name mdNavItem + * @module material.components.navBar + * + * @restrict E + * + * @description + * `` describes a page navigation link within the `` + * component. It renders an md-button as the actual link. + * + * Exactly one of the mdNavClick, mdNavHref, mdNavSref attributes are required to be + * specified. + * + * @param {Function=} mdNavClick Function which will be called when the + * link is clicked to change the page. Renders as an `ng-click`. + * @param {string=} mdNavHref url to transition to when this link is clicked. + * Renders as an `ng-href`. + * @param {string=} mdNavSref Ui-router state to transition to when this link is + * clicked. Renders as a `ui-sref`. + * @param {string=} name The name of this link. Used by the nav bar to know + * which link is currently selected. + * + * @usage + * See `` for usage. + */ - // Setup JS variables based on our CSS variables - var startZIndex = parseInt(window.getComputedStyle(variablesElement).zIndex); - // Always reset the items to their natural position/state - angular.forEach(items, function(item, index) { - var styles = item.style, - offsetDelay = index * delay; +/***************************************************************************** + * IMPLEMENTATION * + *****************************************************************************/ - styles.opacity = ctrl.isOpen ? 1 : 0; - styles.transform = styles.webkitTransform = ctrl.isOpen ? 'scale(1)' : 'scale(0)'; - styles.transitionDelay = (ctrl.isOpen ? offsetDelay : (items.length - offsetDelay)) + 'ms'; +function MdNavBar($mdAria) { + return { + restrict: 'E', + transclude: true, + controller: MdNavBarController, + controllerAs: 'ctrl', + bindToController: true, + scope: { + 'mdSelectedNavItem': '=?', + 'navBarAriaLabel': '@?', + }, + template: + '
' + + '' + + '' + + '
', + link: function(scope, element, attrs, ctrl) { + if (!ctrl.navBarAriaLabel) { + $mdAria.expectAsync(element, 'aria-label', angular.noop); + } + }, + }; +} +MdNavBar.$inject = ["$mdAria"]; - // Make the items closest to the trigger have the highest z-index - styles.zIndex = (items.length - index) + startZIndex; - }); - } +/** + * 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; - return { - addClass: function(element, className, done) { - runAnimation(element); - delayDone(done); - }, + /** @private @const {!angular.Scope} */ + this._$scope = $scope; - removeClass: function(element, className, done) { - runAnimation(element); - delayDone(done); - } - } - } - MdFabSpeedDialScaleAnimation.$inject = ["$timeout"]; -})(); + /** @private @const {!Object} */ + this._$mdConstant = $mdConstant; -})(); -(function(){ -"use strict"; + // Data-bound variables. + /** @type {string} */ + this.mdSelectedNavItem; -(function() { - 'use strict'; + /** @type {string} */ + this.navBarAriaLabel; - /** - * @ngdoc module - * @name material.components.fabToolbar - */ - angular - // Declare our module - .module('material.components.fabToolbar', [ - 'material.core', - 'material.components.fabShared', - 'material.components.fabTrigger', - 'material.components.fabActions' - ]) + // State variables. - // Register our directive - .directive('mdFabToolbar', MdFabToolbarDirective) + /** @type {?angular.JQLite} */ + this._navBarEl = $element[0]; - // Register our custom animations - .animation('.md-fab-toolbar', MdFabToolbarAnimation) + /** @type {?angular.JQLite} */ + this._inkbar; - // Register a service for the animation so that we can easily inject it into unit tests - .service('mdFabToolbarAnimation', MdFabToolbarAnimation); + 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(); + } + }); +} +MdNavBarController.$inject = ["$element", "$scope", "$timeout", "$mdConstant"]; - /** - * @ngdoc directive - * @name mdFabToolbar - * @module material.components.fabToolbar - * - * @restrict E - * - * @description - * - * The `` directive is used present a toolbar of elements (usually ``s) - * for quick access to common actions when a floating action button is activated (via click or - * keyboard navigation). - * - * You may also easily position the trigger by applying one one of the following classes to the - * `` element: - * - `md-fab-top-left` - * - `md-fab-top-right` - * - `md-fab-bottom-left` - * - `md-fab-bottom-right` - * - * These CSS classes use `position: absolute`, so you need to ensure that the container element - * also uses `position: absolute` or `position: relative` in order for them to work. - * - * @usage - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * @param {string} md-direction From which direction you would like the toolbar items to appear - * relative to the trigger element. Supports `left` and `right` directions. - * @param {expression=} md-open Programmatically control whether or not the toolbar is visible. - */ - function MdFabToolbarDirective() { - return { - restrict: 'E', - transclude: true, - template: '
' + - '
' + - '
', - scope: { - direction: '@?mdDirection', - isOpen: '=?mdOpen' - }, - bindToController: true, - controller: 'MdFabController', - controllerAs: 'vm', +/** + * Initializes the tab components once they exist. + * @private + */ +MdNavBarController.prototype._initTabs = function() { + this._inkbar = angular.element(this._navBarEl.getElementsByTagName('md-nav-ink-bar')[0]); - link: link - }; + var self = this; + this._$timeout(function() { + self._updateTabs(self.mdSelectedNavItem, undefined); + }); - function link(scope, element, attributes) { - // Add the base class for animations - element.addClass('md-fab-toolbar'); + 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); + }); + }); +}; - // Prepend the background element to the trigger's button - element.find('md-fab-trigger').find('button') - .prepend('
'); +/** + * 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 tabs = this._getTabs(); + + var oldIndex; + if (oldValue) { + var oldTab = this._getTabByName(oldValue); + if (oldTab) { + oldTab.setSelected(false); + oldIndex = tabs.indexOf(oldTab); } } - function MdFabToolbarAnimation() { - - function runAnimation(element, className, done) { - // If no className was specified, don't do anything - if (!className) { - return; - } + if (newValue) { + var tab = this._getTabByName(newValue); + if (tab) { + tab.setSelected(true); + var newIndex = tabs.indexOf(tab); + var self = this; + this._$timeout(function() { + self._updateInkBarStyles(tab, newIndex, oldIndex); + }); + } + } +}; - var el = element[0]; - var ctrl = element.controller('mdFabToolbar'); +/** + * Repositions the ink bar to the selected tab. + * @private + */ +MdNavBarController.prototype._updateInkBarStyles = function(tab, newIndex, oldIndex) { + var tabEl = tab.getButtonEl(); + var left = tabEl.offsetLeft; - // Grab the relevant child elements - var backgroundElement = el.querySelector('._md-fab-toolbar-background'); - var triggerElement = el.querySelector('md-fab-trigger button'); - var toolbarElement = el.querySelector('md-toolbar'); - var iconElement = el.querySelector('md-fab-trigger button md-icon'); - var actions = element.find('md-fab-actions').children(); + this._inkbar.toggleClass('_md-left', newIndex < oldIndex) + .toggleClass('_md-right', newIndex > oldIndex); + this._inkbar.css({left: left + 'px', width: tabEl.offsetWidth + 'px'}); +}; - // If we have both elements, use them to position the new background - if (triggerElement && backgroundElement) { - // Get our variables - var color = window.getComputedStyle(triggerElement).getPropertyValue('background-color'); - var width = el.offsetWidth; - var height = el.offsetHeight; +/** + * Returns an array of the current tabs. + * @return {!Array} + * @private + */ +MdNavBarController.prototype._getTabs = function() { + var linkArray = Array.prototype.slice.call( + this._navBarEl.querySelectorAll('.md-nav-item')); + return linkArray.map(function(el) { + return angular.element(el).controller('mdNavItem') + }); +}; - // Make it twice as big as it should be since we scale from the center - var scale = 2 * (width / triggerElement.offsetWidth); +/** + * 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; + }); +}; - // Set some basic styles no matter what animation we're doing - backgroundElement.style.backgroundColor = color; - backgroundElement.style.borderRadius = width + 'px'; +/** + * Returns the selected tab. + * @return {!NavItemController|undefined} + * @private + */ +MdNavBarController.prototype._getSelectedTab = function() { + return this._findTab(function(tab) { + return tab.isSelected() + }); +}; - // If we're open - if (ctrl.isOpen) { - // Turn on toolbar pointer events when closed - toolbarElement.style.pointerEvents = 'initial'; +/** + * Returns the focused tab. + * @return {!NavItemController|undefined} + */ +MdNavBarController.prototype.getFocusedTab = function() { + return this._findTab(function(tab) { + return tab.hasFocus() + }); +}; - backgroundElement.style.width = triggerElement.offsetWidth + 'px'; - backgroundElement.style.height = triggerElement.offsetHeight + 'px'; - backgroundElement.style.transform = 'scale(' + scale + ')'; +/** + * 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]; + } + } - // Set the next close animation to have the proper delays - backgroundElement.style.transitionDelay = '0ms'; - iconElement && (iconElement.style.transitionDelay = '.3s'); + return null; +}; - // Apply a transition delay to actions - angular.forEach(actions, function(action, index) { - action.style.transitionDelay = (actions.length - index) * 25 + 'ms'; - }); - } else { - // Turn off toolbar pointer events when closed - toolbarElement.style.pointerEvents = 'none'; +/** + * 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); + } +}; - // Scale it back down to the trigger's size - backgroundElement.style.transform = 'scale(1)'; +/** + * Clear tab focus when focus leaves the nav bar. + */ +MdNavBarController.prototype.onBlur = function() { + var tab = this.getFocusedTab(); + if (tab) { + tab.setFocused(false); + } +}; - // Reset the position - backgroundElement.style.top = '0'; +/** + * 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); +}; - if (element.hasClass('md-right')) { - backgroundElement.style.left = '0'; - backgroundElement.style.right = null; - } +/** + * 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; - if (element.hasClass('md-left')) { - backgroundElement.style.right = '0'; - backgroundElement.style.left = null; - } + var focusedTabIndex = tabs.indexOf(focusedTab); - // Set the next open animation to have the proper delays - backgroundElement.style.transitionDelay = '200ms'; - iconElement && (iconElement.style.transitionDelay = '0ms'); + // 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; + } +}; - // Apply a transition delay to actions - angular.forEach(actions, function(action, index) { - action.style.transitionDelay = 200 + (index * 25) + 'ms'; - }); +/** + * @ngInject + */ +function MdNavItem($$rAF) { + return { + restrict: 'E', + require: ['mdNavItem', '^mdNavBar'], + controller: MdNavItemController, + bindToController: true, + controllerAs: 'ctrl', + replace: true, + transclude: true, + template: + '
  • ' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
  • ', + scope: { + 'mdNavClick': '&?', + 'mdNavHref': '@?', + 'mdNavSref': '@?', + 'name': '@', + }, + link: function(scope, element, attrs, controllers) { + var mdNavItem = controllers[0]; + var mdNavBar = controllers[1]; + + // 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() { + if (!mdNavItem.name) { + mdNavItem.name = angular.element(element[0].querySelector('._md-nav-button-text')) + .text().trim(); } - } + + var navButton = angular.element(element[0].querySelector('._md-nav-button')); + navButton.on('click', function() { + mdNavBar.mdSelectedNavItem = mdNavItem.name; + scope.$apply(); + }); + }); } + }; +} +MdNavItem.$inject = ["$$rAF"]; - return { - addClass: function(element, className, done) { - runAnimation(element, className, done); - done(); - }, +/** + * Controller for the nav-item component. + * @param {!angular.JQLite} $element + * @constructor + * @final + * @ngInject + */ +function MdNavItemController($element) { - removeClass: function(element, className, done) { - runAnimation(element, className, done); - done(); - } - } - } -})(); -})(); -(function(){ -"use strict"; + /** @private @const {!angular.JQLite} */ + this._$element = $element; -(function() { - 'use strict'; + // Data-bound variables + /** @const {?Function} */ + this.mdNavClick; + /** @const {?string} */ + this.mdNavHref; + /** @const {?string} */ + this.name; - /** - * @ngdoc module - * @name material.components.fabTrigger - */ - angular - .module('material.components.fabTrigger', ['material.core']) - .directive('mdFabTrigger', MdFabTriggerDirective); + // State variables + /** @private {boolean} */ + this._selected = false; - /** - * @ngdoc directive - * @name mdFabTrigger - * @module material.components.fabSpeedDial - * - * @restrict E - * - * @description - * The `` directive is used inside of a `` or - * `` directive to mark an element (or elements) as the trigger and setup the - * proper event listeners. - * - * @usage - * See the `` or `` directives for example usage. - */ - function MdFabTriggerDirective() { - // TODO: Remove this completely? - return { - restrict: 'E', + /** @private {boolean} */ + this._focused = false; - require: ['^?mdFabSpeedDial', '^?mdFabToolbar'] - }; + var hasNavClick = !!($element.attr('md-nav-click')); + var hasNavHref = !!($element.attr('md-nav-href')); + var hasNavSref = !!($element.attr('md-nav-sref')); + + // Cannot specify more than one nav attribute + if ((hasNavClick ? 1:0) + (hasNavHref ? 1:0) + (hasNavSref ? 1:0) > 1) { + throw Error( + 'Must specify exactly one of md-nav-click, md-nav-href, ' + + 'md-nav-sref for nav-item directive'); } -})(); +} +MdNavItemController.$inject = ["$element"]; +/** + * 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, + }; +}; -})(); -(function(){ -"use strict"; +/** + * Get the name attribute of the tab. + * @return {string} + */ +MdNavItemController.prototype.getName = function() { + return this.name; +}; /** - * @ngdoc module - * @name material.components.gridList + * Get the button element associated with the tab. + * @return {!Element} */ -angular.module('material.components.gridList', ['material.core']) - .directive('mdGridList', GridListDirective) - .directive('mdGridTile', GridTileDirective) - .directive('mdGridTileFooter', GridTileCaptionDirective) - .directive('mdGridTileHeader', GridTileCaptionDirective) - .factory('$mdGridLayout', GridLayoutFactory); +MdNavItemController.prototype.getButtonEl = function() { + return this._$element[0].querySelector('._md-nav-button'); +}; /** - * @ngdoc directive - * @name mdGridList - * @module material.components.gridList - * @restrict E - * @description - * Grid lists are an alternative to standard list views. Grid lists are distinct - * from grids used for layouts and other visual presentations. - * - * A grid list is best suited to presenting a homogenous data type, typically - * images, and is optimized for visual comprehension and differentiating between - * like data types. - * - * A grid list is a continuous element consisting of tessellated, regular - * subdivisions called cells that contain tiles (`md-grid-tile`). - * - * Concept of grid explained visually - * Grid concepts legend - * - * Cells are arrayed vertically and horizontally within the grid. - * - * Tiles hold content and can span one or more cells vertically or horizontally. - * - * ### Responsive Attributes - * - * The `md-grid-list` directive supports "responsive" attributes, which allow - * different `md-cols`, `md-gutter` and `md-row-height` values depending on the - * currently matching media query. - * - * In order to set a responsive attribute, first define the fallback value with - * the standard attribute name, then add additional attributes with the - * following convention: `{base-attribute-name}-{media-query-name}="{value}"` - * (ie. `md-cols-lg="8"`) - * - * @param {number} md-cols Number of columns in the grid. - * @param {string} md-row-height One of - *
      - *
    • CSS length - Fixed height rows (eg. `8px` or `1rem`)
    • - *
    • `{width}:{height}` - Ratio of width to height (eg. - * `md-row-height="16:9"`)
    • - *
    • `"fit"` - Height will be determined by subdividing the available - * height by the number of rows
    • - *
    - * @param {string=} md-gutter The amount of space between tiles in CSS units - * (default 1px) - * @param {expression=} md-on-layout Expression to evaluate after layout. Event - * object is available as `$event`, and contains performance information. - * - * @usage - * Basic: - * - * - * - * - * - * - * Fixed-height rows: - * - * - * - * - * - * - * Fit rows: - * - * - * - * - * - * - * Using responsive attributes: - * - * - * - * - * + * Set the selected state of the tab. + * @param {boolean} isSelected */ -function GridListDirective($interpolate, $mdConstant, $mdGridLayout, $mdMedia) { - return { - restrict: 'E', - controller: GridListController, - scope: { - mdOnLayout: '&' - }, - link: postLink - }; +MdNavItemController.prototype.setSelected = function(isSelected) { + this._selected = isSelected; +}; + +/** + * @return {boolean} + */ +MdNavItemController.prototype.isSelected = function() { + return this._selected; +}; + +/** + * Set the focused state of the tab. + * @param {boolean} isFocused + */ +MdNavItemController.prototype.setFocused = function(isFocused) { + this._focused = isFocused; +}; + +/** + * @return {boolean} + */ +MdNavItemController.prototype.hasFocus = function() { + return this._focused; +}; + +})(); +(function(){ +"use strict"; - function postLink(scope, element, attrs, ctrl) { - // Apply semantics - element.attr('role', 'list'); +/** + * @ngdoc module + * @name material.components.panel + */ +angular + .module('material.components.panel', [ + 'material.core', + 'material.components.backdrop' + ]) + .service('$mdPanel', MdPanelService); - // Provide the controller with a way to trigger layouts. - ctrl.layoutDelegate = layoutDelegate; - var invalidateLayout = angular.bind(ctrl, ctrl.invalidateLayout), - unwatchAttrs = watchMedia(); - scope.$on('$destroy', unwatchMedia); +/***************************************************************************** + * PUBLIC DOCUMENTATION * + *****************************************************************************/ - /** - * Watches for changes in media, invalidating layout as necessary. - */ - function watchMedia() { - for (var mediaName in $mdConstant.MEDIA) { - $mdMedia(mediaName); // initialize - $mdMedia.getQuery($mdConstant.MEDIA[mediaName]) - .addListener(invalidateLayout); - } - return $mdMedia.watchResponsiveAttributes( - ['md-cols', 'md-row-height', 'md-gutter'], attrs, layoutIfMediaMatch); - } +/** + * @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 = $mdPanelPosition + * .absolute() + * .top('50%') + * .left('50%'); + * + * var panelAnimation = $mdPanelAnimation + * .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, + * template: 'dialog-template.html', + * clickOutsideToClose: true, + * escapeToClose: true, + * focusOnOpen: true + * } + * panelRef = $mdPanel.create(config); + * panelRef.open() + * .finally(function() { + * panelRef = undefined; + * }); + * } + * + * function DialogController(MdPanelRef, toppings) { + * var toppings; + * + * function closeDialog() { + * MdPanelRef.close(); + * } + * } + * })(angular); + * + */ - function unwatchMedia() { - ctrl.layoutDelegate = angular.noop; +/** + * @ngdoc method + * @name $mdPanel#create + * @description + * Creates a panel with the specified options. + * + * @param opt_config {Object=} Specific configuration object that may contain + * the following properties: + * + * - `template` - `{string=}`: HTML template to show in the dialog. 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. + * - `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. + * - `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. + * - `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. + * + * TODO(ErinCoughlan): Add the following config options. + * - `groupName` - `{string=}`: Name of panel groups. This group name is + * used for configuring the number of open panels and identifying specific + * behaviors for groups. For instance, all tooltips will be identified + * using the same groupName. + * + * @returns {MdPanelRef} panelRef + */ - unwatchAttrs(); - for (var mediaName in $mdConstant.MEDIA) { - $mdMedia.getQuery($mdConstant.MEDIA[mediaName]) - .removeListener(invalidateLayout); - } - } - /** - * Performs grid layout if the provided mediaName matches the currently - * active media type. - */ - function layoutIfMediaMatch(mediaName) { - if (mediaName == null) { - // TODO(shyndman): It would be nice to only layout if we have - // instances of attributes using this media type - ctrl.invalidateLayout(); - } else if ($mdMedia(mediaName)) { - ctrl.invalidateLayout(); - } - } +/** + * @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=} opt_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. + */ - var lastLayoutProps; - /** - * Invokes the layout engine, and uses its results to lay out our - * tile elements. - * - * @param {boolean} tilesInvalidated Whether tiles have been - * added/removed/moved since the last layout. This is to avoid situations - * where tiles are replaced with properties identical to their removed - * counterparts. - */ - function layoutDelegate(tilesInvalidated) { - var tiles = getTileElements(); - var props = { - tileSpans: getTileSpans(tiles), - colCount: getColumnCount(), - rowMode: getRowMode(), - rowHeight: getRowHeight(), - gutter: getGutter() - }; +/** + * @ngdoc method + * @name $mdPanel#setGroupMaxOpen + * @description + * Sets the maximum number of panels in a group that can be opened at a given + * time. + * + * @param groupName {string} The name of the group to configure. + * @param maxOpen {number} The max number of panels that can be opened. + */ - if (!tilesInvalidated && angular.equals(props, lastLayoutProps)) { - return; - } - var performance = - $mdGridLayout(props.colCount, props.tileSpans, tiles) - .map(function(tilePositions, rowCount) { - return { - grid: { - element: element, - style: getGridStyle(props.colCount, rowCount, - props.gutter, props.rowMode, props.rowHeight) - }, - tiles: tilePositions.map(function(ps, i) { - return { - element: angular.element(tiles[i]), - style: getTileStyle(ps.position, ps.spans, - props.colCount, rowCount, - props.gutter, props.rowMode, props.rowHeight) - } - }) - } - }) - .reflow() - .performance(); +/** + * @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 + */ - // Report layout - scope.mdOnLayout({ - $event: { - performance: performance - } - }); - lastLayoutProps = props; - } +/** + * @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 + */ - // Use $interpolate to do some simple string interpolation as a convenience. - var startSymbol = $interpolate.startSymbol(); - var endSymbol = $interpolate.endSymbol(); +/***************************************************************************** + * MdPanelRef * + *****************************************************************************/ - // Returns an expression wrapped in the interpolator's start and end symbols. - function expr(exprStr) { - return startSymbol + exprStr + endSymbol; - } - // The amount of space a single 1x1 tile would take up (either width or height), used as - // a basis for other calculations. This consists of taking the base size percent (as would be - // if evenly dividing the size between cells), and then subtracting the size of one gutter. - // However, since there are no gutters on the edges, each tile only uses a fration - // (gutterShare = numGutters / numCells) of the gutter size. (Imagine having one gutter per - // tile, and then breaking up the extra gutter on the edge evenly among the cells). - var UNIT = $interpolate(expr('share') + '% - (' + expr('gutter') + ' * ' + expr('gutterShare') + ')'); +/** + * @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. + */ - // The horizontal or vertical position of a tile, e.g., the 'top' or 'left' property value. - // The position comes the size of a 1x1 tile plus gutter for each previous tile in the - // row/column (offset). - var POSITION = $interpolate('calc((' + expr('unit') + ' + ' + expr('gutter') + ') * ' + expr('offset') + ')'); +/** + * @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. + */ - // The actual size of a tile, e.g., width or height, taking rowSpan or colSpan into account. - // This is computed by multiplying the base unit by the rowSpan/colSpan, and then adding back - // in the space that the gutter would normally have used (which was already accounted for in - // the base unit calculation). - var DIMENSION = $interpolate('calc((' + expr('unit') + ') * ' + expr('span') + ' + (' + expr('span') + ' - 1) * ' + expr('gutter') + ')'); +/** + * @ngdoc method + * @name MdPanelRef#close + * @description + * Hides and detaches the panel. + * + * @returns {!angular.$q.Promise} A promise that is resolved when the panel is + * closed. + */ - /** - * Gets the styles applied to a tile element described by the given parameters. - * @param {{row: number, col: number}} position The row and column indices of the tile. - * @param {{row: number, col: number}} spans The rowSpan and colSpan of the tile. - * @param {number} colCount The number of columns. - * @param {number} rowCount The number of rows. - * @param {string} gutter The amount of space between tiles. This will be something like - * '5px' or '2em'. - * @param {string} rowMode The row height mode. Can be one of: - * 'fixed': all rows have a fixed size, given by rowHeight, - * 'ratio': row height defined as a ratio to width, or - * 'fit': fit to the grid-list element height, divinding evenly among rows. - * @param {string|number} rowHeight The height of a row. This is only used for 'fixed' mode and - * for 'ratio' mode. For 'ratio' mode, this is the *ratio* of width-to-height (e.g., 0.75). - * @returns {Object} Map of CSS properties to be applied to the style element. Will define - * values for top, left, width, height, marginTop, and paddingTop. - */ - function getTileStyle(position, spans, colCount, rowCount, gutter, rowMode, rowHeight) { - // TODO(shyndman): There are style caching opportunities here. +/** + * @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. + */ - // Percent of the available horizontal space that one column takes up. - var hShare = (1 / colCount) * 100; +/** + * @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. + */ - // Fraction of the gutter size that each column takes up. - var hGutterShare = (colCount - 1) / colCount; +/** + * @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. + */ - // Base horizontal size of a column. - var hUnit = UNIT({share: hShare, gutterShare: hGutterShare, gutter: gutter}); +/** + * @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. + */ - // The width and horizontal position of each tile is always calculated the same way, but the - // height and vertical position depends on the rowMode. - var style = { - left: POSITION({ unit: hUnit, offset: position.col, gutter: gutter }), - width: DIMENSION({ unit: hUnit, span: spans.col, gutter: gutter }), - // resets - paddingTop: '', - marginTop: '', - top: '', - height: '' - }; +/** + * @ngdoc method + * @name MdPanelRef#destroy + * @description + * Destroys the panel. The panel cannot be opened again after this is called. + */ - switch (rowMode) { - case 'fixed': - // In fixed mode, simply use the given rowHeight. - style.top = POSITION({ unit: rowHeight, offset: position.row, gutter: gutter }); - style.height = DIMENSION({ unit: rowHeight, span: spans.row, gutter: gutter }); - break; +/** + * @ngdoc method + * @name MdPanelRef#addClass + * @description + * Adds a class to the panel. DO NOT use this to hide/show the panel. + * + * @param {string} newClass Class to be added. + */ + +/** + * @ngdoc method + * @name MdPanelRef#removeClass + * @description + * Removes a class from the panel. DO NOT use this to hide/show the panel. + * + * @param {string} oldClass Class to be removed. + */ - case 'ratio': - // Percent of the available vertical space that one row takes up. Here, rowHeight holds - // the ratio value. For example, if the width:height ratio is 4:3, rowHeight = 1.333. - var vShare = hShare / rowHeight; +/** + * @ngdoc method + * @name MdPanelRef#toggleClass + * @description + * Toggles a class on the panel. DO NOT use this to hide/show the panel. + * + * @param {string} toggleClass Class to be toggled. + */ - // Base veritcal size of a row. - var vUnit = UNIT({ share: vShare, gutterShare: hGutterShare, gutter: gutter }); +/** + * @ngdoc method + * @name MdPanelRef#focusOnOpen + * @description + * Focuses the panel content if the focusOnOpen config value is true. + */ - // padidngTop and marginTop are used to maintain the given aspect ratio, as - // a percentage-based value for these properties is applied to the *width* of the - // containing block. See http://www.w3.org/TR/CSS2/box.html#margin-properties - style.paddingTop = DIMENSION({ unit: vUnit, span: spans.row, gutter: gutter}); - style.marginTop = POSITION({ unit: vUnit, offset: position.row, gutter: gutter }); - break; - case 'fit': - // Fraction of the gutter size that each column takes up. - var vGutterShare = (rowCount - 1) / rowCount; +/***************************************************************************** + * MdPanelPosition * + *****************************************************************************/ - // Percent of the available vertical space that one row takes up. - var vShare = (1 / rowCount) * 100; - // Base vertical size of a row. - var vUnit = UNIT({share: vShare, gutterShare: vGutterShare, gutter: gutter}); +/** + * @ngdoc type + * @name MdPanelPosition + * @module material.components.panel + * @description + * Object for configuring the position of the panel. Examples: + * + * 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); + */ - style.top = POSITION({unit: vUnit, offset: position.row, gutter: gutter}); - style.height = DIMENSION({unit: vUnit, span: spans.row, gutter: gutter}); - break; - } +/** + * @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} + */ - return style; - } +/** + * @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} + */ - function getGridStyle(colCount, rowCount, gutter, rowMode, rowHeight) { - var style = {}; +/** + * @ngdoc method + * @name MdPanelPosition#top + * @description + * Sets the value of `top` for the panel. Clears any previously set + * vertical position. + * @param {string=} opt_top Value of `top`. Defaults to '0'. + * @returns {MdPanelPosition} + */ - switch(rowMode) { - case 'fixed': - style.height = DIMENSION({ unit: rowHeight, span: rowCount, gutter: gutter }); - style.paddingBottom = ''; - break; +/** + * @ngdoc method + * @name MdPanelPosition#bottom + * @description + * Sets the value of `bottom` for the panel. Clears any previously set + * vertical position. + * @param {string=} opt_bottom Value of `bottom`. Defaults to '0'. + * @returns {MdPanelPosition} + */ - case 'ratio': - // rowHeight is width / height - var hGutterShare = colCount === 1 ? 0 : (colCount - 1) / colCount, - hShare = (1 / colCount) * 100, - vShare = hShare * (1 / rowHeight), - vUnit = UNIT({ share: vShare, gutterShare: hGutterShare, gutter: gutter }); +/** + * @ngdoc method + * @name MdPanelPosition#left + * @description + * Sets the value of `left` for the panel. Clears any previously set + * horizontal position. + * @param {string=} opt_left Value of `left`. Defaults to '0'. + * @returns {MdPanelPosition} + */ - style.height = ''; - style.paddingBottom = DIMENSION({ unit: vUnit, span: rowCount, gutter: gutter}); - break; +/** + * @ngdoc method + * @name MdPanelPosition#right + * @description + * Sets the value of `right` for the panel. Clears any previously set + * horizontal position. + * @param {string=} opt_right Value of `right`. Defaults to '0'. + * @returns {MdPanelPosition} + */ - case 'fit': - // noop, as the height is user set - break; - } +/** + * @ngdoc method + * @name MdPanelPosition#centerHorizontally + * @description + * Centers the panel horizontally in the viewport. Clears any previously set + * horizontal position. + * @returns {MdPanelPosition} + */ - return style; - } +/** + * @ngdoc method + * @name MdPanelPosition#centerVertically + * @description + * Centers the panel vertically in the viewport. Clears any previously set + * vertical position. + * @returns {MdPanelPosition} + */ - function getTileElements() { - return [].filter.call(element.children(), function(ele) { - return ele.tagName == 'MD-GRID-TILE' && !ele.$$mdDestroyed; - }); - } +/** + * @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} + */ - /** - * Gets an array of objects containing the rowspan and colspan for each tile. - * @returns {Array<{row: number, col: number}>} - */ - function getTileSpans(tileElements) { - return [].map.call(tileElements, function(ele) { - var ctrl = angular.element(ele).controller('mdGridTile'); - return { - row: parseInt( - $mdMedia.getResponsiveAttribute(ctrl.$attrs, 'md-rowspan'), 10) || 1, - col: parseInt( - $mdMedia.getResponsiveAttribute(ctrl.$attrs, 'md-colspan'), 10) || 1 - }; - }); - } +/** + * @ngdoc method + * @name MdPanelPosition#addPanelPosition + * @param {string} xPosition + * @param {string} yPosition + * @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 + * @returns {MdPanelPosition} + */ - function getColumnCount() { - var colCount = parseInt($mdMedia.getResponsiveAttribute(attrs, 'md-cols'), 10); - if (isNaN(colCount)) { - throw 'md-grid-list: md-cols attribute was not found, or contained a non-numeric value'; - } - return colCount; - } +/** + * @ngdoc method + * @name MdPanelPosition#withOffsetX + * @description + * Sets the value of the offset in the x-direction. + * @param {string} offsetX + * @returns {MdPanelPosition} + */ - function getGutter() { - return applyDefaultUnit($mdMedia.getResponsiveAttribute(attrs, 'md-gutter') || 1); - } +/** + * @ngdoc method + * @name MdPanelPosition#withOffsetY + * @description + * Sets the value of the offset in the y-direction. + * @param {string} offsetY + * @returns {MdPanelPosition} + */ - function getRowHeight() { - var rowHeight = $mdMedia.getResponsiveAttribute(attrs, 'md-row-height'); - if (!rowHeight) { - throw 'md-grid-list: md-row-height attribute was not found'; - } - switch (getRowMode()) { - case 'fixed': - return applyDefaultUnit(rowHeight); - case 'ratio': - var whRatio = rowHeight.split(':'); - return parseFloat(whRatio[0]) / parseFloat(whRatio[1]); - case 'fit': - return 0; // N/A - } - } +/***************************************************************************** + * MdPanelAnimation * + *****************************************************************************/ - function getRowMode() { - var rowHeight = $mdMedia.getResponsiveAttribute(attrs, 'md-row-height'); - if (!rowHeight) { - throw 'md-grid-list: md-row-height attribute was not found'; - } - if (rowHeight == 'fit') { - return 'fit'; - } else if (rowHeight.indexOf(':') !== -1) { - return 'ratio'; - } else { - return 'fixed'; - } - } +/** + * @ngdoc object + * @name MdPanelAnimation + * @description + * Animation configuration object. To use, create an MdPanelAnimation with the + * desired properties, then pass the object as part of $mdPanel creation. + * + * Example: + * + * var panelAnimation = new MdPanelAnimation() + * .openFrom(myButtonEl) + * .closeTo('.my-button') + * .withAnimation($mdPanel.animation.SCALE); + * + * $mdPanel.create({ + * animation: panelAnimation + * }); + */ - function applyDefaultUnit(val) { - return /\D$/.test(val) ? val : val + 'px'; - } - } -} -GridListDirective.$inject = ["$interpolate", "$mdConstant", "$mdGridLayout", "$mdMedia"]; +/** + * @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} + */ -/* @ngInject */ -function GridListController($mdUtil) { - this.layoutInvalidated = false; - this.tilesInvalidated = false; - this.$timeout_ = $mdUtil.nextTick; - this.layoutDelegate = angular.noop; -} -GridListController.$inject = ["$mdUtil"]; +/** + * @ngdoc method + * @name MdPanelAnimation#closeTo + * @description + * Specifies where to animate the dialog 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} + */ -GridListController.prototype = { - invalidateTiles: function() { - this.tilesInvalidated = true; - this.invalidateLayout(); - }, +/** + * @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} + */ - invalidateLayout: function() { - if (this.layoutInvalidated) { - return; - } - this.layoutInvalidated = true; - this.$timeout_(angular.bind(this, this.layout)); - }, - layout: function() { - try { - this.layoutDelegate(this.tilesInvalidated); - } finally { - this.layoutInvalidated = false; - this.tilesInvalidated = false; - } - } -}; +/***************************************************************************** + * IMPLEMENTATION * + *****************************************************************************/ -/* @ngInject */ -function GridLayoutFactory($mdUtil) { - var defaultAnimator = GridTileAnimator; +// Default z-index for the panel. +var defaultZIndex = 80; +var MD_PANEL_HIDDEN = '_md-panel-hidden'; - /** - * Set the reflow animator callback - */ - GridLayout.animateWith = function(customAnimator) { - defaultAnimator = !angular.isFunction(customAnimator) ? GridTileAnimator : customAnimator; - }; +var FOCUS_TRAP_TEMPLATE = angular.element( + '
    '); - return GridLayout; +/** + * A service that is used for controlling/displaying panels on the screen. + * @param {!angular.JQLite} $rootElement + * @param {!angular.Scope} $rootScope + * @param {!angular.$injector} $injector + * @param {!angular.$window} $window + * @final @constructor @ngInject + */ +function MdPanelService($rootElement, $rootScope, $injector, $window) { /** - * Publish layout function + * 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} */ - function GridLayout(colCount, tileSpans) { - var self, layoutInfo, gridStyles, layoutTime, mapTime, reflowTime; - - layoutTime = $mdUtil.time(function() { - layoutInfo = calculateGridFor(colCount, tileSpans); - }); - - return self = { + this._defaultConfigOptions = { + bindToController: true, + clickOutsideToClose: false, + disableParentScroll: false, + escapeToClose: false, + focusOnOpen: true, + fullscreen: false, + hasBackdrop: false, + transformTemplate: angular.bind(this, this._wrapTemplate), + trapFocus: false, + zIndex: defaultZIndex + }; - /** - * An array of objects describing each tile's position in the grid. - */ - layoutInfo: function() { - return layoutInfo; - }, + /** @private {!Object} */ + this._config = {}; - /** - * Maps grid positioning to an element and a set of styles using the - * provided updateFn. - */ - map: function(updateFn) { - mapTime = $mdUtil.time(function() { - var info = self.layoutInfo(); - gridStyles = updateFn(info.positioning, info.rowCount); - }); - return self; - }, + /** @private @const */ + this._$rootElement = $rootElement; - /** - * Default animator simply sets the element.css( ). An alternate - * animator can be provided as an argument. The function has the following - * signature: - * - * function({grid: {element: JQLite, style: Object}, tiles: Array<{element: JQLite, style: Object}>) - */ - reflow: function(animatorFn) { - reflowTime = $mdUtil.time(function() { - var animator = animatorFn || defaultAnimator; - animator(gridStyles.grid, gridStyles.tiles); - }); - return self; - }, + /** @private @const */ + this._$rootScope = $rootScope; + + /** @private @const */ + this._$injector = $injector; + + /** @private @const */ + this._$window = $window; - /** - * Timing for the most recent layout run. - */ - performance: function() { - return { - tileCount: tileSpans.length, - layoutTime: layoutTime, - mapTime: mapTime, - reflowTime: reflowTime, - totalTime: layoutTime + mapTime + reflowTime - }; - } - }; - } /** - * Default Gridlist animator simple sets the css for each element; - * NOTE: any transitions effects must be manually set in the CSS. - * e.g. - * - * md-grid-tile { - * transition: all 700ms ease-out 50ms; - * } - * + * Default animations that can be used within the panel. + * @type {enum} */ - function GridTileAnimator(grid, tiles) { - grid.element.css(grid.style); - tiles.forEach(function(t) { - t.element.css(t.style); - }) - } + this.animation = MdPanelAnimation.animation; /** - * Calculates the positions of tiles. - * - * The algorithm works as follows: - * An Array with length colCount (spaceTracker) keeps track of - * available tiling positions, where elements of value 0 represents an - * empty position. Space for a tile is reserved by finding a sequence of - * 0s with length <= than the tile's colspan. When such a space has been - * found, the occupied tile positions are incremented by the tile's - * rowspan value, as these positions have become unavailable for that - * many rows. - * - * If the end of a row has been reached without finding space for the - * tile, spaceTracker's elements are each decremented by 1 to a minimum - * of 0. Rows are searched in this fashion until space is found. + * Possible values of xPosition for positioning the panel relative to + * another element. + * @type {enum} */ - function calculateGridFor(colCount, tileSpans) { - var curCol = 0, - curRow = 0, - spaceTracker = newSpaceTracker(); + this.xPosition = MdPanelPosition.xPosition; - return { - positioning: tileSpans.map(function(spans, i) { - return { - spans: spans, - position: reserveSpace(spans, i) - }; - }), - rowCount: curRow + Math.max.apply(Math, spaceTracker) - }; + /** + * Possible values of yPosition for positioning the panel relative to + * another element. + * @type {enum} + */ + this.yPosition = MdPanelPosition.yPosition; +} +MdPanelService.$inject = ["$rootElement", "$rootScope", "$injector", "$window"]; - function reserveSpace(spans, i) { - if (spans.col > colCount) { - throw 'md-grid-list: Tile at position ' + i + ' has a colspan ' + - '(' + spans.col + ') that exceeds the column count ' + - '(' + colCount + ')'; - } - var start = 0, - end = 0; +/** + * Creates a panel with the specified options. + * @param {!Object=} opt_config Configuration object for the panel. + * @returns {!MdPanelRef} + */ +MdPanelService.prototype.create = function(opt_config) { + var configSettings = opt_config || {}; - // TODO(shyndman): This loop isn't strictly necessary if you can - // determine the minimum number of rows before a space opens up. To do - // this, recognize that you've iterated across an entire row looking for - // space, and if so fast-forward by the minimum rowSpan count. Repeat - // until the required space opens up. - while (end - start < spans.col) { - if (curCol >= colCount) { - nextRow(); - continue; - } + this._config = { + scope: this._$rootScope.$new(true), + attachTo: this._$rootElement + }; + angular.extend(this._config, this._defaultConfigOptions, configSettings); - start = spaceTracker.indexOf(0, curCol); - if (start === -1 || (end = findEnd(start + 1)) === -1) { - start = end = 0; - nextRow(); - continue; - } + var instanceId = 'panel_' + this._$injector.get('$mdUtil').nextUid(); + var instanceConfig = angular.extend({ id: instanceId }, this._config); - curCol = end + 1; - } + return new MdPanelRef(instanceConfig, this._$injector); +}; - adjustRow(start, spans.col, spans.row); - curCol = start + spans.col; - return { - col: start, - row: curRow - }; - } +/** + * Creates and opens a panel with the specified options. + * @param {!Object=} opt_config Configuration object for the panel. + * @returns {!angular.$q.Promise} The panel created from create. + */ +MdPanelService.prototype.open = function(opt_config) { + var panelRef = this.create(opt_config); + return panelRef.open().then(function() { + return panelRef; + }); +}; - function nextRow() { - curCol = 0; - curRow++; - adjustRow(0, colCount, -1); // Decrement row spans by one - } - function adjustRow(from, cols, by) { - for (var i = from; i < from + cols; i++) { - spaceTracker[i] = Math.max(spaceTracker[i] + by, 0); - } - } +/** + * Returns a new instance of the MdPanelPosition. Use this to create the + * positioning object. + * + * @returns {MdPanelPosition} + */ +MdPanelService.prototype.newPanelPosition = function() { + return new MdPanelPosition(this._$window); +}; - function findEnd(start) { - var i; - for (i = start; i < spaceTracker.length; i++) { - if (spaceTracker[i] !== 0) { - return i; - } - } - if (i === spaceTracker.length) { - return i; - } - } +/** + * 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); +}; - function newSpaceTracker() { - var tracker = []; - for (var i = 0; i < colCount; i++) { - tracker.push(0); - } - return tracker; - } - } -} -GridLayoutFactory.$inject = ["$mdUtil"]; /** - * @ngdoc directive - * @name mdGridTile - * @module material.components.gridList - * @restrict E - * @description - * Tiles contain the content of an `md-grid-list`. They span one or more grid - * cells vertically or horizontally, and use `md-grid-tile-{footer,header}` to - * display secondary content. - * - * ### Responsive Attributes - * - * The `md-grid-tile` directive supports "responsive" attributes, which allow - * different `md-rowspan` and `md-colspan` values depending on the currently - * matching media query. - * - * In order to set a responsive attribute, first define the fallback value with - * the standard attribute name, then add additional attributes with the - * following convention: `{base-attribute-name}-{media-query-name}="{value}"` - * (ie. `md-colspan-sm="4"`) - * - * @param {number=} md-colspan The number of columns to span (default 1). Cannot - * exceed the number of columns in the grid. Supports interpolation. - * @param {number=} md-rowspan The number of rows to span (default 1). Supports - * interpolation. - * - * @usage - * With header: - * - * - * - *

    This is a header

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

    This is a footer

    - *
    - *
    - *
    + * 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. * - * Spanning multiple rows/columns: - * - * - * - * + * @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 + '
    ' + + '
    '; +}; + + +/***************************************************************************** + * 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. * - * Responsive attributes: - * - * - * - * + * @param {!Object} config + * @param {!angular.$injector} $injector + * @final @constructor */ -function GridTileDirective($mdMedia) { - return { - restrict: 'E', - require: '^mdGridList', - template: '
    ', - transclude: true, - scope: {}, - // Simple controller that exposes attributes to the grid directive - controller: ["$attrs", function($attrs) { - this.$attrs = $attrs; - }], - link: postLink - }; +function 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.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. + /** + * Unique id for the panelRef. + * @type {string} + */ + this.id = config.id; + + /** + * 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} + */ + this.isAttached = false; - function postLink(scope, element, attrs, gridCtrl) { - // Apply semantics - element.attr('role', 'listitem'); + // Private variables. + /** @private {!Object} */ + this._config = config; - // If our colspan or rowspan changes, trigger a layout - var unwatchAttrs = $mdMedia.watchResponsiveAttributes(['md-colspan', 'md-rowspan'], - attrs, angular.bind(gridCtrl, gridCtrl.invalidateLayout)); + /** @private {!angular.JQLite|undefined} */ + this._panelContainer; - // Tile registration/deregistration - gridCtrl.invalidateTiles(); - scope.$on('$destroy', function() { - // Mark the tile as destroyed so it is no longer considered in layout, - // even if the DOM element sticks around (like during a leave animation) - element[0].$$mdDestroyed = true; - unwatchAttrs(); - gridCtrl.invalidateLayout(); - }); + /** @private {!angular.JQLite|undefined} */ + this._panelEl; - if (angular.isDefined(scope.$parent.$index)) { - scope.$watch(function() { return scope.$parent.$index; }, - function indexChanged(newIdx, oldIdx) { - if (newIdx === oldIdx) { - return; - } - gridCtrl.invalidateTiles(); - }); - } - } -} -GridTileDirective.$inject = ["$mdMedia"]; + /** @private {Array} */ + this._removeListeners = []; + /** @private {!angular.JQLite|undefined} */ + this._topFocusTrap; -function GridTileCaptionDirective() { - return { - template: '
    ', - transclude: true - }; + /** @private {!angular.JQLite|undefined} */ + this._bottomFocusTrap; + + /** @private {!$mdPanel|undefined} */ + this._backdropRef; + + /** @private {Function?} */ + this._restoreScroll = null; } -})(); -(function(){ -"use strict"; /** - * @ngdoc module - * @name material.components.icon - * @description - * Icon + * 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. */ -angular.module('material.components.icon', [ - 'material.core' - ]); +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); + + self.attach() + .then(show) + .then(done) + .catch(reject); + }); +}; -})(); -(function(){ -"use strict"; /** - * @ngdoc module - * @name material.components.input + * Closes the panel. + * + * @returns {!angular.$q.Promise} A promise that is resolved when the panel is + * closed and animations finish. */ +MdPanelRef.prototype.close = function() { + var self = this; -angular.module('material.components.input', [ - 'material.core' - ]) - .directive('mdInputContainer', mdInputContainerDirective) - .directive('label', labelDirective) - .directive('input', inputTextareaDirective) - .directive('textarea', inputTextareaDirective) - .directive('mdMaxlength', mdMaxlengthDirective) - .directive('placeholder', placeholderDirective) - .directive('ngMessages', ngMessagesDirective) - .directive('ngMessage', ngMessageDirective) - .directive('ngMessageExp', ngMessageDirective) - .directive('mdSelectOnFocus', mdSelectOnFocusDirective) + return this._$q(function(resolve, reject) { + var done = self._done(resolve, self); + var detach = self._simpleBind(self.detach, self); + + self.hide() + .then(detach) + .then(done) + .catch(reject); + }); +}; - .animation('.md-input-invalid', mdInputInvalidMessagesAnimation) - .animation('.md-input-messages-animation', ngMessagesAnimation) - .animation('.md-input-message-animation', ngMessageAnimation); /** - * @ngdoc directive - * @name mdInputContainer - * @module material.components.input - * - * @restrict E - * - * @description - * `` is the parent of any input or textarea element. - * - * Input and textarea elements will not behave properly unless the md-input-container - * parent is provided. - * - * @param md-is-error {expression=} When the given expression evaluates to true, the input container - * will go into error state. Defaults to erroring if the input has been touched and is invalid. - * @param md-no-float {boolean=} When present, `placeholder` attributes on the input will not be converted to floating - * labels. - * - * @usage - * - * - * - * - * - * - * - * - * - * - * - * - * + * Attaches the panel. The panel will be hidden afterwards. * - *

    When disabling floating labels

    - * - * - * - * - * - * - * + * @returns {!angular.$q.Promise} A promise that is resolved when + * the panel is attached. */ -function mdInputContainerDirective($mdTheming, $parse) { +MdPanelRef.prototype.attach = function() { + if (this.isAttached && this._panelEl) { + return this._$q.when(this); + } - var INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT', 'MD-SELECT']; + 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; + }; - var LEFT_SELECTORS = INPUT_TAGS.reduce(function(selectors, isel) { - return selectors.concat(['md-icon ~ ' + isel, '.md-icon ~ ' + isel]); - }, []).join(","); + self._$q.all([ + self._createBackdrop(), + self._createPanel() + .then(addListeners) + .catch(reject) + ]).then(onDomAdded) + .then(done) + .catch(reject); + }); +}; - var RIGHT_SELECTORS = INPUT_TAGS.reduce(function(selectors, isel) { - return selectors.concat([isel + ' ~ md-icon', isel + ' ~ .md-icon']); - }, []).join(","); - ContainerCtrl.$inject = ["$scope", "$element", "$attrs", "$animate"]; - return { - restrict: 'E', - link: postLink, - controller: ContainerCtrl - }; +/** + * 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); + } - function postLink(scope, element) { - $mdTheming(element); + var self = this; + var onDomRemoved = self._config['onDomRemoved'] || angular.noop; - // Check for both a left & right icon - var leftIcon = element[0].querySelector(LEFT_SELECTORS); - var rightIcon = element[0].querySelector(RIGHT_SELECTORS); + var detachFn = function() { + self._removeEventListeners(); - if (leftIcon) { element.addClass('md-icon-left'); } - if (rightIcon) { element.addClass('md-icon-right'); } - } + // 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); + } - function ContainerCtrl($scope, $element, $attrs, $animate) { - var self = this; + if (self._bottomFocusTrap && self._bottomFocusTrap.parentNode) { + self._bottomFocusTrap.parentNode.removeChild(self._bottomFocusTrap); + } - self.isErrorGetter = $attrs.mdIsError && $parse($attrs.mdIsError); + self._panelContainer.remove(); + self.isAttached = false; + return self._$q.when(self); + }; - 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')); - } - }); + if (this._restoreScroll) { + this._restoreScroll(); + this._restoreScroll = null; } -} -mdInputContainerDirective.$inject = ["$mdTheming", "$parse"]; - -function labelDirective() { - return { - restrict: 'E', - require: '^?mdInputContainer', - link: function(scope, element, attr, containerCtrl) { - if (!containerCtrl || attr.mdNoFloat || element.hasClass('_md-container-ignore')) return; - containerCtrl.label = element; - scope.$on('$destroy', function() { - containerCtrl.label = null; - }); - } - }; -} + return this._$q(function(resolve, reject) { + var done = self._done(resolve, self); -/** - * @ngdoc directive - * @name mdInput - * @restrict E - * @module material.components.input - * - * @description - * You can use any `` or ` - *
    - *
    This is required!
    - *
    That's too long!
    - *
    - *
    - * - * - * - * - * - * - * - * + * @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('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 focusOnOrigin = function() { + var origin = self._config['origin']; + if (origin) { + getElement(origin).focus(); + } + }; + + var hidePanel = function() { + self.addClass(MD_PANEL_HIDDEN); + }; + + self._$q.all([ + self._backdropRef ? self._backdropRef.hide() : self, + self._animateClose() + .then(onRemoving) + .then(hidePanel) + .then(focusOnOrigin) + .catch(reject) + ]).then(done, reject); + }); +}; + + +/** + * Add a class to the panel. DO NOT use this to hide/show the panel. * - *

    Notes

    + * @param {string} newClass Class to be added. + */ +MdPanelRef.prototype.addClass = function(newClass) { + if (!this._panelContainer) { + throw new Error('Panel does not exist yet. Call open() or attach().'); + } + + if (!this._panelContainer.hasClass(newClass)) { + this._panelContainer.addClass(newClass); + } +}; + + +/** + * Remove a class from the panel. DO NOT use this to hide/show the panel. * - * - Requires [ngMessages](https://docs.angularjs.org/api/ngMessages). - * - Behaves like the [AngularJS input directive](https://docs.angularjs.org/api/ng/directive/input). + * @param {string} oldClass Class to be removed. + */ +MdPanelRef.prototype.removeClass = function(oldClass) { + if (!this._panelContainer) { + throw new Error('Panel does not exist yet. Call open() or attach().'); + } + + if (this._panelContainer.hasClass(oldClass)) { + this._panelContainer.removeClass(oldClass); + } +}; + + +/** + * Toggle a class on the panel. DO NOT use this to hide/show the panel. * - * 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. + * @param {string} toggleClass The class to toggle. + */ +MdPanelRef.prototype.toggleClass = function(toggleClass) { + if (!this._panelContainer) { + throw new Error('Panel does not exist yet. Call open() or attach().'); + } + + this._panelContainer.toggleClass(toggleClass); +}; + + +/** + * 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; -function inputTextareaDirective($mdUtil, $window, $mdAria) { - return { - restrict: 'E', - require: ['^?mdInputContainer', '?ngModel'], - link: postLink - }; + return this._$q(function(resolve, reject) { + if (!self._config.locals) { + self._config.locals = {}; + } - function postLink(scope, element, attr, ctrls) { + self._config.locals.mdPanelRef = self; + self._$mdCompiler.compile(self._config) + .then(function(compileData) { + self._panelContainer = compileData.link(self._config['scope']); + getElement(self._config['attachTo']).append(self._panelContainer); - 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 (self._config['disableParentScroll']) { + self._restoreScroll = self._$mdUtil.disableScrollAround( + null, self._panelContainer); + } + self._panelEl = angular.element( + self._panelContainer[0].querySelector('.md-panel')); - if (!containerCtrl) return; - if (containerCtrl.input) { - throw new Error(" can only have *one* , - * - * - * + * Sets the value of `top` for the panel. Clears any previously set vertical + * position. + * @param {string=} opt_top Value of `top`. Defaults to '0'. + * @returns {MdPanelPosition} */ -function mdSelectOnFocusDirective() { +MdPanelPosition.prototype.top = function(opt_top) { + this._bottom = ''; + this._top = opt_top || '0'; + return this; +}; - return { - restrict: 'A', - link: postLink - }; - function postLink(scope, element, attr) { - if (element[0].nodeName !== 'INPUT' && element[0].nodeName !== "TEXTAREA") return; +/** + * Sets the value of `bottom` for the panel. Clears any previously set vertical + * position. + * @param {string=} opt_bottom Value of `bottom`. Defaults to '0'. + * @returns {MdPanelPosition} + */ +MdPanelPosition.prototype.bottom = function(opt_bottom) { + this._top = ''; + this._bottom = opt_bottom || '0'; + return this; +}; - element.on('focus', onFocus); - scope.$on('$destroy', function() { - element.off('focus', onFocus); - }); +/** + * Sets the value of `left` for the panel. Clears any previously set + * horizontal position. + * @param {string=} opt_left Value of `left`. Defaults to '0'. + * @returns {MdPanelPosition} + */ +MdPanelPosition.prototype.left = function(opt_left) { + this._right = ''; + this._left = opt_left || '0'; + return this; +}; - function onFocus() { - // Use HTMLInputElement#select to fix firefox select issues - element[0].select(); - } - } -} -var visibilityDirectives = ['ngIf', 'ngShow', 'ngHide', 'ngSwitchWhen', 'ngSwitchDefault']; -function ngMessagesDirective() { - return { - restrict: 'EA', - link: postLink, +/** + * Sets the value of `right` for the panel. Clears any previously set + * horizontal position. + * @param {string=} opt_right Value of `right`. Defaults to '0'. + * @returns {MdPanelPosition} + */ +MdPanelPosition.prototype.right = function(opt_right) { + this._left = ''; + this._right = opt_right || '0'; + return this; +}; + + +/** + * Centers the panel horizontally in the viewport. Clears any previously set + * horizontal position. + * @returns {MdPanelPosition} + */ +MdPanelPosition.prototype.centerHorizontally = function() { + this._left = '50%'; + this._right = ''; + this._translateX = ['-50%']; + return this; +}; + + +/** + * Centers the panel vertically in the viewport. Clears any previously set + * vertical position. + * @returns {MdPanelPosition} + */ +MdPanelPosition.prototype.centerVertically = function() { + this._top = '50%'; + this._bottom = ''; + this._translateY = ['-50%']; + return this; +}; - // This is optional because we don't want target *all* ngMessage instances, just those inside of - // mdInputContainer. - require: '^^?mdInputContainer' - }; - function postLink(scope, element, attrs, inputContainer) { - // If we are not a child of an input container, don't do anything - if (!inputContainer) return; +/** + * 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} + */ +MdPanelPosition.prototype.center = function() { + return this.centerHorizontally().centerVertically(); +}; - // Add our animation class - element.toggleClass('md-input-messages-animation', true); - // Add our md-auto-hide class to automatically hide/show messages when container is invalid - element.toggleClass('md-auto-hide', true); +/** + * Sets element for relative positioning. + * @param {string|!Element|!angular.JQLite} element Query selector, + * DOM element, or angular element to set the panel relative to. + * @returns {MdPanelPosition} + */ +MdPanelPosition.prototype.relativeTo = function(element) { + this._absolute = false; + this._relativeToEl = getElement(element); + return this; +}; - // 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); - } - } - function hasVisibiltyDirective(attrs) { - return visibilityDirectives.some(function(attr) { - return attrs[attr]; - }); +/** + * Sets the x and y positions for the panel relative to another element. + * @param {string} xPosition must be one of the MdPanelPosition.xPosition values. + * @param {string} yPosition must be one of the MdPanelPosition.yPosition values. + * @returns {MdPanelPosition} + */ +MdPanelPosition.prototype.addPanelPosition = function(xPosition, yPosition) { + if (!this._relativeToEl) { + throw new Error('addPanelPosition can only be used with relative ' + + 'positioning. Set relativeTo first.'); } -} - -function ngMessageDirective($mdUtil) { - return { - restrict: 'EA', - compile: compile, - priority: 100 - }; - function compile(element) { - var inputContainer = $mdUtil.getClosest(element, "md-input-container"); + this._validateXPosition(xPosition); + this._validateYPosition(yPosition); - // If we are not a child of an input container, don't do anything - if (!inputContainer) return; + this._positions.push({ + x: xPosition, + y: yPosition, + }); + return this; +}; - // Add our animation class - element.toggleClass('md-input-message-animation', true); - return {}; +/** + * Ensure that yPosition is a valid position name. Throw an exception if not. + * @param {string} yPosition + */ +MdPanelPosition.prototype._validateYPosition = function(yPosition) { + // empty is ok + if (yPosition == null) { + return; } -} -ngMessageDirective.$inject = ["$mdUtil"]; -function mdInputInvalidMessagesAnimation($q, $animateCss) { - return { - addClass: function(element, className, done) { - var messages = getMessagesElement(element); + var positionKeys = Object.keys(MdPanelPosition.yPosition); + var positionValues = []; + for (var key, i = 0; key = positionKeys[i]; i++) { + var position = MdPanelPosition.yPosition[key]; + positionValues.push(position); - if (className == "md-input-invalid" && messages.hasClass('md-auto-hide')) { - showInputMessages(element, $animateCss, $q).finally(done); - } else { - done(); - } + if (position === yPosition) { + return; } - - // NOTE: We do not need the removeClass method, because the message ng-leave animation will fire } -} -mdInputInvalidMessagesAnimation.$inject = ["$q", "$animateCss"]; -function ngMessagesAnimation($q, $animateCss) { - return { - enter: function(element, done) { - showInputMessages(element, $animateCss, $q).finally(done); - }, + throw new Error('Panel y position only accepts the following values:\n' + + positionValues.join(' | ')); +}; - leave: function(element, done) { - hideInputMessages(element, $animateCss, $q).finally(done); - }, - addClass: function(element, className, done) { - if (className == "ng-hide") { - hideInputMessages(element, $animateCss, $q).finally(done); - } else { - done(); - } - }, +/** + * Ensure that xPosition is a valid position name. Throw an exception if not. + * @param {string} xPosition + */ +MdPanelPosition.prototype._validateXPosition = function(xPosition) { + // empty is ok + if (xPosition == null) { + return; + } - removeClass: function(element, className, done) { - if (className == "ng-hide") { - showInputMessages(element, $animateCss, $q).finally(done); - } else { - done(); - } + var positionKeys = Object.keys(MdPanelPosition.xPosition); + var positionValues = []; + for (var key, i = 0; key = positionKeys[i]; i++) { + var position = MdPanelPosition.xPosition[key]; + positionValues.push(position); + if (position === xPosition) { + return; } } -} -ngMessagesAnimation.$inject = ["$q", "$animateCss"]; -function ngMessageAnimation($animateCss) { - return { - enter: function(element, done) { - var messages = getMessagesElement(element); + throw new Error('Panel x Position only accepts the following values:\n' + + positionValues.join(' | ')); +}; - // 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; - } - return showMessage(element, $animateCss); - }, +/** + * Sets the value of the offset in the x-direction. This will add + * to any previously set offsets. + * @param {string} offsetX + * @returns {MdPanelPosition} + */ +MdPanelPosition.prototype.withOffsetX = function(offsetX) { + this._translateX.push(offsetX); + return this; +}; - leave: function(element, done) { - return hideMessage(element, $animateCss); - } - } -} -ngMessageAnimation.$inject = ["$animateCss"]; -function showInputMessages(element, $animateCss, $q) { - var animators = [], animator; - var messages = getMessagesElement(element); +/** + * Sets the value of the offset in the y-direction. This will add + * to any previously set offsets. + * @param {string} offsetY + * @returns {MdPanelPosition} + */ +MdPanelPosition.prototype.withOffsetY = function(offsetY) { + this._translateY.push(offsetY); + return this; +}; - angular.forEach(messages.children(), function(child) { - animator = showMessage(angular.element(child), $animateCss); - animators.push(animator.start()); - }); +/** + * Gets the value of `top` for the panel. + * @returns {string} + */ +MdPanelPosition.prototype.getTop = function() { + return this._top; +}; - return $q.all(animators); -} -function hideInputMessages(element, $animateCss, $q) { - var animators = [], animator; - var messages = getMessagesElement(element); +/** + * Gets the value of `bottom` for the panel. + * @returns {string} + */ +MdPanelPosition.prototype.getBottom = function() { + return this._bottom; +}; - angular.forEach(messages.children(), function(child) { - animator = hideMessage(angular.element(child), $animateCss); - animators.push(animator.start()); - }); +/** + * Gets the value of `left` for the panel. + * @returns {string} + */ +MdPanelPosition.prototype.getLeft = function() { + return this._left; +}; - return $q.all(animators); -} -function showMessage(element, $animateCss) { - var height = element[0].offsetHeight; +/** + * Gets the value of `right` for the panel. + * @returns {string} + */ +MdPanelPosition.prototype.getRight = function() { + return this._right; +}; - return $animateCss(element, { - event: 'enter', - structural: true, - from: {"opacity": 0, "margin-top": -height + "px"}, - to: {"opacity": 1, "margin-top": "0"}, - duration: 0.3 - }); -} -function hideMessage(element, $animateCss) { - var height = element[0].offsetHeight; - var styles = window.getComputedStyle(element[0]); +/** + * Gets the value of `transform` for the panel. + * @returns {string} + */ +MdPanelPosition.prototype.getTransform = function() { + var translateX = this._reduceTranslateValues('translateX', this._translateX); + var translateY = this._reduceTranslateValues('translateY', this._translateY); - // If we are already hidden, just return an empty animation - if (styles.opacity == 0) { - return $animateCss(element, {}); - } + // It's important to trim the result, because the browser will ignore the set + // operation if the string contains only whitespace. + return (translateX + ' ' + translateY).trim(); +}; - // 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 - }); -} +/** + * True if the panel is completely on-screen with this positioning; false + * otherwise. + * @param {!angular.JQLite} panelEl + * @return {boolean} + */ +MdPanelPosition.prototype._isOnscreen = function(panelEl) { + // this works because we always use fixed positioning for the panel, + // which is relative to the viewport. + // TODO(gmoothart): take into account _translateX and _translateY to the + // extent feasible. + + var left = parseInt(this.getLeft()); + var top = parseInt(this.getTop()); + var right = left + panelEl[0].offsetWidth; + var bottom = top + panelEl[0].offsetHeight; + + return (left >= 0) && + (top >= 0) && + (bottom <= this._$window.innerHeight) && + (right <= this._$window.innerWidth); +}; -function getInputElement(element) { - var inputContainer = element.controller('mdInputContainer'); - return inputContainer.element; -} +/** + * Gets the first x/y position that can fit on-screen. + * @returns {{x: string, y: string}} + */ +MdPanelPosition.prototype.getActualPosition = function() { + return this._actualPosition; +}; -function getMessagesElement(element) { - var input = getInputElement(element); - return angular.element(input[0].querySelector('.md-input-messages-animation')); -} +/** + * Reduces a list of translate values to a string that can be used within + * transform. + * @param {string} translateFn + * @param {!Array} values + * @returns {string} + * @private + */ +MdPanelPosition.prototype._reduceTranslateValues = + function(translateFn, values) { + return values.map(function(translation) { + return translateFn + '(' + translation + ')'; + }).join(' '); + }; -})(); -(function(){ -"use strict"; /** - * @ngdoc module - * @name material.components.list - * @description - * List module + * Sets the panel position based on the created panel element and best x/y + * positioning. + * @param {!angular.JQLite} panelEl + * @private */ -angular.module('material.components.list', [ - 'material.core' -]) - .controller('MdListController', MdListController) - .directive('mdList', mdListDirective) - .directive('mdListItem', mdListItemDirective); +MdPanelPosition.prototype._setPanelPosition = function(panelEl) { + // Only calculate the position if necessary. + if (this._absolute) { + return; + } + + // TODO(ErinCoughlan): Position panel intelligently to keep it on screen. + + if (this._actualPosition) { + this._calculatePanelPosition(panelEl, this._actualPosition); + return; + } + + for (var i = 0; i < this._positions.length; i++) { + this._actualPosition = this._positions[i]; + this._calculatePanelPosition(panelEl, this._actualPosition); + if (this._isOnscreen(panelEl)) { + break; + } + } +}; /** - * @ngdoc directive - * @name mdList - * @module material.components.list - * - * @restrict E - * - * @description - * The `` directive is a list container for 1..n `` tags. - * - * @usage - * - * - * - * - *
    - *

    {{item.title}}

    - *

    {{item.description}}

    - *
    - *
    - *
    - *
    + * Calculates the panel position based on the created panel element and the + * provided positioning. + * @param {!angular.JQLite} panelEl + * @param {!{x:string, y:string}} position + * @private */ +MdPanelPosition.prototype._calculatePanelPosition = function(panelEl, position) { + + var panelBounds = panelEl[0].getBoundingClientRect(); + var panelWidth = panelBounds.width; + var panelHeight = panelBounds.height; + + var targetBounds = this._relativeToEl[0].getBoundingClientRect(); + + var targetLeft = targetBounds.left; + var targetRight = targetBounds.right; + var targetWidth = targetBounds.width; + + switch (position.x) { + case MdPanelPosition.xPosition.OFFSET_START: + // TODO(ErinCoughlan): Change OFFSET_START for rtl vs ltr. + this._left = targetLeft - panelWidth + 'px'; + break; + case MdPanelPosition.xPosition.ALIGN_END: + // TODO(ErinCoughlan): Change ALIGN_END for rtl vs ltr. + this._left = targetRight - panelWidth + 'px'; + break; + case MdPanelPosition.xPosition.CENTER: + var left = targetLeft + (0.5 * targetWidth) - (0.5 * panelWidth); + this._left = left + 'px'; + break; + case MdPanelPosition.xPosition.ALIGN_START: + // TODO(ErinCoughlan): Change ALIGN_START for rtl vs ltr. + this._left = targetLeft + 'px'; + break; + case MdPanelPosition.xPosition.OFFSET_END: + // TODO(ErinCoughlan): Change OFFSET_END for rtl vs ltr. + this._left = targetRight + 'px'; + break; + } + + var targetTop = targetBounds.top; + var targetBottom = targetBounds.bottom; + var targetHeight = targetBounds.height; + + switch (position.y) { + case MdPanelPosition.yPosition.ABOVE: + this._top = targetTop - panelHeight + 'px'; + break; + case MdPanelPosition.yPosition.ALIGN_BOTTOMS: + this._top = targetBottom - panelHeight + 'px'; + break; + case MdPanelPosition.yPosition.CENTER: + var top = targetTop + (0.5 * targetHeight) - (0.5 * panelHeight); + this._top = top + 'px'; + break; + case MdPanelPosition.yPosition.ALIGN_TOPS: + this._top = targetTop + 'px'; + break; + case MdPanelPosition.yPosition.BELOW: + this._top = targetBottom + 'px'; + break; + } +}; + + +/***************************************************************************** + * MdPanelAnimation * + *****************************************************************************/ + -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 - * - * @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. - * - * ## CSS - * `.md-avatar` - class for image avatars + * Animation configuration object. To use, create an MdPanelAnimation with the + * desired properties, then pass the object as part of $mdPanel creation. * - * `.md-avatar-icon` - class for icon avatars + * Example: * - * `.md-offset` - on content without an avatar + * var panelAnimation = new MdPanelAnimation() + * .openFrom(myButtonEl) + * .closeTo('.my-button') + * .withAnimation($mdPanel.animation.SCALE); * - * @usage - * - * - * - * - * Item content in list - * - * - * - * Item content in list - * - * - * + * $mdPanel.create({ + * animation: panelAnimation + * }); * - * _**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._ + * @param {!angular.$injector} $injector + * @final @constructor */ -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 MdPanelAnimation($injector) { + /** @private @const {!angular.$mdUtil} */ + this._$mdUtil = $injector.get('$mdUtil'); - tEl[0].setAttribute('role', 'listitem'); + /** + * @private {{element: !angular.JQLite|undefined, bounds: !DOMRect}| + * undefined} + */ + this._openFrom; - 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'); - } - } - wrapSecondaryItems(); - setupToggleAria(); + /** + * @private {{element: !angular.JQLite|undefined, bounds: !DOMRect}| + * undefined} + */ + this._closeTo; + /** @private {string|{open: string, close: string} */ + this._animationClass = ''; +} - function setupToggleAria() { - var toggleTypes = ['md-switch', 'md-checkbox']; - var toggle; - 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); - } - } - } - } +/** + * Possible default animations. + * @enum {string} + */ +MdPanelAnimation.animation = { + SLIDE: 'md-panel-animate-slide', + SCALE: 'md-panel-animate-scale', + FADE: 'md-panel-animate-fade' +}; - 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( - '' - ); - // Button which shows ripple and executes primary action. - var buttonWrap = angular.element( - '' - ); +/** + * 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}} openFrom + * @returns {MdPanelAnimation} + */ +MdPanelAnimation.prototype.openFrom = function(openFrom) { + // Check if 'openFrom' is an Event. + openFrom = openFrom.target ? openFrom.target : openFrom; - buttonWrap[0].setAttribute('aria-label', tEl[0].textContent); - copyAttributes(tEl[0], buttonWrap[0]); + this._openFrom = this._getPanelAnimationTarget(openFrom); - // 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'); - } + if (!this._closeTo) { + this._closeTo = this._openFrom; + } + return this; +}; - tEl[0].setAttribute('tabindex', '-1'); - tEl.append(container); - } - 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); - } - } +/** + * Specifies where to animate the dialog 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}} closeTo + * @returns {MdPanelAnimation} + */ +MdPanelAnimation.prototype.closeTo = function(closeTo) { + this._closeTo = this._getPanelAnimationTarget(closeTo); + return this; +}; - 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]; - } - // 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); - } +/** + * Returns the element and bounds for the animation target. + * @param {string|!Element|{top: number, left: number}} location + * @returns {{element: !angular.JQLite|undefined, bounds: !DOMRect}} + * @private + */ +MdPanelAnimation.prototype._getPanelAnimationTarget = function(location) { + if (angular.isDefined(location.top) || angular.isDefined(location.left)) { + return { + element: undefined, + bounds: { + top: location.top || 0, + left: location.left || 0 } + }; + } else { + return this._getBoundingClientRect(getElement(location)); + } +}; - 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); - } - }); - } - function isProxiedElement(el) { - return proxiedTypes.indexOf(el.nodeName.toLowerCase()) != -1; - } +/** + * Specifies the animation class. + * + * There are several default animations that can be used: + * (MdPanelAnimation.animation) + * SLIDE: The panel slides in and out from the specified + * elements. + * SCALE: The panel scales in and out. + * FADE: The panel fades in and out. + * + * @param {string|{open: string, close: string}} cssClass + * @returns {MdPanelAnimation} + */ - function isButton(el) { - var nodeName = el.nodeName.toUpperCase(); +MdPanelAnimation.prototype.withAnimation = function(cssClass) { + this._animationClass = cssClass; + return this; +}; - return nodeName == "MD-BUTTON" || nodeName == "BUTTON"; - } - return postLink; +/** + * Animate the panel open. + * @param {!angular.JQLite} panelEl + * @returns {!angular.$q.Promise} + */ +MdPanelAnimation.prototype.animateOpen = function(panelEl) { + var animator = this._$mdUtil.dom.animator; - function postLink($scope, $element, $attr, ctrl) { + this._fixBounds(panelEl); + var animationOptions = {}; - var proxies = [], - firstChild = $element[0].firstElementChild, - hasClick = firstChild && firstChild.firstElementChild && - hasClickEvent(firstChild.firstElementChild); + // Include the panel transformations when calculating the animations. + var panelTransform = panelEl[0].style.transform || ''; - computeProxies(); - computeClickable(); + var openFrom = animator.toTransformCss(panelTransform); + var openTo = animator.toTransformCss(panelTransform); - if ($element.hasClass('_md-proxy-focus') && proxies.length) { - angular.forEach(proxies, function(proxy) { - proxy = angular.element(proxy); + switch (this._animationClass) { + case MdPanelAnimation.animation.SLIDE: + // Slide should start with opacity: 1. + panelEl.css('opacity', '1'); - $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); - }); - }); - }); - } + animationOptions = { + transitionInClass: '_md-panel-animate-enter' + }; - 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; - } + var openSlide = animator.calculateSlideToOrigin( + panelEl, this._openFrom) || ''; + openFrom = animator.toTransformCss(openSlide + ' ' + panelTransform); + break; - 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'); + case MdPanelAnimation.animation.SCALE: + animationOptions = { + transitionInClass: '_md-panel-animate-enter' + }; - if (!hasClick) { - ctrl.attachRipple($scope, angular.element($element[0].querySelector('._md-no-style'))); - } - } - } + var openScale = animator.calculateZoomToOrigin( + panelEl, this._openFrom) || ''; + openFrom = animator.toTransformCss(openScale + ' ' + panelTransform); + break; - 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(); - } - } - } + case MdPanelAnimation.animation.FADE: + animationOptions = { + transitionInClass: '_md-panel-animate-enter' + }; + break; + + default: + if (angular.isString(this._animationClass)) { + animationOptions = { + transitionInClass: this._animationClass + }; + } else { + animationOptions = { + transitionInClass: this._animationClass['open'], + transitionOutClass: this._animationClass['close'], }; + } - if (!hasClick && !proxies.length) { - firstChild && firstChild.addEventListener('keypress', firstChildKeypressListener); - } + // TODO(ErinCoughlan): Combine the user's custom transforms with the + // panel transform. + } - $element.off('click'); - $element.off('keypress'); + return animator + .translate3d(panelEl, openFrom, openTo, animationOptions); +}; - 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'); - } - }); - } - }); - } - $scope.$on('$destroy', function () { - firstChild && firstChild.removeEventListener('keypress', firstChildKeypressListener); - }); +/** + * Animate the panel close. + * @param {!angular.JQLite} panelEl + * @returns {!angular.$q.Promise} + */ +MdPanelAnimation.prototype.animateClose = function(panelEl) { + var animator = this._$mdUtil.dom.animator; + var reverseAnimationOptions = {}; + + // Include the panel transformations when calculating the animations. + var panelTransform = panelEl[0].style.transform || ''; + + var closeFrom = animator.toTransformCss(panelTransform); + var closeTo = animator.toTransformCss(panelTransform); + + switch (this._animationClass) { + case MdPanelAnimation.animation.SLIDE: + // Slide should start with opacity: 1. + panelEl.css('opacity', '1'); + reverseAnimationOptions = { + transitionInClass: '_md-panel-animate-leave' + }; + + var closeSlide = animator.calculateSlideToOrigin( + panelEl, this._closeTo) || ''; + closeTo = animator.toTransformCss(closeSlide + ' ' + panelTransform); + break; + + case MdPanelAnimation.animation.SCALE: + reverseAnimationOptions = { + transitionInClass: '_md-panel-animate-scale-out _md-panel-animate-leave' + }; + + var closeScale = animator.calculateZoomToOrigin( + panelEl, this._closeTo) || ''; + closeTo = animator.toTransformCss(closeScale + ' ' + panelTransform); + break; + + case MdPanelAnimation.animation.FADE: + reverseAnimationOptions = { + transitionInClass: '_md-panel-animate-fade-out _md-panel-animate-leave' + }; + break; + + default: + if (angular.isString(this._animationClass)) { + reverseAnimationOptions = { + transitionOutClass: this._animationClass + }; + } else { + reverseAnimationOptions = { + transitionInClass: this._animationClass['close'], + transitionOutClass: this._animationClass['open'] + }; } - } - }; -} -mdListItemDirective.$inject = ["$mdAria", "$mdConstant", "$mdUtil", "$timeout"]; -/* + // TODO(ErinCoughlan): Combine the user's custom transforms with the + // panel transform. + } + + return animator + .translate3d(panelEl, closeFrom, closeTo, reverseAnimationOptions); +}; + + +/** + * Set the height and width to match the panel if not provided. + * @param {!angular.JQLite} panelEl * @private - * @ngdoc controller - * @name MdListController - * @module material.components.list - * */ -function MdListController($scope, $element, $mdListInkRipple) { - var ctrl = this; - ctrl.attachRipple = attachRipple; +MdPanelAnimation.prototype._fixBounds = function(panelEl) { + var panelWidth = panelEl[0].offsetWidth; + var panelHeight = panelEl[0].offsetHeight; - function attachRipple (scope, element) { - var options = {}; - $mdListInkRipple.attach(scope, element, options); + if (this._openFrom && this._openFrom.bounds.height == null) { + this._openFrom.bounds.height = panelHeight; } -} -MdListController.$inject = ["$scope", "$element", "$mdListInkRipple"]; - + if (this._openFrom && this._openFrom.bounds.width == null) { + this._openFrom.bounds.width = panelWidth; + } + if (this._closeTo && this._closeTo.bounds.height == null) { + this._closeTo.bounds.height = panelHeight; + } + if (this._closeTo && this._closeTo.bounds.width == null) { + this._closeTo.bounds.width = panelWidth; + } +}; -})(); -(function(){ -"use strict"; /** - * @ngdoc module - * @name material.components.menu + * Identify the bounding RECT for the target element. + * @param {!angular.JQLite} element + * @returns {{element: !angular.JQLite|undefined, bounds: !DOMRect}} + * @private */ +MdPanelAnimation.prototype._getBoundingClientRect = function(element) { + if (element instanceof angular.element) { + return { + element: element, + bounds: element[0].getBoundingClientRect() + }; + } +}; -angular.module('material.components.menu', [ - 'material.core', - 'material.components.backdrop' -]); -})(); -(function(){ -"use strict"; +/***************************************************************************** + * Util Methods * + *****************************************************************************/ /** - * @ngdoc module - * @name material.components.menu-bar + * Returns the angular element associated with a css selector or element. + * @param el {string|!angular.JQLite|!Element} + * @returns {!angular.JQLite} */ - -angular.module('material.components.menuBar', [ - 'material.core', - 'material.components.menu' -]); +function getElement(el) { + var queryResult = angular.isString(el) ? + document.querySelector(el) : el; + return angular.element(queryResult); +} })(); (function(){ @@ -13006,6 +17598,7 @@ angular.module('material.components.progressLinear', [ * 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 * @@ -13021,10 +17614,11 @@ angular.module('material.components.progressLinear', [ * */ function MdProgressLinearDirective($mdTheming, $mdUtil, $log) { - var MODE_DETERMINATE = "determinate", - MODE_INDETERMINATE = "indeterminate", - MODE_BUFFER = "buffer", - MODE_QUERY = "query"; + var MODE_DETERMINATE = "determinate"; + var MODE_INDETERMINATE = "indeterminate"; + var MODE_BUFFER = "buffer"; + var MODE_QUERY = "query"; + var DISABLED_CLASS = "_md-progress-linear-disabled"; return { restrict: 'E', @@ -13035,7 +17629,7 @@ function MdProgressLinearDirective($mdTheming, $mdUtil, $log) { '
    ', compile: compile }; - + function compile(tElement, tAttrs, transclude) { tElement.attr('aria-valuemin', 0); tElement.attr('aria-valuemax', 100); @@ -13046,12 +17640,16 @@ function MdProgressLinearDirective($mdTheming, $mdUtil, $log) { 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')); + 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')); - element.attr('md-mode', mode()); + element + .attr('md-mode', mode()) + .toggleClass(DISABLED_CLASS, isDisabled); validateMode(); watchAttributes(); @@ -13071,6 +17669,16 @@ function MdProgressLinearDirective($mdTheming, $mdUtil, $log) { animateIndicator(bar1, clamp(value)); }); + attr.$observe('disabled', function(value) { + if (value === true || value === false) { + isDisabled = value; + } else { + isDisabled = angular.isDefined(value); + } + + element.toggleClass(DISABLED_CLASS, !!isDisabled); + }); + attr.$observe('mdMode',function(mode){ if (lastMode) container.removeClass( lastMode ); @@ -13097,10 +17705,10 @@ function MdProgressLinearDirective($mdTheming, $mdUtil, $log) { var mode = hasValue ? MODE_DETERMINATE : MODE_INDETERMINATE; var info = "Auto-adding the missing md-mode='{0}' to the ProgressLinear element"; - $log.debug( $mdUtil.supplant(info, [mode]) ); + //$log.debug( $mdUtil.supplant(info, [mode]) ); element.attr("md-mode",mode); - attr['mdMode'] = mode; + attr.mdMode = mode; } } @@ -13129,7 +17737,7 @@ function MdProgressLinearDirective($mdTheming, $mdUtil, $log) { * percentage value (0-100). */ function animateIndicator(target, value) { - if ( !mode() ) return; + if ( isDisabled || !mode() ) return; var to = $mdUtil.supplant("translateX({0}%) scale({1},1)", [ (value-100)/2, value/100 ]); var styles = toVendorCSS({ transform : to }); @@ -13215,7 +17823,9 @@ function mdRadioGroupDirective($mdUtil, $mdConstant, $mdTheming, $timeout) { }; function linkRadioGroup(scope, element, attr, ctrls) { + element.addClass('_md'); // private md component indicator for styling $mdTheming(element); + var rgCtrl = ctrls[0]; var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel(); @@ -13516,1952 +18126,2074 @@ mdRadioButtonDirective.$inject = ["$mdAria", "$mdUtil", "$mdTheming"]; /** * @ngdoc module - * @name material.components.select + * @name material.components.sidenav + * + * @description + * A Sidenav QP component. */ +angular + .module('material.components.sidenav', [ + 'material.core', + 'material.components.backdrop' + ]) + .factory('$mdSidenav', SidenavService ) + .directive('mdSidenav', SidenavDirective) + .directive('mdSidenavFocus', SidenavFocusDirective) + .controller('$mdSidenavController', SidenavController); -/*************************************************** - ### TODO - POST RC1 ### - - [ ] Abstract placement logic in $mdSelect service to $mdMenu service +/** + * @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(); + * + */ +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 + }; - ***************************************************/ + /** + * 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; -var SELECT_EDGE_MARGIN = 8; -var selectNextId = 0; + var shouldWait = enableWait === true; + var instance = service.find(handle, shouldWait); + return !instance && shouldWait ? service.waitFor(handle) : + !instance && angular.isUndefined(enableWait) ? addLegacyAPI(service, handle) : instance; + }; -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); + /** + * 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 || ""])); + }; + + return angular.extend({ + isLockedOpen : falseFn, + isOpen : falseFn, + toggle : rejectFn, + open : rejectFn, + close : rejectFn, + 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); + + if (!instance && !shouldWait) { + + // Report missing instance + $log.error( $mdUtil.supplant(errorMsg, [handle || ""]) ); + + // 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; + } + return instance; + } + /** + * Asynchronously wait for the component instantiation, + * Deferred lookup of component instance using $component registry + */ + function waitForInstance(handle) { + return $mdComponentRegistry.when(handle).catch($log.error); + } +} +SidenavService.$inject = ["$mdComponentRegistry", "$mdUtil", "$q", "$log"]; /** * @ngdoc directive - * @name mdSelect - * @restrict E - * @module material.components.select + * @name mdSidenavFocus + * @module material.components.sidenav * - * @description Displays a select box, bound to an ng-model. + * @restrict A * - * @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). + * @description + * `mdSidenavFocus` provides a way to specify the focused element when a sidenav opens. + * This is completely optional, as the sidenav itself is focused by default. * * @usage - * With a placeholder (label and aria-label are added dynamically) * - * - * - * {{ opt }} - * - * + * + *
    + * + * + * + * + *
    + *
    *
    + **/ +function SidenavFocusDirective() { + return { + restrict: 'A', + require: '^mdSidenav', + link: function(scope, element, attr, sidenavCtrl) { + // @see $mdUtil.findFocusTarget(...) + } + }; +} +/** + * @ngdoc directive + * @name mdSidenav + * @module material.components.sidenav + * @restrict E * - * With an explicit label + * @description + * + * A Sidenav component that can be opened and closed programatically. + * + * By default, upon opening it will slide out on top of the main content area. + * + * For keyboard and screen reader accessibility, focus is sent to the sidenav wrapper by default. + * It can be overridden with the `md-autofocus` directive on the child element you want focused. + * + * @usage * - * - * - * - * {{ opt }} - * - * + *
    + * + * Left Nav! + * + * + * + * Center Content + * + * Open Left Menu + * + * + * + * + *
    + * + * + * + * + *
    + *
    + *
    *
    * - * ## 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' }; + * var app = angular.module('myApp', ['ngMaterial']); + * app.controller('MyController', function($scope, $mdSidenav) { + * $scope.openLeftMenu = function() { + * $mdSidenav('left').toggle(); + * }; * }); * - * - *
    - * - * {{ 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. + * @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. * - * 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. +* The $mdMedia() servic e 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: * - * Working HTML: - * - *
    - * - * {{ user.name }} - * - *
    - *
    + * - `` + * - `` + * - `` (locks open on small screens) */ -function SelectDirective($mdSelect, $mdUtil, $mdTheming, $mdAria, $compile, $parse) { +function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate, $compile, $parse, $log, $q, $document) { return { restrict: 'E', - require: ['^?mdInputContainer', 'mdSelect', 'ngModel', '?^form'], - compile: compile, - controller: function() { - } // empty placeholder controller to be initialized in link + scope: { + isOpen: '=?mdIsOpen' + }, + controller: '$mdSidenavController', + compile: function(element) { + element.addClass('_md-closed'); + element.attr('tabIndex', '-1'); + return postLink; + } }; - 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()); - } + /** + * Directive Post Link function... + */ + function postLink(scope, element, attr, sidenavCtrl) { + var lastParentOverFlow; + var backdrop; + var triggeringElement = null; + var previousContainerStyles; + var promise = $q.when(true); + var isLockedOpenParsed = $parse(attr.mdIsLockedOpen); + 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 + }); + }; - // 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())); + // Only create the backdrop if the backdrop isn't disabled. + if (!angular.isDefined(attr.mdDisableBackdrop)) { + backdrop = $mdUtil.createBackdrop(scope, "_md-sidenav-backdrop md-opaque ng-enter"); } + element.addClass('_md'); // private md component indicator for styling + $mdTheming(element); - // Add progress spinner for md-options-loading - if (attr.mdOnOpen) { + // The backdrop should inherit the sidenavs theme, + // because the backdrop will take its parent theme by default. + if ( backdrop ) $mdTheming.inherit(backdrop, element); - // 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( - '
    ' + - ' ' + - '
    ' - )); + element.on('$destroy', function() { + backdrop && backdrop.remove(); + sidenavCtrl.destroy(); + }); - // Hide list [of item options] while loading async - element - .find('md-option') - .attr('ng-show', '$$loadingAsyncDone'); - } + scope.$on('$destroy', function(){ + backdrop && backdrop.remove(); + }); - if (attr.name) { - var autofillClone = angular.element(', + * + * + * + * + *

    When disabling floating labels

    + * + * + * + * + * + * + * + */ +function mdInputContainerDirective($mdTheming, $parse) { - // If we have a reference to a raw dom element, always wrap it in jqLite - return angular.element(element || defaultElement); - } + 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(","); - /** - * 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); + var RIGHT_SELECTORS = INPUT_TAGS.reduce(function(selectors, isel) { + return selectors.concat([isel + ' ~ md-icon', isel + ' ~ .md-icon']); + }, []).join(","); - 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); - }; + ContainerCtrl.$inject = ["$scope", "$element", "$attrs", "$animate"]; + return { + restrict: 'E', + link: postLink, + controller: ContainerCtrl + }; - if (options.escapeToClose) { - var parentTarget = options.parent; - var keyHandlerFn = function(ev) { - if (ev.keyCode === $mdConstant.KEY_CODE.ESCAPE) { - ev.stopPropagation(); - ev.preventDefault(); + function postLink(scope, element) { + $mdTheming(element); - smartClose(); - } - }; + // Check for both a left & right icon + var leftIcon = element[0].querySelector(LEFT_SELECTORS); + var rightIcon = element[0].querySelector(RIGHT_SELECTORS); - // Add keydown listeners - element.on('keydown', keyHandlerFn); - parentTarget.on('keydown', keyHandlerFn); + if (leftIcon) { element.addClass('md-icon-left'); } + if (rightIcon) { element.addClass('md-icon-right'); } + } - // Queue remove listeners function - removeListeners.push(function() { + function ContainerCtrl($scope, $element, $attrs, $animate) { + var self = this; - element.off('keydown', keyHandlerFn); - parentTarget.off('keydown', keyHandlerFn); + self.isErrorGetter = $attrs.mdIsError && $parse($attrs.mdIsError); - }); + self.delegateClick = function() { + self.input.focus(); + }; + self.element = $element; + self.setFocused = function(isFocused) { + $element.toggleClass('md-input-focused', !!isFocused); + }; + self.setHasValue = function(hasValue) { + $element.toggleClass('md-input-has-value', !!hasValue); + }; + self.setHasPlaceholder = function(hasPlaceholder) { + $element.toggleClass('md-input-has-placeholder', !!hasPlaceholder); + }; + self.setInvalid = function(isInvalid) { + if (isInvalid) { + $animate.addClass($element, 'md-input-invalid'); + } else { + $animate.removeClass($element, 'md-input-invalid'); + } + }; + $scope.$watch(function() { + return self.label && self.input; + }, function(hasLabelAndInput) { + if (hasLabelAndInput && !self.label.attr('for')) { + self.label.attr('for', self.input.attr('id')); } + }); + } +} +mdInputContainerDirective.$inject = ["$mdTheming", "$parse"]; - // Register listener to update dialog on window resize - window.on('resize', onWindowResize); +function labelDirective() { + return { + restrict: 'E', + require: '^?mdInputContainer', + link: function(scope, element, attr, containerCtrl) { + if (!containerCtrl || attr.mdNoFloat || element.hasClass('md-container-ignore')) return; - removeListeners.push(function() { - window.off('resize', onWindowResize); + containerCtrl.label = element; + scope.$on('$destroy', function() { + containerCtrl.label = null; }); + } + }; +} - if (options.clickOutsideToClose) { - var target = element; - var sourceElem; +/** + * @ngdoc directive + * @name mdInput + * @restrict E + * @module material.components.input + * + * @description + * You can use any `` or ` + *
    + *
    This is required!
    + *
    That's too long!
    + *
    + * + * + * + * + * + * + * + * + * + * + *

    Notes

    + * + * - Requires [ngMessages](https://docs.angularjs.org/api/ngMessages). + * - Behaves like the [AngularJS input directive](https://docs.angularjs.org/api/ng/directive/input). + * + * The `md-input` and `md-input-container` directives use very specific positioning to achieve the + * error animation effects. Therefore, it is *not* advised to use the Layout system inside of the + * `` tags. Instead, use relative or absolute positioning. + * + * + *

    Textarea directive

    + * The `textarea` element within a `md-input-container` has the following specific behavior: + * - By default the `textarea` grows as the user types. This can be disabled via the `md-no-autogrow` + * attribute. + * - If a `textarea` has the `rows` attribute, it will treat the `rows` as the minimum height and will + * continue growing as the user types. For example a textarea with `rows="3"` will be 3 lines of text + * high initially. If no rows are specified, the directive defaults to 1. + * - The textarea's height gets set on initialization, as well as while the user is typing. In certain situations + * (e.g. while animating) the directive might have been initialized, before the element got it's final height. In + * those cases, you can trigger a resize manually by broadcasting a `md-resize-textarea` event on the scope. + * - If you wan't a `textarea` to stop growing at a certain point, you can specify the `max-rows` attribute. + * - The textarea's bottom border acts as a handle which users can drag, in order to resize the element vertically. + * Once the user has resized a `textarea`, the autogrowing functionality becomes disabled. If you don't want a + * `textarea` to be resizeable by the user, you can add the `md-no-resize` attribute. + */ - // 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; - }; +function inputTextareaDirective($mdUtil, $window, $mdAria, $timeout, $mdGesture) { + return { + restrict: 'E', + require: ['^?mdInputContainer', '?ngModel', '?^form'], + link: postLink + }; - // 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(); + function postLink(scope, element, attr, ctrls) { - smartClose(); - } - }; + var containerCtrl = ctrls[0]; + var hasNgModel = !!ctrls[1]; + var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel(); + var parentForm = ctrls[2]; + var isReadonly = angular.isDefined(attr.readonly); + var mdNoAsterisk = $mdUtil.parseAttributeBoolean(attr.mdNoAsterisk); + var tagName = element[0].tagName.toLowerCase(); - // Add listeners - target.on('mousedown', mousedownHandler); - target.on('mouseup', mouseupHandler); - // Queue remove listeners function - removeListeners.push(function() { - target.off('mousedown', mousedownHandler); - target.off('mouseup', mouseupHandler); - }); + if (!containerCtrl) return; + if (attr.type === 'hidden') { + element.attr('aria-hidden', 'true'); + return; + } else if (containerCtrl.input) { + if (containerCtrl.input[0].contains(element[0])) { + return; + } else { + throw new Error(" can only have *one* , + * + * + * + */ +function mdSelectOnFocusDirective($timeout) { - var trigger, actions; + return { + restrict: 'A', + link: postLink + }; - // Watch for changes to md-open - $scope.$watch('vm.isOpen', function(isOpen) { - // Reset the action index since it may have changed - resetActionIndex(); + function postLink(scope, element, attr) { + if (element[0].nodeName !== 'INPUT' && element[0].nodeName !== "TEXTAREA") return; - // 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 preventMouseUp = false; - if (isOpen) { - enableKeyboard(); - } else { - disableKeyboard(); - } + element + .on('focus', onFocus) + .on('mouseup', onMouseUp); - var toAdd = isOpen ? 'md-is-open' : ''; - var toRemove = isOpen ? '' : 'md-is-open'; + scope.$on('$destroy', function() { + element + .off('focus', onFocus) + .off('mouseup', onMouseUp); + }); - // Set the proper ARIA attributes - trigger.attr('aria-haspopup', true); - trigger.attr('aria-expanded', isOpen); - actions.attr('aria-hidden', !isOpen); + function onFocus() { + preventMouseUp = true; - // Animate the CSS classes - $animate.setClass($element, toAdd, toRemove); - }); + $timeout(function() { + // Use HTMLInputElement#select to fix firefox select issues. + // The debounce is here for Edge's sake, otherwise the selection doesn't work. + element[0].select(); + + // This should be reset from inside the `focus`, because the event might + // have originated from something different than a click, e.g. a keyboard event. + preventMouseUp = false; + }, 1, false); } - 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'); - }); + // Prevents the default action of the first `mouseup` after a focus. + // This is necessary, because browsers fire a `mouseup` right after the element + // has been focused. In some browsers (Firefox in particular) this can clear the + // selection. There are examples of the problem in issue #7487. + function onMouseUp(event) { + if (preventMouseUp) { + event.preventDefault(); } + } + } +} +mdSelectOnFocusDirective.$inject = ["$timeout"]; - // Otherwise, try for up to 1 second before giving up - else if (initialAnimationAttempts < 10) { - $timeout(fireInitialAnimations, 100); +var visibilityDirectives = ['ngIf', 'ngShow', 'ngHide', 'ngSwitchWhen', 'ngSwitchDefault']; +function ngMessagesDirective() { + return { + restrict: 'EA', + link: postLink, - // Increment our counter - initialAnimationAttempts = initialAnimationAttempts + 1; - } - } + // This is optional because we don't want target *all* ngMessage instances, just those inside of + // mdInputContainer. + require: '^^?mdInputContainer' + }; - function enableKeyboard() { - $element.on('keydown', keyPressed); + function postLink(scope, element, attrs, inputContainer) { + // If we are not a child of an input container, don't do anything + if (!inputContainer) 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); - }); + // Add our animation class + element.toggleClass('md-input-messages-animation', true); - // 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(); - } + // Add our md-auto-hide class to automatically hide/show messages when container is invalid + element.toggleClass('md-auto-hide', true); - function disableKeyboard() { - $element.off('keydown', keyPressed); - angular.element(document).off('click touchend', checkForOutsideClick); + // 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); } + } - function checkForOutsideClick(event) { - if (event.target) { - var closestTrigger = $mdUtil.getClosest(event.target, 'md-fab-trigger'); - var closestActions = $mdUtil.getClosest(event.target, 'md-fab-actions'); + function hasVisibiltyDirective(attrs) { + return visibilityDirectives.some(function(attr) { + return attrs[attr]; + }); + } +} - if (!closestTrigger && !closestActions) { - vm.close(); - } +function ngMessageDirective($mdUtil) { + return { + restrict: 'EA', + compile: compile, + priority: 100 + }; + + function compile(tElement) { + if (!isInsideInputContainer(tElement)) { + + // When the current element is inside of a document fragment, then we need to check for an input-container + // in the postLink, because the element will be later added to the DOM and is currently just in a temporary + // fragment, which causes the input-container check to fail. + if (isInsideFragment()) { + return function (scope, element) { + if (isInsideInputContainer(element)) { + // Inside of the postLink function, a ngMessage directive will be a comment element, because it's + // currently hidden. To access the shown element, we need to use the element from the compile function. + initMessageElement(tElement); + } + }; } + } else { + initMessageElement(tElement); } - 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; + function isInsideFragment() { + var nextNode = tElement[0]; + while (nextNode = nextNode.parentNode) { + if (nextNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { + return true; + } } + return false; } - function doActionPrev(event) { - focusAction(event, -1); + function isInsideInputContainer(element) { + return !!$mdUtil.getClosest(element, "md-input-container"); } - function doActionNext(event) { - focusAction(event, 1); + function initMessageElement(element) { + // Add our animation class + element.toggleClass('md-input-message-animation', true); } + } +} +ngMessageDirective.$inject = ["$mdUtil"]; - function focusAction(event, direction) { - var actions = resetActionTabIndexes(); - - // 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); +var $$AnimateRunner, $animateCss, $mdUtil; - // Focus the element - var focusElement = angular.element(actions[vm.currentActionIndex]).children()[0]; - angular.element(focusElement).attr('tabindex', 0); - focusElement.focus(); +function mdInputInvalidMessagesAnimation($$AnimateRunner, $animateCss, $mdUtil) { + saveSharedServices($$AnimateRunner, $animateCss, $mdUtil); - // Make sure the event doesn't bubble and cause something else - event.preventDefault(); - event.stopImmediatePropagation(); + return { + addClass: function(element, className, done) { + showInputMessages(element, done); } - function resetActionTabIndexes() { - // Grab all of the actions - var actions = getActionsElement()[0].querySelectorAll('.md-fab-action-item'); + // NOTE: We do not need the removeClass method, because the message ng-leave animation will fire + }; +} +mdInputInvalidMessagesAnimation.$inject = ["$$AnimateRunner", "$animateCss", "$mdUtil"]; - // Disable all other actions for tabbing - angular.forEach(actions, function(action) { - angular.element(angular.element(action).children()[0]).attr('tabindex', -1); - }); +function ngMessagesAnimation($$AnimateRunner, $animateCss, $mdUtil) { + saveSharedServices($$AnimateRunner, $animateCss, $mdUtil); - return actions; - } + return { + enter: function(element, done) { + showInputMessages(element, done); + }, - function doKeyLeft(event) { - if (vm.direction === 'left') { - doActionNext(event); + leave: function(element, done) { + hideInputMessages(element, done); + }, + + addClass: function(element, className, done) { + if (className == "ng-hide") { + hideInputMessages(element, done); } else { - doActionPrev(event); + done(); } - } + }, - function doKeyUp(event) { - if (vm.direction === 'down') { - doActionPrev(event); + removeClass: function(element, className, done) { + if (className == "ng-hide") { + showInputMessages(element, done); } else { - doActionNext(event); + done(); } } + } +} +ngMessagesAnimation.$inject = ["$$AnimateRunner", "$animateCss", "$mdUtil"]; - function doKeyRight(event) { - if (vm.direction === 'left') { - doActionPrev(event); - } else { - doActionNext(event); - } +function ngMessageAnimation($$AnimateRunner, $animateCss, $mdUtil) { + saveSharedServices($$AnimateRunner, $animateCss, $mdUtil); + + return { + enter: function(element, done) { + return showMessage(element); + }, + + leave: function(element, done) { + return hideMessage(element); } + } +} +ngMessageAnimation.$inject = ["$$AnimateRunner", "$animateCss", "$mdUtil"]; + +function showInputMessages(element, done) { + var animators = [], animator; + var messages = getMessagesElement(element); + + angular.forEach(messages.children(), function(child) { + animator = showMessage(angular.element(child)); + + animators.push(animator.start()); + }); + + $$AnimateRunner.all(animators, done); +} + +function hideInputMessages(element, done) { + var animators = [], animator; + var messages = getMessagesElement(element); + + angular.forEach(messages.children(), function(child) { + animator = hideMessage(angular.element(child)); + + animators.push(animator.start()); + }); - function doKeyDown(event) { - if (vm.direction === 'up') { - doActionPrev(event); - } else { - doActionNext(event); - } - } + $$AnimateRunner.all(animators, done); +} - function isTrigger(element) { - return $mdUtil.getClosest(element, 'md-fab-trigger'); - } +function showMessage(element) { + var height = parseInt(window.getComputedStyle(element[0]).height); + var topMargin = parseInt(window.getComputedStyle(element[0]).marginTop); - function isAction(element) { - return $mdUtil.getClosest(element, 'md-fab-actions'); - } + var messages = getMessagesElement(element); + var container = getInputElement(element); - function handleItemClick(event) { - if (isTrigger(event.target)) { - vm.toggle(); - } + // Check to see if the message is already visible so we can skip + var alreadyVisible = (topMargin > -height); - if (isAction(event.target)) { - vm.close(); - } - } + // If we have the md-auto-hide class, the md-input-invalid animation will fire, so we can skip + if (alreadyVisible || (messages.hasClass('md-auto-hide') && !container.hasClass('md-input-invalid'))) { + return $animateCss(element, {}); + } - function getTriggerElement() { - return $element.find('md-fab-trigger'); - } + return $animateCss(element, { + event: 'enter', + structural: true, + from: {"opacity": 0, "margin-top": -height + "px"}, + to: {"opacity": 1, "margin-top": "0"}, + duration: 0.3 + }); +} - function getActionsElement() { - return $element.find('md-fab-actions'); - } +function hideMessage(element) { + var height = element[0].offsetHeight; + var styles = window.getComputedStyle(element[0]); + //var styles = { opacity: element.css('opacity') }; + + // If we are already hidden, just return an empty animation + if (styles.opacity == 0) { + return $animateCss(element, {}); } - MdFabController.$inject = ["$scope", "$element", "$animate", "$mdUtil", "$mdConstant", "$timeout"]; -})(); -})(); -(function(){ -"use strict"; + // 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 + }); +} -(function() { - 'use strict'; +function getInputElement(element) { + var inputContainer = element.controller('mdInputContainer'); - /** - * The duration of the CSS animation in milliseconds. - * - * @type {number} - */ - var cssAnimationDuration = 300; + return inputContainer.element; +} - /** - * @ngdoc module - * @name material.components.fabSpeedDial - */ - angular - // Declare our module - .module('material.components.fabSpeedDial', [ - 'material.core', - 'material.components.fabShared', - 'material.components.fabTrigger', - 'material.components.fabActions' - ]) +function getMessagesElement(element) { + // If we are a ng-message element, we need to traverse up the DOM tree + if (element.hasClass('md-input-message-animation')) { + return angular.element($mdUtil.getClosest(element, function(node) { + return node.classList.contains('md-input-messages-animation'); + })); + } - // Register our directive - .directive('mdFabSpeedDial', MdFabSpeedDialDirective) + // Otherwise, we can traverse down + return angular.element(element[0].querySelector('.md-input-messages-animation')); +} - // Register our custom animations - .animation('.md-fling', MdFabSpeedDialFlingAnimation) - .animation('.md-scale', MdFabSpeedDialScaleAnimation) +function saveSharedServices(_$$AnimateRunner_, _$animateCss_, _$mdUtil_) { + $$AnimateRunner = _$$AnimateRunner_; + $animateCss = _$animateCss_; + $mdUtil = _$mdUtil_; +} - // Register a service for each animation so that we can easily inject them into unit tests - .service('mdFabSpeedDialFlingAnimation', MdFabSpeedDialFlingAnimation) - .service('mdFabSpeedDialScaleAnimation', MdFabSpeedDialScaleAnimation); +})(); +(function(){ +"use strict"; - /** - * @ngdoc directive - * @name mdFabSpeedDial - * @module material.components.fabSpeedDial - * - * @restrict E - * - * @description - * The `` directive is used to present a series of popup elements (usually - * ``s) for quick access to common actions. - * - * There are currently two animations available by applying one of the following classes to - * the component: - * - * - `md-fling` - The speed dial items appear from underneath the trigger and move into their - * appropriate positions. - * - `md-scale` - The speed dial items appear in their proper places by scaling from 0% to 100%. - * - * You may also easily position the trigger by applying one one of the following classes to the - * `` element: - * - `md-fab-top-left` - * - `md-fab-top-right` - * - `md-fab-bottom-left` - * - `md-fab-bottom-right` - * - * These CSS classes use `position: absolute`, so you need to ensure that the container element - * also uses `position: absolute` or `position: relative` in order for them to work. - * - * Additionally, you may use the standard `ng-mouseenter` and `ng-mouseleave` directives to - * open or close the speed dial. However, if you wish to allow users to hover over the empty - * space where the actions will appear, you must also add the `md-hover-full` class to the speed - * dial element. Without this, the hover effect will only occur on top of the trigger. - * - * See the demos for more information. - * - * ## Troubleshooting - * - * If your speed dial shows the closing animation upon launch, you may need to use `ng-cloak` on - * the parent container to ensure that it is only visible once ready. We have plans to remove this - * necessity in the future. - * - * @usage - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * @param {string} md-direction From which direction you would like the speed dial to appear - * relative to the trigger element. - * @param {expression=} md-open Programmatically control whether or not the speed-dial is visible. - */ - function MdFabSpeedDialDirective() { - return { - restrict: 'E', +/** + * @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 + * + * @description + * The `` directive is a list container for 1..n `` tags. + * + * @usage + * + * + * + * + *
    + *

    {{item.title}}

    + *

    {{item.description}}

    + *
    + *
    + *
    + *
    + */ + +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 + * + * @description + * A `md-list-item` element can be used to represent some information in a row.
    + * + * @usage + * ### Single Row Item + * + * + * Single Row Item + * + * + * + * ### Multiple Lines + * By using the following markup, you will be able to have two lines inside of one `md-list-item`. + * + * + * + *
    + *

    First Line

    + *

    Second Line

    + *
    + *
    + *
    + * + * It is also possible to have three lines inside of one list item. + * + * + * + *
    + *

    First Line

    + *

    Second Line

    + *

    Third Line

    + *
    + *
    + *
    + * + * ### Secondary Items + * Secondary items are elements which will be aligned at the end of the `md-list-item`. + * + * + * + * Single Row Item + * + * Secondary Button + * + * + * + * + * It also possible to have multiple secondary items inside of one `md-list-item`. + * + * + * + * Single Row Item + * First Button + * Second Button + * + * + * + * ### Proxy Item + * Proxies are elements, which will execute their specific action on click
    + * Currently supported proxy items are + * - `md-checkbox` (Toggle) + * - `md-switch` (Toggle) + * - `md-menu` (Open) + * + * This means, when using a supported proxy item inside of `md-list-item`, the list item will + * become clickable and executes the associated action of the proxy element on click. + * + * + * + * First Line + * + * + * + * + * The `md-checkbox` element will be automatically detected as a proxy element and will toggle on click. + * + * + * + * First Line + * + * + * + * + * The recognized `md-switch` will toggle its state, when the user clicks on the `md-list-item`. + * + * It is also possible to have a `md-menu` inside of a `md-list-item`. + * + * + *

    Click anywhere to fire the secondary action

    + * + * + * + * + * + * + * + * Redial + * + * + * + * + * Check voicemail + * + * + * + * + * + * Notifications + * + * + * + * + *
    + *
    + * + * The menu will automatically open, when the users clicks on the `md-list-item`.
    + * + * If the developer didn't specify any position mode on the menu, the `md-list-item` will automatically detect the + * position mode and applies it to the `md-menu`. + * + * ### Avatars + * Sometimes you may want to have some avatars inside of the `md-list-item `.
    + * You are able to create a optimized icon for the list item, by applying the `.md-avatar` class on the `` element. + * + * + * + * + * Alan Turing + * + * + * When using `` for an avater, you have to use the `.md-avatar-icon` class. + * + * + * + * Timothy Kopra + * + * + * + * In cases, you have a `md-list-item`, which doesn't have any avatar, + * but you want to align it with the other avatar items, you have to use the `.md-offset` class. + * + * + * + * Jon Doe + * + * + * + * ### DOM modification + * The `md-list-item` component automatically detects if the list item should be clickable. + * + * --- + * If the `md-list-item` is clickable, we wrap all content inside of a `
    ` and create + * an overlaying button, which will will execute the given actions (like `ng-href`, `ng-click`) + * + * We create an overlaying button, instead of wrapping all content inside of the button, + * because otherwise some elements may not be clickable inside of the button. + * + * --- + * When using a secondary item inside of your list item, the `md-list-item` component will automatically create + * a secondary container at the end of the `md-list-item`, which contains all secondary items. + * + * The secondary item container is not static, because otherwise the overflow will not work properly on the + * list item. + * + */ +function mdListItemDirective($mdAria, $mdConstant, $mdUtil, $timeout) { + var proxiedTypes = ['md-checkbox', 'md-switch', 'md-menu']; + return { + restrict: 'E', + controller: 'MdListController', + compile: function(tEl, tAttrs) { - scope: { - direction: '@?mdDirection', - isOpen: '=?mdOpen' - }, + // Check for proxy controls (no ng-click on parent, and a control inside) + var secondaryItems = tEl[0].querySelectorAll('.md-secondary'); + var hasProxiedElement; + var proxyElement; + var itemContainer = tEl; - bindToController: true, - controller: 'MdFabController', - controllerAs: 'vm', + tEl[0].setAttribute('role', 'listitem'); - link: FabSpeedDialLink - }; + if (tAttrs.ngClick || tAttrs.ngDblclick || 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'); + } + } - function FabSpeedDialLink(scope, element) { - // Prepend an element to hold our CSS variables so we can use them in the animations below - element.prepend('
    '); - } - } + wrapSecondaryItems(); + setupToggleAria(); - function MdFabSpeedDialFlingAnimation($timeout) { - function delayDone(done) { $timeout(done, cssAnimationDuration, false); } + if (hasProxiedElement && proxyElement.nodeName === "MD-MENU") { + setupProxiedMenu(); + } - function runAnimation(element) { - // Don't run if we are still waiting and we are not ready - if (element.hasClass('_md-animations-waiting') && !element.hasClass('_md-animations-ready')) { - return; + function setupToggleAria() { + var toggleTypes = ['md-switch', 'md-checkbox']; + var toggle; + + 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); + } + } + } } - var el = element[0]; - var ctrl = element.controller('mdFabSpeedDial'); - var items = el.querySelectorAll('.md-fab-action-item'); + function setupProxiedMenu() { + var menuEl = angular.element(proxyElement); - // Grab our trigger element - var triggerElement = el.querySelector('md-fab-trigger'); + var isEndAligned = menuEl.parent().hasClass('md-secondary-container') || + proxyElement.parentNode.firstElementChild !== proxyElement; - // Grab our element which stores CSS variables - var variablesElement = el.querySelector('._md-css-variables'); + var xAxisPosition = 'left'; - // Setup JS variables based on our CSS variables - var startZIndex = parseInt(window.getComputedStyle(variablesElement).zIndex); + if (isEndAligned) { + // When the proxy item is aligned at the end of the list, we have to set the origin to the end. + xAxisPosition = 'right'; + } + + // Set the position mode / origin of the proxied menu. + if (!menuEl.attr('md-position-mode')) { + menuEl.attr('md-position-mode', xAxisPosition + ' target'); + } - // Always reset the items to their natural position/state - angular.forEach(items, function(item, index) { - var styles = item.style; + // Apply menu open binding to menu button + var menuOpenButton = menuEl.children().eq(0); + if (!hasClickEvent(menuOpenButton[0])) { + menuOpenButton.attr('ng-click', '$mdOpenMenu($event)'); + } - styles.transform = styles.webkitTransform = ''; - styles.transitionDelay = ''; - styles.opacity = 1; + if (!menuOpenButton.attr('aria-label')) { + menuOpenButton.attr('aria-label', 'Open List Menu'); + } + } - // Make the items closest to the trigger have the highest z-index - styles.zIndex = (items.length - index) + startZIndex; - }); + function wrapIn(type) { + if (type == 'div') { + itemContainer = angular.element('
    '); + itemContainer.append(tEl.contents()); + tEl.addClass('md-proxy-focus'); + } else { + // Element which holds the default list-item content. + itemContainer = angular.element( + '
    '+ + '
    '+ + '
    ' + ); - // Set the trigger to be above all of the actions so they disappear behind it. - triggerElement.style.zIndex = startZIndex + items.length + 1; + // Button which shows ripple and executes primary action. + var buttonWrap = angular.element( + '' + ); - // If the control is closed, hide the items behind the trigger - if (!ctrl.isOpen) { - angular.forEach(items, function(item, index) { - var newPosition, axis; - var styles = item.style; + buttonWrap[0].setAttribute('aria-label', tEl[0].textContent); - // Make sure to account for differences in the dimensions of the trigger verses the items - // so that we can properly center everything; this helps hide the item's shadows behind - // the trigger. - var triggerItemHeightOffset = (triggerElement.clientHeight - item.clientHeight) / 2; - var triggerItemWidthOffset = (triggerElement.clientWidth - item.clientWidth) / 2; + copyAttributes(tEl[0], buttonWrap[0]); - switch (ctrl.direction) { - case 'up': - newPosition = (item.scrollHeight * (index + 1) + triggerItemHeightOffset); - axis = 'Y'; - break; - case 'down': - newPosition = -(item.scrollHeight * (index + 1) + triggerItemHeightOffset); - axis = 'Y'; - break; - case 'left': - newPosition = (item.scrollWidth * (index + 1) + triggerItemWidthOffset); - axis = 'X'; - break; - case 'right': - newPosition = -(item.scrollWidth * (index + 1) + triggerItemWidthOffset); - axis = 'X'; - break; + // We allow developers to specify the `md-no-focus` class, to disable the focus style + // on the button executor. Once more classes should be forwarded, we should probably make the + // class forward more generic. + if (tEl.hasClass('md-no-focus')) { + buttonWrap.addClass('md-no-focus'); } - var newTranslate = 'translate' + axis + '(' + newPosition + 'px)'; - - styles.transform = styles.webkitTransform = newTranslate; - }); - } - } + // Append the button wrap before our list-item content, because it will overlay in relative. + itemContainer.prepend(buttonWrap); + itemContainer.children().eq(1).append(tEl.contents()); - return { - addClass: function(element, className, done) { - if (element.hasClass('md-fling')) { - runAnimation(element); - delayDone(done); - } else { - done(); + tEl.addClass('_md-button-wrap'); } - }, - removeClass: function(element, className, done) { - runAnimation(element); - delayDone(done); - } - } - } - MdFabSpeedDialFlingAnimation.$inject = ["$timeout"]; - - function MdFabSpeedDialScaleAnimation($timeout) { - function delayDone(done) { $timeout(done, cssAnimationDuration, false); } - - var delay = 65; - - function runAnimation(element) { - var el = element[0]; - var ctrl = element.controller('mdFabSpeedDial'); - var items = el.querySelectorAll('.md-fab-action-item'); - // Grab our element which stores CSS variables - var variablesElement = el.querySelector('._md-css-variables'); + tEl[0].setAttribute('tabindex', '-1'); + tEl.append(itemContainer); + } - // Setup JS variables based on our CSS variables - var startZIndex = parseInt(window.getComputedStyle(variablesElement).zIndex); + function wrapSecondaryItems() { + var secondaryItemsWrapper = angular.element('
    '); - // Always reset the items to their natural position/state - angular.forEach(items, function(item, index) { - var styles = item.style, - offsetDelay = index * delay; + angular.forEach(secondaryItems, function(secondaryItem) { + wrapSecondaryItem(secondaryItem, secondaryItemsWrapper); + }); - styles.opacity = ctrl.isOpen ? 1 : 0; - styles.transform = styles.webkitTransform = ctrl.isOpen ? 'scale(1)' : 'scale(0)'; - styles.transitionDelay = (ctrl.isOpen ? offsetDelay : (items.length - offsetDelay)) + 'ms'; + itemContainer.append(secondaryItemsWrapper); + } - // Make the items closest to the trigger have the highest z-index - styles.zIndex = (items.length - index) + startZIndex; - }); - } + function wrapSecondaryItem(secondaryItem, container) { + // If the current secondary item is not a button, but contains a ng-click attribute, + // the secondary item will be automatically wrapped inside of a button. + if (secondaryItem && !isButton(secondaryItem) && secondaryItem.hasAttribute('ng-click')) { - return { - addClass: function(element, className, done) { - runAnimation(element); - delayDone(done); - }, + $mdAria.expect(secondaryItem, 'aria-label'); + var buttonWrapper = angular.element(''); - removeClass: function(element, className, done) { - runAnimation(element); - delayDone(done); - } - } - } - MdFabSpeedDialScaleAnimation.$inject = ["$timeout"]; -})(); + // Copy the attributes from the secondary item to the generated button. + // We also support some additional attributes from the secondary item, + // because some developers may use a ngIf, ngHide, ngShow on their item. + copyAttributes(secondaryItem, buttonWrapper[0], ['ng-if', 'ng-hide', 'ng-show']); -})(); -(function(){ -"use strict"; + secondaryItem.setAttribute('tabindex', '-1'); + buttonWrapper.append(secondaryItem); -(function() { - 'use strict'; + secondaryItem = buttonWrapper[0]; + } - /** - * @ngdoc module - * @name material.components.fabToolbar - */ - angular - // Declare our module - .module('material.components.fabToolbar', [ - 'material.core', - 'material.components.fabShared', - 'material.components.fabTrigger', - 'material.components.fabActions' - ]) + if (secondaryItem && (!hasClickEvent(secondaryItem) || (!tAttrs.ngClick && isProxiedElement(secondaryItem)))) { + // In this case we remove the secondary class, so we can identify it later, when we searching for the + // proxy items. + angular.element(secondaryItem).removeClass('md-secondary'); + } - // Register our directive - .directive('mdFabToolbar', MdFabToolbarDirective) + tEl.addClass('md-with-secondary'); + container.append(secondaryItem); + } - // Register our custom animations - .animation('.md-fab-toolbar', MdFabToolbarAnimation) + /** + * Copies attributes from a source element to the destination element + * By default the function will copy the most necessary attributes, supported + * by the button executor for clickable list items. + * @param source Element with the specified attributes + * @param destination Element which will retrieve the attributes + * @param extraAttrs Additional attributes, which will be copied over. + */ + function copyAttributes(source, destination, extraAttrs) { + var copiedAttrs = $mdUtil.prefixer([ + 'ng-if', 'ng-click', 'ng-dblclick', 'aria-label', 'ng-disabled', 'ui-sref', + 'href', 'ng-href', 'target', 'ng-attr-ui-sref', 'ui-sref-opts' + ]); - // Register a service for the animation so that we can easily inject it into unit tests - .service('mdFabToolbarAnimation', MdFabToolbarAnimation); + if (extraAttrs) { + copiedAttrs = copiedAttrs.concat($mdUtil.prefixer(extraAttrs)); + } - /** - * @ngdoc directive - * @name mdFabToolbar - * @module material.components.fabToolbar - * - * @restrict E - * - * @description - * - * The `` directive is used present a toolbar of elements (usually ``s) - * for quick access to common actions when a floating action button is activated (via click or - * keyboard navigation). - * - * You may also easily position the trigger by applying one one of the following classes to the - * `` element: - * - `md-fab-top-left` - * - `md-fab-top-right` - * - `md-fab-bottom-left` - * - `md-fab-bottom-right` - * - * These CSS classes use `position: absolute`, so you need to ensure that the container element - * also uses `position: absolute` or `position: relative` in order for them to work. - * - * @usage - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * @param {string} md-direction From which direction you would like the toolbar items to appear - * relative to the trigger element. Supports `left` and `right` directions. - * @param {expression=} md-open Programmatically control whether or not the toolbar is visible. - */ - function MdFabToolbarDirective() { - return { - restrict: 'E', - transclude: true, - template: '
    ' + - '
    ' + - '
    ', + angular.forEach(copiedAttrs, function(attr) { + if (source.hasAttribute(attr)) { + destination.setAttribute(attr, source.getAttribute(attr)); + source.removeAttribute(attr); + } + }); + } - scope: { - direction: '@?mdDirection', - isOpen: '=?mdOpen' - }, + function isProxiedElement(el) { + return proxiedTypes.indexOf(el.nodeName.toLowerCase()) != -1; + } - bindToController: true, - controller: 'MdFabController', - controllerAs: 'vm', + function isButton(el) { + var nodeName = el.nodeName.toUpperCase(); - link: link - }; + return nodeName == "MD-BUTTON" || nodeName == "BUTTON"; + } - function link(scope, element, attributes) { - // Add the base class for animations - element.addClass('md-fab-toolbar'); + function hasClickEvent (element) { + var attr = element.attributes; + for (var i = 0; i < attr.length; i++) { + if (tAttrs.$normalize(attr[i].name) === 'ngClick') return true; + } + return false; + } - // Prepend the background element to the trigger's button - element.find('md-fab-trigger').find('button') - .prepend('
    '); - } - } + return postLink; - function MdFabToolbarAnimation() { + function postLink($scope, $element, $attr, ctrl) { + $element.addClass('_md'); // private md component indicator for styling - function runAnimation(element, className, done) { - // If no className was specified, don't do anything - if (!className) { - return; - } + var proxies = [], + firstElement = $element[0].firstElementChild, + isButtonWrap = $element.hasClass('_md-button-wrap'), + clickChild = isButtonWrap ? firstElement.firstElementChild : firstElement, + hasClick = clickChild && hasClickEvent(clickChild); - var el = element[0]; - var ctrl = element.controller('mdFabToolbar'); + computeProxies(); + computeClickable(); - // Grab the relevant child elements - var backgroundElement = el.querySelector('._md-fab-toolbar-background'); - var triggerElement = el.querySelector('md-fab-trigger button'); - var toolbarElement = el.querySelector('md-toolbar'); - var iconElement = el.querySelector('md-fab-trigger button md-icon'); - var actions = element.find('md-fab-actions').children(); + if ($element.hasClass('md-proxy-focus') && proxies.length) { + angular.forEach(proxies, function(proxy) { + proxy = angular.element(proxy); - // If we have both elements, use them to position the new background - if (triggerElement && backgroundElement) { - // Get our variables - var color = window.getComputedStyle(triggerElement).getPropertyValue('background-color'); - var width = el.offsetWidth; - var height = el.offsetHeight; + $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); + }); + }); + }); + } - // Make it twice as big as it should be since we scale from the center - var scale = 2 * (width / triggerElement.offsetWidth); - // Set some basic styles no matter what animation we're doing - backgroundElement.style.backgroundColor = color; - backgroundElement.style.borderRadius = width + 'px'; + function computeProxies() { + if (firstElement && firstElement.children && !hasClick) { - // If we're open - if (ctrl.isOpen) { - // Turn on toolbar pointer events when closed - toolbarElement.style.pointerEvents = 'inherit'; + angular.forEach(proxiedTypes, function(type) { - backgroundElement.style.width = triggerElement.offsetWidth + 'px'; - backgroundElement.style.height = triggerElement.offsetHeight + 'px'; - backgroundElement.style.transform = 'scale(' + scale + ')'; + // All elements which are not capable for being used a proxy have the .md-secondary class + // applied. These items had been sorted out in the secondary wrap function. + angular.forEach(firstElement.querySelectorAll(type + ':not(.md-secondary)'), function(child) { + proxies.push(child); + }); + }); - // Set the next close animation to have the proper delays - backgroundElement.style.transitionDelay = '0ms'; - iconElement && (iconElement.style.transitionDelay = '.3s'); + } + } - // Apply a transition delay to actions - angular.forEach(actions, function(action, index) { - action.style.transitionDelay = (actions.length - index) * 25 + 'ms'; - }); - } else { - // Turn off toolbar pointer events when closed - toolbarElement.style.pointerEvents = 'none'; + function computeClickable() { + if (proxies.length == 1 || hasClick) { + $element.addClass('md-clickable'); - // Scale it back down to the trigger's size - backgroundElement.style.transform = 'scale(1)'; + if (!hasClick) { + ctrl.attachRipple($scope, angular.element($element[0].querySelector('.md-no-style'))); + } + } + } - // Reset the position - backgroundElement.style.top = '0'; + function isEventFromControl(event) { + var forbiddenControls = ['md-slider']; - if (element.hasClass('md-right')) { - backgroundElement.style.left = '0'; - backgroundElement.style.right = null; + // If there is no path property in the event, then we can assume that the event was not bubbled. + if (!event.path) { + return forbiddenControls.indexOf(event.target.tagName.toLowerCase()) !== -1; } - if (element.hasClass('md-left')) { - backgroundElement.style.right = '0'; - backgroundElement.style.left = null; + // We iterate the event path up and check for a possible component. + // Our maximum index to search, is the list item root. + var maxPath = event.path.indexOf($element.children()[0]); + + for (var i = 0; i < maxPath; i++) { + if (forbiddenControls.indexOf(event.path[i].tagName.toLowerCase()) !== -1) { + return true; + } } + } - // Set the next open animation to have the proper delays - backgroundElement.style.transitionDelay = '200ms'; - iconElement && (iconElement.style.transitionDelay = '0ms'); + var clickChildKeypressListener = 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 (clickChild) { + clickChild.click(); + e.preventDefault(); + e.stopPropagation(); + } + } + } + }; - // Apply a transition delay to actions - angular.forEach(actions, function(action, index) { - action.style.transitionDelay = 200 + (index * 25) + 'ms'; + if (!hasClick && !proxies.length) { + clickChild && clickChild.addEventListener('keypress', clickChildKeypressListener); + } + + $element.off('click'); + $element.off('keypress'); + + if (proxies.length == 1 && clickChild) { + $element.children().eq(0).on('click', function(e) { + // When the event is coming from an control and it should not trigger the proxied element + // then we are skipping. + if (isEventFromControl(e)) return; + + var parentButton = $mdUtil.getClosest(e.target, 'BUTTON'); + if (!parentButton && clickChild.contains(e.target)) { + angular.forEach(proxies, function(proxy) { + if (e.target !== proxy && !proxy.contains(e.target)) { + if (proxy.nodeName === 'MD-MENU') { + proxy = proxy.children[0]; + } + angular.element(proxy).triggerHandler('click'); + } + }); + } }); } + + $scope.$on('$destroy', function () { + clickChild && clickChild.removeEventListener('keypress', clickChildKeypressListener); + }); } } + }; +} +mdListItemDirective.$inject = ["$mdAria", "$mdConstant", "$mdUtil", "$timeout"]; - return { - addClass: function(element, className, done) { - runAnimation(element, className, done); - done(); - }, +/* + * @private + * @ngdoc controller + * @name MdListController + * @module material.components.list + * + */ +function MdListController($scope, $element, $mdListInkRipple) { + var ctrl = this; + ctrl.attachRipple = attachRipple; - removeClass: function(element, className, done) { - runAnimation(element, className, done); - done(); - } - } + function attachRipple (scope, element) { + var options = {}; + $mdListInkRipple.attach(scope, element, options); } -})(); +} +MdListController.$inject = ["$scope", "$element", "$mdListInkRipple"]; })(); (function(){ "use strict"; -(function() { - 'use strict'; - - /** - * @ngdoc module - * @name material.components.fabTrigger - */ - angular - .module('material.components.fabTrigger', ['material.core']) - .directive('mdFabTrigger', MdFabTriggerDirective); +/** + * @ngdoc module + * @name material.components.menu + */ - /** - * @ngdoc directive - * @name mdFabTrigger - * @module material.components.fabSpeedDial - * - * @restrict E - * - * @description - * The `` directive is used inside of a `` or - * `` directive to mark an element (or elements) as the trigger and setup the - * proper event listeners. - * - * @usage - * See the `` or `` directives for example usage. - */ - function MdFabTriggerDirective() { - // TODO: Remove this completely? - return { - restrict: 'E', +angular.module('material.components.menu', [ + 'material.core', + 'material.components.backdrop' +]); - require: ['^?mdFabSpeedDial', '^?mdFabToolbar'] - }; - } })(); +(function(){ +"use strict"; + +/** + * @ngdoc module + * @name material.components.menu-bar + */ +angular.module('material.components.menuBar', [ + 'material.core', + 'material.components.menu' +]); })(); (function(){ @@ -12509,5616 +12817,6410 @@ MdDividerDirective.$inject = ["$mdTheming"]; /** * @ngdoc module - * @name material.components.gridList + * @name material.components.navBar */ -angular.module('material.components.gridList', ['material.core']) - .directive('mdGridList', GridListDirective) - .directive('mdGridTile', GridTileDirective) - .directive('mdGridTileFooter', GridTileCaptionDirective) - .directive('mdGridTileHeader', GridTileCaptionDirective) - .factory('$mdGridLayout', GridLayoutFactory); + +angular.module('material.components.navBar', ['material.core']) + .controller('MdNavBarController', MdNavBarController) + .directive('mdNavBar', MdNavBar) + .controller('MdNavItemController', MdNavItemController) + .directive('mdNavItem', MdNavItem); + + +/***************************************************************************** + * PUBLIC DOCUMENTATION * + *****************************************************************************/ /** * @ngdoc directive - * @name mdGridList - * @module material.components.gridList + * @name mdNavBar + * @module material.components.navBar + * * @restrict E + * * @description - * Grid lists are an alternative to standard list views. Grid lists are distinct - * from grids used for layouts and other visual presentations. + * The `` directive renders a list of material tabs that can be used + * for top-level page navigation. Unlike ``, it has no concept of a tab + * body and no bar pagination. * - * A grid list is best suited to presenting a homogenous data type, typically - * images, and is optimized for visual comprehension and differentiating between - * like data types. + * Because it deals with page navigation, certain routing concepts are built-in. + * Route changes via via ng-href, ui-sref, or ng-click events are supported. + * Alternatively, the user could simply watch currentNavItem for changes. * - * A grid list is a continuous element consisting of tessellated, regular - * subdivisions called cells that contain tiles (`md-grid-tile`). + * 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 * - * Concept of grid explained visually - * Grid concepts legend + * @param {string=} mdSelectedNavItem The name of the current tab; this must + * match the name attribute of `` + * @param {string=} navBarAriaLabel An aria-label for the nav-bar * - * Cells are arrayed vertically and horizontally within the grid. + * @usage + * + * + * Page One + * Page Two + * Page Three + * + * + * + * (function() { + * ‘use strict’; * - * Tiles hold content and can span one or more cells vertically or horizontally. + * $rootScope.$on('$routeChangeSuccess', function(event, current) { + * $scope.currentLink = getCurrentLinkFromRoute(current); + * }); + * }); + * + */ + +/***************************************************************************** + * mdNavItem + *****************************************************************************/ +/** + * @ngdoc directive + * @name mdNavItem + * @module material.components.navBar * - * ### Responsive Attributes + * @restrict E * - * The `md-grid-list` directive supports "responsive" attributes, which allow - * different `md-cols`, `md-gutter` and `md-row-height` values depending on the - * currently matching media query. + * @description + * `` describes a page navigation link within the `` + * component. It renders an md-button as the actual link. * - * In order to set a responsive attribute, first define the fallback value with - * the standard attribute name, then add additional attributes with the - * following convention: `{base-attribute-name}-{media-query-name}="{value}"` - * (ie. `md-cols-lg="8"`) + * Exactly one of the mdNavClick, mdNavHref, mdNavSref attributes are required to be + * specified. * - * @param {number} md-cols Number of columns in the grid. - * @param {string} md-row-height One of - *
      - *
    • CSS length - Fixed height rows (eg. `8px` or `1rem`)
    • - *
    • `{width}:{height}` - Ratio of width to height (eg. - * `md-row-height="16:9"`)
    • - *
    • `"fit"` - Height will be determined by subdividing the available - * height by the number of rows
    • - *
    - * @param {string=} md-gutter The amount of space between tiles in CSS units - * (default 1px) - * @param {expression=} md-on-layout Expression to evaluate after layout. Event - * object is available as `$event`, and contains performance information. + * @param {Function=} mdNavClick Function which will be called when the + * link is clicked to change the page. Renders as an `ng-click`. + * @param {string=} mdNavHref url to transition to when this link is clicked. + * Renders as an `ng-href`. + * @param {string=} mdNavSref Ui-router state to transition to when this link is + * clicked. Renders as a `ui-sref`. + * @param {string=} name The name of this link. Used by the nav bar to know + * which link is currently selected. * * @usage - * Basic: - * - * - * - * - * - * - * Fixed-height rows: - * - * - * - * - * - * - * Fit rows: - * - * - * - * - * - * - * Using responsive attributes: - * - * - * - * - * + * See `` for usage. */ -function GridListDirective($interpolate, $mdConstant, $mdGridLayout, $mdMedia) { + + +/***************************************************************************** + * IMPLEMENTATION * + *****************************************************************************/ + +function MdNavBar($mdAria) { return { restrict: 'E', - controller: GridListController, + transclude: true, + controller: MdNavBarController, + controllerAs: 'ctrl', + bindToController: true, scope: { - mdOnLayout: '&' + 'mdSelectedNavItem': '=?', + 'navBarAriaLabel': '@?', + }, + template: + '
    ' + + '' + + '' + + '
    ', + link: function(scope, element, attrs, ctrl) { + if (!ctrl.navBarAriaLabel) { + $mdAria.expectAsync(element, 'aria-label', angular.noop); + } }, - link: postLink }; +} +MdNavBar.$inject = ["$mdAria"]; + +/** + * 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; - function postLink(scope, element, attrs, ctrl) { - element.addClass('_md'); // private md component indicator for styling - - // Apply semantics - element.attr('role', 'list'); + /** @private @const {!angular.Scope} */ + this._$scope = $scope; - // Provide the controller with a way to trigger layouts. - ctrl.layoutDelegate = layoutDelegate; + /** @private @const {!Object} */ + this._$mdConstant = $mdConstant; - var invalidateLayout = angular.bind(ctrl, ctrl.invalidateLayout), - unwatchAttrs = watchMedia(); - scope.$on('$destroy', unwatchMedia); + // Data-bound variables. + /** @type {string} */ + this.mdSelectedNavItem; - /** - * Watches for changes in media, invalidating layout as necessary. - */ - function watchMedia() { - for (var mediaName in $mdConstant.MEDIA) { - $mdMedia(mediaName); // initialize - $mdMedia.getQuery($mdConstant.MEDIA[mediaName]) - .addListener(invalidateLayout); - } - return $mdMedia.watchResponsiveAttributes( - ['md-cols', 'md-row-height', 'md-gutter'], attrs, layoutIfMediaMatch); - } + /** @type {string} */ + this.navBarAriaLabel; - function unwatchMedia() { - ctrl.layoutDelegate = angular.noop; + // State variables. - unwatchAttrs(); - for (var mediaName in $mdConstant.MEDIA) { - $mdMedia.getQuery($mdConstant.MEDIA[mediaName]) - .removeListener(invalidateLayout); - } - } + /** @type {?angular.JQLite} */ + this._navBarEl = $element[0]; - /** - * Performs grid layout if the provided mediaName matches the currently - * active media type. - */ - function layoutIfMediaMatch(mediaName) { - if (mediaName == null) { - // TODO(shyndman): It would be nice to only layout if we have - // instances of attributes using this media type - ctrl.invalidateLayout(); - } else if ($mdMedia(mediaName)) { - ctrl.invalidateLayout(); - } + /** @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(); } + }); +} +MdNavBarController.$inject = ["$element", "$scope", "$timeout", "$mdConstant"]; - var lastLayoutProps; - /** - * Invokes the layout engine, and uses its results to lay out our - * tile elements. - * - * @param {boolean} tilesInvalidated Whether tiles have been - * added/removed/moved since the last layout. This is to avoid situations - * where tiles are replaced with properties identical to their removed - * counterparts. - */ - function layoutDelegate(tilesInvalidated) { - var tiles = getTileElements(); - var props = { - tileSpans: getTileSpans(tiles), - colCount: getColumnCount(), - rowMode: getRowMode(), - rowHeight: getRowHeight(), - gutter: getGutter() - }; - if (!tilesInvalidated && angular.equals(props, lastLayoutProps)) { - return; - } +/** + * Initializes the tab components once they exist. + * @private + */ +MdNavBarController.prototype._initTabs = function() { + this._inkbar = angular.element(this._navBarEl.getElementsByTagName('md-nav-ink-bar')[0]); - var performance = - $mdGridLayout(props.colCount, props.tileSpans, tiles) - .map(function(tilePositions, rowCount) { - return { - grid: { - element: element, - style: getGridStyle(props.colCount, rowCount, - props.gutter, props.rowMode, props.rowHeight) - }, - tiles: tilePositions.map(function(ps, i) { - return { - element: angular.element(tiles[i]), - style: getTileStyle(ps.position, ps.spans, - props.colCount, rowCount, - props.gutter, props.rowMode, props.rowHeight) - } - }) - } - }) - .reflow() - .performance(); + var self = this; + this._$timeout(function() { + self._updateTabs(self.mdSelectedNavItem, undefined); + }); - // Report layout - scope.mdOnLayout({ - $event: { - performance: performance - } - }); + 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); + }); + }); +}; - lastLayoutProps = props; - } +/** + * 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(); + var oldIndex = -1; + var newIndex = -1; + var newTab = this._getTabByName(newValue); + var oldTab = this._getTabByName(oldValue); - // Use $interpolate to do some simple string interpolation as a convenience. + if (oldTab) { + oldTab.setSelected(false); + oldIndex = tabs.indexOf(oldTab); + } - var startSymbol = $interpolate.startSymbol(); - var endSymbol = $interpolate.endSymbol(); + if (newTab) { + newTab.setSelected(true); + newIndex = tabs.indexOf(newTab); + } - // Returns an expression wrapped in the interpolator's start and end symbols. - function expr(exprStr) { - return startSymbol + exprStr + endSymbol; + this._$timeout(function() { + self._updateInkBarStyles(newTab, newIndex, oldIndex); + }); +}; + +/** + * 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); + + this._inkbar.css({display: newIndex < 0 ? 'none' : ''}); + + if(tab){ + var tabEl = tab.getButtonEl(); + var left = tabEl.offsetLeft; + + this._inkbar.css({left: left + 'px', width: tabEl.offsetWidth + 'px'}); + } +}; + +/** + * Returns an array of the current tabs. + * @return {!Array} + * @private + */ +MdNavBarController.prototype._getTabs = function() { + var linkArray = Array.prototype.slice.call( + this._navBarEl.querySelectorAll('.md-nav-item')); + return linkArray.map(function(el) { + return angular.element(el).controller('mdNavItem') + }); +}; + +/** + * 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; + }); +}; + +/** + * 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]; } + } - // The amount of space a single 1x1 tile would take up (either width or height), used as - // a basis for other calculations. This consists of taking the base size percent (as would be - // if evenly dividing the size between cells), and then subtracting the size of one gutter. - // However, since there are no gutters on the edges, each tile only uses a fration - // (gutterShare = numGutters / numCells) of the gutter size. (Imagine having one gutter per - // tile, and then breaking up the extra gutter on the edge evenly among the cells). - var UNIT = $interpolate(expr('share') + '% - (' + expr('gutter') + ' * ' + expr('gutterShare') + ')'); + return null; +}; - // The horizontal or vertical position of a tile, e.g., the 'top' or 'left' property value. - // The position comes the size of a 1x1 tile plus gutter for each previous tile in the - // row/column (offset). - var POSITION = $interpolate('calc((' + expr('unit') + ' + ' + expr('gutter') + ') * ' + expr('offset') + ')'); +/** + * 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); + } +}; + +/** + * Clear tab focus when focus leaves the nav bar. + */ +MdNavBarController.prototype.onBlur = function() { + var tab = this.getFocusedTab(); + if (tab) { + tab.setFocused(false); + } +}; - // The actual size of a tile, e.g., width or height, taking rowSpan or colSpan into account. - // This is computed by multiplying the base unit by the rowSpan/colSpan, and then adding back - // in the space that the gutter would normally have used (which was already accounted for in - // the base unit calculation). - var DIMENSION = $interpolate('calc((' + expr('unit') + ') * ' + expr('span') + ' + (' + expr('span') + ' - 1) * ' + expr('gutter') + ')'); +/** + * 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); +}; - /** - * Gets the styles applied to a tile element described by the given parameters. - * @param {{row: number, col: number}} position The row and column indices of the tile. - * @param {{row: number, col: number}} spans The rowSpan and colSpan of the tile. - * @param {number} colCount The number of columns. - * @param {number} rowCount The number of rows. - * @param {string} gutter The amount of space between tiles. This will be something like - * '5px' or '2em'. - * @param {string} rowMode The row height mode. Can be one of: - * 'fixed': all rows have a fixed size, given by rowHeight, - * 'ratio': row height defined as a ratio to width, or - * 'fit': fit to the grid-list element height, divinding evenly among rows. - * @param {string|number} rowHeight The height of a row. This is only used for 'fixed' mode and - * for 'ratio' mode. For 'ratio' mode, this is the *ratio* of width-to-height (e.g., 0.75). - * @returns {Object} Map of CSS properties to be applied to the style element. Will define - * values for top, left, width, height, marginTop, and paddingTop. - */ - function getTileStyle(position, spans, colCount, rowCount, gutter, rowMode, rowHeight) { - // TODO(shyndman): There are style caching opportunities here. +/** + * 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; - // Percent of the available horizontal space that one column takes up. - var hShare = (1 / colCount) * 100; + var focusedTabIndex = tabs.indexOf(focusedTab); - // Fraction of the gutter size that each column takes up. - var hGutterShare = (colCount - 1) / colCount; + // 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; + } +}; - // Base horizontal size of a column. - var hUnit = UNIT({share: hShare, gutterShare: hGutterShare, gutter: gutter}); +/** + * @ngInject + */ +function MdNavItem($$rAF) { + return { + restrict: 'E', + require: ['mdNavItem', '^mdNavBar'], + controller: MdNavItemController, + bindToController: true, + controllerAs: 'ctrl', + replace: true, + transclude: true, + template: + '
  • ' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
  • ', + scope: { + 'mdNavClick': '&?', + 'mdNavHref': '@?', + 'mdNavSref': '@?', + 'name': '@', + }, + link: function(scope, element, attrs, controllers) { + var mdNavItem = controllers[0]; + var mdNavBar = controllers[1]; - // The width and horizontal position of each tile is always calculated the same way, but the - // height and vertical position depends on the rowMode. - var style = { - left: POSITION({ unit: hUnit, offset: position.col, gutter: gutter }), - width: DIMENSION({ unit: hUnit, span: spans.col, gutter: gutter }), - // resets - paddingTop: '', - marginTop: '', - top: '', - height: '' - }; + // 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() { + if (!mdNavItem.name) { + mdNavItem.name = angular.element(element[0].querySelector('._md-nav-button-text')) + .text().trim(); + } - switch (rowMode) { - case 'fixed': - // In fixed mode, simply use the given rowHeight. - style.top = POSITION({ unit: rowHeight, offset: position.row, gutter: gutter }); - style.height = DIMENSION({ unit: rowHeight, span: spans.row, gutter: gutter }); - break; + var navButton = angular.element(element[0].querySelector('._md-nav-button')); + navButton.on('click', function() { + mdNavBar.mdSelectedNavItem = mdNavItem.name; + scope.$apply(); + }); + }); + } + }; +} +MdNavItem.$inject = ["$$rAF"]; - case 'ratio': - // Percent of the available vertical space that one row takes up. Here, rowHeight holds - // the ratio value. For example, if the width:height ratio is 4:3, rowHeight = 1.333. - var vShare = hShare / rowHeight; +/** + * Controller for the nav-item component. + * @param {!angular.JQLite} $element + * @constructor + * @final + * @ngInject + */ +function MdNavItemController($element) { - // Base veritcal size of a row. - var vUnit = UNIT({ share: vShare, gutterShare: hGutterShare, gutter: gutter }); + /** @private @const {!angular.JQLite} */ + this._$element = $element; - // padidngTop and marginTop are used to maintain the given aspect ratio, as - // a percentage-based value for these properties is applied to the *width* of the - // containing block. See http://www.w3.org/TR/CSS2/box.html#margin-properties - style.paddingTop = DIMENSION({ unit: vUnit, span: spans.row, gutter: gutter}); - style.marginTop = POSITION({ unit: vUnit, offset: position.row, gutter: gutter }); - break; + // Data-bound variables + /** @const {?Function} */ + this.mdNavClick; + /** @const {?string} */ + this.mdNavHref; + /** @const {?string} */ + this.name; - case 'fit': - // Fraction of the gutter size that each column takes up. - var vGutterShare = (rowCount - 1) / rowCount; + // State variables + /** @private {boolean} */ + this._selected = false; - // Percent of the available vertical space that one row takes up. - var vShare = (1 / rowCount) * 100; + /** @private {boolean} */ + this._focused = false; - // Base vertical size of a row. - var vUnit = UNIT({share: vShare, gutterShare: vGutterShare, gutter: gutter}); + var hasNavClick = !!($element.attr('md-nav-click')); + var hasNavHref = !!($element.attr('md-nav-href')); + var hasNavSref = !!($element.attr('md-nav-sref')); - style.top = POSITION({unit: vUnit, offset: position.row, gutter: gutter}); - style.height = DIMENSION({unit: vUnit, span: spans.row, gutter: gutter}); - break; - } + // Cannot specify more than one nav attribute + if ((hasNavClick ? 1:0) + (hasNavHref ? 1:0) + (hasNavSref ? 1:0) > 1) { + throw Error( + 'Must specify exactly one of md-nav-click, md-nav-href, ' + + 'md-nav-sref for nav-item directive'); + } +} +MdNavItemController.$inject = ["$element"]; - return style; - } +/** + * 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, + }; +}; - function getGridStyle(colCount, rowCount, gutter, rowMode, rowHeight) { - var style = {}; +/** + * Get the name attribute of the tab. + * @return {string} + */ +MdNavItemController.prototype.getName = function() { + return this.name; +}; - switch(rowMode) { - case 'fixed': - style.height = DIMENSION({ unit: rowHeight, span: rowCount, gutter: gutter }); - style.paddingBottom = ''; - break; +/** + * Get the button element associated with the tab. + * @return {!Element} + */ +MdNavItemController.prototype.getButtonEl = function() { + return this._$element[0].querySelector('._md-nav-button'); +}; - case 'ratio': - // rowHeight is width / height - var hGutterShare = colCount === 1 ? 0 : (colCount - 1) / colCount, - hShare = (1 / colCount) * 100, - vShare = hShare * (1 / rowHeight), - vUnit = UNIT({ share: vShare, gutterShare: hGutterShare, gutter: gutter }); +/** + * Set the selected state of the tab. + * @param {boolean} isSelected + */ +MdNavItemController.prototype.setSelected = function(isSelected) { + this._selected = isSelected; +}; - style.height = ''; - style.paddingBottom = DIMENSION({ unit: vUnit, span: rowCount, gutter: gutter}); - break; +/** + * @return {boolean} + */ +MdNavItemController.prototype.isSelected = function() { + return this._selected; +}; - case 'fit': - // noop, as the height is user set - break; - } +/** + * Set the focused state of the tab. + * @param {boolean} isFocused + */ +MdNavItemController.prototype.setFocused = function(isFocused) { + this._focused = isFocused; +}; - return style; - } +/** + * @return {boolean} + */ +MdNavItemController.prototype.hasFocus = function() { + return this._focused; +}; - function getTileElements() { - return [].filter.call(element.children(), function(ele) { - return ele.tagName == 'MD-GRID-TILE' && !ele.$$mdDestroyed; - }); - } +})(); +(function(){ +"use strict"; - /** - * Gets an array of objects containing the rowspan and colspan for each tile. - * @returns {Array<{row: number, col: number}>} - */ - function getTileSpans(tileElements) { - return [].map.call(tileElements, function(ele) { - var ctrl = angular.element(ele).controller('mdGridTile'); - return { - row: parseInt( - $mdMedia.getResponsiveAttribute(ctrl.$attrs, 'md-rowspan'), 10) || 1, - col: parseInt( - $mdMedia.getResponsiveAttribute(ctrl.$attrs, 'md-colspan'), 10) || 1 - }; - }); - } +/** + * @ngdoc module + * @name material.components.panel + */ +angular + .module('material.components.panel', [ + 'material.core', + 'material.components.backdrop' + ]) + .service('$mdPanel', MdPanelService); - function getColumnCount() { - var colCount = parseInt($mdMedia.getResponsiveAttribute(attrs, 'md-cols'), 10); - if (isNaN(colCount)) { - throw 'md-grid-list: md-cols attribute was not found, or contained a non-numeric value'; - } - return colCount; - } - function getGutter() { - return applyDefaultUnit($mdMedia.getResponsiveAttribute(attrs, 'md-gutter') || 1); - } +/***************************************************************************** + * PUBLIC DOCUMENTATION * + *****************************************************************************/ - function getRowHeight() { - var rowHeight = $mdMedia.getResponsiveAttribute(attrs, 'md-row-height'); - if (!rowHeight) { - throw 'md-grid-list: md-row-height attribute was not found'; - } +/** + * @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 = $mdPanelPosition + * .absolute() + * .top('50%') + * .left('50%'); + * + * var panelAnimation = $mdPanelAnimation + * .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 + * } + * panelRef = $mdPanel.create(config); + * panelRef.open() + * .finally(function() { + * panelRef = undefined; + * }); + * } + * + * function DialogController(MdPanelRef, toppings) { + * var toppings; + * + * function closeDialog() { + * MdPanelRef.close(); + * } + * } + * })(angular); + * + */ - switch (getRowMode()) { - case 'fixed': - return applyDefaultUnit(rowHeight); - case 'ratio': - var whRatio = rowHeight.split(':'); - return parseFloat(whRatio[0]) / parseFloat(whRatio[1]); - case 'fit': - return 0; // N/A - } - } +/** + * @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: + * + * - `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. + * - `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. + * - `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. + * + * @returns {MdPanelRef} panelRef + */ - function getRowMode() { - var rowHeight = $mdMedia.getResponsiveAttribute(attrs, 'md-row-height'); - if (!rowHeight) { - throw 'md-grid-list: md-row-height attribute was not found'; - } - if (rowHeight == 'fit') { - return 'fit'; - } else if (rowHeight.indexOf(':') !== -1) { - return 'ratio'; - } else { - return 'fixed'; - } - } +/** + * @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. + */ - function applyDefaultUnit(val) { - return /\D$/.test(val) ? val : val + 'px'; - } - } -} -GridListDirective.$inject = ["$interpolate", "$mdConstant", "$mdGridLayout", "$mdMedia"]; -/* @ngInject */ -function GridListController($mdUtil) { - this.layoutInvalidated = false; - this.tilesInvalidated = false; - this.$timeout_ = $mdUtil.nextTick; - this.layoutDelegate = angular.noop; -} -GridListController.$inject = ["$mdUtil"]; +/** + * @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 + */ -GridListController.prototype = { - invalidateTiles: function() { - this.tilesInvalidated = true; - this.invalidateLayout(); - }, - invalidateLayout: function() { - if (this.layoutInvalidated) { - return; - } - this.layoutInvalidated = true; - this.$timeout_(angular.bind(this, this.layout)); - }, +/** + * @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 + */ - layout: function() { - try { - this.layoutDelegate(this.tilesInvalidated); - } finally { - this.layoutInvalidated = false; - this.tilesInvalidated = false; - } - } -}; +/***************************************************************************** + * MdPanelRef * + *****************************************************************************/ -/* @ngInject */ -function GridLayoutFactory($mdUtil) { - var defaultAnimator = GridTileAnimator; - /** - * Set the reflow animator callback - */ - GridLayout.animateWith = function(customAnimator) { - defaultAnimator = !angular.isFunction(customAnimator) ? GridTileAnimator : customAnimator; - }; +/** + * @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. + */ - return GridLayout; +/** + * @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. + */ - /** - * Publish layout function - */ - function GridLayout(colCount, tileSpans) { - var self, layoutInfo, gridStyles, layoutTime, mapTime, reflowTime; +/** + * @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. + */ - layoutTime = $mdUtil.time(function() { - layoutInfo = calculateGridFor(colCount, tileSpans); - }); +/** + * @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. + */ - return self = { +/** + * @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. + */ - /** - * An array of objects describing each tile's position in the grid. - */ - layoutInfo: function() { - return layoutInfo; - }, +/** + * @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. + */ - /** - * Maps grid positioning to an element and a set of styles using the - * provided updateFn. - */ - map: function(updateFn) { - mapTime = $mdUtil.time(function() { - var info = self.layoutInfo(); - gridStyles = updateFn(info.positioning, info.rowCount); - }); - return self; - }, +/** + * @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. + */ - /** - * Default animator simply sets the element.css( ). An alternate - * animator can be provided as an argument. The function has the following - * signature: - * - * function({grid: {element: JQLite, style: Object}, tiles: Array<{element: JQLite, style: Object}>) - */ - reflow: function(animatorFn) { - reflowTime = $mdUtil.time(function() { - var animator = animatorFn || defaultAnimator; - animator(gridStyles.grid, gridStyles.tiles); - }); - return self; - }, +/** + * @ngdoc method + * @name MdPanelRef#destroy + * @description + * Destroys the panel. The panel cannot be opened again after this is called. + */ - /** - * Timing for the most recent layout run. - */ - performance: function() { - return { - tileCount: tileSpans.length, - layoutTime: layoutTime, - mapTime: mapTime, - reflowTime: reflowTime, - totalTime: layoutTime + mapTime + reflowTime - }; - } - }; - } +/** + * @ngdoc method + * @name MdPanelRef#addClass + * @description + * Adds a class to the panel. DO NOT use this to 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. + */ - /** - * Default Gridlist animator simple sets the css for each element; - * NOTE: any transitions effects must be manually set in the CSS. - * e.g. - * - * md-grid-tile { - * transition: all 700ms ease-out 50ms; - * } - * - */ - function GridTileAnimator(grid, tiles) { - grid.element.css(grid.style); - tiles.forEach(function(t) { - t.element.css(t.style); - }) - } +/** + * @ngdoc method + * @name MdPanelRef#removeClass + * @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. + */ - /** - * Calculates the positions of tiles. - * - * The algorithm works as follows: - * An Array with length colCount (spaceTracker) keeps track of - * available tiling positions, where elements of value 0 represents an - * empty position. Space for a tile is reserved by finding a sequence of - * 0s with length <= than the tile's colspan. When such a space has been - * found, the occupied tile positions are incremented by the tile's - * rowspan value, as these positions have become unavailable for that - * many rows. - * - * If the end of a row has been reached without finding space for the - * tile, spaceTracker's elements are each decremented by 1 to a minimum - * of 0. Rows are searched in this fashion until space is found. - */ - function calculateGridFor(colCount, tileSpans) { - var curCol = 0, - curRow = 0, - spaceTracker = newSpaceTracker(); +/** + * @ngdoc method + * @name MdPanelRef#toggleClass + * @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 + */ - return { - positioning: tileSpans.map(function(spans, i) { - return { - spans: spans, - position: reserveSpace(spans, i) - }; - }), - rowCount: curRow + Math.max.apply(Math, spaceTracker) - }; - function reserveSpace(spans, i) { - if (spans.col > colCount) { - throw 'md-grid-list: Tile at position ' + i + ' has a colspan ' + - '(' + spans.col + ') that exceeds the column count ' + - '(' + colCount + ')'; - } +/***************************************************************************** + * MdPanelPosition * + *****************************************************************************/ - var start = 0, - end = 0; - // TODO(shyndman): This loop isn't strictly necessary if you can - // determine the minimum number of rows before a space opens up. To do - // this, recognize that you've iterated across an entire row looking for - // space, and if so fast-forward by the minimum rowSpan count. Repeat - // until the required space opens up. - while (end - start < spans.col) { - if (curCol >= colCount) { - nextRow(); - continue; - } +/** + * @ngdoc type + * @name MdPanelPosition + * @module material.components.panel + * @description + * Object for configuring the position of the panel. Examples: + * + * 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); + */ - start = spaceTracker.indexOf(0, curCol); - if (start === -1 || (end = findEnd(start + 1)) === -1) { - start = end = 0; - nextRow(); - continue; - } +/** + * @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} + */ - curCol = end + 1; - } +/** + * @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} + */ - adjustRow(start, spans.col, spans.row); - curCol = start + spans.col; +/** + * @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} + */ - return { - col: start, - row: curRow - }; - } +/** + * @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} + */ - function nextRow() { - curCol = 0; - curRow++; - adjustRow(0, colCount, -1); // Decrement row spans by one - } +/** + * @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} + */ - function adjustRow(from, cols, by) { - for (var i = from; i < from + cols; i++) { - spaceTracker[i] = Math.max(spaceTracker[i] + by, 0); - } - } +/** + * @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} + */ - function findEnd(start) { - var i; - for (i = start; i < spaceTracker.length; i++) { - if (spaceTracker[i] !== 0) { - return i; - } - } +/** + * @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} + */ - if (i === spaceTracker.length) { - return i; - } - } +/** + * @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} + */ - function newSpaceTracker() { - var tracker = []; - for (var i = 0; i < colCount; i++) { - tracker.push(0); - } - return tracker; - } - } -} -GridLayoutFactory.$inject = ["$mdUtil"]; +/** + * @ngdoc method + * @name MdPanelPosition#centerHorizontally + * @description + * Centers the panel horizontally in the viewport. Clears any previously set + * horizontal position. + * @returns {MdPanelPosition} + */ /** - * @ngdoc directive - * @name mdGridTile - * @module material.components.gridList - * @restrict E + * @ngdoc method + * @name MdPanelPosition#centerVertically * @description - * Tiles contain the content of an `md-grid-list`. They span one or more grid - * cells vertically or horizontally, and use `md-grid-tile-{footer,header}` to - * display secondary content. - * - * ### Responsive Attributes + * 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 + * @param {string} xPosition + * @param {string} yPosition + * @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. * - * The `md-grid-tile` directive supports "responsive" attributes, which allow - * different `md-rowspan` and `md-colspan` values depending on the currently - * matching media query. + * xPosition must be one of the following values available on + * $mdPanel.xPosition: * - * In order to set a responsive attribute, first define the fallback value with - * the standard attribute name, then add additional attributes with the - * following convention: `{base-attribute-name}-{media-query-name}="{value}"` - * (ie. `md-colspan-sm="4"`) + * CENTER | ALIGN_START | ALIGN_END | OFFSET_START | OFFSET_END * - * @param {number=} md-colspan The number of columns to span (default 1). Cannot - * exceed the number of columns in the grid. Supports interpolation. - * @param {number=} md-rowspan The number of rows to span (default 1). Supports - * interpolation. + * ************* + * * * + * * PANEL * + * * * + * ************* + * A B C D E * - * @usage - * With header: - * - * - * - *

    This is a header

    - *
    - *
    - *
    + * 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) * - * With footer: - * - * - * - *

    This is a footer

    - *
    - *
    - *
    + * yPosition must be one of the following values available on + * $mdPanel.yPosition: * - * Spanning multiple rows/columns: - * - * - * - * + * CENTER | ALIGN_TOPS | ALIGN_BOTTOMS | ABOVE | BELOW * - * Responsive attributes: - * - * - * - * + * F + * G ************* + * * * + * H * PANEL * + * * * + * I ************* + * J + * + * F: BELOW + * G: ALIGN_TOPS + * H: CENTER + * I: ALIGN_BOTTOMS + * J: ABOVE + * @returns {MdPanelPosition} */ -function GridTileDirective($mdMedia) { - return { - restrict: 'E', - require: '^mdGridList', - template: '
    ', - transclude: true, - scope: {}, - // Simple controller that exposes attributes to the grid directive - controller: ["$attrs", function($attrs) { - this.$attrs = $attrs; - }], - link: postLink - }; - function postLink(scope, element, attrs, gridCtrl) { - // Apply semantics - element.attr('role', 'listitem'); - - // If our colspan or rowspan changes, trigger a layout - var unwatchAttrs = $mdMedia.watchResponsiveAttributes(['md-colspan', 'md-rowspan'], - attrs, angular.bind(gridCtrl, gridCtrl.invalidateLayout)); - - // Tile registration/deregistration - gridCtrl.invalidateTiles(); - scope.$on('$destroy', function() { - // Mark the tile as destroyed so it is no longer considered in layout, - // even if the DOM element sticks around (like during a leave animation) - element[0].$$mdDestroyed = true; - unwatchAttrs(); - gridCtrl.invalidateLayout(); - }); +/** + * @ngdoc method + * @name MdPanelPosition#withOffsetX + * @description + * Sets the value of the offset in the x-direction. + * @param {string} offsetX + * @returns {MdPanelPosition} + */ - if (angular.isDefined(scope.$parent.$index)) { - scope.$watch(function() { return scope.$parent.$index; }, - function indexChanged(newIdx, oldIdx) { - if (newIdx === oldIdx) { - return; - } - gridCtrl.invalidateTiles(); - }); - } - } -} -GridTileDirective.$inject = ["$mdMedia"]; +/** + * @ngdoc method + * @name MdPanelPosition#withOffsetY + * @description + * Sets the value of the offset in the y-direction. + * @param {string} offsetY + * @returns {MdPanelPosition} + */ -function GridTileCaptionDirective() { - return { - template: '
    ', - transclude: true - }; -} +/***************************************************************************** + * MdPanelAnimation * + *****************************************************************************/ -})(); -(function(){ -"use strict"; /** - * @ngdoc module - * @name material.components.icon + * @ngdoc object + * @name MdPanelAnimation * @description - * Icon + * Animation configuration object. To use, create an MdPanelAnimation with the + * desired properties, then pass the object as part of $mdPanel creation. + * + * Example: + * + * var panelAnimation = new MdPanelAnimation() + * .openFrom(myButtonEl) + * .closeTo('.my-button') + * .withAnimation($mdPanel.animation.SCALE); + * + * $mdPanel.create({ + * animation: panelAnimation + * }); */ -angular.module('material.components.icon', ['material.core']); -})(); -(function(){ -"use strict"; +/** + * @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 module - * @name material.components.list + * @ngdoc method + * @name MdPanelAnimation#closeTo * @description - * List module + * 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} */ -angular.module('material.components.list', [ - 'material.core' -]) - .controller('MdListController', MdListController) - .directive('mdList', mdListDirective) - .directive('mdListItem', mdListItemDirective); /** - * @ngdoc directive - * @name mdList - * @module material.components.list + * @ngdoc method + * @name MdPanelAnimation#withAnimation + * @description + * Specifies the animation class. * - * @restrict E + * 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. * - * @description - * The `` directive is a list container for 1..n `` tags. + * Custom classes will by default fade in and out unless + * "transition: opacity 1ms" is added to the to custom class. * - * @usage - * - * - * - * - *
    - *

    {{item.title}}

    - *

    {{item.description}}

    - *
    - *
    - *
    - *
    + * @param {string|{open: string, close: string}} cssClass + * @returns {MdPanelAnimation} */ -function mdListDirective($mdTheming) { - return { - restrict: 'E', - compile: function(tEl) { - tEl[0].setAttribute('role', 'list'); - return $mdTheming; - } + +/***************************************************************************** + * IMPLEMENTATION * + *****************************************************************************/ + + +// Default z-index for the panel. +var defaultZIndex = 80; +var MD_PANEL_HIDDEN = '_md-panel-hidden'; + +var FOCUS_TRAP_TEMPLATE = angular.element( + '
    '); + + +/** + * A service that is used for controlling/displaying panels on the screen. + * @param {!angular.JQLite} $rootElement + * @param {!angular.Scope} $rootScope + * @param {!angular.$injector} $injector + * @param {!angular.$window} $window + * @final @constructor @ngInject + */ +function MdPanelService($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} + */ + 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: defaultZIndex }; + + /** @private {!Object} */ + this._config = {}; + + /** @private @const */ + this._$rootElement = $rootElement; + + /** @private @const */ + this._$rootScope = $rootScope; + + /** @private @const */ + this._$injector = $injector; + + /** @private @const */ + this._$window = $window; + + + /** + * 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; } -mdListDirective.$inject = ["$mdTheming"]; +MdPanelService.$inject = ["$rootElement", "$rootScope", "$injector", "$window"]; + + /** - * @ngdoc directive - * @name mdListItem - * @module material.components.list - * - * @restrict E - * - * @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. - * - * ## CSS - * `.md-avatar` - class for image avatars - * - * `.md-avatar-icon` - class for icon avatars - * - * `.md-offset` - on content without an avatar - * - * @usage - * - * - * - * - * Item content in list - * - * - * - * Item content in list - * - * - * - * - * _**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._ + * Creates a panel with the specified options. + * @param {!Object=} config Configuration object for the panel. + * @returns {!MdPanelRef} */ -function mdListItemDirective($mdAria, $mdConstant, $mdUtil, $timeout) { - var proxiedTypes = ['md-checkbox', 'md-switch']; - return { - restrict: 'E', - controller: 'MdListController', - compile: function(tEl, tAttrs) { +MdPanelService.prototype.create = function(config) { + var configSettings = config || {}; - // Check for proxy controls (no ng-click on parent, and a control inside) - var secondaryItems = tEl[0].querySelectorAll('.md-secondary'); - var hasProxiedElement; - var proxyElement; - var itemContainer = tEl; + this._config = { + scope: this._$rootScope.$new(true), + attachTo: this._$rootElement + }; + angular.extend(this._config, this._defaultConfigOptions, configSettings); - tEl[0].setAttribute('role', 'listitem'); + var instanceId = 'panel_' + this._$injector.get('$mdUtil').nextUid(); + var instanceConfig = angular.extend({ id: instanceId }, this._config); - if (tAttrs.ngClick || tAttrs.ngDblclick || 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'); - } - } - wrapSecondaryItems(); - setupToggleAria(); + return new MdPanelRef(instanceConfig, this._$injector); +}; - function setupToggleAria() { - var toggleTypes = ['md-switch', 'md-checkbox']; - var toggle; +/** + * Creates and opens a panel with the specified options. + * @param {!Object=} config Configuration object for the panel. + * @returns {!angular.$q.Promise} The panel created from create. + */ +MdPanelService.prototype.open = function(config) { + var panelRef = this.create(config); + return panelRef.open().then(function() { + return panelRef; + }); +}; - 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 wrapIn(type) { - if (type == 'div') { - itemContainer = angular.element('
    '); - itemContainer.append(tEl.contents()); - tEl.addClass('_md-proxy-focus'); - } else { - // Element which holds the default list-item content. - itemContainer = angular.element( - '
    '+ - '
    '+ - '
    ' - ); +/** + * 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); +}; - // Button which shows ripple and executes primary action. - var buttonWrap = angular.element( - '' - ); - buttonWrap[0].setAttribute('aria-label', tEl[0].textContent); - copyAttributes(tEl[0], buttonWrap[0]); +/** + * 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); +}; - // Append the button wrap before our list-item content, because it will overlay in relative. - itemContainer.prepend(buttonWrap); - itemContainer.children().eq(1).append(tEl.contents()); - - tEl.addClass('_md-button-wrap'); - } - tEl[0].setAttribute('tabindex', '-1'); - tEl.append(itemContainer); - } +/** + * 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 || ''; - function wrapSecondaryItems() { - var secondaryItemsWrapper = angular.element('
    '); + // The panel should be initially rendered offscreen so we can calculate + // height and width for positioning. + return '' + + '
    ' + + '
    ' + template + '
    ' + + '
    '; +}; - angular.forEach(secondaryItems, function(secondaryItem) { - wrapSecondaryItem(secondaryItem, secondaryItemsWrapper); - }); - itemContainer.append(secondaryItemsWrapper); - } +/***************************************************************************** + * MdPanelRef * + *****************************************************************************/ - function wrapSecondaryItem(secondaryItem, container) { - if (secondaryItem && !isButton(secondaryItem) && secondaryItem.hasAttribute('ng-click')) { - $mdAria.expect(secondaryItem, 'aria-label'); - var buttonWrapper = angular.element(''); - copyAttributes(secondaryItem, buttonWrapper[0]); - secondaryItem.setAttribute('tabindex', '-1'); - buttonWrapper.append(secondaryItem); - secondaryItem = buttonWrapper[0]; - } - if (secondaryItem && (!hasClickEvent(secondaryItem) || (!tAttrs.ngClick && isProxiedElement(secondaryItem)))) { - // In this case we remove the secondary class, so we can identify it later, when we searching for the - // proxy items. - angular.element(secondaryItem).removeClass('md-secondary'); - } +/** + * 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'); - tEl.addClass('md-with-secondary'); - container.append(secondaryItem); - } + /** @private @const {!angular.$mdCompiler} */ + this._$mdCompiler = $injector.get('$mdCompiler'); - function copyAttributes(item, wrapper) { - var copiedAttrs = $mdUtil.prefixer([ - 'ng-if', 'ng-click', 'ng-dblclick', 'aria-label', 'ng-disabled', 'ui-sref', - 'href', 'ng-href', 'target', 'ng-attr-ui-sref', 'ui-sref-opts' - ]); + /** @private @const {!angular.$mdConstant} */ + this._$mdConstant = $injector.get('$mdConstant'); - angular.forEach(copiedAttrs, function(attr) { - if (item.hasAttribute(attr)) { - wrapper.setAttribute(attr, item.getAttribute(attr)); - item.removeAttribute(attr); - } - }); - } + /** @private @const {!angular.$mdUtil} */ + this._$mdUtil = $injector.get('$mdUtil'); - function isProxiedElement(el) { - return proxiedTypes.indexOf(el.nodeName.toLowerCase()) != -1; - } + /** @private @const {!angular.Scope} */ + this._$rootScope = $injector.get('$rootScope'); - function isButton(el) { - var nodeName = el.nodeName.toUpperCase(); + /** @private @const {!angular.$animate} */ + this._$animate = $injector.get('$animate'); - return nodeName == "MD-BUTTON" || nodeName == "BUTTON"; - } + /** @private @const {!MdPanelRef} */ + this._$mdPanel = $injector.get('$mdPanel'); - function hasClickEvent (element) { - var attr = element.attributes; - for (var i = 0; i < attr.length; i++) { - if (tAttrs.$normalize(attr[i].name) === 'ngClick') return true; - } - return false; - } + /** @private @const {!angular.$log} */ + this._$log = $injector.get('$log'); - return postLink; + /** @private @const {!angular.$window} */ + this._$window = $injector.get('$window'); - function postLink($scope, $element, $attr, ctrl) { - $element.addClass('_md'); // private md component indicator for styling - - var proxies = [], - firstElement = $element[0].firstElementChild, - isButtonWrap = $element.hasClass('_md-button-wrap'), - clickChild = isButtonWrap ? firstElement.firstElementChild : firstElement, - hasClick = clickChild && hasClickEvent(clickChild); + /** @private @const {!Function} */ + this._$$rAF = $injector.get('$$rAF'); - computeProxies(); - computeClickable(); + // Public variables. + /** + * Unique id for the panelRef. + * @type {string} + */ + this.id = config.id; - if ($element.hasClass('_md-proxy-focus') && proxies.length) { - angular.forEach(proxies, function(proxy) { - proxy = angular.element(proxy); + /** @type {!Object} */ + this.config = config; - $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); - }); - }); - }); - } + /** + * 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} + */ + this.isAttached = false; + // Private variables. + /** @private {!angular.JQLite|undefined} */ + this._panelContainer; - function computeProxies() { - if (firstElement && firstElement.children && !hasClick) { + /** @private {!angular.JQLite|undefined} */ + this._panelEl; - angular.forEach(proxiedTypes, function(type) { + /** @private {Array} */ + this._removeListeners = []; - // All elements which are not capable for being used a proxy have the .md-secondary class - // applied. These items had been sorted out in the secondary wrap function. - angular.forEach(firstElement.querySelectorAll(type + ':not(.md-secondary)'), function(child) { - proxies.push(child); - }); - }); + /** @private {!angular.JQLite|undefined} */ + this._topFocusTrap; - } - } - function computeClickable() { - if (proxies.length == 1 || hasClick) { - $element.addClass('md-clickable'); + /** @private {!angular.JQLite|undefined} */ + this._bottomFocusTrap; - if (!hasClick) { - ctrl.attachRipple($scope, angular.element($element[0].querySelector('._md-no-style'))); - } - } - } + /** @private {!$mdPanel|undefined} */ + this._backdropRef; - var clickChildKeypressListener = 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 (clickChild) { - clickChild.click(); - e.preventDefault(); - e.stopPropagation(); - } - } - } - }; + /** @private {Function?} */ + this._restoreScroll = null; +} - if (!hasClick && !proxies.length) { - clickChild && clickChild.addEventListener('keypress', clickChildKeypressListener); - } - $element.off('click'); - $element.off('keypress'); +/** + * 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); - if (proxies.length == 1 && clickChild) { - $element.children().eq(0).on('click', function(e) { - var parentButton = $mdUtil.getClosest(e.target, 'BUTTON'); - if (!parentButton && clickChild.contains(e.target)) { - angular.forEach(proxies, function(proxy) { - if (e.target !== proxy && !proxy.contains(e.target)) { - angular.element(proxy).triggerHandler('click'); - } - }); - } - }); - } + self.attach() + .then(show) + .then(done) + .catch(reject); + }); +}; - $scope.$on('$destroy', function () { - clickChild && clickChild.removeEventListener('keypress', clickChildKeypressListener); - }); - } - } - }; -} -mdListItemDirective.$inject = ["$mdAria", "$mdConstant", "$mdUtil", "$timeout"]; -/* - * @private - * @ngdoc controller - * @name MdListController - * @module material.components.list +/** + * Closes the panel. * + * @returns {!angular.$q.Promise} A promise that is resolved when the panel is + * closed and animations finish. */ -function MdListController($scope, $element, $mdListInkRipple) { - var ctrl = this; - ctrl.attachRipple = attachRipple; +MdPanelRef.prototype.close = function() { + var self = this; - function attachRipple (scope, element) { - var options = {}; - $mdListInkRipple.attach(scope, element, options); - } -} -MdListController.$inject = ["$scope", "$element", "$mdListInkRipple"]; + return this._$q(function(resolve, reject) { + var done = self._done(resolve, self); + var detach = self._simpleBind(self.detach, self); + + self.hide() + .then(detach) + .then(done) + .catch(reject); + }); +}; -})(); -(function(){ -"use strict"; /** - * @ngdoc module - * @name material.components.input + * 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); + } -angular.module('material.components.input', [ - 'material.core' - ]) - .directive('mdInputContainer', mdInputContainerDirective) - .directive('label', labelDirective) - .directive('input', inputTextareaDirective) - .directive('textarea', inputTextareaDirective) - .directive('mdMaxlength', mdMaxlengthDirective) - .directive('placeholder', placeholderDirective) - .directive('ngMessages', ngMessagesDirective) - .directive('ngMessage', ngMessageDirective) - .directive('ngMessageExp', ngMessageDirective) - .directive('mdSelectOnFocus', mdSelectOnFocusDirective) + 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); + }); +}; - .animation('.md-input-invalid', mdInputInvalidMessagesAnimation) - .animation('.md-input-messages-animation', ngMessagesAnimation) - .animation('.md-input-message-animation', ngMessageAnimation); /** - * @ngdoc directive - * @name mdInputContainer - * @module material.components.input - * - * @restrict E - * - * @description - * `` is the parent of any input or textarea element. - * - * Input and textarea elements will not behave properly unless the md-input-container - * parent is provided. - * - * A single `` should contain only one `` element, otherwise it will throw an error. - * - * Exception: Hidden inputs (``) are ignored and will not throw an error, so - * you may combine these with other inputs. - * - * @param md-is-error {expression=} When the given expression evaluates to true, the input container - * will go into error state. Defaults to erroring if the input has been touched and is invalid. - * @param md-no-float {boolean=} When present, `placeholder` attributes on the input will not be converted to floating - * labels. - * - * @usage - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *

    When disabling floating labels

    - * - * - * - * - * + * 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); + } + + 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. */ -function mdInputContainerDirective($mdTheming, $parse) { +MdPanelRef.prototype.destroy = function() { + this.config.scope.$destroy(); + this.config.locals = null; +}; - 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(","); +/** + * 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('Panel does not exist yet. Call open() or attach().'); + }); + } - var RIGHT_SELECTORS = INPUT_TAGS.reduce(function(selectors, isel) { - return selectors.concat([isel + ' ~ md-icon', isel + ' ~ .md-icon']); - }, []).join(","); + if (!this._panelContainer.hasClass(MD_PANEL_HIDDEN)) { + return this._$q.when(this); + } - ContainerCtrl.$inject = ["$scope", "$element", "$attrs", "$animate"]; - return { - restrict: 'E', - link: postLink, - controller: ContainerCtrl + var self = this; + var animatePromise = function() { + self.removeClass(MD_PANEL_HIDDEN); + return self._animateOpen(); }; - function postLink(scope, element) { - $mdTheming(element); + return this._$q(function(resolve, reject) { + var done = self._done(resolve, self); + var onOpenComplete = self.config['onOpenComplete'] || angular.noop; - // Check for both a left & right icon - var leftIcon = element[0].querySelector(LEFT_SELECTORS); - var rightIcon = element[0].querySelector(RIGHT_SELECTORS); + self._$q.all([ + self._backdropRef ? self._backdropRef.show() : self, + animatePromise().then(function() { self._focusOnOpen(); }, reject) + ]).then(onOpenComplete) + .then(done) + .catch(reject); + }); +}; - if (leftIcon) { element.addClass('md-icon-left'); } - if (rightIcon) { element.addClass('md-icon-right'); } + +/** + * 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('Panel does not exist yet. Call open() or attach().'); + }); } - function ContainerCtrl($scope, $element, $attrs, $animate) { - var self = this; + if (this._panelContainer.hasClass(MD_PANEL_HIDDEN)) { + return this._$q.when(this); + } - self.isErrorGetter = $attrs.mdIsError && $parse($attrs.mdIsError); + var self = this; - 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'); + return this._$q(function(resolve, reject) { + var done = self._done(resolve, self); + var onRemoving = self.config['onRemoving'] || angular.noop; + + var focusOnOrigin = function() { + var origin = self.config['origin']; + if (origin) { + getElement(origin).focus(); } }; - $scope.$watch(function() { - return self.label && self.input; - }, function(hasLabelAndInput) { - if (hasLabelAndInput && !self.label.attr('for')) { - self.label.attr('for', self.input.attr('id')); - } - }); - } -} -mdInputContainerDirective.$inject = ["$mdTheming", "$parse"]; -function labelDirective() { - return { - restrict: 'E', - require: '^?mdInputContainer', - link: function(scope, element, attr, containerCtrl) { - if (!containerCtrl || attr.mdNoFloat || element.hasClass('_md-container-ignore')) return; + var hidePanel = function() { + self.addClass(MD_PANEL_HIDDEN); + }; + + self._$q.all([ + self._backdropRef ? self._backdropRef.hide() : self, + self._animateClose() + .then(onRemoving) + .then(hidePanel) + .then(focusOnOrigin) + .catch(reject) + ]).then(done, reject); + }); +}; - containerCtrl.label = element; - scope.$on('$destroy', function() { - containerCtrl.label = null; - }); - } - }; -} /** - * @ngdoc directive - * @name mdInput - * @restrict E - * @module material.components.input - * - * @description - * You can use any `` or ` - *
    - *
    This is required!
    - *
    That's too long!
    - *
    - *
    - * - * - * - * - * - * - * - * - * - *

    Notes

    - * - * - Requires [ngMessages](https://docs.angularjs.org/api/ngMessages). - * - Behaves like the [AngularJS input directive](https://docs.angularjs.org/api/ng/directive/input). - * - * The `md-input` and `md-input-container` directives use very specific positioning to achieve the - * error animation effects. Therefore, it is *not* advised to use the Layout system inside of the - * `` tags. Instead, use relative or absolute positioning. + * Add a class to the panel. DO NOT use this to 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. + */ +MdPanelRef.prototype.addClass = function(newClass, toElement) { + if (!this._panelContainer) { + throw new Error('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. * - *

    Textarea directive

    - * The `textarea` element within a `md-input-container` has the following specific behavior: - * - By default the `textarea` grows as the user types. This can be disabled via the `md-no-autogrow` - * attribute. - * - If a `textarea` has the `rows` attribute, it will treat the `rows` as the minimum height and will - * continue growing as the user types. For example a textarea with `rows="3"` will be 3 lines of text - * high initially. If no rows are specified, the directive defaults to 1. - * - If you wan't a `textarea` to stop growing at a certain point, you can specify the `max-rows` attribute. - * - The textarea's bottom border acts as a handle which users can drag, in order to resize the element vertically. - * Once the user has resized a `textarea`, the autogrowing functionality becomes disabled. If you don't want a - * `textarea` to be resizeable by the user, you can add the `md-no-resize` attribute. + * @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) { + if (!this._panelContainer) { + throw new Error('Panel does not exist yet. Call open() or attach().'); + } -function inputTextareaDirective($mdUtil, $window, $mdAria, $timeout, $mdGesture) { - return { - restrict: 'E', - require: ['^?mdInputContainer', '?ngModel'], - link: postLink - }; + if (!fromElement && this._panelContainer.hasClass(oldClass)) { + this._panelContainer.removeClass(oldClass); + } else if (fromElement && this._panelEl.hasClass(oldClass)) { + this._panelEl.removeClass(oldClass); + } +}; - function postLink(scope, element, attr, ctrls) { - var containerCtrl = ctrls[0]; - var hasNgModel = !!ctrls[1]; - var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel(); - var isReadonly = angular.isDefined(attr.readonly); - var mdNoAsterisk = $mdUtil.parseAttributeBoolean(attr.mdNoAsterisk); - var tagName = element[0].tagName.toLowerCase(); +/** + * Toggle a class on the panel. DO NOT use this to hide/show the panel. + * + * @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) { + if (!this._panelContainer) { + throw new Error('Panel does not exist yet. Call open() or attach().'); + } + + if (!onElement) { + this._panelContainer.toggleClass(toggleClass); + } else { + this._panelEl.toggleClass(toggleClass); + } +}; - if (!containerCtrl) return; - if (attr.type === 'hidden') { - element.attr('aria-hidden', 'true'); - return; - } else if (containerCtrl.input) { - throw new Error(" can only have *one* , - * - * - * + * 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} */ -function mdSelectOnFocusDirective($timeout) { +MdPanelPosition.prototype.center = function() { + return this.centerHorizontally().centerVertically(); +}; - return { - restrict: 'A', - link: postLink - }; - function postLink(scope, element, attr) { - if (element[0].nodeName !== 'INPUT' && element[0].nodeName !== "TEXTAREA") return; +/** + * Sets element for relative positioning. + * @param {string|!Element|!angular.JQLite} element Query selector, + * DOM element, or angular element to set the panel relative to. + * @returns {MdPanelPosition} + */ +MdPanelPosition.prototype.relativeTo = function(element) { + this._absolute = false; + this._relativeToEl = getElement(element); + return this; +}; - var preventMouseUp = false; - element - .on('focus', onFocus) - .on('mouseup', onMouseUp); +/** + * Sets the x and y positions for the panel relative to another element. + * @param {string} xPosition must be one of the MdPanelPosition.xPosition values. + * @param {string} yPosition must be one of the MdPanelPosition.yPosition values. + * @returns {MdPanelPosition} + */ +MdPanelPosition.prototype.addPanelPosition = function(xPosition, yPosition) { + if (!this._relativeToEl) { + throw new Error('addPanelPosition can only be used with relative ' + + 'positioning. Set relativeTo first.'); + } - scope.$on('$destroy', function() { - element - .off('focus', onFocus) - .off('mouseup', onMouseUp); - }); + this._validateXPosition(xPosition); + this._validateYPosition(yPosition); - function onFocus() { - preventMouseUp = true; + this._positions.push({ + x: xPosition, + y: yPosition, + }); + return this; +}; - $timeout(function() { - // Use HTMLInputElement#select to fix firefox select issues. - // The debounce is here for Edge's sake, otherwise the selection doesn't work. - element[0].select(); - // This should be reset from inside the `focus`, because the event might - // have originated from something different than a click, e.g. a keyboard event. - preventMouseUp = false; - }, 1, false); +/** + * Ensure that yPosition is a valid position name. Throw an exception if not. + * @param {string} yPosition + */ +MdPanelPosition.prototype._validateYPosition = function(yPosition) { + // empty is ok + if (yPosition == null) { + return; + } + + var positionKeys = Object.keys(MdPanelPosition.yPosition); + var positionValues = []; + for (var key, i = 0; key = positionKeys[i]; i++) { + var position = MdPanelPosition.yPosition[key]; + positionValues.push(position); + + if (position === yPosition) { + return; } + } - // Prevents the default action of the first `mouseup` after a focus. - // This is necessary, because browsers fire a `mouseup` right after the element - // has been focused. In some browsers (Firefox in particular) this can clear the - // selection. There are examples of the problem in issue #7487. - function onMouseUp(event) { - if (preventMouseUp) { - event.preventDefault(); - } + throw new Error('Panel y position only accepts the following values:\n' + + positionValues.join(' | ')); +}; + + +/** + * Ensure that xPosition is a valid position name. Throw an exception if not. + * @param {string} xPosition + */ +MdPanelPosition.prototype._validateXPosition = function(xPosition) { + // empty is ok + if (xPosition == null) { + return; + } + + var positionKeys = Object.keys(MdPanelPosition.xPosition); + var positionValues = []; + for (var key, i = 0; key = positionKeys[i]; i++) { + var position = MdPanelPosition.xPosition[key]; + positionValues.push(position); + if (position === xPosition) { + return; } } -} -mdSelectOnFocusDirective.$inject = ["$timeout"]; -var visibilityDirectives = ['ngIf', 'ngShow', 'ngHide', 'ngSwitchWhen', 'ngSwitchDefault']; -function ngMessagesDirective() { - return { - restrict: 'EA', - link: postLink, + throw new Error('Panel x Position only accepts the following values:\n' + + positionValues.join(' | ')); +}; - // This is optional because we don't want target *all* ngMessage instances, just those inside of - // mdInputContainer. - require: '^^?mdInputContainer' - }; - function postLink(scope, element, attrs, inputContainer) { - // If we are not a child of an input container, don't do anything - if (!inputContainer) return; +/** + * Sets the value of the offset in the x-direction. This will add + * to any previously set offsets. + * @param {string} offsetX + * @returns {MdPanelPosition} + */ +MdPanelPosition.prototype.withOffsetX = function(offsetX) { + this._translateX.push(offsetX); + return this; +}; - // Add our animation class - element.toggleClass('md-input-messages-animation', true); - // Add our md-auto-hide class to automatically hide/show messages when container is invalid - element.toggleClass('md-auto-hide', true); +/** + * Sets the value of the offset in the y-direction. This will add + * to any previously set offsets. + * @param {string} offsetY + * @returns {MdPanelPosition} + */ +MdPanelPosition.prototype.withOffsetY = function(offsetY) { + this._translateY.push(offsetY); + return this; +}; + + +/** + * Gets the value of `top` for the panel. + * @returns {string} + */ +MdPanelPosition.prototype.getTop = function() { + return this._top; +}; + - // 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); - } - } +/** + * Gets the value of `bottom` for the panel. + * @returns {string} + */ +MdPanelPosition.prototype.getBottom = function() { + return this._bottom; +}; - function hasVisibiltyDirective(attrs) { - return visibilityDirectives.some(function(attr) { - return attrs[attr]; - }); - } -} -function ngMessageDirective($mdUtil) { - return { - restrict: 'EA', - compile: compile, - priority: 100 - }; +/** + * Gets the value of `left` for the panel. + * @returns {string} + */ +MdPanelPosition.prototype.getLeft = function() { + return this._left; +}; - function compile(tElement) { - if (!isInsideInputContainer(tElement)) { - // When the current element is inside of a document fragment, then we need to check for an input-container - // in the postLink, because the element will be later added to the DOM and is currently just in a temporary - // fragment, which causes the input-container check to fail. - if (isInsideFragment()) { - return function (scope, element) { - if (isInsideInputContainer(element)) { - // Inside of the postLink function, a ngMessage directive will be a comment element, because it's - // currently hidden. To access the shown element, we need to use the element from the compile function. - initMessageElement(tElement); - } - }; - } - } else { - initMessageElement(tElement); - } +/** + * Gets the value of `right` for the panel. + * @returns {string} + */ +MdPanelPosition.prototype.getRight = function() { + return this._right; +}; - function isInsideFragment() { - var nextNode = tElement[0]; - while (nextNode = nextNode.parentNode) { - if (nextNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { - return true; - } - } - return false; - } - function isInsideInputContainer(element) { - return !!$mdUtil.getClosest(element, "md-input-container"); - } +/** + * Gets the value of `transform` for the panel. + * @returns {string} + */ +MdPanelPosition.prototype.getTransform = function() { + var translateX = this._reduceTranslateValues('translateX', this._translateX); + var translateY = this._reduceTranslateValues('translateY', this._translateY); - function initMessageElement(element) { - // Add our animation class - element.toggleClass('md-input-message-animation', true); - } - } -} -ngMessageDirective.$inject = ["$mdUtil"]; + // It's important to trim the result, because the browser will ignore the set + // operation if the string contains only whitespace. + return (translateX + ' ' + translateY).trim(); +}; -function mdInputInvalidMessagesAnimation($q, $animateCss) { - return { - addClass: function(element, className, done) { - var messages = getMessagesElement(element); +/** + * True if the panel is completely on-screen with this positioning; false + * otherwise. + * @param {!angular.JQLite} panelEl + * @return {boolean} + */ +MdPanelPosition.prototype._isOnscreen = function(panelEl) { + // this works because we always use fixed positioning for the panel, + // which is relative to the viewport. + // TODO(gmoothart): take into account _translateX and _translateY to the + // extent feasible. - if (className == "md-input-invalid" && messages.hasClass('md-auto-hide')) { - showInputMessages(element, $animateCss, $q).finally(done); - } else { - done(); - } - } + var left = parseInt(this.getLeft()); + var top = parseInt(this.getTop()); + var right = left + panelEl[0].offsetWidth; + var bottom = top + panelEl[0].offsetHeight; - // NOTE: We do not need the removeClass method, because the message ng-leave animation will fire - }; -} -mdInputInvalidMessagesAnimation.$inject = ["$q", "$animateCss"]; + return (left >= 0) && + (top >= 0) && + (bottom <= this._$window.innerHeight) && + (right <= this._$window.innerWidth); +}; -function ngMessagesAnimation($q, $animateCss) { - return { - enter: function(element, done) { - showInputMessages(element, $animateCss, $q).finally(done); - }, - leave: function(element, done) { - hideInputMessages(element, $animateCss, $q).finally(done); - }, +/** + * Gets the first x/y position that can fit on-screen. + * @returns {{x: string, y: string}} + */ +MdPanelPosition.prototype.getActualPosition = function() { + return this._actualPosition; +}; - addClass: function(element, className, done) { - if (className == "ng-hide") { - hideInputMessages(element, $animateCss, $q).finally(done); - } else { - done(); - } - }, - removeClass: function(element, className, done) { - if (className == "ng-hide") { - showInputMessages(element, $animateCss, $q).finally(done); - } else { - done(); - } - } - } -} -ngMessagesAnimation.$inject = ["$q", "$animateCss"]; +/** + * Reduces a list of translate values to a string that can be used within + * transform. + * @param {string} translateFn + * @param {!Array} values + * @returns {string} + * @private + */ +MdPanelPosition.prototype._reduceTranslateValues = + function(translateFn, values) { + return values.map(function(translation) { + return translateFn + '(' + translation + ')'; + }).join(' '); + }; -function ngMessageAnimation($animateCss) { - return { - enter: function(element, done) { - var messages = getMessagesElement(element); - // 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; - } +/** + * Sets the panel position based on the created panel element and best x/y + * positioning. + * @param {!angular.JQLite} panelEl + * @private + */ +MdPanelPosition.prototype._setPanelPosition = function(panelEl) { + // Only calculate the position if necessary. + if (this._absolute) { + return; + } - return showMessage(element, $animateCss); - }, + if (this._actualPosition) { + this._calculatePanelPosition(panelEl, this._actualPosition); + return; + } - leave: function(element, done) { - return hideMessage(element, $animateCss); + for (var i = 0; i < this._positions.length; i++) { + this._actualPosition = this._positions[i]; + this._calculatePanelPosition(panelEl, this._actualPosition); + if (this._isOnscreen(panelEl)) { + break; } } -} -ngMessageAnimation.$inject = ["$animateCss"]; +}; -function showInputMessages(element, $animateCss, $q) { - var animators = [], animator; - var messages = getMessagesElement(element); - angular.forEach(messages.children(), function(child) { - animator = showMessage(angular.element(child), $animateCss); +/** + * Switching between 'start' and 'end' + * @param {string} position Horizontal position of the panel + * @returns {string} Reversed position + * @private + */ +MdPanelPosition.prototype._reverseXPosition = function(position) { + if (position === MdPanelPosition.xPosition.CENTER) { + return; + } - animators.push(animator.start()); - }); + var start = 'start'; + var end = 'end'; - return $q.all(animators); -} + return position.indexOf(start) > -1 ? position.replace(start, end) : position.replace(end, start); +}; -function hideInputMessages(element, $animateCss, $q) { - var animators = [], animator; - var messages = getMessagesElement(element); - angular.forEach(messages.children(), function(child) { - animator = hideMessage(angular.element(child), $animateCss); +/** + * Handles horizontal positioning in rtl or ltr environments + * @param {string} position Horizontal position of the panel + * @returns {string} The correct position according the page direction + * @private + */ +MdPanelPosition.prototype._bidi = function(position) { + return this._isRTL ? this._reverseXPosition(position) : position; +}; - animators.push(animator.start()); - }); - return $q.all(animators); -} +/** + * Calculates the panel position based on the created panel element and the + * provided positioning. + * @param {!angular.JQLite} panelEl + * @param {!{x:string, y:string}} position + * @private + */ +MdPanelPosition.prototype._calculatePanelPosition = function(panelEl, position) { -function showMessage(element, $animateCss) { - var height = element[0].offsetHeight; + var panelBounds = panelEl[0].getBoundingClientRect(); + var panelWidth = panelBounds.width; + var panelHeight = panelBounds.height; - return $animateCss(element, { - event: 'enter', - structural: true, - from: {"opacity": 0, "margin-top": -height + "px"}, - to: {"opacity": 1, "margin-top": "0"}, - duration: 0.3 - }); -} + var targetBounds = this._relativeToEl[0].getBoundingClientRect(); -function hideMessage(element, $animateCss) { - var height = element[0].offsetHeight; - var styles = window.getComputedStyle(element[0]); + var targetLeft = targetBounds.left; + var targetRight = targetBounds.right; + var targetWidth = targetBounds.width; - // If we are already hidden, just return an empty animation - if (styles.opacity == 0) { - return $animateCss(element, {}); + switch (this._bidi(position.x)) { + case MdPanelPosition.xPosition.OFFSET_START: + this._left = targetLeft - panelWidth + 'px'; + break; + case MdPanelPosition.xPosition.ALIGN_END: + this._left = targetRight - panelWidth + 'px'; + break; + case MdPanelPosition.xPosition.CENTER: + var left = targetLeft + (0.5 * targetWidth) - (0.5 * panelWidth); + this._left = left + 'px'; + break; + case MdPanelPosition.xPosition.ALIGN_START: + this._left = targetLeft + 'px'; + break; + case MdPanelPosition.xPosition.OFFSET_END: + this._left = targetRight + 'px'; + break; } - // 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 - }); -} - -function getInputElement(element) { - var inputContainer = element.controller('mdInputContainer'); + var targetTop = targetBounds.top; + var targetBottom = targetBounds.bottom; + var targetHeight = targetBounds.height; - return inputContainer.element; -} + switch (position.y) { + case MdPanelPosition.yPosition.ABOVE: + this._top = targetTop - panelHeight + 'px'; + break; + case MdPanelPosition.yPosition.ALIGN_BOTTOMS: + this._top = targetBottom - panelHeight + 'px'; + break; + case MdPanelPosition.yPosition.CENTER: + var top = targetTop + (0.5 * targetHeight) - (0.5 * panelHeight); + this._top = top + 'px'; + break; + case MdPanelPosition.yPosition.ALIGN_TOPS: + this._top = targetTop + 'px'; + break; + case MdPanelPosition.yPosition.BELOW: + this._top = targetBottom + 'px'; + break; + } +}; -function getMessagesElement(element) { - var input = getInputElement(element); - return angular.element(input[0].querySelector('.md-input-messages-animation')); -} +/***************************************************************************** + * MdPanelAnimation * + *****************************************************************************/ -})(); -(function(){ -"use strict"; /** - * @ngdoc module - * @name material.components.menu-bar + * Animation configuration object. To use, create an MdPanelAnimation with the + * desired properties, then pass the object as part of $mdPanel creation. + * + * Example: + * + * var panelAnimation = new MdPanelAnimation() + * .openFrom(myButtonEl) + * .closeTo('.my-button') + * .withAnimation($mdPanel.animation.SCALE); + * + * $mdPanel.create({ + * animation: panelAnimation + * }); + * + * @param {!angular.$injector} $injector + * @final @constructor */ +function MdPanelAnimation($injector) { + /** @private @const {!angular.$mdUtil} */ + this._$mdUtil = $injector.get('$mdUtil'); -angular.module('material.components.menuBar', [ - 'material.core', - 'material.components.menu' -]); + /** + * @private {{element: !angular.JQLite|undefined, bounds: !DOMRect}| + * undefined} + */ + this._openFrom; + + /** + * @private {{element: !angular.JQLite|undefined, bounds: !DOMRect}| + * undefined} + */ + this._closeTo; + + /** @private {string|{open: string, close: string} */ + this._animationClass = ''; +} -})(); -(function(){ -"use strict"; /** - * @ngdoc module - * @name material.components.menu + * Possible default animations. + * @enum {string} */ +MdPanelAnimation.animation = { + SLIDE: 'md-panel-animate-slide', + SCALE: 'md-panel-animate-scale', + FADE: 'md-panel-animate-fade' +}; -angular.module('material.components.menu', [ - 'material.core', - 'material.components.backdrop' -]); - -})(); -(function(){ -"use strict"; /** - * @ngdoc module - * @name material.components.navBar + * 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}} openFrom + * @returns {MdPanelAnimation} */ +MdPanelAnimation.prototype.openFrom = function(openFrom) { + // Check if 'openFrom' is an Event. + openFrom = openFrom.target ? openFrom.target : openFrom; + this._openFrom = this._getPanelAnimationTarget(openFrom); -angular.module('material.components.navBar', ['material.core']) - .controller('MdNavBarController', MdNavBarController) - .directive('mdNavBar', MdNavBar) - .controller('MdNavItemController', MdNavItemController) - .directive('mdNavItem', MdNavItem); + if (!this._closeTo) { + this._closeTo = this._openFrom; + } + return this; +}; -/***************************************************************************** - * PUBLIC DOCUMENTATION * - *****************************************************************************/ /** - * @ngdoc directive - * @name mdNavBar - * @module material.components.navBar - * - * @restrict E - * - * @description - * The `` directive renders a list of material tabs that can be used - * for top-level page navigation. Unlike ``, it has no concept of a tab - * body and no bar pagination. - * - * Because it deals with page navigation, certain routing concepts are built-in. - * Route changes via via ng-href, ui-sref, or ng-click events are supported. - * Alternatively, the user could simply watch currentNavItem for changes. - * - * 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 {string=} mdSelectedNavItem The name of the current tab; this must - * match the name attribute of `` - * @param {string=} navBarAriaLabel An aria-label for the nav-bar - * - * @usage - * - * - * Page One - * Page Two - * Page Three - * - * - * - * (function() { - * ‘use strict’; + * 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. * - * $rootScope.$on('$routeChangeSuccess', function(event, current) { - * $scope.currentLink = getCurrentLinkFromRoute(current); - * }); - * }); - * + * @param {string|!Element|{top: number, left: number}} closeTo + * @returns {MdPanelAnimation} */ +MdPanelAnimation.prototype.closeTo = function(closeTo) { + this._closeTo = this._getPanelAnimationTarget(closeTo); + return this; +}; + -/***************************************************************************** - * mdNavItem - *****************************************************************************/ /** - * @ngdoc directive - * @name mdNavItem - * @module material.components.navBar - * - * @restrict E - * - * @description - * `` describes a page navigation link within the `` - * component. It renders an md-button as the actual link. - * - * Exactly one of the mdNavClick, mdNavHref, mdNavSref attributes are required to be - * specified. - * - * @param {Function=} mdNavClick Function which will be called when the - * link is clicked to change the page. Renders as an `ng-click`. - * @param {string=} mdNavHref url to transition to when this link is clicked. - * Renders as an `ng-href`. - * @param {string=} mdNavSref Ui-router state to transition to when this link is - * clicked. Renders as a `ui-sref`. - * @param {string=} name The name of this link. Used by the nav bar to know - * which link is currently selected. - * - * @usage - * See `` for usage. + * Returns the element and bounds for the animation target. + * @param {string|!Element|{top: number, left: number}} location + * @returns {{element: !angular.JQLite|undefined, bounds: !DOMRect}} + * @private */ - - -/***************************************************************************** - * IMPLEMENTATION * - *****************************************************************************/ - -function MdNavBar($mdAria) { - return { - restrict: 'E', - transclude: true, - controller: MdNavBarController, - controllerAs: 'ctrl', - bindToController: true, - scope: { - 'mdSelectedNavItem': '=?', - 'navBarAriaLabel': '@?', - }, - template: - '
    ' + - '' + - '' + - '
    ', - link: function(scope, element, attrs, ctrl) { - if (!ctrl.navBarAriaLabel) { - $mdAria.expectAsync(element, 'aria-label', angular.noop); +MdPanelAnimation.prototype._getPanelAnimationTarget = function(location) { + if (angular.isDefined(location.top) || angular.isDefined(location.left)) { + return { + element: undefined, + bounds: { + top: location.top || 0, + left: location.left || 0 } - }, - }; -} -MdNavBar.$inject = ["$mdAria"]; + }; + } else { + return this._getBoundingClientRect(getElement(location)); + } +}; + /** - * Controller for the nav-bar component. + * Specifies the animation class. * - * 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 + * There are several default animations that can be used: + * (MdPanelAnimation.animation) + * SLIDE: The panel slides in and out from the specified + * elements. + * SCALE: The panel scales in and out. + * FADE: The panel fades in and out. + * + * @param {string|{open: string, close: string}} cssClass + * @returns {MdPanelAnimation} */ -function MdNavBarController($element, $scope, $timeout, $mdConstant) { - // Injected variables - /** @private @const {!angular.Timeout} */ - this._$timeout = $timeout; - /** @private @const {!angular.Scope} */ - this._$scope = $scope; +MdPanelAnimation.prototype.withAnimation = function(cssClass) { + this._animationClass = cssClass; + return this; +}; - /** @private @const {!Object} */ - this._$mdConstant = $mdConstant; - // Data-bound variables. - /** @type {string} */ - this.mdSelectedNavItem; +/** + * Animate the panel open. + * @param {!angular.JQLite} panelEl + * @returns {!angular.$q.Promise} + */ +MdPanelAnimation.prototype.animateOpen = function(panelEl) { + var animator = this._$mdUtil.dom.animator; - /** @type {string} */ - this.navBarAriaLabel; + this._fixBounds(panelEl); + var animationOptions = {}; - // State variables. + // Include the panel transformations when calculating the animations. + var panelTransform = panelEl[0].style.transform || ''; - /** @type {?angular.JQLite} */ - this._navBarEl = $element[0]; + var openFrom = animator.toTransformCss(panelTransform); + var openTo = animator.toTransformCss(panelTransform); - /** @type {?angular.JQLite} */ - this._inkbar; + switch (this._animationClass) { + case MdPanelAnimation.animation.SLIDE: + // Slide should start with opacity: 1. + panelEl.css('opacity', '1'); - 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(); - } - }); -} -MdNavBarController.$inject = ["$element", "$scope", "$timeout", "$mdConstant"]; + animationOptions = { + transitionInClass: '_md-panel-animate-enter' + }; + var openSlide = animator.calculateSlideToOrigin( + panelEl, this._openFrom) || ''; + openFrom = animator.toTransformCss(openSlide + ' ' + panelTransform); + break; + case MdPanelAnimation.animation.SCALE: + animationOptions = { + transitionInClass: '_md-panel-animate-enter' + }; -/** - * Initializes the tab components once they exist. - * @private - */ -MdNavBarController.prototype._initTabs = function() { - this._inkbar = angular.element(this._navBarEl.getElementsByTagName('md-nav-ink-bar')[0]); + var openScale = animator.calculateZoomToOrigin( + panelEl, this._openFrom) || ''; + openFrom = animator.toTransformCss(openScale + ' ' + panelTransform); + break; - var self = this; - this._$timeout(function() { - self._updateTabs(self.mdSelectedNavItem, undefined); - }); + case MdPanelAnimation.animation.FADE: + animationOptions = { + transitionInClass: '_md-panel-animate-enter' + }; + break; - 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); - }); - }); + default: + if (angular.isString(this._animationClass)) { + animationOptions = { + transitionInClass: this._animationClass + }; + } else { + animationOptions = { + transitionInClass: this._animationClass['open'], + transitionOutClass: this._animationClass['close'], + }; + } + } + + return animator + .translate3d(panelEl, openFrom, openTo, animationOptions); }; + /** - * Set the current tab to be selected. - * @param {string|undefined} newValue New current tab name. - * @param {string|undefined} oldValue Previous tab name. - * @private + * Animate the panel close. + * @param {!angular.JQLite} panelEl + * @returns {!angular.$q.Promise} */ -MdNavBarController.prototype._updateTabs = function(newValue, oldValue) { - var tabs = this._getTabs(); +MdPanelAnimation.prototype.animateClose = function(panelEl) { + var animator = this._$mdUtil.dom.animator; + var reverseAnimationOptions = {}; - var oldIndex; - if (oldValue) { - var oldTab = this._getTabByName(oldValue); - if (oldTab) { - oldTab.setSelected(false); - oldIndex = tabs.indexOf(oldTab); - } - } + // Include the panel transformations when calculating the animations. + var panelTransform = panelEl[0].style.transform || ''; - if (newValue) { - var tab = this._getTabByName(newValue); - if (tab) { - tab.setSelected(true); - var newIndex = tabs.indexOf(tab); - var self = this; - this._$timeout(function() { - self._updateInkBarStyles(tab, newIndex, oldIndex); - }); - } - } -}; + var closeFrom = animator.toTransformCss(panelTransform); + var closeTo = animator.toTransformCss(panelTransform); -/** - * Repositions the ink bar to the selected tab. - * @private - */ -MdNavBarController.prototype._updateInkBarStyles = function(tab, newIndex, oldIndex) { - var tabEl = tab.getButtonEl(); - var left = tabEl.offsetLeft; + switch (this._animationClass) { + case MdPanelAnimation.animation.SLIDE: + // Slide should start with opacity: 1. + panelEl.css('opacity', '1'); + reverseAnimationOptions = { + transitionInClass: '_md-panel-animate-leave' + }; - this._inkbar.toggleClass('_md-left', newIndex < oldIndex) - .toggleClass('_md-right', newIndex > oldIndex); - this._inkbar.css({left: left + 'px', width: tabEl.offsetWidth + 'px'}); -}; + var closeSlide = animator.calculateSlideToOrigin( + panelEl, this._closeTo) || ''; + closeTo = animator.toTransformCss(closeSlide + ' ' + panelTransform); + break; -/** - * Returns an array of the current tabs. - * @return {!Array} - * @private - */ -MdNavBarController.prototype._getTabs = function() { - var linkArray = Array.prototype.slice.call( - this._navBarEl.querySelectorAll('.md-nav-item')); - return linkArray.map(function(el) { - return angular.element(el).controller('mdNavItem') - }); -}; + case MdPanelAnimation.animation.SCALE: + reverseAnimationOptions = { + transitionInClass: '_md-panel-animate-scale-out _md-panel-animate-leave' + }; -/** - * 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; - }); + var closeScale = animator.calculateZoomToOrigin( + panelEl, this._closeTo) || ''; + closeTo = animator.toTransformCss(closeScale + ' ' + panelTransform); + break; + + case MdPanelAnimation.animation.FADE: + reverseAnimationOptions = { + transitionInClass: '_md-panel-animate-fade-out _md-panel-animate-leave' + }; + break; + + default: + if (angular.isString(this._animationClass)) { + reverseAnimationOptions = { + transitionOutClass: this._animationClass + }; + } else { + reverseAnimationOptions = { + transitionInClass: this._animationClass['close'], + transitionOutClass: this._animationClass['open'] + }; + } + } + + return animator + .translate3d(panelEl, closeFrom, closeTo, reverseAnimationOptions); }; + /** - * Returns the selected tab. - * @return {!NavItemController|undefined} + * Set the height and width to match the panel if not provided. + * @param {!angular.JQLite} panelEl * @private */ -MdNavBarController.prototype._getSelectedTab = function() { - return this._findTab(function(tab) { - return tab.isSelected() - }); -}; +MdPanelAnimation.prototype._fixBounds = function(panelEl) { + var panelWidth = panelEl[0].offsetWidth; + var panelHeight = panelEl[0].offsetHeight; -/** - * Returns the focused tab. - * @return {!NavItemController|undefined} - */ -MdNavBarController.prototype.getFocusedTab = function() { - return this._findTab(function(tab) { - return tab.hasFocus() - }); + if (this._openFrom && this._openFrom.bounds.height == null) { + this._openFrom.bounds.height = panelHeight; + } + if (this._openFrom && this._openFrom.bounds.width == null) { + this._openFrom.bounds.width = panelWidth; + } + if (this._closeTo && this._closeTo.bounds.height == null) { + this._closeTo.bounds.height = panelHeight; + } + if (this._closeTo && this._closeTo.bounds.width == null) { + this._closeTo.bounds.width = panelWidth; + } }; + /** - * Find a tab that matches the specified function. + * Identify the bounding RECT for the target element. + * @param {!angular.JQLite} element + * @returns {{element: !angular.JQLite|undefined, bounds: !DOMRect}} * @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]; - } +MdPanelAnimation.prototype._getBoundingClientRect = function(element) { + if (element instanceof angular.element) { + return { + element: element, + bounds: element[0].getBoundingClientRect() + }; } - - return null; }; + +/***************************************************************************** + * Util Methods * + *****************************************************************************/ + /** - * Direct focus to the selected tab when focus enters the nav bar. + * Returns the angular element associated with a css selector or element. + * @param el {string|!angular.JQLite|!Element} + * @returns {!angular.JQLite} */ -MdNavBarController.prototype.onFocus = function() { - var tab = this._getSelectedTab(); - if (tab) { - tab.setFocused(true); - } -}; +function getElement(el) { + var queryResult = angular.isString(el) ? + document.querySelector(el) : el; + return angular.element(queryResult); +} + +})(); +(function(){ +"use strict"; /** - * Clear tab focus when focus leaves the nav bar. + * @ngdoc module + * @name material.components.progressCircular + * @description Module for a circular progressbar */ -MdNavBarController.prototype.onBlur = function() { - var tab = this.getFocusedTab(); - if (tab) { - tab.setFocused(false); - } -}; + +angular.module('material.components.progressCircular', ['material.core']); + +})(); +(function(){ +"use strict"; /** - * Move focus from oldTab to newTab. - * @param {!NavItemController} oldTab - * @param {!NavItemController} newTab - * @private + * @ngdoc module + * @name material.components.progressLinear + * @description Linear Progress module! */ -MdNavBarController.prototype._moveFocus = function(oldTab, newTab) { - oldTab.setFocused(false); - newTab.setFocused(true); -}; +angular.module('material.components.progressLinear', [ + 'material.core' +]) + .directive('mdProgressLinear', MdProgressLinearDirective); /** - * Responds to keypress events. - * @param {!Event} e + * @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 + * + * + * + * + * + * + * + * + * + * + * */ -MdNavBarController.prototype.onKeydown = function(e) { - var keyCodes = this._$mdConstant.KEY_CODE; - var tabs = this._getTabs(); - var focusedTab = this.getFocusedTab(); - if (!focusedTab) return; +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"; - var focusedTabIndex = tabs.indexOf(focusedTab); + return { + restrict: 'E', + template: '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ', + compile: compile + }; - // 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; + 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); -/** - * @ngInject - */ -function MdNavItem($$rAF) { - return { - restrict: 'E', - require: ['mdNavItem', '^mdNavBar'], - controller: MdNavItemController, - bindToController: true, - controllerAs: 'ctrl', - replace: true, - transclude: true, - template: - '
  • ' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '
  • ', - scope: { - 'mdNavClick': '&?', - 'mdNavHref': '@?', - 'mdNavSref': '@?', - 'name': '@', - }, - link: function(scope, element, attrs, controllers) { - var mdNavItem = controllers[0]; - var mdNavBar = controllers[1]; + 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')); - // 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() { - if (!mdNavItem.name) { - mdNavItem.name = angular.element(element[0].querySelector('._md-nav-button-text')) - .text().trim(); + element + .attr('md-mode', mode()) + .toggleClass(DISABLED_CLASS, isDisabled); + + 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('disabled', function(value) { + if (value === true || value === false) { + isDisabled = !!value; + } else { + isDisabled = angular.isDefined(value); + } + + element.toggleClass(DISABLED_CLASS, isDisabled); + container.toggleClass(lastMode, !isDisabled); + }); + + 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; } - - var navButton = angular.element(element[0].querySelector('._md-nav-button')); - navButton.on('click', function() { - mdNavBar.mdSelectedNavItem = mdNavItem.name; - scope.$apply(); - }); }); } - }; -} -MdNavItem.$inject = ["$$rAF"]; -/** - * Controller for the nav-item component. - * @param {!angular.JQLite} $element - * @constructor - * @final - * @ngInject - */ -function MdNavItemController($element) { + /** + * 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"; - /** @private @const {!angular.JQLite} */ - this._$element = $element; + //$log.debug( $mdUtil.supplant(info, [mode]) ); - // Data-bound variables - /** @const {?Function} */ - this.mdNavClick; - /** @const {?string} */ - this.mdNavHref; - /** @const {?string} */ - this.name; + element.attr("md-mode", mode); + attr.mdMode = mode; + } + } - // State variables - /** @private {boolean} */ - this._selected = false; + /** + * 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; + } + } + return value; + } - /** @private {boolean} */ - this._focused = false; + /** + * Manually set CSS to animate the Determinate indicator based on the specified + * percentage value (0-100). + */ + function animateIndicator(target, value) { + if ( isDisabled || !mode() ) return; - var hasNavClick = !!($element.attr('md-nav-click')); - var hasNavHref = !!($element.attr('md-nav-href')); - var hasNavSref = !!($element.attr('md-nav-sref')); + var to = $mdUtil.supplant("translateX({0}%) scale({1},1)", [ (value-100)/2, value/100 ]); + var styles = toVendorCSS({ transform : to }); + angular.element(target).css( styles ); + } + } - // Cannot specify more than one nav attribute - if ((hasNavClick ? 1:0) + (hasNavHref ? 1:0) + (hasNavSref ? 1:0) > 1) { - throw Error( - 'Must specify exactly one of md-nav-click, md-nav-href, ' + - 'md-nav-sref for nav-item directive'); + /** + * 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)); } } -MdNavItemController.$inject = ["$element"]; +MdProgressLinearDirective.$inject = ["$mdTheming", "$mdUtil", "$log"]; -/** - * 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, - }; -}; -/** - * Get the name attribute of the tab. - * @return {string} - */ -MdNavItemController.prototype.getName = function() { - return this.name; -}; +})(); +(function(){ +"use strict"; /** - * Get the button element associated with the tab. - * @return {!Element} + * @ngdoc module + * @name material.components.radioButton + * @description radioButton module! */ -MdNavItemController.prototype.getButtonEl = function() { - return this._$element[0].querySelector('._md-nav-button'); -}; +angular.module('material.components.radioButton', [ + 'material.core' +]) + .directive('mdRadioGroup', mdRadioGroupDirective) + .directive('mdRadioButton', mdRadioButtonDirective); /** - * Set the selected state of the tab. - * @param {boolean} isSelected + * @ngdoc directive + * @module material.components.radioButton + * @name mdRadioGroup + * + * @restrict E + * + * @description + * 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. + * + * @usage + * + * + * + * + * + * {{ d.label }} + * + * + * + * + * + * */ -MdNavItemController.prototype.setSelected = function(isSelected) { - this._selected = isSelected; -}; +function mdRadioGroupDirective($mdUtil, $mdConstant, $mdTheming, $timeout) { + RadioGroupController.prototype = createRadioGroupControllerProto(); + + return { + restrict: 'E', + controller: ['$element', RadioGroupController], + require: ['mdRadioGroup', '?ngModel'], + link: { pre: linkRadioGroup } + }; + + function linkRadioGroup(scope, element, attr, ctrls) { + element.addClass('_md'); // private md component indicator for styling + $mdTheming(element); + + var rgCtrl = ctrls[0]; + var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel(); + + rgCtrl.init(ngModelCtrl); + + 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'); + }); + + /** + * + */ + function setFocus() { + if (!element.hasClass('md-focused')) { element.addClass('md-focused'); } + } + + /** + * + */ + function keydownListener(ev) { + var keyCode = ev.which || ev.keyCode; + + // Only listen to events that we originated ourselves + // so that we don't trigger on things like arrow keys in + // inputs. + + if (keyCode != $mdConstant.KEY_CODE.ENTER && + ev.currentTarget != ev.target) { + return; + } + + switch (keyCode) { + case $mdConstant.KEY_CODE.LEFT_ARROW: + case $mdConstant.KEY_CODE.UP_ARROW: + ev.preventDefault(); + rgCtrl.selectPrevious(); + setFocus(); + break; + + case $mdConstant.KEY_CODE.RIGHT_ARROW: + case $mdConstant.KEY_CODE.DOWN_ARROW: + ev.preventDefault(); + rgCtrl.selectNext(); + setFocus(); + break; + + case $mdConstant.KEY_CODE.ENTER: + var form = angular.element($mdUtil.getClosest(element[0], 'form')); + if (form.length > 0) { + form.triggerHandler('submit'); + } + break; + } + + } + } + + function RadioGroupController($element) { + this._radioButtonRenderFns = []; + this.$element = $element; + } -/** - * @return {boolean} - */ -MdNavItemController.prototype.isSelected = function() { - return this._selected; -}; + 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); + }, + 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); -/** - * Set the focused state of the tab. - * @param {boolean} isFocused - */ -MdNavItemController.prototype.setFocused = function(isFocused) { - this._focused = isFocused; -}; + if (buttons.count()) { + var validate = function (button) { + // If disabled, then NOT valid + return !angular.element(button).attr("disabled"); + }; -/** - * @return {boolean} - */ -MdNavItemController.prototype.hasFocus = function() { - return this._focused; -}; + var selected = parent[0].querySelector('md-radio-button.md-checked'); + var target = buttons[increment < 0 ? 'previous' : 'next'](selected, validate) || buttons.first(); -})(); -(function(){ -"use strict"; + // Activate radioButton's click listener (triggerHandler won't create a real click event) + angular.element(target).triggerHandler('click'); -/** - * @ngdoc module - * @name material.components.panel - */ -angular - .module('material.components.panel', [ - 'material.core', - 'material.components.backdrop' - ]) - .service('$mdPanel', MdPanelService); + } + } -/***************************************************************************** - * PUBLIC DOCUMENTATION * - *****************************************************************************/ +} +mdRadioGroupDirective.$inject = ["$mdUtil", "$mdConstant", "$mdTheming", "$timeout"]; /** - * @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 = $mdPanelPosition - * .absolute() - * .top('50%') - * .left('50%'); - * - * var panelAnimation = $mdPanelAnimation - * .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, - * template: 'dialog-template.html', - * clickOutsideToClose: true, - * escapeToClose: true, - * focusOnOpen: true - * } - * panelRef = $mdPanel.create(config); - * panelRef.open() - * .finally(function() { - * panelRef = undefined; - * }); - * } + * @ngdoc directive + * @module material.components.radioButton + * @name mdRadioButton * - * function DialogController(MdPanelRef, toppings) { - * var toppings; + * @restrict E * - * function closeDialog() { - * MdPanelRef.close(); - * } - * } - * })(angular); - * - */ - -/** - * @ngdoc method - * @name $mdPanel#create * @description - * Creates a panel with the specified options. - * - * @param opt_config {Object=} Specific configuration object that may contain - * the following properties: - * - * - `template` - `{string=}`: HTML template to show in the dialog. 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. - * - `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. - * - `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. - * - `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. + * The ``directive is the child directive required to be used within `` elements. * - * TODO(ErinCoughlan): Add the following config options. - * - `groupName` - `{string=}`: Name of panel groups. This group name is - * used for configuring the number of open panels and identifying specific - * behaviors for groups. For instance, all tooltips will be identified - * using the same groupName. + * While similar to the `` directive, + * the `` directive provides ink effects, ARIA support, and + * supports use within named radio groups. * - * @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 {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. * - * @param {Object=} opt_config Specific configuration object that may contain - * the properties defined in `$mdPanel.create`. + * @usage + * * - * @returns {angular.$q.Promise} panelRef A promise that resolves - * to an instance of the panel. - */ - - -/** - * @ngdoc method - * @name $mdPanel#setGroupMaxOpen - * @description - * Sets the maximum number of panels in a group that can be opened at a given - * time. + * + * Label 1 + * * - * @param groupName {string} The name of the group to configure. - * @param maxOpen {number} The max number of panels that can be opened. - */ - - -/** - * @ngdoc method - * @name $mdPanel#newPanelPosition - * @description - * Returns a new instance of the MdPanelPosition object. Use this to create - * the position config object. + * + * Green + * * - * @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 */ +function mdRadioButtonDirective($mdAria, $mdUtil, $mdTheming) { + var CHECKED_CSS = 'md-checked'; -/***************************************************************************** - * MdPanelRef * - *****************************************************************************/ - + return { + restrict: 'E', + require: '^mdRadioGroup', + transclude: true, + template: '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ', + link: link + }; -/** - * @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. - */ + function link(scope, element, attr, rgCtrl) { + var lastChecked; -/** - * @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. - */ + $mdTheming(element); + configureAria(element, scope); -/** - * @ngdoc method - * @name MdPanelRef#close - * @description - * Hides and detaches the panel. - * - * @returns {!angular.$q.Promise} A promise that is resolved when the panel is - * closed. - */ + initialize(); -/** - * @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. - */ + /** + * + */ + function initialize() { + if (!rgCtrl) { + throw 'RadioButton: No RadioGroupController could be found.'; + } -/** - * @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. - */ + rgCtrl.add(render); + attr.$observe('value', render); -/** - * @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. - */ + element + .on('click', listener) + .on('$destroy', function() { + rgCtrl.remove(render); + }); + } -/** - * @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. - */ + /** + * + */ + function listener(ev) { + if (element[0].hasAttribute('disabled') || rgCtrl.isDisabled()) return; -/** - * @ngdoc method - * @name MdPanelRef#destroy - * @description - * Destroys the panel. The panel cannot be opened again after this is called. - */ + scope.$apply(function() { + rgCtrl.setViewValue(attr.value, ev && ev.type); + }); + } -/** - * @ngdoc method - * @name MdPanelRef#addClass - * @description - * Adds a class to the panel. DO NOT use this to hide/show the panel. - * - * @param {string} newClass Class to be added. - */ + /** + * 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; + } -/** - * @ngdoc method - * @name MdPanelRef#removeClass - * @description - * Removes a class from the panel. DO NOT use this to hide/show the panel. - * - * @param {string} oldClass Class to be removed. - */ + lastChecked = checked; + element.attr('aria-checked', checked); -/** - * @ngdoc method - * @name MdPanelRef#toggleClass - * @description - * Toggles a class on the panel. DO NOT use this to hide/show the panel. - * - * @param {string} toggleClass Class to be toggled. - */ + if (checked) { + markParentAsChecked(true); + element.addClass(CHECKED_CSS); -/** - * @ngdoc method - * @name MdPanelRef#focusOnOpen - * @description - * Focuses the panel content if the focusOnOpen config value is true. - */ + rgCtrl.setActiveDescendant(element.attr('id')); + } else { + markParentAsChecked(false); + element.removeClass(CHECKED_CSS); + } -/***************************************************************************** - * MdPanelPosition * - *****************************************************************************/ + /** + * 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); + } + } + } -/** - * @ngdoc type - * @name MdPanelPosition - * @module material.components.panel - * @description - * Object for configuring the position of the panel. Examples: - * - * 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); - */ + /** + * Inject ARIA-specific attributes appropriate for each radio button + */ + function configureAria( element, scope ){ + scope.ariaId = buildAriaID(); -/** - * @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} - */ + element.attr({ + 'id' : scope.ariaId, + 'role' : 'radio', + 'aria-checked' : 'false' + }); -/** - * @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} - */ + $mdAria.expectWithText(element, 'aria-label'); -/** - * @ngdoc method - * @name MdPanelPosition#top - * @description - * Sets the value of `top` for the panel. Clears any previously set - * vertical position. - * @param {string=} opt_top Value of `top`. Defaults to '0'. - * @returns {MdPanelPosition} - */ + /** + * Build a unique ID for each radio button that will be used with aria-activedescendant. + * Preserve existing ID if already specified. + * @returns {*|string} + */ + function buildAriaID() { + return attr.id || ( 'radio' + "_" + $mdUtil.nextUid() ); + } + } + } +} +mdRadioButtonDirective.$inject = ["$mdAria", "$mdUtil", "$mdTheming"]; -/** - * @ngdoc method - * @name MdPanelPosition#bottom - * @description - * Sets the value of `bottom` for the panel. Clears any previously set - * vertical position. - * @param {string=} opt_bottom Value of `bottom`. Defaults to '0'. - * @returns {MdPanelPosition} - */ +})(); +(function(){ +"use strict"; /** - * @ngdoc method - * @name MdPanelPosition#left - * @description - * Sets the value of `left` for the panel. Clears any previously set - * horizontal position. - * @param {string=} opt_left Value of `left`. Defaults to '0'. - * @returns {MdPanelPosition} + * @ngdoc module + * @name material.components.select */ -/** - * @ngdoc method - * @name MdPanelPosition#right - * @description - * Sets the value of `right` for the panel. Clears any previously set - * horizontal position. - * @param {string=} opt_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} - */ + ### TODO - POST RC1 ### + - [ ] Abstract placement logic in $mdSelect service to $mdMenu service -/** - * @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} - */ +var SELECT_EDGE_MARGIN = 8; +var selectNextId = 0; +var CHECKBOX_SELECTION_INDICATOR = + angular.element('
    '); + +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); /** - * @ngdoc method - * @name MdPanelPosition#addPanelPosition - * @param {string} xPosition - * @param {string} yPosition - * @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. + * @ngdoc directive + * @name mdSelect + * @restrict E + * @module material.components.select * - * xPosition must be one of the following values available on - * $mdPanel.xPosition: + * @description Displays a select box, bound to an ng-model. * - * CENTER | ALIGN_START | ALIGN_END | OFFSET_START | OFFSET_END + * 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. * - * ************* - * * * - * * PANEL * - * * * - * ************* - * A B C D E + * 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. * - * 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) + * ### Option Params * - * yPosition must be one of the following values available on - * $mdPanel.yPosition: + * 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. * - * CENTER | ALIGN_TOPS | ALIGN_BOTTOMS | ABOVE | BELOW + * **Automatically Applied** * - * F - * G ************* - * * * - * H * PANEL * - * * * - * I ************* - * J + * - `` + * - `` + * - `` + * - `` + * - `` * - * F: BELOW - * G: ALIGN_TOPS - * H: CENTER - * I: ALIGN_BOTTOMS - * J: ABOVE - * @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 object - * @name MdPanelAnimation - * @description - * Animation configuration object. To use, create an MdPanelAnimation with the - * desired properties, then pass the object as part of $mdPanel creation. + * **NOT Automatically Applied** * - * Example: + * - `` + * - `` + * - `` + * - `` (this evaluates to the string `"undefined"`) + * - <md-option ng-value="{{someValueThatMightBeUndefined}}"> * - * var panelAnimation = new MdPanelAnimation() - * .openFrom(myButtonEl) - * .closeTo('.my-button') - * .withAnimation($mdPanel.animation.SCALE); + * **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. * - * $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 {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 {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. + * @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. + * @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). * - * @param {string|!Element|!Event|{top: number, left: number}} - * @returns {MdPanelAnimation} - */ - -/** - * @ngdoc method - * @name MdPanelAnimation#closeTo - * @description - * Specifies where to animate the dialog close. `closeTo` accepts a - * query selector, DOM element, or a Rect object that is used to determine - * the bounds. + * @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 }} + * + *
    + *
    * - * @param {string|!Element|{top: number, left: number}} - * @returns {MdPanelAnimation} - */ - -/** - * @ngdoc method - * @name MdPanelAnimation#withAnimation - * @description - * Specifies the animation class. + * 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);`; * - * 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. + * 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. * - * Custom classes will by default fade in and out unless - * "transition: opacity 1ms" is added to the to custom class. + * 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. * - * @param {string|{open: string, close: string}} cssClass - * @returns {MdPanelAnimation} + * Working HTML: + * + *
    + * + * {{ user.name }} + * + *
    + *
    */ +function SelectDirective($mdSelect, $mdUtil, $mdConstant, $mdTheming, $mdAria, $compile, $parse) { + var keyCodes = $mdConstant.KEY_CODE; + var NAVIGATION_KEYS = [keyCodes.SPACE, keyCodes.ENTER, keyCodes.UP_ARROW, keyCodes.DOWN_ARROW]; + return { + restrict: 'E', + require: ['^?mdInputContainer', 'mdSelect', 'ngModel', '?^form'], + compile: compile, + controller: function() { + } // empty placeholder controller to be initialized in link + }; -/***************************************************************************** - * IMPLEMENTATION * - *****************************************************************************/ + 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())); + } -// Default z-index for the panel. -var defaultZIndex = 80; -var MD_PANEL_HIDDEN = '_md-panel-hidden'; -var FOCUS_TRAP_TEMPLATE = angular.element( - '
    '); + // Add progress spinner for md-options-loading + if (attr.mdOnOpen) { + // 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( + '
    ' + + ' ' + + '
    ' + )); -/** - * A service that is used for controlling/displaying panels on the screen. - * @param {!angular.JQLite} $rootElement - * @param {!angular.Scope} $rootScope - * @param {!angular.$injector} $injector - * @param {!angular.$window} $window - * @final @constructor @ngInject - */ -function MdPanelService($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} - */ - this._defaultConfigOptions = { - bindToController: true, - clickOutsideToClose: false, - disableParentScroll: false, - escapeToClose: false, - focusOnOpen: true, - fullscreen: false, - hasBackdrop: false, - transformTemplate: angular.bind(this, this._wrapTemplate), - trapFocus: false, - zIndex: defaultZIndex - }; + // Hide list [of item options] while loading async + element + .find('md-option') + .attr('ng-show', '$$loadingAsyncDone'); + } - /** @private {!Object} */ - this._config = {}; + if (attr.name) { + var autofillClone = angular.element(',