diff --git a/demos/multiview/app.js b/demos/multiview/app.js index 2aec5896..929337c5 100755 --- a/demos/multiview/app.js +++ b/demos/multiview/app.js @@ -78,6 +78,8 @@ var createExplorer = function(dataset, state) { gridOptions: { editable: true, enabledAddRow: true, + enabledDelRow: true, + autoEdit: false, enableCellNavigation: true }, columnsEditor: [ diff --git a/demos/search/demo.search.app.js b/demos/search/demo.search.app.js index 49f5f9b9..9621ffbf 100644 --- a/demos/search/demo.search.app.js +++ b/demos/search/demo.search.app.js @@ -143,7 +143,7 @@ var SearchView = Backbone.View.extend({ this.el.find('.sidebar').append(view.el); var pager = new recline.View.Pager({ - model: this.model.queryState + model: this.model }); this.el.find('.pager-here').append(pager.el); diff --git a/src/backend.memory.js b/src/backend.memory.js index 0c83a7d1..8cb64ddf 100644 --- a/src/backend.memory.js +++ b/src/backend.memory.js @@ -101,6 +101,7 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {}; // register filters var filterFunctions = { term : term, + terms : terms, range : range, geo_distance : geo_distance }; @@ -140,6 +141,14 @@ 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 fromnull = (_.isUndefined(filter.from) || filter.from === null || filter.from === ''); var tonull = (_.isUndefined(filter.to) || filter.to === null || filter.to === ''); diff --git a/src/view.multiview.js b/src/view.multiview.js index ccd35171..918f0e16 100644 --- a/src/view.multiview.js +++ b/src/view.multiview.js @@ -260,7 +260,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); diff --git a/src/view.slickgrid.js b/src/view.slickgrid.js index f7512f8d..cd4f3ce1 100644 --- a/src/view.slickgrid.js +++ b/src/view.slickgrid.js @@ -5,6 +5,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. @@ -39,10 +70,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: [], @@ -55,29 +90,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, @@ -87,18 +125,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, @@ -106,14 +174,13 @@ 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; @@ -137,13 +204,11 @@ my.SlickGrid = Backbone.View.extend({ } } 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){ @@ -163,12 +228,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; } @@ -191,6 +260,7 @@ my.SlickGrid = Backbone.View.extend({ rows[i] = toRow(m); models[i] = m; }; + } var data = new RowSet(); @@ -200,7 +270,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){ @@ -233,19 +302,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){ @@ -402,3 +477,4 @@ my.SlickGrid = Backbone.View.extend({ // Slick.Controls.ColumnPicker $.extend(true, window, { Slick:{ Controls:{ ColumnPicker:SlickColumnPicker }}}); })(jQuery); + diff --git a/src/widget.pager.js b/src/widget.pager.js index b2b5d03d..05e79237 100644 --- a/src/widget.pager.js +++ b/src/widget.pager.js @@ -25,34 +25,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/test/backend.memory.test.js b/test/backend.memory.test.js index 440e7d8d..cbe84ad6 100644 --- a/test/backend.memory.test.js +++ b/test/backend.memory.test.js @@ -99,6 +99,13 @@ test('filters', function () { deepEqual(_.pluck(out.hits, 'country'), ['UK','UK','UK']); }); + query = new recline.Model.Query(); + query.addFilter({type: 'terms', field: 'country', terms: ['UK','DE']}); + data.query(query.toJSON()).then(function(out) { + equal(out.total, 5); + deepEqual(_.pluck(out.hits, 'country'), ['DE','UK','UK','UK','DE']); + }); + query = new recline.Model.Query(); query.addFilter({type: 'range', field: 'date', from: '2011-01-01', to: '2011-05-01'}); data.query(query.toJSON()).then(function(out) { @@ -307,6 +314,13 @@ test('filters', function () { deepEqual(dataset.records.pluck('country'), ['UK', 'UK', 'UK']); }); + dataset = makeBackendDataset(); + dataset.queryState.addFilter({type: 'terms', field: 'country', terms: ['UK','DE']}); + dataset.query().then(function() { + equal(dataset.records.length, 5); + deepEqual(dataset.records.pluck('country'), ['DE','UK', 'UK', 'UK','DE']); + }); + dataset = makeBackendDataset(); dataset.queryState.addFilter({type: 'range', field: 'date', from: '2011-01-01', to: '2011-05-01'}); dataset.query().then(function() { diff --git a/test/index.html b/test/index.html index 1dc22ed8..87698280 100644 --- a/test/index.html +++ b/test/index.html @@ -71,6 +71,7 @@ +

Qunit Tests

diff --git a/test/view.slickgrid.test.js b/test/view.slickgrid.test.js index 597461cd..80c895c6 100644 --- a/test/view.slickgrid.test.js +++ b/test/view.slickgrid.test.js @@ -103,6 +103,77 @@ test('editable', function () { view.remove(); }); +test('delete-row' , function(){ + var dataset = Fixture.getDataset(); + var view = new recline.View.SlickGrid({ + model: dataset, + state: { + hiddenColumns:['x','lat','title'], + columnsOrder:['lon','id','z','date', 'y', 'country'], + columnsWidth:[ + {column:'id',width: 250} + ], + gridOptions: {editable: true , "enabledDelRow":true}, + columnsEditor: [{column: 'country', editor: Slick.Editors.Text}] + } + }); + + $('.fixtures .test-datatable').append(view.el); + view.render(); + view.show(); + old_length = dataset.records.length + dataset.records.on('remove', function(record){ + equal(dataset.records.length, old_length -1 ); + }); + + // Be sure a cell change triggers a change of the model + e = new Slick.EventData(); + view.grid.onClick.notify({ + row: 1, + cell: 0, + grid: view.grid + }, e, view.grid); + + view.remove(); + + +}); + +test('add-row' , function(){ +//To test adding row on slickgrid , we add some menu GridControl +//I am based on the FlotControl in flot wiewer , to add a similary +//to the sclickgrid , The GridControl add a bouton menu +//one the .side-bar place , which will allow to add a row to +//the grid on-click + +var dataset = Fixture.getDataset(); + var view = new recline.View.SlickGrid({ + model: dataset, + state: { + hiddenColumns:['x','lat','title'], + columnsOrder:['lon','id','z','date', 'y', 'country'], + columnsWidth:[ + {column:'id',width: 250} + ], + gridOptions: {editable: true , "enabledAddRow":true}, + columnsEditor: [{column: 'country', editor: Slick.Editors.Text}] + } + }); + +// view will auto render ... +assertPresent('.recline-row-add', view.elSidebar); +// see recline.SlickGrid.GridControl widget +//view.render() +old_length = dataset.records.length +dataset.records.on('add',function(record){ + equal(dataset.records.length ,old_length + 1 ) +}); + +view.elSidebar.find('.recline-row-add').click(); + +}); + + test('update', function() { var dataset = Fixture.getDataset(); var view = new recline.View.SlickGrid({ diff --git a/test/widget.pager.test.js b/test/widget.pager.test.js new file mode 100644 index 00000000..4274fede --- /dev/null +++ b/test/widget.pager.test.js @@ -0,0 +1,99 @@ +module("Widget - Pager"); + +test('basics', function () { + var dataset = Fixture.getDataset(); + var size = dataset.recordCount/2 + 1; + dataset.queryState.set({ size : size }, { silent : true }); + var view = new recline.View.Pager({ + model: dataset + }); + $('.fixtures').append(view.el); + var fromSelector = 'input[name=from]'; + var toSelector = 'input[name=to]'; + + assertPresent('.pagination', view.elSidebar); + // next and prev present + assertPresent('.prev', view.elSidebar); + assertPresent('.next', view.elSidebar); + + // from and to inputs present + assertPresent(fromSelector, view.elSidebar); + assertPresent(toSelector, view.elSidebar); + + // click next: -> reload from+size - recordCount + var prevFromVal = parseInt($(fromSelector).val()); + var prevToVal = parseInt($(toSelector).val()); + view.$el.find('.next a').click(); + equal($(fromSelector).val(), prevFromVal+size); + // to = recordCount since size is more than half of record count + equal($(toSelector).val(), dataset.recordCount); + // UI is 1-based but model is zero-based + equal(dataset.queryState.get('from'), prevFromVal+size-1); + + // click prev -> 1-4, model from=0 + prevFromVal = parseInt($(fromSelector).val()); + prevToVal = parseInt($(toSelector).val()); + view.$el.find('.prev a').click(); + equal($(fromSelector).val(), prevFromVal-size); + equal($(toSelector).val(), prevFromVal-1); + // UI is 1-based but model is zero-based + equal(dataset.queryState.get('from'), prevFromVal-size-1); + + view.remove(); +}); + +test('bounds checking', function () { + var dataset = Fixture.getDataset(); + var size = dataset.recordCount/2 + 1; + dataset.queryState.set({ size : size }, { silent : true }); + var view = new recline.View.Pager({ + model: dataset + }); + $('.fixtures').append(view.el); + var querySpy = sinon.spy(dataset, 'query'); + var fromSelector = 'input[name=from]'; + var toSelector = 'input[name=to]'; + + // click prev on beginning: nothing happens + view.$el.find('.prev a').click(); + equal($(fromSelector).val(), 1); + equal($(toSelector).val(), size); + ok(!dataset.query.called); + + // enter size-1 in from: reloads size-1 - size + var fromVal = size-1; + var toVal = parseInt($(toSelector).val()); + $(fromSelector).val(fromVal).change(); + equal($(fromSelector).val(), fromVal); + equal($(toSelector).val(), toVal); + // UI is 1-based but model is zero-based + equal(dataset.queryState.get('from'), fromVal-1); + + // enter value past the end in from: reloads recordCount - recordCount + fromVal = dataset.recordCount + 10; + $(fromSelector).val(fromVal).change(); + equal($(fromSelector).val(), dataset.recordCount); + equal($(toSelector).val(), dataset.recordCount); + // UI is 1-based but model is zero-based + equal(dataset.queryState.get('from'), dataset.recordCount-1); + + // click next on end -> nothing happens + var queryCalls = querySpy.callCount; + fromVal = parseInt($(fromSelector).val()); + toVal = parseInt($(toSelector).val()); + view.$el.find('.next a').click(); + equal(querySpy.callCount, queryCalls); + equal($(fromSelector).val(), fromVal); + equal($(toSelector).val(), toVal); + + // reset from to 1 + // type value past the end in to: 1-recordCount + fromVal = 1; + toVal = dataset.recordCount + 10; + $(fromSelector).val(fromVal); + $(toSelector).val(toVal).change(); + equal($(fromSelector).val(), 1); + equal($(toSelector).val(), dataset.recordCount); + + view.remove(); +});