diff --git a/examples/select-feature.html b/examples/select-feature.html index 2671d4add3..9dcecd6e98 100644 --- a/examples/select-feature.html +++ b/examples/select-feature.html @@ -13,7 +13,7 @@ @@ -94,13 +111,23 @@
  • - + +
  • -
  • - - -
  • - + +

    Use the shift key to select multiple features. Use the ctrl key to + toggle selection on features one at a time. Note: the "clickout" option has no + effect when "hover" is selected.

    diff --git a/lib/OpenLayers/Control/ModifyFeature.js b/lib/OpenLayers/Control/ModifyFeature.js index 003876a116..ff6f4fe547 100644 --- a/lib/OpenLayers/Control/ModifyFeature.js +++ b/lib/OpenLayers/Control/ModifyFeature.js @@ -28,6 +28,20 @@ OpenLayers.Control.ModifyFeature = OpenLayers.Class(OpenLayers.Control, { */ geometryTypes: null, + /** + * APIProperty: clickout + * {Boolean} Unselect features when clicking outside any feature. + * Default is true. + */ + clickout: true, + + /** + * APIProperty: toggle + * {Boolean} Unselect a selected feature on click. + * Default is true. + */ + toggle: true, + /** * Property: layer * {} @@ -165,6 +179,8 @@ OpenLayers.Control.ModifyFeature = OpenLayers.Class(OpenLayers.Control, { // configure the select control var selectOptions = { geometryTypes: this.geometryTypes, + clickout: this.clickout, + toggle: this.toggle, onSelect: function(feature) { control.selectFeature.apply(control, [feature]); }, diff --git a/lib/OpenLayers/Control/SelectFeature.js b/lib/OpenLayers/Control/SelectFeature.js index d44ee8ea70..45848f71fe 100644 --- a/lib/OpenLayers/Control/SelectFeature.js +++ b/lib/OpenLayers/Control/SelectFeature.js @@ -15,12 +15,40 @@ */ OpenLayers.Control.SelectFeature = OpenLayers.Class(OpenLayers.Control, { + /** + * Property: multipleKey + * {String} An event modifier ('altKey' or 'shiftKey') that temporarily sets + * the property to true. Default is null. + */ + multipleKey: null, + + /** + * Property: toggleKey + * {String} An event modifier ('altKey' or 'shiftKey') that temporarily sets + * the property to true. Default is null. + */ + toggleKey: null, + /** * APIProperty: multiple - * {Boolean} Allow selection of multiple geometries + * {Boolean} Allow selection of multiple geometries. Default is false. */ multiple: false, + /** + * APIProperty: clickout + * {Boolean} Unselect features when clicking outside any feature. + * Default is true. + */ + clickout: true, + + /** + * APIProperty: toggle + * {Boolean} Unselect a selected feature on click. Default is false. Only + * has meaning if hover is false. + */ + toggle: false, + /** * APIProperty: hover * {Boolean} Select on mouse over and deselect on mouse out. If true, this @@ -85,18 +113,31 @@ OpenLayers.Control.SelectFeature = OpenLayers.Class(OpenLayers.Control, { this.layer = layer; this.callbacks = OpenLayers.Util.extend({ click: this.clickFeature, + clickout: this.clickoutFeature, over: this.overFeature, out: this.outFeature }, this.callbacks); - var handlerOptions = {geometryTypes: this.geometryTypes}; + var handlerOptions = { geometryTypes: this.geometryTypes}; this.handler = new OpenLayers.Handler.Feature(this, layer, this.callbacks, handlerOptions); }, + /** + * Method: unselectAll + * Unselect all selected features. + */ + unselectAll: function() { + // we'll want an option to supress notification here + while (this.layer.selectedFeatures.length > 0) { + this.unselect(this.layer.selectedFeatures[0]); + } + }, + /** * Method: clickFeature - * Called when the feature handler detects a click on a feature + * Called on click in a feature + * Only responds if this.hover is false. * * Parameters: * feature - {} @@ -105,56 +146,66 @@ OpenLayers.Control.SelectFeature = OpenLayers.Class(OpenLayers.Control, { if(this.hover) { return; } - if (this.multiple) { - if(OpenLayers.Util.indexOf(this.layer.selectedFeatures, feature) > -1) { + var selected = (OpenLayers.Util.indexOf(this.layer.selectedFeatures, + feature) > -1); + if(!this.multiple && !this.handler.evt[this.multipleKey]) { + // perhaps an "except" argument + this.unselectAll(); + } + if(selected) { + if(this.toggle || this.handler.evt[this.toggleKey]) { + // notify here this.unselect(feature); } else { + // don't notify here - could be removed if unselectAll is modified this.select(feature); } } else { - if(OpenLayers.Util.indexOf(this.layer.selectedFeatures, feature) > -1) { - this.unselect(feature); - } else { - if (this.layer.selectedFeatures) { - for (var i = 0; i < this.layer.selectedFeatures.length; i++) { - this.unselect(this.layer.selectedFeatures[i]); - } - } - this.select(feature); - } + this.select(feature); + } + }, + + /** + * Method: clickoutFeature + * Called on click outside a previously clicked (selected) feature. + * Only responds if this.hover is false. + * + * Parameters: + * feature - {} + */ + clickoutFeature: function(feature) { + if(!this.hover && this.clickout) { + this.unselectAll(); } }, /** * Method: overFeature - * Called when the feature handler detects a mouse-over on a feature. + * Called on over a feature. * Only responds if this.hover is true. * * Parameters: * feature - {} */ overFeature: function(feature) { - if(!this.hover) { - return; - } - if(!(OpenLayers.Util.indexOf(this.layer.selectedFeatures, feature) > -1)) { + if(this.hover && + (OpenLayers.Util.indexOf(this.layer.selectedFeatures, feature) == -1)) { this.select(feature); } }, /** * Method: outFeature - * Called when the feature handler detects a mouse-out on a feature. + * Called on out of a selected feature. * Only responds if this.hover is true. * * Parameters: * feature - {} */ outFeature: function(feature) { - if(!this.hover) { - return; + if(this.hover) { + this.unselect(feature); } - this.unselect(feature); }, /** diff --git a/lib/OpenLayers/Handler.js b/lib/OpenLayers/Handler.js index 675b57cdcd..046f91db6e 100644 --- a/lib/OpenLayers/Handler.js +++ b/lib/OpenLayers/Handler.js @@ -197,7 +197,7 @@ OpenLayers.Handler = OpenLayers.Class({ * the callback (defined by the control). */ callback: function (name, args) { - if (this.callbacks[name]) { + if (name && this.callbacks[name]) { this.callbacks[name].apply(this.control, args); } }, diff --git a/lib/OpenLayers/Handler/Feature.js b/lib/OpenLayers/Handler/Feature.js index 65c1dc90f0..4a2186ea08 100644 --- a/lib/OpenLayers/Handler/Feature.js +++ b/lib/OpenLayers/Handler/Feature.js @@ -7,13 +7,57 @@ * @requires OpenLayers/Handler.js * * Class: OpenLayers.Handler.Feature - * Handler to respond to mouse events related to a drawn feature. - * Callbacks will be called for over, move, out, up, down, and click - * (corresponding to the equivalent mouse events). + * Handler to respond to mouse events related to a drawn feature. Callbacks + * with the following keys will be notified of the following events + * associated with features: click, clickout, over, out, and dblclick. + * + * This handler stops event propagation for mousedown and mouseup if those + * browser events target features that can be selected. */ OpenLayers.Handler.Feature = OpenLayers.Class(OpenLayers.Handler, { /** + * Property: EVENTMAP + * {Object} A object mapping the browser events to objects with callback + * keys for in and out. + */ + EVENTMAP: { + 'click': {'in': 'click', 'out': 'clickout'}, + 'mousemove': {'in': 'over', 'out': 'out'}, + 'dblclick': {'in': 'dblclick', 'out': null}, + 'mousedown': {'in': null, 'out': null}, + 'mouseup': {'in': null, 'out': null} + }, + + /** + * Property: feature + * {} The last feature that was handled. + */ + feature: null, + + /** + * Property: down + * {} The location of the last mousedown. + */ + down: null, + + /** + * Property: up + * {} The location of the last mouseup. + */ + up: null, + + /** + * Property: clickoutTolerance + * {Number} The number of pixels the mouse can move during a click that + * still constitutes a click out. When dragging the map, clicks should + * not trigger the clickout property unless this tolerance is reached. + * Default is 4. + */ + clickoutTolerance: 4, + + /** + * Property: geometryTypes * To restrict dragging to a limited set of geometry types, send a list * of strings corresponding to the geometry class names. * @@ -27,12 +71,6 @@ OpenLayers.Handler.Feature = OpenLayers.Class(OpenLayers.Handler, { */ layerIndex: null, - /** - * Property: feature - * {} - */ - feature: null, - /** * Constructor: OpenLayers.Handler.Feature * @@ -49,117 +87,161 @@ OpenLayers.Handler.Feature = OpenLayers.Class(OpenLayers.Handler, { this.layer = layer; }, - /** - * Method: click - * Handle click. Call the "click" callback if down on a feature. - * - * Parameters: - * evt - {Event} - */ - click: function(evt) { - var selected = this.select('click', evt); - return !selected; // stop event propagation if selected - }, /** * Method: mousedown - * Handle mouse down. Call the "down" callback if down on a feature. + * Handle mouse down. Stop propagation if a feature is targeted by this + * event (stops map dragging during feature selection). * * Parameters: * evt - {Event} */ mousedown: function(evt) { - var selected = this.select('down', evt); - return !selected; // stop event propagation if selected + this.down = evt.xy; + return !this.handle(evt); }, - /** - * Method: mousemove - * Handle mouse moves. Call the "move" callback if moving over a feature. - * Call the "over" callback if moving over a feature for the first time. - * Call the "out" callback if moving off of a feature. - * - * Parameters: - * evt - {Event} - */ - mousemove: function(evt) { - this.select('move', evt); - return true; - }, - /** * Method: mouseup - * Handle mouse up. Call the "up" callback if up on a feature. + * Handle mouse up. Stop propagation if a feature is targeted by this + * event. * * Parameters: * evt - {Event} */ mouseup: function(evt) { - var selected = this.select('up', evt); - return !selected; // stop event propagation if selected + this.up = evt.xy; + return !this.handle(evt); + }, + + /** + * Method: click + * Handle click. Call the "click" callback if click on a feature, + * or the "clickout" callback if click outside any feature. + * + * Parameters: + * evt - {Event} + * + * Returns: + * {Boolean} + */ + click: function(evt) { + return !this.handle(evt); + }, + + /** + * Method: mousemove + * Handle mouse moves. Call the "over" callback if move over a feature, + * or the "out" callback if move outside any feature. + * + * Parameters: + * evt - {Event} + * + * Returns: + * {Boolean} + */ + mousemove: function(evt) { + this.handle(evt); + return true; }, /** * Method: dblclick - * Capture double-clicks. Let the event continue propagating if the - * double-click doesn't hit a feature. Otherwise call the dblclick - * callback. + * Handle dblclick. Call the "dblclick" callback if dblclick on a feature. * * Parameters: * evt - {Event} + * + * Returns: + * {Boolean} */ dblclick: function(evt) { - var selected = this.select('dblclick', evt); - return !selected; // stop event propagation if selected + return !this.handle(evt); }, /** - * Method: select - * Trigger the appropriate callback if a feature is under the mouse. + * Method: geometryTypeMatches + * Return true if the geometry type of the passed feature matches + * one of the geometry types in the geometryTypes array. * * Parameters: - * type - {String} Callback key + * feature - {} * * Returns: - * {Boolean} A feature was selected + * {Boolean} */ - select: function(type, evt) { + geometryTypeMatches: function(feature) { + return this.geometryTypes == null || + OpenLayers.Util.indexOf(this.geometryTypes, + feature.geometry.CLASS_NAME) > -1; + }, + + /** + * Method: handle + * + * Parameters: + * evt - {Event} + * + * Returns: + * {Boolean} Stop event propagation. + */ + handle: function(evt) { + var type = evt.type; + var stopEvtPropag = false; + var lastFeature = this.feature; var feature = this.layer.getFeatureFromEvent(evt); - var selected = false; if(feature) { - if(this.geometryTypes == null || - (OpenLayers.Util.indexOf(this.geometryTypes, - feature.geometry.CLASS_NAME) > -1)) { - // three cases: - // over a new, out of the last and over a new, or still on the last - if(!this.feature) { - // over a new feature - this.callback('over', [feature]); - } else if(this.feature != feature) { - // out of the last and over a new - this.callback('out', [this.feature]); - this.callback('over', [feature]); + if(this.geometryTypeMatches(feature)) { + if(lastFeature && (lastFeature != feature)) { + // out of last feature + this.triggerCallback(type, 'out', [lastFeature]); } - this.feature = feature; - this.callback(type, [feature]); - selected = true; + this.triggerCallback(type, 'in', [feature]); + lastFeature = feature; + stopEvtPropag = true; } else { - if(this.feature && (this.feature != feature)) { - // out of the last and over a new - this.callback('out', [this.feature]); - this.feature = null; + if(lastFeature && (lastFeature != feature)) { + // out of last feature + this.triggerCallback(type, 'out', [lastFeature]); + lastFeature = feature; } - selected = false; } } else { - if(this.feature) { - // out of the last - this.callback('out', [this.feature]); - this.feature = null; + if(lastFeature) { + this.triggerCallback(type, 'out', [lastFeature]); + lastFeature = null; + } + } + if(lastFeature) { + this.feature = lastFeature; + } + return stopEvtPropag; + }, + + /** + * Method: triggerCallback + * Call the callback keyed in the event map with the supplied arguments. + * For click out, the is checked first. + * + * Parameters: + * type - {String} + */ + triggerCallback: function(type, mode, args) { + var key = this.EVENTMAP[type][mode]; + if(key) { + if(type == 'click' && mode == 'out' && this.up && this.down) { + // for clickout, only trigger callback if tolerance is met + var dpx = Math.sqrt( + Math.pow(this.up.x - this.down.x, 2) + + Math.pow(this.up.y - this.down.y, 2) + ); + if(dpx <= this.clickoutTolerance) { + this.callback(key, args); + } + } else { + this.callback(key, args); } - selected = false; } - return selected; }, /** @@ -170,13 +252,13 @@ OpenLayers.Handler.Feature = OpenLayers.Class(OpenLayers.Handler, { * {Boolean} */ activate: function() { + var activated = false; if(OpenLayers.Handler.prototype.activate.apply(this, arguments)) { this.layerIndex = this.layer.div.style.zIndex; this.layer.div.style.zIndex = this.map.Z_INDEX_BASE['Popup'] - 1; - return true; - } else { - return false; + activated = true; } + return activated; }, /** @@ -187,14 +269,17 @@ OpenLayers.Handler.Feature = OpenLayers.Class(OpenLayers.Handler, { * {Boolean} */ deactivate: function() { + var deactivated = false; if(OpenLayers.Handler.prototype.deactivate.apply(this, arguments)) { if (this.layer && this.layer.div) { this.layer.div.style.zIndex = this.layerIndex; - } - return true; - } else { - return false; + } + this.feature = null; + this.down = null; + this.up = null; + deactivated = true; } + return deactivated; }, CLASS_NAME: "OpenLayers.Handler.Feature" diff --git a/tests/Control/test_DragFeature.html b/tests/Control/test_DragFeature.html index e6de4ff3cc..b531bbd586 100644 --- a/tests/Control/test_DragFeature.html +++ b/tests/Control/test_DragFeature.html @@ -85,7 +85,7 @@ layer.getFeatureFromEvent = function(evt) { return "foo"; } - map.events.triggerEvent("mousemove"); + map.events.triggerEvent("mousemove", {type: "mousemove"}); t.eq(control.feature, "foo", "control gets the proper feature from the feature handler"); @@ -107,14 +107,14 @@ layer.getFeatureFromEvent = function(evt) { return "foo"; } - map.events.triggerEvent("mousemove"); + map.events.triggerEvent("mousemove", {type: "mousemove"}); // simulate a mousedown on a feature control.onStart = function(feature, pixel) { t.eq(feature, "foo", "onStart called with the correct feature"); t.eq(pixel, "bar", "onStart called with the correct pixel"); } - map.events.triggerEvent("mousedown", {xy: "bar", which: 1}); + map.events.triggerEvent("mousedown", {xy: "bar", which: 1, type: "mousemove"}); t.eq(control.lastPixel, "bar", "mousedown sets the lastPixel correctly"); @@ -152,15 +152,15 @@ }; // simulate a mouseover on a feature - map.events.triggerEvent("mousemove"); + map.events.triggerEvent("mousemove", {type: "mousemove"}); // simulate a mousedown on a feature var down = new OpenLayers.Pixel(0, 0); - map.events.triggerEvent("mousedown", {xy: down, which: 1}); + map.events.triggerEvent("mousedown", {xy: down, which: 1, type: "mousemove"}); // simulate a mousemove on a feature var move = new OpenLayers.Pixel(1, 2); - map.events.triggerEvent("mousemove", {xy: move, which: 1}); + map.events.triggerEvent("mousemove", {xy: move, which: 1, type: "mousemove"}); } @@ -179,7 +179,7 @@ layer.getFeatureFromEvent = function() { return "foo"; }; - map.events.triggerEvent("mousemove"); + map.events.triggerEvent("mousemove", {type: "mousemove"}); t.eq(control.feature, "foo", "feature is set on mouse over"); control.doneDragging(); @@ -203,7 +203,7 @@ layer.getFeatureFromEvent = function() { return "foo"; }; - map.events.triggerEvent("mousemove"); + map.events.triggerEvent("mousemove", {type: "mousemove"}); t.eq(control.feature, "foo", "feature is set on mouse over"); @@ -211,7 +211,7 @@ layer.getFeatureFromEvent = function() { return null; }; - map.events.triggerEvent("mousemove"); + map.events.triggerEvent("mousemove", {type: "mousemove"}); t.ok(control.feature == null, "feature is set to null on mouse out"); diff --git a/tests/Handler/test_Feature.html b/tests/Handler/test_Feature.html index 07c2b1c021..27fef3e566 100644 --- a/tests/Handler/test_Feature.html +++ b/tests/Handler/test_Feature.html @@ -99,7 +99,7 @@ } function test_Handler_feature_geometrytype_limit(t) { - t.plan(2); + t.plan(1); var feature = new OpenLayers.Feature.Vector(new OpenLayers.Geometry.Point(0,0)); var map = new OpenLayers.Map('map'); var control = new OpenLayers.Control(); @@ -112,112 +112,106 @@ handler.callback = function(type,featurelist) { t.eq(featurelist[0].id, feature.id, "Correct feature called back on"); } - handler.select("foo", {}); + handler.handle({type: "click"}); handler.feature = null; + handler.lastFeature = null; handler.callback = function(type,featurelist) { t.fail("Shouldn't have called back on " + featurelist[0].geometry); } feature = new OpenLayers.Feature.Vector(new OpenLayers.Geometry.LineString(0,0)); - handler.select("foo", {}); + handler.handle("click", {}); } function test_Handler_Feature_callbacks(t) { - t.plan(75); + t.plan(9); var map = new OpenLayers.Map('map', {controls: []}); var control = new OpenLayers.Control(); map.addControl(control); var layer = new OpenLayers.Layer(); map.addLayer(layer); - - var evtsToTest = [ - { - shortName: "down", - longName: "mousedown" - }, - { - shortName: "move", - longName: "mousemove" - }, - { - shortName: "up", - longName: "mouseup" - }, - { - shortName: "click", - longName: "click" - }, - { - shortName: "dblclick", - longName: "dblclick" - } - ]; - var numEvents = {}; var callbacks = {}; + var newFeature, lastFeature; + var evtPx = {xy: new OpenLayers.Pixel(Math.random(), Math.random())}; - var newFeature; - var oldFeature; - + // define a callback factory function function getCallback(evt, feature) { return function(f) { - t.ok(f == feature, "callback called with proper feature"); - numEvents[evt]++; + t.ok(f == feature, evt + " callback called with proper feature"); }; } + // override the layer's getFeatureFromEvent func so that it always + // returns newFeature layer.getFeatureFromEvent = function(evt) { return newFeature; }; var handler = new OpenLayers.Handler.Feature(control, layer, callbacks); handler.activate(); - - for (var i = 0; i < evtsToTest.length; i++) { - evt = evtsToTest[i]; - - var evtShortName = evt.shortName; - var evtLongName = evt.longName; - - var evtPx = {xy: new OpenLayers.Pixel(Math.random(), Math.random())}; - - handler.feature = null; - - numEvents[evtShortName] = 0; - numEvents["over"] = 0; - numEvents["out"] = 0; - oldFeature = null; - newFeature = new OpenLayers.Feature.Vector(); - callbacks[evtShortName] = getCallback(evtShortName, newFeature); - callbacks["over"] = getCallback("over", newFeature); - callbacks["out"] = getCallback("out", oldFeature); - map.events.triggerEvent(evtLongName, evtPx); - t.ok(numEvents[evtShortName] == 1, evtShortName + " triggered click callback"); - t.ok(numEvents["over"] == 1, evtShortName + " triggered over callbacks"); - t.ok(numEvents["out"] == 0, evtShortName + " did not trigger out callback"); - - numEvents[evtShortName] = 0; - numEvents["over"] = 0; - numEvents["out"] = 0; - oldFeature = newFeature; - newFeature = new OpenLayers.Feature.Vector(); - callbacks[evtShortName] = getCallback(evtShortName, newFeature); - callbacks["over"] = getCallback("over", newFeature); - callbacks["out"] = getCallback("out", oldFeature); - map.events.triggerEvent(evtLongName, evtPx); - t.ok(numEvents[evtShortName] == 1, evtShortName + " triggered click callback"); - t.ok(numEvents["over"] == 1, evtShortName + " triggered over callbacks"); - t.ok(numEvents["out"] == 1, evtShortName + " triggered out callback"); - - numEvents[evtShortName] = 0; - numEvents["over"] = 0; - numEvents["out"] = 0; - oldFeature = newFeature; - callbacks[evtShortName] = getCallback(evtShortName, newFeature); - callbacks["over"] = getCallback("over", newFeature); - callbacks["out"] = getCallback("out", oldFeature); - map.events.triggerEvent(evtLongName, evtPx); - t.ok(numEvents[evtShortName] == 1, evtShortName + " triggered click callback"); - t.ok(numEvents["over"] == 0, evtShortName + " did not trigger over callbacks"); - t.ok(numEvents["out"] == 0, evtShortName + " did not trigger out callback"); - } + + // test click in new feature + // only 'click' callback should be called + handler.feature = null; + lastFeature = null; + newFeature = new OpenLayers.Feature.Vector(); + callbacks['click'] = getCallback('click', newFeature); + callbacks['clickout'] = getCallback('clickout', lastFeature); + evtPx.type = "click"; + map.events.triggerEvent('click', evtPx); + + // test click in new feature and out of last feature + // both 'click' and 'clickout' callbacks should be called + lastFeature = newFeature; + newFeature = new OpenLayers.Feature.Vector(); + callbacks['click'] = getCallback('click', newFeature); + callbacks['clickout'] = getCallback('clickout', lastFeature); + evtPx.type = "click"; + map.events.triggerEvent('click', evtPx); + + // test click out of last feature + // only 'clickout' callback should be called + lastFeature = newFeature; + newFeature = null; + callbacks['click'] = getCallback('click', newFeature); + callbacks['clickout'] = getCallback('clickout', lastFeature); + evtPx.type = "click"; + map.events.triggerEvent('click', evtPx); + + // test over a new feature + // only 'over' callback should be called + handler.feature = null; + lastFeature = null; + newFeature = new OpenLayers.Feature.Vector(); + callbacks['over'] = getCallback('over', newFeature); + callbacks['out'] = getCallback('out', lastFeature); + evtPx.type = "mousemove"; + map.events.triggerEvent('mousemove', evtPx); + + // test over a new feature and out of last feature + // both 'over' and 'out' callbacks should be called + lastFeature = newFeature; + newFeature = new OpenLayers.Feature.Vector(); + callbacks['over'] = getCallback('over', newFeature); + callbacks['out'] = getCallback('out', lastFeature); + evtPx.type = "mousemove"; + map.events.triggerEvent('mousemove', evtPx); + + // test out of last feature + // only 'out' callback should be called + lastFeature = newFeature; + newFeature = null; + callbacks['over'] = getCallback('over', newFeature); + callbacks['out'] = getCallback('out', lastFeature); + evtPx.type = "mousemove"; + map.events.triggerEvent('mousemove', evtPx); + + // test dblclick on a feature + // 'dblclick' callback should be called + handler.feature = null; + lastFeature = null; + newFeature = new OpenLayers.Feature.Vector(); + callbacks['dblclick'] = getCallback('dblclick', newFeature); + evtPx.type = "dblclick"; + map.events.triggerEvent('dblclick', evtPx); } function test_Handler_Feature_deactivate(t) {