refactor(tap): Refactor tap system for improved tap/click/keyboard/scroll/focus

Overhaul of the tap system so the keyboard does not cover up focused
inputs, correctly bring up the keyboard on text input focus, disabling
focus during scroll, disabling clicks after a hold then scroll,
removing 300ms delay without additional event handlers on each element,
etc. Refactored the tap/click/scroll/activator events for more
testability, along with adding more tests.
This commit is contained in:
Adam Bradley
2014-04-17 08:26:25 -05:00
parent 445d9420b6
commit d0047cda44
13 changed files with 2054 additions and 894 deletions

View File

@@ -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,

View File

@@ -3,49 +3,6 @@
IonicModule
.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();

View File

@@ -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() {

View File

@@ -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);

View File

@@ -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);

View File

@@ -1,51 +1,86 @@
(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);
function resize() {
//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);
//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');
if(ionic.Platform.isWebView() && window.cordova && cordova.plugins && cordova.plugins.Keyboard) {
if (ionic.Platform.isIOS()) {
window.addEventListener('focusin', onBrowserFocusIn);
}
window.addEventListener('native.showkeyboard', onNativeKeyboardShow);
window.addEventListener('native.hidekeyboard', onNativeKeyboardHide);
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);
}
} else if (ionic.Platform.isAndroid()){
window.addEventListener('resize', onBrowserResize);
window.addEventListener('focusin', onBrowserFocusIn);
}
function onBrowserFocusIn(e) {
if(e.srcElement.tagName == 'INPUT' || e.srcElement.tagName == 'TEXTAREA' || e.srcElement.isContentEditable) {
//stop browser from scrolling the whole frame, use virtual scroll instead
document.body.scrollTop = 0;
}
}
}
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: document.activeElement
}, true);
}, 100);
} else {
// Otherwise we have a keyboard close or a *really* weird resize
document.body.classList.remove('keyboard-open');
}
}
function onNativeKeyboardShow(e) {
rememberedActiveEl = document.activeElement;
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);

View File

