UNPKG

nocca

Version:
1,512 lines (1,319 loc) 536 kB
/*! * Angular Material Design * https://github.com/angular/material * @license MIT * v0.10.0 */ (function( window, angular, undefined ){ "use strict"; (function(){ "use strict"; angular.module('ngMaterial', ["ng","ngAnimate","ngAria","material.core","material.core.gestures","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.content","material.components.chips","material.components.dialog","material.components.fabActions","material.components.divider","material.components.fabSpeedDial","material.components.fabToolbar","material.components.gridList","material.components.fabTrigger","material.components.icon","material.components.input","material.components.list","material.components.menu","material.components.progressCircular","material.components.progressLinear","material.components.radioButton","material.components.select","material.components.sidenav","material.components.slider","material.components.subheader","material.components.sticky","material.components.swipe","material.components.switch","material.components.tabs","material.components.toast","material.components.toolbar","material.components.tooltip","material.components.whiteframe"]); })(); (function(){ "use strict"; /** * Initialization function that validates environment * requirements. */ angular .module('material.core', [ 'material.core.gestures', 'material.core.theming' ]) .config( MdCoreConfigure ); function MdCoreConfigure($provide, $mdThemingProvider) { $provide.decorator('$$rAF', ["$delegate", rAFDecorator]); $mdThemingProvider.theme('default') .primaryPalette('indigo') .accentPalette('pink') .warnPalette('red') .backgroundPalette('grey'); } MdCoreConfigure.$inject = ["$provide", "$mdThemingProvider"]; 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 queueArgs, alreadyQueued, queueCb, context; return function debounced() { queueArgs = arguments; context = this; queueCb = cb; if (!alreadyQueued) { alreadyQueued = true; $delegate(function() { queueCb.apply(context, queueArgs); alreadyQueued = false; }); } }; }; return $delegate; } })(); (function(){ "use strict"; angular.module('material.core') .factory('$mdConstant', MdConstantFactory); function MdConstantFactory($$rAF, $sniffer) { var webkit = /webkit/i.test($sniffer.vendorPrefix); function vendorProperty(name) { return webkit ? ('webkit' + name.charAt(0).toUpperCase() + name.substring(1)) : name; } return { KEY_CODE: { ENTER: 13, ESCAPE: 27, SPACE: 32, LEFT_ARROW : 37, UP_ARROW : 38, RIGHT_ARROW : 39, DOWN_ARROW : 40, TAB : 9, BACKSPACE: 8, DELETE: 46 }, CSS: { /* Constants */ TRANSITIONEND: 'transitionend' + (webkit ? ' webkitTransitionEnd' : ''), ANIMATIONEND: 'animationend' + (webkit ? ' 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') }, MEDIA: { 'sm': '(max-width: 600px)', 'gt-sm': '(min-width: 600px)', 'md': '(min-width: 600px) and (max-width: 960px)', 'gt-md': '(min-width: 960px)', 'lg': '(min-width: 960px) and (max-width: 1200px)', 'gt-lg': '(min-width: 1200px)' }, MEDIA_PRIORITY: [ 'gt-lg', 'lg', 'gt-md', 'md', 'gt-sm', 'sm' ] }; } MdConstantFactory.$inject = ["$$rAF", "$sniffer"]; })(); (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 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; } } } })(); (function(){ "use strict"; 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. * (`sm`, `gt-sm`, `md`, `gt-md`, `lg`, `gt-lg`). * * @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> */ 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] = $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 (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 (!attrs[normalizedName]) { return; } 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)); } } mdMediaFactory.$inject = ["$mdConstant", "$rootScope", "$window"]; })(); (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. */ var nextUniqueId = 0; angular.module('material.core') .factory('$mdUtil', ["$cacheFactory", "$document", "$timeout", "$q", "$window", "$mdConstant", function($cacheFactory, $document, $timeout, $q, $window, $mdConstant) { var Util; function getNode(el) { return el[0] || el; } return Util = { now: window.performance ? angular.bind(window.performance, window.performance.now) : Date.now, 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 Util.clientRect(element, offsetParent, true); }, // Annoying method to copy nodes to an array, thanks to IE nodesToArray: function (nodes) { var results = []; for (var i = 0; i < nodes.length; ++i) { results.push(nodes.item(i)); } return results; }, // Disables scroll around the passed element. disableScrollAround: function(element) { if (Util.disableScrollAround._enableScrolling) return Util.disableScrollAround._enableScrolling; element = angular.element(element); var body = $document[0].body, restoreBody = disableBodyScroll(), restoreElement = disableElementScroll(); return Util.disableScrollAround._enableScrolling = function () { restoreBody(); restoreElement(); delete Util.disableScrollAround._enableScrolling; }; // Creates a virtual scrolling mask to absorb touchmove, keyboard, scrollbar clicking, and wheel events function disableElementScroll() { var zIndex = $window.getComputedStyle(element[0]).zIndex - 1; if (isNaN(zIndex)) zIndex = 99; var scrollMask = angular.element( '<div class="md-scroll-mask" style="z-index: ' + zIndex + '">' + ' <div class="md-scroll-mask-bar"></div>' + '</div>'); body.appendChild(scrollMask[0]); scrollMask.on('wheel', preventDefault); scrollMask.on('touchmove', preventDefault); $document.on('keydown', disableKeyNav); return function restoreScroll () { scrollMask.off('wheel'); scrollMask.off('touchmove'); scrollMask[0].parentNode.removeChild(scrollMask[0]); $document.off('keydown', disableKeyNav); delete Util.disableScrollAround._enableScrolling; }; // Prevent keypresses from elements inside the body // used to stop the keypresses that could cause the page to scroll // (arrow keys, spacebar, tab, etc). function disableKeyNav(e) { //-- temporarily removed this logic, will possibly re-add at a later date return; if (!element[0].contains(e.target)) { e.preventDefault(); e.stopImmediatePropagation(); } } function preventDefault(e) { e.preventDefault(); } } // Converts the body to a position fixed block and translate it to the proper scroll // position function disableBodyScroll() { var restoreStyle = body.getAttribute('style') || ''; var scrollOffset = body.scrollTop + body.parentElement.scrollTop; applyStyles(body, { position: 'fixed', width: '100%', overflowY: 'scroll', top: -scrollOffset + 'px' }); return function restoreScroll() { body.setAttribute('style', restoreStyle); body.scrollTop = scrollOffset; }; } function applyStyles (el, styles) { for (var key in styles) { el.style[key] = styles[key]; } } }, enableScrolling: function () { var method = this.disableScrollAround._enableScrolling; method && method(); }, floatingScrollbars: function() { if (this.floatingScrollbars.cached === undefined) { var tempNode = angular.element('<div style="width: 100%; z-index: -1; position: absolute; height: 35px; overflow-y: scroll"><div style="height: 60;"></div></div>'); $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); }, transitionEndPromise: function(element, opts) { opts = opts || {}; var deferred = $q.defer(); element.on($mdConstant.CSS.TRANSITIONEND, finished); function finished(ev) { // Make sure this transitionend didn't bubble up from a child if (!ev || ev.target === element[0]) { element.off($mdConstant.CSS.TRANSITIONEND, finished); deferred.resolve(); } } if (opts.timeout) $timeout(finished, opts.timeout); return deferred.promise; }, 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 = Util.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 = Util.now(); cb(); return Util.now() - start; }, /** * 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 tagName Tag name to find closest to el, such as 'form' */ getClosest: function getClosest(el, tagName, onlyParent) { if (el instanceof angular.element) el = el[0]; tagName = tagName.toUpperCase(); if (onlyParent) el = el.parentNode; if (!el) return null; do { if (el.nodeName === tagName) { return el; } } while (el = el.parentNode); return null; }, /** * Functional equivalent for $element.filter(‘md-bottom-sheet’) * useful with interimElements where the element and its container are important... */ extractElementByName: function (element, nodeName) { for (var i = 0, len = element.length; i < len; i++) { if (element[i].nodeName.toLowerCase() === nodeName){ return angular.element(element[i]); } } return element; }, /** * Give optional properties with no value a boolean true by default */ initOptionalProperties: function (scope, attr, defaults ) { defaults = defaults || { }; angular.forEach(scope.$$isolateBindings, function (binding, key) { if (binding.optional && angular.isUndefined(scope[key])) { var hasKey = attr.hasOwnProperty(attr.$normalize(binding.attrName)); scope[key] = angular.isDefined(defaults[key]) ? defaults[key] : hasKey; } }); } }; }]); /* * 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`. * * TODO(ajoslin): This should be added in a better place later. */ 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"; angular.module('material.core') .service('$mdAria', AriaService); /* * @ngInject */ function AriaService($$rAF, $log, $window) { return { expect: expect, expectAsync: expectAsync, expectWithText: expectWithText }; /** * Check if expected attribute has been specified on the target element or child * @param element * @param attrName * @param {optional} defaultValue What to set the attr to if no value is found */ function expect(element, attrName, defaultValue) { var node = 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 { $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) { expectAsync(element, attrName, function() { return getText(element); }); } function getText(element) { return element.text().trim(); } 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; } } AriaService.$inject = ["$$rAF", "$log", "$window"]; })(); (function(){ "use strict"; angular.module('material.core') .service('$mdCompiler', mdCompilerService); function mdCompilerService($q, $http, $injector, $compile, $controller, $templateCache) { /* jshint validthis: true */ /* * @ngdoc service * @name $mdCompiler * @module material.core * @description * The $mdCompiler service is an abstraction of angular's compiler, that allows the developer * to easily compile an element with a templateUrl, controller, and locals. * * @usage * <hljs lang="js"> * $mdCompiler.compile({ * templateUrl: 'modal.html', * controller: 'ModalCtrl', * locals: { * modal: myModalInstance; * } * }).then(function(compileData) { * compileData.element; // modal.html's template in an element * compileData.link(myScope); //attach controller & scope to element * }); * </hljs> */ /* * @ngdoc method * @name $mdCompiler#compile * @description A helper to compile an HTML template/templateUrl with a given controller, * locals, and scope. * @param {object} options An options object, with the following properties: * * - `controller` - `{(string=|function()=}` Controller fn that should be associated with * newly created scope or the name of a registered controller if passed as a string. * - `controllerAs` - `{string=}` A controller alias name. If present the controller will be * published to scope under the `controllerAs` name. * - `template` - `{string=}` An html template as a string. * - `templateUrl` - `{string=}` A path to an html template. * - `transformTemplate` - `{function(template)=}` A function which transforms the template after * it is loaded. It will be given the template string as a parameter, and should * return a a new string representing the transformed template. * - `resolve` - `{Object.<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 {object=} promise A promise, which will be resolved with a `compileData` object. * `compileData` has the following properties: * * - `element` - `{element}`: an uncompiled element matching the provided template. * - `link` - `{function(scope)}`: A link function, which, when called, will compile * the element and instantiate the provided controller (if given). * - `locals` - `{object}`: The locals which will be passed into the controller once `link` is * called. If `bindToController` is true, they will be coppied to the ctrl instead * - `bindToController` - `bool`: bind the locals to the controller, instead of passing them in. */ this.compile = function(options) { var templateUrl = options.templateUrl; var template = options.template || ''; var controller = options.controller; var controllerAs = options.controllerAs; var resolve = options.resolve || {}; var locals = options.locals || {}; var transformTemplate = options.transformTemplate || angular.identity; var bindToController = options.bindToController; // Take resolve values and invoke them. // Resolves can either be a string (value: 'MyRegisteredAngularConst'), // or an invokable 'factory' of sorts: (value: function ValueGetter($dependency) {}) angular.forEach(resolve, function(value, key) { if (angular.isString(value)) { resolve[key] = $injector.get(value); } else { resolve[key] = $injector.invoke(value); } }); //Add the locals, which are just straight values to inject //eg locals: { three: 3 }, will inject three into the controller angular.extend(resolve, locals); if (templateUrl) { resolve.$template = $http.get(templateUrl, {cache: $templateCache}) .then(function(response) { return response.data; }); } else { resolve.$template = $q.when(template); } // Wait for all the resolves to finish if they are promises return $q.all(resolve).then(function(locals) { var template = transformTemplate(locals.$template); var element = options.element || angular.element('<div>').html(template.trim()).contents(); var linkFn = $compile(element); //Return a linking function that can be used later when the element is ready return { locals: locals, element: element, link: function link(scope) { locals.$scope = scope; //Instantiate controller if it exists, because we have scope if (controller) { var invokeCtrl = $controller(controller, locals, true); if (bindToController) { angular.extend(invokeCtrl.instance, locals); } var ctrl = invokeCtrl(); //See angular-route source for this logic element.data('$ngControllerController', ctrl); element.children().data('$ngControllerController', ctrl); if (controllerAs) { scope[controllerAs] = ctrl; } } return linkFn(scope); } }; }); }; } mdCompilerService.$inject = ["$q", "$http", "$injector", "$compile", "$controller", "$templateCache"]; })(); (function(){ "use strict"; var HANDLERS = {}; /* The state of the current 'pointer' * The pointer represents the state of the current touch. * It contains normalized x and y coordinates from DOM events, * as well as other information abstracted from the DOM. */ var pointer, lastPointer, forceSkipClickHijack = false; // Used to attach event listeners once when multiple ng-apps are running. var isInitialized = false; 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. * * <hljs lang="js"> * app.config(function($mdGestureProvider) { * * // For mobile devices without jQuery loaded, do not * // intercept click events during the capture phase. * $mdGestureProvider.skipClickHijack(); * * }); * </hljs> * */ function MdGestureProvider() { } MdGestureProvider.prototype = { // Publish access to setter to configure a variable BEFORE the // $mdGesture service is instantiated... skipClickHijack: function() { return forceSkipClickHijack = true; }, /** * $get is used to build an instance of $mdGesture * @ngInject */ $get : ["$$MdGestureHandler", "$$rAF", "$timeout", function($$MdGestureHandler, $$rAF, $timeout) { return new MdGesture($$MdGestureHandler, $$rAF, $timeout); }] }; /** * MdGesture factory construction function * @ngInject */ function MdGesture($$MdGestureHandler, $$rAF, $timeout) { var userAgent = navigator.userAgent || navigator.vendor || window.opera; var isIos = userAgent.match(/ipad|iphone|ipod/i); var isAndroid = userAgent.match(/android/i); var hasJQuery = (typeof window.jQuery !== 'undefined') && (angular.element === window.jQuery); var self = { handler: addHandler, register: register, // On mobile w/out jQuery, we normally intercept clicks. Should we skip that? isHijackingClicks: (isIos || isAndroid) && !hasJQuery && !forceSkipClickHijack }; if (self.isHijackingClicks) { self.handler('click', { options: { maxDistance: 6 }, onEnd: function (ev, pointer) { if (pointer.distance < this.state.options.maxDistance) { this.dispatchEvent(ev, 'click'); } } }); } /* * 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, horziontal: 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. 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 perpindicular 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 }, 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. 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 unnecessray 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. * TODO: add vertical swiping with a `horizontal` option similar to the drag handler. */ .handler('swipe', { options: { minVelocity: 0.65, minDistance: 10 }, onEnd: function (ev, pointer) { if (Math.abs(pointer.velocityX) > this.state.options.minVelocity && Math.abs(pointer.distanceX) > this.state.options.minDistance) { var eventType = pointer.directionX == 'left' ? '$md.swipeleft' : '$md.swiperight'; this.dispatchEvent(ev, eventType); } } }); } MdGesture.$inject = ["$$MdGestureHandler", "$$rAF", "$timeout"]; /** * 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 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.onEnd(ev, pointer); this.state.isRunning = false; }, 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 reigsters 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); return onDestroy; function onDestroy() { delete element[0].$mdGesture[self.name]; element.off('$destroy', onDestroy); } } }; 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') { eventObj = document.createEvent('MouseEvents'); eventObj.initMouseEvent( 'click', true, true, window, srcEvent.detail, eventPointer.x,