From 5b7d530461ca3d089c059445466f455effa24a4e Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Fri, 11 Mar 2011 01:53:26 +0000 Subject: [PATCH] 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 --- lib/OpenLayers/Control/PinchZoom.js | 22 +- lib/OpenLayers/Control/TouchNavigation.js | 19 +- lib/OpenLayers/Handler/Click.js | 308 ++++++++++++++-------- lib/OpenLayers/Handler/Pinch.js | 15 +- tests/Control/TouchNavigation.html | 25 +- tests/Handler/Click.html | 283 ++++++++++++++++---- 6 files changed, 488 insertions(+), 184 deletions(-) diff --git a/lib/OpenLayers/Control/PinchZoom.js b/lib/OpenLayers/Control/PinchZoom.js index bd8896e37a..ff744baac9 100644 --- a/lib/OpenLayers/Control/PinchZoom.js +++ b/lib/OpenLayers/Control/PinchZoom.js @@ -124,6 +124,7 @@ OpenLayers.Control.PinchZoom = OpenLayers.Class(OpenLayers.Control, { */ pinchStart: function(evt, pinchData) { 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. */ pinchDone: function(evt, start, last) { + this.applyTransform(""); 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 zoomPixel = this.currentCenter; - var size = this.map.getSize(); - - location.lon += resolution * ((size.w / 2) - zoomPixel.x); - location.lat -= resolution * ((size.h / 2) - zoomPixel.y); + var location = this.map.getLonLatFromPixel(this.pinchOrigin); + var zoomPixel = this.currentCenter; + var size = this.map.getSize(); - 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; - style['-webkit-transform'] = ""; - style['-moz-transform'] = ""; + this.map.setCenter(location, zoom); + } }, CLASS_NAME: "OpenLayers.Control.PinchZoom" diff --git a/lib/OpenLayers/Control/TouchNavigation.js b/lib/OpenLayers/Control/TouchNavigation.js index dfa28a2823..35f0e20d94 100644 --- a/lib/OpenLayers/Control/TouchNavigation.js +++ b/lib/OpenLayers/Control/TouchNavigation.js @@ -44,6 +44,12 @@ OpenLayers.Control.TouchNavigation = OpenLayers.Class(OpenLayers.Control, { */ pinchZoomOptions: null, + /** + * APIProperty: clickHandlerOptions + * {Object} Options passed to the Click handler. + */ + clickHandlerOptions: null, + /** * APIProperty: documentDrag * {Boolean} Allow panning of the map by dragging outside map viewport. @@ -121,13 +127,14 @@ OpenLayers.Control.TouchNavigation = OpenLayers.Class(OpenLayers.Control, { */ draw: function() { var clickCallbacks = { - 'click': this.defaultClick, - 'dblclick': this.defaultDblClick - }; - var clickOptions = { - 'double': true, - 'stopDouble': true + click: this.defaultClick, + dblclick: this.defaultDblClick }; + var clickOptions = OpenLayers.Util.extend({ + "double": true, + stopDouble: true, + pixelTolerance: 2 + }, this.clickHandlerOptions); this.handlers.click = new OpenLayers.Handler.Click( this, clickCallbacks, clickOptions ); diff --git a/lib/OpenLayers/Handler/Click.js b/lib/OpenLayers/Handler/Click.js index 76870c5f28..a2b37e0e5b 100644 --- a/lib/OpenLayers/Handler/Click.js +++ b/lib/OpenLayers/Handler/Click.js @@ -51,13 +51,21 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, { * constructed. */ 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 * {Boolean} Stop other listeners from being notified of clicks. Default - * is false. If true, any click listeners registered before this one - * will not be notified of *any* click event (associated with double - * or single clicks). + * is false. If true, any listeners registered before this one for + * click or rightclick events will not be notified. */ stopSingle: false, @@ -82,6 +90,13 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, { * {Number} The id of the timeout waiting to clear the . */ 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 @@ -92,15 +107,22 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, { */ down: null, - /** + /** * Property: 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' * property records clientX/clientY of each touches. */ 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 * {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) { 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 * Handle touchstart. @@ -153,8 +158,67 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, { * {Boolean} Continue propagating this event. */ touchstart: function(evt) { + if (!this.touch) { + this.unregisterMouseListeners(); + this.touch = true; + } 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; }, @@ -171,8 +235,7 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, { // Collect right mouse clicks from the mouseup // IE - ignores the second right click in mousedown so using // mouseup instead - if(this.checkModifiers(evt) && - this.control.handleRightClicks && + if (this.checkModifiers(evt) && this.control.handleRightClicks && OpenLayers.Event.isRightClick(evt)) { propagate = this.rightclick(evt); } @@ -194,7 +257,7 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, { if(this.rightclickTimerId != null) { //Second click received before timeout this must be // a double click - this.clearTimer(); + this.clearTimer(); this.callback('dblrightclick', [evt]); return !this.stopDouble; } else { @@ -227,93 +290,93 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, { if (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 - * 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: * {Boolean} Continue propagating this event. */ click: function(evt) { - // Sencha Touch emulates click events, see ticket 3079 for more info - if (this.down && this.down.touches && evt.type === "click") { - return !this.stopSingle; + if (!this.last) { + this.last = this.getEventInfo(evt); } - if(this.passesTolerance(evt)) { - if(this.timerId != null) { + this.handleSingle(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. 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 - var last = this.down || this.last; - if (last && last.touches && last.touches.length > 0) { - // touch device - we may trigger dblclick - this.dblclick(evt); - } else { + if (this.last.touches && this.last.touches.length === 1) { + // touch device, no dblclick event - this may be a double + this.handleDouble(evt); + } + // 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(); } } 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 //use a clone of the event object because it will no longer //be a valid event object in IE in the timer callback var clickEvent = this.single ? OpenLayers.Util.extend({}, evt) : null; - this.timerId = window.setTimeout( - OpenLayers.Function.bind(this.delayedCall, this, clickEvent), - this.delay - ); + this.queuePotentialClick(clickEvent); } } - 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 * 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) { var passes = true; - if(this.pixelTolerance != null && this.down && this.down.xy) { - var dpx = Math.sqrt( - Math.pow(this.down.xy.x - evt.xy.x, 2) + - Math.pow(this.down.xy.y - evt.xy.y, 2) - ); - if(dpx > this.pixelTolerance) { - passes = false; + if (this.pixelTolerance != null && this.down && this.down.xy) { + passes = this.pixelTolerance >= this.down.xy.distanceTo(evt.xy); + // for touch environments, we also enforce that all touches + // start and end within the given tolerance to be considered a click + if (passes && this.touch && + this.down.touches.length === this.last.touches.length) { + // the touchend event doesn't come with touches, so we check + // down and last + for (var i=0, ii=this.down.touches.length; i this.pixelTolerance) { + passes = false; + break; + } + } } } 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 * Clear the timer and set to null. */ clearTimer: function() { - if(this.timerId != null) { + if (this.timerId != null) { window.clearTimeout(this.timerId); this.timerId = null; } - if(this.rightclickTimerId != null) { + if (this.rightclickTimerId != null) { window.clearTimeout(this.rightclickTimerId); this.rightclickTimerId = null; } @@ -361,8 +463,8 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, { */ delayedCall: function(evt) { this.timerId = null; - if(evt) { - this.callback('click', [evt]); + if (evt) { + this.callback("click", [evt]); } }, @@ -407,7 +509,7 @@ OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, { if(OpenLayers.Handler.prototype.deactivate.apply(this, arguments)) { this.clearTimer(); this.down = null; - this.last = null; + this.first = null; deactivated = true; } return deactivated; diff --git a/lib/OpenLayers/Handler/Pinch.js b/lib/OpenLayers/Handler/Pinch.js index c5c61a489d..cac1dc376e 100644 --- a/lib/OpenLayers/Handler/Pinch.js +++ b/lib/OpenLayers/Handler/Pinch.js @@ -37,9 +37,9 @@ OpenLayers.Handler.Pinch = OpenLayers.Class(OpenLayers.Handler, { /** * Property: stopDown * {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 @@ -105,6 +105,7 @@ OpenLayers.Handler.Pinch = OpenLayers.Class(OpenLayers.Handler, { this.start = null; this.last = null; } + // prevent document dragging OpenLayers.Event.stop(evt); return propagate; }, @@ -120,15 +121,15 @@ OpenLayers.Handler.Pinch = OpenLayers.Class(OpenLayers.Handler, { * {Boolean} Let the event propagate. */ touchmove: function(evt) { - var propagate = true; if (this.started && OpenLayers.Event.isMultiTouch(evt)) { this.pinching = true; var current = this.getPinchData(evt); this.callback("move", [evt, 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. */ touchend: function(evt) { - var propagate = true; if (this.started) { this.started = false; this.pinching = false; this.callback("done", [evt, this.start, this.last]); this.start = null; this.last = null; - propagate = false; } - return propagate; + return true; }, /** diff --git a/tests/Control/TouchNavigation.html b/tests/Control/TouchNavigation.html index 7a9d67a59b..21241dd502 100644 --- a/tests/Control/TouchNavigation.html +++ b/tests/Control/TouchNavigation.html @@ -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) { t.plan(1); @@ -114,7 +137,7 @@ var control = new OpenLayers.Control.TouchNavigation(); map.addControl(control); 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({}); t.delay_call(1, function() { t.eq(map.getZoom(), 4, "Did we zoom out?"); diff --git a/tests/Handler/Click.html b/tests/Handler/Click.html index 2598c9ef0d..43ab498463 100644 --- a/tests/Handler/Click.html +++ b/tests/Handler/Click.html @@ -102,11 +102,15 @@ var callbackMap; function callbackSetup(log, options) { - callbackMap = new OpenLayers.Map('map', {controls: []}); - - var control = { - map: callbackMap - }; + callbackMap = new OpenLayers.Map({ + div: "map", + controls: [], // no controls here because these tests use a custom setTimeout and we only want setTimeout calls from a single handler + 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 = { "click": function(evt) { @@ -119,6 +123,7 @@ var handler = new OpenLayers.Handler.Click(control, callbacks, options); handler.activate(); + var timers = {}; window._setTimeout = window.setTimeout; window.setTimeout = function(func, delay) { @@ -270,13 +275,140 @@ handler.map.events.triggerEvent("mouseup", up); handler.map.events.triggerEvent("click", up); - t.eq(log.length, 0, "nothing logged - event outside tolerance"); 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) { t.plan(4); var control = { @@ -395,83 +527,124 @@ }); } - function test_touch_ignoresimulatedclick(t) { - t.plan(2); - - // set up + function test_touch_within_dblclickTolerance(t) { + t.plan(4); var log; - var map = new OpenLayers.Map('map'); - var control = {map: map}; - var callbacks = { - 'dblclick': function(e) { - log.dblclick = {x: e.xy.x, y: e.xy.y, - lastTouches: e.lastTouches}; + 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, pixelTolerance: null}); + control, callbacks, + {"double": true, single: true, pixelTolerance: 2} + ); + handler.activate(); + + function handle(type, x, y) { + map.events.handleBrowserEvent({ + type: type, + touches: [ + {clientX: x, clientY: y} + ] + }); + } // 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 = {}; - handler.touchstart({xy: px(1, 1), touches: ["foo"]}); - handler.touchend({}); - handler.touchstart({xy: px(1, 1), touches: ["foo"]}); - 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"); + t.eq(log.length, 1, "one callback called"); + t.eq(log[0] && log[0].callback, "dblclick", "click callback called"); + t.eq(log[0] && log[0].type, "touchend", "click callback called with touchend event"); + t.ok(!handler.timerId, "handler doesn't have a timerId waiting for click") // tear down map.destroy(); } - function test_touch_dblclick(t) { - t.plan(5); - - // set up + function test_touch_outside_dblclickTolerance(t) { + t.plan(2); var log; - var map = new OpenLayers.Map('map'); - var control = {map: map}; - var callbacks = { - 'click': function(e) { - log.click = {x: e.xy.x, y: e.xy.y, - lastTouches: e.lastTouches}; + click: function(evt) { + log.push({callback: "click", type: evt.type}); }, - 'dblclick': function(e) { - log.dblclick = {x: e.xy.x, y: e.xy.y, - 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( - control, callbacks, - {'double': true, pixelTolerance: null}); + control, callbacks, + {"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 + 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 = {}; - handler.touchstart({xy: px(1, 1), touches: [{clientX:0, clientY:10}]}); - 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"); - } + t.eq(log.length, 0, "no callbacks called"); + t.ok(!handler.timerId, "handler doesn't have a timerId waiting for click") // tear down map.destroy();