@@ -1,305 +1,438 @@
(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();
},
isLabelWithInput: 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';
},
cloneFocusedInput: function(container, instance) {
if(ionic.tap.hasCheckedClone) return;
ionic.tap.hasCheckedClone = true;
simulateClick: function(target, e) {
// simulate a normal click by running the element's click method then focus on it
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.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<clonedInputs.length; x++) {
clonedInputs[x].parentElement.removeChild( clonedInputs[x] );
}
// remember the coordinates of this click so if a tap or click in the
// same area quickly happened again we can ignore it
ionic.tap.recordCoordinates(e);
},
getCoordinates: function(event) {
// This method can get coordinates for both a mouse click
// or a touch depending on the given event
var gesture = (event.gesture ? event.gesture : event);
if(gesture) {
var touches = gesture.touches && gesture.touches.length ? gesture.touches : [gesture];
var e = (gesture.changedTouches && gesture.changedTouches[0]) ||
(gesture.originalEvent && gesture.originalEvent.changedTouches &&
gesture.originalEvent.changedTouches[0]) ||
touches[0].originalEvent || touches[0];
if(e) return { x: e.clientX || e.pageX || 0, y: e.clientY || e.pageY || 0 };
for(x=0; x<previousInputFocus.length; x++) {
previousInputFocus[x].classList.remove('previous-input-focus');
previousInputFocus[x].focus();
}
return { x:0, y:0 };
},
});
}
hasTouchScrolled: function(event) {
if(_hasTouchScrolled) return true;
};
// check if this click's coordinates are different than its touchstart/mousedown
var c = ionic.tap.getCoordinates(event);
function tapEventListener(type, enable, useCapture) {
if(enable !== false) {
tapDoc.addEventListener(type, tapEventListeners[type], useCapture);
} else {
tapDoc.removeEventListener(type, tapEventListeners[type]);
}
}
// Quick check for 0,0 which could be simulated mouse click for form submission
if(c.x === 0 && c.y === 0) {
return false;
function tapClick(e) {
// simulate a normal click by running the element's click method then focus on it
var container = tapContainingElement(e.target);
var ele = tapTargetElement(container);
if( tapRequiresNativeClick(ele) || tapPointerMoved ) return false;
var c = getPointerCoordinates(e);
console.debug('tapClick', e.type, ele.tagName, '('+c.x+','+c.y+')');
triggerMouseEvent('click', ele, c.x, c.y);
// if it's an input, focus in on the target, otherwise blur
tapHandleFocus(ele);
}
function triggerMouseEvent(type, ele, x, y) {
// using initMouseEvent instead of MouseEvent for our Android friends
var clickEvent = document.createEvent("MouseEvents");
clickEvent.initMouseEvent(type, true, true, window, 1, 0, 0, x, y, false, false, false, false, 0, null);
clickEvent.isIonicTap = true;
ele.dispatchEvent(clickEvent);
}
function tapClickGateKeeper(e) {
// do not allow through any click events that were not created by ionic.tap
if( ionic.scroll.isScrolling || !e.isIonicTap && !tapRequiresNativeClick(e.target) ) {
console.debug('clickPrevent', e.target.tagName);
e.stopPropagation();
if( !ionic.tap.isLabelWithInput(e.target) ) {
// labels clicks from native should not preventDefault othersize keyboard will not show on input focus
e.preventDefault();
}
return false;
}
}
function tapRequiresNativeClick(ele) {
if(!ele || ele.disabled || (/file|range/i).test(ele.type) || (/object|video/i).test(ele.tagName) ) {
return true;
}
if(ele.nodeType === 1) {
var element = ele;
while(element) {
if( (element.dataset ? element.dataset.tapDisabled : element.getAttribute('data-tap-disabled')) == 'true' ) {
return true;
}
element = element.parentElement;
}
}
return false;
}
// the allowed distance between touchstart/mousedown and
return (c.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 ) {
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);
});

View File

@@ -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,49 @@ 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";
//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,26 +672,95 @@ 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 = "";
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.isTextInput(e.target) || ionic.tap.isLabelWithInput(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.isTextInput(e.target) || ionic.tap.isLabelWithInput(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 {
@@ -898,9 +1023,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 +1040,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) {
@@ -2070,4 +2195,9 @@ ionic.views.Scroll = ionic.views.View.inherit({
}
});
ionic.scroll = {
isScrolling: false,
lastTop: 0
};
})(ionic);

View File

