Merge branch 'master' into gh-pages
This commit is contained in:
commit
e57340fa56
48
README.md
48
README.md
@ -1,26 +1,60 @@
|
||||
# Recline
|
||||
|
||||
A pure javascript data explorer and data refinery. Imagine it as a spreadsheet plus Google Refine plus Visualization toolkit, all in pure javascript and html.
|
||||
Recline DataExplorer is an open-source pure javascript data explorer and data
|
||||
refinery. Imagine it as a spreadsheet plus Google Refine plus Visualization
|
||||
toolkit, all in pure javascript and html.
|
||||
|
||||
Designed for standalone use or as a library to integrate into your own app.
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
* CSV/JSON export your entire database for integration with spreadsheets or
|
||||
[Google Refine](http://code.google.com/p/google-refine/)
|
||||
* Open-source (and heavy reuser of existing open-source libraries)
|
||||
* Pure javascript (no Flash) and designed for integration -- so it is easy to
|
||||
embed in other sites and applications
|
||||
* View and edit your data in clean tabular interface
|
||||
* Bulk update/clean your data using an easy scripting UI
|
||||
* Import by directly downloading from JSON APIs or by uploading files
|
||||
* Visualize your data
|
||||
|
||||

|
||||
|
||||
|
||||
## Demo App
|
||||
|
||||
Open demo/index.html in your favourite browser.
|
||||
|
||||
## Minifying dependencies
|
||||
|
||||
## Developer Notes
|
||||
|
||||
### Minifying dependencies
|
||||
|
||||
npm install -g uglify
|
||||
cd vendor
|
||||
cat *.js | uglifyjs -o ../src/deps-min.js
|
||||
|
||||
note: make sure underscore.js goes in at the top of the file as a few deps currently depend on it
|
||||
|
||||
|
||||
## Copyright and License
|
||||
|
||||
Copyright 2011 Max Ogden and Rufus Pollock.
|
||||
|
||||
Licensed under the MIT license:
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
||||
|
||||
@ -3,14 +3,14 @@ $(function() {
|
||||
// window.$container = $('.container .right-panel');
|
||||
window.$container = $('.container');
|
||||
var dataset = demoDataset();
|
||||
window.dataExplorer = new recline.DataExplorer({
|
||||
window.dataExplorer = new recline.View.DataExplorer({
|
||||
model: dataset
|
||||
});
|
||||
window.$container.append(window.dataExplorer.el);
|
||||
setupLoadFromWebstore(function(dataset) {
|
||||
window.dataExplorer.remove();
|
||||
window.dataExplorer = null;
|
||||
window.dataExplorer = new recline.DataExplorer({
|
||||
window.dataExplorer = new recline.View.DataExplorer({
|
||||
model: dataset,
|
||||
});
|
||||
window.$container.append(window.dataExplorer.el);
|
||||
@ -27,21 +27,20 @@ function demoDataset() {
|
||||
var indata = {
|
||||
headers: ['x', 'y', 'z']
|
||||
, rows: [
|
||||
{x: 1, y: 2, z: 3}
|
||||
, {x: 2, y: 4, z: 6}
|
||||
, {x: 3, y: 6, z: 9}
|
||||
, {x: 4, y: 8, z: 12}
|
||||
, {x: 5, y: 10, z: 15}
|
||||
, {x: 6, y: 12, z: 18}
|
||||
{id: 0, x: 1, y: 2, z: 3}
|
||||
, {id: 1, x: 2, y: 4, z: 6}
|
||||
, {id: 2, x: 3, y: 6, z: 9}
|
||||
, {id: 3, x: 4, y: 8, z: 12}
|
||||
, {id: 4, x: 5, y: 10, z: 15}
|
||||
, {id: 5, x: 6, y: 12, z: 18}
|
||||
]
|
||||
};
|
||||
// this is all rather artificial here but would make more sense with more complex backend
|
||||
var backend = new recline.BackendMemory();
|
||||
backend.addDataset({
|
||||
var backend = new recline.Model.BackendMemory({
|
||||
metadata: metadata,
|
||||
data: indata
|
||||
});
|
||||
recline.setBackend(backend);
|
||||
recline.Model.setBackend(backend);
|
||||
var dataset = backend.getDataset(datasetId);
|
||||
return dataset;
|
||||
}
|
||||
@ -54,10 +53,10 @@ function setupLoadFromWebstore(callback) {
|
||||
e.preventDefault();
|
||||
var $form = $(e.target);
|
||||
var source = $form.find('input[name="source"]').val();
|
||||
var backend = new recline.BackendWebstore({
|
||||
var backend = new recline.Model.BackendWebstore({
|
||||
url: source
|
||||
});
|
||||
recline.setBackend(backend);
|
||||
recline.Model.setBackend(backend);
|
||||
var dataset = backend.getDataset();
|
||||
callback(dataset);
|
||||
});
|
||||
|
||||
@ -239,3 +239,13 @@ span.tooltip-status {
|
||||
right: 0px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.nav-pagination {
|
||||
display: inline;
|
||||
float: right;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.nav-pagination input {
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
@ -1,48 +0,0 @@
|
||||
// replaces `Backbone.sync` with a OKFN webstore based tabular data source
|
||||
|
||||
var WebStore = function(url) {
|
||||
this.url = url;
|
||||
this.headers = [];
|
||||
this.totalRows = 0;
|
||||
this.getTabularData = function() {
|
||||
var dfd = $.Deferred();
|
||||
var tabularData = {
|
||||
headers: ['x', 'y', 'z']
|
||||
, rows: [
|
||||
{x: 1, y: 2, z: 3}
|
||||
, {x: 2, y: 4, z: 6}
|
||||
, {x: 3, y: 6, z: 9}
|
||||
, {x: 4, y: 8, z: 12}
|
||||
, {x: 5, y: 10, z: 15}
|
||||
, {x: 6, y: 12, z: 18}
|
||||
]
|
||||
, getLength: function() { return this.rows.length; }
|
||||
, getRows: function(numRows, start) {
|
||||
if (start === undefined) {
|
||||
start = 0;
|
||||
}
|
||||
var dfd = $.Deferred();
|
||||
var results = this.rows.slice(start, start + numRows);
|
||||
dfd.resolve(results);
|
||||
return dfd.promise();
|
||||
}
|
||||
}
|
||||
dfd.resolve(tabularData);
|
||||
return dfd.promise();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Override `Backbone.sync` to delegate to the model or collection's
|
||||
// webStore property, which should be an instance of `WebStore`.
|
||||
Backbone.sync = function(method, model, options) {
|
||||
var resp;
|
||||
var store = model.webStore || model.collection.webStore;
|
||||
|
||||
if (method === "read") {
|
||||
store.getTabularData().then(function(tabularData) {
|
||||
tabularData.getRows(10).then(options.success, options.error)
|
||||
})
|
||||
}
|
||||
|
||||
};
|
||||
@ -25,6 +25,7 @@ var costco = function() {
|
||||
preview.push({before: JSON.stringify(before), after: JSON.stringify(after)});
|
||||
}
|
||||
}
|
||||
// TODO: 2012-01-05 Move this out of this function and up into (view) functions that call this
|
||||
util.render('editPreview', 'expression-preview-container', {rows: preview});
|
||||
}
|
||||
|
||||
@ -166,4 +167,4 @@ var costco = function() {
|
||||
ensureCommit: ensureCommit,
|
||||
uploadCSV: uploadCSV
|
||||
};
|
||||
}();
|
||||
}();
|
||||
|
||||
121
src/model.js
121
src/model.js
@ -1,61 +1,105 @@
|
||||
this.recline = this.recline || {};
|
||||
|
||||
// Models module following classic module pattern
|
||||
recline.Model = function($) {
|
||||
|
||||
var my = {};
|
||||
|
||||
// A Dataset model.
|
||||
recline.Dataset = Backbone.Model.extend({
|
||||
my.Dataset = Backbone.Model.extend({
|
||||
__type__: 'Dataset',
|
||||
initialize: function() {
|
||||
this.currentDocuments = new my.DocumentList();
|
||||
},
|
||||
|
||||
getLength: function() {
|
||||
return this.rowCount;
|
||||
},
|
||||
|
||||
// Get rows (documents) from the backend returning a recline.DocumentList
|
||||
//
|
||||
// TODO: ? rename to getDocuments?
|
||||
//
|
||||
// this does not fit very well with Backbone setup. Backbone really expects you to know the ids of objects your are fetching (which you do in classic RESTful ajax-y world). But this paradigm does not fill well with data set up we have here.
|
||||
// This also illustrates the limitations of separating the Dataset and the Backend
|
||||
getRows: function(numRows, start) {
|
||||
return this.backend.getRows(this.id, numRows, start);
|
||||
var self = this;
|
||||
var dfd = $.Deferred();
|
||||
this.backend.getRows(this.id, numRows, start).then(function(rows) {
|
||||
var docs = _.map(rows, function(row) {
|
||||
return new my.Document(row);
|
||||
});
|
||||
self.currentDocuments.reset(docs);
|
||||
dfd.resolve(self.currentDocuments);
|
||||
});
|
||||
return dfd.promise();
|
||||
}
|
||||
});
|
||||
|
||||
recline.Document = Backbone.Model.extend({});
|
||||
my.Document = Backbone.Model.extend({
|
||||
__type__: 'Document'
|
||||
});
|
||||
|
||||
recline.DocumentList = Backbone.Collection.extend({
|
||||
my.DocumentList = Backbone.Collection.extend({
|
||||
__type__: 'DocumentList',
|
||||
// webStore: new WebStore(this.url),
|
||||
model: recline.Document
|
||||
})
|
||||
model: my.Document
|
||||
});
|
||||
|
||||
// Backends section
|
||||
// ================
|
||||
|
||||
recline.setBackend = function(backend) {
|
||||
my.setBackend = function(backend) {
|
||||
Backbone.sync = backend.sync;
|
||||
};
|
||||
|
||||
// Backend which just caches in memory
|
||||
//
|
||||
// Does not need to be a backbone model but provides some conveience
|
||||
recline.BackendMemory = Backbone.Model.extend({
|
||||
initialize: function() {
|
||||
this._datasetCache = {}
|
||||
my.BackendMemory = Backbone.Model.extend({
|
||||
// Initialize a Backend with a local in-memory dataset.
|
||||
//
|
||||
// NB: We can handle one and only one dataset at a time.
|
||||
//
|
||||
// :param dataset: the data for a dataset on which operations will be
|
||||
// performed. In the form of a hash with metadata and data attributes.
|
||||
// - metadata: hash of key/value attributes of any kind (but usually with title attribute)
|
||||
// - data: hash with 2 keys:
|
||||
// - headers: list of header names/labels
|
||||
// - rows: list of hashes, each hash being one row. A row *must* have an id attribute which is unique.
|
||||
//
|
||||
// Example of data:
|
||||
//
|
||||
// {
|
||||
// headers: ['x', 'y', 'z']
|
||||
// , rows: [
|
||||
// {id: 0, x: 1, y: 2, z: 3}
|
||||
// , {id: 1, x: 2, y: 4, z: 6}
|
||||
// ]
|
||||
// };
|
||||
initialize: function(dataset) {
|
||||
// deep copy
|
||||
this._datasetAsData = $.extend(true, {}, dataset);
|
||||
_.bindAll(this, 'sync');
|
||||
},
|
||||
// dataset is object with metadata and data attributes
|
||||
addDataset: function(dataset) {
|
||||
this._datasetCache[dataset.metadata.id] = dataset;
|
||||
},
|
||||
getDataset: function(id) {
|
||||
var dataset = new recline.Dataset({
|
||||
id: id
|
||||
getDataset: function() {
|
||||
var dataset = new my.Dataset({
|
||||
id: this._datasetAsData.metadata.id
|
||||
});
|
||||
// this is a bit weird but problem is in sync this is set to parent model object so need to give dataset a reference to backend explicitly
|
||||
dataset.backend = this;
|
||||
return dataset;
|
||||
},
|
||||
sync: function(method, model, options) {
|
||||
var self = this;
|
||||
if (method === "read") {
|
||||
var dfd = $.Deferred();
|
||||
// this switching on object type is rather horrible
|
||||
// think may make more sense to do work in individual objects rather than in central Backbone.sync
|
||||
if (this.__type__ == 'Dataset') {
|
||||
var dataset = this;
|
||||
var rawDataset = this.backend._datasetCache[model.id];
|
||||
if (model.__type__ == 'Dataset') {
|
||||
var dataset = model;
|
||||
var rawDataset = this._datasetAsData;
|
||||
dataset.set(rawDataset.metadata);
|
||||
// here we munge it all onto Dataset
|
||||
dataset.set({
|
||||
headers: rawDataset.data.headers
|
||||
});
|
||||
@ -63,6 +107,28 @@ recline.BackendMemory = Backbone.Model.extend({
|
||||
dfd.resolve(dataset);
|
||||
}
|
||||
return dfd.promise();
|
||||
} else if (method === 'update') {
|
||||
var dfd = $.Deferred();
|
||||
if (model.__type__ == 'Document') {
|
||||
_.each(this._datasetAsData.data.rows, function(row, idx) {
|
||||
if(row.id === model.id) {
|
||||
self._datasetAsData.data.rows[idx] = model.toJSON();
|
||||
}
|
||||
});
|
||||
dfd.resolve(model);
|
||||
}
|
||||
return dfd.promise();
|
||||
} else if (method === 'delete') {
|
||||
var dfd = $.Deferred();
|
||||
if (model.__type__ == 'Document') {
|
||||
this._datasetAsData.data.rows = _.reject(this._datasetAsData.data.rows, function(row) {
|
||||
return (row.id === model.id);
|
||||
});
|
||||
dfd.resolve(model);
|
||||
}
|
||||
return dfd.promise();
|
||||
} else {
|
||||
alert('Not supported: sync on BackendMemory with method ' + method + ' and model ' + model);
|
||||
}
|
||||
},
|
||||
getRows: function(datasetId, numRows, start) {
|
||||
@ -73,7 +139,7 @@ recline.BackendMemory = Backbone.Model.extend({
|
||||
numRows = 10;
|
||||
}
|
||||
var dfd = $.Deferred();
|
||||
rows = this._datasetCache[datasetId].data.rows;
|
||||
rows = this._datasetAsData.data.rows;
|
||||
var results = rows.slice(start, start+numRows);
|
||||
dfd.resolve(results);
|
||||
return dfd.promise();
|
||||
@ -82,15 +148,15 @@ recline.BackendMemory = Backbone.Model.extend({
|
||||
|
||||
// Webstore Backend for connecting to the Webstore
|
||||
//
|
||||
// Designed to only attached to only dataset and one dataset only ...
|
||||
// Designed to only attach to one dataset and one dataset only ...
|
||||
// Could generalize to support attaching to different datasets
|
||||
recline.BackendWebstore = Backbone.Model.extend({
|
||||
my.BackendWebstore = Backbone.Model.extend({
|
||||
// require url attribute in initialization data
|
||||
initialize: function() {
|
||||
this.webstoreTableUrl = this.get('url');
|
||||
},
|
||||
getDataset: function(id) {
|
||||
var dataset = new recline.Dataset({
|
||||
var dataset = new my.Dataset({
|
||||
id: id
|
||||
});
|
||||
dataset.backend = this;
|
||||
@ -146,3 +212,8 @@ recline.BackendWebstore = Backbone.Model.extend({
|
||||
return dfd.promise();
|
||||
}
|
||||
});
|
||||
|
||||
return my;
|
||||
|
||||
}(jQuery);
|
||||
|
||||
|
||||
@ -1,41 +1,5 @@
|
||||
window.recline = {};
|
||||
|
||||
recline.Document = Backbone.Model.extend({});
|
||||
|
||||
recline.DocumentList = Backbone.Collection.extend({
|
||||
webStore: new WebStore(this.url),
|
||||
model: recline.Document
|
||||
});
|
||||
|
||||
recline.DataTable = Backbone.View.extend({
|
||||
|
||||
el: ".data-table-container",
|
||||
|
||||
documents: new recline.DocumentList(this.url),
|
||||
|
||||
// template: TODO ???
|
||||
|
||||
events: {
|
||||
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
var that = this;
|
||||
this.documents.fetch({
|
||||
success: function(collection, resp) {
|
||||
that.render()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var template = $( ".dataTableTemplate:first" ).html()
|
||||
, htmls = $.mustache(template, {rows: this.documents.toJSON()} )
|
||||
;
|
||||
$(this.el).html(htmls);
|
||||
return this;
|
||||
}
|
||||
});
|
||||
// Original Recline code - **Deprecated**
|
||||
// Left intact while functionality is transferred across to new Backbone setup
|
||||
|
||||
// var recline = function() {
|
||||
//
|
||||
|
||||
474
src/view.js
474
src/view.js
@ -1,6 +1,11 @@
|
||||
this.recline = this.recline || {};
|
||||
|
||||
recline.DataExplorer = Backbone.View.extend({
|
||||
// Views module following classic module pattern
|
||||
recline.View = function($) {
|
||||
|
||||
var my = {};
|
||||
|
||||
my.DataExplorer = Backbone.View.extend({
|
||||
tagName: 'div',
|
||||
className: 'data-explorer',
|
||||
template: ' \
|
||||
@ -11,37 +16,58 @@ recline.DataExplorer = Backbone.View.extend({
|
||||
<input type="radio" id="nav-graph" name="nav-toggle" value="graph" /> \
|
||||
<label for="nav-graph">Graph</label> \
|
||||
</span> \
|
||||
<ul class="nav-pagination"> \
|
||||
<li><form class="display-count"><label for="per-page">Display count</label> <input name="displayCount" type="text" value="{{displayCount}}" /></form></li> \
|
||||
</ul> \
|
||||
</div> \
|
||||
<div class="data-view-container"></div> \
|
||||
',
|
||||
|
||||
events: {
|
||||
'change input[name="nav-toggle"]': 'navChange'
|
||||
'change input[name="nav-toggle"]': 'navChange',
|
||||
'submit form.display-count': 'displayCountUpdate'
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
initialize: function(options) {
|
||||
this.el = $(this.el);
|
||||
this.config = options.config || {};
|
||||
_.extend(this.config, {
|
||||
displayCount: 10
|
||||
});
|
||||
this.draw();
|
||||
},
|
||||
|
||||
displayCountUpdate: function(e) {
|
||||
e.preventDefault();
|
||||
this.config.displayCount = parseInt(this.el.find('input[name="displayCount"]').val());
|
||||
this.draw();
|
||||
},
|
||||
|
||||
draw: function() {
|
||||
var self = this;
|
||||
this.el.empty();
|
||||
this.render();
|
||||
this.$dataViewContainer = this.el.find('.data-view-container');
|
||||
var self = this;
|
||||
// retrieve basic data like headers etc
|
||||
// note this.model and dataset returned are the same
|
||||
this.model.fetch().then(function(dataset) {
|
||||
// initialize of dataTable calls render
|
||||
self.dataTable = new recline.DataTable({
|
||||
self.dataTable = new my.DataTable({
|
||||
model: dataset
|
||||
});
|
||||
self.flotGraph = new recline.FlotGraph({
|
||||
self.flotGraph = new my.FlotGraph({
|
||||
model: dataset
|
||||
});
|
||||
self.flotGraph.el.hide();
|
||||
self.$dataViewContainer.append(self.dataTable.el)
|
||||
self.$dataViewContainer.append(self.flotGraph.el);
|
||||
self.model.getRows(self.config.displayCount);
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
$(this.el).html($(this.template));
|
||||
var template = $.mustache(this.template, this.config);
|
||||
$(this.el).html(template);
|
||||
},
|
||||
|
||||
navChange: function(e) {
|
||||
@ -62,17 +88,20 @@ recline.DataExplorer = Backbone.View.extend({
|
||||
}
|
||||
});
|
||||
|
||||
recline.DataTable = Backbone.View.extend({
|
||||
// DataTable provides a tabular view on a Dataset.
|
||||
//
|
||||
// Initialize it with a recline.Dataset object.
|
||||
my.DataTable = Backbone.View.extend({
|
||||
tagName: "div",
|
||||
className: "data-table-container",
|
||||
|
||||
initialize: function() {
|
||||
this.el = $(this.el);
|
||||
var self = this;
|
||||
this.model.getRows().then(function(rows) {
|
||||
self._currentRows = rows;
|
||||
self.render()
|
||||
});
|
||||
this.el = $(this.el);
|
||||
_.bindAll(this, 'render');
|
||||
this.model.currentDocuments.bind('add', this.render);
|
||||
this.model.currentDocuments.bind('reset', this.render);
|
||||
this.model.currentDocuments.bind('remove', this.render);
|
||||
this.state = {};
|
||||
// this is nasty. Due to fact that .menu element is not inside this view but is elsewhere in DOM
|
||||
$('.menu li a').live('click', function(e) {
|
||||
@ -85,11 +114,7 @@ recline.DataTable = Backbone.View.extend({
|
||||
// see initialize
|
||||
// 'click .menu li': 'onMenuClick',
|
||||
'click .column-header-menu': 'onColumnHeaderClick',
|
||||
'click .row-header-menu': 'onRowHeaderClick',
|
||||
'click .data-table-cell-edit': 'onEditClick',
|
||||
// cell editor
|
||||
'click .data-table-cell-editor .okButton': 'onEditorOK',
|
||||
'click .data-table-cell-editor .cancelButton': 'onEditorCancel'
|
||||
'click .row-header-menu': 'onRowHeaderClick'
|
||||
},
|
||||
|
||||
showDialog: function(template, data) {
|
||||
@ -122,8 +147,8 @@ recline.DataTable = Backbone.View.extend({
|
||||
var self = this;
|
||||
e.preventDefault();
|
||||
var actions = {
|
||||
bulkEdit: function() { self.showDialog('bulkEdit', {name: self.state.currentColumn}) },
|
||||
transform: function() { showDialog('transform') },
|
||||
bulkEdit: function() { self.showTransformColumnDialog('bulkEdit', {name: self.state.currentColumn}) },
|
||||
transform: function() { self.showTransformDialog('transform') },
|
||||
csv: function() { window.location.href = app.csvUrl },
|
||||
json: function() { window.location.href = "_rewrite/api/json" },
|
||||
urlImport: function() { showDialog('urlImport') },
|
||||
@ -137,25 +162,146 @@ recline.DataTable = Backbone.View.extend({
|
||||
if (confirm(msg)) costco.deleteColumn(self.state.currentColumn);
|
||||
},
|
||||
deleteRow: function() {
|
||||
// TODO:
|
||||
alert('This function needs to be re-implemented');
|
||||
return;
|
||||
var doc = _.find(app.cache, function(doc) { return doc._id === app.currentRow });
|
||||
doc._deleted = true;
|
||||
costco.uploadDocs([doc]).then(
|
||||
function(updatedDocs) {
|
||||
var doc = _.find(self.model.currentDocuments.models, function(doc) {
|
||||
// important this is == as the currentRow will be string (as comes
|
||||
// from DOM) while id may be int
|
||||
return doc.id == self.state.currentRow
|
||||
});
|
||||
doc.destroy().then(function() {
|
||||
self.model.currentDocuments.remove(doc);
|
||||
util.notify("Row deleted successfully");
|
||||
recline.initializeTable(app.offset);
|
||||
},
|
||||
function(err) { util.notify("Errorz! " + err) }
|
||||
)
|
||||
})
|
||||
.fail(function(err) {
|
||||
util.notify("Errorz! " + err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
util.hide('menu');
|
||||
actions[$(e.target).attr('data-action')]();
|
||||
},
|
||||
|
||||
showTransformColumnDialog: function() {
|
||||
var $el = $('.dialog-content');
|
||||
util.show('dialog');
|
||||
var view = new my.ColumnTransform({
|
||||
model: this.model
|
||||
});
|
||||
view.state = this.state;
|
||||
view.render();
|
||||
$el.empty();
|
||||
$el.append(view.el);
|
||||
util.observeExit($el, function() {
|
||||
util.hide('dialog');
|
||||
})
|
||||
$('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' });
|
||||
},
|
||||
|
||||
showTransformDialog: function() {
|
||||
var $el = $('.dialog-content');
|
||||
util.show('dialog');
|
||||
var view = new my.DataTransform({
|
||||
});
|
||||
view.render();
|
||||
$el.empty();
|
||||
$el.append(view.el);
|
||||
util.observeExit($el, function() {
|
||||
util.hide('dialog');
|
||||
})
|
||||
$('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' });
|
||||
},
|
||||
|
||||
|
||||
// ======================================================
|
||||
// Core Templating
|
||||
template: ' \
|
||||
<table class="data-table" cellspacing="0"> \
|
||||
<thead> \
|
||||
<tr> \
|
||||
{{#notEmpty}}<td class="column-header"></td>{{/notEmpty}} \
|
||||
{{#headers}} \
|
||||
<td class="column-header"> \
|
||||
<div class="column-header-title"> \
|
||||
<a class="column-header-menu"></a> \
|
||||
<span class="column-header-name">{{.}}</span> \
|
||||
</div> \
|
||||
</div> \
|
||||
</td> \
|
||||
{{/headers}} \
|
||||
</tr> \
|
||||
</thead> \
|
||||
<tbody></tbody> \
|
||||
</table> \
|
||||
',
|
||||
|
||||
toTemplateJSON: function() {
|
||||
var modelData = this.model.toJSON()
|
||||
modelData.notEmpty = ( modelData.headers.length > 0 )
|
||||
return modelData;
|
||||
},
|
||||
render: function() {
|
||||
var self = this;
|
||||
var template = $( ".dataTableTemplate:first" ).html()
|
||||
, htmls = $.mustache(template, this.toTemplateJSON())
|
||||
;
|
||||
this.el.html(htmls);
|
||||
this.model.currentDocuments.forEach(function(doc) {
|
||||
var tr = $('<tr />');
|
||||
self.el.find('tbody').append(tr);
|
||||
var newView = new my.DataTableRow({
|
||||
model: doc,
|
||||
el: tr,
|
||||
headers: self.model.get('headers')
|
||||
});
|
||||
newView.render();
|
||||
});
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
// DataTableRow View for rendering an individual document.
|
||||
//
|
||||
// Since we want this to update in place it is up to creator to provider the element to attach to.
|
||||
// In addition you must pass in a headers in the constructor options. This should be list of headers for the DataTable.
|
||||
my.DataTableRow = Backbone.View.extend({
|
||||
initialize: function(options) {
|
||||
_.bindAll(this, 'render');
|
||||
this._headers = options.headers;
|
||||
this.el = $(this.el);
|
||||
this.model.bind('change', this.render);
|
||||
},
|
||||
template: ' \
|
||||
<td><a class="row-header-menu"></a></td> \
|
||||
{{#cells}} \
|
||||
<td data-header="{{header}}"> \
|
||||
<div class="data-table-cell-content"> \
|
||||
<a href="javascript:{}" class="data-table-cell-edit" title="Edit this cell"> </a> \
|
||||
<div class="data-table-cell-value">{{value}}</div> \
|
||||
</div> \
|
||||
</td> \
|
||||
{{/cells}} \
|
||||
',
|
||||
events: {
|
||||
'click .data-table-cell-edit': 'onEditClick',
|
||||
// cell editor
|
||||
'click .data-table-cell-editor .okButton': 'onEditorOK',
|
||||
'click .data-table-cell-editor .cancelButton': 'onEditorCancel'
|
||||
},
|
||||
|
||||
toTemplateJSON: function() {
|
||||
var doc = this.model;
|
||||
var cellData = _.map(this._headers, function(header) {
|
||||
return {header: header, value: doc.get(header)}
|
||||
})
|
||||
return { id: this.id, cells: cellData }
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.el.attr('data-id', this.model.id);
|
||||
var html = $.mustache(this.template, this.toTemplateJSON());
|
||||
$(this.el).html(html);
|
||||
return this;
|
||||
},
|
||||
|
||||
// ======================================================
|
||||
// Cell Editor
|
||||
|
||||
@ -174,50 +320,225 @@ recline.DataTable = Backbone.View.extend({
|
||||
var cell = $(e.target);
|
||||
var rowId = cell.parents('tr').attr('data-id');
|
||||
var header = cell.parents('td').attr('data-header');
|
||||
var newValue = cell.parents('.data-table-cell-editor').find('.data-table-cell-editor-editor)').val();
|
||||
// TODO:
|
||||
alert('Update: ' + header + ' with value ' + newValue + '(But no save as not yet operational');
|
||||
return;
|
||||
var doc = _.find(app.cache, function(cacheDoc) {
|
||||
return cacheDoc._id === rowId;
|
||||
});
|
||||
doc[header] = newValue;
|
||||
var newValue = cell.parents('.data-table-cell-editor').find('.data-table-cell-editor-editor').val();
|
||||
var newData = {};
|
||||
newData[header] = newValue;
|
||||
this.model.set(newData);
|
||||
util.notify("Updating row...", {persist: true, loader: true});
|
||||
costco.updateDoc(doc).then(function(response) {
|
||||
util.notify("Row updated successfully");
|
||||
recline.initializeTable();
|
||||
})
|
||||
this.model.save().then(function(response) {
|
||||
util.notify("Row updated successfully");
|
||||
})
|
||||
.fail(function() {
|
||||
alert('error saving');
|
||||
});
|
||||
},
|
||||
|
||||
onEditorCancel: function(e) {
|
||||
var cell = $(e.target).parents('.data-table-cell-value');
|
||||
cell.html(cell.data('previousContents')).siblings('.data-table-cell-edit').removeClass("hidden");
|
||||
},
|
||||
|
||||
// ======================================================
|
||||
// Core Templating
|
||||
|
||||
toTemplateJSON: function() {
|
||||
var modelData = this.model.toJSON()
|
||||
modelData.rows = _.map(this._currentRows, function(row) {
|
||||
var cellData = _.map(modelData.headers, function(header) {
|
||||
return {header: header, value: row[header]}
|
||||
})
|
||||
return { id: 'xxx', cells: cellData }
|
||||
})
|
||||
modelData.notEmpty = ( modelData.headers.length > 0 )
|
||||
return modelData;
|
||||
},
|
||||
render: function() {
|
||||
var template = $( ".dataTableTemplate:first" ).html()
|
||||
, htmls = $.mustache(template, this.toTemplateJSON())
|
||||
;
|
||||
$(this.el).html(htmls);
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
recline.FlotGraph = Backbone.View.extend({
|
||||
|
||||
// View (Dialog) for doing data transformations (on columns of data).
|
||||
my.ColumnTransform = Backbone.View.extend({
|
||||
className: 'transform-column-view',
|
||||
template: ' \
|
||||
<div class="dialog-header"> \
|
||||
Functional transform on column {{name}} \
|
||||
</div> \
|
||||
<div class="dialog-body"> \
|
||||
<div class="grid-layout layout-tight layout-full"> \
|
||||
<table> \
|
||||
<tbody> \
|
||||
<tr> \
|
||||
<td colspan="4"> \
|
||||
<div class="grid-layout layout-tight layout-full"> \
|
||||
<table rows="4" cols="4"> \
|
||||
<tbody> \
|
||||
<tr style="vertical-align: bottom;"> \
|
||||
<td colspan="4"> \
|
||||
Expression \
|
||||
</td> \
|
||||
</tr> \
|
||||
<tr> \
|
||||
<td colspan="3"> \
|
||||
<div class="input-container"> \
|
||||
<textarea class="expression-preview-code"></textarea> \
|
||||
</div> \
|
||||
</td> \
|
||||
<td class="expression-preview-parsing-status" width="150" style="vertical-align: top;"> \
|
||||
No syntax error. \
|
||||
</td> \
|
||||
</tr> \
|
||||
<tr> \
|
||||
<td colspan="4"> \
|
||||
<div id="expression-preview-tabs" class="refine-tabs ui-tabs ui-widget ui-widget-content ui-corner-all"> \
|
||||
<span>Preview</span> \
|
||||
<div id="expression-preview-tabs-preview" class="ui-tabs-panel ui-widget-content ui-corner-bottom"> \
|
||||
<div class="expression-preview-container" style="width: 652px; "> \
|
||||
</div> \
|
||||
</div> \
|
||||
</div> \
|
||||
</td> \
|
||||
</tr> \
|
||||
</tbody> \
|
||||
</table> \
|
||||
</div> \
|
||||
</td> \
|
||||
</tr> \
|
||||
</tbody> \
|
||||
</table> \
|
||||
</div> \
|
||||
</div> \
|
||||
<div class="dialog-footer"> \
|
||||
<button class="okButton button"> Update All </button> \
|
||||
<button class="cancelButton button">Cancel</button> \
|
||||
</div> \
|
||||
',
|
||||
|
||||
events: {
|
||||
'click .okButton': 'onSubmit'
|
||||
, 'keydown .expression-preview-code': 'onEditorKeydown'
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
this.el = $(this.el);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var htmls = $.mustache(this.template,
|
||||
{name: this.state.currentColumn}
|
||||
)
|
||||
this.el.html(htmls);
|
||||
// Put in the basic (identity) transform script
|
||||
// TODO: put this into the template?
|
||||
var editor = this.el.find('.expression-preview-code');
|
||||
editor.val("function(doc) {\n doc['"+ this.state.currentColumn+"'] = doc['"+ this.state.currentColumn+"'];\n return doc;\n}");
|
||||
editor.focus().get(0).setSelectionRange(18, 18);
|
||||
editor.keydown();
|
||||
},
|
||||
|
||||
onSubmit: function(e) {
|
||||
var self = this;
|
||||
var funcText = this.el.find('.expression-preview-code').val();
|
||||
var editFunc = costco.evalFunction(funcText);
|
||||
if (editFunc.errorMessage) {
|
||||
util.notify("Error with function! " + editFunc.errorMessage);
|
||||
return;
|
||||
}
|
||||
util.hide('dialog');
|
||||
util.notify("Updating all visible docs. This could take a while...", {persist: true, loader: true});
|
||||
var docs = self.model.currentDocuments.map(function(doc) {
|
||||
return doc.toJSON();
|
||||
});
|
||||
// TODO: notify about failed docs?
|
||||
var toUpdate = costco.mapDocs(docs, editFunc).edited;
|
||||
var totalToUpdate = toUpdate.length;
|
||||
function onCompletedUpdate() {
|
||||
totalToUpdate += -1;
|
||||
if (totalToUpdate === 0) {
|
||||
util.notify(toUpdate.length + " documents updated successfully");
|
||||
alert('WARNING: We have only updated the docs in this view. (Updating of all docs not yet implemented!)');
|
||||
self.remove();
|
||||
}
|
||||
}
|
||||
// TODO: Very inefficient as we search through all docs every time!
|
||||
_.each(toUpdate, function(editedDoc) {
|
||||
var realDoc = self.model.currentDocuments.get(editedDoc.id);
|
||||
realDoc.set(editedDoc);
|
||||
realDoc.save().then(onCompletedUpdate).fail(onCompletedUpdate)
|
||||
});
|
||||
},
|
||||
|
||||
onEditorKeydown: function(e) {
|
||||
var self = this;
|
||||
// if you don't setTimeout it won't grab the latest character if you call e.target.value
|
||||
window.setTimeout( function() {
|
||||
var errors = self.el.find('.expression-preview-parsing-status');
|
||||
var editFunc = costco.evalFunction(e.target.value);
|
||||
if (!editFunc.errorMessage) {
|
||||
errors.text('No syntax error.');
|
||||
var docs = self.model.currentDocuments.map(function(doc) {
|
||||
return doc.toJSON();
|
||||
});
|
||||
costco.previewTransform(docs, editFunc, self.state.currentColumn);
|
||||
} else {
|
||||
errors.text(editFunc.errorMessage);
|
||||
}
|
||||
}, 1, true);
|
||||
}
|
||||
});
|
||||
|
||||
// View (Dialog) for doing data transformations on whole dataset.
|
||||
my.DataTransform = Backbone.View.extend({
|
||||
className: 'transform-view',
|
||||
template: ' \
|
||||
<div class="dialog-header"> \
|
||||
Recursive transform on all rows \
|
||||
</div> \
|
||||
<div class="dialog-body"> \
|
||||
<div class="grid-layout layout-full"> \
|
||||
<p class="info">Traverse and transform objects by visiting every node on a recursive walk using <a href="https://github.com/substack/js-traverse">js-traverse</a>.</p> \
|
||||
<table> \
|
||||
<tbody> \
|
||||
<tr> \
|
||||
<td colspan="4"> \
|
||||
<div class="grid-layout layout-tight layout-full"> \
|
||||
<table rows="4" cols="4"> \
|
||||
<tbody> \
|
||||
<tr style="vertical-align: bottom;"> \
|
||||
<td colspan="4"> \
|
||||
Expression \
|
||||
</td> \
|
||||
</tr> \
|
||||
<tr> \
|
||||
<td colspan="3"> \
|
||||
<div class="input-container"> \
|
||||
<textarea class="expression-preview-code"></textarea> \
|
||||
</div> \
|
||||
</td> \
|
||||
<td class="expression-preview-parsing-status" width="150" style="vertical-align: top;"> \
|
||||
No syntax error. \
|
||||
</td> \
|
||||
</tr> \
|
||||
<tr> \
|
||||
<td colspan="4"> \
|
||||
<div id="expression-preview-tabs" class="refine-tabs ui-tabs ui-widget ui-widget-content ui-corner-all"> \
|
||||
<span>Preview</span> \
|
||||
<div id="expression-preview-tabs-preview" class="ui-tabs-panel ui-widget-content ui-corner-bottom"> \
|
||||
<div class="expression-preview-container" style="width: 652px; "> \
|
||||
</div> \
|
||||
</div> \
|
||||
</div> \
|
||||
</td> \
|
||||
</tr> \
|
||||
</tbody> \
|
||||
</table> \
|
||||
</div> \
|
||||
</td> \
|
||||
</tr> \
|
||||
</tbody> \
|
||||
</table> \
|
||||
</div> \
|
||||
</div> \
|
||||
<div class="dialog-footer"> \
|
||||
<button class="okButton button"> Update All </button> \
|
||||
<button class="cancelButton button">Cancel</button> \
|
||||
</div> \
|
||||
',
|
||||
|
||||
initialize: function() {
|
||||
this.el = $(this.el);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.el.html(this.template);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
my.FlotGraph = Backbone.View.extend({
|
||||
|
||||
tagName: "div",
|
||||
className: "data-graph-container",
|
||||
@ -272,18 +593,17 @@ recline.FlotGraph = Backbone.View.extend({
|
||||
',
|
||||
|
||||
initialize: function(options, chart) {
|
||||
var self = this;
|
||||
this.el = $(this.el);
|
||||
_.bindAll(this, 'render');
|
||||
this.model.currentDocuments.bind('add', this.render);
|
||||
this.model.currentDocuments.bind('reset', this.render);
|
||||
this.chart = chart;
|
||||
this.chartConfig = {
|
||||
group: null,
|
||||
series: [],
|
||||
graphType: 'line'
|
||||
};
|
||||
var self = this;
|
||||
this.model.getRows().then(function(rows) {
|
||||
self._currentRows = rows;
|
||||
self.render()
|
||||
});
|
||||
},
|
||||
|
||||
events: {
|
||||
@ -342,8 +662,9 @@ recline.FlotGraph = Backbone.View.extend({
|
||||
if (this.chartConfig) {
|
||||
$.each(this.chartConfig.series, function (seriesIndex, field) {
|
||||
var points = [];
|
||||
$.each(self._currentRows, function (index) {
|
||||
var x = this[self.chartConfig.group], y = this[field];
|
||||
$.each(self.model.currentDocuments.models, function (index, doc) {
|
||||
var x = doc.get(self.chartConfig.group);
|
||||
var y = doc.get(field);
|
||||
if (typeof x === 'string') {
|
||||
x = index;
|
||||
}
|
||||
@ -370,3 +691,8 @@ recline.FlotGraph = Backbone.View.extend({
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
return my;
|
||||
|
||||
}(jQuery);
|
||||
|
||||
|
||||
@ -12,6 +12,8 @@
|
||||
|
||||
<script type="text/javascript" src="../src/model.js"></script>
|
||||
<script type="text/javascript" src="model.test.js"></script>
|
||||
<script type="text/javascript" src="../src/view.js"></script>
|
||||
<script type="text/javascript" src="view.test.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1 id="qunit-header">Qunit Tests</h1>
|
||||
@ -19,9 +21,9 @@
|
||||
<div id="qunit-testrunner-toolbar"></div>
|
||||
<h2 id="qunit-userAgent"></h2>
|
||||
<ol id="qunit-tests"></ol>
|
||||
<div id="qunit-fixture">
|
||||
<div id="our-dialog">
|
||||
</div>
|
||||
<div class="fixtures">
|
||||
<table class="test-datatable">
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -12,34 +12,48 @@ test('new Dataset', function () {
|
||||
var indata = {
|
||||
headers: ['x', 'y', 'z']
|
||||
, rows: [
|
||||
{x: 1, y: 2, z: 3}
|
||||
, {x: 2, y: 4, z: 6}
|
||||
, {x: 3, y: 6, z: 9}
|
||||
, {x: 4, y: 8, z: 12}
|
||||
, {x: 5, y: 10, z: 15}
|
||||
, {x: 6, y: 12, z: 18}
|
||||
{id: 0, x: 1, y: 2, z: 3}
|
||||
, {id: 1, x: 2, y: 4, z: 6}
|
||||
, {id: 2, x: 3, y: 6, z: 9}
|
||||
, {id: 3, x: 4, y: 8, z: 12}
|
||||
, {id: 4, x: 5, y: 10, z: 15}
|
||||
, {id: 5, x: 6, y: 12, z: 18}
|
||||
]
|
||||
};
|
||||
// this is all rather artificial here but would make more sense with more complex backend
|
||||
backend = new recline.BackendMemory();
|
||||
backend.addDataset({
|
||||
backend = new recline.Model.BackendMemory({
|
||||
metadata: metadata,
|
||||
data: indata
|
||||
});
|
||||
recline.setBackend(backend);
|
||||
recline.Model.setBackend(backend);
|
||||
var dataset = backend.getDataset(datasetId);
|
||||
expect(6);
|
||||
expect(9);
|
||||
dataset.fetch().then(function(dataset) {
|
||||
equal(dataset.get('name'), metadata.name);
|
||||
equal(dataset.get('headers'), indata.headers);
|
||||
deepEqual(dataset.get('headers'), indata.headers);
|
||||
equal(dataset.getLength(), 6);
|
||||
dataset.getRows(4, 2).then(function(rows) {
|
||||
equal(rows[0], indata.rows[2]);
|
||||
dataset.getRows(4, 2).then(function(documentList) {
|
||||
deepEqual(indata.rows[2], documentList.models[0].toJSON());
|
||||
});
|
||||
dataset.getRows().then(function(rows) {
|
||||
equal(rows.length, Math.min(10, indata.rows.length));
|
||||
equal(rows[0], indata.rows[0]);
|
||||
dataset.getRows().then(function(docList) {
|
||||
// Test getRows
|
||||
equal(docList.length, Math.min(10, indata.rows.length));
|
||||
var doc1 = docList.models[0];
|
||||
deepEqual(doc1.toJSON(), indata.rows[0]);
|
||||
|
||||
// Test UPDATA
|
||||
var newVal = 10;
|
||||
doc1.set({x: newVal});
|
||||
doc1.save().then(function() {
|
||||
equal(backend._datasetAsData.data.rows[0].x, newVal);
|
||||
})
|
||||
|
||||
// Test Delete
|
||||
doc1.destroy().then(function() {
|
||||
equal(backend._datasetAsData.data.rows.length, 5);
|
||||
equal(backend._datasetAsData.data.rows[0].x, indata.rows[1].x);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -120,30 +134,34 @@ webstoreData = {
|
||||
};
|
||||
|
||||
test('Webstore Backend', function() {
|
||||
stop();
|
||||
var backend = new recline.BackendWebstore({
|
||||
var backend = new recline.Model.BackendWebstore({
|
||||
url: 'http://webstore.test.ckan.org/rufuspollock/demo/data'
|
||||
});
|
||||
recline.setBackend(backend);
|
||||
recline.Model.setBackend(backend);
|
||||
dataset = backend.getDataset();
|
||||
|
||||
var stub = sinon.stub($, 'ajax', function(options) {
|
||||
return {
|
||||
then: function(callback) {
|
||||
callback(webstoreSchema);
|
||||
if (options.url.indexOf('schema.json') != -1) {
|
||||
return {
|
||||
then: function(callback) {
|
||||
callback(webstoreSchema);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
then: function(callback) {
|
||||
callback(webstoreData);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
dataset.fetch().then(function(dataset) {
|
||||
equal(['__id__', 'date', 'geometry', 'amount'], dataset.get('headers'));
|
||||
deepEqual(['__id__', 'date', 'geometry', 'amount'], dataset.get('headers'));
|
||||
equal(3, dataset.rowCount)
|
||||
// restore mocked method
|
||||
$.ajax.restore();
|
||||
dataset.getRows().then(function(rows) {
|
||||
start();
|
||||
equal(3,rows.length)
|
||||
equal("2009-01-01", rows[0].date);
|
||||
dataset.getRows().then(function(docList) {
|
||||
equal(3, docList.length)
|
||||
equal("2009-01-01", docList.models[0].get('date'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
25
test/view.test.js
Normal file
25
test/view.test.js
Normal file
@ -0,0 +1,25 @@
|
||||
(function ($) {
|
||||
|
||||
module("View");
|
||||
|
||||
test('new DataTableRow View', function () {
|
||||
var $el = $('<tr />');
|
||||
$('.fixtures .test-datatable').append($el);
|
||||
var doc = new recline.Model.Document({
|
||||
'id': 1,
|
||||
'b': '2',
|
||||
'a': '1'
|
||||
});
|
||||
var view = new recline.View.DataTableRow({
|
||||
model: doc
|
||||
, el: $el
|
||||
, headers: ['a', 'b']
|
||||
});
|
||||
view.render();
|
||||
ok($el.attr('data-id'), '1');
|
||||
var tds = $el.find('td');
|
||||
equal(tds.length, 3);
|
||||
equal($(tds[1]).attr('data-header'), 'a');
|
||||
});
|
||||
|
||||
})(this.jQuery);
|
||||
Loading…
x
Reference in New Issue
Block a user