diff --git a/dist/recline.dataset.js b/dist/recline.dataset.js index d10c92eea..0b413101e 100644 --- a/dist/recline.dataset.js +++ b/dist/recline.dataset.js @@ -6,7 +6,9 @@ this.recline.Model = this.recline.Model || {}; // ## Dataset my.Dataset = Backbone.Model.extend({ - __type__: 'Dataset', + constructor: function Dataset() { + Backbone.Model.prototype.constructor.apply(this, arguments); + }, // ### initialize initialize: function() { @@ -20,7 +22,7 @@ my.Dataset = Backbone.Model.extend({ } } this.fields = new my.FieldList(); - this.currentRecords = new my.RecordList(); + this.records = new my.RecordList(); this._changes = { deletes: [], updates: [], @@ -170,7 +172,7 @@ my.Dataset = Backbone.Model.extend({ // It will query based on current query state (given by this.queryState) // updated by queryObj (if provided). // - // Resulting RecordList are used to reset this.currentRecords and are + // Resulting RecordList are used to reset this.records and are // also returned. query: function(queryObj) { var self = this; @@ -186,7 +188,7 @@ my.Dataset = Backbone.Model.extend({ .done(function(queryResult) { self._handleQueryResult(queryResult); self.trigger('query:done'); - dfd.resolve(self.currentRecords); + dfd.resolve(self.records); }) .fail(function(arguments) { self.trigger('query:fail', arguments); @@ -208,7 +210,7 @@ my.Dataset = Backbone.Model.extend({ }); return _doc; }); - self.currentRecords.reset(docs); + self.records.reset(docs); if (queryResult.facets) { var facets = _.map(queryResult.facets, function(facetResult, facetId) { facetResult.id = facetId; @@ -331,7 +333,10 @@ my.Dataset.restore = function(state) { // // A single entry or row in the dataset my.Record = Backbone.Model.extend({ - __type__: 'Record', + constructor: function Record() { + Backbone.Model.prototype.constructor.apply(this, arguments); + }, + initialize: function() { _.bindAll(this, 'getFieldValue'); }, @@ -369,14 +374,21 @@ my.Record = Backbone.Model.extend({ destroy: function() { this.trigger('destroy', this); } }); + // ## A Backbone collection of Records my.RecordList = Backbone.Collection.extend({ - __type__: 'RecordList', + constructor: function RecordList() { + Backbone.Collection.prototype.constructor.apply(this, arguments); + }, model: my.Record }); + // ## A Field (aka Column) on a Dataset my.Field = Backbone.Model.extend({ + constructor: function Field() { + Backbone.Model.prototype.constructor.apply(this, arguments); + }, // ### defaults - define default values defaults: { label: null, @@ -445,11 +457,17 @@ my.Field = Backbone.Model.extend({ }); my.FieldList = Backbone.Collection.extend({ + constructor: function FieldList() { + Backbone.Collection.prototype.constructor.apply(this, arguments); + }, model: my.Field }); // ## Query my.Query = Backbone.Model.extend({ + constructor: function Query() { + Backbone.Model.prototype.constructor.apply(this, arguments); + }, defaults: function() { return { size: 100, @@ -534,6 +552,9 @@ my.Query = Backbone.Model.extend({ // ## A Facet (Result) my.Facet = Backbone.Model.extend({ + constructor: function Facet() { + Backbone.Model.prototype.constructor.apply(this, arguments); + }, defaults: function() { return { _type: 'terms', @@ -547,6 +568,9 @@ my.Facet = Backbone.Model.extend({ // ## A Collection/List of Facets my.FacetList = Backbone.Collection.extend({ + constructor: function FacetList() { + Backbone.Collection.prototype.constructor.apply(this, arguments); + }, model: my.Facet }); diff --git a/dist/recline.js b/dist/recline.js index 1da6ea5bd..a8587e47c 100644 --- a/dist/recline.js +++ b/dist/recline.js @@ -890,7 +890,9 @@ this.recline.Model = this.recline.Model || {}; // ## Dataset my.Dataset = Backbone.Model.extend({ - __type__: 'Dataset', + constructor: function Dataset() { + Backbone.Model.prototype.constructor.apply(this, arguments); + }, // ### initialize initialize: function() { @@ -904,7 +906,7 @@ my.Dataset = Backbone.Model.extend({ } } this.fields = new my.FieldList(); - this.currentRecords = new my.RecordList(); + this.records = new my.RecordList(); this._changes = { deletes: [], updates: [], @@ -1054,7 +1056,7 @@ my.Dataset = Backbone.Model.extend({ // It will query based on current query state (given by this.queryState) // updated by queryObj (if provided). // - // Resulting RecordList are used to reset this.currentRecords and are + // Resulting RecordList are used to reset this.records and are // also returned. query: function(queryObj) { var self = this; @@ -1070,7 +1072,7 @@ my.Dataset = Backbone.Model.extend({ .done(function(queryResult) { self._handleQueryResult(queryResult); self.trigger('query:done'); - dfd.resolve(self.currentRecords); + dfd.resolve(self.records); }) .fail(function(arguments) { self.trigger('query:fail', arguments); @@ -1092,7 +1094,7 @@ my.Dataset = Backbone.Model.extend({ }); return _doc; }); - self.currentRecords.reset(docs); + self.records.reset(docs); if (queryResult.facets) { var facets = _.map(queryResult.facets, function(facetResult, facetId) { facetResult.id = facetId; @@ -1215,7 +1217,10 @@ my.Dataset.restore = function(state) { // // A single entry or row in the dataset my.Record = Backbone.Model.extend({ - __type__: 'Record', + constructor: function Record() { + Backbone.Model.prototype.constructor.apply(this, arguments); + }, + initialize: function() { _.bindAll(this, 'getFieldValue'); }, @@ -1253,14 +1258,21 @@ my.Record = Backbone.Model.extend({ destroy: function() { this.trigger('destroy', this); } }); + // ## A Backbone collection of Records my.RecordList = Backbone.Collection.extend({ - __type__: 'RecordList', + constructor: function RecordList() { + Backbone.Collection.prototype.constructor.apply(this, arguments); + }, model: my.Record }); + // ## A Field (aka Column) on a Dataset my.Field = Backbone.Model.extend({ + constructor: function Field() { + Backbone.Model.prototype.constructor.apply(this, arguments); + }, // ### defaults - define default values defaults: { label: null, @@ -1329,11 +1341,17 @@ my.Field = Backbone.Model.extend({ }); my.FieldList = Backbone.Collection.extend({ + constructor: function FieldList() { + Backbone.Collection.prototype.constructor.apply(this, arguments); + }, model: my.Field }); // ## Query my.Query = Backbone.Model.extend({ + constructor: function Query() { + Backbone.Model.prototype.constructor.apply(this, arguments); + }, defaults: function() { return { size: 100, @@ -1418,6 +1436,9 @@ my.Query = Backbone.Model.extend({ // ## A Facet (Result) my.Facet = Backbone.Model.extend({ + constructor: function Facet() { + Backbone.Model.prototype.constructor.apply(this, arguments); + }, defaults: function() { return { _type: 'terms', @@ -1431,6 +1452,9 @@ my.Facet = Backbone.Model.extend({ // ## A Collection/List of Facets my.FacetList = Backbone.Collection.extend({ + constructor: function FacetList() { + Backbone.Collection.prototype.constructor.apply(this, arguments); + }, model: my.Facet }); @@ -1495,8 +1519,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.currentRecords.bind('add', this.redraw); - this.model.currentRecords.bind('reset', this.redraw); + this.model.records.bind('add', this.redraw); + this.model.records.bind('reset', this.redraw); // because we cannot redraw when hidden we may need when becoming visible this.bind('view:show', function() { if (this.needToRedraw) { @@ -1541,7 +1565,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.currentRecords.length === 0)) { + if ((!areWeVisible || this.model.records.length === 0)) { this.needToRedraw = true; return; } @@ -1571,8 +1595,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.currentRecords.models[val]) { - var out = self.model.currentRecords.models[val].get(self.state.attributes.group); + if (self.model.records.models[val]) { + var out = self.model.records.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; @@ -1628,7 +1652,7 @@ my.Graph = Backbone.View.extend({ tickLength: 1, tickFormatter: tickFormatter, min: -0.5, - max: self.model.currentRecords.length - 0.5 + max: self.model.records.length - 0.5 } } }; @@ -1666,8 +1690,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.currentRecords.models[x]) { - x = self.model.currentRecords.models[x].get(self.state.attributes.group); + if (self.model.records.models[x]) { + x = self.model.records.models[x].get(self.state.attributes.group); } else { x = x.toFixed(2); } @@ -1701,7 +1725,7 @@ my.Graph = Backbone.View.extend({ var series = []; _.each(this.state.attributes.series, function(field) { var points = []; - _.each(self.model.currentRecords.models, function(doc, index) { + _.each(self.model.records.models, function(doc, index) { var xfield = self.model.fields.get(self.state.attributes.group); var x = doc.getFieldValue(xfield); // time series @@ -1904,9 +1928,9 @@ my.Grid = Backbone.View.extend({ var self = this; this.el = $(this.el); _.bindAll(this, 'render', 'onHorizontalScroll'); - this.model.currentRecords.bind('add', this.render); - this.model.currentRecords.bind('reset', this.render); - this.model.currentRecords.bind('remove', this.render); + this.model.records.bind('add', this.render); + this.model.records.bind('reset', this.render); + this.model.records.bind('remove', this.render); this.tempState = {}; var state = _.extend({ hiddenFields: [] @@ -1964,13 +1988,13 @@ my.Grid = Backbone.View.extend({ showColumn: function() { self.showColumn(e); }, deleteRow: function() { var self = this; - var doc = _.find(self.model.currentRecords.models, function(doc) { + var doc = _.find(self.model.records.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.currentRecords.remove(doc); + self.model.records.remove(doc); self.trigger('recline:flash', {message: "Row deleted successfully"}); }).fail(function(err) { self.trigger('recline:flash', {message: "Errorz! " + err}); @@ -2100,7 +2124,7 @@ my.Grid = Backbone.View.extend({ }); var htmls = Mustache.render(this.template, this.toTemplateJSON()); this.el.html(htmls); - this.model.currentRecords.forEach(function(doc) { + this.model.records.forEach(function(doc) { var tr = $(''); self.el.find('tbody').append(tr); var newView = new my.GridRow({ @@ -2306,13 +2330,13 @@ my.Map = Backbone.View.extend({ }); // Listen to changes in the records - this.model.currentRecords.bind('add', function(doc){self.redraw('add',doc)}); - this.model.currentRecords.bind('change', function(doc){ + this.model.records.bind('add', function(doc){self.redraw('add',doc)}); + this.model.records.bind('change', function(doc){ self.redraw('remove',doc); self.redraw('add',doc); }); - this.model.currentRecords.bind('remove', function(doc){self.redraw('remove',doc)}); - this.model.currentRecords.bind('reset', function(){self.redraw('reset')}); + this.model.records.bind('remove', function(doc){self.redraw('remove',doc)}); + this.model.records.bind('reset', function(){self.redraw('reset')}); this.bind('view:show',function(){ // If the div was hidden, Leaflet needs to recalculate some sizes @@ -2388,7 +2412,7 @@ my.Map = Backbone.View.extend({ if (this._geomReady() && this.mapReady){ if (action == 'reset' || action == 'refresh'){ this.features.clearLayers(); - this._add(this.model.currentRecords.models); + this._add(this.model.records.models); } else if (action == 'add' && doc){ this._add(doc); } else if (action == 'remove' && doc){ @@ -3314,9 +3338,9 @@ my.SlickGrid = Backbone.View.extend({ this.el = $(this.el); this.el.addClass('recline-slickgrid'); _.bindAll(this, 'render'); - this.model.currentRecords.bind('add', this.render); - this.model.currentRecords.bind('reset', this.render); - this.model.currentRecords.bind('remove', this.render); + this.model.records.bind('add', this.render); + this.model.records.bind('reset', this.render); + this.model.records.bind('remove', this.render); var state = _.extend({ hiddenColumns: [], @@ -3418,7 +3442,7 @@ my.SlickGrid = Backbone.View.extend({ var data = []; - this.model.currentRecords.each(function(doc){ + this.model.records.each(function(doc){ var row = {}; self.model.fields.each(function(field){ row[field.id] = doc.getFieldValueUnrendered(field); @@ -3636,7 +3660,7 @@ my.Timeline = Backbone.View.extend({ this.model.fields.bind('reset', function() { self._setupTemporalField(); }); - this.model.currentRecords.bind('all', function() { + this.model.records.bind('all', function() { self.reloadData(); }); var stateData = _.extend({ @@ -3715,7 +3739,7 @@ my.Timeline = Backbone.View.extend({ ] } }; - this.model.currentRecords.each(function(record) { + this.model.records.each(function(record) { var newEntry = self.convertRecord(record, self.fields); if (newEntry) { out.timeline.date.push(newEntry); @@ -3874,7 +3898,7 @@ my.Transform = Backbone.View.extend({ var editFunc = costco.evalFunction(e.target.value); if (!editFunc.errorMessage) { errors.text('No syntax error.'); - var docs = self.model.currentRecords.map(function(doc) { + var docs = self.model.records.map(function(doc) { return doc.toJSON(); }); var previewData = costco.previewTransform(docs, editFunc); diff --git a/docs/src/backend.dataproxy.html b/docs/src/backend.dataproxy.html index 97d3d9bfd..79a83906c 100644 --- a/docs/src/backend.dataproxy.html +++ b/docs/src/backend.dataproxy.html @@ -3,7 +3,8 @@ this.recline.Backend.DataProxy = this.recline.Backend.DataProxy || {}; (function($, my) { - my.__type__ = 'dataproxy';