@@ -18,7 +18,6 @@ body,
@include tap-highlight-transparent();
@include user-select(none);
position: fixed;
top: 0;
right: 0;
bottom: 0;
@@ -138,6 +137,14 @@ body.grade-c {
}
}
.previous-input-focus,
.cloned-text-input + input,
.cloned-text-input + textarea {
position: absolute;
left: -9999px;
width: 200px;
}
@keyframes refresh-spin {
0% { transform: translate3d(0,0,0) rotate(0); }
100% { transform: translate3d(0,0,0) rotate(-180deg); }

346
test/html/clickTests2.html Normal file
View File

@@ -0,0 +1,346 @@
<html ng-app="ionicApp">
<head>
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">
<style>
html, body {
width: 100%;
height: 100%;
-webkit-overflow-scrolling: touch;
}
* {
margin: 0;
padding: 0;
outline: none;
-webkit-user-select: none;
}
div {
background-color: #eee;
}
input {
-webkit-user-select: auto;
}
button {
font-size: 24px;
background: gray;
}
.activated {
background: red !important;
}
.notifiers div {
position: fixed;
right: 0;
color: white;
padding: 1px;
width: 70px;
text-align: center;
display: none;
font-size: 10px;
}
#click {
top: 0;
background: green;
}
#mousedown {
top: 15px;
background: lightblue;
}
#mouseup {
top: 30px;
background: blue;
}
#mousemove {
top: 45px;
background: darkblue;
}
#touchstart {
top: 60px;
background: pink;
}
#touchend {
top: 75px;
background: red;
}
#touchmove {
top: 90px;
background: maroon;
}
#touchcancel {
top: 105px;
background: purple;
}
#focusin {
top: 120px;
background: orange;
}
#focusout {
top: 135px;
background: orangered;
}
#clickblocked {
top: 150px;
background: darkgray;
}
</style>
<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.7.2/leaflet.css" />
<script src="../../../../dist/js/ionic.bundle.js"></script>
</head>
<body ng-controller="MainCtrl">
<div style="margin-bottom: 20px">
<button style="padding: 5px">Btn</button>
<a href="#" style="display:inline-block; padding:10px; background:#eee">Link</a>
<label style="background: yellow; padding: 12px 0 12px 40px">
<input type="checkbox" style="width:25px;height:25px">
</label>
<label style="background: orange; padding: 12px 0 12px 40px">
<input type="radio" name="radio" style="width:25px;height:25px">
</label>
<label style="background: orangered; padding: 12px 0 12px 40px">
<input type="radio" name="radio" style="width:25px;height:25px">
</label>
</div>
<div style="margin-bottom: 20px">
<label style="background: green; padding: 12px 0 12px 50px">
<input style="width: 60px; height: 28px">
</label>
<label style="background: blue; padding: 12px 0 12px 50px;">
<select>
<option>Opt 1</option>
<option>Opt 2</option>
<option>Opt 3</option>
<option>Opt 4</option>
<option>Opt 5</option>
</select>
</label>
<div id="myDiv" style="display:inline-block; width: 20px; background: gray; padding: 12px 0 12px 60px;">div</div>
</div>
<div>
<div style="padding: 10px 16px; display:inline-block; background:maroon" ng-click="buttonClick()">ng</div>
<a ng-click="linkClick()" href="#" style="display:inline-block; padding:10px; background:#eee">Link</a>
<label style="background: yellow; padding: 12px 0 12px 40px">
<input ng-click="checkboxClick()" type="checkbox" style="width:25px;height:25px">
</label>
<label style="background: orange; padding: 12px 0 12px 40px">
<input type="radio" name="radio" style="width:25px;height:25px">
</label>
<label style="background: orangered; padding: 12px 0 12px 40px">
<input type="radio" name="radio" style="width:25px;height:25px">
</label>
</div>
<div style="margin:20px 0 10px;">
<input type="range" style="width: 150px">
<input type="file" style="width: 100px">
</div>
<div data-tap-disabled="true" style="display:none">
<leaflet id="map" events="events" center="center" testing="no-testing" markers="markers" defaults="defaults" style="width:80px; height:100px; right: 0; top: 170px; position:absolute; overflow:hidden;"></leaflet>
</div>
<div style="position:absolute; right:0; top: 270px; width: 80px; height:80px; overflow:hidden">
<img style="width:100%; height:100%;" src="" onclick="imgClick()">
</div>
<!--
<video controls width=320 height=200 poster="https://www.apple.com/home/images/promo_iphone5s.jpg">
<source src="http://mastbaumbishop.wikispaces.com/file/view/sample.m4v/77739109/sample.m4v">
<source src="http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4">
<source src="http://clips.vorwaerts-gmbh.de/big_buck_bunny.webm">
</video>
-->
<div class="notifiers">
<div id="click">click</div>
<div id="mousedown">mousedown</div>
<div id="mouseup">mouseup</div>
<div id="mousemove">mousemove</div>
<div id="touchstart">touchstart</div>
<div id="touchend">touchend</div>
<div id="touchmove">touchmove</div>
<div id="touchcancel">touchcancel</div>
<div id="focusin">focusin</div>
<div id="focusout">focusout</div>
<div id="clickblocked">clickblocked</div>
</div>
<div id="logs"></div>
</body>
<script src="http://cdn.leafletjs.com/leaflet-0.7.2/leaflet.js"></script>
<script src="/tmp/angular-leaflet-directive.js"></script>
<script>
window.timers = {};
var duration = 1000;
document.addEventListener('mousedown', setEvent, false);
document.addEventListener('mouseup', setEvent, false);
//document.addEventListener('mousemove', setEvent, false);
document.addEventListener('touchstart', setEvent, false);
document.addEventListener('touchend', setEvent, false);
document.addEventListener('touchmove', setEvent, false);
document.addEventListener('touchcancel', setEvent, false);
document.addEventListener('focusin', onFocusIn, false);
document.addEventListener('focusout', onFocusOut, false);
function setEvent(e) {
if(e.type.indexOf('move') < 0) {
console.debug(e.type, e.target.tagName);
}
var timerId = e.type + 'timerId';
clearTimeout(timerId);
var el = document.getElementById(e.type);
window.timers[timerId] = setTimeout(function(e2){
document.getElementById(e.type).style.display = '';
}, duration);
el.style.display = 'block';
}
function onFocusIn(e) {
console.debug('focusin', e.target.tagName, e.isSimulated);
clearTimeout( window.timers['focusin_timer'] );
document.getElementById('focusin').style.display = 'block';
window.timers['focusin_timer'] = setTimeout(function(){
document.getElementById('focusin').style.display = '';
}, duration);
}
function onFocusOut(e) {
console.debug('focusout', e.target.tagName);
clearTimeout( window.timers['focusout_timer'] );
document.getElementById('focusout').style.display = 'block';
window.timers['focusout_timer'] = setTimeout(function(){
document.getElementById('focusout').style.display = '';
}, duration);
}
function onClick(e) {
console.debug('click', (e.pointerType ? e.pointerType : 'mouse'), e.target.tagName)
clearTimeout( window.timers['onClick_timer'] );
document.getElementById('click').style.display = 'block';
document.getElementById('click').innerText = (e.pointerType ? e.pointerType : '') + ' click';
window.timers['onClick_timer'] = setTimeout(function(){
document.getElementById('click').style.display = '';
}, duration);
}
document.addEventListener('click', onClick, false);
function imgClick() {
console.debug('img click')
}
document.getElementById('myDiv').addEventListener('click', function(e){
console.debug('div clicked!');
}, false);
var msgs = [];
var index = 0;
var timeId;
var winConsoleError = console.error;
function getTime() {
var d = new Date();
return d.getSeconds() + '.' + d.getMilliseconds();
}
console.error = function() {
winConsoleError.apply(this, arguments);
var args = ['ERROR!'];
for (var i = 0, j = arguments.length; i < j; i++){
args.push(arguments[i]);
}
console.debug.apply(this, args);
};
console.debug = function() {
index++;
var msg = [];
msg.push(index);
for (var i = 0, j = arguments.length; i < j; i++){
msg.push(arguments[i]);
}
msg.push(getTime());
msg = msg.join(', ');
if(arguments[0] === 'ERROR!') msg = '<span style="color:red;font-weight:bold">' + msg + '</span>';
if(arguments[0] === 'touchstart') msg = '<span style="color:pink">' + msg + '</span>';
if(arguments[0] === 'touchend') msg = '<span style="color:red">' + msg + '</span>';
if(arguments[0] === 'mousedown') msg = '<span style="color:lightblue">' + msg + '</span>';
if(arguments[0] === 'mouseup') msg = '<span style="color:blue">' + msg + '</span>';
if(arguments[0] === 'focusin') msg = '<span style="color:orange">' + msg + '</span>';
if(arguments[0] === 'focusout') msg = '<span style="color:orangered">' + msg + '</span>';
if(arguments[0] === 'click') msg = '<span style="color:green;font-weight:bold">' + msg + '</span>';
if(arguments[0] === 'clickPrevent') msg = '<span style="color:gray;">' + msg + '</span>';
msgs.unshift( msg );
if(msgs.length > 30) {
msgs.splice(40);
}
// do this so we try not to interfere with the device performance
clearTimeout(timeId);
timeId = setTimeout(function(){
document.getElementById('logs').innerHTML = msgs.join('<br>');
}, 150);
}
angular.module('ionicApp', ['ionic', 'leaflet-directive'])
.controller('MainCtrl', function($scope){
$scope.buttonClick = function() {
console.debug('ng-click div');
};
$scope.linkClick = function() {
console.debug('ng-click link');
};
$scope.checkboxClick = function() {
console.debug('ng-click checkbox');
};
$scope.events = {
map: {
enable: ['click', 'moveend'],
logic: 'emit'
}
};
angular.extend($scope, {
defaults: {
tileLayer: "http://otile1.mqcdn.com/tiles/1.0.0/map/{z}/{x}/{y}.jpg",
maxZoom: 17,
minZoom: 3,
attributionControl: false,
detectRetina: true,
zoomControl: false
},
center: {
lat: 47.366667,
lng: 8.55,
zoom: 12
},
markers: {}
});
$scope.$on("leafletDirectiveMap.click", function(e, args) {
console.debug('map click');
});
});
</script>
</html>

