diff --git a/docs/app/js/app.js b/docs/app/js/app.js index 6d027fbda35..b633f095187 100644 --- a/docs/app/js/app.js +++ b/docs/app/js/app.js @@ -144,8 +144,9 @@ function(SERVICES, COMPONENTS, DEMOS, PAGES, }]) -.config(['AngularyticsProvider', function(AngularyticsProvider) { - AngularyticsProvider.setEventHandlers(['GoogleUniversal']); +.config(['$mdGestureProvider', 'AngularyticsProvider', function($mdGestureProvider, AngularyticsProvider) { + $mdGestureProvider.skipClickHijack(); + AngularyticsProvider.setEventHandlers(['GoogleUniversal']); }]) .run(['$rootScope', '$window', 'Angularytics', function($rootScope, $window, Angularytics) { diff --git a/src/components/autocomplete/js/autocompleteController.js b/src/components/autocomplete/js/autocompleteController.js index 8dbfdb7e7ea..fa87d91f78f 100644 --- a/src/components/autocomplete/js/autocompleteController.js +++ b/src/components/autocomplete/js/autocompleteController.js @@ -63,7 +63,7 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, ctrl.select = select; ctrl.listEnter = onListEnter; ctrl.listLeave = onListLeave; - ctrl.mouseUp = onMouseup; + ctrl.focusInput = focusInputElement; ctrl.getCurrentDisplayValue = getCurrentDisplayValue; ctrl.registerSelectedItemWatcher = registerSelectedItemWatcher; ctrl.unregisterSelectedItemWatcher = unregisterSelectedItemWatcher; @@ -103,6 +103,10 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, gatherElements(); moveDropdown(); + // Touch devices often do not send a click event on tap. We still want to focus the input + // and open the options pop-up in these cases. + $element.on('touchstart', focusInputElement); + // Forward all focus events to the input element when autofocus is enabled if ($scope.autofocus) { $element.on('focus', focusInputElement); @@ -366,12 +370,31 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, // event/change handlers + /** + * @param {Event} $event + */ + function preventDefault($event) { + $event.preventDefault(); + } + + /** + * @param {Event} $event + */ + function stopPropagation($event) { + $event.stopPropagation(); + } + /** * Handles changes to the `hidden` property. - * @param {boolean} hidden - * @param {boolean} oldHidden + * @param {boolean} hidden true to hide the options pop-up, false to show it. + * @param {boolean} oldHidden the previous value of hidden */ function handleHiddenChange (hidden, oldHidden) { + var scrollContainerElement; + + if (elements) { + scrollContainerElement = angular.element(elements.scrollContainer); + } if (!hidden && oldHidden) { positionDropdown(); @@ -380,13 +403,23 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, reportMessages(true, ReportType.Count | ReportType.Selected); if (elements) { - $mdUtil.disableScrollAround(elements.ul); - enableWrapScroll = disableElementScrollEvents(angular.element(elements.wrap)); - ctrl.documentElement.on('click', handleClickOutside); + $mdUtil.disableScrollAround(elements.scrollContainer); + enableWrapScroll = disableElementScrollEvents(elements.wrap); + if ($mdUtil.isIos) { + ctrl.documentElement.on('touchend', handleTouchOutsidePanel); + if (scrollContainerElement) { + scrollContainerElement.on('touchstart touchmove touchend', stopPropagation); + } + } $mdUtil.nextTick(updateActiveOption); } } else if (hidden && !oldHidden) { - ctrl.documentElement.off('click', handleClickOutside); + if ($mdUtil.isIos) { + ctrl.documentElement.off('touchend', handleTouchOutsidePanel); + if (scrollContainerElement) { + scrollContainerElement.off('touchstart touchmove touchend', stopPropagation); + } + } $mdUtil.enableScrolling(); if (enableWrapScroll) { @@ -397,29 +430,27 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, } /** - * Handling click events that bubble up to the document is required for closing the dropdown - * panel on click outside of the panel on iOS. + * Handling touch events that bubble up to the document is required for closing the dropdown + * panel on touch outside of the options pop-up panel on iOS. * @param {Event} $event */ - function handleClickOutside($event) { + function handleTouchOutsidePanel($event) { ctrl.hidden = true; + // iOS does not blur the pop-up for touches on the scroll mask, so we have to do it. + doBlur(true); } /** - * Disables scrolling for a specific element + * Disables scrolling for a specific element. + * @param {!string|!DOMElement} element to disable scrolling + * @return {Function} function to call to re-enable scrolling for the element */ function disableElementScrollEvents(element) { - - function preventDefault(e) { - e.preventDefault(); - } - - element.on('wheel', preventDefault); - element.on('touchmove', preventDefault); + var elementToDisable = angular.element(element); + elementToDisable.on('wheel touchmove', preventDefault); return function() { - element.off('wheel', preventDefault); - element.off('touchmove', preventDefault); + elementToDisable.off('wheel touchmove', preventDefault); }; } @@ -439,13 +470,6 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, ctrl.hidden = shouldHide(); } - /** - * When the mouse button is released, send focus back to the input field. - */ - function onMouseup () { - elements.input.focus(); - } - /** * Handles changes to the selected item. * @param selectedItem @@ -837,14 +861,14 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, * Defines a public property with a handler and a default value. * @param {string} key * @param {Function} handler function - * @param {*} value default value + * @param {*} defaultValue default value */ - function defineProperty (key, handler, value) { + function defineProperty (key, handler, defaultValue) { Object.defineProperty(ctrl, key, { - get: function () { return value; }, + get: function () { return defaultValue; }, set: function (newValue) { - var oldValue = value; - value = newValue; + var oldValue = defaultValue; + defaultValue = newValue; handler(newValue, oldValue); } }); diff --git a/src/components/autocomplete/js/autocompleteDirective.js b/src/components/autocomplete/js/autocompleteDirective.js index 2d94f9be83a..1cc663bfbad 100644 --- a/src/components/autocomplete/js/autocompleteDirective.js +++ b/src/components/autocomplete/js/autocompleteDirective.js @@ -365,7 +365,7 @@ function MdAutocomplete ($$mdSvgRegistry) { // Stop click events from bubbling up to the document and triggering a flicker of the // options panel while still supporting ng-click to be placed on md-autocomplete. - element.on('click', function(event) { + element.on('click touchstart touchend', function(event) { event.stopPropagation(); }); }; @@ -402,7 +402,7 @@ function MdAutocomplete ($$mdSvgRegistry) { id="ul-{{$mdAutocompleteCtrl.id}}"\ ng-mouseenter="$mdAutocompleteCtrl.listEnter()"\ ng-mouseleave="$mdAutocompleteCtrl.listLeave()"\ - ng-mouseup="$mdAutocompleteCtrl.mouseUp()"\ + ng-mouseup="$mdAutocompleteCtrl.focusInput()"\ role="listbox">\
  • * app.config(function($mdGestureProvider) { @@ -105,8 +103,8 @@ MdGestureProvider.prototype = { * $get is used to build an instance of $mdGesture * @ngInject */ - $get : function($$MdGestureHandler, $$rAF, $timeout) { - return new MdGesture($$MdGestureHandler, $$rAF, $timeout); + $get : function($$MdGestureHandler, $$rAF, $timeout, $mdUtil) { + return new MdGesture($$MdGestureHandler, $$rAF, $timeout, $mdUtil); } }; @@ -116,17 +114,17 @@ MdGestureProvider.prototype = { * MdGesture factory construction function * @ngInject */ -function MdGesture($$MdGestureHandler, $$rAF, $timeout) { +function MdGesture($$MdGestureHandler, $$rAF, $timeout, $mdUtil) { var touchActionProperty = getTouchAction(); - var hasJQuery = (typeof window.jQuery !== 'undefined') && (angular.element === window.jQuery); + var hasJQuery = (typeof window.jQuery !== 'undefined') && (angular.element === window.jQuery); var self = { handler: addHandler, register: register, - isAndroid: isAndroid, - isIos: isIos, + isAndroid: $mdUtil.isAndroid, + isIos: $mdUtil.isIos, // On mobile w/out jQuery, we normally intercept clicks. Should we skip that? - isHijackingClicks: (isIos || isAndroid) && !hasJQuery && !forceSkipClickHijack + isHijackingClicks: ($mdUtil.isIos || $mdUtil.isAndroid) && !hasJQuery && !forceSkipClickHijack }; if (self.isHijackingClicks) { @@ -575,7 +573,7 @@ function MdGestureHandler() { * Attach Gestures: hook document and check shouldHijack clicks * @ngInject */ -function attachToDocument($mdGesture, $$MdGestureHandler) { +function attachToDocument($mdGesture, $$MdGestureHandler, $mdUtil) { if (disableAllGestures) { return; } @@ -623,7 +621,7 @@ function attachToDocument($mdGesture, $$MdGestureHandler) { */ function clickHijacker(ev) { var isKeyClick; - if (isIos) { + if ($mdUtil.isIos) { isKeyClick = angular.isDefined(ev.webkitForce) && ev.webkitForce === 0; } else { isKeyClick = ev.clientX === 0 && ev.clientY === 0; diff --git a/src/core/util/util.js b/src/core/util/util.js index 2576ec9cc62..95e127797b9 100644 --- a/src/core/util/util.js +++ b/src/core/util/util.js @@ -4,7 +4,14 @@ * will create its own instance of this array and the app's IDs * will not be unique. */ -var nextUniqueId = 0; +var nextUniqueId = 0, isIos, isAndroid; + +// Support material-tools builds. +if (window.navigator) { + var userAgent = window.navigator.userAgent || window.navigator.vendor || window.opera; + isIos = userAgent.match(/ipad|iphone|ipod/i); + isAndroid = userAgent.match(/android/i); +} /** * @ngdoc module @@ -65,6 +72,8 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in var $mdUtil = { dom: {}, + isIos: isIos, + isAndroid: isAndroid, now: window.performance && window.performance.now ? angular.bind(window.performance, window.performance.now) : Date.now || function() { return new Date().getTime(); @@ -298,16 +307,17 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in wrappedElementToDisable.append(scrollMask); } - function preventDefault(e) { - e.preventDefault(); + /** + * @param {Event} $event + */ + function preventDefault($event) { + $event.preventDefault(); } - scrollMask.on('wheel', preventDefault); - scrollMask.on('touchmove', preventDefault); + scrollMask.on('wheel touchmove', preventDefault); return function restoreElementScroll() { - scrollMask.off('wheel'); - scrollMask.off('touchmove'); + scrollMask.off('wheel touchmove', preventDefault); if (!scrollMaskOptions.disableScrollMask && scrollMask[0].parentNode) { scrollMask[0].parentNode.removeChild(scrollMask[0]); @@ -379,7 +389,10 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in return this.floatingScrollbars.cached; }, - // Mobile safari only allows you to set focus in click event listeners... + /** + * Mobile safari only allows you to set focus in click event listeners. + * @param {Element|angular.JQLite} element to focus + */ forceFocus: function(element) { var node = element[0] || element; @@ -453,17 +466,21 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in }; }, - // 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 + /** + * @param {Function} func original function to be debounced + * @param {number} wait number of milliseconds to delay (since last debounce reset). + * Default value 10 msecs. + * @param {Object} scope in which to apply the function after debouncing ends + * @param {boolean} invokeApply should the $timeout trigger $digest() dirty checking + * @return {Function} 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. + */ debounce: function(func, wait, scope, invokeApply) { var timer; return function debounced() { var context = scope, - args = Array.prototype.slice.call(arguments); + args = Array.prototype.slice.call(arguments); $timeout.cancel(timer); timer = $timeout(function() { @@ -475,9 +492,13 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in }; }, - // 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. + /** + * The function will not be called unless it has been more than `delay` milliseconds since the + * last call. + * @param {Function} func original function to throttle + * @param {number} delay number of milliseconds to delay + * @return {Function} a function that can only be triggered every `delay` milliseconds. + */ throttle: function throttle(func, delay) { var recent; return function throttled() { @@ -527,8 +548,11 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in 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. + /** + * Stop watchers and events from firing on a scope without destroying it, + * by disconnecting it from its parent and its siblings' linked lists. + * @param {Object} scope to disconnect + */ disconnectScope: function disconnectScope(scope) { if (!scope) return; @@ -549,7 +573,10 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in }, - // Undo the effects of disconnectScope above. + /** + * Undo the effects of disconnectScope(). + * @param {Object} scope to reconnect + */ reconnectScope: function reconnectScope(scope) { if (!scope) return; @@ -829,9 +856,7 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in /** * Returns true if the parent form of the element has been submitted. - * * @param element An AngularJS or HTML5 element. - * * @returns {boolean} */ isParentFormSubmitted: function(element) { @@ -843,7 +868,6 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in /** * Animate the requested element's scrollTop to the requested scrollPosition with basic easing. - * * @param {!Element} element The element to scroll. * @param {number} scrollEnd The new/final scroll position. * @param {number=} duration Duration of the scroll. Default is 1000ms. @@ -895,7 +919,6 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in * $mdUtil.uniq(myArray) => [1, 2, 3, 4] * * @param {array} array The array whose unique values should be returned. - * * @returns {array} A copy of the array containing only unique values. */ uniq: function(array) { @@ -956,7 +979,7 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in } } -/* +/** * 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`. */