diff --git a/docs/backend/base.html b/docs/backend/base.html index 38e13e3a..f1188f39 100644 --- a/docs/backend/base.html +++ b/docs/backend/base.html @@ -1,31 +1,21 @@ - base.js
Jump To …

base.js

Recline Backends

+ base.js

base.js

Recline Backends

Backends are connectors to backend data sources and stores

This is just the base module containing a template Base class and convenience methods.

this.recline = this.recline || {};
-this.recline.Backend = this.recline.Backend || {};
+this.recline.Backend = this.recline.Backend || {};

recline.Backend.Base

-(function($, my) {

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);
-  };

recline.Backend.Base

- -

Base class for backends providing a template and convenience functions. -You do not have to inherit from this class but even when not it does -provide guidance on the functions you must implement.

- -

Note also that while this (and other Backends) are implemented as Backbone models this is just a convenience.

  my.Base = Backbone.Model.extend({

type

+

Exemplar 'class' for backends showing what a base class would look like.

this.recline.Backend.Base = function() {

type

'type' of this backend. This should be either the class path for this object as a string (e.g. recline.Backend.Memory) or for Backends within recline.Backend module it may be their class name.

This value is used as an identifier for this backend when initializing -backends (see recline.Model.Dataset.initialize).

    __type__: 'base',

readonly

+backends (see recline.Model.Dataset.initialize).

  this.__type__ = 'base';

readonly

Class level attribute indicating that this backend is read-only (that -is, cannot be written to).

    readonly: true,

sync

+is, cannot be written to).

  this.readonly = true;

sync

An implementation of Backbone.sync that will be used to override Backbone.sync on operations for Datasets and Documents which are using this backend.

@@ -37,9 +27,9 @@ for Documents because they are loaded in bulk by the query method.

For backends supporting write operations you must implement update and delete support for Document objects.

-

All code paths should return an object conforming to the jquery promise API.

    sync: function(method, model, options) {
-    },
-    

query

+

All code paths should return an object conforming to the jquery promise API.

  this.sync = function(method, model, options) {
+  },
+  

query

Query the backend for documents returning them in bulk. This method will be used by the Dataset.query method to search the backend for documents, @@ -79,8 +69,8 @@ details):

// facet results (as per ) } } -
    query: function(model, queryObj) {
-    },

_makeRequest

+
  this.query = function(model, queryObj) {}
+};

makeRequest

Just $.ajax but in any headers in the 'headers' attribute of this Backend instance. Example:

@@ -89,53 +79,19 @@ Backend instance. Example:

var jqxhr = this._makeRequest({ url: the-url }); -
    _makeRequest: function(data) {
-      var headers = this.get('headers');
-      var extras = {};
-      if (headers) {
-        extras = {
-          beforeSend: function(req) {
-            _.each(headers, function(value, key) {
-              req.setRequestHeader(key, value);
-            });
-          }
-        };
-      }
-      var data = _.extend(extras, data);
-      return $.ajax(data);
-    },

convenience method to convert simple set of documents / rows to a QueryResult

    _docsToQueryResult: function(rows) {
-      var hits = _.map(rows, function(row) {
-        return { _source: row };
-      });
-      return {
-        total: null,
-        hits: hits
-      };
-    },

_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.

    _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'
+
this.recline.Backend.makeRequest = function(data, headers) {
+  var extras = {};
+  if (headers) {
+    extras = {
+      beforeSend: function(req) {
+        _.each(headers, function(value, key) {
+          req.setRequestHeader(key, value);
         });
-      }, timeout);
-      ourFunction.done(function(arguments) {
-          clearTimeout(timer);
-          dfd.resolve(arguments);
-        })
-        .fail(function(arguments) {
-          clearTimeout(timer);
-          dfd.reject(arguments);
-        })
-        ;
-      return dfd.promise();
-    }
-  });
-
-}(jQuery, this.recline.Backend));
+      }
+    };
+  }
+  var data = _.extend(extras, data);
+  return $.ajax(data);
+};
 
 
\ No newline at end of file diff --git a/docs/backend/localcsv.html b/docs/backend/csv.html similarity index 76% rename from docs/backend/localcsv.html rename to docs/backend/csv.html index 3c708f28..54474a6e 100644 --- a/docs/backend/localcsv.html +++ b/docs/backend/csv.html @@ -1,15 +1,20 @@ - localcsv.js

localcsv.js

this.recline = this.recline || {};
+      csv.js           
processField; processField = function (field) { - if (fieldQuoted !== true) { }; for (i = 0; i < s.length; i += 1) { - cur = s.charAt(i); }; var rxIsInt = /^\d+$/, - rxIsFloat = /^\d*\.\d+$|^\d+\.\d*$/,

csv.js

this.recline = this.recline || {};
 this.recline.Backend = this.recline.Backend || {};
+this.recline.Backend.CSV = this.recline.Backend.CSV || {};
 
-(function($, my) {
-  my.loadFromCSVFile = function(file, callback, options) {
+(function(my) {

load

+ +

Load data from a CSV file referenced in an HTMl5 file object returning the +dataset in the callback

+ +

@param options as for parseCSV below

  my.load = function(file, callback, options) {
     var encoding = options.encoding || 'UTF-8';
     
     var metadata = {
       id: file.name,
       file: file
     };
-    var reader = new FileReader();

TODO

    reader.onload = function(e) {
+    var reader = new FileReader();

TODO

    reader.onload = function(e) {
       var dataset = my.csvToDataset(e.target.result, options);
       callback(dataset);
     };
@@ -31,9 +36,9 @@
       });
       return _doc;
     });
-    var dataset = recline.Backend.createDataset(data, fields);
+    var dataset = recline.Backend.Memory.createDataset(data, fields);
     return dataset;
-  };

Converts a Comma Separated Values string into an array of arrays. + };

Converts a Comma Separated Values string into an array of arrays. Each line in the CSV becomes an array.

Empty fields are converted to nulls and non-quoted numbers are converted to integers or floats.

@@ -46,7 +51,7 @@ Each line in the CSV becomes an array.

@param {Boolean} [trim=false] If set to True leading and trailing whitespace is stripped off of each non-quoted field as it is imported @param {String} [separator=','] Separator for CSV file Heavily based on uselesscode's JS CSV parser (MIT Licensed): -thttp://www.uselesscode.org/javascript/csv/

  my.parseCSV= function(s, options) {

Get rid of any trailing \n

    s = chomp(s);
+thttp://www.uselesscode.org/javascript/csv/

  my.parseCSV= function(s, options) {

Get rid of any trailing \n

    s = chomp(s);
 
     var options = options || {};
     var trm = options.trim;
@@ -64,10 +69,10 @@ thttp://www.uselesscode.org/javascript/csv/

If field is empty set to null

        if (field === '') {
-          field = null;

If the field was not quoted and we are trimming fields, trim it

        } else if (trm === true) {
+      if (fieldQuoted !== true) {

If field is empty set to null

        if (field === '') {
+          field = null;

If the field was not quoted and we are trimming fields, trim it

        } else if (trm === true) {
           field = trim(field);
-        }

Convert unquoted numbers to their appropriate types

        if (rxIsInt.test(field)) {
+        }

Convert unquoted numbers to their appropriate types

        if (rxIsInt.test(field)) {
           field = parseInt(field, 10);
         } else if (rxIsFloat.test(field)) {
           field = parseFloat(field, 10);
@@ -77,25 +82,25 @@ thttp://www.uselesscode.org/javascript/csv/

If we are at a EOF or EOR

      if (inQuote === false && (cur === separator || cur === "\n")) {
-	field = processField(field);

Add the current field to the current row

        row.push(field);

If this is EOR append row to output and flush row

        if (cur === "\n") {
+      cur = s.charAt(i);

If we are at a EOF or EOR

      if (inQuote === false && (cur === separator || cur === "\n")) {
+	field = processField(field);

Add the current field to the current row

        row.push(field);

If this is EOR append row to output and flush row

        if (cur === "\n") {
           out.push(row);
           row = [];
-        }

Flush the field buffer

        field = '';
+        }

Flush the field buffer

        field = '';
         fieldQuoted = false;
-      } else {

If it's not a delimiter, add it to the field buffer

        if (cur !== delimiter) {
+      } else {

If it's not a delimiter, add it to the field buffer

        if (cur !== delimiter) {
           field += cur;
         } else {
-          if (!inQuote) {

We are not in a quote, start a quote

            inQuote = true;
+          if (!inQuote) {

We are not in a quote, start a quote

            inQuote = true;
             fieldQuoted = true;
-          } else {

Next char is delimiter, this is an escaped delimiter

            if (s.charAt(i + 1) === delimiter) {
-              field += delimiter;

Skip the next char

              i += 1;
-            } else {

It's not escaping, so end quote

              inQuote = false;
+          } else {

Next char is delimiter, this is an escaped delimiter

            if (s.charAt(i + 1) === delimiter) {
+              field += delimiter;

Skip the next char

              i += 1;
+            } else {

It's not escaping, so end quote

              inQuote = false;
             }
           }
         }
       }
-    }

Add the last field

    field = processField(field);
+    }

Add the last field

    field = processField(field);
     row.push(field);
     out.push(row);
 
@@ -103,10 +108,10 @@ thttp://www.uselesscode.org/javascript/csv/

If a string has leading or trailing space, + rxIsFloat = /^\d*\.\d+$|^\d+\.\d*$/,

If a string has leading or trailing space, contains a comma double quote or a newline it needs to be quoted in CSV output

    rxNeedsQuoting = /^\s|\s$|,|"|\n/,
-    trim = (function () {

Fx 3.1 has a native trim function, it's about 10x faster, use it if it exists

      if (String.prototype.trim) {
+    trim = (function () {

Fx 3.1 has a native trim function, it's about 10x faster, use it if it exists

      if (String.prototype.trim) {
         return function (s) {
           return s.trim();
         };
@@ -118,12 +123,12 @@ it needs to be quoted in CSV output

}()); function chomp(s) { - if (s.charAt(s.length - 1) !== "\n") {

Does not end with \n, just return string

      return s;
-    } else {

Remove the \n

      return s.substring(0, s.length - 1);
+    if (s.charAt(s.length - 1) !== "\n") {

Does not end with \n, just return string

      return s;
+    } else {

Remove the \n

      return s.substring(0, s.length - 1);
     }
   }
 
 
-}(jQuery, this.recline.Backend));
+}(this.recline.Backend.CSV));
 
 
\ No newline at end of file diff --git a/docs/backend/dataproxy.html b/docs/backend/dataproxy.html index 0ff29c43..e5229396 100644 --- a/docs/backend/dataproxy.html +++ b/docs/backend/dataproxy.html @@ -1,11 +1,13 @@ - dataproxy.js

dataproxy.js

this.recline = this.recline || {};
+      dataproxy.js           

dataproxy.js

this.recline = this.recline || {};
 this.recline.Backend = this.recline.Backend || {};
+this.recline.Backend.DataProxy = this.recline.Backend.DataProxy || {};
 
 (function($, my) {

DataProxy Backend

For connecting to DataProxy-s.

-

When initializing the DataProxy backend you can set the following attributes:

+

When initializing the DataProxy backend you can set the following +attributes in the options object:

  • dataproxy: {url-to-proxy} (optional). Defaults to http://jsonpdataproxy.appspot.com
  • @@ -18,14 +20,14 @@
  • format: (optional) csv | xls (defaults to csv if not specified)
-

Note that this is a read-only backend.

  my.DataProxy = my.Base.extend({
-    __type__: 'dataproxy',
-    readonly: true,
-    defaults: {
-      dataproxy_url: 'http://jsonpdataproxy.appspot.com'
-    },
-    sync: function(method, model, options) {
-      var self = this;
+

Note that this is a read-only backend.

  my.Backbone = function(options) {
+    var self = this;
+    this.__type__ = 'dataproxy';
+    this.readonly = true;
+
+    this.dataproxy_url = options && options.dataproxy_url ? options.dataproxy_url : 'http://jsonpdataproxy.appspot.com';
+
+    this.sync = function(method, model, options) {
       if (method === "read") {
         if (model.__type__ == 'Dataset') {

Do nothing as we will get fields in query step (and no metadata to retrieve)

          var dfd = $.Deferred();
@@ -35,22 +37,22 @@ retrieve)

} else { alert('This backend only supports read operations'); } - }, - query: function(dataset, queryObj) { + }; + + this.query = function(dataset, queryObj) { var self = this; - var base = this.get('dataproxy_url'); var data = { url: dataset.get('url'), 'max-results': queryObj.size, type: dataset.get('format') }; var jqxhr = $.ajax({ - url: base, + url: this.dataproxy_url, data: data, dataType: 'jsonp' }); var dfd = $.Deferred(); - this._wrapInTimeout(jqxhr).done(function(results) { + _wrapInTimeout(jqxhr).done(function(results) { if (results.error) { dfd.reject(results.error); } @@ -65,15 +67,42 @@ retrieve)

}); return tmp; }); - dfd.resolve(self._docsToQueryResult(_out)); + dfd.resolve({ + total: null, + hits: _.map(_out, function(row) { + return { _source: row }; + }) + }); }) .fail(function(arguments) { dfd.reject(arguments); }); return dfd.promise(); - } - }); + }; + };

_wrapInTimeout

-}(jQuery, this.recline.Backend)); +

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'
+      });
+    }, timeout);
+    ourFunction.done(function(arguments) {
+        clearTimeout(timer);
+        dfd.resolve(arguments);
+      })
+      .fail(function(arguments) {
+        clearTimeout(timer);
+        dfd.reject(arguments);
+      })
+      ;
+    return dfd.promise();
+  }
+
+}(jQuery, this.recline.Backend.DataProxy));
 
 
\ No newline at end of file diff --git a/docs/backend/elasticsearch.html b/docs/backend/elasticsearch.html index 11bb4b3a..ffd2f047 100644 --- a/docs/backend/elasticsearch.html +++ b/docs/backend/elasticsearch.html @@ -1,122 +1,81 @@ - elasticsearch.js