View File

@@ -3,20 +3,33 @@
<meta charset="utf-8">
<title>Inputs</title>
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">
<link rel="stylesheet" href="../../../../dist/css/ionic.css">
<link rel="stylesheet" href="../../dist/css/ionic.css">
<style>
.dot {
position: absolute;
width: 3px;
height: 3px;
background-color: red;
z-index: 1000;
pointer-events: none;
input,
textarea {
background: #ddd !important;
}
input[type=text],
textarea {
background: orange !important;
}
input[type=text]:focus,
textarea:focus {
background: yellow !important;
}
input[type=text].cloned-text-input,
textarea.cloned-text-input {
background: red !important;
}
</style>
</head>
<body ng-controller="AppCtrl">
<div id="logs" style="position:fixed; top:0; left:0; z-index:9999; background: #eee; font-size:8px; line-height:10px; display:none; "></div>
<ion-view id="view">
<header>
<label class="item item-input">
@@ -30,88 +43,88 @@
<div class="list">
<label class="item item-input">
<span class="input-label">Your Name</span>
<input type="text" value="name input">
<input type="text" value="name input" id="name">
</label>
<div class="item item-checkbox">
<span class="input-label">Remember me</span>
<label class="checkbox">
<input type="checkbox">
<input type="checkbox" id="remember-me">
</label>
</div>
</div>
<label class="item item-input">
<span class="input-label">From</span>
<input type="text" value="from input">
<span class="input-label">From 1</span>
<input type="text" value="from input" id="from1">
</label>
<label class="item item-input">
<span class="input-label">To</span>
<input type="text" value="to input">
<span class="input-label">To 1</span>
<input type="text" value="to input" id="to1">
</label>
<label class="item item-input item-stacked-label">
<span class="input-label">Comment</span>
<textarea id="textarea">comment textarea</textarea>
<span class="input-label">Comment 1</span>
<textarea id="textarea" id="comment1">comment textarea</textarea>
</label>
<label class="item item-input">
<span class="input-label">From</span>
<span class="input-label">From 2</span>
<input type="text" value="" id="from2">
</label>
<label class="item item-input">
<span class="input-label">To 2</span>
<input type="text" value="" id="to2">
</label>
<label class="item item-input item-stacked-label">
<span class="input-label">Comment 2</span>
<textarea id="textarea" id="comment2"></textarea>
</label>
<label class="item item-input">
<span class="input-label">From 3</span>
<input type="text" value="">
</label>
<label class="item item-input">
<span class="input-label">To</span>
<span class="input-label">To 3</span>
<input type="text" value="">
</label>
<label class="item item-input item-stacked-label">
<span class="input-label">Comment</span>
<span class="input-label">Comment 3</span>
<textarea id="textarea"></textarea>
</label>
<label class="item item-input">
<span class="input-label">From</span>
<span class="input-label">From 4</span>
<input type="text" value="">
</label>
<label class="item item-input">
<span class="input-label">To</span>
<span class="input-label">To 4</span>
<input type="text" value="">
</label>
<label class="item item-input item-stacked-label">
<span class="input-label">Comment</span>
<span class="input-label">Comment 4</span>
<textarea id="textarea"></textarea>
</label>
<label class="item item-input">
<span class="input-label">From</span>
<span class="input-label">From 5</span>
<input type="text" value="">
</label>
<label class="item item-input">
<span class="input-label">To</span>
<span class="input-label">To 5</span>
<input type="text" value="">
</label>
<label class="item item-input item-stacked-label">
<span class="input-label">Comment</span>
<textarea id="textarea"></textarea>
</label>
<label class="item item-input">
<span class="input-label">From</span>
<input type="text" value="">
</label>
<label class="item item-input">
<span class="input-label">To</span>
<input type="text" value="">
</label>
<label class="item item-input item-stacked-label">
<span class="input-label">Comment</span>
<span class="input-label">Comment 5</span>
<textarea id="textarea"></textarea>
</label>
@@ -215,6 +228,51 @@
}
});
var msgs = [];
var index = 0;
var timeId;
var consoleDebug = console.debug;
console.debug = function() {
index++;
var msg = [];
msg.push(index);
for (var i = 0, j = arguments.length; i < j; i++){
msg.push(arguments[i]);
}
msg = msg.join(', ');
if(arguments[0] === 'ERROR!') msg = '<span style="color:red;font-weight:bold">' + msg + '</span>';
if(arguments[0] === 'touchstart') msg = '<span style="color:pink">' + msg + '</span>';
if(arguments[0] === 'touchend') msg = '<span style="color:red">' + msg + '</span>';
if(arguments[0] === 'mousedown') msg = '<span style="color:lightblue">' + msg + '</span>';
if(arguments[0] === 'mouseup') msg = '<span style="color:blue">' + msg + '</span>';
if(arguments[0] === 'focusin') msg = '<span style="color:orange">' + msg + '</span>';
if(arguments[0] === 'focusout') msg = '<span style="color:orangered">' + msg + '</span>';
if(arguments[0] === 'click') msg = '<span style="color:green;font-weight:bold">' + msg + '</span>';
if(arguments[0] === 'clickPrevent') msg = '<span style="color:gray;">' + msg + '</span>';
msgs.unshift( msg );
if(msgs.length > 30) {
msgs.splice(40);
}
// do this so we try not to interfere with the device performance
clearTimeout(timeId);
timeId = setTimeout(function(){
document.getElementById('logs').innerHTML = msgs.join('<br>');
}, 150);
consoleDebug.apply(this, arguments);
}
</script>
</body>
</html>

View File

@@ -68,14 +68,14 @@
<ion-content class="has-header" padding="true">
<p>Swipe to the right to reveal the left menu.</p>
<div class="list">
<ion-list>
<div class="item item-input range range-positive">
<span class="input-label">Range</span>
5 km
<input type="range" min="5" max="700">
700 km
</div>
<ion-item ion-item="item" ng-href="#" ng-click="itemClick()" ng-repeat="item in range">
<ion-item item="item" ng-href="#" ng-click="itemClick()" ng-repeat="item in range">
<strong>{{$index}}</strong>
<b>:</b>
<em>
@@ -87,7 +87,7 @@
<span>supercalifragilisticexpialidocious<span>supercalifragilisticexpialidocious</span></span>
</em>
</ion-item>
</div>
</ion-list>
<p>(On desktop click and drag from left to right)</p>
</ion-content>

View File

File diff suppressed because it is too large Load Diff