diff --git a/dist/recline.dataset.js b/dist/recline.dataset.js index fd97e552c..4b455aa69 100644 --- a/dist/recline.dataset.js +++ b/dist/recline.dataset.js @@ -43,11 +43,19 @@ my.Dataset = Backbone.Model.extend({ // 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. @@ -189,7 +197,7 @@ my.Dataset = Backbone.Model.extend({ this._store.query(actualQuery, this.toJSON()) .done(function(queryResult) { - self._handleQueryResult(queryResult); + self._handleResult(queryResult); self.trigger('query:done'); dfd.resolve(self.records); }) @@ -481,8 +489,8 @@ my.Query = Backbone.Model.extend({ }, range: { type: 'range', - start: '', - stop: '' + from: '', + to: '' }, geo_distance: { type: 'geo_distance', @@ -510,6 +518,23 @@ my.Query = Backbone.Model.extend({ 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 @@ -526,7 +551,7 @@ my.Query = Backbone.Model.extend({ // Add a Facet to this query // // See - addFacet: function(fieldId) { + 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)) { @@ -535,8 +560,13 @@ my.Query = Backbone.Model.extend({ facets[fieldId] = { terms: { field: fieldId } }; + if (!_.isUndefined(size)) { + facets[fieldId].terms.size = size; + } this.set({facets: facets}, {silent: true}); - this.trigger('facet:add', this); + if (!silent) { + this.trigger('facet:add', this); + } }, addHistogramFacet: function(fieldId) { var facets = this.get('facets'); @@ -548,7 +578,30 @@ my.Query = Backbone.Model.extend({ }; 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); } + }); @@ -586,9 +639,9 @@ 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); -}; +// Backbone.sync = function(method, model, options) { +// return model.backend.sync(method, model, options); +// }; }(this.recline.Model)); @@ -695,6 +748,7 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {}; // register filters var filterFunctions = { term : term, + terms : terms, range : range, geo_distance : geo_distance }; @@ -734,20 +788,28 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {}; 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 startnull = (filter.start === null || filter.start === ''); - var stopnull = (filter.stop === null || filter.stop === ''); + 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 start = parse(filter.start); - var stop = parse(filter.stop); + 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 ((!startnull || !stopnull) && value === '') { + if ((!fromnull || !tonull) && value === '') { return false; } - return ((startnull || value >= start) && (stopnull || value <= stop)); + return ((fromnull || value >= from) && (tonull || value <= to)); } function geo_distance() { diff --git a/dist/recline.js b/dist/recline.js index d95cbb531..c24de4828 100644 --- a/dist/recline.js +++ b/dist/recline.js @@ -484,6 +484,7 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {}; // register filters var filterFunctions = { term : term, + terms : terms, range : range, geo_distance : geo_distance }; @@ -523,22 +524,28 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {}; 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 filterStart = filter.start || filter.from; - var filterStop = filter.stop || filter.to; - var startnull = (_.isUndefined(filterStart) || filterStart === null || filterStart === ''); - var stopnull = (_.isUndefined(filterStop) || filterStop === null || filterStop === ''); + 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 start = parse(startnull ? '' : filterStart); - var stop = parse(stopnull ? '' : filterStop); + 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 ((!startnull || !stopnull) && value === '') { + if ((!fromnull || !tonull) && value === '') { return false; } - return ((startnull || value >= start) && (stopnull || value <= stop)); + return ((fromnull || value >= from) && (tonull || value <= to)); } function geo_distance() { @@ -730,11 +737,19 @@ my.Dataset = Backbone.Model.extend({ // 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. @@ -876,7 +891,7 @@ my.Dataset = Backbone.Model.extend({ this._store.query(actualQuery, this.toJSON()) .done(function(queryResult) { - self._handleQueryResult(queryResult); + self._handleResult(queryResult); self.trigger('query:done'); dfd.resolve(self.records); }) @@ -1168,8 +1183,8 @@ my.Query = Backbone.Model.extend({ }, range: { type: 'range', - start: '', - stop: '' + from: '', + to: '' }, geo_distance: { type: 'geo_distance', @@ -1197,6 +1212,23 @@ my.Query = Backbone.Model.extend({ 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 @@ -1213,7 +1245,7 @@ my.Query = Backbone.Model.extend({ // Add a Facet to this query // // See - addFacet: function(fieldId) { + 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)) { @@ -1222,8 +1254,13 @@ my.Query = Backbone.Model.extend({ facets[fieldId] = { terms: { field: fieldId } }; + if (!_.isUndefined(size)) { + facets[fieldId].terms.size = size; + } this.set({facets: facets}, {silent: true}); - this.trigger('facet:add', this); + if (!silent) { + this.trigger('facet:add', this); + } }, addHistogramFacet: function(fieldId) { var facets = this.get('facets'); @@ -1235,7 +1272,30 @@ my.Query = Backbone.Model.extend({ }; 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); } + }); @@ -1273,9 +1333,9 @@ 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); -}; +// Backbone.sync = function(method, model, options) { +// return model.backend.sync(method, model, options); +// }; }(this.recline.Model)); @@ -1594,21 +1654,33 @@ my.Flot = Backbone.View.extend({ _.each(this.state.attributes.series, function(field) { var points = []; var fieldLabel = self.model.fields.get(field).get('label'); - _.each(self.model.records.models, function(doc, index) { - var x = doc.getFieldValueUnrendered(xfield); - if (isDateTime) { - // cast to string as Date(1990) produces 1970 date but Date('1990') produces 1/1/1990 - var _date = moment(String(x)); - if (_date.isValid()) { - x = _date.toDate().getTime(); - } - } else if (typeof x === 'string') { - x = parseFloat(x); - if (isNaN(x)) { // assume this is a string label - x = index; - self.xvaluesAreIndex = true; - } + 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); @@ -2063,7 +2135,7 @@ this.recline.View = this.recline.View || {}; // 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 +// 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. // @@ -2379,6 +2451,31 @@ my.Map = Backbone.View.extend({ }, + // 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){ @@ -2390,12 +2487,12 @@ my.Map = Backbone.View.extend({ value = $.parseJSON(value); } catch(e) {} } - if (typeof(value) === 'string') { value = value.replace('(', '').replace(')', ''); var parts = value.split(','); - var lat = parseFloat(parts[0]); - var lon = parseFloat(parts[1]); + var lat = this._parseCoordinateString(parts[0]); + var lon = this._parseCoordinateString(parts[1]); + if (!isNaN(lon) && !isNaN(parseFloat(lat))) { return { "type": "Point", @@ -2423,6 +2520,9 @@ my.Map = Backbone.View.extend({ // 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', @@ -2486,8 +2586,8 @@ my.Map = Backbone.View.extend({ var self = this; this.map = new L.Map(this.$map.get(0)); - var mapUrl = "http://otile{s}.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.png"; - var osmAttribution = 'Map data © 2011 OpenStreetMap contributors, Tiles Courtesy of MapQuest '; + var mapUrl = "//otile{s}-s.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.png"; + var osmAttribution = 'Map data © 2011 OpenStreetMap contributors, Tiles Courtesy of MapQuest '; var bg = new L.TileLayer(mapUrl, {maxZoom: 18, attribution: osmAttribution ,subdomains: '1234'}); this.map.addLayer(bg); @@ -2958,7 +3058,7 @@ my.MultiView = Backbone.View.extend({ }, this); this.pager = new recline.View.Pager({ - model: this.model.queryState + model: this.model }); this.$el.find('.recline-results-info').after(this.pager.el); @@ -3266,6 +3366,37 @@ this.recline.View = this.recline.View || {}; (function($, my) { "use strict"; + // 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: '

Add row

', + + initialize: function(options){ + var self = this; + _.bindAll(this, 'render'); + this.state = new recline.Model.ObjectState(); + this.render(); + }, + + render: function() { + var self = this; + this.$el.html(this.template) + }, + + events : { + "click .recline-row-add" : "addNewRow" + }, + + addNewRow : function(e){ + e.preventDefault() + this.state.trigger("change") + } + } + ); // ## SlickGrid Dataset View // // Provides a tabular view on a Dataset, based on SlickGrid. @@ -3284,7 +3415,11 @@ this.recline.View = this.recline.View || {}; // model: dataset, // el: $el, // state: { -// gridOptions: {editable: true}, +// gridOptions: { +// editable: true, +// enableAddRows: true +// ... +// }, // columnsEditor: [ // {column: 'date', editor: Slick.Editors.Date }, // {column: 'title', editor: Slick.Editors.Text} @@ -3296,10 +3431,14 @@ 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" : 'X' + } _.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: [], @@ -3312,29 +3451,32 @@ my.SlickGrid = Backbone.View.extend({ ); this.state = new recline.Model.ObjectState(state); - this._slickHandler = new Slick.EventHandler(); - }, - events: { + //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() { + render: function() { var self = this; - var options = _.extend({ enableCellNavigation: true, enableColumnReorder: true, @@ -3344,18 +3486,48 @@ my.SlickGrid = Backbone.View.extend({ }, self.state.get('gridOptions')); // We need all columns, even the hidden ones, to show on the column picker - var columns = []; + var columns = []; // custom formatter as default one escapes html // plus this way we distinguish between rendering/formatting and computed value (so e.g. sort still works ...) // row = row index, cell = cell index, value = value, columnDef = column definition, dataContext = full row values var formatter = function(row, cell, value, columnDef, dataContext) { - var field = self.model.fields.get(columnDef.id); + if(columnDef.id == "del"){ + return self.templates.deleterow + } + var field = self.model.fields.get(columnDef.id); if (field.renderer) { - return field.renderer(value, field, dataContext); - } else { - return value; + 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: "A date is required, check field field-date-format"}; + }else { + return {valid: true, msg :null } + } + } + }; + //Add row delete support , check if enableDelRow is set to true or not set + if(this.state.get("gridOptions") + && this.state.get("gridOptions").enabledDelRow != undefined + && this.state.get("gridOptions").enabledDelRow == true ){ + columns.push({ + id: 'del', + name: 'del', + field: 'del', + sortable: true, + width: 80, + formatter: formatter, + validator:validator + })} _.each(this.model.fields.toJSON(),function(field){ var column = { id: field.id, @@ -3363,26 +3535,41 @@ my.SlickGrid = Backbone.View.extend({ field: field.id, sortable: true, minWidth: 80, - formatter: formatter + 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){ @@ -3402,12 +3589,16 @@ my.SlickGrid = Backbone.View.extend({ } } columns = columns.concat(tempHiddenColumns); - // Transform a model object into a row function toRow(m) { var row = {}; self.model.fields.each(function(field){ - row[field.id] = m.getFieldValueUnrendered(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; } @@ -3430,6 +3621,7 @@ my.SlickGrid = Backbone.View.extend({ rows[i] = toRow(m); models[i] = m; }; + } var data = new RowSet(); @@ -3439,7 +3631,6 @@ my.SlickGrid = Backbone.View.extend({ }); this.grid = new Slick.Grid(this.el, data, visibleColumns, options); - // Column sorting var sortInfo = this.model.queryState.get('sort'); if (sortInfo){ @@ -3472,19 +3663,25 @@ my.SlickGrid = Backbone.View.extend({ }); 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); - }); - - var columnpicker = new Slick.Controls.ColumnPicker(columns, this.grid, + }); + this._slickHandler.subscribe(this.grid.onClick,function(e, args){ + if (args.cell == 0 && 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){ @@ -3641,6 +3838,7 @@ my.SlickGrid = Backbone.View.extend({ // Slick.Controls.ColumnPicker $.extend(true, window, { Slick:{ Controls:{ ColumnPicker:SlickColumnPicker }}}); })(jQuery); + /*jshint multistr:true */ this.recline = this.recline || {}; @@ -4070,9 +4268,9 @@ my.FilterEditor = Backbone.View.extend({ × \ \ \ - \ + \ \ - \ + \ \ \ ', @@ -4205,34 +4403,43 @@ my.Pager = Backbone.View.extend({ initialize: function() { _.bindAll(this, 'render'); - this.listenTo(this.model, 'change', this.render); + this.listenTo(this.model.queryState, 'change', this.render); this.render(); }, onFormSubmit: function(e) { e.preventDefault(); var newFrom = parseInt(this.$el.find('input[name="from"]').val()); + newFrom = Math.min(this.model.recordCount, Math.max(newFrom, 1))-1; var newSize = parseInt(this.$el.find('input[name="to"]').val()) - newFrom; - newFrom = Math.max(newFrom, 0); - newSize = Math.max(newSize, 1); - this.model.set({size: newSize, from: newFrom}); + newSize = Math.min(Math.max(newSize, 1), this.model.recordCount); + this.model.queryState.set({size: newSize, from: newFrom}); }, 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 = this.model.get('from') - Math.max(0, this.model.get('size')); + newFrom = Math.max(currFrom - Math.max(0, size), 1)-1; + updateQuery = newFrom != currFrom; } else { - newFrom = this.model.get('from') + this.model.get('size'); + newFrom = Math.max(currFrom + size, 1); + updateQuery = (newFrom < this.model.recordCount); + } + if (updateQuery) { + this.model.queryState.set({from: newFrom}); } - newFrom = Math.max(newFrom, 0); - this.model.set({from: newFrom}); }, render: function() { var tmplData = this.model.toJSON(); - tmplData.to = this.model.get('from') + this.model.get('size'); + 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; } }); diff --git a/docs/src/backend.csv.html b/docs/src/backend.csv.html index e8596ea94..bfc6461a5 100644 --- a/docs/src/backend.csv.html +++ b/docs/src/backend.csv.html @@ -1,7 +1,8 @@ - backend.csv.js

