/*! * Angular Material Design * https://github.com/angular/material * @license MIT * v0.5.0 */ (function() { angular.module('ngMaterial', ["ng","ngAnimate","ngAria","material.core","material.decorators","material.animations","material.components.backdrop","material.components.bottomSheet","material.components.button","material.components.card","material.components.checkbox","material.components.content","material.components.dialog","material.components.divider","material.components.icon","material.components.list","material.components.progressCircular","material.components.progressLinear","material.components.radioButton","material.components.sidenav","material.components.slider","material.components.sticky","material.components.subheader","material.components.swipe","material.components.switch","material.components.tabs","material.components.textField","material.components.toast","material.components.toolbar","material.components.tooltip","material.components.whiteframe","material.services.aria","material.services.attrBind","material.services.compiler","material.services.interimElement","material.services.media","material.services.registry","material.services.theming"]);})(); (function() { /** * Angular Mds initialization function that validates environment * requirements. */ angular.module('material.core', [] ) .run(function validateEnvironment() { if (typeof Hammer === 'undefined') { throw new Error( 'ngMaterial requires HammerJS to be preloaded.' ); } }); })(); (function() { angular.module('material.core') .constant('$mdConstant', { KEY_CODE: { ENTER: 13, ESCAPE: 27, SPACE: 32, LEFT_ARROW : 37, UP_ARROW : 38, RIGHT_ARROW : 39, DOWN_ARROW : 40 } }); })(); (function() { angular.module('material.core') .factory('$mdUtil', ['$cacheFactory', function($cacheFactory) { var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g; /* for nextUid() function below */ var uid = ['0','0','0']; var Util; return Util = { now: window.performance ? angular.bind(window.performance, window.performance.now) : Date.now, /** * Checks if the specified element has an ancestor (ancestor being parent, grandparent, etc) * with the given attribute defined. * * Also pass in an optional `limit` (levels of ancestry to scan), default 4. */ ancestorHasAttribute: function ancestorHasAttribute(element, attrName, limit) { limit = limit || 4; var current = element; while (limit-- && current.length) { if (current[0].hasAttribute && current[0].hasAttribute(attrName)) { return true; } current = current.parent(); } return false; }, /** * Checks to see if the element or its parents are disabled. * @param element DOM element to start scanning for `disabled` attribute * @param limit Number of parent levels that should be scanned; defaults to 4 * @returns {*} Boolean */ isParentDisabled: function isParentDisabled(element, limit) { return Util.ancestorHasAttribute(element, 'disabled', limit); }, /** * Checks if two elements have the same parent */ elementIsSibling: function elementIsSibling(element, otherElement) { return element.parent().length && (element.parent()[0] === otherElement.parent()[0]); }, /** * Converts snake_case to camelCase. * @param name Name to normalize */ camelCase: function camelCase(name) { return name .replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) { return offset ? letter.toUpperCase() : letter; }); }, /** * Selects 'n' words from a string * for use in an HTML attribute */ stringFromTextBody: function stringFromTextBody(textBody, numWords) { var string = textBody.trim(); if(string.split(/\s+/).length > numWords){ string = textBody.split(/\s+/).slice(1, (numWords + 1)).join(" ") + '...'; } return string; }, /** * Publish the iterator facade to easily support iteration and accessors * @see iterator below */ iterator: iterator, /** * @see cacheFactory below */ cacheFactory: cacheFactory, // 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. If `immediate` is passed, trigger the function on the // leading edge, instead of the trailing. debounce: function debounce(func, wait, immediate) { var timeout; return function debounced() { var context = this, args = arguments; clearTimeout(timeout); timeout = setTimeout(function() { timeout = null; if (!immediate) func.apply(context, args); }, wait); if (immediate && !timeout) func.apply(context, args); }; }, // 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 || recent - now > delay) { func.apply(context, args); recent = now; } }; }, /** * Wraps an element with a tag * * @param el element to wrap * @param tag tag to wrap it with * @param [className] optional class to apply to the wrapper * @returns new element * */ wrap: function(el, tag, className) { if(el.hasOwnProperty(0)) { el = el[0]; } var wrapper = document.createElement(tag); wrapper.className += className; wrapper.appendChild(el.parentNode.replaceChild(wrapper, el)); return angular.element(wrapper); }, /** * nextUid, from angular.js. * A consistent way of creating unique IDs in angular. The ID is a sequence of alpha numeric * characters such as '012ABC'. The reason why we are not using simply a number counter is that * the number string gets longer over time, and it can also overflow, where as the nextId * will grow much slower, it is a string, and it will never overflow. * * @returns an unique alpha-numeric string */ nextUid: function() { var index = uid.length; var digit; while(index) { index--; digit = uid[index].charCodeAt(0); if (digit == 57 /*'9'*/) { uid[index] = 'A'; return uid.join(''); } if (digit == 90 /*'Z'*/) { uid[index] = '0'; } else { uid[index] = String.fromCharCode(digit + 1); return uid.join(''); } } uid.unshift('0'); return uid.join(''); }, // 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; } } }; /* * 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 iterator(items, reloop) { var trueFn = function() { return true; }; 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: next, previous: previous, 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); } /* * 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 * determine whether the next item is valid. If not valid, it will try to find the * next item again. * @param item * @param {optional} validate * @returns {*} */ function next(item, validate) { validate = validate || trueFn; if (contains(item)) { var index = indexOf(item) + 1, found = inRange(index) ? _items[ index ] : (reloop ? first() : null); return validate(found) ? found : next(found, validate); } return null; } /* * Find the previous item. If reloop is true and at the beginning of the list, it will * go back to the last item. If given ,the `validate` callback will be used * determine whether the previous item is valid. If not valid, it will try to find the * previous item again. * @param item * @param {optional} validate * @returns {*} */ function previous(item, validate) { validate = validate || trueFn; if (contains(item)) { var index = indexOf(item) - 1, found = inRange(index) ? _items[ index ] : (reloop ? last() : null); return validate(found) ? found : previous(found, validate); } return null; } /* * 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; } } /* * Angular's $cacheFactory doesn't have a keys() method, * so we add one ourself. */ function cacheFactory(id, options) { var cache = $cacheFactory(id, options); var keys = {}; cache._put = cache.put; cache.put = function(k,v) { keys[k] = true; return cache._put(k, v); }; cache._remove = cache.remove; cache.remove = function(k) { delete keys[k]; return cache._remove(k); }; cache.keys = function() { return Object.keys(keys); }; return cache; } }]); /* * 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() { angular.module('material.decorators', []) .config(['$provide', function($provide) { $provide.decorator('$$rAF', ['$delegate', '$rootScope', rAFDecorator]); function rAFDecorator($$rAF, $rootScope) { /** * Use this to debounce events that come in often. * The debounced 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-debounced 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 */ $$rAF.debounce = function(cb) { var queueArgs, alreadyQueued, queueCb, context; return function debounced() { queueArgs = arguments; context = this; queueCb = cb; if (!alreadyQueued) { alreadyQueued = true; $$rAF(function() { queueCb.apply(context, queueArgs); alreadyQueued = false; }); } }; }; return $$rAF; } }]); })(); (function() { /* * @ngdoc module * @name material.components.animate * @description * * Ink and Popup Effects */ angular.module('material.animations', ['material.core']) .service('$mdEffects', [ '$rootElement', '$$rAF', '$sniffer', '$q', MdEffects ]); /* * @ngdoc service * @name $mdEffects * @module material.components.animate * * @description * The `$mdEffects` service provides a simple API for various * Material Design effects. * * @returns A `$mdEffects` object with the following properties: * - `{function(element,styles,duration)}` `inkBar` - starts ink bar * animation on specified DOM element * - `{function(element,parentElement,clickElement)}` `popIn` - animated show of element overlayed on parent element * - `{function(element,parentElement)}` `popOut` - animated close of popup overlay * */ function MdEffects($rootElement, $$rAF, $sniffer, $q) { var webkit = /webkit/i.test($sniffer.vendorPrefix); function vendorProperty(name) { return webkit ? ('webkit' + name.charAt(0).toUpperCase() + name.substring(1)) : name; } var self; // Publish API for effects... return self = { popIn: popIn, /* Constants */ TRANSITIONEND_EVENT: 'transitionend' + (webkit ? ' webkitTransitionEnd' : ''), ANIMATIONEND_EVENT: 'animationend' + (webkit ? ' webkitAnimationEnd' : ''), TRANSFORM: vendorProperty('transform'), 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') }; // ********************************************************** // API Methods // ********************************************************** function popIn(element, parentElement, clickElement) { var deferred = $q.defer(); parentElement.append(element); var startPos; if (clickElement) { var clickRect = clickElement[0].getBoundingClientRect(); startPos = translateString( clickRect.left - element[0].offsetWidth, clickRect.top - element[0].offsetHeight, 0 ) + ' scale(0.2)'; } else { startPos = 'translate3d(0,100%,0) scale(0.5)'; } element .css(self.TRANSFORM, startPos) .css('opacity', 0); $$rAF(function() { $$rAF(function() { element .addClass('md-active') .css(self.TRANSFORM, '') .css('opacity', '') .on(self.TRANSITIONEND_EVENT, finished); }); }); function finished(ev) { //Make sure this transitionend didn't bubble up from a child if (ev.target === element[0]) { element.off(self.TRANSITIONEND_EVENT, finished); deferred.resolve(); } } return deferred.promise; } // ********************************************************** // Utility Methods // ********************************************************** function translateString(x, y, z) { return 'translate3d(' + Math.floor(x) + 'px,' + Math.floor(y) + 'px,' + Math.floor(z) + 'px)'; } } })(); (function() { angular.module('material.animations') .directive('inkRipple', [ '$mdInkRipple', InkRippleDirective ]) .factory('$mdInkRipple', [ '$window', '$$rAF', '$mdEffects', '$timeout', '$mdUtil', InkRippleService ]); function InkRippleDirective($mdInkRipple) { return function(scope, element, attr) { if (attr.inkRipple == 'checkbox') { $mdInkRipple.attachCheckboxBehavior(element); } else { $mdInkRipple.attachButtonBehavior(element); } }; } function InkRippleService($window, $$rAF, $mdEffects, $timeout, $mdUtil) { return { attachButtonBehavior: attachButtonBehavior, attachCheckboxBehavior: attachCheckboxBehavior, attach: attach }; function attachButtonBehavior(element) { return attach(element, { mousedown: true, center: false, animationDuration: 350, mousedownPauseTime: 175, animationName: 'inkRippleButton', animationTimingFunction: 'linear' }); } function attachCheckboxBehavior(element) { return attach(element, { mousedown: true, center: true, animationDuration: 300, mousedownPauseTime: 180, animationName: 'inkRippleCheckbox', animationTimingFunction: 'linear' }); } function attach(element, options) { // Parent element with noink attr? Abort. if (element.controller('noink')) return angular.noop; var contentParent = element.controller('mdContent'); options = angular.extend({ mousedown: true, hover: true, focus: true, center: false, animationDuration: 300, mousedownPauseTime: 150, animationName: '', animationTimingFunction: 'linear' }, options || {}); var rippleContainer; var node = element[0]; var hammertime = new Hammer(node); if (options.mousedown) { hammertime.on('hammer.input', onInput); } // Publish self-detach method if desired... return function detach() { hammertime.destroy(); if (rippleContainer) { rippleContainer.remove(); } }; function rippleIsAllowed() { return !element[0].hasAttribute('disabled'); } function createRipple(left, top, positionsAreAbsolute) { var rippleEl = angular.element('
') .css($mdEffects.ANIMATION_DURATION, options.animationDuration + 'ms') .css($mdEffects.ANIMATION_NAME, options.animationName) .css($mdEffects.ANIMATION_TIMING, options.animationTimingFunction) .on($mdEffects.ANIMATIONEND_EVENT, function() { rippleEl.remove(); }); if (!rippleContainer) { rippleContainer = angular.element('
'); element.append(rippleContainer); } rippleContainer.append(rippleEl); var containerWidth = rippleContainer.prop('offsetWidth'); if (options.center) { left = containerWidth / 2; top = rippleContainer.prop('offsetHeight') / 2; } else if (positionsAreAbsolute) { var elementRect = node.getBoundingClientRect(); left -= elementRect.left; top -= elementRect.top; } if (contentParent) { top += contentParent.$element.prop('scrollTop'); } var css = { 'background-color': $window.getComputedStyle(rippleEl[0]).color || $window.getComputedStyle(node).color, 'border-radius': (containerWidth / 2) + 'px', left: (left - containerWidth / 2) + 'px', width: containerWidth + 'px', top: (top - containerWidth / 2) + 'px', height: containerWidth + 'px' }; css[$mdEffects.ANIMATION_DURATION] = options.fadeoutDuration + 'ms'; rippleEl.css(css); return rippleEl; } var pauseTimeout; var rippleEl; function onInput(ev) { if (ev.eventType === Hammer.INPUT_START && ev.isFirst && rippleIsAllowed()) { rippleEl = createRipple(ev.center.x, ev.center.y, true); pauseTimeout = $timeout(function() { rippleEl && rippleEl.css($mdEffects.ANIMATION_PLAY_STATE, 'paused'); }, options.mousedownPauseTime, false); rippleEl.on('$destroy', function() { rippleEl = null; }); } else if (ev.eventType === Hammer.INPUT_END && ev.isFinal) { $timeout.cancel(pauseTimeout); rippleEl && rippleEl.css($mdEffects.ANIMATION_PLAY_STATE, ''); } } } } })(); (function() { angular.module('material.animations') /** * noink/nobar/nostretch directive: make any element that has one of * these attributes be given a controller, so that other directives can * `require:` these and see if there is a `no` parent attribute. * * @usage * * * * * * * * * myApp.directive('detectNo', function() { * return { * require: ['^?noink', ^?nobar'], * link: function(scope, element, attr, ctrls) { * var noinkCtrl = ctrls[0]; * var nobarCtrl = ctrls[1]; * if (noInkCtrl) { * alert("the noink flag has been specified on an ancestor!"); * } * if (nobarCtrl) { * alert("the nobar flag has been specified on an ancestor!"); * } * } * }; * }); * */ .directive({ noink: attrNoDirective(), nobar: attrNoDirective(), nostretch: attrNoDirective() }); function attrNoDirective() { return function() { return { controller: angular.noop }; }; } })(); (function() { /* * @ngdoc module * @name material.components.backdrop * @description Backdrop */ /** * @ngdoc directive * @name mdBackdrop * @module material.components.backdrop * * @restrict E * * @description * `` is a backdrop element used by other coponents, such as dialog and bottom sheet. * Apply class `opaque` to make the backdrop use the theme backdrop color. * */ angular.module('material.components.backdrop', [ 'material.services.theming' ]) .directive('mdBackdrop', [ '$mdTheming', BackdropDirective ]); function BackdropDirective($mdTheming) { return $mdTheming; } })(); (function() { /** * @ngdoc module * @name material.components.bottomSheet * @description * BottomSheet */ angular.module('material.components.bottomSheet', [ 'material.components.backdrop', 'material.services.interimElement', 'material.services.theming' ]) .directive('mdBottomSheet', [ MdBottomSheetDirective ]) .factory('$mdBottomSheet', [ '$$interimElement', '$animate', '$mdEffects', '$timeout', '$$rAF', '$compile', '$mdTheming', MdBottomSheet ]); function MdBottomSheetDirective() { return { restrict: 'E' }; } /** * @ngdoc service * @name $mdBottomSheet * @module material.components.bottomSheet * * @description * `$mdBottomSheet` opens a bottom sheet over the app and provides a simple promise API. * * ### Restrictions * * - The bottom sheet's template must have an outer `` element. * - Add the `md-grid` class to the bottom sheet for a grid layout. * - Add the `md-list` class to the bottom sheet for a list layout. * * @usage * *
* * Open a Bottom Sheet! * *
*
* * var app = angular.module('app', ['ngMaterial']); * app.controller('MyController', function($scope, $mdBottomSheet) { * $scope.openBottomSheet = function() { * $mdBottomSheet.show({ * template: 'Hello!' * }); * }; * }); * */ /** * @ngdoc method * @name $mdBottomSheet#show * * @description * Show a bottom sheet with the specified options. * * @param {object} options An options object, with the following properties: * * - `templateUrl` - `{string=}`: The url of an html template file that will * be used as the content of the bottom sheet. Restrictions: the template must * have an outer `md-bottom-sheet` element. * - `template` - `{string=}`: Same as templateUrl, except this is an actual * template string. * - `controller` - `{string=}`: The controller to associate with this bottom sheet. * - `locals` - `{string=}`: An object containing key/value pairs. The keys will * be used as names of values to inject into the controller. For example, * `locals: {three: 3}` would inject `three` into the controller with the value * of 3. * - `targetEvent` - `{DOMClickEvent=}`: A click's event object. When passed in as an option, * the location of the click will be used as the starting point for the opening animation * of the the dialog. * - `resolve` - `{object=}`: Similar to locals, except it takes promises as values * and the bottom sheet will not open until the promises resolve. * - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope. * * @returns {promise} A promise that can be resolved with `$mdBottomSheet.hide()` or * rejected with `$mdBottomSheet.cancel()`. */ /** * @ngdoc method * @name $mdBottomSheet#hide * * @description * Hide the existing bottom sheet and resolve the promise returned from * `$mdBottomSheet.show()`. * * @param {*=} response An argument for the resolved promise. * */ /** * @ngdoc method * @name $mdBottomSheet#cancel * * @description * Hide the existing bottom sheet and reject the promise returned from * `$mdBottomSheet.show()`. * * @param {*=} response An argument for the rejected promise. * */ function MdBottomSheet($$interimElement, $animate, $mdEffects, $timeout, $$rAF, $compile, $mdTheming) { var backdrop; var $mdBottomSheet; return $mdBottomSheet = $$interimElement({ themable: true, targetEvent: null, onShow: onShow, onRemove: onRemove, }); function onShow(scope, element, options) { // Add a backdrop that will close on click backdrop = $compile('')(scope); backdrop.on('click touchstart', function() { $timeout($mdBottomSheet.cancel); }); $mdTheming.inherit(backdrop, options.parent); $animate.enter(backdrop, options.parent, null); var bottomSheet = new BottomSheet(element); options.bottomSheet = bottomSheet; // Give up focus on calling item options.targetEvent && angular.element(options.targetEvent.target).blur(); $mdTheming.inherit(bottomSheet.element, options.parent); return $animate.enter(bottomSheet.element, options.parent); } function onRemove(scope, element, options) { var bottomSheet = options.bottomSheet; $animate.leave(backdrop); return $animate.leave(bottomSheet.element).then(function() { bottomSheet.cleanup(); // Restore focus options.targetEvent && angular.element(options.targetEvent.target).focus(); }); } /** * BottomSheet class to apply bottom-sheet behavior to an element */ function BottomSheet(element) { var MAX_OFFSET = 80; // amount past the bottom of the element that we can drag down, this is same as in _bottomSheet.scss var WIGGLE_AMOUNT = 20; // point where it starts to get "harder" to drag var CLOSING_VELOCITY = 10; // how fast we need to flick down to close the sheet var startY, lastY, velocity, transitionDelay, startTarget; // coercion incase $mdCompiler returns multiple elements element = element.eq(0); element.on('touchstart', onTouchStart); element.on('touchmove', onTouchMove); element.on('touchend', onTouchEnd); return { element: element, cleanup: function cleanup() { element.off('touchstart', onTouchStart); element.off('touchmove', onTouchMove); element.off('touchend', onTouchEnd); } }; function onTouchStart(e) { e.preventDefault(); startTarget = e.target; startY = getY(e); // Disable transitions on transform so that it feels fast transitionDelay = element.css($mdEffects.TRANSITION_DURATION); element.css($mdEffects.TRANSITION_DURATION, '0s'); } function onTouchEnd(e) { // Re-enable the transitions on transforms element.css($mdEffects.TRANSITION_DURATION, transitionDelay); var currentY = getY(e); // If we didn't scroll much, and we didn't change targets, assume its a click if ( Math.abs(currentY - startY) < 5 && e.target == startTarget) { angular.element(e.target).triggerHandler('click'); } else { // If they went fast enough, trigger a close. if (velocity > CLOSING_VELOCITY) { $timeout($mdBottomSheet.cancel); // Otherwise, untransform so that we go back to our normal position } else { setTransformY(undefined); } } } function onTouchMove(e) { var currentY = getY(e); var delta = currentY - startY; velocity = currentY - lastY; lastY = currentY; // Do some conversion on delta to get a friction-like effect delta = adjustedDelta(delta); setTransformY(delta + MAX_OFFSET); } /** * Helper function to find the Y aspect of various touch events. **/ function getY(e) { var touch = e.touches && e.touches.length ? e.touches[0] : e.changedTouches[0]; return touch.clientY; } /** * Transform the element along the y-axis **/ function setTransformY(amt) { if (amt === null || amt === undefined) { element.css($mdEffects.TRANSFORM, ''); } else { element.css($mdEffects.TRANSFORM, 'translate3d(0, ' + amt + 'px, 0)'); } } // Returns a new value for delta that will never exceed MAX_OFFSET_AMOUNT // Will get harder to exceed it as you get closer to it function adjustedDelta(delta) { if ( delta < 0 && delta < -MAX_OFFSET + WIGGLE_AMOUNT) { delta = -delta; var base = MAX_OFFSET - WIGGLE_AMOUNT; delta = Math.max(-MAX_OFFSET, -Math.min(MAX_OFFSET - 5, base + ( WIGGLE_AMOUNT * (delta - base)) / MAX_OFFSET) - delta / 50); } return delta; } } } })(); (function() { /** * @ngdoc module * @name material.components.button * @description * * Button */ angular.module('material.components.button', [ 'material.core', 'material.animations', 'material.services.aria', 'material.services.theming' ]) .directive('mdButton', [ 'ngHrefDirective', '$mdInkRipple', '$mdAria', '$mdUtil', '$mdTheming', MdButtonDirective ]); /** * @ngdoc directive * @name mdButton * @module material.components.button * * @restrict E * * @description * `` is a button directive with optional ink ripples (default enabled). * * @param {boolean=} noink If present, disable ripple ink effects. * @param {boolean=} disabled If present, disable tab selection. * @param {string=} type Optional attribute to specific button types (useful for forms); such as 'submit', etc. * @param {string=} ng-href Optional attribute to support both ARIA and link navigation * @param {string=} href Optional attribute to support both ARIA and link navigation * @param {string=} ariaLabel Publish the button label used by screen-readers for accessibility. Defaults to the button's text. * * @usage * * Button *
* * Button (noInk) * *
* * Colored (disabled) * *
*/ function MdButtonDirective(ngHrefDirectives, $mdInkRipple, $mdAria, $mdUtil, $mdTheming ) { var ngHrefDirective = ngHrefDirectives[0]; return { restrict: 'E', compile: function(element, attr) { var innerElement; var attributesToCopy; // Add an inner anchor if the element has a `href` or `ngHref` attribute, // so this element can be clicked like a normal ``. if (attr.ngHref || attr.href) { innerElement = angular.element(''); attributesToCopy = ['ng-href', 'href', 'rel', 'target']; // Otherwise, just add an inner button element (for form submission etc) } else { innerElement = angular.element('