From c15a4408ceb40563de1210f656617a6addb898d8 Mon Sep 17 00:00:00 2001 From: DAVID CLARK Date: Tue, 14 Jun 2016 10:25:24 -0700 Subject: [PATCH] Deselect deleted features; closes #357 It involved some refactoring to keep the selection in the store, accessible outside of simple_select. And other refactoring to improve testability and clarify the API of the store. --- package.json | 3 +- src/lib/mode_handler.js | 5 +- src/lib/simple_set.js | 31 ++++++ src/modes/direct_select.js | 1 + src/modes/draw_line_string.js | 1 + src/modes/draw_point.js | 1 + src/modes/draw_polygon.js | 1 + src/modes/simple_select.js | 56 +++++------ src/render.js | 12 +-- src/store.js | 172 +++++++++++++++++++++++++++------ test/simple_set.test.js | 66 +++++++++++++ test/store.test.js | 175 +++++++++++++++++++++++++++------- test/utils.js | 8 ++ 13 files changed, 431 insertions(+), 101 deletions(-) create mode 100644 src/lib/simple_set.js create mode 100644 test/simple_set.test.js diff --git a/package.json b/package.json index 5fb96479620..a2209519a1c 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,9 @@ ] }, "scripts": { - "test": "npm run lint && tape -r ./test/mock-browser.js -r babel-register test/*.test.js", + "test": "npm run lint && npm run tape", "lint": "eslint --no-eslintrc -c .eslintrc index.js src", + "tape": "tape -r ./test/mock-browser.js -r babel-register test/*.test.js", "build": "NODE_ENV=production browserify index.js > dist/mapbox-gl-draw.js", "prepublish": "NODE_ENV=production browserify index.js | uglifyjs -c -m > dist/mapbox-gl-draw.js", "start": "node server.js" diff --git a/src/lib/mode_handler.js b/src/lib/mode_handler.js index fd0934cc653..252c0e184f7 100644 --- a/src/lib/mode_handler.js +++ b/src/lib/mode_handler.js @@ -53,7 +53,10 @@ var ModeHandler = function(mode, DrawContext) { return { render: mode.render || function(geojson) {return geojson; }, - stop: mode.stop || function() {}, + stop: function() { + DrawContext.store.clearSelected(); + if (mode.stop) mode.stop(); + }, drag: function(event) { delegate('drag', event); }, diff --git a/src/lib/simple_set.js b/src/lib/simple_set.js new file mode 100644 index 00000000000..deab080e6d5 --- /dev/null +++ b/src/lib/simple_set.js @@ -0,0 +1,31 @@ +function SimpleSet(items) { + this._items = items || []; +} + +SimpleSet.prototype.add = function(item) { + if (this._items.indexOf(item) !== -1) return; + this._items.push(item); + return this; +}; + +SimpleSet.prototype.delete = function(item) { + var itemIndex = this._items.indexOf(item); + if (itemIndex === -1) return; + this._items.splice(itemIndex, 1); + return this; +}; + +SimpleSet.prototype.has = function(item) { + return this._items.indexOf(item) !== -1; +}; + +SimpleSet.prototype.values = function() { + return this._items; +}; + +SimpleSet.prototype.clear = function() { + this._items = []; + return this; +}; + +module.exports = SimpleSet; diff --git a/src/modes/direct_select.js b/src/modes/direct_select.js index 264155fd1a6..38669cf1b3a 100644 --- a/src/modes/direct_select.js +++ b/src/modes/direct_select.js @@ -45,6 +45,7 @@ module.exports = function(ctx, opts) { return { start: function() { + ctx.store.setSelected(featureId); ctx.map.doubleClickZoom.disable(); this.on('mousedown', isOfMetaType('vertex'), onVertex); this.on('mousedown', isOfMetaType('midpoint'), onMidpoint); diff --git a/src/modes/draw_line_string.js b/src/modes/draw_line_string.js index 8fe08029141..1f4bf37f353 100644 --- a/src/modes/draw_line_string.js +++ b/src/modes/draw_line_string.js @@ -52,6 +52,7 @@ module.exports = function(ctx) { return { start: function() { + ctx.store.setSelected(feature.id); setTimeout(() => { if (ctx.map && ctx.map.doubleClickZoom) { ctx.map.doubleClickZoom.disable(); diff --git a/src/modes/draw_point.js b/src/modes/draw_point.js index 57ac739859d..330db29b99a 100644 --- a/src/modes/draw_point.js +++ b/src/modes/draw_point.js @@ -30,6 +30,7 @@ module.exports = function(ctx) { return { start: function() { + ctx.store.setSelected(feature.id); ctx.ui.setClass({mouse:'add'}); ctx.ui.setButtonActive(types.POINT); this.on('click', () => true, onClick); diff --git a/src/modes/draw_polygon.js b/src/modes/draw_polygon.js index 085ae196a10..35dddb0cf66 100644 --- a/src/modes/draw_polygon.js +++ b/src/modes/draw_polygon.js @@ -53,6 +53,7 @@ module.exports = function(ctx) { return { start: function() { + ctx.store.setSelected(feature.id); setTimeout(() => { if (ctx.map && ctx.map.doubleClickZoom) { ctx.map.doubleClickZoom.disable(); diff --git a/src/modes/simple_select.js b/src/modes/simple_select.js index 388d7b9c003..eb8d4803848 100644 --- a/src/modes/simple_select.js +++ b/src/modes/simple_select.js @@ -2,13 +2,7 @@ var {noFeature, isShiftDown, isFeature, isOfMetaType, isBoxSelecting, isActiveFe var { DOM } = require('../lib/util'); var featuresAt = require('../lib/features_at'); var addCoords = require('../lib/add_coords'); -module.exports = function(ctx, startingSelectedFeatureIds) { - - var selectedFeaturesById = {}; - (startingSelectedFeatureIds || []).forEach(id => { - selectedFeaturesById[id] = ctx.store.get(id); - }); - +module.exports = function(ctx, startingSelectedIds) { var startPos = null; var dragging = null; var featureCoords = null; @@ -36,11 +30,11 @@ module.exports = function(ctx, startingSelectedFeatureIds) { var featuresInBox = featuresAt(null, bbox, ctx); if (featuresInBox.length >= 1000) return ctx.map.dragPan.enable(); var ids = getUniqueIds(featuresInBox) - .filter(id => !isSelected(id)); + .filter(id => !ctx.store.isSelected(id)); if (ids.length) { ids.forEach(id => { - selectedFeaturesById[id] = ctx.store.get(id); + ctx.store.select(id); context.render(id); }); context.fire('selected.start', {featureIds: ids}); @@ -56,15 +50,16 @@ module.exports = function(ctx, startingSelectedFeatureIds) { var readyForDirectSelect = function(e) { if (isFeature(e)) { var about = e.featureTarget.properties; - return selectedFeaturesById[about.id] !== undefined && selectedFeaturesById[about.id].type !== 'Point'; + return ctx.store.isSelected(about.id) + && ctx.store.get(about.id).type !== 'Point'; } return false; }; var buildFeatureCoords = function() { - var featureIds = Object.keys(selectedFeaturesById); - featureCoords = featureIds.map(id => selectedFeaturesById[id].coordinates); - features = featureIds.map(id => selectedFeaturesById[id]); + var featureIds = ctx.store.getSelectedIds(); + featureCoords = featureIds.map(id => ctx.store.get(id).coordinates); + features = featureIds.map(id => ctx.store.get(id)); numFeatures = featureIds.length; }; @@ -74,19 +69,19 @@ module.exports = function(ctx, startingSelectedFeatureIds) { }); }; - var isSelected = function(id) { - return selectedFeaturesById[id] !== undefined; - }; - return { stop: function() { ctx.map.doubleClickZoom.enable(); }, start: function() { + if (ctx.store) { + ctx.store.setSelected(startingSelectedIds); + } + dragging = false; this.on('click', noFeature, function() { - var wasSelected = Object.keys(selectedFeaturesById); - selectedFeaturesById = {}; + var wasSelected = ctx.store.getSelectedIds(); + ctx.store.clearSelected(); this.fire('selected.end', {featureIds: wasSelected}); wasSelected.forEach(id => this.render(id)); ctx.map.doubleClickZoom.enable(); @@ -118,16 +113,16 @@ module.exports = function(ctx, startingSelectedFeatureIds) { this.on('click', isFeature, function(e) { ctx.map.doubleClickZoom.disable(); var id = e.featureTarget.properties.id; - var featureIds = Object.keys(selectedFeaturesById); - if (isSelected(id) && !isShiftDown(e)) { + var featureIds = ctx.store.getSelectedIds(); + if (ctx.store.isSelected(id) && !isShiftDown(e)) { if (featureIds.length > 1) { this.fire('selected.end', {featureIds: featureIds.filter(f => f !== id)}); } this.on('click', readyForDirectSelect, directSelect); ctx.ui.setClass({mouse:'pointer'}); } - else if (isSelected(id) && isShiftDown(e)) { - delete selectedFeaturesById[id]; + else if (ctx.store.isSelected(id) && isShiftDown(e)) { + ctx.store.deselect(id); this.fire('selected.end', {featureIds: [id]}); ctx.ui.setClass({mouse:'pointer'}); this.render(id); @@ -135,18 +130,18 @@ module.exports = function(ctx, startingSelectedFeatureIds) { ctx.map.doubleClickZoom.enable(); } } - else if (!isSelected(id) && isShiftDown(e)) { + else if (!ctx.store.isSelected(id) && isShiftDown(e)) { // add to selected - selectedFeaturesById[id] = ctx.store.get(id); + ctx.store.select(id); this.fire('selected.start', {featureIds: [id]}); ctx.ui.setClass({mouse:'move'}); this.render(id); } - else if (!isSelected(id) && !isShiftDown(e)) { + else if (!ctx.store.isSelected(id) && !isShiftDown(e)) { // make selected featureIds.forEach(formerId => this.render(formerId)); - selectedFeaturesById = {}; - selectedFeaturesById[id] = ctx.store.get(id); + ctx.store.clearSelected(); + ctx.store.select(id); ctx.ui.setClass({mouse:'move'}); this.fire('selected.end', {featureIds: featureIds}); this.fire('selected.start', {featureIds: [id]}); @@ -225,12 +220,11 @@ module.exports = function(ctx, startingSelectedFeatureIds) { featureCoords = null; features = null; numFeatures = null; - ctx.store.delete(Object.keys(selectedFeaturesById)); - selectedFeaturesById = {}; + ctx.store.delete(ctx.store.getSelectedIds()); }); }, render: function(geojson, push) { - geojson.properties.active = selectedFeaturesById[geojson.properties.id] ? 'true' : 'false'; + geojson.properties.active = ctx.store.isSelected(geojson.properties.id) ? 'true' : 'false'; if (geojson.properties.active === 'true' && geojson.geometry.type !== 'Point') { addCoords(geojson, false, push, ctx.map, []); } diff --git a/src/render.js b/src/render.js index 0cb8296a15e..1b549ba96a1 100644 --- a/src/render.js +++ b/src/render.js @@ -10,12 +10,12 @@ module.exports = function render() { var newColdIds = []; if (this.isDirty) { - newColdIds = this.featureIds; + newColdIds = this.getAllIds(); } else { - newHotIds = this.changedIds.filter(id => this.features[id] !== undefined); + newHotIds = this.getChangedIds().filter(id => this.get(id) !== undefined); newColdIds = this.sources.hot.filter(function getColdIds(geojson) { - return geojson.properties.id && newHotIds.indexOf(geojson.properties.id) === -1 && this.features[geojson.properties.id] !== undefined; + return geojson.properties.id && newHotIds.indexOf(geojson.properties.id) === -1 && this.get(geojson.properties.id) !== undefined; }.bind(this)).map(geojson => geojson.properties.id); } @@ -37,7 +37,7 @@ module.exports = function render() { } }).forEach(function calculateViewUpdate(change) { let {id, source} = change; - let feature = this.features[id]; + let feature = this.get(id); let featureInternal = feature.internal(mode); this.ctx.events.currentModeRender(featureInternal, function addGeoJsonToView(geojson) { @@ -57,7 +57,7 @@ module.exports = function render() { features: this.sources.hot }); - let changed = this.changedIds.map(id => this.features[id]) + let changed = this.getChangedIds().map(id => this.get(id)) .filter(feature => feature !== undefined) .filter(feature => feature.isValid()) .map(feature => feature.toGeoJSON()); @@ -68,5 +68,5 @@ module.exports = function render() { } this.isDirty = false; - this.changedIds = []; + this.clearChangedIds(); }; diff --git a/src/store.js b/src/store.js index e88fc3b8afd..65daea11341 100644 --- a/src/store.js +++ b/src/store.js @@ -1,61 +1,177 @@ var {throttle} = require('./lib/util'); +var SimpleSet = require('./lib/simple_set'); var render = require('./render'); var Store = module.exports = function(ctx) { + this._features = {}; + this._featureIds = new SimpleSet(); + this._selectedFeatureIds = new SimpleSet(); + this._changedIds = new SimpleSet(); this.ctx = ctx; - this.features = {}; - this.featureIds = []; this.sources = { hot: [], cold: [] }; this.render = throttle(render, 16, this); - this.isDirty = false; - this.changedIds = []; }; +/** + * Sets the store's state to dirty. + * @return {Store} this + */ Store.prototype.setDirty = function() { this.isDirty = true; + return this; }; -Store.prototype.featureChanged = function(id) { - if (this.changedIds.indexOf(id) === -1) { - this.changedIds.push(id); - } +/** + * Sets a feature's state to changed. + * @param {string} featureId + * @return {Store} this + */ +Store.prototype.featureChanged = function(featureId) { + this._changedIds.add(featureId); + return this; }; -Store.prototype.add = function(feature) { - this.featureChanged(feature.id); - this.features[feature.id] = feature; - if (this.featureIds.indexOf(feature.id) === -1) { - this.featureIds.push(feature.id); - } - return feature.id; +/** + * Gets the ids of all features currently in changed state. + * @return {Store} this + */ +Store.prototype.getChangedIds = function() { + return this._changedIds.values(); }; -Store.prototype.get = function(id) { - return this.features[id]; +/** + * Sets all features to unchanged state. + * @return {Store} this + */ +Store.prototype.clearChangedIds = function() { + this._changedIds.clear(); + return this; }; -Store.prototype.getAll = function() { - return Object.keys(this.features).map(id => this.features[id]); +/** + * Gets the ids of all features in the store. + * @return {Store} this + */ +Store.prototype.getAllIds = function() { + return this._featureIds.values(); }; -Store.prototype.delete = function (ids) { +/** + * Adds a feature to the store. + * @param {Object} feature + * @return {Store} this + */ +Store.prototype.add = function(feature) { + this.featureChanged(feature.id); + this._features[feature.id] = feature; + this._featureIds.add(feature.id); + return this; +}; + +/** + * Deletes a feature or array of features from the store. + * Cleans up after the deletion by deselecting the features. + * If changes were made, sets the state to the dirty + * and fires an event. + * @param {string | Array} featureIds + * @return {Store} this + */ +Store.prototype.delete = function(featureIds) { var deleted = []; - ids.forEach((id) => { - var idx = this.featureIds.indexOf(id); - if (idx !== -1) { - var feature = this.get(id); - deleted.push(feature.toGeoJSON()); - delete this.features[id]; - this.featureIds.splice(idx, 1); - } + [].concat(featureIds).forEach((id) => { + if (!this._featureIds.has(id)) return; + var feature = this.get(id); + deleted.push(feature.toGeoJSON()); + // Must deselect the feature as well as delete it + this.deselect(id); + delete this._features[id]; + this._featureIds.delete(id); }); if (deleted.length > 0) { this.isDirty = true; this.ctx.map.fire('draw.deleted', {featureIds:deleted}); } + return this; +}; + +/** + * Returns a feature in the store matching the specified value. + * @return {Object | undefined} feature + */ +Store.prototype.get = function(id) { + return this._features[id]; +}; + +/** + * Returns all features in the store. + * @return {Array} + */ +Store.prototype.getAll = function() { + return Object.keys(this._features).map(id => this._features[id]); +}; + +/** + * Adds a feature to the current selection. + * @param {string} featureId + * @return {Store} this + */ +Store.prototype.select = function(featureId) { + this._selectedFeatureIds.add(featureId); + return this; +}; + +/** + * Deletes a feature from the current selection. + * @param {string} featureId + * @return {Store} this + */ +Store.prototype.deselect = function(featureId) { + this._selectedFeatureIds.delete(featureId); + return this; +}; + +/** + * Clears the current selection. + * @return {Store} this + */ +Store.prototype.clearSelected = function() { + this._selectedFeatureIds.clear(); + return this; +}; + +/** + * Sets the store's selection, clearing any prior values. + * If no feature ids are passed, the store is just cleared. + * @param {string | Array | undefined} featureIds + * @return {Store} this + */ +Store.prototype.setSelected = function(featureIds) { + this.clearSelected(); + if (!featureIds) return; + [].concat(featureIds).forEach(id => { + this.select(id); + }); + return this; +}; + +/** + * Returns the ids of features in the current selection. + * @return {Array} Selected feature ids. + */ +Store.prototype.getSelectedIds = function() { + return this._selectedFeatureIds.values(); +}; + +/** + * Indicates whether a feature is selected. + * @param {string} featureId + * @return {boolean} `true` if the feature is selected, `false` if not + */ +Store.prototype.isSelected = function(featureId) { + return this._selectedFeatureIds.has(featureId); }; diff --git a/test/simple_set.test.js b/test/simple_set.test.js new file mode 100644 index 00000000000..cd7ebd106d2 --- /dev/null +++ b/test/simple_set.test.js @@ -0,0 +1,66 @@ +import test from 'tape'; +import SimpleSet from '../src/lib/simple_set'; + +test('SimpleSet constructor and API', t => { + const set = new SimpleSet(); + t.deepEqual(set.values(), [], 'empty by default'); + t.equal(Object.keys(set).filter(k => k[0] !== '_').length, 0, 'no unexpected properties'); + // Methods + t.equal(typeof SimpleSet.prototype.add, 'function', 'exposes set.add'); + t.equal(typeof SimpleSet.prototype.delete, 'function', 'exposes set.delete'); + t.equal(typeof SimpleSet.prototype.has, 'function', 'exposes set.has'); + t.equal(typeof SimpleSet.prototype.values, 'function', 'exposes set.values'); + t.equal(typeof SimpleSet.prototype.clear, 'function', 'exposes set.clear'); + t.equal(Object.keys(SimpleSet.prototype).filter(k => k[0] !== '_').length, 5, 'no unexpected methods'); + + const populatedSet = new SimpleSet([1, 2]); + t.deepEqual(populatedSet.values(), [1, 2], 'populated by constructor arg'); + + t.end(); +}); + +test('SimpleSet#add', t => { + const set = new SimpleSet(); + t.deepEqual(set.values(), []); + set.add(1); + t.deepEqual(set.values(), [1]); + set.add(2); + t.deepEqual(set.values(), [1, 2]); + set.add(1); + t.deepEqual(set.values(), [1, 2]); + t.end(); +}); + +test('SimpleSet#delete', t => { + const set = new SimpleSet([1, 2]); + set.delete(1); + t.deepEqual(set.values(), [2]); + set.delete(1); + t.deepEqual(set.values(), [2]); + set.delete(); + t.deepEqual(set.values(), [2]); + set.delete(2); + t.deepEqual(set.values(), []); + t.end(); +}); + +test('SimpleSet#has', t => { + const set = new SimpleSet([1, 2]); + t.equal(set.has(1), true); + t.equal(set.has(2), true); + t.equal(set.has(3), false); + t.end(); +}); + +test('SimpleSet#values', t => { + const set = new SimpleSet([1, 2]); + t.deepEqual(set.values(), [1, 2]); + t.end(); +}); + +test('SimpleSet#clear', t => { + const set = new SimpleSet([1, 2]); + set.clear(); + t.deepEqual(set.values(), []); + t.end(); +}); diff --git a/test/store.test.js b/test/store.test.js index 84d204f8efb..359c1c2a2be 100644 --- a/test/store.test.js +++ b/test/store.test.js @@ -1,13 +1,13 @@ /* eslint no-shadow:[0] */ import test from 'tape'; import Store from '../src/store'; -import Point from '../src/feature_types/point'; -import mapboxgl from 'mapbox-gl-js-mock'; -import { accessToken, createMap, features } from './utils'; -import hat from 'hat'; +import { createMap, createFeature } from './utils'; -var feature = JSON.parse(JSON.stringify(features.point)); -feature.id = hat(); +function createStore() { + const map = createMap(); + const ctx = { map }; + return new Store(ctx); +} test('Store has correct properties', t => { t.ok(Store, 'store exists'); @@ -15,42 +15,149 @@ test('Store has correct properties', t => { t.end(); }); -var map = createMap(); +test('Store constructor and public API', t => { + const map = createMap(); + const ctx = { map }; + const store = new Store(ctx); -test('Store constructor', t => { + // instance members + t.deepEqual(store.sources, { + hot: [], + cold: [] + }, 'exposes store.sources'); + t.equal(store.ctx, ctx, 'exposes store.ctx'); + t.equal(store.isDirty, false, 'exposes store.isDirty'); + t.equal(typeof store.render, 'function', 'exposes store.render'); - var store = new Store({map: map}); + t.equal(Object.keys(store).filter(k => k[0] !== '_').length, 4, 'no unexpected instance members'); - var f; + // prototype members + t.equal(typeof Store.prototype.setDirty, 'function', 'exposes store.setDirty'); + t.equal(typeof Store.prototype.featureChanged, 'function', 'exposes store.featureChanged'); + t.equal(typeof Store.prototype.getChangedIds, 'function', 'exposes store.getChangedIds'); + t.equal(typeof Store.prototype.clearChangedIds, 'function', 'exposes store.clearChangedIds'); + t.equal(typeof Store.prototype.getAllIds, 'function', 'exposes store.getAllIds'); + t.equal(typeof Store.prototype.add, 'function', 'exposes store.add'); + t.equal(typeof Store.prototype.get, 'function', 'exposes store.get'); + t.equal(typeof Store.prototype.getAll, 'function', 'exposes store.getAll'); + t.equal(typeof Store.prototype.select, 'function', 'exposes store.select'); + t.equal(typeof Store.prototype.deselect, 'function', 'exposes store.deselect'); + t.equal(typeof Store.prototype.clearSelected, 'function', 'exposes store.clearSelected'); + t.equal(typeof Store.prototype.getSelectedIds, 'function', 'exposes store.getSelectedIds'); + t.equal(typeof Store.prototype.isSelected, 'function', 'exposes store.isSelected'); + t.equal(typeof Store.prototype.delete, 'function', 'exposes store.delete'); + t.equal(typeof Store.prototype.setSelected, 'function', 'exposes store.setSelected'); - t.test('set', t => { - var id = store.add(new Point({store: store}, feature)); - f = store.get(id); - t.deepEquals(f.coordinates, feature.geometry.coordinates, 'you can set a feature'); - t.end(); - }); + t.equal(Object.keys(Store.prototype).filter(k => k[0] !== '_').length, 15, 'no untested prototype members'); - t.test('get', t => { - var storeFeat = store.get(f.id); - t.deepEqual( - storeFeat.toGeoJSON().geometry, feature.geometry, - 'get returns the same geometry you set'); - t.end(); - }); + t.end(); +}); - t.test('getAll', t => { - var storeFeat = store.getAll(); - t.deepEqual( - storeFeat[0].toGeoJSON().geometry, feature.geometry, - 'get returns the same geometry you set'); - t.end(); - }); +test('Store#setDirty', t => { + const store = createStore(); + t.equal(store.isDirty, false); + store.setDirty(); + t.equal(store.isDirty, true); + t.end(); +}); + +test('Store#featureChanged, Store#getChangedIds, Store#clearChangedIds', t => { + const store = createStore(); + t.deepEqual(store.getChangedIds(), []); + store.featureChanged('x'); + t.deepEqual(store.getChangedIds(), ['x']); + store.featureChanged('y'); + t.deepEqual(store.getChangedIds(), ['x', 'y']); + store.featureChanged('x'); + t.deepEqual(store.getChangedIds(), ['x', 'y'], 'ids do not duplicate'); + store.clearChangedIds(); + t.deepEqual(store.getChangedIds(), []); + t.end(); +}); + +test('Store#add, Store#get, Store#getAll', t => { + const store = createStore(); + t.equal(store.get(1), undefined); + t.deepEqual(store.getAll(), []); + const point = createFeature('point'); + const line = createFeature('line'); + store.add(point); + t.equal(store.get(point.id), point); + t.deepEqual(store.getAll(), [point]); + store.add(line); + t.equal(store.get(point.id), point); + t.equal(store.get(line.id), line); + t.deepEqual(store.getAll(), [point, line]); + store.add(point); + t.equal(store.get(point.id), point); + t.equal(store.get(line.id), line); + t.deepEqual(store.getAll(), [point, line]); + t.end(); +}); + +test('Store#select, Store#deselect, Store#getSelectedIds, Store#isSelected, Store#isSelected', t => { + const store = createStore(); + t.deepEqual(store.getSelectedIds(), []); + store.select(1); + t.deepEqual(store.getSelectedIds(), [1]); + t.equal(store.isSelected(1), true); + t.equal(store.isSelected(2), false); + store.select(2); + t.deepEqual(store.getSelectedIds(), [1, 2]); + t.equal(store.isSelected(1), true); + t.equal(store.isSelected(2), true); + store.select(1); + t.deepEqual(store.getSelectedIds(), [1, 2]); + store.deselect(1); + t.deepEqual(store.getSelectedIds(), [2]); + store.select(3); + store.select(4); + t.deepEqual(store.getSelectedIds(), [2, 3, 4]); + store.clearSelected(); + t.deepEqual(store.getSelectedIds(), []); + t.end(); +}); + +test('Store#delete', t => { + const store = createStore(); + const point = createFeature('point'); + const line = createFeature('line'); + const polygon = createFeature('polygon'); + + store.add(point); + store.add(line); + store.add(polygon); + t.deepEqual(store.getAll(), [point, line, polygon]); + t.deepEqual(store.getAllIds(), [point.id, line.id, polygon.id]); - t.test('delete', t => { - store.delete([f.id]); - t.equals(store.getAll().length, 0, 'calling delete removes the feature'); - t.end(); + t.deepEqual(store.getSelectedIds(), []); + store.select(line.id); + t.deepEqual(store.getSelectedIds(), [line.id]); + + let deletedFeatureIdsReported = []; + store.ctx.map.on('draw.deleted', data => { + deletedFeatureIdsReported = data.featureIds; }); + store.delete(line.id); + t.deepEqual(store.getAll(), [point, polygon]); + t.deepEqual(store.getAllIds(), [point.id, polygon.id]); + t.deepEqual(store.getSelectedIds(), []); + + t.equal(store.isDirty, true, 'after deletion store is dirty'); + t.deepEqual(deletedFeatureIdsReported, [line], 'draw.deleted event fires with data'); + + t.end(); +}); + +test('Store#setSelected', t => { + const store = createStore(); + t.deepEqual(store.getSelectedIds(), []); + store.setSelected(1); + t.deepEqual(store.getSelectedIds(), [1]); + store.setSelected([3, 4]); + t.deepEqual(store.getSelectedIds(), [3, 4]); + store.setSelected(); + t.deepEqual(store.getSelectedIds(), []); t.end(); }); diff --git a/test/utils.js b/test/utils.js index 6fa8f07539c..c4ecfb8b5c2 100644 --- a/test/utils.js +++ b/test/utils.js @@ -1,4 +1,5 @@ import mapboxgl from 'mapbox-gl-js-mock'; +import hat from 'hat'; export function createMap() { @@ -76,3 +77,10 @@ export const features = { } }; + +export function createFeature(featureType) { + const feature = features[featureType]; + feature.id = hat(); + feature.toGeoJSON = () => feature; + return feature; +}