backend.csv.js

this.recline = this.recline || {};
+      backend.csv.js           

backend.csv.js

this.recline = this.recline || {};
 this.recline.Backend = this.recline.Backend || {};
 this.recline.Backend.CSV = this.recline.Backend.CSV || {};

Note that provision of jQuery is optional (it is only needed if you use fetch on a remote file)

(function(my) {
-  my.__type__ = 'csv';

use either jQuery or Underscore Deferred depending on what is available

  var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred;

fetch

+ "use strict"; + my.__type__ = 'csv';

use either jQuery or Underscore Deferred depending on what is available

  var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred;

fetch

fetch supports 3 options depending on the attribute provided on the dataset argument

@@ -25,36 +26,42 @@ var reader = new FileReader(); var encoding = dataset.encoding || 'UTF-8'; reader.onload = function(e) { - var rows = my.parseCSV(e.target.result, dataset); - dfd.resolve({ - records: rows, - metadata: { - filename: dataset.file.name - }, - useMemoryStore: true - }); + var out = my.extractFields(my.parseCSV(e.target.result, dataset), dataset); + out.useMemoryStore = true; + out.metadata = { + filename: dataset.file.name + } + dfd.resolve(out); }; reader.onerror = function (e) { alert('Failed to load file. Code: ' + e.target.error.code); }; reader.readAsText(dataset.file, encoding); } else if (dataset.data) { - var rows = my.parseCSV(dataset.data, dataset); - dfd.resolve({ - records: rows, - useMemoryStore: true - }); + var out = my.extractFields(my.parseCSV(dataset.data, dataset), dataset); + out.useMemoryStore = true; + dfd.resolve(out); } else if (dataset.url) { jQuery.get(dataset.url).done(function(data) { - var rows = my.parseCSV(data, dataset); - dfd.resolve({ - records: rows, - useMemoryStore: true - }); + var out = my.extractFields(my.parseCSV(data, dataset), dataset); + out.useMemoryStore = true; + dfd.resolve(out); }); } return dfd.promise(); - };

parseCSV

+ };

