Reworking click and pinch handling to get better behavior on multi-touch devices. r=ahocevar +testing from others (closes #3133)

git-svn-id: http://svn.openlayers.org/trunk/openlayers@11695 dc9f47b5-9b13-0410-9fdd-eb0c1a62fdaf
This commit is contained in:
Tim Schaub
2011-03-11 01:53:26 +00:00
parent 3ce239208f
commit 5b7d530461
6 changed files with 488 additions and 184 deletions
+11 -11
View File
@@ -124,6 +124,7 @@ OpenLayers.Control.PinchZoom = OpenLayers.Class(OpenLayers.Control, {
*/ */
pinchStart: function(evt, pinchData) { pinchStart: function(evt, pinchData) {
this.pinchOrigin = evt.xy; this.pinchOrigin = evt.xy;
this.currentCenter = evt.xy;
}, },
/** /**
@@ -170,21 +171,20 @@ OpenLayers.Control.PinchZoom = OpenLayers.Class(OpenLayers.Control, {
* of the pinch gesture. This give us the final scale of the pinch. * of the pinch gesture. This give us the final scale of the pinch.
*/ */
pinchDone: function(evt, start, last) { pinchDone: function(evt, start, last) {
this.applyTransform("");
var zoom = this.map.getZoomForResolution(this.map.getResolution() / last.scale, true); var zoom = this.map.getZoomForResolution(this.map.getResolution() / last.scale, true);
var resolution = this.map.getResolutionForZoom(zoom); if (zoom !== this.map.getZoom() || !this.currentCenter.equals(this.pinchOrigin)) {
var resolution = this.map.getResolutionForZoom(zoom);
var location = this.map.getLonLatFromPixel(this.pinchOrigin); var location = this.map.getLonLatFromPixel(this.pinchOrigin);
var zoomPixel = this.currentCenter; var zoomPixel = this.currentCenter;
var size = this.map.getSize(); var size = this.map.getSize();
location.lon += resolution * ((size.w / 2) - zoomPixel.x);
location.lat -= resolution * ((size.h / 2) - zoomPixel.y);
this.map.setCenter(location, zoom); location.lon += resolution * ((size.w / 2) - zoomPixel.x);
location.lat -= resolution * ((size.h / 2) - zoomPixel.y);
var style = this.map.layerContainerDiv.style; this.map.setCenter(location, zoom);
style['-webkit-transform'] = ""; }
style['-moz-transform'] = "";
}, },
CLASS_NAME: "OpenLayers.Control.PinchZoom" CLASS_NAME: "OpenLayers.Control.PinchZoom"
+13 -6
View File
@@ -44,6 +44,12 @@ OpenLayers.Control.TouchNavigation = OpenLayers.Class(OpenLayers.Control, {
*/ */
pinchZoomOptions: null, pinchZoomOptions: null,
/**
* APIProperty: clickHandlerOptions
* {Object} Options passed to the Click handler.
*/
clickHandlerOptions: null,
/** /**
* APIProperty: documentDrag * APIProperty: documentDrag
* {Boolean} Allow panning of the map by dragging outside map viewport. * {Boolean} Allow panning of the map by dragging outside map viewport.
@@ -121,13 +127,14 @@ OpenLayers.Control.TouchNavigation = OpenLayers.Class(OpenLayers.Control, {
*/ */
draw: function() { draw: function() {
var clickCallbacks = { var clickCallbacks = {
'click': this.defaultClick, click: this.defaultClick,
'dblclick': this.defaultDblClick dblclick: this.defaultDblClick
};
var clickOptions = {
'double': true,
'stopDouble': true
}; };
var clickOptions = OpenLayers.Util.extend({
"double": true,
stopDouble: true,
pixelTolerance: 2
}, this.clickHandlerOptions);
this.handlers.click = new OpenLayers.Handler.Click( this.handlers.click = new OpenLayers.Handler.Click(
this, clickCallbacks, clickOptions this, clickCallbacks, clickOptions
); );
+205 -103
View File
@@ -51,13 +51,21 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, {
* constructed. * constructed.
*/ */
pixelTolerance: 0, pixelTolerance: 0,
/**
* APIProperty: dblclickTolerance
* {Number} Maximum distance in pixels between clicks for a sequence of
* events to be considered a double click. Default is 13. If the
* distance between two clicks is greater than this value, a double-
* click will not be fired.
*/
dblclickTolerance: 13,
/** /**
* APIProperty: stopSingle * APIProperty: stopSingle
* {Boolean} Stop other listeners from being notified of clicks. Default * {Boolean} Stop other listeners from being notified of clicks. Default
* is false. If true, any click listeners registered before this one * is false. If true, any listeners registered before this one for
* will not be notified of *any* click event (associated with double * click or rightclick events will not be notified.
* or single clicks).
*/ */
stopSingle: false, stopSingle: false,
@@ -82,6 +90,13 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, {
* {Number} The id of the timeout waiting to clear the <delayedCall>. * {Number} The id of the timeout waiting to clear the <delayedCall>.
*/ */
timerId: null, timerId: null,
/**
* Property: touch
* {Boolean} When a touchstart event is fired, touch will be true and all
* mouse related listeners will do nothing.
*/
touch: false,
/** /**
* Property: down * Property: down
@@ -92,15 +107,22 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, {
*/ */
down: null, down: null,
/** /**
* Property: last * Property: last
* {Object} Object that store relevant information about the last * {Object} Object that store relevant information about the last
* touchmove. Its 'xy' OpenLayers.Pixel property gives * mousemove or touchmove. Its 'xy' OpenLayers.Pixel property gives
* the average location of the mouse/touch event. Its 'touches' * the average location of the mouse/touch event. Its 'touches'
* property records clientX/clientY of each touches. * property records clientX/clientY of each touches.
*/ */
last: null, last: null,
/**
* Property: first
* {Object} When waiting for double clicks, this object will store
* information about the first click in a two click sequence.
*/
first: null,
/** /**
* Property: rightclickTimerId * Property: rightclickTimerId
* {Number} The id of the right mouse timeout waiting to clear the * {Number} The id of the right mouse timeout waiting to clear the
@@ -126,25 +148,8 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, {
*/ */
initialize: function(control, callbacks, options) { initialize: function(control, callbacks, options) {
OpenLayers.Handler.prototype.initialize.apply(this, arguments); OpenLayers.Handler.prototype.initialize.apply(this, arguments);
// optionally register for mousedown
if(this.pixelTolerance != null) {
this.mousedown = function(evt) {
this.down = this.getEventInfo(evt);
return true;
};
}
}, },
/**
* Method: mousedown
* Handle mousedown. Only registered as a listener if pixelTolerance is
* a non-zero value at construction.
*
* Returns:
* {Boolean} Continue propagating this event.
*/
mousedown: null,
/** /**
* Method: touchstart * Method: touchstart
* Handle touchstart. * Handle touchstart.
@@ -153,8 +158,67 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, {
* {Boolean} Continue propagating this event. * {Boolean} Continue propagating this event.
*/ */
touchstart: function(evt) { touchstart: function(evt) {
if (!this.touch) {
this.unregisterMouseListeners();
this.touch = true;
}
this.down = this.getEventInfo(evt); this.down = this.getEventInfo(evt);
this.last = null; this.last = this.getEventInfo(evt);
return true;
},
/**
* Method: touchmove
* Store position of last move, because touchend event can have
* an empty "touches" property.
*
* Returns:
* {Boolean} Continue propagating this event.
*/
touchmove: function(evt) {
this.last = this.getEventInfo(evt);
return true;
},
/**
* Method: touchend
* Correctly set event xy property, and add lastTouches to have
* touches property from last touchstart or touchmove
*/
touchend: function(evt) {
// touchstart may not have been allowed to propagate
if (this.down) {
evt.xy = this.last.xy;
evt.lastTouches = this.last.touches;
this.handleSingle(evt);
}
return true;
},
/**
* Method: unregisterMouseListeners
* In a touch environment, we don't want to handle mouse events.
*/
unregisterMouseListeners: function() {
this.map.events.un({
mousedown: this.mousedown,
mouseup: this.mouseup,
click: this.click,
dblclick: this.dblclick,
scope: this
});
},
/**
* Method: mousedown
* Handle mousedown.
*
* Returns:
* {Boolean} Continue propagating this event.
*/
mousedown: function(evt) {
this.down = this.getEventInfo(evt);
this.last = this.getEventInfo(evt);
return true; return true;
}, },
@@ -171,8 +235,7 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, {
// Collect right mouse clicks from the mouseup // Collect right mouse clicks from the mouseup
// IE - ignores the second right click in mousedown so using // IE - ignores the second right click in mousedown so using
// mouseup instead // mouseup instead
if(this.checkModifiers(evt) && if (this.checkModifiers(evt) && this.control.handleRightClicks &&
this.control.handleRightClicks &&
OpenLayers.Event.isRightClick(evt)) { OpenLayers.Event.isRightClick(evt)) {
propagate = this.rightclick(evt); propagate = this.rightclick(evt);
} }
@@ -194,7 +257,7 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, {
if(this.rightclickTimerId != null) { if(this.rightclickTimerId != null) {
//Second click received before timeout this must be //Second click received before timeout this must be
// a double click // a double click
this.clearTimer(); this.clearTimer();
this.callback('dblrightclick', [evt]); this.callback('dblrightclick', [evt]);
return !this.stopDouble; return !this.stopDouble;
} else { } else {
@@ -227,93 +290,93 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, {
if (evt) { if (evt) {
this.callback('rightclick', [evt]); this.callback('rightclick', [evt]);
} }
return !this.stopSingle;
}, },
/**
* Method: dblclick
* Handle dblclick. For a dblclick, we get two clicks in some browsers
* (FF) and one in others (IE). So we need to always register for
* dblclick to properly handle single clicks.
*
* Returns:
* {Boolean} Continue propagating this event.
*/
dblclick: function(evt) {
// for touch devices trigger dblclick only for
// "one finger" touch
var last = this.down || this.last;
if (this.passesTolerance(evt) &&
(!last || !last.touches || last.touches.length == 1)) {
if(this["double"]) {
this.callback('dblclick', [evt]);
}
this.clearTimer();
}
return !this.stopDouble;
},
/**
* Method: touchmove
* Store position of last move, because touchend event can have
* an empty "touches" property.
*/
touchmove: function(evt) {
this.last = this.getEventInfo(evt);
},
/**
* Method: touchend
* Correctly set event xy property, and add lastTouches to have
* touches property from last touchstart or touchmove
*/
touchend: function(evt) {
var last = this.last || this.down;
if (!evt || !last) {
return false;
}
evt.xy = last.xy;
evt.lastTouches = last.touches;
return evt.xy ? this.click(evt) : false;
},
/** /**
* Method: click * Method: click
* Handle click. * Handle click events from the browser. This is registered as a listener
* for click events and should not be called from other events in this
* handler.
* *
* Returns: * Returns:
* {Boolean} Continue propagating this event. * {Boolean} Continue propagating this event.
*/ */
click: function(evt) { click: function(evt) {
// Sencha Touch emulates click events, see ticket 3079 for more info if (!this.last) {
if (this.down && this.down.touches && evt.type === "click") { this.last = this.getEventInfo(evt);
return !this.stopSingle;
} }
if(this.passesTolerance(evt)) { this.handleSingle(evt);
if(this.timerId != null) { return !this.stopSingle;
},
/**
* Method: dblclick
* Handle dblclick. For a dblclick, we get two clicks in some browsers
* (FF) and one in others (IE). So we need to always register for
* dblclick to properly handle single clicks. This method is registered
* as a listener for the dblclick browser event. It should *not* be
* called by other methods in this handler.
*
* Returns:
* {Boolean} Continue propagating this event.
*/
dblclick: function(evt) {
this.handleDouble(evt);
return !this.stopDouble;
},
/**
* Method: handleDouble
* Handle double-click sequence.
*/
handleDouble: function(evt) {
if (this["double"] && this.passesDblclickTolerance(evt)) {
this.callback("dblclick", [evt]);
}
},
/**
* Method: handleSingle
* Handle single click sequence.
*/
handleSingle: function(evt) {
if (this.passesTolerance(evt)) {
if (this.timerId != null) {
// already received a click // already received a click
var last = this.down || this.last; if (this.last.touches && this.last.touches.length === 1) {
if (last && last.touches && last.touches.length > 0) { // touch device, no dblclick event - this may be a double
// touch device - we may trigger dblclick this.handleDouble(evt);
this.dblclick(evt); }
} else { // if we're not in a touch environment we clear the click timer
// if we've got a second touch, we'll get two touchend events
if (!this.last.touches || this.last.touches.length !== 2) {
this.clearTimer(); this.clearTimer();
} }
} else { } else {
// remember the first click info so we can compare to the second
this.first = this.getEventInfo(evt);
// set the timer, send evt only if single is true // set the timer, send evt only if single is true
//use a clone of the event object because it will no longer //use a clone of the event object because it will no longer
//be a valid event object in IE in the timer callback //be a valid event object in IE in the timer callback
var clickEvent = this.single ? var clickEvent = this.single ?
OpenLayers.Util.extend({}, evt) : null; OpenLayers.Util.extend({}, evt) : null;
this.timerId = window.setTimeout( this.queuePotentialClick(clickEvent);
OpenLayers.Function.bind(this.delayedCall, this, clickEvent),
this.delay
);
} }
} }
return !this.stopSingle;
}, },
/**
* Method: queuePotentialClick
* This method is separated out largely to make testing easier (so we
* don't have to override window.setTimeout)
*/
queuePotentialClick: function(evt) {
this.timerId = window.setTimeout(
OpenLayers.Function.bind(this.delayedCall, this, evt),
this.delay
);
},
/** /**
* Method: passesTolerance * Method: passesTolerance
* Determine whether the event is within the optional pixel tolerance. Note * Determine whether the event is within the optional pixel tolerance. Note
@@ -327,28 +390,67 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, {
*/ */
passesTolerance: function(evt) { passesTolerance: function(evt) {
var passes = true; var passes = true;
if(this.pixelTolerance != null && this.down && this.down.xy) { if (this.pixelTolerance != null && this.down && this.down.xy) {
var dpx = Math.sqrt( passes = this.pixelTolerance >= this.down.xy.distanceTo(evt.xy);
Math.pow(this.down.xy.x - evt.xy.x, 2) + // for touch environments, we also enforce that all touches
Math.pow(this.down.xy.y - evt.xy.y, 2) // start and end within the given tolerance to be considered a click
); if (passes && this.touch &&
if(dpx > this.pixelTolerance) { this.down.touches.length === this.last.touches.length) {
passes = false; // the touchend event doesn't come with touches, so we check
// down and last
for (var i=0, ii=this.down.touches.length; i<ii; ++i) {
if (this.getTouchDistance(
this.down.touches[i],
this.last.touches[i]
) > this.pixelTolerance) {
passes = false;
break;
}
}
} }
} }
return passes; return passes;
}, },
/**
* Method: getTouchDistance
*
* Returns:
* {Boolean} The pixel displacement between two touches.
*/
getTouchDistance: function(from, to) {
return Math.sqrt(
Math.pow(from.clientX - to.clientX, 2) +
Math.pow(from.clientY - to.clientY, 2)
);
},
/**
* Method: passesDblclickTolerance
* Determine whether the event is within the optional double-cick pixel
* tolerance.
*
* Returns:
* {Boolean} The click is within the double-click pixel tolerance.
*/
passesDblclickTolerance: function(evt) {
var passes = true;
if (this.down && this.first) {
passes = this.down.xy.distanceTo(this.first.xy) <= this.dblclickTolerance;
}
return passes;
},
/** /**
* Method: clearTimer * Method: clearTimer
* Clear the timer and set <timerId> to null. * Clear the timer and set <timerId> to null.
*/ */
clearTimer: function() { clearTimer: function() {
if(this.timerId != null) { if (this.timerId != null) {
window.clearTimeout(this.timerId); window.clearTimeout(this.timerId);
this.timerId = null; this.timerId = null;
} }
if(this.rightclickTimerId != null) { if (this.rightclickTimerId != null) {
window.clearTimeout(this.rightclickTimerId); window.clearTimeout(this.rightclickTimerId);
this.rightclickTimerId = null; this.rightclickTimerId = null;
} }
@@ -361,8 +463,8 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, {
*/ */
delayedCall: function(evt) { delayedCall: function(evt) {
this.timerId = null; this.timerId = null;
if(evt) { if (evt) {
this.callback('click', [evt]); this.callback("click", [evt]);
} }
}, },
@@ -407,7 +509,7 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, {
if(OpenLayers.Handler.prototype.deactivate.apply(this, arguments)) { if(OpenLayers.Handler.prototype.deactivate.apply(this, arguments)) {
this.clearTimer(); this.clearTimer();
this.down = null; this.down = null;
this.last = null; this.first = null;
deactivated = true; deactivated = true;
} }
return deactivated; return deactivated;
+7 -8
View File
@@ -37,9 +37,9 @@ OpenLayers.Handler.Pinch = OpenLayers.Class(OpenLayers.Handler, {
/** /**
* Property: stopDown * Property: stopDown
* {Boolean} Stop propagation of touchstart events from getting to * {Boolean} Stop propagation of touchstart events from getting to
* listeners on the same element. Default is true. * listeners on the same element. Default is false.
*/ */
stopDown: true, stopDown: false,
/** /**
* Property: pinching * Property: pinching
@@ -105,6 +105,7 @@ OpenLayers.Handler.Pinch = OpenLayers.Class(OpenLayers.Handler, {
this.start = null; this.start = null;
this.last = null; this.last = null;
} }
// prevent document dragging
OpenLayers.Event.stop(evt); OpenLayers.Event.stop(evt);
return propagate; return propagate;
}, },
@@ -120,15 +121,15 @@ OpenLayers.Handler.Pinch = OpenLayers.Class(OpenLayers.Handler, {
* {Boolean} Let the event propagate. * {Boolean} Let the event propagate.
*/ */
touchmove: function(evt) { touchmove: function(evt) {
var propagate = true;
if (this.started && OpenLayers.Event.isMultiTouch(evt)) { if (this.started && OpenLayers.Event.isMultiTouch(evt)) {
this.pinching = true; this.pinching = true;
var current = this.getPinchData(evt); var current = this.getPinchData(evt);
this.callback("move", [evt, current]); this.callback("move", [evt, current]);
this.last = current; this.last = current;
propagate = false; // prevent document dragging
OpenLayers.Event.stop(evt);
} }
return propagate; return true;
}, },
/** /**
@@ -142,16 +143,14 @@ OpenLayers.Handler.Pinch = OpenLayers.Class(OpenLayers.Handler, {
* {Boolean} Let the event propagate. * {Boolean} Let the event propagate.
*/ */
touchend: function(evt) { touchend: function(evt) {
var propagate = true;
if (this.started) { if (this.started) {
this.started = false; this.started = false;
this.pinching = false; this.pinching = false;
this.callback("done", [evt, this.start, this.last]); this.callback("done", [evt, this.start, this.last]);
this.start = null; this.start = null;
this.last = null; this.last = null;
propagate = false;
} }
return propagate; return true;
}, },
/** /**
+24 -1
View File
@@ -102,6 +102,29 @@
} }
function test_clickHandlerOptions(t) {
t.plan(3);
var nav = new OpenLayers.Control.TouchNavigation();
t.eq(nav.clickHandlerOptions, null, "clickHandlerOptions null by default");
var map = new OpenLayers.Map({
div: document.body,
controls: [
new OpenLayers.Control.TouchNavigation({
clickHandlerOptions: {foo: "bar"}
})
]
});
nav = map.controls[0];
t.eq(nav.handlers.click.foo, "bar", "foo property is set on the click handler");
t.eq(nav.handlers.click.pixelTolerance, 2, "pixelTolerance is 2 by default");
map.destroy();
}
function test_zoomOut(t) { function test_zoomOut(t) {
t.plan(1); t.plan(1);
@@ -114,7 +137,7 @@
var control = new OpenLayers.Control.TouchNavigation(); var control = new OpenLayers.Control.TouchNavigation();
map.addControl(control); map.addControl(control);
var handler = control.handlers.click; var handler = control.handlers.click;
handler.touchstart({xy: {x: 1, y: 1}, touches: ["foo", "bar"]}); handler.touchstart({xy: new OpenLayers.Pixel(1 ,1), touches: ["foo", "bar"]});
handler.touchend({}); handler.touchend({});
t.delay_call(1, function() { t.delay_call(1, function() {
t.eq(map.getZoom(), 4, "Did we zoom out?"); t.eq(map.getZoom(), 4, "Did we zoom out?");
+228 -55
View File
@@ -102,11 +102,15 @@
var callbackMap; var callbackMap;
function callbackSetup(log, options) { function callbackSetup(log, options) {
callbackMap = new OpenLayers.Map('map', {controls: []}); callbackMap = new OpenLayers.Map({
div: "map",
var control = { controls: [], // no controls here because these tests use a custom setTimeout and we only want setTimeout calls from a single handler
map: callbackMap layers: [new OpenLayers.Layer(null, {isBaseLayer: true})],
}; center: new OpenLayers.LonLat(0, 0),
zoom: 1
});
var control = new OpenLayers.Control();
callbackMap.addControl(control);
var callbacks = { var callbacks = {
"click": function(evt) { "click": function(evt) {
@@ -119,6 +123,7 @@
var handler = new OpenLayers.Handler.Click(control, callbacks, options); var handler = new OpenLayers.Handler.Click(control, callbacks, options);
handler.activate(); handler.activate();
var timers = {}; var timers = {};
window._setTimeout = window.setTimeout; window._setTimeout = window.setTimeout;
window.setTimeout = function(func, delay) { window.setTimeout = function(func, delay) {
@@ -270,13 +275,140 @@
handler.map.events.triggerEvent("mouseup", up); handler.map.events.triggerEvent("mouseup", up);
handler.map.events.triggerEvent("click", up); handler.map.events.triggerEvent("click", up);
t.eq(log.length, 0, "nothing logged - event outside tolerance"); t.eq(log.length, 0, "nothing logged - event outside tolerance");
callbackTeardown(); callbackTeardown();
} }
function test_callbacks_within_dblclickTolerance(t) {
t.plan(6);
var log = [];
var handler = callbackSetup(log, {single: false, "double": true, dblclickTolerance: 8});
var first = {
xy: px(0, 0)
};
var second = {
xy: px(0, 5)
};
handler.map.events.triggerEvent("mousedown", first);
handler.map.events.triggerEvent("mouseup", first);
handler.map.events.triggerEvent("click", first);
t.eq(log.length, 1, "one item logged");
t.eq(log[0] && log[0].method, "setTimeout", "setTimeout called");
handler.map.events.triggerEvent("mousedown", second);
handler.map.events.triggerEvent("mouseup", second);
handler.map.events.triggerEvent("click", second);
t.eq(log.length, 2, "two events logged");
t.eq(log[1] && log[1].method, "clearTimeout", "clearTimeout called");
handler.map.events.triggerEvent("dblclick", second);
t.eq(log.length, 3, "three items logged");
t.eq(log[2] && log[2].callback, "dblclick", "dblclick callback called");
callbackTeardown();
}
function test_callbacks_outside_dblclickTolerance(t) {
t.plan(5);
var log = [];
// default dblclickTolerance is 13
var handler = callbackSetup(log, {single: false, "double": true});
var first = {
xy: px(0, 0)
};
var second = {
xy: px(13.5, 0)
};
handler.map.events.triggerEvent("mousedown", first);
handler.map.events.triggerEvent("mouseup", first);
handler.map.events.triggerEvent("click", first);
t.eq(log.length, 1, "one item logged");
t.eq(log[0] && log[0].method, "setTimeout", "setTimeout called");
handler.map.events.triggerEvent("mousedown", second);
handler.map.events.triggerEvent("mouseup", second);
handler.map.events.triggerEvent("click", second);
t.eq(log.length, 2, "two items logged");
t.eq(log[1] && log[1].method, "clearTimeout", "clearTimeout called");
handler.map.events.triggerEvent("dblclick", second);
t.eq(log.length, 2, "still two items logged - dblclick callback is not called");
callbackTeardown();
}
function test_callbacks_multitouch_single(t) {
t.plan(2);
var log = [];
var callbacks = {
click: function(evt) {
log.push({callback: "click", type: evt.type});
},
dblclick: function(evt) {
log.push({callback: "dblclick", type: evt.type});
}
};
var map = new OpenLayers.Map("map");
var layer = new OpenLayers.Layer(null, {isBaseLayer: true});
map.addLayer(layer);
map.setCenter(new OpenLayers.LonLat(0, 0), 1);
var control = new OpenLayers.Control();
map.addControl(control);
var handler = new OpenLayers.Handler.Click(
control, callbacks,
{"double": true, single: true, pixelTolerance: 2}
);
// we override here so we don't have to wait for the timeout
handler.queuePotentialClick = function(evt) {
log.push({potential: true, evt: evt});
OpenLayers.Handler.Click.prototype.queuePotentialClick.call(this, evt);
}
handler.activate();
function handle(o) {
var touches = [];
if (("x0" in o) && ("y0" in o)) {
touches.push({
clientX: o.x0, clientY: o.y0
});
}
if (("x1" in o) && ("y1" in o)) {
touches.push({
clientX: o.x1, clientY: o.y1
});
}
handler.map.events.handleBrowserEvent({
type: o.type, touches: touches
});
}
// a typical multitouch sequence goes like this:
// touchstart, touchstart, touchend, touchend
handle({type: "touchstart", x0: 10, y0: 10});
handle({type: "touchstart", x0: 10, y0: 10, x1: 30, y1: 15});
handle({type: "touchend"});
handle({type: "touchend"});
t.eq(log.length, 1, "one item logged");
t.eq(log[0] && log[0].potential, true, "click in queue - no dblclick called");
map.destroy();
}
function test_Handler_Click_deactivate(t) { function test_Handler_Click_deactivate(t) {
t.plan(4); t.plan(4);
var control = { var control = {
@@ -395,83 +527,124 @@
}); });
} }
function test_touch_ignoresimulatedclick(t) { function test_touch_within_dblclickTolerance(t) {
t.plan(2); t.plan(4);
// set up
var log; var log;
var map = new OpenLayers.Map('map');
var control = {map: map};
var callbacks = { var callbacks = {
'dblclick': function(e) { click: function(evt) {
log.dblclick = {x: e.xy.x, y: e.xy.y, log.push({callback: "click", type: evt.type});
lastTouches: e.lastTouches}; },
dblclick: function(evt) {
log.push({callback: "dblclick", type: evt.type});
} }
}; };
var map = new OpenLayers.Map("map");
var layer = new OpenLayers.Layer(null, {isBaseLayer: true});
map.addLayer(layer);
map.setCenter(new OpenLayers.LonLat(0, 0), 1);
var control = new OpenLayers.Control();
map.addControl(control);
var handler = new OpenLayers.Handler.Click( var handler = new OpenLayers.Handler.Click(
control, callbacks, control, callbacks,
{'double': true, pixelTolerance: null}); {"double": true, single: true, pixelTolerance: 2}
);
handler.activate();
function handle(type, x, y) {
map.events.handleBrowserEvent({
type: type,
touches: [
{clientX: x, clientY: y}
]
});
}
// test // test
log = [];
// sequence of two clicks on a touch device
// click 1
handle("touchstart", 10, 10);
handle("touchend", 11, 10);
handle("mousemove", 11, 10);
handle("mousedown", 10, 10);
handle("mouseup", 11, 10);
handle("click", 11, 10);
// click 2
handle("touchstart", 12, 10);
handle("touchend", 12, 10);
handle("mousedown", 12, 10);
handle("mouseup", 12, 10);
handle("click", 12, 10);
log = {}; t.eq(log.length, 1, "one callback called");
handler.touchstart({xy: px(1, 1), touches: ["foo"]}); t.eq(log[0] && log[0].callback, "dblclick", "click callback called");
handler.touchend({}); t.eq(log[0] && log[0].type, "touchend", "click callback called with touchend event");
handler.touchstart({xy: px(1, 1), touches: ["foo"]}); t.ok(!handler.timerId, "handler doesn't have a timerId waiting for click")
handler.touchend({type: "click"});
t.eq(!!handler.down.touches, true, "Handler down touches property should be truthy");
t.ok(log.dblclick == undefined, "dblclick callback not called with simulated click");
// tear down // tear down
map.destroy(); map.destroy();
} }
function test_touch_dblclick(t) { function test_touch_outside_dblclickTolerance(t) {
t.plan(5); t.plan(2);
// set up
var log; var log;
var map = new OpenLayers.Map('map');
var control = {map: map};
var callbacks = { var callbacks = {
'click': function(e) { click: function(evt) {
log.click = {x: e.xy.x, y: e.xy.y, log.push({callback: "click", type: evt.type});
lastTouches: e.lastTouches};
}, },
'dblclick': function(e) { dblclick: function(evt) {
log.dblclick = {x: e.xy.x, y: e.xy.y, log.push({callback: "dblclick", type: evt.type});
lastTouches: e.lastTouches};
} }
}; };
var map = new OpenLayers.Map("map");
var layer = new OpenLayers.Layer(null, {isBaseLayer: true});
map.addLayer(layer);
map.setCenter(new OpenLayers.LonLat(0, 0), 1);
var control = new OpenLayers.Control();
map.addControl(control);
var handler = new OpenLayers.Handler.Click( var handler = new OpenLayers.Handler.Click(
control, callbacks, control, callbacks,
{'double': true, pixelTolerance: null}); {"double": true, single: true, pixelTolerance: 2, dblclickTolerance: 8}
);
handler.activate();
function handle(type, x, y) {
var touches = [];
if (x !== undefined && y !== undefined) {
touches.push({
clientX: x, clientY: y
});
}
map.events.handleBrowserEvent({
type: type, touches: touches
});
}
// test // test
log = [];
// sequence of two clicks on a touch device
// click 1
handle("touchstart", 10, 10);
handle("touchend");
handle("mousemove", 11, 10);
handle("mousedown", 10, 10);
handle("mouseup", 11, 10);
handle("click", 11, 10);
// click 2
handle("touchstart", 20, 10);
handle("touchend");
handle("mousedown", 20, 10);
handle("mouseup", 20, 10);
handle("click", 20, 10);
log = {}; t.eq(log.length, 0, "no callbacks called");
handler.touchstart({xy: px(1, 1), touches: [{clientX:0, clientY:10}]}); t.ok(!handler.timerId, "handler doesn't have a timerId waiting for click")
handler.touchend({});
handler.touchstart({xy: px(1, 1), touches: [{clientX:0, clientY:10}]});
handler.touchend({});
t.eq(log.click, undefined, "click callback not called");
t.ok(log.dblclick != undefined, "dblclick callback called");
if(log.dblclick != undefined) {
t.eq(log.dblclick.x, 1, "evt.xy.x as expected");
t.eq(log.dblclick.y, 1, "evt.xy.y as expected");
t.ok(log.dblclick.lastTouches, "evt.lastTouches on evt");
}
// tear down // tear down
map.destroy(); map.destroy();