adding a click handler for flexible cross-browser single and double click handling - add to a control and set click or dblclick callbacks - thanks for the review and pairing crschmidt (closes #1211)

git-svn-id: http://svn.openlayers.org/trunk/openlayers@5438 dc9f47b5-9b13-0410-9fdd-eb0c1a62fdaf
This commit is contained in:
Tim Schaub
2007-12-16 04:42:14 +00:00
parent aa3536ed3a
commit 6ad22b8e6a
5 changed files with 783 additions and 0 deletions

225
examples/click-handler.html Normal file
View File

@@ -0,0 +1,225 @@
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>OpenLayers Click Handler Example</title>
<style type="text/css">
#map {
width: 340px;
height: 170px;
border: 1px solid gray;
}
#west {
width: 350px;
}
#east {
position: absolute;
left: 370px;
top: 3em;
}
table td {
text-align: center;
margin: 0;
border: 1px solid gray;
}
textarea.output {
text-align: left;
font-size: 0.9em;
width: 250px;
height: 65px;
overflow: auto;
}
</style>
<script src="../lib/Firebug/firebug.js"></script>
<script src="../lib/OpenLayers.js"></script>
<script type="text/javascript">
OpenLayers.Control.Click = OpenLayers.Class(OpenLayers.Control, {
defaultHandlerOptions: {
'single': true,
'double': false,
'pixelTolerance': null,
'stopSingle': false,
'stopDouble': false
},
initialize: function(options) {
this.handlerOptions = OpenLayers.Util.extend(
{}, this.defaultHandlerOptions
);
OpenLayers.Control.prototype.initialize.apply(
this, arguments
);
this.handler = new OpenLayers.Handler.Click(
this, {
'click': this.onClick,
'dblclick': this.onDblclick
}, this.handlerOptions
);
},
onClick: function(evt) {
var output = document.getElementById(this.key + "Output");
var msg = "click " + evt.xy;
output.value = output.value + msg + "\r\n";
},
onDblclick: function(evt) {
var output = document.getElementById(this.key + "Output");
var msg = "dblclick " + evt.xy;
output.value = output.value + msg + "\n";
}
});
var map, controls;
function init(){
map = new OpenLayers.Map('map');
var layer = new OpenLayers.Layer.WMS(
"OpenLayers WMS",
"http://labs.metacarta.com/wms/vmap0",
{layers: 'basic'}
);
map.addLayers([layer]);
controls = {
"single": new OpenLayers.Control.Click({
hanlerOptions: {
"single": true
}
}),
"double": new OpenLayers.Control.Click({
handlerOptions: {
"single": false,
"double": true
}
}),
"both": new OpenLayers.Control.Click({
handlerOptions: {
"single": true,
"double": true
}
}),
"nodrag": new OpenLayers.Control.Click({
handlerOptions: {
"single": true,
"pixelTolerance": 1
}
}),
"stopsingle": new OpenLayers.Control.Click({
handlerOptions: {
"single": true,
"stopSingle": true
}
}),
"stopdouble": new OpenLayers.Control.Click({
handlerOptions: {
"single": false,
"double": true,
"stopDouble": true
}
})
};
var props = document.getElementById("props");
var control;
for(var key in controls) {
control = controls[key];
// only to route output here
control.key = key;
map.addControl(control);
}
map.zoomToMaxExtent();
}
function toggle(key) {
var control = controls[key];
if(control.active) {
control.deactivate();
} else {
control.activate();
}
var status = document.getElementById(key + "Status");
status.innerHTML = control.active ? "on" : "off";
var output = document.getElementById(key + "Output");
output.value = "";
}
</script>
</head>
<body onload="init()">
<h1 id="title">Click Handler Example</h1>
<div id="west">
<div id="tags">
</div>
<p id="shortdesc">
This example shows the use of the click handler.
</p>
<div id="map"></div>
<p>
The click handler can be used to gain more flexibility over handling
click events. The handler can be constructed with options to handle
only single click events, to handle single and double-click events,
to ignore clicks that include a drag, and to stop propagation of
single and/or double-click events. A single click is a click that
is not followed by another click for more than 300ms. This delay
is configured with the delay property.
</p>
<p>
The options to stop single and double clicks have to do with
stopping event propagation on the map events listener queue
(not stopping events from cascading to other elements). The
ability to stop an event from propagating has to do with the
order in which listeners are registered. With stopSingle or
stopDouble true, a click handler will stop propagation to all
listeners that were registered (or all handlers that were
activated) before the click handler was activated. So, for
example, activating a click handler with stopDouble true after
the navigation control is active will stop double-clicks from
zooming in.
</p>
</div>
<div id="east">
<table>
<caption>Controls with click handlers (toggle on/off to clear output)</caption>
<tbody>
<tr>
<td>single only</td>
<td><button id="singleStatus" onclick="toggle('single')">off</button></td>
<td><textarea class="output" id="singleOutput"></textarea></td>
</tr>
<tr>
<td>double only</td>
<td><button id="doubleStatus" onclick="toggle('double')">off</button></td>
<td><textarea class="output" id="doubleOutput"></textarea></td>
</tr>
<tr>
<td>both</td>
<td><button id="bothStatus" onclick="toggle('both')">off</button></td>
<td><textarea class="output" id="bothOutput"></textarea></td>
</tr>
<tr>
<td>single no drag</td>
<td><button id="nodragStatus" onclick="toggle('nodrag')">off</button></td>
<td><textarea class="output" id="nodragOutput"></textarea></td>
</tr>
<tr>
<td>single with stop</td>
<td><button id="stopsingleStatus" onclick="toggle('stopsingle')">off</button></td>
<td><textarea class="output" id="stopsingleOutput"></textarea></td>
</tr>
<tr>
<td>double with stop</td>
<td><button id="stopdoubleStatus" onclick="toggle('stopdouble')">off</button></td>
<td><textarea class="output" id="stopdoubleOutput"></textarea></td>
</tr>
</tbody>
</table>
</div>
</body>
</html>