URL for the dataproxy

  my.dataproxy_url = 'http://jsonpdataproxy.appspot.com';

load

+ my.__type__ = 'dataproxy';

URL for the dataproxy

  my.dataproxy_url = 'http://jsonpdataproxy.appspot.com';

Timeout for dataproxy (after this time if no response we error) +Needed because use JSONP so do not receive e.g. 500 errors

  my.timeout = 5000;

load

Load data from a URL via the DataProxy.

@@ -34,18 +35,17 @@ dfd.reject(arguments); }); return dfd.promise(); - };

_wrapInTimeout

+ };

_wrapInTimeout

Convenience method providing a crude way to catch backend errors on JSONP calls. Many of backends use JSONP and so will not get error messages and this is a crude way to catch those errors.

  var _wrapInTimeout = function(ourFunction) {
     var dfd = $.Deferred();
-    var timeout = 5000;
     var timer = setTimeout(function() {
       dfd.reject({
-        message: 'Request Error: Backend did not respond after ' + (timeout / 1000) + ' seconds'
+        message: 'Request Error: Backend did not respond after ' + (my.timeout / 1000) + ' seconds'
       });
-    }, timeout);
+    }, my.timeout);
     ourFunction.done(function(arguments) {
         clearTimeout(timer);
         dfd.resolve(arguments);
diff --git a/docs/src/backend.memory.html b/docs/src/backend.memory.html
index f27904859..8f515f200 100644
--- a/docs/src/backend.memory.html
+++ b/docs/src/backend.memory.html
@@ -94,9 +94,12 @@
             var foundmatch = false;
             _.each(self.fields, function(field) {
               var value = rawdoc[field.id];
-              if (value !== null) { value = value.toString(); }

TODO regexes?

              foundmatch = foundmatch || (value.toLowerCase() === term.toLowerCase());

TODO: early out (once we are true should break to spare unnecessary testing) + if (value !== null) { + value = value.toString(); + } else {

value can be null (apparently in some cases)

                value = '';
+              }

TODO regexes?

              foundmatch = foundmatch || (value.toLowerCase() === term.toLowerCase());

TODO: early out (once we are true should break to spare unnecessary testing) if (foundmatch) return true;

            });
-            matches = matches && foundmatch;

TODO: early out (once false should break to spare unnecessary testing) + matches = matches && foundmatch;

TODO: early out (once false should break to spare unnecessary testing) if (!matches) return false;

          });
           return matches;
         });
@@ -109,9 +112,9 @@
       if (!queryObj.facets) {
         return facetResults;
       }
