UNPKG

mcs-ng-material

Version:

MCS NG-Meterial is based on mcs-web.

1,695 lines (1,459 loc) 306 kB
/*! * AngularJS Material Design * https://github.com/angular/material * @license MIT * v1.1.6 */ goog.provide('ngmaterial.core'); /** * 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; queue.forEach(functi