diff --git a/README.md b/README.md index 5d0944b..55b4bb1 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ The reactiveTable helper accepts additional arguments that can be used to config * `useFontAwesome`: Boolean. Whether to use [Font Awesome](http://fortawesome.github.io/Font-Awesome/) for icons. Requires the `fortawesome:fontawesome` package to be installed. Default `true` if `fortawesome:fontawesome` is installed, else `false`. * `enableRegex`: Boolean. Whether to use filter text as a regular expression instead of a regular search term. When true, users won't be able to filter by special characters without escaping them. Default `false`. (Note: Setting this option on the client won't affect server-side filtering - see [Server-side pagination and filtering](#server-side-pagination-and-filtering-beta)) * `noDataTmpl`: Template. Template to render in place of the table when the collection is empty or filtered to 0 rows. Default none (renders table header with no rows). +* `multiColumnSort`: Boolean. Whether to enable sorting with multiple columns based on the order the user clicks them. Default: `true`. * `class`: String. Classes to add to the table element in addition to 'reactive-table'. Default: 'table table-striped table-hover col-sm-12'. * `id`: String. Unique id to add to the table element. Default: generated with [_.uniqueId](http://underscorejs.org/#uniqueId). * `rowClass`: String or function returning a class name. The row element will be passed as first parameter. diff --git a/lib/reactive_table.js b/lib/reactive_table.js index a09603f..54ba43e 100644 --- a/lib/reactive_table.js +++ b/lib/reactive_table.js @@ -35,11 +35,10 @@ var updateHandle = function (set_context) { var options = { skip: currentIndex, - limit: rowsPerPage + limit: rowsPerPage, + sort: getSortQuery(context.fields, context.multiColumnSort) }; - var sortQuery = {}; - - options.sort = getSortQuery(context.fields); + var filters = context.filters.get(); var onReady = function () { @@ -151,6 +150,8 @@ var setup = function () { } context.collection = collection; + context.multiColumnSort = getDefaultTrueSetting('multiColumnSort', this.data); + var fields = this.data.fields || this.data.settings.fields || {}; if (_.keys(fields).length < 1 || (_.keys(fields).length === 1 && @@ -196,6 +197,7 @@ var setup = function () { }; fields = _.map(fields, normalizeField); + context.fields = fields; var visibleFields = []; @@ -389,8 +391,8 @@ Template.reactiveTable.helpers({ 'isPrimarySortField': function () { var parentData = Template.parentData(1); - var primarySortField = getPrimarySortField(parentData.fields); - return primarySortField.fieldId === this.fieldId; + var primarySortField = getPrimarySortField(parentData.fields, parentData.multiColumnSort); + return primarySortField && primarySortField.fieldId === this.fieldId; }, 'isSortable': function () { @@ -435,7 +437,7 @@ Template.reactiveTable.helpers({ } }); } else { - var sortByValue = _.all(this.fields, function (field) { + var sortByValue = _.all(getSortedFields(this.fields, this.multiColumnSort), function (field) { return field.sortByValue || !field.fn; }); var filterQuery = getFilterQuery(getFilterStrings(this.filters.get()), getFilterFields(this.filters.get(), this.fields), {enableRegex: this.enableRegex}); @@ -446,7 +448,7 @@ Template.reactiveTable.helpers({ if (sortByValue) { - var sortQuery = getSortQuery(this.fields); + var sortQuery = getSortQuery(this.fields, this.multiColumnSort); return this.collection.find(filterQuery, { sort: sortQuery, skip: skip, @@ -456,7 +458,7 @@ Template.reactiveTable.helpers({ } else { var rows = this.collection.find(filterQuery).fetch(); - sortedRows = sortWithFunctions(rows, this.fields); + sortedRows = sortWithFunctions(rows, this.fields, this.multiColumnSort); return sortedRows.slice(skip, skip + limit); } @@ -500,7 +502,7 @@ Template.reactiveTable.events({ var template = Template.instance(); var target = $(event.target).is('i') ? $(event.target).parent() : $(event.target); var sortFieldId = target.attr('fieldid'); - changePrimarySort(sortFieldId, template.context.fields); + changePrimarySort(sortFieldId, template.context.fields, template.multiColumnSort); getUpdateHandleForTemplate(template)(template.context); }, diff --git a/lib/sort.js b/lib/sort.js index 4853738..5da4ee9 100644 --- a/lib/sort.js +++ b/lib/sort.js @@ -53,14 +53,26 @@ normalizeSort = function (field, oldField) { field.sortDirection.set(sortDirection); }; -var getSortedFields = function (fields) { - return _.sortBy(fields, function (field) { +getSortedFields = function (fields, multiColumnSort) { + var filteredFields = _.filter(fields, function (field) { + return field.sortOrder.get() < Infinity; + }); + if (!filteredFields.length) { + var firstSortableField = _.find(fields, function (field) { + return _.isUndefined(field.sortable) || field.sortable !== false; + }); + if (firstSortableField) { + filteredFields = [firstSortableField]; + } + } + var sortedFields = _.sortBy(filteredFields, function (field) { return field.sortOrder.get(); }); + return multiColumnSort ? sortedFields : sortedFields.slice(0, 1); } -getSortQuery = function (fields) { - var sortedFields = getSortedFields(fields); +getSortQuery = function (fields, multiColumnSort) { + var sortedFields = getSortedFields(fields, multiColumnSort); var sortQuery = {}; _.each(sortedFields, function (field) { sortQuery[field.key] = field.sortDirection.get(); @@ -68,8 +80,8 @@ getSortQuery = function (fields) { return sortQuery; }; -sortWithFunctions = function (rows, fields) { - var sortedFields = getSortedFields(fields); +sortWithFunctions = function (rows, fields, multiColumnSort) { + var sortedFields = getSortedFields(fields, multiColumnSort); var sortedRows = rows; _.each(sortedFields.reverse(), function (field) { @@ -87,20 +99,13 @@ sortWithFunctions = function (rows, fields) { return sortedRows; }; -getPrimarySortField = function (fields) { - var minSortField = _.min(fields, function (field) { - return field.sortOrder.get(); - }); - // if all values are Infinity _.min returns Infinity instead of the object - if (minSortField === Infinity) { - minSortField = fields[0]; - } - return minSortField; +getPrimarySortField = function (fields, multiColumnSort) { + return getSortedFields(fields, multiColumnSort)[0]; }; -changePrimarySort = function(fieldId, fields) { - var primarySortField = getPrimarySortField(fields); - if (primarySortField.fieldId === fieldId) { +changePrimarySort = function(fieldId, fields, multiColumnSort) { + var primarySortField = getPrimarySortField(fields, multiColumnSort); + if (primarySortField && primarySortField.fieldId === fieldId) { var sortDirection = -1 * primarySortField.sortDirection.get(); primarySortField.sortDirection.set(sortDirection); primarySortField.sortOrder.set(0); @@ -108,6 +113,9 @@ changePrimarySort = function(fieldId, fields) { _.each(fields, function (field) { if (field.fieldId === fieldId) { field.sortOrder.set(0); + if (primarySortField) { + field.sortDirection.set(primarySortField.sortDirection.get()); + } } else { var sortOrder = 1 + field.sortOrder.get(); field.sortOrder.set(sortOrder); diff --git a/package.js b/package.js index b6e1b2f..5984b6e 100644 --- a/package.js +++ b/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "A reactive table designed for Meteor", - version: "0.8.0", + version: "0.8.1", name: "aslagle:reactive-table", git: "https://github.com/aslagle/reactive-table.git" }); diff --git a/test/test_fields.js b/test/test_fields.js index 164d132..1a25d7e 100644 --- a/test/test_fields.js +++ b/test/test_fields.js @@ -471,6 +471,52 @@ testAsyncMulti('Fields - sortDirection ReactiveVar', [function (test, expect) { nameDirection.set(-1); Meteor.setTimeout(expectDescending, 0); }]); + +Tinytest.add('Fields - default sort', function (test) { + testTable( + { + collection: rows, + fields: [ + {key: 'name', label: 'Name'}, + {key: 'score', label: 'Score'} + ] + }, + function () { + test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Ada Lovelace", "sort should be ascending by first column"); + test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Carl Friedrich Gauss", "sort should be ascending by first column"); + test.equal($('.reactive-table tbody tr:nth-child(6) td:first-child').text(), "Nikola Tesla", "sort should be ascending by first column"); + } + ); + + testTable( + { + collection: rows, + fields: [ + {key: 'name', label: 'Name', sortable: false}, + {key: 'score', label: 'Score'} + ] + }, + function () { + test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Carl Friedrich Gauss", "sort should be ascending by second column"); + test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Ada Lovelace", "sort should be ascending by second column"); + test.equal($('.reactive-table tbody tr:nth-child(6) td:first-child').text(), "Nikola Tesla", "sort should be ascending by second column"); + } + ); + + testTable( + { + collection: rows, + fields: [ + {key: 'name', label: 'Name', sortable: false}, + {key: 'score', label: 'Score', sortable: false} + ] + }, + function () { + test.length($('.reactive-table tbody tr'), 6, "rendered table should have 6 rows"); + } + ); +}); + Tinytest.add('Fields - default sort DEPRECATED', function (test) { _.each(['descending', 'desc', -1], function (sort) { testTable( diff --git a/test/test_sorting.js b/test/test_sorting.js index 452bf62..48526f8 100644 --- a/test/test_sorting.js +++ b/test/test_sorting.js @@ -41,14 +41,14 @@ testAsyncMulti('Sorting - column', [function (test, expect) { var expectSecondColumnDescending = expect(function () { test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Nikola Tesla", "2nd column descending first row"); test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Grace Hopper", "2nd column descending second row"); - test.equal($('.reactive-table tbody tr:nth-child(4) td:first-child').text(), "Claude Shannon", "2nd column descending fourth row"); + test.equal($('.reactive-table tbody tr:nth-child(4) td:first-child').text(), "Marie Curie", "2nd column descending fourth row"); Blaze.remove(table); }); var expectSecondColumn = expect(function () { test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Carl Friedrich Gauss", "2nd column first row"); test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Ada Lovelace", "2nd column second row"); - test.equal($('.reactive-table tbody tr:nth-child(4) td:first-child').text(), "Marie Curie", "2nd column fourth row"); + test.equal($('.reactive-table tbody tr:nth-child(4) td:first-child').text(), "Claude Shannon", "2nd column fourth row"); $('.reactive-table th:nth-child(2)').click(); Meteor.setTimeout(expectSecondColumnDescending, 0); @@ -151,10 +151,10 @@ testAsyncMulti('Sorting - server-side', [function (test, expect) { test.equal($('.reactive-table tbody tr:nth-child(4) td:first-child').text(), "Grace Hopper", "initial fourth row"); $('.reactive-table th:first-child').click(); - Meteor.setTimeout(expectDescending, 500); + Meteor.setTimeout(expectDescending, 1000); }); - Meteor.setTimeout(expectDefaultAscending, 500); + Meteor.setTimeout(expectDefaultAscending, 1000); }]); testAsyncMulti('Sorting - virtual columns', [function (test, expect) { @@ -192,3 +192,81 @@ testAsyncMulti('Sorting - virtual columns', [function (test, expect) { $('.reactive-table th:nth-child(2)').click(); Meteor.setTimeout(expectNoChange, 0); }]); + +testAsyncMulti('Sorting - multi-column', [function (test, expect) { + var table = Blaze.renderWithData( + Template.reactiveTable, + {collection: collection}, + document.body + ); + test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Ada Lovelace", "initial first row"); + test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Carl Friedrich Gauss", "initial second row"); + test.equal($('.reactive-table tbody tr:nth-child(4) td:first-child').text(), "Grace Hopper", "initial fourth row"); + + var expectSecondColumnAscending = expect(function () { + test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Carl Friedrich Gauss", "2nd column first row"); + test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Marie Curie", "2nd column second row"); + test.equal($('.reactive-table tbody tr:nth-child(4) td:first-child').text(), "Ada Lovelace", "2nd column fourth row"); + Blaze.remove(table); + }); + + var expectSecondColumnDescending = expect(function () { + test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Nikola Tesla", "2nd column descending first row"); + test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Grace Hopper", "2nd column descending second row"); + test.equal($('.reactive-table tbody tr:nth-child(4) td:first-child').text(), "Claude Shannon", "2nd column descending fourth row"); + + $('.reactive-table th:nth-child(2)').click(); + Meteor.setTimeout(expectSecondColumnAscending, 0); + }); + + var expectFirstColumnDescending = expect(function () { + test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Nikola Tesla", "first column descending first row"); + test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Marie Curie", "first column descending second row"); + test.equal($('.reactive-table tbody tr:nth-child(4) td:first-child').text(), "Claude Shannon", "first column descending fourth row"); + + $('.reactive-table th:nth-child(2)').click(); + Meteor.setTimeout(expectSecondColumnDescending, 0); + }); + + $('.reactive-table th:first-child').click(); + Meteor.setTimeout(expectFirstColumnDescending, 0); +}]); + +testAsyncMulti('Sorting - multi-column disabled', [function (test, expect) { + var table = Blaze.renderWithData( + Template.reactiveTable, + {collection: collection, multiColumnSort: false}, + document.body + ); + test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Ada Lovelace", "initial first row"); + test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Carl Friedrich Gauss", "initial second row"); + test.equal($('.reactive-table tbody tr:nth-child(4) td:first-child').text(), "Grace Hopper", "initial fourth row"); + + var expectSecondColumnAscending = expect(function () { + test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Carl Friedrich Gauss", "2nd column first row"); + test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Ada Lovelace", "2nd column second row"); + test.equal($('.reactive-table tbody tr:nth-child(4) td:first-child').text(), "Claude Shannon", "2nd column fourth row"); + Blaze.remove(table); + }); + + var expectSecondColumnDescending = expect(function () { + test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Nikola Tesla", "2nd column descending first row"); + test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Grace Hopper", "2nd column descending second row"); + test.equal($('.reactive-table tbody tr:nth-child(4) td:first-child').text(), "Marie Curie", "2nd column descending fourth row"); + + $('.reactive-table th:nth-child(2)').click(); + Meteor.setTimeout(expectSecondColumnAscending, 0); + }); + + var expectFirstColumnDescending = expect(function () { + test.equal($('.reactive-table tbody tr:first-child td:first-child').text(), "Nikola Tesla", "first column descending first row"); + test.equal($('.reactive-table tbody tr:nth-child(2) td:first-child').text(), "Marie Curie", "first column descending second row"); + test.equal($('.reactive-table tbody tr:nth-child(4) td:first-child').text(), "Claude Shannon", "first column descending fourth row"); + + $('.reactive-table th:nth-child(2)').click(); + Meteor.setTimeout(expectSecondColumnDescending, 0); + }); + + $('.reactive-table th:first-child').click(); + Meteor.setTimeout(expectFirstColumnDescending, 0); +}]);