diff --git a/js/angular/directive/navBackButton.js b/js/angular/directive/navBackButton.js index 267ebf5e43..74385c4078 100644 --- a/js/angular/directive/navBackButton.js +++ b/js/angular/directive/navBackButton.js @@ -62,9 +62,8 @@ */ IonicModule .directive('ionNavBackButton', [ - '$ionicNgClick', '$animate', -function($ionicNgClick, $animate) { +function($animate) { return { restrict: 'E', require: '^ionNavBar', @@ -73,7 +72,11 @@ function($ionicNgClick, $animate) { return function($scope, $element, $attr, navBarCtrl) { if (!$attr.ngClick) { $scope.$navBack = navBarCtrl.back; - $ionicNgClick($scope, $element, '$navBack($event)'); + $element.on('click', function(event){ + $scope.$apply(function() { + $scope.$navBack(event); + }); + }); } //If the current viewstate does not allow a back button, diff --git a/js/angular/directive/ngClick.js b/js/angular/directive/ngClick.js index d1f63913b5..756134ac85 100644 --- a/js/angular/directive/ngClick.js +++ b/js/angular/directive/ngClick.js @@ -1,6 +1,3 @@ -// Similar to Angular's ngTouch, however it uses Ionic's tap detection -// and click simulation. ngClick - IonicModule .config(['$provide', function($provide) { @@ -15,10 +12,6 @@ IonicModule * @private */ .factory('$ionicNgClick', ['$parse', function($parse) { - function onRelease(e) { - // wire this up to Ionic's tap/click simulation - ionic.tap.simulateClick(e.target, e); - } return function(scope, element, clickExpr) { var clickHandler = $parse(clickExpr); @@ -28,15 +21,9 @@ IonicModule }); }); - ionic.on("release", onRelease, element[0]); - // Hack for iOS Safari's benefit. It goes searching for onclick handlers and is liable to click // something else nearby. element.onclick = function(event) { }; - - scope.$on('$destroy', function () { - ionic.off("release", onRelease, element[0]); - }); }; }]) diff --git a/js/angular/directive/tabNav.js b/js/angular/directive/tabNav.js index c80792e583..e49880a203 100644 --- a/js/angular/directive/tabNav.js +++ b/js/angular/directive/tabNav.js @@ -1,5 +1,5 @@ IonicModule -.directive('ionTabNav', ['$ionicNgClick', function($ionicNgClick) { +.directive('ionTabNav', [function() { return { restrict: 'E', replace: true, @@ -33,7 +33,11 @@ IonicModule tabsCtrl.select(tabCtrl.$scope, true); }; if (!$attrs.ngClick) { - $ionicNgClick($scope, $element, 'selectTab($event)'); + $element.on('click', function(event) { + $scope.$apply(function() { + $scope.selectTab(event); + }); + }); } $scope.getIconOn = function() { diff --git a/js/angular/directive/touch.js b/js/angular/directive/touch.js deleted file mode 100644 index bff629e603..0000000000 --- a/js/angular/directive/touch.js +++ /dev/null @@ -1,65 +0,0 @@ - -// Similar to Angular's ngTouch, however it uses Ionic's tap detection -// and click simulation. ngClick - -(function(angular, ionic) {'use strict'; - -angular.module('ionic.ui.touch', []) - - .config(['$provide', function($provide) { - $provide.decorator('ngClickDirective', ['$delegate', function($delegate) { - // drop the default ngClick directive - $delegate.shift(); - return $delegate; - }]); - }]) - - /** - * @private - */ - .factory('$ionicNgClick', ['$parse', function($parse) { - function onRelease(e) { - // wire this up to Ionic's tap/click simulation - ionic.tap.simulateClick(e.target, e); - } - return function(scope, element, clickExpr) { - var clickHandler = $parse(clickExpr); - - element.on('click', function(event) { - scope.$apply(function() { - clickHandler(scope, {$event: (event)}); - }); - }); - - ionic.on("release", onRelease, element[0]); - - // Hack for iOS Safari's benefit. It goes searching for onclick handlers and is liable to click - // something else nearby. - element.onclick = function(event) { }; - - scope.$on('$destroy', function () { - ionic.off("release", onRelease, element[0]); - }); - }; - }]) - - .directive('ngClick', ['$ionicNgClick', function($ionicNgClick) { - return function(scope, element, attr) { - $ionicNgClick(scope, element, attr.ngClick); - }; - }]) - - .directive('ionStopEvent', function () { - function stopEvent(e) { - e.stopPropagation(); - } - return { - restrict: 'A', - link: function (scope, element, attr) { - element.bind(attr.ionStopEvent, stopEvent); - } - }; - }); - - -})(window.angular, window.ionic); diff --git a/js/utils/activator.js b/js/utils/activator.js index 10e46d9299..c478c5b1c9 100644 --- a/js/utils/activator.js +++ b/js/utils/activator.js @@ -5,13 +5,10 @@ var activeElements = {}; // elements that are currently active var keyId = 0; // a counter for unique keys for the above ojects var ACTIVATED_CLASS = 'activated'; - var touchMoveClearTimer; ionic.activator = { start: function(e) { - clearTimeout(touchMoveClearTimer); - // when an element is touched/clicked, it climbs up a few // parents to see if it is an .item or .button element ionic.requestAnimationFrame(function(){ @@ -19,12 +16,12 @@ var eleToActivate; for(var x=0; x<4; x++) { - if(!ele) break; + if(!ele || ele.nodeType !== 1) break; if(eleToActivate && ele.classList.contains('item')) { eleToActivate = ele; break; } - if( ele.tagName == 'A' || ele.tagName == 'BUTTON' || ele.getAttribute('ng-click') ) { + if( ele.tagName == 'A' || ele.tagName == 'BUTTON' || ele.hasAttribute('ng-click') ) { eleToActivate = ele; } if( ele.classList.contains('button') ) { @@ -39,15 +36,9 @@ queueElements[keyId] = eleToActivate; // in XX milliseconds, set the queued elements to active - // add listeners to clear all queued/active elements onMove if(e.type === 'touchstart') { - document.body.removeEventListener('mousedown', ionic.activator.start); - touchMoveClearTimer = setTimeout(function(){ - document.body.addEventListener('touchmove', onTouchMove, false); - }, 80); setTimeout(activateElements, 80); } else { - document.body.addEventListener('mousemove', clear, false); ionic.requestAnimationFrame(activateElements); } @@ -55,9 +46,23 @@ } }); + }, + + end: function() { + // clear out any active/queued elements after XX milliseconds + setTimeout(clear, 200); } + }; + function clear() { + // clear out any elements that are queued to be set to active + queueElements = {}; + + // in the next frame, remove the active class from all active elements + ionic.requestAnimationFrame(deactivateElements); + } + function activateElements() { // activate all elements in the queue for(var key in queueElements) { @@ -78,41 +83,4 @@ } } - function onTouchMove(e) { - if( ionic.tap.hasTouchScrolled(e) ) { - clear(); - } - } - - function onEnd(e) { - // clear out any active/queued elements after XX milliseconds - setTimeout(clear, 200); - } - - function clear() { - clearTimeout(touchMoveClearTimer); - - // clear out any elements that are queued to be set to active - queueElements = {}; - - // in the next frame, remove the active class from all active elements - ionic.requestAnimationFrame(deactivateElements); - - // remove onMove listeners that clear out active elements - document.body.removeEventListener('mousemove', clear); - document.body.removeEventListener('touchmove', clear); - } - - // use window.onload because this doesn't need to run immediately - window.addEventListener('load', function(){ - // start an active element - document.body.addEventListener('touchstart', ionic.activator.start, false); - document.body.addEventListener('mousedown', ionic.activator.start, false); - - // clear all active elements after XX milliseconds - document.body.addEventListener('touchend', onEnd, false); - document.body.addEventListener('mouseup', onEnd, false); - document.body.addEventListener('touchcancel', onEnd, false); - }, false); - })(document, ionic); diff --git a/js/utils/keyboard.js b/js/utils/keyboard.js index 491e0f22bd..c4716a0873 100644 --- a/js/utils/keyboard.js +++ b/js/utils/keyboard.js @@ -1,51 +1,84 @@ (function(ionic) { ionic.Platform.ready(function() { - if (ionic.Platform.is('android')) { - androidKeyboardFix(); - } -}); - -function androidKeyboardFix() { var rememberedDeviceWidth = window.innerWidth; var rememberedDeviceHeight = window.innerHeight; var keyboardHeight; + var rememberedActiveEl; + var alreadyOpen = false; - window.addEventListener('resize', resize); + window.addEventListener('focusin', onBrowserFocusIn); - function resize() { + if(ionic.Platform.isWebView() && window.cordova && cordova.plugins && cordova.plugins.Keyboard) { + window.addEventListener('native.showkeyboard', onNativeKeyboardShow); + window.addEventListener('native.hidekeyboard', onNativeKeyboardHide); - //If the width of the window changes, we have an orientation change - if (rememberedDeviceWidth !== window.innerWidth) { - rememberedDeviceWidth = window.innerWidth; - rememberedDeviceHeight = window.innerHeight; - console.info('orientation change. deviceWidth =', rememberedDeviceWidth, ', deviceHeight =', rememberedDeviceHeight); + } else if (ionic.Platform.isAndroid()){ + window.addEventListener('resize', onBrowserResize); + } - //If the height changes, and it's less than before, we have a keyboard open - } else if (rememberedDeviceHeight !== window.innerHeight && - window.innerHeight < rememberedDeviceHeight) { - document.body.classList.add('footer-hide'); - //Wait for next frame so document.activeElement is set - ionic.requestAnimationFrame(handleKeyboardChange); - } else { - //Otherwise we have a keyboard close or a *really* weird resize - document.body.classList.remove('footer-hide'); - } - - function handleKeyboardChange() { - //keyboard opens - keyboardHeight = rememberedDeviceHeight - window.innerHeight; - var activeEl = document.activeElement; - if (activeEl) { - //This event is caught by the nearest parent scrollView - //of the activeElement - ionic.trigger('scrollChildIntoView', { - target: activeEl - }, true); + function onBrowserFocusIn(e) { + if (ionic.tap.containsOrIsTextInput(e.target) || e.srcElement.isContentEditable){ + document.body.scrollTop = 0; } + rememberedActiveEl = e.srcElement; + } + + function onBrowserResize() { + if(rememberedDeviceWidth !== window.innerWidth) { + // If the width of the window changes, we have an orientation change + rememberedDeviceWidth = window.innerWidth; + rememberedDeviceHeight = window.innerHeight; + + } else if(rememberedDeviceHeight !== window.innerHeight && + window.innerHeight < rememberedDeviceHeight) { + // If the height changes, and it's less than before, we have a keyboard open + document.body.classList.add('keyboard-open'); + + keyboardHeight = rememberedDeviceHeight - window.innerHeight; + setTimeout(function() { + ionic.trigger('scrollChildIntoView', { + target: rememberedActiveEl, + }, true); + }, 100); + + } else { + // Otherwise we have a keyboard close or a *really* weird resize + document.body.classList.remove('keyboard-open'); } } -} + + function onNativeKeyboardShow(e) { + if(rememberedActiveEl) { + // This event is caught by the nearest parent scrollView + // of the activeElement + if(cordova.plugins.Keyboard.isVisible) { + document.body.classList.add('keyboard-open'); + ionic.trigger('scrollChildIntoView', { + keyboardHeight: e.keyboardHeight, + target: rememberedActiveEl, + firstKeyboardShow: !alreadyOpen + }, true); + + if(!alreadyOpen) alreadyOpen = true; + } + } + } + + function onNativeKeyboardHide() { + // wait to see if we're just switching inputs + setTimeout(function() { + if(!cordova.plugins.Keyboard.isVisible) { + document.body.classList.remove('keyboard-open'); + alreadyOpen = false; + ionic.trigger('resetScrollView', { + target: rememberedActiveEl + }, true); + } + }, 100); + } + +}); })(window.ionic); diff --git a/js/utils/tap.js b/js/utils/tap.js index a8720aa6ca..a9204ce201 100644 --- a/js/utils/tap.js +++ b/js/utils/tap.js @@ -1,305 +1,445 @@ -(function(window, document, ionic) { - 'use strict'; - var CLICK_PREVENT_DURATION = 1500; // max milliseconds ghostclicks in the same area should be prevented - var REMOVE_PREVENT_DELAY = 380; // delay after a touchend/mouseup before removing the ghostclick prevent - var REMOVE_PREVENT_DELAY_GRADE_C = 800; // same as REMOVE_PREVENT_DELAY, but for grade c devices - var HIT_RADIUS = 15; // surrounding area of a click that if a ghostclick happens it would get ignored - var TOUCH_TOLERANCE_X = 10; // how much the X coordinates can be off between start/end, but still a click - var TOUCH_TOLERANCE_Y = 6; // how much the Y coordinates can be off between start/end, but still a click - var tapCoordinates = {}; // used to remember coordinates to ignore if they happen again quickly - var startCoordinates = {}; // used to remember where the coordinates of the start of a touch - var clickPreventTimerId; - var _hasTouchScrolled = false; // if the touchmove already exceeded the touchmove tolerance - var _activeElement; // the element which has focus +/* + IONIC TAP + --------------- + - Both touch and mouse events are added to the document.body on DOM ready + - If a touch event happens, it removes the mouse event listeners (temporarily) + - Remembers the last touchstart event + - On touchend, if the distance between start and end was small, trigger a click + - In the triggered click event, add a 'isIonicTap' property + - The triggered click receives the same x,y coordinates as as the end event + - On document.body click listener (with useCapture=true), only allow clicks with 'isIonicTap' + - After XXms, bring back the mouse event listeners incase they switch from touch and mouse + - If no touch events and only mouse, then touch events never fire, only mouse + - Triggering clicks with mouse events work the same as touch, except with mousedown/mouseup + - Tapping inputs is disabled during scrolling - ionic.tap = { + - Does not require other libraries to hook into ionic.tap, it just works + - Elements can come and go from the DOM and it doesn't have to keep adding and removing listeners + - No "tap delay" after the first "tap" (you can tap as fast as you want, they all click) + - Minimal events listeners, only being added to document.body + - Correct focus in/out on each input type on each platform/device + - Shows and hides virtual keyboard correctly for each platform/device + - No user-agent sniffing + - Works with labels surrounding inputs + - Does not fire off a click if the user moves the pointer too far + - Adds and removes an 'activated' css class + - Multiple unit tests for each scenario - tapInspect: function(orgEvent) { - // if the event doesn't have a gesture then don't continue - if(!orgEvent.gesture || !orgEvent.gesture.srcEvent) return; +*/ - var e = orgEvent.gesture.srcEvent; // evaluate the actual source event, not the created event by gestures.js - var ele = e.target; // get the target element that was actually tapped +var tapDoc; // the element which the listeners are on (document.body) +var tapActiveEle; // the element which is active (probably has focus) +var tapEnabledTouchEvents; +var tapMouseResetTimer; +var tapPointerMoved; +var tapPointerStart; +var tapTouchFocusedInput; - if( ionic.tap.ignoreTapInspect(e) ) { - // if a tap in the same area just happened, - // or it was a touchcanel event, don't continue - console.debug('tapInspect stopEvent', e.type, ele.tagName); - return stopEvent(e); +var TAP_RELEASE_TOLERANCE = 6; // how much the coordinates can be off between start/end, but still a click + +var tapEventListeners = { + 'click': tapClickGateKeeper, + + 'mousedown': tapMouseDown, + 'mouseup': tapMouseUp, + 'mousemove': tapMouseMove, + + 'touchstart': tapTouchStart, + 'touchend': tapTouchEnd, + 'touchcancel': tapTouchCancel, + 'touchmove': tapTouchMove, + + 'focusin': tapFocusIn, + 'focusout': tapFocusOut +}; + +ionic.tap = { + + register: function(ele) { + tapDoc = ele; + + tapEventListener('click', true, true); + tapEventListener('mouseup'); + tapEventListener('mousedown'); + tapEventListener('touchstart'); + tapEventListener('touchend'); + tapEventListener('touchcancel'); + tapEventListener('focusin'); + tapEventListener('focusout'); + + return function() { + for(var type in tapEventListeners) { + tapEventListener(type, false); } + tapDoc = null; + tapActiveEle = null; + tapEnabledTouchEvents = false; + tapPointerMoved = false; + tapPointerStart = null; + }; + }, - for(var x=0; x<5; x++) { - // climb up the DOM looking to see if the tapped element is, or has a parent, of one of these - // only climb up a max of 5 parents, anything more probably isn't beneficial - if(!ele) break; + ignoreScrollStart: function(e) { + return (e.defaultPrevented) || // defaultPrevented has been assigned by another component handling the event + (e.target.isContentEditable) || + (e.target.type === 'range') || + (e.target.dataset ? e.target.dataset.preventScroll : e.target.getAttribute('data-prevent-default')) == 'true' || // manually set within an elements attributes + (!!(/object|embed/i).test(e.target.tagName)); // flash/movie/object touches should not try to scroll + }, - if( ionic.tap.isTapElement(ele.tagName) ) { - return ionic.tap.simulateClick(ele, e); - } - ele = ele.parentElement; - } + isTextInput: function(ele) { + return !!ele && + (ele.tagName == 'TEXTAREA' || + (ele.tagName == 'INPUT' && !(/radio|checkbox|range|file|submit|reset/i).test(ele.type))); + }, - // they didn't tap one of the above elements - // if the currently active element is an input, and they tapped outside - // of the current input, then unset its focus (blur) so the keyboard goes away - ionic.tap.blurActive(); - }, + isLabelWithTextInput: function(ele) { + var container = tapContainingElement(ele, false); - ignoreTapInspect: function(e) { - return !!ionic.tap.isRecentTap(e) || - ionic.tap.hasTouchScrolled(e) || - e.type === 'touchcancel'; - }, + return !!container && + ionic.tap.isTextInput( tapTargetElement( container ) ); + }, - isTapElement: function(tagName) { - return tagName == 'A' || - tagName == 'INPUT' || - tagName == 'BUTTON' || - tagName == 'LABEL' || - tagName == 'TEXTAREA' || - tagName == 'SELECT'; - }, + containsOrIsTextInput: function(ele) { + return ionic.tap.isTextInput(ele) || ionic.tap.isLabelWithTextInput(ele); + }, - simulateClick: function(target, e) { - // simulate a normal click by running the element's click method then focus on it + cloneFocusedInput: function(container, instance) { + if(ionic.tap.hasCheckedClone) return; + ionic.tap.hasCheckedClone = true; - var ele = target.control || target; - - if( ionic.tap.ignoreSimulateClick(ele) ) return; - - console.debug('simulateClick', e.type, ele.tagName, ele.className); - - var c = ionic.tap.getCoordinates(e); - - // using initMouseEvent instead of MouseEvent for our Android friends - var clickEvent = document.createEvent("MouseEvents"); - clickEvent.initMouseEvent('click', true, true, window, - 1, 0, 0, c.x, c.y, - false, false, false, false, 0, null); - - ele.dispatchEvent(clickEvent); - - // if it's an input, focus in on the target, otherwise blur - ionic.tap.handleFocus(ele); - - // remember the coordinates of this tap so if it happens again we can ignore it - // but only if the coordinates are not already being actively disabled - if( !ionic.tap.isRecentTap(e) ) { - ionic.tap.recordCoordinates(e); - } - - if(target.control) { - console.debug('simulateClick, target.control, stop'); - return stopEvent(e); - } - - }, - - ignoreSimulateClick: function(ele) { - return ele.disabled || ele.type === 'range'; - }, - - handleFocus: function(ele) { - if(ionic.tap.activeElement() !== ele) { - // only set the focus if it doesn't already have it - if( ele.tagName.match(/input|textarea|select/i) ) { - ionic.tap.activeElement(ele); - ele.focus(); - } else { - ionic.tap.activeElement(null); - ionic.tap.blurActive(); + ionic.requestAnimationFrame(function(){ + var focusInput = container.querySelector(':focus'); + if( ionic.tap.isTextInput(focusInput) ) { + var clonedInput = focusInput.parentElement.querySelector('.cloned-text-input'); + if(!clonedInput) { + clonedInput = document.createElement(focusInput.tagName); + clonedInput.type = focusInput.type; + clonedInput.value = focusInput.value; + clonedInput.className = 'cloned-text-input'; + focusInput.parentElement.insertBefore(clonedInput, focusInput); + focusInput.style.top = focusInput.offsetTop; + focusInput.classList.add('previous-input-focus'); } } - }, + }); + }, - preventGhostClick: function(e) { + hasCheckedClone: false, - console.debug((function(){ - // Great for debugging, and thankfully this gets removed from the build, OMG it's ugly + removeClonedInputs: function(container) { + ionic.tap.hasCheckedClone = false; - if(e.target.control) { - // this is a label that has an associated input - // the native layer will send the actual event, so stop this one - console.debug('preventGhostClick', 'label'); + ionic.requestAnimationFrame(function(){ + var clonedInputs = container.querySelectorAll('.cloned-text-input'); + var previousInputFocus = container.querySelectorAll('.previous-input-focus'); + var x; - } else if(ionic.tap.isRecentTap(e)) { - // a tap has already happened at these coordinates recently, ignore this event - console.debug('preventGhostClick', 'isRecentTap', e.target.tagName); - - } else if(ionic.tap.hasTouchScrolled(e)) { - // this click's coordinates are different than its touchstart/mousedown, must have been scrolling - console.debug('preventGhostClick', 'hasTouchScrolled'); - } - - var c = ionic.tap.getCoordinates(e); - return 'click(' + c.x + ',' + c.y + '), start(' + startCoordinates.x + ',' + startCoordinates.y + ')'; - })()); - - - if(e.target.control || ionic.tap.isRecentTap(e) || ionic.tap.hasTouchScrolled(e)) { - return stopEvent(e); + for(x=0; x startCoordinates.x + TOUCH_TOLERANCE_X || - c.x < startCoordinates.x - TOUCH_TOLERANCE_X || - c.y > startCoordinates.y + TOUCH_TOLERANCE_Y || - c.y < startCoordinates.y - TOUCH_TOLERANCE_Y); - }, +// MOUSE +function tapMouseDown(e) { + if(e.isIonicTap || tapIgnoreEvent(e)) return; - recordCoordinates: function(event) { - // get the coordinates of this event and remember them for later - var c = ionic.tap.getCoordinates(event); - if(c.x && c.y) { - var tapId = Date.now(); + if(tapEnabledTouchEvents) { + console.debug('mousedown', 'stop event'); + e.stopPropagation(); - // only record tap coordinates if we have valid ones - tapCoordinates[tapId] = { x: c.x, y: c.y, id: tapId }; - - setTimeout(function() { - // delete the tap coordinates after X milliseconds, basically allowing - // it so a tap can happen again in the same area in the future - // this is only a fallback, most tap coordinates will be removed - // from the removeClickPrevent event fired by touchend/mouseup - delete tapCoordinates[tapId]; - }, CLICK_PREVENT_DURATION); - } - }, - - removeClickPrevent: function(e) { - // fired by touchend/mouseup - // after X milliseconds, remove tap coordinates - clearTimeout(clickPreventTimerId); - clickPreventTimerId = setTimeout(function(){ - var tap = ionic.tap.isRecentTap(e); - if(tap) delete tapCoordinates[tap.id]; - }, REMOVE_PREVENT_DELAY); - }, - - isRecentTap: function(event) { - // loop through the tap coordinates and see if the same area has been tapped recently - var tapId, existingCoordinates, currentCoordinates; - - for(tapId in tapCoordinates) { - existingCoordinates = tapCoordinates[tapId]; - if(!currentCoordinates) currentCoordinates = ionic.tap.getCoordinates(event); // lazy load it when needed - - if(currentCoordinates.x > existingCoordinates.x - HIT_RADIUS && - currentCoordinates.x < existingCoordinates.x + HIT_RADIUS && - currentCoordinates.y > existingCoordinates.y - HIT_RADIUS && - currentCoordinates.y < existingCoordinates.y + HIT_RADIUS) { - // the current tap coordinates are in the same area as a recent tap - return existingCoordinates; - } - } - }, - - blurActive: function() { - var ele = ionic.tap.activeElement(); - if(ele && ele.tagName.match(/input|textarea|select/i) ) { - setTimeout(function(){ - ele.blur(); - ionic.tap.activeElement(null); - }, 400); - } - }, - - activeElement: function(ele) { - if(arguments.length) { - _activeElement = ele; - } - return _activeElement || document.activeElement; - }, - - setTouchStart: function(e) { - _hasTouchScrolled = false; - startCoordinates = ionic.tap.getCoordinates(e); - document.body.addEventListener('touchmove', ionic.tap.onTouchMove, false); - }, - - onTouchMove: function(e) { - if( ionic.tap.hasTouchScrolled(e) ) { - _hasTouchScrolled = true; - document.body.removeEventListener('touchmove', ionic.tap.onTouchMove); - console.debug('hasTouchScrolled'); - } - }, - - reset: function() { - tapCoordinates = {}; - startCoordinates = {}; - }, - - ignoreScrollStart: function(e) { - return (e.defaultPrevented) || // defaultPrevented has been assigned by another component handling the event - (e.target.tagName.match(/input|textarea/i) && ionic.tap.activeElement() === e.target) || // target is the active element, so its a second tap to select input text - (e.target.isContentEditable) || - (e.target.dataset ? e.target.dataset.preventScroll : e.target.getAttribute('data-prevent-default')) == 'true' || // manually set within an elements attributes - (!!e.target.tagName.match(/object|embed/i)); // flash/movie/object touches should not try to scroll + if( !ionic.tap.isTextInput(e.target) ) { + // If you preventDefault on a text input then you cannot move its text caret/cursor. + // Allow through only the text input default. However, without preventDefault on an + // input the 300ms delay can change focus on inputs after the keyboard shows up. + // The focusin event handles the chance of focus changing after the keyboard shows. + e.preventDefault(); } - }; - - function stopEvent(e){ - e.stopPropagation(); - e.preventDefault(); return false; } - ionic.Platform.ready(function(){ - if(ionic.Platform.grade === 'c') { - // low performing devices should have a longer ghostclick prevent - REMOVE_PREVENT_DELAY = REMOVE_PREVENT_DELAY_GRADE_C; + tapPointerMoved = false; + tapPointerStart = getPointerCoordinates(e); + + tapEventListener('mousemove'); + ionic.activator.start(e); +} + +function tapMouseUp(e) { + if( tapIgnoreEvent(e) ) return; + + if( !tapHasPointerMoved(e) ) { + tapClick(e); + } + tapEventListener('mousemove', false); + ionic.activator.end(); + tapPointerMoved = false; +} + +function tapMouseMove(e) { + if( tapHasPointerMoved(e) ) { + tapEventListener('mousemove', false); + ionic.activator.end(); + tapPointerMoved = true; + return false; + } +} + + +// TOUCH +function tapTouchStart(e) { + if( tapIgnoreEvent(e) ) return; + + tapPointerMoved = false; + + tapEnableTouchEvents(); + tapPointerStart = getPointerCoordinates(e); + + tapEventListener('touchmove'); + ionic.activator.start(e); +} + +function tapTouchEnd(e) { + if( tapIgnoreEvent(e) ) return; + + tapEnableTouchEvents(); + if( !tapHasPointerMoved(e) ) { + tapClick(e); + } + + tapTouchCancel(); +} + +function tapTouchMove(e) { + if( tapHasPointerMoved(e) ) { + tapPointerMoved = true; + tapEventListener('touchmove', false); + ionic.activator.end(); + return false; + } +} + +function tapTouchCancel(e) { + tapEventListener('touchmove', false); + ionic.activator.end(); + tapPointerMoved = false; +} + +function tapEnableTouchEvents() { + if(!tapEnabledTouchEvents) { + tapEventListener('mouseup', false); + tapEnabledTouchEvents = true; + } + clearTimeout(tapMouseResetTimer); + tapMouseResetTimer = setTimeout(tapResetMouseEvent, 2500); +} + +function tapResetMouseEvent() { + tapEventListener('mouseup', false); + tapEnabledTouchEvents = false; +} + +function tapIgnoreEvent(e) { + if(e.isTapHandled) return true; + e.isTapHandled = true; + + if( ionic.scroll.isScrolling && ionic.tap.containsOrIsTextInput(e.target) ) { + e.preventDefault(); + return true; + } +} + +function tapHandleFocus(ele) { + tapTouchFocusedInput = null; + + if(ele.tagName == 'SELECT') { + // trick to force Android options to show up + console.debug('tapHandleFocus', ele.tagName); + triggerMouseEvent('mousedown', ele, 0, 0); + tapActiveElement(ele); + ele.focus && ele.focus(); + + } else if(tapActiveElement() !== ele) { + if( (/input|textarea/i).test(ele.tagName) ) { + console.debug('tapHandleFocus', ele.tagName, ele.id); + tapActiveElement(ele); + ele.focus && ele.focus(); + ele.value = ele.value; + if( tapEnabledTouchEvents ) { + tapTouchFocusedInput = ele; + } + } else { + tapFocusOutActive(); } - }); + } +} - // set click handler and check if the event should be stopped or not - document.addEventListener('click', ionic.tap.preventGhostClick, true); +function tapFocusOutActive() { + var ele = tapActiveElement(); + if(ele && (/input|textarea|select/i).test(ele.tagName) ) { + console.debug('tapFocusOutActive', ele.tagName); + ele.blur(); + } + tapActiveElement(null); +} - // set release event listener for HTML elements that were tapped or held - ionic.on("release", ionic.tap.tapInspect, document); +function tapFocusIn(e) { + // Because a text input doesn't preventDefault (so the caret still works) there's a chance + // that it's mousedown event 300ms later will change the focus to another element after + // the keyboard shows up. - // listeners used to clear out active taps which are used to prevention ghostclicks - document.addEventListener('touchend', ionic.tap.removeClickPrevent, false); - document.addEventListener('mouseup', ionic.tap.removeClickPrevent, false); + if( tapEnabledTouchEvents && + ionic.tap.isTextInput( tapActiveElement() ) && + ionic.tap.isTextInput(tapTouchFocusedInput) && + tapTouchFocusedInput !== e.target ) { - // remember where the user first started touching the screen - // so that if they scrolled, it shouldn't fire the click - document.addEventListener('touchstart', ionic.tap.setTouchStart, false); + // 1) The pointer is from touch events + // 2) There is an active element which is a text input + // 3) A text input was just set to be focused on by a touch event + // 4) A new focus has been set, however the target isn't the one the touch event wanted + console.debug('focusin', 'tapTouchFocusedInput'); + tapTouchFocusedInput.focus(); + tapTouchFocusedInput = null; + } + ionic.scroll.isScrolling = false; +} -})(this, document, ionic); +function tapFocusOut() { + tapActiveElement(null); +} + +function tapActiveElement(ele) { + if(arguments.length) { + tapActiveEle = ele; + } + return tapActiveEle || document.activeElement; +} + +function tapHasPointerMoved(endEvent) { + if(!endEvent || !tapPointerStart || ( tapPointerStart.x === 0 && tapPointerStart.y === 0 )) { + return false; + } + var endCoordinates = getPointerCoordinates(endEvent); + + return Math.abs(tapPointerStart.x - endCoordinates.x) > TAP_RELEASE_TOLERANCE || + Math.abs(tapPointerStart.y - endCoordinates.y) > TAP_RELEASE_TOLERANCE; +} + +function getPointerCoordinates(event) { + // This method can get coordinates for both a mouse click + // or a touch depending on the given event + var c = { x:0, y:0 }; + if(event) { + var touches = event.touches && event.touches.length ? event.touches : [event]; + var e = (event.changedTouches && event.changedTouches[0]) || touches[0]; + if(e) { + c.x = e.clientX || e.pageX || 0; + c.y = e.clientY || e.pageY || 0; + } + } + return c; +} + +function tapContainingElement(ele, allowSelf) { + var climbEle = ele; + for(var x=0; x<6; x++) { + if(!climbEle) break; + if(climbEle.tagName === 'LABEL') return climbEle; + climbEle = ele.parentElement; + } + if(allowSelf !== false) return ele; +} + +function tapTargetElement(ele) { + if(ele && ele.tagName === 'LABEL') { + if(ele.control) return ele.control; + + // older devices do not support the "control" property + if(ele.querySelector) { + var control = ele.querySelector('input,textarea,select'); + if(control) return control; + } + } + return ele; +} + +ionic.DomUtil.ready(function(){ + + ionic.tap.register(document.body); + +}); diff --git a/js/views/scrollView.js b/js/views/scrollView.js index 57415842e4..cd5b7f749b 100644 --- a/js/views/scrollView.js +++ b/js/views/scrollView.js @@ -23,218 +23,218 @@ * based on the pure time difference. */ (function(global) { - var time = Date.now || function() { - return +new Date(); - }; - var desiredFrames = 60; - var millisecondsPerSecond = 1000; - var running = {}; - var counter = 1; + var time = Date.now || function() { + return +new Date(); + }; + var desiredFrames = 60; + var millisecondsPerSecond = 1000; + var running = {}; + var counter = 1; - // Create namespaces - if (!global.core) { - var core = global.core = { effect : {} }; + // Create namespaces + if (!global.core) { + var core = global.core = { effect : {} }; - } else if (!core.effect) { - core.effect = {}; - } + } else if (!core.effect) { + core.effect = {}; + } - core.effect.Animate = { + core.effect.Animate = { - /** - * A requestAnimationFrame wrapper / polyfill. - * - * @param callback {Function} The callback to be invoked before the next repaint. - * @param root {HTMLElement} The root element for the repaint - */ - requestAnimationFrame: (function() { + /** + * A requestAnimationFrame wrapper / polyfill. + * + * @param callback {Function} The callback to be invoked before the next repaint. + * @param root {HTMLElement} The root element for the repaint + */ + requestAnimationFrame: (function() { - // Check for request animation Frame support - var requestFrame = global.requestAnimationFrame || global.webkitRequestAnimationFrame || global.mozRequestAnimationFrame || global.oRequestAnimationFrame; - var isNative = !!requestFrame; + // Check for request animation Frame support + var requestFrame = global.requestAnimationFrame || global.webkitRequestAnimationFrame || global.mozRequestAnimationFrame || global.oRequestAnimationFrame; + var isNative = !!requestFrame; - if (requestFrame && !/requestAnimationFrame\(\)\s*\{\s*\[native code\]\s*\}/i.test(requestFrame.toString())) { - isNative = false; - } + if (requestFrame && !/requestAnimationFrame\(\)\s*\{\s*\[native code\]\s*\}/i.test(requestFrame.toString())) { + isNative = false; + } - if (isNative) { - return function(callback, root) { - requestFrame(callback, root) - }; - } + if (isNative) { + return function(callback, root) { + requestFrame(callback, root) + }; + } - var TARGET_FPS = 60; - var requests = {}; - var requestCount = 0; - var rafHandle = 1; - var intervalHandle = null; - var lastActive = +new Date(); + var TARGET_FPS = 60; + var requests = {}; + var requestCount = 0; + var rafHandle = 1; + var intervalHandle = null; + var lastActive = +new Date(); - return function(callback, root) { - var callbackHandle = rafHandle++; + return function(callback, root) { + var callbackHandle = rafHandle++; - // Store callback - requests[callbackHandle] = callback; - requestCount++; + // Store callback + requests[callbackHandle] = callback; + requestCount++; - // Create timeout at first request - if (intervalHandle === null) { + // Create timeout at first request + if (intervalHandle === null) { - intervalHandle = setInterval(function() { + intervalHandle = setInterval(function() { - var time = +new Date(); - var currentRequests = requests; + var time = +new Date(); + var currentRequests = requests; - // Reset data structure before executing callbacks - requests = {}; - requestCount = 0; + // Reset data structure before executing callbacks + requests = {}; + requestCount = 0; - for(var key in currentRequests) { - if (currentRequests.hasOwnProperty(key)) { - currentRequests[key](time); - lastActive = time; - } - } + for(var key in currentRequests) { + if (currentRequests.hasOwnProperty(key)) { + currentRequests[key](time); + lastActive = time; + } + } - // Disable the timeout when nothing happens for a certain - // period of time - if (time - lastActive > 2500) { - clearInterval(intervalHandle); - intervalHandle = null; - } + // Disable the timeout when nothing happens for a certain + // period of time + if (time - lastActive > 2500) { + clearInterval(intervalHandle); + intervalHandle = null; + } - }, 1000 / TARGET_FPS); - } + }, 1000 / TARGET_FPS); + } - return callbackHandle; - }; + return callbackHandle; + }; - })(), + })(), - /** - * Stops the given animation. - * - * @param id {Integer} Unique animation ID - * @return {Boolean} Whether the animation was stopped (aka, was running before) - */ - stop: function(id) { - var cleared = running[id] != null; - if (cleared) { - running[id] = null; - } + /** + * Stops the given animation. + * + * @param id {Integer} Unique animation ID + * @return {Boolean} Whether the animation was stopped (aka, was running before) + */ + stop: function(id) { + var cleared = running[id] != null; + if (cleared) { + running[id] = null; + } - return cleared; - }, + return cleared; + }, - /** - * Whether the given animation is still running. - * - * @param id {Integer} Unique animation ID - * @return {Boolean} Whether the animation is still running - */ - isRunning: function(id) { - return running[id] != null; - }, + /** + * Whether the given animation is still running. + * + * @param id {Integer} Unique animation ID + * @return {Boolean} Whether the animation is still running + */ + isRunning: function(id) { + return running[id] != null; + }, - /** - * Start the animation. - * - * @param stepCallback {Function} Pointer to function which is executed on every step. - * Signature of the method should be `function(percent, now, virtual) { return continueWithAnimation; }` - * @param verifyCallback {Function} Executed before every animation step. - * Signature of the method should be `function() { return continueWithAnimation; }` - * @param completedCallback {Function} - * Signature of the method should be `function(droppedFrames, finishedAnimation) {}` - * @param duration {Integer} Milliseconds to run the animation - * @param easingMethod {Function} Pointer to easing function - * Signature of the method should be `function(percent) { return modifiedValue; }` - * @param root {Element} Render root, when available. Used for internal - * usage of requestAnimationFrame. - * @return {Integer} Identifier of animation. Can be used to stop it any time. - */ - start: function(stepCallback, verifyCallback, completedCallback, duration, easingMethod, root) { + /** + * Start the animation. + * + * @param stepCallback {Function} Pointer to function which is executed on every step. + * Signature of the method should be `function(percent, now, virtual) { return continueWithAnimation; }` + * @param verifyCallback {Function} Executed before every animation step. + * Signature of the method should be `function() { return continueWithAnimation; }` + * @param completedCallback {Function} + * Signature of the method should be `function(droppedFrames, finishedAnimation) {}` + * @param duration {Integer} Milliseconds to run the animation + * @param easingMethod {Function} Pointer to easing function + * Signature of the method should be `function(percent) { return modifiedValue; }` + * @param root {Element} Render root, when available. Used for internal + * usage of requestAnimationFrame. + * @return {Integer} Identifier of animation. Can be used to stop it any time. + */ + start: function(stepCallback, verifyCallback, completedCallback, duration, easingMethod, root) { - var start = time(); - var lastFrame = start; - var percent = 0; - var dropCounter = 0; - var id = counter++; + var start = time(); + var lastFrame = start; + var percent = 0; + var dropCounter = 0; + var id = counter++; - if (!root) { - root = document.body; - } + if (!root) { + root = document.body; + } - // Compacting running db automatically every few new animations - if (id % 20 === 0) { - var newRunning = {}; - for (var usedId in running) { - newRunning[usedId] = true; - } - running = newRunning; - } + // Compacting running db automatically every few new animations + if (id % 20 === 0) { + var newRunning = {}; + for (var usedId in running) { + newRunning[usedId] = true; + } + running = newRunning; + } - // This is the internal step method which is called every few milliseconds - var step = function(virtual) { + // This is the internal step method which is called every few milliseconds + var step = function(virtual) { - // Normalize virtual value - var render = virtual !== true; + // Normalize virtual value + var render = virtual !== true; - // Get current time - var now = time(); + // Get current time + var now = time(); - // Verification is executed before next animation step - if (!running[id] || (verifyCallback && !verifyCallback(id))) { + // Verification is executed before next animation step + if (!running[id] || (verifyCallback && !verifyCallback(id))) { - running[id] = null; - completedCallback && completedCallback(desiredFrames - (dropCounter / ((now - start) / millisecondsPerSecond)), id, false); - return; + running[id] = null; + completedCallback && completedCallback(desiredFrames - (dropCounter / ((now - start) / millisecondsPerSecond)), id, false); + return; - } + } - // For the current rendering to apply let's update omitted steps in memory. - // This is important to bring internal state variables up-to-date with progress in time. - if (render) { + // For the current rendering to apply let's update omitted steps in memory. + // This is important to bring internal state variables up-to-date with progress in time. + if (render) { - var droppedFrames = Math.round((now - lastFrame) / (millisecondsPerSecond / desiredFrames)) - 1; - for (var j = 0; j < Math.min(droppedFrames, 4); j++) { - step(true); - dropCounter++; - } + var droppedFrames = Math.round((now - lastFrame) / (millisecondsPerSecond / desiredFrames)) - 1; + for (var j = 0; j < Math.min(droppedFrames, 4); j++) { + step(true); + dropCounter++; + } - } + } - // Compute percent value - if (duration) { - percent = (now - start) / duration; - if (percent > 1) { - percent = 1; - } - } + // Compute percent value + if (duration) { + percent = (now - start) / duration; + if (percent > 1) { + percent = 1; + } + } - // Execute step callback, then... - var value = easingMethod ? easingMethod(percent) : percent; - if ((stepCallback(value, now, render) === false || percent === 1) && render) { - running[id] = null; - completedCallback && completedCallback(desiredFrames - (dropCounter / ((now - start) / millisecondsPerSecond)), id, percent === 1 || duration == null); - } else if (render) { - lastFrame = now; - core.effect.Animate.requestAnimationFrame(step, root); - } - }; + // Execute step callback, then... + var value = easingMethod ? easingMethod(percent) : percent; + if ((stepCallback(value, now, render) === false || percent === 1) && render) { + running[id] = null; + completedCallback && completedCallback(desiredFrames - (dropCounter / ((now - start) / millisecondsPerSecond)), id, percent === 1 || duration == null); + } else if (render) { + lastFrame = now; + core.effect.Animate.requestAnimationFrame(step, root); + } + }; - // Mark as running - running[id] = true; + // Mark as running + running[id] = true; - // Init first step - core.effect.Animate.requestAnimationFrame(step, root); + // Init first step + core.effect.Animate.requestAnimationFrame(step, root); - // Return unique animation ID - return id; - } - }; + // Return unique animation ID + return id; + } + }; })(this); /* @@ -254,28 +254,28 @@ var Scroller; (function(ionic) { - var NOOP = function(){}; + var NOOP = function(){}; - // Easing Equations (c) 2003 Robert Penner, all rights reserved. - // Open source under the BSD License. + // Easing Equations (c) 2003 Robert Penner, all rights reserved. + // Open source under the BSD License. - /** - * @param pos {Number} position between 0 (start of effect) and 1 (end of effect) - **/ - var easeOutCubic = function(pos) { - return (Math.pow((pos - 1), 3) + 1); - }; + /** + * @param pos {Number} position between 0 (start of effect) and 1 (end of effect) + **/ + var easeOutCubic = function(pos) { + return (Math.pow((pos - 1), 3) + 1); + }; - /** - * @param pos {Number} position between 0 (start of effect) and 1 (end of effect) - **/ - var easeInOutCubic = function(pos) { - if ((pos /= 0.5) < 1) { - return 0.5 * Math.pow(pos, 3); - } + /** + * @param pos {Number} position between 0 (start of effect) and 1 (end of effect) + **/ + var easeInOutCubic = function(pos) { + if ((pos /= 0.5) < 1) { + return 0.5 * Math.pow(pos, 3); + } - return 0.5 * (Math.pow((pos - 2), 3) + 2); - }; + return 0.5 * (Math.pow((pos - 2), 3) + 2); + }; /** @@ -301,7 +301,7 @@ ionic.views.Scroll = ionic.views.View.inherit({ } }); - this.options = { + this.options = { /** Disable scrolling on x-axis by default */ scrollingX: false, @@ -370,17 +370,42 @@ ionic.views.Scroll = ionic.views.View.inherit({ // The ms interval for triggering scroll events scrollEventInterval: 50 - }; + }; - for (var key in options) { - this.options[key] = options[key]; - } + for (var key in options) { + this.options[key] = options[key]; + } this.hintResize = ionic.debounce(function() { self.resize(); }, 1000, true); + this.onScroll = function(scrollTop) { + + if(!ionic.scroll.isScrolling) { + setTimeout(self.setScrollStart, 50); + } else { + clearTimeout(self.scrollTimer); + self.scrollTimer = setTimeout(self.setScrollStop, 80); + } + + }; + + this.setScrollStart = function() { + ionic.scroll.isScrolling = Math.abs(ionic.scroll.lastTop - self.__scrollTop) > 1; + clearTimeout(self.scrollTimer); + self.scrollTimer = setTimeout(self.setScrollStop, 80); + }; + + this.setScrollStop = function() { + ionic.scroll.isScrolling = false; + ionic.scroll.lastTop = self.__scrollTop; + }; + this.triggerScrollEvent = ionic.throttle(function() { + + self.onScroll(); + ionic.trigger('scroll', { scrollTop: self.__scrollTop, scrollLeft: self.__scrollLeft, @@ -401,7 +426,7 @@ ionic.views.Scroll = ionic.views.View.inherit({ // Get the render update function, initialize event handlers, // and calculate the size of the scroll container - this.__callback = this.getRenderFn(); + this.__callback = this.getRenderFn(); this.__initEventHandlers(); this.__createScrollbars(); @@ -412,7 +437,7 @@ ionic.views.Scroll = ionic.views.View.inherit({ // Fade them out this.__fadeScrollbars('out', this.options.scrollbarResizeFadeDelay); - }, + }, @@ -597,18 +622,50 @@ ionic.views.Scroll = ionic.views.View.inherit({ //Broadcasted when keyboard is shown on some platforms. //See js/utils/keyboard.js container.addEventListener('scrollChildIntoView', function(e) { + var keyboardHeight = e.detail.keyboardHeight || 0; var deviceHeight = window.innerHeight; - var element = e.target; - var elementHeight = e.target.offsetHeight; - //getBoundingClientRect() will actually give us position relative to the viewport - var elementDeviceTop = element.getBoundingClientRect().top; - var elementScrollTop = ionic.DomUtil.getPositionInParent(element, container).top; + var frameHeight; + if (ionic.Platform.isIOS() && ionic.Platform.version() >= 7.0 || (!ionic.Platform.isWebView() && ionic.Platform.isAndroid())){ + frameHeight = deviceHeight; + } + else { + frameHeight = deviceHeight - keyboardHeight; + } + + var element = e.target; + + //getBoundingClientRect() will give us position relative to the viewport + var elementDeviceBottom = element.getBoundingClientRect().bottom; + + if (e.detail.firstKeyboardShow){ + //shrink scrollview so we can actually scroll if the input is hidden + //if it isn't shrink so we can scroll to inputs under the keyboard + container.style.height = (container.clientHeight - keyboardHeight) + "px"; + container.style.overflow = "visible"; + + //update scroll view + self.resize(); + } //If the element is positioned under the keyboard... - if (elementDeviceTop + elementHeight > deviceHeight) { + if (elementDeviceBottom > frameHeight) { //Put element in middle of visible screen - self.scrollTo(0, elementScrollTop + elementHeight - (deviceHeight * 0.5), true); + //Wait for resize() to reset scroll position + setTimeout(function(){ + //distance from top of input to the top of the keyboard + var keyboardTopOffset = element.getBoundingClientRect().top - frameHeight; + //middle of the scrollview, where we want to scroll to + var scrollViewMidpointOffset = container.clientHeight * 0.5; + var scrollOffset = keyboardTopOffset + scrollViewMidpointOffset; + self.scrollBy(0, scrollOffset, true); + + //please someone tell me there's a better way to do this + //wait until input is scrolled into view, then fix focus + setTimeout(function(){ + element.value = element.value; //thanks @adambradley 1337h4x + }, 600); + }, 32); } //Only the first scrollView parent of the element that broadcasted this event @@ -616,33 +673,103 @@ ionic.views.Scroll = ionic.views.View.inherit({ e.stopPropagation(); }); - if ('ontouchstart' in window) { + container.addEventListener('resetScrollView', function(e) { + //return scrollview to original height once keyboard has hidden + container.style.height = ""; + container.style.overflow = ""; + self.resize(); + }); - container.addEventListener("touchstart", function(e) { - if ( ionic.tap.ignoreScrollStart(e) ) { - return; - } + + self.touchStart = function(e) { + self.startCoordinates = getPointerCoordinates(e); + + if ( ionic.tap.ignoreScrollStart(e) ) { + return; + } + + if( ionic.tap.containsOrIsTextInput(e.target) ) { + // do not start if the target is a text input + // if there is a touchmove on this input, then we can start the scroll + self.__hasStarted = false; + return; + } + + self.__isSelectable = true; + self.__enableScrollY = true; + self.__hasStarted = true; + self.doTouchStart(e.touches, e.timeStamp); + e.preventDefault(); + }; + + self.touchMove = function(e) { + if(e.defaultPrevented) { + return; + } + + if( !self.__hasStarted && ionic.tap.containsOrIsTextInput(e.target) ) { + // the target is a text input and scroll has started + // since the text input doesn't start on touchStart, do it here + self.__hasStarted = true; self.doTouchStart(e.touches, e.timeStamp); e.preventDefault(); - }, false); + return; + } - document.addEventListener("touchmove", function(e) { - if(e.defaultPrevented) { - return; + if(self.startCoordinates) { + // we have start coordinates, so get this touch move's current coordinates + var currentCoordinates = getPointerCoordinates(e); + + if( self.__isSelectable && + ionic.tap.isTextInput(e.target) && + Math.abs(self.startCoordinates.x - currentCoordinates.x) > 20 ) { + // user slid the text input's caret on its x axis, disable any future y scrolling + self.__enableScrollY = false; + self.__isSelectable = true; } - self.doTouchMove(e.touches, e.timeStamp); - }, false); - document.addEventListener("touchend", function(e) { - self.doTouchEnd(e.timeStamp); - }, false); + if( self.__enableScrollY && Math.abs(self.startCoordinates.y - currentCoordinates.y) > 10 ) { + // user scrolled the entire view on the y axis + // disabled being able to select text on an input + // hide the input which has focus, and show a cloned one that doesn't have focus + self.__isSelectable = false; + ionic.tap.cloneFocusedInput(self.__container); + } + } + + self.doTouchMove(e.touches, e.timeStamp); + }; + + self.touchEnd = function(e) { + self.doTouchEnd(e.timeStamp); + self.__hasStarted = false; + self.__isSelectable = true; + self.__enableScrollY = true; + + if( !self.__isDragging && !self.__isDecelerating && !self.__isAnimating ) { + ionic.tap.removeClonedInputs(self.__container); + } + }; + + self.options.orgScrollingComplete = self.options.scrollingComplete; + self.options.scrollingComplete = function() { + ionic.tap.removeClonedInputs(self.__container); + self.options.orgScrollingComplete(); + }; + + + if ('ontouchstart' in window) { + container.addEventListener("touchstart", self.touchStart, false); + document.addEventListener("touchmove", self.touchMove, false); + document.addEventListener("touchend", self.touchEnd, false); + document.addEventListener("touchcancel", self.touchEnd, false); } else { var mousedown = false; container.addEventListener("mousedown", function(e) { - if ( ionic.tap.ignoreScrollStart(e) ) { + if ( ionic.tap.ignoreScrollStart(e) || e.target.tagName === 'SELECT' ) { return; } self.doTouchStart([{ @@ -898,9 +1025,9 @@ ionic.views.Scroll = ionic.views.View.inherit({ // Update Scroller dimensions for changed content // Add padding to bottom of content this.setDimensions( - this.__container.clientWidth, - this.__container.clientHeight, - Math.max(this.__content.scrollWidth, this.__content.offsetWidth), + this.__container.clientWidth, + this.__container.clientHeight, + Math.max(this.__content.scrollWidth, this.__content.offsetWidth), Math.max(this.__content.scrollHeight, this.__content.offsetHeight) ); }, @@ -915,7 +1042,7 @@ ionic.views.Scroll = ionic.views.View.inherit({ var content = this.__content; - var docStyle = document.documentElement.style; + var docStyle = document.documentElement.style; var engine; if ('MozAppearance' in docStyle) { @@ -2075,4 +2202,9 @@ ionic.views.Scroll = ionic.views.View.inherit({ } }); +ionic.scroll = { + isScrolling: false, + lastTop: 0 +}; + })(ionic); diff --git a/scss/_form.scss b/scss/_form.scss index 999ac81864..e3327c7df7 100644 --- a/scss/_form.scss +++ b/scss/_form.scss @@ -213,30 +213,6 @@ input[type="color"] { border: 0; } -// Fix to hide input overlays on old Android -.platform-android2, -.platform-android4_0 { - textarea, - input[type="text"], - input[type="password"], - input[type="datetime"], - input[type="datetime-local"], - input[type="date"], - input[type="month"], - input[type="time"], - input[type="week"], - input[type="number"], - input[type="email"], - input[type="url"], - input[type="search"], - input[type="tel"], - input[type="color"] { - position: relative; - top: -9999px; - @include translate3d(0, 9999px, 0); - } -} - // Position radios and checkboxes better input[type="radio"], input[type="checkbox"] { @@ -260,6 +236,15 @@ input[type="file"] { line-height: $input-height-base; } +// Text input classes to hide text caret during scroll +.previous-input-focus, +.cloned-text-input + input, +.cloned-text-input + textarea { + position: absolute; + left: -9999px; + width: 200px; +} + // Placeholder // ------------------------------- diff --git a/scss/_scaffolding.scss b/scss/_scaffolding.scss index e0b1a678b2..7feac03d1a 100644 --- a/scss/_scaffolding.scss +++ b/scss/_scaffolding.scss @@ -18,7 +18,6 @@ body, @include tap-highlight-transparent(); @include user-select(none); - position: fixed; top: 0; right: 0; bottom: 0; diff --git a/scss/_select.scss b/scss/_select.scss index f1327ea6a6..9037a1bdec 100644 --- a/scss/_select.scss +++ b/scss/_select.scss @@ -4,88 +4,45 @@ * -------------------------------------------------- */ - -// shared styles for transparent select elements -.select-inline select, -.item-select select { - @include appearance(none); - border: none; - background: transparent; - color: #333; - - // hack to hide default dropdown arrow in FF - text-indent: 1px; - text-overflow: ''; - - white-space: nowrap; - font-size: $font-size-base; - - cursor: pointer; -} - -.select-inline { - position: relative; - display: inline-block; - overflow: hidden; - border: $input-border-width solid $input-border; - border-radius: 2px; - background-color: $input-bg; - color: $input-color; - - vertical-align: middle; - font-size: $font-size-base; - line-height: $font-size-base + 2; - - select { - padding: 6px 25px 7px 8px; - } -} - -.select-inline + .select-inline { - margin-left: 6px; -} - .item-select { position: relative; select { + @include appearance(none); position: absolute; top: 0; right: 0; - padding: ($item-padding - 2) ($item-padding * 3) ($item-padding) 5px; - width: 100%; + padding: ($item-padding - 2) ($item-padding * 3) ($item-padding) $item-padding; + + border: none; + background: transparent; + color: #333; + + // hack to hide default dropdown arrow in FF + text-indent: .01px; + text-overflow: ''; + + white-space: nowrap; + font-size: $font-size-base; + + cursor: pointer; direction: rtl; // right align the select text - - &:focus { - min-width: 30%; - width: auto; // shrink width when opened - } } -} - -// shared styles for dropdown arrow -.select-inline:after, -.item-select:after { - position: absolute; - top: 50%; - margin-top: -3px; - width: 0; - height: 0; - border-top: 5px solid; - border-right: 5px solid rgba(0, 0, 0, 0); - border-left: 5px solid rgba(0, 0, 0, 0); - color: #999; - content: ""; - pointer-events: none; -} - -.select-inline:after { - right: 7px; -} - -.item-select:after { - right: $item-padding; + &:after { + position: absolute; + top: 50%; + right: $item-padding; + margin-top: -3px; + width: 0; + height: 0; + border-top: 5px solid; + border-right: 5px solid rgba(0, 0, 0, 0); + border-left: 5px solid rgba(0, 0, 0, 0); + color: #999; + content: ""; + pointer-events: none; + } } select { diff --git a/test/html/clickTests.html b/test/html/clickTests.html index 45181af31b..c3e5f4bd3d 100644 --- a/test/html/clickTests.html +++ b/test/html/clickTests.html @@ -68,7 +68,7 @@ ng-model="radioModel.data" ng-value="'Radio B'">Radio B - +   - +
diff --git a/test/html/clickTests2.html b/test/html/clickTests2.html new file mode 100644 index 0000000000..04d7120bf3 --- /dev/null +++ b/test/html/clickTests2.html @@ -0,0 +1,346 @@ + + + + + + + + + +
+ + Link + + + +
+ +
+ + +
div
+
+ +
+
ng
+ Link + + + +
+ +
+ + +
+ +
+ +
+ +
+ +
+ + + +
+
click
+
mousedown
+
mouseup
+
mousemove
+
touchstart
+
touchend
+
touchmove
+
touchcancel
+
focusin
+
focusout
+
clickblocked
+
+ +
+ + + + + + diff --git a/test/html/input.html b/test/html/input.html index 61178a6c57..8bc0350939 100644 --- a/test/html/input.html +++ b/test/html/input.html @@ -3,20 +3,33 @@ Inputs - + + +
+
+ Select +
+ +
+
+ Div Wrapped Select +
+ +
+ +
+
+ Div Wrapped Select +
+ +
+ + + + +
Other Tests
diff --git a/test/unit/angular/service/tap.unit.js b/test/unit/angular/service/tap.unit.js index 5406a9c2c3..69e38b1562 100644 --- a/test/unit/angular/service/tap.unit.js +++ b/test/unit/angular/service/tap.unit.js @@ -1,125 +1,440 @@ + +/* + +Physical Device Testing Scenarios +--------------------------------- +- Keyboard should show up when tapping on a text input +- Keyboard should show up when tapping on a label which surrounds a text input +- Keyboard should stay up if focused text input is tapped again +- Keyboard should go away when text input is focused, then tapped outside of input +- Keyboard should hide when tapping the virtual keyboard's "Done" or down arrow, but tapping +- Should be able to move an inputs text caret when its focused +- Should be able to move an inputs text caret when its focused, and is wrapped with a label + the input again will bring up the keyboard again +- Options dialog should show when tapping on a select +- Options dialog should show when tapping on a label which surrounds a select (not working in Android 2.3) +- Tapping a button element should fire one click +- Tapping an anchor element should fire one click +- Tapping a checkbox should fire one click +- Tapping a label which surrounds a checkbox should fire one click +- Tapping a radio button should fire one click +- Tapping a label which surrounds a radio button should fire one click +- Moving an input[range] slider should work +- Moving an input[range] slider when its in menu content of a should work +- Tapping the track on an input[range] slider should move the knob to that location (not a default in iOS) +- Tapping an input[file] should bring up the file dialog +- After tapping an input[file] and closing the input file dialog, tap a different + element and the file dialog should NOT show up +- Element which is disabled should not be clicked +- Holding a touchstart, and not moving, should fire the click no matter how long the hold +- Holding a mousedown, and not moving, should fire the click no matter how long the hold +- Holding touchstart, then moving a few pixels cancels the click +- Holding mousedown, then moving a few pixels cancels the click +- Touchstart should set and remove the activated css class +- Mousedown should set and remove the activated css class +- Holding touchstart, then moving a few pixels removes the activated css class +- Holding mousedown, then moving a few pixels removes the activated css class +- An element or one of its parents with data-tap-disabled attribute should still click, but w/ a delay +- ALL THE ABOVE, BUT NOW WITH NG-CLICK ON THE INPUT! +- Tapping a div with an click event added should fire one click +- Tapping an img with an click event added should fire one click +- Can scroll when target is a text input, but it does not have focus +- Can scroll when the target is a text input and it already has focus +- Can hold a text input and move its text caret/cursor +- Can scroll when the target is a label +- Does not change focus 300ms after when tapping an input and the keyboard shows up +- The blinking cursor says in the input 300ms after the tap +- Can hit the keyboard's "next" button to change focus to the next input +- When flicking down a page, and a target was an input, it shouldn't focus and jank scrolling +- Do not show text caret of a focused input while scrolling, no matter what the target is +- After scrolling has come to a halt, previously focused input should be focused again +- Keyboard should stay up while scrolling if an input was focused +- Keyboard should not go away after scrolling stops and it focuses back on the previous focused input +- Should not create clones for tap inputs to hide cursor, like for checkboxes, radio, range, select +- Focus on an input, flick up, let go, and during animation flick down, and clone should go away +- While inputs are actively scrolling you should not be able to change focus +- If you touchstart on a text input, then scrolland touchend, it should not bring up the keyboard +- If you touchstart on a label wrapping a text input, then scroll and touchend, it should not bring up the keyboard +- When focused in a text input, be able to get out when tapping a checkbox +- Can scroll when the target is a select element (touch events only) +- Can scroll when the target is a label which wraps select element +- Can open the select options on touch when not scrolling +- Can open the select options mouse click when target is a select +- Can open the select options when target is a label wrapping a select + +Tested on: +---------------------------- +- iOS 6.1 iPad 3 +- iOS 7.0 iPhone 4 +- iOS 7.0 iPhone 5 +- iOS 7.1 iPhone 5 +- iOS 7.1 Simulator +- Android 2.3 HTC Incredible +- Android 2.3 Samsung Galaxy S +- Android 4.0 HTC Incredible +- Android 4.2 Nexus 4 +- Android 4.3 Samsung S3 +- Android 4.4 Motorola Moto G +- Android 4.4 Nexus 5 +- OSX Chrome +- OSX Chrome (emulated touch screen) +- OSX Firefox + +*/ + +window.console.debug = function(){}; + describe('Ionic Tap', function() { + var deregisterTap; beforeEach(function() { - window.console.debug = function(){}; - ionic.tap.reset(); window._setTimeout = window.setTimeout; window.setTimeout = function(){}; + _activeElement = null; // the element which has focus + + deregisterTap = ionic.tap.register(document.createElement('div')); + ionic.scroll = { isScrolling: false }; }); afterEach(function(){ window.setTimeout = window._setTimeout; + deregisterTap(); }); - it('Should focus on an input if it hasnt scrolled', function() { - var targetEle = { - dispatchEvent: function() {}, - focus: function() { this.isFocused = true; } + it('Should trigger a labels child inputs click, and should not stop the labels end event so Android allows text editing', function() { + var e = { + type: 'touchstart', + target: { + tagName: 'LABEL', + dispatchEvent: function() { e.target.dispatchedEvent = true; }, + focus: function() { e.target.focused = true; }, + control: { + tagName: 'INPUT', + dispatchEvent: function() { e.target.control.dispatchedEvent = true; }, + focus: function() { e.target.control.focused = true; } + } + }, + stopPropagation: function() { e.stoppedPropagation = true; }, + preventDefault: function() { e.preventedDefault = true; } }; - ionic.tap.setTouchStart({clientX: 100, clientY: 100}); + tapClick(e); - targetEle.tagName = 'INPUT'; + expect( e.target.dispatchedEvent ).toBeUndefined(); + expect( e.target.focused ).toBeUndefined(); + + expect( e.target.control.dispatchedEvent ).toBeDefined(); + expect( e.target.control.focused ).toBeDefined(); + + expect( e.stoppedPropagation ).toBeUndefined(); + expect( e.preventedDefault ).toBeUndefined(); + }); + + it('Should trigger a click for an element w/out a wrapping label', function() { + var e = { + type: 'touchstart', + target: { + tagName: 'INPUT', + dispatchEvent: function() { e.target.dispatchedEvent = true; }, + focus: function() { e.target.focused = true; } + }, + stopPropagation: function() { e.stoppedPropagation = true; }, + preventDefault: function() { e.preventedDefault = true; } + }; + + tapClick(e); + + expect( e.target.dispatchedEvent ).toBeDefined(); + expect( e.target.focused ).toBeDefined(); + + expect( e.stoppedPropagation ).toBeUndefined(); + expect( e.preventedDefault ).toBeUndefined(); + + }); + + it('Should not trigger a click if tapPointerMoved has moved', function() { + var e = { + type: 'touchstart', + target: { + tagName: 'INPUT', + dispatchEvent: function() { e.target.dispatchedEvent = true; }, + focus: function() { e.target.focused = true; } + }, + stopPropagation: function() { e.stoppedPropagation = true; }, + preventDefault: function() { e.preventedDefault = true; } + }; + + tapPointerMoved = true; + expect( tapClick(e) ).toEqual(false); + + }); + + it('Should trigger click on mouseup and when nearby mousedown happened', function() { + var e = { + type: 'mousedown', + clientX: 100, clientY: 100, + target: { + tagName: 'INPUT', + dispatchEvent: function() { e.target.dispatchedEvent = true; }, + focus: function() { e.target.focused = true; } + }, + stopPropagation: function() { e.stoppedPropagation = true; }, + preventDefault: function() { e.preventedDefault = true; } + }; + + expect( e.target.dispatchedEvent ).toBeUndefined(); + + tapMouseDown({clientX: 101, clientY: 101}); + tapMouseUp(e); + + expect( e.target.dispatchedEvent ).toBeDefined(); + }); + + it('Should not trigger click on mouseup because mousedown coordinates too far away', function() { var e = { clientX: 100, clientY: 100, - preventDefault: function() {} - }; - ionic.tap.simulateClick(targetEle, e); - - expect(targetEle.isFocused).toEqual(true); - }); - - it('Should setTouchStart and hasTouchScrolled true if >= touch tolerance', function() { - ionic.tap.setTouchStart({ clientX: 100, clientY: 100 }); - - var s = ionic.tap.hasTouchScrolled({ clientX: 111, clientY: 100 }); - expect(s).toEqual(true); - - s = ionic.tap.hasTouchScrolled({ clientX: 89, clientY: 100 }); - expect(s).toEqual(true); - - s = ionic.tap.hasTouchScrolled({ clientX: 100, clientY: 107 }); - expect(s).toEqual(true); - - s = ionic.tap.hasTouchScrolled({ clientX: 100, clientY: 93 }); - expect(s).toEqual(true); - - s = ionic.tap.hasTouchScrolled({ clientX: 100, clientY: 200 }); - expect(s).toEqual(true); - }); - - it('Should setTouchStart and hasTouchScrolled false if less than touch tolerance', function() { - ionic.tap.setTouchStart({ clientX: 100, clientY: 100 }); - - var s = ionic.tap.hasTouchScrolled({ clientX: 100, clientY: 100 }); - expect(s).toEqual(false); - - s = ionic.tap.hasTouchScrolled({ clientX: 104, clientY: 100 }); - expect(s).toEqual(false); - - s = ionic.tap.hasTouchScrolled({ clientX: 96, clientY: 100 }); - expect(s).toEqual(false); - - s = ionic.tap.hasTouchScrolled({ clientX: 100, clientY: 102 }); - expect(s).toEqual(false); - - s = ionic.tap.hasTouchScrolled({ clientX: 100, clientY: 98 }); - expect(s).toEqual(false); - }); - - it('Should not be hasTouchScrolled if 0 coordinates', function() { - var s = ionic.tap.hasTouchScrolled({ clientX: 0, clientY: 0 }); - expect(s).toEqual(false); - }); - - it('Should dispatch a mouse event', function() { - var targetEle = { - dispatchEvent: function(clickEvent) { - this.clickEvent = clickEvent; + target: { + tagName: 'INPUT', + dispatchEvent: function() { e.target.dispatchedEvent = true; }, + focus: function() { e.target.focused = true; } }, - tagName: 'LABEL' + stopPropagation: function() { e.stoppedPropagation = true; }, + preventDefault: function() { e.preventedDefault = true; } }; - var e = { clientX: 99, clientY: 88 }; - ionic.tap.simulateClick(targetEle, e); - expect(targetEle.clickEvent.clientX).toEqual(99); - expect(targetEle.clickEvent.clientY).toEqual(88); + expect( e.target.dispatchedEvent ).toBeUndefined(); + + tapMouseDown({clientX: 201, clientY: 101}); + tapMouseUp(e); + + expect( e.target.dispatchedEvent ).toBeUndefined(); }); - it('Should get coordinates from client originalEvent changedTouches', function() { - var e = { - clientX: 99, - clientY: 99, - originalEvent: { - changedTouches: [{ clientX: 77, clientY: 77 }] - } - }; - var c = ionic.tap.getCoordinates(e); - expect(c).toEqual({x:77, y: 77}); + it('Should set tapHasPointerMoved=false on tapTouchStart', function() { + tapPointerMoved = null; + tapTouchStart({ preventDefault:function(){} }); + expect( tapPointerMoved ).toEqual(false); }); - it('Should get coordinates from client mouse originalEvent', function() { + it('Should set tapPointerMoved=false on tapTouchCancel', function() { + tapPointerMoved = true; + tapTouchCancel(); + expect( tapPointerMoved ).toEqual(false); + }); + + it('Should set tapHasPointerMoved=true on tapTouchMove', function() { + tapPointerMoved = null; + tapTouchStart({ clientX: 100, clientY: 100, preventDefault:function(){} }); + expect( tapPointerMoved ).toEqual(false); + tapTouchMove({ clientX: 200, clientY: 100 }); + expect( tapPointerMoved ).toEqual(true); + }); + + it('Should set tapHasPointerMoved=false on tapMouseDown', function() { + tapPointerMoved = null; + tapMouseDown({}); + expect( tapPointerMoved ).toEqual(false); + }); + + it('Should set tapPointerMoved=false on tapMouseUp', function() { + tapPointerMoved = true; + tapMouseUp({}); + expect( tapPointerMoved ).toEqual(false); + }); + + it('Should set tapHasPointerMoved=true on tapMouseMove', function() { + tapPointerMoved = null; + tapMouseDown({ clientX: 100, clientY: 100 }); + expect( tapPointerMoved ).toEqual(false); + tapMouseMove({ clientX: 200, clientY: 100 }); + expect( tapPointerMoved ).toEqual(true); + }); + + it('Should trigger click on touchend and nearby touchstart happened', function() { var e = { - clientX: 99, - clientY: 99, - originalEvent: { - clientX: 88, - clientY: 88, + type: 'touchend', + clientX: 101, clientY: 101, + target: { + tagName: 'INPUT', + dispatchEvent: function() { e.target.dispatchedEvent = true; }, + focus: function() { e.target.focused = true; } + }, + stopPropagation: function() { e.stoppedPropagation = true; }, + preventDefault: function() { e.preventedDefault = true; } + }; + + tapTouchStart({clientX: 100, clientY: 100, preventDefault:function(){}}); + tapTouchEnd(e); + + expect( e.target.dispatchedEvent ).toBeDefined(); + }); + + it('Should not trigger click on touchend because touchstart coordinates too far away', function() { + var e = { + type: 'touchstart', + clientX: 100, clientY: 100, + target: { + tagName: 'INPUT', + dispatchEvent: function() { e.target.dispatchedEvent = true; }, + focus: function() { e.target.focused = true; } + }, + stopPropagation: function() { e.stoppedPropagation = true; }, + preventDefault: function() { e.preventedDefault = true; } + }; + + expect( e.target.dispatchedEvent ).toBeUndefined(); + + tapTouchStart({clientX: 200, clientY: 100, preventDefault:function(){}}); + + tapTouchEnd(e); + + expect( e.target.dispatchedEvent ).toBeUndefined(); + }); + + it('Should tapEnabledTouchEvents because of touchstart', function() { + tapEnabledTouchEvents = false; + tapTouchStart({preventDefault:function(){}}); + tapEnabledTouchEvents = true; + }); + + it('Should cancel click on touchcancel', function() { + tapTouchCancel(); + expect(tapPointerMoved).toEqual(false); + }); + + it('Should cancel click when touchmove coordinates goes too far from touchstart coordinates', function() { + var e = { clientX: 100, clientY: 100, preventDefault:function(){} }; + tapTouchStart(e); + + expect( tapTouchMove({ clientX: 102, clientY: 100 }) ).toBeUndefined(); + + expect( tapTouchMove({ clientX: 105, clientY: 100 }) ).toBeUndefined(); + + expect( tapTouchMove({ clientX: 200, clientY: 100 }) ).toEqual(false); + }); + + it('Should cancel click when touchend coordinates are too far from touchstart coordinates', function() { + var e = { + clientX: 100, clientY: 100, + dispatchEvent: function(){ this.dispatchedEvent = true; }, + preventDefault:function(){} + }; + tapTouchStart(e); + tapTouchEnd({ clientX: 200, clientY: 100 }); + expect( e.dispatchedEvent ).toBeUndefined(); + }); + + it('Should cancel click when mousemove coordinates goes too far from mousedown coordinates', function() { + var e = { clientX: 100, clientY: 100 }; + tapMouseDown(e); + + expect( tapMouseMove({ clientX: 102, clientY: 100 }) ).toBeUndefined(); + + expect( tapMouseMove({ clientX: 105, clientY: 100 }) ).toBeUndefined(); + + expect( tapMouseMove({ clientX: 200, clientY: 100 }) ).toEqual(false); + }); + + it('Should cancel click when mouseup coordinates are too far from mousedown coordinates', function() { + var e = { + clientX: 100, clientY: 100, + dispatchEvent: function(){ this.dispatchedEvent = true; } + }; + tapMouseDown(e); + tapMouseUp({ clientX: 200, clientY: 100 }); + expect( e.dispatchedEvent ).toBeUndefined(); + }); + + it('Should do nothing if mousedown is a custom event from ionic tap', function() { + var e = { + isTapHandled: false, + isIonicTap: true + }; + tapMouseDown(e); + expect( e.isTapHandled ).toEqual(false); + }); + + it('Should tapClick with touchend and fire immediately', function() { + var e = { + target: { + tagName: 'button', + dispatchEvent: function(){ + this.dispatchedEvent = true; + } } }; - var c = ionic.tap.getCoordinates(e); - expect(c).toEqual({x:88, y: 88}); + tapClick(e); + expect(e.target.dispatchedEvent).toEqual(true); + }); + + it('Should tapHasPointerMoved false if are null', function() { + expect( tapHasPointerMoved(null) ).toEqual(false); + }); + + it('Should tapPointerStart false if are null', function() { + tapPointerStart = null; + expect( tapHasPointerMoved(null) ).toEqual(false); + }); + + it('Should tapPointerStart false if are null', function() { + var e = {}; + tapPointerStart = {x:0, y:0}; + expect( tapHasPointerMoved(e) ).toEqual(false); + }); + + it('Should tapHasPointerMoved true if greater than or equal to release tolerance', function() { + tapPointerStart = { x: 100, y: 100 }; + + var s = tapHasPointerMoved({ clientX: 111, clientY: 100 }); + expect(s).toEqual(true); + + s = tapHasPointerMoved({ clientX: 89, clientY: 100 }); + expect(s).toEqual(true); + + s = tapHasPointerMoved({ clientX: 100, clientY: 109 }); + expect(s).toEqual(true); + + s = tapHasPointerMoved({ clientX: 100, clientY: 91 }); + expect(s).toEqual(true); + + s = tapHasPointerMoved({ clientX: 100, clientY: 200 }); + expect(s).toEqual(true); + }); + + it('Should tapHasPointerMoved false if less than release tolerance', function() { + tapPointerStart = { x: 100, y: 100 }; + + var s = tapHasPointerMoved({ clientX: 100, clientY: 100 }); + expect(s).toEqual(false); + + s = tapHasPointerMoved({ clientX: 104, clientY: 100 }); + expect(s).toEqual(false); + + s = tapHasPointerMoved({ clientX: 96, clientY: 100 }); + expect(s).toEqual(false); + + s = tapHasPointerMoved({ clientX: 100, clientY: 102 }); + expect(s).toEqual(false); + + s = tapHasPointerMoved({ clientX: 100, clientY: 98 }); + expect(s).toEqual(false); + }); + + it('Should not be tapHasPointerMoved if 0 coordinates', function() { + var e = { clientX: 0, clientY: 0 }; + var s = tapHasPointerMoved(e, { clientX: 100, clientY: 100 }); + expect(s).toEqual(false); }); it('Should get coordinates from page mouse event', function() { var e = { pageX: 77, pageY: 77 }; - var c = ionic.tap.getCoordinates(e); + var c = getPointerCoordinates(e); expect(c).toEqual({x:77, y: 77}); }); it('Should get coordinates from client mouse event', function() { var e = { clientX: 77, clientY: 77 }; - var c = ionic.tap.getCoordinates(e); + var c = getPointerCoordinates(e); expect(c).toEqual({x:77, y: 77}); }); @@ -128,7 +443,7 @@ describe('Ionic Tap', function() { touches: [{ clientX: 99, clientY: 99 }], changedTouches: [{ clientX: 88, clientY: 88 }] }; - var c = ionic.tap.getCoordinates(e); + var c = getPointerCoordinates(e); expect(c).toEqual({x:88, y: 88}); }); @@ -136,7 +451,7 @@ describe('Ionic Tap', function() { var e = { touches: [{ pageX: 99, pageY: 99 }] }; - var c = ionic.tap.getCoordinates(e); + var c = getPointerCoordinates(e); expect(c).toEqual({x:99, y: 99}); }); @@ -144,81 +459,223 @@ describe('Ionic Tap', function() { var e = { touches: [{ clientX: 99, clientY: 99 }] }; - var c = ionic.tap.getCoordinates(e); + var c = getPointerCoordinates(e); expect(c).toEqual({x:99, y: 99}); }); it('Should get 0 coordinates', function() { var e = {}; - var c = ionic.tap.getCoordinates(e); + var c = getPointerCoordinates(e); expect(c).toEqual({x:0, y: 0}); }); - it('Should ignoreSimulateClick for disabled elements', function() { + it('Should not tapClick for disabled elements', function() { // Disabled elements should not be tapped var targetEle = document.createElement('input'); targetEle.disabled = true; - expect( ionic.tap.ignoreSimulateClick(targetEle) ).toEqual(true); + + var e = { + target: targetEle + }; + + expect( tapClick(e) ).toEqual(false); }); - it('Should ignoreSimulateClick for input[range] elements', function() { - // Range and tap do not agree, probably because it doesn't have a delay to begin with - var targetEle = document.createElement('input'); - targetEle.type = 'range'; - expect( ionic.tap.ignoreSimulateClick(targetEle) ).toEqual(true); + it('Should tapRequiresNativeClick for invalid element', function() { + expect( tapRequiresNativeClick( null ) ).toEqual(true); }); - it('Should not ignoreSimulateClick for common inputs', function() { + it('Should tapRequiresNativeClick for input.disabled', function() { + var ele = document.createElement('input'); + ele.disabled = true; + expect( tapRequiresNativeClick( ele ) ).toEqual(true); + }); + + it('Should tapRequiresNativeClick for input[range]', function() { + var ele = document.createElement('input'); + ele.type = 'range'; + expect( tapRequiresNativeClick( ele ) ).toEqual(true); + }); + + it('Should tapRequiresNativeClick for input[file]', function() { + var ele = document.createElement('input'); + ele.type = 'file'; + expect( tapRequiresNativeClick( ele ) ).toEqual(true); + }); + + it('Should tapRequiresNativeClick for video element', function() { + var ele = document.createElement('video'); + expect( tapRequiresNativeClick( ele ) ).toEqual(true); + }); + + it('Should tapRequiresNativeClick for object element', function() { + var ele = document.createElement('object'); + expect( tapRequiresNativeClick( ele ) ).toEqual(true); + }); + + it('Should not tapRequiresNativeClick for common inputs', function() { var inputTypes = ['text', 'email', 'search', 'tel', 'number', 'date', 'month', 'password', null, undefined, '']; for(var x=0; x