-      _.each(queryObj.facets, function(query, facetId) {

TODO: remove dependency on recline.Model

        facetResults[facetId] = new recline.Model.Facet({id: facetId}).toJSON();
+      _.each(queryObj.facets, function(query, facetId) {

TODO: remove dependency on recline.Model

        facetResults[facetId] = new recline.Model.Facet({id: facetId}).toJSON();
         facetResults[facetId].termsall = {};
-      });

faceting

      _.each(records, function(doc) {
+      });

faceting

      _.each(records, function(doc) {
         _.each(queryObj.facets, function(query, facetId) {
           var fieldId = query.terms.field;
           var val = doc[fieldId];
@@ -128,12 +131,19 @@
         var terms = _.map(tmp.termsall, function(count, term) {
           return { term: term, count: count };
         });
-        tmp.terms = _.sortBy(terms, function(item) {

want descending order

          return -item.count;
+        tmp.terms = _.sortBy(terms, function(item) {

want descending order

          return -item.count;
         });
         tmp.terms = tmp.terms.slice(0, 10);
       });
       return facetResults;
     };
+
+    this.transform = function(editFunc) {
+      var toUpdate = costco.mapDocs(this.data, editFunc);

TODO: very inefficient -- could probably just walk the documents and updates in tandem and update

      _.each(toUpdate.updates, function(record, idx) {
+        self.data[idx] = record;
+      });
+      return this.save(toUpdate);
+    };
   };
 
 }(jQuery, this.recline.Backend.Memory));
diff --git a/docs/src/costco.html b/docs/src/costco.html
new file mode 100644
index 000000000..35fbc7883
--- /dev/null
+++ b/docs/src/costco.html
@@ -0,0 +1,68 @@
+      costco.js           

costco.js

adapted from https://github.com/harthur/costco. heather rules

var costco = function() {
+  
+  function evalFunction(funcString) {
+    try {
+      eval("var editFunc = " + funcString);
+    } catch(e) {
+      return {errorMessage: e+""};
+    }
+    return editFunc;
+  }
+  
+  function previewTransform(docs, editFunc, currentColumn) {
+    var preview = [];
+    var updated = mapDocs($.extend(true, {}, docs), editFunc);
+    for (var i = 0; i < updated.docs.length; i++) {      
+      var before = docs[i]
+        , after = updated.docs[i]
+        ;
+      if (!after) after = {};
+      if (currentColumn) {
+        preview.push({before: before[currentColumn], after: after[currentColumn]});      
+      } else {
+        preview.push({before: before, after: after});      
+      }
+    }
+    return preview;
+  }
+
+  function mapDocs(docs, editFunc) {
+    var edited = []
+      , deleted = []
+      , failed = []
+      ;
+    
+    var updatedDocs = _.map(docs, function(doc) {
+      try {
+        var updated = editFunc(_.clone(doc));
+      } catch(e) {
+        failed.push(doc);
+        return;
+      }
+      if(updated === null) {
+        updated = {_deleted: true};
+        edited.push(updated);
+        deleted.push(doc);
+      }
+      else if(updated && !_.isEqual(updated, doc)) {
+        edited.push(updated);
+      }
+      return updated;      
+    });
+    
+    return {
+      updates: edited, 
+      docs: updatedDocs, 
+      deletes: deleted, 
+      failed: failed
+    };
+  }
+  
+  return {
+    evalFunction: evalFunction,
+    previewTransform: previewTransform,
+    mapDocs: mapDocs
+  };
+}();
+
+
\ No newline at end of file diff --git a/docs/src/model.html b/docs/src/model.html index b7ae3e0ce..b73e36e92 100644 --- a/docs/src/model.html +++ b/docs/src/model.html @@ -2,7 +2,9 @@ this.recline.Model = this.recline.Model || {}; (function($, my) {

Dataset

my.Dataset = Backbone.Model.extend({
-  __type__: 'Dataset',

initialize

  initialize: function() {
+  constructor: function Dataset() {
+    Backbone.Model.prototype.constructor.apply(this, arguments);
+  },

initialize

  initialize: function() {
     _.bindAll(this, 'query');
     this.backend = null;
     if (this.get('backend')) {
@@ -13,14 +15,14 @@
       }
     }
     this.fields = new my.FieldList();
-    this.currentRecords = new my.RecordList();
+    this.records = new my.RecordList();
     this._changes = {
       deletes: [],
       updates: [],
       creates: []
     };
     this.facets = new my.FacetList();
-    this.docCount = null;
+    this.recordCount = null;
     this.queryState = new my.Query();
     this.queryState.bind('change', this.query);
     this.queryState.bind('facet:add', this.query);

store is what we query and save against @@ -113,21 +115,33 @@ save: function() { var self = this;

TODO: need to reset the changes ...

    return this._store.save(this._changes, this.toJSON());
-  },

query

+ }, + + transform: function(editFunc) { + var self = this; + if (!this._store.transform) { + alert('Transform is not supported with this backend: ' + this.get('backend')); + return; + } + this.trigger('recline:flash', {message: "Updating all visible docs. This could take a while...", persist: true, loader: true}); + this._store.transform(editFunc).done(function() {

reload data as records have changed

      self.query();
+      self.trigger('recline:flash', {message: "Records updated successfully"});
+    });
+  },

query

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 RecordList are used to reset this.currentRecords and are +

Resulting RecordList are used to reset this.records and are also returned.

  query: function(queryObj) {
     var self = this;
     var dfd = $.Deferred();
     this.trigger('query:start');
 
     if (queryObj) {
-      this.queryState.set(queryObj);
+      this.queryState.set(queryObj, {silent: true});
     }
     var actualQuery = this.queryState.toJSON();
 
@@ -135,7 +149,7 @@
       .done(function(queryResult) {
         self._handleQueryResult(queryResult);
         self.trigger('query:done');
-        dfd.resolve(self.currentRecords);
+        dfd.resolve(self.records);
       })
       .fail(function(arguments) {
         self.trigger('query:fail', arguments);
@@ -146,7 +160,7 @@
 
   _handleQueryResult: function(queryResult) {
     var self = this;
-    self.docCount = queryResult.total;
+    self.recordCount = queryResult.total;
     var docs = _.map(queryResult.hits, function(hit) {
       var _doc = new my.Record(hit);
       _doc.bind('change', function(doc) {
@@ -157,7 +171,7 @@
       });
       return _doc;
     });
-    self.currentRecords.reset(docs);
+    self.records.reset(docs);
     if (queryResult.facets) {
       var facets = _.map(queryResult.facets, function(facetResult, facetId) {
         facetResult.id = facetId;
@@ -169,10 +183,12 @@
 
   toTemplateJSON: function() {
     var data = this.toJSON();
-    data.docCount = this.docCount;
+    data.recordCount = this.recordCount;
     data.fields = this.fields.toJSON();
     return data;
-  },

Get a summary for each field in the form of a Facet.

+ },

getFieldsSummary

+ +

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;
@@ -186,16 +202,27 @@
       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);
+          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)

+ },

recordSummary

+ +

Get a simple html summary of a Dataset record in form of key/value list

  recordSummary: function(record) {
+    var html = '<div class="recline-record-summary">';
+    this.fields.each(function(field) { 
+      if (field.id != 'id') {
+        html += '<div class="' + field.id + '"><strong>' + field.get('label') + '</strong>: ' + record.getFieldValue(field) + '</div>';
+      }
+    });
+    html += '</div>';
+    return html;
+  },

_backendFromString(backendString)

See backend argument to initialize for details

  _backendFromString: function(backendString) {
-    var parts = backendString.split('.');

walk through the specified path xxx.yyy.zzz to get the final object which should be backend class

    var current = window;
+    var parts = backendString.split('.');

walk through the specified path xxx.yyy.zzz to get the final object which should be backend class

    var current = window;
     for(ii=0;ii<parts.length;ii++) {
       if (!current) {
         break;
@@ -204,7 +231,7 @@
     }
     if (current) {
       return current;
-    }

alternatively we just had a simple string

    var backend = null;
+    }

alternatively we just had a simple string

    var backend = null;
     if (recline && recline.Backend) {
       _.each(_.keys(recline.Backend), function(name) {
         if (name.toLowerCase() === backendString.toLowerCase()) {
@@ -214,7 +241,7 @@
     }
     return backend;
   }
-});

Dataset.restore

+});

Dataset.restore

Restore a Dataset instance from a serialized state. Serialized state for a Dataset is an Object like:

@@ -227,7 +254,7 @@ url: {dataset url} ... }

my.Dataset.restore = function(state) {
-  var dataset = null;

hack-y - restoring a memory dataset does not mean much ...

  if (state.backend === 'memory') {
+  var dataset = null;

hack-y - restoring a memory dataset does not mean much ...

  if (state.backend === 'memory') {
     var datasetInfo = {
       records: [{stub: 'this is a stub dataset because we do not restore memory datasets'}]
     };
@@ -239,13 +266,16 @@
   }
   dataset = new recline.Model.Dataset(datasetInfo);
   return dataset;
-};

A Record (aka Row)

+};

A Record (aka Row)

A single entry or row in the dataset

my.Record = Backbone.Model.extend({
-  __type__: 'Record',
+  constructor: function Record() {
+    Backbone.Model.prototype.constructor.apply(this, arguments);
+  },
+
   initialize: function() {
     _.bindAll(this, 'getFieldValue');
-  },

getFieldValue

+ },

getFieldValue

For the provided Field get the corresponding rendered computed data value for this record.

  getFieldValue: function(field) {
@@ -254,7 +284,7 @@
       val = field.renderer(val, field, this.toJSON());
     }
     return val;
-  },

getFieldValueUnrendered

+ },

getFieldValueUnrendered

For the provided Field get the corresponding computed data value for this record.

  getFieldValueUnrendered: function(field) {
@@ -263,35 +293,30 @@
       val = field.deriver(val, field, this);
     }
     return val;
-  },
-
-  summary: function(fields) {
-    var html = '';
-    for (key in this.attributes) {
-      if (key != 'id') {
-        html += '<div><strong>' + key + '</strong>: '+ this.attributes[key] + '</div>';
-      }
-    }
-    return html;
-  },

Override Backbone save, fetch and destroy so they do nothing + },

Override Backbone save, fetch and destroy so they do nothing Instead, Dataset object that created this Record should take care of handling these changes (discovery will occur via event notifications) WARNING: these will not persist unless you call save on Dataset

  fetch: function() {},
   save: function() {},
   destroy: function() { this.trigger('destroy', this); }
-});

A Backbone collection of Records

my.RecordList = Backbone.Collection.extend({
-  __type__: 'RecordList',
+});

A Backbone collection of Records

my.RecordList = Backbone.Collection.extend({
+  constructor: function RecordList() {
+    Backbone.Collection.prototype.constructor.apply(this, arguments);
+  },
   model: my.Record
-});

A Field (aka Column) on a Dataset

my.Field = Backbone.Model.extend({

defaults - define default values

  defaults: {
+});

A Field (aka Column) on a Dataset

my.Field = Backbone.Model.extend({
+  constructor: function Field() {
+    Backbone.Model.prototype.constructor.apply(this, arguments);
+  },

defaults - define default values

  defaults: {
     label: null,
     type: 'string',
     format: null,
     is_derived: false
-  },

initialize

+ },

initialize

@param {Object} data: standard Backbone model attributes

-

@param {Object} options: renderer and/or deriver functions.

  initialize: function(data, options) {

if a hash not passed in the first argument throw error

    if ('0' in data) {
+

@param {Object} options: renderer and/or deriver functions.

  initialize: function(data, options) {

if a hash not passed in the first argument throw error

    if ('0' in data) {
       throw new Error('Looks like you did not pass a proper hash with id to Field constructor');
     }
     if (this.attributes.label === null) {
@@ -310,6 +335,9 @@
     object: function(val, field, doc) {
       return JSON.stringify(val);
     },
+    geo_point: function(val, field, doc) {
+      return JSON.stringify(val);
+    },
     'float': function(val, field, doc) {
       var format = field.get('format'); 
       if (format === 'percentage') {
@@ -329,7 +357,7 @@
         }
       } else if (format == 'plain') {
         return val;
-      } else {

as this is the default and default type is string may get things + } else {

as this is the default and default type is string may get things here that are not actually strings

        if (val && typeof val === 'string') {
           val = val.replace(/(https?:\/\/[^ ]+)/g, '<a href="$1">$1</a>');
         }
@@ -340,8 +368,14 @@
 });
 
 my.FieldList = Backbone.Collection.extend({
+  constructor: function FieldList() {
+    Backbone.Collection.prototype.constructor.apply(this, arguments);
+  },
   model: my.Field
-});

Query

my.Query = Backbone.Model.extend({
+});

Query

my.Query = Backbone.Model.extend({
+  constructor: function Query() {
+    Backbone.Model.prototype.constructor.apply(this, arguments);
+  },
   defaults: function() {
     return {
       size: 100,
@@ -365,11 +399,11 @@
         lat: 0
       }
     }
-  },  

addFilter

+ },

addFilter

Add a new filter (appended to the list of filters)

-

@param filter an object specifying the filter - see _filterTemplates for examples. If only type is provided will generate a filter by cloning _filterTemplates

  addFilter: function(filter) {

crude deep copy

    var ourfilter = JSON.parse(JSON.stringify(filter));

not full specified so use template and over-write

    if (_.keys(filter).length <= 2) {
+

@param filter an object specifying the filter - see _filterTemplates for examples. If only type is provided will generate a filter by cloning _filterTemplates

  addFilter: function(filter) {

crude deep copy

    var ourfilter = JSON.parse(JSON.stringify(filter));

not full specified so use template and over-write

    if (_.keys(filter).length <= 2) {
       ourfilter = _.extend(this._filterTemplates[filter.type], ourfilter);
     }
     var filters = this.get('filters');
@@ -377,19 +411,19 @@
     this.trigger('change:filters:new-blank');
   },
   updateFilter: function(index, value) {
-  },

removeFilter

+ },

removeFilter

Remove a filter from filters at index filterIndex

  removeFilter: function(filterIndex) {
     var filters = this.get('filters');
     filters.splice(filterIndex, 1);
     this.set({filters: filters});
     this.trigger('change');
-  },

addFacet

+ },

addFacet

Add a Facet to this query

See http://www.elasticsearch.org/guide/reference/api/search/facets/

  addFacet: function(fieldId) {
-    var facets = this.get('facets');

Assume id and fieldId should be the same (TODO: this need not be true if we want to add two different type of facets on same field)

    if (_.contains(_.keys(facets), fieldId)) {
+    var facets = this.get('facets');

Assume id and fieldId should be the same (TODO: this need not be true if we want to add two different type of facets on same field)

    if (_.contains(_.keys(facets), fieldId)) {
       return;
     }
     facets[fieldId] = {
@@ -409,7 +443,10 @@
     this.set({facets: facets}, {silent: true});
     this.trigger('facet:add', this);
   }
-});

A Facet (Result)

my.Facet = Backbone.Model.extend({
+});

A Facet (Result)

my.Facet = Backbone.Model.extend({
+  constructor: function Facet() {
+    Backbone.Model.prototype.constructor.apply(this, arguments);
+  },
   defaults: function() {
     return {
       _type: 'terms',
@@ -419,12 +456,15 @@
       terms: []
     };
   }
-});

A Collection/List of Facets

my.FacetList = Backbone.Collection.extend({
+});

A Collection/List of Facets

my.FacetList = Backbone.Collection.extend({
+  constructor: function FacetList() {
+    Backbone.Collection.prototype.constructor.apply(this, arguments);
+  },
   model: my.Facet
-});

Object State

+});

Object State

Convenience Backbone model for storing (configuration) state of objects like Views.

my.ObjectState = Backbone.Model.extend({
-});

Backbone.sync

+});

Backbone.sync

Override Backbone.sync to hand off to sync function in relevant backend

Backbone.sync = function(method, model, options) {
   return model.backend.sync(method, model, options);
diff --git a/docs/src/view.graph.html b/docs/src/view.graph.html
index 29d119004..ebbebf8e5 100644
--- a/docs/src/view.graph.html
+++ b/docs/src/view.graph.html
@@ -42,8 +42,8 @@
     this.model.bind('change', this.render);
     this.model.fields.bind('reset', this.render);
     this.model.fields.bind('add', this.render);
-    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() {
+    this.model.records.bind('add', this.redraw);
+    this.model.records.bind('reset', this.redraw);

because we cannot redraw when hidden we may need when becoming visible

    this.bind('view:show', function() {
       if (this.needToRedraw) {
         self.redraw();
       }
@@ -82,7 +82,7 @@
 

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.currentRecords.length === 0)) {
    +    if ((!areWeVisible || this.model.records.length === 0)) {
           this.needToRedraw = true;
           return;
         }

    check we have something to plot

        if (this.state.get('group') && this.state.get('series')) {

    faff around with width because flot draws axes outside of the element width which means graph can get push down as it hits element next to it

          this.$graph.width(this.el.width() - 20);
    @@ -103,8 +103,8 @@
     only if) x-axis values are non-numeric
     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.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') {
    +      if (self.model.records.models[val]) {
    +        var out = self.model.records.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;
             } else {
               return out;
    @@ -156,7 +156,7 @@
               tickLength: 1,
               tickFormatter: tickFormatter,
               min: -0.5,
    -          max: self.model.currentRecords.length - 0.5
    +          max: self.model.records.length - 0.5
             }
           }
         };
    @@ -190,8 +190,8 @@
                 var _tmp = x;
                 x = y;
                 y = _tmp;
    -          }

    convert back from 'index' value on x-axis (e.g. in cases where non-number values)

              if (self.model.currentRecords.models[x]) {
    -            x = self.model.currentRecords.models[x].get(self.state.attributes.group);
    +          }

    convert back from 'index' value on x-axis (e.g. in cases where non-number values)

              if (self.model.records.models[x]) {
    +            x = self.model.records.models[x].get(self.state.attributes.group);
               } else {
                 x = x.toFixed(2);
               }
    @@ -222,7 +222,7 @@
         var series = [];
         _.each(this.state.attributes.series, function(field) {
           var points = [];
    -      _.each(self.model.currentRecords.models, function(doc, index) {
    +      _.each(self.model.records.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) {
    diff --git a/docs/src/view.grid.html b/docs/src/view.grid.html
    index 1720f5ae3..4d2c0d23a 100644
    --- a/docs/src/view.grid.html
    +++ b/docs/src/view.grid.html
    @@ -15,9 +15,9 @@
         var self = this;
         this.el = $(this.el);
         _.bindAll(this, 'render', 'onHorizontalScroll');
    -    this.model.currentRecords.bind('add', this.render);
    -    this.model.currentRecords.bind('reset', this.render);
    -    this.model.currentRecords.bind('remove', this.render);
    +    this.model.records.bind('add', this.render);
    +    this.model.records.bind('reset', this.render);
    +    this.model.records.bind('remove', this.render);
         this.tempState = {};
         var state = _.extend({
             hiddenFields: []
    @@ -69,11 +69,11 @@
           showColumn: function() { self.showColumn(e); },
           deleteRow: function() {
             var self = this;
    -        var doc = _.find(self.model.currentRecords.models, function(doc) {

    important this is == as the currentRow will be string (as comes + var doc = _.find(self.model.records.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.currentRecords.remove(doc);
    +            self.model.records.remove(doc);
                 self.trigger('recline:flash', {message: "Row deleted successfully"});
               }).fail(function(err) {
                 self.trigger('recline:flash', {message: "Errorz! " + err});
    @@ -187,7 +187,7 @@ 

    Templating

    }); var htmls = Mustache.render(this.template, this.toTemplateJSON()); this.el.html(htmls); - this.model.currentRecords.forEach(function(doc) { + this.model.records.forEach(function(doc) { var tr = $('<tr />'); self.el.find('tbody').append(tr); var newView = new my.GridRow({ diff --git a/docs/src/view.map.html b/docs/src/view.map.html index 7c5cf6074..817f29a95 100644 --- a/docs/src/view.map.html +++ b/docs/src/view.map.html @@ -29,20 +29,20 @@ ',

    These are the default (case-insensitive) names of field that are used if found. If not found, the user will need to define the fields via the editor.

      latitudeFieldNames: ['lat','latitude'],
       longitudeFieldNames: ['lon','longitude'],
    -  geometryFieldNames: ['geojson', 'geom','the_geom','geometry','spatial','location'],
    +  geometryFieldNames: ['geojson', 'geom','the_geom','geometry','spatial','location', 'geo', 'lonlat'],
     
       initialize: function(options) {
         var self = this;
         this.el = $(this.el);

    Listen to changes in the fields

        this.model.fields.bind('change', function() {
           self._setupGeometryField()
           self.render()
    -    });

    Listen to changes in the records

        this.model.currentRecords.bind('add', function(doc){self.redraw('add',doc)});
    -    this.model.currentRecords.bind('change', function(doc){
    +    });

    Listen to changes in the records

        this.model.records.bind('add', function(doc){self.redraw('add',doc)});
    +    this.model.records.bind('change', function(doc){
             self.redraw('remove',doc);
             self.redraw('add',doc);
         });
    -    this.model.currentRecords.bind('remove', function(doc){self.redraw('remove',doc)});
    -    this.model.currentRecords.bind('reset', function(){self.redraw('reset')});
    +    this.model.records.bind('remove', function(doc){self.redraw('remove',doc)});
    +    this.model.records.bind('reset', function(){self.redraw('reset')});
     
         this.bind('view:show',function(){

    If the div was hidden, Leaflet needs to recalculate some sizes to display properly

          if (self.map){
    @@ -110,7 +110,7 @@
         if (this._geomReady() && this.mapReady){
           if (action == 'reset' || action == 'refresh'){
             this.features.clearLayers();
    -        this._add(this.model.currentRecords.models);
    +        this._add(this.model.records.models);
           } else if (action == 'add' && doc){
             this._add(doc);
           } else if (action == 'remove' && doc){
    diff --git a/docs/src/view.multiview.html b/docs/src/view.multiview.html
    index e9fdc237f..648b2baac 100644
    --- a/docs/src/view.multiview.html
    +++ b/docs/src/view.multiview.html
    @@ -78,7 +78,7 @@ 

    Parameters

    </div> \ </div> \ <div class="recline-results-info"> \ - Results found <span class="doc-count">{{docCount}}</span> \ + <span class="doc-count">{{recordCount}}</span> records\ </div> \ <div class="menu-right"> \ <div class="btn-group" data-toggle="buttons-checkbox"> \ @@ -107,7 +107,7 @@

    Parameters

    this.pageViews = [{ id: 'grid', label: 'Grid', - view: new my.Grid({ + view: new my.SlickGrid({ model: this.model, state: this.state.get('view-grid') }), @@ -132,6 +132,12 @@

    Parameters

    model: this.model, state: this.state.get('view-timeline') }), + }, { + id: 'transform', + label: 'Transform', + view: new my.Transform({ + model: this.model + }) }]; }

    these must be called after pageViews are created

        this.render();
         this._bindStateChanges();
    @@ -149,7 +155,7 @@ 

    Parameters

    }); this.model.bind('query:done', function() { self.clearNotifications(); - self.el.find('.doc-count').text(self.model.docCount || 'Unknown'); + self.el.find('.doc-count').text(self.model.recordCount || 'Unknown'); }); this.model.bind('query:fail', function(error) { self.clearNotifications(); @@ -242,6 +248,8 @@

    Parameters

    this.$filterEditor.toggle(); } else if (action === 'fields') { this.$fieldsView.toggle(); + } else if (action === 'transform') { + this.transformView.el.toggle(); } }, diff --git a/docs/src/view.slickgrid.html b/docs/src/view.slickgrid.html new file mode 100644 index 000000000..960f8dd7e --- /dev/null +++ b/docs/src/view.slickgrid.html @@ -0,0 +1,279 @@ + view.slickgrid.js

    view.slickgrid.js

    /*jshint multistr:true */
    +
    +this.recline = this.recline || {};
    +this.recline.View = this.recline.View || {};
    +
    +(function($, my) {

    SlickGrid Dataset View

    + +

    Provides a tabular view on a Dataset, based on SlickGrid.

    + +

    https://github.com/mleibman/SlickGrid

    + +

    Initialize it with a recline.Model.Dataset.

    + +

    NB: you need an explicit height on the element for slickgrid to work

    my.SlickGrid = Backbone.View.extend({
    +  tagName:  "div",
    +  className: "recline-slickgrid",
    +
    +  initialize: function(modelEtc) {
    +    var self = this;
    +    this.el = $(this.el);
    +    this.el.addClass('recline-slickgrid');
    +    _.bindAll(this, 'render');
    +    this.model.records.bind('add', this.render);
    +    this.model.records.bind('reset', this.render);
    +    this.model.records.bind('remove', this.render);
    +
    +    var state = _.extend({
    +        hiddenColumns: [],
    +        columnsOrder: [],
    +        columnsSort: {},
    +        columnsWidth: [],
    +        fitColumns: false
    +      }, modelEtc.state
    +    );
    +    this.state = new recline.Model.ObjectState(state);
    +
    +    this.bind('view:show',function(){

    If the div is hidden, SlickGrid will calculate wrongly some +sizes so we must render it explicitly when the view is visible

          if (!self.rendered){
    +        if (!self.grid){
    +          self.render();
    +        }
    +        self.grid.init();
    +        self.rendered = true;
    +      }
    +      self.visible = true;
    +    });
    +    this.bind('view:hide',function(){
    +      self.visible = false;
    +    });
    +
    +  },
    +
    +  events: {
    +  },
    +
    +  render: function() {
    +    var self = this;
    +
    +    var options = {
    +      enableCellNavigation: true,
    +      enableColumnReorder: true,
    +      explicitInitialization: true,
    +      syncColumnCellResize: true,
    +      forceFitColumns: this.state.get('fitColumns')
    +    };

    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,
    +        formatter: formatter
    +      };
    +
    +      var widthInfo = _.find(self.state.get('columnsWidth'),function(c){return c.column == field.id});
    +      if (widthInfo){
    +        column['width'] = widthInfo.width;
    +      }
    +
    +      columns.push(column);
    +    });

    Restrict the visible columns

        var visibleColumns = columns.filter(function(column) {
    +      return _.indexOf(self.state.get('hiddenColumns'), column.id) == -1;
    +    });

    Order them if there is ordering info on the state

        if (this.state.get('columnsOrder')){
    +      visibleColumns = visibleColumns.sort(function(a,b){
    +        return _.indexOf(self.state.get('columnsOrder'),a.id) > _.indexOf(self.state.get('columnsOrder'),b.id);
    +      });
    +      columns = columns.sort(function(a,b){
    +        return _.indexOf(self.state.get('columnsOrder'),a.id) > _.indexOf(self.state.get('columnsOrder'),b.id);
    +      });
    +    }

    Move hidden columns to the end, so they appear at the bottom of the +column picker

        var tempHiddenColumns = [];
    +    for (var i = columns.length -1; i >= 0; i--){
    +      if (_.indexOf(_.pluck(visibleColumns,'id'),columns[i].id) == -1){
    +        tempHiddenColumns.push(columns.splice(i,1)[0]);
    +      }
    +    }
    +    columns = columns.concat(tempHiddenColumns);
    +
    +    var data = [];
    +
    +    this.model.records.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);

    Column sorting

        var sortInfo = this.model.queryState.get('sort');
    +    if (sortInfo){
    +      var column = _.keys(sortInfo[0])[0];
    +      var sortAsc = !(sortInfo[0][column].order == 'desc');
    +      this.grid.setSortColumn(column, sortAsc);
    +    }
    +
    +    this.grid.onSort.subscribe(function(e, args){
    +      var order = (args.sortAsc) ? 'asc':'desc';
    +      var sort = [{}];
    +      sort[0][args.sortCol.field] = {order: order};
    +      self.model.query({sort: sort});
    +    });
    +
    +    this.grid.onColumnsReordered.subscribe(function(e, args){
    +      self.state.set({columnsOrder: _.pluck(self.grid.getColumns(),'id')});
    +    });
    +
    +    this.grid.onColumnsResized.subscribe(function(e, args){
    +        var columns = args.grid.getColumns();
    +        var defaultColumnWidth = args.grid.getOptions().defaultColumnWidth;
    +        var columnsWidth = [];
    +        _.each(columns,function(column){
    +          if (column.width != defaultColumnWidth){
    +            columnsWidth.push({column:column.id,width:column.width});
    +          }
    +        });
    +        self.state.set({columnsWidth:columnsWidth});
    +    });
    +
    +    var columnpicker = new Slick.Controls.ColumnPicker(columns, this.grid,
    +                                                       _.extend(options,{state:this.state}));
    +
    +    if (self.visible){
    +      self.grid.init();
    +      self.rendered = true;
    +    } else {

    Defer rendering until the view is visible

          self.rendered = false;
    +    }
    +
    +    return this;
    + }
    +});
    +
    +})(jQuery, recline.View);
    +
    +/*
    +* Context menu for the column picker, adapted from
    +* http://mleibman.github.com/SlickGrid/examples/example-grouping
    +*
    +*/
    +(function ($) {
    +  function SlickColumnPicker(columns, grid, options) {
    +    var $menu;
    +    var columnCheckboxes;
    +
    +    var defaults = {
    +      fadeSpeed:250
    +    };
    +
    +    function init() {
    +      grid.onHeaderContextMenu.subscribe(handleHeaderContextMenu);
    +      options = $.extend({}, defaults, options);
    +
    +      $menu = $('<ul class="dropdown-menu slick-contextmenu" style="display:none;position:absolute;z-index:20;" />').appendTo(document.body);
    +
    +      $menu.bind('mouseleave', function (e) {
    +        $(this).fadeOut(options.fadeSpeed)
    +      });
    +      $menu.bind('click', updateColumn);
    +
    +    }
    +
    +    function handleHeaderContextMenu(e, args) {
    +      e.preventDefault();
    +      $menu.empty();
    +      columnCheckboxes = [];
    +
    +      var $li, $input;
    +      for (var i = 0; i < columns.length; i++) {
    +        $li = $('<li />').appendTo($menu);
    +        $input = $('<input type="checkbox" />').data('column-id', columns[i].id).attr('id','slick-column-vis-'+columns[i].id);
    +        columnCheckboxes.push($input);
    +
    +        if (grid.getColumnIndex(columns[i].id) != null) {
    +          $input.attr('checked', 'checked');
    +        }
    +        $input.appendTo($li);
    +        $('<label />')
    +            .text(columns[i].name)
    +            .attr('for','slick-column-vis-'+columns[i].id)
    +            .appendTo($li);
    +      }
    +      $('<li/>').addClass('divider').appendTo($menu);
    +      $li = $('<li />').data('option', 'autoresize').appendTo($menu);
    +      $input = $('<input type="checkbox" />').data('option', 'autoresize').attr('id','slick-option-autoresize');
    +      $input.appendTo($li);
    +      $('<label />')
    +          .text('Force fit columns')
    +          .attr('for','slick-option-autoresize')
    +          .appendTo($li);
    +      if (grid.getOptions().forceFitColumns) {
    +        $input.attr('checked', 'checked');
    +      }
    +
    +      $menu.css('top', e.pageY - 10)
    +          .css('left', e.pageX - 10)
    +          .fadeIn(options.fadeSpeed);
    +    }
    +
    +    function updateColumn(e) {
    +      if ($(e.target).data('option') == 'autoresize') {
    +        var checked;
    +        if ($(e.target).is('li')){
    +            var checkbox = $(e.target).find('input').first();
    +            checked = !checkbox.is(':checked');
    +            checkbox.attr('checked',checked);
    +        } else {
    +          checked = e.target.checked;
    +        }
    +
    +        if (checked) {
    +          grid.setOptions({forceFitColumns:true});
    +          grid.autosizeColumns();
    +        } else {
    +          grid.setOptions({forceFitColumns:false});
    +        }
    +        options.state.set({fitColumns:checked});
    +        return;
    +      }
    +
    +      if (($(e.target).is('li') && !$(e.target).hasClass('divider')) ||
    +            $(e.target).is('input')) {
    +        if ($(e.target).is('li')){
    +            var checkbox = $(e.target).find('input').first();
    +            checkbox.attr('checked',!checkbox.is(':checked'));
    +        }
    +        var visibleColumns = [];
    +        var hiddenColumnsIds = [];
    +        $.each(columnCheckboxes, function (i, e) {
    +          if ($(this).is(':checked')) {
    +            visibleColumns.push(columns[i]);
    +          } else {
    +            hiddenColumnsIds.push(columns[i].id);
    +          }
    +        });
    +
    +
    +        if (!visibleColumns.length) {
    +          $(e.target).attr('checked', 'checked');
    +          return;
    +        }
    +
    +        grid.setColumns(visibleColumns);
    +        options.state.set({hiddenColumns:hiddenColumnsIds});
    +      }
    +    }
    +    init();
    +  }

    Slick.Controls.ColumnPicker

      $.extend(true, window, { Slick:{ Controls:{ ColumnPicker:SlickColumnPicker }}});
    +})(jQuery);
    +
    +
    \ No newline at end of file diff --git a/docs/src/view.timeline.html b/docs/src/view.timeline.html new file mode 100644 index 000000000..a32cb7012 --- /dev/null +++ b/docs/src/view.timeline.html @@ -0,0 +1,157 @@ + view.timeline.js

    view.timeline.js

    /*jshint multistr:true */
    +
    +this.recline = this.recline || {};
    +this.recline.View = this.recline.View || {};
    +
    +(function($, my) {

    turn off unnecessary logging from VMM Timeline

    if (typeof VMM !== 'undefined') {
    +  VMM.debug = false;
    +}

    Timeline

    + +

    Timeline view using http://timeline.verite.co/

    my.Timeline = Backbone.View.extend({
    +  tagName:  'div',
    +
    +  template: ' \
    +    <div class="recline-timeline"> \
    +      <div id="vmm-timeline-id"></div> \
    +    </div> \
    +  ',

    These are the default (case-insensitive) names of field that are used if found. +If not found, the user will need to define these fields on initialization

      startFieldNames: ['date','startdate', 'start', 'start-date'],
    +  endFieldNames: ['end','endDate'],
    +  elementId: '#vmm-timeline-id',
    +
    +  initialize: function(options) {
    +    var self = this;
    +    this.el = $(this.el);
    +    this.timeline = new VMM.Timeline();
    +    this._timelineIsInitialized = false;
    +    this.bind('view:show', function() {

    only call _initTimeline once view in DOM as Timeline uses $ internally to look up element

          if (self._timelineIsInitialized === false) {
    +        self._initTimeline();
    +      }
    +    });
    +    this.model.fields.bind('reset', function() {
    +      self._setupTemporalField();
    +    });
    +    this.model.records.bind('all', function() {
    +      self.reloadData();
    +    });
    +    var stateData = _.extend({
    +        startField: null,
    +        endField: null
    +      },
    +      options.state
    +    );
    +    this.state = new recline.Model.ObjectState(stateData);
    +    this._setupTemporalField();
    +    this.render();

    can only call _initTimeline once view in DOM as Timeline uses $ +internally to look up element

        if ($(this.elementId).length > 0) {
    +      this._initTimeline();
    +    }
    +  },
    +
    +  render: function() {
    +    var tmplData = {};
    +    var htmls = Mustache.render(this.template, tmplData);
    +    this.el.html(htmls);
    +  },
    +
    +  _initTimeline: function() {
    +    var $timeline = this.el.find(this.elementId);

    set width explicitly o/w timeline goes wider that screen for some reason

        var width = Math.max(this.el.width(), this.el.find('.recline-timeline').width());
    +    if (width) {
    +      $timeline.width(width);
    +    }
    +    var config = {};
    +    var data = this._timelineJSON();
    +    this.timeline.init(data, this.elementId, config);
    +    this._timelineIsInitialized = true
    +  },
    +
    +  reloadData: function() {
    +    if (this._timelineIsInitialized) {
    +      var data = this._timelineJSON();
    +      this.timeline.reload(data);
    +    }
    +  },

    Convert record to JSON for timeline

    + +

    Designed to be overridden in client apps

      convertRecord: function(record, fields) {
    +    return this._convertRecord(record, fields);
    +  },

    Internal method to generate a Timeline formatted entry

      _convertRecord: function(record, fields) {
    +    var start = this._parseDate(record.get(this.state.get('startField')));
    +    var end = this._parseDate(record.get(this.state.get('endField')));
    +    if (start) {
    +      var tlEntry = {
    +        "startDate": start,
    +        "endDate": end,
    +        "headline": String(record.get('title') || ''),
    +        "text": record.get('description') || this.model.recordSummary(record)
    +      };
    +      return tlEntry;
    +    } else {
    +      return null;
    +    }
    +  },
    +
    +  _timelineJSON: function() {
    +    var self = this;
    +    var out = {
    +      'timeline': {
    +        'type': 'default',
    +        'headline': '',
    +        'date': [
    +        ]
    +      }
    +    };
    +    this.model.records.each(function(record) {
    +      var newEntry = self.convertRecord(record, self.fields);
    +      if (newEntry) {
    +        out.timeline.date.push(newEntry); 
    +      }
    +    });

    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;
    +  },
    +
    +  _parseDate: function(date) {
    +    if (!date) {
    +      return null;
    +    }
    +    var out = date.trim();
    +    out = out.replace(/(\d)th/g, '$1');
    +    out = out.replace(/(\d)st/g, '$1');
    +    out = out.trim() ? moment(out) : null;
    +    if (out.toDate() == 'Invalid Date') {
    +      return null;
    +    } else {

    fix for moment weirdness around date parsing and time zones +moment('1914-08-01').toDate() => 1914-08-01 00:00 +01:00 +which in iso format (with 0 time offset) is 31 July 1914 23:00 +meanwhile native new Date('1914-08-01') => 1914-08-01 01:00 +01:00

          out = out.subtract('minutes', out.zone());
    +      return out.toDate();
    +    }
    +  },
    +
    +  _setupTemporalField: function() {
    +    this.state.set({
    +      startField: this._checkField(this.startFieldNames),
    +      endField: this._checkField(this.endFieldNames)
    +    });
    +  },
    +
    +  _checkField: function(possibleFieldNames) {
    +    var modelFieldNames = this.model.fields.pluck('id');
    +    for (var i = 0; i < possibleFieldNames.length; i++){
    +      for (var j = 0; j < modelFieldNames.length; j++){
    +        if (modelFieldNames[j].toLowerCase() == possibleFieldNames[i].toLowerCase())
    +          return modelFieldNames[j];
    +      }
    +    }
    +    return null;
    +  }
    +});
    +
    +})(jQuery, recline.View);
    +
    +
    \ No newline at end of file diff --git a/docs/src/view.transform.html b/docs/src/view.transform.html new file mode 100644 index 000000000..fbe2dcc2d --- /dev/null +++ b/docs/src/view.transform.html @@ -0,0 +1,125 @@ + view.transform.js

    view.transform.js

    /*jshint multistr:true */
    +
    +this.recline = this.recline || {};
    +this.recline.View = this.recline.View || {};

    Views module following classic module pattern

    (function($, my) {

    ColumnTransform

    + +

    View (Dialog) for doing data transformations

    my.Transform = Backbone.View.extend({
    +  className: 'recline-transform',
    +  template: ' \
    +    <div class="script"> \
    +      <h2> \
    +        Transform Script \
    +        <button class="okButton btn btn-primary">Run on all records</button> \
    +      </h2> \
    +      <textarea class="expression-preview-code"></textarea> \
    +    </div> \
    +    <div class="expression-preview-parsing-status"> \
    +      No syntax error. \
    +    </div> \
    +    <div class="preview"> \
    +      <h3>Preview</h3> \
    +      <div class="expression-preview-container"></div> \
    +    </div> \
    +  ',
    +
    +  events: {
    +    'click .okButton': 'onSubmit',
    +    'keydown .expression-preview-code': 'onEditorKeydown'
    +  },
    +
    +  initialize: function(options) {
    +    this.el = $(this.el);
    +    this.render();
    +  },
    +
    +  render: function() {
    +    var htmls = Mustache.render(this.template);
    +    this.el.html(htmls);

    Put in the basic (identity) transform script +TODO: put this into the template?

        var editor = this.el.find('.expression-preview-code');
    +    if (this.model.fields.length > 0) {
    +      var col = this.model.fields.models[0].id;
    +    } else {
    +      var col = 'unknown';
    +    }
    +    editor.val("function(doc) {\n  doc['"+ col +"'] = doc['"+ col +"'];\n  return doc;\n}");
    +    editor.focus().get(0).setSelectionRange(18, 18);
    +    editor.keydown();
    +  },
    +
    +  onSubmit: function(e) {
    +    var self = this;
    +    var funcText = this.el.find('.expression-preview-code').val();
    +    var editFunc = costco.evalFunction(funcText);
    +    if (editFunc.errorMessage) {
    +      this.trigger('recline:flash', {message: "Error with function! " + editFunc.errorMessage});
    +      return;
    +    }
    +    this.model.transform(editFunc);
    +  },
    +
    +  editPreviewTemplate: ' \
    +      <table class="table table-condensed table-bordered before-after"> \
    +      <thead> \
    +      <tr> \
    +        <th>Field</th> \
    +        <th>Before</th> \
    +        <th>After</th> \
    +      </tr> \
    +      </thead> \
    +      <tbody> \
    +      {{#row}} \
    +      <tr> \
    +        <td> \
    +          {{field}} \
    +        </td> \
    +        <td class="before {{#different}}different{{/different}}"> \
    +          {{before}} \
    +        </td> \
    +        <td class="after {{#different}}different{{/different}}"> \
    +          {{after}} \
    +        </td> \
    +      </tr> \
    +      {{/row}} \
    +      </tbody> \
    +      </table> \
    +  ',
    +
    +  onEditorKeydown: function(e) {
    +    var self = this;

    if you don't setTimeout it won't grab the latest character if you call e.target.value

        window.setTimeout( function() {
    +      var errors = self.el.find('.expression-preview-parsing-status');
    +      var editFunc = costco.evalFunction(e.target.value);
    +      if (!editFunc.errorMessage) {
    +        errors.text('No syntax error.');
    +        var docs = self.model.records.map(function(doc) {
    +          return doc.toJSON();
    +        });
    +        var previewData = costco.previewTransform(docs, editFunc);
    +        var $el = self.el.find('.expression-preview-container');
    +        var fields = self.model.fields.toJSON();
    +        var rows = _.map(previewData.slice(0,4), function(row) {
    +          return _.map(fields, function(field) {
    +            return {
    +              field: field.id,
    +              before: row.before[field.id],
    +              after: row.after[field.id],
    +              different: !_.isEqual(row.before[field.id], row.after[field.id])
    +            }
    +          });
    +        });
    +        $el.html('');
    +        _.each(rows, function(row) {
    +          var templated = Mustache.render(self.editPreviewTemplate, {
    +            row: row
    +          });
    +          $el.append(templated);
    +        });
    +      } else {
    +        errors.text(editFunc.errorMessage);
    +      }
    +    }, 1, true);
    +  }
    +});
    +
    +})(jQuery, recline.View);
    +
    +
    \ No newline at end of file diff --git a/docs/src/widget.facetviewer.html b/docs/src/widget.facetviewer.html new file mode 100644 index 000000000..fb4077485 --- /dev/null +++ b/docs/src/widget.facetviewer.html @@ -0,0 +1,79 @@ + widget.facetviewer.js

    widget.facetviewer.js

    /*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: ' \
    +    <a class="close js-hide" href="#">&times;</a> \
    +    <div class="facets row"> \
    +      <div class="span1"> \
    +        <h3>Facets</h3> \
    +      </div> \
    +      {{#facets}} \
    +      <div class="facet-summary span2 dropdown" data-facet="{{id}}"> \
    +        <a class="btn dropdown-toggle" data-toggle="dropdown" href="#"><i class="icon-chevron-down"></i> {{id}} {{label}}</a> \
    +        <ul class="facet-items dropdown-menu"> \
    +        {{#terms}} \
    +          <li><a class="facet-choice js-facet-filter" data-value="{{term}}">{{term}} ({{count}})</a></li> \
    +        {{/terms}} \
    +        {{#entries}} \
    +          <li><a class="facet-choice js-facet-filter" data-value="{{time}}">{{term}} ({{count}})</a></li> \
    +        {{/entries}} \
    +        </ul> \
    +      </div> \
    +      {{/facets}} \
    +    </div> \
    +  ',
    +
    +  events: {
    +    'click .js-hide': 'onHide',
    +    'click .js-facet-filter': 'onFacetFilter'
    +  },
    +  initialize: function(model) {
    +    _.bindAll(this, 'render');
    +    this.el = $(this.el);
    +    this.model.facets.bind('all', this.render);
    +    this.model.fields.bind('all', this.render);
    +    this.render();
    +  },
    +  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 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);
    +
    +
    \ No newline at end of file diff --git a/docs/src/widget.fields.html b/docs/src/widget.fields.html new file mode 100644 index 000000000..22fa2b0d1 --- /dev/null +++ b/docs/src/widget.fields.html @@ -0,0 +1,99 @@ + widget.fields.js

    widget.fields.js

    /*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', 
    +  template: ' \
    +    <div class="accordion fields-list well"> \
    +    <h3>Fields <a href="#" class="js-show-hide">+</a></h3> \
    +    {{#fields}} \
    +      <div class="accordion-group field"> \
    +        <div class="accordion-heading"> \
    +          <i class="icon-file"></i> \
    +          <h4> \
    +            {{label}} \
    +            <small> \
    +              {{type}} \
    +              <a class="accordion-toggle" data-toggle="collapse" href="#collapse{{id}}"> &raquo; </a> \
    +            </small> \
    +          </h4> \
    +        </div> \
    +        <div id="collapse{{id}}" class="accordion-body collapse in"> \
    +          <div class="accordion-inner"> \
    +            {{#facets}} \
    +            <div class="facet-summary" data-facet="{{id}}"> \
    +              <ul class="facet-items"> \
    +              {{#terms}} \
    +                <li class="facet-item"><span class="term">{{term}}</span> <span class="count">[{{count}}]</span></li> \
    +              {{/terms}} \
    +              </ul> \
    +            </div> \
    +            {{/facets}} \
    +            <div class="clear"></div> \
    +          </div> \
    +        </div> \
    +      </div> \
    +    {{/fields}} \
    +    </div> \
    +  ',
    +
    +  events: {
    +    'click .js-show-hide': 'onShowHide'
    +  },
    +  initialize: function(model) {
    +    var self = this;
    +    this.el = $(this.el);
    +    _.bindAll(this, 'render');

    TODO: this is quite restrictive in terms of when it is re-run +e.g. a change in type will not trigger a re-run atm. +being more liberal (e.g. binding to all) can lead to being called a lot (e.g. for change:width)

        this.model.fields.bind('reset', function(action) {
    +      self.model.fields.each(function(field) {
    +        field.facets.unbind('all', self.render);
    +        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.render();
    +  },
    +  render: function() {
    +    var self = this;
    +    var tmplData = {
    +      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.find('.collapse').collapse('hide');
    +  },
    +  onShowHide: function(e) {
    +    e.preventDefault();
    +    var $target  = $(e.target);

    weird collapse class seems to have been removed (can watch this happen +if you watch dom) but could not work why. Absence of collapse then meant +we could not toggle. +This seems to fix the problem.

        this.el.find('.accordion-body').addClass('collapse');;
    +    if ($target.text() === '+') {
    +      this.el.find('.collapse').collapse('show');
    +      $target.text('-');
    +    } else {
    +      this.el.find('.collapse').collapse('hide');
    +      $target.text('+');
    +    }
    +  }
    +});
    +
    +})(jQuery, recline.View);
    +
    +
    \ No newline at end of file diff --git a/docs/src/widget.filtereditor.html b/docs/src/widget.filtereditor.html new file mode 100644 index 000000000..9c063b9ac --- /dev/null +++ b/docs/src/widget.filtereditor.html @@ -0,0 +1,146 @@ + widget.filtereditor.js

    widget.filtereditor.js

    /*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: ' \
    +    <div class="filters"> \
    +      <h3>Filters</h3> \
    +      <a href="#" class="js-add-filter">Add filter</a> \
    +      <form class="form-stacked js-add" style="display: none;"> \
    +        <fieldset> \
    +          <label>Filter type</label> \
    +          <select class="filterType"> \
    +            <option value="term">Term (text)</option> \
    +            <option value="geo_distance">Geo distance</option> \
    +          </select> \
    +          <label>Field</label> \
    +          <select class="fields"> \
    +            {{#fields}} \
    +            <option value="{{id}}">{{label}}</option> \
    +            {{/fields}} \
    +          </select> \
    +          <button type="submit" class="btn">Add</button> \
    +        </fieldset> \
    +      </form> \
    +      <form class="form-stacked js-edit"> \
    +        {{#filters}} \
    +          {{{filterRender}}} \
    +        {{/filters}} \
    +        {{#filters.length}} \
    +        <button type="submit" class="btn">Update</button> \
    +        {{/filters.length}} \
    +      </form> \
    +    </div> \
    +  ',
    +  filterTemplates: {
    +    term: ' \
    +      <div class="filter-{{type}} filter"> \
    +        <fieldset> \
    +          <legend> \
    +            {{field}} <small>{{type}}</small> \
    +            <a class="js-remove-filter" href="#" title="Remove this filter">&times;</a> \
    +          </legend> \
    +          <input type="text" value="{{term}}" name="term" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
    +        </fieldset> \
    +      </div> \
    +    ',
    +    geo_distance: ' \
    +      <div class="filter-{{type}} filter"> \
    +        <fieldset> \
    +          <legend> \
    +            {{field}} <small>{{type}}</small> \
    +            <a class="js-remove-filter" href="#" title="Remove this filter">&times;</a> \
    +          </legend> \
    +          <label class="control-label" for="">Longitude</label> \
    +          <input type="text" value="{{point.lon}}" name="lon" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
    +          <label class="control-label" for="">Latitude</label> \
    +          <input type="text" value="{{point.lat}}" name="lat" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
    +          <label class="control-label" for="">Distance (km)</label> \
    +          <input type="text" value="{{distance}}" name="distance" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
    +        </fieldset> \
    +      </div> \
    +    '
    +  },
    +  events: {
    +    'click .js-remove-filter': 'onRemoveFilter',
    +    'click .js-add-filter': 'onAddFilterShow',
    +    'submit form.js-edit': 'onTermFiltersUpdate',
    +    'submit form.js-add': 'onAddFilter'
    +  },
    +  initialize: function() {
    +    this.el = $(this.el);
    +    _.bindAll(this, 'render');
    +    this.model.fields.bind('all', this.render);
    +    this.model.queryState.bind('change', this.render);
    +    this.model.queryState.bind('change:filters:new-blank', this.render);
    +    this.render();
    +  },
    +  render: function() {
    +    var self = this;
    +    var tmplData = $.extend(true, {}, this.model.queryState.toJSON());

    we will use idx in list as there id ...

        tmplData.filters = _.map(tmplData.filters, function(filter, idx) {
    +      filter.id = idx;
    +      return filter;
    +    });
    +    tmplData.fields = this.model.fields.toJSON();
    +    tmplData.filterRender = function() {
    +      return Mustache.render(self.filterTemplates[this.type], this);
    +    };
    +    var out = Mustache.render(this.template, tmplData);
    +    this.el.html(out);
    +  },
    +  onAddFilterShow: function(e) {
    +    e.preventDefault();
    +    var $target = $(e.target);
    +    $target.hide();
    +    this.el.find('form.js-add').show();
    +  },
    +  onAddFilter: function(e) {
    +    e.preventDefault();
    +    var $target = $(e.target);
    +    $target.hide();
    +    var filterType = $target.find('select.filterType').val();
    +    var field = $target.find('select.fields').val();
    +    this.model.queryState.addFilter({type: filterType, field: field});

    trigger render explicitly as queryState change will not be triggered (as blank value for filter)

        this.render();
    +  },
    +  onRemoveFilter: function(e) {
    +    e.preventDefault();
    +    var $target = $(e.target);
    +    var filterId = $target.closest('.filter').attr('data-filter-id');
    +    this.model.queryState.removeFilter(filterId);
    +  },
    +  onTermFiltersUpdate: function(e) {
    +   var self = this;
    +    e.preventDefault();
    +    var filters = self.model.queryState.get('filters');
    +    var $form = $(e.target);
    +    _.each($form.find('input'), function(input) {
    +      var $input = $(input);
    +      var filterType = $input.attr('data-filter-type');
    +      var fieldId = $input.attr('data-filter-field');
    +      var filterIndex = parseInt($input.attr('data-filter-id'));
    +      var name = $input.attr('name');
    +      var value = $input.val();
    +      if (filterType === 'term') {
    +        filters[filterIndex].term = value;
    +      } else if (filterType === 'geo_distance') {
    +        if (name === 'distance') {
    +          filters[filterIndex].distance = parseFloat(value);
    +        } else {
    +          filters[filterIndex].point[name] = parseFloat(value);
    +        }
    +      }
    +    });
    +    self.model.queryState.set({filters: filters});
    +    self.model.queryState.trigger('change');
    +  }
    +});
    +
    +
    +})(jQuery, recline.View);
    +
    +
    \ No newline at end of file diff --git a/docs/src/widget.pager.html b/docs/src/widget.pager.html new file mode 100644 index 000000000..a18773096 --- /dev/null +++ b/docs/src/widget.pager.html @@ -0,0 +1,58 @@ + widget.pager.js

    widget.pager.js

    /*jshint multistr:true */
    +
    +this.recline = this.recline || {};
    +this.recline.View = this.recline.View || {};
    +
    +(function($, my) {
    +
    +my.Pager = Backbone.View.extend({
    +  className: 'recline-pager', 
    +  template: ' \
    +    <div class="pagination"> \
    +      <ul> \
    +        <li class="prev action-pagination-update"><a href="">&laquo;</a></li> \
    +        <li class="active"><a><input name="from" type="text" value="{{from}}" /> &ndash; <input name="to" type="text" value="{{to}}" /> </a></li> \
    +        <li class="next action-pagination-update"><a href="">&raquo;</a></li> \
    +      </ul> \
    +    </div> \
    +  ',
    +
    +  events: {
    +    'click .action-pagination-update': 'onPaginationUpdate',
    +    'change input': 'onFormSubmit'
    +  },
    +
    +  initialize: function() {
    +    _.bindAll(this, 'render');
    +    this.el = $(this.el);
    +    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 = 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);
    +
    +
    \ No newline at end of file diff --git a/docs/src/widget.queryeditor.html b/docs/src/widget.queryeditor.html new file mode 100644 index 000000000..1d9adf56f --- /dev/null +++ b/docs/src/widget.queryeditor.html @@ -0,0 +1,45 @@ + widget.queryeditor.js

    widget.queryeditor.js

    /*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: ' \
    +    <form action="" method="GET" class="form-inline"> \
    +      <div class="input-prepend text-query"> \
    +        <span class="add-on"><i class="icon-search"></i></span> \
    +        <input type="text" name="q" value="{{q}}" class="span2" placeholder="Search data ..." class="search-query" /> \
    +      </div> \
    +      <button type="submit" class="btn">Go &raquo;</button> \
    +    </form> \
    +  ',
    +
    +  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);
    +
    +
    \ No newline at end of file