diff --git a/README.md b/README.md index ed2bc918..bcd93e3a 100755 --- a/README.md +++ b/README.md @@ -25,6 +25,60 @@ Designed for standalone use or as a library to integrate into your own app. Running the tests by opening `test/index.html` in your browser. +## Changelog + +### v0.5 - Master + +In progress. + +### v0.4 - April 26th 2012 + +[23 closed issues](https://github.com/okfn/recline/issues?milestone=2&page=1&state=closed) including: + +* Map view using Leaflet - #69, #64, #89, #97 +* Term filter support - #66 +* Faceting support- #62 +* Tidy up CSS and JS - #81 and #78 +* Manage and serialize view and dataset state (plus support for embed and permalinks) - #88, #67 +* Graph view improvements e.g. handle date types correctly - #75 +* Write support for ES backend - #61 +* Remove JQuery-UI dependency in favour of bootstrap modal - #46 +* Improved CSV import support - #92 + +### v0.3 - March 31st 2012 + +[16 closed issues](https://github.com/okfn/recline/issues?milestone=1&state=closed) including: + +* ElasticSearch (and hence DataHub/CKAN) backend - #54 +* Loading of local CSV files - #36 +* Fully worked out Data Query support - #34, #49, #53, #57 +* New Field model object for richer field information - #25 +* Upgrade to Bootstrap v2.0 - #55 +* Recline Data Explorer app improvements e.g. #39 (import menu) +* Graph improvements - #58 (more graph types, graph interaction) + +### v0.2 - Feb 24th 2012 + +[17 closed issues](https://github.com/okfn/recline/issues?milestone=3&state=closed) including: + +* Major refactor of backend and model relationship - #35 and #43 +* Support Google Docs Spreadsheets as a Backend - #15 +* Support for online CSV and Excel files via DataProxy backend - #31 +* Data Explorer is customizable re loaded views - #42 +* Start of documentation - #33 +* Views in separate files - #41 +* Better error reporting from backends on JSONP errors - #30 +* Sorting and show/hide of columns in data grid - #23, #29 +* Support for pagination - #27 +* Split backends into separate files to make them easier to maintain and reuse separately #50 + +### v0.1 - Jan 28th 2012 + +* Core models and structure including Dataset and Document +* Memory and webstore backends +* Grid, Graph and Data Explorer views +* Bootstrap-based theme - #22 + ## Copyright and License Copyright 2011 Max Ogden and Rufus Pollock. diff --git a/_config.yml b/_config.yml new file mode 100644 index 00000000..c86cd02e --- /dev/null +++ b/_config.yml @@ -0,0 +1,5 @@ +pygments: true +auto: true + +title: Recline Data Explorer and Library + diff --git a/_includes/recline-deps.html b/_includes/recline-deps.html new file mode 100644 index 00000000..a03aa292 --- /dev/null +++ b/_includes/recline-deps.html @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/_layouts/container.html b/_layouts/container.html new file mode 100644 index 00000000..4abf7ae8 --- /dev/null +++ b/_layouts/container.html @@ -0,0 +1,8 @@ +--- +layout: default +--- + +
+ {{content}} +
+ diff --git a/_layouts/default.html b/_layouts/default.html index 8f60ae47..53d619d7 100644 --- a/_layouts/default.html +++ b/_layouts/default.html @@ -10,14 +10,21 @@ - + {% if page.recline-deps %} + {% include recline-deps.html %} + {% endif %} + + + + +

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

  onEditClick: function(e) {
+  },

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

  cellEditorTemplate: ' \
+    <div class="menu-container data-table-cell-editor"> \
+      <textarea class="data-table-cell-editor-editor" bind="textarea">{{value}}</textarea> \
+      <div id="data-table-cell-editor-actions"> \
+        <div class="data-table-cell-editor-action"> \
+          <button class="okButton btn primary">Update</button> \
+          <button class="cancelButton btn danger">Cancel</button> \
+        </div> \
+      </div> \
+    </div> \
+  ',
+
+  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");
@@ -278,10 +264,12 @@ Cell Editor methods