View File

@@ -116,6 +116,7 @@
"OpenLayers/Feature/Vector.js",
"OpenLayers/Feature/WFS.js",
"OpenLayers/Handler.js",
"OpenLayers/Handler/Click.js",
"OpenLayers/Handler/Point.js",
"OpenLayers/Handler/Path.js",
"OpenLayers/Handler/Polygon.js",

View File

@@ -0,0 +1,239 @@
/* Copyright (c) 2006-2007 MetaCarta, Inc., published under the clear BSD license.
* See http://svn.openlayers.org/trunk/openlayers/license.txt
* for the full text of the license. */
/**
* @requires OpenLayers/Handler.js
*
* Class: OpenLayers.Handler.Click
* A handler for mouse clicks. The intention of this handler is to give
* controls more flexibility with handling clicks. Browsers trigger
* click events twice for a double-click. In addition, the mousedown,
* mousemove, mouseup sequence fires a click event. With this handler,
* controls can decide whether to ignore clicks associated with a double
* click. By setting a <pixelTolerance>, controls can also ignore clicks
* that include a drag. Create a new instance with the
* <OpenLayers.Handler.Click> constructor.
*
* Inherits from:
* - <OpenLayers.Handler>
*/
OpenLayers.Handler.Click = OpenLayers.Class(OpenLayers.Handler, {
/**
* APIProperty: delay
* {Number} Number of milliseconds between clicks before the event is
* considered a double-click.
*/
delay: 300,
/**
* APIProperty: single
* {Boolean} Handle single clicks. Default is true. If false, clicks
* will not be reported. If true, single-clicks will be reported.
*/
single: true,
/**
* APIProperty: double
* {Boolean} Handle double-clicks. Default is false.
*/
'double': false,
/**
* APIProperty: pixelTolerance
* {Number} Maximum number of pixels between mouseup and mousedown for an
* event to be considered a click. Default is null. If set to an
* integer value, clicks with a drag greater than the value will be
* ignored. This property can only be set when the handler is
* constructed.
*/
pixelTolerance: null,
/**
* 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).
*/
stopSingle: false,
/**
* APIProperty: stopDouble
* {Boolean} Stop other listeners from being notified of double-clicks.
* Default is false. If true, any click listeners registered before
* this one will not be notified of *any* double-click events.
*
* The one caveat with stopDouble is that given a map with two click
* handlers, one with stopDouble true and the other with stopSingle
* true, the stopSingle handler should be activated last to get
* uniform cross-browser performance. Since IE triggers one click
* with a dblclick and FF triggers two, if a stopSingle handler is
* activated first, all it gets in IE is a single click when the
* second handler stops propagation on the dblclick.
*/
stopDouble: false,
/**
* Property: timerId
* {Number} The id of the timeout waiting to clear the <delayedEvent>.
*/
timerId: null,
/**
* Property: down
* {<OpenLayers.Pixel>} The pixel location of the last mousedown.
*/
down: null,
/**
* Property: cachedEvent
* {Event} Since IE doesn't let us call window.setTimeout with extra
* arguments, we cache the last click event here.
*/
cachedEvent: null,
/**
* Constructor: OpenLayers.Handler.Click
* Create a new click handler.
*
* Parameters:
* control - {<OpenLayers.Control>} The control that is making use of
* this handler. If a handler is being used without a control, the
* handler's setMap method must be overridden to deal properly with
* the map.
* callbacks - {Object} An object with keys corresponding to callbacks
* that will be called by the handler. The callbacks should
* expect to recieve a single argument, the click event.
* Callbacks for 'click' and 'dblclick' are supported.
* options - {Object} Optional object whose properties will be set on the
* handler.
*/
initialize: function(control, callbacks, options) {
OpenLayers.Handler.prototype.initialize.apply(this, arguments);
// optionally register for mouseup and mousedown
if(this.pixelTolerance != null) {
this.mousedown = function(evt) {
this.down = evt.xy;
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: 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) {
if(this.passesTolerance(evt)) {
if(this["double"]) {
this.callback('dblclick', [evt]);
}
this.clearTimer();
}
return !this.stopDouble;
},
/**
* Method: click
* Handle click.
*
* Returns:
* {Boolean} Continue propagating this event.
*/
click: function(evt) {
if(this.passesTolerance(evt)) {
if(this.timerId != null) {
// already received a click
this.clearTimer();
} else {
// set the timer, send evt only if single is true
var clickEvent = this.single ? evt : null;
this.timerId = window.setTimeout(
OpenLayers.Function.bind(this.delayedCall, this, clickEvent),
this.delay
);
}
}
return !this.stopSingle;
},
/**
* Method: passesTolerance
* Determine whether the event is within the optional pixel tolerance.
*
* Returns:
* {Boolean} The click is within the pixel tolerance (if specified).
*/
passesTolerance: function(evt) {
var passes = true;
if(this.pixelTolerance) {
var dpx = Math.sqrt(
Math.pow(this.down.x - evt.xy.x, 2) +
Math.pow(this.down.y - evt.xy.y, 2)
);
if(dpx > this.pixelTolerance) {
passes = false;
}
}
return passes;
},
/**
* Method: clearTimer
* Clear the timer and set <timerId> to null.
*/
clearTimer: function() {
if(this.timerId != null) {
window.clearTimeout(this.timerId);
this.timerId = null;
}
},
/**
* Method: delayedCall
* Sets <timerId> to null. And optionally triggers the click callback if
* <cachedEvent> is set.
*/
delayedCall: function(evt) {
this.timerId = null;
if(evt) {
this.callback('click', [evt]);
}
},
/**
* APIMethod: deactivate
* Deactivate the handler.
*
* Returns:
* {Boolean} The handler was successfully deactivated.
*/
deactivate: function() {
var deactivated = false;
if(OpenLayers.Handler.prototype.deactivate.apply(this, arguments)) {
this.clearTimer();
this.down = null;
deactivated = true;
}
return deactivated;
},
CLASS_NAME: "OpenLayers.Handler.Click"
});

View File

@@ -0,0 +1,317 @@
<html>
<head>
<script src="../../lib/OpenLayers.js"></script>
<script type="text/javascript">
function test_Handler_Click_constructor(t) {
t.plan(3);
var control = new OpenLayers.Control();
control.id = Math.random();
var callbacks = {foo: "bar"};
var options = {bar: "foo"};
var oldInit = OpenLayers.Handler.prototype.initialize;
OpenLayers.Handler.prototype.initialize = function(con, call, opt) {
t.eq(con.id, control.id,
"constructor calls parent with the correct control");
t.eq(call, callbacks,
"constructor calls parent with the correct callbacks");
t.eq(opt, options,
"constructor calls parent with the correct options");
}
var handler = new OpenLayers.Handler.Click(control, callbacks, options);
OpenLayers.Handler.prototype.initialize = oldInit;
}
function test_Handler_Click_activate(t) {
t.plan(2);
var control = {
map: new OpenLayers.Map('map')
};
var handler = new OpenLayers.Handler.Click(control);
handler.active = true;
var activated = handler.activate();
t.ok(!activated,
"activate returns false if the handler was already active");
handler.active = false;
handler.dragging = true;
activated = handler.activate();
t.ok(activated,
"activate returns true if the handler was not already active");
}
function test_Handler_Click_events(t) {
t.plan(30);
var map = new OpenLayers.Map('map');
var control = {
map: map
};
map.events.registerPriority = function(type, obj, func) {
var r = func();
if(typeof r == "string") {
// this is one of the mock handler methods
t.eq(OpenLayers.Util.indexOf(nonevents, type), -1,
"registered method is not one of the events " +
"that should not be handled");
t.ok(OpenLayers.Util.indexOf(events, type) > -1,
"activate calls registerPriority with browser event: " + type);
t.eq(typeof func, "function",
"activate calls registerPriority with a function");
t.eq(func(), type,
"activate calls registerPriority with the correct method");
t.eq(obj["CLASS_NAME"], "OpenLayers.Handler.Click",
"activate calls registerPriority with the handler");
}
}
function setMethod(key) {
handler[key] = function() {return key};
}
// list below events that should be handled (events) and those
// that should not be handled (nonevents) by the handler
var events = ["click", "dblclick"];
var nonevents = ["mousedown", "mousemove", "mouseup", "resize", "focus", "blur"];
var handler = new OpenLayers.Handler.Click(control);
// set browser event like properties on the handler
for(var i=0; i<events.length; ++i) {
setMethod(events[i]);
}
handler.activate();
// different listeners registered for pixelTolerance option
var events = ["click", "dblclick", "mousedown", "mouseup"];
var nonevents = ["mousemove", "resize", "focus", "blur"];
var handler = new OpenLayers.Handler.Click(control, {}, {
pixelTolerance: 2
});
for(var i=0; i<events.length; ++i) {
setMethod(events[i]);
}
handler.activate();
}
function test_Handler_Click_callbacks(t) {
t.plan(13);
var map = new OpenLayers.Map('map', {controls: []});
var control = {
map: map
};
var handler = new OpenLayers.Handler.Click(control, {});
handler.activate();
// set up for single click - three tests here
var timers = {};
var sto = window.setTimeout;
window.setTimeout = function(func, delay) {
var key = Math.random();
timers[key] = true;
t.ok(typeof func == "function",
"setTimeout called with a function");
t.eq(delay, handler.delay,
"setTimeout called with proper delay");
// execute function that is supposed to be delayed
func();
return key;
}
var cto = window.clearTimeout;
window.clearTimeout = function(key) {
if(timers[key] === true) {
delete timers[key];
} else {
t.fail("clearTimeout called with non-existent timerId");
}
}
var testEvt = Math.random();
handler.callbacks = {
"click": function(evt) {
t.eq(evt, testEvt,
"(click w/ single true) click callback called with correct evt");
},
"dblclick": function(evt) {
t.fail("(click w/ single true) dblclick should not be called here");
}
};
map.events.triggerEvent("click", testEvt);
// set up for double click with double false - no tests here (only failures)
handler.callbacks = {
"click": function(evt) {
t.fail("(dblclick w/ double false) click should not be called here");
},
"dblclick": function(evt) {
t.fail("(dblclick w/ double false) dblclick should not be called here");
}
};
testEvt = Math.random();
map.events.triggerEvent("dblclick", testEvt);
// set up for double click with double true - one test here
handler.double = true;
handler.callbacks = {
"click": function(evt) {
t.fail("(dblclick w/ double true) click should not be called here");
},
"dblclick": function(evt) {
t.eq(evt, testEvt,
"(dblclick w/ double true) dblclick called with correct evt");
}
};
testEvt = Math.random();
map.events.triggerEvent("dblclick", testEvt);
// set up for two clicks with double true - 6 tests here (with timeout ones from above)
handler.double = true;
handler.callbacks = {
"click": function(evt) {
t.ok(evt != null, "(two clicks w/ double true) click will not be called here if next three tests pass");
},
"dblclick": function(evt) {
t.eq(evt, testEvt,
"(two clicks w/ double true) dblclick called with correct evt");
}
};
testEvt = Math.random();
map.events.triggerEvent("click", testEvt);
t.ok(handler.timerId != null,
"(two clicks w/ double true) timer is set to call click");
map.events.triggerEvent("click", testEvt);
t.ok(handler.timerId == null,
"(two clicks w/ double true) timer is cleared to call click");
map.events.triggerEvent("dblclick", testEvt);
handler.destroy();
// set up to tests pixelTolerance - three tests here (2 from setTimeout above)
handler = new OpenLayers.Handler.Click(control, {}, {
pixelTolerance: 2
});
handler.activate();
var downEvt = {
xy: new OpenLayers.Pixel(0, 0)
};
map.events.triggerEvent("mousedown", downEvt);
var clickEvt = {
xy: new OpenLayers.Pixel(0, 1)
};
// mouse moves one pixel, click should be called
handler.callbacks = {
"click": function(evt) {
t.ok(evt == clickEvt, "(pixelTolerance met) click called");
}
};
map.events.triggerEvent("click", clickEvt);
handler.clearTimer();
// mouse moves 3x3 pixels, click should not be called
map.events.triggerEvent("mousedown", downEvt);
var clickEvt = {
xy: new OpenLayers.Pixel(3, 3)
};
// mouse moves one pixel, click should be called
handler.callbacks = {
"click": function(evt) {
t.fail("(pixelTolerance not met) click should not be called");
}
};
map.events.triggerEvent("click", clickEvt); // no test run
handler.clearTimer();
window.setTimeout = sto;
window.clearTimeout = cto;
}
function test_Handler_Drag_submethods(t) {
t.plan(4);
var map = new OpenLayers.Map('map', {controls: []});
var control = new OpenLayers.Control();
map.addControl(control);
var handler = new OpenLayers.Handler.Drag(control, {});
// set test events
var events = ["down", "move", "up", "out"];
var testEvents = {};
var type, px;
for(var i=0; i<events.length; ++i) {
type = events[i];
px = new OpenLayers.Pixel(Math.random(), Math.random());
testEvents[type] = {xy: px};
setMethod(type);
}
function setMethod(type) {
handler[type] = function(evt) {
t.ok(evt.xy.x == testEvents[type].xy.x &&
evt.xy.y == testEvents[type].xy.y,
"handler." + type + " called with the right event");
}
}
handler.activate();
// test mousedown
handler.checkModifiers = function(evt) {
return true;
}
var oldIsLeftClick = OpenLayers.Event.isLeftClick;
OpenLayers.Event.isLeftClick = function(evt) {
return true;
}
map.events.triggerEvent("mousedown", testEvents.down);
OpenLayers.Event.isLeftClick = oldIsLeftClick;
// test mousemove
map.events.triggerEvent("mousemove", testEvents.move);
// test mouseup
map.events.triggerEvent("mouseup", testEvents.up);
// test mouseout
var oldMouseLeft = OpenLayers.Util.mouseLeft;
OpenLayers.Util.mouseLeft = function() {
return true;
};
handler.started = true;
map.events.triggerEvent("mouseout", testEvents.out);
OpenLayers.Util.mouseLeft = oldMouseLeft;
}
function test_Handler_Click_deactivate(t) {
t.plan(4);
var control = {
map: new OpenLayers.Map('map')
};
var handler = new OpenLayers.Handler.Click(control);
handler.active = false;
var deactivated = handler.deactivate();
t.ok(!deactivated,
"deactivate returns false if the handler was not already active");
handler.active = true;
handler.down = true;
handler.timerId = true;
deactivated = handler.deactivate();
t.ok(deactivated,
"deactivate returns true if the handler was active already");
t.eq(handler.down, null,
"deactivate sets down to null");
t.eq(handler.timerId, null,
"deactivate sets timerId to null");
}
</script>
</head>
<body>
<div id="map" style="width: 300px; height: 150px;"/>
</body>
</html>

View File

@@ -85,6 +85,7 @@
<li>Control/test_Permalink.html</li>
<li>Control/test_Scale.html</li>
<li>test_Handler.html</li>
<li>Handler/test_Click.html</li>
<li>Handler/test_Feature.html</li>
<li>Handler/test_MouseWheel.html</li>
<li>Handler/test_Keyboard.html</li>