From 6d8a0a27006a8203902267944184c2db36c89800 Mon Sep 17 00:00:00 2001 From: rgrp Date: Thu, 10 Nov 2011 23:07:53 +0000 Subject: [PATCH 1/7] [index.html][s]: simple index files for gh pages. --- index.html | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 index.html diff --git a/index.html b/index.html new file mode 100644 index 00000000..6e02ce59 --- /dev/null +++ b/index.html @@ -0,0 +1,53 @@ + + + + + Recline DataExplorer + + + + +
+
+ +

This is a documentation page for the Recline DataExplorer project.

+

Demos

+ + +

Docs

+

Coming soon!

+
+
+ + From 69e020254e020683419bd042113fab87c200df93 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Thu, 5 Jan 2012 16:39:20 +0000 Subject: [PATCH 2/7] [doc][s]: update project home page in minor ways. --- index.html | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index 6e02ce59..22119d1c 100644 --- a/index.html +++ b/index.html @@ -36,10 +36,16 @@
-

This is a documentation page for the Recline DataExplorer project.

+

Recline DataExplorer is a spreadsheet plus Google Refine plus + visualization toolkit, all in pure javascript and html.

+

Designed for standalone use or as a library to integrate into your own + app.

+

More information can be found in the project README on Recline DataExplorer GitHub + project page.