$(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()}); + var templated = $.mustache(this.cellEditorTemplate, {value: cell.text()}); + cell.html(templated); }, onEditorOK: function(e) { + var self = this; var cell = $(e.target); var rowId = cell.parents('tr').attr('data-id'); var field = cell.parents('td').attr('data-field'); @@ -289,12 +277,13 @@ Cell Editor methods

var newData = {}; newData[field] = newValue; this.model.set(newData); - my.notify("Updating row...", {loader: true}); + this.trigger('recline:flash', {message: "Updating row...", loader: true}); this.model.save().then(function(response) { - my.notify("Row updated successfully", {category: 'success'}); + this.trigger('recline:flash', {message: "Row updated successfully", category: 'success'}); }) .fail(function() { - my.notify('Error saving row', { + this.trigger('recline:flash', { + message: 'Error saving row', category: 'error', persist: true }); diff --git a/docs/view-map.html b/docs/view-map.html index 73faab44..e85e46e3 100644 --- a/docs/view-map.html +++ b/docs/view-map.html @@ -15,7 +15,7 @@ have the following (optional) configuration options:

   {
-    // geomField if specified will be used in preference to lat/lon 
+    // geomField if specified will be used in preference to lat/lon
     geomField: {id of field containing geometry in the dataset}
     lonField: {id of field containing longitude in the dataset}
     latField: {id of field containing latitude in the dataset}
@@ -72,6 +72,11 @@ have the following (optional) configuration options:

<div class="editor-buttons"> \ <button class="btn editor-update-map">Update</button> \ </div> \ + <div class="editor-options" > \ + <label class="checkbox"> \ + <input type="checkbox" id="editor-auto-zoom" checked="checked" /> \ + Auto zoom to features</label> \ + </div> \ <input type="hidden" class="editor-id" value="map-1" /> \ </div> \ </form> \ @@ -83,7 +88,8 @@ If not found, the user will need to define the fields via the editor.

longitudeFieldNames: ['lon','longitude'], geometryFieldNames: ['geom','the_geom','geometry','spatial','location'],

Define here events for UI elements

  events: {
     'click .editor-update-map': 'onEditorSubmit',
-    'change .editor-field-type': 'onFieldTypeChange'
+    'change .editor-field-type': 'onFieldTypeChange',
+    'change #editor-auto-zoom': 'onAutoZoomChange'
   },
 
   initialize: function(options) {
@@ -96,12 +102,25 @@ If not found, the user will need to define the fields via the editor.

self._setupGeometryField() self.render() });

Listen to changes in the documents

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

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

    this.bind('view:show',function(){
-        if (self.map) {
-          self.map.invalidateSize();
+    this.model.currentDocuments.bind('reset', function(){self.redraw('reset')});
+
+    this.bind('view:show',function(){

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

      if (self.map){
+        self.map.invalidateSize();
+        if (self._zoomPending && self.autoZoom) {
+          self._zoomToFeatures();
+          self._zoomPending = false;
         }
+      }
+      self.visible = true;
+    });
+    this.bind('view:hide',function(){
+      self.visible = false;
     });
 
     var stateData = _.extend({
@@ -113,6 +132,7 @@ to display properly

); this.state = new recline.Model.ObjectState(stateData); + this.autoZoom = true; this.mapReady = false; this.render(); },

Public: Adds the necessary elements to the page.

@@ -173,6 +193,13 @@ to display properly

this.features.clearLayers(); this._add(this.model.currentDocuments.models); } + if (action != 'reset' && this.autoZoom){ + if (this.visible){ + this._zoomToFeatures(); + } else { + this._zoomPending = true; + } + } } },

UI Event handlers

Public: Update map with user options

@@ -205,6 +232,10 @@ type selected.

$('.editor-field-type-geom').hide(); $('.editor-field-type-latlon').show(); } + }, + + onAutoZoomChange: function(e){ + this.autoZoom = !this.autoZoom; },

Private: Add one or n features to the map

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

Each feature will have a popup associated with all the document fields.

  _add: function(docs){
-
     var self = this;
 
     if (!(docs instanceof Array)) docs = [docs];
@@ -226,7 +256,9 @@ stopped and an error notification shown.

} else if (feature instanceof Object){

Build popup contents TODO: mustache?

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

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

        feature.properties.cid = doc.cid;
@@ -238,13 +270,13 @@ link this Leaflet layer to a Recline doc

var msg = 'Wrong geometry value'; if (except.message) msg += ' (' + except.message + ')'; if (wrongSoFar <= 10) { - my.notify(msg,{category:'error'}); + self.trigger('recline:flash', {message: msg, category:'error'}); } } } else { wrongSoFar += 1 if (wrongSoFar <= 10) { - my.notify('Wrong geometry value',{category:'error'}); + self.trigger('recline:flash', {message: 'Wrong geometry value', category:'error'}); } } return true; @@ -265,27 +297,28 @@ link this Leaflet layer to a Recline doc

},

Private: Return a GeoJSON geomtry extracted from the document fields

  _getGeometryFromDocument: function(doc){
     if (this.geomReady){
-      if (this.state.get('geomField')){

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

        return doc.attributes[this.state.get('geomField')];
-      } else if (this.state.get('lonField') && this.state.get('latField')){

We'll create a GeoJSON like point object from the two lat/lon fields

        var lon = doc.get(this.state.get('lonField'));
+      if (this.state.get('geomField')){
+        var value = doc.get(this.state.get('geomField'));
+        if (typeof(value) === 'string'){

We have a GeoJSON string representation

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

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

          return value;
+        }
+      } else if (this.state.get('lonField') && this.state.get('latField')){

We'll create a GeoJSON like point object from the two lat/lon fields

        var lon = doc.get(this.state.get('lonField'));
         var lat = doc.get(this.state.get('latField'));
-        if (lon && lat) {
+        if (!isNaN(parseFloat(lon)) && !isNaN(parseFloat(lat))) {
           return {
             type: 'Point',
-            coordinates: [
-              doc.attributes[this.state.get('lonField')],
-              doc.attributes[this.state.get('latField')]
-              ]
+            coordinates: [lon,lat]
           };
         }
       }
       return null;
     }
-  },

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

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

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

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

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

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

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

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

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

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

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

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

} } return null; - },

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

+ },

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

  _zoomToFeatures: function(){
+    var bounds = this.features.getBounds();
+    if (bounds){
+      this.map.fitBounds(bounds);
+    } else {
+      this.map.setView(new L.LatLng(0, 0), 2);
+    }
+  },

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

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

  _setupMap: function(){
@@ -325,13 +366,28 @@ on OpenStreetMap.

e.layer.cid = e.properties.cid; } - }); + });

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

    this.features.getBounds = function(){
+      var bounds = new L.LatLngBounds();
+      this._iterateLayers(function (layer) {
+        if (layer instanceof L.Marker){
+          bounds.extend(layer.getLatLng());
+        } else {
+          if (layer.getBounds){
+            bounds.extend(layer.getBounds().getNorthEast());
+            bounds.extend(layer.getBounds().getSouthWest());
+          }
+        }
+      }, this);
+      return (typeof bounds.getNorthEast() !== 'undefined') ? bounds : null;
+    }
+
     this.map.addLayer(this.features);
 
     this.map.setView(new L.LatLng(0, 0), 2);
 
     this.mapReady = true;