Convert array of rows in { records: [ ...] , fields: [ ... ] } +@param {Boolean} noHeaderRow If true assume that first row is not a header (i.e. list of fields but is data.

  my.extractFields = function(rows, noFields) {
+    if (noFields.noHeaderRow !== true && rows.length > 0) {
+      return {
+        fields: rows[0],
+        records: rows.slice(1)
+      }
+    } else {
+      return {
+        records: rows
+      }
+    }
+  };

parseCSV

Converts a Comma Separated Values string into an array of arrays. Each line in the CSV becomes an array.

@@ -74,8 +81,10 @@ fields containing special characters, such as the delimiter or quotechar, or which contain new-line characters. It defaults to '"'

+

@param {Integer} skipInitialRows A integer number of rows to skip (default 0)

+

Heavily based on uselesscode's JS CSV parser (MIT Licensed): -http://www.uselesscode.org/javascript/csv/

  my.parseCSV= function(s, options) {

Get rid of any trailing \n

    s = chomp(s);
+http://www.uselesscode.org/javascript/csv/

  my.parseCSV= function(s, options) {

Get rid of any trailing \n

    s = chomp(s);
 
     var options = options || {};
     var trm = (options.trim === false) ? false : true;
@@ -92,10 +101,10 @@
       processField;
 
     processField = function (field) {
-      if (fieldQuoted !== true) {

If field is empty set to null

        if (field === '') {
-          field = null;

If the field was not quoted and we are trimming fields, trim it

        } else if (trm === true) {
+      if (fieldQuoted !== true) {

If field is empty set to null

        if (field === '') {
+          field = null;

If the field was not quoted and we are trimming fields, trim it

        } else if (trm === true) {
           field = trim(field);
-        }

Convert unquoted numbers to their appropriate types

        if (rxIsInt.test(field)) {
+        }

Convert unquoted numbers to their appropriate types

        if (rxIsInt.test(field)) {
           field = parseInt(field, 10);
         } else if (rxIsFloat.test(field)) {
           field = parseFloat(field, 10);
@@ -105,30 +114,30 @@
     };
 
     for (i = 0; i < s.length; i += 1) {
-      cur = s.charAt(i);

If we are at a EOF or EOR

      if (inQuote === false && (cur === delimiter || cur === "\n")) {
-  field = processField(field);

Add the current field to the current row

        row.push(field);

If this is EOR append row to output and flush row

        if (cur === "\n") {
+      cur = s.charAt(i);

If we are at a EOF or EOR

      if (inQuote === false && (cur === delimiter || cur === "\n")) {
+        field = processField(field);

Add the current field to the current row

        row.push(field);

If this is EOR append row to output and flush row

        if (cur === "\n") {
           out.push(row);
           row = [];
-        }

Flush the field buffer

        field = '';
+        }

Flush the field buffer

        field = '';
         fieldQuoted = false;
-      } else {

If it's not a quotechar, add it to the field buffer

        if (cur !== quotechar) {
+      } else {

If it's not a quotechar, add it to the field buffer

        if (cur !== quotechar) {
           field += cur;
         } else {
-          if (!inQuote) {

We are not in a quote, start a quote

            inQuote = true;
+          if (!inQuote) {

We are not in a quote, start a quote

            inQuote = true;
             fieldQuoted = true;
-          } else {

Next char is quotechar, this is an escaped quotechar

            if (s.charAt(i + 1) === quotechar) {
-              field += quotechar;

Skip the next char

              i += 1;
-            } else {

It's not escaping, so end quote

              inQuote = false;
+          } else {

Next char is quotechar, this is an escaped quotechar

            if (s.charAt(i + 1) === quotechar) {
+              field += quotechar;

Skip the next char

              i += 1;
+            } else {

It's not escaping, so end quote

              inQuote = false;
             }
           }
         }
       }
-    }

Add the last field

    field = processField(field);
+    }

Add the last field

    field = processField(field);
     row.push(field);
-    out.push(row);
+    out.push(row);

Expose the ability to discard initial rows

    if (options.skipInitialRows) out = out.slice(options.skipInitialRows);
 
     return out;
-  };

serializeCSV

+ };

serializeCSV

Convert an Object or a simple array of arrays into a Comma Separated Values string.

@@ -180,9 +189,9 @@ processField; processField = function (field) { - if (field === null) {

If field is null set to empty string

        field = '';
-      } else if (typeof field === "string" && rxNeedsQuoting.test(field)) {

Convert string to delimited string

        field = quotechar + field + quotechar;
-      } else if (typeof field === "number") {

Convert number to string

        field = field.toString(10);
+      if (field === null) {

If field is null set to empty string

        field = '';
+      } else if (typeof field === "string" && rxNeedsQuoting.test(field)) {

Convert string to delimited string

        field = quotechar + field + quotechar;
+      } else if (typeof field === "number") {

Convert number to string

        field = field.toString(10);
       }
 
       return field;
@@ -192,12 +201,12 @@
       cur = a[i];
 
       for (j = 0; j < cur.length; j += 1) {
-        field = processField(cur[j]);

If this is EOR append row to output and flush row

        if (j === (cur.length - 1)) {
+        field = processField(cur[j]);

If this is EOR append row to output and flush row

        if (j === (cur.length - 1)) {
           row += field;
           out += row + "\n";
           row = '';
-        } else {

Add the current field to the current row

          row += field + delimiter;
-        }

Flush the field buffer

        field = '';
+        } else {

Add the current field to the current row

          row += field + delimiter;
+        }

Flush the field buffer

        field = '';
       }
     }
 
@@ -205,10 +214,10 @@
   };
 
   var rxIsInt = /^\d+$/,
-    rxIsFloat = /^\d*\.\d+$|^\d+\.\d*$/,

If a string has leading or trailing space, + rxIsFloat = /^\d*\.\d+$|^\d+\.\d*$/,

If a string has leading or trailing space, contains a comma double quote or a newline it needs to be quoted in CSV output

    rxNeedsQuoting = /^\s|\s$|,|"|\n/,
-    trim = (function () {

Fx 3.1 has a native trim function, it's about 10x faster, use it if it exists

      if (String.prototype.trim) {
+    trim = (function () {

Fx 3.1 has a native trim function, it's about 10x faster, use it if it exists

      if (String.prototype.trim) {
         return function (s) {
           return s.trim();
         };
@@ -220,8 +229,8 @@
     }());
 
   function chomp(s) {
-    if (s.charAt(s.length - 1) !== "\n") {

Does not end with \n, just return string

      return s;
-    } else {

Remove the \n

      return s.substring(0, s.length - 1);
+    if (s.charAt(s.length - 1) !== "\n") {

Does not end with \n, just return string

      return s;
+    } else {

Remove the \n

      return s.substring(0, s.length - 1);
     }
   }
 
diff --git a/docs/src/backend.dataproxy.html b/docs/src/backend.dataproxy.html
index d52d17db8..e3e685ba7 100644
--- a/docs/src/backend.dataproxy.html
+++ b/docs/src/backend.dataproxy.html
@@ -1,12 +1,13 @@
-      backend.dataproxy.js           

backend.dataproxy.js

this.recline = this.recline || {};
+      backend.dataproxy.js           

backend.dataproxy.js

this.recline = this.recline || {};
 this.recline.Backend = this.recline.Backend || {};
 this.recline.Backend.DataProxy = this.recline.Backend.DataProxy || {};
 
 (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 = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred;

load

+

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.

diff --git a/docs/src/backend.memory.html b/docs/src/backend.memory.html index 5585712e3..10d4b52ec 100644 --- a/docs/src/backend.memory.html +++ b/docs/src/backend.memory.html @@ -1,9 +1,10 @@ - backend.memory.js

backend.memory.js

this.recline = this.recline || {};
+      backend.memory.js           

backend.memory.js

this.recline = this.recline || {};
 this.recline.Backend = this.recline.Backend || {};
 this.recline.Backend.Memory = this.recline.Backend.Memory || {};
 
 (function(my) {
-  my.__type__ = 'memory';

private data - use either jQuery or Underscore Deferred depending on what is available

  var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred;

Data Wrapper

+ "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 @@ -82,6 +83,7 @@ };

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
       };
@@ -89,9 +91,9 @@
         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 new Date(e).valueOf() },
-        datetime   : function (e) { return new Date(e).valueOf() }
+        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) {
@@ -112,17 +114,25 @@
         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 startnull = (filter.start == null || filter.start === '');
-        var stopnull = (filter.stop == null || filter.stop === '');
+        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 start = parse(filter.start);
-        var stop  = parse(filter.stop);

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 ((!startnull || !stopnull) && value === '') {
+        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 ((startnull || value >= start) && (stopnull || value <= stop));
+        return ((fromnull || value >= from) && (tonull || value <= to));
       }
 
       function geo_distance() {

TODO code here

      }
@@ -130,8 +140,8 @@
       if (queryObj.q) {
         var terms = queryObj.q.split(' ');
         var patterns=_.map(terms, function(term) {
-          return new RegExp(term.toLowerCase());;
-          });
+          return new RegExp(term.toLowerCase());
+        });
         results = _.filter(results, function(rawdoc) {
           var matches = true;
           _.each(patterns, function(pattern) {
diff --git a/docs/src/ecma-fixes.html b/docs/src/ecma-fixes.html
index 3df0df090..9f4b09dae 100644
--- a/docs/src/ecma-fixes.html
+++ b/docs/src/ecma-fixes.html
@@ -1,4 +1,4 @@
-      ecma-fixes.js           

ecma-fixes.js

This file adds in full array method support in browsers that don't support it + ecma-fixes.js

ecma-fixes.js

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;
diff --git a/docs/src/model.html b/docs/src/model.html
index 80f8bc8f1..3f318fe0b 100644
--- a/docs/src/model.html
+++ b/docs/src/model.html
@@ -1,10 +1,12 @@
-      model.js           

model.js

Recline Backbone Models

this.recline = this.recline || {};
+      model.js           

model.js

Recline Backbone Models

this.recline = this.recline || {};
 this.recline.Model = this.recline.Model || {};
 
-(function(my) {

use either jQuery or Underscore Deferred depending on what is available

var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred;

Dataset

my.Dataset = Backbone.Model.extend({
+(function(my) {
+  "use strict";

use either jQuery or Underscore Deferred depending on what is available

var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred;

Dataset

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')) {
@@ -24,14 +26,20 @@
     this.facets = new my.FacetList();
     this.recordCount = null;
     this.queryState = new my.Query();
-    this.queryState.bind('change', this.query);
-    this.queryState.bind('facet:add', this.query);

store is what we query and save against + 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;
+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();
     }
-  },

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;
@@ -43,15 +51,19 @@
         .fail(function(args) {
           dfd.reject(args);
         });
-    } else {

special case where we have been given data directly

      handleResults({
+    } else {

special case where we have been given data directly

      handleResults({
         records: this.get('records'),
         fields: this.get('fields'),
         useMemoryStore: true
       });
     }
 
-    function handleResults(results) {
-      var out = self._normalizeRecordsAndFields(results.records, results.fields);
+    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);
       }
@@ -68,12 +80,12 @@
     }
 
     return dfd.promise();
-  },

_normalizeRecordsAndFields

+ },

_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 = [ {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 {
@@ -81,14 +93,14 @@
           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 + }

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, '');
+        }

cannot use trim as not supported by IE7

        var fieldId = field.replace(/^\s+|\s+$/g, '');
         if (fieldId === '') {
           fieldId = '_noname_';
           field = fieldId;
@@ -99,10 +111,10 @@
         }
         if (!(field in seen)) {
           seen[field] = 0;
-        }

TODO: decide whether to keep original name as label ... + }

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 + }

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 = {};
@@ -119,8 +131,8 @@
   },
 
   save: function() {
-    var self = this;

TODO: need to reset the changes ...

    return this._store.save(this._changes, this.toJSON());
-  },

query

+ 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.

@@ -140,7 +152,7 @@ this._store.query(actualQuery, this.toJSON()) .done(function(queryResult) { - self._handleQueryResult(queryResult); + self._handleResult(queryResult); self.trigger('query:done'); dfd.resolve(self.records); }) @@ -180,7 +192,7 @@ data.recordCount = this.recordCount; data.fields = this.fields.toJSON(); return data; - },

getFieldsSummary

+ },

getFieldsSummary

Get a summary for each field in the form of a Facet.

@@ -196,15 +208,15 @@ 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);
+          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) {
+  },

Deprecated (as of v0.5) - use record.summary()

  recordSummary: function(record) {
     return record.summary();
-  },

_backendFromString(backendString)

+ },

_backendFromString(backendString)

Look up a backend module from a backend string (look in recline.Backend)

  _backendFromString: function(backendString) {
     var backend = null;
@@ -217,12 +229,12 @@
     }
     return backend;
   }
-});

A Record

+});

A Record

A single record (or row) in the dataset

my.Record = Backbone.Model.extend({
   constructor: function Record() {
     Backbone.Model.prototype.constructor.apply(this, arguments);
-  },

initialize

+ },

initialize

Create a Record

@@ -231,18 +243,18 @@

Certain methods require presence of a fields attribute (identical to that on Dataset)

  initialize: function() {
     _.bindAll(this, 'getFieldValue');
-  },

getFieldValue

+ },

getFieldValue

For the provided Field get the corresponding rendered computed data value for this record.

NB: if field is undefined a default '' value will be returned

  getFieldValue: function(field) {
-    val = this.getFieldValueUnrendered(field);
+    var val = this.getFieldValueUnrendered(field);
     if (field && !_.isUndefined(field.renderer)) {
       val = field.renderer(val, field, this.toJSON());
     }
     return val;
-  },

getFieldValueUnrendered

+ },

getFieldValueUnrendered

For the provided Field get the corresponding computed data value for this record.

@@ -256,7 +268,7 @@ val = field.deriver(val, field, this); } return val; - },

summary

+ },

summary

Get a simple html summary of this record in form of key/value list

  summary: function(record) {
     var self = this;
@@ -268,30 +280,30 @@
     });
     html += '</div>';
     return html;
