diff --git a/README.md b/README.md index be7e8ce6..fc65ceb0 100755 --- a/README.md +++ b/README.md @@ -35,6 +35,8 @@ Possible breaking changes: * State only stores backend (name) and dataset url (in url field) rather than entire dataset object * Backends heavily reorganized +* Rename Document -> Record +* Rename DataExplorer view to MultiView ### v0.4 - April 26th 2012 diff --git a/_includes/recline-deps.html b/_includes/recline-deps.html index ad77703a..d5066d74 100644 --- a/_includes/recline-deps.html +++ b/_includes/recline-deps.html @@ -5,10 +5,10 @@ - + diff --git a/app/index.html b/app/index.html index 0c56105f..6fd4dc9a 100644 --- a/app/index.html +++ b/app/index.html @@ -19,11 +19,11 @@ - + @@ -49,7 +49,6 @@ - @@ -58,13 +57,18 @@ - + + - + + + + + diff --git a/app/js/app.js b/app/js/app.js index 990be82a..fb8d375e 100755 --- a/app/js/app.js +++ b/app/js/app.js @@ -21,7 +21,7 @@ var ExplorerApp = Backbone.View.extend({ this.router.route(/explorer/, 'explorer', this.viewExplorer); Backbone.history.start(); - var state = recline.Util.parseQueryString(decodeURIComponent(window.location.search)); + var state = recline.View.parseQueryString(decodeURIComponent(window.location.search)); if (state) { _.each(state, function(value, key) { try { @@ -108,7 +108,7 @@ var ExplorerApp = Backbone.View.extend({ } ]; - this.dataExplorer = new recline.View.DataExplorer({ + this.dataExplorer = new recline.View.MultiView({ model: dataset, el: $el, state: state, @@ -145,7 +145,7 @@ var ExplorerApp = Backbone.View.extend({ }, makePermaLink: function(state) { - var qs = recline.Util.composeQueryString(state.toJSON()); + var qs = recline.View.composeQueryString(state.toJSON()); return window.location.origin + window.location.pathname + qs; }, diff --git a/css/data-explorer.css b/css/multiview.css similarity index 82% rename from css/data-explorer.css rename to css/multiview.css index d1a6a94f..dee4edd5 100644 --- a/css/data-explorer.css +++ b/css/multiview.css @@ -27,7 +27,7 @@ .header .recline-results-info { line-height: 28px; margin-left: 20px; - display: inline; + float: left; } /********************************************************** @@ -39,42 +39,54 @@ height: 30px; } -.header .recline-query-editor .input-prepend { +.header .input-prepend { margin-bottom: auto; } -.recline-query-editor .add-on { +.header .add-on { float: left; } /* needed for Chrome but not FF */ -.header .recline-query-editor .add-on { +.header .add-on { margin-left: -27px; } /* needed for FF but not chrome */ -.header .recline-query-editor .input-prepend { +.header .input-prepend { vertical-align: top; } -.header .recline-query-editor .pagination input { - width: 30px; - height: 18px; - padding: 2px 4px; - margin-top: -4px; -} - -.header .recline-query-editor .pagination a { - line-height: 26px; - padding: 0 6px; -} - .header .recline-query-editor form button { vertical-align: top; } /********************************************************** - * Query Editor + * Pager + *********************************************************/ + +.header .recline-pager { + float: left; + margin: auto; + display: block; + margin-left: 20px; +} + +.header .recline-pager .pagination input { + width: 30px; + height: 18px; + padding: 2px 4px; + margin: 0; + margin-top: -4px; +} + +.header .recline-pager .pagination a { + line-height: 26px; + padding: 0 6px; +} + +/********************************************************** + * Filter Editor *********************************************************/ .recline-filter-editor .filter-term .input-append a { diff --git a/library-view.markdown b/library-view.markdown new file mode 100644 index 00000000..96ab9353 --- /dev/null +++ b/library-view.markdown @@ -0,0 +1,100 @@ +--- +layout: container +title: Library - Views +--- + +
+ 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.
+
diff --git a/library.html b/library.html
index 7d654861..850afcf3 100644
--- a/library.html
+++ b/library.html
@@ -13,7 +13,7 @@ title: Library - Home
Building on Backbone, Recline supplies components and structure to data-heavy applications by providing a - set of models (Dataset, Document/Row, Field) and views (Grid, Map, Graph + set of models (Dataset, Record/Row, Field) and views (Grid, Map, Graph etc).
There are two main model objects:
Backends connect Dataset and Documents to data from a +
Backends connect Dataset and Records to data from a specific 'Backend' data source. They provide methods for loading and saving - Datasets and individuals Documents as well as for bulk loading via a query API + Datasets and individuals Records as well as for bulk loading via a query API and doing bulk transforms on the backend.
A template Base class can be found in the - Backend base module of the source docs. It documents both the relevant + Backend base module of the source docs. It records both the relevant methods a Backend must have and (optionally) provides a base 'class' for inheritance. You can also find detailed examples of backend implementations in the source documentation below.
@@ -112,7 +112,7 @@ title: Library - HomeComplementing the model are various Views (you can also easily write your own). Each view holds a pointer to a Dataset:
// {
// 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)
diff --git a/src/backend/elasticsearch.js b/src/backend/elasticsearch.js
index a73d4140..bcab5d95 100644
--- a/src/backend/elasticsearch.js
+++ b/src/backend/elasticsearch.js
@@ -39,7 +39,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) {
@@ -52,7 +52,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
@@ -72,7 +72,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
@@ -154,7 +154,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') {
@@ -180,15 +180,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);
}
}
diff --git a/src/backend/memory.js b/src/backend/memory.js
index f7fa8afb..60ef7811 100644
--- a/src/backend/memory.js
+++ b/src/backend/memory.js
@@ -7,7 +7,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
@@ -76,7 +76,7 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {};
results = results.slice(start, start+numRows);
return {
total: total,
- documents: results,
+ records: results,
facets: facets
};
};
@@ -118,7 +118,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;
@@ -129,7 +129,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];
@@ -172,13 +172,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);
}
@@ -191,7 +191,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 = {
diff --git a/src/model.js b/src/model.js
index a32e5521..99ef250f 100644
--- a/src/model.js
+++ b/src/model.js
@@ -12,11 +12,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.
//
@@ -48,7 +48,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();
@@ -58,12 +58,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;
@@ -73,12 +73,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;
@@ -87,7 +87,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);
@@ -176,11 +176,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');
},
@@ -188,7 +188,7 @@ 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) {
var val = this.get(field.id);
if (field.deriver) {
@@ -211,17 +211,17 @@ my.Document = Backbone.Model.extend({
}
});
-// ## 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:
@@ -234,13 +234,13 @@ my.DocumentList = Backbone.Collection.extend({
//
// @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
+// cell, field is corresponding field object and record is the record
// 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
@@ -461,7 +461,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,
diff --git a/src/util.js b/src/util.js
deleted file mode 100644
index dbba7b0e..00000000
--- a/src/util.js
+++ /dev/null
@@ -1,79 +0,0 @@
-/*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);
-
diff --git a/src/view-graph.js b/src/view-graph.js
index ff3cf3b9..e7059cac 100644
--- a/src/view-graph.js
+++ b/src/view-graph.js
@@ -97,8 +97,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) {
@@ -181,7 +181,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;
}
@@ -209,8 +209,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;
@@ -266,7 +266,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
}
}
};
@@ -304,8 +304,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);
}
@@ -339,7 +339,7 @@ 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
diff --git a/src/view-grid.js b/src/view-grid.js
index fc9400b9..03213cd1 100644
--- a/src/view-grid.js
+++ b/src/view-grid.js
@@ -17,9 +17,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: []
@@ -77,13 +77,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});
@@ -213,7 +213,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({
@@ -246,7 +246,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.
//
@@ -256,7 +256,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
// });
diff --git a/src/view-map.js b/src/view-map.js
index 8e622a0a..79b19492 100644
--- a/src/view-map.js
+++ b/src/view-map.js
@@ -7,7 +7,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.
@@ -115,14 +115,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
@@ -184,9 +184,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';
@@ -201,7 +201,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){
@@ -266,11 +266,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;
@@ -281,7 +281,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;
@@ -338,22 +338,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'));
diff --git a/src/view-slickgrid.js b/src/view-slickgrid.js
index f42e905e..efe34eac 100644
--- a/src/view-slickgrid.js
+++ b/src/view-slickgrid.js
@@ -19,9 +19,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: [],
@@ -110,7 +110,7 @@ my.SlickGrid = Backbone.View.extend({
var data = [];
- this.model.currentDocuments.each(function(doc){
+ this.model.currentRecords.each(function(doc){
var row = {};
self.model.fields.each(function(field){
row[field.id] = doc.getFieldValue(field);
diff --git a/src/view-timeline.js b/src/view-timeline.js
index 18ad35ef..9d98d371 100644
--- a/src/view-timeline.js
+++ b/src/view-timeline.js
@@ -31,7 +31,7 @@ my.Timeline = Backbone.View.extend({
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({
@@ -78,11 +78,11 @@ my.Timeline = Backbone.View.extend({
]
}
};
- this.model.currentDocuments.each(function(doc) {
+ this.model.currentRecords.each(function(doc) {
var start = doc.get(self.state.get('startField'));
if (start) {
- var end = moment(doc.get(self.state.get('endField')));
- end = end ? end.toDate() : null;
+ var end = doc.get(self.state.get('endField'));
+ end = end ? moment(end).toDate() : null;
var tlEntry = {
"startDate": moment(start).toDate(),
"endDate": end,
diff --git a/src/view-transform-dialog.js b/src/view-transform-dialog.js
index e4bee9ae..a0ae741f 100644
--- a/src/view-transform-dialog.js
+++ b/src/view-transform-dialog.js
@@ -98,7 +98,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?
@@ -107,14 +107,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);
});
@@ -158,7 +158,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);
diff --git a/src/view.js b/src/view.js
deleted file mode 100644
index c49b6592..00000000
--- a/src/view.js
+++ /dev/null
@@ -1,695 +0,0 @@
-/*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
-//
-// The primary view for the entire application. Usage:
-//
-//
-// var myExplorer = new model.recline.DataExplorer({
-// model: {{recline.Model.Dataset instance}}
-// el: {{an existing dom element}}
-// views: {{dataset views}}
-// state: {{state configuration -- see below}}
-// });
-//
-//
-// ### Parameters
-//
-// **model**: (required) recline.model.Dataset instance.
-//
-// **el**: (required) DOM element to bind to. NB: the element already
-// being in the DOM is important for rendering of some subviews (e.g.
-// Graph).
-//
-// **views**: (optional) the dataset views (Grid, Graph etc) for
-// DataExplorer 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!).
-//
-//
-// var views = [
-// {
-// id: 'grid', // used for routing
-// label: 'Grid', // used for view switcher
-// view: new recline.View.Grid({
-// model: dataset
-// })
-// },
-// {
-// id: 'graph',
-// label: 'Graph',
-// view: new recline.View.Graph({
-// model: dataset
-// })
-// }
-// ];
-//
-//
-// **state**: standard state config for this view. This state is slightly
-// special as it includes config of many of the subviews.
-//
-//
-// state = {
-// query: {dataset query state - see dataset.queryState object}
-// view-{id1}: {view-state for this view}
-// view-{id2}: {view-state for }
-// ...
-// // Explorer
-// currentView: id of current view (defaults to first view if not specified)
-// readOnly: (default: false) run in read-only mode
-// }
-//
-//
-// 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({
- template: ' \
- \
- \
- \
- \
- \
- \
- Results found {{docCount}} \
- \
- \
- \
- \
- \
- \
- \
- ',
- events: {
- 'click .menu-right a': '_onMenuClick',
- 'click .navigation a': '_onSwitchView'
- },
-
- initialize: function(options) {
- var self = this;
- this.el = $(this.el);
- this._setupState(options.state);
- // Hash of 'page' views (i.e. those for whole page) keyed by page name
- if (options.views) {
- this.pageViews = options.views;
- } else {
- this.pageViews = [{
- id: 'grid',
- label: 'Grid',
- view: new my.Grid({
- model: this.model,
- state: this.state.get('view-grid')
- }),
- }, {
- id: 'graph',
- label: 'Graph',
- view: new my.Graph({
- model: this.model,
- state: this.state.get('view-graph')
- }),
- }, {
- id: 'map',
- label: 'Map',
- view: new my.Map({
- model: this.model,
- state: this.state.get('view-map')
- }),
- }, {
- id: 'timeline',
- label: 'Timeline',
- view: new my.Timeline({
- model: this.model,
- state: this.state.get('view-timeline')
- }),
- }];
- }
- // these must be called after pageViews are created
- this.render();
- this._bindStateChanges();
- this._bindFlashNotifications();
- // now do updates based on state (need to come after render)
- if (this.state.get('readOnly')) {
- this.setReadOnly();
- }
- if (this.state.get('currentView')) {
- this.updateNav(this.state.get('currentView'));
- } else {
- this.updateNav(this.pageViews[0].id);
- }
-
- this.model.bind('query:start', function() {
- self.notify({loader: true, persist: true});
- });
- this.model.bind('query:done', function() {
- self.clearNotifications();
- self.el.find('.doc-count').text(self.model.docCount || 'Unknown');
- });
- this.model.bind('query:fail', function(error) {
- self.clearNotifications();
- var msg = '';
- if (typeof(error) == 'string') {
- msg = error;
- } else if (typeof(error) == 'object') {
- if (error.title) {
- msg = error.title + ': ';
- }
- if (error.message) {
- msg += error.message;
- }
- } else {
- msg = 'There was an error querying the backend';
- }
- self.notify({message: msg, category: 'error', persist: true});
- });
-
- // retrieve basic data like fields etc
- // note this.model and dataset returned are the same
- this.model.fetch()
- .done(function(dataset) {
- self.model.query(self.state.get('query'));
- })
- .fail(function(error) {
- self.notify({message: error.message, category: 'error', persist: true});
- });
- },
-
- setReadOnly: function() {
- this.el.addClass('recline-read-only');
- },
-
- render: function() {
- var tmplData = this.model.toTemplateJSON();
- tmplData.views = this.pageViews;
- var template = Mustache.render(this.template, tmplData);
- $(this.el).html(template);
- var $dataViewContainer = this.el.find('.data-view-container');
- _.each(this.pageViews, function(view, pageName) {
- $dataViewContainer.append(view.view.el);
- });
- var queryEditor = new my.QueryEditor({
- model: this.model.queryState
- });
- this.el.find('.query-editor-here').append(queryEditor.el);
- var filterEditor = new my.FilterEditor({
- model: this.model.queryState
- });
- this.$filterEditor = filterEditor.el;
- this.el.find('.header').append(filterEditor.el);
- var facetViewer = new my.FacetViewer({
- model: this.model
- });
- this.$facetViewer = facetViewer.el;
- this.el.find('.header').append(facetViewer.el);
- },
-
- 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');
- // show the specific page
- _.each(this.pageViews, function(view, idx) {
- if (view.id === pageName) {
- view.view.el.show();
- view.view.trigger('view:show');
- } else {
- view.view.el.hide();
- view.view.trigger('view:hide');
- }
- });
- },
-
- _onMenuClick: function(e) {
- e.preventDefault();
- var action = $(e.target).attr('data-action');
- if (action === 'filters') {
- this.$filterEditor.show();
- } else if (action === 'facets') {
- this.$facetViewer.show();
- }
- },
-
- _onSwitchView: function(e) {
- e.preventDefault();
- var viewName = $(e.target).attr('data-view');
- this.updateNav(viewName);
- this.state.set({currentView: viewName});
- },
-
- // create a state object for this view and do the job of
- //
- // a) initializing it from both data passed in and other sources (e.g. hash url)
- //
- // b) ensure the state object is updated in responese to changes in subviews, query etc.
- _setupState: function(initialState) {
- var self = this;
- // get data from the query string / hash url plus some defaults
- var qs = recline.Util.parseHashQueryString();
- var query = qs.reclineQuery;
- query = query ? JSON.parse(query) : self.model.queryState.toJSON();
- // backwards compatability (now named view-graph but was named graph)
- var graphState = qs['view-graph'] || qs.graph;
- graphState = graphState ? JSON.parse(graphState) : {};
-
- // now get default data + hash url plus initial state and initial our state object with it
- var stateData = _.extend({
- query: query,
- 'view-graph': graphState,
- backend: this.model.backend.__type__,
- url: this.model.get('url'),
- currentView: null,
- readOnly: false
- },
- initialState);
- this.state = new recline.Model.ObjectState(stateData);
- },
-
- _bindStateChanges: function() {
- var self = this;
- // finally ensure we update our state object when state of sub-object changes so that state is always up to date
- this.model.queryState.bind('change', function() {
- self.state.set({query: self.model.queryState.toJSON()});
- });
- _.each(this.pageViews, function(pageView) {
- if (pageView.view.state && pageView.view.state.bind) {
- var update = {};
- update['view-' + pageView.id] = pageView.view.state.toJSON();
- self.state.set(update);
- pageView.view.state.bind('change', function() {
- var update = {};
- update['view-' + pageView.id] = pageView.view.state.toJSON();
- // had problems where change not being triggered for e.g. grid view so let's do it explicitly
- self.state.set(update, {silent: true});
- self.state.trigger('change');
- });
- }
- });
- },
-
- _bindFlashNotifications: function() {
- var self = this;
- _.each(this.pageViews, function(pageView) {
- pageView.view.bind('recline:flash', function(flash) {
- self.notify(flash);
- });
- });
- },
-
- // ### notify
- //
- // Create a notification (a div.alert in div.alert-messsages) using provided
- // flash object. Flash attributes (all are 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 loading spinner
- notify: function(flash) {
- var tmplData = _.extend({
- message: 'Loading',
- category: 'warning',
- loader: false
- },
- flash
- );
- if (tmplData.loader) {
- var _template = ' \
- \
- {{message}} \
- \
- ';
- } else {
- var _template = ' \
- × \
- {{message}} \
- ';
- }
- var _templated = $(Mustache.render(_template, tmplData));
- _templated = $(_templated).appendTo($('.recline-data-explorer .alert-messages'));
- if (!flash.persist) {
- setTimeout(function() {
- $(_templated).fadeOut(1000, function() {
- $(this).remove();
- });
- }, 1000);
- }
- },
-
- // ### clearNotifications
- //
- // Clear all existing notifications
- clearNotifications: function() {
- var $notifications = $('.recline-data-explorer .alert-messages .alert');
- $notifications.fadeOut(1500, function() {
- $(this).remove();
- });
- }
-});
-
-// ### DataExplorer.restore
-//
-// Restore a DataExplorer instance from a serialized state including the associated dataset
-my.DataExplorer.restore = function(state) {
- var dataset = recline.Model.Dataset.restore(state);
- var explorer = new my.DataExplorer({
- model: dataset,
- state: state
- });
- return explorer;
-}
-
-my.QueryEditor = Backbone.View.extend({
- className: 'recline-query-editor',
- template: ' \
- \
- ',
-
- events: {
- 'submit form': 'onFormSubmit',
- 'click .action-pagination-update': 'onPaginationUpdate'
- },
-
- 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();
- 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 templated = Mustache.render(this.template, tmplData);
- this.el.html(templated);
- }
-});
-
-my.FilterEditor = Backbone.View.extend({
- className: 'recline-filter-editor well',
- template: ' \
- × \
-
+// var myExplorer = new model.recline.MultiView({
+// model: {{recline.Model.Dataset instance}}
+// el: {{an existing dom element}}
+// views: {{dataset views}}
+// state: {{state configuration -- see below}}
+// });
+//
+//
+// ### Parameters
+//
+// **model**: (required) recline.model.Dataset instance.
+//
+// **el**: (required) DOM element to bind to. NB: the element already
+// being in the DOM is important for rendering of some subviews (e.g.
+// Graph).
+//
+// **views**: (optional) the dataset views (Grid, Graph etc) for
+// 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!).
+//
+//
+// var views = [
+// {
+// id: 'grid', // used for routing
+// label: 'Grid', // used for view switcher
+// view: new recline.View.Grid({
+// model: dataset
+// })
+// },
+// {
+// id: 'graph',
+// label: 'Graph',
+// view: new recline.View.Graph({
+// model: dataset
+// })
+// }
+// ];
+//
+//
+// **state**: standard state config for this view. This state is slightly
+// special as it includes config of many of the subviews.
+//
+//
+// state = {
+// query: {dataset query state - see dataset.queryState object}
+// view-{id1}: {view-state for this view}
+// view-{id2}: {view-state for }
+// ...
+// // Explorer
+// currentView: id of current view (defaults to first view if not specified)
+// readOnly: (default: false) run in read-only mode
+// }
+//
+//
+// 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 MultiView with the relevant views themselves.
+my.MultiView = Backbone.View.extend({
+ template: ' \
+