diff --git a/docs/backend/localcsv.html b/docs/backend/localcsv.html index 71f105bf..3c708f28 100644 --- a/docs/backend/localcsv.html +++ b/docs/backend/localcsv.html @@ -51,7 +51,9 @@ thttp://www.uselesscode.org/javascript/csv/
Flush the field buffer
field = '';
fieldQuoted = false;
- } else {If it's not a ", add it to the field buffer
if (cur !== '"') {
+ } else {If it's not a delimiter, add it to the field buffer
if (cur !== delimiter) {
field += cur;
} else {
if (!inQuote) {We are not in a quote, start a quote
inQuote = true;
fieldQuoted = true;
- } else {Next char is ", this is an escaped "
if (s.charAt(i + 1) === '"') {
- field += '"';Skip the next char
i += 1;
+ } else {Next char is delimiter, this is an escaped delimiter
if (s.charAt(i + 1) === delimiter) {
+ field += delimiter;Skip the next char
i += 1;
} else {It's not escaping, so end quote
inQuote = false;
}
}
diff --git a/docs/backend/memory.html b/docs/backend/memory.html
index 6304cee6..e1c865a7 100644
--- a/docs/backend/memory.html
+++ b/docs/backend/memory.html
@@ -35,6 +35,7 @@ If not defined (or id not provided) id will be autogenerated.
backend.addDataset(datasetInfo);
var dataset = new recline.Model.Dataset({id: metadata.id}, backend);
dataset.fetch();
+ dataset.query();
return dataset;
};we need the model.fields to render properly
this.model.bind('change', this.render);
+ _.bindAll(this, 'render', 'redraw');
+ this.needToRedraw = false;
+ this.model.bind('change', this.render);
this.model.fields.bind('reset', this.render);
this.model.fields.bind('add', this.render);
this.model.currentDocuments.bind('add', this.redraw);
- this.model.currentDocuments.bind('reset', this.redraw);
+ this.model.currentDocuments.bind('reset', this.redraw);because we cannot redraw when hidden we may need when becoming visible
this.bind('view:show', function() {
+ if (this.needToRedraw) {
+ self.redraw();
+ }
+ });
var stateData = _.extend({
- group: null,
- series: [],
+ group: null,so that at least one series chooser box shows up
series: [],
graphType: 'lines-and-points'
},
options.state
@@ -105,17 +121,41 @@ generate the element itself (you can then append view.el to the DOM.
},
render: function() {
- htmls = $.mustache(this.template, this.model.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();
+ var self = this;
+ var tmplData = this.model.toTemplateJSON();
+ var htmls = $.mustache(this.template, tmplData);
+ $(this.el).html(htmls);
+ this.$graph = this.el.find('.panel.graph');set up editor from state
if (this.state.get('graphType')) {
+ this._selectOption('.editor-type', this.state.get('graphType'));
+ }
+ if (this.state.get('group')) {
+ this._selectOption('.editor-group', this.state.get('group'));
+ }ensure at least one series box shows up
var tmpSeries = [""];
+ if (this.state.get('series').length > 0) {
+ tmpSeries = this.state.get('series');
+ }
+ _.each(tmpSeries, function(series, idx) {
+ self.addSeries(idx);
+ self._selectOption('.editor-series.js-series-' + idx, series);
+ });
return this;
+ },Private: Helper function to select an option from a select list
_selectOption: function(id,value){
+ var options = this.el.find(id + ' select > option');
+ if (options) {
+ options.each(function(opt){
+ if (this.value == value) {
+ $(this).attr('selected','selected');
+ return false;
+ }
+ });
+ }
},
onEditorSubmit: function(e) {
var select = this.el.find('.editor-group select');
- $editor = this;
- var series = this.$series.map(function () {
+ var $editor = this;
+ var $series = this.el.find('.editor-series select');
+ var series = $series.map(function () {
return $(this).val();
});
var updatedState = {
@@ -127,50 +167,59 @@ could be simpler just to have a common template! There appear to be issues generating a Flot graph if either:
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
var areWeVisible = !jQuery.expr.filters.hidden(this.el[0]);
if ((!areWeVisible || this.model.currentDocuments.length === 0)) {
+ this.needToRedraw = true;
return;
+ }check we have something to plot
if (this.state.get('group') && this.state.get('series')) {
+ var series = this.createSeries();
+ var options = this.getGraphOptions(this.state.attributes.graphType);
+ this.plot = $.plot(this.$graph, series, options);
+ this.setupTooltips();
}
- var series = this.createSeries();
- var options = this.getGraphOptions(this.state.attributes.graphType);
- this.plot = $.plot(this.$graph, series, options);
- this.setupTooltips();create this.plot and cache it - if (!this.plot) { - this.plot = $.plot(this.$graph, series, options); - } else { - this.plot.parseOptions(options); - this.plot.setData(this.createSeries()); - this.plot.resize(); - this.plot.setupGrid(); - this.plot.draw(); - }
},needs to be function as can depend on state
getGraphOptions: function(typeId) {
- var self = this;special tickformatter to show labels rather than numbers
var tickFormatter = function (val) {
+ },Get options for Flot Graph
+ +needs to be function as can depend on state
+ +@param typeId graphType id (lines, lines-and-points etc)
getGraphOptions: function(typeId) {
+ var self = this;special tickformatter to show labels rather than numbers +TODO: we should really use tickFormatter and 1 interval ticks if (and +only if) x-axis values are non-numeric +However, that is non-trivial to work out from a dataset (datasets may +have no field type info). Thus at present we only do this for bars.
var tickFormatter = function (val) {
if (self.model.currentDocuments.models[val]) {
- var out = self.model.currentDocuments.models[val].get(self.state.attributes.group);if the value was in fact a number we want that not the
if (typeof(out) == 'number') {
+ var out = self.model.currentDocuments.models[val].get(self.state.attributes.group);if the value was in fact a number we want that not the
if (typeof(out) == 'number') {
return val;
} else {
return out;
}
}
return val;
- };TODO: we should really use tickFormatter and 1 interval ticks if (and -only if) x-axis values are non-numeric -However, that is non-trivial to work out from a dataset (datasets may -have no field type info). Thus at present we only do this for bars.
var options = {
+ };
+
+ var xaxis = {};check for time series on x-axis
if (this.model.fields.get(this.state.get('group')).get('type') === 'date') {
+ xaxis.mode = 'time';
+ xaxis.timeformat = '%y-%b';
+ }
+ var optionsPerGraphType = {
lines: {
- series: {
- lines: { show: true }
- }
+ series: {
+ lines: { show: true }
+ },
+ xaxis: xaxis
},
points: {
series: {
points: { show: true }
},
+ xaxis: xaxis,
grid: { hoverable: true, clickable: true }
},
'lines-and-points': {
@@ -178,6 +227,7 @@ have no field type info). Thus at present we only do this for bars.
points: { show: true },
lines: { show: true }
},
+ xaxis: xaxis,
grid: { hoverable: true, clickable: true }
},
bars: {
@@ -201,7 +251,7 @@ have no field type info). Thus at present we only do this for bars.
}
}
};
- return options[typeId];
+ return optionsPerGraphType[typeId];
},
setupTooltips: function() {
@@ -227,12 +277,20 @@ have no field type info). Thus at present we only do this for bars.
$("#flot-tooltip").remove();
var x = item.datapoint[0];
- var y = item.datapoint[1];convert back from 'index' value on x-axis (e.g. in cases where non-number values)
if (self.model.currentDocuments.models[x]) {
+ var y = item.datapoint[1];it's horizontal so we have to flip
if (self.state.attributes.graphType === 'bars') {
+ var _tmp = x;
+ x = y;
+ y = _tmp;
+ }convert back from 'index' value on x-axis (e.g. in cases where non-number values)
if (self.model.currentDocuments.models[x]) {
x = self.model.currentDocuments.models[x].get(self.state.attributes.group);
} else {
x = x.toFixed(2);
}
- y = y.toFixed(2);
+ y = y.toFixed(2);is it time series
var xfield = self.model.fields.get(self.state.attributes.group);
+ var isDateTime = xfield.get('type') === 'date';
+ if (isDateTime) {
+ x = new Date(parseInt(x)).toLocaleDateString();
+ }
var content = _.template('<%= group %> = <%= x %>, <%= series %> = <%= y %>', {
group: self.state.attributes.group,
@@ -256,11 +314,16 @@ have no field type info). Thus at present we only do this for bars.
_.each(this.state.attributes.series, function(field) {
var points = [];
_.each(self.model.currentDocuments.models, function(doc, index) {
- var x = doc.get(self.state.attributes.group);
- var y = doc.get(field);
+ var xfield = self.model.fields.get(self.state.attributes.group);
+ var x = doc.getFieldValue(xfield);time series
var isDateTime = xfield.get('type') === 'date';
+ if (isDateTime) {
+ x = new Date(x);
+ }
+ var yfield = self.model.fields.get(field);
+ var y = doc.getFieldValue(yfield);
if (typeof x === 'string') {
x = index;
- }horizontal bar chart
if (self.state.attributes.graphType == 'bars') {
+ }horizontal bar chart
if (self.state.attributes.graphType == 'bars') {
points.push([y, x]);
} else {
points.push([x, y]);
@@ -269,45 +332,36 @@ have no field type info). Thus at present we only do this for bars.
series.push({data: points, label: field});
});
return series;
- },Public: Adds a new empty series select box to the editor.
+ },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.
+@param [int] idx index of this series in the list of series
-Returns itself.
addSeries: function (e) {
- e.preventDefault();
- var element = this.$seriesClone.clone(),
- label = element.find('label'),
- index = this.$series.length;
+Returns itself.
addSeries: function (idx) {
+ var data = _.extend({
+ seriesIndex: idx,
+ seriesName: String.fromCharCode(idx + 64 + 1),
+ }, this.model.toTemplateJSON());
- 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));
+ var htmls = $.mustache(this.templateSeriesEditor, data);
+ this.el.find('.editor-series-group').append(htmls);
return this;
- },Public: Removes a series list item from the editor.
+ }, + + _onAddSeries: function(e) { + e.preventDefault(); + this.addSeries(this.state.get('series').length); + },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');
- }
+ },
});
})(jQuery, recline.View);
diff --git a/docs/view-grid.html b/docs/view-grid.html
index 7999d978..2c57e2cc 100644
--- a/docs/view-grid.html
+++ b/docs/view-grid.html
@@ -31,16 +31,7 @@
'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)). -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.tempState.currentColumn = $(e.target).closest('.column-header').attr('data-field');
},
@@ -72,20 +63,20 @@ Column and row menus important this is == as the currentRow will be string (as comes + var self = this; + var doc = _.find(self.model.currentDocuments.models, function(doc) {
important this is == as the currentRow will be string (as comes from DOM) while id may be int
return doc.id == self.tempState.currentRow;
});
doc.destroy().then(function() {
self.model.currentDocuments.remove(doc);
- my.notify("Row deleted successfully");
+ self.trigger('recline:flash', {message: "Row deleted successfully"});
}).fail(function(err) {
- my.notify("Errorz! " + err);
+ self.trigger('recline:flash', {message: "Errorz! " + err});
});
}
};
@@ -93,33 +84,16 @@ from DOM) while id may be int pass the flash message up the chain
view.bind('recline:flash', function(flash) {
+ self.trigger('recline:flash', flash);
});
view.state = this.tempState;
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 recline.View.DataTransform({
- });
- view.render();
- $el.empty();
- $el.append(view.el);
- util.observeExit($el, function() {
- util.hide('dialog');
- });
- $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' });
+ this.el.append(view.el);
+ view.el.modal();
},
setColumnSort: function(order) {
@@ -131,7 +105,7 @@ from DOM) while id may be int change event not being triggered (because it is an array?) so trigger manually
this.state.trigger('change');
this.render();
},
@@ -139,7 +113,7 @@ from DOM) while id may be int ======================================================
+ },======================================================
template: ' \
<table class="recline-grid table-striped table-condensed" cellspacing="0"> \
@@ -183,7 +157,7 @@ from DOM) while id may be int TODO: move this sort of thing into a toTemplateJSON method on Dataset?
modelData.fields = _.map(this.fields, function(field) { return field.toJSON(); });
+ modelData.notEmpty = ( this.fields.length > 0 );TODO: move this sort of thing into a toTemplateJSON method on Dataset?
modelData.fields = _.map(this.fields, function(field) { return field.toJSON(); });
return modelData;
},
render: function() {
@@ -203,10 +177,10 @@ from DOM) while id may be int Since we want this to update in place it is up to creator to provider the element to attach to.
@@ -269,8 +243,20 @@ var row = new GridRow({ var html = $.mustache(this.template, this.toTemplateJSON()); $(this.el).html(html); return this; - },=================== -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
{
- // 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 Public: Adds the necessary elements to the page.
@@ -173,6 +193,13 @@ to display properlyUI Event handlers
Public: Update map with user options
@@ -205,6 +232,10 @@ type selected.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 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. 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. 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 Jump To … 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; "> </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
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');
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
var viewName = $(e.target).attr('data-view');
this.updateNav(viewName);
this.state.set({currentView: viewName});
- }, 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
},
_bindStateChanges: 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() {
+ 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
self.state.set(update);
pageView.view.state.bind('change', function() {
var update = {};
- update['view-' + pageView.id] = pageView.view.state.toJSON();
- self.state.set(update);
+ update['view-' + pageView.id] = pageView.view.state.toJSON(); 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"> </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"> </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/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: ' \
- \
- \
- \
- \
- \
- before \
- \
- \
- after \
- \
- \
- \
- \
- {{#rows}} \
- \
- \
- {{before}} \
- \
- \
- {{after}} \
- \
- \
- {{/rows}} \
- \
-
\
- \
- '
- };
+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({
\
\