/** * @ngdoc page * @name tap * @module ionic * @description * On touch devices such as a phone or tablet, some browsers implement a 300ms delay between * the time the user stops touching the display and the moment the browser executes the * click. This delay was initially introduced so the browser can know whether the user wants to * double-tap to zoom in on the webpage. Basically, the browser waits roughly 300ms to see if * the user is double-tapping, or just tapping on the display once. * * Out of the box, Ionic automatically removes the 300ms delay in order to make Ionic apps * feel more "native" like. Resultingly, other solutions such as * [fastclick](https://github.com/ftlabs/fastclick) and Angular's * [ngTouch](https://docs.angularjs.org/api/ngTouch) should not be included, to avoid conflicts. * * In some cases, third-party libraries may also be working with touch events which can interfere * with the tap system. For example, mapping libraries like Google or Leaflet Maps often implement * a touch detection system which conflicts with Ionic's tap system. * * ### Disabling the tap system * * To disable the tap for an element and all of its children elements, * add the attribute `data-tap-disabled="true"`. * * ```html *
*
*
* ``` * * ### Additional Notes: * * - Ionic tap works with Ionic's JavaScript scrolling * - Elements can come and go from the DOM and Ionic tap doesn't 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 * - Correct focus in/out on each input type (select, textearea, range) on each platform/device * - Shows and hides virtual keyboard correctly for each platform/device * - 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](https://github.com/driftyco/ionic/blob/master/test/unit/utils/tap.unit.js) for each scenario * */ /* IONIC TAP --------------- - Both touch and mouse events are added to the document.body on DOM ready - If a touch event happens, it does not use mouse event listeners - 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' - Triggering clicks with mouse events work the same as touch, except with mousedown/mouseup - Tapping inputs is disabled during scrolling */ 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; 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; }; }, ignoreScrollStart: function(e) { return (e.defaultPrevented) || // defaultPrevented has been assigned by another component handling the event (e.target.isContentEditable) || (/file|range/i).test(e.target.type) || (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 }, isTextInput: function(ele) { return !!ele && (ele.tagName == 'TEXTAREA' || ele.contentEditable === 'true' || (ele.tagName == 'INPUT' && !(/radio|checkbox|range|file|submit|reset/i).test(ele.type)) ); }, isLabelWithTextInput: function(ele) { var container = tapContainingElement(ele, false); return !!container && ionic.tap.isTextInput( tapTargetElement( container ) ); }, containsOrIsTextInput: function(ele) { return ionic.tap.isTextInput(ele) || ionic.tap.isLabelWithTextInput(ele); }, cloneFocusedInput: function(container, scrollIntance) { if(ionic.tap.hasCheckedClone) return; ionic.tap.hasCheckedClone = true; 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'; clonedInput.readOnly = true; focusInput.parentElement.insertBefore(clonedInput, focusInput); focusInput.style.top = focusInput.offsetTop; focusInput.classList.add('previous-input-focus'); } } }); }, hasCheckedClone: false, removeClonedInputs: function(container, scrollIntance) { ionic.tap.hasCheckedClone = false; ionic.requestAnimationFrame(function(){ var clonedInputs = container.querySelectorAll('.cloned-text-input'); var previousInputFocus = container.querySelectorAll('.previous-input-focus'); var x; for(x=0; 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); });