-  },

Override Backbone save, fetch and destroy so they do nothing + },

Override Backbone save, fetch and destroy so they do nothing Instead, Dataset object that created this Record should take care of handling these changes (discovery will occur via event notifications) WARNING: these will not persist unless you call save on Dataset

  fetch: function() {},
   save: function() {},
   destroy: function() { this.trigger('destroy', this); }
-});

A Backbone collection of Records

my.RecordList = Backbone.Collection.extend({
+});

A Backbone collection of Records

my.RecordList = Backbone.Collection.extend({
   constructor: function RecordList() {
     Backbone.Collection.prototype.constructor.apply(this, arguments);
   },
   model: my.Record
-});

A Field (aka Column) on a Dataset

my.Field = Backbone.Model.extend({
+});

A Field (aka Column) on a Dataset

my.Field = Backbone.Model.extend({
   constructor: function Field() {
     Backbone.Model.prototype.constructor.apply(this, arguments);
-  },

defaults - define default values

  defaults: {
+  },

defaults - define default values

  defaults: {
     label: null,
     type: 'string',
     format: null,
     is_derived: false
-  },

initialize

+ },

initialize

@param {Object} data: standard Backbone model attributes

-

@param {Object} options: renderer and/or deriver functions.

  initialize: function(data, options) {

if a hash not passed in the first argument throw error

    if ('0' in data) {
+

@param {Object} options: renderer and/or deriver functions.

  initialize: function(data, options) {

if a hash not passed in the first argument throw error

    if ('0' in data) {
       throw new Error('Looks like you did not pass a proper hash with id to Field constructor');
     }
     if (this.attributes.label === null) {
@@ -346,7 +358,7 @@
         }
       } else if (format == 'plain') {
         return val;
-      } else {

as this is the default and default type is string may get things + } else {

as this is the default and default type is string may get things here that are not actually strings

        if (val && typeof val === 'string') {
           val = val.replace(/(https?:\/\/[^ ]+)/g, '<a href="$1">$1</a>');
         }
@@ -361,7 +373,7 @@
     Backbone.Collection.prototype.constructor.apply(this, arguments);
   },
   model: my.Field
-});

Query

my.Query = Backbone.Model.extend({
+});

Query

my.Query = Backbone.Model.extend({
   constructor: function Query() {
     Backbone.Model.prototype.constructor.apply(this, arguments);
   },
@@ -376,13 +388,13 @@
   },
   _filterTemplates: {
     term: {
-      type: 'term',

TODO do we need this attribute here?

      field: '',
+      type: 'term',

TODO do we need this attribute here?

      field: '',
       term: ''
     },
     range: {
       type: 'range',
-      start: '',
-      stop: ''
+      from: '',
+      to: ''
     },
     geo_distance: {
       type: 'geo_distance',
@@ -393,38 +405,56 @@
         lat: 0
       }
     }
-  },  

addFilter(filter)

+ },

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) {
+

@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

+ },

removeFilter

Remove a filter from filters at index filterIndex

  removeFilter: function(filterIndex) {
     var filters = this.get('filters');
     filters.splice(filterIndex, 1);
     this.set({filters: filters});
     this.trigger('change');
-  },

addFacet

+ },

addFacet

Add a Facet to this query

-

See http://www.elasticsearch.org/guide/reference/api/search/facets/

  addFacet: function(fieldId) {
-    var facets = this.get('facets');

Assume id and fieldId should be the same (TODO: this need not be true if we want to add two different type of facets on same field)

    if (_.contains(_.keys(facets), fieldId)) {
+

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});
-    this.trigger('facet:add', this);
+    if (!silent) {
+      this.trigger('facet:add', this);
+    }
   },
   addHistogramFacet: function(fieldId) {
     var facets = this.get('facets');
@@ -436,8 +466,27 @@
     };
     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 Facet (Result)

my.Facet = Backbone.Model.extend({
+
+});

A Facet (Result)

my.Facet = Backbone.Model.extend({
   constructor: function Facet() {
     Backbone.Model.prototype.constructor.apply(this, arguments);
   },
@@ -450,20 +499,19 @@
       terms: []
     };
   }
-});

A Collection/List of Facets

my.FacetList = Backbone.Collection.extend({
+});

A Collection/List of Facets

my.FacetList = Backbone.Collection.extend({
   constructor: function FacetList() {
     Backbone.Collection.prototype.constructor.apply(this, arguments);
   },
   model: my.Facet
-});

Object State

+});

Object State

Convenience Backbone model for storing (configuration) state of objects like Views.

my.ObjectState = Backbone.Model.extend({
-});

Backbone.sync

- -

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);
-};
+});

Backbone.sync

-}(this.recline.Model)); +

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));
 
 
\ No newline at end of file diff --git a/docs/src/view.flot.html b/docs/src/view.flot.html index 1ed374bdb..08a8a0303 100644 --- a/docs/src/view.flot.html +++ b/docs/src/view.flot.html @@ -1,9 +1,10 @@ - view.flot.js

view.flot.js

/*jshint multistr:true */
+      view.flot.js           

view.flot.js

/*jshint multistr:true */
 
 this.recline = this.recline || {};
 this.recline.View = this.recline.View || {};
 
-(function($, my) {

Graph view for a Dataset using Flot graphing library.

+(function($, my) { + "use strict";

Graph view for a Dataset using Flot graphing library.

Initialization arguments (in a hash in first parameter):

@@ -14,7 +15,8 @@

{ group: {column name for x-axis}, series: [{column name for series A}, {column name series B}, ... ], - graphType: 'line', + // options are: lines, points, lines-and-points, bars, columns + graphType: 'lines', graphOptions: {custom [flot options]} }

@@ -37,14 +39,11 @@ var self = this; this.graphColors = ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"]; - this.el = $(this.el); _.bindAll(this, 'render', 'redraw', '_toolTip', '_xaxisLabel'); this.needToRedraw = false; - this.model.bind('change', this.render); - this.model.fields.bind('reset', this.render); - this.model.fields.bind('add', this.render); - this.model.records.bind('add', this.redraw); - this.model.records.bind('reset', this.redraw); + 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'
@@ -57,27 +56,32 @@
       model: this.model,
       state: this.state.toJSON()
     });
-    this.editor.state.bind('change', function() {
+    this.listenTo(this.editor.state, 'change', function() {
       self.state.set(self.editor.state.toJSON());
       self.redraw();
     });
-    this.elSidebar = this.editor.el;
+    this.elSidebar = this.editor.$el;
   },
 
   render: function() {
     var self = this;
     var tmplData = this.model.toTemplateJSON();
     var htmls = Mustache.render(this.template, tmplData);
-    $(this.el).html(htmls);
-    this.$graph = this.el.find('.panel.graph');
+    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[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;
@@ -137,20 +141,16 @@
   },
 
   _xaxisLabel: function (x) {
-    var xfield = this.model.fields.get(this.state.attributes.group);

time series

    var xtype = xfield.get('type');
-    var isDateTime = (xtype === 'date' || xtype === 'date-time' || xtype  === 'time');
-
-    if (this.xvaluesAreIndex) {
+    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);
     }
-    if (isDateTime) {
-      x = new Date(x).toLocaleDateString();
-    }

} else if (isDateTime) { - x = new Date(parseInt(x, 10)).toLocaleDateString(); -}

    return x;
-  },

getGraphOptions

+ + return x; + },

getGraphOptions

Get options for Flot Graph

@@ -159,32 +159,35 @@

@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 = {};
 
-    var tickFormatter = function (x) {

convert x to a string and make sure that it is not too long or the + 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) || "";
+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;
-    };
+        if (typeof label !== 'string') {
+          label = label.toString();
+        }
+        if (self.state.attributes.graphType !== 'bars' && label.length > 10) {
+          label = label.slice(0, 10) + "...";
+        }
 
-    var xaxis = {};
-    xaxis.tickFormatter = tickFormatter;

for labels case we only want ticks at the label intervals + 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 (i=0; i<numTicks; i++) {
+      for (var i=0; i<numTicks; i++) {
         ticks.push(parseInt(i*increment, 10));
       }
       xaxis.ticks = ticks;
+    } else if (groupFieldIsDateTime) {
+      xaxis.mode = 'time';
     }
 
     var yaxis = {};
@@ -266,31 +269,54 @@
     }
   },
 
