diff --git a/Gruntfile.js b/Gruntfile.js index ac2cff85b2..6822d5f41d 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -16,7 +16,7 @@ module.exports = function(grunt) { '<%= grunt.template.today("yyyy-mm-dd") %>\n' + '<%= pkg.homepage ? "* " + pkg.homepage + "\\n" : "" %>' + '* Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>;' + - ' Licensed <%= pkg.license %> */\n', + ' License: <%= pkg.license %> */\n', shell: { options: { @@ -418,12 +418,12 @@ module.exports = function(grunt) { }, scripts: [ '//ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js', // TODO(c0bra): REMOVE! - '//ajax.googleapis.com/ajax/libs/angularjs/1.2.15/angular.js', - '//ajax.googleapis.com/ajax/libs/angularjs/1.2.15/angular-touch.js', - '//ajax.googleapis.com/ajax/libs/angularjs/1.2.15/angular-animate.js', + '//ajax.googleapis.com/ajax/libs/angularjs/1.2.16/angular.js', + '//ajax.googleapis.com/ajax/libs/angularjs/1.2.16/angular-touch.js', + '//ajax.googleapis.com/ajax/libs/angularjs/1.2.16/angular-animate.js', ], hiddenScripts: [ - '//ajax.googleapis.com/ajax/libs/angularjs/1.2.15/angular-animate.js', + '//ajax.googleapis.com/ajax/libs/angularjs/1.2.16/angular-animate.js', 'bower_components/google-code-prettify/src/prettify.js', 'node_modules/marked/lib/marked.js' ], diff --git a/TODO.md b/TODO.md index bca172b9a6..58e1748bb8 100644 --- a/TODO.md +++ b/TODO.md @@ -1,13 +1,24 @@ # TODO -# CURRENT +# CURRENT (row filtering) + +1. [TODO] - Make 'No Rows' message i18n +1. [BUG] - i18n causes an exception if a given value is not present. + 1. I think we need a function that will look for a translation in the current or given language and then return the value for the default language if not present + 1. It could also take a flag and return null if not present + 1. Need to add a test for this... + +1. [TODO] - Does rowSearcher need to allow for custom equality comparators in colDef? +1. [IDEA] - Should RegExps be allowed as search terms? We could test for whether the filter value starts and ends with '/' + +1. [TODO] - Document the autoHide feature for uiGridMenu. Probably need to rename it to hideOnResize 1. [TODO] - Does rowsProcessors make sense for external sorting??? It would be downstream from the rows being added/modified, and would ITSELF be modifying the rows... 1. Would probably be an infinite loop. External sorting needs to be able to hook in further upstream. + 1. Sorting a column prompts a call to refreshRows(). Could we have a hook in there to run BEFORE rowsProcessors? 1. [TODO] - Do rows processors need to be able to modify the count of of rows? As it is the documentation says the count needs to stay the same... but searching would affect that -1. [BUG] - Do we need to validate passed in grid 'id' property to make sure it can be in a CSS rule? 1. [IDEA] - Hook the column menu button into the menu it activates so it can show/hide depending on the number of items it will show. Can we do that? 1. If sorting is enabled or the user / extension has supplied extra menu items, show the menu button. Otherwise don't show it. 1. We'll need a way to separate extension menu items from user menu items so the user doesn't override them. @@ -31,20 +42,20 @@ 1. [NOTE] - Use "-webkit-text-stroke: 0.3px" on icon font to fix jaggies in Chrome on Windows 1. [TODO] - Add a failing test for the IE9-11 column sorting hack (columnSorter.js, line 229) -1. [TODO] - Add row filtering 1. [TODO] - Add notes about browser version support and Angular version support to README.md 1. [TODO] - Add handling for sorting null values with columnDef sortingAlgorithm (PR #940) 1. [TODO] - Currently uiGridColumnMenu uses i18n to create the menu item text on link. If the language is changed, they won't update because they're not bound... # Cleanup +1. [TODO] - Remove commented-out dumps from gridUtil 1. [TODO] - Rename gridUtil to uiGridUtil 1. [TODO] - Rename GridUtil in uiGridBody to gridUtil or the above 1. [TODO] - Move uiGridCell to its own file # Extras -1. Add iit and ddescribe checks as commit hooks + # Native scrolling diff --git a/misc/tutorial/5.1.1_Filtering.ngdoc b/misc/tutorial/5.1.1_Filtering.ngdoc new file mode 100644 index 0000000000..535a99aac5 --- /dev/null +++ b/misc/tutorial/5.1.1_Filtering.ngdoc @@ -0,0 +1,44 @@ +@ngdoc overview +@name Tutorial: Filtering +@description + +UI-Grid allows you to filter rows. Just set the `enableFiltering` flag in your grid options (it is off by default). + +Sorting can be disabled at the column level by setting `enableFiltering: false` in the column def. See the last column below for an example. + +@example + + + var app = angular.module('app', ['ngAnimate', 'ui.grid']); + + app.controller('MainCtrl', ['$scope', '$http', function ($scope, $http) { + $scope.gridOptions = { + enableFiltering: true, + columnDefs: [ + { field: 'name' }, + { field: 'gender' }, + { field: 'company', enableFiltering: false } + ] + }; + + $http.get('/data/100.json') + .success(function(data) { + $scope.gridOptions.data = data; + }); + }]); + + +
+ Click on a column header to open the menu and then filter by that column. (The third column has filtering disabled.) +
+
+
+
+
+ + .grid { + width: 500px; + height: 400px; + } + +
\ No newline at end of file diff --git a/misc/tutorial/5.1_sorting.ngdoc b/misc/tutorial/5.1_sorting.ngdoc index 8a9e4e207c..2ad2f6b102 100644 --- a/misc/tutorial/5.1_sorting.ngdoc +++ b/misc/tutorial/5.1_sorting.ngdoc @@ -2,7 +2,7 @@ @name Tutorial: Sorting @description -UI-Grid allows you to sort rows. Just set the `enableSorting` flag in your grid options. +UI-Grid allows you to sort rows. The feature is on by default. You can set the `enableSorting` flag in your grid options to enable/disable it. Note: You must include ngAnimate in your application if you want the menu to slide up/down, but it's not required. diff --git a/src/font/config.json b/src/font/config.json index 5218dcdf70..4e3667c97a 100644 --- a/src/font/config.json +++ b/src/font/config.json @@ -6,6 +6,12 @@ "units_per_em": 1000, "ascent": 850, "glyphs": [ + { + "uid": "9dd9e835aebe1060ba7190ad2b2ed951", + "css": "search", + "code": 59407, + "src": "fontawesome" + }, { "uid": "5211af474d3a9848f67f945e2ccaf143", "css": "cancel", diff --git a/src/js/core/constants.js b/src/js/core/constants.js index 4be94c00d3..126b5ca0fd 100644 --- a/src/js/core/constants.js +++ b/src/js/core/constants.js @@ -51,6 +51,17 @@ }, ASC: 'asc', DESC: 'desc', + filter: { + STARTS_WITH: 2, + ENDS_WITH: 4, + EXACT: 8, + CONTAINS: 16, + GREATER_THAN: 32, + GREATER_THAN_OR_EQUAL: 64, + LESS_THAN: 128, + LESS_THAN_OR_EQUAL: 256, + NOT_EQUAL: 512 + }, // TODO(c0bra): Create full list of these somehow. NOTE: do any allow a space before or after them? CURRENCY_SYMBOLS: ['ƒ', '$', '£', '$', '¤', '¥', '៛', '₩', '₱', '฿', '₫'] diff --git a/src/js/core/directives/ui-grid-column-menu.js b/src/js/core/directives/ui-grid-column-menu.js index cfec64e37f..5017335882 100644 --- a/src/js/core/directives/ui-grid-column-menu.js +++ b/src/js/core/directives/ui-grid-column-menu.js @@ -41,8 +41,34 @@ angular.module('ui.grid').directive('uiGridColumnMenu', ['$log', '$timeout', '$w return false; } } + + function filterable() { + if (uiGridCtrl.grid.options.enableFiltering && typeof($scope.col) !== 'undefined' && $scope.col && $scope.col.enableFiltering) { + return true; + } + else { + return false; + } + } var defaultMenuItems = [ + // NOTE: disabling this in favor of a little filter text box + // Column filter input + // { + // templateUrl: 'ui-grid/uiGridColumnFilter', + // action: function($event) { + // $event.stopPropagation(); + // $scope.filterColumn($event); + // }, + // cancel: function ($event) { + // $event.stopPropagation(); + + // $scope.col.filter = {}; + // }, + // shown: function () { + // return filterable(); + // } + // }, { title: i18nService.get().sort.ascending, icon: 'ui-grid-icon-sort-alt-up', @@ -199,12 +225,12 @@ angular.module('ui.grid').directive('uiGridColumnMenu', ['$log', '$timeout', '$w $scope.$on('$destroy', $scope.$on(uiGridConstants.events.GRID_SCROLL, function(evt, args) { self.hideMenu(); - if (! $scope.$$phase) { $scope.$apply(); } + // if (! $scope.$$phase) { $scope.$apply(); } })); $scope.$on('$destroy', $scope.$on(uiGridConstants.events.ITEM_DRAGGING, function(evt, args) { self.hideMenu(); - if (! $scope.$$phase) { $scope.$apply(); } + // if (! $scope.$$phase) { $scope.$apply(); } })); $scope.$on('$destroy', function() { diff --git a/src/js/core/directives/ui-grid-header-cell.js b/src/js/core/directives/ui-grid-header-cell.js index f2f49d4e0d..699fb50be5 100644 --- a/src/js/core/directives/ui-grid-header-cell.js +++ b/src/js/core/directives/ui-grid-header-cell.js @@ -27,6 +27,8 @@ angular.module('ui.grid').directive('uiGridHeaderCell', ['$log', '$timeout', '$w // Store a reference to menu element var $colMenu = angular.element( $elm[0].querySelectorAll('.ui-grid-header-cell-menu') ); + var $contentsElm = angular.element( $elm[0].querySelectorAll('.ui-grid-cell-contents') ); + // Figure out whether this column is sortable or not if (uiGridCtrl.grid.options.enableSorting && $scope.col.enableSorting) { $scope.sortable = true; @@ -35,6 +37,13 @@ angular.module('ui.grid').directive('uiGridHeaderCell', ['$log', '$timeout', '$w $scope.sortable = false; } + if (uiGridCtrl.grid.options.enableFiltering && $scope.col.enableFiltering) { + $scope.filterable = true; + } + else { + $scope.filterable = false; + } + function handleClick(evt) { // If the shift key is being held down, add this column to the sort var add = false; @@ -53,7 +62,7 @@ angular.module('ui.grid').directive('uiGridHeaderCell', ['$log', '$timeout', '$w // Long-click (for mobile) var cancelMousedownTimeout; var mousedownStartTime = 0; - $elm.on('mousedown', function(event) { + $contentsElm.on('mousedown', function(event) { if (typeof(event.originalEvent) !== 'undefined' && event.originalEvent !== undefined) { event = event.originalEvent; } @@ -72,7 +81,7 @@ angular.module('ui.grid').directive('uiGridHeaderCell', ['$log', '$timeout', '$w }); }); - $elm.on('mouseup', function () { + $contentsElm.on('mouseup', function () { $timeout.cancel(cancelMousedownTimeout); }); @@ -101,7 +110,7 @@ angular.module('ui.grid').directive('uiGridHeaderCell', ['$log', '$timeout', '$w // If this column is sortable, add a click event handler if ($scope.sortable) { - $elm.on('click', function(evt) { + $contentsElm.on('click', function(evt) { evt.stopPropagation(); $timeout.cancel(cancelMousedownTimeout); @@ -123,6 +132,18 @@ angular.module('ui.grid').directive('uiGridHeaderCell', ['$log', '$timeout', '$w $timeout.cancel(cancelMousedownTimeout); }); } + + if ($scope.filterable) { + $scope.$on('$destroy', $scope.$watch('col.filter.term', function(n, o) { + uiGridCtrl.refreshRows() + .then(function () { + if (uiGridCtrl.prevScrollArgs && uiGridCtrl.prevScrollArgs.y && uiGridCtrl.prevScrollArgs.y.percentage) { + uiGridCtrl.fireScrollingEvent({ y: { percentage: uiGridCtrl.prevScrollArgs.y.percentage } }); + } + // uiGridCtrl.fireEvent('force-vertical-scroll'); + }); + })); + } } }; diff --git a/src/js/core/directives/ui-grid-menu.js b/src/js/core/directives/ui-grid-menu.js index 8b09cc0b19..f2e73ea170 100644 --- a/src/js/core/directives/ui-grid-menu.js +++ b/src/js/core/directives/ui-grid-menu.js @@ -36,7 +36,7 @@ */ angular.module('ui.grid') -.directive('uiGridMenu', ['$log', '$timeout', '$window', '$document', 'gridUtil', function ($log, $timeout, $window, $document, gridUtil) { +.directive('uiGridMenu', ['$log', '$compile', '$timeout', '$window', '$document', 'gridUtil', function ($log, $compile, $timeout, $window, $document, gridUtil) { var uiGridMenu = { priority: 0, scope: { @@ -105,7 +105,7 @@ angular.module('ui.grid') return uiGridMenu; }]) -.directive('uiGridMenuItem', ['$log', function ($log) { +.directive('uiGridMenuItem', ['$log', 'gridUtil', '$compile', 'i18nService', function ($log, gridUtil, $compile, i18nService) { var uiGridMenuItem = { priority: 0, scope: { @@ -114,55 +114,76 @@ angular.module('ui.grid') action: '=', icon: '=', shown: '=', - context: '=' + context: '=', + templateUrl: '=' }, require: ['?^uiGrid', '^uiGridMenu'], templateUrl: 'ui-grid/uiGridMenuItem', replace: true, - link: function ($scope, $elm, $attrs, controllers) { - var uiGridCtrl = controllers[0], - uiGridMenuCtrl = controllers[1]; + compile: function($elm, $attrs) { + return { + pre: function ($scope, $elm, $attrs, controllers) { + var uiGridCtrl = controllers[0], + uiGridMenuCtrl = controllers[1]; + + if ($scope.templateUrl) { + gridUtil.getTemplate($scope.templateUrl) + .then(function (contents) { + var template = angular.element(contents); + + var newElm = $compile(template)($scope); + $elm.replaceWith(newElm); + }); + } + }, + post: function ($scope, $elm, $attrs, controllers) { + var uiGridCtrl = controllers[0], + uiGridMenuCtrl = controllers[1]; + + // TODO(c0bra): validate that shown and active are functions if they're defined. An exception is already thrown above this though + // if (typeof($scope.shown) !== 'undefined' && $scope.shown && typeof($scope.shown) !== 'function') { + // throw new TypeError("$scope.shown is defined but not a function"); + // } + if (typeof($scope.shown) === 'undefined' || $scope.shown === null) { + $scope.shown = function() { return true; }; + } - // TODO(c0bra): validate that shown and active are function if they're defined. An exception is already thrown above this though - // if (typeof($scope.shown) !== 'undefined' && $scope.shown && typeof($scope.shown) !== 'function') { - // throw new TypeError("$scope.shown is defined but not a function"); - // } + $scope.itemShown = function () { + var context = {}; + if ($scope.context) { + context.context = $scope.context; + } - if (typeof($scope.shown) === 'undefined' || $scope.shown === null) { - $scope.shown = function() { return true; }; - } + if (typeof(uiGridCtrl) !== 'undefined' && uiGridCtrl) { + context.grid = uiGridCtrl.grid; + } - $scope.itemShown = function () { - var context = {}; - if ($scope.context) { - context.context = $scope.context; - } + return $scope.shown.call(context); + }; - if (typeof(uiGridCtrl) !== 'undefined' && uiGridCtrl) { - context.grid = uiGridCtrl.grid; - } + $scope.itemAction = function($event) { + $log.debug('itemAction'); + $event.stopPropagation(); - return $scope.shown.call(context); - }; - - $scope.itemAction = function($event) { - $event.stopPropagation(); + if (typeof($scope.action) === 'function') { + var context = {}; - if (typeof($scope.action) === 'function') { - var context = {}; + if ($scope.context) { + context.context = $scope.context; + } - if ($scope.context) { - context.context = $scope.context; - } + // Add the grid to the function call context if the uiGrid controller is present + if (typeof(uiGridCtrl) !== 'undefined' && uiGridCtrl) { + context.grid = uiGridCtrl.grid; + } - // Add the grid to the function call context if the uiGrid controller is present - if (typeof(uiGridCtrl) !== 'undefined' && uiGridCtrl) { - context.grid = uiGridCtrl.grid; - } + $scope.action.call(context, $event); - $scope.action.call(context, $event); + uiGridMenuCtrl.hideMenu(); + } + }; - uiGridMenuCtrl.hideMenu(); + $scope.i18n = i18nService.get(); } }; } diff --git a/src/js/core/directives/ui-grid-viewport.js b/src/js/core/directives/ui-grid-viewport.js index 2c36bd4537..27628d9527 100644 --- a/src/js/core/directives/ui-grid-viewport.js +++ b/src/js/core/directives/ui-grid-viewport.js @@ -5,12 +5,12 @@ function($log, $document, $timeout, uiGridConstants, GridUtil) { return { // priority: 1000, - require: '?^uiGrid', + require: '^uiGrid', scope: false, link: function($scope, $elm, $attrs, uiGridCtrl) { - if (uiGridCtrl === undefined) { - throw new Error('[ui-grid-body] uiGridCtrl is undefined!'); - } + // if (uiGridCtrl === undefined) { + // throw new Error('[ui-grid-body] uiGridCtrl is undefined!'); + // } $elm.on('scroll', function (evt) { var newScrollTop = $elm[0].scrollTop; diff --git a/src/js/core/directives/ui-grid.js b/src/js/core/directives/ui-grid.js index b726fe9548..98bf453417 100644 --- a/src/js/core/directives/ui-grid.js +++ b/src/js/core/directives/ui-grid.js @@ -159,7 +159,7 @@ }; $scope.grid.refreshRows = self.refreshRows = function () { - self.grid.processRowsProcessors(self.grid.rows) + return self.grid.processRowsProcessors(self.grid.rows) .then(function (renderableRows) { self.grid.setVisibleRows(renderableRows); diff --git a/src/js/core/factories/Grid.js b/src/js/core/factories/Grid.js index ff7df9a40a..d4ff889ee0 100644 --- a/src/js/core/factories/Grid.js +++ b/src/js/core/factories/Grid.js @@ -1,7 +1,7 @@ (function(){ angular.module('ui.grid') -.factory('Grid', ['$log', '$q', '$parse', 'gridUtil', 'uiGridConstants', 'GridOptions', 'GridColumn', 'GridRow', 'rowSorter', function($log, $q, $parse, gridUtil, uiGridConstants, GridOptions, GridColumn, GridRow, rowSorter) { +.factory('Grid', ['$log', '$q', '$parse', 'gridUtil', 'uiGridConstants', 'GridOptions', 'GridColumn', 'GridRow', 'rowSorter', 'rowSearcher', function($log, $q, $parse, gridUtil, uiGridConstants, GridOptions, GridColumn, GridRow, rowSorter, rowSearcher) { /** * @ngdoc function @@ -10,8 +10,14 @@ angular.module('ui.grid') * be defined in this class * @param {object} options Object map of options to pass into the grid. An 'id' property is expected. */ - var Grid = function (options) { + var Grid = function Grid(options) { // Get the id out of the options, then remove it + if (typeof(options.id) !== 'undefined' && options.id) { + if (! /^[_a-zA-Z0-9-]+$/.test(options.id)) { + throw new Error("Grid id '" + options.id + '" is invalid. It must follow CSS selector syntax rules.'); + } + } + this.id = options.id; delete options.id; @@ -61,7 +67,7 @@ angular.module('ui.grid') * additional properties to the column. * @param {function(colDef, col, gridOptions)} columnsProcessor function to be called */ - Grid.prototype.registerColumnBuilder = function (columnsProcessor) { + Grid.prototype.registerColumnBuilder = function registerColumnBuilder(columnsProcessor) { this.columnBuilders.push(columnsProcessor); }; @@ -73,7 +79,7 @@ angular.module('ui.grid') * additional properties to the row. * @param {function(colDef, col, gridOptions)} columnsProcessor function to be called */ - Grid.prototype.registerRowBuilder = function (rowBuilder) { + Grid.prototype.registerRowBuilder = function registerRowBuilder(rowBuilder) { this.rowBuilders.push(rowBuilder); }; @@ -84,7 +90,7 @@ angular.module('ui.grid') * @description returns a grid column for the column name * @param {string} name column name */ - Grid.prototype.getColumn = function (name) { + Grid.prototype.getColumn = function getColumn(name) { var columns = this.columns.filter(function (column) { return column.colDef.name === name; }); @@ -99,7 +105,7 @@ angular.module('ui.grid') * columnBuilder to further process the column * @returns {Promise} a promise to load any needed column resources */ - Grid.prototype.buildColumns = function () { + Grid.prototype.buildColumns = function buildColumns() { $log.debug('buildColumns'); var self = this; var builderPromises = []; @@ -132,7 +138,7 @@ angular.module('ui.grid') * @description defaults the name property from field to maintain backwards compatibility with 2.x * validates that name or field is present */ - Grid.prototype.preprocessColDef = function (colDef) { + Grid.prototype.preprocessColDef = function preprocessColDef(colDef) { if (!colDef.field && !colDef.name) { throw new Error('colDef.name or colDef.field property is required'); } @@ -153,7 +159,7 @@ angular.module('ui.grid') * * Rows are identified using the gridOptions.rowEquality function */ - Grid.prototype.modifyRows = function(newRawData) { + Grid.prototype.modifyRows = function modifyRows(newRawData) { var self = this; if (self.rows.length === 0 && newRawData.length > 0) { @@ -182,15 +188,11 @@ angular.module('ui.grid') self.rows.splice( self.rows.indexOf(deletedRows[i] ), 1 ); } } - - // Make a reference copy that we can alter (sort, etc) - // var renderableRows = self.processRowsProcessors(self.rows); + return $q.when(self.processRowsProcessors(self.rows)) .then(function (renderableRows) { return self.setVisibleRows(renderableRows); }); - - // self.setVisibleRows(renderableRows); }; /** @@ -200,7 +202,7 @@ angular.module('ui.grid') * @description adds the newRawData array of rows to the grid and calls all registered * rowBuilders. this keyword will reference the grid */ - Grid.prototype.addRows = function(newRawData) { + Grid.prototype.addRows = function addRows(newRawData) { var self = this; for (var i=0; i < newRawData.length; i++) { @@ -216,7 +218,7 @@ angular.module('ui.grid') * @param {GridRow} gridRow reference to gridRow * @returns {GridRow} the gridRow with all additional behavior added */ - Grid.prototype.processRowBuilders = function(gridRow) { + Grid.prototype.processRowBuilders = function processRowBuilders(gridRow) { var self = this; self.rowBuilders.forEach(function (builder) { @@ -233,7 +235,7 @@ angular.module('ui.grid') * @description registered a styleComputation function * @param {function($scope)} styleComputation function */ - Grid.prototype.registerStyleComputation = function (styleComputationInfo) { + Grid.prototype.registerStyleComputation = function registerStyleComputation(styleComputationInfo) { this.styleComputations.push(styleComputationInfo); }; @@ -274,7 +276,7 @@ angular.module('ui.grid') to alter the set of rows (sorting, etc) as long as the count is not modified. */ - Grid.prototype.registerRowsProcessor = function(processor) { + Grid.prototype.registerRowsProcessor = function registerRowsProcessor(processor) { if (! angular.isFunction(processor)) { throw 'Attempt to register non-function rows processor: ' + processor; } @@ -289,7 +291,7 @@ angular.module('ui.grid') * @param {function(renderableRows)} rows processor function * @description Remove a registered rows processor */ - Grid.prototype.removeRowsProcessor = function(processor) { + Grid.prototype.removeRowsProcessor = function removeRowsProcessor(processor) { var idx = this.rowsProcessors.indexOf(processor); if (typeof(idx) !== 'undefined' && idx !== undefined) { @@ -305,7 +307,7 @@ angular.module('ui.grid') * @param {Array[GridColumn]} The array of columns * @description Run all the registered rows processors on the array of renderable rows */ - Grid.prototype.processRowsProcessors = function(renderableRows) { + Grid.prototype.processRowsProcessors = function processRowsProcessors(renderableRows) { var self = this; // Create a shallow copy of the rows so that we can safely sort them without altering the original grid.rows sort order @@ -349,7 +351,7 @@ angular.module('ui.grid') // Call the processor, passing in the rows to process and the current columns // (note: it's wrapped in $q.when() in case the processor does not return a promise) return $q.when( processor.call(self, renderedRowsToProcess, self.columns) ) - .then(function(processedRows) { + .then(function handleProcessedRows(processedRows) { // Check for errors if (! processedRows) { throw "Processor at index " + i + " did not return a set of renderable rows"; @@ -379,7 +381,7 @@ angular.module('ui.grid') return finished.promise; }; - Grid.prototype.setVisibleRows = function(rows) { + Grid.prototype.setVisibleRows = function setVisibleRows(rows) { var newVisibleRowCache = []; rows.forEach(function (row) { @@ -392,14 +394,14 @@ angular.module('ui.grid') }; - Grid.prototype.setRenderedRows = function (newRows) { + Grid.prototype.setRenderedRows = function setRenderedRows(newRows) { this.renderedRows.length = newRows.length; for (var i = 0; i < newRows.length; i++) { this.renderedRows[i] = newRows[i]; } }; - Grid.prototype.setRenderedColumns = function (newColumns) { + Grid.prototype.setRenderedColumns = function setRenderedColumns(newColumns) { this.renderedColumns.length = newColumns.length; for (var i = 0; i < newColumns.length; i++) { this.renderedColumns[i] = newColumns[i]; @@ -412,7 +414,7 @@ angular.module('ui.grid') * @methodOf ui.grid.class:Grid * @description calls each styleComputation function */ - Grid.prototype.buildStyles = function ($scope) { + Grid.prototype.buildStyles = function buildStyles($scope) { var self = this; self.styleComputations .sort(function(a, b) { @@ -426,11 +428,11 @@ angular.module('ui.grid') }); }; - Grid.prototype.minRowsToRender = function () { + Grid.prototype.minRowsToRender = function minRowsToRender() { return Math.ceil(this.getViewportHeight() / this.options.rowHeight); }; - Grid.prototype.minColumnsToRender = function () { + Grid.prototype.minColumnsToRender = function minColumnsToRender() { var self = this; var viewport = this.getViewportWidth(); @@ -455,7 +457,7 @@ angular.module('ui.grid') return min; }; - Grid.prototype.getBodyHeight = function () { + Grid.prototype.getBodyHeight = function getBodyHeight() { // Start with the viewportHeight var bodyHeight = this.getViewportHeight(); @@ -469,7 +471,7 @@ angular.module('ui.grid') // NOTE: viewport drawable height is the height of the grid minus the header row height (including any border) // TODO(c0bra): account for footer height - Grid.prototype.getViewportHeight = function () { + Grid.prototype.getViewportHeight = function getViewportHeight() { var viewPortHeight = this.gridHeight - this.headerHeight; // Account for native horizontal scrollbar, if present @@ -480,7 +482,7 @@ angular.module('ui.grid') return viewPortHeight; }; - Grid.prototype.getViewportWidth = function () { + Grid.prototype.getViewportWidth = function getViewportWidth() { var viewPortWidth = this.gridWidth; if (typeof(this.verticalScrollbarWidth) !== 'undefined' && this.verticalScrollbarWidth !== undefined && this.verticalScrollbarWidth > 0) { @@ -490,7 +492,7 @@ angular.module('ui.grid') return viewPortWidth; }; - Grid.prototype.getHeaderViewportWidth = function () { + Grid.prototype.getHeaderViewportWidth = function getHeaderViewportWidth() { var viewPortWidth = this.getViewportWidth(); if (typeof(this.verticalScrollbarWidth) !== 'undefined' && this.verticalScrollbarWidth !== undefined && this.verticalScrollbarWidth > 0) { @@ -500,8 +502,20 @@ angular.module('ui.grid') return viewPortWidth; }; - Grid.prototype.getCanvasHeight = function () { - var ret = this.options.rowHeight * this.rows.length; + Grid.prototype.getVisibleRowCount = function getVisibleRowCount() { + // var count = 0; + + // this.rows.forEach(function (row) { + // if (row.visible) { + // count++; + // } + // }); + + return this.visibleRowCache.length; + }; + + Grid.prototype.getCanvasHeight = function getCanvasHeight() { + var ret = this.options.rowHeight * this.getVisibleRowCount(); if (typeof(this.horizontalScrollbarHeight) !== 'undefined' && this.horizontalScrollbarHeight !== undefined && this.horizontalScrollbarHeight > 0) { ret = ret - this.horizontalScrollbarHeight; @@ -510,7 +524,7 @@ angular.module('ui.grid') return ret; }; - Grid.prototype.getCanvasWidth = function () { + Grid.prototype.getCanvasWidth = function getCanvasWidth() { var ret = this.canvasWidth; if (typeof(this.verticalScrollbarWidth) !== 'undefined' && this.verticalScrollbarWidth !== undefined && this.verticalScrollbarWidth > 0) { @@ -520,21 +534,21 @@ angular.module('ui.grid') return ret; }; - Grid.prototype.getTotalRowHeight = function () { - return this.options.rowHeight * this.rows.length; + Grid.prototype.getTotalRowHeight = function getTotalRowHeight() { + return this.options.rowHeight * this.getVisibleRowCount(); }; // Is the grid currently scrolling? - Grid.prototype.isScrolling = function() { + Grid.prototype.isScrolling = function isScrolling() { return this.scrolling ? true : false; }; - Grid.prototype.setScrolling = function(scrolling) { + Grid.prototype.setScrolling = function setScrolling(scrolling) { this.scrolling = scrolling; }; - Grid.prototype.rowSearcher = function rowSearcher(rows) { - var grid = this; + Grid.prototype.searchRows = function searchRows(renderableRows) { + return rowSearcher.search(this, renderableRows, this.columns); }; Grid.prototype.sortByColumn = function sortByColumn(renderableRows) { @@ -552,7 +566,7 @@ angular.module('ui.grid') }; // Reset all sorting on the grid - Grid.prototype.getNextColumnSortPriority = function () { + Grid.prototype.getNextColumnSortPriority = function getNextColumnSortPriority() { var self = this, p = 0; @@ -572,7 +586,7 @@ angular.module('ui.grid') * @description Return the columns that the grid is currently being sorted by * @param {GridColumn} [excludedColumn] Optional GridColumn to exclude from having its sorting reset */ - Grid.prototype.resetColumnSorting = function (excludeCol) { + Grid.prototype.resetColumnSorting = function resetColumnSorting(excludeCol) { var self = this; self.columns.forEach(function (col) { @@ -589,7 +603,7 @@ angular.module('ui.grid') * @description Return the columns that the grid is currently being sorted by * @returns {Array[GridColumn]} An array of GridColumn objects */ - Grid.prototype.getColumnSorting = function() { + Grid.prototype.getColumnSorting = function getColumnSorting() { var self = this; var sortedCols = []; @@ -616,7 +630,7 @@ angular.module('ui.grid') * by this column only * @returns {Promise} A resolved promise that supplies the column. */ - Grid.prototype.sortColumn = function (column, directionOrAdd, add) { + Grid.prototype.sortColumn = function sortColumn(column, directionOrAdd, add) { var self = this, direction = null; diff --git a/src/js/core/factories/GridColumn.js b/src/js/core/factories/GridColumn.js index 7b8eeb88e5..7a5bec913a 100644 --- a/src/js/core/factories/GridColumn.js +++ b/src/js/core/factories/GridColumn.js @@ -39,6 +39,23 @@ angular.module('ui.grid') self.updateColumnDef(colDef); } + GridColumn.prototype.setPropertyOrDefault = function (colDef, propName, defaultValue) { + var self = this; + + // Use the column definition filter if we were passed it + if (typeof(colDef[propName]) !== 'undefined' && colDef[propName]) { + self[propName] = colDef[propName]; + } + // Otherwise use our own if it's set + else if (typeof(self[propName]) !== 'undefined') { + self[propName] = self[propName]; + } + // Default to empty object for the filter + else { + self[propName] = defaultValue ? defaultValue : {}; + } + }; + GridColumn.prototype.updateColumnDef = function(colDef, index) { var self = this; @@ -113,23 +130,29 @@ angular.module('ui.grid') // Turn on sorting by default self.enableSorting = typeof(colDef.enableSorting) !== 'undefined' ? colDef.enableSorting : true; - self.sortingAlgorithm = colDef.sortingAlgorithm; + // Turn on filtering by default (it's disabled by default at the Grid level) + self.enableFiltering = typeof(colDef.enableFiltering) !== 'undefined' ? colDef.enableFiltering : true; + self.menuItems = colDef.menuItems; // Use the column definition sort if we were passed it - if (typeof(colDef.sort) !== 'undefined' && colDef.sort) { - self.sort = colDef.sort; - } - // Otherwise use our own if it's set - else if (typeof(self.sort) !== 'undefined') { - self.sort = self.sort; - } - // Default to empty object for the sort - else { - self.sort = {}; - } + self.setPropertyOrDefault(colDef, 'sort'); + + /* + + self.filters = [ + { + term: 'search term' + condition: uiGridContants.filter.CONTAINS + } + ] + + */ + + self.setPropertyOrDefault(colDef, 'filter'); + self.setPropertyOrDefault(colDef, 'filters', []); }; return GridColumn; diff --git a/src/js/core/factories/GridOptions.js b/src/js/core/factories/GridOptions.js index c7e826f241..d912772876 100644 --- a/src/js/core/factories/GridOptions.js +++ b/src/js/core/factories/GridOptions.js @@ -54,6 +54,9 @@ angular.module('ui.grid') // Sorting on by default this.enableSorting = true; + // Filtering off by default + this.enableFiltering = false; + // Column menu can be used by default this.enableColumnMenu = true; diff --git a/src/js/core/factories/GridRows.js b/src/js/core/factories/GridRow.js similarity index 100% rename from src/js/core/factories/GridRows.js rename to src/js/core/factories/GridRow.js diff --git a/src/js/core/services/gridClassFactory.js b/src/js/core/services/gridClassFactory.js index d604573225..5250bab344 100644 --- a/src/js/core/services/gridClassFactory.js +++ b/src/js/core/services/gridClassFactory.js @@ -43,14 +43,25 @@ grid.registerColumnBuilder(service.defaultColumnBuilder); - grid.registerRowBuilder(grid.rowSearcher); + // Reset all rows to visible initially + grid.registerRowsProcessor(function allRowsVisible(rows) { + rows.forEach(function (row) { + row.visible = true; + }); + + return rows; + }); + + if (grid.options.enableFiltering) { + grid.registerRowsProcessor(grid.searchRows); + } // Register the default row processor, it sorts rows by selected columns - if (!grid.options.externalSort && angular.isFunction) { - grid.registerRowsProcessor(grid.sortByColumn); + if (grid.options.externalSort && angular.isFunction(grid.options.externalSort)) { + grid.registerRowsProcessor(grid.options.externalSort); } else { - grid.registerRowsProcessor(grid.options.externalSort); + grid.registerRowsProcessor(grid.sortByColumn); } return grid; diff --git a/src/js/core/services/rowSearcher.js b/src/js/core/services/rowSearcher.js new file mode 100644 index 0000000000..bc18d2097c --- /dev/null +++ b/src/js/core/services/rowSearcher.js @@ -0,0 +1,378 @@ +(function() { + +var module = angular.module('ui.grid'); + +function escapeRegExp(str) { + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); +} + +function QuickCache() { + var c = function(get, set) { + // Return the cached value of 'get' if it's stored + if (get && c.cache[get]) { + return c.cache[get]; + } + // Otherwise set it and return it + else if (get && set) { + c.cache[get] = set; + return c.cache[get]; + } + else { + return undefined; + } + }; + + c.cache = {}; + + c.clear = function () { + c.cache = {}; + }; + + return c; +} + +/** + * @ngdoc service + * @name ui.grid.service:rowSearcher + * + * @description Service for searching/filtering rows based on column value conditions. + */ +module.service('rowSearcher', ['$log', 'uiGridConstants', function ($log, uiGridConstants) { + var defaultCondition = uiGridConstants.filter.STARTS_WITH; + + var rowSearcher = {}; + + // rowSearcher.searchColumn = function searchColumn(condition, item) { + // var result; + + // var col = self.fieldMap[condition.columnDisplay]; + + // if (!col) { + // return false; + // } + // var sp = col.cellFilter.split(':'); + // var filter = col.cellFilter ? $filter(sp[0]) : null; + // var value = item[condition.column] || item[col.field.split('.')[0]]; + // if (value === null || value === undefined) { + // return false; + // } + // if (typeof filter === "function") { + // var filterResults = filter(typeof value === "object" ? evalObject(value, col.field) : value, sp[1]).toString(); + // result = condition.regex.test(filterResults); + // } + // else { + // result = condition.regex.test(typeof value === "object" ? evalObject(value, col.field).toString() : value.toString()); + // } + // if (result) { + // return true; + // } + // return false; + // }; + + /** + * @ngdoc function + * @name getTerm + * @methodOf ui.grid.service:rowSearcher + * @description Get the term from a filter + * Trims leading and trailing whitespace + * @param {object} filter object to use + * @returns {object} Parsed term + */ + rowSearcher.getTerm = function getTerm(filter) { + if (typeof(filter.term) === 'undefined') { return filter.term; } + + var term = filter.term; + + // Strip leading and trailing whitespace if the term is a string + if (typeof(term) === 'string') { + term = term.trim(); + } + + return term; + }; + + /** + * @ngdoc function + * @name stripTerm + * @methodOf ui.grid.service:rowSearcher + * @description Remove leading and trailing asterisk (*) from the filter's term + * @param {object} filter object to use + * @returns {uiGridConstants.filter} Value representing the condition constant value + */ + rowSearcher.stripTerm = function stripTerm(filter) { + var term = rowSearcher.getTerm(filter); + + if (typeof(term) === 'string') { + return escapeRegExp(term.replace(/(^\*|\*$)/g, '')); + } + else { + return term; + } + }; + + /** + * @ngdoc function + * @name guessCondition + * @methodOf ui.grid.service:rowSearcher + * @description Guess the condition for a filter based on its term + *
+ * Defaults to STARTS_WITH. Uses CONTAINS for strings beginning and ending with *s (*bob*). + * Uses STARTS_WITH for strings ending with * (bo*). Uses ENDS_WITH for strings starting with * (*ob). + * @param {object} filter object to use + * @returns {uiGridConstants.filter} Value representing the condition constant value + */ + rowSearcher.guessCondition = function guessCondition(filter) { + if (typeof(filter.term) === 'undefined' || !filter.term) { + return defaultCondition; + } + + var term = rowSearcher.getTerm(filter); + + // Term starts with and ends with a *, use 'contains' condition + // if (/^\*[\s\S]+?\*$/.test(term)) { + // return uiGridConstants.filter.CONTAINS; + // } + // // Term starts with a *, use 'ends with' condition + // else if (/^\*/.test(term)) { + // return uiGridConstants.filter.ENDS_WITH; + // } + // // Term ends with a *, use 'starts with' condition + // else if (/\*$/.test(term)) { + // return uiGridConstants.filter.STARTS_WITH; + // } + // // Default to default condition + // else { + // return defaultCondition; + // } + + // If the term has *s then turn it into a regex + if (/\*/.test(term)) { + var regexpFlags = ''; + if (!filter.flags || !filter.flags.caseSensitive) { + regexpFlags += 'i'; + } + + var reText = term.replace(/(\\)?\*/g, function ($0, $1) { return $1 ? $0 : '[\\s\\S]*?'; }); + return new RegExp('^' + reText + '$', regexpFlags); + } + // Otherwise default to default condition + else { + return defaultCondition; + } + }; + + rowSearcher.runColumnFilter = function runColumnFilter(grid, row, column, termCache, i, filter) { + // Default to CONTAINS condition + if (typeof(filter.condition) === 'undefined' || !filter.condition) { + filter.condition = uiGridConstants.filter.CONTAINS; + } + + // Term to search for. + var term = rowSearcher.stripTerm(filter); + + if (term === null || term === undefined || term === '') { + return true; + } + + // Get the column value for this row + var value = grid.getCellValue(row, column); + + var regexpFlags = ''; + if (!filter.flags || !filter.flags.caseSensitive) { + regexpFlags += 'i'; + } + + var cacheId = column.field + i; + + // If the filter's condition is a RegExp, then use it + if (filter.condition instanceof RegExp) { + if (! filter.condition.test(value)) { + return false; + } + } + else if (filter.condition === uiGridConstants.filter.STARTS_WITH) { + var startswithRE = termCache(cacheId) ? termCache(cacheId) : termCache(cacheId, new RegExp('^' + term, regexpFlags)); + + if (! startswithRE.test(value)) { + return false; + } + } + else if (filter.condition === uiGridConstants.filter.ENDS_WITH) { + var endswithRE = termCache(cacheId) ? termCache(cacheId) : termCache(cacheId, new RegExp(term + '$', regexpFlags)); + + if (! endswithRE.test(value)) { + return false; + } + } + else if (filter.condition === uiGridConstants.filter.CONTAINS) { + var containsRE = termCache(cacheId) ? termCache(cacheId) : termCache(cacheId, new RegExp(term, regexpFlags)); + + if (! containsRE.test(value)) { + return false; + } + } + else if (filter.condition === uiGridConstants.filter.EXACT) { + var exactRE = termCache(cacheId) ? termCache(cacheId) : termCache(cacheId, new RegExp('^' + term + '$', regexpFlags)); + + if (! exactRE.test(value)) { + return false; + } + } + else if (filter.condition === uiGridConstants.filter.GREATER_THAN) { + if (value <= term) { + return false; + } + } + else if (filter.condition === uiGridConstants.filter.GREATER_THAN_OR_EQUAL) { + if (value < term) { + return false; + } + } + else if (filter.condition === uiGridConstants.filter.LESS_THAN) { + if (value >= term) { + return false; + } + } + else if (filter.condition === uiGridConstants.filter.LESS_THAN_OR_EQUAL) { + if (value > term) { + return false; + } + } + else if (filter.condition === uiGridConstants.filter.NOT_EQUAL) { + if (! angular.equals(value, term)) { + return false; + } + } + + return true; + }; + + /** + * @ngdoc function + * @name searchColumn + * @methodOf ui.grid.service:rowSearcher + * @description Process filters on a given column against a given row. If the row meets the conditions on all the filters, return true. + * @param {Grid} grid Grid to search in + * @param {GridRow} row Row to search on + * @param {GridCol} column Column with the filters to use + * @returns {boolean} Whether the column matches or not. + */ + rowSearcher.searchColumn = function searchColumn(grid, row, column, termCache) { + var filters = []; + + if (typeof(column.filters) !== 'undefined' && column.filters && column.filters.length > 0) { + filters = column.filters; + } + else if (typeof(column.filter) !== 'undefined' && column.filter) { + // Cache custom conditions, building the RegExp takes time + var conditionCacheId = 'cond-' + column.field + '-' + column.filter.term; + var condition = termCache(conditionCacheId) ? termCache(conditionCacheId) : termCache(conditionCacheId, rowSearcher.guessCondition(column.filter)); + + filters[0] = { + term: column.filter.term, + condition: condition, + flags: { + caseSensitive: false + } + }; + } + + for (var i in filters) { + var filter = filters[i]; + + /* + filter: { + term: 'blah', // Search term to search for, could be a string, integer, etc. + condition: uiGridConstants.filter.CONTAINS // Type of match to do. Defaults to CONTAINS (i.e. looking in a string), but could be EXACT, GREATER_THAN, etc. + flags: { // Flags for the conditions + caseSensitive: false // Case-sensitivity defaults to false + } + } + */ + + var ret = rowSearcher.runColumnFilter(grid, row, column, termCache, i, filter); + if (! ret) { + return false; + } + } + + return true; + // } + // else { + // // No filter conditions, default to true + // return true; + // } + }; + + /** + * @ngdoc function + * @name search + * @methodOf ui.grid.service:rowSearcher + * @description Run a search across + * @param {Grid} grid Grid instance to search inside + * @param {Array[GridRow]} rows GridRows to filter + * @param {Array[GridColumn]} columns GridColumns with filters to process + */ + rowSearcher.search = function search(grid, rows, columns) { + // Don't do anything if we weren't passed any rows + if (!rows) { + return; + } + + // Create a term cache + var termCache = new QuickCache(); + + // Build filtered column list + var filterCols = []; + columns.forEach(function (col) { + if (typeof(col.filters) !== 'undefined' && col.filters.length > 0) { + filterCols.push(col); + } + else if (typeof(col.filter) !== 'undefined' && col.filter && typeof(col.filter.term) !== 'undefined' && col.filter.term) { + filterCols.push(col); + } + }); + + if (filterCols.length > 0) { + filterCols.forEach(function foreachFilterCol(col) { + rows.forEach(function foreachRow(row) { + if (! rowSearcher.searchColumn(grid, row, col, termCache)) { + row.visible = false; + } + }); + }); + + // rows.forEach(function (row) { + // var matchesAllColumns = true; + + // for (var i in filterCols) { + // var col = filterCols[i]; + + // if (! rowSearcher.searchColumn(grid, row, col, termCache)) { + // matchesAllColumns = false; + + // // Stop processing other terms + // break; + // } + // } + + // // Row doesn't match all the terms, don't display it + // if (!matchesAllColumns) { + // row.visible = false; + // } + // else { + // row.visible = true; + // } + // }); + } + + // Reset the term cache + termCache.clear(); + + return rows; + }; + + return rowSearcher; +}]); + +})(); \ No newline at end of file diff --git a/src/less/body.less b/src/less/body.less index 4f274c6619..dc7e351f0a 100644 --- a/src/less/body.less +++ b/src/less/body.less @@ -41,4 +41,32 @@ border-bottom-color: @borderColor; border-bottom-style: solid; } +} + +.ui-grid-no-row-overlay { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + margin: 10%; + + .ui-grid-top-panel-background; + .border-radius(@gridBorderRadius); + border: @gridBorderWidth solid @borderColor; + + font-size: 2em; + text-align: center; + + > * { + position: absolute; + display: table; + margin: auto 0; + width: 100%; + top: 0; + bottom: 0; + left: 0; + right: 0; + opacity: 0.66; + } } \ No newline at end of file diff --git a/src/less/header.less b/src/less/header.less index ec5735c9df..6793cd39b5 100644 --- a/src/less/header.less +++ b/src/less/header.less @@ -80,9 +80,8 @@ position: absolute; right: @gridBorderWidth; // So it doesn't overlay the vertical bar top: 0; - bottom: 0; - - .ui-grid-top-panel-background; + // bottom: 0; + // .ui-grid-top-panel-background; .ui-grid-icon-angle-down { vertical-align: sub; @@ -109,4 +108,80 @@ &.ng-hide-remove.ng-hide-remove-active { .transform(translateY(0)); } +} + +// NOTE: not used currently (TODO(c0bra): Remove or use down the road?) +// .ui-grid-column-filter { +// display: table; + +// .input-container { +// position: relative; +// display: table-cell; + +// .column-filter-input { +// margin: 0; +// } + +// .column-filter-cancel-icon-container { +// position: absolute; +// top: 0; +// bottom: 0; +// right: 0; + +// .column-filter-cancel-icon { +// position: absolute; +// top: 50%; +// line-height: 32px; +// margin-top: -16px; +// right: 2px; +// } +// } +// } + +// .button-container { +// display: table-cell; + +// .ui-grid-button { +// margin-left: 4px; +// } +// } +// } + +.ui-grid-filter-container { + padding: 4px 10px; + position: relative; + + .ui-grid-filter-button { + position: absolute; + top: 0; + bottom: 0; + right: 0; + + [class^="ui-grid-icon"].right { + position: absolute; + top: 50%; + line-height: 32px; + margin-top: -16px; + right: 10px; + opacity: 0.66; + + &:hover { + opacity: 1; + } + } + } +} + +input[type="text"].ui-grid-filter-input { + padding: 0; + margin: 0; + border: 0; + width: 100%; + + border: @gridBorderWidth solid @borderColor; + .border-radius(@gridBorderRadius); + + &:hover { + border: @gridBorderWidth solid @borderColor; + } } \ No newline at end of file diff --git a/src/templates/ui-grid/ui-grid-body.html b/src/templates/ui-grid/ui-grid-body.html index 9acbd68772..6a5b2edf5e 100644 --- a/src/templates/ui-grid/ui-grid-body.html +++ b/src/templates/ui-grid/ui-grid-body.html @@ -16,4 +16,8 @@
+ +
+ No Rows +
\ No newline at end of file diff --git a/src/templates/ui-grid/uiGridColumnFilter.html b/src/templates/ui-grid/uiGridColumnFilter.html new file mode 100644 index 0000000000..569797a56e --- /dev/null +++ b/src/templates/ui-grid/uiGridColumnFilter.html @@ -0,0 +1,15 @@ +
  • +
    + + +
    +   +
    +
    + +
    +
    +   +
    +
    +
  • \ No newline at end of file diff --git a/src/templates/ui-grid/uiGridHeaderCell.html b/src/templates/ui-grid/uiGridHeaderCell.html index 72714a77a5..47d2f206e0 100644 --- a/src/templates/ui-grid/uiGridHeaderCell.html +++ b/src/templates/ui-grid/uiGridHeaderCell.html @@ -11,4 +11,12 @@
     
    + +
    + + +
    +   +
    +
    \ No newline at end of file diff --git a/src/templates/ui-grid/uiGridMenu.html b/src/templates/ui-grid/uiGridMenu.html index 5246509793..85bc922e38 100644 --- a/src/templates/ui-grid/uiGridMenu.html +++ b/src/templates/ui-grid/uiGridMenu.html @@ -1,7 +1,15 @@
      -
    • +
    \ No newline at end of file diff --git a/test/unit/core/directives/ui-grid-header-cell.spec.js b/test/unit/core/directives/ui-grid-header-cell.spec.js index a80455c163..39b6dd2f41 100644 --- a/test/unit/core/directives/ui-grid-header-cell.spec.js +++ b/test/unit/core/directives/ui-grid-header-cell.spec.js @@ -44,8 +44,8 @@ describe('uiGridHeaderCell', function () { menu; beforeEach(function () { - headerCell1 = $(grid).find('.ui-grid-header-cell:nth(0)'); - headerCell2 = $(grid).find('.ui-grid-header-cell:nth(1)'); + headerCell1 = $(grid).find('.ui-grid-header-cell:nth(0) .ui-grid-cell-contents'); + headerCell2 = $(grid).find('.ui-grid-header-cell:nth(1) .ui-grid-cell-contents'); menu = $(grid).find('.ui-grid-column-menu .ui-grid-menu-inner'); }); diff --git a/test/unit/core/factories/Grid.spec.js b/test/unit/core/factories/Grid.spec.js index 2bfb8db810..716ff5eaf7 100644 --- a/test/unit/core/factories/Grid.spec.js +++ b/test/unit/core/factories/Grid.spec.js @@ -33,6 +33,14 @@ describe('Grid factory', function () { $scope.$digest(); } + describe('constructor', function() { + it('should throw an exception if the provided id is invalid', function() { + expect(function() { + var grid = new Grid({ id: 'blah blah' }); + }).toThrow(); + }); + }); + describe('row processors', function () { var proc1, proc2, returnedRows; diff --git a/test/unit/core/row-filtering.spec.js b/test/unit/core/row-filtering.spec.js index a610175fd9..ef9eebed90 100644 --- a/test/unit/core/row-filtering.spec.js +++ b/test/unit/core/row-filtering.spec.js @@ -1,6 +1,6 @@ - -describe('row filtering', function() { - var grid, $scope, $compile, recompile; +describe('rowSearcher', function() { + var grid, $scope, $compile, recompile, + rows, columns, rowSearcher, uiGridConstants, filter; var data = [ { "name": "Ethel Price", "gender": "female", "company": "Enersol" }, @@ -11,32 +11,223 @@ describe('row filtering', function() { beforeEach(module('ui.grid')); - beforeEach(inject(function (_$compile_, $rootScope) { + beforeEach(inject(function (_$compile_, $rootScope, _rowSearcher_, Grid, GridRow, GridColumn, _uiGridConstants_) { $scope = $rootScope; - $compile = _$compile_; + rowSearcher = _rowSearcher_; + uiGridConstants = _uiGridConstants_; + + // $compile = _$compile_; + + // $scope.gridOpts = { + // data: data + // }; - $scope.gridOpts = { - data: data - }; + // recompile = function () { + // grid = angular.element('
    '); + // // document.body.appendChild(grid[0]); + // $compile(grid)($scope); + // $scope.$digest(); + // }; + + // recompile(); + + grid = new Grid({ + id: 1, + enableFiltering: true + }); - recompile = function () { - grid = angular.element('
    '); - // document.body.appendChild(grid[0]); - $compile(grid)($scope); - $scope.$digest(); - }; + rows = grid.rows = [ + new GridRow({ name: 'Bill', company: 'Gruber, Inc.' }, 0), + new GridRow({ name: 'Frank', company: 'Foo Co' }, 1) + ]; - recompile(); + columns = grid.columns = [ + new GridColumn({ name: 'name' }, 0), + new GridColumn({ name: 'company' }, 1) + ]; + + filter = null; })); + function setFilter(column, term, condition) { + column.filters = []; + column.filters.push({ + term: term, + condition: condition + }); + } + + function setTermFilter(column, term) { + column.filter = {}; + column.filter.term = term; + } + afterEach(function () { // angular.element(grid).remove(); grid = null; }); - describe('blarg', function () { - it('yargh!', function () { - + describe('guessCondition', function () { + it('should create a RegExp when term ends with a *', function() { + var filter = { term: 'blah*' }; + + var re = new RegExp(/^blah[\s\S]*?$/i); + + expect(rowSearcher.guessCondition(filter)).toEqual(re); + }); + + it('should create a RegExp when term starts with a *', function() { + var filter = { term: '*blah' }; + + var re = new RegExp(/^[\s\S]*?blah$/i); + + expect(rowSearcher.guessCondition(filter)).toEqual(re); + }); + + it('should create a RegExp when term starts and ends with a *', function() { + var filter = { term: '*blah*' }; + + var re = new RegExp(/^[\s\S]*?blah[\s\S]*?$/i); + + expect(rowSearcher.guessCondition(filter)).toEqual(re); + }); + + it('should create a RegExp when term has a * in the middle', function() { + var filter = { term: 'bl*h' }; + + var re = new RegExp(/^bl[\s\S]*?h$/i); + + expect(rowSearcher.guessCondition(filter)).toEqual(re); + }); + + it('should guess STARTS_WITH when term has no *s', function() { + var filter = { term: 'blah' }; + + expect(rowSearcher.guessCondition(filter)).toEqual(uiGridConstants.filter.STARTS_WITH, 'STARTS_WITH'); + }); + + + }); + + describe('getTerm', function() { + it('should return the term from a filter', function () { + var filter = { term: 'bob' }; + + expect(rowSearcher.getTerm(filter)).toEqual('bob'); + }); + + it('should trims strings', function () { + var filter = { term: ' bob ' }; + + expect(rowSearcher.getTerm(filter)).toEqual('bob'); + }); + }); + + describe('stripTerm', function() { + it('should remove leading asterisk ', function () { + var filter = { term: '*bob' }; + + expect(rowSearcher.stripTerm(filter)).toEqual('bob'); + }); + + it('should remove trailing asterisk ', function () { + var filter = { term: 'bob*' }; + + expect(rowSearcher.stripTerm(filter)).toEqual('bob'); + }); + + it('should remove both leading and trailing asterisk ', function () { + var filter = { term: '*bob*' }; + + expect(rowSearcher.stripTerm(filter)).toEqual('bob'); + }); + + it('should remove only one leading and trailing asterisk, and escape the rest', function () { + var filter = { term: '**bob**' }; + + expect(rowSearcher.stripTerm(filter)).toEqual('\\*bob\\*'); + }); + }); + + // TODO(c0bra): add tests for term handling like '< 5', etc. It needs to guess the condition + + describe('with one column filtered', function () { + it('should run the search', function () { + setFilter(columns[0], 'il', uiGridConstants.filter.CONTAINS); + + var ret = rowSearcher.search(grid, rows, columns); + + expect(ret[0].visible).toBe(true); + expect(ret[1].visible).toBe(false); + }); + }); + + describe('with two columns filtered', function () { + it('should run the search', function () { + setFilter(columns[0], 'il', uiGridConstants.filter.CONTAINS); + setFilter(columns[1], 'ub', uiGridConstants.filter.CONTAINS); + + var ret = rowSearcher.search(grid, rows, columns); + + expect(ret[0].visible).toBe(true); + expect(ret[1].visible).toBe(false); + }); + }); + + describe('with one matching term and one failing term set on both columns', function() { + it('should not show the row', function () { + setTermFilter(columns[0], 'Bil'); + setTermFilter(columns[1], 'blargle'); + + rows.splice(1); + + var ret = rowSearcher.search(grid, rows, columns); + + expect(ret[0].visible).toBe(false); + }); + }); + + describe('with a trailing *', function () { + it('needs to match', function () { + setTermFilter(columns[0], 'Bil*'); + + var ret = rowSearcher.search(grid, rows, columns); + + expect(ret[0].visible).toBe(true); + expect(ret[1].visible).toBe(false); + }); + }); + + describe('with a preceding *', function () { + it('needs to match', function () { + setTermFilter(columns[0], '*ll'); + + var ret = rowSearcher.search(grid, rows, columns); + + expect(ret[0].visible).toBe(true); + expect(ret[1].visible).toBe(false); + }); + }); + + describe('with a * inside the term', function () { + it('needs to match', function () { + setTermFilter(columns[0], 'B*ll'); + + var ret = rowSearcher.search(grid, rows, columns); + + expect(ret[0].visible).toBe(true); + expect(ret[1].visible).toBe(false); + }); + }); + + describe('a *', function () { + it('should match zero characters too', function () { + setTermFilter(columns[0], 'Bi*ll'); + + var ret = rowSearcher.search(grid, rows, columns); + + expect(ret[0].visible).toBe(true); + expect(ret[1].visible).toBe(false); }); }); diff --git a/test/unit/core/row-sorting.spec.js b/test/unit/core/row-sorting.spec.js index 88eada9c40..82b907a607 100644 --- a/test/unit/core/row-sorting.spec.js +++ b/test/unit/core/row-sorting.spec.js @@ -254,7 +254,11 @@ describe('rowSorter', function() { returnedRows = newRows; }); - $timeout.flush(); + // Have to flush $timeout once per processor, as they run consecutively + for (var i = 0; i < grid.rowsProcessors.length; i++) { + $timeout.flush(); + } + $scope.$digest(); });