From 59a1a1ed7149f8d91e4f512ff62753abd67c4ecb Mon Sep 17 00:00:00 2001 From: Robert Thau Date: Fri, 10 Oct 2014 17:05:10 -0400 Subject: [PATCH 01/56] Add "Export Data" button, which shows the geoJSON in a new window. Putting modified data in a new browser window is about the only thing a "save" button can do in a purely client-side framework; it's then up to the user to put it where it can do some good. Problems: *) button needs better placement *) button should be suppressed if editing not configured *) generated output is not pretty-printed, and fugly as hell. Still, going for proof of concept here, and this is it. --- index.html | 5 ++++- src/script.js | 1 + src/ui/export.js | 25 +++++++++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 src/ui/export.js diff --git a/index.html b/index.html index 95e20c9..fcde52d 100644 --- a/index.html +++ b/index.html @@ -32,7 +32,7 @@ - + diff --git a/src/script.js b/src/script.js index c8913eb..aa0fe8f 100644 --- a/src/script.js +++ b/src/script.js @@ -40,6 +40,7 @@ define(function(require) { require('ui/search_results').attachTo('#search-results'); require('ui/info').attachTo('#info'); require('ui/facet').attachTo('#facets'); + require('ui/export').attachTo('#export'); require('data/facet').attachTo(document); require('ui/project').attachTo(document); require('data/analytics').attachTo(document); diff --git a/src/ui/export.js b/src/ui/export.js new file mode 100644 index 0000000..7f3e8a6 --- /dev/null +++ b/src/ui/export.js @@ -0,0 +1,25 @@ +define(function(require, exports, module) { + 'use strict'; + var flight = require('flight'); + var $ = require('jquery'); + var _ = require('lodash'); + + module.exports = flight.component(function project() { + + this.noteData = function(ev, data) { + this.data = data; + } + + this.doExport = function(ev) { + var uri; + ev.preventDefault(); + uri = 'data:text/plain,'+encodeURIComponent(JSON.stringify(this.data)); + window.open(uri, "Exported Finda Data") + } + + this.after('initialize', function() { + this.on(document, 'data', this.noteData); + this.on('submit', this.doExport); + }); + }); +}); From 17679879b6f54b713546242a1088f748d2889764 Mon Sep 17 00:00:00 2001 From: Robert Thau Date: Fri, 10 Oct 2014 17:31:18 -0400 Subject: [PATCH 02/56] Export data with half-decent indentation. Turns out this was *not* hard. OK, then. --- src/ui/export.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ui/export.js b/src/ui/export.js index 7f3e8a6..4d7b1bf 100644 --- a/src/ui/export.js +++ b/src/ui/export.js @@ -11,9 +11,10 @@ define(function(require, exports, module) { } this.doExport = function(ev) { - var uri; + var uri, json; ev.preventDefault(); - uri = 'data:text/plain,'+encodeURIComponent(JSON.stringify(this.data)); + json = JSON.stringify(this.data, undefined, 2); + uri = 'data:text/plain,'+encodeURIComponent(json); window.open(uri, "Exported Finda Data") } From 45a657af382db0c6adadf654375c9ca5ccb3bec5 Mon Sep 17 00:00:00 2001 From: Robert Thau Date: Sat, 11 Oct 2014 21:33:52 -0400 Subject: [PATCH 03/56] Add "edit-mode" config flag, to enable/disable editing features. --- config.json | 3 ++- src/ui/export.js | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/config.json b/config.json index 890bcce..d94c0e9 100644 --- a/config.json +++ b/config.json @@ -58,5 +58,6 @@ "hostname": "auto", "detail_enabled": true }, - "geojson_source": "data.geojson" + "geojson_source": "data.geojson", + "edit_mode": true } diff --git a/src/ui/export.js b/src/ui/export.js index 4d7b1bf..b5c6656 100644 --- a/src/ui/export.js +++ b/src/ui/export.js @@ -4,10 +4,21 @@ define(function(require, exports, module) { var $ = require('jquery'); var _ = require('lodash'); - module.exports = flight.component(function project() { + module.exports = flight.component(function () { + + this.configure = function(ev, config) { + if (!config.edit_mode) { + this.$node.hide(); + } + } this.noteData = function(ev, data) { - this.data = data; + + // Squirrel away a copy of the whole in-core data block, + // with the idea that it will be modified by subsequent + // operations, and be available for export... + + this.data = data; } this.doExport = function(ev) { @@ -19,6 +30,7 @@ define(function(require, exports, module) { } this.after('initialize', function() { + this.on(document, 'config', this.configure); this.on(document, 'data', this.noteData); this.on('submit', this.doExport); }); From 4f585d71d67c8c89f7f2aef9bc9917b046afbe92 Mon Sep 17 00:00:00 2001 From: Robert Thau Date: Sat, 11 Oct 2014 22:13:59 -0400 Subject: [PATCH 04/56] Specs for the "export data" widget support code. --- test/mock.js | 3 ++- test/spec/ui/export_spec.js | 53 +++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 test/spec/ui/export_spec.js diff --git a/test/mock.js b/test/mock.js index 704b4c9..eda2c3e 100644 --- a/test/mock.js +++ b/test/mock.js @@ -37,7 +37,8 @@ define({ title: "Services", type: "list" } - } + }, + "edit_mode": false }, "data": { diff --git a/test/spec/ui/export_spec.js b/test/spec/ui/export_spec.js new file mode 100644 index 0000000..616f29d --- /dev/null +++ b/test/spec/ui/export_spec.js @@ -0,0 +1,53 @@ +define(['test/mock', 'jquery'], function(mock, $) { + + 'use strict'; + + describeComponent('ui/export', function() { + beforeEach(function() { + setupComponent('
foo:
'); + }); + + afterEach(function() { + mock.config.edit_mode = false; + }); + + describe('on config', function() { + + it('hides widget if edit-mode is off', function() { + spyOn(this.$node, 'hide'); + $(document).trigger('config', mock.config); + expect(this.$node.hide).toHaveBeenCalled(); + }); + + it('leaves widget shown if edit-mode is on', function() { + spyOn(this.$node, 'hide'); + mock.config.edit_mode = true; + $(document).trigger('config', mock.config); + expect(this.$node.hide).not.toHaveBeenCalled(); + }); + }); + + describe('on export', function() { + it('does an export when triggered', function() { + + spyOn(window, 'open'); + this.component.trigger('data', mock.data); + + this.$node.trigger('submit'); + expect(window.open).toHaveBeenCalled(); + + // Verify that the args are as expected... + // For the URL, we just check that it has the right prefix, and + // *some* of the expected text. + + var args = window.open.mostRecentCall.args; + expect(args[1]).toEqual("Exported Finda Data"); + + var url = args[0]; + expect(url).toMatch(/^data:text\/plain,/); + expect(url).toMatch(/our-groups-programs/); + }); + }); + + }); +}); \ No newline at end of file From b1e802b2afc616983e3238903cbb9d3fd688628e Mon Sep 17 00:00:00 2001 From: Robert Thau Date: Thu, 16 Oct 2014 17:27:45 -0400 Subject: [PATCH 05/56] Create 'edit_state' object in data to manage, well, state of edits. --- src/data/edit_state.js | 34 +++++++++++++++++++++++++++++++ src/script.js | 1 + src/ui/export.js | 22 ++++++++------------ test/spec/data/edit_state_spec.js | 24 ++++++++++++++++++++++ test/spec/ui/export_spec.js | 9 +++++--- 5 files changed, 73 insertions(+), 17 deletions(-) create mode 100644 src/data/edit_state.js create mode 100644 test/spec/data/edit_state_spec.js diff --git a/src/data/edit_state.js b/src/data/edit_state.js new file mode 100644 index 0000000..86da3c8 --- /dev/null +++ b/src/data/edit_state.js @@ -0,0 +1,34 @@ +define(function(require, exports, module) { + 'use strict'; + var flight = require('flight'); + var _ = require('lodash'); + var $ = require('jquery'); + + module.exports = flight.component(function() { + + this.configure = function(ev, config) { + // deep copy property info (so we can make notes w/o bothering anyone) + this.property_info = $.extend(true, {}, config.properties); + } + + this.loadData = function(ev, data) { + this.data = data; + } + + // Only way I can see to provide edited data back to the + // *particular* other flight component that requested it is for it + // to say what event it's listening for in response. Large systems + // have been built this way (it's what Erlang was built for), but it + // still feels odd. + + this.provideEdits = function(ev, handlerEvent) { + this.trigger(handlerEvent, this.data); + } + + this.after('initialize', function() { + this.on(document, 'config', this.configure); + this.on(document, 'data', this.loadData); + this.on(document, 'requestEditedData', this.provideEdits); + }); + }); +}); diff --git a/src/script.js b/src/script.js index aa0fe8f..749adec 100644 --- a/src/script.js +++ b/src/script.js @@ -47,4 +47,5 @@ define(function(require) { require('data/search').attachTo(document); require('data/typeahead').attachTo(document); require('data/loader').attachTo(document); + require('data/edit_state').attachTo(document); }); diff --git a/src/ui/export.js b/src/ui/export.js index b5c6656..ee46133 100644 --- a/src/ui/export.js +++ b/src/ui/export.js @@ -12,27 +12,21 @@ define(function(require, exports, module) { } } - this.noteData = function(ev, data) { - - // Squirrel away a copy of the whole in-core data block, - // with the idea that it will be modified by subsequent - // operations, and be available for export... - - this.data = data; + this.triggerExport = function(ev) { + ev.preventDefault(); + $(document).trigger('requestEditedData', 'editedDataForSave'); } - this.doExport = function(ev) { - var uri, json; - ev.preventDefault(); - json = JSON.stringify(this.data, undefined, 2); - uri = 'data:text/plain,'+encodeURIComponent(json); + this.doExport = function(ev, data) { + var json = JSON.stringify(data, undefined, 2); + var uri = 'data:text/plain,'+encodeURIComponent(json); window.open(uri, "Exported Finda Data") } this.after('initialize', function() { this.on(document, 'config', this.configure); - this.on(document, 'data', this.noteData); - this.on('submit', this.doExport); + this.on(document, 'editedDataForSave', this.doExport); + this.on('submit', this.triggerExport); }); }); }); diff --git a/test/spec/data/edit_state_spec.js b/test/spec/data/edit_state_spec.js new file mode 100644 index 0000000..98dd46a --- /dev/null +++ b/test/spec/data/edit_state_spec.js @@ -0,0 +1,24 @@ +define(['test/mock', 'jquery'], function(mock, $) { + 'use strict'; + describeComponent('data/edit_state', function() { + beforeEach(function() { + setupComponent(); + }); + + describe('on data load', function() { + + it('records the data', function() { + this.component.trigger('data', mock.data); + expect(this.component.data).toEqual(mock.data); + }); + + it('sends edited data back to whoever asks', function() { + spyOn(this.$node, 'trigger'); + this.component.trigger('data', 'grubbitz'); + this.component.trigger('requestEditedData', 'callbackEvent') + expect(this.$node.trigger).toHaveBeenCalledWith('callbackEvent', + mock.data); + }); + }); + }); +}); \ No newline at end of file diff --git a/test/spec/ui/export_spec.js b/test/spec/ui/export_spec.js index 616f29d..7620b60 100644 --- a/test/spec/ui/export_spec.js +++ b/test/spec/ui/export_spec.js @@ -30,10 +30,13 @@ define(['test/mock', 'jquery'], function(mock, $) { describe('on export', function() { it('does an export when triggered', function() { - spyOn(window, 'open'); - this.component.trigger('data', mock.data); + // Note that we are *not* mocking out the request/response + // protocol that gets the request data from the edit-state + // component; setting up that test environment would be... + // complex. - this.$node.trigger('submit'); + spyOn(window, 'open'); + this.component.trigger('editedDataForSave', mock.data); expect(window.open).toHaveBeenCalled(); // Verify that the args are as expected... From 95230616b147e8e3e8bdac58215b5280f0888ddc Mon Sep 17 00:00:00 2001 From: Robert Thau Date: Thu, 16 Oct 2014 18:35:31 -0400 Subject: [PATCH 06/56] In edit mode, currently selected point is draggable. Furthermore, dragging it alters the stored geoJson, and is correctly reflected on export, all managed by the edit_state component (which is listening for "selectFeature" and "selectedFeatureMoved" events). Works when tested manually, and verifies the general theory of edits for other stuff (and how I expect edit state to be bound to the UI, at least in general terms). Automated tests tbd. --- src/data/edit_state.js | 14 ++++++++++++++ src/ui/map.js | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/data/edit_state.js b/src/data/edit_state.js index 86da3c8..9372334 100644 --- a/src/data/edit_state.js +++ b/src/data/edit_state.js @@ -25,10 +25,24 @@ define(function(require, exports, module) { this.trigger(handlerEvent, this.data); } + // Should probably guard against selecting a feature that's not a point. + + this.selectFeature = function(ev, feature) { + this.selectedFeature = feature; + } + + this.selectedFeatureMoved = function(ev, latlng) { + if (this.selectedFeature) { + this.selectedFeature.geometry.coordinates = [latlng.lng, latlng.lat]; + } + } + this.after('initialize', function() { this.on(document, 'config', this.configure); this.on(document, 'data', this.loadData); this.on(document, 'requestEditedData', this.provideEdits); + this.on(document, 'selectFeature', this.selectFeature); + this.on(document, 'selectedFeatureMoved', this.selectedFeatureMoved); }); }); }); diff --git a/src/ui/map.js b/src/ui/map.js index 906282c..3415546 100644 --- a/src/ui/map.js +++ b/src/ui/map.js @@ -45,6 +45,10 @@ define(function(require, exports, module) { // set feature attribute to be used as preview text to config this.featurePreviewAttr = config.map.preview_attribute; + + // Determine whether edit-mode features are enabled (particularly + // dragging selected feature). + this.edit_mode = config.edit_mode; }; this.loadData = function(ev, data) { @@ -97,6 +101,13 @@ define(function(require, exports, module) { layer.setIcon(this.grayIcon); this.previouslyClicked = layer; + if (this.edit_mode) { + layer.dragging.enable(); + layer.on("dragend", function(ev) { + this.trigger(document, 'selectedFeatureMoved', ev.target.getLatLng()); + }.bind(this)); + } + // re-bind popup to feature with specified preview attribute this.bindPopupToFeature( layer, @@ -112,6 +123,9 @@ define(function(require, exports, module) { this.deselectFeature = function(ev, feature) { if (this.previouslyClicked) { this.previouslyClicked.setIcon(this.defaultIcon); + if (this.edit_mode) { + this.previouslyClicked.dragging.disable(); + } } var layer = this.attr.features[feature.geometry.coordinates]; // re-bind popup to feature with specified preview attribute From ad2969a44bea85dd7ba8c3edb2e09efabd293964 Mon Sep 17 00:00:00 2001 From: Robert Thau Date: Thu, 16 Oct 2014 21:06:47 -0400 Subject: [PATCH 07/56] Tests for altering feature location by dragging on the map. These are all component unit tests, and get awkwardly deep in leaflet internals when looking at drag state, but better than nothing. --- src/data/edit_state.js | 2 +- test/spec/data/edit_state_spec.js | 40 +++++++++++++++++++---- test/spec/ui/export_spec.js | 8 +++++ test/spec/ui/map_spec.js | 54 +++++++++++++++++++++++++++++++ 4 files changed, 97 insertions(+), 7 deletions(-) diff --git a/src/data/edit_state.js b/src/data/edit_state.js index 9372334..e0fd38b 100644 --- a/src/data/edit_state.js +++ b/src/data/edit_state.js @@ -22,7 +22,7 @@ define(function(require, exports, module) { // still feels odd. this.provideEdits = function(ev, handlerEvent) { - this.trigger(handlerEvent, this.data); + $(document).trigger(handlerEvent, this.data); } // Should probably guard against selecting a feature that's not a point. diff --git a/test/spec/data/edit_state_spec.js b/test/spec/data/edit_state_spec.js index 98dd46a..f21ddf7 100644 --- a/test/spec/data/edit_state_spec.js +++ b/test/spec/data/edit_state_spec.js @@ -6,18 +6,46 @@ define(['test/mock', 'jquery'], function(mock, $) { }); describe('on data load', function() { - it('records the data', function() { this.component.trigger('data', mock.data); expect(this.component.data).toEqual(mock.data); }); + }); + + describe('editing protocol', function() { + + describe('with a selected feature', function() { + + beforeEach(function() { + + // We load in a deep copy of the mock data, for test isolation; + // if we loaded in 'mock.data' directly, each test would see + // dinks from all other tests that had previously run. + + this.copiedData = $.extend(true, {}, mock.data); + this.component.trigger('data', this.copiedData); + + this.feature = this.copiedData.features[0]; + $(document).trigger('selectFeature', this.feature); + }); + + it("records the selected feature for further work", function() { + expect(this.component.selectedFeature).toBe(this.feature); + }); + + it("updates position upon UI request", function() { + $(document).trigger('selectedFeatureMoved', {lat: 55, lng: 44}); + expect(this.feature.geometry.coordinates).toEqual([44, 55]); + expect(this.component.data.features[0].geometry.coordinates).toEqual([44,55]); + }); + }); it('sends edited data back to whoever asks', function() { - spyOn(this.$node, 'trigger'); - this.component.trigger('data', 'grubbitz'); - this.component.trigger('requestEditedData', 'callbackEvent') - expect(this.$node.trigger).toHaveBeenCalledWith('callbackEvent', - mock.data); + spyOnEvent(document, 'callbackEvent'); + this.component.trigger('data', mock.data); + $(document).trigger('requestEditedData', 'callbackEvent') + expect('callbackEvent').toHaveBeenTriggeredOnAndWith( + document, mock.data); }); }); }); diff --git a/test/spec/ui/export_spec.js b/test/spec/ui/export_spec.js index 7620b60..a50860e 100644 --- a/test/spec/ui/export_spec.js +++ b/test/spec/ui/export_spec.js @@ -5,6 +5,7 @@ define(['test/mock', 'jquery'], function(mock, $) { describeComponent('ui/export', function() { beforeEach(function() { setupComponent('
foo:
'); + spyOnEvent(document,'requestEditedData'); }); afterEach(function() { @@ -28,6 +29,13 @@ define(['test/mock', 'jquery'], function(mock, $) { }); describe('on export', function() { + + it('requests export data on form submittal', function() { + this.$node.trigger('submit'); + expect('requestEditedData').toHaveBeenTriggeredOnAndWith( + document,'editedDataForSave'); + }); + it('does an export when triggered', function() { // Note that we are *not* mocking out the request/response diff --git a/test/spec/ui/map_spec.js b/test/spec/ui/map_spec.js index 27f9961..5b2ed09 100644 --- a/test/spec/ui/map_spec.js +++ b/test/spec/ui/map_spec.js @@ -94,6 +94,12 @@ define( var icon = this.component.$node.find('.leaflet-marker-icon:first'); expect(icon.attr('src')).toMatch(/marker-icon\.png$/); }); + + it('leaves dragging off when not in edit mode', function() { + var feature = mock.data.features[0]; + var marker = this.component.attr.features[feature.geometry.coordinates]; + expect(!marker.dragging._enabled).toBe(true); + }); }); describe('deselectFeature', function() { @@ -111,5 +117,53 @@ define( }); + describe('in edit mode', function() { + var layer; + beforeEach(function() { + + // Initialize with deep copies of mock config & data, so we + // don't have to worry about scribbling on the originals. + // + // Also, set 'edit_mode' to true in the mock config. + + this.component.trigger('config', $.extend(true, {}, mock.config, + {edit_mode: true})); + this.mockData = $.extend(true, {}, mock.data); + this.component.trigger('data', this.mockData); + }); + + describe('with a feature', function() { + + beforeEach(function() { + this.feature = this.mockData.features[0]; + this.marker = this.component.attr.features[this.feature.geometry.coordinates]; + }); + + // For some reason, spies on this.marker.dragging.{enable,disable} + // don't work, so... + + it('enables dragging on select', function() { + this.component.trigger(document, 'selectFeature', this.feature); + expect(this.marker.dragging._enabled).toBe(true); + }); + + it('disables dragging on deselect', function() { + this.component.trigger(document, 'selectFeature', this.feature); + this.component.trigger(document, 'deselectFeature', this.feature); + expect(!this.marker.dragging._enabled).toBe(true); + }); + + it('reports new position on drag-end', function() { + spyOnEvent(document, 'selectedFeatureMoved'); + this.component.trigger(document, 'selectFeature', this.feature); + this.marker.fireEvent('dragend'); + expect('selectedFeatureMoved').toHaveBeenTriggeredOnAndWith( + document, this.marker.getLatLng()); + }); + + }); + + }); + }); }); From 7d147ed9472b0a1bfb2a1f4abdb21dc9b4621745 Mon Sep 17 00:00:00 2001 From: Robert Thau Date: Sun, 19 Oct 2014 19:26:31 -0400 Subject: [PATCH 08/56] Map responds to external selectedFeatureMove events, if they ever come. Right now, I'm just being consistent; this will matter if these events ever come from elsewhere (e.g., updated on changes to address properties). --- src/ui/map.js | 12 ++++++++++++ test/spec/ui/map_spec.js | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/ui/map.js b/src/ui/map.js index 3415546..81b5f36 100644 --- a/src/ui/map.js +++ b/src/ui/map.js @@ -120,6 +120,15 @@ define(function(require, exports, module) { } }; + this.selectedFeatureMoved = function(ev, latlng) { + if (this.previouslyClicked) { + var oldLatLng = this.previouslyClicked.getLatLng(); + if (latlng.lat !== oldLatLng.lat || latlng.lng !== oldLatLng.lng) { + this.previouslyClicked.setLatLng(latlng); + } + } + }; + this.deselectFeature = function(ev, feature) { if (this.previouslyClicked) { this.previouslyClicked.setIcon(this.defaultIcon); @@ -129,6 +138,8 @@ define(function(require, exports, module) { } var layer = this.attr.features[feature.geometry.coordinates]; // re-bind popup to feature with specified preview attribute + // NB if value of preview attr has changed in edit, this is + // where the map picks it up. this.bindPopupToFeature( layer, feature.properties[this.featurePreviewAttr]); @@ -182,6 +193,7 @@ define(function(require, exports, module) { this.on(document, 'deselectFeature', this.deselectFeature); this.on(document, 'hoverFeature', this.hoverFeature); this.on(document, 'clearHoverFeature', this.clearHoverFeature); + this.on(document, 'selectedFeatureMoved', this.selectedFeatureMoved); this.on('panTo', this.panTo); }); }); diff --git a/test/spec/ui/map_spec.js b/test/spec/ui/map_spec.js index 5b2ed09..441594f 100644 --- a/test/spec/ui/map_spec.js +++ b/test/spec/ui/map_spec.js @@ -153,6 +153,25 @@ define( expect(!this.marker.dragging._enabled).toBe(true); }); + it('resets position on external posn change', function() { + this.component.trigger(document, 'selectFeature', this.feature); + var newPos = {lat: 33, lng: 44}; + + spyOn(this.marker, 'setLatLng'); + this.component.trigger(document, 'selectedFeatureMoved', newPos); + expect(this.marker.setLatLng).toHaveBeenCalledWith(newPos); + }); + + it("doesn't reset posn unless 'move' event actually moved", function() { + this.component.trigger(document, 'selectFeature', this.feature); + var oldLatLng = this.marker.getLatLng(); + var newPos = {lat: oldLatLng.lat, lng: oldLatLng.lng}; + + spyOn(this.marker, 'setLatLng'); + this.component.trigger(document, 'selectedFeatureMoved', newPos); + expect(this.marker.setLatLng).not.toHaveBeenCalled(); + }); + it('reports new position on drag-end', function() { spyOnEvent(document, 'selectedFeatureMoved'); this.component.trigger(document, 'selectFeature', this.feature); From 607cea04014bcd661d7105399dc1c100210be106 Mon Sep 17 00:00:00 2001 From: Robert Thau Date: Sun, 19 Oct 2014 19:27:59 -0400 Subject: [PATCH 09/56] Barebones edit-state support for generic prop-change events. Now to integrate a JSON editor so we can start sending them... --- src/data/edit_state.js | 9 +++++++++ test/spec/data/edit_state_spec.js | 13 +++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/data/edit_state.js b/src/data/edit_state.js index e0fd38b..c27fde5 100644 --- a/src/data/edit_state.js +++ b/src/data/edit_state.js @@ -37,12 +37,21 @@ define(function(require, exports, module) { } } + this.propEdit = function(ev, newProps) { + if (this.selectedFeature) { + // Events may specify values for only *some* properties; + // if so, we want to leave the others alone. + $.extend(this.selectedFeature.properties, newProps); + } + } + this.after('initialize', function() { this.on(document, 'config', this.configure); this.on(document, 'data', this.loadData); this.on(document, 'requestEditedData', this.provideEdits); this.on(document, 'selectFeature', this.selectFeature); this.on(document, 'selectedFeatureMoved', this.selectedFeatureMoved); + this.on(document, 'selectedFeaturePropsChanged', this.propEdit); }); }); }); diff --git a/test/spec/data/edit_state_spec.js b/test/spec/data/edit_state_spec.js index f21ddf7..559d35f 100644 --- a/test/spec/data/edit_state_spec.js +++ b/test/spec/data/edit_state_spec.js @@ -38,6 +38,19 @@ define(['test/mock', 'jquery'], function(mock, $) { expect(this.feature.geometry.coordinates).toEqual([44, 55]); expect(this.component.data.features[0].geometry.coordinates).toEqual([44,55]); }); + + it("changes properties on request", function() { + $(document).trigger('selectedFeaturePropsChanged', + {organization_name: "fred"}); + expect(this.feature.properties.organization_name).toBe("fred"); + }); + + it("leaves props alone unless changed value specified", function() { + var oldUrl = this.feature.properties.web_url; + $(document).trigger('selectedFeaturePropsChanged', + {organization_name: "fred"}); + expect(this.feature.properties.web_url).toBe(oldUrl); + }); }); it('sends edited data back to whoever asks', function() { From 7d6ceb040b736c2753623959c8210c03c9a4dfcf Mon Sep 17 00:00:00 2001 From: Robert Thau Date: Mon, 20 Oct 2014 22:02:19 -0400 Subject: [PATCH 10/56] Start on the property-editing UI. Using Jeremy Dorn's json editor, replacing the contents of the info window. Needs a little glue code to connect up to the edit state object. More seriously, needs to have either scrollbars, much smaller fonts, or both, to get adequate space. --- config.json | 18 +++++++++++++++++- karma.conf.js | 1 + lib/jsoneditor.min.js | 10 ++++++++++ src/script.js | 4 +++- src/ui/info.js | 34 +++++++++++++++++++++++++++++++--- 5 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 lib/jsoneditor.min.js diff --git a/config.json b/config.json index d94c0e9..426e0c4 100644 --- a/config.json +++ b/config.json @@ -59,5 +59,21 @@ "detail_enabled": true }, "geojson_source": "data.geojson", - "edit_mode": true + "edit_mode": true, + "feature_property_json_schema": { + "title": "Organization", + "type": "object", + "id": "properties", + "properties": { + "organization_name": { "type": "string" }, + "address": { "type": "string" }, + "web_url": { "type": "string", "format": "url" }, + "contact_names": { + "title": "Contacts", + "type": "array", + "format": "table", + "items": { "type": "string" } + } + } + } } diff --git a/karma.conf.js b/karma.conf.js index 2ced7ad..532bd4c 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -17,6 +17,7 @@ module.exports = function(config) { 'lib/es5-shim.min.js', 'lib/es5-sham.min.js', 'lib/jquery-1.10.2.js', + 'lib/jsoneditor.min.js', 'test/lib/jasmine-jquery.js', 'test/lib/jasmine-flight.js', diff --git a/lib/jsoneditor.min.js b/lib/jsoneditor.min.js new file mode 100644 index 0000000..0effbf5 --- /dev/null +++ b/lib/jsoneditor.min.js @@ -0,0 +1,10 @@ +/*! JSON Editor v0.7.12 - JSON Schema -> HTML Editor + * By Jeremy Dorn - https://github.com/jdorn/json-editor/ + * Released under the MIT license + * + * Date: 2014-10-05 + */ +!function(){var a;!function(){var b=!1,c=/xyz/.test(function(){window.postMessage("xyz")})?/\b_super\b/:/.*/;return a=function(){},a.extend=function(a){function d(){!b&&this.init&&this.init.apply(this,arguments)}var e=this.prototype;b=!0;var f=new this;b=!1;for(var g in a)f[g]="function"==typeof a[g]&&"function"==typeof e[g]&&c.test(a[g])?function(a,b){return function(){var c=this._super;this._super=e[a];var d=b.apply(this,arguments);return this._super=c,d}}(g,a[g]):a[g];return d.prototype=f,d.prototype.constructor=d,d.extend=arguments.callee,d},a}(),function(){function a(a,b){b=b||{bubbles:!1,cancelable:!1,detail:void 0};var c=document.createEvent("CustomEvent");return c.initCustomEvent(a,b.bubbles,b.cancelable,b.detail),c}a.prototype=window.Event.prototype,window.CustomEvent=a}(),function(){for(var a=0,b=["ms","moz","webkit","o"],c=0;c=g&&!h&&(h=!0,b())})}},d.send()}}),g||b()},expandRefs:function(a){for(a=c({},a);a.$ref;){var b=a.$ref;delete a.$ref,a=this.extendSchemas(a,this.refs[b])}return a},expandSchema:function(a){var b,e=this,f=c({},a);if("object"==typeof a.type&&(Array.isArray(a.type)?d(a.type,function(b,c){"object"==typeof c&&(a.type[b]=e.expandSchema(c))}):a.type=e.expandSchema(a.type)),"object"==typeof a.disallow&&(Array.isArray(a.disallow)?d(a.disallow,function(b,c){"object"==typeof c&&(a.disallow[b]=e.expandSchema(c))}):a.disallow=e.expandSchema(a.disallow)),a.anyOf&&d(a.anyOf,function(b,c){a.anyOf[b]=e.expandSchema(c)}),a.dependencies&&d(a.dependencies,function(b,c){"object"!=typeof c||Array.isArray(c)||(a.dependencies[b]=e.expandSchema(c))}),a.not&&(a.not=this.expandSchema(a.not)),a.allOf){for(b=0;b=a.maximum?j.push({path:e,property:"maximum",message:this.translate("error_maximum_excl",[a.maximum])}):!a.exclusiveMaximum&&b>a.maximum&&j.push({path:e,property:"maximum",message:this.translate("error_maximum_incl",[a.maximum])})),a.hasOwnProperty("minimum")&&(a.exclusiveMinimum&&b<=a.minimum?j.push({path:e,property:"minimum",message:this.translate("error_minimum_excl",[a.minimum])}):!a.exclusiveMinimum&&ba.maxLength&&j.push({path:e,property:"maxLength",message:this.translate("error_maxLength",[a.maxLength])}),a.minLength&&(b+"").lengtha.maxItems&&j.push({path:e,property:"maxItems",message:this.translate("error_maxItems",[a.maxItems])}),a.minItems&&b.lengtha.maxProperties&&j.push({path:e,property:"maxProperties",message:this.translate("error_maxProperties",[a.maxProperties])})}if(a.minProperties){g=0;for(h in b)b.hasOwnProperty(h)&&g++;g=0){b=this.theme.getBlockLinkHolder(),c=this.theme.getBlockLink(),c.setAttribute("target","_blank");var h=document.createElement(e);h.setAttribute("controls","controls"),this.theme.createMediaLink(b,c,h),this.link_watchers.push(function(b){var d=f(b);c.setAttribute("href",d),c.textContent=a.rel||d,h.setAttribute("src",d)})}else b=this.theme.getBlockLink(),b.setAttribute("target","_blank"),b.textContent=a.rel,this.link_watchers.push(function(c){var d=f(c);b.setAttribute("href",d),b.textContent=a.rel||d});return b},refreshWatchedFieldValues:function(){if(this.watched_values){var a={},b=!1,c=this;if(this.watched){var d,e;for(var f in this.watched)this.watched.hasOwnProperty(f)&&(e=c.jsoneditor.getEditor(this.watched[f]),d=e?e.getValue():null,c.watched_values[f]!==d&&(b=!0),a[f]=d)}return a.self=this.getValue(),this.watched_values.self!==a.self&&(b=!0),this.watched_values=a,b}},getWatchedFieldValues:function(){return this.watched_values},updateHeaderText:function(){this.header&&(this.header.textContent=this.getHeaderText())},getHeaderText:function(a){return this.header_text?this.header_text:a?this.schema.title:this.getTitle()},onWatchedFieldChange:function(){var a;if(this.header_template){a=c(this.getWatchedFieldValues(),{key:this.key,i:this.key,i0:1*this.key,i1:1*this.key+1,title:this.getTitle()});var b=this.header_template(a);b!==this.header_text&&(this.header_text=b,this.updateHeaderText(),this.notify())}if(this.link_watchers.length){a=this.getWatchedFieldValues();for(var d=0;d1&&(b[a]=d+" "+e[d])}),b},getOption:function(a){try{throw"getOption is deprecated"}catch(b){window.console.error(b)}return this.options[a]},showValidationErrors:function(){}}),f.defaults.editors.null=f.AbstractEditor.extend({getValue:function(){return null},setValue:function(){this.onChange()},getNumColumns:function(){return 2}}),f.defaults.editors.string=f.AbstractEditor.extend({register:function(){this._super(),this.input&&this.input.setAttribute("name",this.formname)},unregister:function(){this._super(),this.input&&this.input.removeAttribute("name")},setValue:function(a,b,c){if((!this.template||c)&&(null===a?a="":"object"==typeof a?a=JSON.stringify(a):"string"!=typeof a&&(a=""+a),a!==this.serialized)){var d=this.sanitize(a);if(this.input.value!==d){this.input.value=d,this.sceditor_instance?this.sceditor_instance.val(d):this.epiceditor?this.epiceditor.importFile(null,d):this.ace_editor&&this.ace_editor.setValue(d);var e=c||this.getValue()!==a;this.refreshValue(),b?this.is_dirty=!1:"change"===this.jsoneditor.options.show_errors&&(this.is_dirty=!0),this.onChange(e)}}},getNumColumns:function(){var a,b=Math.ceil(Math.max(this.getTitle().length,this.schema.maxLength||0,this.schema.minLength||0)/5);return a="textarea"===this.input_type?6:["text","email"].indexOf(this.input_type)>=0?4:2,Math.min(12,Math.max(b,a))},build:function(){var a=this;if(this.options.compact||(this.header=this.label=this.theme.getFormInputLabel(this.getTitle())),this.schema.description&&(this.description=this.theme.getFormInputDescription(this.schema.description)),this.format=this.schema.format,!this.format&&this.schema.media&&this.schema.media.type&&(this.format=this.schema.media.type.replace(/(^(application|text)\/(x-)?(script\.)?)|(-source$)/g,"")),!this.format&&this.options.default_format&&(this.format=this.options.default_format),this.options.format&&(this.format=this.options.format),this.format)if("textarea"===this.format)this.input_type="textarea",this.input=this.theme.getTextareaInput();else if("range"===this.format){this.input_type="range";var b=this.schema.minimum||0,c=this.schema.maximum||Math.max(100,b+1),d=1;this.schema.multipleOf&&(b%this.schema.multipleOf&&(b=Math.ceil(b/this.schema.multipleOf)*this.schema.multipleOf),c%this.schema.multipleOf&&(c=Math.floor(c/this.schema.multipleOf)*this.schema.multipleOf),d=this.schema.multipleOf),this.input=this.theme.getRangeInput(b,c,d)}else["actionscript","batchfile","bbcode","c","c++","cpp","coffee","csharp","css","dart","django","ejs","erlang","golang","handlebars","haskell","haxe","html","ini","jade","java","javascript","json","less","lisp","lua","makefile","markdown","matlab","mysql","objectivec","pascal","perl","pgsql","php","python","r","ruby","sass","scala","scss","smarty","sql","stylus","svg","twig","vbscript","xml","yaml"].indexOf(this.format)>=0?(this.input_type=this.format,this.source_code=!0,this.input=this.theme.getTextareaInput()):(this.input_type=this.format,this.input=this.theme.getFormInputField(this.input_type));else this.input_type="text",this.input=this.theme.getFormInputField(this.input_type);"undefined"!=typeof this.schema.maxLength&&this.input.setAttribute("maxlength",this.schema.maxLength),"undefined"!=typeof this.schema.pattern?this.input.setAttribute("pattern",this.schema.pattern):"undefined"!=typeof this.schema.minLength&&this.input.setAttribute("pattern",".{"+this.schema.minLength+",}"),this.options.compact&&this.container.setAttribute("class",this.container.getAttribute("class")+" compact"),(this.schema.readOnly||this.schema.readonly||this.schema.template)&&(this.always_disabled=!0,this.input.disabled=!0),this.input.addEventListener("change",function(b){if(b.preventDefault(),b.stopPropagation(),a.schema.template)return void(this.value=a.value);var c=this.value,d=a.sanitize(c);c!==d&&(this.value=d),a.is_dirty=!0,a.refreshValue(),a.onChange(!0)}),this.format&&this.input.setAttribute("data-schemaformat",this.format),this.control=this.theme.getFormControl(this.label,this.input,this.description),this.container.appendChild(this.control),window.requestAnimationFrame(function(){a.input.parentNode&&a.afterInputReady()}),this.schema.template?(this.template=this.jsoneditor.compileTemplate(this.schema.template,this.template_engine),this.refreshValue()):this.refreshValue()},enable:function(){this.always_disabled||(this.input.disabled=!1),this._super()},disable:function(){this.input.disabled=!0,this._super()},afterInputReady:function(){var a,b=this;if(this.source_code)if(this.options.wysiwyg&&["html","bbcode"].indexOf(this.input_type)>=0&&window.jQuery&&window.jQuery.fn&&window.jQuery.fn.sceditor)a=c({},{plugins:"html"===b.input_type?"xhtml":"bbcode",emoticonsEnabled:!1,width:"100%",height:300},f.plugins.sceditor,b.options.sceditor_options||{}),window.jQuery(b.input).sceditor(a),b.sceditor_instance=window.jQuery(b.input).sceditor("instance"),b.sceditor_instance.blur(function(){var a=window.jQuery("
"+b.sceditor_instance.val()+"
");window.jQuery("#sceditor-start-marker,#sceditor-end-marker,.sceditor-nlf",a).remove(),b.input.value=a.html(),b.value=b.input.value,b.is_dirty=!0,b.onChange(!0)});else if("markdown"===this.input_type&&window.EpicEditor)this.epiceditor_container=document.createElement("div"),this.input.parentNode.insertBefore(this.epiceditor_container,this.input),this.input.style.display="none",a=c({},f.plugins.epiceditor,{container:this.epiceditor_container,clientSideStorage:!1}),this.epiceditor=new window.EpicEditor(a).load(),this.epiceditor.importFile(null,this.getValue()),this.epiceditor.on("update",function(){var a=b.epiceditor.exportFile();b.input.value=a,b.value=a,b.is_dirty=!0,b.onChange(!0)});else if(window.ace){var d=this.input_type;("cpp"===d||"c++"===d||"c"===d)&&(d="c_cpp"),this.ace_container=document.createElement("div"),this.ace_container.style.width="100%",this.ace_container.style.position="relative",this.ace_container.style.height="400px",this.input.parentNode.insertBefore(this.ace_container,this.input),this.input.style.display="none",this.ace_editor=window.ace.edit(this.ace_container),this.ace_editor.setValue(this.getValue()),f.plugins.ace.theme&&this.ace_editor.setTheme("ace/theme/"+f.plugins.ace.theme),d=window.ace.require("ace/mode/"+d),d&&this.ace_editor.getSession().setMode(new d.Mode),this.ace_editor.on("change",function(){var a=b.ace_editor.getValue();b.input.value=a,b.refreshValue(),b.is_dirty=!0,b.onChange(!0)})}b.theme.afterInputReady(b.input)},refreshValue:function(){this.value=this.input.value,"string"!=typeof this.value&&(this.value=""),this.serialized=this.value},destroy:function(){this.sceditor_instance?this.sceditor_instance.destroy():this.epiceditor?this.epiceditor.unload():this.ace_editor&&this.ace_editor.destroy(),this.template=null,this.input&&this.input.parentNode&&this.input.parentNode.removeChild(this.input),this.label&&this.label.parentNode&&this.label.parentNode.removeChild(this.label),this.description&&this.description.parentNode&&this.description.parentNode.removeChild(this.description),this._super()},sanitize:function(a){return a},onWatchedFieldChange:function(){var a;this.template&&(a=this.getWatchedFieldValues(),this.setValue(this.template(a),!1,!0)),this._super()},showValidationErrors:function(a){var b=this;if("always"===this.jsoneditor.options.show_errors);else if(!this.is_dirty&&this.previous_error_setting===this.jsoneditor.options.show_errors)return; +this.previous_error_setting=this.jsoneditor.options.show_errors;var c=[];d(a,function(a,d){d.path===b.path&&c.push(d.message)}),c.length?this.theme.addInputError(this.input,c.join(". ")+"."):this.theme.removeInputError(this.input)}}),f.defaults.editors.number=f.defaults.editors.string.extend({sanitize:function(a){return(a+"").replace(/[^0-9\.\-eE]/g,"")},getNumColumns:function(){return 2},getValue:function(){return 1*this.value}}),f.defaults.editors.integer=f.defaults.editors.number.extend({sanitize:function(a){return a+="",a.replace(/[^0-9\-]/g,"")},getNumColumns:function(){return 2}}),f.defaults.editors.object=f.AbstractEditor.extend({getDefault:function(){return c({},this.schema.default||{})},getChildEditors:function(){return this.editors},register:function(){if(this._super(),this.editors)for(var a in this.editors)this.editors.hasOwnProperty(a)&&this.editors[a].register()},unregister:function(){if(this._super(),this.editors)for(var a in this.editors)this.editors.hasOwnProperty(a)&&this.editors[a].unregister()},getNumColumns:function(){return Math.max(Math.min(12,this.maxwidth),3)},enable:function(){if(this.editjson_button&&(this.editjson_button.disabled=!1),this.addproperty_button&&(this.addproperty_button.disabled=!1),this._super(),this.editors)for(var a in this.editors)this.editors.hasOwnProperty(a)&&this.editors[a].enable()},disable:function(){if(this.editjson_button&&(this.editjson_button.disabled=!0),this.addproperty_button&&(this.addproperty_button.disabled=!0),this.hideEditJSON(),this._super(),this.editors)for(var a in this.editors)this.editors.hasOwnProperty(a)&&this.editors[a].disable()},layoutEditors:function(){var a,b,c=this;if(this.row_container){this.property_order=Object.keys(this.editors),this.property_order=this.property_order.sort(function(a,b){var d=c.editors[a].schema.propertyOrder,e=c.editors[b].schema.propertyOrder;return"number"!=typeof d&&(d=1e3),"number"!=typeof e&&(e=1e3),d-e});var e;if("grid"===this.format){var f=[];for(d(this.property_order,function(a,b){var d=c.editors[b];if(!d.property_removed){for(var e=!1,g=d.options.hidden?0:d.getNumColumns(),h=d.options.hidden?0:d.container.offsetHeight,i=0;ih)&&(e=i);e===!1&&(f.push({width:0,minh:999999,maxh:0,editors:[]}),e=f.length-1),f[e].editors.push({key:b,width:g,height:h}),f[e].width+=g,f[e].minh=Math.min(f[e].minh,h),f[e].maxh=Math.max(f[e].maxh,h)}}),a=0;af[a].editors[g].width&&(g=b),f[a].editors[b].width*=12/f[a].width,f[a].editors[b].width=Math.floor(f[a].editors[b].width),h+=f[a].editors[b].width;12>h&&(f[a].editors[g].width+=12-h),f[a].width=12}if(this.layout===JSON.stringify(f))return!1;for(this.layout=JSON.stringify(f),e=document.createElement("div"),a=0;a=this.schema.maxProperties),this.addproperty_checkboxes&&(this.addproperty_list.innerHTML=""),this.addproperty_checkboxes={};for(a in this.cached_editors)this.cached_editors.hasOwnProperty(a)&&(this.addPropertyCheckbox(a),this.isRequired(this.cached_editors[a])&&a in this.editors&&(this.addproperty_checkboxes[a].disabled=!0),"undefined"!=typeof this.schema.minProperties&&d<=this.schema.minProperties?(this.addproperty_checkboxes[a].disabled=this.addproperty_checkboxes[a].checked,this.addproperty_checkboxes[a].checked||(e=!0)):a in this.editors?(e=!0,c=!0):b||this.schema.properties.hasOwnProperty(a)?(this.addproperty_checkboxes[a].disabled=!1,e=!0):this.addproperty_checkboxes[a].disabled=!0);this.canHaveAdditionalProperties()&&(e=!0);for(a in this.schema.properties)this.schema.properties.hasOwnProperty(a)&&(this.cached_editors[a]||(e=!0,this.addPropertyCheckbox(a),this.addproperty_checkboxes[a].disabled=!b));e?this.canHaveAdditionalProperties()?this.addproperty_add.disabled=b?!1:!0:(this.addproperty_add.style.display="none",this.addproperty_input.style.display="none"):(this.hideAddProperty(),this.addproperty_controls.style.display="none")},isRequired:function(a){return"boolean"==typeof a.schema.required?a.schema.required:Array.isArray(this.schema.required)?this.schema.required.indexOf(a.key)>-1:this.jsoneditor.options.required_by_default?!0:!1},setValue:function(a,b){var c=this;a=a||{},("object"!=typeof a||Array.isArray(a))&&(a={}),d(this.cached_editors,function(d,e){"undefined"!=typeof a[d]?(c.addObjectProperty(d),e.setValue(a[d],b)):b||c.isRequired(e)?e.setValue(e.getDefault(),b):c.removeObjectProperty(d)}),d(a,function(a,d){c.cached_editors[a]||(c.addObjectProperty(a),c.editors[a]&&c.editors[a].setValue(d,b))}),this.refreshValue(),this.layoutEditors(),this.onChange()},showValidationErrors:function(a){var b=this,c=[],e=[];if(d(a,function(a,d){d.path===b.path?c.push(d):e.push(d)}),this.error_holder)if(c.length){this.error_holder.innerHTML="",this.error_holder.style.display="",d(c,function(a,c){b.error_holder.appendChild(b.theme.getErrorMessage(c.message))})}else this.error_holder.style.display="none";this.options.table_row&&(c.length?this.theme.addTableRowError(this.container):this.theme.removeTableRowError(this.container)),d(this.editors,function(a,b){b.showValidationErrors(e)})}}),f.defaults.editors.array=f.AbstractEditor.extend({getDefault:function(){return this.schema.default||[]},register:function(){if(this._super(),this.rows)for(var a=0;a=this.schema.items.length?this.schema.additionalItems===!0?{}:this.schema.additionalItems?c({},this.schema.additionalItems):void 0:c({},this.schema.items[a]):this.schema.items?c({},this.schema.items):{}},getItemInfo:function(a){var b=this.getItemSchema(a);this.item_info=this.item_info||{};var c=JSON.stringify(b);return"undefined"!=typeof this.item_info[c]?this.item_info[c]:(b=this.jsoneditor.expandRefs(b),this.item_info[c]={title:b.title||"item","default":b.default,width:12,child_editors:b.properties||b.items},this.item_info[c])},getElementEditor:function(a){var b=this.getItemInfo(a),c=this.getItemSchema(a);c=this.jsoneditor.expandRefs(c),c.title=b.title+" "+(a+1);var d,e=this.jsoneditor.getEditorClass(c);d=this.tabs_holder?this.theme.getTabContent():b.child_editors?this.theme.getChildEditorHolder():this.theme.getIndentedPanel(),this.row_holder.appendChild(d);var f=this.jsoneditor.createEditor(e,{jsoneditor:this.jsoneditor,schema:c,container:d,path:this.path+"."+a,parent:this,required:!0});return f.preBuild(),f.build(),f.postBuild(),f.title_controls||(f.array_controls=this.theme.getButtonHolder(),d.appendChild(f.array_controls)),f},destroy:function(){this.empty(!0),this.title&&this.title.parentNode&&this.title.parentNode.removeChild(this.title),this.description&&this.description.parentNode&&this.description.parentNode.removeChild(this.description),this.row_holder&&this.row_holder.parentNode&&this.row_holder.parentNode.removeChild(this.row_holder),this.controls&&this.controls.parentNode&&this.controls.parentNode.removeChild(this.controls),this.panel&&this.panel.parentNode&&this.panel.parentNode.removeChild(this.panel),this.rows=this.row_cache=this.title=this.description=this.row_holder=this.panel=this.controls=null,this._super()},empty:function(a){if(this.rows){var b=this;d(this.rows,function(c,d){a&&(d.tab&&d.tab.parentNode&&d.tab.parentNode.removeChild(d.tab),b.destroyRow(d,!0),b.row_cache[c]=null),b.rows[c]=null}),b.rows=[],a&&(b.row_cache=[])}},destroyRow:function(a,b){var c=a.container;b?(a.destroy(),c.parentNode&&c.parentNode.removeChild(c),a.tab&&a.tab.parentNode&&a.tab.parentNode.removeChild(a.tab)):(a.tab&&(a.tab.style.display="none"),c.style.display="none",a.unregister())},getMax:function(){return Array.isArray(this.schema.items)&&this.schema.additionalItems===!1?Math.min(this.schema.items.length,this.schema.maxItems||1/0):this.schema.maxItems||1/0},refreshTabs:function(a){var b=this;d(this.rows,function(c,d){d.tab&&(a?d.tab_text.textContent=d.getHeaderText():d.tab===b.active_tab?(b.theme.markTabActive(d.tab),d.container.style.display=""):(b.theme.markTabInactive(d.tab),d.container.style.display="none"))})},setValue:function(a,b){a=a||[],Array.isArray(a)||(a=[a]);var c=JSON.stringify(a);if(c!==this.serialized){if(this.schema.minItems)for(;a.lengththis.getMax()&&(a=a.slice(0,this.getMax()));var e=this;d(a,function(a,c){e.rows[a]?e.rows[a].setValue(c,b):e.row_cache[a]?(e.rows[a]=e.row_cache[a],e.rows[a].setValue(c,b),e.rows[a].container.style.display="",e.rows[a].tab&&(e.rows[a].tab.style.display=""),e.rows[a].register()):e.addRow(c,b)});for(var f=a.length;f=this.rows.length;d(this.rows,function(a,c){c.movedown_buttons&&(c.movedown_button.style.display=a===b.rows.length-1?"none":""),c.delete_button&&(c.delete_button.style.display=e?"none":""),b.value[a]=c.getValue()});var f=!1;this.value.length?1===this.value.length?(this.remove_all_rows_button.style.display="none",e||this.hide_delete_buttons?this.delete_last_row_button.style.display="none":(this.delete_last_row_button.style.display="",f=!0)):e||this.hide_delete_buttons?(this.delete_last_row_button.style.display="none",this.remove_all_rows_button.style.display="none"):(this.delete_last_row_button.style.display="",this.remove_all_rows_button.style.display="",f=!0):(this.delete_last_row_button.style.display="none",this.remove_all_rows_button.style.display="none"),this.getMax()&&this.getMax()<=this.rows.length||this.hide_add_button?this.add_row_button.style.display="none":(this.add_row_button.style.display="",f=!0),this.controls.style.display=!this.collapsed&&f?"inline-block":"none"}},addRow:function(a,b){var c=this,e=this.rows.length;c.rows[e]=this.getElementEditor(e),c.row_cache[e]=c.rows[e],c.tabs_holder&&(c.rows[e].tab_text=document.createElement("span"),c.rows[e].tab_text.textContent=c.rows[e].getHeaderText(),c.rows[e].tab=c.theme.getTab(c.rows[e].tab_text),c.rows[e].tab.addEventListener("click",function(a){c.active_tab=c.rows[e].tab,c.refreshTabs(),a.preventDefault(),a.stopPropagation()}),c.theme.addTab(c.tabs_holder,c.rows[e].tab));var f=c.rows[e].title_controls||c.rows[e].array_controls;c.hide_delete_buttons||(c.rows[e].delete_button=this.getButton(c.getItemTitle(),"delete","Delete "+c.getItemTitle()),c.rows[e].delete_button.className+=" delete",c.rows[e].delete_button.setAttribute("data-i",e),c.rows[e].delete_button.addEventListener("click",function(a){a.preventDefault(),a.stopPropagation();var b=1*this.getAttribute("data-i"),e=c.getValue(),f=[],g=null;d(e,function(a,d){return a===b?void(c.rows[a].tab===c.active_tab&&(c.rows[a+1]?g=c.rows[a].tab:a&&(g=c.rows[a-1].tab))):void f.push(d)}),c.setValue(f),g&&(c.active_tab=g,c.refreshTabs()),c.onChange(!0)}),f&&f.appendChild(c.rows[e].delete_button)),e&&!c.hide_move_buttons&&(c.rows[e].moveup_button=this.getButton("","moveup","Move up"),c.rows[e].moveup_button.className+=" moveup",c.rows[e].moveup_button.setAttribute("data-i",e),c.rows[e].moveup_button.addEventListener("click",function(a){a.preventDefault(),a.stopPropagation();var b=1*this.getAttribute("data-i");if(!(0>=b)){var d=c.getValue(),e=d[b-1];d[b-1]=d[b],d[b]=e,c.setValue(d),c.active_tab=c.rows[b-1].tab,c.refreshTabs(),c.onChange(!0)}}),f&&f.appendChild(c.rows[e].moveup_button)),c.hide_move_buttons||(c.rows[e].movedown_button=this.getButton("","movedown","Move down"),c.rows[e].movedown_button.className+=" movedown",c.rows[e].movedown_button.setAttribute("data-i",e),c.rows[e].movedown_button.addEventListener("click",function(a){a.preventDefault(),a.stopPropagation();var b=1*this.getAttribute("data-i"),d=c.getValue();if(!(b>=d.length-1)){var e=d[b+1];d[b+1]=d[b],d[b]=e,c.setValue(d),c.active_tab=c.rows[b+1].tab,c.refreshTabs(),c.onChange(!0)}}),f&&f.appendChild(c.rows[e].movedown_button)),a&&c.rows[e].setValue(a,b),c.refreshTabs()},addControls:function(){var a=this;this.collapsed=!1,this.toggle_button=this.getButton("","collapse","Collapse"),this.title_controls.appendChild(this.toggle_button);var b=a.row_holder.style.display,c=a.controls.style.display;this.toggle_button.addEventListener("click",function(d){d.preventDefault(),d.stopPropagation(),a.collapsed?(a.collapsed=!1,a.panel&&(a.panel.style.display=""),a.row_holder.style.display=b,a.tabs_holder&&(a.tabs_holder.style.display=""),a.controls.style.display=c,a.setButtonText(this,"","collapse","Collapse")):(a.collapsed=!0,a.row_holder.style.display="none",a.tabs_holder&&(a.tabs_holder.style.display="none"),a.controls.style.display="none",a.panel&&(a.panel.style.display="none"),a.setButtonText(this,"","expand","Expand"))}),this.options.collapsed&&e(this.toggle_button,"click"),this.schema.options&&"undefined"!=typeof this.schema.options.disable_collapse?this.schema.options.disable_collapse&&(this.toggle_button.style.display="none"):this.jsoneditor.options.disable_collapse&&(this.toggle_button.style.display="none"),this.add_row_button=this.getButton(this.getItemTitle(),"add","Add "+this.getItemTitle()),this.add_row_button.addEventListener("click",function(b){b.preventDefault(),b.stopPropagation();var c=a.rows.length;a.row_cache[c]?(a.rows[c]=a.row_cache[c],a.rows[c].container.style.display="",a.rows[c].tab&&(a.rows[c].tab.style.display=""),a.rows[c].register()):a.addRow(),a.active_tab=a.rows[c].tab,a.refreshTabs(),a.refreshValue(),a.onChange(!0)}),a.controls.appendChild(this.add_row_button),this.delete_last_row_button=this.getButton("Last "+this.getItemTitle(),"delete","Delete Last "+this.getItemTitle()),this.delete_last_row_button.addEventListener("click",function(b){b.preventDefault(),b.stopPropagation();var c=a.getValue(),d=null;a.rows.length>1&&a.rows[a.rows.length-1].tab===a.active_tab&&(d=a.rows[a.rows.length-2].tab),c.pop(),a.setValue(c),d&&(a.active_tab=d,a.refreshTabs()),a.onChange(!0)}),a.controls.appendChild(this.delete_last_row_button),this.remove_all_rows_button=this.getButton("All","delete","Delete All"),this.remove_all_rows_button.addEventListener("click",function(b){b.preventDefault(),b.stopPropagation(),a.setValue([]),a.onChange(!0)}),a.controls.appendChild(this.remove_all_rows_button),a.tabs&&(this.add_row_button.style.width="100%",this.add_row_button.style.textAlign="left",this.add_row_button.style.marginBottom="3px",this.delete_last_row_button.style.width="100%",this.delete_last_row_button.style.textAlign="left",this.delete_last_row_button.style.marginBottom="3px",this.remove_all_rows_button.style.width="100%",this.remove_all_rows_button.style.textAlign="left",this.remove_all_rows_button.style.marginBottom="3px")},showValidationErrors:function(a){var b=this,c=[],e=[];if(d(a,function(a,d){d.path===b.path?c.push(d):e.push(d)}),this.error_holder)if(c.length){this.error_holder.innerHTML="",this.error_holder.style.display="",d(c,function(a,c){b.error_holder.appendChild(b.theme.getErrorMessage(c.message))})}else this.error_holder.style.display="none";d(this.rows,function(a,b){b.showValidationErrors(e)})}}),f.defaults.editors.table=f.defaults.editors.array.extend({register:function(){if(this._super(),this.rows)for(var a=0;athis.schema.maxItems&&(a=a.slice(0,this.schema.maxItems));var c=JSON.stringify(a);if(c!==this.serialized){var e=!1,f=this;d(a,function(a,b){f.rows[a]?f.rows[a].setValue(b):(f.addRow(b),e=!0)});for(var g=a.length;g=this.rows.length,c=!1;d(this.rows,function(d,e){e.movedown_button&&(d===a.rows.length-1?e.movedown_button.style.display="none":(c=!0,e.movedown_button.style.display="")),e.delete_button&&(b?e.delete_button.style.display="none":(c=!0,e.delete_button.style.display="")),e.moveup_button&&(c=!0)}),d(this.rows,function(a,b){b.controls_cell.style.display=c?"":"none"}),this.controls_header_cell.style.display=c?"":"none";var e=!1;this.value.length?1===this.value.length||this.hide_delete_buttons?(this.table.style.display="",this.remove_all_rows_button.style.display="none",b||this.hide_delete_buttons?this.delete_last_row_button.style.display="none":(this.delete_last_row_button.style.display="",e=!0)):(this.table.style.display="",b||this.hide_delete_buttons?(this.delete_last_row_button.style.display="none",this.remove_all_rows_button.style.display="none"):(this.delete_last_row_button.style.display="",this.remove_all_rows_button.style.display="",e=!0)):(this.delete_last_row_button.style.display="none",this.remove_all_rows_button.style.display="none",this.table.style.display="none"),this.schema.maxItems&&this.schema.maxItems<=this.rows.length||this.hide_add_button?this.add_row_button.style.display="none":(this.add_row_button.style.display="",e=!0),this.controls.style.display=e?"":"none"},refreshValue:function(){var a=this;this.value=[],d(this.rows,function(b,c){a.value[b]=c.getValue()}),this.serialized=JSON.stringify(this.value)},addRow:function(a){var b=this,c=this.rows.length;b.rows[c]=this.getElementEditor(c);var e=b.rows[c].table_controls;this.hide_delete_buttons||(b.rows[c].delete_button=this.getButton("","delete","Delete"),b.rows[c].delete_button.className+=" delete",b.rows[c].delete_button.setAttribute("data-i",c),b.rows[c].delete_button.addEventListener("click",function(a){a.preventDefault(),a.stopPropagation();var c=1*this.getAttribute("data-i"),e=b.getValue(),f=[];d(e,function(a,b){a!==c&&f.push(b)}),b.setValue(f),b.onChange(!0)}),e.appendChild(b.rows[c].delete_button)),c&&!this.hide_move_buttons&&(b.rows[c].moveup_button=this.getButton("","moveup","Move up"),b.rows[c].moveup_button.className+=" moveup",b.rows[c].moveup_button.setAttribute("data-i",c),b.rows[c].moveup_button.addEventListener("click",function(a){a.preventDefault(),a.stopPropagation();var c=1*this.getAttribute("data-i");if(!(0>=c)){var d=b.getValue(),e=d[c-1];d[c-1]=d[c],d[c]=e,b.setValue(d),b.onChange(!0)}}),e.appendChild(b.rows[c].moveup_button)),this.hide_move_buttons||(b.rows[c].movedown_button=this.getButton("","movedown","Move down"),b.rows[c].movedown_button.className+=" movedown",b.rows[c].movedown_button.setAttribute("data-i",c),b.rows[c].movedown_button.addEventListener("click",function(a){a.preventDefault(),a.stopPropagation();var c=1*this.getAttribute("data-i"),d=b.getValue();if(!(c>=d.length-1)){var e=d[c+1];d[c+1]=d[c],d[c]=e,b.setValue(d),b.onChange(!0)}}),e.appendChild(b.rows[c].movedown_button)),a&&b.rows[c].setValue(a)},addControls:function(){var a=this;this.collapsed=!1,this.toggle_button=this.getButton("","collapse","Collapse"),this.title_controls&&(this.title_controls.appendChild(this.toggle_button),this.toggle_button.addEventListener("click",function(b){b.preventDefault(),b.stopPropagation(),a.collapsed?(a.collapsed=!1,a.panel.style.display="",a.setButtonText(this,"","collapse","Collapse")):(a.collapsed=!0,a.panel.style.display="none",a.setButtonText(this,"","expand","Expand"))}),this.options.collapsed&&e(this.toggle_button,"click"),this.schema.options&&"undefined"!=typeof this.schema.options.disable_collapse?this.schema.options.disable_collapse&&(this.toggle_button.style.display="none"):this.jsoneditor.options.disable_collapse&&(this.toggle_button.style.display="none")),this.add_row_button=this.getButton(this.getItemTitle(),"add","Add "+this.getItemTitle()),this.add_row_button.addEventListener("click",function(b){b.preventDefault(),b.stopPropagation(),a.addRow(),a.refreshValue(),a.refreshRowButtons(),a.onChange(!0)}),a.controls.appendChild(this.add_row_button),this.delete_last_row_button=this.getButton("Last "+this.getItemTitle(),"delete","Delete Last "+this.getItemTitle()),this.delete_last_row_button.addEventListener("click",function(b){b.preventDefault(),b.stopPropagation();var c=a.getValue();c.pop(),a.setValue(c),a.onChange(!0)}),a.controls.appendChild(this.delete_last_row_button),this.remove_all_rows_button=this.getButton("All","delete","Delete All"),this.remove_all_rows_button.addEventListener("click",function(b){b.preventDefault(),b.stopPropagation(),a.setValue([]),a.onChange(!0)}),a.controls.appendChild(this.remove_all_rows_button)}}),f.defaults.editors.multiple=f.AbstractEditor.extend({register:function(){if(this.editors){for(var a=0;anull";if("object"==typeof a){var c="";return d(a,function(d,e){var f=b.getHTML(e);Array.isArray(a)||(f="
"+d+": "+f+"
"),c+="
  • "+f+"
  • "}),c=Array.isArray(a)?"
      "+c+"
    ":"
      "+c+"
    "}return"boolean"==typeof a?a?"true":"false":"string"==typeof a?a.replace(/&/g,"&").replace(//g,">"):a},setValue:function(a){this.value!==a&&(this.value=a,this.refreshValue(),this.onChange())},destroy:function(){this.display_area&&this.display_area.parentNode&&this.display_area.parentNode.removeChild(this.display_area),this.title&&this.title.parentNode&&this.title.parentNode.removeChild(this.title),this.switcher&&this.switcher.parentNode&&this.switcher.parentNode.removeChild(this.switcher),this._super()}}),f.defaults.editors.select=f.AbstractEditor.extend({setValue:function(a){a=this.typecast(a||"");var b=a;this.enum_values.indexOf(b)<0&&(b=this.enum_values[0]),this.value!==b&&(this.input.value=this.enum_options[this.enum_values.indexOf(b)],this.select2&&this.select2.select2("val",this.input.value),this.value=b,this.onChange())},register:function(){this._super(),this.input&&this.input.setAttribute("name",this.formname)},unregister:function(){this._super(),this.input&&this.input.removeAttribute("name")},getNumColumns:function(){if(!this.enum_options)return 3;for(var a=this.getTitle().length,b=0;b2||this.enum_options.length&&this.enumSource)){var a=c({},f.plugins.select2);this.schema.options&&this.schema.options.select2_options&&(a=c(a,this.schema.options.select2_options)),this.select2=window.jQuery(this.input).select2(a);var b=this;this.select2.on("select2-blur",function(){b.input.value=b.select2.select2("val"),b.onInputChange()})}else this.select2=null},postBuild:function(){this._super(),this.theme.afterInputReady(this.input),this.setupSelect2()},onWatchedFieldChange:function(){var a,b;if(this.enumSource){a=this.getWatchedFieldValues();for(var c=[],d=[],e=0;eType: "+a+", Size: "+Math.floor((this.value.length-this.value.split(",")[0].length-1)/1.33333)+" bytes","image"===a.substr(0,5)){this.preview.innerHTML+="
    ";var b=document.createElement("img");b.style.maxWidth="100%",b.style.maxHeight="100px",b.src=this.value,this.preview.appendChild(b)}}else this.preview.innerHTML="Invalid data URI"}},enable:function(){this.uploader&&(this.uploader.disabled=!1),this._super()},disable:function(){this.uploader&&(this.uploader.disabled=!0),this._super()},setValue:function(a){this.value!==a&&(this.value=a,this.input.value=this.value,this.refreshPreview(),this.onChange())},destroy:function(){this.preview&&this.preview.parentNode&&this.preview.parentNode.removeChild(this.preview),this.title&&this.title.parentNode&&this.title.parentNode.removeChild(this.title),this.input&&this.input.parentNode&&this.input.parentNode.removeChild(this.input),this.uploader&&this.uploader.parentNode&&this.uploader.parentNode.removeChild(this.uploader),this._super()}}),f.defaults.editors.upload=f.AbstractEditor.extend({getNumColumns:function(){return 4},build:function(){var a=this;if(this.title=this.header=this.label=this.theme.getFormInputLabel(this.getTitle()),this.input=this.theme.getFormInputField("hidden"),this.container.appendChild(this.input),!this.schema.readOnly&&!this.schema.readonly){if(!this.jsoneditor.options.upload)throw"Upload handler required for upload editor";this.uploader=this.theme.getFormInputField("file"),this.uploader.addEventListener("change",function(b){if(b.preventDefault(),b.stopPropagation(),this.files&&this.files.length){var c=new FileReader;c.onload=function(b){a.preview_value=b.target.result,a.refreshPreview(),a.onChange(!0),c=null},c.readAsDataURL(this.files[0])}})}var b=this.schema.description;b||(b=""),this.preview=this.theme.getFormInputDescription(b),this.container.appendChild(this.preview),this.control=this.theme.getFormControl(this.label,this.uploader||this.input,this.preview),this.container.appendChild(this.control)},refreshPreview:function(){if(this.last_preview!==this.preview_value&&(this.last_preview=this.preview_value,this.preview.innerHTML="",this.preview_value)){var a=this,b=this.preview_value.match(/^data:([^;,]+)[;,]/);b&&(b=b[1]),b||(b="unknown");var c=this.uploader.files[0];if(this.preview.innerHTML="Type: "+b+", Size: "+c.size+" bytes","image"===b.substr(0,5)){this.preview.innerHTML+="
    ";var d=document.createElement("img");d.style.maxWidth="100%",d.style.maxHeight="100px",d.src=this.preview_value,this.preview.appendChild(d)}this.preview.innerHTML+="
    ";var e=this.getButton("Upload","upload","Upload");this.preview.appendChild(e),e.addEventListener("click",function(b){b.preventDefault(),e.setAttribute("disabled","disabled"),a.theme.removeInputError(a.uploader),a.theme.getProgressBar&&(a.progressBar=a.theme.getProgressBar(),a.preview.appendChild(a.progressBar)),a.jsoneditor.options.upload(a.path,c,{success:function(b){a.setValue(b),a.parent?a.parent.onChildEditorChange(a):a.jsoneditor.onChange(),a.progressBar&&a.preview.removeChild(a.progressBar),e.removeAttribute("disabled")},failure:function(b){a.theme.addInputError(a.uploader,b),a.progressBar&&a.preview.removeChild(a.progressBar),e.removeAttribute("disabled")},updateProgress:function(b){a.progressBar&&(b?a.theme.updateProgressBar(a.progressBar,b):a.theme.updateProgressBarUnknown(a.progressBar))}})})}},enable:function(){this.uploader&&(this.uploader.disabled=!1),this._super()},disable:function(){this.uploader&&(this.uploader.disabled=!0),this._super()},setValue:function(a){this.value!==a&&(this.value=a,this.input.value=this.value,this.onChange())},destroy:function(){this.preview&&this.preview.parentNode&&this.preview.parentNode.removeChild(this.preview),this.title&&this.title.parentNode&&this.title.parentNode.removeChild(this.title),this.input&&this.input.parentNode&&this.input.parentNode.removeChild(this.input),this.uploader&&this.uploader.parentNode&&this.uploader.parentNode.removeChild(this.uploader),this._super()}}),f.AbstractTheme=a.extend({getContainer:function(){return document.createElement("div")},getFloatRightLinkHolder:function(){var a=document.createElement("div");return a.style=a.style||{},a.style.cssFloat="right",a.style.marginLeft="10px",a},getModal:function(){var a=document.createElement("div");return a.style.backgroundColor="white",a.style.border="1px solid black",a.style.boxShadow="3px 3px black",a.style.position="absolute",a.style.zIndex="10",a.style.display="none",a},getGridContainer:function(){var a=document.createElement("div");return a},getGridRow:function(){var a=document.createElement("div");return a.className="row",a},getGridColumn:function(){var a=document.createElement("div");return a},setGridColumnSize:function(){},getLink:function(a){var b=document.createElement("a");return b.setAttribute("href","#"),b.appendChild(document.createTextNode(a)),b},disableHeader:function(a){a.style.color="#ccc"},disableLabel:function(a){a.style.color="#ccc"},enableHeader:function(a){a.style.color=""},enableLabel:function(a){a.style.color=""},getFormInputLabel:function(a){var b=document.createElement("label");return b.appendChild(document.createTextNode(a)),b},getCheckboxLabel:function(a){var b=this.getFormInputLabel(a);return b.style.fontWeight="normal",b},getHeader:function(a){var b=document.createElement("h3");return"string"==typeof a?b.textContent=a:b.appendChild(a),b},getCheckbox:function(){var a=this.getFormInputField("checkbox");return a.style.display="inline-block",a.style.width="auto",a},getMultiCheckboxHolder:function(a,b,c){var d=document.createElement("div");b&&(b.style.display="block",d.appendChild(b));for(var e in a)a.hasOwnProperty(e)&&(a[e].style.display="inline-block",a[e].style.marginRight="20px",d.appendChild(a[e]));return c&&d.appendChild(c),d},getSelectInput:function(a){var b=document.createElement("select");return a&&this.setSelectOptions(b,a),b},getSwitcher:function(a){var b=this.getSelectInput(a);return b.style.backgroundColor="transparent",b.style.height="auto",b.style.fontStyle="italic",b.style.fontWeight="normal",b.style.padding="0 0 0 3px",b},getSwitcherOptions:function(a){return a.getElementsByTagName("option")},setSwitcherOptions:function(a,b,c){this.setSelectOptions(a,b,c)},setSelectOptions:function(a,b,c){c=c||[],a.innerHTML="";for(var d=0;d'),a.errmsg=a.parentNode.getElementsByClassName("errormsg")[0]),a.errmsg.textContent=b)},removeInputError:function(a){a.errmsg&&(a.group.className=a.group.className.replace(/ error/g,""),a.errmsg.style.display="none")},getProgressBar:function(){var a=document.createElement("div");a.className="progress";var b=document.createElement("span");return b.className="meter",b.style.width="0%",a.appendChild(b),a},updateProgressBar:function(a,b){a&&(a.firstChild.style.width=b+"%")},updateProgressBarUnknown:function(a){a&&(a.firstChild.style.width="100%")}}),f.defaults.themes.foundation3=f.defaults.themes.foundation.extend({getHeaderButtonHolder:function(){var a=this._super();return a.style.fontSize=".6em",a},getFormInputLabel:function(a){var b=this._super(a);return b.style.fontWeight="bold",b},getTabHolder:function(){var a=document.createElement("div");return a.className="row",a.innerHTML="
    ",a},setGridColumnSize:function(a,b){var c=["zero","one","two","three","four","five","six","seven","eight","nine","ten","eleven","twelve"];a.className="columns "+c[b]},getTab:function(a){var b=document.createElement("dd"),c=document.createElement("a");return c.setAttribute("href","#"),c.appendChild(a),b.appendChild(c),b},getTabContentHolder:function(a){return a.children[1]},getTabContent:function(){var a=document.createElement("div");return a.className="content active",a.style.paddingLeft="5px",a},markTabActive:function(a){a.className+=" active"},markTabInactive:function(a){a.className=a.className.replace(/\s*active/g,"")},addTab:function(a,b){a.children[0].appendChild(b)}}),f.defaults.themes.foundation4=f.defaults.themes.foundation.extend({getHeaderButtonHolder:function(){var a=this._super();return a.style.fontSize=".6em",a},setGridColumnSize:function(a,b){a.className="columns large-"+b},getFormInputDescription:function(a){var b=this._super(a);return b.style.fontSize=".8rem",b},getFormInputLabel:function(a){var b=this._super(a);return b.style.fontWeight="bold",b}}),f.defaults.themes.foundation5=f.defaults.themes.foundation.extend({getFormInputDescription:function(a){var b=this._super(a);return b.style.fontSize=".8rem",b},setGridColumnSize:function(a,b){a.className="columns medium-"+b},getButton:function(a,b,c){var d=this._super(a,b,c);return d.className=d.className.replace(/\s*small/g,"")+" tiny",d},getTabHolder:function(){var a=document.createElement("div");return a.innerHTML="
    ",a},getTab:function(a){var b=document.createElement("dd"),c=document.createElement("a");return c.setAttribute("href","#"),c.appendChild(a),b.appendChild(c),b},getTabContentHolder:function(a){return a.children[1]},getTabContent:function(){var a=document.createElement("div");return a.className="content active",a.style.paddingLeft="5px",a},markTabActive:function(a){a.className+=" active"},markTabInactive:function(a){a.className=a.className.replace(/\s*active/g,"")},addTab:function(a,b){a.children[0].appendChild(b)}}),f.defaults.themes.html=f.AbstractTheme.extend({getFormInputLabel:function(a){var b=this._super(a);return b.style.display="block",b.style.marginBottom="3px",b.style.fontWeight="bold",b},getFormInputDescription:function(a){var b=this._super(a);return b.style.fontSize=".8em",b.style.margin=0,b.style.display="inline-block",b.style.fontStyle="italic",b},getIndentedPanel:function(){var a=this._super();return a.style.border="1px solid #ddd",a.style.padding="5px",a.style.margin="5px",a.style.borderRadius="3px",a},getChildEditorHolder:function(){var a=this._super();return a.style.marginBottom="8px",a},getHeaderButtonHolder:function(){var a=this.getButtonHolder();return a.style.display="inline-block",a.style.marginLeft="10px",a.style.fontSize=".8em",a.style.verticalAlign="middle",a},getTable:function(){var a=this._super();return a.style.borderBottom="1px solid #ccc",a.style.marginBottom="5px",a},addInputError:function(a,b){if(a.style.borderColor="red",a.errmsg)a.errmsg.style.display="block";else{var c=this.closest(a,".form-control");a.errmsg=document.createElement("div"),a.errmsg.setAttribute("class","errmsg"),a.errmsg.style=a.errmsg.style||{},a.errmsg.style.color="red",c.appendChild(a.errmsg)}a.errmsg.innerHTML="",a.errmsg.appendChild(document.createTextNode(b))},removeInputError:function(a){a.style.borderColor="",a.errmsg&&(a.errmsg.style.display="none")},getProgressBar:function(){var a=100,b=0,c=document.createElement("progress");return c.setAttribute("max",a),c.setAttribute("value",b),c},updateProgressBar:function(a,b){a&&a.setAttribute("value",b)},updateProgressBarUnknown:function(a){a&&a.removeAttribute("value")}}),f.defaults.themes.jqueryui=f.AbstractTheme.extend({getTable:function(){var a=this._super();return a.setAttribute("cellpadding",5),a.setAttribute("cellspacing",0),a},getTableHeaderCell:function(a){var b=this._super(a);return b.className="ui-state-active",b.style.fontWeight="bold",b},getTableCell:function(){var a=this._super();return a.className="ui-widget-content",a},getHeaderButtonHolder:function(){var a=this.getButtonHolder();return a.style.marginLeft="10px",a.style.fontSize=".6em",a.style.display="inline-block",a},getFormInputDescription:function(a){var b=this.getDescription(a);return b.style.marginLeft="10px",b.style.display="inline-block",b},getFormControl:function(a,b,c){var d=this._super(a,b,c);return"checkbox"===b.type?(d.style.lineHeight="25px",d.style.padding="3px 0"):d.style.padding="4px 0 8px 0",d},getDescription:function(a){var b=document.createElement("span");return b.style.fontSize=".8em",b.style.fontStyle="italic",b.textContent=a,b},getButtonHolder:function(){var a=document.createElement("div");return a.className="ui-buttonset",a.style.fontSize=".7em",a},getFormInputLabel:function(a){var b=document.createElement("label");return b.style.fontWeight="bold",b.style.display="block",b.textContent=a,b},getButton:function(a,b,c){var d=document.createElement("button");d.className="ui-button ui-widget ui-state-default ui-corner-all",b&&!a?(d.className+=" ui-button-icon-only",b.className+=" ui-button-icon-primary ui-icon-primary",d.appendChild(b)):b?(d.className+=" ui-button-text-icon-primary",b.className+=" ui-button-icon-primary ui-icon-primary",d.appendChild(b)):d.className+=" ui-button-text-only";var e=document.createElement("span");return e.className="ui-button-text",e.textContent=a||c||".",d.appendChild(e),d.setAttribute("title",c),d},setButtonText:function(a,b,c,d){a.innerHTML="",a.className="ui-button ui-widget ui-state-default ui-corner-all",c&&!b?(a.className+=" ui-button-icon-only",c.className+=" ui-button-icon-primary ui-icon-primary",a.appendChild(c)):c?(a.className+=" ui-button-text-icon-primary",c.className+=" ui-button-icon-primary ui-icon-primary",a.appendChild(c)):a.className+=" ui-button-text-only";var e=document.createElement("span");e.className="ui-button-text",e.textContent=b||d||".",a.appendChild(e),a.setAttribute("title",d)},getIndentedPanel:function(){var a=document.createElement("div");return a.className="ui-widget-content ui-corner-all",a.style.padding="1em 1.4em",a.style.marginBottom="20px",a},afterInputReady:function(a){a.controls||(a.controls=this.closest(a,".form-control"))},addInputError:function(a,b){a.controls&&(a.errmsg?a.errmsg.style.display="":(a.errmsg=document.createElement("div"),a.errmsg.className="ui-state-error",a.controls.appendChild(a.errmsg)),a.errmsg.textContent=b)},removeInputError:function(a){a.errmsg&&(a.errmsg.style.display="none")},markTabActive:function(a){a.className=a.className.replace(/\s*ui-widget-header/g,"")+" ui-state-active"},markTabInactive:function(a){a.className=a.className.replace(/\s*ui-state-active/g,"")+" ui-widget-header"}}),f.AbstractIconLib=a.extend({mapping:{collapse:"",expand:"","delete":"",edit:"",add:"",cancel:"",save:"",moveup:"",movedown:""},icon_prefix:"",getIconClass:function(a){return this.mapping[a]?this.icon_prefix+this.mapping[a]:null},getIcon:function(a){var b=this.getIconClass(a);if(!b)return null;var c=document.createElement("i");return c.className=b,c}}),f.defaults.iconlibs.bootstrap2=f.AbstractIconLib.extend({mapping:{collapse:"chevron-down",expand:"chevron-up","delete":"trash",edit:"pencil",add:"plus",cancel:"ban-circle",save:"ok",moveup:"arrow-up",movedown:"arrow-down"},icon_prefix:"icon-"}),f.defaults.iconlibs.bootstrap3=f.AbstractIconLib.extend({mapping:{collapse:"chevron-down",expand:"chevron-right","delete":"remove",edit:"pencil",add:"plus",cancel:"floppy-remove",save:"floppy-saved",moveup:"arrow-up",movedown:"arrow-down"},icon_prefix:"glyphicon glyphicon-"}),f.defaults.iconlibs.fontawesome3=f.AbstractIconLib.extend({mapping:{collapse:"chevron-down",expand:"chevron-right","delete":"remove",edit:"pencil",add:"plus",cancel:"ban-circle",save:"save",moveup:"arrow-up",movedown:"arrow-down"},icon_prefix:"icon-"}),f.defaults.iconlibs.fontawesome4=f.AbstractIconLib.extend({mapping:{collapse:"caret-square-o-down",expand:"caret-square-o-right","delete":"times",edit:"pencil",add:"plus",cancel:"ban",save:"save",moveup:"arrow-up",movedown:"arrow-down"},icon_prefix:"fa fa-"}),f.defaults.iconlibs.foundation2=f.AbstractIconLib.extend({mapping:{collapse:"minus",expand:"plus","delete":"remove",edit:"edit",add:"add-doc",cancel:"error",save:"checkmark",moveup:"up-arrow",movedown:"down-arrow"},icon_prefix:"foundicon-"}),f.defaults.iconlibs.foundation3=f.AbstractIconLib.extend({mapping:{collapse:"minus",expand:"plus","delete":"x",edit:"pencil",add:"page-add",cancel:"x-circle",save:"save",moveup:"arrow-up",movedown:"arrow-down"},icon_prefix:"fi-"}),f.defaults.iconlibs.jqueryui=f.AbstractIconLib.extend({mapping:{collapse:"triangle-1-s",expand:"triangle-1-e","delete":"trash",edit:"pencil",add:"plusthick",cancel:"closethick",save:"disk",moveup:"arrowthick-1-n",movedown:"arrowthick-1-s"},icon_prefix:"ui-icon ui-icon-"}),f.defaults.templates.default=function(){var a=function(b){var e={};return d(b,function(b,f){if("object"==typeof f&&null!==f){var g={};d(f,function(a,c){g[b+"."+a]=c}),c(e,a(g))}else e[b]=f}),e};return{compile:function(b){return function(c){var e=a(c),f=b+"";return d(e,function(a,b){f=f.replace(new RegExp("{{\\s*"+a+"\\s*}}","g"),b)}),f}}}},f.defaults.templates.ejs=function(){return window.EJS?{compile:function(a){var b=new window.EJS({text:a});return function(a){return b.render(a)}}}:!1},f.defaults.templates.handlebars=function(){return window.Handlebars},f.defaults.templates.hogan=function(){return window.Hogan?{compile:function(a){var b=window.Hogan.compile(a);return function(a){return b.render(a)}}}:!1},f.defaults.templates.markup=function(){return window.Mark&&window.Mark.up?{compile:function(a){return function(b){return window.Mark.up(a,b)}}}:!1},f.defaults.templates.mustache=function(){return window.Mustache?{compile:function(a){return function(b){return window.Mustache.render(a,b)}}}:!1},f.defaults.templates.swig=function(){return window.swig},f.defaults.templates.underscore=function(){return window._?{compile:function(a){return function(b){return window._.template(a,b)}}}:!1},f.defaults.theme="html",f.defaults.template="default",f.defaults.options={},f.defaults.translate=function(a,b){var c=f.defaults.languages[f.defaults.language];if(!c)throw"Unknown language "+f.defaults.language;var d=c[a]||f.defaults.languages[f.defaults.default_language][a];if("undefined"==typeof d)throw"Unknown translate string "+a;if(b)for(var e=0;e=0?"multiselect":void 0}),f.defaults.resolvers.unshift(function(a){return a.oneOf?"multiple":void 0}),function(){if(window.jQuery||window.Zepto){var a=window.jQuery||window.Zepto;a.jsoneditor=f.defaults,a.fn.jsoneditor=function(a){var b=this,c=this.data("jsoneditor");if("value"===a){if(!c)throw"Must initialize jsoneditor before getting/setting the value";if(!(arguments.length>1))return c.getValue();c.setValue(arguments[1])}else{if("validate"===a){if(!c)throw"Must initialize jsoneditor before validating";return arguments.length>1?c.validate(arguments[1]):c.validate()}"destroy"===a?c&&(c.destroy(),this.data("jsoneditor",null)):(c&&c.destroy(),c=new f(this.get(0),a),this.data("jsoneditor",c),c.on("change",function(){b.trigger("change")}),c.on("ready",function(){b.trigger("ready")}))}return this}}}(),window.JSONEditor=f}(); \ No newline at end of file diff --git a/src/script.js b/src/script.js index 749adec..86eb10d 100644 --- a/src/script.js +++ b/src/script.js @@ -8,7 +8,8 @@ require.config({ 'handlebars': '../lib/handlebars', 'lodash': '../lib/lodash.min', 'flight': '../lib/flight.min', - 'fuse': '../lib/fuse.min' + 'fuse': '../lib/fuse.min', + 'jsoneditor': '../lib/jsoneditor.min' }, shim: { 'handlebars': { @@ -34,6 +35,7 @@ require.config({ define(function(require) { 'use strict'; require('bootstrap'); + require('jsoneditor'); // attach components to the DOM require('ui/map').attachTo('#map'); require('ui/search').attachTo('#search', {mapSelector: '#map'}); diff --git a/src/ui/info.js b/src/ui/info.js index b62f6b1..05bcf42 100644 --- a/src/ui/info.js +++ b/src/ui/info.js @@ -11,6 +11,11 @@ define(function(require, exports, module) { this.configureInfo = function(ev, config) { this.infoConfig = config.properties; + this.editMode = config.edit_mode; + if (this.editMode) { + alert("info edit mode"); + this.editSchema = config.feature_property_json_schema; + } }; this.update = function(ev, feature) { @@ -18,19 +23,42 @@ define(function(require, exports, module) { return; } this.attr.currentFeature = feature; - var popup = templates.popup(this.infoConfig, - feature.properties); var content = this.$node.find("div." + this.attr.contentClass); if (!content.length) { content = $("
    ").addClass(this.attr.contentClass). appendTo(this.$node); } - content.html(popup); + if (this.editMode) { + this.startEditing(content, feature.properties); + } + else { + var popup = templates.popup(this.infoConfig, + feature.properties); + content.html(popup); + } + this.$node.show(); }; + this.startEditing = function(contentNode, props) { + this.killCurrentEditor(); // in case we had one... + this.currentEditor = new JSONEditor(contentNode[0], { + schema: this.editSchema, + startval: props, + theme: "bootstrap3" + }); + } + + this.killCurrentEditor = function() { + if (this.currentEditor) { + this.currentEditor.destroy(); + this.currentEditor = undefined; + } + } + this.hide = function() { this.$node.hide(); + this.killCurrentEditor(); this.trigger(document, 'deselectFeature', this.attr.currentFeature); this.attr.currentFeature = null; }; From a4ab9d86dab15160644f85738485e75ddd7f6e0d Mon Sep 17 00:00:00 2001 From: Robert Thau Date: Tue, 21 Oct 2014 08:52:29 -0400 Subject: [PATCH 11/56] Nuke tabs that crept into previous changes. Late fixup to an editor config screwup on my part. --- config.json | 8 +-- src/data/edit_state.js | 8 +-- src/ui/export.js | 2 +- src/ui/info.js | 20 +++--- src/ui/map.js | 24 +++---- test/spec/data/edit_state_spec.js | 70 +++++++++---------- test/spec/ui/export_spec.js | 52 +++++++------- test/spec/ui/map_spec.js | 108 +++++++++++++++--------------- 8 files changed, 146 insertions(+), 146 deletions(-) diff --git a/config.json b/config.json index 426e0c4..1727cf2 100644 --- a/config.json +++ b/config.json @@ -69,10 +69,10 @@ "address": { "type": "string" }, "web_url": { "type": "string", "format": "url" }, "contact_names": { - "title": "Contacts", - "type": "array", - "format": "table", - "items": { "type": "string" } + "title": "Contacts", + "type": "array", + "format": "table", + "items": { "type": "string" } } } } diff --git a/src/data/edit_state.js b/src/data/edit_state.js index c27fde5..3d9b387 100644 --- a/src/data/edit_state.js +++ b/src/data/edit_state.js @@ -33,15 +33,15 @@ define(function(require, exports, module) { this.selectedFeatureMoved = function(ev, latlng) { if (this.selectedFeature) { - this.selectedFeature.geometry.coordinates = [latlng.lng, latlng.lat]; + this.selectedFeature.geometry.coordinates = [latlng.lng, latlng.lat]; } } this.propEdit = function(ev, newProps) { if (this.selectedFeature) { - // Events may specify values for only *some* properties; - // if so, we want to leave the others alone. - $.extend(this.selectedFeature.properties, newProps); + // Events may specify values for only *some* properties; + // if so, we want to leave the others alone. + $.extend(this.selectedFeature.properties, newProps); } } diff --git a/src/ui/export.js b/src/ui/export.js index ee46133..ce54745 100644 --- a/src/ui/export.js +++ b/src/ui/export.js @@ -8,7 +8,7 @@ define(function(require, exports, module) { this.configure = function(ev, config) { if (!config.edit_mode) { - this.$node.hide(); + this.$node.hide(); } } diff --git a/src/ui/info.js b/src/ui/info.js index 05bcf42..a988578 100644 --- a/src/ui/info.js +++ b/src/ui/info.js @@ -13,8 +13,8 @@ define(function(require, exports, module) { this.infoConfig = config.properties; this.editMode = config.edit_mode; if (this.editMode) { - alert("info edit mode"); - this.editSchema = config.feature_property_json_schema; + alert("info edit mode"); + this.editSchema = config.feature_property_json_schema; } }; @@ -29,12 +29,12 @@ define(function(require, exports, module) { appendTo(this.$node); } if (this.editMode) { - this.startEditing(content, feature.properties); + this.startEditing(content, feature.properties); } else { - var popup = templates.popup(this.infoConfig, + var popup = templates.popup(this.infoConfig, feature.properties); - content.html(popup); + content.html(popup); } this.$node.show(); @@ -43,16 +43,16 @@ define(function(require, exports, module) { this.startEditing = function(contentNode, props) { this.killCurrentEditor(); // in case we had one... this.currentEditor = new JSONEditor(contentNode[0], { - schema: this.editSchema, - startval: props, - theme: "bootstrap3" + schema: this.editSchema, + startval: props, + theme: "bootstrap3" }); } this.killCurrentEditor = function() { if (this.currentEditor) { - this.currentEditor.destroy(); - this.currentEditor = undefined; + this.currentEditor.destroy(); + this.currentEditor = undefined; } } diff --git a/src/ui/map.js b/src/ui/map.js index 81b5f36..f28da6e 100644 --- a/src/ui/map.js +++ b/src/ui/map.js @@ -101,12 +101,12 @@ define(function(require, exports, module) { layer.setIcon(this.grayIcon); this.previouslyClicked = layer; - if (this.edit_mode) { + if (this.edit_mode) { layer.dragging.enable(); - layer.on("dragend", function(ev) { - this.trigger(document, 'selectedFeatureMoved', ev.target.getLatLng()); - }.bind(this)); - } + layer.on("dragend", function(ev) { + this.trigger(document, 'selectedFeatureMoved', ev.target.getLatLng()); + }.bind(this)); + } // re-bind popup to feature with specified preview attribute this.bindPopupToFeature( @@ -122,19 +122,19 @@ define(function(require, exports, module) { this.selectedFeatureMoved = function(ev, latlng) { if (this.previouslyClicked) { - var oldLatLng = this.previouslyClicked.getLatLng(); - if (latlng.lat !== oldLatLng.lat || latlng.lng !== oldLatLng.lng) { - this.previouslyClicked.setLatLng(latlng); - } + var oldLatLng = this.previouslyClicked.getLatLng(); + if (latlng.lat !== oldLatLng.lat || latlng.lng !== oldLatLng.lng) { + this.previouslyClicked.setLatLng(latlng); + } } }; this.deselectFeature = function(ev, feature) { if (this.previouslyClicked) { this.previouslyClicked.setIcon(this.defaultIcon); - if (this.edit_mode) { - this.previouslyClicked.dragging.disable(); - } + if (this.edit_mode) { + this.previouslyClicked.dragging.disable(); + } } var layer = this.attr.features[feature.geometry.coordinates]; // re-bind popup to feature with specified preview attribute diff --git a/test/spec/data/edit_state_spec.js b/test/spec/data/edit_state_spec.js index 559d35f..f170991 100644 --- a/test/spec/data/edit_state_spec.js +++ b/test/spec/data/edit_state_spec.js @@ -7,8 +7,8 @@ define(['test/mock', 'jquery'], function(mock, $) { describe('on data load', function() { it('records the data', function() { - this.component.trigger('data', mock.data); - expect(this.component.data).toEqual(mock.data); + this.component.trigger('data', mock.data); + expect(this.component.data).toEqual(mock.data); }); }); @@ -16,49 +16,49 @@ define(['test/mock', 'jquery'], function(mock, $) { describe('with a selected feature', function() { - beforeEach(function() { + beforeEach(function() { - // We load in a deep copy of the mock data, for test isolation; - // if we loaded in 'mock.data' directly, each test would see - // dinks from all other tests that had previously run. + // We load in a deep copy of the mock data, for test isolation; + // if we loaded in 'mock.data' directly, each test would see + // dinks from all other tests that had previously run. - this.copiedData = $.extend(true, {}, mock.data); - this.component.trigger('data', this.copiedData); + this.copiedData = $.extend(true, {}, mock.data); + this.component.trigger('data', this.copiedData); - this.feature = this.copiedData.features[0]; - $(document).trigger('selectFeature', this.feature); - }); + this.feature = this.copiedData.features[0]; + $(document).trigger('selectFeature', this.feature); + }); - it("records the selected feature for further work", function() { - expect(this.component.selectedFeature).toBe(this.feature); - }); + it("records the selected feature for further work", function() { + expect(this.component.selectedFeature).toBe(this.feature); + }); - it("updates position upon UI request", function() { - $(document).trigger('selectedFeatureMoved', {lat: 55, lng: 44}); - expect(this.feature.geometry.coordinates).toEqual([44, 55]); - expect(this.component.data.features[0].geometry.coordinates).toEqual([44,55]); - }); + it("updates position upon UI request", function() { + $(document).trigger('selectedFeatureMoved', {lat: 55, lng: 44}); + expect(this.feature.geometry.coordinates).toEqual([44, 55]); + expect(this.component.data.features[0].geometry.coordinates).toEqual([44,55]); + }); - it("changes properties on request", function() { - $(document).trigger('selectedFeaturePropsChanged', - {organization_name: "fred"}); - expect(this.feature.properties.organization_name).toBe("fred"); - }); + it("changes properties on request", function() { + $(document).trigger('selectedFeaturePropsChanged', + {organization_name: "fred"}); + expect(this.feature.properties.organization_name).toBe("fred"); + }); - it("leaves props alone unless changed value specified", function() { - var oldUrl = this.feature.properties.web_url; - $(document).trigger('selectedFeaturePropsChanged', - {organization_name: "fred"}); - expect(this.feature.properties.web_url).toBe(oldUrl); - }); + it("leaves props alone unless changed value specified", function() { + var oldUrl = this.feature.properties.web_url; + $(document).trigger('selectedFeaturePropsChanged', + {organization_name: "fred"}); + expect(this.feature.properties.web_url).toBe(oldUrl); + }); }); it('sends edited data back to whoever asks', function() { - spyOnEvent(document, 'callbackEvent'); - this.component.trigger('data', mock.data); - $(document).trigger('requestEditedData', 'callbackEvent') - expect('callbackEvent').toHaveBeenTriggeredOnAndWith( - document, mock.data); + spyOnEvent(document, 'callbackEvent'); + this.component.trigger('data', mock.data); + $(document).trigger('requestEditedData', 'callbackEvent') + expect('callbackEvent').toHaveBeenTriggeredOnAndWith( + document, mock.data); }); }); }); diff --git a/test/spec/ui/export_spec.js b/test/spec/ui/export_spec.js index a50860e..8ba8925 100644 --- a/test/spec/ui/export_spec.js +++ b/test/spec/ui/export_spec.js @@ -15,48 +15,48 @@ define(['test/mock', 'jquery'], function(mock, $) { describe('on config', function() { it('hides widget if edit-mode is off', function() { - spyOn(this.$node, 'hide'); - $(document).trigger('config', mock.config); - expect(this.$node.hide).toHaveBeenCalled(); + spyOn(this.$node, 'hide'); + $(document).trigger('config', mock.config); + expect(this.$node.hide).toHaveBeenCalled(); }); it('leaves widget shown if edit-mode is on', function() { - spyOn(this.$node, 'hide'); - mock.config.edit_mode = true; - $(document).trigger('config', mock.config); - expect(this.$node.hide).not.toHaveBeenCalled(); + spyOn(this.$node, 'hide'); + mock.config.edit_mode = true; + $(document).trigger('config', mock.config); + expect(this.$node.hide).not.toHaveBeenCalled(); }); }); describe('on export', function() { it('requests export data on form submittal', function() { - this.$node.trigger('submit'); - expect('requestEditedData').toHaveBeenTriggeredOnAndWith( - document,'editedDataForSave'); + this.$node.trigger('submit'); + expect('requestEditedData').toHaveBeenTriggeredOnAndWith( + document,'editedDataForSave'); }); it('does an export when triggered', function() { - // Note that we are *not* mocking out the request/response - // protocol that gets the request data from the edit-state - // component; setting up that test environment would be... - // complex. + // Note that we are *not* mocking out the request/response + // protocol that gets the request data from the edit-state + // component; setting up that test environment would be... + // complex. - spyOn(window, 'open'); - this.component.trigger('editedDataForSave', mock.data); - expect(window.open).toHaveBeenCalled(); - - // Verify that the args are as expected... - // For the URL, we just check that it has the right prefix, and - // *some* of the expected text. + spyOn(window, 'open'); + this.component.trigger('editedDataForSave', mock.data); + expect(window.open).toHaveBeenCalled(); + + // Verify that the args are as expected... + // For the URL, we just check that it has the right prefix, and + // *some* of the expected text. - var args = window.open.mostRecentCall.args; - expect(args[1]).toEqual("Exported Finda Data"); + var args = window.open.mostRecentCall.args; + expect(args[1]).toEqual("Exported Finda Data"); - var url = args[0]; - expect(url).toMatch(/^data:text\/plain,/); - expect(url).toMatch(/our-groups-programs/); + var url = args[0]; + expect(url).toMatch(/^data:text\/plain,/); + expect(url).toMatch(/our-groups-programs/); }); }); diff --git a/test/spec/ui/map_spec.js b/test/spec/ui/map_spec.js index 441594f..ccc5378 100644 --- a/test/spec/ui/map_spec.js +++ b/test/spec/ui/map_spec.js @@ -96,9 +96,9 @@ define( }); it('leaves dragging off when not in edit mode', function() { - var feature = mock.data.features[0]; - var marker = this.component.attr.features[feature.geometry.coordinates]; - expect(!marker.dragging._enabled).toBe(true); + var feature = mock.data.features[0]; + var marker = this.component.attr.features[feature.geometry.coordinates]; + expect(!marker.dragging._enabled).toBe(true); }); }); @@ -121,64 +121,64 @@ define( var layer; beforeEach(function() { - // Initialize with deep copies of mock config & data, so we - // don't have to worry about scribbling on the originals. - // - // Also, set 'edit_mode' to true in the mock config. + // Initialize with deep copies of mock config & data, so we + // don't have to worry about scribbling on the originals. + // + // Also, set 'edit_mode' to true in the mock config. this.component.trigger('config', $.extend(true, {}, mock.config, - {edit_mode: true})); - this.mockData = $.extend(true, {}, mock.data); + {edit_mode: true})); + this.mockData = $.extend(true, {}, mock.data); this.component.trigger('data', this.mockData); }); describe('with a feature', function() { - beforeEach(function() { - this.feature = this.mockData.features[0]; - this.marker = this.component.attr.features[this.feature.geometry.coordinates]; - }); - - // For some reason, spies on this.marker.dragging.{enable,disable} - // don't work, so... - - it('enables dragging on select', function() { - this.component.trigger(document, 'selectFeature', this.feature); - expect(this.marker.dragging._enabled).toBe(true); - }); - - it('disables dragging on deselect', function() { - this.component.trigger(document, 'selectFeature', this.feature); - this.component.trigger(document, 'deselectFeature', this.feature); - expect(!this.marker.dragging._enabled).toBe(true); - }); - - it('resets position on external posn change', function() { - this.component.trigger(document, 'selectFeature', this.feature); - var newPos = {lat: 33, lng: 44}; - - spyOn(this.marker, 'setLatLng'); - this.component.trigger(document, 'selectedFeatureMoved', newPos); - expect(this.marker.setLatLng).toHaveBeenCalledWith(newPos); - }); - - it("doesn't reset posn unless 'move' event actually moved", function() { - this.component.trigger(document, 'selectFeature', this.feature); - var oldLatLng = this.marker.getLatLng(); - var newPos = {lat: oldLatLng.lat, lng: oldLatLng.lng}; - - spyOn(this.marker, 'setLatLng'); - this.component.trigger(document, 'selectedFeatureMoved', newPos); - expect(this.marker.setLatLng).not.toHaveBeenCalled(); - }); - - it('reports new position on drag-end', function() { - spyOnEvent(document, 'selectedFeatureMoved'); - this.component.trigger(document, 'selectFeature', this.feature); - this.marker.fireEvent('dragend'); - expect('selectedFeatureMoved').toHaveBeenTriggeredOnAndWith( - document, this.marker.getLatLng()); - }); + beforeEach(function() { + this.feature = this.mockData.features[0]; + this.marker = this.component.attr.features[this.feature.geometry.coordinates]; + }); + + // For some reason, spies on this.marker.dragging.{enable,disable} + // don't work, so... + + it('enables dragging on select', function() { + this.component.trigger(document, 'selectFeature', this.feature); + expect(this.marker.dragging._enabled).toBe(true); + }); + + it('disables dragging on deselect', function() { + this.component.trigger(document, 'selectFeature', this.feature); + this.component.trigger(document, 'deselectFeature', this.feature); + expect(!this.marker.dragging._enabled).toBe(true); + }); + + it('resets position on external posn change', function() { + this.component.trigger(document, 'selectFeature', this.feature); + var newPos = {lat: 33, lng: 44}; + + spyOn(this.marker, 'setLatLng'); + this.component.trigger(document, 'selectedFeatureMoved', newPos); + expect(this.marker.setLatLng).toHaveBeenCalledWith(newPos); + }); + + it("doesn't reset posn unless 'move' event actually moved", function() { + this.component.trigger(document, 'selectFeature', this.feature); + var oldLatLng = this.marker.getLatLng(); + var newPos = {lat: oldLatLng.lat, lng: oldLatLng.lng}; + + spyOn(this.marker, 'setLatLng'); + this.component.trigger(document, 'selectedFeatureMoved', newPos); + expect(this.marker.setLatLng).not.toHaveBeenCalled(); + }); + + it('reports new position on drag-end', function() { + spyOnEvent(document, 'selectedFeatureMoved'); + this.component.trigger(document, 'selectFeature', this.feature); + this.marker.fireEvent('dragend'); + expect('selectedFeatureMoved').toHaveBeenTriggeredOnAndWith( + document, this.marker.getLatLng()); + }); }); From 46bb3f6b22c2647cfd2345bdc751ac4b1c068cf6 Mon Sep 17 00:00:00 2001 From: Robert Thau Date: Tue, 21 Oct 2014 09:11:42 -0400 Subject: [PATCH 12/56] Wire up JSON editor to edit state object. Still work in progress -- needs styling and tests. --- src/ui/info.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/ui/info.js b/src/ui/info.js index a988578..2b737bb 100644 --- a/src/ui/info.js +++ b/src/ui/info.js @@ -13,7 +13,6 @@ define(function(require, exports, module) { this.infoConfig = config.properties; this.editMode = config.edit_mode; if (this.editMode) { - alert("info edit mode"); this.editSchema = config.feature_property_json_schema; } }; @@ -42,11 +41,16 @@ define(function(require, exports, module) { this.startEditing = function(contentNode, props) { this.killCurrentEditor(); // in case we had one... - this.currentEditor = new JSONEditor(contentNode[0], { + var editor = new JSONEditor(contentNode[0], { schema: this.editSchema, - startval: props, + startval: _.cloneDeep(props), theme: "bootstrap3" }); + this.currentEditor = editor; + editor.on('change', function() { + $(document).trigger('selectedFeaturePropsChanged', + editor.getValue()); + }); } this.killCurrentEditor = function() { From 01456614cfa396f14c33cef769e47a297a3443cb Mon Sep 17 00:00:00 2001 From: Robert Thau Date: Tue, 21 Oct 2014 10:26:58 -0400 Subject: [PATCH 13/56] Quarter-decent styling for "export data" button --- index.html | 9 +++++---- src/ui/export.js | 2 +- test/spec/ui/export_spec.js | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/index.html b/index.html index fcde52d..eac71aa 100644 --- a/index.html +++ b/index.html @@ -32,7 +32,7 @@ -
    - - diff --git a/src/ui/export.js b/src/ui/export.js index ce54745..37a1325 100644 --- a/src/ui/export.js +++ b/src/ui/export.js @@ -26,7 +26,7 @@ define(function(require, exports, module) { this.after('initialize', function() { this.on(document, 'config', this.configure); this.on(document, 'editedDataForSave', this.doExport); - this.on('submit', this.triggerExport); + this.on('click', this.triggerExport); }); }); }); diff --git a/test/spec/ui/export_spec.js b/test/spec/ui/export_spec.js index 8ba8925..f1c16f8 100644 --- a/test/spec/ui/export_spec.js +++ b/test/spec/ui/export_spec.js @@ -31,7 +31,7 @@ define(['test/mock', 'jquery'], function(mock, $) { describe('on export', function() { it('requests export data on form submittal', function() { - this.$node.trigger('submit'); + this.$node.trigger('click'); expect('requestEditedData').toHaveBeenTriggeredOnAndWith( document,'editedDataForSave'); }); From cac05ff12f693c438906f4557eb5b883fa2fbd7d Mon Sep 17 00:00:00 2001 From: Robert Thau Date: Tue, 21 Oct 2014 20:55:42 -0400 Subject: [PATCH 14/56] Basic tests for editor manipulation in info box. --- test/mock.js | 18 +++++++++++++++++- test/spec/ui/info_spec.js | 30 +++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/test/mock.js b/test/mock.js index eda2c3e..af2589f 100644 --- a/test/mock.js +++ b/test/mock.js @@ -38,7 +38,23 @@ define({ type: "list" } }, - "edit_mode": false + "edit_mode": false, + "feature_property_json_schema": { + "title": "Organization", + "type": "object", + "id": "properties", + "properties": { + "organization_name": { "type": "string" }, + "address": { "type": "string" }, + "web_url": { "type": "string", "format": "url" }, + "contact_names": { + "title": "Contacts", + "type": "array", + "format": "table", + "items": { "type": "string" } + } + } + } }, "data": { diff --git a/test/spec/ui/info_spec.js b/test/spec/ui/info_spec.js index 52dddfd..daf51d8 100644 --- a/test/spec/ui/info_spec.js +++ b/test/spec/ui/info_spec.js @@ -1,4 +1,6 @@ -define(['infotemplates', 'jquery'], function(templates, $) { +define(['infotemplates', 'jquery', 'test/mock', 'lodash'], + function(templates, $, mock, _) { + 'use strict'; describeComponent('ui/info', function() { beforeEach(function() { @@ -24,5 +26,31 @@ define(['infotemplates', 'jquery'], function(templates, $) { expect(this.$node.hide).toHaveBeenCalledWith(); }); }); + + describe('in edit mode', function() { + beforeEach(function() { + + var editConfig = _.cloneDeep(mock.config); + editConfig.edit_mode = true; + $(document).trigger('config', editConfig); + + this.feature = mock.data.features[0]; + }); + + // Ideally, would also be testing the behavior of the JSONEditor + // object itself, but hard to do that without tickling its innards... + + it('creates an editor on select feature', function() { + $(document).trigger('selectFeature', this.feature); + expect(this.component.currentEditor).not.toBe(undefined); + }); + + it('destroys the editor on close click', function() { + $(document).trigger('selectFeature', this.feature); + this.$node.find('.close').click(); + expect(this.component.currentEditor).toBe(undefined); + }); + }); + }); }); From 490babf84ae610f45ae261d801ebcbe532120643 Mon Sep 17 00:00:00 2001 From: Robert Thau Date: Thu, 23 Oct 2014 13:41:31 -0400 Subject: [PATCH 15/56] (Barely) usable styling for edit widgets in info pane. Also set max height for the pane itself, so it gets a scroll bar. --- src/ui/info.js | 1 + styles/style.css | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/ui/info.js b/src/ui/info.js index 2b737bb..c33b3cc 100644 --- a/src/ui/info.js +++ b/src/ui/info.js @@ -14,6 +14,7 @@ define(function(require, exports, module) { this.editMode = config.edit_mode; if (this.editMode) { this.editSchema = config.feature_property_json_schema; + this.$node.addClass('editing'); } }; diff --git a/styles/style.css b/styles/style.css index f4e4c6e..83d0da5 100644 --- a/styles/style.css +++ b/styles/style.css @@ -158,6 +158,42 @@ body > div.container { border-radius: 5px; } +/* Styling info overlay for the JSON editor. + * + * Just copying in Bootstrap 3 rules for '-sm'all inputs and buttons for + * now, to avoid doing a custom theme and custom build of JsonEditor itself, + * which would add the '-sm' classes. + */ + +#info.editing { + width: 350px; + font-size: 80%; + height: 90%; +} + +#info.editing input { + height:30px; + padding:5px 10px; + font-size:12px; + line-height:1.5; + border-radius:3px +} + +#info.editing select.input-sm{ + height:30px; + line-height:30px +} + +#info.editing textarea, #info.editing select[multiple]{ + height:auto +} + +#info.editing .btn { + padding:5px 10px; + font-size:12px; + line-height:1.5; + border-radius:3px +} #facets { position: absolute; From d245b8d1d07db8ff1e1d98aaa7413d18d2c66217 Mon Sep 17 00:00:00 2001 From: Robert Thau Date: Thu, 23 Oct 2014 14:05:28 -0400 Subject: [PATCH 16/56] More complete edit schema for sample data. If nothing else, this cuts down on spurious widgetry from the JSON editor. --- config.json | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/config.json b/config.json index 1727cf2..41e2131 100644 --- a/config.json +++ b/config.json @@ -67,13 +67,38 @@ "properties": { "organization_name": { "type": "string" }, "address": { "type": "string" }, + "city": {"type": "string" }, "web_url": { "type": "string", "format": "url" }, + "phone_numbers": { + "title": "Phone numbers", + "type": "array", "format": "table", "items": { "type": "string" } + }, + "contact_emails": { + "title": "Email addresses", + "type": "array", "format": "table", "items": { "type": "string" } + }, "contact_names": { - "title": "Contacts", - "type": "array", - "format": "table", - "items": { "type": "string" } - } + "title": "Contact names", + "type": "array", "format": "table", "items": { "type": "string" } + }, + "target_populations": { + "title": "Target Populations", + "type": "array", "format": "table", "items": { "type": "string" } + }, + "youth_category": { + "title": "Youth category", + "type": "array", "format": "table", "items": { "type": "string" } + }, + "service_class_level_1": { + "title": "Service class lvl 1", + "type": "array", "format": "table", "items": { "type": "string" } + }, + "service_class_level_2": { + "title": "Service class lvl 2", + "type": "array", "format": "table", "items": { "type": "string" } + }, + "age_range": { "type": "string" }, + "additional_notes": { "type": "string" } } } } From 9eceab6b31c7dabb66e943e6bbe202477f9de789 Mon Sep 17 00:00:00 2001 From: Robert Thau Date: Thu, 23 Oct 2014 14:22:36 -0400 Subject: [PATCH 17/56] Use bootstrap's icons in JSON editor. This does involve storing its font files. Sigh... --- fonts/glyphicons-halflings-regular.eot | Bin 0 -> 20335 bytes fonts/glyphicons-halflings-regular.svg | 229 ++++++++++++++++++++++++ fonts/glyphicons-halflings-regular.ttf | Bin 0 -> 41280 bytes fonts/glyphicons-halflings-regular.woff | Bin 0 -> 23320 bytes src/ui/info.js | 3 +- styles/style.css | 2 +- 6 files changed, 232 insertions(+), 2 deletions(-) create mode 100644 fonts/glyphicons-halflings-regular.eot create mode 100644 fonts/glyphicons-halflings-regular.svg create mode 100644 fonts/glyphicons-halflings-regular.ttf create mode 100644 fonts/glyphicons-halflings-regular.woff diff --git a/fonts/glyphicons-halflings-regular.eot b/fonts/glyphicons-halflings-regular.eot new file mode 100644 index 0000000000000000000000000000000000000000..4a4ca865d67e86f961bc6e2ef00bffa4e34bb9ed GIT binary patch literal 20335 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

    !oRptb@n+ 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 literal 0 HcmV?d00001 diff --git a/fonts/glyphicons-halflings-regular.svg b/fonts/glyphicons-halflings-regular.svg new file mode 100644 index 0000000..25691af --- /dev/null +++ b/fonts/glyphicons-halflings-regular.svg @@ -0,0 +1,229 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fonts/glyphicons-halflings-regular.ttf b/fonts/glyphicons-halflings-regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..67fa00bf83801d2fa568546b982c80d27f6ef74e GIT binary patch literal 41280 zcmc${2b>$#wLd<0X4JKkMs=IoY9(#guC%-Ix~!LV@5XgawLzwtVoFRi&4B<;Yzzq| z1QHw)z@da0*@PsIyqA!`6G@b6oWOe_b_$P#@)GbXG2Zd-d+unfZAkvV-{LBX3Wc;?Pswd9i3FaAXkSUrx`&zn7GF0_`M^SUUB}0?t9iO6@<@rQX4MYaNTB6W_twTb8q4L*yS58+j!vF z2j3Nh`>lc?ZQXpu)z^G$?&B8=!spQk>+PGb+PGPLztt}YU&eW%aO!9EjS$4lmWxSf0(+a;I;S#pX$!?81r zPxe(ID}q`APM!R3^`f;)g#n@JcY^fY+Km6eDgyYBYd&V!e;1`7xevutA z9r7HC9qK$ZaA-Mx@w`Ku58Zlb*I{&GuRWclsyf4l#;7ri09Ui*6RHTP@wSWT=t=8ZXH=9myY8a)#IAo_0fKca`D z*F~?2UK+h1x;}btbX|01bV+nx^t9+egvQ|i`5yx>jQlJU@$>W=|A&(_6vm%?s-YdZ z;Q!}OV(bZjm;rz1-#tQ;_`j;qrV74A>f+@?>cTDSR3S05S~a&0%~;2e-Lx)tKxMv; z>UNd2#a>sPt?jDVwrIuBoW#0#yDGI^Tpd#fmJh|%fpzVw+(uuGC*n5@{id$Gt`64? z4cEQ9t}YQ*O|3)f+%4<)iFNDnd#1Lkv(9K&&23r(y9;-Z-F4Pkb*g}$v9xK8{LsMY zA#0mgiS=dLRa;x^Cc4QF@cS`UN-jvmR5`U!6_yWe-?)84j5em!#pCPhw)4Fe#va|! zZnVx*=ZWJcj<(n@cz2v_v5abIJ!>cyo0pio;gZ-;tZ<(36Leh_-5IxzZI8{{K6gW6 zdu)4x-!7pFD~8koT#5eCZPkH|w1e-s_?>1Ptd7U)Vh6W_4EWLlv~6{zZD=1ZbGId8 z2P-#E#D*5Ftc$B`-OzS)XhC9oBDQ_O_QVEi33Z3wsXZPV1}}y|p$^c7cTxw?(8S!t zhD+9u?+Ja?*M?4Pzmv$eu#nhpQDe)8rq_KJXZ&sZgaI}%ILH=#(<7WO@OQd+HCi6q zzG5hG9$KFmtiuOO41)3lD~5_fOqg~4V3EZbKGfLxYR$%a-ctNxpiRY5&;@Vp#E_7w zkT-73wkGUcB*ievEJBCIgv|7!MHb)9YG%{FPcKR$HU&+h!zMahw3wx1(~FFb=ajgT z%qfW`HlV-tm%m7{V~3g`k(p2s3i4uku@Dj(1y#tXRXLTFRY#Vo)fv@yP&H*$Z&|fu zwHnqcbawfA;^}-y$tn4eB_4=}ENLa7Skn0dlb+x4dBA$NMe@P+tN3)UA)gG`7`p@g}ksuP_r4esa$Nz(oZ#Y*myhQ zydBZ3YRahfIn`WNYqM$~qdLmPfP*d!c&KGlGHRZ;tf8!hquH$5;L+MytLn+B9c9&> z)%sYg){s}cs-;hDSBj2Uwy&>`sF=@n=M(u{Z@xE|4FyAq?hY~0;1VryOWYj5TSU%f z`^BD|*kB}m6&MwIx%*C_4-Kj)_rGq6J%mIJM#ave| z6W_b;$tSPtXlr}!^3VTT99+%bTYl9u??3I@aP6-itZ}+F;Z~$u6l4`VD`Otmv91d} zER<(S#b#32t`d6j;d0id9}tJcA&h=ofez}MOMLIh@MGecx|6jH@5S#($3Hm!f&3l$ zJD6Q&(h@95us6di-`kyGsRm0GTk_j84vH5XTyyaJs;URwjqa+=zdhYJa8^~?^^8KtwNh&Fei-jtC-6@O7#R52HmK*O{ zb{aZAuyEO0ulKHHb62|T!ydZ}`=7qNxi+xAMLg%B;s5c3YOm_eH`jzt&r4U@9n$wC zpM7|lQe8tUd+7K(@(<((1)oqStP_e*@>*4IMh%tKx(s^5)cTCd4yu8&8t{;8P)(Qv zVE3AU;@u~S9&cl)PcOVYDiH%eQKR|9}_GlobT-NdeEVO-@<}^H#0Y+ z8Q5L)1Y^CPR4l~m!D{tOS)0XjnbmLA4_v#m^vM^Q_j}*d-(&C6IsFf%o!9CIaPl&X zg|#geFV+9@;`eX`hJ?@aA^BN(won6(WNK|j6%Gd{TZs`|W+=eeBozwtMwk^=|gMSwn`IzBM5z3t%CUFVn_xPg)&+-Z}Nm+_k}F^P&%JTTTZ;stRF1+?)Mjd z@9iZ^PjW}`nw`J<%#J^P=9j)n&CF?*>`C{+zjvK zuNOv-VW}N|3CU6jr(;`3FW{u)Z?q=6LBotNQy3JAAabkPmIDEaWZ{fDos*^;yfMJ( zfi(x~V>RAAS`5<>L~AaqQ?lA=oNs!R?p{dTU_il`#v4*K7~%2z>|@S{!3BYEIG}H) z_pxnpX#C#z?d;e^VeztYJHy`@w=?040O^T8t{05-eVK5saD{M-a1YjMP6ciHrCKltrL=JU^%w? z%G&%P`t)e)acuLg*uJ=|U3XVDtKG{fM{{8sGiF08Ye*?QAHB~$=KSRE|D)H310@=Q zQ@pWVr#!_^eBAl$=-)<^As zJhjCaXt;)F)BDM{$J2alXh-S%@f4-CE-W<2@5?O&s9@VPh1%VaGs>!k%%NCOX!q7hU38p|b zovTxd{u+j_eYEZ&L7wLVxj-V2==n%JWNx8UD3m@%8`0O%MTNo`?Y_YEs;F@G1lm<7 z6B|dFie`mXi)&WTk!DpN9@opsy47=}Th&KCR=bk0jD2*^NKaw!Rn)8<*XyrZg3!aP zBWl)*%=02T#&ty@BtHoKp$@D49Dxi+JJ#tozAjnHMJVYQMGK5M)#A~d7;9g-==9M+ zC+sLPnKY*bgA}T+PoUvsAa#550cf*+sDeG+sdP`!3k^+d=n$DPfw7($6FBsXCobH2 zl%02U>xEDJ;>?F$edpDO&Sbv{2MRQk@FosD&zkxl&zG*#jvm#nE9D>W*MI%|7F>mk znUk(EmLpgb1%W{>X`^~fr%;5k(W+UUxg1kH8C5<=T0J^pMJF6Ela21U%bLQaO&%6D zgK<3auK;7Dt%RX3F)~Ql5#33aHxvaxlcG>7)XBT$-NHQKbm2UK)a&JCbx}s`1@%^N z>dh~!^F7)U+zkubO3-P(KsMA2u>BHcpF5E2BUWhiYBd=cmfCW#yk>y{qb^eRN%8a? zI@{~jT2CW}_xYn@Fv={!P(BpIW-dEZ?48L%z4>&$7n?oZ88MY%`Bd7HPGK|A;1YEiG@Keut^O%am$rsLQ0x9U0T7rgScss@?4KCe!Dc zCnPOzoBkzKkurMPR~sJlqu6;PIcA{-F)-Vx|?r? z`d|?X$B)aZ$q&7MOasjecMHWhX;F=^_B*??Sm@K4VoSC+2X&#Y3>A}<3RfGBXENMw zg?V3lkXD^WkCwy`019a$&9s)?Cn=eC2St6RCAO;o}h)=XB2SH>r+jiH(R9}{

    PBK;&Wcg|NX{>QR@W3{K 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 literal 0 HcmV?d00001 diff --git a/fonts/glyphicons-halflings-regular.woff b/fonts/glyphicons-halflings-regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..8c54182aa5d4d1ab3c9171976b615c1dcb1dc187 GIT binary patch literal 23320 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 mX4Rts div.container { */ #info.editing { - width: 350px; + width: 380px; font-size: 80%; height: 90%; } From 8efb51b09bd1ca7dc3187ddefa8fe6a775d7ec89 Mon Sep 17 00:00:00 2001 From: Robert Thau Date: Thu, 23 Oct 2014 14:28:30 -0400 Subject: [PATCH 18/56] Scroll edit pane to the top when loading a new feature. Otherwise, if the user scrolled down, they wind up in the middle of the new feature's properties, which is almost certainly not what they want to be first looking at. --- src/ui/info.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ui/info.js b/src/ui/info.js index 831357a..6ec39a8 100644 --- a/src/ui/info.js +++ b/src/ui/info.js @@ -53,6 +53,10 @@ define(function(require, exports, module) { $(document).trigger('selectedFeaturePropsChanged', editor.getValue()); }); + + // In case we were scrolled down editing a previous feature, + // scroll our pane back up to the top. + this.$node.scrollTop(0); } this.killCurrentEditor = function() { From 16e274901732797f4177d635f18101f79649239b Mon Sep 17 00:00:00 2001 From: Robert Thau Date: Thu, 23 Oct 2014 14:46:33 -0400 Subject: [PATCH 19/56] Widen edit box, so all list widgetry shows on one row. --- styles/style.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/styles/style.css b/styles/style.css index 1cbc8fe..795975c 100644 --- a/styles/style.css +++ b/styles/style.css @@ -166,9 +166,10 @@ body > div.container { */ #info.editing { - width: 380px; + width: 390px; font-size: 80%; height: 90%; + right: 5px; } #info.editing input { From 5695dec4e8e93e726f72b4b1d99473a1fd49663d Mon Sep 17 00:00:00 2001 From: Robert Thau Date: Thu, 23 Oct 2014 14:57:26 -0400 Subject: [PATCH 20/56] Add some property names to edit schema. --- config.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/config.json b/config.json index 41e2131..c6675ee 100644 --- a/config.json +++ b/config.json @@ -65,10 +65,10 @@ "type": "object", "id": "properties", "properties": { - "organization_name": { "type": "string" }, - "address": { "type": "string" }, - "city": {"type": "string" }, - "web_url": { "type": "string", "format": "url" }, + "organization_name": { "title": "Name", "type": "string" }, + "address": { "title": "Address", "type": "string" }, + "city": { "title": "City", "type": "string" }, + "web_url": { "title": "Web URL", "type": "string", "format": "url" }, "phone_numbers": { "title": "Phone numbers", "type": "array", "format": "table", "items": { "type": "string" } @@ -97,8 +97,8 @@ "title": "Service class lvl 2", "type": "array", "format": "table", "items": { "type": "string" } }, - "age_range": { "type": "string" }, - "additional_notes": { "type": "string" } + "age_range": { "title": "Age Range", "type": "string" }, + "additional_notes": { "title": "Further Notes", "type": "string" } } } } From 9d31e82fc1305bfda0b00addb75ff5b4128ed57e Mon Sep 17 00:00:00 2001 From: Robert Thau Date: Thu, 23 Oct 2014 14:57:57 -0400 Subject: [PATCH 21/56] Lop off some JSON editor widgetry that probably isn't worth the clutter. --- src/ui/info.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ui/info.js b/src/ui/info.js index 6ec39a8..30b41ab 100644 --- a/src/ui/info.js +++ b/src/ui/info.js @@ -46,7 +46,9 @@ define(function(require, exports, module) { schema: this.editSchema, startval: _.cloneDeep(props), theme: "bootstrap3", - iconlib: "bootstrap3" + iconlib: "bootstrap3", + disable_collapse: true, + disable_edit_json: true }); this.currentEditor = editor; editor.on('change', function() { From 040192e648bfd5e4eadfd3cab61d6ae68a52f81b Mon Sep 17 00:00:00 2001 From: Robert Thau Date: Thu, 23 Oct 2014 21:11:20 -0400 Subject: [PATCH 22/56] Correctly handle editing of multi-line addresses. Turns out this is just a change in the JSON schema we give the editor. --- config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.json b/config.json index c6675ee..8a21634 100644 --- a/config.json +++ b/config.json @@ -66,7 +66,7 @@ "id": "properties", "properties": { "organization_name": { "title": "Name", "type": "string" }, - "address": { "title": "Address", "type": "string" }, + "address": { "title": "Address", "type": "string", "format": "textarea" }, "city": { "title": "City", "type": "string" }, "web_url": { "title": "Web URL", "type": "string", "format": "url" }, "phone_numbers": { From 0c54937cb6fdb70ca5eebea88468e125df6c6651 Mon Sep 17 00:00:00 2001 From: Robert Thau Date: Fri, 24 Oct 2014 12:40:15 -0400 Subject: [PATCH 23/56] Double-clicking on the map offers to add a point. This is actually done in two stages: the double-click gives you a popup in which you enter the name of the feature you're creating (well, the map's preview feature, whatever that is); submitting the form actually creates the feature, and gives you the full feature editor. To minimize mousing-around, the popup form automatically gets keyboard focus when created. The intent of this dance is to make a stray double-click easy to undo. Internally, what happens is that on submission, the popup form handler sends a 'newFeature' event, which anything maintaining its own copy of the data ought to handle. The edit-state does that by adding the feature to its data. The map adds a new marker. (The map is also, currently, what sends the event, but you can imagine cases where it wouldn't be.) --- config.json | 1 + index.html | 11 +++++++ src/TODO | 67 ++++++++++++++++++++++++++++++++++++++ src/data/edit_state.js | 5 +++ src/ui/info.js | 3 +- src/ui/map.js | 73 ++++++++++++++++++++++++++++++++++++++++++ styles/style.css | 6 ++++ 7 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 src/TODO diff --git a/config.json b/config.json index 8a21634..8d84c47 100644 --- a/config.json +++ b/config.json @@ -60,6 +60,7 @@ }, "geojson_source": "data.geojson", "edit_mode": true, + "new_feature_popup_label": "Add service provider named...", "feature_property_json_schema": { "title": "Organization", "type": "object", diff --git a/index.html b/index.html index eac71aa..63a5d7b 100644 --- a/index.html +++ b/index.html @@ -82,6 +82,17 @@

    About Code for Boston

    + + diff --git a/src/TODO b/src/TODO new file mode 100644 index 0000000..3bbcc14 --- /dev/null +++ b/src/TODO @@ -0,0 +1,67 @@ +Expected protocol: + + A flight component can either listen for incremental changes to + data items, or wait for the edit state to pump out an 'editedData' + event. The latter is for things that need to rebuild the world, + and we may want to put some hysteresis on it. (Current component + that does that, for the moment, the fuse index for data/typeahead.) + + If you're listening for incremental changes, you need to listen + for all of 'em. So, map should be listening for selectedFeatureMoved + as well as sending it, lest something else start sending it as well + (in which case, it'll need to move the icon -- plausible case: + changes to address moving the icon as well). + + Incremental changes: + + selectFeature + deselectFeature + + selectedFeatureMoved [carries a latlng] + selectedFeaturePropsChanged [carries new props; + those not mentioned to be left alone] + + selectedFeatureDeleted [sets a flag; actual cull happens at export] + selectedFeatureUndeleted + + newFeature [carries a latlng *and* properties] + +TODO: + + +) decide what we're doing about list features. Set '"list": true' in + config for 'em to get off the dime. + + For next three: jdorn/json-editor + + *) alternative infotemplates that render form inputs, + with '"data-property": "foo"' + + *) code to scrape property values *out* of the form inputs + + *) code to add and remove list items from list-valued props + + +) glue code to send and process changedProperty events + + *) glue code to send the editedData events from edit_state + + *) delete event support + *) editState sets flag on item, and culls from export + *) map turns opacity on icon to 50% + *) "delete/undelete" button on above forms to set it + + *) newFeature event support + *) double click sets preliminary marker, and pops up a form asking + for a value for the preview_attribute. + *) cancel on that form is a no-op; prelim marker goes away. + may need startCreate/cancelCreate events to mediate this + *) submit sends a newFeature event + + *) Edit help text and display, similar to current "About" + + *) Undo mechanism: + items have an undo state which can be null or "revert properties" + for pre-exisiting items, edit_state caches properties at first + change + + + \ No newline at end of file diff --git a/src/data/edit_state.js b/src/data/edit_state.js index 3d9b387..8625541 100644 --- a/src/data/edit_state.js +++ b/src/data/edit_state.js @@ -25,6 +25,10 @@ define(function(require, exports, module) { $(document).trigger(handlerEvent, this.data); } + this.newFeature = function(ev, feature) { + this.data.features.push(feature); + } + // Should probably guard against selecting a feature that's not a point. this.selectFeature = function(ev, feature) { @@ -49,6 +53,7 @@ define(function(require, exports, module) { this.on(document, 'config', this.configure); this.on(document, 'data', this.loadData); this.on(document, 'requestEditedData', this.provideEdits); + this.on(document, 'newFeature', this.newFeature); this.on(document, 'selectFeature', this.selectFeature); this.on(document, 'selectedFeatureMoved', this.selectedFeatureMoved); this.on(document, 'selectedFeaturePropsChanged', this.propEdit); diff --git a/src/ui/info.js b/src/ui/info.js index 30b41ab..be701c7 100644 --- a/src/ui/info.js +++ b/src/ui/info.js @@ -48,7 +48,8 @@ define(function(require, exports, module) { theme: "bootstrap3", iconlib: "bootstrap3", disable_collapse: true, - disable_edit_json: true + disable_edit_json: true, + required_by_default: true }); this.currentEditor = editor; editor.on('change', function() { diff --git a/src/ui/map.js b/src/ui/map.js index f28da6e..dfab8c3 100644 --- a/src/ui/map.js +++ b/src/ui/map.js @@ -49,6 +49,13 @@ define(function(require, exports, module) { // Determine whether edit-mode features are enabled (particularly // dragging selected feature). this.edit_mode = config.edit_mode; + + if (this.edit_mode) { + var label = config.new_feature_popup_label; + if (label !== undefined) { + $("#new-feature-popup-label").html(label); + } + } }; this.loadData = function(ev, data) { @@ -76,6 +83,11 @@ define(function(require, exports, module) { this.attr.layer = L.geoJson(data, {onEachFeature: setupFeature}); this.attr.layer.addTo(this.map); + + if (this.edit_mode) { + this.map.doubleClickZoom.disable(); + this.map.on('dblclick', this.startCreate.bind(this)); + } }; this.emitClick = function(e) { @@ -173,6 +185,66 @@ define(function(require, exports, module) { this.map.panTo(latlng); }; + // Managing the two-step process of creating a new feature. + // (It's two steps, with the simple form in a popup, so stray + // double-clicks are easy to undo.) + + // Start create: handles a Leaflet double-click event. + + this.startCreate = function(e) { + var popup = L.popup(); + this.createPopup = popup; + this.otherthing = 'foo'; + popup.setLatLng(e.latlng); + popup.setContent($("#new-feature-popup").html()); + popup.openOn(this.map); + + var form = this.$node.find('.create-feature-popup-form'); + form.on('submit', this.finishCreate.bind(this)); + form.find('input').first().focus(); + }; + + // Finish create: handles ordinary submission of the form in the + // "startCreate" popup. + + this.finishCreate = function(e) { + e.preventDefault(); + + var popup = this.createPopup; // stashed away in step 1 above + if (popup === undefined) { + // Ordinarily "can't happen", but the test framework leaves old + // map components lying around. The finishCreate tests only create + // a mock popup on the one set up for them, and we need to keep + // the others from blowing up. (All due to the use of a 'live' + // event handler declaration below.) + return; + } + + var latlng = popup.getLatLng(); + + var props = {}; + props[this.featurePreviewAttr] = $(e.target).serializeArray()[0].value; + + var feature = { + type: "Feature", + geometry: { + type: "Point", + coordinates: [latlng.lng, latlng.lat] + }, + properties: props + }; + + this.lastCreatedFeature = feature; // for tests (only! no other use!) + + this.map.removeLayer(popup); // don't need it any longer... + $(document).trigger('newFeature', feature); + $(document).trigger('selectFeature', feature); + }; + + this.handleNewFeature = function(e, feature) { + this.attr.layer.addData(feature); + }; + this.after('initialize', function() { this.map = L.map(this.node, {}); @@ -194,6 +266,7 @@ define(function(require, exports, module) { this.on(document, 'hoverFeature', this.hoverFeature); this.on(document, 'clearHoverFeature', this.clearHoverFeature); this.on(document, 'selectedFeatureMoved', this.selectedFeatureMoved); + this.on(document, 'newFeature', this.handleNewFeature); this.on('panTo', this.panTo); }); }); diff --git a/styles/style.css b/styles/style.css index 795975c..15f63b3 100644 --- a/styles/style.css +++ b/styles/style.css @@ -196,6 +196,12 @@ body > div.container { border-radius:3px } +/* More editing widgetry; styling for the "create feature" popup */ + +form.create-feature-popup-form { + width: 250px; +} + #facets { position: absolute; padding: 10px 14px; From 0bc1d312d55933c8a7c98fefd8466a64ccdaf890 Mon Sep 17 00:00:00 2001 From: Robert Thau Date: Fri, 24 Oct 2014 12:43:10 -0400 Subject: [PATCH 24/56] Tests for UI to add new features. --- test/mock.js | 1 + test/spec/data/edit_state_spec.js | 18 +++++++++++- test/spec/ui/map_spec.js | 48 +++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/test/mock.js b/test/mock.js index af2589f..1984cd2 100644 --- a/test/mock.js +++ b/test/mock.js @@ -39,6 +39,7 @@ define({ } }, "edit_mode": false, + "new_feature_popup_label": "Add new frobnitz", "feature_property_json_schema": { "title": "Organization", "type": "object", diff --git a/test/spec/data/edit_state_spec.js b/test/spec/data/edit_state_spec.js index f170991..5f3f634 100644 --- a/test/spec/data/edit_state_spec.js +++ b/test/spec/data/edit_state_spec.js @@ -1,4 +1,4 @@ -define(['test/mock', 'jquery'], function(mock, $) { +define(['test/mock', 'jquery', 'lodash'], function(mock, $, _) { 'use strict'; describeComponent('data/edit_state', function() { beforeEach(function() { @@ -14,6 +14,22 @@ define(['test/mock', 'jquery'], function(mock, $) { describe('editing protocol', function() { + it('adds new features on request', function() { + var newFeature = { + type: "Feature", + geometry: { type: "Point", coordinates: [0, 90] }, + properties: { name: "North Pole" } + }; + this.component.trigger('data', _.cloneDeep(mock.data)); + this.component.trigger('newFeature', newFeature); + + var features = this.component.data.features; + expect(features.length).toBe(mock.data.features.length + 1); + + var lastFeature = features[features.length - 1]; + expect(lastFeature).toBe(newFeature); + }); + describe('with a selected feature', function() { beforeEach(function() { diff --git a/test/spec/ui/map_spec.js b/test/spec/ui/map_spec.js index ccc5378..9dba591 100644 --- a/test/spec/ui/map_spec.js +++ b/test/spec/ui/map_spec.js @@ -132,6 +132,54 @@ define( this.component.trigger('data', this.mockData); }); + it('sets create-feature popup label from config', function() { + setFixtures(''); + this.component.trigger('config', $.extend(true, {}, mock.config, + {edit_mode: true})); + expect($('label').html()).toBe(mock.config.new_feature_popup_label); + }); + + it('shows create-new-feature popup on double click', function() { + this.component.map.fireEvent('dblclick', {latlng: L.latLng(90.0, 0.0)}); + var popup = this.component.createPopup; + expect(popup).not.toBe(undefined); + expect(popup.getLatLng().lat).toBe(90); + expect(popup.getLatLng().lng).toBe(0); + expect(popup._map).toBe(this.component.map); + this.component.map.removeLayer(popup); + }); + + it('triggers events on create-feature form submit', function() { + + // Put a mock-form in the fixtures. + setFixtures('
    '); + spyOnEvent(document,'newFeature'); + spyOnEvent(document,'selectFeature'); + + // Get a popup in position, since that's where we grab the latlng from. + this.component.map.fireEvent('dblclick', {latlng: L.latLng(90, 0)}); + var popup = this.component.createPopup; + expect(popup).not.toBe(undefined); + + // Also fake up the event binding ordinarily done by startCreate. + var form = $("form.create-feature-popup-form"); + form.on('submit', this.component.finishCreate.bind(this.component)); + + // Fake submittal... + this.component.trigger(form, 'submit'); + expect('newFeature').toHaveBeenTriggeredOn(document); + expect('selectFeature').toHaveBeenTriggeredOn(document); + + var feature = this.component.lastCreatedFeature; + expect(feature.geometry.coordinates).toEqual([0, 90]); + + var prop = this.component.featurePreviewAttr; + expect(feature.properties[prop]).toBe('North Pole'); // from mock form + + // Should have taken down the popup on its own as well. + expect(popup._map).toBe(null); + }); + describe('with a feature', function() { beforeEach(function() { From a4bad0ec30b6937ec1904fc7c3f6476fda47755a Mon Sep 17 00:00:00 2001 From: Robert Thau Date: Fri, 24 Oct 2014 13:39:00 -0400 Subject: [PATCH 25/56] Quiet complaints from jshint. The only slightly messy thing is handling JSONEditor, but the ugliness was already there; this just makes it explicit. --- src/data/edit_state.js | 15 +++++++-------- src/ui/export.js | 9 ++++----- src/ui/info.js | 9 ++++++--- src/ui/map.js | 3 ++- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/data/edit_state.js b/src/data/edit_state.js index 8625541..280d7f5 100644 --- a/src/data/edit_state.js +++ b/src/data/edit_state.js @@ -1,7 +1,6 @@ define(function(require, exports, module) { 'use strict'; var flight = require('flight'); - var _ = require('lodash'); var $ = require('jquery'); module.exports = flight.component(function() { @@ -9,11 +8,11 @@ define(function(require, exports, module) { this.configure = function(ev, config) { // deep copy property info (so we can make notes w/o bothering anyone) this.property_info = $.extend(true, {}, config.properties); - } + }; this.loadData = function(ev, data) { this.data = data; - } + }; // Only way I can see to provide edited data back to the // *particular* other flight component that requested it is for it @@ -23,23 +22,23 @@ define(function(require, exports, module) { this.provideEdits = function(ev, handlerEvent) { $(document).trigger(handlerEvent, this.data); - } + }; this.newFeature = function(ev, feature) { this.data.features.push(feature); - } + }; // Should probably guard against selecting a feature that's not a point. this.selectFeature = function(ev, feature) { this.selectedFeature = feature; - } + }; this.selectedFeatureMoved = function(ev, latlng) { if (this.selectedFeature) { this.selectedFeature.geometry.coordinates = [latlng.lng, latlng.lat]; } - } + }; this.propEdit = function(ev, newProps) { if (this.selectedFeature) { @@ -47,7 +46,7 @@ define(function(require, exports, module) { // if so, we want to leave the others alone. $.extend(this.selectedFeature.properties, newProps); } - } + }; this.after('initialize', function() { this.on(document, 'config', this.configure); diff --git a/src/ui/export.js b/src/ui/export.js index 37a1325..5df21b4 100644 --- a/src/ui/export.js +++ b/src/ui/export.js @@ -2,7 +2,6 @@ define(function(require, exports, module) { 'use strict'; var flight = require('flight'); var $ = require('jquery'); - var _ = require('lodash'); module.exports = flight.component(function () { @@ -10,18 +9,18 @@ define(function(require, exports, module) { if (!config.edit_mode) { this.$node.hide(); } - } + }; this.triggerExport = function(ev) { ev.preventDefault(); $(document).trigger('requestEditedData', 'editedDataForSave'); - } + }; this.doExport = function(ev, data) { var json = JSON.stringify(data, undefined, 2); var uri = 'data:text/plain,'+encodeURIComponent(json); - window.open(uri, "Exported Finda Data") - } + window.open(uri, "Exported Finda Data"); + }; this.after('initialize', function() { this.on(document, 'config', this.configure); diff --git a/src/ui/info.js b/src/ui/info.js index be701c7..22b1dc5 100644 --- a/src/ui/info.js +++ b/src/ui/info.js @@ -2,7 +2,10 @@ define(function(require, exports, module) { 'use strict'; var flight = require('flight'); var $ = require('jquery'); + var _ = require('lodash'); var templates = require('infotemplates'); + var JSONEditor = window.JSONEditor; + module.exports = flight.component(function info() { this.defaultAttrs({ "contentClass": "content", @@ -53,21 +56,21 @@ define(function(require, exports, module) { }); this.currentEditor = editor; editor.on('change', function() { - $(document).trigger('selectedFeaturePropsChanged', + $(document).trigger('selectedFeaturePropsChanged', editor.getValue()); }); // In case we were scrolled down editing a previous feature, // scroll our pane back up to the top. this.$node.scrollTop(0); - } + }; this.killCurrentEditor = function() { if (this.currentEditor) { this.currentEditor.destroy(); this.currentEditor = undefined; } - } + }; this.hide = function() { this.$node.hide(); diff --git a/src/ui/map.js b/src/ui/map.js index dfab8c3..f5e178f 100644 --- a/src/ui/map.js +++ b/src/ui/map.js @@ -1,6 +1,7 @@ define(function(require, exports, module) { 'use strict'; var flight = require('flight'); + var $ = require('jquery'); var L = require('leaflet'); require('L.Control.Locate'); module.exports = flight.component(function map() { @@ -225,7 +226,7 @@ define(function(require, exports, module) { var props = {}; props[this.featurePreviewAttr] = $(e.target).serializeArray()[0].value; - var feature = { + var feature = { type: "Feature", geometry: { type: "Point", From 4c67344e18a9bf2ea7c62ac04c966d76409a994e Mon Sep 17 00:00:00 2001 From: Robert Thau Date: Fri, 24 Oct 2014 13:53:43 -0400 Subject: [PATCH 26/56] Update TODO state, as long as it's drifted into git anwyay. Might want to do a rebase and a new PR before this gets pulled into mainline (if they don't just squash on merge), but for now, if it's there, it might as well be current. --- src/TODO | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/TODO b/src/TODO index 3bbcc14..87d0282 100644 --- a/src/TODO +++ b/src/TODO @@ -33,28 +33,26 @@ TODO: For next three: jdorn/json-editor - *) alternative infotemplates that render form inputs, + +) alternative infotemplates that render form inputs, with '"data-property": "foo"' - *) code to scrape property values *out* of the form inputs + +) code to scrape property values *out* of the form inputs - *) code to add and remove list items from list-valued props + +) code to add and remove list items from list-valued props +) glue code to send and process changedProperty events - *) glue code to send the editedData events from edit_state + +) newFeature event support + +) double click sets preliminary marker, and pops up a form asking + for a value for the preview_attribute. + +) cancel on that form is a no-op; prelim marker goes away. + may need startCreate/cancelCreate events to mediate this + +) submit sends a newFeature event *) delete event support *) editState sets flag on item, and culls from export *) map turns opacity on icon to 50% - *) "delete/undelete" button on above forms to set it - - *) newFeature event support - *) double click sets preliminary marker, and pops up a form asking - for a value for the preview_attribute. - *) cancel on that form is a no-op; prelim marker goes away. - may need startCreate/cancelCreate events to mediate this - *) submit sends a newFeature event + *) "delete/undelete" button on above forms to show status & trigger changes *) Edit help text and display, similar to current "About" @@ -63,5 +61,8 @@ TODO: for pre-exisiting items, edit_state caches properties at first change + *) glue code to send the editedData events from edit_state, and + have typeahead.js update the FUSE index. - \ No newline at end of file + *) UI for filtering to only edited items, for quick review of current + change set. Could possibly use undo-state info. \ No newline at end of file From 340222a93f11af5d19c8994dcaf4c16eaa141c7d Mon Sep 17 00:00:00 2001 From: Robert Thau Date: Sun, 26 Oct 2014 16:13:54 -0400 Subject: [PATCH 27/56] Stop blowing up names of array properties in editor pane. The JSON Editor marks these up as

    s; we style 'em here to have (pretty much) the same size and weight as the normal labels it uses for simpler properties. --- styles/style.css | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/styles/style.css b/styles/style.css index 15f63b3..0088afb 100644 --- a/styles/style.css +++ b/styles/style.css @@ -196,6 +196,15 @@ body > div.container { border-radius:3px } +/* Further changes; style the

    elements that head arrays same as labels, + since that's the semantic role they serve... + */ + +#info.editing div.well h3 { + font-size: 100%; + font-weight: bold; +} + /* More editing widgetry; styling for the "create feature" popup */ form.create-feature-popup-form { From 412a1ef6c3019d58cdcb4832ab5d695dbd762ff3 Mon Sep 17 00:00:00 2001 From: Robert Thau Date: Sun, 26 Oct 2014 21:07:33 -0400 Subject: [PATCH 28/56] Be more aggressive about disabling dragging on deselect. Due to a leaflet bug, we need to disable dragging *before* calling setIcon. http://stackoverflow.com/questions/22671733/why-doesnt-marker-dragging-disable-work --- src/ui/map.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ui/map.js b/src/ui/map.js index f5e178f..54e5771 100644 --- a/src/ui/map.js +++ b/src/ui/map.js @@ -105,6 +105,7 @@ define(function(require, exports, module) { this.selectFeature = function(ev, feature) { if (this.previouslyClicked) { + this.previouslyClicked.dragging.disable(); this.previouslyClicked.setIcon(this.defaultIcon); this.trigger(document, 'deselectFeature', this.currentFeature); } @@ -144,10 +145,8 @@ define(function(require, exports, module) { this.deselectFeature = function(ev, feature) { if (this.previouslyClicked) { + this.previouslyClicked.dragging.disable(); this.previouslyClicked.setIcon(this.defaultIcon); - if (this.edit_mode) { - this.previouslyClicked.dragging.disable(); - } } var layer = this.attr.features[feature.geometry.coordinates]; // re-bind popup to feature with specified preview attribute From d095733e84ab41753bdcc0f0ac0cdaaf5485abc2 Mon Sep 17 00:00:00 2001 From: Robert Thau Date: Sun, 26 Oct 2014 21:32:11 -0400 Subject: [PATCH 29/56] "Delete" functionality, and tests. As planned, works by sending 'selectedFeatureDeleted' and 'selectedFeatureUndeleted' events, which gray the marker and set a "deleted" flag on the feature itself. --- src/TODO | 8 +++---- src/data/edit_state.js | 29 +++++++++++++++++++++++- src/ui/info.js | 31 +++++++++++++++++++++++--- src/ui/map.js | 14 ++++++++++++ test/spec/data/edit_state_spec.js | 31 ++++++++++++++++++++++++-- test/spec/ui/info_spec.js | 37 +++++++++++++++++++++++++++++++ test/spec/ui/map_spec.js | 14 ++++++++++++ 7 files changed, 154 insertions(+), 10 deletions(-) diff --git a/src/TODO b/src/TODO index 87d0282..f17164f 100644 --- a/src/TODO +++ b/src/TODO @@ -49,10 +49,10 @@ TODO: may need startCreate/cancelCreate events to mediate this +) submit sends a newFeature event - *) delete event support - *) editState sets flag on item, and culls from export - *) map turns opacity on icon to 50% - *) "delete/undelete" button on above forms to show status & trigger changes + +) delete event support + +) editState sets flag on item, and culls from export + +) map turns opacity on icon to 50% + +) "delete/undelete" button on above forms to show status & trigger changes *) Edit help text and display, similar to current "About" diff --git a/src/data/edit_state.js b/src/data/edit_state.js index 280d7f5..9350d0a 100644 --- a/src/data/edit_state.js +++ b/src/data/edit_state.js @@ -2,6 +2,7 @@ define(function(require, exports, module) { 'use strict'; var flight = require('flight'); var $ = require('jquery'); + var _ = require('lodash'); module.exports = flight.component(function() { @@ -21,7 +22,19 @@ define(function(require, exports, module) { // still feels odd. this.provideEdits = function(ev, handlerEvent) { - $(document).trigger(handlerEvent, this.data); + var copy = _.cloneDeep(this.data); + var filtered = _.filter(copy.features, function(feature) { + if (feature.deleted) { + return false; + } + else { + delete feature.deleted; + return true; + } + }); + copy.features = filtered; + this.lastExport = copy; // for tests. sigh... + $(document).trigger(handlerEvent, copy); }; this.newFeature = function(ev, feature) { @@ -40,6 +53,18 @@ define(function(require, exports, module) { } }; + this.markDeletion = function(ev) { + if (this.selectedFeature) { + this.selectedFeature.deleted = true; + } + }; + + this.markUndeletion = function(ev) { + if (this.selectedFeature) { + this.selectedFeature.deleted = false; + } + }; + this.propEdit = function(ev, newProps) { if (this.selectedFeature) { // Events may specify values for only *some* properties; @@ -56,6 +81,8 @@ define(function(require, exports, module) { this.on(document, 'selectFeature', this.selectFeature); this.on(document, 'selectedFeatureMoved', this.selectedFeatureMoved); this.on(document, 'selectedFeaturePropsChanged', this.propEdit); + this.on(document, 'selectedFeatureDeleted', this.markDeletion); + this.on(document, 'selectedFeatureUndeleted', this.markUndeletion); }); }); }); diff --git a/src/ui/info.js b/src/ui/info.js index 22b1dc5..cabfb1a 100644 --- a/src/ui/info.js +++ b/src/ui/info.js @@ -32,7 +32,7 @@ define(function(require, exports, module) { appendTo(this.$node); } if (this.editMode) { - this.startEditing(content, feature.properties); + this.startEditing(content, feature); } else { var popup = templates.popup(this.infoConfig, @@ -43,11 +43,11 @@ define(function(require, exports, module) { this.$node.show(); }; - this.startEditing = function(contentNode, props) { + this.startEditing = function(contentNode, feature) { this.killCurrentEditor(); // in case we had one... var editor = new JSONEditor(contentNode[0], { schema: this.editSchema, - startval: _.cloneDeep(props), + startval: _.cloneDeep(feature.properties), theme: "bootstrap3", iconlib: "bootstrap3", disable_collapse: true, @@ -60,11 +60,34 @@ define(function(require, exports, module) { editor.getValue()); }); + // Add delete button. + var deleteButton = + $(' + + + + + + + +