From ef74f13163cfee02dd28b8f5ecf6f08e59619ce3 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Wed, 6 Jun 2012 14:22:02 +0100 Subject: [PATCH] [#145,view/refactor][m]: switch to a proper sidebar setup where views can have both a main element and a sidebar element and this added to central sidebar. * this fixes functional and visual bugs with current (new) setup where fields where in separate in RHS sidebar * Refactored map and graph view to put menu/controls in the sidebar (this is quite nice!) --- css/graph.css | 23 --- css/multiview.css | 2 + src/view.graph.js | 234 ++++++++++++++++------------- src/view.map.js | 319 +++++++++++++++++++++++----------------- src/view.multiview.js | 20 ++- test/view.graph.test.js | 25 +++- test/view.map.test.js | 26 +++- 7 files changed, 380 insertions(+), 269 deletions(-) diff --git a/css/graph.css b/css/graph.css index d88168c4..363d1fe9 100644 --- a/css/graph.css +++ b/css/graph.css @@ -1,6 +1,5 @@ .recline-graph .graph { height: 500px; - margin-right: 200px; } .recline-graph .legend table { @@ -18,25 +17,3 @@ margin: auto; } -/********************************************************** - * Editor - *********************************************************/ - -.recline-graph .editor { - float: right; - width: 200px; - padding-left: 0px; -} - -.recline-graph .editor form { - padding-left: 4px; -} - -.recline-graph .editor select { - width: 100%; -} - -.recline-graph .editor-hide-info p { - display: none; -} - diff --git a/css/multiview.css b/css/multiview.css index 2404e304..a73a8f0f 100644 --- a/css/multiview.css +++ b/css/multiview.css @@ -1,10 +1,12 @@ .recline-data-explorer .data-view-container { display: block; + margin-right: 225px; } .recline-data-explorer .data-view-sidebar { float: right; margin-left: 8px; + width: 220px; } .recline-data-explorer .header .navigation { diff --git a/src/view.graph.js b/src/view.graph.js index e7059cac..c67dc293 100644 --- a/src/view.graph.js +++ b/src/view.graph.js @@ -21,44 +21,10 @@ this.recline.View = this.recline.View || {}; // NB: should *not* provide an el argument to the view but must let the view // generate the element itself (you can then append view.el to the DOM. my.Graph = Backbone.View.extend({ - tagName: "div", className: "recline-graph", template: ' \ -
\ -
\ -
\ - \ -
\ - \ -
\ - \ -
\ - \ -
\ -
\ -
\ -
\ -
\ - \ -
\ - \ -
\ -
\
\
\

Hey there!

\ @@ -68,26 +34,6 @@ my.Graph = Backbone.View.extend({
\
\ ', - templateSeriesEditor: ' \ -
\ - \ -
\ - \ -
\ -
\ - ', - - events: { - 'change form select': 'onEditorSubmit', - 'click .editor-add': '_onAddSeries', - 'click .action-remove-series': 'removeSeries' - }, initialize: function(options) { var self = this; @@ -114,6 +60,15 @@ my.Graph = Backbone.View.extend({ options.state ); this.state = new recline.Model.ObjectState(stateData); + this.editor = new my.GraphControls({ + model: this.model, + state: this.state.toJSON() + }); + this.editor.state.bind('change', function() { + self.state.set(self.editor.state.toJSON()); + self.redraw(); + }); + this.elSidebar = this.editor.el; this.render(); }, @@ -123,56 +78,9 @@ my.Graph = Backbone.View.extend({ var htmls = Mustache.render(this.template, tmplData); $(this.el).html(htmls); this.$graph = this.el.find('.panel.graph'); - - // set up editor from state - if (this.state.get('graphType')) { - this._selectOption('.editor-type', this.state.get('graphType')); - } - if (this.state.get('group')) { - this._selectOption('.editor-group', this.state.get('group')); - } - // ensure at least one series box shows up - var tmpSeries = [""]; - if (this.state.get('series').length > 0) { - tmpSeries = this.state.get('series'); - } - _.each(tmpSeries, function(series, idx) { - self.addSeries(idx); - self._selectOption('.editor-series.js-series-' + idx, series); - }); return this; }, - // Private: Helper function to select an option from a select list - // - _selectOption: function(id,value){ - var options = this.el.find(id + ' select > option'); - if (options) { - options.each(function(opt){ - if (this.value == value) { - $(this).attr('selected','selected'); - return false; - } - }); - } - }, - - onEditorSubmit: function(e) { - var select = this.el.find('.editor-group select'); - var $editor = this; - var $series = this.el.find('.editor-series select'); - var series = $series.map(function () { - return $(this).val(); - }); - var updatedState = { - series: $.makeArray(series), - group: this.el.find('.editor-group select').val(), - graphType: this.el.find('.editor-type select').val() - }; - this.state.set(updatedState); - this.redraw(); - }, - redraw: function() { // There appear to be issues generating a Flot graph if either: @@ -187,6 +95,8 @@ my.Graph = Backbone.View.extend({ } // check we have something to plot if (this.state.get('group') && this.state.get('series')) { + // faff around with width because flot draws axes *outside* of the element width which means graph can get push down as it hits element next to it + this.$graph.width(this.el.width() - 20); var series = this.createSeries(); var options = this.getGraphOptions(this.state.attributes.graphType); this.plot = $.plot(this.$graph, series, options); @@ -362,6 +272,128 @@ my.Graph = Backbone.View.extend({ series.push({data: points, label: field}); }); return series; + } +}); + +my.GraphControls = Backbone.View.extend({ + className: "editor", + template: ' \ +
\ +
\ +
\ + \ +
\ + \ +
\ + \ +
\ + \ +
\ +
\ +
\ +
\ +
\ + \ +
\ + \ +
\ +
\ +', + templateSeriesEditor: ' \ +
\ + \ +
\ + \ +
\ +
\ + ', + events: { + 'change form select': 'onEditorSubmit', + 'click .editor-add': '_onAddSeries', + 'click .action-remove-series': 'removeSeries' + }, + + initialize: function(options) { + var self = this; + this.el = $(this.el); + _.bindAll(this, 'render'); + this.model.fields.bind('reset', this.render); + this.model.fields.bind('add', this.render); + this.state = new recline.Model.ObjectState(options.state); + this.render(); + }, + + render: function() { + var self = this; + var tmplData = this.model.toTemplateJSON(); + var htmls = Mustache.render(this.template, tmplData); + this.el.html(htmls); + + // set up editor from state + if (this.state.get('graphType')) { + this._selectOption('.editor-type', this.state.get('graphType')); + } + if (this.state.get('group')) { + this._selectOption('.editor-group', this.state.get('group')); + } + // ensure at least one series box shows up + var tmpSeries = [""]; + if (this.state.get('series').length > 0) { + tmpSeries = this.state.get('series'); + } + _.each(tmpSeries, function(series, idx) { + self.addSeries(idx); + self._selectOption('.editor-series.js-series-' + idx, series); + }); + return this; + }, + + // Private: Helper function to select an option from a select list + // + _selectOption: function(id,value){ + var options = this.el.find(id + ' select > option'); + if (options) { + options.each(function(opt){ + if (this.value == value) { + $(this).attr('selected','selected'); + return false; + } + }); + } + }, + + onEditorSubmit: function(e) { + var select = this.el.find('.editor-group select'); + var $editor = this; + var $series = this.el.find('.editor-series select'); + var series = $series.map(function () { + return $(this).val(); + }); + var updatedState = { + series: $.makeArray(series), + group: this.el.find('.editor-group select').val(), + graphType: this.el.find('.editor-type select').val() + }; + this.state.set(updatedState); }, // Public: Adds a new empty series select box to the editor. diff --git a/src/view.map.js b/src/view.map.js index 79b19492..eb8f9631 100644 --- a/src/view.map.js +++ b/src/view.map.js @@ -24,68 +24,11 @@ this.recline.View = this.recline.View || {}; // } // my.Map = Backbone.View.extend({ - tagName: 'div', className: 'recline-map', template: ' \ -
\ -
\ -
\ -
\ - \ - \ -
\ -
\ - \ -
\ - \ -
\ - \ -
\ - \ -
\ -
\ - \ -
\ -
\ - \ -
\ -
\ - \ -
\ - \ -
\ - \ - \ -
\ -
\ +
\ ', // These are the default (case-insensitive) names of field that are used if found. @@ -94,23 +37,12 @@ my.Map = Backbone.View.extend({ longitudeFieldNames: ['lon','longitude'], geometryFieldNames: ['geom','the_geom','geometry','spatial','location'], - // Define here events for UI elements - events: { - 'click .editor-update-map': 'onEditorSubmit', - 'change .editor-field-type': 'onFieldTypeChange', - 'change #editor-auto-zoom': 'onAutoZoomChange' - }, - initialize: function(options) { var self = this; this.el = $(this.el); // Listen to changes in the fields this.model.fields.bind('change', function() { - self._setupGeometryField(); - }); - this.model.fields.bind('add', this.render); - this.model.fields.bind('reset', function(){ self._setupGeometryField() self.render() }); @@ -143,39 +75,36 @@ my.Map = Backbone.View.extend({ var stateData = _.extend({ geomField: null, lonField: null, - latField: null + latField: null, + autoZoom: true }, options.state ); this.state = new recline.Model.ObjectState(stateData); + this.menu = new my.MapMenu({ + model: this.model, + state: this.state.toJSON() + }); + this.menu.state.bind('change', function() { + self.state.set(self.menu.state.toJSON()); + self.redraw(); + }); + this.elSidebar = this.menu.el; - this.autoZoom = true; this.mapReady = false; this.render(); + this.redraw(); }, // ### Public: Adds the necessary elements to the page. // // Also sets up the editor fields and the map if necessary. render: function() { - var self = this; htmls = Mustache.render(this.template, this.model.toTemplateJSON()); - $(this.el).html(htmls); this.$map = this.el.find('.panel.map'); - - if (this.geomReady && this.model.fields.length){ - if (this.state.get('geomField')){ - this._selectOption('editor-geom-field',this.state.get('geomField')); - $('#editor-field-type-geom').attr('checked','checked').change(); - } else{ - this._selectOption('editor-lon-field',this.state.get('lonField')); - this._selectOption('editor-lat-field',this.state.get('latField')); - $('#editor-field-type-latlon').attr('checked','checked').change(); - } - } return this; }, @@ -191,14 +120,14 @@ my.Map = Backbone.View.extend({ var self = this; action = action || 'refresh'; // try to set things up if not already - if (!self.geomReady){ + if (!self._geomReady()){ self._setupGeometryField(); } if (!self.mapReady){ self._setupMap(); } - if (this.geomReady && this.mapReady){ + if (this._geomReady() && this.mapReady){ if (action == 'reset' || action == 'refresh'){ this.features.clearLayers(); this._add(this.model.currentRecords.models); @@ -207,7 +136,7 @@ my.Map = Backbone.View.extend({ } else if (action == 'remove' && doc){ this._remove(doc); } - if (this.autoZoom){ + if (this.state.get('autoZoom')){ if (this.visible){ this._zoomToFeatures(); } else { @@ -217,51 +146,8 @@ my.Map = Backbone.View.extend({ } }, - // - // UI Event handlers - // - - // Public: Update map with user options - // - // Right now the only configurable option is what field(s) contains the - // location information. - // - onEditorSubmit: function(e){ - e.preventDefault(); - if ($('#editor-field-type-geom').attr('checked')){ - this.state.set({ - geomField: $('.editor-geom-field > select > option:selected').val(), - lonField: null, - latField: null - }); - } else { - this.state.set({ - geomField: null, - lonField: $('.editor-lon-field > select > option:selected').val(), - latField: $('.editor-lat-field > select > option:selected').val() - }); - } - this.geomReady = (this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField'))); - this.redraw(); - - return false; - }, - - // Public: Shows the relevant select lists depending on the location field - // type selected. - // - onFieldTypeChange: function(e){ - if (e.target.value == 'geom'){ - $('.editor-field-type-geom').show(); - $('.editor-field-type-latlon').hide(); - } else { - $('.editor-field-type-geom').hide(); - $('.editor-field-type-latlon').show(); - } - }, - - onAutoZoomChange: function(e){ - this.autoZoom = !this.autoZoom; + _geomReady: function() { + return Boolean(this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField'))); }, // Private: Add one or n features to the map @@ -341,7 +227,7 @@ my.Map = Backbone.View.extend({ // Private: Return a GeoJSON geomtry extracted from the record fields // _getGeometryFromRecord: function(doc){ - if (this.geomReady){ + if (this._geomReady()){ if (this.state.get('geomField')){ var value = doc.get(this.state.get('geomField')); if (typeof(value) === 'string'){ @@ -380,16 +266,14 @@ my.Map = Backbone.View.extend({ // // If not found, the user can define them via the UI form. _setupGeometryField: function(){ - var geomField, latField, lonField; - this.geomReady = (this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField'))); // should not overwrite if we have already set this (e.g. explicitly via state) - if (!this.geomReady) { + if (!this._geomReady()) { this.state.set({ geomField: this._checkField(this.geometryFieldNames), latField: this._checkField(this.latitudeFieldNames), lonField: this._checkField(this.longitudeFieldNames) }); - this.geomReady = (this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField'))); + this.menu.state.set(this.state.toJSON()); } }, @@ -427,7 +311,6 @@ my.Map = Backbone.View.extend({ // on [OpenStreetMap](http://openstreetmap.org). // _setupMap: function(){ - this.map = new L.Map(this.$map.get(0)); var mapUrl = "http://otile{s}.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.png"; @@ -483,8 +366,166 @@ my.Map = Backbone.View.extend({ }); } } +}); - }); +my.MapMenu = Backbone.View.extend({ + className: 'editor', + + template: ' \ +
\ +
\ +
\ + \ + \ +
\ +
\ + \ +
\ + \ +
\ + \ +
\ + \ +
\ +
\ + \ +
\ +
\ + \ +
\ +
\ + \ +
\ + \ + \ +
\ +', + + // Define here events for UI elements + events: { + 'click .editor-update-map': 'onEditorSubmit', + 'change .editor-field-type': 'onFieldTypeChange', + 'change #editor-auto-zoom': 'onAutoZoomChange' + }, + + initialize: function(options) { + var self = this; + this.el = $(this.el); + _.bindAll(this, 'render'); + this.model.fields.bind('change', this.render); + this.state = new recline.Model.ObjectState(options.state); + this.state.bind('change', this.render); + this.render(); + }, + + // ### Public: Adds the necessary elements to the page. + // + // Also sets up the editor fields and the map if necessary. + render: function() { + var self = this; + htmls = Mustache.render(this.template, this.model.toTemplateJSON()); + $(this.el).html(htmls); + + if (this._geomReady() && this.model.fields.length){ + if (this.state.get('geomField')){ + this._selectOption('editor-geom-field',this.state.get('geomField')); + $('#editor-field-type-geom').attr('checked','checked').change(); + } else{ + this._selectOption('editor-lon-field',this.state.get('lonField')); + this._selectOption('editor-lat-field',this.state.get('latField')); + $('#editor-field-type-latlon').attr('checked','checked').change(); + } + } + return this; + }, + + _geomReady: function() { + return Boolean(this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField'))); + }, + + // ## UI Event handlers + // + + // Public: Update map with user options + // + // Right now the only configurable option is what field(s) contains the + // location information. + // + onEditorSubmit: function(e){ + e.preventDefault(); + if (this.el.find('#editor-field-type-geom').attr('checked')){ + this.state.set({ + geomField: this.el.find('.editor-geom-field > select > option:selected').val(), + lonField: null, + latField: null + }); + } else { + this.state.set({ + geomField: null, + lonField: this.el.find('.editor-lon-field > select > option:selected').val(), + latField: this.el.find('.editor-lat-field > select > option:selected').val() + }); + } + return false; + }, + + // Public: Shows the relevant select lists depending on the location field + // type selected. + // + onFieldTypeChange: function(e){ + if (e.target.value == 'geom'){ + this.el.find('.editor-field-type-geom').show(); + this.el.find('.editor-field-type-latlon').hide(); + } else { + this.el.find('.editor-field-type-geom').hide(); + this.el.find('.editor-field-type-latlon').show(); + } + }, + + onAutoZoomChange: function(e){ + this.state.set({autoZoom: !this.state.get('autoZoom')}); + }, + + // Private: Helper function to select an option from a select list + // + _selectOption: function(id,value){ + var options = this.el.find('.' + id + ' > select > option'); + if (options){ + options.each(function(opt){ + if (this.value == value) { + $(this).attr('selected','selected'); + return false; + } + }); + } + } +}); })(jQuery, recline.View); diff --git a/src/view.multiview.js b/src/view.multiview.js index dd90d641..96447891 100644 --- a/src/view.multiview.js +++ b/src/view.multiview.js @@ -200,28 +200,40 @@ my.MultiView = Backbone.View.extend({ tmplData.views = this.pageViews; var template = Mustache.render(this.template, tmplData); $(this.el).html(template); + + // now create and append other views var $dataViewContainer = this.el.find('.data-view-container'); + var $dataSidebar = this.el.find('.data-view-sidebar'); + + // the main views _.each(this.pageViews, function(view, pageName) { $dataViewContainer.append(view.view.el); + if (view.view.elSidebar) { + $dataSidebar.append(view.view.elSidebar); + } }); + var pager = new recline.View.Pager({ model: this.model.queryState }); this.el.find('.recline-results-info').after(pager.el); + var queryEditor = new recline.View.QueryEditor({ model: this.model.queryState }); this.el.find('.query-editor-here').append(queryEditor.el); + var filterEditor = new recline.View.FilterEditor({ model: this.model.queryState }); this.$filterEditor = filterEditor.el; this.el.find('.header').append(filterEditor.el); + var fieldsView = new recline.View.Fields({ model: this.model }); this.$fieldsView = fieldsView.el; - this.el.find('.data-view-sidebar').append(fieldsView.el); + $dataSidebar.append(fieldsView.el); }, updateNav: function(pageName) { @@ -232,9 +244,15 @@ my.MultiView = Backbone.View.extend({ _.each(this.pageViews, function(view, idx) { if (view.id === pageName) { view.view.el.show(); + if (view.view.elSidebar) { + view.view.elSidebar.show(); + } view.view.trigger('view:show'); } else { view.view.el.hide(); + if (view.view.elSidebar) { + view.view.elSidebar.hide(); + } view.view.trigger('view:hide'); } }); diff --git a/test/view.graph.test.js b/test/view.graph.test.js index 3b015f12..13c48efc 100644 --- a/test/view.graph.test.js +++ b/test/view.graph.test.js @@ -8,7 +8,7 @@ test('basics', function () { $('.fixtures').append(view.el); equal(view.state.get('graphType'), 'lines-and-points'); // view will auto render ... - assertPresent('.editor', view.el); + assertPresent('.editor', view.elSidebar); view.remove(); }); @@ -27,9 +27,9 @@ test('initialize', function () { deepEqual(view.state.get('series'), ['y', 'z']); // check we have updated editor with state info - equal(view.el.find('.editor-type select').val(), 'lines'); - equal(view.el.find('.editor-group select').val(), 'x'); - var out = _.map(view.el.find('.editor-series select'), function($el) { + equal(view.elSidebar.find('.editor-type select').val(), 'lines'); + equal(view.elSidebar.find('.editor-group select').val(), 'x'); + var out = _.map(view.elSidebar.find('.editor-series select'), function($el) { return $($el).val(); }); deepEqual(out, ['y', 'z']); @@ -51,3 +51,20 @@ test('dates in graph view', function () { view.remove(); }); + +test('GraphControls basics', function () { + var dataset = Fixture.getDataset(); + var view = new recline.View.GraphControls({ + model: dataset, + state: { + graphType: 'bars', + series: [] + } + }); + $('.fixtures').append(view.el); + equal(view.state.get('graphType'), 'bars'); + // view will auto render ... + assertPresent('.editor', view.el); + view.remove(); +}); + diff --git a/test/view.map.test.js b/test/view.map.test.js index 61e4d5a3..f5d26406 100644 --- a/test/view.map.test.js +++ b/test/view.map.test.js @@ -31,7 +31,7 @@ test('basics', function () { //Fire query, otherwise the map won't be initialized dataset.query(); - assertPresent('.editor',view.el); + assertPresent('.editor-field-type', view.elSidebar); // Check that the Leaflet map was set up assertPresent('.leaflet-container',view.el); @@ -42,6 +42,21 @@ test('basics', function () { view.remove(); }); +test('_setupGeometryField', function () { + var dataset = Fixture.getDataset(); + var view = new recline.View.Map({ + model: dataset + }); + var exp = { + geomField: null, + lonField: 'lon', + latField: 'lat', + autoZoom: true + }; + deepEqual(view.state.toJSON(), exp); + deepEqual(view.menu.state.toJSON(), exp); +}); + test('Lat/Lon geom fields', function () { var dataset = Fixture.getDataset(); var view = new recline.View.Map({ @@ -138,6 +153,15 @@ test('Popup', function () { view.remove(); }); +test('MapMenu', function () { + var dataset = Fixture.getDataset(); + var controls = new recline.View.MapMenu({ + model: dataset, + state: {} + }); + assertPresent('.editor-field-type', controls.el); +}); + var _getFeaturesCount = function(features){ var cnt = 0; features._iterateLayers(function(layer){