+  _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) {
-        var xfield = self.model.fields.get(self.state.attributes.group);
-        var x = doc.getFieldValue(xfield);

time series

        var xtype = xfield.get('type');
-        var isDateTime = (xtype === 'date' || xtype === 'date-time' || xtype  === 'time');
-
-        if (isDateTime) {
-          self.xvaluesAreIndex = true;
-          x = index;
-        } else if (typeof x === 'string') {
-          x = parseFloat(x);
-          if (isNaN(x)) { // assume this is a string label
-            x = index;
-            self.xvaluesAreIndex = true;
-          }
+        if(self.xvaluesAreIndex){
+            var x = index;
+        }else{
+            var x = cast(doc.getFieldValueUnrendered(xfield));
         }
 
         var yfield = self.model.fields.get(field);
-        var y = doc.getFieldValue(yfield);
+        var y = doc.getFieldValueUnrendered(yfield);
 
         if (self.state.attributes.graphType == 'bars') {
           points.push([y, x]);
@@ -368,10 +394,8 @@
 
   initialize: function(options) {
     var self = this;
-    this.el = $(this.el);
     _.bindAll(this, 'render');
-    this.model.fields.bind('reset', this.render);
-    this.model.fields.bind('add', this.render);
+    this.listenTo(this.model.fields, 'reset add', this.render);
     this.state = new recline.Model.ObjectState(options.state);
     this.render();
   },
@@ -380,12 +404,12 @@
     var self = this;
     var tmplData = this.model.toTemplateJSON();
     var htmls = Mustache.render(this.template, tmplData);
-    this.el.html(htmls);

set up editor from state

    if (this.state.get('graphType')) {
+    this.$el.html(htmls);

set up editor from state

    if (this.state.get('graphType')) {
       this._selectOption('.editor-type', this.state.get('graphType'));
     }
     if (this.state.get('group')) {
       this._selectOption('.editor-group', this.state.get('group'));
-    }

ensure at least one series box shows up

    var tmpSeries = [""];
+    }

ensure at least one series box shows up

    var tmpSeries = [""];
     if (this.state.get('series').length > 0) {
       tmpSeries = this.state.get('series');
     }
@@ -394,8 +418,8 @@
       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');
+  },

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) {
@@ -407,19 +431,19 @@
   },
 
   onEditorSubmit: function(e) {
-    var select = this.el.find('.editor-group select');
+    var select = this.$el.find('.editor-group select');
     var $editor = this;
-    var $series  = this.el.find('.editor-series select');
+    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()
+      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.

+ },

Public: Adds a new empty series select box to the editor.

@param [int] idx index of this series in the list of series

@@ -430,14 +454,14 @@ }, this.model.toTemplateJSON()); var htmls = Mustache.render(this.templateSeriesEditor, data); - this.el.find('.editor-series-group').append(htmls); + 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.

+ },

Public: Removes a series list item from the editor.