Demos

  • Demo with local data
  • From b3664e5912084524c0da2ccca2b083f926bca2c3 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Thu, 26 Jan 2012 12:04:37 +0000 Subject: [PATCH 3/7] [doc][m]: rework home page adding significant content. * Features * History * Put in Bootstrap topbar and Github fork me --- index.html | 56 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/index.html b/index.html index 22119d1c..803af84f 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ Recline DataExplorer - + +Fork me on GitHub +
    -

    Recline DataExplorer is a spreadsheet plus Google Refine plus - visualization toolkit, all in pure javascript and html.

    -

    Designed for standalone use or as a library to integrate into your own - app.

    -

    More information can be found in the project README on Recline DataExplorer GitHub - project page.

    -

    Demos

    +

    Recline combines a data grid, Google Refine-style data transforms + and visualizations all in lightweight javascript and html.

    +

    Designed for standalone use or as a library to integrate into your own + app.

    + +

    Main Features

    +
      +
    • Open-source (and heavy reuser of existing open-source libraries like Backbone)
    • +
    • Pure javascript (no Flash) and designed for integration -- so it is easy to embed in other sites and applications
    • +
    • View and edit your data in a clean grid interface
    • +
    • Bulk update/clean your data using an easy scripting UI
    • +
    • Easily extensible with new Backends so you can connect to your database or storage layer
    • +
    • Visualize data
    • +
    + +

    Demo

    Docs

    Coming soon!

    + +

    History

    +

    Max Ogden was developing Recline as the frontend data browser and editor his http://datacouch.com/ project. Meanwhile, Rufus Pollock and the CKAN team at the Open Knowledge Foundation had been working on a Data Explorer for use in the DataHub and CKAN software.

    +

    When they met up, they realized that they were pretty much working on the same thing and so decided to join forces to produce the new Recline Data Explorer.

    +

    The new project forked off Max's original recline codebase combining some portions of the original Data Explorer. However, it was rapidly rewritten from the ground up using Backbone.

    +
    From 8115236d1a7ea93b653525cdc445377bcab8c0c4 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Thu, 26 Jan 2012 15:19:07 +0000 Subject: [PATCH 4/7] [docs][m]: create docs for model and view using docco and link from index page. --- docs/docco.css | 186 ++++++++++++ docs/model.html | 281 ++++++++++++++++++ docs/view.html | 755 ++++++++++++++++++++++++++++++++++++++++++++++++ index.html | 32 +- 4 files changed, 1247 insertions(+), 7 deletions(-) create mode 100644 docs/docco.css create mode 100644 docs/model.html create mode 100644 docs/view.html diff --git a/docs/docco.css b/docs/docco.css new file mode 100644 index 00000000..5aa0a8d7 --- /dev/null +++ b/docs/docco.css @@ -0,0 +1,186 @@ +/*--------------------- Layout and Typography ----------------------------*/ +body { + font-family: 'Palatino Linotype', 'Book Antiqua', Palatino, FreeSerif, serif; + font-size: 15px; + line-height: 22px; + color: #252519; + margin: 0; padding: 0; +} +a { + color: #261a3b; +} + a:visited { + color: #261a3b; + } +p { + margin: 0 0 15px 0; +} +h1, h2, h3, h4, h5, h6 { + margin: 0px 0 15px 0; +} + h1 { + margin-top: 40px; + } +#container { + position: relative; +} +#background { + position: fixed; + top: 0; left: 525px; right: 0; bottom: 0; + background: #f5f5ff; + border-left: 1px solid #e5e5ee; + z-index: -1; +} +#jump_to, #jump_page { + background: white; + -webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777; + -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px; + font: 10px Arial; + text-transform: uppercase; + cursor: pointer; + text-align: right; +} +#jump_to, #jump_wrapper { + position: fixed; + right: 0; top: 0; + padding: 5px 10px; +} + #jump_wrapper { + padding: 0; + display: none; + } + #jump_to:hover #jump_wrapper { + display: block; + } + #jump_page { + padding: 5px 0 3px; + margin: 0 0 25px 25px; + } + #jump_page .source { + display: block; + padding: 5px 10px; + text-decoration: none; + border-top: 1px solid #eee; + } + #jump_page .source:hover { + background: #f5f5ff; + } + #jump_page .source:first-child { + } +table td { + border: 0; + outline: 0; +} + td.docs, th.docs { + max-width: 450px; + min-width: 450px; + min-height: 5px; + padding: 10px 25px 1px 50px; + overflow-x: hidden; + vertical-align: top; + text-align: left; + } + .docs pre { + margin: 15px 0 15px; + padding-left: 15px; + } + .docs p tt, .docs p code { + background: #f8f8ff; + border: 1px solid #dedede; + font-size: 12px; + padding: 0 0.2em; + } + .pilwrap { + position: relative; + } + .pilcrow { + font: 12px Arial; + text-decoration: none; + color: #454545; + position: absolute; + top: 3px; left: -20px; + padding: 1px 2px; + opacity: 0; + -webkit-transition: opacity 0.2s linear; + } + td.docs:hover .pilcrow { + opacity: 1; + } + td.code, th.code { + padding: 14px 15px 16px 25px; + width: 100%; + vertical-align: top; + background: #f5f5ff; + border-left: 1px solid #e5e5ee; + } + pre, tt, code { + font-size: 12px; line-height: 18px; + font-family: Monaco, Consolas, "Lucida Console", monospace; + margin: 0; padding: 0; + } + + +/*---------------------- Syntax Highlighting -----------------------------*/ +td.linenos { background-color: #f0f0f0; padding-right: 10px; } +span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; } +body .hll { background-color: #ffffcc } +body .c { color: #408080; font-style: italic } /* Comment */ +body .err { border: 1px solid #FF0000 } /* Error */ +body .k { color: #954121 } /* Keyword */ +body .o { color: #666666 } /* Operator */ +body .cm { color: #408080; font-style: italic } /* Comment.Multiline */ +body .cp { color: #BC7A00 } /* Comment.Preproc */ +body .c1 { color: #408080; font-style: italic } /* Comment.Single */ +body .cs { color: #408080; font-style: italic } /* Comment.Special */ +body .gd { color: #A00000 } /* Generic.Deleted */ +body .ge { font-style: italic } /* Generic.Emph */ +body .gr { color: #FF0000 } /* Generic.Error */ +body .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +body .gi { color: #00A000 } /* Generic.Inserted */ +body .go { color: #808080 } /* Generic.Output */ +body .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ +body .gs { font-weight: bold } /* Generic.Strong */ +body .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +body .gt { color: #0040D0 } /* Generic.Traceback */ +body .kc { color: #954121 } /* Keyword.Constant */ +body .kd { color: #954121; font-weight: bold } /* Keyword.Declaration */ +body .kn { color: #954121; font-weight: bold } /* Keyword.Namespace */ +body .kp { color: #954121 } /* Keyword.Pseudo */ +body .kr { color: #954121; font-weight: bold } /* Keyword.Reserved */ +body .kt { color: #B00040 } /* Keyword.Type */ +body .m { color: #666666 } /* Literal.Number */ +body .s { color: #219161 } /* Literal.String */ +body .na { color: #7D9029 } /* Name.Attribute */ +body .nb { color: #954121 } /* Name.Builtin */ +body .nc { color: #0000FF; font-weight: bold } /* Name.Class */ +body .no { color: #880000 } /* Name.Constant */ +body .nd { color: #AA22FF } /* Name.Decorator */ +body .ni { color: #999999; font-weight: bold } /* Name.Entity */ +body .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ +body .nf { color: #0000FF } /* Name.Function */ +body .nl { color: #A0A000 } /* Name.Label */ +body .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ +body .nt { color: #954121; font-weight: bold } /* Name.Tag */ +body .nv { color: #19469D } /* Name.Variable */ +body .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ +body .w { color: #bbbbbb } /* Text.Whitespace */ +body .mf { color: #666666 } /* Literal.Number.Float */ +body .mh { color: #666666 } /* Literal.Number.Hex */ +body .mi { color: #666666 } /* Literal.Number.Integer */ +body .mo { color: #666666 } /* Literal.Number.Oct */ +body .sb { color: #219161 } /* Literal.String.Backtick */ +body .sc { color: #219161 } /* Literal.String.Char */ +body .sd { color: #219161; font-style: italic } /* Literal.String.Doc */ +body .s2 { color: #219161 } /* Literal.String.Double */ +body .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ +body .sh { color: #219161 } /* Literal.String.Heredoc */ +body .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ +body .sx { color: #954121 } /* Literal.String.Other */ +body .sr { color: #BB6688 } /* Literal.String.Regex */ +body .s1 { color: #219161 } /* Literal.String.Single */ +body .ss { color: #19469D } /* Literal.String.Symbol */ +body .bp { color: #954121 } /* Name.Builtin.Pseudo */ +body .vc { color: #19469D } /* Name.Variable.Class */ +body .vg { color: #19469D } /* Name.Variable.Global */ +body .vi { color: #19469D } /* Name.Variable.Instance */ +body .il { color: #666666 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/model.html b/docs/model.html new file mode 100644 index 00000000..cc4967e7 --- /dev/null +++ b/docs/model.html @@ -0,0 +1,281 @@ + model.js

    model.js

    this.recline = this.recline || {};

    Models module following classic module pattern

    recline.Model = function($) {
    +
    +var my = {};

    A Dataset model.

    + +

    Other than standard list of Backbone attributes it has two important attributes:

    + +
      +
    • currentDocuments: a DocumentList containing the Documents we have currently loaded for viewing (you update currentDocuments by calling getRows)
    • +
    • docCount: total number of documents in this dataset (obtained on a fetch for this Dataset)
    • +
    my.Dataset = Backbone.Model.extend({
    +  __type__: 'Dataset',
    +  initialize: function() {
    +    this.currentDocuments = new my.DocumentList();
    +    this.docCount = null;
    +  },

    AJAX method with promise API to get rows (documents) from the backend.

    + +

    Resulting DocumentList are used to reset this.currentDocuments and are +also returned.

    + +

    :param numRows: passed onto backend getDocuments. +:param start: passed onto backend getDocuments.

    + +

    this does not fit very well with Backbone setup. Backbone really expects you to know the ids of objects your are fetching (which you do in classic RESTful ajax-y world). But this paradigm does not fill well with data set up we have here. +This also illustrates the limitations of separating the Dataset and the Backend

      getDocuments: function(numRows, start) {
    +    var self = this;
    +    var dfd = $.Deferred();
    +    this.backend.getDocuments(this.id, numRows, start).then(function(rows) {
    +      var docs = _.map(rows, function(row) {
    +        return new my.Document(row);
    +      });
    +      self.currentDocuments.reset(docs);
    +      dfd.resolve(self.currentDocuments);
    +    });
    +    return dfd.promise();
    +  },
    +
    +  toTemplateJSON: function() {
    +    var data = this.toJSON();
    +    data.docCount = this.docCount;
    +    return data;
    +  }
    +});
    +
    +my.Document = Backbone.Model.extend({
    +  __type__: 'Document'
    +});
    +
    +my.DocumentList = Backbone.Collection.extend({
    +  __type__: 'DocumentList',

    webStore: new WebStore(this.url),

      model: my.Document
    +});

    Backends section

    my.setBackend = function(backend) {
    +  Backbone.sync = backend.sync;
    +};

    Backend which just caches in memory

    + +

    Does not need to be a backbone model but provides some conveniences

    my.BackendMemory = Backbone.Model.extend({

    Initialize a Backend with a local in-memory dataset.

    + +

    NB: We can handle one and only one dataset at a time.

    + +

    :param dataset: the data for a dataset on which operations will be +performed. Its form should be a hash with metadata and data +attributes.

    + +
      +
    • metadata: hash of key/value attributes of any kind (but usually with title attribute)
    • +
    • data: hash with 2 keys:

      + +
      • headers: list of header names/labels
      • +
      • rows: list of hashes, each hash being one row. A row must have an id attribute which is unique.
      + +

      Example of data:

      + +

      { + headers: ['x', 'y', 'z'] + , rows: [ + {id: 0, x: 1, y: 2, z: 3} + , {id: 1, x: 2, y: 4, z: 6} + ] + };

    • +
      initialize: function(dataset) {

    deep copy

        this._datasetAsData = $.extend(true, {}, dataset);
    +    _.bindAll(this, 'sync');
    +  }, 
    +  getDataset: function() {
    +    var dataset = new my.Dataset({
    +      id: this._datasetAsData.metadata.id
    +    });

    this is a bit weird but problem is in sync this is set to parent model object so need to give dataset a reference to backend explicitly

        dataset.backend = this;
    +    return dataset;
    +  },
    +  sync: function(method, model, options) {
    +    var self = this;
    +    if (method === "read") {
    +      var dfd = $.Deferred();

    this switching on object type is rather horrible +think may make more sense to do work in individual objects rather than in central Backbone.sync

          if (model.__type__ == 'Dataset') {
    +        var dataset = model;
    +        var rawDataset = this._datasetAsData;
    +        dataset.set(rawDataset.metadata);
    +        dataset.set({
    +          headers: rawDataset.data.headers
    +          });
    +        dataset.docCount = rawDataset.data.rows.length;
    +        dfd.resolve(dataset);
    +      }
    +      return dfd.promise();
    +    } else if (method === 'update') {
    +      var dfd = $.Deferred();
    +      if (model.__type__ == 'Document') {
    +        _.each(this._datasetAsData.data.rows, function(row, idx) {
    +          if(row.id === model.id) {
    +            self._datasetAsData.data.rows[idx] = model.toJSON();
    +          }
    +        });
    +        dfd.resolve(model);
    +      }
    +      return dfd.promise();
    +    } else if (method === 'delete') {
    +      var dfd = $.Deferred();
    +      if (model.__type__ == 'Document') {
    +        this._datasetAsData.data.rows = _.reject(this._datasetAsData.data.rows, function(row) {
    +          return (row.id === model.id);
    +        });
    +        dfd.resolve(model);
    +      }
    +      return dfd.promise();
    +    } else {
    +      alert('Not supported: sync on BackendMemory with method ' + method + ' and model ' + model);
    +    }
    +  },
    +  getDocuments: function(datasetId, numRows, start) {
    +    if (start === undefined) {
    +      start = 0;
    +    }
    +    if (numRows === undefined) {
    +      numRows = 10;
    +    }
    +    var dfd = $.Deferred();
    +    rows = this._datasetAsData.data.rows;
    +    var results = rows.slice(start, start+numRows);
    +    dfd.resolve(results);
    +    return dfd.promise();
    + }
    +});

    Webstore Backend for connecting to the Webstore

    + +

    Initializing model argument must contain a url attribute pointing to +relevant Webstore table.

    + +

    Designed to only attach to one dataset and one dataset only ... +Could generalize to support attaching to different datasets

    my.BackendWebstore = Backbone.Model.extend({
    +  getDataset: function(id) {
    +    var dataset = new my.Dataset({
    +      id: id
    +    });
    +    dataset.backend = this;
    +    return dataset;
    +  },
    +  sync: function(method, model, options) {
    +    if (method === "read") {

    this switching on object type is rather horrible +think may make more sense to do work in individual objects rather than in central Backbone.sync

          if (this.__type__ == 'Dataset') {
    +        var dataset = this;

    get the schema and return

            var base = this.backend.get('url');
    +        var schemaUrl = base + '/schema.json';
    +        var jqxhr = $.ajax({
    +          url: schemaUrl,
    +          dataType: 'jsonp',
    +          jsonp: '_callback'
    +          });
    +        var dfd = $.Deferred();
    +        jqxhr.then(function(schema) {
    +          headers = _.map(schema.data, function(item) {
    +            return item.name;
    +          });
    +          dataset.set({
    +            headers: headers
    +          });
    +          dataset.docCount = schema.count;
    +          dfd.resolve(dataset, jqxhr);
    +        });
    +        return dfd.promise();
    +      }
    +    }
    +  },
    +  getDocuments: function(datasetId, numRows, start) {
    +    if (start === undefined) {
    +      start = 0;
    +    }
    +    if (numRows === undefined) {
    +      numRows = 10;
    +    }
    +    var base = this.get('url');
    +    var jqxhr = $.ajax({
    +      url: base + '.json?_limit=' + numRows,
    +      dataType: 'jsonp',
    +      jsonp: '_callback',
    +      cache: true
    +      });
    +    var dfd = $.Deferred();
    +    jqxhr.then(function(results) {
    +      dfd.resolve(results.data);
    +    });
    +    return dfd.promise();
    + }
    +});

    DataProxy Backend for connecting to the DataProxy

    + +

    Example initialization:

    + +
    BackendDataProxy({
    +  model: {
    +    url: {url-of-data-to-proxy},
    +    type: xls || csv,
    +    format: json || jsonp # return format (defaults to jsonp)
    +    dataproxy: {url-to-proxy} # defaults to http://jsonpdataproxy.appspot.com
    +  }
    +})
    +
    my.BackendDataProxy = Backbone.Model.extend({
    +  defaults: {
    +    dataproxy: 'http://jsonpdataproxy.appspot.com'
    +    , type: 'csv'
    +    , format: 'jsonp'
    +  },
    +  getDataset: function(id) {
    +    var dataset = new my.Dataset({
    +      id: id
    +    });
    +    dataset.backend = this;
    +    return dataset;
    +  },
    +  sync: function(method, model, options) {
    +    if (method === "read") {

    this switching on object type is rather horrible +think may make more sense to do work in individual objects rather than in central Backbone.sync

          if (this.__type__ == 'Dataset') {
    +        var dataset = this;

    get the schema and return

            var base = this.backend.get('dataproxy');
    +        var data = this.backend.toJSON();
    +        delete data['dataproxy'];

    TODO: should we cache for extra efficiency

            data['max-results'] = 1;
    +        var jqxhr = $.ajax({
    +          url: base
    +          , data: data
    +          , dataType: 'jsonp'
    +        });
    +        var dfd = $.Deferred();
    +        jqxhr.then(function(results) {
    +          dataset.set({
    +            headers: results.fields
    +          });
    +          dfd.resolve(dataset, jqxhr);
    +        });
    +        return dfd.promise();
    +      }
    +    } else {
    +      alert('This backend only supports read operations');
    +    }
    +  },
    +  getDocuments: function(datasetId, numRows, start) {
    +    if (start === undefined) {
    +      start = 0;
    +    }
    +    if (numRows === undefined) {
    +      numRows = 10;
    +    }
    +    var base = this.get('dataproxy');
    +    var data = this.toJSON();
    +    delete data['dataproxy'];
    +    data['max-results'] = numRows;
    +    var jqxhr = $.ajax({
    +      url: base
    +      , data: data
    +      , dataType: 'jsonp'

    , cache: true

          });
    +    var dfd = $.Deferred();
    +    jqxhr.then(function(results) {
    +      var _out = _.map(results.data, function(row) {
    +        var tmp = {};
    +        _.each(results.fields, function(key, idx) {
    +          tmp[key] = row[idx];
    +        });
    +        return tmp;
    +      });
    +      dfd.resolve(_out);
    +    });
    +    return dfd.promise();
    + }
    +});
    +
    +return my;
    +
    +}(jQuery);
    +
    +
    \ No newline at end of file diff --git a/docs/view.html b/docs/view.html new file mode 100644 index 00000000..11cf161d --- /dev/null +++ b/docs/view.html @@ -0,0 +1,755 @@ + view.js

    view.js

    this.recline = this.recline || {};

    Views module following classic module pattern

    recline.View = function($) {
    +
    +var my = {};

    Parse a URL query string (?xyz=abc...) into a dictionary.

    function parseQueryString(q) {
    +  var urlParams = {},
    +    e, d = function (s) {
    +      return unescape(s.replace(/\+/g, " "));
    +    },
    +    r = /([^&=]+)=?([^&]*)/g;
    +
    +  if (q && q.length && q[0] === '?') {
    +    q = q.slice(1);
    +  }
    +  while (e = r.exec(q)) {

    TODO: have values be array as query string allow repetition of keys

        urlParams[d(e[1])] = d(e[2]);
    +  }
    +  return urlParams;
    +}

    The primary view for the entire application.

    + +

    It should be initialized with a recline.Model.Dataset object and an existing +dom element to attach to (the existing DOM element is important for +rendering of FlotGraph subview).

    + +

    To pass in configuration options use the config key in initialization hash +e.g.

    + +
     var explorer = new DataExplorer({
    +   config: {...}
    + })
    +
    + +

    Config options:

    + +
      +
    • displayCount: how many documents to display initially (default: 10)
    • +
    • readOnly: true/false (default: false) value indicating whether to +operate in read-only mode (hiding all editing options).
    • +
    + +

    All other views as contained in this one.

    my.DataExplorer = Backbone.View.extend({
    +  template: ' \
    +  <div class="data-explorer"> \
    +    <div class="alert-messages"></div> \
    +    \
    +    <div class="header"> \
    +      <ul class="navigation"> \
    +        <li class="active"><a href="#grid" class="btn">Grid</a> \
    +        <li><a href="#graph" class="btn">Graph</a></li> \
    +      </ul> \
    +      <div class="pagination"> \
    +        <form class="display-count"> \
    +          Showing 0 to <input name="displayCount" type="text" value="{{displayCount}}" /> of  <span class="doc-count">{{docCount}}</span> \
    +        </form> \
    +      </div> \
    +    </div> \
    +    <div class="data-view-container"></div> \
    +    <div class="dialog-overlay" style="display: none; z-index: 101; ">&nbsp;</div> \
    +    <div class="dialog ui-draggable" style="display: none; z-index: 102; top: 101px; "> \
    +      <div class="dialog-frame" style="width: 700px; visibility: visible; "> \
    +        <div class="dialog-content dialog-border"></div> \
    +      </div> \
    +    </div> \
    +  </div> \
    +  ',
    +
    +  events: {
    +    'submit form.display-count': 'onDisplayCountUpdate'
    +  },
    +
    +  initialize: function(options) {
    +    var self = this;
    +    this.el = $(this.el);
    +    this.config = options.config || {};
    +    _.extend(this.config, {
    +      displayCount: 10
    +      , readOnly: false
    +    });
    +    if (this.config.readOnly) {
    +      this.setReadOnly();
    +    }

    Hash of 'page' views (i.e. those for whole page) keyed by page name

        this.pageViews = {
    +      grid: new my.DataTable({
    +          model: this.model
    +        })
    +      , graph: new my.FlotGraph({
    +          model: this.model
    +        })
    +    };

    this must be called after pageViews are created

        this.render();
    +
    +    this.router = new Backbone.Router();
    +    this.setupRouting();

    retrieve basic data like headers etc +note this.model and dataset returned are the same

        this.model.fetch().then(function(dataset) {
    +      self.el.find('.doc-count').text(self.model.docCount);

    initialize of dataTable calls render

          self.model.getDocuments(self.config.displayCount);
    +    });
    +  },
    +
    +  onDisplayCountUpdate: function(e) {
    +    e.preventDefault();
    +    this.config.displayCount = parseInt(this.el.find('input[name="displayCount"]').val());
    +    this.model.getDocuments(this.config.displayCount);
    +  },
    +
    +  setReadOnly: function() {
    +    this.el.addClass('read-only');
    +  },
    +
    +  render: function() {
    +    var tmplData = this.model.toTemplateJSON();
    +    tmplData.displayCount = this.config.displayCount;
    +    var template = $.mustache(this.template, tmplData);
    +    $(this.el).html(template);
    +    var $dataViewContainer = this.el.find('.data-view-container');
    +    _.each(this.pageViews, function(view, pageName) {
    +      $dataViewContainer.append(view.el)
    +    });
    +  },
    +
    +  setupRouting: function() {
    +    var self = this;
    +    this.router.route('', 'grid', function() {
    +      self.updateNav('grid');
    +    });
    +    this.router.route(/grid(\?.*)?/, 'view', function(queryString) {
    +      self.updateNav('grid', queryString);
    +    });
    +    this.router.route(/graph(\?.*)?/, 'graph', function(queryString) {
    +      self.updateNav('graph', queryString);

    we have to call here due to fact plot may not have been able to draw +if it was hidden until now - see comments in FlotGraph.redraw

          qsParsed = parseQueryString(queryString);
    +      if ('graph' in qsParsed) {
    +        var chartConfig = JSON.parse(qsParsed['graph']);
    +        _.extend(self.pageViews['graph'].chartConfig, chartConfig);
    +      }
    +      self.pageViews['graph'].redraw();
    +    });
    +  },
    +
    +  updateNav: function(pageName, queryString) {
    +    this.el.find('.navigation li').removeClass('active');
    +    var $el = this.el.find('.navigation li a[href=#' + pageName + ']');
    +    $el.parent().addClass('active');

    show the specific page

        _.each(this.pageViews, function(view, pageViewName) {
    +      if (pageViewName === pageName) {
    +        view.el.show();
    +      } else {
    +        view.el.hide();
    +      }
    +    });
    +  }
    +});

    DataTable provides a tabular view on a Dataset.

    + +

    Initialize it with a recline.Dataset object.

    my.DataTable = Backbone.View.extend({
    +  tagName:  "div",
    +  className: "data-table-container",
    +
    +  initialize: function() {
    +    var self = this;
    +    this.el = $(this.el);
    +    _.bindAll(this, 'render');
    +    this.model.currentDocuments.bind('add', this.render);
    +    this.model.currentDocuments.bind('reset', this.render);
    +    this.model.currentDocuments.bind('remove', this.render);
    +    this.state = {};
    +  },
    +
    +  events: {
    +    'click .column-header-menu': 'onColumnHeaderClick'
    +    , 'click .row-header-menu': 'onRowHeaderClick'
    +    , 'click .data-table-menu li a': 'onMenuClick'
    +  },

    TODO: delete or re-enable (currently this code is not used from anywhere except deprecated or disabled methods (see above)). +showDialog: function(template, data) { + if (!data) data = {}; + util.show('dialog'); + util.render(template, 'dialog-content', data); + util.observeExit($('.dialog-content'), function() { + util.hide('dialog'); + }) + $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' }); +},

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

      onColumnHeaderClick: function(e) {
    +    this.state.currentColumn = $(e.target).siblings().text();
    +    util.position('data-table-menu', e);
    +    util.render('columnActions', 'data-table-menu');
    +  },
    +
    +  onRowHeaderClick: function(e) {
    +    this.state.currentRow = $(e.target).parents('tr:first').attr('data-id');
    +    util.position('data-table-menu', e);
    +    util.render('rowActions', 'data-table-menu');
    +  },
    +
    +  onMenuClick: function(e) {
    +    var self = this;
    +    e.preventDefault();
    +    var actions = {
    +      bulkEdit: function() { self.showTransformColumnDialog('bulkEdit', {name: self.state.currentColumn}) },
    +      transform: function() { self.showTransformDialog('transform') },

    TODO: Delete or re-implement ...

          csv: function() { window.location.href = app.csvUrl },
    +      json: function() { window.location.href = "_rewrite/api/json" },
    +      urlImport: function() { showDialog('urlImport') },
    +      pasteImport: function() { showDialog('pasteImport') },
    +      uploadImport: function() { showDialog('uploadImport') },

    END TODO

          deleteColumn: function() {
    +        var msg = "Are you sure? This will delete '" + self.state.currentColumn + "' from all documents.";

    TODO:

            alert('This function needs to be re-implemented');
    +        return;
    +        if (confirm(msg)) costco.deleteColumn(self.state.currentColumn);
    +      },
    +      deleteRow: function() {
    +        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.state.currentRow
    +        });
    +        doc.destroy().then(function() { 
    +            self.model.currentDocuments.remove(doc);
    +            util.notify("Row deleted successfully");
    +          })
    +          .fail(function(err) {
    +            util.notify("Errorz! " + err)
    +          })
    +      }
    +    }
    +    util.hide('data-table-menu');
    +    actions[$(e.target).attr('data-action')]();
    +  },
    +
    +  showTransformColumnDialog: function() {
    +    var $el = $('.dialog-content');
    +    util.show('dialog');
    +    var view = new my.ColumnTransform({
    +      model: this.model
    +    });
    +    view.state = this.state;
    +    view.render();
    +    $el.empty();
    +    $el.append(view.el);
    +    util.observeExit($el, function() {
    +      util.hide('dialog');
    +    })
    +    $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' });
    +  },
    +
    +  showTransformDialog: function() {
    +    var $el = $('.dialog-content');
    +    util.show('dialog');
    +    var view = new my.DataTransform({
    +    });
    +    view.render();
    +    $el.empty();
    +    $el.append(view.el);
    +    util.observeExit($el, function() {
    +      util.hide('dialog');
    +    })
    +    $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' });
    +  },

    ====================================================== +Core Templating

      template: ' \
    +    <div class="data-table-menu-overlay" style="display: none; z-index: 101; ">&nbsp;</div> \
    +    <ul class="data-table-menu"></ul> \
    +    <table class="data-table" cellspacing="0"> \
    +      <thead> \
    +        <tr> \
    +          {{#notEmpty}}<th class="column-header"></th>{{/notEmpty}} \
    +          {{#headers}} \
    +            <th class="column-header"> \
    +              <div class="column-header-title"> \
    +                <a class="column-header-menu"></a> \
    +                <span class="column-header-name">{{.}}</span> \
    +              </div> \
    +              </div> \
    +            </th> \
    +          {{/headers}} \
    +        </tr> \
    +      </thead> \
    +      <tbody></tbody> \
    +    </table> \
    +  ',
    +
    +  toTemplateJSON: function() {
    +    var modelData = this.model.toJSON()
    +    modelData.notEmpty = ( modelData.headers.length > 0 )
    +    return modelData;
    +  },
    +  render: function() {
    +    var self = this;
    +    var htmls = $.mustache(this.template, this.toTemplateJSON());
    +    this.el.html(htmls);
    +    this.model.currentDocuments.forEach(function(doc) {
    +      var tr = $('<tr />');
    +      self.el.find('tbody').append(tr);
    +      var newView = new my.DataTableRow({
    +          model: doc,
    +          el: tr,
    +          headers: self.model.get('headers')
    +        });
    +      newView.render();
    +    });
    +    return this;
    +  }
    +});

    DataTableRow 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. +In addition you must pass in a headers in the constructor options. This should be list of headers for the DataTable.

    my.DataTableRow = Backbone.View.extend({
    +  initialize: function(options) {
    +    _.bindAll(this, 'render');
    +    this._headers = options.headers;
    +    this.el = $(this.el);
    +    this.model.bind('change', this.render);
    +  },
    +  template: ' \
    +      <td><a class="row-header-menu"></a></td> \
    +      {{#cells}} \
    +      <td data-header="{{header}}"> \
    +        <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> \
    +        </div> \
    +      </td> \
    +      {{/cells}} \
    +    ',
    +  events: {
    +    'click .data-table-cell-edit': 'onEditClick',

    cell editor

        'click .data-table-cell-editor .okButton': 'onEditorOK',
    +    'click .data-table-cell-editor .cancelButton': 'onEditorCancel'
    +  },
    +  
    +  toTemplateJSON: function() {
    +    var doc = this.model;
    +    var cellData = _.map(this._headers, function(header) {
    +      return {header: header, value: doc.get(header)}
    +    })
    +    return { id: this.id, cells: cellData }
    +  },
    +
    +  render: function() {
    +    this.el.attr('data-id', this.model.id);
    +    var html = $.mustache(this.template, this.toTemplateJSON());
    +    $(this.el).html(html);
    +    return this;
    +  },

    ====================================================== +Cell Editor

      onEditClick: function(e) {
    +    var editing = this.el.find('.data-table-cell-editor-editor');
    +    if (editing.length > 0) {
    +      editing.parents('.data-table-cell-value').html(editing.text()).siblings('.data-table-cell-edit').removeClass("hidden");
    +    }
    +    $(e.target).addClass("hidden");
    +    var cell = $(e.target).siblings('.data-table-cell-value');
    +    cell.data("previousContents", cell.text());
    +    util.render('cellEditor', cell, {value: cell.text()});
    +  },
    +
    +  onEditorOK: function(e) {
    +    var cell = $(e.target);
    +    var rowId = cell.parents('tr').attr('data-id');
    +    var header = cell.parents('td').attr('data-header');
    +    var newValue = cell.parents('.data-table-cell-editor').find('.data-table-cell-editor-editor').val();
    +    var newData = {};
    +    newData[header] = newValue;
    +    this.model.set(newData);
    +    util.notify("Updating row...", {loader: true});
    +    this.model.save().then(function(response) {
    +        util.notify("Row updated successfully", {category: 'success'});
    +      })
    +      .fail(function() {
    +        util.notify('Error saving row', {
    +          category: 'error',
    +          persist: true
    +        });
    +      });
    +  },
    +
    +  onEditorCancel: function(e) {
    +    var cell = $(e.target).parents('.data-table-cell-value');
    +    cell.html(cell.data('previousContents')).siblings('.data-table-cell-edit').removeClass("hidden");
    +  }
    +});

    View (Dialog) for doing data transformations (on columns of data).

    my.ColumnTransform = Backbone.View.extend({
    +  className: 'transform-column-view',
    +  template: ' \
    +    <div class="dialog-header"> \
    +      Functional transform on column {{name}} \
    +    </div> \
    +    <div class="dialog-body"> \
    +      <div class="grid-layout layout-tight layout-full"> \
    +        <table> \
    +        <tbody> \
    +        <tr> \
    +          <td colspan="4"> \
    +            <div class="grid-layout layout-tight layout-full"> \
    +              <table rows="4" cols="4"> \
    +              <tbody> \
    +              <tr style="vertical-align: bottom;"> \
    +                <td colspan="4"> \
    +                  Expression \
    +                </td> \
    +              </tr> \
    +              <tr> \
    +                <td colspan="3"> \
    +                  <div class="input-container"> \
    +                    <textarea class="expression-preview-code"></textarea> \
    +                  </div> \
    +                </td> \
    +                <td class="expression-preview-parsing-status" width="150" style="vertical-align: top;"> \
    +                  No syntax error. \
    +                </td> \
    +              </tr> \
    +              <tr> \
    +                <td colspan="4"> \
    +                  <div id="expression-preview-tabs" class="refine-tabs ui-tabs ui-widget ui-widget-content ui-corner-all"> \
    +                    <span>Preview</span> \
    +                    <div id="expression-preview-tabs-preview" class="ui-tabs-panel ui-widget-content ui-corner-bottom"> \
    +                      <div class="expression-preview-container" style="width: 652px; "> \
    +                      </div> \
    +                    </div> \
    +                  </div> \
    +                </td> \
    +              </tr> \
    +              </tbody> \
    +              </table> \
    +            </div> \
    +          </td> \
    +        </tr> \
    +        </tbody> \
    +        </table> \
    +      </div> \
    +    </div> \
    +    <div class="dialog-footer"> \
    +      <button class="okButton btn primary">&nbsp;&nbsp;Update All&nbsp;&nbsp;</button> \
    +      <button class="cancelButton btn danger">Cancel</button> \
    +    </div> \
    +  ',
    +
    +  events: {
    +    'click .okButton': 'onSubmit'
    +    , 'keydown .expression-preview-code': 'onEditorKeydown'
    +  },
    +
    +  initialize: function() {
    +    this.el = $(this.el);
    +  },
    +
    +  render: function() {
    +    var htmls = $.mustache(this.template, 
    +      {name: this.state.currentColumn}
    +      )
    +    this.el.html(htmls);

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

        var editor = this.el.find('.expression-preview-code');
    +    editor.val("function(doc) {\n  doc['"+ this.state.currentColumn+"'] = doc['"+ this.state.currentColumn+"'];\n  return doc;\n}");
    +    editor.focus().get(0).setSelectionRange(18, 18);
    +    editor.keydown();
    +  },
    +
    +  onSubmit: function(e) {
    +    var self = this;
    +    var funcText = this.el.find('.expression-preview-code').val();
    +    var editFunc = costco.evalFunction(funcText);
    +    if (editFunc.errorMessage) {
    +      util.notify("Error with function! " + editFunc.errorMessage);
    +      return;
    +    }
    +    util.hide('dialog');
    +    util.notify("Updating all visible docs. This could take a while...", {persist: true, loader: true});
    +      var docs = self.model.currentDocuments.map(function(doc) {
    +       return doc.toJSON();
    +      });

    TODO: notify about failed docs?

        var toUpdate = costco.mapDocs(docs, editFunc).edited;
    +    var totalToUpdate = toUpdate.length;
    +    function onCompletedUpdate() {
    +      totalToUpdate += -1;
    +      if (totalToUpdate === 0) {
    +        util.notify(toUpdate.length + " documents updated successfully");
    +        alert('WARNING: We have only updated the docs in this view. (Updating of all docs not yet implemented!)');
    +        self.remove();
    +      }
    +    }

    TODO: Very inefficient as we search through all docs every time!

        _.each(toUpdate, function(editedDoc) {
    +      var realDoc = self.model.currentDocuments.get(editedDoc.id);
    +      realDoc.set(editedDoc);
    +      realDoc.save().then(onCompletedUpdate).fail(onCompletedUpdate)
    +    });
    +  },
    +
    +  onEditorKeydown: function(e) {
    +    var self = this;

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

        window.setTimeout( function() {
    +      var errors = self.el.find('.expression-preview-parsing-status');
    +      var editFunc = costco.evalFunction(e.target.value);
    +      if (!editFunc.errorMessage) {
    +        errors.text('No syntax error.');
    +        var docs = self.model.currentDocuments.map(function(doc) {
    +          return doc.toJSON();
    +        });
    +        var previewData = costco.previewTransform(docs, editFunc, self.state.currentColumn);
    +        util.render('editPreview', 'expression-preview-container', {rows: previewData});
    +      } else {
    +        errors.text(editFunc.errorMessage);
    +      }
    +    }, 1, true);
    +  }
    +});

    View (Dialog) for doing data transformations on whole dataset.

    my.DataTransform = Backbone.View.extend({
    +  className: 'transform-view',
    +  template: ' \
    +    <div class="dialog-header"> \
    +      Recursive transform on all rows \
    +    </div> \
    +    <div class="dialog-body"> \
    +      <div class="grid-layout layout-full"> \
    +        <p class="info">Traverse and transform objects by visiting every node on a recursive walk using <a href="https://github.com/substack/js-traverse">js-traverse</a>.</p> \
    +        <table> \
    +        <tbody> \
    +        <tr> \
    +          <td colspan="4"> \
    +            <div class="grid-layout layout-tight layout-full"> \
    +              <table rows="4" cols="4"> \
    +              <tbody> \
    +              <tr style="vertical-align: bottom;"> \
    +                <td colspan="4"> \
    +                  Expression \
    +                </td> \
    +              </tr> \
    +              <tr> \
    +                <td colspan="3"> \
    +                  <div class="input-container"> \
    +                    <textarea class="expression-preview-code"></textarea> \
    +                  </div> \
    +                </td> \
    +                <td class="expression-preview-parsing-status" width="150" style="vertical-align: top;"> \
    +                  No syntax error. \
    +                </td> \
    +              </tr> \
    +              <tr> \
    +                <td colspan="4"> \
    +                  <div id="expression-preview-tabs" class="refine-tabs ui-tabs ui-widget ui-widget-content ui-corner-all"> \
    +                    <span>Preview</span> \
    +                    <div id="expression-preview-tabs-preview" class="ui-tabs-panel ui-widget-content ui-corner-bottom"> \
    +                      <div class="expression-preview-container" style="width: 652px; "> \
    +                      </div> \
    +                    </div> \
    +                  </div> \
    +                </td> \
    +              </tr> \
    +              </tbody> \
    +              </table> \
    +            </div> \
    +          </td> \
    +        </tr> \
    +        </tbody> \
    +        </table> \
    +      </div> \
    +    </div> \
    +    <div class="dialog-footer"> \
    +      <button class="okButton button">&nbsp;&nbsp;Update All&nbsp;&nbsp;</button> \
    +      <button class="cancelButton button">Cancel</button> \
    +    </div> \
    +  ',
    +
    +  initialize: function() {
    +    this.el = $(this.el);
    +  },
    +
    +  render: function() {
    +    this.el.html(this.template);
    +  }
    +});

    Graph view for a Dataset using Flot graphing library.

    + +

    Initialization arguments:

    + +
      +
    • model: recline.Model.Dataset
    • +
    • config: (optional) graph configuration hash of form:

      + +

      { + group: {column name for x-axis}, + series: [{column name for series A}, {column name series B}, ... ], + graphType: 'line' + }

    • +
    + +

    NB: should not provide an el argument to the view but must let the view +generate the element itself (you can then append view.el to the DOM.

    my.FlotGraph = Backbone.View.extend({
    +
    +  tagName:  "div",
    +  className: "data-graph-container",
    +
    +  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> \
    +        <div class="input editor-type"> \
    +          <select> \
    +          <option value="line">Line</option> \
    +          </select> \
    +        </div> \
    +        <label>Group Column (x-axis)</label> \
    +        <div class="input editor-group"> \
    +          <select> \
    +          {{#headers}} \
    +          <option value="{{.}}">{{.}}</option> \
    +          {{/headers}} \
    +          </select> \
    +        </div> \
    +        <div class="editor-series-group"> \
    +          <div class="editor-series"> \
    +            <label>Series <span>A (y-axis)</span></label> \
    +            <div class="input"> \
    +              <select> \
    +              {{#headers}} \
    +              <option value="{{.}}">{{.}}</option> \
    +              {{/headers}} \
    +              </select> \
    +            </div> \
    +          </div> \
    +        </div> \
    +      </div> \
    +      <div class="editor-buttons"> \
    +        <button class="btn editor-add">Add Series</button> \
    +      </div> \
    +      <div class="editor-buttons editor-submit" comment="hidden temporarily" style="display: none;"> \
    +        <button class="editor-save">Save</button> \
    +        <input type="hidden" class="editor-id" value="chart-1" /> \
    +      </div> \
    +    </form> \
    +  </div> \
    +  <div class="panel graph"></div> \
    +</div> \
    +',
    +
    +  events: {
    +    'change form select': 'onEditorSubmit'
    +    , 'click .editor-add': 'addSeries'
    +    , 'click .action-remove-series': 'removeSeries'
    +    , 'click .action-toggle-help': 'toggleHelp'
    +  },
    +
    +  initialize: function(options, config) {
    +    var self = this;
    +    this.el = $(this.el);
    +    _.bindAll(this, 'render', 'redraw');

    we need the model.headers to render properly

        this.model.bind('change', this.render);
    +    this.model.currentDocuments.bind('add', this.redraw);
    +    this.model.currentDocuments.bind('reset', this.redraw);
    +    this.chartConfig = _.extend({
    +        group: null,
    +        series: [],
    +        graphType: 'line'
    +      },
    +      config)
    +    this.render();
    +  },
    +
    +  toTemplateJSON: function() {
    +    return this.model.toJSON();
    +  },
    +
    +  render: function() {
    +    htmls = $.mustache(this.template, this.toTemplateJSON());
    +    $(this.el).html(htmls);

    now set a load of stuff up

        this.$graph = this.el.find('.panel.graph');

    for use later when adding additional series +could be simpler just to have a common template!

        this.$seriesClone = this.el.find('.editor-series').clone();
    +    this._updateSeries();
    +    return this;
    +  },
    +
    +  onEditorSubmit: function(e) {
    +    var select = this.el.find('.editor-group select');
    +    this._getEditorData();

    update navigation +TODO: make this less invasive (e.g. preserve other keys in query string)

        window.location.hash = window.location.hash.split('?')[0] +
    +        '?graph=' + JSON.stringify(this.chartConfig);
    +    this.redraw();
    +  },
    +
    +  redraw: function() {

    There appear to be issues generating a Flot graph if either:

      +
    • The relevant div that graph attaches to his hidden at the moment of creating the plot -- Flot will complain with

      + +

      Uncaught Invalid dimensions for plot, width = 0, height = 0

    • +
    • There is no data for the plot -- either same error or may have issues later with errors like 'non-existent node-value'
    • +
        var areWeVisible = !jQuery.expr.filters.hidden(this.el[0]);
    +    if (!this.plot && (!areWeVisible || this.model.currentDocuments.length == 0)) {
    +      return
    +    }

    create this.plot and cache it

        if (!this.plot) {

    only lines for the present

          options = {
    +        id: 'line',
    +        name: 'Line Chart'
    +      };
    +      this.plot = $.plot(this.$graph, this.createSeries(), options);
    +    } 
    +    this.plot.setData(this.createSeries());
    +    this.plot.resize();
    +    this.plot.setupGrid();
    +    this.plot.draw();
    +  },
    +
    +  _getEditorData: function() {
    +    $editor = this
    +    var series = this.$series.map(function () {
    +      return $(this).val();
    +    });
    +    this.chartConfig.series = $.makeArray(series)
    +    this.chartConfig.group = this.el.find('.editor-group select').val();
    +  },
    +
    +  createSeries: function () {
    +    var self = this;
    +    var series = [];
    +    if (this.chartConfig) {
    +      $.each(this.chartConfig.series, function (seriesIndex, field) {
    +        var points = [];
    +        $.each(self.model.currentDocuments.models, function (index, doc) {
    +          var x = doc.get(self.chartConfig.group);
    +          var y = doc.get(field);
    +          if (typeof x === 'string') {
    +            x = index;
    +          }
    +          points.push([x, y]);
    +        });
    +        series.push({data: points, label: field});
    +      });
    +    }
    +    return series;
    +  },

    Public: Adds a new empty series select box to the editor.

    + +

    All but the first select box will have a remove button that allows them +to be removed.

    + +

    Returns itself.

      addSeries: function (e) {
    +    e.preventDefault();
    +    var element = this.$seriesClone.clone(),
    +        label   = element.find('label'),
    +        index   = this.$series.length;
    +
    +    this.el.find('.editor-series-group').append(element);
    +    this._updateSeries();
    +    label.append(' [<a href="#remove" class="action-remove-series">Remove</a>]');
    +    label.find('span').text(String.fromCharCode(this.$series.length + 64));
    +    return this;
    +  },

    Public: Removes a series list item from the editor.

    + +

    Also updates the labels of the remaining series elements.

      removeSeries: function (e) {
    +    e.preventDefault();
    +    var $el = $(e.target);
    +    $el.parent().parent().remove();
    +    this._updateSeries();
    +    this.$series.each(function (index) {
    +      if (index > 0) {
    +        var labelSpan = $(this).prev().find('span');
    +        labelSpan.text(String.fromCharCode(index + 65));
    +      }
    +    });
    +    this.onEditorSubmit();
    +  },
    +
    +  toggleHelp: function() {
    +    this.el.find('.editor-info').toggleClass('editor-hide-info');
    +  },

    Private: Resets the series property to reference the select elements.

    + +

    Returns itself.

      _updateSeries: function () {
    +    this.$series  = this.el.find('.editor-series select');
    +  }
    +});
    +
    +return my;
    +
    +}(jQuery);
    +
    +
    \ No newline at end of file diff --git a/index.html b/index.html index 803af84f..aa52dbb7 100644 --- a/index.html +++ b/index.html @@ -68,11 +68,14 @@

    Main Features

      -
    • Open-source (and heavy reuser of existing open-source libraries like Backbone)
    • -
    • Pure javascript (no Flash) and designed for integration -- so it is easy to embed in other sites and applications
    • +
    • Open-source (and heavy reuser of existing open-source libraries like + Backbone)
    • +
    • Pure javascript (no Flash) and designed for integration -- so it is + easy to embed in other sites and applications
    • View and edit your data in a clean grid interface
    • Bulk update/clean your data using an easy scripting UI
    • -
    • Easily extensible with new Backends so you can connect to your database or storage layer
    • +
    • Easily extensible with new Backends so you can connect to your + database or storage layer
    • Visualize data
    @@ -82,12 +85,27 @@

Docs

-

Coming soon!

+

Want to see how to embed this in your own application. Check out the the + Demo and view source.

+

History

-

Max Ogden was developing Recline as the frontend data browser and editor his http://datacouch.com/ project. Meanwhile, Rufus Pollock and the CKAN team at the Open Knowledge Foundation had been working on a Data Explorer for use in the DataHub and CKAN software.

-

When they met up, they realized that they were pretty much working on the same thing and so decided to join forces to produce the new Recline Data Explorer.

-

The new project forked off Max's original recline codebase combining some portions of the original Data Explorer. However, it was rapidly rewritten from the ground up using Backbone.

+

Max Ogden was developing Recline as the frontend data browser and editor + his http://datacouch.com/ project. + Meanwhile, Rufus Pollock and the CKAN team + at the Open Knowledge Foundation had been + working on a Data + Explorer for use in the DataHub + and CKAN software.

When they met up, + they realized that they were pretty much working on the same thing and so + decided to join forces to produce the new Recline Data Explorer.

The + new project forked off Max's + original recline codebase combining some portions of the original Data Explorer. + However, it was rapidly rewritten from the ground up using Backbone.

From 9adf0ba5184b9f93ec1f0005fb68351216935da1 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Thu, 26 Jan 2012 15:22:38 +0000 Subject: [PATCH 5/7] [docs][xs]: add ids to main headings in docs. --- index.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/index.html b/index.html index aa52dbb7..96977532 100644 --- a/index.html +++ b/index.html @@ -66,7 +66,7 @@

Designed for standalone use or as a library to integrate into your own app.

-

Main Features

+

Main Features

  • Open-source (and heavy reuser of existing open-source libraries like Backbone)
  • @@ -79,12 +79,12 @@
  • Visualize data
-

Demo

+

Demo

-

Docs

+

Docs

Want to see how to embed this in your own application. Check out the the Demo and view source.

-

History

+

History

Max Ogden was developing Recline as the frontend data browser and editor his http://datacouch.com/ project. Meanwhile, Rufus Pollock and the CKAN team From 99b8cb425bed94f72e0fa2742ca7a1bf219213d9 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Thu, 26 Jan 2012 15:24:45 +0000 Subject: [PATCH 6/7] [docs][xs]: correct formatting and spelling. --- index.html | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/index.html b/index.html index 96977532..6fd04915 100644 --- a/index.html +++ b/index.html @@ -89,23 +89,25 @@ Demo and view source.

History

Max Ogden was developing Recline as the frontend data browser and editor - his http://datacouch.com/ project. + for his http://datacouch.com/ project. Meanwhile, Rufus Pollock and the CKAN team at the Open Knowledge Foundation had been working on a Data Explorer for use in the DataHub - and CKAN software.

When they met up, - they realized that they were pretty much working on the same thing and so - decided to join forces to produce the new Recline Data Explorer.

The - new project forked off Max's - original recline codebase combining some portions of the CKAN software.

+

When they met up, they realized that they were pretty much working on + the same thing and so decided to join forces to produce the new Recline + Data Explorer.

+

The new project forked off Max's original recline + codebase combining some portions of the original Data Explorer. - However, it was rapidly rewritten from the ground up using Backbone.

+ However, it has been rapidly rewritten from the ground up using Backbone.

From 120a3589bfdae37da03463439ca765e1cbd08fcb Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Thu, 26 Jan 2012 20:22:30 +0000 Subject: [PATCH 7/7] [view/explorer][xs]: show unknown when doc count unknown (rather than nothing). --- src/view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view.js b/src/view.js index 064795f5..8195e26a 100644 --- a/src/view.js +++ b/src/view.js @@ -102,7 +102,7 @@ my.DataExplorer = Backbone.View.extend({ // retrieve basic data like headers etc // note this.model and dataset returned are the same this.model.fetch().then(function(dataset) { - self.el.find('.doc-count').text(self.model.docCount); + self.el.find('.doc-count').text(self.model.docCount || 'Unknown'); // initialize of dataTable calls render self.model.getDocuments(self.config.displayCount); });