-  },

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

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

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

  _selectOption: function(id,value){
     var options = $('.' + id + ' > select > option');
     if (options){
       options.each(function(opt){
diff --git a/docs/view.html b/docs/view.html
index 34f9cb34..dcb33dd2 100644
--- a/docs/view.html
+++ b/docs/view.html
@@ -1,9 +1,10 @@
       view.js           
this.el.find('.header').append(facetViewer.el);}, - setupRouting:function(){ - varself=this;varviewName=$(e.target).attr('data-view');this.updateNav(viewName);this.state.set({currentView:viewName}); - },},_bindStateChanges:function(){ - varself=this;self.state.set(update);pageView.view.state.bind('change',function(){varupdate={}; - update['view-'+pageView.id]=pageView.view.state.toJSON(); - self.state.set(update); + update['view-'+pageView.id]=pageView.view.state.toJSON();}}); -/* ========================================================== */

view.js

/*jshint multistr:true */

Recline Views

-

Recline Views are Backbone Views and in keeping with normal Backbone views -are Widgets / Components displaying something in the DOM. Like all Backbone -views they have a pointer to a model or a collection and is bound to an -element.

+

Recline Views are instances of Backbone Views and they act as 'WUI' (web +user interface) component displaying some model object in the DOM. Like all +Backbone views they have a pointer to a model (or a collection) and have an +associated DOM-style element (usually this element will be bound into the +page at some point).

Views provided by core Recline are crudely divided into two types:

@@ -165,12 +166,6 @@ initialized the DataExplorer with the relevant views themselves.

<div class="clearfix"></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: { @@ -207,7 +202,8 @@ initialized the DataExplorer with the relevant views themselves.

}), }]; }

these must be called after pageViews are created

    this.render();
-    this._bindStateChanges();

now do updates based on state (need to come after render)

    if (this.state.get('readOnly')) {
+    this._bindStateChanges();
+    this._bindFlashNotifications();

now do updates based on state (need to come after render)

    if (this.state.get('readOnly')) {
       this.setReadOnly();
     }
     if (this.state.get('currentView')) {
@@ -216,20 +212,16 @@ initialized the DataExplorer with the relevant views themselves.

this.updateNav(this.pageViews[0].id); } - this.router = new Backbone.Router(); - this.setupRouting(); - this.model.bind('query:start', function() { - my.notify('Loading data', {loader: true}); + self.notify({message: 'Loading data', loader: true}); }); this.model.bind('query:done', function() { - my.clearNotifications(); + self.clearNotifications(); self.el.find('.doc-count').text(self.model.docCount || 'Unknown'); - my.notify('Data loaded', {category: 'success'});

update navigation

        var qs = my.parseHashQueryString();
-        qs.reclineQuery = JSON.stringify(self.model.queryState.toJSON());
-        var out = my.getNewHashForQueryString(qs);

self.router.navigate(out);

      });
+        self.notify({message: 'Data loaded', category: 'success'});
+      });
     this.model.bind('query:fail', function(error) {
-        my.clearNotifications();
+        self.clearNotifications();
         var msg = '';
         if (typeof(error) == 'string') {
           msg = error;
@@ -243,14 +235,14 @@ initialized the DataExplorer with the relevant views themselves.

} else { msg = 'There was an error querying the backend'; } - my.notify(msg, {category: 'error', persist: true}); - });

retrieve basic data like fields etc + self.notify({message: msg, category: 'error', persist: true}); + });

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

    this.model.fetch()
       .done(function(dataset) {
         self.model.query(self.state.get('query'));
       })
       .fail(function(error) {
-        my.notify(error.message, {category: 'error', persist: true});
+        self.notify({message: error.message, category: 'error', persist: true});
       });
   },
 
@@ -283,25 +275,12 @@ note this.model and dataset returned are the same

