diff --git a/Grid.js b/Grid.js index cb229a55f..1b5857f95 100644 --- a/Grid.js +++ b/Grid.js @@ -218,16 +218,11 @@ function(kernel, declare, listen, has, put, List, miscUtil){ // respond to click, space keypress, or enter keypress if(event.type == "click" || event.keyCode == 32 /* space bar */ || (!has("opera") && event.keyCode == 13) /* enter */){ var target = event.target, - field, sort, newSort, eventObj; + newSort, eventObj; do{ if(target.sortable){ - // If the click is on the same column as the active sort, - // reverse sort direction - newSort = [{ - attribute: (field = target.field || target.columnId), - descending: (sort = grid._sort[0]) && sort.attribute == field && - !sort.descending - }]; + // Construct a new sort based on the targeted column + newSort = grid._constructSort(target); // Emit an event with the new sort eventObj = { @@ -287,6 +282,22 @@ function(kernel, declare, listen, has, put, List, miscUtil){ this.inherited(arguments); }, + + _constructSort: function(columnHeader){ + // summary: + // Construct a new sort array from the given column header node. This + // will be called after the header is clicked *if* the column in + // question is sortable. + + var field, sort; + // If the click is on the same column as the active sort, + // reverse sort direction + return [{ + attribute: (field = columnHeader.field || columnHeader.columnId), + descending: (sort = this._sort[0]) && sort.attribute == field && + !sort.descending + }]; + }, _setSort: function(property, descending){ // summary: diff --git a/extensions/NestedSort.js b/extensions/NestedSort.js new file mode 100644 index 000000000..c22f39f95 --- /dev/null +++ b/extensions/NestedSort.js @@ -0,0 +1,53 @@ +define(["dojo/_base/declare", "dojo/_base/lang"], +function(declare, lang){ +/* + * Nested Sort plugin for dgrid + * + * A plugin that allows your grid to have multi-column sorts, thus giving users + * the experience of stable sorting. The behaviors are: + * + * 1. Clicking on a header that's already at the start of the sort list: + * Toggles 'descending' for that column. + * 2. Clicking on a header that's not in the sort list: + * Add it to the start of the sort list, ascending. + * 3. Clicking on a header that's down in the sort list: + * Move it to the front of the list without changing 'descending'. + * + */ + + return declare(null, { + // sortDepthLimit: Integer + // The maximum nested sort depth. The default of 'null' means 'no limit'. + sortDepthLimit: null, + + _constructSort: function (columnHeader) { + // summary: + // Construct a new sort array based on the given column header node. + // Instead of always creating an array of one element like Grid.js, + // this implementation maintains a stable sort with multiple elements. + + var sort = lang.clone(this.get('sort') || []); + var field = columnHeader.field || columnHeader.columnId; + for (var i = 0; i < sort.length; i++) { + var col = sort[i]; + if (col.attribute === field) { + if (i === 0) { + // If the old one was already at the top, toggle descending. + col.descending = !col.descending; + } else { + sort.splice(i, 1); // remove from middle + sort.splice(0, 0, col); // add to the start + } + break; + } + } + if (i >= sort.length) { + sort.splice(0, 0, { attribute: field, descending: false }); + } + if (this.sortDepthLimit && sort.length > this.sortDepthLimit) { + sort.splice(this.sortDepthLimit); + } + return sort; + } + }); +}); diff --git a/test/intern/all.js b/test/intern/all.js index ecd96205e..3fabac2d0 100644 --- a/test/intern/all.js +++ b/test/intern/all.js @@ -4,7 +4,8 @@ define([ 'intern/node_modules/dojo/has!host-browser?./core/setClass', 'intern/node_modules/dojo/has!host-browser?./mixins/Keyboard', 'intern/node_modules/dojo/has!host-browser?./mixins/Selection', + 'intern/node_modules/dojo/has!host-browser?./mixins/extensions/NestedSort', 'intern/node_modules/dojo/has!host-browser?./core/stores', 'intern/node_modules/dojo/has!host-browser?./core/_StoreMixin', 'intern/node_modules/dojo/has!host-browser?./core/OnDemand-removeRow' -], function(){}); \ No newline at end of file +], function(){}); diff --git a/test/intern/mixins/extensions/NestedSort.js b/test/intern/mixins/extensions/NestedSort.js new file mode 100644 index 000000000..a388b0b01 --- /dev/null +++ b/test/intern/mixins/extensions/NestedSort.js @@ -0,0 +1,140 @@ +define([ + "intern!tdd", + "intern/chai!assert", + "dgrid/Grid", + "dgrid/extensions/NestedSort", + "dojo/_base/declare", + "dgrid/test/data/base" +], function(test, assert, Grid, NestedSort, declare){ + var columns = { + col1: "Column 1", + col3: "Column 3", + col5: "Column 5" + }, + grid; + + test.suite("NestedSort (Grid)", function(){ + test.before(function(){ + grid = new (declare([Grid, NestedSort]))({ + columns: columns, + sort: "id", + store: testStore + }); + document.body.appendChild(grid.domNode); + grid.startup(); + }); + + test.after(function(){ + grid.destroy(); + }); + + test.beforeEach(function(){ + grid.set("sort", "id"); + grid.set("sortDepthLimit", null); + }); + + test.test("grid.sort unaffected", function(){ + assert.deepEqual(grid.get("sort"), [ + { attribute: "id", descending: undefined } + ], "default sort did not return expected value"); + + grid.set("sort", "col1"); + assert.deepEqual(grid.get("sort"), [ + { attribute: "col1", descending: undefined } + ], "sort of one element did not return expected value"); + + var sort = [ + { attribute: "id", descending: true }, + { attribute: "col1", descending: false } + ]; + grid.set("sort", sort); + assert.deepEqual(grid.get("sort"), sort, + "sort of multiple elements did not return expected value"); + }); + + test.test("column sorting - insert new", function(){ + grid.columns["col1"].headerNode.click(); + assert.deepEqual(grid.get("sort"), [ + { attribute: "col1", descending: false }, + { attribute: "id", descending: undefined } + ], "sort incorrect after one column click"); + + grid.columns["col5"].headerNode.click(); + assert.deepEqual(grid.get("sort"), [ + { attribute: "col5", descending: false }, + { attribute: "col1", descending: false }, + { attribute: "id", descending: undefined } + ], "sort incorrect after two column clicks"); + }); + + test.test("column sorting - toggle first", function(){ + grid.set("sort", [ + { attribute: "col1", descending: undefined }, + { attribute: "col5", descending: false } + ]); + + grid.columns["col1"].headerNode.click(); + assert.deepEqual(grid.get("sort"), [ + { attribute: "col1", descending: true }, + { attribute: "col5", descending: false } + ], "sort not changed to descending"); + + grid.columns["col1"].headerNode.click(); + assert.deepEqual(grid.get("sort"), [ + { attribute: "col1", descending: false }, + { attribute: "col5", descending: false } + ], "sort not changed to ascending"); + }); + + test.test("column sorting - move to front", function(){ + grid.set("sort", [ + { attribute: "col1", descending: undefined }, + { attribute: "col3", descending: true }, + { attribute: "col5", descending: false } + ]); + + grid.columns["col3"].headerNode.click(); + assert.deepEqual(grid.get("sort"), [ + { attribute: "col3", descending: true }, + { attribute: "col1", descending: undefined }, + { attribute: "col5", descending: false } + ], "sort not changed to col3"); + + grid.columns["col5"].headerNode.click(); + assert.deepEqual(grid.get("sort"), [ + { attribute: "col5", descending: false }, + { attribute: "col3", descending: true }, + { attribute: "col1", descending: undefined } + ], "sort not changed to col5"); + + grid.columns["col1"].headerNode.click(); + assert.deepEqual(grid.get("sort"), [ + { attribute: "col1", descending: undefined }, + { attribute: "col5", descending: false }, + { attribute: "col3", descending: true } + ], "sort not changed to col1"); + }); + + test.test("column sorting - sortDepthLimit", function(){ + grid.set("sortDepthLimit", 2); + + grid.columns["col1"].headerNode.click(); + assert.deepEqual(grid.get("sort"), [ + { attribute: "col1", descending: false }, + { attribute: "id", descending: undefined } + ], "sort incorrect after one column click"); + + grid.columns["col5"].headerNode.click(); + assert.deepEqual(grid.get("sort"), [ + { attribute: "col5", descending: false }, + { attribute: "col1", descending: false } + ], "sort incorrect after two column clicks"); + + grid.columns["col3"].headerNode.click(); + assert.deepEqual(grid.get("sort"), [ + { attribute: "col3", descending: false }, + { attribute: "col5", descending: false } + ], "sort incorrect after three column clicks"); + }); + }); +});