Also updates the labels of the remaining series elements.

  removeSeries: function (e) {
     e.preventDefault();
diff --git a/docs/src/view.graph.html b/docs/src/view.graph.html
index 4088506ae..bf5b23a5d 100644
--- a/docs/src/view.graph.html
+++ b/docs/src/view.graph.html
@@ -1,4 +1,4 @@
-      view.graph.js           

view.graph.js

this.recline = this.recline || {};
+      view.graph.js           

view.graph.js

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;
diff --git a/docs/src/view.grid.html b/docs/src/view.grid.html
index fc0d31e49..7f1fc32d5 100644
--- a/docs/src/view.grid.html
+++ b/docs/src/view.grid.html
@@ -1,9 +1,10 @@
-      view.grid.js           

view.grid.js

/*jshint multistr:true */
+      view.grid.js           

view.grid.js

/*jshint multistr:true */
 
 this.recline = this.recline || {};
 this.recline.View = this.recline.View || {};
 
-(function($, my) {

(Data) Grid Dataset View

+(function($, my) { + "use strict";

(Data) Grid Dataset View

Provides a tabular view on a Dataset.

@@ -13,11 +14,8 @@ initialize: function(modelEtc) { var self = this; - this.el = $(this.el); _.bindAll(this, 'render', 'onHorizontalScroll'); - this.model.records.bind('add', this.render); - this.model.records.bind('reset', this.render); - this.model.records.bind('remove', this.render); + this.listenTo(this.model.records, 'add reset remove', this.render); this.tempState = {}; var state = _.extend({ hiddenFields: [] @@ -49,7 +47,7 @@ onHorizontalScroll: function(e) { var currentScroll = $(e.target).scrollLeft(); - this.el.find('.recline-grid thead tr').scrollLeft(currentScroll); + this.$el.find('.recline-grid thead tr').scrollLeft(currentScroll); },

======================================================

Templating

  template: ' \
@@ -73,42 +71,43 @@ 

Templating

toTemplateJSON: function() { var self = this; var modelData = this.model.toJSON(); - modelData.notEmpty = ( this.fields.length > 0 );

TODO: move this sort of thing into a toTemplateJSON method on Dataset?

    modelData.fields = _.map(this.fields, function(field) {
+    modelData.notEmpty = ( this.fields.length > 0 );

TODO: move this sort of thing into a toTemplateJSON method on Dataset?

    modelData.fields = 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 = this.model.fields.filter(function(field) {
+    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 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);
-    _.each(this.fields, function(field, idx) {

add the remainder to the first field width so we make up full col

      if (idx === 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 htmls = Mustache.render(this.template, this.toTemplateJSON());
-    this.el.html(htmls);
+    this.$el.html(htmls);
     this.model.records.forEach(function(doc) {
       var tr = $('<tr />');
-      self.el.find('tbody').append(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];
+    });

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('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);
+    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

@@ -138,8 +137,7 @@

Templating

initialize: function(initData) { _.bindAll(this, 'render'); this._fields = initData.fields; - this.el = $(this.el); - this.model.bind('change', this.render); + this.listenTo(this.model, 'change', this.render); }, template: ' \ @@ -172,9 +170,9 @@

Templating

}, render: function() { - this.el.attr('data-id', this.model.id); + this.$el.attr('data-id', this.model.id); var html = Mustache.render(this.template, this.toTemplateJSON()); - $(this.el).html(html); + this.$el.html(html); return this; },

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

  cellEditorTemplate: ' \
@@ -190,7 +188,7 @@ 

Templating

', onEditClick: function(e) { - var editing = this.el.find('.data-table-cell-editor-editor'); + 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"); } diff --git a/docs/src/view.map.html b/docs/src/view.map.html index c37709b7f..d5115b00d 100644 --- a/docs/src/view.map.html +++ b/docs/src/view.map.html @@ -1,15 +1,16 @@ - view.map.js

view.map.js

/*jshint multistr:true */
+      view.map.js           

view.map.js

/*jshint multistr:true */
 
 this.recline = this.recline || {};
 this.recline.View = this.recline.View || {};
 
-(function($, my) {

Map view for a Dataset using Leaflet mapping library.

+(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 +
  2. Via a single field. This field must be either a geo_point or GeoJSON object
  3. Via two fields with latitude and longitude coordinates.
@@ -28,6 +29,8 @@ latField: {id of field containing latitude in the dataset} autoZoom: true, // use cluster support + // cluster: true = always on + // cluster: false = always off cluster: false } @@ -49,7 +52,6 @@ initialize: function(options) { var self = this; - this.el = $(this.el); this.visible = true; this.mapReady = false;

this will be the Leaflet L.Map object (setup below)

    this.map = null;
 
@@ -69,29 +71,29 @@
       singleMarkerMode: false,
       skipDuplicateAddTesting: true,
       animateAddingMarkers: false
-    };

Listen to changes in the fields

    this.model.fields.bind('change', function() {
+    };

Listen to changes in the fields

    this.listenTo(this.model.fields, 'change', function() {
       self._setupGeometryField();
       self.render();
-    });

Listen to changes in the records

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

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.model.records.bind('remove', function(doc){self.redraw('remove',doc);});
-    this.model.records.bind('reset', function(){self.redraw('reset');});
+    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.menu.state.bind('change', function() {
+    this.listenTo(this.menu.state, 'change', function() {
       self.state.set(self.menu.state.toJSON());
       self.redraw();
     });
-    this.state.bind('change', function() {
+    this.listenTo(this.state, 'change', function() {
       self.redraw();
     });
-    this.elSidebar = this.menu.el;
+    this.elSidebar = this.menu.$el;
   },

Customization Functions

The following methods are designed for overriding in order to customize @@ -107,7 +109,7 @@ }

  infobox: function(record) {
     var html = '';
-    for (key in record.attributes){
+    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>';
       }
@@ -142,10 +144,9 @@
 
 

Also sets up the editor fields and the map if necessary.

  render: function() {
     var self = this;
-
-    htmls = Mustache.render(this.template, this.model.toTemplateJSON());
-    $(this.el).html(htmls);
-    this.$map = this.el.find('.panel.map');
+    var htmls = Mustache.render(this.template, this.model.toTemplateJSON());
+    this.$el.html(htmls);
+    this.$map = this.$el.find('.panel.map');
     this.redraw();
     return this;
   },

Public: Redraws the features on the map according to the action provided

@@ -180,13 +181,7 @@ this._add(doc); } else if (action == 'remove' && doc){ this._remove(doc); - }

enable clustering if there is a large number of markers

      var countAfter = 0;
-      this.features.eachLayer(function(){countAfter++;});
-      var sizeIncreased = countAfter - countBefore > 0;
-      if (!this.state.get('cluster') && countAfter > 64 && sizeIncreased) {
-        this.state.set({cluster: true});
-        return;
-      }

this must come before zooming! + }

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);
@@ -204,7 +199,7 @@
     }
   },
 
-  show: function() {

If the div was hidden, Leaflet needs to recalculate some sizes + 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')) {
@@ -221,7 +216,7 @@
 
   _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

+ },

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 @@ -237,10 +232,10 @@ _.every(docs, function(doc){ count += 1; var feature = self._getGeometryFromRecord(doc); - if (typeof feature === 'undefined' || feature === null){

Empty field

        return true;
+      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 + 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
         };
 
@@ -262,20 +257,41 @@
       }
       return true;
     });
-  },

Private: Remove one or n features from the map

  _remove: function(docs){
+  },

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 (key in self.features._layers){
+      for (var key in self.features._layers){
         if (self.features._layers[key].feature.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'));
@@ -283,12 +299,12 @@
           value = $.parseJSON(value);
         } catch(e) {}
       }
-
       if (typeof(value) === 'string') {
         value = value.replace('(', '').replace(')', '');
         var parts = value.split(',');
-        var lat = parseFloat(parts[0]);
-        var lon = parseFloat(parts[1]);
+        var lat = this._parseCoordinateString(parts[0]);
+        var lon = this._parseCoordinateString(parts[1]);
+
         if (!isNaN(lon) && !isNaN(parseFloat(lat))) {
           return {
             "type": "Point",
@@ -308,6 +324,9 @@
       }

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',
@@ -353,8 +372,8 @@
     var self = this;
     this.map = new L.Map(this.$map.get(0));
 
-    var mapUrl = "http://otile{s}.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.png";
-    var osmAttribution = 'Map data &copy; 2011 OpenStreetMap contributors, Tiles Courtesy of <a href="http://www.mapquest.com/" target="_blank">MapQuest</a> <img src="http://developer.mapquest.com/content/osm/mq_logo.png">';
+    var mapUrl = "//otile{s}-s.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.png";
+    var osmAttribution = 'Map data &copy; 2011 OpenStreetMap contributors, Tiles Courtesy of <a href="http://www.mapquest.com/" target="_blank">MapQuest</a> <img src="//developer.mapquest.com/content/osm/mq_logo.png">';
     var bg = new L.TileLayer(mapUrl, {maxZoom: 18, attribution: osmAttribution ,subdomains: '1234'});
     this.map.addLayer(bg);
 
@@ -437,7 +456,6 @@
           Cluster markers</label> \
       </div> \
       <input type="hidden" class="editor-id" value="map-1" /> \
-      </div> \
     </form> \
   ',

Define here events for UI elements

  events: {
     'click .editor-update-map': 'onEditorSubmit',
@@ -448,38 +466,37 @@
 
   initialize: function(options) {
     var self = this;
-    this.el = $(this.el);
     _.bindAll(this, 'render');
-    this.model.fields.bind('change', this.render);
+    this.listenTo(this.model.fields, 'change', this.render);
     this.state = new recline.Model.ObjectState(options.state);
-    this.state.bind('change', this.render);
+    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;
-    htmls = Mustache.render(this.template, this.model.toTemplateJSON());
-    $(this.el).html(htmls);
+    var htmls = Mustache.render(this.template, this.model.toTemplateJSON());
+    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();
+        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();
+        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');
+      this.$el.find('#editor-auto-zoom').attr('checked', 'checked');
     } else {
-      this.el.find('#editor-auto-zoom').removeAttr('checked');
+      this.$el.find('#editor-auto-zoom').removeAttr('checked');
     }
     if (this.state.get('cluster')) {
-      this.el.find('#editor-cluster').attr('checked', 'checked');
+      this.$el.find('#editor-cluster').attr('checked', 'checked');
     } else {
-      this.el.find('#editor-cluster').removeAttr('checked');
+      this.$el.find('#editor-cluster').removeAttr('checked');
     }
     return this;
   },
@@ -491,28 +508,28 @@
 

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')){
+    if (this.$el.find('#editor-field-type-geom').attr('checked')){
       this.state.set({
-        geomField: this.el.find('.editor-geom-field > select > option:selected').val(),
+        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()
+        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();
+        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();
+        this.$el.find('.editor-field-type-geom').hide();
+        this.$el.find('.editor-field-type-latlon').show();
     }
   },
 
@@ -523,7 +540,7 @@
   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');
+    var options = this.$el.find('.' + id + ' > select > option');
     if (options){
       options.each(function(opt){
         if (this.value == value) {
diff --git a/docs/src/view.multiview.html b/docs/src/view.multiview.html
index 0bba6c7f3..a49cd9ab1 100644
--- a/docs/src/view.multiview.html
+++ b/docs/src/view.multiview.html
@@ -1,7 +1,8 @@
-      view.multiview.js           

view.multiview.js

/*jshint multistr:true */

Standard JS module setup

this.recline = this.recline || {};
+      view.multiview.js           

view.multiview.js

/*jshint multistr:true */

Standard JS module setup

this.recline = this.recline || {};
 this.recline.View = this.recline.View || {};
 
-(function($, my) {

MultiView

+(function($, my) { + "use strict";

MultiView

Manage multiple views together along with query editor etc. Usage:

@@ -124,7 +125,6 @@

Parameters

initialize: function(options) { var self = this; - this.el = $(this.el); this._setupState(options.state);

Hash of 'page' views (i.e. those for whole page) keyed by page name

    if (options.views) {
       this.pageViews = options.views;
     } else {
@@ -185,40 +185,36 @@ 

Parameters

} this._showHideSidebar(); - this.model.bind('query:start', function() { - self.notify({loader: true, persist: true}); - }); - this.model.bind('query:done', function() { - self.clearNotifications(); - self.el.find('.doc-count').text(self.model.recordCount || 'Unknown'); - }); - this.model.bind('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 = 'There was an error querying the backend'; + 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 || '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 + ': '; } - self.notify({message: msg, category: 'error', persist: true}); - });

retrieve basic data like fields etc + if (error.message) { + msg += error.message; + } + } else { + msg = '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});
-    this.model.fetch()
-      .fail(function(error) {
-        self.notify({message: error.message, category: 'error', persist: true});
-      });
   },
 
   setReadOnly: function() {
-    this.el.addClass('recline-read-only');
+    this.$el.addClass('recline-read-only');
   },
 
   render: function() {
@@ -226,8 +222,8 @@ 

Parameters

tmplData.views = this.pageViews; tmplData.sidebarViews = this.sidebarViews; var template = Mustache.render(this.template, tmplData); - $(this.el).html(template);

now create and append other views

    var $dataViewContainer = this.el.find('.data-view-container');
-    var $dataSidebar = this.el.find('.data-view-sidebar');

the main views

    _.each(this.pageViews, function(view, pageName) {
+    this.$el.html(template);

now create and append other views

    var $dataViewContainer = this.$el.find('.data-view-container');
+    var $dataSidebar = this.$el.find('.data-view-sidebar');

the main views

    _.each(this.pageViews, function(view, pageName) {
       view.view.render();
       $dataViewContainer.append(view.view.el);
       if (view.view.elSidebar) {
@@ -236,22 +232,34 @@ 

Parameters

}); _.each(this.sidebarViews, function(view) { - this['$'+view.id] = view.view.el; + this['$'+view.id] = view.view.$el; $dataSidebar.append(view.view.el); }, this); - var pager = new recline.View.Pager({ - model: this.model.queryState + this.pager = new recline.View.Pager({ + model: this.model }); - this.el.find('.recline-results-info').after(pager.el); + this.$el.find('.recline-results-info').after(this.pager.el); - var queryEditor = new recline.View.QueryEditor({ + this.queryEditor = new recline.View.QueryEditor({ model: this.model.queryState }); - this.el.find('.query-editor-here').append(queryEditor.el); + 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 $dataSidebar = this.$el.find('.data-view-sidebar');
     var visibleChildren = $dataSidebar.children().filter(function() {
       return $(this).css("display") != "none";
     }).length;
@@ -264,16 +272,16 @@ 

Parameters

}, updateNav: function(pageName) { - this.el.find('.navigation a').removeClass('active'); - var $el = this.el.find('.navigation a[data-view="' + pageName + '"]'); + this.$el.find('.navigation a').removeClass('active'); + var $el = this.$el.find('.navigation a[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();
+        view.view.$el.show();
         if (view.view.elSidebar) {
           view.view.elSidebar.show();
         }
       } else {
-        view.view.el.hide();
+        view.view.$el.hide();
         if (view.view.elSidebar) {
           view.view.elSidebar.hide();
         }
@@ -327,7 +335,7 @@ 

Parameters

}, _bindStateChanges: function() { - var self = this;

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

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

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

    this.listenTo(this.model.queryState, 'change', function() {
       self.state.set({query: self.model.queryState.toJSON()});
     });
     _.each(this.pageViews, function(pageView) {
@@ -335,7 +343,7 @@ 

Parameters

var update = {}; update['view-' + pageView.id] = pageView.view.state.toJSON(); self.state.set(update); - pageView.view.state.bind('change', function() { + 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');
@@ -347,7 +355,7 @@ 

Parameters

_bindFlashNotifications: function() { var self = this; _.each(this.pageViews, function(pageView) { - pageView.view.bind('recline:flash', function(flash) { + self.listenTo(pageView.view, 'recline:flash', function(flash) { self.notify(flash); }); }); @@ -450,7 +458,7 @@

Parameters

} return urlParams; };

Parse the query string out of the URL hash

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

Compse a Query String

my.composeQueryString = function(queryParams) {
   var queryString = '?';
diff --git a/docs/src/view.slickgrid.html b/docs/src/view.slickgrid.html
index 08cab3a1d..110e8ea5d 100644
--- a/docs/src/view.slickgrid.html
+++ b/docs/src/view.slickgrid.html
@@ -1,9 +1,36 @@
-      view.slickgrid.js           

view.slickgrid.js

/*jshint multistr:true */
+      view.slickgrid.js           

view.slickgrid.js

/*jshint multistr:true */
 
 this.recline = this.recline || {};
 this.recline.View = this.recline.View || {};
 
-(function($, my) {

SlickGrid Dataset View

+(function($, my) { + "use strict";

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><a href="#" class="recline-row-add btn">Add row</a></h1>',
+    
+    initialize: function(options){
+      var self = this;
+      _.bindAll(this, 'render');
+      this.state = new recline.Model.ObjectState();
+      this.render();
+    },
+
+    render: function() {
+      var self = this;
+      this.$el.html(this.template)
+    },
+
+    events : {
+      "click .recline-row-add" : "addNewRow"
+    },
+
+    addNewRow : function(e){
+      e.preventDefault()
+      this.state.trigger("change")
+   }
+ }
+ );

SlickGrid Dataset View

Provides a tabular view on a Dataset, based on SlickGrid.

@@ -21,7 +48,11 @@ model: dataset, el: $el, state: { - gridOptions: {editable: true}, + gridOptions: { + editable: true, + enableAddRows: true + ... + }, columnsEditor: [ {column: 'date', editor: Slick.Editors.Date }, {column: 'title', editor: Slick.Editors.Text} @@ -31,14 +62,13 @@ // 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 = $(this.el);
-    this.el.addClass('recline-slickgrid');
-    _.bindAll(this, 'render');
-    this.model.records.bind('add', this.render);
-    this.model.records.bind('reset', this.render);
-    this.model.records.bind('remove', this.render);
-    this.model.records.bind('change', this.onRecordChanged, this);
-
+    this.$el.addClass('recline-slickgrid');
+  

Template for row delete menu , change it if you don't love

    this.templates = {
+   "deleterow" : '<a href="#" class="recline-row-delete btn">X</a>'
+     }
+    _.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: [],
@@ -51,38 +81,67 @@
 
     );
     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())
+      });
+    }
   },
-
-  events: {
-  },
-
-  onRecordChanged: function(record) {

Ignore if the grid is not yet drawn

    if (!this.grid) {
+  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 );
+    }

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() {
+   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 = [];

custom formatter as default one escapes html + }, self.state.get('gridOptions'));

We need all columns, even the hidden ones, to show on the column picker

    var columns = []; 

custom formatter as default one escapes html plus this way we distinguish between rendering/formatting and computed value (so e.g. sort still works ...) row = row index, cell = cell index, value = value, columnDef = column definition, dataContext = full row values

    var formatter = function(row, cell, value, columnDef, dataContext) {
-      var field = self.model.fields.get(columnDef.id);
+      if(columnDef.id == "del"){
+        return self.templates.deleterow 
+  }
+  var field = self.model.fields.get(columnDef.id);
       if (field.renderer) {
-        return field.renderer(value, field, dataContext);
-      } else {
-        return value;
+        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: "A date is required, check field field-date-format"};
+     }else {
+          return {valid: true, msg :null } 
+    }
+  }
+    };

Add row delete support , check if enableDelRow is set to true or not set

    if(this.state.get("gridOptions") 
+  && this.state.get("gridOptions").enabledDelRow != undefined 
+      && this.state.get("gridOptions").enabledDelRow == true ){
+    columns.push({
+        id: 'del',
+        name: 'del',
+        field: 'del',
+        sortable: true,
+        width: 80,
+        formatter: formatter,
+        validator:validator
+    })}
     _.each(this.model.fields.toJSON(),function(field){
       var column = {
         id: field.id,
@@ -90,39 +149,53 @@
         field: field.id,
         sortable: true,
         minWidth: 80,
-        formatter: formatter
+        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 = columns.filter(function(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) {
+    });

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 + }

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) {
+    columns = columns.concat(tempHiddenColumns);

Transform a model object into a row

    function toRow(m) {
       var row = {};
       self.model.fields.each(function(field){
-        row[field.id] = m.getFieldValueUnrendered(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;
     }
@@ -140,11 +213,12 @@
       this.getItem = function(index) {return rows[index];};
       this.getItemMetadata = function(index) {return {};};
       this.getModel = function(index) {return models[index];};
-      this.getModelRow = function(m) {return models.indexOf(m);};
+      this.getModelRow = function(m) {return _.indexOf(models, m);};
       this.updateItem = function(m,i) {
         rows[i] = toRow(m);
         models[i] = m;
       };
+     
     }
 
     var data = new RowSet();
@@ -153,14 +227,14 @@
       data.push(doc, toRow(doc));
     });
 
-    this.grid = new Slick.Grid(this.el, data, visibleColumns, options);

Column sorting

    var sortInfo = this.model.queryState.get('sort');
+    this.grid = new Slick.Grid(this.el, data, visibleColumns, options);

Column sorting

    var sortInfo = this.model.queryState.get('sort');
     if (sortInfo){
       var column = sortInfo[0].field;
       var sortAsc = sortInfo[0].order !== 'desc';
       this.grid.setSortColumn(column, sortAsc);
     }
 
-    this.grid.onSort.subscribe(function(e, args){
+    this._slickHandler.subscribe(this.grid.onSort, function(e, args){
       var order = (args.sortAsc) ? 'asc':'desc';
       var sort = [{
         field: args.sortCol.field,
@@ -169,7 +243,7 @@
       self.model.query({sort: sort});
     });
 
-    this.grid.onColumnsReordered.subscribe(function(e, args){
+    this._slickHandler.subscribe(this.grid.onColumnsReordered, function(e, args){
       self.state.set({columnsOrder: _.pluck(self.grid.getColumns(),'id')});
     });
 
@@ -184,28 +258,38 @@
         });
         self.state.set({columnsWidth:columnsWidth});
     });
-
-    this.grid.onCellChange.subscribe(function (e, args) {

We need to change the model associated value

      var grid = args.grid;
+    
+    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);
-    });
-
-    var columnpicker = new Slick.Controls.ColumnPicker(columns, this.grid,
+    });  
+    this._slickHandler.subscribe(this.grid.onClick,function(e, args){
+      if (args.cell == 0 && 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;
+    } else {

Defer rendering until the view is visible

      self.rendered = false;
     }
 
     return this;
- },
+  },
+
+  remove: function () {
+    this._slickHandler.unsubscribeAll();
+    Backbone.View.prototype.remove.apply(this, arguments);
+  },
 
-  show: function() {

If the div is hidden, SlickGrid will calculate wrongly some + 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();
@@ -336,7 +420,7 @@
       }
     }
     init();
-  }

Slick.Controls.ColumnPicker

  $.extend(true, window, { Slick:{ Controls:{ ColumnPicker:SlickColumnPicker }}});
+  }

Slick.Controls.ColumnPicker

  $.extend(true, window, { Slick:{ Controls:{ ColumnPicker:SlickColumnPicker }}});
 })(jQuery);
 
 
\ No newline at end of file diff --git a/docs/src/view.timeline.html b/docs/src/view.timeline.html index 6f903f8bf..f86f6c452 100644 --- a/docs/src/view.timeline.html +++ b/docs/src/view.timeline.html @@ -1,9 +1,10 @@ - view.timeline.js

view.timeline.js

/*jshint multistr:true */
+      view.timeline.js           

view.timeline.js

/*jshint multistr:true */
 
 this.recline = this.recline || {};
 this.recline.View = this.recline.View || {};
 
-(function($, my) {

turn off unnecessary logging from VMM Timeline

if (typeof VMM !== 'undefined') {
+(function($, my) {
+  "use strict";

turn off unnecessary logging from VMM Timeline

if (typeof VMM !== 'undefined') {
   VMM.debug = false;
 }

Timeline

@@ -19,18 +20,18 @@ initialize: function(options) { var self = this; - this.el = $(this.el); - this.timeline = new VMM.Timeline(); + this.timeline = new VMM.Timeline(this.elementId); this._timelineIsInitialized = false; - this.model.fields.bind('reset', function() { + this.listenTo(this.model.fields, 'reset', function() { self._setupTemporalField(); }); - this.model.records.bind('all', function() { + this.listenTo(this.model.records, 'all', function() { self.reloadData(); }); var stateData = _.extend({ startField: null, - endField: 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
@@ -42,21 +43,22 @@
   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 $ + 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) {
+  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 $timeline = this.el.find(this.elementId);
     var data = this._timelineJSON();
-    this.timeline.init(data, this.elementId, this.state.get("timelineJSOptions"));
+    var config = this.state.get("timelineJSOptions");
+    config.id = this.elementId;
+    this.timeline.init(config, data);
     this._timelineIsInitialized = true
   },
 
@@ -65,11 +67,11 @@
       var data = this._timelineJSON();
       this.timeline.reload(data);
     }
-  },

Convert record to JSON for timeline

+ },

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) {
+  },

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) {
@@ -100,7 +102,7 @@
       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) {
+    });

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!'
@@ -108,21 +110,32 @@
       out.timeline.date.push(tlEntry);
     }
     return out;
-  },
-
-  _parseDate: function(date) {
+  },

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 = date.trim();
+    var out = $.trim(date);
     out = out.replace(/(\d)th/g, '$1');
     out = out.replace(/(\d)st/g, '$1');
-    out = out.trim() ? moment(out) : null;
-    if (out.toDate() == 'Invalid Date') {
-      return null;
-    } else {
-      return out.toDate();
+    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() {
diff --git a/docs/src/widget.facetviewer.html b/docs/src/widget.facetviewer.html
index 57ef47afc..ddc3ab72f 100644
--- a/docs/src/widget.facetviewer.html
+++ b/docs/src/widget.facetviewer.html
@@ -1,9 +1,10 @@
-      widget.facetviewer.js           

widget.facetviewer.js

/*jshint multistr:true */
+      widget.facetviewer.js           

widget.facetviewer.js

/*jshint multistr:true */
 
 this.recline = this.recline || {};
 this.recline.View = this.recline.View || {};
 
-(function($, my) {

FacetViewer

+(function($, my) { + "use strict";

FacetViewer

Widget for displaying facets

@@ -39,9 +40,8 @@ }, initialize: function(model) { _.bindAll(this, 'render'); - this.el = $(this.el); - this.model.facets.bind('all', this.render); - this.model.fields.bind('all', this.render); + this.listenTo(this.model.facets, 'all', this.render); + this.listenTo(this.model.fields, 'all', this.render); this.render(); }, render: function() { @@ -58,15 +58,15 @@ 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();
+    this.$el.html(templated);

are there actually any facets to show?

    if (this.model.facets.length > 0) {
+      this.$el.show();
     } else {
-      this.el.hide();
+      this.$el.hide();
     }
   },
   onHide: function(e) {
     e.preventDefault();
-    this.el.hide();
+    this.$el.hide();
   },
   onFacetFilter: function(e) {
     e.preventDefault();
diff --git a/docs/src/widget.fields.html b/docs/src/widget.fields.html
index c98b3c4ba..d4fe22822 100644
--- a/docs/src/widget.fields.html
+++ b/docs/src/widget.fields.html
@@ -1,4 +1,4 @@
-      widget.fields.js           

widget.fields.js

/*jshint multistr:true */

Field Info

+ widget.fields.js

widget.fields.js

/*jshint multistr:true */

Field Info

For each field

@@ -10,7 +10,8 @@ this.recline.View = this.recline.View || {}; (function($, my) { - + "use strict"; + my.Fields = Backbone.View.extend({ className: 'recline-fields-view', template: ' \ @@ -49,17 +50,16 @@ initialize: function(model) { var self = this; - this.el = $(this.el); _.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.model.fields.bind('reset', function(action) {
+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.$el.find('.collapse').collapse();
     this.render();
   },
   render: function() {
@@ -73,7 +73,7 @@
       tmplData.fields.push(out);
     });
     var templated = Mustache.render(this.template, tmplData);
-    this.el.html(templated);
+    this.$el.html(templated);
   }
 });
 
diff --git a/docs/src/widget.filtereditor.html b/docs/src/widget.filtereditor.html
index 4d0079b34..d4bf97bb9 100644
--- a/docs/src/widget.filtereditor.html
+++ b/docs/src/widget.filtereditor.html
@@ -1,9 +1,10 @@
-      widget.filtereditor.js           

widget.filtereditor.js

/*jshint multistr:true */
+      widget.filtereditor.js           

widget.filtereditor.js

/*jshint multistr:true */
 
 this.recline = this.recline || {};
 this.recline.View = this.recline.View || {};
 
 (function($, my) {
+  "use strict";
 
 my.FilterEditor = Backbone.View.extend({
   className: 'recline-filter-editor well', 
@@ -58,9 +59,9 @@
             <a class="js-remove-filter" href="#" title="Remove this filter" data-filter-id="{{id}}">&times;</a> \
           </legend> \
           <label class="control-label" for="">From</label> \
-          <input type="text" value="{{start}}" name="start" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
+          <input type="text" value="{{from}}" name="from" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
           <label class="control-label" for="">To</label> \
-          <input type="text" value="{{stop}}" name="stop" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
+          <input type="text" value="{{to}}" name="to" data-filter-field="{{field}}" data-filter-id="{{id}}" data-filter-type="{{type}}" /> \
         </fieldset> \
       </div> \
     ',
@@ -88,11 +89,9 @@
     'submit form.js-add': 'onAddFilter'
   },
   initialize: function() {
-    this.el = $(this.el);
     _.bindAll(this, 'render');
-    this.model.fields.bind('all', this.render);
-    this.model.queryState.bind('change', this.render);
-    this.model.queryState.bind('change:filters:new-blank', 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() {
@@ -106,13 +105,13 @@
       return Mustache.render(self.filterTemplates[this.type], this);
     };
     var out = Mustache.render(this.template, tmplData);
-    this.el.html(out);
+    this.$el.html(out);
   },
   onAddFilterShow: function(e) {
     e.preventDefault();
     var $target = $(e.target);
     $target.hide();
-    this.el.find('form.js-add').show();
+    this.$el.find('form.js-add').show();
   },
   onAddFilter: function(e) {
     e.preventDefault();
diff --git a/docs/src/widget.pager.html b/docs/src/widget.pager.html
index 5956b87e4..c03c3d98d 100644
--- a/docs/src/widget.pager.html
+++ b/docs/src/widget.pager.html
@@ -1,9 +1,10 @@
-      widget.pager.js           

widget.pager.js

/*jshint multistr:true */
+      widget.pager.js           

widget.pager.js

/*jshint multistr:true */
 
 this.recline = this.recline || {};
 this.recline.View = this.recline.View || {};
 
 (function($, my) {
+  "use strict";
 
 my.Pager = Backbone.View.extend({
   className: 'recline-pager', 
@@ -24,35 +25,43 @@
 
   initialize: function() {
     _.bindAll(this, 'render');
-    this.el = $(this.el);
-    this.model.bind('change', this.render);
+    this.listenTo(this.model.queryState, 'change', this.render);
     this.render();
   },
   onFormSubmit: function(e) {
     e.preventDefault();
-    var newFrom = parseInt(this.el.find('input[name="from"]').val());
-    var newSize = parseInt(this.el.find('input[name="to"]').val()) - newFrom;
-    newFrom = Math.max(newFrom, 0);
-    newSize = Math.max(newSize, 1);
-    this.model.set({size: newSize, from: newFrom});
+    var newFrom = parseInt(this.$el.find('input[name="from"]').val());
+    newFrom = Math.min(this.model.recordCount, Math.max(newFrom, 1))-1;
+    var newSize = parseInt(this.$el.find('input[name="to"]').val()) - newFrom;
+    newSize = Math.min(Math.max(newSize, 1), this.model.recordCount);
+    this.model.queryState.set({size: newSize, from: newFrom});
   },
   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 = this.model.get('from') - Math.max(0, this.model.get('size'));
+      newFrom = Math.max(currFrom - Math.max(0, size), 1)-1;
+      updateQuery = newFrom != currFrom;
     } else {
-      newFrom = this.model.get('from') + this.model.get('size');
+      newFrom = Math.max(currFrom + size, 1);
+      updateQuery = (newFrom < this.model.recordCount);
+    }
+    if (updateQuery) {
+      this.model.queryState.set({from: newFrom});
     }
-    newFrom = Math.max(newFrom, 0);
-    this.model.set({from: newFrom});
   },
   render: function() {
     var tmplData = this.model.toJSON();
-    tmplData.to = this.model.get('from') + this.model.get('size');
+    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);
+    this.$el.html(templated);
+    return this;
   }
 });
 
diff --git a/docs/src/widget.queryeditor.html b/docs/src/widget.queryeditor.html
index 0684637d7..79262cf0d 100644
--- a/docs/src/widget.queryeditor.html
+++ b/docs/src/widget.queryeditor.html
@@ -1,9 +1,10 @@
-      widget.queryeditor.js           

widget.queryeditor.js

/*jshint multistr:true */
+      widget.queryeditor.js           

widget.queryeditor.js

/*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', 
@@ -23,19 +24,18 @@
 
   initialize: function() {
     _.bindAll(this, 'render');
-    this.el = $(this.el);
-    this.model.bind('change', this.render);
+    this.listenTo(this.model, 'change', this.render);
     this.render();
   },
   onFormSubmit: function(e) {
     e.preventDefault();
-    var query = this.el.find('.text-query input').val();
+    var query = this.$el.find('.text-query input').val();
     this.model.set({q: query});
   },
   render: function() {
     var tmplData = this.model.toJSON();
     var templated = Mustache.render(this.template, tmplData);
-    this.el.html(templated);
+    this.$el.html(templated);
   }
 });