diff --git a/docs/view.html b/docs/view.html index 11cf161d..466f10d5 100644 --- a/docs/view.html +++ b/docs/view.html @@ -1,33 +1,49 @@ - view.js
Jump To …

view.js

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

Views module following classic module pattern

recline.View = function($) {
+      view.js           
render:function(){vartmplData=this.model.toTemplateJSON();tmplData.displayCount=this.config.displayCount; + tmplData.views=this.pageViews;vartemplate=$.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) + $dataViewContainer.append(view.view.el)});},setupRouting:function(){ - varself=this; - this.router.route('','grid',function(){ - self.updateNav('grid'); + varself=this;

view.js

this.recline = this.recline || {};
+this.recline.View = this.recline.View || {};
 
-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;
+(function($, my) {

DataExplorer

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

+

The primary view for the entire application. Usage:

-

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

+
+var myExplorer = new model.recline.DataExplorer({
+  model: {{recline.Model.Dataset instance}}
+  el: {{an existing dom element}}
+  views: {{page views}}
+  config: {{config options -- see below}}
+});
+
-

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

+

Parameters

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

model: (required) Dataset instance.

-

Config options:

+

el: (required) DOM element.

+ +

views: (optional) the views (Grid, Graph etc) for DataExplorer to +show. This is an array of view hashes. If not provided +just initialize a DataTable with id 'grid'. Example:

+ +
+var views = [
+  {
+    id: 'grid', // used for routing
+    label: 'Grid', // used for view switcher
+    view: new recline.View.DataTable({
+      model: dataset
+    })
+  },
+  {
+    id: 'graph',
+    label: 'Graph',
+    view: new recline.View.FlotGraph({
+      model: dataset
+    })
+  }
+];
+
+ +

config: Config options like:

  • displayCount: how many documents to display initially (default: 10)
  • @@ -35,19 +51,21 @@ e.g.

    operate in read-only mode (hiding all editing options).
-

All other views as contained in this one.

my.DataExplorer = Backbone.View.extend({
+

NB: the element already being in the DOM is important for rendering of +FlotGraph subview.

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> \
+        {{#views}} \
+        <li><a href="#{{id}}" class="btn">{{label}}</a> \
+        {{/views}} \
       </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> \
+          Showing 0 to <input name="displayCount" type="text" value="{{displayCount}}" title="Edit and hit enter to change the number of rows displayed" /> of  <span class="doc-count">{{docCount}}</span> \
         </form> \
       </div> \
     </div> \
@@ -68,33 +86,57 @@ operate in read-only mode (hiding all editing options).
   initialize: function(options) {
     var self = this;
     this.el = $(this.el);
-    this.config = options.config || {};
-    _.extend(this.config, {
-      displayCount: 10
-      , readOnly: false
-    });
+    this.config = _.extend({
+        displayCount: 50
+        , readOnly: false
+      },
+      options.config);
     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();
+    }

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

    if (options.views) {
+      this.pageViews = options.views;
+    } else {
+      this.pageViews = [{
+        id: 'grid',
+        label: 'Grid',
+        view: new my.DataTable({
+            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);
-    });
+    this.setupRouting();

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

    this.model.fetch()
+      .done(function(dataset) {
+        self.el.find('.doc-count').text(self.model.docCount || 'Unknown');
+        self.query();
+      })
+      .fail(function(error) {
+        my.notify(error.message, {category: 'error', persist: true});
+      });
+  },
+
+  query: function() {
+    this.config.displayCount = parseInt(this.el.find('input[name="displayCount"]').val());
+    var queryObj = {
+      size: this.config.displayCount
+    };
+    my.notify('Loading data', {loader: true});
+    this.model.query(queryObj)
+      .done(function() {
+        my.clearNotifications();
+        my.notify('Data loaded', {category: 'success'});
+      })
+      .fail(function(error) {
+        my.clearNotifications();
+        my.notify(error.message, {category: 'error', persist: true});
+      });
   },
 
   onDisplayCountUpdate: function(e) {
     e.preventDefault();
-    this.config.displayCount = parseInt(this.el.find('input[name="displayCount"]').val());
-    this.model.getDocuments(this.config.displayCount);
+    this.query();
   },
 
   setReadOnly: function() {
@@ -104,45 +146,40 @@ note this.model and dataset returned are the same

Default route

    this.router.route('', this.pageViews[0].id, function() {
+      self.updateNav(self.pageViews[0].id);
     });
-    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();
+    $.each(this.pageViews, function(idx, view) {
+      self.router.route(/^([^?]+)(\?.*)?/, 'view', function(viewId, queryString) {
+        self.updateNav(viewId, queryString);
+      });
     });
   },
 
   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();
+    $el.parent().addClass('active');

show the specific page

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

DataTable provides a tabular view on a Dataset.

+});

DataTable

+ +

Provides a tabular view on a Dataset.

Initialize it with a recline.Dataset object.

my.DataTable = Backbone.View.extend({
   tagName:  "div",
@@ -156,13 +193,15 @@ if it was hidden until now - see comments in FlotGraph.redraw

this.model.currentDocuments.bind('reset', this.render); this.model.currentDocuments.bind('remove', this.render); this.state = {}; + this.hiddenHeaders = []; }, events: { 'click .column-header-menu': 'onColumnHeaderClick' , 'click .row-header-menu': 'onRowHeaderClick' + , 'click .root-header-menu': 'onRootHeaderClick' , '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)). + },

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'); @@ -171,7 +210,7 @@ showDialog: function(template, data) { 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);
@@ -183,31 +222,40 @@ Column and row menus

util.position('data-table-menu', e); util.render('rowActions', 'data-table-menu'); }, + + onRootHeaderClick: function(e) { + util.position('data-table-menu', e); + util.render('rootActions', 'data-table-menu', {'columns': this.hiddenHeaders}); + }, 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 },
+      transform: function() { self.showTransformDialog('transform') },
+      sortAsc: function() { self.setColumnSort('asc') },
+      sortDesc: function() { self.setColumnSort('desc') },
+      hideColumn: function() { self.hideColumn() },
+      showColumn: function() { self.showColumn(e) },

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');
+      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 + 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");
+            my.notify("Row deleted successfully");
           })
           .fail(function(err) {
-            util.notify("Errorz! " + err)
+            my.notify("Errorz! " + err)
           })
       }
     }
@@ -234,7 +282,7 @@ from DOM) while id may be int

showTransformDialog: function() { var $el = $('.dialog-content'); util.show('dialog'); - var view = new my.DataTransform({ + var view = new recline.View.DataTransform({ }); view.render(); $el.empty(); @@ -243,14 +291,37 @@ from DOM) while id may be int

util.hide('dialog'); }) $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' }); - },

====================================================== -Core Templating

  template: ' \
+  },
+
+  setColumnSort: function(order) {
+    var query = _.extend(this.model.queryState, {sort: [[this.state.currentColumn, order]]});
+    this.model.query(query);
+  },
+  
+  hideColumn: function() {
+    this.hiddenHeaders.push(this.state.currentColumn);
+    this.render();
+  },
+  
+  showColumn: function(e) {
+    this.hiddenHeaders = _.without(this.hiddenHeaders, $(e.target).data('column'));
+    this.render();
+  },

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

+ +

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}} \
+          {{#notEmpty}} \
+            <th class="column-header"> \
+              <div class="column-header-title"> \
+                <a class="root-header-menu"></a> \
+                <span class="column-header-name"></span> \
+              </div> \
+            </th> \
+          {{/notEmpty}} \
           {{#headers}} \
             <th class="column-header"> \
               <div class="column-header-title"> \
@@ -268,11 +339,15 @@ Core Templating

toTemplateJSON: function() { var modelData = this.model.toJSON() - modelData.notEmpty = ( modelData.headers.length > 0 ) + modelData.notEmpty = ( this.headers.length > 0 ) + modelData.headers = this.headers; return modelData; }, render: function() { var self = this; + this.headers = _.filter(this.model.get('headers'), function(header) { + return _.indexOf(self.hiddenHeaders, header) == -1; + }); var htmls = $.mustache(this.template, this.toTemplateJSON()); this.el.html(htmls); this.model.currentDocuments.forEach(function(doc) { @@ -281,13 +356,14 @@ Core Templating

var newView = new my.DataTableRow({ model: doc, el: tr, - headers: self.model.get('headers') + headers: self.headers, }); newView.render(); }); + this.el.toggleClass('no-hidden', (self.hiddenHeaders.length == 0)); return this; } -});

DataTableRow View for rendering an individual document.

+});

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({
@@ -297,6 +373,7 @@ In addition you must pass in a headers in the constructor options. This should b
     this.el = $(this.el);
     this.model.bind('change', this.render);
   },
+
   template: ' \
       <td><a class="row-header-menu"></a></td> \
       {{#cells}} \
@@ -309,7 +386,7 @@ In addition you must pass in a headers in the constructor options. This should b
       {{/cells}} \
     ',
   events: {
-    'click .data-table-cell-edit': 'onEditClick',

cell editor

    'click .data-table-cell-editor .okButton': 'onEditorOK',
+    'click .data-table-cell-edit': 'onEditClick',

cell editor

    'click .data-table-cell-editor .okButton': 'onEditorOK',
     'click .data-table-cell-editor .cancelButton': 'onEditorCancel'
   },
   
@@ -326,8 +403,7 @@ In addition you must pass in a headers in the constructor options. This should b
     var html = $.mustache(this.template, this.toTemplateJSON());
     $(this.el).html(html);
     return this;
-  },

====================================================== -Cell Editor

  onEditClick: function(e) {
+  },

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");
@@ -346,12 +422,12 @@ Cell Editor

var newData = {}; newData[header] = newValue; this.model.set(newData); - util.notify("Updating row...", {loader: true}); + my.notify("Updating row...", {loader: true}); this.model.save().then(function(response) { - util.notify("Row updated successfully", {category: 'success'}); + my.notify("Row updated successfully", {category: 'success'}); }) .fail(function() { - util.notify('Error saving row', { + my.notify('Error saving row', { category: 'error', persist: true }); @@ -362,394 +438,61 @@ Cell Editor

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);
+/* ========================================================== */

Miscellaneous Utilities

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

notify

+ +

Create a notification (a div.alert-message in div.alert-messsages) using provide messages and options. Options are:

+ +
    +
  • category: warning (default), success, error
  • +
  • persist: if true alert is persistent, o/w hidden after 3s (default = false)
  • +
  • loader: if true show loading spinner
  • +
my.notify = function(message, options) {
+  if (!options) var options = {};
+  var tmplData = _.extend({
+    msg: message,
+    category: 'warning'
+    },
+    options);
+  var _template = ' \
+    <div class="alert-message {{category}} fade in" data-alert="alert"><a class="close" href="#">×</a> \
+      <p>{{msg}} \
+        {{#loader}} \
+        <img src="images/small-spinner.gif" class="notification-loader"> \
+        {{/loader}} \
+      </p> \
+    </div>';
+  var _templated = $.mustache(_template, tmplData); 
+  _templated = $(_templated).appendTo($('.data-explorer .alert-messages'));
+  if (!options.persist) {
+    setTimeout(function() {
+      $(_templated).fadeOut(1000, function() {
+        $(this).remove();
+      });
+    }, 1000);
+  }
+}

clearNotifications

+ +

Clear all existing notifications

my.clearNotifications = function() {
+  var $notifications = $('.data-explorer .alert-message');
+  $notifications.remove();
+}
+
+})(jQuery, recline.View);
 
 
\ No newline at end of file diff --git a/src/view.js b/src/view.js index 1cf5ec47..10f29a8c 100644 --- a/src/view.js +++ b/src/view.js @@ -17,34 +17,34 @@ this.recline.View = this.recline.View || {}; // // ### Parameters // -// **:param model:** (required) Dataset instance. +// **model**: (required) Dataset instance. // -// **:param el:** (required) DOM element. +// **el**: (required) DOM element. // -// **:param views:** (optional) the views (Grid, Graph etc) for DataExplorer to -// show. This is an array with the key used in routing. If not provided -// just initialize a DataTable with key 'grid'. Example: +// **views**: (optional) the views (Grid, Graph etc) for DataExplorer to +// show. This is an array of view hashes. If not provided +// just initialize a DataTable with id 'grid'. Example: // //
-//  var views = [
-//    {
-//      id: 'grid',
-//      label: 'Grid',
-//      view: new recline.View.DataTable({
-//        model: dataset
-//      })
-//    },
-//    {
-//      id: 'graph',
-//      label: 'Graph',
-//      view: new recline.View.FlotGraph({
-//        model: dataset
-//      })
-//    }
-//  ];
+// var views = [
+//   {
+//     id: 'grid', // used for routing
+//     label: 'Grid', // used for view switcher
+//     view: new recline.View.DataTable({
+//       model: dataset
+//     })
+//   },
+//   {
+//     id: 'graph',
+//     label: 'Graph',
+//     view: new recline.View.FlotGraph({
+//       model: dataset
+//     })
+//   }
+// ];
 // 
// -// **:param config:** Config options like: +// **config**: Config options like: // // * displayCount: how many documents to display initially (default: 10) // * readOnly: true/false (default: false) value indicating whether to