diff --git a/CHANGELOG.md b/CHANGELOG.md index 15d73f42bc..c03043fe34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +### Features +- Feature selection API is now enabled automatically if any event handlers are bounds to the feature (#921) + ### Changes - Removed the dependency on the vgl module for the `object` and `timestamp` classes (#918) diff --git a/src/feature.js b/src/feature.js index 8424c7c705..f2e018bb5b 100644 --- a/src/feature.js +++ b/src/feature.js @@ -9,9 +9,11 @@ var geo_event = require('./event'); * * @typedef {object} geo.feature.spec * @property {geo.layer} [layer] the parent layer associated with the feature. - * @property {boolean} [selectionAPI=false] If truthy, enable selection events - * on the feature. Selection events are those in {@link geo.event.feature}. - * They can be bound via a call like + * @property {boolean|'auto'} [selectionAPI='auto'] If `'auto'`, enable + * selection events if any {@link geo.event.feature} events are bound to the + * feature. Otherwise, if truthy, enable selection events on the feature. + * Selection events are those in {@link geo.event.feature}. They can be + * bound via a call like * ``` * feature.geoOn(geo.event.feature.mousemove, function (evt) { * // do something with the feature @@ -111,8 +113,10 @@ var feature = function (arg) { var m_this = this, s_exit = this._exit, + s_geoOn = this.geoOn, + s_geoOff = this.geoOff, m_ready, - m_selectionAPI = arg.selectionAPI === undefined ? false : !!arg.selectionAPI, + m_selectionAPI = arg.selectionAPI === undefined ? 'auto' : arg.selectionAPI, m_style = {}, m_layer = arg.layer === undefined ? null : arg.layer, m_gcs = arg.gcs, @@ -772,22 +776,29 @@ var feature = function (arg) { /** * Get/Set if the selection API is enabled for this feature. * - * @param {boolean} [arg] `undefined` to return the selectionAPI state, or a - * boolean to change the state. + * @param {boolean|string} [arg] `undefined` to return the selectionAPI + * state, a boolean to change the state, or `'auto'` to set the state + * based on the existence of event handlers. When getting the state, if + * `direct` is not specified, `'auto'` is never returned. * @param {boolean} [direct] If `true`, when getting the selectionAPI state, * disregard the state of the parent layer, and when setting, refresh the * state regardless of whether it has changed or not. - * @returns {boolean|this} Either the selectionAPI state (if getting) or the - * feature (if setting). + * @returns {boolean|string|this} Either the selectionAPI state or the + * feature instance. */ this.selectionAPI = function (arg, direct) { if (arg === undefined) { if (!direct && m_layer && m_layer.selectionAPI && !m_layer.selectionAPI()) { return false; } + if (!direct && m_selectionAPI === 'auto') { + return !!m_this.geoIsOn(Object.values(geo_event.feature)); + } return m_selectionAPI; } - arg = !!arg; + if (arg !== 'auto') { + arg = !!arg; + } if (arg !== m_selectionAPI || direct) { m_selectionAPI = arg; m_this._unbindMouseHandlers(); @@ -848,6 +859,47 @@ var feature = function (arg) { this._update = function () { }; + /** + * Bind an event handler to this object. + * + * @param {string} event An event from {@link geo.event} or a user-defined + * value. + * @param {function} handler A function that is called when `event` is + * triggered. The function is passed a {@link geo.event} object. + * @returns {this} + */ + this.geoOn = function (event, handler) { + var isAuto = m_this.selectionAPI(undefined, true) === 'auto', + selection = isAuto && m_this.selectionAPI(); + var result = s_geoOn.apply(m_this, arguments); + if (isAuto && !selection && m_this.selectionAPI()) { + m_this._bindMouseHandlers(); + } + return result; + }; + + /** + * Remove handlers from one event or an array of events. If no event is + * provided all handlers will be removed. + * + * @param {string|string[]} [event] An event or a list of events from + * {@link geo.event} or defined by the user, or `undefined` to remove all + * events (in which case `arg` is ignored). + * @param {(function|function[])?} [arg] A function or array of functions to + * remove from the events or a falsy value to remove all handlers from the + * events. + * @returns {this} + */ + this.geoOff = function (event, arg) { + var isAuto = m_this.selectionAPI(undefined, true) === 'auto', + selection = isAuto && m_this.selectionAPI(); + var result = s_geoOff.apply(m_this, arguments); + if (isAuto && selection && !m_this.selectionAPI()) { + m_this._unbindMouseHandlers(); + } + return result; + }; + /** * Destroy. Unbind mouse handlers, clear internal variables, and call the * parent destroy method. diff --git a/tests/cases/feature.js b/tests/cases/feature.js index 3f421e4237..b4fae4fbac 100644 --- a/tests/cases/feature.js +++ b/tests/cases/feature.js @@ -59,6 +59,7 @@ describe('geo.feature', function () { feat = geo.feature({layer: layer}); expect(feat.style('opacity')).toBe(1); expect(feat.selectionAPI()).toBe(false); + expect(feat.selectionAPI(undefined, true)).toBe('auto'); }); it('_exit', function () { feat = geo.feature({layer: layer}); @@ -338,6 +339,8 @@ describe('geo.feature', function () { expect(feat.style('data')).toEqual([1]); }); it('selectionAPI', function () { + var bindSpy, unbindSpy; + expect(feat.selectionAPI()).toBe(false); feat = geo.feature({layer: layer, renderer: layer.renderer(), selectionAPI: true}); expect(feat.selectionAPI()).toBe(true); @@ -362,6 +365,25 @@ describe('geo.feature', function () { expect(feat.selectionAPI()).toBe(false); expect(feat.selectionAPI(true, true)).toBe(feat); expect(feat.selectionAPI()).toBe(true); + + // auto will change the selection mode depending on bound events + bindSpy = sinon.spy(feat, '_bindMouseHandlers'); + unbindSpy = sinon.spy(feat, '_unbindMouseHandlers'); + expect(feat.selectionAPI('auto')).toBe(feat); + expect(feat.selectionAPI()).toBe(false); + expect(feat.selectionAPI(undefined, true)).toBe('auto'); + expect(bindSpy.callCount).toBe(1); + expect(unbindSpy.callCount).toBe(1); + feat.geoOn(geo.event.feature.mouseover, function () {}); + expect(feat.selectionAPI()).toBe(true); + expect(feat.selectionAPI(undefined, true)).toBe('auto'); + expect(bindSpy.callCount).toBe(2); + expect(unbindSpy.callCount).toBe(2); + feat.geoOff(geo.event.feature.mouseover); + expect(feat.selectionAPI()).toBe(false); + expect(feat.selectionAPI(undefined, true)).toBe('auto'); + expect(bindSpy.callCount).toBe(2); + expect(unbindSpy.callCount).toBe(3); }); it('ready', function () { feat = geo.feature({layer: layer});