From a6dff8ce285dcdccc3040c8263c45bc3cddaed35 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sat, 2 Jun 2012 20:02:10 +0100 Subject: [PATCH 01/13] [backend/memory][xs]: rename DataWrapper to Store. --- src/backend/memory.js | 10 ++++++++-- test/backend/memory.test.js | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/backend/memory.js b/src/backend/memory.js index 60ef7811..f42aede6 100644 --- a/src/backend/memory.js +++ b/src/backend/memory.js @@ -15,7 +15,7 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {}; // @param metadata: (optional) dataset metadata - see recline.Model.Dataset. // If not defined (or id not provided) id will be autogenerated. my.createDataset = function(data, fields, metadata) { - var wrapper = new my.DataWrapper(data, fields); + var wrapper = new my.Store(data, fields); var backend = new my.Backbone(); var dataset = new recline.Model.Dataset(metadata, backend); dataset._dataCache = wrapper; @@ -29,7 +29,13 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {}; // Turn a simple array of JS objects into a mini data-store with // functionality like querying, faceting, updating (by ID) and deleting (by // ID). - my.DataWrapper = function(data, fields) { + // + // @param data list of hashes for each record/row in the data ({key: + // value, key: value}) + // @param fields (optional) list of field hashes (each hash defining a field + // as per recline.Model.Field). If fields not specified they will be taken + // from the data. + my.Store = function(data, fields) { var self = this; this.data = data; if (fields) { diff --git a/test/backend/memory.test.js b/test/backend/memory.test.js index 06e32e77..ed76f668 100644 --- a/test/backend/memory.test.js +++ b/test/backend/memory.test.js @@ -1,6 +1,6 @@ (function ($) { -module("Backend Memory - DataWrapper"); +module("Backend Memory - Store"); var memoryData = [ {id: 0, x: 1, y: 2, z: 3, country: 'DE', label: 'first'} @@ -13,7 +13,7 @@ var memoryData = [ var _wrapData = function() { var dataCopy = $.extend(true, [], memoryData); - return new recline.Backend.Memory.DataWrapper(dataCopy); + return new recline.Backend.Memory.Store(dataCopy); } test('basics', function () { From 00052bba0fcf8d1e1d393600bdb390366f901413 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sat, 2 Jun 2012 22:42:34 +0100 Subject: [PATCH 02/13] [view/slickgrid][s]: slickgrid supports renderers/formatters correctly and does not escape html. --- src/model.js | 19 ++++++++++++++----- src/view-slickgrid.js | 26 ++++++++++++++++++++------ test/view-slickgrid.test.js | 4 ++-- 3 files changed, 36 insertions(+), 13 deletions(-) diff --git a/src/model.js b/src/model.js index 99ef250f..d84b5090 100644 --- a/src/model.js +++ b/src/model.js @@ -190,13 +190,22 @@ my.Record = Backbone.Model.extend({ // For the provided Field get the corresponding rendered computed data value // for this record. getFieldValue: function(field) { + val = this.getFieldValueUnrendered(field); + if (field.renderer) { + val = field.renderer(val, field, this.toJSON()); + } + return val; + }, + + // ### getFieldValueUnrendered + // + // For the provided Field get the corresponding computed data value + // for this record. + getFieldValueUnrendered: function(field) { var val = this.get(field.id); if (field.deriver) { val = field.deriver(val, field, this); } - if (field.renderer) { - val = field.renderer(val, field, this); - } return val; }, @@ -233,9 +242,9 @@ my.RecordList = Backbone.Collection.extend({ // Following additional instance properties: // // @property {Function} renderer: a function to render the data for this field. -// Signature: function(value, field, doc) where value is the value of this +// Signature: function(value, field, record) where value is the value of this // cell, field is corresponding field object and record is the record -// object. Note that implementing functions can ignore arguments (e.g. +// object (as simple JS object). Note that implementing functions can ignore arguments (e.g. // function(value) would be a valid formatter function). // // @property {Function} deriver: a function to derive/compute the value of data diff --git a/src/view-slickgrid.js b/src/view-slickgrid.js index efe34eac..1b0e69ac 100644 --- a/src/view-slickgrid.js +++ b/src/view-slickgrid.js @@ -68,12 +68,26 @@ my.SlickGrid = Backbone.View.extend({ // We need all columns, even the hidden ones, to show on the column picker var columns = []; + // custom formatter as default one escapes html + // plus this way we distinguish between rendering/formatting and computed value (so e.g. sort still works ...) + // row = row index, cell = cell index, value = value, columnDef = column definition, dataContext = full row values + var formatter = function(row, cell, value, columnDef, dataContext) { + var field = self.model.fields.get(columnDef.id); + if (field.renderer) { + return field.renderer(value, field, dataContext); + } else { + return value; + } + } _.each(this.model.fields.toJSON(),function(field){ - var column = {id:field['id'], - name:field['label'], - field:field['id'], - sortable: true, - minWidth: 80}; + var column = { + id:field['id'], + name:field['label'], + field:field['id'], + sortable: true, + minWidth: 80, + formatter: formatter + }; var widthInfo = _.find(self.state.get('columnsWidth'),function(c){return c.column == field.id}); if (widthInfo){ @@ -113,7 +127,7 @@ my.SlickGrid = Backbone.View.extend({ this.model.currentRecords.each(function(doc){ var row = {}; self.model.fields.each(function(field){ - row[field.id] = doc.getFieldValue(field); + row[field.id] = doc.getFieldValueUnrendered(field); }); data.push(row); }); diff --git a/test/view-slickgrid.test.js b/test/view-slickgrid.test.js index 1b3448cc..6e5e7017 100644 --- a/test/view-slickgrid.test.js +++ b/test/view-slickgrid.test.js @@ -65,7 +65,7 @@ test('renderers', function () { var dataset = Fixture.getDataset(); dataset.fields.get('country').renderer = function(val, field, doc){ - return 'Country: ' + val; + return 'Country: ' + val + ''; }; var deriver = function(val, field, doc){ @@ -73,7 +73,6 @@ test('renderers', function () { } dataset.fields.add(new recline.Model.Field({id:'computed'},{deriver:deriver})); - var view = new recline.View.SlickGrid({ model: dataset }); @@ -84,6 +83,7 @@ test('renderers', function () { view.grid.init(); equal($(view.grid.getCellNode(0,view.grid.getColumnIndex('country'))).text(),'Country: DE'); + equal($(view.grid.getCellNode(0,view.grid.getColumnIndex('country'))).html(),'Country: DE'); equal($(view.grid.getCellNode(0,view.grid.getColumnIndex('computed'))).text(),'10'); view.remove(); }); From e42510602267b7336464a700e160a594e3a138aa Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 3 Jun 2012 09:49:21 +0100 Subject: [PATCH 03/13] [ux,view/multiview][s]: turn view chooser menu and controls (filters/facets) menu into button groups and make filters/facets section toggle correctly. --- css/multiview.css | 3 --- src/view.multiview.js | 26 ++++++++++++++------------ test/view.multiview.test.js | 4 ++-- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/css/multiview.css b/css/multiview.css index dee4edd5..7a6df9fe 100644 --- a/css/multiview.css +++ b/css/multiview.css @@ -4,7 +4,6 @@ } .recline-data-explorer .header .navigation, -.recline-data-explorer .header .navigation li, .recline-data-explorer .header .pagination, .recline-data-explorer .header .pagination form { @@ -13,8 +12,6 @@ .recline-data-explorer .header .navigation { float: left; - margin-left: 0; - padding-left: 0; } .recline-data-explorer .header .menu-right { diff --git a/src/view.multiview.js b/src/view.multiview.js index ebec0ab9..04d0b847 100644 --- a/src/view.multiview.js +++ b/src/view.multiview.js @@ -75,17 +75,21 @@ my.MultiView = Backbone.View.extend({
\ \
\ - \ +
\ + \
\ Results found {{docCount}} \
\ \
\
\ @@ -220,11 +224,9 @@ my.MultiView = Backbone.View.extend({ }, updateNav: function(pageName) { - this.el.find('.navigation li').removeClass('active'); - this.el.find('.navigation li a').removeClass('disabled'); - var $el = this.el.find('.navigation li a[data-view="' + pageName + '"]'); - $el.parent().addClass('active'); - $el.addClass('disabled'); + this.el.find('.navigation a').removeClass('active'); + var $el = this.el.find('.navigation a[data-view="' + pageName + '"]'); + $el.addClass('active'); // show the specific page _.each(this.pageViews, function(view, idx) { if (view.id === pageName) { @@ -241,9 +243,9 @@ my.MultiView = Backbone.View.extend({ e.preventDefault(); var action = $(e.target).attr('data-action'); if (action === 'filters') { - this.$filterEditor.show(); + this.$filterEditor.toggle(); } else if (action === 'facets') { - this.$facetViewer.show(); + this.$facetViewer.toggle(); } }, diff --git a/test/view.multiview.test.js b/test/view.multiview.test.js index fc7ce53a..6f99fafa 100644 --- a/test/view.multiview.test.js +++ b/test/view.multiview.test.js @@ -61,9 +61,9 @@ test('initialize state', function () { // check the correct view is visible var css = explorer.el.find('.navigation a[data-view="graph"]').attr('class').split(' '); - ok(_.contains(css, 'disabled'), css); + ok(_.contains(css, 'active'), css); var css = explorer.el.find('.navigation a[data-view="grid"]').attr('class').split(' '); - ok(!(_.contains(css, 'disabled')), css); + ok(!(_.contains(css, 'active')), css); // check pass through of view config deepEqual(explorer.state.get('view-grid')['hiddenFields'], ['x']); From 028dfca8591c3024037ee3ae0113ec852b840e29 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 3 Jun 2012 09:50:47 +0100 Subject: [PATCH 04/13] [#110,app,load,ux][s]: refactor import/load menu to be called load, to support DataHub directly, and have better UX. * List options to load from in dropdown menu rather than modal dialog * Provide some help info in load dialog * Support DataHub (including auto-guessing of Data API) - fixes #110. --- app/index.html | 52 ++++++++++++++++++++++++-------------------------- app/js/app.js | 40 ++++++++++++++++++++++++++++---------- 2 files changed, 55 insertions(+), 37 deletions(-) diff --git a/app/index.html b/app/index.html index 6fd4dc9a..2b355186 100644 --- a/app/index.html +++ b/app/index.html @@ -84,13 +84,23 @@
  • - + Share and Embed
  • +
  • + + Settings + + +
  • @@ -229,6 +235,27 @@ + + diff --git a/app/js/app.js b/app/js/app.js index 3cc8c186..6e670b60 100755 --- a/app/js/app.js +++ b/app/js/app.js @@ -8,7 +8,8 @@ var ExplorerApp = Backbone.View.extend({ events: { 'click .nav .js-load-dialog-url': '_onLoadURLDialog', 'submit form.js-load-url': '_onLoadURL', - 'submit .js-load-dialog-file form': '_onLoadFile' + 'submit .js-load-dialog-file form': '_onLoadFile', + 'submit .js-settings form': '_onSettingsSave' }, initialize: function() { @@ -46,6 +47,7 @@ var ExplorerApp = Backbone.View.extend({ if (dataset) { this.createExplorer(dataset, state); } + this._initializeSettings(); }, viewHome: function() { @@ -212,6 +214,28 @@ var ExplorerApp = Backbone.View.extend({ }, options ); + }, + + _getSettings: function() { + var settings = localStorage.getItem('dataexplorer.settings'); + settings = JSON.parse(settings) || {}; + return settings; + }, + + _initializeSettings: function() { + var settings = this._getSettings(); + $('.modal.js-settings form input[name="datahub_api_key"]').val(settings.datahubApiKey); + }, + + _onSettingsSave: function(e) { + var self = this; + e.preventDefault(); + var $form = $(e.target); + $('.modal.js-settings').modal('hide'); + var datahubKey = $form.find('input[name="datahub_api_key"]').val(); + var settings = this._getSettings(); + settings.datahubApiKey = datahubKey; + localStorage.setItem('dataexplorer.settings', JSON.stringify(settings)); } }); From 8cc80d70f115d8f060828d307e73958fd0c1d482 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 3 Jun 2012 21:27:22 +0100 Subject: [PATCH 06/13] [doc/download][xs]: update for latest deps. --- download.markdown | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/download.markdown b/download.markdown index e60f0e95..3d66c99c 100644 --- a/download.markdown +++ b/download.markdown @@ -31,11 +31,15 @@ Recline has dependencies on some third-party libraries, notably JQuery and Backb Optional dependencies: -* JQuery Mustache (required for all views) +* [Mustache.js](https://github.com/janl/mustache.js/) >= 0.5.0-dev (required for all views) * [JQuery Flot](http://code.google.com/p/flot/) >= 0.7 (required for for graph view) * [Leaflet](http://leaflet.cloudmade.com/) >= 0.3.1 (required for map view +* [Verite Timeline](https://github.com/VeriteCo/Timeline/) as of 2012-05-02 * [Bootstrap](http://twitter.github.com/bootstrap/) >= v2.0 (default option for CSS and UI JS but you can use your own) +If you grab the full zipball for Recline this will include all of the relevant +dependencies in the vendor directory. + ### Example Here is an example of the page setup for an app using every Recline component: From 530ebc44a2fa339adfe00ea42ef3331d5eac1928 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 3 Jun 2012 21:55:26 +0100 Subject: [PATCH 07/13] [build][xs]: build recline.js. --- recline.js | 827 +++++++++++++++++++++++++++-------------------------- 1 file changed, 426 insertions(+), 401 deletions(-) diff --git a/recline.js b/recline.js index 51a9bc23..667216b2 100644 --- a/recline.js +++ b/recline.js @@ -80,11 +80,11 @@ this.recline.Model = this.recline.Model || {}; // fields on this Dataset (this can be set explicitly, or, will be set by // Dataset.fetch() or Dataset.query() // -// @property {DocumentList} currentDocuments: a `DocumentList` containing the -// Documents we have currently loaded for viewing (updated by calling query +// @property {RecordList} currentRecords: a `RecordList` containing the +// Records we have currently loaded for viewing (updated by calling query // method) // -// @property {number} docCount: total number of documents in this dataset +// @property {number} docCount: total number of records in this dataset // // @property {Backend} backend: the Backend (instance) for this Dataset. // @@ -116,7 +116,7 @@ my.Dataset = Backbone.Model.extend({ this.backend = this._backendFromString(backend); } this.fields = new my.FieldList(); - this.currentDocuments = new my.DocumentList(); + this.currentRecords = new my.RecordList(); this.facets = new my.FacetList(); this.docCount = null; this.queryState = new my.Query(); @@ -126,12 +126,12 @@ my.Dataset = Backbone.Model.extend({ // ### query // - // AJAX method with promise API to get documents from the backend. + // AJAX method with promise API to get records from the backend. // // It will query based on current query state (given by this.queryState) // updated by queryObj (if provided). // - // Resulting DocumentList are used to reset this.currentDocuments and are + // Resulting RecordList are used to reset this.currentRecords and are // also returned. query: function(queryObj) { var self = this; @@ -141,12 +141,12 @@ my.Dataset = Backbone.Model.extend({ this.backend.query(this, actualQuery).done(function(queryResult) { self.docCount = queryResult.total; var docs = _.map(queryResult.hits, function(hit) { - var _doc = new my.Document(hit._source); + var _doc = new my.Record(hit._source); _doc.backend = self.backend; _doc.dataset = self; return _doc; }); - self.currentDocuments.reset(docs); + self.currentRecords.reset(docs); if (queryResult.facets) { var facets = _.map(queryResult.facets, function(facetResult, facetId) { facetResult.id = facetId; @@ -155,7 +155,7 @@ my.Dataset = Backbone.Model.extend({ self.facets.reset(facets); } self.trigger('query:done'); - dfd.resolve(self.currentDocuments); + dfd.resolve(self.currentRecords); }) .fail(function(arguments) { self.trigger('query:fail', arguments); @@ -244,11 +244,11 @@ my.Dataset.restore = function(state) { return dataset; }; -// ## A Document (aka Row) +// ## A Record (aka Row) // // A single entry or row in the dataset -my.Document = Backbone.Model.extend({ - __type__: 'Document', +my.Record = Backbone.Model.extend({ + __type__: 'Record', initialize: function() { _.bindAll(this, 'getFieldValue'); }, @@ -256,30 +256,49 @@ my.Document = Backbone.Model.extend({ // ### getFieldValue // // For the provided Field get the corresponding rendered computed data value - // for this document. + // for this record. getFieldValue: function(field) { + val = this.getFieldValueUnrendered(field); + if (field.renderer) { + val = field.renderer(val, field, this.toJSON()); + } + return val; + }, + + // ### getFieldValueUnrendered + // + // For the provided Field get the corresponding computed data value + // for this record. + getFieldValueUnrendered: function(field) { var val = this.get(field.id); if (field.deriver) { val = field.deriver(val, field, this); } - if (field.renderer) { - val = field.renderer(val, field, this); - } return val; + }, + + summary: function(fields) { + var html = ''; + for (key in this.attributes) { + if (key != 'id') { + html += '
    ' + key + ': '+ this.attributes[key] + '
    '; + } + } + return html; } }); -// ## A Backbone collection of Documents -my.DocumentList = Backbone.Collection.extend({ - __type__: 'DocumentList', - model: my.Document +// ## A Backbone collection of Records +my.RecordList = Backbone.Collection.extend({ + __type__: 'RecordList', + model: my.Record }); // ## A Field (aka Column) on a Dataset // // Following (Backbone) attributes as standard: // -// * id: a unique identifer for this field- usually this should match the key in the documents hash +// * id: a unique identifer for this field- usually this should match the key in the records hash // * label: (optional: defaults to id) the visible label used for this field // * type: (optional: defaults to string) the type of the data in this field. Should be a string as per type names defined by ElasticSearch - see Types list on // * format: (optional) used to indicate how the data should be formatted. For example: @@ -291,14 +310,14 @@ my.DocumentList = Backbone.Collection.extend({ // Following additional instance properties: // // @property {Function} renderer: a function to render the data for this field. -// Signature: function(value, field, doc) where value is the value of this -// cell, field is corresponding field object and document is the document -// object. Note that implementing functions can ignore arguments (e.g. +// Signature: function(value, field, record) where value is the value of this +// cell, field is corresponding field object and record is the record +// object (as simple JS object). Note that implementing functions can ignore arguments (e.g. // function(value) would be a valid formatter function). // // @property {Function} deriver: a function to derive/compute the value of data // in this field as a function of this field's value (if any) and the current -// document, its signature and behaviour is the same as for renderer. Use of +// record, its signature and behaviour is the same as for renderer. Use of // this function allows you to define an entirely new value for data in this // field. This provides support for a) 'derived/computed' fields: i.e. fields // whose data are functions of the data in other fields b) transforming the @@ -519,7 +538,7 @@ my.Query = Backbone.Model.extend({ // "_type" : "terms", // // total number of tokens in the facet // "total": 5, -// // @property {number} number of documents which have no value for the field +// // @property {number} number of records which have no value for the field // "missing" : 0, // // number of facet values not included in the returned facets // "other": 0, @@ -572,85 +591,6 @@ Backbone.sync = function(method, model, options) { /*jshint multistr:true */ -this.recline = this.recline || {}; -this.recline.Util = this.recline.Util || {}; - -(function(my) { -// ## Miscellaneous Utilities - -var urlPathRegex = /^([^?]+)(\?.*)?/; - -// Parse the Hash section of a URL into path and query string -my.parseHashUrl = function(hashUrl) { - var parsed = urlPathRegex.exec(hashUrl); - if (parsed === null) { - return {}; - } else { - return { - path: parsed[1], - query: parsed[2] || '' - }; - } -}; - -// Parse a URL query string (?xyz=abc...) into a dictionary. -my.parseQueryString = function(q) { - if (!q) { - return {}; - } - var urlParams = {}, - e, d = function (s) { - return unescape(s.replace(/\+/g, " ")); - }, - r = /([^&=]+)=?([^&]*)/g; - - if (q && q.length && q[0] === '?') { - q = q.slice(1); - } - while (e = r.exec(q)) { - // TODO: have values be array as query string allow repetition of keys - urlParams[d(e[1])] = d(e[2]); - } - return urlParams; -}; - -// Parse the query string out of the URL hash -my.parseHashQueryString = function() { - q = my.parseHashUrl(window.location.hash).query; - return my.parseQueryString(q); -}; - -// Compse a Query String -my.composeQueryString = function(queryParams) { - var queryString = '?'; - var items = []; - $.each(queryParams, function(key, value) { - if (typeof(value) === 'object') { - value = JSON.stringify(value); - } - items.push(key + '=' + encodeURIComponent(value)); - }); - queryString += items.join('&'); - return queryString; -}; - -my.getNewHashForQueryString = function(queryParams) { - var queryPart = my.composeQueryString(queryParams); - if (window.location.hash) { - // slice(1) to remove # at start - return window.location.hash.split('?')[0].slice(1) + queryPart; - } else { - return queryPart; - } -}; - -my.setHashQueryString = function(queryParams) { - window.location.hash = my.getNewHashForQueryString(queryParams); -}; -})(this.recline.Util); - -/*jshint multistr:true */ - this.recline = this.recline || {}; this.recline.View = this.recline.View || {}; @@ -748,8 +688,8 @@ my.Graph = Backbone.View.extend({ this.model.bind('change', this.render); this.model.fields.bind('reset', this.render); this.model.fields.bind('add', this.render); - this.model.currentDocuments.bind('add', this.redraw); - this.model.currentDocuments.bind('reset', this.redraw); + this.model.currentRecords.bind('add', this.redraw); + this.model.currentRecords.bind('reset', this.redraw); // because we cannot redraw when hidden we may need when becoming visible this.bind('view:show', function() { if (this.needToRedraw) { @@ -832,7 +772,7 @@ my.Graph = Backbone.View.extend({ // Uncaught Invalid dimensions for plot, width = 0, height = 0 // * There is no data for the plot -- either same error or may have issues later with errors like 'non-existent node-value' var areWeVisible = !jQuery.expr.filters.hidden(this.el[0]); - if ((!areWeVisible || this.model.currentDocuments.length === 0)) { + if ((!areWeVisible || this.model.currentRecords.length === 0)) { this.needToRedraw = true; return; } @@ -860,8 +800,8 @@ my.Graph = Backbone.View.extend({ // However, that is non-trivial to work out from a dataset (datasets may // have no field type info). Thus at present we only do this for bars. var tickFormatter = function (val) { - if (self.model.currentDocuments.models[val]) { - var out = self.model.currentDocuments.models[val].get(self.state.attributes.group); + if (self.model.currentRecords.models[val]) { + var out = self.model.currentRecords.models[val].get(self.state.attributes.group); // if the value was in fact a number we want that not the if (typeof(out) == 'number') { return val; @@ -917,7 +857,7 @@ my.Graph = Backbone.View.extend({ tickLength: 1, tickFormatter: tickFormatter, min: -0.5, - max: self.model.currentDocuments.length - 0.5 + max: self.model.currentRecords.length - 0.5 } } }; @@ -955,8 +895,8 @@ my.Graph = Backbone.View.extend({ y = _tmp; } // convert back from 'index' value on x-axis (e.g. in cases where non-number values) - if (self.model.currentDocuments.models[x]) { - x = self.model.currentDocuments.models[x].get(self.state.attributes.group); + if (self.model.currentRecords.models[x]) { + x = self.model.currentRecords.models[x].get(self.state.attributes.group); } else { x = x.toFixed(2); } @@ -990,13 +930,13 @@ my.Graph = Backbone.View.extend({ var series = []; _.each(this.state.attributes.series, function(field) { var points = []; - _.each(self.model.currentDocuments.models, function(doc, index) { + _.each(self.model.currentRecords.models, function(doc, index) { var xfield = self.model.fields.get(self.state.attributes.group); var x = doc.getFieldValue(xfield); // time series var isDateTime = xfield.get('type') === 'date'; if (isDateTime) { - x = new Date(x); + x = moment(x).toDate(); } var yfield = self.model.fields.get(field); var y = doc.getFieldValue(yfield); @@ -1068,9 +1008,9 @@ my.Grid = Backbone.View.extend({ var self = this; this.el = $(this.el); _.bindAll(this, 'render', 'onHorizontalScroll'); - this.model.currentDocuments.bind('add', this.render); - this.model.currentDocuments.bind('reset', this.render); - this.model.currentDocuments.bind('remove', this.render); + this.model.currentRecords.bind('add', this.render); + this.model.currentRecords.bind('reset', this.render); + this.model.currentRecords.bind('remove', this.render); this.tempState = {}; var state = _.extend({ hiddenFields: [] @@ -1128,13 +1068,13 @@ my.Grid = Backbone.View.extend({ showColumn: function() { self.showColumn(e); }, deleteRow: function() { var self = this; - var doc = _.find(self.model.currentDocuments.models, function(doc) { + var doc = _.find(self.model.currentRecords.models, function(doc) { // important this is == as the currentRow will be string (as comes // from DOM) while id may be int return doc.id == self.tempState.currentRow; }); doc.destroy().then(function() { - self.model.currentDocuments.remove(doc); + self.model.currentRecords.remove(doc); self.trigger('recline:flash', {message: "Row deleted successfully"}); }).fail(function(err) { self.trigger('recline:flash', {message: "Errorz! " + err}); @@ -1264,7 +1204,7 @@ my.Grid = Backbone.View.extend({ }); var htmls = Mustache.render(this.template, this.toTemplateJSON()); this.el.html(htmls); - this.model.currentDocuments.forEach(function(doc) { + this.model.currentRecords.forEach(function(doc) { var tr = $(''); self.el.find('tbody').append(tr); var newView = new my.GridRow({ @@ -1297,7 +1237,7 @@ my.Grid = Backbone.View.extend({ } }); -// ## GridRow View for rendering an individual document. +// ## GridRow View for rendering an individual record. // // Since we want this to update in place it is up to creator to provider the element to attach to. // @@ -1307,7 +1247,7 @@ my.Grid = Backbone.View.extend({ // //
     // var row = new GridRow({
    -//   model: dataset-document,
    +//   model: dataset-record,
     //     el: dom-element,
     //     fields: mydatasets.fields // a FieldList object
     //   });
    @@ -1429,7 +1369,7 @@ this.recline.View = this.recline.View || {};
     
     // ## Map view for a Dataset using Leaflet mapping library.
     //
    -// This view allows to plot gereferenced documents on a map. The location
    +// This view allows to plot gereferenced records on a map. The location
     // information can be provided either via a field with
     // [GeoJSON](http://geojson.org) objects or two fields with latitude and
     // longitude coordinates.
    @@ -1537,14 +1477,14 @@ my.Map = Backbone.View.extend({
           self.render()
         });
     
    -    // Listen to changes in the documents
    -    this.model.currentDocuments.bind('add', function(doc){self.redraw('add',doc)});
    -    this.model.currentDocuments.bind('change', function(doc){
    +    // Listen to changes in the records
    +    this.model.currentRecords.bind('add', function(doc){self.redraw('add',doc)});
    +    this.model.currentRecords.bind('change', function(doc){
             self.redraw('remove',doc);
             self.redraw('add',doc);
         });
    -    this.model.currentDocuments.bind('remove', function(doc){self.redraw('remove',doc)});
    -    this.model.currentDocuments.bind('reset', function(){self.redraw('reset')});
    +    this.model.currentRecords.bind('remove', function(doc){self.redraw('remove',doc)});
    +    this.model.currentRecords.bind('reset', function(){self.redraw('reset')});
     
         this.bind('view:show',function(){
           // If the div was hidden, Leaflet needs to recalculate some sizes
    @@ -1606,9 +1546,9 @@ my.Map = Backbone.View.extend({
       // Actions can be:
       //
       // * reset: Clear all features
    -  // * add: Add one or n features (documents)
    -  // * remove: Remove one or n features (documents)
    -  // * refresh: Clear existing features and add all current documents
    +  // * add: Add one or n features (records)
    +  // * remove: Remove one or n features (records)
    +  // * refresh: Clear existing features and add all current records
       redraw: function(action, doc){
         var self = this;
         action = action || 'refresh';
    @@ -1623,7 +1563,7 @@ my.Map = Backbone.View.extend({
         if (this.geomReady && this.mapReady){
           if (action == 'reset' || action == 'refresh'){
             this.features.clearLayers();
    -        this._add(this.model.currentDocuments.models);
    +        this._add(this.model.currentRecords.models);
           } else if (action == 'add' && doc){
             this._add(doc);
           } else if (action == 'remove' && doc){
    @@ -1688,11 +1628,11 @@ my.Map = Backbone.View.extend({
     
       // Private: Add one or n features to the map
       //
    -  // For each document passed, a GeoJSON geometry will be extracted and added
    +  // For each record passed, a GeoJSON geometry will be extracted and added
       // to the features layer. If an exception is thrown, the process will be
       // stopped and an error notification shown.
       //
    -  // Each feature will have a popup associated with all the document fields.
    +  // Each feature will have a popup associated with all the record fields.
       //
       _add: function(docs){
         var self = this;
    @@ -1703,7 +1643,7 @@ my.Map = Backbone.View.extend({
         var wrongSoFar = 0;
         _.every(docs,function(doc){
           count += 1;
    -      var feature = self._getGeometryFromDocument(doc);
    +      var feature = self._getGeometryFromRecord(doc);
           if (typeof feature === 'undefined' || feature === null){
             // Empty field
             return true;
    @@ -1760,22 +1700,28 @@ my.Map = Backbone.View.extend({
     
       },
     
    -  // Private: Return a GeoJSON geomtry extracted from the document fields
    +  // Private: Return a GeoJSON geomtry extracted from the record fields
       //
    -  _getGeometryFromDocument: function(doc){
    +  _getGeometryFromRecord: function(doc){
         if (this.geomReady){
           if (this.state.get('geomField')){
             var value = doc.get(this.state.get('geomField'));
             if (typeof(value) === 'string'){
               // We *may* have a GeoJSON string representation
               try {
    -            return $.parseJSON(value);
    +            value = $.parseJSON(value);
               } catch(e) {
               }
    -        } else {
    -          // We assume that the contents of the field are a valid GeoJSON object
    -          return value;
             }
    +        if (value && value.lat) {
    +          // not yet geojson so convert
    +          value = {
    +            "type": "Point",
    +            "coordinates": [value.lon || value.lng, value.lat]
    +          };
    +        }
    +        // We now assume that contents of the field are a valid GeoJSON object
    +        return value;
           } else if (this.state.get('lonField') && this.state.get('latField')){
             // We'll create a GeoJSON like point object from the two lat/lon fields
             var lon = doc.get(this.state.get('lonField'));
    @@ -1925,9 +1871,9 @@ my.SlickGrid = Backbone.View.extend({
         var self = this;
         this.el = $(this.el);
         _.bindAll(this, 'render');
    -    this.model.currentDocuments.bind('add', this.render);
    -    this.model.currentDocuments.bind('reset', this.render);
    -    this.model.currentDocuments.bind('remove', this.render);
    +    this.model.currentRecords.bind('add', this.render);
    +    this.model.currentRecords.bind('reset', this.render);
    +    this.model.currentRecords.bind('remove', this.render);
     
         var state = _.extend({
             hiddenColumns: [],
    @@ -1974,12 +1920,26 @@ my.SlickGrid = Backbone.View.extend({
     
         // We need all columns, even the hidden ones, to show on the column picker
         var columns = [];
    +    // custom formatter as default one escapes html
    +    // plus this way we distinguish between rendering/formatting and computed value (so e.g. sort still works ...)
    +    // row = row index, cell = cell index, value = value, columnDef = column definition, dataContext = full row values
    +    var formatter = function(row, cell, value, columnDef, dataContext) {
    +      var field = self.model.fields.get(columnDef.id);
    +      if (field.renderer) {
    +        return field.renderer(value, field, dataContext);
    +      } else {
    +        return value;
    +      }
    +    }
         _.each(this.model.fields.toJSON(),function(field){
    -      var column = {id:field['id'],
    -                    name:field['label'],
    -                    field:field['id'],
    -                    sortable: true,
    -                    minWidth: 80};
    +      var column = {
    +        id:field['id'],
    +        name:field['label'],
    +        field:field['id'],
    +        sortable: true,
    +        minWidth: 80,
    +        formatter: formatter
    +      };
     
           var widthInfo = _.find(self.state.get('columnsWidth'),function(c){return c.column == field.id});
           if (widthInfo){
    @@ -2014,8 +1974,15 @@ my.SlickGrid = Backbone.View.extend({
         }
         columns = columns.concat(tempHiddenColumns);
     
    +    var data = [];
     
    -    var data = this.model.currentDocuments.toJSON();
    +    this.model.currentRecords.each(function(doc){
    +      var row = {};
    +      self.model.fields.each(function(field){
    +        row[field.id] = doc.getFieldValueUnrendered(field);
    +      });
    +      data.push(row);
    +    });
     
         this.grid = new Slick.Grid(this.el, data, visibleColumns, options);
     
    @@ -2231,10 +2198,10 @@ my.Timeline = Backbone.View.extend({
             self._initTimeline();
           }
         });
    -    this.model.fields.bind('change', function() {
    +    this.model.fields.bind('reset', function() {
           self._setupTemporalField();
         });
    -    this.model.currentDocuments.bind('all', function() {
    +    this.model.currentRecords.bind('all', function() {
           self.reloadData();
         });
         var stateData = _.extend({
    @@ -2281,14 +2248,17 @@ my.Timeline = Backbone.View.extend({
             ]
           }
         };
    -    this.model.currentDocuments.each(function(doc) {
    -      var tlEntry = {
    -        "startDate": doc.get(self.state.get('startField')),
    -        "endDate": doc.get(self.state.get('endField')) || null,
    -        "headline": String(doc.get(self.model.fields.models[0].id)),
    -        "text": ''
    -      };
    -      if (tlEntry.startDate) {
    +    this.model.currentRecords.each(function(doc) {
    +      var start = doc.get(self.state.get('startField'));
    +      if (start) {
    +        var end = doc.get(self.state.get('endField'));
    +        end = end ? moment(end).toDate() : null;
    +        var tlEntry = {
    +          "startDate": moment(start).toDate(),
    +          "endDate": end,
    +          "headline": String(doc.get('title') || ''),
    +          "text": doc.summary()
    +        };
             out.timeline.date.push(tlEntry);
           }
         });
    @@ -2415,7 +2385,7 @@ my.ColumnTransform = Backbone.View.extend({
         }
         this.el.modal('hide');
         this.trigger('recline:flash', {message: "Updating all visible docs. This could take a while...", persist: true, loader: true});
    -      var docs = self.model.currentDocuments.map(function(doc) {
    +      var docs = self.model.currentRecords.map(function(doc) {
            return doc.toJSON();
           });
         // TODO: notify about failed docs? 
    @@ -2424,14 +2394,14 @@ my.ColumnTransform = Backbone.View.extend({
         function onCompletedUpdate() {
           totalToUpdate += -1;
           if (totalToUpdate === 0) {
    -        self.trigger('recline:flash', {message: toUpdate.length + " documents updated successfully"});
    +        self.trigger('recline:flash', {message: toUpdate.length + " records updated successfully"});
             alert('WARNING: We have only updated the docs in this view. (Updating of all docs not yet implemented!)');
             self.remove();
           }
         }
         // TODO: Very inefficient as we search through all docs every time!
         _.each(toUpdate, function(editedDoc) {
    -      var realDoc = self.model.currentDocuments.get(editedDoc.id);
    +      var realDoc = self.model.currentRecords.get(editedDoc.id);
           realDoc.set(editedDoc);
           realDoc.save().then(onCompletedUpdate).fail(onCompletedUpdate);
         });
    @@ -2475,7 +2445,7 @@ my.ColumnTransform = Backbone.View.extend({
           var editFunc = costco.evalFunction(e.target.value);
           if (!editFunc.errorMessage) {
             errors.text('No syntax error.');
    -        var docs = self.model.currentDocuments.map(function(doc) {
    +        var docs = self.model.currentRecords.map(function(doc) {
               return doc.toJSON();
             });
             var previewData = costco.previewTransform(docs, editFunc, self.state.currentColumn);
    @@ -2492,110 +2462,17 @@ my.ColumnTransform = Backbone.View.extend({
     })(jQuery, recline.View);
     /*jshint multistr:true */
     
    -// # Recline Views
    -//
    -// Recline Views are instances of Backbone Views and they act as 'WUI' (web
    -// user interface) component displaying some model object in the DOM. Like all
    -// Backbone views they have a pointer to a model (or a collection) and have an
    -// associated DOM-style element (usually this element will be bound into the
    -// page at some point).
    -//
    -// Views provided by core Recline are crudely divided into two types:
    -//
    -// * Dataset Views: a View intended for displaying a recline.Model.Dataset
    -//   in some fashion. Examples are the Grid, Graph and Map views.
    -// * Widget Views: a widget used for displaying some specific (and
    -//   smaller) aspect of a dataset or the application. Examples are
    -//   QueryEditor and FilterEditor which both provide a way for editing (a
    -//   part of) a `recline.Model.Query` associated to a Dataset.
    -//
    -// ## Dataset View
    -//
    -// These views are just Backbone views with a few additional conventions:
    -//
    -// 1. The model passed to the View should always be a recline.Model.Dataset instance
    -// 2. Views should generate their own root element rather than having it passed
    -//    in.
    -// 3. Views should apply a css class named 'recline-{view-name-lower-cased} to
    -//    the root element (and for all CSS for this view to be qualified using this
    -//    CSS class)
    -// 4. Read-only mode: CSS for this view should respect/utilize
    -//    recline-read-only class to trigger read-only behaviour (this class will
    -//    usually be set on some parent element of the view's root element.
    -// 5. State: state (configuration) information for the view should be stored on
    -//    an attribute named state that is an instance of a Backbone Model (or, more
    -//    speficially, be an instance of `recline.Model.ObjectState`). In addition,
    -//    a state attribute may be specified in the Hash passed to a View on
    -//    iniitialization and this information should be used to set the initial
    -//    state of the view.
    -//
    -//    Example of state would be the set of fields being plotted in a graph
    -//    view.
    -//
    -//    More information about State can be found below.
    -//
    -// To summarize some of this, the initialize function for a Dataset View should
    -// look like:
    -//
    -// 
    -//    initialize: {
    -//        model: {a recline.Model.Dataset instance}
    -//        // el: {do not specify - instead view should create}
    -//        state: {(optional) Object / Hash specifying initial state}
    -//        ...
    -//    }
    -// 
    -// -// Note: Dataset Views in core Recline have a common layout on disk as -// follows, where ViewName is the named of View class: -// -//
    -// src/view-{lower-case-ViewName}.js
    -// css/{lower-case-ViewName}.css
    -// test/view-{lower-case-ViewName}.js
    -// 
    -// -// ### State -// -// State information exists in order to support state serialization into the -// url or elsewhere and reloading of application from a stored state. -// -// State is available not only for individual views (as described above) but -// for the dataset (e.g. the current query). For an example of pulling together -// state from across multiple components see `recline.View.DataExplorer`. -// -// ### Flash Messages / Notifications -// -// To send 'flash messages' or notifications the convention is that views -// should fire an event named `recline:flash` with a payload that is a -// flash object with the following attributes (all optional): -// -// * message: message to show. -// * category: warning (default), success, error -// * persist: if true alert is persistent, o/w hidden after 3s (default=false) -// * loader: if true show a loading message -// -// Objects or views wishing to bind to flash messages may then subscribe to -// these events and take some action such as displaying them to the user. For -// an example of such behaviour see the DataExplorer view. -// -// ### Writing your own Views -// -// See the existing Views. -// -// ---- - // Standard JS module setup this.recline = this.recline || {}; this.recline.View = this.recline.View || {}; (function($, my) { -// ## DataExplorer +// ## MultiView // -// The primary view for the entire application. Usage: +// Manage multiple views together along with query editor etc. Usage: // //
    -// var myExplorer = new model.recline.DataExplorer({
    +// var myExplorer = new model.recline.MultiView({
     //   model: {{recline.Model.Dataset instance}}
     //   el: {{an existing dom element}}
     //   views: {{dataset views}}
    @@ -2612,7 +2489,7 @@ this.recline.View = this.recline.View || {};
     // Graph).
     //
     // **views**: (optional) the dataset views (Grid, Graph etc) for
    -// DataExplorer to show. This is an array of view hashes. If not provided
    +// MultiView to show. This is an array of view hashes. If not provided
     // initialize with (recline.View.)Grid, Graph, and Map views (with obvious id
     // and labels!).
     //
    @@ -2653,24 +2530,28 @@ this.recline.View = this.recline.View || {};
     // Note that at present we do *not* serialize information about the actual set
     // of views in use -- e.g. those specified by the views argument -- but instead 
     // expect either that the default views are fine or that the client to have
    -// initialized the DataExplorer with the relevant views themselves.
    -my.DataExplorer = Backbone.View.extend({
    +// initialized the MultiView with the relevant views themselves.
    +my.MultiView = Backbone.View.extend({
       template: ' \
       
    \
    \ \
    \ - \ +
    \ +
    \
    \ Results found {{docCount}} \
    \ \
    \
    \ @@ -2784,16 +2665,20 @@ my.DataExplorer = Backbone.View.extend({ _.each(this.pageViews, function(view, pageName) { $dataViewContainer.append(view.view.el); }); - var queryEditor = new my.QueryEditor({ + 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 my.FilterEditor({ + var filterEditor = new recline.View.FilterEditor({ model: this.model.queryState }); this.$filterEditor = filterEditor.el; this.el.find('.header').append(filterEditor.el); - var facetViewer = new my.FacetViewer({ + var facetViewer = new recline.View.FacetViewer({ model: this.model }); this.$facetViewer = facetViewer.el; @@ -2801,11 +2686,9 @@ my.DataExplorer = Backbone.View.extend({ }, updateNav: function(pageName) { - this.el.find('.navigation li').removeClass('active'); - this.el.find('.navigation li a').removeClass('disabled'); - var $el = this.el.find('.navigation li a[data-view="' + pageName + '"]'); - $el.parent().addClass('active'); - $el.addClass('disabled'); + this.el.find('.navigation a').removeClass('active'); + var $el = this.el.find('.navigation a[data-view="' + pageName + '"]'); + $el.addClass('active'); // show the specific page _.each(this.pageViews, function(view, idx) { if (view.id === pageName) { @@ -2822,9 +2705,9 @@ my.DataExplorer = Backbone.View.extend({ e.preventDefault(); var action = $(e.target).attr('data-action'); if (action === 'filters') { - this.$filterEditor.show(); + this.$filterEditor.toggle(); } else if (action === 'facets') { - this.$facetViewer.show(); + this.$facetViewer.toggle(); } }, @@ -2843,7 +2726,7 @@ my.DataExplorer = Backbone.View.extend({ _setupState: function(initialState) { var self = this; // get data from the query string / hash url plus some defaults - var qs = recline.Util.parseHashQueryString(); + var qs = my.parseHashQueryString(); var query = qs.reclineQuery; query = query ? JSON.parse(query) : self.model.queryState.toJSON(); // backwards compatability (now named view-graph but was named graph) @@ -2945,74 +2828,179 @@ my.DataExplorer = Backbone.View.extend({ } }); -// ### DataExplorer.restore +// ### MultiView.restore // -// Restore a DataExplorer instance from a serialized state including the associated dataset -my.DataExplorer.restore = function(state) { +// Restore a MultiView instance from a serialized state including the associated dataset +my.MultiView.restore = function(state) { var dataset = recline.Model.Dataset.restore(state); - var explorer = new my.DataExplorer({ + var explorer = new my.MultiView({ model: dataset, state: state }); return explorer; } -my.QueryEditor = Backbone.View.extend({ - className: 'recline-query-editor', + +// ## Miscellaneous Utilities +var urlPathRegex = /^([^?]+)(\?.*)?/; + +// Parse the Hash section of a URL into path and query string +my.parseHashUrl = function(hashUrl) { + var parsed = urlPathRegex.exec(hashUrl); + if (parsed === null) { + return {}; + } else { + return { + path: parsed[1], + query: parsed[2] || '' + }; + } +}; + +// Parse a URL query string (?xyz=abc...) into a dictionary. +my.parseQueryString = function(q) { + if (!q) { + return {}; + } + var urlParams = {}, + e, d = function (s) { + return unescape(s.replace(/\+/g, " ")); + }, + r = /([^&=]+)=?([^&]*)/g; + + if (q && q.length && q[0] === '?') { + q = q.slice(1); + } + while (e = r.exec(q)) { + // TODO: have values be array as query string allow repetition of keys + urlParams[d(e[1])] = d(e[2]); + } + return urlParams; +}; + +// Parse the query string out of the URL hash +my.parseHashQueryString = function() { + q = my.parseHashUrl(window.location.hash).query; + return my.parseQueryString(q); +}; + +// Compse a Query String +my.composeQueryString = function(queryParams) { + var queryString = '?'; + var items = []; + $.each(queryParams, function(key, value) { + if (typeof(value) === 'object') { + value = JSON.stringify(value); + } + items.push(key + '=' + encodeURIComponent(value)); + }); + queryString += items.join('&'); + return queryString; +}; + +my.getNewHashForQueryString = function(queryParams) { + var queryPart = my.composeQueryString(queryParams); + if (window.location.hash) { + // slice(1) to remove # at start + return window.location.hash.split('?')[0].slice(1) + queryPart; + } else { + return queryPart; + } +}; + +my.setHashQueryString = function(queryParams) { + window.location.hash = my.getNewHashForQueryString(queryParams); +}; + +})(jQuery, recline.View); + +/*jshint multistr:true */ + +this.recline = this.recline || {}; +this.recline.View = this.recline.View || {}; + +(function($, my) { + +my.FacetViewer = Backbone.View.extend({ + className: 'recline-facet-viewer well', template: ' \ -
    \ -
    \ - \ - \ + × \ +
    \ +
    \ +

    Facets

    \
    \ - \ ', events: { - 'submit form': 'onFormSubmit', - 'click .action-pagination-update': 'onPaginationUpdate' + 'click .js-hide': 'onHide', + 'click .js-facet-filter': 'onFacetFilter' }, - - initialize: function() { + initialize: function(model) { _.bindAll(this, 'render'); this.el = $(this.el); - this.model.bind('change', this.render); + this.model.facets.bind('all', this.render); + this.model.fields.bind('all', this.render); this.render(); }, - onFormSubmit: function(e) { - e.preventDefault(); - var query = this.el.find('.text-query input').val(); - var newFrom = parseInt(this.el.find('input[name="from"]').val()); - var newSize = parseInt(this.el.find('input[name="to"]').val()) - newFrom; - this.model.set({size: newSize, from: newFrom, q: query}); - }, - onPaginationUpdate: function(e) { - e.preventDefault(); - var $el = $(e.target); - var newFrom = 0; - if ($el.parent().hasClass('prev')) { - newFrom = this.model.get('from') - Math.max(0, this.model.get('size')); - } else { - newFrom = this.model.get('from') + this.model.get('size'); - } - this.model.set({from: newFrom}); - }, render: function() { - var tmplData = this.model.toJSON(); - tmplData.to = this.model.get('from') + this.model.get('size'); + var tmplData = { + facets: this.model.facets.toJSON(), + fields: this.model.fields.toJSON() + }; + tmplData.facets = _.map(tmplData.facets, function(facet) { + if (facet._type === 'date_histogram') { + facet.entries = _.map(facet.entries, function(entry) { + entry.term = new Date(entry.time).toDateString(); + return entry; + }); + } + return facet; + }); var templated = Mustache.render(this.template, tmplData); this.el.html(templated); + // are there actually any facets to show? + if (this.model.facets.length > 0) { + this.el.show(); + } else { + this.el.hide(); + } + }, + onHide: function(e) { + e.preventDefault(); + this.el.hide(); + }, + onFacetFilter: function(e) { + var $target= $(e.target); + var fieldId = $target.closest('.facet-summary').attr('data-facet'); + var value = $target.attr('data-value'); + this.model.queryState.addTermFilter(fieldId, value); } }); + +})(jQuery, recline.View); + +/*jshint multistr:true */ + +this.recline = this.recline || {}; +this.recline.View = this.recline.View || {}; + +(function($, my) { + my.FilterEditor = Backbone.View.extend({ className: 'recline-filter-editor well', template: ' \ @@ -3112,76 +3100,107 @@ my.FilterEditor = Backbone.View.extend({ } }); -my.FacetViewer = Backbone.View.extend({ - className: 'recline-facet-viewer well', + +})(jQuery, recline.View); + +/*jshint multistr:true */ + +this.recline = this.recline || {}; +this.recline.View = this.recline.View || {}; + +(function($, my) { + +my.Pager = Backbone.View.extend({ + className: 'recline-pager', template: ' \ - × \ -
    \ -
    \ -

    Facets

    \ -
    \ - {{#facets}} \ - \ - {{/facets}} \ + \ ', events: { - 'click .js-hide': 'onHide', - 'click .js-facet-filter': 'onFacetFilter' + 'click .action-pagination-update': 'onPaginationUpdate', + 'change input': 'onFormSubmit' }, - initialize: function(model) { + + initialize: function() { _.bindAll(this, 'render'); this.el = $(this.el); - this.model.facets.bind('all', this.render); - this.model.fields.bind('all', this.render); + this.model.bind('change', this.render); this.render(); }, + onFormSubmit: function(e) { + e.preventDefault(); + var newFrom = parseInt(this.el.find('input[name="from"]').val()); + var newSize = parseInt(this.el.find('input[name="to"]').val()) - newFrom; + this.model.set({size: newSize, from: newFrom}); + }, + onPaginationUpdate: function(e) { + e.preventDefault(); + var $el = $(e.target); + var newFrom = 0; + if ($el.parent().hasClass('prev')) { + newFrom = this.model.get('from') - Math.max(0, this.model.get('size')); + } else { + newFrom = this.model.get('from') + this.model.get('size'); + } + this.model.set({from: newFrom}); + }, render: function() { - var tmplData = { - facets: this.model.facets.toJSON(), - fields: this.model.fields.toJSON() - }; - tmplData.facets = _.map(tmplData.facets, function(facet) { - if (facet._type === 'date_histogram') { - facet.entries = _.map(facet.entries, function(entry) { - entry.term = new Date(entry.time).toDateString(); - return entry; - }); - } - return facet; - }); + var tmplData = this.model.toJSON(); + tmplData.to = this.model.get('from') + this.model.get('size'); var templated = Mustache.render(this.template, tmplData); this.el.html(templated); - // are there actually any facets to show? - if (this.model.facets.length > 0) { - this.el.show(); - } else { - this.el.hide(); - } - }, - onHide: function(e) { - e.preventDefault(); - this.el.hide(); - }, - onFacetFilter: function(e) { - var $target= $(e.target); - var fieldId = $target.closest('.facet-summary').attr('data-facet'); - var value = $target.attr('data-value'); - this.model.queryState.addTermFilter(fieldId, value); } }); +})(jQuery, recline.View); + +/*jshint multistr:true */ + +this.recline = this.recline || {}; +this.recline.View = this.recline.View || {}; + +(function($, my) { + +my.QueryEditor = Backbone.View.extend({ + className: 'recline-query-editor', + template: ' \ +
    \ +
    \ + \ + \ +
    \ + \ +
    \ + ', + + events: { + 'submit form': 'onFormSubmit' + }, + + initialize: function() { + _.bindAll(this, 'render'); + this.el = $(this.el); + this.model.bind('change', this.render); + this.render(); + }, + onFormSubmit: function(e) { + e.preventDefault(); + var query = this.el.find('.text-query input').val(); + this.model.set({q: query}); + }, + render: function() { + var tmplData = this.model.toJSON(); + tmplData.to = this.model.get('from') + this.model.get('size'); + var templated = Mustache.render(this.template, tmplData); + this.el.html(templated); + } +}); })(jQuery, recline.View); @@ -3216,14 +3235,14 @@ this.recline.Backend.Base = function() { // ### sync // // An implementation of Backbone.sync that will be used to override - // Backbone.sync on operations for Datasets and Documents which are using this backend. + // Backbone.sync on operations for Datasets and Records which are using this backend. // // For read-only implementations you will need only to implement read method // for Dataset models (and even this can be a null operation). The read method // should return relevant metadata for the Dataset. We do not require read support - // for Documents because they are loaded in bulk by the query method. + // for Records because they are loaded in bulk by the query method. // - // For backends supporting write operations you must implement update and delete support for Document objects. + // For backends supporting write operations you must implement update and delete support for Record objects. // // All code paths should return an object conforming to the jquery promise API. this.sync = function(method, model, options) { @@ -3231,8 +3250,8 @@ this.recline.Backend.Base = function() { // ### query // - // Query the backend for documents returning them in bulk. This method will - // be used by the Dataset.query method to search the backend for documents, + // Query the backend for records returning them in bulk. This method will + // be used by the Dataset.query method to search the backend for records, // retrieving the results in bulk. // // @param {recline.model.Dataset} model: Dataset model. @@ -3258,11 +3277,11 @@ this.recline.Backend.Base = function() { //
       // {
       //   total: // (required) total number of results (can be null)
    -  //   hits: [ // (required) one entry for each result document
    +  //   hits: [ // (required) one entry for each result record
       //     {
    -  //        _score:   // (optional) match score for document
    -  //        _type: // (optional) document type
    -  //        _source: // (required) document/row object
    +  //        _score:   // (optional) match score for record
    +  //        _type: // (optional) record type
    +  //        _source: // (required) record/row object
       //     } 
       //   ],
       //   facets: { // (optional) 
    @@ -3626,7 +3645,7 @@ this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {};
     
         // ### get
         //
    -    // Get document corresponding to specified id
    +    // Get record corresponding to specified id
         //
         // @return promise compatible deferred object.
         this.get = function(id) {
    @@ -3639,7 +3658,7 @@ this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {};
     
         // ### upsert
         //
    -    // create / update a document to ElasticSearch backend
    +    // create / update a record to ElasticSearch backend
         //
         // @param {Object} doc an object to insert to the index.
         // @return deferred supporting promise API
    @@ -3659,7 +3678,7 @@ this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {};
     
         // ### delete
         //
    -    // Delete a document from the ElasticSearch backend.
    +    // Delete a record from the ElasticSearch backend.
         //
         // @param {Object} id id of object to delete
         // @return deferred supporting promise API
    @@ -3741,7 +3760,7 @@ this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {};
         // Backbone sync implementation for this backend.
         //
         // URL of ElasticSearch endpoint to use must be specified on the dataset
    -    // (and on a Document via its dataset attribute) by the dataset having a
    +    // (and on a Record via its dataset attribute) by the dataset having a
         // url attribute.
         this.sync = function(method, model, options) {
           if (model.__type__ == 'Dataset') {
    @@ -3767,15 +3786,15 @@ this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {};
                 dfd.reject(arguments);
               });
               return dfd.promise();
    -        } else if (model.__type__ == 'Document') {
    +        } else if (model.__type__ == 'Record') {
               return es.get(model.dataset.id);
             }
           } else if (method === 'update') {
    -        if (model.__type__ == 'Document') {
    +        if (model.__type__ == 'Record') {
               return es.upsert(model.toJSON());
             }
           } else if (method === 'delete') {
    -        if (model.__type__ == 'Document') {
    +        if (model.__type__ == 'Record') {
               return es.delete(model.id);
             }
           }
    @@ -3987,7 +4006,7 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
       //
       // Convenience function to create a simple 'in-memory' dataset in one step.
       //
    -  // @param data: list of hashes for each document/row in the data ({key:
    +  // @param data: list of hashes for each record/row in the data ({key:
       // value, key: value})
       // @param fields: (optional) list of field hashes (each hash defining a hash
       // as per recline.Model.Field). If fields not specified they will be taken
    @@ -3995,7 +4014,7 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
       // @param metadata: (optional) dataset metadata - see recline.Model.Dataset.
       // If not defined (or id not provided) id will be autogenerated.
       my.createDataset = function(data, fields, metadata) {
    -    var wrapper = new my.DataWrapper(data, fields);
    +    var wrapper = new my.Store(data, fields);
         var backend = new my.Backbone();
         var dataset = new recline.Model.Dataset(metadata, backend);
         dataset._dataCache = wrapper;
    @@ -4009,7 +4028,13 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
       // Turn a simple array of JS objects into a mini data-store with
       // functionality like querying, faceting, updating (by ID) and deleting (by
       // ID).
    -  my.DataWrapper = function(data, fields) {
    +  //
    +  // @param data list of hashes for each record/row in the data ({key:
    +  // value, key: value})
    +  // @param fields (optional) list of field hashes (each hash defining a field
    +  // as per recline.Model.Field). If fields not specified they will be taken
    +  // from the data.
    +  my.Store = function(data, fields) {
         var self = this;
         this.data = data;
         if (fields) {
    @@ -4056,7 +4081,7 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
           results = results.slice(start, start+numRows);
           return {
             total: total,
    -        documents: results,
    +        records: results,
             facets: facets
           };
         };
    @@ -4098,7 +4123,7 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
           return results;
         };
     
    -    this.computeFacets = function(documents, queryObj) {
    +    this.computeFacets = function(records, queryObj) {
           var facetResults = {};
           if (!queryObj.facets) {
             return facetResults;
    @@ -4109,7 +4134,7 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
             facetResults[facetId].termsall = {};
           });
           // faceting
    -      _.each(documents, function(doc) {
    +      _.each(records, function(doc) {
             _.each(queryObj.facets, function(query, facetId) {
               var fieldId = query.terms.field;
               var val = doc[fieldId];
    @@ -4152,13 +4177,13 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
             }
             return dfd.promise();
           } else if (method === 'update') {
    -        if (model.__type__ == 'Document') {
    +        if (model.__type__ == 'Record') {
               model.dataset._dataCache.update(model.toJSON());
               dfd.resolve(model);
             }
             return dfd.promise();
           } else if (method === 'delete') {
    -        if (model.__type__ == 'Document') {
    +        if (model.__type__ == 'Record') {
               model.dataset._dataCache.delete(model.toJSON());
               dfd.resolve(model);
             }
    @@ -4171,7 +4196,7 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
         this.query = function(model, queryObj) {
           var dfd = $.Deferred();
           var results = model._dataCache.query(queryObj);
    -      var hits = _.map(results.documents, function(row) {
    +      var hits = _.map(results.records, function(row) {
             return { _source: row };
           });
           var out = {
    
    From 915f00e21a984ba219618b98d91c48e5596809ff Mon Sep 17 00:00:00 2001
    From: Rufus Pollock 
    Date: Sun, 3 Jun 2012 22:08:22 +0100
    Subject: [PATCH 08/13] [view/timeline,bugfix][xs]: timeline crashes if no
     entries so create placeholder entry for timeline if no entries from data.
    
    ---
     src/view-timeline.js | 8 ++++++++
     1 file changed, 8 insertions(+)
    
    diff --git a/src/view-timeline.js b/src/view-timeline.js
    index 9d98d371..c556a1e4 100644
    --- a/src/view-timeline.js
    +++ b/src/view-timeline.js
    @@ -92,6 +92,14 @@ my.Timeline = Backbone.View.extend({
             out.timeline.date.push(tlEntry);
           }
         });
    +    // if no entries create a placeholder entry to prevent Timeline crashing with error
    +    if (out.timeline.date.length === 0) {
    +      var tlEntry = {
    +        "startDate": '2000,1,1',
    +        "headline": 'No data to show!'
    +      };
    +      out.timeline.date.push(tlEntry);
    +    }
         return out;
       },
     
    
    From a7b6a6ac5d7da8391c7b1462edc759cbfeb68fc4 Mon Sep 17 00:00:00 2001
    From: Rufus Pollock 
    Date: Mon, 4 Jun 2012 08:26:57 +0100
    Subject: [PATCH 09/13] [#145,widget/fields][m]: add new fields widget view
     which lists all fields with field info and replace facet widget in multiview
     with it.
    
    * widget is very incomplete and does not yet show anything of great value (plan is to have summary info).
    ---
     app/index.html        |  2 +-
     css/multiview.css     | 13 ++++++++
     src/view.multiview.js | 12 ++++----
     src/widget.fields.js  | 70 +++++++++++++++++++++++++++++++++++++++++++
     test/index.html       |  2 +-
     5 files changed, 91 insertions(+), 8 deletions(-)
     create mode 100644 src/widget.fields.js
    
    diff --git a/app/index.html b/app/index.html
    index cc807f41..11d576c8 100644
    --- a/app/index.html
    +++ b/app/index.html
    @@ -67,7 +67,7 @@
       
       
       
    -  
    +  
       
     
       
    diff --git a/css/multiview.css b/css/multiview.css
    index 7a6df9fe..4c661429 100644
    --- a/css/multiview.css
    +++ b/css/multiview.css
    @@ -94,6 +94,19 @@
       display: inline;
     }
     
    +/**********************************************************
    +  * Fields Widget
    +  *********************************************************/
    +
    +.recline-fields-view .fields-list .accordion-heading a,
    +.recline-fields-view .fields-list .accordion-heading h3 {
    +  display: inline;
    +}
    +
    +.recline-fields-view .fields-list .accordion-heading h3 {
    +  margin-left: 10px;
    +}
    +
     /**********************************************************
       * Notifications
       *********************************************************/
    diff --git a/src/view.multiview.js b/src/view.multiview.js
    index 04d0b847..4b8417c3 100644
    --- a/src/view.multiview.js
    +++ b/src/view.multiview.js
    @@ -88,7 +88,7 @@ my.MultiView = Backbone.View.extend({
            \
           
    \ @@ -216,11 +216,11 @@ my.MultiView = Backbone.View.extend({ }); this.$filterEditor = filterEditor.el; this.el.find('.header').append(filterEditor.el); - var facetViewer = new recline.View.FacetViewer({ + var fieldsView = new recline.View.Fields({ model: this.model }); - this.$facetViewer = facetViewer.el; - this.el.find('.header').append(facetViewer.el); + this.$fieldsView = fieldsView.el; + this.el.find('.header').append(fieldsView.el); }, updateNav: function(pageName) { @@ -244,8 +244,8 @@ my.MultiView = Backbone.View.extend({ var action = $(e.target).attr('data-action'); if (action === 'filters') { this.$filterEditor.toggle(); - } else if (action === 'facets') { - this.$facetViewer.toggle(); + } else if (action === 'fields') { + this.$fieldsView.toggle(); } }, diff --git a/src/widget.fields.js b/src/widget.fields.js new file mode 100644 index 00000000..a2568467 --- /dev/null +++ b/src/widget.fields.js @@ -0,0 +1,70 @@ +/*jshint multistr:true */ + +// Field Info +// +// For each field +// +// Id / Label / type / format + +// Editor -- to change type (and possibly format) +// Editor for show/hide ... + +// Summaries of fields +// +// Top values / number empty +// If number: max, min average ... + +// Box to boot transform editor ... + +this.recline = this.recline || {}; +this.recline.View = this.recline.View || {}; + +(function($, my) { + +my.Fields = Backbone.View.extend({ + className: 'recline-fields-view well', + template: ' \ +
    \ + {{#fields}} \ +
    \ +
    \ +

    \ + {{label}} ({{id}}) \ + \ + {{type}} \ + More information » \ + \ + \ +

    \ +
    \ +
    \ +
    \ + {{type}} \ +
    \ +
    \ +
    \ + {{/fields}} \ +
    \ + ', + + events: { + }, + initialize: function(model) { + _.bindAll(this, 'render'); + this.el = $(this.el); + this.model.fields.bind('all', this.render); + this.render(); + }, + render: function() { + var tmplData = { + fields: this.model.fields.toJSON() + }; + var templated = Mustache.render(this.template, tmplData); + this.el.html(templated); + // this.el.hide(); + this.el.find('.collapse').collapse() + } +}); + +})(jQuery, recline.View); + diff --git a/test/index.html b/test/index.html index 84d0231e..26fed5dd 100644 --- a/test/index.html +++ b/test/index.html @@ -49,7 +49,7 @@ - + From 58cac002dc20615a05e39d2eb608e3e35703f73b Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Mon, 4 Jun 2012 23:57:24 +0100 Subject: [PATCH 10/13] [#145,widget/fields,model][m]: working fields widget with field summary data generated by Dataset.getFieldsSummary function. * Layout is still not right -- need to put widget in sidebar (though looks a lot better) * Datset.getFieldsSummary to compute facets for fields * Remove facet usage in the demo --- app/js/app.js | 1 - css/multiview.css | 28 +++++++++++++++++++++++-- src/model.js | 26 +++++++++++++++++++++++ src/widget.fields.js | 49 ++++++++++++++++++++++++++++++++------------ test/model.test.js | 15 ++++++++++++++ 5 files changed, 103 insertions(+), 16 deletions(-) diff --git a/app/js/app.js b/app/js/app.js index 6e670b60..2e456f17 100755 --- a/app/js/app.js +++ b/app/js/app.js @@ -242,7 +242,6 @@ var ExplorerApp = Backbone.View.extend({ // provide a demonstration in memory dataset function localDataset() { var dataset = Fixture.getDataset(); - dataset.queryState.addFacet('country'); return dataset; } diff --git a/css/multiview.css b/css/multiview.css index 4c661429..ccdb52e6 100644 --- a/css/multiview.css +++ b/css/multiview.css @@ -98,15 +98,39 @@ * Fields Widget *********************************************************/ +.recline-fields-view { + width: 200px; +} + +.recline-fields-view .fields-list { + padding: 0; +} + .recline-fields-view .fields-list .accordion-heading a, -.recline-fields-view .fields-list .accordion-heading h3 { +.recline-fields-view .fields-list .accordion-heading h4 { display: inline; } -.recline-fields-view .fields-list .accordion-heading h3 { +.recline-fields-view .fields-list .accordion-heading h4 { margin-left: 10px; } +.recline-fields-view .clear { + clear: both; +} + +.recline-fields-view .facet-items { + list-style-type: none; + margin-left: 0; +} + +.recline-fields-view .facet-item .term { + font-weight: bold; +} + +.recline-fields-view .facet-item .count { +} + /********************************************************** * Notifications *********************************************************/ diff --git a/src/model.js b/src/model.js index d84b5090..11eca39f 100644 --- a/src/model.js +++ b/src/model.js @@ -111,6 +111,31 @@ my.Dataset = Backbone.Model.extend({ return data; }, + // Get a summary for each field in the form of a `Facet`. + // + // @return null as this is async function. Provides deferred/promise interface. + getFieldsSummary: function() { + var self = this; + var query = new my.Query(); + query.set({size: 0}); + this.fields.each(function(field) { + query.addFacet(field.id); + }); + var dfd = $.Deferred(); + this.backend.query(this, query.toJSON()).done(function(queryResult) { + if (queryResult.facets) { + _.each(queryResult.facets, function(facetResult, facetId) { + facetResult.id = facetId; + var facet = new my.Facet(facetResult); + // TODO: probably want replace rather than reset (i.e. just replace the facet with this id) + self.fields.get(facetId).facets.reset(facet); + }); + } + dfd.resolve(queryResult); + }); + return dfd.promise(); + }, + // ### _backendFromString(backendString) // // See backend argument to initialize for details @@ -291,6 +316,7 @@ my.Field = Backbone.Model.extend({ if (!this.renderer) { this.renderer = this.defaultRenderers[this.get('type')]; } + this.facets = new my.FacetList(); }, defaultRenderers: { object: function(val, field, doc) { diff --git a/src/widget.fields.js b/src/widget.fields.js index a2568467..91ff4d16 100644 --- a/src/widget.fields.js +++ b/src/widget.fields.js @@ -22,24 +22,33 @@ this.recline.View = this.recline.View || {}; (function($, my) { my.Fields = Backbone.View.extend({ - className: 'recline-fields-view well', + className: 'recline-fields-view', template: ' \ -
    \ +
    \ {{#fields}} \ -
    \ +
    \
    \ -

    \ - {{label}} ({{id}}) \ +

    \ + {{label}} \ \ {{type}} \ - More information » \ + » \ \ \ -

    \ + \
    \
    \
    \ - {{type}} \ + {{#facets}} \ +
    \ +
      \ + {{#terms}} \ +
    • {{term}} [{{count}}]
    • \ + {{/terms}} \ +
    \ +
    \ + {{/facets}} \ +
    \
    \
    \
    \ @@ -50,19 +59,33 @@ my.Fields = Backbone.View.extend({ events: { }, initialize: function(model) { - _.bindAll(this, 'render'); + var self = this; this.el = $(this.el); - this.model.fields.bind('all', this.render); + _.bindAll(this, 'render'); + + this.model.fields.bind('all', function() { + // fields can get reset or changed in which case we need to recalculate + self.model.getFieldsSummary(); + self.render(); + }); + this.model.fields.each(function(field) { + field.facets.bind('all', self.render); + }); this.render(); }, render: function() { + var self = this; var tmplData = { - fields: this.model.fields.toJSON() + fields: [] }; + this.model.fields.each(function(field) { + var out = field.toJSON(); + out.facets = field.facets.toJSON(); + tmplData.fields.push(out); + }); var templated = Mustache.render(this.template, tmplData); this.el.html(templated); - // this.el.hide(); - this.el.find('.collapse').collapse() + this.el.find('.collapse').collapse(); } }); diff --git a/test/model.test.js b/test/model.test.js index 90d2e6d7..0433be74 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -125,6 +125,21 @@ test('Dataset _prepareQuery', function () { deepEqual(out, exp); }); +test('Dataset getFieldsSummary', function () { + var dataset = Fixture.getDataset(); + dataset.getFieldsSummary().done(function() { + var countryField = dataset.fields.get('country'); + var facet = countryField.facets.models[0]; + equal(facet.get('terms').length, 3); + var exp = [ + { count: 3, term: 'UK' }, + { count: 2, term: 'DE' }, + { count: 1, term: 'US' } + ]; + deepEqual(facet.get('terms'), exp); + }); +}); + // ================================= // Query From 91ec4ee6b99b3c0f8f09cf017487ee183e09bda2 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Tue, 5 Jun 2012 00:19:01 +0100 Subject: [PATCH 11/13] [#145,view/multiview,fields][s]: much nicer sidebar layout for fields. --- css/multiview.css | 5 ++++- src/view.multiview.js | 5 +++-- src/widget.fields.js | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/css/multiview.css b/css/multiview.css index ccdb52e6..021033cb 100644 --- a/css/multiview.css +++ b/css/multiview.css @@ -1,6 +1,9 @@ .recline-data-explorer .data-view-container { display: block; - clear: both; +} + +.recline-data-explorer .data-view-sidebar { + float: right; } .recline-data-explorer .header .navigation, diff --git a/src/view.multiview.js b/src/view.multiview.js index 4b8417c3..dd90d641 100644 --- a/src/view.multiview.js +++ b/src/view.multiview.js @@ -88,12 +88,13 @@ my.MultiView = Backbone.View.extend({ \
    \
    \
    \ +
    \
    \
    \ ', @@ -220,7 +221,7 @@ my.MultiView = Backbone.View.extend({ model: this.model }); this.$fieldsView = fieldsView.el; - this.el.find('.header').append(fieldsView.el); + this.el.find('.data-view-sidebar').append(fieldsView.el); }, updateNav: function(pageName) { diff --git a/src/widget.fields.js b/src/widget.fields.js index 91ff4d16..7d86cc09 100644 --- a/src/widget.fields.js +++ b/src/widget.fields.js @@ -85,7 +85,7 @@ my.Fields = Backbone.View.extend({ }); var templated = Mustache.render(this.template, tmplData); this.el.html(templated); - this.el.find('.collapse').collapse(); + this.el.find('.collapse').collapse('hide'); } }); From a1a62321d4c95eef3e98da92b93adaace71942f5 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Tue, 5 Jun 2012 00:28:26 +0100 Subject: [PATCH 12/13] [#145,bugfix][s]: was not re-rendering fields on data update correctly for case of genuine async load of field summaries. --- src/widget.fields.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/widget.fields.js b/src/widget.fields.js index 7d86cc09..f4894273 100644 --- a/src/widget.fields.js +++ b/src/widget.fields.js @@ -64,13 +64,13 @@ my.Fields = Backbone.View.extend({ _.bindAll(this, 'render'); this.model.fields.bind('all', function() { + self.model.fields.each(function(field) { + field.facets.bind('all', self.render); + }); // fields can get reset or changed in which case we need to recalculate self.model.getFieldsSummary(); self.render(); }); - this.model.fields.each(function(field) { - field.facets.bind('all', self.render); - }); this.render(); }, render: function() { From 56a29c80a70c999513c55dba20fb5a228230a5a7 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Tue, 5 Jun 2012 00:31:28 +0100 Subject: [PATCH 13/13] [#145,css/multiview][xs]: micro tweaks to make sidebar look better and to space header from rest of material. --- css/multiview.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/css/multiview.css b/css/multiview.css index 021033cb..2404e304 100644 --- a/css/multiview.css +++ b/css/multiview.css @@ -4,6 +4,11 @@ .recline-data-explorer .data-view-sidebar { float: right; + margin-left: 8px; +} + +.recline-data-explorer .header .navigation { + margin-bottom: 8px; } .recline-data-explorer .header .navigation,