[recline]: Delete old files and code
This commit is contained in:
@@ -1,76 +0,0 @@
|
||||
this.recline = this.recline || {};
|
||||
this.recline.Backend = this.recline.Backend || {};
|
||||
this.recline.Backend.DataProxy = this.recline.Backend.DataProxy || {};
|
||||
|
||||
(function(my) {
|
||||
"use strict";
|
||||
my.__type__ = 'dataproxy';
|
||||
// URL for the dataproxy
|
||||
my.dataproxy_url = '//jsonpdataproxy.appspot.com';
|
||||
// Timeout for dataproxy (after this time if no response we error)
|
||||
// Needed because use JSONP so do not receive e.g. 500 errors
|
||||
my.timeout = 5000;
|
||||
|
||||
|
||||
// use either jQuery or Underscore Deferred depending on what is available
|
||||
var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred;
|
||||
|
||||
// ## load
|
||||
//
|
||||
// Load data from a URL via the [DataProxy](http://github.com/okfn/dataproxy).
|
||||
//
|
||||
// Returns array of field names and array of arrays for records
|
||||
my.fetch = function(dataset) {
|
||||
var data = {
|
||||
url: dataset.url,
|
||||
'max-results': dataset.size || dataset.rows || 1000,
|
||||
type: dataset.format || ''
|
||||
};
|
||||
var jqxhr = jQuery.ajax({
|
||||
url: my.dataproxy_url,
|
||||
data: data,
|
||||
dataType: 'jsonp'
|
||||
});
|
||||
var dfd = new Deferred();
|
||||
_wrapInTimeout(jqxhr).done(function(results) {
|
||||
if (results.error) {
|
||||
dfd.reject(results.error);
|
||||
}
|
||||
|
||||
dfd.resolve({
|
||||
records: results.data,
|
||||
fields: results.fields,
|
||||
useMemoryStore: true
|
||||
});
|
||||
})
|
||||
.fail(function(args) {
|
||||
dfd.reject(args);
|
||||
});
|
||||
return dfd.promise();
|
||||
};
|
||||
|
||||
// ## _wrapInTimeout
|
||||
//
|
||||
// Convenience method providing a crude way to catch backend errors on JSONP calls.
|
||||
// Many of backends use JSONP and so will not get error messages and this is
|
||||
// a crude way to catch those errors.
|
||||
var _wrapInTimeout = function(ourFunction) {
|
||||
var dfd = new Deferred();
|
||||
var timer = setTimeout(function() {
|
||||
dfd.reject({
|
||||
message: 'Request Error: Backend did not respond after ' + (my.timeout / 1000) + ' seconds'
|
||||
});
|
||||
}, my.timeout);
|
||||
ourFunction.done(function(args) {
|
||||
clearTimeout(timer);
|
||||
dfd.resolve(args);
|
||||
})
|
||||
.fail(function(args) {
|
||||
clearTimeout(timer);
|
||||
dfd.reject(args);
|
||||
})
|
||||
;
|
||||
return dfd.promise();
|
||||
};
|
||||
|
||||
}(this.recline.Backend.DataProxy));
|
||||
@@ -1,245 +0,0 @@
|
||||
this.recline = this.recline || {};
|
||||
this.recline.Backend = this.recline.Backend || {};
|
||||
this.recline.Backend.Memory = this.recline.Backend.Memory || {};
|
||||
|
||||
(function(my) {
|
||||
"use strict";
|
||||
my.__type__ = 'memory';
|
||||
|
||||
// private data - use either jQuery or Underscore Deferred depending on what is available
|
||||
var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred;
|
||||
|
||||
// ## Data Wrapper
|
||||
//
|
||||
// Turn a simple array of JS objects into a mini data-store with
|
||||
// functionality like querying, faceting, updating (by ID) and deleting (by
|
||||
// ID).
|
||||
//
|
||||
// @param records list of hashes for each record/row in the data ({key:
|
||||
// value, key: value})
|
||||
// @param fields (optional) list of field hashes (each hash defining a field
|
||||
// as per recline.Model.Field). If fields not specified they will be taken
|
||||
// from the data.
|
||||
my.Store = function(records, fields) {
|
||||
var self = this;
|
||||
this.records = records;
|
||||
// backwards compatability (in v0.5 records was named data)
|
||||
this.data = this.records;
|
||||
if (fields) {
|
||||
this.fields = fields;
|
||||
} else {
|
||||
if (records) {
|
||||
this.fields = _.map(records[0], function(value, key) {
|
||||
return {id: key, type: 'string'};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.update = function(doc) {
|
||||
_.each(self.records, function(internalDoc, idx) {
|
||||
if(doc.id === internalDoc.id) {
|
||||
self.records[idx] = doc;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.remove = function(doc) {
|
||||
var newdocs = _.reject(self.records, function(internalDoc) {
|
||||
return (doc.id === internalDoc.id);
|
||||
});
|
||||
this.records = newdocs;
|
||||
};
|
||||
|
||||
this.save = function(changes, dataset) {
|
||||
var self = this;
|
||||
var dfd = new Deferred();
|
||||
// TODO _.each(changes.creates) { ... }
|
||||
_.each(changes.updates, function(record) {
|
||||
self.update(record);
|
||||
});
|
||||
_.each(changes.deletes, function(record) {
|
||||
self.remove(record);
|
||||
});
|
||||
dfd.resolve();
|
||||
return dfd.promise();
|
||||
},
|
||||
|
||||
this.query = function(queryObj) {
|
||||
var dfd = new Deferred();
|
||||
var numRows = queryObj.size || this.records.length;
|
||||
var start = queryObj.from || 0;
|
||||
var results = this.records;
|
||||
|
||||
results = this._applyFilters(results, queryObj);
|
||||
results = this._applyFreeTextQuery(results, queryObj);
|
||||
|
||||
// TODO: this is not complete sorting!
|
||||
// What's wrong is we sort on the *last* entry in the sort list if there are multiple sort criteria
|
||||
_.each(queryObj.sort, function(sortObj) {
|
||||
var fieldName = sortObj.field;
|
||||
results = _.sortBy(results, function(doc) {
|
||||
var _out = doc[fieldName];
|
||||
return _out;
|
||||
});
|
||||
if (sortObj.order == 'desc') {
|
||||
results.reverse();
|
||||
}
|
||||
});
|
||||
var facets = this.computeFacets(results, queryObj);
|
||||
var out = {
|
||||
total: results.length,
|
||||
hits: results.slice(start, start+numRows),
|
||||
facets: facets
|
||||
};
|
||||
dfd.resolve(out);
|
||||
return dfd.promise();
|
||||
};
|
||||
|
||||
// in place filtering
|
||||
this._applyFilters = function(results, queryObj) {
|
||||
var filters = queryObj.filters;
|
||||
// register filters
|
||||
var filterFunctions = {
|
||||
term : term,
|
||||
terms : terms,
|
||||
range : range,
|
||||
geo_distance : geo_distance
|
||||
};
|
||||
var dataParsers = {
|
||||
integer: function (e) { return parseFloat(e, 10); },
|
||||
'float': function (e) { return parseFloat(e, 10); },
|
||||
number: function (e) { return parseFloat(e, 10); },
|
||||
string : function (e) { return e.toString(); },
|
||||
date : function (e) { return moment(e).valueOf(); },
|
||||
datetime : function (e) { return new Date(e).valueOf(); }
|
||||
};
|
||||
var keyedFields = {};
|
||||
_.each(self.fields, function(field) {
|
||||
keyedFields[field.id] = field;
|
||||
});
|
||||
function getDataParser(filter) {
|
||||
var fieldType = keyedFields[filter.field].type || 'string';
|
||||
return dataParsers[fieldType];
|
||||
}
|
||||
|
||||
// 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 = getDataParser(filter);
|
||||
var value = parse(record[filter.field]);
|
||||
var term = parse(filter.term);
|
||||
|
||||
return (value === term);
|
||||
}
|
||||
|
||||
function terms(record, filter) {
|
||||
var parse = getDataParser(filter);
|
||||
var value = parse(record[filter.field]);
|
||||
var terms = parse(filter.terms).split(",");
|
||||
|
||||
return (_.indexOf(terms, value) >= 0);
|
||||
}
|
||||
|
||||
function range(record, filter) {
|
||||
var fromnull = (_.isUndefined(filter.from) || filter.from === null || filter.from === '');
|
||||
var tonull = (_.isUndefined(filter.to) || filter.to === null || filter.to === '');
|
||||
var parse = getDataParser(filter);
|
||||
var value = parse(record[filter.field]);
|
||||
var from = parse(fromnull ? '' : filter.from);
|
||||
var to = parse(tonull ? '' : filter.to);
|
||||
|
||||
// if at least one end of range is set do not allow '' to get through
|
||||
// note that for strings '' <= {any-character} e.g. '' <= 'a'
|
||||
if ((!fromnull || !tonull) && value === '') {
|
||||
return false;
|
||||
}
|
||||
return ((fromnull || value >= from) && (tonull || value <= to));
|
||||
}
|
||||
|
||||
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(' ');
|
||||
var patterns=_.map(terms, function(term) {
|
||||
return new RegExp(term.toLowerCase());
|
||||
});
|
||||
results = _.filter(results, function(rawdoc) {
|
||||
var matches = true;
|
||||
_.each(patterns, function(pattern) {
|
||||
var foundmatch = false;
|
||||
_.each(self.fields, function(field) {
|
||||
var value = rawdoc[field.id];
|
||||
if ((value !== null) && (value !== undefined)) {
|
||||
value = value.toString();
|
||||
} else {
|
||||
// value can be null (apparently in some cases)
|
||||
value = '';
|
||||
}
|
||||
// TODO regexes?
|
||||
foundmatch = foundmatch || (pattern.test(value.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)
|
||||
// if (!matches) return false;
|
||||
});
|
||||
return matches;
|
||||
});
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
this.computeFacets = function(records, queryObj) {
|
||||
var facetResults = {};
|
||||
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();
|
||||
facetResults[facetId].termsall = {};
|
||||
});
|
||||
// faceting
|
||||
_.each(records, function(doc) {
|
||||
_.each(queryObj.facets, function(query, facetId) {
|
||||
var fieldId = query.terms.field;
|
||||
var val = doc[fieldId];
|
||||
var tmp = facetResults[facetId];
|
||||
if (val) {
|
||||
tmp.termsall[val] = tmp.termsall[val] ? tmp.termsall[val] + 1 : 1;
|
||||
} else {
|
||||
tmp.missing = tmp.missing + 1;
|
||||
}
|
||||
});
|
||||
});
|
||||
_.each(queryObj.facets, function(query, facetId) {
|
||||
var tmp = facetResults[facetId];
|
||||
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 = tmp.terms.slice(0, 10);
|
||||
});
|
||||
return facetResults;
|
||||
};
|
||||
};
|
||||
|
||||
}(this.recline.Backend.Memory));
|
||||
@@ -1,67 +0,0 @@
|
||||
// This file adds in full array method support in browsers that don't support it
|
||||
// see: http://stackoverflow.com/questions/2790001/fixing-javascript-array-functions-in-internet-explorer-indexof-foreach-etc
|
||||
|
||||
// Add ECMA262-5 Array methods if not supported natively
|
||||
if (!('indexOf' in Array.prototype)) {
|
||||
Array.prototype.indexOf= function(find, i /*opt*/) {
|
||||
if (i===undefined) i= 0;
|
||||
if (i<0) i+= this.length;
|
||||
if (i<0) i= 0;
|
||||
for (var n= this.length; i<n; i++)
|
||||
if (i in this && this[i]===find)
|
||||
return i;
|
||||
return -1;
|
||||
};
|
||||
}
|
||||
if (!('lastIndexOf' in Array.prototype)) {
|
||||
Array.prototype.lastIndexOf= function(find, i /*opt*/) {
|
||||
if (i===undefined) i= this.length-1;
|
||||
if (i<0) i+= this.length;
|
||||
if (i>this.length-1) i= this.length-1;
|
||||
for (i++; i-->0;) /* i++ because from-argument is sadly inclusive */
|
||||
if (i in this && this[i]===find)
|
||||
return i;
|
||||
return -1;
|
||||
};
|
||||
}
|
||||
if (!('forEach' in Array.prototype)) {
|
||||
Array.prototype.forEach= function(action, that /*opt*/) {
|
||||
for (var i= 0, n= this.length; i<n; i++)
|
||||
if (i in this)
|
||||
action.call(that, this[i], i, this);
|
||||
};
|
||||
}
|
||||
if (!('map' in Array.prototype)) {
|
||||
Array.prototype.map= function(mapper, that /*opt*/) {
|
||||
var other= new Array(this.length);
|
||||
for (var i= 0, n= this.length; i<n; i++)
|
||||
if (i in this)
|
||||
other[i]= mapper.call(that, this[i], i, this);
|
||||
return other;
|
||||
};
|
||||
}
|
||||
if (!('filter' in Array.prototype)) {
|
||||
Array.prototype.filter= function(filter, that /*opt*/) {
|
||||
var other= [], v;
|
||||
for (var i=0, n= this.length; i<n; i++)
|
||||
if (i in this && filter.call(that, v= this[i], i, this))
|
||||
other.push(v);
|
||||
return other;
|
||||
};
|
||||
}
|
||||
if (!('every' in Array.prototype)) {
|
||||
Array.prototype.every= function(tester, that /*opt*/) {
|
||||
for (var i= 0, n= this.length; i<n; i++)
|
||||
if (i in this && !tester.call(that, this[i], i, this))
|
||||
return false;
|
||||
return true;
|
||||
};
|
||||
}
|
||||
if (!('some' in Array.prototype)) {
|
||||
Array.prototype.some= function(tester, that /*opt*/) {
|
||||
for (var i= 0, n= this.length; i<n; i++)
|
||||
if (i in this && tester.call(that, this[i], i, this))
|
||||
return true;
|
||||
return false;
|
||||
};
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
this.recline = this.recline || {};
|
||||
this.recline.View = this.recline.View || {};
|
||||
this.recline.View.translations = this.recline.View.translations || {};
|
||||
|
||||
this.recline.View.translations['en'] = {
|
||||
'date_required': "A date is required, check field field-date-format",
|
||||
'backend_error': 'There was an error querying the backend',
|
||||
'Distance_km': 'Distance (km)',
|
||||
flot_Group_Column: 'Group Column (Axis 1)',
|
||||
|
||||
map_mapping: 'Coordinates source',
|
||||
map_mapping_lat_lon: 'Latitude / Longitude fields',
|
||||
map_mapping_geojson: 'GeoJSON field'
|
||||
};
|
||||
@@ -1,69 +0,0 @@
|
||||
this.recline = this.recline || {};
|
||||
this.recline.View = this.recline.View || {};
|
||||
this.recline.View.translations = this.recline.View.translations || {};
|
||||
|
||||
this.recline.View.translations['pl'] = {
|
||||
Grid: 'Tabela',
|
||||
Graph: 'Wykres',
|
||||
Map: 'Mapa',
|
||||
Timeline: 'Oś czasu',
|
||||
Search_data: 'Wyszukaj w danych',
|
||||
Search: 'Szukaj',
|
||||
Add: 'Dodaj',
|
||||
Add_row: 'Dodaj wiersz',
|
||||
Delete_row: 'Usuń wiersz',
|
||||
Reorder_row: 'Przesuń wiersz',
|
||||
Update: 'Zaktualizuj',
|
||||
Cancel: 'Anuluj',
|
||||
Updating_row: 'Aktualizuję wiersz',
|
||||
Row_updated_successfully: 'Wiersz został zaktualizowany',
|
||||
Error_saving_row: 'Wystąpił błąd aktualizacji wiersza',
|
||||
Filters: 'Filtry',
|
||||
Add_filter: 'Dodaj filtr',
|
||||
Remove_this_filter: 'Usuń filtr',
|
||||
Fields: 'Kolumny',
|
||||
Field: 'Kolumna',
|
||||
Filter_type: 'Typ filtra',
|
||||
Value: 'Wartość',
|
||||
Range: 'Zakres',
|
||||
Geo_distance: 'Odległość',
|
||||
From: 'Od',
|
||||
To: 'Do',
|
||||
Longitude: 'Długość geograficzna',
|
||||
Latitude: 'Szerokość geograficzna',
|
||||
Distance_km: 'Odległość (km)',
|
||||
backend_error: 'Wystąpił błąd połączenia z serwerem',
|
||||
Unknown: '???',
|
||||
Edit_this_cell: 'Edytuj komórkę',
|
||||
date_required: "Data jest wymagana: sprawdź kolumnę field-date-format",
|
||||
Show_field: 'Pokaż kolumnę',
|
||||
Force_fit_columns: 'Dopasuj kolumny do zawartości',
|
||||
Expand_and_collapse: 'Rozwiń i zwiń',
|
||||
|
||||
flot_info: '<h3 class="alert-heading">Witamy!</h3> \
|
||||
<p>Jakie kolumny powinny zostać narysowane na wykresie?</p> \
|
||||
<p>Wybierz je <strong>używając menu po prawej</strong>, a wykres pojawi się automatycznie.</p>',
|
||||
Graph_Type: 'Typ wykresu',
|
||||
Lines_and_Points: 'Linie z punktami',
|
||||
Lines: 'Linie',
|
||||
Points: 'Punkty',
|
||||
Bars: 'Słupki poziome',
|
||||
Columns: 'Słupki',
|
||||
flot_Group_Column: 'Kolumna (Oś X)',
|
||||
Please_choose: 'Proszę wybrać',
|
||||
Remove: 'Usuń',
|
||||
Series: 'Seria',
|
||||
Axis_2: 'Oś Y',
|
||||
Add_Series: 'Dodaj serię danych',
|
||||
Save: 'Zapisz',
|
||||
|
||||
map_mapping: 'Źródło koordynatów',
|
||||
map_mapping_lat_lon: 'Szerokość i długość geo.',
|
||||
map_mapping_geojson: 'Jedna kolumna typu GeoJSON',
|
||||
Latitude_field: 'Kolumna szerokości geo. (WGS84)',
|
||||
Longitude_field: 'Kolumna długości geo. (WGS84)',
|
||||
Auto_zoom_to_features: 'Kadruj, aby pokazać wszytkie punkty',
|
||||
Cluster_markers: 'Łącz pobliskie punkty w grupy',
|
||||
|
||||
num_records: '<span class="doc-count">{recordCount}</span> rekordów'
|
||||
};
|
||||
651
src/model.js
651
src/model.js
@@ -1,651 +0,0 @@
|
||||
// # Recline Backbone Models
|
||||
this.recline = this.recline || {};
|
||||
this.recline.Model = this.recline.Model || {};
|
||||
|
||||
(function(my) {
|
||||
"use strict";
|
||||
|
||||
// use either jQuery or Underscore Deferred depending on what is available
|
||||
var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred;
|
||||
|
||||
// ## <a id="dataset">Dataset</a>
|
||||
my.Dataset = Backbone.Model.extend({
|
||||
constructor: function Dataset() {
|
||||
Backbone.Model.prototype.constructor.apply(this, arguments);
|
||||
},
|
||||
|
||||
// ### initialize
|
||||
initialize: function() {
|
||||
var self = this;
|
||||
_.bindAll(this, 'query');
|
||||
this.backend = null;
|
||||
if (this.get('backend')) {
|
||||
this.backend = this._backendFromString(this.get('backend'));
|
||||
} else { // try to guess backend ...
|
||||
if (this.get('records')) {
|
||||
this.backend = recline.Backend.Memory;
|
||||
}
|
||||
}
|
||||
this.fields = new my.FieldList();
|
||||
this.records = new my.RecordList();
|
||||
this._changes = {
|
||||
deletes: [],
|
||||
updates: [],
|
||||
creates: []
|
||||
};
|
||||
this.facets = new my.FacetList();
|
||||
this.recordCount = null;
|
||||
this.queryState = new my.Query();
|
||||
this.queryState.bind('change facet:add', function () {
|
||||
self.query(); // We want to call query() without any arguments.
|
||||
});
|
||||
// store is what we query and save against
|
||||
// store will either be the backend or be a memory store if Backend fetch
|
||||
// tells us to use memory store
|
||||
this._store = this.backend;
|
||||
|
||||
// if backend has a handleQueryResultFunction, use that
|
||||
this._handleResult = (this.backend != null && _.has(this.backend, 'handleQueryResult')) ?
|
||||
this.backend.handleQueryResult : this._handleQueryResult;
|
||||
if (this.backend == recline.Backend.Memory) {
|
||||
this.fetch();
|
||||
}
|
||||
},
|
||||
|
||||
sync: function(method, model, options) {
|
||||
return this.backend.sync(method, model, options);
|
||||
},
|
||||
|
||||
// ### fetch
|
||||
//
|
||||
// Retrieve dataset and (some) records from the backend.
|
||||
fetch: function() {
|
||||
var self = this;
|
||||
var dfd = new Deferred();
|
||||
|
||||
if (this.backend !== recline.Backend.Memory) {
|
||||
this.backend.fetch(this.toJSON())
|
||||
.done(handleResults)
|
||||
.fail(function(args) {
|
||||
dfd.reject(args);
|
||||
});
|
||||
} else {
|
||||
// special case where we have been given data directly
|
||||
handleResults({
|
||||
records: this.get('records'),
|
||||
fields: this.get('fields'),
|
||||
useMemoryStore: true
|
||||
});
|
||||
}
|
||||
|
||||
function handleResults(results) {
|
||||
// if explicitly given the fields
|
||||
// (e.g. var dataset = new Dataset({fields: fields, ...})
|
||||
// use that field info over anything we get back by parsing the data
|
||||
// (results.fields)
|
||||
var fields = self.get('fields') || results.fields;
|
||||
|
||||
var out = self._normalizeRecordsAndFields(results.records, fields);
|
||||
if (results.useMemoryStore) {
|
||||
self._store = new recline.Backend.Memory.Store(out.records, out.fields);
|
||||
}
|
||||
|
||||
self.set(results.metadata);
|
||||
self.fields.reset(out.fields);
|
||||
self.query()
|
||||
.done(function() {
|
||||
dfd.resolve(self);
|
||||
})
|
||||
.fail(function(args) {
|
||||
dfd.reject(args);
|
||||
});
|
||||
}
|
||||
|
||||
return dfd.promise();
|
||||
},
|
||||
|
||||
// ### _normalizeRecordsAndFields
|
||||
//
|
||||
// Get a proper set of fields and records from incoming set of fields and records either of which may be null or arrays or objects
|
||||
//
|
||||
// e.g. fields = ['a', 'b', 'c'] and records = [ [1,2,3] ] =>
|
||||
// fields = [ {id: a}, {id: b}, {id: c}], records = [ {a: 1}, {b: 2}, {c: 3}]
|
||||
_normalizeRecordsAndFields: function(records, fields) {
|
||||
// if no fields get them from records
|
||||
if (!fields && records && records.length > 0) {
|
||||
// records is array then fields is first row of records ...
|
||||
if (records[0] instanceof Array) {
|
||||
fields = records[0];
|
||||
records = records.slice(1);
|
||||
} else {
|
||||
fields = _.map(_.keys(records[0]), function(key) {
|
||||
return {id: key};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// fields is an array of strings (i.e. list of field headings/ids)
|
||||
if (fields && fields.length > 0 && (fields[0] === null || typeof(fields[0]) != 'object')) {
|
||||
// Rename duplicate fieldIds as each field name needs to be
|
||||
// unique.
|
||||
var seen = {};
|
||||
fields = _.map(fields, function(field, index) {
|
||||
if (field === null) {
|
||||
field = '';
|
||||
} else {
|
||||
field = field.toString();
|
||||
}
|
||||
// cannot use trim as not supported by IE7
|
||||
var fieldId = field.replace(/^\s+|\s+$/g, '');
|
||||
if (fieldId === '') {
|
||||
fieldId = '_noname_';
|
||||
field = fieldId;
|
||||
}
|
||||
while (fieldId in seen) {
|
||||
seen[field] += 1;
|
||||
fieldId = field + seen[field];
|
||||
}
|
||||
if (!(field in seen)) {
|
||||
seen[field] = 0;
|
||||
}
|
||||
// TODO: decide whether to keep original name as label ...
|
||||
// return { id: fieldId, label: field || fieldId }
|
||||
return { id: fieldId };
|
||||
});
|
||||
}
|
||||
// records is provided as arrays so need to zip together with fields
|
||||
// NB: this requires you to have fields to match arrays
|
||||
if (records && records.length > 0 && records[0] instanceof Array) {
|
||||
records = _.map(records, function(doc) {
|
||||
var tmp = {};
|
||||
_.each(fields, function(field, idx) {
|
||||
tmp[field.id] = doc[idx];
|
||||
});
|
||||
return tmp;
|
||||
});
|
||||
}
|
||||
return {
|
||||
fields: fields,
|
||||
records: records
|
||||
};
|
||||
},
|
||||
|
||||
save: function() {
|
||||
var self = this;
|
||||
// TODO: need to reset the changes ...
|
||||
return this._store.save(this._changes, this.toJSON());
|
||||
},
|
||||
|
||||
// ### query
|
||||
//
|
||||
// AJAX method with promise API to get records from the backend.
|
||||
//
|
||||
// It will query based on current query state (given by this.queryState)
|
||||
// updated by queryObj (if provided).
|
||||
//
|
||||
// Resulting RecordList are used to reset this.records and are
|
||||
// also returned.
|
||||
query: function(queryObj) {
|
||||
var self = this;
|
||||
var dfd = new Deferred();
|
||||
this.trigger('query:start');
|
||||
|
||||
if (queryObj) {
|
||||
var attributes = queryObj;
|
||||
if (queryObj instanceof my.Query) {
|
||||
attributes = queryObj.toJSON();
|
||||
}
|
||||
this.queryState.set(attributes, {silent: true});
|
||||
}
|
||||
var actualQuery = this.queryState.toJSON();
|
||||
|
||||
this._store.query(actualQuery, this.toJSON())
|
||||
.done(function(queryResult) {
|
||||
self._handleResult(queryResult);
|
||||
self.trigger('query:done');
|
||||
dfd.resolve(self.records);
|
||||
})
|
||||
.fail(function(args) {
|
||||
self.trigger('query:fail', args);
|
||||
dfd.reject(args);
|
||||
});
|
||||
return dfd.promise();
|
||||
},
|
||||
|
||||
_handleQueryResult: function(queryResult) {
|
||||
var self = this;
|
||||
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());
|
||||
});
|
||||
_doc.bind('destroy', function(doc) {
|
||||
self._changes.deletes.push(doc.toJSON());
|
||||
});
|
||||
return _doc;
|
||||
});
|
||||
self.records.reset(docs);
|
||||
if (queryResult.facets) {
|
||||
var facets = _.map(queryResult.facets, function(facetResult, facetId) {
|
||||
facetResult.id = facetId;
|
||||
return new my.Facet(facetResult);
|
||||
});
|
||||
self.facets.reset(facets);
|
||||
}
|
||||
},
|
||||
|
||||
toTemplateJSON: function() {
|
||||
var data = this.toJSON();
|
||||
data.recordCount = this.recordCount;
|
||||
data.fields = this.fields.toJSON();
|
||||
return data;
|
||||
},
|
||||
|
||||
// ### getFieldsSummary
|
||||
//
|
||||
// Get a summary for each field in the form of a `Facet`.
|
||||
//
|
||||
// @return null as this is async function. Provides deferred/promise interface.
|
||||
getFieldsSummary: function() {
|
||||
var self = this;
|
||||
var query = new my.Query();
|
||||
query.set({size: 0});
|
||||
this.fields.each(function(field) {
|
||||
query.addFacet(field.id);
|
||||
});
|
||||
var dfd = new Deferred();
|
||||
this._store.query(query.toJSON(), this.toJSON()).done(function(queryResult) {
|
||||
if (queryResult.facets) {
|
||||
_.each(queryResult.facets, function(facetResult, facetId) {
|
||||
facetResult.id = facetId;
|
||||
var facet = new my.Facet(facetResult);
|
||||
// TODO: probably want replace rather than reset (i.e. just replace the facet with this id)
|
||||
self.fields.get(facetId).facets.reset(facet);
|
||||
});
|
||||
}
|
||||
dfd.resolve(queryResult);
|
||||
});
|
||||
return dfd.promise();
|
||||
},
|
||||
|
||||
// Deprecated (as of v0.5) - use record.summary()
|
||||
recordSummary: function(record) {
|
||||
return record.summary();
|
||||
},
|
||||
|
||||
// ### _backendFromString(backendString)
|
||||
//
|
||||
// Look up a backend module from a backend string (look in recline.Backend)
|
||||
_backendFromString: function(backendString) {
|
||||
var backend = null;
|
||||
if (recline && recline.Backend) {
|
||||
_.each(_.keys(recline.Backend), function(name) {
|
||||
if (name.toLowerCase() === backendString.toLowerCase()) {
|
||||
backend = recline.Backend[name];
|
||||
}
|
||||
});
|
||||
}
|
||||
return backend;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// ## <a id="record">A Record</a>
|
||||
//
|
||||
// A single record (or row) in the dataset
|
||||
my.Record = Backbone.Model.extend({
|
||||
constructor: function Record() {
|
||||
Backbone.Model.prototype.constructor.apply(this, arguments);
|
||||
},
|
||||
|
||||
// ### initialize
|
||||
//
|
||||
// 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
|
||||
//
|
||||
// For the provided Field get the corresponding rendered computed data value
|
||||
// for this record.
|
||||
//
|
||||
// NB: if field is undefined a default '' value will be returned
|
||||
getFieldValue: function(field) {
|
||||
var val = this.getFieldValueUnrendered(field);
|
||||
if (field && !_.isUndefined(field.renderer)) {
|
||||
val = field.renderer(val, field, this.toJSON());
|
||||
}
|
||||
return val;
|
||||
},
|
||||
|
||||
// ### getFieldValueUnrendered
|
||||
//
|
||||
// For the provided Field get the corresponding computed data value
|
||||
// for this record.
|
||||
//
|
||||
// NB: if field is undefined a default '' value will be returned
|
||||
getFieldValueUnrendered: function(field) {
|
||||
if (!field) {
|
||||
return '';
|
||||
}
|
||||
var val = this.get(field.id);
|
||||
if (field.deriver) {
|
||||
val = field.deriver(val, field, this);
|
||||
}
|
||||
return val;
|
||||
},
|
||||
|
||||
// ### 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({
|
||||
constructor: function RecordList() {
|
||||
Backbone.Collection.prototype.constructor.apply(this, arguments);
|
||||
},
|
||||
model: my.Record
|
||||
});
|
||||
|
||||
|
||||
// ## <a id="field">A Field (aka Column) on a Dataset</a>
|
||||
my.Field = Backbone.Model.extend({
|
||||
constructor: function Field() {
|
||||
Backbone.Model.prototype.constructor.apply(this, arguments);
|
||||
},
|
||||
// ### defaults - define default values
|
||||
defaults: {
|
||||
label: null,
|
||||
type: 'string',
|
||||
format: null,
|
||||
is_derived: false
|
||||
},
|
||||
// ### 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) {
|
||||
throw new Error('Looks like you did not pass a proper hash with id to Field constructor');
|
||||
}
|
||||
if (this.attributes.label === null) {
|
||||
this.set({label: this.id});
|
||||
}
|
||||
if (this.attributes.type.toLowerCase() in this._typeMap) {
|
||||
this.attributes.type = this._typeMap[this.attributes.type.toLowerCase()];
|
||||
}
|
||||
if (options) {
|
||||
this.renderer = options.renderer;
|
||||
this.deriver = options.deriver;
|
||||
}
|
||||
if (!this.renderer) {
|
||||
this.renderer = this.defaultRenderers[this.get('type')];
|
||||
}
|
||||
this.facets = new my.FacetList();
|
||||
},
|
||||
_typeMap: {
|
||||
'text': 'string',
|
||||
'double': 'number',
|
||||
'float': 'number',
|
||||
'numeric': 'number',
|
||||
'int': 'integer',
|
||||
'datetime': 'date-time',
|
||||
'bool': 'boolean',
|
||||
'timestamp': 'date-time',
|
||||
'json': 'object'
|
||||
},
|
||||
defaultRenderers: {
|
||||
object: function(val, field, doc) {
|
||||
return JSON.stringify(val);
|
||||
},
|
||||
geo_point: function(val, field, doc) {
|
||||
return JSON.stringify(val);
|
||||
},
|
||||
'number': function(val, field, doc) {
|
||||
var format = field.get('format');
|
||||
if (format === 'percentage') {
|
||||
return val + '%';
|
||||
}
|
||||
return val;
|
||||
},
|
||||
'string': function(val, field, doc) {
|
||||
var format = field.get('format');
|
||||
if (format === 'markdown') {
|
||||
if (typeof Showdown !== 'undefined') {
|
||||
var showdown = new Showdown.converter();
|
||||
out = showdown.makeHtml(val);
|
||||
return out;
|
||||
} else {
|
||||
return val;
|
||||
}
|
||||
} else if (format == 'plain') {
|
||||
return val;
|
||||
} 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>');
|
||||
}
|
||||
return val;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
my.FieldList = Backbone.Collection.extend({
|
||||
constructor: function FieldList() {
|
||||
Backbone.Collection.prototype.constructor.apply(this, arguments);
|
||||
},
|
||||
model: my.Field
|
||||
});
|
||||
|
||||
// ## <a id="query">Query</a>
|
||||
my.Query = Backbone.Model.extend({
|
||||
constructor: function Query() {
|
||||
Backbone.Model.prototype.constructor.apply(this, arguments);
|
||||
},
|
||||
defaults: function() {
|
||||
return {
|
||||
size: 100,
|
||||
from: 0,
|
||||
q: '',
|
||||
facets: {},
|
||||
filters: []
|
||||
};
|
||||
},
|
||||
_filterTemplates: {
|
||||
term: {
|
||||
type: 'term',
|
||||
// TODO do we need this attribute here?
|
||||
field: '',
|
||||
term: ''
|
||||
},
|
||||
range: {
|
||||
type: 'range',
|
||||
from: '',
|
||||
to: ''
|
||||
},
|
||||
geo_distance: {
|
||||
type: 'geo_distance',
|
||||
distance: 10,
|
||||
unit: 'km',
|
||||
point: {
|
||||
lon: 0,
|
||||
lat: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
// ### addFilter(filter)
|
||||
//
|
||||
// Add a new filter specified by the filter hash and append 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 fully specified so use template and over-write
|
||||
if (_.keys(filter).length <= 3) {
|
||||
ourfilter = _.defaults(ourfilter, this._filterTemplates[filter.type]);
|
||||
}
|
||||
var filters = this.get('filters');
|
||||
filters.push(ourfilter);
|
||||
this.trigger('change:filters:new-blank');
|
||||
},
|
||||
replaceFilter: function(filter) {
|
||||
// delete filter on the same field, then add
|
||||
var filters = this.get('filters');
|
||||
var idx = -1;
|
||||
_.each(this.get('filters'), function(f, key, list) {
|
||||
if (filter.field == f.field) {
|
||||
idx = key;
|
||||
}
|
||||
});
|
||||
// trigger just one event (change:filters:new-blank) instead of one for remove and
|
||||
// one for add
|
||||
if (idx >= 0) {
|
||||
filters.splice(idx, 1);
|
||||
this.set({filters: filters});
|
||||
}
|
||||
this.addFilter(filter);
|
||||
},
|
||||
updateFilter: function(index, value) {
|
||||
},
|
||||
// ### 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
|
||||
//
|
||||
// Add a Facet to this query
|
||||
//
|
||||
// See <http://www.elasticsearch.org/guide/reference/api/search/facets/>
|
||||
addFacet: function(fieldId, size, silent) {
|
||||
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] = {
|
||||
terms: { field: fieldId }
|
||||
};
|
||||
if (!_.isUndefined(size)) {
|
||||
facets[fieldId].terms.size = size;
|
||||
}
|
||||
this.set({facets: facets}, {silent: true});
|
||||
if (!silent) {
|
||||
this.trigger('facet:add', this);
|
||||
}
|
||||
},
|
||||
addHistogramFacet: function(fieldId) {
|
||||
var facets = this.get('facets');
|
||||
facets[fieldId] = {
|
||||
date_histogram: {
|
||||
field: fieldId,
|
||||
interval: 'day'
|
||||
}
|
||||
};
|
||||
this.set({facets: facets}, {silent: true});
|
||||
this.trigger('facet:add', this);
|
||||
},
|
||||
removeFacet: 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)) {
|
||||
return;
|
||||
}
|
||||
delete facets[fieldId];
|
||||
this.set({facets: facets}, {silent: true});
|
||||
this.trigger('facet:remove', this);
|
||||
},
|
||||
clearFacets: function() {
|
||||
var facets = this.get('facets');
|
||||
_.each(_.keys(facets), function(fieldId) {
|
||||
delete facets[fieldId];
|
||||
});
|
||||
this.trigger('facet:remove', this);
|
||||
},
|
||||
// trigger a facet add; use this to trigger a single event after adding
|
||||
// multiple facets
|
||||
refreshFacets: function() {
|
||||
this.trigger('facet:add', this);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
// ## <a id="facet">A Facet (Result)</a>
|
||||
my.Facet = Backbone.Model.extend({
|
||||
constructor: function Facet() {
|
||||
Backbone.Model.prototype.constructor.apply(this, arguments);
|
||||
},
|
||||
defaults: function() {
|
||||
return {
|
||||
_type: 'terms',
|
||||
total: 0,
|
||||
other: 0,
|
||||
missing: 0,
|
||||
terms: []
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// ## 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
|
||||
//
|
||||
// Convenience Backbone model for storing (configuration) state of objects like Views.
|
||||
my.ObjectState = Backbone.Model.extend({
|
||||
});
|
||||
|
||||
|
||||
// ## 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);
|
||||
// };
|
||||
|
||||
}(this.recline.Model));
|
||||
|
||||
520
src/view.flot.js
520
src/view.flot.js
@@ -1,520 +0,0 @@
|
||||
/*jshint multistr:true */
|
||||
|
||||
this.recline = this.recline || {};
|
||||
this.recline.View = this.recline.View || {};
|
||||
|
||||
(function($, my) {
|
||||
"use strict";
|
||||
// ## Graph view for a Dataset using Flot graphing library.
|
||||
//
|
||||
// Initialization arguments (in a hash in first parameter):
|
||||
//
|
||||
// * model: recline.Model.Dataset
|
||||
// * state: (optional) configuration hash of form:
|
||||
//
|
||||
// {
|
||||
// group: {column name for x-axis},
|
||||
// series: [{column name for series A}, {column name series B}, ... ],
|
||||
// // options are: lines, points, lines-and-points, bars, columns
|
||||
// graphType: 'lines',
|
||||
// graphOptions: {custom [flot options]}
|
||||
// }
|
||||
//
|
||||
// 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.Flot = Backbone.View.extend({
|
||||
template: ' \
|
||||
<div class="recline-flot"> \
|
||||
<div class="panel graph" style="display: block;"> \
|
||||
<div class="js-temp-notice alert alert-warning alert-block"> \
|
||||
{{#t.flot_info}}<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>{{/t.flot_info}} \
|
||||
</div> \
|
||||
</div> \
|
||||
</div> \
|
||||
',
|
||||
|
||||
initialize: function(options) {
|
||||
var self = this;
|
||||
this.graphColors = ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"];
|
||||
|
||||
_.bindAll(this, 'render', 'redraw', '_toolTip', '_xaxisLabel');
|
||||
this.needToRedraw = false;
|
||||
this.listenTo(this.model, 'change', this.render);
|
||||
this.listenTo(this.model.fields, 'reset add', this.render);
|
||||
this.listenTo(this.model.records, 'reset add', this.redraw);
|
||||
var stateData = _.extend({
|
||||
group: null,
|
||||
// so that at least one series chooser box shows up
|
||||
series: [],
|
||||
graphType: 'lines-and-points'
|
||||
},
|
||||
options.state
|
||||
);
|
||||
this.state = new recline.Model.ObjectState(stateData);
|
||||
this.previousTooltipPoint = {x: null, y: null};
|
||||
this.editor = new my.FlotControls({
|
||||
model: this.model,
|
||||
state: this.state.toJSON()
|
||||
});
|
||||
this.listenTo(this.editor.state, 'change', function() {
|
||||
self.state.set(self.editor.state.toJSON());
|
||||
self.redraw();
|
||||
});
|
||||
this.elSidebar = this.editor.$el;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var self = this;
|
||||
var tmplData = I18nMessages('recline', recline.View.translations).injectMustache(this.model.toTemplateJSON());
|
||||
var htmls = Mustache.render(this.template, tmplData);
|
||||
this.$el.html(htmls);
|
||||
this.$graph = this.$el.find('.panel.graph');
|
||||
this.$graph.on("plothover", this._toolTip);
|
||||
return this;
|
||||
},
|
||||
|
||||
remove: function () {
|
||||
this.editor.remove();
|
||||
Backbone.View.prototype.remove.apply(this, arguments);
|
||||
},
|
||||
|
||||
redraw: function() {
|
||||
// There are issues generating a Flot graph if either:
|
||||
// * The relevant div that graph attaches to his hidden at the moment of creating the plot -- Flot will complain with
|
||||
// Uncaught Invalid dimensions for plot, width = 0, height = 0
|
||||
// * There is no data for the plot -- either same error or may have issues later with errors like 'non-existent node-value'
|
||||
var areWeVisible = !jQuery.expr.filters.hidden(this.el);
|
||||
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')) {
|
||||
var series = this.createSeries();
|
||||
var options = this.getGraphOptions(this.state.attributes.graphType, series[0].data.length);
|
||||
this.plot = $.plot(this.$graph, series, options);
|
||||
}
|
||||
},
|
||||
|
||||
show: function() {
|
||||
// because we cannot redraw when hidden we may need to when becoming visible
|
||||
if (this.needToRedraw) {
|
||||
this.redraw();
|
||||
}
|
||||
},
|
||||
|
||||
// infoboxes on mouse hover on points/bars etc
|
||||
_toolTip: function (event, pos, item) {
|
||||
if (item) {
|
||||
if (this.previousTooltipPoint.x !== item.dataIndex ||
|
||||
this.previousTooltipPoint.y !== item.seriesIndex) {
|
||||
this.previousTooltipPoint.x = item.dataIndex;
|
||||
this.previousTooltipPoint.y = item.seriesIndex;
|
||||
$("#recline-flot-tooltip").remove();
|
||||
|
||||
var x = item.datapoint[0].toFixed(2),
|
||||
y = item.datapoint[1].toFixed(2);
|
||||
|
||||
if (this.state.attributes.graphType === 'bars') {
|
||||
x = item.datapoint[1].toFixed(2),
|
||||
y = item.datapoint[0].toFixed(2);
|
||||
}
|
||||
|
||||
var template = _.template('<%= group %> = <%= x %>, <%= series %> = <%= y %>');
|
||||
var content = template({
|
||||
group: this.state.attributes.group,
|
||||
x: this._xaxisLabel(x),
|
||||
series: item.series.label,
|
||||
y: y
|
||||
});
|
||||
|
||||
// use a different tooltip location offset for bar charts
|
||||
var xLocation, yLocation;
|
||||
if (this.state.attributes.graphType === 'bars') {
|
||||
xLocation = item.pageX + 15;
|
||||
yLocation = item.pageY - 10;
|
||||
} else if (this.state.attributes.graphType === 'columns') {
|
||||
xLocation = item.pageX + 15;
|
||||
yLocation = item.pageY;
|
||||
} else {
|
||||
xLocation = item.pageX + 10;
|
||||
yLocation = item.pageY - 20;
|
||||
}
|
||||
|
||||
$('<div id="recline-flot-tooltip">' + content + '</div>').css({
|
||||
top: yLocation,
|
||||
left: xLocation
|
||||
}).appendTo("body").fadeIn(200);
|
||||
}
|
||||
} else {
|
||||
$("#recline-flot-tooltip").remove();
|
||||
this.previousTooltipPoint.x = null;
|
||||
this.previousTooltipPoint.y = null;
|
||||
}
|
||||
},
|
||||
|
||||
_xaxisLabel: function (x) {
|
||||
if (this._groupFieldIsDateTime()) {
|
||||
// oddly x comes through as milliseconds *string* (rather than int
|
||||
// or float) so we have to reparse
|
||||
x = new Date(parseFloat(x)).toLocaleDateString();
|
||||
} else if (this.xvaluesAreIndex) {
|
||||
x = parseInt(x, 10);
|
||||
// HACK: deal with bar graph style cases where x-axis items were strings
|
||||
// In this case x at this point is the index of the item in the list of
|
||||
// records not its actual x-axis value
|
||||
x = this.model.records.models[x].get(this.state.attributes.group);
|
||||
}
|
||||
|
||||
return x;
|
||||
},
|
||||
|
||||
// ### getGraphOptions
|
||||
//
|
||||
// Get options for Flot Graph
|
||||
//
|
||||
// needs to be function as can depend on state
|
||||
//
|
||||
// @param typeId graphType id (lines, lines-and-points etc)
|
||||
// @param numPoints the number of points that will be plotted
|
||||
getGraphOptions: function(typeId, numPoints) {
|
||||
var self = this;
|
||||
var groupFieldIsDateTime = self._groupFieldIsDateTime();
|
||||
var xaxis = {};
|
||||
|
||||
if (!groupFieldIsDateTime) {
|
||||
xaxis.tickFormatter = function (x) {
|
||||
// convert x to a string and make sure that it is not too long or the
|
||||
// tick labels will overlap
|
||||
// TODO: find a more accurate way of calculating the size of tick labels
|
||||
var label = self._xaxisLabel(x) || "";
|
||||
|
||||
if (typeof label !== 'string') {
|
||||
label = label.toString();
|
||||
}
|
||||
if (self.state.attributes.graphType !== 'bars' && label.length > 10) {
|
||||
label = label.slice(0, 10) + "...";
|
||||
}
|
||||
|
||||
return label;
|
||||
};
|
||||
}
|
||||
|
||||
// for labels case we only want ticks at the label intervals
|
||||
// HACK: however we also get this case with Date fields. In that case we
|
||||
// could have a lot of values and so we limit to max 15 (we assume)
|
||||
if (this.xvaluesAreIndex) {
|
||||
var numTicks = Math.min(this.model.records.length, 15);
|
||||
var increment = this.model.records.length / numTicks;
|
||||
var ticks = [];
|
||||
for (var i=0; i<numTicks; i++) {
|
||||
ticks.push(parseInt(i*increment, 10));
|
||||
}
|
||||
xaxis.ticks = ticks;
|
||||
} else if (groupFieldIsDateTime) {
|
||||
xaxis.mode = 'time';
|
||||
}
|
||||
|
||||
var yaxis = {};
|
||||
yaxis.autoscale = true;
|
||||
yaxis.autoscaleMargin = 0.02;
|
||||
|
||||
var legend = {};
|
||||
legend.position = 'ne';
|
||||
|
||||
var grid = {};
|
||||
grid.hoverable = true;
|
||||
grid.clickable = true;
|
||||
grid.borderColor = "#aaaaaa";
|
||||
grid.borderWidth = 1;
|
||||
|
||||
var optionsPerGraphType = {
|
||||
lines: {
|
||||
legend: legend,
|
||||
colors: this.graphColors,
|
||||
lines: { show: true },
|
||||
xaxis: xaxis,
|
||||
yaxis: yaxis,
|
||||
grid: grid
|
||||
},
|
||||
points: {
|
||||
legend: legend,
|
||||
colors: this.graphColors,
|
||||
points: { show: true, hitRadius: 5 },
|
||||
xaxis: xaxis,
|
||||
yaxis: yaxis,
|
||||
grid: grid
|
||||
},
|
||||
'lines-and-points': {
|
||||
legend: legend,
|
||||
colors: this.graphColors,
|
||||
points: { show: true, hitRadius: 5 },
|
||||
lines: { show: true },
|
||||
xaxis: xaxis,
|
||||
yaxis: yaxis,
|
||||
grid: grid
|
||||
},
|
||||
bars: {
|
||||
legend: legend,
|
||||
colors: this.graphColors,
|
||||
lines: { show: false },
|
||||
xaxis: yaxis,
|
||||
yaxis: xaxis,
|
||||
grid: grid,
|
||||
bars: {
|
||||
show: true,
|
||||
horizontal: true,
|
||||
shadowSize: 0,
|
||||
align: 'center',
|
||||
barWidth: 0.8
|
||||
}
|
||||
},
|
||||
columns: {
|
||||
legend: legend,
|
||||
colors: this.graphColors,
|
||||
lines: { show: false },
|
||||
xaxis: xaxis,
|
||||
yaxis: yaxis,
|
||||
grid: grid,
|
||||
bars: {
|
||||
show: true,
|
||||
horizontal: false,
|
||||
shadowSize: 0,
|
||||
align: 'center',
|
||||
barWidth: 0.8
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (self.state.get('graphOptions')) {
|
||||
return _.extend(optionsPerGraphType[typeId],
|
||||
self.state.get('graphOptions'));
|
||||
} else {
|
||||
return optionsPerGraphType[typeId];
|
||||
}
|
||||
},
|
||||
|
||||
_groupFieldIsDateTime: function() {
|
||||
var xfield = this.model.fields.get(this.state.attributes.group);
|
||||
var xtype = xfield.get('type');
|
||||
var isDateTime = (xtype === 'date' || xtype === 'date-time' || xtype === 'time');
|
||||
return isDateTime;
|
||||
},
|
||||
|
||||
createSeries: function() {
|
||||
var self = this;
|
||||
self.xvaluesAreIndex = false;
|
||||
var series = [];
|
||||
var xfield = self.model.fields.get(self.state.attributes.group);
|
||||
var isDateTime = self._groupFieldIsDateTime();
|
||||
|
||||
_.each(this.state.attributes.series, function(field) {
|
||||
var points = [];
|
||||
var fieldLabel = self.model.fields.get(field).get('label');
|
||||
|
||||
if (isDateTime){
|
||||
var cast = function(x){
|
||||
var _date = moment(String(x));
|
||||
if (_date.isValid()) {
|
||||
x = _date.toDate().getTime();
|
||||
}
|
||||
return x
|
||||
}
|
||||
} else {
|
||||
var raw = _.map(self.model.records.models,
|
||||
function(doc, index){
|
||||
return doc.getFieldValueUnrendered(xfield)
|
||||
});
|
||||
|
||||
if (_.all(raw, function(x){ return !isNaN(parseFloat(x)) })){
|
||||
var cast = function(x){ return parseFloat(x) }
|
||||
} else {
|
||||
self.xvaluesAreIndex = true
|
||||
}
|
||||
}
|
||||
|
||||
_.each(self.model.records.models, function(doc, index) {
|
||||
if(self.xvaluesAreIndex){
|
||||
var x = index;
|
||||
}else{
|
||||
var x = cast(doc.getFieldValueUnrendered(xfield));
|
||||
}
|
||||
|
||||
var yfield = self.model.fields.get(field);
|
||||
var y = parseFloat(doc.getFieldValueUnrendered(yfield));
|
||||
|
||||
if (self.state.attributes.graphType == 'bars') {
|
||||
points.push([y, x]);
|
||||
} else {
|
||||
points.push([x, y]);
|
||||
}
|
||||
});
|
||||
series.push({
|
||||
data: points,
|
||||
label: fieldLabel,
|
||||
hoverable: true
|
||||
});
|
||||
});
|
||||
return series;
|
||||
}
|
||||
});
|
||||
|
||||
my.FlotControls = Backbone.View.extend({
|
||||
className: "editor",
|
||||
template: ' \
|
||||
<div class="editor"> \
|
||||
<form class="form-stacked"> \
|
||||
<div class="clearfix"> \
|
||||
<div class="form-group"> \
|
||||
<label>{{t.Graph_Type}}</label> \
|
||||
<div class="input editor-type"> \
|
||||
<select class="form-control"> \
|
||||
<option value="lines-and-points">{{t.Lines_and_Points}}</option> \
|
||||
<option value="lines">{{t.Lines}}</option> \
|
||||
<option value="points">{{t.Points}}</option> \
|
||||
<option value="bars">{{t.Bars}}</option> \
|
||||
<option value="columns">{{t.Columns}}</option> \
|
||||
</select> \
|
||||
</div> \
|
||||
</div> \
|
||||
<div class="form-group"> \
|
||||
<label>{{t.flot_Group_Column}}</label> \
|
||||
<div class="input editor-group"> \
|
||||
<select class="form-control"> \
|
||||
<option value="">{{t.Please_choose}} ...</option> \
|
||||
{{#fields}} \
|
||||
<option value="{{id}}">{{label}}</option> \
|
||||
{{/fields}} \
|
||||
</select> \
|
||||
</div> \
|
||||
</div> \
|
||||
<div class="editor-series-group"> \
|
||||
</div> \
|
||||
</div> \
|
||||
<div class="editor-buttons"> \
|
||||
<button class="btn btn-default editor-add">{{t.Add_Series}}</button> \
|
||||
</div> \
|
||||
<div class="editor-buttons editor-submit" comment="hidden temporarily" style="display: none;"> \
|
||||
<button class="editor-save">{{t.Save}}</button> \
|
||||
<input type="hidden" class="editor-id" value="chart-1" /> \
|
||||
</div> \
|
||||
</form> \
|
||||
</div> \
|
||||
',
|
||||
templateSeriesEditor: ' \
|
||||
<div class="editor-series js-series-{{seriesIndex}}"> \
|
||||
<div class="form-group"> \
|
||||
<label>{{t.Series}} <span>{{seriesName}} ({{t.Axis_2}})</span> \
|
||||
[<a href="#remove" class="action-remove-series">{{t.Remove}}</a>] \
|
||||
<option value="{{id}}">{{label}}</option> \
|
||||
{{/fields}} \
|
||||
</div> \
|
||||
</div> \
|
||||
</div> \
|
||||
',
|
||||
events: {
|
||||
'change form select': 'onEditorSubmit',
|
||||
'click .editor-add': '_onAddSeries',
|
||||
'click .action-remove-series': 'removeSeries'
|
||||
},
|
||||
|
||||
initialize: function(options) {
|
||||
var self = this;
|
||||
_.bindAll(this, 'render');
|
||||
this.listenTo(this.model.fields, 'reset add', this.render);
|
||||
this.state = new recline.Model.ObjectState(options.state);
|
||||
this.render();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var self = this;
|
||||
var tmplData = this.model.toTemplateJSON();
|
||||
tmplData = I18nMessages('recline', recline.View.translations).injectMustache(tmplData);
|
||||
var htmls = Mustache.render(this.template, tmplData);
|
||||
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 = [""];
|
||||
if (this.state.get('series').length > 0) {
|
||||
tmpSeries = this.state.get('series');
|
||||
}
|
||||
_.each(tmpSeries, function(series, idx) {
|
||||
self.addSeries(idx);
|
||||
self._selectOption('.editor-series.js-series-' + idx, series);
|
||||
});
|
||||
return this;
|
||||
},
|
||||
|
||||
// Private: Helper function to select an option from a select list
|
||||
//
|
||||
_selectOption: function(id,value){
|
||||
var options = this.$el.find(id + ' select > option');
|
||||
if (options) {
|
||||
options.each(function(opt){
|
||||
if (this.value == value) {
|
||||
$(this).attr('selected','selected');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onEditorSubmit: function(e) {
|
||||
var select = this.$el.find('.editor-group select');
|
||||
var $editor = this;
|
||||
var $series = this.$el.find('.editor-series select');
|
||||
var series = $series.map(function () {
|
||||
return $(this).val();
|
||||
});
|
||||
var updatedState = {
|
||||
series: $.makeArray(series),
|
||||
group: this.$el.find('.editor-group select').val(),
|
||||
graphType: this.$el.find('.editor-type select').val()
|
||||
};
|
||||
this.state.set(updatedState);
|
||||
},
|
||||
|
||||
// Public: Adds a new empty series select box to the editor.
|
||||
//
|
||||
// @param [int] idx index of this series in the list of series
|
||||
//
|
||||
// Returns itself.
|
||||
addSeries: function (idx) {
|
||||
var data = _.extend({
|
||||
seriesIndex: idx,
|
||||
seriesName: String.fromCharCode(idx + 64 + 1)
|
||||
}, this.model.toTemplateJSON());
|
||||
|
||||
data = I18nMessages('recline', recline.View.translations).injectMustache(data);
|
||||
var htmls = Mustache.render(this.templateSeriesEditor, data);
|
||||
this.$el.find('.editor-series-group').append(htmls);
|
||||
return this;
|
||||
},
|
||||
|
||||
_onAddSeries: function(e) {
|
||||
e.preventDefault();
|
||||
this.addSeries(this.state.get('series').length);
|
||||
},
|
||||
|
||||
// Public: Removes a series list item from the editor.
|
||||
//
|
||||
// Also updates the labels of the remaining series elements.
|
||||
removeSeries: function (e) {
|
||||
e.preventDefault();
|
||||
var $el = $(e.target);
|
||||
$el.parent().parent().remove();
|
||||
this.onEditorSubmit();
|
||||
}
|
||||
});
|
||||
|
||||
})(jQuery, recline.View);
|
||||
@@ -1,4 +0,0 @@
|
||||
this.recline = this.recline || {};
|
||||
this.recline.View = this.recline.View || {};
|
||||
this.recline.View.Graph = this.recline.View.Flot;
|
||||
this.recline.View.GraphControls = this.recline.View.FlotControls;
|
||||
273
src/view.grid.js
273
src/view.grid.js
@@ -1,273 +0,0 @@
|
||||
/*jshint multistr:true */
|
||||
|
||||
this.recline = this.recline || {};
|
||||
this.recline.View = this.recline.View || {};
|
||||
|
||||
(function($, my) {
|
||||
"use strict";
|
||||
// ## (Data) Grid Dataset View
|
||||
//
|
||||
// Provides a tabular view on a Dataset.
|
||||
//
|
||||
// Initialize it with a `recline.Model.Dataset`.
|
||||
my.Grid = Backbone.View.extend({
|
||||
tagName: "div",
|
||||
className: "recline-grid-container",
|
||||
|
||||
initialize: function(modelEtc) {
|
||||
var self = this;
|
||||
_.bindAll(this, 'render', 'onHorizontalScroll');
|
||||
this.listenTo(this.model.records, 'add reset remove', this.render);
|
||||
this.tempState = {};
|
||||
var state = _.extend({
|
||||
hiddenFields: []
|
||||
}, modelEtc.state
|
||||
);
|
||||
this.state = new recline.Model.ObjectState(state);
|
||||
},
|
||||
|
||||
events: {
|
||||
// does not work here so done at end of render function
|
||||
// 'scroll .recline-grid tbody': 'onHorizontalScroll'
|
||||
},
|
||||
|
||||
// ======================================================
|
||||
// Column and row menus
|
||||
|
||||
setColumnSort: function(order) {
|
||||
var sort = [{}];
|
||||
sort[0][this.tempState.currentColumn] = {order: order};
|
||||
this.model.query({sort: sort});
|
||||
},
|
||||
|
||||
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.render();
|
||||
},
|
||||
|
||||
showColumn: function(e) {
|
||||
var hiddenFields = _.without(this.state.get('hiddenFields'), $(e.target).data('column'));
|
||||
this.state.set({hiddenFields: hiddenFields});
|
||||
this.render();
|
||||
},
|
||||
|
||||
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> \
|
||||
{{#fields}} \
|
||||
<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}} \
|
||||
<th class="last-header" style="width: {{lastHeaderWidth}}px; max-width: {{lastHeaderWidth}}px; min-width: {{lastHeaderWidth}}px; padding: 0; margin: 0;"></th> \
|
||||
</tr> \
|
||||
</thead> \
|
||||
<tbody class="scroll-content"></tbody> \
|
||||
</table> \
|
||||
</div> \
|
||||
',
|
||||
|
||||
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 = this.fields.map(function(field) {
|
||||
return field.toJSON();
|
||||
});
|
||||
// last header width = scroll bar - border (2px) */
|
||||
modelData.lastHeaderWidth = this.scrollbarDimensions.width - 2;
|
||||
return modelData;
|
||||
},
|
||||
render: function() {
|
||||
var self = this;
|
||||
this.fields = new recline.Model.FieldList(this.model.fields.filter(function(field) {
|
||||
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), 10);
|
||||
// if columns extend outside viewport then remainder is 0
|
||||
var remainder = Math.max(fullWidth - numFields * width,0);
|
||||
this.fields.each(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});
|
||||
}
|
||||
});
|
||||
var tmplData = this.toTemplateJSON();
|
||||
tmplData = I18nMessages('recline', recline.View.translations).injectMustache(tmplData);
|
||||
|
||||
var htmls = Mustache.render(this.template, tmplData);
|
||||
this.$el.html(htmls);
|
||||
this.model.records.forEach(function(doc) {
|
||||
var tr = $('<tr />');
|
||||
self.$el.find('tbody').append(tr);
|
||||
var newView = new my.GridRow({
|
||||
model: doc,
|
||||
el: tr,
|
||||
fields: self.fields
|
||||
});
|
||||
newView.render();
|
||||
});
|
||||
// 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
|
||||
//
|
||||
// Measure width of a vertical scrollbar and height of a horizontal scrollbar.
|
||||
//
|
||||
// @return: { width: pixelWidth, height: pixelHeight }
|
||||
_scrollbarSize: function() {
|
||||
var $c = $("<div style='position:absolute; top:-10000px; left:-10000px; width:100px; height:100px; overflow:scroll;'></div>").appendTo("body");
|
||||
var dim = { width: $c.width() - $c[0].clientWidth + 1, height: $c.height() - $c[0].clientHeight };
|
||||
$c.remove();
|
||||
return dim;
|
||||
}
|
||||
});
|
||||
|
||||
// ## 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.
|
||||
//
|
||||
// In addition you *must* pass in a FieldList in the constructor options. This should be list of fields for the Grid.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// <pre>
|
||||
// var row = new GridRow({
|
||||
// model: dataset-record,
|
||||
// el: dom-element,
|
||||
// fields: mydatasets.fields // a FieldList object
|
||||
// });
|
||||
// </pre>
|
||||
my.GridRow = Backbone.View.extend({
|
||||
initialize: function(initData) {
|
||||
_.bindAll(this, 'render');
|
||||
this._fields = initData.fields;
|
||||
this.listenTo(this.model, 'change', this.render);
|
||||
},
|
||||
|
||||
template: ' \
|
||||
{{#cells}} \
|
||||
<td data-field="{{field}}" style="width: {{width}}px; max-width: {{width}}px; min-width: {{width}}px;"> \
|
||||
<div class="data-table-cell-content"> \
|
||||
<a href="javascript:{}" class="data-table-cell-edit" title="{{t.Edit_this_cell}}"> </a> \
|
||||
<div class="data-table-cell-value">{{{value}}}</div> \
|
||||
</div> \
|
||||
</td> \
|
||||
{{/cells}} \
|
||||
',
|
||||
events: {
|
||||
'click .data-table-cell-edit': 'onEditClick',
|
||||
'click .data-table-cell-editor .okButton': 'onEditorOK',
|
||||
'click .data-table-cell-editor .cancelButton': 'onEditorCancel'
|
||||
},
|
||||
|
||||
toTemplateJSON: function() {
|
||||
var self = this;
|
||||
var doc = this.model;
|
||||
var cellData = this._fields.map(function(field) {
|
||||
return {
|
||||
field: field.id,
|
||||
width: field.get('width'),
|
||||
value: doc.getFieldValue(field)
|
||||
};
|
||||
});
|
||||
return { id: this.id, cells: cellData };
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.$el.attr('data-id', this.model.id);
|
||||
var tmplData = this.toTemplateJSON();
|
||||
tmplData = I18nMessages('recline', recline.View.translations).injectMustache(tmplData);
|
||||
var html = Mustache.render(this.template, tmplData);
|
||||
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> \
|
||||
<div id="data-table-cell-editor-actions"> \
|
||||
<div class="data-table-cell-editor-action"> \
|
||||
<button class="okButton btn primary">{{t.Update}}</button> \
|
||||
<button class="cancelButton btn danger">{{t.Cancel}}</button> \
|
||||
</div> \
|
||||
</div> \
|
||||
</div> \
|
||||
',
|
||||
|
||||
onEditClick: function(e) {
|
||||
var editing = this.$el.find('.data-table-cell-editor-editor');
|
||||
if (editing.length > 0) {
|
||||
editing.parents('.data-table-cell-value').html(editing.text()).siblings('.data-table-cell-edit').removeClass("hidden");
|
||||
}
|
||||
$(e.target).addClass("hidden");
|
||||
var cell = $(e.target).siblings('.data-table-cell-value');
|
||||
cell.data("previousContents", cell.text());
|
||||
|
||||
var tmplData = I18nMessages('recline', recline.View.translations).injectMustache({value: cell.text()});
|
||||
var output = Mustache.render(this.cellEditorTemplate, tmplData);
|
||||
cell.html(output);
|
||||
},
|
||||
|
||||
onEditorOK: function(e) {
|
||||
var self = this;
|
||||
var cell = $(e.target);
|
||||
var rowId = cell.parents('tr').attr('data-id');
|
||||
var field = cell.parents('td').attr('data-field');
|
||||
var newValue = cell.parents('.data-table-cell-editor').find('.data-table-cell-editor-editor').val();
|
||||
var newData = {};
|
||||
newData[field] = newValue;
|
||||
this.model.set(newData);
|
||||
|
||||
var fmt = I18nMessages('recline', recline.View.translations);
|
||||
this.trigger('recline:flash', {message: fmt.t("Updating_row") + "...", loader: true});
|
||||
this.model.save().then(function(response) {
|
||||
this.trigger('recline:flash', {message: fmt.t("Row_updated_successfully"), category: 'success'});
|
||||
})
|
||||
.fail(function() {
|
||||
this.trigger('recline:flash', {
|
||||
message: fmt.t('Error_saving_row'),
|
||||
category: 'error',
|
||||
persist: true
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
onEditorCancel: function(e) {
|
||||
var cell = $(e.target).parents('.data-table-cell-value');
|
||||
cell.html(cell.data('previousContents')).siblings('.data-table-cell-edit').removeClass("hidden");
|
||||
}
|
||||
});
|
||||
|
||||
})(jQuery, recline.View);
|
||||
687
src/view.map.js
687
src/view.map.js
@@ -1,687 +0,0 @@
|
||||
/*jshint multistr:true */
|
||||
|
||||
this.recline = this.recline || {};
|
||||
this.recline.View = this.recline.View || {};
|
||||
|
||||
(function($, my) {
|
||||
"use strict";
|
||||
// ## Map view for a Dataset using Leaflet mapping library.
|
||||
//
|
||||
// This view allows to plot gereferenced records on a map. The location
|
||||
// information can be provided in 2 ways:
|
||||
//
|
||||
// 1. Via a single field. This field must be either a geo_point or
|
||||
// [GeoJSON](http://geojson.org) object
|
||||
// 2. Via two fields with latitude and longitude coordinates.
|
||||
//
|
||||
// Which fields in the data these correspond to can be configured via the state
|
||||
// (and are guessed if no info is provided).
|
||||
//
|
||||
// Initialization arguments are as standard for Dataset Views. State object may
|
||||
// have the following (optional) configuration options:
|
||||
//
|
||||
// <pre>
|
||||
// {
|
||||
// // geomField if specified will be used in preference to lat/lon
|
||||
// geomField: {id of field containing geometry in the dataset}
|
||||
// lonField: {id of field containing longitude in the dataset}
|
||||
// latField: {id of field containing latitude in the dataset}
|
||||
// autoZoom: true,
|
||||
// // use cluster support
|
||||
// // cluster: true = always on
|
||||
// // cluster: false = always off
|
||||
// cluster: false
|
||||
// }
|
||||
// </pre>
|
||||
//
|
||||
// Useful attributes to know about (if e.g. customizing)
|
||||
//
|
||||
// * map: the Leaflet map (L.Map)
|
||||
// * features: Leaflet GeoJSON layer containing all the features (L.GeoJSON)
|
||||
my.Map = Backbone.View.extend({
|
||||
template: ' \
|
||||
<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'],
|
||||
geometryFieldNames: ['geojson', 'geom','the_geom','geometry','spatial','location', 'geo', 'lonlat'],
|
||||
|
||||
initialize: function(options) {
|
||||
var self = this;
|
||||
this.options = options;
|
||||
this.visible = this.$el.is(':visible');
|
||||
this.mapReady = false;
|
||||
// this will be the Leaflet L.Map object (setup below)
|
||||
this.map = null;
|
||||
|
||||
var stateData = _.extend({
|
||||
geomField: null,
|
||||
lonField: null,
|
||||
latField: null,
|
||||
autoZoom: true,
|
||||
cluster: false
|
||||
},
|
||||
options.state
|
||||
);
|
||||
this.state = new recline.Model.ObjectState(stateData);
|
||||
|
||||
this._clusterOptions = {
|
||||
zoomToBoundsOnClick: true,
|
||||
//disableClusteringAtZoom: 10,
|
||||
maxClusterRadius: 80,
|
||||
singleMarkerMode: false,
|
||||
skipDuplicateAddTesting: true,
|
||||
animateAddingMarkers: false
|
||||
};
|
||||
|
||||
// Listen to changes in the fields
|
||||
this.listenTo(this.model.fields, 'change', function() {
|
||||
self._setupGeometryField();
|
||||
self.render();
|
||||
});
|
||||
|
||||
// Listen to changes in the records
|
||||
this.listenTo(this.model.records, 'add', function(doc){self.redraw('add',doc);});
|
||||
this.listenTo(this.model.records, 'change', function(doc){
|
||||
self.redraw('remove',doc);
|
||||
self.redraw('add',doc);
|
||||
});
|
||||
this.listenTo(this.model.records, 'remove', function(doc){self.redraw('remove',doc);});
|
||||
this.listenTo(this.model.records, 'reset', function(){self.redraw('reset');});
|
||||
|
||||
this.menu = new my.MapMenu({
|
||||
model: this.model,
|
||||
state: this.state.toJSON()
|
||||
});
|
||||
this.listenTo(this.menu.state, 'change', function() {
|
||||
self.state.set(self.menu.state.toJSON());
|
||||
self.redraw();
|
||||
});
|
||||
this.listenTo(this.state, 'change', function() {
|
||||
self.redraw();
|
||||
});
|
||||
this.elSidebar = this.menu.$el;
|
||||
},
|
||||
|
||||
// ## Customization Functions
|
||||
//
|
||||
// The following methods are designed for overriding in order to customize
|
||||
// behaviour
|
||||
|
||||
// ### infobox
|
||||
//
|
||||
// Function to create infoboxes used in popups. The default behaviour is very simple and just lists all attributes.
|
||||
//
|
||||
// Users should override this function to customize behaviour i.e.
|
||||
//
|
||||
// view = new View({...});
|
||||
// view.infobox = function(record) {
|
||||
// ...
|
||||
// }
|
||||
infobox: function(record) {
|
||||
var html = '';
|
||||
for (var key in record.attributes){
|
||||
if (!(this.state.get('geomField') && key == this.state.get('geomField'))){
|
||||
html += '<div><strong>' + key + '</strong>: '+ record.attributes[key] + '</div>';
|
||||
}
|
||||
}
|
||||
return html;
|
||||
},
|
||||
|
||||
// Options to use for the [Leaflet GeoJSON layer](http://leaflet.cloudmade.com/reference.html#geojson)
|
||||
// See also <http://leaflet.cloudmade.com/examples/geojson.html>
|
||||
//
|
||||
// e.g.
|
||||
//
|
||||
// pointToLayer: function(feature, latLng)
|
||||
// onEachFeature: function(feature, layer)
|
||||
//
|
||||
// See defaults for examples
|
||||
geoJsonLayerOptions: {
|
||||
// pointToLayer function to use when creating points
|
||||
//
|
||||
// Default behaviour shown here is to create a marker using the
|
||||
// popupContent set on the feature properties (created via infobox function
|
||||
// during feature generation)
|
||||
//
|
||||
// NB: inside pointToLayer `this` will be set to point to this map view
|
||||
// instance (which allows e.g. this.markers to work in this default case)
|
||||
pointToLayer: function (feature, latlng) {
|
||||
var marker = new L.Marker(latlng);
|
||||
marker.bindPopup(feature.properties.popupContent);
|
||||
// this is for cluster case
|
||||
this.markers.addLayer(marker);
|
||||
return marker;
|
||||
},
|
||||
// onEachFeature default which adds popup in
|
||||
onEachFeature: function(feature, layer) {
|
||||
if (feature.properties && feature.properties.popupContent) {
|
||||
layer.bindPopup(feature.properties.popupContent);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// END: Customization section
|
||||
// ----
|
||||
|
||||
// ### Public: Adds the necessary elements to the page.
|
||||
//
|
||||
// Also sets up the editor fields and the map if necessary.
|
||||
render: function() {
|
||||
var self = this;
|
||||
var tmplData = I18nMessages('recline', recline.View.translations).injectMustache(this.model.toTemplateJSON());
|
||||
var htmls = Mustache.render(this.template, tmplData);
|
||||
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
|
||||
//
|
||||
// Actions can be:
|
||||
//
|
||||
// * reset: Clear all features
|
||||
// * add: Add one or n features (records)
|
||||
// * remove: Remove one or n features (records)
|
||||
// * 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()){
|
||||
self._setupGeometryField();
|
||||
}
|
||||
if (!self.mapReady){
|
||||
self._setupMap();
|
||||
}
|
||||
|
||||
if (this._geomReady() && this.mapReady){
|
||||
// removing ad re-adding the layer enables faster bulk loading
|
||||
this.map.removeLayer(this.features);
|
||||
this.map.removeLayer(this.markers);
|
||||
|
||||
var countBefore = 0;
|
||||
this.features.eachLayer(function(){countBefore++;});
|
||||
|
||||
if (action == 'refresh' || action == 'reset') {
|
||||
this.features.clearLayers();
|
||||
// recreate cluster group because of issues with clearLayer
|
||||
this.map.removeLayer(this.markers);
|
||||
this.markers = new L.MarkerClusterGroup(this._clusterOptions);
|
||||
this._add(this.model.records.models);
|
||||
} else if (action == 'add' && doc){
|
||||
this._add(doc);
|
||||
} else if (action == 'remove' && doc){
|
||||
this._remove(doc);
|
||||
}
|
||||
|
||||
// this must come before zooming!
|
||||
// if not: errors when using e.g. circle markers like
|
||||
// "Cannot call method 'project' of undefined"
|
||||
if (this.state.get('cluster')) {
|
||||
this.map.addLayer(this.markers);
|
||||
} else {
|
||||
this.map.addLayer(this.features);
|
||||
}
|
||||
|
||||
if (this.state.get('autoZoom')){
|
||||
if (this.visible){
|
||||
this._zoomToFeatures();
|
||||
} else {
|
||||
this._zoomPending = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
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
|
||||
//
|
||||
// For each record passed, a GeoJSON geometry will be extracted and added
|
||||
// to the features layer. If an exception is thrown, the process will be
|
||||
// stopped and an error notification shown.
|
||||
//
|
||||
// Each feature will have a popup associated with all the record fields.
|
||||
//
|
||||
_add: function(docs){
|
||||
var self = this;
|
||||
|
||||
if (!(docs instanceof Array)) docs = [docs];
|
||||
|
||||
var count = 0;
|
||||
var wrongSoFar = 0;
|
||||
_.every(docs, function(doc){
|
||||
count += 1;
|
||||
var feature = self._getGeometryFromRecord(doc);
|
||||
if (typeof feature === 'undefined' || feature === null){
|
||||
// Empty field
|
||||
return true;
|
||||
} else if (feature instanceof Object){
|
||||
feature.properties = {
|
||||
popupContent: self.infobox(doc),
|
||||
// Add a reference to the model id, which will allow us to
|
||||
// link this Leaflet layer to a Recline doc
|
||||
cid: doc.cid
|
||||
};
|
||||
|
||||
try {
|
||||
self.features.addData(feature);
|
||||
} catch (except) {
|
||||
wrongSoFar += 1;
|
||||
var msg = 'Wrong geometry value';
|
||||
if (except.message) msg += ' (' + except.message + ')';
|
||||
if (wrongSoFar <= 10) {
|
||||
self.trigger('recline:flash', {message: msg, category:'error'});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
wrongSoFar += 1;
|
||||
if (wrongSoFar <= 10) {
|
||||
self.trigger('recline:flash', {message: 'Wrong geometry value', category:'error'});
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
},
|
||||
|
||||
// Private: Remove one or n features from the map
|
||||
//
|
||||
_remove: function(docs){
|
||||
|
||||
var self = this;
|
||||
|
||||
if (!(docs instanceof Array)) docs = [docs];
|
||||
|
||||
_.each(docs,function(doc){
|
||||
for (var key in self.features._layers){
|
||||
if (self.features._layers[key].feature.geometry.properties.cid == doc.cid){
|
||||
self.features.removeLayer(self.features._layers[key]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
// Private: convert DMS coordinates to decimal
|
||||
//
|
||||
// north and east are positive, south and west are negative
|
||||
//
|
||||
_parseCoordinateString: function(coord){
|
||||
if (typeof(coord) != 'string') {
|
||||
return(parseFloat(coord));
|
||||
}
|
||||
var dms = coord.split(/[^-?\.\d\w]+/);
|
||||
var deg = 0; var m = 0;
|
||||
var toDeg = [1, 60, 3600]; // conversion factors for Deg, min, sec
|
||||
var i;
|
||||
for (i = 0; i < dms.length; ++i) {
|
||||
if (isNaN(parseFloat(dms[i]))) {
|
||||
continue;
|
||||
}
|
||||
deg += parseFloat(dms[i]) / toDeg[m];
|
||||
m += 1;
|
||||
}
|
||||
if (coord.match(/[SW]/)) {
|
||||
deg = -1*deg;
|
||||
}
|
||||
return(deg);
|
||||
},
|
||||
|
||||
// Private: Return a GeoJSON geomtry extracted from the record fields
|
||||
//
|
||||
_getGeometryFromRecord: function(doc){
|
||||
if (this.state.get('geomField')){
|
||||
var value = doc.get(this.state.get('geomField'));
|
||||
if (typeof(value) === 'string'){
|
||||
// We *may* have a GeoJSON string representation
|
||||
try {
|
||||
value = $.parseJSON(value);
|
||||
} catch(e) {}
|
||||
}
|
||||
if (typeof(value) === 'string') {
|
||||
value = value.replace('(', '').replace(')', '');
|
||||
var parts = value.split(',');
|
||||
var lat = this._parseCoordinateString(parts[0]);
|
||||
var lon = this._parseCoordinateString(parts[1]);
|
||||
|
||||
if (!isNaN(lon) && !isNaN(parseFloat(lat))) {
|
||||
return {
|
||||
"type": "Point",
|
||||
"coordinates": [lon, lat]
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else if (value && _.isArray(value)) {
|
||||
// [ lon, lat ]
|
||||
return {
|
||||
"type": "Point",
|
||||
"coordinates": [value[0], value[1]]
|
||||
};
|
||||
} else if (value && value.lat) {
|
||||
// of form { lat: ..., lon: ...}
|
||||
return {
|
||||
"type": "Point",
|
||||
"coordinates": [value.lon || value.lng, value.lat]
|
||||
};
|
||||
}
|
||||
// We o/w assume that contents of the field are a valid GeoJSON object
|
||||
return value;
|
||||
} else if (this.state.get('lonField') && this.state.get('latField')){
|
||||
// We'll create a GeoJSON like point object from the two lat/lon fields
|
||||
var lon = doc.get(this.state.get('lonField'));
|
||||
var lat = doc.get(this.state.get('latField'));
|
||||
lon = this._parseCoordinateString(lon);
|
||||
lat = this._parseCoordinateString(lat);
|
||||
|
||||
if (!isNaN(parseFloat(lon)) && !isNaN(parseFloat(lat))) {
|
||||
return {
|
||||
type: 'Point',
|
||||
coordinates: [lon,lat]
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
// Private: Check if there is a field with GeoJSON geometries or alternatively,
|
||||
// two fields with lat/lon values.
|
||||
//
|
||||
// If not found, the user can define them via the UI form.
|
||||
_setupGeometryField: function(){
|
||||
// should not overwrite if we have already set this (e.g. explicitly via state)
|
||||
if (!this._geomReady()) {
|
||||
this.state.set({
|
||||
geomField: this._checkField(this.geometryFieldNames),
|
||||
latField: this._checkField(this.latitudeFieldNames),
|
||||
lonField: this._checkField(this.longitudeFieldNames)
|
||||
});
|
||||
this.menu.state.set(this.state.toJSON());
|
||||
}
|
||||
},
|
||||
|
||||
// Private: Check if a field in the current model exists in the provided
|
||||
// list of names.
|
||||
//
|
||||
//
|
||||
_checkField: function(fieldNames){
|
||||
var field;
|
||||
var modelFieldNames = this.model.fields.pluck('id');
|
||||
for (var i = 0; i < fieldNames.length; i++){
|
||||
for (var j = 0; j < modelFieldNames.length; j++){
|
||||
if (modelFieldNames[j].toLowerCase() == fieldNames[i].toLowerCase())
|
||||
return modelFieldNames[j];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
// Private: Zoom to map to current features extent if any, or to the full
|
||||
// extent if none.
|
||||
//
|
||||
_zoomToFeatures: function(){
|
||||
var bounds = this.features.getBounds();
|
||||
if (bounds && bounds.getNorthEast() && bounds.getSouthWest()){
|
||||
this.map.fitBounds(bounds);
|
||||
} else {
|
||||
this.map.setView([0, 0], 2);
|
||||
}
|
||||
},
|
||||
|
||||
// Private: Sets up the Leaflet map control and the features layer.
|
||||
//
|
||||
// The map uses a base layer from [Stamen](http://maps.stamen.com) based
|
||||
// on [OpenStreetMap data](http://openstreetmap.org) by default, but it can
|
||||
// be configured passing the `mapTilesURL` and `mapTilesAttribution` options
|
||||
// (`mapTilesSubdomains` is also supported), eg:
|
||||
//
|
||||
// view = new recline.View.Map({
|
||||
// model: dataset,
|
||||
// mapTilesURL: '//{s}.tiles.mapbox.com/v4/mapbox.mapbox-streets-v7/{z}/{x}/{y}.png?access_token=pk.XXXX',
|
||||
// mapTilesAttribution: '© MapBox etc..',
|
||||
// mapTilesSubdomains: 'ab'
|
||||
// })
|
||||
//
|
||||
//
|
||||
_setupMap: function(){
|
||||
var self = this;
|
||||
this.map = new L.Map(this.$map.get(0));
|
||||
var mapUrl = this.options.mapTilesURL || 'https://stamen-tiles-{s}.a.ssl.fastly.net/terrain/{z}/{x}/{y}.png';
|
||||
var attribution = this.options.mapTilesAttribution ||'Map tiles by <a href="http://stamen.com">Stamen Design</a> (<a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>). Data by <a href="http://openstreetmap.org">OpenStreetMap</a> (<a href="http://creativecommons.org/licenses/by-sa/3.0">CC BY SA</a>)';
|
||||
var subdomains = this.options.mapTilesSubdomains || 'abc';
|
||||
|
||||
var bg = new L.TileLayer(mapUrl, {maxZoom: 19, attribution: attribution, subdomains: subdomains});
|
||||
this.map.addLayer(bg);
|
||||
|
||||
this.markers = new L.MarkerClusterGroup(this._clusterOptions);
|
||||
|
||||
// rebind this (as needed in e.g. default case above)
|
||||
this.geoJsonLayerOptions.pointToLayer = _.bind(
|
||||
this.geoJsonLayerOptions.pointToLayer,
|
||||
this);
|
||||
this.features = new L.GeoJSON(null, this.geoJsonLayerOptions);
|
||||
|
||||
this.map.setView([0, 0], 2);
|
||||
|
||||
this.mapReady = true;
|
||||
},
|
||||
|
||||
// Private: Helper function to select an option from a select list
|
||||
//
|
||||
_selectOption: function(id,value){
|
||||
var options = $('.' + id + ' > select > option');
|
||||
if (options){
|
||||
options.each(function(opt){
|
||||
if (this.value == value) {
|
||||
$(this).attr('selected','selected');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
my.MapMenu = Backbone.View.extend({
|
||||
className: 'editor',
|
||||
|
||||
template: ' \
|
||||
<form class="form-stacked"> \
|
||||
<div class="clearfix"> \
|
||||
<div class="editor-field-type"> \
|
||||
<span>{{t.map_mapping}}:</span> \
|
||||
<label class="radio"> \
|
||||
<input type="radio" id="editor-field-type-latlon" name="editor-field-type" value="latlon" checked="checked"/> \
|
||||
{{t.map_mapping_lat_lon}}</label> \
|
||||
<label class="radio"> \
|
||||
<input type="radio" id="editor-field-type-geom" name="editor-field-type" value="geom" /> \
|
||||
{{t.map_mapping_geojson}}</label> \
|
||||
</div> \
|
||||
<div class="editor-field-type-latlon"> \
|
||||
<label>{{t.Latitude_field}}</label> \
|
||||
<div class="input editor-lat-field"> \
|
||||
<select id="form-field-lat-field" class="form-control"> \
|
||||
<option value=""></option> \
|
||||
{{#fields}} \
|
||||
<option value="{{id}}">{{label}}</option> \
|
||||
{{/fields}} \
|
||||
</select> \
|
||||
</div> \
|
||||
<label>{{t.Longitude_field}}</label> \
|
||||
<div class="input editor-lon-field"> \
|
||||
<select id="form-field-lon-field" class="form-control"> \
|
||||
<option value=""></option> \
|
||||
{{#fields}} \
|
||||
<option value="{{id}}">{{label}}</option> \
|
||||
{{/fields}} \
|
||||
</select> \
|
||||
</div> \
|
||||
</div> \
|
||||
<div class="editor-field-type-geom" style="display:none"> \
|
||||
<label>{{t.map_mapping_geojson}}</label> \
|
||||
<div class="input editor-geom-field"> \
|
||||
<select id="form-field-type-geom" class="form-control"> \
|
||||
<option value=""></option> \
|
||||
{{#fields}} \
|
||||
<option value="{{id}}">{{label}}</option> \
|
||||
{{/fields}} \
|
||||
</select> \
|
||||
</div> \
|
||||
</div> \
|
||||
</div> \
|
||||
<div class="editor-buttons"> \
|
||||
<button class="btn btn-default editor-update-map">{{t.Update}}</button> \
|
||||
</div> \
|
||||
<div class="editor-options" > \
|
||||
<label class="checkbox"> \
|
||||
<input type="checkbox" id="editor-auto-zoom" value="autozoom" checked="checked" /> \
|
||||
{{t.Auto_zoom_to_features}}</label> \
|
||||
<label class="checkbox"> \
|
||||
<input type="checkbox" id="editor-cluster" value="cluster"/> \
|
||||
{{t.Cluster_markers}}</label> \
|
||||
</div> \
|
||||
<input type="hidden" class="editor-id" value="map-1" /> \
|
||||
</form> \
|
||||
',
|
||||
|
||||
// Define here events for UI elements
|
||||
events: {
|
||||
'click .editor-update-map': 'onEditorSubmit',
|
||||
'change .editor-field-type': 'onFieldTypeChange',
|
||||
'click #editor-auto-zoom': 'onAutoZoomChange',
|
||||
'click #editor-cluster': 'onClusteringChange'
|
||||
},
|
||||
|
||||
initialize: function(options) {
|
||||
var self = this;
|
||||
_.bindAll(this, 'render');
|
||||
this.listenTo(this.model.fields, 'change', this.render);
|
||||
this.state = new recline.Model.ObjectState(options.state);
|
||||
this.listenTo(this.state, 'change', this.render);
|
||||
this.render();
|
||||
},
|
||||
|
||||
// ### Public: Adds the necessary elements to the page.
|
||||
//
|
||||
// Also sets up the editor fields and the map if necessary.
|
||||
render: function() {
|
||||
var self = this;
|
||||
var tmplData = I18nMessages('recline', recline.View.translations).injectMustache(this.model.toTemplateJSON());
|
||||
var htmls = Mustache.render(this.template, tmplData);
|
||||
this.$el.html(htmls);
|
||||
|
||||
if (this._geomReady() && this.model.fields.length){
|
||||
if (this.state.get('geomField')){
|
||||
this._selectOption('editor-geom-field',this.state.get('geomField'));
|
||||
this.$el.find('#editor-field-type-geom').attr('checked','checked').change();
|
||||
} else{
|
||||
this._selectOption('editor-lon-field',this.state.get('lonField'));
|
||||
this._selectOption('editor-lat-field',this.state.get('latField'));
|
||||
this.$el.find('#editor-field-type-latlon').attr('checked','checked').change();
|
||||
}
|
||||
}
|
||||
if (this.state.get('autoZoom')) {
|
||||
this.$el.find('#editor-auto-zoom').attr('checked', 'checked');
|
||||
} else {
|
||||
this.$el.find('#editor-auto-zoom').removeAttr('checked');
|
||||
}
|
||||
if (this.state.get('cluster')) {
|
||||
this.$el.find('#editor-cluster').attr('checked', 'checked');
|
||||
} else {
|
||||
this.$el.find('#editor-cluster').removeAttr('checked');
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
_geomReady: function() {
|
||||
return Boolean(this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField')));
|
||||
},
|
||||
|
||||
// ## UI Event handlers
|
||||
//
|
||||
|
||||
// Public: Update map with user options
|
||||
//
|
||||
// Right now the only configurable option is what field(s) contains the
|
||||
// location information.
|
||||
//
|
||||
onEditorSubmit: function(e){
|
||||
e.preventDefault();
|
||||
if (this.$el.find('#editor-field-type-geom').attr('checked')){
|
||||
this.state.set({
|
||||
geomField: this.$el.find('.editor-geom-field > select > option:selected').val(),
|
||||
lonField: null,
|
||||
latField: null
|
||||
});
|
||||
} else {
|
||||
this.state.set({
|
||||
geomField: null,
|
||||
lonField: this.$el.find('.editor-lon-field > select > option:selected').val(),
|
||||
latField: this.$el.find('.editor-lat-field > select > option:selected').val()
|
||||
});
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
// Public: Shows the relevant select lists depending on the location field
|
||||
// type selected.
|
||||
//
|
||||
onFieldTypeChange: function(e){
|
||||
if (e.target.value == 'geom'){
|
||||
this.$el.find('.editor-field-type-geom').show();
|
||||
this.$el.find('.editor-field-type-latlon').hide();
|
||||
} else {
|
||||
this.$el.find('.editor-field-type-geom').hide();
|
||||
this.$el.find('.editor-field-type-latlon').show();
|
||||
}
|
||||
},
|
||||
|
||||
onAutoZoomChange: function(e){
|
||||
this.state.set({autoZoom: !this.state.get('autoZoom')});
|
||||
},
|
||||
|
||||
onClusteringChange: function(e){
|
||||
this.state.set({cluster: !this.state.get('cluster')});
|
||||
},
|
||||
|
||||
// Private: Helper function to select an option from a select list
|
||||
//
|
||||
_selectOption: function(id,value){
|
||||
var options = this.$el.find('.' + id + ' > select > option');
|
||||
if (options){
|
||||
options.each(function(opt){
|
||||
if (this.value == value) {
|
||||
$(this).attr('selected','selected');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
})(jQuery, recline.View);
|
||||
@@ -1,568 +0,0 @@
|
||||
/*jshint multistr:true */
|
||||
|
||||
// Standard JS module setup
|
||||
this.recline = this.recline || {};
|
||||
this.recline.View = this.recline.View || {};
|
||||
|
||||
(function($, my) {
|
||||
"use strict";
|
||||
// ## MultiView
|
||||
//
|
||||
// Manage multiple views together along with query editor etc. Usage:
|
||||
//
|
||||
// <pre>
|
||||
// var myExplorer = new recline.View.MultiView({
|
||||
// model: {{recline.Model.Dataset instance}}
|
||||
// el: {{an existing dom element}}
|
||||
// views: {{dataset views}}
|
||||
// state: {{state configuration -- see below}}
|
||||
// });
|
||||
// </pre>
|
||||
//
|
||||
// ### Parameters
|
||||
//
|
||||
// **model**: (required) recline.model.Dataset instance.
|
||||
//
|
||||
// **el**: (required) DOM element to bind to. NB: the element already
|
||||
// being in the DOM is important for rendering of some subviews (e.g.
|
||||
// Graph).
|
||||
//
|
||||
// **views**: (optional) the dataset views (Grid, Graph etc) for
|
||||
// MultiView to show. This is an array of view hashes. If not provided
|
||||
// initialize with (recline.View.)Grid, Graph, and Map views (with obvious id
|
||||
// and labels!).
|
||||
//
|
||||
// <pre>
|
||||
// var views = [
|
||||
// {
|
||||
// id: 'grid', // used for routing
|
||||
// label: 'Grid', // used for view switcher
|
||||
// view: new recline.View.Grid({
|
||||
// model: dataset
|
||||
// })
|
||||
// },
|
||||
// {
|
||||
// id: 'graph',
|
||||
// label: 'Graph',
|
||||
// view: new recline.View.Graph({
|
||||
// model: dataset
|
||||
// })
|
||||
// }
|
||||
// ];
|
||||
// </pre>
|
||||
//
|
||||
// **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!).
|
||||
//
|
||||
// <pre>
|
||||
// var sidebarViews = [
|
||||
// {
|
||||
// id: 'filterEditor', // used for routing
|
||||
// label: 'Filters', // used for view switcher
|
||||
// view: new recline.View.FilterEditor({
|
||||
// model: dataset
|
||||
// })
|
||||
// },
|
||||
// {
|
||||
// id: 'fieldsView',
|
||||
// label: 'Fields',
|
||||
// view: new recline.View.Fields({
|
||||
// model: dataset
|
||||
// })
|
||||
// }
|
||||
// ];
|
||||
// </pre>
|
||||
//
|
||||
// **state**: standard state config for this view. This state is slightly
|
||||
// special as it includes config of many of the subviews.
|
||||
//
|
||||
// <pre>
|
||||
// var state = {
|
||||
// query: {dataset query state - see dataset.queryState object}
|
||||
// 'view-{id1}': {view-state for this view}
|
||||
// 'view-{id2}': {view-state for }
|
||||
// ...
|
||||
// // Explorer
|
||||
// currentView: id of current view (defaults to first view if not specified)
|
||||
// readOnly: (default: false) run in read-only mode
|
||||
// }
|
||||
// </pre>
|
||||
//
|
||||
// Note that at present we do *not* serialize information about the actual set
|
||||
// of views in use -- e.g. those specified by the views argument -- but instead
|
||||
// expect either that the default views are fine or that the client to have
|
||||
// initialized the MultiView with the relevant views themselves.
|
||||
my.MultiView = Backbone.View.extend({
|
||||
template: ' \
|
||||
<div class="recline-data-explorer"> \
|
||||
<div class="alert-messages"></div> \
|
||||
\
|
||||
<div class="header clearfix"> \
|
||||
<div class="navigation"> \
|
||||
<div class="btn-group" data-toggle="buttons-radio"> \
|
||||
{{#views}} \
|
||||
<button href="#{{id}}" data-view="{{id}}" class="btn btn-default" aria-label="{{id}} view">{{label}}</button> \
|
||||
{{/views}} \
|
||||
</div> \
|
||||
</div> \
|
||||
<div class="recline-results-info"> \
|
||||
{{#t.num_records}}<span class="doc-count">{recordCount}</span> {recordCount, plural, =1{record} other{records}}{{/t.num_records}}\
|
||||
</div> \
|
||||
<div class="menu-right"> \
|
||||
<div class="btn-group" data-toggle="buttons-checkbox"> \
|
||||
{{#sidebarViews}} \
|
||||
<button href="#" data-action="{{id}}" class="btn btn-default">{{label}}</button> \
|
||||
{{/sidebarViews}} \
|
||||
</div> \
|
||||
</div> \
|
||||
<div class="query-editor-here" style="display:inline;"></div> \
|
||||
</div> \
|
||||
<div class="data-view-sidebar"></div> \
|
||||
<div class="data-view-container"></div> \
|
||||
</div> \
|
||||
',
|
||||
events: {
|
||||
'click .menu-right button': '_onMenuClick',
|
||||
'click .navigation button': '_onSwitchView'
|
||||
},
|
||||
|
||||
initialize: function(options) {
|
||||
var self = this;
|
||||
this._setupState(options.state);
|
||||
var fmt = I18nMessages('recline', recline.View.translations);
|
||||
|
||||
// Hash of 'page' views (i.e. those for whole page) keyed by page name
|
||||
if (options.views) {
|
||||
this.pageViews = options.views;
|
||||
} else {
|
||||
this.pageViews = [{
|
||||
id: 'grid',
|
||||
label: fmt.t('Grid'),
|
||||
view: new my.SlickGrid({
|
||||
model: this.model,
|
||||
state: this.state.get('view-grid')
|
||||
})
|
||||
}, {
|
||||
id: 'graph',
|
||||
label: fmt.t('Graph'),
|
||||
view: new my.Graph({
|
||||
model: this.model,
|
||||
state: this.state.get('view-graph')
|
||||
})
|
||||
}, {
|
||||
id: 'map',
|
||||
label: fmt.t('Map'),
|
||||
view: new my.Map({
|
||||
model: this.model,
|
||||
state: this.state.get('view-map')
|
||||
})
|
||||
}, {
|
||||
id: 'timeline',
|
||||
label: fmt.t('Timeline'),
|
||||
view: new my.Timeline({
|
||||
model: this.model,
|
||||
state: this.state.get('view-timeline')
|
||||
})
|
||||
}];
|
||||
}
|
||||
// Hashes of sidebar elements
|
||||
if(options.sidebarViews) {
|
||||
this.sidebarViews = options.sidebarViews;
|
||||
} else {
|
||||
this.sidebarViews = [{
|
||||
id: 'filterEditor',
|
||||
label: fmt.t('Filters'),
|
||||
view: new my.FilterEditor({
|
||||
model: this.model
|
||||
})
|
||||
}, {
|
||||
id: 'fieldsView',
|
||||
label: fmt.t('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.setReadOnly();
|
||||
}
|
||||
if (this.state.get('currentView')) {
|
||||
this.updateNav(this.state.get('currentView'));
|
||||
} else {
|
||||
this.updateNav(this.pageViews[0].id);
|
||||
}
|
||||
this._showHideSidebar();
|
||||
|
||||
this.listenTo(this.model, 'query:start', function() {
|
||||
self.notify({loader: true, persist: true});
|
||||
});
|
||||
this.listenTo(this.model, 'query:done', function() {
|
||||
self.clearNotifications();
|
||||
self.$el.find('.doc-count').text(self.model.recordCount || fmt.t('Unknown'));
|
||||
});
|
||||
this.listenTo(this.model, 'query:fail', function(error) {
|
||||
self.clearNotifications();
|
||||
var msg = '';
|
||||
if (typeof(error) == 'string') {
|
||||
msg = error;
|
||||
} else if (typeof(error) == 'object') {
|
||||
if (error.title) {
|
||||
msg = error.title + ': ';
|
||||
}
|
||||
if (error.message) {
|
||||
msg += error.message;
|
||||
}
|
||||
} else {
|
||||
msg = fmt.t('backend_error', {}, 'There was an error querying the backend');
|
||||
}
|
||||
self.notify({message: msg, category: 'error', persist: true});
|
||||
});
|
||||
|
||||
// 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});
|
||||
},
|
||||
|
||||
setReadOnly: function() {
|
||||
this.$el.addClass('recline-read-only');
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var tmplData = this.model.toTemplateJSON();
|
||||
tmplData.views = this.pageViews;
|
||||
tmplData.sidebarViews = this.sidebarViews;
|
||||
tmplData = I18nMessages('recline', recline.View.translations).injectMustache(tmplData);
|
||||
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) {
|
||||
view.view.render();
|
||||
if (view.view.redraw) {
|
||||
view.view.redraw();
|
||||
}
|
||||
$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);
|
||||
}, this);
|
||||
|
||||
this.pager = new recline.View.Pager({
|
||||
model: this.model
|
||||
});
|
||||
this.$el.find('.recline-results-info').after(this.pager.el);
|
||||
|
||||
this.queryEditor = new recline.View.QueryEditor({
|
||||
model: this.model.queryState
|
||||
});
|
||||
this.$el.find('.query-editor-here').append(this.queryEditor.el);
|
||||
|
||||
},
|
||||
|
||||
remove: function () {
|
||||
_.each(this.pageViews, function (view) {
|
||||
view.view.remove();
|
||||
});
|
||||
_.each(this.sidebarViews, function (view) {
|
||||
view.view.remove();
|
||||
});
|
||||
this.pager.remove();
|
||||
this.queryEditor.remove();
|
||||
Backbone.View.prototype.remove.apply(this, arguments);
|
||||
},
|
||||
|
||||
// hide the sidebar if empty
|
||||
_showHideSidebar: function() {
|
||||
var $dataSidebar = this.$el.find('.data-view-sidebar');
|
||||
var visibleChildren = $dataSidebar.children().filter(function() {
|
||||
return $(this).css("display") != "none";
|
||||
}).length;
|
||||
|
||||
if (visibleChildren > 0) {
|
||||
$dataSidebar.show();
|
||||
} else {
|
||||
$dataSidebar.hide();
|
||||
}
|
||||
},
|
||||
|
||||
updateNav: function(pageName) {
|
||||
this.$el.find('.navigation button').removeClass('active');
|
||||
var $el = this.$el.find('.navigation button[data-view="' + pageName + '"]');
|
||||
$el.addClass('active');
|
||||
|
||||
// add/remove sidebars and hide inactive views
|
||||
_.each(this.pageViews, function(view, idx) {
|
||||
if (view.id === pageName) {
|
||||
view.view.$el.show();
|
||||
if (view.view.elSidebar) {
|
||||
view.view.elSidebar.show();
|
||||
}
|
||||
} else {
|
||||
view.view.$el.hide();
|
||||
if (view.view.elSidebar) {
|
||||
view.view.elSidebar.hide();
|
||||
}
|
||||
if (view.view.hide) {
|
||||
view.view.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this._showHideSidebar();
|
||||
|
||||
// call view.view.show after sidebar visibility has been determined so
|
||||
// that views can correctly calculate their maximum width
|
||||
_.each(this.pageViews, function(view, idx) {
|
||||
if (view.id === pageName) {
|
||||
if (view.view.show) {
|
||||
view.view.show();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_onMenuClick: function(e) {
|
||||
e.preventDefault();
|
||||
var action = $(e.target).attr('data-action');
|
||||
this['$'+action].toggle();
|
||||
this._showHideSidebar();
|
||||
},
|
||||
|
||||
_onSwitchView: function(e) {
|
||||
e.preventDefault();
|
||||
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
|
||||
//
|
||||
// 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 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,
|
||||
'view-graph': graphState,
|
||||
backend: this.model.backend.__type__,
|
||||
url: this.model.get('url'),
|
||||
dataset: this.model.toJSON(),
|
||||
currentView: null,
|
||||
readOnly: false
|
||||
},
|
||||
initialState);
|
||||
this.state = new recline.Model.ObjectState(stateData);
|
||||
},
|
||||
|
||||
_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.listenTo(this.model.queryState, 'change', function() {
|
||||
self.state.set({query: self.model.queryState.toJSON()});
|
||||
});
|
||||
_.each(this.pageViews, function(pageView) {
|
||||
if (pageView.view.state && pageView.view.state.bind) {
|
||||
var update = {};
|
||||
update['view-' + pageView.id] = pageView.view.state.toJSON();
|
||||
self.state.set(update);
|
||||
self.listenTo(pageView.view.state, '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});
|
||||
self.state.trigger('change');
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_bindFlashNotifications: function() {
|
||||
var self = this;
|
||||
_.each(this.pageViews, function(pageView) {
|
||||
self.listenTo(pageView.view, 'recline:flash', function(flash) {
|
||||
self.notify(flash);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// ### notify
|
||||
//
|
||||
// Create a notification (a div.alert in div.alert-messsages) using provided
|
||||
// flash object. Flash attributes (all are optional):
|
||||
//
|
||||
// * message: message to show.
|
||||
// * category: warning (default), success, error
|
||||
// * persist: if true alert is persistent, o/w hidden after 3s (default = false)
|
||||
// * loader: if true show loading spinner
|
||||
notify: function(flash) {
|
||||
var tmplData = _.extend({
|
||||
message: 'Loading',
|
||||
category: 'warning',
|
||||
loader: false
|
||||
},
|
||||
flash
|
||||
);
|
||||
var _template;
|
||||
if (tmplData.loader) {
|
||||
_template = ' \
|
||||
<div class="alert alert-info alert-loader"> \
|
||||
{{message}} \
|
||||
<span class="notification-loader"> </span> \
|
||||
</div>';
|
||||
} else {
|
||||
_template = ' \
|
||||
<div class="alert alert-{{category}} fade in" data-alert="alert"><a class="close" data-dismiss="alert" href="#">×</a> \
|
||||
{{message}} \
|
||||
</div>';
|
||||
}
|
||||
var _templated = $(Mustache.render(_template, tmplData));
|
||||
_templated = $(_templated).appendTo($('.recline-data-explorer .alert-messages'));
|
||||
if (!flash.persist) {
|
||||
setTimeout(function() {
|
||||
$(_templated).fadeOut(1000, function() {
|
||||
$(this).remove();
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
},
|
||||
|
||||
// ### clearNotifications
|
||||
//
|
||||
// Clear all existing notifications
|
||||
clearNotifications: function() {
|
||||
var $notifications = $('.recline-data-explorer .alert-messages .alert');
|
||||
$notifications.fadeOut(1500, function() {
|
||||
$(this).remove();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ### MultiView.restore
|
||||
//
|
||||
// Restore a MultiView instance from a serialized state including the associated dataset
|
||||
//
|
||||
// This inverts the state serialization process in Multiview
|
||||
my.MultiView.restore = function(state) {
|
||||
// hack-y - restoring a memory dataset does not mean much ... (but useful for testing!)
|
||||
var datasetInfo;
|
||||
if (state.backend === 'memory') {
|
||||
datasetInfo = {
|
||||
backend: 'memory',
|
||||
records: [{stub: 'this is a stub dataset because we do not restore memory datasets'}]
|
||||
};
|
||||
} else {
|
||||
datasetInfo = _.extend({
|
||||
url: state.url,
|
||||
backend: state.backend
|
||||
},
|
||||
state.dataset
|
||||
);
|
||||
}
|
||||
var dataset = new recline.Model.Dataset(datasetInfo);
|
||||
var explorer = new my.MultiView({
|
||||
model: dataset,
|
||||
state: state
|
||||
});
|
||||
return explorer;
|
||||
};
|
||||
|
||||
// ## Miscellaneous Utilities
|
||||
var urlPathRegex = /^([^?]+)(\?.*)?/;
|
||||
|
||||
// Parse the Hash section of a URL into path and query string
|
||||
my.parseHashUrl = function(hashUrl) {
|
||||
var parsed = urlPathRegex.exec(hashUrl);
|
||||
if (parsed === null) {
|
||||
return {};
|
||||
} else {
|
||||
return {
|
||||
path: parsed[1],
|
||||
query: parsed[2] || ''
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Parse a URL query string (?xyz=abc...) into a dictionary.
|
||||
my.parseQueryString = function(q) {
|
||||
if (!q) {
|
||||
return {};
|
||||
}
|
||||
var urlParams = {},
|
||||
e, d = function (s) {
|
||||
return unescape(s.replace(/\+/g, " "));
|
||||
},
|
||||
r = /([^&=]+)=?([^&]*)/g;
|
||||
|
||||
if (q && q.length && q[0] === '?') {
|
||||
q = q.slice(1);
|
||||
}
|
||||
while (e = r.exec(q)) {
|
||||
// TODO: have values be array as query string allow repetition of keys
|
||||
urlParams[d(e[1])] = d(e[2]);
|
||||
}
|
||||
return urlParams;
|
||||
};
|
||||
|
||||
// Parse the query string out of the URL hash
|
||||
my.parseHashQueryString = function() {
|
||||
var q = my.parseHashUrl(window.location.hash).query;
|
||||
return my.parseQueryString(q);
|
||||
};
|
||||
|
||||
// Compse a Query String
|
||||
my.composeQueryString = function(queryParams) {
|
||||
var queryString = '?';
|
||||
var items = [];
|
||||
$.each(queryParams, function(key, value) {
|
||||
if (typeof(value) === 'object') {
|
||||
value = JSON.stringify(value);
|
||||
}
|
||||
items.push(key + '=' + encodeURIComponent(value));
|
||||
});
|
||||
queryString += items.join('&');
|
||||
return queryString;
|
||||
};
|
||||
|
||||
my.getNewHashForQueryString = function(queryParams) {
|
||||
var queryPart = my.composeQueryString(queryParams);
|
||||
if (window.location.hash) {
|
||||
// slice(1) to remove # at start
|
||||
return window.location.hash.split('?')[0].slice(1) + queryPart;
|
||||
} else {
|
||||
return queryPart;
|
||||
}
|
||||
};
|
||||
|
||||
my.setHashQueryString = function(queryParams) {
|
||||
window.location.hash = my.getNewHashForQueryString(queryParams);
|
||||
};
|
||||
|
||||
})(jQuery, recline.View);
|
||||
|
||||
@@ -1,602 +0,0 @@
|
||||
/*jshint multistr:true */
|
||||
|
||||
this.recline = this.recline || {};
|
||||
this.recline.View = this.recline.View || {};
|
||||
|
||||
(function($, my) {
|
||||
"use strict";
|
||||
|
||||
// ## SlickGrid Dataset View
|
||||
//
|
||||
// Provides a tabular view on a Dataset, based on SlickGrid.
|
||||
//
|
||||
// https://github.com/mleibman/SlickGrid
|
||||
//
|
||||
// Initialize it with a `recline.Model.Dataset`.
|
||||
//
|
||||
// Additional options to drive SlickGrid grid can be given through state.
|
||||
// The following keys allow for customization:
|
||||
// * gridOptions: to add options at grid level
|
||||
// * columnsEditor: to add editor for editable columns
|
||||
//
|
||||
// For example:
|
||||
// var grid = new recline.View.SlickGrid({
|
||||
// model: dataset,
|
||||
// el: $el,
|
||||
// state: {
|
||||
// gridOptions: {
|
||||
// editable: true,
|
||||
// enableAddRow: true
|
||||
// // Enable support for row delete
|
||||
// enabledDelRow: true,
|
||||
// // Enable support for row Reorder
|
||||
// enableReOrderRow:true,
|
||||
// ...
|
||||
// },
|
||||
// columnsEditor: [
|
||||
// {column: 'date', editor: Slick.Editors.Date },
|
||||
// {column: 'title', editor: Slick.Editors.Text}
|
||||
// ]
|
||||
// }
|
||||
// });
|
||||
//// NB: you need an explicit height on the element for slickgrid to work
|
||||
my.SlickGrid = Backbone.View.extend({
|
||||
initialize: function(modelEtc) {
|
||||
var self = this;
|
||||
this.$el.addClass('recline-slickgrid');
|
||||
|
||||
// Template for row delete menu , change it if you don't love
|
||||
this.templates = {
|
||||
deleterow : '<button href="#" class="recline-row-delete btn btn-default" title="{{t.Delete_row}}"><span class="wcag_hide">{{t.Delete_row}}</span><span aria-hidden="true">X</span></button>',
|
||||
reorderrows: '<div title="{{t.Reorder_row}}" style="height: 100%;"><span class="wcag_hide">{{t.Reorder_row}}</span></div>'
|
||||
};
|
||||
|
||||
_.bindAll(this, 'render', 'onRecordChanged');
|
||||
this.listenTo(this.model.records, 'add remove reset', this.render);
|
||||
this.listenTo(this.model.records, 'change', this.onRecordChanged);
|
||||
var state = _.extend({
|
||||
hiddenColumns: [],
|
||||
columnsOrder: [],
|
||||
columnsSort: {},
|
||||
columnsWidth: [],
|
||||
columnsEditor: [],
|
||||
options: {},
|
||||
fitColumns: false
|
||||
}, modelEtc.state
|
||||
|
||||
);
|
||||
this.state = new recline.Model.ObjectState(state);
|
||||
this._slickHandler = new Slick.EventHandler();
|
||||
|
||||
//add menu for new row , check if enableAddRow is set to true or not set
|
||||
if(this.state.get("gridOptions")
|
||||
&& this.state.get("gridOptions").enabledAddRow != undefined
|
||||
&& this.state.get("gridOptions").enabledAddRow == true ){
|
||||
this.editor = new my.GridControl()
|
||||
this.elSidebar = this.editor.$el
|
||||
this.listenTo(this.editor.state, 'change', function(){
|
||||
this.model.records.add(new recline.Model.Record())
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onRecordChanged: function(record) {
|
||||
// Ignore if the grid is not yet drawn
|
||||
if (!this.grid) {
|
||||
return;
|
||||
}
|
||||
// Let's find the row corresponding to the index
|
||||
var row_index = this.grid.getData().getModelRow( record );
|
||||
this.grid.invalidateRow(row_index);
|
||||
this.grid.getData().updateItem(record, row_index);
|
||||
this.grid.render();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var self = this;
|
||||
var options = _.extend({
|
||||
enableCellNavigation: true,
|
||||
enableColumnReorder: true,
|
||||
explicitInitialization: true,
|
||||
syncColumnCellResize: true,
|
||||
forceFitColumns: this.state.get('fitColumns')
|
||||
}, self.state.get('gridOptions'));
|
||||
|
||||
// We need all columns, even the hidden ones, to show on the column picker
|
||||
var columns = [];
|
||||
var fmt = I18nMessages('recline', recline.View.translations);
|
||||
|
||||
// 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) {
|
||||
if(columnDef.id == "del"){
|
||||
var formatted = Mustache.render(self.templates.deleterow, fmt.injectMustache({}));
|
||||
return formatted;
|
||||
}
|
||||
if(columnDef.id == "#"){
|
||||
var formatted = Mustache.render(self.templates.reorderrows, fmt.injectMustache({}));
|
||||
return formatted;
|
||||
}
|
||||
|
||||
var field = self.model.fields.get(columnDef.id);
|
||||
if (field.renderer) {
|
||||
return field.renderer(value, field, dataContext);
|
||||
} else {
|
||||
return value
|
||||
}
|
||||
};
|
||||
|
||||
// we need to be sure that user is entering a valid input , for exemple if
|
||||
// field is date type and field.format ='YY-MM-DD', we should be sure that
|
||||
// user enter a correct value
|
||||
var validator = function(field) {
|
||||
return function(value){
|
||||
if (field.type == "date" && isNaN(Date.parse(value))){
|
||||
return {
|
||||
valid: false,
|
||||
msg: fmt.t('date_required', {}, "A date is required, check field field-date-format")
|
||||
};
|
||||
} else {
|
||||
return {valid: true, msg :null }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Add column for row reorder support
|
||||
if (this.state.get("gridOptions") && this.state.get("gridOptions").enableReOrderRow == true) {
|
||||
columns.push({
|
||||
id: "#",
|
||||
name: "",
|
||||
width: 22,
|
||||
behavior: "selectAndMove",
|
||||
selectable: false,
|
||||
resizable: false,
|
||||
cssClass: "recline-cell-reorder",
|
||||
formatter: formatter
|
||||
})
|
||||
}
|
||||
// Add column for row delete support
|
||||
if (this.state.get("gridOptions") && this.state.get("gridOptions").enabledDelRow == true) {
|
||||
columns.push({
|
||||
id: 'del',
|
||||
name: '',
|
||||
field: 'del',
|
||||
sortable: true,
|
||||
width: 38,
|
||||
formatter: formatter,
|
||||
validator:validator
|
||||
})
|
||||
}
|
||||
|
||||
function sanitizeFieldName(name) {
|
||||
return $('<div>').text(name).html();
|
||||
}
|
||||
|
||||
_.each(this.model.fields.toJSON(),function(field){
|
||||
var column = {
|
||||
id: field.id,
|
||||
name: sanitizeFieldName(field.label),
|
||||
field: field.id,
|
||||
sortable: true,
|
||||
minWidth: 80,
|
||||
formatter: formatter,
|
||||
validator:validator(field)
|
||||
};
|
||||
var widthInfo = _.find(self.state.get('columnsWidth'),function(c){return c.column === field.id;});
|
||||
if (widthInfo){
|
||||
column.width = widthInfo.width;
|
||||
}
|
||||
var editInfo = _.find(self.state.get('columnsEditor'),function(c){return c.column === field.id;});
|
||||
if (editInfo){
|
||||
column.editor = editInfo.editor;
|
||||
} else {
|
||||
// guess editor type
|
||||
var typeToEditorMap = {
|
||||
'string': Slick.Editors.LongText,
|
||||
'integer': Slick.Editors.IntegerEditor,
|
||||
'number': Slick.Editors.Text,
|
||||
// TODO: need a way to ensure we format date in the right way
|
||||
// Plus what if dates are in distant past or future ... (?)
|
||||
// 'date': Slick.Editors.DateEditor,
|
||||
'date': Slick.Editors.Text,
|
||||
'boolean': Slick.Editors.YesNoSelectEditor
|
||||
// TODO: (?) percent ...
|
||||
};
|
||||
if (field.type in typeToEditorMap) {
|
||||
column.editor = typeToEditorMap[field.type]
|
||||
} else {
|
||||
column.editor = Slick.Editors.LongText;
|
||||
}
|
||||
}
|
||||
columns.push(column);
|
||||
});
|
||||
// Restrict the visible columns
|
||||
var visibleColumns = _.filter(columns, 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') && this.state.get('columnsOrder').length > 0) {
|
||||
visibleColumns = visibleColumns.sort(function(a,b){
|
||||
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) ? 1 : -1;
|
||||
});
|
||||
}
|
||||
|
||||
// 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){
|
||||
tempHiddenColumns.push(columns.splice(i,1)[0]);
|
||||
}
|
||||
}
|
||||
columns = columns.concat(tempHiddenColumns);
|
||||
|
||||
// Transform a model object into a row
|
||||
function toRow(m) {
|
||||
var row = {};
|
||||
self.model.fields.each(function(field) {
|
||||
var render = "";
|
||||
//when adding row from slickgrid the field value is undefined
|
||||
if(!_.isUndefined(m.getFieldValueUnrendered(field))){
|
||||
render =m.getFieldValueUnrendered(field)
|
||||
}
|
||||
row[field.id] = render
|
||||
});
|
||||
return row;
|
||||
}
|
||||
|
||||
function RowSet() {
|
||||
var models = [];
|
||||
var rows = [];
|
||||
|
||||
this.push = function(model, row) {
|
||||
models.push(model);
|
||||
rows.push(row);
|
||||
};
|
||||
|
||||
this.getLength = function() {return rows.length; };
|
||||
this.getItem = function(index) {return rows[index];};
|
||||
this.getItemMetadata = function(index) {return {};};
|
||||
this.getModel = function(index) {return models[index];};
|
||||
this.getModelRow = function(m) {return _.indexOf(models, m);};
|
||||
this.updateItem = function(m,i) {
|
||||
rows[i] = toRow(m);
|
||||
models[i] = m;
|
||||
};
|
||||
}
|
||||
|
||||
var data = new RowSet();
|
||||
|
||||
this.model.records.each(function(doc){
|
||||
data.push(doc, toRow(doc));
|
||||
});
|
||||
|
||||
this.grid = new Slick.Grid(this.el, data, visibleColumns, options);
|
||||
// Column sorting
|
||||
var sortInfo = this.model.queryState.get('sort');
|
||||
if (sortInfo){
|
||||
var column = sortInfo[0].field;
|
||||
var sortAsc = sortInfo[0].order !== 'desc';
|
||||
this.grid.setSortColumn(column, sortAsc);
|
||||
}
|
||||
|
||||
if (this.state.get("gridOptions") && this.state.get("gridOptions").enableReOrderRow) {
|
||||
this._setupRowReordering();
|
||||
}
|
||||
|
||||
this._slickHandler.subscribe(this.grid.onSort, function(e, args){
|
||||
var order = (args.sortAsc) ? 'asc':'desc';
|
||||
var sort = [{
|
||||
field: args.sortCol.field,
|
||||
order: order
|
||||
}];
|
||||
self.model.query({sort: sort});
|
||||
});
|
||||
|
||||
this._slickHandler.subscribe(this.grid.onColumnsReordered, function(e, args){
|
||||
self.state.set({columnsOrder: _.pluck(self.grid.getColumns(),'id')});
|
||||
});
|
||||
|
||||
this.grid.onColumnsResized.subscribe(function(e, args){
|
||||
var columns = args.grid.getColumns();
|
||||
var defaultColumnWidth = args.grid.getOptions().defaultColumnWidth;
|
||||
var columnsWidth = [];
|
||||
_.each(columns,function(column){
|
||||
if (column.width != defaultColumnWidth){
|
||||
columnsWidth.push({column:column.id,width:column.width});
|
||||
}
|
||||
});
|
||||
self.state.set({columnsWidth:columnsWidth});
|
||||
});
|
||||
|
||||
this._slickHandler.subscribe(this.grid.onCellChange, function (e, args) {
|
||||
// We need to change the model associated value
|
||||
var grid = args.grid;
|
||||
var model = data.getModel(args.row);
|
||||
var field = grid.getColumns()[args.cell].id;
|
||||
var v = {};
|
||||
v[field] = args.item[field];
|
||||
model.set(v);
|
||||
});
|
||||
this._slickHandler.subscribe(this.grid.onClick,function(e, args){
|
||||
//try catch , because this fail in qunit , but no
|
||||
//error on browser.
|
||||
try{e.preventDefault()}catch(e){}
|
||||
|
||||
// The cell of grid that handle row delete is The first cell (0) if
|
||||
// The grid ReOrder is not present ie enableReOrderRow == false
|
||||
// else it is The the second cell (1) , because The 0 is now cell
|
||||
// that handle row Reoder.
|
||||
var cell =0
|
||||
if(self.state.get("gridOptions")
|
||||
&& self.state.get("gridOptions").enableReOrderRow != undefined
|
||||
&& self.state.get("gridOptions").enableReOrderRow == true ){
|
||||
cell =1
|
||||
}
|
||||
if (args.cell == cell && self.state.get("gridOptions").enabledDelRow == true){
|
||||
// We need to delete the associated model
|
||||
var model = data.getModel(args.row);
|
||||
model.destroy()
|
||||
}
|
||||
}) ;
|
||||
var columnpicker = new Slick.Controls.ColumnPicker(columns, this.grid,
|
||||
_.extend(options,{state:this.state}));
|
||||
if (self.visible){
|
||||
self.grid.init();
|
||||
self.rendered = true;
|
||||
} else {
|
||||
// Defer rendering until the view is visible
|
||||
self.rendered = false;
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
// Row reordering support based on
|
||||
// https://github.com/mleibman/SlickGrid/blob/gh-pages/examples/example9-row-reordering.html
|
||||
_setupRowReordering: function() {
|
||||
var self = this;
|
||||
self.grid.setSelectionModel(new Slick.RowSelectionModel());
|
||||
|
||||
var moveRowsPlugin = new Slick.RowMoveManager({
|
||||
cancelEditOnDrag: true
|
||||
});
|
||||
|
||||
moveRowsPlugin.onBeforeMoveRows.subscribe(function (e, data) {
|
||||
for (var i = 0; i < data.rows.length; i++) {
|
||||
// no point in moving before or after itself
|
||||
if (data.rows[i] == data.insertBefore || data.rows[i] == data.insertBefore - 1) {
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
moveRowsPlugin.onMoveRows.subscribe(function (e, args) {
|
||||
var extractedRows = [], left, right;
|
||||
var rows = args.rows;
|
||||
var insertBefore = args.insertBefore;
|
||||
|
||||
var data = self.model.records.toJSON()
|
||||
left = data.slice(0, insertBefore);
|
||||
right= data.slice(insertBefore, data.length);
|
||||
|
||||
rows.sort(function(a,b) { return a-b; });
|
||||
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
extractedRows.push(data[rows[i]]);
|
||||
}
|
||||
|
||||
rows.reverse();
|
||||
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
var row = rows[i];
|
||||
if (row < insertBefore) {
|
||||
left.splice(row, 1);
|
||||
} else {
|
||||
right.splice(row - insertBefore, 1);
|
||||
}
|
||||
}
|
||||
|
||||
data = left.concat(extractedRows.concat(right));
|
||||
var selectedRows = [];
|
||||
for (var i = 0; i < rows.length; i++)
|
||||
selectedRows.push(left.length + i);
|
||||
|
||||
self.model.records.reset(data)
|
||||
|
||||
});
|
||||
//register The plugin to handle row Reorder
|
||||
if(this.state.get("gridOptions") && this.state.get("gridOptions").enableReOrderRow) {
|
||||
self.grid.registerPlugin(moveRowsPlugin);
|
||||
}
|
||||
},
|
||||
|
||||
remove: function () {
|
||||
this._slickHandler.unsubscribeAll();
|
||||
Backbone.View.prototype.remove.apply(this, arguments);
|
||||
},
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
// Add new grid Control to display a new row add menu bouton
|
||||
// It display a simple side-bar menu ,for user to add new
|
||||
// row to grid
|
||||
my.GridControl= Backbone.View.extend({
|
||||
className: "recline-row-add",
|
||||
// Template for row edit menu , change it if you don't love
|
||||
template: '<h1><button href="#" class="recline-row-add btn btn-default">{{t.Add_row}}</button></h1>',
|
||||
|
||||
initialize: function(options){
|
||||
var self = this;
|
||||
_.bindAll(this, 'render');
|
||||
this.state = new recline.Model.ObjectState();
|
||||
this.render();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var self = this;
|
||||
|
||||
var tmplData = I18nMessages('recline', recline.View.translations).injectMustache({});
|
||||
var formatted = Mustache.render(this.template, tmplData);
|
||||
this.$el.html(formatted);
|
||||
},
|
||||
|
||||
events : {
|
||||
"click .recline-row-add" : "addNewRow"
|
||||
},
|
||||
|
||||
addNewRow : function(e){
|
||||
e.preventDefault()
|
||||
this.state.trigger("change")
|
||||
}
|
||||
});
|
||||
|
||||
})(jQuery, recline.View);
|
||||
|
||||
/*
|
||||
* Context menu for the column picker, adapted from
|
||||
* http://mleibman.github.com/SlickGrid/examples/example-grouping
|
||||
*
|
||||
*/
|
||||
(function ($) {
|
||||
function SlickColumnPicker(columns, grid, options) {
|
||||
var $menu;
|
||||
var columnCheckboxes;
|
||||
|
||||
var defaults = {
|
||||
fadeSpeed:250
|
||||
};
|
||||
|
||||
function init() {
|
||||
grid.onHeaderContextMenu.subscribe(handleHeaderContextMenu);
|
||||
options = $.extend({}, defaults, options);
|
||||
|
||||
$menu = $('<ul class="dropdown-menu slick-contextmenu" style="display:none;position:absolute;z-index:20;" />').appendTo(document.body);
|
||||
|
||||
$menu.bind('mouseleave', function (e) {
|
||||
$(this).fadeOut(options.fadeSpeed);
|
||||
});
|
||||
$menu.bind('click', updateColumn);
|
||||
|
||||
}
|
||||
|
||||
function handleHeaderContextMenu(e, args) {
|
||||
e.preventDefault();
|
||||
$menu.empty();
|
||||
columnCheckboxes = [];
|
||||
|
||||
var $li, $input;
|
||||
for (var i = 0; i < columns.length; i++) {
|
||||
$li = $('<li />').appendTo($menu);
|
||||
$input = $('<input type="checkbox" />').data('column-id', columns[i].id).attr('id','slick-column-vis-'+columns[i].id);
|
||||
columnCheckboxes.push($input);
|
||||
|
||||
if (grid.getColumnIndex(columns[i].id) !== null) {
|
||||
$input.attr('checked', 'checked');
|
||||
}
|
||||
$input.appendTo($li);
|
||||
$('<label />')
|
||||
.text(columns[i].name)
|
||||
.attr('for','slick-column-vis-'+columns[i].id)
|
||||
.appendTo($li);
|
||||
}
|
||||
$('<li/>').addClass('divider').appendTo($menu);
|
||||
$li = $('<li />').data('option', 'autoresize').appendTo($menu);
|
||||
$input = $('<input type="checkbox" />').data('option', 'autoresize').attr('id','slick-option-autoresize');
|
||||
$input.appendTo($li);
|
||||
$('<label />')
|
||||
.text(this.t('Force_fit_columns'))
|
||||
.attr('for','slick-option-autoresize')
|
||||
.appendTo($li);
|
||||
if (grid.getOptions().forceFitColumns) {
|
||||
$input.attr('checked', 'checked');
|
||||
}
|
||||
|
||||
$menu.css('top', e.pageY - 10)
|
||||
.css('left', e.pageX - 10)
|
||||
.fadeIn(options.fadeSpeed);
|
||||
}
|
||||
|
||||
function updateColumn(e) {
|
||||
var checkbox;
|
||||
|
||||
if ($(e.target).data('option') === 'autoresize') {
|
||||
var checked;
|
||||
if ($(e.target).is('li')){
|
||||
checkbox = $(e.target).find('input').first();
|
||||
checked = !checkbox.is(':checked');
|
||||
checkbox.attr('checked',checked);
|
||||
} else {
|
||||
checked = e.target.checked;
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
grid.setOptions({forceFitColumns:true});
|
||||
grid.autosizeColumns();
|
||||
} else {
|
||||
grid.setOptions({forceFitColumns:false});
|
||||
}
|
||||
options.state.set({fitColumns:checked});
|
||||
return;
|
||||
}
|
||||
|
||||
if (($(e.target).is('li') && !$(e.target).hasClass('divider')) ||
|
||||
$(e.target).is('input')) {
|
||||
if ($(e.target).is('li')){
|
||||
checkbox = $(e.target).find('input').first();
|
||||
checkbox.attr('checked',!checkbox.is(':checked'));
|
||||
}
|
||||
var visibleColumns = [];
|
||||
var hiddenColumnsIds = [];
|
||||
$.each(columnCheckboxes, function (i, e) {
|
||||
if ($(this).is(':checked')) {
|
||||
visibleColumns.push(columns[i]);
|
||||
} else {
|
||||
hiddenColumnsIds.push(columns[i].id);
|
||||
}
|
||||
});
|
||||
|
||||
if (!visibleColumns.length) {
|
||||
$(e.target).attr('checked', 'checked');
|
||||
return;
|
||||
}
|
||||
|
||||
grid.setColumns(visibleColumns);
|
||||
options.state.set({hiddenColumns:hiddenColumnsIds});
|
||||
}
|
||||
}
|
||||
init();
|
||||
}
|
||||
|
||||
// Slick.Controls.ColumnPicker
|
||||
$.extend(true, window, {
|
||||
Slick: {
|
||||
Controls: {
|
||||
ColumnPicker: SlickColumnPicker
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
})(jQuery);
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
/*jshint multistr:true */
|
||||
|
||||
this.recline = this.recline || {};
|
||||
this.recline.View = this.recline.View || {};
|
||||
|
||||
(function($, my) {
|
||||
"use strict";
|
||||
// turn off unnecessary logging from VMM Timeline
|
||||
if (typeof VMM !== 'undefined') {
|
||||
VMM.debug = false;
|
||||
}
|
||||
|
||||
// ## Timeline
|
||||
//
|
||||
// Timeline view using http://timeline.verite.co/
|
||||
my.Timeline = Backbone.View.extend({
|
||||
template: ' \
|
||||
<div class="recline-timeline"> \
|
||||
<div id="vmm-timeline-id"></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 these fields on initialization
|
||||
startFieldNames: ['date','startdate', 'start', 'start-date'],
|
||||
endFieldNames: ['end','endDate'],
|
||||
elementId: '#vmm-timeline-id',
|
||||
|
||||
initialize: function(options) {
|
||||
var self = this;
|
||||
this.timeline = new VMM.Timeline(this.elementId);
|
||||
this._timelineIsInitialized = false;
|
||||
this.listenTo(this.model.fields, 'reset', function() {
|
||||
self._setupTemporalField();
|
||||
});
|
||||
this.listenTo(this.model.records, 'all', function() {
|
||||
self.reloadData();
|
||||
});
|
||||
var stateData = _.extend({
|
||||
startField: null,
|
||||
endField: null,
|
||||
// by default timelinejs (and browsers) will parse ambiguous dates in US format (mm/dd/yyyy)
|
||||
// set to true to interpret dd/dd/dddd as dd/mm/yyyy
|
||||
nonUSDates: false,
|
||||
timelineJSOptions: {}
|
||||
},
|
||||
options.state
|
||||
);
|
||||
this.state = new recline.Model.ObjectState(stateData);
|
||||
this._setupTemporalField();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var tmplData = {};
|
||||
var htmls = Mustache.render(this.template, tmplData);
|
||||
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() {
|
||||
var data = this._timelineJSON();
|
||||
var config = this.state.get("timelineJSOptions");
|
||||
config.id = this.elementId;
|
||||
this.timeline.init(config, data);
|
||||
this._timelineIsInitialized = true
|
||||
},
|
||||
|
||||
reloadData: function() {
|
||||
if (this._timelineIsInitialized) {
|
||||
var data = this._timelineJSON();
|
||||
this.timeline.reload(data);
|
||||
}
|
||||
},
|
||||
|
||||
// Convert record to JSON for timeline
|
||||
//
|
||||
// Designed to be overridden in client apps
|
||||
convertRecord: function(record, fields) {
|
||||
return this._convertRecord(record, fields);
|
||||
},
|
||||
|
||||
// Internal method to generate a Timeline formatted entry
|
||||
_convertRecord: function(record, fields) {
|
||||
var start = this._parseDate(record.get(this.state.get('startField')));
|
||||
var end = this._parseDate(record.get(this.state.get('endField')));
|
||||
if (start) {
|
||||
var tlEntry = {
|
||||
"startDate": start,
|
||||
"endDate": end,
|
||||
"headline": String(record.get('title') || ''),
|
||||
"text": record.get('description') || record.summary(),
|
||||
"tag": record.get('tags')
|
||||
};
|
||||
return tlEntry;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
_timelineJSON: function() {
|
||||
var self = this;
|
||||
var out = {
|
||||
'timeline': {
|
||||
'type': 'default',
|
||||
'headline': '',
|
||||
'date': [
|
||||
]
|
||||
}
|
||||
};
|
||||
this.model.records.each(function(record) {
|
||||
var newEntry = self.convertRecord(record, self.fields);
|
||||
if (newEntry) {
|
||||
out.timeline.date.push(newEntry);
|
||||
}
|
||||
});
|
||||
// if no entries create a placeholder entry to prevent Timeline crashing with error
|
||||
if (out.timeline.date.length === 0) {
|
||||
var tlEntry = {
|
||||
"startDate": '2000,1,1',
|
||||
"headline": 'No data to show!'
|
||||
};
|
||||
out.timeline.date.push(tlEntry);
|
||||
}
|
||||
return out;
|
||||
},
|
||||
|
||||
// convert dates into a format TimelineJS will handle
|
||||
// TimelineJS does not document this at all so combo of read the code +
|
||||
// trial and error
|
||||
// Summary (AFAICt):
|
||||
// Preferred: [-]yyyy[,mm,dd,hh,mm,ss]
|
||||
// Supported: mm/dd/yyyy
|
||||
_parseDate: function(date) {
|
||||
if (!date) {
|
||||
return null;
|
||||
}
|
||||
var out = $.trim(date);
|
||||
out = out.replace(/(\d)th/g, '$1');
|
||||
out = out.replace(/(\d)st/g, '$1');
|
||||
out = $.trim(out);
|
||||
if (out.match(/\d\d\d\d-\d\d-\d\d(T.*)?/)) {
|
||||
out = out.replace(/-/g, ',').replace('T', ',').replace(':',',');
|
||||
}
|
||||
if (out.match(/\d\d-\d\d-\d\d.*/)) {
|
||||
out = out.replace(/-/g, '/');
|
||||
}
|
||||
if (this.state.get('nonUSDates')) {
|
||||
var parts = out.match(/(\d\d)\/(\d\d)\/(\d\d.*)/);
|
||||
if (parts) {
|
||||
out = [parts[2], parts[1], parts[3]].join('/');
|
||||
}
|
||||
}
|
||||
return out;
|
||||
},
|
||||
|
||||
_setupTemporalField: function() {
|
||||
this.state.set({
|
||||
startField: this._checkField(this.startFieldNames),
|
||||
endField: this._checkField(this.endFieldNames)
|
||||
});
|
||||
},
|
||||
|
||||
_checkField: function(possibleFieldNames) {
|
||||
var modelFieldNames = this.model.fields.pluck('id');
|
||||
for (var i = 0; i < possibleFieldNames.length; i++){
|
||||
for (var j = 0; j < modelFieldNames.length; j++){
|
||||
if (modelFieldNames[j].toLowerCase() == possibleFieldNames[i].toLowerCase())
|
||||
return modelFieldNames[j];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
})(jQuery, recline.View);
|
||||
@@ -1,88 +0,0 @@
|
||||
/*jshint multistr:true */
|
||||
|
||||
this.recline = this.recline || {};
|
||||
this.recline.View = this.recline.View || {};
|
||||
|
||||
(function($, my) {
|
||||
"use strict";
|
||||
|
||||
// ## FacetViewer
|
||||
//
|
||||
// Widget for displaying facets
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// var viewer = new FacetViewer({
|
||||
// model: dataset
|
||||
// });
|
||||
my.FacetViewer = Backbone.View.extend({
|
||||
className: 'recline-facet-viewer',
|
||||
template: ' \
|
||||
<div class="facets"> \
|
||||
{{#facets}} \
|
||||
<div class="facet-summary" data-facet="{{id}}"> \
|
||||
<h3> \
|
||||
{{id}} \
|
||||
</h3> \
|
||||
<ul class="facet-items"> \
|
||||
{{#terms}} \
|
||||
<li><a class="facet-choice js-facet-filter" data-value="{{term}}" href="#{{term}}">{{term}} ({{count}})</a></li> \
|
||||
{{/terms}} \
|
||||
{{#entries}} \
|
||||
<li><a class="facet-choice js-facet-filter" data-value="{{time}}">{{term}} ({{count}})</a></li> \
|
||||
{{/entries}} \
|
||||
</ul> \
|
||||
</div> \
|
||||
{{/facets}} \
|
||||
</div> \
|
||||
',
|
||||
|
||||
events: {
|
||||
'click .js-facet-filter': 'onFacetFilter'
|
||||
},
|
||||
initialize: function(model) {
|
||||
_.bindAll(this, 'render');
|
||||
this.listenTo(this.model.facets, 'all', this.render);
|
||||
this.listenTo(this.model.fields, 'all', this.render);
|
||||
this.render();
|
||||
},
|
||||
render: function() {
|
||||
var tmplData = {
|
||||
fields: this.model.fields.toJSON()
|
||||
};
|
||||
tmplData.facets = _.map(this.model.facets.toJSON(), function(facet) {
|
||||
if (facet._type === 'date_histogram') {
|
||||
facet.entries = _.map(facet.entries, function(entry) {
|
||||
entry.term = new Date(entry.time).toDateString();
|
||||
return entry;
|
||||
});
|
||||
}
|
||||
return facet;
|
||||
});
|
||||
var templated = Mustache.render(this.template, tmplData);
|
||||
this.$el.html(templated);
|
||||
// are there actually any facets to show?
|
||||
if (this.model.facets.length > 0) {
|
||||
this.$el.show();
|
||||
} else {
|
||||
this.$el.hide();
|
||||
}
|
||||
},
|
||||
onHide: function(e) {
|
||||
e.preventDefault();
|
||||
this.$el.hide();
|
||||
},
|
||||
onFacetFilter: function(e) {
|
||||
e.preventDefault();
|
||||
var $target= $(e.target);
|
||||
var fieldId = $target.closest('.facet-summary').attr('data-facet');
|
||||
var value = $target.attr('data-value');
|
||||
this.model.queryState.addFilter({type: 'term', field: fieldId, term: value});
|
||||
// have to trigger explicitly for some reason
|
||||
this.model.query();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
})(jQuery, recline.View);
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
/*jshint multistr:true */
|
||||
|
||||
// Field Info
|
||||
//
|
||||
// For each field
|
||||
//
|
||||
// Id / Label / type / format
|
||||
|
||||
// Editor -- to change type (and possibly format)
|
||||
// Editor for show/hide ...
|
||||
|
||||
// Summaries of fields
|
||||
//
|
||||
// Top values / number empty
|
||||
// If number: max, min average ...
|
||||
|
||||
// Box to boot transform editor ...
|
||||
|
||||
this.recline = this.recline || {};
|
||||
this.recline.View = this.recline.View || {};
|
||||
|
||||
(function($, my) {
|
||||
"use strict";
|
||||
|
||||
my.Fields = Backbone.View.extend({
|
||||
className: 'recline-fields-view',
|
||||
template: ' \
|
||||
<div class="panel-group fields-list well"> \
|
||||
<h3>{{t.Fields}} <a href="#" class="js-show-hide" title="{{t.Show_field}}"><span class="wcag_hide">{{t.Show_field}}</span><span aria-hidden="true">+</span></a></h3> \
|
||||
{{#fields}} \
|
||||
<div class="panel panel-default field"> \
|
||||
<div class="panel-heading"> \
|
||||
<i class="glyphicon glyphicon-file"></i> \
|
||||
<h4> \
|
||||
{{label}} \
|
||||
<small> \
|
||||
{{type}} \
|
||||
<a class="accordion-toggle" data-toggle="collapse" href="#collapse{{id}}" title="{{t.Expand_and_collapse}}"> <span class="wcag_hide">{{t.Expand_and_collapse}}</span><span aria-hidden="true">»</span> </a> \
|
||||
</small> \
|
||||
</h4> \
|
||||
</div> \
|
||||
<div id="collapse{{id}}" class="panel-collapse collapse"> \
|
||||
<div class="panel-body"> \
|
||||
{{#facets}} \
|
||||
<div class="facet-summary" data-facet="{{id}}"> \
|
||||
<ul class="facet-items"> \
|
||||
{{#terms}} \
|
||||
<li class="facet-item"><span class="term">{{term}}</span> <span class="count">[{{count}}]</span></li> \
|
||||
{{/terms}} \
|
||||
</ul> \
|
||||
</div> \
|
||||
{{/facets}} \
|
||||
<div class="clear"></div> \
|
||||
</div> \
|
||||
</div> \
|
||||
</div> \
|
||||
{{/fields}} \
|
||||
</div> \
|
||||
',
|
||||
|
||||
initialize: function(model) {
|
||||
var self = this;
|
||||
_.bindAll(this, 'render');
|
||||
|
||||
// TODO: this is quite restrictive in terms of when it is re-run
|
||||
// e.g. a change in type will not trigger a re-run atm.
|
||||
// being more liberal (e.g. binding to all) can lead to being called a lot (e.g. for change:width)
|
||||
this.listenTo(this.model.fields, 'reset', function(action) {
|
||||
self.model.fields.each(function(field) {
|
||||
field.facets.unbind('all', self.render);
|
||||
field.facets.bind('all', self.render);
|
||||
});
|
||||
// fields can get reset or changed in which case we need to recalculate
|
||||
self.model.getFieldsSummary();
|
||||
self.render();
|
||||
});
|
||||
this.$el.find('.collapse').collapse();
|
||||
this.render();
|
||||
},
|
||||
render: function() {
|
||||
var self = this;
|
||||
var tmplData = {
|
||||
fields: []
|
||||
};
|
||||
this.model.fields.each(function(field) {
|
||||
var out = field.toJSON();
|
||||
out.facets = field.facets.toJSON();
|
||||
tmplData.fields.push(out);
|
||||
});
|
||||
var tmplData = I18nMessages('recline', recline.View.translations).injectMustache(tmplData);
|
||||
var templated = Mustache.render(this.template, tmplData);
|
||||
this.$el.html(templated);
|
||||
}
|
||||
});
|
||||
|
||||
})(jQuery, recline.View);
|
||||
@@ -1,183 +0,0 @@
|
||||
/*jshint multistr:true */
|
||||
|
||||
this.recline = this.recline || {};
|
||||
this.recline.View = this.recline.View || {};
|
||||
|
||||
(function($, my) {
|
||||
"use strict";
|
||||
|
||||
my.FilterEditor = Backbone.View.extend({
|
||||
className: 'recline-filter-editor well',
|
||||
template: ' \
|
||||
<div class="filters"> \
|
||||
<h3>{{t.Filters}}</h3> \
|
||||
<a href="#" class="js-add-filter">{{t.Add_filter}}</a> \
|
||||
<form class="form-stacked js-add" style="display: none;"> \
|
||||
<div class="form-group"> \
|
||||
<label>{{t.Field}}</label> \
|
||||
<select class="fields form-control"> \
|
||||
{{#fields}} \
|
||||
<option value="{{id}}">{{label}}</option> \
|
||||
{{/fields}} \
|
||||
</select> \
|
||||
</div> \
|
||||
<div class="form-group"> \
|
||||
<label>{{t.Filter_type}}</label> \
|
||||
<select class="filterType form-control"> \
|
||||
<option value="term">{{t.Value}}</option> \
|
||||
<option value="range">{{t.Range}}</option> \
|
||||
<option value="geo_distance">{{t.Geo_distance}}</option> \
|
||||
</select> \
|
||||
</div> \
|
||||
<button type="submit" class="btn btn-default">{{t.Add}}</button> \
|
||||
</form> \
|
||||
<form class="form-stacked js-edit"> \
|
||||
{{#filters}} \
|
||||
{{{filterRender}}} \
|
||||
{{/filters}} \
|
||||
{{#filters.length}} \
|
||||
<button type="submit" class="btn btn-default">{{t.Update}}</button> \
|
||||
{{/filters.length}} \
|
||||
</form> \
|
||||
</div> \
|
||||
',
|
||||
filterTemplates: {
|
||||
term: ' \
|
||||
<div class="filter-{{type}} filter"> \
|
||||
<fieldset> \
|
||||
<legend> \
|
||||
{{field}} <small>{{type}}</small> \
|
||||
<a class="js-remove-filter" href="#" title="{{t.Remove_this_filter}}" data-filter-id="{{id}}"><span class="wcag_hide">{{t.Remove_this_filter}}</span><span aria-hidden="true">×</span></a> \
|
||||
</legend> \
|
||||
<input class="input-sm" 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="{{t.Remove_this_filter}}" data-filter-id="{{id}}"><span class="wcag_hide">{{t.Remove_this_filter}}</span><span aria-hidden="true">×</span></a> \
|
||||
</legend> \
|
||||
<div class="form-group"> \
|
||||
<label class="control-label" for="">{{t.From}}</label> \
|
||||
<input class="input-sm" type="text" value="{{from}}" name="from" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
|
||||
</div> \
|
||||
<div class="form-group"> \
|
||||
<label class="control-label" for="">{{t.To}}</label> \
|
||||
<input class="input-sm" type="text" value="{{to}}" name="to" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
|
||||
</div> \
|
||||
</fieldset> \
|
||||
</div> \
|
||||
',
|
||||
geo_distance: ' \
|
||||
<div class="filter-{{type}} filter"> \
|
||||
<fieldset> \
|
||||
<legend> \
|
||||
{{field}} <small>{{type}}</small> \
|
||||
<a class="js-remove-filter" href="#" title="{{t.Remove_this_filter}}" data-filter-id="{{id}}"><span class="wcag_hide">{{t.Remove_this_filter}}</span><span aria-hidden="true">×</span></a> \
|
||||
</legend> \
|
||||
<div class="form-group"> \
|
||||
<label class="control-label" for="">{{t.Longitude}}</label> \
|
||||
<input class="input-sm" type="text" value="{{point.lon}}" name="lon" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
|
||||
</div> \
|
||||
<div class="form-group"> \
|
||||
<label class="control-label" for="">{{t.Latitude}}</label> \
|
||||
<input class="input-sm" type="text" value="{{point.lat}}" name="lat" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
|
||||
</div> \
|
||||
<div class="form-group"> \
|
||||
<label class="control-label" for="">{{t.Distance_km}}</label> \
|
||||
<input class="input-sm" type="text" value="{{distance}}" name="distance" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
|
||||
</div> \
|
||||
</fieldset> \
|
||||
</div> \
|
||||
'
|
||||
},
|
||||
events: {
|
||||
'click .js-remove-filter': 'onRemoveFilter',
|
||||
'click .js-add-filter': 'onAddFilterShow',
|
||||
'submit form.js-edit': 'onTermFiltersUpdate',
|
||||
'submit form.js-add': 'onAddFilter'
|
||||
},
|
||||
initialize: function() {
|
||||
_.bindAll(this, 'render');
|
||||
this.listenTo(this.model.fields, 'all', this.render);
|
||||
this.listenTo(this.model.queryState, 'change change:filters:new-blank', this.render);
|
||||
this.render();
|
||||
},
|
||||
render: function() {
|
||||
var self = this;
|
||||
var tmplData = $.extend(true, {}, this.model.queryState.toJSON());
|
||||
// we will use idx in list as there id ...
|
||||
tmplData.filters = _.map(tmplData.filters, function(filter, idx) {
|
||||
filter.id = idx;
|
||||
return filter;
|
||||
});
|
||||
tmplData.fields = this.model.fields.toJSON();
|
||||
tmplData.filterRender = function() {
|
||||
var filterData = I18nMessages('recline', recline.View.translations).injectMustache(this);
|
||||
return Mustache.render(self.filterTemplates[this.type], filterData);
|
||||
};
|
||||
tmplData = I18nMessages('recline', recline.View.translations).injectMustache(tmplData);
|
||||
var out = Mustache.render(this.template, tmplData);
|
||||
this.$el.html(out);
|
||||
},
|
||||
onAddFilterShow: function(e) {
|
||||
e.preventDefault();
|
||||
var $target = $(e.target);
|
||||
$target.hide();
|
||||
this.$el.find('form.js-add').show();
|
||||
},
|
||||
onAddFilter: function(e) {
|
||||
e.preventDefault();
|
||||
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});
|
||||
},
|
||||
onRemoveFilter: function(e) {
|
||||
e.preventDefault();
|
||||
var $target = $(e.target);
|
||||
var filterId = $target.attr('data-filter-id');
|
||||
this.model.queryState.removeFilter(filterId);
|
||||
},
|
||||
onTermFiltersUpdate: function(e) {
|
||||
var self = this;
|
||||
e.preventDefault();
|
||||
var filters = self.model.queryState.get('filters');
|
||||
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 filterIndex = parseInt($input.attr('data-filter-id'), 10);
|
||||
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, from: 0});
|
||||
self.model.queryState.trigger('change');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
})(jQuery, recline.View);
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
/*jshint multistr:true */
|
||||
|
||||
this.recline = this.recline || {};
|
||||
this.recline.View = this.recline.View || {};
|
||||
|
||||
(function($, my) {
|
||||
"use strict";
|
||||
|
||||
my.Pager = Backbone.View.extend({
|
||||
className: 'recline-pager',
|
||||
template: ' \
|
||||
<div class="pagination"> \
|
||||
<ul class="pagination"> \
|
||||
<li class="prev action-pagination-update"><a href="" class="btn btn-default">«</a></li> \
|
||||
<li class="page-range"><a><label for="from">From</label><input id="from" name="from" type="text" value="{{from}}" /> – <label for="to">To</label><input id="to" name="to" type="text" value="{{to}}" /> </a></li> \
|
||||
<li class="next action-pagination-update"><a href="" class="btn btn-default">»</a></li> \
|
||||
</ul> \
|
||||
</div> \
|
||||
',
|
||||
|
||||
events: {
|
||||
'click .action-pagination-update': 'onPaginationUpdate',
|
||||
'change input': 'onFormSubmit'
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
_.bindAll(this, 'render');
|
||||
this.listenTo(this.model.queryState, 'change', this.render);
|
||||
this.render();
|
||||
},
|
||||
onFormSubmit: function(e) {
|
||||
e.preventDefault();
|
||||
// filter is 0-based; form is 1-based
|
||||
var formFrom = parseInt(this.$el.find('input[name="from"]').val())-1;
|
||||
var formTo = parseInt(this.$el.find('input[name="to"]').val())-1;
|
||||
var maxRecord = this.model.recordCount-1;
|
||||
if (this.model.queryState.get('from') != formFrom) { // changed from; update from
|
||||
this.model.queryState.set({from: Math.min(maxRecord, Math.max(formFrom, 0))});
|
||||
} else if (this.model.queryState.get('to') != formTo) { // change to; update size
|
||||
var to = Math.min(maxRecord, Math.max(formTo, 0));
|
||||
this.model.queryState.set({size: Math.min(maxRecord+1, Math.max(to-formFrom+1, 1))});
|
||||
}
|
||||
},
|
||||
onPaginationUpdate: function(e) {
|
||||
e.preventDefault();
|
||||
var $el = $(e.target);
|
||||
var newFrom = 0;
|
||||
var currFrom = this.model.queryState.get('from');
|
||||
var size = this.model.queryState.get('size');
|
||||
var updateQuery = false;
|
||||
if ($el.parent().hasClass('prev')) {
|
||||
newFrom = Math.max(currFrom - Math.max(0, size), 0);
|
||||
updateQuery = newFrom != currFrom;
|
||||
} else {
|
||||
newFrom = Math.max(currFrom + size, 0);
|
||||
updateQuery = (newFrom < this.model.recordCount);
|
||||
}
|
||||
if (updateQuery) {
|
||||
this.model.queryState.set({from: newFrom});
|
||||
}
|
||||
},
|
||||
render: function() {
|
||||
var tmplData = this.model.toJSON();
|
||||
var from = parseInt(this.model.queryState.get('from'));
|
||||
tmplData.from = from+1;
|
||||
tmplData.to = Math.min(from+this.model.queryState.get('size'), this.model.recordCount);
|
||||
var templated = Mustache.render(this.template, tmplData);
|
||||
this.$el.html(templated);
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
})(jQuery, recline.View);
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
/*jshint multistr:true */
|
||||
|
||||
this.recline = this.recline || {};
|
||||
this.recline.View = this.recline.View || {};
|
||||
|
||||
(function($, my) {
|
||||
"use strict";
|
||||
|
||||
my.QueryEditor = Backbone.View.extend({
|
||||
className: 'recline-query-editor',
|
||||
template: ' \
|
||||
<form action="" method="GET" class="form-inline" role="form"> \
|
||||
<div class="form-group"> \
|
||||
<div class="input-group text-query"> \
|
||||
<div class="input-group-addon"> \
|
||||
<i class="glyphicon glyphicon-search"></i> \
|
||||
</div> \
|
||||
<label for="q">{{t.Search}}</label> \
|
||||
<input class="form-control search-query" type="text" id="q" name="q" value="{{q}}" placeholder="{{t.Search_data}} ..."> \
|
||||
</div> \
|
||||
</div> \
|
||||
<button type="submit" class="btn btn-default">{{t.Search}} »</button> \
|
||||
</form> \
|
||||
',
|
||||
|
||||
events: {
|
||||
'submit form': 'onFormSubmit'
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
_.bindAll(this, 'render');
|
||||
this.listenTo(this.model, 'change', this.render);
|
||||
this.render();
|
||||
},
|
||||
onFormSubmit: function(e) {
|
||||
e.preventDefault();
|
||||
var query = this.$el.find('.search-query').val();
|
||||
this.model.set({q: query});
|
||||
},
|
||||
render: function() {
|
||||
var tmplData = I18nMessages('recline', recline.View.translations).injectMustache(this.model.toJSON());
|
||||
var templated = Mustache.render(this.template, tmplData);
|
||||
this.$el.html(templated);
|
||||
}
|
||||
});
|
||||
|
||||
})(jQuery, recline.View);
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
/*jshint multistr:true */
|
||||
|
||||
this.recline = this.recline || {};
|
||||
this.recline.View = this.recline.View || {};
|
||||
|
||||
(function($, my) {
|
||||
"use strict";
|
||||
|
||||
my.ValueFilter = Backbone.View.extend({
|
||||
className: 'recline-filter-editor well',
|
||||
template: ' \
|
||||
<div class="filters"> \
|
||||
<h3>{{t.Filters}}</h3> \
|
||||
<button class="btn js-add-filter add-filter">{{t.Add_filter}}</button> \
|
||||
<form class="form-stacked js-add" style="display: none;"> \
|
||||
<fieldset> \
|
||||
<label>{{t.Field}}</label> \
|
||||
<select class="fields form-control"> \
|
||||
{{#fields}} \
|
||||
<option value="{{id}}">{{label}}</option> \
|
||||
{{/fields}} \
|
||||
</select> \
|
||||
<button type="submit" class="btn">{{t.Add}}</button> \
|
||||
</fieldset> \
|
||||
</form> \
|
||||
<form class="form-stacked js-edit"> \
|
||||
{{#filters}} \
|
||||
{{{filterRender}}} \
|
||||
{{/filters}} \
|
||||
{{#filters.length}} \
|
||||
<button type="submit" class="btn update-filter">{{t.Update}}</button> \
|
||||
{{/filters.length}} \
|
||||
</form> \
|
||||
</div> \
|
||||
',
|
||||
filterTemplates: {
|
||||
term: ' \
|
||||
<div class="filter-{{type}} filter"> \
|
||||
<fieldset> \
|
||||
{{field}} \
|
||||
<a class="js-remove-filter" href="#" title="{{t.Remove_this_filter}}" data-filter-id="{{id}}"><span class="wcag_hide">{{t.Remove_this_filter}}</span><span aria-hidden="true">×</span></a> \
|
||||
<input type="text" value="{{term}}" name="term" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
|
||||
</fieldset> \
|
||||
</div> \
|
||||
'
|
||||
},
|
||||
events: {
|
||||
'click .js-remove-filter': 'onRemoveFilter',
|
||||
'click .js-add-filter': 'onAddFilterShow',
|
||||
'submit form.js-edit': 'onTermFiltersUpdate',
|
||||
'submit form.js-add': 'onAddFilter'
|
||||
},
|
||||
initialize: function() {
|
||||
_.bindAll(this, 'render');
|
||||
this.listenTo(this.model.fields, 'all', this.render);
|
||||
this.listenTo(this.model.queryState, 'change change:filters:new-blank', this.render);
|
||||
this.render();
|
||||
},
|
||||
render: function() {
|
||||
var self = this;
|
||||
var tmplData = $.extend(true, {}, this.model.queryState.toJSON());
|
||||
// we will use idx in list as the id ...
|
||||
tmplData.filters = _.map(tmplData.filters, function(filter, idx) {
|
||||
filter.id = idx;
|
||||
return filter;
|
||||
});
|
||||
tmplData.fields = this.model.fields.toJSON();
|
||||
var fmt = I18nMessages('recline', recline.View.translations);
|
||||
tmplData.filterRender = function() {
|
||||
return Mustache.render(self.filterTemplates.term, fmt.injectMustache(this));
|
||||
};
|
||||
var out = Mustache.render(this.template, fmt.injectMustache(tmplData));
|
||||
this.$el.html(out);
|
||||
},
|
||||
updateFilter: function(input) {
|
||||
var self = this;
|
||||
var filters = self.model.queryState.get('filters');
|
||||
var $input = $(input);
|
||||
var filterIndex = parseInt($input.attr('data-filter-id'), 10);
|
||||
var value = $input.val();
|
||||
filters[filterIndex].term = value;
|
||||
},
|
||||
onAddFilterShow: function(e) {
|
||||
e.preventDefault();
|
||||
var $target = $(e.target);
|
||||
$target.hide();
|
||||
this.$el.find('form.js-add').show();
|
||||
},
|
||||
onAddFilter: function(e) {
|
||||
e.preventDefault();
|
||||
var $target = $(e.target);
|
||||
$target.hide();
|
||||
var field = $target.find('select.fields').val();
|
||||
this.model.queryState.addFilter({type: 'term', field: field});
|
||||
},
|
||||
onRemoveFilter: function(e) {
|
||||
e.preventDefault();
|
||||
var $target = $(e.target);
|
||||
var filterId = $target.attr('data-filter-id');
|
||||
this.model.queryState.removeFilter(filterId);
|
||||
},
|
||||
onTermFiltersUpdate: function(e) {
|
||||
var self = this;
|
||||
e.preventDefault();
|
||||
var filters = self.model.queryState.get('filters');
|
||||
var $form = $(e.target);
|
||||
_.each($form.find('input'), function(input) {
|
||||
self.updateFilter(input);
|
||||
});
|
||||
self.model.queryState.set({filters: filters, from: 0});
|
||||
self.model.queryState.trigger('change');
|
||||
}
|
||||
});
|
||||
|
||||
})(jQuery, recline.View);
|
||||
Reference in New Issue
Block a user