mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
tap detection updates
This commit is contained in:
13
dist/css/ionic.css
vendored
13
dist/css/ionic.css
vendored
@@ -4993,7 +4993,8 @@ input[type="checkbox"][readonly] {
|
||||
|
||||
.item-radio input {
|
||||
/* hide any radio button inputs elements (the ugly circles) */
|
||||
display: none; }
|
||||
position: absolute;
|
||||
left: -9999px; }
|
||||
.item-radio input:checked ~ .item-content {
|
||||
/* style the item content when its checked */
|
||||
background: #f7f7f7; }
|
||||
@@ -5001,6 +5002,16 @@ input[type="checkbox"][readonly] {
|
||||
/* show the checkmark icon when its checked */
|
||||
visibility: visible; }
|
||||
|
||||
.item-radio {
|
||||
-webkit-animation: androidCheckedbugfix infinite 1s; }
|
||||
|
||||
@-webkit-keyframes androidCheckedbugfix {
|
||||
from {
|
||||
padding: 0; }
|
||||
|
||||
to {
|
||||
padding: 0; } }
|
||||
|
||||
/**
|
||||
* Range
|
||||
* --------------------------------------------------
|
||||
|
||||
12
dist/js/ionic-angular.js
vendored
12
dist/js/ionic-angular.js
vendored
@@ -1256,7 +1256,7 @@ angular.module('ionic.ui.checkbox', [])
|
||||
'<label class="checkbox">' +
|
||||
'<input type="checkbox" ng-model="ngModel" ng-value="ngValue" ng-change="ngChange()">' +
|
||||
'</label>' +
|
||||
'<div class="item-content" ng-transclude></div>' +
|
||||
'<div class="item-content disable-pointer-events" ng-transclude></div>' +
|
||||
'</div>',
|
||||
|
||||
compile: function(element, attr) {
|
||||
@@ -1674,13 +1674,13 @@ angular.module('ionic.ui.radio', [])
|
||||
template: '<label class="item item-radio">' +
|
||||
'<input type="radio" name="radio-group"' +
|
||||
' ng-model="ngModel" ng-value="ngValue" ng-change="ngChange()">' +
|
||||
'<div class="item-content" ng-transclude></div>' +
|
||||
'<i class="radio-icon icon ion-checkmark"></i>' +
|
||||
'<div class="item-content disable-pointer-events" ng-transclude></div>' +
|
||||
'<i class="radio-icon disable-pointer-events icon ion-checkmark"></i>' +
|
||||
'</label>',
|
||||
|
||||
compile: function(element, attr) {
|
||||
if(attr.name) element.find('input').attr('name', attr.name);
|
||||
if(attr.icon) element.find('i').removeClass('ion-checkmark').addClass(attr.icon);
|
||||
if(attr.name) element.children().eq(0).attr('name', attr.name);
|
||||
if(attr.icon) element.children().eq(2).removeClass('ion-checkmark').addClass(attr.icon);
|
||||
}
|
||||
};
|
||||
})
|
||||
@@ -2520,7 +2520,7 @@ angular.module('ionic.ui.toggle', [])
|
||||
},
|
||||
transclude: true,
|
||||
template: '<div class="item item-toggle">' +
|
||||
'<div ng-transclude></div>' +
|
||||
'<div class="disable-pointer-events" ng-transclude></div>' +
|
||||
'<label class="toggle">' +
|
||||
'<input type="checkbox" ng-model="ngModel" ng-value="ngValue" ng-change="ngChange()">' +
|
||||
'<div class="track">' +
|
||||
|
||||
178
dist/js/ionic.js
vendored
178
dist/js/ionic.js
vendored
@@ -259,37 +259,13 @@ window.ionic = {
|
||||
ionic.EventController = {
|
||||
VIRTUALIZED_EVENTS: ['tap', 'swipe', 'swiperight', 'swipeleft', 'drag', 'hold', 'release'],
|
||||
|
||||
isAndroidBrowser: (navigator.userAgent.indexOf('Android') > 0 && navigator.userAgent.indexOf('Chrome') < 0),
|
||||
|
||||
// Trigger a new event
|
||||
trigger: function(eventType, data) {
|
||||
var event = new CustomEvent(eventType, { detail: data });
|
||||
|
||||
// Make sure to trigger the event on the given target, or dispatch it from
|
||||
// the window if we don't have an event target
|
||||
if(data && data.target) {
|
||||
|
||||
// fire the event
|
||||
data.target.dispatchEvent(event) || window.dispatchEvent(event);
|
||||
|
||||
// fix for "click" firing twice on our Android friends
|
||||
if(ionic.EventController.isAndroidBrowser && eventType === 'click') {
|
||||
// Due to a bug, old Android browser fires both touchstart/touchend
|
||||
// and mousedown/mouseup. Because both are fired it results in
|
||||
// the "click" running twice on an element. Since this was a
|
||||
// triggered "click", which probably came from our "tap", then
|
||||
// set this element to be disabled for X milliseconds. While this
|
||||
// element is disabled, a second "click" by the browser would not
|
||||
// execute, hence the "click" only fires once from the initial "tap".
|
||||
var orgVal = data.target.disabled;
|
||||
data.target.disabled = true;
|
||||
|
||||
// After X milliseconds set the disabled value back to what it was
|
||||
setTimeout(function(){
|
||||
data.target.disabled = orgVal;
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
data && data.target && data.target.dispatchEvent(event) || window.dispatchEvent(event);
|
||||
},
|
||||
|
||||
// Bind an event
|
||||
@@ -1934,17 +1910,27 @@ window.ionic = {
|
||||
|
||||
// polyfill use to simulate native "tap"
|
||||
function inputTapPolyfill(ele, e) {
|
||||
if(ele.type === "radio") {
|
||||
if(!ele.checked) ele.checked = true;
|
||||
ionic.trigger('click', { target: ele });
|
||||
} else if(ele.type === "checkbox") {
|
||||
ele.checked = !ele.checked;
|
||||
ionic.trigger('click', { target: ele });
|
||||
} else if(ele.type === "submit" || ele.type === "button") {
|
||||
ionic.trigger('click', { target: ele });
|
||||
if(ele.tagName === 'A' ||
|
||||
ele.tagName === 'BUTTON' ||
|
||||
ele.type === 'checkbox' ||
|
||||
ele.type === 'radio' ||
|
||||
ele.type === 'button' ||
|
||||
ele.type === 'submit' ||
|
||||
ele.type === 'file') {
|
||||
// fire off this elements click method
|
||||
ele.click();
|
||||
} else {
|
||||
// if its not one of the elements above, just focus on this element
|
||||
ele.focus();
|
||||
}
|
||||
|
||||
// remember the coordinates of this tap so if it happens again we can ignore it
|
||||
recordTapCoordinates(e);
|
||||
|
||||
// set the last tap time so if a click event quickly happens it knows to ignore it
|
||||
ele.lastTap = Date.now();
|
||||
|
||||
// Stop! HOWEVER!! with touch devices there are still ghostclicks, hence the above logic
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
return false;
|
||||
@@ -1954,33 +1940,34 @@ window.ionic = {
|
||||
// if the source event wasn't from a touch event then don't use this polyfill
|
||||
if(!orgEvent.gesture || orgEvent.gesture.pointerType !== "touch" || !orgEvent.gesture.srcEvent) return;
|
||||
|
||||
// An internal Ionic indicator for angular directives that contain
|
||||
// elements that normally need poly behavior, but are already processed
|
||||
// (like the radio directive that has a radio button in it, but handles
|
||||
// the tap stuff itself). This is in contrast to preventDefault which will
|
||||
// mess up other operations like change events and such
|
||||
if(orgEvent.alreadyHandled) return;
|
||||
|
||||
var e = orgEvent.gesture.srcEvent; // evaluate the actual source event, not the created event by gestures.js
|
||||
var ele = e.target;
|
||||
|
||||
if( isRecentTap(e) ) {
|
||||
// if a tap in the same area just happened, don't continue
|
||||
return;
|
||||
}
|
||||
|
||||
if(e.target.lastClick && e.target.lastClick + CLICK_PREVENT_DURATION > Date.now()) {
|
||||
// if a click recently happend on this element, don't continue
|
||||
return;
|
||||
}
|
||||
|
||||
while(ele) {
|
||||
if( ele.tagName === "INPUT" || ele.tagName === "TEXTAREA" || ele.tagName === "SELECT" ) {
|
||||
orgEvent.alreadyHandled = true;
|
||||
// climb up the DOM looking to see the tapped is, or has a parent, of one of these
|
||||
if( ele.tagName === "INPUT" ||
|
||||
ele.tagName === "A" ||
|
||||
ele.tagName === "BUTTON" ||
|
||||
ele.tagName === "TEXTAREA" ||
|
||||
ele.tagName === "SELECT" ) {
|
||||
|
||||
return inputTapPolyfill(ele, e);
|
||||
|
||||
} else if( ele.tagName === "LABEL" ) {
|
||||
// check if the tapped label has an input associated to it
|
||||
if(ele.control) {
|
||||
orgEvent.alreadyHandled = true;
|
||||
return inputTapPolyfill(ele.control, e);
|
||||
}
|
||||
} else if( ele.tagName === "A" || ele.tagName === "BUTTON" ) {
|
||||
ionic.trigger('click', {
|
||||
target: ele
|
||||
});
|
||||
orgEvent.alreadyHandled = true;
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
ele = ele.parentElement;
|
||||
}
|
||||
@@ -1988,15 +1975,98 @@ window.ionic = {
|
||||
// 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
|
||||
var activeElement = document.activeElement;
|
||||
if(activeElement && (activeElement.tagName === "INPUT" || activeElement.tagName === "TEXTAREA" || activeElement.tagName === "SELECT")) {
|
||||
activeElement.blur();
|
||||
ele = document.activeElement;
|
||||
if(ele && (ele.tagName === "INPUT" ||
|
||||
ele.tagName === "TEXTAREA" ||
|
||||
ele.tagName === "SELECT")) {
|
||||
ele.blur();
|
||||
}
|
||||
}
|
||||
|
||||
function preventGhostClick(e) {
|
||||
if(e.target.tagName === "LABEL" && e.target.control) {
|
||||
// if this is a label and it has an associated input, then the input's click event will
|
||||
// propagate so don't bother letting this label's click propagate cuz it causes double clicks
|
||||
// However, do NOT preventDefault, because the label still needs to click the input
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if( isRecentTap(e) ) {
|
||||
// a tap has already happened at these coordinates recently, ignore this event
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
if(e.target.lastTap && e.target.lastTap + CLICK_PREVENT_DURATION > Date.now()) {
|
||||
// this element has already had the tap poly fill run on it recently, ignore this event
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
// remember the last time this element was clicked
|
||||
e.target.lastClick = Date.now();
|
||||
|
||||
// remember the coordinates of this click so if a tap or click in the
|
||||
// same area quickly happened again we can ignore it
|
||||
recordTapCoordinates(e);
|
||||
}
|
||||
|
||||
function isRecentTap(event) {
|
||||
// loop through the tap coordinates and see if the same area has been tapped recently
|
||||
var tapId, existingCoordinates, currentCoordinates,
|
||||
hitRadius = 15;
|
||||
|
||||
for(tapId in tapCoordinates) {
|
||||
existingCoordinates = tapCoordinates[tapId];
|
||||
if(!currentCoordinates) currentCoordinates = getCoordinates(event); // lazy load it when needed
|
||||
|
||||
if(currentCoordinates.x > existingCoordinates.x - hitRadius &&
|
||||
currentCoordinates.x < existingCoordinates.x + hitRadius &&
|
||||
currentCoordinates.y > existingCoordinates.y - hitRadius &&
|
||||
currentCoordinates.y < existingCoordinates.y + hitRadius) {
|
||||
// the current tap coordinates are in the same area as a recent tap
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function recordTapCoordinates(event) {
|
||||
var c = getCoordinates(event);
|
||||
if(c.x && c.y) {
|
||||
var tapId = 'ts' + Date.now();
|
||||
|
||||
// only record tap coordinates if we have valid ones
|
||||
tapCoordinates[tapId] = { x: c.x, y:c.y };
|
||||
|
||||
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
|
||||
delete tapCoordinates[tapId];
|
||||
}, CLICK_PREVENT_DURATION);
|
||||
}
|
||||
}
|
||||
|
||||
function getCoordinates(event) {
|
||||
// This method can get coordinates for both a mouse click
|
||||
// or a touch depending on the given event
|
||||
var touches = event.touches && event.touches.length ? event.touches : [event];
|
||||
var e = (event.changedTouches && event.changedTouches[0]) ||
|
||||
(event.originalEvent && event.originalEvent.changedTouches &&
|
||||
event.originalEvent.changedTouches[0]) ||
|
||||
touches[0].originalEvent || touches[0];
|
||||
|
||||
return { x: e.clientX, y: e.clientY }; // return the click or touch coordinates
|
||||
}
|
||||
|
||||
var tapCoordinates = {}; // used to remember coordinates to ignore if they happen again quickly
|
||||
var CLICK_PREVENT_DURATION = 400; // amount of milliseconds to check for ghostclicks
|
||||
|
||||
// set global click handler and check if the event should stop or not
|
||||
document.addEventListener('click', preventGhostClick, true);
|
||||
|
||||
// global tap event listener polyfill for HTML elements that were "tapped" by the user
|
||||
ionic.on("tap", tapPolyfill, document);
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ angular.module('ionic.ui.checkbox', [])
|
||||
'<label class="checkbox">' +
|
||||
'<input type="checkbox" ng-model="ngModel" ng-value="ngValue" ng-change="ngChange()">' +
|
||||
'</label>' +
|
||||
'<div class="item-content" ng-transclude></div>' +
|
||||
'<div class="item-content disable-pointer-events" ng-transclude></div>' +
|
||||
'</div>',
|
||||
|
||||
compile: function(element, attr) {
|
||||
|
||||
8
js/ext/angular/src/directive/ionicRadio.js
vendored
8
js/ext/angular/src/directive/ionicRadio.js
vendored
@@ -20,13 +20,13 @@ angular.module('ionic.ui.radio', [])
|
||||
template: '<label class="item item-radio">' +
|
||||
'<input type="radio" name="radio-group"' +
|
||||
' ng-model="ngModel" ng-value="ngValue" ng-change="ngChange()">' +
|
||||
'<div class="item-content" ng-transclude></div>' +
|
||||
'<i class="radio-icon icon ion-checkmark"></i>' +
|
||||
'<div class="item-content disable-pointer-events" ng-transclude></div>' +
|
||||
'<i class="radio-icon disable-pointer-events icon ion-checkmark"></i>' +
|
||||
'</label>',
|
||||
|
||||
compile: function(element, attr) {
|
||||
if(attr.name) element.find('input').attr('name', attr.name);
|
||||
if(attr.icon) element.find('i').removeClass('ion-checkmark').addClass(attr.icon);
|
||||
if(attr.name) element.children().eq(0).attr('name', attr.name);
|
||||
if(attr.icon) element.children().eq(2).removeClass('ion-checkmark').addClass(attr.icon);
|
||||
}
|
||||
};
|
||||
})
|
||||
|
||||
2
js/ext/angular/src/directive/ionicToggle.js
vendored
2
js/ext/angular/src/directive/ionicToggle.js
vendored
@@ -19,7 +19,7 @@ angular.module('ionic.ui.toggle', [])
|
||||
},
|
||||
transclude: true,
|
||||
template: '<div class="item item-toggle">' +
|
||||
'<div ng-transclude></div>' +
|
||||
'<div class="disable-pointer-events" ng-transclude></div>' +
|
||||
'<label class="toggle">' +
|
||||
'<input type="checkbox" ng-model="ngModel" ng-value="ngValue" ng-change="ngChange()">' +
|
||||
'<div class="track">' +
|
||||
|
||||
174
js/ext/angular/test/clickTests.html
Normal file
174
js/ext/angular/test/clickTests.html
Normal file
@@ -0,0 +1,174 @@
|
||||
<!DOCTYPE html>
|
||||
<html ng-app="ionicApp">
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
||||
<title>Click Tests</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">
|
||||
|
||||
<script src="../../../../dist/js/ionic.js"></script>
|
||||
<script src="../../../../dist/js/angular/angular.min.js"></script>
|
||||
<script src="../../../../dist/js/angular/angular-animate.min.js"></script>
|
||||
<script src="../../../../dist/js/angular/angular-sanitize.min.js"></script>
|
||||
<script src="../../../../dist/js/angular-ui/angular-ui-router.min.js"></script>
|
||||
<script src="../../../../dist/js/ionic-angular.js"></script>
|
||||
<style>
|
||||
td { padding: 2px 4px }
|
||||
input { margin: 10px !important; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body ng-controller="MyCtrl">
|
||||
<form id="form">
|
||||
<button class="button" ng-click="buttonClick()">Button</button>
|
||||
{{ buttonValue }} -
|
||||
{{ radioModel.data }}
|
||||
|
||||
<div class="list">
|
||||
|
||||
<radio ng-change="radioChange()"
|
||||
ng-click="radioClick(radioModel.data)"
|
||||
ng-model="radioModel.data"
|
||||
ng-value="'Radio A'">Radio A</radio>
|
||||
|
||||
<radio ng-change="radioChange()"
|
||||
ng-click="radioClick(radioModel.data)"
|
||||
ng-model="radioModel.data"
|
||||
ng-value="'Radio B'">Radio B</radio>
|
||||
|
||||
</div>
|
||||
|
||||
<label class="label1">
|
||||
<input type="radio" name="radio" id="radio1" class="radio1">
|
||||
</label>
|
||||
<label class="label2">
|
||||
<input type="radio" name="radio" id="radio2" class="radio2">
|
||||
</label>
|
||||
|
||||
<button id="clear">Clear</button>
|
||||
<button id="stop">Stop</button>
|
||||
|
||||
<div id="logs"></div>
|
||||
|
||||
|
||||
<script>
|
||||
angular.module('ionicApp', ['ionic'])
|
||||
|
||||
.controller('MyCtrl', function($scope) {
|
||||
|
||||
$scope.buttonClick = function() {
|
||||
console.log('button ng-click', 'click')
|
||||
$scope.buttonValue = Math.floor(Math.random() * 9999);
|
||||
};
|
||||
$scope.radioModel = {}
|
||||
$scope.radioChange = function() {
|
||||
console.log('radio ng-change', 'change', $scope.radioModel.data)
|
||||
};
|
||||
|
||||
$scope.radioClick = function(val) {
|
||||
console.log('radio ng-click', 'click', val)
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
function getTime() {
|
||||
var d = new Date();
|
||||
return d.getSeconds() + '.' + d.getMilliseconds();
|
||||
}
|
||||
|
||||
document.getElementById('radio1').addEventListener('change', function(){
|
||||
console.log('radio 1', 'change');
|
||||
});
|
||||
document.getElementById('radio1').addEventListener('click', function(){
|
||||
console.log('radio 1', 'click');
|
||||
});
|
||||
document.getElementById('radio2').addEventListener('change', function(){
|
||||
console.log('radio 2', 'change');
|
||||
});
|
||||
document.getElementById('radio2').addEventListener('click', function(){
|
||||
console.log('radio 2', 'click');
|
||||
});
|
||||
|
||||
document.addEventListener('touchstart', function(e){
|
||||
console.log('touchstart');
|
||||
});
|
||||
document.addEventListener('touchend', function(){
|
||||
console.log('touchend');
|
||||
});
|
||||
document.addEventListener('mousedown', function(){
|
||||
console.log('mousedown');
|
||||
});
|
||||
document.addEventListener('mouseup', function(){
|
||||
console.log('mouseup');
|
||||
});
|
||||
document.addEventListener('click', function(event){
|
||||
console.log('click', 'x: ' + event.clientX, 'y: ' + event.clientY);
|
||||
});
|
||||
document.getElementById('clear').addEventListener('click', function(){
|
||||
setTimeout(clearMsgs, 200);
|
||||
});
|
||||
document.getElementById('stop').addEventListener('click', function(){
|
||||
if(stopped) {
|
||||
this.innerText = 'Stop';
|
||||
window.stopped = false;
|
||||
} else {
|
||||
this.innerText = 'Start';
|
||||
window.stopped = true;
|
||||
}
|
||||
});
|
||||
|
||||
var msgs = [];
|
||||
var index = 0;
|
||||
window.stopped = false;
|
||||
console.log = function() {
|
||||
//return;
|
||||
if(stopped) return;
|
||||
|
||||
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] === 'touchstart') msg = '<span style="color:blue">' + msg + '</span>';
|
||||
if(arguments[0] === 'touchend') msg = '<span style="color:darkblue">' + msg + '</span>';
|
||||
|
||||
if(arguments[0] === 'mousedown') msg = '<span style="color:red">' + msg + '</span>';
|
||||
if(arguments[0] === 'mouseup') msg = '<span style="color:maroon">' + msg + '</span>';
|
||||
|
||||
if(arguments[0] === 'click') msg = '<span style="color:purple">' + msg + '</span>';
|
||||
|
||||
if(arguments[1] === 'click') msg = '<span style="color:green;font-weight:bold">' + msg + '</span>';
|
||||
if(arguments[1] === 'change') msg = '<span style="color:orange;font-weight:bold">' + msg + '</span>';
|
||||
|
||||
msgs.unshift( msg );
|
||||
|
||||
// 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);
|
||||
|
||||
}
|
||||
|
||||
var timeId;
|
||||
|
||||
function clearMsgs() {
|
||||
msgs = [];
|
||||
index = 0;
|
||||
document.getElementById('logs').innerHTML = '';
|
||||
document.getElementById('form').reset();
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
</form>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -38,37 +38,13 @@
|
||||
ionic.EventController = {
|
||||
VIRTUALIZED_EVENTS: ['tap', 'swipe', 'swiperight', 'swipeleft', 'drag', 'hold', 'release'],
|
||||
|
||||
isAndroidBrowser: (navigator.userAgent.indexOf('Android') > 0 && navigator.userAgent.indexOf('Chrome') < 0),
|
||||
|
||||
// Trigger a new event
|
||||
trigger: function(eventType, data) {
|
||||
var event = new CustomEvent(eventType, { detail: data });
|
||||
|
||||
// Make sure to trigger the event on the given target, or dispatch it from
|
||||
// the window if we don't have an event target
|
||||
if(data && data.target) {
|
||||
|
||||
// fire the event
|
||||
data.target.dispatchEvent(event) || window.dispatchEvent(event);
|
||||
|
||||
// fix for "click" firing twice on our Android friends
|
||||
if(ionic.EventController.isAndroidBrowser && eventType === 'click') {
|
||||
// Due to a bug, old Android browser fires both touchstart/touchend
|
||||
// and mousedown/mouseup. Because both are fired it results in
|
||||
// the "click" running twice on an element. Since this was a
|
||||
// triggered "click", which probably came from our "tap", then
|
||||
// set this element to be disabled for X milliseconds. While this
|
||||
// element is disabled, a second "click" by the browser would not
|
||||
// execute, hence the "click" only fires once from the initial "tap".
|
||||
var orgVal = data.target.disabled;
|
||||
data.target.disabled = true;
|
||||
|
||||
// After X milliseconds set the disabled value back to what it was
|
||||
setTimeout(function(){
|
||||
data.target.disabled = orgVal;
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
data && data.target && data.target.dispatchEvent(event) || window.dispatchEvent(event);
|
||||
},
|
||||
|
||||
// Bind an event
|
||||
|
||||
152
js/utils/poly.js
152
js/utils/poly.js
@@ -30,17 +30,27 @@
|
||||
|
||||
// polyfill use to simulate native "tap"
|
||||
function inputTapPolyfill(ele, e) {
|
||||
if(ele.type === "radio") {
|
||||
if(!ele.checked) ele.checked = true;
|
||||
ionic.trigger('click', { target: ele });
|
||||
} else if(ele.type === "checkbox") {
|
||||
ele.checked = !ele.checked;
|
||||
ionic.trigger('click', { target: ele });
|
||||
} else if(ele.type === "submit" || ele.type === "button") {
|
||||
ionic.trigger('click', { target: ele });
|
||||
if(ele.tagName === 'A' ||
|
||||
ele.tagName === 'BUTTON' ||
|
||||
ele.type === 'checkbox' ||
|
||||
ele.type === 'radio' ||
|
||||
ele.type === 'button' ||
|
||||
ele.type === 'submit' ||
|
||||
ele.type === 'file') {
|
||||
// fire off this elements click method
|
||||
ele.click();
|
||||
} else {
|
||||
// if its not one of the elements above, just focus on this element
|
||||
ele.focus();
|
||||
}
|
||||
|
||||
// remember the coordinates of this tap so if it happens again we can ignore it
|
||||
recordTapCoordinates(e);
|
||||
|
||||
// set the last tap time so if a click event quickly happens it knows to ignore it
|
||||
ele.lastTap = Date.now();
|
||||
|
||||
// Stop! HOWEVER!! with touch devices there are still ghostclicks, hence the above logic
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
return false;
|
||||
@@ -50,33 +60,34 @@
|
||||
// if the source event wasn't from a touch event then don't use this polyfill
|
||||
if(!orgEvent.gesture || orgEvent.gesture.pointerType !== "touch" || !orgEvent.gesture.srcEvent) return;
|
||||
|
||||
// An internal Ionic indicator for angular directives that contain
|
||||
// elements that normally need poly behavior, but are already processed
|
||||
// (like the radio directive that has a radio button in it, but handles
|
||||
// the tap stuff itself). This is in contrast to preventDefault which will
|
||||
// mess up other operations like change events and such
|
||||
if(orgEvent.alreadyHandled) return;
|
||||
|
||||
var e = orgEvent.gesture.srcEvent; // evaluate the actual source event, not the created event by gestures.js
|
||||
var ele = e.target;
|
||||
|
||||
if( isRecentTap(e) ) {
|
||||
// if a tap in the same area just happened, don't continue
|
||||
return;
|
||||
}
|
||||
|
||||
if(e.target.lastClick && e.target.lastClick + CLICK_PREVENT_DURATION > Date.now()) {
|
||||
// if a click recently happend on this element, don't continue
|
||||
return;
|
||||
}
|
||||
|
||||
while(ele) {
|
||||
if( ele.tagName === "INPUT" || ele.tagName === "TEXTAREA" || ele.tagName === "SELECT" ) {
|
||||
orgEvent.alreadyHandled = true;
|
||||
// climb up the DOM looking to see the tapped is, or has a parent, of one of these
|
||||
if( ele.tagName === "INPUT" ||
|
||||
ele.tagName === "A" ||
|
||||
ele.tagName === "BUTTON" ||
|
||||
ele.tagName === "TEXTAREA" ||
|
||||
ele.tagName === "SELECT" ) {
|
||||
|
||||
return inputTapPolyfill(ele, e);
|
||||
|
||||
} else if( ele.tagName === "LABEL" ) {
|
||||
// check if the tapped label has an input associated to it
|
||||
if(ele.control) {
|
||||
orgEvent.alreadyHandled = true;
|
||||
return inputTapPolyfill(ele.control, e);
|
||||
}
|
||||
} else if( ele.tagName === "A" || ele.tagName === "BUTTON" ) {
|
||||
ionic.trigger('click', {
|
||||
target: ele
|
||||
});
|
||||
orgEvent.alreadyHandled = true;
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
ele = ele.parentElement;
|
||||
}
|
||||
@@ -84,15 +95,98 @@
|
||||
// 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
|
||||
var activeElement = document.activeElement;
|
||||
if(activeElement && (activeElement.tagName === "INPUT" || activeElement.tagName === "TEXTAREA" || activeElement.tagName === "SELECT")) {
|
||||
activeElement.blur();
|
||||
ele = document.activeElement;
|
||||
if(ele && (ele.tagName === "INPUT" ||
|
||||
ele.tagName === "TEXTAREA" ||
|
||||
ele.tagName === "SELECT")) {
|
||||
ele.blur();
|
||||
}
|
||||
}
|
||||
|
||||
function preventGhostClick(e) {
|
||||
if(e.target.tagName === "LABEL" && e.target.control) {
|
||||
// if this is a label and it has an associated input, then the input's click event will
|
||||
// propagate so don't bother letting this label's click propagate cuz it causes double clicks
|
||||
// However, do NOT preventDefault, because the label still needs to click the input
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if( isRecentTap(e) ) {
|
||||
// a tap has already happened at these coordinates recently, ignore this event
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
if(e.target.lastTap && e.target.lastTap + CLICK_PREVENT_DURATION > Date.now()) {
|
||||
// this element has already had the tap poly fill run on it recently, ignore this event
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
// remember the last time this element was clicked
|
||||
e.target.lastClick = Date.now();
|
||||
|
||||
// remember the coordinates of this click so if a tap or click in the
|
||||
// same area quickly happened again we can ignore it
|
||||
recordTapCoordinates(e);
|
||||
}
|
||||
|
||||
function isRecentTap(event) {
|
||||
// loop through the tap coordinates and see if the same area has been tapped recently
|
||||
var tapId, existingCoordinates, currentCoordinates,
|
||||
hitRadius = 15;
|
||||
|
||||
for(tapId in tapCoordinates) {
|
||||
existingCoordinates = tapCoordinates[tapId];
|
||||
if(!currentCoordinates) currentCoordinates = getCoordinates(event); // lazy load it when needed
|
||||
|
||||
if(currentCoordinates.x > existingCoordinates.x - hitRadius &&
|
||||
currentCoordinates.x < existingCoordinates.x + hitRadius &&
|
||||
currentCoordinates.y > existingCoordinates.y - hitRadius &&
|
||||
currentCoordinates.y < existingCoordinates.y + hitRadius) {
|
||||
// the current tap coordinates are in the same area as a recent tap
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function recordTapCoordinates(event) {
|
||||
var c = getCoordinates(event);
|
||||
if(c.x && c.y) {
|
||||
var tapId = 'ts' + Date.now();
|
||||
|
||||
// only record tap coordinates if we have valid ones
|
||||
tapCoordinates[tapId] = { x: c.x, y:c.y };
|
||||
|
||||
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
|
||||
delete tapCoordinates[tapId];
|
||||
}, CLICK_PREVENT_DURATION);
|
||||
}
|
||||
}
|
||||
|
||||
function getCoordinates(event) {
|
||||
// This method can get coordinates for both a mouse click
|
||||
// or a touch depending on the given event
|
||||
var touches = event.touches && event.touches.length ? event.touches : [event];
|
||||
var e = (event.changedTouches && event.changedTouches[0]) ||
|
||||
(event.originalEvent && event.originalEvent.changedTouches &&
|
||||
event.originalEvent.changedTouches[0]) ||
|
||||
touches[0].originalEvent || touches[0];
|
||||
|
||||
return { x: e.clientX, y: e.clientY }; // return the click or touch coordinates
|
||||
}
|
||||
|
||||
var tapCoordinates = {}; // used to remember coordinates to ignore if they happen again quickly
|
||||
var CLICK_PREVENT_DURATION = 400; // amount of milliseconds to check for ghostclicks
|
||||
|
||||
// set global click handler and check if the event should stop or not
|
||||
document.addEventListener('click', preventGhostClick, true);
|
||||
|
||||
// global tap event listener polyfill for HTML elements that were "tapped" by the user
|
||||
ionic.on("tap", tapPolyfill, document);
|
||||
|
||||
|
||||
@@ -31,7 +31,8 @@
|
||||
|
||||
.item-radio input {
|
||||
/* hide any radio button inputs elements (the ugly circles) */
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
|
||||
&:checked ~ .item-content {
|
||||
/* style the item content when its checked */
|
||||
@@ -43,3 +44,13 @@
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
// Hack for Android to correctly display the checked item
|
||||
// http://timpietrusky.com/advanced-checkbox-hack
|
||||
.item-radio {
|
||||
-webkit-animation: androidCheckedbugfix infinite 1s;
|
||||
}
|
||||
@-webkit-keyframes androidCheckedbugfix {
|
||||
from { padding:0; }
|
||||
to { padding:0; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user