Skip to content

Commit

Permalink
feat #1237 clickbuster implementation
Browse files Browse the repository at this point in the history
a new touch event has been created (safetap) in order to handle the tap gesture without any side effects due to the ghost click, generated by the browsers in mobile devices.
If a mousedown, click or touchend event is detected after a safetap on the same area, it is ignored since considered as a ghost click: preventDefault() and stopPropagation() are called.
In this way, if the DOM changes after the safetap event is handled, the browser's ghost click will not have effects.

reference:
http://ariatemplates.com/blog/2014/05/ghost-clicks-in-mobile-browsers/

close #1237
  • Loading branch information
lsimone committed Sep 2, 2014
1 parent 9481e9a commit e0e6442
Show file tree
Hide file tree
Showing 8 changed files with 233 additions and 86 deletions.
3 changes: 3 additions & 0 deletions src/aria/templates/MarkupWriter.js
Expand Up @@ -174,6 +174,9 @@ require("../utils/Type");
var eventWrapper = new ariaTemplatesDomEventWrapper(event), result = true;
var targetCallback = delegateMap[event.type];
if (targetCallback) {
if (event.type == "safetap") {
aria.touch.ClickBuster.registerTap(event);
}
result = targetCallback.call(eventWrapper);
}
eventWrapper.$dispose();
Expand Down
109 changes: 109 additions & 0 deletions src/aria/touch/BaseTap.js
@@ -0,0 +1,109 @@
/*
* Copyright 2012 Amadeus s.a.s.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var Aria = require("../Aria");
var ariaTouchGesture = require("./Gesture");


/**
* Contains delegated handler for a tap event
*/
module.exports = Aria.classDefinition({
$classpath : "aria.touch.BaseTap",
$extends : ariaTouchGesture,
$statics : {
/**
* The move tolerance to validate the gesture.
* @type Integer
*/
MARGIN : 10
},
$prototype : {
/**
* Initial listeners for the Tap gesture.
* @protected
*/
_getInitialListenersList : function () {
return [{
evt : this.touchEventMap.touchstart,
cb : {
fn : this._tapStart,
scope : this
}
}];
},

/**
* Additional listeners for the Tap gesture.
* @protected
*/
_getAdditionalListenersList : function () {
return [{
evt : this.touchEventMap.touchmove,
cb : {
fn : this._tapMove,
scope : this
}
}, {
evt : this.touchEventMap.touchend,
cb : {
fn : this._tapEnd,
scope : this
}
}];
},

/**
* Tap start mgmt: gesture is started if only one touch.
* @param {Object} event the original event
* @protected
* @return {Boolean} false if preventDefault is true
*/
_tapStart : function (event) {
var status = this._gestureStart(event);
return (status == null)
? ((event.returnValue != null) ? event.returnValue : !event.defaultPrevented)
: status;
},

/**
* Tap move mgmt: gesture continues if only one touch and if the move is within margins.
* @param {Object} event the original event
* @protected
* @return {Boolean} false if preventDefault is true
*/
_tapMove : function (event) {
var position = aria.touch.Event.getPositions(event);
if (this.MARGIN >= this._calculateDistance(this.startData.positions[0].x, this.startData.positions[0].y, position[0].x, position[0].y)) {
var status = this._gestureMove(event);
return (status == null) ? this._gestureCancel(event) : status;
} else {
return this._gestureCancel(event);
}
},

/**
* Tap end mgmt: gesture ends if only one touch.
* @param {Object} event the original event
* @protected
* @return {Boolean} false if preventDefault is true
*/
_tapEnd : function (event) {
var status = this._gestureEnd(event);
return (status == null) ? this._gestureCancel(event) : (event.returnValue != null)
? event.returnValue
: !event.defaultPrevented;
}
}
});
66 changes: 66 additions & 0 deletions src/aria/touch/ClickBuster.js
@@ -0,0 +1,66 @@
/*
* Copyright 2012 Amadeus s.a.s.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var Aria = require("../Aria");
var ariaTouchEvent = require("./Event");
var ariaCoreBrowser = require("../core/Browser");


/**
* Contains delegated handler for a tap event
*/
module.exports = Aria.classDefinition({
$singleton : true,
$classpath : "aria.touch.ClickBuster",
$statics : {
RADIO : 25,
DELAY : 500
},
$constructor : function () {
this.isDesktop = ariaCoreBrowser.DesktopView;
this.lastEvt = null;
},
$prototype : {
registerTap : function (event) {
this.lastEvt = {
pos : {
x : event.detail.currentX,
y : event.detail.currentY
},
date : new Date()
};
},
preventGhostClick : function (event) {
if (this._alreadyHandled(event)) {
event.preventDefault();
event.stopPropagation();
return false;
}
return true;
},

_alreadyHandled : function (e) {
var position = ariaTouchEvent.getPositions(e)[0];
return (this.lastEvt && this._isShortDelay(this.lastEvt.date, new Date()) && this._isSameArea(this.lastEvt.pos, position));
},

_isSameArea : function (pos1, pos2) {
return (Math.abs(pos1.x - pos2.x) < this.RADIO && Math.abs(pos1.y - pos2.y) < this.RADIO);
},

_isShortDelay : function (date1, date2) {
return (date2 - date1 <= this.DELAY);
}
}
});
4 changes: 4 additions & 0 deletions src/aria/touch/Event.js
Expand Up @@ -45,6 +45,10 @@ module.exports = Aria.classDefinition({
* @private
*/
__touchDetection : function () {
if(!Aria.$frameworkWindow){
this.touch = false;
return;
}
this.touch = (('ontouchstart' in Aria.$frameworkWindow) || Aria.$frameworkWindow.DocumentTouch
&& Aria.$frameworkWindow.document instanceof Aria.$frameworkWindow.DocumentTouch);
if (!this.touch) {
Expand Down
40 changes: 40 additions & 0 deletions src/aria/touch/SafeTap.js
@@ -0,0 +1,40 @@
/*
* Copyright 2012 Amadeus s.a.s.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var Aria = require("../Aria");
require("./ClickBuster");
var ariaTouchBaseTap = require("./BaseTap");


/**
* Contains delegated handler for a safetap event
*/
module.exports = Aria.classDefinition({
$singleton : true,
$classpath : "aria.touch.SafeTap",
$extends : ariaTouchBaseTap,
$prototype : {
/**
* The fake events raised during the safetap lifecycle.
* @protected
*/
_getFakeEventsMap : function () {
return {
start : "safetapstart",
end : "safetap",
cancel : "safetapcancel"
};
}
}
});
87 changes: 2 additions & 85 deletions src/aria/touch/Tap.js
Expand Up @@ -13,7 +13,7 @@
* limitations under the License.
*/
var Aria = require("../Aria");
var ariaTouchGesture = require("./Gesture");
var ariaTouchBaseTap = require("./BaseTap");


/**
Expand All @@ -22,49 +22,8 @@ var ariaTouchGesture = require("./Gesture");
module.exports = Aria.classDefinition({
$singleton : true,
$classpath : "aria.touch.Tap",
$extends : ariaTouchGesture,
$statics : {
/**
* The move tolerance to validate the gesture.
* @type Integer
*/
MARGIN : 10
},
$extends : ariaTouchBaseTap,
$prototype : {
/**
* Initial listeners for the Tap gesture.
* @protected
*/
_getInitialListenersList : function () {
return [{
evt : this.touchEventMap.touchstart,
cb : {
fn : this._tapStart,
scope : this
}
}];
},

/**
* Additional listeners for the Tap gesture.
* @protected
*/
_getAdditionalListenersList : function () {
return [{
evt : this.touchEventMap.touchmove,
cb : {
fn : this._tapMove,
scope : this
}
}, {
evt : this.touchEventMap.touchend,
cb : {
fn : this._tapEnd,
scope : this
}
}];
},

/**
* The fake events raised during the Tap lifecycle.
* @protected
Expand All @@ -75,48 +34,6 @@ module.exports = Aria.classDefinition({
end : "tap",
cancel : "tapcancel"
};
},

/**
* Tap start mgmt: gesture is started if only one touch.
* @param {Object} event the original event
* @protected
* @return {Boolean} false if preventDefault is true
*/
_tapStart : function (event) {
var status = this._gestureStart(event);
return (status == null)
? ((event.returnValue != null) ? event.returnValue : !event.defaultPrevented)
: status;
},

/**
* Tap move mgmt: gesture continues if only one touch and if the move is within margins.
* @param {Object} event the original event
* @protected
* @return {Boolean} false if preventDefault is true
*/
_tapMove : function (event) {
var position = aria.touch.Event.getPositions(event);
if (this.MARGIN >= this._calculateDistance(this.startData.positions[0].x, this.startData.positions[0].y, position[0].x, position[0].y)) {
var status = this._gestureMove(event);
return (status == null) ? this._gestureCancel(event) : status;
} else {
return this._gestureCancel(event);
}
},

/**
* Tap end mgmt: gesture ends if only one touch.
* @param {Object} event the original event
* @protected
* @return {Boolean} false if preventDefault is true
*/
_tapEnd : function (event) {
var status = this._gestureEnd(event);
return (status == null) ? this._gestureCancel(event) : (event.returnValue != null)
? event.returnValue
: !event.defaultPrevented;
}
}
});
3 changes: 3 additions & 0 deletions src/aria/utils/Delegate.js
Expand Up @@ -117,6 +117,9 @@ module.exports = Aria.classDefinition({
* @type Object
*/
this.delegatedGestures = {
"safetap" : "aria.touch.SafeTap",
"safetapstart" : "aria.touch.SafeTap",
"safetapcancel" : "aria.touch.SafeTap",
"tap" : "aria.touch.Tap",
"tapstart" : "aria.touch.Tap",
"tapcancel" : "aria.touch.Tap",
Expand Down
7 changes: 6 additions & 1 deletion src/aria/utils/Event.js
Expand Up @@ -270,7 +270,12 @@ var ariaCoreBrowser = require("../core/Browser");
var wrappedCallback;
if (event != "mousemove" || !ariaCoreBrowser.isWebkit) {
wrappedCallback = function (e) {
return handlerCBInstance.call(aria.utils.Event.getEvent(e, element));
// only if the event is safetap is used (so aria.touch.ClickBuster is loaded, not null),
// click, mousedown or touchend, it is analised in order to avoid ghost click's side effects
if ((e.type != "click" && e.type != "mousedown" && e.type != "touchend")
|| !aria.touch || !aria.touch.ClickBuster || aria.touch.ClickBuster.preventGhostClick(e)) {
return handlerCBInstance.call(aria.utils.Event.getEvent(e, element));
}
};
} else {

Expand Down

0 comments on commit e0e6442

Please sign in to comment.