angular-ui-grid
Version:
A data grid for Angular
1,244 lines (1,124 loc) • 68.6 kB
JavaScript
/*!
* ui-grid - v4.9.1 - 2020-10-26
* Copyright (c) 2020 ; License: MIT
*/
(function () {
'use strict';
/**
* @ngdoc overview
* @name ui.grid.treeBase
* @description
*
* # ui.grid.treeBase
*
* <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 base tree handling functions that are shared by other features, notably grouping
* and treeView. It provides a tree view of the data, with nodes in that
* tree and leaves.
*
* Design information:
* -------------------
*
* The raw data that is provided must come with a $$treeLevel on any non-leaf node. Grouping will create
* these on all the group header rows, treeView will expect these to be set in the raw data by the user.
* TreeBase will run a rowsProcessor that:
* - builds `treeBase.tree` out of the provided rows
* - permits a recursive sort of the tree
* - maintains the expand/collapse state of each node
* - provides the expand/collapse all button and the expand/collapse buttons
* - maintains the count of children for each node
*
* Each row is updated with a link to the tree node that represents it. Refer {@link ui.grid.treeBase.grid:treeBase.tree tree documentation}
* for information.
*
* TreeBase adds information to the rows
* - treeLevel: if present and > -1 tells us the level (level 0 is the top level)
* - treeNode: pointer to the node in the grid.treeBase.tree that refers
* to this row, allowing us to manipulate the state
*
* Since the logic is baked into the rowsProcessors, it should get triggered whenever
* row order or filtering or anything like that is changed. We recall the expanded state
* across invocations of the rowsProcessors by the reference to the treeNode on the individual
* rows. We rebuild the tree itself quite frequently, when we do this we use the saved treeNodes to
* get the state, but we overwrite the other data in that treeNode.
*
* 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.
*
* We rely on the rowsProcessors to do the actual expanding and collapsing, so we set the flags we want into
* grid.treeBase.tree, then call refresh. This is because we can't easily change the visible
* row cache without calling the processors, and once we've built the logic into the rowProcessors we may as
* well use it all the time.
*
* Tree base provides sorting (on non-grouped columns).
*
* Sorting works in two passes. The standard sorting is performed for any columns that are important to building
* the tree (for example, any grouped columns). Then after the tree is built, a recursive tree sort is performed
* for the remaining sort columns (including the original sort) - these columns are sorted within each tree level
* (so all the level 1 nodes are sorted, then all the level 2 nodes within each level 1 node etc).
*
* To achieve this we make use of the `ignoreSort` property on the sort configuration. The parent feature (treeView or grouping)
* must provide a rowsProcessor that runs with very low priority (typically in the 60-65 range), and that sets
* the `ignoreSort`on any sort that it wants to run on the tree. TreeBase will clear the ignoreSort on all sorts - so it
* will turn on any sorts that haven't run. It will then call a recursive sort on the tree.
*
* Tree base provides treeAggregation. It checks the treeAggregation configuration on each column, and aggregates based on
* the logic provided as it builds the tree. Footer aggregation from the uiGrid core should not be used with treeBase aggregation,
* since it operates on all visible rows, as opposed to to leaf nodes only. Setting `showColumnFooter: true` will show the
* treeAggregations in the column footer. Aggregation information will be collected in the format:
*
* ```
* {
* type: 'count',
* value: 4,
* label: 'count: ',
* rendered: 'count: 4'
* }
* ```
*
* A callback is provided to format the value once it is finalised (aka a valueFilter).
*
* <br/>
* <br/>
*
* <div doc-module-components="ui.grid.treeBase"></div>
*/
var module = angular.module('ui.grid.treeBase', ['ui.grid']);
/**
* @ngdoc object
* @name ui.grid.treeBase.constant:uiGridTreeBaseConstants
*
* @description constants available in treeBase module.
*
* These constants are manually copied into grouping and treeView,
* as I haven't found a way to simply include them, and it's not worth
* investing time in for something that changes very infrequently.
*
*/
module.constant('uiGridTreeBaseConstants', {
featureName: "treeBase",
rowHeaderColName: 'treeBaseRowHeaderCol',
EXPANDED: 'expanded',
COLLAPSED: 'collapsed',
aggregation: {
COUNT: 'count',
SUM: 'sum',
MAX: 'max',
MIN: 'min',
AVG: 'avg'
}
});
/**
* @ngdoc service
* @name ui.grid.treeBase.service:uiGridTreeBaseService
*
* @description Services for treeBase feature
*/
/**
* @ngdoc object
* @name ui.grid.treeBase.api:ColumnDef
*
* @description ColumnDef for tree feature, these are available to be
* set using the ui-grid {@link ui.grid.class:GridOptions.columnDef gridOptions.columnDefs}
*/
module.service('uiGridTreeBaseService', ['$q', 'uiGridTreeBaseConstants', 'gridUtil', 'GridRow', 'gridClassFactory', 'i18nService', 'uiGridConstants', 'rowSorter',
function ($q, uiGridTreeBaseConstants, gridUtil, GridRow, gridClassFactory, i18nService, uiGridConstants, rowSorter) {
var service = {
initializeGrid: function (grid) {
// add feature namespace and any properties to grid for needed
/**
* @ngdoc object
* @name ui.grid.treeBase.grid:treeBase
*
* @description Grid properties and functions added for treeBase
*/
grid.treeBase = {};
/**
* @ngdoc property
* @propertyOf ui.grid.treeBase.grid:treeBase
* @name numberLevels
*
* @description Total number of tree levels currently used, calculated by the rowsProcessor by
* retaining the highest tree level it sees
*/
grid.treeBase.numberLevels = 0;
/**
* @ngdoc property
* @propertyOf ui.grid.treeBase.grid:treeBase
* @name expandAll
*
* @description Whether or not the expandAll box is selected
*/
grid.treeBase.expandAll = false;
/**
* @ngdoc property
* @propertyOf ui.grid.treeBase.grid:treeBase
* @name tree
*
* @description Tree represented as a nested array that holds the state of each node, along with a
* pointer to the row. The array order is material - we will display the children in the order
* they are stored in the array
*
* Each node stores:
*
* - the state of this node
* - an array of children of this node
* - a pointer to the parent of this node (reverse pointer, allowing us to walk up the tree)
* - the number of children of this node
* - aggregation information calculated from the nodes
*
* ```
* [{
* state: 'expanded',
* row: <reference to row>,
* parentRow: null,
* aggregations: [{
* type: 'count',
* col: <gridCol>,
* value: 2,
* label: 'count: ',
* rendered: 'count: 2'
* }],
* children: [
* {
* state: 'expanded',
* row: <reference to row>,
* parentRow: <reference to row>,
* aggregations: [{
* type: 'count',
* col: '<gridCol>,
* value: 4,
* label: 'count: ',
* rendered: 'count: 4'
* }],
* children: [
* { state: 'expanded', row: <reference to row>, parentRow: <reference to row> },
* { state: 'collapsed', row: <reference to row>, parentRow: <reference to row> },
* { state: 'expanded', row: <reference to row>, parentRow: <reference to row> },
* { state: 'collapsed', row: <reference to row>, parentRow: <reference to row> }
* ]
* },
* {
* state: 'collapsed',
* row: <reference to row>,
* parentRow: <reference to row>,
* aggregations: [{
* type: 'count',
* col: <gridCol>,
* value: 3,
* label: 'count: ',
* rendered: 'count: 3'
* }],
* children: [
* { state: 'expanded', row: <reference to row>, parentRow: <reference to row> },
* { state: 'collapsed', row: <reference to row>, parentRow: <reference to row> },
* { state: 'expanded', row: <reference to row>, parentRow: <reference to row> }
* ]
* }
* ]
* }, {<another level 0 node maybe>} ]
* ```
* Missing state values are false - meaning they aren't expanded.
*
* This is used because the rowProcessors run every time the grid is refreshed, so
* we'd lose the expanded state every time the grid was refreshed. This instead gives
* us a reliable lookup that persists across rowProcessors.
*
* This tree is rebuilt every time we run the rowsProcessors. Since each row holds a pointer
* to it's tree node we can persist expand/collapse state across calls to rowsProcessor, we discard
* all transient information on the tree (children, childCount) and recalculate it
*
*/
grid.treeBase.tree = [];
service.defaultGridOptions(grid.options);
grid.registerRowsProcessor(service.treeRows, 410);
grid.registerColumnBuilder( service.treeBaseColumnBuilder );
service.createRowHeader( grid );
/**
* @ngdoc object
* @name ui.grid.treeBase.api:PublicApi
*
* @description Public Api for treeBase feature
*/
var publicApi = {
events: {
treeBase: {
/**
* @ngdoc event
* @eventOf ui.grid.treeBase.api:PublicApi
* @name rowExpanded
* @description raised whenever a row is expanded. If you are dynamically
* rendering your tree you can listen to this event, and then retrieve
* the children of this row and load them into the grid data.
*
* When the data is loaded the grid will automatically refresh to show these new rows
*
* <pre>
* gridApi.treeBase.on.rowExpanded(scope,function(row) {})
* </pre>
* @param {gridRow} row the row that was expanded. You can also
* retrieve the grid from this row with row.grid
*/
rowExpanded: {},
/**
* @ngdoc event
* @eventOf ui.grid.treeBase.api:PublicApi
* @name rowCollapsed
* @description raised whenever a row is collapsed. Doesn't really have
* a purpose at the moment, included for symmetry
*
* <pre>
* gridApi.treeBase.on.rowCollapsed(scope,function(row) {})
* </pre>
* @param {gridRow} row the row that was collapsed. You can also
* retrieve the grid from this row with row.grid
*/
rowCollapsed: {}
}
},
methods: {
treeBase: {
/**
* @ngdoc function
* @name expandAllRows
* @methodOf ui.grid.treeBase.api:PublicApi
* @description Expands all tree rows
*/
expandAllRows: function () {
service.expandAllRows(grid);
},
/**
* @ngdoc function
* @name collapseAllRows
* @methodOf ui.grid.treeBase.api:PublicApi
* @description collapse all tree rows
*/
collapseAllRows: function () {
service.collapseAllRows(grid);
},
/**
* @ngdoc function
* @name toggleRowTreeState
* @methodOf ui.grid.treeBase.api:PublicApi
* @description call expand if the row is collapsed, collapse if it is expanded
* @param {gridRow} row the row you wish to toggle
*/
toggleRowTreeState: function (row) {
service.toggleRowTreeState(grid, row);
},
/**
* @ngdoc function
* @name expandRow
* @methodOf ui.grid.treeBase.api:PublicApi
* @description expand the immediate children of the specified row
* @param {gridRow} row the row you wish to expand
* @param {boolean} recursive true if you wish to expand the row's ancients
*/
expandRow: function (row, recursive) {
service.expandRow(grid, row, recursive);
},
/**
* @ngdoc function
* @name expandRowChildren
* @methodOf ui.grid.treeBase.api:PublicApi
* @description expand all children of the specified row
* @param {gridRow} row the row you wish to expand
*/
expandRowChildren: function (row) {
service.expandRowChildren(grid, row);
},
/**
* @ngdoc function
* @name collapseRow
* @methodOf ui.grid.treeBase.api:PublicApi
* @description collapse the specified row. When
* you expand the row again, all grandchildren will retain their state
* @param {gridRow} row the row you wish to collapse
*/
collapseRow: function ( row ) {
service.collapseRow(grid, row);
},
/**
* @ngdoc function
* @name collapseRowChildren
* @methodOf ui.grid.treeBase.api:PublicApi
* @description collapse all children of the specified row. When
* you expand the row again, all grandchildren will be collapsed
* @param {gridRow} row the row you wish to collapse children for
*/
collapseRowChildren: function ( row ) {
service.collapseRowChildren(grid, row);
},
/**
* @ngdoc function
* @name getTreeState
* @methodOf ui.grid.treeBase.api:PublicApi
* @description Get the tree state for this grid,
* used by the saveState feature
* Returned treeState as an object
* `{ expandedState: { uid: 'expanded', uid: 'collapsed' } }`
* where expandedState is a hash of row uid and the current expanded state
*
* @returns {object} tree state
*
* TODO - this needs work - we need an identifier that persists across instantiations,
* not uid. This really means we need a row identity defined, but that won't work for
* grouping. Perhaps this needs to be moved up to treeView and grouping, rather than
* being in base.
*/
getTreeExpandedState: function () {
return { expandedState: service.getTreeState(grid) };
},
/**
* @ngdoc function
* @name setTreeState
* @methodOf ui.grid.treeBase.api:PublicApi
* @description Set the expanded states of the tree
* @param {object} config the config you want to apply, in the format
* provided by getTreeState
*/
setTreeState: function ( config ) {
service.setTreeState( grid, config );
},
/**
* @ngdoc function
* @name getRowChildren
* @methodOf ui.grid.treeBase.api:PublicApi
* @description Get the children of the specified row
* @param {GridRow} row the row you want the children of
* @returns {Array} array of children of this row, the children
* are all gridRows
*/
getRowChildren: function ( row ) {
return row.treeNode.children.map( function( childNode ) {
return childNode.row;
});
}
}
}
};
grid.api.registerEventsFromObject(publicApi.events);
grid.api.registerMethodsFromObject(publicApi.methods);
},
defaultGridOptions: function (gridOptions) {
// default option to true unless it was explicitly set to false
/**
* @ngdoc object
* @name ui.grid.treeBase.api:GridOptions
*
* @description GridOptions for treeBase feature, these are available to be
* set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
*/
/**
* @ngdoc object
* @name treeRowHeaderBaseWidth
* @propertyOf ui.grid.treeBase.api:GridOptions
* @description Base width of the tree header, provides for a single level of tree. This
* is incremented by `treeIndent` for each extra level
* <br/>Defaults to 30
*/
gridOptions.treeRowHeaderBaseWidth = gridOptions.treeRowHeaderBaseWidth || 30;
/**
* @ngdoc object
* @name treeIndent
* @propertyOf ui.grid.treeBase.api:GridOptions
* @description Number of pixels of indent for the icon at each tree level, wider indents are visually more pleasing,
* but will make the tree row header wider
* <br/>Defaults to 10
*/
gridOptions.treeIndent = (gridOptions.treeIndent != null) ? gridOptions.treeIndent : 10;
/**
* @ngdoc object
* @name showTreeRowHeader
* @propertyOf ui.grid.treeBase.api:GridOptions
* @description If set to false, don't create the row header. You'll need to programmatically control the expand
* states
* <br/>Defaults to true
*/
gridOptions.showTreeRowHeader = gridOptions.showTreeRowHeader !== false;
/**
* @ngdoc object
* @name showTreeExpandNoChildren
* @propertyOf ui.grid.treeBase.api:GridOptions
* @description If set to true, show the expand/collapse button even if there are no
* children of a node. You'd use this if you're planning to dynamically load the children
*
* <br/>Defaults to true, grouping overrides to false
*/
gridOptions.showTreeExpandNoChildren = gridOptions.showTreeExpandNoChildren !== false;
/**
* @ngdoc object
* @name treeRowHeaderAlwaysVisible
* @propertyOf ui.grid.treeBase.api:GridOptions
* @description If set to true, row header even if there are no tree nodes
*
* <br/>Defaults to true
*/
gridOptions.treeRowHeaderAlwaysVisible = gridOptions.treeRowHeaderAlwaysVisible !== false;
/**
* @ngdoc object
* @name treeCustomAggregations
* @propertyOf ui.grid.treeBase.api:GridOptions
* @description Define custom aggregation functions. The properties of this object will be
* aggregation types available for use on columnDef with {@link ui.grid.treeBase.api:ColumnDef treeAggregationType} or through the column menu.
* If a function defined here uses the same name as one of the native aggregations, this one will take precedence.
* The object format is:
*
* <pre>
* {
* aggregationName: {
* label: (optional) string,
* aggregationFn: function( aggregation, fieldValue, numValue, row ) {...},
* finalizerFn: (optional) function( aggregation ) {...}
* },
* mean: {
* label: 'mean',
* aggregationFn: function( aggregation, fieldValue, numValue ) {
* aggregation.count = (aggregation.count || 1) + 1;
* aggregation.sum = (aggregation.sum || 0) + numValue;
* },
* finalizerFn: function( aggregation ) {
* aggregation.value = aggregation.sum / aggregation.count
* }
* }
* }
* </pre>
*
* <br/>The `finalizerFn` may be used to manipulate the value before rendering, or to
* apply a custom rendered value. If `aggregation.rendered` is left undefined, the value will be
* rendered. Note that the native aggregation functions use an `finalizerFn` to concatenate
* the label and the value.
*
* <br/>Defaults to {}
*/
gridOptions.treeCustomAggregations = gridOptions.treeCustomAggregations || {};
/**
* @ngdoc object
* @name enableExpandAll
* @propertyOf ui.grid.treeBase.api:GridOptions
* @description Enable the expand all button at the top of the row header
*
* <br/>Defaults to true
*/
gridOptions.enableExpandAll = gridOptions.enableExpandAll !== false;
},
/**
* @ngdoc function
* @name treeBaseColumnBuilder
* @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
* @description Sets the tree 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
*/
treeBaseColumnBuilder: function (colDef, col, gridOptions) {
/**
* @ngdoc object
* @name customTreeAggregationFn
* @propertyOf ui.grid.treeBase.api:ColumnDef
* @description A custom function that aggregates rows into some form of
* total. Aggregations run row-by-row, the function needs to be capable of
* creating a running total.
*
* The function will be provided the aggregation item (in which you can store running
* totals), the row value that is to be aggregated, and that same row value converted to
* a number (most aggregations work on numbers)
* @example
* <pre>
* customTreeAggregationFn = function ( aggregation, fieldValue, numValue, row ) {
* // calculates the average of the squares of the values
* if ( typeof(aggregation.count) === 'undefined' ) {
* aggregation.count = 0;
* }
* aggregation.count++;
*
* if ( !isNaN(numValue) ) {
* if ( typeof(aggregation.total) === 'undefined' ) {
* aggregation.total = 0;
* }
* aggregation.total = aggregation.total + numValue * numValue;
* }
*
* aggregation.value = aggregation.total / aggregation.count;
* }
* </pre>
* <br/>Defaults to undefined. May be overwritten by treeAggregationType, the two options should not be used together.
*/
if ( typeof(colDef.customTreeAggregationFn) !== 'undefined' ) {
col.treeAggregationFn = colDef.customTreeAggregationFn;
}
/**
* @ngdoc object
* @name treeAggregationType
* @propertyOf ui.grid.treeBase.api:ColumnDef
* @description Use one of the native or grid-level aggregation methods for calculating aggregations on this column.
* Native method are in the constants file and include: SUM, COUNT, MIN, MAX, AVG. This may also be the property the
* name of an aggregation function defined with {@link ui.grid.treeBase.api:GridOptions treeCustomAggregations}.
*
* <pre>
* treeAggregationType = uiGridTreeBaseConstants.aggregation.SUM,
* }
* </pre>
*
* If you are using aggregations you should either:
*
* - also use grouping, in which case the aggregations are displayed in the group header, OR
* - use treeView, in which case you can set `treeAggregationUpdateEntity: true` in the colDef, and
* treeBase will store the aggregation information in the entity, or you can set `treeAggregationUpdateEntity: false`
* in the colDef, and you need to manual retrieve the calculated aggregations from the row.treeNode.aggregations
*
* <br/>Takes precendence over a treeAggregationFn, the two options should not be used together.
* <br/>Defaults to undefined.
*/
if ( typeof(colDef.treeAggregationType) !== 'undefined' ) {
col.treeAggregation = { type: colDef.treeAggregationType };
if ( typeof(gridOptions.treeCustomAggregations[colDef.treeAggregationType]) !== 'undefined' ) {
col.treeAggregationFn = gridOptions.treeCustomAggregations[colDef.treeAggregationType].aggregationFn;
col.treeAggregationFinalizerFn = gridOptions.treeCustomAggregations[colDef.treeAggregationType].finalizerFn;
col.treeAggregation.label = gridOptions.treeCustomAggregations[colDef.treeAggregationType].label;
}
else if ( typeof(service.nativeAggregations()[colDef.treeAggregationType]) !== 'undefined' ) {
col.treeAggregationFn = service.nativeAggregations()[colDef.treeAggregationType].aggregationFn;
col.treeAggregation.label = service.nativeAggregations()[colDef.treeAggregationType].label;
}
}
/**
* @ngdoc object
* @name treeAggregationLabel
* @propertyOf ui.grid.treeBase.api:ColumnDef
* @description A custom label to use for this aggregation. If provided we don't use native i18n.
*/
if ( typeof(colDef.treeAggregationLabel) !== 'undefined' ) {
if (typeof(col.treeAggregation) === 'undefined' ) {
col.treeAggregation = {};
}
col.treeAggregation.label = colDef.treeAggregationLabel;
}
/**
* @ngdoc object
* @name treeAggregationUpdateEntity
* @propertyOf ui.grid.treeBase.api:ColumnDef
* @description Store calculated aggregations into the entity, allowing them
* to be displayed in the grid using a standard cellTemplate. This defaults to true,
* if you are using grouping then you shouldn't set it to false, as then the aggregations won't
* display.
*
* If you are using treeView in most cases you'll want to set this to true. This will result in
* getCellValue returning the aggregation rather than whatever was stored in the cell attribute on
* the entity. If you want to render the underlying entity value (and do something else with the aggregation)
* then you could use a custom cellTemplate to display `row.entity.myAttribute`, rather than using getCellValue.
*
* <br/>Defaults to true
*
* @example
* <pre>
* gridOptions.columns = [{
* name: 'myCol',
* treeAggregation: { type: uiGridTreeBaseConstants.aggregation.SUM },
* treeAggregationUpdateEntity: true
* cellTemplate: '<div>{{row.entity.myCol + " " + row.treeNode.aggregations[0].rendered}}</div>'
* }];
* </pre>
*/
col.treeAggregationUpdateEntity = colDef.treeAggregationUpdateEntity !== false;
/**
* @ngdoc object
* @name customTreeAggregationFinalizerFn
* @propertyOf ui.grid.treeBase.api:ColumnDef
* @description A custom function that populates aggregation.rendered, this is called when
* a particular aggregation has been fully calculated, and we want to render the value.
*
* With the native aggregation options we just concatenate `aggregation.label` and
* `aggregation.value`, but if you wanted to apply a filter or otherwise manipulate the label
* or the value, you can do so with this function. This function will be called after the
* the default `finalizerFn`.
*
* @example
* <pre>
* customTreeAggregationFinalizerFn = function ( aggregation ) {
* aggregation.rendered = aggregation.label + aggregation.value / 100 + '%';
* }
* </pre>
* <br/>Defaults to undefined.
*/
if ( typeof(col.customTreeAggregationFinalizerFn) === 'undefined' ) {
col.customTreeAggregationFinalizerFn = colDef.customTreeAggregationFinalizerFn;
}
},
/**
* @ngdoc function
* @name createRowHeader
* @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
* @description Create the rowHeader. If treeRowHeaderAlwaysVisible then
* set it to visible, otherwise set it to invisible
*
* @param {Grid} grid grid object
*/
createRowHeader: function( grid ) {
var rowHeaderColumnDef = {
name: uiGridTreeBaseConstants.rowHeaderColName,
displayName: '',
width: grid.options.treeRowHeaderBaseWidth,
minWidth: 10,
cellTemplate: 'ui-grid/treeBaseRowHeader',
headerCellTemplate: 'ui-grid/treeBaseHeaderCell',
enableColumnResizing: false,
enableColumnMenu: false,
exporterSuppressExport: true,
allowCellFocus: true
};
rowHeaderColumnDef.visible = grid.options.treeRowHeaderAlwaysVisible;
grid.addRowHeaderColumn(rowHeaderColumnDef, -100);
},
/**
* @ngdoc function
* @name expandAllRows
* @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
* @description Expands all nodes in the tree
*
* @param {Grid} grid grid object
*/
expandAllRows: function (grid) {
grid.treeBase.tree.forEach( function( node ) {
service.setAllNodes( grid, node, uiGridTreeBaseConstants.EXPANDED);
});
grid.treeBase.expandAll = true;
grid.queueGridRefresh();
},
/**
* @ngdoc function
* @name collapseAllRows
* @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
* @description Collapses all nodes in the tree
*
* @param {Grid} grid grid object
*/
collapseAllRows: function (grid) {
grid.treeBase.tree.forEach( function( node ) {
service.setAllNodes( grid, node, uiGridTreeBaseConstants.COLLAPSED);
});
grid.treeBase.expandAll = false;
grid.queueGridRefresh();
},
/**
* @ngdoc function
* @name setAllNodes
* @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
* @description Works through a subset of grid.treeBase.rowExpandedStates, setting
* all child nodes (and their descendents) of the provided node to the given state.
*
* Calls itself recursively on all nodes so as to achieve this.
*
* @param {Grid} grid the grid we're operating on (so we can raise events)
* @param {object} treeNode a node in the tree that we want to update
* @param {string} targetState the state we want to set it to
*/
setAllNodes: function (grid, treeNode, targetState) {
if ( typeof(treeNode.state) !== 'undefined' && treeNode.state !== targetState ) {
treeNode.state = targetState;
if ( targetState === uiGridTreeBaseConstants.EXPANDED ) {
grid.api.treeBase.raise.rowExpanded(treeNode.row);
}
else {
grid.api.treeBase.raise.rowCollapsed(treeNode.row);
}
}
// set all child nodes
if ( treeNode.children ) {
treeNode.children.forEach(function( childNode ) {
service.setAllNodes(grid, childNode, targetState);
});
}
},
/**
* @ngdoc function
* @name toggleRowTreeState
* @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
* @description Toggles the expand or collapse state of this grouped row, if
* it's a parent row
*
* @param {Grid} grid grid object
* @param {GridRow} row the row we want to toggle
*/
toggleRowTreeState: function ( grid, row ) {
if ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ) {
return;
}
if (row.treeNode.state === uiGridTreeBaseConstants.EXPANDED) {
service.collapseRow(grid, row);
}
else {
service.expandRow(grid, row, false);
}
grid.queueGridRefresh();
},
/**
* @ngdoc function
* @name expandRow
* @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
* @description Expands this specific row, showing only immediate children.
*
* @param {Grid} grid grid object
* @param {GridRow} row the row we want to expand
* @param {boolean} recursive true if you wish to expand the row's ancients
*/
expandRow: function ( grid, row, recursive ) {
if ( recursive ) {
var parents = [];
while ( row && typeof(row.treeLevel) !== 'undefined' && row.treeLevel !== null && row.treeLevel >= 0 && row.treeNode.state !== uiGridTreeBaseConstants.EXPANDED ) {
parents.push(row);
row = row.treeNode.parentRow;
}
if ( parents.length > 0 ) {
row = parents.pop();
while ( row ) {
row.treeNode.state = uiGridTreeBaseConstants.EXPANDED;
grid.api.treeBase.raise.rowExpanded(row);
row = parents.pop();
}
grid.treeBase.expandAll = service.allExpanded(grid.treeBase.tree);
grid.queueGridRefresh();
}
}
else {
if ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ) {
return;
}
if ( row.treeNode.state !== uiGridTreeBaseConstants.EXPANDED ) {
row.treeNode.state = uiGridTreeBaseConstants.EXPANDED;
grid.api.treeBase.raise.rowExpanded(row);
grid.treeBase.expandAll = service.allExpanded(grid.treeBase.tree);
grid.queueGridRefresh();
}
}
},
/**
* @ngdoc function
* @name expandRowChildren
* @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
* @description Expands this specific row, showing all children.
*
* @param {Grid} grid grid object
* @param {GridRow} row the row we want to expand
*/
expandRowChildren: function ( grid, row ) {
if ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ) {
return;
}
service.setAllNodes(grid, row.treeNode, uiGridTreeBaseConstants.EXPANDED);
grid.treeBase.expandAll = service.allExpanded(grid.treeBase.tree);
grid.queueGridRefresh();
},
/**
* @ngdoc function
* @name collapseRow
* @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
* @description Collapses this specific row
*
* @param {Grid} grid grid object
* @param {GridRow} row the row we want to collapse
*/
collapseRow: function( grid, row ) {
if ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ) {
return;
}
if ( row.treeNode.state !== uiGridTreeBaseConstants.COLLAPSED ) {
row.treeNode.state = uiGridTreeBaseConstants.COLLAPSED;
grid.treeBase.expandAll = false;
grid.api.treeBase.raise.rowCollapsed(row);
grid.queueGridRefresh();
}
},
/**
* @ngdoc function
* @name collapseRowChildren
* @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
* @description Collapses this specific row and all children
*
* @param {Grid} grid grid object
* @param {GridRow} row the row we want to collapse
*/
collapseRowChildren: function( grid, row ) {
if ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ) {
return;
}
service.setAllNodes(grid, row.treeNode, uiGridTreeBaseConstants.COLLAPSED);
grid.treeBase.expandAll = false;
grid.queueGridRefresh();
},
/**
* @ngdoc function
* @name allExpanded
* @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
* @description Returns true if all rows are expanded, false
* if they're not. Walks the tree to determine this. Used
* to set the expandAll state.
*
* If the node has no children, then return true (it's immaterial
* whether it is expanded). If the node has children, then return
* false if this node is collapsed, or if any child node is not all expanded
*
* @param {object} tree the grid to check
* @returns {boolean} whether or not the tree is all expanded
*/
allExpanded: function( tree ) {
var allExpanded = true;
tree.forEach(function( node ) {
if ( !service.allExpandedInternal( node ) ) {
allExpanded = false;
}
});
return allExpanded;
},
allExpandedInternal: function( treeNode ) {
if ( treeNode.children && treeNode.children.length > 0 ) {
if ( treeNode.state === uiGridTreeBaseConstants.COLLAPSED ) {
return false;
}
var allExpanded = true;
treeNode.children.forEach( function( node ) {
if ( !service.allExpandedInternal( node ) ) {
allExpanded = false;
}
});
return allExpanded;
}
else {
return true;
}
},
/**
* @ngdoc function
* @name treeRows
* @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
* @description The rowProcessor that adds the nodes to the tree, and sets the visible
* state of each row based on it's parent state
*
* Assumes it is always called after the sorting processor, and the grouping processor if there is one.
* Performs any tree sorts itself after having built the tree
*
* Processes all the rows in order, setting the group level based on the $$treeLevel in the associated
* entity, and setting the visible state based on the parent's state.
*
* Calculates the deepest level of tree whilst it goes, and updates that so that the header column can be correctly
* sized.
*
* Aggregates if necessary along the way.
*
* @param {array} renderableRows the rows we want to process, usually the output from the previous rowProcessor
* @returns {array} the updated rows
*/
treeRows: function( renderableRows ) {
var grid = this;
if (renderableRows.length === 0) {
service.updateRowHeaderWidth( grid );
return renderableRows;
}
grid.treeBase.tree = service.createTree( grid, renderableRows );
service.updateRowHeaderWidth( grid );
service.sortTree( grid );
service.fixFilter( grid );
return service.renderTree( grid.treeBase.tree );
},
/**
* @ngdoc function
* @name createOrUpdateRowHeaderWidth
* @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
* @description Calculates the rowHeader width.
*
* If rowHeader is always present, updates the width.
*
* If rowHeader is only sometimes present (`treeRowHeaderAlwaysVisible: false`), determines whether there
* should be one, then creates or removes it as appropriate, with the created rowHeader having the
* right width.
*
* If there's never a rowHeader then never creates one: `showTreeRowHeader: false`
*
* @param {Grid} grid the grid we want to set the row header on
*/
updateRowHeaderWidth: function( grid ) {
var rowHeader = grid.getColumn(uiGridTreeBaseConstants.rowHeaderColName),
newWidth = grid.options.treeRowHeaderBaseWidth + grid.options.treeIndent * Math.max(grid.treeBase.numberLevels - 1, 0);
if ( rowHeader && newWidth !== rowHeader.width ) {
rowHeader.width = newWidth;
grid.queueRefresh();
}
var newVisibility = true;
if ( grid.options.showTreeRowHeader === false ) {
newVisibility = false;
}
if ( grid.options.treeRowHeaderAlwaysVisible === false && grid.treeBase.numberLevels <= 0 ) {
newVisibility = false;
}
if ( rowHeader && rowHeader.visible !== newVisibility ) {
rowHeader.visible = newVisibility;
rowHeader.colDef.visible = newVisibility;
grid.queueGridRefresh();
}
},
/**
* @ngdoc function
* @name renderTree
* @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
* @description Creates an array of rows based on the tree, exporting only
* the visible nodes and leaves
*
* @param {array} nodeList The list of nodes - can be grid.treeBase.tree, or can be node.children when
* we're calling recursively
* @returns {array} renderable rows
*/
renderTree: function( nodeList ) {
var renderableRows = [];
nodeList.forEach( function ( node ) {
if ( node.row.visible ) {
renderableRows.push( node.row );
}
if ( node.state === uiGridTreeBaseConstants.EXPANDED && node.children && node.children.length > 0 ) {
renderableRows = renderableRows.concat( service.renderTree( node.children ) );
}
});
return renderableRows;
},
/**
* @ngdoc function
* @name createTree
* @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
* @description Creates a tree from the renderableRows
*
* @param {Grid} grid The grid
* @param {array} renderableRows The rows we want to create a tree from
* @returns {object} The tree we've build
*/
createTree: function( grid, renderableRows ) {
var currentLevel = -1,
parents = [],
currentState;
grid.treeBase.tree = [];
grid.treeBase.numberLevels = 0;
var aggregations = service.getAggregations( grid );
function createNode( row ) {
if ( !row.internalRow && row.treeLevel !== row.entity.$$treeLevel ) {
row.treeLevel = row.entity.$$treeLevel;
}
if ( row.treeLevel <= currentLevel ) {
// pop any levels that aren't parents of this level, formatting the aggregation at the same time
while ( row.treeLevel <= currentLevel ) {
var lastParent = parents.pop();
service.finaliseAggregations( lastParent );
currentLevel--;
}
// reset our current state based on the new parent, set to expanded if this is a level 0 node
if ( parents.length > 0 ) {
currentState = service.setCurrentState(parents);
}
else {
currentState = uiGridTreeBaseConstants.EXPANDED;
}
}
// aggregate if this is a leaf node
if ( ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ) && row.visible ) {
service.aggregate( grid, row, parents );
}
// add this node to the tree
service.addOrUseNode(grid, row, parents, aggregations);
if ( typeof(row.treeLevel) !== 'undefined' && row.treeLevel !== null && row.treeLevel >= 0 ) {
parents.push(row);
currentLevel++;
currentState = service.setCurrentState(parents);
}
// update the tree number of levels, so we can set header width if we need to
if ( grid.treeBase.numberLevels < row.treeLevel + 1) {
grid.treeBase.numberLevels = row.treeLevel + 1;
}
}
renderableRows.forEach( createNode );
// finalize remaining aggregations
while ( parents.length > 0 ) {
var lastParent = parents.pop();
service.finaliseAggregations( lastParent );
}
return grid.treeBase.tree;
},
/**
* @ngdoc function
* @name addOrUseNode
* @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
* @description Creates a tree node for this row. If this row already has a treeNode
* recorded against it, preserves the state, but otherwise overwrites the data.
*
* @param {grid} grid The grid we're operating on
* @param {gridRow} row The row we want to set
* @param {array} parents An array of the parents this row should have
* @param {array} aggregationBase Empty aggregation information
* @returns {undefined} Updates the parents array, updates the row to have a treeNode, and updates the
* grid.treeBase.tree
*/
addOrUseNode: function( grid, row, parents, aggregationBase ) {
var newAggregations = [];
aggregationBase.forEach( function(aggregation) {
newAggregations.push(service.buildAggregationObject(aggregation.col));
});
var newNode = { state: uiGridTreeBaseConstants.COLLAPSED, row: row, parentRow: null, aggregations: newAggregations, children: [] };
if ( row.treeNode ) {
newNode.state = row.treeNode.state;
}
if ( parents.length > 0 ) {
newNode.parentRow = parents[parents.length - 1];
}
row.treeNode = newNode;
if ( parents.length === 0 ) {
grid.treeBase.tree.push( newNode );
} else {
parents[parents.length - 1].treeNode.children.push( newNode );
}
},
/**
* @ngdoc function
* @name setCurrentState
* @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
* @description Looks at the parents array to determine our current state.
* If any node in the hierarchy is collapsed, then return collapsed, otherwise return
* expanded.
*
* @param {array} parents An array of the parents this row should have
* @returns {string} The state we should be setting to any nodes we see
*/
setCurrentState: function( parents ) {
var currentState = uiGridTreeBaseConstants.EXPANDED;
parents.forEach( function(parent) {
if ( parent.treeNode.state === uiGridTreeBaseConstants.COLLAPSED ) {
currentState = uiGridTreeBaseConstants.COLLAPSED;
}
});
return currentState;
},
/**
* @ngdoc function
* @name sortTree
* @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
* @description Performs a recursive sort on the tree nodes, sorting the
* children of each node and putting them back into the children array.
*
* Before doing this it turns back on all the sortIgnore - things that were previously
* ignored we process now. Since we're sorting within the nodes, presumably anything
* that was already sorted is how we derived the nodes, we can keep those sorts too.
*
* We only sort tree nodes that are expanded - no point in wasting effort sorting collapsed
* nodes
*
* @param {Grid} grid The grid to get the aggregation information from
* @returns {array} The aggregation information
*/
sortTree: function( grid ) {
grid.columns.forEach( function( column ) {
if ( column.sort && column.sort.ignoreSort ) {
delete column.sort.ignoreSort;
}
});
grid.treeBase.tree = service.sortInternal( grid, grid.treeBase.tree );
},
sortInternal: function( grid, treeList ) {
var rows = treeList.map( func