mirror of
https://github.com/inverse-inc/sogo.git
synced 2026-03-22 23:02:43 +00:00
39666 lines
1.3 MiB
39666 lines
1.3 MiB
/*!
|
||
* AngularJS Material Design
|
||
* https://github.com/angular/material
|
||
* @license MIT
|
||
* v1.2.4
|
||
*/
|
||
(function( window, angular, undefined ){
|
||
"use strict";
|
||
|
||
(function(){
|
||
"use strict";
|
||
|
||
angular.module('ngMaterial', ["ng","ngAnimate","ngAria","material.core","material.core.animate","material.core.gestures","material.core.interaction","material.core.layout","material.core.meta","material.core.theming.palette","material.core.theming","material.components.autocomplete","material.components.backdrop","material.components.bottomSheet","material.components.button","material.components.card","material.components.checkbox","material.components.chips","material.components.colors","material.components.content","material.components.datepicker","material.components.dialog","material.components.divider","material.components.fabActions","material.components.fabShared","material.components.fabSpeedDial","material.components.fabToolbar","material.components.gridList","material.components.icon","material.components.input","material.components.list","material.components.menu","material.components.menuBar","material.components.navBar","material.components.panel","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.truncate","material.components.virtualRepeat","material.components.whiteframe"]);
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* Initialization function that validates environment
|
||
* requirements.
|
||
*/
|
||
DetectNgTouch.$inject = ["$log", "$injector"];
|
||
MdCoreConfigure.$inject = ["$provide", "$mdThemingProvider"];
|
||
rAFDecorator.$inject = ["$delegate"];
|
||
qDecorator.$inject = ["$delegate"];
|
||
angular
|
||
.module('material.core', [
|
||
'ngAnimate',
|
||
'material.core.animate',
|
||
'material.core.layout',
|
||
'material.core.interaction',
|
||
'material.core.gestures',
|
||
'material.core.theming'
|
||
])
|
||
.config(MdCoreConfigure)
|
||
.run(DetectNgTouch);
|
||
|
||
|
||
/**
|
||
* Detect if the ng-Touch module is also being used.
|
||
* Warn if detected.
|
||
* @ngInject
|
||
*/
|
||
function DetectNgTouch($log, $injector) {
|
||
if ($injector.has('$swipe')) {
|
||
var msg = "" +
|
||
"You are using the ngTouch module. \n" +
|
||
"AngularJS Material already has mobile click, tap, and swipe support... \n" +
|
||
"ngTouch is not supported with AngularJS Material!";
|
||
$log.warn(msg);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @ngInject
|
||
*/
|
||
function MdCoreConfigure($provide, $mdThemingProvider) {
|
||
|
||
$provide.decorator('$$rAF', ['$delegate', rAFDecorator]);
|
||
$provide.decorator('$q', ['$delegate', qDecorator]);
|
||
|
||
$mdThemingProvider.theme('default')
|
||
.primaryPalette('indigo')
|
||
.accentPalette('pink')
|
||
.warnPalette('deep-orange')
|
||
.backgroundPalette('grey');
|
||
}
|
||
|
||
/**
|
||
* @ngInject
|
||
*/
|
||
function rAFDecorator($delegate) {
|
||
/**
|
||
* Use this to throttle events that come in often.
|
||
* The throttled function will always use the *last* invocation before the
|
||
* coming frame.
|
||
*
|
||
* For example, window resize events that fire many times a second:
|
||
* If we set to use an raf-throttled callback on window resize, then
|
||
* our callback will only be fired once per frame, with the last resize
|
||
* event that happened before that frame.
|
||
*
|
||
* @param {function} cb function to debounce
|
||
*/
|
||
$delegate.throttle = function(cb) {
|
||
var queuedArgs, alreadyQueued, queueCb, context;
|
||
return function debounced() {
|
||
queuedArgs = arguments;
|
||
context = this;
|
||
queueCb = cb;
|
||
if (!alreadyQueued) {
|
||
alreadyQueued = true;
|
||
$delegate(function() {
|
||
queueCb.apply(context, Array.prototype.slice.call(queuedArgs));
|
||
alreadyQueued = false;
|
||
});
|
||
}
|
||
};
|
||
};
|
||
return $delegate;
|
||
}
|
||
|
||
/**
|
||
* @ngInject
|
||
*/
|
||
function qDecorator($delegate) {
|
||
/**
|
||
* Adds a shim for $q.resolve for AngularJS version that don't have it,
|
||
* so we don't have to think about it.
|
||
*
|
||
* via https://github.com/angular/angular.js/pull/11987
|
||
*/
|
||
|
||
// TODO(crisbeto): this won't be necessary once we drop AngularJS 1.3
|
||
if (!$delegate.resolve) {
|
||
$delegate.resolve = $delegate.when;
|
||
}
|
||
return $delegate;
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
|
||
MdAutofocusDirective.$inject = ["$parse"];angular.module('material.core')
|
||
.directive('mdAutofocus', MdAutofocusDirective);
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdAutofocus
|
||
* @module material.core.util
|
||
*
|
||
* @description
|
||
*
|
||
* `[md-autofocus]` provides an optional way to identify the focused element when a `$mdDialog`,
|
||
* `$mdBottomSheet`, `$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
|
||
* value to enable conditional activation of the autofocus.
|
||
*
|
||
* @usage
|
||
*
|
||
* ### Dialog
|
||
* <hljs lang="html">
|
||
* <md-dialog>
|
||
* <form>
|
||
* <md-input-container>
|
||
* <label for="testInput">Label</label>
|
||
* <input id="testInput" type="text" md-autofocus>
|
||
* </md-input-container>
|
||
* </form>
|
||
* </md-dialog>
|
||
* </hljs>
|
||
*
|
||
* ### Bottomsheet
|
||
* <hljs lang="html">
|
||
* <md-bottom-sheet class="md-list md-has-header">
|
||
* <md-subheader>Comment Actions</md-subheader>
|
||
* <md-list>
|
||
* <md-list-item ng-repeat="item in items">
|
||
*
|
||
* <md-button md-autofocus="$index == 2">
|
||
* <md-icon md-svg-src="{{item.icon}}"></md-icon>
|
||
* <span class="md-inline-list-icon-label">{{ item.name }}</span>
|
||
* </md-button>
|
||
*
|
||
* </md-list-item>
|
||
* </md-list>
|
||
* </md-bottom-sheet>
|
||
* </hljs>
|
||
*
|
||
* ### Autocomplete
|
||
* <hljs lang="html">
|
||
* <md-autocomplete
|
||
* md-autofocus
|
||
* md-selected-item="selectedItem"
|
||
* md-search-text="searchText"
|
||
* md-items="item in getMatches(searchText)"
|
||
* md-item-text="item.display">
|
||
* <span md-highlight-text="searchText">{{item.display}}</span>
|
||
* </md-autocomplete>
|
||
* </hljs>
|
||
*
|
||
* ### Sidenav
|
||
* <hljs lang="html">
|
||
* <div layout="row" ng-controller="MyController">
|
||
* <md-sidenav md-component-id="left" class="md-sidenav-left">
|
||
* Left Nav!
|
||
* </md-sidenav>
|
||
*
|
||
* <md-content>
|
||
* Center Content
|
||
* <md-button ng-click="openLeftMenu()">
|
||
* Open Left Menu
|
||
* </md-button>
|
||
* </md-content>
|
||
*
|
||
* <md-sidenav md-component-id="right"
|
||
* md-is-locked-open="$mdMedia('min-width: 333px')"
|
||
* class="md-sidenav-right">
|
||
* <form>
|
||
* <md-input-container>
|
||
* <label for="testInput">Test input</label>
|
||
* <input id="testInput" type="text"
|
||
* ng-model="data" md-autofocus>
|
||
* </md-input-container>
|
||
* </form>
|
||
* </md-sidenav>
|
||
* </div>
|
||
* </hljs>
|
||
**/
|
||
function MdAutofocusDirective($parse) {
|
||
return {
|
||
restrict: 'A',
|
||
link: {
|
||
pre: preLink
|
||
}
|
||
};
|
||
|
||
function preLink(scope, element, attr) {
|
||
var attrExp = attr.mdAutoFocus || attr.mdAutofocus || attr.mdSidenavFocus;
|
||
|
||
// Initially update the expression by manually parsing the expression as per $watch source.
|
||
updateExpression($parse(attrExp)(scope));
|
||
|
||
// Only watch the expression if it is not empty.
|
||
if (attrExp) {
|
||
scope.$watch(attrExp, updateExpression);
|
||
}
|
||
|
||
/**
|
||
* Updates the autofocus class which is used to determine whether the attribute
|
||
* expression evaluates to true or false.
|
||
* @param {string|boolean} value Attribute Value
|
||
*/
|
||
function updateExpression(value) {
|
||
|
||
// Rather than passing undefined to the jqLite toggle class function we explicitly set the
|
||
// value to true. Otherwise the class will be just toggled instead of being forced.
|
||
if (angular.isUndefined(value)) {
|
||
value = true;
|
||
}
|
||
|
||
element.toggleClass('md-autofocus', !!value);
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"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 {string} color
|
||
* @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 {string} color
|
||
* @returns {string}
|
||
*/
|
||
function rgbToRgba (color) {
|
||
return color.replace(')', ', 0.1)').replace('(', 'a(');
|
||
}
|
||
|
||
/**
|
||
* Converts an RGBA color to RGB
|
||
* @param {string} color
|
||
* @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);
|
||
|
||
/**
|
||
* Factory function that creates the grab-bag $mdConstant service.
|
||
* @ngInject
|
||
*/
|
||
function MdConstantFactory() {
|
||
|
||
var prefixTestEl = document.createElement('div');
|
||
var vendorPrefix = getVendorPrefix(prefixTestEl);
|
||
var isWebkit = /webkit/i.test(vendorPrefix);
|
||
var SPECIAL_CHARS_REGEXP = /([:\-_]+(.))/g;
|
||
|
||
/**
|
||
* @param {string} name CSS property name
|
||
* @return {string} the property name supported by the browser
|
||
*/
|
||
function vendorProperty(name) {
|
||
// Add a dash between the prefix and name, to be able to transform the string into camelcase.
|
||
var prefixedName = vendorPrefix + '-' + name;
|
||
var ucPrefix = camelCase(prefixedName);
|
||
var lcPrefix = ucPrefix.charAt(0).toLowerCase() + ucPrefix.substring(1);
|
||
|
||
return hasStyleProperty(prefixTestEl, name) ? name : // The current browser supports the un-prefixed property
|
||
hasStyleProperty(prefixTestEl, ucPrefix) ? ucPrefix : // The current browser only supports the prefixed property.
|
||
hasStyleProperty(prefixTestEl, lcPrefix) ? lcPrefix : name; // Some browsers are only supporting the prefix in lowercase.
|
||
}
|
||
|
||
function hasStyleProperty(testElement, property) {
|
||
return angular.isDefined(testElement.style[property]);
|
||
}
|
||
|
||
/**
|
||
* @param {!string} input value to convert to camelCase
|
||
* @return {string} camelCased version of the input string
|
||
*/
|
||
function camelCase(input) {
|
||
return input.replace(SPECIAL_CHARS_REGEXP, function(matches, separator, letter, offset) {
|
||
return offset ? letter.toUpperCase() : letter;
|
||
});
|
||
}
|
||
|
||
function getVendorPrefix(testElement) {
|
||
var prop, match;
|
||
var vendorRegex = /^(Moz|webkit|ms)(?=[A-Z])/;
|
||
|
||
for (prop in testElement.style) {
|
||
if (match = vendorRegex.exec(prop)) {
|
||
return match[0];
|
||
}
|
||
}
|
||
}
|
||
|
||
var self = {
|
||
isInputKey : function(e) { return (e.keyCode >= 31 && e.keyCode <= 90); },
|
||
isNumPadKey : function(e) { return (3 === e.location && e.keyCode >= 97 && e.keyCode <= 105); },
|
||
isMetaKey: function(e) { return (e.keyCode >= 91 && e.keyCode <= 93); },
|
||
isFnLockKey: function(e) { return (e.keyCode >= 112 && e.keyCode <= 145); },
|
||
isNavigationKey : function(e) {
|
||
var kc = self.KEY_CODE, NAVIGATION_KEYS = [kc.SPACE, kc.ENTER, kc.UP_ARROW, kc.DOWN_ARROW];
|
||
return (NAVIGATION_KEYS.indexOf(e.keyCode) != -1);
|
||
},
|
||
hasModifierKey: function(e) {
|
||
return e.ctrlKey || e.metaKey || e.altKey;
|
||
},
|
||
|
||
/**
|
||
* Maximum size, in pixels, that can be explicitly set to an element. The actual value varies
|
||
* between browsers, but IE11 has the very lowest size at a mere 1,533,917px. Ideally we could
|
||
* compute this value, but Firefox always reports an element to have a size of zero if it
|
||
* goes over the max, meaning that we'd have to binary search for the value.
|
||
*/
|
||
ELEMENT_MAX_PIXELS: 1533917,
|
||
|
||
/**
|
||
* Priority for a directive that should run before the directives from ngAria.
|
||
*/
|
||
BEFORE_NG_ARIA: 210,
|
||
|
||
/**
|
||
* Common Keyboard actions and their associated keycode.
|
||
*/
|
||
KEY_CODE: {
|
||
COMMA: 188,
|
||
SEMICOLON : 186,
|
||
ENTER: 13,
|
||
ESCAPE: 27,
|
||
SPACE: 32,
|
||
PAGE_UP: 33,
|
||
PAGE_DOWN: 34,
|
||
END: 35,
|
||
HOME: 36,
|
||
LEFT_ARROW : 37,
|
||
UP_ARROW : 38,
|
||
RIGHT_ARROW : 39,
|
||
DOWN_ARROW : 40,
|
||
TAB : 9,
|
||
BACKSPACE: 8,
|
||
DELETE: 46
|
||
},
|
||
|
||
/**
|
||
* Vendor prefixed CSS properties to be used to support the given functionality in older browsers
|
||
* as well.
|
||
*/
|
||
CSS: {
|
||
/* Constants */
|
||
TRANSITIONEND: 'transitionend' + (isWebkit ? ' webkitTransitionEnd' : ''),
|
||
ANIMATIONEND: 'animationend' + (isWebkit ? ' webkitAnimationEnd' : ''),
|
||
|
||
TRANSFORM: vendorProperty('transform'),
|
||
TRANSFORM_ORIGIN: vendorProperty('transformOrigin'),
|
||
TRANSITION: vendorProperty('transition'),
|
||
TRANSITION_DURATION: vendorProperty('transitionDuration'),
|
||
ANIMATION_PLAY_STATE: vendorProperty('animationPlayState'),
|
||
ANIMATION_DURATION: vendorProperty('animationDuration'),
|
||
ANIMATION_NAME: vendorProperty('animationName'),
|
||
ANIMATION_TIMING: vendorProperty('animationTimingFunction'),
|
||
ANIMATION_DIRECTION: vendorProperty('animationDirection')
|
||
},
|
||
|
||
/**
|
||
* As defined in core/style/_variables.scss
|
||
*
|
||
* $layout-breakpoint-xs: 600px !default;
|
||
* $layout-breakpoint-sm: 960px !default;
|
||
* $layout-breakpoint-md: 1280px !default;
|
||
* $layout-breakpoint-lg: 1920px !default;
|
||
*
|
||
*/
|
||
MEDIA: {
|
||
'xs' : '(max-width: 599px)' ,
|
||
'gt-xs' : '(min-width: 600px)' ,
|
||
'sm' : '(min-width: 600px) and (max-width: 959px)' ,
|
||
'gt-sm' : '(min-width: 960px)' ,
|
||
'md' : '(min-width: 960px) and (max-width: 1279px)' ,
|
||
'gt-md' : '(min-width: 1280px)' ,
|
||
'lg' : '(min-width: 1280px) and (max-width: 1919px)',
|
||
'gt-lg' : '(min-width: 1920px)' ,
|
||
'xl' : '(min-width: 1920px)' ,
|
||
'landscape' : '(orientation: landscape)' ,
|
||
'portrait' : '(orientation: portrait)' ,
|
||
'print' : 'print'
|
||
},
|
||
|
||
MEDIA_PRIORITY: [
|
||
'xl',
|
||
'gt-lg',
|
||
'lg',
|
||
'gt-md',
|
||
'md',
|
||
'gt-sm',
|
||
'sm',
|
||
'gt-xs',
|
||
'xs',
|
||
'landscape',
|
||
'portrait',
|
||
'print'
|
||
]
|
||
};
|
||
|
||
return self;
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
angular
|
||
.module('material.core')
|
||
.config(["$provide", function($provide){
|
||
$provide.decorator('$mdUtil', ['$delegate', function ($delegate){
|
||
/**
|
||
* Inject the iterator facade to easily support iteration and accessors
|
||
* @see iterator below
|
||
*/
|
||
$delegate.iterator = MdIterator;
|
||
|
||
return $delegate;
|
||
}
|
||
]);
|
||
}]);
|
||
|
||
/**
|
||
* iterator is a list facade to easily support iteration and accessors/
|
||
*
|
||
* @param {any[]} items Array list which this iterator will enumerate
|
||
* @param {boolean=} reloop enables iterator to consider the list as an endless loop
|
||
* @return {{add: add, next: (function()), last: (function(): any|null), previous: (function()), count: (function(): number), hasNext: (function(*=): Array.length|*|number|boolean), inRange: (function(*): boolean), remove: remove, contains: (function(*=): *|boolean), itemAt: (function(*=): any|null), findBy: (function(*, *): *[]), hasPrevious: (function(*=): Array.length|*|number|boolean), items: (function(): *[]), indexOf: (function(*=): number), first: (function(): any|null)}}
|
||
* @constructor
|
||
*/
|
||
function MdIterator(items, reloop) {
|
||
var trueFn = function() { return true; };
|
||
|
||
if (items && !angular.isArray(items)) {
|
||
items = Array.prototype.slice.call(items);
|
||
}
|
||
|
||
reloop = !!reloop;
|
||
var _items = items || [];
|
||
|
||
// Published API
|
||
return {
|
||
items: getItems,
|
||
count: count,
|
||
|
||
inRange: inRange,
|
||
contains: contains,
|
||
indexOf: indexOf,
|
||
itemAt: itemAt,
|
||
|
||
findBy: findBy,
|
||
|
||
add: add,
|
||
remove: remove,
|
||
|
||
first: first,
|
||
last: last,
|
||
next: angular.bind(null, findSubsequentItem, false),
|
||
previous: angular.bind(null, findSubsequentItem, true),
|
||
|
||
hasPrevious: hasPrevious,
|
||
hasNext: hasNext
|
||
};
|
||
|
||
/**
|
||
* Publish copy of the enumerable set
|
||
* @returns {Array|*}
|
||
*/
|
||
function getItems() {
|
||
return [].concat(_items);
|
||
}
|
||
|
||
/**
|
||
* Determine length of the list
|
||
* @returns {Array.length|*|number}
|
||
*/
|
||
function count() {
|
||
return _items.length;
|
||
}
|
||
|
||
/**
|
||
* Is the index specified valid
|
||
* @param index
|
||
* @returns {Array.length|*|number|boolean}
|
||
*/
|
||
function inRange(index) {
|
||
return _items.length && (index > -1) && (index < _items.length);
|
||
}
|
||
|
||
/**
|
||
* Can the iterator proceed to the next item in the list; relative to
|
||
* the specified item.
|
||
*
|
||
* @param item
|
||
* @returns {Array.length|*|number|boolean}
|
||
*/
|
||
function hasNext(item) {
|
||
return item ? inRange(indexOf(item) + 1) : false;
|
||
}
|
||
|
||
/**
|
||
* Can the iterator proceed to the previous item in the list; relative to
|
||
* the specified item.
|
||
*
|
||
* @param item
|
||
* @returns {Array.length|*|number|boolean}
|
||
*/
|
||
function hasPrevious(item) {
|
||
return item ? inRange(indexOf(item) - 1) : false;
|
||
}
|
||
|
||
/**
|
||
* Get item at specified index/position
|
||
* @param index
|
||
* @returns {*}
|
||
*/
|
||
function itemAt(index) {
|
||
return inRange(index) ? _items[index] : null;
|
||
}
|
||
|
||
/**
|
||
* Find all elements matching the key/value pair
|
||
* otherwise return null
|
||
*
|
||
* @param val
|
||
* @param key
|
||
*
|
||
* @return array
|
||
*/
|
||
function findBy(key, val) {
|
||
return _items.filter(function(item) {
|
||
return item[key] === val;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Add item to list
|
||
* @param item
|
||
* @param index
|
||
* @returns {*}
|
||
*/
|
||
function add(item, index) {
|
||
if (!item) return -1;
|
||
|
||
if (!angular.isNumber(index)) {
|
||
index = _items.length;
|
||
}
|
||
|
||
_items.splice(index, 0, item);
|
||
|
||
return indexOf(item);
|
||
}
|
||
|
||
/**
|
||
* Remove item from list...
|
||
* @param item
|
||
*/
|
||
function remove(item) {
|
||
if (contains(item)){
|
||
_items.splice(indexOf(item), 1);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get the zero-based index of the target item
|
||
* @param item
|
||
* @returns {*}
|
||
*/
|
||
function indexOf(item) {
|
||
return _items.indexOf(item);
|
||
}
|
||
|
||
/**
|
||
* Boolean existence check
|
||
* @param item
|
||
* @returns {boolean}
|
||
*/
|
||
function contains(item) {
|
||
return item && (indexOf(item) > -1);
|
||
}
|
||
|
||
/**
|
||
* Return first item in the list
|
||
* @returns {*}
|
||
*/
|
||
function first() {
|
||
return _items.length ? _items[0] : null;
|
||
}
|
||
|
||
/**
|
||
* Return last item in the list...
|
||
* @returns {*}
|
||
*/
|
||
function last() {
|
||
return _items.length ? _items[_items.length - 1] : null;
|
||
}
|
||
|
||
/**
|
||
* Find the next item. If reloop is true and at the end of the list, it will go back to the
|
||
* first item. If given, the `validate` callback will be used to determine whether the next item
|
||
* is valid. If not valid, it will try to find the next item again.
|
||
*
|
||
* @param {boolean} backwards Specifies the direction of searching (forwards/backwards)
|
||
* @param {*} item The item whose subsequent item we are looking for
|
||
* @param {Function=} validate The `validate` function
|
||
* @param {integer=} limit The recursion limit
|
||
*
|
||
* @returns {*} The subsequent item or null
|
||
*/
|
||
function findSubsequentItem(backwards, item, validate, limit) {
|
||
validate = validate || trueFn;
|
||
|
||
var curIndex = indexOf(item);
|
||
while (true) {
|
||
if (!inRange(curIndex)) return null;
|
||
|
||
var nextIndex = curIndex + (backwards ? -1 : 1);
|
||
var foundItem = null;
|
||
if (inRange(nextIndex)) {
|
||
foundItem = _items[nextIndex];
|
||
} else if (reloop) {
|
||
foundItem = backwards ? last() : first();
|
||
nextIndex = indexOf(foundItem);
|
||
}
|
||
|
||
if ((foundItem === null) || (nextIndex === limit)) return null;
|
||
if (validate(foundItem)) return foundItem;
|
||
|
||
if (angular.isUndefined(limit)) limit = nextIndex;
|
||
|
||
curIndex = nextIndex;
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
|
||
mdMediaFactory.$inject = ["$mdConstant", "$rootScope", "$window"];angular.module('material.core')
|
||
.factory('$mdMedia', mdMediaFactory);
|
||
|
||
/**
|
||
* @ngdoc service
|
||
* @name $mdMedia
|
||
* @module material.core
|
||
*
|
||
* @description
|
||
* `$mdMedia` is used to evaluate whether a given media query is true or false given the
|
||
* current device's screen / window size. The media query will be re-evaluated on resize, allowing
|
||
* you to register a watch.
|
||
*
|
||
* `$mdMedia` also has pre-programmed support for media queries that match the layout breakpoints:
|
||
*
|
||
* <table class="md-api-table">
|
||
* <thead>
|
||
* <tr>
|
||
* <th>Breakpoint</th>
|
||
* <th>mediaQuery</th>
|
||
* </tr>
|
||
* </thead>
|
||
* <tbody>
|
||
* <tr>
|
||
* <td>xs</td>
|
||
* <td>(max-width: 599px)</td>
|
||
* </tr>
|
||
* <tr>
|
||
* <td>gt-xs</td>
|
||
* <td>(min-width: 600px)</td>
|
||
* </tr>
|
||
* <tr>
|
||
* <td>sm</td>
|
||
* <td>(min-width: 600px) and (max-width: 959px)</td>
|
||
* </tr>
|
||
* <tr>
|
||
* <td>gt-sm</td>
|
||
* <td>(min-width: 960px)</td>
|
||
* </tr>
|
||
* <tr>
|
||
* <td>md</td>
|
||
* <td>(min-width: 960px) and (max-width: 1279px)</td>
|
||
* </tr>
|
||
* <tr>
|
||
* <td>gt-md</td>
|
||
* <td>(min-width: 1280px)</td>
|
||
* </tr>
|
||
* <tr>
|
||
* <td>lg</td>
|
||
* <td>(min-width: 1280px) and (max-width: 1919px)</td>
|
||
* </tr>
|
||
* <tr>
|
||
* <td>gt-lg</td>
|
||
* <td>(min-width: 1920px)</td>
|
||
* </tr>
|
||
* <tr>
|
||
* <td>xl</td>
|
||
* <td>(min-width: 1920px)</td>
|
||
* </tr>
|
||
* <tr>
|
||
* <td>landscape</td>
|
||
* <td>landscape</td>
|
||
* </tr>
|
||
* <tr>
|
||
* <td>portrait</td>
|
||
* <td>portrait</td>
|
||
* </tr>
|
||
* <tr>
|
||
* <td>print</td>
|
||
* <td>print</td>
|
||
* </tr>
|
||
* </tbody>
|
||
* </table>
|
||
*
|
||
* See Material Design's <a href="https://material.google.com/layout/responsive-ui.html">Layout - Adaptive UI</a> for more details.
|
||
*
|
||
* <a href="https://material.io/archive/guidelines/layout/responsive-ui.html#">
|
||
* <img src="https://material-design.storage.googleapis.com/publish/material_v_4/material_ext_publish/0B8olV15J7abPSGFxemFiQVRtb1k/layout_adaptive_breakpoints_01.png" width="100%" height="100%"></img>
|
||
* </a>
|
||
*
|
||
* @returns {boolean} a boolean representing whether or not the given media query is true or false.
|
||
*
|
||
* @usage
|
||
* <hljs lang="js">
|
||
* app.controller('MyController', function($mdMedia, $scope) {
|
||
* $scope.$watch(function() { return $mdMedia('lg'); }, function(big) {
|
||
* $scope.bigScreen = big;
|
||
* });
|
||
*
|
||
* $scope.screenIsSmall = $mdMedia('sm');
|
||
* $scope.customQuery = $mdMedia('(min-width: 1234px)');
|
||
* $scope.anotherCustom = $mdMedia('max-width: 300px');
|
||
* });
|
||
* </hljs>
|
||
*/
|
||
|
||
/* @ngInject */
|
||
function mdMediaFactory($mdConstant, $rootScope, $window) {
|
||
var queries = {};
|
||
var mqls = {};
|
||
var results = {};
|
||
var normalizeCache = {};
|
||
|
||
$mdMedia.getResponsiveAttribute = getResponsiveAttribute;
|
||
$mdMedia.getQuery = getQuery;
|
||
$mdMedia.watchResponsiveAttributes = watchResponsiveAttributes;
|
||
|
||
return $mdMedia;
|
||
|
||
function $mdMedia(query) {
|
||
var validated = queries[query];
|
||
if (angular.isUndefined(validated)) {
|
||
validated = queries[query] = validate(query);
|
||
}
|
||
|
||
var result = results[validated];
|
||
if (angular.isUndefined(result)) {
|
||
result = add(validated);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
function validate(query) {
|
||
return $mdConstant.MEDIA[query] ||
|
||
((query.charAt(0) !== '(') ? ('(' + query + ')') : query);
|
||
}
|
||
|
||
function add(query) {
|
||
var result = mqls[query];
|
||
if (!result) {
|
||
result = mqls[query] = $window.matchMedia(query);
|
||
}
|
||
|
||
result.addListener(onQueryChange);
|
||
return (results[result.media] = !!result.matches);
|
||
}
|
||
|
||
function onQueryChange(query) {
|
||
$rootScope.$evalAsync(function() {
|
||
results[query.media] = !!query.matches;
|
||
});
|
||
}
|
||
|
||
function getQuery(name) {
|
||
return mqls[name];
|
||
}
|
||
|
||
function getResponsiveAttribute(attrs, attrName) {
|
||
for (var i = 0; i < $mdConstant.MEDIA_PRIORITY.length; i++) {
|
||
var mediaName = $mdConstant.MEDIA_PRIORITY[i];
|
||
if (!mqls[queries[mediaName]].matches) {
|
||
continue;
|
||
}
|
||
|
||
var normalizedName = getNormalizedName(attrs, attrName + '-' + mediaName);
|
||
if (attrs[normalizedName]) {
|
||
return attrs[normalizedName];
|
||
}
|
||
}
|
||
|
||
// fallback on unprefixed
|
||
return attrs[getNormalizedName(attrs, attrName)];
|
||
}
|
||
|
||
function watchResponsiveAttributes(attrNames, attrs, watchFn) {
|
||
var unwatchFns = [];
|
||
attrNames.forEach(function(attrName) {
|
||
var normalizedName = getNormalizedName(attrs, attrName);
|
||
if (angular.isDefined(attrs[normalizedName])) {
|
||
unwatchFns.push(
|
||
attrs.$observe(normalizedName, angular.bind(void 0, watchFn, null)));
|
||
}
|
||
|
||
for (var mediaName in $mdConstant.MEDIA) {
|
||
normalizedName = getNormalizedName(attrs, attrName + '-' + mediaName);
|
||
if (angular.isDefined(attrs[normalizedName])) {
|
||
unwatchFns.push(
|
||
attrs.$observe(normalizedName, angular.bind(void 0, watchFn, mediaName)));
|
||
}
|
||
}
|
||
});
|
||
|
||
return function unwatch() {
|
||
unwatchFns.forEach(function(fn) { fn(); });
|
||
};
|
||
}
|
||
|
||
// Improves performance dramatically
|
||
function getNormalizedName(attrs, attrName) {
|
||
return normalizeCache[attrName] ||
|
||
(normalizeCache[attrName] = attrs.$normalize(attrName));
|
||
}
|
||
}
|
||
|
||
})();
|
||
(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;
|
||
}]);
|
||
}]);
|
||
|
||
/**
|
||
* @param {string|string[]} initialAttributes
|
||
* @param {boolean} buildSelector
|
||
* @return {string|string[]|{buildSelector: (function(string|string[]): string),
|
||
* buildList: (function(string|string[]): string[]),
|
||
* hasAttribute: (function(JQLite|Element, string): HTMLElement),
|
||
* removeAttribute: (function(JQLite|Element, string): void)}}
|
||
* @constructor
|
||
*/
|
||
function MdPrefixer(initialAttributes, buildSelector) {
|
||
var PREFIXES = ['data', 'x'];
|
||
|
||
if (initialAttributes) {
|
||
// The prefixer also accepts attributes as a parameter, and immediately builds a list or selector for
|
||
// the specified attributes.
|
||
return buildSelector ? _buildSelector(initialAttributes) : _buildList(initialAttributes);
|
||
}
|
||
|
||
return {
|
||
buildList: _buildList,
|
||
buildSelector: _buildSelector,
|
||
hasAttribute: _hasAttribute,
|
||
removeAttribute: _removeAttribute
|
||
};
|
||
|
||
function _buildList(attributes) {
|
||
attributes = angular.isArray(attributes) ? attributes : [attributes];
|
||
|
||
attributes.forEach(function(item) {
|
||
PREFIXES.forEach(function(prefix) {
|
||
attributes.push(prefix + '-' + item);
|
||
});
|
||
});
|
||
|
||
return attributes;
|
||
}
|
||
|
||
function _buildSelector(attributes) {
|
||
attributes = angular.isArray(attributes) ? attributes : [attributes];
|
||
|
||
return _buildList(attributes)
|
||
.map(function(item) {
|
||
return '[' + item + ']';
|
||
})
|
||
.join(',');
|
||
}
|
||
|
||
function _hasAttribute(element, attribute) {
|
||
element = _getNativeElement(element);
|
||
|
||
if (!element) {
|
||
return false;
|
||
}
|
||
|
||
var prefixedAttrs = _buildList(attribute);
|
||
|
||
for (var i = 0; i < prefixedAttrs.length; i++) {
|
||
if (element.hasAttribute(prefixedAttrs[i])) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
function _removeAttribute(element, attribute) {
|
||
element = _getNativeElement(element);
|
||
|
||
if (!element) {
|
||
return;
|
||
}
|
||
|
||
_buildList(attribute).forEach(function(prefixedAttribute) {
|
||
element.removeAttribute(prefixedAttribute);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Transforms a jqLite or DOM element into a HTML element.
|
||
* This is useful when supporting jqLite elements and DOM elements at
|
||
* same time.
|
||
* @param element {JQLite|Element} Element to be parsed
|
||
* @returns {HTMLElement} Parsed HTMLElement
|
||
*/
|
||
function _getNativeElement(element) {
|
||
element = element[0] || element;
|
||
|
||
if (element.nodeType) {
|
||
return element;
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/*
|
||
* This var has to be outside the angular factory, otherwise when
|
||
* there are multiple material apps on the same page, each app
|
||
* will create its own instance of this array and the app's IDs
|
||
* will not be unique.
|
||
*/
|
||
UtilFactory.$inject = ["$document", "$timeout", "$compile", "$rootScope", "$$mdAnimate", "$interpolate", "$log", "$rootElement", "$window", "$$rAF"];
|
||
var nextUniqueId = 0, isIos, isAndroid, isFirefox;
|
||
|
||
// Support material-tools builds.
|
||
if (window.navigator) {
|
||
var userAgent = window.navigator.userAgent || window.navigator.vendor || window.opera;
|
||
isIos = userAgent.match(/ipad|iphone|ipod/i);
|
||
isAndroid = userAgent.match(/android/i);
|
||
isFirefox = userAgent.match(/(firefox|minefield)/i);
|
||
}
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.core.util
|
||
* @description
|
||
* Util
|
||
*/
|
||
angular
|
||
.module('material.core')
|
||
.factory('$mdUtil', UtilFactory);
|
||
|
||
/**
|
||
* @ngInject
|
||
*/
|
||
function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $interpolate, $log,
|
||
$rootElement, $window, $$rAF) {
|
||
// Setup some core variables for the processTemplate method
|
||
var startSymbol = $interpolate.startSymbol(),
|
||
endSymbol = $interpolate.endSymbol(),
|
||
usesStandardSymbols = ((startSymbol === '{{') && (endSymbol === '}}'));
|
||
|
||
// Polyfill document.contains for IE11.
|
||
document.contains || (document.contains = function (node) {
|
||
return document.body.contains(node);
|
||
});
|
||
|
||
/**
|
||
* Checks if the target element has the requested style by key
|
||
* @param {DOMElement|JQLite} target Target element
|
||
* @param {string} key Style key
|
||
* @param {string=} expectedVal Optional expected value
|
||
* @returns {boolean} Whether the target element has the style or not
|
||
*/
|
||
var hasComputedStyle = function (target, key, expectedVal) {
|
||
var hasValue = false;
|
||
|
||
if (target && target.length) {
|
||
var computedStyles = $window.getComputedStyle(target[0]);
|
||
hasValue = angular.isDefined(computedStyles[key]) &&
|
||
(expectedVal ? computedStyles[key] == expectedVal : true);
|
||
}
|
||
|
||
return hasValue;
|
||
};
|
||
|
||
function validateCssValue(value) {
|
||
return !value ? '0' :
|
||
hasPx(value) || hasPercent(value) ? value : value + 'px';
|
||
}
|
||
|
||
function hasPx(value) {
|
||
return String(value).indexOf('px') > -1;
|
||
}
|
||
|
||
function hasPercent(value) {
|
||
return String(value).indexOf('%') > -1;
|
||
}
|
||
|
||
var $mdUtil = {
|
||
dom: {},
|
||
isIos: isIos,
|
||
isAndroid: isAndroid,
|
||
now: window.performance && window.performance.now ?
|
||
angular.bind(window.performance, window.performance.now) : Date.now || function() {
|
||
return new Date().getTime();
|
||
},
|
||
|
||
/**
|
||
* Cross-version compatibility method to retrieve an option of a ngModel controller,
|
||
* which supports the breaking changes in the AngularJS snapshot (SHA 87a2ff76af5d0a9268d8eb84db5755077d27c84c).
|
||
* @param {!ngModel.NgModelController} ngModelCtrl
|
||
* @param {!string} optionName
|
||
* @returns {string|number|boolean|Object|undefined}
|
||
*/
|
||
getModelOption: function (ngModelCtrl, optionName) {
|
||
if (!ngModelCtrl.$options) {
|
||
return;
|
||
}
|
||
|
||
var $options = ngModelCtrl.$options;
|
||
|
||
// The newer versions of AngularJS introduced a getOption function and made the option values
|
||
// no longer visible on the $options object.
|
||
return $options.getOption ? $options.getOption(optionName) : $options[optionName];
|
||
},
|
||
|
||
/**
|
||
* Determines the current 'dir'ectional value based on the value of 'dir'
|
||
* attribute of the element. If that is not defined, it will try to use
|
||
* a 'dir' attribute of the body or html tag.
|
||
*
|
||
* @param {Object=} attrs a hash object with key-value pairs of normalized
|
||
* attribute names and their corresponding attribute values.
|
||
* @returns {boolean} true if the element's passed in attributes,
|
||
* the document, or the body indicates RTL mode, false otherwise.
|
||
*/
|
||
isRtl: function(attrs) {
|
||
var dir = angular.isDefined(attrs) && attrs.hasOwnProperty('dir') && attrs.dir;
|
||
|
||
switch (dir) {
|
||
case 'ltr':
|
||
return false;
|
||
|
||
case 'rtl':
|
||
return true;
|
||
}
|
||
|
||
return ($document[0].dir === 'rtl' || $document[0].body.dir === 'rtl');
|
||
},
|
||
|
||
/**
|
||
* Bi-directional accessor/mutator used to easily update an element's
|
||
* property based on the current 'dir'ectional value.
|
||
*/
|
||
bidi: function(element, property, lValue, rValue) {
|
||
var ltr = !this.isRtl();
|
||
|
||
// If accessor
|
||
if (arguments.length == 0) return ltr ? 'ltr' : 'rtl';
|
||
|
||
// If mutator
|
||
var elem = angular.element(element);
|
||
|
||
if (ltr && angular.isDefined(lValue)) {
|
||
elem.css(property, validateCssValue(lValue));
|
||
}
|
||
else if (!ltr && angular.isDefined(rValue)) {
|
||
elem.css(property, validateCssValue(rValue));
|
||
}
|
||
},
|
||
|
||
bidiProperty: function (element, lProperty, rProperty, value) {
|
||
var ltr = !this.isRtl();
|
||
|
||
var elem = angular.element(element);
|
||
|
||
if (ltr && angular.isDefined(lProperty)) {
|
||
elem.css(lProperty, validateCssValue(value));
|
||
elem.css(rProperty, '');
|
||
}
|
||
else if (!ltr && angular.isDefined(rProperty)) {
|
||
elem.css(rProperty, validateCssValue(value));
|
||
elem.css(lProperty, '');
|
||
}
|
||
},
|
||
|
||
clientRect: function(element, offsetParent, isOffsetRect) {
|
||
var node = getNode(element);
|
||
offsetParent = getNode(offsetParent || node.offsetParent || document.body);
|
||
var nodeRect = node.getBoundingClientRect();
|
||
|
||
// The user can ask for an offsetRect: a rect relative to the offsetParent,
|
||
// or a clientRect: a rect relative to the page
|
||
var offsetRect = isOffsetRect ?
|
||
offsetParent.getBoundingClientRect() :
|
||
{left: 0, top: 0, width: 0, height: 0};
|
||
return {
|
||
left: nodeRect.left - offsetRect.left,
|
||
top: nodeRect.top - offsetRect.top,
|
||
width: nodeRect.width,
|
||
height: nodeRect.height
|
||
};
|
||
},
|
||
offsetRect: function(element, offsetParent) {
|
||
return $mdUtil.clientRect(element, offsetParent, true);
|
||
},
|
||
|
||
/**
|
||
* Annoying method to copy nodes to an array, thanks to IE.
|
||
* @param nodes
|
||
* @return {Array}
|
||
*/
|
||
nodesToArray: function(nodes) {
|
||
var results = [], i;
|
||
nodes = nodes || [];
|
||
|
||
for (i = 0; i < nodes.length; ++i) {
|
||
results.push(nodes.item(i));
|
||
}
|
||
return results;
|
||
},
|
||
|
||
/**
|
||
* Determines the absolute position of the viewport.
|
||
* Useful when making client rectangles absolute.
|
||
* @returns {number}
|
||
*/
|
||
getViewportTop: function() {
|
||
// If body scrolling is disabled, then use the cached viewport top value, otherwise get it
|
||
// fresh from the $window.
|
||
if ($mdUtil.disableScrollAround._count && $mdUtil.disableScrollAround._viewPortTop) {
|
||
return $mdUtil.disableScrollAround._viewPortTop;
|
||
} else {
|
||
return $window.scrollY || $window.pageYOffset || 0;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Finds the proper focus target by searching the DOM.
|
||
*
|
||
* @param {!JQLite} containerEl
|
||
* @param {string=} attributeVal
|
||
* @returns {JQLite|undefined}
|
||
*/
|
||
findFocusTarget: function(containerEl, attributeVal) {
|
||
var AUTO_FOCUS = this.prefixer('md-autofocus', true);
|
||
var elToFocus;
|
||
|
||
elToFocus = scanForFocusable(containerEl, attributeVal || AUTO_FOCUS);
|
||
|
||
// Scan for fallback to 'universal' API
|
||
if (!elToFocus) {
|
||
elToFocus = scanForFocusable(containerEl, AUTO_FOCUS);
|
||
}
|
||
|
||
return elToFocus;
|
||
|
||
/**
|
||
* Can target and nested children for specified Selector (attribute)
|
||
* whose value may be an expression that evaluates to True/False.
|
||
* @param {!JQLite} target
|
||
* @param {!string} selector
|
||
* @return {JQLite|undefined}
|
||
*/
|
||
function scanForFocusable(target, selector) {
|
||
var elFound, items = target[0].querySelectorAll(selector);
|
||
|
||
// Find the last child element with the focus attribute
|
||
if (items && items.length) {
|
||
items.length && angular.forEach(items, function(it) {
|
||
it = angular.element(it);
|
||
|
||
// Check the element for the md-autofocus class to ensure any associated expression
|
||
// evaluated to true.
|
||
var isFocusable = it.hasClass('md-autofocus');
|
||
if (isFocusable) elFound = it;
|
||
});
|
||
}
|
||
return elFound;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Disables scroll around the passed parent element.
|
||
* @param {Element|JQLite=} element Origin Element (not used)
|
||
* @param {Element|JQLite=} parent Element to disable scrolling within.
|
||
* Defaults to body if none supplied.
|
||
* @param {Object=} options Object of options to modify functionality
|
||
* - disableScrollMask Boolean of whether or not to create a scroll mask element or
|
||
* use the passed parent element.
|
||
*/
|
||
disableScrollAround: function(element, parent, options) {
|
||
options = options || {};
|
||
|
||
$mdUtil.disableScrollAround._count = Math.max(0, $mdUtil.disableScrollAround._count || 0);
|
||
$mdUtil.disableScrollAround._count++;
|
||
|
||
if ($mdUtil.disableScrollAround._restoreScroll) {
|
||
return $mdUtil.disableScrollAround._restoreScroll;
|
||
}
|
||
|
||
var body = $document[0].body;
|
||
var restoreBody = disableBodyScroll();
|
||
var restoreElement = disableElementScroll(parent, options);
|
||
|
||
return $mdUtil.disableScrollAround._restoreScroll = function() {
|
||
if (--$mdUtil.disableScrollAround._count <= 0) {
|
||
delete $mdUtil.disableScrollAround._viewPortTop;
|
||
restoreBody();
|
||
restoreElement();
|
||
delete $mdUtil.disableScrollAround._restoreScroll;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Creates a virtual scrolling mask to prevent touchmove, keyboard, scrollbar clicking,
|
||
* and wheel events.
|
||
* @param {!Element|!JQLite} elementToDisable
|
||
* @param {Object=} scrollMaskOptions Object of options to modify functionality
|
||
* - disableScrollMask Boolean of whether or not to create a scroll mask element or
|
||
* use the passed parent element.
|
||
* @returns {Function}
|
||
*/
|
||
function disableElementScroll(elementToDisable, scrollMaskOptions) {
|
||
var scrollMask;
|
||
var wrappedElementToDisable = angular.element(elementToDisable || body);
|
||
|
||
if (scrollMaskOptions.disableScrollMask) {
|
||
scrollMask = wrappedElementToDisable;
|
||
} else {
|
||
scrollMask = angular.element(
|
||
'<div class="md-scroll-mask">' +
|
||
' <div class="md-scroll-mask-bar"></div>' +
|
||
'</div>');
|
||
wrappedElementToDisable.append(scrollMask);
|
||
}
|
||
|
||
/**
|
||
* @param {Event} $event
|
||
*/
|
||
function preventDefault($event) {
|
||
$event.preventDefault();
|
||
}
|
||
|
||
scrollMask.on('wheel touchmove', preventDefault);
|
||
|
||
return function restoreElementScroll() {
|
||
scrollMask.off('wheel touchmove', preventDefault);
|
||
|
||
if (!scrollMaskOptions.disableScrollMask && scrollMask[0].parentNode) {
|
||
scrollMask[0].parentNode.removeChild(scrollMask[0]);
|
||
}
|
||
};
|
||
}
|
||
|
||
// Converts the body to a position fixed block and translate it to the proper scroll position
|
||
function disableBodyScroll() {
|
||
var documentElement = $document[0].documentElement;
|
||
|
||
var prevDocumentStyle = documentElement.style.cssText || '';
|
||
var prevBodyStyle = body.style.cssText || '';
|
||
|
||
var viewportTop = $mdUtil.getViewportTop();
|
||
$mdUtil.disableScrollAround._viewPortTop = viewportTop;
|
||
var clientWidth = body.clientWidth;
|
||
var hasVerticalScrollbar = body.scrollHeight > body.clientHeight + 1;
|
||
|
||
// Scroll may be set on <html> element (for example by overflow-y: scroll)
|
||
// but Chrome is reporting the scrollTop position always on <body>.
|
||
// scrollElement will allow to restore the scrollTop position to proper target.
|
||
var scrollElement = documentElement.scrollTop > 0 ? documentElement : body;
|
||
|
||
if (hasVerticalScrollbar) {
|
||
angular.element(body).css({
|
||
position: 'fixed',
|
||
width: '100%',
|
||
top: -viewportTop + 'px'
|
||
});
|
||
}
|
||
|
||
if (body.clientWidth < clientWidth) {
|
||
body.style.overflow = 'hidden';
|
||
}
|
||
|
||
return function restoreScroll() {
|
||
// Reset the inline style CSS to the previous.
|
||
body.style.cssText = prevBodyStyle;
|
||
documentElement.style.cssText = prevDocumentStyle;
|
||
|
||
// The scroll position while being fixed
|
||
scrollElement.scrollTop = viewportTop;
|
||
};
|
||
}
|
||
|
||
},
|
||
|
||
enableScrolling: function() {
|
||
var restoreFn = this.disableScrollAround._restoreScroll;
|
||
restoreFn && restoreFn();
|
||
},
|
||
|
||
floatingScrollbars: function() {
|
||
if (this.floatingScrollbars.cached === undefined) {
|
||
var tempNode = angular.element('<div><div></div></div>').css({
|
||
width: '100%',
|
||
'z-index': -1,
|
||
position: 'absolute',
|
||
height: '35px',
|
||
'overflow-y': 'scroll'
|
||
});
|
||
tempNode.children().css('height', '60px');
|
||
|
||
$document[0].body.appendChild(tempNode[0]);
|
||
this.floatingScrollbars.cached =
|
||
(tempNode[0].offsetWidth === tempNode[0].childNodes[0].offsetWidth);
|
||
tempNode.remove();
|
||
}
|
||
return this.floatingScrollbars.cached;
|
||
},
|
||
|
||
/**
|
||
* Mobile safari only allows you to set focus in click event listeners.
|
||
* @param {Element|JQLite} element to focus
|
||
*/
|
||
forceFocus: function(element) {
|
||
var node = element[0] || element;
|
||
|
||
document.addEventListener('click', function focusOnClick(ev) {
|
||
if (ev.target === node && ev.$focus) {
|
||
node.focus();
|
||
ev.stopImmediatePropagation();
|
||
ev.preventDefault();
|
||
node.removeEventListener('click', focusOnClick);
|
||
}
|
||
}, true);
|
||
|
||
var newEvent = document.createEvent('MouseEvents');
|
||
newEvent.initMouseEvent('click', false, true, window, {}, 0, 0, 0, 0,
|
||
false, false, false, false, 0, null);
|
||
newEvent.$material = true;
|
||
newEvent.$focus = true;
|
||
node.dispatchEvent(newEvent);
|
||
},
|
||
|
||
/**
|
||
* facade to build md-backdrop element with desired styles
|
||
* NOTE: Use $compile to trigger backdrop postLink function
|
||
*/
|
||
createBackdrop: function(scope, addClass) {
|
||
return $compile($mdUtil.supplant('<md-backdrop class="{0}">', [addClass]))(scope);
|
||
},
|
||
|
||
/**
|
||
* supplant() method from Crockford's `Remedial Javascript`
|
||
* Equivalent to use of $interpolate; without dependency on
|
||
* interpolation symbols and scope. Note: the '{<token>}' can
|
||
* be property names, property chains, or array indices.
|
||
*/
|
||
supplant: function(template, values, pattern) {
|
||
pattern = pattern || /\{([^{}]*)\}/g;
|
||
return template.replace(pattern, function(a, b) {
|
||
var p = b.split('.'),
|
||
r = values;
|
||
try {
|
||
for (var s in p) {
|
||
if (p.hasOwnProperty(s)) {
|
||
r = r[p[s]];
|
||
}
|
||
}
|
||
} catch (e) {
|
||
r = a;
|
||
}
|
||
return (typeof r === 'string' || typeof r === 'number') ? r : a;
|
||
});
|
||
},
|
||
|
||
fakeNgModel: function() {
|
||
return {
|
||
$fake: true,
|
||
$setTouched: angular.noop,
|
||
$setViewValue: function(value) {
|
||
this.$viewValue = value;
|
||
this.$render(value);
|
||
this.$viewChangeListeners.forEach(function(cb) {
|
||
cb();
|
||
});
|
||
},
|
||
$isEmpty: function(value) {
|
||
return ('' + value).length === 0;
|
||
},
|
||
$parsers: [],
|
||
$formatters: [],
|
||
$viewChangeListeners: [],
|
||
$render: angular.noop
|
||
};
|
||
},
|
||
|
||
/**
|
||
* @param {Function} func original function to be debounced
|
||
* @param {number} wait number of milliseconds to delay (since last debounce reset).
|
||
* Default value 10 msecs.
|
||
* @param {Object} scope in which to apply the function after debouncing ends
|
||
* @param {boolean} invokeApply should the $timeout trigger $digest() dirty checking
|
||
* @return {Function} A function, that, as long as it continues to be invoked, will not be
|
||
* triggered. The function will be called after it stops being called for N milliseconds.
|
||
*/
|
||
debounce: function(func, wait, scope, invokeApply) {
|
||
var timer;
|
||
|
||
return function debounced() {
|
||
var context = scope,
|
||
args = Array.prototype.slice.call(arguments);
|
||
|
||
$timeout.cancel(timer);
|
||
timer = $timeout(function() {
|
||
|
||
timer = undefined;
|
||
func.apply(context, args);
|
||
|
||
}, wait || 10, invokeApply);
|
||
};
|
||
},
|
||
|
||
/**
|
||
* The function will not be called unless it has been more than `delay` milliseconds since the
|
||
* last call.
|
||
* @param {Function} func original function to throttle
|
||
* @param {number} delay number of milliseconds to delay
|
||
* @return {Function} a function that can only be triggered every `delay` milliseconds.
|
||
*/
|
||
throttle: function throttle(func, delay) {
|
||
var recent;
|
||
return function throttled() {
|
||
var context = this;
|
||
var args = arguments;
|
||
var now = $mdUtil.now();
|
||
|
||
if (!recent || (now - recent > delay)) {
|
||
func.apply(context, args);
|
||
recent = now;
|
||
}
|
||
};
|
||
},
|
||
|
||
/**
|
||
* Measures the number of milliseconds taken to run the provided callback
|
||
* function. Uses a high-precision timer if available.
|
||
*/
|
||
time: function time(cb) {
|
||
var start = $mdUtil.now();
|
||
cb();
|
||
return $mdUtil.now() - start;
|
||
},
|
||
|
||
/**
|
||
* Create an implicit getter that caches its `getter()`
|
||
* lookup value
|
||
*/
|
||
valueOnUse : function (scope, key, getter) {
|
||
var value = null, args = Array.prototype.slice.call(arguments);
|
||
var params = (args.length > 3) ? args.slice(3) : [];
|
||
|
||
Object.defineProperty(scope, key, {
|
||
get: function () {
|
||
if (value === null) value = getter.apply(scope, params);
|
||
return value;
|
||
}
|
||
});
|
||
},
|
||
|
||
/**
|
||
* Get a unique ID.
|
||
*
|
||
* @returns {string} an unique numeric string
|
||
*/
|
||
nextUid: function() {
|
||
return '' + nextUniqueId++;
|
||
},
|
||
|
||
/**
|
||
* Stop watchers and events from firing on a scope without destroying it,
|
||
* by disconnecting it from its parent and its siblings' linked lists.
|
||
* @param {Object} scope to disconnect
|
||
*/
|
||
disconnectScope: function disconnectScope(scope) {
|
||
if (!scope) return;
|
||
|
||
// we can't destroy the root scope or a scope that has been already destroyed
|
||
if (scope.$root === scope) return;
|
||
if (scope.$$destroyed) return;
|
||
|
||
var parent = scope.$parent;
|
||
scope.$$disconnected = true;
|
||
|
||
// See Scope.$destroy
|
||
if (parent.$$childHead === scope) parent.$$childHead = scope.$$nextSibling;
|
||
if (parent.$$childTail === scope) parent.$$childTail = scope.$$prevSibling;
|
||
if (scope.$$prevSibling) scope.$$prevSibling.$$nextSibling = scope.$$nextSibling;
|
||
if (scope.$$nextSibling) scope.$$nextSibling.$$prevSibling = scope.$$prevSibling;
|
||
|
||
scope.$$nextSibling = scope.$$prevSibling = null;
|
||
|
||
},
|
||
|
||
/**
|
||
* Undo the effects of disconnectScope().
|
||
* @param {Object} scope to reconnect
|
||
*/
|
||
reconnectScope: function reconnectScope(scope) {
|
||
if (!scope) return;
|
||
|
||
// we can't disconnect the root node or scope already disconnected
|
||
if (scope.$root === scope) return;
|
||
if (!scope.$$disconnected) return;
|
||
|
||
var child = scope;
|
||
|
||
var parent = child.$parent;
|
||
child.$$disconnected = false;
|
||
// See Scope.$new for this logic...
|
||
child.$$prevSibling = parent.$$childTail;
|
||
if (parent.$$childHead) {
|
||
parent.$$childTail.$$nextSibling = child;
|
||
parent.$$childTail = child;
|
||
} else {
|
||
parent.$$childHead = parent.$$childTail = child;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Get an element's siblings matching a given tag name.
|
||
*
|
||
* @param {JQLite|angular.element|HTMLElement} element Element to start walking the DOM from
|
||
* @param {string} tagName HTML tag name to match against
|
||
* @returns {Object[]} JQLite
|
||
*/
|
||
getSiblings: function getSiblings(element, tagName) {
|
||
var upperCasedTagName = tagName.toUpperCase();
|
||
if (element instanceof angular.element) {
|
||
element = element[0];
|
||
}
|
||
var siblings = Array.prototype.filter.call(element.parentNode.children, function(node) {
|
||
return element !== node && node.tagName.toUpperCase() === upperCasedTagName;
|
||
});
|
||
return siblings.map(function (sibling) {
|
||
return angular.element(sibling);
|
||
});
|
||
},
|
||
|
||
/**
|
||
* getClosest replicates jQuery.closest() to walk up the DOM tree until it finds a matching
|
||
* nodeName.
|
||
*
|
||
* @param {Node} el Element to start walking the DOM from
|
||
* @param {string|function} validateWith If a string is passed, it will be evaluated against
|
||
* each of the parent nodes' tag name. If a function is passed, the loop will call it with each
|
||
* of the parents and will use the return value to determine whether the node is a match.
|
||
* @param {boolean=} onlyParent Only start checking from the parent element, not `el`.
|
||
* @returns {Node|null} closest matching parent Node or null if not found
|
||
*/
|
||
getClosest: function getClosest(el, validateWith, onlyParent) {
|
||
if (angular.isString(validateWith)) {
|
||
var tagName = validateWith.toUpperCase();
|
||
validateWith = function(el) {
|
||
return el.nodeName.toUpperCase() === tagName;
|
||
};
|
||
}
|
||
|
||
if (el instanceof angular.element) el = el[0];
|
||
if (onlyParent) el = el.parentNode;
|
||
if (!el) return null;
|
||
|
||
do {
|
||
if (validateWith(el)) {
|
||
return el;
|
||
}
|
||
} while (el = el.parentNode);
|
||
|
||
return null;
|
||
},
|
||
|
||
/**
|
||
* Build polyfill for the Node.contains feature (if needed)
|
||
* @param {Node} node
|
||
* @param {Node} child
|
||
* @returns {Node}
|
||
*/
|
||
elementContains: function(node, child) {
|
||
var hasContains = (window.Node && window.Node.prototype && Node.prototype.contains);
|
||
var findFn = hasContains ? angular.bind(node, node.contains) : angular.bind(node, function(arg) {
|
||
// compares the positions of two nodes and returns a bitmask
|
||
return (node === child) || !!(this.compareDocumentPosition(arg) & 16);
|
||
});
|
||
|
||
return findFn(child);
|
||
},
|
||
|
||
/**
|
||
* Functional equivalent for $element.filter(‘md-bottom-sheet’)
|
||
* useful with interimElements where the element and its container are important...
|
||
*
|
||
* @param {JQLite} element to scan
|
||
* @param {string} nodeName of node to find (e.g. 'md-dialog')
|
||
* @param {boolean=} scanDeep optional flag to allow deep scans; defaults to 'false'.
|
||
* @param {boolean=} warnNotFound optional flag to enable log warnings; defaults to false
|
||
*/
|
||
extractElementByName: function(element, nodeName, scanDeep, warnNotFound) {
|
||
var found = scanTree(element);
|
||
if (!found && !!warnNotFound) {
|
||
$log.warn($mdUtil.supplant("Unable to find node '{0}' in element '{1}'.",[nodeName, element[0].outerHTML]));
|
||
}
|
||
|
||
return angular.element(found || element);
|
||
|
||
/**
|
||
* Breadth-First tree scan for element with matching `nodeName`
|
||
*/
|
||
function scanTree(element) {
|
||
return scanLevel(element) || (scanDeep ? scanChildren(element) : null);
|
||
}
|
||
|
||
/**
|
||
* Case-insensitive scan of current elements only (do not descend).
|
||
*/
|
||
function scanLevel(element) {
|
||
if (element) {
|
||
for (var i = 0, len = element.length; i < len; i++) {
|
||
if (element[i].nodeName.toLowerCase() === nodeName) {
|
||
return element[i];
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Scan children of specified node
|
||
*/
|
||
function scanChildren(element) {
|
||
var found;
|
||
if (element) {
|
||
for (var i = 0, len = element.length; i < len; i++) {
|
||
var target = element[i];
|
||
if (!found) {
|
||
for (var j = 0, numChild = target.childNodes.length; j < numChild; j++) {
|
||
found = found || scanTree([target.childNodes[j]]);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return found;
|
||
}
|
||
|
||
},
|
||
|
||
/**
|
||
* Give optional properties with no value a boolean true if attr provided or false otherwise
|
||
*/
|
||
initOptionalProperties: function(scope, attr, defaults) {
|
||
defaults = defaults || {};
|
||
angular.forEach(scope.$$isolateBindings, function(binding, key) {
|
||
if (binding.optional && angular.isUndefined(scope[key])) {
|
||
var attrIsDefined = angular.isDefined(attr[binding.attrName]);
|
||
scope[key] = angular.isDefined(defaults[key]) ? defaults[key] : attrIsDefined;
|
||
}
|
||
});
|
||
},
|
||
|
||
/**
|
||
* Alternative to $timeout calls with 0 delay.
|
||
* nextTick() coalesces all calls within a single frame
|
||
* to minimize $digest thrashing
|
||
*
|
||
* @param {Function} callback function to be called after the tick
|
||
* @param {boolean=} digest true to call $rootScope.$digest() after callback
|
||
* @param {Object=} scope associated with callback. If the scope is destroyed, the callback will
|
||
* be skipped.
|
||
* @returns {*}
|
||
*/
|
||
nextTick: function(callback, digest, scope) {
|
||
// grab function reference for storing state details
|
||
var nextTick = $mdUtil.nextTick;
|
||
var timeout = nextTick.timeout;
|
||
var queue = nextTick.queue || [];
|
||
|
||
// add callback to the queue
|
||
queue.push({scope: scope, callback: callback});
|
||
|
||
// set default value for digest
|
||
if (digest == null) digest = true;
|
||
|
||
// store updated digest/queue values
|
||
nextTick.digest = nextTick.digest || digest;
|
||
nextTick.queue = queue;
|
||
|
||
// either return existing timeout or create a new one
|
||
return timeout || (nextTick.timeout = $timeout(processQueue, 0, false));
|
||
|
||
/**
|
||
* Grab a copy of the current queue
|
||
* Clear the queue for future use
|
||
* Process the existing queue
|
||
* Trigger digest if necessary
|
||
*/
|
||
function processQueue() {
|
||
var queue = nextTick.queue;
|
||
var digest = nextTick.digest;
|
||
|
||
nextTick.queue = [];
|
||
nextTick.timeout = null;
|
||
nextTick.digest = false;
|
||
|
||
queue.forEach(function(queueItem) {
|
||
var skip = queueItem.scope && queueItem.scope.$$destroyed;
|
||
if (!skip) {
|
||
queueItem.callback();
|
||
}
|
||
});
|
||
|
||
if (digest) $rootScope.$digest();
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Processes a template and replaces the start/end symbols if the application has
|
||
* overridden them.
|
||
*
|
||
* @param template The template to process whose start/end tags may be replaced.
|
||
* @returns {*}
|
||
*/
|
||
processTemplate: function(template) {
|
||
if (usesStandardSymbols) {
|
||
return template;
|
||
} else {
|
||
if (!template || !angular.isString(template)) return template;
|
||
return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol);
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Scan up dom hierarchy for enabled parent;
|
||
*/
|
||
getParentWithPointerEvents: function (element) {
|
||
var parent = element.parent();
|
||
|
||
// jqLite might return a non-null, but still empty, parent; so check for parent and length
|
||
while (hasComputedStyle(parent, 'pointer-events', 'none')) {
|
||
parent = parent.parent();
|
||
}
|
||
|
||
return parent;
|
||
},
|
||
|
||
getNearestContentElement: function (element) {
|
||
var current = element.parent()[0];
|
||
// Look for the nearest parent md-content, stopping at the rootElement.
|
||
while (current && current !== $rootElement[0] && current !== document.body && current.nodeName.toUpperCase() !== 'MD-CONTENT') {
|
||
current = current.parentNode;
|
||
}
|
||
return current;
|
||
},
|
||
|
||
/**
|
||
* Checks if the current browser is natively supporting the `sticky` position.
|
||
* @returns {string} supported sticky property name
|
||
*/
|
||
checkStickySupport: function() {
|
||
var stickyProp;
|
||
var testEl = angular.element('<div>');
|
||
$document[0].body.appendChild(testEl[0]);
|
||
|
||
var stickyProps = ['sticky', '-webkit-sticky'];
|
||
for (var i = 0; i < stickyProps.length; ++i) {
|
||
testEl.css({
|
||
position: stickyProps[i],
|
||
top: 0,
|
||
'z-index': 2
|
||
});
|
||
|
||
if (testEl.css('position') == stickyProps[i]) {
|
||
stickyProp = stickyProps[i];
|
||
break;
|
||
}
|
||
}
|
||
|
||
testEl.remove();
|
||
|
||
return stickyProp;
|
||
},
|
||
|
||
/**
|
||
* Parses an attribute value, mostly a string.
|
||
* By default checks for negated values and returns `false´ if present.
|
||
* Negated values are: (native falsy) and negative strings like:
|
||
* `false` or `0`.
|
||
* @param value Attribute value which should be parsed.
|
||
* @param negatedCheck When set to false, won't check for negated values.
|
||
* @returns {boolean}
|
||
*/
|
||
parseAttributeBoolean: function(value, negatedCheck) {
|
||
return value === '' || !!value && (negatedCheck === false || value !== 'false' && value !== '0');
|
||
},
|
||
|
||
hasComputedStyle: hasComputedStyle,
|
||
|
||
/**
|
||
* Returns true if the parent form of the element has been submitted.
|
||
* @param element An AngularJS or HTML5 element.
|
||
* @returns {boolean}
|
||
*/
|
||
isParentFormSubmitted: function(element) {
|
||
var parent = $mdUtil.getClosest(element, 'form');
|
||
var form = parent ? angular.element(parent).controller('form') : null;
|
||
|
||
return form ? form.$submitted : false;
|
||
},
|
||
|
||
/**
|
||
* Animate the requested element's scrollTop to the requested scrollPosition with basic easing.
|
||
* @param {!Element} element The element to scroll.
|
||
* @param {number} scrollEnd The new/final scroll position.
|
||
* @param {number=} duration Duration of the scroll. Default is 1000ms.
|
||
*/
|
||
animateScrollTo: function(element, scrollEnd, duration) {
|
||
var scrollStart = element.scrollTop;
|
||
var scrollChange = scrollEnd - scrollStart;
|
||
var scrollingDown = scrollStart < scrollEnd;
|
||
var startTime = $mdUtil.now();
|
||
|
||
$$rAF(scrollChunk);
|
||
|
||
function scrollChunk() {
|
||
var newPosition = calculateNewPosition();
|
||
|
||
element.scrollTop = newPosition;
|
||
|
||
if (scrollingDown ? newPosition < scrollEnd : newPosition > scrollEnd) {
|
||
$$rAF(scrollChunk);
|
||
}
|
||
}
|
||
|
||
function calculateNewPosition() {
|
||
var easeDuration = duration || 1000;
|
||
var currentTime = $mdUtil.now() - startTime;
|
||
|
||
return ease(currentTime, scrollStart, scrollChange, easeDuration);
|
||
}
|
||
|
||
function ease(currentTime, start, change, duration) {
|
||
// If the duration has passed (which can occur if our app loses focus due to $$rAF), jump
|
||
// straight to the proper position
|
||
if (currentTime > duration) {
|
||
return start + change;
|
||
}
|
||
|
||
var ts = (currentTime /= duration) * currentTime;
|
||
var tc = ts * currentTime;
|
||
|
||
return start + change * (-2 * tc + 3 * ts);
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Provides an easy mechanism for removing duplicates from an array.
|
||
*
|
||
* var myArray = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4];
|
||
*
|
||
* $mdUtil.uniq(myArray) => [1, 2, 3, 4]
|
||
*
|
||
* @param {Array} array The array whose unique values should be returned.
|
||
* @returns {Array|void} A copy of the array containing only unique values.
|
||
*/
|
||
uniq: function(array) {
|
||
if (!array) { return; }
|
||
|
||
return array.filter(function(value, index, self) {
|
||
return self.indexOf(value) === index;
|
||
});
|
||
},
|
||
|
||
/**
|
||
* Gets the inner HTML content of the given HTMLElement.
|
||
* Only intended for use with SVG or Symbol elements in IE11.
|
||
* @param {Element} element
|
||
* @returns {string} the inner HTML of the element passed in
|
||
*/
|
||
getInnerHTML: function(element) {
|
||
// For SVG or Symbol elements, innerHTML returns `undefined` in IE.
|
||
// Reference: https://stackoverflow.com/q/28129956/633107
|
||
// The XMLSerializer API is supported on IE11 and is the recommended workaround.
|
||
var serializer = new XMLSerializer();
|
||
|
||
return Array.prototype.map.call(element.childNodes, function (child) {
|
||
return serializer.serializeToString(child);
|
||
}).join('');
|
||
},
|
||
|
||
/**
|
||
* Gets the outer HTML content of the given HTMLElement.
|
||
* Only intended for use with SVG or Symbol elements in IE11.
|
||
* @param {Element} element
|
||
* @returns {string} the outer HTML of the element passed in
|
||
*/
|
||
getOuterHTML: function(element) {
|
||
// For SVG or Symbol elements, outerHTML returns `undefined` in IE.
|
||
// Reference: https://stackoverflow.com/q/29888050/633107
|
||
// The XMLSerializer API is supported on IE11 and is the recommended workaround.
|
||
var serializer = new XMLSerializer();
|
||
return serializer.serializeToString(element);
|
||
},
|
||
|
||
/**
|
||
* Support: IE 9-11 only
|
||
* documentMode is an IE-only property
|
||
* http://msdn.microsoft.com/en-us/library/ie/cc196988(v=vs.85).aspx
|
||
*/
|
||
msie: window.document.documentMode,
|
||
|
||
getTouchAction: function() {
|
||
var testEl = document.createElement('div');
|
||
var vendorPrefixes = ['', 'webkit', 'Moz', 'MS', 'ms', 'o'];
|
||
|
||
for (var i = 0; i < vendorPrefixes.length; i++) {
|
||
var prefix = vendorPrefixes[i];
|
||
var property = prefix ? prefix + 'TouchAction' : 'touchAction';
|
||
if (angular.isDefined(testEl.style[property])) {
|
||
return property;
|
||
}
|
||
}
|
||
},
|
||
|
||
/**
|
||
* @param {Event} event the event to calculate the bubble path for
|
||
* @return {EventTarget[]} the set of nodes that this event could bubble up to
|
||
*/
|
||
getEventPath: function(event) {
|
||
var path = [];
|
||
var currentTarget = event.target;
|
||
while (currentTarget) {
|
||
path.push(currentTarget);
|
||
currentTarget = currentTarget.parentElement;
|
||
}
|
||
if (path.indexOf(window) === -1 && path.indexOf(document) === -1)
|
||
path.push(document);
|
||
if (path.indexOf(window) === -1)
|
||
path.push(window);
|
||
return path;
|
||
},
|
||
|
||
/**
|
||
* Gets the string the user has entered and removes Regex identifiers
|
||
* @param {string} term
|
||
* @returns {string} sanitized string
|
||
*/
|
||
sanitize: function(term) {
|
||
if (!term) return term;
|
||
return term.replace(/[\\^$*+?.()|{}[]/g, '\\$&');
|
||
},
|
||
|
||
/**********************************************************************************************
|
||
* The following functions were sourced from
|
||
* https://github.com/angular/components/blob/3c37e4b1c1cb74a3d0a90d173240fc730d21d9d4/src/cdk/a11y/interactivity-checker/interactivity-checker.ts
|
||
**********************************************************************************************/
|
||
|
||
/**
|
||
* Gets whether an element is disabled.
|
||
* @param {HTMLElement} element Element to be checked.
|
||
* @returns {boolean} Whether the element is disabled.
|
||
*/
|
||
isDisabled: function(element) {
|
||
// This does not capture some cases, such as a non-form control with a disabled attribute or
|
||
// a form control inside of a disabled form, but should capture the most common cases.
|
||
return element.hasAttribute('disabled');
|
||
},
|
||
|
||
/**
|
||
* Gets whether an element is visible for the purposes of interactivity.
|
||
*
|
||
* This will capture states like `display: none` and `visibility: hidden`, but not things like
|
||
* being clipped by an `overflow: hidden` parent or being outside the viewport.
|
||
*
|
||
* @param {HTMLElement} element
|
||
* @returns {boolean} Whether the element is visible.
|
||
*/
|
||
isVisible: function(element) {
|
||
return $mdUtil.hasGeometry(element) && getComputedStyle(element).visibility === 'visible';
|
||
},
|
||
|
||
/**
|
||
* Gets whether an element can be reached via Tab key.
|
||
* Assumes that the element has already been checked with isFocusable.
|
||
* @param {HTMLElement} element Element to be checked.
|
||
* @returns {boolean} Whether the element is tabbable.
|
||
*/
|
||
isTabbable: function(element) {
|
||
var frameElement = $mdUtil.getFrameElement($mdUtil.getWindow(element));
|
||
|
||
if (frameElement) {
|
||
// Frame elements inherit their tabindex onto all child elements.
|
||
if ($mdUtil.getTabIndexValue(frameElement) === -1) {
|
||
return false;
|
||
}
|
||
|
||
// Browsers disable tabbing to an element inside of an invisible frame.
|
||
if (!$mdUtil.isVisible(frameElement)) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
var nodeName = element.nodeName.toLowerCase();
|
||
var tabIndexValue = $mdUtil.getTabIndexValue(element);
|
||
|
||
if (element.hasAttribute('contenteditable')) {
|
||
return tabIndexValue !== -1;
|
||
}
|
||
|
||
if (nodeName === 'iframe' || nodeName === 'object') {
|
||
// The frame or object's content may be tabbable depending on the content, but it's
|
||
// not possibly to reliably detect the content of the frames. We always consider such
|
||
// elements as non-tabbable.
|
||
return false;
|
||
}
|
||
|
||
// In iOS, the browser only considers some specific elements as tabbable.
|
||
if (isIos && !$mdUtil.isPotentiallyTabbableIOS(element)) {
|
||
return false;
|
||
}
|
||
|
||
if (nodeName === 'audio') {
|
||
// Audio elements without controls enabled are never tabbable, regardless
|
||
// of the tabindex attribute explicitly being set.
|
||
if (!element.hasAttribute('controls')) {
|
||
return false;
|
||
}
|
||
// Audio elements with controls are by default tabbable unless the
|
||
// tabindex attribute is set to `-1` explicitly.
|
||
return tabIndexValue !== -1;
|
||
}
|
||
|
||
if (nodeName === 'video') {
|
||
// For all video elements, if the tabindex attribute is set to `-1`, the video
|
||
// is not tabbable. Note: We cannot rely on the default `HTMLElement.tabIndex`
|
||
// property as that one is set to `-1` in Chrome, Edge and Safari v13.1. The
|
||
// tabindex attribute is the source of truth here.
|
||
if (tabIndexValue === -1) {
|
||
return false;
|
||
}
|
||
// If the tabindex is explicitly set, and not `-1` (as per check before), the
|
||
// video element is always tabbable (regardless of whether it has controls or not).
|
||
if (tabIndexValue !== null) {
|
||
return true;
|
||
}
|
||
// Otherwise (when no explicit tabindex is set), a video is only tabbable if it
|
||
// has controls enabled. Firefox is special as videos are always tabbable regardless
|
||
// of whether there are controls or not.
|
||
return isFirefox || element.hasAttribute('controls');
|
||
}
|
||
|
||
return element.tabIndex >= 0;
|
||
},
|
||
|
||
/**
|
||
* Gets whether an element can be focused by the user.
|
||
* @param {HTMLElement} element Element to be checked.
|
||
* @returns {boolean} Whether the element is focusable.
|
||
*/
|
||
isFocusable: function(element) {
|
||
// Perform checks in order of left to most expensive.
|
||
// Again, naive approach that does not capture many edge cases and browser quirks.
|
||
return $mdUtil.isPotentiallyFocusable(element) && !$mdUtil.isDisabled(element) &&
|
||
$mdUtil.isVisible(element);
|
||
},
|
||
|
||
/**
|
||
* Gets whether an element is potentially focusable without taking current visible/disabled
|
||
* state into account.
|
||
* @param {HTMLElement} element
|
||
* @returns {boolean}
|
||
*/
|
||
isPotentiallyFocusable: function(element) {
|
||
// Inputs are potentially focusable *unless* they're type="hidden".
|
||
if ($mdUtil.isHiddenInput(element)) {
|
||
return false;
|
||
}
|
||
|
||
return $mdUtil.isNativeFormElement(element) ||
|
||
$mdUtil.isAnchorWithHref(element) ||
|
||
element.hasAttribute('contenteditable') ||
|
||
$mdUtil.hasValidTabIndex(element);
|
||
},
|
||
|
||
/**
|
||
* Checks whether the specified element is potentially tabbable on iOS.
|
||
* @param {HTMLElement} element
|
||
* @returns {boolean}
|
||
*/
|
||
isPotentiallyTabbableIOS: function(element) {
|
||
var nodeName = element.nodeName.toLowerCase();
|
||
var inputType = nodeName === 'input' && element.type;
|
||
|
||
return inputType === 'text'
|
||
|| inputType === 'password'
|
||
|| nodeName === 'select'
|
||
|| nodeName === 'textarea';
|
||
},
|
||
|
||
/**
|
||
* Returns the parsed tabindex from the element attributes instead of returning the
|
||
* evaluated tabindex from the browsers defaults.
|
||
* @param {HTMLElement} element
|
||
* @returns {null|number}
|
||
*/
|
||
getTabIndexValue: function(element) {
|
||
if (!$mdUtil.hasValidTabIndex(element)) {
|
||
return null;
|
||
}
|
||
|
||
// See browser issue in Gecko https://bugzilla.mozilla.org/show_bug.cgi?id=1128054
|
||
var tabIndex = parseInt(element.getAttribute('tabindex') || '', 10);
|
||
|
||
return isNaN(tabIndex) ? -1 : tabIndex;
|
||
},
|
||
|
||
/**
|
||
* Gets whether an element has a valid tabindex.
|
||
* @param {HTMLElement} element
|
||
* @returns {boolean}
|
||
*/
|
||
hasValidTabIndex: function(element) {
|
||
if (!element.hasAttribute('tabindex') || element.tabIndex === undefined) {
|
||
return false;
|
||
}
|
||
|
||
var tabIndex = element.getAttribute('tabindex');
|
||
|
||
// IE11 parses tabindex="" as the value "-32768"
|
||
if (tabIndex == '-32768') {
|
||
return false;
|
||
}
|
||
|
||
return !!(tabIndex && !isNaN(parseInt(tabIndex, 10)));
|
||
},
|
||
|
||
/**
|
||
* Checks whether the specified element has any geometry / rectangles.
|
||
* @param {HTMLElement} element
|
||
* @returns {boolean}
|
||
*/
|
||
hasGeometry: function(element) {
|
||
// Use logic from jQuery to check for an invisible element.
|
||
// See https://github.com/jquery/jquery/blob/8969732518470a7f8e654d5bc5be0b0076cb0b87/src/css/hiddenVisibleSelectors.js#L9
|
||
return !!(element.offsetWidth || element.offsetHeight ||
|
||
(typeof element.getClientRects === 'function' && element.getClientRects().length));
|
||
},
|
||
|
||
/**
|
||
* Returns the frame element from a window object. Since browsers like MS Edge throw errors if
|
||
* the frameElement property is being accessed from a different host address, this property
|
||
* should be accessed carefully.
|
||
* @param {Window} window
|
||
* @returns {null|HTMLElement}
|
||
*/
|
||
getFrameElement: function(window) {
|
||
try {
|
||
return window.frameElement;
|
||
} catch (error) {
|
||
return null;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Gets the parent window of a DOM node with regards of being inside of an iframe.
|
||
* @param {HTMLElement} node
|
||
* @returns {Window}
|
||
*/
|
||
getWindow: function(node) {
|
||
// ownerDocument is null if `node` itself *is* a document.
|
||
return node.ownerDocument && node.ownerDocument.defaultView || window;
|
||
},
|
||
|
||
/**
|
||
* Gets whether an element's
|
||
* @param {Node} element
|
||
* @returns {boolean}
|
||
*/
|
||
isNativeFormElement: function(element) {
|
||
var nodeName = element.nodeName.toLowerCase();
|
||
return nodeName === 'input' ||
|
||
nodeName === 'select' ||
|
||
nodeName === 'button' ||
|
||
nodeName === 'textarea';
|
||
},
|
||
|
||
/**
|
||
* Gets whether an element is an `<input type="hidden">`.
|
||
* @param {HTMLElement} element
|
||
* @returns {boolean}
|
||
*/
|
||
isHiddenInput: function(element) {
|
||
return $mdUtil.isInputElement(element) && element.type == 'hidden';
|
||
},
|
||
|
||
/**
|
||
* Gets whether an element is an anchor that has an href attribute.
|
||
* @param {HTMLElement} element
|
||
* @returns {boolean}
|
||
*/
|
||
isAnchorWithHref: function(element) {
|
||
return $mdUtil.isAnchorElement(element) && element.hasAttribute('href');
|
||
},
|
||
|
||
/**
|
||
* Gets whether an element is an input element.
|
||
* @param {HTMLElement} element
|
||
* @returns {boolean}
|
||
*/
|
||
isInputElement: function(element) {
|
||
return element.nodeName.toLowerCase() == 'input';
|
||
},
|
||
|
||
/**
|
||
* Gets whether an element is an anchor element.
|
||
* @param {HTMLElement} element
|
||
* @returns {boolean}
|
||
*/
|
||
isAnchorElement: function(element) {
|
||
return element.nodeName.toLowerCase() == 'a';
|
||
},
|
||
|
||
/**********************************************************************************************
|
||
* The following two functions were sourced from
|
||
* https://github.com/angular/components/blob/3c37e4b1c1cb74a3d0a90d173240fc730d21d9d4/src/cdk/a11y/focus-trap/focus-trap.ts#L268-L311
|
||
**********************************************************************************************/
|
||
|
||
/**
|
||
* Get the first tabbable element from a DOM subtree (inclusive).
|
||
* @param {HTMLElement} root
|
||
* @returns {HTMLElement|null}
|
||
*/
|
||
getFirstTabbableElement: function(root) {
|
||
if ($mdUtil.isFocusable(root) && $mdUtil.isTabbable(root)) {
|
||
return root;
|
||
}
|
||
|
||
// Iterate in DOM order. Note that IE doesn't have `children` for SVG so we fall
|
||
// back to `childNodes` which includes text nodes, comments etc.
|
||
var children = root.children || root.childNodes;
|
||
|
||
for (var i = 0; i < children.length; i++) {
|
||
var tabbableChild = children[i].nodeType === $document[0].ELEMENT_NODE ?
|
||
$mdUtil.getFirstTabbableElement(children[i]) : null;
|
||
|
||
if (tabbableChild) {
|
||
return tabbableChild;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
},
|
||
|
||
/**
|
||
* Get the last tabbable element from a DOM subtree (inclusive).
|
||
* @param {HTMLElement} root
|
||
* @returns {HTMLElement|null}
|
||
*/
|
||
getLastTabbableElement: function(root) {
|
||
if ($mdUtil.isFocusable(root) && $mdUtil.isTabbable(root)) {
|
||
return root;
|
||
}
|
||
|
||
// Iterate in reverse DOM order.
|
||
var children = root.children || root.childNodes;
|
||
|
||
for (var i = children.length - 1; i >= 0; i--) {
|
||
var tabbableChild = children[i].nodeType === $document[0].ELEMENT_NODE ?
|
||
$mdUtil.getLastTabbableElement(children[i]) : null;
|
||
|
||
if (tabbableChild) {
|
||
return tabbableChild;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
};
|
||
|
||
// Instantiate other namespace utility methods
|
||
|
||
$mdUtil.dom.animator = $$mdAnimate($mdUtil);
|
||
|
||
return $mdUtil;
|
||
|
||
function getNode(el) {
|
||
return el[0] || el;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Since removing jQuery from the demos, some code that uses `element.focus()` is broken.
|
||
* We need to add `element.focus()`, because it's testable unlike `element[0].focus`.
|
||
*/
|
||
angular.element.prototype.focus = angular.element.prototype.focus || function() {
|
||
if (this.length) {
|
||
this[0].focus();
|
||
}
|
||
return this;
|
||
};
|
||
|
||
angular.element.prototype.blur = angular.element.prototype.blur || function() {
|
||
if (this.length) {
|
||
this[0].blur();
|
||
}
|
||
return this;
|
||
};
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
// Polyfill angular < 1.4 (provide $animateCss)
|
||
angular
|
||
.module('material.core')
|
||
.factory('$$mdAnimate', ["$q", "$timeout", "$mdConstant", "$animateCss", function($q, $timeout, $mdConstant, $animateCss) {
|
||
// Since $$mdAnimate is injected into $mdUtil... use a wrapper function
|
||
// to subsequently inject $mdUtil as an argument to the AnimateDomUtils
|
||
return function($mdUtil) {
|
||
return AnimateDomUtils($mdUtil, $q, $timeout, $mdConstant, $animateCss);
|
||
};
|
||
}]);
|
||
|
||
/**
|
||
* Factory function that requires special injections
|
||
*/
|
||
function AnimateDomUtils($mdUtil, $q, $timeout, $mdConstant, $animateCss) {
|
||
var self;
|
||
return self = {
|
||
translate3d : function(target, from, to, options) {
|
||
return $animateCss(target, {
|
||
from: from,
|
||
to: to,
|
||
addClass: options.transitionInClass,
|
||
removeClass: options.transitionOutClass,
|
||
duration: options.duration
|
||
})
|
||
.start()
|
||
.then(function() {
|
||
// Resolve with reverser function...
|
||
return reverseTranslate;
|
||
});
|
||
|
||
/**
|
||
* Specific reversal of the request translate animation above...
|
||
*/
|
||
function reverseTranslate (newFrom) {
|
||
return $animateCss(target, {
|
||
to: newFrom || from,
|
||
addClass: options.transitionOutClass,
|
||
removeClass: options.transitionInClass,
|
||
duration: options.duration
|
||
}).start();
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Listen for transitionEnd event (with optional timeout)
|
||
* Announce completion or failure via promise handlers
|
||
*/
|
||
waitTransitionEnd: function (element, opts) {
|
||
var TIMEOUT = 3000; // fallback is 3 secs
|
||
|
||
return $q(function(resolve, reject){
|
||
opts = opts || { };
|
||
|
||
// If there is no transition is found, resolve immediately
|
||
//
|
||
// NOTE: using $mdUtil.nextTick() causes delays/issues
|
||
if (noTransitionFound(opts.cachedTransitionStyles)) {
|
||
TIMEOUT = 0;
|
||
}
|
||
|
||
var timer = $timeout(finished, opts.timeout || TIMEOUT);
|
||
element.on($mdConstant.CSS.TRANSITIONEND, finished);
|
||
|
||
/**
|
||
* Upon timeout or transitionEnd, reject or resolve (respectively) this promise.
|
||
* NOTE: Make sure this transitionEnd didn't bubble up from a child
|
||
*/
|
||
function finished(ev) {
|
||
if (ev && ev.target !== element[0]) return;
|
||
|
||
if (ev) $timeout.cancel(timer);
|
||
element.off($mdConstant.CSS.TRANSITIONEND, finished);
|
||
|
||
// Never reject since ngAnimate may cause timeouts due missed transitionEnd events
|
||
resolve();
|
||
}
|
||
|
||
/**
|
||
* 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;
|
||
|
||
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);
|
||
|
||
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
|
||
};
|
||
}
|
||
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
|
||
* origin bounds simply becomes the current bounds for the dialogContainer's parent.
|
||
* @returns {null|DOMRect}
|
||
*/
|
||
function currentBounds() {
|
||
var container = element ? element.parent() : null;
|
||
var parent = container ? container.parent() : null;
|
||
|
||
return parent ? self.clientRect(parent) : null;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 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...
|
||
*/
|
||
toCss : function(raw) {
|
||
var css = { };
|
||
var lookups = 'left top right bottom width height x y min-width min-height max-width max-height';
|
||
|
||
angular.forEach(raw, function(value,key) {
|
||
if (angular.isUndefined(value)) return;
|
||
|
||
if (lookups.indexOf(key) >= 0) {
|
||
css[key] = value + 'px';
|
||
} else {
|
||
switch (key) {
|
||
case 'transition':
|
||
convertToVendor(key, $mdConstant.CSS.TRANSITION, value);
|
||
break;
|
||
case 'transform':
|
||
convertToVendor(key, $mdConstant.CSS.TRANSFORM, value);
|
||
break;
|
||
case 'transformOrigin':
|
||
convertToVendor(key, $mdConstant.CSS.TRANSFORM_ORIGIN, value);
|
||
break;
|
||
case 'font-size':
|
||
css['font-size'] = value; // font sizes aren't always in px
|
||
break;
|
||
}
|
||
}
|
||
});
|
||
|
||
return css;
|
||
|
||
function convertToVendor(key, vendor, value) {
|
||
angular.forEach(vendor.split(' '), function (key) {
|
||
css[key] = value;
|
||
});
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Convert the translate CSS value to key/value pair(s).
|
||
* @param {string} transform
|
||
* @param {boolean=} addTransition
|
||
* @param {string=} transition
|
||
* @return {Object} object containing CSS translate key/value pair(s)
|
||
*/
|
||
toTransformCss: function (transform, addTransition, transition) {
|
||
var css = {};
|
||
angular.forEach($mdConstant.CSS.TRANSFORM.split(' '), function (key) {
|
||
css[key] = transform;
|
||
});
|
||
|
||
if (addTransition) {
|
||
transition = transition || "all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) !important";
|
||
css.transition = transition;
|
||
}
|
||
|
||
return css;
|
||
},
|
||
|
||
/**
|
||
* Clone the Rect and calculate the height/width if needed.
|
||
* @param {DOMRect} source
|
||
* @param {DOMRect=} destination
|
||
* @returns {null|DOMRect}
|
||
*/
|
||
copyRect: function (source, destination) {
|
||
if (!source) return null;
|
||
|
||
destination = destination || {};
|
||
|
||
angular.forEach('left top right bottom width height'.split(' '), function (key) {
|
||
destination[key] = Math.round(source[key]);
|
||
});
|
||
|
||
destination.width = destination.width || (destination.right - destination.left);
|
||
destination.height = destination.height || (destination.bottom - destination.top);
|
||
|
||
return destination;
|
||
},
|
||
|
||
/**
|
||
* Calculate ClientRect of element; return null if hidden or zero size.
|
||
* @param {Element|string} element
|
||
* @returns {null|DOMRect}
|
||
*/
|
||
clientRect: function (element) {
|
||
var bounds = angular.element(element)[0].getBoundingClientRect();
|
||
var isPositiveSizeClientRect = function (rect) {
|
||
return rect && (rect.width > 0) && (rect.height > 0);
|
||
};
|
||
|
||
// If the event origin element has zero size, it has probably been hidden.
|
||
return isPositiveSizeClientRect(bounds) ? self.copyRect(bounds) : null;
|
||
},
|
||
|
||
/**
|
||
* Calculate 'rounded' center point of Rect
|
||
* @param {DOMRect} targetRect
|
||
* @returns {{x: number, y: number}}
|
||
*/
|
||
centerPointFor: function (targetRect) {
|
||
return targetRect ? {
|
||
x: Math.round(targetRect.left + (targetRect.width / 2)),
|
||
y: Math.round(targetRect.top + (targetRect.height / 2))
|
||
} : { x : 0, y : 0 };
|
||
}
|
||
};
|
||
}
|
||
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
if (angular.version.minor >= 4) {
|
||
angular.module('material.core.animate', []);
|
||
} else {
|
||
(function() {
|
||
"use strict";
|
||
|
||
var forEach = angular.forEach;
|
||
|
||
var WEBKIT = angular.isDefined(document.documentElement.style.WebkitAppearance);
|
||
var TRANSITION_PROP = WEBKIT ? 'WebkitTransition' : 'transition';
|
||
var ANIMATION_PROP = WEBKIT ? 'WebkitAnimation' : 'animation';
|
||
var PREFIX = WEBKIT ? '-webkit-' : '';
|
||
|
||
var TRANSITION_EVENTS = (WEBKIT ? 'webkitTransitionEnd ' : '') + 'transitionend';
|
||
var ANIMATION_EVENTS = (WEBKIT ? 'webkitAnimationEnd ' : '') + 'animationend';
|
||
|
||
var $$ForceReflowFactory = ['$document', function($document) {
|
||
return function() {
|
||
return $document[0].body.clientWidth + 1;
|
||
};
|
||
}];
|
||
|
||
var $$rAFMutexFactory = ['$$rAF', function($$rAF) {
|
||
return function() {
|
||
var passed = false;
|
||
$$rAF(function() {
|
||
passed = true;
|
||
});
|
||
return function(fn) {
|
||
passed ? fn() : $$rAF(fn);
|
||
};
|
||
};
|
||
}];
|
||
|
||
var $$AnimateRunnerFactory = ['$q', '$$rAFMutex', function($q, $$rAFMutex) {
|
||
var INITIAL_STATE = 0;
|
||
var DONE_PENDING_STATE = 1;
|
||
var DONE_COMPLETE_STATE = 2;
|
||
|
||
function AnimateRunner(host) {
|
||
this.setHost(host);
|
||
|
||
this._doneCallbacks = [];
|
||
this._runInAnimationFrame = $$rAFMutex();
|
||
this._state = 0;
|
||
}
|
||
|
||
AnimateRunner.prototype = {
|
||
setHost: function(host) {
|
||
this.host = host || {};
|
||
},
|
||
|
||
done: function(fn) {
|
||
if (this._state === DONE_COMPLETE_STATE) {
|
||
fn();
|
||
} else {
|
||
this._doneCallbacks.push(fn);
|
||
}
|
||
},
|
||
|
||
progress: angular.noop,
|
||
|
||
getPromise: function() {
|
||
if (!this.promise) {
|
||
var self = this;
|
||
this.promise = $q(function(resolve, reject) {
|
||
self.done(function(status) {
|
||
status === false ? reject() : resolve();
|
||
});
|
||
});
|
||
}
|
||
return this.promise;
|
||
},
|
||
|
||
then: function(resolveHandler, rejectHandler) {
|
||
return this.getPromise().then(resolveHandler, rejectHandler);
|
||
},
|
||
|
||
'catch': function(handler) {
|
||
return this.getPromise()['catch'](handler);
|
||
},
|
||
|
||
'finally': function(handler) {
|
||
return this.getPromise()['finally'](handler);
|
||
},
|
||
|
||
pause: function() {
|
||
if (this.host.pause) {
|
||
this.host.pause();
|
||
}
|
||
},
|
||
|
||
resume: function() {
|
||
if (this.host.resume) {
|
||
this.host.resume();
|
||
}
|
||
},
|
||
|
||
end: function() {
|
||
if (this.host.end) {
|
||
this.host.end();
|
||
}
|
||
this._resolve(true);
|
||
},
|
||
|
||
cancel: function() {
|
||
if (this.host.cancel) {
|
||
this.host.cancel();
|
||
}
|
||
this._resolve(false);
|
||
},
|
||
|
||
complete: function(response) {
|
||
var self = this;
|
||
if (self._state === INITIAL_STATE) {
|
||
self._state = DONE_PENDING_STATE;
|
||
self._runInAnimationFrame(function() {
|
||
self._resolve(response);
|
||
});
|
||
}
|
||
},
|
||
|
||
_resolve: function(response) {
|
||
if (this._state !== DONE_COMPLETE_STATE) {
|
||
forEach(this._doneCallbacks, function(fn) {
|
||
fn(response);
|
||
});
|
||
this._doneCallbacks.length = 0;
|
||
this._state = DONE_COMPLETE_STATE;
|
||
}
|
||
}
|
||
};
|
||
|
||
// Polyfill AnimateRunner.all which is used by input animations
|
||
AnimateRunner.all = function(runners, callback) {
|
||
var count = 0;
|
||
var status = true;
|
||
forEach(runners, function(runner) {
|
||
runner.done(onProgress);
|
||
});
|
||
|
||
function onProgress(response) {
|
||
status = status && response;
|
||
if (++count === runners.length) {
|
||
callback(status);
|
||
}
|
||
}
|
||
};
|
||
|
||
return AnimateRunner;
|
||
}];
|
||
|
||
angular
|
||
.module('material.core.animate', [])
|
||
.factory('$$forceReflow', $$ForceReflowFactory)
|
||
.factory('$$AnimateRunner', $$AnimateRunnerFactory)
|
||
.factory('$$rAFMutex', $$rAFMutexFactory)
|
||
.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();
|
||
|
||
var hasCompleteStyles = false;
|
||
var hasCompleteClasses = false;
|
||
|
||
if (areAnimationsAllowed) {
|
||
if (options.transitionStyle) {
|
||
temporaryStyles.push([PREFIX + 'transition', options.transitionStyle]);
|
||
}
|
||
|
||
if (options.keyframeStyle) {
|
||
temporaryStyles.push([PREFIX + 'animation', options.keyframeStyle]);
|
||
}
|
||
|
||
if (options.delay) {
|
||
temporaryStyles.push([PREFIX + 'transition-delay', options.delay + 's']);
|
||
}
|
||
|
||
if (options.duration) {
|
||
temporaryStyles.push([PREFIX + 'transition-duration', options.duration + 's']);
|
||
}
|
||
|
||
hasCompleteStyles = options.keyframeStyle ||
|
||
(options.to && (options.duration > 0 || options.transitionStyle));
|
||
hasCompleteClasses = !!options.addClass || !!options.removeClass;
|
||
|
||
blockTransition(element, true);
|
||
}
|
||
|
||
var hasCompleteAnimation = areAnimationsAllowed && (hasCompleteStyles || hasCompleteClasses);
|
||
|
||
applyAnimationFromStyles(element, options);
|
||
|
||
var animationClosed = false;
|
||
var events, eventFn;
|
||
|
||
return {
|
||
close: $window.close,
|
||
start: function() {
|
||
var runner = new $$AnimateRunner();
|
||
waitUntilQuiet(function() {
|
||
blockTransition(element, false);
|
||
if (!hasCompleteAnimation) {
|
||
return close();
|
||
}
|
||
|
||
forEach(temporaryStyles, function(entry) {
|
||
var key = entry[0];
|
||
var value = entry[1];
|
||
node.style[camelCase(key)] = value;
|
||
});
|
||
|
||
applyClasses(element, options);
|
||
|
||
var timings = computeTimings(element);
|
||
if (timings.duration === 0) {
|
||
return close();
|
||
}
|
||
|
||
var moreStyles = [];
|
||
|
||
if (options.easing) {
|
||
if (timings.transitionDuration) {
|
||
moreStyles.push([PREFIX + 'transition-timing-function', options.easing]);
|
||
}
|
||
if (timings.animationDuration) {
|
||
moreStyles.push([PREFIX + 'animation-timing-function', options.easing]);
|
||
}
|
||
}
|
||
|
||
if (options.delay && timings.animationDelay) {
|
||
moreStyles.push([PREFIX + 'animation-delay', options.delay + 's']);
|
||
}
|
||
|
||
if (options.duration && timings.animationDuration) {
|
||
moreStyles.push([PREFIX + 'animation-duration', options.duration + 's']);
|
||
}
|
||
|
||
forEach(moreStyles, function(entry) {
|
||
var key = entry[0];
|
||
var value = entry[1];
|
||
node.style[camelCase(key)] = value;
|
||
temporaryStyles.push(entry);
|
||
});
|
||
|
||
var maxDelay = timings.delay;
|
||
var maxDelayTime = maxDelay * 1000;
|
||
var maxDuration = timings.duration;
|
||
var maxDurationTime = maxDuration * 1000;
|
||
var startTime = Date.now();
|
||
|
||
events = [];
|
||
if (timings.transitionDuration) {
|
||
events.push(TRANSITION_EVENTS);
|
||
}
|
||
if (timings.animationDuration) {
|
||
events.push(ANIMATION_EVENTS);
|
||
}
|
||
events = events.join(' ');
|
||
eventFn = function(event) {
|
||
event.stopPropagation();
|
||
var ev = event.originalEvent || event;
|
||
var timeStamp = ev.timeStamp || Date.now();
|
||
var elapsedTime = parseFloat(ev.elapsedTime.toFixed(3));
|
||
if (Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) {
|
||
close();
|
||
}
|
||
};
|
||
element.on(events, eventFn);
|
||
|
||
applyAnimationToStyles(element, options);
|
||
|
||
$timeout(close, maxDelayTime + maxDurationTime * 1.5, false);
|
||
});
|
||
|
||
return runner;
|
||
|
||
function close() {
|
||
if (animationClosed) return;
|
||
animationClosed = true;
|
||
|
||
if (events && eventFn) {
|
||
element.off(events, eventFn);
|
||
}
|
||
applyClasses(element, options);
|
||
applyAnimationStyles(element, options);
|
||
forEach(temporaryStyles, function(entry) {
|
||
node.style[camelCase(entry[0])] = '';
|
||
});
|
||
runner.complete(true);
|
||
return runner;
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
function applyClasses(element, options) {
|
||
if (options.addClass) {
|
||
$$jqLite.addClass(element, options.addClass);
|
||
options.addClass = null;
|
||
}
|
||
if (options.removeClass) {
|
||
$$jqLite.removeClass(element, options.removeClass);
|
||
options.removeClass = null;
|
||
}
|
||
}
|
||
|
||
function computeTimings(element) {
|
||
var node = getDomNode(element);
|
||
var cs = $window.getComputedStyle(node);
|
||
var tdr = parseMaxTime(cs[prop('transitionDuration')]);
|
||
var adr = parseMaxTime(cs[prop('animationDuration')]);
|
||
var tdy = parseMaxTime(cs[prop('transitionDelay')]);
|
||
var ady = parseMaxTime(cs[prop('animationDelay')]);
|
||
|
||
adr *= (parseInt(cs[prop('animationIterationCount')], 10) || 1);
|
||
var duration = Math.max(adr, tdr);
|
||
var delay = Math.max(ady, tdy);
|
||
|
||
return {
|
||
duration: duration,
|
||
delay: delay,
|
||
animationDuration: adr,
|
||
transitionDuration: tdr,
|
||
animationDelay: ady,
|
||
transitionDelay: tdy
|
||
};
|
||
|
||
function prop(key) {
|
||
return WEBKIT ? 'Webkit' + key.charAt(0).toUpperCase() + key.substr(1)
|
||
: key;
|
||
}
|
||
}
|
||
|
||
function parseMaxTime(str) {
|
||
var maxValue = 0;
|
||
var values = (str || "").split(/\s*,\s*/);
|
||
forEach(values, function(value) {
|
||
// it's always safe to consider only second values and omit `ms` values since
|
||
// getComputedStyle will always handle the conversion for us
|
||
if (value.charAt(value.length - 1) == 's') {
|
||
value = value.substring(0, value.length - 1);
|
||
}
|
||
value = parseFloat(value) || 0;
|
||
maxValue = maxValue ? Math.max(value, maxValue) : value;
|
||
});
|
||
return maxValue;
|
||
}
|
||
|
||
var cancelLastRAFRequest;
|
||
var rafWaitQueue = [];
|
||
function waitUntilQuiet(callback) {
|
||
if (cancelLastRAFRequest) {
|
||
cancelLastRAFRequest(); // cancels the request
|
||
}
|
||
rafWaitQueue.push(callback);
|
||
cancelLastRAFRequest = $$rAF(function() {
|
||
cancelLastRAFRequest = null;
|
||
|
||
// DO NOT REMOVE THIS LINE OR REFACTOR OUT THE `pageWidth` variable.
|
||
// PLEASE EXAMINE THE `$$forceReflow` service to understand why.
|
||
var pageWidth = $$forceReflow();
|
||
|
||
// we use a for loop to ensure that if the queue is changed
|
||
// during this looping then it will consider new requests
|
||
for (var i = 0; i < rafWaitQueue.length; i++) {
|
||
rafWaitQueue[i](pageWidth);
|
||
}
|
||
rafWaitQueue.length = 0;
|
||
});
|
||
}
|
||
|
||
function applyAnimationStyles(element, options) {
|
||
applyAnimationFromStyles(element, options);
|
||
applyAnimationToStyles(element, options);
|
||
}
|
||
|
||
function applyAnimationFromStyles(element, options) {
|
||
if (options.from) {
|
||
element.css(options.from);
|
||
options.from = null;
|
||
}
|
||
}
|
||
|
||
function applyAnimationToStyles(element, options) {
|
||
if (options.to) {
|
||
element.css(options.to);
|
||
options.to = null;
|
||
}
|
||
}
|
||
|
||
function getDomNode(element) {
|
||
for (var i = 0; i < element.length; i++) {
|
||
if (element[i].nodeType === 1) return element[i];
|
||
}
|
||
}
|
||
|
||
function blockTransition(element, bool) {
|
||
var node = getDomNode(element);
|
||
var key = camelCase(PREFIX + 'transition-delay');
|
||
node.style[key] = bool ? '-9999s' : '';
|
||
}
|
||
|
||
return init;
|
||
}]);
|
||
|
||
/**
|
||
* Older browsers [FF31] expect camelCase
|
||
* property keys.
|
||
* e.g.
|
||
* animation-duration --> animationDuration
|
||
*/
|
||
function camelCase(str) {
|
||
return str.replace(/-[a-z]/g, function(str) {
|
||
return str.charAt(1).toUpperCase();
|
||
});
|
||
}
|
||
|
||
})();
|
||
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.core.aria
|
||
* @description
|
||
* Aria Expectations for AngularJS Material components.
|
||
*/
|
||
MdAriaService.$inject = ["$$rAF", "$log", "$window", "$interpolate"];
|
||
angular
|
||
.module('material.core')
|
||
.provider('$mdAria', MdAriaProvider);
|
||
|
||
/**
|
||
* @ngdoc service
|
||
* @name $mdAriaProvider
|
||
* @module material.core.aria
|
||
*
|
||
* @description
|
||
*
|
||
* Modify options of the `$mdAria` service, which will be used by most of the AngularJS Material
|
||
* components.
|
||
*
|
||
* You are able to disable `$mdAria` warnings, by using the following markup.
|
||
*
|
||
* <hljs lang="js">
|
||
* app.config(function($mdAriaProvider) {
|
||
* // Globally disables all ARIA warnings.
|
||
* $mdAriaProvider.disableWarnings();
|
||
* });
|
||
* </hljs>
|
||
*
|
||
*/
|
||
function MdAriaProvider() {
|
||
|
||
var config = {
|
||
/** Whether we should show ARIA warnings in the console if labels are missing on the element */
|
||
showWarnings: true
|
||
};
|
||
|
||
return {
|
||
disableWarnings: disableWarnings,
|
||
$get: ["$$rAF", "$log", "$window", "$interpolate", function($$rAF, $log, $window, $interpolate) {
|
||
return MdAriaService.apply(config, arguments);
|
||
}]
|
||
};
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdAriaProvider#disableWarnings
|
||
* @description Disables all ARIA warnings generated by AngularJS Material.
|
||
*/
|
||
function disableWarnings() {
|
||
config.showWarnings = false;
|
||
}
|
||
}
|
||
|
||
/*
|
||
* @ngInject
|
||
*/
|
||
function MdAriaService($$rAF, $log, $window, $interpolate) {
|
||
|
||
// Load the showWarnings option from the current context and store it inside of a scope variable,
|
||
// because the context will be probably lost in some function calls.
|
||
var showWarnings = this.showWarnings;
|
||
|
||
return {
|
||
expect: expect,
|
||
expectAsync: expectAsync,
|
||
expectWithText: expectWithText,
|
||
expectWithoutText: expectWithoutText,
|
||
getText: getText,
|
||
hasAriaLabel: hasAriaLabel,
|
||
parentHasAriaLabel: parentHasAriaLabel
|
||
};
|
||
|
||
/**
|
||
* Check if expected attribute has been specified on the target element or child
|
||
* @param {string|JQLite} element
|
||
* @param {string} attrName
|
||
* @param {string=} defaultValue What to set the attr to if no value is found
|
||
*/
|
||
function expect(element, attrName, defaultValue) {
|
||
|
||
var node = angular.element(element)[0] || element;
|
||
|
||
// if node exists and neither it nor its children have the attribute
|
||
if (node &&
|
||
((!node.hasAttribute(attrName) ||
|
||
node.getAttribute(attrName).length === 0) &&
|
||
!childHasAttribute(node, attrName))) {
|
||
|
||
defaultValue = angular.isString(defaultValue) ? defaultValue.trim() : '';
|
||
if (defaultValue.length) {
|
||
element.attr(attrName, defaultValue);
|
||
} else if (showWarnings) {
|
||
$log.warn('ARIA: Attribute "', attrName, '", required for accessibility, is missing on node:', node);
|
||
}
|
||
|
||
}
|
||
}
|
||
|
||
function expectAsync(element, attrName, defaultValueGetter) {
|
||
// Problem: when retrieving the element's contents synchronously to find the label,
|
||
// the text may not be defined yet in the case of a binding.
|
||
// There is a higher chance that a binding will be defined if we wait one frame.
|
||
$$rAF(function() {
|
||
expect(element, attrName, defaultValueGetter());
|
||
});
|
||
}
|
||
|
||
function expectWithText(element, attrName) {
|
||
var content = getText(element) || "";
|
||
var hasBinding = content.indexOf($interpolate.startSymbol()) > -1;
|
||
|
||
if (hasBinding) {
|
||
expectAsync(element, attrName, function() {
|
||
return getText(element);
|
||
});
|
||
} else {
|
||
expect(element, attrName, content);
|
||
}
|
||
}
|
||
|
||
function expectWithoutText(element, attrName) {
|
||
var content = getText(element);
|
||
var hasBinding = content.indexOf($interpolate.startSymbol()) > -1;
|
||
|
||
if (!hasBinding && !content) {
|
||
expect(element, attrName, content);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {Element|JQLite} element
|
||
* @returns {string}
|
||
*/
|
||
function getText(element) {
|
||
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() || '';
|
||
|
||
/**
|
||
* @param {Node} node
|
||
* @returns {boolean}
|
||
*/
|
||
function isAriaHiddenNode(node) {
|
||
while (node.parentNode && (node = node.parentNode) !== element) {
|
||
if (node.getAttribute && node.getAttribute('aria-hidden') === 'true') {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function childHasAttribute(node, attrName) {
|
||
var hasChildren = node.hasChildNodes(),
|
||
hasAttr = false;
|
||
|
||
function isHidden(el) {
|
||
var style = el.currentStyle ? el.currentStyle : $window.getComputedStyle(el);
|
||
return (style.display === 'none');
|
||
}
|
||
|
||
if (hasChildren) {
|
||
var children = node.childNodes;
|
||
for (var i=0; i < children.length; i++) {
|
||
var child = children[i];
|
||
if (child.nodeType === 1 && child.hasAttribute(attrName)) {
|
||
if (!isHidden(child)) {
|
||
hasAttr = true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return hasAttr;
|
||
}
|
||
|
||
/**
|
||
* Check if expected element has aria label attribute
|
||
* @param element
|
||
*/
|
||
function hasAriaLabel(element) {
|
||
var node = angular.element(element)[0] || element;
|
||
|
||
/* Check if compatible node type (ie: not HTML Document node) */
|
||
if (!node.hasAttribute) {
|
||
return false;
|
||
}
|
||
|
||
/* Check label or description attributes */
|
||
return node.hasAttribute('aria-label') || node.hasAttribute('aria-labelledby') || node.hasAttribute('aria-describedby');
|
||
}
|
||
|
||
/**
|
||
* Check if expected element's parent has aria label attribute and has valid role and tagName
|
||
* @param {string|JQLite|Node & ParentNode} element
|
||
* @param {number=} level Number of levels deep search should be performed
|
||
*/
|
||
function parentHasAriaLabel(element, level) {
|
||
level = level || 1;
|
||
var node = angular.element(element)[0] || element;
|
||
if (!node.parentNode) {
|
||
return false;
|
||
}
|
||
if (performCheck(node.parentNode)) {
|
||
return true;
|
||
}
|
||
level--;
|
||
if (level) {
|
||
return parentHasAriaLabel(node.parentNode, level);
|
||
}
|
||
return false;
|
||
|
||
function performCheck(parentNode) {
|
||
if (!hasAriaLabel(parentNode)) {
|
||
return false;
|
||
}
|
||
/* Perform role block-list check */
|
||
if (parentNode.hasAttribute('role')) {
|
||
switch (parentNode.getAttribute('role').toLowerCase()) {
|
||
case 'command':
|
||
case 'definition':
|
||
case 'directory':
|
||
case 'grid':
|
||
case 'list':
|
||
case 'listitem':
|
||
case 'log':
|
||
case 'marquee':
|
||
case 'menu':
|
||
case 'menubar':
|
||
case 'note':
|
||
case 'presentation':
|
||
case 'separator':
|
||
case 'scrollbar':
|
||
case 'status':
|
||
case 'tablist':
|
||
return false;
|
||
}
|
||
}
|
||
/* Perform tagName block-list check */
|
||
switch (parentNode.tagName.toLowerCase()) {
|
||
case 'abbr':
|
||
case 'acronym':
|
||
case 'address':
|
||
case 'applet':
|
||
case 'audio':
|
||
case 'b':
|
||
case 'bdi':
|
||
case 'bdo':
|
||
case 'big':
|
||
case 'blockquote':
|
||
case 'br':
|
||
case 'canvas':
|
||
case 'caption':
|
||
case 'center':
|
||
case 'cite':
|
||
case 'code':
|
||
case 'col':
|
||
case 'data':
|
||
case 'dd':
|
||
case 'del':
|
||
case 'dfn':
|
||
case 'dir':
|
||
case 'div':
|
||
case 'dl':
|
||
case 'em':
|
||
case 'embed':
|
||
case 'fieldset':
|
||
case 'figcaption':
|
||
case 'font':
|
||
case 'h1':
|
||
case 'h2':
|
||
case 'h3':
|
||
case 'h4':
|
||
case 'h5':
|
||
case 'h6':
|
||
case 'hgroup':
|
||
case 'html':
|
||
case 'i':
|
||
case 'ins':
|
||
case 'isindex':
|
||
case 'kbd':
|
||
case 'keygen':
|
||
case 'label':
|
||
case 'legend':
|
||
case 'li':
|
||
case 'map':
|
||
case 'mark':
|
||
case 'menu':
|
||
case 'object':
|
||
case 'ol':
|
||
case 'output':
|
||
case 'pre':
|
||
case 'presentation':
|
||
case 'q':
|
||
case 'rt':
|
||
case 'ruby':
|
||
case 'samp':
|
||
case 'small':
|
||
case 'source':
|
||
case 'span':
|
||
case 'status':
|
||
case 'strike':
|
||
case 'strong':
|
||
case 'sub':
|
||
case 'sup':
|
||
case 'svg':
|
||
case 'tbody':
|
||
case 'td':
|
||
case 'th':
|
||
case 'thead':
|
||
case 'time':
|
||
case 'tr':
|
||
case 'track':
|
||
case 'tt':
|
||
case 'ul':
|
||
case 'var':
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.core.compiler
|
||
* @description
|
||
* AngularJS Material template and element compiler.
|
||
*/
|
||
angular
|
||
.module('material.core')
|
||
.provider('$mdCompiler', MdCompilerProvider);
|
||
|
||
MdCompilerProvider.$inject = ['$compileProvider'];
|
||
function MdCompilerProvider() {
|
||
|
||
this.$get = ["$q", "$templateRequest", "$injector", "$compile", "$controller",
|
||
function($q, $templateRequest, $injector, $compile, $controller) {
|
||
return new MdCompilerService($q, $templateRequest, $injector, $compile, $controller);
|
||
}];
|
||
|
||
/**
|
||
* @ngdoc service
|
||
* @name $mdCompiler
|
||
* @module material.core.compiler
|
||
* @description
|
||
* The $mdCompiler service is an abstraction of AngularJS's compiler, that allows developers
|
||
* to compile an element with options like in a Directive Definition Object.
|
||
*
|
||
* > The compiler powers a lot of components inside of AngularJS Material.
|
||
* > Like the `$mdPanel` or `$mdDialog` services.
|
||
*
|
||
* @usage
|
||
*
|
||
* Basic Usage with a template
|
||
*
|
||
* <hljs lang="js">
|
||
* $mdCompiler.compile({
|
||
* templateUrl: 'modal.html',
|
||
* controller: 'ModalCtrl',
|
||
* locals: {
|
||
* modal: myModalInstance;
|
||
* }
|
||
* }).then(function (compileData) {
|
||
* compileData.element; // Compiled DOM element
|
||
* compileData.link(myScope); // Instantiate controller and link element to scope.
|
||
* });
|
||
* </hljs>
|
||
*
|
||
* Example with a content element
|
||
*
|
||
* <hljs lang="js">
|
||
*
|
||
* // Create a virtual element and link it manually.
|
||
* // The compiler doesn't need to recompile the element each time.
|
||
* var myElement = $compile('<span>Test</span>')(myScope);
|
||
*
|
||
* $mdCompiler.compile({
|
||
* contentElement: myElement
|
||
* }).then(function (compileData) {
|
||
* compileData.element // Content Element (same as above)
|
||
* compileData.link // This does nothing when using a contentElement.
|
||
* });
|
||
* </hljs>
|
||
*
|
||
* > Content Element is a significant performance improvement when the developer already knows
|
||
* > that the compiled element will be always the same and the scope will not change either.
|
||
*
|
||
* The `contentElement` option also supports DOM elements which will be temporary removed and
|
||
* restored at its old position.
|
||
*
|
||
* <hljs lang="js">
|
||
* var domElement = document.querySelector('#myElement');
|
||
*
|
||
* $mdCompiler.compile({
|
||
* contentElement: myElement
|
||
* }).then(function (compileData) {
|
||
* compileData.element // Content Element (same as above)
|
||
* compileData.link // This does nothing when using a contentElement.
|
||
* });
|
||
* </hljs>
|
||
*
|
||
* The `$mdCompiler` can also query for the element in the DOM itself.
|
||
*
|
||
* <hljs lang="js">
|
||
* $mdCompiler.compile({
|
||
* contentElement: '#myElement'
|
||
* }).then(function (compileData) {
|
||
* compileData.element // Content Element (same as above)
|
||
* compileData.link // This does nothing when using a contentElement.
|
||
* });
|
||
* </hljs>
|
||
*
|
||
*/
|
||
function MdCompilerService($q, $templateRequest, $injector, $compile, $controller) {
|
||
|
||
/**
|
||
* @private @const
|
||
* @type {!IQService}
|
||
*/
|
||
this.$q = $q;
|
||
|
||
/**
|
||
* @private @const
|
||
* @type {!ITemplateRequestService}
|
||
*/
|
||
this.$templateRequest = $templateRequest;
|
||
|
||
/**
|
||
* @private @const
|
||
* @type {!IInjectorService}
|
||
*/
|
||
this.$injector = $injector;
|
||
|
||
/**
|
||
* @private @const
|
||
* @type{!ICompileService}
|
||
*/
|
||
this.$compile = $compile;
|
||
|
||
/**
|
||
* @private @const
|
||
* @type {!IControllerService}
|
||
*/
|
||
this.$controller = $controller;
|
||
}
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdCompiler#compile
|
||
* @description
|
||
*
|
||
* A method to compile a HTML template with the AngularJS compiler.
|
||
* The `$mdCompiler` is wrapper around the AngularJS compiler and provides extra functionality
|
||
* like controller instantiation or async resolves.
|
||
*
|
||
* @param {!Object} options An options object, with the following properties:
|
||
*
|
||
* - `controller` - `{string|function}` Controller fn that should be associated with
|
||
* newly created scope or the name of a registered controller if passed as a string.
|
||
* - `controllerAs` - `{string=}` A controller alias name. If present the controller will be
|
||
* published to scope under the `controllerAs` name.
|
||
* - `contentElement` - `{string|Element}`: Instead of using a template, which will be
|
||
* compiled each time, you can also use a DOM element.<br/>
|
||
* - `template` - `{string=}` An html template as a string.
|
||
* - `templateUrl` - `{string=}` A path to an html template.
|
||
* - `transformTemplate` - `{function(template)=}` A function which transforms the template after
|
||
* it is loaded. It will be given the template string as a parameter, and should
|
||
* return a a new string representing the transformed template.
|
||
* - `resolve` - `{Object.<string, function>=}` - An optional map of dependencies which should
|
||
* be injected into the controller. If any of these dependencies are promises, the compiler
|
||
* will wait for them all to be resolved, or if one is rejected before the controller is
|
||
* instantiated `compile()` will fail..
|
||
* * `key` - `{string}`: a name of a dependency to be injected into the controller.
|
||
* * `factory` - `{string|function}`: If `string` then it is an alias for a service.
|
||
* Otherwise if function, then it is injected and the return value is treated as the
|
||
* dependency. If the result is a promise, it is resolved before its value is
|
||
* injected into the controller.
|
||
*
|
||
* @returns {Q.Promise<{element: JQLite, link: Function, locals: Object, cleanup: any,
|
||
* controller: Object=}>} promise A promise, which will be resolved with a `compileData` object.
|
||
* `compileData` has the following properties:
|
||
*
|
||
* - `element` - `{JQLite}`: an uncompiled element matching the provided template.
|
||
* - `link` - `{function(scope)}`: A link function, which, when called, will compile
|
||
* the element and instantiate the provided controller (if given).
|
||
* - `locals` - `{Object}`: The locals which will be passed into the controller once `link` is
|
||
* called. If `bindToController` is true, they will be copied to the ctrl instead
|
||
*/
|
||
MdCompilerService.prototype.compile = function(options) {
|
||
if (options.contentElement) {
|
||
return this._prepareContentElement(options);
|
||
} else {
|
||
return this._compileTemplate(options);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Instead of compiling any template, the compiler just fetches an existing HTML element from the
|
||
* DOM and provides a restore function to put the element back it old DOM position.
|
||
* @param {!Object} options Options to be used for the compiler.
|
||
* @returns {Q.Promise<{element: JQLite, link: Function, locals: Object, cleanup: any}>}
|
||
*/
|
||
MdCompilerService.prototype._prepareContentElement = function(options) {
|
||
|
||
var contentElement = this._fetchContentElement(options);
|
||
|
||
return this.$q.resolve({
|
||
element: contentElement.element,
|
||
cleanup: contentElement.restore,
|
||
locals: {},
|
||
link: function() {
|
||
return contentElement.element;
|
||
}
|
||
});
|
||
|
||
};
|
||
|
||
/**
|
||
* Compiles a template by considering all options and waiting for all resolves to be ready.
|
||
* @param {!Object} options Compile options
|
||
* @returns {!Q.Promise<{element: JQLite, link: Function, locals: Object, cleanup: any}>} Compile
|
||
* data with link function.
|
||
*/
|
||
MdCompilerService.prototype._compileTemplate = function(options) {
|
||
|
||
var self = this;
|
||
var templateUrl = options.templateUrl;
|
||
var template = options.template || '';
|
||
var resolve = angular.extend({}, options.resolve);
|
||
var locals = angular.extend({}, options.locals);
|
||
var transformTemplate = options.transformTemplate || angular.identity;
|
||
|
||
// Take resolve values and invoke them.
|
||
// Resolves can either be a string (value: 'MyRegisteredAngularConst'),
|
||
// or an invokable 'factory' of sorts: (value: function ValueGetter($dependency) {})
|
||
angular.forEach(resolve, function(value, key) {
|
||
if (angular.isString(value)) {
|
||
resolve[key] = self.$injector.get(value);
|
||
} else {
|
||
resolve[key] = self.$injector.invoke(value);
|
||
}
|
||
});
|
||
|
||
// Add the locals, which are just straight values to inject
|
||
// eg locals: { three: 3 }, will inject three into the controller
|
||
angular.extend(resolve, locals);
|
||
|
||
if (templateUrl) {
|
||
resolve.$$ngTemplate = this.$templateRequest(templateUrl);
|
||
} else {
|
||
resolve.$$ngTemplate = this.$q.when(template);
|
||
}
|
||
|
||
|
||
// Wait for all the resolves to finish if they are promises
|
||
return this.$q.all(resolve).then(function(locals) {
|
||
|
||
var template = transformTemplate(locals.$$ngTemplate, options);
|
||
var element = options.element || angular.element('<div>').html(template.trim()).contents();
|
||
|
||
return self._compileElement(locals, element, options);
|
||
});
|
||
|
||
};
|
||
|
||
/**
|
||
* Method to compile an element with the given options.
|
||
* @param {!Object} locals Locals to be injected to the controller if present
|
||
* @param {!JQLite} element Element to be compiled and linked
|
||
* @param {!Object} options Options to be used for linking.
|
||
* @returns {!{element: JQLite, link: Function, locals: Object, cleanup: any, controller: Object}} Compile data with link function.
|
||
*/
|
||
MdCompilerService.prototype._compileElement = function(locals, element, options) {
|
||
var self = this;
|
||
var ngLinkFn = this.$compile(element);
|
||
|
||
var compileData = {
|
||
element: element,
|
||
cleanup: element.remove.bind(element),
|
||
locals: locals,
|
||
link: linkFn
|
||
};
|
||
|
||
function linkFn(scope) {
|
||
locals.$scope = scope;
|
||
|
||
// Instantiate controller if the developer provided one.
|
||
if (options.controller) {
|
||
|
||
var injectLocals = angular.extend({}, locals, {
|
||
$element: element
|
||
});
|
||
|
||
// Create the specified controller instance.
|
||
var ctrl = self._createController(options, injectLocals, locals);
|
||
|
||
// Registering extra $destroy listeners should be avoided.
|
||
// Only register the listener if the controller implements a $onDestroy hook.
|
||
if (angular.isFunction(ctrl.$onDestroy)) {
|
||
scope.$on('$destroy', function() {
|
||
// Call the $onDestroy hook if it's present on the controller.
|
||
angular.isFunction(ctrl.$onDestroy) && ctrl.$onDestroy();
|
||
});
|
||
}
|
||
|
||
// Unique identifier for AngularJS Route ngView controllers.
|
||
element.data('$ngControllerController', ctrl);
|
||
element.children().data('$ngControllerController', ctrl);
|
||
|
||
// Expose the instantiated controller to the compile data
|
||
compileData.controller = ctrl;
|
||
}
|
||
|
||
// Invoke the AngularJS $compile link function.
|
||
return ngLinkFn(scope);
|
||
}
|
||
|
||
return compileData;
|
||
|
||
};
|
||
|
||
/**
|
||
* Creates and instantiates a new controller with the specified options.
|
||
* @param {!Object} options Options that include the controller function or string.
|
||
* @param {!Object} injectLocals Locals to to be provided in the controller DI.
|
||
* @param {!Object} locals Locals to be injected to the controller.
|
||
* @returns {!Object} Created controller instance.
|
||
*/
|
||
MdCompilerService.prototype._createController = function(options, injectLocals, locals) {
|
||
var ctrl = this.$controller(options.controller, injectLocals);
|
||
|
||
if (options.bindToController) {
|
||
angular.extend(ctrl, locals);
|
||
}
|
||
|
||
if (options.controllerAs) {
|
||
injectLocals.$scope[options.controllerAs] = ctrl;
|
||
}
|
||
|
||
// Call the $onInit hook if it's present on the controller.
|
||
angular.isFunction(ctrl.$onInit) && ctrl.$onInit();
|
||
|
||
return ctrl;
|
||
};
|
||
|
||
/**
|
||
* Fetches an element removing it from the DOM and using it temporary for the compiler.
|
||
* Elements which were fetched will be restored after use.
|
||
* @param {!Object} options Options to be used for the compilation.
|
||
* @returns {{element: !JQLite, restore: !function}}
|
||
*/
|
||
MdCompilerService.prototype._fetchContentElement = function(options) {
|
||
var contentEl = options.contentElement;
|
||
var restoreFn;
|
||
|
||
if (angular.isString(contentEl)) {
|
||
contentEl = document.querySelector(contentEl);
|
||
restoreFn = createRestoreFn(contentEl);
|
||
} else {
|
||
contentEl = contentEl[0] || contentEl;
|
||
|
||
// When the element is visible in the DOM, then we restore it at close of the dialog.
|
||
// Otherwise it will be removed from the DOM after close.
|
||
if (document.contains(contentEl)) {
|
||
restoreFn = createRestoreFn(contentEl);
|
||
} else {
|
||
restoreFn = function() {
|
||
if (contentEl.parentNode) {
|
||
contentEl.parentNode.removeChild(contentEl);
|
||
}
|
||
};
|
||
}
|
||
}
|
||
|
||
return {
|
||
element: angular.element(contentEl),
|
||
restore: restoreFn
|
||
};
|
||
|
||
function createRestoreFn(element) {
|
||
var parent = element.parentNode;
|
||
var nextSibling = element.nextElementSibling;
|
||
|
||
return function() {
|
||
if (!nextSibling) {
|
||
// When the element didn't had any sibling, then it can be simply appended to the
|
||
// parent, because it plays no role, which index it had before.
|
||
parent.appendChild(element);
|
||
} else {
|
||
// When the element had a sibling, which marks the previous position of the element
|
||
// in the DOM, we insert it correctly before the sibling, to have the same index as
|
||
// before.
|
||
parent.insertBefore(element, nextSibling);
|
||
}
|
||
};
|
||
}
|
||
};
|
||
}
|
||
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
|
||
MdGesture.$inject = ["$$MdGestureHandler", "$$rAF", "$timeout", "$mdUtil"];
|
||
attachToDocument.$inject = ["$mdGesture", "$$MdGestureHandler", "$mdUtil"];var HANDLERS = {};
|
||
|
||
/**
|
||
* The state of the current 'pointer'. The pointer represents the state of the current touch.
|
||
* It contains normalized x and y coordinates from DOM events,
|
||
* as well as other information abstracted from the DOM.
|
||
*/
|
||
var pointer, lastPointer, maxClickDistance = 6;
|
||
var forceSkipClickHijack = false, disableAllGestures = false;
|
||
|
||
/**
|
||
* The position of the most recent click if that click was on a label element.
|
||
* @type {{x: number, y: number}|null}
|
||
*/
|
||
var lastLabelClickPos = null;
|
||
|
||
/**
|
||
* Used to attach event listeners once when multiple ng-apps are running.
|
||
* @type {boolean}
|
||
*/
|
||
var isInitialized = false;
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.core.gestures
|
||
* @description
|
||
* AngularJS Material Gesture handling for touch devices.
|
||
* This module replaced the usage of the HammerJS library.
|
||
*/
|
||
angular
|
||
.module('material.core.gestures', [])
|
||
.provider('$mdGesture', MdGestureProvider)
|
||
.factory('$$MdGestureHandler', MdGestureHandler)
|
||
.run(attachToDocument);
|
||
|
||
/**
|
||
* @ngdoc service
|
||
* @name $mdGestureProvider
|
||
* @module material.core.gestures
|
||
*
|
||
* @description
|
||
* In some scenarios on mobile devices (without jQuery), the click events should NOT be hijacked.
|
||
* `$mdGestureProvider` is used to configure the Gesture module to ignore or skip click hijacking
|
||
* on mobile devices.
|
||
*
|
||
* You can also change the max click distance, `6px` by default, if you have issues on some touch
|
||
* screens.
|
||
*
|
||
* <hljs lang="js">
|
||
* app.config(function($mdGestureProvider) {
|
||
*
|
||
* // For mobile devices without jQuery loaded, do not
|
||
* // intercept click events during the capture phase.
|
||
* $mdGestureProvider.skipClickHijack();
|
||
*
|
||
* // If hijacking clicks, you may want to change the default click distance
|
||
* $mdGestureProvider.setMaxClickDistance(12);
|
||
* });
|
||
* </hljs>
|
||
*
|
||
*/
|
||
function MdGestureProvider() { }
|
||
|
||
MdGestureProvider.prototype = {
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdGestureProvider#disableAll
|
||
*
|
||
* @description
|
||
* Disable all gesture detection. This can be beneficial to application performance
|
||
* and memory usage.
|
||
*/
|
||
disableAll: function () {
|
||
disableAllGestures = true;
|
||
},
|
||
|
||
// Publish access to setter to configure a variable BEFORE the
|
||
// $mdGesture service is instantiated...
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdGestureProvider#skipClickHijack
|
||
*
|
||
* @description
|
||
* Tell the AngularJS Material Gesture module to skip (or ignore) click hijacking on mobile devices.
|
||
*/
|
||
skipClickHijack: function() {
|
||
return forceSkipClickHijack = true;
|
||
},
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdGestureProvider#setMaxClickDistance
|
||
* @param clickDistance {string} Distance in pixels. I.e. `12px`.
|
||
* @description
|
||
* Set the max distance from the origin of the touch event to trigger touch handlers.
|
||
*/
|
||
setMaxClickDistance: function(clickDistance) {
|
||
maxClickDistance = parseInt(clickDistance);
|
||
},
|
||
|
||
/**
|
||
* $get is used to build an instance of $mdGesture
|
||
* @ngInject
|
||
*/
|
||
$get : ["$$MdGestureHandler", "$$rAF", "$timeout", "$mdUtil", function($$MdGestureHandler, $$rAF, $timeout, $mdUtil) {
|
||
return new MdGesture($$MdGestureHandler, $$rAF, $timeout, $mdUtil);
|
||
}]
|
||
};
|
||
|
||
|
||
|
||
/**
|
||
* MdGesture factory construction function
|
||
* @ngInject
|
||
*/
|
||
function MdGesture($$MdGestureHandler, $$rAF, $timeout, $mdUtil) {
|
||
var touchActionProperty = $mdUtil.getTouchAction();
|
||
var hasJQuery = (typeof window.jQuery !== 'undefined') && (angular.element === window.jQuery);
|
||
|
||
var self = {
|
||
handler: addHandler,
|
||
register: register,
|
||
isAndroid: $mdUtil.isAndroid,
|
||
isIos: $mdUtil.isIos,
|
||
// On mobile w/out jQuery, we normally intercept clicks. Should we skip that?
|
||
isHijackingClicks: ($mdUtil.isIos || $mdUtil.isAndroid) && !hasJQuery && !forceSkipClickHijack
|
||
};
|
||
|
||
if (self.isHijackingClicks) {
|
||
self.handler('click', {
|
||
options: {
|
||
maxDistance: maxClickDistance
|
||
},
|
||
onEnd: checkDistanceAndEmit('click')
|
||
});
|
||
|
||
self.handler('focus', {
|
||
options: {
|
||
maxDistance: maxClickDistance
|
||
},
|
||
onEnd: function(ev, pointer) {
|
||
if (pointer.distance < this.state.options.maxDistance && canFocus(ev.target)) {
|
||
this.dispatchEvent(ev, 'focus', pointer);
|
||
ev.target.focus();
|
||
}
|
||
}
|
||
});
|
||
|
||
self.handler('mouseup', {
|
||
options: {
|
||
maxDistance: maxClickDistance
|
||
},
|
||
onEnd: checkDistanceAndEmit('mouseup')
|
||
});
|
||
|
||
self.handler('mousedown', {
|
||
onStart: function(ev) {
|
||
this.dispatchEvent(ev, 'mousedown');
|
||
}
|
||
});
|
||
}
|
||
|
||
function checkDistanceAndEmit(eventName) {
|
||
return function(ev, pointer) {
|
||
if (pointer.distance < this.state.options.maxDistance) {
|
||
this.dispatchEvent(ev, eventName, pointer);
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Register an element to listen for a handler.
|
||
* This allows an element to override the default options for a handler.
|
||
* Additionally, some handlers like drag and hold only dispatch events if
|
||
* the domEvent happens inside an element that's registered to listen for these events.
|
||
*
|
||
* @see GestureHandler for how overriding of default options works.
|
||
* @example $mdGesture.register(myElement, 'drag', { minDistance: 20, horizontal: false })
|
||
*/
|
||
function register(element, handlerName, options) {
|
||
var handler = HANDLERS[handlerName.replace(/^\$md./, '')];
|
||
if (!handler) {
|
||
throw new Error('Failed to register element with handler ' + handlerName + '. ' +
|
||
'Available handlers: ' + Object.keys(HANDLERS).join(', '));
|
||
}
|
||
return handler.registerElement(element, options);
|
||
}
|
||
|
||
/*
|
||
* add a handler to $mdGesture. see below.
|
||
*/
|
||
function addHandler(name, definition) {
|
||
var handler = new $$MdGestureHandler(name);
|
||
angular.extend(handler, definition);
|
||
HANDLERS[name] = handler;
|
||
|
||
return self;
|
||
}
|
||
|
||
/**
|
||
* Register handlers. These listen to touch/start/move events, interpret them,
|
||
* and dispatch gesture events depending on options & conditions. These are all
|
||
* instances of GestureHandler.
|
||
* @see GestureHandler
|
||
*/
|
||
return self
|
||
/*
|
||
* The press handler dispatches an event on touchdown/touchend.
|
||
* It's a simple abstraction of touch/mouse/pointer start and end.
|
||
*/
|
||
.handler('press', {
|
||
onStart: function (ev, pointer) {
|
||
this.dispatchEvent(ev, '$md.pressdown');
|
||
},
|
||
onEnd: function (ev, pointer) {
|
||
this.dispatchEvent(ev, '$md.pressup');
|
||
}
|
||
})
|
||
|
||
/*
|
||
* The hold handler dispatches an event if the user keeps their finger within
|
||
* the same <maxDistance> area for <delay> ms.
|
||
* The hold handler will only run if a parent of the touch target is registered
|
||
* to listen for hold events through $mdGesture.register()
|
||
*/
|
||
.handler('hold', {
|
||
options: {
|
||
maxDistance: 6,
|
||
delay: 500
|
||
},
|
||
onCancel: function () {
|
||
$timeout.cancel(this.state.timeout);
|
||
},
|
||
onStart: function (ev, pointer) {
|
||
// For hold, require a parent to be registered with $mdGesture.register()
|
||
// Because we prevent scroll events, this is necessary.
|
||
if (!this.state.registeredParent) return this.cancel();
|
||
|
||
this.state.pos = {x: pointer.x, y: pointer.y};
|
||
this.state.timeout = $timeout(angular.bind(this, function holdDelayFn() {
|
||
this.dispatchEvent(ev, '$md.hold');
|
||
this.cancel(); // we're done!
|
||
}), this.state.options.delay, false);
|
||
},
|
||
onMove: function (ev, pointer) {
|
||
// Don't scroll while waiting for hold.
|
||
// If we don't preventDefault touchmove events here, Android will assume we don't
|
||
// want to listen to anymore touch events. It will start scrolling and stop sending
|
||
// touchmove events.
|
||
if (!touchActionProperty && ev.type === 'touchmove') ev.preventDefault();
|
||
|
||
// If the user moves greater than <maxDistance> pixels, stop the hold timer
|
||
// set in onStart
|
||
var dx = this.state.pos.x - pointer.x;
|
||
var dy = this.state.pos.y - pointer.y;
|
||
if (Math.sqrt(dx * dx + dy * dy) > this.options.maxDistance) {
|
||
this.cancel();
|
||
}
|
||
},
|
||
onEnd: function () {
|
||
this.onCancel();
|
||
}
|
||
})
|
||
|
||
/*
|
||
* The drag handler dispatches a drag event if the user holds and moves his finger greater than
|
||
* <minDistance> px in the x or y direction, depending on options.horizontal.
|
||
* The drag will be cancelled if the user moves his finger greater than <minDistance>*<cancelMultiplier> in
|
||
* the perpendicular direction. Eg if the drag is horizontal and the user moves his finger <minDistance>*<cancelMultiplier>
|
||
* pixels vertically, this handler won't consider the move part of a drag.
|
||
*/
|
||
.handler('drag', {
|
||
options: {
|
||
minDistance: 6,
|
||
horizontal: true,
|
||
cancelMultiplier: 1.5
|
||
},
|
||
/**
|
||
* @param {angular.JQLite} element where touch action styles need to be adjusted
|
||
* @param {{horizontal: boolean}=} options object whose horizontal property can specify to
|
||
* apply 'pan-y' or 'pan-x' touch actions.
|
||
*/
|
||
onSetup: function(element, options) {
|
||
if (touchActionProperty) {
|
||
// We check for horizontal to be false, because otherwise we would overwrite the default opts.
|
||
this.oldTouchAction = element[0].style[touchActionProperty];
|
||
element[0].style[touchActionProperty] = options.horizontal ? 'pan-y' : 'pan-x';
|
||
}
|
||
},
|
||
/**
|
||
* @param {angular.JQLite} element where styles need to be cleaned up
|
||
*/
|
||
onCleanup: function(element) {
|
||
if (this.oldTouchAction) {
|
||
element[0].style[touchActionProperty] = this.oldTouchAction;
|
||
} else {
|
||
element[0].style[touchActionProperty] = null;
|
||
}
|
||
},
|
||
onStart: function (ev) {
|
||
// For drag, require a parent to be registered with $mdGesture.register()
|
||
if (!this.state.registeredParent) this.cancel();
|
||
},
|
||
onMove: function (ev, pointer) {
|
||
var shouldStartDrag, shouldCancel;
|
||
// Don't scroll while deciding if this touchmove qualifies as a drag event.
|
||
// If we don't preventDefault touchmove events here, Android will assume we don't
|
||
// want to listen to anymore touch events. It will start scrolling and stop sending
|
||
// touchmove events.
|
||
if (!touchActionProperty && ev.type === 'touchmove') ev.preventDefault();
|
||
|
||
if (!this.state.dragPointer) {
|
||
if (this.state.options.horizontal) {
|
||
shouldStartDrag = Math.abs(pointer.distanceX) > this.state.options.minDistance;
|
||
shouldCancel = Math.abs(pointer.distanceY) > this.state.options.minDistance * this.state.options.cancelMultiplier;
|
||
} else {
|
||
shouldStartDrag = Math.abs(pointer.distanceY) > this.state.options.minDistance;
|
||
shouldCancel = Math.abs(pointer.distanceX) > this.state.options.minDistance * this.state.options.cancelMultiplier;
|
||
}
|
||
|
||
if (shouldStartDrag) {
|
||
// Create a new pointer representing this drag, starting at this point where the drag started.
|
||
this.state.dragPointer = makeStartPointer(ev);
|
||
updatePointerState(ev, this.state.dragPointer);
|
||
this.dispatchEvent(ev, '$md.dragstart', this.state.dragPointer);
|
||
|
||
} else if (shouldCancel) {
|
||
this.cancel();
|
||
}
|
||
} else {
|
||
this.dispatchDragMove(ev);
|
||
}
|
||
},
|
||
// Only dispatch dragmove events every frame; any more is unnecessary
|
||
dispatchDragMove: $$rAF.throttle(function (ev) {
|
||
// Make sure the drag didn't stop while waiting for the next frame
|
||
if (this.state.isRunning) {
|
||
updatePointerState(ev, this.state.dragPointer);
|
||
this.dispatchEvent(ev, '$md.drag', this.state.dragPointer);
|
||
}
|
||
}),
|
||
onEnd: function (ev, pointer) {
|
||
if (this.state.dragPointer) {
|
||
updatePointerState(ev, this.state.dragPointer);
|
||
this.dispatchEvent(ev, '$md.dragend', this.state.dragPointer);
|
||
}
|
||
}
|
||
})
|
||
|
||
/*
|
||
* The swipe handler will dispatch a swipe event if, on the end of a touch,
|
||
* the velocity and distance were high enough.
|
||
*/
|
||
.handler('swipe', {
|
||
options: {
|
||
minVelocity: 0.65,
|
||
minDistance: 10
|
||
},
|
||
onEnd: function (ev, pointer) {
|
||
var eventType;
|
||
|
||
if (Math.abs(pointer.velocityX) > this.state.options.minVelocity &&
|
||
Math.abs(pointer.distanceX) > this.state.options.minDistance) {
|
||
eventType = pointer.directionX == 'left' ? '$md.swipeleft' : '$md.swiperight';
|
||
this.dispatchEvent(ev, eventType);
|
||
}
|
||
else if (Math.abs(pointer.velocityY) > this.state.options.minVelocity &&
|
||
Math.abs(pointer.distanceY) > this.state.options.minDistance) {
|
||
eventType = pointer.directionY == 'up' ? '$md.swipeup' : '$md.swipedown';
|
||
this.dispatchEvent(ev, eventType);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* MdGestureHandler
|
||
* A GestureHandler is an object which is able to dispatch custom dom events
|
||
* based on native dom {touch,pointer,mouse}{start,move,end} events.
|
||
*
|
||
* A gesture will manage its lifecycle through the start,move,end, and cancel
|
||
* functions, which are called by native dom events.
|
||
*
|
||
* A gesture has the concept of 'options' (eg. a swipe's required velocity), which can be
|
||
* overridden by elements registering through $mdGesture.register().
|
||
*/
|
||
function GestureHandler (name) {
|
||
this.name = name;
|
||
this.state = {};
|
||
}
|
||
|
||
function MdGestureHandler() {
|
||
var hasJQuery = (typeof window.jQuery !== 'undefined') && (angular.element === window.jQuery);
|
||
|
||
GestureHandler.prototype = {
|
||
options: {},
|
||
// jQuery listeners don't work with custom DOMEvents, so we have to dispatch events
|
||
// differently when jQuery is loaded
|
||
dispatchEvent: hasJQuery ? jQueryDispatchEvent : nativeDispatchEvent,
|
||
|
||
// These are overridden by the registered handler
|
||
onSetup: angular.noop,
|
||
onCleanup: angular.noop,
|
||
onStart: angular.noop,
|
||
onMove: angular.noop,
|
||
onEnd: angular.noop,
|
||
onCancel: angular.noop,
|
||
|
||
// onStart sets up a new state for the handler, which includes options from the
|
||
// nearest registered parent element of ev.target.
|
||
start: function (ev, pointer) {
|
||
if (this.state.isRunning) return;
|
||
var parentTarget = this.getNearestParent(ev.target);
|
||
// Get the options from the nearest registered parent
|
||
var parentTargetOptions = parentTarget && parentTarget.$mdGesture[this.name] || {};
|
||
|
||
this.state = {
|
||
isRunning: true,
|
||
// Override the default options with the nearest registered parent's options
|
||
options: angular.extend({}, this.options, parentTargetOptions),
|
||
// Pass in the registered parent node to the state so the onStart listener can use
|
||
registeredParent: parentTarget
|
||
};
|
||
this.onStart(ev, pointer);
|
||
},
|
||
move: function (ev, pointer) {
|
||
if (!this.state.isRunning) return;
|
||
this.onMove(ev, pointer);
|
||
},
|
||
end: function (ev, pointer) {
|
||
if (!this.state.isRunning) return;
|
||
this.state.isRunning = false;
|
||
this.onEnd(ev, pointer);
|
||
},
|
||
cancel: function (ev, pointer) {
|
||
this.onCancel(ev, pointer);
|
||
this.state = {};
|
||
},
|
||
|
||
// Find and return the nearest parent element that has been registered to
|
||
// listen for this handler via $mdGesture.register(element, 'handlerName').
|
||
getNearestParent: function (node) {
|
||
var current = node;
|
||
while (current) {
|
||
if ((current.$mdGesture || {})[this.name]) {
|
||
return current;
|
||
}
|
||
current = current.parentNode;
|
||
}
|
||
return null;
|
||
},
|
||
|
||
// Called from $mdGesture.register when an element registers itself with a handler.
|
||
// Store the options the user gave on the DOMElement itself. These options will
|
||
// be retrieved with getNearestParent when the handler starts.
|
||
registerElement: function (element, options) {
|
||
var self = this;
|
||
element[0].$mdGesture = element[0].$mdGesture || {};
|
||
element[0].$mdGesture[this.name] = options || {};
|
||
element.on('$destroy', onDestroy);
|
||
|
||
self.onSetup(element, options || {});
|
||
|
||
return onDestroy;
|
||
|
||
function onDestroy() {
|
||
delete element[0].$mdGesture[self.name];
|
||
element.off('$destroy', onDestroy);
|
||
|
||
self.onCleanup(element, options || {});
|
||
}
|
||
}
|
||
};
|
||
|
||
return GestureHandler;
|
||
|
||
/**
|
||
* Dispatch an event with jQuery
|
||
* TODO: Make sure this sends bubbling events
|
||
*
|
||
* @param srcEvent the original DOM touch event that started this.
|
||
* @param eventType the name of the custom event to send (eg 'click' or '$md.drag')
|
||
* @param eventPointer the pointer object that matches this event.
|
||
*/
|
||
function jQueryDispatchEvent(srcEvent, eventType, eventPointer) {
|
||
eventPointer = eventPointer || pointer;
|
||
var eventObj = new angular.element.Event(eventType);
|
||
|
||
eventObj.$material = true;
|
||
eventObj.pointer = eventPointer;
|
||
eventObj.srcEvent = srcEvent;
|
||
|
||
angular.extend(eventObj, {
|
||
clientX: eventPointer.x,
|
||
clientY: eventPointer.y,
|
||
screenX: eventPointer.x,
|
||
screenY: eventPointer.y,
|
||
pageX: eventPointer.x,
|
||
pageY: eventPointer.y,
|
||
ctrlKey: srcEvent.ctrlKey,
|
||
altKey: srcEvent.altKey,
|
||
shiftKey: srcEvent.shiftKey,
|
||
metaKey: srcEvent.metaKey
|
||
});
|
||
angular.element(eventPointer.target).trigger(eventObj);
|
||
}
|
||
|
||
/**
|
||
* NOTE: nativeDispatchEvent is very performance sensitive.
|
||
* @param srcEvent the original DOM touch event that started this.
|
||
* @param eventType the name of the custom event to send (eg 'click' or '$md.drag')
|
||
* @param eventPointer the pointer object that matches this event.
|
||
*/
|
||
function nativeDispatchEvent(srcEvent, eventType, eventPointer) {
|
||
eventPointer = eventPointer || pointer;
|
||
var eventObj;
|
||
|
||
if (eventType === 'click' || eventType === 'mouseup' || eventType === 'mousedown') {
|
||
if (typeof window.MouseEvent === "function") {
|
||
eventObj = new MouseEvent(eventType, {
|
||
bubbles: true,
|
||
cancelable: true,
|
||
screenX: Number(srcEvent.screenX),
|
||
screenY: Number(srcEvent.screenY),
|
||
clientX: Number(eventPointer.x),
|
||
clientY: Number(eventPointer.y),
|
||
ctrlKey: srcEvent.ctrlKey,
|
||
altKey: srcEvent.altKey,
|
||
shiftKey: srcEvent.shiftKey,
|
||
metaKey: srcEvent.metaKey,
|
||
button: srcEvent.button,
|
||
buttons: srcEvent.buttons,
|
||
relatedTarget: srcEvent.relatedTarget || null
|
||
});
|
||
} else {
|
||
eventObj = document.createEvent('MouseEvents');
|
||
// This has been deprecated
|
||
// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/initMouseEvent
|
||
eventObj.initMouseEvent(
|
||
eventType, true, true, window, srcEvent.detail,
|
||
eventPointer.x, eventPointer.y, eventPointer.x, eventPointer.y,
|
||
srcEvent.ctrlKey, srcEvent.altKey, srcEvent.shiftKey, srcEvent.metaKey,
|
||
srcEvent.button, srcEvent.relatedTarget || null
|
||
);
|
||
}
|
||
} else {
|
||
if (typeof window.CustomEvent === "function") {
|
||
eventObj = new CustomEvent(eventType, {
|
||
bubbles: true,
|
||
cancelable: true,
|
||
detail: {}
|
||
});
|
||
} else {
|
||
// This has been deprecated
|
||
// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/initCustomEvent
|
||
eventObj = document.createEvent('CustomEvent');
|
||
eventObj.initCustomEvent(eventType, true, true, {});
|
||
}
|
||
}
|
||
eventObj.$material = true;
|
||
eventObj.pointer = eventPointer;
|
||
eventObj.srcEvent = srcEvent;
|
||
eventPointer.target.dispatchEvent(eventObj);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Attach Gestures: hook document and check shouldHijack clicks
|
||
* @ngInject
|
||
*/
|
||
function attachToDocument($mdGesture, $$MdGestureHandler, $mdUtil) {
|
||
if (disableAllGestures) {
|
||
return;
|
||
}
|
||
|
||
if (!isInitialized && $mdGesture.isHijackingClicks) {
|
||
/*
|
||
* If hijack clicks is true, we preventDefault any click that wasn't
|
||
* sent by AngularJS Material. This is because on older Android & iOS, a false, or 'ghost',
|
||
* click event will be sent ~400ms after a touchend event happens.
|
||
* The only way to know if this click is real is to prevent any normal
|
||
* click events, and add a flag to events sent by material so we know not to prevent those.
|
||
*
|
||
* Two exceptions to click events that should be prevented are:
|
||
* - click events sent by the keyboard (eg form submit)
|
||
* - events that originate from an Ionic app
|
||
*/
|
||
document.addEventListener('click' , clickHijacker , true);
|
||
document.addEventListener('mouseup' , mouseInputHijacker, true);
|
||
document.addEventListener('mousedown', mouseInputHijacker, true);
|
||
document.addEventListener('focus' , mouseInputHijacker, true);
|
||
|
||
isInitialized = true;
|
||
}
|
||
|
||
function mouseInputHijacker(ev) {
|
||
var isKeyClick = !ev.clientX && !ev.clientY;
|
||
|
||
if (
|
||
!isKeyClick &&
|
||
!ev.$material &&
|
||
!ev.isIonicTap &&
|
||
!isInputEventFromLabelClick(ev) &&
|
||
(ev.type !== 'mousedown' || (!canFocus(ev.target) && !canFocus(document.activeElement)))
|
||
) {
|
||
ev.preventDefault();
|
||
ev.stopPropagation();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Ignore click events that don't come from AngularJS Material, Ionic, Input Label clicks,
|
||
* or key presses that generate click events. This helps to ignore the ghost tap events on
|
||
* older mobile browsers that get sent after a 300-400ms delay.
|
||
* @param ev MouseEvent or modified MouseEvent with $material, pointer, and other fields
|
||
*/
|
||
function clickHijacker(ev) {
|
||
var isKeyClick;
|
||
if ($mdUtil.isIos) {
|
||
isKeyClick = angular.isDefined(ev.webkitForce) && ev.webkitForce === 0;
|
||
} else {
|
||
isKeyClick = ev.clientX === 0 && ev.clientY === 0;
|
||
}
|
||
if (!isKeyClick && !ev.$material && !ev.isIonicTap && !isInputEventFromLabelClick(ev)) {
|
||
ev.preventDefault();
|
||
ev.stopPropagation();
|
||
lastLabelClickPos = null;
|
||
} else {
|
||
lastLabelClickPos = null;
|
||
if (ev.target.tagName.toLowerCase() === 'label') {
|
||
lastLabelClickPos = {x: ev.x, y: ev.y};
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
// Listen to all events to cover all platforms.
|
||
var START_EVENTS = 'mousedown touchstart pointerdown';
|
||
var MOVE_EVENTS = 'mousemove touchmove pointermove';
|
||
var END_EVENTS = 'mouseup mouseleave touchend touchcancel pointerup pointercancel';
|
||
|
||
angular.element(document)
|
||
.on(START_EVENTS, gestureStart)
|
||
.on(MOVE_EVENTS, gestureMove)
|
||
.on(END_EVENTS, gestureEnd)
|
||
// For testing
|
||
.on('$$mdGestureReset', function gestureClearCache () {
|
||
lastPointer = pointer = null;
|
||
});
|
||
|
||
/**
|
||
* When a DOM event happens, run all registered gesture handlers' lifecycle
|
||
* methods which match the DOM event.
|
||
* Eg. when a 'touchstart' event happens, runHandlers('start') will call and
|
||
* run `handler.cancel()` and `handler.start()` on all registered handlers.
|
||
*/
|
||
function runHandlers(handlerEvent, event) {
|
||
var handler;
|
||
for (var name in HANDLERS) {
|
||
handler = HANDLERS[name];
|
||
if (handler instanceof $$MdGestureHandler) {
|
||
|
||
if (handlerEvent === 'start') {
|
||
// Run cancel to reset any handlers' state
|
||
handler.cancel();
|
||
}
|
||
handler[handlerEvent](event, pointer);
|
||
}
|
||
}
|
||
}
|
||
|
||
/*
|
||
* gestureStart vets if a start event is legitimate (and not part of a 'ghost click' from iOS/Android)
|
||
* If it is legitimate, we initiate the pointer state and mark the current pointer's type
|
||
* For example, for a touchstart event, mark the current pointer as a 'touch' pointer, so mouse events
|
||
* won't effect it.
|
||
*/
|
||
function gestureStart(ev) {
|
||
// If we're already touched down, abort
|
||
if (pointer) return;
|
||
|
||
var now = +Date.now();
|
||
|
||
// iOS & old android bug: after a touch event, a click event is sent 350 ms later.
|
||
// If <400ms have passed, don't allow an event of a different type than the previous event
|
||
if (lastPointer && !typesMatch(ev, lastPointer) && (now - lastPointer.endTime < 1500)) {
|
||
return;
|
||
}
|
||
|
||
pointer = makeStartPointer(ev);
|
||
|
||
runHandlers('start', ev);
|
||
}
|
||
|
||
/**
|
||
* If a move event happens of the right type, update the pointer and run all the move handlers.
|
||
* "of the right type": if a mousemove happens but our pointer started with a touch event, do
|
||
* nothing.
|
||
*/
|
||
function gestureMove(ev) {
|
||
if (!pointer || !typesMatch(ev, pointer)) return;
|
||
|
||
updatePointerState(ev, pointer);
|
||
runHandlers('move', ev);
|
||
}
|
||
|
||
/**
|
||
* If an end event happens of the right type, update the pointer, run endHandlers, and save the
|
||
* pointer as 'lastPointer'.
|
||
*/
|
||
function gestureEnd(ev) {
|
||
if (!pointer || !typesMatch(ev, pointer)) return;
|
||
|
||
updatePointerState(ev, pointer);
|
||
pointer.endTime = +Date.now();
|
||
|
||
if (ev.type !== 'pointercancel') {
|
||
runHandlers('end', ev);
|
||
}
|
||
|
||
lastPointer = pointer;
|
||
pointer = null;
|
||
}
|
||
|
||
}
|
||
|
||
// ********************
|
||
// Module Functions
|
||
// ********************
|
||
|
||
/*
|
||
* Initiate the pointer. x, y, and the pointer's type.
|
||
*/
|
||
function makeStartPointer(ev) {
|
||
var point = getEventPoint(ev);
|
||
var startPointer = {
|
||
startTime: +Date.now(),
|
||
target: ev.target,
|
||
// 'p' for pointer events, 'm' for mouse, 't' for touch
|
||
type: ev.type.charAt(0)
|
||
};
|
||
startPointer.startX = startPointer.x = point.pageX;
|
||
startPointer.startY = startPointer.y = point.pageY;
|
||
return startPointer;
|
||
}
|
||
|
||
/*
|
||
* return whether the pointer's type matches the event's type.
|
||
* Eg if a touch event happens but the pointer has a mouse type, return false.
|
||
*/
|
||
function typesMatch(ev, pointer) {
|
||
return ev && pointer && ev.type.charAt(0) === pointer.type;
|
||
}
|
||
|
||
/**
|
||
* Gets whether the given event is an input event that was caused by clicking on an
|
||
* associated label element.
|
||
*
|
||
* This is necessary because the browser will, upon clicking on a label element, fire an
|
||
* *extra* click event on its associated input (if any). mdGesture is able to flag the label
|
||
* click as with `$material` correctly, but not the second input click.
|
||
*
|
||
* In order to determine whether an input event is from a label click, we compare the (x, y) for
|
||
* the event to the (x, y) for the most recent label click (which is cleared whenever a non-label
|
||
* click occurs). Unfortunately, there are no event properties that tie the input and the label
|
||
* together (such as relatedTarget).
|
||
*
|
||
* @param {MouseEvent} event
|
||
* @returns {boolean}
|
||
*/
|
||
function isInputEventFromLabelClick(event) {
|
||
return lastLabelClickPos
|
||
&& lastLabelClickPos.x === event.x
|
||
&& lastLabelClickPos.y === event.y;
|
||
}
|
||
|
||
/*
|
||
* Update the given pointer based upon the given DOMEvent.
|
||
* Distance, velocity, direction, duration, etc
|
||
*/
|
||
function updatePointerState(ev, pointer) {
|
||
var point = getEventPoint(ev);
|
||
var x = pointer.x = point.pageX;
|
||
var y = pointer.y = point.pageY;
|
||
|
||
pointer.distanceX = x - pointer.startX;
|
||
pointer.distanceY = y - pointer.startY;
|
||
pointer.distance = Math.sqrt(
|
||
pointer.distanceX * pointer.distanceX + pointer.distanceY * pointer.distanceY
|
||
);
|
||
|
||
pointer.directionX = pointer.distanceX > 0 ? 'right' : pointer.distanceX < 0 ? 'left' : '';
|
||
pointer.directionY = pointer.distanceY > 0 ? 'down' : pointer.distanceY < 0 ? 'up' : '';
|
||
|
||
pointer.duration = +Date.now() - pointer.startTime;
|
||
pointer.velocityX = pointer.distanceX / pointer.duration;
|
||
pointer.velocityY = pointer.distanceY / pointer.duration;
|
||
}
|
||
|
||
/**
|
||
* Normalize the point where the DOM event happened whether it's touch or mouse.
|
||
* @returns point event obj with pageX and pageY on it.
|
||
*/
|
||
function getEventPoint(ev) {
|
||
ev = ev.originalEvent || ev; // support jQuery events
|
||
return (ev.touches && ev.touches[0]) ||
|
||
(ev.changedTouches && ev.changedTouches[0]) ||
|
||
ev;
|
||
}
|
||
|
||
/** Checks whether an element can be focused. */
|
||
function canFocus(element) {
|
||
return (
|
||
!!element &&
|
||
element.getAttribute('tabindex') !== '-1' &&
|
||
!element.hasAttribute('disabled') &&
|
||
(
|
||
element.hasAttribute('tabindex') ||
|
||
element.hasAttribute('href') ||
|
||
element.isContentEditable ||
|
||
['INPUT', 'SELECT', 'BUTTON', 'TEXTAREA', 'VIDEO', 'AUDIO'].indexOf(element.nodeName) !== -1
|
||
)
|
||
);
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.core.interaction
|
||
* @description
|
||
* User interaction detection to provide proper accessibility.
|
||
*/
|
||
MdInteractionService.$inject = ["$timeout", "$mdUtil", "$rootScope"];
|
||
angular
|
||
.module('material.core.interaction', [])
|
||
.service('$mdInteraction', MdInteractionService);
|
||
|
||
|
||
/**
|
||
* @ngdoc service
|
||
* @name $mdInteraction
|
||
* @module material.core.interaction
|
||
*
|
||
* @description
|
||
*
|
||
* Service which keeps track of the last interaction type and validates them for several browsers.
|
||
* The service hooks into the document's body and listens for touch, mouse and keyboard events.
|
||
*
|
||
* The most recent interaction type can be retrieved by calling the `getLastInteractionType` method.
|
||
*
|
||
* Here is an example markup for using the interaction service.
|
||
*
|
||
* <hljs lang="js">
|
||
* var lastType = $mdInteraction.getLastInteractionType();
|
||
*
|
||
* if (lastType === 'keyboard') {
|
||
* // We only restore the focus for keyboard users.
|
||
* restoreFocus();
|
||
* }
|
||
* </hljs>
|
||
*
|
||
*/
|
||
function MdInteractionService($timeout, $mdUtil, $rootScope) {
|
||
this.$timeout = $timeout;
|
||
this.$mdUtil = $mdUtil;
|
||
this.$rootScope = $rootScope;
|
||
|
||
// IE browsers can also trigger pointer events, which also leads to an interaction.
|
||
this.pointerEvent = 'MSPointerEvent' in window ? 'MSPointerDown' : 'PointerEvent' in window ? 'pointerdown' : null;
|
||
this.bodyElement = angular.element(document.body);
|
||
this.isBuffering = false;
|
||
this.bufferTimeout = null;
|
||
this.lastInteractionType = null;
|
||
this.lastInteractionTime = null;
|
||
this.inputHandler = this.onInputEvent.bind(this);
|
||
this.bufferedInputHandler = this.onBufferInputEvent.bind(this);
|
||
|
||
// Type Mappings for the different events
|
||
// There will be three three interaction types
|
||
// `keyboard`, `mouse` and `touch`
|
||
// type `pointer` will be evaluated in `pointerMap` for IE Browser events
|
||
this.inputEventMap = {
|
||
'keydown': 'keyboard',
|
||
'mousedown': 'mouse',
|
||
'mouseenter': 'mouse',
|
||
'touchstart': 'touch',
|
||
'pointerdown': 'pointer',
|
||
'MSPointerDown': 'pointer'
|
||
};
|
||
|
||
// IE PointerDown events will be validated in `touch` or `mouse`
|
||
// Index numbers referenced here: https://msdn.microsoft.com/library/windows/apps/hh466130.aspx
|
||
this.iePointerMap = {
|
||
2: 'touch',
|
||
3: 'touch',
|
||
4: 'mouse'
|
||
};
|
||
|
||
this.initializeEvents();
|
||
this.$rootScope.$on('$destroy', this.deregister.bind(this));
|
||
}
|
||
|
||
/**
|
||
* Removes all event listeners created by $mdInteration on the
|
||
* body element.
|
||
*/
|
||
MdInteractionService.prototype.deregister = function() {
|
||
|
||
this.bodyElement.off('keydown mousedown', this.inputHandler);
|
||
|
||
if ('ontouchstart' in document.documentElement) {
|
||
this.bodyElement.off('touchstart', this.bufferedInputHandler);
|
||
}
|
||
|
||
if (this.pointerEvent) {
|
||
this.bodyElement.off(this.pointerEvent, this.inputHandler);
|
||
}
|
||
|
||
};
|
||
|
||
/**
|
||
* Initializes the interaction service, by registering all interaction events to the
|
||
* body element.
|
||
*/
|
||
MdInteractionService.prototype.initializeEvents = function() {
|
||
|
||
this.bodyElement.on('keydown mousedown', this.inputHandler);
|
||
|
||
if ('ontouchstart' in document.documentElement) {
|
||
this.bodyElement.on('touchstart', this.bufferedInputHandler);
|
||
}
|
||
|
||
if (this.pointerEvent) {
|
||
this.bodyElement.on(this.pointerEvent, this.inputHandler);
|
||
}
|
||
|
||
};
|
||
|
||
/**
|
||
* Event listener for normal interaction events, which should be tracked.
|
||
* @param event {MouseEvent|KeyboardEvent|PointerEvent|TouchEvent}
|
||
*/
|
||
MdInteractionService.prototype.onInputEvent = function(event) {
|
||
if (this.isBuffering) {
|
||
return;
|
||
}
|
||
|
||
var type = this.inputEventMap[event.type];
|
||
|
||
if (type === 'pointer') {
|
||
type = this.iePointerMap[event.pointerType] || event.pointerType;
|
||
}
|
||
|
||
this.lastInteractionType = type;
|
||
this.lastInteractionTime = this.$mdUtil.now();
|
||
};
|
||
|
||
/**
|
||
* Event listener for interaction events which should be buffered (touch events).
|
||
* @param event {TouchEvent}
|
||
*/
|
||
MdInteractionService.prototype.onBufferInputEvent = function(event) {
|
||
this.$timeout.cancel(this.bufferTimeout);
|
||
|
||
this.onInputEvent(event);
|
||
this.isBuffering = true;
|
||
|
||
// The timeout of 650ms is needed to delay the touchstart, because otherwise the touch will call
|
||
// the `onInput` function multiple times.
|
||
this.bufferTimeout = this.$timeout(function() {
|
||
this.isBuffering = false;
|
||
}.bind(this), 650, false);
|
||
|
||
};
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdInteraction#getLastInteractionType
|
||
* @description Retrieves the last interaction type triggered in body.
|
||
* @returns {string|null} Last interaction type.
|
||
*/
|
||
MdInteractionService.prototype.getLastInteractionType = function() {
|
||
return this.lastInteractionType;
|
||
};
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdInteraction#isUserInvoked
|
||
* @description Method to detect whether any interaction happened recently or not.
|
||
* @param {number=} checkDelay Time to check for any interaction to have been triggered.
|
||
* @returns {boolean} Whether there was any interaction or not.
|
||
*/
|
||
MdInteractionService.prototype.isUserInvoked = function(checkDelay) {
|
||
var delay = angular.isNumber(checkDelay) ? checkDelay : 15;
|
||
|
||
// Check for any interaction to be within the specified check time.
|
||
return this.lastInteractionTime >= this.$mdUtil.now() - delay;
|
||
};
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
angular.module('material.core')
|
||
.provider('$$interimElement', InterimElementProvider);
|
||
|
||
/**
|
||
* @ngdoc service
|
||
* @name $$interimElementProvider
|
||
* @module material.core.interimElement
|
||
*
|
||
* @description
|
||
*
|
||
* Factory that constructs `$$interimElement.$service` services.
|
||
* Used internally in material design for elements that appear on screen temporarily.
|
||
* The service provides a promise-like API for interacting with the temporary
|
||
* elements.
|
||
*
|
||
* <hljs lang="js">
|
||
* app.service('$mdToast', function($$interimElement) {
|
||
* var $mdToast = $$interimElement(toastDefaultOptions);
|
||
* return $mdToast;
|
||
* });
|
||
* </hljs>
|
||
*
|
||
* @param {object=} defaultOptions Options used by default for the `show` method on the service.
|
||
*
|
||
* @returns {$$interimElement.$service}
|
||
*/
|
||
|
||
function InterimElementProvider() {
|
||
InterimElementFactory.$inject = ["$document", "$q", "$rootScope", "$timeout", "$rootElement", "$animate", "$mdUtil", "$mdCompiler", "$mdTheming", "$injector", "$exceptionHandler"];
|
||
createInterimElementProvider.$get = InterimElementFactory;
|
||
return createInterimElementProvider;
|
||
|
||
/**
|
||
* Returns a new provider which allows configuration of a new interimElement
|
||
* service. Allows configuration of default options & methods for options,
|
||
* as well as configuration of 'preset' methods (eg dialog.basic(): basic is a preset method)
|
||
*/
|
||
function createInterimElementProvider(interimFactoryName) {
|
||
factory.$inject = ["$$interimElement", "$injector"];
|
||
var EXPOSED_METHODS = ['onHide', 'onShow', 'onRemove'];
|
||
|
||
var customMethods = {};
|
||
var providerConfig = {
|
||
presets: {}
|
||
};
|
||
|
||
var provider = {
|
||
setDefaults: setDefaults,
|
||
addPreset: addPreset,
|
||
addMethod: addMethod,
|
||
$get: factory
|
||
};
|
||
|
||
/**
|
||
* all interim elements will come with the 'build' preset
|
||
*/
|
||
provider.addPreset('build', {
|
||
methods: ['controller', 'controllerAs', 'resolve', 'multiple',
|
||
'template', 'templateUrl', 'themable', 'transformTemplate', 'parent', 'contentElement']
|
||
});
|
||
|
||
return provider;
|
||
|
||
/**
|
||
* Save the configured defaults to be used when the factory is instantiated
|
||
*/
|
||
function setDefaults(definition) {
|
||
providerConfig.optionsFactory = definition.options;
|
||
providerConfig.methods = (definition.methods || []).concat(EXPOSED_METHODS);
|
||
return provider;
|
||
}
|
||
|
||
/**
|
||
* Add a method to the factory that isn't specific to any interim element operations
|
||
*/
|
||
function addMethod(name, fn) {
|
||
customMethods[name] = fn;
|
||
return provider;
|
||
}
|
||
|
||
/**
|
||
* Save the configured preset to be used when the factory is instantiated
|
||
*/
|
||
function addPreset(name, definition) {
|
||
definition = definition || {};
|
||
definition.methods = definition.methods || [];
|
||
definition.options = definition.options || function() { return {}; };
|
||
|
||
if (/^cancel|hide|show$/.test(name)) {
|
||
throw new Error("Preset '" + name + "' in " + interimFactoryName + " is reserved!");
|
||
}
|
||
if (definition.methods.indexOf('_options') > -1) {
|
||
throw new Error("Method '_options' in " + interimFactoryName + " is reserved!");
|
||
}
|
||
providerConfig.presets[name] = {
|
||
methods: definition.methods.concat(EXPOSED_METHODS),
|
||
optionsFactory: definition.options,
|
||
argOption: definition.argOption
|
||
};
|
||
return provider;
|
||
}
|
||
|
||
function addPresetMethod(presetName, methodName, method) {
|
||
providerConfig.presets[presetName][methodName] = method;
|
||
}
|
||
|
||
/**
|
||
* Create a factory that has the given methods & defaults implementing interimElement
|
||
*/
|
||
/* @ngInject */
|
||
function factory($$interimElement, $injector) {
|
||
var defaultMethods;
|
||
var defaultOptions;
|
||
var interimElementService = $$interimElement();
|
||
|
||
/*
|
||
* publicService is what the developer will be using.
|
||
* It has methods hide(), cancel(), show(), build(), and any other
|
||
* presets which were set during the config phase.
|
||
*/
|
||
var publicService = {
|
||
hide: interimElementService.hide,
|
||
cancel: interimElementService.cancel,
|
||
show: showInterimElement,
|
||
|
||
// Special internal method to destroy an interim element without animations
|
||
// used when navigation changes causes a $scope.$destroy() action
|
||
destroy : destroyInterimElement
|
||
};
|
||
|
||
|
||
defaultMethods = providerConfig.methods || [];
|
||
// This must be invoked after the publicService is initialized
|
||
defaultOptions = invokeFactory(providerConfig.optionsFactory, {});
|
||
|
||
// Copy over the simple custom methods
|
||
angular.forEach(customMethods, function(fn, name) {
|
||
publicService[name] = fn;
|
||
});
|
||
|
||
angular.forEach(providerConfig.presets, function(definition, name) {
|
||
var presetDefaults = invokeFactory(definition.optionsFactory, {});
|
||
var presetMethods = (definition.methods || []).concat(defaultMethods);
|
||
|
||
// Every interimElement built with a preset has a field called `$type`,
|
||
// which matches the name of the preset.
|
||
// Eg in preset 'confirm', options.$type === 'confirm'
|
||
angular.extend(presetDefaults, { $type: name });
|
||
|
||
// This creates a preset class which has setter methods for every
|
||
// method given in the `.addPreset()` function, as well as every
|
||
// method given in the `.setDefaults()` function.
|
||
//
|
||
// @example
|
||
// .setDefaults({
|
||
// methods: ['hasBackdrop', 'clickOutsideToClose', 'escapeToClose', 'targetEvent'],
|
||
// options: dialogDefaultOptions
|
||
// })
|
||
// .addPreset('alert', {
|
||
// methods: ['title', 'ok'],
|
||
// options: alertDialogOptions
|
||
// })
|
||
//
|
||
// Set values will be passed to the options when interimElement.show() is called.
|
||
function Preset(opts) {
|
||
this._options = angular.extend({}, presetDefaults, opts);
|
||
}
|
||
angular.forEach(presetMethods, function(name) {
|
||
Preset.prototype[name] = function(value) {
|
||
this._options[name] = value;
|
||
return this;
|
||
};
|
||
});
|
||
|
||
// Create shortcut method for one-linear methods
|
||
if (definition.argOption) {
|
||
var methodName = 'show' + name.charAt(0).toUpperCase() + name.slice(1);
|
||
publicService[methodName] = function(arg) {
|
||
var config = publicService[name](arg);
|
||
return publicService.show(config);
|
||
};
|
||
}
|
||
|
||
// eg $mdDialog.alert() will return a new alert preset
|
||
publicService[name] = function(arg) {
|
||
// If argOption is supplied, eg `argOption: 'content'`, then we assume
|
||
// if the argument is not an options object then it is the `argOption` option.
|
||
//
|
||
// @example `$mdToast.simple('hello')` // sets options.content to hello
|
||
// // because argOption === 'content'
|
||
if (arguments.length && definition.argOption &&
|
||
!angular.isObject(arg) && !angular.isArray(arg)) {
|
||
|
||
return (new Preset())[definition.argOption](arg);
|
||
|
||
} else {
|
||
return new Preset(arg);
|
||
}
|
||
|
||
};
|
||
});
|
||
|
||
return publicService;
|
||
|
||
/**
|
||
*
|
||
*/
|
||
function showInterimElement(opts) {
|
||
// opts is either a preset which stores its options on an _options field,
|
||
// or just an object made up of options
|
||
opts = opts || { };
|
||
if (opts._options) opts = opts._options;
|
||
|
||
return interimElementService.show(
|
||
angular.extend({}, defaultOptions, opts)
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Special method to hide and destroy an interimElement WITHOUT
|
||
* any 'leave` or hide animations ( an immediate force hide/remove )
|
||
*
|
||
* NOTE: This calls the onRemove() subclass method for each component...
|
||
* which must have code to respond to `options.$destroy == true`
|
||
*/
|
||
function destroyInterimElement(opts) {
|
||
return interimElementService.destroy(opts);
|
||
}
|
||
|
||
/**
|
||
* Helper to call $injector.invoke with a local of the factory name for
|
||
* this provider.
|
||
* If an $mdDialog is providing options for a dialog and tries to inject
|
||
* $mdDialog, a circular dependency error will happen.
|
||
* We get around that by manually injecting $mdDialog as a local.
|
||
*/
|
||
function invokeFactory(factory, defaultVal) {
|
||
var locals = {};
|
||
locals[interimFactoryName] = publicService;
|
||
return $injector.invoke(factory || function() { return defaultVal; }, {}, locals);
|
||
}
|
||
}
|
||
}
|
||
|
||
/* @ngInject */
|
||
function InterimElementFactory($document, $q, $rootScope, $timeout, $rootElement, $animate,
|
||
$mdUtil, $mdCompiler, $mdTheming, $injector, $exceptionHandler) {
|
||
return function createInterimElementService() {
|
||
var SHOW_CANCELLED = false;
|
||
|
||
/**
|
||
* @ngdoc service
|
||
* @name $$interimElementProvider.$service
|
||
*
|
||
* @description
|
||
* A service used to control inserting and removing of an element from the DOM.
|
||
* It is used by $mdBottomSheet, $mdDialog, $mdToast, $mdMenu, $mdPanel, and $mdSelect.
|
||
*/
|
||
var service;
|
||
|
||
var showPromises = []; // Promises for the interim's which are currently opening.
|
||
var hidePromises = []; // Promises for the interim's which are currently hiding.
|
||
var showingInterims = []; // Interim elements which are currently showing up.
|
||
|
||
// Publish instance $$interimElement service;
|
||
return service = {
|
||
show: show,
|
||
hide: waitForInterim(hide),
|
||
cancel: waitForInterim(cancel),
|
||
destroy : destroy,
|
||
$injector_: $injector
|
||
};
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $$interimElementProvider.$service#show
|
||
* @kind function
|
||
*
|
||
* @description
|
||
* Adds the `$interimElement` to the DOM and returns a special promise that will be resolved
|
||
* or rejected with hide or cancel, respectively.
|
||
*
|
||
* @param {Object} options map of options and values
|
||
* @returns {Promise} a Promise that will be resolved when hide() is called or rejected when
|
||
* cancel() is called.
|
||
*/
|
||
function show(options) {
|
||
options = options || {};
|
||
var interimElement = new InterimElement(options || {});
|
||
|
||
// When an interim element is currently showing, we have to cancel it.
|
||
// Just hiding it, will resolve the InterimElement's promise, the promise should be
|
||
// rejected instead.
|
||
var hideAction = options.multiple ? $q.resolve() : $q.all(showPromises);
|
||
|
||
if (!options.multiple) {
|
||
// Wait for all opening interim's to finish their transition.
|
||
hideAction = hideAction.then(function() {
|
||
// Wait for all closing and showing interim's to be completely closed.
|
||
var promiseArray = hidePromises.concat(showingInterims.map(service.cancel));
|
||
return $q.all(promiseArray);
|
||
});
|
||
}
|
||
|
||
var showAction = hideAction.then(function() {
|
||
|
||
return interimElement
|
||
.show()
|
||
.then(function () {
|
||
showingInterims.push(interimElement);
|
||
})
|
||
.catch(function (reason) {
|
||
return reason;
|
||
})
|
||
.finally(function() {
|
||
showPromises.splice(showPromises.indexOf(showAction), 1);
|
||
});
|
||
|
||
});
|
||
|
||
showPromises.push(showAction);
|
||
|
||
// In AngularJS 1.6+, exceptions inside promises will cause a rejection. We need to handle
|
||
// the rejection and only log it if it's an error.
|
||
interimElement.deferred.promise.catch(function(fault) {
|
||
if (fault instanceof Error) {
|
||
$exceptionHandler(fault);
|
||
}
|
||
|
||
return fault;
|
||
});
|
||
|
||
// Return a promise that will be resolved when the interim
|
||
// element is hidden or cancelled...
|
||
return interimElement.deferred.promise;
|
||
}
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $$interimElementProvider.$service#hide
|
||
* @kind function
|
||
*
|
||
* @description
|
||
* Removes the `$interimElement` from the DOM and resolves the Promise returned from `show()`.
|
||
*
|
||
* @param {*} reason Data used to resolve the Promise
|
||
* @param {object} options map of options and values
|
||
* @returns {Promise} a Promise that will be resolved after the element has been removed
|
||
* from the DOM.
|
||
*/
|
||
function hide(reason, options) {
|
||
options = options || {};
|
||
|
||
if (options.closeAll) {
|
||
// We have to make a shallow copy of the array, because otherwise the map will break.
|
||
return $q.all(showingInterims.slice().reverse().map(closeElement));
|
||
} else if (options.closeTo !== undefined) {
|
||
return $q.all(showingInterims.slice(options.closeTo).map(closeElement));
|
||
}
|
||
|
||
// Hide the latest showing interim element.
|
||
return closeElement(showingInterims[showingInterims.length - 1]);
|
||
|
||
/**
|
||
* @param {InterimElement} interim element to close
|
||
* @returns {Promise<InterimElement>}
|
||
*/
|
||
function closeElement(interim) {
|
||
if (!interim) {
|
||
return $q.when(reason);
|
||
}
|
||
|
||
var hideAction = interim
|
||
.remove(reason, false, options || { })
|
||
.catch(function(reason) { return reason; })
|
||
.finally(function() {
|
||
hidePromises.splice(hidePromises.indexOf(hideAction), 1);
|
||
});
|
||
|
||
showingInterims.splice(showingInterims.indexOf(interim), 1);
|
||
hidePromises.push(hideAction);
|
||
|
||
return interim.deferred.promise;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $$interimElementProvider.$service#cancel
|
||
* @kind function
|
||
*
|
||
* @description
|
||
* Removes the `$interimElement` from the DOM and rejects the Promise returned from `show()`.
|
||
*
|
||
* @param {*} reason Data used to resolve the Promise
|
||
* @param {object} options map of options and values
|
||
* @returns {Promise} Promise that will be resolved after the element has been removed
|
||
* from the DOM.
|
||
*/
|
||
function cancel(reason, options) {
|
||
var interim = showingInterims.pop();
|
||
if (!interim) {
|
||
return $q.when(reason);
|
||
}
|
||
|
||
var cancelAction = interim
|
||
.remove(reason, true, options || {})
|
||
.catch(function(reason) { return reason; })
|
||
.finally(function() {
|
||
hidePromises.splice(hidePromises.indexOf(cancelAction), 1);
|
||
});
|
||
|
||
hidePromises.push(cancelAction);
|
||
|
||
// Since AngularJS 1.6.7, promises will be logged to $exceptionHandler when the promise
|
||
// is not handling the rejection. We create a pseudo catch handler, which will prevent the
|
||
// promise from being logged to the $exceptionHandler.
|
||
return interim.deferred.promise.catch(angular.noop);
|
||
}
|
||
|
||
/**
|
||
* Creates a function to wait for at least one interim element to be available.
|
||
* @param callbackFn Function to be used as callback
|
||
* @returns {Function}
|
||
*/
|
||
function waitForInterim(callbackFn) {
|
||
return function() {
|
||
var fnArguments = arguments;
|
||
|
||
if (!showingInterims.length) {
|
||
// When there are still interim's opening, then wait for the first interim element to
|
||
// finish its open animation.
|
||
if (showPromises.length) {
|
||
return showPromises[0].finally(function () {
|
||
return callbackFn.apply(service, fnArguments);
|
||
});
|
||
}
|
||
|
||
return $q.when("No interim elements currently showing up.");
|
||
}
|
||
|
||
return callbackFn.apply(service, fnArguments);
|
||
};
|
||
}
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $$interimElementProvider.$service#destroy
|
||
* @kind function
|
||
*
|
||
* Special method to quick-remove the interim element without running animations. This is
|
||
* useful when the parent component has been or is being destroyed.
|
||
*
|
||
* Note: interim elements are in "interim containers".
|
||
*/
|
||
function destroy(targetEl) {
|
||
var interim = !targetEl ? showingInterims.shift() : null;
|
||
|
||
var parentEl = angular.element(targetEl).length && angular.element(targetEl)[0].parentNode;
|
||
|
||
if (parentEl) {
|
||
// Try to find the interim in the stack which corresponds to the supplied DOM element.
|
||
var filtered = showingInterims.filter(function(entry) {
|
||
return entry.options.element[0] === parentEl;
|
||
});
|
||
|
||
// Note: This function might be called when the element already has been removed,
|
||
// in which case we won't find any matches.
|
||
if (filtered.length) {
|
||
interim = filtered[0];
|
||
showingInterims.splice(showingInterims.indexOf(interim), 1);
|
||
}
|
||
}
|
||
|
||
return interim ? interim.remove(SHOW_CANCELLED, false, { '$destroy': true }) :
|
||
$q.when(SHOW_CANCELLED);
|
||
}
|
||
|
||
/*
|
||
* Internal Interim Element Object
|
||
* Used internally to manage the DOM element and related data
|
||
*/
|
||
function InterimElement(options) {
|
||
var self, element, showAction = $q.when(true);
|
||
|
||
options = configureScopeAndTransitions(options);
|
||
|
||
return self = {
|
||
options : options,
|
||
deferred: $q.defer(),
|
||
show : createAndTransitionIn,
|
||
remove : transitionOutAndRemove
|
||
};
|
||
|
||
/**
|
||
* Compile, link, and show this interim element. Use optional autoHide and transition-in
|
||
* effects.
|
||
* @return {Q.Promise}
|
||
*/
|
||
function createAndTransitionIn() {
|
||
return $q(function(resolve, reject) {
|
||
|
||
// Trigger onCompiling callback before the compilation starts.
|
||
// This is useful, when modifying options, which can be influenced by developers.
|
||
options.onCompiling && options.onCompiling(options);
|
||
|
||
compileElement(options)
|
||
.then(function(compiledData) {
|
||
element = linkElement(compiledData, options);
|
||
|
||
// Expose the cleanup function from the compiler.
|
||
options.cleanupElement = compiledData.cleanup;
|
||
|
||
showAction = showElement(element, options, compiledData.controller)
|
||
.then(resolve, rejectAll);
|
||
}).catch(rejectAll);
|
||
|
||
function rejectAll(fault) {
|
||
// Force the '$md<xxx>.show()' promise to reject
|
||
self.deferred.reject(fault);
|
||
|
||
// Continue rejection propagation
|
||
reject(fault);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* After the show process has finished/rejected:
|
||
* - announce 'removing',
|
||
* - perform the transition-out, and
|
||
* - perform optional clean up scope.
|
||
*/
|
||
function transitionOutAndRemove(response, isCancelled, opts) {
|
||
|
||
// abort if the show() and compile failed
|
||
if (!element) return $q.when(false);
|
||
|
||
options = angular.extend(options || {}, opts || {});
|
||
options.cancelAutoHide && options.cancelAutoHide();
|
||
options.element.triggerHandler('$mdInterimElementRemove');
|
||
|
||
if (options.$destroy === true) {
|
||
|
||
return hideElement(options.element, options).then(function(){
|
||
(isCancelled && rejectAll(response)) || resolveAll(response);
|
||
});
|
||
|
||
} else {
|
||
$q.when(showAction).finally(function() {
|
||
hideElement(options.element, options).then(function() {
|
||
isCancelled ? rejectAll(response) : resolveAll(response);
|
||
}, rejectAll);
|
||
});
|
||
|
||
return self.deferred.promise;
|
||
}
|
||
|
||
|
||
/**
|
||
* The `show()` returns a promise that will be resolved when the interim
|
||
* element is hidden or cancelled...
|
||
*/
|
||
function resolveAll(response) {
|
||
self.deferred.resolve(response);
|
||
}
|
||
|
||
/**
|
||
* Force the '$md<xxx>.show()' promise to reject
|
||
*/
|
||
function rejectAll(fault) {
|
||
self.deferred.reject(fault);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Prepare optional isolated scope and prepare $animate with default enter and leave
|
||
* transitions for the new element instance.
|
||
*/
|
||
function configureScopeAndTransitions(options) {
|
||
options = options || { };
|
||
if (options.template) {
|
||
options.template = $mdUtil.processTemplate(options.template);
|
||
}
|
||
|
||
return angular.extend({
|
||
preserveScope: false,
|
||
cancelAutoHide : angular.noop,
|
||
scope: options.scope || $rootScope.$new(options.isolateScope),
|
||
|
||
/**
|
||
* Default usage to enable $animate to transition-in; can be easily overridden via 'options'
|
||
*/
|
||
onShow: function transitionIn(scope, element, options) {
|
||
return $animate.enter(element, options.parent);
|
||
},
|
||
|
||
/**
|
||
* Default usage to enable $animate to transition-out; can be easily overridden via 'options'
|
||
*/
|
||
onRemove: function transitionOut(scope, element) {
|
||
// Element could be undefined if a new element is shown before
|
||
// the old one finishes compiling.
|
||
return element && $animate.leave(element) || $q.when();
|
||
}
|
||
}, options);
|
||
|
||
}
|
||
|
||
/**
|
||
* Compile an element with a templateUrl, controller, and locals
|
||
* @param {Object} options
|
||
* @return {Q.Promise<{element: JQLite=, link: Function, locals: Object, cleanup: any=,
|
||
* controller: Object=}>}
|
||
*/
|
||
function compileElement(options) {
|
||
|
||
var compiled = !options.skipCompile ? $mdCompiler.compile(options) : null;
|
||
|
||
return compiled || $q(function (resolve) {
|
||
resolve({
|
||
locals: {},
|
||
link: function () {
|
||
return options.element;
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Link an element with compiled configuration
|
||
* @param {{element: JQLite=, link: Function, locals: Object, controller: Object=}} compileData
|
||
* @param {Object} options
|
||
* @return {JQLite}
|
||
*/
|
||
function linkElement(compileData, options) {
|
||
angular.extend(compileData.locals, options);
|
||
|
||
var element = compileData.link(options.scope);
|
||
|
||
// Search for parent at insertion time, if not specified
|
||
options.element = element;
|
||
options.parent = findParent(element, options);
|
||
if (options.themable) $mdTheming(element);
|
||
|
||
return element;
|
||
}
|
||
|
||
/**
|
||
* Search for parent at insertion time, if not specified.
|
||
* @param {JQLite} element
|
||
* @param {Object} options
|
||
* @return {JQLite}
|
||
*/
|
||
function findParent(element, options) {
|
||
var parent = options.parent;
|
||
|
||
// Search for parent at insertion time, if not specified
|
||
if (angular.isFunction(parent)) {
|
||
parent = parent(options.scope, element, options);
|
||
} else if (angular.isString(parent)) {
|
||
parent = angular.element($document[0].querySelector(parent));
|
||
} else {
|
||
parent = angular.element(parent);
|
||
}
|
||
|
||
// If parent querySelector/getter function fails, or it's just null,
|
||
// find a default.
|
||
if (!(parent || {}).length) {
|
||
var el;
|
||
if ($rootElement[0] && $rootElement[0].querySelector) {
|
||
el = $rootElement[0].querySelector(':not(svg) > body');
|
||
}
|
||
if (!el) el = $rootElement[0];
|
||
if (el.nodeName === '#comment') {
|
||
el = $document[0].body;
|
||
}
|
||
return angular.element(el);
|
||
}
|
||
|
||
return parent;
|
||
}
|
||
|
||
/**
|
||
* If auto-hide is enabled, start timer and prepare cancel function
|
||
*/
|
||
function startAutoHide() {
|
||
var autoHideTimer, cancelAutoHide = angular.noop;
|
||
|
||
if (options.hideDelay) {
|
||
autoHideTimer = $timeout(service.hide, options.hideDelay) ;
|
||
cancelAutoHide = function() {
|
||
$timeout.cancel(autoHideTimer);
|
||
};
|
||
}
|
||
|
||
// Cache for subsequent use
|
||
options.cancelAutoHide = function() {
|
||
cancelAutoHide();
|
||
options.cancelAutoHide = undefined;
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Show the element (with transitions), notify complete and start optional auto hiding
|
||
* timer.
|
||
* @param {JQLite} element
|
||
* @param {Object} options
|
||
* @param {Object} controller
|
||
* @return {Q.Promise<JQLite>}
|
||
*/
|
||
function showElement(element, options, controller) {
|
||
// Trigger onShowing callback before the `show()` starts
|
||
var notifyShowing = options.onShowing || angular.noop;
|
||
// Trigger onComplete callback when the `show()` finishes
|
||
var notifyComplete = options.onComplete || angular.noop;
|
||
|
||
// Necessary for consistency between AngularJS 1.5 and 1.6.
|
||
try {
|
||
// This fourth controller parameter is used by $mdDialog in beforeShow().
|
||
notifyShowing(options.scope, element, options, controller);
|
||
} catch (e) {
|
||
return $q.reject(e);
|
||
}
|
||
|
||
return $q(function (resolve, reject) {
|
||
try {
|
||
// Start transitionIn
|
||
$q.when(options.onShow(options.scope, element, options))
|
||
.then(function () {
|
||
notifyComplete(options.scope, element, options);
|
||
startAutoHide();
|
||
|
||
resolve(element);
|
||
}, reject);
|
||
|
||
} catch (e) {
|
||
reject(e.message);
|
||
}
|
||
});
|
||
}
|
||
|
||
function hideElement(element, options) {
|
||
var announceRemoving = options.onRemoving || angular.noop;
|
||
|
||
return $q(function (resolve, reject) {
|
||
try {
|
||
// Start transitionIn
|
||
var action = $q.when(options.onRemove(options.scope, element, options) || true);
|
||
|
||
// Trigger callback *before* the remove operation starts
|
||
announceRemoving(element, action);
|
||
|
||
if (options.$destroy) {
|
||
// For $destroy, onRemove should be synchronous
|
||
resolve(element);
|
||
|
||
if (!options.preserveScope && options.scope) {
|
||
// scope destroy should still be be done after the current digest is done
|
||
action.then(function() { options.scope.$destroy(); });
|
||
}
|
||
} else {
|
||
// Wait until transition-out is done
|
||
action.then(function () {
|
||
if (!options.preserveScope && options.scope) {
|
||
options.scope.$destroy();
|
||
}
|
||
|
||
resolve(element);
|
||
}, reject);
|
||
}
|
||
} catch (e) {
|
||
reject(e.message);
|
||
}
|
||
});
|
||
}
|
||
|
||
}
|
||
};
|
||
}
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
(function() {
|
||
'use strict';
|
||
|
||
var $mdUtil, $interpolate, $log;
|
||
|
||
var SUFFIXES = /(-gt)?-(sm|md|lg|print)/g;
|
||
var WHITESPACE = /\s+/g;
|
||
|
||
var FLEX_OPTIONS = ['grow', 'initial', 'auto', 'none', 'noshrink', 'nogrow'];
|
||
var LAYOUT_OPTIONS = ['row', 'column'];
|
||
var ALIGNMENT_MAIN_AXIS= ["", "start", "center", "end", "stretch", "space-around", "space-between"];
|
||
var ALIGNMENT_CROSS_AXIS= ["", "start", "center", "end", "stretch"];
|
||
|
||
var config = {
|
||
/**
|
||
* Enable directive attribute-to-class conversions
|
||
* Developers can use `<body md-layout-css />` to quickly
|
||
* disable the Layout directives and prohibit the injection of Layout classNames
|
||
*/
|
||
enabled: true,
|
||
|
||
/**
|
||
* List of mediaQuery breakpoints and associated suffixes
|
||
* [
|
||
* { suffix: "sm", mediaQuery: "screen and (max-width: 599px)" },
|
||
* { suffix: "md", mediaQuery: "screen and (min-width: 600px) and (max-width: 959px)" }
|
||
* ]
|
||
*/
|
||
breakpoints: []
|
||
};
|
||
|
||
registerLayoutAPI(angular.module('material.core.layout', ['ng']));
|
||
|
||
/**
|
||
* registerLayoutAPI()
|
||
*
|
||
* The original AngularJS Material Layout solution used attribute selectors and CSS.
|
||
*
|
||
* ```html
|
||
* <div layout="column"> My Content </div>
|
||
* ```
|
||
*
|
||
* ```css
|
||
* [layout] {
|
||
* box-sizing: border-box;
|
||
* display:flex;
|
||
* }
|
||
* [layout=column] {
|
||
* flex-direction : column
|
||
* }
|
||
* ```
|
||
*
|
||
* Use of attribute selectors creates significant performance impacts in some
|
||
* browsers... mainly IE.
|
||
*
|
||
* This module registers directives that allow the same layout attributes to be
|
||
* interpreted and converted to class selectors. The directive will add equivalent classes to
|
||
* each element that contains a Layout directive.
|
||
*
|
||
* ```html
|
||
* <div layout="column" class="layout layout-column"> My Content </div>
|
||
* ```
|
||
*
|
||
* ```css
|
||
* .layout {
|
||
* box-sizing: border-box;
|
||
* display:flex;
|
||
* }
|
||
* .layout-column {
|
||
* flex-direction : column
|
||
* }
|
||
* ```
|
||
*/
|
||
function registerLayoutAPI(module){
|
||
var PREFIX_REGEXP = /^((?:x|data)[:\-_])/i;
|
||
var SPECIAL_CHARS_REGEXP = /([:\-_]+(.))/g;
|
||
|
||
// NOTE: these are also defined in constants::MEDIA_PRIORITY and constants::MEDIA
|
||
var BREAKPOINTS = ["", "xs", "gt-xs", "sm", "gt-sm", "md", "gt-md", "lg", "gt-lg", "xl", "print"];
|
||
var API_WITH_VALUES = ["layout", "flex", "flex-order", "flex-offset", "layout-align"];
|
||
var API_NO_VALUES = ["show", "hide", "layout-padding", "layout-margin"];
|
||
|
||
|
||
// Build directive registration functions for the standard Layout API... for all breakpoints.
|
||
angular.forEach(BREAKPOINTS, function(mqb) {
|
||
|
||
// Attribute directives with expected, observable value(s)
|
||
angular.forEach(API_WITH_VALUES, function(name){
|
||
var fullName = mqb ? name + "-" + mqb : name;
|
||
module.directive(directiveNormalize(fullName), attributeWithObserve(fullName));
|
||
});
|
||
|
||
// Attribute directives with no expected value(s)
|
||
angular.forEach(API_NO_VALUES, function(name){
|
||
var fullName = mqb ? name + "-" + mqb : name;
|
||
module.directive(directiveNormalize(fullName), attributeWithoutValue(fullName));
|
||
});
|
||
|
||
});
|
||
|
||
// Register other, special directive functions for the Layout features:
|
||
module
|
||
.provider('$$mdLayout', function() {
|
||
// Publish internal service for Layouts
|
||
return {
|
||
$get : angular.noop,
|
||
validateAttributeValue : validateAttributeValue,
|
||
validateAttributeUsage : validateAttributeUsage,
|
||
/**
|
||
* Easy way to disable/enable the Layout API.
|
||
* When disabled, this stops all attribute-to-classname generations
|
||
*/
|
||
disableLayouts : function(isDisabled) {
|
||
config.enabled = (isDisabled !== true);
|
||
}
|
||
};
|
||
})
|
||
|
||
.directive('mdLayoutCss' , disableLayoutDirective)
|
||
.directive('ngCloak' , buildCloakInterceptor('ng-cloak'))
|
||
|
||
.directive('layoutWrap' , attributeWithoutValue('layout-wrap'))
|
||
.directive('layoutNowrap' , attributeWithoutValue('layout-nowrap'))
|
||
.directive('layoutNoWrap' , attributeWithoutValue('layout-no-wrap'))
|
||
.directive('layoutFill' , attributeWithoutValue('layout-fill'))
|
||
|
||
// Determine if
|
||
.config(detectDisabledLayouts);
|
||
|
||
/**
|
||
* Converts snake_case to camelCase.
|
||
* Also there is special case for Moz prefix starting with upper case letter.
|
||
* @param name Name to normalize
|
||
*/
|
||
function directiveNormalize(name) {
|
||
return name
|
||
.replace(PREFIX_REGEXP, '')
|
||
.replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) {
|
||
return offset ? letter.toUpperCase() : letter;
|
||
});
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* Detect if any of the HTML tags has a [md-layouts-disabled] attribute;
|
||
* If yes, then immediately disable all layout API features
|
||
*
|
||
* Note: this attribute should be specified on either the HTML or BODY tags
|
||
* @ngInject
|
||
*/
|
||
function detectDisabledLayouts() {
|
||
var isDisabled = !!document.querySelector('[md-layouts-disabled]');
|
||
config.enabled = !isDisabled;
|
||
}
|
||
|
||
/**
|
||
* Special directive that will disable ALL Layout conversions of layout
|
||
* attribute(s) to classname(s).
|
||
*
|
||
* <link rel="stylesheet" href="angular-material.min.css">
|
||
* <link rel="stylesheet" href="angular-material.layout.css">
|
||
*
|
||
* <body md-layout-css>
|
||
* ...
|
||
* </body>
|
||
*
|
||
* Note: Using md-layout-css directive requires the developer to load the Material
|
||
* Layout Attribute stylesheet (which only uses attribute selectors):
|
||
*
|
||
* `angular-material.layout.css`
|
||
*
|
||
* Another option is to use the LayoutProvider to configure and disable the attribute
|
||
* conversions; this would obviate the use of the `md-layout-css` directive
|
||
*/
|
||
function disableLayoutDirective() {
|
||
// Return a 1x-only, first-match attribute directive
|
||
config.enabled = false;
|
||
|
||
return {
|
||
restrict : 'A',
|
||
priority : '900'
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Tail-hook ngCloak to delay the uncloaking while Layout transformers
|
||
* finish processing. Eliminates flicker with Material.Layouts
|
||
*/
|
||
function buildCloakInterceptor(className) {
|
||
return ['$timeout', function($timeout){
|
||
return {
|
||
restrict : 'A',
|
||
priority : -10, // run after normal ng-cloak
|
||
compile : function(element) {
|
||
if (!config.enabled) return angular.noop;
|
||
|
||
// Re-add the cloak
|
||
element.addClass(className);
|
||
|
||
return function(scope, element) {
|
||
// Wait while layout injectors configure, then uncloak
|
||
// NOTE: $rAF does not delay enough... and this is a 1x-only event,
|
||
// $timeout is acceptable.
|
||
$timeout(function(){
|
||
element.removeClass(className);
|
||
}, 10, false);
|
||
};
|
||
}
|
||
};
|
||
}];
|
||
}
|
||
|
||
|
||
// *********************************************************************************
|
||
//
|
||
// These functions create registration functions for AngularJS Material Layout attribute
|
||
// directives. This provides easy translation to switch AngularJS Material attribute selectors to
|
||
// CLASS selectors and directives; which has huge performance implications for IE Browsers.
|
||
//
|
||
// *********************************************************************************
|
||
|
||
/**
|
||
* Creates a directive registration function where a possible dynamic attribute
|
||
* value will be observed/watched.
|
||
* @param {string} className attribute name; eg `layout-gt-md` with value ="row"
|
||
*/
|
||
function attributeWithObserve(className) {
|
||
|
||
return ['$mdUtil', '$interpolate', "$log", function(_$mdUtil_, _$interpolate_, _$log_) {
|
||
$mdUtil = _$mdUtil_;
|
||
$interpolate = _$interpolate_;
|
||
$log = _$log_;
|
||
|
||
return {
|
||
restrict: 'A',
|
||
compile: function(element, attr) {
|
||
var linkFn;
|
||
if (config.enabled) {
|
||
// immediately replace static (non-interpolated) invalid values...
|
||
|
||
validateAttributeUsage(className, attr, element, $log);
|
||
|
||
validateAttributeValue(className,
|
||
getNormalizedAttrValue(className, attr, ""),
|
||
buildUpdateFn(element, className, attr)
|
||
);
|
||
|
||
linkFn = translateWithValueToCssClass;
|
||
}
|
||
|
||
// Use for postLink to account for transforms after ng-transclude.
|
||
return linkFn || angular.noop;
|
||
}
|
||
};
|
||
}];
|
||
|
||
/**
|
||
* Observe deprecated layout attributes and update the element's layout classes to match.
|
||
*/
|
||
function translateWithValueToCssClass(scope, element, attrs) {
|
||
var updateFn = updateClassWithValue(element, className, attrs);
|
||
var unwatch = attrs.$observe(attrs.$normalize(className), updateFn);
|
||
|
||
updateFn(getNormalizedAttrValue(className, attrs, ""));
|
||
scope.$on("$destroy", function() { unwatch(); });
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Creates a registration function for AngularJS Material Layout attribute directive.
|
||
* This is a `simple` transpose of attribute usage to class usage; where we ignore
|
||
* any attribute value.
|
||
*/
|
||
function attributeWithoutValue(className) {
|
||
return ['$mdUtil', '$interpolate', "$log", function(_$mdUtil_, _$interpolate_, _$log_) {
|
||
$mdUtil = _$mdUtil_;
|
||
$interpolate = _$interpolate_;
|
||
$log = _$log_;
|
||
|
||
return {
|
||
restrict: 'A',
|
||
compile: function(element, attr) {
|
||
var linkFn;
|
||
if (config.enabled) {
|
||
// immediately replace static (non-interpolated) invalid values...
|
||
|
||
validateAttributeValue(className,
|
||
getNormalizedAttrValue(className, attr, ""),
|
||
buildUpdateFn(element, className, attr)
|
||
);
|
||
|
||
translateToCssClass(null, element);
|
||
|
||
// Use for postLink to account for transforms after ng-transclude.
|
||
linkFn = translateToCssClass;
|
||
}
|
||
|
||
return linkFn || angular.noop;
|
||
}
|
||
};
|
||
}];
|
||
|
||
/**
|
||
* Add transformed class selector.
|
||
*/
|
||
function translateToCssClass(scope, element) {
|
||
element.addClass(className);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* After link-phase, do NOT remove deprecated layout attribute selector.
|
||
* Instead watch the attribute so interpolated data-bindings to layout
|
||
* selectors will continue to be supported.
|
||
*
|
||
* $observe() the className and update with new class (after removing the last one)
|
||
*
|
||
* e.g. `layout="{{layoutDemo.direction}}"` will update...
|
||
*
|
||
* NOTE: The value must match one of the specified styles in the CSS.
|
||
* For example `flex-gt-md="{{size}}` where `scope.size == 47` will NOT work since
|
||
* only breakpoints for 0, 5, 10, 15... 100, 33, 34, 66, 67 are defined.
|
||
*/
|
||
function updateClassWithValue(element, className) {
|
||
var lastClass;
|
||
|
||
return function updateClassFn(newValue) {
|
||
var value = validateAttributeValue(className, newValue || "");
|
||
if (angular.isDefined(value)) {
|
||
if (lastClass) element.removeClass(lastClass);
|
||
lastClass = !value ? className : className + "-" + value.trim().replace(WHITESPACE, "-");
|
||
element.addClass(lastClass);
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Centralize warnings for known flexbox issues (especially IE-related issues)
|
||
*/
|
||
function validateAttributeUsage(className, attr, element, $log){
|
||
var message, usage, url;
|
||
var nodeName = element[0].nodeName.toLowerCase();
|
||
|
||
switch (className.replace(SUFFIXES,"")) {
|
||
case "flex":
|
||
if ((nodeName === "md-button") || (nodeName === "fieldset")){
|
||
// @see https://github.com/philipwalton/flexbugs#9-some-html-elements-cant-be-flex-containers
|
||
// Use <div flex> wrapper inside (preferred) or outside
|
||
|
||
usage = "<" + nodeName + " " + className + "></" + nodeName + ">";
|
||
url = "https://github.com/philipwalton/flexbugs#9-some-html-elements-cant-be-flex-containers";
|
||
message = "Markup '{0}' may not work as expected in IE Browsers. Consult '{1}' for details.";
|
||
|
||
$log.warn($mdUtil.supplant(message, [usage, url]));
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* For the Layout attribute value, validate or replace with default fallback value.
|
||
*/
|
||
function validateAttributeValue(className, value, updateFn) {
|
||
var origValue = value;
|
||
|
||
if (!needsInterpolation(value)) {
|
||
switch (className.replace(SUFFIXES,"")) {
|
||
case 'layout' :
|
||
if (!findIn(value, LAYOUT_OPTIONS)) {
|
||
value = LAYOUT_OPTIONS[0]; // 'row';
|
||
}
|
||
break;
|
||
|
||
case 'flex' :
|
||
if (!findIn(value, FLEX_OPTIONS)) {
|
||
if (isNaN(value)) {
|
||
value = '';
|
||
}
|
||
}
|
||
break;
|
||
|
||
case 'flex-offset' :
|
||
case 'flex-order' :
|
||
if (!value || isNaN(+value)) {
|
||
value = '0';
|
||
}
|
||
break;
|
||
|
||
case 'layout-align' :
|
||
var axis = extractAlignAxis(value);
|
||
value = $mdUtil.supplant("{main}-{cross}",axis);
|
||
break;
|
||
|
||
case 'layout-padding' :
|
||
case 'layout-margin' :
|
||
case 'layout-fill' :
|
||
case 'layout-wrap' :
|
||
case 'layout-nowrap' :
|
||
value = '';
|
||
break;
|
||
}
|
||
|
||
if (value !== origValue) {
|
||
(updateFn || angular.noop)(value);
|
||
}
|
||
}
|
||
|
||
return value ? value.trim() : "";
|
||
}
|
||
|
||
/**
|
||
* Replace current attribute value with fallback value
|
||
*/
|
||
function buildUpdateFn(element, className, attrs) {
|
||
return function updateAttrValue(fallback) {
|
||
if (!needsInterpolation(fallback)) {
|
||
// Do not modify the element's attribute value; so
|
||
// uses '<ui-layout layout="/api/sidebar.html" />' will not
|
||
// be affected. Just update the attrs value.
|
||
attrs[attrs.$normalize(className)] = fallback;
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* See if the original value has interpolation symbols:
|
||
* e.g. flex-gt-md="{{triggerPoint}}"
|
||
*/
|
||
function needsInterpolation(value) {
|
||
return (value || "").indexOf($interpolate.startSymbol()) > -1;
|
||
}
|
||
|
||
function getNormalizedAttrValue(className, attrs, defaultVal) {
|
||
var normalizedAttr = attrs.$normalize(className);
|
||
return attrs[normalizedAttr] ? attrs[normalizedAttr].trim().replace(WHITESPACE, "-") :
|
||
defaultVal || null;
|
||
}
|
||
|
||
function findIn(item, list, replaceWith) {
|
||
item = replaceWith && item ? item.replace(WHITESPACE, replaceWith) : item;
|
||
|
||
var found = false;
|
||
if (item) {
|
||
list.forEach(function(it) {
|
||
it = replaceWith ? it.replace(WHITESPACE, replaceWith) : it;
|
||
found = found || (it === item);
|
||
});
|
||
}
|
||
return found;
|
||
}
|
||
|
||
function extractAlignAxis(attrValue) {
|
||
var axis = {
|
||
main : "start",
|
||
cross: "stretch"
|
||
}, values;
|
||
|
||
attrValue = (attrValue || "");
|
||
|
||
if (attrValue.indexOf("-") === 0 || attrValue.indexOf(" ") === 0) {
|
||
// For missing main-axis values
|
||
attrValue = "none" + attrValue;
|
||
}
|
||
|
||
values = attrValue.toLowerCase().trim().replace(WHITESPACE, "-").split("-");
|
||
if (values.length && (values[0] === "space")) {
|
||
// for main-axis values of "space-around" or "space-between"
|
||
values = [values[0]+"-"+values[1],values[2]];
|
||
}
|
||
|
||
if (values.length > 0) axis.main = values[0] || axis.main;
|
||
if (values.length > 1) axis.cross = values[1] || axis.cross;
|
||
|
||
if (ALIGNMENT_MAIN_AXIS.indexOf(axis.main) < 0) axis.main = "start";
|
||
if (ALIGNMENT_CROSS_AXIS.indexOf(axis.cross) < 0) axis.cross = "stretch";
|
||
|
||
return axis;
|
||
}
|
||
})();
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.core.liveannouncer
|
||
* @description
|
||
* AngularJS Material Live Announcer to provide accessibility for Voice Readers.
|
||
*/
|
||
MdLiveAnnouncer.$inject = ["$timeout"];
|
||
angular
|
||
.module('material.core')
|
||
.service('$mdLiveAnnouncer', MdLiveAnnouncer);
|
||
|
||
/**
|
||
* @ngdoc service
|
||
* @name $mdLiveAnnouncer
|
||
* @module material.core.liveannouncer
|
||
*
|
||
* @description
|
||
*
|
||
* Service to announce messages to supported screenreaders.
|
||
*
|
||
* > The `$mdLiveAnnouncer` service is internally used for components to provide proper accessibility.
|
||
*
|
||
* <hljs lang="js">
|
||
* module.controller('AppCtrl', function($mdLiveAnnouncer) {
|
||
* // Basic announcement (Polite Mode)
|
||
* $mdLiveAnnouncer.announce('Hey Google');
|
||
*
|
||
* // Custom announcement (Assertive Mode)
|
||
* $mdLiveAnnouncer.announce('Hey Google', 'assertive');
|
||
* });
|
||
* </hljs>
|
||
*
|
||
*/
|
||
function MdLiveAnnouncer($timeout) {
|
||
/** @private @const @type {!angular.$timeout} */
|
||
this._$timeout = $timeout;
|
||
|
||
/** @private @const @type {!HTMLElement} */
|
||
this._liveElement = this._createLiveElement();
|
||
|
||
/** @private @const @type {!number} */
|
||
this._announceTimeout = 100;
|
||
}
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdLiveAnnouncer#announce
|
||
* @description Announces messages to supported screenreaders.
|
||
* @param {string} message Message to be announced to the screenreader
|
||
* @param {'off'|'polite'|'assertive'} politeness The politeness of the announcer element.
|
||
*/
|
||
MdLiveAnnouncer.prototype.announce = function(message, politeness) {
|
||
if (!politeness) {
|
||
politeness = 'polite';
|
||
}
|
||
|
||
var self = this;
|
||
|
||
self._liveElement.textContent = '';
|
||
self._liveElement.setAttribute('aria-live', politeness);
|
||
|
||
// This 100ms timeout is necessary for some browser + screen-reader combinations:
|
||
// - Both JAWS and NVDA over IE11 will not announce anything without a non-zero timeout.
|
||
// - With Chrome and IE11 with NVDA or JAWS, a repeated (identical) message won't be read a
|
||
// second time without clearing and then using a non-zero delay.
|
||
// (using JAWS 17 at time of this writing).
|
||
self._$timeout(function() {
|
||
self._liveElement.textContent = message;
|
||
}, self._announceTimeout, false);
|
||
};
|
||
|
||
/**
|
||
* Creates a live announcer element, which listens for DOM changes and announces them
|
||
* to the screenreaders.
|
||
* @returns {!HTMLElement}
|
||
* @private
|
||
*/
|
||
MdLiveAnnouncer.prototype._createLiveElement = function() {
|
||
var liveEl = document.createElement('div');
|
||
|
||
liveEl.classList.add('md-visually-hidden');
|
||
liveEl.setAttribute('role', 'status');
|
||
liveEl.setAttribute('aria-atomic', 'true');
|
||
liveEl.setAttribute('aria-live', 'polite');
|
||
|
||
document.body.appendChild(liveEl);
|
||
|
||
return liveEl;
|
||
};
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc service
|
||
* @name $$mdMeta
|
||
* @module material.core.meta
|
||
*
|
||
* @description
|
||
*
|
||
* A provider and a service that simplifies meta tags access
|
||
*
|
||
* Note: This is intended only for use with dynamic meta tags such as browser color and title.
|
||
* Tags that are only processed when the page is rendered (such as `charset`, and `http-equiv`)
|
||
* will not work since `$$mdMeta` adds the tags after the page has already been loaded.
|
||
*
|
||
* ```js
|
||
* app.config(function($$mdMetaProvider) {
|
||
* var removeMeta = $$mdMetaProvider.setMeta('meta-name', 'content');
|
||
* var metaValue = $$mdMetaProvider.getMeta('meta-name'); // -> 'content'
|
||
*
|
||
* removeMeta();
|
||
* });
|
||
*
|
||
* app.controller('myController', function($$mdMeta) {
|
||
* var removeMeta = $$mdMeta.setMeta('meta-name', 'content');
|
||
* var metaValue = $$mdMeta.getMeta('meta-name'); // -> 'content'
|
||
*
|
||
* removeMeta();
|
||
* });
|
||
* ```
|
||
*
|
||
* @returns {$$mdMeta.$service}
|
||
*
|
||
*/
|
||
angular.module('material.core.meta', [])
|
||
.provider('$$mdMeta', function () {
|
||
var head = angular.element(document.head);
|
||
var metaElements = {};
|
||
|
||
/**
|
||
* Checks if the requested element was written manually and maps it
|
||
*
|
||
* @param {string} name meta tag 'name' attribute value
|
||
* @returns {boolean} returns true if there is an element with the requested name
|
||
*/
|
||
function mapExistingElement(name) {
|
||
if (metaElements[name]) {
|
||
return true;
|
||
}
|
||
|
||
var element = document.getElementsByName(name)[0];
|
||
|
||
if (!element) {
|
||
return false;
|
||
}
|
||
|
||
metaElements[name] = angular.element(element);
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $$mdMeta#setMeta
|
||
*
|
||
* @description
|
||
* Creates meta element with the 'name' and 'content' attributes,
|
||
* if the meta tag is already created than we replace the 'content' value
|
||
*
|
||
* @param {string} name meta tag 'name' attribute value
|
||
* @param {string} content meta tag 'content' attribute value
|
||
* @returns {function} remove function
|
||
*
|
||
*/
|
||
function setMeta(name, content) {
|
||
mapExistingElement(name);
|
||
|
||
if (!metaElements[name]) {
|
||
var newMeta = angular.element('<meta name="' + name + '" content="' + content + '"/>');
|
||
head.append(newMeta);
|
||
metaElements[name] = newMeta;
|
||
}
|
||
else {
|
||
metaElements[name].attr('content', content);
|
||
}
|
||
|
||
return function () {
|
||
metaElements[name].attr('content', '');
|
||
metaElements[name].remove();
|
||
delete metaElements[name];
|
||
};
|
||
}
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $$mdMeta#getMeta
|
||
*
|
||
* @description
|
||
* Gets the 'content' attribute value of the wanted meta element
|
||
*
|
||
* @param {string} name meta tag 'name' attribute value
|
||
* @returns {string} content attribute value
|
||
*/
|
||
function getMeta(name) {
|
||
if (!mapExistingElement(name)) {
|
||
throw Error('$$mdMeta: could not find a meta tag with the name \'' + name + '\'');
|
||
}
|
||
|
||
return metaElements[name].attr('content');
|
||
}
|
||
|
||
var module = {
|
||
setMeta: setMeta,
|
||
getMeta: getMeta
|
||
};
|
||
|
||
return angular.extend({}, module, {
|
||
$get: function () {
|
||
return module;
|
||
}
|
||
});
|
||
});
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.core.componentRegistry
|
||
*
|
||
* @description
|
||
* A component instance registration service.
|
||
* Note: currently this as a private service in the SideNav component.
|
||
*/
|
||
ComponentRegistry.$inject = ["$log", "$q"];
|
||
angular.module('material.core')
|
||
.factory('$mdComponentRegistry', ComponentRegistry);
|
||
|
||
/*
|
||
* @private
|
||
* @ngdoc factory
|
||
* @name ComponentRegistry
|
||
* @module material.core.componentRegistry
|
||
*
|
||
*/
|
||
function ComponentRegistry($log, $q) {
|
||
|
||
var self;
|
||
var instances = [];
|
||
var pendings = { };
|
||
|
||
return self = {
|
||
/**
|
||
* Used to print an error when an instance for a handle isn't found.
|
||
*/
|
||
notFoundError: function(handle, msgContext) {
|
||
$log.error((msgContext || "") + 'No instance found for handle', handle);
|
||
},
|
||
/**
|
||
* Return all registered instances as an array.
|
||
*/
|
||
getInstances: function() {
|
||
return instances;
|
||
},
|
||
|
||
/**
|
||
* Get a registered instance.
|
||
* @param handle the String handle to look up for a registered instance.
|
||
*/
|
||
get: function(handle) {
|
||
if (!isValidID(handle)) return null;
|
||
|
||
var i, j, instance;
|
||
for (i = 0, j = instances.length; i < j; i++) {
|
||
instance = instances[i];
|
||
if (instance.$$mdHandle === handle) {
|
||
return instance;
|
||
}
|
||
}
|
||
return null;
|
||
},
|
||
|
||
/**
|
||
* Register an instance.
|
||
* @param instance the instance to register
|
||
* @param handle the handle to identify the instance under.
|
||
*/
|
||
register: function(instance, handle) {
|
||
if (!handle) return angular.noop;
|
||
|
||
instance.$$mdHandle = handle;
|
||
instances.push(instance);
|
||
resolveWhen();
|
||
|
||
return deregister;
|
||
|
||
/**
|
||
* Remove registration for an instance
|
||
*/
|
||
function deregister() {
|
||
var index = instances.indexOf(instance);
|
||
if (index !== -1) {
|
||
instances.splice(index, 1);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Resolve any pending promises for this instance
|
||
*/
|
||
function resolveWhen() {
|
||
var dfd = pendings[handle];
|
||
if (dfd) {
|
||
dfd.forEach(function (promise) {
|
||
promise.resolve(instance);
|
||
});
|
||
delete pendings[handle];
|
||
}
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Async accessor to registered component instance
|
||
* If not available then a promise is created to notify
|
||
* all listeners when the instance is registered.
|
||
*/
|
||
when : function(handle) {
|
||
if (isValidID(handle)) {
|
||
var deferred = $q.defer();
|
||
var instance = self.get(handle);
|
||
|
||
if (instance) {
|
||
deferred.resolve(instance);
|
||
} else {
|
||
if (pendings[handle] === undefined) {
|
||
pendings[handle] = [];
|
||
}
|
||
pendings[handle].push(deferred);
|
||
}
|
||
|
||
return deferred.promise;
|
||
}
|
||
return $q.reject("Invalid `md-component-id` value.");
|
||
}
|
||
|
||
};
|
||
|
||
function isValidID(handle){
|
||
return handle && (handle !== "");
|
||
}
|
||
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
(function() {
|
||
'use strict';
|
||
|
||
/**
|
||
* @ngdoc service
|
||
* @name $mdButtonInkRipple
|
||
* @module material.core
|
||
*
|
||
* @description
|
||
* Provides ripple effects for md-button. See $mdInkRipple service for all possible configuration options.
|
||
*
|
||
* @param {object=} scope Scope within the current context
|
||
* @param {object=} element The element the ripple effect should be applied to
|
||
* @param {object=} options (Optional) Configuration options to override the default ripple configuration
|
||
*/
|
||
|
||
MdButtonInkRipple.$inject = ["$mdInkRipple"];
|
||
angular.module('material.core')
|
||
.factory('$mdButtonInkRipple', MdButtonInkRipple);
|
||
|
||
function MdButtonInkRipple($mdInkRipple) {
|
||
return {
|
||
attach: function attachRipple(scope, element, options) {
|
||
options = angular.extend(optionsForElement(element), options);
|
||
|
||
return $mdInkRipple.attach(scope, element, options);
|
||
}
|
||
};
|
||
|
||
function optionsForElement(element) {
|
||
if (element.hasClass('md-icon-button')) {
|
||
return {
|
||
isMenuItem: element.hasClass('md-menu-item'),
|
||
fitRipple: true,
|
||
center: true
|
||
};
|
||
} else {
|
||
return {
|
||
isMenuItem: element.hasClass('md-menu-item'),
|
||
dimBackground: true
|
||
};
|
||
}
|
||
}
|
||
}
|
||
})();
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
(function() {
|
||
'use strict';
|
||
|
||
/**
|
||
* @ngdoc service
|
||
* @name $mdCheckboxInkRipple
|
||
* @module material.core
|
||
*
|
||
* @description
|
||
* Provides ripple effects for md-checkbox. See $mdInkRipple service for all possible configuration options.
|
||
*
|
||
* @param {object=} scope Scope within the current context
|
||
* @param {object=} element The element the ripple effect should be applied to
|
||
* @param {object=} options (Optional) Configuration options to override the defaultripple configuration
|
||
*/
|
||
|
||
MdCheckboxInkRipple.$inject = ["$mdInkRipple"];
|
||
angular.module('material.core')
|
||
.factory('$mdCheckboxInkRipple', MdCheckboxInkRipple);
|
||
|
||
function MdCheckboxInkRipple($mdInkRipple) {
|
||
return {
|
||
attach: attach
|
||
};
|
||
|
||
function attach(scope, element, options) {
|
||
return $mdInkRipple.attach(scope, element, angular.extend({
|
||
center: true,
|
||
dimBackground: false,
|
||
fitRipple: true
|
||
}, options));
|
||
}
|
||
}
|
||
})();
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
(function() {
|
||
'use strict';
|
||
|
||
/**
|
||
* @ngdoc service
|
||
* @name $mdListInkRipple
|
||
* @module material.core
|
||
*
|
||
* @description
|
||
* Provides ripple effects for md-list. See $mdInkRipple service for all possible configuration options.
|
||
*
|
||
* @param {object=} scope Scope within the current context
|
||
* @param {object=} element The element the ripple effect should be applied to
|
||
* @param {object=} options (Optional) Configuration options to override the defaultripple configuration
|
||
*/
|
||
|
||
MdListInkRipple.$inject = ["$mdInkRipple"];
|
||
angular.module('material.core')
|
||
.factory('$mdListInkRipple', MdListInkRipple);
|
||
|
||
function MdListInkRipple($mdInkRipple) {
|
||
return {
|
||
attach: attach
|
||
};
|
||
|
||
function attach(scope, element, options) {
|
||
return $mdInkRipple.attach(scope, element, angular.extend({
|
||
center: false,
|
||
dimBackground: true,
|
||
outline: false,
|
||
rippleSize: 'full'
|
||
}, options));
|
||
}
|
||
}
|
||
})();
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.core.ripple
|
||
* @description
|
||
* Ripple
|
||
*/
|
||
InkRippleCtrl.$inject = ["$scope", "$element", "rippleOptions", "$window", "$timeout", "$mdUtil", "$mdColorUtil"];
|
||
InkRippleDirective.$inject = ["$mdButtonInkRipple", "$mdCheckboxInkRipple"];
|
||
angular.module('material.core')
|
||
.provider('$mdInkRipple', InkRippleProvider)
|
||
.directive('mdInkRipple', InkRippleDirective)
|
||
.directive('mdNoInk', attrNoDirective)
|
||
.directive('mdNoBar', attrNoDirective)
|
||
.directive('mdNoStretch', attrNoDirective);
|
||
|
||
var DURATION = 450;
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdInkRipple
|
||
* @module material.core.ripple
|
||
*
|
||
* @description
|
||
* The `md-ink-ripple` directive allows you to specify the ripple color or if a ripple is allowed.
|
||
*
|
||
* @param {string|boolean} md-ink-ripple A color string `#FF0000` or boolean (`false` or `0`) for
|
||
* preventing ripple
|
||
*
|
||
* @usage
|
||
* ### String values
|
||
* <hljs lang="html">
|
||
* <ANY md-ink-ripple="#FF0000">
|
||
* Ripples in red
|
||
* </ANY>
|
||
*
|
||
* <ANY md-ink-ripple="false">
|
||
* Not rippling
|
||
* </ANY>
|
||
* </hljs>
|
||
*
|
||
* ### Interpolated values
|
||
* <hljs lang="html">
|
||
* <ANY md-ink-ripple="{{ randomColor() }}">
|
||
* Ripples with the return value of 'randomColor' function
|
||
* </ANY>
|
||
*
|
||
* <ANY md-ink-ripple="{{ canRipple() }}">
|
||
* Ripples if 'canRipple' function return value is not 'false' or '0'
|
||
* </ANY>
|
||
* </hljs>
|
||
*/
|
||
function InkRippleDirective ($mdButtonInkRipple, $mdCheckboxInkRipple) {
|
||
return {
|
||
controller: angular.noop,
|
||
link: function (scope, element, attr) {
|
||
attr.hasOwnProperty('mdInkRippleCheckbox')
|
||
? $mdCheckboxInkRipple.attach(scope, element)
|
||
: $mdButtonInkRipple.attach(scope, element);
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* @ngdoc service
|
||
* @name $mdInkRipple
|
||
* @module material.core.ripple
|
||
*
|
||
* @description
|
||
* `$mdInkRipple` is a service for adding ripples to any element.
|
||
*
|
||
* @usage
|
||
* <hljs lang="js">
|
||
* app.factory('$myElementInkRipple', function($mdInkRipple) {
|
||
* return {
|
||
* attach: function (scope, element, options) {
|
||
* return $mdInkRipple.attach(scope, element, angular.extend({
|
||
* center: false,
|
||
* dimBackground: true
|
||
* }, options));
|
||
* }
|
||
* };
|
||
* });
|
||
*
|
||
* app.controller('myController', function ($scope, $element, $myElementInkRipple) {
|
||
* $scope.onClick = function (ev) {
|
||
* $myElementInkRipple.attach($scope, angular.element(ev.target), { center: true });
|
||
* }
|
||
* });
|
||
* </hljs>
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc service
|
||
* @name $mdInkRippleProvider
|
||
* @module material.core.ripple
|
||
*
|
||
* @description
|
||
* If you want to disable ink ripples globally, for all components, you can call the
|
||
* `disableInkRipple` method in your app's config.
|
||
*
|
||
*
|
||
* @usage
|
||
* <hljs lang="js">
|
||
* app.config(function ($mdInkRippleProvider) {
|
||
* $mdInkRippleProvider.disableInkRipple();
|
||
* });
|
||
* </hljs>
|
||
*/
|
||
|
||
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 $mdInkRippleProvider#disableInkRipple
|
||
*
|
||
* @description
|
||
* A config-time method that, when called, disables ripples globally.
|
||
*/
|
||
function disableInkRipple () {
|
||
isDisabledGlobally = true;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Controller used by the ripple service in order to apply ripples
|
||
* @ngInject
|
||
*/
|
||
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;
|
||
this.mousedown = false;
|
||
this.ripples = [];
|
||
this.timeout = null; // Stores a reference to the most-recent ripple timeout
|
||
this.lastRipple = null;
|
||
|
||
$mdUtil.valueOnUse(this, 'container', this.createContainer);
|
||
|
||
this.$element.addClass('md-ink-ripple');
|
||
|
||
// attach method for unit tests
|
||
($element.controller('mdInkRipple') || {}).createRipple = angular.bind(this, this.createRipple);
|
||
($element.controller('mdInkRipple') || {}).setColor = angular.bind(this, this.color);
|
||
|
||
this.bindEvents();
|
||
}
|
||
|
||
|
||
/**
|
||
* Either remove or unlock any remaining ripples when the user mouses off of the element (either by
|
||
* mouseup or mouseleave event)
|
||
*/
|
||
function autoCleanup (self, cleanupFn) {
|
||
if (self.mousedown || self.lastRipple) {
|
||
self.mousedown = false;
|
||
self.$mdUtil.nextTick(angular.bind(self, cleanupFn), false);
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* Returns the color that the ripple should be (either based on CSS or hard-coded)
|
||
* @returns {string}
|
||
*/
|
||
InkRippleCtrl.prototype.color = function (value) {
|
||
var self = this;
|
||
|
||
// If assigning a color value, apply it to background and the ripple color
|
||
if (angular.isDefined(value)) {
|
||
self._color = self._parseColor(value);
|
||
}
|
||
|
||
// If color lookup, use assigned, defined, or inherited
|
||
return self._color || self._parseColor(self.inkRipple()) || self._parseColor(getElementColor());
|
||
|
||
/**
|
||
* Finds the color element and returns its text color for use as default ripple color
|
||
* @returns {string}
|
||
*/
|
||
function getElementColor () {
|
||
var items = self.options && self.options.colorElement ? self.options.colorElement : [];
|
||
var elem = items.length ? items[ 0 ] : self.$element[ 0 ];
|
||
|
||
return elem ? self.$window.getComputedStyle(elem).color : 'rgb(0,0,0)';
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Updating the ripple colors based on the current inkRipple value
|
||
* or the element's computed style color
|
||
*/
|
||
InkRippleCtrl.prototype.calculateColor = function () {
|
||
return this.color();
|
||
};
|
||
|
||
|
||
/**
|
||
* Takes a string color and converts it to RGBA format
|
||
* @param {string} color
|
||
* @param {number} multiplier
|
||
* @returns {string}
|
||
*/
|
||
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 colorUtil.rgbToRgba(color);
|
||
if (color.indexOf('#') === 0) return colorUtil.hexToRgba(color);
|
||
|
||
};
|
||
|
||
/**
|
||
* Binds events to the root element for
|
||
*/
|
||
InkRippleCtrl.prototype.bindEvents = function () {
|
||
this.$element.on('mousedown', angular.bind(this, this.handleMousedown));
|
||
this.$element.on('mouseup touchend', angular.bind(this, this.handleMouseup));
|
||
this.$element.on('mouseleave', angular.bind(this, this.handleMouseup));
|
||
this.$element.on('touchmove', angular.bind(this, this.handleTouchmove));
|
||
};
|
||
|
||
/**
|
||
* Create a new ripple on every mousedown event from the root element
|
||
* @param event {MouseEvent}
|
||
*/
|
||
InkRippleCtrl.prototype.handleMousedown = function (event) {
|
||
if (this.mousedown) return;
|
||
|
||
// When jQuery is loaded, we have to get the original event
|
||
if (event.hasOwnProperty('originalEvent')) event = event.originalEvent;
|
||
this.mousedown = true;
|
||
if (this.options.center) {
|
||
this.createRipple(this.container.prop('clientWidth') / 2, this.container.prop('clientWidth') / 2);
|
||
} else {
|
||
|
||
// We need to calculate the relative coordinates if the target is a sublayer of the ripple element
|
||
if (event.srcElement !== this.$element[0]) {
|
||
var layerRect = this.$element[0].getBoundingClientRect();
|
||
var layerX = event.clientX - layerRect.left;
|
||
var layerY = event.clientY - layerRect.top;
|
||
|
||
this.createRipple(layerX, layerY);
|
||
} else {
|
||
this.createRipple(event.offsetX, event.offsetY);
|
||
}
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Either remove or unlock any remaining ripples when the user mouses off of the element (either by
|
||
* mouseup, touchend or mouseleave event)
|
||
*/
|
||
InkRippleCtrl.prototype.handleMouseup = function () {
|
||
this.$timeout(function () {
|
||
autoCleanup(this, this.clearRipples);
|
||
}.bind(this));
|
||
};
|
||
|
||
/**
|
||
* Either remove or unlock any remaining ripples when the user mouses off of the element (by
|
||
* touchmove)
|
||
*/
|
||
InkRippleCtrl.prototype.handleTouchmove = function () {
|
||
autoCleanup(this, this.deleteRipples);
|
||
};
|
||
|
||
/**
|
||
* Cycles through all ripples and attempts to remove them.
|
||
*/
|
||
InkRippleCtrl.prototype.deleteRipples = function () {
|
||
for (var i = 0; i < this.ripples.length; i++) {
|
||
this.ripples[ i ].remove();
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Cycles through all ripples and attempts to remove them with fade.
|
||
* Depending on logic within `fadeInComplete`, some removals will be postponed.
|
||
*/
|
||
InkRippleCtrl.prototype.clearRipples = function () {
|
||
for (var i = 0; i < this.ripples.length; i++) {
|
||
this.fadeInComplete(this.ripples[ i ]);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Creates the ripple container element
|
||
* @returns {*}
|
||
*/
|
||
InkRippleCtrl.prototype.createContainer = function () {
|
||
var container = angular.element('<div class="md-ripple-container"></div>');
|
||
this.$element.append(container);
|
||
return container;
|
||
};
|
||
|
||
InkRippleCtrl.prototype.clearTimeout = function () {
|
||
if (this.timeout) {
|
||
this.$timeout.cancel(this.timeout);
|
||
this.timeout = null;
|
||
}
|
||
};
|
||
|
||
InkRippleCtrl.prototype.isRippleAllowed = function () {
|
||
var element = this.$element[0];
|
||
do {
|
||
if (!element.tagName || element.tagName === 'BODY') break;
|
||
|
||
if (element && angular.isFunction(element.hasAttribute)) {
|
||
if (element.hasAttribute('disabled')) return false;
|
||
if (this.inkRipple() === 'false' || this.inkRipple() === '0') return false;
|
||
}
|
||
|
||
} while (element = element.parentNode);
|
||
return true;
|
||
};
|
||
|
||
/**
|
||
* The attribute `md-ink-ripple` may be a static or interpolated
|
||
* color value OR a boolean indicator (used to disable ripples)
|
||
*/
|
||
InkRippleCtrl.prototype.inkRipple = function () {
|
||
return this.$element.attr('md-ink-ripple');
|
||
};
|
||
|
||
/**
|
||
* Creates a new ripple and adds it to the container. Also tracks ripple in `this.ripples`.
|
||
* @param left
|
||
* @param top
|
||
*/
|
||
InkRippleCtrl.prototype.createRipple = function (left, top) {
|
||
if (!this.isRippleAllowed()) return;
|
||
|
||
var ctrl = this;
|
||
var colorUtil = ctrl.$mdColorUtil;
|
||
var ripple = angular.element('<div class="md-ripple"></div>');
|
||
var width = this.$element.prop('clientWidth');
|
||
var height = this.$element.prop('clientHeight');
|
||
var x = Math.max(Math.abs(width - left), left) * 2;
|
||
var y = Math.max(Math.abs(height - top), top) * 2;
|
||
var size = getSize(this.options.fitRipple, x, y);
|
||
var color = this.calculateColor();
|
||
|
||
ripple.css({
|
||
left: left + 'px',
|
||
top: top + 'px',
|
||
background: 'black',
|
||
width: size + 'px',
|
||
height: size + 'px',
|
||
backgroundColor: colorUtil.rgbaToRgb(color),
|
||
borderColor: colorUtil.rgbaToRgb(color)
|
||
});
|
||
this.lastRipple = ripple;
|
||
|
||
// we only want one timeout to be running at a time
|
||
this.clearTimeout();
|
||
this.timeout = this.$timeout(function () {
|
||
ctrl.clearTimeout();
|
||
if (!ctrl.mousedown) ctrl.fadeInComplete(ripple);
|
||
}, DURATION * 0.35, false);
|
||
|
||
if (this.options.dimBackground) this.container.css({ backgroundColor: color });
|
||
this.container.append(ripple);
|
||
this.ripples.push(ripple);
|
||
ripple.addClass('md-ripple-placed');
|
||
|
||
this.$mdUtil.nextTick(function () {
|
||
|
||
ripple.addClass('md-ripple-scaled md-ripple-active');
|
||
ctrl.$timeout(function () {
|
||
ctrl.clearRipples();
|
||
}, DURATION, false);
|
||
|
||
}, false);
|
||
|
||
function getSize (fit, x, y) {
|
||
return fit
|
||
? Math.max(x, y)
|
||
: Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
|
||
}
|
||
};
|
||
|
||
|
||
|
||
/**
|
||
* After fadeIn finishes, either kicks off the fade-out animation or queues the element for removal on mouseup
|
||
* @param ripple
|
||
*/
|
||
InkRippleCtrl.prototype.fadeInComplete = function (ripple) {
|
||
if (this.lastRipple === ripple) {
|
||
if (!this.timeout && !this.mousedown) {
|
||
this.removeRipple(ripple);
|
||
}
|
||
} else {
|
||
this.removeRipple(ripple);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Kicks off the animation for removing a ripple
|
||
* @param ripple {Element}
|
||
*/
|
||
InkRippleCtrl.prototype.removeRipple = function (ripple) {
|
||
var ctrl = this;
|
||
var index = this.ripples.indexOf(ripple);
|
||
if (index < 0) return;
|
||
this.ripples.splice(this.ripples.indexOf(ripple), 1);
|
||
ripple.removeClass('md-ripple-active');
|
||
ripple.addClass('md-ripple-remove');
|
||
if (this.ripples.length === 0) this.container.css({ backgroundColor: '' });
|
||
// use a 2-second timeout in order to allow for the animation to finish
|
||
// we don't actually care how long the animation takes
|
||
this.$timeout(function () {
|
||
ctrl.fadeOutComplete(ripple);
|
||
}, DURATION, false);
|
||
};
|
||
|
||
/**
|
||
* Removes the provided ripple from the DOM
|
||
* @param ripple
|
||
*/
|
||
InkRippleCtrl.prototype.fadeOutComplete = function (ripple) {
|
||
ripple.remove();
|
||
this.lastRipple = null;
|
||
};
|
||
|
||
/**
|
||
* Used to create an empty directive. This is used to track flag-directives whose children may have
|
||
* functionality based on them.
|
||
*
|
||
* Example: `md-no-ink` will potentially be used by all child directives.
|
||
*/
|
||
function attrNoDirective () {
|
||
return { controller: angular.noop };
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
(function() {
|
||
'use strict';
|
||
|
||
/**
|
||
* @ngdoc service
|
||
* @name $mdTabInkRipple
|
||
* @module material.core
|
||
*
|
||
* @description
|
||
* Provides ripple effects for md-tabs. See $mdInkRipple service for all possible configuration options.
|
||
*
|
||
* @param {object=} scope Scope within the current context
|
||
* @param {object=} element The element the ripple effect should be applied to
|
||
* @param {object=} options (Optional) Configuration options to override the defaultripple configuration
|
||
*/
|
||
|
||
MdTabInkRipple.$inject = ["$mdInkRipple"];
|
||
angular.module('material.core')
|
||
.factory('$mdTabInkRipple', MdTabInkRipple);
|
||
|
||
function MdTabInkRipple($mdInkRipple) {
|
||
return {
|
||
attach: attach
|
||
};
|
||
|
||
function attach(scope, element, options) {
|
||
return $mdInkRipple.attach(scope, element, angular.extend({
|
||
center: false,
|
||
dimBackground: true,
|
||
outline: false,
|
||
rippleSize: 'full'
|
||
}, options));
|
||
}
|
||
}
|
||
})();
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
angular.module('material.core.theming.palette', [])
|
||
.constant('$mdColorPalette', {
|
||
'red': {
|
||
'50': '#ffebee',
|
||
'100': '#ffcdd2',
|
||
'200': '#ef9a9a',
|
||
'300': '#e57373',
|
||
'400': '#ef5350',
|
||
'500': '#f44336',
|
||
'600': '#e53935',
|
||
'700': '#d32f2f',
|
||
'800': '#c62828',
|
||
'900': '#b71c1c',
|
||
'A100': '#ff8a80',
|
||
'A200': '#ff5252',
|
||
'A400': '#ff1744',
|
||
'A700': '#d50000',
|
||
'contrastDefaultColor': 'light',
|
||
'contrastDarkColors': '50 100 200 300 400 500 600 A100 A200 A400',
|
||
'contrastStrongLightColors': '700 800 900 A700'
|
||
},
|
||
'pink': {
|
||
'50': '#fce4ec',
|
||
'100': '#f8bbd0',
|
||
'200': '#f48fb1',
|
||
'300': '#f06292',
|
||
'400': '#ec407a',
|
||
'500': '#e91e63',
|
||
'600': '#d81b60',
|
||
'700': '#c2185b',
|
||
'800': '#ad1457',
|
||
'900': '#880e4f',
|
||
'A100': '#ff80ab',
|
||
'A200': '#ff4081',
|
||
'A400': '#f50057',
|
||
'A700': '#c51162',
|
||
'contrastDefaultColor': 'light',
|
||
'contrastDarkColors': '50 100 200 300 400 A100 A200 A400',
|
||
// White on 500 does not meet the minimum 4.5 contrast ratio (at 4.34),
|
||
// but it's worse with a dark foreground (3.61).
|
||
'contrastStrongLightColors': '500 600 700 800 900 A700'
|
||
},
|
||
'purple': {
|
||
'50': '#f3e5f5',
|
||
'100': '#e1bee7',
|
||
'200': '#ce93d8',
|
||
'300': '#ba68c8',
|
||
'400': '#ab47bc',
|
||
'500': '#9c27b0',
|
||
'600': '#8e24aa',
|
||
'700': '#7b1fa2',
|
||
'800': '#6a1b9a',
|
||
'900': '#4a148c',
|
||
'A100': '#ea80fc',
|
||
'A200': '#e040fb',
|
||
'A400': '#d500f9',
|
||
'A700': '#aa00ff',
|
||
'contrastDefaultColor': 'light',
|
||
'contrastDarkColors': '50 100 200 300 A100 A200 A400',
|
||
'contrastStrongLightColors': '400 500 600 700 800 900 A700'
|
||
},
|
||
'deep-purple': {
|
||
'50': '#ede7f6',
|
||
'100': '#d1c4e9',
|
||
'200': '#b39ddb',
|
||
'300': '#9575cd',
|
||
'400': '#7e57c2',
|
||
'500': '#673ab7',
|
||
'600': '#5e35b1',
|
||
'700': '#512da8',
|
||
'800': '#4527a0',
|
||
'900': '#311b92',
|
||
'A100': '#b388ff',
|
||
'A200': '#7c4dff',
|
||
'A400': '#651fff',
|
||
'A700': '#6200ea',
|
||
'contrastDefaultColor': 'light',
|
||
'contrastDarkColors': '50 100 200 300 A100',
|
||
'contrastStrongLightColors': '400 500 600 700 800 900 A200 A400 A700'
|
||
},
|
||
'indigo': {
|
||
'50': '#e8eaf6',
|
||
'100': '#c5cae9',
|
||
'200': '#9fa8da',
|
||
'300': '#7986cb',
|
||
'400': '#5c6bc0',
|
||
'500': '#3f51b5',
|
||
'600': '#3949ab',
|
||
'700': '#303f9f',
|
||
'800': '#283593',
|
||
'900': '#1a237e',
|
||
'A100': '#8c9eff',
|
||
'A200': '#536dfe',
|
||
'A400': '#3d5afe',
|
||
'A700': '#304ffe',
|
||
'contrastDefaultColor': 'light',
|
||
'contrastDarkColors': '50 100 200 300 A100 A200',
|
||
'contrastStrongLightColors': '400 500 600 700 800 900 A400 A700'
|
||
},
|
||
'blue': {
|
||
'50': '#e3f2fd',
|
||
'100': '#bbdefb',
|
||
'200': '#90caf9',
|
||
'300': '#64b5f6',
|
||
'400': '#42a5f5',
|
||
'500': '#2196f3',
|
||
'600': '#1e88e5',
|
||
'700': '#1976d2',
|
||
'800': '#1565c0',
|
||
'900': '#0d47a1',
|
||
'A100': '#82b1ff',
|
||
'A200': '#448aff',
|
||
'A400': '#2979ff',
|
||
'A700': '#2962ff',
|
||
'contrastDefaultColor': 'light',
|
||
// White on A400 does not meet the minimum 4.5 contrast ratio (at 3.98),
|
||
// but it's worse with a dark foreground (3.94).
|
||
'contrastDarkColors': '50 100 200 300 400 500 600 A100 A200',
|
||
'contrastStrongLightColors': '700 800 900 A400 A700'
|
||
},
|
||
'light-blue': {
|
||
'50': '#e1f5fe',
|
||
'100': '#b3e5fc',
|
||
'200': '#81d4fa',
|
||
'300': '#4fc3f7',
|
||
'400': '#29b6f6',
|
||
'500': '#03a9f4',
|
||
'600': '#039be5',
|
||
'700': '#0288d1',
|
||
'800': '#0277bd',
|
||
'900': '#01579b',
|
||
'A100': '#80d8ff',
|
||
'A200': '#40c4ff',
|
||
'A400': '#00b0ff',
|
||
'A700': '#0091ea',
|
||
'contrastDefaultColor': 'dark',
|
||
// Dark on 700 does not meet the minimum 4.5 contrast ratio (at 4.07),
|
||
// but it's worse with a white foreground (3.85).
|
||
'contrastStrongLightColors': '800 900 A700'
|
||
},
|
||
'cyan': {
|
||
'50': '#e0f7fa',
|
||
'100': '#b2ebf2',
|
||
'200': '#80deea',
|
||
'300': '#4dd0e1',
|
||
'400': '#26c6da',
|
||
'500': '#00bcd4',
|
||
'600': '#00acc1',
|
||
'700': '#0097a7',
|
||
'800': '#00838f',
|
||
'900': '#006064',
|
||
'A100': '#84ffff',
|
||
'A200': '#18ffff',
|
||
'A400': '#00e5ff',
|
||
'A700': '#00b8d4',
|
||
'contrastDefaultColor': 'dark',
|
||
// Dark on 700 does not meet the minimum 4.5 contrast ratio (at 4.47),
|
||
// but it's worse with a white foreground (3.5).
|
||
'contrastStrongLightColors': '800 900'
|
||
},
|
||
'teal': {
|
||
'50': '#e0f2f1',
|
||
'100': '#b2dfdb',
|
||
'200': '#80cbc4',
|
||
'300': '#4db6ac',
|
||
'400': '#26a69a',
|
||
'500': '#009688',
|
||
'600': '#00897b',
|
||
'700': '#00796b',
|
||
'800': '#00695c',
|
||
'900': '#004d40',
|
||
'A100': '#a7ffeb',
|
||
'A200': '#64ffda',
|
||
'A400': '#1de9b6',
|
||
'A700': '#00bfa5',
|
||
'contrastDefaultColor': 'dark',
|
||
// Dark on 500 does not meet the minimum 4.5 contrast ratio (at 4.27),
|
||
// but it's worse with a white foreground (3.67).
|
||
// White on 600 does not meet the minimum 4.5 contrast ratio (at 4.31),
|
||
// but it's worse with a dark foreground (3.64).
|
||
'contrastStrongLightColors': '600 700 800 900'
|
||
},
|
||
'green': {
|
||
'50': '#e8f5e9',
|
||
'100': '#c8e6c9',
|
||
'200': '#a5d6a7',
|
||
'300': '#81c784',
|
||
'400': '#66bb6a',
|
||
'500': '#4caf50',
|
||
'600': '#43a047',
|
||
'700': '#388e3c',
|
||
'800': '#2e7d32',
|
||
'900': '#1b5e20',
|
||
'A100': '#b9f6ca',
|
||
'A200': '#69f0ae',
|
||
'A400': '#00e676',
|
||
'A700': '#00c853',
|
||
'contrastDefaultColor': 'dark',
|
||
// White on 700 does not meet the minimum 4.5 contrast ratio (at 4.11),
|
||
// but it's worse with a dark foreground (3.81).
|
||
'contrastStrongLightColors': '700 800 900'
|
||
},
|
||
'light-green': {
|
||
'50': '#f1f8e9',
|
||
'100': '#dcedc8',
|
||
'200': '#c5e1a5',
|
||
'300': '#aed581',
|
||
'400': '#9ccc65',
|
||
'500': '#8bc34a',
|
||
'600': '#7cb342',
|
||
'700': '#689f38',
|
||
'800': '#558b2f',
|
||
'900': '#33691e',
|
||
'A100': '#ccff90',
|
||
'A200': '#b2ff59',
|
||
'A400': '#76ff03',
|
||
'A700': '#64dd17',
|
||
'contrastDefaultColor': 'dark',
|
||
'contrastStrongLightColors': '800 900'
|
||
},
|
||
'lime': {
|
||
'50': '#f9fbe7',
|
||
'100': '#f0f4c3',
|
||
'200': '#e6ee9c',
|
||
'300': '#dce775',
|
||
'400': '#d4e157',
|
||
'500': '#cddc39',
|
||
'600': '#c0ca33',
|
||
'700': '#afb42b',
|
||
'800': '#9e9d24',
|
||
'900': '#827717',
|
||
'A100': '#f4ff81',
|
||
'A200': '#eeff41',
|
||
'A400': '#c6ff00',
|
||
'A700': '#aeea00',
|
||
'contrastDefaultColor': 'dark',
|
||
'contrastStrongLightColors': '900'
|
||
},
|
||
'yellow': {
|
||
'50': '#fffde7',
|
||
'100': '#fff9c4',
|
||
'200': '#fff59d',
|
||
'300': '#fff176',
|
||
'400': '#ffee58',
|
||
'500': '#ffeb3b',
|
||
'600': '#fdd835',
|
||
'700': '#fbc02d',
|
||
'800': '#f9a825',
|
||
'900': '#f57f17',
|
||
'A100': '#ffff8d',
|
||
'A200': '#ffff00',
|
||
'A400': '#ffea00',
|
||
'A700': '#ffd600',
|
||
'contrastDefaultColor': 'dark'
|
||
},
|
||
'amber': {
|
||
'50': '#fff8e1',
|
||
'100': '#ffecb3',
|
||
'200': '#ffe082',
|
||
'300': '#ffd54f',
|
||
'400': '#ffca28',
|
||
'500': '#ffc107',
|
||
'600': '#ffb300',
|
||
'700': '#ffa000',
|
||
'800': '#ff8f00',
|
||
'900': '#ff6f00',
|
||
'A100': '#ffe57f',
|
||
'A200': '#ffd740',
|
||
'A400': '#ffc400',
|
||
'A700': '#ffab00',
|
||
'contrastDefaultColor': 'dark'
|
||
},
|
||
'orange': {
|
||
'50': '#fff3e0',
|
||
'100': '#ffe0b2',
|
||
'200': '#ffcc80',
|
||
'300': '#ffb74d',
|
||
'400': '#ffa726',
|
||
'500': '#ff9800',
|
||
'600': '#fb8c00',
|
||
'700': '#f57c00',
|
||
'800': '#ef6c00',
|
||
'900': '#e65100',
|
||
'A100': '#ffd180',
|
||
'A200': '#ffab40',
|
||
'A400': '#ff9100',
|
||
'A700': '#ff6d00',
|
||
'contrastDefaultColor': 'dark',
|
||
'contrastStrongLightColors': '900'
|
||
},
|
||
'deep-orange': {
|
||
'50': '#fbe9e7',
|
||
'100': '#ffccbc',
|
||
'200': '#ffab91',
|
||
'300': '#ff8a65',
|
||
'400': '#ff7043',
|
||
'500': '#ff5722',
|
||
'600': '#f4511e',
|
||
'700': '#e64a19',
|
||
'800': '#d84315',
|
||
'900': '#bf360c',
|
||
'A100': '#ff9e80',
|
||
'A200': '#ff6e40',
|
||
'A400': '#ff3d00',
|
||
'A700': '#dd2c00',
|
||
'contrastDefaultColor': 'dark',
|
||
// Dark on 700 does not meet the minimum 4.5 contrast ratio (at 4.01),
|
||
// but it's worse with a white foreground (3.91).
|
||
// White on 800 does not meet the minimum 4.5 contrast ratio (at 4.43),
|
||
// but it's worse with a dark foreground (3.54).
|
||
'contrastStrongLightColors': '800 900 A400 A700',
|
||
},
|
||
'brown': {
|
||
'50': '#efebe9',
|
||
'100': '#d7ccc8',
|
||
'200': '#bcaaa4',
|
||
'300': '#a1887f',
|
||
'400': '#8d6e63',
|
||
'500': '#795548',
|
||
'600': '#6d4c41',
|
||
'700': '#5d4037',
|
||
'800': '#4e342e',
|
||
'900': '#3e2723',
|
||
'A100': '#d7ccc8',
|
||
'A200': '#bcaaa4',
|
||
'A400': '#8d6e63',
|
||
'A700': '#5d4037',
|
||
'contrastDefaultColor': 'light',
|
||
'contrastDarkColors': '50 100 200 300 A100 A200',
|
||
'contrastStrongLightColors': '400 500 600 700 800 900 A400 A700'
|
||
},
|
||
'grey': {
|
||
'50': '#fafafa',
|
||
'100': '#f5f5f5',
|
||
'200': '#eeeeee',
|
||
'300': '#e0e0e0',
|
||
'400': '#bdbdbd',
|
||
'500': '#9e9e9e',
|
||
'600': '#757575',
|
||
'700': '#616161',
|
||
'800': '#424242',
|
||
'900': '#212121',
|
||
'A100': '#ffffff',
|
||
'A200': '#000000',
|
||
'A400': '#303030',
|
||
'A700': '#616161',
|
||
'contrastDefaultColor': 'dark',
|
||
'contrastLightColors': '700 800 900 A200 A400 A700',
|
||
'contrastStrongLightColors': '600'
|
||
},
|
||
'blue-grey': {
|
||
'50': '#eceff1',
|
||
'100': '#cfd8dc',
|
||
'200': '#b0bec5',
|
||
'300': '#90a4ae',
|
||
'400': '#78909c',
|
||
'500': '#607d8b',
|
||
'600': '#546e7a',
|
||
'700': '#455a64',
|
||
'800': '#37474f',
|
||
'900': '#263238',
|
||
'A100': '#cfd8dc',
|
||
'A200': '#b0bec5',
|
||
'A400': '#78909c',
|
||
'A700': '#455a64',
|
||
'contrastDefaultColor': 'light',
|
||
'contrastDarkColors': '50 100 200 300 400 A100 A200 A400',
|
||
// White on 500 does not meet the minimum 4.5 contrast ratio (at 4.37),
|
||
// but it's worse with a dark foreground.
|
||
'contrastStrongLightColors': '500 600 700 800 900 A700'
|
||
}
|
||
});
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
(function(angular) {
|
||
'use strict';
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.core.theming
|
||
* @description
|
||
* Theming
|
||
*/
|
||
detectDisabledThemes.$inject = ["$mdThemingProvider"];
|
||
ThemingDirective.$inject = ["$mdTheming", "$interpolate", "$parse", "$mdUtil", "$q", "$log"];
|
||
ThemableDirective.$inject = ["$mdTheming"];
|
||
ThemingProvider.$inject = ["$mdColorPalette", "$$mdMetaProvider"];
|
||
generateAllThemes.$inject = ["$injector", "$mdTheming"];
|
||
angular.module('material.core.theming', ['material.core.theming.palette', 'material.core.meta'])
|
||
.directive('mdTheme', ThemingDirective)
|
||
.directive('mdThemable', ThemableDirective)
|
||
.directive('mdThemesDisabled', disableThemesDirective)
|
||
.provider('$mdTheming', ThemingProvider)
|
||
.config(detectDisabledThemes)
|
||
.run(generateAllThemes);
|
||
|
||
/**
|
||
* Detect if the HTML or the BODY tags has a [md-themes-disabled] attribute
|
||
* If yes, then immediately disable all theme stylesheet generation and DOM injection
|
||
*/
|
||
/**
|
||
* @ngInject
|
||
*/
|
||
function detectDisabledThemes($mdThemingProvider) {
|
||
var isDisabled = !!document.querySelector('[md-themes-disabled]');
|
||
$mdThemingProvider.disableTheming(isDisabled);
|
||
}
|
||
|
||
/**
|
||
* @ngdoc service
|
||
* @name $mdThemingProvider
|
||
* @module material.core.theming
|
||
*
|
||
* @description Provider to configure the `$mdTheming` service.
|
||
*
|
||
* ### Default Theme
|
||
* The `$mdThemingProvider` uses by default the following theme configuration:
|
||
*
|
||
* - Primary Palette: `Blue`
|
||
* - Accent Palette: `Pink`
|
||
* - Warn Palette: `Deep-Orange`
|
||
* - Background Palette: `Grey`
|
||
*
|
||
* If you don't want to use the `md-theme` directive on the elements itself, you may want to overwrite
|
||
* the default theme.<br/>
|
||
* This can be done by using the following markup.
|
||
*
|
||
* <hljs lang="js">
|
||
* myAppModule.config(function($mdThemingProvider) {
|
||
* $mdThemingProvider
|
||
* .theme('default')
|
||
* .primaryPalette('blue')
|
||
* .accentPalette('teal')
|
||
* .warnPalette('red')
|
||
* .backgroundPalette('grey');
|
||
* });
|
||
* </hljs>
|
||
*
|
||
|
||
* ### Dynamic Themes
|
||
*
|
||
* By default, if you change a theme at runtime, the `$mdTheming` service will not detect those changes.<br/>
|
||
* If you have an application, which changes its theme on runtime, you have to enable theme watching.
|
||
*
|
||
* <hljs lang="js">
|
||
* myAppModule.config(function($mdThemingProvider) {
|
||
* // Enable theme watching.
|
||
* $mdThemingProvider.alwaysWatchTheme(true);
|
||
* });
|
||
* </hljs>
|
||
*
|
||
* ### Custom Theme Styles
|
||
*
|
||
* Sometimes you may want to use your own theme styles for some custom components.<br/>
|
||
* You are able to register your own styles by using the following markup.
|
||
*
|
||
* <hljs lang="js">
|
||
* myAppModule.config(function($mdThemingProvider) {
|
||
* // Register our custom stylesheet into the theming provider.
|
||
* $mdThemingProvider.registerStyles(STYLESHEET);
|
||
* });
|
||
* </hljs>
|
||
*
|
||
* The `registerStyles` method only accepts strings as value, so you're actually not able to load an external
|
||
* stylesheet file into the `$mdThemingProvider`.
|
||
*
|
||
* If it's necessary to load an external stylesheet, we suggest using a bundler, which supports including raw content,
|
||
* like [raw-loader](https://github.com/webpack/raw-loader) for `webpack`.
|
||
*
|
||
* <hljs lang="js">
|
||
* myAppModule.config(function($mdThemingProvider) {
|
||
* // Register your custom stylesheet into the theming provider.
|
||
* $mdThemingProvider.registerStyles(require('../styles/my-component.theme.css'));
|
||
* });
|
||
* </hljs>
|
||
*
|
||
* ### Browser color
|
||
*
|
||
* Enables browser header coloring
|
||
* for more info please visit:
|
||
* https://developers.google.com/web/fundamentals/design-and-ui/browser-customization/theme-color
|
||
*
|
||
* Options parameter: <br/>
|
||
* `theme` - A defined theme via `$mdThemeProvider` to use the palettes from. Default is `default` theme. <br/>
|
||
* `palette` - Can be any one of the basic material design palettes, extended defined palettes and 'primary',
|
||
* 'accent', 'background' and 'warn'. Default is `primary`. <br/>
|
||
* `hue` - The hue from the selected palette. Default is `800`<br/>
|
||
*
|
||
* <hljs lang="js">
|
||
* myAppModule.config(function($mdThemingProvider) {
|
||
* // Enable browser color
|
||
* $mdThemingProvider.enableBrowserColor({
|
||
* theme: 'myTheme', // Default is 'default'
|
||
* palette: 'accent', // Default is 'primary', any basic material palette and extended palettes are available
|
||
* hue: '200' // Default is '800'
|
||
* });
|
||
* });
|
||
* </hljs>
|
||
*/
|
||
|
||
/**
|
||
* Some Example Valid Theming Expressions
|
||
* =======================================
|
||
*
|
||
* Intention group expansion: (valid for primary, accent, warn, background)
|
||
*
|
||
* {{primary-100}} - grab shade 100 from the primary palette
|
||
* {{primary-100-0.7}} - grab shade 100, apply opacity of 0.7
|
||
* {{primary-100-contrast}} - grab shade 100's contrast color
|
||
* {{primary-hue-1}} - grab the shade assigned to hue-1 from the primary palette
|
||
* {{primary-hue-1-0.7}} - apply 0.7 opacity to primary-hue-1
|
||
* {{primary-color}} - Generates .md-hue-1, .md-hue-2, .md-hue-3 with configured shades set for each hue
|
||
* {{primary-color-0.7}} - Apply 0.7 opacity to each of the above rules
|
||
* {{primary-contrast}} - Generates .md-hue-1, .md-hue-2, .md-hue-3 with configured contrast (ie. text) color shades set for each hue
|
||
* {{primary-contrast-0.7}} - Apply 0.7 opacity to each of the above rules
|
||
* {{primary-contrast-divider}} - Apply divider opacity to contrast color
|
||
*
|
||
* Foreground expansion: Applies rgba to black/white foreground text
|
||
*
|
||
* Old Foreground Expressions:
|
||
* {{foreground-1}} - used for primary text
|
||
* {{foreground-2}} - used for secondary text/divider
|
||
* {{foreground-3}} - used for disabled text
|
||
* {{foreground-4}} - used for dividers
|
||
*
|
||
* New Foreground Expressions:
|
||
*
|
||
* Apply primary text color for contrasting with default background
|
||
* {{background-default-contrast}} - default opacity
|
||
* {{background-default-contrast-secondary}} - opacity for secondary text
|
||
* {{background-default-contrast-hint}} - opacity for hints and placeholders
|
||
* {{background-default-contrast-disabled}} - opacity for disabled text
|
||
* {{background-default-contrast-divider}} - opacity for dividers
|
||
*
|
||
* Apply contrast color for specific shades
|
||
* {{background-50-contrast-icon}} - Apply contrast color for icon on background's shade 50 hue
|
||
*/
|
||
|
||
// In memory generated CSS rules; registered by theme.name
|
||
var GENERATED = { };
|
||
|
||
// In memory storage of defined themes and color palettes (both loaded by CSS, and user specified)
|
||
var PALETTES;
|
||
|
||
// Text colors are automatically generated based on background color when not specified
|
||
// Custom palettes can provide override colors
|
||
// @see https://material.io/archive/guidelines/style/color.html#color-usability
|
||
var DARK_FOREGROUND = {
|
||
name: 'dark',
|
||
};
|
||
var LIGHT_FOREGROUND = {
|
||
name: 'light',
|
||
};
|
||
|
||
var DARK_SHADOW = '1px 1px 0px rgba(0,0,0,0.4), -1px -1px 0px rgba(0,0,0,0.4)';
|
||
var LIGHT_SHADOW = '';
|
||
|
||
var DARK_CONTRAST_COLOR = colorToRgbaArray('rgba(0,0,0,0.87)');
|
||
var LIGHT_CONTRAST_COLOR = colorToRgbaArray('rgba(255,255,255,0.87)');
|
||
var STRONG_LIGHT_CONTRAST_COLOR = colorToRgbaArray('rgb(255,255,255)');
|
||
|
||
var THEME_COLOR_TYPES = ['primary', 'accent', 'warn', 'background'];
|
||
var DEFAULT_COLOR_TYPE = 'primary';
|
||
|
||
// A color in a theme will use these hues by default, if not specified by user.
|
||
var LIGHT_DEFAULT_HUES = {
|
||
'accent': {
|
||
'default': 'A200',
|
||
'hue-1': 'A100',
|
||
'hue-2': 'A400',
|
||
'hue-3': 'A700'
|
||
},
|
||
'background': {
|
||
'default': '50',
|
||
'hue-1': 'A100',
|
||
'hue-2': '100',
|
||
'hue-3': '300'
|
||
}
|
||
};
|
||
|
||
var DARK_DEFAULT_HUES = {
|
||
'background': {
|
||
'default': 'A400',
|
||
'hue-1': '800',
|
||
'hue-2': '900',
|
||
'hue-3': 'A200'
|
||
}
|
||
};
|
||
|
||
// Icon opacity values (active/inactive) from
|
||
// https://material.io/archive/guidelines/style/color.html#color-usability
|
||
var DARK_CONTRAST_OPACITY = {
|
||
'icon': 0.54,
|
||
'secondary': 0.54,
|
||
'disabled': 0.38,
|
||
'hint': 0.38,
|
||
'divider': 0.12,
|
||
};
|
||
|
||
var LIGHT_CONTRAST_OPACITY = {
|
||
'icon': 0.87,
|
||
'secondary': 0.7,
|
||
'disabled': 0.5,
|
||
'hint': 0.5,
|
||
'divider': 0.12
|
||
};
|
||
|
||
// Icon opacity values (active/inactive) from
|
||
// https://material.io/archive/guidelines/style/color.html#color-usability
|
||
var STRONG_LIGHT_CONTRAST_OPACITY = {
|
||
'icon': 1.0,
|
||
'secondary': 0.7,
|
||
'disabled': 0.5,
|
||
'hint': 0.5,
|
||
'divider': 0.12
|
||
};
|
||
|
||
THEME_COLOR_TYPES.forEach(function(colorType) {
|
||
// Color types with unspecified default hues will use these default hue values
|
||
var defaultDefaultHues = {
|
||
'default': '500',
|
||
'hue-1': '300',
|
||
'hue-2': '800',
|
||
'hue-3': 'A100'
|
||
};
|
||
if (!LIGHT_DEFAULT_HUES[colorType]) LIGHT_DEFAULT_HUES[colorType] = defaultDefaultHues;
|
||
if (!DARK_DEFAULT_HUES[colorType]) DARK_DEFAULT_HUES[colorType] = defaultDefaultHues;
|
||
});
|
||
|
||
var VALID_HUE_VALUES = [
|
||
'50', '100', '200', '300', '400', '500', '600',
|
||
'700', '800', '900', 'A100', 'A200', 'A400', 'A700'
|
||
];
|
||
|
||
var themeConfig = {
|
||
disableTheming : false, // Generate our themes at run time; also disable stylesheet DOM injection
|
||
generateOnDemand : false, // Whether or not themes are to be generated on-demand (vs. eagerly).
|
||
registeredStyles : [], // Custom styles registered to be used in the theming of custom components.
|
||
nonce : null // Nonce to be added as an attribute to the generated themes style tags.
|
||
};
|
||
|
||
/**
|
||
*
|
||
*/
|
||
function ThemingProvider($mdColorPalette, $$mdMetaProvider) {
|
||
ThemingService.$inject = ["$rootScope", "$mdUtil", "$q", "$log"];
|
||
PALETTES = { };
|
||
var THEMES = { };
|
||
|
||
var themingProvider;
|
||
|
||
var alwaysWatchTheme = false;
|
||
var defaultTheme = 'default';
|
||
|
||
// Load JS Defined Palettes
|
||
angular.extend(PALETTES, $mdColorPalette);
|
||
|
||
// Default theme defined in core.js
|
||
|
||
/**
|
||
* Adds `theme-color` and `msapplication-navbutton-color` meta tags with the color parameter
|
||
* @param {string} color Hex value of the wanted browser color
|
||
* @returns {function} Remove function of the meta tags
|
||
*/
|
||
var setBrowserColor = function (color) {
|
||
// Chrome, Firefox OS and Opera
|
||
var removeChrome = $$mdMetaProvider.setMeta('theme-color', color);
|
||
// Windows Phone
|
||
var removeWindows = $$mdMetaProvider.setMeta('msapplication-navbutton-color', color);
|
||
|
||
return function () {
|
||
removeChrome();
|
||
removeWindows();
|
||
};
|
||
};
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdThemingProvider#enableBrowserColor
|
||
* @description
|
||
* Enables browser header coloring. For more info please visit
|
||
* <a href="https://developers.google.com/web/fundamentals/design-and-ui/browser-customization/theme-color">
|
||
* Web Fundamentals</a>.
|
||
* @param {object=} options Options for the browser color, which include:<br/>
|
||
* - `theme` - `{string}`: A defined theme via `$mdThemeProvider` to use the palettes from. Default is `default` theme. <br/>
|
||
* - `palette` - `{string}`: Can be any one of the basic material design palettes, extended defined palettes, or `primary`,
|
||
* `accent`, `background`, and `warn`. Default is `primary`.<br/>
|
||
* - `hue` - `{string}`: The hue from the selected palette. Default is `800`.<br/>
|
||
* @returns {function} Function that removes the browser coloring when called.
|
||
*/
|
||
var enableBrowserColor = function (options) {
|
||
options = angular.isObject(options) ? options : {};
|
||
|
||
var theme = options.theme || 'default';
|
||
var hue = options.hue || '800';
|
||
|
||
var palette = PALETTES[options.palette] ||
|
||
PALETTES[THEMES[theme].colors[options.palette || 'primary'].name];
|
||
|
||
var color = angular.isObject(palette[hue]) ? palette[hue].hex : palette[hue];
|
||
if (color.substr(0, 1) !== '#') color = '#' + color;
|
||
|
||
return setBrowserColor(color);
|
||
};
|
||
|
||
return themingProvider = {
|
||
definePalette: definePalette,
|
||
extendPalette: extendPalette,
|
||
theme: registerTheme,
|
||
|
||
/**
|
||
* return a read-only clone of the current theme configuration
|
||
*/
|
||
configuration : function() {
|
||
return angular.extend({ }, themeConfig, {
|
||
defaultTheme : defaultTheme,
|
||
alwaysWatchTheme : alwaysWatchTheme,
|
||
registeredStyles : [].concat(themeConfig.registeredStyles)
|
||
});
|
||
},
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdThemingProvider#disableTheming
|
||
* @description
|
||
* An easier way to disable theming without having to use `.constant("$MD_THEME_CSS","");`.
|
||
* This disables all dynamic theme style sheet generations and injections.
|
||
* @param {boolean=} isDisabled Disable all dynamic theme style sheet generations and injections
|
||
* if `true` or `undefined`.
|
||
*/
|
||
disableTheming: function(isDisabled) {
|
||
themeConfig.disableTheming = angular.isUndefined(isDisabled) || !!isDisabled;
|
||
},
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdThemingProvider#registerStyles
|
||
* @param {string} styles The styles to be appended to AngularJS Material's built in theme CSS.
|
||
*/
|
||
registerStyles: function(styles) {
|
||
themeConfig.registeredStyles.push(styles);
|
||
},
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdThemingProvider#setNonce
|
||
* @param {string} nonceValue The nonce to be added as an attribute to the theme style tags.
|
||
* Setting a value allows the use of CSP policy without using the `'unsafe-inline'` directive.
|
||
* The string must already be base64 encoded. You can use `btoa(string)` to do this encoding.
|
||
* In your CSP's `style-src`, you would then add an entry for `'nonce-nonceValue'`.
|
||
*/
|
||
setNonce: function(nonceValue) {
|
||
themeConfig.nonce = nonceValue;
|
||
},
|
||
|
||
generateThemesOnDemand: function(onDemand) {
|
||
themeConfig.generateOnDemand = onDemand;
|
||
},
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdThemingProvider#setDefaultTheme
|
||
* @param {string} theme Default theme name to be applied to elements.
|
||
* Default value is `default`.
|
||
*/
|
||
setDefaultTheme: function(theme) {
|
||
defaultTheme = theme;
|
||
},
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdThemingProvider#alwaysWatchTheme
|
||
* @param {boolean} alwaysWatch Whether or not to always watch themes for changes and re-apply
|
||
* classes when they change. Default is `false`. Enabling can reduce performance.
|
||
*/
|
||
alwaysWatchTheme: function(alwaysWatch) {
|
||
alwaysWatchTheme = alwaysWatch;
|
||
},
|
||
|
||
enableBrowserColor: enableBrowserColor,
|
||
|
||
$get: ThemingService,
|
||
_LIGHT_DEFAULT_HUES: LIGHT_DEFAULT_HUES,
|
||
_DARK_DEFAULT_HUES: DARK_DEFAULT_HUES,
|
||
_PALETTES: PALETTES,
|
||
_THEMES: THEMES,
|
||
_parseRules: parseRules,
|
||
_rgba: rgba
|
||
};
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdThemingProvider#definePalette
|
||
* @description
|
||
* In the event that you need to define a custom color palette, you can use this function to
|
||
* make it available to your theme for use in its intention groups.<br>
|
||
* Note that you must specify all hues in the definition map.
|
||
* @param {string} name Name of palette being defined
|
||
* @param {object} map Palette definition that includes hue definitions and contrast colors:
|
||
* - `'50'` - `{string}`: HEX color
|
||
* - `'100'` - `{string}`: HEX color
|
||
* - `'200'` - `{string}`: HEX color
|
||
* - `'300'` - `{string}`: HEX color
|
||
* - `'400'` - `{string}`: HEX color
|
||
* - `'500'` - `{string}`: HEX color
|
||
* - `'600'` - `{string}`: HEX color
|
||
* - `'700'` - `{string}`: HEX color
|
||
* - `'800'` - `{string}`: HEX color
|
||
* - `'900'` - `{string}`: HEX color
|
||
* - `'A100'` - `{string}`: HEX color
|
||
* - `'A200'` - `{string}`: HEX color
|
||
* - `'A400'` - `{string}`: HEX color
|
||
* - `'A700'` - `{string}`: HEX color
|
||
* - `'contrastDefaultColor'` - `{string}`: `light` or `dark`
|
||
* - `'contrastDarkColors'` - `{string[]}`: Hues which should use dark contrast colors (i.e. raised button text).
|
||
* For example: `['50', '100', '200', '300', '400', 'A100']`.
|
||
* - `'contrastLightColors'` - `{string[]}`: Hues which should use light contrast colors (i.e. raised button text).
|
||
* For example: `['500', '600', '700', '800', '900', 'A200', 'A400', 'A700']`.
|
||
*/
|
||
function definePalette(name, map) {
|
||
map = map || {};
|
||
PALETTES[name] = checkPaletteValid(name, map);
|
||
return themingProvider;
|
||
}
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdThemingProvider#extendPalette
|
||
* @description
|
||
* Sometimes it is easier to extend an existing color palette and then change a few properties,
|
||
* rather than defining a whole new palette.
|
||
* @param {string} name Name of palette being extended
|
||
* @param {object} map Palette definition that includes optional hue definitions and contrast colors:
|
||
* - `'50'` - `{string}`: HEX color
|
||
* - `'100'` - `{string}`: HEX color
|
||
* - `'200'` - `{string}`: HEX color
|
||
* - `'300'` - `{string}`: HEX color
|
||
* - `'400'` - `{string}`: HEX color
|
||
* - `'500'` - `{string}`: HEX color
|
||
* - `'600'` - `{string}`: HEX color
|
||
* - `'700'` - `{string}`: HEX color
|
||
* - `'800'` - `{string}`: HEX color
|
||
* - `'900'` - `{string}`: HEX color
|
||
* - `'A100'` - `{string}`: HEX color
|
||
* - `'A200'` - `{string}`: HEX color
|
||
* - `'A400'` - `{string}`: HEX color
|
||
* - `'A700'` - `{string}`: HEX color
|
||
* - `'contrastDefaultColor'` - `{string}`: `light` or `dark`
|
||
* - `'contrastDarkColors'` - `{string[]}`: Hues which should use dark contrast colors (i.e. raised button text).
|
||
* For example: `['50', '100', '200', '300', '400', 'A100']`.
|
||
* - `'contrastLightColors'` - `{string[]}`: Hues which should use light contrast colors (i.e. raised button text).
|
||
* For example: `['500', '600', '700', '800', '900', 'A200', 'A400', 'A700']`.
|
||
* @returns {object} A new object which is a copy of the given palette, `name`,
|
||
* with variables from `map` overwritten.
|
||
*/
|
||
function extendPalette(name, map) {
|
||
return checkPaletteValid(name, angular.extend({}, PALETTES[name] || {}, map));
|
||
}
|
||
|
||
// Make sure that palette has all required hues
|
||
function checkPaletteValid(name, map) {
|
||
var missingColors = VALID_HUE_VALUES.filter(function(field) {
|
||
return !map[field];
|
||
});
|
||
if (missingColors.length) {
|
||
throw new Error("Missing colors %1 in palette %2!"
|
||
.replace('%1', missingColors.join(', '))
|
||
.replace('%2', name));
|
||
}
|
||
|
||
return map;
|
||
}
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdThemingProvider#theme
|
||
* @description
|
||
* Register a theme (which is a collection of color palettes); i.e. `warn`, `accent`,
|
||
* `background`, and `primary`.<br>
|
||
* Optionally inherit from an existing theme.
|
||
* @param {string} name Name of theme being registered
|
||
* @param {string=} inheritFrom Existing theme name to inherit from
|
||
*/
|
||
function registerTheme(name, inheritFrom) {
|
||
if (THEMES[name]) return THEMES[name];
|
||
|
||
inheritFrom = inheritFrom || 'default';
|
||
|
||
var parentTheme = typeof inheritFrom === 'string' ? THEMES[inheritFrom] : inheritFrom;
|
||
var theme = new Theme(name);
|
||
|
||
if (parentTheme) {
|
||
angular.forEach(parentTheme.colors, function(color, colorType) {
|
||
theme.colors[colorType] = {
|
||
name: color.name,
|
||
// Make sure a COPY of the hues is given to the child color,
|
||
// not the same reference.
|
||
hues: angular.extend({}, color.hues)
|
||
};
|
||
});
|
||
}
|
||
THEMES[name] = theme;
|
||
|
||
return theme;
|
||
}
|
||
|
||
function Theme(name) {
|
||
var self = this;
|
||
self.name = name;
|
||
self.colors = {};
|
||
|
||
self.dark = setDark;
|
||
setDark(false);
|
||
|
||
function setDark(isDark) {
|
||
isDark = arguments.length === 0 ? true : !!isDark;
|
||
|
||
// If no change, abort
|
||
if (isDark === self.isDark) return;
|
||
|
||
self.isDark = isDark;
|
||
|
||
self.foregroundPalette = self.isDark ? LIGHT_FOREGROUND : DARK_FOREGROUND;
|
||
self.foregroundShadow = self.isDark ? DARK_SHADOW : LIGHT_SHADOW;
|
||
|
||
// Light and dark themes have different default hues.
|
||
// Go through each existing color type for this theme, and for every
|
||
// hue value that is still the default hue value from the previous light/dark setting,
|
||
// set it to the default hue value from the new light/dark setting.
|
||
var newDefaultHues = self.isDark ? DARK_DEFAULT_HUES : LIGHT_DEFAULT_HUES;
|
||
var oldDefaultHues = self.isDark ? LIGHT_DEFAULT_HUES : DARK_DEFAULT_HUES;
|
||
angular.forEach(newDefaultHues, function(newDefaults, colorType) {
|
||
var color = self.colors[colorType];
|
||
var oldDefaults = oldDefaultHues[colorType];
|
||
if (color) {
|
||
for (var hueName in color.hues) {
|
||
if (color.hues[hueName] === oldDefaults[hueName]) {
|
||
color.hues[hueName] = newDefaults[hueName];
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
return self;
|
||
}
|
||
|
||
THEME_COLOR_TYPES.forEach(function(colorType) {
|
||
var defaultHues = (self.isDark ? DARK_DEFAULT_HUES : LIGHT_DEFAULT_HUES)[colorType];
|
||
self[colorType + 'Palette'] = function setPaletteType(paletteName, hues) {
|
||
var color = self.colors[colorType] = {
|
||
name: paletteName,
|
||
hues: angular.extend({}, defaultHues, hues)
|
||
};
|
||
|
||
Object.keys(color.hues).forEach(function(name) {
|
||
if (!defaultHues[name]) {
|
||
throw new Error("Invalid hue name '%1' in theme %2's %3 color %4. Available hue names: %4"
|
||
.replace('%1', name)
|
||
.replace('%2', self.name)
|
||
.replace('%3', paletteName)
|
||
.replace('%4', Object.keys(defaultHues).join(', '))
|
||
);
|
||
}
|
||
});
|
||
Object.keys(color.hues).map(function(key) {
|
||
return color.hues[key];
|
||
}).forEach(function(hueValue) {
|
||
if (VALID_HUE_VALUES.indexOf(hueValue) === -1) {
|
||
throw new Error("Invalid hue value '%1' in theme %2's %3 color %4. Available hue values: %5"
|
||
.replace('%1', hueValue)
|
||
.replace('%2', self.name)
|
||
.replace('%3', colorType)
|
||
.replace('%4', paletteName)
|
||
.replace('%5', VALID_HUE_VALUES.join(', '))
|
||
);
|
||
}
|
||
});
|
||
return self;
|
||
};
|
||
});
|
||
}
|
||
|
||
/**
|
||
* @ngdoc service
|
||
* @name $mdTheming
|
||
* @module material.core.theming
|
||
* @description
|
||
* Service that makes an element apply theming related <b>classes</b> to itself.
|
||
*
|
||
* For more information on the hue objects, their default values, as well as valid hue values, please visit <a ng-href="Theming/03_configuring_a_theme#specifying-custom-hues-for-color-intentions">the custom hues section of Configuring a Theme</a>.
|
||
*
|
||
* <hljs lang="js">
|
||
* // Example component directive that we want to apply theming classes to.
|
||
* app.directive('myFancyDirective', function($mdTheming) {
|
||
* return {
|
||
* restrict: 'AE',
|
||
* link: function(scope, element, attrs) {
|
||
* // Initialize the service using our directive's element
|
||
* $mdTheming(element);
|
||
*
|
||
* $mdTheming.defineTheme('myTheme', {
|
||
* primary: 'blue',
|
||
* primaryHues: {
|
||
* default: '500',
|
||
* hue-1: '300',
|
||
* hue-2: '900',
|
||
* hue-3: 'A100'
|
||
* },
|
||
* accent: 'pink',
|
||
* accentHues: {
|
||
* default: '600',
|
||
* hue-1: '300',
|
||
* hue-2: '200',
|
||
* hue-3: 'A500'
|
||
* },
|
||
* warn: 'red',
|
||
* // It's not necessary to specify all hues in the object.
|
||
* warnHues: {
|
||
* default: '200',
|
||
* hue-3: 'A100'
|
||
* },
|
||
* // It's not necessary to specify custom hues at all.
|
||
* background: 'grey',
|
||
* dark: true
|
||
* });
|
||
* // Your directive's custom code here.
|
||
* }
|
||
* };
|
||
* });
|
||
* </hljs>
|
||
* @param {element=} element Element that will have theming classes applied to it.
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc property
|
||
* @name $mdTheming#THEMES
|
||
* @description
|
||
* Property to get all the themes defined
|
||
* @returns {object} All the themes defined with their properties.
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc property
|
||
* @name $mdTheming#PALETTES
|
||
* @description
|
||
* Property to get all the palettes defined
|
||
* @returns {object} All the palettes defined with their colors.
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdTheming#registered
|
||
* @description
|
||
* Determine is specified theme name is a valid, registered theme
|
||
* @param {string} themeName the theme to check if registered
|
||
* @returns {boolean} whether the theme is registered or not
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdTheming#defaultTheme
|
||
* @description
|
||
* Returns the default theme
|
||
* @returns {string} The default theme
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdTheming#generateTheme
|
||
* @description
|
||
* Lazy generate themes - by default, every theme is generated when defined.
|
||
* You can disable this in the configuration section using the
|
||
* `$mdThemingProvider.generateThemesOnDemand(true);`
|
||
*
|
||
* The theme name that is passed in must match the name of the theme that was defined as part of
|
||
* the configuration block.
|
||
*
|
||
* @param {string} name theme name to generate
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdTheming#setBrowserColor
|
||
* @description
|
||
* Enables browser header coloring. For more info please visit
|
||
* <a href="https://developers.google.com/web/fundamentals/design-and-ui/browser-customization/theme-color">
|
||
* Web Fundamentals</a>.
|
||
* @param {object=} options Options for the browser color, which include:<br/>
|
||
* - `theme` - `{string}`: A defined theme via `$mdThemeProvider` to use the palettes from.
|
||
* Default is `default` theme. <br/>
|
||
* - `palette` - `{string}`: Can be any one of the basic material design palettes, extended
|
||
* defined palettes, or `primary`, `accent`, `background`, and `warn`. Default is `primary`.
|
||
* <br/>
|
||
* - `hue` - `{string}`: The hue from the selected palette. Default is `800`.<br/>
|
||
* @returns {function} Function that removes the browser coloring when called.
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdTheming#defineTheme
|
||
* @description
|
||
* Dynamically define a theme by using an options object that contains palette names.
|
||
*
|
||
* @param {string} name Theme name to define
|
||
* @param {object} options Theme definition options
|
||
*
|
||
* Options are:<br/>
|
||
* - `primary` - `{string}`: The name of the primary palette to use in the theme.<br/>
|
||
* - `primaryHues` - `{object=}`: Override hues for primary palette.<br/>
|
||
* - `accent` - `{string}`: The name of the accent palette to use in the theme.<br/>
|
||
* - `accentHues` - `{object=}`: Override hues for accent palette.<br/>
|
||
* - `warn` - `{string}`: The name of the warn palette to use in the theme.<br/>
|
||
* - `warnHues` - `{object=}`: Override hues for warn palette.<br/>
|
||
* - `background` - `{string}`: The name of the background palette to use in the theme.<br/>
|
||
* - `backgroundHues` - `{object=}`: Override hues for background palette.<br/>
|
||
* - `dark` - `{boolean}`: Indicates if it's a dark theme.<br/>
|
||
* @returns {Promise<string>} A resolved promise with the new theme name.
|
||
*/
|
||
|
||
/* @ngInject */
|
||
function ThemingService($rootScope, $mdUtil, $q, $log) {
|
||
// Allow us to be invoked via a linking function signature.
|
||
var applyTheme = function (scope, el) {
|
||
if (el === undefined) { el = scope; scope = undefined; }
|
||
if (scope === undefined) { scope = $rootScope; }
|
||
applyTheme.inherit(el, el);
|
||
};
|
||
|
||
Object.defineProperty(applyTheme, 'THEMES', {
|
||
get: function () {
|
||
return angular.extend({}, THEMES);
|
||
}
|
||
});
|
||
Object.defineProperty(applyTheme, 'PALETTES', {
|
||
get: function () {
|
||
return angular.extend({}, PALETTES);
|
||
}
|
||
});
|
||
Object.defineProperty(applyTheme, 'ALWAYS_WATCH', {
|
||
get: function () {
|
||
return alwaysWatchTheme;
|
||
}
|
||
});
|
||
applyTheme.inherit = inheritTheme;
|
||
applyTheme.registered = registered;
|
||
applyTheme.defaultTheme = function() { return defaultTheme; };
|
||
applyTheme.generateTheme = function(name) { generateTheme(THEMES[name], name, themeConfig.nonce); };
|
||
applyTheme.defineTheme = function(name, options) {
|
||
options = options || {};
|
||
|
||
var theme = registerTheme(name);
|
||
|
||
if (options.primary) {
|
||
theme.primaryPalette(options.primary, options.primaryHues);
|
||
}
|
||
if (options.accent) {
|
||
theme.accentPalette(options.accent, options.accentHues);
|
||
}
|
||
if (options.warn) {
|
||
theme.warnPalette(options.warn, options.warnHues);
|
||
}
|
||
if (options.background) {
|
||
theme.backgroundPalette(options.background, options.backgroundHues);
|
||
}
|
||
if (options.dark){
|
||
theme.dark();
|
||
}
|
||
|
||
this.generateTheme(name);
|
||
|
||
return $q.resolve(name);
|
||
};
|
||
applyTheme.setBrowserColor = enableBrowserColor;
|
||
|
||
return applyTheme;
|
||
|
||
/**
|
||
* Determine is specified theme name is a valid, registered theme
|
||
*/
|
||
function registered(themeName) {
|
||
if (themeName === undefined || themeName === '') return true;
|
||
return applyTheme.THEMES[themeName] !== undefined;
|
||
}
|
||
|
||
/**
|
||
* Get theme name for the element, then update with Theme CSS class
|
||
*/
|
||
function inheritTheme (el, parent) {
|
||
var ctrl = parent.controller('mdTheme') || el.data('$mdThemeController');
|
||
var scope = el.scope();
|
||
|
||
updateThemeClass(lookupThemeName());
|
||
|
||
if (ctrl) {
|
||
var watchTheme = alwaysWatchTheme ||
|
||
ctrl.$shouldWatch ||
|
||
$mdUtil.parseAttributeBoolean(el.attr('md-theme-watch'));
|
||
|
||
if (watchTheme || ctrl.isAsyncTheme) {
|
||
var clearNameWatcher = function () {
|
||
if (unwatch) {
|
||
unwatch();
|
||
unwatch = undefined;
|
||
}
|
||
};
|
||
|
||
var unwatch = ctrl.registerChanges(function(name) {
|
||
updateThemeClass(name);
|
||
|
||
if (!watchTheme) {
|
||
clearNameWatcher();
|
||
}
|
||
});
|
||
|
||
if (scope) {
|
||
scope.$on('$destroy', clearNameWatcher);
|
||
} else {
|
||
el.on('$destroy', clearNameWatcher);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Find the theme name from the parent controller or element data
|
||
*/
|
||
function lookupThemeName() {
|
||
// As a few components (dialog) add their controllers later, we should also watch for a controller init.
|
||
return ctrl && ctrl.$mdTheme || (defaultTheme === 'default' ? '' : defaultTheme);
|
||
}
|
||
|
||
/**
|
||
* Remove old theme class and apply a new one
|
||
* NOTE: if not a valid theme name, then the current name is not changed
|
||
*/
|
||
function updateThemeClass(theme) {
|
||
if (!theme) return;
|
||
if (!registered(theme)) {
|
||
$log.warn('Attempted to use unregistered theme \'' + theme + '\'. ' +
|
||
'Register it with $mdThemingProvider.theme().');
|
||
}
|
||
|
||
var oldTheme = el.data('$mdThemeName');
|
||
if (oldTheme) el.removeClass('md-' + oldTheme +'-theme');
|
||
el.addClass('md-' + theme + '-theme');
|
||
el.data('$mdThemeName', theme);
|
||
if (ctrl) {
|
||
el.data('$mdThemeController', ctrl);
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
}
|
||
|
||
function ThemingDirective($mdTheming, $interpolate, $parse, $mdUtil, $q, $log) {
|
||
return {
|
||
priority: 101, // has to be more than 100 to be before interpolation (issue on IE)
|
||
link: {
|
||
pre: function(scope, el, attrs) {
|
||
var registeredCallbacks = [];
|
||
|
||
var startSymbol = $interpolate.startSymbol();
|
||
var endSymbol = $interpolate.endSymbol();
|
||
|
||
var theme = attrs.mdTheme.trim();
|
||
|
||
var hasInterpolation =
|
||
theme.substr(0, startSymbol.length) === startSymbol &&
|
||
theme.lastIndexOf(endSymbol) === theme.length - endSymbol.length;
|
||
|
||
var oneTimeOperator = '::';
|
||
var oneTimeBind = attrs.mdTheme
|
||
.split(startSymbol).join('')
|
||
.split(endSymbol).join('')
|
||
.trim()
|
||
.substr(0, oneTimeOperator.length) === oneTimeOperator;
|
||
|
||
var getTheme = function () {
|
||
var interpolation = $interpolate(attrs.mdTheme)(scope);
|
||
return $parse(interpolation)(scope) || interpolation;
|
||
};
|
||
|
||
var ctrl = {
|
||
isAsyncTheme: angular.isFunction(getTheme()) || angular.isFunction(getTheme().then),
|
||
registerChanges: function (cb, context) {
|
||
if (context) {
|
||
cb = angular.bind(context, cb);
|
||
}
|
||
|
||
registeredCallbacks.push(cb);
|
||
|
||
return function () {
|
||
var index = registeredCallbacks.indexOf(cb);
|
||
|
||
if (index > -1) {
|
||
registeredCallbacks.splice(index, 1);
|
||
}
|
||
};
|
||
},
|
||
$setTheme: function (theme) {
|
||
if (!$mdTheming.registered(theme)) {
|
||
$log.warn('attempted to use unregistered theme \'' + theme + '\'');
|
||
}
|
||
|
||
ctrl.$mdTheme = theme;
|
||
|
||
// Iterating backwards to support unregistering during iteration
|
||
// http://stackoverflow.com/a/9882349/890293
|
||
// we don't use `reverse()` of array because it mutates the array and we don't want it
|
||
// to get re-indexed
|
||
for (var i = registeredCallbacks.length; i--;) {
|
||
registeredCallbacks[i](theme);
|
||
}
|
||
},
|
||
$shouldWatch: $mdUtil.parseAttributeBoolean(el.attr('md-theme-watch')) ||
|
||
$mdTheming.ALWAYS_WATCH ||
|
||
(hasInterpolation && !oneTimeBind)
|
||
};
|
||
|
||
el.data('$mdThemeController', ctrl);
|
||
|
||
var setParsedTheme = function (theme) {
|
||
if (typeof theme === 'string') {
|
||
return ctrl.$setTheme(theme);
|
||
}
|
||
|
||
$q.when(angular.isFunction(theme) ? theme() : theme)
|
||
.then(function(name) {
|
||
ctrl.$setTheme(name);
|
||
});
|
||
};
|
||
|
||
setParsedTheme(getTheme());
|
||
|
||
var unwatch = scope.$watch(getTheme, function(theme) {
|
||
if (theme) {
|
||
setParsedTheme(theme);
|
||
|
||
if (!ctrl.$shouldWatch) {
|
||
unwatch();
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Special directive that will disable ALL runtime Theme style generation and DOM injection
|
||
*
|
||
* <link rel="stylesheet" href="angular-material.min.css">
|
||
* <link rel="stylesheet" href="angular-material.themes.css">
|
||
*
|
||
* <body md-themes-disabled>
|
||
* ...
|
||
* </body>
|
||
*
|
||
* Note: Using md-themes-css directive requires the developer to load external
|
||
* theme stylesheets; e.g. custom themes from Material-Tools:
|
||
*
|
||
* `angular-material.themes.css`
|
||
*
|
||
* Another option is to use the ThemingProvider to configure and disable the attribute
|
||
* conversions; this would obviate the use of the `md-themes-css` directive
|
||
*
|
||
*/
|
||
function disableThemesDirective() {
|
||
themeConfig.disableTheming = true;
|
||
|
||
// Return a 1x-only, first-match attribute directive
|
||
return {
|
||
restrict : 'A',
|
||
priority : '900'
|
||
};
|
||
}
|
||
|
||
function ThemableDirective($mdTheming) {
|
||
return $mdTheming;
|
||
}
|
||
|
||
function parseRules(theme, colorType, rules) {
|
||
checkValidPalette(theme, colorType);
|
||
|
||
rules = rules.replace(/THEME_NAME/g, theme.name);
|
||
var themeNameRegex = new RegExp('\\.md-' + theme.name + '-theme', 'g');
|
||
// Matches '{{ primary-color }}', etc
|
||
var hueRegex = new RegExp('([\'"])?{{\\s*([a-zA-Z]+)-?(color|default)?-?(contrast)?-?((?:\\d\\.?\\d*)|(?:[a-zA-Z]+))?\\s*}}(["\'])?','g');
|
||
var simpleVariableRegex = /'?"?{{\s*([a-zA-Z]+)-(A?\d+|hue-[0-3]|shadow|default)-?(contrast)?-?((?:\d\.?\d*)|(?:[a-zA-Z]+))?\s*}}'?"?/g;
|
||
var defaultBgHue = theme.colors['background'].hues['default'];
|
||
var defaultBgContrastType = PALETTES[theme.colors['background'].name][defaultBgHue].contrastType;
|
||
|
||
// find and replace simple variables where we use a specific hue, not an entire palette
|
||
// eg. "{{primary-100}}"
|
||
// \(' + THEME_COLOR_TYPES.join('\|') + '\)'
|
||
rules = rules.replace(simpleVariableRegex, function(match, colorType, hue, contrast, opacity) {
|
||
var regexColorType = colorType;
|
||
if (colorType === 'foreground') {
|
||
if (hue === 'shadow') {
|
||
return theme.foregroundShadow;
|
||
} else if (theme.foregroundPalette[hue]) {
|
||
// Use user defined palette number (ie: foreground-2)
|
||
return rgba(colorToRgbaArray(theme.foregroundPalette[hue]));
|
||
} else if (theme.foregroundPalette['1']){
|
||
return rgba(colorToRgbaArray(theme.foregroundPalette['1']));
|
||
}
|
||
// Default to background-default-contrast-{opacity}
|
||
colorType = 'background';
|
||
contrast = 'contrast';
|
||
if (!opacity && hue) {
|
||
// Convert references to legacy hues to opacities (i.e. foreground-4 to *-divider)
|
||
switch (hue) {
|
||
// hue-1 uses default opacity
|
||
case '2':
|
||
opacity = 'secondary';
|
||
break;
|
||
case '3':
|
||
opacity = 'disabled';
|
||
break;
|
||
case '4':
|
||
opacity = 'divider';
|
||
}
|
||
}
|
||
hue = 'default';
|
||
}
|
||
|
||
// `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];
|
||
}
|
||
|
||
var colorDetails = (PALETTES[ theme.colors[colorType].name ][hue] || '');
|
||
|
||
// If user has specified a foreground color, use those
|
||
if (colorType === 'background' && contrast && regexColorType !== 'foreground' &&
|
||
colorDetails.contrastType === defaultBgContrastType) {
|
||
// Don't process if colorType was changed
|
||
switch (opacity) {
|
||
case 'secondary':
|
||
case 'icon':
|
||
if (theme.foregroundPalette['2']) {
|
||
return rgba(colorToRgbaArray(theme.foregroundPalette['2']));
|
||
}
|
||
break;
|
||
case 'disabled':
|
||
case 'hint':
|
||
if (theme.foregroundPalette['3']) {
|
||
return rgba(colorToRgbaArray(theme.foregroundPalette['3']));
|
||
}
|
||
break;
|
||
case 'divider':
|
||
if (theme.foregroundPalette['4']) {
|
||
return rgba(colorToRgbaArray(theme.foregroundPalette['4']));
|
||
}
|
||
break;
|
||
default:
|
||
if (theme.foregroundPalette['1']) {
|
||
return rgba(colorToRgbaArray(theme.foregroundPalette['1']));
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (contrast && opacity) {
|
||
opacity = colorDetails.opacity[opacity] || opacity;
|
||
}
|
||
|
||
return rgba(colorDetails[contrast ? 'contrast' : 'value'], opacity);
|
||
});
|
||
|
||
var generatedRules = [];
|
||
|
||
// For each type, generate rules for each hue (ie. default, md-hue-1, md-hue-2, md-hue-3)
|
||
angular.forEach(['default', 'hue-1', 'hue-2', 'hue-3'], function(hueName) {
|
||
var newRule = rules
|
||
.replace(hueRegex, function(match, _, matchedColorType, hueType, contrast, opacity) {
|
||
var color = theme.colors[matchedColorType];
|
||
var palette = PALETTES[color.name];
|
||
var hueValue = color.hues[hueName];
|
||
if (contrast && opacity) {
|
||
opacity = palette[hueValue].opacity[opacity] || opacity;
|
||
}
|
||
return rgba(palette[hueValue][hueType === 'color' ? 'value' : 'contrast'], opacity);
|
||
});
|
||
if (hueName !== 'default') {
|
||
newRule = newRule.replace(themeNameRegex, '.md-' + theme.name + '-theme.md-' + hueName);
|
||
}
|
||
|
||
// Don't apply a selector rule to the default theme, making it easier to override
|
||
// styles of the base-component
|
||
if (theme.name === 'default') {
|
||
var themeRuleRegex = /((?:\s|>|\.|\w|-|:|\(|\)|\[|]|"|'|=)*)\.md-default-theme((?:\s|>|\.|\w|-|:|\(|\)|\[|]|"|'|=)*)/g;
|
||
|
||
newRule = newRule.replace(themeRuleRegex, function(match, start, end) {
|
||
return match + ', ' + start + end;
|
||
});
|
||
}
|
||
generatedRules.push(newRule);
|
||
});
|
||
|
||
return generatedRules;
|
||
}
|
||
|
||
var rulesByType = {};
|
||
|
||
// Generate our themes at run time given the state of THEMES and PALETTES
|
||
function generateAllThemes($injector, $mdTheming) {
|
||
var head = document.head;
|
||
var firstChild = head ? head.firstElementChild : null;
|
||
var themeCss = !themeConfig.disableTheming && $injector.has('$MD_THEME_CSS') ? $injector.get('$MD_THEME_CSS') : '';
|
||
|
||
// Append our custom registered styles to the theme stylesheet.
|
||
themeCss += themeConfig.registeredStyles.join('');
|
||
|
||
if (!firstChild) return;
|
||
if (themeCss.length === 0) return; // no rules, so no point in running this expensive task
|
||
|
||
// Expose contrast colors for palettes to ensure that text is always readable
|
||
angular.forEach(PALETTES, sanitizePalette);
|
||
|
||
// MD_THEME_CSS is a string generated by the build process that includes all the themeable
|
||
// components as templates
|
||
|
||
// Break the CSS into individual rules
|
||
var rules = splitCss(themeCss).map(function(rule) {
|
||
return rule.trim();
|
||
});
|
||
|
||
THEME_COLOR_TYPES.forEach(function(type) {
|
||
rulesByType[type] = '';
|
||
});
|
||
|
||
// Sort the rules based on type, allowing us to do color substitution on a per-type basis
|
||
rules.forEach(function(rule) {
|
||
// First: test that if the rule has '.md-accent', it goes into the accent set of rules
|
||
for (var i = 0, type; type = THEME_COLOR_TYPES[i]; i++) {
|
||
if (rule.indexOf('.md-' + type) > -1) {
|
||
return rulesByType[type] += rule;
|
||
}
|
||
}
|
||
|
||
// If no eg 'md-accent' class is found, try to just find 'accent' in the rule and guess from
|
||
// there
|
||
for (i = 0; type = THEME_COLOR_TYPES[i]; i++) {
|
||
if (rule.indexOf(type) > -1) {
|
||
return rulesByType[type] += rule;
|
||
}
|
||
}
|
||
|
||
// Default to the primary array
|
||
return rulesByType[DEFAULT_COLOR_TYPE] += rule;
|
||
});
|
||
|
||
// If themes are being generated on-demand, quit here. The user will later manually
|
||
// call generateTheme to do this on a theme-by-theme basis.
|
||
if (themeConfig.generateOnDemand) return;
|
||
|
||
angular.forEach($mdTheming.THEMES, function(theme) {
|
||
if (!GENERATED[theme.name] && !($mdTheming.defaultTheme() !== 'default' && theme.name === 'default')) {
|
||
generateTheme(theme, theme.name, themeConfig.nonce);
|
||
}
|
||
});
|
||
|
||
|
||
// *************************
|
||
// Internal functions
|
||
// *************************
|
||
|
||
/**
|
||
* The user specifies a 'default' contrast color as either light or dark, then explicitly lists
|
||
* which hues are the opposite contrast (eg. A100 has dark, A200 has light).
|
||
* @param {!object} palette to sanitize
|
||
*/
|
||
function sanitizePalette(palette) {
|
||
var defaultContrast = palette.contrastDefaultColor;
|
||
var lightColors = palette.contrastLightColors || [];
|
||
var strongLightColors = palette.contrastStrongLightColors || [];
|
||
var darkColors = palette.contrastDarkColors || [];
|
||
|
||
// These colors are provided as space-separated lists
|
||
if (typeof lightColors === 'string') lightColors = lightColors.split(' ');
|
||
if (typeof strongLightColors === 'string') strongLightColors = strongLightColors.split(' ');
|
||
if (typeof darkColors === 'string') darkColors = darkColors.split(' ');
|
||
|
||
// Cleanup after ourselves
|
||
delete palette.contrastDefaultColor;
|
||
delete palette.contrastLightColors;
|
||
delete palette.contrastStrongLightColors;
|
||
delete palette.contrastDarkColors;
|
||
|
||
/**
|
||
* @param {string} hueName
|
||
* @return {'dark'|'light'|'strongLight'}
|
||
*/
|
||
function getContrastType(hueName) {
|
||
if (defaultContrast === 'light' ? darkColors.indexOf(hueName) !== -1 :
|
||
(lightColors.indexOf(hueName) === -1 && strongLightColors.indexOf(hueName) === -1)) {
|
||
return 'dark';
|
||
}
|
||
if (strongLightColors.indexOf(hueName) !== -1) {
|
||
return 'strongLight';
|
||
}
|
||
return 'light';
|
||
}
|
||
|
||
/**
|
||
* @param {'dark'|'light'|'strongLight'} contrastType
|
||
* @return {[number, number, number]} [red, green, blue] array
|
||
*/
|
||
function getContrastColor(contrastType) {
|
||
switch (contrastType) {
|
||
default:
|
||
case 'strongLight':
|
||
return STRONG_LIGHT_CONTRAST_COLOR;
|
||
case 'light':
|
||
return LIGHT_CONTRAST_COLOR;
|
||
case 'dark':
|
||
return DARK_CONTRAST_COLOR;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {'dark'|'light'|'strongLight'} contrastType
|
||
* @return {{secondary: number, divider: number, hint: number, icon: number, disabled: number}}
|
||
*/
|
||
function getOpacityValues(contrastType) {
|
||
switch (contrastType) {
|
||
default:
|
||
case 'strongLight':
|
||
return STRONG_LIGHT_CONTRAST_OPACITY;
|
||
case 'light':
|
||
return LIGHT_CONTRAST_OPACITY;
|
||
case 'dark':
|
||
return DARK_CONTRAST_OPACITY;
|
||
}
|
||
}
|
||
// Change { 'A100': '#fffeee' } to { 'A100': { value: '#fffeee', contrast:DARK_CONTRAST_COLOR }
|
||
angular.forEach(palette, function(hueValue, hueName) {
|
||
if (angular.isObject(hueValue)) return; // Already converted
|
||
// Map everything to rgb colors
|
||
var rgbValue = colorToRgbaArray(hueValue);
|
||
if (!rgbValue) {
|
||
throw new Error("Color %1, in palette %2's hue %3, is invalid. Hex or rgb(a) color expected."
|
||
.replace('%1', hueValue)
|
||
.replace('%2', palette.name)
|
||
.replace('%3', hueName));
|
||
}
|
||
|
||
var contrastType = getContrastType(hueName);
|
||
palette[hueName] = {
|
||
hex: palette[hueName],
|
||
value: rgbValue,
|
||
contrastType: contrastType,
|
||
contrast: getContrastColor(contrastType),
|
||
opacity: getOpacityValues(contrastType)
|
||
};
|
||
});
|
||
}
|
||
|
||
/**
|
||
* @param {string} themeCss
|
||
* @returns {[]} a string representing a CSS file that is split, producing an array with a rule
|
||
* at each index.
|
||
*/
|
||
function splitCss(themeCss) {
|
||
var result = [];
|
||
var currentRule = '';
|
||
var openedCurlyBrackets = 0;
|
||
var closedCurlyBrackets = 0;
|
||
|
||
for (var i = 0; i < themeCss.length; i++) {
|
||
var character = themeCss.charAt(i);
|
||
|
||
// Check for content in quotes
|
||
if (character === '\'' || character === '"') {
|
||
// Append text in quotes to current rule
|
||
var textInQuotes = themeCss.substring(i, themeCss.indexOf(character, i + 1));
|
||
currentRule += textInQuotes;
|
||
|
||
// Jump to the closing quote char
|
||
i += textInQuotes.length;
|
||
} else {
|
||
currentRule += character;
|
||
|
||
if (character === '}') {
|
||
closedCurlyBrackets++;
|
||
if (closedCurlyBrackets === openedCurlyBrackets) {
|
||
closedCurlyBrackets = 0;
|
||
openedCurlyBrackets = 0;
|
||
result.push(currentRule);
|
||
currentRule = '';
|
||
}
|
||
} else if (character === '{') {
|
||
openedCurlyBrackets++;
|
||
}
|
||
}
|
||
}
|
||
// Add comments added after last valid rule.
|
||
if (currentRule !== '') {
|
||
result.push(currentRule);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
}
|
||
|
||
function generateTheme(theme, name, nonce) {
|
||
var head = document.head;
|
||
var firstChild = head ? head.firstElementChild : null;
|
||
|
||
if (!GENERATED[name]) {
|
||
// For each theme, use the color palettes specified for
|
||
// `primary`, `warn` and `accent` to generate CSS rules.
|
||
THEME_COLOR_TYPES.forEach(function(colorType) {
|
||
var styleStrings = parseRules(theme, colorType, rulesByType[colorType]);
|
||
while (styleStrings.length) {
|
||
var styleContent = styleStrings.shift();
|
||
if (styleContent) {
|
||
var style = document.createElement('style');
|
||
style.setAttribute('md-theme-style', '');
|
||
if (nonce) {
|
||
style.setAttribute('nonce', nonce);
|
||
}
|
||
style.appendChild(document.createTextNode(styleContent));
|
||
head.insertBefore(style, firstChild);
|
||
}
|
||
}
|
||
});
|
||
|
||
GENERATED[theme.name] = true;
|
||
}
|
||
|
||
}
|
||
|
||
|
||
function checkValidPalette(theme, colorType) {
|
||
// If theme attempts to use a palette that doesnt exist, throw error
|
||
if (!PALETTES[ (theme.colors[colorType] || {}).name ]) {
|
||
throw new Error(
|
||
"You supplied an invalid color palette for theme %1's %2 palette. Available palettes: %3"
|
||
.replace('%1', theme.name)
|
||
.replace('%2', colorType)
|
||
.replace('%3', Object.keys(PALETTES).join(', '))
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {string} clr rbg or rgba color
|
||
* @return {number[]|undefined} [red, green, blue] array if it can be computed
|
||
*/
|
||
function colorToRgbaArray(clr) {
|
||
if (angular.isArray(clr) && clr.length === 3) return clr;
|
||
if (/^rgb/.test(clr)) {
|
||
return clr.replace(/(^\s*rgba?\(|\)\s*$)/g, '').split(',').map(function(value, i) {
|
||
return i === 3 ? parseFloat(value) : parseInt(value, 10);
|
||
});
|
||
}
|
||
if (clr.charAt(0) === '#') clr = clr.substring(1);
|
||
if (!/^([a-fA-F0-9]{3}){1,2}$/g.test(clr)) return;
|
||
|
||
var dig = clr.length / 3;
|
||
var red = clr.substr(0, dig);
|
||
var grn = clr.substr(dig, dig);
|
||
var blu = clr.substr(dig * 2);
|
||
if (dig === 1) {
|
||
red += red;
|
||
grn += grn;
|
||
blu += blu;
|
||
}
|
||
return [parseInt(red, 16), parseInt(grn, 16), parseInt(blu, 16)];
|
||
}
|
||
|
||
function rgba(rgbArray, opacity) {
|
||
if (!rgbArray) return "rgb('0,0,0')";
|
||
|
||
if (rgbArray.length === 4) {
|
||
rgbArray = angular.copy(rgbArray);
|
||
opacity ? rgbArray.pop() : opacity = rgbArray.pop();
|
||
}
|
||
return opacity && (typeof opacity == 'number' || (typeof opacity == 'string' && opacity.length)) ?
|
||
'rgba(' + rgbArray.join(',') + ',' + opacity + ')' :
|
||
'rgb(' + rgbArray.join(',') + ')';
|
||
}
|
||
|
||
|
||
})(window.angular);
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.autocomplete
|
||
*/
|
||
/*
|
||
* @see js folder for autocomplete implementation
|
||
*/
|
||
angular.module('material.components.autocomplete', [
|
||
'material.core',
|
||
'material.components.icon',
|
||
'material.components.virtualRepeat'
|
||
]);
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
|
||
MdAutocompleteCtrl.$inject = ["$scope", "$element", "$mdUtil", "$mdConstant", "$mdTheming", "$window", "$animate", "$rootElement", "$attrs", "$q", "$log", "$mdLiveAnnouncer"];angular
|
||
.module('material.components.autocomplete')
|
||
.controller('MdAutocompleteCtrl', MdAutocompleteCtrl);
|
||
|
||
var ITEM_HEIGHT = 48,
|
||
MAX_ITEMS = 5,
|
||
MENU_PADDING = 8,
|
||
INPUT_PADDING = 2, // Padding provided by `md-input-container`
|
||
MODE_STANDARD = 'standard',
|
||
MODE_VIRTUAL = 'virtual';
|
||
|
||
function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, $window,
|
||
$animate, $rootElement, $attrs, $q, $log, $mdLiveAnnouncer) {
|
||
|
||
// Internal Variables.
|
||
var ctrl = this,
|
||
itemParts = $scope.itemsExpr.split(/ in /i),
|
||
itemExpr = itemParts[ 1 ],
|
||
elements = null,
|
||
cache = {},
|
||
noBlur = false,
|
||
selectedItemWatchers = [],
|
||
hasFocus = false,
|
||
fetchesInProgress = 0,
|
||
enableWrapScroll = null,
|
||
inputModelCtrl = null,
|
||
debouncedOnResize = $mdUtil.debounce(onWindowResize),
|
||
mode = MODE_VIRTUAL; // default
|
||
|
||
/**
|
||
* The root document element. This is used for attaching a top-level click handler to
|
||
* close the options 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 {!Object} angular.JQLite
|
||
*/
|
||
ctrl.documentElement = angular.element(document.documentElement);
|
||
|
||
// Public Exported Variables with handlers
|
||
defineProperty('hidden', handleHiddenChange, true);
|
||
|
||
// Public Exported Variables
|
||
ctrl.scope = $scope;
|
||
ctrl.parent = $scope.$parent;
|
||
ctrl.itemName = itemParts[0];
|
||
ctrl.matches = [];
|
||
ctrl.loading = false;
|
||
ctrl.hidden = true;
|
||
ctrl.index = -1;
|
||
ctrl.activeOption = null;
|
||
ctrl.id = $mdUtil.nextUid();
|
||
ctrl.isDisabled = null;
|
||
ctrl.isRequired = null;
|
||
ctrl.isReadonly = null;
|
||
ctrl.hasNotFound = false;
|
||
ctrl.selectedMessage = $scope.selectedMessage || 'selected';
|
||
ctrl.noMatchMessage = $scope.noMatchMessage || 'There are no matches available.';
|
||
ctrl.singleMatchMessage = $scope.singleMatchMessage || 'There is 1 match available.';
|
||
ctrl.multipleMatchStartMessage = $scope.multipleMatchStartMessage || 'There are ';
|
||
ctrl.multipleMatchEndMessage = $scope.multipleMatchEndMessage || ' matches available.';
|
||
ctrl.defaultEscapeOptions = 'clear';
|
||
|
||
// Public Exported Methods
|
||
ctrl.keydown = keydown;
|
||
ctrl.blur = blur;
|
||
ctrl.focus = focus;
|
||
ctrl.clear = clearValue;
|
||
ctrl.select = select;
|
||
ctrl.listEnter = onListEnter;
|
||
ctrl.listLeave = onListLeave;
|
||
ctrl.focusInput = focusInputElement;
|
||
ctrl.getCurrentDisplayValue = getCurrentDisplayValue;
|
||
ctrl.registerSelectedItemWatcher = registerSelectedItemWatcher;
|
||
ctrl.unregisterSelectedItemWatcher = unregisterSelectedItemWatcher;
|
||
ctrl.notFoundVisible = notFoundVisible;
|
||
ctrl.loadingIsVisible = loadingIsVisible;
|
||
ctrl.positionDropdown = positionDropdown;
|
||
|
||
/**
|
||
* Report types to be used for the $mdLiveAnnouncer
|
||
* @enum {number} Unique flag id.
|
||
*/
|
||
var ReportType = {
|
||
Count: 1,
|
||
Selected: 2
|
||
};
|
||
|
||
return init();
|
||
|
||
// initialization methods
|
||
|
||
/**
|
||
* Initialize the controller, setup watchers, gather elements
|
||
*/
|
||
function init () {
|
||
|
||
$mdUtil.initOptionalProperties($scope, $attrs, {
|
||
searchText: '',
|
||
selectedItem: null,
|
||
clearButton: false,
|
||
disableVirtualRepeat: false,
|
||
});
|
||
|
||
$mdTheming($element);
|
||
configureWatchers();
|
||
$mdUtil.nextTick(function () {
|
||
|
||
gatherElements();
|
||
moveDropdown();
|
||
|
||
// Touch devices often do not send a click event on tap. We still want to focus the input
|
||
// and open the options pop-up in these cases.
|
||
$element.on('touchstart', focusInputElement);
|
||
|
||
// Forward all focus events to the input element when autofocus is enabled
|
||
if ($scope.autofocus) {
|
||
$element.on('focus', focusInputElement);
|
||
}
|
||
if ($scope.inputAriaDescribedBy) {
|
||
elements.input.setAttribute('aria-describedby', $scope.inputAriaDescribedBy);
|
||
}
|
||
if (!$scope.floatingLabel) {
|
||
if ($scope.inputAriaLabel) {
|
||
elements.input.setAttribute('aria-label', $scope.inputAriaLabel);
|
||
} else if ($scope.inputAriaLabelledBy) {
|
||
elements.input.setAttribute('aria-labelledby', $scope.inputAriaLabelledBy);
|
||
} else if ($scope.placeholder) {
|
||
// If no aria-label or aria-labelledby references are defined, then just label using the
|
||
// placeholder.
|
||
elements.input.setAttribute('aria-label', $scope.placeholder);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
function updateModelValidators() {
|
||
if (!$scope.requireMatch || !inputModelCtrl) return;
|
||
|
||
inputModelCtrl.$setValidity('md-require-match', !!$scope.selectedItem || !$scope.searchText);
|
||
}
|
||
|
||
/**
|
||
* Calculates the dropdown's position and applies the new styles to the menu element
|
||
* @returns {*}
|
||
*/
|
||
function positionDropdown () {
|
||
if (!elements) {
|
||
return $mdUtil.nextTick(positionDropdown, false, $scope);
|
||
}
|
||
|
||
var dropdownHeight = ($scope.dropdownItems || MAX_ITEMS) * ITEM_HEIGHT;
|
||
var hrect = elements.wrap.getBoundingClientRect(),
|
||
vrect = elements.snap.getBoundingClientRect(),
|
||
root = elements.root.getBoundingClientRect(),
|
||
top = vrect.bottom - root.top,
|
||
bot = root.bottom - vrect.top,
|
||
left = hrect.left - root.left,
|
||
width = hrect.width,
|
||
offset = getVerticalOffset(),
|
||
position = $scope.dropdownPosition,
|
||
styles, enoughBottomSpace, enoughTopSpace;
|
||
var bottomSpace = root.bottom - vrect.bottom - MENU_PADDING + $mdUtil.getViewportTop();
|
||
var topSpace = vrect.top - MENU_PADDING;
|
||
|
||
// Automatically determine dropdown placement based on available space in viewport.
|
||
if (!position) {
|
||
enoughTopSpace = topSpace > dropdownHeight;
|
||
enoughBottomSpace = bottomSpace > dropdownHeight;
|
||
if (enoughBottomSpace) {
|
||
position = 'bottom';
|
||
} else if (enoughTopSpace) {
|
||
position = 'top';
|
||
} else {
|
||
position = topSpace > bottomSpace ? 'top' : 'bottom';
|
||
}
|
||
}
|
||
// Adjust the width to account for the padding provided by `md-input-container`
|
||
if ($attrs.mdFloatingLabel) {
|
||
left += INPUT_PADDING;
|
||
width -= INPUT_PADDING * 2;
|
||
}
|
||
styles = {
|
||
left: left + 'px',
|
||
minWidth: width + 'px',
|
||
maxWidth: Math.max(hrect.right - root.left, root.right - hrect.left) - MENU_PADDING + 'px'
|
||
};
|
||
|
||
if (position === 'top') {
|
||
styles.top = 'auto';
|
||
styles.bottom = bot + 'px';
|
||
styles.maxHeight = Math.min(dropdownHeight, topSpace) + 'px';
|
||
} else {
|
||
bottomSpace = root.bottom - hrect.bottom - MENU_PADDING + $mdUtil.getViewportTop();
|
||
|
||
styles.top = (top - offset) + 'px';
|
||
styles.bottom = 'auto';
|
||
styles.maxHeight = Math.min(dropdownHeight, bottomSpace) + 'px';
|
||
}
|
||
|
||
elements.$.scrollContainer.css(styles);
|
||
$mdUtil.nextTick(correctHorizontalAlignment, false, $scope);
|
||
|
||
/**
|
||
* Calculates the vertical offset for floating label examples to account for ngMessages
|
||
* @returns {number}
|
||
*/
|
||
function getVerticalOffset () {
|
||
var offset = 0;
|
||
var inputContainer = $element.find('md-input-container');
|
||
if (inputContainer.length) {
|
||
var input = inputContainer.find('input');
|
||
offset = inputContainer.prop('offsetHeight');
|
||
offset -= input.prop('offsetTop');
|
||
offset -= input.prop('offsetHeight');
|
||
// add in the height left up top for the floating label text
|
||
offset += inputContainer.prop('offsetTop');
|
||
}
|
||
return offset;
|
||
}
|
||
|
||
/**
|
||
* Makes sure that the menu doesn't go off of the screen on either side.
|
||
*/
|
||
function correctHorizontalAlignment () {
|
||
var dropdown = elements.scrollContainer.getBoundingClientRect(),
|
||
styles = {};
|
||
if (dropdown.right > root.right) {
|
||
styles.left = (hrect.right - dropdown.width) + 'px';
|
||
}
|
||
elements.$.scrollContainer.css(styles);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Moves the dropdown menu to the body tag in order to avoid z-index and overflow issues.
|
||
*/
|
||
function moveDropdown () {
|
||
if (!elements.$.root.length) return;
|
||
$mdTheming(elements.$.scrollContainer);
|
||
elements.$.scrollContainer.detach();
|
||
elements.$.root.append(elements.$.scrollContainer);
|
||
if ($animate.pin) $animate.pin(elements.$.scrollContainer, $rootElement);
|
||
}
|
||
|
||
/**
|
||
* Sends focus to the input element.
|
||
*/
|
||
function focusInputElement () {
|
||
elements.input.focus();
|
||
}
|
||
|
||
/**
|
||
* Update the activeOption based on the selected item in the listbox.
|
||
* The activeOption is used in the template to set the aria-activedescendant attribute, which
|
||
* enables screen readers to properly handle visual focus within the listbox and announce the
|
||
* item's place in the list. I.e. "List item 3 of 50". Anytime that `ctrl.index` changes, this
|
||
* function needs to be called to update the activeOption.
|
||
*/
|
||
function updateActiveOption() {
|
||
var selectedOption = elements.scroller.querySelector('.selected');
|
||
if (selectedOption) {
|
||
ctrl.activeOption = selectedOption.id;
|
||
} else {
|
||
ctrl.activeOption = null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Sets up any watchers used by autocomplete
|
||
*/
|
||
function configureWatchers () {
|
||
var wait = parseInt($scope.delay, 10) || 0;
|
||
|
||
$attrs.$observe('disabled', function (value) { ctrl.isDisabled = $mdUtil.parseAttributeBoolean(value, false); });
|
||
$attrs.$observe('required', function (value) { ctrl.isRequired = $mdUtil.parseAttributeBoolean(value, false); });
|
||
$attrs.$observe('readonly', function (value) { ctrl.isReadonly = $mdUtil.parseAttributeBoolean(value, false); });
|
||
|
||
$scope.$watch('searchText', wait ? $mdUtil.debounce(handleSearchText, wait) : handleSearchText);
|
||
$scope.$watch('selectedItem', selectedItemChange);
|
||
|
||
angular.element($window).on('resize', debouncedOnResize);
|
||
|
||
$scope.$on('$destroy', cleanup);
|
||
}
|
||
|
||
/**
|
||
* Removes any events or leftover elements created by this controller
|
||
*/
|
||
function cleanup () {
|
||
if (!ctrl.hidden) {
|
||
$mdUtil.enableScrolling();
|
||
}
|
||
|
||
angular.element($window).off('resize', debouncedOnResize);
|
||
|
||
if (elements){
|
||
var items = ['ul', 'scroller', 'scrollContainer', 'input'];
|
||
angular.forEach(items, function(key){
|
||
elements.$[key].remove();
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Event handler to be called whenever the window resizes.
|
||
*/
|
||
function onWindowResize() {
|
||
if (!ctrl.hidden) {
|
||
positionDropdown();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Gathers all of the elements needed for this controller
|
||
*/
|
||
function gatherElements () {
|
||
|
||
var snapWrap = gatherSnapWrap();
|
||
|
||
elements = {
|
||
main: $element[0],
|
||
scrollContainer: $element[0].querySelector('.md-virtual-repeat-container, .md-standard-list-container'),
|
||
scroller: $element[0].querySelector('.md-virtual-repeat-scroller, .md-standard-list-scroller'),
|
||
ul: $element.find('ul')[0],
|
||
input: $element.find('input')[0],
|
||
wrap: snapWrap.wrap,
|
||
snap: snapWrap.snap,
|
||
root: document.body,
|
||
};
|
||
|
||
elements.li = elements.ul.getElementsByTagName('li');
|
||
elements.$ = getAngularElements(elements);
|
||
mode = elements.scrollContainer.classList.contains('md-standard-list-container') ? MODE_STANDARD : MODE_VIRTUAL;
|
||
inputModelCtrl = elements.$.input.controller('ngModel');
|
||
}
|
||
|
||
/**
|
||
* Gathers the snap and wrap elements
|
||
*
|
||
*/
|
||
function gatherSnapWrap() {
|
||
var element;
|
||
var value;
|
||
for (element = $element; element.length; element = element.parent()) {
|
||
value = element.attr('md-autocomplete-snap');
|
||
if (angular.isDefined(value)) break;
|
||
}
|
||
|
||
if (element.length) {
|
||
return {
|
||
snap: element[0],
|
||
wrap: (value.toLowerCase() === 'width') ? element[0] : $element.find('md-autocomplete-wrap')[0]
|
||
};
|
||
}
|
||
|
||
var wrap = $element.find('md-autocomplete-wrap')[0];
|
||
return {
|
||
snap: wrap,
|
||
wrap: wrap
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Gathers angular-wrapped versions of each element
|
||
* @param elements
|
||
* @returns {{}}
|
||
*/
|
||
function getAngularElements (elements) {
|
||
var obj = {};
|
||
for (var key in elements) {
|
||
if (elements.hasOwnProperty(key)) obj[ key ] = angular.element(elements[ key ]);
|
||
}
|
||
return obj;
|
||
}
|
||
|
||
// event/change handlers
|
||
|
||
/**
|
||
* @param {Event} $event
|
||
*/
|
||
function preventDefault($event) {
|
||
$event.preventDefault();
|
||
}
|
||
|
||
/**
|
||
* @param {Event} $event
|
||
*/
|
||
function stopPropagation($event) {
|
||
$event.stopPropagation();
|
||
}
|
||
|
||
/**
|
||
* Handles changes to the `hidden` property.
|
||
* @param {boolean} hidden true to hide the options pop-up, false to show it.
|
||
* @param {boolean} oldHidden the previous value of hidden
|
||
*/
|
||
function handleHiddenChange (hidden, oldHidden) {
|
||
var scrollContainerElement;
|
||
|
||
if (elements) {
|
||
scrollContainerElement = angular.element(elements.scrollContainer);
|
||
}
|
||
if (!hidden && oldHidden) {
|
||
positionDropdown();
|
||
|
||
// Report in polite mode, because the screen reader should finish the default description of
|
||
// the input element.
|
||
reportMessages(true, ReportType.Count | ReportType.Selected);
|
||
|
||
if (elements) {
|
||
$mdUtil.disableScrollAround(elements.scrollContainer);
|
||
enableWrapScroll = disableElementScrollEvents(elements.wrap);
|
||
if ($mdUtil.isIos) {
|
||
ctrl.documentElement.on('touchend', handleTouchOutsidePanel);
|
||
if (scrollContainerElement) {
|
||
scrollContainerElement.on('touchstart touchmove touchend', stopPropagation);
|
||
}
|
||
}
|
||
ctrl.index = getDefaultIndex();
|
||
$mdUtil.nextTick(function() {
|
||
updateActiveOption();
|
||
updateScroll();
|
||
});
|
||
}
|
||
} else if (hidden && !oldHidden) {
|
||
if ($mdUtil.isIos) {
|
||
ctrl.documentElement.off('touchend', handleTouchOutsidePanel);
|
||
if (scrollContainerElement) {
|
||
scrollContainerElement.off('touchstart touchmove touchend', stopPropagation);
|
||
}
|
||
}
|
||
$mdUtil.enableScrolling();
|
||
|
||
if (enableWrapScroll) {
|
||
enableWrapScroll();
|
||
enableWrapScroll = null;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Handling touch events that bubble up to the document is required for closing the dropdown
|
||
* panel on touch outside of the options pop-up panel on iOS.
|
||
* @param {Event} $event
|
||
*/
|
||
function handleTouchOutsidePanel($event) {
|
||
ctrl.hidden = true;
|
||
// iOS does not blur the pop-up for touches on the scroll mask, so we have to do it.
|
||
doBlur(true);
|
||
}
|
||
|
||
/**
|
||
* Disables scrolling for a specific element.
|
||
* @param {!string|!DOMElement} element to disable scrolling
|
||
* @return {Function} function to call to re-enable scrolling for the element
|
||
*/
|
||
function disableElementScrollEvents(element) {
|
||
var elementToDisable = angular.element(element);
|
||
elementToDisable.on('wheel touchmove', preventDefault);
|
||
|
||
return function() {
|
||
elementToDisable.off('wheel touchmove', preventDefault);
|
||
};
|
||
}
|
||
|
||
/**
|
||
* When the user mouses over the dropdown menu, ignore blur events.
|
||
*/
|
||
function onListEnter () {
|
||
noBlur = true;
|
||
}
|
||
|
||
/**
|
||
* When the user's mouse leaves the menu, blur events may hide the menu again.
|
||
*/
|
||
function onListLeave () {
|
||
if (!hasFocus && !ctrl.hidden) elements.input.focus();
|
||
noBlur = false;
|
||
ctrl.hidden = shouldHide();
|
||
}
|
||
|
||
/**
|
||
* Handles changes to the selected item.
|
||
* @param selectedItem
|
||
* @param previousSelectedItem
|
||
*/
|
||
function selectedItemChange (selectedItem, previousSelectedItem) {
|
||
|
||
updateModelValidators();
|
||
|
||
if (selectedItem) {
|
||
getDisplayValue(selectedItem).then(function (val) {
|
||
$scope.searchText = val;
|
||
handleSelectedItemChange(selectedItem, previousSelectedItem);
|
||
});
|
||
} else if (previousSelectedItem && $scope.searchText) {
|
||
getDisplayValue(previousSelectedItem).then(function(displayValue) {
|
||
// Clear the searchText, when the selectedItem is set to null.
|
||
// Do not clear the searchText, when the searchText isn't matching with the previous
|
||
// selected item.
|
||
if (angular.isString($scope.searchText)
|
||
&& displayValue.toString().toLowerCase() === $scope.searchText.toLowerCase()) {
|
||
$scope.searchText = '';
|
||
}
|
||
});
|
||
}
|
||
|
||
if (selectedItem !== previousSelectedItem) {
|
||
announceItemChange();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Use the user-defined expression to announce changes each time a new item is selected
|
||
*/
|
||
function announceItemChange () {
|
||
angular.isFunction($scope.itemChange) &&
|
||
$scope.itemChange(getItemAsNameVal($scope.selectedItem));
|
||
}
|
||
|
||
/**
|
||
* Use the user-defined expression to announce changes each time the search text is changed
|
||
*/
|
||
function announceTextChange () {
|
||
angular.isFunction($scope.textChange) && $scope.textChange();
|
||
}
|
||
|
||
/**
|
||
* Calls any external watchers listening for the selected item. Used in conjunction with
|
||
* `registerSelectedItemWatcher`.
|
||
* @param selectedItem
|
||
* @param previousSelectedItem
|
||
*/
|
||
function handleSelectedItemChange (selectedItem, previousSelectedItem) {
|
||
selectedItemWatchers.forEach(function (watcher) {
|
||
watcher(selectedItem, previousSelectedItem);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Register a function to be called when the selected item changes.
|
||
* @param cb
|
||
*/
|
||
function registerSelectedItemWatcher (cb) {
|
||
if (selectedItemWatchers.indexOf(cb) === -1) {
|
||
selectedItemWatchers.push(cb);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Unregister a function previously registered for selected item changes.
|
||
* @param cb
|
||
*/
|
||
function unregisterSelectedItemWatcher (cb) {
|
||
var i = selectedItemWatchers.indexOf(cb);
|
||
if (i !== -1) {
|
||
selectedItemWatchers.splice(i, 1);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Handles changes to the searchText property.
|
||
* @param {string} searchText
|
||
* @param {string} previousSearchText
|
||
*/
|
||
function handleSearchText (searchText, previousSearchText) {
|
||
ctrl.index = getDefaultIndex();
|
||
|
||
// do nothing on init
|
||
if (searchText === previousSearchText) return;
|
||
|
||
updateModelValidators();
|
||
|
||
getDisplayValue($scope.selectedItem).then(function (val) {
|
||
// clear selected item if search text no longer matches it
|
||
if (searchText !== val) {
|
||
$scope.selectedItem = null;
|
||
|
||
// trigger change event if available
|
||
if (searchText !== previousSearchText) {
|
||
announceTextChange();
|
||
}
|
||
|
||
// cancel results if search text is not long enough
|
||
if (!isMinLengthMet()) {
|
||
ctrl.matches = [];
|
||
|
||
setLoading(false);
|
||
reportMessages(true, ReportType.Count);
|
||
|
||
} else {
|
||
handleQuery();
|
||
}
|
||
}
|
||
});
|
||
|
||
}
|
||
|
||
/**
|
||
* Handles input blur event, determines if the dropdown should hide.
|
||
* @param {Event=} $event
|
||
*/
|
||
function blur($event) {
|
||
hasFocus = false;
|
||
|
||
if (!noBlur) {
|
||
ctrl.hidden = shouldHide();
|
||
evalAttr('ngBlur', { $event: $event });
|
||
} else if (angular.isObject($event)) {
|
||
$event.stopImmediatePropagation();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Force blur on input element
|
||
* @param {boolean} forceBlur
|
||
*/
|
||
function doBlur(forceBlur) {
|
||
if (forceBlur) {
|
||
noBlur = false;
|
||
hasFocus = false;
|
||
}
|
||
elements.input.blur();
|
||
}
|
||
|
||
/**
|
||
* Handles input focus event, determines if the dropdown should show.
|
||
*/
|
||
function focus($event) {
|
||
hasFocus = true;
|
||
|
||
if (isSearchable() && isMinLengthMet()) {
|
||
handleQuery();
|
||
}
|
||
|
||
ctrl.hidden = shouldHide();
|
||
|
||
evalAttr('ngFocus', { $event: $event });
|
||
}
|
||
|
||
/**
|
||
* Handles keyboard input.
|
||
* @param event
|
||
*/
|
||
function keydown (event) {
|
||
switch (event.keyCode) {
|
||
case $mdConstant.KEY_CODE.DOWN_ARROW:
|
||
if (ctrl.loading || hasSelection()) return;
|
||
event.stopPropagation();
|
||
event.preventDefault();
|
||
ctrl.index = ctrl.index + 1 > ctrl.matches.length - 1 ? 0 : Math.min(ctrl.index + 1, ctrl.matches.length - 1);
|
||
$mdUtil.nextTick(updateActiveOption);
|
||
updateScroll();
|
||
break;
|
||
case $mdConstant.KEY_CODE.UP_ARROW:
|
||
if (ctrl.loading || hasSelection()) return;
|
||
event.stopPropagation();
|
||
event.preventDefault();
|
||
ctrl.index = ctrl.index - 1 < 0 ? ctrl.matches.length - 1 : Math.max(0, ctrl.index - 1);
|
||
$mdUtil.nextTick(updateActiveOption);
|
||
updateScroll();
|
||
break;
|
||
case $mdConstant.KEY_CODE.TAB:
|
||
// If we hit tab, assume that we've left the list so it will close
|
||
onListLeave();
|
||
|
||
if (ctrl.hidden || ctrl.loading || ctrl.index < 0 || ctrl.matches.length < 1) return;
|
||
select(ctrl.index);
|
||
break;
|
||
case $mdConstant.KEY_CODE.ENTER:
|
||
if (ctrl.hidden || ctrl.loading || ctrl.index < 0 || ctrl.matches.length < 1) return;
|
||
if (hasSelection()) return;
|
||
event.stopImmediatePropagation();
|
||
event.preventDefault();
|
||
select(ctrl.index);
|
||
break;
|
||
case $mdConstant.KEY_CODE.ESCAPE:
|
||
event.preventDefault(); // Prevent browser from always clearing input
|
||
if (!shouldProcessEscape()) return;
|
||
event.stopPropagation();
|
||
|
||
clearSelectedItem();
|
||
if ($scope.searchText && hasEscapeOption('clear')) {
|
||
clearSearchText();
|
||
}
|
||
|
||
// Manually hide (needed for mdNotFound support)
|
||
ctrl.hidden = true;
|
||
|
||
if (hasEscapeOption('blur')) {
|
||
// Force the component to blur if they hit escape
|
||
doBlur(true);
|
||
}
|
||
|
||
break;
|
||
default:
|
||
}
|
||
}
|
||
|
||
// getters
|
||
|
||
/**
|
||
* Returns the minimum length needed to display the dropdown.
|
||
* @returns {*}
|
||
*/
|
||
function getMinLength () {
|
||
return angular.isNumber($scope.minLength) ? $scope.minLength : 1;
|
||
}
|
||
|
||
/**
|
||
* Returns the display value for an item.
|
||
* @param {*} item
|
||
* @returns {*}
|
||
*/
|
||
function getDisplayValue (item) {
|
||
return $q.when(getItemText(item) || item).then(function(itemText) {
|
||
if (itemText && !angular.isString(itemText)) {
|
||
$log.warn('md-autocomplete: Could not resolve display value to a string. ' +
|
||
'Please check the `md-item-text` attribute.');
|
||
}
|
||
|
||
return itemText;
|
||
});
|
||
|
||
/**
|
||
* Getter function to invoke user-defined expression (in the directive)
|
||
* to convert your object to a single string.
|
||
* @param {*} item
|
||
* @returns {string|null}
|
||
*/
|
||
function getItemText (item) {
|
||
return (item && $scope.itemText) ? $scope.itemText(getItemAsNameVal(item)) : null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Returns the locals object for compiling item templates.
|
||
* @param {*} item
|
||
* @returns {Object|undefined}
|
||
*/
|
||
function getItemAsNameVal (item) {
|
||
if (!item) {
|
||
return undefined;
|
||
}
|
||
|
||
var locals = {};
|
||
if (ctrl.itemName) {
|
||
locals[ ctrl.itemName ] = item;
|
||
}
|
||
|
||
return locals;
|
||
}
|
||
|
||
/**
|
||
* Returns the default index based on whether or not autoselect is enabled.
|
||
* @returns {number} 0 if autoselect is enabled, -1 if not.
|
||
*/
|
||
function getDefaultIndex () {
|
||
return $scope.autoselect ? 0 : -1;
|
||
}
|
||
|
||
/**
|
||
* Sets the loading parameter and updates the hidden state.
|
||
* @param value {boolean} Whether or not the component is currently loading.
|
||
*/
|
||
function setLoading(value) {
|
||
if (ctrl.loading !== value) {
|
||
ctrl.loading = value;
|
||
}
|
||
|
||
// Always refresh the hidden variable as something else might have changed
|
||
ctrl.hidden = shouldHide();
|
||
}
|
||
|
||
/**
|
||
* Determines if the menu should be hidden.
|
||
* @returns {boolean} true if the menu should be hidden
|
||
*/
|
||
function shouldHide () {
|
||
return !shouldShow();
|
||
}
|
||
|
||
/**
|
||
* Determines whether the autocomplete is able to query within the current state.
|
||
* @returns {boolean} true if the query can be run
|
||
*/
|
||
function isSearchable() {
|
||
if (ctrl.loading && !hasMatches()) {
|
||
// No query when query is in progress.
|
||
return false;
|
||
} else if (hasSelection()) {
|
||
// No query if there is already a selection
|
||
return false;
|
||
}
|
||
else if (!hasFocus) {
|
||
// No query if the input does not have focus
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* @returns {boolean} if the escape keydown should be processed, return true.
|
||
* Otherwise return false.
|
||
*/
|
||
function shouldProcessEscape() {
|
||
return hasEscapeOption('blur') || !ctrl.hidden || ctrl.loading || hasEscapeOption('clear') && $scope.searchText;
|
||
}
|
||
|
||
/**
|
||
* @param {string} option check if this option is set
|
||
* @returns {boolean} if the specified escape option is set, return true. Return false otherwise.
|
||
*/
|
||
function hasEscapeOption(option) {
|
||
if (!angular.isString($scope.escapeOptions)) {
|
||
return ctrl.defaultEscapeOptions.indexOf(option) !== -1;
|
||
} else {
|
||
return $scope.escapeOptions.toLowerCase().indexOf(option) !== -1;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Determines if the menu should be shown.
|
||
* @returns {boolean} true if the menu should be shown
|
||
*/
|
||
function shouldShow() {
|
||
if (ctrl.isReadonly) {
|
||
// Don't show if read only is set
|
||
return false;
|
||
} else if (!isSearchable()) {
|
||
// Don't show if a query is in progress, there is already a selection,
|
||
// or the input is not focused.
|
||
return false;
|
||
}
|
||
return (isMinLengthMet() && hasMatches()) || notFoundVisible();
|
||
}
|
||
|
||
/**
|
||
* @returns {boolean} true if the search text has matches.
|
||
*/
|
||
function hasMatches() {
|
||
return ctrl.matches.length ? true : false;
|
||
}
|
||
|
||
/**
|
||
* @returns {boolean} true if the autocomplete has a valid selection.
|
||
*/
|
||
function hasSelection() {
|
||
return ctrl.scope.selectedItem ? true : false;
|
||
}
|
||
|
||
/**
|
||
* @returns {boolean} true if the loading indicator is, or should be, visible.
|
||
*/
|
||
function loadingIsVisible() {
|
||
return ctrl.loading && !hasSelection();
|
||
}
|
||
|
||
/**
|
||
* @returns {*} the display value of the current item.
|
||
*/
|
||
function getCurrentDisplayValue () {
|
||
return getDisplayValue(ctrl.matches[ ctrl.index ]);
|
||
}
|
||
|
||
/**
|
||
* Determines if the minimum length is met by the search text.
|
||
* @returns {*} true if the minimum length is met by the search text
|
||
*/
|
||
function isMinLengthMet () {
|
||
return ($scope.searchText || '').length >= getMinLength();
|
||
}
|
||
|
||
// actions
|
||
|
||
/**
|
||
* Defines a public property with a handler and a default value.
|
||
* @param {string} key
|
||
* @param {Function} handler function
|
||
* @param {*} defaultValue default value
|
||
*/
|
||
function defineProperty (key, handler, defaultValue) {
|
||
Object.defineProperty(ctrl, key, {
|
||
get: function () { return defaultValue; },
|
||
set: function (newValue) {
|
||
var oldValue = defaultValue;
|
||
defaultValue = newValue;
|
||
handler(newValue, oldValue);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Selects the item at the given index.
|
||
* @param {number} index to select
|
||
*/
|
||
function select (index) {
|
||
// force form to update state for validation
|
||
$mdUtil.nextTick(function () {
|
||
getDisplayValue(ctrl.matches[ index ]).then(function (val) {
|
||
var ngModel = elements.$.input.controller('ngModel');
|
||
$mdLiveAnnouncer.announce(val + ' ' + ctrl.selectedMessage, 'assertive');
|
||
ngModel.$setViewValue(val);
|
||
ngModel.$render();
|
||
}).finally(function () {
|
||
$scope.selectedItem = ctrl.matches[ index ];
|
||
setLoading(false);
|
||
});
|
||
}, false);
|
||
}
|
||
|
||
/**
|
||
* Clears the searchText value and selected item.
|
||
* @param {Event} $event
|
||
*/
|
||
function clearValue ($event) {
|
||
if ($event) {
|
||
$event.stopPropagation();
|
||
}
|
||
clearSelectedItem();
|
||
clearSearchText();
|
||
}
|
||
|
||
/**
|
||
* Clears the selected item
|
||
*/
|
||
function clearSelectedItem () {
|
||
// Reset our variables
|
||
ctrl.index = -1;
|
||
$mdUtil.nextTick(updateActiveOption);
|
||
ctrl.matches = [];
|
||
}
|
||
|
||
/**
|
||
* Clears the searchText value
|
||
*/
|
||
function clearSearchText () {
|
||
// Set the loading to true so we don't see flashes of content.
|
||
// The flashing will only occur when an async request is running.
|
||
// So the loading process will stop when the results had been retrieved.
|
||
setLoading(true);
|
||
|
||
$scope.searchText = '';
|
||
|
||
// Normally, triggering the change / input event is unnecessary, because the browser detects it properly.
|
||
// But some browsers are not detecting it properly, which means that we have to trigger the event.
|
||
// Using the `input` is not working properly, because for example IE11 is not supporting the `input` event.
|
||
// The `change` event is a good alternative and is supported by all supported browsers.
|
||
var eventObj = document.createEvent('CustomEvent');
|
||
eventObj.initCustomEvent('change', true, true, { value: '' });
|
||
elements.input.dispatchEvent(eventObj);
|
||
|
||
// For some reason, firing the above event resets the value of $scope.searchText if
|
||
// $scope.searchText has a space character at the end, so we blank it one more time and then
|
||
// focus.
|
||
elements.input.blur();
|
||
$scope.searchText = '';
|
||
elements.input.focus();
|
||
}
|
||
|
||
/**
|
||
* Fetches the results for the provided search text.
|
||
* @param searchText
|
||
*/
|
||
function fetchResults (searchText) {
|
||
var items = $scope.$parent.$eval(itemExpr),
|
||
term = searchText.toLowerCase(),
|
||
isList = angular.isArray(items),
|
||
isPromise = !!items.then; // Every promise should contain a `then` property
|
||
|
||
if (isList) onResultsRetrieved(items);
|
||
else if (isPromise) handleAsyncResults(items);
|
||
|
||
function handleAsyncResults(items) {
|
||
if (!items) return;
|
||
|
||
items = $q.when(items);
|
||
fetchesInProgress++;
|
||
setLoading(true);
|
||
|
||
$mdUtil.nextTick(function () {
|
||
items
|
||
.then(onResultsRetrieved)
|
||
.finally(function(){
|
||
if (--fetchesInProgress === 0) {
|
||
setLoading(false);
|
||
}
|
||
});
|
||
},true, $scope);
|
||
}
|
||
|
||
function onResultsRetrieved(matches) {
|
||
cache[term] = matches;
|
||
|
||
// Just cache the results if the request is now outdated.
|
||
// The request becomes outdated, when the new searchText has changed during the result fetching.
|
||
if ((searchText || '') !== ($scope.searchText || '')) {
|
||
return;
|
||
}
|
||
|
||
handleResults(matches);
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* Reports given message types to supported screen readers.
|
||
* @param {boolean} isPolite Whether the announcement should be polite.
|
||
* @param {!number} types Message flags to be reported to the screen reader.
|
||
*/
|
||
function reportMessages(isPolite, types) {
|
||
var politeness = isPolite ? 'polite' : 'assertive';
|
||
var messages = [];
|
||
|
||
if (types & ReportType.Selected && ctrl.index !== -1) {
|
||
messages.push(getCurrentDisplayValue());
|
||
}
|
||
|
||
if (types & ReportType.Count) {
|
||
messages.push($q.resolve(getCountMessage()));
|
||
}
|
||
|
||
$q.all(messages).then(function(data) {
|
||
$mdLiveAnnouncer.announce(data.join(' '), politeness);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* @returns {string} the ARIA message for how many results match the current query.
|
||
*/
|
||
function getCountMessage () {
|
||
switch (ctrl.matches.length) {
|
||
case 0:
|
||
return ctrl.noMatchMessage;
|
||
case 1:
|
||
return ctrl.singleMatchMessage;
|
||
default:
|
||
return ctrl.multipleMatchStartMessage + ctrl.matches.length + ctrl.multipleMatchEndMessage;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Makes sure that the focused element is within view.
|
||
*/
|
||
function updateScroll () {
|
||
if (!elements.li[0]) return;
|
||
if (mode === MODE_STANDARD) {
|
||
updateStandardScroll();
|
||
} else {
|
||
updateVirtualScroll();
|
||
}
|
||
}
|
||
|
||
function updateVirtualScroll() {
|
||
// elements in virtual scroll have consistent heights
|
||
var optionHeight = elements.li[0].offsetHeight,
|
||
top = optionHeight * Math.max(0, ctrl.index),
|
||
bottom = top + optionHeight,
|
||
containerHeight = elements.scroller.clientHeight,
|
||
scrollTop = elements.scroller.scrollTop;
|
||
|
||
if (top < scrollTop) {
|
||
scrollTo(top);
|
||
} else if (bottom > scrollTop + containerHeight) {
|
||
scrollTo(bottom - containerHeight);
|
||
}
|
||
}
|
||
|
||
function updateStandardScroll() {
|
||
// elements in standard scroll have variable heights
|
||
var selected = elements.li[Math.max(0, ctrl.index)];
|
||
var containerHeight = elements.scrollContainer.offsetHeight,
|
||
top = selected && selected.offsetTop || 0,
|
||
bottom = top + selected.clientHeight,
|
||
scrollTop = elements.scrollContainer.scrollTop;
|
||
|
||
if (top < scrollTop) {
|
||
scrollTo(top);
|
||
} else if (bottom > scrollTop + containerHeight) {
|
||
scrollTo(bottom - containerHeight);
|
||
}
|
||
}
|
||
|
||
function isPromiseFetching() {
|
||
return fetchesInProgress !== 0;
|
||
}
|
||
|
||
function scrollTo (offset) {
|
||
if (mode === MODE_STANDARD) {
|
||
elements.scrollContainer.scrollTop = offset;
|
||
} else {
|
||
elements.$.scrollContainer.controller('mdVirtualRepeatContainer').scrollTo(offset);
|
||
}
|
||
}
|
||
|
||
function notFoundVisible () {
|
||
var textLength = (ctrl.scope.searchText || '').length;
|
||
|
||
return ctrl.hasNotFound && !hasMatches() && (!ctrl.loading || isPromiseFetching()) && textLength >= getMinLength() && (hasFocus || noBlur) && !hasSelection();
|
||
}
|
||
|
||
/**
|
||
* Starts the query to gather the results for the current searchText. Attempts to return cached
|
||
* results first, then forwards the process to `fetchResults` if necessary.
|
||
*/
|
||
function handleQuery () {
|
||
var searchText = $scope.searchText || '';
|
||
var term = searchText.toLowerCase();
|
||
|
||
// If caching is enabled and the current searchText is stored in the cache
|
||
if (!$scope.noCache && cache[term]) {
|
||
// The results should be handled as same as a normal un-cached request does.
|
||
handleResults(cache[term]);
|
||
} else {
|
||
fetchResults(searchText);
|
||
}
|
||
|
||
ctrl.hidden = shouldHide();
|
||
}
|
||
|
||
/**
|
||
* Handles the retrieved results by showing them in the autocompletes dropdown.
|
||
* @param results Retrieved results
|
||
*/
|
||
function handleResults(results) {
|
||
ctrl.matches = results;
|
||
ctrl.hidden = shouldHide();
|
||
|
||
// If loading is in progress, then we'll end the progress. This is needed for example,
|
||
// when the `clear` button was clicked, because there we always show the loading process, to prevent flashing.
|
||
if (ctrl.loading) setLoading(false);
|
||
|
||
if ($scope.selectOnMatch) selectItemOnMatch();
|
||
|
||
positionDropdown();
|
||
reportMessages(true, ReportType.Count);
|
||
}
|
||
|
||
/**
|
||
* If there is only one matching item and the search text matches its display value exactly,
|
||
* automatically select that item. Note: This function is only called if the user uses the
|
||
* `md-select-on-match` flag.
|
||
*/
|
||
function selectItemOnMatch () {
|
||
var searchText = $scope.searchText,
|
||
matches = ctrl.matches,
|
||
item = matches[ 0 ];
|
||
if (matches.length === 1) getDisplayValue(item).then(function (displayValue) {
|
||
var isMatching = searchText === displayValue;
|
||
if ($scope.matchInsensitive && !isMatching) {
|
||
isMatching = searchText.toLowerCase() === displayValue.toLowerCase();
|
||
}
|
||
|
||
if (isMatching) {
|
||
select(0);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Evaluates an attribute expression against the parent scope.
|
||
* @param {String} attr Name of the attribute to be evaluated.
|
||
* @param {Object?} locals Properties to be injected into the evaluation context.
|
||
*/
|
||
function evalAttr(attr, locals) {
|
||
if ($attrs[attr]) {
|
||
$scope.$parent.$eval($attrs[attr], locals || {});
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
|
||
MdAutocomplete.$inject = ["$$mdSvgRegistry"];angular
|
||
.module('material.components.autocomplete')
|
||
.directive('mdAutocomplete', MdAutocomplete);
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdAutocomplete
|
||
* @module material.components.autocomplete
|
||
*
|
||
* @description
|
||
* `<md-autocomplete>` is a special input component with a drop-down of all possible matches to a
|
||
* custom query. This component allows you to provide real-time suggestions as the user types
|
||
* in the input area.
|
||
*
|
||
* To start, you will need to specify the required parameters and provide a template for your
|
||
* results. The content inside `md-autocomplete` will be treated as a template.
|
||
*
|
||
* In more complex cases, you may want to include other content such as a message to display when
|
||
* no matches were found. You can do this by wrapping your template in `md-item-template` and
|
||
* adding a tag for `md-not-found`. An example of this is shown below.
|
||
*
|
||
* To reset the displayed value you must clear both values for `md-search-text` and
|
||
* `md-selected-item`.
|
||
*
|
||
* ### Validation
|
||
*
|
||
* You can use `ng-messages` to include validation the same way that you would normally validate;
|
||
* however, if you want to replicate a standard input with a floating label, you will have to
|
||
* do the following:
|
||
*
|
||
* - Make sure that your template is wrapped in `md-item-template`
|
||
* - Add your `ng-messages` code inside of `md-autocomplete`
|
||
* - Add your validation properties to `md-autocomplete` (ie. `required`)
|
||
* - Add a `name` to `md-autocomplete` (to be used on the generated `input`)
|
||
*
|
||
* There is an example below of how this should look.
|
||
*
|
||
* ### Snapping Drop-Down
|
||
*
|
||
* You can cause the autocomplete drop-down to snap to an ancestor element by applying the
|
||
* `md-autocomplete-snap` attribute to that element. You can also snap to the width of
|
||
* the `md-autocomplete-snap` element by setting the attribute's value to `width`
|
||
* (ie. `md-autocomplete-snap="width"`).
|
||
*
|
||
* ### Notes
|
||
*
|
||
* **Autocomplete Dropdown Items Rendering**
|
||
*
|
||
* The `md-autocomplete` uses the the <a ng-href="api/directive/mdVirtualRepeat">
|
||
* mdVirtualRepeat</a> directive for displaying the results inside of the dropdown.<br/>
|
||
*
|
||
* > When encountering issues regarding the item template please take a look at the
|
||
* <a ng-href="api/directive/mdVirtualRepeatContainer">VirtualRepeatContainer</a> documentation.
|
||
*
|
||
* **Autocomplete inside of a Virtual Repeat**
|
||
*
|
||
* When using the `md-autocomplete` directive inside of a
|
||
* <a ng-href="api/directive/mdVirtualRepeatContainer">VirtualRepeatContainer</a> the dropdown items
|
||
* might not update properly, because caching of the results is enabled by default.
|
||
*
|
||
* The autocomplete will then show invalid dropdown items, because the Virtual Repeat only updates
|
||
* the scope bindings rather than re-creating the `md-autocomplete`. This means that the previous
|
||
* cached results will be used.
|
||
*
|
||
* > To avoid such problems, ensure that the autocomplete does not cache any results via
|
||
* `md-no-cache="true"`:
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-autocomplete
|
||
* md-no-cache="true"
|
||
* md-selected-item="selectedItem"
|
||
* md-items="item in items"
|
||
* md-search-text="searchText"
|
||
* md-item-text="item.display">
|
||
* <span>{{ item.display }}</span>
|
||
* </md-autocomplete>
|
||
* </hljs>
|
||
*
|
||
*
|
||
* @param {expression} md-items An expression in the format of `item in results` to iterate over
|
||
* matches for your search.<br/><br/>
|
||
* The `results` expression can be also a function, which returns the results synchronously
|
||
* or asynchronously (per Promise).
|
||
* @param {expression=} md-selected-item-change An expression to be run each time a new item is
|
||
* selected.
|
||
* @param {expression=} md-search-text-change An expression to be run each time the search text
|
||
* updates.
|
||
* @param {expression=} md-search-text A model to bind the search query text to.
|
||
* @param {object=} md-selected-item A model to bind the selected item to.
|
||
* @param {expression=} md-item-text An expression that will convert your object to a single string.
|
||
* @param {string=} placeholder Placeholder text that will be forwarded to the input.
|
||
* @param {boolean=} md-no-cache Disables the internal caching that happens in autocomplete.
|
||
* @param {boolean=} ng-disabled Determines whether or not to disable the input field.
|
||
* @param {boolean=} md-require-match When set to true, the autocomplete will add a validator,
|
||
* which will evaluate to false, when no item is currently selected.
|
||
* @param {number=} md-min-length Specifies the minimum length of text before autocomplete will
|
||
* make suggestions.
|
||
* @param {number=} md-delay Specifies the amount of time (in milliseconds) to wait before looking
|
||
* for results.
|
||
* @param {boolean=} md-clear-button Whether the clear button for the autocomplete input should show
|
||
* up or not. When `md-floating-label` is set, defaults to false, defaults to true otherwise.
|
||
* @param {boolean=} md-autofocus If true, the autocomplete will be automatically focused when a
|
||
* `$mdDialog`, `$mdBottomsheet` or `$mdSidenav`, which contains the autocomplete, is opening.
|
||
* <br/><br/>
|
||
* Also the autocomplete will immediately focus the input element.
|
||
* @param {boolean=} md-no-asterisk When present, asterisk will not be appended to the floating
|
||
* label.
|
||
* @param {boolean=} md-autoselect If set to true, the first item will be automatically selected
|
||
* in the dropdown upon open.
|
||
* @param {string=} md-input-name The name attribute given to the input element to be used with
|
||
* FormController.
|
||
* @param {string=} md-menu-class This class will be applied to the dropdown menu for styling.
|
||
* @param {string=} md-menu-container-class This class will be applied to the parent container
|
||
* of the dropdown panel.
|
||
* @param {string=} md-input-class This will be applied to the input for styling. This attribute
|
||
* is only valid when a `md-floating-label` is defined.
|
||
* @param {string=} md-floating-label This will add a floating label to autocomplete and wrap it in
|
||
* `md-input-container`.
|
||
* @param {string=} md-select-on-focus When present the input's text will be automatically selected
|
||
* on focus.
|
||
* @param {string=} md-input-id An ID to be added to the input element.
|
||
* @param {number=} md-input-minlength The minimum length for the input's value for validation.
|
||
* @param {number=} md-input-maxlength The maximum length for the input's value for validation.
|
||
* @param {boolean=} md-select-on-match When set, autocomplete will automatically select
|
||
* the item if the search text is an exact match. <br/><br/>
|
||
* An exact match is when only one match is displayed.
|
||
* @param {boolean=} md-match-case-insensitive When set and using `md-select-on-match`, autocomplete
|
||
* will select on case-insensitive match.
|
||
* @param {string=} md-escape-options Override escape key logic. Default is `clear`.<br/>
|
||
* Options: `blur`, `clear`, `none`.
|
||
* @param {string=} md-dropdown-items Specifies the maximum amount of items to be shown in
|
||
* the dropdown.<br/><br/>
|
||
* When the dropdown doesn't fit into the viewport, the dropdown will shrink
|
||
* as much as possible.
|
||
* @param {string=} md-dropdown-position Overrides the default dropdown position. Options: `top`,
|
||
* `bottom`.
|
||
* @param {string=} input-aria-describedby A space-separated list of element IDs. This should
|
||
* contain the IDs of any elements that describe this autocomplete. Screen readers will read the
|
||
* content of these elements at the end of announcing that the autocomplete has been selected
|
||
* and describing its current state. The descriptive elements do not need to be visible on the
|
||
* page.
|
||
* @param {string=} input-aria-labelledby A space-separated list of element IDs. The ideal use case
|
||
* is that this would contain the ID of a `<label>` element that is associated with this
|
||
* autocomplete. This will only have affect when `md-floating-label` is not defined.<br><br>
|
||
* For `<label id="state">US State</label>`, you would set this to
|
||
* `input-aria-labelledby="state"`.
|
||
* @param {string=} input-aria-label A label that will be applied to the autocomplete's input.
|
||
* This will be announced by screen readers before the placeholder.
|
||
* This will only have affect when `md-floating-label` is not defined. If you define both
|
||
* `input-aria-label` and `input-aria-labelledby`, then `input-aria-label` will take precedence.
|
||
* @param {string=} md-selected-message Attribute to specify the text that the screen reader will
|
||
* announce after a value is selected. Default is: "selected". If `Alaska` is selected in the
|
||
* options panel, it will read "Alaska selected". You will want to override this when your app
|
||
* runs in a non-English locale.
|
||
* @param {string=} md-no-match-message Attribute to specify the text that the screen reader will
|
||
* announce after a query returns no matching results.
|
||
* Default is: "There are no matches available.". You will want to override this when your app
|
||
* runs in a non-English locale.
|
||
* @param {string=} md-single-match-message Attribute to specify the text that the screen reader
|
||
* will announce after a query returns a single matching result.
|
||
* Default is: "There is 1 match available.". You will want to override this when your app
|
||
* runs in a non-English locale.
|
||
* @param {string=} md-multiple-match-start-message Attribute to specify the text that the screen
|
||
* reader will announce after a query returns multiple matching results. The number of matching
|
||
* results will be read after this text. Default is: "There are ". You will want to override this
|
||
* when your app runs in a non-English locale.
|
||
* @param {string=} md-multiple-match-end-message Attribute to specify the text that the screen
|
||
* reader will announce after a query returns multiple matching results. The number of matching
|
||
* results will be read before this text. Default is: " matches available.". You will want to
|
||
* override this when your app runs in a non-English locale.
|
||
* @param {boolean=} ng-trim If set to false, the search text will be not trimmed automatically.
|
||
* Defaults to true.
|
||
* @param {string=} ng-pattern Adds the pattern validator to the ngModel of the search text.
|
||
* See the [ngPattern Directive](https://docs.angularjs.org/api/ng/directive/ngPattern)
|
||
* for more details.
|
||
* @param {string=} md-mode Specify the repeat mode for suggestion lists. Acceptable values include
|
||
* `virtual` (md-virtual-repeat) and `standard` (ng-repeat). See the
|
||
* `Specifying Repeat Mode` example for mode details. Default is `virtual`.
|
||
*
|
||
* @usage
|
||
* ### Basic Example
|
||
* <hljs lang="html">
|
||
* <md-autocomplete
|
||
* md-selected-item="selectedItem"
|
||
* md-search-text="searchText"
|
||
* md-items="item in getMatches(searchText)"
|
||
* md-item-text="item.display">
|
||
* <span md-highlight-text="searchText">{{item.display}}</span>
|
||
* </md-autocomplete>
|
||
* </hljs>
|
||
*
|
||
* ### Example with "not found" message
|
||
* <hljs lang="html">
|
||
* <md-autocomplete
|
||
* md-selected-item="selectedItem"
|
||
* md-search-text="searchText"
|
||
* md-items="item in getMatches(searchText)"
|
||
* md-item-text="item.display">
|
||
* <md-item-template>
|
||
* <span md-highlight-text="searchText">{{item.display}}</span>
|
||
* </md-item-template>
|
||
* <md-not-found>
|
||
* No matches found.
|
||
* </md-not-found>
|
||
* </md-autocomplete>
|
||
* </hljs>
|
||
*
|
||
* In this example, our code utilizes `md-item-template` and `md-not-found` to specify the
|
||
* different parts that make up our component.
|
||
*
|
||
* ### Clear button for the input
|
||
* By default, the clear button is displayed when there is input. This aligns with the spec's
|
||
* [Search Pattern](https://material.io/archive/guidelines/patterns/search.html#search-in-app-search).
|
||
* In floating label mode, when `md-floating-label="My Label"` is applied, the clear button is not
|
||
* displayed by default (see the spec's
|
||
* [Autocomplete Text Field](https://material.io/archive/guidelines/components/text-fields.html#text-fields-layout)).
|
||
*
|
||
* Nevertheless, developers are able to explicitly toggle the clear button for all autocomplete
|
||
* components with `md-clear-button`.
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-autocomplete ... md-clear-button="true"></md-autocomplete>
|
||
* <md-autocomplete ... md-clear-button="false"></md-autocomplete>
|
||
* </hljs>
|
||
*
|
||
* In previous versions, the clear button was always hidden when the component was disabled.
|
||
* This changed in `1.1.5` to give the developer control of this behavior. This example
|
||
* will hide the clear button only when the component is disabled.
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-autocomplete ... ng-disabled="disabled" md-clear-button="!disabled"></md-autocomplete>
|
||
* </hljs>
|
||
*
|
||
* ### Example with validation
|
||
* <hljs lang="html">
|
||
* <form name="autocompleteForm">
|
||
* <md-autocomplete
|
||
* required
|
||
* md-input-name="autocomplete"
|
||
* md-selected-item="selectedItem"
|
||
* md-search-text="searchText"
|
||
* md-items="item in getMatches(searchText)"
|
||
* md-item-text="item.display">
|
||
* <md-item-template>
|
||
* <span md-highlight-text="searchText">{{item.display}}</span>
|
||
* </md-item-template>
|
||
* <div ng-messages="autocompleteForm.autocomplete.$error">
|
||
* <div ng-message="required">This field is required</div>
|
||
* </div>
|
||
* </md-autocomplete>
|
||
* </form>
|
||
* </hljs>
|
||
*
|
||
* In this example, our code utilizes `md-item-template` and `ng-messages` to specify
|
||
* input validation for the field.
|
||
*
|
||
* ### Asynchronous Results
|
||
* The autocomplete items expression also supports promises, which will resolve with the query
|
||
* results.
|
||
*
|
||
* <hljs lang="js">
|
||
* function AppController($scope, $http) {
|
||
* $scope.query = function(searchText) {
|
||
* return $http
|
||
* .get(BACKEND_URL + '/items/' + searchText)
|
||
* .then(function(data) {
|
||
* // Map the response object to the data object.
|
||
* return data;
|
||
* });
|
||
* };
|
||
* }
|
||
* </hljs>
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-autocomplete
|
||
* md-selected-item="selectedItem"
|
||
* md-search-text="searchText"
|
||
* md-items="item in query(searchText)">
|
||
* <md-item-template>
|
||
* <span md-highlight-text="searchText">{{item}}</span>
|
||
* </md-item-template>
|
||
* </md-autocomplete>
|
||
* </hljs>
|
||
*
|
||
* ### Specifying Repeat Mode
|
||
* You can use `md-mode` to specify whether to use standard or virtual lists for
|
||
* rendering autocomplete options.
|
||
* The `md-mode` accepts two values:
|
||
* - `virtual` (default) Uses `md-virtual-repeat` to render list items. Virtual
|
||
* mode requires you to have consistent heights for all suggestions.
|
||
* - `standard` uses `ng-repeat` to render list items. This allows you to have
|
||
* options of varying heights.
|
||
*
|
||
* Note that using 'standard' mode will require you to address any list
|
||
* performance issues (e.g. pagination) separately within your application.
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-autocomplete
|
||
* md-selected-item="selectedItem"
|
||
* md-search-text="searchText"
|
||
* md-items="item in getMatches(searchText)"
|
||
* md-item-text="item.display"
|
||
* md-mode="standard">
|
||
* <span md-highlight-text="searchText">{{item.display}}</span>
|
||
* </md-autocomplete>
|
||
* </hljs>
|
||
*/
|
||
function MdAutocomplete ($$mdSvgRegistry) {
|
||
var REPEAT_STANDARD = 'standard';
|
||
var REPEAT_VIRTUAL = 'virtual';
|
||
var REPEAT_MODES = [REPEAT_STANDARD, REPEAT_VIRTUAL];
|
||
|
||
/** get a valid repeat mode from an md-mode attribute string. */
|
||
function getRepeatMode(modeStr) {
|
||
if (!modeStr) { return REPEAT_VIRTUAL; }
|
||
modeStr = modeStr.toLowerCase();
|
||
return REPEAT_MODES.indexOf(modeStr) > -1 ? modeStr : REPEAT_VIRTUAL;
|
||
}
|
||
|
||
return {
|
||
controller: 'MdAutocompleteCtrl',
|
||
controllerAs: '$mdAutocompleteCtrl',
|
||
scope: {
|
||
inputName: '@mdInputName',
|
||
inputMinlength: '@mdInputMinlength',
|
||
inputMaxlength: '@mdInputMaxlength',
|
||
searchText: '=?mdSearchText',
|
||
selectedItem: '=?mdSelectedItem',
|
||
itemsExpr: '@mdItems',
|
||
itemText: '&mdItemText',
|
||
placeholder: '@placeholder',
|
||
inputAriaDescribedBy: '@?inputAriaDescribedby',
|
||
inputAriaLabelledBy: '@?inputAriaLabelledby',
|
||
inputAriaLabel: '@?inputAriaLabel',
|
||
noCache: '=?mdNoCache',
|
||
requireMatch: '=?mdRequireMatch',
|
||
selectOnMatch: '=?mdSelectOnMatch',
|
||
matchInsensitive: '=?mdMatchCaseInsensitive',
|
||
itemChange: '&?mdSelectedItemChange',
|
||
textChange: '&?mdSearchTextChange',
|
||
minLength: '=?mdMinLength',
|
||
delay: '=?mdDelay',
|
||
autofocus: '=?mdAutofocus',
|
||
floatingLabel: '@?mdFloatingLabel',
|
||
autoselect: '=?mdAutoselect',
|
||
menuClass: '@?mdMenuClass',
|
||
menuContainerClass: '@?mdMenuContainerClass',
|
||
inputClass: '@?mdInputClass',
|
||
inputId: '@?mdInputId',
|
||
escapeOptions: '@?mdEscapeOptions',
|
||
dropdownItems: '=?mdDropdownItems',
|
||
dropdownPosition: '@?mdDropdownPosition',
|
||
clearButton: '=?mdClearButton',
|
||
selectedMessage: '@?mdSelectedMessage',
|
||
noMatchMessage: '@?mdNoMatchMessage',
|
||
singleMatchMessage: '@?mdSingleMatchMessage',
|
||
multipleMatchStartMessage: '@?mdMultipleMatchStartMessage',
|
||
multipleMatchEndMessage: '@?mdMultipleMatchEndMessage',
|
||
mdMode: '=?mdMode'
|
||
},
|
||
compile: function(tElement, tAttrs) {
|
||
var attributes = ['md-select-on-focus', 'md-no-asterisk', 'ng-trim', 'ng-pattern'];
|
||
var input = tElement.find('input');
|
||
|
||
attributes.forEach(function(attribute) {
|
||
var attrValue = tAttrs[tAttrs.$normalize(attribute)];
|
||
|
||
if (attrValue !== null) {
|
||
input.attr(attribute, attrValue);
|
||
}
|
||
});
|
||
|
||
return function(scope, element, attrs, ctrl) {
|
||
// Retrieve the state of using a md-not-found template by using our attribute, which will
|
||
// be added to the element in the template function.
|
||
ctrl.hasNotFound = !!element.attr('md-has-not-found');
|
||
|
||
// By default the inset autocomplete should show the clear button when not explicitly
|
||
// overwritten or in floating label mode.
|
||
if (!angular.isDefined(attrs.mdClearButton) && !scope.floatingLabel) {
|
||
scope.clearButton = true;
|
||
}
|
||
|
||
scope.mdMode = getRepeatMode(attrs.mdMode);
|
||
|
||
// Stop click events from bubbling up to the document and triggering a flicker of the
|
||
// options panel while still supporting ng-click to be placed on md-autocomplete.
|
||
element.on('click touchstart touchend', function(event) {
|
||
event.stopPropagation();
|
||
});
|
||
};
|
||
},
|
||
template: function (element, attr) {
|
||
var noItemsTemplate = getNoItemsTemplate(),
|
||
itemTemplate = getItemTemplate(),
|
||
leftover = element.html(),
|
||
tabindex = attr.tabindex;
|
||
|
||
// Set our attribute for the link function above which runs later.
|
||
// We will set an attribute, because otherwise the stored variables will be trashed when
|
||
// removing the element is hidden while retrieving the template. For example when using ngIf.
|
||
if (noItemsTemplate) element.attr('md-has-not-found', true);
|
||
|
||
// Always set our tabindex of the autocomplete directive to -1, because our input
|
||
// will hold the actual tabindex.
|
||
element.attr('tabindex', '-1');
|
||
|
||
return '\
|
||
<md-autocomplete-wrap\
|
||
ng-class="{ \'md-whiteframe-z1\': !floatingLabel, \
|
||
\'md-menu-showing\': !$mdAutocompleteCtrl.hidden, \
|
||
\'md-show-clear-button\': !!clearButton }">\
|
||
' + getInputElement() + '\
|
||
' + getClearButton() + '\
|
||
<md-progress-linear\
|
||
class="' + (attr.mdFloatingLabel ? 'md-inline' : '') + '"\
|
||
ng-if="$mdAutocompleteCtrl.loadingIsVisible()"\
|
||
md-mode="indeterminate"></md-progress-linear>\
|
||
' + getContainer(attr.mdMenuContainerClass, attr.mdMode) + '\
|
||
<ul class="md-autocomplete-suggestions"\
|
||
ng-class="::menuClass"\
|
||
id="ul-{{$mdAutocompleteCtrl.id}}"\
|
||
ng-mouseup="$mdAutocompleteCtrl.focusInput()"\
|
||
role="listbox">\
|
||
<li class="md-autocomplete-suggestion" ' + getRepeatType(attr.mdMode) + ' ="item in $mdAutocompleteCtrl.matches"\
|
||
ng-class="{ selected: $index === $mdAutocompleteCtrl.index }"\
|
||
ng-attr-id="{{\'md-option-\' + $mdAutocompleteCtrl.id + \'-\' + $index}}"\
|
||
ng-click="$mdAutocompleteCtrl.select($index)"\
|
||
role="option"\
|
||
aria-setsize="{{$mdAutocompleteCtrl.matches.length}}"\
|
||
aria-posinset="{{$index+1}}"\
|
||
aria-selected="{{$index === $mdAutocompleteCtrl.index ? true : false}}" \
|
||
md-extra-name="$mdAutocompleteCtrl.itemName">\
|
||
' + itemTemplate + '\
|
||
</li>' + noItemsTemplate + '\
|
||
</ul>\
|
||
' + getContainerClosingTags(attr.mdMode) + '\
|
||
</md-autocomplete-wrap>';
|
||
|
||
function getItemTemplate() {
|
||
var templateTag = element.find('md-item-template').detach(),
|
||
html = templateTag.length ? templateTag.html() : element.html();
|
||
if (!templateTag.length) element.empty();
|
||
return '<md-autocomplete-parent-scope md-autocomplete-replace>' + html +
|
||
'</md-autocomplete-parent-scope>';
|
||
}
|
||
|
||
function getNoItemsTemplate() {
|
||
var templateTag = element.find('md-not-found').detach(),
|
||
template = templateTag.length ? templateTag.html() : '';
|
||
return template
|
||
? '<li ng-if="$mdAutocompleteCtrl.notFoundVisible()" class="md-autocomplete-suggestion"\
|
||
md-autocomplete-parent-scope>' + template + '</li>'
|
||
: '';
|
||
}
|
||
|
||
function getContainer(menuContainerClass, repeatMode) {
|
||
// prepend a space if needed
|
||
menuContainerClass = menuContainerClass ? ' ' + menuContainerClass : '';
|
||
|
||
if (isVirtualRepeatDisabled(repeatMode)) {
|
||
return '\
|
||
<div \
|
||
ng-hide="$mdAutocompleteCtrl.hidden"\
|
||
class="md-standard-list-container md-autocomplete-suggestions-container md-whiteframe-z1' + menuContainerClass + '"\
|
||
ng-class="{ \'md-not-found\': $mdAutocompleteCtrl.notFoundVisible() }"\
|
||
ng-mouseenter="$mdAutocompleteCtrl.listEnter()"\
|
||
ng-mouseleave="$mdAutocompleteCtrl.listLeave()"\
|
||
role="presentation">\
|
||
<div class="md-standard-list-scroller" role="presentation">';
|
||
}
|
||
|
||
return '\
|
||
<md-virtual-repeat-container\
|
||
md-auto-shrink\
|
||
md-auto-shrink-min="1"\
|
||
ng-hide="$mdAutocompleteCtrl.hidden"\
|
||
class="md-virtual-repeat-container md-autocomplete-suggestions-container md-whiteframe-z1' + menuContainerClass + '"\
|
||
ng-class="{ \'md-not-found\': $mdAutocompleteCtrl.notFoundVisible() }"\
|
||
ng-mouseenter="$mdAutocompleteCtrl.listEnter()"\
|
||
ng-mouseleave="$mdAutocompleteCtrl.listLeave()"\
|
||
role="presentation">';
|
||
}
|
||
|
||
function getContainerClosingTags(repeatMode) {
|
||
return isVirtualRepeatDisabled(repeatMode) ?
|
||
' </div>\
|
||
</div>\
|
||
</div>' : '</md-virtual-repeat-container>';
|
||
}
|
||
|
||
function getRepeatType(repeatMode) {
|
||
return isVirtualRepeatDisabled(repeatMode) ?
|
||
'ng-repeat' : 'md-virtual-repeat';
|
||
}
|
||
|
||
function isVirtualRepeatDisabled(repeatMode) {
|
||
// ensure we have a valid repeat mode
|
||
var correctedRepeatMode = getRepeatMode(repeatMode);
|
||
return correctedRepeatMode !== REPEAT_VIRTUAL;
|
||
}
|
||
|
||
function getInputElement () {
|
||
if (attr.mdFloatingLabel) {
|
||
return '\
|
||
<md-input-container ng-if="floatingLabel">\
|
||
<label>{{floatingLabel}}</label>\
|
||
<input type="text"\
|
||
' + (tabindex != null ? 'tabindex="' + tabindex + '"' : '') + '\
|
||
id="{{inputId || \'fl-input-\' + $mdAutocompleteCtrl.id}}"\
|
||
name="{{inputName || \'fl-input-\' + $mdAutocompleteCtrl.id }}"\
|
||
ng-class="::inputClass"\
|
||
autocomplete="off"\
|
||
ng-required="$mdAutocompleteCtrl.isRequired"\
|
||
ng-readonly="$mdAutocompleteCtrl.isReadonly"\
|
||
ng-minlength="inputMinlength"\
|
||
ng-maxlength="inputMaxlength"\
|
||
ng-disabled="$mdAutocompleteCtrl.isDisabled"\
|
||
ng-model="$mdAutocompleteCtrl.scope.searchText"\
|
||
ng-model-options="{ allowInvalid: true }"\
|
||
ng-mousedown="$mdAutocompleteCtrl.focusInput()"\
|
||
ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
|
||
ng-blur="$mdAutocompleteCtrl.blur($event)"\
|
||
ng-focus="$mdAutocompleteCtrl.focus($event)"\
|
||
aria-label="{{floatingLabel}}"\
|
||
ng-attr-aria-autocomplete="{{$mdAutocompleteCtrl.isDisabled ? undefined : \'list\'}}"\
|
||
ng-attr-role="{{$mdAutocompleteCtrl.isDisabled ? undefined : \'combobox\'}}"\
|
||
aria-haspopup="{{!$mdAutocompleteCtrl.isDisabled}}"\
|
||
aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"\
|
||
ng-attr-aria-owns="{{$mdAutocompleteCtrl.hidden || $mdAutocompleteCtrl.isDisabled ? undefined : \'ul-\' + $mdAutocompleteCtrl.id}}"\
|
||
ng-attr-aria-activedescendant="{{!$mdAutocompleteCtrl.hidden && $mdAutocompleteCtrl.activeOption ? $mdAutocompleteCtrl.activeOption : undefined}}">\
|
||
<div md-autocomplete-parent-scope md-autocomplete-replace>' + leftover + '</div>\
|
||
</md-input-container>';
|
||
} else {
|
||
return '\
|
||
<input type="text"\
|
||
' + (tabindex != null ? 'tabindex="' + tabindex + '"' : '') + '\
|
||
id="{{inputId || \'input-\' + $mdAutocompleteCtrl.id}}"\
|
||
name="{{inputName || \'input-\' + $mdAutocompleteCtrl.id }}"\
|
||
ng-class="::inputClass"\
|
||
ng-if="!floatingLabel"\
|
||
autocomplete="off"\
|
||
ng-required="$mdAutocompleteCtrl.isRequired"\
|
||
ng-disabled="$mdAutocompleteCtrl.isDisabled"\
|
||
ng-readonly="$mdAutocompleteCtrl.isReadonly"\
|
||
ng-minlength="inputMinlength"\
|
||
ng-maxlength="inputMaxlength"\
|
||
ng-model="$mdAutocompleteCtrl.scope.searchText"\
|
||
ng-mousedown="$mdAutocompleteCtrl.focusInput()"\
|
||
ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
|
||
ng-blur="$mdAutocompleteCtrl.blur($event)"\
|
||
ng-focus="$mdAutocompleteCtrl.focus($event)"\
|
||
placeholder="{{placeholder}}"\
|
||
aria-label="{{placeholder}}"\
|
||
ng-attr-aria-autocomplete="{{$mdAutocompleteCtrl.isDisabled ? undefined : \'list\'}}"\
|
||
ng-attr-role="{{$mdAutocompleteCtrl.isDisabled ? undefined : \'combobox\'}}"\
|
||
aria-haspopup="{{!$mdAutocompleteCtrl.isDisabled}}"\
|
||
aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"\
|
||
ng-attr-aria-owns="{{$mdAutocompleteCtrl.hidden || $mdAutocompleteCtrl.isDisabled ? undefined : \'ul-\' + $mdAutocompleteCtrl.id}}"\
|
||
ng-attr-aria-activedescendant="{{!$mdAutocompleteCtrl.hidden && $mdAutocompleteCtrl.activeOption ? $mdAutocompleteCtrl.activeOption : undefined}}">';
|
||
}
|
||
}
|
||
|
||
function getClearButton() {
|
||
return '' +
|
||
'<button ' +
|
||
'type="button" ' +
|
||
'aria-label="Clear Input" ' +
|
||
'tabindex="0" ' +
|
||
'ng-if="clearButton && $mdAutocompleteCtrl.scope.searchText" ' +
|
||
'ng-click="$mdAutocompleteCtrl.clear($event)">' +
|
||
'<md-icon md-svg-src="' + $$mdSvgRegistry.mdClose + '"></md-icon>' +
|
||
'</button>';
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
|
||
MdAutocompleteItemScopeDirective.$inject = ["$compile", "$mdUtil"];angular
|
||
.module('material.components.autocomplete')
|
||
.directive('mdAutocompleteParentScope', MdAutocompleteItemScopeDirective);
|
||
|
||
function MdAutocompleteItemScopeDirective($compile, $mdUtil) {
|
||
return {
|
||
restrict: 'AE',
|
||
compile: compile,
|
||
terminal: true,
|
||
transclude: 'element'
|
||
};
|
||
|
||
function compile(tElement, tAttr, transclude) {
|
||
return function postLink(scope, element, attr) {
|
||
var ctrl = scope.$mdAutocompleteCtrl;
|
||
var newScope = ctrl.parent.$new();
|
||
var itemName = ctrl.itemName;
|
||
|
||
// Watch for changes to our scope's variables and copy them to the new scope
|
||
watchVariable('$index', '$index');
|
||
watchVariable('item', itemName);
|
||
|
||
// Ensure that $digest calls on our scope trigger $digest on newScope.
|
||
connectScopes();
|
||
|
||
// Link the element against newScope.
|
||
transclude(newScope, function(clone) {
|
||
element.after(clone);
|
||
});
|
||
|
||
/**
|
||
* Creates a watcher for variables that are copied from the parent scope
|
||
* @param variable
|
||
* @param alias
|
||
*/
|
||
function watchVariable(variable, alias) {
|
||
newScope[alias] = scope[variable];
|
||
|
||
scope.$watch(variable, function(value) {
|
||
$mdUtil.nextTick(function() {
|
||
newScope[alias] = value;
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Creates watchers on scope and newScope that ensure that for any
|
||
* $digest of scope, newScope is also $digested.
|
||
*/
|
||
function connectScopes() {
|
||
var scopeDigesting = false;
|
||
var newScopeDigesting = false;
|
||
|
||
scope.$watch(function() {
|
||
if (newScopeDigesting || scopeDigesting) {
|
||
return;
|
||
}
|
||
|
||
scopeDigesting = true;
|
||
scope.$$postDigest(function() {
|
||
if (!newScopeDigesting) {
|
||
newScope.$digest();
|
||
}
|
||
|
||
scopeDigesting = newScopeDigesting = false;
|
||
});
|
||
});
|
||
|
||
newScope.$watch(function() {
|
||
newScopeDigesting = true;
|
||
});
|
||
}
|
||
};
|
||
}
|
||
}
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
|
||
MdHighlightCtrl.$inject = ["$scope", "$element", "$attrs", "$mdUtil"];angular
|
||
.module('material.components.autocomplete')
|
||
.controller('MdHighlightCtrl', MdHighlightCtrl);
|
||
|
||
function MdHighlightCtrl ($scope, $element, $attrs, $mdUtil) {
|
||
this.$scope = $scope;
|
||
this.$element = $element;
|
||
this.$attrs = $attrs;
|
||
this.$mdUtil = $mdUtil;
|
||
|
||
// Cache the Regex to avoid rebuilding each time.
|
||
this.regex = null;
|
||
}
|
||
|
||
MdHighlightCtrl.prototype.init = function(unsafeTermFn, unsafeContentFn) {
|
||
|
||
this.flags = this.$attrs.mdHighlightFlags || '';
|
||
|
||
this.unregisterFn = this.$scope.$watch(function($scope) {
|
||
return {
|
||
term: unsafeTermFn($scope),
|
||
contentText: unsafeContentFn($scope)
|
||
};
|
||
}.bind(this), this.onRender.bind(this), true);
|
||
|
||
this.$element.on('$destroy', this.unregisterFn);
|
||
};
|
||
|
||
/**
|
||
* Triggered once a new change has been recognized and the highlighted
|
||
* text needs to be updated.
|
||
*/
|
||
MdHighlightCtrl.prototype.onRender = function(state, prevState) {
|
||
|
||
var contentText = state.contentText;
|
||
|
||
/* Update the regex if it's outdated, because we don't want to rebuilt it constantly. */
|
||
if (this.regex === null || state.term !== prevState.term) {
|
||
this.regex = this.createRegex(state.term, this.flags);
|
||
}
|
||
|
||
/* If a term is available apply the regex to the content */
|
||
if (state.term) {
|
||
this.applyRegex(contentText);
|
||
} else {
|
||
this.$element.text(contentText);
|
||
}
|
||
|
||
};
|
||
|
||
/**
|
||
* Decomposes the specified text into different tokens (whether match or not).
|
||
* Breaking down the string guarantees proper XSS protection due to the native browser
|
||
* escaping of unsafe text.
|
||
*/
|
||
MdHighlightCtrl.prototype.applyRegex = function(text) {
|
||
var tokens = this.resolveTokens(text);
|
||
|
||
this.$element.empty();
|
||
|
||
tokens.forEach(function (token) {
|
||
|
||
if (token.isMatch) {
|
||
var tokenEl = angular.element('<span class="highlight">').text(token.text);
|
||
|
||
this.$element.append(tokenEl);
|
||
} else {
|
||
this.$element.append(document.createTextNode(token));
|
||
}
|
||
|
||
}.bind(this));
|
||
|
||
};
|
||
|
||
/**
|
||
* Decomposes the specified text into different tokens by running the regex against the text.
|
||
*/
|
||
MdHighlightCtrl.prototype.resolveTokens = function(string) {
|
||
var tokens = [];
|
||
var lastIndex = 0;
|
||
|
||
// Use replace here, because it supports global and single regular expressions at same time.
|
||
string.replace(this.regex, function(match, index) {
|
||
appendToken(lastIndex, index);
|
||
|
||
tokens.push({
|
||
text: match,
|
||
isMatch: true
|
||
});
|
||
|
||
lastIndex = index + match.length;
|
||
});
|
||
|
||
// Append the missing text as a token.
|
||
appendToken(lastIndex);
|
||
|
||
return tokens;
|
||
|
||
function appendToken(from, to) {
|
||
var targetText = string.slice(from, to);
|
||
targetText && tokens.push(targetText);
|
||
}
|
||
};
|
||
|
||
/** Creates a regex for the specified text with the given flags. */
|
||
MdHighlightCtrl.prototype.createRegex = function(term, flags) {
|
||
var startFlag = '', endFlag = '';
|
||
var regexTerm = this.$mdUtil.sanitize(term);
|
||
|
||
if (flags.indexOf('^') >= 0) startFlag = '^';
|
||
if (flags.indexOf('$') >= 0) endFlag = '$';
|
||
|
||
return new RegExp(startFlag + regexTerm + endFlag, flags.replace(/[$^]/g, ''));
|
||
};
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
|
||
MdHighlight.$inject = ["$interpolate", "$parse"];angular
|
||
.module('material.components.autocomplete')
|
||
.directive('mdHighlightText', MdHighlight);
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdHighlightText
|
||
* @module material.components.autocomplete
|
||
*
|
||
* @description
|
||
* The `md-highlight-text` directive allows you to specify text that should be highlighted within
|
||
* an element. Highlighted text will be wrapped in `<span class="highlight"></span>` which can
|
||
* be styled through CSS. Please note that child elements may not be used with this directive.
|
||
*
|
||
* @param {string} md-highlight-text A model to be searched for
|
||
* @param {string=} md-highlight-flags A list of flags (loosely based on JavaScript RexExp flags).
|
||
* #### **Supported flags**:
|
||
* - `g`: Find all matches within the provided text
|
||
* - `i`: Ignore case when searching for matches
|
||
* - `$`: Only match if the text ends with the search term
|
||
* - `^`: Only match if the text begins with the search term
|
||
*
|
||
* @usage
|
||
* <hljs lang="html">
|
||
* <input placeholder="Enter a search term..." ng-model="searchTerm" type="text" />
|
||
* <ul>
|
||
* <li ng-repeat="result in results" md-highlight-text="searchTerm" md-highlight-flags="i">
|
||
* {{result.text}}
|
||
* </li>
|
||
* </ul>
|
||
* </hljs>
|
||
*/
|
||
|
||
function MdHighlight ($interpolate, $parse) {
|
||
return {
|
||
terminal: true,
|
||
controller: 'MdHighlightCtrl',
|
||
compile: function mdHighlightCompile(tElement, tAttr) {
|
||
var termExpr = $parse(tAttr.mdHighlightText);
|
||
var unsafeContentExpr = $interpolate(tElement.html());
|
||
|
||
return function mdHighlightLink(scope, element, attr, ctrl) {
|
||
ctrl.init(termExpr, unsafeContentExpr);
|
||
};
|
||
}
|
||
};
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/*
|
||
* @ngdoc module
|
||
* @name material.components.backdrop
|
||
* @description Backdrop
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdBackdrop
|
||
* @module material.components.backdrop
|
||
*
|
||
* @restrict E
|
||
*
|
||
* @description
|
||
* `<md-backdrop>` is a backdrop element used by other components, such as dialog and bottom sheet.
|
||
* Apply class `opaque` to make the backdrop use the theme backdrop color.
|
||
*
|
||
*/
|
||
|
||
angular
|
||
.module('material.components.backdrop', ['material.core'])
|
||
.directive('mdBackdrop', ["$mdTheming", "$mdUtil", "$animate", "$rootElement", "$window", "$log", "$$rAF", "$document", function BackdropDirective($mdTheming, $mdUtil, $animate, $rootElement, $window, $log, $$rAF, $document) {
|
||
var ERROR_CSS_POSITION = '<md-backdrop> may not work properly in a scrolled, static-positioned parent container.';
|
||
|
||
return {
|
||
restrict: 'E',
|
||
link: postLink
|
||
};
|
||
|
||
function postLink(scope, element, attrs) {
|
||
// backdrop may be outside the $rootElement, tell ngAnimate to animate regardless
|
||
if ($animate.pin) $animate.pin(element, $rootElement);
|
||
|
||
var bodyStyles;
|
||
|
||
$$rAF(function() {
|
||
// If body scrolling has been disabled using mdUtil.disableBodyScroll(),
|
||
// adjust the 'backdrop' height to account for the fixed 'body' top offset.
|
||
// Note that this can be pretty expensive and is better done inside the $$rAF.
|
||
bodyStyles = $window.getComputedStyle($document[0].body);
|
||
|
||
if (bodyStyles.position === 'fixed') {
|
||
var resizeHandler = $mdUtil.debounce(function(){
|
||
bodyStyles = $window.getComputedStyle($document[0].body);
|
||
resize();
|
||
}, 60, null, false);
|
||
|
||
resize();
|
||
angular.element($window).on('resize', resizeHandler);
|
||
|
||
scope.$on('$destroy', function() {
|
||
angular.element($window).off('resize', resizeHandler);
|
||
});
|
||
}
|
||
|
||
// Often $animate.enter() is used to append the backDrop element
|
||
// so let's wait until $animate is done...
|
||
var parent = element.parent();
|
||
|
||
if (parent.length) {
|
||
if (parent[0].nodeName === 'BODY') {
|
||
element.css('position', 'fixed');
|
||
}
|
||
|
||
var styles = $window.getComputedStyle(parent[0]);
|
||
|
||
if (styles.position === 'static') {
|
||
// backdrop uses position:absolute and will not work properly with parent position:static (default)
|
||
$log.warn(ERROR_CSS_POSITION);
|
||
}
|
||
|
||
// Only inherit the parent if the backdrop has a parent.
|
||
$mdTheming.inherit(element, parent);
|
||
}
|
||
});
|
||
|
||
function resize() {
|
||
var viewportHeight = parseInt(bodyStyles.height, 10) + Math.abs(parseInt(bodyStyles.top, 10));
|
||
element.css('height', viewportHeight + 'px');
|
||
}
|
||
}
|
||
|
||
}]);
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.bottomSheet
|
||
* @description
|
||
* BottomSheet
|
||
*/
|
||
MdBottomSheetDirective.$inject = ["$mdBottomSheet"];
|
||
MdBottomSheetProvider.$inject = ["$$interimElementProvider"];
|
||
angular
|
||
.module('material.components.bottomSheet', [
|
||
'material.core',
|
||
'material.components.backdrop'
|
||
])
|
||
.directive('mdBottomSheet', MdBottomSheetDirective)
|
||
.provider('$mdBottomSheet', MdBottomSheetProvider);
|
||
|
||
/* @ngInject */
|
||
function MdBottomSheetDirective($mdBottomSheet) {
|
||
return {
|
||
restrict: 'E',
|
||
link : function postLink(scope, element) {
|
||
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() {
|
||
$mdBottomSheet.destroy();
|
||
});
|
||
}
|
||
};
|
||
}
|
||
|
||
|
||
/**
|
||
* @ngdoc service
|
||
* @name $mdBottomSheet
|
||
* @module material.components.bottomSheet
|
||
*
|
||
* @description
|
||
* `$mdBottomSheet` opens a bottom sheet over the app and provides a simple promise API.
|
||
*
|
||
* ## Restrictions
|
||
*
|
||
* - The bottom sheet's template must have an outer `<md-bottom-sheet>` element.
|
||
* - Add the `md-grid` class to the bottom sheet for a grid layout.
|
||
* - Add the `md-list` class to the bottom sheet for a list layout.
|
||
*
|
||
* @usage
|
||
* <hljs lang="html">
|
||
* <div ng-controller="MyController">
|
||
* <md-button ng-click="openBottomSheet()">
|
||
* Open a Bottom Sheet!
|
||
* </md-button>
|
||
* </div>
|
||
* </hljs>
|
||
* <hljs lang="js">
|
||
* var app = angular.module('app', ['ngMaterial']);
|
||
* app.controller('MyController', function($scope, $mdBottomSheet) {
|
||
* $scope.openBottomSheet = function() {
|
||
* $mdBottomSheet.show({
|
||
* template: '<md-bottom-sheet>' +
|
||
* 'Hello! <md-button ng-click="closeBottomSheet()">Close</md-button>' +
|
||
* '</md-bottom-sheet>'
|
||
* })
|
||
*
|
||
* // Fires when the hide() method is used
|
||
* .then(function() {
|
||
* console.log('You clicked the button to close the bottom sheet!');
|
||
* })
|
||
*
|
||
* // Fires when the cancel() method is used
|
||
* .catch(function() {
|
||
* console.log('You hit escape or clicked the backdrop to close.');
|
||
* });
|
||
* };
|
||
*
|
||
* $scope.closeBottomSheet = function($scope, $mdBottomSheet) {
|
||
* $mdBottomSheet.hide();
|
||
* }
|
||
*
|
||
* });
|
||
* </hljs>
|
||
*
|
||
* ### Custom Presets
|
||
* Developers are also able to create their own preset, which can be easily used without repeating
|
||
* their options each time.
|
||
*
|
||
* <hljs lang="js">
|
||
* $mdBottomSheetProvider.addPreset('testPreset', {
|
||
* options: function() {
|
||
* return {
|
||
* template:
|
||
* '<md-bottom-sheet>' +
|
||
* 'This is a custom preset' +
|
||
* '</md-bottom-sheet>',
|
||
* controllerAs: 'bottomSheet',
|
||
* bindToController: true,
|
||
* clickOutsideToClose: true,
|
||
* escapeToClose: true
|
||
* };
|
||
* }
|
||
* });
|
||
* </hljs>
|
||
*
|
||
* After you create your preset during the config phase, you can easily access it.
|
||
*
|
||
* <hljs lang="js">
|
||
* $mdBottomSheet.show(
|
||
* $mdBottomSheet.testPreset()
|
||
* );
|
||
* </hljs>
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdBottomSheet#show
|
||
*
|
||
* @description
|
||
* Show a bottom sheet with the specified options.
|
||
*
|
||
* <em><b>Note:</b> You should <b>always</b> provide a `.catch()` method in case the user hits the
|
||
* `esc` key or clicks the background to close. In this case, the `cancel()` method will
|
||
* automatically be called on the bottom sheet which will `reject()` the promise. See the @usage
|
||
* section above for an example.
|
||
*
|
||
* Newer versions of Angular will throw a `Possibly unhandled rejection` exception if you forget
|
||
* this.</em>
|
||
*
|
||
* @param {Object} optionsOrPreset Either provide an `$mdBottomSheetPreset` defined during the
|
||
* config phase or an options object, with the following properties:
|
||
*
|
||
* - `templateUrl` - `{string=}`: The url of an html template file that will
|
||
* be used as the content of the bottom sheet. Restrictions: the template must
|
||
* have an outer `md-bottom-sheet` element.
|
||
* - `template` - `{string=}`: Same as templateUrl, except this is an actual
|
||
* template string.
|
||
* - `scope` - `{Object=}`: the scope to link the template / controller to. If none is specified,
|
||
* it will create a new child scope. This scope will be destroyed when the bottom sheet is
|
||
* removed unless `preserveScope` is set to true.
|
||
* - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed.
|
||
* Default is false
|
||
* - `controller` - `{string=}`: The controller to associate with this bottom sheet.
|
||
* - `locals` - `{string=}`: An object containing key/value pairs. The keys will be used as names
|
||
* of values to inject into the controller. For example, `locals: {three: 3}` would inject
|
||
* `three` into the controller with the value of 3.
|
||
* - `clickOutsideToClose` - `{boolean=}`: Whether the user can click outside the bottom sheet to
|
||
* close it. Default true.
|
||
* - `bindToController` - `{boolean=}`: When set to true, the locals will be bound to the
|
||
* controller instance and available in it's $onInit function.
|
||
* - `disableBackdrop` - `{boolean=}`: When set to true, the bottomsheet will not show a backdrop.
|
||
* - `escapeToClose` - `{boolean=}`: Whether the user can press escape to close the bottom sheet.
|
||
* Default true.
|
||
* - `isLockedOpen` - `{boolean=}`: Disables all default ways of closing the bottom sheet.
|
||
* **Note:** this will override the `clickOutsideToClose` and `escapeToClose` options, leaving
|
||
* only the `hide` and `cancel` methods as ways of closing the bottom sheet. Defaults to false.
|
||
* - `resolve` - `{Object=}`: Similar to locals, except it takes promises as values
|
||
* and the bottom sheet will not open until the promises resolve.
|
||
* - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope.
|
||
* - `parent` - `{element=}`: The element to append the bottom sheet to. The `parent` may be a
|
||
* `function`, `string`, `Object`, or null. Defaults to appending to the body of the root element
|
||
* (or the root element) of the application.
|
||
* e.g. angular.element(document.getElementById('content')) or "#content"
|
||
* - `disableParentScroll` - `{boolean=}`: Whether to disable scrolling while the bottom sheet is
|
||
* open. Default true.
|
||
*
|
||
* @returns {promise} A promise that can be resolved with `$mdBottomSheet.hide()` or
|
||
* rejected with `$mdBottomSheet.cancel()`.
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdBottomSheet#hide
|
||
*
|
||
* @description
|
||
* Hide the existing bottom sheet and resolve the promise returned from
|
||
* `$mdBottomSheet.show()`. This call will close the most recently opened/current bottom sheet (if
|
||
* any).
|
||
*
|
||
* <em><b>Note:</b> Use a `.then()` on your `.show()` to handle this callback.</em>
|
||
*
|
||
* @param {*=} response An argument for the resolved promise.
|
||
*
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdBottomSheet#cancel
|
||
*
|
||
* @description
|
||
* Hide the existing bottom sheet and reject the promise returned from
|
||
* `$mdBottomSheet.show()`.
|
||
*
|
||
* <em><b>Note:</b> Use a `.catch()` on your `.show()` to handle this callback.</em>
|
||
*
|
||
* @param {*=} response An argument for the rejected promise.
|
||
*
|
||
*/
|
||
|
||
function MdBottomSheetProvider($$interimElementProvider) {
|
||
// how fast we need to flick down to close the sheet, pixels/ms
|
||
bottomSheetDefaults.$inject = ["$animate", "$mdConstant", "$mdUtil", "$mdTheming", "$mdBottomSheet", "$rootElement", "$mdGesture", "$log"];
|
||
var CLOSING_VELOCITY = 0.5;
|
||
var PADDING = 80; // same as css
|
||
|
||
return $$interimElementProvider('$mdBottomSheet')
|
||
.setDefaults({
|
||
methods: ['disableParentScroll', 'escapeToClose', 'clickOutsideToClose'],
|
||
options: bottomSheetDefaults
|
||
});
|
||
|
||
/* @ngInject */
|
||
function bottomSheetDefaults($animate, $mdConstant, $mdUtil, $mdTheming, $mdBottomSheet, $rootElement,
|
||
$mdGesture, $log) {
|
||
var backdrop;
|
||
|
||
return {
|
||
themable: true,
|
||
onShow: onShow,
|
||
onRemove: onRemove,
|
||
disableBackdrop: false,
|
||
escapeToClose: true,
|
||
clickOutsideToClose: true,
|
||
disableParentScroll: true,
|
||
isLockedOpen: false
|
||
};
|
||
|
||
function onShow(scope, element, options) {
|
||
element = $mdUtil.extractElementByName(element, 'md-bottom-sheet');
|
||
|
||
// prevent tab focus or click focus on the bottom-sheet container
|
||
element.attr('tabindex', '-1');
|
||
|
||
// Once the md-bottom-sheet has `ng-cloak` applied on his template the opening animation will not work properly.
|
||
// This is a very common problem, so we have to notify the developer about this.
|
||
if (element.hasClass('ng-cloak')) {
|
||
var message = '$mdBottomSheet: using `<md-bottom-sheet ng-cloak>` will affect the bottom-sheet opening animations.';
|
||
$log.warn(message, element[0]);
|
||
}
|
||
|
||
if (options.isLockedOpen) {
|
||
options.clickOutsideToClose = false;
|
||
options.escapeToClose = false;
|
||
} else {
|
||
options.cleanupGestures = registerGestures(element, options.parent);
|
||
}
|
||
|
||
if (!options.disableBackdrop) {
|
||
// Add a backdrop that will close on click
|
||
backdrop = $mdUtil.createBackdrop(scope, "md-bottom-sheet-backdrop md-opaque");
|
||
|
||
// Prevent mouse focus on backdrop; ONLY programmatic focus allowed.
|
||
// This allows clicks on backdrop to propagate to the $rootElement and
|
||
// ESC key events to be detected properly.
|
||
backdrop[0].tabIndex = -1;
|
||
|
||
if (options.clickOutsideToClose) {
|
||
backdrop.on('click', function() {
|
||
$mdUtil.nextTick($mdBottomSheet.cancel, true);
|
||
});
|
||
}
|
||
|
||
$mdTheming.inherit(backdrop, options.parent);
|
||
|
||
$animate.enter(backdrop, options.parent, null);
|
||
}
|
||
|
||
$mdTheming.inherit(element, options.parent);
|
||
|
||
if (options.disableParentScroll) {
|
||
options.restoreScroll = $mdUtil.disableScrollAround(element, options.parent);
|
||
}
|
||
|
||
return $animate.enter(element, options.parent, backdrop)
|
||
.then(function() {
|
||
var focusable = $mdUtil.findFocusTarget(element) || angular.element(
|
||
element[0].querySelector('button') ||
|
||
element[0].querySelector('a') ||
|
||
element[0].querySelector($mdUtil.prefixer('ng-click', true))
|
||
) || backdrop;
|
||
|
||
if (options.escapeToClose) {
|
||
options.rootElementKeyupCallback = function(e) {
|
||
if (e.keyCode === $mdConstant.KEY_CODE.ESCAPE) {
|
||
$mdUtil.nextTick($mdBottomSheet.cancel, true);
|
||
}
|
||
};
|
||
|
||
$rootElement.on('keyup', options.rootElementKeyupCallback);
|
||
focusable && focusable.focus();
|
||
}
|
||
});
|
||
|
||
}
|
||
|
||
function onRemove(scope, element, options) {
|
||
if (!options.disableBackdrop) $animate.leave(backdrop);
|
||
|
||
return $animate.leave(element).then(function() {
|
||
if (options.disableParentScroll) {
|
||
options.restoreScroll();
|
||
delete options.restoreScroll;
|
||
}
|
||
|
||
options.cleanupGestures && options.cleanupGestures();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Adds the drag gestures to the bottom sheet.
|
||
* @param {JQLite} element where CSS transitions will be applied
|
||
* @param {JQLite} parent used for registering gesture listeners
|
||
* @return {Function} function that removes gesture listeners that were set up by
|
||
* registerGestures()
|
||
*/
|
||
function registerGestures(element, parent) {
|
||
var deregister = $mdGesture.register(parent, 'drag', { horizontal: false });
|
||
parent.on('$md.dragstart', onDragStart)
|
||
.on('$md.drag', onDrag)
|
||
.on('$md.dragend', onDragEnd);
|
||
|
||
return function cleanupGestures() {
|
||
deregister();
|
||
parent.off('$md.dragstart', onDragStart);
|
||
parent.off('$md.drag', onDrag);
|
||
parent.off('$md.dragend', onDragEnd);
|
||
};
|
||
|
||
function onDragStart() {
|
||
// Disable transitions on transform so that it feels fast
|
||
element.css($mdConstant.CSS.TRANSITION_DURATION, '0ms');
|
||
}
|
||
|
||
function onDrag(ev) {
|
||
var transform = ev.pointer.distanceY;
|
||
if (transform < 5) {
|
||
// Slow down drag when trying to drag up, and stop after PADDING
|
||
transform = Math.max(-PADDING, transform / 2);
|
||
}
|
||
element.css($mdConstant.CSS.TRANSFORM, 'translate3d(0,' + (PADDING + transform) + 'px,0)');
|
||
}
|
||
|
||
function onDragEnd(ev) {
|
||
if (ev.pointer.distanceY > 0 &&
|
||
(ev.pointer.distanceY > 20 || Math.abs(ev.pointer.velocityY) > CLOSING_VELOCITY)) {
|
||
var distanceRemaining = element.prop('offsetHeight') - ev.pointer.distanceY;
|
||
var transitionDuration = Math.min(distanceRemaining / ev.pointer.velocityY * 0.75, 500);
|
||
element.css($mdConstant.CSS.TRANSITION_DURATION, transitionDuration + 'ms');
|
||
$mdUtil.nextTick($mdBottomSheet.cancel, true);
|
||
} else {
|
||
element.css($mdConstant.CSS.TRANSITION_DURATION, '');
|
||
element.css($mdConstant.CSS.TRANSFORM, '');
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.button
|
||
* @description
|
||
*
|
||
* Button
|
||
*/
|
||
MdButtonDirective.$inject = ["$mdButtonInkRipple", "$mdTheming", "$mdAria", "$mdInteraction"];
|
||
MdAnchorDirective.$inject = ["$mdTheming"];
|
||
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
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-content md-theme="myTheme">
|
||
* <a href="#chapter1" class="md-accent"></a>
|
||
* </md-content>
|
||
* </hljs>
|
||
*/
|
||
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);
|
||
}
|
||
};
|
||
}
|
||
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdButton
|
||
* @module material.components.button
|
||
*
|
||
* @restrict E
|
||
*
|
||
* @description
|
||
* `<md-button>` is a button directive with optional ink ripples (default enabled).
|
||
*
|
||
* If you supply a `href` or `ng-href` attribute, it will become an `<a>` element. Otherwise, it
|
||
* will become a `<button>` element. As per the
|
||
* [Material Design specifications](https://material.google.com/style/color.html#color-color-palette)
|
||
* the FAB button background is filled with the accent color [by default]. The primary color palette
|
||
* may be used with the `md-primary` class.
|
||
*
|
||
* Developers can also change the color palette of the button, by using the following classes
|
||
* - `md-primary`
|
||
* - `md-accent`
|
||
* - `md-warn`
|
||
*
|
||
* See for example
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-button class="md-primary">Primary Button</md-button>
|
||
* </hljs>
|
||
*
|
||
* Button can be also raised, which means that they will use the current color palette to fill the button.
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-button class="md-accent md-raised">Raised and Accent Button</md-button>
|
||
* </hljs>
|
||
*
|
||
* It is also possible to disable the focus effect on the button, by using the following markup.
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-button class="md-no-focus">No Focus Style</md-button>
|
||
* </hljs>
|
||
*
|
||
* @param {string=} aria-label Adds alternative text to button for accessibility, useful for icon buttons.
|
||
* If no default text is found, a warning will be logged.
|
||
* @param {boolean=} md-no-ink If present, disable ink ripple effects.
|
||
* @param {string=} md-ripple-size Overrides the default ripple size logic. Options: `full`, `partial`, `auto`.
|
||
* @param {expression=} ng-disabled Disable the button when the expression is truthy.
|
||
* @param {expression=} ng-blur Expression evaluated when focus is removed from the button.
|
||
*
|
||
* @usage
|
||
*
|
||
* Regular buttons:
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-button> Flat Button </md-button>
|
||
* <md-button href="http://google.com"> Flat link </md-button>
|
||
* <md-button class="md-raised"> Raised Button </md-button>
|
||
* <md-button ng-disabled="true"> Disabled Button </md-button>
|
||
* <md-button>
|
||
* <md-icon md-svg-src="your/icon.svg"></md-icon>
|
||
* Register Now
|
||
* </md-button>
|
||
* </hljs>
|
||
*
|
||
* FAB buttons:
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-button class="md-fab" aria-label="FAB">
|
||
* <md-icon md-svg-src="your/icon.svg"></md-icon>
|
||
* </md-button>
|
||
* <!-- mini-FAB -->
|
||
* <md-button class="md-fab md-mini" aria-label="Mini FAB">
|
||
* <md-icon md-svg-src="your/icon.svg"></md-icon>
|
||
* </md-button>
|
||
* <!-- Button with SVG Icon -->
|
||
* <md-button class="md-icon-button" aria-label="Custom Icon Button">
|
||
* <md-icon md-svg-icon="path/to/your.svg"></md-icon>
|
||
* </md-button>
|
||
* </hljs>
|
||
*/
|
||
function MdButtonDirective($mdButtonInkRipple, $mdTheming, $mdAria, $mdInteraction) {
|
||
|
||
return {
|
||
restrict: 'EA',
|
||
replace: true,
|
||
transclude: true,
|
||
template: getTemplate,
|
||
link: postLink
|
||
};
|
||
|
||
function isAnchor(attr) {
|
||
return angular.isDefined(attr.href) || angular.isDefined(attr.ngHref) || angular.isDefined(attr.ngLink) || angular.isDefined(attr.uiSref);
|
||
}
|
||
|
||
function getTemplate(element, attr) {
|
||
if (isAnchor(attr)) {
|
||
return '<a class="md-button" ng-transclude></a>';
|
||
} else {
|
||
// If buttons don't have type="button", they will submit forms automatically.
|
||
var btnType = (typeof attr.type === 'undefined') ? 'button' : attr.type;
|
||
return '<button class="md-button" type="' + btnType + '" ng-transclude></button>';
|
||
}
|
||
}
|
||
|
||
function postLink(scope, element, attr) {
|
||
$mdTheming(element);
|
||
$mdButtonInkRipple.attach(scope, element);
|
||
|
||
// Use async expect to support possible bindings in the button label
|
||
$mdAria.expectWithoutText(element, 'aria-label');
|
||
|
||
// For anchor elements, we have to set tabindex manually when the element is disabled.
|
||
// We don't do this for md-nav-bar anchors as the component manages its own tabindex values.
|
||
if (isAnchor(attr) && angular.isDefined(attr.ngDisabled) &&
|
||
!element.hasClass('_md-nav-button')) {
|
||
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 (!element.hasClass('md-no-focus')) {
|
||
|
||
element.on('focus', function() {
|
||
|
||
// Only show the focus effect when being focused through keyboard interaction or programmatically
|
||
if (!$mdInteraction.isUserInvoked() || $mdInteraction.getLastInteractionType() === 'keyboard') {
|
||
element.addClass('md-focused');
|
||
}
|
||
|
||
});
|
||
|
||
element.on('blur', function() {
|
||
element.removeClass('md-focused');
|
||
});
|
||
}
|
||
|
||
}
|
||
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.card
|
||
*
|
||
* @description
|
||
* Card components.
|
||
*/
|
||
mdCardDirective.$inject = ["$mdTheming"];
|
||
angular.module('material.components.card', [
|
||
'material.core'
|
||
])
|
||
.directive('mdCard', mdCardDirective);
|
||
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdCard
|
||
* @module material.components.card
|
||
*
|
||
* @restrict E
|
||
*
|
||
* @description
|
||
* The `<md-card>` directive is a container element used within `<md-content>` containers.
|
||
*
|
||
* An image included as a direct descendant will fill the card's width. If you want to avoid this,
|
||
* you can add the `md-image-no-fill` class to the parent element. The `<md-card-content>`
|
||
* container will wrap text content and provide padding. An `<md-card-footer>` element can be
|
||
* optionally included to put content flush against the bottom edge of the card.
|
||
*
|
||
* Action buttons can be included in an `<md-card-actions>` element, similar to `<md-dialog-actions>`.
|
||
* You can then position buttons using layout attributes.
|
||
*
|
||
* Card is built with:
|
||
* * `<md-card-header>` - Header for the card, holds avatar, text and squared image
|
||
* - `<md-card-avatar>` - Card avatar
|
||
* - `md-user-avatar` - Class for user image
|
||
* - `<md-icon>`
|
||
* - `<md-card-header-text>` - Contains elements for the card description
|
||
* - `md-title` - Class for the card title
|
||
* - `md-subhead` - Class for the card sub header
|
||
* * `<img>` - Image for the card
|
||
* * `<md-card-title>` - Card content title
|
||
* - `<md-card-title-text>`
|
||
* - `md-headline` - Class for the card content title
|
||
* - `md-subhead` - Class for the card content sub header
|
||
* - `<md-card-title-media>` - Squared image within the title
|
||
* - `md-media-sm` - Class for small image
|
||
* - `md-media-md` - Class for medium image
|
||
* - `md-media-lg` - Class for large image
|
||
* - `md-media-xl` - Class for extra large image
|
||
* * `<md-card-content>` - Card content
|
||
* * `<md-card-actions>` - Card actions
|
||
* - `<md-card-icon-actions>` - Icon actions
|
||
*
|
||
* Cards have constant width and variable heights; where the maximum height is limited to what can
|
||
* fit within a single view on a platform, but it can temporarily expand as needed.
|
||
*
|
||
* @usage
|
||
* ### Card with optional footer
|
||
* <hljs lang="html">
|
||
* <md-card>
|
||
* <img src="card-image.png" class="md-card-image" alt="image caption">
|
||
* <md-card-content>
|
||
* <h2>Card headline</h2>
|
||
* <p>Card content</p>
|
||
* </md-card-content>
|
||
* <md-card-footer>
|
||
* Card footer
|
||
* </md-card-footer>
|
||
* </md-card>
|
||
* </hljs>
|
||
*
|
||
* ### Card with actions
|
||
* <hljs lang="html">
|
||
* <md-card>
|
||
* <img src="card-image.png" class="md-card-image" alt="image caption">
|
||
* <md-card-content>
|
||
* <h2>Card headline</h2>
|
||
* <p>Card content</p>
|
||
* </md-card-content>
|
||
* <md-card-actions layout="row" layout-align="end center">
|
||
* <md-button>Action 1</md-button>
|
||
* <md-button>Action 2</md-button>
|
||
* </md-card-actions>
|
||
* </md-card>
|
||
* </hljs>
|
||
*
|
||
* ### Card with header, image, title actions and content
|
||
* <hljs lang="html">
|
||
* <md-card>
|
||
* <md-card-header>
|
||
* <md-card-avatar>
|
||
* <img class="md-user-avatar" src="avatar.png"/>
|
||
* </md-card-avatar>
|
||
* <md-card-header-text>
|
||
* <span class="md-title">Title</span>
|
||
* <span class="md-subhead">Sub header</span>
|
||
* </md-card-header-text>
|
||
* </md-card-header>
|
||
* <img ng-src="card-image.png" class="md-card-image" alt="image caption">
|
||
* <md-card-title>
|
||
* <md-card-title-text>
|
||
* <span class="md-headline">Card headline</span>
|
||
* <span class="md-subhead">Card subheader</span>
|
||
* </md-card-title-text>
|
||
* </md-card-title>
|
||
* <md-card-actions layout="row" layout-align="start center">
|
||
* <md-button>Action 1</md-button>
|
||
* <md-button>Action 2</md-button>
|
||
* <md-card-icon-actions>
|
||
* <md-button class="md-icon-button" aria-label="icon">
|
||
* <md-icon md-svg-icon="icon"></md-icon>
|
||
* </md-button>
|
||
* </md-card-icon-actions>
|
||
* </md-card-actions>
|
||
* <md-card-content>
|
||
* <p>
|
||
* Card content
|
||
* </p>
|
||
* </md-card-content>
|
||
* </md-card>
|
||
* </hljs>
|
||
*/
|
||
function mdCardDirective($mdTheming) {
|
||
return {
|
||
restrict: 'E',
|
||
link: function ($scope, $element, attr) {
|
||
$element.addClass('_md'); // private md component indicator for styling
|
||
$mdTheming($element);
|
||
}
|
||
};
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.checkbox
|
||
* @description Checkbox module!
|
||
*/
|
||
MdCheckboxDirective.$inject = ["inputDirective", "$mdAria", "$mdConstant", "$mdTheming", "$mdUtil", "$mdInteraction"];
|
||
angular
|
||
.module('material.components.checkbox', ['material.core'])
|
||
.directive('mdCheckbox', MdCheckboxDirective);
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdCheckbox
|
||
* @module material.components.checkbox
|
||
* @restrict E
|
||
*
|
||
* @description
|
||
* The checkbox directive is used like the normal
|
||
* [angular checkbox](https://docs.angularjs.org/api/ng/input/input%5Bcheckbox%5D).
|
||
*
|
||
* As per the [Material Design spec](https://material.io/archive/guidelines/style/color.html#color-color-palette)
|
||
* the checkbox is in the accent color by default. The primary color palette may be used with
|
||
* the `md-primary` class.
|
||
*
|
||
* @param {expression} ng-model Assignable angular expression to data-bind to.
|
||
* @param {string=} name Property name of the form under which the control is published.
|
||
* @param {expression=} ng-true-value The value to which the expression should be set when selected.
|
||
* @param {expression=} ng-false-value The value to which the expression should be set when not
|
||
* selected.
|
||
* @param {expression=} ng-change Expression to be executed when the model value changes.
|
||
* @param {boolean=} md-no-ink If present, disable ink ripple 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.
|
||
* @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
|
||
* <hljs lang="html">
|
||
* <md-checkbox ng-model="isChecked" aria-label="Finished?">
|
||
* Finished ?
|
||
* </md-checkbox>
|
||
*
|
||
* <md-checkbox md-no-ink ng-model="hasInk" aria-label="No Ink Effects">
|
||
* No Ink Effects
|
||
* </md-checkbox>
|
||
*
|
||
* <md-checkbox ng-disabled="true" ng-model="isDisabled" aria-label="Disabled">
|
||
* Disabled
|
||
* </md-checkbox>
|
||
*
|
||
* </hljs>
|
||
*
|
||
*/
|
||
function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $mdUtil, $mdInteraction) {
|
||
inputDirective = inputDirective[0];
|
||
|
||
return {
|
||
restrict: 'E',
|
||
transclude: true,
|
||
require: ['^?mdInputContainer', '?ngModel', '?^form'],
|
||
priority: $mdConstant.BEFORE_NG_ARIA,
|
||
template:
|
||
'<div class="md-container" md-ink-ripple md-ink-ripple-checkbox>' +
|
||
'<div class="md-icon"></div>' +
|
||
'</div>' +
|
||
'<div ng-transclude class="md-label"></div>',
|
||
compile: compile
|
||
};
|
||
|
||
// **********************************************************
|
||
// Private Methods
|
||
// **********************************************************
|
||
|
||
function compile (tElement, tAttrs) {
|
||
tAttrs.$set('tabindex', tAttrs.tabindex || '0');
|
||
tAttrs.$set('type', 'checkbox');
|
||
tAttrs.$set('role', tAttrs.type);
|
||
tElement.addClass('md-auto-horizontal-margin');
|
||
|
||
return {
|
||
pre: function(scope, element) {
|
||
// Attach a click handler during preLink, in order to immediately stop propagation
|
||
// (especially for ng-click) when the checkbox is disabled.
|
||
element.on('click', function(e) {
|
||
if (this.hasAttribute('disabled')) {
|
||
e.stopImmediatePropagation();
|
||
}
|
||
});
|
||
},
|
||
post: postLink
|
||
};
|
||
|
||
function postLink(scope, element, attr, ctrls) {
|
||
var isIndeterminate;
|
||
var containerCtrl = ctrls[0];
|
||
var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel();
|
||
var formCtrl = ctrls[2];
|
||
var labelHasLink = element.find('a').length > 0;
|
||
|
||
// The original component structure is not accessible when the checkbox's label contains a link.
|
||
// In order to keep backwards compatibility, we're only changing the structure of the component
|
||
// when we detect a link within the label. Using a span after the md-checkbox and attaching it
|
||
// via aria-labelledby allows screen readers to find and work with the link within the label.
|
||
if (labelHasLink) {
|
||
var labelId = 'label-' + $mdUtil.nextUid();
|
||
attr.$set('aria-labelledby', labelId);
|
||
|
||
var label = element.children()[1];
|
||
// Use jQLite here since ChildNode.remove() is not supported in IE11.
|
||
angular.element(label).remove();
|
||
label.removeAttribute('ng-transclude');
|
||
label.className = 'md-checkbox-link-label';
|
||
label.setAttribute('id', labelId);
|
||
element.after(label);
|
||
// Make sure that clicking on the label still causes the checkbox to be toggled, when appropriate.
|
||
var externalLabel = element.next();
|
||
externalLabel.on('click', listener);
|
||
}
|
||
|
||
if (containerCtrl) {
|
||
var isErrorGetter = containerCtrl.isErrorGetter || function() {
|
||
return ngModelCtrl.$invalid && (ngModelCtrl.$touched || (formCtrl && formCtrl.$submitted));
|
||
};
|
||
|
||
containerCtrl.input = element;
|
||
|
||
scope.$watch(isErrorGetter, containerCtrl.setInvalid);
|
||
}
|
||
|
||
$mdTheming(element);
|
||
|
||
// 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`
|
||
element.children().on('focus', function() {
|
||
element.focus();
|
||
});
|
||
|
||
if ($mdUtil.parseAttributeBoolean(attr.mdIndeterminate)) {
|
||
setIndeterminateState();
|
||
scope.$watch(attr.mdIndeterminate, setIndeterminateState);
|
||
}
|
||
|
||
if (attr.ngChecked) {
|
||
scope.$watch(scope.$eval.bind(scope, attr.ngChecked), function(value) {
|
||
ngModelCtrl.$setViewValue(value);
|
||
ngModelCtrl.$render();
|
||
});
|
||
}
|
||
|
||
$$watchExpr('ngDisabled', 'tabindex', {
|
||
true: '-1',
|
||
false: attr.tabindex
|
||
});
|
||
|
||
// Don't emit a warning when the label has a link within it. In that case we'll use
|
||
// aria-labelledby to point to another span that should be read as the label.
|
||
if (!labelHasLink) {
|
||
$mdAria.expectWithText(element, 'aria-label');
|
||
}
|
||
|
||
// Reuse the original input[type=checkbox] directive from AngularJS core.
|
||
// This is a bit hacky as we need our own event listener and own render
|
||
// function.
|
||
inputDirective.link.pre(scope, {
|
||
on: angular.noop,
|
||
0: {}
|
||
}, attr, [ngModelCtrl]);
|
||
|
||
element.on('click', listener)
|
||
.on('keypress', keypressHandler)
|
||
.on('focus', function() {
|
||
if ($mdInteraction.getLastInteractionType() === 'keyboard') {
|
||
element.addClass('md-focused');
|
||
}
|
||
})
|
||
.on('blur', function() {
|
||
element.removeClass('md-focused');
|
||
});
|
||
|
||
ngModelCtrl.$render = render;
|
||
|
||
function $$watchExpr(expr, htmlAttr, valueOpts) {
|
||
if (attr[expr]) {
|
||
scope.$watch(attr[expr], function(val) {
|
||
if (valueOpts[val]) {
|
||
element.attr(htmlAttr, valueOpts[val]);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {KeyboardEvent} ev 'keypress' event to handle
|
||
*/
|
||
function keypressHandler(ev) {
|
||
var keyCode = ev.which || ev.keyCode;
|
||
var submit, form;
|
||
|
||
ev.preventDefault();
|
||
switch (keyCode) {
|
||
case $mdConstant.KEY_CODE.SPACE:
|
||
element.addClass('md-focused');
|
||
listener(ev);
|
||
break;
|
||
case $mdConstant.KEY_CODE.ENTER:
|
||
// Match the behavior of the native <input type="checkbox">.
|
||
// When the enter key is pressed while focusing a native checkbox inside a form,
|
||
// the browser will trigger a `click` on the first non-disabled submit button/input
|
||
// in the form. Note that this is different from text inputs, which
|
||
// will directly submit the form without needing a submit button/input to be present.
|
||
form = $mdUtil.getClosest(ev.target, 'form');
|
||
if (form) {
|
||
submit = form.querySelector('button[type="submit"]:enabled, input[type="submit"]:enabled');
|
||
if (submit) {
|
||
submit.click();
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
function listener(ev) {
|
||
// skipToggle boolean is used by the switch directive to prevent the click event
|
||
// when releasing the drag. There will be always a click if releasing the drag over the checkbox.
|
||
// If the click came from a link in the checkbox, don't toggle the value.
|
||
// We want the link to be opened without changing the value in this case.
|
||
if (element[0].hasAttribute('disabled') || scope.skipToggle || ev.target.tagName === 'A') {
|
||
return;
|
||
}
|
||
|
||
scope.$apply(function() {
|
||
// Toggle the checkbox value...
|
||
var viewValue = attr.ngChecked && attr.ngClick ? attr.checked : !ngModelCtrl.$viewValue;
|
||
|
||
ngModelCtrl.$setViewValue(viewValue, ev && ev.type);
|
||
ngModelCtrl.$render();
|
||
});
|
||
}
|
||
|
||
function render() {
|
||
// Cast the $viewValue to a boolean since it could be undefined
|
||
var checked = !!ngModelCtrl.$viewValue && !isIndeterminate;
|
||
element.toggleClass('md-checked', checked);
|
||
if (!isIndeterminate) {
|
||
if (checked) {
|
||
element.attr('aria-checked', 'true');
|
||
} else {
|
||
element.attr('aria-checked', 'false');
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {string=} newValue
|
||
*/
|
||
function setIndeterminateState(newValue) {
|
||
isIndeterminate = newValue !== false;
|
||
if (isIndeterminate) {
|
||
element.attr('aria-checked', 'mixed');
|
||
}
|
||
element.toggleClass('md-indeterminate', isIndeterminate);
|
||
ngModelCtrl.$render();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.chips
|
||
*/
|
||
/*
|
||
* @see js folder for chips implementation
|
||
*/
|
||
angular.module('material.components.chips', [
|
||
'material.core',
|
||
'material.components.autocomplete'
|
||
]);
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
|
||
MdChipCtrl.$inject = ["$scope", "$element", "$mdConstant", "$timeout", "$mdUtil"];angular
|
||
.module('material.components.chips')
|
||
.controller('MdChipCtrl', MdChipCtrl);
|
||
|
||
/**
|
||
* Controller for the MdChip component. Responsible for handling keyboard
|
||
* events and editing the chip if needed.
|
||
*
|
||
* @param $scope
|
||
* @param $element
|
||
* @param $mdConstant
|
||
* @param $timeout
|
||
* @param $mdUtil
|
||
* @constructor
|
||
*/
|
||
function MdChipCtrl ($scope, $element, $mdConstant, $timeout, $mdUtil) {
|
||
/**
|
||
* @type {$scope}
|
||
*/
|
||
this.$scope = $scope;
|
||
|
||
/**
|
||
* @type {$element}
|
||
*/
|
||
this.$element = $element;
|
||
|
||
/**
|
||
* @type {$mdConstant}
|
||
*/
|
||
this.$mdConstant = $mdConstant;
|
||
|
||
/**
|
||
* @type {$timeout}
|
||
*/
|
||
this.$timeout = $timeout;
|
||
|
||
/**
|
||
* @type {$mdUtil}
|
||
*/
|
||
this.$mdUtil = $mdUtil;
|
||
|
||
/**
|
||
* @type {boolean}
|
||
*/
|
||
this.isEditing = false;
|
||
|
||
/**
|
||
* @type {MdChipsCtrl}
|
||
*/
|
||
this.parentController = undefined;
|
||
|
||
/**
|
||
* @type {boolean}
|
||
*/
|
||
this.enableChipEdit = false;
|
||
}
|
||
|
||
|
||
/**
|
||
* @param {MdChipsCtrl} controller
|
||
*/
|
||
MdChipCtrl.prototype.init = function(controller) {
|
||
this.parentController = controller;
|
||
this.enableChipEdit = this.parentController.enableChipEdit;
|
||
|
||
if (this.enableChipEdit) {
|
||
this.$element.on('keydown', this.chipKeyDown.bind(this));
|
||
this.$element.on('dblclick', this.chipMouseDoubleClick.bind(this));
|
||
this.getChipContent().addClass('_md-chip-content-edit-is-enabled');
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* @return {Object} first element with the md-chip-content class
|
||
*/
|
||
MdChipCtrl.prototype.getChipContent = function() {
|
||
var chipContents = this.$element[0].getElementsByClassName('md-chip-content');
|
||
return angular.element(chipContents[0]);
|
||
};
|
||
|
||
|
||
/**
|
||
* When editing the chip, if the user modifies the existing contents, we'll get a span back and
|
||
* need to ignore text elements as they only contain blank space.
|
||
* `children()` ignores text elements.
|
||
*
|
||
* When editing the chip, if the user deletes the contents and then enters some new content
|
||
* we'll only get a text element back.
|
||
* @return {Object} jQuery object representing the content element of the chip
|
||
*/
|
||
MdChipCtrl.prototype.getContentElement = function() {
|
||
var contentElement = angular.element(this.getChipContent().children()[0]);
|
||
if (!contentElement || contentElement.length === 0) {
|
||
contentElement = angular.element(this.getChipContent().contents()[0]);
|
||
}
|
||
return contentElement;
|
||
};
|
||
|
||
|
||
/**
|
||
* @return {number} index of this chip
|
||
*/
|
||
MdChipCtrl.prototype.getChipIndex = function() {
|
||
return parseInt(this.$element.attr('index'));
|
||
};
|
||
|
||
|
||
/**
|
||
* Update the chip's contents, focus the chip if it's selected, and exit edit mode.
|
||
* If the contents were updated to be empty, remove the chip and re-focus the input element.
|
||
*/
|
||
MdChipCtrl.prototype.goOutOfEditMode = function() {
|
||
if (!this.isEditing) {
|
||
return;
|
||
}
|
||
|
||
this.isEditing = false;
|
||
this.$element.removeClass('_md-chip-editing');
|
||
this.getChipContent()[0].contentEditable = 'false';
|
||
var chipIndex = this.getChipIndex();
|
||
|
||
var content = this.getContentElement().text();
|
||
if (content) {
|
||
this.parentController.updateChipContents(chipIndex, content);
|
||
|
||
this.$mdUtil.nextTick(function() {
|
||
if (this.parentController.selectedChip === chipIndex) {
|
||
this.parentController.focusChip(chipIndex);
|
||
}
|
||
}.bind(this));
|
||
} else {
|
||
this.parentController.removeChipAndFocusInput(chipIndex);
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* Given an HTML element. Selects contents of it.
|
||
* @param {Element} node
|
||
*/
|
||
MdChipCtrl.prototype.selectNodeContents = function(node) {
|
||
var range, selection;
|
||
if (document.body.createTextRange) {
|
||
range = document.body.createTextRange();
|
||
range.moveToElementText(node);
|
||
range.select();
|
||
} else if (window.getSelection) {
|
||
selection = window.getSelection();
|
||
range = document.createRange();
|
||
range.selectNodeContents(node);
|
||
selection.removeAllRanges();
|
||
selection.addRange(range);
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* Presents an input element to edit the contents of the chip.
|
||
*/
|
||
MdChipCtrl.prototype.goInEditMode = function() {
|
||
this.isEditing = true;
|
||
this.$element.addClass('_md-chip-editing');
|
||
this.getChipContent()[0].contentEditable = 'true';
|
||
this.getChipContent().on('blur', function() {
|
||
this.goOutOfEditMode();
|
||
}.bind(this));
|
||
|
||
this.selectNodeContents(this.getChipContent()[0]);
|
||
};
|
||
|
||
|
||
/**
|
||
* Handles the keydown event on the chip element. If enable-chip-edit attribute is
|
||
* set to true, space or enter keys can trigger going into edit mode. Enter can also
|
||
* trigger submitting if the chip is already being edited.
|
||
* @param {KeyboardEvent} event
|
||
*/
|
||
MdChipCtrl.prototype.chipKeyDown = function(event) {
|
||
if (!this.isEditing &&
|
||
(event.keyCode === this.$mdConstant.KEY_CODE.ENTER ||
|
||
event.keyCode === this.$mdConstant.KEY_CODE.SPACE)) {
|
||
event.preventDefault();
|
||
this.goInEditMode();
|
||
} else if (this.isEditing && event.keyCode === this.$mdConstant.KEY_CODE.ENTER) {
|
||
event.preventDefault();
|
||
this.goOutOfEditMode();
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* Enter edit mode if we're not already editing and the enable-chip-edit attribute is enabled.
|
||
*/
|
||
MdChipCtrl.prototype.chipMouseDoubleClick = function() {
|
||
if (this.enableChipEdit && !this.isEditing) {
|
||
this.goInEditMode();
|
||
}
|
||
};
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
|
||
MdChip.$inject = ["$mdTheming", "$mdUtil", "$compile", "$timeout"];angular
|
||
.module('material.components.chips')
|
||
.directive('mdChip', MdChip);
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdChip
|
||
* @module material.components.chips
|
||
*
|
||
* @description
|
||
* `<md-chip>` is a component used within `<md-chips>`. It is responsible for rendering an
|
||
* individual chip.
|
||
*
|
||
*
|
||
* @usage
|
||
* <hljs lang="html">
|
||
* <md-chips>
|
||
* <md-chip>{{$chip}}</md-chip>
|
||
* </md-chips>
|
||
* </hljs>
|
||
*
|
||
*/
|
||
|
||
/**
|
||
* MDChip Directive Definition
|
||
*
|
||
* @param $mdTheming
|
||
* @param $mdUtil
|
||
* @param $compile
|
||
* @param $timeout
|
||
* @ngInject
|
||
*/
|
||
function MdChip($mdTheming, $mdUtil, $compile, $timeout) {
|
||
return {
|
||
restrict: 'E',
|
||
require: ['^?mdChips', 'mdChip'],
|
||
link: postLink,
|
||
controller: 'MdChipCtrl'
|
||
};
|
||
|
||
function postLink(scope, element, attr, ctrls) {
|
||
var chipsController = ctrls.shift();
|
||
var chipController = ctrls.shift();
|
||
var chipContentElement = angular.element(element[0].querySelector('.md-chip-content'));
|
||
|
||
$mdTheming(element);
|
||
|
||
if (chipsController) {
|
||
chipController.init(chipsController);
|
||
|
||
// When a chip is blurred, make sure to unset (or reset) the selected chip so that tabbing
|
||
// through elements works properly
|
||
chipContentElement.on('blur', function() {
|
||
chipsController.resetSelectedChip();
|
||
chipsController.$scope.$applyAsync();
|
||
});
|
||
}
|
||
|
||
// Use $timeout to ensure we run AFTER the element has been added to the DOM so we can focus it.
|
||
$timeout(function() {
|
||
if (!chipsController) {
|
||
return;
|
||
}
|
||
|
||
if (chipsController.shouldFocusLastChip) {
|
||
chipsController.focusLastChipThenInput();
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
|
||
MdChipRemove.$inject = ["$timeout"];angular
|
||
.module('material.components.chips')
|
||
.directive('mdChipRemove', MdChipRemove);
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdChipRemove
|
||
* @restrict A
|
||
* @module material.components.chips
|
||
*
|
||
* @description
|
||
* Indicates that the associated element should be used as the delete button template for all chips.
|
||
* The associated element must be a child of `md-chips`.
|
||
*
|
||
* The provided button template will be appended to each chip and will remove the associated chip
|
||
* on click.
|
||
*
|
||
* The button is not styled or themed based on the theme set on the `md-chips` component. A theme
|
||
* class and custom icon can be specified in your template.
|
||
*
|
||
* You can also specify the `type` of the button in your template.
|
||
*
|
||
* @usage
|
||
* ### With Standard Chips
|
||
* <hljs lang="html">
|
||
* <md-chips ...>
|
||
* <button md-chip-remove type="button" aria-label="Remove {{$chip}}">
|
||
* <md-icon md-svg-icon="md-cancel"></md-icon>
|
||
* </button>
|
||
* </md-chips>
|
||
* </hljs>
|
||
*
|
||
* ### With Object Chips
|
||
* <hljs lang="html">
|
||
* <md-chips ...>
|
||
* <button md-chip-remove type="button" aria-label="Remove {{$chip.name}}">
|
||
* <md-icon md-svg-icon="md-cancel"></md-icon>
|
||
* </button>
|
||
* </md-chips>
|
||
* </hljs>
|
||
*/
|
||
|
||
|
||
/**
|
||
* MdChipRemove Directive Definition.
|
||
*
|
||
* @param $timeout
|
||
* @returns {{restrict: string, require: string[], link: Function, scope: boolean}}
|
||
* @constructor
|
||
*/
|
||
function MdChipRemove ($timeout) {
|
||
return {
|
||
restrict: 'A',
|
||
require: '^mdChips',
|
||
scope: false,
|
||
link: postLink
|
||
};
|
||
|
||
function postLink(scope, element, attr, ctrl) {
|
||
element.on('click', function() {
|
||
scope.$apply(function() {
|
||
ctrl.removeChip(scope.$$replacedScope.$index);
|
||
});
|
||
});
|
||
|
||
// Child elements aren't available until after a $timeout tick as they are hidden by an
|
||
// `ng-if`. see http://goo.gl/zIWfuw
|
||
$timeout(function() {
|
||
element.attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
|
||
element.find('button').attr('tabindex', '-1');
|
||
});
|
||
}
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
|
||
MdChipTransclude.$inject = ["$compile"];angular
|
||
.module('material.components.chips')
|
||
.directive('mdChipTransclude', MdChipTransclude);
|
||
|
||
function MdChipTransclude ($compile) {
|
||
return {
|
||
restrict: 'EA',
|
||
terminal: true,
|
||
link: link,
|
||
scope: false
|
||
};
|
||
function link (scope, element, attr) {
|
||
var ctrl = scope.$parent.$mdChipsCtrl,
|
||
newScope = ctrl.parent.$new(false, ctrl.parent);
|
||
newScope.$$replacedScope = scope;
|
||
newScope.$chip = scope.$chip;
|
||
newScope.$index = scope.$index;
|
||
newScope.$mdChipsCtrl = ctrl;
|
||
|
||
var newHtml = ctrl.$scope.$eval(attr.mdChipTransclude);
|
||
|
||
element.html(newHtml);
|
||
$compile(element.contents())(newScope);
|
||
}
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* The default chip append delay.
|
||
*
|
||
* @type {number}
|
||
*/
|
||
MdChipsCtrl.$inject = ["$scope", "$attrs", "$mdConstant", "$log", "$element", "$timeout", "$mdUtil", "$mdLiveAnnouncer", "$exceptionHandler"];
|
||
var DEFAULT_CHIP_APPEND_DELAY = 300;
|
||
|
||
angular
|
||
.module('material.components.chips')
|
||
.controller('MdChipsCtrl', MdChipsCtrl);
|
||
|
||
/**
|
||
* Controller for the MdChips component. Responsible for adding to and
|
||
* removing from the list of chips, marking chips as selected, and binding to
|
||
* the models of various input components.
|
||
*
|
||
* @param $scope
|
||
* @param $attrs
|
||
* @param $mdConstant
|
||
* @param $log
|
||
* @param $element
|
||
* @param $timeout
|
||
* @param $mdUtil
|
||
* @param $mdLiveAnnouncer
|
||
* @param $exceptionHandler
|
||
* @constructor
|
||
*/
|
||
function MdChipsCtrl ($scope, $attrs, $mdConstant, $log, $element, $timeout, $mdUtil,
|
||
$mdLiveAnnouncer, $exceptionHandler) {
|
||
/** @type {Function} **/
|
||
this.$timeout = $timeout;
|
||
|
||
/** @type {Object} */
|
||
this.$mdConstant = $mdConstant;
|
||
|
||
/** @type {angular.$scope} */
|
||
this.$scope = $scope;
|
||
|
||
/** @type {angular.$scope} */
|
||
this.parent = $scope.$parent;
|
||
|
||
/** @type {$mdUtil} */
|
||
this.$mdUtil = $mdUtil;
|
||
|
||
/** @type {$log} */
|
||
this.$log = $log;
|
||
|
||
/** @type {$mdLiveAnnouncer} */
|
||
this.$mdLiveAnnouncer = $mdLiveAnnouncer;
|
||
|
||
/** @type {$exceptionHandler} */
|
||
this.$exceptionHandler = $exceptionHandler;
|
||
|
||
/** @type {$element} */
|
||
this.$element = $element;
|
||
|
||
/** @type {$attrs} */
|
||
this.$attrs = $attrs;
|
||
|
||
/** @type {angular.NgModelController} */
|
||
this.ngModelCtrl = null;
|
||
|
||
/** @type {angular.NgModelController} */
|
||
this.userInputNgModelCtrl = null;
|
||
|
||
/** @type {MdAutocompleteCtrl} */
|
||
this.autocompleteCtrl = null;
|
||
|
||
/** @type {Element} */
|
||
this.userInputElement = null;
|
||
|
||
/** @type {Array.<Object>} */
|
||
this.items = [];
|
||
|
||
/** @type {number} */
|
||
this.selectedChip = -1;
|
||
|
||
/** @type {string} */
|
||
this.enableChipEdit = $mdUtil.parseAttributeBoolean($attrs.mdEnableChipEdit);
|
||
|
||
/** @type {string} */
|
||
this.addOnBlur = $mdUtil.parseAttributeBoolean($attrs.mdAddOnBlur);
|
||
|
||
/**
|
||
* The class names to apply to the autocomplete or input.
|
||
* @type {string}
|
||
*/
|
||
this.inputClass = '';
|
||
|
||
/**
|
||
* The text to be used as the aria-label for the input.
|
||
* @type {string}
|
||
*/
|
||
this.inputAriaLabel = 'Chips input.';
|
||
|
||
/**
|
||
* Label text to describe the chips container. Used to give context and instructions to screen
|
||
* reader users when the chips container is selected.
|
||
* @type {string}
|
||
*/
|
||
this.containerHint = 'Chips container. Use arrow keys to select chips.';
|
||
|
||
/**
|
||
* Label text to describe the chips container when it is empty. Used to give context and
|
||
* instructions to screen reader users when the chips container is selected and it contains
|
||
* no chips.
|
||
* @type {string}
|
||
*/
|
||
this.containerEmptyHint =
|
||
'Chips container. Enter the text area, then type text, and press enter to add a chip.';
|
||
|
||
/**
|
||
* Hidden hint text for how to delete a chip. Used to give context to screen readers.
|
||
* @type {string}
|
||
*/
|
||
this.deleteHint = 'Press delete to remove this chip.';
|
||
|
||
/**
|
||
* Hidden label for the delete button. Used to give context to screen readers.
|
||
* @type {string}
|
||
*/
|
||
this.deleteButtonLabel = 'Remove';
|
||
|
||
/**
|
||
* Model used by the input element.
|
||
* @type {string}
|
||
*/
|
||
this.chipBuffer = '';
|
||
|
||
/**
|
||
* Whether to use the transformChip expression to transform the chip buffer
|
||
* before appending it to the list.
|
||
* @type {boolean}
|
||
*/
|
||
this.useTransformChip = false;
|
||
|
||
/**
|
||
* Whether to use the onAdd expression to notify of chip additions.
|
||
* @type {boolean}
|
||
*/
|
||
this.useOnAdd = false;
|
||
|
||
/**
|
||
* Whether to use the onRemove expression to notify of chip removals.
|
||
* @type {boolean}
|
||
*/
|
||
this.useOnRemove = false;
|
||
|
||
/**
|
||
* The ID of the chips wrapper which is used to build unique IDs for the chips and the aria-owns
|
||
* attribute.
|
||
*
|
||
* Defaults to '_md-chips-wrapper-' plus a unique number.
|
||
*
|
||
* @type {string}
|
||
*/
|
||
this.wrapperId = '';
|
||
|
||
/**
|
||
* Array of unique numbers which will be auto-generated any time the items change, and is used to
|
||
* create unique IDs for the aria-owns attribute.
|
||
*
|
||
* @type {Array<number>}
|
||
*/
|
||
this.contentIds = [];
|
||
|
||
/**
|
||
* The index of the chip that should have it's `tabindex` property set to `0` so it is selectable
|
||
* via the keyboard.
|
||
*
|
||
* @type {number|null}
|
||
*/
|
||
this.ariaTabIndex = null;
|
||
|
||
/**
|
||
* After appending a chip, the chip will be focused for this number of milliseconds before the
|
||
* input is refocused.
|
||
*
|
||
* **Note:** This is **required** for compatibility with certain screen readers in order for
|
||
* them to properly allow keyboard access.
|
||
*
|
||
* @type {number}
|
||
*/
|
||
this.chipAppendDelay = DEFAULT_CHIP_APPEND_DELAY;
|
||
|
||
/**
|
||
* Collection of functions to call to un-register watchers
|
||
*
|
||
* @type {Array}
|
||
*/
|
||
this.deRegister = [];
|
||
|
||
/**
|
||
* The screen reader will announce the chip content followed by this message when a chip is added.
|
||
* @type {string}
|
||
*/
|
||
this.addedMessage = 'added';
|
||
|
||
/**
|
||
* The screen reader will announce the chip content followed by this message when a chip is
|
||
* removed.
|
||
* @type {string}
|
||
*/
|
||
this.removedMessage = 'removed';
|
||
|
||
this.init();
|
||
}
|
||
|
||
/**
|
||
* Initializes variables and sets up watchers
|
||
*/
|
||
MdChipsCtrl.prototype.init = function() {
|
||
var ctrl = this;
|
||
|
||
// Set the wrapper ID
|
||
this.wrapperId = '_md-chips-wrapper-' + this.$mdUtil.nextUid();
|
||
|
||
// If we're using static chips, then we need to initialize a few things.
|
||
if (!this.$element.attr('ng-model')) {
|
||
this.setupStaticChips();
|
||
}
|
||
|
||
// Setup a watcher which manages the role and aria-owns attributes.
|
||
// This is never called for static chips since items is not defined.
|
||
this.deRegister.push(
|
||
this.$scope.$watchCollection('$mdChipsCtrl.items', function() {
|
||
// Make sure our input and wrapper have the correct ARIA attributes
|
||
ctrl.setupInputAria();
|
||
ctrl.setupWrapperAria();
|
||
})
|
||
);
|
||
|
||
this.deRegister.push(
|
||
this.$attrs.$observe('mdChipAppendDelay', function(newValue) {
|
||
ctrl.chipAppendDelay = parseInt(newValue) || DEFAULT_CHIP_APPEND_DELAY;
|
||
})
|
||
);
|
||
};
|
||
|
||
/**
|
||
* Destructor for cleanup
|
||
*/
|
||
MdChipsCtrl.prototype.$onDestroy = function $onDestroy() {
|
||
var $destroyFn;
|
||
while (($destroyFn = this.deRegister.pop())) {
|
||
$destroyFn.call(this);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* If we have an input, ensure it has the appropriate ARIA attributes.
|
||
*/
|
||
MdChipsCtrl.prototype.setupInputAria = function() {
|
||
var input = this.$element.find('input');
|
||
|
||
// If we have no input, just return
|
||
if (!input) {
|
||
return;
|
||
}
|
||
|
||
input.attr('role', 'textbox');
|
||
input.attr('aria-multiline', true);
|
||
if (this.inputAriaDescribedBy) {
|
||
input.attr('aria-describedby', this.inputAriaDescribedBy);
|
||
}
|
||
if (this.inputAriaLabelledBy) {
|
||
input.attr('aria-labelledby', this.inputAriaLabelledBy);
|
||
input.removeAttr('aria-label');
|
||
} else {
|
||
input.attr('aria-label', this.inputAriaLabel);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Ensure our wrapper has the appropriate ARIA attributes.
|
||
*/
|
||
MdChipsCtrl.prototype.setupWrapperAria = function() {
|
||
var ctrl = this,
|
||
wrapper = this.$element.find('md-chips-wrap');
|
||
|
||
if (this.items && this.items.length) {
|
||
// Dynamically add the listbox role on every change because it must be removed when there are
|
||
// no items.
|
||
wrapper.attr('role', 'listbox');
|
||
|
||
// Generate some random (but unique) IDs for each chip
|
||
this.contentIds = this.items.map(function() {
|
||
return ctrl.wrapperId + '-chip-' + ctrl.$mdUtil.nextUid();
|
||
});
|
||
|
||
// Use the contentIDs above to generate the aria-owns attribute
|
||
wrapper.attr('aria-owns', this.contentIds.join(' '));
|
||
wrapper.attr('aria-label', this.containerHint);
|
||
} else {
|
||
// If we have no items, then the role and aria-owns attributes MUST be removed
|
||
wrapper.removeAttr('role');
|
||
wrapper.removeAttr('aria-owns');
|
||
wrapper.attr('aria-label', this.containerEmptyHint);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Apply specific roles and aria attributes for static chips
|
||
*/
|
||
MdChipsCtrl.prototype.setupStaticChips = function() {
|
||
var ctrl = this, i, staticChips;
|
||
var wrapper = this.$element.find('md-chips-wrap');
|
||
|
||
this.$timeout(function() {
|
||
wrapper.attr('role', 'list');
|
||
staticChips = wrapper[0].children;
|
||
for (i = 0; i < staticChips.length; i++) {
|
||
staticChips[i].setAttribute('role', 'listitem');
|
||
staticChips[i].setAttribute('aria-setsize', staticChips.length);
|
||
}
|
||
if (ctrl.inputAriaDescribedBy) {
|
||
wrapper.attr('aria-describedby', ctrl.inputAriaDescribedBy);
|
||
}
|
||
if (ctrl.inputAriaLabelledBy) {
|
||
wrapper.attr('aria-labelledby', ctrl.inputAriaLabelledBy);
|
||
wrapper.removeAttr('aria-label');
|
||
} else {
|
||
wrapper.attr('aria-label', ctrl.inputAriaLabel);
|
||
}
|
||
}, 10);
|
||
};
|
||
|
||
/**
|
||
* Handles the keydown event on the input element: by default <enter> appends
|
||
* the buffer to the chip list, while backspace removes the last chip in the
|
||
* list if the current buffer is empty.
|
||
* @param {jQuery.Event|KeyboardEvent} event
|
||
*/
|
||
MdChipsCtrl.prototype.inputKeydown = function(event) {
|
||
var chipBuffer = this.getChipBuffer();
|
||
|
||
// If we have an autocomplete, and it handled the event, we have nothing to do
|
||
if (this.autocompleteCtrl && event.isDefaultPrevented && event.isDefaultPrevented()) {
|
||
return;
|
||
}
|
||
|
||
if (event.keyCode === this.$mdConstant.KEY_CODE.BACKSPACE) {
|
||
// Only select and focus the previous chip, if the current caret position of the
|
||
// input element is at the beginning.
|
||
if (this.getCursorPosition(event.target) !== 0) {
|
||
return;
|
||
}
|
||
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
|
||
if (this.items.length) {
|
||
this.selectAndFocusChipSafe(this.items.length - 1);
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
// By default <enter> appends the buffer to the chip list.
|
||
if (!this.separatorKeys || this.separatorKeys.length < 1) {
|
||
this.separatorKeys = [this.$mdConstant.KEY_CODE.ENTER];
|
||
}
|
||
|
||
// Support additional separator key codes in an array of `md-separator-keys`.
|
||
if (this.separatorKeys.indexOf(event.keyCode) !== -1) {
|
||
if ((this.autocompleteCtrl && this.requireMatch) || !chipBuffer) return;
|
||
event.preventDefault();
|
||
|
||
// Only append the chip and reset the chip buffer if the max chips limit isn't reached.
|
||
if (this.hasMaxChipsReached()) return;
|
||
|
||
this.appendChip(chipBuffer.trim());
|
||
this.resetChipBuffer();
|
||
|
||
return false;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Returns the cursor position of the specified input element.
|
||
* @param {HTMLInputElement} element relevant input element
|
||
* @returns {Number} Cursor Position of the input.
|
||
*/
|
||
MdChipsCtrl.prototype.getCursorPosition = function(element) {
|
||
/*
|
||
* Figure out whether the current input for the chips buffer is valid for using
|
||
* the selectionStart / end property to retrieve the cursor position.
|
||
* Some browsers do not allow the use of those attributes, on different input types.
|
||
*/
|
||
try {
|
||
if (element.selectionStart === element.selectionEnd) {
|
||
return element.selectionStart;
|
||
}
|
||
} catch (e) {
|
||
if (!element.value) {
|
||
return 0;
|
||
}
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* Updates the content of the chip at given index
|
||
* @param {number} chipIndex
|
||
* @param {string} chipContents
|
||
*/
|
||
MdChipsCtrl.prototype.updateChipContents = function(chipIndex, chipContents) {
|
||
if (chipIndex >= 0 && chipIndex < this.items.length) {
|
||
this.items[chipIndex] = chipContents;
|
||
this.updateNgModel(true);
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* @return {boolean} true if a chip is currently being edited. False otherwise.
|
||
*/
|
||
MdChipsCtrl.prototype.isEditingChip = function() {
|
||
return !!this.$element[0].querySelector('._md-chip-editing');
|
||
};
|
||
|
||
/**
|
||
* @param {string|Object} chip contents of a single chip
|
||
* @returns {boolean} true if the chip is an Object, false otherwise.
|
||
* @private
|
||
*/
|
||
MdChipsCtrl.prototype._isChipObject = function(chip) {
|
||
return angular.isObject(chip);
|
||
};
|
||
|
||
/**
|
||
* @returns {boolean} true if chips can be removed, false otherwise.
|
||
*/
|
||
MdChipsCtrl.prototype.isRemovable = function() {
|
||
// Return false if we have static chips
|
||
if (!this.ngModelCtrl) {
|
||
return false;
|
||
}
|
||
|
||
return this.readonly ? this.removable :
|
||
angular.isDefined(this.removable) ? this.removable : true;
|
||
};
|
||
|
||
/**
|
||
* Handles the keydown event on the chip elements: backspace removes the selected chip, arrow
|
||
* keys switch which chip is active.
|
||
* @param {KeyboardEvent} event
|
||
*/
|
||
MdChipsCtrl.prototype.chipKeydown = function (event) {
|
||
if (this.getChipBuffer()) return;
|
||
if (this.isEditingChip()) return;
|
||
|
||
switch (event.keyCode) {
|
||
case this.$mdConstant.KEY_CODE.BACKSPACE:
|
||
case this.$mdConstant.KEY_CODE.DELETE:
|
||
if (this.selectedChip < 0) return;
|
||
event.preventDefault();
|
||
// Cancel the delete action only after the event cancel. Otherwise the page will go back.
|
||
if (!this.isRemovable()) return;
|
||
this.removeAndSelectAdjacentChip(this.selectedChip, event);
|
||
break;
|
||
case this.$mdConstant.KEY_CODE.LEFT_ARROW:
|
||
event.preventDefault();
|
||
// By default, allow selection of -1 which will focus the input; if we're readonly, don't go
|
||
// below 0.
|
||
if (this.selectedChip < 0 || (this.readonly && this.selectedChip === 0)) {
|
||
this.selectedChip = this.items.length;
|
||
}
|
||
if (this.items.length) this.selectAndFocusChipSafe(this.selectedChip - 1);
|
||
break;
|
||
case this.$mdConstant.KEY_CODE.RIGHT_ARROW:
|
||
event.preventDefault();
|
||
this.selectAndFocusChipSafe(this.selectedChip + 1);
|
||
break;
|
||
case this.$mdConstant.KEY_CODE.ESCAPE:
|
||
case this.$mdConstant.KEY_CODE.TAB:
|
||
if (this.selectedChip < 0) return;
|
||
event.preventDefault();
|
||
this.onFocus();
|
||
break;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Get the input's placeholder - uses `placeholder` when list is empty and `secondary-placeholder`
|
||
* when the list is non-empty. If `secondary-placeholder` is not provided, `placeholder` is used
|
||
* always.
|
||
* @returns {string}
|
||
*/
|
||
MdChipsCtrl.prototype.getPlaceholder = function() {
|
||
// Allow `secondary-placeholder` to be blank.
|
||
var useSecondary = (this.items && this.items.length &&
|
||
(this.secondaryPlaceholder === '' || this.secondaryPlaceholder));
|
||
return useSecondary ? this.secondaryPlaceholder : this.placeholder;
|
||
};
|
||
|
||
/**
|
||
* Removes chip at {@code index} and selects the adjacent chip.
|
||
* @param {number} index adjacent chip to select
|
||
* @param {Event=} event
|
||
*/
|
||
MdChipsCtrl.prototype.removeAndSelectAdjacentChip = function(index, event) {
|
||
var self = this;
|
||
var selIndex = self.getAdjacentChipIndex(index);
|
||
var wrap = this.$element[0].querySelector('md-chips-wrap');
|
||
var chip = this.$element[0].querySelector('md-chip[index="' + index + '"]');
|
||
|
||
self.removeChip(index, event);
|
||
|
||
// The double-timeout is currently necessary to ensure that the DOM has finalized and the select()
|
||
// will find the proper chip since the selection is index-based.
|
||
//
|
||
// TODO: Investigate calling from within chip $scope.$on('$destroy') to reduce/remove timeouts
|
||
self.$timeout(function() {
|
||
self.$timeout(function() {
|
||
self.selectAndFocusChipSafe(selIndex);
|
||
});
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Sets the selected chip index to -1.
|
||
*/
|
||
MdChipsCtrl.prototype.resetSelectedChip = function() {
|
||
this.selectedChip = -1;
|
||
this.ariaTabIndex = null;
|
||
};
|
||
|
||
/**
|
||
* Gets the index of an adjacent chip to select after deletion. Adjacency is
|
||
* determined as the next chip in the list, unless the target chip is the
|
||
* last in the list, then it is the chip immediately preceding the target. If
|
||
* there is only one item in the list, -1 is returned (select none).
|
||
* The number returned is the index to select AFTER the target has been removed.
|
||
* If the current chip is not selected, then -1 is returned to select none.
|
||
* @param {number} index
|
||
* @returns {number}
|
||
*/
|
||
MdChipsCtrl.prototype.getAdjacentChipIndex = function(index) {
|
||
var len = this.items.length - 1;
|
||
return (len === 0) ? -1 :
|
||
(index === len) ? index - 1 : index;
|
||
};
|
||
|
||
/**
|
||
* Append the contents of the buffer to the chip list. This method will first
|
||
* call out to the md-transform-chip method, if provided.
|
||
* @param {string} newChip chip buffer contents that will be used to create the new chip
|
||
*/
|
||
MdChipsCtrl.prototype.appendChip = function(newChip) {
|
||
this.shouldFocusLastChip = !this.addOnBlur;
|
||
if (this.useTransformChip && this.transformChip) {
|
||
var transformedChip = this.transformChip({'$chip': newChip});
|
||
|
||
// Check to make sure the chip is defined before assigning it, otherwise, we'll just assume
|
||
// they want the string version.
|
||
if (angular.isDefined(transformedChip)) {
|
||
newChip = transformedChip;
|
||
}
|
||
}
|
||
|
||
// If items contains an identical object to newChip, do not append
|
||
if (angular.isObject(newChip)) {
|
||
var identical = this.items.some(function(item) {
|
||
return angular.equals(newChip, item);
|
||
});
|
||
if (identical) return;
|
||
}
|
||
|
||
// Check for a null (but not undefined), or existing chip and cancel appending
|
||
if (newChip == null || this.items.indexOf(newChip) + 1) return;
|
||
|
||
// Append the new chip onto our list
|
||
var length = this.items.push(newChip);
|
||
var index = length - 1;
|
||
|
||
this.updateNgModel();
|
||
|
||
// Tell screen reader users that the chip was successfully added.
|
||
// TODO add a way for developers to specify which field of the object should be announced here.
|
||
var chipContent = angular.isObject(newChip) ? '' : newChip;
|
||
this.$mdLiveAnnouncer.announce(chipContent + ' ' + this.addedMessage, 'assertive');
|
||
|
||
// If the md-on-add attribute is specified, send a chip addition event
|
||
if (this.useOnAdd && this.onAdd) {
|
||
this.onAdd({ '$chip': newChip, '$index': index });
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Sets whether to use the md-transform-chip expression. This expression is
|
||
* bound to scope and controller in {@code MdChipsDirective} as
|
||
* {@code transformChip}. Due to the nature of directive scope bindings, the
|
||
* controller cannot know on its own/from the scope whether an expression was
|
||
* actually provided.
|
||
*/
|
||
MdChipsCtrl.prototype.useTransformChipExpression = function() {
|
||
this.useTransformChip = true;
|
||
};
|
||
|
||
/**
|
||
* Sets whether to use the md-on-add expression. This expression is
|
||
* bound to scope and controller in {@code MdChipsDirective} as
|
||
* {@code onAdd}. Due to the nature of directive scope bindings, the
|
||
* controller cannot know on its own/from the scope whether an expression was
|
||
* actually provided.
|
||
*/
|
||
MdChipsCtrl.prototype.useOnAddExpression = function() {
|
||
this.useOnAdd = true;
|
||
};
|
||
|
||
/**
|
||
* Sets whether to use the md-on-remove expression. This expression is
|
||
* bound to scope and controller in {@code MdChipsDirective} as
|
||
* {@code onRemove}. Due to the nature of directive scope bindings, the
|
||
* controller cannot know on its own/from the scope whether an expression was
|
||
* actually provided.
|
||
*/
|
||
MdChipsCtrl.prototype.useOnRemoveExpression = function() {
|
||
this.useOnRemove = true;
|
||
};
|
||
|
||
/**
|
||
* Sets whether to use the md-on-select expression. This expression is
|
||
* bound to scope and controller in {@code MdChipsDirective} as
|
||
* {@code onSelect}. Due to the nature of directive scope bindings, the
|
||
* controller cannot know on its own/from the scope whether an expression was
|
||
* actually provided.
|
||
*/
|
||
MdChipsCtrl.prototype.useOnSelectExpression = function() {
|
||
this.useOnSelect = true;
|
||
};
|
||
|
||
/**
|
||
* Gets the input buffer. The input buffer can be the model bound to the
|
||
* default input item {@code this.chipBuffer}, the {@code selectedItem}
|
||
* model of an {@code md-autocomplete}, or, through some magic, the model
|
||
* bound to any input or text area element found within a
|
||
* {@code md-input-container} element.
|
||
* @return {string} the input buffer
|
||
*/
|
||
MdChipsCtrl.prototype.getChipBuffer = function() {
|
||
var chipBuffer = !this.userInputElement ? this.chipBuffer :
|
||
this.userInputNgModelCtrl ? this.userInputNgModelCtrl.$viewValue :
|
||
this.userInputElement[0].value;
|
||
|
||
// Ensure that the chip buffer is always a string. For example, the input element buffer
|
||
// might be falsy.
|
||
return angular.isString(chipBuffer) ? chipBuffer : '';
|
||
};
|
||
|
||
/**
|
||
* Resets the input buffer for either the internal input or user provided input element.
|
||
*/
|
||
MdChipsCtrl.prototype.resetChipBuffer = function() {
|
||
if (this.userInputElement) {
|
||
if (this.userInputNgModelCtrl) {
|
||
this.userInputNgModelCtrl.$setViewValue('');
|
||
this.userInputNgModelCtrl.$render();
|
||
} else {
|
||
this.userInputElement[0].value = '';
|
||
}
|
||
} else {
|
||
this.chipBuffer = '';
|
||
}
|
||
};
|
||
|
||
/**
|
||
* @returns {boolean} true if the max chips limit has been reached, false otherwise.
|
||
*/
|
||
MdChipsCtrl.prototype.hasMaxChipsReached = function() {
|
||
if (angular.isString(this.maxChips)) {
|
||
this.maxChips = parseInt(this.maxChips, 10) || 0;
|
||
}
|
||
|
||
return this.maxChips > 0 && this.items.length >= this.maxChips;
|
||
};
|
||
|
||
/**
|
||
* Updates the validity properties for the ngModel.
|
||
*
|
||
* TODO add the md-max-chips validator to this.ngModelCtrl.validators so that the validation will
|
||
* be performed automatically.
|
||
*/
|
||
MdChipsCtrl.prototype.validateModel = function() {
|
||
this.ngModelCtrl.$setValidity('md-max-chips', !this.hasMaxChipsReached());
|
||
this.ngModelCtrl.$validate(); // rerun any registered validators
|
||
};
|
||
|
||
/**
|
||
* Function to handle updating the model, validation, and change notification when a chip
|
||
* is added, removed, or changed.
|
||
* @param {boolean=} skipValidation true to skip calling validateModel()
|
||
*/
|
||
MdChipsCtrl.prototype.updateNgModel = function(skipValidation) {
|
||
if (!skipValidation) {
|
||
this.validateModel();
|
||
}
|
||
// This will trigger ng-change to fire, even in cases where $setViewValue() would not.
|
||
angular.forEach(this.ngModelCtrl.$viewChangeListeners, function(listener) {
|
||
try {
|
||
listener();
|
||
} catch (e) {
|
||
this.$exceptionHandler(e);
|
||
}
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Removes the chip at the given index.
|
||
* @param {number} index of chip to remove
|
||
* @param {Event=} event optionally passed to the onRemove callback
|
||
*/
|
||
MdChipsCtrl.prototype.removeChip = function(index, event) {
|
||
var removed = this.items.splice(index, 1);
|
||
|
||
this.updateNgModel();
|
||
this.ngModelCtrl.$setDirty();
|
||
|
||
// Tell screen reader users that the chip was successfully removed.
|
||
// TODO add a way for developers to specify which field of the object should be announced here.
|
||
var chipContent = angular.isObject(removed[0]) ? '' : removed[0];
|
||
this.$mdLiveAnnouncer.announce(chipContent + ' ' + this.removedMessage, 'assertive');
|
||
|
||
if (removed && removed.length && this.useOnRemove && this.onRemove) {
|
||
this.onRemove({ '$chip': removed[0], '$index': index, '$event': event });
|
||
}
|
||
};
|
||
|
||
/**
|
||
* @param {number} index location of chip to remove
|
||
* @param {Event=} $event
|
||
*/
|
||
MdChipsCtrl.prototype.removeChipAndFocusInput = function (index, $event) {
|
||
this.removeChip(index, $event);
|
||
|
||
if (this.autocompleteCtrl) {
|
||
// Always hide the autocomplete dropdown before focusing the autocomplete input.
|
||
// Wait for the input to move horizontally, because the chip was removed.
|
||
// This can lead to an incorrect dropdown position.
|
||
this.autocompleteCtrl.hidden = true;
|
||
this.$mdUtil.nextTick(this.onFocus.bind(this));
|
||
} else {
|
||
this.onFocus();
|
||
}
|
||
|
||
};
|
||
/**
|
||
* Selects the chip at `index`,
|
||
* @param {number} index location of chip to select and focus
|
||
*/
|
||
MdChipsCtrl.prototype.selectAndFocusChipSafe = function(index) {
|
||
// If we have no chips, or are asked to select a chip before the first, just focus the input
|
||
if (!this.items.length || index === -1) {
|
||
return this.focusInput();
|
||
}
|
||
|
||
// If we are asked to select a chip greater than the number of chips...
|
||
if (index >= this.items.length) {
|
||
if (this.readonly) {
|
||
// If we are readonly, jump back to the start (because we have no input)
|
||
index = 0;
|
||
} else {
|
||
// If we are not readonly, we should attempt to focus the input
|
||
return this.onFocus();
|
||
}
|
||
}
|
||
|
||
index = Math.max(index, 0);
|
||
index = Math.min(index, this.items.length - 1);
|
||
|
||
this.selectChip(index);
|
||
this.focusChip(index);
|
||
};
|
||
|
||
/**
|
||
* Focus last chip, then focus the input. This is needed for screen reader support.
|
||
*/
|
||
MdChipsCtrl.prototype.focusLastChipThenInput = function() {
|
||
var ctrl = this;
|
||
|
||
ctrl.shouldFocusLastChip = false;
|
||
|
||
ctrl.focusChip(this.items.length - 1);
|
||
|
||
ctrl.$timeout(function() {
|
||
ctrl.focusInput();
|
||
}, ctrl.chipAppendDelay);
|
||
};
|
||
|
||
/**
|
||
* Focus the input element.
|
||
*/
|
||
MdChipsCtrl.prototype.focusInput = function() {
|
||
this.selectChip(-1);
|
||
this.onFocus();
|
||
};
|
||
|
||
/**
|
||
* Marks the chip at the given index as selected.
|
||
* @param {number} index location of chip to select
|
||
*/
|
||
MdChipsCtrl.prototype.selectChip = function(index) {
|
||
if (index >= -1 && index <= this.items.length) {
|
||
this.selectedChip = index;
|
||
|
||
// Fire the onSelect if provided
|
||
if (this.useOnSelect && this.onSelect) {
|
||
this.onSelect({'$chip': this.items[index] });
|
||
}
|
||
} else {
|
||
this.$log.warn('Selected Chip index out of bounds; ignoring.');
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Call {@code focus()} on the chip at {@code index}
|
||
* @param {number} index location of chip to focus
|
||
*/
|
||
MdChipsCtrl.prototype.focusChip = function(index) {
|
||
var chipContent = this.$element[0].querySelector(
|
||
'md-chip[index="' + index + '"] .md-chip-content'
|
||
);
|
||
|
||
this.ariaTabIndex = index;
|
||
|
||
chipContent.focus();
|
||
};
|
||
|
||
/**
|
||
* Configures the required interactions with the ngModel Controller.
|
||
* Specifically, set {@code this.items} to the {@code NgModelController#$viewValue}.
|
||
* @param {NgModelController} ngModelCtrl
|
||
*/
|
||
MdChipsCtrl.prototype.configureNgModel = function(ngModelCtrl) {
|
||
this.ngModelCtrl = ngModelCtrl;
|
||
|
||
var self = this;
|
||
|
||
// in chips the meaning of $isEmpty changes
|
||
ngModelCtrl.$isEmpty = function(value) {
|
||
return !value || value.length === 0;
|
||
};
|
||
|
||
ngModelCtrl.$render = function() {
|
||
// model is updated. do something.
|
||
self.items = self.ngModelCtrl.$viewValue;
|
||
};
|
||
};
|
||
|
||
MdChipsCtrl.prototype.onFocus = function () {
|
||
var input = this.$element[0].querySelector('input');
|
||
input && input.focus();
|
||
this.resetSelectedChip();
|
||
};
|
||
|
||
MdChipsCtrl.prototype.onInputFocus = function () {
|
||
this.inputHasFocus = true;
|
||
|
||
// Make sure we have the appropriate ARIA attributes
|
||
this.setupInputAria();
|
||
|
||
// Make sure we don't have any chips selected
|
||
this.resetSelectedChip();
|
||
};
|
||
|
||
MdChipsCtrl.prototype.onInputBlur = function () {
|
||
this.inputHasFocus = false;
|
||
|
||
if (this.shouldAddOnBlur()) {
|
||
this.appendChip(this.getChipBuffer().trim());
|
||
this.resetChipBuffer();
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Configure event bindings on input element.
|
||
* @param {angular.element} inputElement
|
||
*/
|
||
MdChipsCtrl.prototype.configureInput = function configureInput(inputElement) {
|
||
// Find the NgModelCtrl for the input element
|
||
var ngModelCtrl = inputElement.controller('ngModel');
|
||
var ctrl = this;
|
||
|
||
if (ngModelCtrl) {
|
||
|
||
// sync touched-state from inner input to chips-element
|
||
this.deRegister.push(
|
||
this.$scope.$watch(
|
||
function() {
|
||
return ngModelCtrl.$touched;
|
||
},
|
||
function(isTouched) {
|
||
isTouched && ctrl.ngModelCtrl.$setTouched();
|
||
}
|
||
)
|
||
);
|
||
|
||
// sync dirty-state from inner input to chips-element
|
||
this.deRegister.push(
|
||
this.$scope.$watch(
|
||
function() {
|
||
return ngModelCtrl.$dirty;
|
||
},
|
||
function(isDirty) {
|
||
isDirty && ctrl.ngModelCtrl.$setDirty();
|
||
}
|
||
)
|
||
);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Configure event bindings on a user-provided input element.
|
||
* @param {angular.element} inputElement
|
||
*/
|
||
MdChipsCtrl.prototype.configureUserInput = function(inputElement) {
|
||
this.userInputElement = inputElement;
|
||
|
||
// Find the NgModelCtrl for the input element
|
||
var ngModelCtrl = inputElement.controller('ngModel');
|
||
// `.controller` will look in the parent as well.
|
||
if (ngModelCtrl !== this.ngModelCtrl) {
|
||
this.userInputNgModelCtrl = ngModelCtrl;
|
||
}
|
||
|
||
var scope = this.$scope;
|
||
var ctrl = this;
|
||
|
||
// Run all of the events using evalAsync because a focus may fire a blur in the same digest loop
|
||
var scopeApplyFn = function(event, fn) {
|
||
scope.$evalAsync(angular.bind(ctrl, fn, event));
|
||
};
|
||
|
||
// Bind to keydown and focus events of input
|
||
inputElement
|
||
.attr({ tabindex: 0 })
|
||
.on('keydown', function(event) { scopeApplyFn(event, ctrl.inputKeydown); })
|
||
.on('focus', function(event) { scopeApplyFn(event, ctrl.onInputFocus); })
|
||
.on('blur', function(event) { scopeApplyFn(event, ctrl.onInputBlur); });
|
||
};
|
||
|
||
/**
|
||
* @param {MdAutocompleteCtrl} ctrl controller from the autocomplete component
|
||
*/
|
||
MdChipsCtrl.prototype.configureAutocomplete = function(ctrl) {
|
||
if (ctrl) {
|
||
this.autocompleteCtrl = ctrl;
|
||
// Update the default container empty hint when we're inside of an autocomplete.
|
||
if (!this.$element.attr('container-empty-hint')) {
|
||
this.containerEmptyHint = 'Chips container with autocompletion. Enter the text area, ' +
|
||
'type text to search, and then use the up and down arrow keys to select an option. ' +
|
||
'Press enter to add the selected option as a chip.';
|
||
this.setupWrapperAria();
|
||
}
|
||
|
||
ctrl.registerSelectedItemWatcher(angular.bind(this, function (item) {
|
||
if (item) {
|
||
// Only append the chip and reset the chip buffer if the max chips limit isn't reached.
|
||
if (this.hasMaxChipsReached()) return;
|
||
|
||
this.appendChip(item);
|
||
this.resetChipBuffer();
|
||
}
|
||
}));
|
||
|
||
this.$element.find('input')
|
||
.on('focus',angular.bind(this, this.onInputFocus))
|
||
.on('blur', angular.bind(this, this.onInputBlur));
|
||
}
|
||
};
|
||
|
||
/**
|
||
* @returns {boolean} Whether the current chip buffer should be added on input blur or not.
|
||
*/
|
||
MdChipsCtrl.prototype.shouldAddOnBlur = function() {
|
||
|
||
// Update the custom ngModel validators from the chips component.
|
||
this.validateModel();
|
||
|
||
var chipBuffer = this.getChipBuffer().trim();
|
||
// If the model value is empty and required is set on the element, then the model will be invalid.
|
||
// In that case, we still want to allow adding the chip. The main (but not only) case we want
|
||
// to disallow is adding a chip on blur when md-max-chips validation fails.
|
||
var isModelValid = this.ngModelCtrl.$isEmpty(this.ngModelCtrl.$modelValue) ||
|
||
this.ngModelCtrl.$valid;
|
||
var isAutocompleteShowing = this.autocompleteCtrl && !this.autocompleteCtrl.hidden;
|
||
|
||
if (this.userInputNgModelCtrl) {
|
||
isModelValid = isModelValid && this.userInputNgModelCtrl.$valid;
|
||
}
|
||
|
||
return this.addOnBlur && !this.requireMatch && chipBuffer && isModelValid &&
|
||
!isAutocompleteShowing;
|
||
};
|
||
|
||
/**
|
||
* @returns {boolean} true if the input or a chip is focused. False otherwise.
|
||
*/
|
||
MdChipsCtrl.prototype.hasFocus = function () {
|
||
return this.inputHasFocus || this.selectedChip >= 0;
|
||
};
|
||
|
||
/**
|
||
* @param {number} index location of content id
|
||
* @returns {number} unique id for the aria-owns attribute
|
||
*/
|
||
MdChipsCtrl.prototype.contentIdFor = function(index) {
|
||
return this.contentIds[index];
|
||
};
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
|
||
MdChips.$inject = ["$mdTheming", "$mdUtil", "$compile", "$log", "$timeout", "$$mdSvgRegistry"];angular
|
||
.module('material.components.chips')
|
||
.directive('mdChips', MdChips);
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdChips
|
||
* @module material.components.chips
|
||
*
|
||
* @description
|
||
* `<md-chips>` is an input component for building lists of strings or objects. The list items are
|
||
* displayed as 'chips'. This component can make use of an `<input>` element or an
|
||
* `<md-autocomplete>` element.
|
||
*
|
||
* ### Custom templates
|
||
* A custom template may be provided to render the content of each chip. This is achieved by
|
||
* specifying an `<md-chip-template>` element containing the custom content as a child of
|
||
* `<md-chips>`.
|
||
*
|
||
* Note: Any attributes on
|
||
* `<md-chip-template>` will be dropped as only the innerHTML is used for the chip template. The
|
||
* variables `$chip` and `$index` are available in the scope of `<md-chip-template>`, representing
|
||
* the chip object and its index in the list of chips, respectively.
|
||
* To override the chip delete control, include an element (ideally a button) with the attribute
|
||
* `md-chip-remove`. A click listener to remove the chip will be added automatically. The element
|
||
* is also placed as a sibling to the chip content (on which there are also click listeners) to
|
||
* avoid a nested ng-click situation.
|
||
*
|
||
* <!-- Note: We no longer want to include this in the site docs; but it should remain here for
|
||
* future developers and those looking at the documentation.
|
||
*
|
||
* <h3> Pending Features </h3>
|
||
* <ul style="padding-left:20px;">
|
||
*
|
||
* <ul>Style
|
||
* <li>Colors for hover, press states (ripple?).</li>
|
||
* </ul>
|
||
*
|
||
* <ul>Validation
|
||
* <li>allow a validation callback</li>
|
||
* <li>highlighting style for invalid chips</li>
|
||
* </ul>
|
||
*
|
||
* <ul>Item mutation
|
||
* <li>Support `
|
||
* <md-chip-edit>` template, show/hide the edit element on tap/click? double tap/double
|
||
* click?
|
||
* </li>
|
||
* </ul>
|
||
*
|
||
* <ul>Truncation and Disambiguation (?)
|
||
* <li>Truncate chip text where possible, but do not truncate entries such that two are
|
||
* indistinguishable.</li>
|
||
* </ul>
|
||
*
|
||
* <ul>Drag and Drop
|
||
* <li>Drag and drop chips between related `<md-chips>` elements.
|
||
* </li>
|
||
* </ul>
|
||
* </ul>
|
||
*
|
||
* //-->
|
||
*
|
||
* Sometimes developers want to limit the amount of possible chips.<br/>
|
||
* You can specify the maximum amount of chips by using the following markup.
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-chips
|
||
* ng-model="myItems"
|
||
* placeholder="Add an item"
|
||
* md-max-chips="5">
|
||
* </md-chips>
|
||
* </hljs>
|
||
*
|
||
* In some cases, you have an autocomplete inside of the `md-chips`.<br/>
|
||
* When the maximum amount of chips has been reached, you can also disable the autocomplete
|
||
* selection.<br/>
|
||
* Here is an example markup.
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-chips ng-model="myItems" md-max-chips="5">
|
||
* <md-autocomplete ng-hide="myItems.length > 5" ...></md-autocomplete>
|
||
* </md-chips>
|
||
* </hljs>
|
||
*
|
||
* ### Accessibility
|
||
*
|
||
* The `md-chips` component supports keyboard and screen reader users since Version 1.1.2. In
|
||
* order to achieve this, we modified the chips behavior to select newly appended chips for
|
||
* `300ms` before re-focusing the input and allowing the user to type.
|
||
*
|
||
* For most users, this delay is small enough that it will not be noticeable but allows certain
|
||
* screen readers to function properly (JAWS and NVDA in particular).
|
||
*
|
||
* We introduced a new `md-chip-append-delay` option to allow developers to better control this
|
||
* behavior.
|
||
*
|
||
* Please refer to the documentation of this option (below) for more information.
|
||
*
|
||
* @param {expression} ng-model Assignable AngularJS expression to be data-bound to the list of
|
||
* chips. The expression should evaluate to a `string` or `Object` Array. The type of this
|
||
* array should align with the return value of `md-transform-chip`.
|
||
* @param {expression=} ng-change AngularJS expression to be executed on chip addition, removal,
|
||
* or content change.
|
||
* @param {string=} placeholder Placeholder text that will be forwarded to the input.
|
||
* @param {string=} secondary-placeholder Placeholder text that will be forwarded to the input,
|
||
* displayed when there is at least one item in the list
|
||
* @param {boolean=} md-removable Enables or disables the deletion of chips through the
|
||
* removal icon or the Delete/Backspace key. Defaults to true.
|
||
* @param {boolean=} readonly Disables list manipulation (deleting or adding list items), hiding
|
||
* the input and delete buttons. If no `ng-model` is provided, the chips will automatically be
|
||
* marked as readonly.<br/><br/>
|
||
* When `md-removable` is not defined, the `md-remove` behavior will be overwritten and
|
||
* disabled.
|
||
* @param {boolean=} md-enable-chip-edit Set this to `"true"` to enable editing of chip contents.
|
||
* The user can go into edit mode by pressing the `space` or `enter` keys, or by double
|
||
* clicking on the chip. Chip editing is only supported for chips using the basic template.
|
||
* **Note:** This attribute is only evaluated once; it is not watched.
|
||
* @param {boolean=} ng-required Whether ng-model is allowed to be empty or not.
|
||
* @param {number=} md-max-chips The maximum number of chips allowed to add through user input.
|
||
* <br/><br/>The validation property `md-max-chips` can be used when the max chips
|
||
* amount is reached.
|
||
* @param {boolean=} md-add-on-blur When set to `"true"`, the remaining text inside of the input
|
||
* will be converted into a new chip on blur.
|
||
* **Note:** This attribute is only evaluated once; it is not watched.
|
||
* @param {expression} md-transform-chip An expression of form `myFunction($chip)` that when
|
||
* called expects one of the following return values:
|
||
* - an object representing the `$chip` input string
|
||
* - `undefined` to simply add the `$chip` input string, or
|
||
* - `null` to prevent the chip from being appended
|
||
* @param {expression=} md-on-add An expression which will be called when a chip has been
|
||
* added with `$chip` and `$index` available as parameters.
|
||
* @param {expression=} md-on-remove An expression which will be called when a chip has been
|
||
* removed with `$chip`, `$index`, and `$event` available as parameters.
|
||
* @param {expression=} md-on-select An expression which will be called when a chip is selected.
|
||
* @param {boolean=} md-require-match If true, and the chips template contains an autocomplete,
|
||
* only allow selection of pre-defined chips (i.e. you cannot add new ones).
|
||
* @param {string=} md-input-class This class will be applied to the child input for custom
|
||
* styling. If you are using an `md-autocomplete`, then you need to put this attribute on the
|
||
* `md-autocomplete` rather than the `md-chips`.
|
||
* @param {string=} input-aria-describedby A space-separated list of element IDs. This should
|
||
* contain the IDs of any elements that describe this autocomplete. Screen readers will read
|
||
* the content of these elements at the end of announcing that the chips input has been
|
||
* selected and describing its current state. The descriptive elements do not need to be
|
||
* visible on the page.
|
||
* @param {string=} input-aria-labelledby A space-separated list of element IDs. The ideal use
|
||
* case is that this would contain the ID of a `<label>` element that is associated with these
|
||
* chips.<br><br>
|
||
* For `<label id="state">US State</label>`, you would set this to
|
||
* `input-aria-labelledby="state"`.
|
||
* @param {string=} input-aria-label A string read by screen readers to identify the input.
|
||
* For static chips, this will be applied to the chips container.
|
||
* @param {string=} container-hint A string read by screen readers informing users of how to
|
||
* navigate the chips when there are chips. Only applies when `ng-model` is defined.
|
||
* @param {string=} container-empty-hint A string read by screen readers informing users of how to
|
||
* add chips when there are no chips. You will want to use this to override the default when
|
||
* in a non-English locale. Only applies when `ng-model` is defined.
|
||
* @param {string=} delete-hint A string read by screen readers instructing users that pressing
|
||
* the delete key will remove the chip. You will want to use this to override the default when
|
||
* in a non-English locale.
|
||
* @param {string=} delete-button-label Text for the `aria-label` of the button with the
|
||
* `md-chip-remove` class. If the chip is an Object, then this will be the only text in the
|
||
* label. Otherwise, this is prepended to the string representation of the chip. Defaults to
|
||
* "Remove", which would be "Remove Apple" for a chip that contained the string "Apple".
|
||
* You will want to use this to override the default when in a non-English locale.
|
||
* @param {string=} md-removed-message Screen readers will announce this message following the
|
||
* chips contents. The default is `"removed"`. If a chip with the content of "Apple" was
|
||
* removed, the screen reader would read "Apple removed". You will want to use this to override
|
||
* the default when in a non-English locale.
|
||
* @param {string=} md-added-message Screen readers will announce this message following the
|
||
* chips contents. The default is `"added"`. If a chip with the content of "Apple" was
|
||
* created, the screen reader would read "Apple added". You will want to use this to override
|
||
* the default when in a non-English locale.
|
||
* @param {expression=} md-separator-keys An array of key codes used to separate chips.
|
||
* @param {string=} md-chip-append-delay The number of milliseconds that the component will select
|
||
* a newly appended chip before allowing a user to type into the input. This is **necessary**
|
||
* for keyboard accessibility for screen readers. It defaults to 300ms and any number less than
|
||
* 300 can cause issues with screen readers (particularly JAWS and sometimes NVDA).
|
||
*
|
||
* _Available since Version 1.1.2._
|
||
*
|
||
* **Note:** You can safely set this to `0` in one of the following two instances:
|
||
*
|
||
* 1. You are targeting an iOS or Safari-only application (where users would use VoiceOver) or
|
||
* only ChromeVox users.
|
||
*
|
||
* 2. If you have utilized the `md-separator-keys` to disable the `enter` keystroke in
|
||
* favor of another one (such as `,` or `;`).
|
||
*
|
||
* @usage
|
||
* <hljs lang="html">
|
||
* <md-chips
|
||
* ng-model="myItems"
|
||
* placeholder="Add an item"
|
||
* readonly="isReadOnly">
|
||
* </md-chips>
|
||
* </hljs>
|
||
*
|
||
* <h3>Validation</h3>
|
||
* When using [ngMessages](https://docs.angularjs.org/api/ngMessages), you can show errors based
|
||
* on our custom validators.
|
||
* <hljs lang="html">
|
||
* <form name="userForm">
|
||
* <md-chips
|
||
* name="fruits"
|
||
* ng-model="myItems"
|
||
* placeholder="Add an item"
|
||
* md-max-chips="5">
|
||
* </md-chips>
|
||
* <div ng-messages="userForm.fruits.$error" ng-if="userForm.$dirty">
|
||
* <div ng-message="md-max-chips">You reached the maximum amount of chips</div>
|
||
* </div>
|
||
* </form>
|
||
* </hljs>
|
||
*
|
||
*/
|
||
|
||
// TODO add a way for developers to specify which field of the object should used in the
|
||
// aria-label.
|
||
var MD_CHIPS_TEMPLATE = '\
|
||
<md-chips-wrap\
|
||
id="{{$mdChipsCtrl.wrapperId}}"\
|
||
tabindex="{{$mdChipsCtrl.readonly ? 0 : -1}}"\
|
||
ng-keydown="$mdChipsCtrl.chipKeydown($event)"\
|
||
ng-class="{ \'md-focused\': $mdChipsCtrl.hasFocus(), \
|
||
\'md-readonly\': !$mdChipsCtrl.ngModelCtrl || $mdChipsCtrl.readonly,\
|
||
\'md-removable\': $mdChipsCtrl.isRemovable() }"\
|
||
class="md-chips">\
|
||
<md-chip ng-repeat="$chip in $mdChipsCtrl.items"\
|
||
index="{{$index}}" \
|
||
ng-class="{\'md-focused\': $mdChipsCtrl.selectedChip == $index, \'md-readonly\': !$mdChipsCtrl.ngModelCtrl || $mdChipsCtrl.readonly}">\
|
||
<div class="md-chip-content"\
|
||
tabindex="{{$mdChipsCtrl.ariaTabIndex === $index ? 0 : -1}}"\
|
||
id="{{$mdChipsCtrl.contentIdFor($index)}}"\
|
||
role="option"\
|
||
aria-selected="{{$mdChipsCtrl.selectedChip === $index}}"\
|
||
aria-setsize="{{$mdChipsCtrl.items.length}}"\
|
||
aria-posinset="{{$index+1}}"\
|
||
ng-click="!$mdChipsCtrl.readonly && $mdChipsCtrl.focusChip($index)"\
|
||
aria-label="{{$mdChipsCtrl._isChipObject($chip) ? \'\' : $chip + \'. \'}}{{$mdChipsCtrl.isRemovable() ? \'\' + $mdChipsCtrl.deleteHint : \'\'}}" \
|
||
ng-focus="!$mdChipsCtrl.readonly && $mdChipsCtrl.selectChip($index)"\
|
||
md-chip-transclude="$mdChipsCtrl.chipContentsTemplate"></div>\
|
||
<div ng-if="$mdChipsCtrl.isRemovable()"\
|
||
class="md-chip-remove-container"\
|
||
tabindex="-1"\
|
||
md-chip-transclude="$mdChipsCtrl.chipRemoveTemplate"></div>\
|
||
</md-chip>\
|
||
<div class="md-chip-input-container" ng-if="!$mdChipsCtrl.readonly && $mdChipsCtrl.ngModelCtrl">\
|
||
<div md-chip-transclude="$mdChipsCtrl.chipInputTemplate"></div>\
|
||
</div>\
|
||
</md-chips-wrap>';
|
||
|
||
var CHIP_INPUT_TEMPLATE = '\
|
||
<input\
|
||
class="md-input{{ $mdChipsCtrl.inputClass ? \' \' + $mdChipsCtrl.inputClass: \'\'}}"\
|
||
tabindex="0"\
|
||
aria-label="{{$mdChipsCtrl.inputAriaLabel}}"\
|
||
placeholder="{{$mdChipsCtrl.getPlaceholder()}}"\
|
||
ng-model="$mdChipsCtrl.chipBuffer"\
|
||
ng-focus="$mdChipsCtrl.onInputFocus()"\
|
||
ng-blur="$mdChipsCtrl.onInputBlur()"\
|
||
ng-keydown="$mdChipsCtrl.inputKeydown($event)">';
|
||
|
||
var CHIP_DEFAULT_TEMPLATE = '\
|
||
<span>{{$chip}}</span>';
|
||
|
||
var CHIP_REMOVE_TEMPLATE = '\
|
||
<button\
|
||
class="md-chip-remove"\
|
||
ng-if="$mdChipsCtrl.isRemovable()"\
|
||
ng-click="$mdChipsCtrl.removeChipAndFocusInput($$replacedScope.$index, $event)"\
|
||
type="button"\
|
||
tabindex="-1"\
|
||
aria-label="{{$mdChipsCtrl.deleteButtonLabel}}{{$mdChipsCtrl._isChipObject($chip) ? \'\' : \' \' + $chip}}">\
|
||
<md-icon md-svg-src="{{$mdChipsCtrl.mdCloseIcon}}" aria-hidden="true"></md-icon>\
|
||
</button>';
|
||
|
||
/**
|
||
* MDChips Directive Definition
|
||
*/
|
||
function MdChips ($mdTheming, $mdUtil, $compile, $log, $timeout, $$mdSvgRegistry) {
|
||
// Run our templates through $mdUtil.processTemplate() to allow custom start/end symbols
|
||
var templates = getTemplates();
|
||
|
||
return {
|
||
template: function(element, attrs) {
|
||
// Clone the element into an attribute. By prepending the attribute
|
||
// name with '$', AngularJS won't write it into the DOM. The cloned
|
||
// element propagates to the link function via the attrs argument,
|
||
// where various contained-elements can be consumed.
|
||
attrs['$mdUserTemplate'] = element.clone();
|
||
return templates.chips;
|
||
},
|
||
require: ['mdChips'],
|
||
restrict: 'E',
|
||
controller: 'MdChipsCtrl',
|
||
controllerAs: '$mdChipsCtrl',
|
||
bindToController: true,
|
||
compile: compile,
|
||
scope: {
|
||
readonly: '=?readonly',
|
||
removable: '=?mdRemovable',
|
||
placeholder: '@?',
|
||
secondaryPlaceholder: '@?',
|
||
maxChips: '@?mdMaxChips',
|
||
transformChip: '&mdTransformChip',
|
||
onAdd: '&?mdOnAdd',
|
||
onRemove: '&?mdOnRemove',
|
||
addedMessage: '@?mdAddedMessage',
|
||
removedMessage: '@?mdRemovedMessage',
|
||
onSelect: '&?mdOnSelect',
|
||
inputClass: '@?mdInputClass',
|
||
inputAriaDescribedBy: '@?inputAriaDescribedby',
|
||
inputAriaLabelledBy: '@?inputAriaLabelledby',
|
||
inputAriaLabel: '@?',
|
||
containerHint: '@?',
|
||
containerEmptyHint: '@?',
|
||
deleteHint: '@?',
|
||
deleteButtonLabel: '@?',
|
||
separatorKeys: '=?mdSeparatorKeys',
|
||
requireMatch: '=?mdRequireMatch',
|
||
chipAppendDelayString: '@?mdChipAppendDelay',
|
||
ngChange: '&?'
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Builds the final template for `md-chips` and returns the postLink function.
|
||
*
|
||
* Building the template involves 3 key components:
|
||
* static chips
|
||
* chip template
|
||
* input control
|
||
*
|
||
* If no `ng-model` is provided, only the static chip work needs to be done.
|
||
*
|
||
* If no user-passed `md-chip-template` exists, the default template is used. This resulting
|
||
* template is appended to the chip content element.
|
||
*
|
||
* The remove button may be overridden by passing an element with an md-chip-remove attribute.
|
||
*
|
||
* If an `input` or `md-autocomplete` element is provided by the caller, it is set aside for
|
||
* transclusion later. The transclusion happens in `postLink` as the parent scope is required.
|
||
* If no user input is provided, a default one is appended to the input container node in the
|
||
* template.
|
||
*
|
||
* Static Chips (i.e. `md-chip` elements passed from the caller) are gathered and set aside for
|
||
* transclusion in the `postLink` function.
|
||
*
|
||
*
|
||
* @param element
|
||
* @param attr
|
||
* @returns {Function}
|
||
*/
|
||
function compile(element, attr) {
|
||
// Grab the user template from attr and reset the attribute to null.
|
||
var userTemplate = attr['$mdUserTemplate'];
|
||
attr['$mdUserTemplate'] = null;
|
||
|
||
var chipTemplate = getTemplateByQuery('md-chips>md-chip-template');
|
||
|
||
var chipRemoveSelector = $mdUtil
|
||
.prefixer()
|
||
.buildList('md-chip-remove')
|
||
.map(function(attr) {
|
||
return 'md-chips>*[' + attr + ']';
|
||
})
|
||
.join(',');
|
||
|
||
// Set the chip remove, chip contents and chip input templates. The link function will put
|
||
// them on the scope for transclusion later.
|
||
var chipRemoveTemplate = getTemplateByQuery(chipRemoveSelector) || templates.remove,
|
||
chipContentsTemplate = chipTemplate || templates.default,
|
||
chipInputTemplate = getTemplateByQuery('md-chips>md-autocomplete')
|
||
|| getTemplateByQuery('md-chips>input')
|
||
|| templates.input,
|
||
staticChips = userTemplate.find('md-chip');
|
||
|
||
// Warn of malformed template. See #2545
|
||
if (userTemplate[0].querySelector('md-chip-template>*[md-chip-remove]')) {
|
||
$log.warn('invalid placement of md-chip-remove within md-chip-template.');
|
||
}
|
||
|
||
function getTemplateByQuery (query) {
|
||
if (!attr.ngModel) return;
|
||
var element = userTemplate[0].querySelector(query);
|
||
return element && element.outerHTML;
|
||
}
|
||
|
||
/**
|
||
* Configures controller and transcludes.
|
||
*/
|
||
return function postLink(scope, element, attrs, controllers) {
|
||
$mdUtil.initOptionalProperties(scope, attr);
|
||
|
||
$mdTheming(element);
|
||
var mdChipsCtrl = controllers[0];
|
||
if (chipTemplate) {
|
||
// Chip editing functionality assumes we are using the default chip template.
|
||
mdChipsCtrl.enableChipEdit = false;
|
||
}
|
||
|
||
mdChipsCtrl.chipContentsTemplate = chipContentsTemplate;
|
||
mdChipsCtrl.chipRemoveTemplate = chipRemoveTemplate;
|
||
mdChipsCtrl.chipInputTemplate = chipInputTemplate;
|
||
|
||
mdChipsCtrl.mdCloseIcon = $$mdSvgRegistry.mdCancel;
|
||
|
||
element
|
||
.attr({ tabindex: -1 })
|
||
.on('focus', function () { mdChipsCtrl.onFocus(); })
|
||
.on('click', function () {
|
||
if (!mdChipsCtrl.readonly && mdChipsCtrl.selectedChip === -1) {
|
||
mdChipsCtrl.onFocus();
|
||
}
|
||
});
|
||
|
||
if (attr.ngModel) {
|
||
mdChipsCtrl.configureNgModel(element.controller('ngModel'));
|
||
|
||
// If an `md-transform-chip` attribute was set, tell the controller to use the expression
|
||
// before appending chips.
|
||
if (attrs.mdTransformChip) mdChipsCtrl.useTransformChipExpression();
|
||
|
||
// If an `md-on-add` attribute was set, tell the controller to use the expression
|
||
// when adding chips.
|
||
if (attrs.mdOnAdd) mdChipsCtrl.useOnAddExpression();
|
||
|
||
// If an `md-on-remove` attribute was set, tell the controller to use the expression
|
||
// when removing chips.
|
||
if (attrs.mdOnRemove) mdChipsCtrl.useOnRemoveExpression();
|
||
|
||
// If an `md-on-select` attribute was set, tell the controller to use the expression
|
||
// when selecting chips.
|
||
if (attrs.mdOnSelect) mdChipsCtrl.useOnSelectExpression();
|
||
|
||
// The md-autocomplete and input elements won't be compiled until after this directive
|
||
// is complete (due to their nested nature). Wait a tick before looking for them to
|
||
// configure the controller.
|
||
if (chipInputTemplate !== templates.input) {
|
||
// The autocomplete will not appear until the readonly attribute is not true (i.e.
|
||
// false or undefined), so we have to watch the readonly and then on the next tick
|
||
// after the chip transclusion has run, we can configure the autocomplete and user
|
||
// input.
|
||
scope.$watch('$mdChipsCtrl.readonly', function(readonly) {
|
||
if (!readonly) {
|
||
|
||
$mdUtil.nextTick(function(){
|
||
|
||
if (chipInputTemplate.indexOf('<md-autocomplete') === 0) {
|
||
var autocompleteEl = element.find('md-autocomplete');
|
||
mdChipsCtrl.configureAutocomplete(autocompleteEl.controller('mdAutocomplete'));
|
||
}
|
||
|
||
mdChipsCtrl.configureUserInput(element.find('input'));
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
// At the next tick, if we find an input, make sure it has the md-input class
|
||
$mdUtil.nextTick(function() {
|
||
var input = element.find('input');
|
||
|
||
if (input) {
|
||
mdChipsCtrl.configureInput(input);
|
||
input.toggleClass('md-input', true);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Compile with the parent's scope and prepend any static chips to the wrapper.
|
||
if (staticChips.length > 0) {
|
||
var compiledStaticChips = $compile(staticChips.clone())(scope.$parent);
|
||
$timeout(function() { element.find('md-chips-wrap').prepend(compiledStaticChips); });
|
||
}
|
||
};
|
||
}
|
||
|
||
function getTemplates() {
|
||
return {
|
||
chips: $mdUtil.processTemplate(MD_CHIPS_TEMPLATE),
|
||
input: $mdUtil.processTemplate(CHIP_INPUT_TEMPLATE),
|
||
default: $mdUtil.processTemplate(CHIP_DEFAULT_TEMPLATE),
|
||
remove: $mdUtil.processTemplate(CHIP_REMOVE_TEMPLATE)
|
||
};
|
||
}
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
|
||
MdContactChipsCtrl.$inject = ["$attrs", "$element", "$timeout"];angular
|
||
.module('material.components.chips')
|
||
.controller('MdContactChipsCtrl', MdContactChipsCtrl);
|
||
|
||
/**
|
||
* Controller for the MdContactChips component
|
||
* @constructor
|
||
*/
|
||
function MdContactChipsCtrl ($attrs, $element, $timeout) {
|
||
/** @type {$element} */
|
||
this.$element = $element;
|
||
|
||
/** @type {$attrs} */
|
||
this.$attrs = $attrs;
|
||
|
||
/** @type {Function} */
|
||
this.$timeout = $timeout;
|
||
|
||
/** @type {Object} */
|
||
this.selectedItem = null;
|
||
|
||
/** @type {string} */
|
||
this.searchText = '';
|
||
|
||
/**
|
||
* Collection of functions to call to un-register watchers
|
||
* @type {Array}
|
||
*/
|
||
this.deRegister = [];
|
||
|
||
this.init();
|
||
}
|
||
|
||
MdContactChipsCtrl.prototype.init = function() {
|
||
var ctrl = this;
|
||
var deRegister = this.deRegister;
|
||
var element = this.$element;
|
||
|
||
// Setup a watcher which manages chips a11y messages and autocomplete aria.
|
||
// Timeout required to allow the child elements to be compiled.
|
||
this.$timeout(function() {
|
||
deRegister.push(
|
||
element.find('md-chips').controller('mdChips').$scope.$watchCollection('$mdChipsCtrl.items', function() {
|
||
// Make sure our input and wrapper have the correct ARIA attributes
|
||
ctrl.setupChipsAria();
|
||
ctrl.setupAutocompleteAria();
|
||
})
|
||
);
|
||
});
|
||
};
|
||
|
||
MdContactChipsCtrl.prototype.setupChipsAria = function() {
|
||
var chips = this.$element.find('md-chips');
|
||
var chipsCtrl = chips.controller('mdChips');
|
||
|
||
// Configure MdChipsCtrl
|
||
if (this.removedMessage) {
|
||
chipsCtrl.removedMessage = this.removedMessage;
|
||
}
|
||
if (this.containerHint) {
|
||
chipsCtrl.containerHint = this.containerHint;
|
||
}
|
||
if (this.containerEmptyHint) {
|
||
// Apply attribute to avoid the hint being overridden by MdChipsCtrl.configureAutocomplete()
|
||
chips.attr('container-empty-hint', this.containerEmptyHint);
|
||
chipsCtrl.containerEmptyHint = this.containerEmptyHint;
|
||
}
|
||
if (this.deleteHint) {
|
||
chipsCtrl.deleteHint = this.deleteHint;
|
||
}
|
||
if (this.inputAriaLabel) {
|
||
chipsCtrl.inputAriaLabel = this.inputAriaLabel;
|
||
}
|
||
if (this.inputClass) {
|
||
chipsCtrl.inputClass = this.inputClass;
|
||
}
|
||
};
|
||
|
||
MdContactChipsCtrl.prototype.setupAutocompleteAria = function() {
|
||
var autocompleteInput = this.$element.find('md-chips-wrap').find('md-autocomplete').find('input');
|
||
|
||
// Set attributes on the input of the md-autocomplete
|
||
if (this.inputAriaDescribedBy) {
|
||
autocompleteInput.attr('aria-describedby', this.inputAriaDescribedBy);
|
||
}
|
||
if (this.inputAriaLabelledBy) {
|
||
autocompleteInput.removeAttr('aria-label');
|
||
autocompleteInput.attr('aria-labelledby', this.inputAriaLabelledBy);
|
||
}
|
||
};
|
||
|
||
MdContactChipsCtrl.prototype.queryContact = function(searchText) {
|
||
return this.contactQuery({'$query': searchText});
|
||
};
|
||
|
||
MdContactChipsCtrl.prototype.inputKeydown = function(event) {
|
||
if (!this.separatorKeys || this.separatorKeys.indexOf(event.keyCode) < 0) {
|
||
return;
|
||
}
|
||
|
||
event.stopPropagation();
|
||
event.preventDefault();
|
||
|
||
var autocompleteCtrl = angular.element(event.target).controller('mdAutocomplete');
|
||
autocompleteCtrl.select(autocompleteCtrl.index);
|
||
};
|
||
|
||
MdContactChipsCtrl.prototype.itemName = function(item) {
|
||
return item[this.contactName];
|
||
};
|
||
|
||
/**
|
||
* Destructor for cleanup
|
||
*/
|
||
MdContactChipsCtrl.prototype.$onDestroy = function $onDestroy() {
|
||
var $destroyFn;
|
||
while (($destroyFn = this.deRegister.pop())) {
|
||
$destroyFn.call(this);
|
||
}
|
||
};
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
|
||
MdContactChips.$inject = ["$mdTheming", "$mdUtil"];angular
|
||
.module('material.components.chips')
|
||
.directive('mdContactChips', MdContactChips);
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdContactChips
|
||
* @module material.components.chips
|
||
*
|
||
* @description
|
||
* `<md-contact-chips>` is an input component based on `md-chips` and makes use of an
|
||
* `md-autocomplete` element. The component allows the caller to supply a query expression which
|
||
* returns a list of possible contacts. The user can select one of these and add it to the list of
|
||
* chips.
|
||
*
|
||
* You may also use the <a ng-href="api/directive/mdHighlightText">md-highlight-flags</a> attribute
|
||
* along with its parameters to control the appearance of the matched text inside of the contacts'
|
||
* autocomplete popup.
|
||
*
|
||
* @param {expression} ng-model Assignable AngularJS expression to be data-bound to the list of
|
||
* contact chips. The expression should evaluate to an `Object` Array.
|
||
* @param {expression=} ng-change AngularJS expression to be executed on chip addition, removal,
|
||
* or content change.
|
||
* @param {string=} placeholder Placeholder text that will be forwarded to the input.
|
||
* @param {string=} secondary-placeholder Placeholder text that will be forwarded to the input,
|
||
* displayed when there is at least on item in the list
|
||
* @param {expression} md-contacts An expression expected to return contacts matching the search
|
||
* test, `$query`. If this expression involves a promise, a loading bar is displayed while
|
||
* waiting for it to resolve.
|
||
* @param {string} md-contact-name The field name of the contact object representing the
|
||
* contact's name.
|
||
* @param {string} md-contact-email The field name of the contact object representing the
|
||
* contact's email address.
|
||
* @param {string} md-contact-image The field name of the contact object representing the
|
||
* contact's image.
|
||
* @param {number=} md-max-chips The maximum number of chips allowed to add through user input.
|
||
* <br/><br/>The validation property `md-max-chips` can be used when the max chips
|
||
* amount is reached.
|
||
* @param {number=} md-min-length Specifies the minimum length of text before autocomplete will
|
||
* make suggestions
|
||
* @param {string=} md-input-class This class will be applied to the child `md-autocomplete` for
|
||
* custom styling.
|
||
* @param {string=} input-aria-describedby A space-separated list of element IDs. This should
|
||
* contain the IDs of any elements that describe this autocomplete. Screen readers will read
|
||
* the content of these elements at the end of announcing that the chips input has been
|
||
* selected and describing its current state. The descriptive elements do not need to be
|
||
* visible on the page.
|
||
* @param {string=} input-aria-labelledby A space-separated list of element IDs. The ideal use
|
||
* case is that this would contain the ID of a `<label>` element that is associated with these
|
||
* chips.<br><br>
|
||
* For `<label id="state">US State</label>`, you would set this to
|
||
* `input-aria-labelledby="state"`.
|
||
* @param {string=} input-aria-label A string read by screen readers to identify the input.
|
||
* For static chips, this will be applied to the chips container.
|
||
* @param {string=} container-hint A string read by screen readers informing users of how to
|
||
* navigate the chips when there are chips.
|
||
* @param {string=} container-empty-hint A string read by screen readers informing users of how to
|
||
* add chips when there are no chips. You will want to use this to override the default when
|
||
* in a non-English locale.
|
||
* @param {string=} delete-hint A string read by screen readers instructing users that pressing
|
||
* the delete key will remove the chip. You will want to use this to override the default when
|
||
* in a non-English locale.
|
||
* @param {string=} md-removed-message Screen readers will announce this message following the
|
||
* chips contents. The default is `"removed"`. If a chip with the content of "Apple" was
|
||
* removed, the screen reader would read "Apple removed". You will want to use this to override
|
||
* the default when in a non-English locale.
|
||
*
|
||
*
|
||
* @usage
|
||
* <hljs lang="html">
|
||
* <md-contact-chips
|
||
* ng-model="ctrl.contacts"
|
||
* md-contacts="ctrl.querySearch($query)"
|
||
* md-contact-name="name"
|
||
* md-contact-image="image"
|
||
* md-contact-email="email"
|
||
* placeholder="To">
|
||
* </md-contact-chips>
|
||
* </hljs>
|
||
*
|
||
*/
|
||
|
||
|
||
var MD_CONTACT_CHIPS_TEMPLATE = '\
|
||
<md-chips class="md-contact-chips"\
|
||
ng-model="$mdContactChipsCtrl.contacts"\
|
||
ng-change="$mdContactChipsCtrl.ngChange($mdContactChipsCtrl.contacts)"\
|
||
md-require-match="$mdContactChipsCtrl.requireMatch"\
|
||
md-max-chips="{{$mdContactChipsCtrl.maxChips}}"\
|
||
md-chip-append-delay="{{$mdContactChipsCtrl.chipAppendDelay}}"\
|
||
md-separator-keys="$mdContactChipsCtrl.separatorKeys"\
|
||
md-autocomplete-snap>\
|
||
<md-autocomplete\
|
||
md-menu-class="md-contact-chips-suggestions"\
|
||
md-selected-item="$mdContactChipsCtrl.selectedItem"\
|
||
md-search-text="$mdContactChipsCtrl.searchText"\
|
||
md-items="item in $mdContactChipsCtrl.queryContact($mdContactChipsCtrl.searchText)"\
|
||
md-item-text="$mdContactChipsCtrl.itemName(item)"\
|
||
md-no-cache="true"\
|
||
md-min-length="$mdContactChipsCtrl.minLength"\
|
||
md-autoselect\
|
||
ng-attr-md-input-class="{{$mdContactChipsCtrl.inputClass}}"\
|
||
ng-keydown="$mdContactChipsCtrl.inputKeydown($event)"\
|
||
placeholder="{{$mdContactChipsCtrl.contacts.length === 0 ?\
|
||
$mdContactChipsCtrl.placeholder : $mdContactChipsCtrl.secondaryPlaceholder}}">\
|
||
<div class="md-contact-suggestion">\
|
||
<img \
|
||
ng-src="{{item[$mdContactChipsCtrl.contactImage]}}"\
|
||
alt="{{item[$mdContactChipsCtrl.contactName]}}"\
|
||
ng-if="item[$mdContactChipsCtrl.contactImage]" />\
|
||
<span class="md-contact-name" md-highlight-text="$mdContactChipsCtrl.searchText"\
|
||
md-highlight-flags="{{$mdContactChipsCtrl.highlightFlags}}">\
|
||
{{item[$mdContactChipsCtrl.contactName]}}\
|
||
</span>\
|
||
<span class="md-contact-email" >{{item[$mdContactChipsCtrl.contactEmail]}}</span>\
|
||
</div>\
|
||
</md-autocomplete>\
|
||
<md-chip-template>\
|
||
<div class="md-contact-avatar">\
|
||
<img \
|
||
ng-src="{{$chip[$mdContactChipsCtrl.contactImage]}}"\
|
||
alt="{{$chip[$mdContactChipsCtrl.contactName]}}"\
|
||
ng-if="$chip[$mdContactChipsCtrl.contactImage]" />\
|
||
</div>\
|
||
<div class="md-contact-name">\
|
||
{{$chip[$mdContactChipsCtrl.contactName]}}\
|
||
</div>\
|
||
</md-chip-template>\
|
||
</md-chips>';
|
||
|
||
|
||
/**
|
||
* MDContactChips Directive Definition
|
||
*
|
||
* @param $mdTheming
|
||
* @param $mdUtil
|
||
* @returns {*}
|
||
* @ngInject
|
||
*/
|
||
function MdContactChips($mdTheming, $mdUtil) {
|
||
return {
|
||
template: function(element, attrs) {
|
||
return MD_CONTACT_CHIPS_TEMPLATE;
|
||
},
|
||
restrict: 'E',
|
||
controller: 'MdContactChipsCtrl',
|
||
controllerAs: '$mdContactChipsCtrl',
|
||
bindToController: true,
|
||
compile: compile,
|
||
scope: {
|
||
contactQuery: '&mdContacts',
|
||
placeholder: '@?',
|
||
secondaryPlaceholder: '@?',
|
||
contactName: '@mdContactName',
|
||
contactImage: '@mdContactImage',
|
||
contactEmail: '@mdContactEmail',
|
||
contacts: '=ngModel',
|
||
ngChange: '&?',
|
||
requireMatch: '=?mdRequireMatch',
|
||
minLength: '=?mdMinLength',
|
||
maxChips: '=?mdMaxChips',
|
||
highlightFlags: '@?mdHighlightFlags',
|
||
chipAppendDelay: '@?mdChipAppendDelay',
|
||
separatorKeys: '=?mdSeparatorKeys',
|
||
removedMessage: '@?mdRemovedMessage',
|
||
inputClass: '@?mdInputClass',
|
||
inputAriaDescribedBy: '@?inputAriaDescribedby',
|
||
inputAriaLabelledBy: '@?inputAriaLabelledby',
|
||
inputAriaLabel: '@?',
|
||
containerHint: '@?',
|
||
containerEmptyHint: '@?',
|
||
deleteHint: '@?'
|
||
}
|
||
};
|
||
|
||
function compile(element, attr) {
|
||
return function postLink(scope, element, attrs, controllers) {
|
||
var contactChipsController = controllers;
|
||
|
||
$mdUtil.initOptionalProperties(scope, attr);
|
||
$mdTheming(element);
|
||
|
||
element.attr('tabindex', '-1');
|
||
|
||
attrs.$observe('mdChipAppendDelay', function(newValue) {
|
||
contactChipsController.chipAppendDelay = newValue;
|
||
});
|
||
};
|
||
}
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
(function () {
|
||
"use strict";
|
||
|
||
/**
|
||
* Use a RegExp to check if the `md-colors="<expression>"` is static string
|
||
* or one that should be observed and dynamically interpolated.
|
||
*/
|
||
MdColorsDirective.$inject = ["$mdColors", "$mdUtil", "$log", "$parse"];
|
||
MdColorsService.$inject = ["$mdTheming", "$mdUtil", "$log"];
|
||
var STATIC_COLOR_EXPRESSION = /^{((\s|,)*?["'a-zA-Z-]+?\s*?:\s*?(['"])[a-zA-Z0-9-.]*(['"]))+\s*}$/;
|
||
var colorPalettes = null;
|
||
|
||
/**
|
||
* @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);
|
||
|
||
/**
|
||
* @ngdoc service
|
||
* @name $mdColors
|
||
* @module material.components.colors
|
||
*
|
||
* @description
|
||
* By default, defining a theme does not make its colors available for applying to non AngularJS
|
||
* Material elements. The `$mdColors` service is used by the `md-color` directive to convert a
|
||
* set of color expressions to RGBA values and then apply those values to the element as CSS
|
||
* property values.
|
||
*
|
||
* @usage
|
||
* Getting a color based on a theme
|
||
*
|
||
* <hljs lang="js">
|
||
* angular.controller('myCtrl', function ($mdColors) {
|
||
* var color = $mdColors.getThemeColor('myTheme-primary-900-0.5');
|
||
* ...
|
||
* });
|
||
* </hljs>
|
||
*
|
||
* Applying a color from a palette to an element
|
||
* <hljs lang="js">
|
||
* app.directive('myDirective', function($mdColors) {
|
||
* return {
|
||
* ...
|
||
* link: function (scope, elem) {
|
||
* $mdColors.applyThemeColors(elem, {color: 'red-A200-0.2'});
|
||
* }
|
||
* }
|
||
* });
|
||
* </hljs>
|
||
*/
|
||
function MdColorsService($mdTheming, $mdUtil, $log) {
|
||
colorPalettes = colorPalettes || Object.keys($mdTheming.PALETTES);
|
||
|
||
// Publish service instance
|
||
return {
|
||
applyThemeColors: applyThemeColors,
|
||
getThemeColor: getThemeColor,
|
||
hasTheme: hasTheme
|
||
};
|
||
|
||
// ********************************************
|
||
// Internal Methods
|
||
// ********************************************
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdColors#applyThemeColors
|
||
*
|
||
* @description
|
||
* Lookup a set of colors by hue, theme, and palette, then apply those colors
|
||
* with the provided opacity (via `rgba()`) to the specified CSS property.
|
||
*
|
||
* @param {angular.element} element the element to apply the styles to
|
||
* @param {Object} colorExpression Keys are CSS properties and values are strings representing
|
||
* the `theme-palette-hue-opacity` of the desired color. For example:
|
||
* `{'color': 'red-A200-0.3', 'background-color': 'myTheme-primary-700-0.8'}`. Theme, hue, and
|
||
* opacity are optional.
|
||
*/
|
||
function applyThemeColors(element, colorExpression) {
|
||
try {
|
||
if (colorExpression) {
|
||
// Assign the calculate RGBA color values directly as inline CSS
|
||
element.css(interpolateColors(colorExpression));
|
||
}
|
||
} catch (e) {
|
||
$log.error(e.message);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdColors#getThemeColor
|
||
*
|
||
* @description
|
||
* Get a parsed RGBA color using a string representing the `theme-palette-hue-opacity` of the
|
||
* desired color.
|
||
*
|
||
* @param {string} expression color expression like `'red-A200-0.3'` or
|
||
* `'myTheme-primary-700-0.8'`. Theme, hue, and opacity are optional.
|
||
* @returns {string} a CSS color value like `rgba(211, 47, 47, 0.8)`
|
||
*/
|
||
function getThemeColor(expression) {
|
||
var color = extractColorOptions(expression);
|
||
|
||
return parseColor(color);
|
||
}
|
||
|
||
/**
|
||
* Return the parsed color
|
||
* @param {{hue: *, theme: any, palette: *, opacity: (*|string|number)}} color hash map of color
|
||
* definitions
|
||
* @param {boolean=} contrast whether use contrast color for foreground. Defaults to false.
|
||
* @returns {string} rgba color string
|
||
*/
|
||
function parseColor(color, contrast) {
|
||
contrast = contrast || false;
|
||
var rgbValues = $mdTheming.PALETTES[color.palette][color.hue];
|
||
|
||
rgbValues = contrast ? rgbValues.contrast : rgbValues.value;
|
||
|
||
return $mdUtil.supplant('rgba({0}, {1}, {2}, {3})',
|
||
[rgbValues[0], rgbValues[1], rgbValues[2], rgbValues[3] || color.opacity]
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Convert the color expression into an object with scope-interpolated values
|
||
* Then calculate the rgba() values based on the theme color parts
|
||
* @param {Object} themeColors json object, keys are css properties and values are string of
|
||
* the wanted color, for example: `{color: 'red-A200-0.3'}`.
|
||
* @return {Object} Hashmap of CSS properties with associated `rgba()` string values
|
||
*/
|
||
function interpolateColors(themeColors) {
|
||
var rgbColors = {};
|
||
|
||
var hasColorProperty = themeColors.hasOwnProperty('color');
|
||
|
||
angular.forEach(themeColors, function (value, key) {
|
||
var color = extractColorOptions(value);
|
||
var hasBackground = key.indexOf('background') > -1;
|
||
|
||
rgbColors[key] = parseColor(color);
|
||
if (hasBackground && !hasColorProperty) {
|
||
rgbColors.color = parseColor(color, true);
|
||
}
|
||
});
|
||
|
||
return rgbColors;
|
||
}
|
||
|
||
/**
|
||
* Check if expression has defined theme
|
||
* For instance:
|
||
* 'myTheme-primary' => true
|
||
* 'red-800' => false
|
||
* @param {string} expression color expression like 'red-800', 'red-A200-0.3',
|
||
* 'myTheme-primary', or 'myTheme-primary-400'
|
||
* @return {boolean} true if the expression has a theme part, false otherwise.
|
||
*/
|
||
function hasTheme(expression) {
|
||
return angular.isDefined($mdTheming.THEMES[expression.split('-')[0]]);
|
||
}
|
||
|
||
/**
|
||
* For the evaluated expression, extract the color parts into a hash map
|
||
* @param {string} expression color expression like 'red-800', 'red-A200-0.3',
|
||
* 'myTheme-primary', or 'myTheme-primary-400'
|
||
* @returns {{hue: *, theme: any, palette: *, opacity: (*|string|number)}}
|
||
*/
|
||
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
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Calculate the theme palette name
|
||
* @param {Array} parts
|
||
* @param {string} theme name
|
||
* @return {string}
|
||
*/
|
||
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
|
||
|
||
var isTwoWord = parts.length > 1 && colorPalettes.indexOf(parts[1]) !== -1;
|
||
var palette = parts[0].replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
||
|
||
if (isTwoWord) palette = parts[0] + '-' + parts.splice(1, 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;
|
||
}
|
||
|
||
/**
|
||
* @param {Array} parts
|
||
* @param {string} theme name
|
||
* @return {*}
|
||
*/
|
||
function extractHue(parts, theme) {
|
||
var themeColors = $mdTheming.THEMES[theme].colors;
|
||
|
||
if (parts[1] === 'hue') {
|
||
var hueNumber = parseInt(parts.splice(2, 1)[0], 10);
|
||
|
||
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]
|
||
}));
|
||
}
|
||
|
||
return themeColors[parts[0]].hues[parts[1]];
|
||
}
|
||
|
||
return parts[1] || themeColors[parts[0] in themeColors ? parts[0] : 'primary'].hues['default'];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @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 the colors defined in the Sass 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
|
||
* <hljs lang="html">
|
||
* <div md-colors="{background: 'myTheme-accent-900-0.43'}">
|
||
* <div md-colors="{color: 'red-A100', 'border-color': 'primary-600'}">
|
||
* <span>Color demo</span>
|
||
* </div>
|
||
* </div>
|
||
* </hljs>
|
||
*
|
||
* The `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.
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-card md-colors="::{background: '{{theme}}-primary-700'}">
|
||
* </md-card>
|
||
* </hljs>
|
||
*/
|
||
function MdColorsDirective($mdColors, $mdUtil, $log, $parse) {
|
||
return {
|
||
restrict: 'A',
|
||
require: ['^?mdTheme'],
|
||
compile: function (tElem, tAttrs) {
|
||
var shouldWatch = shouldColorsWatch();
|
||
|
||
return function (scope, element, attrs, ctrl) {
|
||
var mdThemeController = ctrl[0];
|
||
|
||
var lastColors = {};
|
||
|
||
/**
|
||
* @param {string=} theme
|
||
* @return {Object} colors found in the specified theme
|
||
*/
|
||
var parseColors = function (theme) {
|
||
if (typeof theme !== 'string') {
|
||
theme = '';
|
||
}
|
||
|
||
if (!attrs.mdColors) {
|
||
attrs.mdColors = '{}';
|
||
}
|
||
|
||
/**
|
||
* 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);
|
||
|
||
/**
|
||
* If mdTheme is defined higher up the DOM tree,
|
||
* we add mdTheme's theme to the colors which don't specify a theme.
|
||
*
|
||
* @example
|
||
* <hljs lang="html">
|
||
* <div md-theme="myTheme">
|
||
* <div md-colors="{background: 'primary-600'}">
|
||
* <span md-colors="{background: 'mySecondTheme-accent-200'}">Color demo</span>
|
||
* </div>
|
||
* </div>
|
||
* </hljs>
|
||
*
|
||
* 'primary-600' will be changed to 'myTheme-primary-600',
|
||
* but 'mySecondTheme-accent-200' will not be changed since it has a theme defined.
|
||
*/
|
||
if (mdThemeController) {
|
||
Object.keys(colors).forEach(function (prop) {
|
||
var color = colors[prop];
|
||
if (!$mdColors.hasTheme(color)) {
|
||
colors[prop] = (theme || mdThemeController.$mdTheme) + '-' + color;
|
||
}
|
||
});
|
||
}
|
||
|
||
cleanElement(colors);
|
||
|
||
return colors;
|
||
};
|
||
|
||
/**
|
||
* @param {Object} colors
|
||
*/
|
||
var cleanElement = function (colors) {
|
||
if (!angular.equals(colors, lastColors)) {
|
||
var keys = Object.keys(lastColors);
|
||
|
||
if (lastColors.background && !keys.color) {
|
||
keys.push('color');
|
||
}
|
||
|
||
keys.forEach(function (key) {
|
||
element.css(key, '');
|
||
});
|
||
}
|
||
|
||
lastColors = colors;
|
||
};
|
||
|
||
/**
|
||
* Registering for mgTheme changes and asking mdTheme controller run our callback whenever
|
||
* a theme changes.
|
||
*/
|
||
var unregisterChanges = angular.noop;
|
||
|
||
if (mdThemeController) {
|
||
unregisterChanges = mdThemeController.registerChanges(function (theme) {
|
||
$mdColors.applyThemeColors(element, parseColors(theme));
|
||
});
|
||
}
|
||
|
||
scope.$on('$destroy', function () {
|
||
unregisterChanges();
|
||
});
|
||
|
||
try {
|
||
if (shouldWatch) {
|
||
scope.$watch(parseColors, angular.bind(this,
|
||
$mdColors.applyThemeColors, element
|
||
), true);
|
||
}
|
||
else {
|
||
$mdColors.applyThemeColors(element, parseColors());
|
||
}
|
||
|
||
}
|
||
catch (e) {
|
||
$log.error(e.message);
|
||
}
|
||
|
||
};
|
||
|
||
/**
|
||
* @return {boolean}
|
||
*/
|
||
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);
|
||
|
||
// Remove it for the postLink...
|
||
tAttrs.mdColors = rawColorExpression.replace('::', '');
|
||
|
||
var hasWatchAttr = angular.isDefined(tAttrs.mdColorsWatch);
|
||
|
||
return (bindOnce || isStatic) ? false :
|
||
hasWatchAttr ? $mdUtil.parseAttributeBoolean(tAttrs.mdColorsWatch) : true;
|
||
}
|
||
}
|
||
};
|
||
}
|
||
})();
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.content
|
||
*
|
||
* @description
|
||
* Scrollable content
|
||
*/
|
||
mdContentDirective.$inject = ["$mdTheming"];
|
||
angular.module('material.components.content', [
|
||
'material.core'
|
||
])
|
||
.directive('mdContent', mdContentDirective);
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdContent
|
||
* @module material.components.content
|
||
*
|
||
* @restrict E
|
||
*
|
||
* @description
|
||
*
|
||
* The `<md-content>` 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, `<md-content>` 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.
|
||
*
|
||
* Additionally, we now also offer the `md-no-flicker` class which can be applied to any element
|
||
* and uses a Webkit-specific filter of `blur(0px)` that forces GPU rendering of all elements
|
||
* inside (which eliminates the flicker on iOS devices).
|
||
*
|
||
* _<b>Note:</b> Forcing an element to render on the GPU can have unintended side-effects, especially
|
||
* related to the z-index of elements. Please use with caution and only on the elements needed._
|
||
*
|
||
* @usage
|
||
*
|
||
* Add the `[layout-padding]` attribute to make the content padded.
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-content layout-padding>
|
||
* Lorem ipsum dolor sit amet, ne quod novum mei.
|
||
* </md-content>
|
||
* </hljs>
|
||
*/
|
||
|
||
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]);
|
||
}
|
||
};
|
||
|
||
function ContentController($scope, $element) {
|
||
this.$scope = $scope;
|
||
this.$element = $element;
|
||
}
|
||
}
|
||
|
||
function iosScrollFix(node) {
|
||
// IOS FIX:
|
||
// If we scroll where there is no more room for the webview to scroll,
|
||
// by default the webview itself will scroll up and down, this looks really
|
||
// bad. So if we are scrolling to the very top or bottom, add/subtract one
|
||
angular.element(node).on('$md.pressdown', function(ev) {
|
||
// Only touch events
|
||
if (ev.pointer.type !== 't') return;
|
||
// Don't let a child content's touchstart ruin it for us.
|
||
if (ev.$materialScrollFixed) return;
|
||
ev.$materialScrollFixed = true;
|
||
|
||
if (node.scrollTop === 0) {
|
||
node.scrollTop = 1;
|
||
} else if (node.scrollHeight === node.scrollTop + node.offsetHeight) {
|
||
node.scrollTop -= 1;
|
||
}
|
||
});
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.datepicker
|
||
* @description Module for the datepicker component.
|
||
*/
|
||
|
||
angular.module('material.components.datepicker', [
|
||
'material.core',
|
||
'material.components.icon',
|
||
'material.components.virtualRepeat'
|
||
]);
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
(function() {
|
||
'use strict';
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdCalendar
|
||
* @module material.components.datepicker
|
||
*
|
||
* @param {Date} ng-model The component's model. Should be a Date object.
|
||
* @param {Object=} ng-model-options Allows tuning of the way in which `ng-model` is being
|
||
* updated. Also allows for a timezone to be specified.
|
||
* <a href="https://docs.angularjs.org/api/ng/directive/ngModelOptions#usage">Read more at the
|
||
* ngModelOptions docs.</a>
|
||
* @param {Date=} md-min-date Expression representing the minimum date.
|
||
* @param {Date=} md-max-date Expression representing the maximum date.
|
||
* @param {(function(Date): boolean)=} md-date-filter Function expecting a date and returning a
|
||
* boolean whether it can be selected in "day" mode or not.
|
||
* @param {(function(Date): boolean)=} md-month-filter Function expecting a date and returning a
|
||
* boolean whether it can be selected in "month" mode or not.
|
||
* @param {String=} md-current-view Current view of the calendar. Can be either "month" or "year".
|
||
* @param {String=} md-mode Restricts the user to only selecting a value from a particular view.
|
||
* This option can be used if the user is only supposed to choose from a certain date type
|
||
* (e.g. only selecting the month). Can be either "month" or "day". **Note** that this will
|
||
* overwrite the `md-current-view` value.
|
||
*
|
||
* @description
|
||
* `<md-calendar>` is a component that renders a calendar that can be used to select a date.
|
||
* It is a part of the `<md-datepicker>` pane, however it can also be used on it's own.
|
||
*
|
||
* @usage
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-calendar ng-model="birthday"></md-calendar>
|
||
* </hljs>
|
||
*/
|
||
CalendarCtrl.$inject = ["$element", "$scope", "$$mdDateUtil", "$mdUtil", "$mdConstant", "$mdTheming", "$$rAF", "$attrs", "$mdDateLocale", "$filter", "$document"];
|
||
calendarDirective.$inject = ["inputDirective"];
|
||
angular.module('material.components.datepicker')
|
||
.directive('mdCalendar', calendarDirective);
|
||
|
||
// 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).
|
||
// TODO Read-only calendar (not just date-picker).
|
||
|
||
function calendarDirective(inputDirective) {
|
||
return {
|
||
template: function(tElement, tAttr) {
|
||
// This allows the calendar to work, without a datepicker. 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"';
|
||
return '' +
|
||
'<div ng-switch="calendarCtrl.currentView" ' + extraAttrs + '>' +
|
||
'<md-calendar-year ng-switch-when="year"></md-calendar-year>' +
|
||
'<md-calendar-month ng-switch-default></md-calendar-month>' +
|
||
'</div>';
|
||
},
|
||
scope: {
|
||
minDate: '=mdMinDate',
|
||
maxDate: '=mdMaxDate',
|
||
dateFilter: '=mdDateFilter',
|
||
monthFilter: '=mdMonthFilter',
|
||
|
||
// These need to be prefixed, because Angular resets
|
||
// any changes to the value due to bindToController.
|
||
_mode: '@mdMode',
|
||
_currentView: '@mdCurrentView'
|
||
},
|
||
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, inputDirective);
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 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.
|
||
*/
|
||
var FALLBACK_WIDTH = 340;
|
||
|
||
/** Next identifier for calendar instance. */
|
||
var nextUniqueId = 0;
|
||
|
||
/** Maps the `md-mode` values to their corresponding calendar views. */
|
||
var MODE_MAP = {
|
||
day: 'month',
|
||
month: 'year'
|
||
};
|
||
|
||
/**
|
||
* Controller for the mdCalendar component.
|
||
* @ngInject @constructor
|
||
*/
|
||
function CalendarCtrl($element, $scope, $$mdDateUtil, $mdUtil, $mdConstant, $mdTheming, $$rAF,
|
||
$attrs, $mdDateLocale, $filter, $document) {
|
||
$mdTheming($element);
|
||
|
||
/**
|
||
* @final
|
||
* @type {!JQLite}
|
||
*/
|
||
this.$element = $element;
|
||
|
||
/**
|
||
* @final
|
||
* @type {!angular.Scope}
|
||
*/
|
||
this.$scope = $scope;
|
||
|
||
/**
|
||
* @final
|
||
* @type {!angular.$attrs} Current attributes object for the element
|
||
*/
|
||
this.$attrs = $attrs;
|
||
|
||
/** @final */
|
||
this.dateUtil = $$mdDateUtil;
|
||
|
||
/** @final */
|
||
this.$mdUtil = $mdUtil;
|
||
|
||
/** @final */
|
||
this.keyCode = $mdConstant.KEY_CODE;
|
||
|
||
/** @final */
|
||
this.$$rAF = $$rAF;
|
||
|
||
/** @final */
|
||
this.$mdDateLocale = $mdDateLocale;
|
||
|
||
/** @final The built-in Angular date filter. */
|
||
this.ngDateFilter = $filter('date');
|
||
|
||
/**
|
||
* @final
|
||
* @type {Date}
|
||
*/
|
||
this.today = this.dateUtil.createDateAtMidnight();
|
||
|
||
/** @type {!ngModel.NgModelController} */
|
||
this.ngModelCtrl = undefined;
|
||
|
||
/** @type {string} Class applied to the selected date cell. */
|
||
this.SELECTED_DATE_CLASS = 'md-calendar-selected-date';
|
||
|
||
/** @type {string} Class applied to the cell for today. */
|
||
this.TODAY_CLASS = 'md-calendar-date-today';
|
||
|
||
/** @type {string} Class applied to the focused cell. */
|
||
this.FOCUSED_DATE_CLASS = 'md-focus';
|
||
|
||
/**
|
||
* @final
|
||
* @type {number} Unique ID for this calendar instance.
|
||
*/
|
||
this.id = nextUniqueId++;
|
||
|
||
/**
|
||
* 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 necessarily always have
|
||
* focus in the document (such as for cases when the user is scrolling the calendar).
|
||
* @type {Date}
|
||
*/
|
||
this.displayDate = null;
|
||
|
||
/**
|
||
* Allows restricting the calendar to only allow selecting a month or a day.
|
||
* @type {'month'|'day'|null}
|
||
*/
|
||
this.mode = 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}
|
||
*/
|
||
this.selectedDate = null;
|
||
|
||
/**
|
||
* The first date that can be rendered by the calendar. The default is taken
|
||
* from the mdDateLocale provider and is limited by the mdMinDate.
|
||
* @type {Date}
|
||
*/
|
||
this.firstRenderableDate = null;
|
||
|
||
/**
|
||
* The last date that can be rendered by the calendar. The default comes
|
||
* from the mdDateLocale provider and is limited by the maxDate.
|
||
* @type {Date}
|
||
*/
|
||
this.lastRenderableDate = null;
|
||
|
||
/**
|
||
* 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;
|
||
|
||
/**
|
||
* Caches the width of the scrollbar in order to be used when hiding it and to avoid extra reflows.
|
||
* @type {number}
|
||
*/
|
||
this.scrollbarWidth = 0;
|
||
|
||
/**
|
||
* @type {boolean} set to true if the calendar is being used "standalone" (outside of a
|
||
* md-datepicker).
|
||
*/
|
||
this.standaloneMode = false;
|
||
|
||
// Unless the user specifies so, the calendar should not be a tab stop.
|
||
// This is necessary because ngAria might add a tabindex to anything with an ng-model
|
||
// (based on whether or not the user has turned that particular feature on/off).
|
||
if (!$attrs.tabindex) {
|
||
$element.attr('tabindex', '-1');
|
||
}
|
||
|
||
var boundKeyHandler = angular.bind(this, this.handleKeyEvent);
|
||
|
||
// If use the md-calendar directly in the body without datepicker,
|
||
// handleKeyEvent will disable other inputs on the page.
|
||
// So only apply the handleKeyEvent on the body when the md-calendar inside datepicker,
|
||
// otherwise apply on the calendar element only.
|
||
|
||
var handleKeyElement;
|
||
if ($element.parent().hasClass('md-datepicker-calendar')) {
|
||
handleKeyElement = angular.element($document[0].body);
|
||
} else {
|
||
this.standaloneMode = true;
|
||
handleKeyElement = $element;
|
||
}
|
||
|
||
// Bind the keydown handler to the body, in order to handle cases where the focused
|
||
// element gets removed from the DOM and stops propagating click events.
|
||
handleKeyElement.on('keydown', boundKeyHandler);
|
||
|
||
$scope.$on('$destroy', function() {
|
||
handleKeyElement.off('keydown', boundKeyHandler);
|
||
});
|
||
|
||
// For AngularJS 1.4 and older, where there are no lifecycle hooks but bindings are pre-assigned,
|
||
// manually call the $onInit hook.
|
||
if (angular.version.major === 1 && angular.version.minor <= 4) {
|
||
this.$onInit();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* AngularJS Lifecycle hook for newer AngularJS versions.
|
||
* Bindings are not guaranteed to have been assigned in the controller, but they are in the
|
||
* $onInit hook.
|
||
*/
|
||
CalendarCtrl.prototype.$onInit = function() {
|
||
/**
|
||
* The currently visible calendar view. Note the prefix on the scope value,
|
||
* which is necessary, because the datepicker seems to reset the real one value if the
|
||
* calendar is open, but the `currentView` on the datepicker's scope is empty.
|
||
* @type {String}
|
||
*/
|
||
if (this._mode && MODE_MAP.hasOwnProperty(this._mode)) {
|
||
this.currentView = MODE_MAP[this._mode];
|
||
this.mode = this._mode;
|
||
} else {
|
||
this.currentView = this._currentView || 'month';
|
||
this.mode = null;
|
||
}
|
||
|
||
if (this.minDate && this.minDate > this.$mdDateLocale.firstRenderableDate) {
|
||
this.firstRenderableDate = this.minDate;
|
||
} else {
|
||
this.firstRenderableDate = this.$mdDateLocale.firstRenderableDate;
|
||
}
|
||
|
||
if (this.maxDate && this.maxDate < this.$mdDateLocale.lastRenderableDate) {
|
||
this.lastRenderableDate = this.maxDate;
|
||
} else {
|
||
this.lastRenderableDate = this.$mdDateLocale.lastRenderableDate;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Sets up the controller's reference to ngModelController.
|
||
* @param {!ngModel.NgModelController} ngModelCtrl Instance of the ngModel controller.
|
||
* @param {Object} inputDirective Config for AngularJS's `input` directive.
|
||
*/
|
||
CalendarCtrl.prototype.configureNgModel = function(ngModelCtrl, inputDirective) {
|
||
var self = this;
|
||
self.ngModelCtrl = ngModelCtrl;
|
||
|
||
// The component needs to be [type="date"] in order to be picked up by AngularJS.
|
||
this.$attrs.$set('type', 'date');
|
||
|
||
// Invoke the `input` directive link function, adding a stub for the element.
|
||
// This allows us to re-use AngularJS' logic for setting the timezone via ng-model-options.
|
||
// It works by calling the link function directly which then adds the proper `$parsers` and
|
||
// `$formatters` to the NgModelController.
|
||
inputDirective[0].link.pre(this.$scope, {
|
||
on: angular.noop,
|
||
val: angular.noop,
|
||
0: {}
|
||
}, this.$attrs, [ngModelCtrl]);
|
||
|
||
ngModelCtrl.$render = function() {
|
||
var value = this.$viewValue, convertedDate;
|
||
|
||
// In the case where a conversion is needed, the $viewValue here will be a string like
|
||
// "2020-05-10" instead of a Date object.
|
||
if (!self.dateUtil.isValidDate(value)) {
|
||
convertedDate = self.dateUtil.removeLocalTzAndReparseDate(new Date(value));
|
||
if (self.dateUtil.isValidDate(convertedDate)) {
|
||
value = convertedDate;
|
||
}
|
||
}
|
||
|
||
// Notify the child scopes of any changes.
|
||
self.$scope.$broadcast('md-calendar-parent-changed', value);
|
||
|
||
// Set up the selectedDate if it hasn't been already.
|
||
if (!self.selectedDate) {
|
||
self.selectedDate = value;
|
||
}
|
||
|
||
// Also set up the displayDate.
|
||
if (!self.displayDate) {
|
||
self.displayDate = self.selectedDate || self.today;
|
||
}
|
||
};
|
||
|
||
self.$mdUtil.nextTick(function() {
|
||
self.isInitialized = true;
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Sets the ng-model value for the calendar and emits a change event.
|
||
* @param {Date} date new value for the calendar
|
||
*/
|
||
CalendarCtrl.prototype.setNgModelValue = function(date) {
|
||
var timezone = this.$mdUtil.getModelOption(this.ngModelCtrl, 'timezone');
|
||
var value = this.dateUtil.createDateAtMidnight(date);
|
||
this.focusDate(value);
|
||
this.$scope.$emit('md-calendar-change', value);
|
||
// Using the timezone when the offset is negative (GMT+X) causes the previous day to be
|
||
// selected here. This check avoids that.
|
||
if (timezone == null || value.getTimezoneOffset() < 0) {
|
||
this.ngModelCtrl.$setViewValue(this.ngDateFilter(value, 'yyyy-MM-dd'), 'default');
|
||
} else {
|
||
this.ngModelCtrl.$setViewValue(this.ngDateFilter(value, 'yyyy-MM-dd', timezone), 'default');
|
||
}
|
||
this.ngModelCtrl.$render();
|
||
return value;
|
||
};
|
||
|
||
/**
|
||
* 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;
|
||
|
||
self.$mdUtil.nextTick(function() {
|
||
self.currentView = newView;
|
||
|
||
if (time) {
|
||
self.displayDate = angular.isDate(time) ? time : new Date(time);
|
||
}
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Focus the cell corresponding to the given date.
|
||
* @param {Date=} date The date to be focused.
|
||
*/
|
||
CalendarCtrl.prototype.focusDate = function(date) {
|
||
if (this.dateUtil.isValidDate(date)) {
|
||
var previousFocus = this.$element[0].querySelector('.' + this.FOCUSED_DATE_CLASS);
|
||
if (previousFocus) {
|
||
previousFocus.classList.remove(this.FOCUSED_DATE_CLASS);
|
||
}
|
||
|
||
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();
|
||
}
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Highlights a date cell on the calendar and changes the selected date.
|
||
* @param {Date=} date Date to be marked as selected.
|
||
*/
|
||
CalendarCtrl.prototype.changeSelectedDate = function(date) {
|
||
var selectedDateClass = this.SELECTED_DATE_CLASS;
|
||
var prevDateCell = this.$element[0].querySelector('.' + selectedDateClass);
|
||
|
||
// Remove the selected class from the previously selected date, if any.
|
||
if (prevDateCell) {
|
||
prevDateCell.classList.remove(selectedDateClass);
|
||
prevDateCell.setAttribute('aria-selected', 'false');
|
||
}
|
||
|
||
// Apply the select class to the new selected date if it is set.
|
||
if (date) {
|
||
var dateCell = document.getElementById(this.getDateId(date, this.currentView));
|
||
if (dateCell) {
|
||
dateCell.classList.add(selectedDateClass);
|
||
dateCell.setAttribute('aria-selected', 'true');
|
||
}
|
||
}
|
||
|
||
this.selectedDate = date;
|
||
};
|
||
|
||
/**
|
||
* 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.
|
||
*/
|
||
CalendarCtrl.prototype.getActionFromKeyEvent = function(event) {
|
||
var keyCode = this.keyCode;
|
||
|
||
switch (event.which) {
|
||
case keyCode.ENTER: return 'select';
|
||
|
||
case keyCode.RIGHT_ARROW: return 'move-right';
|
||
case keyCode.LEFT_ARROW: return 'move-left';
|
||
|
||
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';
|
||
|
||
case keyCode.PAGE_DOWN: return 'move-page-down';
|
||
case keyCode.PAGE_UP: return 'move-page-up';
|
||
|
||
case keyCode.HOME: return 'start';
|
||
case keyCode.END: return 'end';
|
||
|
||
default: return null;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Handles a key event in the calendar with the appropriate action.
|
||
* The action will either
|
||
* - select the focused date
|
||
* - navigate to focus a new date
|
||
* - emit a md-calendar-close event if in a md-datepicker panel
|
||
* - emit a md-calendar-parent-action
|
||
* - delegate to normal tab order if the TAB key is pressed in standalone mode
|
||
* @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.standaloneMode)) {
|
||
self.$scope.$emit('md-calendar-close');
|
||
|
||
if (event.which === self.keyCode.TAB) {
|
||
event.preventDefault();
|
||
}
|
||
|
||
return;
|
||
} else if (event.which === self.keyCode.TAB && self.standaloneMode) {
|
||
// delegate to the normal tab order if the TAB key is pressed in standalone mode
|
||
return;
|
||
}
|
||
|
||
// 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);
|
||
}
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 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.
|
||
*/
|
||
CalendarCtrl.prototype.hideVerticalScrollbar = function(childCtrl) {
|
||
var self = this;
|
||
var element = childCtrl.$element[0];
|
||
var scrollMask = element.querySelector('.md-calendar-scroll-mask');
|
||
|
||
if (self.width > 0) {
|
||
setWidth();
|
||
} else {
|
||
self.$$rAF(function() {
|
||
var scroller = childCtrl.calendarScroller;
|
||
|
||
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';
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 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}
|
||
*/
|
||
CalendarCtrl.prototype.getDateId = function(date, namespace) {
|
||
if (!namespace) {
|
||
throw new Error('A namespace for the date id has to be specified.');
|
||
}
|
||
|
||
return [
|
||
'md',
|
||
this.id,
|
||
namespace,
|
||
date.getFullYear(),
|
||
date.getMonth(),
|
||
date.getDate()
|
||
].join('-');
|
||
};
|
||
|
||
/**
|
||
* Util to trigger an extra digest on a parent scope, in order to to ensure that
|
||
* any child virtual repeaters have 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 issue, is when the repeater has less
|
||
* than a page of content (e.g. a month or year view has a min or max date).
|
||
*/
|
||
CalendarCtrl.prototype.updateVirtualRepeat = function() {
|
||
var scope = this.$scope;
|
||
var virtualRepeatResizeListener = scope.$on('$md-resize-enable', function() {
|
||
if (!scope.$$phase) {
|
||
scope.$apply();
|
||
}
|
||
|
||
virtualRepeatResizeListener();
|
||
});
|
||
};
|
||
})();
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
(function() {
|
||
'use strict';
|
||
|
||
CalendarMonthCtrl.$inject = ["$element", "$scope", "$animate", "$q", "$$mdDateUtil", "$mdDateLocale"];
|
||
angular.module('material.components.datepicker')
|
||
.directive('mdCalendarMonth', calendarDirective);
|
||
|
||
/**
|
||
* 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;
|
||
|
||
/** Private directive that represents a list of months inside the calendar. */
|
||
function calendarDirective() {
|
||
return {
|
||
template:
|
||
'<table aria-hidden="true" class="md-calendar-day-header"><thead></thead></table>' +
|
||
'<div class="md-calendar-scroll-mask">' +
|
||
'<md-virtual-repeat-container class="md-calendar-scroll-container" ' +
|
||
'md-offset-size="' + (TBODY_SINGLE_ROW_HEIGHT - TBODY_HEIGHT) + '">' +
|
||
'<table role="grid" tabindex="0" class="md-calendar" aria-readonly="true">' +
|
||
'<tbody ' +
|
||
'md-calendar-month-body ' +
|
||
'role="rowgroup" ' +
|
||
'md-virtual-repeat="i in monthCtrl.items" ' +
|
||
'md-month-offset="$index" ' +
|
||
'class="md-calendar-month" ' +
|
||
'md-start-index="monthCtrl.getSelectedMonthIndex()" ' +
|
||
'md-item-size="' + TBODY_HEIGHT + '">' +
|
||
|
||
// The <tr> ensures that the <tbody> will always have the
|
||
// proper height, even if it's empty. If it's content is
|
||
// compiled, the <tr> will be overwritten.
|
||
'<tr aria-hidden="true" md-force-height="\'' + TBODY_HEIGHT + 'px\'"></tr>' +
|
||
'</tbody>' +
|
||
'</table>' +
|
||
'</md-virtual-repeat-container>' +
|
||
'</div>',
|
||
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);
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 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 {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() {
|
||
// The timestamp has to be converted to a valid date.
|
||
self.calendarCtrl.setNgModelValue(new Date(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));
|
||
};
|
||
}
|
||
|
||
/** 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) {
|
||
/**
|
||
* Dummy array-like object for virtual-repeat to iterate over. The length is the total
|
||
* number of months that can be viewed. We add 2 months: one to include the current month
|
||
* and one for the last dummy month.
|
||
*
|
||
* This is shorter than ideal because of a (potential) Firefox bug
|
||
* https://bugzilla.mozilla.org/show_bug.cgi?id=1181658.
|
||
*/
|
||
|
||
this.items = {
|
||
length: this.dateUtil.getMonthDistance(
|
||
calendarCtrl.firstRenderableDate,
|
||
calendarCtrl.lastRenderableDate
|
||
) + 2
|
||
};
|
||
|
||
this.calendarCtrl = calendarCtrl;
|
||
this.attachScopeListeners();
|
||
calendarCtrl.updateVirtualRepeat();
|
||
|
||
// 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} the "index" of the currently selected date
|
||
*/
|
||
CalendarMonthCtrl.prototype.getSelectedMonthIndex = function() {
|
||
var calendarCtrl = this.calendarCtrl;
|
||
|
||
return this.dateUtil.getMonthDistance(
|
||
calendarCtrl.firstRenderableDate,
|
||
calendarCtrl.displayDate || calendarCtrl.selectedDate || calendarCtrl.today
|
||
);
|
||
};
|
||
|
||
/**
|
||
* 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();
|
||
}
|
||
|
||
// If trying to show an invalid date or a transition is in progress, do nothing.
|
||
if (!this.dateUtil.isValidDate(date) || this.isMonthTransitionInProgress) {
|
||
return this.$q.when();
|
||
}
|
||
|
||
this.isMonthTransitionInProgress = true;
|
||
var animationPromise = this.animateDateChange(date);
|
||
|
||
this.calendarCtrl.displayDate = date;
|
||
|
||
var self = this;
|
||
animationPromise.then(function() {
|
||
self.isMonthTransitionInProgress = false;
|
||
});
|
||
|
||
return animationPromise;
|
||
};
|
||
|
||
/**
|
||
* Animates the transition from the calendar's current month to the given month.
|
||
* @param {Date} date
|
||
* @returns {angular.$q.Promise} The animation promise.
|
||
*/
|
||
CalendarMonthCtrl.prototype.animateDateChange = function(date) {
|
||
if (this.dateUtil.isValidDate(date)) {
|
||
var monthDistance = this.dateUtil.getMonthDistance(this.calendarCtrl.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.calendarCtrl.changeSelectedDate(value);
|
||
self.changeDisplayDate(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.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.focusDate(date);
|
||
});
|
||
}
|
||
}
|
||
};
|
||
})();
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
(function() {
|
||
'use strict';
|
||
|
||
mdCalendarMonthBodyDirective.$inject = ["$compile", "$$mdSvgRegistry"];
|
||
CalendarMonthBodyCtrl.$inject = ["$element", "$$mdDateUtil", "$mdDateLocale"];
|
||
angular.module('material.components.datepicker')
|
||
.directive('mdCalendarMonthBody', mdCalendarMonthBodyDirective);
|
||
|
||
/**
|
||
* 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.
|
||
* @ngInject
|
||
*/
|
||
function mdCalendarMonthBodyDirective($compile, $$mdSvgRegistry) {
|
||
var ARROW_ICON = $compile('<md-icon md-svg-src="' +
|
||
$$mdSvgRegistry.mdTabsArrow + '"></md-icon>')({})[0];
|
||
|
||
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];
|
||
|
||
monthBodyCtrl.calendarCtrl = calendarCtrl;
|
||
monthBodyCtrl.monthCtrl = monthCtrl;
|
||
monthBodyCtrl.arrowIcon = ARROW_ICON.cloneNode(true);
|
||
|
||
// 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 updated.
|
||
// 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) {
|
||
if (angular.isNumber(offset)) {
|
||
monthBodyCtrl.generateContent();
|
||
}
|
||
});
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Controller for a single calendar month.
|
||
* @ngInject @constructor
|
||
*/
|
||
function CalendarMonthBodyCtrl($element, $$mdDateUtil, $mdDateLocale) {
|
||
/**
|
||
* @final
|
||
* @type {!JQLite}
|
||
*/
|
||
this.$element = $element;
|
||
|
||
/** @final */
|
||
this.dateUtil = $$mdDateUtil;
|
||
|
||
/** @final */
|
||
this.dateLocale = $mdDateLocale;
|
||
|
||
/** @type {Object} Reference to the month view. */
|
||
this.monthCtrl = null;
|
||
|
||
/** @type {Object} Reference to the calendar. */
|
||
this.calendarCtrl = null;
|
||
|
||
/**
|
||
* Number of months from the start of the month "items" that the currently rendered month
|
||
* occurs. Set via angular data binding.
|
||
* @type {number|null}
|
||
*/
|
||
this.offset = null;
|
||
|
||
/**
|
||
* Date cell to focus after appending the month to the document.
|
||
* @type {HTMLElement}
|
||
*/
|
||
this.focusAfterAppend = null;
|
||
}
|
||
|
||
/** Generate and append the content for this month to the directive element. */
|
||
CalendarMonthBodyCtrl.prototype.generateContent = function() {
|
||
var date = this.dateUtil.incrementMonths(this.calendarCtrl.firstRenderableDate, this.offset);
|
||
|
||
this.$element
|
||
.empty()
|
||
.append(this.buildCalendarForMonth(date));
|
||
|
||
if (this.focusAfterAppend) {
|
||
this.focusAfterAppend.classList.add(this.calendarCtrl.FOCUSED_DATE_CLASS);
|
||
this.focusAfterAppend = null;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Creates a single cell to contain a date in the calendar with all appropriate
|
||
* attributes and classes added. If a date is given, the cell content will be set
|
||
* based on the date.
|
||
* @param {Date=} opt_date
|
||
* @returns {HTMLElement}
|
||
*/
|
||
CalendarMonthBodyCtrl.prototype.buildDateCell = function(opt_date) {
|
||
var monthCtrl = this.monthCtrl;
|
||
var calendarCtrl = this.calendarCtrl;
|
||
|
||
// TODO(jelbourn): cloneNode is likely a faster way of doing this.
|
||
var cell = document.createElement('td');
|
||
cell.tabIndex = -1;
|
||
cell.classList.add('md-calendar-date');
|
||
cell.setAttribute('role', 'gridcell');
|
||
|
||
if (opt_date) {
|
||
cell.setAttribute('tabindex', '-1');
|
||
cell.setAttribute('aria-label', this.dateLocale.longDateFormatter(opt_date));
|
||
cell.id = calendarCtrl.getDateId(opt_date, 'month');
|
||
|
||
// Use `data-timestamp` attribute because IE10 does not support the `dataset` property.
|
||
cell.setAttribute('data-timestamp', opt_date.getTime());
|
||
|
||
// TODO(jelourn): Doing these comparisons for class addition during generation might be slow.
|
||
// It may be better to finish the construction and then query the node and add the class.
|
||
if (this.dateUtil.isSameDay(opt_date, calendarCtrl.today)) {
|
||
cell.classList.add(calendarCtrl.TODAY_CLASS);
|
||
}
|
||
|
||
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');
|
||
}
|
||
|
||
var cellText = this.dateLocale.dates[opt_date.getDate()];
|
||
|
||
if (this.isDateEnabled(opt_date)) {
|
||
// Add a indicator for select, hover, and focus states.
|
||
var selectionIndicator = document.createElement('span');
|
||
selectionIndicator.classList.add('md-calendar-date-selection-indicator');
|
||
selectionIndicator.textContent = cellText;
|
||
cell.appendChild(selectionIndicator);
|
||
cell.addEventListener('click', monthCtrl.cellClickHandler);
|
||
|
||
if (calendarCtrl.displayDate && this.dateUtil.isSameDay(opt_date, calendarCtrl.displayDate)) {
|
||
this.focusAfterAppend = cell;
|
||
}
|
||
} else {
|
||
cell.classList.add('md-calendar-date-disabled');
|
||
cell.textContent = cellText;
|
||
}
|
||
}
|
||
|
||
return cell;
|
||
};
|
||
|
||
/**
|
||
* Check whether date is in range and enabled
|
||
* @param {Date=} opt_date
|
||
* @return {boolean} Whether the date is enabled.
|
||
*/
|
||
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 <tbody> content for the given date's month.
|
||
* @param {Date=} opt_dateInMonth
|
||
* @returns {DocumentFragment} A document fragment containing the <tr> 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');
|
||
var monthLabelCellContent = document.createElement('span');
|
||
var calendarCtrl = this.calendarCtrl;
|
||
|
||
monthLabelCellContent.textContent = this.dateLocale.monthHeaderFormatter(date);
|
||
monthLabelCell.appendChild(monthLabelCellContent);
|
||
monthLabelCell.classList.add('md-calendar-month-label');
|
||
// If the entire month is after the max date, render the label as a disabled state.
|
||
if (calendarCtrl.maxDate && firstDayOfMonth > calendarCtrl.maxDate) {
|
||
monthLabelCell.classList.add('md-calendar-month-label-disabled');
|
||
// If the user isn't supposed to be able to change views, render the
|
||
// label as usual, but disable the clicking functionality.
|
||
} else if (!calendarCtrl.mode) {
|
||
monthLabelCell.addEventListener('click', this.monthCtrl.headerClickHandler);
|
||
monthLabelCell.setAttribute('data-timestamp', firstDayOfMonth.getTime());
|
||
monthLabelCell.setAttribute('aria-label', this.dateLocale.monthFormatter(date));
|
||
monthLabelCell.classList.add('md-calendar-label-clickable');
|
||
monthLabelCell.appendChild(this.arrowIcon.cloneNode(true));
|
||
}
|
||
|
||
if (firstDayOfTheWeek <= 2) {
|
||
monthLabelCell.setAttribute('colspan', '7');
|
||
|
||
var monthLabelRow = this.buildDateRow();
|
||
monthLabelRow.appendChild(monthLabelCell);
|
||
monthBody.insertBefore(monthLabelRow, row);
|
||
|
||
if (isFinalMonth) {
|
||
return monthBody;
|
||
}
|
||
} else {
|
||
blankCellOffset = 3;
|
||
monthLabelCell.setAttribute('colspan', '3');
|
||
row.appendChild(monthLabelCell);
|
||
}
|
||
|
||
// Add a blank cell for each day of the week that occurs before the first of the month.
|
||
// For example, if the first day of the month is a Tuesday, add blank cells for Sun and Mon.
|
||
// The blankCellOffset is needed in cases where the first N cells are used by the month label.
|
||
for (var i = blankCellOffset; i < firstDayOfTheWeek; i++) {
|
||
row.appendChild(this.buildDateCell());
|
||
}
|
||
|
||
// Add a cell for each day of the month, keeping track of the day of the week so that
|
||
// we know when to start a new row.
|
||
var dayOfWeek = firstDayOfTheWeek;
|
||
var iterationDate = firstDayOfMonth;
|
||
for (var d = 1; d <= numberOfDaysInMonth; d++) {
|
||
// If we've reached the end of the week, start a new row.
|
||
if (dayOfWeek === 7) {
|
||
// We've finished the first row, so we're done if this is the final month.
|
||
if (isFinalMonth) {
|
||
return monthBody;
|
||
}
|
||
dayOfWeek = 0;
|
||
rowNumber++;
|
||
row = this.buildDateRow(rowNumber);
|
||
monthBody.appendChild(row);
|
||
}
|
||
|
||
iterationDate.setDate(d);
|
||
var cell = this.buildDateCell(iterationDate);
|
||
row.appendChild(cell);
|
||
|
||
dayOfWeek++;
|
||
}
|
||
|
||
// Ensure that the last row of the month has 7 cells.
|
||
while (row.childNodes.length < 7) {
|
||
row.appendChild(this.buildDateCell());
|
||
}
|
||
|
||
// Ensure that all months have 6 rows. This is necessary for now because the virtual-repeat
|
||
// requires that all items have exactly the same height.
|
||
while (monthBody.childNodes.length < 6) {
|
||
var whitespaceRow = this.buildDateRow();
|
||
for (var 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;
|
||
};
|
||
})();
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
(function() {
|
||
'use strict';
|
||
|
||
CalendarYearCtrl.$inject = ["$element", "$scope", "$animate", "$q", "$$mdDateUtil", "$mdUtil"];
|
||
angular.module('material.components.datepicker')
|
||
.directive('mdCalendarYear', calendarDirective);
|
||
|
||
/**
|
||
* Height of one calendar year tbody. This must be made known to the virtual-repeat and is
|
||
* subsequently used for scrolling to specific years.
|
||
*/
|
||
var TBODY_HEIGHT = 88;
|
||
|
||
/** Private component, representing a list of years in the calendar. */
|
||
function calendarDirective() {
|
||
return {
|
||
template:
|
||
'<div class="md-calendar-scroll-mask">' +
|
||
'<md-virtual-repeat-container class="md-calendar-scroll-container">' +
|
||
'<table role="grid" tabindex="0" class="md-calendar" aria-readonly="true">' +
|
||
'<tbody ' +
|
||
'md-calendar-year-body ' +
|
||
'role="rowgroup" ' +
|
||
'md-virtual-repeat="i in yearCtrl.items" ' +
|
||
'md-year-offset="$index" class="md-calendar-year" ' +
|
||
'md-start-index="yearCtrl.getFocusedYearIndex()" ' +
|
||
'md-item-size="' + TBODY_HEIGHT + '">' +
|
||
// The <tr> ensures that the <tbody> will have the proper
|
||
// height, even though it may be empty.
|
||
'<tr aria-hidden="true" md-force-height="\'' + TBODY_HEIGHT + 'px\'"></tr>' +
|
||
'</tbody>' +
|
||
'</table>' +
|
||
'</md-virtual-repeat-container>' +
|
||
'</div>',
|
||
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 the mdCalendar component.
|
||
* @ngInject @constructor
|
||
*/
|
||
function CalendarYearCtrl($element, $scope, $animate, $q, $$mdDateUtil, $mdUtil) {
|
||
|
||
/** @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 {HTMLElement} */
|
||
this.calendarScroller = $element[0].querySelector('.md-virtual-repeat-scroller');
|
||
|
||
/** @type {boolean} */
|
||
this.isInitialized = false;
|
||
|
||
/** @type {boolean} */
|
||
this.isMonthTransitionInProgress = false;
|
||
|
||
/** @final */
|
||
this.$mdUtil = $mdUtil;
|
||
|
||
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() {
|
||
self.onTimestampSelected($$mdDateUtil.getTimestampFromNode(this));
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 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) {
|
||
/**
|
||
* Dummy array-like object for virtual-repeat to iterate over. The length is the total
|
||
* number of years that can be viewed. We add 1 extra in order to include the current year.
|
||
*/
|
||
|
||
this.items = {
|
||
length: this.dateUtil.getYearDistance(
|
||
calendarCtrl.firstRenderableDate,
|
||
calendarCtrl.lastRenderableDate
|
||
) + 1
|
||
};
|
||
|
||
this.calendarCtrl = calendarCtrl;
|
||
this.attachScopeListeners();
|
||
calendarCtrl.updateVirtualRepeat();
|
||
|
||
// 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}
|
||
*/
|
||
CalendarYearCtrl.prototype.getFocusedYearIndex = function() {
|
||
var calendarCtrl = this.calendarCtrl;
|
||
|
||
return this.dateUtil.getYearDistance(
|
||
calendarCtrl.firstRenderableDate,
|
||
calendarCtrl.displayDate || calendarCtrl.selectedDate || calendarCtrl.today
|
||
);
|
||
};
|
||
|
||
/**
|
||
* 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);
|
||
|
||
self.isMonthTransitionInProgress = true;
|
||
self.calendarCtrl.displayDate = date;
|
||
|
||
return animationPromise.then(function() {
|
||
self.isMonthTransitionInProgress = false;
|
||
});
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Animates the transition from the calendar's current month to the given month.
|
||
* @param {Date} date
|
||
* @returns {angular.$q.Promise} The animation promise.
|
||
*/
|
||
CalendarYearCtrl.prototype.animateDateChange = function(date) {
|
||
if (this.dateUtil.isValidDate(date)) {
|
||
var monthDistance = this.dateUtil.getYearDistance(this.calendarCtrl.firstRenderableDate, date);
|
||
this.calendarScroller.scrollTop = monthDistance * TBODY_HEIGHT;
|
||
}
|
||
|
||
return this.$q.when();
|
||
};
|
||
|
||
/**
|
||
* 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 self = this;
|
||
var calendarCtrl = self.calendarCtrl;
|
||
var displayDate = calendarCtrl.displayDate;
|
||
|
||
if (action === 'select') {
|
||
self.changeDate(displayDate).then(function() {
|
||
self.onTimestampSelected(displayDate);
|
||
});
|
||
} else {
|
||
var date = null;
|
||
var dateUtil = self.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;
|
||
}
|
||
|
||
if (date) {
|
||
var min = calendarCtrl.minDate ? dateUtil.getFirstDateOfMonth(calendarCtrl.minDate) : null;
|
||
var max = calendarCtrl.maxDate ? dateUtil.getFirstDateOfMonth(calendarCtrl.maxDate) : null;
|
||
date = dateUtil.getFirstDateOfMonth(self.dateUtil.clampDate(date, min, max));
|
||
|
||
self.changeDate(date).then(function() {
|
||
calendarCtrl.focusDate(date);
|
||
});
|
||
}
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Attaches listeners for the scope events that are broadcast by the calendar.
|
||
*/
|
||
CalendarYearCtrl.prototype.attachScopeListeners = function() {
|
||
var self = this;
|
||
|
||
self.$scope.$on('md-calendar-parent-changed', function(event, value) {
|
||
self.calendarCtrl.changeSelectedDate(value ? self.dateUtil.getFirstDateOfMonth(value) : value);
|
||
self.changeDate(value);
|
||
});
|
||
|
||
self.$scope.$on('md-calendar-parent-action', angular.bind(self, self.handleKeyEvent));
|
||
};
|
||
|
||
/**
|
||
* Handles the behavior when a date is selected. Depending on the `mode`
|
||
* of the calendar, this can either switch back to the calendar view or
|
||
* set the model value.
|
||
* @param {number} timestamp The selected timestamp.
|
||
*/
|
||
CalendarYearCtrl.prototype.onTimestampSelected = function(timestamp) {
|
||
var calendarCtrl = this.calendarCtrl;
|
||
|
||
if (calendarCtrl.mode) {
|
||
this.$mdUtil.nextTick(function() {
|
||
// The timestamp has to be converted to a valid date.
|
||
calendarCtrl.setNgModelValue(new Date(timestamp));
|
||
});
|
||
} else {
|
||
calendarCtrl.setCurrentView('month', timestamp);
|
||
}
|
||
};
|
||
})();
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
(function() {
|
||
'use strict';
|
||
|
||
CalendarYearBodyCtrl.$inject = ["$element", "$$mdDateUtil", "$mdDateLocale"];
|
||
angular.module('material.components.datepicker')
|
||
.directive('mdCalendarYearBody', mdCalendarYearDirective);
|
||
|
||
/**
|
||
* 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.
|
||
*/
|
||
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;
|
||
|
||
scope.$watch(function() { return yearBodyCtrl.offset; }, function(offset) {
|
||
if (angular.isNumber(offset)) {
|
||
yearBodyCtrl.generateContent();
|
||
}
|
||
});
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Controller for a single year.
|
||
* @ngInject @constructor
|
||
*/
|
||
function CalendarYearBodyCtrl($element, $$mdDateUtil, $mdDateLocale) {
|
||
/**
|
||
* @final
|
||
* @type {!JQLite}
|
||
*/
|
||
this.$element = $element;
|
||
|
||
/** @final */
|
||
this.dateUtil = $$mdDateUtil;
|
||
|
||
/** @final */
|
||
this.dateLocale = $mdDateLocale;
|
||
|
||
/** @type {Object} Reference to the calendar. */
|
||
this.calendarCtrl = null;
|
||
|
||
/** @type {Object} Reference to the year view. */
|
||
this.yearCtrl = null;
|
||
|
||
/**
|
||
* Number of months from the start of the month "items" that the currently rendered month
|
||
* occurs. Set via angular data binding.
|
||
* @type {number|null}
|
||
*/
|
||
this.offset = null;
|
||
|
||
/**
|
||
* Date cell to focus after appending the month to the document.
|
||
* @type {HTMLElement}
|
||
*/
|
||
this.focusAfterAppend = null;
|
||
}
|
||
|
||
/** Generate and append the content for this year to the directive element. */
|
||
CalendarYearBodyCtrl.prototype.generateContent = function() {
|
||
var date = this.dateUtil.incrementYears(this.calendarCtrl.firstRenderableDate, this.offset);
|
||
|
||
this.$element
|
||
.empty()
|
||
.append(this.buildCalendarForYear(date));
|
||
|
||
if (this.focusAfterAppend) {
|
||
this.focusAfterAppend.classList.add(this.calendarCtrl.FOCUSED_DATE_CLASS);
|
||
this.focusAfterAppend = null;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Creates a single cell to contain a year in the calendar.
|
||
* @param {number} year Four-digit year.
|
||
* @param {number} month Zero-indexed month.
|
||
* @returns {HTMLElement}
|
||
*/
|
||
CalendarYearBodyCtrl.prototype.buildMonthCell = function(year, month) {
|
||
var calendarCtrl = this.calendarCtrl;
|
||
var yearCtrl = this.yearCtrl;
|
||
var cell = this.buildBlankCell();
|
||
|
||
// 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');
|
||
|
||
// Use `data-timestamp` attribute because IE10 does not support the `dataset` property.
|
||
cell.setAttribute('data-timestamp', String(firstOfMonth.getTime()));
|
||
|
||
if (this.dateUtil.isSameMonthAndYear(firstOfMonth, calendarCtrl.today)) {
|
||
cell.classList.add(calendarCtrl.TODAY_CLASS);
|
||
}
|
||
|
||
if (this.dateUtil.isValidDate(calendarCtrl.selectedDate) &&
|
||
this.dateUtil.isSameMonthAndYear(firstOfMonth, calendarCtrl.selectedDate)) {
|
||
cell.classList.add(calendarCtrl.SELECTED_DATE_CLASS);
|
||
cell.setAttribute('aria-selected', 'true');
|
||
}
|
||
|
||
var cellText = this.dateLocale.shortMonths[month];
|
||
|
||
if (this.dateUtil.isMonthWithinRange(
|
||
firstOfMonth, calendarCtrl.minDate, calendarCtrl.maxDate) &&
|
||
(!angular.isFunction(calendarCtrl.monthFilter) ||
|
||
calendarCtrl.monthFilter(firstOfMonth))) {
|
||
var selectionIndicator = document.createElement('span');
|
||
selectionIndicator.classList.add('md-calendar-date-selection-indicator');
|
||
selectionIndicator.textContent = cellText;
|
||
cell.appendChild(selectionIndicator);
|
||
cell.addEventListener('click', yearCtrl.cellClickHandler);
|
||
|
||
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;
|
||
};
|
||
|
||
/**
|
||
* Builds a blank cell.
|
||
* @return {HTMLElement}
|
||
*/
|
||
CalendarYearBodyCtrl.prototype.buildBlankCell = function() {
|
||
var cell = document.createElement('td');
|
||
cell.tabIndex = -1;
|
||
cell.classList.add('md-calendar-date');
|
||
cell.setAttribute('role', 'gridcell');
|
||
|
||
cell.setAttribute('tabindex', '-1');
|
||
return cell;
|
||
};
|
||
|
||
/**
|
||
* Builds the <tbody> 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.
|
||
*/
|
||
CalendarYearBodyCtrl.prototype.buildCalendarForYear = function(date) {
|
||
// Store rows for the month in a document fragment so that we can append them all at once.
|
||
var year = date.getFullYear();
|
||
var yearBody = document.createDocumentFragment();
|
||
|
||
var monthCell, i;
|
||
// First row contains label and Jan-Jun.
|
||
var firstRow = document.createElement('tr');
|
||
var labelCell = document.createElement('td');
|
||
labelCell.className = 'md-calendar-month-label';
|
||
labelCell.textContent = String(year);
|
||
firstRow.appendChild(labelCell);
|
||
|
||
for (i = 0; i < 6; i++) {
|
||
firstRow.appendChild(this.buildMonthCell(year, i));
|
||
}
|
||
yearBody.appendChild(firstRow);
|
||
|
||
// Second row contains a blank cell and Jul-Dec.
|
||
var secondRow = document.createElement('tr');
|
||
secondRow.appendChild(this.buildBlankCell());
|
||
for (i = 6; i < 12; i++) {
|
||
secondRow.appendChild(this.buildMonthCell(year, i));
|
||
}
|
||
yearBody.appendChild(secondRow);
|
||
|
||
return yearBody;
|
||
};
|
||
})();
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
(function() {
|
||
'use strict';
|
||
|
||
/**
|
||
* @ngdoc service
|
||
* @name $mdDateLocaleProvider
|
||
* @module material.components.datepicker
|
||
*
|
||
* @description
|
||
* The `$mdDateLocaleProvider` is the provider that creates the `$mdDateLocale` service.
|
||
* This provider that allows the user to specify messages, formatters, and parsers for date
|
||
* internationalization. The `$mdDateLocale` service itself is consumed by AngularJS Material
|
||
* components that deal with dates
|
||
* (i.e. <a ng-href="api/directive/mdDatepicker">mdDatepicker</a>).
|
||
*
|
||
* @property {Array<string>} months Array of month names (in order).
|
||
* @property {Array<string>} shortMonths Array of abbreviated month names.
|
||
* @property {Array<string>} days Array of the days of the week (in order).
|
||
* @property {Array<string>} shortDays Array of abbreviated days of the week.
|
||
* @property {Array<string>} dates Array of dates of the month. Only necessary for locales
|
||
* using a numeral system other than [1, 2, 3...].
|
||
* @property {Array<string>} firstDayOfWeek The first day of the week. Sunday = 0, Monday = 1,
|
||
* etc.
|
||
* @property {function(string): Date} parseDate Function that converts a date string to a Date
|
||
* object (the date portion).
|
||
* @property {function(Date, string): string} formatDate Function to format a date object to a
|
||
* string. The datepicker directive also provides the time zone, if it was specified.
|
||
* @property {function(Date): string} monthHeaderFormatter Function that returns the label for
|
||
* a month given a date.
|
||
* @property {function(Date): string} monthFormatter Function that returns the full name of a month
|
||
* for a given date.
|
||
* @property {function(number): string} weekNumberFormatter Function that returns a label for
|
||
* a week given the week number.
|
||
* @property {function(Date): string} longDateFormatter Function that formats a date into a long
|
||
* `aria-label` that is read by the screen reader when the focused date changes.
|
||
* @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.
|
||
* @property {Date} firstRenderableDate The date from which the datepicker calendar will begin
|
||
* rendering. Note that this will be ignored if a minimum date is set.
|
||
* Defaults to January 1st 1880.
|
||
* @property {Date} lastRenderableDate The last date that will be rendered by the datepicker
|
||
* calendar. Note that this will be ignored if a maximum date is set.
|
||
* Defaults to January 1st 2130.
|
||
* @property {function(string): boolean} isDateComplete Function to determine whether a string
|
||
* makes sense to be parsed to a `Date` object. Returns `true` if the date appears to be complete
|
||
* and parsing should occur. By default, this checks for 3 groups of text or numbers separated
|
||
* by delimiters. This means that by default, date strings must include a month, day, and year
|
||
* to be parsed and for the model to be updated.
|
||
*
|
||
* @usage
|
||
* <hljs lang="js">
|
||
* 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) {
|
||
* var m = moment(date);
|
||
* return m.isValid() ? m.format('L') : '';
|
||
* };
|
||
*
|
||
* // Allow only a day and month to be specified.
|
||
* // This is required if using the 'M/D' format with moment.js.
|
||
* $mdDateLocaleProvider.isDateComplete = function(dateString) {
|
||
* dateString = dateString.trim();
|
||
*
|
||
* // Look for two chunks of content (either numbers or text) separated by delimiters.
|
||
* var re = /^(([a-zA-Z]{3,}|[0-9]{1,4})([ .,]+|[/-]))([a-zA-Z]{3,}|[0-9]{1,4})/;
|
||
* return re.test(dateString);
|
||
* };
|
||
*
|
||
* $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';
|
||
*
|
||
* // You can also set when your calendar begins and ends.
|
||
* $mdDateLocaleProvider.firstRenderableDate = new Date(1776, 6, 4);
|
||
* $mdDateLocaleProvider.lastRenderableDate = new Date(2012, 11, 21);
|
||
* });
|
||
* </hljs>
|
||
*
|
||
*/
|
||
angular.module('material.components.datepicker').config(["$provide", function($provide) {
|
||
// TODO(jelbourn): Assert provided values are correctly formatted. Need assertions.
|
||
|
||
/** @constructor */
|
||
function DateLocaleProvider() {
|
||
/** Array of full month names. E.g., ['January', 'February', ...] */
|
||
this.months = null;
|
||
|
||
/** Array of abbreviated month names. E.g., ['Jan', 'Feb', ...] */
|
||
this.shortMonths = null;
|
||
|
||
/** Array of full day of the week names. E.g., ['Monday', 'Tuesday', ...] */
|
||
this.days = null;
|
||
|
||
/** Array of abbreviated dat of the week names. E.g., ['M', 'T', ...] */
|
||
this.shortDays = null;
|
||
|
||
/** Array of dates of a month (1 - 31). Characters might be different in some locales. */
|
||
this.dates = null;
|
||
|
||
/** Index of the first day of the week. 0 = Sunday, 1 = Monday, etc. */
|
||
this.firstDayOfWeek = 0;
|
||
|
||
/**
|
||
* Function that converts the date portion of a Date to a string.
|
||
* @type {(function(Date): string)}
|
||
*/
|
||
this.formatDate = null;
|
||
|
||
/**
|
||
* Function that converts a date string to a Date object (the date portion)
|
||
* @type {function(string): Date}
|
||
*/
|
||
this.parseDate = null;
|
||
|
||
/**
|
||
* Function that formats a Date into a month header string.
|
||
* @type {function(Date): string}
|
||
*/
|
||
this.monthHeaderFormatter = null;
|
||
|
||
/**
|
||
* Function that formats a week number into a label for the week.
|
||
* @type {function(number): string}
|
||
*/
|
||
this.weekNumberFormatter = null;
|
||
|
||
/**
|
||
* Function that formats a date into a long aria-label that is read
|
||
* when the focused date changes.
|
||
* @type {function(Date): string}
|
||
*/
|
||
this.longDateFormatter = null;
|
||
|
||
/**
|
||
* Function to determine whether a string makes sense to be
|
||
* parsed to a Date object.
|
||
* @type {function(string): boolean}
|
||
*/
|
||
this.isDateComplete = null;
|
||
|
||
/**
|
||
* ARIA label for the calendar "dialog" used in the datepicker.
|
||
* @type {string}
|
||
*/
|
||
this.msgCalendar = '';
|
||
|
||
/**
|
||
* ARIA label for the datepicker's "Open calendar" buttons.
|
||
* @type {string}
|
||
*/
|
||
this.msgOpenCalendar = '';
|
||
}
|
||
|
||
/**
|
||
* Factory function that returns an instance of the dateLocale service.
|
||
* @ngInject
|
||
* @param $locale
|
||
* @param $filter
|
||
* @returns {DateLocale}
|
||
*/
|
||
DateLocaleProvider.prototype.$get = function($locale, $filter) {
|
||
/**
|
||
* Default date-to-string formatting function.
|
||
* @param {!Date} date
|
||
* @param {string=} timezone
|
||
* @returns {string}
|
||
*/
|
||
function defaultFormatDate(date, timezone) {
|
||
if (!date) {
|
||
return '';
|
||
}
|
||
|
||
// All of the dates created through ng-material *should* be set to midnight.
|
||
// If we encounter a date where the localeTime shows at 11pm instead of midnight,
|
||
// we have run into an issue with DST where we need to increment the hour by one:
|
||
// var d = new Date(1992, 9, 8, 0, 0, 0);
|
||
// d.toLocaleString(); // == "10/7/1992, 11:00:00 PM"
|
||
var localeTime = date.toLocaleTimeString();
|
||
var formatDate = date;
|
||
if (date.getHours() === 0 &&
|
||
(localeTime.indexOf('11:') !== -1 || localeTime.indexOf('23:') !== -1)) {
|
||
formatDate = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 1, 0, 0);
|
||
}
|
||
|
||
return $filter('date')(formatDate, 'M/d/yyyy', timezone);
|
||
}
|
||
|
||
/**
|
||
* Default string-to-date parsing function.
|
||
* @param {string|number} dateString
|
||
* @returns {!Date}
|
||
*/
|
||
function defaultParseDate(dateString) {
|
||
return new Date(dateString);
|
||
}
|
||
|
||
/**
|
||
* Default function to determine whether a string makes sense to be
|
||
* parsed to a Date object.
|
||
*
|
||
* This is very permissive and is just a basic check to ensure that
|
||
* things like single integers aren't able to be parsed into dates.
|
||
* @param {string} dateString
|
||
* @returns {boolean}
|
||
*/
|
||
function defaultIsDateComplete(dateString) {
|
||
dateString = dateString.trim();
|
||
|
||
// Looks for three chunks of content (either numbers or text) separated
|
||
// by delimiters.
|
||
var re = /^(([a-zA-Z]{3,}|[0-9]{1,4})([ .,]+|[/-])){2}([a-zA-Z]{3,}|[0-9]{1,4})$/;
|
||
return re.test(dateString);
|
||
}
|
||
|
||
/**
|
||
* Default date-to-string formatter to get a month header.
|
||
* @param {!Date} date
|
||
* @returns {string}
|
||
*/
|
||
function defaultMonthHeaderFormatter(date) {
|
||
return service.shortMonths[date.getMonth()] + ' ' + date.getFullYear();
|
||
}
|
||
|
||
/**
|
||
* Default formatter for a month.
|
||
* @param {!Date} date
|
||
* @returns {string}
|
||
*/
|
||
function defaultMonthFormatter(date) {
|
||
return service.months[date.getMonth()] + ' ' + date.getFullYear();
|
||
}
|
||
|
||
/**
|
||
* Default week number formatter.
|
||
* @param number
|
||
* @returns {string}
|
||
*/
|
||
function defaultWeekNumberFormatter(number) {
|
||
return 'Week ' + number;
|
||
}
|
||
|
||
/**
|
||
* Default formatter for date cell aria-labels.
|
||
* @param {!Date} date
|
||
* @returns {string}
|
||
*/
|
||
function defaultLongDateFormatter(date) {
|
||
// Example: 'Thursday June 18 2015'
|
||
return [
|
||
service.days[date.getDay()],
|
||
service.months[date.getMonth()],
|
||
service.dates[date.getDate()],
|
||
date.getFullYear()
|
||
].join(' ');
|
||
}
|
||
|
||
// The default "short" day strings are the first character of each day,
|
||
// e.g., "Monday" => "M".
|
||
var defaultShortDays = $locale.DATETIME_FORMATS.SHORTDAY.map(function(day) {
|
||
return day.substring(0, 1);
|
||
});
|
||
|
||
// The default dates are simply the numbers 1 through 31.
|
||
var defaultDates = Array(32);
|
||
for (var i = 1; i <= 31; i++) {
|
||
defaultDates[i] = i;
|
||
}
|
||
|
||
// Default ARIA messages are in English (US).
|
||
var defaultMsgCalendar = 'Calendar';
|
||
var defaultMsgOpenCalendar = 'Open calendar';
|
||
|
||
// Default start/end dates that are rendered in the calendar.
|
||
var defaultFirstRenderableDate = new Date(1880, 0, 1);
|
||
var defaultLastRendereableDate = new Date(defaultFirstRenderableDate.getFullYear() + 250, 0, 1);
|
||
|
||
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,
|
||
monthFormatter: this.monthFormatter || defaultMonthFormatter,
|
||
weekNumberFormatter: this.weekNumberFormatter || defaultWeekNumberFormatter,
|
||
longDateFormatter: this.longDateFormatter || defaultLongDateFormatter,
|
||
msgCalendar: this.msgCalendar || defaultMsgCalendar,
|
||
msgOpenCalendar: this.msgOpenCalendar || defaultMsgOpenCalendar,
|
||
firstRenderableDate: this.firstRenderableDate || defaultFirstRenderableDate,
|
||
lastRenderableDate: this.lastRenderableDate || defaultLastRendereableDate
|
||
};
|
||
|
||
return service;
|
||
};
|
||
DateLocaleProvider.prototype.$get.$inject = ["$locale", "$filter"];
|
||
|
||
$provide.provider('$mdDateLocale', new DateLocaleProvider());
|
||
}]);
|
||
})();
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
(function() {
|
||
'use strict';
|
||
|
||
/**
|
||
* Utility for performing date calculations to facilitate operation of the calendar and
|
||
* datepicker.
|
||
*/
|
||
angular.module('material.components.datepicker').factory('$$mdDateUtil', ["$mdDateLocale", function($mdDateLocale) {
|
||
return {
|
||
getFirstDateOfMonth: getFirstDateOfMonth,
|
||
getNumberOfDaysInMonth: getNumberOfDaysInMonth,
|
||
getDateInNextMonth: getDateInNextMonth,
|
||
getDateInPreviousMonth: getDateInPreviousMonth,
|
||
isInNextMonth: isInNextMonth,
|
||
isInPreviousMonth: isInPreviousMonth,
|
||
getDateMidpoint: getDateMidpoint,
|
||
isSameMonthAndYear: isSameMonthAndYear,
|
||
getWeekOfMonth: getWeekOfMonth,
|
||
incrementDays: incrementDays,
|
||
incrementMonths: incrementMonths,
|
||
getLastDateOfMonth: getLastDateOfMonth,
|
||
isSameDay: isSameDay,
|
||
getMonthDistance: getMonthDistance,
|
||
isValidDate: isValidDate,
|
||
setDateTimeToMidnight: setDateTimeToMidnight,
|
||
createDateAtMidnight: createDateAtMidnight,
|
||
isDateWithinRange: isDateWithinRange,
|
||
incrementYears: incrementYears,
|
||
getYearDistance: getYearDistance,
|
||
clampDate: clampDate,
|
||
getTimestampFromNode: getTimestampFromNode,
|
||
isMonthWithinRange: isMonthWithinRange,
|
||
removeLocalTzAndReparseDate: removeLocalTzAndReparseDate
|
||
};
|
||
|
||
/**
|
||
* Gets the first day of the month for the given date's month.
|
||
* @param {Date} date
|
||
* @returns {Date}
|
||
*/
|
||
function getFirstDateOfMonth(date) {
|
||
return new Date(date.getFullYear(), date.getMonth(), 1);
|
||
}
|
||
|
||
/**
|
||
* Gets the number of days in the month for the given date's month.
|
||
* @param date
|
||
* @returns {number}
|
||
*/
|
||
function getNumberOfDaysInMonth(date) {
|
||
return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
|
||
}
|
||
|
||
/**
|
||
* Get an arbitrary date in the month after the given date's month.
|
||
* @param date
|
||
* @returns {Date}
|
||
*/
|
||
function getDateInNextMonth(date) {
|
||
return new Date(date.getFullYear(), date.getMonth() + 1, 1);
|
||
}
|
||
|
||
/**
|
||
* Get an arbitrary date in the month before the given date's month.
|
||
* @param date
|
||
* @returns {Date}
|
||
*/
|
||
function getDateInPreviousMonth(date) {
|
||
return new Date(date.getFullYear(), date.getMonth() - 1, 1);
|
||
}
|
||
|
||
/**
|
||
* Gets whether two dates have the same month and year.
|
||
* @param {Date} d1
|
||
* @param {Date} d2
|
||
* @returns {boolean}
|
||
*/
|
||
function isSameMonthAndYear(d1, d2) {
|
||
return d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth();
|
||
}
|
||
|
||
/**
|
||
* Gets whether two dates are the same day (not not necessarily the same time).
|
||
* @param {Date} d1
|
||
* @param {Date} d2
|
||
* @returns {boolean}
|
||
*/
|
||
function isSameDay(d1, d2) {
|
||
return d1.getDate() == d2.getDate() && isSameMonthAndYear(d1, d2);
|
||
}
|
||
|
||
/**
|
||
* Gets whether a date is in the month immediately after some date.
|
||
* @param {Date} startDate The date from which to compare.
|
||
* @param {Date} endDate The date to check.
|
||
* @returns {boolean}
|
||
*/
|
||
function isInNextMonth(startDate, endDate) {
|
||
var nextMonth = getDateInNextMonth(startDate);
|
||
return isSameMonthAndYear(nextMonth, endDate);
|
||
}
|
||
|
||
/**
|
||
* Gets whether a date is in the month immediately before some date.
|
||
* @param {Date} startDate The date from which to compare.
|
||
* @param {Date} endDate The date to check.
|
||
* @returns {boolean}
|
||
*/
|
||
function isInPreviousMonth(startDate, endDate) {
|
||
var previousMonth = getDateInPreviousMonth(startDate);
|
||
return isSameMonthAndYear(endDate, previousMonth);
|
||
}
|
||
|
||
/**
|
||
* Gets the midpoint between two dates.
|
||
* @param {Date} d1
|
||
* @param {Date} d2
|
||
* @returns {Date}
|
||
*/
|
||
function getDateMidpoint(d1, d2) {
|
||
return createDateAtMidnight((d1.getTime() + d2.getTime()) / 2);
|
||
}
|
||
|
||
/**
|
||
* Gets the week of the month that a given date occurs in.
|
||
* @param {Date} date
|
||
* @returns {number} Index of the week of the month (zero-based).
|
||
*/
|
||
function getWeekOfMonth(date) {
|
||
var firstDayOfMonth = getFirstDateOfMonth(date);
|
||
return Math.floor((firstDayOfMonth.getDay() + date.getDate() - 1) / 7);
|
||
}
|
||
|
||
/**
|
||
* Gets a new date incremented by the given number of days. Number of days can be negative.
|
||
* @param {Date} date
|
||
* @param {number} numberOfDays
|
||
* @returns {Date}
|
||
*/
|
||
function incrementDays(date, numberOfDays) {
|
||
return new Date(date.getFullYear(), date.getMonth(), date.getDate() + numberOfDays);
|
||
}
|
||
|
||
/**
|
||
* Gets a new date incremented by the given number of months. Number of months can be negative.
|
||
* If the date of the given month does not match the target month, the date will be set to the
|
||
* last day of the month.
|
||
* @param {Date} date
|
||
* @param {number} numberOfMonths
|
||
* @returns {Date}
|
||
*/
|
||
function incrementMonths(date, numberOfMonths) {
|
||
// If the same date in the target month does not actually exist, the Date object will
|
||
// automatically advance *another* month by the number of missing days.
|
||
// For example, if you try to go from Jan. 30 to Feb. 30, you'll end up on March 2.
|
||
// So, we check if the month overflowed and go to the last day of the target month instead.
|
||
var dateInTargetMonth = new Date(date.getFullYear(), date.getMonth() + numberOfMonths, 1);
|
||
var numberOfDaysInMonth = getNumberOfDaysInMonth(dateInTargetMonth);
|
||
if (numberOfDaysInMonth < date.getDate()) {
|
||
dateInTargetMonth.setDate(numberOfDaysInMonth);
|
||
} else {
|
||
dateInTargetMonth.setDate(date.getDate());
|
||
}
|
||
|
||
return dateInTargetMonth;
|
||
}
|
||
|
||
/**
|
||
* Get the integer distance between two months. This *only* considers the month and year
|
||
* portion of the Date instances.
|
||
*
|
||
* @param {Date} start
|
||
* @param {Date} end
|
||
* @returns {number} Number of months between `start` and `end`. If `end` is before `start`
|
||
* chronologically, this number will be negative.
|
||
*/
|
||
function getMonthDistance(start, end) {
|
||
return (12 * (end.getFullYear() - start.getFullYear())) + (end.getMonth() - start.getMonth());
|
||
}
|
||
|
||
/**
|
||
* Gets the last day of the month for the given date.
|
||
* @param {Date} date
|
||
* @returns {Date}
|
||
*/
|
||
function getLastDateOfMonth(date) {
|
||
return new Date(date.getFullYear(), date.getMonth(), getNumberOfDaysInMonth(date));
|
||
}
|
||
|
||
/**
|
||
* Checks whether a date is valid.
|
||
* @param {Date} date
|
||
* @return {boolean} Whether the date is a valid Date.
|
||
*/
|
||
function isValidDate(date) {
|
||
return date && date.getTime && !isNaN(date.getTime());
|
||
}
|
||
|
||
/**
|
||
* Sets a date's time to midnight.
|
||
* @param {Date} date
|
||
*/
|
||
function setDateTimeToMidnight(date) {
|
||
if (isValidDate(date)) {
|
||
date.setHours(0, 0, 0, 0);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Creates a date with the time set to midnight.
|
||
* Drop-in replacement for two forms of the Date constructor via opt_value.
|
||
* @param {number|Date=} opt_value Leave undefined for a Date representing now. Or use a
|
||
* single value representing the number of seconds since the Unix Epoch or a Date object.
|
||
* @return {Date} New date with time set to midnight.
|
||
*/
|
||
function createDateAtMidnight(opt_value) {
|
||
var date;
|
||
if (angular.isDate(opt_value)) {
|
||
date = opt_value;
|
||
} else if (angular.isNumber(opt_value)) {
|
||
date = new Date(opt_value);
|
||
} else {
|
||
date = new Date();
|
||
}
|
||
setDateTimeToMidnight(date);
|
||
return date;
|
||
}
|
||
|
||
/**
|
||
* Checks if a date is within a min and max range, ignoring the time component.
|
||
* If minDate or maxDate are not dates, they are ignored.
|
||
* @param {Date} date
|
||
* @param {Date} minDate
|
||
* @param {Date} maxDate
|
||
*/
|
||
function isDateWithinRange(date, minDate, maxDate) {
|
||
var dateAtMidnight = createDateAtMidnight(date);
|
||
var minDateAtMidnight = isValidDate(minDate) ? createDateAtMidnight(minDate) : null;
|
||
var maxDateAtMidnight = isValidDate(maxDate) ? createDateAtMidnight(maxDate) : null;
|
||
return (!minDateAtMidnight || minDateAtMidnight <= dateAtMidnight) &&
|
||
(!maxDateAtMidnight || maxDateAtMidnight >= dateAtMidnight);
|
||
}
|
||
|
||
/**
|
||
* Gets a new date incremented by the given number of years. Number of years can be negative.
|
||
* See `incrementMonths` for notes on overflow for specific dates.
|
||
* @param {Date} date
|
||
* @param {number} numberOfYears
|
||
* @returns {Date}
|
||
*/
|
||
function incrementYears(date, numberOfYears) {
|
||
return incrementMonths(date, numberOfYears * 12);
|
||
}
|
||
|
||
/**
|
||
* Get the integer distance between two years. This *only* considers the year portion of the
|
||
* Date instances.
|
||
*
|
||
* @param {Date} start
|
||
* @param {Date} end
|
||
* @returns {number} Number of months between `start` and `end`. If `end` is before `start`
|
||
* chronologically, this number will be negative.
|
||
*/
|
||
function getYearDistance(start, end) {
|
||
return end.getFullYear() - start.getFullYear();
|
||
}
|
||
|
||
/**
|
||
* Clamps a date between a minimum and a maximum date.
|
||
* @param {Date} date Date to be clamped
|
||
* @param {Date=} minDate Minimum date
|
||
* @param {Date=} maxDate Maximum date
|
||
* @return {Date}
|
||
*/
|
||
function clampDate(date, minDate, maxDate) {
|
||
var boundDate = date;
|
||
if (minDate && date < minDate) {
|
||
boundDate = new Date(minDate.getTime());
|
||
}
|
||
if (maxDate && date > maxDate) {
|
||
boundDate = new Date(maxDate.getTime());
|
||
}
|
||
return boundDate;
|
||
}
|
||
|
||
/**
|
||
* Extracts and parses the timestamp from a DOM node.
|
||
* @param {HTMLElement} node Node from which the timestamp will be extracted.
|
||
* @return {number} Time since epoch.
|
||
*/
|
||
function getTimestampFromNode(node) {
|
||
if (node && node.hasAttribute('data-timestamp')) {
|
||
return Number(node.getAttribute('data-timestamp'));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Checks if a month is within a min and max range, ignoring the date and time components.
|
||
* If minDate or maxDate are not dates, they are ignored.
|
||
* @param {Date} date
|
||
* @param {Date} minDate
|
||
* @param {Date} maxDate
|
||
*/
|
||
function isMonthWithinRange(date, minDate, maxDate) {
|
||
var month = date.getMonth();
|
||
var year = date.getFullYear();
|
||
|
||
return (!minDate || minDate.getFullYear() < year || minDate.getMonth() <= month) &&
|
||
(!maxDate || maxDate.getFullYear() > year || maxDate.getMonth() >= month);
|
||
}
|
||
|
||
/**
|
||
* @param {Date} value date in local timezone
|
||
* @return {Date} date with local timezone offset removed
|
||
*/
|
||
function removeLocalTzAndReparseDate(value) {
|
||
var dateValue, formattedDate;
|
||
// Remove the local timezone offset before calling formatDate.
|
||
dateValue = new Date(value.getTime() + 60000 * value.getTimezoneOffset());
|
||
formattedDate = $mdDateLocale.formatDate(dateValue);
|
||
// parseDate only works with a date formatted by formatDate when using Moment validation.
|
||
return $mdDateLocale.parseDate(formattedDate);
|
||
}
|
||
}]);
|
||
})();
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
(function() {
|
||
'use strict';
|
||
|
||
// 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?)
|
||
|
||
DatePickerCtrl.$inject = ["$scope", "$element", "$attrs", "$window", "$mdConstant", "$mdTheming", "$mdUtil", "$mdDateLocale", "$$mdDateUtil", "$$rAF", "$filter", "$timeout"];
|
||
datePickerDirective.$inject = ["$$mdSvgRegistry", "$mdUtil", "$mdAria", "inputDirective"];
|
||
angular.module('material.components.datepicker')
|
||
.directive('mdDatepicker', datePickerDirective);
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdDatepicker
|
||
* @module material.components.datepicker
|
||
*
|
||
* @param {Date} ng-model The component's model. Expects either a JavaScript Date object or a
|
||
* value that can be parsed into one (e.g. a ISO 8601 string).
|
||
* @param {Object=} ng-model-options Allows tuning of the way in which `ng-model` is being
|
||
* updated. Also allows for a timezone to be specified.
|
||
* <a href="https://docs.angularjs.org/api/ng/directive/ngModelOptions#usage">
|
||
* Read more at the ngModelOptions docs.</a>
|
||
* @param {expression=} ng-change Expression evaluated when the model value changes.
|
||
* @param {expression=} ng-focus Expression evaluated when the input is focused or the calendar
|
||
* is opened.
|
||
* @param {expression=} ng-blur Expression evaluated when focus is removed from the input or the
|
||
* calendar is closed.
|
||
* @param {boolean=} ng-disabled Whether the datepicker is disabled.
|
||
* @param {boolean=} ng-required Whether a value is required for the datepicker.
|
||
* @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 in "day" mode or not. Returning false will also trigger a
|
||
* `filtered` model validation error.
|
||
* @param {(function(Date): boolean)=} md-month-filter Function expecting a date and returning a
|
||
* boolean whether it can be selected in "month" mode or not. Returning false will also trigger a
|
||
* `filtered` model validation error.
|
||
* @param {string=} md-placeholder The date input placeholder value.
|
||
* @param {string=} md-open-on-focus When present, the calendar will be opened when the input
|
||
* is focused.
|
||
* @param {Boolean=} md-is-open Expression that can be used to open the datepicker's calendar
|
||
* on-demand.
|
||
* @param {string=} md-current-view Default open view of the calendar pane. Can be either
|
||
* "month" or "year".
|
||
* @param {string=} md-mode Restricts the user to only selecting a value from a particular view.
|
||
* This option can be used if the user is only supposed to choose from a certain date type
|
||
* (e.g. only selecting the month).
|
||
* Can be either "month" or "day". **Note** that this will overwrite the `md-current-view` value.
|
||
* @param {string=} md-hide-icons Determines which datepicker icons should be hidden. Note that
|
||
* this may cause the datepicker to not align properly with other components.
|
||
* **Use at your own risk.** Possible values are:
|
||
* * `"all"` - Hides all icons.
|
||
* * `"calendar"` - Only hides the calendar icon.
|
||
* * `"triangle"` - Only hides the triangle icon.
|
||
* @param {Object=} md-date-locale Allows for the values from the `$mdDateLocaleProvider` to be
|
||
* overwritten on a per-element basis (e.g. `msgOpenCalendar` can be overwritten with
|
||
* `md-date-locale="{ msgOpenCalendar: 'Open a special calendar' }"`).
|
||
* @param {string=} input-aria-describedby A space-separated list of element IDs. This should
|
||
* contain the IDs of any elements that describe this datepicker. Screen readers will read the
|
||
* content of these elements at the end of announcing that the datepicker has been selected
|
||
* and describing its current state. The descriptive elements do not need to be visible on the
|
||
* page.
|
||
* @param {string=} input-aria-labelledby A space-separated list of element IDs. The ideal use
|
||
* case is that this would contain the ID of a `<label>` element should be associated with this
|
||
* datepicker. This is necessary when using `md-datepicker` inside of an `md-input-container`
|
||
* with a `<label>`.<br><br>
|
||
* For `<label id="start-date">Start Date</label>`, you would set this to
|
||
* `input-aria-labelledby="start-date"`.
|
||
*
|
||
* @description
|
||
* `<md-datepicker>` is a component used to select a single date.
|
||
* For information on how to configure internationalization for the date picker,
|
||
* see <a ng-href="api/service/$mdDateLocaleProvider">$mdDateLocaleProvider</a>.
|
||
*
|
||
* 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.
|
||
* * `debounceInterval`: ms to delay input processing (since last debounce reset);
|
||
* default value 500ms
|
||
*
|
||
* @usage
|
||
* <hljs lang="html">
|
||
* <md-datepicker ng-model="birthday"></md-datepicker>
|
||
* </hljs>
|
||
*
|
||
*/
|
||
|
||
function datePickerDirective($$mdSvgRegistry, $mdUtil, $mdAria, inputDirective) {
|
||
return {
|
||
template: function(tElement, tAttrs) {
|
||
// 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.
|
||
var hiddenIcons = tAttrs.mdHideIcons;
|
||
var inputAriaDescribedby = tAttrs.inputAriaDescribedby;
|
||
var inputAriaLabelledby = tAttrs.inputAriaLabelledby;
|
||
var ariaLabelValue = tAttrs.ariaLabel || tAttrs.mdPlaceholder;
|
||
var ngModelOptions = tAttrs.ngModelOptions;
|
||
|
||
var calendarButton = (hiddenIcons === 'all' || hiddenIcons === 'calendar') ? '' :
|
||
'<md-button class="md-datepicker-button md-icon-button" type="button" ' +
|
||
'tabindex="-1" aria-hidden="true" ' +
|
||
'ng-click="ctrl.openCalendarPane($event)">' +
|
||
'<md-icon class="md-datepicker-calendar-icon" aria-label="md-calendar" ' +
|
||
'md-svg-src="' + $$mdSvgRegistry.mdCalendar + '"></md-icon>' +
|
||
'</md-button>';
|
||
|
||
var triangleButton = '';
|
||
|
||
if (hiddenIcons !== 'all' && hiddenIcons !== 'triangle') {
|
||
triangleButton = '' +
|
||
'<md-button type="button" md-no-ink ' +
|
||
'class="md-datepicker-triangle-button md-icon-button" ' +
|
||
'ng-click="ctrl.openCalendarPane($event)" ' +
|
||
'aria-label="{{::ctrl.locale.msgOpenCalendar}}">' +
|
||
'<div class="md-datepicker-expand-triangle"></div>' +
|
||
'</md-button>';
|
||
|
||
tElement.addClass(HAS_TRIANGLE_ICON_CLASS);
|
||
}
|
||
|
||
return calendarButton +
|
||
'<div class="md-datepicker-input-container" ng-class="{\'md-datepicker-focused\': ctrl.isFocused}">' +
|
||
'<input ' +
|
||
(ariaLabelValue ? 'aria-label="' + ariaLabelValue + '" ' : '') +
|
||
(inputAriaDescribedby ? 'aria-describedby="' + inputAriaDescribedby + '" ' : '') +
|
||
(inputAriaLabelledby ? 'aria-labelledby="' + inputAriaLabelledby + '" ' : '') +
|
||
'class="md-datepicker-input" ' +
|
||
'aria-haspopup="dialog" ' +
|
||
'ng-focus="ctrl.setFocused(true)" ' +
|
||
'ng-blur="ctrl.setFocused(false)"> ' +
|
||
triangleButton +
|
||
'</div>' +
|
||
|
||
// This pane will be detached from here and re-attached to the document body.
|
||
'<div class="md-datepicker-calendar-pane md-whiteframe-z1" id="{{::ctrl.calendarPaneId}}">' +
|
||
'<div class="md-datepicker-input-mask">' +
|
||
'<div class="md-datepicker-input-mask-opaque"></div>' +
|
||
'</div>' +
|
||
'<div class="md-datepicker-calendar">' +
|
||
'<md-calendar role="dialog" aria-label="{{::ctrl.locale.msgCalendar}}" ' +
|
||
'md-current-view="{{::ctrl.currentView}}" ' +
|
||
'md-mode="{{::ctrl.mode}}" ' +
|
||
'md-min-date="ctrl.minDate" ' +
|
||
'md-max-date="ctrl.maxDate" ' +
|
||
'md-date-filter="ctrl.dateFilter" ' +
|
||
'md-month-filter="ctrl.monthFilter" ' +
|
||
(ngModelOptions ? 'ng-model-options="' + ngModelOptions + '" ' : '') +
|
||
'ng-model="ctrl.date" ng-if="ctrl.isCalendarOpen">' +
|
||
'</md-calendar>' +
|
||
'</div>' +
|
||
'</div>';
|
||
},
|
||
require: ['ngModel', 'mdDatepicker', '?^mdInputContainer', '?^form'],
|
||
scope: {
|
||
minDate: '=mdMinDate',
|
||
maxDate: '=mdMaxDate',
|
||
placeholder: '@mdPlaceholder',
|
||
currentView: '@mdCurrentView',
|
||
mode: '@mdMode',
|
||
dateFilter: '=mdDateFilter',
|
||
monthFilter: '=mdMonthFilter',
|
||
isOpen: '=?mdIsOpen',
|
||
debounceInterval: '=mdDebounceInterval',
|
||
dateLocale: '=mdDateLocale'
|
||
},
|
||
controller: DatePickerCtrl,
|
||
controllerAs: 'ctrl',
|
||
bindToController: true,
|
||
link: function(scope, element, attr, controllers) {
|
||
var ngModelCtrl = controllers[0];
|
||
var mdDatePickerCtrl = controllers[1];
|
||
var mdInputContainer = controllers[2];
|
||
var parentForm = controllers[3];
|
||
var mdNoAsterisk = $mdUtil.parseAttributeBoolean(attr.mdNoAsterisk);
|
||
|
||
mdDatePickerCtrl.configureNgModel(ngModelCtrl, mdInputContainer, inputDirective);
|
||
|
||
if (mdInputContainer) {
|
||
// We need to move the spacer after the datepicker itself,
|
||
// because md-input-container adds it after the
|
||
// md-datepicker-input by default. The spacer gets wrapped in a
|
||
// div, because it floats and gets aligned next to the datepicker.
|
||
// There are easier ways of working around this with CSS (making the
|
||
// datepicker 100% wide, change the `display` etc.), however they
|
||
// break the alignment with any other form controls.
|
||
var spacer = element[0].querySelector('.md-errors-spacer');
|
||
|
||
if (spacer) {
|
||
element.after(angular.element('<div>').append(spacer));
|
||
}
|
||
|
||
mdInputContainer.setHasPlaceholder(attr.mdPlaceholder);
|
||
mdInputContainer.input = element;
|
||
mdInputContainer.element
|
||
.addClass(INPUT_CONTAINER_CLASS)
|
||
.toggleClass(HAS_CALENDAR_ICON_CLASS,
|
||
attr.mdHideIcons !== 'calendar' && attr.mdHideIcons !== 'all');
|
||
|
||
if (!mdInputContainer.label) {
|
||
$mdAria.expect(element, 'aria-label', attr.mdPlaceholder);
|
||
} else if (!mdNoAsterisk) {
|
||
attr.$observe('required', function(value) {
|
||
mdInputContainer.label.toggleClass('md-required', !!value);
|
||
});
|
||
}
|
||
|
||
scope.$watch(mdInputContainer.isErrorGetter || function() {
|
||
return ngModelCtrl.$invalid && (ngModelCtrl.$touched ||
|
||
(parentForm && parentForm.$submitted));
|
||
}, mdInputContainer.setInvalid);
|
||
} else if (parentForm) {
|
||
// If invalid, highlights the input when the parent form is submitted.
|
||
var parentSubmittedWatcher = scope.$watch(function() {
|
||
return parentForm.$submitted;
|
||
}, function(isSubmitted) {
|
||
if (isSubmitted) {
|
||
mdDatePickerCtrl.updateErrorState();
|
||
parentSubmittedWatcher();
|
||
}
|
||
});
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
/** 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';
|
||
|
||
/** Class applied to the datepicker when it's open. */
|
||
var OPEN_CLASS = 'md-datepicker-open';
|
||
|
||
/** Class applied to the md-input-container, if a datepicker is placed inside it */
|
||
var INPUT_CONTAINER_CLASS = '_md-datepicker-floating-label';
|
||
|
||
/** Class to be applied when the calendar icon is enabled. */
|
||
var HAS_CALENDAR_ICON_CLASS = '_md-datepicker-has-calendar-icon';
|
||
|
||
/** Class to be applied when the triangle icon is enabled. */
|
||
var HAS_TRIANGLE_ICON_CLASS = '_md-datepicker-has-triangle-icon';
|
||
|
||
/** 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;
|
||
|
||
/** Used for checking whether the current user agent is on iOS or Android. */
|
||
var IS_MOBILE_REGEX = /ipad|iphone|ipod|android/i;
|
||
|
||
/**
|
||
* Controller for md-datepicker.
|
||
*
|
||
* @ngInject @constructor
|
||
*/
|
||
function DatePickerCtrl($scope, $element, $attrs, $window, $mdConstant, $mdTheming, $mdUtil,
|
||
$mdDateLocale, $$mdDateUtil, $$rAF, $filter, $timeout) {
|
||
|
||
/** @final */
|
||
this.$window = $window;
|
||
|
||
/** @final */
|
||
this.dateUtil = $$mdDateUtil;
|
||
|
||
/** @final */
|
||
this.$mdConstant = $mdConstant;
|
||
|
||
/** @final */
|
||
this.$mdUtil = $mdUtil;
|
||
|
||
/** @final */
|
||
this.$$rAF = $$rAF;
|
||
|
||
/** @final */
|
||
this.$mdDateLocale = $mdDateLocale;
|
||
|
||
/** @final */
|
||
this.$timeout = $timeout;
|
||
|
||
/**
|
||
* 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 {!JQLite}
|
||
*/
|
||
this.documentElement = angular.element(document.documentElement);
|
||
|
||
/** @type {!ngModel.NgModelController} */
|
||
this.ngModelCtrl = null;
|
||
|
||
/** @type {HTMLInputElement} */
|
||
this.inputElement = $element[0].querySelector('input');
|
||
|
||
/**
|
||
* @final
|
||
* @type {!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 {!JQLite}
|
||
*/
|
||
this.inputMask = angular.element($element[0].querySelector('.md-datepicker-input-mask-opaque'));
|
||
|
||
/**
|
||
* @final
|
||
* @type {!JQLite}
|
||
*/
|
||
this.$element = $element;
|
||
|
||
/**
|
||
* @final
|
||
* @type {!angular.Attributes}
|
||
*/
|
||
this.$attrs = $attrs;
|
||
|
||
/**
|
||
* @final
|
||
* @type {!angular.Scope}
|
||
*/
|
||
this.$scope = $scope;
|
||
|
||
/**
|
||
* This holds the model that will be used by the calendar.
|
||
* @type {Date|null|undefined}
|
||
*/
|
||
this.date = null;
|
||
|
||
/** @type {boolean} */
|
||
this.isFocused = false;
|
||
|
||
/** @type {boolean} */
|
||
this.isDisabled = undefined;
|
||
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');
|
||
|
||
/** @type {Object} Instance of the mdInputContainer controller */
|
||
this.mdInputContainer = null;
|
||
|
||
/**
|
||
* 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;
|
||
|
||
/** @type {String} Unique id for the calendar pane. */
|
||
this.calendarPaneId = 'md-date-pane-' + $mdUtil.nextUid();
|
||
|
||
/** Pre-bound click handler is saved so that the event listener can be removed. */
|
||
this.bodyClickHandler = angular.bind(this, this.handleBodyClick);
|
||
|
||
/**
|
||
* Name of the event that will trigger a close. Necessary to sniff the browser, because
|
||
* the resize event doesn't make sense on mobile and can have a negative impact since it
|
||
* triggers whenever the browser zooms in on a focused input.
|
||
*/
|
||
this.windowEventName = IS_MOBILE_REGEX.test(
|
||
navigator.userAgent || navigator.vendor || window.opera
|
||
) ? 'orientationchange' : 'resize';
|
||
|
||
/** Pre-bound close handler so that the event listener can be removed. */
|
||
this.windowEventHandler = $mdUtil.debounce(angular.bind(this, this.closeCalendarPane), 100);
|
||
|
||
/** Pre-bound handler for the window blur event. Allows for it to be removed later. */
|
||
this.windowBlurHandler = angular.bind(this, this.handleWindowBlur);
|
||
|
||
/** The built-in AngularJS date filter. */
|
||
this.ngDateFilter = $filter('date');
|
||
|
||
/** @type {Number} Extra margin for the left side of the floating calendar pane. */
|
||
this.leftMargin = 20;
|
||
|
||
/** @type {Number} Extra margin for the top of the floating calendar. Gets determined on the first open. */
|
||
this.topMargin = null;
|
||
|
||
// 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) {
|
||
this.ngInputElement.attr('tabindex', $attrs.tabindex);
|
||
$attrs.$set('tabindex', null);
|
||
} else {
|
||
$attrs.$set('tabindex', '-1');
|
||
}
|
||
|
||
$attrs.$set('aria-owns', this.calendarPaneId);
|
||
|
||
$mdTheming($element);
|
||
$mdTheming(angular.element(this.calendarPane));
|
||
|
||
var self = this;
|
||
|
||
$scope.$on('$destroy', function() {
|
||
self.detachCalendarPane();
|
||
});
|
||
|
||
if ($attrs.mdIsOpen) {
|
||
$scope.$watch('ctrl.isOpen', function(shouldBeOpen) {
|
||
if (shouldBeOpen) {
|
||
self.openCalendarPane({
|
||
target: self.inputElement
|
||
});
|
||
} else {
|
||
self.closeCalendarPane();
|
||
}
|
||
});
|
||
}
|
||
|
||
// For AngularJS 1.4 and older, where there are no lifecycle hooks but bindings are
|
||
// pre-assigned, manually call the $onInit hook.
|
||
if (angular.version.major === 1 && angular.version.minor <= 4) {
|
||
this.$onInit();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* AngularJS Lifecycle hook for newer AngularJS versions.
|
||
* Bindings are not guaranteed to have been assigned in the controller, but they are in the
|
||
* $onInit hook.
|
||
*/
|
||
DatePickerCtrl.prototype.$onInit = function() {
|
||
|
||
/**
|
||
* Holds locale-specific formatters, parsers, labels etc. Allows
|
||
* the user to override specific ones from the $mdDateLocale provider.
|
||
* @type {!Object}
|
||
*/
|
||
this.locale = this.dateLocale ? angular.extend({}, this.$mdDateLocale, this.dateLocale)
|
||
: this.$mdDateLocale;
|
||
|
||
this.installPropertyInterceptors();
|
||
this.attachChangeListeners();
|
||
this.attachInteractionListeners();
|
||
};
|
||
|
||
/**
|
||
* Sets up the controller's reference to ngModelController and
|
||
* applies AngularJS's `input[type="date"]` directive.
|
||
* @param {!angular.NgModelController} ngModelCtrl Instance of the ngModel controller.
|
||
* @param {Object} mdInputContainer Instance of the mdInputContainer controller.
|
||
* @param {Object} inputDirective Config for AngularJS's `input` directive.
|
||
*/
|
||
DatePickerCtrl.prototype.configureNgModel = function(ngModelCtrl, mdInputContainer, inputDirective) {
|
||
this.ngModelCtrl = ngModelCtrl;
|
||
this.mdInputContainer = mdInputContainer;
|
||
|
||
// The input needs to be [type="date"] in order to be picked up by AngularJS.
|
||
this.$attrs.$set('type', 'date');
|
||
|
||
// Invoke the `input` directive link function, adding a stub for the element.
|
||
// This allows us to re-use AngularJS's logic for setting the timezone via ng-model-options.
|
||
// It works by calling the link function directly which then adds the proper `$parsers` and
|
||
// `$formatters` to the ngModel controller.
|
||
inputDirective[0].link.pre(this.$scope, {
|
||
on: angular.noop,
|
||
val: angular.noop,
|
||
0: {}
|
||
}, this.$attrs, [ngModelCtrl]);
|
||
|
||
var self = this;
|
||
|
||
// Responds to external changes to the model value.
|
||
self.ngModelCtrl.$formatters.push(function(value) {
|
||
var parsedValue = angular.isDefined(value) ? value : null;
|
||
|
||
if (!(value instanceof Date)) {
|
||
parsedValue = Date.parse(value);
|
||
|
||
// `parsedValue` is the time since epoch if valid or `NaN` if invalid.
|
||
if (!isNaN(parsedValue) && angular.isNumber(parsedValue)) {
|
||
value = new Date(parsedValue);
|
||
}
|
||
|
||
if (value && !(value instanceof Date)) {
|
||
throw Error(
|
||
'The ng-model for md-datepicker must be a Date instance or a value ' +
|
||
'that can be parsed into a date. Currently the model is of type: ' + typeof value
|
||
);
|
||
}
|
||
}
|
||
|
||
self.onExternalChange(value);
|
||
|
||
return value;
|
||
});
|
||
|
||
// Responds to external error state changes (e.g. ng-required based on another input).
|
||
ngModelCtrl.$viewChangeListeners.unshift(angular.bind(this, this.updateErrorState));
|
||
|
||
// Forwards any events from the input to the root element. This is necessary to get `updateOn`
|
||
// working for events that don't bubble (e.g. 'blur') since AngularJS binds the handlers to
|
||
// the `<md-datepicker>`.
|
||
var updateOn = self.$mdUtil.getModelOption(ngModelCtrl, 'updateOn');
|
||
|
||
if (updateOn) {
|
||
this.ngInputElement.on(
|
||
updateOn,
|
||
angular.bind(this.$element, this.$element.triggerHandler, updateOn)
|
||
);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 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.setModelValue(date);
|
||
self.onExternalChange(date);
|
||
self.closeCalendarPane();
|
||
});
|
||
|
||
self.ngInputElement.on('input', angular.bind(self, self.resizeInputElement));
|
||
|
||
var debounceInterval = angular.isDefined(this.debounceInterval) ?
|
||
this.debounceInterval : DEFAULT_DEBOUNCE_INTERVAL;
|
||
self.ngInputElement.on('input', self.$mdUtil.debounce(self.handleInputEvent,
|
||
debounceInterval, 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));
|
||
self.ngInputElement.on('click', function(event) {
|
||
event.stopPropagation();
|
||
});
|
||
self.ngInputElement.on('pointerdown',function(event) {
|
||
if (event.target && event.target.setPointerCapture) {
|
||
event.target.setPointerCapture(event.pointerId);
|
||
}
|
||
});
|
||
|
||
angular.element(self.$window).on('blur', self.windowBlurHandler);
|
||
|
||
$scope.$on('$destroy', function() {
|
||
angular.element(self.$window).off('blur', self.windowBlurHandler);
|
||
});
|
||
}
|
||
|
||
$scope.$on('md-calendar-close', function() {
|
||
self.closeCalendarPane();
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Capture properties set to the date-picker and imperatively 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;
|
||
|
||
if (this.calendarButton) {
|
||
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;
|
||
if (opt_date) {
|
||
date = new Date(opt_date.valueOf());
|
||
} else {
|
||
if (angular.isString(this.ngModelCtrl.$modelValue)) {
|
||
date = new Date(this.ngModelCtrl.$modelValue);
|
||
} else {
|
||
date = angular.copy(this.ngModelCtrl.$modelValue);
|
||
}
|
||
}
|
||
|
||
// 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));
|
||
}
|
||
|
||
if (angular.isFunction(this.monthFilter)) {
|
||
this.ngModelCtrl.$setValidity('filtered', this.monthFilter(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);
|
||
}
|
||
|
||
var input = this.inputElement.value;
|
||
var parsedDate = this.locale.parseDate(input);
|
||
|
||
if (!this.isInputValid(input, parsedDate) && this.ngModelCtrl.$valid) {
|
||
this.ngModelCtrl.$setValidity('valid', date == null);
|
||
}
|
||
|
||
angular.element(this.inputContainer).toggleClass(INVALID_CLASS,
|
||
this.ngModelCtrl.$invalid && (this.ngModelCtrl.$touched || this.ngModelCtrl.$submitted));
|
||
};
|
||
|
||
/**
|
||
* Check to see if the input is valid, as the validation should fail if the model is invalid.
|
||
*
|
||
* @param {string} inputString
|
||
* @param {Date} parsedDate
|
||
* @return {boolean} Whether the input is valid
|
||
*/
|
||
DatePickerCtrl.prototype.isInputValid = function (inputString, parsedDate) {
|
||
return inputString === '' || (
|
||
this.dateUtil.isValidDate(parsedDate) &&
|
||
this.locale.isDateComplete(inputString) &&
|
||
this.isDateEnabled(parsedDate)
|
||
);
|
||
};
|
||
|
||
/** 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.locale.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 = this.isInputValid(inputString, parsedDate);
|
||
|
||
// The datepicker's model is only updated when there is a valid input.
|
||
if (isValidInput) {
|
||
this.setModelValue(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)) &&
|
||
(!angular.isFunction(this.monthFilter) || this.monthFilter(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(OPEN_CLASS);
|
||
this.mdInputContainer && this.mdInputContainer.element.addClass(OPEN_CLASS);
|
||
angular.element(body).addClass('md-datepicker-is-showing');
|
||
|
||
var elementRect = this.inputContainer.getBoundingClientRect();
|
||
var bodyRect = body.getBoundingClientRect();
|
||
|
||
if (!this.topMargin || this.topMargin < 0) {
|
||
this.topMargin =
|
||
(this.inputMask.parent().prop('clientHeight')
|
||
- this.ngInputElement.prop('clientHeight')) / 2;
|
||
}
|
||
|
||
// 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 - this.topMargin;
|
||
var paneLeft = elementRect.left - bodyRect.left - this.leftMargin;
|
||
|
||
// 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;
|
||
|
||
// Creates an overlay with a hole the same size as element. We remove a pixel or two
|
||
// on each end to make it overlap slightly. The overlay's background is added in
|
||
// the theme in the form of a box-shadow with a huge spread.
|
||
this.inputMask.css({
|
||
position: 'absolute',
|
||
left: this.leftMargin + 'px',
|
||
top: this.topMargin + 'px',
|
||
width: (elementRect.width - 1) + 'px',
|
||
height: (elementRect.height - 2) + 'px'
|
||
});
|
||
|
||
// 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);
|
||
|
||
// 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(OPEN_CLASS);
|
||
this.mdInputContainer && this.mdInputContainer.element.removeClass(OPEN_CLASS);
|
||
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 {MouseEvent|KeyboardEvent|{target: HTMLInputElement}} event
|
||
*/
|
||
DatePickerCtrl.prototype.openCalendarPane = function(event) {
|
||
if (!this.isCalendarOpen && !this.isDisabled && !this.inputFocusedOnWindowBlur) {
|
||
this.isCalendarOpen = this.isOpen = 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 `<select>` on Mac and Windows.
|
||
this.$mdUtil.disableScrollAround(this.calendarPane);
|
||
|
||
this.attachCalendarPane();
|
||
this.focusCalendar();
|
||
this.evalAttr('ngFocus');
|
||
|
||
// Attach click listener inside of a timeout because, if this open call was triggered by a
|
||
// click, we don't want it to be immediately propagated up to the body and handled.
|
||
var self = this;
|
||
this.$mdUtil.nextTick(function() {
|
||
// Use 'touchstart` in addition to click in order to work on iOS Safari, where click
|
||
// events aren't propagated under most circumstances.
|
||
// See http://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
|
||
self.documentElement.on('click touchstart', self.bodyClickHandler);
|
||
}, false);
|
||
|
||
window.addEventListener(this.windowEventName, this.windowEventHandler);
|
||
} else if (this.inputFocusedOnWindowBlur) {
|
||
this.resetInputFocused();
|
||
}
|
||
};
|
||
|
||
/** Close the floating calendar pane. */
|
||
DatePickerCtrl.prototype.closeCalendarPane = function() {
|
||
if (this.isCalendarOpen) {
|
||
var self = this;
|
||
|
||
self.detachCalendarPane();
|
||
self.ngModelCtrl.$setTouched();
|
||
self.evalAttr('ngBlur');
|
||
|
||
self.documentElement.off('click touchstart', self.bodyClickHandler);
|
||
window.removeEventListener(self.windowEventName, self.windowEventHandler);
|
||
|
||
self.calendarPaneOpenedFrom.focus();
|
||
self.calendarPaneOpenedFrom = null;
|
||
|
||
if (self.openOnFocus) {
|
||
// Ensures that all focus events have fired before resetting
|
||
// the calendar. Prevents the calendar from reopening immediately
|
||
// in IE when md-open-on-focus is set. Also it needs to trigger
|
||
// a digest, in order to prevent issues where the calendar wasn't
|
||
// showing up on the next open.
|
||
self.$timeout(reset);
|
||
} else {
|
||
reset();
|
||
}
|
||
}
|
||
|
||
function reset() {
|
||
self.isCalendarOpen = self.isOpen = false;
|
||
}
|
||
};
|
||
|
||
/** Gets the controller instance for the calendar in the floating pane. */
|
||
DatePickerCtrl.prototype.getCalendarCtrl = function() {
|
||
return angular.element(this.calendarPane.querySelector('md-calendar')).controller('mdCalendar');
|
||
};
|
||
|
||
/** Focus the calendar in the floating pane. */
|
||
DatePickerCtrl.prototype.focusCalendar = function() {
|
||
// Use a timeout in order to allow the calendar to be rendered, as it is gated behind an ng-if.
|
||
var self = this;
|
||
this.$mdUtil.nextTick(function() {
|
||
self.getCalendarCtrl().focusDate(self.date);
|
||
}, false);
|
||
};
|
||
|
||
/**
|
||
* Sets whether the input is currently focused.
|
||
* @param {boolean} isFocused
|
||
*/
|
||
DatePickerCtrl.prototype.setFocused = function(isFocused) {
|
||
if (!isFocused) {
|
||
this.ngModelCtrl.$setTouched();
|
||
}
|
||
|
||
// The ng* expressions shouldn't be evaluated when mdOpenOnFocus is on,
|
||
// because they also get called when the calendar is opened/closed.
|
||
if (!this.openOnFocus) {
|
||
this.evalAttr(isFocused ? 'ngFocus' : 'ngBlur');
|
||
}
|
||
|
||
this.isFocused = isFocused;
|
||
};
|
||
|
||
/**
|
||
* Handles a click on the document body when the floating calendar pane is open.
|
||
* Closes the floating calendar pane if the click is not inside of it.
|
||
* @param {MouseEvent} event
|
||
*/
|
||
DatePickerCtrl.prototype.handleBodyClick = function(event) {
|
||
if (this.isCalendarOpen) {
|
||
var isInCalendar = this.$mdUtil.getClosest(event.target, 'md-calendar');
|
||
|
||
if (!isInCalendar) {
|
||
this.closeCalendarPane();
|
||
}
|
||
|
||
this.$scope.$digest();
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Handles the event when the user navigates away from the current tab. Keeps track of
|
||
* whether the input was focused when the event happened, in order to prevent the calendar
|
||
* from re-opening.
|
||
*/
|
||
DatePickerCtrl.prototype.handleWindowBlur = function() {
|
||
this.inputFocusedOnWindowBlur = document.activeElement === this.inputElement;
|
||
};
|
||
|
||
/**
|
||
* Reset the flag inputFocusedOnWindowBlur to default state, to permit user to open calendar
|
||
* again when he back to tab with calendar focused.
|
||
*/
|
||
DatePickerCtrl.prototype.resetInputFocused = function() {
|
||
this.inputFocusedOnWindowBlur = false;
|
||
};
|
||
|
||
/**
|
||
* Evaluates an attribute expression against the parent scope.
|
||
* @param {String} attr Name of the attribute to be evaluated.
|
||
*/
|
||
DatePickerCtrl.prototype.evalAttr = function(attr) {
|
||
if (this.$attrs[attr]) {
|
||
this.$scope.$parent.$eval(this.$attrs[attr]);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Sets the ng-model value by first converting the date object into a string. Converting it
|
||
* is necessary, in order to pass AngularJS's `input[type="date"]` validations. AngularJS turns
|
||
* the value into a Date object afterwards, before setting it on the model.
|
||
* @param {Date=} value Date to be set as the model value.
|
||
*/
|
||
DatePickerCtrl.prototype.setModelValue = function(value) {
|
||
var timezone = this.$mdUtil.getModelOption(this.ngModelCtrl, 'timezone');
|
||
// Using the timezone when the offset is negative (GMT+X) causes the previous day to be
|
||
// set as the model value here. This check avoids that.
|
||
if (timezone == null || value == null || value.getTimezoneOffset() < 0) {
|
||
this.ngModelCtrl.$setViewValue(this.ngDateFilter(value, 'yyyy-MM-dd'), 'default');
|
||
} else {
|
||
this.ngModelCtrl.$setViewValue(this.ngDateFilter(value, 'yyyy-MM-dd', timezone), 'default');
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Updates the datepicker when a model change occurred externally.
|
||
* @param {Date=} value Value that was set to the model.
|
||
*/
|
||
DatePickerCtrl.prototype.onExternalChange = function(value) {
|
||
var self = this;
|
||
var timezone = this.$mdUtil.getModelOption(this.ngModelCtrl, 'timezone');
|
||
|
||
// Update the model used by the calendar.
|
||
if (this.dateUtil.isValidDate(value) && timezone != null && value.getTimezoneOffset() >= 0) {
|
||
this.date = this.dateUtil.removeLocalTzAndReparseDate(value);
|
||
} else {
|
||
this.date = value;
|
||
}
|
||
// Using the timezone when the offset is negative (GMT+X) causes the previous day to be
|
||
// used here. This check avoids that.
|
||
if (timezone == null || value == null || value.getTimezoneOffset() < 0) {
|
||
this.inputElement.value = this.locale.formatDate(value);
|
||
} else {
|
||
this.inputElement.value = this.locale.formatDate(value, timezone);
|
||
}
|
||
this.mdInputContainer && this.mdInputContainer.setHasValue(!!value);
|
||
this.resizeInputElement();
|
||
// This is often called from the $formatters section of the $validators pipeline.
|
||
// In that case, we need to delay to let $render and $validate run, so that the checks for
|
||
// error state are accurate.
|
||
this.$mdUtil.nextTick(function() {self.updateErrorState();}, false, self.$scope);
|
||
};
|
||
})();
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.dialog
|
||
*/
|
||
MdDialogDirective.$inject = ["$$rAF", "$mdTheming", "$mdDialog"];
|
||
MdDialogProvider.$inject = ["$$interimElementProvider"];
|
||
angular
|
||
.module('material.components.dialog', [
|
||
'material.core',
|
||
'material.components.backdrop'
|
||
])
|
||
.directive('mdDialog', MdDialogDirective)
|
||
.provider('$mdDialog', MdDialogProvider);
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdDialog
|
||
* @module material.components.dialog
|
||
*
|
||
* @restrict E
|
||
*
|
||
* @description
|
||
* `<md-dialog>` - The dialog's template must be inside this element.
|
||
*
|
||
* Inside, use an `<md-dialog-content>` element for the dialog's content, and use
|
||
* an `<md-dialog-actions>` element for the dialog's actions.
|
||
*
|
||
* ## CSS
|
||
* - `.md-dialog-content` - class that sets the padding on the content as the spec file
|
||
*
|
||
* ## Notes
|
||
* - If you specify an `id` for the `<md-dialog>`, the `<md-dialog-content>` will have the same `id`
|
||
* prefixed with `dialogContent_`.
|
||
*
|
||
* @usage
|
||
* ### Dialog template
|
||
* <hljs lang="html">
|
||
* <md-dialog aria-label="List dialog">
|
||
* <md-dialog-content>
|
||
* <md-list>
|
||
* <md-list-item ng-repeat="item in items">
|
||
* <p>Number {{item}}</p>
|
||
* </md-list-item>
|
||
* </md-list>
|
||
* </md-dialog-content>
|
||
* <md-dialog-actions>
|
||
* <md-button ng-click="closeDialog()" class="md-primary">Close Dialog</md-button>
|
||
* </md-dialog-actions>
|
||
* </md-dialog>
|
||
* </hljs>
|
||
*/
|
||
function MdDialogDirective($$rAF, $mdTheming, $mdDialog) {
|
||
return {
|
||
restrict: 'E',
|
||
link: function(scope, element) {
|
||
element.addClass('_md'); // private md component indicator for styling
|
||
|
||
$mdTheming(element);
|
||
$$rAF(function() {
|
||
var images;
|
||
var content = element[0].querySelector('md-dialog-content');
|
||
|
||
if (content) {
|
||
images = content.getElementsByTagName('img');
|
||
addOverflowClass();
|
||
// delayed image loading may impact scroll height, check after images are loaded
|
||
angular.element(images).on('load', addOverflowClass);
|
||
}
|
||
|
||
scope.$on('$destroy', function() {
|
||
$mdDialog.destroy(element);
|
||
});
|
||
|
||
/**
|
||
*
|
||
*/
|
||
function addOverflowClass() {
|
||
element.toggleClass('md-content-overflow', content.scrollHeight > content.clientHeight);
|
||
}
|
||
|
||
|
||
});
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* @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 `<md-dialog>` element.
|
||
* Inside, use an `<md-dialog-content>` element for the dialog's content, and use
|
||
* an `<md-dialog-actions>` 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
|
||
* <hljs lang="html">
|
||
* <div ng-app="demoApp" ng-controller="AppController as ctrl">
|
||
* <div>
|
||
* <md-button ng-click="ctrl.showAlert()" class="md-raised md-warn">
|
||
* Basic Alert!
|
||
* </md-button>
|
||
* </div>
|
||
* <div>
|
||
* <md-button ng-click="ctrl.showDialog($event)" class="md-raised">
|
||
* Custom Dialog
|
||
* </md-button>
|
||
* </div>
|
||
* </div>
|
||
* </hljs>
|
||
*
|
||
* ### JavaScript: object syntax
|
||
* <hljs lang="js">
|
||
* (function(angular, undefined) {
|
||
* "use strict";
|
||
*
|
||
* angular
|
||
* .module('demoApp', ['ngMaterial'])
|
||
* .controller('AppCtrl', AppController);
|
||
*
|
||
* function AppController($mdDialog) {
|
||
* var alert;
|
||
* var ctrl = this;
|
||
* ctrl.showAlert = showAlert;
|
||
* ctrl.showDialog = showDialog;
|
||
* ctrl.items = [1, 2, 3];
|
||
*
|
||
* // Internal method
|
||
* function showAlert() {
|
||
* alert = $mdDialog.alert({
|
||
* title: 'Attention',
|
||
* textContent: 'This is an example of how simple 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:
|
||
* '<md-dialog aria-label="List dialog">' +
|
||
* ' <md-dialog-content>'+
|
||
* ' <md-list>'+
|
||
* ' <md-list-item ng-repeat="item in ctrl.items">'+
|
||
* ' <p>Number {{item}}</p>' +
|
||
* ' </md-item>'+
|
||
* ' </md-list>'+
|
||
* ' </md-dialog-content>' +
|
||
* ' <md-dialog-actions>' +
|
||
* ' <md-button ng-click="ctrl.closeDialog()" class="md-primary">' +
|
||
* ' Close Dialog' +
|
||
* ' </md-button>' +
|
||
* ' </md-dialog-actions>' +
|
||
* '</md-dialog>',
|
||
* locals: {
|
||
* items: ctrl.items
|
||
* },
|
||
* controller: DialogController
|
||
* controllerAs: 'ctrl'
|
||
* });
|
||
* function DialogController($mdDialog) {
|
||
* this.closeDialog = function() {
|
||
* $mdDialog.hide();
|
||
* }
|
||
* }
|
||
* }
|
||
* }
|
||
* })(angular);
|
||
* </hljs>
|
||
*
|
||
* ### Multiple Dialogs
|
||
* Using the `multiple` option for the `$mdDialog` service allows developers to show multiple
|
||
* dialogs at the same time.
|
||
*
|
||
* <hljs lang="js">
|
||
* // From plain options
|
||
* $mdDialog.show({
|
||
* multiple: true
|
||
* });
|
||
*
|
||
* // From a dialog preset
|
||
* $mdDialog.show(
|
||
* $mdDialog
|
||
* .alert()
|
||
* .multiple(true)
|
||
* );
|
||
*
|
||
* </hljs>
|
||
*
|
||
* ### Pre-Rendered Dialogs
|
||
* By using the `contentElement` option, it is possible to use an already existing element in the
|
||
* DOM.
|
||
*
|
||
* > Pre-rendered dialogs will be not linked to any scope and will not instantiate any new
|
||
* > controller.<br/>
|
||
* > You can manually link the elements to a scope or instantiate a controller from the template
|
||
* > (using `ng-controller`).
|
||
*
|
||
* <hljs lang="js">
|
||
* function showPrerenderedDialog() {
|
||
* $mdDialog.show({
|
||
* contentElement: '#myStaticDialog',
|
||
* parent: angular.element(document.body)
|
||
* });
|
||
* }
|
||
* </hljs>
|
||
*
|
||
* When using a string as value, `$mdDialog` will automatically query the DOM for the specified CSS
|
||
* selector.
|
||
*
|
||
* <hljs lang="html">
|
||
* <div style="visibility: hidden">
|
||
* <div class="md-dialog-container" id="myStaticDialog">
|
||
* <md-dialog>
|
||
* This is a pre-rendered dialog.
|
||
* </md-dialog>
|
||
* </div>
|
||
* </div>
|
||
* </hljs>
|
||
*
|
||
* **Notice**: It is important, to use the `.md-dialog-container` as the content element, otherwise
|
||
* the dialog will not show up.
|
||
*
|
||
* It also possible to use a DOM element for the `contentElement` option.
|
||
* - `contentElement: document.querySelector('#myStaticDialog')`
|
||
* - `contentElement: angular.element(TEMPLATE)`
|
||
*
|
||
* When using a `template` as content element, it will be not compiled upon open.
|
||
* This allows you to compile the element yourself and use it each time the dialog opens.
|
||
*
|
||
* ### Custom Presets
|
||
* Developers are also able to create their own preset, which can be used without repeating
|
||
* their options each time.
|
||
*
|
||
* <hljs lang="js">
|
||
* $mdDialogProvider.addPreset('testPreset', {
|
||
* options: function() {
|
||
* return {
|
||
* template:
|
||
* '<md-dialog>' +
|
||
* 'This is a custom preset' +
|
||
* '</md-dialog>',
|
||
* controllerAs: 'dialog',
|
||
* bindToController: true,
|
||
* clickOutsideToClose: true,
|
||
* escapeToClose: true
|
||
* };
|
||
* }
|
||
* });
|
||
* </hljs>
|
||
*
|
||
* After creating your preset in the `config` phase, you can access it.
|
||
*
|
||
* <hljs lang="js">
|
||
* $mdDialog.show(
|
||
* $mdDialog.testPreset()
|
||
* );
|
||
* </hljs>
|
||
*
|
||
* ### JavaScript: promise API syntax, custom dialog template
|
||
*
|
||
* <hljs lang="js">
|
||
* (function(angular, undefined) {
|
||
* "use strict";
|
||
*
|
||
* angular
|
||
* .module('demoApp', ['ngMaterial'])
|
||
* .controller('EmployeeController', EmployeeController)
|
||
* .controller('GreetingController', GreetingController);
|
||
*
|
||
* // Fictitious Employee Editor to show how to use simple and complex dialogs.
|
||
*
|
||
* function EmployeeController($mdDialog) {
|
||
* var alert;
|
||
* var ctrl = this;
|
||
*
|
||
* ctrl.showAlert = showAlert;
|
||
* ctrl.showGreeting = showCustomGreeting;
|
||
*
|
||
* ctrl.hasAlert = function() { return !!alert };
|
||
* ctrl.userName = ctrl.userName || 'Bobby';
|
||
*
|
||
* // Dialog #1 - Show simple alert dialog and cache reference to dialog instance
|
||
*
|
||
* function showAlert() {
|
||
* alert = $mdDialog.alert()
|
||
* .title('Attention, ' + ctrl.userName)
|
||
* .textContent('This is an example of how simple dialogs can be!')
|
||
* .ok('Close');
|
||
*
|
||
* $mdDialog
|
||
* .show(alert)
|
||
* .finally(function() {
|
||
* alert = undefined;
|
||
* });
|
||
* }
|
||
*
|
||
* // Dialog #2 - Demonstrate more complex dialogs construction and popup.
|
||
*
|
||
* function showCustomGreeting($event) {
|
||
* $mdDialog.show({
|
||
* targetEvent: $event,
|
||
* template:
|
||
* '<md-dialog>' +
|
||
* ' <md-dialog-content>Hello {{ ctrl.employee }}!</md-dialog-content>' +
|
||
* ' <md-dialog-actions>' +
|
||
* ' <md-button ng-click="ctrl.closeDialog()" class="md-primary">' +
|
||
* ' Close Greeting' +
|
||
* ' </md-button>' +
|
||
* ' </md-dialog-actions>' +
|
||
* '</md-dialog>',
|
||
* controller: GreetingController,
|
||
* controllerAs: 'ctrl',
|
||
* onComplete: afterShowAnimation,
|
||
* locals: { employee: ctrl.userName }
|
||
* });
|
||
*
|
||
* // When the 'enter' animation finishes...
|
||
* function afterShowAnimation(scope, element, options) {
|
||
* // post-show code here: DOM element focus, etc.
|
||
* }
|
||
* }
|
||
* }
|
||
*
|
||
* // Greeting controller used with the 'showCustomGreeting()' custom dialog
|
||
* function GreetingController($mdDialog, $log) {
|
||
* var ctrl = this;
|
||
* this.$log = $log;
|
||
*
|
||
* ctrl.closeDialog = function() {
|
||
* // Hides the most recent dialog shown.
|
||
* // No specific dialog instance reference is needed.
|
||
* $mdDialog.hide();
|
||
* };
|
||
* }
|
||
*
|
||
* GreetingController.prototype.$onInit = function() {
|
||
* // Assigned from the locals options passed to $mdDialog.show.
|
||
* this.$log.log('Employee Name: ', ctrl.employee);
|
||
* };
|
||
*
|
||
* })(angular);
|
||
* </hljs>
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdDialog#alert
|
||
*
|
||
* @description
|
||
* Builds a preconfigured dialog with the specified message.
|
||
*
|
||
* @returns {Object} a dialog preset with the chainable configuration methods:
|
||
*
|
||
* - `title(string)` - Sets the alert title.
|
||
* - `textContent(string)` - Sets the alert message.
|
||
* - `htmlContent(string)` - Sets the alert message as HTML. Requires the `ngSanitize`
|
||
* module to be loaded. HTML is not run through AngularJS' compiler.
|
||
* - `ok(string)` - Sets the alert "Okay" button text.
|
||
* - `theme(string)` - Sets the theme of the alert dialog.
|
||
* - `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.
|
||
*/
|
||
|
||
/**
|
||
* @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 if the user clicks the confirm action on the dialog. The promise will
|
||
* be rejected if the user clicks the cancel action or dismisses the dialog.
|
||
*
|
||
* @returns {Object} a dialog preset with the chainable configuration methods:
|
||
*
|
||
* Additionally, it supports the following methods:
|
||
*
|
||
* - `title(string)` - Sets the confirm title.
|
||
* - `textContent(string)` - Sets the confirm message.
|
||
* - `htmlContent(string)` - Sets the confirm message as HTML. Requires the `ngSanitize`
|
||
* module to be loaded. HTML is not run through AngularJS' compiler.
|
||
* - `ok(string)` - Sets the confirm "Okay" button text.
|
||
* - `cancel(string)` - Sets the confirm "Cancel" button text.
|
||
* - `theme(string)` - Sets the theme of the confirm dialog.
|
||
* - `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.
|
||
*/
|
||
|
||
/**
|
||
* @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, if the user clicks the prompt action on the dialog, passing
|
||
* the input value as the first argument. The promise will be rejected if the user clicks the cancel
|
||
* action or dismisses the dialog.
|
||
*
|
||
* @returns {Object} a dialog preset with the chainable configuration methods:
|
||
*
|
||
* Additionally, it supports the following methods:
|
||
*
|
||
* - `title(string)` - Sets the prompt title.
|
||
* - `textContent(string)` - Sets the prompt message.
|
||
* - `htmlContent(string)` - Sets the prompt message as HTML. Requires the `ngSanitize`
|
||
* module to be loaded. HTML is not run through AngularJS' compiler.
|
||
* - `placeholder(string)` - Sets the placeholder text for the input.
|
||
* - `required(boolean)` - Sets the input required value.
|
||
* - `initialValue(string)` - Sets the initial value for the prompt input.
|
||
* - `ok(string)` - Sets the prompt "Okay" button text.
|
||
* - `cancel(string)` - Sets the prompt "Cancel" button text.
|
||
* - `theme(string)` - Sets the theme of the prompt dialog.
|
||
* - `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.
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdDialog#show
|
||
*
|
||
* @description
|
||
* Show a dialog with the specified options.
|
||
*
|
||
* @param {Object} optionsOrPreset Either provide a dialog preset returned from `alert()`,
|
||
* `prompt()`, or `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 AngularJS' [$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.
|
||
* - `contentElement` - `{string|Element}`: Instead of using a template, which will be compiled
|
||
* each time a dialog opens, you can also use a DOM element.<br/>
|
||
* * When specifying an element, which is present on the DOM, `$mdDialog` will temporary fetch
|
||
* the element into the dialog and restores it at the old DOM position upon close.
|
||
* * When specifying a string, the string be used as a CSS selector, to lookup for the element
|
||
* in the DOM.
|
||
* - `autoWrap` - `{boolean=}`: Whether or not to automatically wrap the template with a
|
||
* `<md-dialog>` 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` - `{Function=}`: Similar to locals, except it takes as values functions that return
|
||
* promises, 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(scope, element, options: Object=, controller: Object)=`: Callback
|
||
* function used to notify the show() animation is starting.
|
||
* - `onComplete` - `Function(scope, element, options: Object=)=`: Callback function used to
|
||
* notify when the show() animation is finished.
|
||
* - `onRemoving` - `Function(element, removePromise)`: Callback function used to announce the
|
||
* close/hide() action is starting. This allows developers to run custom animations
|
||
* in parallel with the close animations.
|
||
* - `fullscreen` `{boolean=}`: An option to toggle whether the dialog should show in fullscreen
|
||
* or not. Defaults to `false`.
|
||
* - `multiple` `{boolean=}`: An option to allow this dialog to display over one that's currently
|
||
* open.
|
||
* @returns {Promise} A promise that can be resolved with `$mdDialog.hide()` or
|
||
* rejected with `$mdDialog.cancel()`.
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdDialog#hide
|
||
*
|
||
* @description
|
||
* Hide an existing dialog and resolve the promise returned from `$mdDialog.show()`.
|
||
*
|
||
* @param {*=} response An argument for the resolved promise.
|
||
*
|
||
* @returns {promise} A promise that is resolved when the dialog has been closed.
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdDialog#cancel
|
||
*
|
||
* @description
|
||
* Hide an existing dialog and reject the promise returned from `$mdDialog.show()`.
|
||
*
|
||
* @param {*=} response An argument for the rejected promise.
|
||
*
|
||
* @returns {promise} A promise that is resolved when the dialog has been closed.
|
||
*/
|
||
|
||
function MdDialogProvider($$interimElementProvider) {
|
||
// Elements to capture and redirect focus when the user presses tab at the dialog boundary.
|
||
MdDialogController.$inject = ["$mdDialog", "$mdConstant"];
|
||
dialogDefaultOptions.$inject = ["$mdDialog", "$mdAria", "$mdUtil", "$mdConstant", "$animate", "$document", "$window", "$rootElement", "$log", "$injector", "$mdTheming", "$interpolate", "$mdInteraction"];
|
||
var topFocusTrap, bottomFocusTrap;
|
||
var removeFocusTrap;
|
||
|
||
return $$interimElementProvider('$mdDialog')
|
||
.setDefaults({
|
||
methods: ['disableParentScroll', 'hasBackdrop', 'clickOutsideToClose', 'escapeToClose',
|
||
'targetEvent', 'closeTo', 'openFrom', 'parent', 'fullscreen', 'multiple'],
|
||
options: dialogDefaultOptions
|
||
})
|
||
.addPreset('alert', {
|
||
methods: ['title', 'htmlContent', 'textContent', 'ariaLabel', 'ok', 'theme',
|
||
'css'],
|
||
options: advancedDialogOptions
|
||
})
|
||
.addPreset('confirm', {
|
||
methods: ['title', 'htmlContent', 'textContent', 'ariaLabel', 'ok', 'cancel',
|
||
'theme', 'css'],
|
||
options: advancedDialogOptions
|
||
})
|
||
.addPreset('prompt', {
|
||
methods: ['title', 'htmlContent', 'textContent', 'initialValue', 'placeholder', 'ariaLabel',
|
||
'ok', 'cancel', 'theme', 'css', 'required'],
|
||
options: advancedDialogOptions
|
||
});
|
||
|
||
/* @ngInject */
|
||
function advancedDialogOptions() {
|
||
return {
|
||
template: [
|
||
'<md-dialog md-theme="{{ dialog.theme || dialog.defaultTheme }}" aria-label="{{ dialog.ariaLabel }}" ng-class="dialog.css">',
|
||
' <md-dialog-content class="md-dialog-content" role="document" tabIndex="-1">',
|
||
' <h2 class="md-title">{{ dialog.title }}</h2>',
|
||
' <div ng-if="::dialog.mdHtmlContent" class="md-dialog-content-body" ',
|
||
' ng-bind-html="::dialog.mdHtmlContent"></div>',
|
||
' <div ng-if="::!dialog.mdHtmlContent" class="md-dialog-content-body">',
|
||
' <p>{{::dialog.mdTextContent}}</p>',
|
||
' </div>',
|
||
' <md-input-container md-no-float ng-if="::dialog.$type == \'prompt\'" class="md-prompt-input-container">',
|
||
' <input ng-keypress="dialog.keypress($event)" md-autofocus ng-model="dialog.result" ' +
|
||
' placeholder="{{::dialog.placeholder}}" ng-required="dialog.required">',
|
||
' </md-input-container>',
|
||
' </md-dialog-content>',
|
||
' <md-dialog-actions>',
|
||
' <md-button ng-if="dialog.$type === \'confirm\' || dialog.$type === \'prompt\'"' +
|
||
' ng-click="dialog.abort()" class="md-primary md-cancel-button">',
|
||
' {{ dialog.cancel }}',
|
||
' </md-button>',
|
||
' <md-button ng-click="dialog.hide()" class="md-primary md-confirm-button" md-autofocus="dialog.$type===\'alert\'"' +
|
||
' ng-disabled="dialog.required && !dialog.result">',
|
||
' {{ dialog.ok }}',
|
||
' </md-button>',
|
||
' </md-dialog-actions>',
|
||
'</md-dialog>'
|
||
].join('').replace(/\s\s+/g, ''),
|
||
controller: MdDialogController,
|
||
controllerAs: 'dialog',
|
||
bindToController: true,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Controller for the md-dialog interim elements
|
||
* @ngInject
|
||
*/
|
||
function MdDialogController($mdDialog, $mdConstant) {
|
||
// For compatibility with AngularJS 1.6+, we should always use the $onInit hook in
|
||
// interimElements. The $mdCompiler simulates the $onInit hook for all versions.
|
||
this.$onInit = function() {
|
||
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) {
|
||
var invalidPrompt = isPrompt && this.required && !angular.isDefined(this.result);
|
||
|
||
if ($event.keyCode === $mdConstant.KEY_CODE.ENTER && !invalidPrompt) {
|
||
$mdDialog.hide(this.result);
|
||
}
|
||
};
|
||
};
|
||
}
|
||
|
||
/* @ngInject */
|
||
function dialogDefaultOptions($mdDialog, $mdAria, $mdUtil, $mdConstant, $animate, $document,
|
||
$window, $rootElement, $log, $injector, $mdTheming, $interpolate,
|
||
$mdInteraction) {
|
||
return {
|
||
hasBackdrop: true,
|
||
isolateScope: true,
|
||
onCompiling: beforeCompile,
|
||
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.
|
||
// 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.
|
||
var startSymbol = $interpolate.startSymbol();
|
||
var endSymbol = $interpolate.endSymbol();
|
||
var theme = startSymbol + (options.themeWatch ? '' : '::') + 'theme' + endSymbol;
|
||
var themeAttr = (options.hasTheme) ? 'md-theme="'+theme+'"': '';
|
||
return '<div class="md-dialog-container" tabindex="-1" ' + themeAttr + '>' + validatedTemplate(template) + '</div>';
|
||
|
||
/**
|
||
* The specified template should contain a <md-dialog> wrapper element....
|
||
*/
|
||
function validatedTemplate(template) {
|
||
if (options.autoWrap && !/<\/md-dialog>/g.test(template)) {
|
||
return '<md-dialog>' + (template || '') + '</md-dialog>';
|
||
} else {
|
||
return template || '';
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
function beforeCompile(options) {
|
||
// Automatically apply the theme, if the user didn't specify a theme explicitly.
|
||
// Those option changes need to be done, before the compilation has started, because otherwise
|
||
// the option changes will be not available in the $mdCompilers locales.
|
||
options.defaultTheme = $mdTheming.defaultTheme();
|
||
|
||
detectTheming(options);
|
||
}
|
||
|
||
function beforeShow(scope, element, options, controller) {
|
||
|
||
if (controller) {
|
||
var mdHtmlContent = controller.htmlContent || options.htmlContent || '';
|
||
var mdTextContent = controller.textContent || options.textContent || '';
|
||
|
||
if (mdHtmlContent && !$injector.has('$sanitize')) {
|
||
throw Error('The ngSanitize module must be loaded in order to use htmlContent.');
|
||
}
|
||
|
||
if (mdHtmlContent && mdTextContent) {
|
||
throw Error('md-dialog cannot have both `htmlContent` and `textContent`');
|
||
}
|
||
|
||
// Only assign the content if nothing throws, otherwise it'll still be compiled.
|
||
controller.mdHtmlContent = mdHtmlContent;
|
||
controller.mdTextContent = mdTextContent;
|
||
}
|
||
}
|
||
|
||
/** Show method for dialogs */
|
||
function onShow(scope, element, options) {
|
||
angular.element($document[0].body).addClass('md-dialog-is-showing');
|
||
|
||
var dialogElement = element.find('md-dialog');
|
||
|
||
// Once a dialog has `ng-cloak` applied on his template the dialog animation will not work
|
||
// properly. This is a very common problem, so we have to notify the developer about this.
|
||
if (dialogElement.hasClass('ng-cloak')) {
|
||
var message =
|
||
'$mdDialog: using `<md-dialog ng-cloak>` will affect the dialog opening animations.';
|
||
$log.warn(message, element[0]);
|
||
}
|
||
|
||
captureParentAndFromToElements(options);
|
||
configureAria(dialogElement, options);
|
||
showBackdrop(scope, element, options);
|
||
activateListeners(element, options);
|
||
|
||
return dialogPopIn(element, options)
|
||
.then(function() {
|
||
lockScreenReader(element, options);
|
||
focusOnOpen();
|
||
});
|
||
|
||
/**
|
||
* For alerts, focus on content... otherwise focus on the close button (or equivalent)
|
||
*/
|
||
function focusOnOpen() {
|
||
if (options.focusOnOpen) {
|
||
var target = $mdUtil.findFocusTarget(element) || findCloseButton() || dialogElement;
|
||
target.focus();
|
||
}
|
||
|
||
/**
|
||
* If no element with class dialog-close, try to find the last
|
||
* button child in md-dialog-actions and assume it is a close button.
|
||
*
|
||
* If we find no actions at all, log a warning to the console.
|
||
*/
|
||
function findCloseButton() {
|
||
return element[0].querySelector('.dialog-close, md-dialog-actions button:last-child');
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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 (removeFocusTrap) {
|
||
removeFocusTrap();
|
||
removeFocusTrap = null;
|
||
}
|
||
|
||
// 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);
|
||
}
|
||
|
||
/**
|
||
* Detach the element
|
||
*/
|
||
function detachAndClean() {
|
||
angular.element($document[0].body).removeClass('md-dialog-is-showing');
|
||
|
||
// Reverse the container stretch if using a content element.
|
||
if (options.contentElement) {
|
||
options.reverseContainerStretch();
|
||
}
|
||
|
||
// Exposed cleanup function from the $mdCompiler.
|
||
options.cleanupElement();
|
||
|
||
// Restores the focus to the origin element if the last interaction upon opening was a keyboard.
|
||
if (!options.$destroy && options.originInteraction === 'keyboard') {
|
||
options.origin.focus();
|
||
}
|
||
}
|
||
}
|
||
|
||
function detectTheming(options) {
|
||
// Once the user specifies a targetEvent, we will automatically try to find the correct
|
||
// nested theme.
|
||
var targetEl;
|
||
if (options.targetEvent && options.targetEvent.target) {
|
||
targetEl = angular.element(options.targetEvent.target);
|
||
}
|
||
|
||
var themeCtrl = targetEl && targetEl.controller('mdTheme');
|
||
|
||
options.hasTheme = (!!themeCtrl);
|
||
|
||
if (!options.hasTheme) {
|
||
return;
|
||
}
|
||
|
||
options.themeWatch = themeCtrl.$shouldWatch;
|
||
|
||
var theme = options.theme || themeCtrl.$mdTheme;
|
||
|
||
if (theme) {
|
||
options.scope.theme = theme;
|
||
}
|
||
|
||
var unwatch = themeCtrl.registerChanges(function (newTheme) {
|
||
options.scope.theme = newTheme;
|
||
|
||
if (!options.themeWatch) {
|
||
unwatch();
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 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);
|
||
options.originInteraction = $mdInteraction.getLastInteractionType();
|
||
}
|
||
|
||
|
||
/**
|
||
* 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.stopImmediatePropagation();
|
||
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 && 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() {
|
||
// If dialog title is specified, set aria-label with it
|
||
// See https://github.com/angular/material/issues/10582
|
||
if (options.title) {
|
||
return options.title;
|
||
} else {
|
||
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 end of the dialog, we intercept it and redirect it
|
||
* back to the md-dialog element.
|
||
* When focus is about to move out of the start of the dialog, we intercept it and redirect it
|
||
* back to the last focusable element in the md-dialog.
|
||
* @param {FocusEvent} event
|
||
*/
|
||
var focusHandler = function(event) {
|
||
if (event.target && event.target.nextSibling &&
|
||
event.target.nextSibling.nodeName === 'MD-DIALOG') {
|
||
var lastFocusableElement = $mdUtil.getLastTabbableElement(element[0]);
|
||
if (angular.isElement(lastFocusableElement)) {
|
||
lastFocusableElement.focus();
|
||
}
|
||
} else {
|
||
element.focus();
|
||
}
|
||
};
|
||
|
||
topFocusTrap.addEventListener('focus', focusHandler);
|
||
bottomFocusTrap.addEventListener('focus', focusHandler);
|
||
|
||
removeFocusTrap = function () {
|
||
topFocusTrap.removeEventListener('focus', focusHandler);
|
||
bottomFocusTrap.removeEventListener('focus', focusHandler);
|
||
|
||
if (topFocusTrap && topFocusTrap.parentNode) {
|
||
topFocusTrap.parentNode.removeChild(topFocusTrap);
|
||
}
|
||
|
||
if (bottomFocusTrap && bottomFocusTrap.parentNode) {
|
||
bottomFocusTrap.parentNode.removeChild(bottomFocusTrap);
|
||
}
|
||
};
|
||
|
||
// The top focus trap inserted immediately before the md-dialog element (as a sibling).
|
||
// The bottom focus trap is inserted immediately after the md-dialog element (as a sibling).
|
||
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;
|
||
};
|
||
|
||
/**
|
||
* Get all of an element's parent elements up the DOM tree.
|
||
* @param {Node & ParentNode} element the element to start from
|
||
* @return {Element[]} The parent elements
|
||
*/
|
||
function getParents(element) {
|
||
var parents = [];
|
||
while (element.parentNode) {
|
||
if (element === document.body) {
|
||
return parents;
|
||
}
|
||
var children = element.parentNode.children;
|
||
for (var i = 0; i < children.length; i++) {
|
||
// skip over child if it is an ascendant of the dialog
|
||
// a script or style tag, or a live region.
|
||
if (element !== children[i] &&
|
||
!isNodeOneOf(children[i], ['SCRIPT', 'STYLE']) &&
|
||
!children[i].hasAttribute('aria-live')) {
|
||
parents.push(children[i]);
|
||
}
|
||
}
|
||
element = element.parentNode;
|
||
}
|
||
return parents;
|
||
}
|
||
|
||
/**
|
||
* Walk DOM to apply or remove aria-hidden on sibling nodes and parent sibling nodes.
|
||
* @param {Element} element the element to start from when walking up the DOM
|
||
* @returns {void}
|
||
*/
|
||
function walkDOM(element) {
|
||
var elements = getParents(element);
|
||
for (var i = 0; i < elements.length; i++) {
|
||
elements[i].setAttribute('aria-hidden', isHidden);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Ensure the dialog container fill-stretches to the viewport.
|
||
* @param {JQLite} container dialog container
|
||
* @param {Object} options
|
||
* @returns {function(): void} function that reverts the modified styles
|
||
*/
|
||
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')
|
||
};
|
||
|
||
// If the body is fixed, determine the distance to the viewport in relative from the parent.
|
||
var parentTop = Math.abs(options.parent[0].getBoundingClientRect().top);
|
||
|
||
container.css({
|
||
top: (isFixed ? parentTop : 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.
|
||
* @param {JQLite} container dialog container
|
||
* @param {Object} options
|
||
* @returns {*}
|
||
*/
|
||
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)
|
||
|
||
dialogEl.toggleClass('md-dialog-fullscreen', !!options.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)
|
||
)
|
||
);
|
||
};
|
||
|
||
// Function to revert the generated animation styles on the dialog element.
|
||
// Useful when using a contentElement instead of a template.
|
||
options.clearAnimate = function() {
|
||
delete options.clearAnimate;
|
||
|
||
// Remove the transition classes, added from $animateCSS, since those can't be removed
|
||
// by reversely running the animator.
|
||
dialogEl.removeClass([
|
||
translateOptions.transitionOutClass,
|
||
translateOptions.transitionInClass
|
||
].join(' '));
|
||
|
||
// Run the animation reversely to remove the previous added animation styles.
|
||
return animator.translate3d(dialogEl, to, animator.toTransformCss(''), {});
|
||
};
|
||
|
||
return true;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Dialog close and pop-out animation.
|
||
* @param {JQLite} container dialog container
|
||
* @param {Object} options
|
||
* @returns {*}
|
||
*/
|
||
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.
|
||
* @param {Node} elem
|
||
* @param {string[]} nodeTypeArray
|
||
* @returns {boolean}
|
||
*/
|
||
function isNodeOneOf(elem, nodeTypeArray) {
|
||
return nodeTypeArray.indexOf(elem.nodeName) !== -1;
|
||
}
|
||
}
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.divider
|
||
* @description Divider module!
|
||
*/
|
||
MdDividerDirective.$inject = ["$mdTheming"];
|
||
angular.module('material.components.divider', [
|
||
'material.core'
|
||
])
|
||
.directive('mdDivider', MdDividerDirective);
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdDivider
|
||
* @module material.components.divider
|
||
* @restrict E
|
||
*
|
||
* @description
|
||
* Dividers group and separate content within lists and page layouts using strong visual and spatial distinctions. This divider is a thin rule, lightweight enough to not distract the user from content.
|
||
*
|
||
* @param {boolean=} md-inset Add this attribute to activate the inset divider style.
|
||
* @usage
|
||
* <hljs lang="html">
|
||
* <md-divider></md-divider>
|
||
*
|
||
* <md-divider md-inset></md-divider>
|
||
* </hljs>
|
||
*
|
||
*/
|
||
function MdDividerDirective($mdTheming) {
|
||
return {
|
||
restrict: 'E',
|
||
link: $mdTheming
|
||
};
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
(function() {
|
||
'use strict';
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.fabActions
|
||
*/
|
||
MdFabActionsDirective.$inject = ["$mdUtil"];
|
||
angular
|
||
.module('material.components.fabActions', ['material.core'])
|
||
.directive('mdFabActions', MdFabActionsDirective);
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdFabActions
|
||
* @module material.components.fabActions
|
||
*
|
||
* @restrict E
|
||
*
|
||
* @description
|
||
* The `<md-fab-actions>` directive is used inside of a `<md-fab-speed-dial>` or
|
||
* `<md-fab-toolbar>` directive to mark an element (or elements) as the actions and setup the
|
||
* proper event listeners.
|
||
*
|
||
* @usage
|
||
* See the `<md-fab-speed-dial>` or `<md-fab-toolbar>` directives for example usage.
|
||
*/
|
||
function MdFabActionsDirective($mdUtil) {
|
||
return {
|
||
restrict: 'E',
|
||
|
||
require: ['^?mdFabSpeedDial', '^?mdFabToolbar'],
|
||
|
||
compile: function(element, attributes) {
|
||
var children = element.children();
|
||
var actionItemButtons;
|
||
var hasNgRepeat = $mdUtil.prefixer().hasAttribute(children, 'ng-repeat');
|
||
|
||
// Action item buttons should not be in the tab order when the speed dial is closed.
|
||
actionItemButtons = element.find('md-button');
|
||
angular.forEach(actionItemButtons, function(button) {
|
||
button.setAttribute('tabindex', -1);
|
||
});
|
||
|
||
// 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('<div class="md-fab-action-item">');
|
||
}
|
||
}
|
||
};
|
||
}
|
||
})();
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
(function() {
|
||
'use strict';
|
||
|
||
MdFabController.$inject = ["$scope", "$element", "$animate", "$mdUtil", "$mdConstant", "$timeout"];
|
||
angular.module('material.components.fabShared', ['material.core'])
|
||
.controller('MdFabController', MdFabController);
|
||
|
||
function MdFabController($scope, $element, $animate, $mdUtil, $mdConstant, $timeout) {
|
||
var ctrl = this;
|
||
var initialAnimationAttempts = 0;
|
||
|
||
// NOTE: We use async eval(s) below to avoid conflicts with any existing digest loops
|
||
|
||
ctrl.open = function() {
|
||
$scope.$evalAsync("ctrl.isOpen = true");
|
||
};
|
||
|
||
ctrl.close = function() {
|
||
// Async eval to avoid conflicts with existing digest loops
|
||
$scope.$evalAsync("ctrl.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
|
||
ctrl.toggle = function() {
|
||
$scope.$evalAsync("ctrl.isOpen = !ctrl.isOpen");
|
||
};
|
||
|
||
/*
|
||
* AngularJS Lifecycle hook for newer AngularJS versions.
|
||
* Bindings are not guaranteed to have been assigned in the controller, but they are in the
|
||
* $onInit hook.
|
||
*/
|
||
ctrl.$onInit = function() {
|
||
setupDefaults();
|
||
setupListeners();
|
||
setupWatchers();
|
||
|
||
fireInitialAnimations();
|
||
};
|
||
|
||
// For AngularJS 1.4 and older, where there are no lifecycle hooks but bindings are pre-assigned,
|
||
// manually call the $onInit hook.
|
||
if (angular.version.major === 1 && angular.version.minor <= 4) {
|
||
this.$onInit();
|
||
}
|
||
|
||
function setupDefaults() {
|
||
// Set the default direction to 'down' if none is specified
|
||
ctrl.direction = ctrl.direction || 'down';
|
||
|
||
// Set the default to be closed
|
||
ctrl.isOpen = ctrl.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;
|
||
|
||
/**
|
||
* @param {MouseEvent} event
|
||
*/
|
||
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() {
|
||
ctrl.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() {
|
||
ctrl.currentActionIndex = -1;
|
||
}
|
||
|
||
function setupWatchers() {
|
||
// Watch for changes to the direction and update classes/attributes
|
||
$scope.$watch('ctrl.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('ctrl.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);
|
||
});
|
||
}
|
||
|
||
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) {
|
||
ctrl.close();
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {KeyboardEvent} event
|
||
* @returns {boolean}
|
||
*/
|
||
function keyPressed(event) {
|
||
switch (event.which) {
|
||
case $mdConstant.KEY_CODE.ESCAPE: ctrl.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;
|
||
case $mdConstant.KEY_CODE.TAB: doShift(event); return false;
|
||
}
|
||
}
|
||
|
||
function doActionPrev(event) {
|
||
focusAction(event, -1);
|
||
}
|
||
|
||
function doActionNext(event) {
|
||
focusAction(event, 1);
|
||
}
|
||
|
||
function focusAction(event, direction) {
|
||
var actions = getActionsElement()[0].querySelectorAll('.md-fab-action-item');
|
||
var previousActionIndex = ctrl.currentActionIndex;
|
||
|
||
// Increment/decrement the counter with restrictions
|
||
ctrl.currentActionIndex = ctrl.currentActionIndex + direction;
|
||
ctrl.currentActionIndex = Math.min(actions.length - 1, ctrl.currentActionIndex);
|
||
ctrl.currentActionIndex = Math.max(0, ctrl.currentActionIndex);
|
||
|
||
// Let Tab and Shift+Tab escape if we're trying to move past the start/end.
|
||
if (event.which !== $mdConstant.KEY_CODE.TAB ||
|
||
previousActionIndex !== ctrl.currentActionIndex) {
|
||
// Focus the element
|
||
var focusElement = angular.element(actions[ctrl.currentActionIndex]).children()[0];
|
||
focusElement.focus();
|
||
|
||
// Make sure the event doesn't bubble and cause something else
|
||
event.preventDefault();
|
||
event.stopImmediatePropagation();
|
||
}
|
||
}
|
||
|
||
function doKeyLeft(event) {
|
||
if (ctrl.direction === 'left') {
|
||
doActionNext(event);
|
||
} else {
|
||
doActionPrev(event);
|
||
}
|
||
}
|
||
|
||
function doKeyUp(event) {
|
||
if (ctrl.direction === 'down') {
|
||
doActionPrev(event);
|
||
} else {
|
||
doActionNext(event);
|
||
}
|
||
}
|
||
|
||
function doKeyRight(event) {
|
||
if (ctrl.direction === 'left') {
|
||
doActionPrev(event);
|
||
} else {
|
||
doActionNext(event);
|
||
}
|
||
}
|
||
|
||
function doKeyDown(event) {
|
||
if (ctrl.direction === 'up') {
|
||
doActionPrev(event);
|
||
} else {
|
||
doActionNext(event);
|
||
}
|
||
}
|
||
|
||
function doShift(event) {
|
||
if (event.shiftKey) {
|
||
doActionPrev(event);
|
||
} else {
|
||
doActionNext(event);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {Node} element
|
||
* @returns {Node|null}
|
||
*/
|
||
function getClosestButton(element) {
|
||
return $mdUtil.getClosest(element, 'button') || $mdUtil.getClosest(element, 'md-button');
|
||
}
|
||
|
||
/**
|
||
* @param {Node} element
|
||
* @returns {Node|null}
|
||
*/
|
||
function getClosestTrigger(element) {
|
||
return $mdUtil.getClosest(element, 'md-fab-trigger');
|
||
}
|
||
|
||
/**
|
||
* @param {Node} element
|
||
* @returns {Node|null}
|
||
*/
|
||
function getClosestAction(element) {
|
||
return $mdUtil.getClosest(element, 'md-fab-actions');
|
||
}
|
||
|
||
/**
|
||
* @param {MouseEvent|FocusEvent} event
|
||
*/
|
||
function handleItemClick(event) {
|
||
var closestButton = event.target ? getClosestButton(event.target) : null;
|
||
|
||
// Check that the button in the trigger is not disabled
|
||
if (closestButton && !closestButton.disabled) {
|
||
if (getClosestTrigger(event.target)) {
|
||
ctrl.toggle();
|
||
}
|
||
}
|
||
|
||
if (getClosestAction(event.target)) {
|
||
ctrl.close();
|
||
}
|
||
}
|
||
|
||
function getTriggerElement() {
|
||
return $element.find('md-fab-trigger');
|
||
}
|
||
|
||
function getActionsElement() {
|
||
return $element.find('md-fab-actions');
|
||
}
|
||
}
|
||
})();
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
(function() {
|
||
'use strict';
|
||
|
||
/**
|
||
* The duration of the CSS animation in milliseconds.
|
||
*
|
||
* @type {number}
|
||
*/
|
||
MdFabSpeedDialFlingAnimation.$inject = ["$timeout"];
|
||
MdFabSpeedDialScaleAnimation.$inject = ["$timeout"];
|
||
var cssAnimationDuration = 300;
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.fabSpeedDial
|
||
*/
|
||
angular
|
||
// Declare our module
|
||
.module('material.components.fabSpeedDial', [
|
||
'material.core',
|
||
'material.components.fabShared',
|
||
'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 `<md-fab-speed-dial>` directive is used to present a series of popup elements (usually
|
||
* `<md-button>`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
|
||
* `<md-fab-speed-dial>` 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
|
||
* <hljs lang="html">
|
||
* <md-fab-speed-dial md-direction="up" class="md-fling">
|
||
* <md-fab-trigger>
|
||
* <md-button aria-label="Add..."><md-icon md-svg-src="/img/icons/plus.svg"></md-icon></md-button>
|
||
* </md-fab-trigger>
|
||
*
|
||
* <md-fab-actions>
|
||
* <md-button aria-label="Add User">
|
||
* <md-icon md-svg-src="/img/icons/user.svg"></md-icon>
|
||
* </md-button>
|
||
*
|
||
* <md-button aria-label="Add Group">
|
||
* <md-icon md-svg-src="/img/icons/group.svg"></md-icon>
|
||
* </md-button>
|
||
* </md-fab-actions>
|
||
* </md-fab-speed-dial>
|
||
* </hljs>
|
||
*
|
||
* @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: 'ctrl',
|
||
|
||
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('<div class="_md-css-variables"></div>');
|
||
}
|
||
}
|
||
|
||
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 = ctrl.isOpen ? 1 : 0;
|
||
|
||
// 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);
|
||
}
|
||
};
|
||
}
|
||
|
||
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);
|
||
}
|
||
};
|
||
}
|
||
})();
|
||
|
||
})();
|
||
(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.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 `<md-fab-toolbar>` directive is used to present a toolbar of elements (usually `<md-button>`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
|
||
* `<md-fab-toolbar>` 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
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-fab-toolbar md-direction='left'>
|
||
* <md-fab-trigger>
|
||
* <md-button aria-label="Add..."><md-icon md-svg-src="/img/icons/plus.svg"></md-icon></md-button>
|
||
* </md-fab-trigger>
|
||
*
|
||
* <md-toolbar>
|
||
* <md-fab-actions>
|
||
* <md-button aria-label="Add User">
|
||
* <md-icon md-svg-src="/img/icons/user.svg"></md-icon>
|
||
* </md-button>
|
||
*
|
||
* <md-button aria-label="Add Group">
|
||
* <md-icon md-svg-src="/img/icons/group.svg"></md-icon>
|
||
* </md-button>
|
||
* </md-fab-actions>
|
||
* </md-toolbar>
|
||
* </md-fab-toolbar>
|
||
* </hljs>
|
||
*
|
||
* @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: '<div class="md-fab-toolbar-wrapper">' +
|
||
' <div class="md-fab-toolbar-content" ng-transclude></div>' +
|
||
'</div>',
|
||
|
||
scope: {
|
||
direction: '@?mdDirection',
|
||
isOpen: '=?mdOpen'
|
||
},
|
||
|
||
bindToController: true,
|
||
controller: 'MdFabController',
|
||
controllerAs: 'ctrl',
|
||
|
||
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('<div class="md-fab-toolbar-background"></div>');
|
||
}
|
||
}
|
||
|
||
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";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.gridList
|
||
*/
|
||
GridListController.$inject = ["$mdUtil"];
|
||
GridLayoutFactory.$inject = ["$mdUtil"];
|
||
GridListDirective.$inject = ["$interpolate", "$mdConstant", "$mdGridLayout", "$mdMedia", "$mdUtil"];
|
||
GridTileDirective.$inject = ["$mdMedia"];
|
||
angular.module('material.components.gridList', ['material.core'])
|
||
.directive('mdGridList', GridListDirective)
|
||
.directive('mdGridTile', GridTileDirective)
|
||
.directive('mdGridTileFooter', GridTileCaptionDirective)
|
||
.directive('mdGridTileHeader', GridTileCaptionDirective)
|
||
.factory('$mdGridLayout', GridLayoutFactory);
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdGridList
|
||
* @module material.components.gridList
|
||
* @restrict E
|
||
* @description
|
||
* Grid lists are an alternative to standard list views. Grid lists are distinct
|
||
* from grids used for layouts and other visual presentations.
|
||
*
|
||
* A grid list is best suited to presenting a homogenous data type, typically
|
||
* images, and is optimized for visual comprehension and differentiating between
|
||
* like data types.
|
||
*
|
||
* A grid list is a continuous element consisting of tessellated, regular
|
||
* subdivisions called cells that contain tiles (`md-grid-tile`).
|
||
*
|
||
* <img src="//material-design.storage.googleapis.com/publish/v_2/material_ext_publish/0Bx4BSt6jniD7OVlEaXZ5YmU1Xzg/components_grids_usage2.png"
|
||
* style="width: 300px; height: auto; margin-right: 16px;" alt="Concept of grid explained visually">
|
||
* <img src="//material-design.storage.googleapis.com/publish/v_2/material_ext_publish/0Bx4BSt6jniD7VGhsOE5idWlJWXM/components_grids_usage3.png"
|
||
* style="width: 300px; height: auto;" alt="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
|
||
* <ul>
|
||
* <li>CSS length - Fixed height rows (eg. `8px` or `1rem`)</li>
|
||
* <li>`{width}:{height}` - Ratio of width to height (eg.
|
||
* `md-row-height="16:9"`)</li>
|
||
* <li>`"fit"` - Height will be determined by subdividing the available
|
||
* height by the number of rows</li>
|
||
* </ul>
|
||
* @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:
|
||
* <hljs lang="html">
|
||
* <md-grid-list md-cols="5" md-gutter="1em" md-row-height="4:3">
|
||
* <md-grid-tile></md-grid-tile>
|
||
* </md-grid-list>
|
||
* </hljs>
|
||
*
|
||
* Fixed-height rows:
|
||
* <hljs lang="html">
|
||
* <md-grid-list md-cols="4" md-row-height="200px" ...>
|
||
* <md-grid-tile></md-grid-tile>
|
||
* </md-grid-list>
|
||
* </hljs>
|
||
*
|
||
* Fit rows:
|
||
* <hljs lang="html">
|
||
* <md-grid-list md-cols="4" md-row-height="fit" style="height: 400px;" ...>
|
||
* <md-grid-tile></md-grid-tile>
|
||
* </md-grid-list>
|
||
* </hljs>
|
||
*
|
||
* Using responsive attributes:
|
||
* <hljs lang="html">
|
||
* <md-grid-list
|
||
* md-cols-sm="2"
|
||
* md-cols-md="4"
|
||
* md-cols-lg="8"
|
||
* md-cols-gt-lg="12"
|
||
* ...>
|
||
* <md-grid-tile></md-grid-tile>
|
||
* </md-grid-list>
|
||
* </hljs>
|
||
*/
|
||
function GridListDirective($interpolate, $mdConstant, $mdGridLayout, $mdMedia, $mdUtil) {
|
||
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 = (!$mdUtil.isRtl(attrs)) ? {
|
||
left: POSITION({ unit: hUnit, offset: position.col, gutter: gutter }),
|
||
width: DIMENSION({ unit: hUnit, span: spans.col, gutter: gutter }),
|
||
// resets
|
||
paddingTop: '',
|
||
marginTop: '',
|
||
top: '',
|
||
height: ''
|
||
} : {
|
||
right: POSITION({ unit: hUnit, offset: position.col, gutter: gutter }),
|
||
width: DIMENSION({ unit: hUnit, span: spans.col, gutter: gutter }),
|
||
// resets
|
||
paddingTop: '',
|
||
marginTop: '',
|
||
top: '',
|
||
height: ''
|
||
};
|
||
|
||
switch (rowMode) {
|
||
case 'fixed':
|
||
// In fixed mode, simply use the given rowHeight.
|
||
style.top = POSITION({ unit: rowHeight, offset: position.row, gutter: gutter });
|
||
style.height = DIMENSION({ unit: rowHeight, span: spans.row, gutter: gutter });
|
||
break;
|
||
|
||
case 'ratio':
|
||
// Percent of the available vertical space that one row takes up. Here, rowHeight holds
|
||
// the ratio value. For example, if the width:height ratio is 4:3, rowHeight = 1.333.
|
||
var vShare = hShare / rowHeight;
|
||
|
||
// Base veritcal size of a row.
|
||
var vUnit = UNIT({ share: vShare, gutterShare: hGutterShare, gutter: gutter });
|
||
|
||
// padidngTop and marginTop are used to maintain the given aspect ratio, as
|
||
// a percentage-based value for these properties is applied to the *width* of the
|
||
// containing block. See http://www.w3.org/TR/CSS2/box.html#margin-properties
|
||
style.paddingTop = DIMENSION({ unit: vUnit, span: spans.row, gutter: gutter});
|
||
style.marginTop = POSITION({ unit: vUnit, offset: position.row, gutter: gutter });
|
||
break;
|
||
|
||
case 'fit':
|
||
// Fraction of the gutter size that each column takes up.
|
||
var vGutterShare = (rowCount - 1) / rowCount;
|
||
|
||
// Percent of the available vertical space that one row takes up.
|
||
vShare = (1 / rowCount) * 100;
|
||
|
||
// Base vertical size of a row.
|
||
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';
|
||
}
|
||
}
|
||
}
|
||
|
||
/* @ngInject */
|
||
function GridListController($mdUtil) {
|
||
this.layoutInvalidated = false;
|
||
this.tilesInvalidated = false;
|
||
this.$timeout_ = $mdUtil.nextTick;
|
||
this.layoutDelegate = angular.noop;
|
||
}
|
||
|
||
GridListController.prototype = {
|
||
invalidateTiles: function() {
|
||
this.tilesInvalidated = true;
|
||
this.invalidateLayout();
|
||
},
|
||
|
||
invalidateLayout: function() {
|
||
if (this.layoutInvalidated) {
|
||
return;
|
||
}
|
||
this.layoutInvalidated = true;
|
||
this.$timeout_(angular.bind(this, this.layout));
|
||
},
|
||
|
||
layout: function() {
|
||
try {
|
||
this.layoutDelegate(this.tilesInvalidated);
|
||
} finally {
|
||
this.layoutInvalidated = false;
|
||
this.tilesInvalidated = false;
|
||
}
|
||
}
|
||
};
|
||
|
||
|
||
/* @ngInject */
|
||
function GridLayoutFactory($mdUtil) {
|
||
var defaultAnimator = GridTileAnimator;
|
||
|
||
/**
|
||
* Set the reflow animator callback
|
||
*/
|
||
GridLayout.animateWith = function(customAnimator) {
|
||
defaultAnimator = !angular.isFunction(customAnimator) ? GridTileAnimator : customAnimator;
|
||
};
|
||
|
||
return GridLayout;
|
||
|
||
/**
|
||
* Publish layout function
|
||
*/
|
||
function GridLayout(colCount, tileSpans) {
|
||
var self, layoutInfo, gridStyles, layoutTime, mapTime, reflowTime;
|
||
|
||
layoutTime = $mdUtil.time(function() {
|
||
layoutInfo = calculateGridFor(colCount, tileSpans);
|
||
});
|
||
|
||
return self = {
|
||
|
||
/**
|
||
* An array of objects describing each tile's position in the grid.
|
||
*/
|
||
layoutInfo: function() {
|
||
return layoutInfo;
|
||
},
|
||
|
||
/**
|
||
* Maps grid positioning to an element and a set of styles using the
|
||
* provided updateFn.
|
||
*/
|
||
map: function(updateFn) {
|
||
mapTime = $mdUtil.time(function() {
|
||
var info = self.layoutInfo();
|
||
gridStyles = updateFn(info.positioning, info.rowCount);
|
||
});
|
||
return self;
|
||
},
|
||
|
||
/**
|
||
* Default animator simply sets the element.css( <styles> ). An alternate
|
||
* animator can be provided as an argument. The function has the following
|
||
* signature:
|
||
*
|
||
* function({grid: {element: JQLite, style: Object}, tiles: Array<{element: JQLite, style: Object}>)
|
||
*/
|
||
reflow: function(animatorFn) {
|
||
reflowTime = $mdUtil.time(function() {
|
||
var animator = animatorFn || defaultAnimator;
|
||
animator(gridStyles.grid, gridStyles.tiles);
|
||
});
|
||
return self;
|
||
},
|
||
|
||
/**
|
||
* Timing for the most recent layout run.
|
||
*/
|
||
performance: function() {
|
||
return {
|
||
tileCount: tileSpans.length,
|
||
layoutTime: layoutTime,
|
||
mapTime: mapTime,
|
||
reflowTime: reflowTime,
|
||
totalTime: layoutTime + mapTime + reflowTime
|
||
};
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Default Gridlist animator simple sets the css for each element;
|
||
* NOTE: any transitions effects must be manually set in the CSS.
|
||
* e.g.
|
||
*
|
||
* md-grid-tile {
|
||
* transition: all 700ms ease-out 50ms;
|
||
* }
|
||
*
|
||
*/
|
||
function GridTileAnimator(grid, tiles) {
|
||
grid.element.css(grid.style);
|
||
tiles.forEach(function(t) {
|
||
t.element.css(t.style);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Calculates the positions of tiles.
|
||
*
|
||
* The algorithm works as follows:
|
||
* An Array<Number> with length colCount (spaceTracker) keeps track of
|
||
* available tiling positions, where elements of value 0 represents an
|
||
* empty position. Space for a tile is reserved by finding a sequence of
|
||
* 0s with length <= than the tile's colspan. When such a space has been
|
||
* found, the occupied tile positions are incremented by the tile's
|
||
* rowspan value, as these positions have become unavailable for that
|
||
* many rows.
|
||
*
|
||
* If the end of a row has been reached without finding space for the
|
||
* tile, spaceTracker's elements are each decremented by 1 to a minimum
|
||
* of 0. Rows are searched in this fashion until space is found.
|
||
*/
|
||
function calculateGridFor(colCount, tileSpans) {
|
||
var curCol = 0,
|
||
curRow = 0,
|
||
spaceTracker = newSpaceTracker();
|
||
|
||
return {
|
||
positioning: tileSpans.map(function(spans, i) {
|
||
return {
|
||
spans: spans,
|
||
position: reserveSpace(spans, i)
|
||
};
|
||
}),
|
||
rowCount: curRow + Math.max.apply(Math, spaceTracker)
|
||
};
|
||
|
||
function reserveSpace(spans, i) {
|
||
if (spans.col > colCount) {
|
||
throw 'md-grid-list: Tile at position ' + i + ' has a colspan ' +
|
||
'(' + spans.col + ') that exceeds the column count ' +
|
||
'(' + colCount + ')';
|
||
}
|
||
|
||
var start = 0,
|
||
end = 0;
|
||
|
||
// TODO(shyndman): This loop isn't strictly necessary if you can
|
||
// determine the minimum number of rows before a space opens up. To do
|
||
// this, recognize that you've iterated across an entire row looking for
|
||
// space, and if so fast-forward by the minimum rowSpan count. Repeat
|
||
// until the required space opens up.
|
||
while (end - start < spans.col) {
|
||
if (curCol >= colCount) {
|
||
nextRow();
|
||
continue;
|
||
}
|
||
|
||
start = spaceTracker.indexOf(0, curCol);
|
||
if (start === -1 || (end = findEnd(start + 1)) === -1) {
|
||
start = end = 0;
|
||
nextRow();
|
||
continue;
|
||
}
|
||
|
||
curCol = end + 1;
|
||
}
|
||
|
||
adjustRow(start, spans.col, spans.row);
|
||
curCol = start + spans.col;
|
||
|
||
return {
|
||
col: start,
|
||
row: curRow
|
||
};
|
||
}
|
||
|
||
function nextRow() {
|
||
curCol = 0;
|
||
curRow++;
|
||
adjustRow(0, colCount, -1); // Decrement row spans by one
|
||
}
|
||
|
||
function adjustRow(from, cols, by) {
|
||
for (var i = from; i < from + cols; i++) {
|
||
spaceTracker[i] = Math.max(spaceTracker[i] + by, 0);
|
||
}
|
||
}
|
||
|
||
function findEnd(start) {
|
||
var i;
|
||
for (i = start; i < spaceTracker.length; i++) {
|
||
if (spaceTracker[i] !== 0) {
|
||
return i;
|
||
}
|
||
}
|
||
|
||
if (i === spaceTracker.length) {
|
||
return i;
|
||
}
|
||
}
|
||
|
||
function newSpaceTracker() {
|
||
var tracker = [];
|
||
for (var i = 0; i < colCount; i++) {
|
||
tracker.push(0);
|
||
}
|
||
return tracker;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @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:
|
||
* <hljs lang="html">
|
||
* <md-grid-tile>
|
||
* <md-grid-tile-header>
|
||
* <h3>This is a header</h3>
|
||
* </md-grid-tile-header>
|
||
* </md-grid-tile>
|
||
* </hljs>
|
||
*
|
||
* With footer:
|
||
* <hljs lang="html">
|
||
* <md-grid-tile>
|
||
* <md-grid-tile-footer>
|
||
* <h3>This is a footer</h3>
|
||
* </md-grid-tile-footer>
|
||
* </md-grid-tile>
|
||
* </hljs>
|
||
*
|
||
* Spanning multiple rows/columns:
|
||
* <hljs lang="html">
|
||
* <md-grid-tile md-colspan="2" md-rowspan="3">
|
||
* </md-grid-tile>
|
||
* </hljs>
|
||
*
|
||
* Responsive attributes:
|
||
* <hljs lang="html">
|
||
* <md-grid-tile md-colspan="1" md-colspan-sm="3" md-colspan-md="5">
|
||
* </md-grid-tile>
|
||
* </hljs>
|
||
*/
|
||
function GridTileDirective($mdMedia) {
|
||
return {
|
||
restrict: 'E',
|
||
require: '^mdGridList',
|
||
template: '<figure ng-transclude></figure>',
|
||
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();
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
function GridTileCaptionDirective() {
|
||
return {
|
||
template: '<figcaption ng-transclude></figcaption>',
|
||
transclude: true
|
||
};
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.icon
|
||
* @description
|
||
* Icon
|
||
*/
|
||
angular.module('material.components.icon', ['material.core']);
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
angular
|
||
.module('material.components.icon')
|
||
.directive('mdIcon', ['$mdIcon', '$mdTheming', '$mdAria', '$sce', mdIconDirective]);
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdIcon
|
||
* @module material.components.icon
|
||
*
|
||
* @restrict E
|
||
*
|
||
* @description
|
||
* The `md-icon` directive makes it easier to use vector-based icons in your app (as opposed to
|
||
* raster-based icons types like PNG). The directive supports both icon fonts and SVG icons.
|
||
*
|
||
* Icons should be considered view-only elements that should not be used directly as buttons; instead nest a `<md-icon>`
|
||
* inside a `md-button` to add hover and click features.
|
||
*
|
||
* ### Icon fonts
|
||
* Icon fonts are a technique in which you use a font where the glyphs in the font are
|
||
* your icons instead of text. Benefits include a straightforward way to bundle everything into a
|
||
* single HTTP request, simple scaling, easy color changing, and more.
|
||
*
|
||
* `md-icon` lets you consume an icon font by letting you reference specific icons in that font
|
||
* by name rather than character code.
|
||
*
|
||
* When using font-icons, developers must follow three (3) simple steps:
|
||
*
|
||
* <ol>
|
||
* <li>Load the font library. e.g.<br/>
|
||
* `<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">`
|
||
* </li>
|
||
* <li>
|
||
* Use either (a) font-icon class names or (b) a fontset and a font ligature to render the font glyph by
|
||
* using its textual name _or_ numerical character reference. Note that `material-icons` is the default fontset when
|
||
* none is specified.
|
||
* </li>
|
||
* <li> Use any of the following templates: <br/>
|
||
* <ul>
|
||
* <li>`<md-icon md-font-icon="classname"></md-icon>`</li>
|
||
* <li>`<md-icon md-font-set="font library classname or alias">textual_name</md-icon>`</li>
|
||
* <li>`<md-icon> numerical_character_reference </md-icon>`</li>
|
||
* <li>`<md-icon ng_bind="'textual_name'"></md-icon>`</li>
|
||
* <li>`<md-icon ng-bind="scopeVariable"></md-icon>`</li>
|
||
* </ul>
|
||
* </li>
|
||
* </ol>
|
||
*
|
||
* Full details for these steps can be found in the
|
||
* <a href="http://google.github.io/material-design-icons/#icon-font-for-the-web" target="_blank">
|
||
* Material Design Icon font for the web docs</a>.
|
||
*
|
||
* You can browse and search the Material Design icon style <code>.material-icons</code>
|
||
* in the <a href="https://material.io/tools/icons/" target="_blank">Material Design Icons tool</a>.
|
||
*
|
||
* ### SVG
|
||
* For SVGs, the problem with using `<img>` or a CSS `background-image` is that you can't take
|
||
* advantage of some SVG features, such as styling specific parts of the icon with CSS or SVG
|
||
* animation.
|
||
*
|
||
* `md-icon` makes it easier to use SVG icons by *inlining* the SVG into an `<svg>` element in the
|
||
* document. The most straightforward way of referencing an SVG icon is via URL, just like a
|
||
* traditional `<img>`. `$mdIconProvider`, as a convenience, lets you _name_ an icon so you can
|
||
* reference it by name instead of URL throughout your templates.
|
||
*
|
||
* Additionally, you may not want to make separate HTTP requests for every icon, so you can bundle
|
||
* your SVG icons together and pre-load them with `$mdIconProvider` as an icon set. An icon set can
|
||
* also be given a name, which acts as a namespace for individual icons, so you can reference them
|
||
* like `"social:cake"`.
|
||
*
|
||
* When using SVGs, both external SVGs (via URLs) or sets of SVGs (from icon sets) can be
|
||
* easily loaded and used.
|
||
*
|
||
* ### Localization
|
||
*
|
||
* Because an `md-icon` element's text content is not intended to be translated, it is recommended
|
||
* to declare the text content for an `md-icon` element in its start tag. Instead of using the HTML
|
||
* text content, consider using `ng-bind` with a scope variable or literal string.
|
||
*
|
||
* Examples:
|
||
*
|
||
* <ul>
|
||
* <li>`<md-icon ng-bind="myIconVariable"></md-icon>`</li>
|
||
* <li>`<md-icon ng-bind="'menu'"></md-icon>`
|
||
* </ul>
|
||
*
|
||
* <h2 id="material_design_icons">Material Design Icons tool</h2>
|
||
* Using the Material Design Icons tool, developers can easily and quickly search for a specific
|
||
* open source Material Design icon. The search is in the top left. Below search, you can select
|
||
* from the new icon themes or filter by icon category.
|
||
*
|
||
* <a href="https://material.io/tools/icons/" target="_blank" style="border-bottom:none;">
|
||
* <img src="https://user-images.githubusercontent.com/3506071/41942584-ef0695d0-796d-11e8-9436-44f25023a111.png"
|
||
* aria-label="Material Design Icons tool" style="max-width:95%">
|
||
* </a>
|
||
*
|
||
* <div class="md-caption" style="text-align: center; margin-bottom: 24px">
|
||
* Click on the image above to open the
|
||
* <a href="https://material.io/tools/icons/" target="_blank">Material Design Icons tool</a>.
|
||
* </div>
|
||
*
|
||
* Click on any icon, then click on the "Selected Icon" chevron to see the slide-up
|
||
* information panel with details regarding a SVG download and information on the font-icon's
|
||
* textual name. This panel also allows you to select a black on transparent or white on transparent
|
||
* icon and to change the icon size. These settings only affect the downloaded icons.
|
||
*
|
||
* @param {string} md-font-icon String name of CSS icon associated with the font-face will be used
|
||
* to render the icon. Requires the fonts and the named CSS styles to be preloaded.
|
||
* @param {string} md-font-set CSS style name associated with the font library; which will be assigned as
|
||
* the class for the font-icon ligature. This value may also be an alias that is used to lookup the classname;
|
||
* internally use `$mdIconProvider.fontSet(<alias>)` to determine the style name.
|
||
* @param {string} md-svg-src String URL (or expression) used to load, cache, and display an
|
||
* external SVG.
|
||
* @param {string} md-svg-icon md-svg-icon String name used for lookup of the icon from the internal cache;
|
||
* interpolated strings or expressions may also be used. Specific set names can be used with
|
||
* the syntax `<set name>:<icon name>`.<br/><br/>
|
||
* To use icon sets, developers are required to pre-register the sets using the `$mdIconProvider` service.
|
||
* @param {string=} aria-label Labels the icon for accessibility. If an empty string is provided,
|
||
* the icon will be hidden from the accessibility layer with `aria-hidden="true"`. If there is no
|
||
* `aria-label` attribute on the icon, we check the following, in order: the `alt` attribute, the
|
||
* `aria-label` from the parent element, the icon's `md-font-icon` or `md-svg-icon` string, and the
|
||
* text content inside `<md-icon></md-icon>`. If none of these have any text, the icon is hidden
|
||
* from the accessibility layer with `aria-hidden="true"`.
|
||
* @param {string=} alt Labels the icon for accessibility. If an empty string is provided and the
|
||
* icon has no `aria-label`, then the icon will be hidden from accessibility layer with
|
||
* `aria-hidden="true"`.
|
||
*
|
||
* @usage
|
||
* When using SVGs:
|
||
* <hljs lang="html">
|
||
*
|
||
*<!-- Icon ID; may contain optional icon set prefix.
|
||
* Icons must be registered using $mdIconProvider. -->
|
||
*<md-icon md-svg-icon="social:android" aria-label="android " ></md-icon>
|
||
*
|
||
*<!-- Icon urls; may be preloaded in templateCache -->
|
||
*<md-icon md-svg-src="/android.svg" aria-label="android " ></md-icon>
|
||
*<md-icon md-svg-src="{{ getAndroid() }}" aria-label="android " ></md-icon>
|
||
*
|
||
* </hljs>
|
||
*
|
||
* Use the <code>$mdIconProvider</code> to configure your application with
|
||
* SVG icon sets.
|
||
*
|
||
* <hljs lang="js">
|
||
* angular.module('appSvgIconSets', ['ngMaterial'])
|
||
* .controller('DemoCtrl', function($scope) {})
|
||
* .config(function($mdIconProvider) {
|
||
* $mdIconProvider
|
||
* .iconSet('social', 'img/icons/sets/social-icons.svg', 24)
|
||
* .defaultIconSet('img/icons/sets/core-icons.svg', 24);
|
||
* });
|
||
* </hljs>
|
||
*
|
||
*
|
||
* When using Font Icons with classnames:
|
||
* <hljs lang="html">
|
||
*
|
||
* <md-icon md-font-icon="android" aria-label="android" ></md-icon>
|
||
* <md-icon class="icon_home" aria-label="Home"></md-icon>
|
||
*
|
||
* </hljs>
|
||
*
|
||
* When using Material Font Icons with ligatures:
|
||
* <hljs lang="html">
|
||
* <!--
|
||
* For Material Design Icons
|
||
* The class '.material-icons' is auto-added if a style has NOT been specified
|
||
* since `material-icons` is the default fontset. So your markup:
|
||
* -->
|
||
* <md-icon> face </md-icon>
|
||
* <!-- becomes this at runtime: -->
|
||
* <md-icon md-font-set="material-icons"> face </md-icon>
|
||
* <!-- If the fontset does not support ligature names, then we need to use the ligature unicode.-->
|
||
* <md-icon>  </md-icon>
|
||
* <!-- The class '.material-icons' must be manually added if other styles are also specified-->
|
||
* <md-icon class="material-icons md-light md-48"> face </md-icon>
|
||
* </hljs>
|
||
*
|
||
* When using other Font-Icon libraries:
|
||
*
|
||
* <hljs lang="js">
|
||
* // Specify a font-icon style alias
|
||
* angular.config(function($mdIconProvider) {
|
||
* $mdIconProvider.fontSet('md', 'material-icons');
|
||
* });
|
||
* </hljs>
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-icon md-font-set="md">favorite</md-icon>
|
||
* </hljs>
|
||
*
|
||
*/
|
||
function mdIconDirective($mdIcon, $mdTheming, $mdAria, $sce) {
|
||
|
||
return {
|
||
restrict: 'E',
|
||
link : postLink
|
||
};
|
||
|
||
|
||
/**
|
||
* Directive postLink
|
||
* Supports embedded SVGs, font-icons, & external SVGs.
|
||
* @param {IScope} scope
|
||
* @param {JQLite} element
|
||
* @param {IAttributes} attr
|
||
*/
|
||
function postLink(scope, element, attr) {
|
||
$mdTheming(element);
|
||
var lastFontIcon = attr.mdFontIcon;
|
||
var lastFontSet = $mdIcon.fontSet(attr.mdFontSet);
|
||
|
||
prepareForFontIcon();
|
||
|
||
attr.$observe('mdFontIcon', fontIconChanged);
|
||
attr.$observe('mdFontSet', fontIconChanged);
|
||
|
||
/* Provide a default accessibility role of img */
|
||
if (!attr.role) {
|
||
$mdAria.expect(element, 'role', 'img');
|
||
/* manually update attr variable */
|
||
attr.role = 'img';
|
||
}
|
||
|
||
// If the aria-label is explicitly set to the empty string, then hide this element from the
|
||
// accessibility layer.
|
||
if (element[0].hasAttribute('aria-label') && attr.ariaLabel === '') {
|
||
element.attr('aria-hidden', true);
|
||
}
|
||
|
||
/* Don't process ARIA if already valid */
|
||
if (attr.role === "img" && !attr.ariaHidden && !$mdAria.hasAriaLabel(element)) {
|
||
// If the developer signals to hide this icon from the accessibility layer, do so.
|
||
if (element[0].hasAttribute('alt') && attr.alt === '') {
|
||
element.attr('aria-hidden', true);
|
||
} else if (attr.alt) {
|
||
/* Use the alt text for the aria-label by default, if available. */
|
||
$mdAria.expect(element, 'aria-label', attr.alt);
|
||
} else if ($mdAria.parentHasAriaLabel(element, 2)) {
|
||
/* Parent has ARIA so we will assume it will describe the icon. */
|
||
$mdAria.expect(element, 'aria-hidden', 'true');
|
||
} else if (attr.mdFontIcon || attr.mdSvgIcon || element.text()) {
|
||
/* Use icon name or node's text content as the aria-label */
|
||
$mdAria.expect(element, 'aria-label', attr.mdFontIcon || attr.mdSvgIcon || element.text());
|
||
} else {
|
||
/* No label found, hide this icon from the accessibility layer */
|
||
$mdAria.expect(element, 'aria-hidden', 'true');
|
||
}
|
||
}
|
||
|
||
var attrName = attr.$normalize(attr.$attr.mdSvgIcon || attr.$attr.mdSvgSrc || '');
|
||
if (attrName) {
|
||
// Use either pre-configured SVG or URL source, respectively.
|
||
attr.$observe(attrName, function(attrVal) {
|
||
element.empty();
|
||
if (attrVal) {
|
||
$mdIcon(attrVal)
|
||
.then(function(svg) {
|
||
element.empty();
|
||
element.append(svg);
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
function prepareForFontIcon() {
|
||
if (!attr.mdSvgIcon && !attr.mdSvgSrc) {
|
||
if (attr.mdFontIcon) {
|
||
element.addClass('md-font ' + attr.mdFontIcon);
|
||
}
|
||
|
||
element.addClass(lastFontSet);
|
||
}
|
||
}
|
||
|
||
function fontIconChanged() {
|
||
if (!attr.mdSvgIcon && !attr.mdSvgSrc) {
|
||
if (attr.mdFontIcon) {
|
||
element.removeClass(lastFontIcon);
|
||
element.addClass(attr.mdFontIcon);
|
||
|
||
lastFontIcon = attr.mdFontIcon;
|
||
}
|
||
|
||
var fontSet = $mdIcon.fontSet(attr.mdFontSet);
|
||
|
||
if (lastFontSet !== fontSet) {
|
||
element.removeClass(lastFontSet);
|
||
element.addClass(fontSet);
|
||
|
||
lastFontSet = fontSet;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
|
||
MdIconService.$inject = ["config", "$templateRequest", "$q", "$log", "$mdUtil", "$sce"];angular
|
||
.module('material.components.icon')
|
||
.constant('$$mdSvgRegistry', {
|
||
'mdTabsArrow': 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxnPjxwb2x5Z29uIHBvaW50cz0iMTUuNCw3LjQgMTQsNiA4LDEyIDE0LDE4IDE1LjQsMTYuNiAxMC44LDEyICIvPjwvZz48L3N2Zz4=',
|
||
'mdClose': 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxnPjxwYXRoIGQ9Ik0xOSA2LjQxbC0xLjQxLTEuNDEtNS41OSA1LjU5LTUuNTktNS41OS0xLjQxIDEuNDEgNS41OSA1LjU5LTUuNTkgNS41OSAxLjQxIDEuNDEgNS41OS01LjU5IDUuNTkgNS41OSAxLjQxLTEuNDEtNS41OS01LjU5eiIvPjwvZz48L3N2Zz4=',
|
||
'mdCancel': 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxnPjxwYXRoIGQ9Ik0xMiAyYy01LjUzIDAtMTAgNC40Ny0xMCAxMHM0LjQ3IDEwIDEwIDEwIDEwLTQuNDcgMTAtMTAtNC40Ny0xMC0xMC0xMHptNSAxMy41OWwtMS40MSAxLjQxLTMuNTktMy41OS0zLjU5IDMuNTktMS40MS0xLjQxIDMuNTktMy41OS0zLjU5LTMuNTkgMS40MS0xLjQxIDMuNTkgMy41OSAzLjU5LTMuNTkgMS40MSAxLjQxLTMuNTkgMy41OSAzLjU5IDMuNTl6Ii8+PC9nPjwvc3ZnPg==',
|
||
'mdMenu': 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0zLDZIMjFWOEgzVjZNMywxMUgyMVYxM0gzVjExTTMsMTZIMjFWMThIM1YxNloiIC8+PC9zdmc+',
|
||
'mdToggleArrow': 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgNDggNDgiPjxwYXRoIGQ9Ik0yNCAxNmwtMTIgMTIgMi44MyAyLjgzIDkuMTctOS4xNyA5LjE3IDkuMTcgMi44My0yLjgzeiIvPjxwYXRoIGQ9Ik0wIDBoNDh2NDhoLTQ4eiIgZmlsbD0ibm9uZSIvPjwvc3ZnPg==',
|
||
'mdCalendar': 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTkgM2gtMVYxaC0ydjJIOFYxSDZ2Mkg1Yy0xLjExIDAtMS45OS45LTEuOTkgMkwzIDE5YzAgMS4xLjg5IDIgMiAyaDE0YzEuMSAwIDItLjkgMi0yVjVjMC0xLjEtLjktMi0yLTJ6bTAgMTZINVY4aDE0djExek03IDEwaDV2NUg3eiIvPjwvc3ZnPg==',
|
||
'mdChecked': 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxnPjxwYXRoIGQ9Ik05IDE2LjE3TDQuODMgMTJsLTEuNDIgMS40MUw5IDE5IDIxIDdsLTEuNDEtMS40MXoiLz48L2c+PC9zdmc+'
|
||
})
|
||
.provider('$mdIcon', MdIconProvider);
|
||
|
||
/**
|
||
* @ngdoc service
|
||
* @name $mdIconProvider
|
||
* @module material.components.icon
|
||
*
|
||
* @description
|
||
* `$mdIconProvider` is used only to register icon IDs with URLs. These configuration features allow
|
||
* icons and icon sets to be pre-registered and associated with source URLs **before** the
|
||
* `<md-icon />` directives are compiled.
|
||
*
|
||
* If using font-icons, the developer is responsible for loading the fonts.
|
||
*
|
||
* If using SVGs, loading of the actual svg files are deferred to on-demand requests and are loaded
|
||
* internally by the `$mdIcon` service using the `$templateRequest` service. When an SVG is
|
||
* requested by name/ID, the `$mdIcon` service searches its registry for the associated source URL;
|
||
* that URL is used to on-demand load and parse the SVG dynamically.
|
||
*
|
||
* The `$templateRequest` service expects the icons source to be loaded over trusted URLs.<br/>
|
||
* This means, when loading icons from an external URL, you have to trust the URL in the
|
||
* `$sceDelegateProvider`.
|
||
*
|
||
* <hljs lang="js">
|
||
* app.config(function($sceDelegateProvider) {
|
||
* $sceDelegateProvider.trustedResourceUrlList([
|
||
* // Adding 'self' to the allow-list, will allow requests from the current origin.
|
||
* 'self',
|
||
* // Using double asterisks here, will allow all URLs to load.
|
||
* // However, we recommend only specifying the given domain you want to allow.
|
||
* '**'
|
||
* ]);
|
||
* });
|
||
* </hljs>
|
||
*
|
||
* Read more about the [$sceDelegateProvider](https://docs.angularjs.org/api/ng/provider/$sceDelegateProvider).
|
||
*
|
||
* **Notice:** Most font-icon libraries do not support ligatures (for example `fontawesome`).<br/>
|
||
* In such cases you are not able to use the icon's ligature name - Like so:
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-icon md-font-set="fa">fa-bell</md-icon>
|
||
* </hljs>
|
||
*
|
||
* You should instead use the given unicode, instead of the ligature name.
|
||
*
|
||
* <p ng-hide="true"> ##// Notice we can't use a hljs element here, because the characters will be escaped.</p>
|
||
* ```html
|
||
* <md-icon md-font-set="fa"></md-icon>
|
||
* ```
|
||
*
|
||
* All unicode ligatures are prefixed with the `&#x` string.
|
||
*
|
||
* @usage
|
||
* <hljs lang="js">
|
||
* app.config(function($mdIconProvider) {
|
||
*
|
||
* // Configure URLs for icons specified by [set:]id.
|
||
* $mdIconProvider
|
||
* .defaultFontSet( 'fa' ) // This sets our default fontset className.
|
||
* .defaultIconSet('my/app/icons.svg') // Register a default set of SVG icons
|
||
* .iconSet('social', 'my/app/social.svg') // Register a named icon set of SVGs
|
||
* .icon('android', 'my/app/android.svg') // Register a specific icon (by name)
|
||
* .icon('work:chair', 'my/app/chair.svg'); // Register icon in a specific set
|
||
* });
|
||
* </hljs>
|
||
*
|
||
* SVG icons and icon sets can be easily pre-loaded and cached using either (a) a build process or
|
||
* (b) a runtime **startup** process (shown below):
|
||
*
|
||
* <hljs lang="js">
|
||
* app.config(function($mdIconProvider) {
|
||
*
|
||
* // Register a default set of SVG icon definitions
|
||
* $mdIconProvider.defaultIconSet('my/app/icons.svg')
|
||
* })
|
||
* .run(function($templateRequest) {
|
||
*
|
||
* // Pre-fetch icons sources by URL and cache in the $templateCache...
|
||
* // subsequent $templateRequest calls will look there first.
|
||
* var urls = [ 'imy/app/icons.svg', 'img/icons/android.svg'];
|
||
*
|
||
* angular.forEach(urls, function(url) {
|
||
* $templateRequest(url);
|
||
* });
|
||
* });
|
||
*
|
||
* </hljs>
|
||
*
|
||
* > <b>Note:</b> The loaded SVG data is subsequently cached internally for future requests.
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdIconProvider#icon
|
||
*
|
||
* @description
|
||
* Register a source URL for a specific icon name; the name may include optional 'icon set' name
|
||
* prefix. These icons will later be retrieved from the cache using `$mdIcon(<icon name>)`.
|
||
*
|
||
* @param {string} id Icon name/id used to register the icon
|
||
* @param {string} url specifies the external location for the data file. Used internally by
|
||
* `$templateRequest` to load the data or as part of the lookup in `$templateCache` if pre-loading
|
||
* was configured.
|
||
* @param {number=} viewBoxSize Sets the width and height the icon's viewBox.
|
||
* It is ignored for icons with an existing viewBox. Default size is 24.
|
||
*
|
||
* @returns {obj} an `$mdIconProvider` reference; used to support method call chains for the API
|
||
*
|
||
* @usage
|
||
* <hljs lang="js">
|
||
* app.config(function($mdIconProvider) {
|
||
*
|
||
* // Configure URLs for icons specified by [set:]id.
|
||
* $mdIconProvider
|
||
* .icon('android', 'my/app/android.svg') // Register a specific icon (by name)
|
||
* .icon('work:chair', 'my/app/chair.svg'); // Register icon in a specific set
|
||
* });
|
||
* </hljs>
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdIconProvider#iconSet
|
||
*
|
||
* @description
|
||
* Register a source URL for a 'named' set of icons; group of SVG definitions where each definition
|
||
* has an icon id. Individual icons can be subsequently retrieved from this cached set using
|
||
* `$mdIcon(<icon set name>:<icon name>)`
|
||
*
|
||
* @param {string} id Icon name/id used to register the iconset
|
||
* @param {string} url specifies the external location for the data file. Used internally by
|
||
* `$templateRequest` to load the data or as part of the lookup in `$templateCache` if pre-loading
|
||
* was configured.
|
||
* @param {number=} viewBoxSize Sets the width and height of the viewBox of all icons in the set.
|
||
* It is ignored for icons with an existing viewBox. All icons in the icon set should be the same size.
|
||
* Default value is 24.
|
||
*
|
||
* @returns {obj} an `$mdIconProvider` reference; used to support method call chains for the API
|
||
*
|
||
* @usage
|
||
* <hljs lang="js">
|
||
* app.config(function($mdIconProvider) {
|
||
*
|
||
* // Configure URLs for icons specified by [set:]id.
|
||
* $mdIconProvider
|
||
* .iconSet('social', 'my/app/social.svg'); // Register a named icon set
|
||
* });
|
||
* </hljs>
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdIconProvider#defaultIconSet
|
||
*
|
||
* @description
|
||
* Register a source URL for the default 'named' set of icons. Unless explicitly registered,
|
||
* subsequent lookups of icons will fail over to search this 'default' icon set.
|
||
* Icon can be retrieved from this cached, default set using `$mdIcon(<name>)`
|
||
*
|
||
* @param {string} url specifies the external location for the data file. Used internally by
|
||
* `$templateRequest` to load the data or as part of the lookup in `$templateCache` if pre-loading
|
||
* was configured.
|
||
* @param {number=} viewBoxSize Sets the width and height of the viewBox of all icons in the set.
|
||
* It is ignored for icons with an existing viewBox. All icons in the icon set should be the same
|
||
* size. Default value is 24.
|
||
*
|
||
* @returns {Object} an `$mdIconProvider` reference; used to support method call chains for the API
|
||
*
|
||
* @usage
|
||
* <hljs lang="js">
|
||
* app.config(function($mdIconProvider) {
|
||
*
|
||
* // Configure URLs for icons specified by [set:]id.
|
||
* $mdIconProvider
|
||
* .defaultIconSet('my/app/social.svg'); // Register a default icon set
|
||
* });
|
||
* </hljs>
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdIconProvider#defaultFontSet
|
||
*
|
||
* @description
|
||
* When using Font-Icons, AngularJS Material assumes the the Material Design icons will be used and
|
||
* automatically configures the default `font-set == 'material-icons'`. Note that the font-set
|
||
* references the font-icon library class style that should be applied to the `<md-icon>`.
|
||
*
|
||
* Configuring the default means that the attributes
|
||
* `md-font-set="material-icons"` or `class="material-icons"` do not need to be explicitly declared
|
||
* on the `<md-icon>` markup.
|
||
*
|
||
* For example:<br/>
|
||
* `<md-icon>face</md-icon>` will render as `<span class="material-icons">face</span>`,<br/>
|
||
* and<br/>
|
||
* `<md-icon md-font-set="fa">face</md-icon>` will render as `<span class="fa">face</span>`
|
||
*
|
||
* @param {string} name Name of the font-library style that should be applied to the md-icon DOM
|
||
* element.
|
||
*
|
||
* @usage
|
||
* <hljs lang="js">
|
||
* app.config(function($mdIconProvider) {
|
||
* $mdIconProvider.defaultFontSet('fa');
|
||
* });
|
||
* </hljs>
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdIconProvider#fontSet
|
||
*
|
||
* @description
|
||
* When using a font-set for `<md-icon>` you must specify the correct font classname in the
|
||
* `md-font-set` attribute. If the font-set className is really long, your markup may become
|
||
* cluttered... an easy solution is to define an `alias` for your font-set:
|
||
*
|
||
* @param {string} alias Alias name of the specified font-set.
|
||
* @param {string} className Name of the class for the font-set.
|
||
*
|
||
* @usage
|
||
* <hljs lang="js">
|
||
* app.config(function($mdIconProvider) {
|
||
* // In this case, we set an alias for the `material-icons` font-set.
|
||
* $mdIconProvider.fontSet('md', 'material-icons');
|
||
* });
|
||
* </hljs>
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdIconProvider#defaultViewBoxSize
|
||
*
|
||
* @description
|
||
* While `<md-icon>` markup can also be styled with sizing CSS, this method configures
|
||
* the default width **and** height used for all icons; unless overridden by specific CSS.
|
||
* The default sizing is (`24px`, `24px`).
|
||
* @param {number=} viewBoxSize Sets the width and height of the viewBox for an icon or an icon set.
|
||
* All icons in a set should be the same size. The default value is 24.
|
||
*
|
||
* @returns {Object} an `$mdIconProvider` reference; used to support method call chains for the API
|
||
*
|
||
* @usage
|
||
* <hljs lang="js">
|
||
* app.config(function($mdIconProvider) {
|
||
*
|
||
* // Configure URLs for icons specified by [set:]id.
|
||
* $mdIconProvider
|
||
* .defaultViewBoxSize(36); // Register a default icon size (width == height)
|
||
* });
|
||
* </hljs>
|
||
*/
|
||
|
||
var config = {
|
||
defaultViewBoxSize: 24,
|
||
defaultFontSet: 'material-icons',
|
||
fontSets: []
|
||
};
|
||
|
||
function MdIconProvider() {
|
||
}
|
||
|
||
MdIconProvider.prototype = {
|
||
icon: function(id, url, viewBoxSize) {
|
||
if (id.indexOf(':') == -1) id = '$default:' + id;
|
||
|
||
config[id] = new ConfigurationItem(url, viewBoxSize);
|
||
return this;
|
||
},
|
||
|
||
iconSet: function(id, url, viewBoxSize) {
|
||
config[id] = new ConfigurationItem(url, viewBoxSize);
|
||
return this;
|
||
},
|
||
|
||
defaultIconSet: function(url, viewBoxSize) {
|
||
var setName = '$default';
|
||
|
||
if (!config[setName]) {
|
||
config[setName] = new ConfigurationItem(url, viewBoxSize);
|
||
}
|
||
|
||
config[setName].viewBoxSize = viewBoxSize || config.defaultViewBoxSize;
|
||
|
||
return this;
|
||
},
|
||
|
||
defaultViewBoxSize: function(viewBoxSize) {
|
||
config.defaultViewBoxSize = viewBoxSize;
|
||
return this;
|
||
},
|
||
|
||
/**
|
||
* Register an alias name associated with a font-icon library style ;
|
||
*/
|
||
fontSet: function fontSet(alias, className) {
|
||
config.fontSets.push({
|
||
alias: alias,
|
||
fontSet: className || alias
|
||
});
|
||
return this;
|
||
},
|
||
|
||
/**
|
||
* Specify a default style name associated with a font-icon library
|
||
* fallback to Material Icons.
|
||
*
|
||
*/
|
||
defaultFontSet: function defaultFontSet(className) {
|
||
config.defaultFontSet = !className ? '' : className;
|
||
return this;
|
||
},
|
||
|
||
defaultIconSize: function defaultIconSize(iconSize) {
|
||
config.defaultIconSize = iconSize;
|
||
return this;
|
||
},
|
||
|
||
$get: ['$templateRequest', '$q', '$log', '$mdUtil', '$sce', function($templateRequest, $q, $log, $mdUtil, $sce) {
|
||
return MdIconService(config, $templateRequest, $q, $log, $mdUtil, $sce);
|
||
}]
|
||
};
|
||
|
||
/**
|
||
* Configuration item stored in the Icon registry; used for lookups
|
||
* to load if not already cached in the `loaded` cache
|
||
* @param {string} url
|
||
* @param {=number} viewBoxSize
|
||
* @constructor
|
||
*/
|
||
function ConfigurationItem(url, viewBoxSize) {
|
||
this.url = url;
|
||
this.viewBoxSize = viewBoxSize || config.defaultViewBoxSize;
|
||
}
|
||
|
||
/**
|
||
* @ngdoc service
|
||
* @name $mdIcon
|
||
* @module material.components.icon
|
||
*
|
||
* @description
|
||
* The `$mdIcon` service is a function used to lookup SVG icons.
|
||
*
|
||
* @param {string} id Query value for a unique Id or URL. If the argument is a URL, then the service will retrieve the icon element
|
||
* from its internal cache or load the icon and cache it first. If the value is not a URL-type string, then an ID lookup is
|
||
* performed. The Id may be a unique icon ID or may include an iconSet ID prefix.
|
||
*
|
||
* For the **id** query to work properly, this means that all id-to-URL mappings must have been previously configured
|
||
* using the `$mdIconProvider`.
|
||
*
|
||
* @returns {angular.$q.Promise} A promise that gets resolved to a clone of the initial SVG DOM element; which was
|
||
* created from the SVG markup in the SVG data file. If an error occurs (e.g. the icon cannot be found) the promise
|
||
* will get rejected.
|
||
*
|
||
* @usage
|
||
* <hljs lang="js">
|
||
* function SomeDirective($mdIcon) {
|
||
*
|
||
* // See if the icon has already been loaded, if not then lookup the icon from the
|
||
* // registry cache, load and cache it for future requests.
|
||
* // NOTE: Non-URL queries require configuration with $mdIconProvider.
|
||
* $mdIcon('android').then(function(iconEl) { element.append(iconEl); });
|
||
* $mdIcon('work:chair').then(function(iconEl) { element.append(iconEl); });
|
||
*
|
||
* // Load and cache the external SVG using a URL.
|
||
* $mdIcon('img/icons/android.svg').then(function(iconEl) {
|
||
* element.append(iconEl);
|
||
* });
|
||
* };
|
||
* </hljs>
|
||
*
|
||
* > <b>Note:</b> The `<md-icon>` directive internally uses the `$mdIcon` service to query, load,
|
||
* and instantiate SVG DOM elements.
|
||
*/
|
||
|
||
/* @ngInject */
|
||
function MdIconService(config, $templateRequest, $q, $log, $mdUtil, $sce) {
|
||
var iconCache = {};
|
||
var svgCache = {};
|
||
var urlRegex = /[-\w@:%+.~#?&//=]{2,}\.[a-z]{2,4}\b(\/[-\w@:%+.~#?&//=]*)?/i;
|
||
var dataUrlRegex = /^data:image\/svg\+xml[\s*;\w\-=]*?(base64)?,(.*)$/i;
|
||
|
||
Icon.prototype = {clone: cloneSVG, prepare: prepareAndStyle};
|
||
getIcon.fontSet = findRegisteredFontSet;
|
||
|
||
// Publish service...
|
||
return getIcon;
|
||
|
||
/**
|
||
* Actual $mdIcon service is essentially a lookup function
|
||
* @param {*} id $sce trust wrapper over a URL string, URL, icon registry id, or icon set id
|
||
* @returns {angular.$q.Promise}
|
||
*/
|
||
function getIcon(id) {
|
||
id = id || '';
|
||
|
||
// If the "id" provided is not a string, the only other valid value is a $sce trust wrapper
|
||
// over a URL string. If the value is not trusted, this will intentionally throw an error
|
||
// because the user is attempted to use an unsafe URL, potentially opening themselves up
|
||
// to an XSS attack.
|
||
if (!angular.isString(id)) {
|
||
id = $sce.getTrustedUrl(id);
|
||
}
|
||
|
||
// If already loaded and cached, use a clone of the cached icon.
|
||
// Otherwise either load by URL, or lookup in the registry and then load by URL, and cache.
|
||
|
||
if (iconCache[id]) {
|
||
return $q.when(transformClone(iconCache[id]));
|
||
}
|
||
|
||
if (urlRegex.test(id) || dataUrlRegex.test(id)) {
|
||
return loadByURL(id).then(cacheIcon(id));
|
||
}
|
||
|
||
if (id.indexOf(':') === -1) {
|
||
id = '$default:' + id;
|
||
}
|
||
|
||
var load = config[id] ? loadByID : loadFromIconSet;
|
||
return load(id)
|
||
.then(cacheIcon(id));
|
||
}
|
||
|
||
/**
|
||
* Lookup a registered fontSet style using its alias.
|
||
* @param {string} alias used to lookup the alias in the array of fontSets
|
||
* @returns {*} matching fontSet or the defaultFontSet if that alias does not match
|
||
*/
|
||
function findRegisteredFontSet(alias) {
|
||
var useDefault = angular.isUndefined(alias) || !(alias && alias.length);
|
||
if (useDefault) {
|
||
return config.defaultFontSet;
|
||
}
|
||
|
||
var result = alias;
|
||
angular.forEach(config.fontSets, function(fontSet) {
|
||
if (fontSet.alias === alias) {
|
||
result = fontSet.fontSet || result;
|
||
}
|
||
});
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* @param {!Icon} cacheElement cached icon from the iconCache
|
||
* @returns {Icon} cloned Icon element with unique ids
|
||
*/
|
||
function transformClone(cacheElement) {
|
||
var clone = cacheElement.clone();
|
||
var newUid = $mdUtil.nextUid();
|
||
var cacheSuffix, svgUrlQuerySelector, i, xlinkHrefValue;
|
||
// These are SVG attributes that can reference element ids.
|
||
var svgUrlAttributes = [
|
||
'clip-path', 'color-profile', 'cursor', 'fill', 'filter', 'href', 'marker-start',
|
||
'marker-mid', 'marker-end', 'mask', 'stroke', 'style', 'vector-effect'
|
||
];
|
||
var isIeSvg = clone.innerHTML === undefined;
|
||
|
||
// Verify that the newUid only contains a number and not some XSS content.
|
||
if (!isFinite(Number(newUid))) {
|
||
throw new Error('Unsafe and unexpected non-number result from $mdUtil.nextUid().');
|
||
}
|
||
cacheSuffix = '_cache' + newUid;
|
||
|
||
// For each cached icon, we need to modify the id attributes and references.
|
||
// This is needed because SVG ids are treated as normal DOM ids and should not be duplicated on
|
||
// the page.
|
||
if (clone.id) {
|
||
clone.id += cacheSuffix;
|
||
}
|
||
|
||
// Do as much as possible with querySelectorAll as it provides much greater performance
|
||
// than RegEx against serialized DOM.
|
||
angular.forEach(clone.querySelectorAll('[id]'), function(descendantElem) {
|
||
svgUrlQuerySelector = '';
|
||
for (i = 0; i < svgUrlAttributes.length; i++) {
|
||
svgUrlQuerySelector += '[' + svgUrlAttributes[i] + '="url(#' + descendantElem.id + ')"]';
|
||
if (i + 1 < svgUrlAttributes.length) {
|
||
svgUrlQuerySelector += ', ';
|
||
}
|
||
}
|
||
// Append the cacheSuffix to references of the element's id within url(#id) calls.
|
||
angular.forEach(clone.querySelectorAll(svgUrlQuerySelector), function(refItem) {
|
||
updateSvgIdReferences(descendantElem, refItem, isIeSvg, newUid);
|
||
});
|
||
// Handle usages of url(#id) in the SVG's stylesheets
|
||
angular.forEach(clone.querySelectorAll('style'), function(refItem) {
|
||
updateSvgIdReferences(descendantElem, refItem, isIeSvg, newUid);
|
||
});
|
||
|
||
// Update ids referenced by the deprecated (in SVG v2) xlink:href XML attribute. The now
|
||
// preferred href attribute is handled above. However, this non-standard XML namespaced
|
||
// attribute cannot be handled in the same way. Explanation of this query selector here:
|
||
// https://stackoverflow.com/q/23034283/633107.
|
||
angular.forEach(clone.querySelectorAll('[*|href]:not([href])'), function(refItem) {
|
||
xlinkHrefValue = refItem.getAttribute('xlink:href');
|
||
if (xlinkHrefValue) {
|
||
xlinkHrefValue = xlinkHrefValue.replace("#" + descendantElem.id, "#" + descendantElem.id + cacheSuffix);
|
||
refItem.setAttribute('xlink:href', xlinkHrefValue);
|
||
}
|
||
});
|
||
|
||
descendantElem.id += cacheSuffix;
|
||
});
|
||
|
||
return clone;
|
||
}
|
||
|
||
/**
|
||
* @param {Element} referencedElement element w/ id that needs to be updated
|
||
* @param {Element} referencingElement element that references the original id
|
||
* @param {boolean} isIeSvg true if we're dealing with an SVG in IE11, false otherwise
|
||
* @param {string} newUid the cache id to add as part of the cache suffix
|
||
*/
|
||
function updateSvgIdReferences(referencedElement, referencingElement, isIeSvg, newUid) {
|
||
var svgElement, cacheSuffix;
|
||
|
||
// Verify that the newUid only contains a number and not some XSS content.
|
||
if (!isFinite(Number(newUid))) {
|
||
throw new Error('Unsafe and unexpected non-number result for newUid.');
|
||
}
|
||
cacheSuffix = '_cache' + newUid;
|
||
|
||
// outerHTML of SVG elements is not supported by IE11
|
||
if (isIeSvg) {
|
||
svgElement = $mdUtil.getOuterHTML(referencingElement);
|
||
svgElement = svgElement.replace("url(#" + referencedElement.id + ")",
|
||
"url(#" + referencedElement.id + cacheSuffix + ")");
|
||
referencingElement.textContent = angular.element(svgElement)[0].innerHTML;
|
||
} else {
|
||
// This use of outerHTML should be safe from XSS attack since we are only injecting the
|
||
// cacheSuffix with content from $mdUtil.nextUid which we verify is a finite number above.
|
||
referencingElement.outerHTML = referencingElement.outerHTML.replace(
|
||
"url(#" + referencedElement.id + ")",
|
||
"url(#" + referencedElement.id + cacheSuffix + ")");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Prepare and cache the loaded icon for the specified `id`.
|
||
* @param {string} id icon cache id
|
||
* @returns {function(*=): *}
|
||
*/
|
||
function cacheIcon(id) {
|
||
|
||
return function updateCache(icon) {
|
||
iconCache[id] = isIcon(icon) ? icon : new Icon(icon, config[id]);
|
||
|
||
return transformClone(iconCache[id]);
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Lookup the configuration in the registry, if !registered throw an error
|
||
* otherwise load the icon [on-demand] using the registered URL.
|
||
* @param {string} id icon registry id
|
||
* @returns {angular.$q.Promise}
|
||
*/
|
||
function loadByID(id) {
|
||
var iconConfig = config[id];
|
||
return loadByURL(iconConfig.url).then(function(icon) {
|
||
return new Icon(icon, iconConfig);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Loads the file as XML and uses querySelector( <id> ) to find the desired node...
|
||
* @param {string} id icon id in icon set
|
||
* @returns {angular.$q.Promise}
|
||
*/
|
||
function loadFromIconSet(id) {
|
||
var setName = id.substring(0, id.lastIndexOf(':')) || '$default';
|
||
var iconSetConfig = config[setName];
|
||
|
||
return !iconSetConfig ? announceIdNotFound(id) : loadByURL(iconSetConfig.url).then(extractFromSet);
|
||
|
||
function extractFromSet(set) {
|
||
var iconName = id.slice(id.lastIndexOf(':') + 1);
|
||
var icon = set.querySelector('#' + iconName);
|
||
return icon ? new Icon(icon, iconSetConfig) : announceIdNotFound(id);
|
||
}
|
||
|
||
function announceIdNotFound(id) {
|
||
var msg = 'icon ' + id + ' not found';
|
||
$log.warn(msg);
|
||
|
||
return $q.reject(msg || id);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Load the icon by URL (may use the $templateCache).
|
||
* Extract the data for later conversion to Icon
|
||
* @param {string} url icon URL
|
||
* @returns {angular.$q.Promise}
|
||
*/
|
||
function loadByURL(url) {
|
||
/* Load the icon from embedded data URL. */
|
||
function loadByDataUrl(url) {
|
||
var results = dataUrlRegex.exec(url);
|
||
var isBase64 = /base64/i.test(url);
|
||
var data = isBase64 ? window.atob(results[2]) : results[2];
|
||
|
||
return $q.when(angular.element(data)[0]);
|
||
}
|
||
|
||
/* Load the icon by URL using HTTP. */
|
||
function loadByHttpUrl(url) {
|
||
return $q(function(resolve, reject) {
|
||
// Catch HTTP or generic errors not related to incorrect icon IDs.
|
||
var announceAndReject = function(err) {
|
||
var msg = angular.isString(err) ? err : (err.message || err.data || err.statusText);
|
||
$log.warn(msg);
|
||
reject(err);
|
||
},
|
||
extractSvg = function(response) {
|
||
if (!svgCache[url]) {
|
||
svgCache[url] = angular.element('<div>').append(response)[0].querySelector('svg');
|
||
}
|
||
resolve(svgCache[url]);
|
||
};
|
||
|
||
$templateRequest(url, true).then(extractSvg, announceAndReject);
|
||
});
|
||
}
|
||
|
||
return dataUrlRegex.test(url)
|
||
? loadByDataUrl(url)
|
||
: loadByHttpUrl(url);
|
||
}
|
||
|
||
/**
|
||
* Check target signature to see if it is an Icon instance.
|
||
* @param {Icon|Element} target
|
||
* @returns {boolean} true if the specified target is an Icon object, false otherwise.
|
||
*/
|
||
function isIcon(target) {
|
||
return angular.isDefined(target.element) && angular.isDefined(target.config);
|
||
}
|
||
|
||
/**
|
||
* Define the Icon class
|
||
* @param {Element} el
|
||
* @param {=ConfigurationItem} config
|
||
* @constructor
|
||
*/
|
||
function Icon(el, config) {
|
||
// If the node is a <symbol>, it won't be rendered so we have to convert it into <svg>.
|
||
if (el && el.tagName.toLowerCase() === 'symbol') {
|
||
var viewbox = el.getAttribute('viewBox');
|
||
// // Check if innerHTML is supported as IE11 does not support innerHTML on SVG elements.
|
||
if (el.innerHTML) {
|
||
el = angular.element('<svg xmlns="http://www.w3.org/2000/svg">')
|
||
.html(el.innerHTML)[0];
|
||
} else {
|
||
el = angular.element('<svg xmlns="http://www.w3.org/2000/svg">')
|
||
.append($mdUtil.getInnerHTML(el))[0];
|
||
}
|
||
if (viewbox) el.setAttribute('viewBox', viewbox);
|
||
}
|
||
|
||
if (el && el.tagName.toLowerCase() !== 'svg') {
|
||
el = angular.element(
|
||
'<svg xmlns="http://www.w3.org/2000/svg">').append(el.cloneNode(true))[0];
|
||
}
|
||
|
||
// Inject the namespace if not available...
|
||
if (!el.getAttribute('xmlns')) {
|
||
el.setAttribute('xmlns', "http://www.w3.org/2000/svg");
|
||
}
|
||
|
||
this.element = el;
|
||
this.config = config;
|
||
this.prepare();
|
||
}
|
||
|
||
/**
|
||
* Prepare the DOM element that will be cached in the
|
||
* loaded iconCache store.
|
||
*/
|
||
function prepareAndStyle() {
|
||
var viewBoxSize = this.config ? this.config.viewBoxSize : config.defaultViewBoxSize;
|
||
angular.forEach({
|
||
'fit': '',
|
||
'height': '100%',
|
||
'width': '100%',
|
||
'preserveAspectRatio': 'xMidYMid meet',
|
||
'viewBox': this.element.getAttribute('viewBox') || ('0 0 ' + viewBoxSize + ' ' + viewBoxSize),
|
||
'focusable': false // Disable IE11s default behavior to make SVGs focusable
|
||
}, function(val, attr) {
|
||
this.element.setAttribute(attr, val);
|
||
}, this);
|
||
}
|
||
|
||
/**
|
||
* Clone the Icon DOM element.
|
||
*/
|
||
function cloneSVG() {
|
||
// If the element or any of its children have a style attribute, then a CSP policy without
|
||
// 'unsafe-inline' in the style-src directive, will result in a violation.
|
||
return this.element.cloneNode(true);
|
||
}
|
||
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.input
|
||
*/
|
||
mdInputContainerDirective.$inject = ["$mdTheming", "$parse", "$$rAF"];
|
||
inputTextareaDirective.$inject = ["$mdUtil", "$window", "$mdAria", "$timeout", "$mdGesture"];
|
||
mdMaxlengthDirective.$inject = ["$animate", "$mdUtil"];
|
||
placeholderDirective.$inject = ["$compile"];
|
||
ngMessageDirective.$inject = ["$mdUtil"];
|
||
mdSelectOnFocusDirective.$inject = ["$document", "$timeout"];
|
||
mdInputInvalidMessagesAnimation.$inject = ["$$AnimateRunner", "$animateCss", "$mdUtil"];
|
||
ngMessagesAnimation.$inject = ["$$AnimateRunner", "$animateCss", "$mdUtil"];
|
||
ngMessageAnimation.$inject = ["$$AnimateRunner", "$animateCss", "$mdUtil", "$log"];
|
||
var inputModule = 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)
|
||
|
||
.animation('.md-input-invalid', mdInputInvalidMessagesAnimation)
|
||
.animation('.md-input-messages-animation', ngMessagesAnimation)
|
||
.animation('.md-input-message-animation', ngMessageAnimation);
|
||
|
||
// If we are running inside of tests; expose some extra services so that we can test them
|
||
if (window._mdMocksIncluded) {
|
||
inputModule.service('$$mdInput', function() {
|
||
return {
|
||
// special accessor to internals... useful for testing
|
||
messages: {
|
||
getElement : getMessagesElement
|
||
}
|
||
};
|
||
})
|
||
|
||
// Register a service for each animation so that we can easily inject them into unit tests
|
||
.service('mdInputInvalidAnimation', mdInputInvalidMessagesAnimation)
|
||
.service('mdInputMessagesAnimation', ngMessagesAnimation)
|
||
.service('mdInputMessageAnimation', ngMessageAnimation);
|
||
}
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdInputContainer
|
||
* @module material.components.input
|
||
*
|
||
* @restrict E
|
||
*
|
||
* @description
|
||
* `<md-input-container>` is the parent of any input or textarea element. It can also optionally
|
||
* wrap `<md-select>` elements so that they will be formatted for use in a form.
|
||
*
|
||
* Input and textarea elements will not behave properly unless the md-input-container parent is
|
||
* provided.
|
||
*
|
||
* A single `<md-input-container>` should contain only one `<input>` or `<md-select>` element,
|
||
* otherwise it will throw an error.
|
||
*
|
||
* <b>Exception:</b> Hidden inputs (`<input type="hidden" />`) are ignored and will not throw an
|
||
* error, so you may combine these with other inputs.
|
||
*
|
||
* <b>Note:</b> When using `ngMessages` with your input element, make sure the message and container
|
||
* elements are *block* elements, otherwise animations applied to the messages will not look as
|
||
* intended. Either use a `div` and apply the `ng-message` and `ng-messages` classes respectively,
|
||
* or use the `md-block` class on your element.
|
||
*
|
||
* @param {expression=} md-is-error When the given expression evaluates to `true`, the input
|
||
* container will go into the error state. Defaults to erroring if the input has been touched and
|
||
* is invalid.
|
||
* @param {boolean=} md-no-float When present, `placeholder` attributes on the input will not be
|
||
* converted to floating labels.
|
||
*
|
||
* @usage
|
||
* <hljs lang="html">
|
||
* <md-input-container>
|
||
* <label>Username</label>
|
||
* <input type="text" ng-model="user.name">
|
||
* </md-input-container>
|
||
*
|
||
* <md-input-container>
|
||
* <label>Description</label>
|
||
* <textarea ng-model="user.description"></textarea>
|
||
* </md-input-container>
|
||
*
|
||
* <md-input-container>
|
||
* <md-select ng-model="user.state" placeholder="State of Residence">
|
||
* <md-option ng-value="state" ng-repeat="state in states">{{ state }}</md-option>
|
||
* </md-select>
|
||
* </md-input-container>
|
||
* </hljs>
|
||
*
|
||
* <h3>When disabling floating labels</h3>
|
||
* <hljs lang="html">
|
||
* <md-input-container md-no-float>
|
||
* <input type="text" placeholder="Non-Floating Label">
|
||
* </md-input-container>
|
||
* </hljs>
|
||
*
|
||
* <h3>Aligning Form Elements</h3>
|
||
* Wrap your form elements with the `md-inline-form` class in order to align them horizontally
|
||
* within a form.
|
||
*
|
||
* <hljs lang="html">
|
||
* <form class="md-inline-form">
|
||
* <md-input-container>
|
||
* <label>Username</label>
|
||
* <input type="text" ng-model="user.name">
|
||
* </md-input-container>
|
||
*
|
||
* <md-input-container>
|
||
* <label>Description</label>
|
||
* <textarea ng-model="user.description"></textarea>
|
||
* </md-input-container>
|
||
*
|
||
* <md-input-container>
|
||
* <label>State of Residence</label>
|
||
* <md-select ng-model="user.state">
|
||
* <md-option ng-value="state" ng-repeat="state in states">{{ state }}</md-option>
|
||
* </md-select>
|
||
* </md-input-container>
|
||
*
|
||
* <md-input-container>
|
||
* <label>Enter date</label>
|
||
* <md-datepicker ng-model="user.submissionDate"></md-datepicker>
|
||
* </md-input-container>
|
||
*
|
||
* <md-input-container>
|
||
* <md-checkbox ng-model="user.licenseAccepted">
|
||
* I agree to the license terms.
|
||
* </md-checkbox>
|
||
* </md-input-container>
|
||
* </form>
|
||
* </hljs>
|
||
*/
|
||
function mdInputContainerDirective($mdTheming, $parse, $$rAF) {
|
||
|
||
ContainerCtrl.$inject = ["$scope", "$element", "$attrs", "$animate"];
|
||
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(",");
|
||
|
||
var RIGHT_SELECTORS = INPUT_TAGS.reduce(function(selectors, isel) {
|
||
return selectors.concat([isel + ' ~ md-icon', isel + ' ~ .md-icon']);
|
||
}, []).join(",");
|
||
|
||
return {
|
||
restrict: 'E',
|
||
compile: compile,
|
||
controller: ContainerCtrl
|
||
};
|
||
|
||
function compile(tElement) {
|
||
// Check for both a left & right icon
|
||
var hasLeftIcon = tElement[0].querySelector(LEFT_SELECTORS);
|
||
var hasRightIcon = tElement[0].querySelector(RIGHT_SELECTORS);
|
||
|
||
return function postLink(scope, element) {
|
||
$mdTheming(element);
|
||
|
||
if (hasLeftIcon || hasRightIcon) {
|
||
// When accessing the element's contents synchronously, they may not be defined yet because
|
||
// of the use of ng-if. If we wait one frame, then the element should be there if the ng-if
|
||
// resolves to true.
|
||
$$rAF(function() {
|
||
// Handle the case where the md-icon element is initially hidden via ng-if from #9529.
|
||
// We don't want to preserve the space for the icon in the case of ng-if, like we do for
|
||
// ng-show.
|
||
// Note that we can't use the same selectors from above because the elements are no longer
|
||
// siblings for textareas at this point due to the insertion of the md-resize-wrapper.
|
||
var iconNotRemoved = element[0].querySelector('md-icon') ||
|
||
element[0].querySelector('.md-icon');
|
||
if (hasLeftIcon && iconNotRemoved) {
|
||
element.addClass('md-icon-left');
|
||
}
|
||
if (hasRightIcon && iconNotRemoved) {
|
||
element.addClass('md-icon-right');
|
||
}
|
||
});
|
||
}
|
||
};
|
||
}
|
||
|
||
function ContainerCtrl($scope, $element, $attrs, $animate) {
|
||
var self = this;
|
||
|
||
$element.addClass('md-auto-horizontal-margin');
|
||
|
||
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'));
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
function labelDirective() {
|
||
return {
|
||
restrict: 'E',
|
||
require: '^?mdInputContainer',
|
||
link: function(scope, element, attr, containerCtrl) {
|
||
if (!containerCtrl || attr.mdNoFloat || element.hasClass('md-container-ignore')) return;
|
||
|
||
containerCtrl.label = element;
|
||
scope.$on('$destroy', function() {
|
||
containerCtrl.label = null;
|
||
});
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdInput
|
||
* @restrict E
|
||
* @module material.components.input
|
||
*
|
||
* @description
|
||
* You can use any `<input>` or `<textarea>` element as a child of an `<md-input-container>`. This
|
||
* allows you to build complex forms for data entry.
|
||
*
|
||
* When the input is required and uses a floating label, then the label will automatically contain
|
||
* an asterisk (`*`).<br/>
|
||
* This behavior can be disabled by using the `md-no-asterisk` attribute.
|
||
*
|
||
* @param {number=} md-maxlength The maximum number of characters allowed in this input. If this is
|
||
* specified, a character counter will be shown underneath the input.<br/><br/>
|
||
* The purpose of **`md-maxlength`** is exactly to show the max length counter text. If you don't
|
||
* want the counter text and only need "plain" validation, you can use the "simple" `ng-maxlength`
|
||
* or maxlength attributes.<br/><br/>
|
||
* @param {boolean=} ng-trim If set to false, the input text will be not trimmed automatically.
|
||
* Defaults to true.
|
||
* @param {string=} aria-label Aria-label is required when no label is present. A warning message
|
||
* will be logged in the console if not present.
|
||
* @param {string=} placeholder An alternative approach to using aria-label when the label is not
|
||
* PRESENT. The placeholder text is copied to the aria-label attribute.
|
||
* @param {boolean=} md-no-autogrow When present, textareas will not grow automatically.
|
||
* @param {boolean=} md-no-asterisk When present, an asterisk will not be appended to the inputs
|
||
* floating label.
|
||
* @param {boolean=} md-no-resize Disables the textarea resize handle.
|
||
* @param {number=} max-rows The maximum amount of rows for a textarea.
|
||
* @param {boolean=} md-detect-hidden When present, textareas will be sized properly when they are
|
||
* revealed after being hidden. This is off by default for performance reasons because it
|
||
* guarantees a reflow every digest cycle.
|
||
*
|
||
* @usage
|
||
* <hljs lang="html">
|
||
* <md-input-container>
|
||
* <label>Color</label>
|
||
* <input type="text" ng-model="color" required md-maxlength="10">
|
||
* </md-input-container>
|
||
* </hljs>
|
||
*
|
||
* <h3>With Errors</h3>
|
||
*
|
||
* `md-input-container` also supports errors using the standard `ng-messages` directives and
|
||
* animates the messages when they become visible using from the `ngEnter`/`ngLeave` events or
|
||
* the `ngShow`/`ngHide` events.
|
||
*
|
||
* By default, the messages will be hidden until the input is in an error state. This is based off
|
||
* of the `md-is-error` expression of the `md-input-container`. This gives the user a chance to
|
||
* fill out the form before the errors become visible.
|
||
*
|
||
* <hljs lang="html">
|
||
* <form name="colorForm">
|
||
* <md-input-container>
|
||
* <label>Favorite Color</label>
|
||
* <input name="favoriteColor" ng-model="favoriteColor" required>
|
||
* <div ng-messages="colorForm.favoriteColor.$error">
|
||
* <div ng-message="required">This is required!</div>
|
||
* </div>
|
||
* </md-input-container>
|
||
* </form>
|
||
* </hljs>
|
||
*
|
||
* We automatically disable this auto-hiding functionality if you provide any of the following
|
||
* visibility directives on the `ng-messages` container:
|
||
*
|
||
* - `ng-if`
|
||
* - `ng-show`/`ng-hide`
|
||
* - `ng-switch-when`/`ng-switch-default`
|
||
*
|
||
* You can also disable this functionality manually by adding the `md-auto-hide="false"` expression
|
||
* to the `ng-messages` container. This may be helpful if you always want to see the error messages
|
||
* or if you are building your own visibility directive.
|
||
*
|
||
* _<b>Note:</b> The `md-auto-hide` attribute is a static string that is only checked upon
|
||
* initialization of the `ng-messages` directive to see if it equals the string `false`._
|
||
*
|
||
* <hljs lang="html">
|
||
* <form name="userForm">
|
||
* <md-input-container>
|
||
* <label>Last Name</label>
|
||
* <input name="lastName" ng-model="lastName" required md-maxlength="10" minlength="4">
|
||
* <div ng-messages="userForm.lastName.$error" ng-show="userForm.lastName.$dirty">
|
||
* <div ng-message="required">This is required!</div>
|
||
* <div ng-message="md-maxlength">That's too long!</div>
|
||
* <div ng-message="minlength">That's too short!</div>
|
||
* </div>
|
||
* </md-input-container>
|
||
* <md-input-container>
|
||
* <label>Biography</label>
|
||
* <textarea name="bio" ng-model="biography" required md-maxlength="150"></textarea>
|
||
* <div ng-messages="userForm.bio.$error" ng-show="userForm.bio.$dirty">
|
||
* <div ng-message="required">This is required!</div>
|
||
* <div ng-message="md-maxlength">That's too long!</div>
|
||
* </div>
|
||
* </md-input-container>
|
||
* <md-input-container>
|
||
* <input aria-label='title' ng-model='title'>
|
||
* </md-input-container>
|
||
* <md-input-container>
|
||
* <input placeholder='title' ng-model='title'>
|
||
* </md-input-container>
|
||
* </form>
|
||
* </hljs>
|
||
*
|
||
* <h3>Notes</h3>
|
||
*
|
||
* - 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
|
||
* `<md-input-container>` tags. Instead, use relative or absolute positioning.
|
||
*
|
||
*
|
||
* <h3>Textarea directive</h3>
|
||
* 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 want 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.
|
||
*/
|
||
|
||
function inputTextareaDirective($mdUtil, $window, $mdAria, $timeout, $mdGesture) {
|
||
return {
|
||
restrict: 'E',
|
||
require: ['^?mdInputContainer', '?ngModel', '?^form'],
|
||
link: postLink
|
||
};
|
||
|
||
function postLink(scope, element, attr, ctrls) {
|
||
|
||
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();
|
||
|
||
|
||
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("<md-input-container> can only have *one* <input>, <textarea> or <md-select> child element!");
|
||
}
|
||
}
|
||
containerCtrl.input = element;
|
||
|
||
setupAttributeWatchers();
|
||
|
||
// Add an error spacer div after our input to provide space for the char counter and any ng-messages
|
||
var errorsSpacer = angular.element('<div class="md-errors-spacer">');
|
||
element.after(errorsSpacer);
|
||
|
||
var placeholderText = angular.isString(attr.placeholder) ? attr.placeholder.trim() : '';
|
||
if (!containerCtrl.label && !placeholderText.length) {
|
||
$mdAria.expect(element, 'aria-label');
|
||
}
|
||
|
||
element.addClass('md-input');
|
||
if (!element.attr('id')) {
|
||
element.attr('id', 'input_' + $mdUtil.nextUid());
|
||
}
|
||
|
||
// This works around a Webkit issue where number inputs, placed in a flexbox, that have
|
||
// a `min` and `max` will collapse to about 1/3 of their proper width. Please check #7349
|
||
// for more info. Also note that we don't override the `step` if the user has specified it,
|
||
// in order to prevent some unexpected behaviour.
|
||
if (tagName === 'input' && attr.type === 'number' && attr.min && attr.max && !attr.step) {
|
||
element.attr('step', 'any');
|
||
} else if (tagName === 'textarea') {
|
||
setupTextarea();
|
||
}
|
||
|
||
// If the input doesn't have an ngModel, it may have a static value. For that case,
|
||
// we have to do one initial check to determine if the container should be in the
|
||
// "has a value" state.
|
||
if (!hasNgModel) {
|
||
inputCheckValue();
|
||
}
|
||
|
||
var isErrorGetter = containerCtrl.isErrorGetter || function() {
|
||
return ngModelCtrl.$invalid && (ngModelCtrl.$touched || (parentForm && parentForm.$submitted));
|
||
};
|
||
|
||
scope.$watch(isErrorGetter, containerCtrl.setInvalid);
|
||
|
||
// When the developer uses the ngValue directive for the input, we have to observe the attribute, because
|
||
// AngularJS's ngValue directive is just setting the `value` attribute.
|
||
if (attr.ngValue) {
|
||
attr.$observe('value', inputCheckValue);
|
||
}
|
||
|
||
ngModelCtrl.$parsers.push(ngModelPipelineCheckValue);
|
||
ngModelCtrl.$formatters.push(ngModelPipelineCheckValue);
|
||
|
||
element.on('input', inputCheckValue);
|
||
|
||
if (!isReadonly) {
|
||
element
|
||
.on('focus', function(ev) {
|
||
$mdUtil.nextTick(function() {
|
||
containerCtrl.setFocused(true);
|
||
});
|
||
})
|
||
.on('blur', function(ev) {
|
||
$mdUtil.nextTick(function() {
|
||
containerCtrl.setFocused(false);
|
||
inputCheckValue();
|
||
});
|
||
});
|
||
}
|
||
|
||
scope.$on('$destroy', function() {
|
||
containerCtrl.setFocused(false);
|
||
containerCtrl.setHasValue(false);
|
||
containerCtrl.input = null;
|
||
});
|
||
|
||
/** Gets run through ngModel's pipeline and set the `has-value` class on the container. */
|
||
function ngModelPipelineCheckValue(arg) {
|
||
containerCtrl.setHasValue(!ngModelCtrl.$isEmpty(arg));
|
||
return arg;
|
||
}
|
||
|
||
function setupAttributeWatchers() {
|
||
if (containerCtrl.label) {
|
||
attr.$observe('required', function (value) {
|
||
// We don't need to parse the required value, it's always a boolean because of AngularJS'
|
||
// required directive.
|
||
if (containerCtrl.label) {
|
||
containerCtrl.label.toggleClass('md-required', value && !mdNoAsterisk);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
function inputCheckValue() {
|
||
// An input's value counts if its length > 0,
|
||
// or if the input's validity state says it has bad input (eg string in a number input)
|
||
containerCtrl.setHasValue(element.val().length > 0 || (element[0].validity || {}).badInput);
|
||
}
|
||
|
||
function setupTextarea() {
|
||
var isAutogrowing = !attr.hasOwnProperty('mdNoAutogrow');
|
||
|
||
attachResizeHandle();
|
||
|
||
if (!isAutogrowing) return;
|
||
|
||
// Can't check if height was or not explicity set,
|
||
// so rows attribute will take precedence if present
|
||
var minRows = attr.hasOwnProperty('rows') ? parseInt(attr.rows) : NaN;
|
||
var maxRows = attr.hasOwnProperty('maxRows') ? parseInt(attr.maxRows) : NaN;
|
||
var scopeResizeListener = scope.$on('md-resize-textarea', growTextarea);
|
||
var lineHeight = null;
|
||
var node = element[0];
|
||
|
||
// This timeout is necessary, because the browser needs a little bit
|
||
// of time to calculate the `clientHeight` and `scrollHeight`.
|
||
$timeout(function() {
|
||
$mdUtil.nextTick(growTextarea);
|
||
}, 10, false);
|
||
|
||
// We could leverage ngModel's $parsers here, however it
|
||
// isn't reliable, because AngularJS trims the input by default,
|
||
// which means that growTextarea won't fire when newlines and
|
||
// spaces are added.
|
||
element.on('input', growTextarea);
|
||
|
||
// We should still use the $formatters, because they fire when
|
||
// the value was changed from outside the textarea.
|
||
if (hasNgModel) {
|
||
ngModelCtrl.$formatters.push(formattersListener);
|
||
}
|
||
|
||
if (!minRows) {
|
||
element.attr('rows', 1);
|
||
}
|
||
|
||
angular.element($window).on('resize', growTextarea);
|
||
scope.$on('$destroy', disableAutogrow);
|
||
|
||
function growTextarea() {
|
||
// temporarily disables element's flex so its height 'runs free'
|
||
element
|
||
.attr('rows', 1)
|
||
.css('height', 'auto')
|
||
.addClass('md-no-flex');
|
||
|
||
var height = getHeight();
|
||
|
||
if (!lineHeight) {
|
||
// offsetHeight includes padding which can throw off our value
|
||
var originalPadding = element[0].style.padding || '';
|
||
lineHeight = element.css('padding', 0).prop('offsetHeight');
|
||
element[0].style.padding = originalPadding;
|
||
}
|
||
|
||
if (minRows && lineHeight) {
|
||
height = Math.max(height, lineHeight * minRows);
|
||
}
|
||
|
||
if (maxRows && lineHeight) {
|
||
var maxHeight = lineHeight * maxRows;
|
||
|
||
if (maxHeight < height) {
|
||
element.attr('md-no-autogrow', '');
|
||
height = maxHeight;
|
||
} else {
|
||
element.removeAttr('md-no-autogrow');
|
||
}
|
||
}
|
||
|
||
if (lineHeight) {
|
||
element.attr('rows', Math.round(height / lineHeight));
|
||
}
|
||
|
||
element
|
||
.css('height', height + 'px')
|
||
.removeClass('md-no-flex');
|
||
}
|
||
|
||
function getHeight() {
|
||
var offsetHeight = node.offsetHeight;
|
||
var line = node.scrollHeight - offsetHeight;
|
||
return offsetHeight + Math.max(line, 0);
|
||
}
|
||
|
||
function formattersListener(value) {
|
||
$mdUtil.nextTick(growTextarea);
|
||
return value;
|
||
}
|
||
|
||
function disableAutogrow() {
|
||
if (!isAutogrowing) return;
|
||
|
||
isAutogrowing = false;
|
||
angular.element($window).off('resize', growTextarea);
|
||
scopeResizeListener && scopeResizeListener();
|
||
element
|
||
.attr('md-no-autogrow', '')
|
||
.off('input', growTextarea);
|
||
|
||
if (hasNgModel) {
|
||
var listenerIndex = ngModelCtrl.$formatters.indexOf(formattersListener);
|
||
|
||
if (listenerIndex > -1) {
|
||
ngModelCtrl.$formatters.splice(listenerIndex, 1);
|
||
}
|
||
}
|
||
}
|
||
|
||
function attachResizeHandle() {
|
||
if (attr.hasOwnProperty('mdNoResize')) return;
|
||
|
||
var handle = angular.element('<div class="md-resize-handle"></div>');
|
||
var isDragging = false;
|
||
var dragStart = null;
|
||
var startHeight = 0;
|
||
var container = containerCtrl.element;
|
||
var dragGestureHandler = $mdGesture.register(handle, 'drag', { horizontal: false });
|
||
|
||
|
||
element.wrap('<div class="md-resize-wrapper">').after(handle);
|
||
handle.on('mousedown', onMouseDown);
|
||
|
||
container
|
||
.on('$md.dragstart', onDragStart)
|
||
.on('$md.drag', onDrag)
|
||
.on('$md.dragend', onDragEnd);
|
||
|
||
scope.$on('$destroy', function() {
|
||
handle
|
||
.off('mousedown', onMouseDown)
|
||
.remove();
|
||
|
||
container
|
||
.off('$md.dragstart', onDragStart)
|
||
.off('$md.drag', onDrag)
|
||
.off('$md.dragend', onDragEnd);
|
||
|
||
dragGestureHandler();
|
||
handle = null;
|
||
container = null;
|
||
dragGestureHandler = null;
|
||
});
|
||
|
||
function onMouseDown(ev) {
|
||
ev.preventDefault();
|
||
isDragging = true;
|
||
dragStart = ev.clientY;
|
||
startHeight = parseFloat(element.css('height')) || element.prop('offsetHeight');
|
||
}
|
||
|
||
function onDragStart(ev) {
|
||
if (!isDragging) return;
|
||
ev.preventDefault();
|
||
disableAutogrow();
|
||
container.addClass('md-input-resized');
|
||
}
|
||
|
||
function onDrag(ev) {
|
||
if (!isDragging) return;
|
||
|
||
element.css('height', (startHeight + ev.pointer.distanceY) + 'px');
|
||
}
|
||
|
||
function onDragEnd(ev) {
|
||
if (!isDragging) return;
|
||
isDragging = false;
|
||
container.removeClass('md-input-resized');
|
||
}
|
||
}
|
||
|
||
// Attach a watcher to detect when the textarea gets shown.
|
||
if (attr.hasOwnProperty('mdDetectHidden')) {
|
||
|
||
var handleHiddenChange = function() {
|
||
var wasHidden = false;
|
||
|
||
return function() {
|
||
var isHidden = node.offsetHeight === 0;
|
||
|
||
if (isHidden === false && wasHidden === true) {
|
||
growTextarea();
|
||
}
|
||
|
||
wasHidden = isHidden;
|
||
};
|
||
}();
|
||
|
||
// Check every digest cycle whether the visibility of the textarea has changed.
|
||
// Queue up to run after the digest cycle is complete.
|
||
scope.$watch(function() {
|
||
$mdUtil.nextTick(handleHiddenChange, false);
|
||
return true;
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function mdMaxlengthDirective($animate, $mdUtil) {
|
||
return {
|
||
restrict: 'A',
|
||
require: ['ngModel', '^mdInputContainer'],
|
||
link: postLink
|
||
};
|
||
|
||
function postLink(scope, element, attr, ctrls) {
|
||
var maxlength = parseInt(attr.mdMaxlength);
|
||
if (isNaN(maxlength)) maxlength = -1;
|
||
var ngModelCtrl = ctrls[0];
|
||
var containerCtrl = ctrls[1];
|
||
var charCountEl, errorsSpacer;
|
||
var ngTrim = angular.isDefined(attr.ngTrim) ? $mdUtil.parseAttributeBoolean(attr.ngTrim) : true;
|
||
var isPasswordInput = attr.type === 'password';
|
||
|
||
scope.$watch(attr.mdMaxlength, function(value) {
|
||
maxlength = value;
|
||
});
|
||
|
||
ngModelCtrl.$validators['md-maxlength'] = function(modelValue, viewValue) {
|
||
if (!angular.isNumber(maxlength) || maxlength < 0) {
|
||
return true;
|
||
}
|
||
|
||
// We always update the char count, when the modelValue has changed.
|
||
// Using the $validators for triggering the update works very well.
|
||
renderCharCount();
|
||
|
||
var elementVal = element.val() || viewValue;
|
||
if (elementVal === undefined || elementVal === null) {
|
||
elementVal = '';
|
||
}
|
||
elementVal = ngTrim && !isPasswordInput && angular.isString(elementVal) ? elementVal.trim() : elementVal;
|
||
// Force the value into a string since it may be a number,
|
||
// which does not have a length property.
|
||
return String(elementVal).length <= maxlength;
|
||
};
|
||
|
||
/**
|
||
* Override the default NgModelController $isEmpty check to take ng-trim, password inputs,
|
||
* etc. into account.
|
||
* @param value {*} the input's value
|
||
* @returns {boolean} true if the input's value should be considered empty, false otherwise
|
||
*/
|
||
ngModelCtrl.$isEmpty = function(value) {
|
||
return calculateInputValueLength(value) === 0;
|
||
};
|
||
|
||
// Wait until the next tick to ensure that the input has setup the errors spacer where we will
|
||
// append our counter
|
||
$mdUtil.nextTick(function() {
|
||
errorsSpacer = angular.element(containerCtrl.element[0].querySelector('.md-errors-spacer'));
|
||
charCountEl = angular.element('<div class="md-char-counter">');
|
||
|
||
// Append our character counter inside the errors spacer
|
||
errorsSpacer.append(charCountEl);
|
||
|
||
attr.$observe('ngTrim', function (value) {
|
||
ngTrim = angular.isDefined(value) ? $mdUtil.parseAttributeBoolean(value) : true;
|
||
});
|
||
|
||
scope.$watch(attr.mdMaxlength, function(value) {
|
||
if (angular.isNumber(value) && value > 0) {
|
||
if (!charCountEl.parent().length) {
|
||
$animate.enter(charCountEl, errorsSpacer);
|
||
}
|
||
renderCharCount();
|
||
} else {
|
||
$animate.leave(charCountEl);
|
||
}
|
||
});
|
||
});
|
||
|
||
/**
|
||
* Calculate the input value's length after coercing it to a string
|
||
* and trimming it if appropriate.
|
||
* @param value {*} the input's value
|
||
* @returns {number} calculated length of the input's value
|
||
*/
|
||
function calculateInputValueLength(value) {
|
||
value = ngTrim && !isPasswordInput && angular.isString(value) ? value.trim() : value;
|
||
if (value === undefined || value === null) {
|
||
value = '';
|
||
}
|
||
return String(value).length;
|
||
}
|
||
|
||
function renderCharCount() {
|
||
// If we have not been initialized or appended to the body yet; do not render.
|
||
if (!charCountEl || !charCountEl.parent()) {
|
||
return;
|
||
}
|
||
// Force the value into a string since it may be a number,
|
||
// which does not have a length property.
|
||
charCountEl.text(calculateInputValueLength(element.val()) + ' / ' + maxlength);
|
||
}
|
||
}
|
||
}
|
||
|
||
function placeholderDirective($compile) {
|
||
return {
|
||
restrict: 'A',
|
||
require: '^^?mdInputContainer',
|
||
priority: 200,
|
||
link: {
|
||
// Note that we need to do this in the pre-link, as opposed to the post link, if we want to
|
||
// support data bindings in the placeholder. This is necessary, because we have a case where
|
||
// we transfer the placeholder value to the `<label>` and we remove it from the original `<input>`.
|
||
// If we did this in the post-link, AngularJS would have set up the observers already and would be
|
||
// re-adding the attribute, even though we removed it from the element.
|
||
pre: preLink
|
||
}
|
||
};
|
||
|
||
function preLink(scope, element, attr, inputContainer) {
|
||
// If there is no input container, just return
|
||
if (!inputContainer) return;
|
||
|
||
var label = inputContainer.element.find('label');
|
||
var noFloat = inputContainer.element.attr('md-no-float');
|
||
|
||
// If we have a label, or they specify the md-no-float attribute, just return
|
||
if ((label && label.length) || noFloat === '' || scope.$eval(noFloat)) {
|
||
// Add a placeholder class so we can target it in the CSS
|
||
inputContainer.setHasPlaceholder(true);
|
||
return;
|
||
}
|
||
|
||
// md-select handles placeholders on it's own
|
||
if (element[0].nodeName !== 'MD-SELECT') {
|
||
// Move the placeholder expression to the label
|
||
var newLabel = angular.element(
|
||
'<label ng-click="delegateClick()" tabindex="-1" aria-hidden="true">' + attr.placeholder +
|
||
'</label>');
|
||
|
||
// Note that we unset it via `attr`, in order to get AngularJS
|
||
// to remove any observers that it might have set up. Otherwise
|
||
// the attribute will be added on the next digest.
|
||
attr.$set('placeholder', null);
|
||
|
||
// We need to compile the label manually in case it has any bindings.
|
||
// A gotcha here is that we first add the element to the DOM and we compile
|
||
// it later. This is necessary, because if we compile the element beforehand,
|
||
// it won't be able to find the `mdInputContainer` controller.
|
||
inputContainer.element
|
||
.addClass('md-icon-float')
|
||
.prepend(newLabel);
|
||
|
||
$compile(newLabel)(scope);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdSelectOnFocus
|
||
* @module material.components.input
|
||
*
|
||
* @restrict A
|
||
*
|
||
* @description
|
||
* The `md-select-on-focus` directive allows you to automatically select the element's input text on focus.
|
||
*
|
||
* <h3>Notes</h3>
|
||
* - The use of `md-select-on-focus` is restricted to `<input>` and `<textarea>` elements.
|
||
*
|
||
* @usage
|
||
* <h3>Using with an Input</h3>
|
||
* <hljs lang="html">
|
||
*
|
||
* <md-input-container>
|
||
* <label>Auto Select</label>
|
||
* <input type="text" md-select-on-focus>
|
||
* </md-input-container>
|
||
* </hljs>
|
||
*
|
||
* <h3>Using with a Textarea</h3>
|
||
* <hljs lang="html">
|
||
*
|
||
* <md-input-container>
|
||
* <label>Auto Select</label>
|
||
* <textarea md-select-on-focus>This text will be selected on focus.</textarea>
|
||
* </md-input-container>
|
||
*
|
||
* </hljs>
|
||
*/
|
||
function mdSelectOnFocusDirective($document, $timeout) {
|
||
|
||
return {
|
||
restrict: 'A',
|
||
link: postLink
|
||
};
|
||
|
||
function postLink(scope, element, attr) {
|
||
if (element[0].nodeName !== 'INPUT' && element[0].nodeName !== "TEXTAREA") return;
|
||
|
||
var preventMouseUp = false;
|
||
|
||
element
|
||
.on('focus', onFocus)
|
||
.on('mouseup', onMouseUp);
|
||
|
||
scope.$on('$destroy', function() {
|
||
element
|
||
.off('focus', onFocus)
|
||
.off('mouseup', onMouseUp);
|
||
});
|
||
|
||
function onFocus() {
|
||
preventMouseUp = true;
|
||
|
||
$timeout(function() {
|
||
|
||
// Use HTMLInputElement#select to fix firefox select issues.
|
||
// The debounce is here for Edge's sake, otherwise the selection doesn't work.
|
||
// Since focus may already have been lost on the input (and because `select()`
|
||
// will re-focus), make sure the element is still active before applying.
|
||
if ($document[0].activeElement === element[0]) {
|
||
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();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
var visibilityDirectives = ['ngIf', 'ngShow', 'ngHide', 'ngSwitchWhen', 'ngSwitchDefault'];
|
||
function ngMessagesDirective() {
|
||
return {
|
||
restrict: 'EA',
|
||
link: postLink,
|
||
|
||
// 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;
|
||
|
||
// 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);
|
||
|
||
// 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];
|
||
});
|
||
}
|
||
}
|
||
|
||
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 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");
|
||
}
|
||
|
||
function initMessageElement(element) {
|
||
// Add our animation class
|
||
element.toggleClass('md-input-message-animation', true);
|
||
}
|
||
}
|
||
}
|
||
|
||
var $$AnimateRunner, $animateCss, $mdUtil;
|
||
|
||
function mdInputInvalidMessagesAnimation($$AnimateRunner, $animateCss, $mdUtil) {
|
||
saveSharedServices($$AnimateRunner, $animateCss, $mdUtil);
|
||
|
||
return {
|
||
addClass: function(element, className, done) {
|
||
showInputMessages(element, done);
|
||
}
|
||
|
||
// NOTE: We do not need the removeClass method, because the message ng-leave animation will fire
|
||
};
|
||
}
|
||
|
||
function ngMessagesAnimation($$AnimateRunner, $animateCss, $mdUtil) {
|
||
saveSharedServices($$AnimateRunner, $animateCss, $mdUtil);
|
||
|
||
return {
|
||
enter: function(element, done) {
|
||
showInputMessages(element, done);
|
||
},
|
||
|
||
leave: function(element, done) {
|
||
hideInputMessages(element, done);
|
||
},
|
||
|
||
addClass: function(element, className, done) {
|
||
if (className == "ng-hide") {
|
||
hideInputMessages(element, done);
|
||
} else {
|
||
done();
|
||
}
|
||
},
|
||
|
||
removeClass: function(element, className, done) {
|
||
if (className == "ng-hide") {
|
||
showInputMessages(element, done);
|
||
} else {
|
||
done();
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
function ngMessageAnimation($$AnimateRunner, $animateCss, $mdUtil, $log) {
|
||
saveSharedServices($$AnimateRunner, $animateCss, $mdUtil, $log);
|
||
|
||
return {
|
||
enter: function(element, done) {
|
||
var animator = showMessage(element);
|
||
|
||
animator.start().done(done);
|
||
},
|
||
|
||
leave: function(element, done) {
|
||
var animator = hideMessage(element);
|
||
|
||
animator.start().done(done);
|
||
}
|
||
};
|
||
}
|
||
|
||
function showInputMessages(element, done) {
|
||
var animators = [], animator;
|
||
var messages = getMessagesElement(element);
|
||
var children = messages.children();
|
||
|
||
if (messages.length == 0 || children.length == 0) {
|
||
done();
|
||
return;
|
||
}
|
||
|
||
angular.forEach(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);
|
||
var children = messages.children();
|
||
|
||
if (messages.length == 0 || children.length == 0) {
|
||
done();
|
||
return;
|
||
}
|
||
|
||
angular.forEach(children, function(child) {
|
||
animator = hideMessage(angular.element(child));
|
||
|
||
animators.push(animator.start());
|
||
});
|
||
|
||
$$AnimateRunner.all(animators, done);
|
||
}
|
||
|
||
function showMessage(element) {
|
||
var height = parseInt(window.getComputedStyle(element[0]).height);
|
||
var topMargin = parseInt(window.getComputedStyle(element[0]).marginTop);
|
||
|
||
var messages = getMessagesElement(element);
|
||
var container = getInputElement(element);
|
||
|
||
// Check to see if the message is already visible so we can skip
|
||
var alreadyVisible = (topMargin > -height);
|
||
|
||
// 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, {});
|
||
}
|
||
|
||
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) {
|
||
var height = element[0].offsetHeight;
|
||
var styles = window.getComputedStyle(element[0]);
|
||
|
||
// If we are already hidden, just return an empty animation
|
||
if (parseInt(styles.opacity) === 0) {
|
||
return $animateCss(element, {});
|
||
}
|
||
|
||
// 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) {
|
||
// If we ARE the messages element, just return ourself
|
||
if (element.hasClass('md-input-messages-animation')) {
|
||
return 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');
|
||
}));
|
||
}
|
||
|
||
// Otherwise, we can traverse down
|
||
return angular.element(element[0].querySelector('.md-input-messages-animation'));
|
||
}
|
||
|
||
function saveSharedServices(_$$AnimateRunner_, _$animateCss_, _$mdUtil_) {
|
||
$$AnimateRunner = _$$AnimateRunner_;
|
||
$animateCss = _$animateCss_;
|
||
$mdUtil = _$mdUtil_;
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.list
|
||
* @description
|
||
* List module
|
||
*/
|
||
MdListController.$inject = ["$scope", "$element", "$mdListInkRipple"];
|
||
mdListDirective.$inject = ["$mdTheming"];
|
||
mdListItemDirective.$inject = ["$mdAria", "$mdConstant", "$mdUtil", "$timeout"];
|
||
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 `<md-list>` directive is a list container for 1..n `<md-list-item>` tags.
|
||
*
|
||
* @usage
|
||
* <hljs lang="html">
|
||
* <md-list>
|
||
* <md-list-item class="md-2-line" ng-repeat="item in todos">
|
||
* <md-checkbox ng-model="item.done"></md-checkbox>
|
||
* <div class="md-list-item-text">
|
||
* <h3>{{item.title}}</h3>
|
||
* <p>{{item.description}}</p>
|
||
* </div>
|
||
* </md-list-item>
|
||
* </md-list>
|
||
* </hljs>
|
||
*/
|
||
|
||
function mdListDirective($mdTheming) {
|
||
return {
|
||
restrict: 'E',
|
||
compile: function(tEl) {
|
||
tEl[0].setAttribute('role', 'list');
|
||
return $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.<br/>
|
||
*
|
||
* @usage
|
||
* ### Single Row Item
|
||
* <hljs lang="html">
|
||
* <md-list-item>
|
||
* <span>Single Row Item</span>
|
||
* </md-list-item>
|
||
* </hljs>
|
||
*
|
||
* ### Multiple Lines
|
||
* By using the following markup, you will be able to have two lines inside of one `md-list-item`.
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-list-item class="md-2-line">
|
||
* <div class="md-list-item-text" layout="column">
|
||
* <p>First Line</p>
|
||
* <p>Second Line</p>
|
||
* </div>
|
||
* </md-list-item>
|
||
* </hljs>
|
||
*
|
||
* It is also possible to have three lines inside of one list item.
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-list-item class="md-3-line">
|
||
* <div class="md-list-item-text" layout="column">
|
||
* <p>First Line</p>
|
||
* <p>Second Line</p>
|
||
* <p>Third Line</p>
|
||
* </div>
|
||
* </md-list-item>
|
||
* </hljs>
|
||
*
|
||
* ### Secondary Items
|
||
* Secondary items are elements which will be aligned at the end of the `md-list-item`.
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-list-item>
|
||
* <span>Single Row Item</span>
|
||
* <md-button class="md-secondary">
|
||
* Secondary Button
|
||
* </md-button>
|
||
* </md-list-item>
|
||
* </hljs>
|
||
*
|
||
* It also possible to have multiple secondary items inside of one `md-list-item`.
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-list-item>
|
||
* <span>Single Row Item</span>
|
||
* <md-button class="md-secondary">First Button</md-button>
|
||
* <md-button class="md-secondary">Second Button</md-button>
|
||
* </md-list-item>
|
||
* </hljs>
|
||
*
|
||
* ### Proxy Item
|
||
* Proxies are elements, which will execute their specific action on click<br/>
|
||
* 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
|
||
* automatically become clickable and executes the associated action of the proxy element on click.
|
||
*
|
||
* It is possible to disable this behavior by applying the `md-no-proxy` class to the list item.
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-list-item class="md-no-proxy">
|
||
* <span>No Proxy List</span>
|
||
* <md-checkbox class="md-secondary"></md-checkbox>
|
||
* </md-list-item>
|
||
* </hljs>
|
||
*
|
||
* Here are a few examples of proxy elements inside of a list item.
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-list-item>
|
||
* <span>First Line</span>
|
||
* <md-checkbox class="md-secondary"></md-checkbox>
|
||
* </md-list-item>
|
||
* </hljs>
|
||
*
|
||
* The `md-checkbox` element will be automatically detected as a proxy element and will toggle on
|
||
* click.
|
||
*
|
||
* If not provided, an `aria-label` will be applied using the text of the list item.
|
||
* In this case, the following will be applied to the `md-checkbox`:
|
||
* `aria-label="Toggle First Line"`.
|
||
* When localizing your application, you should supply a localized `aria-label`.
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-list-item>
|
||
* <span>First Line</span>
|
||
* <md-switch class="md-secondary"></md-switch>
|
||
* </md-list-item>
|
||
* </hljs>
|
||
*
|
||
* 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`.
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-list-item>
|
||
* <p>Click anywhere to fire the secondary action</p>
|
||
* <md-menu class="md-secondary">
|
||
* <md-button class="md-icon-button">
|
||
* <md-icon md-svg-icon="communication:message"></md-icon>
|
||
* </md-button>
|
||
* <md-menu-content width="4">
|
||
* <md-menu-item>
|
||
* <md-button>
|
||
* Redial
|
||
* </md-button>
|
||
* </md-menu-item>
|
||
* <md-menu-item>
|
||
* <md-button>
|
||
* Check voicemail
|
||
* </md-button>
|
||
* </md-menu-item>
|
||
* <md-menu-divider></md-menu-divider>
|
||
* <md-menu-item>
|
||
* <md-button>
|
||
* Notifications
|
||
* </md-button>
|
||
* </md-menu-item>
|
||
* </md-menu-content>
|
||
* </md-menu>
|
||
* </md-list-item>
|
||
* </hljs>
|
||
*
|
||
* The menu will automatically open, when the users clicks on the `md-list-item`.<br/>
|
||
*
|
||
* If the developer didn't specify any position mode on the menu, the `md-list-item` will
|
||
* automatically detect the position mode and apply it to the `md-menu`.
|
||
*
|
||
* ### Avatars
|
||
* Sometimes you may want to have avatars inside of the `md-list-item `.<br/>
|
||
* You are able to create an optimized icon for the list item, by applying the `.md-avatar` class on
|
||
* the `<img>` element.
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-list-item>
|
||
* <img src="my-avatar.png" class="md-avatar">
|
||
* <span>Alan Turing</span>
|
||
* </hljs>
|
||
*
|
||
* When using `<md-icon>` for an avatar, you have to use the `.md-avatar-icon` class.
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-list-item>
|
||
* <md-icon class="md-avatar-icon" md-svg-icon="social:person"></md-icon>
|
||
* <span>Timothy Kopra</span>
|
||
* </md-list-item>
|
||
* </hljs>
|
||
*
|
||
* In cases where you have a `md-list-item`, which doesn't have an avatar,
|
||
* but you want to align it with the other avatar items, you need to use the `.md-offset` class.
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-list-item class="md-offset">
|
||
* <span>Jon Doe</span>
|
||
* </md-list-item>
|
||
* </hljs>
|
||
*
|
||
* ### 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 `<div>` 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 that would cause issues with the overflow
|
||
* of the list item.
|
||
*/
|
||
function mdListItemDirective($mdAria, $mdConstant, $mdUtil, $timeout) {
|
||
var proxiedTypes = ['md-checkbox', 'md-switch', 'md-menu'];
|
||
return {
|
||
restrict: 'E',
|
||
controller: 'MdListController',
|
||
|
||
compile: function(tElement, tAttrs) {
|
||
|
||
// Check for proxy controls (no ng-click on parent, and a control inside)
|
||
var secondaryItems = tElement[0].querySelectorAll('.md-secondary');
|
||
var hasProxiedElement;
|
||
var proxyElement;
|
||
var itemContainer = tElement;
|
||
|
||
tElement[0].setAttribute('role', 'listitem');
|
||
|
||
if (tAttrs.ngClick || tAttrs.ngDblclick || tAttrs.ngHref || tAttrs.href || tAttrs.uiSref || tAttrs.ngAttrUiSref) {
|
||
wrapIn('button');
|
||
} else if (!tElement.hasClass('md-no-proxy')) {
|
||
|
||
for (var i = 0, type; i < proxiedTypes.length; ++i) {
|
||
proxyElement = tElement[0].querySelector(proxiedTypes[i]);
|
||
if (proxyElement !== null) {
|
||
hasProxiedElement = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (hasProxiedElement) {
|
||
wrapIn('div');
|
||
} else {
|
||
tElement.addClass('md-no-proxy');
|
||
}
|
||
}
|
||
|
||
wrapSecondaryItems();
|
||
setupToggleAria();
|
||
|
||
if (hasProxiedElement && proxyElement.nodeName === "MD-MENU") {
|
||
setupProxiedMenu();
|
||
}
|
||
|
||
function setupToggleAria() {
|
||
var toggleTypes = ['md-switch', 'md-checkbox'];
|
||
var toggle;
|
||
|
||
for (var i = 0, toggleType; i < toggleTypes.length; ++i) {
|
||
toggle = tElement.find(toggleTypes[i])[0];
|
||
if (toggle) {
|
||
if (!toggle.hasAttribute('aria-label')) {
|
||
var labelElement = tElement.find('p')[0];
|
||
if (!labelElement) {
|
||
labelElement = tElement.find('span')[0];
|
||
}
|
||
if (!labelElement) return;
|
||
toggle.setAttribute('aria-label', 'Toggle ' + labelElement.textContent);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function setupProxiedMenu() {
|
||
var menuEl = angular.element(proxyElement);
|
||
|
||
var isEndAligned = menuEl.parent().hasClass('md-secondary-container') ||
|
||
proxyElement.parentNode.firstElementChild !== proxyElement;
|
||
|
||
var xAxisPosition = 'left';
|
||
|
||
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');
|
||
}
|
||
|
||
// Apply menu open binding to menu button
|
||
var menuOpenButton = menuEl.children().eq(0);
|
||
if (!hasClickEvent(menuOpenButton[0])) {
|
||
menuOpenButton.attr('ng-click', '$mdMenu.open($event)');
|
||
}
|
||
|
||
if (!menuOpenButton.attr('aria-label')) {
|
||
menuOpenButton.attr('aria-label', 'Open List Menu');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {'div'|'button'} type
|
||
*/
|
||
function wrapIn(type) {
|
||
if (type === 'div') {
|
||
itemContainer = angular.element('<div class="md-no-style md-list-item-inner">');
|
||
itemContainer.append(tElement.contents());
|
||
tElement.addClass('md-proxy-focus');
|
||
} else {
|
||
// Element which holds the default list-item content.
|
||
itemContainer = angular.element(
|
||
'<div class="md-button md-no-style">' +
|
||
' <div class="md-list-item-inner"></div>' +
|
||
'</div>'
|
||
);
|
||
|
||
// Button which shows ripple and executes primary action.
|
||
var buttonWrap = angular.element('<md-button class="md-no-style"></md-button>');
|
||
|
||
moveAttributes(tElement[0], buttonWrap[0]);
|
||
|
||
// If there is no aria-label set on the button (previously copied over if present)
|
||
// we determine the label from the content and copy it to the button.
|
||
if (!buttonWrap.attr('aria-label')) {
|
||
buttonWrap.attr('aria-label', $mdAria.getText(tElement));
|
||
|
||
// If we set the button's aria-label to the text content, then make the content hidden
|
||
// from screen readers so that it isn't read/traversed twice.
|
||
var listItemInner = itemContainer[0].querySelector('.md-list-item-inner');
|
||
if (listItemInner) {
|
||
listItemInner.setAttribute('aria-hidden', 'true');
|
||
}
|
||
}
|
||
|
||
// 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 (tElement.hasClass('md-no-focus')) {
|
||
buttonWrap.addClass('md-no-focus');
|
||
}
|
||
|
||
// Append the button wrap before our list-item content, because it will overlay in
|
||
// relative.
|
||
itemContainer.prepend(buttonWrap);
|
||
itemContainer.children().eq(1).append(tElement.contents());
|
||
|
||
tElement.addClass('_md-button-wrap');
|
||
}
|
||
|
||
tElement[0].setAttribute('tabindex', '-1');
|
||
tElement.append(itemContainer);
|
||
}
|
||
|
||
function wrapSecondaryItems() {
|
||
var secondaryItemsWrapper = angular.element('<div class="md-secondary-container">');
|
||
|
||
angular.forEach(secondaryItems, function(secondaryItem) {
|
||
wrapSecondaryItem(secondaryItem, secondaryItemsWrapper);
|
||
});
|
||
|
||
itemContainer.append(secondaryItemsWrapper);
|
||
}
|
||
|
||
/**
|
||
* @param {HTMLElement} secondaryItem
|
||
* @param {HTMLDivElement} container
|
||
*/
|
||
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')) {
|
||
|
||
$mdAria.expect(secondaryItem, 'aria-label');
|
||
var buttonWrapper = angular.element('<md-button class="md-secondary md-icon-button">');
|
||
|
||
// Move 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.
|
||
moveAttributes(secondaryItem, buttonWrapper[0], ['ng-if', 'ng-hide', 'ng-show']);
|
||
|
||
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 searching
|
||
// for the proxy items.
|
||
angular.element(secondaryItem).removeClass('md-secondary');
|
||
}
|
||
|
||
tElement.addClass('md-with-secondary');
|
||
container.append(secondaryItem);
|
||
}
|
||
|
||
/**
|
||
* Moves 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 {Element} source Element with the specified attributes
|
||
* @param {Element} destination Element which will receive the attributes
|
||
* @param {string|string[]} extraAttrs Additional attributes, which will be moved over
|
||
*/
|
||
function moveAttributes(source, destination, extraAttrs) {
|
||
var copiedAttrs = $mdUtil.prefixer([
|
||
'ng-if', 'ng-click', 'ng-dblclick', 'aria-label', 'ng-disabled', 'ui-sref',
|
||
'href', 'ng-href', 'rel', 'target', 'ng-attr-ui-sref', 'ui-sref-opts', 'download'
|
||
]);
|
||
|
||
if (extraAttrs) {
|
||
copiedAttrs = copiedAttrs.concat($mdUtil.prefixer(extraAttrs));
|
||
}
|
||
|
||
angular.forEach(copiedAttrs, function(attr) {
|
||
if (source.hasAttribute(attr)) {
|
||
destination.setAttribute(attr, source.getAttribute(attr));
|
||
source.removeAttribute(attr);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* @param {HTMLElement} element
|
||
* @return {boolean} true if the element has one of the proxied tags, false otherwise
|
||
*/
|
||
function isProxiedElement(element) {
|
||
return proxiedTypes.indexOf(element.nodeName.toLowerCase()) !== -1;
|
||
}
|
||
|
||
/**
|
||
* @param {HTMLElement} element
|
||
* @return {boolean} true if the element is a button or md-button, false otherwise
|
||
*/
|
||
function isButton(element) {
|
||
var nodeName = element.nodeName.toUpperCase();
|
||
|
||
return nodeName === "MD-BUTTON" || nodeName === "BUTTON";
|
||
}
|
||
|
||
/**
|
||
* @param {Element} element
|
||
* @return {boolean} true if the element has an ng-click attribute, false otherwise
|
||
*/
|
||
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;
|
||
}
|
||
|
||
return postLink;
|
||
|
||
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),
|
||
noProxies = $element.hasClass('md-no-proxy');
|
||
|
||
computeProxies();
|
||
computeClickable();
|
||
|
||
if (proxies.length) {
|
||
angular.forEach(proxies, function(proxy) {
|
||
proxy = angular.element(proxy);
|
||
|
||
$scope.mouseActive = false;
|
||
proxy.on('mousedown', function() {
|
||
$scope.mouseActive = true;
|
||
$timeout(function() {
|
||
$scope.mouseActive = false;
|
||
}, 100);
|
||
})
|
||
.on('focus', function() {
|
||
if ($scope.mouseActive === false) { $element.addClass('md-focused'); }
|
||
proxy.on('blur', function proxyOnBlur() {
|
||
$element.removeClass('md-focused');
|
||
proxy.off('blur', proxyOnBlur);
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
function computeProxies() {
|
||
if (firstElement && firstElement.children && !hasClick && !noProxies) {
|
||
|
||
angular.forEach(proxiedTypes, function(type) {
|
||
// All elements which are not capable of being used as a proxy have the .md-secondary
|
||
// class applied. These items were identified in the secondary wrap function.
|
||
angular.forEach(firstElement.querySelectorAll(type + ':not(.md-secondary)'), function(child) {
|
||
proxies.push(child);
|
||
});
|
||
});
|
||
}
|
||
}
|
||
|
||
function computeClickable() {
|
||
if (proxies.length === 1 || hasClick) {
|
||
$element.addClass('md-clickable');
|
||
|
||
if (!hasClick) {
|
||
ctrl.attachRipple($scope, angular.element($element[0].querySelector('.md-no-style')));
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {MouseEvent} event
|
||
* @return {boolean}
|
||
*/
|
||
function isEventFromControl(event) {
|
||
var forbiddenControls = ['md-slider'];
|
||
var eventBubblePath = $mdUtil.getEventPath(event);
|
||
|
||
// If there is no bubble path, then the event was not bubbled.
|
||
if (!eventBubblePath || eventBubblePath.length === 0) {
|
||
return forbiddenControls.indexOf(event.target.tagName.toLowerCase()) !== -1;
|
||
}
|
||
|
||
// We iterate the event bubble path up and check for a possible component.
|
||
// Our maximum index to search, is the list item root.
|
||
var maxPath = eventBubblePath.indexOf($element.children()[0]);
|
||
|
||
for (var i = 0; i < maxPath; i++) {
|
||
if (forbiddenControls.indexOf(eventBubblePath[i].tagName.toLowerCase()) !== -1) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* @param {KeyboardEvent} keypressEvent
|
||
*/
|
||
var clickChildKeypressListener = function(keypressEvent) {
|
||
if (keypressEvent.target.nodeName !== 'INPUT' &&
|
||
keypressEvent.target.nodeName !== 'TEXTAREA' &&
|
||
!keypressEvent.target.isContentEditable) {
|
||
var keyCode = keypressEvent.which || keypressEvent.keyCode;
|
||
if (keyCode === $mdConstant.KEY_CODE.SPACE) {
|
||
if (clickChild) {
|
||
clickChild.click();
|
||
keypressEvent.preventDefault();
|
||
keypressEvent.stopPropagation();
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
if (!hasClick && !proxies.length) {
|
||
clickChild && clickChild.addEventListener('keypress', clickChildKeypressListener);
|
||
}
|
||
|
||
$element.off('click');
|
||
$element.off('keypress');
|
||
// Disable ng-aria's "helpful" keydown event that causes our ng-click handlers to be called
|
||
// twice.
|
||
$element.off('keydown');
|
||
|
||
if (proxies.length === 1 && clickChild) {
|
||
$element.children().eq(0).on('click', function(clickEvent) {
|
||
// When the event is coming from a control and it should not trigger the proxied element
|
||
// then we are skipping.
|
||
if (isEventFromControl(clickEvent)) return;
|
||
|
||
var parentButton = $mdUtil.getClosest(clickEvent.target, 'BUTTON');
|
||
if (!parentButton && clickChild.contains(clickEvent.target)) {
|
||
angular.forEach(proxies, function(proxy) {
|
||
if (clickEvent.target !== proxy && !proxy.contains(clickEvent.target)) {
|
||
if (proxy.nodeName === 'MD-MENU') {
|
||
proxy = proxy.children[0];
|
||
}
|
||
angular.element(proxy).triggerHandler('click');
|
||
}
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
$scope.$on('$destroy', function () {
|
||
clickChild && clickChild.removeEventListener('keypress', clickChildKeypressListener);
|
||
});
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
/*
|
||
* @private
|
||
* @ngdoc controller
|
||
* @name MdListController
|
||
* @module material.components.list
|
||
*/
|
||
function MdListController($scope, $element, $mdListInkRipple) {
|
||
var ctrl = this;
|
||
ctrl.attachRipple = attachRipple;
|
||
|
||
function attachRipple (scope, element) {
|
||
var options = {};
|
||
$mdListInkRipple.attach(scope, element, options);
|
||
}
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.menu
|
||
*/
|
||
|
||
angular.module('material.components.menu', [
|
||
'material.core',
|
||
'material.components.backdrop'
|
||
]);
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
|
||
|
||
MenuController.$inject = ["$mdMenu", "$attrs", "$element", "$scope", "$mdUtil", "$timeout", "$rootScope", "$q", "$log"];
|
||
angular
|
||
.module('material.components.menu')
|
||
.controller('mdMenuCtrl', MenuController);
|
||
|
||
/**
|
||
* @ngInject
|
||
*/
|
||
function MenuController($mdMenu, $attrs, $element, $scope, $mdUtil, $timeout, $rootScope, $q, $log) {
|
||
|
||
var prefixer = $mdUtil.prefixer();
|
||
var menuContainer;
|
||
var self = this;
|
||
var triggerElement;
|
||
|
||
this.nestLevel = parseInt($attrs.mdNestLevel, 10) || 0;
|
||
|
||
/**
|
||
* Called by our linking fn to provide access to the menu-content
|
||
* element removed during link
|
||
*/
|
||
this.init = function init(setMenuContainer, opts) {
|
||
opts = opts || {};
|
||
menuContainer = setMenuContainer;
|
||
|
||
// Default element for ARIA attributes has the ngClick or ngMouseenter expression
|
||
triggerElement = $element[0].querySelector(prefixer.buildSelector(['ng-click', 'ng-mouseenter']));
|
||
triggerElement.setAttribute('aria-expanded', 'false');
|
||
|
||
this.isInMenuBar = opts.isInMenuBar;
|
||
this.mdMenuBarCtrl = opts.mdMenuBarCtrl;
|
||
this.nestedMenus = $mdUtil.nodesToArray(menuContainer[0].querySelectorAll('.md-nested-menu'));
|
||
|
||
menuContainer.on('$mdInterimElementRemove', function() {
|
||
self.isOpen = false;
|
||
$mdUtil.nextTick(function(){ self.onIsOpenChanged(self.isOpen);});
|
||
});
|
||
$mdUtil.nextTick(function(){ self.onIsOpenChanged(self.isOpen);});
|
||
|
||
var menuContainerId = 'menu_container_' + $mdUtil.nextUid();
|
||
menuContainer.attr('id', menuContainerId);
|
||
angular.element(triggerElement).attr({
|
||
'aria-owns': menuContainerId,
|
||
'aria-haspopup': 'true'
|
||
});
|
||
|
||
$scope.$on('$destroy', angular.bind(this, function() {
|
||
this.disableHoverListener();
|
||
$mdMenu.destroy();
|
||
}));
|
||
|
||
menuContainer.on('$destroy', function() {
|
||
$mdMenu.destroy();
|
||
});
|
||
};
|
||
|
||
var openMenuTimeout, menuItems, deregisterScopeListeners = [];
|
||
this.enableHoverListener = function() {
|
||
deregisterScopeListeners.push($rootScope.$on('$mdMenuOpen', function(event, el) {
|
||
if (menuContainer[0].contains(el[0])) {
|
||
self.currentlyOpenMenu = el.controller('mdMenu');
|
||
self.isAlreadyOpening = false;
|
||
self.currentlyOpenMenu.registerContainerProxy(self.triggerContainerProxy.bind(self));
|
||
}
|
||
}));
|
||
deregisterScopeListeners.push($rootScope.$on('$mdMenuClose', function(event, el) {
|
||
if (menuContainer[0].contains(el[0])) {
|
||
self.currentlyOpenMenu = undefined;
|
||
}
|
||
}));
|
||
menuItems = angular.element($mdUtil.nodesToArray(menuContainer[0].children[0].children));
|
||
menuItems.on('mouseenter', self.handleMenuItemHover);
|
||
menuItems.on('mouseleave', self.handleMenuItemMouseLeave);
|
||
};
|
||
|
||
this.disableHoverListener = function() {
|
||
while (deregisterScopeListeners.length) {
|
||
deregisterScopeListeners.shift()();
|
||
}
|
||
menuItems && menuItems.off('mouseenter', self.handleMenuItemHover);
|
||
menuItems && menuItems.off('mouseleave', self.handleMenuItemMouseLeave);
|
||
};
|
||
|
||
this.handleMenuItemHover = function(event) {
|
||
if (self.isAlreadyOpening) return;
|
||
var nestedMenu = (
|
||
event.target.querySelector('md-menu')
|
||
|| $mdUtil.getClosest(event.target, 'MD-MENU')
|
||
);
|
||
openMenuTimeout = $timeout(function() {
|
||
if (nestedMenu) {
|
||
nestedMenu = angular.element(nestedMenu).controller('mdMenu');
|
||
}
|
||
|
||
if (self.currentlyOpenMenu && self.currentlyOpenMenu != nestedMenu) {
|
||
var closeTo = self.nestLevel + 1;
|
||
self.currentlyOpenMenu.close(true, { closeTo: closeTo });
|
||
self.isAlreadyOpening = !!nestedMenu;
|
||
nestedMenu && nestedMenu.open();
|
||
} else if (nestedMenu && !nestedMenu.isOpen && nestedMenu.open) {
|
||
self.isAlreadyOpening = !!nestedMenu;
|
||
nestedMenu && nestedMenu.open();
|
||
}
|
||
}, nestedMenu ? 100 : 250);
|
||
var focusableTarget = event.currentTarget.querySelector('.md-button:not([disabled])');
|
||
focusableTarget && focusableTarget.focus();
|
||
};
|
||
|
||
this.handleMenuItemMouseLeave = function() {
|
||
if (openMenuTimeout) {
|
||
$timeout.cancel(openMenuTimeout);
|
||
openMenuTimeout = undefined;
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* Uses the $mdMenu interim element service to open the menu contents
|
||
*/
|
||
this.open = function openMenu(ev) {
|
||
ev && ev.stopPropagation();
|
||
ev && ev.preventDefault();
|
||
if (self.isOpen) return;
|
||
self.enableHoverListener();
|
||
self.isOpen = true;
|
||
$mdUtil.nextTick(function(){ self.onIsOpenChanged(self.isOpen);});
|
||
triggerElement = triggerElement || (ev ? ev.target : $element[0]);
|
||
triggerElement.setAttribute('aria-expanded', 'true');
|
||
$scope.$emit('$mdMenuOpen', $element);
|
||
$mdMenu.show({
|
||
scope: $scope,
|
||
mdMenuCtrl: self,
|
||
nestLevel: self.nestLevel,
|
||
element: menuContainer,
|
||
target: triggerElement,
|
||
preserveElement: true,
|
||
parent: 'body'
|
||
}).finally(function() {
|
||
triggerElement.setAttribute('aria-expanded', 'false');
|
||
self.disableHoverListener();
|
||
});
|
||
};
|
||
|
||
this.onIsOpenChanged = function(isOpen) {
|
||
if (isOpen) {
|
||
menuContainer.attr('aria-hidden', 'false');
|
||
$element[0].classList.add('md-open');
|
||
angular.forEach(self.nestedMenus, function(el) {
|
||
el.classList.remove('md-open');
|
||
});
|
||
} else {
|
||
menuContainer.attr('aria-hidden', 'true');
|
||
$element[0].classList.remove('md-open');
|
||
}
|
||
$scope.$mdMenuIsOpen = self.isOpen;
|
||
};
|
||
|
||
this.focusMenuContainer = function focusMenuContainer() {
|
||
var focusTarget = menuContainer[0]
|
||
.querySelector(prefixer.buildSelector(['md-menu-focus-target', 'md-autofocus']));
|
||
|
||
if (!focusTarget) focusTarget = menuContainer[0].querySelector('.md-button:not([disabled])');
|
||
focusTarget.focus();
|
||
};
|
||
|
||
this.registerContainerProxy = function registerContainerProxy(handler) {
|
||
this.containerProxy = handler;
|
||
};
|
||
|
||
this.triggerContainerProxy = function triggerContainerProxy(ev) {
|
||
this.containerProxy && this.containerProxy(ev);
|
||
};
|
||
|
||
this.destroy = function() {
|
||
return self.isOpen ? $mdMenu.destroy() : $q.when(false);
|
||
};
|
||
|
||
// Use the $mdMenu interim element service to close the menu contents
|
||
this.close = function closeMenu(skipFocus, closeOpts) {
|
||
if (!self.isOpen) return;
|
||
self.isOpen = false;
|
||
$mdUtil.nextTick(function(){ self.onIsOpenChanged(self.isOpen);});
|
||
|
||
var eventDetails = angular.extend({}, closeOpts, { skipFocus: skipFocus });
|
||
$scope.$emit('$mdMenuClose', $element, eventDetails);
|
||
$mdMenu.hide(null, closeOpts);
|
||
|
||
if (!skipFocus) {
|
||
var el = self.restoreFocusTo || $element.find('button')[0];
|
||
if (el instanceof angular.element) el = el[0];
|
||
if (el) el.focus();
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Build a nice object out of our string attribute which specifies the
|
||
* target mode for left and top positioning
|
||
*/
|
||
this.positionMode = function positionMode() {
|
||
var attachment = ($attrs.mdPositionMode || 'target').split(' ');
|
||
|
||
// If attachment is a single item, duplicate it for our second value.
|
||
// ie. 'target' -> 'target target'
|
||
if (attachment.length === 1) {
|
||
attachment.push(attachment[0]);
|
||
}
|
||
|
||
return {
|
||
left: attachment[0],
|
||
top: attachment[1]
|
||
};
|
||
};
|
||
|
||
/**
|
||
* Build a nice object out of our string attribute which specifies
|
||
* the offset of top and left in pixels.
|
||
*/
|
||
this.offsets = function offsets() {
|
||
var position = ($attrs.mdOffset || '0 0').split(' ').map(parseFloat);
|
||
if (position.length === 2) {
|
||
return {
|
||
left: position[0],
|
||
top: position[1]
|
||
};
|
||
} else if (position.length === 1) {
|
||
return {
|
||
top: position[0],
|
||
left: position[0]
|
||
};
|
||
} else {
|
||
throw Error('Invalid offsets specified. Please follow format <x, y> or <n>');
|
||
}
|
||
};
|
||
|
||
// Functionality that is exposed in the view.
|
||
$scope.$mdMenu = {
|
||
open: this.open,
|
||
close: this.close
|
||
};
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdMenu
|
||
* @module material.components.menu
|
||
* @restrict E
|
||
* @description
|
||
*
|
||
* Menus are elements that open when clicked. They are useful for displaying
|
||
* additional options within the context of an action.
|
||
*
|
||
* Every `md-menu` must specify exactly two child elements. The first element is what is
|
||
* left in the DOM and is used to open the menu. This element is called the trigger element.
|
||
* The trigger element's scope has access to `$mdMenu.open($event)`
|
||
* which it may call to open the menu. By passing $event as argument, the
|
||
* corresponding event is stopped from propagating up the DOM-tree. Similarly, `$mdMenu.close()`
|
||
* can be used to close the menu.
|
||
*
|
||
* The second element is the `md-menu-content` element which represents the
|
||
* contents of the menu when it is open. Typically this will contain `md-menu-item`s,
|
||
* but you can do custom content as well.
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-menu>
|
||
* <!-- Trigger element is a md-button with an icon -->
|
||
* <md-button ng-click="$mdMenu.open($event)" class="md-icon-button" aria-label="Open sample menu">
|
||
* <md-icon md-svg-icon="call:phone"></md-icon>
|
||
* </md-button>
|
||
* <md-menu-content>
|
||
* <md-menu-item><md-button ng-click="doSomething()">Do Something</md-button></md-menu-item>
|
||
* </md-menu-content>
|
||
* </md-menu>
|
||
* </hljs>
|
||
|
||
* ## Sizing Menus
|
||
*
|
||
* The width of the menu when it is open may be specified by specifying a `width`
|
||
* attribute on the `md-menu-content` element.
|
||
* See the [Material Design Spec](https://material.io/archive/guidelines/components/menus.html#menus-simple-menus)
|
||
* for more information.
|
||
*
|
||
* ## Menu Density
|
||
*
|
||
* You can use dense menus by adding the `md-dense` class to the `md-menu-content` element.
|
||
* This reduces the height of menu items, their top and bottom padding, and default font size.
|
||
* Without the `md-dense` class, we use the "mobile" height of `48px`. With the `md-dense` class,
|
||
* we use the "desktop" height of `32px`. We do not support the "dense desktop" option in the spec,
|
||
* which uses a height of `24px`, at this time.
|
||
* See the [Menu Specs](https://material.io/archive/guidelines/components/menus.html#menus-specs)
|
||
* section of the Material Design Spec for more information.
|
||
*
|
||
* ## Aligning Menus
|
||
*
|
||
* When a menu opens, it is important that the content aligns with the trigger element.
|
||
* Failure to align menus can result in jarring experiences for users as content
|
||
* suddenly shifts. To help with this, `md-menu` provides several APIs to help
|
||
* with alignment.
|
||
*
|
||
* ### Target Mode
|
||
*
|
||
* By default, `md-menu` will attempt to align the `md-menu-content` by aligning
|
||
* designated child elements in both the trigger and the menu content.
|
||
*
|
||
* To specify the alignment element in the `trigger` you can use the `md-menu-origin`
|
||
* attribute on a child element. If no `md-menu-origin` is specified, the `md-menu`
|
||
* will be used as the origin element.
|
||
*
|
||
* Similarly, the `md-menu-content` may specify a `md-menu-align-target` for a
|
||
* `md-menu-item` to specify the node that it should try and align with.
|
||
*
|
||
* In this example code, we specify an icon to be our origin element, and an
|
||
* icon in our menu content to be our alignment target. This ensures that both
|
||
* icons are aligned when the menu opens.
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-menu>
|
||
* <md-button ng-click="$mdMenu.open($event)" class="md-icon-button" aria-label="Open some menu">
|
||
* <md-icon md-menu-origin md-svg-icon="call:phone"></md-icon>
|
||
* </md-button>
|
||
* <md-menu-content>
|
||
* <md-menu-item>
|
||
* <md-button ng-click="doSomething()" aria-label="Do something">
|
||
* <md-icon md-menu-align-target md-svg-icon="call:phone"></md-icon>
|
||
* Do Something
|
||
* </md-button>
|
||
* </md-menu-item>
|
||
* </md-menu-content>
|
||
* </md-menu>
|
||
* </hljs>
|
||
*
|
||
* ### Position Mode
|
||
*
|
||
* We can specify the origin of the menu by using the `md-position-mode` attribute.
|
||
* This attribute allows specifying the positioning by the `x` and `y` axes.
|
||
*
|
||
* The default mode is `target target`. This mode uses the left and top edges of the origin element
|
||
* to position the menu in LTR layouts. The `x` axis modes will adjust when in RTL layouts.
|
||
*
|
||
* Sometimes you want to specify alignment from the right side of a origin element. For example,
|
||
* if we have a menu on the right side a toolbar, we may want to right align our menu content.
|
||
* We can use `target-right target` to specify a right-oriented alignment target.
|
||
* There is a working example of this in the Menu Position Modes demo.
|
||
*
|
||
* #### Horizontal Positioning Options
|
||
* - `target`
|
||
* - `target-left`
|
||
* - `target-right`
|
||
* - `cascade`
|
||
* - `right`
|
||
* - `left`
|
||
*
|
||
* #### Vertical Positioning Options
|
||
* - `target`
|
||
* - `cascade`
|
||
* - `bottom`
|
||
*
|
||
* ### Menu Offsets
|
||
*
|
||
* It is sometimes unavoidable to need to have a deeper level of control for
|
||
* the positioning of a menu to ensure perfect alignment. `md-menu` provides
|
||
* the `md-offset` attribute to allow pixel-level specificity when adjusting
|
||
* menu positioning.
|
||
*
|
||
* This offset is provided in the format of `x y` or `n` where `n` will be used
|
||
* in both the `x` and `y` axis.
|
||
* For example, to move a menu by `2px` down from the top, we can use:
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-menu md-offset="0 2">
|
||
* <!-- menu-content -->
|
||
* </md-menu>
|
||
* </hljs>
|
||
*
|
||
* Specifying `md-offset="2 2"` would shift the menu two pixels down and two pixels to the right.
|
||
*
|
||
* ### Auto Focus
|
||
* By default, when a menu opens, `md-menu` focuses the first button in the menu content.
|
||
*
|
||
* Sometimes you would like to focus another menu item instead of the first.<br/>
|
||
* This can be done by applying the `md-autofocus` directive on the given element.
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-menu-item>
|
||
* <md-button md-autofocus ng-click="doSomething()">
|
||
* Auto Focus
|
||
* </md-button>
|
||
* </md-menu-item>
|
||
* </hljs>
|
||
*
|
||
*
|
||
* ### Preventing close
|
||
*
|
||
* Sometimes you would like to be able to click on a menu item without having the menu
|
||
* close. To do this, AngularJS Material exposes the `md-prevent-menu-close` attribute which
|
||
* can be added to a button inside a menu to stop the menu from automatically closing.
|
||
* You can then close the menu either by using `$mdMenu.close()` in the template,
|
||
* or programmatically by injecting `$mdMenu` and calling `$mdMenu.hide()`.
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-menu-content ng-mouseleave="$mdMenu.close()">
|
||
* <md-menu-item>
|
||
* <md-button ng-click="doSomething()" aria-label="Do something" md-prevent-menu-close="md-prevent-menu-close">
|
||
* <md-icon md-menu-align-target md-svg-icon="call:phone"></md-icon>
|
||
* Do Something
|
||
* </md-button>
|
||
* </md-menu-item>
|
||
* </md-menu-content>
|
||
* </hljs>
|
||
*
|
||
* @usage
|
||
* <hljs lang="html">
|
||
* <md-menu>
|
||
* <md-button ng-click="$mdMenu.open($event)" class="md-icon-button">
|
||
* <md-icon md-svg-icon="call:phone"></md-icon>
|
||
* </md-button>
|
||
* <md-menu-content>
|
||
* <md-menu-item><md-button ng-click="doSomething()">Do Something</md-button></md-menu-item>
|
||
* </md-menu-content>
|
||
* </md-menu>
|
||
* </hljs>
|
||
*
|
||
* @param {string=} md-position-mode Specify pre-defined position modes for the `x` and `y` axes.
|
||
* The default modes are `target target`. This positions the origin of the menu using the left and top edges
|
||
* of the origin element in LTR layouts.<br>
|
||
* #### Valid modes for horizontal positioning
|
||
* - `target`
|
||
* - `target-left`
|
||
* - `target-right`
|
||
* - `cascade`
|
||
* - `right`
|
||
* - `left`<br>
|
||
* #### Valid modes for vertical positioning
|
||
* - `target`
|
||
* - `cascade`
|
||
* - `bottom`
|
||
* @param {string=} md-offset An offset to apply to the dropdown on opening, after positioning.
|
||
* Defined as `x` and `y` pixel offset values in the form of `x y`.<br>
|
||
* The default value is `0 0`.
|
||
*/
|
||
MenuDirective.$inject = ["$mdUtil"];
|
||
angular
|
||
.module('material.components.menu')
|
||
.directive('mdMenu', MenuDirective);
|
||
|
||
/**
|
||
* @ngInject
|
||
*/
|
||
function MenuDirective($mdUtil) {
|
||
var INVALID_PREFIX = 'Invalid HTML for md-menu: ';
|
||
return {
|
||
restrict: 'E',
|
||
require: ['mdMenu', '?^mdMenuBar'],
|
||
controller: 'mdMenuCtrl', // empty function to be built by link
|
||
scope: true,
|
||
compile: compile
|
||
};
|
||
|
||
function compile(templateElement) {
|
||
templateElement.addClass('md-menu');
|
||
|
||
var triggerEl = templateElement.children()[0];
|
||
var prefixer = $mdUtil.prefixer();
|
||
|
||
if (!prefixer.hasAttribute(triggerEl, 'ng-click')) {
|
||
triggerEl = triggerEl
|
||
.querySelector(prefixer.buildSelector(['ng-click', 'ng-mouseenter'])) || triggerEl;
|
||
}
|
||
|
||
var isButtonTrigger = triggerEl.nodeName === 'MD-BUTTON' || triggerEl.nodeName === 'BUTTON';
|
||
|
||
if (triggerEl && isButtonTrigger && !triggerEl.hasAttribute('type')) {
|
||
triggerEl.setAttribute('type', 'button');
|
||
}
|
||
|
||
if (!triggerEl) {
|
||
throw Error(INVALID_PREFIX + 'Expected the menu to have a trigger element.');
|
||
}
|
||
|
||
if (templateElement.children().length !== 2) {
|
||
throw Error(INVALID_PREFIX + 'Expected two children elements. The second element must have a `md-menu-content` element.');
|
||
}
|
||
|
||
// Default element for ARIA attributes has the ngClick or ngMouseenter expression
|
||
triggerEl && triggerEl.setAttribute('aria-haspopup', 'true');
|
||
|
||
var nestedMenus = templateElement[0].querySelectorAll('md-menu');
|
||
var nestingDepth = parseInt(templateElement[0].getAttribute('md-nest-level'), 10) || 0;
|
||
if (nestedMenus) {
|
||
angular.forEach($mdUtil.nodesToArray(nestedMenus), function(menuEl) {
|
||
if (!menuEl.hasAttribute('md-position-mode')) {
|
||
menuEl.setAttribute('md-position-mode', 'cascade');
|
||
}
|
||
menuEl.classList.add('_md-nested-menu');
|
||
menuEl.setAttribute('md-nest-level', nestingDepth + 1);
|
||
});
|
||
}
|
||
return link;
|
||
}
|
||
|
||
function link(scope, element, attr, ctrls) {
|
||
var mdMenuCtrl = ctrls[0];
|
||
var isInMenuBar = !!ctrls[1];
|
||
var mdMenuBarCtrl = ctrls[1];
|
||
// Move everything into a md-menu-container and pass it to the controller
|
||
var menuContainer = angular.element('<div class="_md md-open-menu-container md-whiteframe-z2"></div>');
|
||
var menuContents = element.children()[1];
|
||
|
||
element.addClass('_md'); // private md component indicator for styling
|
||
|
||
if (!menuContents.hasAttribute('role')) {
|
||
menuContents.setAttribute('role', 'menu');
|
||
}
|
||
menuContainer.append(menuContents);
|
||
|
||
element.on('$destroy', function() {
|
||
menuContainer.remove();
|
||
});
|
||
|
||
element.append(menuContainer);
|
||
menuContainer[0].style.display = 'none';
|
||
mdMenuCtrl.init(menuContainer, { isInMenuBar: isInMenuBar, mdMenuBarCtrl: mdMenuBarCtrl });
|
||
}
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
|
||
MenuProvider.$inject = ["$$interimElementProvider"];angular
|
||
.module('material.components.menu')
|
||
.provider('$mdMenu', MenuProvider);
|
||
|
||
/**
|
||
* Interim element provider for the menu.
|
||
* Handles behavior for a menu while it is open, including:
|
||
* - handling animating the menu opening/closing
|
||
* - handling key/mouse events on the menu element
|
||
* - handling enabling/disabling scroll while the menu is open
|
||
* - handling redrawing during resizes and orientation changes
|
||
*
|
||
*/
|
||
|
||
function MenuProvider($$interimElementProvider) {
|
||
menuDefaultOptions.$inject = ["$mdUtil", "$mdTheming", "$mdConstant", "$document", "$window", "$q", "$$rAF", "$animateCss", "$animate", "$log"];
|
||
var MENU_EDGE_MARGIN = 8;
|
||
|
||
return $$interimElementProvider('$mdMenu')
|
||
.setDefaults({
|
||
methods: ['target'],
|
||
options: menuDefaultOptions
|
||
});
|
||
|
||
/* @ngInject */
|
||
function menuDefaultOptions($mdUtil, $mdTheming, $mdConstant, $document, $window, $q, $$rAF,
|
||
$animateCss, $animate, $log) {
|
||
|
||
var prefixer = $mdUtil.prefixer();
|
||
var animator = $mdUtil.dom.animator;
|
||
|
||
return {
|
||
parent: 'body',
|
||
onShow: onShow,
|
||
onRemove: onRemove,
|
||
hasBackdrop: true,
|
||
disableParentScroll: true,
|
||
skipCompile: true,
|
||
preserveScope: true,
|
||
multiple: true,
|
||
themable: true
|
||
};
|
||
|
||
/**
|
||
* Show modal backdrop element...
|
||
* @returns {function(): void} A function that removes this backdrop
|
||
*/
|
||
function showBackdrop(scope, element, options) {
|
||
if (options.nestLevel) return angular.noop;
|
||
|
||
// If we are not within a dialog...
|
||
if (options.disableParentScroll && !$mdUtil.getClosest(options.target, 'MD-DIALOG')) {
|
||
// !! DO this before creating the backdrop; since disableScrollAround()
|
||
// configures the scroll offset; which is used by mdBackDrop postLink()
|
||
options.restoreScroll = $mdUtil.disableScrollAround(options.element, options.parent);
|
||
} else {
|
||
options.disableParentScroll = false;
|
||
}
|
||
|
||
if (options.hasBackdrop) {
|
||
options.backdrop = $mdUtil.createBackdrop(scope, "md-menu-backdrop md-click-catcher");
|
||
|
||
$animate.enter(options.backdrop, options.backdropParent || $document[0].body);
|
||
}
|
||
|
||
/**
|
||
* Hide and destroys the backdrop created by showBackdrop()
|
||
*/
|
||
return function hideBackdrop() {
|
||
if (options.backdrop) options.backdrop.remove();
|
||
if (options.disableParentScroll) options.restoreScroll();
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Removing the menu element from the DOM and remove all associated event listeners
|
||
* and backdrop
|
||
*/
|
||
function onRemove(scope, element, opts) {
|
||
opts.cleanupInteraction();
|
||
opts.cleanupBackdrop();
|
||
opts.cleanupResizing();
|
||
opts.hideBackdrop();
|
||
|
||
// Before the menu is closing remove the clickable class.
|
||
element.removeClass('md-clickable');
|
||
|
||
// For navigation $destroy events, do a quick, non-animated removal,
|
||
// but for normal closes (from clicks, etc) animate the removal
|
||
|
||
return (opts.$destroy === true) ? detachAndClean() : animateRemoval().then(detachAndClean);
|
||
|
||
/**
|
||
* For normal closes, animate the removal.
|
||
* For forced closes (like $destroy events), skip the animations
|
||
*/
|
||
function animateRemoval() {
|
||
return $animateCss(element, {addClass: 'md-leave'}).start();
|
||
}
|
||
|
||
/**
|
||
* Detach the element
|
||
*/
|
||
function detachAndClean() {
|
||
element.removeClass('md-active');
|
||
detachElement(element, opts);
|
||
opts.alreadyOpen = false;
|
||
}
|
||
|
||
}
|
||
|
||
/**
|
||
* Inserts and configures the staged Menu element into the DOM, positioning it,
|
||
* and wiring up various interaction events
|
||
*/
|
||
function onShow(scope, element, opts) {
|
||
sanitizeAndConfigure(opts);
|
||
|
||
if (opts.menuContentEl[0]) {
|
||
// Inherit the theme from the target element.
|
||
$mdTheming.inherit(opts.menuContentEl, opts.target);
|
||
} else {
|
||
$log.warn(
|
||
'$mdMenu: Menu elements should always contain a `md-menu-content` element,' +
|
||
'otherwise interactivity features will not work properly.',
|
||
element
|
||
);
|
||
}
|
||
|
||
// Register various listeners to move menu on resize/orientation change
|
||
opts.cleanupResizing = startRepositioningOnResize();
|
||
opts.hideBackdrop = showBackdrop(scope, element, opts);
|
||
|
||
// Return the promise for when our menu is done animating in
|
||
return showMenu()
|
||
.then(function(response) {
|
||
opts.alreadyOpen = true;
|
||
opts.cleanupInteraction = activateInteraction();
|
||
opts.cleanupBackdrop = setupBackdrop();
|
||
|
||
// Since the menu finished its animation, mark the menu as clickable.
|
||
element.addClass('md-clickable');
|
||
|
||
return response;
|
||
});
|
||
|
||
/**
|
||
* Place the menu into the DOM and call positioning related functions
|
||
*/
|
||
function showMenu() {
|
||
opts.parent.append(element);
|
||
element[0].style.display = '';
|
||
|
||
return $q(function(resolve) {
|
||
var position = calculateMenuPosition(element, opts);
|
||
|
||
element.removeClass('md-leave');
|
||
|
||
// Animate the menu scaling, and opacity [from its position origin (default == top-left)]
|
||
// to normal scale.
|
||
$animateCss(element, {
|
||
addClass: 'md-active',
|
||
from: animator.toCss(position),
|
||
to: animator.toCss({transform: ''})
|
||
})
|
||
.start()
|
||
.then(resolve);
|
||
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Check for valid opts and set some useful defaults
|
||
*/
|
||
function sanitizeAndConfigure() {
|
||
if (!opts.target) {
|
||
throw Error(
|
||
'$mdMenu.show() expected a target to animate from in options.target'
|
||
);
|
||
}
|
||
angular.extend(opts, {
|
||
alreadyOpen: false,
|
||
isRemoved: false,
|
||
target: angular.element(opts.target), // make sure it's not a naked DOM node
|
||
parent: angular.element(opts.parent),
|
||
menuContentEl: angular.element(element[0].querySelector('md-menu-content'))
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Configure various resize listeners for screen changes
|
||
*/
|
||
function startRepositioningOnResize() {
|
||
|
||
var repositionMenu = (function(target, options) {
|
||
return $$rAF.throttle(function() {
|
||
if (opts.isRemoved) return;
|
||
var position = calculateMenuPosition(target, options);
|
||
|
||
target.css(animator.toCss(position));
|
||
});
|
||
})(element, opts);
|
||
|
||
$window.addEventListener('resize', repositionMenu);
|
||
$window.addEventListener('orientationchange', repositionMenu);
|
||
|
||
return function stopRepositioningOnResize() {
|
||
|
||
// Disable resizing handlers
|
||
$window.removeEventListener('resize', repositionMenu);
|
||
$window.removeEventListener('orientationchange', repositionMenu);
|
||
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Sets up the backdrop and listens for click elements.
|
||
* Once the backdrop will be clicked, the menu will automatically close.
|
||
* @returns {!Function} Function to remove the backdrop.
|
||
*/
|
||
function setupBackdrop() {
|
||
if (!opts.backdrop) return angular.noop;
|
||
|
||
opts.backdrop.on('click', onBackdropClick);
|
||
|
||
return function() {
|
||
opts.backdrop.off('click', onBackdropClick);
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Function to be called whenever the backdrop is clicked.
|
||
* @param {!MouseEvent} event
|
||
*/
|
||
function onBackdropClick(event) {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
|
||
scope.$apply(function() {
|
||
opts.mdMenuCtrl.close(true, { closeAll: true });
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Activate interaction on the menu. Resolves the focus target and closes the menu on
|
||
* escape or option click.
|
||
* @returns {!Function} Function to deactivate the interaction listeners.
|
||
*/
|
||
function activateInteraction() {
|
||
if (!opts.menuContentEl[0]) return angular.noop;
|
||
|
||
// Wire up keyboard listeners.
|
||
// - Close on escape,
|
||
// - focus next item on down arrow,
|
||
// - focus prev item on up
|
||
opts.menuContentEl.on('keydown', onMenuKeyDown);
|
||
opts.menuContentEl[0].addEventListener('click', captureClickListener, true);
|
||
|
||
// kick off initial focus in the menu on the first enabled element
|
||
var focusTarget = opts.menuContentEl[0]
|
||
.querySelector(prefixer.buildSelector(['md-menu-focus-target', 'md-autofocus']));
|
||
|
||
if (!focusTarget) {
|
||
var childrenLen = opts.menuContentEl[0].children.length;
|
||
for (var childIndex = 0; childIndex < childrenLen; childIndex++) {
|
||
var child = opts.menuContentEl[0].children[childIndex];
|
||
focusTarget = child.querySelector('.md-button:not([disabled])');
|
||
if (focusTarget) {
|
||
break;
|
||
}
|
||
// Need to check the attribute as well since this might be a custom element whose
|
||
// disabled property is undefined.
|
||
if (child.firstElementChild && !child.firstElementChild.disabled &&
|
||
!child.firstElementChild.getAttribute('disabled')) {
|
||
focusTarget = child.firstElementChild;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
focusTarget && focusTarget.focus();
|
||
|
||
return function cleanupInteraction() {
|
||
opts.menuContentEl.off('keydown', onMenuKeyDown);
|
||
opts.menuContentEl[0].removeEventListener('click', captureClickListener, true);
|
||
};
|
||
|
||
// ************************************
|
||
// internal functions
|
||
// ************************************
|
||
|
||
function onMenuKeyDown(ev) {
|
||
var handled;
|
||
switch (ev.keyCode) {
|
||
case $mdConstant.KEY_CODE.ESCAPE:
|
||
if (opts.nestLevel) {
|
||
opts.mdMenuCtrl.close();
|
||
} else {
|
||
opts.mdMenuCtrl.close(false, { closeAll: true });
|
||
}
|
||
handled = true;
|
||
break;
|
||
case $mdConstant.KEY_CODE.TAB:
|
||
opts.mdMenuCtrl.close(false, { closeAll: true });
|
||
// Don't prevent default or stop propagation on this event as we want tab
|
||
// to move the focus to the next focusable element on the page.
|
||
handled = false;
|
||
break;
|
||
case $mdConstant.KEY_CODE.UP_ARROW:
|
||
if (!focusMenuItem(ev, opts.menuContentEl, opts, -1) && !opts.nestLevel) {
|
||
opts.mdMenuCtrl.triggerContainerProxy(ev);
|
||
}
|
||
handled = true;
|
||
break;
|
||
case $mdConstant.KEY_CODE.DOWN_ARROW:
|
||
if (!focusMenuItem(ev, opts.menuContentEl, opts, 1) && !opts.nestLevel) {
|
||
opts.mdMenuCtrl.triggerContainerProxy(ev);
|
||
}
|
||
handled = true;
|
||
break;
|
||
case $mdConstant.KEY_CODE.LEFT_ARROW:
|
||
if (opts.nestLevel) {
|
||
opts.mdMenuCtrl.close();
|
||
} else {
|
||
opts.mdMenuCtrl.triggerContainerProxy(ev);
|
||
}
|
||
handled = true;
|
||
break;
|
||
case $mdConstant.KEY_CODE.RIGHT_ARROW:
|
||
var parentMenu = $mdUtil.getClosest(ev.target, 'MD-MENU');
|
||
if (parentMenu && parentMenu != opts.parent[0]) {
|
||
ev.target.click();
|
||
} else {
|
||
opts.mdMenuCtrl.triggerContainerProxy(ev);
|
||
}
|
||
handled = true;
|
||
break;
|
||
}
|
||
if (handled) {
|
||
ev.preventDefault();
|
||
ev.stopImmediatePropagation();
|
||
}
|
||
}
|
||
|
||
function onBackdropClick(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
scope.$apply(function() {
|
||
opts.mdMenuCtrl.close(true, { closeAll: true });
|
||
});
|
||
}
|
||
|
||
// Close menu on menu item click, if said menu-item is not disabled
|
||
function captureClickListener(e) {
|
||
var target = e.target;
|
||
// Traverse up the event until we get to the menuContentEl to see if
|
||
// there is an ng-click and that the ng-click is not disabled
|
||
do {
|
||
if (target == opts.menuContentEl[0]) return;
|
||
if ((hasAnyAttribute(target, ['ng-click', 'ng-href', 'ui-sref']) ||
|
||
target.nodeName == 'BUTTON' || target.nodeName == 'MD-BUTTON') && !hasAnyAttribute(target, ['md-prevent-menu-close'])) {
|
||
var closestMenu = $mdUtil.getClosest(target, 'MD-MENU');
|
||
if (!target.hasAttribute('disabled') && (!closestMenu || closestMenu == opts.parent[0])) {
|
||
close();
|
||
}
|
||
break;
|
||
}
|
||
} while (target = target.parentNode);
|
||
|
||
function close() {
|
||
scope.$apply(function() {
|
||
opts.mdMenuCtrl.close(true, { closeAll: true });
|
||
});
|
||
}
|
||
|
||
function hasAnyAttribute(target, attrs) {
|
||
if (!target) return false;
|
||
|
||
for (var i = 0, attr; attr = attrs[i]; ++i) {
|
||
if (prefixer.hasAttribute(target, attr)) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
}
|
||
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Takes a keypress event and focuses the next/previous menu
|
||
* item from the emitting element
|
||
* @param {event} e - The origin keypress event
|
||
* @param {angular.element} menuEl - The menu element
|
||
* @param {object} opts - The interim element options for the mdMenu
|
||
* @param {number} direction - The direction to move in (+1 = next, -1 = prev)
|
||
*/
|
||
function focusMenuItem(e, menuEl, opts, direction) {
|
||
var currentItem = $mdUtil.getClosest(e.target, 'MD-MENU-ITEM');
|
||
|
||
var items = $mdUtil.nodesToArray(menuEl[0].children);
|
||
var currentIndex = items.indexOf(currentItem);
|
||
|
||
// Traverse through our elements in the specified direction (+/-1) and try to
|
||
// focus them until we find one that accepts focus
|
||
var didFocus;
|
||
for (var i = currentIndex + direction; i >= 0 && i < items.length; i = i + direction) {
|
||
var focusTarget = items[i].querySelector('.md-button');
|
||
didFocus = attemptFocus(focusTarget);
|
||
if (didFocus) {
|
||
break;
|
||
}
|
||
}
|
||
return didFocus;
|
||
}
|
||
|
||
/**
|
||
* Attempts to focus an element. Checks whether that element is the currently
|
||
* focused element after attempting.
|
||
* @param {HTMLElement} el - the element to attempt focus on
|
||
* @returns {boolean} - whether the element was successfully focused
|
||
*/
|
||
function attemptFocus(el) {
|
||
if (el && el.getAttribute('tabindex') != -1) {
|
||
el.focus();
|
||
return ($document[0].activeElement == el);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Use browser to remove this element without triggering a $destroy event
|
||
*/
|
||
function detachElement(element, opts) {
|
||
if (!opts.preserveElement) {
|
||
if (toNode(element).parentNode === toNode(opts.parent)) {
|
||
toNode(opts.parent).removeChild(toNode(element));
|
||
}
|
||
} else {
|
||
toNode(element).style.display = 'none';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Computes menu position and sets the style on the menu container
|
||
* @param {HTMLElement} el - the menu container element
|
||
* @param {object} opts - the interim element options object
|
||
*/
|
||
function calculateMenuPosition(el, opts) {
|
||
|
||
var containerNode = el[0],
|
||
openMenuNode = el[0].firstElementChild,
|
||
openMenuNodeRect = openMenuNode.getBoundingClientRect(),
|
||
boundryNode = $document[0].body,
|
||
boundryNodeRect = boundryNode.getBoundingClientRect();
|
||
|
||
var menuStyle = $window.getComputedStyle(openMenuNode);
|
||
|
||
var originNode = opts.target[0].querySelector(prefixer.buildSelector('md-menu-origin')) || opts.target[0],
|
||
originNodeRect = originNode.getBoundingClientRect();
|
||
|
||
var bounds = {
|
||
left: boundryNodeRect.left + MENU_EDGE_MARGIN,
|
||
top: Math.max(boundryNodeRect.top, 0) + MENU_EDGE_MARGIN,
|
||
bottom: Math.max(boundryNodeRect.bottom, Math.max(boundryNodeRect.top, 0) + boundryNodeRect.height) - MENU_EDGE_MARGIN,
|
||
right: boundryNodeRect.right - MENU_EDGE_MARGIN
|
||
};
|
||
|
||
var alignTarget, alignTargetRect = { top:0, left : 0, right:0, bottom:0 }, existingOffsets = { top:0, left : 0, right:0, bottom:0 };
|
||
var positionMode = opts.mdMenuCtrl.positionMode();
|
||
|
||
if (positionMode.top === 'target' || positionMode.left === 'target' || positionMode.left === 'target-right') {
|
||
alignTarget = firstVisibleChild();
|
||
if (alignTarget) {
|
||
// TODO: Allow centering on an arbitrary node, for now center on first menu-item's child
|
||
alignTarget = alignTarget.firstElementChild || alignTarget;
|
||
alignTarget = alignTarget.querySelector(prefixer.buildSelector('md-menu-align-target')) || alignTarget;
|
||
alignTargetRect = alignTarget.getBoundingClientRect();
|
||
|
||
existingOffsets = {
|
||
top: parseFloat(containerNode.style.top || 0),
|
||
left: parseFloat(containerNode.style.left || 0)
|
||
};
|
||
}
|
||
}
|
||
|
||
var position = {};
|
||
var transformOrigin = 'top ';
|
||
|
||
switch (positionMode.top) {
|
||
case 'target':
|
||
position.top = existingOffsets.top + originNodeRect.top - alignTargetRect.top;
|
||
break;
|
||
case 'cascade':
|
||
position.top = originNodeRect.top - parseFloat(menuStyle.paddingTop) - originNode.style.top;
|
||
break;
|
||
case 'bottom':
|
||
position.top = originNodeRect.top + originNodeRect.height;
|
||
break;
|
||
default:
|
||
throw new Error('Invalid target mode "' + positionMode.top + '" specified for md-menu on Y axis.');
|
||
}
|
||
|
||
var rtl = $mdUtil.isRtl(el);
|
||
|
||
switch (positionMode.left) {
|
||
case 'target':
|
||
position.left = existingOffsets.left + originNodeRect.left - alignTargetRect.left;
|
||
transformOrigin += rtl ? 'right' : 'left';
|
||
break;
|
||
case 'target-left':
|
||
position.left = originNodeRect.left;
|
||
transformOrigin += 'left';
|
||
break;
|
||
case 'target-right':
|
||
position.left = originNodeRect.right - openMenuNodeRect.width + (openMenuNodeRect.right - alignTargetRect.right);
|
||
transformOrigin += 'right';
|
||
break;
|
||
case 'cascade':
|
||
var willFitRight = rtl ? (originNodeRect.left - openMenuNodeRect.width) < bounds.left : (originNodeRect.right + openMenuNodeRect.width) < bounds.right;
|
||
position.left = willFitRight ? originNodeRect.right - originNode.style.left : originNodeRect.left - originNode.style.left - openMenuNodeRect.width;
|
||
transformOrigin += willFitRight ? 'left' : 'right';
|
||
break;
|
||
case 'right':
|
||
if (rtl) {
|
||
position.left = originNodeRect.right - originNodeRect.width;
|
||
transformOrigin += 'left';
|
||
} else {
|
||
position.left = originNodeRect.right - openMenuNodeRect.width;
|
||
transformOrigin += 'right';
|
||
}
|
||
break;
|
||
case 'left':
|
||
if (rtl) {
|
||
position.left = originNodeRect.right - openMenuNodeRect.width;
|
||
transformOrigin += 'right';
|
||
} else {
|
||
position.left = originNodeRect.left;
|
||
transformOrigin += 'left';
|
||
}
|
||
break;
|
||
default:
|
||
throw new Error('Invalid target mode "' + positionMode.left + '" specified for md-menu on X axis.');
|
||
}
|
||
|
||
var offsets = opts.mdMenuCtrl.offsets();
|
||
position.top += offsets.top;
|
||
position.left += offsets.left;
|
||
|
||
clamp(position);
|
||
|
||
var scaleX = Math.round(100 * Math.min(originNodeRect.width / containerNode.offsetWidth, 1.0)) / 100;
|
||
var scaleY = Math.round(100 * Math.min(originNodeRect.height / containerNode.offsetHeight, 1.0)) / 100;
|
||
|
||
return {
|
||
top: Math.round(position.top),
|
||
left: Math.round(position.left),
|
||
// Animate a scale out if we aren't just repositioning
|
||
transform: !opts.alreadyOpen ? $mdUtil.supplant('scale({0},{1})', [scaleX, scaleY]) : undefined,
|
||
transformOrigin: transformOrigin
|
||
};
|
||
|
||
/**
|
||
* Clamps the repositioning of the menu within the confines of
|
||
* bounding element (often the screen/body)
|
||
*/
|
||
function clamp(pos) {
|
||
pos.top = Math.max(Math.min(pos.top, bounds.bottom - containerNode.offsetHeight), bounds.top);
|
||
pos.left = Math.max(Math.min(pos.left, bounds.right - containerNode.offsetWidth), bounds.left);
|
||
}
|
||
|
||
/**
|
||
* Gets the first visible child in the openMenuNode
|
||
* Necessary incase menu nodes are being dynamically hidden
|
||
*/
|
||
function firstVisibleChild() {
|
||
for (var i = 0; i < openMenuNode.children.length; ++i) {
|
||
if ($window.getComputedStyle(openMenuNode.children[i]).display != 'none') {
|
||
return openMenuNode.children[i];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
function toNode(el) {
|
||
if (el instanceof angular.element) {
|
||
el = el[0];
|
||
}
|
||
return el;
|
||
}
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.menuBar
|
||
*/
|
||
|
||
angular.module('material.components.menuBar', [
|
||
'material.core',
|
||
'material.components.icon',
|
||
'material.components.menu'
|
||
]);
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
|
||
MenuBarController.$inject = ["$scope", "$rootScope", "$element", "$attrs", "$mdConstant", "$document", "$mdUtil", "$timeout"];
|
||
angular
|
||
.module('material.components.menuBar')
|
||
.controller('MenuBarController', MenuBarController);
|
||
|
||
var BOUND_MENU_METHODS = ['handleKeyDown', 'handleMenuHover', 'scheduleOpenHoveredMenu', 'cancelScheduledOpen'];
|
||
|
||
/**
|
||
* @ngInject
|
||
*/
|
||
function MenuBarController($scope, $rootScope, $element, $attrs, $mdConstant, $document, $mdUtil, $timeout) {
|
||
this.$element = $element;
|
||
this.$attrs = $attrs;
|
||
this.$mdConstant = $mdConstant;
|
||
this.$mdUtil = $mdUtil;
|
||
this.$document = $document;
|
||
this.$scope = $scope;
|
||
this.$rootScope = $rootScope;
|
||
this.$timeout = $timeout;
|
||
|
||
var self = this;
|
||
angular.forEach(BOUND_MENU_METHODS, function(methodName) {
|
||
self[methodName] = angular.bind(self, self[methodName]);
|
||
});
|
||
}
|
||
|
||
MenuBarController.prototype.init = function() {
|
||
var $element = this.$element;
|
||
var $mdUtil = this.$mdUtil;
|
||
var $scope = this.$scope;
|
||
|
||
var self = this;
|
||
var deregisterFns = [];
|
||
$element.on('keydown', this.handleKeyDown);
|
||
this.parentToolbar = $mdUtil.getClosest($element, 'MD-TOOLBAR');
|
||
|
||
deregisterFns.push(this.$rootScope.$on('$mdMenuOpen', function(event, el) {
|
||
if (self.getMenus().indexOf(el[0]) != -1) {
|
||
$element[0].classList.add('md-open');
|
||
el[0].classList.add('md-open');
|
||
self.currentlyOpenMenu = el.controller('mdMenu');
|
||
self.currentlyOpenMenu.registerContainerProxy(self.handleKeyDown);
|
||
self.enableOpenOnHover();
|
||
}
|
||
}));
|
||
|
||
deregisterFns.push(this.$rootScope.$on('$mdMenuClose', function(event, el, opts) {
|
||
var rootMenus = self.getMenus();
|
||
if (rootMenus.indexOf(el[0]) != -1) {
|
||
$element[0].classList.remove('md-open');
|
||
el[0].classList.remove('md-open');
|
||
}
|
||
|
||
var ctrl = angular.element(el[0]).controller('mdMenu');
|
||
if (ctrl.isInMenuBar && ctrl.mdMenuBarCtrl === self) {
|
||
var parentMenu = el[0];
|
||
while (parentMenu && rootMenus.indexOf(parentMenu) == -1) {
|
||
parentMenu = $mdUtil.getClosest(parentMenu, 'MD-MENU', true);
|
||
}
|
||
if (parentMenu) {
|
||
if (!opts.skipFocus) parentMenu.querySelector('button:not([disabled])').focus();
|
||
self.currentlyOpenMenu = undefined;
|
||
}
|
||
self.disableOpenOnHover();
|
||
self.setKeyboardMode(true);
|
||
}
|
||
}));
|
||
|
||
$scope.$on('$destroy', function() {
|
||
self.disableOpenOnHover();
|
||
while (deregisterFns.length) {
|
||
deregisterFns.shift()();
|
||
}
|
||
});
|
||
|
||
|
||
this.setKeyboardMode(true);
|
||
};
|
||
|
||
MenuBarController.prototype.setKeyboardMode = function(enabled) {
|
||
if (enabled) this.$element[0].classList.add('md-keyboard-mode');
|
||
else this.$element[0].classList.remove('md-keyboard-mode');
|
||
};
|
||
|
||
MenuBarController.prototype.enableOpenOnHover = function() {
|
||
if (this.openOnHoverEnabled) return;
|
||
|
||
var self = this;
|
||
|
||
self.openOnHoverEnabled = true;
|
||
|
||
if (self.parentToolbar) {
|
||
self.parentToolbar.classList.add('md-has-open-menu');
|
||
|
||
// Needs to be on the next tick so it doesn't close immediately.
|
||
self.$mdUtil.nextTick(function() {
|
||
angular.element(self.parentToolbar).on('click', self.handleParentClick);
|
||
}, false);
|
||
}
|
||
|
||
angular
|
||
.element(self.getMenus())
|
||
.on('mouseenter', self.handleMenuHover);
|
||
};
|
||
|
||
MenuBarController.prototype.handleMenuHover = function(e) {
|
||
this.setKeyboardMode(false);
|
||
if (this.openOnHoverEnabled) {
|
||
this.scheduleOpenHoveredMenu(e);
|
||
}
|
||
};
|
||
|
||
MenuBarController.prototype.disableOpenOnHover = function() {
|
||
if (!this.openOnHoverEnabled) return;
|
||
|
||
this.openOnHoverEnabled = false;
|
||
|
||
if (this.parentToolbar) {
|
||
this.parentToolbar.classList.remove('md-has-open-menu');
|
||
angular.element(this.parentToolbar).off('click', this.handleParentClick);
|
||
}
|
||
|
||
angular
|
||
.element(this.getMenus())
|
||
.off('mouseenter', this.handleMenuHover);
|
||
};
|
||
|
||
MenuBarController.prototype.scheduleOpenHoveredMenu = function(e) {
|
||
var menuEl = angular.element(e.currentTarget);
|
||
var menuCtrl = menuEl.controller('mdMenu');
|
||
this.setKeyboardMode(false);
|
||
this.scheduleOpenMenu(menuCtrl);
|
||
};
|
||
|
||
MenuBarController.prototype.scheduleOpenMenu = function(menuCtrl) {
|
||
var self = this;
|
||
var $timeout = this.$timeout;
|
||
if (menuCtrl != self.currentlyOpenMenu) {
|
||
$timeout.cancel(self.pendingMenuOpen);
|
||
self.pendingMenuOpen = $timeout(function() {
|
||
self.pendingMenuOpen = undefined;
|
||
if (self.currentlyOpenMenu) {
|
||
self.currentlyOpenMenu.close(true, { closeAll: true });
|
||
}
|
||
menuCtrl.open();
|
||
}, 200, false);
|
||
}
|
||
};
|
||
|
||
MenuBarController.prototype.handleKeyDown = function(e) {
|
||
var keyCodes = this.$mdConstant.KEY_CODE;
|
||
var currentMenu = this.currentlyOpenMenu;
|
||
var wasOpen = currentMenu && currentMenu.isOpen;
|
||
this.setKeyboardMode(true);
|
||
var handled, newMenu, newMenuCtrl;
|
||
switch (e.keyCode) {
|
||
case keyCodes.DOWN_ARROW:
|
||
if (currentMenu) {
|
||
currentMenu.focusMenuContainer();
|
||
} else {
|
||
this.openFocusedMenu();
|
||
}
|
||
handled = true;
|
||
break;
|
||
case keyCodes.UP_ARROW:
|
||
currentMenu && currentMenu.close();
|
||
handled = true;
|
||
break;
|
||
case keyCodes.LEFT_ARROW:
|
||
newMenu = this.focusMenu(-1);
|
||
if (wasOpen) {
|
||
newMenuCtrl = angular.element(newMenu).controller('mdMenu');
|
||
this.scheduleOpenMenu(newMenuCtrl);
|
||
}
|
||
handled = true;
|
||
break;
|
||
case keyCodes.RIGHT_ARROW:
|
||
newMenu = this.focusMenu(+1);
|
||
if (wasOpen) {
|
||
newMenuCtrl = angular.element(newMenu).controller('mdMenu');
|
||
this.scheduleOpenMenu(newMenuCtrl);
|
||
}
|
||
handled = true;
|
||
break;
|
||
}
|
||
if (handled) {
|
||
e && e.preventDefault && e.preventDefault();
|
||
e && e.stopImmediatePropagation && e.stopImmediatePropagation();
|
||
}
|
||
};
|
||
|
||
MenuBarController.prototype.focusMenu = function(direction) {
|
||
var menus = this.getMenus();
|
||
var focusedIndex = this.getFocusedMenuIndex();
|
||
|
||
if (focusedIndex == -1) { focusedIndex = this.getOpenMenuIndex(); }
|
||
|
||
var changed = false;
|
||
|
||
if (focusedIndex == -1) { focusedIndex = 0; changed = true; }
|
||
else if (
|
||
direction < 0 && focusedIndex > 0 ||
|
||
direction > 0 && focusedIndex < menus.length - direction
|
||
) {
|
||
focusedIndex += direction;
|
||
changed = true;
|
||
}
|
||
if (changed) {
|
||
menus[focusedIndex].querySelector('button').focus();
|
||
return menus[focusedIndex];
|
||
}
|
||
};
|
||
|
||
MenuBarController.prototype.openFocusedMenu = function() {
|
||
var menu = this.getFocusedMenu();
|
||
menu && angular.element(menu).controller('mdMenu').open();
|
||
};
|
||
|
||
MenuBarController.prototype.getMenus = function() {
|
||
var $element = this.$element;
|
||
return this.$mdUtil.nodesToArray($element[0].children)
|
||
.filter(function(el) { return el.nodeName == 'MD-MENU'; });
|
||
};
|
||
|
||
MenuBarController.prototype.getFocusedMenu = function() {
|
||
return this.getMenus()[this.getFocusedMenuIndex()];
|
||
};
|
||
|
||
MenuBarController.prototype.getFocusedMenuIndex = function() {
|
||
var $mdUtil = this.$mdUtil;
|
||
var focusedEl = $mdUtil.getClosest(
|
||
this.$document[0].activeElement,
|
||
'MD-MENU'
|
||
);
|
||
if (!focusedEl) return -1;
|
||
|
||
var focusedIndex = this.getMenus().indexOf(focusedEl);
|
||
return focusedIndex;
|
||
};
|
||
|
||
MenuBarController.prototype.getOpenMenuIndex = function() {
|
||
var menus = this.getMenus();
|
||
for (var i = 0; i < menus.length; ++i) {
|
||
if (menus[i].classList.contains('md-open')) return i;
|
||
}
|
||
return -1;
|
||
};
|
||
|
||
MenuBarController.prototype.handleParentClick = function(event) {
|
||
var openMenu = this.querySelector('md-menu.md-open');
|
||
|
||
if (openMenu && !openMenu.contains(event.target)) {
|
||
angular.element(openMenu).controller('mdMenu').close(true, {
|
||
closeAll: true
|
||
});
|
||
}
|
||
};
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdMenuBar
|
||
* @module material.components.menuBar
|
||
* @restrict E
|
||
* @description
|
||
*
|
||
* Menu bars are containers that hold multiple menus. They change the behavior and appearance
|
||
* of the `md-menu` directive to behave similar to an operating system provided menu.
|
||
*
|
||
* @usage
|
||
* <hljs lang="html">
|
||
* <md-menu-bar>
|
||
* <md-menu>
|
||
* <button ng-click="$mdMenu.open()">
|
||
* File
|
||
* </button>
|
||
* <md-menu-content>
|
||
* <md-menu-item>
|
||
* <md-button ng-click="ctrl.sampleAction('share', $event)">
|
||
* Share...
|
||
* </md-button>
|
||
* </md-menu-item>
|
||
* <md-menu-divider></md-menu-divider>
|
||
* <md-menu-item>
|
||
* <md-menu-item>
|
||
* <md-menu>
|
||
* <md-button ng-click="$mdMenu.open()">New</md-button>
|
||
* <md-menu-content>
|
||
* <md-menu-item><md-button ng-click="ctrl.sampleAction('New Document', $event)">Document</md-button></md-menu-item>
|
||
* <md-menu-item><md-button ng-click="ctrl.sampleAction('New Spreadsheet', $event)">Spreadsheet</md-button></md-menu-item>
|
||
* <md-menu-item><md-button ng-click="ctrl.sampleAction('New Presentation', $event)">Presentation</md-button></md-menu-item>
|
||
* <md-menu-item><md-button ng-click="ctrl.sampleAction('New Form', $event)">Form</md-button></md-menu-item>
|
||
* <md-menu-item><md-button ng-click="ctrl.sampleAction('New Drawing', $event)">Drawing</md-button></md-menu-item>
|
||
* </md-menu-content>
|
||
* </md-menu>
|
||
* </md-menu-item>
|
||
* </md-menu-content>
|
||
* </md-menu>
|
||
* </md-menu-bar>
|
||
* </hljs>
|
||
*
|
||
* ## Menu Bar Controls
|
||
*
|
||
* You may place `md-menu-item`s that function as controls within menu bars.
|
||
* There are two modes that are exposed via the `type` attribute of the `md-menu-item`.
|
||
* `type="checkbox"` will function as a boolean control for the `ng-model` attribute of the
|
||
* `md-menu-item`. `type="radio"` will function like a radio button, setting the `ngModel`
|
||
* to the `string` value of the `value` attribute. If you need non-string values, you can use
|
||
* `ng-value` to provide an expression (this is similar to how angular's native `input[type=radio]`
|
||
* works.
|
||
*
|
||
* If you want either to disable closing the opened menu when clicked, you can add the
|
||
* `md-prevent-menu-close` attribute to the `md-menu-item`. The attribute will be forwarded to the
|
||
* `button` element that is generated.
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-menu-bar>
|
||
* <md-menu>
|
||
* <button ng-click="$mdMenu.open()">
|
||
* Sample Menu
|
||
* </button>
|
||
* <md-menu-content>
|
||
* <md-menu-item type="checkbox" ng-model="settings.allowChanges" md-prevent-menu-close>
|
||
* Allow changes
|
||
* </md-menu-item>
|
||
* <md-menu-divider></md-menu-divider>
|
||
* <md-menu-item type="radio" ng-model="settings.mode" ng-value="1">Mode 1</md-menu-item>
|
||
* <md-menu-item type="radio" ng-model="settings.mode" ng-value="2">Mode 2</md-menu-item>
|
||
* <md-menu-item type="radio" ng-model="settings.mode" ng-value="3">Mode 3</md-menu-item>
|
||
* </md-menu-content>
|
||
* </md-menu>
|
||
* </md-menu-bar>
|
||
* </hljs>
|
||
*
|
||
*
|
||
* ### Nesting Menus
|
||
*
|
||
* Menus may be nested within menu bars. This is commonly called cascading menus.
|
||
* To nest a menu place the nested menu inside the content of the `md-menu-item`.
|
||
* <hljs lang="html">
|
||
* <md-menu-item>
|
||
* <md-menu>
|
||
* <button ng-click="$mdMenu.open()">New</md-button>
|
||
* <md-menu-content>
|
||
* <md-menu-item><md-button ng-click="ctrl.sampleAction('New Document', $event)">Document</md-button></md-menu-item>
|
||
* <md-menu-item><md-button ng-click="ctrl.sampleAction('New Spreadsheet', $event)">Spreadsheet</md-button></md-menu-item>
|
||
* <md-menu-item><md-button ng-click="ctrl.sampleAction('New Presentation', $event)">Presentation</md-button></md-menu-item>
|
||
* <md-menu-item><md-button ng-click="ctrl.sampleAction('New Form', $event)">Form</md-button></md-menu-item>
|
||
* <md-menu-item><md-button ng-click="ctrl.sampleAction('New Drawing', $event)">Drawing</md-button></md-menu-item>
|
||
* </md-menu-content>
|
||
* </md-menu>
|
||
* </md-menu-item>
|
||
* </hljs>
|
||
*
|
||
*/
|
||
|
||
MenuBarDirective.$inject = ["$mdUtil", "$mdTheming"];
|
||
angular
|
||
.module('material.components.menuBar')
|
||
.directive('mdMenuBar', MenuBarDirective);
|
||
|
||
/* @ngInject */
|
||
function MenuBarDirective($mdUtil, $mdTheming) {
|
||
return {
|
||
restrict: 'E',
|
||
require: 'mdMenuBar',
|
||
controller: 'MenuBarController',
|
||
|
||
compile: function compile(templateEl, templateAttrs) {
|
||
if (!templateAttrs.ariaRole) {
|
||
templateEl[0].setAttribute('role', 'menubar');
|
||
}
|
||
angular.forEach(templateEl[0].children, function(menuEl) {
|
||
if (menuEl.nodeName == 'MD-MENU') {
|
||
if (!menuEl.hasAttribute('md-position-mode')) {
|
||
menuEl.setAttribute('md-position-mode', 'left bottom');
|
||
|
||
// Since we're in the compile function and actual `md-buttons` are not compiled yet,
|
||
// we need to query for possible `md-buttons` as well.
|
||
menuEl.querySelector('button, a, md-button').setAttribute('role', 'menuitem');
|
||
}
|
||
var contentEls = $mdUtil.nodesToArray(menuEl.querySelectorAll('md-menu-content'));
|
||
angular.forEach(contentEls, function(contentEl) {
|
||
contentEl.classList.add('md-menu-bar-menu');
|
||
contentEl.classList.add('md-dense');
|
||
if (!contentEl.hasAttribute('width')) {
|
||
contentEl.setAttribute('width', 5);
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
// Mark the child menu items that they're inside a menu bar. This is necessary,
|
||
// because mnMenuItem has special behaviour during compilation, depending on
|
||
// whether it is inside a mdMenuBar. We can usually figure this out via the DOM,
|
||
// however if a directive that uses documentFragment is applied to the child (e.g. ngRepeat),
|
||
// the element won't have a parent and won't compile properly.
|
||
templateEl.find('md-menu-item').addClass('md-in-menu-bar');
|
||
|
||
return function postLink(scope, el, attr, ctrl) {
|
||
el.addClass('_md'); // private md component indicator for styling
|
||
$mdTheming(scope, el);
|
||
ctrl.init();
|
||
};
|
||
}
|
||
};
|
||
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
|
||
angular
|
||
.module('material.components.menuBar')
|
||
.directive('mdMenuDivider', MenuDividerDirective);
|
||
|
||
|
||
function MenuDividerDirective() {
|
||
return {
|
||
restrict: 'E',
|
||
compile: function(templateEl, templateAttrs) {
|
||
if (!templateAttrs.role) {
|
||
templateEl[0].setAttribute('role', 'separator');
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
|
||
MenuItemController.$inject = ["$scope", "$element", "$attrs"];
|
||
angular
|
||
.module('material.components.menuBar')
|
||
.controller('MenuItemController', MenuItemController);
|
||
|
||
|
||
/**
|
||
* @ngInject
|
||
*/
|
||
function MenuItemController($scope, $element, $attrs) {
|
||
this.$element = $element;
|
||
this.$attrs = $attrs;
|
||
this.$scope = $scope;
|
||
}
|
||
|
||
MenuItemController.prototype.init = function(ngModel) {
|
||
var $element = this.$element;
|
||
var $attrs = this.$attrs;
|
||
|
||
this.ngModel = ngModel;
|
||
if ($attrs.type == 'checkbox' || $attrs.type == 'radio') {
|
||
this.mode = $attrs.type;
|
||
this.iconEl = $element[0].children[0];
|
||
this.buttonEl = $element[0].children[1];
|
||
if (ngModel) {
|
||
// Clear ngAria set attributes
|
||
this.initClickListeners();
|
||
}
|
||
}
|
||
};
|
||
|
||
// ngAria auto sets attributes on a menu-item with a ngModel.
|
||
// We don't want this because our content (buttons) get the focus
|
||
// and set their own aria attributes appropritately. Having both
|
||
// breaks NVDA / JAWS. This undeoes ngAria's attrs.
|
||
MenuItemController.prototype.clearNgAria = function() {
|
||
var el = this.$element[0];
|
||
var clearAttrs = ['role', 'tabindex', 'aria-invalid', 'aria-checked'];
|
||
angular.forEach(clearAttrs, function(attr) {
|
||
el.removeAttribute(attr);
|
||
});
|
||
};
|
||
|
||
MenuItemController.prototype.initClickListeners = function() {
|
||
var self = this;
|
||
var ngModel = this.ngModel;
|
||
var $scope = this.$scope;
|
||
var $attrs = this.$attrs;
|
||
var $element = this.$element;
|
||
var mode = this.mode;
|
||
|
||
this.handleClick = angular.bind(this, this.handleClick);
|
||
|
||
var icon = this.iconEl;
|
||
var button = angular.element(this.buttonEl);
|
||
var handleClick = this.handleClick;
|
||
|
||
$attrs.$observe('disabled', setDisabled);
|
||
setDisabled($attrs.disabled);
|
||
|
||
ngModel.$render = function render() {
|
||
self.clearNgAria();
|
||
if (isSelected()) {
|
||
icon.style.display = '';
|
||
button.attr('aria-checked', 'true');
|
||
} else {
|
||
icon.style.display = 'none';
|
||
button.attr('aria-checked', 'false');
|
||
}
|
||
};
|
||
|
||
$scope.$$postDigest(ngModel.$render);
|
||
|
||
function isSelected() {
|
||
if (mode == 'radio') {
|
||
var val = $attrs.ngValue ? $scope.$eval($attrs.ngValue) : $attrs.value;
|
||
return ngModel.$modelValue == val;
|
||
} else {
|
||
return ngModel.$modelValue;
|
||
}
|
||
}
|
||
|
||
function setDisabled(disabled) {
|
||
if (disabled) {
|
||
button.off('click', handleClick);
|
||
} else {
|
||
button.on('click', handleClick);
|
||
}
|
||
}
|
||
};
|
||
|
||
MenuItemController.prototype.handleClick = function(e) {
|
||
var mode = this.mode;
|
||
var ngModel = this.ngModel;
|
||
var $attrs = this.$attrs;
|
||
var newVal;
|
||
if (mode == 'checkbox') {
|
||
newVal = !ngModel.$modelValue;
|
||
} else if (mode == 'radio') {
|
||
newVal = $attrs.ngValue ? this.$scope.$eval($attrs.ngValue) : $attrs.value;
|
||
}
|
||
ngModel.$setViewValue(newVal);
|
||
ngModel.$render();
|
||
};
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
|
||
MenuItemDirective.$inject = ["$mdUtil", "$mdConstant", "$$mdSvgRegistry"];
|
||
angular
|
||
.module('material.components.menuBar')
|
||
.directive('mdMenuItem', MenuItemDirective);
|
||
|
||
/* @ngInject */
|
||
function MenuItemDirective($mdUtil, $mdConstant, $$mdSvgRegistry) {
|
||
return {
|
||
controller: 'MenuItemController',
|
||
require: ['mdMenuItem', '?ngModel'],
|
||
priority: $mdConstant.BEFORE_NG_ARIA,
|
||
compile: function(templateEl, templateAttrs) {
|
||
var type = templateAttrs.type;
|
||
var inMenuBarClass = 'md-in-menu-bar';
|
||
|
||
// Note: This allows us to show the `check` icon for the md-menu-bar items.
|
||
// The `md-in-menu-bar` class is set by the mdMenuBar directive.
|
||
if ((type === 'checkbox' || type === 'radio') && templateEl.hasClass(inMenuBarClass)) {
|
||
var text = templateEl[0].textContent;
|
||
var buttonEl = angular.element('<md-button type="button"></md-button>');
|
||
var iconTemplate = '<md-icon md-svg-src="' + $$mdSvgRegistry.mdChecked + '"></md-icon>';
|
||
|
||
buttonEl.html(text);
|
||
buttonEl.attr('tabindex', '0');
|
||
|
||
if (angular.isDefined(templateAttrs.mdPreventMenuClose)) {
|
||
buttonEl.attr('md-prevent-menu-close', templateAttrs.mdPreventMenuClose);
|
||
}
|
||
|
||
templateEl.html('');
|
||
templateEl.append(angular.element(iconTemplate));
|
||
templateEl.append(buttonEl);
|
||
templateEl.addClass('md-indent').removeClass(inMenuBarClass);
|
||
|
||
setDefault('role', type === 'checkbox' ? 'menuitemcheckbox' : 'menuitemradio', buttonEl);
|
||
moveAttrToButton('ng-disabled');
|
||
|
||
} else {
|
||
setDefault('role', 'menuitem', templateEl[0].querySelector('md-button, button, a'));
|
||
}
|
||
|
||
|
||
return function(scope, el, attrs, ctrls) {
|
||
var ctrl = ctrls[0];
|
||
var ngModel = ctrls[1];
|
||
ctrl.init(ngModel);
|
||
};
|
||
|
||
function setDefault(attr, val, el) {
|
||
el = el || templateEl;
|
||
if (el instanceof angular.element) {
|
||
el = el[0];
|
||
}
|
||
if (!el.hasAttribute(attr)) {
|
||
el.setAttribute(attr, val);
|
||
}
|
||
}
|
||
|
||
function moveAttrToButton(attribute) {
|
||
var attributes = $mdUtil.prefixer(attribute);
|
||
|
||
angular.forEach(attributes, function(attr) {
|
||
if (templateEl[0].hasAttribute(attr)) {
|
||
var val = templateEl[0].getAttribute(attr);
|
||
buttonEl[0].setAttribute(attr, val);
|
||
templateEl[0].removeAttribute(attr);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.navBar
|
||
*/
|
||
MdNavBar.$inject = ["$mdAria", "$mdTheming", "$window", "$mdUtil"];
|
||
MdNavBarController.$inject = ["$element", "$scope", "$timeout", "$mdConstant"];
|
||
MdNavItem.$inject = ["$mdAria", "$$rAF", "$mdUtil", "$window"];
|
||
MdNavItemController.$inject = ["$element"];
|
||
angular.module('material.components.navBar', ['material.core'])
|
||
.controller('MdNavBarController', MdNavBarController)
|
||
.directive('mdNavBar', MdNavBar)
|
||
.controller('MdNavItemController', MdNavItemController)
|
||
.directive('mdNavItem', MdNavItem);
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdNavBar
|
||
* @module material.components.navBar
|
||
*
|
||
* @restrict E
|
||
*
|
||
* @description
|
||
* The `<md-nav-bar>` directive renders a list of material tabs that can be used
|
||
* for top-level page navigation. Unlike `<md-tabs>`, 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 `ng-href`, `ui-sref`, or `ng-click` events are supported.
|
||
* Alternatively, the user could simply watch the value of `md-selected-nav-item`
|
||
* (`currentNavItem` in the below example) for changes.
|
||
*
|
||
* Accessibility functionality is implemented as a
|
||
* <a href="https://www.w3.org/TR/wai-aria-1.0/complete#tablist">
|
||
* tablist</a> with
|
||
* <a href="https://www.w3.org/TR/wai-aria-1.0/complete#tab">tabs</a>.
|
||
* We've kept the `role="navigation"` on the `<nav>`, for backwards compatibility, even though
|
||
* it is not required in the
|
||
* <a href="https://www.w3.org/TR/wai-aria-practices/#aria_lh_navigation">
|
||
* latest Working Group Note from December 2017</a>.
|
||
*
|
||
* <h3>Keyboard Navigation</h3>
|
||
* - `Tab`/`Shift+Tab` moves the focus to the next/previous interactive element on the page
|
||
* - `Enter`/`Space` selects the focused nav item and navigates to display the item's contents
|
||
* - `Right`/`Down` moves focus to the next nav item, wrapping at the end
|
||
* - `Left`/`Up` moves focus to the previous nav item, wrapping at the end
|
||
* - `Home`/`End` moves the focus to the first/last nav item
|
||
*
|
||
* @param {string=} md-selected-nav-item The name of the current tab; this must
|
||
* match the `name` attribute of `<md-nav-item>`.
|
||
* @param {boolean=} md-no-ink-bar If set to true, the ink bar will be hidden.
|
||
* @param {string=} nav-bar-aria-label An `aria-label` applied to the `md-nav-bar`'s tablist
|
||
* for accessibility.
|
||
*
|
||
* @usage
|
||
* <hljs lang="html">
|
||
* <md-nav-bar md-selected-nav-item="currentNavItem">
|
||
* <md-nav-item md-nav-click="goto('page1')" name="page1">
|
||
* Page One
|
||
* </md-nav-item>
|
||
* <md-nav-item md-nav-href="#page2" name="page3">Page Two</md-nav-item>
|
||
* <md-nav-item md-nav-sref="page3" name="page2">Page Three</md-nav-item>
|
||
* <md-nav-item
|
||
* md-nav-sref="app.page4"
|
||
* sref-opts="{reload: true, notify: true}"
|
||
* name="page4">
|
||
* Page Four
|
||
* </md-nav-item>
|
||
* </md-nav-bar>
|
||
*</hljs>
|
||
* <hljs lang="js">
|
||
* (function() {
|
||
* 'use strict';
|
||
*
|
||
* $rootScope.$on('$routeChangeSuccess', function(event, current) {
|
||
* $scope.currentLink = getCurrentLinkFromRoute(current);
|
||
* });
|
||
* });
|
||
* </hljs>
|
||
*/
|
||
/**
|
||
* @param $mdAria
|
||
* @param $mdTheming
|
||
* @param $window
|
||
* @param $mdUtil
|
||
* @constructor
|
||
* @ngInject
|
||
*/
|
||
function MdNavBar($mdAria, $mdTheming, $window, $mdUtil) {
|
||
return {
|
||
restrict: 'E',
|
||
transclude: true,
|
||
controller: MdNavBarController,
|
||
controllerAs: 'ctrl',
|
||
bindToController: true,
|
||
scope: {
|
||
'mdSelectedNavItem': '=?',
|
||
'mdNoInkBar': '=?',
|
||
'navBarAriaLabel': '@?',
|
||
},
|
||
template:
|
||
'<div class="md-nav-bar">' +
|
||
'<nav role="navigation">' +
|
||
'<ul class="_md-nav-bar-list" ng-transclude role="tablist" ' +
|
||
'ng-focus="ctrl.onFocus()" ' + // Deprecated but kept for now in order to not break tests
|
||
'aria-label="{{ctrl.navBarAriaLabel}}">' +
|
||
'</ul>' +
|
||
'</nav>' +
|
||
'<md-nav-ink-bar ng-hide="ctrl.mdNoInkBar"></md-nav-ink-bar>' +
|
||
'</div>',
|
||
link: function(scope, element, attrs, ctrl) {
|
||
|
||
ctrl.width = $window.innerWidth;
|
||
|
||
function onResize() {
|
||
if (ctrl.width !== $window.innerWidth) {
|
||
ctrl.updateSelectedTabInkBar();
|
||
ctrl.width = $window.innerWidth;
|
||
scope.$digest();
|
||
}
|
||
}
|
||
|
||
function cleanUp() {
|
||
angular.element($window).off('resize', onResize);
|
||
}
|
||
|
||
angular.element($window).on('resize', $mdUtil.debounce(onResize, 300));
|
||
scope.$on('$destroy', cleanUp);
|
||
|
||
$mdTheming(element);
|
||
if (!ctrl.navBarAriaLabel) {
|
||
$mdAria.expectAsync(element, 'aria-label', angular.noop);
|
||
}
|
||
},
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Controller for the nav-bar component.
|
||
* Accessibility functionality is implemented as a tablist
|
||
* (https://www.w3.org/TR/wai-aria-1.0/complete#tablist) and
|
||
* tabs (https://www.w3.org/TR/wai-aria-1.0/complete#tab).
|
||
*
|
||
* @param {!JQLite} $element
|
||
* @param {!IScope} $scope
|
||
* @param {!ITimeoutService} $timeout
|
||
* @param {!Object} $mdConstant
|
||
* @constructor
|
||
* @final
|
||
* @ngInject
|
||
*/
|
||
function MdNavBarController($element, $scope, $timeout, $mdConstant) {
|
||
// Injected variables
|
||
/**
|
||
* @private @const
|
||
* @type {!ITimeoutService}
|
||
*/
|
||
this._$timeout = $timeout;
|
||
|
||
/**
|
||
* @private @const
|
||
* @type {!IScope}
|
||
*/
|
||
this._$scope = $scope;
|
||
|
||
/**
|
||
* @private @const
|
||
* @type {!Object}
|
||
*/
|
||
this._$mdConstant = $mdConstant;
|
||
|
||
// Data-bound variables.
|
||
/** @type {?string} */
|
||
this.mdSelectedNavItem;
|
||
|
||
/** @type {?string} */
|
||
this.navBarAriaLabel;
|
||
|
||
// State variables.
|
||
/** @type {?HTMLElement} */
|
||
this._navBarEl = $element[0];
|
||
|
||
/** @type {?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();
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Initializes the tab components once they exist.
|
||
* @private
|
||
*/
|
||
MdNavBarController.prototype._initTabs = function() {
|
||
this._inkbar = angular.element(this._navBarEl.querySelector('md-nav-ink-bar'));
|
||
|
||
var self = this;
|
||
this._$timeout(function() {
|
||
self._updateTabs(self.mdSelectedNavItem, null);
|
||
});
|
||
|
||
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);
|
||
});
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Set the current tab to be selected.
|
||
* @param {string|undefined} newValue New current tab name.
|
||
* @param {string|undefined|null} oldValue Previous tab name.
|
||
* @private
|
||
*/
|
||
MdNavBarController.prototype._updateTabs = function(newValue, oldValue) {
|
||
var self = this;
|
||
var tabs = this._getTabs();
|
||
var sameTab = newValue === oldValue;
|
||
|
||
// this._getTabs can return null if nav-bar has not yet been initialized
|
||
if (!tabs) return;
|
||
|
||
var newIndex = -1;
|
||
var newTab = this._getTabByName(newValue);
|
||
var oldTab = this._getTabByName(oldValue);
|
||
|
||
if (oldTab) {
|
||
oldTab.setSelected(false);
|
||
}
|
||
|
||
if (newTab) {
|
||
newTab.setSelected(true);
|
||
newIndex = tabs.indexOf(newTab);
|
||
}
|
||
|
||
this._$timeout(function() {
|
||
self._updateInkBarStyles(newTab, newIndex);
|
||
// Don't change focus when there is no newTab, the new and old tabs are the same, or when
|
||
// called from MdNavBarController._initTabs() which would have no oldTab defined.
|
||
if (newTab && oldTab && !sameTab) {
|
||
self._moveFocus(oldTab, newTab);
|
||
}
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Repositions the ink bar to the selected tab.
|
||
* @param {MdNavItemController} tab the nav item that should have ink bar styles applied
|
||
* @param {number=} newIndex the index of the newly selected nav item
|
||
* @private
|
||
*/
|
||
MdNavBarController.prototype._updateInkBarStyles = function(tab, newIndex) {
|
||
this._inkbar.css({display: newIndex < 0 ? 'none' : ''});
|
||
|
||
if (tab) {
|
||
var tabEl = tab.getButtonEl();
|
||
var left = tabEl.offsetLeft;
|
||
var tabWidth = tabEl.offsetWidth;
|
||
var navBarWidth = this._navBarEl.getBoundingClientRect().width;
|
||
var scale = tabWidth / navBarWidth;
|
||
var translate = left / navBarWidth * 100;
|
||
|
||
this._inkbar.css({ transform: 'translateX(' + translate + '%) scaleX(' + scale + ')' });
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Updates ink bar to match current tab.
|
||
*/
|
||
MdNavBarController.prototype.updateSelectedTabInkBar = function() {
|
||
this._updateInkBarStyles(this._getSelectedTab());
|
||
};
|
||
|
||
/**
|
||
* Returns an array of the current tabs.
|
||
* @return {Array<!MdNavItemController>}
|
||
* @private
|
||
*/
|
||
MdNavBarController.prototype._getTabs = function() {
|
||
var controllers = Array.prototype.slice.call(
|
||
this._navBarEl.querySelectorAll('.md-nav-item'))
|
||
.map(function(el) {
|
||
return angular.element(el).controller('mdNavItem');
|
||
});
|
||
return controllers.indexOf(undefined) ? controllers : [];
|
||
};
|
||
|
||
/**
|
||
* Returns the tab with the specified name.
|
||
* @param {string} name The name of the tab, found in its name attribute.
|
||
* @return {MdNavItemController}
|
||
* @private
|
||
*/
|
||
MdNavBarController.prototype._getTabByName = function(name) {
|
||
return this._findTab(function(tab) {
|
||
return tab.getName() === name;
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Returns the selected tab.
|
||
* @return {MdNavItemController}
|
||
* @private
|
||
*/
|
||
MdNavBarController.prototype._getSelectedTab = function() {
|
||
return this._findTab(function(tab) {
|
||
return tab.isSelected();
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Returns the focused tab.
|
||
* @return {MdNavItemController}
|
||
*/
|
||
MdNavBarController.prototype.getFocusedTab = function() {
|
||
return this._findTab(function(tab) {
|
||
return tab.hasFocus();
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Find a tab that matches the specified function, starting from the first tab.
|
||
* @param {Function} fn
|
||
* @param {number=} startIndex index to start at. Defaults to 0.
|
||
* @returns {MdNavItemController|null}
|
||
* @private
|
||
*/
|
||
MdNavBarController.prototype._findTab = function(fn, startIndex) {
|
||
var tabs = this._getTabs(), i;
|
||
if (startIndex == null) {
|
||
startIndex = 0;
|
||
}
|
||
for (i = startIndex; i < tabs.length; i++) {
|
||
if (fn(tabs[i])) {
|
||
return tabs[i];
|
||
}
|
||
}
|
||
return null;
|
||
};
|
||
|
||
/**
|
||
* Find a tab that matches the specified function, going backwards from the end of the list.
|
||
* @param {Function} fn
|
||
* @param {number=} startIndex index to start at. Defaults to tabs.length - 1.
|
||
* @returns {MdNavItemController}
|
||
* @private
|
||
*/
|
||
MdNavBarController.prototype._findTabReverse = function(fn, startIndex) {
|
||
var tabs = this._getTabs();
|
||
if (startIndex === undefined || startIndex === null) {
|
||
startIndex = tabs.length - 1;
|
||
}
|
||
for (var i = startIndex; i >= 0 ; i--) {
|
||
if (fn(tabs[i])) {
|
||
return tabs[i];
|
||
}
|
||
}
|
||
return null;
|
||
};
|
||
|
||
/**
|
||
* Direct focus to the selected tab when focus enters the nav bar.
|
||
*/
|
||
MdNavBarController.prototype.onFocus = function() {
|
||
var tab = this._getSelectedTab();
|
||
if (tab && !tab.isFocused) {
|
||
tab.setFocused(true);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Move focus from oldTab to newTab.
|
||
* @param {!MdNavItemController} oldTab
|
||
* @param {!MdNavItemController} newTab
|
||
* @private
|
||
*/
|
||
MdNavBarController.prototype._moveFocus = function(oldTab, newTab) {
|
||
oldTab.setFocused(false);
|
||
newTab.setFocused(true);
|
||
};
|
||
|
||
/**
|
||
* Focus the first tab.
|
||
* @private
|
||
*/
|
||
MdNavBarController.prototype._focusFirstTab = function() {
|
||
var tabs = this._getTabs();
|
||
if (!tabs) return;
|
||
var tabToFocus = this._findTab(function(tab) {
|
||
return tab._isEnabled();
|
||
});
|
||
if (tabToFocus) {
|
||
this._moveFocus(this.getFocusedTab(), tabToFocus);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Focus the last tab.
|
||
* @private
|
||
*/
|
||
MdNavBarController.prototype._focusLastTab = function() {
|
||
var tabs = this._getTabs();
|
||
if (!tabs) return;
|
||
var tabToFocus = this._findTabReverse(function(tab) {
|
||
return tab._isEnabled();
|
||
});
|
||
if (tabToFocus) {
|
||
this._moveFocus(this.getFocusedTab(), tabToFocus);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Focus the next non-disabled tab.
|
||
* @param {number} focusedTabIndex the index of the currently focused tab
|
||
* @private
|
||
*/
|
||
MdNavBarController.prototype._focusNextTab = function(focusedTabIndex) {
|
||
var tabs = this._getTabs();
|
||
if (!tabs) return;
|
||
var tabToFocus = this._findTab(function(tab) {
|
||
return tab._isEnabled();
|
||
}, focusedTabIndex + 1);
|
||
if (tabToFocus) {
|
||
this._moveFocus(this.getFocusedTab(), tabToFocus);
|
||
} else {
|
||
this._focusFirstTab();
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Focus the previous non-disabled tab.
|
||
* @param {number} focusedTabIndex the index of the currently focused tab
|
||
* @private
|
||
*/
|
||
MdNavBarController.prototype._focusPreviousTab = function(focusedTabIndex) {
|
||
var tabs = this._getTabs();
|
||
if (!tabs) return;
|
||
var tabToFocus = this._findTabReverse(function(tab) {
|
||
return tab._isEnabled();
|
||
}, focusedTabIndex - 1);
|
||
if (tabToFocus) {
|
||
this._moveFocus(this.getFocusedTab(), tabToFocus);
|
||
} else {
|
||
this._focusLastTab();
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Responds to keydown events.
|
||
* Calls to preventDefault() stop the page from scrolling when changing focus in the nav-bar.
|
||
* @param {!KeyboardEvent} e
|
||
*/
|
||
MdNavBarController.prototype.onKeydown = function(e) {
|
||
var keyCodes = this._$mdConstant.KEY_CODE;
|
||
var tabs = this._getTabs();
|
||
var focusedTab = this.getFocusedTab();
|
||
if (!focusedTab || !tabs) return;
|
||
|
||
var focusedTabIndex = tabs.indexOf(focusedTab);
|
||
|
||
// use arrow keys to navigate between tabs
|
||
switch (e.keyCode) {
|
||
case keyCodes.UP_ARROW:
|
||
case keyCodes.LEFT_ARROW:
|
||
e.preventDefault();
|
||
this._focusPreviousTab(focusedTabIndex);
|
||
break;
|
||
case keyCodes.DOWN_ARROW:
|
||
case keyCodes.RIGHT_ARROW:
|
||
e.preventDefault();
|
||
this._focusNextTab(focusedTabIndex);
|
||
break;
|
||
case keyCodes.SPACE:
|
||
case keyCodes.ENTER:
|
||
// timeout to avoid a "digest already in progress" console error
|
||
this._$timeout(function() {
|
||
focusedTab.getButtonEl().click();
|
||
});
|
||
break;
|
||
case keyCodes.HOME:
|
||
e.preventDefault();
|
||
this._focusFirstTab();
|
||
break;
|
||
case keyCodes.END:
|
||
e.preventDefault();
|
||
this._focusLastTab();
|
||
break;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdNavItem
|
||
* @module material.components.navBar
|
||
*
|
||
* @restrict E
|
||
*
|
||
* @description
|
||
* `<md-nav-item>` describes a page navigation link within the `<md-nav-bar>` component.
|
||
* It renders an `<md-button>` as the actual link.
|
||
*
|
||
* Exactly one of the `md-nav-click`, `md-nav-href`, or `md-nav-sref` attributes are required
|
||
* to be specified.
|
||
*
|
||
* @param {string=} nav-item-aria-label Allows setting or overriding the label that is announced by
|
||
* a screen reader for the nav item's button. If this is not set, the nav item's transcluded
|
||
* content will be announced. Make sure to set this if the nav item's transcluded content does
|
||
* not include descriptive text, for example only an icon.
|
||
* @param {expression=} md-nav-click Expression which will be evaluated when the
|
||
* link is clicked to change the page. Renders as an `ng-click`.
|
||
* @param {string=} md-nav-href url to transition to when this link is clicked.
|
||
* Renders as an `ng-href`.
|
||
* @param {string=} md-nav-sref 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.
|
||
* @param {!object=} sref-opts UI-Router options that are passed to the `$state.go()` function. See
|
||
* the <a ng-href="https://ui-router.github.io/docs/latest/interfaces/transition.transitionoptions.html"
|
||
* target="_blank">UI-Router documentation for details</a>.
|
||
*
|
||
* @usage
|
||
* See <a ng-href="api/directive/mdNavBar">md-nav-bar</a> for usage.
|
||
*/
|
||
/**
|
||
* @param $mdAria
|
||
* @param $$rAF
|
||
* @param $mdUtil
|
||
* @param $window
|
||
* @constructor
|
||
* @ngInject
|
||
*/
|
||
function MdNavItem($mdAria, $$rAF, $mdUtil, $window) {
|
||
return {
|
||
restrict: 'E',
|
||
require: ['mdNavItem', '^mdNavBar'],
|
||
controller: MdNavItemController,
|
||
bindToController: true,
|
||
controllerAs: 'ctrl',
|
||
replace: true,
|
||
transclude: true,
|
||
template: function(tElement, tAttrs) {
|
||
var hasNavClick = tAttrs.mdNavClick;
|
||
var hasNavHref = tAttrs.mdNavHref;
|
||
var hasNavSref = tAttrs.mdNavSref;
|
||
var hasSrefOpts = tAttrs.srefOpts;
|
||
var navigationAttribute;
|
||
var navigationOptions;
|
||
var buttonTemplate;
|
||
|
||
// Cannot specify more than one nav attribute
|
||
if ((hasNavClick ? 1 : 0) + (hasNavHref ? 1 : 0) + (hasNavSref ? 1 : 0) > 1) {
|
||
throw Error(
|
||
'Please do not specify more than one of the md-nav-click, md-nav-href, ' +
|
||
'or md-nav-sref attributes per nav-item directive.'
|
||
);
|
||
}
|
||
|
||
if (hasNavClick !== undefined && hasNavClick !== null) {
|
||
navigationAttribute = 'ng-click="ctrl.mdNavClick()"';
|
||
} else if (hasNavHref !== undefined && hasNavHref !== null) {
|
||
navigationAttribute = 'ng-href="{{ctrl.mdNavHref}}"';
|
||
} else if (hasNavSref !== undefined && hasNavSref !== null) {
|
||
navigationAttribute = 'ui-sref="{{ctrl.mdNavSref}}"';
|
||
} else {
|
||
throw Error(
|
||
'Please specify at least one of the md-nav-click, md-nav-href, or md-nav-sref ' +
|
||
'attributes per nav-item directive.');
|
||
}
|
||
|
||
navigationOptions = hasSrefOpts ? 'ui-sref-opts="{{ctrl.srefOpts}}" ' : '';
|
||
|
||
if (navigationAttribute) {
|
||
buttonTemplate = '' +
|
||
'<md-button class="_md-nav-button md-accent" ' +
|
||
'ng-class="ctrl.getNgClassMap()" ' +
|
||
'ng-blur="ctrl.setFocused(false)" ' +
|
||
'ng-disabled="ctrl.disabled" ' +
|
||
'tabindex="-1" ' +
|
||
'role="tab" ' +
|
||
'ng-attr-aria-label="{{ctrl.navItemAriaLabel ? ctrl.navItemAriaLabel : undefined}}" ' +
|
||
'aria-selected="{{ctrl.isSelected()}}" ' +
|
||
navigationOptions +
|
||
navigationAttribute + '>' +
|
||
'<span ng-transclude class="_md-nav-button-text"></span>' +
|
||
'</md-button>';
|
||
}
|
||
|
||
return '' +
|
||
'<li class="md-nav-item" ' +
|
||
'role="presentation">' +
|
||
(buttonTemplate || '') +
|
||
'</li>';
|
||
},
|
||
scope: {
|
||
'mdNavClick': '&?',
|
||
'mdNavHref': '@?',
|
||
'mdNavSref': '@?',
|
||
'srefOpts': '=?',
|
||
'name': '@',
|
||
'navItemAriaLabel': '@?',
|
||
},
|
||
link: function(scope, element, attrs, controllers) {
|
||
var disconnect;
|
||
var mdNavItem;
|
||
var mdNavBar;
|
||
var navButton;
|
||
|
||
// 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() {
|
||
mdNavItem = controllers[0];
|
||
mdNavBar = controllers[1];
|
||
navButton = angular.element(element[0].querySelector('._md-nav-button'));
|
||
|
||
if (!mdNavItem.name) {
|
||
mdNavItem.name = angular.element(element[0]
|
||
.querySelector('._md-nav-button-text')).text().trim();
|
||
}
|
||
|
||
navButton.on('keydown', function($event) {
|
||
mdNavBar.onKeydown($event);
|
||
});
|
||
|
||
navButton.on('focus', function() {
|
||
mdNavItem._focused = true;
|
||
});
|
||
|
||
navButton.on('click', function() {
|
||
// This triggers a watcher on mdNavBar.mdSelectedNavItem which calls
|
||
// MdNavBarController._updateTabs() after a $timeout. That function calls
|
||
// MdNavItemController.setSelected() for the old tab with false and the new tab with true.
|
||
mdNavBar.mdSelectedNavItem = mdNavItem.name;
|
||
scope.$apply();
|
||
});
|
||
|
||
// Get the disabled attribute value first, then setup observing of value changes
|
||
mdNavItem.disabled = $mdUtil.parseAttributeBoolean(attrs['disabled'], false);
|
||
if ('MutationObserver' in $window) {
|
||
var config = {attributes: true, attributeFilter: ['disabled']};
|
||
var targetNode = element[0];
|
||
var mutationCallback = function(mutationList) {
|
||
$mdUtil.nextTick(function() {
|
||
mdNavItem.disabled = $mdUtil.parseAttributeBoolean(attrs[mutationList[0].attributeName], false);
|
||
});
|
||
};
|
||
var observer = new MutationObserver(mutationCallback);
|
||
observer.observe(targetNode, config);
|
||
disconnect = observer.disconnect.bind(observer);
|
||
} else {
|
||
attrs.$observe('disabled', function (value) {
|
||
mdNavItem.disabled = $mdUtil.parseAttributeBoolean(value, false);
|
||
});
|
||
}
|
||
|
||
if (!mdNavItem.navItemAriaLabel) {
|
||
$mdAria.expectWithText(navButton, 'aria-label');
|
||
}
|
||
});
|
||
|
||
scope.$on('destroy', function() {
|
||
navButton.off('keydown');
|
||
navButton.off('focus');
|
||
navButton.off('click');
|
||
disconnect();
|
||
});
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Controller for the nav-item component.
|
||
* @param {!JQLite} $element
|
||
* @constructor
|
||
* @final
|
||
* @ngInject
|
||
*/
|
||
function MdNavItemController($element) {
|
||
|
||
/**
|
||
* @private @const
|
||
* @type {!JQLite}
|
||
*/
|
||
this._$element = $element;
|
||
|
||
// Data-bound variables
|
||
|
||
/**
|
||
* @const
|
||
* @type {?Function}
|
||
*/
|
||
this.mdNavClick;
|
||
|
||
/**
|
||
* @const
|
||
* @type {?string}
|
||
*/
|
||
this.mdNavHref;
|
||
|
||
/**
|
||
* @const
|
||
* @type {?string}
|
||
*/
|
||
this.mdNavSref;
|
||
/**
|
||
* @const
|
||
* @type {?Object}
|
||
*/
|
||
this.srefOpts;
|
||
/**
|
||
* @const
|
||
* @type {?string}
|
||
*/
|
||
this.name;
|
||
|
||
/**
|
||
* @const
|
||
* @type {string}
|
||
*/
|
||
this.navItemAriaLabel;
|
||
|
||
// State variables
|
||
/**
|
||
* @private
|
||
* @type {boolean}
|
||
*/
|
||
this._selected = false;
|
||
|
||
/**
|
||
* @type {boolean}
|
||
*/
|
||
this.isFocused = false;
|
||
}
|
||
|
||
/**
|
||
* Returns a map of class names and values for use by ng-class.
|
||
* @return {!Object<string,boolean>}
|
||
*/
|
||
MdNavItemController.prototype.getNgClassMap = function() {
|
||
return {
|
||
'md-active': this._selected,
|
||
'md-primary': this._selected,
|
||
'md-unselected': !this._selected,
|
||
'md-focused': this.isFocused,
|
||
};
|
||
};
|
||
|
||
/**
|
||
* Get the name attribute of the tab.
|
||
* @return {string}
|
||
*/
|
||
MdNavItemController.prototype.getName = function() {
|
||
return this.name;
|
||
};
|
||
|
||
/**
|
||
* Get the button element associated with the tab.
|
||
* @return {!Element}
|
||
*/
|
||
MdNavItemController.prototype.getButtonEl = function() {
|
||
return this._$element[0].querySelector('._md-nav-button');
|
||
};
|
||
|
||
/**
|
||
* Set the selected state of the tab and updates the tabindex.
|
||
* This function is called for the oldTab and newTab when selection changes.
|
||
* @param {boolean} isSelected true to select the tab, false to deselect the tab
|
||
*/
|
||
MdNavItemController.prototype.setSelected = function(isSelected) {
|
||
this._selected = isSelected;
|
||
if (isSelected) {
|
||
// https://www.w3.org/TR/wai-aria-practices/examples/tabs/tabs-2/tabs.html suggests that we call
|
||
// removeAttribute('tabindex') here, but that causes our unit tests to fail due to
|
||
// document.activeElement staying set to the body instead of the focused nav button.
|
||
this.getButtonEl().setAttribute('tabindex', '0');
|
||
} else {
|
||
this.getButtonEl().setAttribute('tabindex', '-1');
|
||
}
|
||
};
|
||
|
||
/**
|
||
* @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.isFocused = isFocused;
|
||
|
||
if (isFocused) {
|
||
this.getButtonEl().focus();
|
||
}
|
||
};
|
||
|
||
/**
|
||
* @return {boolean} true if the tab has focus, false if not.
|
||
*/
|
||
MdNavItemController.prototype.hasFocus = function() {
|
||
return this.isFocused;
|
||
};
|
||
|
||
/**
|
||
* @return {boolean} true if the tab is enabled, false if disabled.
|
||
* @private
|
||
*/
|
||
MdNavItemController.prototype._isEnabled = function() {
|
||
return !this._$element.attr('disabled');
|
||
};
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.panel
|
||
*/
|
||
MdPanelService.$inject = ["presets", "$rootElement", "$rootScope", "$injector", "$window"];
|
||
angular
|
||
.module('material.components.panel', [
|
||
'material.core',
|
||
'material.components.backdrop'
|
||
])
|
||
.provider('$mdPanel', MdPanelProvider);
|
||
|
||
|
||
/*****************************************************************************
|
||
* PUBLIC DOCUMENTATION *
|
||
*****************************************************************************/
|
||
|
||
|
||
/**
|
||
* @ngdoc service
|
||
* @name $mdPanelProvider
|
||
* @module material.components.panel
|
||
*
|
||
* @description
|
||
* `$mdPanelProvider` allows users to create configuration presets that will be
|
||
* stored within a cached presets object. When the configuration is needed, the
|
||
* user can request the preset by passing it as the first parameter in the
|
||
* `$mdPanel.create` or `$mdPanel.open` methods.
|
||
*
|
||
* @usage
|
||
* <hljs lang="js">
|
||
* (function(angular, undefined) {
|
||
* 'use strict';
|
||
*
|
||
* angular
|
||
* .module('demoApp', ['ngMaterial'])
|
||
* .config(DemoConfig)
|
||
* .controller('DemoCtrl', DemoCtrl)
|
||
* .controller('DemoMenuCtrl', DemoMenuCtrl);
|
||
*
|
||
* function DemoConfig($mdPanelProvider) {
|
||
* $mdPanelProvider.definePreset('demoPreset', {
|
||
* attachTo: angular.element(document.body),
|
||
* controller: DemoMenuCtrl,
|
||
* controllerAs: 'ctrl',
|
||
* template: '' +
|
||
* '<div class="menu-panel" md-whiteframe="4">' +
|
||
* ' <div class="menu-content">' +
|
||
* ' <div class="menu-item" ng-repeat="item in ctrl.items">' +
|
||
* ' <button class="md-button">' +
|
||
* ' <span>{{item}}</span>' +
|
||
* ' </button>' +
|
||
* ' </div>' +
|
||
* ' <md-divider></md-divider>' +
|
||
* ' <div class="menu-item">' +
|
||
* ' <button class="md-button" ng-click="ctrl.closeMenu()">' +
|
||
* ' <span>Close Menu</span>' +
|
||
* ' </button>' +
|
||
* ' </div>' +
|
||
* ' </div>' +
|
||
* '</div>',
|
||
* panelClass: 'menu-panel-container',
|
||
* focusOnOpen: false,
|
||
* zIndex: 100,
|
||
* propagateContainerEvents: true,
|
||
* groupName: 'menus'
|
||
* });
|
||
* }
|
||
*
|
||
* function PanelProviderCtrl($mdPanel) {
|
||
* this.navigation = {
|
||
* name: 'navigation',
|
||
* items: [
|
||
* 'Home',
|
||
* 'About',
|
||
* 'Contact'
|
||
* ]
|
||
* };
|
||
* this.favorites = {
|
||
* name: 'favorites',
|
||
* items: [
|
||
* 'Add to Favorites'
|
||
* ]
|
||
* };
|
||
* this.more = {
|
||
* name: 'more',
|
||
* items: [
|
||
* 'Account',
|
||
* 'Sign Out'
|
||
* ]
|
||
* };
|
||
*
|
||
* $mdPanel.newPanelGroup('menus', {
|
||
* maxOpen: 2
|
||
* });
|
||
*
|
||
* this.showMenu = function($event, menu) {
|
||
* $mdPanel.open('demoPreset', {
|
||
* id: 'menu_' + menu.name,
|
||
* position: $mdPanel.newPanelPosition()
|
||
* .relativeTo($event.target)
|
||
* .addPanelPosition(
|
||
* $mdPanel.xPosition.ALIGN_START,
|
||
* $mdPanel.yPosition.BELOW
|
||
* ),
|
||
* locals: {
|
||
* items: menu.items
|
||
* },
|
||
* openFrom: $event
|
||
* });
|
||
* };
|
||
* }
|
||
*
|
||
* function PanelMenuCtrl(mdPanelRef) {
|
||
* // 'mdPanelRef' is injected in the controller.
|
||
* this.closeMenu = function() {
|
||
* if (mdPanelRef) {
|
||
* mdPanelRef.close();
|
||
* }
|
||
* };
|
||
* }
|
||
* })(angular);
|
||
* </hljs>
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdPanelProvider#definePreset
|
||
* @description
|
||
* Takes the passed in preset name and preset configuration object and adds it
|
||
* to the `_presets` object of the provider. This `_presets` object is then
|
||
* passed along to the `$mdPanel` service.
|
||
*
|
||
* @param {string} name Preset name.
|
||
* @param {!Object} preset Specific configuration object that can contain any
|
||
* and all of the parameters available within the `$mdPanel.create` method.
|
||
* However, parameters that pertain to id, position, animation, and user
|
||
* interaction are not allowed and will be removed from the preset
|
||
* configuration.
|
||
*/
|
||
|
||
|
||
/*****************************************************************************
|
||
* MdPanel Service *
|
||
*****************************************************************************/
|
||
|
||
|
||
/**
|
||
* @ngdoc service
|
||
* @name $mdPanel
|
||
* @module material.components.panel
|
||
*
|
||
* @description
|
||
* `$mdPanel` is a robust, low-level service for creating floating panels on
|
||
* the screen. It can be used to implement tooltips, dialogs, pop-ups, etc.
|
||
*
|
||
* The following types, referenced below, have separate documentation:
|
||
* - <a ng-href="api/type/MdPanelAnimation">MdPanelAnimation</a> from `$mdPanel.newPanelAnimation()`
|
||
* - <a ng-href="api/type/MdPanelPosition">MdPanelPosition</a> from `$mdPanel.newPanelPosition()`
|
||
* - <a ng-href="api/type/MdPanelRef">MdPanelRef</a> from the `$mdPanel.open()` Promise or
|
||
* injected in the panel's controller
|
||
*
|
||
* @usage
|
||
* <hljs lang="js">
|
||
* (function(angular, undefined) {
|
||
* 'use strict';
|
||
*
|
||
* angular
|
||
* .module('demoApp', ['ngMaterial'])
|
||
* .controller('DemoDialogController', DialogController)
|
||
* .controller('DemoCtrl', function($mdPanel) {
|
||
*
|
||
* var panelRef;
|
||
*
|
||
* function showPanel($event) {
|
||
* var panelPosition = $mdPanel.newPanelPosition()
|
||
* .absolute()
|
||
* .top('50%')
|
||
* .left('50%');
|
||
*
|
||
* var panelAnimation = $mdPanel.newPanelAnimation()
|
||
* .openFrom($event)
|
||
* .duration(200)
|
||
* .closeTo('.show-button')
|
||
* .withAnimation($mdPanel.animation.SCALE);
|
||
*
|
||
* var config = {
|
||
* attachTo: angular.element(document.body),
|
||
* controller: DialogController,
|
||
* controllerAs: 'ctrl',
|
||
* position: panelPosition,
|
||
* animation: panelAnimation,
|
||
* targetEvent: $event,
|
||
* templateUrl: 'dialog-template.html',
|
||
* clickOutsideToClose: true,
|
||
* escapeToClose: true,
|
||
* focusOnOpen: true
|
||
* };
|
||
*
|
||
* $mdPanel.open(config)
|
||
* .then(function(result) {
|
||
* panelRef = result;
|
||
* });
|
||
* }
|
||
* }
|
||
*
|
||
* function DialogController(MdPanelRef) {
|
||
* function closeDialog() {
|
||
* if (MdPanelRef) MdPanelRef.close();
|
||
* }
|
||
* }
|
||
* })(angular);
|
||
* </hljs>
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdPanel#create
|
||
* @description
|
||
* Creates a panel with the specified options.
|
||
*
|
||
* @param {!Object=} config Specific configuration object that may contain the
|
||
* following properties:
|
||
*
|
||
* - `id` - `{string=}`: An ID to track the panel by. When an ID is provided,
|
||
* the created panel is added to a tracked panels object. Any subsequent
|
||
* requests made to create a panel with that ID are ignored. This is useful
|
||
* in having the panel service not open multiple panels from the same user
|
||
* interaction when there is no backdrop and events are propagated. Defaults
|
||
* to an arbitrary string that is not tracked.
|
||
* - `template` - `{string=}`: HTML template to show in the panel. This
|
||
* **must** be trusted HTML with respect to AngularJS’s
|
||
* [$sce service](https://docs.angularjs.org/api/ng/service/$sce).
|
||
* - `templateUrl` - `{string=}`: The URL that will be used as the content of
|
||
* the panel.
|
||
* - `contentElement` - `{(string|!JQLite|!Element)=}`: Pre-compiled
|
||
* element to be used as the panel's content.
|
||
* - `controller` - `{(function|string)=}`: The controller to associate with
|
||
* the panel. The controller can inject a reference to the returned
|
||
* panelRef, which allows the panel to be closed, hidden, and shown. Any
|
||
* fields passed in through locals or resolve will be bound to the
|
||
* controller.
|
||
* - `controllerAs` - `{string=}`: An alias to assign the controller to on
|
||
* the scope.
|
||
* - `bindToController` - `{boolean=}`: Binds locals to the controller
|
||
* instead of passing them in. Defaults to true, as this is a best
|
||
* practice.
|
||
* - `locals` - `{Object=}`: An object containing key/value pairs. The keys
|
||
* will be used as names of values to inject into the controller. For
|
||
* example, `locals: {three: 3}` would inject `three` into the controller,
|
||
* with the value 3. 'mdPanelRef' is a reserved key, and will always
|
||
* be set to the created `MdPanelRef` instance.
|
||
* - `resolve` - `{Object=}`: Similar to locals, except it takes promises as
|
||
* values. The panel will not open until all of the promises resolve.
|
||
* - `attachTo` - `{(string|!JQLite|!Element)=}`: The element to
|
||
* attach the panel to. Defaults to appending to the root element of the
|
||
* application.
|
||
* - `propagateContainerEvents` - `{boolean=}`: Whether pointer or touch
|
||
* events should be allowed to propagate 'go through' the container, aka the
|
||
* wrapper, of the panel. Defaults to false.
|
||
* - `panelClass` - `{string=}`: A css class to apply to the panel element.
|
||
* This class should define any borders, box-shadow, etc. for the panel.
|
||
* - `zIndex` - `{number=}`: The z-index to place the panel at.
|
||
* Defaults to 80.
|
||
* - `position` - `{MdPanelPosition=}`: An MdPanelPosition object that
|
||
* specifies the alignment of the panel. For more information, see
|
||
* `MdPanelPosition`.
|
||
* - `clickOutsideToClose` - `{boolean=}`: Whether the user can click
|
||
* outside the panel to close it. Defaults to false.
|
||
* - `escapeToClose` - `{boolean=}`: Whether the user can press escape to
|
||
* close the panel. Defaults to false.
|
||
* - `onCloseSuccess` - `{function(!panelRef, string)=}`: Function that is
|
||
* called after the close successfully finishes. The first parameter passed
|
||
* into this function is the current panelRef and the 2nd is an optional
|
||
* string explaining the close reason. The currently supported closeReasons
|
||
* can be found in the `MdPanelRef.closeReasons` enum. These are by default
|
||
* passed along by the panel.
|
||
* - `trapFocus` - `{boolean=}`: Whether focus should be trapped within the
|
||
* panel. If `trapFocus` is true, the user will not be able to interact
|
||
* with the rest of the page until the panel is dismissed. Defaults to
|
||
* false.
|
||
* - `focusOnOpen` - `{boolean=}`: An option to override focus behavior on
|
||
* open. Only disable if focusing some other way, as focus management is
|
||
* required for panels to be accessible. Defaults to true.
|
||
* - `fullscreen` - `{boolean=}`: Whether the panel should be full screen.
|
||
* Applies the class `._md-panel-fullscreen` to the panel on open. Defaults
|
||
* to false.
|
||
* - `animation` - `{MdPanelAnimation=}`: An MdPanelAnimation object that
|
||
* specifies the animation of the panel. For more information, see
|
||
* `MdPanelAnimation`.
|
||
* - `hasBackdrop` - `{boolean=}`: Whether there should be an opaque backdrop
|
||
* behind the panel. Defaults to false.
|
||
* - `disableParentScroll` - `{boolean=}`: Whether the user can scroll the
|
||
* page behind the panel. Defaults to false.
|
||
* - `onDomAdded` - `{function=}`: Callback function used to announce when
|
||
* the panel is added to the DOM.
|
||
* - `onOpenComplete` - `{function=}`: Callback function used to announce
|
||
* when the open() action is finished.
|
||
* - `onRemoving` - `{function=}`: Callback function used to announce the
|
||
* close/hide() action is starting.
|
||
* - `onDomRemoved` - `{function=}`: Callback function used to announce when
|
||
* the panel is removed from the DOM.
|
||
* - `origin` - `{(string|!JQLite|!Element)=}`: The element to focus
|
||
* on when the panel closes. This is commonly the element which triggered
|
||
* the opening of the panel. If you do not use `origin`, you need to control
|
||
* the focus manually.
|
||
* - `groupName` - `{(string|!Array<string>)=}`: A group name or an array of
|
||
* group names. The group name is used for creating a group of panels. The
|
||
* group is used for configuring the number of open panels and identifying
|
||
* specific behaviors for groups. For instance, all tooltips could be
|
||
* identified using the same groupName.
|
||
*
|
||
* @returns {!MdPanelRef} panelRef
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdPanel#open
|
||
* @description
|
||
* Calls the create method above, then opens the panel. This is a shortcut for
|
||
* creating and then calling open manually. If custom methods need to be
|
||
* called when the panel is added to the DOM or opened, do not use this method.
|
||
* Instead create the panel, chain promises on the domAdded and openComplete
|
||
* methods, and call open from the returned panelRef.
|
||
*
|
||
* @param {!Object=} config Specific configuration object that may contain
|
||
* the properties defined in `$mdPanel.create`.
|
||
* @returns {!Q.IPromise<!MdPanelRef>} panelRef A promise that resolves
|
||
* to an instance of the panel.
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdPanel#newPanelPosition
|
||
* @description
|
||
* Returns a new instance of the MdPanelPosition object. Use this to create
|
||
* the position config object.
|
||
*
|
||
* @returns {!MdPanelPosition} panelPosition
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdPanel#newPanelAnimation
|
||
* @description
|
||
* Returns a new instance of the MdPanelAnimation object. Use this to create
|
||
* the animation config object.
|
||
*
|
||
* @returns {!MdPanelAnimation} panelAnimation
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdPanel#setGroupMaxOpen
|
||
* @description
|
||
* Sets the maximum number of panels in a group that can be opened at a given
|
||
* time.
|
||
*
|
||
* @param {string} groupName The name of the group to configure.
|
||
* @param {number} maxOpen The maximum number of panels that can be
|
||
* opened. Infinity can be passed in to remove the maxOpen limit.
|
||
*/
|
||
|
||
|
||
/*****************************************************************************
|
||
* MdPanelRef *
|
||
*****************************************************************************/
|
||
|
||
|
||
/**
|
||
* @ngdoc type
|
||
* @name MdPanelRef
|
||
* @module material.components.panel
|
||
* @description
|
||
* A reference to a created panel. This reference contains a unique id for the
|
||
* panel, along with the following properties:
|
||
*
|
||
* - `id` - `{string}`: The unique id for the panel. This id is used to track
|
||
* when a panel was interacted with.
|
||
* - `config` - `{!Object=}`: The entire config object that was used in
|
||
* create.
|
||
* - `isAttached` - `{boolean}`: Whether the panel is attached to the DOM.
|
||
* Visibility to the user does not factor into isAttached.
|
||
* - `panelContainer` - `{JQLite}`: The wrapper element containing the
|
||
* panel. This property is added in order to have access to the `addClass`,
|
||
* `removeClass`, `toggleClass`, etc methods.
|
||
* - `panelEl` - `{JQLite}`: The panel element. This property is added
|
||
* in order to have access to the `addClass`, `removeClass`, `toggleClass`,
|
||
* etc methods.
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name MdPanelRef#open
|
||
* @description
|
||
* Attaches and shows the panel.
|
||
*
|
||
* @returns {!Q.IPromise} A promise that is resolved when the panel is
|
||
* opened.
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name MdPanelRef#close
|
||
* @description
|
||
* Hides and detaches the panel. Note that this will **not** destroy the panel.
|
||
* If you don't intend on using the panel again, call the {@link #destroy
|
||
* destroy} method afterwards.
|
||
*
|
||
* @returns {!Q.IPromise} A promise that is resolved when the panel is
|
||
* closed.
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name MdPanelRef#attach
|
||
* @description
|
||
* Create the panel elements and attach them to the DOM. The panel will be
|
||
* hidden by default.
|
||
*
|
||
* @returns {!Q.IPromise} A promise that is resolved when the panel is
|
||
* attached.
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name MdPanelRef#detach
|
||
* @description
|
||
* Removes the panel from the DOM. This will NOT hide the panel before removing
|
||
* it.
|
||
*
|
||
* @returns {!Q.IPromise} A promise that is resolved when the panel is
|
||
* detached.
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name MdPanelRef#show
|
||
* @description
|
||
* Shows the panel.
|
||
*
|
||
* @returns {!Q.IPromise} A promise that is resolved when the panel has
|
||
* shown and animations are completed.
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name MdPanelRef#hide
|
||
* @description
|
||
* Hides the panel.
|
||
*
|
||
* @returns {!Q.IPromise} A promise that is resolved when the panel has
|
||
* hidden and animations are completed.
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name MdPanelRef#destroy
|
||
* @description
|
||
* Destroys the panel. The panel cannot be opened again after this is called.
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name MdPanelRef#updatePosition
|
||
* @description
|
||
* Updates the position configuration of a panel. Use this to update the
|
||
* position of a panel that is open, without having to close and re-open the
|
||
* panel.
|
||
*
|
||
* @param {!MdPanelPosition} position
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name MdPanelRef#addToGroup
|
||
* @description
|
||
* Adds a panel to a group if the panel does not exist within the group already.
|
||
* A panel can only exist within a single group.
|
||
*
|
||
* @param {string} groupName The name of the group to add the panel to.
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name MdPanelRef#removeFromGroup
|
||
* @description
|
||
* Removes a panel from a group if the panel exists within that group. The group
|
||
* must be created ahead of time.
|
||
*
|
||
* @param {string} groupName The name of the group.
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name MdPanelRef#registerInterceptor
|
||
* @description
|
||
* Registers an interceptor with the panel. The callback should return a promise,
|
||
* which will allow the action to continue when it gets resolved, or will
|
||
* prevent an action if it is rejected. The interceptors are called sequentially
|
||
* and it reverse order. `type` must be one of the following
|
||
* values available on `$mdPanel.interceptorTypes`:
|
||
* * `CLOSE` - Gets called before the panel begins closing.
|
||
*
|
||
* @param {string} type Type of interceptor.
|
||
* @param {!Q.IPromise<any>} callback Callback to be registered.
|
||
* @returns {!MdPanelRef}
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name MdPanelRef#removeInterceptor
|
||
* @description
|
||
* Removes a registered interceptor.
|
||
*
|
||
* @param {string} type Type of interceptor to be removed.
|
||
* @param {function(): !Q.IPromise<any>} callback Interceptor to be removed.
|
||
* @returns {!MdPanelRef}
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name MdPanelRef#removeAllInterceptors
|
||
* @description
|
||
* Removes all interceptors. If a type is supplied, only the
|
||
* interceptors of that type will be cleared.
|
||
*
|
||
* @param {string=} type Type of interceptors to be removed.
|
||
* @returns {!MdPanelRef}
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name MdPanelRef#updateAnimation
|
||
* @description
|
||
* Updates the animation configuration for a panel. You can use this to change
|
||
* the panel's animation without having to re-create it.
|
||
*
|
||
* @param {!MdPanelAnimation} animation
|
||
*/
|
||
|
||
|
||
/*****************************************************************************
|
||
* MdPanelPosition *
|
||
*****************************************************************************/
|
||
|
||
|
||
/**
|
||
* @ngdoc type
|
||
* @name MdPanelPosition
|
||
* @module material.components.panel
|
||
* @description
|
||
*
|
||
* Object for configuring the position of the panel.
|
||
*
|
||
* @usage
|
||
*
|
||
* #### Centering the panel
|
||
*
|
||
* <hljs lang="js">
|
||
* new MdPanelPosition().absolute().center();
|
||
* </hljs>
|
||
*
|
||
* #### Overlapping the panel with an element
|
||
*
|
||
* <hljs lang="js">
|
||
* new MdPanelPosition()
|
||
* .relativeTo(someElement)
|
||
* .addPanelPosition(
|
||
* $mdPanel.xPosition.ALIGN_START,
|
||
* $mdPanel.yPosition.ALIGN_TOPS
|
||
* );
|
||
* </hljs>
|
||
*
|
||
* #### Aligning the panel with the bottom of an element
|
||
*
|
||
* <hljs lang="js">
|
||
* new MdPanelPosition()
|
||
* .relativeTo(someElement)
|
||
* .addPanelPosition($mdPanel.xPosition.CENTER, $mdPanel.yPosition.BELOW);
|
||
* </hljs>
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name MdPanelPosition#absolute
|
||
* @description
|
||
* Positions the panel absolutely relative to the parent element. If the parent
|
||
* is document.body, this is equivalent to positioning the panel absolutely
|
||
* within the viewport.
|
||
*
|
||
* @returns {!MdPanelPosition}
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name MdPanelPosition#relativeTo
|
||
* @description
|
||
* Positions the panel relative to a specific element.
|
||
*
|
||
* @param {string|!Element|!JQLite} element Query selector, DOM element,
|
||
* or angular element to position the panel with respect to.
|
||
* @returns {!MdPanelPosition}
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name MdPanelPosition#top
|
||
* @description
|
||
* Sets the value of `top` for the panel. Clears any previously set vertical
|
||
* position.
|
||
*
|
||
* @param {string=} top Value of `top`. Defaults to '0'.
|
||
* @returns {!MdPanelPosition}
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name MdPanelPosition#bottom
|
||
* @description
|
||
* Sets the value of `bottom` for the panel. Clears any previously set vertical
|
||
* position.
|
||
*
|
||
* @param {string=} bottom Value of `bottom`. Defaults to '0'.
|
||
* @returns {!MdPanelPosition}
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name MdPanelPosition#start
|
||
* @description
|
||
* Sets the panel to the start of the page - `left` if `ltr` or `right` for
|
||
* `rtl`. Clears any previously set horizontal position.
|
||
*
|
||
* @param {string=} start Value of position. Defaults to '0'.
|
||
* @returns {!MdPanelPosition}
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name MdPanelPosition#end
|
||
* @description
|
||
* Sets the panel to the end of the page - `right` if `ltr` or `left` for `rtl`.
|
||
* Clears any previously set horizontal position.
|
||
*
|
||
* @param {string=} end Value of position. Defaults to '0'.
|
||
* @returns {!MdPanelPosition}
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name MdPanelPosition#left
|
||
* @description
|
||
* Sets the value of `left` for the panel. Clears any previously set
|
||
* horizontal position.
|
||
*
|
||
* @param {string=} left Value of `left`. Defaults to '0'.
|
||
* @returns {!MdPanelPosition}
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name MdPanelPosition#right
|
||
* @description
|
||
* Sets the value of `right` for the panel. Clears any previously set
|
||
* horizontal position.
|
||
*
|
||
* @param {string=} right Value of `right`. Defaults to '0'.
|
||
* @returns {!MdPanelPosition}
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name MdPanelPosition#centerHorizontally
|
||
* @description
|
||
* Centers the panel horizontally in the viewport. Clears any previously set
|
||
* horizontal position.
|
||
*
|
||
* @returns {!MdPanelPosition}
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name MdPanelPosition#centerVertically
|
||
* @description
|
||
* Centers the panel vertically in the viewport. Clears any previously set
|
||
* vertical position.
|
||
*
|
||
* @returns {!MdPanelPosition}
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name MdPanelPosition#center
|
||
* @description
|
||
* Centers the panel horizontally and vertically in the viewport. This is
|
||
* equivalent to calling both `centerHorizontally` and `centerVertically`.
|
||
* Clears any previously set horizontal and vertical positions.
|
||
*
|
||
* @returns {!MdPanelPosition}
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name MdPanelPosition#addPanelPosition
|
||
* @description
|
||
* Sets the x and y position for the panel relative to another element. Can be
|
||
* called multiple times to specify an ordered list of panel positions. The
|
||
* first position which allows the panel to be completely on-screen will be
|
||
* chosen; the last position will be chose whether it is on-screen or not.
|
||
*
|
||
* xPosition must be one of the following values available on
|
||
* $mdPanel.xPosition:
|
||
*
|
||
*
|
||
* CENTER | ALIGN_START | ALIGN_END | OFFSET_START | OFFSET_END
|
||
*
|
||
* <pre>
|
||
* *************
|
||
* * *
|
||
* * 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)
|
||
* </pre>
|
||
*
|
||
* yPosition must be one of the following values available on
|
||
* $mdPanel.yPosition:
|
||
*
|
||
* CENTER | ALIGN_TOPS | ALIGN_BOTTOMS | ABOVE | BELOW
|
||
*
|
||
* <pre>
|
||
* F
|
||
* G *************
|
||
* * *
|
||
* H * PANEL *
|
||
* * *
|
||
* I *************
|
||
* J
|
||
*
|
||
* F: BELOW
|
||
* G: ALIGN_TOPS
|
||
* H: CENTER
|
||
* I: ALIGN_BOTTOMS
|
||
* J: ABOVE
|
||
* </pre>
|
||
*
|
||
* @param {string} xPosition
|
||
* @param {string} yPosition
|
||
* @returns {!MdPanelPosition}
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name MdPanelPosition#withOffsetX
|
||
* @description
|
||
* Sets the value of the offset in the x-direction.
|
||
*
|
||
* @param {string|number} offsetX
|
||
* @returns {!MdPanelPosition}
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name MdPanelPosition#withOffsetY
|
||
* @description
|
||
* Sets the value of the offset in the y-direction.
|
||
*
|
||
* @param {string|number} offsetY
|
||
* @returns {!MdPanelPosition}
|
||
*/
|
||
|
||
|
||
/*****************************************************************************
|
||
* MdPanelAnimation *
|
||
*****************************************************************************/
|
||
|
||
|
||
/**
|
||
* @ngdoc type
|
||
* @name MdPanelAnimation
|
||
* @module material.components.panel
|
||
* @description
|
||
* Animation configuration object. To use, create an MdPanelAnimation with the
|
||
* desired properties, then pass the object as part of $mdPanel creation.
|
||
*
|
||
* @usage
|
||
*
|
||
* <hljs lang="js">
|
||
* var panelAnimation = new MdPanelAnimation()
|
||
* .openFrom(myButtonEl)
|
||
* .duration(1337)
|
||
* .closeTo('.my-button')
|
||
* .withAnimation($mdPanel.animation.SCALE);
|
||
*
|
||
* $mdPanel.create({
|
||
* animation: panelAnimation
|
||
* });
|
||
* </hljs>
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name MdPanelAnimation#openFrom
|
||
* @description
|
||
* Specifies where to start the open animation. `openFrom` accepts a
|
||
* click event object, query selector, DOM element, or a Rect object that
|
||
* is used to determine the bounds. When passed a click event, the location
|
||
* of the click will be used as the position to start the animation.
|
||
*
|
||
* @param {string|!Element|!Event|{top: number, left: number}}
|
||
* @returns {!MdPanelAnimation}
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name MdPanelAnimation#closeTo
|
||
* @description
|
||
* Specifies where to animate the panel close. `closeTo` accepts a
|
||
* query selector, DOM element, or a Rect object that is used to determine
|
||
* the bounds.
|
||
*
|
||
* @param {string|!Element|{top: number, left: number}}
|
||
* @returns {!MdPanelAnimation}
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name MdPanelAnimation#withAnimation
|
||
* @description
|
||
* Specifies the animation class.
|
||
*
|
||
* There are several default animations that can be used: `$mdPanel.animation.`
|
||
* - `SLIDE`: The panel slides in and out from the specified
|
||
* elements. It will not fade in or out.
|
||
* - `SCALE`: The panel scales in and out. Slide and fade are
|
||
* included in this animation.
|
||
* - `FADE`: The panel fades in and out.
|
||
*
|
||
* Custom classes will by default fade in and out unless
|
||
* `transition: opacity 1ms` is added to the to custom class.
|
||
*
|
||
* @param {string|{open: string, close: string}} cssClass
|
||
* @returns {!MdPanelAnimation}
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name MdPanelAnimation#duration
|
||
* @description
|
||
* Specifies the duration of the animation in milliseconds. The `duration`
|
||
* method accepts either a number or an object with separate open and close
|
||
* durations.
|
||
*
|
||
* @param {number|{open: number, close: number}} duration
|
||
* @returns {!MdPanelAnimation}
|
||
*/
|
||
|
||
|
||
/*****************************************************************************
|
||
* PUBLIC DOCUMENTATION *
|
||
*****************************************************************************/
|
||
|
||
|
||
var MD_PANEL_Z_INDEX = 80;
|
||
var MD_PANEL_HIDDEN = '_md-panel-hidden';
|
||
var FOCUS_TRAP_TEMPLATE;
|
||
|
||
var _presets = {};
|
||
|
||
|
||
/**
|
||
* A provider that is used for creating presets for the panel API.
|
||
* @final @constructor @ngInject
|
||
*/
|
||
function MdPanelProvider() {
|
||
return {
|
||
'definePreset': definePreset,
|
||
'getAllPresets': getAllPresets,
|
||
'clearPresets': clearPresets,
|
||
'$get': $getProvider()
|
||
};
|
||
}
|
||
|
||
|
||
/**
|
||
* Takes the passed in panel configuration object and adds it to the `_presets`
|
||
* object at the specified name.
|
||
* @param {string} name Name of the preset to set.
|
||
* @param {!Object} preset Specific configuration object that can contain any
|
||
* and all of the parameters available within the `$mdPanel.create` method.
|
||
* However, parameters that pertain to id, position, animation, and user
|
||
* interaction are not allowed and will be removed from the preset
|
||
* configuration.
|
||
*/
|
||
function definePreset(name, preset) {
|
||
if (!name || !preset) {
|
||
throw new Error('mdPanelProvider: The panel preset definition is ' +
|
||
'malformed. The name and preset object are required.');
|
||
} else if (_presets.hasOwnProperty(name)) {
|
||
throw new Error('mdPanelProvider: The panel preset you have requested ' +
|
||
'has already been defined.');
|
||
}
|
||
|
||
// Delete any property on the preset that is not allowed.
|
||
delete preset.id;
|
||
delete preset.position;
|
||
delete preset.animation;
|
||
|
||
_presets[name] = preset;
|
||
}
|
||
|
||
|
||
/**
|
||
* Gets a clone of the `_presets`.
|
||
* @return {!Object}
|
||
*/
|
||
function getAllPresets() {
|
||
return angular.copy(_presets);
|
||
}
|
||
|
||
|
||
/**
|
||
* Clears all of the stored presets.
|
||
*/
|
||
function clearPresets() {
|
||
_presets = {};
|
||
}
|
||
|
||
|
||
/**
|
||
* Represents the `$get` method of the AngularJS provider. From here, a new
|
||
* reference to the MdPanelService is returned where the needed arguments are
|
||
* passed in including the MdPanelProvider `_presets`.
|
||
* @param {!Object} _presets
|
||
* @param {!JQLite} $rootElement
|
||
* @param {!angular.Scope} $rootScope
|
||
* @param {!IInjectorService} $injector
|
||
* @param {!IWindowService} $window
|
||
*/
|
||
function $getProvider() {
|
||
return [
|
||
'$rootElement', '$rootScope', '$injector', '$window',
|
||
function($rootElement, $rootScope, $injector, $window) {
|
||
return new MdPanelService(_presets, $rootElement, $rootScope,
|
||
$injector, $window);
|
||
}
|
||
];
|
||
}
|
||
|
||
/**
|
||
* @param {string|[]} value
|
||
* @returns {[]} the input string wrapped in an Array or the original Array
|
||
*/
|
||
function coerceToArray(value) {
|
||
if (angular.isString(value)) {
|
||
value = [value];
|
||
}
|
||
return value;
|
||
}
|
||
|
||
/*****************************************************************************
|
||
* MdPanel Service *
|
||
*****************************************************************************/
|
||
|
||
|
||
/**
|
||
* A service that is used for controlling/displaying panels on the screen.
|
||
* @param {!Object} presets
|
||
* @param {!JQLite} $rootElement
|
||
* @param {!angular.Scope} $rootScope
|
||
* @param {!IInjectorService} $injector
|
||
* @param {!IWindowService} $window
|
||
* @final @constructor @ngInject
|
||
*/
|
||
function MdPanelService(presets, $rootElement, $rootScope, $injector, $window) {
|
||
/**
|
||
* Default config options for the panel.
|
||
* Anything angular related needs to be done later. Therefore
|
||
* scope: $rootScope.$new(true),
|
||
* attachTo: $rootElement,
|
||
* are added later.
|
||
* @private {!Object}
|
||
*/
|
||
this._defaultConfigOptions = {
|
||
bindToController: true,
|
||
clickOutsideToClose: false,
|
||
disableParentScroll: false,
|
||
escapeToClose: false,
|
||
focusOnOpen: true,
|
||
fullscreen: false,
|
||
hasBackdrop: false,
|
||
propagateContainerEvents: false,
|
||
transformTemplate: angular.bind(this, this._wrapTemplate),
|
||
trapFocus: false,
|
||
zIndex: MD_PANEL_Z_INDEX
|
||
};
|
||
|
||
/** @private {!Object} */
|
||
this._config = {};
|
||
|
||
/** @private {!Object} */
|
||
this._presets = presets;
|
||
|
||
/** @private @const */
|
||
this._$rootElement = $rootElement;
|
||
|
||
/** @private @const */
|
||
this._$rootScope = $rootScope;
|
||
|
||
/** @private @const */
|
||
this._$injector = $injector;
|
||
|
||
/** @private @const */
|
||
this._$window = $window;
|
||
|
||
/** @private @const */
|
||
this._$mdUtil = this._$injector.get('$mdUtil');
|
||
|
||
/** @private {!Object<string, !MdPanelRef>} */
|
||
this._trackedPanels = {};
|
||
|
||
/**
|
||
* @private {!Object<string,
|
||
* {panels: !Array<!MdPanelRef>,
|
||
* openPanels: !Array<!MdPanelRef>,
|
||
* maxOpen: number}>}
|
||
*/
|
||
this._groups = Object.create(null);
|
||
|
||
/**
|
||
* Default animations that can be used within the panel.
|
||
* @type {enum}
|
||
*/
|
||
this.animation = MdPanelAnimation.animation;
|
||
|
||
/**
|
||
* Possible values of xPosition for positioning the panel relative to
|
||
* another element.
|
||
* @type {enum}
|
||
*/
|
||
this.xPosition = MdPanelPosition.xPosition;
|
||
|
||
/**
|
||
* Possible values of yPosition for positioning the panel relative to
|
||
* another element.
|
||
* @type {enum}
|
||
*/
|
||
this.yPosition = MdPanelPosition.yPosition;
|
||
|
||
/**
|
||
* Possible values for the interceptors that can be registered on a panel.
|
||
* @type {enum}
|
||
*/
|
||
this.interceptorTypes = MdPanelRef.interceptorTypes;
|
||
|
||
/**
|
||
* Possible values for closing of a panel.
|
||
* @type {enum}
|
||
*/
|
||
this.closeReasons = MdPanelRef.closeReasons;
|
||
|
||
/**
|
||
* Possible values of absolute position.
|
||
* @type {enum}
|
||
*/
|
||
this.absPosition = MdPanelPosition.absPosition;
|
||
}
|
||
|
||
|
||
/**
|
||
* Creates a panel with the specified options.
|
||
* @param {string|Object=} preset Name of a preset configuration that can be used to
|
||
* extend the panel configuration.
|
||
* @param {!Object=} config Configuration object for the panel.
|
||
* @returns {!MdPanelRef}
|
||
*/
|
||
MdPanelService.prototype.create = function(preset, config) {
|
||
if (typeof preset === 'string') {
|
||
preset = this._getPresetByName(preset);
|
||
} else if (typeof preset === 'object' &&
|
||
(angular.isUndefined(config) || !config)) {
|
||
config = preset;
|
||
preset = {};
|
||
}
|
||
|
||
preset = preset || {};
|
||
config = config || {};
|
||
|
||
// If the passed-in config contains an ID and the ID is within _trackedPanels,
|
||
// return the tracked panel after updating its config with the passed-in
|
||
// config.
|
||
if (angular.isDefined(config.id) && this._trackedPanels[config.id]) {
|
||
var trackedPanel = this._trackedPanels[config.id];
|
||
angular.extend(trackedPanel.config, config);
|
||
return trackedPanel;
|
||
}
|
||
|
||
// Combine the passed-in config, the _defaultConfigOptions, and the preset
|
||
// configuration into the `_config`.
|
||
this._config = angular.extend({
|
||
// If no ID is set within the passed-in config, then create an arbitrary ID.
|
||
id: config.id || 'panel_' + this._$mdUtil.nextUid(),
|
||
scope: this._$rootScope.$new(true),
|
||
attachTo: this._$rootElement
|
||
}, this._defaultConfigOptions, config, preset);
|
||
|
||
// Create the panelRef and add it to the `_trackedPanels` object.
|
||
var panelRef = new MdPanelRef(this._config, this._$injector);
|
||
this._trackedPanels[this._config.id] = panelRef;
|
||
|
||
// Add the panel to each of its requested groups.
|
||
if (this._config.groupName) {
|
||
this._config.groupName = coerceToArray(this._config.groupName);
|
||
angular.forEach(this._config.groupName, function(group) {
|
||
panelRef.addToGroup(group);
|
||
});
|
||
}
|
||
|
||
this._config.scope.$on('$destroy', angular.bind(panelRef, panelRef.detach));
|
||
|
||
return panelRef;
|
||
};
|
||
|
||
|
||
/**
|
||
* Creates and opens a panel with the specified options.
|
||
* @param {string=} preset Name of a preset configuration that can be used to
|
||
* extend the panel configuration.
|
||
* @param {!Object=} config Configuration object for the panel.
|
||
* @returns {!Q.IPromise<!MdPanelRef>} The panel created from create.
|
||
*/
|
||
MdPanelService.prototype.open = function(preset, config) {
|
||
var panelRef = this.create(preset, config);
|
||
return panelRef.open().then(function() {
|
||
return panelRef;
|
||
});
|
||
};
|
||
|
||
|
||
/**
|
||
* Gets a specific preset configuration object saved within `_presets`.
|
||
* @param {string} preset Name of the preset to search for.
|
||
* @returns {!Object} The preset configuration object.
|
||
*/
|
||
MdPanelService.prototype._getPresetByName = function(preset) {
|
||
if (!this._presets[preset]) {
|
||
throw new Error('mdPanel: The panel preset configuration that you ' +
|
||
'requested does not exist. Use the $mdPanelProvider to create a ' +
|
||
'preset before requesting one.');
|
||
}
|
||
return this._presets[preset];
|
||
};
|
||
|
||
|
||
/**
|
||
* Returns a new instance of the MdPanelPosition. Use this to create the
|
||
* positioning object.
|
||
* @returns {!MdPanelPosition}
|
||
*/
|
||
MdPanelService.prototype.newPanelPosition = function() {
|
||
return new MdPanelPosition(this._$injector);
|
||
};
|
||
|
||
|
||
/**
|
||
* Returns a new instance of the MdPanelAnimation. Use this to create the
|
||
* animation object.
|
||
* @returns {!MdPanelAnimation}
|
||
*/
|
||
MdPanelService.prototype.newPanelAnimation = function() {
|
||
return new MdPanelAnimation(this._$injector);
|
||
};
|
||
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdPanel#newPanelGroup
|
||
* @description
|
||
* Creates a panel group and adds it to a tracked list of panel groups.
|
||
* @param {string} groupName Name of the group to create.
|
||
* @param {{maxOpen: number}=} config Configuration object that may contain the following
|
||
* properties:
|
||
*
|
||
* - `maxOpen`: The maximum number of panels that are allowed open within a defined panel group.
|
||
*
|
||
* @returns {!{panels: !Array<!MdPanelRef>, openPanels: !Array<!MdPanelRef>, maxOpen: number}}
|
||
* the new panel group
|
||
*/
|
||
MdPanelService.prototype.newPanelGroup = function(groupName, config) {
|
||
if (!this._groups[groupName]) {
|
||
config = config || {};
|
||
this._groups[groupName] = {
|
||
panels: [],
|
||
openPanels: [],
|
||
maxOpen: config.maxOpen > 0 ? config.maxOpen : Infinity
|
||
};
|
||
}
|
||
return this._groups[groupName];
|
||
};
|
||
|
||
|
||
/**
|
||
* Sets the maximum number of panels in a group that can be opened at a given
|
||
* time.
|
||
* @param {string} groupName The name of the group to configure.
|
||
* @param {number} maxOpen The maximum number of panels that can be
|
||
* opened. Infinity can be passed in to remove the maxOpen limit.
|
||
*/
|
||
MdPanelService.prototype.setGroupMaxOpen = function(groupName, maxOpen) {
|
||
if (this._groups[groupName]) {
|
||
this._groups[groupName].maxOpen = maxOpen;
|
||
} else {
|
||
throw new Error('mdPanel: Group does not exist yet. Call newPanelGroup().');
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* Determines if the current number of open panels within a group exceeds the
|
||
* limit of allowed open panels.
|
||
* @param {string} groupName The name of the group to check.
|
||
* @returns {boolean} true if open count does exceed maxOpen and false if not.
|
||
* @private
|
||
*/
|
||
MdPanelService.prototype._openCountExceedsMaxOpen = function(groupName) {
|
||
if (this._groups[groupName]) {
|
||
var group = this._groups[groupName];
|
||
return group.maxOpen > 0 && group.openPanels.length > group.maxOpen;
|
||
}
|
||
return false;
|
||
};
|
||
|
||
|
||
/**
|
||
* Closes the first open panel within a specific group.
|
||
* @param {string} groupName The name of the group.
|
||
* @private
|
||
*/
|
||
MdPanelService.prototype._closeFirstOpenedPanel = function(groupName) {
|
||
var group = this._groups[groupName];
|
||
if (group && group.openPanels.length) {
|
||
group.openPanels[0].close();
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* Wraps the user's template in three elements:
|
||
* - md-panel-outer-wrapper - covers the entire `attachTo` element.
|
||
* - md-panel-inner-wrapper - handles the positioning.
|
||
* - md-panel - contains the user's content and deals with the animations.
|
||
* @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 '' +
|
||
'<div class="md-panel-outer-wrapper">' +
|
||
'<div class="md-panel-inner-wrapper _md-panel-offscreen">' +
|
||
'<div class="md-panel _md-panel-offscreen">' + template + '</div>' +
|
||
'</div>' +
|
||
'</div>';
|
||
};
|
||
|
||
|
||
/**
|
||
* Wraps a content element in a `md-panel-outer-wrapper`, as well as
|
||
* a `md-panel-inner-wrapper`, and positions it off-screen. Allows for
|
||
* proper control over positioning and animations.
|
||
* @param {!JQLite} contentElement Element to be wrapped.
|
||
* @return {!JQLite} Wrapper element.
|
||
* @private
|
||
*/
|
||
MdPanelService.prototype._wrapContentElement = function(contentElement) {
|
||
var outerWrapper = angular.element(
|
||
'<div class="md-panel-outer-wrapper">' +
|
||
'<div class="md-panel-inner-wrapper _md-panel-offscreen"></div>' +
|
||
'</div>'
|
||
);
|
||
|
||
contentElement.addClass('md-panel _md-panel-offscreen');
|
||
outerWrapper.children().eq(0).append(contentElement);
|
||
|
||
return outerWrapper;
|
||
};
|
||
|
||
|
||
/*****************************************************************************
|
||
* MdPanelRef *
|
||
*****************************************************************************/
|
||
|
||
|
||
/**
|
||
* A reference to a created panel. This reference contains a unique id for the
|
||
* panel, along with properties/functions used to control the panel.
|
||
* @param {!Object} config
|
||
* @param {!IInjectorService} $injector
|
||
* @final @constructor
|
||
*/
|
||
function MdPanelRef(config, $injector) {
|
||
// Injected variables.
|
||
/** @private @const {!IQService} */
|
||
this._$q = $injector.get('$q');
|
||
|
||
/** @private @const {!angular.$mdCompiler} */
|
||
this._$mdCompiler = $injector.get('$mdCompiler');
|
||
|
||
/** @private @const {!angular.$mdConstant} */
|
||
this._$mdConstant = $injector.get('$mdConstant');
|
||
|
||
/** @private @const {!angular.$mdUtil} */
|
||
this._$mdUtil = $injector.get('$mdUtil');
|
||
|
||
/** @private @const {!angular.$mdTheming} */
|
||
this._$mdTheming = $injector.get('$mdTheming');
|
||
|
||
/** @private @const {!IRootScopeService} */
|
||
this._$rootScope = $injector.get('$rootScope');
|
||
|
||
/** @private @const {!angular.$animate} */
|
||
this._$animate = $injector.get('$animate');
|
||
|
||
/** @private @const {!MdPanelRef} */
|
||
this._$mdPanel = $injector.get('$mdPanel');
|
||
|
||
/** @private @const {!ILogService} */
|
||
this._$log = $injector.get('$log');
|
||
|
||
/** @private @const {!IWindowService} */
|
||
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;
|
||
|
||
/** @type {!Object} */
|
||
this.config = config;
|
||
|
||
/** @type {!JQLite|undefined} */
|
||
this.panelContainer = undefined;
|
||
|
||
/** @type {!JQLite|undefined} */
|
||
this.panelEl = undefined;
|
||
|
||
/** @type {!JQLite|undefined} */
|
||
this.innerWrapper = undefined;
|
||
|
||
/**
|
||
* 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 {Array<function()>} */
|
||
this._removeListeners = [];
|
||
|
||
/** @private {!JQLite|undefined} */
|
||
this._topFocusTrap = undefined;
|
||
|
||
/** @private {!JQLite|undefined} */
|
||
this._bottomFocusTrap = undefined;
|
||
|
||
/** @private {!$mdPanel|undefined} */
|
||
this._backdropRef = undefined;
|
||
|
||
/** @private {Function?} */
|
||
this._restoreScroll = null;
|
||
|
||
/**
|
||
* Keeps track of all the panel interceptors.
|
||
* @private {!Object}
|
||
*/
|
||
this._interceptors = Object.create(null);
|
||
|
||
/**
|
||
* Cleanup function, provided by `$mdCompiler` and assigned after the element
|
||
* has been compiled. When `contentElement` is used, the function is used to
|
||
* restore the element to it's proper place in the DOM.
|
||
* @private {Function|null}
|
||
*/
|
||
this._compilerCleanup = null;
|
||
|
||
/**
|
||
* Cache for saving and restoring element inline styles, CSS classes etc.
|
||
* @type {{styles: string, classes: string}}
|
||
*/
|
||
this._restoreCache = {
|
||
styles: '',
|
||
classes: ''
|
||
};
|
||
}
|
||
|
||
|
||
MdPanelRef.interceptorTypes = {
|
||
CLOSE: 'onClose'
|
||
};
|
||
|
||
|
||
/**
|
||
* Opens an already created and configured panel. If the panel is already
|
||
* visible, does nothing.
|
||
* @returns {!Q.IPromise<!MdPanelRef>} A promise that is resolved when
|
||
* the panel is opened and animations finish.
|
||
*/
|
||
MdPanelRef.prototype.open = function() {
|
||
var self = this;
|
||
return this._$q(function(resolve, reject) {
|
||
var done = self._done(resolve, self);
|
||
var show = self._simpleBind(self.show, self);
|
||
var checkGroupMaxOpen = function() {
|
||
if (self.config.groupName) {
|
||
self.config.groupName = coerceToArray(self.config.groupName);
|
||
angular.forEach(self.config.groupName, function(group) {
|
||
if (self._$mdPanel._openCountExceedsMaxOpen(group)) {
|
||
self._$mdPanel._closeFirstOpenedPanel(group);
|
||
}
|
||
});
|
||
}
|
||
};
|
||
|
||
self.attach()
|
||
.then(show)
|
||
.then(checkGroupMaxOpen)
|
||
.then(done)
|
||
.catch(reject);
|
||
});
|
||
};
|
||
|
||
|
||
/**
|
||
* Closes the panel.
|
||
* @param {string} closeReason The event type that triggered the close.
|
||
* @returns {!Q.IPromise<!MdPanelRef>} A promise that is resolved when
|
||
* the panel is closed and animations finish.
|
||
*/
|
||
MdPanelRef.prototype.close = function(closeReason) {
|
||
var self = this;
|
||
|
||
return this._$q(function(resolve, reject) {
|
||
self._callInterceptors(MdPanelRef.interceptorTypes.CLOSE).then(function() {
|
||
var done = self._done(resolve, self);
|
||
var detach = self._simpleBind(self.detach, self);
|
||
var onCloseSuccess = self.config['onCloseSuccess'] || angular.noop;
|
||
onCloseSuccess = angular.bind(self, onCloseSuccess, self, closeReason);
|
||
|
||
self.hide()
|
||
.then(detach)
|
||
.then(done)
|
||
.then(onCloseSuccess)
|
||
.catch(reject);
|
||
}, reject);
|
||
});
|
||
};
|
||
|
||
|
||
/**
|
||
* Attaches the panel. The panel will be hidden afterwards.
|
||
* @returns {!Q.IPromise<!MdPanelRef>} A promise that is resolved when
|
||
* the panel is attached.
|
||
*/
|
||
MdPanelRef.prototype.attach = function() {
|
||
if (this.isAttached && this.panelEl) {
|
||
return this._$q.when(this);
|
||
}
|
||
|
||
var self = this;
|
||
return this._$q(function(resolve, reject) {
|
||
var done = self._done(resolve, self);
|
||
var onDomAdded = self.config['onDomAdded'] || angular.noop;
|
||
var addListeners = function(response) {
|
||
self.isAttached = true;
|
||
self._addEventListeners();
|
||
return response;
|
||
};
|
||
|
||
self._$q.all([
|
||
self._createBackdrop(),
|
||
self._createPanel()
|
||
.then(addListeners)
|
||
.catch(reject)
|
||
]).then(onDomAdded)
|
||
.then(done)
|
||
.catch(reject);
|
||
});
|
||
};
|
||
|
||
|
||
/**
|
||
* Only detaches the panel. Will NOT hide the panel first.
|
||
* @returns {!Q.IPromise<!MdPanelRef>} A promise that is resolved when
|
||
* the panel is detached.
|
||
*/
|
||
MdPanelRef.prototype.detach = function() {
|
||
if (!this.isAttached) {
|
||
return this._$q.when(this);
|
||
}
|
||
|
||
var self = this;
|
||
var onDomRemoved = self.config['onDomRemoved'] || angular.noop;
|
||
|
||
var detachFn = function() {
|
||
self._removeEventListeners();
|
||
|
||
// Remove the focus traps that we added earlier for keeping focus within
|
||
// the panel.
|
||
if (self._topFocusTrap && self._topFocusTrap.parentNode) {
|
||
self._topFocusTrap.parentNode.removeChild(self._topFocusTrap);
|
||
}
|
||
|
||
if (self._bottomFocusTrap && self._bottomFocusTrap.parentNode) {
|
||
self._bottomFocusTrap.parentNode.removeChild(self._bottomFocusTrap);
|
||
}
|
||
|
||
if (self._restoreCache.classes) {
|
||
self.panelEl[0].className = self._restoreCache.classes;
|
||
}
|
||
|
||
// Either restore the saved styles or clear the ones set by mdPanel.
|
||
self.panelEl[0].style.cssText = self._restoreCache.styles || '';
|
||
|
||
self._compilerCleanup();
|
||
self.panelContainer.remove();
|
||
self.isAttached = false;
|
||
return self._$q.when(self);
|
||
};
|
||
|
||
if (this._restoreScroll) {
|
||
this._restoreScroll();
|
||
this._restoreScroll = null;
|
||
}
|
||
|
||
return this._$q(function(resolve, reject) {
|
||
var done = self._done(resolve, self);
|
||
|
||
self._$q.all([
|
||
detachFn(),
|
||
self._backdropRef ? self._backdropRef.detach() : true
|
||
]).then(onDomRemoved)
|
||
.then(done)
|
||
.catch(reject);
|
||
});
|
||
};
|
||
|
||
|
||
/**
|
||
* Destroys the panel. The Panel cannot be opened again after this.
|
||
*/
|
||
MdPanelRef.prototype.destroy = function() {
|
||
var self = this;
|
||
if (this.config.groupName) {
|
||
this.config.groupName = coerceToArray(this.config.groupName);
|
||
angular.forEach(this.config.groupName, function(group) {
|
||
self.removeFromGroup(group);
|
||
});
|
||
}
|
||
this.config.scope.$destroy();
|
||
this.config.locals = null;
|
||
this.config.onDomAdded = null;
|
||
this.config.onDomRemoved = null;
|
||
this.config.onRemoving = null;
|
||
this.config.onOpenComplete = null;
|
||
this._interceptors = undefined;
|
||
};
|
||
|
||
|
||
/**
|
||
* Shows the panel.
|
||
* @returns {!Q.IPromise<!MdPanelRef>} A promise that is resolved when
|
||
* the panel has shown and animations finish.
|
||
*/
|
||
MdPanelRef.prototype.show = function() {
|
||
if (!this.panelContainer) {
|
||
return this._$q(function(resolve, reject) {
|
||
reject('mdPanel: Panel does not exist yet. Call open() or attach().');
|
||
});
|
||
}
|
||
|
||
if (!this.panelContainer.hasClass(MD_PANEL_HIDDEN)) {
|
||
return this._$q.when(this);
|
||
}
|
||
|
||
var self = this;
|
||
var animatePromise = function() {
|
||
self.panelContainer.removeClass(MD_PANEL_HIDDEN);
|
||
return self._animateOpen();
|
||
};
|
||
|
||
return this._$q(function(resolve, reject) {
|
||
var done = self._done(resolve, self);
|
||
var onOpenComplete = self.config['onOpenComplete'] || angular.noop;
|
||
var addToGroupOpen = function() {
|
||
if (self.config.groupName) {
|
||
self.config.groupName = coerceToArray(self.config.groupName);
|
||
angular.forEach(self.config.groupName, function(group) {
|
||
group = self._$mdPanel._groups[group];
|
||
if (group) {
|
||
group.openPanels.push(self);
|
||
}
|
||
});
|
||
}
|
||
};
|
||
|
||
self._$q.all([
|
||
self._backdropRef ? self._backdropRef.show() : self,
|
||
animatePromise().then(function() { self._focusOnOpen(); }, reject)
|
||
]).then(onOpenComplete)
|
||
.then(addToGroupOpen)
|
||
.then(done)
|
||
.catch(reject);
|
||
});
|
||
};
|
||
|
||
|
||
/**
|
||
* Hides the panel.
|
||
* @returns {!Q.IPromise<!MdPanelRef>} A promise that is resolved when
|
||
* the panel has hidden and animations finish.
|
||
*/
|
||
MdPanelRef.prototype.hide = function() {
|
||
if (!this.panelContainer) {
|
||
return this._$q(function(resolve, reject) {
|
||
reject('mdPanel: Panel does not exist yet. Call open() or attach().');
|
||
});
|
||
}
|
||
|
||
if (this.panelContainer.hasClass(MD_PANEL_HIDDEN)) {
|
||
return this._$q.when(this);
|
||
}
|
||
|
||
var self = this;
|
||
|
||
return this._$q(function(resolve, reject) {
|
||
var done = self._done(resolve, self);
|
||
var onRemoving = self.config['onRemoving'] || angular.noop;
|
||
var hidePanel = function() {
|
||
self.panelContainer.addClass(MD_PANEL_HIDDEN);
|
||
};
|
||
var removeFromGroupOpen = function() {
|
||
if (self.config.groupName) {
|
||
var index;
|
||
self.config.groupName = coerceToArray(self.config.groupName);
|
||
angular.forEach(self.config.groupName, function(group) {
|
||
group = self._$mdPanel._groups[group];
|
||
index = group.openPanels.indexOf(self);
|
||
if (index > -1) {
|
||
group.openPanels.splice(index, 1);
|
||
}
|
||
});
|
||
}
|
||
};
|
||
var focusOnOrigin = function() {
|
||
var origin = self.config['origin'];
|
||
if (origin) {
|
||
getElement(origin).focus();
|
||
}
|
||
};
|
||
|
||
self._$q.all([
|
||
self._backdropRef ? self._backdropRef.hide() : self,
|
||
self._animateClose()
|
||
.then(onRemoving)
|
||
.then(hidePanel)
|
||
.then(removeFromGroupOpen)
|
||
.then(focusOnOrigin)
|
||
.catch(reject)
|
||
]).then(done, reject);
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Compiles the panel, according to the passed in config and appends it to
|
||
* the DOM. Helps normalize differences in the compilation process between
|
||
* using a string template and a content element.
|
||
* @returns {!Q.IPromise<!MdPanelRef>} Promise that is resolved when
|
||
* the element has been compiled and added to the DOM.
|
||
* @private
|
||
*/
|
||
MdPanelRef.prototype._compile = function() {
|
||
var self = this;
|
||
|
||
// Compile the element via $mdCompiler. Note that when using a
|
||
// contentElement, the element isn't actually being compiled, rather the
|
||
// compiler saves it's place in the DOM and provides a way of restoring it.
|
||
return self._$mdCompiler.compile(self.config).then(function(compileData) {
|
||
var config = self.config;
|
||
|
||
if (config.contentElement) {
|
||
var panelEl = compileData.element;
|
||
|
||
// Since mdPanel modifies the inline styles and CSS classes, we need
|
||
// to save them in order to be able to restore on close.
|
||
self._restoreCache.styles = panelEl[0].style.cssText;
|
||
self._restoreCache.classes = panelEl[0].className;
|
||
|
||
self.panelContainer = self._$mdPanel._wrapContentElement(panelEl);
|
||
self.panelEl = panelEl;
|
||
} else {
|
||
self.panelContainer = compileData.link(config['scope']);
|
||
self.panelEl = angular.element(
|
||
self.panelContainer[0].querySelector('.md-panel')
|
||
);
|
||
}
|
||
|
||
// Save a reference to the inner wrapper.
|
||
self.innerWrapper = angular.element(
|
||
self.panelContainer[0].querySelector('.md-panel-inner-wrapper')
|
||
);
|
||
|
||
// Save a reference to the cleanup function from the compiler.
|
||
self._compilerCleanup = compileData.cleanup;
|
||
|
||
// Attach the panel to the proper place in the DOM.
|
||
getElement(self.config['attachTo']).append(self.panelContainer);
|
||
|
||
return self;
|
||
});
|
||
};
|
||
|
||
|
||
/**
|
||
* Creates a panel and adds it to the dom.
|
||
* @returns {!Q.IPromise} A promise that is resolved when the panel is
|
||
* created.
|
||
* @private
|
||
*/
|
||
MdPanelRef.prototype._createPanel = function() {
|
||
var self = this;
|
||
|
||
return this._$q(function(resolve, reject) {
|
||
if (!self.config.locals) {
|
||
self.config.locals = {};
|
||
}
|
||
|
||
self.config.locals.mdPanelRef = self;
|
||
|
||
self._compile().then(function() {
|
||
if (self.config['disableParentScroll']) {
|
||
self._restoreScroll = self._$mdUtil.disableScrollAround(
|
||
null,
|
||
self.panelContainer,
|
||
{ disableScrollMask: true }
|
||
);
|
||
}
|
||
|
||
// Add a custom CSS class to the panel element.
|
||
if (self.config['panelClass']) {
|
||
self.panelEl.addClass(self.config['panelClass']);
|
||
}
|
||
|
||
// Handle click and touch events for the panel container.
|
||
if (self.config['propagateContainerEvents']) {
|
||
self.panelContainer.css('pointer-events', 'none');
|
||
self.panelEl.css('pointer-events', 'all');
|
||
}
|
||
|
||
// Panel may be outside the $rootElement, tell ngAnimate to animate
|
||
// regardless.
|
||
if (self._$animate.pin) {
|
||
self._$animate.pin(
|
||
self.panelContainer,
|
||
getElement(self.config['attachTo'])
|
||
);
|
||
}
|
||
|
||
self._configureTrapFocus();
|
||
self._addStyles().then(function() {
|
||
resolve(self);
|
||
}, reject);
|
||
}, reject);
|
||
|
||
});
|
||
};
|
||
|
||
|
||
/**
|
||
* Adds the styles for the panel, such as positioning and z-index. Also,
|
||
* themes the panel element and panel container using `$mdTheming`.
|
||
* @returns {!Q.IPromise<!MdPanelRef>}
|
||
* @private
|
||
*/
|
||
MdPanelRef.prototype._addStyles = function() {
|
||
var self = this;
|
||
return this._$q(function(resolve) {
|
||
self.panelContainer.css('z-index', self.config['zIndex']);
|
||
self.innerWrapper.css('z-index', self.config['zIndex'] + 1);
|
||
|
||
var hideAndResolve = function() {
|
||
// Theme the element and container.
|
||
self._setTheming();
|
||
|
||
// Remove offscreen classes and add hidden class.
|
||
self.panelEl.removeClass('_md-panel-offscreen');
|
||
self.innerWrapper.removeClass('_md-panel-offscreen');
|
||
self.panelContainer.addClass(MD_PANEL_HIDDEN);
|
||
|
||
resolve(self);
|
||
};
|
||
|
||
if (self.config['fullscreen']) {
|
||
self.panelEl.addClass('_md-panel-fullscreen');
|
||
hideAndResolve();
|
||
return; // Don't setup positioning.
|
||
}
|
||
|
||
var positionConfig = self.config['position'];
|
||
if (!positionConfig) {
|
||
hideAndResolve();
|
||
return; // Don't setup positioning.
|
||
}
|
||
|
||
// Wait for angular to finish processing the template
|
||
self._$rootScope['$$postDigest'](function() {
|
||
// Position it correctly. This is necessary so that the panel will have a
|
||
// defined height and width.
|
||
self._updatePosition(true);
|
||
|
||
// Theme the element and container.
|
||
self._setTheming();
|
||
|
||
resolve(self);
|
||
});
|
||
});
|
||
};
|
||
|
||
|
||
/**
|
||
* Sets the `$mdTheming` classes on the `panelContainer` and `panelEl`.
|
||
* @private
|
||
*/
|
||
MdPanelRef.prototype._setTheming = function() {
|
||
this._$mdTheming(this.panelEl);
|
||
this._$mdTheming(this.panelContainer);
|
||
};
|
||
|
||
|
||
/**
|
||
* Updates the position configuration of a panel
|
||
* @param {!MdPanelPosition} position
|
||
*/
|
||
MdPanelRef.prototype.updatePosition = function(position) {
|
||
if (!this.panelContainer) {
|
||
throw new Error(
|
||
'mdPanel: Panel does not exist yet. Call open() or attach().');
|
||
}
|
||
|
||
this.config['position'] = position;
|
||
this._updatePosition();
|
||
};
|
||
|
||
|
||
/**
|
||
* Calculates and updates the position of the panel.
|
||
* @param {boolean=} init
|
||
* @private
|
||
*/
|
||
MdPanelRef.prototype._updatePosition = function(init) {
|
||
var positionConfig = this.config['position'];
|
||
|
||
if (positionConfig) {
|
||
positionConfig._setPanelPosition(this.innerWrapper);
|
||
|
||
// Hide the panel now that position is known.
|
||
if (init) {
|
||
this.panelEl.removeClass('_md-panel-offscreen');
|
||
this.innerWrapper.removeClass('_md-panel-offscreen');
|
||
this.panelContainer.addClass(MD_PANEL_HIDDEN);
|
||
}
|
||
|
||
this.innerWrapper.css(
|
||
MdPanelPosition.absPosition.TOP,
|
||
positionConfig.getTop()
|
||
);
|
||
this.innerWrapper.css(
|
||
MdPanelPosition.absPosition.BOTTOM,
|
||
positionConfig.getBottom()
|
||
);
|
||
this.innerWrapper.css(
|
||
MdPanelPosition.absPosition.LEFT,
|
||
positionConfig.getLeft()
|
||
);
|
||
this.innerWrapper.css(
|
||
MdPanelPosition.absPosition.RIGHT,
|
||
positionConfig.getRight()
|
||
);
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* Focuses on the panel or the first focus target.
|
||
* @private
|
||
*/
|
||
MdPanelRef.prototype._focusOnOpen = function() {
|
||
if (this.config['focusOnOpen']) {
|
||
// Wait for the template to finish rendering to guarantee md-autofocus has
|
||
// finished adding the class md-autofocus, otherwise the focusable element
|
||
// isn't available to focus.
|
||
var self = this;
|
||
this._$rootScope['$$postDigest'](function() {
|
||
var target = self._$mdUtil.findFocusTarget(self.panelEl) ||
|
||
self.panelEl;
|
||
target.focus();
|
||
});
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* Shows the backdrop.
|
||
* @returns {!Q.IPromise} A promise that is resolved when the backdrop
|
||
* is created and attached.
|
||
* @private
|
||
*/
|
||
MdPanelRef.prototype._createBackdrop = function() {
|
||
if (this.config.hasBackdrop) {
|
||
if (!this._backdropRef) {
|
||
var backdropAnimation = this._$mdPanel.newPanelAnimation()
|
||
.openFrom(this.config.attachTo)
|
||
.withAnimation({
|
||
open: '_md-opaque-enter',
|
||
close: '_md-opaque-leave'
|
||
});
|
||
|
||
if (this.config.animation) {
|
||
backdropAnimation.duration(this.config.animation._rawDuration);
|
||
}
|
||
|
||
var backdropConfig = {
|
||
animation: backdropAnimation,
|
||
attachTo: this.config.attachTo,
|
||
focusOnOpen: false,
|
||
panelClass: '_md-panel-backdrop',
|
||
zIndex: this.config.zIndex - 1
|
||
};
|
||
|
||
this._backdropRef = this._$mdPanel.create(backdropConfig);
|
||
}
|
||
if (!this._backdropRef.isAttached) {
|
||
return this._backdropRef.attach();
|
||
}
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* Listen for escape keys and outside clicks to auto close.
|
||
* @private
|
||
*/
|
||
MdPanelRef.prototype._addEventListeners = function() {
|
||
this._configureEscapeToClose();
|
||
this._configureClickOutsideToClose();
|
||
this._configureScrollListener();
|
||
};
|
||
|
||
|
||
/**
|
||
* Remove event listeners added in _addEventListeners.
|
||
* @private
|
||
*/
|
||
MdPanelRef.prototype._removeEventListeners = function() {
|
||
this._removeListeners && this._removeListeners.forEach(function(removeFn) {
|
||
removeFn();
|
||
});
|
||
this._removeListeners = [];
|
||
};
|
||
|
||
|
||
/**
|
||
* Setup the escapeToClose event listeners.
|
||
* @private
|
||
*/
|
||
MdPanelRef.prototype._configureEscapeToClose = function() {
|
||
if (this.config['escapeToClose']) {
|
||
var parentTarget = getElement(this.config['attachTo']);
|
||
var self = this;
|
||
|
||
var keyHandlerFn = function(ev) {
|
||
if (ev.keyCode === self._$mdConstant.KEY_CODE.ESCAPE) {
|
||
ev.stopPropagation();
|
||
ev.preventDefault();
|
||
|
||
self.close(MdPanelRef.closeReasons.ESCAPE);
|
||
}
|
||
};
|
||
|
||
// Add keydown listeners
|
||
this.panelContainer.on('keydown', keyHandlerFn);
|
||
parentTarget.on('keydown', keyHandlerFn);
|
||
|
||
// Queue remove listeners function
|
||
this._removeListeners.push(function() {
|
||
self.panelContainer.off('keydown', keyHandlerFn);
|
||
parentTarget.off('keydown', keyHandlerFn);
|
||
});
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* Setup the clickOutsideToClose event listeners.
|
||
* @private
|
||
*/
|
||
MdPanelRef.prototype._configureClickOutsideToClose = function() {
|
||
if (this.config['clickOutsideToClose']) {
|
||
var target = this.config['propagateContainerEvents'] ?
|
||
angular.element(document.body) :
|
||
this.panelContainer;
|
||
var sourceEl;
|
||
|
||
// Keep track of the element on which the mouse originally went down
|
||
// so that we can only close the backdrop when the 'click' started on it.
|
||
// A simple 'click' handler does not work, it sets the target object as the
|
||
// element the mouse went down on.
|
||
var mousedownHandler = function(ev) {
|
||
sourceEl = ev.target;
|
||
};
|
||
|
||
// We check if our original element and the target is the backdrop
|
||
// because if the original was the backdrop and the target was inside the
|
||
// panel we don't want to panel to close.
|
||
var self = this;
|
||
var mouseupHandler = function(ev) {
|
||
if (self.config['propagateContainerEvents']) {
|
||
|
||
// We check if the sourceEl of the event is the panel element or one
|
||
// of it's children. If it is not, then close the panel.
|
||
if (sourceEl !== self.panelEl[0] && !self.panelEl[0].contains(sourceEl)) {
|
||
self.close();
|
||
}
|
||
|
||
} else if (sourceEl === target[0] && ev.target === target[0]) {
|
||
ev.stopPropagation();
|
||
ev.preventDefault();
|
||
|
||
self.close(MdPanelRef.closeReasons.CLICK_OUTSIDE);
|
||
}
|
||
};
|
||
|
||
// Add listeners
|
||
target.on('mousedown', mousedownHandler);
|
||
target.on('mouseup', mouseupHandler);
|
||
|
||
// Queue remove listeners function
|
||
this._removeListeners.push(function() {
|
||
target.off('mousedown', mousedownHandler);
|
||
target.off('mouseup', mouseupHandler);
|
||
});
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* Configures the listeners for updating the panel position on scroll.
|
||
* @private
|
||
*/
|
||
MdPanelRef.prototype._configureScrollListener = function() {
|
||
// No need to bind the event if scrolling is disabled.
|
||
if (!this.config['disableParentScroll']) {
|
||
var updatePosition = angular.bind(this, this._updatePosition);
|
||
var debouncedUpdatePosition = this._$$rAF.throttle(updatePosition);
|
||
var self = this;
|
||
|
||
var onScroll = function() {
|
||
debouncedUpdatePosition();
|
||
};
|
||
|
||
// Add listeners.
|
||
this._$window.addEventListener('scroll', onScroll, true);
|
||
|
||
// Queue remove listeners function.
|
||
this._removeListeners.push(function() {
|
||
self._$window.removeEventListener('scroll', onScroll, true);
|
||
});
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* Setup the focus traps. These traps will wrap focus when tabbing past the
|
||
* panel. When shift-tabbing, the focus will stick in place.
|
||
* @private
|
||
*/
|
||
MdPanelRef.prototype._configureTrapFocus = function() {
|
||
// Focus doesn't remain inside of the panel without this.
|
||
this.panelEl.attr('tabIndex', '-1');
|
||
if (this.config['trapFocus']) {
|
||
var element = this.panelEl;
|
||
// Set up elements before and after the panel to capture focus and
|
||
// redirect back into the panel.
|
||
if (!FOCUS_TRAP_TEMPLATE) {
|
||
var template = document.createElement('div');
|
||
template.className = '_md-panel-focus-trap';
|
||
template.tabIndex = 0;
|
||
FOCUS_TRAP_TEMPLATE = angular.element(template);
|
||
}
|
||
this._topFocusTrap = FOCUS_TRAP_TEMPLATE.clone()[0];
|
||
this._bottomFocusTrap = FOCUS_TRAP_TEMPLATE.clone()[0];
|
||
|
||
// When focus is about to move out of the panel, we want to intercept it
|
||
// and redirect it back to the panel element.
|
||
var focusHandler = function() {
|
||
element.focus();
|
||
};
|
||
this._topFocusTrap.addEventListener('focus', focusHandler);
|
||
this._bottomFocusTrap.addEventListener('focus', focusHandler);
|
||
|
||
// Queue remove listeners function
|
||
this._removeListeners.push(this._simpleBind(function() {
|
||
this._topFocusTrap.removeEventListener('focus', focusHandler);
|
||
this._bottomFocusTrap.removeEventListener('focus', focusHandler);
|
||
}, this));
|
||
|
||
// The top focus trap inserted immediately before the md-panel element (as
|
||
// a sibling). The bottom focus trap inserted immediately after the
|
||
// md-panel element (as a sibling).
|
||
element[0].parentNode.insertBefore(this._topFocusTrap, element[0]);
|
||
element.after(this._bottomFocusTrap);
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* Updates the animation of a panel.
|
||
* @param {!MdPanelAnimation} animation
|
||
*/
|
||
MdPanelRef.prototype.updateAnimation = function(animation) {
|
||
this.config['animation'] = animation;
|
||
|
||
if (this._backdropRef) {
|
||
this._backdropRef.config.animation.duration(animation._rawDuration);
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* Animate the panel opening.
|
||
* @returns {!Q.IPromise} A promise that is resolved when the panel has
|
||
* animated open.
|
||
* @private
|
||
*/
|
||
MdPanelRef.prototype._animateOpen = function() {
|
||
this.panelContainer.addClass('md-panel-is-showing');
|
||
var animationConfig = this.config['animation'];
|
||
if (!animationConfig) {
|
||
// Promise is in progress, return it.
|
||
this.panelContainer.addClass('_md-panel-shown');
|
||
return this._$q.when(this);
|
||
}
|
||
|
||
var self = this;
|
||
return this._$q(function(resolve) {
|
||
var done = self._done(resolve, self);
|
||
var warnAndOpen = function() {
|
||
self._$log.warn(
|
||
'mdPanel: MdPanel Animations failed. ' +
|
||
'Showing panel without animating.');
|
||
done();
|
||
};
|
||
|
||
animationConfig.animateOpen(self.panelEl)
|
||
.then(done, warnAndOpen);
|
||
});
|
||
};
|
||
|
||
|
||
/**
|
||
* Animate the panel closing.
|
||
* @returns {!Q.IPromise} A promise that is resolved when the panel has animated closed.
|
||
* @private
|
||
*/
|
||
MdPanelRef.prototype._animateClose = function() {
|
||
var self = this;
|
||
var animationConfig = this.config['animation'];
|
||
|
||
if (!animationConfig) {
|
||
this.panelContainer.removeClass('md-panel-is-showing');
|
||
this.panelContainer.removeClass('_md-panel-shown');
|
||
return this._$q.when(this);
|
||
} else {
|
||
return this._$q(function (resolve) {
|
||
var done = function () {
|
||
self.panelContainer.removeClass('md-panel-is-showing');
|
||
// Remove the transform so that re-used panels don't accumulate transforms.
|
||
self.panelEl.css('transform', '');
|
||
resolve(self);
|
||
};
|
||
var warnAndClose = function () {
|
||
self._$log.warn(
|
||
'mdPanel: MdPanel Animations failed. Hiding panel without animating.');
|
||
done();
|
||
};
|
||
|
||
animationConfig.animateClose(self.panelEl).then(done, warnAndClose);
|
||
});
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* Registers a interceptor with the panel. The callback should return a promise,
|
||
* which will allow the action to continue when it gets resolved, or will
|
||
* prevent an action if it is rejected.
|
||
* @param {string} type Type of interceptor.
|
||
* @param {!Q.IPromise<!any>} callback Callback to be registered.
|
||
* @returns {!MdPanelRef}
|
||
*/
|
||
MdPanelRef.prototype.registerInterceptor = function(type, callback) {
|
||
var error = null;
|
||
|
||
if (!angular.isString(type)) {
|
||
error = 'Interceptor type must be a string, instead got ' + typeof type;
|
||
} else if (!angular.isFunction(callback)) {
|
||
error = 'Interceptor callback must be a function, instead got ' + typeof callback;
|
||
}
|
||
|
||
if (error) {
|
||
throw new Error('MdPanel: ' + error);
|
||
}
|
||
|
||
var interceptors = this._interceptors[type] = this._interceptors[type] || [];
|
||
|
||
if (interceptors.indexOf(callback) === -1) {
|
||
interceptors.push(callback);
|
||
}
|
||
|
||
return this;
|
||
};
|
||
|
||
|
||
/**
|
||
* Removes a registered interceptor.
|
||
* @param {string} type Type of interceptor to be removed.
|
||
* @param {Function} callback Interceptor to be removed.
|
||
* @returns {!MdPanelRef}
|
||
*/
|
||
MdPanelRef.prototype.removeInterceptor = function(type, callback) {
|
||
var index = this._interceptors[type] ?
|
||
this._interceptors[type].indexOf(callback) : -1;
|
||
|
||
if (index > -1) {
|
||
this._interceptors[type].splice(index, 1);
|
||
}
|
||
|
||
return this;
|
||
};
|
||
|
||
|
||
/**
|
||
* Removes all interceptors.
|
||
* @param {string=} type Type of interceptors to be removed.
|
||
* If ommited, all interceptors types will be removed.
|
||
* @returns {!MdPanelRef}
|
||
*/
|
||
MdPanelRef.prototype.removeAllInterceptors = function(type) {
|
||
if (type) {
|
||
this._interceptors[type] = [];
|
||
} else {
|
||
this._interceptors = Object.create(null);
|
||
}
|
||
|
||
return this;
|
||
};
|
||
|
||
|
||
/**
|
||
* Invokes all the interceptors of a certain type sequantially in
|
||
* reverse order. Works in a similar way to `$q.all`, except it
|
||
* respects the order of the functions.
|
||
* @param {string} type Type of interceptors to be invoked.
|
||
* @returns {!Q.IPromise<!MdPanelRef>}
|
||
* @private
|
||
*/
|
||
MdPanelRef.prototype._callInterceptors = function(type) {
|
||
var self = this;
|
||
var $q = self._$q;
|
||
var interceptors = self._interceptors && self._interceptors[type] || [];
|
||
|
||
return interceptors.reduceRight(function(promise, interceptor) {
|
||
var isPromiseLike = interceptor && angular.isFunction(interceptor.then);
|
||
var response = isPromiseLike ? interceptor : null;
|
||
|
||
/**
|
||
* For interceptors to reject/cancel subsequent portions of the chain, simply
|
||
* return a `$q.reject(<value>)`
|
||
*/
|
||
return promise.then(function() {
|
||
if (!response) {
|
||
try {
|
||
response = interceptor(self);
|
||
} catch (e) {
|
||
response = $q.reject(e);
|
||
}
|
||
}
|
||
|
||
return response;
|
||
});
|
||
}, $q.resolve(self));
|
||
};
|
||
|
||
|
||
/**
|
||
* Faster, more basic than angular.bind
|
||
* http://jsperf.com/angular-bind-vs-custom-vs-native
|
||
* @param {function} callback
|
||
* @param {!Object} self
|
||
* @return {function} Callback function with a bound self.
|
||
*/
|
||
MdPanelRef.prototype._simpleBind = function(callback, self) {
|
||
return function(value) {
|
||
return callback.apply(self, value);
|
||
};
|
||
};
|
||
|
||
|
||
/**
|
||
* @param {function|IQResolveReject} callback
|
||
* @param {!Object} self
|
||
* @return {function} Callback function with a self param.
|
||
*/
|
||
MdPanelRef.prototype._done = function(callback, self) {
|
||
return function() {
|
||
callback(self);
|
||
};
|
||
};
|
||
|
||
|
||
/**
|
||
* Adds a panel to a group if the panel does not exist within the group already.
|
||
* A panel can only exist within a single group.
|
||
* @param {string} groupName The name of the group.
|
||
*/
|
||
MdPanelRef.prototype.addToGroup = function(groupName) {
|
||
if (!this._$mdPanel._groups[groupName]) {
|
||
this._$mdPanel.newPanelGroup(groupName);
|
||
}
|
||
|
||
var group = this._$mdPanel._groups[groupName];
|
||
var index = group.panels.indexOf(this);
|
||
|
||
if (index < 0) {
|
||
group.panels.push(this);
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* Removes a panel from a group if the panel exists within that group. The group
|
||
* must be created ahead of time.
|
||
* @param {string} groupName The name of the group.
|
||
*/
|
||
MdPanelRef.prototype.removeFromGroup = function(groupName) {
|
||
if (!this._$mdPanel._groups[groupName]) {
|
||
throw new Error('mdPanel: The group ' + groupName + ' does not exist.');
|
||
}
|
||
|
||
var group = this._$mdPanel._groups[groupName];
|
||
var index = group.panels.indexOf(this);
|
||
|
||
if (index > -1) {
|
||
group.panels.splice(index, 1);
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* Possible default closeReasons for the close function.
|
||
* @enum {string}
|
||
*/
|
||
MdPanelRef.closeReasons = {
|
||
CLICK_OUTSIDE: 'clickOutsideToClose',
|
||
ESCAPE: 'escapeToClose',
|
||
};
|
||
|
||
|
||
/*****************************************************************************
|
||
* MdPanelPosition *
|
||
*****************************************************************************/
|
||
|
||
|
||
/**
|
||
* Position configuration object. To use, create an MdPanelPosition with the
|
||
* desired properties, then pass the object as part of $mdPanel creation.
|
||
*
|
||
* Example:
|
||
*
|
||
* var panelPosition = new MdPanelPosition()
|
||
* .relativeTo(myButtonEl)
|
||
* .addPanelPosition(
|
||
* $mdPanel.xPosition.CENTER,
|
||
* $mdPanel.yPosition.ALIGN_TOPS
|
||
* );
|
||
*
|
||
* $mdPanel.create({
|
||
* position: panelPosition
|
||
* });
|
||
*
|
||
* @param {!IInjectorService} $injector
|
||
* @final @constructor
|
||
*/
|
||
function MdPanelPosition($injector) {
|
||
/** @private @const {!IWindowService} */
|
||
this._$window = $injector.get('$window');
|
||
|
||
/** @private {boolean} */
|
||
this._isRTL = $injector.get('$mdUtil').isRtl();
|
||
|
||
/** @private @const {!angular.$mdConstant} */
|
||
this._$mdConstant = $injector.get('$mdConstant');
|
||
|
||
/** @private {boolean} */
|
||
this._absolute = false;
|
||
|
||
/** @private {!JQLite} */
|
||
this._relativeToEl = undefined;
|
||
|
||
/** @private {string} */
|
||
this._top = '';
|
||
|
||
/** @private {string} */
|
||
this._bottom = '';
|
||
|
||
/** @private {string} */
|
||
this._left = '';
|
||
|
||
/** @private {string} */
|
||
this._right = '';
|
||
|
||
/** @private {!Array<string>} */
|
||
this._translateX = [];
|
||
|
||
/** @private {!Array<string>} */
|
||
this._translateY = [];
|
||
|
||
/** @private {!Array<{x:string, y:string}>} */
|
||
this._positions = [];
|
||
|
||
/** @private {?{x:string, y:string}} */
|
||
this._actualPosition = undefined;
|
||
}
|
||
|
||
|
||
/**
|
||
* Possible values of xPosition.
|
||
* @enum {string}
|
||
*/
|
||
MdPanelPosition.xPosition = {
|
||
CENTER: 'center',
|
||
ALIGN_START: 'align-start',
|
||
ALIGN_END: 'align-end',
|
||
OFFSET_START: 'offset-start',
|
||
OFFSET_END: 'offset-end'
|
||
};
|
||
|
||
|
||
/**
|
||
* Possible values of yPosition.
|
||
* @enum {string}
|
||
*/
|
||
MdPanelPosition.yPosition = {
|
||
CENTER: 'center',
|
||
ALIGN_TOPS: 'align-tops',
|
||
ALIGN_BOTTOMS: 'align-bottoms',
|
||
ABOVE: 'above',
|
||
BELOW: 'below'
|
||
};
|
||
|
||
|
||
/**
|
||
* Possible values of absolute position.
|
||
* @enum {string}
|
||
*/
|
||
MdPanelPosition.absPosition = {
|
||
TOP: 'top',
|
||
RIGHT: 'right',
|
||
BOTTOM: 'bottom',
|
||
LEFT: 'left'
|
||
};
|
||
|
||
/**
|
||
* Margin between the edges of a panel and the viewport.
|
||
* @const {number}
|
||
*/
|
||
MdPanelPosition.viewportMargin = 8;
|
||
|
||
|
||
/**
|
||
* Sets absolute positioning for the panel.
|
||
* @return {!MdPanelPosition}
|
||
*/
|
||
MdPanelPosition.prototype.absolute = function() {
|
||
this._absolute = true;
|
||
return this;
|
||
};
|
||
|
||
|
||
/**
|
||
* Sets the value of a position for the panel. Clears any previously set
|
||
* position.
|
||
* @param {string} position Position to set
|
||
* @param {string=} value Value of the position. Defaults to '0'.
|
||
* @returns {!MdPanelPosition}
|
||
* @private
|
||
*/
|
||
MdPanelPosition.prototype._setPosition = function(position, value) {
|
||
if (position === MdPanelPosition.absPosition.RIGHT ||
|
||
position === MdPanelPosition.absPosition.LEFT) {
|
||
this._left = this._right = '';
|
||
} else if (
|
||
position === MdPanelPosition.absPosition.BOTTOM ||
|
||
position === MdPanelPosition.absPosition.TOP) {
|
||
this._top = this._bottom = '';
|
||
} else {
|
||
var positions = Object.keys(MdPanelPosition.absPosition).join()
|
||
.toLowerCase();
|
||
|
||
throw new Error('mdPanel: Position must be one of ' + positions + '.');
|
||
}
|
||
|
||
this['_' + position] = angular.isString(value) ? value : '0';
|
||
|
||
return this;
|
||
};
|
||
|
||
|
||
/**
|
||
* 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}
|
||
*/
|
||
MdPanelPosition.prototype.top = function(top) {
|
||
return this._setPosition(MdPanelPosition.absPosition.TOP, top);
|
||
};
|
||
|
||
|
||
/**
|
||
* 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}
|
||
*/
|
||
MdPanelPosition.prototype.bottom = function(bottom) {
|
||
return this._setPosition(MdPanelPosition.absPosition.BOTTOM, bottom);
|
||
};
|
||
|
||
|
||
/**
|
||
* 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}
|
||
*/
|
||
MdPanelPosition.prototype.start = function(start) {
|
||
var position = this._isRTL ? MdPanelPosition.absPosition.RIGHT : MdPanelPosition.absPosition.LEFT;
|
||
return this._setPosition(position, start);
|
||
};
|
||
|
||
|
||
/**
|
||
* 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}
|
||
*/
|
||
MdPanelPosition.prototype.end = function(end) {
|
||
var position = this._isRTL ? MdPanelPosition.absPosition.LEFT : MdPanelPosition.absPosition.RIGHT;
|
||
return this._setPosition(position, end);
|
||
};
|
||
|
||
|
||
/**
|
||
* 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}
|
||
*/
|
||
MdPanelPosition.prototype.left = function(left) {
|
||
return this._setPosition(MdPanelPosition.absPosition.LEFT, left);
|
||
};
|
||
|
||
|
||
/**
|
||
* 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}
|
||
*/
|
||
MdPanelPosition.prototype.right = function(right) {
|
||
return this._setPosition(MdPanelPosition.absPosition.RIGHT, right);
|
||
};
|
||
|
||
|
||
/**
|
||
* 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;
|
||
};
|
||
|
||
|
||
/**
|
||
* 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();
|
||
};
|
||
|
||
|
||
/**
|
||
* Sets element for relative positioning.
|
||
* @param {string|!Element|!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;
|
||
};
|
||
|
||
|
||
/**
|
||
* 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('mdPanel: addPanelPosition can only be used with ' +
|
||
'relative positioning. Set relativeTo first.');
|
||
}
|
||
|
||
validatePosition(MdPanelPosition.xPosition, xPosition);
|
||
validatePosition(MdPanelPosition.yPosition, yPosition);
|
||
|
||
this._positions.push({
|
||
x: xPosition,
|
||
y: yPosition
|
||
});
|
||
|
||
return this;
|
||
};
|
||
|
||
|
||
/**
|
||
* Sets the value of the offset in the x-direction. This will add to any
|
||
* previously set offsets.
|
||
* @param {string|number|function(MdPanelPosition): string} offsetX
|
||
* @returns {!MdPanelPosition}
|
||
*/
|
||
MdPanelPosition.prototype.withOffsetX = function(offsetX) {
|
||
this._translateX.push(addUnits(offsetX));
|
||
return this;
|
||
};
|
||
|
||
|
||
/**
|
||
* Sets the value of the offset in the y-direction. This will add to any
|
||
* previously set offsets.
|
||
* @param {string|number|function(MdPanelPosition): string} offsetY
|
||
* @returns {!MdPanelPosition}
|
||
*/
|
||
MdPanelPosition.prototype.withOffsetY = function(offsetY) {
|
||
this._translateY.push(addUnits(offsetY));
|
||
return this;
|
||
};
|
||
|
||
|
||
/**
|
||
* Gets the value of `top` for the panel.
|
||
* @returns {string}
|
||
*/
|
||
MdPanelPosition.prototype.getTop = function() {
|
||
return this._top;
|
||
};
|
||
|
||
|
||
/**
|
||
* Gets the value of `bottom` for the panel.
|
||
* @returns {string}
|
||
*/
|
||
MdPanelPosition.prototype.getBottom = function() {
|
||
return this._bottom;
|
||
};
|
||
|
||
|
||
/**
|
||
* Gets the value of `left` for the panel.
|
||
* @returns {string}
|
||
*/
|
||
MdPanelPosition.prototype.getLeft = function() {
|
||
return this._left;
|
||
};
|
||
|
||
|
||
/**
|
||
* Gets the value of `right` for the panel.
|
||
* @returns {string}
|
||
*/
|
||
MdPanelPosition.prototype.getRight = function() {
|
||
return this._right;
|
||
};
|
||
|
||
|
||
/**
|
||
* Gets the value of `transform` for the panel.
|
||
* @returns {string} representation of the translateX and translateY rules and values
|
||
*/
|
||
MdPanelPosition.prototype.getTransform = function() {
|
||
var translateX = this._reduceTranslateValues('translateX', this._translateX);
|
||
var translateY = this._reduceTranslateValues('translateY', this._translateY);
|
||
|
||
// 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();
|
||
};
|
||
|
||
|
||
/**
|
||
* Sets the `transform` value for an element.
|
||
* @param {!JQLite} el
|
||
* @returns {!JQLite}
|
||
* @private
|
||
*/
|
||
MdPanelPosition.prototype._setTransform = function(el) {
|
||
return el.css(this._$mdConstant.CSS.TRANSFORM, this.getTransform());
|
||
};
|
||
|
||
|
||
/**
|
||
* True if the panel is completely on-screen with this positioning; false
|
||
* otherwise.
|
||
* @param {!JQLite} el
|
||
* @return {boolean}
|
||
* @private
|
||
*/
|
||
MdPanelPosition.prototype._isOnscreen = function(el) {
|
||
// this works because we always use fixed positioning for the panel,
|
||
// which is relative to the viewport.
|
||
var left = parseInt(this.getLeft());
|
||
var top = parseInt(this.getTop());
|
||
|
||
if (this._translateX.length || this._translateY.length) {
|
||
var prefixedTransform = this._$mdConstant.CSS.TRANSFORM;
|
||
var offsets = getComputedTranslations(el, prefixedTransform);
|
||
left += offsets.x;
|
||
top += offsets.y;
|
||
}
|
||
|
||
var right = left + el[0].offsetWidth;
|
||
var bottom = top + el[0].offsetHeight;
|
||
|
||
return (left >= 0) &&
|
||
(top >= 0) &&
|
||
(bottom <= this._$window.innerHeight) &&
|
||
(right <= this._$window.innerWidth);
|
||
};
|
||
|
||
|
||
/**
|
||
* Gets the first x/y position that can fit on-screen.
|
||
* @returns {{x: string, y: string}}
|
||
*/
|
||
MdPanelPosition.prototype.getActualPosition = function() {
|
||
return this._actualPosition;
|
||
};
|
||
|
||
|
||
/**
|
||
* Reduces a list of translate values to a string that can be used within
|
||
* transform.
|
||
* @param {string} translateFn
|
||
* @param {!Array<string>} values
|
||
* @returns {string}
|
||
* @private
|
||
*/
|
||
MdPanelPosition.prototype._reduceTranslateValues =
|
||
function(translateFn, values) {
|
||
return values.map(function(translation) {
|
||
var translationValue = angular.isFunction(translation) ?
|
||
addUnits(translation(this)) : translation;
|
||
return translateFn + '(' + translationValue + ')';
|
||
}, this).join(' ');
|
||
};
|
||
|
||
|
||
/**
|
||
* Sets the panel position based on the created panel element and best x/y
|
||
* positioning.
|
||
* @param {!JQLite} el
|
||
* @private
|
||
*/
|
||
MdPanelPosition.prototype._setPanelPosition = function(el) {
|
||
// Remove the class in case it has been added before.
|
||
el.removeClass('_md-panel-position-adjusted');
|
||
|
||
// Only calculate the position if necessary.
|
||
if (this._absolute) {
|
||
this._setTransform(el);
|
||
return;
|
||
}
|
||
|
||
if (this._actualPosition) {
|
||
this._calculatePanelPosition(el, this._actualPosition);
|
||
this._setTransform(el);
|
||
this._constrainToViewport(el);
|
||
return;
|
||
}
|
||
|
||
for (var i = 0; i < this._positions.length; i++) {
|
||
this._actualPosition = this._positions[i];
|
||
this._calculatePanelPosition(el, this._actualPosition);
|
||
this._setTransform(el);
|
||
|
||
if (this._isOnscreen(el)) {
|
||
return;
|
||
}
|
||
}
|
||
|
||
this._constrainToViewport(el);
|
||
};
|
||
|
||
|
||
/**
|
||
* Constrains a panel's position to the viewport.
|
||
* @param {!JQLite} el
|
||
* @private
|
||
*/
|
||
MdPanelPosition.prototype._constrainToViewport = function(el) {
|
||
var margin = MdPanelPosition.viewportMargin;
|
||
var initialTop = this._top;
|
||
var initialLeft = this._left;
|
||
|
||
if (this.getTop()) {
|
||
var top = parseInt(this.getTop());
|
||
var bottom = el[0].offsetHeight + top;
|
||
var viewportHeight = this._$window.innerHeight;
|
||
|
||
if (top < margin) {
|
||
this._top = margin + 'px';
|
||
} else if (bottom > viewportHeight) {
|
||
this._top = top - (bottom - viewportHeight + margin) + 'px';
|
||
}
|
||
}
|
||
|
||
if (this.getLeft()) {
|
||
var left = parseInt(this.getLeft());
|
||
var right = el[0].offsetWidth + left;
|
||
var viewportWidth = this._$window.innerWidth;
|
||
|
||
if (left < margin) {
|
||
this._left = margin + 'px';
|
||
} else if (right > viewportWidth) {
|
||
this._left = left - (right - viewportWidth + margin) + 'px';
|
||
}
|
||
}
|
||
|
||
// Class that can be used to re-style the panel if it was repositioned.
|
||
el.toggleClass(
|
||
'_md-panel-position-adjusted',
|
||
this._top !== initialTop || this._left !== initialLeft
|
||
);
|
||
};
|
||
|
||
|
||
/**
|
||
* Switches 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 position;
|
||
}
|
||
|
||
var start = 'start';
|
||
var end = 'end';
|
||
|
||
return position.indexOf(start) > -1 ? position.replace(start, end) : position.replace(end, start);
|
||
};
|
||
|
||
|
||
/**
|
||
* 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;
|
||
};
|
||
|
||
|
||
/**
|
||
* Calculates the panel position based on the created panel element and the
|
||
* provided positioning.
|
||
* @param {!JQLite} el
|
||
* @param {!{x:string, y:string}} position
|
||
* @private
|
||
*/
|
||
MdPanelPosition.prototype._calculatePanelPosition = function(el, position) {
|
||
|
||
var panelBounds = el[0].getBoundingClientRect();
|
||
var panelWidth = Math.max(panelBounds.width, el[0].clientWidth);
|
||
var panelHeight = Math.max(panelBounds.height, el[0].clientHeight);
|
||
|
||
var targetBounds = this._relativeToEl[0].getBoundingClientRect();
|
||
|
||
var targetLeft = targetBounds.left;
|
||
var targetRight = targetBounds.right;
|
||
var targetWidth = targetBounds.width;
|
||
|
||
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;
|
||
}
|
||
|
||
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 *
|
||
*****************************************************************************/
|
||
|
||
|
||
/**
|
||
* 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 {!IInjectorService} $injector
|
||
* @final @constructor
|
||
*/
|
||
function MdPanelAnimation($injector) {
|
||
/** @private @const {!angular.$mdUtil} */
|
||
this._$mdUtil = $injector.get('$mdUtil');
|
||
|
||
/**
|
||
* @private {{element: !JQLite|undefined, bounds: !DOMRect}|
|
||
* undefined}
|
||
*/
|
||
this._openFrom;
|
||
|
||
/**
|
||
* @private {{element: !JQLite|undefined, bounds: !DOMRect}|
|
||
* undefined}
|
||
*/
|
||
this._closeTo;
|
||
|
||
/** @private {string|{open: string, close: string}} */
|
||
this._animationClass = '';
|
||
|
||
/** @private {number} */
|
||
this._openDuration;
|
||
|
||
/** @private {number} */
|
||
this._closeDuration;
|
||
|
||
/** @private {number|{open: number, close: number}} */
|
||
this._rawDuration;
|
||
}
|
||
|
||
|
||
/**
|
||
* Possible default animations.
|
||
* @enum {string}
|
||
*/
|
||
MdPanelAnimation.animation = {
|
||
SLIDE: 'md-panel-animate-slide',
|
||
SCALE: 'md-panel-animate-scale',
|
||
FADE: 'md-panel-animate-fade'
|
||
};
|
||
|
||
|
||
/**
|
||
* 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);
|
||
|
||
if (!this._closeTo) {
|
||
this._closeTo = this._openFrom;
|
||
}
|
||
return this;
|
||
};
|
||
|
||
|
||
/**
|
||
* 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}} closeTo
|
||
* @returns {!MdPanelAnimation}
|
||
*/
|
||
MdPanelAnimation.prototype.closeTo = function(closeTo) {
|
||
this._closeTo = this._getPanelAnimationTarget(closeTo);
|
||
return this;
|
||
};
|
||
|
||
|
||
/**
|
||
* Specifies the duration of the animation in milliseconds.
|
||
* @param {number|{open: number, close: number}} duration
|
||
* @returns {!MdPanelAnimation}
|
||
*/
|
||
MdPanelAnimation.prototype.duration = function(duration) {
|
||
if (duration) {
|
||
if (angular.isNumber(duration)) {
|
||
this._openDuration = this._closeDuration = toSeconds(duration);
|
||
} else if (angular.isObject(duration)) {
|
||
this._openDuration = toSeconds(duration.open);
|
||
this._closeDuration = toSeconds(duration.close);
|
||
}
|
||
}
|
||
|
||
// Save the original value so it can be passed to the backdrop.
|
||
this._rawDuration = duration;
|
||
|
||
return this;
|
||
|
||
function toSeconds(value) {
|
||
if (angular.isNumber(value)) return value / 1000;
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* Returns the element and bounds for the animation target.
|
||
* @param {string|!Element|{top: number, left: number}} location
|
||
* @returns {{element: !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));
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* 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}
|
||
*/
|
||
MdPanelAnimation.prototype.withAnimation = function(cssClass) {
|
||
this._animationClass = cssClass;
|
||
return this;
|
||
};
|
||
|
||
|
||
/**
|
||
* Animate the panel open.
|
||
* @param {!JQLite} panelEl
|
||
* @returns {!Q.IPromise} A promise that is resolved when the open
|
||
* animation is complete.
|
||
*/
|
||
MdPanelAnimation.prototype.animateOpen = function(panelEl) {
|
||
var animator = this._$mdUtil.dom.animator;
|
||
|
||
this._fixBounds(panelEl);
|
||
var animationOptions = {};
|
||
|
||
// Include the panel transformations when calculating the animations.
|
||
var panelTransform = panelEl[0].style.transform || '';
|
||
|
||
var openFrom = animator.toTransformCss(panelTransform);
|
||
var openTo = animator.toTransformCss(panelTransform);
|
||
|
||
switch (this._animationClass) {
|
||
case MdPanelAnimation.animation.SLIDE:
|
||
// Slide should start with opacity: 1.
|
||
panelEl.css('opacity', '1');
|
||
|
||
animationOptions = {
|
||
transitionInClass: '_md-panel-animate-enter',
|
||
transitionOutClass: '_md-panel-animate-leave',
|
||
};
|
||
|
||
var openSlide = animator.calculateSlideToOrigin(
|
||
panelEl, this._openFrom) || '';
|
||
openFrom = animator.toTransformCss(openSlide + ' ' + panelTransform);
|
||
break;
|
||
|
||
case MdPanelAnimation.animation.SCALE:
|
||
animationOptions = {
|
||
transitionInClass: '_md-panel-animate-enter'
|
||
};
|
||
|
||
var openScale = animator.calculateZoomToOrigin(
|
||
panelEl, this._openFrom) || '';
|
||
openFrom = animator.toTransformCss(panelTransform + ' ' + openScale);
|
||
break;
|
||
|
||
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'],
|
||
};
|
||
}
|
||
}
|
||
|
||
animationOptions.duration = this._openDuration;
|
||
|
||
return animator
|
||
.translate3d(panelEl, openFrom, openTo, animationOptions);
|
||
};
|
||
|
||
|
||
/**
|
||
* Animate the panel close.
|
||
* @param {!JQLite} panelEl
|
||
* @returns {!Q.IPromise} A promise that resolves when the close animation is complete.
|
||
*/
|
||
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',
|
||
transitionOutClass: '_md-panel-animate-enter _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',
|
||
transitionOutClass: '_md-panel-animate-scale-out _md-panel-animate-enter _md-panel-animate-leave'
|
||
};
|
||
|
||
var closeScale = animator.calculateZoomToOrigin(panelEl, this._closeTo) || '';
|
||
closeTo = animator.toTransformCss(panelTransform + ' ' + closeScale);
|
||
break;
|
||
|
||
case MdPanelAnimation.animation.FADE:
|
||
reverseAnimationOptions = {
|
||
transitionInClass: '_md-panel-animate-fade-out _md-panel-animate-leave',
|
||
transitionOutClass: '_md-panel-animate-fade-out _md-panel-animate-enter _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']
|
||
};
|
||
}
|
||
}
|
||
|
||
reverseAnimationOptions.duration = this._closeDuration;
|
||
|
||
return animator
|
||
.translate3d(panelEl, closeFrom, closeTo, reverseAnimationOptions);
|
||
};
|
||
|
||
|
||
/**
|
||
* Set the height and width to match the panel if not provided.
|
||
* @param {!JQLite} panelEl
|
||
* @private
|
||
*/
|
||
MdPanelAnimation.prototype._fixBounds = function(panelEl) {
|
||
var panelWidth = panelEl[0].offsetWidth;
|
||
var panelHeight = panelEl[0].offsetHeight;
|
||
|
||
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;
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* Identify the bounding RECT for the target element.
|
||
* @param {!JQLite} element
|
||
* @returns {{element: !JQLite|undefined, bounds: !DOMRect}}
|
||
* @private
|
||
*/
|
||
MdPanelAnimation.prototype._getBoundingClientRect = function(element) {
|
||
if (element instanceof angular.element) {
|
||
return {
|
||
element: element,
|
||
bounds: element[0].getBoundingClientRect()
|
||
};
|
||
}
|
||
};
|
||
|
||
|
||
/*****************************************************************************
|
||
* Util Methods *
|
||
*****************************************************************************/
|
||
|
||
|
||
/**
|
||
* Returns the angular element associated with a css selector or element.
|
||
* @param el {string|!JQLite|!Element}
|
||
* @returns {!JQLite}
|
||
*/
|
||
function getElement(el) {
|
||
var queryResult = angular.isString(el) ?
|
||
document.querySelector(el) : el;
|
||
return angular.element(queryResult);
|
||
}
|
||
|
||
/**
|
||
* Gets the computed values for an element's translateX and translateY in px.
|
||
* @param {!JQLite|!Element} el the element to evaluate
|
||
* @param {string} property
|
||
* @return {{x: number, y: number}} an element's translateX and translateY in px
|
||
*/
|
||
function getComputedTranslations(el, property) {
|
||
// The transform being returned by `getComputedStyle` is in the format:
|
||
// `matrix(a, b, c, d, translateX, translateY)` if defined and `none`
|
||
// if the element doesn't have a transform.
|
||
var transform = getComputedStyle(el[0] || el)[property];
|
||
var openIndex = transform.indexOf('(');
|
||
var closeIndex = transform.lastIndexOf(')');
|
||
var output = { x: 0, y: 0 };
|
||
|
||
if (openIndex > -1 && closeIndex > -1) {
|
||
var parsedValues = transform
|
||
.substring(openIndex + 1, closeIndex)
|
||
.split(', ')
|
||
.slice(-2);
|
||
|
||
output.x = parseInt(parsedValues[0]);
|
||
output.y = parseInt(parsedValues[1]);
|
||
}
|
||
|
||
return output;
|
||
}
|
||
|
||
/*
|
||
* Ensures that a value is a valid position name. Throw an exception if not.
|
||
* @param {Object} positionMap Object against which the value will be checked.
|
||
* @param {string} value
|
||
*/
|
||
function validatePosition(positionMap, value) {
|
||
// empty is ok
|
||
if (value === null || angular.isUndefined(value)) {
|
||
return;
|
||
}
|
||
|
||
var positionKeys = Object.keys(positionMap);
|
||
var positionValues = [];
|
||
|
||
for (var key, i = 0; key = positionKeys[i]; i++) {
|
||
var position = positionMap[key];
|
||
positionValues.push(position);
|
||
|
||
if (position === value) {
|
||
return;
|
||
}
|
||
}
|
||
|
||
throw new Error('Panel position only accepts the following values:\n' +
|
||
positionValues.join(' | '));
|
||
}
|
||
|
||
/**
|
||
* Adds units to a number value.
|
||
* @param {string|number} value
|
||
* @return {string}
|
||
*/
|
||
function addUnits(value) {
|
||
return angular.isNumber(value) ? value + 'px' : value;
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.progressCircular
|
||
* @description Module for a circular progressbar
|
||
*/
|
||
|
||
angular.module('material.components.progressCircular', ['material.core']);
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdProgressCircular
|
||
* @module material.components.progressCircular
|
||
* @restrict E
|
||
*
|
||
* @description
|
||
* The circular 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.
|
||
*
|
||
* 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 two modes: **'determinate'** and **'indeterminate'**.
|
||
*
|
||
* Note: if the `md-mode` value is set as undefined or specified as not 1 of the two (2) 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 mode, this number represents the percentage of the
|
||
* circular progress. Default: 0
|
||
* @param {number=} md-diameter This specifies the diameter of the circular progress. The value
|
||
* should be a pixel-size value (eg '100'). If this attribute is
|
||
* not present then a default value of '50px' is assumed.
|
||
*
|
||
* @param {boolean=} ng-disabled Determines whether to disable the progress element.
|
||
*
|
||
* @usage
|
||
* <hljs lang="html">
|
||
* <md-progress-circular md-mode="determinate" value="..."></md-progress-circular>
|
||
*
|
||
* <md-progress-circular md-mode="determinate" ng-value="..."></md-progress-circular>
|
||
*
|
||
* <md-progress-circular md-mode="determinate" value="..." md-diameter="100"></md-progress-circular>
|
||
*
|
||
* <md-progress-circular md-mode="indeterminate"></md-progress-circular>
|
||
* </hljs>
|
||
*/
|
||
|
||
MdProgressCircularDirective.$inject = ["$window", "$mdProgressCircular", "$mdTheming", "$mdUtil", "$interval", "$log"];
|
||
angular
|
||
.module('material.components.progressCircular')
|
||
.directive('mdProgressCircular', MdProgressCircularDirective);
|
||
|
||
/* @ngInject */
|
||
function MdProgressCircularDirective($window, $mdProgressCircular, $mdTheming,
|
||
$mdUtil, $interval, $log) {
|
||
|
||
// Note that this shouldn't use use $$rAF, because it can cause an infinite loop
|
||
// in any tests that call $animate.flush.
|
||
var rAF = $window.requestAnimationFrame ||
|
||
$window.webkitRequestAnimationFrame ||
|
||
angular.noop;
|
||
|
||
var cAF = $window.cancelAnimationFrame ||
|
||
$window.webkitCancelAnimationFrame ||
|
||
$window.webkitCancelRequestAnimationFrame ||
|
||
angular.noop;
|
||
|
||
var MODE_DETERMINATE = 'determinate';
|
||
var MODE_INDETERMINATE = 'indeterminate';
|
||
var DISABLED_CLASS = '_md-progress-circular-disabled';
|
||
var INDETERMINATE_CLASS = 'md-mode-indeterminate';
|
||
|
||
return {
|
||
restrict: 'E',
|
||
scope: {
|
||
value: '@',
|
||
mdDiameter: '@',
|
||
mdMode: '@'
|
||
},
|
||
template:
|
||
'<svg xmlns="http://www.w3.org/2000/svg">' +
|
||
'<path fill="none"/>' +
|
||
'</svg>',
|
||
compile: function(element, attrs) {
|
||
element.attr({
|
||
'aria-valuemin': 0,
|
||
'aria-valuemax': 100,
|
||
'role': 'progressbar'
|
||
});
|
||
|
||
if (angular.isUndefined(attrs.mdMode)) {
|
||
var mode = attrs.hasOwnProperty('value') ? MODE_DETERMINATE : MODE_INDETERMINATE;
|
||
attrs.$set('mdMode', mode);
|
||
} else {
|
||
attrs.$set('mdMode', attrs.mdMode.trim());
|
||
}
|
||
|
||
return MdProgressCircularLink;
|
||
}
|
||
};
|
||
|
||
function MdProgressCircularLink(scope, element, attrs) {
|
||
var node = element[0];
|
||
var svg = angular.element(node.querySelector('svg'));
|
||
var path = angular.element(node.querySelector('path'));
|
||
var startIndeterminate = $mdProgressCircular.startIndeterminate;
|
||
var endIndeterminate = $mdProgressCircular.endIndeterminate;
|
||
var iterationCount = 0;
|
||
var lastAnimationId = 0;
|
||
var lastDrawFrame;
|
||
var interval;
|
||
|
||
$mdTheming(element);
|
||
element.toggleClass(DISABLED_CLASS, attrs.hasOwnProperty('disabled'));
|
||
|
||
// If the mode is indeterminate, it doesn't need to
|
||
// wait for the next digest. It can start right away.
|
||
if (scope.mdMode === MODE_INDETERMINATE){
|
||
startIndeterminateAnimation();
|
||
}
|
||
|
||
scope.$on('$destroy', function(){
|
||
cleanupIndeterminateAnimation();
|
||
|
||
if (lastDrawFrame) {
|
||
cAF(lastDrawFrame);
|
||
}
|
||
});
|
||
|
||
scope.$watchGroup(['value', 'mdMode', function() {
|
||
var isDisabled = node.disabled;
|
||
|
||
// Sometimes the browser doesn't return a boolean, in
|
||
// which case we should check whether the attribute is
|
||
// present.
|
||
if (isDisabled === true || isDisabled === false){
|
||
return isDisabled;
|
||
}
|
||
|
||
return angular.isDefined(element.attr('disabled'));
|
||
}], function(newValues, oldValues) {
|
||
var mode = newValues[1];
|
||
var isDisabled = newValues[2];
|
||
var wasDisabled = oldValues[2];
|
||
var diameter = 0;
|
||
var strokeWidth = 0;
|
||
|
||
if (isDisabled !== wasDisabled) {
|
||
element.toggleClass(DISABLED_CLASS, !!isDisabled);
|
||
}
|
||
|
||
if (isDisabled) {
|
||
cleanupIndeterminateAnimation();
|
||
} else {
|
||
if (mode !== MODE_DETERMINATE && mode !== MODE_INDETERMINATE) {
|
||
mode = MODE_INDETERMINATE;
|
||
attrs.$set('mdMode', mode);
|
||
}
|
||
|
||
if (mode === MODE_INDETERMINATE) {
|
||
if (oldValues[1] === MODE_DETERMINATE) {
|
||
diameter = getSize(scope.mdDiameter);
|
||
strokeWidth = getStroke(diameter);
|
||
path.attr('d', getSvgArc(diameter, strokeWidth, true));
|
||
path.attr('stroke-dasharray', getDashLength(diameter, strokeWidth, 75));
|
||
}
|
||
startIndeterminateAnimation();
|
||
} else {
|
||
var newValue = clamp(newValues[0]);
|
||
var oldValue = clamp(oldValues[0]);
|
||
|
||
cleanupIndeterminateAnimation();
|
||
|
||
if (oldValues[1] === MODE_INDETERMINATE) {
|
||
diameter = getSize(scope.mdDiameter);
|
||
strokeWidth = getStroke(diameter);
|
||
path.attr('d', getSvgArc(diameter, strokeWidth, false));
|
||
path.attr('stroke-dasharray', getDashLength(diameter, strokeWidth, 100));
|
||
}
|
||
|
||
element.attr('aria-valuenow', newValue);
|
||
renderCircle(oldValue, newValue);
|
||
}
|
||
}
|
||
|
||
});
|
||
|
||
// This is in a separate watch in order to avoid layout, unless
|
||
// the value has actually changed.
|
||
scope.$watch('mdDiameter', function(newValue) {
|
||
var diameter = getSize(newValue);
|
||
var strokeWidth = getStroke(diameter);
|
||
var value = clamp(scope.value);
|
||
var transformOrigin = (diameter / 2) + 'px';
|
||
var dimensions = {
|
||
width: diameter + 'px',
|
||
height: diameter + 'px'
|
||
};
|
||
|
||
// The viewBox has to be applied via setAttribute, because it is
|
||
// case-sensitive. If jQuery is included in the page, `.attr` lowercases
|
||
// all attribute names.
|
||
svg[0].setAttribute('viewBox', '0 0 ' + diameter + ' ' + diameter);
|
||
|
||
// Usually viewBox sets the dimensions for the SVG, however that doesn't
|
||
// seem to be the case on IE10.
|
||
// Important! The transform origin has to be set from here and it has to
|
||
// be in the format of "Ypx Ypx Ypx", otherwise the rotation wobbles in
|
||
// IE and Edge, because they don't account for the stroke width when
|
||
// rotating. Also "center" doesn't help in this case, it has to be a
|
||
// precise value.
|
||
svg
|
||
.css(dimensions)
|
||
.css('transform-origin', transformOrigin + ' ' + transformOrigin + ' ' + transformOrigin);
|
||
|
||
element.css(dimensions);
|
||
|
||
path.attr('stroke-width', strokeWidth);
|
||
path.attr('stroke-linecap', 'square');
|
||
if (scope.mdMode == MODE_INDETERMINATE) {
|
||
path.attr('d', getSvgArc(diameter, strokeWidth, true));
|
||
path.attr('stroke-dasharray', getDashLength(diameter, strokeWidth, 75));
|
||
path.attr('stroke-dashoffset', getDashOffset(diameter, strokeWidth, 1, 75));
|
||
} else {
|
||
path.attr('d', getSvgArc(diameter, strokeWidth, false));
|
||
path.attr('stroke-dasharray', getDashLength(diameter, strokeWidth, 100));
|
||
path.attr('stroke-dashoffset', getDashOffset(diameter, strokeWidth, 0, 100));
|
||
renderCircle(value, value);
|
||
}
|
||
|
||
});
|
||
|
||
function renderCircle(animateFrom, animateTo, easing, duration, iterationCount, maxValue) {
|
||
var id = ++lastAnimationId;
|
||
var startTime = $mdUtil.now();
|
||
var changeInValue = animateTo - animateFrom;
|
||
var diameter = getSize(scope.mdDiameter);
|
||
var strokeWidth = getStroke(diameter);
|
||
var ease = easing || $mdProgressCircular.easeFn;
|
||
var animationDuration = duration || $mdProgressCircular.duration;
|
||
var rotation = -90 * (iterationCount || 0);
|
||
var dashLimit = maxValue || 100;
|
||
|
||
// No need to animate it if the values are the same
|
||
if (animateTo === animateFrom) {
|
||
renderFrame(animateTo);
|
||
} else {
|
||
lastDrawFrame = rAF(function animation() {
|
||
var currentTime = $window.Math.max(0, $window.Math.min($mdUtil.now() - startTime, animationDuration));
|
||
|
||
renderFrame(ease(currentTime, animateFrom, changeInValue, animationDuration));
|
||
|
||
// Do not allow overlapping animations
|
||
if (id === lastAnimationId && currentTime < animationDuration) {
|
||
lastDrawFrame = rAF(animation);
|
||
}
|
||
});
|
||
}
|
||
|
||
function renderFrame(value) {
|
||
path.attr('stroke-dashoffset', getDashOffset(diameter, strokeWidth, value, dashLimit));
|
||
path.attr('transform','rotate(' + (rotation) + ' ' + diameter/2 + ' ' + diameter/2 + ')');
|
||
}
|
||
}
|
||
|
||
function animateIndeterminate() {
|
||
renderCircle(
|
||
startIndeterminate,
|
||
endIndeterminate,
|
||
$mdProgressCircular.easeFnIndeterminate,
|
||
$mdProgressCircular.durationIndeterminate,
|
||
iterationCount,
|
||
75
|
||
);
|
||
|
||
// The %4 technically isn't necessary, but it keeps the rotation
|
||
// under 360, instead of becoming a crazy large number.
|
||
iterationCount = ++iterationCount % 4;
|
||
|
||
}
|
||
|
||
function startIndeterminateAnimation() {
|
||
if (!interval) {
|
||
// Note that this interval isn't supposed to trigger a digest.
|
||
interval = $interval(
|
||
animateIndeterminate,
|
||
$mdProgressCircular.durationIndeterminate,
|
||
0,
|
||
false
|
||
);
|
||
|
||
animateIndeterminate();
|
||
|
||
element
|
||
.addClass(INDETERMINATE_CLASS)
|
||
.removeAttr('aria-valuenow');
|
||
}
|
||
}
|
||
|
||
function cleanupIndeterminateAnimation() {
|
||
if (interval) {
|
||
$interval.cancel(interval);
|
||
interval = null;
|
||
element.removeClass(INDETERMINATE_CLASS);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Returns SVG path data for progress circle
|
||
* Syntax spec: https://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands
|
||
*
|
||
* @param {number} diameter Diameter of the container.
|
||
* @param {number} strokeWidth Stroke width to be used when drawing circle
|
||
* @param {boolean} indeterminate Use if progress circle will be used for indeterminate
|
||
*
|
||
* @returns {string} String representation of an SVG arc.
|
||
*/
|
||
function getSvgArc(diameter, strokeWidth, indeterminate) {
|
||
var radius = diameter / 2;
|
||
var offset = strokeWidth / 2;
|
||
var start = radius + ',' + offset; // ie: (25, 2.5) or 12 o'clock
|
||
var end = offset + ',' + radius; // ie: (2.5, 25) or 9 o'clock
|
||
var arcRadius = radius - offset;
|
||
return 'M' + start
|
||
+ 'A' + arcRadius + ',' + arcRadius + ' 0 1 1 ' + end // 75% circle
|
||
+ (indeterminate ? '' : 'A' + arcRadius + ',' + arcRadius + ' 0 0 1 ' + start); // loop to start
|
||
}
|
||
|
||
/**
|
||
* Return stroke length for progress circle
|
||
*
|
||
* @param {number} diameter Diameter of the container.
|
||
* @param {number} strokeWidth Stroke width to be used when drawing circle
|
||
* @param {number} value Percentage of circle (between 0 and 100)
|
||
* @param {number} maxArcLength Maximum length of arc as a percentage of circle (between 0 and 100)
|
||
*
|
||
* @returns {number} Stroke length for progress circle
|
||
*/
|
||
function getDashOffset(diameter, strokeWidth, value, maxArcLength) {
|
||
return getSpinnerCircumference(diameter, strokeWidth) * ((maxArcLength - value) / 100);
|
||
}
|
||
|
||
/**
|
||
* Limits a value between 0 and 100.
|
||
*/
|
||
function clamp(value) {
|
||
return $window.Math.max(0, $window.Math.min(value || 0, 100));
|
||
}
|
||
|
||
/**
|
||
* Determines the size of a progress circle, based on the provided
|
||
* value in the following formats: `X`, `Ypx`, `Z%`.
|
||
*/
|
||
function getSize(value) {
|
||
var defaultValue = $mdProgressCircular.progressSize;
|
||
|
||
if (value) {
|
||
var parsed = parseFloat(value);
|
||
|
||
if (value.lastIndexOf('%') === value.length - 1) {
|
||
parsed = (parsed / 100) * defaultValue;
|
||
}
|
||
|
||
return parsed;
|
||
}
|
||
|
||
return defaultValue;
|
||
}
|
||
|
||
/**
|
||
* Determines the circle's stroke width, based on
|
||
* the provided diameter.
|
||
*/
|
||
function getStroke(diameter) {
|
||
return $mdProgressCircular.strokeWidth / 100 * diameter;
|
||
}
|
||
|
||
/**
|
||
* Return length of the dash
|
||
*
|
||
* @param {number} diameter Diameter of the container.
|
||
* @param {number} strokeWidth Stroke width to be used when drawing circle
|
||
* @param {number} value Percentage of circle (between 0 and 100)
|
||
*
|
||
* @returns {number} Length of the dash
|
||
*/
|
||
function getDashLength(diameter, strokeWidth, value) {
|
||
return getSpinnerCircumference(diameter, strokeWidth) * (value / 100);
|
||
}
|
||
|
||
/**
|
||
* Return circumference of the spinner
|
||
*
|
||
* @param {number} diameter Diameter of the container.
|
||
* @param {number} strokeWidth Stroke width to be used when drawing circle
|
||
*
|
||
* @returns {number} Circumference of the spinner
|
||
*/
|
||
function getSpinnerCircumference(diameter, strokeWidth) {
|
||
return ((diameter - strokeWidth) * $window.Math.PI);
|
||
}
|
||
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc service
|
||
* @name $mdProgressCircular
|
||
* @module material.components.progressCircular
|
||
*
|
||
* @description
|
||
* Allows the user to specify the default options for the `progressCircular` directive.
|
||
*
|
||
* @property {number} progressSize Diameter of the progress circle in pixels.
|
||
* @property {number} strokeWidth Width of the circle's stroke as a percentage of the circle's size.
|
||
* @property {number} duration Length of the circle animation in milliseconds.
|
||
* @property {function} easeFn Default easing animation function.
|
||
* @property {object} easingPresets Collection of pre-defined easing functions.
|
||
*
|
||
* @property {number} durationIndeterminate Duration of the indeterminate animation.
|
||
* @property {number} startIndeterminate Indeterminate animation start point.
|
||
* @property {number} endIndeterminate Indeterminate animation end point.
|
||
* @property {function} easeFnIndeterminate Easing function to be used when animating
|
||
* between the indeterminate values.
|
||
*
|
||
* @property {(function(object): object)} configure Used to modify the default options.
|
||
*
|
||
* @usage
|
||
* <hljs lang="js">
|
||
* myAppModule.config(function($mdProgressCircularProvider) {
|
||
*
|
||
* // Example of changing the default progress options.
|
||
* $mdProgressCircularProvider.configure({
|
||
* progressSize: 100,
|
||
* strokeWidth: 20,
|
||
* duration: 800
|
||
* });
|
||
* });
|
||
* </hljs>
|
||
*
|
||
*/
|
||
|
||
angular
|
||
.module('material.components.progressCircular')
|
||
.provider("$mdProgressCircular", MdProgressCircularProvider);
|
||
|
||
function MdProgressCircularProvider() {
|
||
var progressConfig = {
|
||
progressSize: 50,
|
||
strokeWidth: 10,
|
||
duration: 100,
|
||
easeFn: linearEase,
|
||
|
||
durationIndeterminate: 1333,
|
||
startIndeterminate: 1,
|
||
endIndeterminate: 149,
|
||
easeFnIndeterminate: materialEase,
|
||
|
||
easingPresets: {
|
||
linearEase: linearEase,
|
||
materialEase: materialEase
|
||
}
|
||
};
|
||
|
||
return {
|
||
configure: function(options) {
|
||
progressConfig = angular.extend(progressConfig, options || {});
|
||
return progressConfig;
|
||
},
|
||
$get: function() { return progressConfig; }
|
||
};
|
||
|
||
function linearEase(t, b, c, d) {
|
||
return c * t / d + b;
|
||
}
|
||
|
||
function materialEase(t, b, c, d) {
|
||
// via http://www.timotheegroleau.com/Flash/experiments/easing_function_generator.htm
|
||
// with settings of [0, 0, 1, 1]
|
||
var ts = (t /= d) * t;
|
||
var tc = ts * t;
|
||
return b + c * (6 * tc * ts + -15 * ts * ts + 10 * tc);
|
||
}
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.progressLinear
|
||
* @description Linear Progress module!
|
||
*/
|
||
MdProgressLinearDirective.$inject = ["$mdTheming", "$mdUtil", "$log"];
|
||
angular.module('material.components.progressLinear', [
|
||
'material.core'
|
||
])
|
||
.directive('mdProgressLinear', MdProgressLinearDirective);
|
||
|
||
/**
|
||
* @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
|
||
* <hljs lang="html">
|
||
* <md-progress-linear md-mode="determinate" value="..."></md-progress-linear>
|
||
*
|
||
* <md-progress-linear md-mode="determinate" ng-value="..."></md-progress-linear>
|
||
*
|
||
* <md-progress-linear md-mode="indeterminate"></md-progress-linear>
|
||
*
|
||
* <md-progress-linear md-mode="buffer" value="..." md-buffer-value="..."></md-progress-linear>
|
||
*
|
||
* <md-progress-linear md-mode="query"></md-progress-linear>
|
||
* </hljs>
|
||
*/
|
||
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";
|
||
|
||
return {
|
||
restrict: 'E',
|
||
template: '<div class="md-container">' +
|
||
'<div class="md-dashed"></div>' +
|
||
'<div class="md-bar md-bar1"></div>' +
|
||
'<div class="md-bar md-bar2"></div>' +
|
||
'</div>',
|
||
compile: compile
|
||
};
|
||
|
||
function compile(tElement, tAttrs, transclude) {
|
||
tElement.attr('aria-valuemin', 0);
|
||
tElement.attr('aria-valuemax', 100);
|
||
tElement.attr('role', 'progressbar');
|
||
|
||
return postLink;
|
||
}
|
||
function postLink(scope, element, attr) {
|
||
$mdTheming(element);
|
||
|
||
var lastMode;
|
||
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())
|
||
.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;
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 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";
|
||
element.attr("md-mode", mode);
|
||
attr.mdMode = mode;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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;
|
||
}
|
||
|
||
/**
|
||
* 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 to = $mdUtil.supplant("translateX({0}%) scale({1},1)", [(value-100)/2, value/100]);
|
||
var styles = toVendorCSS({ transform : to });
|
||
angular.element(target).css(styles);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Clamps the value to be between 0 and 100.
|
||
* @param {number} value The value to clamp.
|
||
* @returns {number}
|
||
*/
|
||
function clamp(value) {
|
||
return Math.max(0, Math.min(value || 0, 100));
|
||
}
|
||
}
|
||
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.radioButton
|
||
* @description radioButton module!
|
||
*/
|
||
mdRadioGroupDirective.$inject = ["$mdUtil", "$mdConstant", "$mdTheming", "$timeout"];
|
||
mdRadioButtonDirective.$inject = ["$mdAria", "$mdUtil", "$mdTheming"];
|
||
angular.module('material.components.radioButton', [
|
||
'material.core'
|
||
])
|
||
.directive('mdRadioGroup', mdRadioGroupDirective)
|
||
.directive('mdRadioButton', mdRadioButtonDirective);
|
||
|
||
/**
|
||
* @type {Readonly<{NEXT: number, CURRENT: number, PREVIOUS: number}>}
|
||
*/
|
||
var incrementSelection = Object.freeze({PREVIOUS: -1, CURRENT: 0, NEXT: 1});
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @module material.components.radioButton
|
||
* @name mdRadioGroup
|
||
*
|
||
* @restrict E
|
||
*
|
||
* @description
|
||
* The `<md-radio-group>` directive identifies a grouping
|
||
* container for the 1..n grouped radio buttons; specified using nested
|
||
* `<md-radio-button>` elements.
|
||
*
|
||
* The radio button uses the accent color by default. The primary color palette may be used with
|
||
* the `md-primary` class.
|
||
*
|
||
* Note: `<md-radio-group>` and `<md-radio-button>` handle `tabindex` differently
|
||
* than the native `<input type="radio">` controls. Whereas the native controls
|
||
* force the user to tab through all the radio buttons, `<md-radio-group>`
|
||
* is focusable and by default the `<md-radio-button>`s are not.
|
||
*
|
||
* @param {string} ng-model Assignable angular expression to data-bind to.
|
||
* @param {string=} ng-change AngularJS expression to be executed when input changes due to user
|
||
* interaction.
|
||
* @param {boolean=} md-no-ink If present, disables ink ripple effects.
|
||
*
|
||
* @usage
|
||
* <hljs lang="html">
|
||
* <md-radio-group ng-model="selected">
|
||
* <md-radio-button ng-repeat="item in items"
|
||
* ng-value="item.value" aria-label="{{item.label}}">
|
||
* {{ item.label }}
|
||
* </md-radio-button>
|
||
* </md-radio-group>
|
||
* </hljs>
|
||
*/
|
||
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, controllers) {
|
||
// private md component indicator for styling
|
||
element.addClass('_md');
|
||
$mdTheming(element);
|
||
|
||
var radioGroupController = controllers[0];
|
||
var ngModelCtrl = controllers[1] || $mdUtil.fakeNgModel();
|
||
|
||
radioGroupController.init(ngModelCtrl);
|
||
|
||
scope.mouseActive = false;
|
||
|
||
element
|
||
.attr({
|
||
'role': 'radiogroup',
|
||
'tabIndex': element.attr('tabindex') || '0'
|
||
})
|
||
.on('keydown', keydownListener)
|
||
.on('mousedown', function() {
|
||
scope.mouseActive = true;
|
||
$timeout(function() {
|
||
scope.mouseActive = false;
|
||
}, 100);
|
||
})
|
||
.on('focus', function() {
|
||
if (scope.mouseActive === false) {
|
||
radioGroupController.$element.addClass('md-focused');
|
||
}
|
||
})
|
||
.on('blur', function() {
|
||
radioGroupController.$element.removeClass('md-focused');
|
||
});
|
||
|
||
// Initially set the first radio button as the aria-activedescendant. This will be overridden
|
||
// if a 'checked' radio button gets rendered. We need to wait for the nextTick here so that the
|
||
// radio buttons have their id values assigned.
|
||
$mdUtil.nextTick(function () {
|
||
var radioButtons = getRadioButtons(radioGroupController.$element);
|
||
if (radioButtons.count() &&
|
||
!radioGroupController.$element[0].hasAttribute('aria-activedescendant')) {
|
||
radioGroupController.setActiveDescendant(radioButtons.first().id);
|
||
}
|
||
});
|
||
|
||
/**
|
||
* Apply the md-focused class if it isn't already applied.
|
||
*/
|
||
function setFocus() {
|
||
if (!element.hasClass('md-focused')) { element.addClass('md-focused'); }
|
||
}
|
||
|
||
/**
|
||
* @param {KeyboardEvent} keyboardEvent
|
||
*/
|
||
function keydownListener(keyboardEvent) {
|
||
var keyCode = keyboardEvent.which || keyboardEvent.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 &&
|
||
keyboardEvent.currentTarget !== keyboardEvent.target) {
|
||
return;
|
||
}
|
||
|
||
switch (keyCode) {
|
||
case $mdConstant.KEY_CODE.LEFT_ARROW:
|
||
case $mdConstant.KEY_CODE.UP_ARROW:
|
||
keyboardEvent.preventDefault();
|
||
radioGroupController.selectPrevious();
|
||
setFocus();
|
||
break;
|
||
|
||
case $mdConstant.KEY_CODE.RIGHT_ARROW:
|
||
case $mdConstant.KEY_CODE.DOWN_ARROW:
|
||
keyboardEvent.preventDefault();
|
||
radioGroupController.selectNext();
|
||
setFocus();
|
||
break;
|
||
|
||
case $mdConstant.KEY_CODE.SPACE:
|
||
keyboardEvent.preventDefault();
|
||
radioGroupController.selectCurrent();
|
||
break;
|
||
|
||
case $mdConstant.KEY_CODE.ENTER:
|
||
var form = angular.element($mdUtil.getClosest(element[0], 'form'));
|
||
if (form.length > 0) {
|
||
form.triggerHandler('submit');
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {JQLite} $element
|
||
* @constructor
|
||
*/
|
||
function RadioGroupController($element) {
|
||
this._radioButtonRenderFns = [];
|
||
this.$element = $element;
|
||
}
|
||
|
||
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;
|
||
},
|
||
selectCurrent: function() {
|
||
return changeSelectedButton(this.$element, incrementSelection.CURRENT);
|
||
},
|
||
selectNext: function() {
|
||
return changeSelectedButton(this.$element, incrementSelection.NEXT);
|
||
},
|
||
selectPrevious: function() {
|
||
return changeSelectedButton(this.$element, incrementSelection.PREVIOUS);
|
||
},
|
||
setActiveDescendant: function (radioId) {
|
||
this.$element.attr('aria-activedescendant', radioId);
|
||
},
|
||
isDisabled: function() {
|
||
return this.$element[0].hasAttribute('disabled');
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Coerce all child radio buttons into an array, then wrap them in an iterator.
|
||
* @param parent {!JQLite}
|
||
* @return {{add: add, next: (function()), last: (function(): any|null), previous: (function()), count: (function(): number), hasNext: (function(*=): Array.length|*|number|boolean), inRange: (function(*): boolean), remove: remove, contains: (function(*=): *|boolean), itemAt: (function(*=): any|null), findBy: (function(*, *): *[]), hasPrevious: (function(*=): Array.length|*|number|boolean), items: (function(): *[]), indexOf: (function(*=): number), first: (function(): any|null)}}
|
||
*/
|
||
function getRadioButtons(parent) {
|
||
return $mdUtil.iterator(parent[0].querySelectorAll('md-radio-button'), true);
|
||
}
|
||
|
||
/**
|
||
* Change the radio group's selected button by a given increment.
|
||
* If no button is selected, select the first button.
|
||
* @param {JQLite} parent the md-radio-group
|
||
* @param {incrementSelection} increment enum that determines whether the next or
|
||
* previous button is clicked. For current, only the first button is selected, otherwise the
|
||
* current selection is maintained (by doing nothing).
|
||
*/
|
||
function changeSelectedButton(parent, increment) {
|
||
var buttons = getRadioButtons(parent);
|
||
var target;
|
||
|
||
if (buttons.count()) {
|
||
var validate = function (button) {
|
||
// If disabled, then NOT valid
|
||
return !angular.element(button).attr("disabled");
|
||
};
|
||
|
||
var selected = parent[0].querySelector('md-radio-button.md-checked');
|
||
if (!selected) {
|
||
target = buttons.first();
|
||
} else if (increment === incrementSelection.PREVIOUS ||
|
||
increment === incrementSelection.NEXT) {
|
||
target = buttons[
|
||
increment === incrementSelection.PREVIOUS ? 'previous' : 'next'
|
||
](selected, validate);
|
||
}
|
||
|
||
if (target) {
|
||
// Activate radioButton's click listener (triggerHandler won't create a real click event)
|
||
angular.element(target).triggerHandler('click');
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @module material.components.radioButton
|
||
* @name mdRadioButton
|
||
*
|
||
* @restrict E
|
||
*
|
||
* @description
|
||
* The `<md-radio-button>`directive is the child directive required to be used within `<md-radio-group>` elements.
|
||
*
|
||
* While similar to the `<input type="radio" ng-model="" value="">` directive,
|
||
* the `<md-radio-button>` directive provides ink effects, ARIA support, and
|
||
* supports use within named radio groups.
|
||
*
|
||
* One of `value` or `ng-value` must be set so that the `md-radio-group`'s model is set properly when the
|
||
* `md-radio-button` is selected.
|
||
*
|
||
* @param {string} value The value to which the model should be set when selected.
|
||
* @param {string} ng-value AngularJS expression which sets the value to which the model should
|
||
* be set when selected.
|
||
* @param {string=} name Property name of the form under which the control is published.
|
||
* @param {string=} aria-label Adds label to radio button for accessibility.
|
||
* Defaults to radio button's text. If no text content is available, a warning will be logged.
|
||
*
|
||
* @usage
|
||
* <hljs lang="html">
|
||
*
|
||
* <md-radio-button value="1" aria-label="Label 1">
|
||
* Label 1
|
||
* </md-radio-button>
|
||
*
|
||
* <md-radio-button ng-value="specialValue" aria-label="Green">
|
||
* Green
|
||
* </md-radio-button>
|
||
*
|
||
* </hljs>
|
||
*
|
||
*/
|
||
function mdRadioButtonDirective($mdAria, $mdUtil, $mdTheming) {
|
||
|
||
var CHECKED_CSS = 'md-checked';
|
||
|
||
return {
|
||
restrict: 'E',
|
||
require: '^mdRadioGroup',
|
||
transclude: true,
|
||
template: '<div class="md-container" md-ink-ripple md-ink-ripple-checkbox>' +
|
||
'<div class="md-off"></div>' +
|
||
'<div class="md-on"></div>' +
|
||
'</div>' +
|
||
'<div ng-transclude class="md-label"></div>',
|
||
link: link
|
||
};
|
||
|
||
function link(scope, element, attr, radioGroupController) {
|
||
var lastChecked;
|
||
|
||
$mdTheming(element);
|
||
configureAria(element);
|
||
element.addClass('md-auto-horizontal-margin');
|
||
|
||
// ngAria overwrites the aria-checked inside a $watch for ngValue.
|
||
// We should defer the initialization until all the watches have fired.
|
||
// This can also be fixed by removing the `lastChecked` check, but that'll
|
||
// cause more DOM manipulation on each digest.
|
||
if (attr.ngValue) {
|
||
$mdUtil.nextTick(initialize, false);
|
||
} else {
|
||
initialize();
|
||
}
|
||
|
||
/**
|
||
* Initializes the component.
|
||
*/
|
||
function initialize() {
|
||
if (!radioGroupController) {
|
||
throw 'RadioButton: No RadioGroupController could be found.';
|
||
}
|
||
|
||
radioGroupController.add(render);
|
||
attr.$observe('value', render);
|
||
|
||
element
|
||
.on('click', listener)
|
||
.on('$destroy', function() {
|
||
radioGroupController.remove(render);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* On click functionality.
|
||
*/
|
||
function listener(ev) {
|
||
if (element[0].hasAttribute('disabled') || radioGroupController.isDisabled()) return;
|
||
|
||
scope.$apply(function() {
|
||
radioGroupController.setViewValue(attr.value, ev && ev.type);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Add or remove the `.md-checked` class from the RadioButton (and conditionally its parent).
|
||
* Update the `aria-activedescendant` attribute.
|
||
*/
|
||
function render() {
|
||
var checked = radioGroupController.getViewValue() == attr.value;
|
||
|
||
if (checked === lastChecked) return;
|
||
|
||
if (element[0] && element[0].parentNode &&
|
||
element[0].parentNode.nodeName.toLowerCase() !== 'md-radio-group') {
|
||
// If the radioButton is inside a div, then add class so highlighting will work.
|
||
element.parent().toggleClass(CHECKED_CSS, checked);
|
||
}
|
||
|
||
if (checked) {
|
||
radioGroupController.setActiveDescendant(element.attr('id'));
|
||
}
|
||
|
||
lastChecked = checked;
|
||
|
||
element
|
||
.attr('aria-checked', checked)
|
||
.toggleClass(CHECKED_CSS, checked);
|
||
}
|
||
|
||
/**
|
||
* Inject ARIA-specific attributes appropriate for each radio button
|
||
*/
|
||
function configureAria(element) {
|
||
element.attr({
|
||
id: attr.id || 'radio_' + $mdUtil.nextUid(),
|
||
role: 'radio',
|
||
'aria-checked': 'false'
|
||
});
|
||
|
||
$mdAria.expectWithText(element, 'aria-label');
|
||
}
|
||
}
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.select
|
||
*/
|
||
|
||
/***************************************************
|
||
|
||
### TODO ###
|
||
- [ ] Abstract placement logic in $mdSelect service to $mdMenu service
|
||
|
||
***************************************************/
|
||
|
||
SelectDirective.$inject = ["$mdSelect", "$mdUtil", "$mdConstant", "$mdTheming", "$mdAria", "$parse", "$sce"];
|
||
SelectMenuDirective.$inject = ["$parse", "$mdUtil", "$mdConstant", "$mdTheming"];
|
||
OptionDirective.$inject = ["$mdButtonInkRipple", "$mdUtil", "$mdTheming"];
|
||
SelectProvider.$inject = ["$$interimElementProvider"];
|
||
OptionController.$inject = ["$element"];
|
||
var SELECT_EDGE_MARGIN = 8;
|
||
var selectNextId = 0;
|
||
var CHECKBOX_SELECTION_INDICATOR;
|
||
|
||
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 directive
|
||
* @name mdSelect
|
||
* @restrict E
|
||
* @module material.components.select
|
||
*
|
||
* @description Displays a select box, bound to an `ng-model`. Selectable options are defined using
|
||
* the <a ng-href="api/directive/mdOption">md-option</a> element directive. Options can be grouped
|
||
* using the <a ng-href="api/directive/mdOptgroup">md-optgroup</a> element directive.
|
||
*
|
||
* When the select is required and uses a floating label, then the label will automatically contain
|
||
* an asterisk (`*`). This behavior can be disabled by using the `md-no-asterisk` attribute.
|
||
*
|
||
* By default, the select will display with an underline to match other form elements. This can be
|
||
* disabled by applying the `md-no-underline` CSS class.
|
||
*
|
||
* @param {expression} ng-model Assignable angular expression to data-bind to.
|
||
* @param {expression=} ng-change Expression to be executed when the model value changes.
|
||
* @param {boolean=} multiple When present, allows for more than one option to be selected.
|
||
* The model is an array with the selected choices. **Note:** This attribute is only evaluated
|
||
* once; it is not watched.
|
||
* @param {expression=} md-on-close Expression to be evaluated when the select is closed.
|
||
* @param {expression=} md-on-open Expression to be evaluated when opening the select.
|
||
* Will hide the select options and show a spinner until the evaluated promise resolves.
|
||
* @param {expression=} md-selected-text Expression to be evaluated that will return a string
|
||
* to be displayed as a placeholder in the select input box when it is closed. The value
|
||
* will be treated as *text* (not html).
|
||
* @param {expression=} md-selected-html Expression to be evaluated that will return a string
|
||
* to be displayed as a placeholder in the select input box when it is closed. The value
|
||
* will be treated as *html*. The value must either be explicitly marked as trustedHtml or
|
||
* the ngSanitize module must be loaded.
|
||
* @param {string=} placeholder Placeholder hint text.
|
||
* @param {boolean=} md-no-asterisk When set to true, an asterisk will not be appended to the
|
||
* floating label. **Note:** This attribute is only evaluated once; it is not watched.
|
||
* @param {string=} aria-label Optional label for accessibility. Only necessary if no 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=} md-select-only-option If specified, a `<md-select>` will automatically select
|
||
* it's first option, if it only has one.
|
||
*
|
||
* @usage
|
||
* With a placeholder (label and aria-label are added dynamically)
|
||
* <hljs lang="html">
|
||
* <md-input-container>
|
||
* <md-select
|
||
* ng-model="someModel"
|
||
* placeholder="Select a state">
|
||
* <md-option ng-value="opt" ng-repeat="opt in neighborhoods2">{{ opt }}</md-option>
|
||
* </md-select>
|
||
* </md-input-container>
|
||
* </hljs>
|
||
*
|
||
* With an explicit label
|
||
* <hljs lang="html">
|
||
* <md-input-container>
|
||
* <label>State</label>
|
||
* <md-select
|
||
* ng-model="someModel">
|
||
* <md-option ng-value="opt" ng-repeat="opt in neighborhoods2">{{ opt }}</md-option>
|
||
* </md-select>
|
||
* </md-input-container>
|
||
* </hljs>
|
||
*
|
||
* Using the `md-select-header` element directive
|
||
*
|
||
* When a developer needs to put more than just a text label in the `md-select-menu`, they should
|
||
* use one or more `md-select-header`s. These elements can contain custom HTML which can be styled
|
||
* as desired. Use cases for this element include a sticky search bar and custom option group
|
||
* labels.
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-input-container>
|
||
* <md-select ng-model="someModel">
|
||
* <md-select-header>
|
||
* <span> Neighborhoods - </span>
|
||
* </md-select-header>
|
||
* <md-option ng-value="opt" ng-repeat="opt in neighborhoods2">{{ opt }}</md-option>
|
||
* </md-select>
|
||
* </md-input-container>
|
||
* </hljs>
|
||
*
|
||
* ## 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:
|
||
* <hljs lang="js">
|
||
* 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' };
|
||
* });
|
||
* </hljs>
|
||
* <hljs lang="html">
|
||
* <div ng-controller="MyCtrl">
|
||
* <md-select ng-model="selectedUser">
|
||
* <md-option ng-value="user" ng-repeat="user in users">{{ user.name }}</md-option>
|
||
* </md-select>
|
||
* </div>
|
||
* </hljs>
|
||
*
|
||
* At first one might expect that the select should be populated with "Bob" as the selected user.
|
||
* However, this is not true. To determine whether something is selected,
|
||
* `ngModelController` is looking at whether `$scope.selectedUser == (any user in $scope.users);`;
|
||
*
|
||
* Javascript's `==` operator does not check for deep equality (ie. that all properties
|
||
* on the object are the same), but instead whether the objects are *the same object in memory*.
|
||
* In this case, we have two instances of identical objects, but they exist in memory as unique
|
||
* entities. Because of this, the select will have no value populated for a selected user.
|
||
*
|
||
* To get around this, `ngModelController` provides a `track by` option that allows us to specify a
|
||
* different expression which will be used for the equality operator. As such, we can update our
|
||
* `html` to make use of this by specifying the `ng-model-options="{trackBy: '$value.id'}"` on the
|
||
* `md-select` element. This converts our equality expression to be
|
||
* `$scope.selectedUser.id == (any id in $scope.users.map(function(u) { return u.id; }));`
|
||
* which results in Bob being selected as desired.
|
||
*
|
||
* **Note:** We do not support AngularJS's `track by` syntax. For instance
|
||
* `ng-options="user in users track by user.id"` will not work with `md-select`.
|
||
*
|
||
* Working HTML:
|
||
* <hljs lang="html">
|
||
* <div ng-controller="MyCtrl">
|
||
* <md-select ng-model="selectedUser" ng-model-options="{trackBy: '$value.id'}">
|
||
* <md-option ng-value="user" ng-repeat="user in users">{{ user.name }}</md-option>
|
||
* </md-select>
|
||
* </div>
|
||
* </hljs>
|
||
*/
|
||
function SelectDirective($mdSelect, $mdUtil, $mdConstant, $mdTheming, $mdAria, $parse, $sce) {
|
||
return {
|
||
restrict: 'E',
|
||
require: ['^?mdInputContainer', 'mdSelect', 'ngModel', '?^form'],
|
||
compile: compile,
|
||
controller: function() {
|
||
} // empty placeholder controller to be initialized in link
|
||
};
|
||
|
||
/**
|
||
* @param {JQLite} tElement
|
||
* @param {IAttributes} tAttrs
|
||
* @return {postLink}
|
||
*/
|
||
function compile(tElement, tAttrs) {
|
||
var isMultiple = $mdUtil.parseAttributeBoolean(tAttrs.multiple);
|
||
tElement.addClass('md-auto-horizontal-margin');
|
||
|
||
// add the select value that will hold our placeholder or selected option value
|
||
var valueEl = angular.element('<md-select-value><span></span></md-select-value>');
|
||
valueEl.append('<span class="md-select-icon" aria-hidden="true"></span>');
|
||
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.
|
||
var mdContentEl = tElement.find('md-content');
|
||
if (!mdContentEl.length) {
|
||
tElement.append(angular.element('<md-content>').append(tElement.contents()));
|
||
mdContentEl = tElement.find('md-content');
|
||
}
|
||
mdContentEl.attr('role', 'listbox');
|
||
mdContentEl.attr('tabindex', '-1');
|
||
|
||
if (isMultiple) {
|
||
mdContentEl.attr('aria-multiselectable', 'true');
|
||
} else {
|
||
mdContentEl.attr('aria-multiselectable', 'false');
|
||
}
|
||
|
||
// Add progress spinner for md-options-loading
|
||
if (tAttrs.mdOnOpen) {
|
||
|
||
// Show progress indicator while loading async
|
||
// Use ng-hide for `display:none` so the indicator does not interfere with the options list
|
||
tElement
|
||
.find('md-content')
|
||
.prepend(angular.element(
|
||
'<div>' +
|
||
' <md-progress-circular md-mode="indeterminate" ng-if="$$loadingAsyncDone === false"' +
|
||
' md-diameter="25px"></md-progress-circular>' +
|
||
'</div>'
|
||
));
|
||
|
||
// Hide list [of item options] while loading async
|
||
tElement
|
||
.find('md-option')
|
||
.attr('ng-show', '$$loadingAsyncDone');
|
||
}
|
||
|
||
if (tAttrs.name) {
|
||
var autofillClone = angular.element('<select class="md-visually-hidden"></select>');
|
||
autofillClone.attr({
|
||
'name': tAttrs.name,
|
||
'aria-hidden': 'true',
|
||
'tabindex': '-1'
|
||
});
|
||
var opts = tElement.find('md-option');
|
||
angular.forEach(opts, function(el) {
|
||
var newEl = angular.element('<option>' + el.innerHTML + '</option>');
|
||
if (el.hasAttribute('ng-value')) {
|
||
newEl.attr('ng-value', el.getAttribute('ng-value'));
|
||
}
|
||
else if (el.hasAttribute('value')) {
|
||
newEl.attr('value', el.getAttribute('value'));
|
||
}
|
||
autofillClone.append(newEl);
|
||
});
|
||
|
||
// Adds an extra option that will hold the selected value for the
|
||
// cases where the select is a part of a non-AngularJS form. This can be done with a ng-model,
|
||
// however if the `md-option` is being `ng-repeat`-ed, AngularJS seems to insert a similar
|
||
// `option` node, but with a value of `? string: <value> ?` which would then get submitted.
|
||
// This also goes around having to prepend a dot to the name attribute.
|
||
autofillClone.append(
|
||
'<option ng-value="' + tAttrs.ngModel + '" selected></option>'
|
||
);
|
||
|
||
tElement.parent().append(autofillClone);
|
||
}
|
||
|
||
// Use everything that's left inside element.contents() as the contents of the menu
|
||
var multipleContent = isMultiple ? 'multiple' : '';
|
||
var ngModelOptions = tAttrs.ngModelOptions ? $mdUtil.supplant('ng-model-options="{0}"', [tAttrs.ngModelOptions]) : '';
|
||
var selectTemplate = '' +
|
||
'<div class="md-select-menu-container" aria-hidden="true" role="presentation">' +
|
||
' <md-select-menu role="presentation" {0} {1}>{2}</md-select-menu>' +
|
||
'</div>';
|
||
|
||
selectTemplate = $mdUtil.supplant(selectTemplate, [multipleContent, ngModelOptions, tElement.html()]);
|
||
tElement.empty().append(valueEl);
|
||
tElement.append(selectTemplate);
|
||
|
||
if (!tAttrs.tabindex) {
|
||
tAttrs.$set('tabindex', 0);
|
||
}
|
||
|
||
return function postLink(scope, element, attrs, ctrls) {
|
||
var untouched = true;
|
||
var isDisabled;
|
||
|
||
var containerCtrl = ctrls[0];
|
||
var mdSelectCtrl = ctrls[1];
|
||
var ngModelCtrl = ctrls[2];
|
||
var formCtrl = ctrls[3];
|
||
// grab a reference to the select menu value label
|
||
var selectValueElement = element.find('md-select-value');
|
||
var isReadonly = angular.isDefined(attrs.readonly);
|
||
var disableAsterisk = $mdUtil.parseAttributeBoolean(attrs.mdNoAsterisk);
|
||
var stopMdMultipleWatch;
|
||
var userDefinedLabelledby = angular.isDefined(attrs.ariaLabelledby);
|
||
var listboxContentElement = element.find('md-content');
|
||
var initialPlaceholder = element.attr('placeholder');
|
||
|
||
if (disableAsterisk) {
|
||
element.addClass('md-no-asterisk');
|
||
}
|
||
|
||
if (containerCtrl) {
|
||
var isErrorGetter = containerCtrl.isErrorGetter || function() {
|
||
return ngModelCtrl.$invalid && (ngModelCtrl.$touched || (formCtrl && formCtrl.$submitted));
|
||
};
|
||
|
||
if (containerCtrl.input) {
|
||
// We ignore inputs that are in the md-select-header.
|
||
// One case where this might be useful would be adding as searchbox.
|
||
if (element.find('md-select-header').find('input')[0] !== containerCtrl.input[0]) {
|
||
throw new Error("<md-input-container> can only have *one* child <input>, <textarea>, or <select> element!");
|
||
}
|
||
}
|
||
|
||
containerCtrl.input = element;
|
||
if (!containerCtrl.label) {
|
||
$mdAria.expect(element, 'aria-label', initialPlaceholder);
|
||
var selectLabel = element.attr('aria-label');
|
||
if (!selectLabel) {
|
||
selectLabel = initialPlaceholder;
|
||
}
|
||
listboxContentElement.attr('aria-label', selectLabel);
|
||
} else {
|
||
containerCtrl.label.attr('aria-hidden', 'true');
|
||
listboxContentElement.attr('aria-label', containerCtrl.label.text());
|
||
containerCtrl.setHasPlaceholder(!!initialPlaceholder);
|
||
}
|
||
|
||
var stopInvalidWatch = scope.$watch(isErrorGetter, containerCtrl.setInvalid);
|
||
}
|
||
|
||
var selectContainer, selectScope, selectMenuCtrl;
|
||
|
||
selectContainer = findSelectContainer();
|
||
$mdTheming(element);
|
||
|
||
var originalRender = ngModelCtrl.$render;
|
||
ngModelCtrl.$render = function() {
|
||
originalRender();
|
||
syncSelectValueText();
|
||
inputCheckValue();
|
||
};
|
||
|
||
var stopPlaceholderObserver = attrs.$observe('placeholder', ngModelCtrl.$render);
|
||
|
||
var stopRequiredObserver = attrs.$observe('required', function (value) {
|
||
if (containerCtrl && containerCtrl.label) {
|
||
// Toggle the md-required class on the input containers label, because the input container
|
||
// is automatically applying the asterisk indicator on the label.
|
||
containerCtrl.label.toggleClass('md-required', value && !disableAsterisk);
|
||
}
|
||
element.removeAttr('aria-required');
|
||
if (value) {
|
||
listboxContentElement.attr('aria-required', 'true');
|
||
} else {
|
||
listboxContentElement.removeAttr('aria-required');
|
||
}
|
||
});
|
||
|
||
/**
|
||
* Set the contents of the md-select-value element. This element's contents are announced by
|
||
* screen readers and used for displaying the value of the select in both single and multiple
|
||
* selection modes.
|
||
* @param {string=} text A sanitized and trusted HTML string or a pure text string from user
|
||
* input.
|
||
*/
|
||
mdSelectCtrl.setSelectValueText = function(text) {
|
||
var useDefaultText = text === undefined || text === '';
|
||
// Whether the select label has been given via user content rather than the internal
|
||
// template of <md-option>
|
||
var isSelectLabelFromUser = false;
|
||
|
||
mdSelectCtrl.setIsPlaceholder(!text);
|
||
|
||
if (attrs.mdSelectedText && attrs.mdSelectedHtml) {
|
||
throw Error('md-select cannot have both `md-selected-text` and `md-selected-html`');
|
||
}
|
||
|
||
if (attrs.mdSelectedText || attrs.mdSelectedHtml) {
|
||
text = $parse(attrs.mdSelectedText || attrs.mdSelectedHtml)(scope);
|
||
isSelectLabelFromUser = true;
|
||
} else if (useDefaultText) {
|
||
// Use placeholder attribute, otherwise fallback to the md-input-container label
|
||
var tmpPlaceholder = attrs.placeholder ||
|
||
(containerCtrl && containerCtrl.label ? containerCtrl.label.text() : '');
|
||
|
||
text = tmpPlaceholder || '';
|
||
isSelectLabelFromUser = true;
|
||
}
|
||
|
||
var target = selectValueElement.children().eq(0);
|
||
|
||
if (attrs.mdSelectedHtml) {
|
||
// Using getTrustedHtml will run the content through $sanitize if it is not already
|
||
// explicitly trusted. If the ngSanitize module is not loaded, this will
|
||
// *correctly* throw an sce error.
|
||
target.html($sce.getTrustedHtml(text));
|
||
} else if (isSelectLabelFromUser) {
|
||
target.text(text);
|
||
} else {
|
||
// If we've reached this point, the text is not user-provided.
|
||
target.html(text);
|
||
}
|
||
|
||
if (useDefaultText) {
|
||
// Avoid screen readers double announcing the label name when no value has been selected
|
||
selectValueElement.attr('aria-hidden', 'true');
|
||
if (!userDefinedLabelledby) {
|
||
element.removeAttr('aria-labelledby');
|
||
}
|
||
} else {
|
||
selectValueElement.removeAttr('aria-hidden');
|
||
if (!userDefinedLabelledby) {
|
||
element.attr('aria-labelledby', element[0].id + ' ' + selectValueElement[0].id);
|
||
}
|
||
}
|
||
};
|
||
|
||
/**
|
||
* @param {boolean} isPlaceholder true to mark the md-select-value element and
|
||
* input container, if one exists, with classes for styling when a placeholder is present.
|
||
* false to remove those classes.
|
||
*/
|
||
mdSelectCtrl.setIsPlaceholder = function(isPlaceholder) {
|
||
if (isPlaceholder) {
|
||
selectValueElement.addClass('md-select-placeholder');
|
||
// Don't hide the floating label if the md-select has a placeholder.
|
||
if (containerCtrl && containerCtrl.label && !element.attr('placeholder')) {
|
||
containerCtrl.label.addClass('md-placeholder');
|
||
}
|
||
} else {
|
||
selectValueElement.removeClass('md-select-placeholder');
|
||
if (containerCtrl && containerCtrl.label && !element.attr('placeholder')) {
|
||
containerCtrl.label.removeClass('md-placeholder');
|
||
}
|
||
}
|
||
};
|
||
|
||
if (!isReadonly) {
|
||
var handleBlur = function(event) {
|
||
// Attach before ngModel's blur listener to stop propagation of blur event
|
||
// and prevent setting $touched.
|
||
if (untouched) {
|
||
untouched = false;
|
||
if (selectScope._mdSelectIsOpen) {
|
||
event.stopImmediatePropagation();
|
||
}
|
||
}
|
||
|
||
containerCtrl && containerCtrl.setFocused(false);
|
||
inputCheckValue();
|
||
};
|
||
var handleFocus = function() {
|
||
// Always focus the container (if we have one) so floating labels and other styles are
|
||
// applied properly
|
||
containerCtrl && containerCtrl.setFocused(true);
|
||
};
|
||
|
||
element.on('focus', handleFocus);
|
||
element.on('blur', handleBlur);
|
||
}
|
||
|
||
mdSelectCtrl.triggerClose = function() {
|
||
$parse(attrs.mdOnClose)(scope);
|
||
};
|
||
|
||
scope.$$postDigest(function() {
|
||
initAriaLabel();
|
||
syncSelectValueText();
|
||
});
|
||
|
||
function initAriaLabel() {
|
||
var labelText = element.attr('aria-label') || element.attr('placeholder');
|
||
if (!labelText && containerCtrl && containerCtrl.label) {
|
||
labelText = containerCtrl.label.text();
|
||
}
|
||
$mdAria.expect(element, 'aria-label', labelText);
|
||
}
|
||
|
||
var stopSelectedLabelsWatcher = scope.$watch(function() {
|
||
return selectMenuCtrl.getSelectedLabels();
|
||
}, syncSelectValueText);
|
||
|
||
function syncSelectValueText() {
|
||
selectMenuCtrl = selectMenuCtrl ||
|
||
selectContainer.find('md-select-menu').controller('mdSelectMenu');
|
||
mdSelectCtrl.setSelectValueText(selectMenuCtrl.getSelectedLabels());
|
||
}
|
||
|
||
// TODO add tests for mdMultiple
|
||
// TODO add docs for mdMultiple
|
||
var stopMdMultipleObserver = attrs.$observe('mdMultiple', function(val) {
|
||
if (stopMdMultipleWatch) {
|
||
stopMdMultipleWatch();
|
||
}
|
||
var parser = $parse(val);
|
||
stopMdMultipleWatch = scope.$watch(function() {
|
||
return parser(scope);
|
||
}, function(multiple, prevVal) {
|
||
var selectMenu = selectContainer.find('md-select-menu');
|
||
// assume compiler did a good job
|
||
if (multiple === undefined && prevVal === undefined) {
|
||
return;
|
||
}
|
||
if (multiple) {
|
||
var setMultipleAttrs = {'multiple': 'multiple'};
|
||
element.attr(setMultipleAttrs);
|
||
selectMenu.attr(setMultipleAttrs);
|
||
} else {
|
||
element.removeAttr('multiple');
|
||
selectMenu.removeAttr('multiple');
|
||
}
|
||
element.find('md-content').attr('aria-multiselectable', multiple ? 'true' : 'false');
|
||
|
||
if (selectContainer) {
|
||
selectMenuCtrl.setMultiple(Boolean(multiple));
|
||
originalRender = ngModelCtrl.$render;
|
||
ngModelCtrl.$render = function() {
|
||
originalRender();
|
||
syncSelectValueText();
|
||
inputCheckValue();
|
||
};
|
||
ngModelCtrl.$render();
|
||
}
|
||
});
|
||
});
|
||
|
||
var stopDisabledObserver = attrs.$observe('disabled', function(disabled) {
|
||
if (angular.isString(disabled)) {
|
||
disabled = true;
|
||
}
|
||
// Prevent click event being registered twice
|
||
if (isDisabled !== undefined && isDisabled === disabled) {
|
||
return;
|
||
}
|
||
isDisabled = disabled;
|
||
if (disabled) {
|
||
element
|
||
.attr({'aria-disabled': 'true'})
|
||
.removeAttr('tabindex')
|
||
.removeAttr('aria-expanded')
|
||
.removeAttr('aria-haspopup')
|
||
.off('click', openSelect)
|
||
.off('keydown', handleKeypress);
|
||
} else {
|
||
element
|
||
.attr({
|
||
'tabindex': attrs.tabindex,
|
||
'aria-haspopup': 'listbox'
|
||
})
|
||
.removeAttr('aria-disabled')
|
||
.on('click', openSelect)
|
||
.on('keydown', handleKeypress);
|
||
}
|
||
});
|
||
|
||
if (!attrs.hasOwnProperty('disabled') && !attrs.hasOwnProperty('ngDisabled')) {
|
||
element.attr({'aria-disabled': 'false'});
|
||
element.on('click', openSelect);
|
||
element.on('keydown', handleKeypress);
|
||
}
|
||
|
||
var ariaAttrs = {
|
||
role: 'button',
|
||
'aria-haspopup': 'listbox'
|
||
};
|
||
|
||
if (!element[0].hasAttribute('id')) {
|
||
ariaAttrs.id = 'select_' + $mdUtil.nextUid();
|
||
}
|
||
|
||
var containerId = 'select_container_' + $mdUtil.nextUid();
|
||
selectContainer.attr('id', containerId);
|
||
var listboxContentId = 'select_listbox_' + $mdUtil.nextUid();
|
||
selectContainer.find('md-content').attr('id', listboxContentId);
|
||
// Only add aria-owns if element ownership is NOT represented in the DOM.
|
||
if (!element.find('md-select-menu').length) {
|
||
ariaAttrs['aria-owns'] = listboxContentId;
|
||
}
|
||
element.attr(ariaAttrs);
|
||
|
||
scope.$on('$destroy', function() {
|
||
stopRequiredObserver && stopRequiredObserver();
|
||
stopDisabledObserver && stopDisabledObserver();
|
||
stopMdMultipleWatch && stopMdMultipleWatch();
|
||
stopMdMultipleObserver && stopMdMultipleObserver();
|
||
stopSelectedLabelsWatcher && stopSelectedLabelsWatcher();
|
||
stopPlaceholderObserver && stopPlaceholderObserver();
|
||
stopInvalidWatch && stopInvalidWatch();
|
||
|
||
element.off('focus');
|
||
element.off('blur');
|
||
|
||
$mdSelect
|
||
.destroy()
|
||
.finally(function() {
|
||
if (containerCtrl) {
|
||
containerCtrl.setFocused(false);
|
||
containerCtrl.setHasValue(false);
|
||
containerCtrl.input = null;
|
||
}
|
||
ngModelCtrl.$setTouched();
|
||
});
|
||
});
|
||
|
||
function inputCheckValue() {
|
||
// The select counts as having a value if one or more options are selected,
|
||
// or if the input's validity state says it has bad input (eg: string in a number input).
|
||
// We must do this on nextTick as the $render is sometimes invoked on nextTick.
|
||
$mdUtil.nextTick(function () {
|
||
containerCtrl && containerCtrl.setHasValue(
|
||
selectMenuCtrl.getSelectedLabels().length > 0 || (element[0].validity || {}).badInput);
|
||
});
|
||
}
|
||
|
||
function findSelectContainer() {
|
||
var selectContainer = angular.element(
|
||
element[0].querySelector('.md-select-menu-container')
|
||
);
|
||
selectScope = scope;
|
||
attrs.mdContainerClass && selectContainer.addClass(attrs.mdContainerClass);
|
||
selectMenuCtrl = selectContainer.find('md-select-menu').controller('mdSelectMenu');
|
||
selectMenuCtrl.init(ngModelCtrl, attrs);
|
||
element.on('$destroy', function() {
|
||
selectContainer.remove();
|
||
});
|
||
return selectContainer;
|
||
}
|
||
|
||
/**
|
||
* Determine if the select menu should be opened or an option in the select menu should be
|
||
* selected.
|
||
* @param {KeyboardEvent} e keyboard event to handle
|
||
*/
|
||
function handleKeypress(e) {
|
||
if ($mdConstant.isNavigationKey(e)) {
|
||
// prevent page scrolling on interaction
|
||
e.preventDefault();
|
||
openSelect(e);
|
||
} else {
|
||
if (shouldHandleKey(e, $mdConstant)) {
|
||
e.preventDefault();
|
||
|
||
var node = selectMenuCtrl.optNodeForKeyboardSearch(e);
|
||
if (!node || node.hasAttribute('disabled')) {
|
||
return;
|
||
}
|
||
var optionCtrl = angular.element(node).controller('mdOption');
|
||
if (!selectMenuCtrl.isMultiple) {
|
||
angular.forEach(Object.keys(selectMenuCtrl.selected), function (key) {
|
||
selectMenuCtrl.deselect(key);
|
||
});
|
||
}
|
||
selectMenuCtrl.select(optionCtrl.hashKey, optionCtrl.value);
|
||
selectMenuCtrl.refreshViewValue();
|
||
}
|
||
}
|
||
}
|
||
|
||
function openSelect() {
|
||
selectScope._mdSelectIsOpen = true;
|
||
element.attr('aria-expanded', 'true');
|
||
|
||
$mdSelect.show({
|
||
scope: selectScope,
|
||
preserveScope: true,
|
||
skipCompile: true,
|
||
element: selectContainer,
|
||
target: element[0],
|
||
selectCtrl: mdSelectCtrl,
|
||
preserveElement: true,
|
||
hasBackdrop: true,
|
||
loadingAsync: attrs.mdOnOpen ? scope.$eval(attrs.mdOnOpen) || true : false
|
||
}).finally(function() {
|
||
selectScope._mdSelectIsOpen = false;
|
||
element.removeAttr('aria-expanded');
|
||
element.removeAttr('aria-activedescendant');
|
||
ngModelCtrl.$setTouched();
|
||
});
|
||
}
|
||
|
||
};
|
||
}
|
||
}
|
||
|
||
function SelectMenuDirective($parse, $mdUtil, $mdConstant, $mdTheming) {
|
||
// We want the scope to be set to 'false' so an isolated scope is not created
|
||
// which would interfere with the md-select-header's access to the
|
||
// parent scope.
|
||
SelectMenuController.$inject = ["$scope", "$attrs", "$element"];
|
||
return {
|
||
restrict: 'E',
|
||
require: ['mdSelectMenu'],
|
||
scope: false,
|
||
controller: SelectMenuController,
|
||
link: {pre: preLink}
|
||
};
|
||
|
||
// We use preLink instead of postLink to ensure that the select is initialized before
|
||
// its child options run postLink.
|
||
function preLink(scope, element, attrs, ctrls) {
|
||
var selectMenuCtrl = ctrls[0];
|
||
|
||
element.addClass('_md'); // private md component indicator for styling
|
||
|
||
$mdTheming(element);
|
||
element.on('click', clickListener);
|
||
element.on('keypress', keyListener);
|
||
|
||
/**
|
||
* @param {KeyboardEvent} keyboardEvent
|
||
*/
|
||
function keyListener(keyboardEvent) {
|
||
if (keyboardEvent.keyCode === 13 || keyboardEvent.keyCode === 32) {
|
||
clickListener(keyboardEvent);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {Event} mouseEvent
|
||
* @return {void}
|
||
*/
|
||
function clickListener(mouseEvent) {
|
||
var option = $mdUtil.getClosest(mouseEvent.target, 'md-option');
|
||
var optionCtrl = option && angular.element(option).data('$mdOptionController');
|
||
|
||
if (!option || !optionCtrl) {
|
||
// Avoid closing the menu when the select header's input is clicked
|
||
if (mouseEvent.target && mouseEvent.target.parentNode &&
|
||
mouseEvent.target.parentNode.tagName === 'MD-SELECT-HEADER') {
|
||
mouseEvent.stopImmediatePropagation();
|
||
}
|
||
return;
|
||
} else if (option.hasAttribute('disabled')) {
|
||
mouseEvent.stopImmediatePropagation();
|
||
return;
|
||
}
|
||
|
||
var optionHashKey = selectMenuCtrl.hashGetter(optionCtrl.value);
|
||
var isSelected = angular.isDefined(selectMenuCtrl.selected[optionHashKey]);
|
||
|
||
scope.$apply(function() {
|
||
if (selectMenuCtrl.isMultiple) {
|
||
if (isSelected) {
|
||
selectMenuCtrl.deselect(optionHashKey);
|
||
} else {
|
||
selectMenuCtrl.select(optionHashKey, optionCtrl.value);
|
||
}
|
||
} else {
|
||
if (!isSelected) {
|
||
angular.forEach(Object.keys(selectMenuCtrl.selected), function (key) {
|
||
selectMenuCtrl.deselect(key);
|
||
});
|
||
selectMenuCtrl.select(optionHashKey, optionCtrl.value);
|
||
}
|
||
}
|
||
selectMenuCtrl.refreshViewValue();
|
||
});
|
||
}
|
||
}
|
||
|
||
function SelectMenuController($scope, $attrs, $element) {
|
||
var self = this;
|
||
var defaultIsEmpty;
|
||
var searchStr = '';
|
||
var clearSearchTimeout, optNodes, optText;
|
||
var CLEAR_SEARCH_AFTER = 300;
|
||
|
||
self.isMultiple = angular.isDefined($attrs.multiple);
|
||
// selected is an object with keys matching all of the selected options' hashed values
|
||
self.selected = {};
|
||
// options is an object with keys matching every option's hash value,
|
||
// and values containing an instance of every option's controller.
|
||
self.options = {};
|
||
|
||
$scope.$watchCollection(function() {
|
||
return self.options;
|
||
}, function() {
|
||
self.ngModel.$render();
|
||
updateOptionSetSizeAndPosition();
|
||
});
|
||
|
||
/**
|
||
* @param {boolean} isMultiple
|
||
*/
|
||
self.setMultiple = function(isMultiple) {
|
||
var ngModel = self.ngModel;
|
||
defaultIsEmpty = defaultIsEmpty || ngModel.$isEmpty;
|
||
self.isMultiple = isMultiple;
|
||
|
||
if (self.isMultiple) {
|
||
// We want to delay the render method so that the directive has a chance to load before
|
||
// rendering, this prevents the control being marked as dirty onload.
|
||
var loaded = false;
|
||
var delayedRender = function(val) {
|
||
if (!loaded) {
|
||
$mdUtil.nextTick(function () {
|
||
renderMultiple(val);
|
||
loaded = true;
|
||
});
|
||
} else {
|
||
renderMultiple(val);
|
||
}
|
||
};
|
||
ngModel.$validators['md-multiple'] = validateArray;
|
||
ngModel.$render = delayedRender;
|
||
|
||
// watchCollection on the model because by default ngModel only watches the model's
|
||
// reference. This allows the developer to also push and pop from their array.
|
||
$scope.$watchCollection(self.modelBinding, function(value) {
|
||
if (validateArray(value)) {
|
||
delayedRender(value);
|
||
}
|
||
});
|
||
|
||
ngModel.$isEmpty = function(value) {
|
||
return !value || value.length === 0;
|
||
};
|
||
} else {
|
||
delete ngModel.$validators['md-multiple'];
|
||
ngModel.$render = renderSingular;
|
||
}
|
||
|
||
function validateArray(modelValue, viewValue) {
|
||
// If a value is truthy but not an array, reject it.
|
||
// If value is undefined/falsy, accept that it's an empty array.
|
||
return angular.isArray(modelValue || viewValue || []);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* @param {KeyboardEvent} keyboardEvent keyboard event to handle
|
||
* @return {Element|HTMLElement|undefined}
|
||
*/
|
||
self.optNodeForKeyboardSearch = function(keyboardEvent) {
|
||
var search, i;
|
||
clearSearchTimeout && clearTimeout(clearSearchTimeout);
|
||
clearSearchTimeout = setTimeout(function() {
|
||
clearSearchTimeout = undefined;
|
||
searchStr = '';
|
||
optText = undefined;
|
||
optNodes = undefined;
|
||
}, CLEAR_SEARCH_AFTER);
|
||
|
||
searchStr += keyboardEvent.key;
|
||
search = new RegExp('^' + $mdUtil.sanitize(searchStr), 'i');
|
||
if (!optNodes) {
|
||
optNodes = $element.find('md-option');
|
||
optText = new Array(optNodes.length);
|
||
angular.forEach(optNodes, function(el, i) {
|
||
optText[i] = el.textContent.trim();
|
||
});
|
||
}
|
||
|
||
for (i = 0; i < optText.length; ++i) {
|
||
if (search.test(optText[i])) {
|
||
return optNodes[i];
|
||
}
|
||
}
|
||
};
|
||
|
||
self.init = function(ngModel, parentAttrs) {
|
||
self.ngModel = ngModel;
|
||
self.modelBinding = parentAttrs.ngModel;
|
||
|
||
// Setup a more robust version of isEmpty to ensure value is a valid option
|
||
self.ngModel.$isEmpty = function($viewValue) {
|
||
// We have to transform the viewValue into the hashKey, because otherwise the
|
||
// OptionCtrl may not exist. Developers may have specified a trackBy function.
|
||
var hashedValue = self.options[self.hashGetter($viewValue)] ? self.options[self.hashGetter($viewValue)].value : null;
|
||
// Base this check on the default AngularJS $isEmpty() function.
|
||
// eslint-disable-next-line no-self-compare
|
||
return !angular.isDefined(hashedValue) || hashedValue === null || hashedValue === '' || hashedValue !== hashedValue;
|
||
};
|
||
|
||
// Allow users to provide `ng-model="foo" ng-model-options="{trackBy: '$value.id'}"` so
|
||
// that we can properly compare objects set on the model to the available options
|
||
//
|
||
// If the user doesn't provide a trackBy, we automatically generate an id for every
|
||
// value passed in with the getId function
|
||
if ($attrs.ngModelOptions) {
|
||
self.hashGetter = function(value) {
|
||
var ngModelOptions = $parse($attrs.ngModelOptions)($scope);
|
||
var trackByOption = ngModelOptions && ngModelOptions.trackBy;
|
||
|
||
if (trackByOption) {
|
||
return $parse(trackByOption)($scope, { $value: value });
|
||
} else if (angular.isObject(value)) {
|
||
return getId(value);
|
||
}
|
||
return value;
|
||
};
|
||
} else {
|
||
self.hashGetter = getId;
|
||
}
|
||
self.setMultiple(self.isMultiple);
|
||
|
||
/**
|
||
* If the value is an object, get the unique, incremental id of the value.
|
||
* If it's not an object, the value will be converted to a string and then returned.
|
||
* @param value
|
||
* @returns {string}
|
||
*/
|
||
function getId(value) {
|
||
if (angular.isObject(value) && !angular.isArray(value)) {
|
||
return 'object_' + (value.$$mdSelectId || (value.$$mdSelectId = ++selectNextId));
|
||
}
|
||
return value + '';
|
||
}
|
||
|
||
if (parentAttrs.hasOwnProperty('mdSelectOnlyOption')) {
|
||
$mdUtil.nextTick(function() {
|
||
var optionKeys = Object.keys(self.options);
|
||
|
||
if (optionKeys.length === 1) {
|
||
var option = self.options[optionKeys[0]];
|
||
|
||
self.deselect(Object.keys(self.selected)[0]);
|
||
self.select(self.hashGetter(option.value), option.value);
|
||
self.refreshViewValue();
|
||
self.ngModel.$setPristine();
|
||
}
|
||
}, false);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* @param {string=} id
|
||
*/
|
||
self.setActiveDescendant = function(id) {
|
||
if (angular.isDefined(id)) {
|
||
$element.find('md-content').attr('aria-activedescendant', id);
|
||
} else {
|
||
$element.find('md-content').removeAttr('aria-activedescendant');
|
||
}
|
||
};
|
||
|
||
/**
|
||
* @param {{mode: string}=} opts options object to allow specifying html (default) or aria mode.
|
||
* @return {string} comma separated set of selected values
|
||
*/
|
||
self.getSelectedLabels = function(opts) {
|
||
opts = opts || {};
|
||
var mode = opts.mode || 'html';
|
||
var selectedOptionEls =
|
||
$mdUtil.nodesToArray($element[0].querySelectorAll('md-option[selected]'));
|
||
|
||
if (selectedOptionEls.length) {
|
||
var mapFn;
|
||
|
||
if (mode === 'html') {
|
||
// Map the given element to its innerHTML string. If the element has a child ripple
|
||
// container remove it from the HTML string, before returning the string.
|
||
mapFn = function(el) {
|
||
// If we do not have a `value` or `ng-value`, assume it is an empty option which clears
|
||
// the select.
|
||
if (el.hasAttribute('md-option-empty')) {
|
||
return '';
|
||
}
|
||
|
||
var html = el.innerHTML;
|
||
|
||
// Remove the ripple container from the selected option, copying it would cause a CSP
|
||
// violation.
|
||
var rippleContainer = el.querySelector('.md-ripple-container');
|
||
if (rippleContainer) {
|
||
html = html.replace(rippleContainer.outerHTML, '');
|
||
}
|
||
|
||
// Remove the checkbox container, because it will cause the label to wrap inside of the
|
||
// placeholder. It should be not displayed inside of the label element.
|
||
var checkboxContainer = el.querySelector('.md-container');
|
||
if (checkboxContainer) {
|
||
html = html.replace(checkboxContainer.outerHTML, '');
|
||
}
|
||
|
||
return html;
|
||
};
|
||
} else if (mode === 'aria') {
|
||
mapFn = function(el) {
|
||
return el.hasAttribute('aria-label') ? el.getAttribute('aria-label') : el.textContent;
|
||
};
|
||
}
|
||
|
||
// Ensure there are no duplicates; see https://github.com/angular/material/issues/9442
|
||
return $mdUtil.uniq(selectedOptionEls.map(mapFn)).join(', ');
|
||
} else {
|
||
return '';
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Mark an option as selected
|
||
* @param {string} hashKey key within the SelectMenuController.options object, which is an
|
||
* instance of OptionController.
|
||
* @param {OptionController} hashedValue value to associate with the key
|
||
*/
|
||
self.select = function(hashKey, hashedValue) {
|
||
var option = self.options[hashKey];
|
||
option && option.setSelected(true, self.isMultiple);
|
||
self.selected[hashKey] = hashedValue;
|
||
};
|
||
|
||
/**
|
||
* Mark an option as not selected
|
||
* @param {string} hashKey key within the SelectMenuController.options object, which is an
|
||
* instance of OptionController.
|
||
*/
|
||
self.deselect = function(hashKey) {
|
||
var option = self.options[hashKey];
|
||
option && option.setSelected(false, self.isMultiple);
|
||
delete self.selected[hashKey];
|
||
};
|
||
|
||
/**
|
||
* Add an option to the select
|
||
* @param {string} hashKey key within the SelectMenuController.options object, which is an
|
||
* instance of OptionController.
|
||
* @param {OptionController} optionCtrl instance to associate with the key
|
||
*/
|
||
self.addOption = function(hashKey, optionCtrl) {
|
||
if (angular.isDefined(self.options[hashKey])) {
|
||
throw new Error('Duplicate md-option values are not allowed in a select. ' +
|
||
'Duplicate value "' + optionCtrl.value + '" found.');
|
||
}
|
||
|
||
self.options[hashKey] = optionCtrl;
|
||
|
||
// If this option's value was already in our ngModel, go ahead and select it.
|
||
if (angular.isDefined(self.selected[hashKey])) {
|
||
self.select(hashKey, optionCtrl.value);
|
||
|
||
// When the current $modelValue of the ngModel Controller is using the same hash as
|
||
// the current option, which will be added, then we can be sure, that the validation
|
||
// of the option has occurred before the option was added properly.
|
||
// This means, that we have to manually trigger a new validation of the current option.
|
||
if (angular.isDefined(self.ngModel.$$rawModelValue) &&
|
||
self.hashGetter(self.ngModel.$$rawModelValue) === hashKey) {
|
||
self.ngModel.$validate();
|
||
}
|
||
|
||
self.refreshViewValue();
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Remove an option from the select
|
||
* @param {string} hashKey key within the SelectMenuController.options object, which is an
|
||
* instance of OptionController.
|
||
*/
|
||
self.removeOption = function(hashKey) {
|
||
delete self.options[hashKey];
|
||
// Don't deselect an option when it's removed - the user's ngModel should be allowed
|
||
// to have values that do not match a currently available option.
|
||
};
|
||
|
||
self.refreshViewValue = function() {
|
||
var values = [];
|
||
var option;
|
||
for (var hashKey in self.selected) {
|
||
// If this hashKey has an associated option, push that option's value to the model.
|
||
if ((option = self.options[hashKey])) {
|
||
values.push(option.value);
|
||
} else {
|
||
// Otherwise, the given hashKey has no associated option, and we got it
|
||
// from an ngModel value at an earlier time. Push the unhashed value of
|
||
// this hashKey to the model.
|
||
// This allows the developer to put a value in the model that doesn't yet have
|
||
// an associated option.
|
||
values.push(self.selected[hashKey]);
|
||
}
|
||
}
|
||
|
||
var newVal = self.isMultiple ? values : values[0];
|
||
var prevVal = self.ngModel.$modelValue;
|
||
|
||
if (!equals(prevVal, newVal)) {
|
||
self.ngModel.$setViewValue(newVal);
|
||
self.ngModel.$render();
|
||
}
|
||
|
||
function equals(prevVal, newVal) {
|
||
if (self.isMultiple) {
|
||
if (!angular.isArray(prevVal)) {
|
||
// newVal is always an array when self.isMultiple is true
|
||
// thus, if prevVal is not an array they are different
|
||
return false;
|
||
} else if (prevVal.length !== newVal.length) {
|
||
// they are different if they have different length
|
||
return false;
|
||
} else {
|
||
// if they have the same length, then they are different
|
||
// if an item in the newVal array can't be found in the prevVal
|
||
var prevValHashes = prevVal.map(function(prevValItem) {
|
||
return self.hashGetter(prevValItem);
|
||
});
|
||
return newVal.every(function(newValItem) {
|
||
var newValItemHash = self.hashGetter(newValItem);
|
||
return prevValHashes.some(function(prevValHash) {
|
||
return prevValHash === newValItemHash;
|
||
});
|
||
});
|
||
}
|
||
} else {
|
||
return self.hashGetter(prevVal) === self.hashGetter(newVal);
|
||
}
|
||
}
|
||
};
|
||
|
||
/**
|
||
* If the options include md-optgroups, then we need to apply aria-setsize and aria-posinset
|
||
* to help screen readers understand the indexes. When md-optgroups are not used, we save on
|
||
* perf and extra attributes by not applying these attributes as they are not needed by screen
|
||
* readers.
|
||
*/
|
||
function updateOptionSetSizeAndPosition() {
|
||
var i, options;
|
||
var hasOptGroup = $element.find('md-optgroup');
|
||
if (!hasOptGroup.length) {
|
||
return;
|
||
}
|
||
|
||
options = $element.find('md-option');
|
||
|
||
for (i = 0; i < options.length; i++) {
|
||
options[i].setAttribute('aria-setsize', options.length);
|
||
options[i].setAttribute('aria-posinset', i + 1);
|
||
}
|
||
}
|
||
|
||
function renderMultiple() {
|
||
var newSelectedValues = self.ngModel.$modelValue || self.ngModel.$viewValue || [];
|
||
if (!angular.isArray(newSelectedValues)) {
|
||
return;
|
||
}
|
||
|
||
var oldSelected = Object.keys(self.selected);
|
||
|
||
var newSelectedHashes = newSelectedValues.map(self.hashGetter);
|
||
var deselected = oldSelected.filter(function(hash) {
|
||
return newSelectedHashes.indexOf(hash) === -1;
|
||
});
|
||
|
||
deselected.forEach(self.deselect);
|
||
newSelectedHashes.forEach(function(hashKey, i) {
|
||
self.select(hashKey, newSelectedValues[i]);
|
||
});
|
||
}
|
||
|
||
function renderSingular() {
|
||
var value = self.ngModel.$viewValue || self.ngModel.$modelValue;
|
||
Object.keys(self.selected).forEach(self.deselect);
|
||
self.select(self.hashGetter(value), value);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdOption
|
||
* @restrict E
|
||
* @module material.components.select
|
||
*
|
||
* @description Displays an option in a <a ng-href="api/directive/mdSelect">md-select</a> box's
|
||
* dropdown menu. Options can be grouped using
|
||
* <a ng-href="api/directive/mdOptgroup">md-optgroup</a> element directives.
|
||
*
|
||
* ### Option Params
|
||
*
|
||
* When applied, `md-option-empty` will mark the option as "empty" allowing the option to clear the
|
||
* select and put it back in it's default state. You may supply this attribute on any option you
|
||
* wish, however, it is automatically applied to an option whose `value` or `ng-value` are not
|
||
* defined.
|
||
*
|
||
* **Automatically Applied**
|
||
*
|
||
* - `<md-option>`
|
||
* - `<md-option value>`
|
||
* - `<md-option value="">`
|
||
* - `<md-option ng-value>`
|
||
* - `<md-option ng-value="">`
|
||
*
|
||
* **NOT Automatically Applied**
|
||
*
|
||
* - `<md-option ng-value="1">`
|
||
* - `<md-option ng-value="''">`
|
||
* - `<md-option ng-value="undefined">`
|
||
* - `<md-option value="undefined">` (this evaluates to the string `"undefined"`)
|
||
* - <code ng-non-bindable><md-option ng-value="{{someValueThatMightBeUndefined}}"></code>
|
||
*
|
||
* **Note:** A value of `undefined` ***is considered a valid value*** (and does not auto-apply this
|
||
* attribute) since you may wish this to be your "Not Available" or "None" option.
|
||
*
|
||
* **Note:** Using the
|
||
* <a ng-href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/option#Attributes">value</a>
|
||
* attribute from the `<option>` element (as opposed to the `<md-option>` element's
|
||
* <a ng-href="https://docs.angularjs.org/api/ng/directive/ngValue">ng-value</a>) always evaluates
|
||
* to a `string`. This means that `value="null"` will cause a check against `myValue != "null"`
|
||
* rather than `!myValue` or `myValue != null`.
|
||
* Importantly, this also applies to `number` values. `value="1"` will not match up with an
|
||
* `ng-model` like `$scope.selectedValue = 1`. Use `ng-value="1"` in this case and other cases where
|
||
* you have values that are not strings.
|
||
*
|
||
* **Note:** Please see our <a ng-href="api/directive/mdSelect#selects-and-object-equality">docs on
|
||
* using objects with `md-select`</a> for additional guidance on using the `trackBy` option with
|
||
* `ng-model-options`.
|
||
*
|
||
* @param {expression=} ng-value Binds the given expression to the value of the option.
|
||
* @param {string=} value Attribute to set the value of the option.
|
||
* @param {expression=} ng-repeat <a ng-href="https://docs.angularjs.org/api/ng/directive/ngRepeat">
|
||
* AngularJS directive</a> that instantiates a template once per item from a collection.
|
||
* @param {expression=} ng-selected <a ng-href="https://docs.angularjs.org/api/ng/directive/ngSelected">
|
||
* AngularJS directive</a> that adds the `selected` attribute to the option when the expression
|
||
* evaluates as truthy.
|
||
*
|
||
* **Note:** Unlike native `option` elements used with AngularJS, `md-option` elements
|
||
* watch their `selected` attributes for changes and trigger model value changes on `md-select`.
|
||
* @param {boolean=} md-option-empty If the attribute exists, 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.
|
||
* @param {number=} tabindex The `tabindex` of the option. Defaults to `0`.
|
||
*
|
||
* @usage
|
||
* <hljs lang="html">
|
||
* <md-select ng-model="currentState" placeholder="Select a state">
|
||
* <md-option ng-value="AL">Alabama</md-option>
|
||
* <md-option ng-value="AK">Alaska</md-option>
|
||
* <md-option ng-value="FL">Florida</md-option>
|
||
* </md-select>
|
||
* </hljs>
|
||
*
|
||
* With `ng-repeat`:
|
||
* <hljs lang="html">
|
||
* <md-select ng-model="currentState" placeholder="Select a state">
|
||
* <md-option ng-value="state" ng-repeat="state in states">{{ state }}</md-option>
|
||
* </md-select>
|
||
* </hljs>
|
||
*/
|
||
function OptionDirective($mdButtonInkRipple, $mdUtil, $mdTheming) {
|
||
|
||
return {
|
||
restrict: 'E',
|
||
require: ['mdOption', '^^mdSelectMenu'],
|
||
controller: OptionController,
|
||
compile: compile
|
||
};
|
||
|
||
/**
|
||
* @param {JQLite} element
|
||
* @param {IAttributes} attrs
|
||
* @return {postLink}
|
||
*/
|
||
function compile(element, attrs) {
|
||
// Manual transclusion to avoid the extra inner <span> that ng-transclude generates
|
||
element.append(angular.element('<div class="md-text">').append(element.contents()));
|
||
|
||
element.attr('tabindex', attrs.tabindex || '0');
|
||
|
||
if (!hasDefinedValue(attrs)) {
|
||
element.attr('md-option-empty', '');
|
||
}
|
||
|
||
return postLink;
|
||
}
|
||
|
||
/**
|
||
* @param {Object} attrs list of attributes from the compile function
|
||
* @return {string|undefined|null} if defined and non-empty, return the value of the option's
|
||
* value attribute, otherwise return the value of the option's ng-value attribute.
|
||
*/
|
||
function hasDefinedValue(attrs) {
|
||
var value = attrs.value;
|
||
var ngValue = attrs.ngValue;
|
||
|
||
return value || ngValue;
|
||
}
|
||
|
||
function postLink(scope, element, attrs, ctrls) {
|
||
var optionCtrl = ctrls[0];
|
||
var selectMenuCtrl = ctrls[1];
|
||
|
||
$mdTheming(element);
|
||
|
||
if (selectMenuCtrl.isMultiple) {
|
||
element.addClass('md-checkbox-enabled');
|
||
if (!CHECKBOX_SELECTION_INDICATOR) {
|
||
var indicator = document.createElement('div');
|
||
indicator.className = 'md-container';
|
||
indicator.appendChild(document.createElement('div'));
|
||
indicator.firstChild.className = 'md-icon';
|
||
CHECKBOX_SELECTION_INDICATOR = angular.element(indicator);
|
||
}
|
||
element.prepend(CHECKBOX_SELECTION_INDICATOR.clone());
|
||
}
|
||
|
||
if (angular.isDefined(attrs.ngValue)) {
|
||
scope.$watch(attrs.ngValue, function (newValue, oldValue) {
|
||
setOptionValue(newValue, oldValue);
|
||
element.removeAttr('aria-checked');
|
||
});
|
||
} else if (angular.isDefined(attrs.value)) {
|
||
setOptionValue(attrs.value);
|
||
} else {
|
||
scope.$watch(function() {
|
||
return element.text().trim();
|
||
}, setOptionValue);
|
||
}
|
||
|
||
attrs.$observe('disabled', function(disabled) {
|
||
if (disabled) {
|
||
element.attr('tabindex', '-1');
|
||
} else {
|
||
element.attr('tabindex', '0');
|
||
}
|
||
});
|
||
|
||
scope.$$postDigest(function() {
|
||
attrs.$observe('selected', function(selected) {
|
||
if (!angular.isDefined(selected)) return;
|
||
if (typeof selected == 'string') selected = true;
|
||
if (selected) {
|
||
if (!selectMenuCtrl.isMultiple) {
|
||
selectMenuCtrl.deselect(Object.keys(selectMenuCtrl.selected)[0]);
|
||
}
|
||
selectMenuCtrl.select(optionCtrl.hashKey, optionCtrl.value);
|
||
} else {
|
||
selectMenuCtrl.deselect(optionCtrl.hashKey);
|
||
}
|
||
selectMenuCtrl.refreshViewValue();
|
||
});
|
||
});
|
||
|
||
$mdButtonInkRipple.attach(scope, element);
|
||
configureAria();
|
||
|
||
/**
|
||
* @param {*} newValue the option's new value
|
||
* @param {*=} oldValue the option's previous value
|
||
* @param {boolean=} prevAttempt true if this had to be attempted again due to an undefined
|
||
* hashGetter on the selectMenuCtrl, undefined otherwise.
|
||
*/
|
||
function setOptionValue(newValue, oldValue, prevAttempt) {
|
||
if (!selectMenuCtrl.hashGetter) {
|
||
if (!prevAttempt) {
|
||
scope.$$postDigest(function() {
|
||
setOptionValue(newValue, oldValue, true);
|
||
});
|
||
}
|
||
return;
|
||
}
|
||
var oldHashKey = selectMenuCtrl.hashGetter(oldValue, scope);
|
||
var newHashKey = selectMenuCtrl.hashGetter(newValue, scope);
|
||
|
||
optionCtrl.hashKey = newHashKey;
|
||
optionCtrl.value = newValue;
|
||
|
||
selectMenuCtrl.removeOption(oldHashKey, optionCtrl);
|
||
selectMenuCtrl.addOption(newHashKey, optionCtrl);
|
||
}
|
||
|
||
scope.$on('$destroy', function() {
|
||
selectMenuCtrl.removeOption(optionCtrl.hashKey, optionCtrl);
|
||
});
|
||
|
||
function configureAria() {
|
||
var ariaAttrs = {
|
||
'role': 'option'
|
||
};
|
||
|
||
// We explicitly omit the `aria-selected` attribute from single-selection, unselected
|
||
// options. Including the `aria-selected="false"` attributes adds a significant amount of
|
||
// noise to screen-reader users without providing useful information.
|
||
if (selectMenuCtrl.isMultiple) {
|
||
ariaAttrs['aria-selected'] = 'false';
|
||
}
|
||
|
||
if (!element[0].hasAttribute('id')) {
|
||
ariaAttrs.id = 'select_option_' + $mdUtil.nextUid();
|
||
}
|
||
element.attr(ariaAttrs);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {JQLite} $element
|
||
* @constructor
|
||
*/
|
||
function OptionController($element) {
|
||
/**
|
||
* @param {boolean} isSelected
|
||
* @param {boolean=} isMultiple
|
||
*/
|
||
this.setSelected = function(isSelected, isMultiple) {
|
||
if (isSelected) {
|
||
$element.attr({
|
||
'selected': 'true',
|
||
'aria-selected': 'true'
|
||
});
|
||
} else if (!isSelected) {
|
||
$element.removeAttr('selected');
|
||
|
||
if (isMultiple) {
|
||
$element.attr('aria-selected', 'false');
|
||
} else {
|
||
// We explicitly omit the `aria-selected` attribute from single-selection, unselected
|
||
// options. Including the `aria-selected="false"` attributes adds a significant amount of
|
||
// noise to screen-reader users without providing useful information.
|
||
$element.removeAttr('aria-selected');
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdOptgroup
|
||
* @restrict E
|
||
* @module material.components.select
|
||
*
|
||
* @description Displays a label separating groups of
|
||
* <a ng-href="api/directive/mdOption">md-option</a> element directives in a
|
||
* <a ng-href="api/directive/mdSelect">md-select</a> box's dropdown menu.
|
||
*
|
||
* **Note:** When using `md-select-header` element directives within a `md-select`, the labels that
|
||
* would normally be added to the <a ng-href="api/directive/mdOptgroup">md-optgroup</a> directives
|
||
* are omitted, allowing the `md-select-header` to represent the option group label
|
||
* (and possibly more).
|
||
*
|
||
* @usage
|
||
* With label attributes
|
||
* <hljs lang="html">
|
||
* <md-select ng-model="currentState" placeholder="Select a state">
|
||
* <md-optgroup label="Southern">
|
||
* <md-option ng-value="AL">Alabama</md-option>
|
||
* <md-option ng-value="FL">Florida</md-option>
|
||
* </md-optgroup>
|
||
* <md-optgroup label="Northern">
|
||
* <md-option ng-value="AK">Alaska</md-option>
|
||
* <md-option ng-value="MA">Massachusetts</md-option>
|
||
* </md-optgroup>
|
||
* </md-select>
|
||
* </hljs>
|
||
*
|
||
* With label elements
|
||
* <hljs lang="html">
|
||
* <md-select ng-model="currentState" placeholder="Select a state">
|
||
* <md-optgroup>
|
||
* <label>Southern</label>
|
||
* <md-option ng-value="AL">Alabama</md-option>
|
||
* <md-option ng-value="FL">Florida</md-option>
|
||
* </md-optgroup>
|
||
* <md-optgroup>
|
||
* <label>Northern</label>
|
||
* <md-option ng-value="AK">Alaska</md-option>
|
||
* <md-option ng-value="MA">Massachusetts</md-option>
|
||
* </md-optgroup>
|
||
* </md-select>
|
||
* </hljs>
|
||
*
|
||
* @param {string=} label The option group's label.
|
||
*/
|
||
function OptgroupDirective() {
|
||
return {
|
||
restrict: 'E',
|
||
compile: compile
|
||
};
|
||
function compile(element, attrs) {
|
||
// If we have a select header element, we don't want to add the normal label
|
||
// header.
|
||
if (!hasSelectHeader()) {
|
||
setupLabelElement();
|
||
}
|
||
element.attr('role', 'group');
|
||
|
||
function hasSelectHeader() {
|
||
return element.parent().find('md-select-header').length;
|
||
}
|
||
|
||
function setupLabelElement() {
|
||
var labelElement = element.find('label');
|
||
if (!labelElement.length) {
|
||
labelElement = angular.element('<label>');
|
||
element.prepend(labelElement);
|
||
}
|
||
labelElement.addClass('md-container-ignore');
|
||
labelElement.attr('aria-hidden', 'true');
|
||
if (attrs.label) {
|
||
labelElement.text(attrs.label);
|
||
}
|
||
element.attr('aria-label', labelElement.text());
|
||
}
|
||
}
|
||
}
|
||
|
||
function SelectHeaderDirective() {
|
||
return {
|
||
restrict: 'E',
|
||
};
|
||
}
|
||
|
||
function SelectProvider($$interimElementProvider) {
|
||
selectDefaultOptions.$inject = ["$mdSelect", "$mdConstant", "$mdUtil", "$window", "$q", "$$rAF", "$animateCss", "$animate", "$document"];
|
||
return $$interimElementProvider('$mdSelect')
|
||
.setDefaults({
|
||
methods: ['target'],
|
||
options: selectDefaultOptions
|
||
});
|
||
|
||
/* @ngInject */
|
||
function selectDefaultOptions($mdSelect, $mdConstant, $mdUtil, $window, $q, $$rAF, $animateCss, $animate, $document) {
|
||
var ERROR_TARGET_EXPECTED = "$mdSelect.show() expected a target element in options.target but got '{0}'!";
|
||
var animator = $mdUtil.dom.animator;
|
||
var keyCodes = $mdConstant.KEY_CODE;
|
||
|
||
return {
|
||
parent: 'body',
|
||
themable: true,
|
||
onShow: onShow,
|
||
onRemove: onRemove,
|
||
hasBackdrop: true,
|
||
disableParentScroll: true
|
||
};
|
||
|
||
/**
|
||
* Interim-element onRemove logic....
|
||
*/
|
||
function onRemove(scope, element, opts) {
|
||
var animationRunner = null;
|
||
var destroyListener = scope.$on('$destroy', function() {
|
||
// Listen for the case where the element was destroyed while there was an
|
||
// ongoing close animation. If this happens, we need to end the animation
|
||
// manually.
|
||
animationRunner.end();
|
||
});
|
||
|
||
opts = opts || { };
|
||
opts.cleanupInteraction();
|
||
opts.cleanupResizing();
|
||
opts.hideBackdrop();
|
||
|
||
// For navigation $destroy events, do a quick, non-animated removal,
|
||
// but for normal closes (from clicks, etc) animate the removal
|
||
return (opts.$destroy === true) ? cleanElement() : animateRemoval().then(cleanElement);
|
||
|
||
/**
|
||
* For normal closes (eg clicks), animate the removal.
|
||
* For forced closes (like $destroy events from navigation),
|
||
* skip the animations.
|
||
*/
|
||
function animateRemoval() {
|
||
animationRunner = $animateCss(element, {addClass: 'md-leave'});
|
||
return animationRunner.start();
|
||
}
|
||
|
||
/**
|
||
* Restore the element to a closed state
|
||
*/
|
||
function cleanElement() {
|
||
destroyListener();
|
||
|
||
element
|
||
.removeClass('md-active')
|
||
.attr('aria-hidden', 'true')
|
||
.css({
|
||
'display': 'none',
|
||
'top': '',
|
||
'right': '',
|
||
'bottom': '',
|
||
'left': '',
|
||
'font-size': '',
|
||
'min-width': ''
|
||
});
|
||
|
||
announceClosed(opts);
|
||
|
||
if (!opts.$destroy) {
|
||
if (opts.restoreFocus) {
|
||
opts.target.focus();
|
||
} else {
|
||
// Make sure that the container's md-input-focused is removed on backdrop click.
|
||
$mdUtil.nextTick(function() {
|
||
opts.target.triggerHandler('blur');
|
||
}, true);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Interim-element onShow logic.
|
||
*/
|
||
function onShow(scope, element, opts) {
|
||
|
||
watchAsyncLoad();
|
||
sanitizeAndConfigure(scope, opts);
|
||
|
||
opts.hideBackdrop = showBackdrop(scope, element, opts);
|
||
|
||
return showDropDown(scope, element, opts)
|
||
.then(function(response) {
|
||
element.attr('aria-hidden', 'false');
|
||
opts.alreadyOpen = true;
|
||
opts.cleanupInteraction = activateInteraction();
|
||
opts.cleanupResizing = activateResizing();
|
||
opts.contentEl[0].focus();
|
||
|
||
return response;
|
||
}, opts.hideBackdrop);
|
||
|
||
// ************************************
|
||
// Closure Functions
|
||
// ************************************
|
||
|
||
/**
|
||
* Attach the select DOM element(s) and animate to the correct positions and scale.
|
||
*/
|
||
function showDropDown(scope, element, opts) {
|
||
if (opts.parent !== element.parent()) {
|
||
element.parent().attr('aria-owns', element.find('md-content').attr('id'));
|
||
}
|
||
|
||
opts.parent.append(element);
|
||
|
||
return $q(function(resolve, reject) {
|
||
try {
|
||
$animateCss(element, {removeClass: 'md-leave', duration: 0})
|
||
.start()
|
||
.then(positionAndFocusMenu)
|
||
.then(resolve);
|
||
|
||
} catch (e) {
|
||
reject(e);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Initialize container and dropDown menu positions/scale, then animate to show.
|
||
* @return {*} a Promise that resolves after the menu is animated in and an item is focused
|
||
*/
|
||
function positionAndFocusMenu() {
|
||
return $q(function(resolve) {
|
||
if (opts.isRemoved) return $q.reject(false);
|
||
|
||
var info = calculateMenuPositions(scope, element, opts);
|
||
|
||
info.container.element.css(animator.toCss(info.container.styles));
|
||
info.dropDown.element.css(animator.toCss(info.dropDown.styles));
|
||
|
||
$$rAF(function() {
|
||
element.addClass('md-active');
|
||
info.dropDown.element.css(animator.toCss({transform: ''}));
|
||
autoFocus(opts.focusedNode);
|
||
|
||
resolve();
|
||
});
|
||
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Show modal backdrop element.
|
||
*/
|
||
function showBackdrop(scope, element, options) {
|
||
|
||
// If we are not within a dialog...
|
||
if (options.disableParentScroll && !$mdUtil.getClosest(options.target, 'MD-DIALOG')) {
|
||
// !! DO this before creating the backdrop; since disableScrollAround()
|
||
// configures the scroll offset; which is used by mdBackDrop postLink()
|
||
options.restoreScroll = $mdUtil.disableScrollAround(options.element, options.parent);
|
||
} else {
|
||
options.disableParentScroll = false;
|
||
}
|
||
|
||
if (options.hasBackdrop) {
|
||
// Override duration to immediately show invisible backdrop
|
||
options.backdrop = $mdUtil.createBackdrop(scope, "md-select-backdrop md-click-catcher");
|
||
$animate.enter(options.backdrop, $document[0].body, null, {duration: 0});
|
||
}
|
||
|
||
/**
|
||
* Hide modal backdrop element...
|
||
*/
|
||
return function hideBackdrop() {
|
||
if (options.backdrop) options.backdrop.remove();
|
||
if (options.disableParentScroll) options.restoreScroll();
|
||
|
||
delete options.restoreScroll;
|
||
};
|
||
}
|
||
|
||
/**
|
||
* @param {Element|HTMLElement|null=} previousNode
|
||
* @param {Element|HTMLElement} node
|
||
* @param {SelectMenuController|Function|object=} menuController SelectMenuController instance
|
||
*/
|
||
function focusOptionNode(previousNode, node, menuController) {
|
||
var listboxContentNode = opts.contentEl[0];
|
||
|
||
if (node) {
|
||
if (previousNode) {
|
||
previousNode.classList.remove('md-focused');
|
||
}
|
||
|
||
node.classList.add('md-focused');
|
||
if (menuController && menuController.setActiveDescendant) {
|
||
menuController.setActiveDescendant(node.id);
|
||
}
|
||
|
||
// Scroll the node into view if needed.
|
||
if (listboxContentNode.scrollHeight > listboxContentNode.clientHeight) {
|
||
var scrollBottom = listboxContentNode.clientHeight + listboxContentNode.scrollTop;
|
||
var nodeBottom = node.offsetTop + node.offsetHeight;
|
||
if (nodeBottom > scrollBottom) {
|
||
listboxContentNode.scrollTop = nodeBottom - listboxContentNode.clientHeight;
|
||
} else if (node.offsetTop < listboxContentNode.scrollTop) {
|
||
listboxContentNode.scrollTop = node.offsetTop;
|
||
}
|
||
}
|
||
opts.focusedNode = node;
|
||
if (menuController && menuController.refreshViewValue) {
|
||
menuController.refreshViewValue();
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {Element|HTMLElement} nodeToFocus
|
||
*/
|
||
function autoFocus(nodeToFocus) {
|
||
var selectMenuController;
|
||
if (nodeToFocus && !nodeToFocus.hasAttribute('disabled')) {
|
||
selectMenuController = opts.selectEl.controller('mdSelectMenu');
|
||
focusOptionNode(null, nodeToFocus, selectMenuController);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Check for valid opts and set some useful defaults
|
||
*/
|
||
function sanitizeAndConfigure(scope, options) {
|
||
var selectMenuElement = element.find('md-select-menu');
|
||
|
||
if (!options.target) {
|
||
throw new Error($mdUtil.supplant(ERROR_TARGET_EXPECTED, [options.target]));
|
||
}
|
||
|
||
angular.extend(options, {
|
||
isRemoved: false,
|
||
target: angular.element(options.target), // make sure it's not a naked DOM node
|
||
parent: angular.element(options.parent),
|
||
selectEl: selectMenuElement,
|
||
contentEl: element.find('md-content'),
|
||
optionNodes: selectMenuElement[0].getElementsByTagName('md-option')
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Configure various resize listeners for screen changes
|
||
*/
|
||
function activateResizing() {
|
||
var debouncedOnResize = (function(scope, target, options) {
|
||
|
||
return function() {
|
||
if (options.isRemoved) return;
|
||
|
||
var updates = calculateMenuPositions(scope, target, options);
|
||
var container = updates.container;
|
||
var dropDown = updates.dropDown;
|
||
|
||
container.element.css(animator.toCss(container.styles));
|
||
dropDown.element.css(animator.toCss(dropDown.styles));
|
||
};
|
||
|
||
})(scope, element, opts);
|
||
|
||
var window = angular.element($window);
|
||
window.on('resize', debouncedOnResize);
|
||
window.on('orientationchange', debouncedOnResize);
|
||
|
||
// Publish deactivation closure...
|
||
return function deactivateResizing() {
|
||
|
||
// Disable resizing handlers
|
||
window.off('resize', debouncedOnResize);
|
||
window.off('orientationchange', debouncedOnResize);
|
||
};
|
||
}
|
||
|
||
/**
|
||
* If asynchronously loading, watch and update internal '$$loadingAsyncDone' flag.
|
||
*/
|
||
function watchAsyncLoad() {
|
||
if (opts.loadingAsync && !opts.isRemoved) {
|
||
scope.$$loadingAsyncDone = false;
|
||
|
||
$q.when(opts.loadingAsync)
|
||
.then(function() {
|
||
scope.$$loadingAsyncDone = true;
|
||
delete opts.loadingAsync;
|
||
}).then(function() {
|
||
$$rAF(positionAndFocusMenu);
|
||
});
|
||
}
|
||
}
|
||
|
||
function activateInteraction() {
|
||
if (opts.isRemoved) {
|
||
return;
|
||
}
|
||
|
||
var dropDown = opts.selectEl;
|
||
var selectMenuController = dropDown.controller('mdSelectMenu') || {};
|
||
|
||
element.addClass('md-clickable');
|
||
|
||
// Close on backdrop click
|
||
opts.backdrop && opts.backdrop.on('click', onBackdropClick);
|
||
|
||
// Escape to close
|
||
// Cycling of options, and closing on enter
|
||
dropDown.on('keydown', onMenuKeyDown);
|
||
dropDown.on('click', checkCloseMenu);
|
||
|
||
return function cleanupInteraction() {
|
||
opts.backdrop && opts.backdrop.off('click', onBackdropClick);
|
||
dropDown.off('keydown', onMenuKeyDown);
|
||
dropDown.off('click', checkCloseMenu);
|
||
|
||
element.removeClass('md-clickable');
|
||
opts.isRemoved = true;
|
||
};
|
||
|
||
// ************************************
|
||
// Closure Functions
|
||
// ************************************
|
||
|
||
function onBackdropClick(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
opts.restoreFocus = false;
|
||
$mdUtil.nextTick($mdSelect.hide, true);
|
||
}
|
||
|
||
function onMenuKeyDown(ev) {
|
||
ev.preventDefault();
|
||
ev.stopPropagation();
|
||
|
||
switch (ev.keyCode) {
|
||
case keyCodes.UP_ARROW:
|
||
return focusPrevOption();
|
||
case keyCodes.DOWN_ARROW:
|
||
return focusNextOption();
|
||
case keyCodes.SPACE:
|
||
case keyCodes.ENTER:
|
||
if (opts.focusedNode) {
|
||
dropDown.triggerHandler({
|
||
type: 'click',
|
||
target: opts.focusedNode
|
||
});
|
||
ev.preventDefault();
|
||
}
|
||
checkCloseMenu(ev);
|
||
break;
|
||
case keyCodes.TAB:
|
||
case keyCodes.ESCAPE:
|
||
ev.stopPropagation();
|
||
ev.preventDefault();
|
||
opts.restoreFocus = true;
|
||
$mdUtil.nextTick($mdSelect.hide, true);
|
||
break;
|
||
default:
|
||
if (shouldHandleKey(ev, $mdConstant)) {
|
||
var optNode = selectMenuController.optNodeForKeyboardSearch(ev);
|
||
if (optNode && !optNode.hasAttribute('disabled')) {
|
||
focusOptionNode(opts.focusedNode, optNode, selectMenuController);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Change the focus to another option. If there is no focused option, focus the first
|
||
* option. If there is a focused option, then use the direction to determine if we should
|
||
* focus the previous or next option in the list.
|
||
* @param {'next'|'prev'} direction
|
||
*/
|
||
function focusOption(direction) {
|
||
var optionsArray = $mdUtil.nodesToArray(opts.optionNodes);
|
||
var index = optionsArray.indexOf(opts.focusedNode);
|
||
var prevOption = optionsArray[index];
|
||
var newOption;
|
||
|
||
do {
|
||
if (index === -1) {
|
||
// We lost the previously focused element, reset to first option
|
||
index = 0;
|
||
} else if (direction === 'next' && index < optionsArray.length - 1) {
|
||
index++;
|
||
} else if (direction === 'prev' && index > 0) {
|
||
index--;
|
||
}
|
||
newOption = optionsArray[index];
|
||
if (newOption.hasAttribute('disabled')) {
|
||
newOption = null;
|
||
}
|
||
} while (!newOption && index < optionsArray.length - 1 && index > 0);
|
||
|
||
focusOptionNode(prevOption, newOption, selectMenuController);
|
||
}
|
||
|
||
function focusNextOption() {
|
||
focusOption('next');
|
||
}
|
||
|
||
function focusPrevOption() {
|
||
focusOption('prev');
|
||
}
|
||
|
||
/**
|
||
* @param {KeyboardEvent|MouseEvent} event
|
||
*/
|
||
function checkCloseMenu(event) {
|
||
if (event && (event.type === 'click') && (event.currentTarget !== dropDown[0])) {
|
||
return;
|
||
}
|
||
if (mouseOnScrollbar()) {
|
||
return;
|
||
}
|
||
|
||
if (opts.focusedNode && opts.focusedNode.hasAttribute &&
|
||
!opts.focusedNode.hasAttribute('disabled')) {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
if (!selectMenuController.isMultiple) {
|
||
opts.restoreFocus = true;
|
||
|
||
$mdUtil.nextTick(function () {
|
||
$mdSelect.hide(selectMenuController.ngModel.$viewValue);
|
||
opts.focusedNode.classList.remove('md-focused');
|
||
}, true);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* check if the mouseup event was on a scrollbar
|
||
*/
|
||
function mouseOnScrollbar() {
|
||
var clickOnScrollbar = false;
|
||
if (event && (event.currentTarget.children.length > 0)) {
|
||
var child = event.currentTarget.children[0];
|
||
var hasScrollbar = child.scrollHeight > child.clientHeight;
|
||
if (hasScrollbar && child.children.length > 0) {
|
||
var relPosX = event.pageX - event.currentTarget.getBoundingClientRect().left;
|
||
if (relPosX > child.querySelector('md-option').offsetWidth)
|
||
clickOnScrollbar = true;
|
||
}
|
||
}
|
||
return clickOnScrollbar;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* To notify listeners that the Select menu has closed,
|
||
* trigger the [optional] user-defined expression
|
||
*/
|
||
function announceClosed(opts) {
|
||
var mdSelect = opts.selectCtrl;
|
||
if (mdSelect) {
|
||
var menuController = opts.selectEl.controller('mdSelectMenu');
|
||
mdSelect.setSelectValueText(menuController ? menuController.getSelectedLabels() : '');
|
||
mdSelect.triggerClose();
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* Calculate the menu positions after an event like options changing, screen resizing, or
|
||
* animations finishing.
|
||
* @param {Object} scope
|
||
* @param element
|
||
* @param opts
|
||
* @return {{container: {styles: {top: number, left: number, 'font-size': *, 'min-width': number}, element: Object}, dropDown: {styles: {transform: string, transformOrigin: string}, element: Object}}}
|
||
*/
|
||
function calculateMenuPositions(scope, element, opts) {
|
||
var
|
||
containerNode = element[0],
|
||
targetNode = opts.target[0].children[0], // target the label
|
||
parentNode = $document[0].body,
|
||
selectNode = opts.selectEl[0],
|
||
contentNode = opts.contentEl[0],
|
||
parentRect = parentNode.getBoundingClientRect(),
|
||
targetRect = targetNode.getBoundingClientRect(),
|
||
shouldOpenAroundTarget = false,
|
||
bounds = {
|
||
left: parentRect.left + SELECT_EDGE_MARGIN,
|
||
top: SELECT_EDGE_MARGIN,
|
||
bottom: parentRect.height - SELECT_EDGE_MARGIN,
|
||
right: parentRect.width - SELECT_EDGE_MARGIN - ($mdUtil.floatingScrollbars() ? 16 : 0)
|
||
},
|
||
spaceAvailable = {
|
||
top: targetRect.top - bounds.top,
|
||
left: targetRect.left - bounds.left,
|
||
right: bounds.right - (targetRect.left + targetRect.width),
|
||
bottom: bounds.bottom - (targetRect.top + targetRect.height)
|
||
},
|
||
maxWidth = parentRect.width - SELECT_EDGE_MARGIN * 2,
|
||
selectedNode = selectNode.querySelector('md-option[selected]'),
|
||
optionNodes = selectNode.getElementsByTagName('md-option'),
|
||
optgroupNodes = selectNode.getElementsByTagName('md-optgroup'),
|
||
isScrollable = calculateScrollable(element, contentNode),
|
||
centeredNode;
|
||
|
||
var loading = isPromiseLike(opts.loadingAsync);
|
||
if (!loading) {
|
||
// If a selected node, center around that
|
||
if (selectedNode) {
|
||
centeredNode = selectedNode;
|
||
// If there are option groups, center around the first option group
|
||
} else if (optgroupNodes.length) {
|
||
centeredNode = optgroupNodes[0];
|
||
// Otherwise - if we are not loading async - center around the first optionNode
|
||
} else if (optionNodes.length) {
|
||
centeredNode = optionNodes[0];
|
||
// In case there are no options, center on whatever's in there... (eg progress indicator)
|
||
} else {
|
||
centeredNode = contentNode.firstElementChild || contentNode;
|
||
}
|
||
} else {
|
||
// If loading, center on progress indicator
|
||
centeredNode = contentNode.firstElementChild || contentNode;
|
||
}
|
||
|
||
if (contentNode.offsetWidth > maxWidth) {
|
||
contentNode.style['max-width'] = maxWidth + 'px';
|
||
} else {
|
||
contentNode.style.maxWidth = null;
|
||
}
|
||
if (shouldOpenAroundTarget) {
|
||
contentNode.style['min-width'] = targetRect.width + 'px';
|
||
}
|
||
|
||
// Remove padding before we compute the position of the menu
|
||
if (isScrollable) {
|
||
selectNode.classList.add('md-overflow');
|
||
}
|
||
|
||
var focusedNode = centeredNode;
|
||
if ((focusedNode.tagName || '').toUpperCase() === 'MD-OPTGROUP') {
|
||
focusedNode = optionNodes[0] || contentNode.firstElementChild || contentNode;
|
||
centeredNode = focusedNode;
|
||
}
|
||
// Cache for autoFocus()
|
||
opts.focusedNode = focusedNode;
|
||
|
||
// Get the selectMenuRect *after* max-width is possibly set above
|
||
containerNode.style.display = 'block';
|
||
var selectMenuRect = selectNode.getBoundingClientRect();
|
||
var centeredRect = getOffsetRect(centeredNode);
|
||
|
||
if (centeredNode) {
|
||
var centeredStyle = $window.getComputedStyle(centeredNode);
|
||
centeredRect.paddingLeft = parseInt(centeredStyle.paddingLeft, 10) || 0;
|
||
centeredRect.paddingRight = parseInt(centeredStyle.paddingRight, 10) || 0;
|
||
}
|
||
|
||
if (isScrollable) {
|
||
var scrollBuffer = contentNode.offsetHeight / 2;
|
||
contentNode.scrollTop = centeredRect.top + centeredRect.height / 2 - scrollBuffer;
|
||
|
||
if (spaceAvailable.top < scrollBuffer) {
|
||
contentNode.scrollTop = Math.min(
|
||
centeredRect.top,
|
||
contentNode.scrollTop + scrollBuffer - spaceAvailable.top
|
||
);
|
||
} else if (spaceAvailable.bottom < scrollBuffer) {
|
||
contentNode.scrollTop = Math.max(
|
||
centeredRect.top + centeredRect.height - selectMenuRect.height,
|
||
contentNode.scrollTop - scrollBuffer + spaceAvailable.bottom
|
||
);
|
||
}
|
||
}
|
||
|
||
var left, top, transformOrigin, minWidth, fontSize;
|
||
if (shouldOpenAroundTarget) {
|
||
left = targetRect.left;
|
||
top = targetRect.top + targetRect.height;
|
||
transformOrigin = '50% 0';
|
||
if (top + selectMenuRect.height > bounds.bottom) {
|
||
top = targetRect.top - selectMenuRect.height;
|
||
transformOrigin = '50% 100%';
|
||
}
|
||
} else {
|
||
left = (targetRect.left + centeredRect.left - centeredRect.paddingLeft);
|
||
top = Math.floor(targetRect.top + targetRect.height / 2 - centeredRect.height / 2 -
|
||
centeredRect.top + contentNode.scrollTop) + 2;
|
||
|
||
transformOrigin = (centeredRect.left + targetRect.width / 2) + 'px ' +
|
||
(centeredRect.top + centeredRect.height / 2 - contentNode.scrollTop) + 'px 0px';
|
||
|
||
minWidth = Math.min(targetRect.width + centeredRect.paddingLeft + centeredRect.paddingRight, maxWidth);
|
||
|
||
fontSize = window.getComputedStyle(targetNode)['font-size'];
|
||
}
|
||
|
||
// Keep left and top within the window
|
||
var containerRect = containerNode.getBoundingClientRect();
|
||
var scaleX = Math.round(100 * Math.min(targetRect.width / selectMenuRect.width, 1.0)) / 100;
|
||
var scaleY = Math.round(100 * Math.min(targetRect.height / selectMenuRect.height, 1.0)) / 100;
|
||
|
||
return {
|
||
container: {
|
||
element: angular.element(containerNode),
|
||
styles: {
|
||
left: Math.floor(clamp(bounds.left, left, bounds.right - minWidth)),
|
||
top: Math.floor(clamp(bounds.top, top, bounds.bottom - containerRect.height)),
|
||
'min-width': minWidth,
|
||
'font-size': fontSize
|
||
}
|
||
},
|
||
dropDown: {
|
||
element: angular.element(selectNode),
|
||
styles: {
|
||
transformOrigin: transformOrigin,
|
||
transform: !opts.alreadyOpen ? $mdUtil.supplant('scale({0},{1})', [scaleX, scaleY]) : ""
|
||
}
|
||
}
|
||
};
|
||
}
|
||
}
|
||
|
||
function isPromiseLike(obj) {
|
||
return obj && angular.isFunction(obj.then);
|
||
}
|
||
|
||
function clamp(min, n, max) {
|
||
return Math.max(min, Math.min(n, max));
|
||
}
|
||
|
||
function getOffsetRect(node) {
|
||
return node ? {
|
||
left: node.offsetLeft,
|
||
top: node.offsetTop,
|
||
width: node.offsetWidth,
|
||
height: node.offsetHeight
|
||
} : {left: 0, top: 0, width: 0, height: 0};
|
||
}
|
||
|
||
function calculateScrollable(element, contentNode) {
|
||
var isScrollable = false;
|
||
|
||
try {
|
||
var oldDisplay = element[0].style.display;
|
||
|
||
// Set the element's display to block so that this calculation is correct
|
||
element[0].style.display = 'block';
|
||
|
||
isScrollable = contentNode.scrollHeight > contentNode.offsetHeight;
|
||
|
||
// Reset it back afterwards
|
||
element[0].style.display = oldDisplay;
|
||
} finally {
|
||
// Nothing to do
|
||
}
|
||
return isScrollable;
|
||
}
|
||
}
|
||
|
||
function shouldHandleKey(ev, $mdConstant) {
|
||
var char = String.fromCharCode(ev.keyCode);
|
||
var isNonUsefulKey = (ev.keyCode <= 31);
|
||
|
||
return (char && char.length && !isNonUsefulKey &&
|
||
!$mdConstant.isMetaKey(ev) && !$mdConstant.isFnLockKey(ev) && !$mdConstant.hasModifierKey(ev));
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.showHide
|
||
*/
|
||
|
||
// Add additional handlers to ng-show and ng-hide that notify directives
|
||
// contained within that they should recompute their size.
|
||
// These run in addition to AngularJS's built-in ng-hide and ng-show directives.
|
||
angular.module('material.components.showHide', [
|
||
'material.core'
|
||
])
|
||
.directive('ngShow', createDirective('ngShow', true))
|
||
.directive('ngHide', createDirective('ngHide', false));
|
||
|
||
|
||
function createDirective(name, targetValue) {
|
||
return ['$mdUtil', '$window', function($mdUtil, $window) {
|
||
return {
|
||
restrict: 'A',
|
||
multiElement: true,
|
||
link: function($scope, $element, $attr) {
|
||
var unregister = $scope.$on('$md-resize-enable', function() {
|
||
unregister();
|
||
|
||
var node = $element[0];
|
||
var cachedTransitionStyles = node.nodeType === $window.Node.ELEMENT_NODE ?
|
||
$window.getComputedStyle(node) : {};
|
||
|
||
$scope.$watch($attr[name], function(value) {
|
||
if (!!value === targetValue) {
|
||
$mdUtil.nextTick(function() {
|
||
$scope.$broadcast('$md-resize');
|
||
});
|
||
|
||
var opts = {
|
||
cachedTransitionStyles: cachedTransitionStyles
|
||
};
|
||
|
||
$mdUtil.dom.animator.waitTransitionEnd($element, opts).then(function() {
|
||
$scope.$broadcast('$md-resize');
|
||
});
|
||
}
|
||
});
|
||
});
|
||
}
|
||
};
|
||
}];
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.sidenav
|
||
*
|
||
* @description
|
||
* A Sidenav component.
|
||
*/
|
||
SidenavService.$inject = ["$mdComponentRegistry", "$mdUtil", "$q", "$log"];
|
||
SidenavDirective.$inject = ["$mdMedia", "$mdUtil", "$mdConstant", "$mdTheming", "$mdInteraction", "$animate", "$compile", "$parse", "$log", "$q", "$document", "$window", "$$rAF"];
|
||
SidenavController.$inject = ["$scope", "$attrs", "$mdComponentRegistry", "$q", "$interpolate"];
|
||
angular
|
||
.module('material.components.sidenav', [
|
||
'material.core',
|
||
'material.components.backdrop'
|
||
])
|
||
.factory('$mdSidenav', SidenavService)
|
||
.directive('mdSidenav', SidenavDirective)
|
||
.controller('$mdSidenavController', SidenavController);
|
||
|
||
|
||
/**
|
||
* @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 initialized
|
||
* asynchronously. This is done by passing the second argument to `$mdSidenav`.
|
||
*
|
||
* @usage
|
||
* <hljs lang="js">
|
||
* // 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');
|
||
* });
|
||
* // Async lookup for sidenav instance
|
||
* $mdSidenav(componentId, true).then(function(instance) {
|
||
* // On close callback to handle close, backdrop click, or escape key pressed.
|
||
* // Callback happens BEFORE the close action occurs.
|
||
* instance.onClose(function() {
|
||
* $log.debug('closing');
|
||
* });
|
||
* });
|
||
* // 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();
|
||
* </hljs>
|
||
*/
|
||
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 shouldWait = enableWait === true;
|
||
var instance = service.find(handle, shouldWait);
|
||
return !instance && shouldWait ? service.waitFor(handle) :
|
||
!instance && angular.isUndefined(enableWait) ? addLegacyAPI(service, handle) : instance;
|
||
};
|
||
|
||
/**
|
||
* For failed instance/handle lookups, older-clients expect an response object with noops
|
||
* that include `rejected promise APIs`
|
||
* @param service
|
||
* @param handle
|
||
* @returns {Object}
|
||
*/
|
||
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,
|
||
onClose: angular.noop,
|
||
then: function(callback) {
|
||
return waitForInstance(handle).then(callback || angular.noop);
|
||
}
|
||
}, service);
|
||
}
|
||
|
||
/**
|
||
* Synchronously lookup the controller instance for the specified sidNav instance which has been
|
||
* registered with the markup `md-component-id`
|
||
*/
|
||
function findInstance(handle, shouldWait) {
|
||
var instance = $mdComponentRegistry.get(handle);
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdSidenav
|
||
* @module material.components.sidenav
|
||
* @restrict E
|
||
*
|
||
* @description
|
||
* A Sidenav component that can be opened and closed programmatically.
|
||
*
|
||
* 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
|
||
* <hljs lang="html">
|
||
* <div layout="row" ng-controller="MyController">
|
||
* <md-sidenav md-component-id="left" class="md-sidenav-left">
|
||
* Left Nav!
|
||
* </md-sidenav>
|
||
*
|
||
* <md-content>
|
||
* Center Content
|
||
* <md-button ng-click="openLeftMenu()">
|
||
* Open Left Menu
|
||
* </md-button>
|
||
* </md-content>
|
||
*
|
||
* <md-sidenav md-component-id="right"
|
||
* md-is-locked-open="$mdMedia('min-width: 333px')"
|
||
* class="md-sidenav-right">
|
||
* <form>
|
||
* <md-input-container>
|
||
* <label for="testInput">Test input</label>
|
||
* <input id="testInput" type="text"
|
||
* ng-model="data" md-autofocus>
|
||
* </md-input-container>
|
||
* </form>
|
||
* </md-sidenav>
|
||
* </div>
|
||
* </hljs>
|
||
*
|
||
* <hljs lang="js">
|
||
* var app = angular.module('myApp', ['ngMaterial']);
|
||
* app.controller('MyController', function($scope, $mdSidenav) {
|
||
* $scope.openLeftMenu = function() {
|
||
* $mdSidenav('left').toggle();
|
||
* };
|
||
* });
|
||
* </hljs>
|
||
*
|
||
* @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 {boolean=} md-disable-close-events When present in the markup, clicking the backdrop or
|
||
* pressing the 'Escape' key will not close the sidenav.
|
||
* @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.
|
||
*
|
||
* The `$mdMedia()` service is exposed to the `md-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.
|
||
* <br><br>Examples:
|
||
*
|
||
* Lock open when `true`:<br>
|
||
* `<md-sidenav md-is-locked-open="shouldLockOpen"></md-sidenav>`
|
||
*
|
||
* Lock open when the width is `1000px` or greater:<br>
|
||
* `<md-sidenav md-is-locked-open="$mdMedia('min-width: 1000px')"></md-sidenav>`
|
||
*
|
||
* Lock open on small screens:<br>
|
||
* `<md-sidenav md-is-locked-open="$mdMedia('sm')"></md-sidenav>`
|
||
*
|
||
* @param {string=} md-disable-scroll-target Selector, pointing to an element, whose scrolling will
|
||
* be disabled when the sidenav is opened. By default this is the sidenav's direct parent.
|
||
*/
|
||
function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $mdInteraction, $animate,
|
||
$compile, $parse, $log, $q, $document, $window, $$rAF) {
|
||
return {
|
||
restrict: 'E',
|
||
scope: {
|
||
isOpen: '=?mdIsOpen'
|
||
},
|
||
controller: '$mdSidenavController',
|
||
compile: function(element) {
|
||
element.addClass('md-closed').attr('tabIndex', '-1');
|
||
return postLink;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Directive Post Link function...
|
||
*/
|
||
function postLink(scope, element, attr, sidenavCtrl) {
|
||
var lastParentOverFlow;
|
||
var backdrop;
|
||
var disableScrollTarget = null;
|
||
var disableCloseEvents;
|
||
var triggeringInteractionType;
|
||
var triggeringElement = null;
|
||
var previousContainerStyles;
|
||
var promise = $q.when(true);
|
||
var isLockedOpenParsed = $parse(attr.mdIsLockedOpen);
|
||
var ngWindow = angular.element($window);
|
||
var isLocked = function() {
|
||
return isLockedOpenParsed(scope.$parent, {
|
||
$mdMedia: $mdMedia
|
||
});
|
||
};
|
||
|
||
if (attr.mdDisableScrollTarget) {
|
||
disableScrollTarget = $document[0].querySelector(attr.mdDisableScrollTarget);
|
||
|
||
if (disableScrollTarget) {
|
||
disableScrollTarget = angular.element(disableScrollTarget);
|
||
} else {
|
||
$log.warn($mdUtil.supplant('mdSidenav: couldn\'t find element matching ' +
|
||
'selector "{selector}". Falling back to parent.',
|
||
{ selector: attr.mdDisableScrollTarget }));
|
||
}
|
||
}
|
||
|
||
if (!disableScrollTarget) {
|
||
disableScrollTarget = element.parent();
|
||
}
|
||
|
||
// Only create the backdrop if the backdrop isn't disabled.
|
||
if (!attr.hasOwnProperty('mdDisableBackdrop')) {
|
||
backdrop = $mdUtil.createBackdrop(scope, "md-sidenav-backdrop md-opaque ng-enter");
|
||
}
|
||
|
||
// If md-disable-close-events is set on the sidenav we will disable
|
||
// backdrop click and Escape key events
|
||
if (attr.hasOwnProperty('mdDisableCloseEvents')) {
|
||
disableCloseEvents = true;
|
||
}
|
||
|
||
element.addClass('_md'); // private md component indicator for styling
|
||
$mdTheming(element);
|
||
|
||
// The backdrop should inherit the sidenavs theme,
|
||
// because the backdrop will take its parent theme by default.
|
||
if (backdrop) $mdTheming.inherit(backdrop, element);
|
||
|
||
element.on('$destroy', function() {
|
||
backdrop && backdrop.remove();
|
||
sidenavCtrl.destroy();
|
||
});
|
||
|
||
scope.$on('$destroy', function(){
|
||
backdrop && backdrop.remove();
|
||
});
|
||
|
||
scope.$watch(isLocked, updateIsLocked);
|
||
scope.$watch('isOpen', updateIsOpen);
|
||
|
||
|
||
// Publish special accessor for the Controller instance
|
||
sidenavCtrl.$toggleOpen = toggleOpen;
|
||
|
||
/**
|
||
* Toggle the DOM classes to indicate `locked`
|
||
* @param isLocked
|
||
* @param oldValue
|
||
*/
|
||
function updateIsLocked(isLocked, oldValue) {
|
||
scope.isLockedOpen = isLocked;
|
||
if (isLocked === oldValue) {
|
||
element.toggleClass('md-locked-open', !!isLocked);
|
||
} else {
|
||
$animate[isLocked ? 'addClass' : 'removeClass'](element, 'md-locked-open');
|
||
}
|
||
if (backdrop) {
|
||
backdrop.toggleClass('md-locked-open', !!isLocked);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Toggle the SideNav view and attach/detach listeners
|
||
* @param {boolean} isOpen
|
||
*/
|
||
function updateIsOpen(isOpen) {
|
||
var focusEl = $mdUtil.findFocusTarget(element) || element;
|
||
var parent = element.parent();
|
||
var restorePositioning;
|
||
|
||
// If the user hasn't set the disable close events property we are adding
|
||
// click and escape events to close the sidenav
|
||
if (!disableCloseEvents) {
|
||
parent[isOpen ? 'on' : 'off']('keydown', onKeyDown);
|
||
if (backdrop) backdrop[isOpen ? 'on' : 'off']('click', close);
|
||
}
|
||
|
||
restorePositioning = updateContainerPositions(parent, isOpen);
|
||
|
||
if (isOpen) {
|
||
// Capture upon opening..
|
||
triggeringElement = $document[0].activeElement;
|
||
triggeringInteractionType = $mdInteraction.getLastInteractionType();
|
||
}
|
||
|
||
disableParentScroll(isOpen);
|
||
|
||
return promise = $q.all([
|
||
isOpen && backdrop ? $animate.enter(backdrop, parent) : backdrop ?
|
||
$animate.leave(backdrop) : $q.when(true),
|
||
$animate[isOpen ? 'removeClass' : 'addClass'](element, 'md-closed')
|
||
]).then(function() {
|
||
// Perform focus when animations are ALL done...
|
||
if (scope.isOpen) {
|
||
$$rAF(function() {
|
||
// Notifies child components that the sidenav was opened. Should wait
|
||
// a frame in order to allow for the element height to be computed.
|
||
ngWindow.triggerHandler('resize');
|
||
});
|
||
|
||
focusEl && focusEl.focus();
|
||
}
|
||
|
||
// Restores the positioning on the sidenav and backdrop.
|
||
restorePositioning && restorePositioning();
|
||
});
|
||
}
|
||
|
||
function updateContainerPositions(parent, willOpen) {
|
||
var drawerEl = element[0];
|
||
var scrollTop = parent[0].scrollTop;
|
||
|
||
if (willOpen && scrollTop) {
|
||
previousContainerStyles = {
|
||
top: drawerEl.style.top,
|
||
bottom: drawerEl.style.bottom,
|
||
height: drawerEl.style.height
|
||
};
|
||
|
||
// When the parent is scrolled down, then we want to be able to show the sidenav at the
|
||
// current scroll position. We're moving the sidenav down to the correct scroll position
|
||
// and apply the height of the parent, to increase the performance. Using 100% as height,
|
||
// will impact the performance heavily.
|
||
var positionStyle = {
|
||
top: scrollTop + 'px',
|
||
bottom: 'auto',
|
||
height: parent[0].clientHeight + 'px'
|
||
};
|
||
|
||
// Apply the new position styles to the sidenav and backdrop.
|
||
element.css(positionStyle);
|
||
backdrop.css(positionStyle);
|
||
}
|
||
|
||
// When the sidenav is closing and we have previous defined container styles,
|
||
// then we return a restore function, which resets the sidenav and backdrop.
|
||
if (!willOpen && previousContainerStyles) {
|
||
return function() {
|
||
drawerEl.style.top = previousContainerStyles.top;
|
||
drawerEl.style.bottom = previousContainerStyles.bottom;
|
||
drawerEl.style.height = previousContainerStyles.height;
|
||
|
||
backdrop[0].style.top = null;
|
||
backdrop[0].style.bottom = null;
|
||
backdrop[0].style.height = null;
|
||
|
||
previousContainerStyles = null;
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Prevent parent scrolling (when the SideNav is open)
|
||
*/
|
||
function disableParentScroll(disabled) {
|
||
if (disabled && !lastParentOverFlow) {
|
||
lastParentOverFlow = disableScrollTarget.css('overflow');
|
||
disableScrollTarget.css('overflow', 'hidden');
|
||
} else if (angular.isDefined(lastParentOverFlow)) {
|
||
disableScrollTarget.css('overflow', lastParentOverFlow);
|
||
lastParentOverFlow = undefined;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Toggle the sideNav view and publish a promise to be resolved when
|
||
* the view animation finishes.
|
||
* @param {boolean} isOpen true to open the sidenav, false to close it
|
||
* @returns {*} promise to be resolved when the view animation finishes
|
||
*/
|
||
function toggleOpen(isOpen) {
|
||
if (scope.isOpen === isOpen) {
|
||
return $q.when(true);
|
||
} else {
|
||
if (scope.isOpen && sidenavCtrl.onCloseCb) sidenavCtrl.onCloseCb();
|
||
|
||
return $q(function(resolve) {
|
||
// Toggle value to force an async `updateIsOpen()` to run
|
||
scope.isOpen = isOpen;
|
||
|
||
$mdUtil.nextTick(function() {
|
||
// When the current `updateIsOpen()` animation finishes
|
||
promise.then(function(result) {
|
||
|
||
if (!scope.isOpen && triggeringElement && triggeringInteractionType === 'keyboard') {
|
||
// reset focus to originating element (if available) upon close
|
||
triggeringElement.focus();
|
||
triggeringElement = null;
|
||
}
|
||
|
||
resolve(result);
|
||
});
|
||
});
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Auto-close sideNav when the `escape` key is pressed.
|
||
* @param {KeyboardEvent} ev keydown event
|
||
*/
|
||
function onKeyDown(ev) {
|
||
var isEscape = (ev.keyCode === $mdConstant.KEY_CODE.ESCAPE);
|
||
return isEscape ? close(ev) : $q.when(true);
|
||
}
|
||
|
||
/**
|
||
* With backdrop `clicks` or `escape` key-press, immediately apply the CSS close transition...
|
||
* Then notify the controller to close() and perform its own actions.
|
||
* @param {Event} ev
|
||
* @returns {*}
|
||
*/
|
||
function close(ev) {
|
||
ev.preventDefault();
|
||
|
||
return sidenavCtrl.close();
|
||
}
|
||
}
|
||
}
|
||
|
||
/*
|
||
* @private
|
||
* @ngdoc controller
|
||
* @name SidenavController
|
||
* @module material.components.sidenav
|
||
*/
|
||
function SidenavController($scope, $attrs, $mdComponentRegistry, $q, $interpolate) {
|
||
var self = this;
|
||
|
||
// Use Default internal method until overridden by directive postLink
|
||
|
||
// Synchronous getters
|
||
self.isOpen = function() { return !!$scope.isOpen; };
|
||
self.isLockedOpen = function() { return !!$scope.isLockedOpen; };
|
||
|
||
// Synchronous setters
|
||
self.onClose = function (callback) {
|
||
self.onCloseCb = callback;
|
||
return self;
|
||
};
|
||
|
||
// Async actions
|
||
self.open = function() { return self.$toggleOpen(true); };
|
||
self.close = function() { return self.$toggleOpen(false); };
|
||
self.toggle = function() { return self.$toggleOpen(!$scope.isOpen); };
|
||
self.$toggleOpen = function(value) { return $q.when($scope.isOpen = value); };
|
||
|
||
// Evaluate the component id.
|
||
var rawId = $attrs.mdComponentId;
|
||
var hasDataBinding = rawId && rawId.indexOf($interpolate.startSymbol()) > -1;
|
||
var componentId = hasDataBinding ? $interpolate(rawId)($scope.$parent) : rawId;
|
||
|
||
// Register the component.
|
||
self.destroy = $mdComponentRegistry.register(self, componentId);
|
||
|
||
// Watch and update the component, if the id has changed.
|
||
if (hasDataBinding) {
|
||
$attrs.$observe('mdComponentId', function(id) {
|
||
if (id && id !== self.$$mdHandle) {
|
||
// `destroy` only deregisters the old component id so we can add the new one.
|
||
self.destroy();
|
||
self.destroy = $mdComponentRegistry.register(self, id);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.slider
|
||
*/
|
||
SliderDirective.$inject = ["$$rAF", "$window", "$mdAria", "$mdUtil", "$mdConstant", "$mdTheming", "$mdGesture", "$parse", "$log", "$timeout"];
|
||
angular.module('material.components.slider', [
|
||
'material.core'
|
||
])
|
||
.directive('mdSlider', SliderDirective)
|
||
.directive('mdSliderContainer', SliderContainerDirective);
|
||
|
||
/**
|
||
* @type {number} the page size used for stepping when page up/down keys are pressed.
|
||
*/
|
||
var stepPageSize = 10;
|
||
/**
|
||
* @type {number} the multiplier applied to a step when the arrow key is pressed along with
|
||
* alt, meta, or ctrl.
|
||
*/
|
||
var modifierMultiplier = 4;
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdSliderContainer
|
||
* @module material.components.slider
|
||
* @restrict E
|
||
* @description
|
||
* The `<md-slider-container>` can hold the slider with two other elements.
|
||
* In this case, the other elements are a `span` for the label and an `input` for displaying
|
||
* the model value.
|
||
*
|
||
* @usage
|
||
* <hljs lang="html">
|
||
* <md-slider-container>
|
||
* <span>Red</span>
|
||
* <md-slider min="0" max="255" ng-model="color.red" aria-label="red" id="red-slider">
|
||
* </md-slider>
|
||
* <md-input-container>
|
||
* <input type="number" ng-model="color.red" aria-label="Red" aria-controls="red-slider">
|
||
* </md-input-container>
|
||
* </md-slider-container>
|
||
* </hljs>
|
||
*/
|
||
function SliderContainerDirective() {
|
||
return {
|
||
controller: function () {},
|
||
compile: function (elem) {
|
||
var slider = elem.find('md-slider');
|
||
|
||
if (!slider) {
|
||
return;
|
||
}
|
||
|
||
var vertical = slider.attr('md-vertical');
|
||
|
||
if (vertical !== undefined) {
|
||
elem.attr('md-vertical', '');
|
||
}
|
||
|
||
if (!slider.attr('flex')) {
|
||
slider.attr('flex', '');
|
||
}
|
||
|
||
return function postLink(scope, element, attr, ctrl) {
|
||
element.addClass('_md'); // private md component indicator for styling
|
||
|
||
// We have to manually stop the $watch on ngDisabled because it exists
|
||
// on the parent scope, and won't be automatically destroyed when
|
||
// the component is destroyed.
|
||
function setDisable(value) {
|
||
element.children().attr('disabled', value);
|
||
element.find('input').attr('disabled', value);
|
||
}
|
||
|
||
var stopDisabledWatch = angular.noop;
|
||
|
||
if (attr.disabled) {
|
||
setDisable(true);
|
||
}
|
||
else if (attr.ngDisabled) {
|
||
stopDisabledWatch = scope.$watch(attr.ngDisabled, function (value) {
|
||
setDisable(value);
|
||
});
|
||
}
|
||
|
||
scope.$on('$destroy', function () {
|
||
stopDisabledWatch();
|
||
});
|
||
|
||
var initialMaxWidth;
|
||
|
||
/**
|
||
* @param {number} length of the input's string value
|
||
*/
|
||
ctrl.fitInputWidthToTextLength = function (length) {
|
||
var input = element[0].querySelector('md-input-container');
|
||
|
||
if (input) {
|
||
var computedStyle = getComputedStyle(input);
|
||
var minWidth = parseInt(computedStyle.minWidth);
|
||
var padding = parseInt(computedStyle.paddingLeft) + parseInt(computedStyle.paddingRight);
|
||
|
||
initialMaxWidth = initialMaxWidth || parseInt(computedStyle.maxWidth);
|
||
var newMaxWidth = Math.max(initialMaxWidth, minWidth + padding + (minWidth / 2 * length));
|
||
|
||
input.style.maxWidth = newMaxWidth + 'px';
|
||
}
|
||
};
|
||
};
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdSlider
|
||
* @module material.components.slider
|
||
* @restrict E
|
||
* @description
|
||
* The `<md-slider>` component allows the user to choose from a range of values.
|
||
*
|
||
* As per the [Material Design spec](https://material.io/archive/guidelines/style/color.html#color-color-system)
|
||
* the slider is in the accent color by default. The primary color palette may be used with
|
||
* the `md-primary` class.
|
||
*
|
||
* The slider has two modes:
|
||
* - "normal" mode where the user slides between a wide range of values
|
||
* - "discrete" mode where the user slides between only a few select values
|
||
*
|
||
* To enable discrete mode, add the `md-discrete` attribute to a slider
|
||
* and use the `step` attribute to change the distance between
|
||
* values the user is allowed to pick.
|
||
*
|
||
* When using the keyboard:
|
||
* - pressing the arrow keys will increase or decrease the slider's value by one step
|
||
* - holding the Meta, Control, or Alt key while pressing the arrow keys will
|
||
* move the slider four steps at a time
|
||
* - pressing the Home key will move the slider to the first allowed value
|
||
* - pressing the End key will move the slider to the last allowed value
|
||
* - pressing the Page Up key will increase the slider value by ten
|
||
* - pressing the Page Down key will decrease the slider value by ten
|
||
*
|
||
* @usage
|
||
* <h4>Normal Mode</h4>
|
||
* <hljs lang="html">
|
||
* <md-slider ng-model="myValue" min="5" max="500">
|
||
* </md-slider>
|
||
* </hljs>
|
||
* <h4>Discrete Mode</h4>
|
||
* <hljs lang="html">
|
||
* <md-slider md-discrete ng-model="myDiscreteValue" step="10" min="10" max="130">
|
||
* </md-slider>
|
||
* </hljs>
|
||
* <h4>Invert Mode</h4>
|
||
* <hljs lang="html">
|
||
* <md-slider md-invert ng-model="myValue" step="10" min="10" max="130">
|
||
* </md-slider>
|
||
* </hljs>
|
||
*
|
||
* @param {expression} ng-model Assignable angular expression to be data-bound.
|
||
* The expression should evaluate to a `number`.
|
||
* @param {expression=} ng-disabled If this expression evaluates as truthy, the slider will be
|
||
* disabled.
|
||
* @param {expression=} ng-readonly If this expression evaluates as truthy, the slider will be in
|
||
* read only mode.
|
||
* @param {boolean=} md-discrete If this attribute exists during initialization, enable discrete
|
||
* mode. Defaults to `false`.
|
||
* @param {boolean=} md-vertical If this attribute exists during initialization, enable vertical
|
||
* orientation mode. Defaults to `false`.
|
||
* @param {boolean=} md-invert If this attribute exists during initialization, enable inverted mode.
|
||
* Defaults to `false`.
|
||
* @param {number=} step The distance between values the user is allowed to pick. Defaults to `1`.
|
||
* @param {number=} min The minimum value the user is allowed to pick. Defaults to `0`.
|
||
* @param {number=} max The maximum value the user is allowed to pick. Defaults to `100`.
|
||
* @param {number=} round The amount of numbers after the decimal point. The maximum is 6 to
|
||
* prevent scientific notation. Defaults to `3`.
|
||
*/
|
||
function SliderDirective($$rAF, $window, $mdAria, $mdUtil, $mdConstant, $mdTheming, $mdGesture,
|
||
$parse, $log, $timeout) {
|
||
return {
|
||
scope: {},
|
||
require: ['?ngModel', '?^mdSliderContainer'],
|
||
template:
|
||
'<div class="md-slider-wrapper">' +
|
||
'<div class="md-slider-content">' +
|
||
'<div class="md-track-container">' +
|
||
'<div class="md-track"></div>' +
|
||
'<div class="md-track md-track-fill"></div>' +
|
||
'<div class="md-track-ticks"></div>' +
|
||
'</div>' +
|
||
'<div class="md-thumb-container">' +
|
||
'<div class="md-thumb"></div>' +
|
||
'<div class="md-focus-thumb"></div>' +
|
||
'<div class="md-focus-ring"></div>' +
|
||
'<div class="md-sign">' +
|
||
'<span class="md-thumb-text"></span>' +
|
||
'</div>' +
|
||
'<div class="md-disabled-thumb"></div>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'</div>',
|
||
compile: compile
|
||
};
|
||
|
||
// **********************************************************
|
||
// Private Methods
|
||
// **********************************************************
|
||
|
||
function compile (tElement, tAttrs) {
|
||
var wrapper = angular.element(tElement[0].getElementsByClassName('md-slider-wrapper'));
|
||
|
||
var tabIndex = tAttrs.tabindex || 0;
|
||
wrapper.attr('tabindex', tabIndex);
|
||
|
||
if (tAttrs.disabled || tAttrs.ngDisabled) wrapper.attr('tabindex', -1);
|
||
|
||
wrapper.attr('role', 'slider');
|
||
|
||
$mdAria.expect(tElement, 'aria-label');
|
||
|
||
return postLink;
|
||
}
|
||
|
||
function postLink(scope, element, attr, ctrls) {
|
||
$mdTheming(element);
|
||
var ngModelCtrl = ctrls[0] || {
|
||
// Mock ngModelController if it doesn't exist to give us
|
||
// the minimum functionality needed
|
||
$setViewValue: function(val) {
|
||
this.$viewValue = val;
|
||
this.$viewChangeListeners.forEach(function(cb) { cb(); });
|
||
},
|
||
$parsers: [],
|
||
$formatters: [],
|
||
$viewChangeListeners: []
|
||
};
|
||
|
||
var containerCtrl = ctrls[1];
|
||
var container = angular.element($mdUtil.getClosest(element, '_md-slider-container', true));
|
||
var isDisabled = attr.ngDisabled ? angular.bind(null, $parse(attr.ngDisabled), scope.$parent) : function () {
|
||
return element[0].hasAttribute('disabled');
|
||
};
|
||
|
||
var thumb = angular.element(element[0].querySelector('.md-thumb'));
|
||
var thumbText = angular.element(element[0].querySelector('.md-thumb-text'));
|
||
var thumbContainer = thumb.parent();
|
||
var trackContainer = angular.element(element[0].querySelector('.md-track-container'));
|
||
var activeTrack = angular.element(element[0].querySelector('.md-track-fill'));
|
||
var tickContainer = angular.element(element[0].querySelector('.md-track-ticks'));
|
||
var wrapper = angular.element(element[0].getElementsByClassName('md-slider-wrapper'));
|
||
var content = angular.element(element[0].getElementsByClassName('md-slider-content'));
|
||
var throttledRefreshDimensions = $mdUtil.throttle(refreshSliderDimensions, 5000);
|
||
|
||
// Default values, overridable by attrs
|
||
var DEFAULT_ROUND = 3;
|
||
var vertical = angular.isDefined(attr.mdVertical);
|
||
var discrete = angular.isDefined(attr.mdDiscrete);
|
||
var invert = angular.isDefined(attr.mdInvert);
|
||
angular.isDefined(attr.min) ? attr.$observe('min', updateMin) : updateMin(0);
|
||
angular.isDefined(attr.max) ? attr.$observe('max', updateMax) : updateMax(100);
|
||
angular.isDefined(attr.step)? attr.$observe('step', updateStep) : updateStep(1);
|
||
angular.isDefined(attr.round)? attr.$observe('round', updateRound) : updateRound(DEFAULT_ROUND);
|
||
|
||
// We have to manually stop the $watch on ngDisabled because it exists
|
||
// on the parent scope, and won't be automatically destroyed when
|
||
// the component is destroyed.
|
||
var stopDisabledWatch = angular.noop;
|
||
if (attr.ngDisabled) {
|
||
stopDisabledWatch = scope.$parent.$watch(attr.ngDisabled, updateAriaDisabled);
|
||
}
|
||
|
||
$mdGesture.register(wrapper, 'drag', { horizontal: !vertical });
|
||
|
||
scope.mouseActive = false;
|
||
|
||
wrapper
|
||
.on('keydown', keydownListener)
|
||
.on('mousedown', mouseDownListener)
|
||
.on('focus', focusListener)
|
||
.on('blur', blurListener)
|
||
.on('$md.pressdown', onPressDown)
|
||
.on('$md.pressup', onPressUp)
|
||
.on('$md.dragstart', onDragStart)
|
||
.on('$md.drag', onDrag)
|
||
.on('$md.dragend', onDragEnd);
|
||
|
||
// On resize, recalculate the slider's dimensions and re-render
|
||
function updateAll() {
|
||
refreshSliderDimensions();
|
||
ngModelRender();
|
||
}
|
||
setTimeout(updateAll, 0);
|
||
|
||
var debouncedUpdateAll = $$rAF.throttle(updateAll);
|
||
angular.element($window).on('resize', debouncedUpdateAll);
|
||
|
||
scope.$on('$destroy', function() {
|
||
angular.element($window).off('resize', debouncedUpdateAll);
|
||
});
|
||
|
||
ngModelCtrl.$render = ngModelRender;
|
||
ngModelCtrl.$viewChangeListeners.push(ngModelRender);
|
||
ngModelCtrl.$formatters.push(minMaxValidator);
|
||
ngModelCtrl.$formatters.push(stepValidator);
|
||
|
||
/**
|
||
* Attributes
|
||
*/
|
||
var min;
|
||
var max;
|
||
var step;
|
||
var round;
|
||
function updateMin(value) {
|
||
min = parseFloat(value);
|
||
ngModelCtrl.$viewValue = minMaxValidator(ngModelCtrl.$modelValue, min, max);
|
||
wrapper.attr('aria-valuemin', value);
|
||
updateAll();
|
||
}
|
||
function updateMax(value) {
|
||
max = parseFloat(value);
|
||
ngModelCtrl.$viewValue = minMaxValidator(ngModelCtrl.$modelValue, min, max);
|
||
wrapper.attr('aria-valuemax', value);
|
||
updateAll();
|
||
}
|
||
function updateStep(value) {
|
||
step = parseFloat(value);
|
||
}
|
||
function updateRound(value) {
|
||
// Set max round digits to 6, after 6 the input uses scientific notation
|
||
round = minMaxValidator(parseInt(value), 0, 6);
|
||
}
|
||
function updateAriaDisabled() {
|
||
element.attr('aria-disabled', !!isDisabled());
|
||
}
|
||
|
||
// Draw the ticks with canvas.
|
||
// The alternative to drawing ticks with canvas is to draw one element for each tick,
|
||
// which could quickly become a performance bottleneck.
|
||
var tickCanvas, tickCtx;
|
||
function redrawTicks() {
|
||
if (!discrete || isDisabled()) return;
|
||
if (angular.isUndefined(step)) return;
|
||
|
||
if (step <= 0) {
|
||
var msg = 'Slider step value must be greater than zero when in discrete mode';
|
||
$log.error(msg);
|
||
throw new Error(msg);
|
||
}
|
||
|
||
var numSteps = Math.floor((max - min) / step);
|
||
if (!tickCanvas) {
|
||
tickCanvas = angular.element('<canvas>').css('position', 'absolute');
|
||
tickContainer.append(tickCanvas);
|
||
|
||
tickCtx = tickCanvas[0].getContext('2d');
|
||
}
|
||
|
||
var dimensions = getSliderDimensions();
|
||
|
||
// If `dimensions` doesn't have height and width it might be the first attempt so we will refresh dimensions
|
||
if (dimensions && !dimensions.height && !dimensions.width) {
|
||
refreshSliderDimensions();
|
||
dimensions = sliderDimensions;
|
||
}
|
||
|
||
tickCanvas[0].width = dimensions.width;
|
||
tickCanvas[0].height = dimensions.height;
|
||
|
||
var distance;
|
||
for (var i = 0; i <= numSteps; i++) {
|
||
var trackTicksStyle = $window.getComputedStyle(tickContainer[0]);
|
||
tickCtx.fillStyle = trackTicksStyle.color || 'black';
|
||
|
||
distance = Math.floor((vertical ? dimensions.height : dimensions.width) * (i / numSteps));
|
||
|
||
tickCtx.fillRect(vertical ? 0 : distance - 1,
|
||
vertical ? distance - 1 : 0,
|
||
vertical ? dimensions.width : 2,
|
||
vertical ? 2 : dimensions.height);
|
||
}
|
||
}
|
||
|
||
function clearTicks() {
|
||
if (tickCanvas && tickCtx) {
|
||
var dimensions = getSliderDimensions();
|
||
tickCtx.clearRect(0, 0, dimensions.width, dimensions.height);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Refreshing Dimensions
|
||
*/
|
||
var sliderDimensions = {};
|
||
refreshSliderDimensions();
|
||
function refreshSliderDimensions() {
|
||
sliderDimensions = trackContainer[0].getBoundingClientRect();
|
||
}
|
||
function getSliderDimensions() {
|
||
throttledRefreshDimensions();
|
||
return sliderDimensions;
|
||
}
|
||
|
||
/**
|
||
* left/right/up/down arrow listener
|
||
* @param {!KeyboardEvent} ev
|
||
*/
|
||
function keydownListener(ev) {
|
||
if (isDisabled()) return;
|
||
var keyCodes = $mdConstant.KEY_CODE;
|
||
|
||
var changeAmount;
|
||
switch (ev.keyCode) {
|
||
case keyCodes.DOWN_ARROW:
|
||
case keyCodes.LEFT_ARROW:
|
||
ev.preventDefault();
|
||
changeAmount = -step;
|
||
break;
|
||
case keyCodes.UP_ARROW:
|
||
case keyCodes.RIGHT_ARROW:
|
||
ev.preventDefault();
|
||
changeAmount = step;
|
||
break;
|
||
case keyCodes.PAGE_DOWN:
|
||
ev.preventDefault();
|
||
changeAmount = -step * stepPageSize;
|
||
break;
|
||
case keyCodes.PAGE_UP:
|
||
ev.preventDefault();
|
||
changeAmount = step * stepPageSize;
|
||
break;
|
||
case keyCodes.HOME:
|
||
ev.preventDefault();
|
||
ev.stopPropagation();
|
||
updateValue(min);
|
||
break;
|
||
case keyCodes.END:
|
||
ev.preventDefault();
|
||
ev.stopPropagation();
|
||
updateValue(max);
|
||
break;
|
||
}
|
||
if (changeAmount) {
|
||
changeAmount = invert ? -changeAmount : changeAmount;
|
||
if (ev.metaKey || ev.ctrlKey || ev.altKey) {
|
||
changeAmount *= modifierMultiplier;
|
||
}
|
||
ev.preventDefault();
|
||
ev.stopPropagation();
|
||
updateValue(ngModelCtrl.$viewValue + changeAmount);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param value new slider value used for setting the model value
|
||
*/
|
||
function updateValue(value) {
|
||
scope.$evalAsync(function() {
|
||
setModelValue(value);
|
||
});
|
||
}
|
||
|
||
function mouseDownListener() {
|
||
redrawTicks();
|
||
|
||
scope.mouseActive = true;
|
||
wrapper.removeClass('md-focused');
|
||
|
||
$timeout(function() {
|
||
scope.mouseActive = false;
|
||
}, 100);
|
||
}
|
||
|
||
function focusListener() {
|
||
if (scope.mouseActive === false) {
|
||
wrapper.addClass('md-focused');
|
||
}
|
||
}
|
||
|
||
function blurListener() {
|
||
wrapper.removeClass('md-focused');
|
||
element.removeClass('md-active');
|
||
clearTicks();
|
||
}
|
||
|
||
/**
|
||
* ngModel setters and validators
|
||
*/
|
||
function setModelValue(value) {
|
||
ngModelCtrl.$setViewValue(minMaxValidator(stepValidator(value)));
|
||
}
|
||
function ngModelRender() {
|
||
if (isNaN(ngModelCtrl.$viewValue)) {
|
||
ngModelCtrl.$viewValue = ngModelCtrl.$modelValue;
|
||
}
|
||
|
||
ngModelCtrl.$viewValue = minMaxValidator(ngModelCtrl.$viewValue);
|
||
|
||
var percent = valueToPercent(ngModelCtrl.$viewValue);
|
||
scope.modelValue = ngModelCtrl.$viewValue;
|
||
wrapper.attr('aria-valuenow', ngModelCtrl.$viewValue);
|
||
setSliderPercent(percent);
|
||
thumbText.text(ngModelCtrl.$viewValue);
|
||
}
|
||
|
||
function minMaxValidator(value, minValue, maxValue) {
|
||
if (angular.isNumber(value)) {
|
||
minValue = angular.isNumber(minValue) ? minValue : min;
|
||
maxValue = angular.isNumber(maxValue) ? maxValue : max;
|
||
|
||
return Math.max(minValue, Math.min(maxValue, value));
|
||
}
|
||
}
|
||
|
||
function stepValidator(value) {
|
||
if (angular.isNumber(value)) {
|
||
var formattedValue = (Math.round((value - min) / step) * step + min);
|
||
formattedValue = (Math.round(formattedValue * Math.pow(10, round)) / Math.pow(10, round));
|
||
|
||
if (containerCtrl && containerCtrl.fitInputWidthToTextLength) {
|
||
$mdUtil.debounce(function () {
|
||
containerCtrl.fitInputWidthToTextLength(formattedValue.toString().length);
|
||
}, 100)();
|
||
}
|
||
|
||
return formattedValue;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {number} percent 0-1
|
||
*/
|
||
function setSliderPercent(percent) {
|
||
|
||
percent = clamp(percent);
|
||
|
||
var thumbPosition = (percent * 100) + '%';
|
||
var activeTrackPercent = invert ? (1 - percent) * 100 + '%' : thumbPosition;
|
||
|
||
if (vertical) {
|
||
thumbContainer.css('bottom', thumbPosition);
|
||
}
|
||
else {
|
||
$mdUtil.bidiProperty(thumbContainer, 'left', 'right', thumbPosition);
|
||
}
|
||
|
||
|
||
activeTrack.css(vertical ? 'height' : 'width', activeTrackPercent);
|
||
|
||
element.toggleClass((invert ? 'md-max' : 'md-min'), percent === 0);
|
||
element.toggleClass((invert ? 'md-min' : 'md-max'), percent === 1);
|
||
}
|
||
|
||
/**
|
||
* Slide listeners
|
||
*/
|
||
var isDragging = false;
|
||
|
||
function onPressDown(ev) {
|
||
if (isDisabled()) return;
|
||
|
||
element.addClass('md-active');
|
||
element[0].focus();
|
||
refreshSliderDimensions();
|
||
|
||
var exactVal = percentToValue(positionToPercent(vertical ? ev.srcEvent.clientY : ev.srcEvent.clientX));
|
||
var closestVal = minMaxValidator(stepValidator(exactVal));
|
||
scope.$apply(function() {
|
||
setModelValue(closestVal);
|
||
setSliderPercent(valueToPercent(closestVal));
|
||
});
|
||
}
|
||
function onPressUp(ev) {
|
||
if (isDisabled()) return;
|
||
|
||
element.removeClass('md-dragging');
|
||
|
||
var exactVal = percentToValue(positionToPercent(vertical ? ev.srcEvent.clientY : ev.srcEvent.clientX));
|
||
var closestVal = minMaxValidator(stepValidator(exactVal));
|
||
scope.$apply(function() {
|
||
setModelValue(closestVal);
|
||
ngModelRender();
|
||
});
|
||
}
|
||
function onDragStart(ev) {
|
||
if (isDisabled()) return;
|
||
isDragging = true;
|
||
|
||
ev.stopPropagation();
|
||
|
||
element.addClass('md-dragging');
|
||
setSliderFromEvent(ev);
|
||
}
|
||
function onDrag(ev) {
|
||
if (!isDragging) return;
|
||
ev.stopPropagation();
|
||
setSliderFromEvent(ev);
|
||
}
|
||
function onDragEnd(ev) {
|
||
if (!isDragging) return;
|
||
ev.stopPropagation();
|
||
isDragging = false;
|
||
}
|
||
|
||
function setSliderFromEvent(ev) {
|
||
// While panning discrete, update only the
|
||
// visual positioning but not the model value.
|
||
if (discrete) adjustThumbPosition(vertical ? ev.srcEvent.clientY : ev.srcEvent.clientX);
|
||
else doSlide(vertical ? ev.srcEvent.clientY : ev.srcEvent.clientX);
|
||
}
|
||
|
||
/**
|
||
* Slide the UI by changing the model value
|
||
* @param x
|
||
*/
|
||
function doSlide(x) {
|
||
scope.$evalAsync(function() {
|
||
setModelValue(percentToValue(positionToPercent(x)));
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Slide the UI without changing the model (while dragging/panning)
|
||
* @param x
|
||
*/
|
||
function adjustThumbPosition(x) {
|
||
var exactVal = percentToValue(positionToPercent(x));
|
||
var closestVal = minMaxValidator(stepValidator(exactVal));
|
||
setSliderPercent(positionToPercent(x));
|
||
thumbText.text(closestVal);
|
||
}
|
||
|
||
/**
|
||
* Clamps the value to be between 0 and 1.
|
||
* @param {number} value The value to clamp.
|
||
* @returns {number}
|
||
*/
|
||
function clamp(value) {
|
||
return Math.max(0, Math.min(value || 0, 1));
|
||
}
|
||
|
||
/**
|
||
* Convert position on slider to percentage value of offset from beginning...
|
||
* @param position
|
||
* @returns {number}
|
||
*/
|
||
function positionToPercent(position) {
|
||
var offset = vertical ? sliderDimensions.top : sliderDimensions.left;
|
||
var size = vertical ? sliderDimensions.height : sliderDimensions.width;
|
||
var calc = (position - offset) / size;
|
||
|
||
if (!vertical && $mdUtil.isRtl(attr)) {
|
||
calc = 1 - calc;
|
||
}
|
||
|
||
return Math.max(0, Math.min(1, vertical ? 1 - calc : calc));
|
||
}
|
||
|
||
/**
|
||
* Convert percentage offset on slide to equivalent model value
|
||
* @param percent
|
||
* @returns {*}
|
||
*/
|
||
function percentToValue(percent) {
|
||
var adjustedPercent = invert ? (1 - percent) : percent;
|
||
return (min + adjustedPercent * (max - min));
|
||
}
|
||
|
||
function valueToPercent(val) {
|
||
var percent = (val - min) / (max - min);
|
||
return invert ? (1 - percent) : percent;
|
||
}
|
||
}
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.sticky
|
||
* @description
|
||
* Sticky effects for md.
|
||
*/
|
||
MdSticky.$inject = ["$mdConstant", "$$rAF", "$mdUtil", "$compile"];
|
||
angular
|
||
.module('material.components.sticky', [
|
||
'material.core',
|
||
'material.components.content'
|
||
])
|
||
.factory('$mdSticky', MdSticky);
|
||
|
||
/**
|
||
* @ngdoc service
|
||
* @name $mdSticky
|
||
* @module material.components.sticky
|
||
*
|
||
* @description
|
||
* The `$mdSticky` service provides the capability to make elements sticky, even when the browser
|
||
* does not support `position: sticky`.
|
||
*
|
||
* Whenever the current browser supports stickiness natively, the `$mdSticky` service will leverage
|
||
* the native browser's sticky functionality.
|
||
*
|
||
* By default the `$mdSticky` service compiles the cloned element, when not specified through the
|
||
* `stickyClone` parameter, in the same scope as the actual element lives.
|
||
*
|
||
* @usage
|
||
* <hljs lang="js">
|
||
* angular.module('myModule')
|
||
* .directive('stickyText', function($mdSticky) {
|
||
* return {
|
||
* restrict: 'E',
|
||
* template: '<span>Sticky Text</span>',
|
||
* link: function(scope, element) {
|
||
* $mdSticky(scope, element);
|
||
* }
|
||
* };
|
||
* });
|
||
* </hljs>
|
||
*
|
||
* <h3>Notes</h3>
|
||
* When using an element which contains a compiled directive that changes the DOM structure
|
||
* during compilation, you should compile the clone yourself.
|
||
*
|
||
* An example of this usage can be found below:
|
||
* <hljs lang="js">
|
||
* angular.module('myModule')
|
||
* .directive('stickySelect', function($mdSticky, $compile) {
|
||
* var SELECT_TEMPLATE =
|
||
* '<md-select ng-model="selected">' +
|
||
* ' <md-option>Option 1</md-option>' +
|
||
* '</md-select>';
|
||
*
|
||
* return {
|
||
* restrict: 'E',
|
||
* replace: true,
|
||
* template: SELECT_TEMPLATE,
|
||
* link: function(scope, element) {
|
||
* $mdSticky(scope, element, $compile(SELECT_TEMPLATE)(scope));
|
||
* }
|
||
* };
|
||
* });
|
||
* </hljs>
|
||
*
|
||
* @returns {function(IScope, JQLite, ITemplateLinkingFunction=): void} `$mdSticky` returns a
|
||
* function that takes three arguments:
|
||
* - `scope`: the scope to use when compiling the clone and listening for the `$destroy` event,
|
||
* which triggers removal of the clone
|
||
* - `element`: the element that will be 'sticky'
|
||
* - `stickyClone`: An optional clone of the element (returned from AngularJS'
|
||
* [$compile service](https://docs.angularjs.org/api/ng/service/$compile#usage)),
|
||
* that will be shown when the user starts scrolling past the original element. If not
|
||
* provided, the result of `element.clone()` will be used and compiled in the given scope.
|
||
*/
|
||
function MdSticky($mdConstant, $$rAF, $mdUtil, $compile) {
|
||
|
||
var browserStickySupport = $mdUtil.checkStickySupport();
|
||
|
||
/**
|
||
* Registers an element as sticky, used internally by directives to register themselves.
|
||
*/
|
||
return function registerStickyElement(scope, element, stickyClone) {
|
||
var contentCtrl = element.controller('mdContent');
|
||
if (!contentCtrl) return;
|
||
|
||
if (browserStickySupport) {
|
||
element.css({
|
||
position: browserStickySupport,
|
||
top: 0,
|
||
'z-index': 2
|
||
});
|
||
} else {
|
||
var $$sticky = contentCtrl.$element.data('$$sticky');
|
||
if (!$$sticky) {
|
||
$$sticky = setupSticky(contentCtrl);
|
||
contentCtrl.$element.data('$$sticky', $$sticky);
|
||
}
|
||
|
||
// Compile our cloned element, when cloned in this service, into the given scope.
|
||
var cloneElement = stickyClone || $compile(element.clone())(scope);
|
||
|
||
var deregister = $$sticky.add(element, cloneElement);
|
||
scope.$on('$destroy', deregister);
|
||
}
|
||
};
|
||
|
||
function setupSticky(contentCtrl) {
|
||
var contentEl = contentCtrl.$element;
|
||
|
||
// Refresh elements is very expensive, so we use the debounced
|
||
// version when possible.
|
||
var debouncedRefreshElements = $$rAF.throttle(refreshElements);
|
||
|
||
// setupAugmentedScrollEvents gives us `$scrollstart` and `$scroll`,
|
||
// more reliable than `scroll` on android.
|
||
setupAugmentedScrollEvents(contentEl);
|
||
contentEl.on('$scrollstart', debouncedRefreshElements);
|
||
contentEl.on('$scroll', onScroll);
|
||
|
||
var self;
|
||
return self = {
|
||
prev: null,
|
||
current: null, // the currently stickied item
|
||
next: null,
|
||
items: [],
|
||
add: add,
|
||
refreshElements: refreshElements
|
||
};
|
||
|
||
/***************
|
||
* Public
|
||
***************/
|
||
// Add an element and its sticky clone to this content's sticky collection
|
||
function add(element, stickyClone) {
|
||
stickyClone.addClass('md-sticky-clone');
|
||
|
||
var item = {
|
||
element: element,
|
||
clone: stickyClone
|
||
};
|
||
self.items.push(item);
|
||
|
||
$mdUtil.nextTick(function() {
|
||
contentEl.prepend(item.clone);
|
||
});
|
||
|
||
debouncedRefreshElements();
|
||
|
||
return function remove() {
|
||
self.items.forEach(function(item, index) {
|
||
if (item.element[0] === element[0]) {
|
||
self.items.splice(index, 1);
|
||
item.clone.remove();
|
||
}
|
||
});
|
||
debouncedRefreshElements();
|
||
};
|
||
}
|
||
|
||
function refreshElements() {
|
||
// Sort our collection of elements by their current position in the DOM.
|
||
// We need to do this because our elements' order of being added may not
|
||
// be the same as their order of display.
|
||
self.items.forEach(refreshPosition);
|
||
self.items = self.items.sort(function(a, b) {
|
||
return a.top < b.top ? -1 : 1;
|
||
});
|
||
|
||
// Find which item in the list should be active,
|
||
// based upon the content's current scroll position
|
||
var item;
|
||
var currentScrollTop = contentEl.prop('scrollTop');
|
||
for (var i = self.items.length - 1; i >= 0; i--) {
|
||
if (currentScrollTop > self.items[i].top) {
|
||
item = self.items[i];
|
||
break;
|
||
}
|
||
}
|
||
setCurrentItem(item);
|
||
}
|
||
|
||
/***************
|
||
* Private
|
||
***************/
|
||
|
||
// Find the `top` of an item relative to the content element,
|
||
// and also the height.
|
||
function refreshPosition(item) {
|
||
// Find the top of an item by adding to the offsetHeight until we reach the
|
||
// content element.
|
||
var current = item.element[0];
|
||
item.top = 0;
|
||
item.left = 0;
|
||
item.right = 0;
|
||
while (current && current !== contentEl[0]) {
|
||
item.top += current.offsetTop;
|
||
item.left += current.offsetLeft;
|
||
if (current.offsetParent) {
|
||
// Compute offsetRight
|
||
item.right += current.offsetParent.offsetWidth - current.offsetWidth - current.offsetLeft;
|
||
}
|
||
current = current.offsetParent;
|
||
}
|
||
item.height = item.element.prop('offsetHeight');
|
||
|
||
var defaultVal = $mdUtil.floatingScrollbars() ? '0' : undefined;
|
||
$mdUtil.bidi(item.clone, 'margin-left', item.left, defaultVal);
|
||
$mdUtil.bidi(item.clone, 'margin-right', defaultVal, item.right);
|
||
}
|
||
|
||
// As we scroll, push in and select the correct sticky element.
|
||
function onScroll() {
|
||
var scrollTop = contentEl.prop('scrollTop');
|
||
var isScrollingDown = scrollTop > (onScroll.prevScrollTop || 0);
|
||
|
||
// Store the previous scroll so we know which direction we are scrolling
|
||
onScroll.prevScrollTop = scrollTop;
|
||
|
||
//
|
||
// AT TOP (not scrolling)
|
||
//
|
||
if (scrollTop === 0) {
|
||
// If we're at the top, just clear the current item and return
|
||
setCurrentItem(null);
|
||
return;
|
||
}
|
||
|
||
//
|
||
// SCROLLING DOWN (going towards the next item)
|
||
//
|
||
if (isScrollingDown) {
|
||
|
||
// If we've scrolled down past the next item's position, sticky it and return
|
||
if (self.next && self.next.top <= scrollTop) {
|
||
setCurrentItem(self.next);
|
||
return;
|
||
}
|
||
|
||
// If the next item is close to the current one, push the current one up out of the way
|
||
if (self.current && self.next && self.next.top - scrollTop <= self.next.height) {
|
||
translate(self.current, scrollTop + (self.next.top - self.next.height - scrollTop));
|
||
return;
|
||
}
|
||
}
|
||
|
||
//
|
||
// SCROLLING UP (not at the top & not scrolling down; must be scrolling up)
|
||
//
|
||
if (!isScrollingDown) {
|
||
|
||
// If we've scrolled up past the previous item's position, sticky it and return
|
||
if (self.current && self.prev && scrollTop < self.current.top) {
|
||
setCurrentItem(self.prev);
|
||
return;
|
||
}
|
||
|
||
// If the next item is close to the current one, pull the current one down into view
|
||
if (self.next && self.current && (scrollTop >= (self.next.top - self.current.height))) {
|
||
translate(self.current, scrollTop + (self.next.top - scrollTop - self.current.height));
|
||
return;
|
||
}
|
||
}
|
||
|
||
//
|
||
// Otherwise, just move the current item to the proper place (scrolling up or down)
|
||
//
|
||
if (self.current) {
|
||
translate(self.current, scrollTop);
|
||
}
|
||
}
|
||
|
||
function setCurrentItem(item) {
|
||
if (self.current === item) return;
|
||
// Deactivate currently active item
|
||
if (self.current) {
|
||
translate(self.current, null);
|
||
setStickyState(self.current, null);
|
||
}
|
||
|
||
// Activate new item if given
|
||
if (item) {
|
||
setStickyState(item, 'active');
|
||
}
|
||
|
||
self.current = item;
|
||
var index = self.items.indexOf(item);
|
||
// If index === -1, index + 1 = 0. It works out.
|
||
self.next = self.items[index + 1];
|
||
self.prev = self.items[index - 1];
|
||
setStickyState(self.next, 'next');
|
||
setStickyState(self.prev, 'prev');
|
||
}
|
||
|
||
function setStickyState(item, state) {
|
||
if (!item || item.state === state) return;
|
||
if (item.state) {
|
||
item.clone.attr('sticky-prev-state', item.state);
|
||
item.element.attr('sticky-prev-state', item.state);
|
||
}
|
||
item.clone.attr('sticky-state', state);
|
||
item.element.attr('sticky-state', state);
|
||
item.state = state;
|
||
}
|
||
|
||
function translate(item, amount) {
|
||
if (!item) return;
|
||
if (amount === null || amount === undefined) {
|
||
if (item.translateY) {
|
||
item.translateY = null;
|
||
item.clone.css($mdConstant.CSS.TRANSFORM, '');
|
||
}
|
||
} else {
|
||
item.translateY = amount;
|
||
|
||
$mdUtil.bidi(item.clone, $mdConstant.CSS.TRANSFORM,
|
||
'translate3d(' + item.left + 'px,' + amount + 'px,0)',
|
||
'translateY(' + amount + 'px)'
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
// Android 4.4 don't accurately give scroll events.
|
||
// To fix this problem, we setup a fake scroll event. We say:
|
||
// > If a scroll or touchmove event has happened in the last DELAY milliseconds,
|
||
// then send a `$scroll` event every animationFrame.
|
||
// Additionally, we add $scrollstart and $scrollend events.
|
||
function setupAugmentedScrollEvents(element) {
|
||
var SCROLL_END_DELAY = 200;
|
||
var isScrolling;
|
||
var lastScrollTime;
|
||
element.on('scroll touchmove', function() {
|
||
if (!isScrolling) {
|
||
isScrolling = true;
|
||
$$rAF.throttle(loopScrollEvent);
|
||
element.triggerHandler('$scrollstart');
|
||
}
|
||
element.triggerHandler('$scroll');
|
||
lastScrollTime = +$mdUtil.now();
|
||
});
|
||
|
||
function loopScrollEvent() {
|
||
if (+$mdUtil.now() - lastScrollTime > SCROLL_END_DELAY) {
|
||
isScrolling = false;
|
||
element.triggerHandler('$scrollend');
|
||
} else {
|
||
element.triggerHandler('$scroll');
|
||
$$rAF.throttle(loopScrollEvent);
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.subheader
|
||
* @description
|
||
* SubHeader module
|
||
*
|
||
* Subheaders are special list tiles that delineate distinct sections of a
|
||
* list or grid list and are typically related to the current filtering or
|
||
* sorting criteria. Subheader tiles are either displayed inline with tiles or
|
||
* can be associated with content, for example, in an adjacent column.
|
||
*
|
||
* Upon scrolling, subheaders remain pinned to the top of the screen and remain
|
||
* pinned until pushed on or off screen by the next subheader. @see [Material
|
||
* Design Specifications](https://material.io/archive/guidelines/components/subheaders.html)
|
||
*
|
||
* > To improve the visual grouping of content, use the system color for your subheaders.
|
||
*
|
||
*/
|
||
MdSubheaderDirective.$inject = ["$mdSticky", "$compile", "$mdTheming", "$mdUtil", "$mdAria"];
|
||
angular
|
||
.module('material.components.subheader', [
|
||
'material.core',
|
||
'material.components.sticky'
|
||
])
|
||
.directive('mdSubheader', MdSubheaderDirective);
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdSubheader
|
||
* @module material.components.subheader
|
||
*
|
||
* @restrict E
|
||
*
|
||
* @description
|
||
* The `md-subheader` directive creates a sticky subheader for a section.
|
||
*
|
||
* Developers are able to disable the stickiness of the subheader by using the following markup
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-subheader class="md-no-sticky">Not Sticky</md-subheader>
|
||
* </hljs>
|
||
*
|
||
* ### Notes
|
||
* - The `md-subheader` directive uses the <a ng-href="api/service/$mdSticky">$mdSticky</a> service
|
||
* to make the subheader sticky.
|
||
*
|
||
* > Whenever the current browser doesn't support stickiness natively, the subheader
|
||
* will be compiled twice to create a sticky clone of the subheader.
|
||
*
|
||
* @usage
|
||
* <hljs lang="html">
|
||
* <md-subheader>Online Friends</md-subheader>
|
||
* </hljs>
|
||
*/
|
||
|
||
function MdSubheaderDirective($mdSticky, $compile, $mdTheming, $mdUtil, $mdAria) {
|
||
return {
|
||
restrict: 'E',
|
||
replace: true,
|
||
transclude: true,
|
||
template: (
|
||
'<div class="md-subheader _md">' +
|
||
' <div class="md-subheader-inner">' +
|
||
' <div class="md-subheader-content"></div>' +
|
||
' </div>' +
|
||
'</div>'
|
||
),
|
||
link: function postLink(scope, element, attr, controllers, transclude) {
|
||
$mdTheming(element);
|
||
element.addClass('_md');
|
||
|
||
// Remove the ngRepeat attribute from the root element, because we don't want to compile
|
||
// the ngRepeat for the sticky clone again.
|
||
$mdUtil.prefixer().removeAttribute(element, 'ng-repeat');
|
||
|
||
var outerHTML = element[0].outerHTML;
|
||
|
||
function getContent(el) {
|
||
return angular.element(el[0].querySelector('.md-subheader-content'));
|
||
}
|
||
|
||
// Set the ARIA attributes on the original element since it keeps it's original place in
|
||
// the DOM, whereas the clones are in reverse order. Should be done after the outerHTML,
|
||
// in order to avoid having multiple element be marked as headers.
|
||
attr.$set('role', 'heading');
|
||
$mdAria.expect(element, 'aria-level', '2');
|
||
|
||
// Transclude the user-given contents of the subheader
|
||
// the conventional way.
|
||
transclude(scope, function(clone) {
|
||
getContent(element).append(clone);
|
||
});
|
||
|
||
// Create another clone, that uses the outer and inner contents
|
||
// of the element, that will be 'stickied' as the user scrolls.
|
||
if (!element.hasClass('md-no-sticky')) {
|
||
transclude(scope, function(clone) {
|
||
// If the user adds an ng-if or ng-repeat directly to the md-subheader element, the
|
||
// compiled clone below will only be a comment tag (since they replace their elements with
|
||
// a comment) which cannot be properly passed to the $mdSticky; so we wrap it in our own
|
||
// DIV to ensure we have something $mdSticky can use
|
||
var wrapper = $compile('<div class="md-subheader-wrapper" aria-hidden="true">' + outerHTML + '</div>')(scope);
|
||
|
||
// Delay initialization until after any `ng-if`/`ng-repeat`/etc has finished before
|
||
// attempting to create the clone
|
||
$mdUtil.nextTick(function() {
|
||
// Append our transcluded clone into the wrapper.
|
||
// We don't have to recompile the element again, because the clone is already
|
||
// compiled in it's transclusion scope. If we recompile the outerHTML of the new clone, we would lose
|
||
// our ngIf's and other previous registered bindings / properties.
|
||
getContent(wrapper).append(clone);
|
||
});
|
||
|
||
// Make the element sticky and provide the stickyClone our self, to avoid recompilation of the subheader
|
||
// element.
|
||
$mdSticky(scope, element, wrapper);
|
||
});
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.swipe
|
||
* @description Swipe module!
|
||
*/
|
||
/**
|
||
* @ngdoc directive
|
||
* @module material.components.swipe
|
||
* @name mdSwipeLeft
|
||
*
|
||
* @restrict A
|
||
*
|
||
* @description
|
||
* The md-swipe-left directive allows you to specify custom behavior when an element is swiped
|
||
* left.
|
||
*
|
||
* ### Notes
|
||
* - The `$event.currentTarget` of the swiped element will be `null`, but you can get a
|
||
* reference to the element that actually holds the `md-swipe-left` directive by using
|
||
* `$target.current`
|
||
*
|
||
* > You can see this in action on the <a ng-href="demo/swipe">demo page</a> (Look at the Developer
|
||
* Tools console while swiping).
|
||
*
|
||
* @usage
|
||
* <hljs lang="html">
|
||
* <div md-swipe-left="onSwipeLeft($event, $target)">Swipe me left!</div>
|
||
* </hljs>
|
||
*/
|
||
/**
|
||
* @ngdoc directive
|
||
* @module material.components.swipe
|
||
* @name mdSwipeRight
|
||
*
|
||
* @restrict A
|
||
*
|
||
* @description
|
||
* The md-swipe-right directive allows you to specify custom behavior when an element is swiped
|
||
* right.
|
||
*
|
||
* ### Notes
|
||
* - The `$event.currentTarget` of the swiped element will be `null`, but you can get a
|
||
* reference to the element that actually holds the `md-swipe-right` directive by using
|
||
* `$target.current`
|
||
*
|
||
* > You can see this in action on the <a ng-href="demo/swipe">demo page</a> (Look at the Developer
|
||
* Tools console while swiping).
|
||
*
|
||
* @usage
|
||
* <hljs lang="html">
|
||
* <div md-swipe-right="onSwipeRight($event, $target)">Swipe me right!</div>
|
||
* </hljs>
|
||
*/
|
||
/**
|
||
* @ngdoc directive
|
||
* @module material.components.swipe
|
||
* @name mdSwipeUp
|
||
*
|
||
* @restrict A
|
||
*
|
||
* @description
|
||
* The md-swipe-up directive allows you to specify custom behavior when an element is swiped
|
||
* up.
|
||
*
|
||
* ### Notes
|
||
* - The `$event.currentTarget` of the swiped element will be `null`, but you can get a
|
||
* reference to the element that actually holds the `md-swipe-up` directive by using
|
||
* `$target.current`
|
||
*
|
||
* > You can see this in action on the <a ng-href="demo/swipe">demo page</a> (Look at the Developer
|
||
* Tools console while swiping).
|
||
*
|
||
* @usage
|
||
* <hljs lang="html">
|
||
* <div md-swipe-up="onSwipeUp($event, $target)">Swipe me up!</div>
|
||
* </hljs>
|
||
*/
|
||
/**
|
||
* @ngdoc directive
|
||
* @module material.components.swipe
|
||
* @name mdSwipeDown
|
||
*
|
||
* @restrict A
|
||
*
|
||
* @description
|
||
* The md-swipe-down directive allows you to specify custom behavior when an element is swiped
|
||
* down.
|
||
*
|
||
* ### Notes
|
||
* - The `$event.currentTarget` of the swiped element will be `null`, but you can get a
|
||
* reference to the element that actually holds the `md-swipe-down` directive by using
|
||
* `$target.current`
|
||
*
|
||
* > You can see this in action on the <a ng-href="demo/swipe">demo page</a> (Look at the Developer
|
||
* Tools console while swiping).
|
||
*
|
||
* @usage
|
||
* <hljs lang="html">
|
||
* <div md-swipe-down="onSwipeDown($event, $target)">Swipe me down!</div>
|
||
* </hljs>
|
||
*/
|
||
|
||
angular.module('material.components.swipe', ['material.core'])
|
||
.directive('mdSwipeLeft', getDirective('SwipeLeft'))
|
||
.directive('mdSwipeRight', getDirective('SwipeRight'))
|
||
.directive('mdSwipeUp', getDirective('SwipeUp'))
|
||
.directive('mdSwipeDown', getDirective('SwipeDown'));
|
||
|
||
function getDirective(name) {
|
||
DirectiveFactory.$inject = ["$parse"];
|
||
var directiveName = 'md' + name;
|
||
var eventName = '$md.' + name.toLowerCase();
|
||
|
||
return DirectiveFactory;
|
||
|
||
/* @ngInject */
|
||
function DirectiveFactory($parse) {
|
||
return { restrict: 'A', link: postLink };
|
||
function postLink(scope, element, attr) {
|
||
var fn = $parse(attr[directiveName]);
|
||
element.on(eventName, function(ev) {
|
||
var currentTarget = ev.currentTarget;
|
||
scope.$applyAsync(function() { fn(scope, { $event: ev, $target: { current: currentTarget } }); });
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.switch
|
||
*/
|
||
|
||
MdSwitch.$inject = ["mdCheckboxDirective", "$mdUtil", "$mdConstant", "$parse", "$$rAF", "$mdGesture", "$timeout"];
|
||
angular.module('material.components.switch', [
|
||
'material.core',
|
||
'material.components.checkbox'
|
||
])
|
||
.directive('mdSwitch', MdSwitch);
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @module material.components.switch
|
||
* @name mdSwitch
|
||
* @restrict E
|
||
*
|
||
* The switch directive is used very much like the normal [angular checkbox](https://docs.angularjs.org/api/ng/input/input%5Bcheckbox%5D).
|
||
*
|
||
* As per the [Material Design spec](https://material.io/archive/guidelines/style/color.html#color-color-system)
|
||
* the switch is in the accent color by default. The primary color palette may be used with
|
||
* the `md-primary` class.
|
||
*
|
||
* @param {expression} ng-model Assignable angular expression to data-bind to.
|
||
* @param {string=} name Property name of the form under which the control is published.
|
||
* @param {expression=} ng-true-value The value to which the expression should be set when selected.
|
||
* @param {expression=} ng-false-value The value to which the expression should be set when not selected.
|
||
* @param {expression=} ng-change Expression to be executed when the model value changes.
|
||
* @param {expression=} ng-disabled En/Disable based on the expression.
|
||
* @param {boolean=} md-no-ink Use of attribute indicates use of ripple ink effects.
|
||
* @param {string=} aria-label Publish the button label used by screen-readers for accessibility. Defaults to the switch's text.
|
||
* @param {boolean=} md-invert When set to true, the switch will be inverted.
|
||
*
|
||
* @usage
|
||
* <hljs lang="html">
|
||
* <md-switch ng-model="isActive" aria-label="Finished?">
|
||
* Finished ?
|
||
* </md-switch>
|
||
*
|
||
* <md-switch md-no-ink ng-model="hasInk" aria-label="No Ink Effects">
|
||
* No Ink Effects
|
||
* </md-switch>
|
||
*
|
||
* <md-switch ng-disabled="true" ng-model="isDisabled" aria-label="Disabled">
|
||
* Disabled
|
||
* </md-switch>
|
||
*
|
||
* </hljs>
|
||
*/
|
||
function MdSwitch(mdCheckboxDirective, $mdUtil, $mdConstant, $parse, $$rAF, $mdGesture, $timeout) {
|
||
var checkboxDirective = mdCheckboxDirective[0];
|
||
|
||
return {
|
||
restrict: 'E',
|
||
priority: $mdConstant.BEFORE_NG_ARIA,
|
||
transclude: true,
|
||
template:
|
||
'<div class="md-container">' +
|
||
'<div class="md-bar"></div>' +
|
||
'<div class="md-thumb-container">' +
|
||
'<div class="md-thumb" md-ink-ripple md-ink-ripple-checkbox></div>' +
|
||
'</div>'+
|
||
'</div>' +
|
||
'<div ng-transclude class="md-label"></div>',
|
||
require: ['^?mdInputContainer', '?ngModel', '?^form'],
|
||
compile: mdSwitchCompile
|
||
};
|
||
|
||
function mdSwitchCompile(element, attr) {
|
||
var checkboxLink = checkboxDirective.compile(element, attr).post;
|
||
// No transition on initial load.
|
||
element.addClass('md-dragging');
|
||
|
||
return function (scope, element, attr, ctrls) {
|
||
var containerCtrl = ctrls[0];
|
||
var ngModel = ctrls[1] || $mdUtil.fakeNgModel();
|
||
var formCtrl = ctrls[2];
|
||
|
||
var disabledGetter = null;
|
||
if (attr.disabled != null) {
|
||
disabledGetter = function() { return true; };
|
||
} else if (attr.ngDisabled) {
|
||
disabledGetter = $parse(attr.ngDisabled);
|
||
}
|
||
|
||
var thumbContainer = angular.element(element[0].querySelector('.md-thumb-container'));
|
||
var switchContainer = angular.element(element[0].querySelector('.md-container'));
|
||
var labelContainer = angular.element(element[0].querySelector('.md-label'));
|
||
|
||
// no transition on initial load
|
||
$$rAF(function() {
|
||
element.removeClass('md-dragging');
|
||
});
|
||
|
||
checkboxLink(scope, element, attr, ctrls);
|
||
|
||
if (disabledGetter) {
|
||
scope.$watch(disabledGetter, function(isDisabled) {
|
||
element.attr('tabindex', isDisabled ? -1 : 0);
|
||
});
|
||
}
|
||
|
||
attr.$observe('mdInvert', function(newValue) {
|
||
var isInverted = $mdUtil.parseAttributeBoolean(newValue);
|
||
|
||
isInverted ? element.prepend(labelContainer) : element.prepend(switchContainer);
|
||
|
||
// Toggle a CSS class to update the margin.
|
||
element.toggleClass('md-inverted', isInverted);
|
||
});
|
||
|
||
// These events are triggered by setup drag
|
||
$mdGesture.register(switchContainer, 'drag');
|
||
switchContainer
|
||
.on('$md.dragstart', onDragStart)
|
||
.on('$md.drag', onDrag)
|
||
.on('$md.dragend', onDragEnd);
|
||
|
||
var drag;
|
||
function onDragStart(ev) {
|
||
// Don't go if the switch is disabled.
|
||
if (disabledGetter && disabledGetter(scope)) return;
|
||
ev.stopPropagation();
|
||
|
||
element.addClass('md-dragging');
|
||
drag = {width: thumbContainer.prop('offsetWidth')};
|
||
}
|
||
|
||
function onDrag(ev) {
|
||
if (!drag) return;
|
||
ev.stopPropagation();
|
||
ev.srcEvent && ev.srcEvent.preventDefault();
|
||
|
||
var percent = ev.pointer.distanceX / drag.width;
|
||
|
||
// if checked, start from right. else, start from left
|
||
var translate = ngModel.$viewValue ? 1 + percent : percent;
|
||
// Make sure the switch stays inside its bounds, 0-1%
|
||
translate = Math.max(0, Math.min(1, translate));
|
||
|
||
thumbContainer.css($mdConstant.CSS.TRANSFORM, 'translate3d(' + (100*translate) + '%,0,0)');
|
||
drag.translate = translate;
|
||
}
|
||
|
||
function onDragEnd(ev) {
|
||
if (!drag) return;
|
||
ev.stopPropagation();
|
||
|
||
element.removeClass('md-dragging');
|
||
thumbContainer.css($mdConstant.CSS.TRANSFORM, '');
|
||
|
||
// We changed if there is no distance (this is a click a click),
|
||
// or if the drag distance is >50% of the total.
|
||
var isChanged = ngModel.$viewValue ? drag.translate < 0.5 : drag.translate > 0.5;
|
||
if (isChanged) {
|
||
applyModelValue(!ngModel.$viewValue);
|
||
}
|
||
drag = null;
|
||
|
||
// Wait for incoming mouse click
|
||
scope.skipToggle = true;
|
||
$timeout(function() {
|
||
scope.skipToggle = false;
|
||
}, 1);
|
||
}
|
||
|
||
function applyModelValue(newValue) {
|
||
scope.$apply(function() {
|
||
ngModel.$setViewValue(newValue);
|
||
ngModel.$render();
|
||
});
|
||
}
|
||
|
||
};
|
||
}
|
||
|
||
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.tabs
|
||
* @description
|
||
*
|
||
* Tabs, created with the `<md-tabs>` directive provide *tabbed* navigation with different styles.
|
||
* The Tabs component consists of clickable tabs that are aligned horizontally side-by-side.
|
||
*
|
||
* Features include support for:
|
||
*
|
||
* - static or dynamic tabs,
|
||
* - responsive designs,
|
||
* - accessibility support (ARIA),
|
||
* - tab pagination,
|
||
* - external or internal tab content,
|
||
* - focus indicators and arrow-key navigations,
|
||
* - programmatic lookup and access to tab controllers, and
|
||
* - dynamic transitions through different tab contents.
|
||
*
|
||
*/
|
||
/*
|
||
* @see js folder for tabs implementation
|
||
*/
|
||
angular.module('material.components.tabs', [
|
||
'material.core',
|
||
'material.components.icon'
|
||
]);
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
angular
|
||
.module('material.components.tabs')
|
||
.service('MdTabsPaginationService', MdTabsPaginationService);
|
||
|
||
/**
|
||
* @private
|
||
* @module material.components.tabs
|
||
* @name MdTabsPaginationService
|
||
* @description Provides many standalone functions to ease in pagination calculations.
|
||
*
|
||
* Most functions accept the elements and the current offset.
|
||
*
|
||
* The `elements` parameter is typically the value returned from the `getElements()` function of the
|
||
* tabsController.
|
||
*
|
||
* The `offset` parameter is always positive regardless of LTR or RTL (we simply make the LTR one
|
||
* negative when we apply our transform). This is typically the `ctrl.leftOffset` variable in the
|
||
* tabsController.
|
||
*
|
||
* @returns MdTabsPaginationService
|
||
* @constructor
|
||
*/
|
||
function MdTabsPaginationService() {
|
||
return {
|
||
decreasePageOffset: decreasePageOffset,
|
||
increasePageOffset: increasePageOffset,
|
||
getTabOffsets: getTabOffsets,
|
||
getTotalTabsWidth: getTotalTabsWidth
|
||
};
|
||
|
||
/**
|
||
* Returns the offset for the next decreasing page.
|
||
*
|
||
* @param elements
|
||
* @param currentOffset
|
||
* @returns {number}
|
||
*/
|
||
function decreasePageOffset(elements, currentOffset) {
|
||
var canvas = elements.canvas,
|
||
tabOffsets = getTabOffsets(elements),
|
||
i, firstVisibleTabOffset;
|
||
|
||
// Find the first fully visible tab in offset range
|
||
for (i = 0; i < tabOffsets.length; i++) {
|
||
if (tabOffsets[i] >= currentOffset) {
|
||
firstVisibleTabOffset = tabOffsets[i];
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Return (the first visible tab offset - the tabs container width) without going negative
|
||
return Math.max(0, firstVisibleTabOffset - canvas.clientWidth);
|
||
}
|
||
|
||
/**
|
||
* Returns the offset for the next increasing page.
|
||
*
|
||
* @param elements
|
||
* @param currentOffset
|
||
* @returns {number}
|
||
*/
|
||
function increasePageOffset(elements, currentOffset) {
|
||
var canvas = elements.canvas,
|
||
maxOffset = getTotalTabsWidth(elements) - canvas.clientWidth,
|
||
tabOffsets = getTabOffsets(elements),
|
||
i, firstHiddenTabOffset;
|
||
|
||
// Find the first partially (or fully) invisible tab
|
||
for (i = 0; i < tabOffsets.length, tabOffsets[i] <= currentOffset + canvas.clientWidth; i++) {
|
||
firstHiddenTabOffset = tabOffsets[i];
|
||
}
|
||
|
||
// Return the offset of the first hidden tab, or the maximum offset (whichever is smaller)
|
||
return Math.min(maxOffset, firstHiddenTabOffset);
|
||
}
|
||
|
||
/**
|
||
* Returns the offsets of all of the tabs based on their widths.
|
||
*
|
||
* @param elements
|
||
* @returns {number[]}
|
||
*/
|
||
function getTabOffsets(elements) {
|
||
var i, tab, currentOffset = 0, offsets = [];
|
||
|
||
for (i = 0; i < elements.tabs.length; i++) {
|
||
tab = elements.tabs[i];
|
||
offsets.push(currentOffset);
|
||
currentOffset += tab.offsetWidth;
|
||
}
|
||
|
||
return offsets;
|
||
}
|
||
|
||
/**
|
||
* Sum the width of all tabs.
|
||
*
|
||
* @param elements
|
||
* @returns {number}
|
||
*/
|
||
function getTotalTabsWidth(elements) {
|
||
var sum = 0, i, tab;
|
||
|
||
for (i = 0; i < elements.tabs.length; i++) {
|
||
tab = elements.tabs[i];
|
||
sum += tab.offsetWidth;
|
||
}
|
||
|
||
return sum;
|
||
}
|
||
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdTab
|
||
* @module material.components.tabs
|
||
*
|
||
* @restrict E
|
||
*
|
||
* @description
|
||
* The `<md-tab>` is a nested directive used within `<md-tabs>` to specify a tab with a **label**
|
||
* and optional *view content*.
|
||
*
|
||
* If the `label` attribute is not specified, then an optional `<md-tab-label>` tag can be used to
|
||
* specify more complex tab header markup. If neither the **label** nor the **md-tab-label** are
|
||
* specified, then the nested markup of the `<md-tab>` is used as the tab header markup.
|
||
*
|
||
* Please note that if you use `<md-tab-label>`, your content **MUST** be wrapped in the
|
||
* `<md-tab-body>` tag. This is to define a clear separation between the tab content and the tab
|
||
* label.
|
||
*
|
||
* This container is used by the TabsController to show/hide the active tab's content view. This
|
||
* synchronization is automatically managed by the internal TabsController whenever the tab
|
||
* selection changes. Selection changes can be initiated via data binding changes, programmatic
|
||
* invocation, or user gestures.
|
||
*
|
||
* @param {string=} label Optional attribute to specify a simple string as the tab label
|
||
* @param {boolean=} ng-disabled If present and expression evaluates to truthy, disabled tab
|
||
* selection.
|
||
* @param {string=} md-tab-class Optional attribute to specify a class that will be applied to the
|
||
* tab's button
|
||
* @param {expression=} md-on-deselect Expression to be evaluated after the tab has been
|
||
* de-selected.
|
||
* @param {expression=} md-on-select Expression to be evaluated after the tab has been selected.
|
||
* @param {boolean=} md-active When true, sets the active tab. Note: There can only be one active
|
||
* tab at a time.
|
||
*
|
||
*
|
||
* @usage
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-tab label="My Tab" md-tab-class="my-content-tab" ng-disabled md-on-select="onSelect()"
|
||
* md-on-deselect="onDeselect()">
|
||
* <h3>My Tab content</h3>
|
||
* </md-tab>
|
||
*
|
||
* <md-tab>
|
||
* <md-tab-label>
|
||
* <h3>My Tab</h3>
|
||
* </md-tab-label>
|
||
* <md-tab-body>
|
||
* <p>
|
||
* Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque
|
||
* laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi
|
||
* architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit
|
||
* aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione
|
||
* voluptatem sequi nesciunt.
|
||
* </p>
|
||
* </md-tab-body>
|
||
* </md-tab>
|
||
* </hljs>
|
||
*
|
||
*/
|
||
angular
|
||
.module('material.components.tabs')
|
||
.directive('mdTab', MdTab);
|
||
|
||
function MdTab () {
|
||
return {
|
||
require: '^?mdTabs',
|
||
terminal: true,
|
||
compile: function (element, attr) {
|
||
var label = firstChild(element, 'md-tab-label'),
|
||
body = firstChild(element, 'md-tab-body');
|
||
|
||
if (label.length === 0) {
|
||
label = angular.element('<md-tab-label></md-tab-label>');
|
||
if (attr.label) label.text(attr.label);
|
||
else label.append(element.contents());
|
||
|
||
if (body.length === 0) {
|
||
var contents = element.contents().detach();
|
||
body = angular.element('<md-tab-body></md-tab-body>');
|
||
body.append(contents);
|
||
}
|
||
}
|
||
|
||
element.append(label);
|
||
if (body.html()) element.append(body);
|
||
|
||
return postLink;
|
||
},
|
||
scope: {
|
||
active: '=?mdActive',
|
||
disabled: '=?ngDisabled',
|
||
select: '&?mdOnSelect',
|
||
deselect: '&?mdOnDeselect',
|
||
tabClass: '@mdTabClass'
|
||
}
|
||
};
|
||
|
||
function postLink (scope, element, attr, ctrl) {
|
||
if (!ctrl) return;
|
||
var index = ctrl.getTabElementIndex(element),
|
||
body = firstChild(element, 'md-tab-body').remove(),
|
||
label = firstChild(element, 'md-tab-label').remove(),
|
||
data = ctrl.insertTab({
|
||
scope: scope,
|
||
parent: scope.$parent,
|
||
index: index,
|
||
element: element,
|
||
template: body.html(),
|
||
label: label.html()
|
||
}, index);
|
||
|
||
scope.select = scope.select || angular.noop;
|
||
scope.deselect = scope.deselect || angular.noop;
|
||
|
||
scope.$watch('active', function (active) { if (active) ctrl.select(data.getIndex(), true); });
|
||
scope.$watch('disabled', function () { ctrl.refreshIndex(); });
|
||
scope.$watch(
|
||
function () {
|
||
return ctrl.getTabElementIndex(element);
|
||
},
|
||
function (newIndex) {
|
||
data.index = newIndex;
|
||
ctrl.updateTabOrder();
|
||
}
|
||
);
|
||
scope.$on('$destroy', function () { ctrl.removeTab(data); });
|
||
}
|
||
|
||
function firstChild (element, tagName) {
|
||
var children = element[0].children;
|
||
for (var i = 0, len = children.length; i < len; i++) {
|
||
var child = children[i];
|
||
if (child.tagName === tagName.toUpperCase()) return angular.element(child);
|
||
}
|
||
return angular.element();
|
||
}
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
angular
|
||
.module('material.components.tabs')
|
||
.directive('mdTabItem', MdTabItem);
|
||
|
||
function MdTabItem () {
|
||
return {
|
||
require: '^?mdTabs',
|
||
link: function link (scope, element, attr, ctrl) {
|
||
if (!ctrl) return;
|
||
ctrl.attachRipple(scope, element);
|
||
}
|
||
};
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
angular
|
||
.module('material.components.tabs')
|
||
.directive('mdTabLabel', MdTabLabel);
|
||
|
||
function MdTabLabel () {
|
||
return { terminal: true };
|
||
}
|
||
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
|
||
MdTabScroll.$inject = ["$parse"];angular.module('material.components.tabs')
|
||
.directive('mdTabScroll', MdTabScroll);
|
||
|
||
function MdTabScroll ($parse) {
|
||
return {
|
||
restrict: 'A',
|
||
compile: function ($element, attr) {
|
||
var fn = $parse(attr.mdTabScroll, null, true);
|
||
return function ngEventHandler (scope, element) {
|
||
element.on('wheel', function (event) {
|
||
scope.$apply(function () { fn(scope, { $event: event }); });
|
||
});
|
||
};
|
||
}
|
||
};
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
|
||
MdTabsController.$inject = ["$scope", "$element", "$window", "$mdConstant", "$mdTabInkRipple", "$mdUtil", "$animateCss", "$attrs", "$compile", "$mdTheming", "$mdInteraction", "$timeout", "MdTabsPaginationService"];angular
|
||
.module('material.components.tabs')
|
||
.controller('MdTabsController', MdTabsController);
|
||
|
||
/**
|
||
* @ngInject
|
||
*/
|
||
function MdTabsController ($scope, $element, $window, $mdConstant, $mdTabInkRipple, $mdUtil,
|
||
$animateCss, $attrs, $compile, $mdTheming, $mdInteraction, $timeout,
|
||
MdTabsPaginationService) {
|
||
// define private properties
|
||
var ctrl = this,
|
||
locked = false,
|
||
queue = [],
|
||
destroyed = false,
|
||
loaded = false;
|
||
|
||
// Define public methods
|
||
ctrl.$onInit = $onInit;
|
||
ctrl.updatePagination = $mdUtil.debounce(updatePagination, 100);
|
||
ctrl.redirectFocus = redirectFocus;
|
||
ctrl.attachRipple = attachRipple;
|
||
ctrl.insertTab = insertTab;
|
||
ctrl.removeTab = removeTab;
|
||
ctrl.select = select;
|
||
ctrl.scroll = scroll;
|
||
ctrl.nextPage = nextPage;
|
||
ctrl.previousPage = previousPage;
|
||
ctrl.keydown = keydown;
|
||
ctrl.canPageForward = canPageForward;
|
||
ctrl.canPageBack = canPageBack;
|
||
ctrl.refreshIndex = refreshIndex;
|
||
ctrl.incrementIndex = incrementIndex;
|
||
ctrl.getTabElementIndex = getTabElementIndex;
|
||
ctrl.updateInkBarStyles = $mdUtil.debounce(updateInkBarStyles, 100);
|
||
ctrl.updateTabOrder = $mdUtil.debounce(updateTabOrder, 100);
|
||
ctrl.getFocusedTabId = getFocusedTabId;
|
||
|
||
// For AngularJS 1.4 and older, where there are no lifecycle hooks but bindings are pre-assigned,
|
||
// manually call the $onInit hook.
|
||
if (angular.version.major === 1 && angular.version.minor <= 4) {
|
||
this.$onInit();
|
||
}
|
||
|
||
/**
|
||
* AngularJS Lifecycle hook for newer AngularJS versions.
|
||
* Bindings are not guaranteed to have been assigned in the controller, but they are in the
|
||
* $onInit hook.
|
||
*/
|
||
function $onInit() {
|
||
// Define one-way bindings
|
||
defineOneWayBinding('stretchTabs', handleStretchTabs);
|
||
|
||
// Define public properties with change handlers
|
||
defineProperty('focusIndex', handleFocusIndexChange, ctrl.selectedIndex || 0);
|
||
defineProperty('offsetLeft', handleOffsetChange, 0);
|
||
defineProperty('hasContent', handleHasContent, false);
|
||
defineProperty('maxTabWidth', handleMaxTabWidth, getMaxTabWidth());
|
||
defineProperty('shouldPaginate', handleShouldPaginate, false);
|
||
|
||
// Define boolean attributes
|
||
defineBooleanAttribute('noInkBar', handleInkBar);
|
||
defineBooleanAttribute('dynamicHeight', handleDynamicHeight);
|
||
defineBooleanAttribute('noPagination');
|
||
defineBooleanAttribute('swipeContent');
|
||
defineBooleanAttribute('autoselect');
|
||
defineBooleanAttribute('noSelectClick');
|
||
defineBooleanAttribute('centerTabs', handleCenterTabs);
|
||
defineBooleanAttribute('enableDisconnect');
|
||
|
||
// Define public properties
|
||
ctrl.scope = $scope;
|
||
ctrl.parent = $scope.$parent;
|
||
ctrl.tabs = [];
|
||
ctrl.lastSelectedIndex = null;
|
||
ctrl.hasFocus = false;
|
||
ctrl.styleTabItemFocus = false;
|
||
ctrl.shouldCenterTabs = shouldCenterTabs();
|
||
ctrl.tabContentPrefix = 'tab-content-';
|
||
ctrl.navigationHint = 'Use the left and right arrow keys to navigate between tabs';
|
||
|
||
// Setup the tabs controller after all bindings are available.
|
||
setupTabsController();
|
||
}
|
||
|
||
/**
|
||
* Perform setup for the controller, setup events and watcher(s)
|
||
*/
|
||
function setupTabsController () {
|
||
ctrl.selectedIndex = ctrl.selectedIndex || 0;
|
||
compileTemplate();
|
||
configureWatchers();
|
||
bindEvents();
|
||
$mdTheming($element);
|
||
$mdUtil.nextTick(function () {
|
||
updateHeightFromContent();
|
||
adjustOffset();
|
||
updateInkBarStyles();
|
||
ctrl.tabs[ ctrl.selectedIndex ] && ctrl.tabs[ ctrl.selectedIndex ].scope.select();
|
||
loaded = true;
|
||
updatePagination();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Compiles the template provided by the user. This is passed as an attribute from the tabs
|
||
* directive's template function.
|
||
*/
|
||
function compileTemplate () {
|
||
var template = $attrs.$mdTabsTemplate,
|
||
element = angular.element($element[0].querySelector('md-tab-data'));
|
||
|
||
element.html(template);
|
||
$compile(element.contents())(ctrl.parent);
|
||
delete $attrs.$mdTabsTemplate;
|
||
}
|
||
|
||
/**
|
||
* Binds events used by the tabs component.
|
||
*/
|
||
function bindEvents () {
|
||
angular.element($window).on('resize', handleWindowResize);
|
||
$scope.$on('$destroy', cleanup);
|
||
}
|
||
|
||
/**
|
||
* Configure watcher(s) used by Tabs
|
||
*/
|
||
function configureWatchers () {
|
||
$scope.$watch('$mdTabsCtrl.selectedIndex', handleSelectedIndexChange);
|
||
}
|
||
|
||
/**
|
||
* Creates a one-way binding manually rather than relying on AngularJS's isolated scope
|
||
* @param key
|
||
* @param handler
|
||
*/
|
||
function defineOneWayBinding (key, handler) {
|
||
var attr = $attrs.$normalize('md-' + key);
|
||
if (handler) defineProperty(key, handler);
|
||
$attrs.$observe(attr, function (newValue) { ctrl[ key ] = newValue; });
|
||
}
|
||
|
||
/**
|
||
* Defines boolean attributes with default value set to true. I.e. md-stretch-tabs with no value
|
||
* will be treated as being truthy.
|
||
* @param {string} key
|
||
* @param {Function=} handler
|
||
*/
|
||
function defineBooleanAttribute (key, handler) {
|
||
var attr = $attrs.$normalize('md-' + key);
|
||
if (handler) defineProperty(key, handler, undefined);
|
||
if ($attrs.hasOwnProperty(attr)) updateValue($attrs[attr]);
|
||
$attrs.$observe(attr, updateValue);
|
||
function updateValue (newValue) {
|
||
ctrl[ key ] = newValue !== 'false';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Remove any events defined by this controller
|
||
*/
|
||
function cleanup () {
|
||
destroyed = true;
|
||
angular.element($window).off('resize', handleWindowResize);
|
||
}
|
||
|
||
// Change handlers
|
||
|
||
/**
|
||
* Toggles stretch tabs class and updates inkbar when tab stretching changes.
|
||
*/
|
||
function handleStretchTabs () {
|
||
var elements = getElements();
|
||
angular.element(elements.wrapper).toggleClass('md-stretch-tabs', shouldStretchTabs());
|
||
updateInkBarStyles();
|
||
}
|
||
|
||
/**
|
||
* Update the value of ctrl.shouldCenterTabs.
|
||
*/
|
||
function handleCenterTabs () {
|
||
ctrl.shouldCenterTabs = shouldCenterTabs();
|
||
}
|
||
|
||
/**
|
||
* @param {number} newWidth new max tab width in pixels
|
||
* @param {number} oldWidth previous max tab width in pixels
|
||
*/
|
||
function handleMaxTabWidth (newWidth, oldWidth) {
|
||
if (newWidth !== oldWidth) {
|
||
var elements = getElements();
|
||
|
||
// Set the max width for the real tabs
|
||
angular.forEach(elements.tabs, function(tab) {
|
||
tab.style.maxWidth = newWidth + 'px';
|
||
});
|
||
|
||
// Set the max width for the dummy tabs too
|
||
angular.forEach(elements.dummies, function(tab) {
|
||
tab.style.maxWidth = newWidth + 'px';
|
||
});
|
||
|
||
$mdUtil.nextTick(ctrl.updateInkBarStyles);
|
||
}
|
||
}
|
||
|
||
function handleShouldPaginate (newValue, oldValue) {
|
||
if (newValue !== oldValue) {
|
||
ctrl.maxTabWidth = getMaxTabWidth();
|
||
ctrl.shouldCenterTabs = shouldCenterTabs();
|
||
$mdUtil.nextTick(function () {
|
||
ctrl.maxTabWidth = getMaxTabWidth();
|
||
adjustOffset(ctrl.selectedIndex);
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Add/remove the `md-no-tab-content` class depending on `ctrl.hasContent`
|
||
* @param {boolean} hasContent
|
||
*/
|
||
function handleHasContent (hasContent) {
|
||
$element[ hasContent ? 'removeClass' : 'addClass' ]('md-no-tab-content');
|
||
}
|
||
|
||
/**
|
||
* Apply ctrl.offsetLeft to the paging element when it changes
|
||
* @param {string|number} left
|
||
*/
|
||
function handleOffsetChange (left) {
|
||
var newValue = ((ctrl.shouldCenterTabs || isRtl() ? '' : '-') + left + 'px');
|
||
|
||
// Fix double-negative which can happen with RTL support
|
||
newValue = newValue.replace('--', '');
|
||
|
||
angular.element(getElements().paging).css($mdConstant.CSS.TRANSFORM,
|
||
'translate(' + newValue + ', 0)');
|
||
$scope.$broadcast('$mdTabsPaginationChanged');
|
||
}
|
||
|
||
/**
|
||
* Update the UI whenever `ctrl.focusIndex` is updated
|
||
* @param {number} newIndex
|
||
* @param {number} oldIndex
|
||
*/
|
||
function handleFocusIndexChange (newIndex, oldIndex) {
|
||
if (newIndex === oldIndex) return;
|
||
if (!getElements().tabs[ newIndex ]) return;
|
||
adjustOffset();
|
||
redirectFocus();
|
||
}
|
||
|
||
/**
|
||
* Update the UI whenever the selected index changes. Calls user-defined select/deselect methods.
|
||
* @param {number} newValue selected index's new value
|
||
* @param {number} oldValue selected index's previous value
|
||
*/
|
||
function handleSelectedIndexChange (newValue, oldValue) {
|
||
if (newValue === oldValue) return;
|
||
|
||
ctrl.selectedIndex = getNearestSafeIndex(newValue);
|
||
ctrl.lastSelectedIndex = oldValue;
|
||
ctrl.updateInkBarStyles();
|
||
updateHeightFromContent();
|
||
adjustOffset(newValue);
|
||
$scope.$broadcast('$mdTabsChanged');
|
||
ctrl.tabs[ oldValue ] && ctrl.tabs[ oldValue ].scope.deselect();
|
||
ctrl.tabs[ newValue ] && ctrl.tabs[ newValue ].scope.select();
|
||
}
|
||
|
||
function getTabElementIndex(tabEl){
|
||
var tabs = $element[0].getElementsByTagName('md-tab');
|
||
return Array.prototype.indexOf.call(tabs, tabEl[0]);
|
||
}
|
||
|
||
/**
|
||
* Queues up a call to `handleWindowResize` when a resize occurs while the tabs component is
|
||
* hidden.
|
||
*/
|
||
function handleResizeWhenVisible () {
|
||
// if there is already a watcher waiting for resize, do nothing
|
||
if (handleResizeWhenVisible.watcher) return;
|
||
// otherwise, we will abuse the $watch function to check for visible
|
||
handleResizeWhenVisible.watcher = $scope.$watch(function () {
|
||
// since we are checking for DOM size, we use $mdUtil.nextTick() to wait for after the DOM updates
|
||
$mdUtil.nextTick(function () {
|
||
// if the watcher has already run (ie. multiple digests in one cycle), do nothing
|
||
if (!handleResizeWhenVisible.watcher) return;
|
||
|
||
if ($element.prop('offsetParent')) {
|
||
handleResizeWhenVisible.watcher();
|
||
handleResizeWhenVisible.watcher = null;
|
||
|
||
handleWindowResize();
|
||
}
|
||
}, false);
|
||
});
|
||
}
|
||
|
||
// Event handlers / actions
|
||
|
||
/**
|
||
* Handle user keyboard interactions
|
||
* @param {KeyboardEvent} event keydown event
|
||
*/
|
||
function keydown (event) {
|
||
switch (event.keyCode) {
|
||
case $mdConstant.KEY_CODE.LEFT_ARROW:
|
||
event.preventDefault();
|
||
incrementIndex(-1, true);
|
||
break;
|
||
case $mdConstant.KEY_CODE.RIGHT_ARROW:
|
||
event.preventDefault();
|
||
incrementIndex(1, true);
|
||
break;
|
||
case $mdConstant.KEY_CODE.SPACE:
|
||
case $mdConstant.KEY_CODE.ENTER:
|
||
event.preventDefault();
|
||
if (!locked) select(ctrl.focusIndex);
|
||
break;
|
||
case $mdConstant.KEY_CODE.TAB:
|
||
// On tabbing out of the tablist, reset hasFocus to reset ng-focused and
|
||
// its md-focused class if the focused tab is not the active tab.
|
||
if (ctrl.focusIndex !== ctrl.selectedIndex) {
|
||
ctrl.focusIndex = ctrl.selectedIndex;
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Update the selected index. Triggers a click event on the original `md-tab` element in order
|
||
* to fire user-added click events if canSkipClick or `md-no-select-click` are false.
|
||
* @param index
|
||
* @param canSkipClick Optionally allow not firing the click event if `md-no-select-click` is also true.
|
||
*/
|
||
function select (index, canSkipClick) {
|
||
if (!locked) ctrl.focusIndex = ctrl.selectedIndex = index;
|
||
// skip the click event if noSelectClick is enabled
|
||
if (canSkipClick && ctrl.noSelectClick) return;
|
||
// nextTick is required to prevent errors in user-defined click events
|
||
$mdUtil.nextTick(function () {
|
||
ctrl.tabs[ index ].element.triggerHandler('click');
|
||
}, false);
|
||
}
|
||
|
||
/**
|
||
* When pagination is on, this makes sure the selected index is in view.
|
||
* @param {WheelEvent} event
|
||
*/
|
||
function scroll (event) {
|
||
if (!ctrl.shouldPaginate) return;
|
||
event.preventDefault();
|
||
if (event.deltaY) {
|
||
ctrl.offsetLeft = fixOffset(ctrl.offsetLeft + event.deltaY);
|
||
} else if (event.deltaX) {
|
||
ctrl.offsetLeft = fixOffset(ctrl.offsetLeft + event.deltaX);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Slides the tabs over approximately one page forward.
|
||
*/
|
||
function nextPage () {
|
||
if (!ctrl.canPageForward()) { return; }
|
||
|
||
var newOffset = MdTabsPaginationService.increasePageOffset(getElements(), ctrl.offsetLeft);
|
||
|
||
ctrl.offsetLeft = fixOffset(newOffset);
|
||
}
|
||
|
||
/**
|
||
* Slides the tabs over approximately one page backward.
|
||
*/
|
||
function previousPage () {
|
||
if (!ctrl.canPageBack()) { return; }
|
||
|
||
var newOffset = MdTabsPaginationService.decreasePageOffset(getElements(), ctrl.offsetLeft);
|
||
|
||
// Set the new offset
|
||
ctrl.offsetLeft = fixOffset(newOffset);
|
||
}
|
||
|
||
/**
|
||
* Update size calculations when the window is resized.
|
||
*/
|
||
function handleWindowResize () {
|
||
ctrl.lastSelectedIndex = ctrl.selectedIndex;
|
||
ctrl.offsetLeft = fixOffset(ctrl.offsetLeft);
|
||
|
||
$mdUtil.nextTick(function () {
|
||
ctrl.updateInkBarStyles();
|
||
updatePagination();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Hides or shows the tabs ink bar.
|
||
* @param {boolean} hide A Boolean (not just truthy/falsy) value to determine whether the class
|
||
* should be added or removed.
|
||
*/
|
||
function handleInkBar (hide) {
|
||
angular.element(getElements().inkBar).toggleClass('ng-hide', hide);
|
||
}
|
||
|
||
/**
|
||
* Enables or disables tabs dynamic height.
|
||
* @param {boolean} value A Boolean (not just truthy/falsy) value to determine whether the class
|
||
* should be added or removed.
|
||
*/
|
||
function handleDynamicHeight (value) {
|
||
$element.toggleClass('md-dynamic-height', value);
|
||
}
|
||
|
||
/**
|
||
* Remove a tab from the data and select the nearest valid tab.
|
||
* @param {Object} tabData tab to remove
|
||
*/
|
||
function removeTab (tabData) {
|
||
if (destroyed) return;
|
||
var selectedIndex = ctrl.selectedIndex,
|
||
tab = ctrl.tabs.splice(tabData.getIndex(), 1)[ 0 ];
|
||
refreshIndex();
|
||
// when removing a tab, if the selected index did not change, we have to manually trigger the
|
||
// tab select/deselect events
|
||
if (ctrl.selectedIndex === selectedIndex) {
|
||
tab.scope.deselect();
|
||
ctrl.tabs[ ctrl.selectedIndex ] && ctrl.tabs[ ctrl.selectedIndex ].scope.select();
|
||
}
|
||
$mdUtil.nextTick(function () {
|
||
updatePagination();
|
||
ctrl.offsetLeft = fixOffset(ctrl.offsetLeft);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Create an entry in the tabs array for a new tab at the specified index.
|
||
* @param {Object} tabData tab to insert
|
||
* @param {number} index location to insert the new tab
|
||
* @returns {Object} the inserted tab
|
||
*/
|
||
function insertTab (tabData, index) {
|
||
var hasLoaded = loaded;
|
||
var proto = {
|
||
getIndex: function () { return ctrl.tabs.indexOf(tab); },
|
||
isActive: function () { return this.getIndex() === ctrl.selectedIndex; },
|
||
isLeft: function () { return this.getIndex() < ctrl.selectedIndex; },
|
||
isRight: function () { return this.getIndex() > ctrl.selectedIndex; },
|
||
shouldRender: function () { return ctrl.dynamicHeight || this.isActive(); },
|
||
hasFocus: function () {
|
||
return ctrl.styleTabItemFocus
|
||
&& ctrl.hasFocus && this.getIndex() === ctrl.focusIndex;
|
||
},
|
||
id: $mdUtil.nextUid(),
|
||
hasContent: !!(tabData.template && tabData.template.trim())
|
||
};
|
||
var tab = angular.extend(proto, tabData);
|
||
|
||
if (angular.isDefined(index)) {
|
||
ctrl.tabs.splice(index, 0, tab);
|
||
} else {
|
||
ctrl.tabs.push(tab);
|
||
}
|
||
processQueue();
|
||
updateHasContent();
|
||
|
||
$mdUtil.nextTick(function () {
|
||
updatePagination();
|
||
setAriaControls(tab);
|
||
|
||
// if autoselect is enabled, select the newly added tab
|
||
if (hasLoaded && ctrl.autoselect) {
|
||
$mdUtil.nextTick(function () {
|
||
$mdUtil.nextTick(function () { select(ctrl.tabs.indexOf(tab)); });
|
||
});
|
||
}
|
||
});
|
||
return tab;
|
||
}
|
||
|
||
// Getter methods
|
||
|
||
/**
|
||
* Gathers references to all of the DOM elements used by this controller.
|
||
* @returns {Object}
|
||
*/
|
||
function getElements () {
|
||
var elements = {};
|
||
var node = $element[0];
|
||
|
||
// gather tab bar elements
|
||
elements.wrapper = node.querySelector('md-tabs-wrapper');
|
||
elements.canvas = elements.wrapper.querySelector('md-tabs-canvas');
|
||
elements.paging = elements.canvas.querySelector('md-pagination-wrapper');
|
||
elements.inkBar = elements.paging.querySelector('md-ink-bar');
|
||
elements.nextButton = node.querySelector('md-next-button');
|
||
elements.prevButton = node.querySelector('md-prev-button');
|
||
|
||
elements.contents = node.querySelectorAll('md-tabs-content-wrapper > md-tab-content');
|
||
elements.tabs = elements.paging.querySelectorAll('md-tab-item');
|
||
elements.dummies = elements.canvas.querySelectorAll('md-dummy-tab');
|
||
|
||
return elements;
|
||
}
|
||
|
||
/**
|
||
* Determines whether or not the left pagination arrow should be enabled.
|
||
* @returns {boolean}
|
||
*/
|
||
function canPageBack () {
|
||
// This works for both LTR and RTL
|
||
return ctrl.offsetLeft > 0;
|
||
}
|
||
|
||
/**
|
||
* Determines whether or not the right pagination arrow should be enabled.
|
||
* @returns {*|boolean}
|
||
*/
|
||
function canPageForward () {
|
||
var elements = getElements();
|
||
var lastTab = elements.tabs[ elements.tabs.length - 1 ];
|
||
|
||
if (isRtl()) {
|
||
return ctrl.offsetLeft < elements.paging.offsetWidth - elements.canvas.offsetWidth;
|
||
}
|
||
|
||
return lastTab && lastTab.offsetLeft + lastTab.offsetWidth > elements.canvas.clientWidth +
|
||
ctrl.offsetLeft;
|
||
}
|
||
|
||
/**
|
||
* Returns currently focused tab item's element ID
|
||
*/
|
||
function getFocusedTabId() {
|
||
var focusedTab = ctrl.tabs[ctrl.focusIndex];
|
||
if (!focusedTab || !focusedTab.id) {
|
||
return null;
|
||
}
|
||
return 'tab-item-' + focusedTab.id;
|
||
}
|
||
|
||
/**
|
||
* Determines if the UI should stretch the tabs to fill the available space.
|
||
* @returns {*}
|
||
*/
|
||
function shouldStretchTabs () {
|
||
switch (ctrl.stretchTabs) {
|
||
case 'always':
|
||
return true;
|
||
case 'never':
|
||
return false;
|
||
default:
|
||
return !ctrl.shouldPaginate
|
||
&& $window.matchMedia('(max-width: 600px)').matches;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Determines if the tabs should appear centered.
|
||
* @returns {boolean}
|
||
*/
|
||
function shouldCenterTabs () {
|
||
return ctrl.centerTabs && !ctrl.shouldPaginate;
|
||
}
|
||
|
||
/**
|
||
* Determines if pagination is necessary to display the tabs within the available space.
|
||
* @returns {boolean} true if pagination is necessary, false otherwise
|
||
*/
|
||
function shouldPaginate () {
|
||
var shouldPaginate;
|
||
if (ctrl.noPagination || !loaded) return false;
|
||
var canvasWidth = $element.prop('clientWidth');
|
||
|
||
angular.forEach(getElements().tabs, function (tab) {
|
||
canvasWidth -= tab.offsetWidth;
|
||
});
|
||
|
||
shouldPaginate = canvasWidth < 0;
|
||
// Work around width calculation issues on IE11 when pagination is enabled.
|
||
// Don't do this on other browsers because it breaks scroll to new tab animation.
|
||
if ($mdUtil.msie) {
|
||
if (shouldPaginate) {
|
||
getElements().paging.style.width = '999999px';
|
||
} else {
|
||
getElements().paging.style.width = undefined;
|
||
}
|
||
}
|
||
return shouldPaginate;
|
||
}
|
||
|
||
/**
|
||
* Finds the nearest tab index that is available. This is primarily used for when the active
|
||
* tab is removed.
|
||
* @param newIndex
|
||
* @returns {*}
|
||
*/
|
||
function getNearestSafeIndex (newIndex) {
|
||
if (newIndex === -1) return -1;
|
||
var maxOffset = Math.max(ctrl.tabs.length - newIndex, newIndex),
|
||
i, tab;
|
||
for (i = 0; i <= maxOffset; i++) {
|
||
tab = ctrl.tabs[ newIndex + i ];
|
||
if (tab && (tab.scope.disabled !== true)) return tab.getIndex();
|
||
tab = ctrl.tabs[ newIndex - i ];
|
||
if (tab && (tab.scope.disabled !== true)) return tab.getIndex();
|
||
}
|
||
return newIndex;
|
||
}
|
||
|
||
// Utility methods
|
||
|
||
/**
|
||
* Defines a property using a getter and setter in order to trigger a change handler without
|
||
* using `$watch` to observe changes.
|
||
* @param {PropertyKey} key
|
||
* @param {Function} handler
|
||
* @param {any} value
|
||
*/
|
||
function defineProperty (key, handler, value) {
|
||
Object.defineProperty(ctrl, key, {
|
||
get: function () { return value; },
|
||
set: function (newValue) {
|
||
var oldValue = value;
|
||
value = newValue;
|
||
handler && handler(newValue, oldValue);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Updates whether or not pagination should be displayed.
|
||
*/
|
||
function updatePagination () {
|
||
ctrl.maxTabWidth = getMaxTabWidth();
|
||
ctrl.shouldPaginate = shouldPaginate();
|
||
}
|
||
|
||
/**
|
||
* @param {Array<HTMLElement>} tabs tab item elements for use in computing total width
|
||
* @returns {number} the width of the tabs in the specified array in pixels
|
||
*/
|
||
function calcTabsWidth(tabs) {
|
||
var width = 0;
|
||
|
||
angular.forEach(tabs, function (tab) {
|
||
// Uses the larger value between `getBoundingClientRect().width` and `offsetWidth`. This
|
||
// prevents `offsetWidth` value from being rounded down and causing wrapping issues, but
|
||
// also handles scenarios where `getBoundingClientRect()` is inaccurate (ie. tabs inside
|
||
// of a dialog).
|
||
width += Math.max(tab.offsetWidth, tab.getBoundingClientRect().width);
|
||
});
|
||
|
||
return Math.ceil(width);
|
||
}
|
||
|
||
/**
|
||
* @returns {number} either the max width as constrained by the container or the max width from
|
||
* the 2017 version of the Material Design spec.
|
||
*/
|
||
function getMaxTabWidth() {
|
||
var elements = getElements(),
|
||
containerWidth = elements.canvas.clientWidth,
|
||
|
||
// See https://material.io/archive/guidelines/components/tabs.html#tabs-specs
|
||
specMax = 264;
|
||
|
||
// Do the spec maximum, or the canvas width; whichever is *smaller* (tabs larger than the canvas
|
||
// width can break the pagination) but not less than 0
|
||
return Math.max(0, Math.min(containerWidth - 1, specMax));
|
||
}
|
||
|
||
/**
|
||
* Re-orders the tabs and updates the selected and focus indexes to their new positions.
|
||
* This is triggered by `tabDirective.js` when the user's tabs have been re-ordered.
|
||
*/
|
||
function updateTabOrder () {
|
||
var selectedItem = ctrl.tabs[ ctrl.selectedIndex ],
|
||
focusItem = ctrl.tabs[ ctrl.focusIndex ];
|
||
ctrl.tabs = ctrl.tabs.sort(function (a, b) {
|
||
return a.index - b.index;
|
||
});
|
||
ctrl.selectedIndex = ctrl.tabs.indexOf(selectedItem);
|
||
ctrl.focusIndex = ctrl.tabs.indexOf(focusItem);
|
||
}
|
||
|
||
/**
|
||
* This moves the selected or focus index left or right. This is used by the keydown handler.
|
||
* @param {number} inc amount to increment
|
||
* @param {boolean} focus true to increment the focus index, false to increment the selected index
|
||
*/
|
||
function incrementIndex (inc, focus) {
|
||
var newIndex,
|
||
key = focus ? 'focusIndex' : 'selectedIndex',
|
||
index = ctrl[ key ];
|
||
for (newIndex = index + inc;
|
||
ctrl.tabs[ newIndex ] && ctrl.tabs[ newIndex ].scope.disabled;
|
||
newIndex += inc) { /* do nothing */ }
|
||
|
||
newIndex = (index + inc + ctrl.tabs.length) % ctrl.tabs.length;
|
||
|
||
if (ctrl.tabs[ newIndex ]) {
|
||
ctrl[ key ] = newIndex;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* This is used to forward focus to tab container elements. This method is necessary to avoid
|
||
* animation issues when attempting to focus an item that is out of view.
|
||
*/
|
||
function redirectFocus () {
|
||
ctrl.styleTabItemFocus = ($mdInteraction.getLastInteractionType() === 'keyboard');
|
||
var tabToFocus = getElements().tabs[ctrl.focusIndex];
|
||
if (tabToFocus) {
|
||
tabToFocus.focus();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Forces the pagination to move the focused tab into view.
|
||
* @param {number=} index of tab to have its offset adjusted
|
||
*/
|
||
function adjustOffset (index) {
|
||
var elements = getElements();
|
||
|
||
if (!angular.isNumber(index)) index = ctrl.focusIndex;
|
||
if (!elements.tabs[ index ]) return;
|
||
if (ctrl.shouldCenterTabs) return;
|
||
var tab = elements.tabs[ index ],
|
||
left = tab.offsetLeft,
|
||
right = tab.offsetWidth + left,
|
||
extraOffset = 32;
|
||
|
||
// If we are selecting the first tab (in LTR and RTL), always set the offset to 0
|
||
if (index === 0) {
|
||
ctrl.offsetLeft = 0;
|
||
return;
|
||
}
|
||
|
||
if (isRtl()) {
|
||
var tabWidthsBefore = calcTabsWidth(Array.prototype.slice.call(elements.tabs, 0, index));
|
||
var tabWidthsIncluding = calcTabsWidth(Array.prototype.slice.call(elements.tabs, 0, index + 1));
|
||
|
||
ctrl.offsetLeft = Math.min(ctrl.offsetLeft, fixOffset(tabWidthsBefore));
|
||
ctrl.offsetLeft = Math.max(ctrl.offsetLeft, fixOffset(tabWidthsIncluding - elements.canvas.clientWidth));
|
||
} else {
|
||
ctrl.offsetLeft = Math.max(ctrl.offsetLeft, fixOffset(right - elements.canvas.clientWidth + extraOffset));
|
||
ctrl.offsetLeft = Math.min(ctrl.offsetLeft, fixOffset(left));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Iterates through all queued functions and clears the queue. This is used for functions that
|
||
* are called before the UI is ready, such as size calculations.
|
||
*/
|
||
function processQueue () {
|
||
queue.forEach(function (func) { $mdUtil.nextTick(func); });
|
||
queue = [];
|
||
}
|
||
|
||
/**
|
||
* Determines if the tab content area is needed.
|
||
*/
|
||
function updateHasContent () {
|
||
var hasContent = false;
|
||
var i;
|
||
|
||
for (i = 0; i < ctrl.tabs.length; i++) {
|
||
if (ctrl.tabs[i].hasContent) {
|
||
hasContent = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
ctrl.hasContent = hasContent;
|
||
}
|
||
|
||
/**
|
||
* Moves the indexes to their nearest valid values.
|
||
*/
|
||
function refreshIndex () {
|
||
ctrl.selectedIndex = getNearestSafeIndex(ctrl.selectedIndex);
|
||
ctrl.focusIndex = getNearestSafeIndex(ctrl.focusIndex);
|
||
}
|
||
|
||
/**
|
||
* Calculates the content height of the current tab.
|
||
* @returns {*}
|
||
*/
|
||
function updateHeightFromContent () {
|
||
if (!ctrl.dynamicHeight) return $element.css('height', '');
|
||
if (!ctrl.tabs.length) return queue.push(updateHeightFromContent);
|
||
|
||
var elements = getElements();
|
||
|
||
var tabContent = elements.contents[ ctrl.selectedIndex ],
|
||
contentHeight = tabContent ? tabContent.offsetHeight : 0,
|
||
tabsHeight = elements.wrapper.offsetHeight,
|
||
newHeight = contentHeight + tabsHeight,
|
||
currentHeight = $element.prop('clientHeight');
|
||
|
||
if (currentHeight === newHeight) return;
|
||
|
||
// Adjusts calculations for when the buttons are bottom-aligned since this relies on absolute
|
||
// positioning. This should probably be cleaned up if a cleaner solution is possible.
|
||
if ($element.attr('md-align-tabs') === 'bottom') {
|
||
currentHeight -= tabsHeight;
|
||
newHeight -= tabsHeight;
|
||
// Need to include bottom border in these calculations
|
||
if ($element.attr('md-border-bottom') !== undefined) {
|
||
++currentHeight;
|
||
}
|
||
}
|
||
|
||
// Lock during animation so the user can't change tabs
|
||
locked = true;
|
||
|
||
var fromHeight = { height: currentHeight + 'px' },
|
||
toHeight = { height: newHeight + 'px' };
|
||
|
||
// Set the height to the current, specific pixel height to fix a bug on iOS where the height
|
||
// first animates to 0, then back to the proper height causing a visual glitch
|
||
$element.css(fromHeight);
|
||
|
||
// Animate the height from the old to the new
|
||
$animateCss($element, {
|
||
from: fromHeight,
|
||
to: toHeight,
|
||
easing: 'cubic-bezier(0.35, 0, 0.25, 1)',
|
||
duration: 0.5
|
||
}).start().done(function () {
|
||
// Then (to fix the same iOS issue as above), disable transitions and remove the specific
|
||
// pixel height so the height can size with browser width/content changes, etc.
|
||
$element.css({
|
||
transition: 'none',
|
||
height: ''
|
||
});
|
||
|
||
// In the next tick, re-allow transitions (if we do it all at once, $element.css is "smart"
|
||
// enough to batch it for us instead of doing it immediately, which undoes the original
|
||
// transition: none)
|
||
$mdUtil.nextTick(function() {
|
||
$element.css('transition', '');
|
||
});
|
||
|
||
// And unlock so tab changes can occur
|
||
locked = false;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Repositions the ink bar to the selected tab.
|
||
* Parameters are used when calling itself recursively when md-center-tabs is used as we need to
|
||
* run two passes to properly center the tabs. These parameters ensure that we only run two passes
|
||
* and that we don't run indefinitely.
|
||
* @param {number=} previousTotalWidth previous width of pagination wrapper
|
||
* @param {number=} previousWidthOfTabItems previous width of all tab items
|
||
*/
|
||
function updateInkBarStyles (previousTotalWidth, previousWidthOfTabItems) {
|
||
if (ctrl.noInkBar) {
|
||
return;
|
||
}
|
||
var elements = getElements();
|
||
|
||
if (!elements.tabs[ ctrl.selectedIndex ]) {
|
||
angular.element(elements.inkBar).css({ left: 'auto', right: 'auto' });
|
||
return;
|
||
}
|
||
|
||
if (!ctrl.tabs.length) {
|
||
queue.push(ctrl.updateInkBarStyles);
|
||
return;
|
||
}
|
||
// If the element is not visible, we will not be able to calculate sizes until it becomes
|
||
// visible. We should treat that as a resize event rather than just updating the ink bar.
|
||
if (!$element.prop('offsetParent')) {
|
||
handleResizeWhenVisible();
|
||
return;
|
||
}
|
||
|
||
var index = ctrl.selectedIndex,
|
||
totalWidth = elements.paging.offsetWidth,
|
||
tab = elements.tabs[ index ],
|
||
left = tab.offsetLeft,
|
||
right = totalWidth - left - tab.offsetWidth;
|
||
|
||
if (ctrl.shouldCenterTabs) {
|
||
// We need to use the same calculate process as in the pagination wrapper, to avoid rounding
|
||
// deviations.
|
||
var totalWidthOfTabItems = calcTabsWidth(elements.tabs);
|
||
|
||
if (totalWidth > totalWidthOfTabItems &&
|
||
previousTotalWidth !== totalWidth &&
|
||
previousWidthOfTabItems !== totalWidthOfTabItems) {
|
||
$timeout(updateInkBarStyles, 0, true, totalWidth, totalWidthOfTabItems);
|
||
}
|
||
}
|
||
updateInkBarClassName();
|
||
angular.element(elements.inkBar).css({ left: left + 'px', right: right + 'px' });
|
||
}
|
||
|
||
/**
|
||
* Adds left/right classes so that the ink bar will animate properly.
|
||
*/
|
||
function updateInkBarClassName () {
|
||
var elements = getElements();
|
||
var newIndex = ctrl.selectedIndex,
|
||
oldIndex = ctrl.lastSelectedIndex,
|
||
ink = angular.element(elements.inkBar);
|
||
if (!angular.isNumber(oldIndex)) return;
|
||
ink
|
||
.toggleClass('md-left', newIndex < oldIndex)
|
||
.toggleClass('md-right', newIndex > oldIndex);
|
||
}
|
||
|
||
/**
|
||
* Takes an offset value and makes sure that it is within the min/max allowed values.
|
||
* @param {number} value
|
||
* @returns {number}
|
||
*/
|
||
function fixOffset (value) {
|
||
var elements = getElements();
|
||
|
||
if (!elements.tabs.length || !ctrl.shouldPaginate) return 0;
|
||
|
||
var lastTab = elements.tabs[ elements.tabs.length - 1 ],
|
||
totalWidth = lastTab.offsetLeft + lastTab.offsetWidth;
|
||
|
||
if (isRtl()) {
|
||
value = Math.min(elements.paging.offsetWidth - elements.canvas.clientWidth, value);
|
||
value = Math.max(0, value);
|
||
} else {
|
||
value = Math.max(0, value);
|
||
value = Math.min(totalWidth - elements.canvas.clientWidth, value);
|
||
}
|
||
|
||
return value;
|
||
}
|
||
|
||
/**
|
||
* Attaches a ripple to the tab item element.
|
||
* @param scope
|
||
* @param element
|
||
*/
|
||
function attachRipple (scope, element) {
|
||
var elements = getElements();
|
||
var options = { colorElement: angular.element(elements.inkBar) };
|
||
$mdTabInkRipple.attach(scope, element, options);
|
||
}
|
||
|
||
/**
|
||
* Sets the `aria-controls` attribute to the elements that correspond to the passed-in tab.
|
||
* @param tab
|
||
*/
|
||
function setAriaControls (tab) {
|
||
if (tab.hasContent) {
|
||
var nodes = $element[0].querySelectorAll('[md-tab-id="' + tab.id + '"]');
|
||
angular.element(nodes).attr('aria-controls', ctrl.tabContentPrefix + tab.id);
|
||
}
|
||
}
|
||
|
||
function isRtl() {
|
||
return $mdUtil.isRtl($attrs);
|
||
}
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdTabs
|
||
* @module material.components.tabs
|
||
*
|
||
* @restrict E
|
||
*
|
||
* @description
|
||
* The `<md-tabs>` directive serves as the container for 1..n
|
||
* <a ng-href="api/directive/mdTab">`<md-tab>`</a> child directives.
|
||
* In turn, the nested `<md-tab>` directive is used to specify a tab label for the
|
||
* **header button** and <i>optional</i> tab view content that will be associated with each tab
|
||
* button.
|
||
*
|
||
* Below is the markup for its simplest usage:
|
||
*
|
||
* <hljs lang="html">
|
||
* <md-tabs>
|
||
* <md-tab label="Tab #1"></md-tab>
|
||
* <md-tab label="Tab #2"></md-tab>
|
||
* <md-tab label="Tab #3"></md-tab>
|
||
* </md-tabs>
|
||
* </hljs>
|
||
*
|
||
* Tabs support three (3) usage scenarios:
|
||
*
|
||
* 1. Tabs (buttons only)
|
||
* 2. Tabs with internal view content
|
||
* 3. Tabs with external view content
|
||
*
|
||
* **Tabs-only** support is useful when tab buttons are used for custom navigation regardless of any
|
||
* other components, content, or views.
|
||
*
|
||
* <blockquote><b>Note:</b> If you are using the Tabs component for page-level navigation, please
|
||
* use the <a ng-href="./api/directive/mdNavBar">NavBar component</a> instead. It handles this
|
||
* case a more natively and more performantly.</blockquote>
|
||
*
|
||
* **Tabs with internal views** are the traditional usage where each tab has associated view
|
||
* content and the view switching is managed internally by the Tabs component.
|
||
*
|
||
* **Tabs with external view content** is often useful when content associated with each tab is
|
||
* independently managed and data-binding notifications announce tab selection changes.
|
||
*
|
||
* Additional features include:
|
||
*
|
||
* * Content can include any markup.
|
||
* * If a tab is disabled while active/selected, then the next tab will be auto-selected.
|
||
*
|
||
* ### Theming
|
||
*
|
||
* By default, tabs use your app's accent color for the selected tab's text and ink bar.
|
||
*
|
||
* You can use the theming classes to change the color of the `md-tabs` background:
|
||
* * Applying `class="md-primary"` will use your app's primary color for the background, your
|
||
* accent color for the ink bar, and your primary palette's contrast color for the text of the
|
||
* selected tab.
|
||
* * When using the `md-primary` class, you can add the `md-no-ink-bar-color` class to make the
|
||
* ink bar use your theme's primary contrast color instead of the accent color.
|
||
* * Applying `class="md-accent"` will use your app's accent color for the background and your
|
||
* accent palette's contrast color for the text and ink bar of the selected tab.
|
||
* * Applying `class="md-warn"` will use your app's warn color for the background and your
|
||
* warn palette's contrast color for the text and ink bar of the selected tab.
|
||
*
|
||
* ### Explanation of tab stretching
|
||
*
|
||
* Initially, tabs will have an inherent size. This size will either be defined by how much space
|
||
* is needed to accommodate their text or set by the user through CSS.
|
||
* Calculations will be based on this size.
|
||
*
|
||
* On mobile devices, tabs will be expanded to fill the available horizontal space.
|
||
* When this happens, all tabs will become the same size.
|
||
*
|
||
* On desktops, by default, stretching will never occur.
|
||
*
|
||
* This default behavior can be overridden through the `md-stretch-tabs` attribute.
|
||
* Here is a table showing when stretching will occur:
|
||
*
|
||
* `md-stretch-tabs` | mobile | desktop
|
||
* ------------------|-----------|--------
|
||
* `auto` | stretched | ---
|
||
* `always` | stretched | stretched
|
||
* `never` | --- | ---
|
||
*
|
||
* @param {number=} md-selected Index of the active/selected tab.
|
||
* @param {expression=} md-no-ink-bar If `true` or no value, disables the selection ink bar.
|
||
* @param {string=} md-align-tabs Attribute to indicate position of tab buttons: `bottom` or `top`;
|
||
* Default is `top`.
|
||
* @param {string=} md-stretch-tabs Attribute to indicate whether or not to stretch tabs: `auto`,
|
||
* `always`, or `never`; Default is `auto`.
|
||
* @param {expression=} md-dynamic-height If `true` or no value, the tab wrapper will resize based
|
||
* on the contents of the selected tab.
|
||
* @param {boolean=} md-border-bottom If the attribute is present, shows a solid `1px` border
|
||
* between the tabs and their content.
|
||
* @param {boolean=} md-center-tabs If the attribute is present, tabs will be centered provided
|
||
* there is no need for pagination.
|
||
* @param {boolean=} md-no-pagination If the attribute is present, pagination will remain off.
|
||
* @param {expression=} md-swipe-content When enabled, swipe gestures will be enabled for the content
|
||
* area to allow swiping between tabs.
|
||
* @param {boolean=} md-enable-disconnect When enabled, scopes will be disconnected for tabs that
|
||
* are not being displayed. This provides a performance boost, but may also cause unexpected
|
||
* issues. It is not recommended for most users.
|
||
* @param {boolean=} md-autoselect If the attribute is present, any tabs added after the initial
|
||
* load will be automatically selected.
|
||
* @param {boolean=} md-no-select-click When true, click events will not be fired when the value of
|
||
* `md-active` on an `md-tab` changes. This is useful when using tabs with UI-Router's child
|
||
* states, as triggering a click event in that case can cause an extra tab change to occur.
|
||
* @param {string=} md-navigation-hint Attribute to override the default `tablist` navigation hint
|
||
* that screen readers will announce to provide instructions for navigating between tabs. This is
|
||
* desirable when you want the hint to be in a different language. Default is "Use the left and
|
||
* right arrow keys to navigate between tabs".
|
||
*
|
||
* @usage
|
||
* <hljs lang="html">
|
||
* <md-tabs md-selected="selectedIndex">
|
||
* <img ng-src="img/angular.png" class="centered" alt="Angular icon">
|
||
* <md-tab
|
||
* ng-repeat="tab in tabs | orderBy:predicate:reversed"
|
||
* md-on-select="onTabSelected(tab)"
|
||
* md-on-deselect="announceDeselected(tab)"
|
||
* ng-disabled="tab.disabled">
|
||
* <md-tab-label>
|
||
* {{tab.title}}
|
||
* <img src="img/removeTab.png" ng-click="removeTab(tab)" class="delete" alt="Remove tab">
|
||
* </md-tab-label>
|
||
* <md-tab-body>
|
||
* {{tab.content}}
|
||
* </md-tab-body>
|
||
* </md-tab>
|
||
* </md-tabs>
|
||
* </hljs>
|
||
*
|
||
*/
|
||
MdTabs.$inject = ["$$mdSvgRegistry"];
|
||
angular
|
||
.module('material.components.tabs')
|
||
.directive('mdTabs', MdTabs);
|
||
|
||
function MdTabs ($$mdSvgRegistry) {
|
||
return {
|
||
scope: {
|
||
navigationHint: '@?mdNavigationHint',
|
||
selectedIndex: '=?mdSelected'
|
||
},
|
||
template: function (element, attr) {
|
||
attr.$mdTabsTemplate = element.html();
|
||
return '' +
|
||
'<md-tabs-wrapper> ' +
|
||
'<md-tab-data></md-tab-data> ' +
|
||
'<md-prev-button ' +
|
||
'tabindex="-1" ' +
|
||
'role="button" ' +
|
||
'aria-label="Previous Page" ' +
|
||
'aria-disabled="{{!$mdTabsCtrl.canPageBack()}}" ' +
|
||
'ng-class="{ \'md-disabled\': !$mdTabsCtrl.canPageBack() }" ' +
|
||
'ng-if="$mdTabsCtrl.shouldPaginate" ' +
|
||
'ng-click="$mdTabsCtrl.previousPage()"> ' +
|
||
'<md-icon md-svg-src="'+ $$mdSvgRegistry.mdTabsArrow +'"></md-icon> ' +
|
||
'</md-prev-button> ' +
|
||
'<md-next-button ' +
|
||
'tabindex="-1" ' +
|
||
'role="button" ' +
|
||
'aria-label="Next Page" ' +
|
||
'aria-disabled="{{!$mdTabsCtrl.canPageForward()}}" ' +
|
||
'ng-class="{ \'md-disabled\': !$mdTabsCtrl.canPageForward() }" ' +
|
||
'ng-if="$mdTabsCtrl.shouldPaginate" ' +
|
||
'ng-click="$mdTabsCtrl.nextPage()"> ' +
|
||
'<md-icon md-svg-src="'+ $$mdSvgRegistry.mdTabsArrow +'"></md-icon> ' +
|
||
'</md-next-button> ' +
|
||
'<md-tabs-canvas ' +
|
||
'tabindex="{{ $mdTabsCtrl.hasFocus ? -1 : 0 }}" ' +
|
||
'ng-focus="$mdTabsCtrl.redirectFocus()" ' +
|
||
'ng-class="{ ' +
|
||
'\'md-paginated\': $mdTabsCtrl.shouldPaginate, ' +
|
||
'\'md-center-tabs\': $mdTabsCtrl.shouldCenterTabs ' +
|
||
'}" ' +
|
||
'ng-keydown="$mdTabsCtrl.keydown($event)"> ' +
|
||
'<md-pagination-wrapper ' +
|
||
'ng-class="{ \'md-center-tabs\': $mdTabsCtrl.shouldCenterTabs }" ' +
|
||
'md-tab-scroll="$mdTabsCtrl.scroll($event)" ' +
|
||
'role="tablist" ' +
|
||
'aria-label="{{::$mdTabsCtrl.navigationHint}}">' +
|
||
'<md-tab-item ' +
|
||
'tabindex="{{ tab.isActive() ? 0 : -1 }}" ' +
|
||
'class="md-tab {{::tab.scope.tabClass}}" ' +
|
||
'ng-repeat="tab in $mdTabsCtrl.tabs" ' +
|
||
'role="tab" ' +
|
||
'id="tab-item-{{::tab.id}}" ' +
|
||
'md-tab-id="{{::tab.id}}" ' +
|
||
'aria-selected="{{tab.isActive()}}" ' +
|
||
'aria-disabled="{{tab.scope.disabled || \'false\'}}" ' +
|
||
'ng-click="$mdTabsCtrl.select(tab.getIndex())" ' +
|
||
'ng-focus="$mdTabsCtrl.hasFocus = true" ' +
|
||
'ng-blur="$mdTabsCtrl.hasFocus = false" ' +
|
||
'ng-class="{ ' +
|
||
'\'md-active\': tab.isActive(), ' +
|
||
'\'md-focused\': tab.hasFocus(), ' +
|
||
'\'md-disabled\': tab.scope.disabled ' +
|
||
'}" ' +
|
||
'ng-disabled="tab.scope.disabled" ' +
|
||
'md-swipe-left="$mdTabsCtrl.nextPage()" ' +
|
||
'md-swipe-right="$mdTabsCtrl.previousPage()" ' +
|
||
'md-tabs-template="::tab.label" ' +
|
||
'md-scope="::tab.parent"></md-tab-item> ' +
|
||
'<md-ink-bar></md-ink-bar> ' +
|
||
'</md-pagination-wrapper> ' +
|
||
'<md-tabs-dummy-wrapper aria-hidden="true" class="md-visually-hidden md-dummy-wrapper"> ' +
|
||
'<md-dummy-tab ' +
|
||
'class="md-tab" ' +
|
||
'tabindex="-1" ' +
|
||
'ng-focus="$mdTabsCtrl.hasFocus = true" ' +
|
||
'ng-blur="$mdTabsCtrl.hasFocus = false" ' +
|
||
'ng-repeat="tab in $mdTabsCtrl.tabs" ' +
|
||
'md-tabs-template="::tab.label" ' +
|
||
'md-scope="::tab.parent"></md-dummy-tab> ' +
|
||
'</md-tabs-dummy-wrapper> ' +
|
||
'</md-tabs-canvas> ' +
|
||
'</md-tabs-wrapper> ' +
|
||
'<md-tabs-content-wrapper ng-show="$mdTabsCtrl.hasContent && $mdTabsCtrl.selectedIndex >= 0" class="_md"> ' +
|
||
'<md-tab-content ' +
|
||
'id="{{:: $mdTabsCtrl.tabContentPrefix + tab.id}}" ' +
|
||
'class="_md" ' +
|
||
'role="tabpanel" ' +
|
||
'aria-labelledby="tab-item-{{::tab.id}}" ' +
|
||
'md-swipe-left="$mdTabsCtrl.swipeContent && $mdTabsCtrl.incrementIndex(1)" ' +
|
||
'md-swipe-right="$mdTabsCtrl.swipeContent && $mdTabsCtrl.incrementIndex(-1)" ' +
|
||
'ng-if="tab.hasContent" ' +
|
||
'ng-repeat="(index, tab) in $mdTabsCtrl.tabs" ' +
|
||
'ng-class="{ ' +
|
||
'\'md-no-transition\': $mdTabsCtrl.lastSelectedIndex == null, ' +
|
||
'\'md-active\': tab.isActive(), ' +
|
||
'\'md-left\': tab.isLeft(), ' +
|
||
'\'md-right\': tab.isRight(), ' +
|
||
'\'md-no-scroll\': $mdTabsCtrl.dynamicHeight ' +
|
||
'}"> ' +
|
||
'<div ' +
|
||
'md-tabs-template="::tab.template" ' +
|
||
'md-connected-if="tab.isActive()" ' +
|
||
'md-scope="::tab.parent" ' +
|
||
'ng-if="$mdTabsCtrl.enableDisconnect || tab.shouldRender()"></div> ' +
|
||
'</md-tab-content> ' +
|
||
'</md-tabs-content-wrapper>';
|
||
},
|
||
controller: 'MdTabsController',
|
||
controllerAs: '$mdTabsCtrl',
|
||
bindToController: true
|
||
};
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
|
||
MdTabsDummyWrapper.$inject = ["$mdUtil", "$window"];angular
|
||
.module('material.components.tabs')
|
||
.directive('mdTabsDummyWrapper', MdTabsDummyWrapper);
|
||
|
||
/**
|
||
* @private
|
||
*
|
||
* @param $mdUtil
|
||
* @param $window
|
||
* @returns {{require: string, link: link}}
|
||
* @constructor
|
||
*
|
||
* @ngInject
|
||
*/
|
||
function MdTabsDummyWrapper ($mdUtil, $window) {
|
||
return {
|
||
require: '^?mdTabs',
|
||
link: function link (scope, element, attr, ctrl) {
|
||
if (!ctrl) return;
|
||
|
||
var observer;
|
||
var disconnect;
|
||
|
||
var mutationCallback = function() {
|
||
ctrl.updatePagination();
|
||
ctrl.updateInkBarStyles();
|
||
};
|
||
|
||
if ('MutationObserver' in $window) {
|
||
var config = {
|
||
childList: true,
|
||
subtree: true,
|
||
// Per https://bugzilla.mozilla.org/show_bug.cgi?id=1138368, browsers will not fire
|
||
// the childList mutation, once a <span> element's innerText changes.
|
||
// The characterData of the <span> element will change.
|
||
characterData: true
|
||
};
|
||
|
||
observer = new MutationObserver(mutationCallback);
|
||
observer.observe(element[0], config);
|
||
disconnect = observer.disconnect.bind(observer);
|
||
} else {
|
||
var debounced = $mdUtil.debounce(mutationCallback, 15, null, false);
|
||
|
||
element.on('DOMSubtreeModified', debounced);
|
||
disconnect = element.off.bind(element, 'DOMSubtreeModified', debounced);
|
||
}
|
||
|
||
// Disconnect the observer
|
||
scope.$on('$destroy', function() {
|
||
disconnect();
|
||
});
|
||
}
|
||
};
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
|
||
MdTabsTemplate.$inject = ["$compile", "$mdUtil"];angular
|
||
.module('material.components.tabs')
|
||
.directive('mdTabsTemplate', MdTabsTemplate);
|
||
|
||
function MdTabsTemplate ($compile, $mdUtil) {
|
||
return {
|
||
restrict: 'A',
|
||
link: link,
|
||
scope: {
|
||
template: '=mdTabsTemplate',
|
||
connected: '=?mdConnectedIf',
|
||
compileScope: '=mdScope'
|
||
},
|
||
require: '^?mdTabs'
|
||
};
|
||
function link (scope, element, attr, ctrl) {
|
||
if (!ctrl) return;
|
||
|
||
var compileScope = ctrl.enableDisconnect ? scope.compileScope.$new() : scope.compileScope;
|
||
|
||
element.html(scope.template);
|
||
$compile(element.contents())(compileScope);
|
||
|
||
return $mdUtil.nextTick(handleScope);
|
||
|
||
function handleScope () {
|
||
scope.$watch('connected', function (value) { value === false ? disconnect() : reconnect(); });
|
||
scope.$on('$destroy', reconnect);
|
||
}
|
||
|
||
function disconnect () {
|
||
if (ctrl.enableDisconnect) $mdUtil.disconnectScope(compileScope);
|
||
}
|
||
|
||
function reconnect () {
|
||
if (ctrl.enableDisconnect) $mdUtil.reconnectScope(compileScope);
|
||
}
|
||
}
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.toast
|
||
* @description
|
||
* Toast and Snackbar component.
|
||
*/
|
||
MdToastDirective.$inject = ["$mdToast"];
|
||
MdToastProvider.$inject = ["$$interimElementProvider"];
|
||
angular.module('material.components.toast', [
|
||
'material.core',
|
||
'material.components.button'
|
||
])
|
||
.directive('mdToast', MdToastDirective)
|
||
.provider('$mdToast', MdToastProvider);
|
||
|
||
/* @ngInject */
|
||
function MdToastDirective($mdToast) {
|
||
return {
|
||
restrict: 'E',
|
||
link: function postLink(scope, element) {
|
||
element.addClass('_md'); // private md component indicator for styling
|
||
|
||
// When navigation force destroys an interimElement, then
|
||
// listen and $destroy() that interim instance...
|
||
scope.$on('$destroy', function() {
|
||
$mdToast.destroy();
|
||
});
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* @ngdoc service
|
||
* @name $mdToast
|
||
* @module material.components.toast
|
||
*
|
||
* @description
|
||
* `$mdToast` is a service to build a toast notification on any position
|
||
* on the screen with an optional duration, and provides a simple promise API.
|
||
*
|
||
* The toast will be always positioned at the `bottom`, when the screen size is
|
||
* between `600px` and `959px` (`sm` breakpoint)
|
||
*
|
||
* ## Restrictions on custom toasts
|
||
* - The toast's template must have an outer `<md-toast>` element.
|
||
* - For a toast action, use element with class `md-action`.
|
||
* - Add the class `md-capsule` for curved corners.
|
||
*
|
||
* ### Custom Presets
|
||
* Developers are also able to create their own preset, which can be easily used without repeating
|
||
* their options each time.
|
||
*
|
||
* <hljs lang="js">
|
||
* $mdToastProvider.addPreset('testPreset', {
|
||
* options: function() {
|
||
* return {
|
||
* template:
|
||
* '<md-toast>' +
|
||
* '<div class="md-toast-content">' +
|
||
* 'This is a custom preset' +
|
||
* '</div>' +
|
||
* '</md-toast>',
|
||
* controllerAs: 'toast',
|
||
* bindToController: true
|
||
* };
|
||
* }
|
||
* });
|
||
* </hljs>
|
||
*
|
||
* After you created your preset at config phase, you can easily access it.
|
||
*
|
||
* <hljs lang="js">
|
||
* $mdToast.show(
|
||
* $mdToast.testPreset()
|
||
* );
|
||
* </hljs>
|
||
*
|
||
* ## Parent container notes
|
||
*
|
||
* The toast is positioned using absolute positioning relative to its first non-static parent
|
||
* container. Thus, if the requested parent container uses static positioning, we will temporarily
|
||
* set its positioning to `relative` while the toast is visible and reset it when the toast is
|
||
* hidden.
|
||
*
|
||
* Because of this, it is usually best to ensure that the parent container has a fixed height and
|
||
* prevents scrolling by setting the `overflow: hidden;` style. Since the position is based off of
|
||
* the parent's height, the toast may be mispositioned if you allow the parent to scroll.
|
||
*
|
||
* You can, however, have a scrollable element inside of the container; just make sure the
|
||
* container itself does not scroll.
|
||
*
|
||
* <hljs lang="html">
|
||
* <div layout-fill id="toast-container">
|
||
* <md-content>
|
||
* I can have lots of content and scroll!
|
||
* </md-content>
|
||
* </div>
|
||
* </hljs>
|
||
*
|
||
* @usage
|
||
* <hljs lang="html">
|
||
* <div ng-controller="MyController">
|
||
* <md-button ng-click="openToast()">
|
||
* Open a Toast!
|
||
* </md-button>
|
||
* </div>
|
||
* </hljs>
|
||
*
|
||
* <hljs lang="js">
|
||
* var app = angular.module('app', ['ngMaterial']);
|
||
* app.controller('MyController', function($scope, $mdToast) {
|
||
* $scope.openToast = function($event) {
|
||
* $mdToast.show($mdToast.simple().textContent('Hello!'));
|
||
* // Could also do $mdToast.showSimple('Hello');
|
||
* };
|
||
* });
|
||
* </hljs>
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdToast#showSimple
|
||
*
|
||
* @param {string} message The message to display inside the toast
|
||
* @description
|
||
* Convenience method which builds and shows a simple toast.
|
||
*
|
||
* @returns {promise} A promise that can be resolved with `$mdToast.hide()`.
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdToast#simple
|
||
*
|
||
* @description
|
||
* Builds a preconfigured toast.
|
||
*
|
||
* @returns {obj} a `$mdToastPreset` with the following chainable configuration methods.
|
||
*
|
||
* _**Note:** These configuration methods are provided in addition to the methods provided by
|
||
* the `build()` and `show()` methods below._
|
||
*
|
||
* <table class="md-api-table methods">
|
||
* <thead>
|
||
* <tr>
|
||
* <th>Method</th>
|
||
* <th>Description</th>
|
||
* </tr>
|
||
* </thead>
|
||
* <tbody>
|
||
* <tr>
|
||
* <td>`.textContent(string)`</td>
|
||
* <td>Sets the toast content to the specified string</td>
|
||
* </tr>
|
||
* <tr>
|
||
* <td>`.action(string)`</td>
|
||
* <td>
|
||
* Adds an action button. <br/>
|
||
* If clicked, the promise (returned from `show()`) will resolve with the value `'ok'`;
|
||
* otherwise, it is resolved with `true` after a `hideDelay` timeout.
|
||
* </td>
|
||
* </tr>
|
||
* <tr>
|
||
* <td>`.actionKey(string)`</td>
|
||
* <td>
|
||
* Adds a hotkey for the action button to the page. <br/>
|
||
* If the `actionKey` and `Control` key are pressed, the toast's action will be triggered.
|
||
* </td>
|
||
* </tr>
|
||
* <tr>
|
||
* <td>`.actionHint(string)`</td>
|
||
* <td>
|
||
* Text that a screen reader will announce to let the user know how to activate the
|
||
* action. <br>
|
||
* If an `actionKey` is defined, this defaults to:
|
||
* 'Press Control-"`actionKey`" to ' followed by the `action`.
|
||
* </td>
|
||
* </tr>
|
||
* <tr>
|
||
* <td>`.dismissHint(string)`</td>
|
||
* <td>
|
||
* Text that a screen reader will announce to let the user know how to dismiss the toast.
|
||
* <br>Defaults to: "Press Escape to dismiss."
|
||
* </td>
|
||
* </tr>
|
||
* <tr>
|
||
* <td>`.highlightAction(boolean)`</td>
|
||
* <td>
|
||
* Whether or not the action button will have an additional highlight class.<br/>
|
||
* By default the `accent` color will be applied to the action button.
|
||
* </td>
|
||
* </tr>
|
||
* <tr>
|
||
* <td>`.highlightClass(string)`</td>
|
||
* <td>
|
||
* If set, the given class will be applied to the highlighted action button.<br/>
|
||
* This allows you to specify the highlight color easily. Highlight classes are
|
||
* `md-primary`, `md-warn`, and `md-accent`
|
||
* </td>
|
||
* </tr>
|
||
* <tr>
|
||
* <td>`.capsule(boolean)`</td>
|
||
* <td>
|
||
* Whether or not to add the `md-capsule` class to the toast to provide rounded corners
|
||
* </td>
|
||
* </tr>
|
||
* <tr>
|
||
* <td>`.theme(string)`</td>
|
||
* <td>
|
||
* Sets the theme on the toast to the requested theme. Default is `$mdThemingProvider`'s
|
||
* default.
|
||
* </td>
|
||
* </tr>
|
||
* <tr>
|
||
* <td>`.toastClass(string)`</td>
|
||
* <td>Sets a class on the toast element</td>
|
||
* </tr>
|
||
* </tbody>
|
||
* </table>
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdToast#updateTextContent
|
||
*
|
||
* @description
|
||
* Updates the content of an existing toast. Useful for updating things like counts, etc.
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdToast#build
|
||
*
|
||
* @description
|
||
* Creates a custom `$mdToastPreset` that you can configure.
|
||
*
|
||
* @returns {obj} a `$mdToastPreset` with the chainable configuration methods for shows' options
|
||
* (see below).
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdToast#show
|
||
*
|
||
* @description Shows the toast.
|
||
*
|
||
* @param {Object} optionsOrPreset Either provide an `$mdToastPreset` returned from `simple()`
|
||
* and `build()`, or an options object with the following properties:
|
||
*
|
||
* - `templateUrl` - `{string=}`: The url of an html template file that will
|
||
* be used as the content of the toast. Restrictions: the template must
|
||
* have an outer `md-toast` element.
|
||
* - `template` - `{string=}`: Same as templateUrl, except this is an actual
|
||
* template string.
|
||
* - `autoWrap` - `{boolean=}`: Whether or not to automatically wrap the template content with a
|
||
* `<div class="md-toast-content">` if one is not provided. Defaults to true. Can be disabled
|
||
* if you provide a custom toast directive.
|
||
* - `scope` - `{Object=}`: the scope to link the template / controller to. If none is specified,
|
||
* it will create a new child scope. This scope will be destroyed when the toast is removed
|
||
* unless `preserveScope` is set to true.
|
||
* - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed.
|
||
* Default is false
|
||
* - `hideDelay` - `{number=}`: The number of milliseconds the toast should stay active before
|
||
* automatically closing. Set to `0` or `false` to have the toast stay open until closed
|
||
* manually via an action in the toast, a hotkey, or a swipe gesture. For accessibility, toasts
|
||
* should not automatically close when they contain an action.<br>
|
||
* Defaults to: `3000`.
|
||
* - `position` - `{string=}`: Sets the position of the toast. <br/>
|
||
* Available: any combination of `'bottom'`, `'left'`, `'top'`, `'right'`, `'end'`, and
|
||
* `'start'`. The properties `'end'` and `'start'` are dynamic and can be used for RTL support.
|
||
* <br/>
|
||
* Default combination: `'bottom left'`.
|
||
* - `toastClass` - `{string=}`: A class to set on the toast element.
|
||
* - `controller` - `{string=}`: The controller to associate with this toast.
|
||
* The controller will be injected the local `$mdToast.hide()`, which is a function
|
||
* used to hide the toast.
|
||
* - `locals` - `{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 of 3.
|
||
* - `bindToController` - `{boolean=}`: bind the locals to the controller, instead of passing
|
||
* them in.
|
||
* - `resolve` - `{Object=}`: Similar to locals, except it takes promises as values
|
||
* and the toast will not open until the promises resolve.
|
||
* - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope.
|
||
* - `parent` - `{element=}`: The element to append the toast to. Defaults to appending
|
||
* to the root element of the application.
|
||
*
|
||
* @returns {promise} A promise that can be resolved with `$mdToast.hide()`. `$mdToast.hide()` will
|
||
* resolve either with the boolean value `true` or the value passed as an argument to
|
||
* `$mdToast.hide()`.
|
||
*/
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $mdToast#hide
|
||
*
|
||
* @description
|
||
* Hide an existing toast and resolve the promise returned from `$mdToast.show()`.
|
||
*
|
||
* @param {*=} response An argument for the resolved promise.
|
||
*
|
||
* @returns {promise} A promise that is called when the existing element is removed from the DOM.
|
||
* The promise is resolved with either the Boolean value `true` or the value passed as the
|
||
* argument to `$mdToast.hide()`.
|
||
*/
|
||
|
||
function MdToastProvider($$interimElementProvider) {
|
||
// Differentiate promise resolves: hide timeout (value == true) and hide action clicks
|
||
// (value == ok).
|
||
MdToastController.$inject = ["$mdToast", "$scope", "$log"];
|
||
toastDefaultOptions.$inject = ["$animate", "$mdToast", "$mdUtil", "$mdMedia", "$document", "$q"];
|
||
var ACTION_RESOLVE = 'ok';
|
||
|
||
var activeToastContent;
|
||
var $mdToast = $$interimElementProvider('$mdToast')
|
||
.setDefaults({
|
||
methods: ['position', 'hideDelay', 'capsule', 'parent', 'position', 'toastClass'],
|
||
options: toastDefaultOptions
|
||
})
|
||
.addPreset('simple', {
|
||
argOption: 'textContent',
|
||
methods: ['textContent', 'action', 'actionKey', 'actionHint', 'highlightAction',
|
||
'highlightClass', 'theme', 'parent', 'dismissHint'],
|
||
options: /* @ngInject */ ["$mdToast", "$mdTheming", function($mdToast, $mdTheming) {
|
||
return {
|
||
template:
|
||
'<md-toast md-theme="{{ toast.theme }}" ng-class="{\'md-capsule\': toast.capsule}">' +
|
||
' <div class="md-toast-content" aria-live="polite" aria-relevant="all">' +
|
||
' <span class="md-toast-text">' +
|
||
' {{ toast.content }}' +
|
||
' </span>' +
|
||
' <span class="md-visually-hidden">{{ toast.dismissHint }}</span>' +
|
||
' <span class="md-visually-hidden" ng-if="toast.action && toast.actionKey">' +
|
||
' {{ toast.actionHint }}' +
|
||
' </span>' +
|
||
' <md-button class="md-action" ng-if="toast.action" ng-click="toast.resolve()" ' +
|
||
' ng-class="highlightClasses">' +
|
||
' {{ toast.action }}' +
|
||
' </md-button>' +
|
||
' </div>' +
|
||
'</md-toast>',
|
||
controller: MdToastController,
|
||
theme: $mdTheming.defaultTheme(),
|
||
controllerAs: 'toast',
|
||
bindToController: true
|
||
};
|
||
}]
|
||
})
|
||
.addMethod('updateTextContent', updateTextContent);
|
||
|
||
function updateTextContent(newContent) {
|
||
activeToastContent = newContent;
|
||
}
|
||
|
||
return $mdToast;
|
||
|
||
/**
|
||
* Controller for the Toast interim elements.
|
||
* @ngInject
|
||
*/
|
||
function MdToastController($mdToast, $scope, $log) {
|
||
// For compatibility with AngularJS 1.6+, we should always use the $onInit hook in
|
||
// interimElements. The $mdCompiler simulates the $onInit hook for all versions.
|
||
this.$onInit = function() {
|
||
var self = this;
|
||
|
||
if (self.highlightAction) {
|
||
$scope.highlightClasses = [
|
||
'md-highlight',
|
||
self.highlightClass
|
||
];
|
||
}
|
||
|
||
// If an action is defined and no actionKey is specified, then log a warning.
|
||
if (self.action && !self.actionKey) {
|
||
$log.warn('Toasts with actions should define an actionKey for accessibility.',
|
||
'Details: https://material.angularjs.org/latest/api/service/$mdToast#mdtoast-simple');
|
||
}
|
||
|
||
if (self.actionKey && !self.actionHint) {
|
||
self.actionHint = 'Press Control-"' + self.actionKey + '" to ';
|
||
}
|
||
|
||
if (!self.dismissHint) {
|
||
self.dismissHint = 'Press Escape to dismiss.';
|
||
}
|
||
|
||
$scope.$watch(function() { return activeToastContent; }, function() {
|
||
self.content = activeToastContent;
|
||
});
|
||
|
||
this.resolve = function() {
|
||
$mdToast.hide(ACTION_RESOLVE);
|
||
};
|
||
};
|
||
}
|
||
|
||
/* @ngInject */
|
||
function toastDefaultOptions($animate, $mdToast, $mdUtil, $mdMedia, $document, $q) {
|
||
var SWIPE_EVENTS = '$md.swipeleft $md.swiperight $md.swipeup $md.swipedown';
|
||
return {
|
||
onShow: onShow,
|
||
onRemove: onRemove,
|
||
toastClass: '',
|
||
position: 'bottom left',
|
||
themable: true,
|
||
hideDelay: 3000,
|
||
autoWrap: true,
|
||
transformTemplate: function(template, options) {
|
||
var shouldAddWrapper = options.autoWrap && template && !/md-toast-content/g.test(template);
|
||
|
||
if (shouldAddWrapper) {
|
||
// Root element of template will be <md-toast>. We need to wrap all of its content inside
|
||
// of <div class="md-toast-content">. All templates provided here should be static,
|
||
// developer-controlled content (meaning we're not attempting to guard against XSS).
|
||
var templateRoot = document.createElement('md-template');
|
||
templateRoot.innerHTML = template;
|
||
|
||
// Iterate through all root children, to detect possible md-toast directives.
|
||
for (var i = 0; i < templateRoot.children.length; i++) {
|
||
if (templateRoot.children[i].nodeName === 'MD-TOAST') {
|
||
var wrapper = angular.element('<div class="md-toast-content">');
|
||
|
||
// Wrap the children of the `md-toast` directive in jqLite, to be able to append
|
||
// multiple nodes with the same execution.
|
||
wrapper.append(angular.element(templateRoot.children[i].childNodes));
|
||
|
||
// Append the new wrapped element to the `md-toast` directive.
|
||
templateRoot.children[i].appendChild(wrapper[0]);
|
||
}
|
||
}
|
||
|
||
// We have to return the innerHTML, because we do not want to have the `md-template`
|
||
// element to be the root element of our interimElement.
|
||
return templateRoot.innerHTML;
|
||
}
|
||
|
||
return template || '';
|
||
}
|
||
};
|
||
|
||
/**
|
||
* @param {{toast: {actionKey: string=}}=} scope
|
||
* @param {JQLite} element
|
||
* @param {Object.<string, string>} options
|
||
* @return {*}
|
||
*/
|
||
function onShow(scope, element, options) {
|
||
activeToastContent = options.textContent;
|
||
|
||
var isSmScreen = !$mdMedia('gt-sm');
|
||
|
||
element = $mdUtil.extractElementByName(element, 'md-toast', true);
|
||
options.element = element;
|
||
|
||
options.onSwipe = function(ev) {
|
||
// Add the relevant swipe class to the element so it can animate correctly
|
||
var swipe = ev.type.replace('$md.','');
|
||
var direction = swipe.replace('swipe', '');
|
||
|
||
// If the swipe direction is down/up but the toast came from top/bottom don't fade away
|
||
// Unless the screen is small, then the toast always on bottom
|
||
if ((direction === 'down' && options.position.indexOf('top') !== -1 && !isSmScreen) ||
|
||
(direction === 'up' && (options.position.indexOf('bottom') !== -1 || isSmScreen))) {
|
||
return;
|
||
}
|
||
|
||
if ((direction === 'left' || direction === 'right') && isSmScreen) {
|
||
return;
|
||
}
|
||
|
||
element.addClass('md-' + swipe);
|
||
$mdUtil.nextTick($mdToast.cancel);
|
||
};
|
||
options.openClass = toastOpenClass(options.position);
|
||
|
||
element.addClass(options.toastClass);
|
||
|
||
// 'top left' -> 'md-top md-left'
|
||
options.parent.addClass(options.openClass);
|
||
|
||
// static is the default position
|
||
if ($mdUtil.hasComputedStyle(options.parent, 'position', 'static')) {
|
||
options.parent.css('position', 'relative');
|
||
}
|
||
|
||
setupActionKeyListener(scope.toast && scope.toast.actionKey ?
|
||
scope.toast.actionKey : undefined);
|
||
|
||
element.on(SWIPE_EVENTS, options.onSwipe);
|
||
|
||
var verticalPositionDefined = false;
|
||
var positionClasses = options.position.split(' ').map(function (position) {
|
||
if (position) {
|
||
var className = 'md-' + position;
|
||
if (className === 'md-top' || className === 'md-bottom') {
|
||
verticalPositionDefined = true;
|
||
}
|
||
return className;
|
||
}
|
||
return 'md-bottom';
|
||
});
|
||
// If only "right" or "left" are defined, default to a vertical position of "bottom"
|
||
// as documented.
|
||
if (!verticalPositionDefined) {
|
||
positionClasses.push('md-bottom');
|
||
}
|
||
element.addClass(isSmScreen ? 'md-bottom' : positionClasses.join(' '));
|
||
|
||
if (options.parent) {
|
||
options.parent.addClass('md-toast-animating');
|
||
}
|
||
return $animate.enter(element, options.parent).then(function() {
|
||
if (options.parent) {
|
||
options.parent.removeClass('md-toast-animating');
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* @param {Object} scope the toast's scope
|
||
* @param {JQLite} element the toast to be removed
|
||
* @param {Object} options
|
||
* @return {Promise<*>} a Promise to remove the element immediately or to animate it out.
|
||
*/
|
||
function onRemove(scope, element, options) {
|
||
if (scope.toast && scope.toast.actionKey) {
|
||
removeActionKeyListener();
|
||
}
|
||
element.off(SWIPE_EVENTS, options.onSwipe);
|
||
if (options.parent) options.parent.addClass('md-toast-animating');
|
||
if (options.openClass) options.parent.removeClass(options.openClass);
|
||
|
||
// Don't run the leave animation if the element has already been destroyed.
|
||
return ((options.$destroy === true) ? $q.when(element.remove()) : $animate.leave(element))
|
||
.then(function () {
|
||
if (options.parent) options.parent.removeClass('md-toast-animating');
|
||
if ($mdUtil.hasComputedStyle(options.parent, 'position', 'static')) {
|
||
options.parent.css('position', '');
|
||
}
|
||
});
|
||
}
|
||
|
||
function toastOpenClass(position) {
|
||
// For mobile, always open full-width on bottom
|
||
if (!$mdMedia('gt-xs')) {
|
||
return 'md-toast-open-bottom';
|
||
}
|
||
|
||
return 'md-toast-open-' + (position.indexOf('top') > -1 ? 'top' : 'bottom');
|
||
}
|
||
|
||
/**
|
||
* @param {string} actionKey
|
||
*/
|
||
function setupActionKeyListener(actionKey) {
|
||
/**
|
||
* @param {KeyboardEvent} event
|
||
*/
|
||
var handleKeyDown = function(event) {
|
||
if (event.key === 'Escape') {
|
||
$mdToast.hide(false);
|
||
}
|
||
if (actionKey && event.key === actionKey && event.ctrlKey) {
|
||
$mdToast.hide(ACTION_RESOLVE);
|
||
}
|
||
};
|
||
$document.on('keydown', handleKeyDown);
|
||
}
|
||
|
||
function removeActionKeyListener() {
|
||
$document.off('keydown');
|
||
}
|
||
}
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.toolbar
|
||
*/
|
||
mdToolbarDirective.$inject = ["$$rAF", "$mdConstant", "$mdUtil", "$mdTheming", "$animate", "$timeout"];
|
||
angular.module('material.components.toolbar', [
|
||
'material.core',
|
||
'material.components.content'
|
||
])
|
||
.directive('mdToolbar', mdToolbarDirective);
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdToolbar
|
||
* @module material.components.toolbar
|
||
* @restrict E
|
||
* @description
|
||
* `md-toolbar` is used to place a toolbar in your app.
|
||
*
|
||
* Toolbars are usually used above a content area to display the title of the
|
||
* current page, and show relevant action buttons for that page.
|
||
*
|
||
* You can change the height of the toolbar by adding either the
|
||
* `md-medium-tall` or `md-tall` class to the toolbar.
|
||
*
|
||
* @usage
|
||
* <hljs lang="html">
|
||
* <div layout="column" layout-fill>
|
||
* <md-toolbar>
|
||
*
|
||
* <div class="md-toolbar-tools">
|
||
* <h2 md-truncate flex>My App's Title</h2>
|
||
*
|
||
* <md-button>
|
||
* Right Bar Button
|
||
* </md-button>
|
||
* </div>
|
||
*
|
||
* </md-toolbar>
|
||
* <md-content>
|
||
* Hello!
|
||
* </md-content>
|
||
* </div>
|
||
* </hljs>
|
||
*
|
||
* <i><b>Note:</b> The code above shows usage with the `md-truncate` component which provides an
|
||
* ellipsis if the title is longer than the width of the Toolbar.</i>
|
||
*
|
||
* ## CSS & Styles
|
||
*
|
||
* The `<md-toolbar>` provides a few custom CSS classes that you may use to enhance the
|
||
* functionality of your toolbar.
|
||
*
|
||
* <div>
|
||
* <docs-css-api-table>
|
||
*
|
||
* <docs-css-selector code="md-toolbar .md-toolbar-tools">
|
||
* The `md-toolbar-tools` class provides quite a bit of automatic styling for your toolbar
|
||
* buttons and text. When applied, it will center the buttons and text vertically for you.
|
||
* </docs-css-selector>
|
||
*
|
||
* </docs-css-api-table>
|
||
* </div>
|
||
*
|
||
* ### Private Classes
|
||
*
|
||
* Currently, the only private class is the `md-toolbar-transitions` class. All other classes are
|
||
* considered public.
|
||
*
|
||
* @param {boolean=} md-scroll-shrink Whether the header should shrink away as
|
||
* the user scrolls down, and reveal itself as the user scrolls up.
|
||
*
|
||
* _**Note (1):** for scrollShrink to work, the toolbar must be a sibling of a
|
||
* `md-content` element, placed before it. See the scroll shrink demo._
|
||
*
|
||
* _**Note (2):** The `md-scroll-shrink` attribute is only parsed on component
|
||
* initialization, it does not watch for scope changes._
|
||
*
|
||
*
|
||
* @param {number=} md-shrink-speed-factor How much to change the speed of the toolbar's
|
||
* shrinking by. For example, if 0.25 is given then the toolbar will shrink
|
||
* at one fourth the rate at which the user scrolls down. Default 0.5.
|
||
*
|
||
*/
|
||
|
||
function mdToolbarDirective($$rAF, $mdConstant, $mdUtil, $mdTheming, $animate, $timeout) {
|
||
var translateY = angular.bind(null, $mdUtil.supplant, 'translate3d(0,{0}px,0)');
|
||
|
||
return {
|
||
template: '',
|
||
restrict: 'E',
|
||
|
||
link: function(scope, element, attr) {
|
||
|
||
element.addClass('_md'); // private md component indicator for styling
|
||
$mdTheming(element);
|
||
|
||
$mdUtil.nextTick(function () {
|
||
element.addClass('_md-toolbar-transitions'); // adding toolbar transitions after digest
|
||
}, false);
|
||
|
||
if (angular.isDefined(attr.mdScrollShrink)) {
|
||
setupScrollShrink();
|
||
}
|
||
|
||
function setupScrollShrink() {
|
||
|
||
var toolbarHeight;
|
||
var contentElement;
|
||
var disableScrollShrink = angular.noop;
|
||
|
||
// Current "y" position of scroll
|
||
// Store the last scroll top position
|
||
var y = 0;
|
||
var prevScrollTop = 0;
|
||
var shrinkSpeedFactor = attr.mdShrinkSpeedFactor || 0.5;
|
||
|
||
var debouncedContentScroll = $$rAF.throttle(onContentScroll);
|
||
var debouncedUpdateHeight = $mdUtil.debounce(updateToolbarHeight, 5 * 1000);
|
||
|
||
// Wait for $mdContentLoaded event from mdContent directive.
|
||
// If the mdContent element is a sibling of our toolbar, hook it up
|
||
// to scroll events.
|
||
|
||
scope.$on('$mdContentLoaded', onMdContentLoad);
|
||
|
||
// If the toolbar is used inside an ng-if statement, we may miss the
|
||
// $mdContentLoaded event, so we attempt to fake it if we have a
|
||
// md-content close enough.
|
||
|
||
attr.$observe('mdScrollShrink', onChangeScrollShrink);
|
||
|
||
// If the toolbar has ngShow or ngHide we need to update height immediately as it changed
|
||
// and not wait for $mdUtil.debounce to happen
|
||
|
||
if (attr.ngShow) { scope.$watch(attr.ngShow, updateToolbarHeight); }
|
||
if (attr.ngHide) { scope.$watch(attr.ngHide, updateToolbarHeight); }
|
||
|
||
// If the scope is destroyed (which could happen with ng-if), make sure
|
||
// to disable scroll shrinking again
|
||
|
||
scope.$on('$destroy', disableScrollShrink);
|
||
|
||
/**
|
||
* @param {string} shrinkWithScroll value of md-scroll-shrink attribute
|
||
*/
|
||
function onChangeScrollShrink(shrinkWithScroll) {
|
||
var closestContent = $mdUtil.getSiblings(element, 'md-content');
|
||
|
||
// If there are content elements, fake the call using the first content element.
|
||
// This might still fail if the content element isn't a sibling of the toolbar.
|
||
if (!contentElement && closestContent.length) {
|
||
onMdContentLoad(null, closestContent[0]);
|
||
}
|
||
|
||
// Evaluate the expression
|
||
shrinkWithScroll = scope.$eval(shrinkWithScroll);
|
||
|
||
// Disable only if the attribute's expression evaluates to false
|
||
if (shrinkWithScroll === false) {
|
||
disableScrollShrink();
|
||
} else {
|
||
disableScrollShrink = enableScrollShrink();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {null} $event $mdContentLoaded always has a null event
|
||
* @param {JQLite} newContentEl JQLite object containing an md-content
|
||
*/
|
||
function onMdContentLoad($event, newContentEl) {
|
||
// Toolbar and content must be siblings
|
||
if (newContentEl && element.parent()[0] === newContentEl.parent()[0]) {
|
||
// unhook old content event listener if exists
|
||
if (contentElement) {
|
||
contentElement.off('scroll', debouncedContentScroll);
|
||
}
|
||
|
||
contentElement = newContentEl;
|
||
disableScrollShrink = enableScrollShrink();
|
||
}
|
||
}
|
||
|
||
/**
|
||
*
|
||
*/
|
||
function onContentScroll(e) {
|
||
var scrollTop = e ? e.target.scrollTop : prevScrollTop;
|
||
|
||
debouncedUpdateHeight();
|
||
|
||
y = Math.min(
|
||
toolbarHeight / shrinkSpeedFactor,
|
||
Math.max(0, y + scrollTop - prevScrollTop)
|
||
);
|
||
|
||
element.css($mdConstant.CSS.TRANSFORM, translateY([-y * shrinkSpeedFactor]));
|
||
contentElement.css($mdConstant.CSS.TRANSFORM, translateY([(toolbarHeight - y) * shrinkSpeedFactor]));
|
||
|
||
prevScrollTop = scrollTop;
|
||
|
||
$mdUtil.nextTick(function() {
|
||
var hasWhiteFrame = element.hasClass('md-whiteframe-z1');
|
||
|
||
if (hasWhiteFrame && !y) {
|
||
$animate.removeClass(element, 'md-whiteframe-z1');
|
||
} else if (!hasWhiteFrame && y) {
|
||
$animate.addClass(element, 'md-whiteframe-z1');
|
||
}
|
||
});
|
||
|
||
}
|
||
|
||
/**
|
||
*
|
||
*/
|
||
function enableScrollShrink() {
|
||
if (!contentElement) return angular.noop; // no md-content
|
||
|
||
contentElement.on('scroll', debouncedContentScroll);
|
||
contentElement.attr('scroll-shrink', 'true');
|
||
|
||
$timeout(updateToolbarHeight);
|
||
|
||
return function disableScrollShrink() {
|
||
contentElement.off('scroll', debouncedContentScroll);
|
||
contentElement.attr('scroll-shrink', 'false');
|
||
|
||
updateToolbarHeight();
|
||
};
|
||
}
|
||
|
||
/**
|
||
*
|
||
*/
|
||
function updateToolbarHeight() {
|
||
toolbarHeight = element.prop('offsetHeight');
|
||
// Add a negative margin-top the size of the toolbar to the content el.
|
||
// The content will start transformed down the toolbarHeight amount,
|
||
// so everything looks normal.
|
||
//
|
||
// As the user scrolls down, the content will be transformed up slowly
|
||
// to put the content underneath where the toolbar was.
|
||
var margin = (-toolbarHeight * shrinkSpeedFactor) + 'px';
|
||
|
||
contentElement.css({
|
||
"margin-top": margin,
|
||
"margin-bottom": margin
|
||
});
|
||
|
||
onContentScroll();
|
||
}
|
||
|
||
}
|
||
|
||
}
|
||
};
|
||
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.tooltip
|
||
*/
|
||
MdTooltipDirective.$inject = ["$timeout", "$window", "$$rAF", "$document", "$interpolate", "$mdUtil", "$mdPanel", "$$mdTooltipRegistry"];
|
||
angular
|
||
.module('material.components.tooltip', [
|
||
'material.core',
|
||
'material.components.panel'
|
||
])
|
||
.directive('mdTooltip', MdTooltipDirective)
|
||
.service('$$mdTooltipRegistry', MdTooltipRegistry);
|
||
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdTooltip
|
||
* @module material.components.tooltip
|
||
* @description
|
||
* Tooltips are used to describe elements that are interactive and primarily
|
||
* graphical (not textual).
|
||
*
|
||
* Place a `<md-tooltip>` as a child of the element it describes.
|
||
*
|
||
* A tooltip will activate when the user hovers over, focuses, or touches the
|
||
* parent element.
|
||
*
|
||
* @usage
|
||
* <hljs lang="html">
|
||
* <md-button class="md-fab md-accent" aria-label="Play">
|
||
* <md-tooltip>Play Music</md-tooltip>
|
||
* <md-icon md-svg-src="img/icons/ic_play_arrow_24px.svg"></md-icon>
|
||
* </md-button>
|
||
* </hljs>
|
||
*
|
||
* @param {number=} md-z-index The visual level that the tooltip will appear
|
||
* in comparison with the rest of the elements of the application.
|
||
* @param {expression=} md-visible Boolean bound to whether the tooltip is
|
||
* currently visible.
|
||
* @param {number=} md-delay How many milliseconds to wait to show the tooltip
|
||
* after the user hovers over, focuses, or touches the parent element.
|
||
* Defaults to 0ms on non-touch devices and 75ms on touch.
|
||
* @param {boolean=} md-autohide If present or provided with a boolean value,
|
||
* the tooltip will hide on mouse leave, regardless of focus.
|
||
* @param {string=} md-direction The direction that the tooltip is shown,
|
||
* relative to the parent element. Supports top, right, bottom, and left.
|
||
* Defaults to bottom.
|
||
*/
|
||
function MdTooltipDirective($timeout, $window, $$rAF, $document, $interpolate,
|
||
$mdUtil, $mdPanel, $$mdTooltipRegistry) {
|
||
|
||
var ENTER_EVENTS = 'focus touchstart mouseenter';
|
||
var LEAVE_EVENTS = 'blur touchcancel mouseleave';
|
||
var TOOLTIP_DEFAULT_Z_INDEX = 100;
|
||
var TOOLTIP_DEFAULT_SHOW_DELAY = 0;
|
||
var TOOLTIP_DEFAULT_DIRECTION = 'bottom';
|
||
var TOOLTIP_DIRECTIONS = {
|
||
top: { x: $mdPanel.xPosition.CENTER, y: $mdPanel.yPosition.ABOVE },
|
||
right: { x: $mdPanel.xPosition.OFFSET_END, y: $mdPanel.yPosition.CENTER },
|
||
bottom: { x: $mdPanel.xPosition.CENTER, y: $mdPanel.yPosition.BELOW },
|
||
left: { x: $mdPanel.xPosition.OFFSET_START, y: $mdPanel.yPosition.CENTER }
|
||
};
|
||
|
||
return {
|
||
restrict: 'E',
|
||
priority: 210, // Before ngAria
|
||
scope: {
|
||
mdZIndex: '=?mdZIndex',
|
||
mdDelay: '=?mdDelay',
|
||
mdVisible: '=?mdVisible',
|
||
mdAutohide: '=?mdAutohide',
|
||
mdDirection: '@?mdDirection' // Do not expect expressions.
|
||
},
|
||
link: linkFunc
|
||
};
|
||
|
||
function linkFunc(scope, element, attr) {
|
||
// Set constants.
|
||
var tooltipId = 'md-tooltip-' + $mdUtil.nextUid();
|
||
var parent = $mdUtil.getParentWithPointerEvents(element);
|
||
var debouncedOnResize = $$rAF.throttle(updatePosition);
|
||
var mouseActive = false;
|
||
var origin, position, panelPosition, panelRef, autohide, showTimeout,
|
||
elementFocusedOnWindowBlur = null;
|
||
|
||
// Set defaults
|
||
setDefaults();
|
||
|
||
// Set parent aria-label.
|
||
addAriaLabel();
|
||
|
||
// Remove the element from its current DOM position.
|
||
element.detach();
|
||
|
||
updatePosition();
|
||
bindEvents();
|
||
configureWatchers();
|
||
|
||
function setDefaults() {
|
||
scope.mdZIndex = scope.mdZIndex || TOOLTIP_DEFAULT_Z_INDEX;
|
||
scope.mdDelay = scope.mdDelay || TOOLTIP_DEFAULT_SHOW_DELAY;
|
||
if (!TOOLTIP_DIRECTIONS[scope.mdDirection]) {
|
||
scope.mdDirection = TOOLTIP_DEFAULT_DIRECTION;
|
||
}
|
||
}
|
||
|
||
function addAriaLabel(labelText) {
|
||
// Only interpolate the text from the HTML element because otherwise the custom text could
|
||
// be interpolated twice and cause XSS violations.
|
||
var interpolatedText = labelText || $interpolate(element.text().trim())(scope.$parent);
|
||
|
||
// Only add the `aria-label` to the parent if there isn't already one, if there isn't an
|
||
// already present `aria-labelledby`, or if the previous `aria-label` was added by the
|
||
// tooltip directive.
|
||
if (
|
||
(!parent.attr('aria-label') && !parent.attr('aria-labelledby')) ||
|
||
parent.attr('md-labeled-by-tooltip')
|
||
) {
|
||
parent.attr('aria-label', interpolatedText);
|
||
|
||
// Set the `md-labeled-by-tooltip` attribute if it has not already been set.
|
||
if (!parent.attr('md-labeled-by-tooltip')) {
|
||
parent.attr('md-labeled-by-tooltip', tooltipId);
|
||
}
|
||
}
|
||
}
|
||
|
||
function updatePosition() {
|
||
setDefaults();
|
||
|
||
// If the panel has already been created, remove the current origin
|
||
// class from the panel element.
|
||
if (panelRef && panelRef.panelEl) {
|
||
panelRef.panelEl.removeClass(origin);
|
||
}
|
||
|
||
// Set the panel element origin class based off of the current
|
||
// mdDirection.
|
||
origin = 'md-origin-' + scope.mdDirection;
|
||
|
||
// Create the position of the panel based off of the mdDirection.
|
||
position = TOOLTIP_DIRECTIONS[scope.mdDirection];
|
||
|
||
// Using the newly created position object, use the MdPanel
|
||
// panelPosition API to build the panel's position.
|
||
panelPosition = $mdPanel.newPanelPosition()
|
||
.relativeTo(parent)
|
||
.addPanelPosition(position.x, position.y);
|
||
|
||
// If the panel has already been created, add the new origin class to
|
||
// the panel element and update it's position with the panelPosition.
|
||
if (panelRef && panelRef.panelEl) {
|
||
panelRef.panelEl.addClass(origin);
|
||
panelRef.updatePosition(panelPosition);
|
||
}
|
||
}
|
||
|
||
function bindEvents() {
|
||
// Add a mutationObserver where there is support for it and the need
|
||
// for it in the form of viable host(parent[0]).
|
||
if (parent[0] && 'MutationObserver' in $window) {
|
||
// Use a mutationObserver to tackle #2602.
|
||
var attributeObserver = new MutationObserver(function(mutations) {
|
||
if (isDisabledMutation(mutations)) {
|
||
$mdUtil.nextTick(function() {
|
||
setVisible(false);
|
||
});
|
||
}
|
||
});
|
||
|
||
attributeObserver.observe(parent[0], {
|
||
attributes: true
|
||
});
|
||
}
|
||
|
||
elementFocusedOnWindowBlur = false;
|
||
|
||
$$mdTooltipRegistry.register('scroll', windowScrollEventHandler, true);
|
||
$$mdTooltipRegistry.register('blur', windowBlurEventHandler);
|
||
$$mdTooltipRegistry.register('resize', debouncedOnResize);
|
||
|
||
scope.$on('$destroy', onDestroy);
|
||
|
||
// To avoid 'synthetic clicks', we listen to mousedown instead of
|
||
// 'click'.
|
||
parent.on('mousedown', mousedownEventHandler);
|
||
parent.on(ENTER_EVENTS, enterEventHandler);
|
||
|
||
function isDisabledMutation(mutations) {
|
||
mutations.some(function(mutation) {
|
||
return mutation.attributeName === 'disabled' && parent[0].disabled;
|
||
});
|
||
return false;
|
||
}
|
||
|
||
function windowScrollEventHandler() {
|
||
setVisible(false);
|
||
}
|
||
|
||
function windowBlurEventHandler() {
|
||
elementFocusedOnWindowBlur = document.activeElement === parent[0];
|
||
}
|
||
|
||
function enterEventHandler($event) {
|
||
// Prevent the tooltip from showing when the window is receiving
|
||
// focus.
|
||
if ($event.type === 'focus' && elementFocusedOnWindowBlur) {
|
||
elementFocusedOnWindowBlur = false;
|
||
} else if (!scope.mdVisible) {
|
||
parent.on(LEAVE_EVENTS, leaveEventHandler);
|
||
setVisible(true);
|
||
|
||
// If the user is on a touch device, we should bind the tap away
|
||
// after the 'touched' in order to prevent the tooltip being
|
||
// removed immediately.
|
||
if ($event.type === 'touchstart') {
|
||
parent.one('touchend', function() {
|
||
$mdUtil.nextTick(function() {
|
||
$document.one('touchend', leaveEventHandler);
|
||
}, false);
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
function leaveEventHandler() {
|
||
autohide = scope.hasOwnProperty('mdAutohide') ?
|
||
scope.mdAutohide :
|
||
attr.hasOwnProperty('mdAutohide');
|
||
|
||
if (autohide || mouseActive ||
|
||
$document[0].activeElement !== parent[0]) {
|
||
// When a show timeout is currently in progress, then we have
|
||
// to cancel it, otherwise the tooltip will remain showing
|
||
// without focus or hover.
|
||
if (showTimeout) {
|
||
$timeout.cancel(showTimeout);
|
||
setVisible.queued = false;
|
||
showTimeout = null;
|
||
}
|
||
|
||
parent.off(LEAVE_EVENTS, leaveEventHandler);
|
||
parent.triggerHandler('blur');
|
||
setVisible(false);
|
||
}
|
||
mouseActive = false;
|
||
}
|
||
|
||
function mousedownEventHandler() {
|
||
mouseActive = true;
|
||
}
|
||
|
||
function onDestroy() {
|
||
$$mdTooltipRegistry.deregister('scroll', windowScrollEventHandler, true);
|
||
$$mdTooltipRegistry.deregister('blur', windowBlurEventHandler);
|
||
$$mdTooltipRegistry.deregister('resize', debouncedOnResize);
|
||
|
||
parent
|
||
.off(ENTER_EVENTS, enterEventHandler)
|
||
.off(LEAVE_EVENTS, leaveEventHandler)
|
||
.off('mousedown', mousedownEventHandler);
|
||
|
||
// Trigger the handler in case any of the tooltips are
|
||
// still visible.
|
||
leaveEventHandler();
|
||
attributeObserver && attributeObserver.disconnect();
|
||
}
|
||
}
|
||
|
||
function configureWatchers() {
|
||
if (element[0] && 'MutationObserver' in $window) {
|
||
var attributeObserver = new MutationObserver(function(mutations) {
|
||
mutations.forEach(function(mutation) {
|
||
if (mutation.attributeName === 'md-visible' &&
|
||
!scope.visibleWatcher) {
|
||
scope.visibleWatcher = scope.$watch('mdVisible',
|
||
onVisibleChanged);
|
||
}
|
||
});
|
||
});
|
||
|
||
attributeObserver.observe(element[0], {
|
||
attributes: true
|
||
});
|
||
|
||
// Build watcher only if mdVisible is being used.
|
||
if (attr.hasOwnProperty('mdVisible')) {
|
||
scope.visibleWatcher = scope.$watch('mdVisible',
|
||
onVisibleChanged);
|
||
}
|
||
} else {
|
||
// MutationObserver not supported
|
||
scope.visibleWatcher = scope.$watch('mdVisible', onVisibleChanged);
|
||
}
|
||
|
||
// Direction watcher
|
||
scope.$watch('mdDirection', updatePosition);
|
||
|
||
// Clean up if the element or parent was removed via jqLite's .remove.
|
||
// A couple of notes:
|
||
// - In these cases the scope might not have been destroyed, which
|
||
// is why we destroy it manually. An example of this can be having
|
||
// `md-visible="false"` and adding tooltips while they're
|
||
// invisible. If `md-visible` becomes true, at some point, you'd
|
||
// usually get a lot of tooltips.
|
||
// - We use `.one`, not `.on`, because this only needs to fire once.
|
||
// If we were using `.on`, it would get thrown into an infinite
|
||
// loop.
|
||
// - This kicks off the scope's `$destroy` event which finishes the
|
||
// cleanup.
|
||
element.one('$destroy', onElementDestroy);
|
||
parent.one('$destroy', onElementDestroy);
|
||
scope.$on('$destroy', function() {
|
||
setVisible(false);
|
||
panelRef && panelRef.destroy();
|
||
attributeObserver && attributeObserver.disconnect();
|
||
element.remove();
|
||
});
|
||
|
||
// Updates the aria-label when the element text changes. This watch
|
||
// doesn't need to be set up if the element doesn't have any data
|
||
// bindings.
|
||
if (element.text().indexOf($interpolate.startSymbol()) > -1) {
|
||
scope.$watch(function() {
|
||
return element.text().trim();
|
||
}, addAriaLabel);
|
||
}
|
||
|
||
function onElementDestroy() {
|
||
scope.$destroy();
|
||
}
|
||
}
|
||
|
||
function setVisible(value) {
|
||
// Break if passed value is already in queue or there is no queue and
|
||
// passed value is current in the controller.
|
||
if (setVisible.queued && setVisible.value === !!value ||
|
||
!setVisible.queued && scope.mdVisible === !!value) {
|
||
return;
|
||
}
|
||
setVisible.value = !!value;
|
||
|
||
if (!setVisible.queued) {
|
||
if (value) {
|
||
setVisible.queued = true;
|
||
showTimeout = $timeout(function() {
|
||
scope.mdVisible = setVisible.value;
|
||
setVisible.queued = false;
|
||
showTimeout = null;
|
||
if (!scope.visibleWatcher) {
|
||
onVisibleChanged(scope.mdVisible);
|
||
}
|
||
}, scope.mdDelay);
|
||
} else {
|
||
$mdUtil.nextTick(function() {
|
||
scope.mdVisible = false;
|
||
if (!scope.visibleWatcher) {
|
||
onVisibleChanged(false);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
function onVisibleChanged(isVisible) {
|
||
isVisible ? showTooltip() : hideTooltip();
|
||
}
|
||
|
||
function showTooltip() {
|
||
// Do not show the tooltip if the text is empty.
|
||
if (!element[0].textContent.trim()) {
|
||
throw new Error('Text for the tooltip has not been provided. ' +
|
||
'Please include text within the mdTooltip element.');
|
||
}
|
||
|
||
if (!panelRef) {
|
||
var attachTo = angular.element(document.body);
|
||
var panelAnimation = $mdPanel.newPanelAnimation()
|
||
.openFrom(parent)
|
||
.closeTo(parent)
|
||
.withAnimation({
|
||
open: 'md-show',
|
||
close: 'md-hide'
|
||
});
|
||
|
||
var panelConfig = {
|
||
id: tooltipId,
|
||
attachTo: attachTo,
|
||
contentElement: element,
|
||
propagateContainerEvents: true,
|
||
panelClass: 'md-tooltip',
|
||
animation: panelAnimation,
|
||
position: panelPosition,
|
||
zIndex: scope.mdZIndex,
|
||
focusOnOpen: false,
|
||
onDomAdded: function() {
|
||
panelRef.panelEl.addClass(origin);
|
||
}
|
||
};
|
||
|
||
panelRef = $mdPanel.create(panelConfig);
|
||
}
|
||
|
||
panelRef.open().then(function() {
|
||
panelRef.panelEl.attr('role', 'tooltip');
|
||
});
|
||
}
|
||
|
||
function hideTooltip() {
|
||
panelRef && panelRef.close();
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
|
||
/**
|
||
* Service that is used to reduce the amount of listeners that are being
|
||
* registered on the `window` by the tooltip component. Works by collecting
|
||
* the individual event handlers and dispatching them from a global handler.
|
||
*
|
||
* @ngInject
|
||
*/
|
||
function MdTooltipRegistry() {
|
||
var listeners = {};
|
||
var ngWindow = angular.element(window);
|
||
|
||
return {
|
||
register: register,
|
||
deregister: deregister
|
||
};
|
||
|
||
/**
|
||
* Global event handler that dispatches the registered handlers in the
|
||
* service.
|
||
* @param {!Event} event Event object passed in by the browser
|
||
*/
|
||
function globalEventHandler(event) {
|
||
if (listeners[event.type]) {
|
||
listeners[event.type].forEach(function(currentHandler) {
|
||
currentHandler.call(this, event);
|
||
}, this);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Registers a new handler with the service.
|
||
* @param {string} type Type of event to be registered.
|
||
* @param {!Function} handler Event handler.
|
||
* @param {boolean} useCapture Whether to use event capturing.
|
||
*/
|
||
function register(type, handler, useCapture) {
|
||
var handlers = listeners[type] = listeners[type] || [];
|
||
|
||
if (!handlers.length) {
|
||
useCapture ? window.addEventListener(type, globalEventHandler, true) :
|
||
ngWindow.on(type, globalEventHandler);
|
||
}
|
||
|
||
if (handlers.indexOf(handler) === -1) {
|
||
handlers.push(handler);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Removes an event handler from the service.
|
||
* @param {string} type Type of event handler.
|
||
* @param {!Function} handler The event handler itself.
|
||
* @param {boolean} useCapture Whether the event handler used event capturing.
|
||
*/
|
||
function deregister(type, handler, useCapture) {
|
||
var handlers = listeners[type];
|
||
var index = handlers ? handlers.indexOf(handler) : -1;
|
||
|
||
if (index > -1) {
|
||
handlers.splice(index, 1);
|
||
|
||
if (handlers.length === 0) {
|
||
useCapture ? window.removeEventListener(type, globalEventHandler, true) :
|
||
ngWindow.off(type, globalEventHandler);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.truncate
|
||
*/
|
||
MdTruncateController.$inject = ["$element"];
|
||
angular.module('material.components.truncate', ['material.core'])
|
||
.directive('mdTruncate', MdTruncateDirective);
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdTruncate
|
||
* @module material.components.truncate
|
||
* @restrict AE
|
||
* @description
|
||
*
|
||
* The `md-truncate` component displays a label that will automatically clip text which is wider
|
||
* than the component. By default, it displays an ellipsis, but you may apply the `md-clip` CSS
|
||
* class to override this default and use a standard "clipping" approach.
|
||
*
|
||
* <i><b>Note:</b> The `md-truncate` component does not automatically adjust it's width. You must
|
||
* provide the `flex` attribute, or some other CSS-based width management. See the
|
||
* <a ng-href="./demo/truncate">demos</a> for examples.</i>
|
||
*
|
||
* @usage
|
||
*
|
||
* ### As an Element
|
||
*
|
||
* <hljs lang="html">
|
||
* <div layout="row">
|
||
* <md-button>Back</md-button>
|
||
*
|
||
* <md-truncate flex>Chapter 1 - The Way of the Old West</md-truncate>
|
||
*
|
||
* <md-button>Forward</md-button>
|
||
* </div>
|
||
* </hljs>
|
||
*
|
||
* ### As an Attribute
|
||
*
|
||
* <hljs lang="html">
|
||
* <h2 md-truncate style="max-width: 100px;">Some Title With a Lot of Text</h2>
|
||
* </hljs>
|
||
*
|
||
* ## CSS & Styles
|
||
*
|
||
* `<md-truncate>` provides two CSS classes that you may use to control the type of clipping.
|
||
*
|
||
* <i><b>Note:</b> The `md-truncate` also applies a setting of `width: 0;` when used with the `flex`
|
||
* attribute to fix an issue with the flex element not shrinking properly.</i>
|
||
*
|
||
* <div>
|
||
* <docs-css-api-table>
|
||
*
|
||
* <docs-css-selector code=".md-ellipsis">
|
||
* Assigns the "ellipsis" behavior (default) which will cut off mid-word and append an ellipsis
|
||
* (…) to the end of the text.
|
||
* </docs-css-selector>
|
||
*
|
||
* <docs-css-selector code=".md-clip">
|
||
* Assigns the "clipping" behavior which will simply chop off the text. This may happen
|
||
* mid-word or even mid-character.
|
||
* </docs-css-selector>
|
||
*
|
||
* </docs-css-api-table>
|
||
* </div>
|
||
*/
|
||
function MdTruncateDirective() {
|
||
return {
|
||
restrict: 'AE',
|
||
|
||
controller: MdTruncateController
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Controller for the <md-truncate> component.
|
||
*
|
||
* @param $element The md-truncate element.
|
||
*
|
||
* @constructor
|
||
* @ngInject
|
||
*/
|
||
function MdTruncateController($element) {
|
||
$element.addClass('md-truncate');
|
||
}
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.virtualRepeat
|
||
*/
|
||
VirtualRepeatContainerController.$inject = ["$$rAF", "$mdUtil", "$mdConstant", "$parse", "$rootScope", "$window", "$scope", "$element", "$attrs"];
|
||
VirtualRepeatController.$inject = ["$scope", "$element", "$attrs", "$browser", "$document", "$rootScope", "$$rAF", "$mdUtil"];
|
||
VirtualRepeatDirective.$inject = ["$parse"];
|
||
angular.module('material.components.virtualRepeat', [
|
||
'material.core',
|
||
'material.components.showHide'
|
||
])
|
||
.directive('mdVirtualRepeatContainer', VirtualRepeatContainerDirective)
|
||
.directive('mdVirtualRepeat', VirtualRepeatDirective)
|
||
.directive('mdForceHeight', ForceHeightDirective);
|
||
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdVirtualRepeatContainer
|
||
* @module material.components.virtualRepeat
|
||
* @restrict E
|
||
* @description
|
||
* `md-virtual-repeat-container` provides the scroll container for
|
||
* <a ng-href="api/directive/mdVirtualRepeat">md-virtual-repeat</a>.
|
||
*
|
||
* VirtualRepeat is a limited substitute for `ng-repeat` that renders only
|
||
* enough DOM nodes to fill the container, recycling them as the user scrolls.
|
||
*
|
||
* Once an element is not visible anymore, the Virtual Repeat recycles the element and reuses it
|
||
* for another visible item by replacing the previous data set with the set of currently visible
|
||
* elements.
|
||
*
|
||
* ### Common Issues
|
||
*
|
||
* - When having one-time bindings inside of the view template, the Virtual Repeat will not properly
|
||
* update the bindings for new items, since the view will be recycled.
|
||
* - Directives inside of a Virtual Repeat will be only compiled (linked) once, because those
|
||
* items will be recycled and used for other items.
|
||
* The Virtual Repeat just updates the scope bindings.
|
||
*
|
||
*
|
||
* ### Notes
|
||
*
|
||
* > The VirtualRepeat is a similar implementation to the Android
|
||
* [RecyclerView](https://developer.android.com/reference/android/support/v7/widget/RecyclerView.html).
|
||
*
|
||
* <!-- This comment forces a break between blockquotes //-->
|
||
*
|
||
* > Please also review the <a ng-href="api/directive/mdVirtualRepeat">mdVirtualRepeat</a>
|
||
* documentation for more information.
|
||
*
|
||
*
|
||
* @usage
|
||
* <hljs lang="html">
|
||
*
|
||
* <md-virtual-repeat-container md-top-index="topIndex">
|
||
* <div md-virtual-repeat="i in items" md-item-size="20">Hello {{i}}!</div>
|
||
* </md-virtual-repeat-container>
|
||
* </hljs>
|
||
*
|
||
* @param {boolean=} md-auto-shrink When present and the container will shrink to fit
|
||
* the number of items in the `md-virtual-repeat`.
|
||
* @param {number=} md-auto-shrink-min Minimum number of items that md-auto-shrink
|
||
* will shrink to. Default: `0`.
|
||
* @param {boolean=} md-orient-horizontal Whether the container should scroll horizontally.
|
||
* The default is `false` which indicates vertical orientation and scrolling.
|
||
* @param {number=} md-top-index Binds the index of the item that is at the top of the scroll
|
||
* container to `$scope`. It can both read and set the scroll position.
|
||
*/
|
||
function VirtualRepeatContainerDirective() {
|
||
return {
|
||
controller: VirtualRepeatContainerController,
|
||
template: virtualRepeatContainerTemplate,
|
||
compile: function virtualRepeatContainerCompile($element, $attrs) {
|
||
$element
|
||
.addClass('md-virtual-repeat-container')
|
||
.addClass($attrs.hasOwnProperty('mdOrientHorizontal')
|
||
? 'md-orient-horizontal'
|
||
: 'md-orient-vertical');
|
||
}
|
||
};
|
||
}
|
||
|
||
|
||
function virtualRepeatContainerTemplate($element) {
|
||
return '<div class="md-virtual-repeat-scroller" role="presentation">' +
|
||
'<div class="md-virtual-repeat-sizer" role="presentation"></div>' +
|
||
'<div class="md-virtual-repeat-offsetter" role="presentation">' +
|
||
$element[0].innerHTML +
|
||
'</div></div>';
|
||
}
|
||
|
||
/**
|
||
* Number of additional elements to render above and below the visible area inside
|
||
* of the virtual repeat container. A higher number results in less flicker when scrolling
|
||
* very quickly in Safari, but comes with a higher rendering and dirty-checking cost.
|
||
* @const {number}
|
||
*/
|
||
var NUM_EXTRA = 3;
|
||
|
||
/** @ngInject */
|
||
function VirtualRepeatContainerController($$rAF, $mdUtil, $mdConstant, $parse, $rootScope, $window,
|
||
$scope, $element, $attrs) {
|
||
this.$rootScope = $rootScope;
|
||
this.$scope = $scope;
|
||
this.$element = $element;
|
||
this.$attrs = $attrs;
|
||
|
||
/** @type {number} The width or height of the container */
|
||
this.size = 0;
|
||
/** @type {number} The scroll width or height of the scroller */
|
||
this.scrollSize = 0;
|
||
/** @type {number} The scrollLeft or scrollTop of the scroller */
|
||
this.scrollOffset = 0;
|
||
/** @type {boolean} Whether the scroller is oriented horizontally */
|
||
this.horizontal = this.$attrs.hasOwnProperty('mdOrientHorizontal');
|
||
/** @type {!VirtualRepeatController} The repeater inside of this container */
|
||
this.repeater = null;
|
||
/** @type {boolean} Whether auto-shrink is enabled */
|
||
this.autoShrink = this.$attrs.hasOwnProperty('mdAutoShrink');
|
||
/** @type {number} Minimum number of items to auto-shrink to */
|
||
this.autoShrinkMin = parseInt(this.$attrs.mdAutoShrinkMin, 10) || 0;
|
||
/** @type {?number} Original container size when shrank */
|
||
this.originalSize = null;
|
||
/** @type {number} Amount to offset the total scroll size by. */
|
||
this.offsetSize = parseInt(this.$attrs.mdOffsetSize, 10) || 0;
|
||
/** @type {?string} height or width element style on the container prior to auto-shrinking. */
|
||
this.oldElementSize = null;
|
||
/** @type {!number} Maximum amount of pixels allowed for a single DOM element */
|
||
this.maxElementPixels = $mdConstant.ELEMENT_MAX_PIXELS;
|
||
/** @type {string} Direction of the text */
|
||
this.ltr = !$mdUtil.isRtl(this.$attrs);
|
||
|
||
if (this.$attrs.mdTopIndex) {
|
||
/** @type {function(angular.Scope): number} Binds to topIndex on AngularJS scope */
|
||
this.bindTopIndex = $parse(this.$attrs.mdTopIndex);
|
||
/** @type {number} The index of the item that is at the top of the scroll container */
|
||
this.topIndex = this.bindTopIndex(this.$scope);
|
||
|
||
if (!angular.isDefined(this.topIndex)) {
|
||
this.topIndex = 0;
|
||
this.bindTopIndex.assign(this.$scope, 0);
|
||
}
|
||
|
||
this.$scope.$watch(this.bindTopIndex, angular.bind(this, function(newIndex) {
|
||
if (newIndex !== this.topIndex) {
|
||
this.scrollToIndex(newIndex);
|
||
}
|
||
}));
|
||
} else {
|
||
this.topIndex = 0;
|
||
}
|
||
|
||
this.scroller = $element[0].querySelector('.md-virtual-repeat-scroller');
|
||
this.sizer = this.scroller.querySelector('.md-virtual-repeat-sizer');
|
||
this.offsetter = this.scroller.querySelector('.md-virtual-repeat-offsetter');
|
||
|
||
// After the DOM stabilizes, measure the initial size of the container and
|
||
// make a best effort at re-measuring as it changes.
|
||
var boundUpdateSize = angular.bind(this, this.updateSize);
|
||
|
||
$$rAF(angular.bind(this, function() {
|
||
boundUpdateSize();
|
||
|
||
var debouncedUpdateSize = $mdUtil.debounce(boundUpdateSize, 10, null, false);
|
||
var jWindow = angular.element($window);
|
||
|
||
// Make one more attempt to get the size if it is 0.
|
||
// This is not by any means a perfect approach, but there's really no
|
||
// silver bullet here.
|
||
if (!this.size) {
|
||
debouncedUpdateSize();
|
||
}
|
||
|
||
jWindow.on('resize', debouncedUpdateSize);
|
||
$scope.$on('$destroy', function() {
|
||
jWindow.off('resize', debouncedUpdateSize);
|
||
});
|
||
|
||
$scope.$emit('$md-resize-enable');
|
||
$scope.$on('$md-resize', boundUpdateSize);
|
||
}));
|
||
}
|
||
|
||
|
||
/** Called by the md-virtual-repeat inside of the container at startup. */
|
||
VirtualRepeatContainerController.prototype.register = function(repeaterCtrl) {
|
||
this.repeater = repeaterCtrl;
|
||
|
||
angular.element(this.scroller)
|
||
.on('scroll wheel touchmove touchend', angular.bind(this, this.handleScroll_));
|
||
};
|
||
|
||
|
||
/** @return {boolean} Whether the container is configured for horizontal scrolling. */
|
||
VirtualRepeatContainerController.prototype.isHorizontal = function() {
|
||
return this.horizontal;
|
||
};
|
||
|
||
|
||
/** @return {number} The size (width or height) of the container. */
|
||
VirtualRepeatContainerController.prototype.getSize = function() {
|
||
return this.size;
|
||
};
|
||
|
||
|
||
/**
|
||
* Resizes the container.
|
||
* @private
|
||
* @param {number} size The new size to set.
|
||
*/
|
||
VirtualRepeatContainerController.prototype.setSize_ = function(size) {
|
||
var dimension = this.getDimensionName_();
|
||
|
||
this.size = size;
|
||
this.$element[0].style[dimension] = size + 'px';
|
||
};
|
||
|
||
|
||
VirtualRepeatContainerController.prototype.unsetSize_ = function() {
|
||
this.$element[0].style[this.getDimensionName_()] = this.oldElementSize;
|
||
this.oldElementSize = null;
|
||
};
|
||
|
||
|
||
/** Instructs the container to re-measure its size. */
|
||
VirtualRepeatContainerController.prototype.updateSize = function() {
|
||
// If the original size is already determined, we can skip the update.
|
||
if (this.originalSize) return;
|
||
|
||
var size = this.isHorizontal()
|
||
? this.$element[0].clientWidth
|
||
: this.$element[0].clientHeight;
|
||
|
||
if (size) {
|
||
this.size = size;
|
||
}
|
||
|
||
// Recheck the scroll position after updating the size. This resolves
|
||
// problems that can result if the scroll position was measured while the
|
||
// element was display: none or detached from the document.
|
||
this.handleScroll_();
|
||
|
||
this.repeater && this.repeater.containerUpdated();
|
||
};
|
||
|
||
|
||
/** @return {number} The container's scrollHeight or scrollWidth. */
|
||
VirtualRepeatContainerController.prototype.getScrollSize = function() {
|
||
return this.scrollSize;
|
||
};
|
||
|
||
/**
|
||
* @returns {string} either width or height dimension
|
||
* @private
|
||
*/
|
||
VirtualRepeatContainerController.prototype.getDimensionName_ = function() {
|
||
return this.isHorizontal() ? 'width' : 'height';
|
||
};
|
||
|
||
|
||
/**
|
||
* Sets the scroller element to the specified size.
|
||
* @private
|
||
* @param {number} size The new size.
|
||
*/
|
||
VirtualRepeatContainerController.prototype.sizeScroller_ = function(size) {
|
||
var dimension = this.getDimensionName_();
|
||
var crossDimension = this.isHorizontal() ? 'height' : 'width';
|
||
|
||
// Clear any existing dimensions.
|
||
this.sizer.innerHTML = '';
|
||
|
||
// If the size falls within the browser's maximum explicit size for a single element, we can
|
||
// set the size and be done. Otherwise, we have to create children that add up the the desired
|
||
// size.
|
||
if (size < this.maxElementPixels) {
|
||
this.sizer.style[dimension] = size + 'px';
|
||
} else {
|
||
this.sizer.style[dimension] = 'auto';
|
||
this.sizer.style[crossDimension] = 'auto';
|
||
|
||
// Divide the total size we have to render into N max-size pieces.
|
||
var numChildren = Math.floor(size / this.maxElementPixels);
|
||
|
||
// Element template to clone for each max-size piece.
|
||
var sizerChild = document.createElement('div');
|
||
sizerChild.style[dimension] = this.maxElementPixels + 'px';
|
||
sizerChild.style[crossDimension] = '1px';
|
||
|
||
for (var i = 0; i < numChildren; i++) {
|
||
this.sizer.appendChild(sizerChild.cloneNode(false));
|
||
}
|
||
|
||
// Re-use the element template for the remainder.
|
||
sizerChild.style[dimension] = (size - (numChildren * this.maxElementPixels)) + 'px';
|
||
this.sizer.appendChild(sizerChild);
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* If auto-shrinking is enabled, shrinks or unshrinks as appropriate.
|
||
* @private
|
||
* @param {number} size The new size.
|
||
*/
|
||
VirtualRepeatContainerController.prototype.autoShrink_ = function(size) {
|
||
var shrinkSize = Math.max(size, this.autoShrinkMin * this.repeater.getItemSize());
|
||
|
||
if (this.autoShrink && shrinkSize !== this.size) {
|
||
if (this.oldElementSize === null) {
|
||
this.oldElementSize = this.$element[0].style[this.getDimensionName_()];
|
||
}
|
||
|
||
var currentSize = this.originalSize || this.size;
|
||
|
||
if (!currentSize || shrinkSize < currentSize) {
|
||
if (!this.originalSize) {
|
||
this.originalSize = this.size;
|
||
}
|
||
|
||
// Now we update the containers size, because shrinking is enabled.
|
||
this.setSize_(shrinkSize);
|
||
} else if (this.originalSize !== null) {
|
||
// Set the size back to our initial size.
|
||
this.unsetSize_();
|
||
|
||
var _originalSize = this.originalSize;
|
||
this.originalSize = null;
|
||
|
||
// We determine the repeaters size again, if the original size was zero.
|
||
// The originalSize needs to be null, to be able to determine the size.
|
||
if (!_originalSize) this.updateSize();
|
||
|
||
// Apply the original size or the determined size back to the container, because
|
||
// it has been overwritten before, in the shrink block.
|
||
this.setSize_(_originalSize || this.size);
|
||
}
|
||
|
||
this.repeater.containerUpdated();
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* Sets the scrollHeight or scrollWidth. Called by the repeater based on
|
||
* its item count and item size.
|
||
* @param {number} itemsSize The total size of the items.
|
||
*/
|
||
VirtualRepeatContainerController.prototype.setScrollSize = function(itemsSize) {
|
||
var size = itemsSize + this.offsetSize;
|
||
if (this.scrollSize === size) return;
|
||
|
||
this.sizeScroller_(size);
|
||
this.autoShrink_(size);
|
||
this.scrollSize = size;
|
||
};
|
||
|
||
|
||
/** @return {number} The container's current scroll offset. */
|
||
VirtualRepeatContainerController.prototype.getScrollOffset = function() {
|
||
return this.scrollOffset;
|
||
};
|
||
|
||
/**
|
||
* Scrolls to a given scrollTop position.
|
||
* @param {number} position
|
||
*/
|
||
VirtualRepeatContainerController.prototype.scrollTo = function(position) {
|
||
this.scroller[this.isHorizontal() ? 'scrollLeft' : 'scrollTop'] = position;
|
||
this.handleScroll_();
|
||
};
|
||
|
||
/**
|
||
* Scrolls the item with the given index to the top of the scroll container.
|
||
* @param {number} index
|
||
*/
|
||
VirtualRepeatContainerController.prototype.scrollToIndex = function(index) {
|
||
var itemSize = this.repeater.getItemSize();
|
||
var itemsLength = this.repeater.itemsLength;
|
||
if (index > itemsLength) {
|
||
index = itemsLength - 1;
|
||
}
|
||
this.scrollTo(itemSize * index);
|
||
};
|
||
|
||
VirtualRepeatContainerController.prototype.resetScroll = function() {
|
||
this.scrollTo(0);
|
||
};
|
||
|
||
|
||
VirtualRepeatContainerController.prototype.handleScroll_ = function() {
|
||
if (!this.ltr && !this.maxSize) {
|
||
this.scroller.scrollLeft = this.scrollSize;
|
||
this.maxSize = this.scroller.scrollLeft;
|
||
}
|
||
var offset = this.isHorizontal() ?
|
||
(this.ltr ? this.scroller.scrollLeft : this.maxSize - this.scroller.scrollLeft)
|
||
: this.scroller.scrollTop;
|
||
if (this.scrollSize < this.size) return;
|
||
if (offset > this.scrollSize - this.size) {
|
||
offset = this.scrollSize - this.size;
|
||
}
|
||
if (offset === this.scrollOffset) return;
|
||
|
||
var itemSize = this.repeater.getItemSize();
|
||
if (!itemSize) return;
|
||
|
||
var numItems = Math.max(0, Math.floor(offset / itemSize) - NUM_EXTRA);
|
||
|
||
var transform = (this.isHorizontal() ? 'translateX(' : 'translateY(') +
|
||
(!this.isHorizontal() || this.ltr ? (numItems * itemSize) : - (numItems * itemSize)) + 'px)';
|
||
|
||
this.scrollOffset = offset;
|
||
this.offsetter.style.webkitTransform = transform;
|
||
this.offsetter.style.transform = transform;
|
||
|
||
if (this.bindTopIndex) {
|
||
var topIndex = Math.floor(offset / itemSize);
|
||
if (topIndex !== this.topIndex && topIndex < this.repeater.getItemCount()) {
|
||
this.topIndex = topIndex;
|
||
this.bindTopIndex.assign(this.$scope, topIndex);
|
||
if (!this.$rootScope.$$phase) this.$scope.$digest();
|
||
}
|
||
}
|
||
|
||
this.repeater.containerUpdated();
|
||
};
|
||
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdVirtualRepeat
|
||
* @module material.components.virtualRepeat
|
||
* @restrict A
|
||
* @priority 1000
|
||
* @description
|
||
* The `md-virtual-repeat` attribute is applied to a template that is repeated using virtual
|
||
* scrolling. This provides smooth and performant scrolling through very large lists of elements.
|
||
*
|
||
* Virtual repeat is a limited substitute for `ng-repeat` that renders only
|
||
* enough DOM nodes to fill the container, recycling them as the user scrolls.
|
||
*
|
||
* ### Notes
|
||
*
|
||
* - Arrays are supported for iteration by default.
|
||
* - An object can used use if `md-on-demand` is specified and the object implements the interface
|
||
* described in the `md-on-demand` <a ng-href="#attributes">documentation</a>.
|
||
* - `trackBy`, `as` alias, and `(key, value)` syntax from `ng-repeat` are not supported.
|
||
*
|
||
* ### On-Demand Async Item Loading
|
||
*
|
||
* When using the `md-on-demand` attribute and loading some asynchronous data,
|
||
* the `getItemAtIndex` function will mostly return nothing.
|
||
*
|
||
* <hljs lang="js">
|
||
* DynamicItems.prototype.getItemAtIndex = function(index) {
|
||
* if (this.pages[index]) {
|
||
* return this.pages[index];
|
||
* } else {
|
||
* // This is an asynchronous action and does not return any value.
|
||
* this.loadPage(index);
|
||
* }
|
||
* };
|
||
* </hljs>
|
||
*
|
||
* This means that the Virtual Repeat will not have any value for the given index.<br/>
|
||
* After the data loading completes, the user expects the Virtual Repeat to recognize the change.
|
||
*
|
||
* To make sure that the Virtual Repeat properly detects any change, you need to run the operation
|
||
* in another digest.
|
||
*
|
||
* <hljs lang="js">
|
||
* DynamicItems.prototype.loadPage = function(index) {
|
||
* var self = this;
|
||
*
|
||
* // Trigger a new digest by using $timeout
|
||
* $timeout(function() {
|
||
* self.pages[index] = Data;
|
||
* });
|
||
* };
|
||
* </hljs>
|
||
*
|
||
* > <b>Note:</b> Please also review the
|
||
* <a ng-href="api/directive/mdVirtualRepeatContainer">VirtualRepeatContainer</a> documentation
|
||
* for more information.
|
||
*
|
||
* @usage
|
||
* <hljs lang="html">
|
||
* <md-virtual-repeat-container>
|
||
* <div md-virtual-repeat="i in items">Hello {{i}}!</div>
|
||
* </md-virtual-repeat-container>
|
||
*
|
||
* <md-virtual-repeat-container md-orient-horizontal>
|
||
* <div md-virtual-repeat="i in items" md-item-size="20">Hello {{i}}!</div>
|
||
* </md-virtual-repeat-container>
|
||
* </hljs>
|
||
*
|
||
* @param {expression=} md-extra-name Evaluates to an additional name to which the current iterated
|
||
* item can be assigned on the repeated scope (needed for use in `md-autocomplete`).
|
||
* @param {number=} md-item-size Optional height or width of the repeated elements (which **must be
|
||
* identical for each element**). Virtual repeat will attempt to read the size from the DOM,
|
||
* if missing, but it still assumes that all repeated nodes have the **same height**
|
||
* (when scrolling vertically) or **same width** (when scrolling horizontally).
|
||
* @param {boolean=} md-on-demand When present, treats the `md-virtual-repeat` argument as an object
|
||
* that can fetch rows rather than an array.
|
||
*
|
||
* **NOTE:** This object **must** implement the following interface with two methods:
|
||
*
|
||
* - `getItemAtIndex` - `{function(index): Object}`: The item at that `index` or `null` if it is
|
||
* not yet loaded (it should start downloading the item in that case).
|
||
* - `getLength` - `{function(): number}`: The data length to which the repeater container
|
||
* should be sized. Ideally, when the count is known, this method should return it.
|
||
* Otherwise, return a higher number than the currently loaded items to produce an
|
||
* infinite-scroll behavior.
|
||
*/
|
||
function VirtualRepeatDirective($parse) {
|
||
return {
|
||
controller: VirtualRepeatController,
|
||
priority: 1000,
|
||
require: ['mdVirtualRepeat', '^^mdVirtualRepeatContainer'],
|
||
restrict: 'A',
|
||
terminal: true,
|
||
transclude: 'element',
|
||
compile: function VirtualRepeatCompile($element, $attrs) {
|
||
var expression = $attrs.mdVirtualRepeat;
|
||
var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)\s*$/);
|
||
var repeatName = match[1];
|
||
var repeatListExpression = $parse(match[2]);
|
||
var extraName = $attrs.mdExtraName && $parse($attrs.mdExtraName);
|
||
|
||
return function VirtualRepeatLink($scope, $element, $attrs, ctrl, $transclude) {
|
||
ctrl[0].link_(ctrl[1], $transclude, repeatName, repeatListExpression, extraName);
|
||
};
|
||
}
|
||
};
|
||
}
|
||
|
||
|
||
/** @ngInject */
|
||
function VirtualRepeatController($scope, $element, $attrs, $browser, $document, $rootScope,
|
||
$$rAF, $mdUtil) {
|
||
this.$scope = $scope;
|
||
this.$element = $element;
|
||
this.$attrs = $attrs;
|
||
this.$browser = $browser;
|
||
this.$document = $document;
|
||
this.$mdUtil = $mdUtil;
|
||
this.$rootScope = $rootScope;
|
||
this.$$rAF = $$rAF;
|
||
|
||
/** @type {boolean} Whether we are in on-demand mode. */
|
||
this.onDemand = $mdUtil.parseAttributeBoolean($attrs.mdOnDemand);
|
||
/** @type {!Function} Backup reference to $browser.$$checkUrlChange */
|
||
this.browserCheckUrlChange = $browser.$$checkUrlChange;
|
||
/** @type {number} Most recent starting repeat index (based on scroll offset) */
|
||
this.newStartIndex = 0;
|
||
/** @type {number} Most recent ending repeat index (based on scroll offset) */
|
||
this.newEndIndex = 0;
|
||
/** @type {number} Most recent end visible index (based on scroll offset) */
|
||
this.newVisibleEnd = 0;
|
||
/** @type {number} Previous starting repeat index (based on scroll offset) */
|
||
this.startIndex = 0;
|
||
/** @type {number} Previous ending repeat index (based on scroll offset) */
|
||
this.endIndex = 0;
|
||
/** @type {?number} Height/width of repeated elements. */
|
||
this.itemSize = $scope.$eval($attrs.mdItemSize) || null;
|
||
|
||
/** @type {boolean} Whether this is the first time that items are rendered. */
|
||
this.isFirstRender = true;
|
||
|
||
/**
|
||
* @private {boolean} Whether the items in the list are already being updated. Used to prevent
|
||
* nested calls to virtualRepeatUpdate_.
|
||
*/
|
||
this.isVirtualRepeatUpdating_ = false;
|
||
|
||
/** @type {number} Most recently seen length of items. */
|
||
this.itemsLength = 0;
|
||
|
||
/**
|
||
* @type {!Function} Unwatch callback for item size (when md-items-size is
|
||
* not specified), or angular.noop otherwise.
|
||
*/
|
||
this.unwatchItemSize_ = angular.noop;
|
||
|
||
/**
|
||
* Presently rendered blocks by repeat index.
|
||
* @type {Object<number, !VirtualRepeatController.Block>}
|
||
*/
|
||
this.blocks = {};
|
||
/** @type {Array<!VirtualRepeatController.Block>} A pool of presently unused blocks. */
|
||
this.pooledBlocks = [];
|
||
|
||
$scope.$on('$destroy', angular.bind(this, this.cleanupBlocks_));
|
||
}
|
||
|
||
|
||
/**
|
||
* An object representing a repeated item.
|
||
* @typedef {{element: !jqLite, new: boolean, scope: !angular.Scope}}
|
||
*/
|
||
VirtualRepeatController.Block;
|
||
|
||
|
||
/**
|
||
* Called at startup by the md-virtual-repeat postLink function.
|
||
* @param {!VirtualRepeatContainerController} container The container's controller.
|
||
* @param {!Function} transclude The repeated element's bound transclude function.
|
||
* @param {string} repeatName The left hand side of the repeat expression, indicating
|
||
* the name for each item in the array.
|
||
* @param {!Function} repeatListExpression A compiled expression based on the right hand side
|
||
* of the repeat expression. Points to the array to repeat over.
|
||
* @param {string|undefined} extraName The optional extra repeatName.
|
||
*/
|
||
VirtualRepeatController.prototype.link_ =
|
||
function(container, transclude, repeatName, repeatListExpression, extraName) {
|
||
this.container = container;
|
||
this.transclude = transclude;
|
||
this.repeatName = repeatName;
|
||
this.rawRepeatListExpression = repeatListExpression;
|
||
this.extraName = extraName;
|
||
this.sized = false;
|
||
|
||
this.repeatListExpression = angular.bind(this, this.repeatListExpression_);
|
||
|
||
this.container.register(this);
|
||
};
|
||
|
||
|
||
/** @private Cleans up unused blocks. */
|
||
VirtualRepeatController.prototype.cleanupBlocks_ = function() {
|
||
angular.forEach(this.pooledBlocks, function cleanupBlock(block) {
|
||
block.element.remove();
|
||
});
|
||
};
|
||
|
||
|
||
/** @private Attempts to set itemSize by measuring a repeated element in the dom */
|
||
VirtualRepeatController.prototype.readItemSize_ = function() {
|
||
if (this.itemSize) {
|
||
// itemSize was successfully read in a different asynchronous call.
|
||
return;
|
||
}
|
||
|
||
this.items = this.repeatListExpression(this.$scope);
|
||
this.parentNode = this.$element[0].parentNode;
|
||
var block = this.getBlock_(0);
|
||
if (!block.element[0].parentNode) {
|
||
this.parentNode.appendChild(block.element[0]);
|
||
}
|
||
|
||
this.itemSize = block.element[0][
|
||
this.container.isHorizontal() ? 'offsetWidth' : 'offsetHeight'] || null;
|
||
|
||
this.blocks[0] = block;
|
||
this.poolBlock_(0);
|
||
|
||
if (this.itemSize) {
|
||
this.containerUpdated();
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* Returns the user-specified repeat list, transforming it into an array-like
|
||
* object in the case of infinite scroll/dynamic load mode.
|
||
* @param {!angular.Scope} scope The scope.
|
||
* @return {!Array|!Object} An array or array-like object for iteration.
|
||
*/
|
||
VirtualRepeatController.prototype.repeatListExpression_ = function(scope) {
|
||
var repeatList = this.rawRepeatListExpression(scope);
|
||
|
||
if (this.onDemand && repeatList) {
|
||
var virtualList = new VirtualRepeatModelArrayLike(repeatList);
|
||
virtualList.$$includeIndexes(this.newStartIndex, this.newVisibleEnd);
|
||
return virtualList;
|
||
} else {
|
||
return repeatList;
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* Called by the container. Informs us that the container's scroll or size has
|
||
* changed.
|
||
*/
|
||
VirtualRepeatController.prototype.containerUpdated = function() {
|
||
// If itemSize is unknown, attempt to measure it.
|
||
if (!this.itemSize) {
|
||
// Make sure to clean up watchers if we can (see #8178)
|
||
if (this.unwatchItemSize_ && this.unwatchItemSize_ !== angular.noop){
|
||
this.unwatchItemSize_();
|
||
}
|
||
this.unwatchItemSize_ = this.$scope.$watchCollection(
|
||
this.repeatListExpression,
|
||
angular.bind(this, function(items) {
|
||
if (items && items.length) {
|
||
this.readItemSize_();
|
||
}
|
||
}));
|
||
if (!this.$rootScope.$$phase) this.$scope.$digest();
|
||
|
||
return;
|
||
} else if (!this.sized) {
|
||
this.items = this.repeatListExpression(this.$scope);
|
||
}
|
||
|
||
if (!this.sized) {
|
||
this.unwatchItemSize_();
|
||
this.sized = true;
|
||
this.$scope.$watchCollection(this.repeatListExpression,
|
||
angular.bind(this, function(items, oldItems) {
|
||
if (!this.isVirtualRepeatUpdating_) {
|
||
this.virtualRepeatUpdate_(items, oldItems);
|
||
}
|
||
}));
|
||
}
|
||
|
||
this.updateIndexes_();
|
||
|
||
if (this.newStartIndex !== this.startIndex ||
|
||
this.newEndIndex !== this.endIndex ||
|
||
this.container.getScrollOffset() > this.container.getScrollSize()) {
|
||
if (this.items instanceof VirtualRepeatModelArrayLike) {
|
||
this.items.$$includeIndexes(this.newStartIndex, this.newEndIndex);
|
||
}
|
||
this.virtualRepeatUpdate_(this.items, this.items);
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* Called by the container. Returns the size of a single repeated item.
|
||
* @return {?number} size of a repeated item.
|
||
*/
|
||
VirtualRepeatController.prototype.getItemSize = function() {
|
||
return this.itemSize;
|
||
};
|
||
|
||
|
||
/**
|
||
* Called by the container.
|
||
* @return {?number} the most recently seen length of items.
|
||
*/
|
||
VirtualRepeatController.prototype.getItemCount = function() {
|
||
return this.itemsLength;
|
||
};
|
||
|
||
|
||
/**
|
||
* Updates the order and visible offset of repeated blocks in response to scrolling
|
||
* or updates to `items`.
|
||
* @param {Array} items visible elements
|
||
* @param {Array} oldItems previously visible elements
|
||
* @private
|
||
*/
|
||
VirtualRepeatController.prototype.virtualRepeatUpdate_ = function(items, oldItems) {
|
||
this.isVirtualRepeatUpdating_ = true;
|
||
|
||
var itemsLength = items && items.length || 0;
|
||
var lengthChanged = false;
|
||
|
||
// If the number of items shrank, keep the scroll position.
|
||
if (this.items && itemsLength < this.items.length && this.container.getScrollOffset() !== 0) {
|
||
this.items = items;
|
||
var previousScrollOffset = this.container.getScrollOffset();
|
||
this.container.resetScroll();
|
||
this.container.scrollTo(previousScrollOffset);
|
||
}
|
||
|
||
if (itemsLength !== this.itemsLength) {
|
||
lengthChanged = true;
|
||
this.itemsLength = itemsLength;
|
||
}
|
||
|
||
this.items = items;
|
||
if (items !== oldItems || lengthChanged) {
|
||
this.updateIndexes_();
|
||
}
|
||
|
||
this.parentNode = this.$element[0].parentNode;
|
||
|
||
if (lengthChanged) {
|
||
this.container.setScrollSize(itemsLength * this.itemSize);
|
||
}
|
||
|
||
// Detach and pool any blocks that are no longer in the viewport.
|
||
Object.keys(this.blocks).forEach(function(blockIndex) {
|
||
var index = parseInt(blockIndex, 10);
|
||
if (index < this.newStartIndex || index >= this.newEndIndex) {
|
||
this.poolBlock_(index);
|
||
}
|
||
}, this);
|
||
|
||
// Add needed blocks.
|
||
// For performance reasons, temporarily block browser url checks as we digest
|
||
// the restored block scopes ($$checkUrlChange reads window.location to
|
||
// check for changes and trigger route change, etc, which we don't need when
|
||
// trying to scroll at 60fps).
|
||
this.$browser.$$checkUrlChange = angular.noop;
|
||
|
||
var i, block,
|
||
newStartBlocks = [],
|
||
newEndBlocks = [];
|
||
|
||
// Collect blocks at the top.
|
||
for (i = this.newStartIndex; i < this.newEndIndex && this.blocks[i] == null; i++) {
|
||
block = this.getBlock_(i);
|
||
this.updateBlock_(block, i);
|
||
newStartBlocks.push(block);
|
||
}
|
||
|
||
// Update blocks that are already rendered.
|
||
for (; this.blocks[i] != null; i++) {
|
||
this.updateBlock_(this.blocks[i], i);
|
||
}
|
||
var maxIndex = i - 1;
|
||
|
||
// Collect blocks at the end.
|
||
for (; i < this.newEndIndex; i++) {
|
||
block = this.getBlock_(i);
|
||
this.updateBlock_(block, i);
|
||
newEndBlocks.push(block);
|
||
}
|
||
|
||
// Attach collected blocks to the document.
|
||
if (newStartBlocks.length) {
|
||
this.parentNode.insertBefore(
|
||
this.domFragmentFromBlocks_(newStartBlocks),
|
||
this.$element[0].nextSibling);
|
||
}
|
||
if (newEndBlocks.length) {
|
||
this.parentNode.insertBefore(
|
||
this.domFragmentFromBlocks_(newEndBlocks),
|
||
this.blocks[maxIndex] && this.blocks[maxIndex].element[0].nextSibling);
|
||
}
|
||
|
||
// Restore $$checkUrlChange.
|
||
this.$browser.$$checkUrlChange = this.browserCheckUrlChange;
|
||
|
||
this.startIndex = this.newStartIndex;
|
||
this.endIndex = this.newEndIndex;
|
||
|
||
if (this.isFirstRender) {
|
||
this.isFirstRender = false;
|
||
var firstRenderStartIndex = this.$attrs.mdStartIndex ?
|
||
this.$scope.$eval(this.$attrs.mdStartIndex) :
|
||
this.container.topIndex;
|
||
|
||
// The first call to virtualRepeatUpdate_ may not be when the virtual repeater is ready.
|
||
// Introduce a slight delay so that the update happens when it is actually ready.
|
||
this.$mdUtil.nextTick(function() {
|
||
this.container.scrollToIndex(firstRenderStartIndex);
|
||
}.bind(this));
|
||
}
|
||
|
||
this.isVirtualRepeatUpdating_ = false;
|
||
};
|
||
|
||
|
||
/**
|
||
* @param {number} index Where the block is to be in the repeated list.
|
||
* @return {!VirtualRepeatController.Block} A new or pooled block to place at the specified index.
|
||
* @private
|
||
*/
|
||
VirtualRepeatController.prototype.getBlock_ = function(index) {
|
||
if (this.pooledBlocks.length) {
|
||
return this.pooledBlocks.pop();
|
||
}
|
||
|
||
var block;
|
||
this.transclude(angular.bind(this, function(clone, scope) {
|
||
block = {
|
||
element: clone,
|
||
new: true,
|
||
scope: scope
|
||
};
|
||
|
||
this.updateScope_(scope, index);
|
||
this.parentNode.appendChild(clone[0]);
|
||
}));
|
||
|
||
return block;
|
||
};
|
||
|
||
|
||
/**
|
||
* Updates and if not in a digest cycle, digests the specified block's scope to the data
|
||
* at the specified index.
|
||
* @param {!VirtualRepeatController.Block} block The block whose scope should be updated.
|
||
* @param {number} index The index to set.
|
||
* @private
|
||
*/
|
||
VirtualRepeatController.prototype.updateBlock_ = function(block, index) {
|
||
this.blocks[index] = block;
|
||
|
||
if (!block.new &&
|
||
(block.scope.$index === index && block.scope[this.repeatName] === this.items[index])) {
|
||
return;
|
||
}
|
||
block.new = false;
|
||
|
||
// Update and digest the block's scope.
|
||
this.updateScope_(block.scope, index);
|
||
|
||
// Perform digest before reattaching the block.
|
||
// Any resulting synchronous DOM mutations should be much faster as a result.
|
||
// This might break some directives.
|
||
if (!this.$rootScope.$$phase) {
|
||
block.scope.$digest();
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* Updates scope to the data at the specified index.
|
||
* @param {!angular.Scope} scope The scope which should be updated.
|
||
* @param {number} index The index to set.
|
||
* @private
|
||
*/
|
||
VirtualRepeatController.prototype.updateScope_ = function(scope, index) {
|
||
scope.$index = index;
|
||
scope[this.repeatName] = this.items && this.items[index];
|
||
if (this.extraName) scope[this.extraName(this.$scope)] = this.items[index];
|
||
};
|
||
|
||
|
||
/**
|
||
* Pools the block at the specified index (Pulls its element out of the dom and stores it).
|
||
* @param {number} index The index at which the block to pool is stored.
|
||
* @private
|
||
*/
|
||
VirtualRepeatController.prototype.poolBlock_ = function(index) {
|
||
this.pooledBlocks.push(this.blocks[index]);
|
||
this.parentNode.removeChild(this.blocks[index].element[0]);
|
||
delete this.blocks[index];
|
||
};
|
||
|
||
|
||
/**
|
||
* Produces a dom fragment containing the elements from the list of blocks.
|
||
* @param {!Array<!VirtualRepeatController.Block>} blocks The blocks whose elements
|
||
* should be added to the document fragment.
|
||
* @return {DocumentFragment}
|
||
* @private
|
||
*/
|
||
VirtualRepeatController.prototype.domFragmentFromBlocks_ = function(blocks) {
|
||
var fragment = this.$document[0].createDocumentFragment();
|
||
blocks.forEach(function(block) {
|
||
fragment.appendChild(block.element[0]);
|
||
});
|
||
return fragment;
|
||
};
|
||
|
||
|
||
/**
|
||
* Updates start and end indexes based on length of repeated items and container size.
|
||
* @private
|
||
*/
|
||
VirtualRepeatController.prototype.updateIndexes_ = function() {
|
||
var itemsLength = this.items ? this.items.length : 0;
|
||
var containerLength = Math.ceil(this.container.getSize() / this.itemSize);
|
||
|
||
this.newStartIndex = Math.max(0, Math.min(
|
||
itemsLength - containerLength,
|
||
Math.floor(this.container.getScrollOffset() / this.itemSize)));
|
||
this.newVisibleEnd = this.newStartIndex + containerLength + NUM_EXTRA;
|
||
this.newEndIndex = Math.min(itemsLength, this.newVisibleEnd);
|
||
this.newStartIndex = Math.max(0, this.newStartIndex - NUM_EXTRA);
|
||
};
|
||
|
||
/**
|
||
* This VirtualRepeatModelArrayLike class enforces the interface requirements
|
||
* for infinite scrolling within a mdVirtualRepeatContainer.
|
||
*
|
||
* @param {Object} model An object with this interface must implement the following interface with
|
||
* two (2) methods:
|
||
*
|
||
* getItemAtIndex: function(index) -> item at that index or null if it is not yet
|
||
* loaded (It should start downloading the item in that case).
|
||
*
|
||
* getLength: function() -> number The data length to which the repeater container
|
||
* should be sized. Ideally, when the count is known, this method should return it.
|
||
* Otherwise, return a higher number than the currently loaded items to produce an
|
||
* infinite-scroll behavior.
|
||
*
|
||
* @usage
|
||
* <hljs lang="html">
|
||
* <md-virtual-repeat-container md-orient-horizontal>
|
||
* <div md-virtual-repeat="i in items" md-on-demand>
|
||
* Hello {{i}}!
|
||
* </div>
|
||
* </md-virtual-repeat-container>
|
||
* </hljs>
|
||
*
|
||
*/
|
||
function VirtualRepeatModelArrayLike(model) {
|
||
if (!angular.isFunction(model.getItemAtIndex) ||
|
||
!angular.isFunction(model.getLength)) {
|
||
throw Error('When md-on-demand is enabled, the Object passed to md-virtual-repeat must ' +
|
||
'implement functions getItemAtIndex() and getLength().');
|
||
}
|
||
|
||
this.model = model;
|
||
}
|
||
|
||
/**
|
||
* @param {number} start
|
||
* @param {number} end
|
||
*/
|
||
VirtualRepeatModelArrayLike.prototype.$$includeIndexes = function(start, end) {
|
||
for (var i = start; i < end; i++) {
|
||
if (!this.hasOwnProperty(i)) {
|
||
this[i] = this.model.getItemAtIndex(i);
|
||
}
|
||
}
|
||
this.length = this.model.getLength();
|
||
};
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @name mdForceHeight
|
||
* @module material.components.virtualRepeat
|
||
* @restrict A
|
||
* @description
|
||
*
|
||
* Force an element to have a certain `px` height. This is used in place of a style tag in order to
|
||
* conform to the
|
||
* <a href="https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/script-src">
|
||
* Content Security Policy</a> regarding `unsafe-inline` `<style>` tags.
|
||
*
|
||
* This directive is related to <a ng-href="api/directive/mdVirtualRepeat">mdVirtualRepeat</a>.
|
||
*
|
||
* @usage
|
||
* <hljs lang="html">
|
||
* <div md-force-height="'100px'"></div>
|
||
* </hljs>
|
||
*/
|
||
function ForceHeightDirective($mdUtil) {
|
||
return {
|
||
restrict: 'A',
|
||
link: function(scope, element, attrs) {
|
||
var height = scope.$eval(attrs.mdForceHeight) || null;
|
||
|
||
if (height && element) {
|
||
element[0].style.height = height;
|
||
}
|
||
}
|
||
};
|
||
}
|
||
ForceHeightDirective.$inject = ['$mdUtil'];
|
||
|
||
})();
|
||
(function(){
|
||
"use strict";
|
||
|
||
/**
|
||
* @ngdoc module
|
||
* @name material.components.whiteframe
|
||
*/
|
||
MdWhiteframeDirective.$inject = ["$log"];
|
||
angular
|
||
.module('material.components.whiteframe', ['material.core'])
|
||
.directive('mdWhiteframe', MdWhiteframeDirective);
|
||
|
||
/**
|
||
* @ngdoc directive
|
||
* @module material.components.whiteframe
|
||
* @name mdWhiteframe
|
||
*
|
||
* @description
|
||
* The md-whiteframe directive allows you to apply an elevation shadow to an element.
|
||
*
|
||
* The attribute values needs to be a number between 1 and 24 or -1.
|
||
* When set to -1 no style is applied.
|
||
*
|
||
* ### Notes
|
||
* - If there is no value specified it defaults to 4dp.
|
||
* - If the value is not valid it defaults to 4dp.
|
||
|
||
* @usage
|
||
* <hljs lang="html">
|
||
* <div md-whiteframe="3">
|
||
* <span>Elevation of 3dp</span>
|
||
* </div>
|
||
* </hljs>
|
||
*
|
||
* <hljs lang="html">
|
||
* <div md-whiteframe="-1">
|
||
* <span>No elevation shadow applied</span>
|
||
* </div>
|
||
* </hljs>
|
||
*
|
||
* <hljs lang="html">
|
||
* <div ng-init="elevation = 5" md-whiteframe="{{elevation}}">
|
||
* <span>Elevation of 5dp with an interpolated value</span>
|
||
* </div>
|
||
* </hljs>
|
||
*/
|
||
function MdWhiteframeDirective($log) {
|
||
var DISABLE_DP = -1;
|
||
var MIN_DP = 1;
|
||
var MAX_DP = 24;
|
||
var DEFAULT_DP = 4;
|
||
|
||
return {
|
||
link: postLink
|
||
};
|
||
|
||
function postLink(scope, element, attr) {
|
||
var oldClass = '';
|
||
|
||
attr.$observe('mdWhiteframe', function(elevation) {
|
||
elevation = parseInt(elevation, 10) || DEFAULT_DP;
|
||
|
||
if (elevation != DISABLE_DP && (elevation > MAX_DP || elevation < MIN_DP)) {
|
||
$log.warn('md-whiteframe attribute value is invalid. It should be a number between ' + MIN_DP + ' and ' + MAX_DP, element[0]);
|
||
elevation = DEFAULT_DP;
|
||
}
|
||
|
||
var newClass = elevation == DISABLE_DP ? '' : 'md-whiteframe-' + elevation + 'dp';
|
||
attr.$updateClass(newClass, oldClass);
|
||
oldClass = newClass;
|
||
});
|
||
}
|
||
}
|
||
|
||
|
||
})();
|
||
(function(){
|
||
angular.module("material.core").constant("$MD_THEME_CSS", "md-autocomplete.md-THEME_NAME-theme{background:\"{{background-hue-1}}\"}md-autocomplete.md-THEME_NAME-theme[disabled]:not([md-floating-label]){background:\"{{background-hue-2}}\"}md-autocomplete.md-THEME_NAME-theme button md-icon path{fill:\"{{background-600}}\"}md-autocomplete.md-THEME_NAME-theme button:after{background:\"{{background-600-0.3}}\"}md-autocomplete.md-THEME_NAME-theme input{color:\"{{foreground-1}}\"}md-autocomplete.md-THEME_NAME-theme.md-accent md-input-container.md-input-focused .md-input{border-color:\"{{accent-color}}\"}md-autocomplete.md-THEME_NAME-theme.md-accent md-input-container.md-input-focused label,md-autocomplete.md-THEME_NAME-theme.md-accent md-input-container.md-input-focused md-icon{color:\"{{accent-color}}\"}md-autocomplete.md-THEME_NAME-theme.md-accent md-progress-linear .md-container{background-color:\"{{accent-100}}\"}md-autocomplete.md-THEME_NAME-theme.md-accent md-progress-linear .md-bar{background-color:\"{{accent-color}}\"}md-autocomplete.md-THEME_NAME-theme.md-warn md-input-container.md-input-focused .md-input{border-color:\"{{warn-A700}}\"}md-autocomplete.md-THEME_NAME-theme.md-warn md-input-container.md-input-focused label,md-autocomplete.md-THEME_NAME-theme.md-warn md-input-container.md-input-focused md-icon{color:\"{{warn-A700}}\"}md-autocomplete.md-THEME_NAME-theme.md-warn md-progress-linear .md-container{background-color:\"{{warn-100}}\"}md-autocomplete.md-THEME_NAME-theme.md-warn md-progress-linear .md-bar{background-color:\"{{warn-color}}\"}.md-autocomplete-standard-list-container.md-THEME_NAME-theme,.md-autocomplete-suggestions-container.md-THEME_NAME-theme{background:\"{{background-hue-1}}\"}.md-autocomplete-standard-list-container.md-THEME_NAME-theme .md-autocomplete-suggestion,.md-autocomplete-suggestions-container.md-THEME_NAME-theme .md-autocomplete-suggestion{color:\"{{foreground-1}}\"}.md-autocomplete-standard-list-container.md-THEME_NAME-theme .md-autocomplete-suggestion.selected,.md-autocomplete-standard-list-container.md-THEME_NAME-theme .md-autocomplete-suggestion:hover,.md-autocomplete-suggestions-container.md-THEME_NAME-theme .md-autocomplete-suggestion.selected,.md-autocomplete-suggestions-container.md-THEME_NAME-theme .md-autocomplete-suggestion:hover{background:\"{{background-500-0.18}}\"}md-backdrop{background-color:\"{{background-900-0.0}}\"}md-backdrop.md-opaque.md-THEME_NAME-theme{background-color:\"{{background-900-1.0}}\"}md-bottom-sheet.md-THEME_NAME-theme{background-color:\"{{background-color}}\";border-top-color:\"{{background-hue-3}}\"}md-bottom-sheet.md-THEME_NAME-theme.md-list md-list-item{color:\"{{foreground-1}}\"}md-bottom-sheet.md-THEME_NAME-theme .md-subheader{background-color:\"{{background-color}}\";color:\"{{foreground-1}}\"}.md-button.md-THEME_NAME-theme:not([disabled]).md-focused,.md-button.md-THEME_NAME-theme:not([disabled]):hover{background-color:\"{{background-500-0.2}}\"}.md-button.md-THEME_NAME-theme:not([disabled]).md-icon-button:hover{background-color:transparent}.md-button.md-THEME_NAME-theme.md-fab{background-color:\"{{accent-color}}\";color:\"{{accent-contrast}}\"}.md-button.md-THEME_NAME-theme.md-fab md-icon{color:\"{{accent-contrast}}\"}.md-button.md-THEME_NAME-theme.md-fab:not([disabled]).md-focused,.md-button.md-THEME_NAME-theme.md-fab:not([disabled]):hover{background-color:\"{{accent-A700}}\"}.md-button.md-THEME_NAME-theme.md-primary{color:\"{{primary-color}}\"}.md-button.md-THEME_NAME-theme.md-primary.md-fab,.md-button.md-THEME_NAME-theme.md-primary.md-raised{background-color:\"{{primary-color}}\";color:\"{{primary-contrast}}\"}.md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]) md-icon,.md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]) md-icon{color:\"{{primary-contrast}}\"}.md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]).md-focused,.md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]):hover,.md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]).md-focused,.md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]):hover{background-color:\"{{primary-600}}\"}.md-button.md-THEME_NAME-theme.md-primary:not([disabled]) md-icon{color:\"{{primary-color}}\"}.md-button.md-THEME_NAME-theme.md-raised{background-color:\"{{background-50}}\";color:\"{{background-900}}\"}.md-button.md-THEME_NAME-theme.md-raised:not([disabled]) md-icon{color:\"{{background-900}}\"}.md-button.md-THEME_NAME-theme.md-raised:not([disabled]):hover{background-color:\"{{background-50}}\"}.md-button.md-THEME_NAME-theme.md-raised:not([disabled]).md-focused{background-color:\"{{background-200}}\"}.md-button.md-THEME_NAME-theme.md-warn{color:\"{{warn-color}}\"}.md-button.md-THEME_NAME-theme.md-warn.md-fab,.md-button.md-THEME_NAME-theme.md-warn.md-raised{background-color:\"{{warn-color}}\";color:\"{{warn-contrast}}\"}.md-button.md-THEME_NAME-theme.md-warn.md-fab:not([disabled]) md-icon,.md-button.md-THEME_NAME-theme.md-warn.md-raised:not([disabled]) md-icon{color:\"{{warn-contrast}}\"}.md-button.md-THEME_NAME-theme.md-warn.md-fab:not([disabled]).md-focused,.md-button.md-THEME_NAME-theme.md-warn.md-fab:not([disabled]):hover,.md-button.md-THEME_NAME-theme.md-warn.md-raised:not([disabled]).md-focused,.md-button.md-THEME_NAME-theme.md-warn.md-raised:not([disabled]):hover{background-color:\"{{warn-600}}\"}.md-button.md-THEME_NAME-theme.md-warn:not([disabled]) md-icon{color:\"{{warn-color}}\"}.md-button.md-THEME_NAME-theme.md-accent{color:\"{{accent-color}}\"}.md-button.md-THEME_NAME-theme.md-accent.md-fab,.md-button.md-THEME_NAME-theme.md-accent.md-raised{background-color:\"{{accent-color}}\";color:\"{{accent-contrast}}\"}.md-button.md-THEME_NAME-theme.md-accent.md-fab:not([disabled]) md-icon,.md-button.md-THEME_NAME-theme.md-accent.md-raised:not([disabled]) md-icon{color:\"{{accent-contrast}}\"}.md-button.md-THEME_NAME-theme.md-accent.md-fab:not([disabled]).md-focused,.md-button.md-THEME_NAME-theme.md-accent.md-fab:not([disabled]):hover,.md-button.md-THEME_NAME-theme.md-accent.md-raised:not([disabled]).md-focused,.md-button.md-THEME_NAME-theme.md-accent.md-raised:not([disabled]):hover{background-color:\"{{accent-A700}}\"}.md-button.md-THEME_NAME-theme.md-accent:not([disabled]) md-icon{color:\"{{accent-color}}\"}.md-button.md-THEME_NAME-theme.md-accent[disabled],.md-button.md-THEME_NAME-theme.md-fab[disabled],.md-button.md-THEME_NAME-theme.md-raised[disabled],.md-button.md-THEME_NAME-theme.md-warn[disabled],.md-button.md-THEME_NAME-theme[disabled]{color:\"{{foreground-3}}\";cursor:default}.md-button.md-THEME_NAME-theme.md-accent[disabled] md-icon,.md-button.md-THEME_NAME-theme.md-fab[disabled] md-icon,.md-button.md-THEME_NAME-theme.md-raised[disabled] md-icon,.md-button.md-THEME_NAME-theme.md-warn[disabled] md-icon,.md-button.md-THEME_NAME-theme[disabled] md-icon{color:\"{{foreground-3}}\"}.md-button.md-THEME_NAME-theme.md-fab[disabled],.md-button.md-THEME_NAME-theme.md-raised[disabled]{background-color:\"{{foreground-4}}\"}.md-button.md-THEME_NAME-theme[disabled]{background-color:transparent}._md a.md-THEME_NAME-theme:not(.md-button).md-primary{color:\"{{primary-color}}\"}._md a.md-THEME_NAME-theme:not(.md-button).md-primary:hover{color:\"{{primary-700}}\"}._md a.md-THEME_NAME-theme:not(.md-button).md-accent{color:\"{{accent-color}}\"}._md a.md-THEME_NAME-theme:not(.md-button).md-accent:hover{color:\"{{accent-A700}}\"}._md a.md-THEME_NAME-theme:not(.md-button).md-warn{color:\"{{warn-color}}\"}._md a.md-THEME_NAME-theme:not(.md-button).md-warn:hover{color:\"{{warn-700}}\"}md-card.md-THEME_NAME-theme{background-color:\"{{background-hue-1}}\";border-radius:2px;color:\"{{foreground-1}}\"}md-card.md-THEME_NAME-theme .md-card-image{border-radius:2px 2px 0 0}md-card.md-THEME_NAME-theme md-card-header md-card-avatar md-icon{background-color:\"{{foreground-3}}\";color:\"{{background-color}}\"}md-card.md-THEME_NAME-theme md-card-header md-card-header-text .md-subhead,md-card.md-THEME_NAME-theme md-card-title md-card-title-text:not(:only-child) .md-subhead{color:\"{{foreground-2}}\"}md-checkbox.md-THEME_NAME-theme .md-ripple{color:\"{{accent-A700}}\"}md-checkbox.md-THEME_NAME-theme.md-checked .md-ripple{color:\"{{background-600}}\"}md-checkbox.md-THEME_NAME-theme.md-checked.md-focused .md-container:before{background-color:\"{{accent-color-0.26}}\"}md-checkbox.md-THEME_NAME-theme .md-ink-ripple{color:\"{{foreground-2}}\"}md-checkbox.md-THEME_NAME-theme.md-checked .md-ink-ripple{color:\"{{accent-color-0.87}}\"}md-checkbox.md-THEME_NAME-theme:not(.md-checked) .md-icon{border-color:\"{{foreground-2}}\"}md-checkbox.md-THEME_NAME-theme.md-checked .md-icon{background-color:\"{{accent-color-0.87}}\"}md-checkbox.md-THEME_NAME-theme.md-checked .md-icon:after{border-color:\"{{background-default}}\"}md-checkbox.md-THEME_NAME-theme:not([disabled]).md-primary .md-ripple{color:\"{{primary-600}}\"}md-checkbox.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-ripple{color:\"{{background-600}}\"}md-checkbox.md-THEME_NAME-theme:not([disabled]).md-primary .md-ink-ripple{color:\"{{foreground-2}}\"}md-checkbox.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-ink-ripple{color:\"{{primary-color-0.87}}\"}md-checkbox.md-THEME_NAME-theme:not([disabled]).md-primary:not(.md-checked) .md-icon{border-color:\"{{foreground-2}}\"}md-checkbox.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-icon{background-color:\"{{primary-color-0.87}}\"}md-checkbox.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked.md-focused .md-container:before{background-color:\"{{primary-color-0.26}}\"}md-checkbox.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-icon:after{border-color:\"{{primary-contrast-0.87}}\"}md-checkbox.md-THEME_NAME-theme:not([disabled]).md-primary .md-indeterminate[disabled] .md-container{color:\"{{foreground-3}}\"}md-checkbox.md-THEME_NAME-theme:not([disabled]).md-warn .md-ripple{color:\"{{warn-600}}\"}md-checkbox.md-THEME_NAME-theme:not([disabled]).md-warn .md-ink-ripple{color:\"{{foreground-2}}\"}md-checkbox.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked .md-ink-ripple{color:\"{{warn-color-0.87}}\"}md-checkbox.md-THEME_NAME-theme:not([disabled]).md-warn:not(.md-checked) .md-icon{border-color:\"{{foreground-2}}\"}md-checkbox.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked .md-icon{background-color:\"{{warn-color-0.87}}\"}md-checkbox.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked.md-focused:not([disabled]) .md-container:before{background-color:\"{{warn-color-0.26}}\"}md-checkbox.md-THEME_NAME-theme[disabled]:not(.md-checked) .md-icon{border-color:\"{{foreground-3}}\"}md-checkbox.md-THEME_NAME-theme[disabled].md-checked .md-icon{background-color:\"{{foreground-3}}\"}md-checkbox.md-THEME_NAME-theme[disabled] .md-label{color:\"{{foreground-3}}\"}md-chips.md-THEME_NAME-theme .md-chips{box-shadow:0 1px \"{{foreground-4}}\"}md-chips.md-THEME_NAME-theme .md-chips.md-focused{box-shadow:0 2px \"{{primary-color}}\"}md-chips.md-THEME_NAME-theme .md-chips .md-chip-input-container input{color:\"{{foreground-1}}\"}md-chips.md-THEME_NAME-theme .md-chips .md-chip-input-container input::-webkit-input-placeholder{color:\"{{foreground-3}}\"}md-chips.md-THEME_NAME-theme .md-chips .md-chip-input-container input:-ms-input-placeholder{color:\"{{foreground-3}}\"}md-chips.md-THEME_NAME-theme .md-chips .md-chip-input-container input::-ms-input-placeholder{color:\"{{foreground-3}}\"}md-chips.md-THEME_NAME-theme .md-chips .md-chip-input-container input::placeholder{color:\"{{foreground-3}}\"}md-chips.md-THEME_NAME-theme .md-chips .md-chip-input-container input:-moz-placeholder,md-chips.md-THEME_NAME-theme .md-chips .md-chip-input-container input::-moz-placeholder{color:\"{{foreground-3}}\";opacity:1}md-chips.md-THEME_NAME-theme md-chip{background:\"{{background-300}}\";color:\"{{background-800}}\"}md-chips.md-THEME_NAME-theme md-chip md-icon{color:\"{{background-700}}\"}md-chips.md-THEME_NAME-theme md-chip.md-focused{background:\"{{primary-color}}\";color:\"{{primary-contrast}}\"}md-chips.md-THEME_NAME-theme md-chip.md-focused md-icon{color:\"{{primary-contrast}}\"}md-chips.md-THEME_NAME-theme md-chip._md-chip-editing{background:transparent;color:\"{{background-800}}\"}md-chips.md-THEME_NAME-theme .md-chip-remove-container button.md-chip-remove md-icon,md-chips.md-THEME_NAME-theme .md-chip-remove-container buttonmd-chip-remove md-icon{fill:\"{{foreground-2}}\";color:\"{{foreground-2}}\"}.md-contact-suggestion span.md-contact-email{color:\"{{background-400}}\"}md-content.md-THEME_NAME-theme{background-color:\"{{background-default}}\";color:\"{{foreground-1}}\"}.md-THEME_NAME-theme .md-calendar{background:\"{{background-hue-1}}\";color:\"{{foreground-1-0.87}}\"}.md-THEME_NAME-theme .md-calendar tr:last-child td{border-bottom-color:\"{{background-hue-2}}\"}.md-THEME_NAME-theme .md-calendar-day-header{background:\"{{background-500-0.32}}\";color:\"{{foreground-1-0.87}}\"}.md-THEME_NAME-theme .md-calendar-date.md-calendar-date-today .md-calendar-date-selection-indicator{border:1px solid \"{{primary-500}}\"}.md-THEME_NAME-theme .md-calendar-date.md-calendar-date-today.md-calendar-date-disabled{color:\"{{primary-500-0.6}}\"}.md-calendar-date.md-focus .md-THEME_NAME-theme .md-calendar-date-selection-indicator,.md-THEME_NAME-theme .md-calendar-date-selection-indicator:hover{background:\"{{background-500-0.32}}\"}.md-THEME_NAME-theme .md-calendar-date.md-calendar-selected-date .md-calendar-date-selection-indicator,.md-THEME_NAME-theme .md-calendar-date.md-focus.md-calendar-selected-date .md-calendar-date-selection-indicator{background:\"{{primary-500}}\";border-color:transparent;color:\"{{primary-500-contrast}}\"}.md-THEME_NAME-theme .md-calendar-date-disabled,.md-THEME_NAME-theme .md-calendar-month-label-disabled{color:\"{{foreground-3}}\"}.md-THEME_NAME-theme .md-calendar-month-label md-icon,.md-THEME_NAME-theme .md-datepicker-input{color:\"{{foreground-1}}\"}.md-THEME_NAME-theme .md-datepicker-input::-webkit-input-placeholder{color:\"{{foreground-3}}\"}.md-THEME_NAME-theme .md-datepicker-input:-ms-input-placeholder{color:\"{{foreground-3}}\"}.md-THEME_NAME-theme .md-datepicker-input::-ms-input-placeholder{color:\"{{foreground-3}}\"}.md-THEME_NAME-theme .md-datepicker-input::placeholder{color:\"{{foreground-3}}\"}.md-THEME_NAME-theme .md-datepicker-input:-moz-placeholder,.md-THEME_NAME-theme .md-datepicker-input::-moz-placeholder{color:\"{{foreground-3}}\";opacity:1}.md-THEME_NAME-theme .md-datepicker-input-container{border-bottom-color:\"{{foreground-4}}\"}.md-THEME_NAME-theme .md-datepicker-input-container.md-datepicker-focused{border-bottom-color:\"{{primary-color}}\"}.md-accent .md-THEME_NAME-theme .md-datepicker-input-container.md-datepicker-focused{border-bottom-color:\"{{accent-color}}\"}.md-THEME_NAME-theme .md-datepicker-input-container.md-datepicker-invalid,.md-warn .md-THEME_NAME-theme .md-datepicker-input-container.md-datepicker-focused{border-bottom-color:\"{{warn-A700}}\"}.md-THEME_NAME-theme .md-datepicker-calendar-pane{border-color:\"{{background-hue-1}}\"}.md-THEME_NAME-theme .md-datepicker-triangle-button .md-datepicker-expand-triangle{border-top-color:\"{{foreground-2}}\"}.md-THEME_NAME-theme .md-datepicker-open .md-datepicker-calendar-icon{color:\"{{primary-color}}\"}.md-accent .md-THEME_NAME-theme .md-datepicker-open .md-datepicker-calendar-icon,.md-THEME_NAME-theme .md-datepicker-open.md-accent .md-datepicker-calendar-icon{color:\"{{accent-color}}\"}.md-THEME_NAME-theme .md-datepicker-open.md-warn .md-datepicker-calendar-icon,.md-warn .md-THEME_NAME-theme .md-datepicker-open .md-datepicker-calendar-icon{color:\"{{warn-A700}}\"}.md-THEME_NAME-theme .md-datepicker-calendar{background:\"{{background-hue-1}}\"}.md-THEME_NAME-theme .md-datepicker-input-mask-opaque{box-shadow:0 0 0 9999px \"{{background-hue-1}}\"}.md-THEME_NAME-theme .md-datepicker-open .md-datepicker-input-container{background:\"{{background-hue-1}}\"}md-dialog.md-THEME_NAME-theme{background-color:\"{{background-hue-1}}\";border-radius:4px;color:\"{{foreground-1}}\"}md-dialog.md-THEME_NAME-theme.md-content-overflow md-dialog-actions{border-top-color:\"{{foreground-4}}\"}md-divider.md-THEME_NAME-theme{border-color:\"{{foreground-4}}\"}md-fab-speed-dial.md-THEME_NAME-theme md-fab-trigger .md-fab.md-button[disabled]{background-color:\"{{foreground-4}}\"}md-fab-speed-dial.md-THEME_NAME-theme md-fab-actions .md-fab-action-item .md-button.md-fab.md-raised.md-mini.md-focused,md-fab-speed-dial.md-THEME_NAME-theme md-fab-actions .md-fab-action-item .md-button.md-fab.md-raised.md-mini:hover{background-color:\"{{background-500}}\"}md-icon.md-THEME_NAME-theme{color:\"{{foreground-2}}\"}md-icon.md-THEME_NAME-theme.md-primary{color:\"{{primary-color}}\"}md-icon.md-THEME_NAME-theme.md-accent{color:\"{{accent-color}}\"}md-icon.md-THEME_NAME-theme.md-warn{color:\"{{warn-color}}\"}md-input-container.md-THEME_NAME-theme .md-input{border-color:\"{{background-default-contrast-divider}}\";color:\"{{background-default-contrast}}\"}md-input-container.md-THEME_NAME-theme .md-input::-webkit-input-placeholder{color:\"{{background-default-contrast-secondary}}\"}md-input-container.md-THEME_NAME-theme .md-input:-ms-input-placeholder{color:\"{{background-default-contrast-secondary}}\"}md-input-container.md-THEME_NAME-theme .md-input::-ms-input-placeholder{color:\"{{background-default-contrast-secondary}}\"}md-input-container.md-THEME_NAME-theme .md-input::placeholder{color:\"{{background-default-contrast-secondary}}\"}md-input-container.md-THEME_NAME-theme .md-input:-moz-placeholder,md-input-container.md-THEME_NAME-theme .md-input::-moz-placeholder{color:\"{{background-default-contrast-secondary}}\";opacity:1}md-input-container.md-THEME_NAME-theme>md-icon{color:\"{{background-default-contrast}}\"}md-input-container.md-THEME_NAME-theme .md-placeholder,md-input-container.md-THEME_NAME-theme label{color:\"{{background-default-contrast-secondary}}\"}md-input-container.md-THEME_NAME-theme label.md-required:after{color:\"{{warn-A700}}\"}md-input-container.md-THEME_NAME-theme:not(.md-input-focused):not(.md-input-invalid) label.md-required:after{color:\"{{background-default-contrast-secondary}}\"}md-input-container.md-THEME_NAME-theme .md-input-message-animation,md-input-container.md-THEME_NAME-theme .md-input-messages-animation{color:\"{{warn-A700}}\"}md-input-container.md-THEME_NAME-theme .md-input-message-animation .md-char-counter,md-input-container.md-THEME_NAME-theme .md-input-messages-animation .md-char-counter{color:\"{{background-default-contrast}}\"}md-input-container.md-THEME_NAME-theme.md-input-focused .md-input::-webkit-input-placeholder{color:\"{{background-default-contrast-secondary}}\"}md-input-container.md-THEME_NAME-theme.md-input-focused .md-input:-ms-input-placeholder{color:\"{{background-default-contrast-secondary}}\"}md-input-container.md-THEME_NAME-theme.md-input-focused .md-input::-ms-input-placeholder{color:\"{{background-default-contrast-secondary}}\"}md-input-container.md-THEME_NAME-theme.md-input-focused .md-input::placeholder{color:\"{{background-default-contrast-secondary}}\"}md-input-container.md-THEME_NAME-theme.md-input-focused .md-input:-moz-placeholder,md-input-container.md-THEME_NAME-theme.md-input-focused .md-input::-moz-placeholder{color:\"{{background-default-contrast-secondary}}\";opacity:1}md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-has-value label{color:\"{{background-default-contrast-secondary}}\"}md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused .md-input,md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-resized .md-input{border-color:\"{{primary-color}}\"}md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused label,md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused md-icon{color:\"{{primary-color}}\"}md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused.md-accent .md-input{border-color:\"{{accent-color}}\"}md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused.md-accent label,md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused.md-accent md-icon{color:\"{{accent-color}}\"}md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused.md-warn .md-input{border-color:\"{{warn-A700}}\"}md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused.md-warn label,md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused.md-warn md-icon{color:\"{{warn-A700}}\"}md-input-container.md-THEME_NAME-theme.md-input-invalid .md-input{border-color:\"{{warn-A700}}\"}md-input-container.md-THEME_NAME-theme.md-input-invalid .md-char-counter,md-input-container.md-THEME_NAME-theme.md-input-invalid .md-input-message-animation,md-input-container.md-THEME_NAME-theme.md-input-invalid label{color:\"{{warn-A700}}\"}[disabled] md-input-container.md-THEME_NAME-theme .md-input,md-input-container.md-THEME_NAME-theme .md-input[disabled]{background-image:linear-gradient(90deg,\"{{background-default-contrast-disabled}}\" 0,\"{{background-default-contrast-disabled}}\" 33%,transparent 0);background-image:-ms-linear-gradient(left,transparent 0,\"{{background-default-contrast-disabled}}\" 100%);border-bottom-color:transparent;color:\"{{background-default-contrast-disabled}}\"}md-list.md-THEME_NAME-theme md-list-item.md-2-line .md-list-item-text h3,md-list.md-THEME_NAME-theme md-list-item.md-2-line .md-list-item-text h4,md-list.md-THEME_NAME-theme md-list-item.md-3-line .md-list-item-text h3,md-list.md-THEME_NAME-theme md-list-item.md-3-line .md-list-item-text h4{color:\"{{foreground-1}}\"}md-list.md-THEME_NAME-theme md-list-item.md-2-line .md-list-item-text p,md-list.md-THEME_NAME-theme md-list-item.md-3-line .md-list-item-text p{color:\"{{foreground-2}}\"}md-list.md-THEME_NAME-theme .md-proxy-focus.md-focused div.md-no-style{background-color:\"{{background-100}}\"}md-list.md-THEME_NAME-theme md-list-item .md-avatar-icon{background-color:\"{{foreground-3}}\";color:\"{{background-color}}\"}md-list.md-THEME_NAME-theme md-list-item>md-icon{color:\"{{foreground-2}}\"}md-list.md-THEME_NAME-theme md-list-item>md-icon.md-highlight{color:\"{{primary-color}}\"}md-list.md-THEME_NAME-theme md-list-item>md-icon.md-highlight.md-accent{color:\"{{accent-color}}\"}md-menu-content.md-THEME_NAME-theme{background-color:\"{{background-hue-1}}\"}md-menu-content.md-THEME_NAME-theme md-menu-item{color:\"{{foreground-1}}\"}md-menu-content.md-THEME_NAME-theme md-menu-item md-icon{color:\"{{foreground-2}}\"}md-menu-content.md-THEME_NAME-theme md-menu-item .md-button[disabled],md-menu-content.md-THEME_NAME-theme md-menu-item .md-button[disabled] md-icon{color:\"{{foreground-3}}\"}md-menu-content.md-THEME_NAME-theme md-menu-divider{background-color:\"{{foreground-4}}\"}md-menu-bar.md-THEME_NAME-theme>button.md-button{border-radius:2px;color:\"{{foreground-1}}\"}md-menu-bar.md-THEME_NAME-theme md-menu>button{color:\"{{foreground-1}}\"}md-menu-bar.md-THEME_NAME-theme md-menu.md-open>button,md-menu-bar.md-THEME_NAME-theme md-menu>button:focus{background-color:\"{{ background-500-0.18}}\";outline:none}md-menu-bar.md-THEME_NAME-theme.md-open:not(.md-keyboard-mode) md-menu:hover>button{background-color:\"{{ background-500-0.18}}\"}md-menu-bar.md-THEME_NAME-theme:not(.md-keyboard-mode):not(.md-open) md-menu button:focus,md-menu-bar.md-THEME_NAME-theme:not(.md-keyboard-mode):not(.md-open) md-menu button:hover{background:transparent}md-menu-content.md-THEME_NAME-theme .md-menu>.md-button:after{color:\"{{foreground-2}}\"}md-menu-content.md-THEME_NAME-theme .md-menu.md-open>.md-button{background-color:\"{{ background-500-0.18}}\"}md-toolbar.md-THEME_NAME-theme.md-menu-toolbar{background-color:\"{{background-hue-1}}\";color:\"{{foreground-1}}\"}md-toolbar.md-THEME_NAME-theme.md-menu-toolbar md-toolbar-filler{background-color:\"{{primary-color}}\";color:\"{{primary-contrast}}\"}md-toolbar.md-THEME_NAME-theme.md-menu-toolbar md-toolbar-filler md-icon{color:\"{{primary-contrast}}\"}md-nav-bar.md-THEME_NAME-theme .md-nav-bar{background-color:transparent;border-color:\"{{foreground-4}}\"}md-nav-bar.md-THEME_NAME-theme .md-button._md-nav-button.md-unselected{color:\"{{foreground-2}}\"}md-nav-bar.md-THEME_NAME-theme .md-button._md-nav-button[disabled]{color:\"{{foreground-3}}\"}md-nav-bar.md-THEME_NAME-theme md-nav-ink-bar{background:\"{{accent-color}}\";color:\"{{accent-color}}\"}md-nav-bar.md-THEME_NAME-theme.md-accent>.md-nav-bar{background-color:\"{{accent-color}}\"}md-nav-bar.md-THEME_NAME-theme.md-accent>.md-nav-bar .md-button._md-nav-button{color:\"{{accent-A100}}\"}md-nav-bar.md-THEME_NAME-theme.md-accent>.md-nav-bar .md-button._md-nav-button.md-active,md-nav-bar.md-THEME_NAME-theme.md-accent>.md-nav-bar .md-button._md-nav-button.md-focused{color:\"{{accent-contrast}}\"}md-nav-bar.md-THEME_NAME-theme.md-accent>.md-nav-bar .md-button._md-nav-button.md-focused{background:\"{{accent-contrast-0.1}}\"}md-nav-bar.md-THEME_NAME-theme.md-accent>.md-nav-bar md-nav-ink-bar{background:\"{{primary-600-1}}\";color:\"{{primary-600-1}}\"}md-nav-bar.md-THEME_NAME-theme.md-warn>.md-nav-bar{background-color:\"{{warn-color}}\"}md-nav-bar.md-THEME_NAME-theme.md-warn>.md-nav-bar .md-button._md-nav-button{color:\"{{warn-100}}\"}md-nav-bar.md-THEME_NAME-theme.md-warn>.md-nav-bar .md-button._md-nav-button.md-active,md-nav-bar.md-THEME_NAME-theme.md-warn>.md-nav-bar .md-button._md-nav-button.md-focused{color:\"{{warn-contrast}}\"}md-nav-bar.md-THEME_NAME-theme.md-warn>.md-nav-bar .md-button._md-nav-button.md-focused{background:\"{{warn-contrast-0.1}}\"}md-nav-bar.md-THEME_NAME-theme.md-primary>.md-nav-bar{background-color:\"{{primary-color}}\"}md-nav-bar.md-THEME_NAME-theme.md-primary>.md-nav-bar .md-button._md-nav-button{color:\"{{primary-100}}\"}md-nav-bar.md-THEME_NAME-theme.md-primary>.md-nav-bar .md-button._md-nav-button.md-active,md-nav-bar.md-THEME_NAME-theme.md-primary>.md-nav-bar .md-button._md-nav-button.md-focused{color:\"{{primary-contrast}}\"}md-nav-bar.md-THEME_NAME-theme.md-primary>.md-nav-bar .md-button._md-nav-button.md-focused{background:\"{{primary-contrast-0.1}}\"}md-toolbar>md-nav-bar.md-THEME_NAME-theme>.md-nav-bar{background-color:\"{{primary-color}}\"}md-toolbar>md-nav-bar.md-THEME_NAME-theme>.md-nav-bar .md-button._md-nav-button{color:\"{{primary-100}}\"}md-toolbar>md-nav-bar.md-THEME_NAME-theme>.md-nav-bar .md-button._md-nav-button.md-active,md-toolbar>md-nav-bar.md-THEME_NAME-theme>.md-nav-bar .md-button._md-nav-button.md-focused{color:\"{{primary-contrast}}\"}md-toolbar>md-nav-bar.md-THEME_NAME-theme>.md-nav-bar .md-button._md-nav-button.md-focused{background:\"{{primary-contrast-0.1}}\"}md-toolbar.md-accent>md-nav-bar.md-THEME_NAME-theme>.md-nav-bar{background-color:\"{{accent-color}}\"}md-toolbar.md-accent>md-nav-bar.md-THEME_NAME-theme>.md-nav-bar .md-button._md-nav-button{color:\"{{accent-A100}}\"}md-toolbar.md-accent>md-nav-bar.md-THEME_NAME-theme>.md-nav-bar .md-button._md-nav-button.md-active,md-toolbar.md-accent>md-nav-bar.md-THEME_NAME-theme>.md-nav-bar .md-button._md-nav-button.md-focused{color:\"{{accent-contrast}}\"}md-toolbar.md-accent>md-nav-bar.md-THEME_NAME-theme>.md-nav-bar .md-button._md-nav-button.md-focused{background:\"{{accent-contrast-0.1}}\"}md-toolbar.md-accent>md-nav-bar.md-THEME_NAME-theme>.md-nav-bar md-nav-ink-bar{background:\"{{primary-600-1}}\";color:\"{{primary-600-1}}\"}md-toolbar.md-warn>md-nav-bar.md-THEME_NAME-theme>.md-nav-bar{background-color:\"{{warn-color}}\"}md-toolbar.md-warn>md-nav-bar.md-THEME_NAME-theme>.md-nav-bar .md-button._md-nav-button{color:\"{{warn-100}}\"}md-toolbar.md-warn>md-nav-bar.md-THEME_NAME-theme>.md-nav-bar .md-button._md-nav-button.md-active,md-toolbar.md-warn>md-nav-bar.md-THEME_NAME-theme>.md-nav-bar .md-button._md-nav-button.md-focused{color:\"{{warn-contrast}}\"}md-toolbar.md-warn>md-nav-bar.md-THEME_NAME-theme>.md-nav-bar .md-button._md-nav-button.md-focused{background:\"{{warn-contrast-0.1}}\"}._md-panel-backdrop.md-THEME_NAME-theme{background-color:\"{{background-900-1.0}}\"}md-progress-circular.md-THEME_NAME-theme path{stroke:\"{{primary-color}}\"}md-progress-circular.md-THEME_NAME-theme.md-warn path{stroke:\"{{warn-color}}\"}md-progress-circular.md-THEME_NAME-theme.md-accent path{stroke:\"{{accent-color}}\"}md-progress-linear.md-THEME_NAME-theme .md-container{background-color:\"{{primary-100}}\"}md-progress-linear.md-THEME_NAME-theme .md-bar{background-color:\"{{primary-color}}\"}md-progress-linear.md-THEME_NAME-theme.md-warn .md-container{background-color:\"{{warn-100}}\"}md-progress-linear.md-THEME_NAME-theme.md-warn .md-bar{background-color:\"{{warn-color}}\"}md-progress-linear.md-THEME_NAME-theme.md-accent .md-container{background-color:\"{{accent-100}}\"}md-progress-linear.md-THEME_NAME-theme.md-accent .md-bar{background-color:\"{{accent-color}}\"}md-progress-linear.md-THEME_NAME-theme[md-mode=buffer].md-primary .md-bar1{background-color:\"{{primary-100}}\"}md-progress-linear.md-THEME_NAME-theme[md-mode=buffer].md-primary .md-dashed:before{background:radial-gradient(\"{{primary-100}}\" 0,\"{{primary-100}}\" 16%,transparent 42%)}md-progress-linear.md-THEME_NAME-theme[md-mode=buffer].md-warn .md-bar1{background-color:\"{{warn-100}}\"}md-progress-linear.md-THEME_NAME-theme[md-mode=buffer].md-warn .md-dashed:before{background:radial-gradient(\"{{warn-100}}\" 0,\"{{warn-100}}\" 16%,transparent 42%)}md-progress-linear.md-THEME_NAME-theme[md-mode=buffer].md-accent .md-bar1{background-color:\"{{accent-100}}\"}md-progress-linear.md-THEME_NAME-theme[md-mode=buffer].md-accent .md-dashed:before{background:radial-gradient(\"{{accent-100}}\" 0,\"{{accent-100}}\" 16%,transparent 42%)}md-radio-button.md-THEME_NAME-theme .md-off{border-color:\"{{foreground-2}}\"}md-radio-button.md-THEME_NAME-theme .md-on{background-color:\"{{accent-color-0.87}}\"}md-radio-button.md-THEME_NAME-theme.md-checked .md-off{border-color:\"{{accent-color-0.87}}\"}md-radio-button.md-THEME_NAME-theme.md-checked .md-ink-ripple{color:\"{{accent-color-0.87}}\"}md-radio-button.md-THEME_NAME-theme .md-container .md-ripple{color:\"{{accent-A700}}\"}md-radio-button.md-THEME_NAME-theme:not([disabled]).md-primary .md-on,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-primary .md-on,md-radio-group.md-THEME_NAME-theme:not([disabled]).md-primary .md-on,md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-primary .md-on{background-color:\"{{primary-color-0.87}}\"}md-radio-button.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-off,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-primary.md-checked .md-off,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-primary .md-checked .md-off,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-primary .md-checked .md-off,md-radio-group.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-off,md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-primary.md-checked .md-off,md-radio-group.md-THEME_NAME-theme:not([disabled]).md-primary .md-checked .md-off,md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-primary .md-checked .md-off{border-color:\"{{primary-color-0.87}}\"}md-radio-button.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-ink-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-primary.md-checked .md-ink-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-primary .md-checked .md-ink-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-primary .md-checked .md-ink-ripple,md-radio-group.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-ink-ripple,md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-primary.md-checked .md-ink-ripple,md-radio-group.md-THEME_NAME-theme:not([disabled]).md-primary .md-checked .md-ink-ripple,md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-primary .md-checked .md-ink-ripple{color:\"{{primary-color-0.87}}\"}md-radio-button.md-THEME_NAME-theme:not([disabled]).md-primary .md-container .md-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-primary .md-container .md-ripple,md-radio-group.md-THEME_NAME-theme:not([disabled]).md-primary .md-container .md-ripple,md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-primary .md-container .md-ripple{color:\"{{primary-600}}\"}md-radio-button.md-THEME_NAME-theme:not([disabled]).md-warn .md-on,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-warn .md-on,md-radio-group.md-THEME_NAME-theme:not([disabled]).md-warn .md-on,md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-warn .md-on{background-color:\"{{warn-color-0.87}}\"}md-radio-button.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked .md-off,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-warn.md-checked .md-off,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-warn .md-checked .md-off,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-warn .md-checked .md-off,md-radio-group.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked .md-off,md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-warn.md-checked .md-off,md-radio-group.md-THEME_NAME-theme:not([disabled]).md-warn .md-checked .md-off,md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-warn .md-checked .md-off{border-color:\"{{warn-color-0.87}}\"}md-radio-button.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked .md-ink-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-warn.md-checked .md-ink-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-warn .md-checked .md-ink-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-warn .md-checked .md-ink-ripple,md-radio-group.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked .md-ink-ripple,md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-warn.md-checked .md-ink-ripple,md-radio-group.md-THEME_NAME-theme:not([disabled]).md-warn .md-checked .md-ink-ripple,md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-warn .md-checked .md-ink-ripple{color:\"{{warn-color-0.87}}\"}md-radio-button.md-THEME_NAME-theme:not([disabled]).md-warn .md-container .md-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-warn .md-container .md-ripple,md-radio-group.md-THEME_NAME-theme:not([disabled]).md-warn .md-container .md-ripple,md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-warn .md-container .md-ripple{color:\"{{warn-600}}\"}md-radio-button.md-THEME_NAME-theme[disabled],md-radio-group.md-THEME_NAME-theme[disabled]{color:\"{{foreground-3}}\"}md-radio-button.md-THEME_NAME-theme[disabled] .md-container .md-off,md-radio-button.md-THEME_NAME-theme[disabled] .md-container .md-on,md-radio-group.md-THEME_NAME-theme[disabled] .md-container .md-off,md-radio-group.md-THEME_NAME-theme[disabled] .md-container .md-on{border-color:\"{{foreground-3}}\"}md-radio-group.md-THEME_NAME-theme .md-checked .md-ink-ripple{color:\"{{accent-color-0.26}}\"}md-radio-group.md-THEME_NAME-theme .md-checked:not([disabled]).md-primary .md-ink-ripple,md-radio-group.md-THEME_NAME-theme.md-primary .md-checked:not([disabled]) .md-ink-ripple{color:\"{{primary-color-0.26}}\"}md-radio-group.md-THEME_NAME-theme.md-focused.ng-empty>md-radio-button:first-child .md-container:before{background-color:\"{{foreground-3-0.26}}\"}md-radio-group.md-THEME_NAME-theme.md-focused:not(:empty) .md-checked .md-container:before{background-color:\"{{accent-color-0.26}}\"}md-radio-group.md-THEME_NAME-theme.md-focused:not(:empty) .md-checked.md-primary .md-container:before,md-radio-group.md-THEME_NAME-theme.md-focused:not(:empty).md-primary .md-checked .md-container:before{background-color:\"{{primary-color-0.26}}\"}md-radio-group.md-THEME_NAME-theme.md-focused:not(:empty) .md-checked.md-warn .md-container:before,md-radio-group.md-THEME_NAME-theme.md-focused:not(:empty).md-warn .md-checked .md-container:before{background-color:\"{{warn-color-0.26}}\"}md-input-container md-select.md-THEME_NAME-theme .md-select-value span:first-child:after{color:\"{{warn-A700}}\"}md-input-container:not(.md-input-focused):not(.md-input-invalid) md-select.md-THEME_NAME-theme .md-select-value span:first-child:after{color:\"{{foreground-3}}\"}md-input-container.md-input-focused:not(.md-input-has-value) md-select.md-THEME_NAME-theme .md-select-value,md-input-container.md-input-focused:not(.md-input-has-value) md-select.md-THEME_NAME-theme .md-select-value.md-select-placeholder{color:\"{{primary-color}}\"}md-input-container.md-input-invalid md-select.md-THEME_NAME-theme .md-select-value{border-bottom-color:\"{{warn-A700}}\"!important;color:\"{{warn-A700}}\"!important}md-input-container.md-input-invalid md-select.md-THEME_NAME-theme.md-no-underline .md-select-value{border-bottom-color:transparent!important}md-input-container:not(.md-input-invalid).md-input-focused.md-accent .md-select-value{border-color:\"{{accent-color}}\"}md-input-container:not(.md-input-invalid).md-input-focused.md-accent .md-select-value span{color:\"{{accent-color}}\"}md-input-container:not(.md-input-invalid).md-input-focused.md-warn .md-select-value{border-color:\"{{warn-A700}}\"}md-input-container:not(.md-input-invalid).md-input-focused.md-warn .md-select-value span{color:\"{{warn-A700}}\"}md-select.md-THEME_NAME-theme[disabled] .md-select-value{background-image:linear-gradient(90deg,\"{{foreground-3}}\" 0,\"{{foreground-3}}\" 33%,transparent 0);background-image:-ms-linear-gradient(left,transparent 0,\"{{foreground-3}}\" 100%);border-bottom-color:transparent}md-select.md-THEME_NAME-theme .md-select-value{border-bottom-color:\"{{foreground-4}}\"}md-select.md-THEME_NAME-theme .md-select-value.md-select-placeholder{color:\"{{foreground-3}}\"}md-select.md-THEME_NAME-theme .md-select-value span:first-child:after{color:\"{{warn-A700}}\"}md-select.md-THEME_NAME-theme.md-no-underline .md-select-value{border-bottom-color:transparent!important}md-select.md-THEME_NAME-theme.ng-invalid.ng-touched .md-select-value{border-bottom-color:\"{{warn-A700}}\"!important;color:\"{{warn-A700}}\"!important}md-select.md-THEME_NAME-theme.ng-invalid.ng-touched.md-no-underline .md-select-value{border-bottom-color:transparent!important}md-select.md-THEME_NAME-theme:not([disabled]):focus .md-select-value{border-bottom-color:\"{{primary-color}}\";color:\"{{ foreground-1 }}\"}md-select.md-THEME_NAME-theme:not([disabled]):focus .md-select-value.md-select-placeholder{color:\"{{ foreground-1 }}\"}md-select.md-THEME_NAME-theme:not([disabled]):focus.md-no-underline .md-select-value{border-bottom-color:transparent!important}md-select.md-THEME_NAME-theme:not([disabled]):focus.md-accent .md-select-value{border-bottom-color:\"{{accent-color}}\"}md-select.md-THEME_NAME-theme:not([disabled]):focus.md-warn .md-select-value{border-bottom-color:\"{{warn-color}}\"}md-select.md-THEME_NAME-theme[disabled] .md-select-icon,md-select.md-THEME_NAME-theme[disabled] .md-select-value,md-select.md-THEME_NAME-theme[disabled] .md-select-value.md-select-placeholder{color:\"{{foreground-3}}\"}md-select.md-THEME_NAME-theme .md-select-icon{color:\"{{foreground-2}}\"}md-select-menu.md-THEME_NAME-theme md-content{background-color:\"{{background-hue-1}}\"}md-select-menu.md-THEME_NAME-theme md-content md-optgroup{color:\"{{foreground-2}}\"}md-select-menu.md-THEME_NAME-theme md-content md-option{color:\"{{foreground-1}}\"}md-select-menu.md-THEME_NAME-theme md-content md-option[disabled] .md-text{color:\"{{foreground-3}}\"}md-select-menu.md-THEME_NAME-theme md-content md-option:not([disabled]):hover{background-color:\"{{background-500-0.10}}\"}md-select-menu.md-THEME_NAME-theme md-content md-option:not([disabled]).md-focused,md-select-menu.md-THEME_NAME-theme md-content md-option:not([disabled]):focus{background-color:\"{{background-500-0.18}}\"}md-select-menu.md-THEME_NAME-theme md-content md-option[selected]{color:\"{{primary-500}}\"}md-select-menu.md-THEME_NAME-theme md-content md-option[selected].md-focused,md-select-menu.md-THEME_NAME-theme md-content md-option[selected]:focus{color:\"{{primary-600}}\"}md-select-menu.md-THEME_NAME-theme md-content md-option[selected].md-accent{color:\"{{accent-color}}\"}md-select-menu.md-THEME_NAME-theme md-content md-option[selected].md-accent.md-focused,md-select-menu.md-THEME_NAME-theme md-content md-option[selected].md-accent:focus{color:\"{{accent-A700}}\"}.md-checkbox-enabled.md-THEME_NAME-theme .md-ripple{color:\"{{primary-600}}\"}.md-checkbox-enabled.md-THEME_NAME-theme[selected] .md-ripple{color:\"{{background-600}}\"}.md-checkbox-enabled.md-THEME_NAME-theme .md-ink-ripple{color:\"{{foreground-2}}\"}.md-checkbox-enabled.md-THEME_NAME-theme[selected] .md-ink-ripple{color:\"{{primary-color-0.87}}\"}.md-checkbox-enabled.md-THEME_NAME-theme:not(.md-checked) .md-icon{border-color:\"{{foreground-2}}\"}.md-checkbox-enabled.md-THEME_NAME-theme[selected] .md-icon{background-color:\"{{primary-color-0.87}}\"}.md-checkbox-enabled.md-THEME_NAME-theme[selected].md-focused .md-container:before{background-color:\"{{primary-color-0.26}}\"}.md-checkbox-enabled.md-THEME_NAME-theme[selected] .md-icon:after{border-color:\"{{primary-contrast-0.87}}\"}.md-checkbox-enabled.md-THEME_NAME-theme .md-indeterminate[disabled] .md-container{color:\"{{foreground-3}}\"}.md-checkbox-enabled.md-THEME_NAME-theme md-option .md-text{color:\"{{foreground-1}}\"}md-sidenav.md-THEME_NAME-theme,md-sidenav.md-THEME_NAME-theme md-content{background-color:\"{{background-hue-1}}\"}md-slider.md-THEME_NAME-theme .md-track{background-color:\"{{foreground-3}}\"}md-slider.md-THEME_NAME-theme .md-track-ticks{color:\"{{background-contrast}}\"}md-slider.md-THEME_NAME-theme .md-focus-ring{background-color:\"{{accent-A200-0.2}}\"}md-slider.md-THEME_NAME-theme .md-disabled-thumb{background-color:\"{{background-color}}\";border-color:\"{{background-color}}\"}md-slider.md-THEME_NAME-theme.md-min .md-thumb:after{background-color:\"{{background-color}}\";border-color:\"{{foreground-3}}\"}md-slider.md-THEME_NAME-theme.md-min .md-focus-ring{background-color:\"{{foreground-3-0.38}}\"}md-slider.md-THEME_NAME-theme.md-min[md-discrete] .md-thumb:after{background-color:\"{{background-contrast}}\";border-color:transparent}md-slider.md-THEME_NAME-theme.md-min[md-discrete] .md-sign{background-color:\"{{background-400}}\"}md-slider.md-THEME_NAME-theme.md-min[md-discrete] .md-sign:after{border-top-color:\"{{background-400}}\"}md-slider.md-THEME_NAME-theme.md-min[md-discrete][md-vertical] .md-sign:after{border-left-color:\"{{background-400}}\";border-top-color:transparent}md-slider.md-THEME_NAME-theme .md-track.md-track-fill{background-color:\"{{accent-color}}\"}md-slider.md-THEME_NAME-theme .md-thumb:after{background-color:\"{{accent-color}}\";border-color:\"{{accent-color}}\"}md-slider.md-THEME_NAME-theme .md-sign{background-color:\"{{accent-color}}\"}md-slider.md-THEME_NAME-theme .md-sign:after{border-top-color:\"{{accent-color}}\"}md-slider.md-THEME_NAME-theme[md-vertical] .md-sign:after{border-left-color:\"{{accent-color}}\";border-top-color:transparent}md-slider.md-THEME_NAME-theme .md-thumb-text{color:\"{{accent-contrast}}\"}md-slider.md-THEME_NAME-theme.md-warn .md-focus-ring{background-color:\"{{warn-200-0.38}}\"}md-slider.md-THEME_NAME-theme.md-warn .md-track.md-track-fill{background-color:\"{{warn-color}}\"}md-slider.md-THEME_NAME-theme.md-warn .md-thumb:after{background-color:\"{{warn-color}}\";border-color:\"{{warn-color}}\"}md-slider.md-THEME_NAME-theme.md-warn .md-sign{background-color:\"{{warn-color}}\"}md-slider.md-THEME_NAME-theme.md-warn .md-sign:after{border-top-color:\"{{warn-color}}\"}md-slider.md-THEME_NAME-theme.md-warn[md-vertical] .md-sign:after{border-left-color:\"{{warn-color}}\";border-top-color:transparent}md-slider.md-THEME_NAME-theme.md-warn .md-thumb-text{color:\"{{warn-contrast}}\"}md-slider.md-THEME_NAME-theme.md-primary .md-focus-ring{background-color:\"{{primary-200-0.38}}\"}md-slider.md-THEME_NAME-theme.md-primary .md-track.md-track-fill{background-color:\"{{primary-color}}\"}md-slider.md-THEME_NAME-theme.md-primary .md-thumb:after{background-color:\"{{primary-color}}\";border-color:\"{{primary-color}}\"}md-slider.md-THEME_NAME-theme.md-primary .md-sign{background-color:\"{{primary-color}}\"}md-slider.md-THEME_NAME-theme.md-primary .md-sign:after{border-top-color:\"{{primary-color}}\"}md-slider.md-THEME_NAME-theme.md-primary[md-vertical] .md-sign:after{border-left-color:\"{{primary-color}}\";border-top-color:transparent}md-slider.md-THEME_NAME-theme.md-primary .md-thumb-text{color:\"{{primary-contrast}}\"}md-slider.md-THEME_NAME-theme[disabled] .md-thumb:after{border-color:transparent}md-slider.md-THEME_NAME-theme[disabled]:not(.md-min) .md-thumb:after,md-slider.md-THEME_NAME-theme[disabled][md-discrete] .md-thumb:after{background-color:\"{{foreground-3}}\";border-color:transparent}md-slider.md-THEME_NAME-theme[disabled][readonly] .md-sign{background-color:\"{{background-400}}\"}md-slider.md-THEME_NAME-theme[disabled][readonly] .md-sign:after{border-top-color:\"{{background-400}}\"}md-slider.md-THEME_NAME-theme[disabled][readonly][md-vertical] .md-sign:after{border-left-color:\"{{background-400}}\";border-top-color:transparent}md-slider.md-THEME_NAME-theme[disabled][readonly] .md-disabled-thumb{background-color:transparent;border-color:transparent}md-slider-container[disabled]>:first-child:not(md-slider),md-slider-container[disabled]>:last-child:not(md-slider){color:\"{{foreground-3}}\"}.md-subheader.md-THEME_NAME-theme{background-color:\"{{background-default}}\";color:\"{{ foreground-2-0.54 }}\"}.md-subheader.md-THEME_NAME-theme.md-primary{color:\"{{primary-color}}\"}.md-subheader.md-THEME_NAME-theme.md-accent{color:\"{{accent-color}}\"}.md-subheader.md-THEME_NAME-theme.md-warn{color:\"{{warn-color}}\"}md-switch.md-THEME_NAME-theme .md-ink-ripple{color:\"{{background-500}}\"}md-switch.md-THEME_NAME-theme .md-thumb{background-color:\"{{background-50}}\"}md-switch.md-THEME_NAME-theme .md-bar{background-color:\"{{background-500}}\"}md-switch.md-THEME_NAME-theme.md-focused:not(.md-checked) .md-thumb:before,md-switch.md-THEME_NAME-theme.md-focused[disabled] .md-thumb:before{background-color:\"{{foreground-4}}\"}md-switch.md-THEME_NAME-theme.md-checked:not([disabled]) .md-ink-ripple{color:\"{{accent-color}}\"}md-switch.md-THEME_NAME-theme.md-checked:not([disabled]) .md-thumb{background-color:\"{{accent-color}}\"}md-switch.md-THEME_NAME-theme.md-checked:not([disabled]) .md-bar{background-color:\"{{accent-color-0.5}}\"}md-switch.md-THEME_NAME-theme.md-checked:not([disabled]).md-focused .md-thumb:before{background-color:\"{{accent-color-0.26}}\"}md-switch.md-THEME_NAME-theme.md-checked:not([disabled]).md-primary .md-ink-ripple{color:\"{{primary-color}}\"}md-switch.md-THEME_NAME-theme.md-checked:not([disabled]).md-primary .md-thumb{background-color:\"{{primary-color}}\"}md-switch.md-THEME_NAME-theme.md-checked:not([disabled]).md-primary .md-bar{background-color:\"{{primary-color-0.5}}\"}md-switch.md-THEME_NAME-theme.md-checked:not([disabled]).md-primary.md-focused .md-thumb:before{background-color:\"{{primary-color-0.26}}\"}md-switch.md-THEME_NAME-theme.md-checked:not([disabled]).md-warn .md-ink-ripple{color:\"{{warn-color}}\"}md-switch.md-THEME_NAME-theme.md-checked:not([disabled]).md-warn .md-thumb{background-color:\"{{warn-color}}\"}md-switch.md-THEME_NAME-theme.md-checked:not([disabled]).md-warn .md-bar{background-color:\"{{warn-color-0.5}}\"}md-switch.md-THEME_NAME-theme.md-checked:not([disabled]).md-warn.md-focused .md-thumb:before{background-color:\"{{warn-color-0.26}}\"}md-switch.md-THEME_NAME-theme[disabled] .md-thumb{background-color:\"{{background-400}}\"}md-switch.md-THEME_NAME-theme[disabled] .md-bar{background-color:\"{{foreground-4}}\"}md-tabs.md-THEME_NAME-theme md-tabs-wrapper{background-color:transparent;border-color:\"{{foreground-4}}\"}md-tabs.md-THEME_NAME-theme md-next-button md-icon,md-tabs.md-THEME_NAME-theme md-prev-button md-icon{color:\"{{foreground-2}}\"}md-tabs.md-THEME_NAME-theme md-ink-bar{background:\"{{accent-color}}\";color:\"{{accent-color}}\"}md-tabs.md-THEME_NAME-theme .md-tab{color:\"{{foreground-2}}\"}md-tabs.md-THEME_NAME-theme .md-tab[disabled],md-tabs.md-THEME_NAME-theme .md-tab[disabled] md-icon{color:\"{{foreground-3}}\"}md-tabs.md-THEME_NAME-theme .md-tab.md-active,md-tabs.md-THEME_NAME-theme .md-tab.md-active md-icon,md-tabs.md-THEME_NAME-theme .md-tab.md-focused,md-tabs.md-THEME_NAME-theme .md-tab.md-focused md-icon{color:\"{{accent-color}}\"}md-tabs.md-THEME_NAME-theme .md-tab.md-focused{background:\"{{primary-color-0.1}}\"}md-tabs.md-THEME_NAME-theme .md-tab .md-ripple-container{color:\"{{accent-A100}}\"}md-tabs.md-THEME_NAME-theme.md-accent>md-tabs-wrapper{background-color:\"{{accent-500}}\"}md-tabs.md-THEME_NAME-theme.md-accent>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]),md-tabs.md-THEME_NAME-theme.md-accent>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]) md-icon,md-tabs.md-THEME_NAME-theme.md-accent>md-tabs-wrapper md-next-button md-icon,md-tabs.md-THEME_NAME-theme.md-accent>md-tabs-wrapper md-prev-button md-icon{color:\"{{accent-500-contrast-0.7}}\"}md-tabs.md-THEME_NAME-theme.md-accent>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-active,md-tabs.md-THEME_NAME-theme.md-accent>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-active md-icon,md-tabs.md-THEME_NAME-theme.md-accent>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-focused,md-tabs.md-THEME_NAME-theme.md-accent>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-focused md-icon{color:\"{{accent-500-contrast-1}}\"}md-tabs.md-THEME_NAME-theme.md-accent>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-focused{background:\"{{accent-500-contrast-0.1}}\"}md-tabs.md-THEME_NAME-theme.md-accent>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-ink-bar{background:\"{{accent-500-contrast}}\";color:\"{{accent-500-contrast}}\"}md-tabs.md-THEME_NAME-theme.md-primary>md-tabs-wrapper{background-color:\"{{primary-color}}\"}md-tabs.md-THEME_NAME-theme.md-primary>md-tabs-wrapper md-next-button md-icon,md-tabs.md-THEME_NAME-theme.md-primary>md-tabs-wrapper md-prev-button md-icon{color:\"{{primary-contrast}}\"}md-tabs.md-THEME_NAME-theme.md-primary>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]),md-tabs.md-THEME_NAME-theme.md-primary>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]) md-icon{color:\"{{primary-contrast-0.7}}\"}md-tabs.md-THEME_NAME-theme.md-primary>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-active,md-tabs.md-THEME_NAME-theme.md-primary>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-active md-icon,md-tabs.md-THEME_NAME-theme.md-primary>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-focused,md-tabs.md-THEME_NAME-theme.md-primary>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-focused md-icon{color:\"{{primary-contrast}}\"}md-tabs.md-THEME_NAME-theme.md-primary>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-focused{background:\"{{primary-contrast-0.1}}\"}md-tabs.md-THEME_NAME-theme.md-primary>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-ink-bar{background:\"{{accent-color}}\";color:\"{{accent-color}}\"}md-tabs.md-THEME_NAME-theme.md-primary.md-no-ink-bar-color>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-ink-bar{background:\"{{primary-contrast}}\";color:\"{{primary-contrast}}\"}md-tabs.md-THEME_NAME-theme.md-warn>md-tabs-wrapper{background-color:\"{{warn-500}}\"}md-tabs.md-THEME_NAME-theme.md-warn>md-tabs-wrapper md-next-button md-icon,md-tabs.md-THEME_NAME-theme.md-warn>md-tabs-wrapper md-prev-button md-icon{color:\"{{warn-500-contrast}}\"}md-tabs.md-THEME_NAME-theme.md-warn>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]),md-tabs.md-THEME_NAME-theme.md-warn>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]) md-icon{color:\"{{warn-500-contrast-0.7}}\"}md-tabs.md-THEME_NAME-theme.md-warn>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-active,md-tabs.md-THEME_NAME-theme.md-warn>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-active md-icon,md-tabs.md-THEME_NAME-theme.md-warn>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-focused,md-tabs.md-THEME_NAME-theme.md-warn>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-focused md-icon{color:\"{{warn-500-contrast-1}}\"}md-tabs.md-THEME_NAME-theme.md-warn>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-focused{background:\"{{warn-500-contrast-0.1}}\"}md-tabs.md-THEME_NAME-theme.md-warn>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-ink-bar{background:\"{{warn-500-contrast}}\";color:\"{{warn-500-contrast}}\"}md-toolbar>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper{background-color:\"{{primary-color}}\"}md-toolbar>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper md-next-button md-icon,md-toolbar>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper md-prev-button md-icon{color:\"{{primary-contrast}}\"}md-toolbar>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]),md-toolbar>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]) md-icon{color:\"{{primary-contrast-0.7}}\"}md-toolbar>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-active,md-toolbar>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-active md-icon,md-toolbar>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-focused,md-toolbar>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-focused md-icon{color:\"{{primary-contrast}}\"}md-toolbar>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-focused{background:\"{{primary-contrast-0.1}}\"}md-toolbar>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-ink-bar{background:\"{{accent-color}}\";color:\"{{accent-color}}\"}md-toolbar>md-tabs.md-THEME_NAME-theme.md-no-ink-bar-color>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-ink-bar{background:\"{{primary-contrast}}\";color:\"{{primary-contrast}}\"}md-toolbar.md-accent>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper{background-color:\"{{accent-500}}\"}md-toolbar.md-accent>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]),md-toolbar.md-accent>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]) md-icon,md-toolbar.md-accent>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper md-next-button md-icon,md-toolbar.md-accent>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper md-prev-button md-icon{color:\"{{accent-500-contrast-0.7}}\"}md-toolbar.md-accent>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-active,md-toolbar.md-accent>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-active md-icon,md-toolbar.md-accent>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-focused,md-toolbar.md-accent>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-focused md-icon{color:\"{{accent-500-contrast-1}}\"}md-toolbar.md-accent>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-focused{background:\"{{accent-500-contrast-0.1}}\"}md-toolbar.md-accent>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-ink-bar{background:\"{{accent-500-contrast}}\";color:\"{{accent-500-contrast}}\"}md-toolbar.md-warn>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper{background-color:\"{{warn-500}}\"}md-toolbar.md-warn>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper md-next-button md-icon,md-toolbar.md-warn>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper md-prev-button md-icon{color:\"{{warn-500-contrast}}\"}md-toolbar.md-warn>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]),md-toolbar.md-warn>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]) md-icon{color:\"{{warn-500-contrast-0.7}}\"}md-toolbar.md-warn>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-active,md-toolbar.md-warn>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-active md-icon,md-toolbar.md-warn>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-focused,md-toolbar.md-warn>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-focused md-icon{color:\"{{warn-500-contrast-1}}\"}md-toolbar.md-warn>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-tab-item:not([disabled]).md-focused{background:\"{{warn-500-contrast-0.1}}\"}md-toolbar.md-warn>md-tabs.md-THEME_NAME-theme>md-tabs-wrapper>md-tabs-canvas>md-pagination-wrapper>md-ink-bar{background:\"{{warn-500-contrast}}\";color:\"{{warn-500-contrast}}\"}md-toast.md-THEME_NAME-theme .md-toast-content{background-color:#323232;color:\"{{background-50}}\"}md-toast.md-THEME_NAME-theme .md-toast-content .md-button{color:\"{{background-50}}\"}md-toast.md-THEME_NAME-theme .md-toast-content .md-button.md-highlight{color:\"{{accent-color}}\"}md-toast.md-THEME_NAME-theme .md-toast-content .md-button.md-highlight.md-primary{color:\"{{primary-color}}\"}md-toast.md-THEME_NAME-theme .md-toast-content .md-button.md-highlight.md-warn{color:\"{{warn-color}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar){background-color:\"{{primary-color}}\";color:\"{{primary-contrast}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar) md-icon{fill:\"{{primary-contrast}}\";color:\"{{primary-contrast}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar) .md-button[disabled] md-icon{fill:\"{{primary-contrast-0.26}}\";color:\"{{primary-contrast-0.26}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar) md-input-container[md-no-float] .md-input{border-color:\"{{primary-default-contrast-divider}}\";color:\"{{primary-default-contrast}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar) md-input-container[md-no-float] .md-input::-webkit-input-placeholder{color:\"{{primary-default-contrast-hint}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar) md-input-container[md-no-float] .md-input:-ms-input-placeholder{color:\"{{primary-default-contrast-hint}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar) md-input-container[md-no-float] .md-input::-ms-input-placeholder{color:\"{{primary-default-contrast-hint}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar) md-input-container[md-no-float] .md-input::placeholder{color:\"{{primary-default-contrast-hint}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar) md-input-container[md-no-float] .md-input:-moz-placeholder,md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar) md-input-container[md-no-float] .md-input::-moz-placeholder{color:\"{{primary-default-contrast-hint}}\";opacity:1}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar) md-input-container[md-no-float].md-input-focused .md-input::-webkit-input-placeholder{color:\"{{primary-default-contrast-secondary}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar) md-input-container[md-no-float].md-input-focused .md-input:-ms-input-placeholder{color:\"{{primary-default-contrast-secondary}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar) md-input-container[md-no-float].md-input-focused .md-input::-ms-input-placeholder{color:\"{{primary-default-contrast-secondary}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar) md-input-container[md-no-float].md-input-focused .md-input::placeholder{color:\"{{primary-default-contrast-secondary}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar) md-input-container[md-no-float].md-input-focused .md-input:-moz-placeholder,md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar) md-input-container[md-no-float].md-input-focused .md-input::-moz-placeholder{color:\"{{primary-default-contrast-secondary}}\";opacity:1}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar) md-input-container[md-no-float]:not(.md-input-invalid).md-input-focused .md-input,md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar) md-input-container[md-no-float]:not(.md-input-invalid).md-input-resized .md-input{border-color:\"{{primary-contrast}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar) md-input-container[md-no-float]:not(.md-input-invalid).md-input-focused.md-accent .md-input{border-color:\"{{accent-color}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar) md-input-container[md-no-float]:not(.md-input-invalid).md-input-focused.md-warn .md-input{border-color:\"{{warn-A700}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-accent{background-color:\"{{accent-500}}\";color:\"{{accent-500-contrast}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-accent .md-ink-ripple{color:\"{{accent-500-contrast}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-accent md-icon{fill:\"{{accent-500-contrast}}\";color:\"{{accent-500-contrast}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-accent .md-button[disabled] md-icon{fill:\"{{accent-500-contrast-0.26}}\";color:\"{{accent-500-contrast-0.26}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-accent md-input-container[md-no-float] .md-input{border-color:\"{{accent-500-contrast-divider}}\";color:\"{{accent-500-contrast}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-accent md-input-container[md-no-float] .md-input::-webkit-input-placeholder{color:\"{{accent-500-contrast-hint}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-accent md-input-container[md-no-float] .md-input:-ms-input-placeholder{color:\"{{accent-500-contrast-hint}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-accent md-input-container[md-no-float] .md-input::-ms-input-placeholder{color:\"{{accent-500-contrast-hint}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-accent md-input-container[md-no-float] .md-input::placeholder{color:\"{{accent-500-contrast-hint}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-accent md-input-container[md-no-float] .md-input:-moz-placeholder,md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-accent md-input-container[md-no-float] .md-input::-moz-placeholder{color:\"{{accent-500-contrast-hint}}\";opacity:1}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-accent md-input-container[md-no-float].md-input-focused .md-input::-webkit-input-placeholder{color:\"{{accent-500-contrast-secondary}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-accent md-input-container[md-no-float].md-input-focused .md-input:-ms-input-placeholder{color:\"{{accent-500-contrast-secondary}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-accent md-input-container[md-no-float].md-input-focused .md-input::-ms-input-placeholder{color:\"{{accent-500-contrast-secondary}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-accent md-input-container[md-no-float].md-input-focused .md-input::placeholder{color:\"{{accent-500-contrast-secondary}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-accent md-input-container[md-no-float].md-input-focused .md-input:-moz-placeholder,md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-accent md-input-container[md-no-float].md-input-focused .md-input::-moz-placeholder{color:\"{{accent-500-contrast-secondary}}\";opacity:1}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-accent md-input-container[md-no-float]:not(.md-input-invalid).md-input-focused .md-input,md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-accent md-input-container[md-no-float]:not(.md-input-invalid).md-input-resized .md-input{border-color:\"{{primary-color}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-accent md-input-container[md-no-float]:not(.md-input-invalid).md-input-focused.md-accent .md-input{border-color:\"{{accent-500-contrast}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-accent md-input-container[md-no-float]:not(.md-input-invalid).md-input-focused.md-warn .md-input{border-color:\"{{warn-A700}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-warn{background-color:\"{{warn-500}}\";color:\"{{warn-500-contrast}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-warn md-icon{fill:\"{{warn-500-contrast}}\";color:\"{{warn-500-contrast}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-warn md-input-container[md-no-float] .md-input{border-color:\"{{warn-500-contrast-divider}}\";color:\"{{warn-500-contrast}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-warn md-input-container[md-no-float] .md-input::-webkit-input-placeholder{color:\"{{warn-500-contrast-hint}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-warn md-input-container[md-no-float] .md-input:-ms-input-placeholder{color:\"{{warn-500-contrast-hint}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-warn md-input-container[md-no-float] .md-input::-ms-input-placeholder{color:\"{{warn-500-contrast-hint}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-warn md-input-container[md-no-float] .md-input::placeholder{color:\"{{warn-500-contrast-hint}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-warn md-input-container[md-no-float] .md-input:-moz-placeholder,md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-warn md-input-container[md-no-float] .md-input::-moz-placeholder{color:\"{{warn-500-contrast-hint}}\";opacity:1}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-warn md-input-container[md-no-float].md-input-focused .md-input::-webkit-input-placeholder{color:\"{{warn-500-contrast-secondary}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-warn md-input-container[md-no-float].md-input-focused .md-input:-ms-input-placeholder{color:\"{{warn-500-contrast-secondary}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-warn md-input-container[md-no-float].md-input-focused .md-input::-ms-input-placeholder{color:\"{{warn-500-contrast-secondary}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-warn md-input-container[md-no-float].md-input-focused .md-input::placeholder{color:\"{{warn-500-contrast-secondary}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-warn md-input-container[md-no-float].md-input-focused .md-input:-moz-placeholder,md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-warn md-input-container[md-no-float].md-input-focused .md-input::-moz-placeholder{color:\"{{warn-500-contrast-secondary}}\";opacity:1}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-warn md-input-container[md-no-float]:not(.md-input-invalid).md-input-focused .md-input,md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-warn md-input-container[md-no-float]:not(.md-input-invalid).md-input-resized .md-input{border-color:\"{{primary-color}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-warn md-input-container[md-no-float]:not(.md-input-invalid).md-input-focused.md-accent .md-input{border-color:\"{{accent-color}}\"}md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-warn md-input-container[md-no-float]:not(.md-input-invalid).md-input-focused.md-warn .md-input{border-color:\"{{warn-500-contrast}}\"}.md-panel.md-tooltip.md-THEME_NAME-theme{background-color:\"{{background-700}}\";color:\"{{background-700-contrast}}\"}body.md-THEME_NAME-theme,html.md-THEME_NAME-theme{background-color:\"{{background-color}}\";color:\"{{foreground-1}}\"}");
|
||
})();
|
||
|
||
|
||
})(window, window.angular);;window.ngMaterial={version:{full: "1.2.4"}}; |