elasticsearch.js

this.recline = this.recline || {};
+      elasticsearch.js           

elasticsearch.js

this.recline = this.recline || {};
 this.recline.Backend = this.recline.Backend || {};
+this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {};
 
-(function($, my) {

ElasticSearch Backend

+(function($, my) {

ElasticSearch Wrapper

-

Connecting to ElasticSearch.

- -

Usage:

- -
-var backend = new recline.Backend.ElasticSearch({
-  // optional as can also be provided by Dataset/Document
-  url: {url to ElasticSearch endpoint i.e. ES 'type/table' url - more info below}
-  // optional
-  headers: {dict of headers to add to each request}
-});
-
-@param {String} url: url for ElasticSearch type/table, e.g. for ES running
-on localhost:9200 with index // twitter and type tweet it would be:
+

Connecting to ElasticSearch endpoints. +@param {String} endpoint: url for ElasticSearch type/table, e.g. for ES running +on localhost:9200 with index // twitter and type tweet it would be:

http://localhost:9200/twitter/tweet
-This url is optional since the ES endpoint url may be specified on the the -dataset (and on a Document by the document having a dataset attribute) by -having one of the following (see also `_getESUrl` function): +

@param {Object} options: set of options such as:

-
-elasticsearch_url
-webstore_url
-url
-
  my.ElasticSearch = my.Base.extend({
-    __type__: 'elasticsearch',
-    readonly: false,
-    sync: function(method, model, options) {
-      var self = this;
-      if (method === "read") {
-        if (model.__type__ == 'Dataset') {
-          var schemaUrl = self._getESUrl(model) + '/_mapping';
-          var jqxhr = this._makeRequest({
-            url: schemaUrl,
-            dataType: 'jsonp'
-          });
-          var dfd = $.Deferred();
-          this._wrapInTimeout(jqxhr).done(function(schema) {

only one top level key in ES = the type so we can ignore it

            var key = _.keys(schema)[0];
-            var fieldData = _.map(schema[key].properties, function(dict, fieldName) {
-              dict.id = fieldName;
-              return dict;
-            });
-            model.fields.reset(fieldData);
-            dfd.resolve(model, jqxhr);
-          })
-          .fail(function(arguments) {
-            dfd.reject(arguments);
-          });
-          return dfd.promise();
-        } else if (model.__type__ == 'Document') {
-          var base = this._getESUrl(model.dataset) + '/' + model.id;
-          return this._makeRequest({
-            url: base,
-            dataType: 'json'
-          });
-        }
-      } else if (method === 'update') {
-        if (model.__type__ == 'Document') {
-          return this.upsert(model.toJSON(), this._getESUrl(model.dataset));
-        }
-      } else if (method === 'delete') {
-        if (model.__type__ == 'Document') {
-          var url = this._getESUrl(model.dataset);
-          return this.delete(model.id, url);
-        }
-      }
-    },

upsert

+
    +
  • headers - {dict of headers to add to each request}
  • +
  • dataType: dataType for AJAx requests e.g. set to jsonp to make jsonp requests (default is json requests)
  • +
  my.Wrapper = function(endpoint, options) { 
+    var self = this;
+    this.endpoint = endpoint;
+    this.options = _.extend({
+        dataType: 'json'
+      },
+      options);

mapping

+ +

Get ES mapping for this type/table

+ +

@return promise compatible deferred object.

    this.mapping = function() {
+      var schemaUrl = self.endpoint + '/_mapping';
+      var jqxhr = recline.Backend.makeRequest({
+        url: schemaUrl,
+        dataType: this.options.dataType
+      });
+      return jqxhr;
+    };

get

+ +

Get document corresponding to specified id

+ +

@return promise compatible deferred object.

    this.get = function(id) {
+      var base = this.endpoint + '/' + id;
+      return recline.Backend.makeRequest({
+        url: base,
+        dataType: 'json'
+      });
+    };

upsert

create / update a document to ElasticSearch backend

@param {Object} doc an object to insert to the index. -@param {string} url (optional) url for ElasticSearch endpoint (if not -defined called this._getESUrl()

    upsert: function(doc, url) {
+@return deferred supporting promise API

    this.upsert = function(doc) {
       var data = JSON.stringify(doc);
-      url = url ? url : this._getESUrl();
+      url = this.endpoint;
       if (doc.id) {
         url += '/' + doc.id;
       }
-      return this._makeRequest({
+      return recline.Backend.makeRequest({
         url: url,
         type: 'POST',
         data: data,
         dataType: 'json'
       });
-    },

delete

+ };

delete

Delete a document from the ElasticSearch backend.

@param {Object} id id of object to delete -@param {string} url (optional) url for ElasticSearch endpoint (if not -provided called this._getESUrl()

    delete: function(id, url) {
-      url = url ? url : this._getESUrl();
+@return deferred supporting promise API

    this.delete = function(id) {
+      url = this.endpoint;
       url += '/' + id;
-      return this._makeRequest({
+      return recline.Backend.makeRequest({
         url: url,
         type: 'DELETE',
         dataType: 'json'
       });
-    },

_getESUrl

+ }; -

get url to ElasticSearch endpoint (see above)

    _getESUrl: function(dataset) {
-      if (dataset) {
-        var out = dataset.get('elasticsearch_url');
-        if (out) return out;
-        out = dataset.get('webstore_url');
-        if (out) return out;
-        out = dataset.get('url');
-        return out;
-      }
-      return this.get('url');
-    },
-    _normalizeQuery: function(queryObj) {
-      var out = queryObj.toJSON ? queryObj.toJSON() : _.extend({}, queryObj);
+    this._normalizeQuery = function(queryObj) {
+      var out = queryObj && queryObj.toJSON ? queryObj.toJSON() : _.extend({}, queryObj);
       if (out.q !== undefined && out.q.trim() === '') {
         delete out.q;
       }
@@ -144,17 +103,77 @@ provided called this._getESUrl()

delete out.filters; } return out; - }, - query: function(model, queryObj) { + };

query

+ +

@return deferred supporting promise API

    this.query = function(queryObj) {
       var queryNormalized = this._normalizeQuery(queryObj);
       var data = {source: JSON.stringify(queryNormalized)};
-      var base = this._getESUrl(model);
-      var jqxhr = this._makeRequest({
-        url: base + '/_search',
+      var url = this.endpoint + '/_search';
+      var jqxhr = recline.Backend.makeRequest({
+        url: url,
         data: data,
-        dataType: 'jsonp'
+        dataType: this.options.dataType
       });
-      var dfd = $.Deferred();

TODO: fail case

      jqxhr.done(function(results) {
+      return jqxhr;
+    }
+  };

ElasticSearch Backbone Backend

+ +

Backbone connector for an ES backend.

+ +

Usage:

+ +

var backend = new recline.Backend.ElasticSearch(options);

+ +

options are passed through to Wrapper

  my.Backbone = function(options) {
+    var self = this;
+    var esOptions = options;
+    this.__type__ = 'elasticsearch';

sync

+ +

Backbone sync implementation for this backend.

+ +

URL of ElasticSearch endpoint to use must be specified on the dataset +(and on a Document via its dataset attribute) by the dataset having a +url attribute.

    this.sync = function(method, model, options) {
+      if (model.__type__ == 'Dataset') {
+        var endpoint = model.get('url');
+      } else {
+        var endpoint = model.dataset.get('url');
+      }
+      var es = new my.Wrapper(endpoint, esOptions);
+      if (method === "read") {
+        if (model.__type__ == 'Dataset') {
+          var dfd = $.Deferred();
+          es.mapping().done(function(schema) {

only one top level key in ES = the type so we can ignore it

            var key = _.keys(schema)[0];
+            var fieldData = _.map(schema[key].properties, function(dict, fieldName) {
+              dict.id = fieldName;
+              return dict;
+            });
+            model.fields.reset(fieldData);
+            dfd.resolve(model);
+          })
+          .fail(function(arguments) {
+            dfd.reject(arguments);
+          });
+          return dfd.promise();
+        } else if (model.__type__ == 'Document') {
+          return es.get(model.dataset.id);
+        }
+      } else if (method === 'update') {
+        if (model.__type__ == 'Document') {
+          return es.upsert(model.toJSON());
+        }
+      } else if (method === 'delete') {
+        if (model.__type__ == 'Document') {
+          return es.delete(model.id);
+        }
+      }
+    };

query

+ +

query the ES backend

    this.query = function(model, queryObj) {
+      var dfd = $.Deferred();
+      var url = model.get('url');
+      var es = new my.Wrapper(url, esOptions);
+      var jqxhr = es.query(queryObj);

TODO: fail case

      jqxhr.done(function(results) {
         _.each(results.hits.hits, function(hit) {
           if (!('id' in hit._source) && hit._id) {
             hit._source.id = hit._id;
@@ -166,9 +185,9 @@ provided called this._getESUrl()

dfd.resolve(results.hits); }); return dfd.promise(); - } - }); + }; + }; -}(jQuery, this.recline.Backend)); +}(jQuery, this.recline.Backend.ElasticSearch));
\ No newline at end of file diff --git a/docs/backend/gdocs.html b/docs/backend/gdocs.html index b84397fa..e014d57d 100644 --- a/docs/backend/gdocs.html +++ b/docs/backend/gdocs.html @@ -1,5 +1,6 @@ - gdocs.js

gdocs.js

this.recline = this.recline || {};
+      gdocs.js           

gdocs.js

this.recline = this.recline || {};
 this.recline.Backend = this.recline.Backend || {};
+this.recline.Backend.GDocs = this.recline.Backend.GDocs || {};
 
 (function($, my) {

Google spreadsheet backend

@@ -14,107 +15,135 @@ var dataset = new recline.Model.Dataset({ }, 'gdocs' ); -
  my.GDoc = my.Base.extend({
-    __type__: 'gdoc',
-    readonly: true,
-    getUrl: function(dataset) {
-      var url = dataset.get('url');
-      if (url.indexOf('feeds/list') != -1) {
-        return url;
-      } else {

https://docs.google.com/spreadsheet/ccc?key=XXXX#gid=0

        var regex = /.*spreadsheet\/ccc?.*key=([^#?&+]+).*/;
-        var matches = url.match(regex);
-        if (matches) {
-          var key = matches[1];
-          var worksheet = 1;
-          var out = 'https://spreadsheets.google.com/feeds/list/' + key + '/' + worksheet + '/public/values?alt=json';
-          return out;
-        } else {
-          alert('Failed to extract gdocs key from ' + url);
-        }
-      }
-    },
-    sync: function(method, model, options) {
+
  my.Backbone = function() {
+    var self = this;
+    this.__type__ = 'gdocs';
+    this.readonly = true;
+
+    this.sync = function(method, model, options) {
       var self = this;
       if (method === "read") { 
         var dfd = $.Deferred(); 
-        var dataset = model;
-
-        var url = this.getUrl(model);
-
-        $.getJSON(url, function(d) {
-          result = self.gdocsToJavascript(d);
-          model.fields.reset(_.map(result.field, function(fieldId) {
-              return {id: fieldId};
-            })
-          );

cache data onto dataset (we have loaded whole gdoc it seems!)

          model._dataCache = result.data;
-          dfd.resolve(model);
-        });
+        dfd.resolve(model);
         return dfd.promise();
       }
-    },
+    };
 
-    query: function(dataset, queryObj) { 
+    this.query = function(dataset, queryObj) { 
       var dfd = $.Deferred();
-      var fields = _.pluck(dataset.fields.toJSON(), 'id');

zip the fields with the data rows to produce js objs -TODO: factor this out as a common method with other backends

      var objs = _.map(dataset._dataCache, function (d) { 
+      if (dataset._dataCache) {
+        dfd.resolve(dataset._dataCache);
+      } else {
+        loadData(dataset.get('url')).done(function(result) {
+          dataset.fields.reset(result.fields);

cache data onto dataset (we have loaded whole gdoc it seems!)

          dataset._dataCache = self._formatResults(dataset, result.data);
+          dfd.resolve(dataset._dataCache);
+        });
+      }
+      return dfd.promise();
+    };
+
+    this._formatResults = function(dataset, data) {
+      var fields = _.pluck(dataset.fields.toJSON(), 'id');

zip the fields with the data rows to produce js objs +TODO: factor this out as a common method with other backends

      var objs = _.map(data, function (d) { 
         var obj = {};
         _.each(_.zip(fields, d), function (x) {
           obj[x[0]] = x[1];
         });
         return obj;
       });
-      dfd.resolve(this._docsToQueryResult(objs));
-      return dfd;
-    },
-    gdocsToJavascript:  function(gdocsSpreadsheet) {
-      /*
-         :options: (optional) optional argument dictionary:
-         columnsToUse: list of columns to use (specified by field names)
-         colTypes: dictionary (with column names as keys) specifying types (e.g. range, percent for use in conversion).
-         :return: tabular data object (hash with keys: field and data).
-
-         Issues: seems google docs return columns in rows in random order and not even sure whether consistent across rows.
-         */
-      var options = {};
-      if (arguments.length > 1) {
-        options = arguments[1];
+      var out = {
+        total: objs.length,
+        hits: _.map(objs, function(row) {
+          return { _source: row }
+        })
       }
-      var results = {
-        'field': [],
-        'data': []
-      };

default is no special info on type of columns

      var colTypes = {};
-      if (options.colTypes) {
-        colTypes = options.colTypes;
-      }

either extract column headings from spreadsheet directly, or used supplied ones

      if (options.columnsToUse) {

columns set to subset supplied

        results.field = options.columnsToUse;
-      } else {

set columns to use to be all available

        if (gdocsSpreadsheet.feed.entry.length > 0) {
-          for (var k in gdocsSpreadsheet.feed.entry[0]) {
-            if (k.substr(0, 3) == 'gsx') {
-              var col = k.substr(4);
-              results.field.push(col);
-            }
-          }
-        }
-      }

converts non numberical values that should be numerical (22.3%[string] -> 0.223[float])

      var rep = /^([\d\.\-]+)\%$/;
-      $.each(gdocsSpreadsheet.feed.entry, function (i, entry) {
-        var row = [];
-        for (var k in results.field) {
-          var col = results.field[k];
-          var _keyname = 'gsx$' + col;
-          var value = entry[_keyname]['$t'];

if labelled as % and value contains %, convert

          if (colTypes[col] == 'percent') {
-            if (rep.test(value)) {
-              var value2 = rep.exec(value);
-              var value3 = parseFloat(value2);
-              value = value3 / 100;
-            }
-          }
-          row.push(value);
-        }
-        results.data.push(row);
-      });
-      return results;
-    }
-  });
+      return out;
+    };
+  };

loadData

-}(jQuery, this.recline.Backend)); +

loadData from a google docs URL

+ +

@return object with two attributes

+ +
    +
  • fields: array of objects
  • +
  • data: array of arrays
  • +
  var loadData = function(url) {
+    var dfd = $.Deferred(); 
+    var url = my.getSpreadsheetAPIUrl(url);
+    var out = {
+      fields: [],
+      data: []
+    }
+    $.getJSON(url, function(d) {
+      result = my.parseData(d);
+      result.fields = _.map(result.fields, function(fieldId) {
+        return {id: fieldId};
+      });
+      dfd.resolve(result);
+    });
+    return dfd.promise();
+  };

parseData

+ +

Parse data from Google Docs API into a reasonable form

+ +

:options: (optional) optional argument dictionary: +columnsToUse: list of columns to use (specified by field names) +colTypes: dictionary (with column names as keys) specifying types (e.g. range, percent for use in conversion). +:return: tabular data object (hash with keys: field and data).

+ +

Issues: seems google docs return columns in rows in random order and not even sure whether consistent across rows.

  my.parseData = function(gdocsSpreadsheet) {
+    var options = {};
+    if (arguments.length > 1) {
+      options = arguments[1];
+    }
+    var results = {
+      'fields': [],
+      'data': []
+    };

default is no special info on type of columns

    var colTypes = {};
+    if (options.colTypes) {
+      colTypes = options.colTypes;
+    }
+    if (gdocsSpreadsheet.feed.entry.length > 0) {
+      for (var k in gdocsSpreadsheet.feed.entry[0]) {
+        if (k.substr(0, 3) == 'gsx') {
+          var col = k.substr(4);
+          results.fields.push(col);
+        }
+      }
+    }

converts non numberical values that should be numerical (22.3%[string] -> 0.223[float])

    var rep = /^([\d\.\-]+)\%$/;
+    $.each(gdocsSpreadsheet.feed.entry, function (i, entry) {
+      var row = [];
+      for (var k in results.fields) {
+        var col = results.fields[k];
+        var _keyname = 'gsx$' + col;
+        var value = entry[_keyname]['$t'];

if labelled as % and value contains %, convert

        if (colTypes[col] == 'percent') {
+          if (rep.test(value)) {
+            var value2 = rep.exec(value);
+            var value3 = parseFloat(value2);
+            value = value3 / 100;
+          }
+        }
+        row.push(value);
+      }
+      results.data.push(row);
+    });
+    return results;
+  };

Convenience function to get GDocs JSON API Url from standard URL

  my.getSpreadsheetAPIUrl = function(url) {
+    if (url.indexOf('feeds/list') != -1) {
+      return url;
+    } else {

https://docs.google.com/spreadsheet/ccc?key=XXXX#gid=0

      var regex = /.*spreadsheet\/ccc?.*key=([^#?&+]+).*/;
+      var matches = url.match(regex);
+      if (matches) {
+        var key = matches[1];
+        var worksheet = 1;
+        var out = 'https://spreadsheets.google.com/feeds/list/' + key + '/' + worksheet + '/public/values?alt=json';
+        return out;
+      } else {
+        alert('Failed to extract gdocs key from ' + url);
+      }
+    }
+  };
+}(jQuery, this.recline.Backend.GDocs));
 
 
\ No newline at end of file diff --git a/docs/backend/memory.html b/docs/backend/memory.html index e1c865a7..a1a95921 100644 --- a/docs/backend/memory.html +++ b/docs/backend/memory.html @@ -1,5 +1,6 @@ - memory.js
e.layer.cid=e.properties.cid;} - });this.map.setView(newL.LatLng(0,0),2);this.mapReady=true; - },render:function(){vartmplData=this.model.toTemplateJSON();tmplData.views=this.pageViews; - vartemplate=$.mustache(this.template,tmplData); + vartemplate=Mustache.render(this.template,tmplData);$(this.el).html(template);var$dataViewContainer=this.el.find('.data-view-container');_.each(this.pageViews,function(view,pageName){ @@ -318,7 +341,7 @@ note this.model and dataset returned are the same

query:query,'view-graph':graphState,backend:this.model.backend.__type__, - dataset:this.model.toJSON(), + url:this.model.get('url'),currentView:null,readOnly:false}, @@ -363,19 +386,25 @@ flash object. Flash attributes (all are optional):

  • loader: if true show loading spinner
  • '); @@ -1355,7 +1359,7 @@ my.GridRow = Backbone.View.extend({ render: function() { this.el.attr('data-id', this.model.id); - var html = $.mustache(this.template, this.toTemplateJSON()); + var html = Mustache.render(this.template, this.toTemplateJSON()); $(this.el).html(html); return this; }, @@ -1383,7 +1387,7 @@ my.GridRow = Backbone.View.extend({ $(e.target).addClass("hidden"); var cell = $(e.target).siblings('.data-table-cell-value'); cell.data("previousContents", cell.text()); - var templated = $.mustache(this.cellEditorTemplate, {value: cell.text()}); + var templated = Mustache.render(this.cellEditorTemplate, {value: cell.text()}); cell.html(templated); }, @@ -1579,7 +1583,7 @@ my.Map = Backbone.View.extend({ var self = this; - htmls = $.mustache(this.template, this.model.toTemplateJSON()); + htmls = Mustache.render(this.template, this.model.toTemplateJSON()); $(this.el).html(htmls); this.$map = this.el.find('.panel.map'); @@ -1594,7 +1598,6 @@ my.Map = Backbone.View.extend({ $('#editor-field-type-latlon').attr('checked','checked').change(); } } - this.redraw(); return this; }, @@ -1764,8 +1767,11 @@ my.Map = Backbone.View.extend({ if (this.state.get('geomField')){ var value = doc.get(this.state.get('geomField')); if (typeof(value) === 'string'){ - // We have a GeoJSON string representation - return $.parseJSON(value); + // We *may* have a GeoJSON string representation + try { + return $.parseJSON(value); + } catch(e) { + } } else { // We assume that the contents of the field are a valid GeoJSON object return value; @@ -1903,6 +1909,417 @@ my.Map = Backbone.View.extend({ 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`. +my.SlickGrid = Backbone.View.extend({ + tagName: "div", + className: "recline-slickgrid", + + initialize: function(modelEtc) { + 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); + + 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; + this.el = $(this.el); + + 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 = []; + _.each(this.model.fields.toJSON(),function(field){ + var column = {id:field['id'], + name:field['label'], + field:field['id'], + sortable: true, + minWidth: 80}; + + 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.currentDocuments.toJSON(); + + this.grid = new Slick.Grid(this.el, data, visibleColumns, options); + + // Column sorting + var gridSorter = function(field, ascending, grid, data){ + + data.sort(function(a, b){ + var result = + a[field] > b[field] ? 1 : + a[field] < b[field] ? -1 : + 0 + ; + return ascending ? result : -result; + }); + + grid.setData(data); + grid.updateRowCount(); + grid.render(); + } + + var sortInfo = this.state.get('columnsSort'); + if (sortInfo){ + var sortAsc = !(sortInfo['direction'] == 'desc'); + gridSorter(sortInfo.column, sortAsc, self.grid, data); + this.grid.setSortColumn(sortInfo.column, sortAsc); + } + + this.grid.onSort.subscribe(function(e, args){ + gridSorter(args.sortCol.field,args.sortAsc,self.grid,data); + self.state.set({columnsSort:{ + column:args.sortCol, + direction: (args.sortAsc) ? 'asc':'desc' + }}); + }); + + 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 = $('

    memory.js

    this.recline = this.recline || {};
    +      memory.js           

    memory.js

    this.recline = this.recline || {};
     this.recline.Backend = this.recline.Backend || {};
    +this.recline.Backend.Memory = this.recline.Backend.Memory || {};
     
     (function($, my) {

    createDataset

    @@ -12,127 +13,66 @@ as per recline.Model.Field). If fields not specified they will be taken from the data. @param metadata: (optional) dataset metadata - see recline.Model.Dataset. If not defined (or id not provided) id will be autogenerated.

      my.createDataset = function(data, fields, metadata) {
    -    if (!metadata) {
    -      metadata = {};
    -    }
    -    if (!metadata.id) {
    -      metadata.id = String(Math.floor(Math.random() * 100000000) + 1);
    -    }
    -    var backend = new recline.Backend.Memory();
    -    var datasetInfo = {
    -      documents: data,
    -      metadata: metadata
    -    };
    +    var wrapper = new my.DataWrapper(data, fields);
    +    var backend = new my.Backbone();
    +    var dataset = new recline.Model.Dataset(metadata, backend);
    +    dataset._dataCache = wrapper;
    +    dataset.fetch();
    +    dataset.query();
    +    return dataset;
    +  };

    Data Wrapper

    + +

    Turn a simple array of JS objects into a mini data-store with +functionality like querying, faceting, updating (by ID) and deleting (by +ID).

      my.DataWrapper = function(data, fields) {
    +    var self = this;
    +    this.data = data;
         if (fields) {
    -      datasetInfo.fields = fields;
    +      this.fields = fields;
         } else {
           if (data) {
    -        datasetInfo.fields = _.map(data[0], function(value, key) {
    +        this.fields = _.map(data[0], function(value, key) {
               return {id: key};
             });
           }
         }
    -    backend.addDataset(datasetInfo);
    -    var dataset = new recline.Model.Dataset({id: metadata.id}, backend);
    -    dataset.fetch();
    -    dataset.query();
    -    return dataset;
    -  };

    Memory Backend - uses in-memory data

    -

    To use it you should provide in your constructor data:

    - -
      -
    • metadata (including fields array)
    • -
    • documents: list of hashes, each hash being one doc. A doc must have an id attribute which is unique.
    • -
    - -

    Example:

    - -

    - // Backend setup
    - var backend = recline.Backend.Memory();
    - backend.addDataset({
    -   metadata: {
    -     id: 'my-id',
    -     title: 'My Title'
    -   },
    -   fields: [{id: 'x'}, {id: 'y'}, {id: 'z'}],
    -   documents: [
    -       {id: 0, x: 1, y: 2, z: 3},
    -       {id: 1, x: 2, y: 4, z: 6}
    -     ]
    - });
    - // later ...
    - var dataset = Dataset({id: 'my-id'}, 'memory');
    - dataset.fetch();
    - etc ...
    - 

      my.Memory = my.Base.extend({
    -    __type__: 'memory',
    -    readonly: false,
    -    initialize: function() {
    -      this.datasets = {};
    -    },
    -    addDataset: function(data) {
    -      this.datasets[data.metadata.id] = $.extend(true, {}, data);
    -    },
    -    sync: function(method, model, options) {
    -      var self = this;
    -      var dfd = $.Deferred();
    -      if (method === "read") {
    -        if (model.__type__ == 'Dataset') {
    -          var rawDataset = this.datasets[model.id];
    -          model.set(rawDataset.metadata);
    -          model.fields.reset(rawDataset.fields);
    -          model.docCount = rawDataset.documents.length;
    -          dfd.resolve(model);
    +    this.update = function(doc) {
    +      _.each(self.data, function(internalDoc, idx) {
    +        if(doc.id === internalDoc.id) {
    +          self.data[idx] = doc;
             }
    -        return dfd.promise();
    -      } else if (method === 'update') {
    -        if (model.__type__ == 'Document') {
    -          _.each(self.datasets[model.dataset.id].documents, function(doc, idx) {
    -            if(doc.id === model.id) {
    -              self.datasets[model.dataset.id].documents[idx] = model.toJSON();
    -            }
    -          });
    -          dfd.resolve(model);
    -        }
    -        return dfd.promise();
    -      } else if (method === 'delete') {
    -        if (model.__type__ == 'Document') {
    -          var rawDataset = self.datasets[model.dataset.id];
    -          var newdocs = _.reject(rawDataset.documents, function(doc) {
    -            return (doc.id === model.id);
    -          });
    -          rawDataset.documents = newdocs;
    -          dfd.resolve(model);
    -        }
    -        return dfd.promise();
    -      } else {
    -        alert('Not supported: sync on Memory backend with method ' + method + ' and model ' + model);
    -      }
    -    },
    -    query: function(model, queryObj) {
    -      var dfd = $.Deferred();
    -      var out = {};
    -      var numRows = queryObj.size;
    -      var start = queryObj.from;
    -      var results = this.datasets[model.id].documents;
    +      });
    +    };
    +
    +    this.delete = function(doc) {
    +      var newdocs = _.reject(self.data, function(internalDoc) {
    +        return (doc.id === internalDoc.id);
    +      });
    +      this.data = newdocs;
    +    };
    +
    +    this.query = function(queryObj) {
    +      var numRows = queryObj.size || this.data.length;
    +      var start = queryObj.from || 0;
    +      var results = this.data;
           results = this._applyFilters(results, queryObj);
    -      results = this._applyFreeTextQuery(model, results, queryObj);

    not complete sorting!

          _.each(queryObj.sort, function(sortObj) {
    +      results = this._applyFreeTextQuery(results, queryObj);

    not complete sorting!

          _.each(queryObj.sort, function(sortObj) {
             var fieldName = _.keys(sortObj)[0];
             results = _.sortBy(results, function(doc) {
               var _out = doc[fieldName];
               return (sortObj[fieldName].order == 'asc') ? _out : -1*_out;
             });
           });
    -      out.facets = this._computeFacets(results, queryObj);
           var total = results.length;
    -      resultsObj = this._docsToQueryResult(results.slice(start, start+numRows));
    -      _.extend(out, resultsObj);
    -      out.total = total;
    -      dfd.resolve(out);
    -      return dfd.promise();
    -    },

    in place filtering

        _applyFilters: function(results, queryObj) {
    +      var facets = this.computeFacets(results, queryObj);
    +      results = results.slice(start, start+numRows);
    +      return {
    +        total: total,
    +        documents: results,
    +        facets: facets
    +      };
    +    };

    in place filtering

        this._applyFilters = function(results, queryObj) {
           _.each(queryObj.filters, function(filter) {
             results = _.filter(results, function(doc) {
               var fieldId = _.keys(filter.term)[0];
    @@ -140,14 +80,14 @@ If not defined (or id not provided) id will be autogenerated.

    }); }); return results; - },

    we OR across fields but AND across terms in query string

        _applyFreeTextQuery: function(dataset, results, queryObj) {
    +    };

    we OR across fields but AND across terms in query string

        this._applyFreeTextQuery = function(results, queryObj) {
           if (queryObj.q) {
             var terms = queryObj.q.split(' ');
             results = _.filter(results, function(rawdoc) {
               var matches = true;
               _.each(terms, function(term) {
                 var foundmatch = false;
    -            dataset.fields.each(function(field) {
    +            _.each(self.fields, function(field) {
                   var value = rawdoc[field.id];
                   if (value !== null) { value = value.toString(); }

    TODO regexes?

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

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

                });
    @@ -157,17 +97,16 @@ if (!matches) return false;

    }); } return results; - }, + }; - _computeFacets: function(documents, queryObj) { + this.computeFacets = function(documents, queryObj) { var facetResults = {}; if (!queryObj.facets) { - return facetsResults; + return facetResults; } - _.each(queryObj.facets, function(query, facetId) { - 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(documents, function(doc) {
    +      });

    faceting

          _.each(documents, function(doc) {
             _.each(queryObj.facets, function(query, facetId) {
               var fieldId = query.terms.field;
               var val = doc[fieldId];
    @@ -184,14 +123,59 @@ if (!matches) return false;

    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;
    -    }
    -  });
    +    };
    +  };
    +  

    Backbone

    -}(jQuery, this.recline.Backend)); +

    Backbone connector for memory store attached to a Dataset object

      my.Backbone = function() {
    +    this.__type__ = 'memory';
    +    this.sync = function(method, model, options) {
    +      var self = this;
    +      var dfd = $.Deferred();
    +      if (method === "read") {
    +        if (model.__type__ == 'Dataset') {
    +          model.fields.reset(model._dataCache.fields);
    +          dfd.resolve(model);
    +        }
    +        return dfd.promise();
    +      } else if (method === 'update') {
    +        if (model.__type__ == 'Document') {
    +          model.dataset._dataCache.update(model.toJSON());
    +          dfd.resolve(model);
    +        }
    +        return dfd.promise();
    +      } else if (method === 'delete') {
    +        if (model.__type__ == 'Document') {
    +          model.dataset._dataCache.delete(model.toJSON());
    +          dfd.resolve(model);
    +        }
    +        return dfd.promise();
    +      } else {
    +        alert('Not supported: sync on Memory backend with method ' + method + ' and model ' + model);
    +      }
    +    };
    +
    +    this.query = function(model, queryObj) {
    +      var dfd = $.Deferred();
    +      var results = model._dataCache.query(queryObj);
    +      var hits = _.map(results.documents, function(row) {
    +        return { _source: row };
    +      });
    +      var out = {
    +        total: results.total,
    +        hits: hits,
    +        facets: results.facets
    +      };
    +      dfd.resolve(out);
    +      return dfd.promise();
    +    };
    +  };
    +
    +}(jQuery, this.recline.Backend.Memory));
     
     
    \ No newline at end of file diff --git a/docs/model.html b/docs/model.html index 3009088a..2fd24970 100644 --- a/docs/model.html +++ b/docs/model.html @@ -115,7 +115,7 @@ also returned.

    if (recline && recline.Backend) { _.each(_.keys(recline.Backend), function(name) { if (name.toLowerCase() === backendString.toLowerCase()) { - backend = new recline.Backend[name](); + backend = new recline.Backend[name].Backbone(); } }); } @@ -133,19 +133,19 @@ Dataset is an Object like:

    // convenience - if url provided and dataste not this be used as dataset url url: {dataset url} ... -}

    my.Dataset.restore = function(state) {

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

      var dataset = null;
    -  if (state.url && !state.dataset) {
    -    state.dataset = {url: state.url};
    -  }
    -  if (state.backend === 'memory') {
    -    dataset = recline.Backend.createDataset(
    +}

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

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

      if (state.backend === 'memory') {
    +    dataset = recline.Backend.Memory.createDataset(
           [{stub: 'this is a stub dataset because we do not restore memory datasets'}],
           [],
           state.dataset // metadata
         );
       } else {
    +    var datasetInfo = {
    +      url: state.url
    +    };
         dataset = new recline.Model.Dataset(
    -      state.dataset,
    +      datasetInfo,
           state.backend
         );
       }
    @@ -183,7 +183,6 @@ for this document.

  • format: (optional) used to indicate how the data should be formatted. For example:
    • type=date, format=yyyy-mm-dd
    • type=float, format=percentage
    • -
    • type=string, format=link (render as hyperlink)
    • type=string, format=markdown (render as markdown if Showdown available)
  • is_derived: (default: false) attribute indicating this field has no backend data but is just derived from other fields (see below).
  • @@ -202,7 +201,18 @@ document, its signature and behaviour is the same as for renderer. Use of this function allows you to define an entirely new value for data in this field. This provides support for a) 'derived/computed' fields: i.e. fields whose data are functions of the data in other fields b) transforming the -value of this field prior to rendering.

    my.Field = Backbone.Model.extend({

    defaults - define default values

      defaults: {
    +value of this field prior to rendering.

    + +

    Default renderers

    + +
      +
    • string +
      • no format provided: pass through but convert http:// to hyperlinks
      • +
      • format = plain: do no processing on the source text
      • +
      • format = markdown: process as markdown (if Showdown library available)
    • +
    • float +
      • format = percentage: format as a percentage
    • +
    my.Field = Backbone.Model.extend({

    defaults - define default values

      defaults: {
         label: null,
         type: 'string',
         format: null,
    @@ -238,9 +248,7 @@ value of this field prior to rendering.

    }, 'string': function(val, field, doc) { var format = field.get('format'); - if (format === 'link') { - return '<a href="VAL">VAL</a>'.replace(/VAL/g, val); - } else if (format === 'markdown') { + if (format === 'markdown') { if (typeof Showdown !== 'undefined') { var showdown = new Showdown.converter(); out = showdown.makeHtml(val); @@ -248,15 +256,21 @@ value of this field prior to rendering.

    } else { return val; } + } else if (format == 'plain') { + return val; + } 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>');
    +        }
    +        return val
           }
    -      return val;
         }
       }
     });
     
     my.FieldList = Backbone.Collection.extend({
       model: my.Field
    -});

    Query

    +});

    Query

    Query instances encapsulate a query to the backend (see query method on backend). Useful both @@ -312,10 +326,10 @@ execution.

    return { size: 100, from: 0, - facets: {},

    http://www.elasticsearch.org/guide/reference/query-dsl/and-filter.html + facets: {},

    http://www.elasticsearch.org/guide/reference/query-dsl/and-filter.html , filter: {}

          filters: []
         };
    -  },

    addTermFilter

    + },

    addTermFilter

    Set (update or add) a terms filter to filters

    @@ -324,23 +338,23 @@ execution.

    var filter = { term: {} }; filter.term[fieldId] = value; filters.push(filter); - this.set({filters: filters});

    change does not seem to be triggered automatically

        if (value) {
    +    this.set({filters: filters});

    change does not seem to be triggered automatically

        if (value) {
           this.trigger('change');
    -    } else {

    adding a new blank filter and do not want to trigger a new query

          this.trigger('change:filters:new-blank');
    +    } else {

    adding a new blank filter and do not want to trigger a new query

          this.trigger('change:filters:new-blank');
         }
    -  },

    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] = {
    @@ -360,7 +374,7 @@ execution.

    this.set({facets: facets}, {silent: true}); this.trigger('facet:add', this); } -});

    A Facet (Result)

    +});

    A Facet (Result)

    Object to store Facet information, that is summary information (e.g. values and counts) about a field obtained by some faceting method on the @@ -407,14 +421,16 @@ key used to specify this facet in the facet query):

    terms: [] }; } -});

    A Collection/List of Facets

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

    A Collection/List of Facets

    my.FacetList = Backbone.Collection.extend({
       model: my.Facet
    -});

    Object State

    +});

    Object State

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

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

    Backend registry

    +});

    Backbone.sync

    -

    Backends will register themselves by id into this registry

    my.backends = {};
    +

    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);
    +};
     
     }(jQuery, this.recline.Model));
     
    diff --git a/docs/view-graph.html b/docs/view-graph.html
    index e71795ba..2f457a7f 100644
    --- a/docs/view-graph.html
    +++ b/docs/view-graph.html
    @@ -26,13 +26,6 @@ generate the element itself (you can then append view.el to the DOM.

    template: ' \ <div class="editor"> \ - <div class="editor-info editor-hide-info"> \ - <h3 class="action-toggle-help">Help &raquo;</h3> \ - <p>To create a chart select a column (group) to use as the x-axis \ - then another column (Series A) to plot against it.</p> \ - <p>You can add add \ - additional series by clicking the "Add series" button</p> \ - </div> \ <form class="form-stacked"> \ <div class="clearfix"> \ <label>Graph Type</label> \ @@ -92,8 +85,7 @@ generate the element itself (you can then append view.el to the DOM.

    events: { 'change form select': 'onEditorSubmit', 'click .editor-add': '_onAddSeries', - 'click .action-remove-series': 'removeSeries', - 'click .action-toggle-help': 'toggleHelp' + 'click .action-remove-series': 'removeSeries' }, initialize: function(options) { @@ -123,7 +115,7 @@ generate the element itself (you can then append view.el to the DOM.

    render: function() { var self = this; var tmplData = this.model.toTemplateJSON(); - var htmls = $.mustache(this.template, tmplData); + var htmls = Mustache.render(this.template, tmplData); $(this.el).html(htmls); this.$graph = this.el.find('.panel.graph');

    set up editor from state

        if (this.state.get('graphType')) {
           this._selectOption('.editor-type', this.state.get('graphType'));
    @@ -342,7 +334,7 @@ have no field type info). Thus at present we only do this for bars.

    seriesName: String.fromCharCode(idx + 64 + 1), }, this.model.toTemplateJSON()); - var htmls = $.mustache(this.templateSeriesEditor, data); + var htmls = Mustache.render(this.templateSeriesEditor, data); this.el.find('.editor-series-group').append(htmls); return this; }, @@ -357,11 +349,7 @@ have no field type info). Thus at present we only do this for bars.

    var $el = $(e.target); $el.parent().parent().remove(); this.onEditorSubmit(); - }, - - toggleHelp: function() { - this.el.find('.editor-info').toggleClass('editor-hide-info'); - }, + } }); })(jQuery, recline.View); diff --git a/docs/view-grid.html b/docs/view-grid.html index 2c57e2cc..af6d95e9 100644 --- a/docs/view-grid.html +++ b/docs/view-grid.html @@ -14,7 +14,7 @@ initialize: function(modelEtc) { var self = this; this.el = $(this.el); - _.bindAll(this, 'render'); + _.bindAll(this, 'render', 'onHorizontalScroll'); this.model.currentDocuments.bind('add', this.render); this.model.currentDocuments.bind('reset', this.render); this.model.currentDocuments.bind('remove', this.render); @@ -30,8 +30,8 @@ 'click .column-header-menu .data-table-menu li a': 'onColumnHeaderClick', 'click .row-header-menu': 'onRowHeaderClick', 'click .root-header-menu': 'onRootHeaderClick', - 'click .data-table-menu li a': 'onMenuClick' - },

    ====================================================== + 'click .data-table-menu li a': 'onMenuClick',

    does not work here so done at end of render function +'scroll .recline-grid tbody': 'onHorizontalScroll'

      },

    ====================================================== Column and row menus

      onColumnHeaderClick: function(e) {
         this.tempState.currentColumn = $(e.target).closest('.column-header').attr('data-field');
       },
    @@ -45,7 +45,7 @@ Column and row menus

    {{#columns}} \ <li><a data-action="showColumn" data-column="{{.}}" href="JavaScript:void(0);">Show column: {{.}}</a></li> \ {{/columns}}'; - var tmp = $.mustache(tmpl, {'columns': this.state.get('hiddenFields')}); + var tmp = Mustache.render(tmpl, {'columns': this.state.get('hiddenFields')}); this.el.find('.root-header-menu .dropdown-menu').html(tmp); }, @@ -69,7 +69,7 @@ Column and row menus

    showColumn: function() { self.showColumn(e); }, deleteRow: function() { var self = this; - var doc = _.find(self.model.currentDocuments.models, function(doc) {

    important this is == as the currentRow will be string (as comes + var doc = _.find(self.model.currentDocuments.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() { 
    @@ -87,7 +87,7 @@ from DOM) while id may be int

    var self = this; var view = new my.ColumnTransform({ model: this.model - });

    pass the flash message up the chain

        view.bind('recline:flash', function(flash) {
    +    });

    pass the flash message up the chain

        view.bind('recline:flash', function(flash) {
           self.trigger('recline:flash', flash);
         });
         view.state = this.tempState;
    @@ -105,7 +105,7 @@ from DOM) while id may be int

    hideColumn: function() { var hiddenFields = this.state.get('hiddenFields'); hiddenFields.push(this.tempState.currentColumn); - this.state.set({hiddenFields: hiddenFields});

    change event not being triggered (because it is an array?) so trigger manually

        this.state.trigger('change');
    +    this.state.set({hiddenFields: hiddenFields});

    change event not being triggered (because it is an array?) so trigger manually

        this.state.trigger('change');
         this.render();
       },
       
    @@ -113,11 +113,17 @@ from DOM) while id may be int

    var hiddenFields = _.without(this.state.get('hiddenFields'), $(e.target).data('column')); this.state.set({hiddenFields: hiddenFields}); this.render(); - },

    ======================================================

    + }, + + onHorizontalScroll: function(e) { + var currentScroll = $(e.target).scrollLeft(); + this.el.find('.recline-grid thead tr').scrollLeft(currentScroll); + },

    ======================================================

    Templating

      template: ' \
    +    <div class="table-container"> \
         <table class="recline-grid table-striped table-condensed" cellspacing="0"> \
    -      <thead> \
    +      <thead class="fixed-header"> \
             <tr> \
               {{#notEmpty}} \
                 <th class="column-header"> \
    @@ -130,7 +136,7 @@ from DOM) while id may be int

    </th> \ {{/notEmpty}} \ {{#fields}} \ - <th class="column-header {{#hidden}}hidden{{/hidden}}" data-field="{{id}}"> \ + <th class="column-header {{#hidden}}hidden{{/hidden}}" data-field="{{id}}" style="width: {{width}}px; max-width: {{width}}px; min-width: {{width}}px;"> \ <div class="btn-group column-header-menu"> \ <a class="btn dropdown-toggle" data-toggle="dropdown"><i class="icon-cog"></i><span class="caret"></span></a> \ <ul class="dropdown-menu data-table-menu pull-right"> \ @@ -149,15 +155,20 @@ from DOM) while id may be int

    <span class="column-header-name">{{label}}</span> \ </th> \ {{/fields}} \ + <th class="last-header" style="width: {{lastHeaderWidth}}px; max-width: {{lastHeaderWidth}}px; min-width: {{lastHeaderWidth}}px; padding: 0; margin: 0;"></th> \ </tr> \ </thead> \ - <tbody></tbody> \ + <tbody class="scroll-content"></tbody> \ </table> \ + </div> \ ', toTemplateJSON: function() { + var self = this; var modelData = this.model.toJSON(); - modelData.notEmpty = ( this.fields.length > 0 );

    TODO: move this sort of thing into a toTemplateJSON method on Dataset?

        modelData.fields = _.map(this.fields, function(field) { return field.toJSON(); });
    +    modelData.notEmpty = ( this.fields.length > 0 );

    TODO: move this sort of thing into a toTemplateJSON method on Dataset?

        modelData.fields = _.map(this.fields, function(field) {
    +      return field.toJSON();
    +    });

    last header width = scroll bar - border (2px) */

        modelData.lastHeaderWidth = this.scrollbarDimensions.width - 2;
         return modelData;
       },
       render: function() {
    @@ -165,7 +176,16 @@ from DOM) while id may be int

    this.fields = this.model.fields.filter(function(field) { return _.indexOf(self.state.get('hiddenFields'), field.id) == -1; }); - var htmls = $.mustache(this.template, this.toTemplateJSON()); + this.scrollbarDimensions = this.scrollbarDimensions || this._scrollbarSize(); // skip measurement if already have dimensions + var numFields = this.fields.length;

    compute field widths (-20 for first menu col + 10px for padding on each col and finally 16px for the scrollbar)

        var fullWidth = self.el.width() - 20 - 10 * numFields - this.scrollbarDimensions.width;
    +    var width = parseInt(Math.max(50, fullWidth / numFields));

    if columns extend outside viewport then remainder is 0

        var remainder = Math.max(fullWidth - numFields * width,0);
    +    _.each(this.fields, function(field, idx) {

    add the remainder to the first field width so we make up full col

          if (idx == 0) {
    +        field.set({width: width+remainder});
    +      } else {
    +        field.set({width: width});
    +      }
    +    });
    +    var htmls = Mustache.render(this.template, this.toTemplateJSON());
         this.el.html(htmls);
         this.model.currentDocuments.forEach(function(doc) {
           var tr = $('<tr />');
    @@ -176,11 +196,24 @@ from DOM) while id may be int

    fields: self.fields }); newView.render(); - }); + });

    hide extra header col if no scrollbar to avoid unsightly overhang

        var $tbody = this.el.find('tbody')[0];
    +    if ($tbody.scrollHeight <= $tbody.offsetHeight) {
    +      this.el.find('th.last-header').hide();
    +    }
         this.el.find('.recline-grid').toggleClass('no-hidden', (self.state.get('hiddenFields').length === 0));
    +    this.el.find('.recline-grid tbody').scroll(this.onHorizontalScroll);
         return this;
    +  },

    _scrollbarSize

    + +

    Measure width of a vertical scrollbar and height of a horizontal scrollbar.

    + +

    @return: { width: pixelWidth, height: pixelHeight }

      _scrollbarSize: function() {
    +    var $c = $("<div style='position:absolute; top:-10000px; left:-10000px; width:100px; height:100px; overflow:scroll;'></div>").appendTo("body");
    +    var dim = { width: $c.width() - $c[0].clientWidth + 1, height: $c.height() - $c[0].clientHeight };
    +    $c.remove();
    +    return dim;
       }
    -});

    GridRow View for rendering an individual document.

    +});

    GridRow View for rendering an individual document.

    Since we want this to update in place it is up to creator to provider the element to attach to.

    @@ -212,7 +245,7 @@ var row = new GridRow({ </div> \ </td> \ {{#cells}} \ - <td data-field="{{field}}"> \ + <td data-field="{{field}}" style="width: {{width}}px; max-width: {{width}}px; min-width: {{width}}px;"> \ <div class="data-table-cell-content"> \ <a href="javascript:{}" class="data-table-cell-edit" title="Edit this cell">&nbsp;</a> \ <div class="data-table-cell-value">{{{value}}}</div> \ @@ -232,6 +265,7 @@ var row = new GridRow({ var cellData = this._fields.map(function(field) { return { field: field.id, + width: field.get('width'), value: doc.getFieldValue(field) }; }); @@ -240,10 +274,10 @@ var row = new GridRow({ render: function() { this.el.attr('data-id', this.model.id); - var html = $.mustache(this.template, this.toTemplateJSON()); + var html = Mustache.render(this.template, this.toTemplateJSON()); $(this.el).html(html); return this; - },

    =================== + },

    =================== Cell Editor methods

      cellEditorTemplate: ' \
         <div class="menu-container data-table-cell-editor"> \
           <textarea class="data-table-cell-editor-editor" bind="textarea">{{value}}</textarea> \
    @@ -264,7 +298,7 @@ Cell Editor methods

    $(e.target).addClass("hidden"); var cell = $(e.target).siblings('.data-table-cell-value'); cell.data("previousContents", cell.text()); - var templated = $.mustache(this.cellEditorTemplate, {value: cell.text()}); + var templated = Mustache.render(this.cellEditorTemplate, {value: cell.text()}); cell.html(templated); }, diff --git a/docs/view-map.html b/docs/view-map.html index e85e46e3..bca8e38b 100644 --- a/docs/view-map.html +++ b/docs/view-map.html @@ -83,7 +83,7 @@ have the following (optional) configuration options:

    </div> \ <div class="panel map"> \ </div> \ -',

    These are the default field names that will be used if found. +',

    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: ['geom','the_geom','geometry','spatial','location'],

    Define here events for UI elements

      events: {
    @@ -135,13 +135,13 @@ to display properly

    this.autoZoom = true; this.mapReady = false; this.render(); - },

    Public: Adds the necessary elements to the page.

    + },

    Public: Adds the necessary elements to the page.

    Also sets up the editor fields and the map if necessary.

      render: function() {
     
         var self = this;
     
    -    htmls = $.mustache(this.template, this.model.toTemplateJSON());
    +    htmls = Mustache.render(this.template, this.model.toTemplateJSON());
     
         $(this.el).html(htmls);
         this.$map = this.el.find('.panel.map');
    @@ -156,20 +156,8 @@ to display properly

    $('#editor-field-type-latlon').attr('checked','checked').change(); } } - - this.model.bind('query:done', function() { - if (!self.geomReady){ - self._setupGeometryField(); - } - - if (!self.mapReady){ - self._setupMap(); - } - self.redraw(); - }); - return this; - },

    Public: Redraws the features on the map according to the action provided

    + },

    Public: Redraws the features on the map according to the action provided

    Actions can be:

    @@ -178,22 +166,25 @@ to display properly

  • add: Add one or n features (documents)
  • remove: Remove one or n features (documents)
  • refresh: Clear existing features and add all current documents
  • -
      redraw: function(action,doc){
    +             
      redraw: function(action, doc){
         var self = this;
    -    action = action || 'refresh';
    +    action = action || 'refresh';

    try to set things up if not already

        if (!self.geomReady){
    +      self._setupGeometryField();
    +    }
    +    if (!self.mapReady){
    +      self._setupMap();
    +    }
     
         if (this.geomReady && this.mapReady){
    -      if (action == 'reset'){
    +      if (action == 'reset' || action == 'refresh'){
             this.features.clearLayers();
    +        this._add(this.model.currentDocuments.models);
           } else if (action == 'add' && doc){
             this._add(doc);
           } else if (action == 'remove' && doc){
             this._remove(doc);
    -      } else if (action == 'refresh'){
    -        this.features.clearLayers();
    -        this._add(this.model.currentDocuments.models);
           }
    -      if (action != 'reset' && this.autoZoom){
    +      if (this.autoZoom){
             if (this.visible){
               this._zoomToFeatures();
             } else {
    @@ -201,7 +192,7 @@ to display properly

    } } } - },

    UI Event handlers

    Public: Update map with user options

    + },

    UI Event handlers

    Public: Update map with user options

    Right now the only configurable option is what field(s) contains the location information.

      onEditorSubmit: function(e){
    @@ -223,7 +214,7 @@ location information.

    this.redraw(); return false; - },

    Public: Shows the relevant select lists depending on the location field + },

    Public: Shows the relevant select lists depending on the location field type selected.

      onFieldTypeChange: function(e){
         if (e.target.value == 'geom'){
             $('.editor-field-type-geom').show();
    @@ -236,7 +227,7 @@ type selected.

    onAutoZoomChange: function(e){ this.autoZoom = !this.autoZoom; - },

    Private: Add one or n features to the map

    + },

    Private: Add one or n features to the map

    For each document passed, a GeoJSON geometry will be extracted and added to the features layer. If an exception is thrown, the process will be @@ -252,15 +243,15 @@ stopped and an error notification shown.

    _.every(docs,function(doc){ count += 1; var feature = self._getGeometryFromDocument(doc); - if (typeof feature === 'undefined' || feature === null){

    Empty field

            return true;
    -      } else if (feature instanceof Object){

    Build popup contents + if (typeof feature === 'undefined' || feature === null){

    Empty field

            return true;
    +      } else if (feature instanceof Object){

    Build popup contents TODO: mustache?

            html = ''
             for (key in doc.attributes){
               if (!(self.state.get('geomField') && key == self.state.get('geomField'))){
                 html += '<div><strong>' + key + '</strong>: '+ doc.attributes[key] + '</div>';
               }
             }
    -        feature.properties = {popupContent: html};

    Add a reference to the model id, which will allow us to + feature.properties = {popupContent: html};

    Add a reference to the model id, which will allow us to link this Leaflet layer to a Recline doc

            feature.properties.cid = doc.cid;
     
             try {
    @@ -281,7 +272,7 @@ link this Leaflet layer to a Recline doc

    } return true; }); - },

    Private: Remove one or n features to the map

      _remove: function(docs){
    +  },

    Private: Remove one or n features to the map

      _remove: function(docs){
     
         var self = this;
     
    @@ -295,14 +286,17 @@ link this Leaflet layer to a Recline doc

    } }); - },

    Private: Return a GeoJSON geomtry extracted from the document fields

      _getGeometryFromDocument: function(doc){
    +  },

    Private: Return a GeoJSON geomtry extracted from the document fields

      _getGeometryFromDocument: function(doc){
         if (this.geomReady){
           if (this.state.get('geomField')){
             var value = doc.get(this.state.get('geomField'));
    -        if (typeof(value) === 'string'){

    We have a GeoJSON string representation

              return $.parseJSON(value);
    -        } else {

    We assume that the contents of the field are a valid GeoJSON object

              return value;
    +        if (typeof(value) === 'string'){

    We may have a GeoJSON string representation

              try {
    +            return $.parseJSON(value);
    +          } catch(e) {
    +          }
    +        } else {

    We assume that the 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'));
    +      } 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'));
             var lat = doc.get(this.state.get('latField'));
             if (!isNaN(parseFloat(lon)) && !isNaN(parseFloat(lat))) {
               return {
    @@ -313,12 +307,12 @@ link this Leaflet layer to a Recline doc

    } return null; } - },

    Private: Check if there is a field with GeoJSON geometries or alternatively, + },

    Private: Check if there is a field with GeoJSON geometries or alternatively, two fields with lat/lon values.

    If not found, the user can define them via the UI form.

      _setupGeometryField: function(){
         var geomField, latField, lonField;
    -    this.geomReady = (this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField')));

    should not overwrite if we have already set this (e.g. explicitly via state)

        if (!this.geomReady) {
    +    this.geomReady = (this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField')));

    should not overwrite if we have already set this (e.g. explicitly via state)

        if (!this.geomReady) {
           this.state.set({
             geomField: this._checkField(this.geometryFieldNames),
             latField: this._checkField(this.latitudeFieldNames),
    @@ -326,7 +320,7 @@ two fields with lat/lon values.

    }); this.geomReady = (this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField'))); } - },

    Private: Check if a field in the current model exists in the provided + },

    Private: Check if a field in the current model exists in the provided list of names.

      _checkField: function(fieldNames){
         var field;
         var modelFieldNames = this.model.fields.pluck('id');
    @@ -337,7 +331,7 @@ list of names.

    } } return null; - },

    Private: Zoom to map to current features extent if any, or to the full + },

    Private: Zoom to map to current features extent if any, or to the full extent if none.

      _zoomToFeatures: function(){
         var bounds = this.features.getBounds();
         if (bounds){
    @@ -345,7 +339,7 @@ extent if none.

    } else { this.map.setView(new L.LatLng(0, 0), 2); } - },

    Private: Sets up the Leaflet map control and the features layer.

    + },

    Private: Sets up the Leaflet map control and the features layer.

    The map uses a base layer from MapQuest based on OpenStreetMap.

      _setupMap: function(){
    @@ -366,7 +360,7 @@ on OpenStreetMap.

    This will be available in the next Leaflet stable release. + });

    This will be available in the next Leaflet stable release. In the meantime we add it manually to our layer.

        this.features.getBounds = function(){
           var bounds = new L.LatLngBounds();
           this._iterateLayers(function (layer) {
    @@ -387,7 +381,7 @@ In the meantime we add it manually to our layer.

    Private: Helper function to select an option from a select list

      _selectOption: function(id,value){
    +  },

    Private: Helper function to select an option from a select list

      _selectOption: function(id,value){
         var options = $('.' + id + ' > select > option');
         if (options){
           options.each(function(opt){
    diff --git a/docs/view.html b/docs/view.html
    index dcb33dd2..317e2ada 100644
    --- a/docs/view.html
    +++ b/docs/view.html
    @@ -74,6 +74,23 @@ url or elsewhere and reloading of application from a stored state.

    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.

    @@ -200,6 +217,13 @@ initialized the DataExplorer with the relevant views themselves.

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

    these must be called after pageViews are created

        this.render();
         this._bindStateChanges();
    @@ -213,12 +237,11 @@ initialized the DataExplorer with the relevant views themselves.

    } this.model.bind('query:start', function() { - self.notify({message: 'Loading data', loader: true}); + self.notify({loader: true, persist: true}); }); this.model.bind('query:done', function() { self.clearNotifications(); self.el.find('.doc-count').text(self.model.docCount || 'Unknown'); - self.notify({message: 'Data loaded', category: 'success'}); }); this.model.bind('query:fail', function(error) { self.clearNotifications(); @@ -253,7 +276,7 @@ note this.model and dataset returned are the same

      notify: function(flash) {
         var tmplData = _.extend({
    -      message: '',
    -      category: 'warning'
    +      message: 'Loading',
    +      category: 'warning',
    +      loader: false
           },
           flash
         );
    -    var _template = ' \
    -      <div class="alert alert-{{category}} fade in" data-alert="alert"><a class="close" data-dismiss="alert" href="#">×</a> \
    -        {{message}} \
    -          {{#loader}} \
    +    if (tmplData.loader) {
    +      var _template = ' \
    +        <div class="alert alert-info alert-loader"> \
    +          {{message}} \
               <span class="notification-loader">&nbsp;</span> \
    -          {{/loader}} \
    -      </div>';
    -    var _templated = $.mustache(_template, tmplData); 
    +        </div>';
    +    } else {
    +      var _template = ' \
    +        <div class="alert alert-{{category}} fade in" data-alert="alert"><a class="close" data-dismiss="alert" href="#">×</a> \
    +          {{message}} \
    +        </div>';
    +    }
    +    var _templated = $(Mustache.render(_template, tmplData));
         _templated = $(_templated).appendTo($('.recline-data-explorer .alert-messages'));
         if (!flash.persist) {
           setTimeout(function() {
    @@ -388,7 +417,9 @@ flash object. Flash attributes (all are optional):

    Clear all existing notifications

      clearNotifications: function() {
         var $notifications = $('.recline-data-explorer .alert-messages .alert');
    -    $notifications.remove();
    +    $notifications.fadeOut(1500, function() {
    +      $(this).remove();
    +    });
       }
     });

    DataExplorer.restore

    @@ -452,7 +483,7 @@ flash object. Flash attributes (all are optional):

    render: function() { var tmplData = this.model.toJSON(); tmplData.to = this.model.get('from') + this.model.get('size'); - var templated = $.mustache(this.template, tmplData); + var templated = Mustache.render(this.template, tmplData); this.el.html(templated); } }); @@ -518,7 +549,7 @@ flash object. Flash attributes (all are optional):

    value: filter.term[fieldId] }; }); - var out = $.mustache(this.template, tmplData); + var out = Mustache.render(this.template, tmplData); this.el.html(out);

    are there actually any facets to show?

        if (this.model.get('filters').length > 0) {
           this.el.show();
         } else {
    @@ -601,7 +632,7 @@ flash object. Flash attributes (all are optional):

    } return facet; }); - var templated = $.mustache(this.template, tmplData); + 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 {
    diff --git a/library.html b/library.html
    index 6742e859..7d654861 100644
    --- a/library.html
    +++ b/library.html
    @@ -147,12 +147,12 @@ title: Library - Home
           
         
    diff --git a/recline.js b/recline.js index 5672ee47..51a9bc23 100644 --- a/recline.js +++ b/recline.js @@ -201,7 +201,7 @@ my.Dataset = Backbone.Model.extend({ if (recline && recline.Backend) { _.each(_.keys(recline.Backend), function(name) { if (name.toLowerCase() === backendString.toLowerCase()) { - backend = new recline.Backend[name](); + backend = new recline.Backend[name].Backbone(); } }); } @@ -224,20 +224,20 @@ my.Dataset = Backbone.Model.extend({ // ... // } my.Dataset.restore = function(state) { - // hack-y - restoring a memory dataset does not mean much ... var dataset = null; - if (state.url && !state.dataset) { - state.dataset = {url: state.url}; - } + // hack-y - restoring a memory dataset does not mean much ... if (state.backend === 'memory') { - dataset = recline.Backend.createDataset( + dataset = recline.Backend.Memory.createDataset( [{stub: 'this is a stub dataset because we do not restore memory datasets'}], [], state.dataset // metadata ); } else { + var datasetInfo = { + url: state.url + }; dataset = new recline.Model.Dataset( - state.dataset, + datasetInfo, state.backend ); } @@ -285,7 +285,6 @@ my.DocumentList = Backbone.Collection.extend({ // * format: (optional) used to indicate how the data should be formatted. For example: // * type=date, format=yyyy-mm-dd // * type=float, format=percentage -// * type=string, format=link (render as hyperlink) // * type=string, format=markdown (render as markdown if Showdown available) // * is_derived: (default: false) attribute indicating this field has no backend data but is just derived from other fields (see below). // @@ -304,6 +303,15 @@ my.DocumentList = Backbone.Collection.extend({ // field. This provides support for a) 'derived/computed' fields: i.e. fields // whose data are functions of the data in other fields b) transforming the // value of this field prior to rendering. +// +// #### Default renderers +// +// * string +// * no format provided: pass through but convert http:// to hyperlinks +// * format = plain: do no processing on the source text +// * format = markdown: process as markdown (if Showdown library available) +// * float +// * format = percentage: format as a percentage my.Field = Backbone.Model.extend({ // ### defaults - define default values defaults: { @@ -346,9 +354,7 @@ my.Field = Backbone.Model.extend({ }, 'string': function(val, field, doc) { var format = field.get('format'); - if (format === 'link') { - return 'VAL'.replace(/VAL/g, val); - } else if (format === 'markdown') { + if (format === 'markdown') { if (typeof Showdown !== 'undefined') { var showdown = new Showdown.converter(); out = showdown.makeHtml(val); @@ -356,8 +362,16 @@ my.Field = Backbone.Model.extend({ } else { return val; } + } else if (format == 'plain') { + return val; + } 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, '$1'); + } + return val } - return val; } } }); @@ -547,10 +561,12 @@ my.ObjectState = Backbone.Model.extend({ }); -// ## Backend registry +// ## Backbone.sync // -// Backends will register themselves by id into this registry -my.backends = {}; +// 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); +}; }(jQuery, this.recline.Model)); @@ -662,13 +678,6 @@ my.Graph = Backbone.View.extend({ template: ' \
    \ -
    \ -

    Help »

    \ -

    To create a chart select a column (group) to use as the x-axis \ - then another column (Series A) to plot against it.

    \ -

    You can add add \ - additional series by clicking the "Add series" button

    \ -
    \
    \
    \ \ @@ -728,8 +737,7 @@ my.Graph = Backbone.View.extend({ events: { 'change form select': 'onEditorSubmit', 'click .editor-add': '_onAddSeries', - 'click .action-remove-series': 'removeSeries', - 'click .action-toggle-help': 'toggleHelp' + 'click .action-remove-series': 'removeSeries' }, initialize: function(options) { @@ -763,7 +771,7 @@ my.Graph = Backbone.View.extend({ render: function() { var self = this; var tmplData = this.model.toTemplateJSON(); - var htmls = $.mustache(this.template, tmplData); + var htmls = Mustache.render(this.template, tmplData); $(this.el).html(htmls); this.$graph = this.el.find('.panel.graph'); @@ -1018,7 +1026,7 @@ my.Graph = Backbone.View.extend({ seriesName: String.fromCharCode(idx + 64 + 1), }, this.model.toTemplateJSON()); - var htmls = $.mustache(this.templateSeriesEditor, data); + var htmls = Mustache.render(this.templateSeriesEditor, data); this.el.find('.editor-series-group').append(htmls); return this; }, @@ -1036,11 +1044,7 @@ my.Graph = Backbone.View.extend({ var $el = $(e.target); $el.parent().parent().remove(); this.onEditorSubmit(); - }, - - toggleHelp: function() { - this.el.find('.editor-info').toggleClass('editor-hide-info'); - }, + } }); })(jQuery, recline.View); @@ -1100,7 +1104,7 @@ my.Grid = Backbone.View.extend({ {{#columns}} \
  • Show column: {{.}}
  • \ {{/columns}}'; - var tmp = $.mustache(tmpl, {'columns': this.state.get('hiddenFields')}); + var tmp = Mustache.render(tmpl, {'columns': this.state.get('hiddenFields')}); this.el.find('.root-header-menu .dropdown-menu').html(tmp); }, @@ -1258,7 +1262,7 @@ my.Grid = Backbone.View.extend({ field.set({width: width}); } }); - var htmls = $.mustache(this.template, this.toTemplateJSON()); + var htmls = Mustache.render(this.template, this.toTemplateJSON()); this.el.html(htmls); this.model.currentDocuments.forEach(function(doc) { var tr = $('