UNPKG

angular-ui-grid

Version:

A data grid for Angular

1,244 lines (1,091 loc) 51.3 kB
/*! * ui-grid - v4.9.1 - 2020-10-26 * Copyright (c) 2020 ; License: MIT */ (function () { 'use strict'; /** * @ngdoc overview * @name ui.grid.grouping * @description * * # ui.grid.grouping * * <div class="alert alert-warning" role="alert"><strong>Beta</strong> This feature is ready for testing, but it either hasn't seen a lot of use or has some known bugs.</div> * * This module provides grouping of rows based on the data in them, similar * in concept to excel grouping. You can group multiple columns, resulting in * nested grouping. * * In concept this feature is similar to sorting + grid footer/aggregation, it * sorts the data based on the grouped columns, then creates group rows that * reflect a break in the data. Each of those group rows can have aggregations for * the data within that group. * * This feature leverages treeBase to provide the tree functionality itself, * the key thing this feature does therefore is to set treeLevels on the rows * and insert the group headers. * * Design information: * ------------------- * * Each column will get new menu items - group by, and aggregate by. Group by * will cause this column to be sorted (if not already), and will move this column * to the front of the sorted columns (i.e. grouped columns take precedence over * sorted columns). It will respect the sort order already set if there is one, * and it will allow the sorting logic to change that sort order, it just forces * the column to the front of the sorting. You can group by multiple columns, the * logic will add this column to the sorting after any already grouped columns. * * Once a grouping is defined, grouping logic is added to the rowsProcessors. This * will process the rows, identifying a break in the data value, and inserting a grouping row. * Grouping rows have specific attributes on them: * * - internalRow = true: tells us that this isn't a real row, so we can ignore it * from any processing that it looking at core data rows. This is used by the core * logic (or will be one day), as it's not grouping specific * - groupHeader = true: tells us this is a groupHeader. This is used by the grouping logic * to know if this is a groupHeader row or not * * Since the logic is baked into the rowsProcessors, it should get triggered whenever * row order or filtering or anything like that is changed. In order to avoid the row instantiation * time, and to preserve state across invocations, we hold a cache of the rows that we created * last time, and we use them again this time if we can. * * By default rows are collapsed, which means all data rows have their visible property * set to false, and only level 0 group rows are set to visible. * * <br/> * <br/> * * <div doc-module-components="ui.grid.grouping"></div> */ var module = angular.module('ui.grid.grouping', ['ui.grid', 'ui.grid.treeBase']); /** * @ngdoc object * @name ui.grid.grouping.constant:uiGridGroupingConstants * * @description constants available in grouping module, this includes * all the constants declared in the treeBase module (these are manually copied * as there isn't an easy way to include constants in another constants file, and * we don't want to make users include treeBase) * */ module.constant('uiGridGroupingConstants', { featureName: "grouping", rowHeaderColName: 'treeBaseRowHeaderCol', EXPANDED: 'expanded', COLLAPSED: 'collapsed', aggregation: { COUNT: 'count', SUM: 'sum', MAX: 'max', MIN: 'min', AVG: 'avg' } }); /** * @ngdoc service * @name ui.grid.grouping.service:uiGridGroupingService * * @description Services for grouping features */ module.service('uiGridGroupingService', ['$q', 'uiGridGroupingConstants', 'gridUtil', 'rowSorter', 'GridRow', 'gridClassFactory', 'i18nService', 'uiGridConstants', 'uiGridTreeBaseService', function ($q, uiGridGroupingConstants, gridUtil, rowSorter, GridRow, gridClassFactory, i18nService, uiGridConstants, uiGridTreeBaseService) { var service = { initializeGrid: function (grid, $scope) { uiGridTreeBaseService.initializeGrid( grid, $scope ); // add feature namespace and any properties to grid for needed /** * @ngdoc object * @name ui.grid.grouping.grid:grouping * * @description Grid properties and functions added for grouping */ grid.grouping = {}; /** * @ngdoc property * @propertyOf ui.grid.grouping.grid:grouping * @name groupHeaderCache * * @description Cache that holds the group header rows we created last time, we'll * reuse these next time, not least because they hold our expanded states. * * We need to take care with these that they don't become a memory leak, we * create a new cache each time using the values from the old cache. This works * so long as we're creating group rows for invisible rows as well. * * The cache is a nested hash, indexed on the value we grouped by. So if we * grouped by gender then age, we'd maybe have something like: * ``` * { * male: { * row: <pointer to the old row>, * children: { * 22: { row: <pointer to the old row> }, * 31: { row: <pointer to the old row> } * }, * female: { * row: <pointer to the old row>, * children: { * 28: { row: <pointer to the old row> }, * 55: { row: <pointer to the old row> } * } * } * ``` * * We create new rows for any missing rows, this means that they come in as collapsed. * */ grid.grouping.groupHeaderCache = {}; service.defaultGridOptions(grid.options); grid.registerRowsProcessor(service.groupRows, 400); grid.registerColumnBuilder( service.groupingColumnBuilder); grid.registerColumnsProcessor(service.groupingColumnProcessor, 400); /** * @ngdoc object * @name ui.grid.grouping.api:PublicApi * * @description Public Api for grouping feature */ var publicApi = { events: { grouping: { /** * @ngdoc event * @eventOf ui.grid.grouping.api:PublicApi * @name aggregationChanged * @description raised whenever aggregation is changed, added or removed from a column * * <pre> * gridApi.grouping.on.aggregationChanged(scope,function(col) {}) * </pre> * @param {GridColumn} col the column on which aggregation changed. The aggregation * type is available as `col.treeAggregation.type` */ aggregationChanged: {}, /** * @ngdoc event * @eventOf ui.grid.grouping.api:PublicApi * @name groupingChanged * @description raised whenever the grouped columns changes * * <pre> * gridApi.grouping.on.groupingChanged(scope,function(col) {}) * </pre> * @param {GridColumn} col the column on which grouping changed. The new grouping is * available as `col.grouping` */ groupingChanged: {} } }, methods: { grouping: { /** * @ngdoc function * @name getGrouping * @methodOf ui.grid.grouping.api:PublicApi * @description Get the grouping configuration for this grid, * used by the saveState feature. Adds expandedState to the information * provided by the internal getGrouping, and removes any aggregations that have a source * of grouping (i.e. will be automatically reapplied when we regroup the column) * Returned grouping is an object * `{ grouping: groupArray, treeAggregations: aggregateArray, expandedState: hash }` * where grouping contains an array of objects: * `{ field: column.field, colName: column.name, groupPriority: column.grouping.groupPriority }` * and aggregations contains an array of objects: * `{ field: column.field, colName: column.name, aggregation: column.grouping.aggregation }` * and expandedState is a hash of the currently expanded nodes * * The groupArray will be sorted by groupPriority. * * @param {boolean} getExpanded whether or not to return the expanded state * @returns {object} grouping configuration */ getGrouping: function ( getExpanded ) { var grouping = service.getGrouping(grid); grouping.grouping.forEach( function( group ) { group.colName = group.col.name; delete group.col; }); grouping.aggregations.forEach( function( aggregation ) { aggregation.colName = aggregation.col.name; delete aggregation.col; }); grouping.aggregations = grouping.aggregations.filter( function( aggregation ) { return !aggregation.aggregation.source || aggregation.aggregation.source !== 'grouping'; }); if ( getExpanded ) { grouping.rowExpandedStates = service.getRowExpandedStates( grid.grouping.groupingHeaderCache ); } return grouping; }, /** * @ngdoc function * @name setGrouping * @methodOf ui.grid.grouping.api:PublicApi * @description Set the grouping configuration for this grid, * used by the saveState feature, but can also be used by any * user to specify a combined grouping and aggregation configuration * @param {object} config the config you want to apply, in the format * provided out by getGrouping */ setGrouping: function ( config ) { service.setGrouping(grid, config); }, /** * @ngdoc function * @name groupColumn * @methodOf ui.grid.grouping.api:PublicApi * @description Adds this column to the existing grouping, at the end of the priority order. * If the column doesn't have a sort, adds one, by default ASC * * This column will move to the left of any non-group columns, the * move is handled in a columnProcessor, so gets called as part of refresh * * @param {string} columnName the name of the column we want to group */ groupColumn: function(columnName) { var column = grid.getColumn(columnName); service.groupColumn(grid, column); }, /** * @ngdoc function * @name ungroupColumn * @methodOf ui.grid.grouping.api:PublicApi * @description Removes the groupPriority from this column. If the * column was previously aggregated the aggregation will come back. * The sort will remain. * * This column will move to the right of any other group columns, the * move is handled in a columnProcessor, so gets called as part of refresh * * @param {string} columnName the name of the column we want to ungroup */ ungroupColumn: function(columnName) { var column = grid.getColumn(columnName); service.ungroupColumn(grid, column); }, /** * @ngdoc function * @name clearGrouping * @methodOf ui.grid.grouping.api:PublicApi * @description Clear any grouped columns and any aggregations. Doesn't remove sorting, * as we don't know whether that sorting was added by grouping or was there beforehand * */ clearGrouping: function() { service.clearGrouping(grid); }, /** * @ngdoc function * @name aggregateColumn * @methodOf ui.grid.grouping.api:PublicApi * @description Sets the aggregation type on a column, if the * column is currently grouped then it removes the grouping first. * If the aggregationDef is null then will result in the aggregation * being removed * * @param {string} columnName the column we want to aggregate * @param {string|function} aggregationDef one of the recognised types * from uiGridGroupingConstants or a custom aggregation function. * @param {string} aggregationLabel (optional) The label to use for this aggregation. */ aggregateColumn: function(columnName, aggregationDef, aggregationLabel) { var column = grid.getColumn(columnName); service.aggregateColumn(grid, column, aggregationDef, aggregationLabel); } } } }; grid.api.registerEventsFromObject(publicApi.events); grid.api.registerMethodsFromObject(publicApi.methods); grid.api.core.on.sortChanged($scope, service.tidyPriorities); }, defaultGridOptions: function (gridOptions) { // default option to true unless it was explicitly set to false /** * @ngdoc object * @name ui.grid.grouping.api:GridOptions * * @description GridOptions for grouping feature, these are available to be * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions} */ /** * @ngdoc object * @name enableGrouping * @propertyOf ui.grid.grouping.api:GridOptions * @description Enable row grouping for entire grid. * <br/>Defaults to true */ gridOptions.enableGrouping = gridOptions.enableGrouping !== false; /** * @ngdoc object * @name groupingShowCounts * @propertyOf ui.grid.grouping.api:GridOptions * @description shows counts on the groupHeader rows. Not that if you are using a cellFilter or a * sortingAlgorithm which relies on a specific format or data type, showing counts may cause that * to break, since the group header rows will always be a string with groupingShowCounts enabled. * <br/>Defaults to true except on columns of types 'date' and 'object' */ gridOptions.groupingShowCounts = gridOptions.groupingShowCounts !== false; /** * @ngdoc object * @name groupingNullLabel * @propertyOf ui.grid.grouping.api:GridOptions * @description The string to use for the grouping header row label on rows which contain a null or undefined value in the grouped column. * <br/>Defaults to "Null" */ gridOptions.groupingNullLabel = typeof(gridOptions.groupingNullLabel) === 'undefined' ? 'Null' : gridOptions.groupingNullLabel; /** * @ngdoc object * @name enableGroupHeaderSelection * @propertyOf ui.grid.grouping.api:GridOptions * @description Allows group header rows to be selected. * <br/>Defaults to false */ gridOptions.enableGroupHeaderSelection = gridOptions.enableGroupHeaderSelection === true; }, /** * @ngdoc function * @name groupingColumnBuilder * @methodOf ui.grid.grouping.service:uiGridGroupingService * @description Sets the grouping defaults based on the columnDefs * * @param {object} colDef columnDef we're basing on * @param {GridColumn} col the column we're to update * @param {object} gridOptions the options we should use * @returns {promise} promise for the builder - actually we do it all inline so it's immediately resolved */ groupingColumnBuilder: function (colDef, col, gridOptions) { /** * @ngdoc object * @name ui.grid.grouping.api:ColumnDef * * @description ColumnDef for grouping feature, these are available to be * set using the ui-grid {@link ui.grid.class:GridOptions.columnDef gridOptions.columnDefs} */ /** * @ngdoc object * @name enableGrouping * @propertyOf ui.grid.grouping.api:ColumnDef * @description Enable grouping on this column * <br/>Defaults to true. */ if (colDef.enableGrouping === false) { return; } /** * @ngdoc object * @name grouping * @propertyOf ui.grid.grouping.api:ColumnDef * @description Set the grouping for a column. Format is: * ``` * { * groupPriority: <number, starts at 0, if less than 0 or undefined then we're aggregating in this column> * } * ``` * * **Note that aggregation used to be included in grouping, but is now separately set on the column via treeAggregation * setting in treeBase** * * We group in the priority order given, this will also put these columns to the high order of the sort irrespective * of the sort priority given them. If there is no sort defined then we sort ascending, if there is a sort defined then * we use that sort. * * If the groupPriority is undefined or less than 0, then we expect to be aggregating, and we look at the * aggregation types to determine what sort of aggregation we can do. Values are in the constants file, but * include SUM, COUNT, MAX, MIN * * groupPriorities should generally be sequential, if they're not then the next time getGrouping is called * we'll renumber them to be sequential. * <br/>Defaults to undefined. */ if ( typeof(col.grouping) === 'undefined' && typeof(colDef.grouping) !== 'undefined') { col.grouping = angular.copy(colDef.grouping); if ( typeof(col.grouping.groupPriority) !== 'undefined' && col.grouping.groupPriority > -1 ) { col.treeAggregationFn = uiGridTreeBaseService.nativeAggregations()[uiGridGroupingConstants.aggregation.COUNT].aggregationFn; col.treeAggregationFinalizerFn = service.groupedFinalizerFn; } } else if (typeof(col.grouping) === 'undefined') { col.grouping = {}; } if (typeof(col.grouping) !== 'undefined' && typeof(col.grouping.groupPriority) !== 'undefined' && col.grouping.groupPriority >= 0) { col.suppressRemoveSort = true; } var groupColumn = { name: 'ui.grid.grouping.group', title: i18nService.get().grouping.group, icon: 'ui-grid-icon-indent-right', shown: function () { return typeof(this.context.col.grouping) === 'undefined' || typeof(this.context.col.grouping.groupPriority) === 'undefined' || this.context.col.grouping.groupPriority < 0; }, action: function () { service.groupColumn( this.context.col.grid, this.context.col ); } }; var ungroupColumn = { name: 'ui.grid.grouping.ungroup', title: i18nService.get().grouping.ungroup, icon: 'ui-grid-icon-indent-left', shown: function () { return typeof(this.context.col.grouping) !== 'undefined' && typeof(this.context.col.grouping.groupPriority) !== 'undefined' && this.context.col.grouping.groupPriority >= 0; }, action: function () { service.ungroupColumn( this.context.col.grid, this.context.col ); } }; var aggregateRemove = { name: 'ui.grid.grouping.aggregateRemove', title: i18nService.get().grouping.aggregate_remove, shown: function () { return typeof(this.context.col.treeAggregationFn) !== 'undefined'; }, action: function () { service.aggregateColumn( this.context.col.grid, this.context.col, null); } }; // generic adder for the aggregation menus, which follow a pattern var addAggregationMenu = function(type, title) { title = title || i18nService.get().grouping['aggregate_' + type] || type; var menuItem = { name: 'ui.grid.grouping.aggregate' + type, title: title, shown: function () { return typeof(this.context.col.treeAggregation) === 'undefined' || typeof(this.context.col.treeAggregation.type) === 'undefined' || this.context.col.treeAggregation.type !== type; }, action: function () { service.aggregateColumn( this.context.col.grid, this.context.col, type); } }; if (!gridUtil.arrayContainsObjectWithProperty(col.menuItems, 'name', 'ui.grid.grouping.aggregate' + type)) { col.menuItems.push(menuItem); } }; /** * @ngdoc object * @name groupingShowGroupingMenu * @propertyOf ui.grid.grouping.api:ColumnDef * @description Show the grouping (group and ungroup items) menu on this column * <br/>Defaults to true. */ if ( col.colDef.groupingShowGroupingMenu !== false ) { if (!gridUtil.arrayContainsObjectWithProperty(col.menuItems, 'name', 'ui.grid.grouping.group')) { col.menuItems.push(groupColumn); } if (!gridUtil.arrayContainsObjectWithProperty(col.menuItems, 'name', 'ui.grid.grouping.ungroup')) { col.menuItems.push(ungroupColumn); } } /** * @ngdoc object * @name groupingShowAggregationMenu * @propertyOf ui.grid.grouping.api:ColumnDef * @description Show the aggregation menu on this column * <br/>Defaults to true. */ if ( col.colDef.groupingShowAggregationMenu !== false ) { angular.forEach(uiGridTreeBaseService.nativeAggregations(), function(aggregationDef, name) { addAggregationMenu(name); }); angular.forEach(gridOptions.treeCustomAggregations, function(aggregationDef, name) { addAggregationMenu(name, aggregationDef.menuTitle); }); if (!gridUtil.arrayContainsObjectWithProperty(col.menuItems, 'name', 'ui.grid.grouping.aggregateRemove')) { col.menuItems.push(aggregateRemove); } } }, /** * @ngdoc function * @name groupingColumnProcessor * @methodOf ui.grid.grouping.service:uiGridGroupingService * @description Moves the columns around based on which are grouped * * @param {array} columns the columns to consider rendering * @param {array} rows the grid rows, which we don't use but are passed to us * @returns {array} updated columns array */ groupingColumnProcessor: function( columns, rows ) { columns = service.moveGroupColumns(this, columns, rows); return columns; }, /** * @ngdoc function * @name groupedFinalizerFn * @methodOf ui.grid.grouping.service:uiGridGroupingService * @description Used on group columns to display the rendered value and optionally * display the count of rows. * * @param {aggregation} aggregation The aggregation entity for a grouped column */ groupedFinalizerFn: function( aggregation ) { var col = this; if ( typeof(aggregation.groupVal) !== 'undefined') { aggregation.rendered = aggregation.groupVal; if ( col.grid.options.groupingShowCounts && col.colDef.type !== 'date' && col.colDef.type !== 'object' ) { aggregation.rendered += (' (' + aggregation.value + ')'); } } else { aggregation.rendered = null; } }, /** * @ngdoc function * @name moveGroupColumns * @methodOf ui.grid.grouping.service:uiGridGroupingService * @description Moves the column order so that the grouped columns are lined up * to the left (well, unless you're RTL, then it's the right). By doing this in * the columnsProcessor, we make it transient - when the column is ungrouped it'll * go back to where it was. * * Does nothing if the option `moveGroupColumns` is set to false. * * @param {Grid} grid grid object * @param {array} columns the columns that we should process/move * @returns {array} updated columns */ moveGroupColumns: function( grid, columns ) { if ( grid.options.moveGroupColumns === false) { return columns; } columns.forEach(function(column, index) { // position used to make stable sort in moveGroupColumns column.groupingPosition = index; }); columns.sort(function(a, b) { var a_group, b_group; if (a.isRowHeader) { a_group = a.headerPriority; } else if ( typeof(a.grouping) === 'undefined' || typeof(a.grouping.groupPriority) === 'undefined' || a.grouping.groupPriority < 0) { a_group = null; } else { a_group = a.grouping.groupPriority; } if (b.isRowHeader) { b_group = b.headerPriority; } else if ( typeof(b.grouping) === 'undefined' || typeof(b.grouping.groupPriority) === 'undefined' || b.grouping.groupPriority < 0) { b_group = null; } else { b_group = b.grouping.groupPriority; } // groups get sorted to the top if ( a_group !== null && b_group === null) { return -1; } if ( b_group !== null && a_group === null) { return 1; } if ( a_group !== null && b_group !== null) {return a_group - b_group; } return a.groupingPosition - b.groupingPosition; }); columns.forEach( function(column) { delete column.groupingPosition; }); return columns; }, /** * @ngdoc function * @name groupColumn * @methodOf ui.grid.grouping.service:uiGridGroupingService * @description Adds this column to the existing grouping, at the end of the priority order. * If the column doesn't have a sort, adds one, by default ASC * * This column will move to the left of any non-group columns, the * move is handled in a columnProcessor, so gets called as part of refresh * * @param {Grid} grid grid object * @param {GridColumn} column the column we want to group */ groupColumn: function( grid, column) { if ( typeof(column.grouping) === 'undefined' ) { column.grouping = {}; } // set the group priority to the next number in the hierarchy var existingGrouping = service.getGrouping( grid ); column.grouping.groupPriority = existingGrouping.grouping.length; // save sort in order to restore it when column is ungrouped column.previousSort = angular.copy(column.sort); // add sort if not present if ( !column.sort ) { column.sort = { direction: uiGridConstants.ASC }; } else if ( typeof(column.sort.direction) === 'undefined' || column.sort.direction === null ) { column.sort.direction = uiGridConstants.ASC; } column.treeAggregation = { type: uiGridGroupingConstants.aggregation.COUNT, source: 'grouping' }; if ( column.colDef && angular.isFunction(column.colDef.customTreeAggregationFn) ) { column.treeAggregationFn = column.colDef.customTreeAggregationFn; } else { column.treeAggregationFn = uiGridTreeBaseService.nativeAggregations()[uiGridGroupingConstants.aggregation.COUNT].aggregationFn; } column.treeAggregationFinalizerFn = service.groupedFinalizerFn; grid.api.grouping.raise.groupingChanged(column); // This indirectly calls service.tidyPriorities( grid ); grid.api.core.raise.sortChanged(grid, grid.getColumnSorting()); grid.queueGridRefresh(); }, /** * @ngdoc function * @name ungroupColumn * @methodOf ui.grid.grouping.service:uiGridGroupingService * @description Removes the groupPriority from this column. If the * column was previously aggregated the aggregation will come back. * The sort will remain. * * This column will move to the right of any other group columns, the * move is handled in a columnProcessor, so gets called as part of refresh * * @param {Grid} grid grid object * @param {GridColumn} column the column we want to ungroup */ ungroupColumn: function( grid, column) { if ( typeof(column.grouping) === 'undefined' ) { return; } delete column.grouping.groupPriority; delete column.treeAggregation; delete column.customTreeAggregationFinalizer; if (column.previousSort) { column.sort = column.previousSort; delete column.previousSort; } service.tidyPriorities( grid ); grid.api.grouping.raise.groupingChanged(column); grid.api.core.raise.sortChanged(grid, grid.getColumnSorting()); grid.queueGridRefresh(); }, /** * @ngdoc function * @name aggregateColumn * @methodOf ui.grid.grouping.service:uiGridGroupingService * @description Sets the aggregation type on a column, if the * column is currently grouped then it removes the grouping first. * * @param {Grid} grid grid object * @param {GridColumn} column the column we want to aggregate * @param {string} aggregationType of the recognised types from uiGridGroupingConstants or one of the custom aggregations from gridOptions * @param {string} aggregationLabel to be used instead of the default label. If empty string is passed, label is omitted */ aggregateColumn: function( grid, column, aggregationType, aggregationLabel ) { if (typeof(column.grouping) !== 'undefined' && typeof(column.grouping.groupPriority) !== 'undefined' && column.grouping.groupPriority >= 0) { service.ungroupColumn( grid, column ); } var aggregationDef = {}; if ( typeof(grid.options.treeCustomAggregations[aggregationType]) !== 'undefined' ) { aggregationDef = grid.options.treeCustomAggregations[aggregationType]; } else if ( typeof(uiGridTreeBaseService.nativeAggregations()[aggregationType]) !== 'undefined' ) { aggregationDef = uiGridTreeBaseService.nativeAggregations()[aggregationType]; } column.treeAggregation = { type: aggregationType, label: ( typeof aggregationLabel === 'string') ? aggregationLabel : i18nService.get().aggregation[aggregationDef.label] || aggregationDef.label }; column.treeAggregationFn = aggregationDef.aggregationFn; column.treeAggregationFinalizerFn = aggregationDef.finalizerFn; grid.api.grouping.raise.aggregationChanged(column); grid.queueGridRefresh(); }, /** * @ngdoc function * @name setGrouping * @methodOf ui.grid.grouping.service:uiGridGroupingService * @description Set the grouping based on a config object, used by the save state feature * (more specifically, by the restore function in that feature ) * * @param {Grid} grid grid object * @param {object} config the config we want to set, same format as that returned by getGrouping */ setGrouping: function ( grid, config ) { if ( typeof(config) === 'undefined' ) { return; } // first remove any existing grouping service.clearGrouping(grid); if ( config.grouping && config.grouping.length && config.grouping.length > 0 ) { config.grouping.forEach( function( group ) { var col = grid.getColumn(group.colName); if ( col ) { service.groupColumn( grid, col ); } }); } if ( config.aggregations && config.aggregations.length ) { config.aggregations.forEach( function( aggregation ) { var col = grid.getColumn(aggregation.colName); if ( col ) { service.aggregateColumn( grid, col, aggregation.aggregation.type ); } }); } if ( config.rowExpandedStates ) { service.applyRowExpandedStates( grid.grouping.groupingHeaderCache, config.rowExpandedStates ); } }, /** * @ngdoc function * @name clearGrouping * @methodOf ui.grid.grouping.service:uiGridGroupingService * @description Clear any grouped columns and any aggregations. Doesn't remove sorting, * as we don't know whether that sorting was added by grouping or was there beforehand * * @param {Grid} grid grid object */ clearGrouping: function( grid ) { var currentGrouping = service.getGrouping(grid); if ( currentGrouping.grouping.length > 0 ) { currentGrouping.grouping.forEach( function( group ) { if (!group.col) { // should have a group.colName if there's no col group.col = grid.getColumn(group.colName); } service.ungroupColumn(grid, group.col); }); } if ( currentGrouping.aggregations.length > 0 ) { currentGrouping.aggregations.forEach( function( aggregation ) { if (!aggregation.col) { // should have a group.colName if there's no col aggregation.col = grid.getColumn(aggregation.colName); } service.aggregateColumn(grid, aggregation.col, null); }); } }, /** * @ngdoc function * @name tidyPriorities * @methodOf ui.grid.grouping.service:uiGridGroupingService * @description Renumbers groupPriority and sortPriority such that * groupPriority is contiguous, and sortPriority either matches * groupPriority (for group columns), and otherwise is contiguous and * higher than groupPriority. * * @param {Grid} grid grid object */ tidyPriorities: function( grid ) { // if we're called from sortChanged, grid is in this, not passed as param, the param can be a column or undefined if ( ( typeof(grid) === 'undefined' || typeof(grid.grid) !== 'undefined' ) && typeof(this.grid) !== 'undefined' ) { grid = this.grid; } var groupArray = [], sortArray = []; grid.columns.forEach( function(column, index) { if ( typeof(column.grouping) !== 'undefined' && typeof(column.grouping.groupPriority) !== 'undefined' && column.grouping.groupPriority >= 0) { groupArray.push(column); } else if ( typeof(column.sort) !== 'undefined' && typeof(column.sort.priority) !== 'undefined' && column.sort.priority >= 0) { sortArray.push(column); } }); groupArray.sort(function(a, b) { return a.grouping.groupPriority - b.grouping.groupPriority; }); groupArray.forEach( function(column, index) { column.grouping.groupPriority = index; column.suppressRemoveSort = true; if ( typeof(column.sort) === 'undefined') { column.sort = {}; } column.sort.priority = index; }); var i = groupArray.length; sortArray.sort(function(a, b) { return a.sort.priority - b.sort.priority; }); sortArray.forEach(function(column) { column.sort.priority = i; column.suppressRemoveSort = column.colDef.suppressRemoveSort; i++; }); }, /** * @ngdoc function * @name groupRows * @methodOf ui.grid.grouping.service:uiGridGroupingService * @description The rowProcessor that creates the groupHeaders (i.e. does * the actual grouping). * * Assumes it is always called after the sorting processor, guaranteed by the priority setting * * Processes all the rows in order, inserting a groupHeader row whenever there is a change * in value of a grouped row, based on the sortAlgorithm used for the column. The group header row * is looked up in the groupHeaderCache, and used from there if there is one. The entity is reset * to {} if one is found. * * As it processes it maintains a `processingState` array. This records, for each level of grouping we're * working with, the following information: * ``` * { * fieldName: name, * col: col, * initialised: boolean, * currentValue: value, * currentRow: gridRow, * } * ``` * We look for changes in the currentValue at any of the levels. Where we find a change we: * * - create a new groupHeader row in the array * * @param {array} renderableRows the rows we want to process, usually the output from the previous rowProcessor * @returns {array} the updated rows, including our new group rows */ groupRows: function( renderableRows ) { if (renderableRows.length === 0) { return renderableRows; } var grid = this; grid.grouping.oldGroupingHeaderCache = grid.grouping.groupingHeaderCache || {}; grid.grouping.groupingHeaderCache = {}; var processingState = service.initialiseProcessingState( grid ); // processes each of the fields we are grouping by, checks if the value has changed and inserts a groupHeader // Broken out as shouldn't create functions in a loop. var updateProcessingState = function( groupFieldState, stateIndex ) { var fieldValue = grid.getCellValue(row, groupFieldState.col); // look for change of value - and insert a header if ( !groupFieldState.initialised || rowSorter.getSortFn(grid, groupFieldState.col, renderableRows)(fieldValue, groupFieldState.currentValue) !== 0 ) { service.insertGroupHeader( grid, renderableRows, i, processingState, stateIndex ); i++; } }; // use a for loop because it's tolerant of the array length changing whilst we go - we can // manipulate the iterator when we insert groupHeader rows for (var i = 0; i < renderableRows.length; i++ ) { var row = renderableRows[i]; if ( row.visible ) { processingState.forEach( updateProcessingState ); } } delete grid.grouping.oldGroupingHeaderCache; return renderableRows; }, /** * @ngdoc function * @name initialiseProcessingState * @methodOf ui.grid.grouping.service:uiGridGroupingService * @description Creates the processing state array that is used * for groupRows. * * @param {Grid} grid grid object * @returns {array} an array in the format described in the groupRows method, * initialised with blank values */ initialiseProcessingState: function( grid ) { var processingState = []; var columnSettings = service.getGrouping( grid ); columnSettings.grouping.forEach( function( groupItem, index) { processingState.push({ fieldName: groupItem.field, col: groupItem.col, initialised: false, currentValue: null, currentRow: null }); }); return processingState; }, /** * @ngdoc function * @name getGrouping * @methodOf ui.grid.grouping.service:uiGridGroupingService * @description Get the grouping settings from the columns. As a side effect * this always renumbers the grouping starting at 0 * @param {Grid} grid grid object * @returns {array} an array of the group fields, in order of priority */ getGrouping: function( grid ) { var groupArray = [], aggregateArray = []; // get all the grouping grid.columns.forEach(function(column) { if ( column.grouping ) { if ( typeof(column.grouping.groupPriority) !== 'undefined' && column.grouping.groupPriority >= 0) { groupArray.push({ field: column.field, col: column, groupPriority: column.grouping.groupPriority, grouping: column.grouping }); } } if ( column.treeAggregation && column.treeAggregation.type ) { aggregateArray.push({ field: column.field, col: column, aggregation: column.treeAggregation }); } }); // sort grouping into priority order groupArray.sort( function(a, b) { return a.groupPriority - b.groupPriority; }); // renumber the priority in case it was somewhat messed up, then remove the grouping reference groupArray.forEach( function( group, index) { group.grouping.groupPriority = index; group.groupPriority = index; delete group.grouping; }); return { grouping: groupArray, aggregations: aggregateArray }; }, /** * @ngdoc function * @name insertGroupHeader * @methodOf ui.grid.grouping.service:uiGridGroupingService * @description Create a group header row, and link it to the various configuration * items that we use. * * Look for the row in the oldGroupingHeaderCache, write the row into the new groupingHeaderCache. * * @param {Grid} grid grid object * @param {array} renderableRows the rows that we are processing * @param {number} rowIndex the row we were up to processing * @param {array} processingState the current processing state * @param {number} stateIndex the processing state item that we were on when we triggered a new group header - * i.e. the column that we want to create a header for */ insertGroupHeader: function( grid, renderableRows, rowIndex, processingState, stateIndex ) { // set the value that caused the end of a group into the header row and the processing state var col = processingState[stateIndex].col, newValue = grid.getCellValue(renderableRows[rowIndex], col), newDisplayValue = newValue; if ( typeof(newValue) === 'undefined' || newValue === null ) { newDisplayValue = grid.options.groupingNullLabel; } function getKeyAsValueForCacheMap(key) { return angular.isObject(key) ? JSON.stringify(key) : key; } var cacheItem = grid.grouping.oldGroupingHeaderCache; for ( var i = 0; i < stateIndex; i++ ) { if ( cacheItem && cacheItem[getKeyAsValueForCacheMap(processingState[i].currentValue)] ) { cacheItem = cacheItem[getKeyAsValueForCacheMap(processingState[i].currentValue)].children; } } var headerRow; if ( cacheItem && cacheItem[getKeyAsValueForCacheMap(newValue)]) { headerRow = cacheItem[getKeyAsValueForCacheMap(newValue)].row; headerRow.entity = {}; } else { headerRow = new GridRow( {}, null, grid ); gridClassFactory.rowTemplateAssigner.call(grid, headerRow); } headerRow.entity['$$' + processingState[stateIndex].col.uid] = { groupVal: newDisplayValue }; headerRow.treeLevel = stateIndex; headerRow.groupHeader = true; headerRow.internalRow = true; headerRow.enableCellEdit = false; headerRow.enableSelection = grid.options.enableGroupHeaderSelection; processingState[stateIndex].initialised = true; processingState[stateIndex].currentValue = newValue; processingState[stateIndex].currentRow = headerRow; // set all processing states below this one to not be initialised - change of this state // means all those need to start again service.finaliseProcessingState( processingState, stateIndex + 1); // insert our new header row renderableRows.splice(rowIndex, 0, headerRow); // add our new header row to the cache cacheItem = grid.grouping.groupingHeaderCache; for ( i = 0; i < stateIndex; i++ ) { cacheItem = cacheItem[getKeyAsValueForCacheMap(processingState[i].currentValue)].children; } cacheItem[getKeyAsValueForCacheMap(newValue)] = { row: headerRow, children: {} }; }, /** * @ngdoc function * @name finaliseProcessingState * @methodOf ui.grid.grouping.service:uiGridGroupingService * @description Set all processing states lower than the one that had a break in value to * no longer be initialised. Render the counts into the entity ready for display. * * @param {array} processingState the current processing state * @param {number} stateIndex the processing state item that we were on when we triggered a new group header, all * processing states after this need to be finalised */ finaliseProcessingState: function( processingState, stateIndex ) { for ( var i = stateIndex; i < processingState.length; i++) { processingState[i].initialised = false; processingState[i].currentRow = null; processingState[i].currentValue = null; } }, /** * @ngdoc function * @name getRowExpandedStates * @methodOf ui.grid.grouping.service:uiGridGroupingService * @description Extract the groupHeaderCache hash, pulling out only the states. * * The example below shows a grid that is grouped by gender then age * * <pre> * { * male: { * state: 'expanded', * children: { * 22: { state: 'expanded' }, * 30: { state: 'collapsed' } * } * }, * female: { * state: 'expanded', * children: { * 28: { state: 'expanded' }, * 55: { state: 'collapsed' } * } * } * } * </pre> * * @param {object} treeChildren The tree children elements object * @returns {object} the expanded states as an object */ getRowExpandedStates: function(treeChildren) { if ( typeof(treeChildren) === 'undefined' ) { return {}; } var newChildren = {}; angular.forEach( treeChildren, function( value, key ) { newChildren[key] = { state: value.row.treeNode.state }; if ( value.children ) { newChildren[key].children = service.getRowExpandedStates( value.children ); } else { newChildren[key].children = {}; } }); return newChildren; }, /** * @ngdoc function * @name applyRowExpandedStates * @methodOf ui.grid.grouping.service:uiGridGroupingService * @description Take a hash in the format as created by getRowExpandedStates, * and apply it to the grid.grouping.groupHeaderCache. * * Takes a treeSubset, and applies to a treeSubset - so can be called * recursively. * * @param {object} currentNode can be grid.grouping.groupHeaderCache, or any of * the children of that hash * @param {object} expandedStates can be the full expanded states, or children * of that expanded states (which hopefully matches the subset of the groupHeaderCache) */ applyRowExpandedStates: function( currentNode, expandedStates ) { if ( typeof(expandedStates) === 'undefined' ) { return; } angular.forEach(expandedStates, function( value, key ) { if ( currentNode[key] ) { currentNode[key].row.treeNode.state = value.state; if (value.children && currentNode[key].children) { service.applyRowExpandedStates( currentNode[key].children, value.children ); } } }); } }; return service; }]); /** * @ngdoc directive * @name ui.grid.grouping.directive:uiGridGrouping * @element div * @restrict A * * @description Adds grouping features to grid * * @example <example module=