From d9bba4537ee71b9fc2c97576903e9c9cabb63701 Mon Sep 17 00:00:00 2001 From: webiks Date: Sat, 1 Aug 2015 23:31:15 +0300 Subject: [PATCH] Added "columnsFilters" feature Columns filters is a feature that enables the user to add a filter button to the column header. This button opens a popup with a filter form. This extends the regular filtering by ui-grid by enabling an "or" logic to filter by. Fixed a bug with service.clear The clear function would have thrown an error when clearing a non array filter.term property. Added "dynamic selectOptions" Now also clears predefined filters Fixed the dynamic select change Added feature tutorial Debugged columnsFeatures unit tests Fixed indentation in select filter template Exposing a "filter" function to the API Exposing a filter function to the grid's API, that allows to programmatically filter each column according to various parameters. Sorted functions into the API Added more unit tests Changed "dd" and "ii" in tests Added "$scope.digest()" to filter test Remove "dd" and "iit" from tests Fixed unit tests Set utils back to original Fix grunt utils again... Undo local changes --- lib/grunt/utils.js | 2 +- misc/tutorial/218_columns_filters.ngdoc | 236 +++++++++ .../uiColumnFilter/js/column-filter.js | 500 ++++++++++++++++++ .../uiColumnFilter/less/uiColumnsFilters.less | 17 + .../templates/filterButton.html | 15 + .../uiColumnFilter/templates/filterPopup.html | 16 + .../templates/filters/dateColumnFilter.html | 41 ++ .../templates/filters/numberColumnFilter.html | 42 ++ .../templates/filters/selectColumnFilter.html | 17 + .../templates/filters/stringColumnFilter.html | 14 + .../test/uiColumnsFilters.spec.js | 181 +++++++ test/karma.debug.conf.js | 8 +- 12 files changed, 1084 insertions(+), 5 deletions(-) create mode 100644 misc/tutorial/218_columns_filters.ngdoc create mode 100644 src/features/uiColumnFilter/js/column-filter.js create mode 100644 src/features/uiColumnFilter/less/uiColumnsFilters.less create mode 100644 src/features/uiColumnFilter/templates/filterButton.html create mode 100644 src/features/uiColumnFilter/templates/filterPopup.html create mode 100644 src/features/uiColumnFilter/templates/filters/dateColumnFilter.html create mode 100644 src/features/uiColumnFilter/templates/filters/numberColumnFilter.html create mode 100644 src/features/uiColumnFilter/templates/filters/selectColumnFilter.html create mode 100644 src/features/uiColumnFilter/templates/filters/stringColumnFilter.html create mode 100644 src/features/uiColumnFilter/test/uiColumnsFilters.spec.js diff --git a/lib/grunt/utils.js b/lib/grunt/utils.js index edc8ef1175..189de442bf 100644 --- a/lib/grunt/utils.js +++ b/lib/grunt/utils.js @@ -5,7 +5,7 @@ var semver = require('semver'); var shell = require('shelljs'); // Get the list of angular files (angular.js, angular-mocks.js, etc) -var cachedAngularFiles = grunt.file.readJSON('lib/test/angular/files.json'); +var cachedAngularFiles = grunt.file.readJSON('/lib/test/angular/files.json'); var util = module.exports = { diff --git a/misc/tutorial/218_columns_filters.ngdoc b/misc/tutorial/218_columns_filters.ngdoc new file mode 100644 index 0000000000..87eb721ef9 --- /dev/null +++ b/misc/tutorial/218_columns_filters.ngdoc @@ -0,0 +1,236 @@ +@ngdoc overview +@name Tutorial: 218 Columns Filters +@description + + +Feature ui.grid.columnsFilters allows to add a filter button to the column header, which opens a filter form popup. In addition, it adds an "OR" filtering capacity to the grid. + +Documentation for the columnsFilters feature is provided in the api documentation, in particular: + +- {@link api/ui.grid.columnsFilters.api:ColumnDef columnDef} + +In order to enable the columnsFilters in a column, you can either declare "enableFiltering" in the `columnDef`, or leave it blank and set the global `enableFiltering` to `true`. +There are 2 ways to setup the columnsFilters. The first is to use the `filter` property inside the `columnDef`. The second is the use the {@link api/ui.grid.columnsFilters.api:ColumnDef columnFilter} property, which has some extra options in store. + +There are 4 types of filters: string (default), number, date and select. If no type is mentioned, then "string" is used. +The "select" has 2 ways to setup: +1) Setup a 'selectOptions' in the filter or the {@link api/ui.grid.columnsFilters.api:ColumnDef columnFilter}. +2) Don't set the `selectOptions` and the options will be set dynamically from the available data (and will also be updated anytime the `notifyDataChange` event is triggered). + +In below exaple you can see the usage in action. + +@example + + + var app = angular.module('app', ['ngAnimate', 'ngTouch', 'ui.grid', 'ui.grid.columnsFilters']); + + app.controller('MainCtrl', ['$scope', '$http', 'uiGridConstants', function ($scope, $http, uiGridConstants) { + var today = new Date(); + var nextWeek = new Date(); + nextWeek.setDate(nextWeek.getDate() + 7); + + $scope.addToGender = function(){ + $scope.gridApi.grid.options.data[0].gender = 3; + $scope.gridApi.grid.api.core.notifyDataChange(uiGridConstants.dataChange.ALL); + } + + $scope.highlightFilteredHeader = function( row, rowRenderIndex, col, colRenderIndex ) { + if( col.filters[0].term ){ + return 'header-filtered'; + } else { + return ''; + } + }; + + $scope.gridOptions = { + enableFiltering: true, + onRegisterApi: function(gridApi){ + $scope.gridApi = gridApi; + }, + columnDefs: [ + // default + { field: 'name', headerCellClass: $scope.highlightFilteredHeader }, + // pre-populated search field + { field: 'gender', + columnFilter: { + type: 'select', + selectOptions: [ { value: '1', label: 'male' }, { value: '2', label: 'female' }, { value: '3', label: 'unknown'}, { value: '4', label: 'not stated' }, { value: '5', label: 'a really long value that extends things' } ] + }, + filter: { + term: '1', + type: uiGridConstants.filter.SELECT, + selectOptions: [ { value: '1', label: 'male' }, { value: '2', label: 'female' }, { value: '3', label: 'unknown'}, { value: '4', label: 'not stated' }, { value: '5', label: 'a really long value that extends things' } ] + }, + cellFilter: 'mapGender', headerCellClass: $scope.highlightFilteredHeader }, + { field: 'gender', + columnFilter: { + multiple: false + }, + filter: { + term: '1', + type: uiGridConstants.filter.SELECT + }, + cellFilter: 'mapGender', headerCellClass: $scope.highlightFilteredHeader }, + // no filter input + { field: 'company', enableFiltering: false, filter: { + noTerm: true, + condition: function(searchTerm, cellValue) { + return cellValue.match(/a/); + } + }}, + // no filter input + { field: 'company', enableFiltering: false, filter: { + noTerm: true, + condition: function(searchTerm, cellValue) { + return cellValue.match(/a/); + } + }}, + // specifies one of the built-in conditions + // and a placeholder for the input + { + field: 'email', + filter: { + condition: uiGridConstants.filter.ENDS_WITH, + placeholder: 'ends with' + }, headerCellClass: $scope.highlightFilteredHeader + }, + // custom condition function + { + field: 'phone', + filter: { + condition: function(searchTerm, cellValue) { + var strippedValue = (cellValue + '').replace(/[^\d]/g, ''); + return strippedValue.indexOf(searchTerm) >= 0; + } + }, headerCellClass: $scope.highlightFilteredHeader + }, + // multiple filters + { field: 'age', + columnFilter: { + type: 'number' + } + + , headerCellClass: $scope.highlightFilteredHeader}, + // date filter + { + field: 'mixedDate', + cellFilter: 'date', + width: '15%', filter: { + condition: uiGridConstants.filter.LESS_THAN, + placeholder: 'less than', + term: nextWeek, + type: 'date' + }, columnFilter: {dateType: 'datetime-local'}, + headerCellClass: $scope.highlightFilteredHeader + }, + { field: 'mixedDate', displayName: "Long Date", cellFilter: 'date:"longDate"', filterCellFiltered:true, width: '15%', + } + ] + }; + + $http.get('/data/500_complex.json') + .success(function(data) { + $scope.gridOptions.data = data; + $scope.gridOptions.data[0].age = -5; + + data.forEach( function addDates( row, index ){ + row.mixedDate = new Date(); + row.mixedDate.setDate(today.getDate() + ( index % 14 ) ); + row.gender = row.gender==='male' ? '1' : '2'; + }); + }); + + $scope.toggleFiltering = function(){ + $scope.gridOptions.enableFiltering = !$scope.gridOptions.enableFiltering; + $scope.gridApi.core.notifyDataChange( uiGridConstants.dataChange.COLUMN ); + }; + }]) + .filter('mapGender', function() { + var genderHash = { + 1: 'male', + 2: 'female', + 3: 'Unknown' + }; + + return function(input) { + if (!input){ + return ''; + } else { + return genderHash[input]; + } + }; + }); + + +
+ You can use asterisks to fuzz-match, i.e. use "*z*" as your filter to show any row where that column contains a "z". +
+
+ Note: The third column has the filter input disabled, but actually has a filter set in code that requires every company to have an 'a' in their name. +
+
+ +
+ +
+
+ + .grid { + width: 650px; + height: 400px; + } + + .header-filtered { + color: blue; + } + + + var gridTestUtils = require('../../test/e2e/gridTestUtils.spec.js'); + + describe('first grid on the page, filtered by male by default', function() { + gridTestUtils.firefoxReload(); + + it('grid should have seven visible columns', function () { + gridTestUtils.expectHeaderColumnCount( 'grid1', 7 ); + }); + + it('filter on 4 columns, filter with greater than/less than on one, one with no filter, then one with one filter', function () { + gridTestUtils.expectFilterBoxInColumn( 'grid1', 0, 1 ); + gridTestUtils.expectFilterSelectInColumn( 'grid1', 1, 1 ); + gridTestUtils.expectFilterBoxInColumn( 'grid1', 2, 0 ); + gridTestUtils.expectFilterBoxInColumn( 'grid1', 3, 1 ); + gridTestUtils.expectFilterBoxInColumn( 'grid1', 4, 1 ); + gridTestUtils.expectFilterBoxInColumn( 'grid1', 5, 2 ); + gridTestUtils.expectFilterBoxInColumn( 'grid1', 6, 1 ); + }); + + it('third row should be Hatfield Hudson - will be Terry Clay if filtering broken', function () { + gridTestUtils.expectCellValueMatch( 'grid1', 2, 0, 'Hatfield Hudson' ); + }); + + it('cancel filter on gender column and on date column, should now see Bishop Carr in third row', function() { + gridTestUtils.cancelFilterInColumn( 'grid1', 1 ) + .then(function () { + return gridTestUtils.cancelFilterInColumn( 'grid1', 6 ); + }) + .then(function () { + gridTestUtils.expectCellValueMatch( 'grid1', 2, 0, 'Bishop Carr' ); + }); + }); + + it('filter on email column, should automatically do "ends with"', function() { + gridTestUtils.cancelFilterInColumn( 'grid1', 1 ) + .then(function () { + return gridTestUtils.cancelFilterInColumn( 'grid1', 6 ); + }) + .then(function () { + return gridTestUtils.enterFilterInColumn( 'grid1', 3, 'digirang.com' ); + }) + .then(function () { + gridTestUtils.expectRowCount( 'grid1', 2 ); + }); + }); + }); + +
diff --git a/src/features/uiColumnFilter/js/column-filter.js b/src/features/uiColumnFilter/js/column-filter.js new file mode 100644 index 0000000000..7ed8d59be0 --- /dev/null +++ b/src/features/uiColumnFilter/js/column-filter.js @@ -0,0 +1,500 @@ +(function () { + 'use strict'; + /** + * @ngdoc overview + * @name ui.grid.columnsFilters + * + * @description + * + * #ui.grid.columnsFilters + * + * + * + * This module provides column filter in popup from the filter header cells. + */ + var module = angular.module('ui.grid.columnsFilters', ['ui.grid']); + + /** + * @ngdoc object + * @name ui.grid.columnsFilters.constant:uiGridColumnsFiltersConstants + * + * @description constants available in columnsFilters module. + * + * @property {string} featureName - The name of the feature. + * @property {object} filterType - { + STRING: 'string', + NUMBER: 'number', + DATE: 'date', + SELECT: 'select' + } + * @property {object} dateType - { + DATE: 'date', + TIME: 'time', + DATETIME: 'datetime-local', + DATETIMELOCALE: 'datetime-locale' + } + * @property {object} numberOperators - { + 8: 'Exact', + 512: 'Not Equal', + 128: 'Less than', + 256: 'Less than or equal', + 32: 'More than', + 64: 'More than or equal' + } + * @property {object} dateOperators - { + 8: 'Exact', + 512: 'Not Equal', + 128: 'Before', + 256: 'Before or equal', + 32: 'Later', + 64: 'Later or equal' + } + * @property {object} stringOperators - { + 16: 'Contains', + 4: 'Ends With', + 8: 'Exact', + 512: 'Not Equal', + 2: 'Starts With' + } + * @property {object} selectOperators - { + 16: 'Contains', + 4: 'Ends With', + 8: 'Exact', + 512: 'Not Equal', + 2: 'Starts With' + } + * @property {object} logics - { + "OR": 'Or', + "AND": 'And' + } + * + */ + + module.constant('uiGridColumnsFiltersConstants', { + featureName: "columnsFilters", + filterType: { + STRING: 'string', + NUMBER: 'number', + DATE: 'date', + SELECT: 'select' + }, + dateTypes: { + DATE: 'date', + TIME: 'time', + DATETIME: 'datetime-locale', + DATETIMELOCALE: 'datetime-locale' + }, + numberOperators: { + 8: 'Exact', + 512: 'Not Equal', + 128: 'Less than', + 256: 'Less than or equal', + 32: 'More than', + 64: 'More than or equal' + }, + dateOperators: { + 8: 'Exact', + 512: 'Not Equal', + 128: 'Before', + 256: 'Before or equal', + 32: 'Later', + 64: 'Later or equal' + }, + stringOperators: { + 16: 'Contains', + 4: 'Ends With', + 8: 'Exact', + 512: 'Not Equal', + 2: 'Starts With' + }, + selectOperators: { + 16: 'Contains', + 4: 'Ends With', + 8: 'Exact', + 512: 'Not Equal', + 2: 'Starts With' + }, + logics: { + "OR": 'Or', + "AND": 'And' + } + }); + + /** + * @ngdoc service + * @name ui.grid.columnsFilters.service:uiGridColumnsFiltersService + * + * @description Services for columnsFilters feature + */ + /** + * @ngdoc object + * @name ui.grid.columnsFilters.api:ColumnDef + * + * @description ColumnDef for column filter feature, these are available to be + * set using the ui-grid {@link ui.grid.class:GridOptions.columnDef gridOptions.columnDefs} + * + * @property {object} columnFilter - Specific column columnsFilters definitions + * @property {string} columnFilter.type - can be: 'date', 'select', 'string', 'number' + * @property {string} columnFilter.type - can be: 'date', 'select', 'string', 'number' + * @property {boolean} columnFilter.multiple - Boolean stating is a select filter would show as multiple or singular choice + * @property {object} columnFilter.selectOptions - states the values of a select option (if needed) + * @property {object} columnFilter.terms - holds the search terms for every filter form + * @property {object} columnFilter.logics - holds the logics (and/or) for every filter form + * @property {object} columnFilter.operators - holds the operators (bigger than, smaller than etc.) for every filter form + */ + + module.service('uiGridColumnsFiltersService', ['$q', 'uiGridColumnsFiltersConstants', 'rowSearcher', 'GridRow', 'gridClassFactory', 'i18nService', 'uiGridConstants', 'rowSorter', '$templateCache', + function ($q, uiGridColumnsFiltersConstants, rowSearcher, GridRow, gridClassFactory, i18nService, uiGridConstants, rowSorter, $templateCache) { + + var runColumnFilter = rowSearcher.runColumnFilter; + + function columnFilter(searchTerm, cellValue, row, column) { + var conditions = column.colDef.columnFilter.operators; + var logics = column.colDef.columnFilter.logics; + var filterPass = true; + var logic = "And"; + + for (var i = 0; i < searchTerm.length; i++) { + var term = searchTerm[i]; + + // get the condition - if not defined, it means we have one condition for all of the terms - most likely a function + var condition = angular.isDefined(conditions[i]) ? conditions[i] : conditions[0]; + + // now set the condition in the right format - either a function or a number + condition = angular.isFunction(condition) ? condition : Number(condition); + + var newFilter = rowSearcher.setupFilters([{ + term: term, + condition: condition, + flags: { + caseSensitive: false + } + }])[0]; + // if we are on the second run check for "OR"/"AND" logics + if (i) { + // if we have logics, we might have "OR", otherwise, will stay with the default "AND" + if (angular.isDefined(logics)) { + logic = angular.isDefined(logic[i - 1]) ? logic[i - 1] : logic[0]; + } + + if (logic === 'Or') { + // if we have a truthy value we pass since we have an "OR" + if (filterPass) { + return filterPass; + } + filterPass = runColumnFilter(row.grid, row, column, newFilter); + } + else { + if (!filterPass) { + return filterPass; + } + filterPass = runColumnFilter(row.grid, row, column, newFilter); + } + + + } + // TODO::check for the "select" condition in order to make sure we check for mulitple select + filterPass = rowSearcher.runColumnFilter(row.grid, row, column, newFilter); + } + + return filterPass; + } + + var service = { + initializeGrid: function (grid, $scope) { + //add feature namespace and any properties to grid for needed + /** + * @ngdoc object + * @name columnsFilters + * @name ui.grid.columnsFilters:Grid + * + * @description Grid properties + * + * @property {object} currentColumn - current column being filtered + */ + + /** + * @ngdoc object + * @name columnsFilters + * @name ui.grid.api.columnsFilters.api:Grid + * + * @description Grid functions added for columnsFilters + * + * @property {clear} clear - clears a column's filter + * @property {function} filter - a function that enables programmatically filter the grid according to various rules + */ + grid.columnsFilters = { + currentColumn: undefined + }; + + grid.api.columnsFilters = { + filter: this.filter, + clear: this.clear + }; + + angular.forEach(grid.options.columnDefs, function (colDef) { + if (colDef.enableFiltering !== false) { + var columnFilter = { + terms: [], + operators: [], + logics: [] + }; + + if (angular.isUndefined(colDef.columnFilter)) { + colDef.columnFilter = columnFilter; + } + else { + colDef.columnFilter = angular.merge({}, columnFilter, colDef.columnFilter); + } + colDef.filterHeaderTemplate = $templateCache.get('ui-grid/filterButton'); + } + else { + colDef.filterHeaderTemplate = ''; + } + }); + }, + /** + * @ngdoc method + * @methodOf ui.grid.columnsFilters.service:uiGridColumnsFiltersService + * @name filterPopupStyle + * @description Calculates the column filter's popup absolute position + * @param {event} $event the event from the click event + * @returns {object} and object with top and left styling expressions + */ + filterPopupStyle: function ($event) { + var rect = $event.target.parentElement.getClientRects()[0]; + return { + top: document.body.scrollTop + (rect.height + rect.top) + 'px', + left: rect.left + 'px' + }; + }, + /** + * @ngdoc method + * @methodOf ui.grid.columnsFilters.service:uiGridColumnsFiltersService + * @name filter + * @description Sets the filter parameters of the column + * @param {column} col - the column that is now being filtered + */ + filter: function (col) { + var terms = col.colDef.columnFilter.terms; + + var logics = col.colDef.columnFilter.logics; + + // add the data into the filter object of the column + // the terms array is the "term" + col.filters[0].term = terms; + + // set condition as our filter function + col.filters[0].condition = columnFilter; + + // logic is new, so we will add it, and handle it in our override function + col.filters[0].logic = logics; + col.grid.api.core.notifyDataChange(uiGridConstants.dataChange.COLUMN); + }, + /** + * @ngdoc method + * @methodOf ui.grid.columnsFilters.service:uiGridColumnsFiltersService + * @name clear + * @description Clears the filter parameters of the column + * @param {column} col - the column that is now being filtered + */ + clear: function (col) { + if (angular.isUndefined(col.filters[0].term)) { + return; + } + + if (!angular.isArray(col.filters[0].term)) { + col.filters[0].term = []; + } + else { + col.filters[0].term.length = 0; + } + + + col.filters[0].condition = undefined; + col.grid.api.core.notifyDataChange(uiGridConstants.dataChange.COLUMN); + } + }; + + return service; + }]); + + module.directive('uiGridColumnsFiltersDirective', ['$compile', 'gridUtil', 'uiGridColumnsFiltersService', 'uiGridColumnsFiltersConstants', '$templateCache', '$document', + function ($compile, gridUtil, uiGridColumnsFiltersService, uiGridColumnsFiltersConstants, $templateCache, $document) { + return { + require: 'uiGrid', + scope: false, + link: function ($scope, $elm, $attrs, uiGridCtrl) { + uiGridColumnsFiltersService.initializeGrid(uiGridCtrl.grid, $scope); + } + }; + + /** + * @ngdoc directive + * @name ui.grid.columnsFilters.directive:uiGridColumnFiltersDirective + * + * @description directive for columnsFilters button in column header + */ + + }]); + + + /** + * @ngdoc directive + * @name ui.grid.columnsFilters.directive.api:uiGridFilter + * + * @description Extanding the uiGridFilter directive to prepare the column filter + */ + module.directive('uiGridFilter', ['uiGridColumnsFiltersService', 'uiGridColumnsFiltersConstants', '$templateCache', '$compile', 'uiGridConstants', + function (uiGridColumnsFiltersService, uiGridColumnsFiltersConstants, $templateCache, $compile, uiGridConstants) { + return { + priority: 500, + scope: false, + link: function ($scope, $elm, $attrs, uiGridCtrl) { + + function dataChangeCallback() { + // now wait for the rows to be updated with the new data + var watchForRows = $scope.$watch('col.grid.rows.length', function (newRowsLength) { + // make sure we have updated... + if (newRowsLength !== $scope.col.grid.options.data.length) { + return; + } + // set the options + $scope.selectOptions = $scope.setSelectOptions($scope.selectOptions, currentColumn); + // remove the listener + watchForRows(); + }); + } + + //TODO::need to decide if we work with the filter API when it is sufficient and only expand it... + var currentColumn = $scope.col; // cache current column + + // if we're not supposed to filter this column, no need to activate filter for it... + if (angular.isDefined(currentColumn.colDef.enableFiltering) && !currentColumn.colDef.enableFiltering) { + return; + } + + // get the filter type (default is string) + var filterType = 'string'; + if (angular.isDefined(currentColumn.colDef.columnFilter) && angular.isDefined(currentColumn.colDef.columnFilter.type)) { + filterType = currentColumn.colDef.columnFilter.type; + } + else if (angular.isDefined(currentColumn.colDef.filter) && angular.isDefined(currentColumn.colDef.filter.type)) { + filterType = currentColumn.colDef.filter.type; + } + + // get the filter popup template + var thisFilterTemplate = 'ui-grid/filters/%%^^ColumnFilter'.replace('%%^^', filterType); // get the filter type template name + var formElementsTemplate = $templateCache.get(thisFilterTemplate); + var popupTemplate = $templateCache.get('ui-grid/filterPopup').replace('', formElementsTemplate); // get the full popup template + + // get the selection options if needed + if (filterType === 'select') { + currentColumn.colDef.columnFilter.logics = ["OR"]; + if (angular.isDefined(currentColumn.colDef.columnFilter) && angular.isDefined(currentColumn.colDef.columnFilter.selectOptions)) { + $scope.selectOptions = currentColumn.colDef.columnFilter.selectOptions; + } + else if (angular.isDefined(currentColumn.colDef.filter) && angular.isDefined(currentColumn.colDef.filter.selectOptions)) { + $scope.selectOptions = currentColumn.colDef.filter.selectOptions; + } + + // remove multiple selection if needed - can be defined only in th e columnFilter right now + if (angular.isDefined(currentColumn.colDef.columnFilter) && !currentColumn.colDef.columnFilter.multiple) { + popupTemplate = popupTemplate.replace('multiple', ''); + popupTemplate = popupTemplate.replace('.terms', '.terms[0]'); + } + + if (angular.isUndefined($scope.selectOptions)) { + // if we have select options, it means we have definitions set and we use the static def + $scope.setSelectOptions = function (items, col) { + // if we have static definitions, do nothing + if (angular.isDefined(col.colDef.filter) && angular.isDefined(col.colDef.filter.selectOptions) || + angular.isDefined(col.colDef.columnFilter) && angular.isDefined(col.colDef.columnFilter.selectOptions)) { + return items; + } + + // if we don't create a dynamic selectOptions array + var filteredItems = []; + var tmpIDs = []; + var tmpItem = {}; + var rows = col.grid.rows; + + // for every row in the grid + for (var i = 0; i < rows.length; i++) { + // get the label and the value + tmpItem.label = col.grid.getCellDisplayValue(rows[i], col); + tmpItem.value = col.grid.getCellValue(rows[i], col); + + // make sure we take only unique values + if (tmpIDs.indexOf(tmpItem.value) === -1) { + tmpIDs.push(tmpItem.value); + filteredItems.push(angular.copy(tmpItem)); + } + + } + + // insert the items into the selectOptions array + items = filteredItems; + return items; + }; + + $scope.selectOptions = $scope.setSelectOptions($scope.selectOptions, currentColumn); + + currentColumn.grid.registerDataChangeCallback(dataChangeCallback, [uiGridConstants.dataChange.ALL]); + + } + } + + $scope.filter = uiGridColumnsFiltersService.filter; // set the filtering function in the scope + $scope.clear = uiGridColumnsFiltersService.clear; // set the clear filter function in the scope + $scope.operators = uiGridColumnsFiltersConstants[filterType + 'Operators']; // set the operators in the scope + $scope.logics = uiGridColumnsFiltersConstants.logics; // set the logics in the scope + + // toggle filter popup + $scope.toggleFilter = function () { + event.stopPropagation(); + event.preventDefault(); + + if (currentColumn.grid.columnsFilters.currentColumn) { + // if we have an open filter + angular.element(document.getElementById('uiGridFilterPopup')).remove(); //remove it + if (angular.equals(currentColumn.grid.columnsFilters.currentColumn, currentColumn)) { + // if the same column that its filter shown is clicked, close it + currentColumn.grid.columnsFilters.currentColumn = undefined; //clear the current open column popup + return; + } + } + + // open a popup + currentColumn.grid.columnsFilters.currentColumn = currentColumn; // set the current opened columnFilter + $scope.filterPopupStyle = uiGridColumnsFiltersService.filterPopupStyle(event); //set the style in the scope + var popupElement = $compile(popupTemplate)($scope); // compile it + angular.element(document.body).append(popupElement); // append to body + + angular.element(document.body).on('click', $scope.toggleFilter); // make sure the popup closes when clicking outside + + // make sure popup is not closing when clicking inside + popupElement.on('click', function () { + event.preventDefault(); + event.stopPropagation(); + }); + + // remove the click events on destroy + popupElement.on('$destroy', function () { + popupElement.off('click'); + angular.element(document.body).off('click', $scope.toggleFilter); + }); + }; + } + }; + }]); + + module.filter('filterSelectValues', function () { + return function (items, col) { + + + }; + }); + + +})(); diff --git a/src/features/uiColumnFilter/less/uiColumnsFilters.less b/src/features/uiColumnFilter/less/uiColumnsFilters.less new file mode 100644 index 0000000000..12f601bd5c --- /dev/null +++ b/src/features/uiColumnFilter/less/uiColumnsFilters.less @@ -0,0 +1,17 @@ +@import '../../../less/variables'; + +.ui-grid-column-filter-button { + width: 1em; + height: 1.4em; +} + +#uiGridFilterPopup{ + min-height: 150px; + min-width: 150px; + max-width: 200px; + position: absolute; + border-radius: 25px; + background-color: white; + padding: 1.25em; + border: 1px solid rgba(0,0,0,0.2); +} diff --git a/src/features/uiColumnFilter/templates/filterButton.html b/src/features/uiColumnFilter/templates/filterButton.html new file mode 100644 index 0000000000..0c6e0eb9fe --- /dev/null +++ b/src/features/uiColumnFilter/templates/filterButton.html @@ -0,0 +1,15 @@ +
+ +
\ No newline at end of file diff --git a/src/features/uiColumnFilter/templates/filterPopup.html b/src/features/uiColumnFilter/templates/filterPopup.html new file mode 100644 index 0000000000..89ee8c2c37 --- /dev/null +++ b/src/features/uiColumnFilter/templates/filterPopup.html @@ -0,0 +1,16 @@ +
+
+
+ +
+ + +
+ + +
+
+
\ No newline at end of file diff --git a/src/features/uiColumnFilter/templates/filters/dateColumnFilter.html b/src/features/uiColumnFilter/templates/filters/dateColumnFilter.html new file mode 100644 index 0000000000..3232d356d9 --- /dev/null +++ b/src/features/uiColumnFilter/templates/filters/dateColumnFilter.html @@ -0,0 +1,41 @@ + + + +{{col.colDef.columnFilter.terms[0]}} + + + + + \ No newline at end of file diff --git a/src/features/uiColumnFilter/templates/filters/numberColumnFilter.html b/src/features/uiColumnFilter/templates/filters/numberColumnFilter.html new file mode 100644 index 0000000000..b5ffa1fd93 --- /dev/null +++ b/src/features/uiColumnFilter/templates/filters/numberColumnFilter.html @@ -0,0 +1,42 @@ + + + +{{col.colDef.columnFilter.terms[0]}} + + + + + + + + diff --git a/src/features/uiColumnFilter/templates/filters/selectColumnFilter.html b/src/features/uiColumnFilter/templates/filters/selectColumnFilter.html new file mode 100644 index 0000000000..495802fa47 --- /dev/null +++ b/src/features/uiColumnFilter/templates/filters/selectColumnFilter.html @@ -0,0 +1,17 @@ + + + diff --git a/src/features/uiColumnFilter/templates/filters/stringColumnFilter.html b/src/features/uiColumnFilter/templates/filters/stringColumnFilter.html new file mode 100644 index 0000000000..52e7fd18bd --- /dev/null +++ b/src/features/uiColumnFilter/templates/filters/stringColumnFilter.html @@ -0,0 +1,14 @@ + + + diff --git a/src/features/uiColumnFilter/test/uiColumnsFilters.spec.js b/src/features/uiColumnFilter/test/uiColumnsFilters.spec.js new file mode 100644 index 0000000000..427b8f4dc6 --- /dev/null +++ b/src/features/uiColumnFilter/test/uiColumnsFilters.spec.js @@ -0,0 +1,181 @@ +describe('ui.grid.columnsFilters', function () { + var gridScope, gridElm, viewportElm, $scope, $compile, $timeout, recompile, uiGridConstants, $templateCache, uiGridColumnsFiltersService; + + var data = [ + {"name": "Ethel Price", "gender": "female", "company": "Enersol"}, + {"name": "Claudine Neal", "gender": "female", "company": "Sealoud"}, + {"name": "Beryl Rice", "gender": "female", "company": "Velity"}, + {"name": "Wilder Gonzales", "gender": "male", "company": "Geekko"} + ]; + + beforeEach(module('ui.grid.columnsFilters')); + + beforeEach(inject(function (_$compile_, _$rootScope_, _$templateCache_, _$timeout_, _uiGridConstants_, _uiGridColumnsFiltersService_) { + $scope = _$rootScope_; + $compile = _$compile_; + $timeout = _$timeout_; + $templateCache = _$templateCache_; + uiGridConstants = _uiGridConstants_; + uiGridColumnsFiltersService = _uiGridColumnsFiltersService_; + + $scope.gridOpts = { + data: data, + enableFiltering: true, + columnDefs: [ + { + field: 'name', + enableFiltering: true + }, + { + field: 'gender', + enableFiltering: true + }, + { + field: 'company', + enableFiltering: true + } + ], + onRegisterApi: function (gridApi) { + $scope.gridApi = gridApi; + } + }; + + recompile = function () { + gridElm = angular.element('
'); + document.body.appendChild(gridElm[0]); + $compile(gridElm)($scope); + $scope.$digest(); + gridScope = gridElm.isolateScope(); + + viewportElm = $(gridElm).find('.ui-grid-viewport'); + }; + + recompile(); + })); + + afterEach(function () { + angular.element(gridElm).remove(); + gridElm = null; + }); + + describe('on load initialization', function () { + it('columnFilters property should be available in the grid', inject(function ($timeout) { + expect(gridScope.grid.columnsFilters).toBeDefined(); + expect(gridScope.grid.api.columnsFilters).toBeDefined(); + expect(typeof gridScope.grid.api.columnsFilters.clear).toEqual('function'); + expect(typeof gridScope.grid.api.columnsFilters.filter).toEqual('function'); + })); + + it('every column should have columnFilter definitions', function () { + var ammountInitiated = 0; + var colDef; + for (var i = 0; i < gridScope.grid.options.columnDefs.length; i++) { + colDef = gridScope.grid.options.columnDefs[i]; + if (colDef.enableFiltering) { + if (colDef.columnFilter && colDef.columnFilter.terms && colDef.columnFilter.operators && colDef.columnFilter.logics) { + ammountInitiated++; + } + } + } + expect(ammountInitiated).toEqual(3); // one doesn't have enableFiltering... + }); + + it('every column should have the filter button as a filterHeaderTemplate', function () { + var ammountInitiated = 0; + var colDef; + for (var i = 0; i < gridScope.grid.options.columnDefs.length; i++) { + colDef = gridScope.grid.options.columnDefs[i]; + if (colDef.enableFiltering) { + if (colDef.filterHeaderTemplate === $templateCache.get('ui-grid/filterButton')) { + ammountInitiated++; + } + } + } + expect(ammountInitiated).toEqual(3); + }); + + }); + + describe('test service.filter', function () { + beforeEach(inject(function () { + gridScope.grid.options.columnDefs[0].columnFilter = { + terms: ["Price"], + operators: [16], + logics: undefined + }; + + })); + + it('test filter should setup the filter parameter correctly', function () { + uiGridColumnsFiltersService.filter(gridScope.grid.columns[0]); + expect(gridScope.grid.columns[0].filters[0].term).toEqual(["Price"]); + expect(typeof gridScope.grid.columns[0].filters[0].condition).toEqual('function'); + expect(gridScope.grid.columns[0].filters[0].logics).toBeUndefined(); + }); + + it('test filter should call notifyDataChange', function () { + + spyOn(gridScope.grid.api.core, 'notifyDataChange'); + + uiGridColumnsFiltersService.filter(gridScope.grid.columns[0]); + + expect(gridScope.grid.api.core.notifyDataChange).toHaveBeenCalledWith(uiGridConstants.dataChange.COLUMN); + + }); + + describe('service.filter results', function(){ + var rowSearcher; + beforeEach(inject(function(_rowSearcher_){ + rowSearcher = _rowSearcher_; + // clear all columns filters + gridScope.grid.columns.forEach(function(col){ + gridScope.grid.api.columnsFilters.clear(col); + }); + })); + + it('filtering according to "male" should return only one visible row', function(){ + var genderColumn = gridScope.grid.columns[1]; + var genderColumnDef = genderColumn.colDef; + genderColumnDef.columnFilter.terms = ['male']; + genderColumnDef.columnFilter.operators = [8]; + gridScope.grid.api.columnsFilters.filter(genderColumn); + + $scope.$digest(); + $timeout.flush(500); + rowSearcher.search(gridScope.grid, gridScope.grid.rows, gridScope.grid.columns); + + expect(gridScope.grid.api.core.getVisibleRows(gridScope.grid).length).toEqual(1); + + }); + }); + }); + + describe('test service.clear', function () { + beforeEach(inject(function () { + gridScope.grid.options.columnDefs[0].columnFilter = { + terms: ["Price"], + operators: [16], + logics: undefined + }; + uiGridColumnsFiltersService.filter(gridScope.grid.columns[0]); + })); + + it('clear should reset the filter parameters', function () { + uiGridColumnsFiltersService.clear(gridScope.grid.columns[0]); + expect(gridScope.grid.columns[0].filters[0].term).toEqual([]); + expect(gridScope.grid.columns[0].filters[0].condition).toBeUndefined(); + expect(gridScope.grid.columns[0].filters[0].logics).toBeUndefined(); + }); + + it('clear should call notifyDataChange', function () { + + spyOn(gridScope.grid.api.core, 'notifyDataChange'); + + uiGridColumnsFiltersService.clear(gridScope.grid.columns[0]); + + expect(gridScope.grid.api.core.notifyDataChange).toHaveBeenCalledWith(uiGridConstants.dataChange.COLUMN); + + }); + }); + +}); diff --git a/test/karma.debug.conf.js b/test/karma.debug.conf.js index c25bbf8ba2..c13970836e 100644 --- a/test/karma.debug.conf.js +++ b/test/karma.debug.conf.js @@ -21,7 +21,7 @@ module.exports = function(config) { 'lib/test/angular/1.3.6/angular.js', 'lib/test/angular/1.3.6/angular-mocks.js', 'lib/test/angular/1.3.6/angular-animate.js', - + 'src/js/core/bootstrap.js', 'src/js/**/*.js', 'src/features/**/js/**/*.js', @@ -105,7 +105,7 @@ module.exports = function(config) { customLaunchers: util.customLaunchers() }); - + // TODO(c0bra): remove once SauceLabs supports websockets. // This speeds up the capturing a bit, as browsers don't even try to use websocket. -- (thanks vojta) if (process.env.TRAVIS) { @@ -114,7 +114,7 @@ module.exports = function(config) { config.reporters = ['dots', 'coverage']; var buildLabel = 'TRAVIS #' + process.env.TRAVIS_BUILD_NUMBER + ' (' + process.env.TRAVIS_BUILD_ID + ')'; - + // config.transports = ['websocket', 'xhr-polling']; config.sauceLabs.build = buildLabel; @@ -141,4 +141,4 @@ module.exports = function(config) { var bs = grunt.option('browsers').split(/,/).map(function(b) { return b.trim(); }); config.browsers = bs; } -}; \ No newline at end of file +};