this.recline = this.recline || {};
+ backend.csv.js backend.csv.js | |
| | this.recline = this.recline || {};
this.recline.Backend = this.recline.Backend || {};
this.recline.Backend.CSV = this.recline.Backend.CSV || {};
@@ -39,7 +39,7 @@
});
} else if (dataset.url) {
$.get(dataset.url).done(function(data) {
- var rows = my.parseCSV(dataset.data, dataset);
+ var rows = my.parseCSV(data, dataset);
dfd.resolve({
records: rows,
useMemoryStore: true
@@ -112,14 +112,62 @@ http://www.uselesscode.org/javascript/csv/ | row.push(field);
out.push(row);
+ return out;
+ }; |
| Converts an array of arrays into a Comma Separated Values string.
+Each array becomes a line in the CSV.
+
+Nulls are converted to empty fields and integers or floats are converted to non-quoted numbers.
+
+@return The array serialized as a CSV
+@type String
+
+@param {Array} a The array of arrays to convert
+@param {Object} options Options for loading CSV including
+@param {String} [separator=','] Separator for CSV file
+Heavily based on uselesscode's JS CSV parser (MIT Licensed):
+http://www.uselesscode.org/javascript/csv/ | my.serializeCSV= function(a, options) {
+ var options = options || {};
+ var separator = options.separator || ',';
+ var delimiter = options.delimiter || '"';
+
+ var cur = '', // The character we are currently processing.
+ field = '', // Buffer for building up the current field
+ row = '',
+ out = '',
+ i,
+ j,
+ processField;
+
+ processField = function (field) {
+ if (field === null) { |
| If field is null set to empty string | field = '';
+ } else if (typeof field === "string" && rxNeedsQuoting.test(field)) { |
| Convert string to delimited string | field = delimiter + field + delimiter;
+ } else if (typeof field === "number") { |
| Convert number to string | field = field.toString(10);
+ }
+
+ return field;
+ };
+
+ for (i = 0; i < a.length; i += 1) {
+ cur = a[i];
+
+ for (j = 0; j < cur.length; j += 1) {
+ field = processField(cur[j]); |
| If this is EOR append row to output and flush row | if (j === (cur.length - 1)) {
+ row += field;
+ out += row + "\n";
+ row = '';
+ } else { |
| Add the current field to the current row | row += field + separator;
+ } |
| Flush the field buffer | field = '';
+ }
+ }
+
return out;
};
var rxIsInt = /^\d+$/,
- rxIsFloat = /^\d*\.\d+$|^\d+\.\d*$/, |
| If a string has leading or trailing space,
+ rxIsFloat = /^\d*\.\d+$|^\d+\.\d*$/, |
| If a string has leading or trailing space,
contains a comma double quote or a newline
it needs to be quoted in CSV output | rxNeedsQuoting = /^\s|\s$|,|"|\n/,
- trim = (function () { |
| Fx 3.1 has a native trim function, it's about 10x faster, use it if it exists | if (String.prototype.trim) {
+ trim = (function () { |
| Fx 3.1 has a native trim function, it's about 10x faster, use it if it exists | if (String.prototype.trim) {
return function (s) {
return s.trim();
};
@@ -131,8 +179,8 @@ it needs to be quoted in CSV output | }());
function chomp(s) {
- if (s.charAt(s.length - 1) !== "\n") { |
| Does not end with \n, just return string | |
| Remove the \n | return s.substring(0, s.length - 1);
+ if (s.charAt(s.length - 1) !== "\n") { |
| Does not end with \n, just return string | |
| Remove the \n | return s.substring(0, s.length - 1);
}
}
diff --git a/docs/src/backend.dataproxy.html b/docs/src/backend.dataproxy.html
index 79a83906..f0232bf4 100644
--- a/docs/src/backend.dataproxy.html
+++ b/docs/src/backend.dataproxy.html
@@ -1,4 +1,4 @@
- backend.dataproxy.js backend.dataproxy.js | | | | this.recline = this.recline || {};
+ backend.dataproxy.js backend.dataproxy.js | | | | this.recline = this.recline || {};
this.recline.Backend = this.recline.Backend || {};
this.recline.Backend.DataProxy = this.recline.Backend.DataProxy || {};
diff --git a/docs/src/backend.elasticsearch.html b/docs/src/backend.elasticsearch.html
index df2073d0..ea0e1344 100644
--- a/docs/src/backend.elasticsearch.html
+++ b/docs/src/backend.elasticsearch.html
@@ -1,4 +1,4 @@
- backend.elasticsearch.js backend.elasticsearch.js | | | | this.recline = this.recline || {};
+ backend.elasticsearch.js backend.elasticsearch.js | | | | this.recline = this.recline || {};
this.recline.Backend = this.recline.Backend || {};
this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {};
@@ -140,7 +140,12 @@ on http://localhost:9200 with index twitter and type tweet it would be:
via the url attribute. | | | ES options which are passed through to options on Wrapper (see Wrapper for details) | | fetch | my.fetch = function(dataset) {
var es = new my.Wrapper(dataset.url, my.esOptions);
var dfd = $.Deferred();
- es.mapping().done(function(schema) { | | only one top level key in ES = the type so we can ignore it | var key = _.keys(schema)[0];
+ es.mapping().done(function(schema) {
+
+ if (!schema){
+ dfd.reject({'message':'Elastic Search did not return a mapping'});
+ return;
+ } | | only one top level key in ES = the type so we can ignore it | var key = _.keys(schema)[0];
var fieldData = _.map(schema[key].properties, function(dict, fieldName) {
dict.id = fieldName;
return dict;
diff --git a/docs/src/backend.gdocs.html b/docs/src/backend.gdocs.html
index c9ce9ed4..9425ca1d 100644
--- a/docs/src/backend.gdocs.html
+++ b/docs/src/backend.gdocs.html
@@ -1,4 +1,4 @@
- backend.gdocs.js backend.gdocs.js | | | | this.recline = this.recline || {};
+ backend.gdocs.js backend.gdocs.js | | | | this.recline = this.recline || {};
this.recline.Backend = this.recline.Backend || {};
this.recline.Backend.GDocs = this.recline.Backend.GDocs || {};
@@ -29,21 +29,39 @@ var dataset = new recline.Model.Dataset({
fields: array of Field objects
records: array of objects for each row
| my.fetch = function(dataset) {
- var dfd = $.Deferred();
- var url = my.getSpreadsheetAPIUrl(dataset.url);
- $.getJSON(url, function(d) {
- result = my.parseData(d);
- var fields = _.map(result.fields, function(fieldId) {
- return {id: fieldId};
+ var dfd = $.Deferred();
+ var urls = my.getGDocsAPIUrls(dataset.url); | | TODO cover it with tests
+get the spreadsheet title | (function () {
+ var titleDfd = $.Deferred();
+
+ $.getJSON(urls.spreadsheet, function (d) {
+ titleDfd.resolve({
+ spreadsheetTitle: d.feed.title.$t
+ });
});
- dfd.resolve({
- records: result.records,
- fields: fields,
- useMemoryStore: true
+
+ return titleDfd.promise();
+ }()).then(function (response) { | | get the actual worksheet data | $.getJSON(urls.worksheet, function(d) {
+ var result = my.parseData(d);
+ var fields = _.map(result.fields, function(fieldId) {
+ return {id: fieldId};
+ });
+
+ dfd.resolve({
+ metadata: {
+ title: response.spreadsheetTitle +" :: "+ result.worksheetTitle,
+ spreadsheetTitle: response.spreadsheetTitle,
+ worksheetTitle : result.worksheetTitle
+ },
+ records : result.records,
+ fields : fields,
+ useMemoryStore: true
+ });
});
});
+
return dfd.promise();
- }; | parseData
+ }; | parseData
Parse data from Google Docs API into a reasonable form
@@ -52,56 +70,64 @@ columnsToUse: list of columns to use (specified by field names)
colTypes: dictionary (with column names as keys) specifying types (e.g. range, percent for use in conversion).
:return: tabular data object (hash with keys: field and data).
-Issues: seems google docs return columns in rows in random order and not even sure whether consistent across rows. | my.parseData = function(gdocsSpreadsheet) {
- var options = {};
- if (arguments.length > 1) {
- options = arguments[1];
- }
+Issues: seems google docs return columns in rows in random order and not even sure whether consistent across rows. | my.parseData = function(gdocsSpreadsheet, options) {
+ var options = options || {};
+ var colTypes = options.colTypes || {};
var results = {
- fields: [],
+ fields : [],
records: []
- }; | | default is no special info on type of columns | var colTypes = {};
- if (options.colTypes) {
- colTypes = options.colTypes;
- }
- if (gdocsSpreadsheet.feed.entry.length > 0) {
- for (var k in gdocsSpreadsheet.feed.entry[0]) {
- if (k.substr(0, 3) == 'gsx') {
- var col = k.substr(4);
- results.fields.push(col);
- }
+ };
+ var entries = gdocsSpreadsheet.feed.entry || [];
+ var key;
+ var colName; | | percentage values (e.g. 23.3%) | var rep = /^([\d\.\-]+)\%$/;
+
+ for(key in entries[0]) { | | it's barely possible it has inherited keys starting with 'gsx$' | if(/^gsx/.test(key)) {
+ colName = key.substr(4);
+ results.fields.push(colName);
}
- } | | converts non numberical values that should be numerical (22.3%[string] -> 0.223[float]) | var rep = /^([\d\.\-]+)\%$/;
- results.records = _.map(gdocsSpreadsheet.feed.entry, function(entry) {
+ } | | converts non numberical values that should be numerical (22.3%[string] -> 0.223[float]) | results.records = _.map(entries, function(entry) {
var row = {};
+
_.each(results.fields, function(col) {
var _keyname = 'gsx$' + col;
- var value = entry[_keyname]['$t']; | | if labelled as % and value contains %, convert | if (colTypes[col] == 'percent') {
- if (rep.test(value)) {
- var value2 = rep.exec(value);
- var value3 = parseFloat(value2);
- value = value3 / 100;
- }
+ var value = entry[_keyname].$t;
+ var num;
+ | | TODO cover this part of code with test
+TODO use the regexp only once
+if labelled as % and value contains %, convert | if(colTypes[col] === 'percent' && rep.test(value)) {
+ num = rep.exec(value)[1];
+ value = parseFloat(num) / 100;
}
+
row[col] = value;
});
+
return row;
});
+
+ results.worksheetTitle = gdocsSpreadsheet.feed.title.$t;
return results;
- }; | | Convenience function to get GDocs JSON API Url from standard URL | my.getSpreadsheetAPIUrl = function(url) {
- if (url.indexOf('feeds/list') != -1) {
- return url;
- } else { | | https://docs.google.com/spreadsheet/ccc?key=XXXX#gid=0 | var regex = /.*spreadsheet\/ccc?.*key=([^#?&+]+).*/;
- var matches = url.match(regex);
- if (matches) {
- var key = matches[1];
- var worksheet = 1;
- var out = 'https://spreadsheets.google.com/feeds/list/' + key + '/' + worksheet + '/public/values?alt=json';
- return out;
- } else {
- alert('Failed to extract gdocs key from ' + url);
- }
+ }; | | Convenience function to get GDocs JSON API Url from standard URL | my.getGDocsAPIUrls = function(url) { | | https://docs.google.com/spreadsheet/ccc?key=XXXX#gid=YYY | var regex = /.*spreadsheet\/ccc?.*key=([^#?&+]+).*gid=([\d]+).*/;
+ var matches = url.match(regex);
+ var key;
+ var worksheet;
+ var urls;
+
+ if(!!matches) {
+ key = matches[1]; | | the gid in url is 0-based and feed url is 1-based | worksheet = parseInt(matches[2]) + 1;
+ urls = {
+ worksheet : 'https://spreadsheets.google.com/feeds/list/'+ key +'/'+ worksheet +'/public/values?alt=json',
+ spreadsheet: 'https://spreadsheets.google.com/feeds/worksheets/'+ key +'/public/basic?alt=json'
+ }
}
+ else { | | we assume that it's one of the feeds urls | | | by default then, take first worksheet | worksheet = 1;
+ urls = {
+ worksheet : 'https://spreadsheets.google.com/feeds/list/'+ key +'/'+ worksheet +'/public/values?alt=json',
+ spreadsheet: 'https://spreadsheets.google.com/feeds/worksheets/'+ key +'/public/basic?alt=json'
+ }
+ }
+
+ return urls;
};
}(jQuery, this.recline.Backend.GDocs));
diff --git a/docs/src/backend.memory.html b/docs/src/backend.memory.html
index 8f515f20..2ea8beb5 100644
--- a/docs/src/backend.memory.html
+++ b/docs/src/backend.memory.html
@@ -1,4 +1,4 @@
- backend.memory.js backend.memory.js | | | | this.recline = this.recline || {};
+ backend.memory.js backend.memory.js | | | | this.recline = this.recline || {};
this.recline.Backend = this.recline.Backend || {};
this.recline.Backend.Memory = this.recline.Backend.Memory || {};
@@ -58,6 +58,7 @@ from the data. |
var numRows = queryObj.size || this.data.length;
var start = queryObj.from || 0;
var results = this.data;
+
results = this._applyFilters(results, queryObj);
results = this._applyFreeTextQuery(results, queryObj); | | not complete sorting! | _.each(queryObj.sort, function(sortObj) {
var fieldName = _.keys(sortObj)[0];
@@ -78,14 +79,38 @@ from the data. |
dfd.resolve(out);
return dfd.promise();
}; | | in place filtering | this._applyFilters = function(results, queryObj) {
- _.each(queryObj.filters, function(filter) { | | if a term filter ... | if (filter.type === 'term') {
- results = _.filter(results, function(doc) {
- return (doc[filter.field] == filter.term);
- });
- }
- });
- return results;
- }; | | we OR across fields but AND across terms in query string | this._applyFreeTextQuery = function(results, queryObj) {
+ var filters = queryObj.filters; | | register filters | var filterFunctions = {
+ term : term,
+ range : range,
+ geo_distance : geo_distance
+ };
+ var dataParsers = {
+ number : function (e) { return parseFloat(e, 10); },
+ string : function (e) { return e.toString() },
+ date : function (e) { return new Date(e).valueOf() }
+ }; | | filter records | return _.filter(results, function (record) {
+ var passes = _.map(filters, function (filter) {
+ return filterFunctions[filter.type](record, filter);
+ }); | | return only these records that pass all filters | return _.all(passes, _.identity);
+ }); | | filters definitions | function term(record, filter) {
+ var parse = dataParsers[filter.fieldType];
+ var value = parse(record[filter.field]);
+ var term = parse(filter.term);
+
+ return (value === term);
+ }
+
+ function range(record, filter) {
+ var parse = dataParsers[filter.fieldType];
+ var value = parse(record[filter.field]);
+ var start = parse(filter.start);
+ var stop = parse(filter.stop);
+
+ return (value >= start && value <= stop);
+ }
+
+ function geo_distance() { | | TODO code here | | | we OR across fields but AND across terms in query string | this._applyFreeTextQuery = function(results, queryObj) {
if (queryObj.q) {
var terms = queryObj.q.split(' ');
results = _.filter(results, function(rawdoc) {
@@ -96,10 +121,10 @@ from the data. |
var value = rawdoc[field.id];
if (value !== null) {
value = value.toString();
- } else { | | value can be null (apparently in some cases) | | | TODO regexes? | foundmatch = foundmatch || (value.toLowerCase() === term.toLowerCase()); | | TODO: early out (once we are true should break to spare unnecessary testing)
+ } else { | | value can be null (apparently in some cases) | | | TODO regexes? | foundmatch = foundmatch || (value.toLowerCase() === term.toLowerCase()); | | TODO: early out (once we are true should break to spare unnecessary testing)
if (foundmatch) return true; | });
- matches = matches && foundmatch; | | TODO: early out (once false should break to spare unnecessary testing)
+ matches = matches && foundmatch; | | TODO: early out (once false should break to spare unnecessary testing)
if (!matches) return false; | });
return matches;
});
@@ -112,9 +137,9 @@ if (!matches) return false; |
if (!queryObj.facets) {
return facetResults;
}
- _.each(queryObj.facets, function(query, facetId) { | | TODO: remove dependency on recline.Model | facetResults[facetId] = new recline.Model.Facet({id: facetId}).toJSON();
+ _.each(queryObj.facets, function(query, facetId) { | | TODO: remove dependency on recline.Model | facetResults[facetId] = new recline.Model.Facet({id: facetId}).toJSON();
facetResults[facetId].termsall = {};
- }); | | faceting | _.each(records, function(doc) {
+ }); | | faceting | _.each(records, function(doc) {
_.each(queryObj.facets, function(query, facetId) {
var fieldId = query.terms.field;
var val = doc[fieldId];
@@ -131,7 +156,7 @@ if (!matches) return false; |
var terms = _.map(tmp.termsall, function(count, term) {
return { term: term, count: count };
});
- tmp.terms = _.sortBy(terms, function(item) { | | want descending order | return -item.count;
+ tmp.terms = _.sortBy(terms, function(item) { | | want descending order | return -item.count;
});
tmp.terms = tmp.terms.slice(0, 10);
});
@@ -139,7 +164,7 @@ if (!matches) return false; |
};
this.transform = function(editFunc) {
- var toUpdate = costco.mapDocs(this.data, editFunc); | | TODO: very inefficient -- could probably just walk the documents and updates in tandem and update | _.each(toUpdate.updates, function(record, idx) {
+ var toUpdate = recline.Data.Transform.mapDocs(this.data, editFunc); | | TODO: very inefficient -- could probably just walk the documents and updates in tandem and update | _.each(toUpdate.updates, function(record, idx) {
self.data[idx] = record;
});
return this.save(toUpdate);
diff --git a/docs/src/costco.html b/docs/src/costco.html
deleted file mode 100644
index 35fbc788..00000000
--- a/docs/src/costco.html
+++ /dev/null
@@ -1,68 +0,0 @@
- costco.js costco.js | | | adapted from https://github.com/harthur/costco. heather rules | var costco = function() {
-
- function evalFunction(funcString) {
- try {
- eval("var editFunc = " + funcString);
- } catch(e) {
- return {errorMessage: e+""};
- }
- return editFunc;
- }
-
- function previewTransform(docs, editFunc, currentColumn) {
- var preview = [];
- var updated = mapDocs($.extend(true, {}, docs), editFunc);
- for (var i = 0; i < updated.docs.length; i++) {
- var before = docs[i]
- , after = updated.docs[i]
- ;
- if (!after) after = {};
- if (currentColumn) {
- preview.push({before: before[currentColumn], after: after[currentColumn]});
- } else {
- preview.push({before: before, after: after});
- }
- }
- return preview;
- }
-
- function mapDocs(docs, editFunc) {
- var edited = []
- , deleted = []
- , failed = []
- ;
-
- var updatedDocs = _.map(docs, function(doc) {
- try {
- var updated = editFunc(_.clone(doc));
- } catch(e) {
- failed.push(doc);
- return;
- }
- if(updated === null) {
- updated = {_deleted: true};
- edited.push(updated);
- deleted.push(doc);
- }
- else if(updated && !_.isEqual(updated, doc)) {
- edited.push(updated);
- }
- return updated;
- });
-
- return {
- updates: edited,
- docs: updatedDocs,
- deletes: deleted,
- failed: failed
- };
- }
-
- return {
- evalFunction: evalFunction,
- previewTransform: previewTransform,
- mapDocs: mapDocs
- };
-}();
-
- |
\ No newline at end of file
diff --git a/docs/src/data.transform.html b/docs/src/data.transform.html
new file mode 100644
index 00000000..ef709101
--- /dev/null
+++ b/docs/src/data.transform.html
@@ -0,0 +1,66 @@
+ data.transform.js data.transform.js | | | | this.recline = this.recline || {};
+this.recline.Data = this.recline.Data || {};
+
+(function(my) { | | adapted from https://github.com/harthur/costco. heather rules | my.Transform = {};
+
+my.Transform.evalFunction = function(funcString) {
+ try {
+ eval("var editFunc = " + funcString);
+ } catch(e) {
+ return {errorMessage: e+""};
+ }
+ return editFunc;
+};
+
+my.Transform.previewTransform = function(docs, editFunc, currentColumn) {
+ var preview = [];
+ var updated = my.Transform.mapDocs($.extend(true, {}, docs), editFunc);
+ for (var i = 0; i < updated.docs.length; i++) {
+ var before = docs[i]
+ , after = updated.docs[i]
+ ;
+ if (!after) after = {};
+ if (currentColumn) {
+ preview.push({before: before[currentColumn], after: after[currentColumn]});
+ } else {
+ preview.push({before: before, after: after});
+ }
+ }
+ return preview;
+};
+
+my.Transform.mapDocs = function(docs, editFunc) {
+ var edited = []
+ , deleted = []
+ , failed = []
+ ;
+
+ var updatedDocs = _.map(docs, function(doc) {
+ try {
+ var updated = editFunc(_.clone(doc));
+ } catch(e) {
+ failed.push(doc);
+ return;
+ }
+ if(updated === null) {
+ updated = {_deleted: true};
+ edited.push(updated);
+ deleted.push(doc);
+ }
+ else if(updated && !_.isEqual(updated, doc)) {
+ edited.push(updated);
+ }
+ return updated;
+ });
+
+ return {
+ updates: edited,
+ docs: updatedDocs,
+ deletes: deleted,
+ failed: failed
+ };
+};
+
+}(this.recline.Data))
+
+ |
\ No newline at end of file
diff --git a/docs/src/model.html b/docs/src/model.html
index b73e36e9..26572c1a 100644
--- a/docs/src/model.html
+++ b/docs/src/model.html
@@ -1,4 +1,4 @@
- model.js model.js | | Recline Backbone Models | this.recline = this.recline || {};
+ model.js model.js | | Recline Backbone Models | this.recline = this.recline || {};
this.recline.Model = this.recline.Model || {};
(function($, my) { | | | my.Dataset = Backbone.Model.extend({
@@ -163,6 +163,7 @@ also returned. |
self.recordCount = queryResult.total;
var docs = _.map(queryResult.hits, function(hit) {
var _doc = new my.Record(hit);
+ _doc.fields = self.fields;
_doc.bind('change', function(doc) {
self._changes.updates.push(doc.toJSON());
});
@@ -208,17 +209,8 @@ also returned. |
dfd.resolve(queryResult);
});
return dfd.promise();
- }, | recordSummary
-
-Get a simple html summary of a Dataset record in form of key/value list | recordSummary: function(record) {
- var html = '<div class="recline-record-summary">';
- this.fields.each(function(field) {
- if (field.id != 'id') {
- html += '<div class="' + field.id + '"><strong>' + field.get('label') + '</strong>: ' + record.getFieldValue(field) + '</div>';
- }
- });
- html += '</div>';
- return html;
+ }, | | Deprecated (as of v0.5) - use record.summary() | recordSummary: function(record) {
+ return record.summary();
}, | _backendFromString(backendString)
See backend argument to initialize for details | _backendFromString: function(backendString) {
@@ -266,16 +258,21 @@ Dataset is an Object like:
}
dataset = new recline.Model.Dataset(datasetInfo);
return dataset;
-}; | |
+}; | |
- A single entry or row in the dataset | my.Record = Backbone.Model.extend({
+A single record (or row) in the dataset | my.Record = Backbone.Model.extend({
constructor: function Record() {
Backbone.Model.prototype.constructor.apply(this, arguments);
- },
+ }, | initialize
- initialize: function() {
+Create a Record
+
+You usually will not do this directly but will have records created by
+Dataset e.g. in query method
+
+Certain methods require presence of a fields attribute (identical to that on Dataset) | initialize: function() {
_.bindAll(this, 'getFieldValue');
- }, | getFieldValue
+ }, | getFieldValue
For the provided Field get the corresponding rendered computed data value
for this record. | getFieldValue: function(field) {
@@ -284,7 +281,7 @@ for this record. |
val = field.renderer(val, field, this.toJSON());
}
return val;
- }, | getFieldValueUnrendered
+ }, | getFieldValueUnrendered
For the provided Field get the corresponding computed data value
for this record. | getFieldValueUnrendered: function(field) {
@@ -293,30 +290,42 @@ for this record. |
val = field.deriver(val, field, this);
}
return val;
- }, | | Override Backbone save, fetch and destroy so they do nothing
+ }, | summary
+
+Get a simple html summary of this record in form of key/value list | summary: function(record) {
+ var self = this;
+ var html = '<div class="recline-record-summary">';
+ this.fields.each(function(field) {
+ if (field.id != 'id') {
+ html += '<div class="' + field.id + '"><strong>' + field.get('label') + '</strong>: ' + self.getFieldValue(field) + '</div>';
+ }
+ });
+ html += '</div>';
+ return html;
+ }, | | Override Backbone save, fetch and destroy so they do nothing
Instead, Dataset object that created this Record should take care of
handling these changes (discovery will occur via event notifications)
WARNING: these will not persist unless you call save on Dataset | fetch: function() {},
save: function() {},
destroy: function() { this.trigger('destroy', this); }
-}); | A Backbone collection of Records | my.RecordList = Backbone.Collection.extend({
+}); | A Backbone collection of Records | my.RecordList = Backbone.Collection.extend({
constructor: function RecordList() {
Backbone.Collection.prototype.constructor.apply(this, arguments);
},
model: my.Record
-}); | | | my.Field = Backbone.Model.extend({
+}); | | | my.Field = Backbone.Model.extend({
constructor: function Field() {
Backbone.Model.prototype.constructor.apply(this, arguments);
- }, | defaults - define default values | | defaults - define default values | defaults: {
label: null,
type: 'string',
format: null,
is_derived: false
- }, | initialize
+ }, | initialize
@param {Object} data: standard Backbone model attributes
-@param {Object} options: renderer and/or deriver functions. | initialize: function(data, options) { | | if a hash not passed in the first argument throw error | if ('0' in data) {
+@param {Object} options: renderer and/or deriver functions. | initialize: function(data, options) { | | if a hash not passed in the first argument throw error | if ('0' in data) {
throw new Error('Looks like you did not pass a proper hash with id to Field constructor');
}
if (this.attributes.label === null) {
@@ -357,7 +366,7 @@ WARNING: these will not persist unless you call save on Dataset
}
} else if (format == 'plain') {
return val;
- } else { | | as this is the default and default type is string may get things
+ } else { | | as this is the default and default type is string may get things
here that are not actually strings | if (val && typeof val === 'string') {
val = val.replace(/(https?:\/\/[^ ]+)/g, '<a href="$1">$1</a>');
}
@@ -372,7 +381,7 @@ here that are not actually strings | Backbone.Collection.prototype.constructor.apply(this, arguments);
},
model: my.Field
-}); | | | my.Query = Backbone.Model.extend({
+}); | | | my.Query = Backbone.Model.extend({
constructor: function Query() {
Backbone.Model.prototype.constructor.apply(this, arguments);
},
@@ -387,11 +396,16 @@ here that are not actually strings | },
_filterTemplates: {
term: {
- type: 'term',
- field: '',
+ type: 'term', | | TODO do we need this attribute here? | field: '',
term: ''
},
+ range: {
+ type: 'range',
+ start: '',
+ stop: ''
+ },
geo_distance: {
+ type: 'geo_distance',
distance: 10,
unit: 'km',
point: {
@@ -399,11 +413,12 @@ here that are not actually strings | lat: 0
}
}
- }, | addFilter
+ }, | addFilter
Add a new filter (appended to the list of filters)
-@param filter an object specifying the filter - see _filterTemplates for examples. If only type is provided will generate a filter by cloning _filterTemplates | addFilter: function(filter) { | | crude deep copy | var ourfilter = JSON.parse(JSON.stringify(filter)); | | not full specified so use template and over-write | if (_.keys(filter).length <= 2) {
+@param filter an object specifying the filter - see _filterTemplates for examples. If only type is provided will generate a filter by cloning _filterTemplates | addFilter: function(filter) { | | crude deep copy | var ourfilter = JSON.parse(JSON.stringify(filter)); | | not full specified so use template and over-write
+3 as for 'type', 'field' and 'fieldType' | if (_.keys(filter).length <= 3) {
ourfilter = _.extend(this._filterTemplates[filter.type], ourfilter);
}
var filters = this.get('filters');
@@ -411,19 +426,19 @@ here that are not actually strings | this.trigger('change:filters:new-blank');
},
updateFilter: function(index, value) {
- }, | removeFilter
+ }, | removeFilter
Remove a filter from filters at index filterIndex | removeFilter: function(filterIndex) {
var filters = this.get('filters');
filters.splice(filterIndex, 1);
this.set({filters: filters});
this.trigger('change');
- }, | addFacet
+ }, | addFacet
Add a Facet to this query
See http://www.elasticsearch.org/guide/reference/api/search/facets/ | addFacet: function(fieldId) {
- var facets = this.get('facets'); | | Assume id and fieldId should be the same (TODO: this need not be true if we want to add two different type of facets on same field) | if (_.contains(_.keys(facets), fieldId)) {
+ var facets = this.get('facets'); | | Assume id and fieldId should be the same (TODO: this need not be true if we want to add two different type of facets on same field) | if (_.contains(_.keys(facets), fieldId)) {
return;
}
facets[fieldId] = {
@@ -443,7 +458,7 @@ here that are not actually strings | this.set({facets: facets}, {silent: true});
this.trigger('facet:add', this);
}
-}); | | | my.Facet = Backbone.Model.extend({
+}); | | | my.Facet = Backbone.Model.extend({
constructor: function Facet() {
Backbone.Model.prototype.constructor.apply(this, arguments);
},
@@ -456,15 +471,15 @@ here that are not actually strings | terms: []
};
}
-}); | A Collection/List of Facets | my.FacetList = Backbone.Collection.extend({
+}); | A Collection/List of Facets | my.FacetList = Backbone.Collection.extend({
constructor: function FacetList() {
Backbone.Collection.prototype.constructor.apply(this, arguments);
},
model: my.Facet
-}); | Object State
+}); | Object State
Convenience Backbone model for storing (configuration) state of objects like Views. | my.ObjectState = Backbone.Model.extend({
-}); | Backbone.sync
+}); | Backbone.sync
Override Backbone.sync to hand off to sync function in relevant backend | Backbone.sync = function(method, model, options) {
return model.backend.sync(method, model, options);
diff --git a/docs/src/view.graph.html b/docs/src/view.graph.html
index ebbebf8e..62c65a23 100644
--- a/docs/src/view.graph.html
+++ b/docs/src/view.graph.html
@@ -1,4 +1,4 @@
- view.graph.js view.graph.js | | | | /*jshint multistr:true */
+ view.graph.js view.graph.js | | | | /*jshint multistr:true */
this.recline = this.recline || {};
this.recline.View = this.recline.View || {};
@@ -20,22 +20,22 @@
NB: should not provide an el argument to the view but must let the view
generate the element itself (you can then append view.el to the DOM. | my.Graph = Backbone.View.extend({
- tagName: "div",
- className: "recline-graph",
-
template: ' \
- <div class="panel graph"> \
- <div class="js-temp-notice alert alert-block"> \
- <h3 class="alert-heading">Hey there!</h3> \
- <p>There\'s no graph here yet because we don\'t know what fields you\'d like to see plotted.</p> \
- <p>Please tell us by <strong>using the menu on the right</strong> and a graph will automatically appear.</p> \
+ <div class="recline-graph"> \
+ <div class="panel graph" style="display: block;"> \
+ <div class="js-temp-notice alert alert-block"> \
+ <h3 class="alert-heading">Hey there!</h3> \
+ <p>There\'s no graph here yet because we don\'t know what fields you\'d like to see plotted.</p> \
+ <p>Please tell us by <strong>using the menu on the right</strong> and a graph will automatically appear.</p> \
+ </div> \
+ </div> \
</div> \
- </div> \
-</div> \
',
initialize: function(options) {
var self = this;
+ this.graphColors = ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"];
+
this.el = $(this.el);
_.bindAll(this, 'render', 'redraw');
this.needToRedraw = false;
@@ -43,13 +43,9 @@ generate the element itself (you can then append view.el to the DOM.
this.model.fields.bind('reset', this.render);
this.model.fields.bind('add', this.render);
this.model.records.bind('add', this.redraw);
- this.model.records.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();
- }
- });
+ this.model.records.bind('reset', this.redraw);
var stateData = _.extend({
- group: null, | | so that at least one series chooser box shows up | series: [],
+ group: null, | | so that at least one series chooser box shows up | series: [],
graphType: 'lines-and-points'
},
options.state
@@ -64,7 +60,6 @@ generate the element itself (you can then append view.el to the DOM.
self.redraw();
});
this.elSidebar = this.editor.el;
- this.render();
},
render: function() {
@@ -76,7 +71,7 @@ generate the element itself (you can then append view.el to the DOM.
return this;
},
- redraw: function() { | | 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
@@ -85,11 +80,15 @@ generate the element itself (you can then append view.el to the DOM.
if ((!areWeVisible || this.model.records.length === 0)) {
this.needToRedraw = true;
return;
- } | | check we have something to plot | if (this.state.get('group') && this.state.get('series')) { | | faff around with width because flot draws axes outside of the element width which means graph can get push down as it hits element next to it | this.$graph.width(this.el.width() - 20);
+ } | | check we have something to plot | if (this.state.get('group') && this.state.get('series')) { | | faff around with width because flot draws axes outside of the element width which means graph can get push down as it hits element next to it | this.$graph.width(this.el.width() - 20);
var series = this.createSeries();
var options = this.getGraphOptions(this.state.attributes.graphType);
- this.plot = $.plot(this.$graph, series, options);
- this.setupTooltips();
+ this.plot = Flotr.draw(this.$graph.get(0), series, options);
+ }
+ },
+
+ show: function() { | | because we cannot redraw when hidden we may need to when becoming visible | if (this.needToRedraw) {
+ this.redraw();
}
}, | getGraphOptions
@@ -98,150 +97,162 @@ generate the element itself (you can then append view.el to the DOM.
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.records.models[val]) {
- var out = self.model.records.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;
- };
+ var self = this;
- 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 tickFormatter = function (x) {
+ return getFormattedX(x);
+ };
+
+ var trackFormatter = function (obj) {
+ var x = obj.x;
+ var y = obj.y; | | it's horizontal so we have to flip | if (self.state.attributes.graphType === 'bars') {
+ var _tmp = x;
+ x = y;
+ y = _tmp;
+ }
+
+ x = getFormattedX(x);
+
+ var content = _.template('<%= group %> = <%= x %>, <%= series %> = <%= y %>', {
+ group: self.state.attributes.group,
+ x: x,
+ series: obj.series.label,
+ y: y
+ });
+
+ return content;
+ };
+
+ var getFormattedX = function (x) {
+ var xfield = self.model.fields.get(self.state.attributes.group); | | time series | var isDateTime = xfield.get('type') === 'date';
+
+ if (self.model.records.models[parseInt(x)]) {
+ x = self.model.records.models[parseInt(x)].get(self.state.attributes.group);
+ if (isDateTime) {
+ x = new Date(x).toLocaleDateString();
+ }
+ } else if (isDateTime) {
+ x = new Date(parseInt(x)).toLocaleDateString();
+ }
+ return x;
}
- var optionsPerGraphType = {
+
+ var xaxis = {};
+ xaxis.tickFormatter = tickFormatter;
+
+ var yaxis = {};
+ yaxis.autoscale = true;
+ yaxis.autoscaleMargin = 0.02;
+
+ var mouse = {};
+ mouse.track = true;
+ mouse.relative = true;
+ mouse.trackFormatter = trackFormatter;
+
+ var legend = {};
+ legend.position = 'ne';
+ | | mouse.lineColor is set in createSeries | var optionsPerGraphType = {
lines: {
- series: {
- lines: { show: true }
- },
- xaxis: xaxis
+ legend: legend,
+ colors: this.graphColors,
+ lines: { show: true },
+ xaxis: xaxis,
+ yaxis: yaxis,
+ mouse: mouse
},
points: {
- series: {
- points: { show: true }
- },
+ legend: legend,
+ colors: this.graphColors,
+ points: { show: true, hitRadius: 5 },
xaxis: xaxis,
+ yaxis: yaxis,
+ mouse: mouse,
grid: { hoverable: true, clickable: true }
},
'lines-and-points': {
- series: {
- points: { show: true },
- lines: { show: true }
- },
+ legend: legend,
+ colors: this.graphColors,
+ points: { show: true, hitRadius: 5 },
+ lines: { show: true },
xaxis: xaxis,
+ yaxis: yaxis,
+ mouse: mouse,
grid: { hoverable: true, clickable: true }
},
bars: {
- series: {
- lines: {show: false},
- bars: {
- show: true,
- barWidth: 1,
- align: "center",
- fill: true,
- horizontal: true
- }
+ legend: legend,
+ colors: this.graphColors,
+ lines: { show: false },
+ xaxis: yaxis,
+ yaxis: xaxis,
+ mouse: {
+ track: true,
+ relative: true,
+ trackFormatter: trackFormatter,
+ fillColor: '#FFFFFF',
+ fillOpacity: 0.3,
+ position: 'e'
},
- grid: { hoverable: true, clickable: true },
- yaxis: {
- tickSize: 1,
- tickLength: 1,
- tickFormatter: tickFormatter,
- min: -0.5,
- max: self.model.records.length - 0.5
- }
- }
+ bars: {
+ show: true,
+ horizontal: true,
+ shadowSize: 0,
+ barWidth: 0.8
+ },
+ },
+ columns: {
+ legend: legend,
+ colors: this.graphColors,
+ lines: { show: false },
+ xaxis: xaxis,
+ yaxis: yaxis,
+ mouse: {
+ track: true,
+ relative: true,
+ trackFormatter: trackFormatter,
+ fillColor: '#FFFFFF',
+ fillOpacity: 0.3,
+ position: 'n'
+ },
+ bars: {
+ show: true,
+ horizontal: false,
+ shadowSize: 0,
+ barWidth: 0.8
+ },
+ },
+ grid: { hoverable: true, clickable: true },
};
return optionsPerGraphType[typeId];
},
- setupTooltips: function() {
- var self = this;
- function showTooltip(x, y, contents) {
- $('<div id="flot-tooltip">' + contents + '</div>').css( {
- position: 'absolute',
- display: 'none',
- top: y + 5,
- left: x + 5,
- border: '1px solid #fdd',
- padding: '2px',
- 'background-color': '#fee',
- opacity: 0.80
- }).appendTo("body").fadeIn(200);
- }
-
- var previousPoint = null;
- this.$graph.bind("plothover", function (event, pos, item) {
- if (item) {
- if (previousPoint != item.datapoint) {
- previousPoint = item.datapoint;
-
- $("#flot-tooltip").remove();
- var x = item.datapoint[0];
- 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.records.models[x]) {
- x = self.model.records.models[x].get(self.state.attributes.group);
- } else {
- x = x.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,
- x: x,
- series: item.series.label,
- y: y
- });
- showTooltip(item.pageX, item.pageY, content);
- }
- }
- else {
- $("#flot-tooltip").remove();
- previousPoint = null;
- }
- });
- },
-
- createSeries: function () {
+ createSeries: function() {
var self = this;
var series = [];
_.each(this.state.attributes.series, function(field) {
var points = [];
_.each(self.model.records.models, function(doc, index) {
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 = moment(x).toDate();
- }
- var yfield = self.model.fields.get(field);
- var y = doc.getFieldValue(yfield);
- if (typeof x === 'string') {
- x = parseFloat(x);
+ var x = doc.getFieldValue(xfield); | | time series | var isDateTime = xfield.get('type') === 'date';
+
+ if (isDateTime) { | | datetime | if (self.state.attributes.graphType != 'bars' && self.state.attributes.graphType != 'columns') { | | not bar or column | x = new Date(x).getTime();
+ } else { | | bar or column | x = index;
+ }
+ } else if (typeof x === 'string') { | | string | x = parseFloat(x);
if (isNaN(x)) {
x = index;
}
- } | | horizontal bar chart | if (self.state.attributes.graphType == 'bars') {
+ }
+
+ var yfield = self.model.fields.get(field);
+ var y = doc.getFieldValue(yfield);
+ | | horizontal bar chart | if (self.state.attributes.graphType == 'bars') {
points.push([y, x]);
} else {
points.push([x, y]);
}
});
- series.push({data: points, label: field});
+ series.push({data: points, label: field, mouse:{lineColor: self.graphColors[series.length]}});
});
return series;
}
@@ -260,6 +271,7 @@ have no field type info). Thus at present we only do this for bars.
<option value="lines">Lines</option> \
<option value="points">Points</option> \
<option value="bars">Bars</option> \
+ <option value="columns">Columns</option> \
</select> \
</div> \
<label>Group Column (x-axis)</label> \
@@ -318,12 +330,12 @@ have no field type info). Thus at present we only do this for bars.
var self = this;
var tmplData = this.model.toTemplateJSON();
var htmls = Mustache.render(this.template, tmplData);
- this.el.html(htmls); | | set up editor from state | if (this.state.get('graphType')) {
+ this.el.html(htmls); | | 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 = [""];
+ } | | ensure at least one series box shows up | var tmpSeries = [""];
if (this.state.get('series').length > 0) {
tmpSeries = this.state.get('series');
}
@@ -332,7 +344,7 @@ have no field type info). Thus at present we only do this for bars.
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){
+ }, | | 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){
@@ -357,7 +369,7 @@ have no field type info). Thus at present we only do this for bars.
graphType: this.el.find('.editor-type select').val()
};
this.state.set(updatedState);
- }, | | Public: Adds a new empty series select box to the editor.
+ }, | | Public: Adds a new empty series select box to the editor.
@param [int] idx index of this series in the list of series
@@ -375,7 +387,7 @@ have no field type info). Thus at present we only do this for bars.
_onAddSeries: function(e) {
e.preventDefault();
this.addSeries(this.state.get('series').length);
- }, | | Public: Removes a series list item from the editor.
+ }, | | Public: Removes a series list item from the editor.
Also updates the labels of the remaining series elements. | removeSeries: function (e) {
e.preventDefault();
diff --git a/docs/src/view.grid.html b/docs/src/view.grid.html
index 4d2c0d23..7e613802 100644
--- a/docs/src/view.grid.html
+++ b/docs/src/view.grid.html
@@ -1,4 +1,4 @@
- view.grid.js view.grid.js | | | | /*jshint multistr:true */
+ view.grid.js view.grid.js | | | | /*jshint multistr:true */
this.recline = this.recline || {};
this.recline.View = this.recline.View || {};
@@ -26,77 +26,9 @@
this.state = new recline.Model.ObjectState(state);
},
- events: {
- 'click .column-header-menu .data-table-menu li a': 'onColumnHeaderClick',
- 'click .row-header-menu': 'onRowHeaderClick',
- 'click .root-header-menu': 'onRootHeaderClick',
- 'click .data-table-menu li a': 'onMenuClick', | | does not work here so done at end of render function
+ events: { | | does not work here so done at end of render function
'scroll .recline-grid tbody': 'onHorizontalScroll' | | | ======================================================
-Column and row menus | onColumnHeaderClick: function(e) {
- this.tempState.currentColumn = $(e.target).closest('.column-header').attr('data-field');
- },
-
- onRowHeaderClick: function(e) {
- this.tempState.currentRow = $(e.target).parents('tr:first').attr('data-id');
- },
-
- onRootHeaderClick: function(e) {
- var tmpl = ' \
- {{#columns}} \
- <li><a data-action="showColumn" data-column="{{.}}" href="JavaScript:void(0);">Show column: {{.}}</a></li> \
- {{/columns}}';
- var tmp = Mustache.render(tmpl, {'columns': this.state.get('hiddenFields')});
- this.el.find('.root-header-menu .dropdown-menu').html(tmp);
- },
-
- onMenuClick: function(e) {
- var self = this;
- e.preventDefault();
- var actions = {
- bulkEdit: function() { self.showTransformColumnDialog('bulkEdit', {name: self.tempState.currentColumn}); },
- facet: function() {
- self.model.queryState.addFacet(self.tempState.currentColumn);
- },
- facet_histogram: function() {
- self.model.queryState.addHistogramFacet(self.tempState.currentColumn);
- },
- filter: function() {
- self.model.queryState.addTermFilter(self.tempState.currentColumn, '');
- },
- sortAsc: function() { self.setColumnSort('asc'); },
- sortDesc: function() { self.setColumnSort('desc'); },
- hideColumn: function() { self.hideColumn(); },
- showColumn: function() { self.showColumn(e); },
- deleteRow: function() {
- var self = this;
- var doc = _.find(self.model.records.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.records.remove(doc);
- self.trigger('recline:flash', {message: "Row deleted successfully"});
- }).fail(function(err) {
- self.trigger('recline:flash', {message: "Errorz! " + err});
- });
- }
- };
- actions[$(e.target).attr('data-action')]();
- },
-
- showTransformColumnDialog: function() {
- var self = this;
- var view = new my.ColumnTransform({
- model: this.model
- }); | | pass the flash message up the chain | view.bind('recline:flash', function(flash) {
- self.trigger('recline:flash', flash);
- });
- view.state = this.tempState;
- view.render();
- this.el.append(view.el);
- view.el.modal();
- },
-
- setColumnSort: function(order) {
+Column and row menus | setColumnSort: function(order) {
var sort = [{}];
sort[0][this.tempState.currentColumn] = {order: order};
this.model.query({sort: sort});
@@ -105,7 +37,7 @@ from DOM) while id may be int | hideColumn: function() {
var hiddenFields = this.state.get('hiddenFields');
hiddenFields.push(this.tempState.currentColumn);
- this.state.set({hiddenFields: hiddenFields}); | | change event not being triggered (because it is an array?) so trigger manually | this.state.trigger('change');
+ this.state.set({hiddenFields: hiddenFields}); | | change event not being triggered (because it is an array?) so trigger manually | this.state.trigger('change');
this.render();
},
@@ -118,40 +50,15 @@ from DOM) while id may be int | onHorizontalScroll: function(e) {
var currentScroll = $(e.target).scrollLeft();
this.el.find('.recline-grid thead tr').scrollLeft(currentScroll);
- }, | | ======================================================
+ }, | | ======================================================
Templating | template: ' \
<div class="table-container"> \
<table class="recline-grid table-striped table-condensed" cellspacing="0"> \
<thead class="fixed-header"> \
<tr> \
- {{#notEmpty}} \
- <th class="column-header"> \
- <div class="btn-group root-header-menu"> \
- <a class="btn dropdown-toggle" data-toggle="dropdown"><span class="caret"></span></a> \
- <ul class="dropdown-menu data-table-menu"> \
- </ul> \
- </div> \
- <span class="column-header-name"></span> \
- </th> \
- {{/notEmpty}} \
{{#fields}} \
- <th class="column-header {{#hidden}}hidden{{/hidden}}" data-field="{{id}}" style="width: {{width}}px; max-width: {{width}}px; min-width: {{width}}px;"> \
- <div class="btn-group column-header-menu"> \
- <a class="btn dropdown-toggle" data-toggle="dropdown"><i class="icon-cog"></i><span class="caret"></span></a> \
- <ul class="dropdown-menu data-table-menu pull-right"> \
- <li><a data-action="facet" href="JavaScript:void(0);">Term Facet</a></li> \
- <li><a data-action="facet_histogram" href="JavaScript:void(0);">Date Histogram Facet</a></li> \
- <li><a data-action="filter" href="JavaScript:void(0);">Text Filter</a></li> \
- <li class="divider"></li> \
- <li><a data-action="sortAsc" href="JavaScript:void(0);">Sort ascending</a></li> \
- <li><a data-action="sortDesc" href="JavaScript:void(0);">Sort descending</a></li> \
- <li class="divider"></li> \
- <li><a data-action="hideColumn" href="JavaScript:void(0);">Hide this column</a></li> \
- <li class="divider"></li> \
- <li class="write-op"><a data-action="bulkEdit" href="JavaScript:void(0);">Transform...</a></li> \
- </ul> \
- </div> \
+ <th class="column-header {{#hidden}}hidden{{/hidden}}" data-field="{{id}}" style="width: {{width}}px; max-width: {{width}}px; min-width: {{width}}px;" title="{{label}}"> \
<span class="column-header-name">{{label}}</span> \
</th> \
{{/fields}} \
@@ -166,9 +73,9 @@ from DOM) while id may be int | toTemplateJSON: function() {
var self = this;
var modelData = this.model.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) {
+ 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();
- }); | | last header width = scroll bar - border (2px) */ | modelData.lastHeaderWidth = this.scrollbarDimensions.width - 2;
+ }); | | last header width = scroll bar - border (2px) */ | modelData.lastHeaderWidth = this.scrollbarDimensions.width - 2;
return modelData;
},
render: function() {
@@ -177,9 +84,9 @@ from DOM) while id may be int | return _.indexOf(self.state.get('hiddenFields'), field.id) == -1;
});
this.scrollbarDimensions = this.scrollbarDimensions || this._scrollbarSize(); // skip measurement if already have dimensions
- var numFields = this.fields.length; | | compute field widths (-20 for first menu col + 10px for padding on each col and finally 16px for the scrollbar) | var fullWidth = self.el.width() - 20 - 10 * numFields - this.scrollbarDimensions.width;
- var width = parseInt(Math.max(50, fullWidth / numFields)); | | if columns extend outside viewport then remainder is 0 | var remainder = Math.max(fullWidth - numFields * width,0);
- _.each(this.fields, function(field, idx) { | | add the remainder to the first field width so we make up full col | if (idx == 0) {
+ var numFields = this.fields.length; | | compute field widths (-20 for first menu col + 10px for padding on each col and finally 16px for the scrollbar) | var fullWidth = self.el.width() - 20 - 10 * numFields - this.scrollbarDimensions.width;
+ var width = parseInt(Math.max(50, fullWidth / numFields)); | | if columns extend outside viewport then remainder is 0 | var remainder = Math.max(fullWidth - numFields * width,0);
+ _.each(this.fields, function(field, idx) { | | add the remainder to the first field width so we make up full col | if (idx == 0) {
field.set({width: width+remainder});
} else {
field.set({width: width});
@@ -196,14 +103,14 @@ from DOM) while id may be int | fields: self.fields
});
newView.render();
- }); | | hide extra header col if no scrollbar to avoid unsightly overhang | var $tbody = this.el.find('tbody')[0];
+ }); | | hide extra header col if no scrollbar to avoid unsightly overhang | var $tbody = this.el.find('tbody')[0];
if ($tbody.scrollHeight <= $tbody.offsetHeight) {
this.el.find('th.last-header').hide();
}
this.el.find('.recline-grid').toggleClass('no-hidden', (self.state.get('hiddenFields').length === 0));
this.el.find('.recline-grid tbody').scroll(this.onHorizontalScroll);
return this;
- }, | _scrollbarSize
+ }, | _scrollbarSize
Measure width of a vertical scrollbar and height of a horizontal scrollbar.
@@ -213,7 +120,7 @@ from DOM) while id may be int | $c.remove();
return dim;
}
-}); | GridRow View for rendering an individual record.
+}); | GridRow View for rendering an individual record.
Since we want this to update in place it is up to creator to provider the element to attach to.
@@ -236,14 +143,6 @@ var row = new GridRow({
},
template: ' \
- <td> \
- <div class="btn-group row-header-menu"> \
- <a class="btn dropdown-toggle" data-toggle="dropdown"><span class="caret"></span></a> \
- <ul class="dropdown-menu data-table-menu"> \
- <li class="write-op"><a data-action="deleteRow" href="JavaScript:void(0);">Delete this row</a></li> \
- </ul> \
- </div> \
- </td> \
{{#cells}} \
<td data-field="{{field}}" style="width: {{width}}px; max-width: {{width}}px; min-width: {{width}}px;"> \
<div class="data-table-cell-content"> \
@@ -277,7 +176,7 @@ var row = new GridRow({
var html = Mustache.render(this.template, this.toTemplateJSON());
$(this.el).html(html);
return this;
- }, | | ===================
+ }, | | ===================
Cell Editor methods | cellEditorTemplate: ' \
<div class="menu-container data-table-cell-editor"> \
<textarea class="data-table-cell-editor-editor" bind="textarea">{{value}}</textarea> \
diff --git a/docs/src/view.map.html b/docs/src/view.map.html
index 817f29a9..b30e2692 100644
--- a/docs/src/view.map.html
+++ b/docs/src/view.map.html
@@ -1,4 +1,4 @@
- view.map.js view.map.js | | | | /*jshint multistr:true */
+ view.map.js view.map.js | | | | /*jshint multistr:true */
this.recline = this.recline || {};
this.recline.View = this.recline.View || {};
@@ -21,11 +21,10 @@ have the following (optional) configuration options:
latField: {id of field containing latitude in the dataset}
}
| my.Map = Backbone.View.extend({
- tagName: 'div',
- className: 'recline-map',
-
template: ' \
- <div class="panel map"></div> \
+ <div class="recline-map"> \
+ <div class="panel map"></div> \
+ </div> \
', | | These are the default (case-insensitive) names of field that are used if found.
If not found, the user will need to define the fields via the editor. | latitudeFieldNames: ['lat','latitude'],
longitudeFieldNames: ['lon','longitude'],
@@ -33,7 +32,19 @@ If not found, the user will need to define the fields via the editor.
initialize: function(options) {
var self = this;
- this.el = $(this.el); | | Listen to changes in the fields | this.model.fields.bind('change', function() {
+ this.el = $(this.el);
+ this.visible = true;
+ this.mapReady = false;
+
+ var stateData = _.extend({
+ geomField: null,
+ lonField: null,
+ latField: null,
+ autoZoom: true
+ },
+ options.state
+ );
+ this.state = new recline.Model.ObjectState(stateData); | | Listen to changes in the fields | this.model.fields.bind('change', function() {
self._setupGeometryField()
self.render()
}); | | Listen to changes in the records | this.model.records.bind('add', function(doc){self.redraw('add',doc)});
@@ -44,29 +55,6 @@ If not found, the user will need to define the fields via the editor.
this.model.records.bind('remove', function(doc){self.redraw('remove',doc)});
this.model.records.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.state.get('autoZoom')) {
- self._zoomToFeatures();
- self._zoomPending = false;
- }
- }
- self.visible = true;
- });
- this.bind('view:hide',function(){
- self.visible = false;
- });
-
- var stateData = _.extend({
- geomField: null,
- lonField: null,
- latField: null,
- autoZoom: true
- },
- options.state
- );
- this.state = new recline.Model.ObjectState(stateData);
this.menu = new my.MapMenu({
model: this.model,
state: this.state.toJSON()
@@ -76,11 +64,7 @@ to display properly |
self.redraw();
});
this.elSidebar = this.menu.el;
-
- this.mapReady = false;
- this.render();
- this.redraw();
- }, | Public: Adds the necessary elements to the page.
+ }, | Public: Adds the necessary elements to the page.
Also sets up the editor fields and the map if necessary. | render: function() {
var self = this;
@@ -88,8 +72,9 @@ to display properly |
htmls = Mustache.render(this.template, this.model.toTemplateJSON());
$(this.el).html(htmls);
this.$map = this.el.find('.panel.map');
+ this.redraw();
return this;
- }, | Public: Redraws the features on the map according to the action provided
+ }, | Public: Redraws the features on the map according to the action provided
Actions can be:
@@ -100,7 +85,7 @@ to display properly |
refresh: Clear existing features and add all current records
| redraw: function(action, doc){
var self = this;
- action = action || 'refresh'; | | try to set things up if not already | if (!self._geomReady()){
+ action = action || 'refresh'; | | try to set things up if not already | if (!self._geomReady()){
self._setupGeometryField();
}
if (!self.mapReady){
@@ -126,6 +111,21 @@ to display properly |
}
},
+ show: function() { | | If the div was hidden, Leaflet needs to recalculate some sizes
+to display properly | if (this.map){
+ this.map.invalidateSize();
+ if (this._zoomPending && this.state.get('autoZoom')) {
+ this._zoomToFeatures();
+ this._zoomPending = false;
+ }
+ }
+ this.visible = true;
+ },
+
+ hide: function() {
+ this.visible = false;
+ },
+
_geomReady: function() {
return Boolean(this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField')));
}, | | Private: Add one or n features to the map
diff --git a/docs/src/view.multiview.html b/docs/src/view.multiview.html
index 648b2baa..902aff81 100644
--- a/docs/src/view.multiview.html
+++ b/docs/src/view.multiview.html
@@ -1,4 +1,4 @@
- view.multiview.js view.multiview.js | | | | /*jshint multistr:true */ | | Standard JS module setup | this.recline = this.recline || {};
+ view.multiview.js view.multiview.js | | | | /*jshint multistr:true */ | | Standard JS module setup | this.recline = this.recline || {};
this.recline.View = this.recline.View || {};
(function($, my) { | MultiView
@@ -46,6 +46,30 @@ var views = [
];
+sidebarViews: (optional) the sidebar views (Filters, Fields) for
+MultiView to show. This is an array of view hashes. If not provided
+initialize with (recline.View.)FilterEditor and Fields views (with obvious
+id and labels!).
+
+
+var sidebarViews = [
+ {
+ id: 'filterEditor', // used for routing
+ label: 'Filters', // used for view switcher
+ view: new recline.View.FielterEditor({
+ model: dataset
+ })
+ },
+ {
+ id: 'fieldsView',
+ label: 'Fields',
+ view: new recline.View.Fields({
+ model: dataset
+ })
+ }
+];
+
+
state: standard state config for this view. This state is slightly
special as it includes config of many of the subviews.
@@ -139,9 +163,25 @@ initialized the MultiView with the relevant views themselves.
model: this.model
})
}];
- } | | these must be called after pageViews are created | | | Hashes of sidebar elements | if(options.sidebarViews) {
+ this.sidebarViews = options.sidebarViews;
+ } else {
+ this.sidebarViews = [{
+ id: 'filterEditor',
+ label: 'Filters',
+ view: new my.FilterEditor({
+ model: this.model
+ })
+ }, {
+ id: 'fieldsView',
+ label: 'Fields',
+ view: new my.Fields({
+ model: this.model
+ })
+ }];
+ } | | 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._bindFlashNotifications(); | | now do updates based on state (need to come after render) | if (this.state.get('readOnly')) {
this.setReadOnly();
}
if (this.state.get('currentView')) {
@@ -173,7 +213,7 @@ initialized the MultiView with the relevant views themselves.
msg = 'There was an error querying the backend';
}
self.notify({message: msg, category: 'error', persist: true});
- }); | | retrieve basic data like fields etc
+ }); | | retrieve basic data like fields etc
note this.model and dataset returned are the same
TODO: set query state ...? | this.model.queryState.set(self.state.get('query'), {silent: true});
this.model.fetch()
@@ -190,14 +230,20 @@ TODO: set query state ...? |
var tmplData = this.model.toTemplateJSON();
tmplData.views = this.pageViews;
var template = Mustache.render(this.template, tmplData);
- $(this.el).html(template); | | now create and append other views | var $dataViewContainer = this.el.find('.data-view-container');
- var $dataSidebar = this.el.find('.data-view-sidebar'); | | the main views | _.each(this.pageViews, function(view, pageName) {
+ $(this.el).html(template); | | now create and append other views | var $dataViewContainer = this.el.find('.data-view-container');
+ var $dataSidebar = this.el.find('.data-view-sidebar'); | | the main views | _.each(this.pageViews, function(view, pageName) {
+ view.view.render();
$dataViewContainer.append(view.view.el);
if (view.view.elSidebar) {
$dataSidebar.append(view.view.elSidebar);
}
});
+ _.each(this.sidebarViews, function(view) {
+ this['$'+view.id] = view.view.el;
+ $dataSidebar.append(view.view.el);
+ });
+
var pager = new recline.View.Pager({
model: this.model.queryState
});
@@ -208,35 +254,28 @@ TODO: set query state ...? |
});
this.el.find('.query-editor-here').append(queryEditor.el);
- var filterEditor = new recline.View.FilterEditor({
- model: this.model
- });
- this.$filterEditor = filterEditor.el;
- $dataSidebar.append(filterEditor.el);
-
- var fieldsView = new recline.View.Fields({
- model: this.model
- });
- this.$fieldsView = fieldsView.el;
- $dataSidebar.append(fieldsView.el);
},
updateNav: function(pageName) {
this.el.find('.navigation a').removeClass('active');
var $el = this.el.find('.navigation a[data-view="' + pageName + '"]');
- $el.addClass('active'); | | show the specific page | _.each(this.pageViews, function(view, idx) {
+ $el.addClass('active'); | | show the specific page | _.each(this.pageViews, function(view, idx) {
if (view.id === pageName) {
view.view.el.show();
if (view.view.elSidebar) {
view.view.elSidebar.show();
}
- view.view.trigger('view:show');
+ if (view.view.show) {
+ view.view.show();
+ }
} else {
view.view.el.hide();
if (view.view.elSidebar) {
view.view.elSidebar.hide();
}
- view.view.trigger('view:hide');
+ if (view.view.hide) {
+ view.view.hide();
+ }
}
});
},
@@ -258,15 +297,15 @@ TODO: set query state ...? |
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 = my.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__,
@@ -279,7 +318,7 @@ TODO: set query state ...? |
},
_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) {
@@ -289,7 +328,7 @@ TODO: set query state ...? |
self.state.set(update);
pageView.view.state.bind('change', function() {
var 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});
+ 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');
});
}
@@ -303,7 +342,7 @@ TODO: set query state ...? |
self.notify(flash);
});
});
- }, | notify
+ }, | notify
Create a notification (a div.alert in div.alert-messsages) using provided
flash object. Flash attributes (all are optional):
@@ -342,7 +381,7 @@ flash object. Flash attributes (all are optional):
});
}, 1000);
}
- }, | clearNotifications
+ }, | clearNotifications
Clear all existing notifications | clearNotifications: function() {
var $notifications = $('.recline-data-explorer .alert-messages .alert');
@@ -350,7 +389,7 @@ flash object. Flash attributes (all are optional):
$(this).remove();
});
}
-}); | MultiView.restore
+}); | MultiView.restore
Restore a MultiView instance from a serialized state including the associated dataset | my.MultiView.restore = function(state) {
var dataset = recline.Model.Dataset.restore(state);
@@ -359,7 +398,7 @@ flash object. Flash attributes (all are optional):
state: state
});
return explorer;
-} | Miscellaneous Utilities | var urlPathRegex = /^([^?]+)(\?.*)?/; | | Parse the Hash section of a URL into path and query string | my.parseHashUrl = function(hashUrl) {
+} | 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 {};
@@ -369,7 +408,7 @@ flash object. Flash attributes (all are optional):
query: parsed[2] || ''
};
}
-}; | | Parse a URL query string (?xyz=abc...) into a dictionary. | my.parseQueryString = function(q) {
+}; | | Parse a URL query string (?xyz=abc...) into a dictionary. | my.parseQueryString = function(q) {
if (!q) {
return {};
}
@@ -382,13 +421,13 @@ flash object. Flash attributes (all are optional):
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]);
+ 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() {
+}; | | 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) {
+}; | | Compse a Query String | my.composeQueryString = function(queryParams) {
var queryString = '?';
var items = [];
$.each(queryParams, function(key, value) {
@@ -403,7 +442,7 @@ flash object. Flash attributes (all are optional):
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;
+ if (window.location.hash) { | | slice(1) to remove # at start | return window.location.hash.split('?')[0].slice(1) + queryPart;
} else {
return queryPart;
}
diff --git a/docs/src/view.slickgrid.html b/docs/src/view.slickgrid.html
index 960f8dd7..72be29fd 100644
--- a/docs/src/view.slickgrid.html
+++ b/docs/src/view.slickgrid.html
@@ -1,4 +1,4 @@
- view.slickgrid.js view.slickgrid.js | | | | /*jshint multistr:true */
+ view.slickgrid.js view.slickgrid.js | | | | /*jshint multistr:true */
this.recline = this.recline || {};
this.recline.View = this.recline.View || {};
@@ -12,9 +12,6 @@
Initialize it with a recline.Model.Dataset.
NB: you need an explicit height on the element for slickgrid to work | my.SlickGrid = Backbone.View.extend({
- tagName: "div",
- className: "recline-slickgrid",
-
initialize: function(modelEtc) {
var self = this;
this.el = $(this.el);
@@ -33,21 +30,6 @@
}, modelEtc.state
);
this.state = new recline.Model.ObjectState(state);
-
- this.bind('view:show',function(){ | | If the div is hidden, SlickGrid will calculate wrongly some
-sizes so we must render it explicitly when the view is visible | if (!self.rendered){
- if (!self.grid){
- self.render();
- }
- self.grid.init();
- self.rendered = true;
- }
- self.visible = true;
- });
- this.bind('view:hide',function(){
- self.visible = false;
- });
-
},
events: {
@@ -62,7 +44,7 @@ sizes so we must render it explicitly when the view is visible <
explicitInitialization: true,
syncColumnCellResize: true,
forceFitColumns: this.state.get('fitColumns')
- }; | | We need all columns, even the hidden ones, to show on the column picker | | | custom formatter as default one escapes html
+ }; | | We need all columns, even the hidden ones, to show on the column picker | | | custom formatter as default one escapes html
plus this way we distinguish between rendering/formatting and computed value (so e.g. sort still works ...)
row = row index, cell = cell index, value = value, columnDef = column definition, dataContext = full row values | var formatter = function(row, cell, value, columnDef, dataContext) {
var field = self.model.fields.get(columnDef.id);
@@ -88,16 +70,16 @@ row = row index, cell = cell index, value = value, columnDef = column definition
}
columns.push(column);
- }); | | Restrict the visible columns | var visibleColumns = columns.filter(function(column) {
+ }); | | Restrict the visible columns | var visibleColumns = columns.filter(function(column) {
return _.indexOf(self.state.get('hiddenColumns'), column.id) == -1;
- }); | | Order them if there is ordering info on the state | if (this.state.get('columnsOrder')){
+ }); | | Order them if there is ordering info on the state | if (this.state.get('columnsOrder')){
visibleColumns = visibleColumns.sort(function(a,b){
- return _.indexOf(self.state.get('columnsOrder'),a.id) > _.indexOf(self.state.get('columnsOrder'),b.id);
+ return _.indexOf(self.state.get('columnsOrder'),a.id) > _.indexOf(self.state.get('columnsOrder'),b.id) ? 1 : -1;
});
columns = columns.sort(function(a,b){
- return _.indexOf(self.state.get('columnsOrder'),a.id) > _.indexOf(self.state.get('columnsOrder'),b.id);
+ return _.indexOf(self.state.get('columnsOrder'),a.id) > _.indexOf(self.state.get('columnsOrder'),b.id) ? 1 : -1;
});
- } | | Move hidden columns to the end, so they appear at the bottom of the
+ } | | Move hidden columns to the end, so they appear at the bottom of the
column picker | var tempHiddenColumns = [];
for (var i = columns.length -1; i >= 0; i--){
if (_.indexOf(_.pluck(visibleColumns,'id'),columns[i].id) == -1){
@@ -116,7 +98,7 @@ column picker |
data.push(row);
});
- this.grid = new Slick.Grid(this.el, data, visibleColumns, options); | | Column sorting | var sortInfo = this.model.queryState.get('sort');
+ this.grid = new Slick.Grid(this.el, data, visibleColumns, options); | | Column sorting | var sortInfo = this.model.queryState.get('sort');
if (sortInfo){
var column = _.keys(sortInfo[0])[0];
var sortAsc = !(sortInfo[0][column].order == 'desc');
@@ -152,11 +134,26 @@ column picker |
if (self.visible){
self.grid.init();
self.rendered = true;
- } else { | | Defer rendering until the view is visible | self.rendered = false;
+ } else { | | Defer rendering until the view is visible | self.rendered = false;
}
return this;
- }
+ },
+
+ show: function() { | | If the div is hidden, SlickGrid will calculate wrongly some
+sizes so we must render it explicitly when the view is visible | if (!this.rendered){
+ if (!this.grid){
+ this.render();
+ }
+ this.grid.init();
+ this.rendered = true;
+ }
+ this.visible = true;
+ },
+
+ hide: function() {
+ this.visible = false;
+ }
});
})(jQuery, recline.View);
diff --git a/docs/src/view.timeline.html b/docs/src/view.timeline.html
index a32cb701..295900ce 100644
--- a/docs/src/view.timeline.html
+++ b/docs/src/view.timeline.html
@@ -1,4 +1,4 @@
- view.timeline.js view.timeline.js | | | | /*jshint multistr:true */
+ view.timeline.js view.timeline.js | | | | /*jshint multistr:true */
this.recline = this.recline || {};
this.recline.View = this.recline.View || {};
@@ -8,8 +8,6 @@
} | Timeline
Timeline view using http://timeline.verite.co/ | my.Timeline = Backbone.View.extend({
- tagName: 'div',
-
template: ' \
<div class="recline-timeline"> \
<div id="vmm-timeline-id"></div> \
@@ -24,10 +22,6 @@ If not found, the user will need to define these fields on initialization
this.el = $(this.el);
this.timeline = new VMM.Timeline();
this._timelineIsInitialized = false;
- this.bind('view:show', function() { | | only call _initTimeline once view in DOM as Timeline uses $ internally to look up element | if (self._timelineIsInitialized === false) {
- self._initTimeline();
- }
- });
this.model.fields.bind('reset', function() {
self._setupTemporalField();
});
@@ -42,16 +36,20 @@ If not found, the user will need to define these fields on initialization
);
this.state = new recline.Model.ObjectState(stateData);
this._setupTemporalField();
- this.render(); | | can only call _initTimeline once view in DOM as Timeline uses $
-internally to look up element | if ($(this.elementId).length > 0) {
- this._initTimeline();
- }
},
render: function() {
var tmplData = {};
var htmls = Mustache.render(this.template, tmplData);
- this.el.html(htmls);
+ this.el.html(htmls); | | can only call _initTimeline once view in DOM as Timeline uses $
+internally to look up element | if ($(this.elementId).length > 0) {
+ this._initTimeline();
+ }
+ },
+
+ show: function() { | | only call _initTimeline once view in DOM as Timeline uses $ internally to look up element | if (this._timelineIsInitialized === false) {
+ this._initTimeline();
+ }
},
_initTimeline: function() {
@@ -82,7 +80,7 @@ internally to look up element | "startDate": start,
"endDate": end,
"headline": String(record.get('title') || ''),
- "text": record.get('description') || this.model.recordSummary(record)
+ "text": record.get('description') || record.summary()
};
return tlEntry;
} else {
diff --git a/docs/src/view.transform.html b/docs/src/view.transform.html
index fbe2dcc2..fdca17c9 100644
--- a/docs/src/view.transform.html
+++ b/docs/src/view.transform.html
@@ -1,24 +1,25 @@
- view.transform.js view.transform.js | | | | /*jshint multistr:true */
+ view.transform.js view.transform.js | | | | /*jshint multistr:true */
this.recline = this.recline || {};
this.recline.View = this.recline.View || {}; | | Views module following classic module pattern | | ColumnTransform
View (Dialog) for doing data transformations | my.Transform = Backbone.View.extend({
- className: 'recline-transform',
template: ' \
- <div class="script"> \
- <h2> \
- Transform Script \
- <button class="okButton btn btn-primary">Run on all records</button> \
- </h2> \
- <textarea class="expression-preview-code"></textarea> \
- </div> \
- <div class="expression-preview-parsing-status"> \
- No syntax error. \
- </div> \
- <div class="preview"> \
- <h3>Preview</h3> \
- <div class="expression-preview-container"></div> \
+ <div class="recline-transform"> \
+ <div class="script"> \
+ <h2> \
+ Transform Script \
+ <button class="okButton btn btn-primary">Run on all records</button> \
+ </h2> \
+ <textarea class="expression-preview-code"></textarea> \
+ </div> \
+ <div class="expression-preview-parsing-status"> \
+ No syntax error. \
+ </div> \
+ <div class="preview"> \
+ <h3>Preview</h3> \
+ <div class="expression-preview-container"></div> \
+ </div> \
</div> \
',
@@ -29,7 +30,6 @@
initialize: function(options) {
this.el = $(this.el);
- this.render();
},
render: function() {
@@ -42,14 +42,13 @@ TODO: put this into the template? | var col = 'unknown';
}
editor.val("function(doc) {\n doc['"+ col +"'] = doc['"+ col +"'];\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);
+ var editFunc = recline.Data.Transform.evalFunction(funcText);
if (editFunc.errorMessage) {
this.trigger('recline:flash', {message: "Error with function! " + editFunc.errorMessage});
return;
@@ -87,13 +86,13 @@ TODO: put this into the template? | 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);
+ var editFunc = recline.Data.Transform.evalFunction(e.target.value);
if (!editFunc.errorMessage) {
errors.text('No syntax error.');
var docs = self.model.records.map(function(doc) {
return doc.toJSON();
});
- var previewData = costco.previewTransform(docs, editFunc);
+ var previewData = recline.Data.Transform.previewTransform(docs, editFunc);
var $el = self.el.find('.expression-preview-container');
var fields = self.model.fields.toJSON();
var rows = _.map(previewData.slice(0,4), function(row) {
diff --git a/docs/src/widget.facetviewer.html b/docs/src/widget.facetviewer.html
index fb407748..62af60b6 100644
--- a/docs/src/widget.facetviewer.html
+++ b/docs/src/widget.facetviewer.html
@@ -1,4 +1,4 @@
- widget.facetviewer.js widget.facetviewer.js | | | | /*jshint multistr:true */
+ widget.facetviewer.js widget.facetviewer.js | | | | /*jshint multistr:true */
this.recline = this.recline || {};
this.recline.View = this.recline.View || {};
diff --git a/docs/src/widget.fields.html b/docs/src/widget.fields.html
index 22fa2b0d..2821e218 100644
--- a/docs/src/widget.fields.html
+++ b/docs/src/widget.fields.html
@@ -1,4 +1,4 @@
- widget.fields.js widget.fields.js | | | | /*jshint multistr:true */ | | Field Info
+ widget.fields.js widget.fields.js | | | | /*jshint multistr:true */ | | Field Info
For each field
diff --git a/docs/src/widget.filtereditor.html b/docs/src/widget.filtereditor.html
index 9c063b9a..30085297 100644
--- a/docs/src/widget.filtereditor.html
+++ b/docs/src/widget.filtereditor.html
@@ -1,4 +1,4 @@
- widget.filtereditor.js widget.filtereditor.js | | | | /*jshint multistr:true */
+ widget.filtereditor.js widget.filtereditor.js | | | | /*jshint multistr:true */
this.recline = this.recline || {};
this.recline.View = this.recline.View || {};
@@ -16,6 +16,7 @@
<label>Filter type</label> \
<select class="filterType"> \
<option value="term">Term (text)</option> \
+ <option value="range">Range</option> \
<option value="geo_distance">Geo distance</option> \
</select> \
<label>Field</label> \
@@ -48,6 +49,20 @@
<input type="text" value="{{term}}" name="term" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
</fieldset> \
</div> \
+ ',
+ range: ' \
+ <div class="filter-{{type}} filter"> \
+ <fieldset> \
+ <legend> \
+ {{field}} <small>{{type}}</small> \
+ <a class="js-remove-filter" href="#" title="Remove this filter">×</a> \
+ </legend> \
+ <label class="control-label" for="">From</label> \
+ <input type="text" value="{{start}}" name="start" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
+ <label class="control-label" for="">To</label> \
+ <input type="text" value="{{stop}}" name="stop" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
+ </fieldset> \
+ </div> \
',
geo_distance: ' \
<div class="filter-{{type}} filter"> \
@@ -104,8 +119,9 @@
var $target = $(e.target);
$target.hide();
var filterType = $target.find('select.filterType').val();
- var field = $target.find('select.fields').val();
- this.model.queryState.addFilter({type: filterType, field: field}); | | trigger render explicitly as queryState change will not be triggered (as blank value for filter) | this.render();
+ var field = $target.find('select.fields').val();
+ var fieldType = this.model.fields.find(function (e) { return e.get('id') === field }).get('type');
+ this.model.queryState.addFilter({type: filterType, field: field, fieldType: fieldType}); | | trigger render explicitly as queryState change will not be triggered (as blank value for filter) | this.render();
},
onRemoveFilter: function(e) {
e.preventDefault();
@@ -120,19 +136,27 @@
var $form = $(e.target);
_.each($form.find('input'), function(input) {
var $input = $(input);
- var filterType = $input.attr('data-filter-type');
- var fieldId = $input.attr('data-filter-field');
+ var filterType = $input.attr('data-filter-type');
+ var fieldId = $input.attr('data-filter-field');
var filterIndex = parseInt($input.attr('data-filter-id'));
- var name = $input.attr('name');
- var value = $input.val();
- if (filterType === 'term') {
- filters[filterIndex].term = value;
- } else if (filterType === 'geo_distance') {
- if (name === 'distance') {
- filters[filterIndex].distance = parseFloat(value);
- } else {
- filters[filterIndex].point[name] = parseFloat(value);
- }
+ var name = $input.attr('name');
+ var value = $input.val();
+
+ switch (filterType) {
+ case 'term':
+ filters[filterIndex].term = value;
+ break;
+ case 'range':
+ filters[filterIndex][name] = value;
+ break;
+ case 'geo_distance':
+ if(name === 'distance') {
+ filters[filterIndex].distance = parseFloat(value);
+ }
+ else {
+ filters[filterIndex].point[name] = parseFloat(value);
+ }
+ break;
}
});
self.model.queryState.set({filters: filters});
diff --git a/docs/src/widget.pager.html b/docs/src/widget.pager.html
index a1877309..9ecfadc9 100644
--- a/docs/src/widget.pager.html
+++ b/docs/src/widget.pager.html
@@ -1,4 +1,4 @@
- widget.pager.js widget.pager.js | | | | /*jshint multistr:true */
+ widget.pager.js widget.pager.js | | | | /*jshint multistr:true */
this.recline = this.recline || {};
this.recline.View = this.recline.View || {};
diff --git a/docs/src/widget.queryeditor.html b/docs/src/widget.queryeditor.html
index 1d9adf56..846d8ee5 100644
--- a/docs/src/widget.queryeditor.html
+++ b/docs/src/widget.queryeditor.html
@@ -1,4 +1,4 @@
- widget.queryeditor.js widget.queryeditor.js | | | | /*jshint multistr:true */
+ widget.queryeditor.js | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | |