[OpenLayers-Commits] r11572 - in sandbox/tschaub/click: lib/OpenLayers/Handler tests/Handler

commits-20090109 at openlayers.org commits-20090109 at openlayers.org
Sun Feb 27 20:57:52 EST 2011


Author: tschaub
Date: 2011-02-27 17:57:51 -0800 (Sun, 27 Feb 2011)
New Revision: 11572

Modified:
   sandbox/tschaub/click/lib/OpenLayers/Handler/Click.js
   sandbox/tschaub/click/tests/Handler/Click.html
Log:
Applying patch for #3133.

Modified: sandbox/tschaub/click/lib/OpenLayers/Handler/Click.js
===================================================================
--- sandbox/tschaub/click/lib/OpenLayers/Handler/Click.js	2011-02-28 01:54:21 UTC (rev 11571)
+++ sandbox/tschaub/click/lib/OpenLayers/Handler/Click.js	2011-02-28 01:57:51 UTC (rev 11572)
@@ -51,13 +51,21 @@
      *     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 10.  If the
+     *     distance between two clicks is greater than this value, a double-
+     *     click will not be fired.
+     */
+    dblclickTolerance: 10,
+        
+    /**
      * 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 @@
      * {Number} The id of the timeout waiting to clear the <delayedCall>.
      */
     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 @@
      */
     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,35 +148,60 @@
      */
     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.
+     * Method: touchstart
+     * Handle touchstart.
      *
      * Returns:
      * {Boolean} Continue propagating this event.
      */
-    mousedown: null,
+    touchstart: function(evt) {
+        this.touch = true;
+        this.down = this.getEventInfo(evt);
+        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: touchstart
-     * Handle touchstart.
+     * Method: touchend
+     *   Correctly set event xy property, and add lastTouches to have
+     *   touches property from last touchstart or touchmove
+     */
+    touchend: function(evt) {
+        // touchend doesn't come with a pixel position or touches
+        evt.xy = this.last.xy;
+        evt.lastTouches = this.last.touches;
+        this.handleSingle(evt);
+        return true;
+    },
+
+    /**
+     * Method: mousedown
+     * Handle mousedown.
      *
      * Returns:
      * {Boolean} Continue propagating this event.
      */
-    touchstart: function(evt) {
-        this.down = this.getEventInfo(evt);
-        this.last = null;
+    mousedown: function(evt) {
+        if (!this.touch) {
+            this.down = this.getEventInfo(evt);
+            this.last = this.getEventInfo(evt);
+        }
         return true;
     },
 
@@ -171,8 +218,7 @@
         // 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 +240,7 @@
            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 +273,97 @@
         if (evt) {
            this.callback('rightclick', [evt]);
         }
-        return !this.stopSingle;
     },
     
     /**
+     * Method: 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) {
+        if (!this.touch) {
+            if (!this.last) {
+                this.last = this.getEventInfo(evt);
+            }
+            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.
+     *     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) {
-        // 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();
+        if (!this.touch) {
+            this.handleDouble(evt);
         }
         return !this.stopDouble;
     },
     
-    /**
-     * Method: touchmove
-     *    Store position of last move, because touchend event can have
-     *    an empty "touches" property.
+    /** 
+     * Method: handleDouble
+     * Handle double-click sequence.
      */
-    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;
+    handleDouble: function(evt) {
+        if (this["double"] && this.passesDblclickTolerance(evt)) {
+            this.callback("dblclick", [evt]);
         }
-        evt.xy = last.xy;
-        evt.lastTouches = last.touches;
-        return evt.xy ? this.click(evt) : false;
     },
-
-    /**
-     * Method: click
-     * Handle click.
-     *
-     * Returns:
-     * {Boolean} Continue propagating this event.
+    
+    /** 
+     * Method: handleSingle
+     * Handle single click sequence.
      */
-    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.passesTolerance(evt)) {
-            if(this.timerId != null) {
+    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 +377,38 @@
      */
     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);
         }
         return passes;
     },
+    
+    /**
+     * 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 <timerId> 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 +421,8 @@
      */
     delayedCall: function(evt) {
         this.timerId = null;
-        if(evt) {
-            this.callback('click', [evt]);
+        if (evt) {
+            this.callback("click", [evt]);
         }
     },
 
@@ -407,7 +467,7 @@
         if(OpenLayers.Handler.prototype.deactivate.apply(this, arguments)) {
             this.clearTimer();
             this.down = null;
-            this.last = null;
+            this.first = null;
             deactivated = true;
         }
         return deactivated;

Modified: sandbox/tschaub/click/tests/Handler/Click.html
===================================================================
--- sandbox/tschaub/click/tests/Handler/Click.html	2011-02-28 01:54:21 UTC (rev 11571)
+++ sandbox/tschaub/click/tests/Handler/Click.html	2011-02-28 01:57:51 UTC (rev 11572)
@@ -102,12 +102,16 @@
     
     var callbackMap;
     function callbackSetup(log, options) {
-        callbackMap = new OpenLayers.Map('map', {controls: []});
+        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 control = {
-            map: callbackMap
-        };
-
         var callbacks = {
             "click": function(evt) {
                 log.push({callback: "click", evt: 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 10
+        var handler = callbackSetup(log, {single: false, "double": true});
+
+        var first = {
+            xy: px(0, 0)
+        };
+        var second = {
+            xy: px(10.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,84 +527,125 @@
         });
     }
 
-    function test_touch_ignoresimulatedclick(t) {
-        t.plan(2);
+    function test_touch_within_dblclickTolerance(t) {
+        t.plan(4);
 
-        // set up
-
         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(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")
 
-        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
         map.destroy();
     }
 
-    function test_touch_dblclick(t) {
-        t.plan(5);
+    function test_touch_outside_dblclickTolerance(t) {
+        t.plan(2);
 
-        // set up
-
         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.length, 0, "no callbacks called");
+        t.ok(!handler.timerId, "handler doesn't have a timerId waiting for click")
 
-        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
         map.destroy();
     }



More information about the Commits mailing list