Default route - this.router.route(/^(\?.)?$/, this.pageViews[0].id, function(queryString) { - self.updateNav(self.pageViews[0].id, queryString); - }); - $.each(this.pageViews, function(idx, view) { - self.router.route(/^([^?]+)(\?.)?/, 'view', function(viewId, queryString) { - self.updateNav(viewId, queryString); - }); - });

    this.router.route(/.*/, 'view', function() {
-    });
-  },
-
   updateNav: function(pageName) {
     this.el.find('.navigation li').removeClass('active');
     this.el.find('.navigation li a').removeClass('disabled');
     var $el = this.el.find('.navigation li a[data-view="' + pageName + '"]');
     $el.parent().addClass('active');
-    $el.addClass('disabled');

show the specific page

    _.each(this.pageViews, function(view, idx) {
+    $el.addClass('disabled');

show the specific page

    _.each(this.pageViews, function(view, idx) {
       if (view.id === pageName) {
         view.view.el.show();
         view.view.trigger('view:show');
@@ -327,15 +306,15 @@ note this.model and dataset returned are the same

create a state object for this view and do the job of

+ },

create a state object for this view and do the job of

a) initializing it from both data passed in and other sources (e.g. hash url)

b) ensure the state object is updated in responese to changes in subviews, query etc.

  _setupState: function(initialState) {
-    var self = this;

get data from the query string / hash url plus some defaults

    var qs = my.parseHashQueryString();
+    var self = this;

get data from the query string / hash url plus some defaults

    var qs = recline.Util.parseHashQueryString();
     var query = qs.reclineQuery;
-    query = query ? JSON.parse(query) : self.model.queryState.toJSON();

backwards compatability (now named view-graph but was named graph)

    var graphState = qs['view-graph'] || qs.graph;
-    graphState = graphState ? JSON.parse(graphState) : {};

now get default data + hash url plus initial state and initial our state object with it

    var stateData = _.extend({
+    query = query ? JSON.parse(query) : self.model.queryState.toJSON();

backwards compatability (now named view-graph but was named graph)

    var graphState = qs['view-graph'] || qs.graph;
+    graphState = graphState ? JSON.parse(graphState) : {};

now get default data + hash url plus initial state and initial our state object with it

    var stateData = _.extend({
         query: query,
         'view-graph': graphState,
         backend: this.model.backend.__type__,
@@ -348,7 +327,7 @@ note this.model and dataset returned are the same

finally ensure we update our state object when state of sub-object changes so that state is always up to date

    this.model.queryState.bind('change', function() {
+    var self = this;

finally ensure we update our state object when state of sub-object changes so that state is always up to date

    this.model.queryState.bind('change', function() {
       self.state.set({query: self.model.queryState.toJSON()});
     });
     _.each(this.pageViews, function(pageView) {
@@ -358,11 +337,58 @@ note this.model and dataset returned are the same

had problems where change not being triggered for e.g. grid view so let's do it explicitly

          self.state.set(update, {silent: true});
+          self.state.trigger('change');
         });
       }
     });
+  },
+
+  _bindFlashNotifications: function() {
+    var self = this;
+    _.each(this.pageViews, function(pageView) {
+      pageView.view.bind('recline:flash', function(flash) {
+        self.notify(flash); 
+      });
+    });
+  },

notify

+ +

Create a notification (a div.alert in div.alert-messsages) using provided +flash object. Flash attributes (all are optional):

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

clearNotifications

+ +

Clear all existing notifications

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

DataExplorer.restore

@@ -594,95 +620,6 @@ note this.model and dataset returned are the same

Miscellaneous Utilities

var urlPathRegex = /^([^?]+)(\?.*)?/;

Parse the Hash section of a URL into path and query string

my.parseHashUrl = function(hashUrl) {
-  var parsed = urlPathRegex.exec(hashUrl);
-  if (parsed === null) {
-    return {};
-  } else {
-    return {
-      path: parsed[1],
-      query: parsed[2] || ''
-    };
-  }
-};

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

my.parseQueryString = function(q) {
-  if (!q) {
-    return {};
-  }
-  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;
-};

Parse the query string out of the URL hash

my.parseHashQueryString = function() {
-  q = my.parseHashUrl(window.location.hash).query;
-  return my.parseQueryString(q);
-};

Compse a Query String

my.composeQueryString = function(queryParams) {
-  var queryString = '?';
-  var items = [];
-  $.each(queryParams, function(key, value) {
-    if (typeof(value) === 'object') {
-      value = JSON.stringify(value);
-    }
-    items.push(key + '=' + value);
-  });
-  queryString += items.join('&');
-  return queryString;
-};
-
-my.getNewHashForQueryString = function(queryParams) {
-  var queryPart = my.composeQueryString(queryParams);
-  if (window.location.hash) {

slice(1) to remove # at start

    return window.location.hash.split('?')[0].slice(1) + queryPart;
-  } else {
-    return queryPart;
-  }
-};
-
-my.setHashQueryString = function(queryParams) {
-  window.location.hash = my.getNewHashForQueryString(queryParams);
-};

notify

- -

Create a notification (a div.alert 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) options = {};
-  var tmplData = _.extend({
-    msg: message,
-    category: 'warning'
-    },
-    options);
-  var _template = ' \
-    <div class="alert alert-{{category}} fade in" data-alert="alert"><a class="close" data-dismiss="alert" href="#">×</a> \
-      {{msg}} \
-        {{#loader}} \
-        <span class="notification-loader">&nbsp;</span> \
-        {{/loader}} \
-    </div>';
-  var _templated = $.mustache(_template, tmplData); 
-  _templated = $(_templated).appendTo($('.recline-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 = $('.recline-data-explorer .alert-messages .alert');
-  $notifications.remove();
-};
 
 })(jQuery, recline.View);
 
diff --git a/download.markdown b/download.markdown
new file mode 100644
index 00000000..e60f0e95
--- /dev/null
+++ b/download.markdown
@@ -0,0 +1,53 @@
+---
+layout: container
+title: Download
+---
+
+
+
+Besides the library itself, the download package contains full source code,
+unit tests, files for debugging and a build system. The production files
+(included the same way as in the code above) are in the dist folder.
+
+

Download Recline v0.5 (master) (in-progress version)

+ +Just want the all-in-one file containing all of Recline library in a single file? Here it is: + +

recline.js all-in-one (master)

+ +[View Changelog](https://github.com/okfn/recline#changelog) + +### Dependencies + +Recline has dependencies on some third-party libraries, notably JQuery and Backbone: + +* [JQuery](http://jquery.com/) >= 1.6 +* [Backbone](http://backbonejs.org/) >= 0.5.1 +* [Underscore](http://documentcloud.github.com/underscore/) >= 1.0 + +Optional dependencies: + +* JQuery Mustache (required for all views) +* [JQuery Flot](http://code.google.com/p/flot/) >= 0.7 (required for for graph view) +* [Leaflet](http://leaflet.cloudmade.com/) >= 0.3.1 (required for map view +* [Bootstrap](http://twitter.github.com/bootstrap/) >= v2.0 (default option for CSS and UI JS but you can use your own) + +### Example + +Here is an example of the page setup for an app using every Recline component: + +{% highlight html %} + + + + + +{% include recline-deps.html %} +{% endhighlight %} + diff --git a/example-quickstart.markdown b/example-quickstart.markdown new file mode 100644 index 00000000..e87ab283 --- /dev/null +++ b/example-quickstart.markdown @@ -0,0 +1,98 @@ +--- +layout: container +title: Library - Example - Quickstart +recline-deps: true +--- + + + +This step-by-step guide will quickly get you started with Recline basics, including creating a dataset from local data and setting up a data grid to display this data. + +### Preparing your page + +Before writing any code with Recline, you need to do the following preparation steps on your page: + +1. [Download ReclineJS](download.html) and relevant dependencies. +2. Include the relevant CSS in the head section of your document: + {% highlight html %} + + + + {% endhighlight %} + +3. Include the relevant Javascript files somewhere on the page (preferably before body close tag): + {% highlight html %} + + + + + + + + +{% endhighlight %} + +4. Create a div to hold the Recline view(s): + {% highlight html %} +
{% endhighlight %} + +You're now ready to start working with Recline. + +### Creating a Dataset + +We are going to be working with the following set of data: + +{% highlight javascript %} +var data = [ + {id: 0, x: 1, y: 2, z: 3, country: 'UK', label: 'first'}, + {id: 1, x: 2, y: 4, z: 6, country: 'UK', label: 'second'}, + {id: 2, x: 3, y: 6, z: 9, country: 'US', label: 'third'} + ]; +{% endhighlight %} + +Here we have 3 documents / rows each of which is a javascript object containing keys and values (note that all values here are 'simple' but there is no reason you cannot have full objects as values. + +We can now create a recline Dataset object (and memory backend) from this raw data: + +{% highlight javascript %} +var dataset = recline.Backend.createDataset(data); +{% endhighlight %} + +Note that behind the scenes Recline will create a Memory backend for this dataset as in Recline every dataset object must have a backend from which it can push and pull data. In the case of in-memory data this is a little artificial since all the data is available locally but this makes more sense for situations where one is connecting to a remote data source (and one which may contain a lot of data). + + +### Setting up the Grid + +Let's create a data grid view to display the dataset we have just created, binding the view to the `
` we created earlier: + +{% highlight javascript %} +var grid = new recline.View.Grid({ + model: dataset, + el: $('#recline-grid') +}); +grid.render(); +{% endhighlight %} + +And hey presto: + +
 
+ + + diff --git a/library.html b/library.html index e771e7ab..7165f566 100644 --- a/library.html +++ b/library.html @@ -3,76 +3,66 @@ layout: default title: Library - Home --- -
+
-
-
-

Examples

- -

Note: A quick read through of the Concepts section will - likely be useful in understanding the details of the examples.

- -

Note: for all the following examples you should have - included relevant Recline dependencies.

- -

Simple in-memory dataset.

-
-        // Some data you have
-        // Your data must be in the form of list of documents / rows
-        // Each document/row is an Object with keys and values
-        var data = [
-            {id: 0, x: 1, y: 2, z: 3, country: 'UK', label: 'first'}
-            , {id: 1, x: 2, y: 4, z: 6, country: 'UK', label: 'second'}
-            , {id: 2, x: 3, y: 6, z: 9, country: 'US', label: 'third'}
-          ];
-        
-        // Create a Dataset object from local in-memory data
-        // Dataset object is a Backbone model - more info on attributes in model docs below
-        var dataset = recline.Backend.createDataset(data);
-        
-        // Now create the main explorer view (it will create other views as needed)
-        // DataExplorer is a Backbone View
-        var explorer = recline.View.DataExplorer({
-          model: dataset,
-          // you can specify any element to bind to in the dom
-          el: $('.data-explorer-here')
-        });
-        // Start Backbone routing (if you want routing support)
-        Backbone.history.start();
-        
- -

Creating a Dataset Explicitly with a Backend

-
-        // Connect to ElasticSearch index/type as our data source
-        // There are many other backends you can use (and you can write your own)
-        var backend = new recline.Backend.ElasticSearch();
-        
-        // Dataset is a Backbone model so the first hash become model attributes
-        var dataset = recline.Model.Dataset({
-            id: 'my-id',
-            // url for source of this dataset - will be used by backend
-            url: 'http://localhost:9200/my-index/my-type',
-            // any other metadata e.g.
-            title: 'My Dataset Title'
-          },
-          backend
-        );
-        
-
+ +

Building on Backbone, Recline + supplies components and structure to data-heavy applications by providing a + set of models (Dataset, Document/Row, Field) and views (Grid, Map, Graph + etc).

+ +

Examples

+
Note: A quick read through of the Concepts section will + likely be useful in understanding the details of the examples.
+ +
+ +
+

Loading from difference sources: Google Docs, Local CSV, DataHub

+
+
+

Twitter Example

+
+
+
+
+

Customing Display and Import using Fields

+
+
+

Listening to events

+
+
+

Setting and Getting State

+
+
+ +

Extending Recline

+
+
+

Create a new View

+
+
+

Create a new Backend

+
+
+

Create a Custom Document Object

+
+
+
-

Concepts and Structure

+

Concepts and Structure

-

Recline has a simple structure layered on top of the basic Model/View distinction inherent in Backbone.

diff --git a/recline.js b/recline.js index 271e9c54..261de327 100644 --- a/recline.js +++ b/recline.js @@ -556,157 +556,83 @@ my.backends = {}; /*jshint multistr:true */ -var util = function() { - var templates = { - transformActions: '
  • Global transform...
  • ', - cellEditor: ' \ - \ - ', - editPreview: ' \ -
    \ - \ - \ - \ - \ - \ - \ - \ - \ - {{#rows}} \ - \ - \ - \ - \ - {{/rows}} \ - \ -
    \ - before \ - \ - after \ -
    \ - {{before}} \ - \ - {{after}} \ -
    \ -
    \ - ' - }; +this.recline = this.recline || {}; +this.recline.Util = this.recline.Util || {}; - $.fn.serializeObject = function() { - var o = {}; - var a = this.serializeArray(); - $.each(a, function() { - if (o[this.name]) { - if (!o[this.name].push) { - o[this.name] = [o[this.name]]; - } - o[this.name].push(this.value || ''); - } else { - o[this.name] = this.value || ''; - } - }); - return o; - }; +(function(my) { +// ## Miscellaneous Utilities - function registerEmitter() { - var Emitter = function(obj) { - this.emit = function(obj, channel) { - if (!channel) channel = 'data'; - this.trigger(channel, obj); - }; +var urlPathRegex = /^([^?]+)(\?.*)?/; + +// Parse the Hash section of a URL into path and query string +my.parseHashUrl = function(hashUrl) { + var parsed = urlPathRegex.exec(hashUrl); + if (parsed === null) { + return {}; + } else { + return { + path: parsed[1], + query: parsed[2] || '' }; - MicroEvent.mixin(Emitter); - return new Emitter(); - } - - function listenFor(keys) { - var shortcuts = { // from jquery.hotkeys.js - 8: "backspace", 9: "tab", 13: "return", 16: "shift", 17: "ctrl", 18: "alt", 19: "pause", - 20: "capslock", 27: "esc", 32: "space", 33: "pageup", 34: "pagedown", 35: "end", 36: "home", - 37: "left", 38: "up", 39: "right", 40: "down", 45: "insert", 46: "del", - 96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", 103: "7", - 104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/", - 112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8", - 120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 191: "/", 224: "meta" - }; - window.addEventListener("keyup", function(e) { - var pressed = shortcuts[e.keyCode]; - if(_.include(keys, pressed)) app.emitter.emit("keyup", pressed); - }, false); - } - - function observeExit(elem, callback) { - var cancelButton = elem.find('.cancelButton'); - // TODO: remove (commented out as part of Backbon-i-fication - // app.emitter.on('esc', function() { - // cancelButton.click(); - // app.emitter.clear('esc'); - // }); - cancelButton.click(callback); - } - - function show( thing ) { - $('.' + thing ).show(); - $('.' + thing + '-overlay').show(); } +}; - function hide( thing ) { - $('.' + thing ).hide(); - $('.' + thing + '-overlay').hide(); - // TODO: remove or replace (commented out as part of Backbon-i-fication - // if (thing === "dialog") app.emitter.clear('esc'); // todo more elegant solution - } - - function position( thing, elem, offset ) { - var position = $(elem.target).position(); - if (offset) { - if (offset.top) position.top += offset.top; - if (offset.left) position.left += offset.left; - } - $('.' + thing + '-overlay').show().click(function(e) { - $(e.target).hide(); - $('.' + thing).hide(); - }); - $('.' + thing).show().css({top: position.top + $(elem.target).height(), left: position.left}); +// Parse a URL query string (?xyz=abc...) into a dictionary. +my.parseQueryString = function(q) { + if (!q) { + return {}; } + var urlParams = {}, + e, d = function (s) { + return unescape(s.replace(/\+/g, " ")); + }, + r = /([^&=]+)=?([^&]*)/g; - function render( template, target, options ) { - if ( !options ) options = {data: {}}; - if ( !options.data ) options = {data: options}; - var html = $.mustache( templates[template], options.data ); - var targetDom = null; - if (target instanceof jQuery) { - targetDom = target; - } else { - targetDom = $( "." + target + ":first" ); - } - if( options.append ) { - targetDom.append( html ); - } else { - targetDom.html( html ); - } - // TODO: remove (commented out as part of Backbon-i-fication - // if (template in app.after) app.after[template](); + 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; +}; + +// Parse the query string out of the URL hash +my.parseHashQueryString = function() { + q = my.parseHashUrl(window.location.hash).query; + return my.parseQueryString(q); +}; + +// Compse a Query String +my.composeQueryString = function(queryParams) { + var queryString = '?'; + var items = []; + $.each(queryParams, function(key, value) { + if (typeof(value) === 'object') { + value = JSON.stringify(value); + } + items.push(key + '=' + value); + }); + queryString += items.join('&'); + return queryString; +}; + +my.getNewHashForQueryString = function(queryParams) { + var queryPart = my.composeQueryString(queryParams); + if (window.location.hash) { + // slice(1) to remove # at start + return window.location.hash.split('?')[0].slice(1) + queryPart; + } else { + return queryPart; + } +}; + +my.setHashQueryString = function(queryParams) { + window.location.hash = my.getNewHashForQueryString(queryParams); +}; +})(this.recline.Util); - return { - registerEmitter: registerEmitter, - listenFor: listenFor, - show: show, - hide: hide, - position: position, - render: render, - observeExit: observeExit - }; -}(); /*jshint multistr:true */ this.recline = this.recline || {}; @@ -791,7 +717,6 @@ my.Graph = Backbone.View.extend({ \
    \ \ +
    \ +
    \ + \ + \ +
    \ +
    \ +
    \ + ', + onEditClick: function(e) { var editing = this.el.find('.data-table-cell-editor-editor'); if (editing.length > 0) { @@ -1405,10 +1334,12 @@ my.GridRow = Backbone.View.extend({ $(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()}); + var templated = $.mustache(this.cellEditorTemplate, {value: cell.text()}); + cell.html(templated); }, onEditorOK: function(e) { + var self = this; var cell = $(e.target); var rowId = cell.parents('tr').attr('data-id'); var field = cell.parents('td').attr('data-field'); @@ -1416,12 +1347,13 @@ my.GridRow = Backbone.View.extend({ var newData = {}; newData[field] = newValue; this.model.set(newData); - my.notify("Updating row...", {loader: true}); + this.trigger('recline:flash', {message: "Updating row...", loader: true}); this.model.save().then(function(response) { - my.notify("Row updated successfully", {category: 'success'}); + this.trigger('recline:flash', {message: "Row updated successfully", category: 'success'}); }) .fail(function() { - my.notify('Error saving row', { + this.trigger('recline:flash', { + message: 'Error saving row', category: 'error', persist: true }); @@ -1718,7 +1650,6 @@ my.Map = Backbone.View.extend({ // Each feature will have a popup associated with all the document fields. // _add: function(docs){ - var self = this; if (!(docs instanceof Array)) docs = [docs]; @@ -1753,13 +1684,13 @@ my.Map = Backbone.View.extend({ var msg = 'Wrong geometry value'; if (except.message) msg += ' (' + except.message + ')'; if (wrongSoFar <= 10) { - my.notify(msg,{category:'error'}); + self.trigger('recline:flash', {message: msg, category:'error'}); } } } else { wrongSoFar += 1 if (wrongSoFar <= 10) { - my.notify('Wrong geometry value',{category:'error'}); + self.trigger('recline:flash', {message: 'Wrong geometry value', category:'error'}); } } return true; @@ -1864,7 +1795,7 @@ my.Map = Backbone.View.extend({ // on [OpenStreetMap](http://openstreetmap.org). // _setupMap: function(){ - + var self = this; this.map = new L.Map(this.$map.get(0)); var mapUrl = "http://otile{s}.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.png"; @@ -1904,6 +1835,14 @@ my.Map = Backbone.View.extend({ this.map.setView(new L.LatLng(0, 0), 2); + var popup = new L.Popup(); + this.map.on('click', function(e) { + var latlngStr = '(' + e.latlng.lat.toFixed(3) + ', ' + e.latlng.lng.toFixed(3) + ')'; + popup.setLatLng(e.latlng); + popup.setContent("You clicked the map at " + latlngStr); + self.map.openPopup(popup); + }); + this.mapReady = true; }, @@ -1933,82 +1872,17 @@ this.recline.View = this.recline.View || {}; // Views module following classic module pattern (function($, my) { -// View (Dialog) for doing data transformations on whole dataset. -my.DataTransform = Backbone.View.extend({ - className: 'transform-view', - template: ' \ -
    \ - Recursive transform on all rows \ -
    \ -
    \ -
    \ -

    Traverse and transform objects by visiting every node on a recursive walk using js-traverse.

    \ - \ - \ - \ - \ - \ - \ -
    \ -
    \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ -
    \ - Expression \ -
    \ -
    \ - \ -
    \ -
    \ - No syntax error. \ -
    \ -
    \ - Preview \ -
    \ -
    \ -
    \ -
    \ -
    \ -
    \ -
    \ -
    \ -
    \ -
    \ - \ - ', - - initialize: function() { - this.el = $(this.el); - }, - - render: function() { - this.el.html(this.template); - } -}); - - +// ## ColumnTransform +// // View (Dialog) for doing data transformations (on columns of data). my.ColumnTransform = Backbone.View.extend({ - className: 'transform-column-view', + className: 'transform-column-view modal fade in', template: ' \ -
    \ - Functional transform on column {{name}} \ + \ -
    \ + \ - \
    \ - \ - \
    \ ', events: { @@ -2350,6 +2250,7 @@ my.DataExplorer = Backbone.View.extend({ // these must be called after pageViews are created this.render(); this._bindStateChanges(); + this._bindFlashNotifications(); // now do updates based on state (need to come after render) if (this.state.get('readOnly')) { this.setReadOnly(); @@ -2360,24 +2261,16 @@ my.DataExplorer = Backbone.View.extend({ this.updateNav(this.pageViews[0].id); } - this.router = new Backbone.Router(); - this.setupRouting(); - this.model.bind('query:start', function() { - my.notify('Loading data', {loader: true}); + self.notify({message: 'Loading data', loader: true}); }); this.model.bind('query:done', function() { - my.clearNotifications(); + self.clearNotifications(); self.el.find('.doc-count').text(self.model.docCount || 'Unknown'); - my.notify('Data loaded', {category: 'success'}); - // update navigation - var qs = my.parseHashQueryString(); - qs.reclineQuery = JSON.stringify(self.model.queryState.toJSON()); - var out = my.getNewHashForQueryString(qs); - // self.router.navigate(out); + self.notify({message: 'Data loaded', category: 'success'}); }); this.model.bind('query:fail', function(error) { - my.clearNotifications(); + self.clearNotifications(); var msg = ''; if (typeof(error) == 'string') { msg = error; @@ -2391,7 +2284,7 @@ my.DataExplorer = Backbone.View.extend({ } else { msg = 'There was an error querying the backend'; } - my.notify(msg, {category: 'error', persist: true}); + self.notify({message: msg, category: 'error', persist: true}); }); // retrieve basic data like fields etc @@ -2401,7 +2294,7 @@ my.DataExplorer = Backbone.View.extend({ self.model.query(self.state.get('query')); }) .fail(function(error) { - my.notify(error.message, {category: 'error', persist: true}); + self.notify({message: error.message, category: 'error', persist: true}); }); }, @@ -2434,21 +2327,6 @@ my.DataExplorer = Backbone.View.extend({ this.el.find('.header').append(facetViewer.el); }, - setupRouting: function() { - var self = this; - // Default route -// this.router.route(/^(\?.*)?$/, this.pageViews[0].id, function(queryString) { -// self.updateNav(self.pageViews[0].id, queryString); -// }); -// $.each(this.pageViews, function(idx, view) { -// self.router.route(/^([^?]+)(\?.*)?/, 'view', function(viewId, queryString) { -// self.updateNav(viewId, queryString); -// }); -// }); - this.router.route(/.*/, 'view', function() { - }); - }, - updateNav: function(pageName) { this.el.find('.navigation li').removeClass('active'); this.el.find('.navigation li a').removeClass('disabled'); @@ -2492,7 +2370,7 @@ my.DataExplorer = Backbone.View.extend({ _setupState: function(initialState) { var self = this; // get data from the query string / hash url plus some defaults - var qs = my.parseHashQueryString(); + var qs = recline.Util.parseHashQueryString(); var query = qs.reclineQuery; query = query ? JSON.parse(query) : self.model.queryState.toJSON(); // backwards compatability (now named view-graph but was named graph) @@ -2532,6 +2410,57 @@ my.DataExplorer = Backbone.View.extend({ }); } }); + }, + + _bindFlashNotifications: function() { + var self = this; + _.each(this.pageViews, function(pageView) { + pageView.view.bind('recline:flash', function(flash) { + self.notify(flash); + }); + }); + }, + + // ### notify + // + // Create a notification (a div.alert in div.alert-messsages) using provided + // flash object. Flash attributes (all are optional): + // + // * message: message to show. + // * category: warning (default), success, error + // * persist: if true alert is persistent, o/w hidden after 3s (default = false) + // * loader: if true show loading spinner + notify: function(flash) { + var tmplData = _.extend({ + message: '', + category: 'warning' + }, + flash + ); + var _template = ' \ +
    × \ + {{message}} \ + {{#loader}} \ +   \ + {{/loader}} \ +
    '; + var _templated = $.mustache(_template, tmplData); + _templated = $(_templated).appendTo($('.recline-data-explorer .alert-messages')); + if (!flash.persist) { + setTimeout(function() { + $(_templated).fadeOut(1000, function() { + $(this).remove(); + }); + }, 1000); + } + }, + + // ### clearNotifications + // + // Clear all existing notifications + clearNotifications: function() { + var $notifications = $('.recline-data-explorer .alert-messages .alert'); + $notifications.remove(); } }); @@ -2772,118 +2701,6 @@ my.FacetViewer = Backbone.View.extend({ } }); -/* ========================================================== */ -// ## Miscellaneous Utilities - -var urlPathRegex = /^([^?]+)(\?.*)?/; - -// Parse the Hash section of a URL into path and query string -my.parseHashUrl = function(hashUrl) { - var parsed = urlPathRegex.exec(hashUrl); - if (parsed === null) { - return {}; - } else { - return { - path: parsed[1], - query: parsed[2] || '' - }; - } -}; - -// Parse a URL query string (?xyz=abc...) into a dictionary. -my.parseQueryString = function(q) { - if (!q) { - return {}; - } - 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; -}; - -// Parse the query string out of the URL hash -my.parseHashQueryString = function() { - q = my.parseHashUrl(window.location.hash).query; - return my.parseQueryString(q); -}; - -// Compse a Query String -my.composeQueryString = function(queryParams) { - var queryString = '?'; - var items = []; - $.each(queryParams, function(key, value) { - if (typeof(value) === 'object') { - value = JSON.stringify(value); - } - items.push(key + '=' + value); - }); - queryString += items.join('&'); - return queryString; -}; - -my.getNewHashForQueryString = function(queryParams) { - var queryPart = my.composeQueryString(queryParams); - if (window.location.hash) { - // slice(1) to remove # at start - return window.location.hash.split('?')[0].slice(1) + queryPart; - } else { - return queryPart; - } -}; - -my.setHashQueryString = function(queryParams) { - window.location.hash = my.getNewHashForQueryString(queryParams); -}; - -// ## notify -// -// Create a notification (a div.alert 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) options = {}; - var tmplData = _.extend({ - msg: message, - category: 'warning' - }, - options); - var _template = ' \ -
    × \ - {{msg}} \ - {{#loader}} \ -   \ - {{/loader}} \ -
    '; - var _templated = $.mustache(_template, tmplData); - _templated = $(_templated).appendTo($('.recline-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 = $('.recline-data-explorer .alert-messages .alert'); - $notifications.remove(); -}; })(jQuery, recline.View); @@ -3671,6 +3488,7 @@ this.recline.Backend = this.recline.Backend || {}; backend.addDataset(datasetInfo); var dataset = new recline.Model.Dataset({id: metadata.id}, backend); dataset.fetch(); + dataset.query(); return dataset; }; diff --git a/src/backend/memory.js b/src/backend/memory.js index e013aa19..4783c20d 100644 --- a/src/backend/memory.js +++ b/src/backend/memory.js @@ -37,6 +37,7 @@ this.recline.Backend = this.recline.Backend || {}; backend.addDataset(datasetInfo); var dataset = new recline.Model.Dataset({id: metadata.id}, backend); dataset.fetch(); + dataset.query(); return dataset; }; diff --git a/src/view-grid.js b/src/view-grid.js index 30e81a33..fc158723 100644 --- a/src/view-grid.js +++ b/src/view-grid.js @@ -193,7 +193,7 @@ my.Grid = Backbone.View.extend({ }); newView.render(); }); - this.el.toggleClass('no-hidden', (self.state.get('hiddenFields').length === 0)); + this.el.find('.recline-grid').toggleClass('no-hidden', (self.state.get('hiddenFields').length === 0)); return this; } }); diff --git a/src/view.js b/src/view.js index ac4a2f28..ed3edfce 100644 --- a/src/view.js +++ b/src/view.js @@ -2,10 +2,11 @@ // # Recline Views // -// Recline Views are Backbone Views and in keeping with normal Backbone views -// are Widgets / Components displaying something in the DOM. Like all Backbone -// views they have a pointer to a model or a collection and is bound to an -// element. +// Recline Views are instances of Backbone Views and they act as 'WUI' (web +// user interface) component displaying some model object in the DOM. Like all +// Backbone views they have a pointer to a model (or a collection) and have an +// associated DOM-style element (usually this element will be bound into the +// page at some point). // // Views provided by core Recline are crudely divided into two types: //