mcs-ng-material
Version:
MCS NG-Meterial is based on mcs-web.
1,691 lines (1,460 loc) • 306 kB
JavaScript
/*!
* AngularJS Material Design
* https://github.com/angular/material
* @license MIT
* v1.1.6
*/
(function( window, angular, undefined ){
"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} callback function to debounce
*/
$delegate.throttle = function(cb) {
var queuedArgs, alreadyQueued, queueCb, context;
return function debounced() {
queuedArgs = arguments;
context = this;
queueCb = cb;
if (!alreadyQueued) {
alreadyQueued = true;
$delegate(function() {
queueCb.apply(context, Array.prototype.slice.call(queuedArgs));
alreadyQueued = false;
});
}
};
};
return $delegate;
}
/**
* 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;
}
MdAutofocusDirective['$inject'] = ["$parse"];angular.module('material.core')
.directive('mdAutofocus', MdAutofocusDirective)
// Support the deprecated md-auto-focus and md-sidenav-focus as well
.directive('mdAutoFocus', MdAutofocusDirective)
.directive('mdSidenavFocus', MdAutofocusDirective);
/**
* @ngdoc directive
* @name mdAutofocus
* @module material.core.util
*
* @description
*
* `[md-autofocus]` provides an optional way to identify the focused element when a `$mdDialog`,
* `$mdBottomSheet`, `$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);
}
}
}
/**
* @ngdoc module
* @name material.core.colorUtil
* @description
* Color Util
*/
angular
.module('material.core')
.factory('$mdColorUtil', ColorUtilFactory);
function ColorUtilFactory() {
/**
* Converts hex value to RGBA string
* @param color {string}
* @returns {string}
*/
function hexToRgba (color) {
var hex = color[ 0 ] === '#' ? color.substr(1) : color,
dig = hex.length / 3,
red = hex.substr(0, dig),
green = hex.substr(dig, dig),
blue = hex.substr(dig * 2);
if (dig === 1) {
red += red;
green += green;
blue += blue;
}
return 'rgba(' + parseInt(red, 16) + ',' + parseInt(green, 16) + ',' + parseInt(blue, 16) + ',0.1)';
}
/**
* Converts rgba value to hex string
* @param color {string}
* @returns {string}
*/
function rgbaToHex(color) {
color = color.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
var hex = (color && color.length === 4) ? "#" +
("0" + parseInt(color[1],10).toString(16)).slice(-2) +
("0" + parseInt(color[2],10).toString(16)).slice(-2) +
("0" + parseInt(color[3],10).toString(16)).slice(-2) : '';
return hex.toUpperCase();
}
/**
* Converts an RGB color to RGBA
* @param color {string}
* @returns {string}
*/
function rgbToRgba (color) {
return color.replace(')', ', 0.1)').replace('(', 'a(');
}
/**
* Converts an RGBA color to RGB
* @param color {string}
* @returns {string}
*/
function rgbaToRgb (color) {
return color
? color.replace('rgba', 'rgb').replace(/,[^),]+\)/, ')')
: 'rgb(0,0,0)';
}
return {
rgbaToHex: rgbaToHex,
hexToRgba: hexToRgba,
rgbToRgba: rgbToRgba,
rgbaToRgb: rgbaToRgb
};
}
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;
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]);
}
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;
}
angular
.module('material.core')
.config( ["$provide", function($provide){
$provide.decorator('$mdUtil', ['$delegate', function ($delegate){
/**
* Inject the iterator facade to easily support iteration and accessors
* @see iterator below
*/
$delegate.iterator = MdIterator;
return $delegate;
}
]);
}]);
/**
* iterator is a list facade to easily support iteration and accessors
*
* @param items Array list which this iterator will enumerate
* @param reloop Boolean enables iterator to consider the list as an endless reloop
*/
function MdIterator(items, reloop) {
var trueFn = function() { return true; };
if (items && !angular.isArray(items)) {
items = Array.prototype.slice.call(items);
}
reloop = !!reloop;
var _items = items || [ ];
// Published API
return {
items: getItems,
count: count,
inRange: inRange,
contains: contains,
indexOf: indexOf,
itemAt: itemAt,
findBy: findBy,
add: add,
remove: remove,
first: first,
last: last,
next: angular.bind(null, findSubsequentItem, false),
previous: angular.bind(null, findSubsequentItem, true),
hasPrevious: hasPrevious,
hasNext: hasNext
};
/**
* Publish copy of the enumerable set
* @returns {Array|*}
*/
function getItems() {
return [].concat(_items);
}
/**
* Determine length of the list
* @returns {Array.length|*|number}
*/
function count() {
return _items.length;
}
/**
* Is the index specified valid
* @param index
* @returns {Array.length|*|number|boolean}
*/
function inRange(index) {
return _items.length && ( index > -1 ) && (index < _items.length );
}
/**
* Can the iterator proceed to the next item in the list; relative to
* the specified item.
*
* @param item
* @returns {Array.length|*|number|boolean}
*/
function hasNext(item) {
return item ? inRange(indexOf(item) + 1) : false;
}
/**
* Can the iterator proceed to the previous item in the list; relative to
* the specified item.
*
* @param item
* @returns {Array.length|*|number|boolean}
*/
function hasPrevious(item) {
return item ? inRange(indexOf(item) - 1) : false;
}
/**
* Get item at specified index/position
* @param index
* @returns {*}
*/
function itemAt(index) {
return inRange(index) ? _items[index] : null;
}
/**
* Find all elements matching the key/value pair
* otherwise return null
*
* @param val
* @param key
*
* @return array
*/
function findBy(key, val) {
return _items.filter(function(item) {
return item[key] === val;
});
}
/**
* Add item to list
* @param item
* @param index
* @returns {*}
*/
function add(item, index) {
if ( !item ) return -1;
if (!angular.isNumber(index)) {
index = _items.length;
}
_items.splice(index, 0, item);
return indexOf(item);
}
/**
* Remove item from list...
* @param item
*/
function remove(item) {
if ( contains(item) ){
_items.splice(indexOf(item), 1);
}
}
/**
* Get the zero-based index of the target item
* @param item
* @returns {*}
*/
function indexOf(item) {
return _items.indexOf(item);
}
/**
* Boolean existence check
* @param item
* @returns {boolean}
*/
function contains(item) {
return item && (indexOf(item) > -1);
}
/**
* Return first item in the list
* @returns {*}
*/
function first() {
return _items.length ? _items[0] : null;
}
/**
* Return last item in the list...
* @returns {*}
*/
function last() {
return _items.length ? _items[_items.length - 1] : null;
}
/**
* Find the next item. If reloop is true and at the end of the list, it will go back to the
* first item. If given, the `validate` callback will be used to determine whether the next item
* is valid. If not valid, it will try to find the next item again.
*
* @param {boolean} backwards Specifies the direction of searching (forwards/backwards)
* @param {*} item The item whose subsequent item we are looking for
* @param {Function=} validate The `validate` function
* @param {integer=} limit The recursion limit
*
* @returns {*} The subsequent item or null
*/
function findSubsequentItem(backwards, item, validate, limit) {
validate = validate || trueFn;
var curIndex = indexOf(item);
while (true) {
if (!inRange(curIndex)) return null;
var nextIndex = curIndex + (backwards ? -1 : 1);
var foundItem = null;
if (inRange(nextIndex)) {
foundItem = _items[nextIndex];
} else if (reloop) {
foundItem = backwards ? last() : first();
nextIndex = indexOf(foundItem);
}
if ((foundItem === null) || (nextIndex === limit)) return null;
if (validate(foundItem)) return foundItem;
if (angular.isUndefined(limit)) limit = nextIndex;
curIndex = nextIndex;
}
}
}
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://www.google.com/design/spec/layout/adaptive-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));
}
}
angular
.module('material.core')
.config( ["$provide", function($provide) {
$provide.decorator('$mdUtil', ['$delegate', function ($delegate) {
// Inject the prefixer into our original $mdUtil service.
$delegate.prefixer = MdPrefixer;
return $delegate;
}]);
}]);
function MdPrefixer(initialAttributes, buildSelector) {
var PREFIXES = ['data', 'x'];
if (initialAttributes) {
// The prefixer also accepts attributes as a parameter, and immediately builds a list or selector for
// the specified attributes.
return buildSelector ? _buildSelector(initialAttributes) : _buildList(initialAttributes);
}
return {
buildList: _buildList,
buildSelector: _buildSelector,
hasAttribute: _hasAttribute,
removeAttribute: _removeAttribute
};
function _buildList(attributes) {
attributes = angular.isArray(attributes) ? attributes : [attributes];
attributes.forEach(function(item) {
PREFIXES.forEach(function(prefix) {
attributes.push(prefix + '-' + item);
});
});
return attributes;
}
function _buildSelector(attributes) {
attributes = angular.isArray(attributes) ? attributes : [attributes];
return _buildList(attributes)
.map(function(item) {
return '[' + item + ']';
})
.join(',');
}
function _hasAttribute(element, attribute) {
element = _getNativeElement(element);
if (!element) {
return false;
}
var prefixedAttrs = _buildList(attribute);
for (var i = 0; i < prefixedAttrs.length; i++) {
if (element.hasAttribute(prefixedAttrs[i])) {
return true;
}
}
return false;
}
function _removeAttribute(element, attribute) {
element = _getNativeElement(element);
if (!element) {
return;
}
_buildList(attribute).forEach(function(prefixedAttribute) {
element.removeAttribute(prefixedAttribute);
});
}
/**
* Transforms a jqLite or DOM element into a HTML element.
* This is useful when supporting jqLite elements and DOM elements at
* same time.
* @param element {JQLite|Element} Element to be parsed
* @returns {HTMLElement} Parsed HTMLElement
*/
function _getNativeElement(element) {
element = element[0] || element;
if (element.nodeType) {
return element;
}
}
}
/*
* 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;
/**
* @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 === '}}'));
/**
* 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: {},
now: window.performance && window.performance.now ?
angular.bind(window.performance, window.performance.now) : Date.now || function() {
return new Date().getTime();
},
/**
* Cross-version compatibility method to retrieve an option of a ngModel controller,
* which supports the breaking changes in the AngularJS snapshot (SHA 87a2ff76af5d0a9268d8eb84db5755077d27c84c).
* @param {!angular.ngModelCtrl} ngModelCtrl
* @param {!string} optionName
* @returns {Object|undefined}
*/
getModelOption: function (ngModelCtrl, optionName) {
if (!ngModelCtrl.$options) {
return;
}
var $options = ngModelCtrl.$options;
// The newer versions of 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]
},
/**
* 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 = !($document[0].dir == 'rtl' || $document[0].body.dir == 'rtl');
// 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 = !($document[0].dir == 'rtl' || $document[0].body.dir == 'rtl');
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
nodesToArray: function(nodes) {
nodes = nodes || [];
var results = [];
for (var 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() {
return window.scrollY || window.pageYOffset || 0;
},
/**
* Finds the proper focus target by searching the DOM.
*
* @param containerEl
* @param attributeVal
* @returns {*}
*/
findFocusTarget: function(containerEl, attributeVal) {
var AUTO_FOCUS = this.prefixer('md-autofocus', true);
var elToFocus;
elToFocus = scanForFocusable(containerEl, attributeVal || AUTO_FOCUS);
if ( !elToFocus && attributeVal != AUTO_FOCUS) {
// Scan for deprecated attribute
elToFocus = scanForFocusable(containerEl, this.prefixer('md-auto-focus', true));
if ( !elToFocus ) {
// Scan for fallback to 'universal' API
elToFocus = scanForFocusable(containerEl, AUTO_FOCUS);
}
}
return elToFocus;
/**
* Can target and nested children for specified Selector (attribute)
* whose value may be an expression that evaluates to True/False.
*/
function scanForFocusable(target, selector) {
var elFound, items = target[0].querySelectorAll(selector);
// Find the last child element with the focus attribute
if ( items && items.length ){
items.length && angular.forEach(items, function(it) {
it = angular.element(it);
// Check the element for the md-autofocus class to ensure any associated expression
// evaluated to true.
var isFocusable = it.hasClass('md-autofocus');
if (isFocusable) elFound = it;
});
}
return elFound;
}
},
/**
* Disables scroll around the passed parent element.
* @param element Unused
* @param {!Element|!angular.JQLite} parent Element to disable scrolling within.
* Defaults to body if none supplied.
* @param options Object of options to modify functionality
* - disableScrollMask Boolean of whether or not to create a scroll mask element or
* use the passed parent element.
*/
disableScrollAround: function(element, parent, options) {
options = options || {};
$mdUtil.disableScrollAround._count = Math.max(0, $mdUtil.disableScrollAround._count || 0);
$mdUtil.disableScrollAround._count++;
if ($mdUtil.disableScrollAround._restoreScroll) {
return $mdUtil.disableScrollAround._restoreScroll;
}
var body = $document[0].body;
var restoreBody = disableBodyScroll();
var restoreElement = disableElementScroll(parent);
return $mdUtil.disableScrollAround._restoreScroll = function() {
if (--$mdUtil.disableScrollAround._count <= 0) {
restoreBody();
restoreElement();
delete $mdUtil.disableScrollAround._restoreScroll;
}
};
/**
* Creates a virtual scrolling mask to prevent touchmove, keyboard, scrollbar clicking,
* and wheel events
*/
function disableElementScroll(element) {
element = angular.element(element || body);
var scrollMask;
if (options.disableScrollMask) {
scrollMask = element;
} else {
scrollMask = angular.element(
'<div class="md-scroll-mask">' +
' <div class="md-scroll-mask-bar"></div>' +
'</div>');
element.append(scrollMask);
}
scrollMask.on('wheel', preventDefault);
scrollMask.on('touchmove', preventDefault);
return function restoreElementScroll() {
scrollMask.off('wheel');
scrollMask.off('touchmove');
if (!options.disableScrollMask && scrollMask[0].parentNode ) {
scrollMask[0].parentNode.removeChild(scrollMask[0]);
}
};
function preventDefault(e) {
e.preventDefault();
}
}
// Converts the body to a position fixed block and translate it to the proper scroll position
function disableBodyScroll() {
var documentElement = $document[0].documentElement;
var prevDocumentStyle = documentElement.style.cssText || '';
var prevBodyStyle = body.style.cssText || '';
var viewportTop = $mdUtil.getViewportTop();
var clientWidth = body.clientWidth;
var hasVerticalScrollbar = body.scrollHeight > body.clientHeight + 1;
// 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';
}
// This should be applied after the manipulation to the body, because
// adding a scrollbar can potentially resize it, causing the measurement
// to change.
if (hasVerticalScrollbar) {
documentElement.style.overflowY = 'scroll';
}
return function restoreScroll() {
// 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...
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
};
},
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds.
// @param wait Integer value of msecs to delay (since last debounce reset); default value 10 msecs
// @param invokeApply should the $timeout trigger $digest() dirty checking
debounce: function(func, wait, scope, invokeApply) {
var timer;
return function debounced() {
var context = scope,
args = Array.prototype.slice.call(arguments);
$timeout.cancel(timer);
timer = $timeout(function() {
timer = undefined;
func.apply(context, args);
}, wait || 10, invokeApply);
};
},
// Returns a function that can only be triggered every `delay` milliseconds.
// In other words, the function will not be called unless it has been more
// than `delay` milliseconds since the last call.
throttle: function throttle(func, delay) {
var recent;
return function throttled() {
var context = this;
var args = arguments;
var now = $mdUtil.now();
if (!recent || (now - recent > delay)) {
func.apply(context, args);
recent = now;
}
};
},
/**
* Measures the number of milliseconds taken to run the provided callback
* function. Uses a high-precision timer if available.
*/
time: function time(cb) {
var start = $mdUtil.now();
cb();
return $mdUtil.now() - start;
},
/**
* Create an implicit getter that caches its `getter()`
* lookup value
*/
valueOnUse : function (scope, key, getter) {
var value = null, args = Array.prototype.slice.call(arguments);
var params = (args.length > 3) ? args.slice(3) : [ ];
Object.defineProperty(scope, key, {
get: function () {
if (value === null) value = getter.apply(scope, params);
return value;
}
});
},
/**
* Get a unique ID.
*
* @returns {string} an unique numeric string
*/
nextUid: function() {
return '' + nextUniqueId++;
},
// Stop watchers and events from firing on a scope without destroying it,
// by disconnecting it from its parent and its siblings' linked lists.
disconnectScope: function disconnectScope(scope) {
if (!scope) return;
// we can't destroy the root scope or a scope that has been already destroyed
if (scope.$root === scope) return;
if (scope.$$destroyed) return;
var parent = scope.$parent;
scope.$$disconnected = true;
// See Scope.$destroy
if (parent.$$childHead === scope) parent.$$childHead = scope.$$nextSibling;
if (parent.$$childTail === scope) parent.$$childTail = scope.$$prevSibling;
if (scope.$$prevSibling) scope.$$prevSibling.$$nextSibling = scope.$$nextSibling;
if (scope.$$nextSibling) scope.$$nextSibling.$$prevSibling = scope.$$prevSibling;
scope.$$nextSibling = scope.$$prevSibling = null;
},
// Undo the effects of disconnectScope above.
reconnectScope: function reconnectScope(scope) {
if (!scope) return;
// we can't disconnect the root node or scope already disconnected
if (scope.$root === scope) return;
if (!scope.$$disconnected) return;
var child = scope;
var parent = child.$parent;
child.$$disconnected = false;
// See Scope.$new for this logic...
child.$$prevSibling = parent.$$childTail;
if (parent.$$childHead) {
parent.$$childTail.$$nextSibling = child;
parent.$$childTail = child;
} else {
parent.$$childHead = parent.$$childTail = child;
}
},
/*
* getClosest replicates jQuery.closest() to walk up the DOM tree until it finds a matching nodeName
*
* @param el Element to start walking the DOM from
* @param check Either a string or a function. If a string is passed, it will be evaluated against
* each of the parent nodes' tag name. If a function is passed, the loop will call it with each of
* the parents and will use the return value to determine whether the node is a match.
* @param onlyParent Only start checking from the parent element, not `el`.
*/
getClosest: function getClosest(el, 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)
*/
elementContains: function(node, child) {
var hasContains = (window.Node && window.Node.prototype && Node.prototype.contains);
var findFn = hasContains ? angular.bind(node, node.contains) : angular.bind(node, function(arg) {
// compares the positions of two nodes and returns a bitmask
return (node === child) || !!(this.compareDocumentPosition(arg) & 16)
});
return findFn(child);
},
/**
* Functional equivalent for $element.filter(‘md-bottom-sheet’)
* useful with interimElements where the element and its container are important...
*
* @param {[]} elements to scan
* @param {string} name of node to find (e.g. 'md-dialog')
* @param {boolean=} optional flag to allow deep scans; defaults to 'false'.
* @param {boolean=} optional flag to enable log warnings; defaults to false
*/
extractElementByName: function(element, nodeName, scanDeep, warnNotFound) {
var found = scanTree(element);
if (!found && !!warnNotFound) {
$log.warn( $mdUtil.supplant("Unable to find node '{0}' in element '{1}'.",[nodeName, element[0].outerHTML]) );
}
return angular.element(found || element);
/**
* Breadth-First tree scan for element with matching `nodeName`
*/
function scanTree(element) {
return scanLevel(element) || (scanDeep ? scanChildren(element) : null);
}
/**
* Case-insensitive scan of current elements only (do not descend).
*/
function scanLevel(element) {
if ( element ) {
for (var i = 0, len = element.length; i < len; i++) {
if (element[i].nodeName.toLowerCase() === nodeName) {
return element[i];
}
}
}
return null;
}
/**
* Scan children of specified node
*/
function scanChildren(element) {
var found;
if ( element ) {
for (var i = 0, len = element.length; i < len; i++) {
var target = element[i];
if ( !found ) {
for (var j = 0, numChild = target.childNodes.length; j < numChild; j++) {
found = found || scanTree([target.childNodes[j]]);
}
}
}
}
return found;
}
},
/**
* Give optional properties with no value a boolean true if attr provided or false otherwise
*/
initOptionalProperties: function(scope, attr, defaults) {
defaults = defaults || {};
angular.forEach(scope.$$isolateBindings, function(binding, key) {
if (binding.optional && angular.isUndefined(scope[key])) {
var attrIsDefined = angular.isDefined(attr[binding.attrName]);
scope[key] = angular.isDefined(defaults[key]) ? defaults[key] : attrIsDefined;
}
});
},
/**
* Alternative to $timeout calls with 0 delay.
* nextTick() coalesces all calls within a single frame
* to minimize $digest thrashing
*
* @param callback
* @param digest
* @returns {*}
*/
nextTick: function(callback, digest, scope) {
//-- grab function reference for storing state details
var nextTick = $mdUtil.nextTick;
var timeout = nextTick.timeout;
var queue = nextTick.queue || [];
//-- add callback to the queue
queue.push({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;
que