/*! * Angular Material Design * https://github.com/angular/material * @license MIT * v0.4 */ (function(){ angular.module('ngMaterial', [ 'ng', 'ngAnimate', 'ngAria', 'material.core', 'material.services.attrBind', 'material.services.compiler', 'material.services.registry', 'material.decorators', 'material.services.aria', "material.components.bottomSheet","material.components.button","material.components.card","material.components.checkbox","material.components.circularProgress","material.components.content","material.components.dialog","material.components.divider","material.components.icon","material.components.linearProgress","material.components.list","material.components.radioButton","material.components.sidenav","material.components.slider","material.components.subheader","material.components.switch","material.components.tabs","material.components.textField","material.components.toast","material.components.toolbar","material.components.tooltip","material.components.whiteframe"]); var Constant = { KEY_CODE: { ENTER: 13, ESCAPE: 27, SPACE: 32, LEFT_ARROW : 37, UP_ARROW : 38, RIGHT_ARROW : 39, DOWN_ARROW : 40 } }; /** * Angular Materials initialization function that validates environment * requirements. */ angular.module('material.core',['ng']) .run(function validateEnvironment() { if (angular.isUndefined( window.Hammer )) { throw new Error( '$materialSwipe requires HammerJS to be preloaded.' ); } }); /* * 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; } } var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g; /* for nextUid() function below */ var uid = ['0','0','0']; var 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.js */ iterator: iterator, // 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; } } }; /* * 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; }; /** * @ngdoc module * @name material.components.animate * @description * * Ink and Popup Effects */ angular.module('material.animations', []) .service('$materialEffects', [ '$rootElement', '$$rAF', '$sniffer', '$q', MaterialEffects ]); /** * @ngdoc service * @name $materialEffects * @module material.components.animate * * @description * The `$materialEffects` service provides a simple API for various * Material Design effects. * * @returns A `$materialEffects` 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 MaterialEffects($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('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') /** * 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 }; }; } })(); angular.module('material.animations') .directive('inkRipple', [ '$materialInkRipple', InkRippleDirective ]) .factory('$materialInkRipple', [ '$window', '$$rAF', '$materialEffects', '$timeout', InkRippleService ]); function InkRippleDirective($materialInkRipple) { return function(scope, element, attr) { if (attr.inkRipple == 'checkbox') { $materialInkRipple.attachCheckboxBehavior(element); } else { $materialInkRipple.attachButtonBehavior(element); } }; } function InkRippleService($window, $$rAF, $materialEffects, $timeout) { // TODO fix this. doesn't support touch AND click devices (eg chrome pixel) var hasTouch = !!('ontouchend' in document); var POINTERDOWN_EVENT = hasTouch ? 'touchstart' : 'mousedown'; var POINTERUP_EVENT = hasTouch ? 'touchend touchcancel' : 'mouseup mouseleave'; 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; 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]; if (options.mousedown) { listenPointerDown(true); } // Publish self-detach method if desired... return function detach() { listenPointerDown(false); if (rippleContainer) { rippleContainer.remove(); } }; function listenPointerDown(shouldListen) { element[shouldListen ? 'on' : 'off'](POINTERDOWN_EVENT, onPointerDown); } function rippleIsAllowed() { return !Util.isParentDisabled(element); } function createRipple(left, top, positionsAreAbsolute) { var rippleEl = angular.element('
') .css($materialEffects.ANIMATION_DURATION, options.animationDuration + 'ms') .css($materialEffects.ANIMATION_NAME, options.animationName) .css($materialEffects.ANIMATION_TIMING, options.animationTimingFunction) .on($materialEffects.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; } 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[$materialEffects.ANIMATION_DURATION] = options.fadeoutDuration + 'ms'; rippleEl.css(css); return rippleEl; } function onPointerDown(ev) { if (!rippleIsAllowed()) return; var rippleEl = createRippleFromEvent(ev); var ripplePauseTimeout = $timeout(pauseRipple, options.mousedownPauseTime, false); rippleEl.on('$destroy', cancelRipplePause); // Stop listening to pointer down for now, until the user lifts their finger/mouse listenPointerDown(false); element.on(POINTERUP_EVENT, onPointerUp); function onPointerUp() { cancelRipplePause(); rippleEl.css($materialEffects.ANIMATION_PLAY_STATE, 'running'); element.off(POINTERUP_EVENT, onPointerUp); listenPointerDown(true); } function pauseRipple() { rippleEl.css($materialEffects.ANIMATION_PLAY_STATE, 'paused'); } function cancelRipplePause() { $timeout.cancel(ripplePauseTimeout); } function createRippleFromEvent(ev) { ev = ev.touches ? ev.touches[0] : ev; return createRipple(ev.pageX, ev.pageY, true); } } } } /** * @ngdoc module * @name material.components.sticky * @description * * Sticky effects for material */ angular.module('material.components.sticky', [ 'material.components.content', 'material.decorators', 'material.animations' ]) .factory('$materialSticky', [ '$document', '$materialEffects', '$compile', '$$rAF', MaterialSticky ]); /** * @ngdoc factory * @name $materialSticky * @module material.components.sticky * * @description * The `$materialSticky`service provides a mixin to make elements sticky. * * @returns A `$materialSticky` function that takes three arguments: * - `scope` * - `element`: The element that will be 'sticky' * - `{optional}` `clone`: A clone of the element, that will be shown * when the user starts scrolling past the original element. * If not provided, it will use the result of `element.clone()`. */ function MaterialSticky($document, $materialEffects, $compile, $$rAF) { var browserStickySupport = checkStickySupport(); /** * Registers an element as sticky, used internally by directives to register themselves */ return function registerStickyElement(scope, element, stickyClone) { var contentCtrl = element.controller('materialContent'); if (!contentCtrl) return; if (browserStickySupport) { element.css({ position: browserStickySupport, top: 0, 'z-index': 2 }); } else { var $$sticky = contentCtrl.$element.data('$$sticky'); if (!$$sticky) { $$sticky = setupSticky(contentCtrl); contentCtrl.$element.data('$$sticky', $$sticky); } var deregister = $$sticky.add(element, stickyClone || element.clone()); scope.$on('$destroy', deregister); } }; function setupSticky(contentCtrl) { var contentEl = contentCtrl.$element; // Refresh elements is very expensive, so we use the debounced // version when possible. var debouncedRefreshElements = $$rAF.debounce(refreshElements); // setupAugmentedScrollEvents gives us `$scrollstart` and `$scroll`, // more reliable than `scroll` on android. setupAugmentedScrollEvents(contentEl); contentEl.on('$scrollstart', debouncedRefreshElements); contentEl.on('$scroll', onScroll); var self; return self = { prev: null, current: null, //the currently stickied item next: null, items: [], add: add, refreshElements: refreshElements }; /*************** * Public ***************/ // Add an element and its sticky clone to this content's sticky collection function add(element, stickyClone) { stickyClone.addClass('material-sticky-clone'); var item = { element: element, clone: stickyClone }; self.items.push(item); contentEl.parent().prepend(item.clone); debouncedRefreshElements(); return function remove() { self.items.forEach(function(item, index) { if (item.element[0] === element[0]) { self.items.splice(index, 1); item.clone.remove(); } }); debouncedRefreshElements(); }; } function refreshElements() { var contentRect = contentEl[0].getBoundingClientRect(); // Sort our collection of elements by their current position in the DOM. // We need to do this because our elements' order of being added may not // be the same as their order of display. self.items.forEach(refreshPosition); self.items = self.items.sort(function(a, b) { return a.top > b.top; }); // Find which item in the list should be active, // based upon the content's current scroll position var item; var currentScrollTop = contentEl.prop('scrollTop'); for (var i = self.items.length - 1; i >= 0; i--) { if (currentScrollTop > self.items[i].top) { item = self.items[i]; break; } } setCurrentItem(item); } /*************** * Private ***************/ // Find the `top` of an item relative to the content element, // and also the height. function refreshPosition(item) { // Find the top of an item by adding to the offsetHeight until we reach the // content element. var current = item.element[0]; item.top = 0; item.left = 0; while (current && current !== contentEl[0]) { item.top += current.offsetTop; item.left += current.offsetLeft; current = current.offsetParent; } item.height = item.element.prop('offsetHeight'); item.clone.css('margin-left', item.left + 'px'); } // As we scroll, push in and select the correct sticky element. function onScroll() { var scrollTop = contentEl.prop('scrollTop'); var isScrollingDown = scrollTop > (onScroll.prevScrollTop || 0); onScroll.prevScrollTop = scrollTop; // At the top? if (scrollTop === 0) { setCurrentItem(null); // Going to next item? } else if (isScrollingDown && self.next) { if (self.next.top - scrollTop <= 0) { // Sticky the next item if we've scrolled past its position. setCurrentItem(self.next); } else if (self.current) { // Push the current item up when we're almost at the next item. if (self.next.top - scrollTop <= self.next.height) { translate(self.current, self.next.top - self.next.height - scrollTop); } else { translate(self.current, null); } } // Scrolling up with a current sticky item? } else if (!isScrollingDown && self.current) { if (scrollTop < self.current.top) { // Sticky the previous item if we've scrolled up past // the original position of the currently stickied item. setCurrentItem(self.prev); } // Scrolling up, and just bumping into the item above (just set to current)? // If we have a next item bumping into the current item, translate // the current item up from the top as it scrolls into view. if (self.current && self.next) { if (scrollTop >= self.next.top - self.current.height) { translate(self.current, self.next.top - scrollTop - self.current.height); } else { translate(self.current, null); } } } } function setCurrentItem(item) { if (self.current === item) return; // Deactivate currently active item if (self.current) { translate(self.current, null); setStickyState(self.current, null); } // Activate new item if given if (item) { setStickyState(item, 'active'); } self.current = item; var index = self.items.indexOf(item); // If index === -1, index + 1 = 0. It works out. self.next = self.items[index + 1]; self.prev = self.items[index - 1]; setStickyState(self.next, 'next'); setStickyState(self.prev, 'prev'); } function setStickyState(item, state) { if (!item || item.state === state) return; if (item.state) { item.clone.attr('sticky-prev-state', item.state); item.element.attr('sticky-prev-state', item.state); } item.clone.attr('sticky-state', state); item.element.attr('sticky-state', state); item.state = state; } function translate(item, amount) { if (!item) return; if (amount === null || amount === undefined) { if (item.translateY) { item.translateY = null; item.clone.css($materialEffects.TRANSFORM, ''); } } else { item.translateY = amount; item.clone.css( $materialEffects.TRANSFORM, 'translate3d(' + item.left + 'px,' + amount + 'px,0)' ); } } } // Function to check for browser sticky support function checkStickySupport($el) { var stickyProp; var testEl = angular.element('
'); $document[0].body.appendChild(testEl[0]); var stickyProps = ['sticky', '-webkit-sticky']; for (var i = 0; i < stickyProps.length; ++i) { testEl.css({position: stickyProps[i], top: 0, 'z-index': 2}); if (testEl.css('position') == stickyProps[i]) { stickyProp = stickyProps[i]; break; } } testEl.remove(); return stickyProp; } // Android 4.4 don't accurately give scroll events. // To fix this problem, we setup a fake scroll event. We say: // > If a scroll or touchmove event has happened in the last DELAY milliseconds, // then send a `$scroll` event every animationFrame. // Additionally, we add $scrollstart and $scrollend events. function setupAugmentedScrollEvents(element) { var SCROLL_END_DELAY = 200; var isScrolling; var lastScrollTime; element.on('scroll touchmove', function() { if (!isScrolling) { isScrolling = true; $$rAF(loopScrollEvent); element.triggerHandler('$scrollstart'); } element.triggerHandler('$scroll'); lastScrollTime = +Util.now(); }); function loopScrollEvent() { if (+Util.now() - lastScrollTime > SCROLL_END_DELAY) { isScrolling = false; element.triggerHandler('$scrollend'); } else { element.triggerHandler('$scroll'); $$rAF(loopScrollEvent); } } } } /** * @ngdoc module * @name material.components.bottomSheet * @description * BottomSheet */ angular.module('material.components.bottomSheet', [ 'material.services.interimElement' ]) .directive('materialBottomSheet', [ MaterialBottomSheetDirective ]) .factory('$materialBottomSheet', [ '$$interimElement', '$animate', '$materialEffects', '$timeout', '$$rAF', MaterialBottomSheet ]); function MaterialBottomSheetDirective() { return { restrict: 'E' }; } /** * @ngdoc service * @name $materialBottomSheet * @module material.components.bottomSheet * * @description * Used to open a bottom sheet on the screen, `$materialBottomSheet` is a service * created by `$$interimElement` and provides a simple promise-based, behavior API: * * - `$materialBottomSheet.show()` * - `$materialBottomSheet.hide()` * - `$materialBottomSheet.cancel()` * * #### Notes: * * Only one bottom sheet may ever be active at any time. If a new sheet is * shown while a different one is active, the previous sheet will be automatically * hidden. * The bottom sheet's template must have an outer `` element. * * @usage * *
* * Open a Bottom Sheet! * *
*
* * var app = angular.module('app', ['ngMaterial']); * app.controller('MyController', function($scope, $materialBottomSheet) { * $scope.openBottomSheet = function() { * $materialBottomSheet.show({ * template: 'Hello!' * }); * }; * }); * */ /** * @ngdoc method * @name $materialBottomSheet#show * * @description * Show a bottom sheet with the specified options. * * @paramType Options * @param {string=} templateUrl 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 `material-bottom-sheet` element. * @param {string=} template Same as templateUrl, except this is an actual * template string. * @param {string=} controller The controller to associate with this bottom sheet. * @param {string=} locals 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. * @param {DOMClickEvent=} targetEvent 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. * @param {object=} resolve Similar to locals, except it takes promises as values * and the bottom sheet will not open until the promises resolve. * @param {string=} controllerAs An alias to assign the controller to on the scope. * * @returns {Promise} Returns a promise that will be resolved or rejected when * `$materialBottomSheet.hide()` or `$materialBottomSheet.cancel()` is called respectively. */ /** * @ngdoc method * @name $materialBottomSheet#hide * * @description * Hide the existing bottom sheet and `resolve` the promise returned from * `$materialBottomSheet.show()`. * * @param {*} arg An argument to resolve the promise with. * */ /** * @ngdoc method * @name $materialBottomSheet#cancel * * @description * Hide the existing bottom sheet and `reject` the promise returned from * `$materialBottomSheet.show()`. * * @param {*} arg An argument to reject the promise with. * */ function MaterialBottomSheet($$interimElement, $animate, $materialEffects, $timeout, $$rAF) { var backdrop; var $materialBottomSheet = $$interimElement({ targetEvent: null, onShow: onShow, onRemove: onRemove, }); return $materialBottomSheet; function onShow(scope, element, options) { // Add a backdrop that will close on click backdrop = angular.element(''); backdrop.on('click touchstart', function() { $timeout($materialBottomSheet.cancel); }); $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(); 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 $materialCompiler 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($materialEffects.TRANSITION_DURATION); element.css($materialEffects.TRANSITION_DURATION, '0s'); } function onTouchEnd(e) { // Re-enable the transitions on transforms element.css($materialEffects.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($materialBottomSheet.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($materialEffects.TRANSFORM, ''); } else { element.css($materialEffects.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; } } } /** * @ngdoc module * @name material.components.buttons * @description * * Button */ angular.module('material.components.button', [ 'material.animations', 'material.services.aria' ]) .directive('materialButton', [ 'ngHrefDirective', '$materialInkRipple', '$materialAria', MaterialButtonDirective ]); /** * @ngdoc directive * @name materialButton * @order 0 * * @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 MaterialButtonDirective(ngHrefDirectives, $materialInkRipple, $materialAria ) { 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('