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 += '
// 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: ' \
\
\
\
\
-