ag-grid
Version:
Advanced Data Grid / Data Table supporting Javascript / React / AngularJS / Web Components
2,588 lines • 102 kB
text/typescript
import {Utils as _} from "../utils";
import {ColumnGroup} from "../entities/columnGroup";
import {Column} from "../entities/column";
import {AbstractColDef, ColDef, ColGroupDef, IAggFunc} from "../entities/colDef";
import {ColumnGroupChild} from "../entities/columnGroupChild";
import {GridOptionsWrapper} from "../gridOptionsWrapper";
import {ExpressionService} from "../valueService/expressionService";
import {BalancedColumnTreeBuilder} from "./balancedColumnTreeBuilder";
import {DisplayedGroupCreator} from "./displayedGroupCreator";
import {AutoWidthCalculator} from "../rendering/autoWidthCalculator";
import {OriginalColumnGroupChild} from "../entities/originalColumnGroupChild";
import {EventService} from "../eventService";
import {ColumnUtils} from "./columnUtils";
import {Logger, LoggerFactory} from "../logger";
import {
ColumnEvent, ColumnEventType, ColumnEverythingChangedEvent,
ColumnGroupOpenedEvent,
ColumnMovedEvent,
ColumnPinnedEvent,
ColumnPivotModeChangedEvent,
ColumnResizedEvent,
ColumnRowGroupChangedEvent,
ColumnValueChangedEvent,
ColumnVisibleEvent,
DisplayedColumnsChangedEvent,
DisplayedColumnsWidthChangedEvent,
Events,
GridColumnsChangedEvent,
NewColumnsLoadedEvent,
VirtualColumnsChangedEvent
} from "../events";
import {OriginalColumnGroup} from "../entities/originalColumnGroup";
import {GroupInstanceIdCreator} from "./groupInstanceIdCreator";
import {Autowired, Bean, Context, Optional, PostConstruct, Qualifier} from "../context/context";
import {GridPanel} from "../gridPanel/gridPanel";
import {IAggFuncService} from "../interfaces/iAggFuncService";
import {ColumnAnimationService} from "../rendering/columnAnimationService";
import {AutoGroupColService} from "./autoGroupColService";
import {RowNode} from "../entities/rowNode";
import {ValueCache} from "../valueService/valueCache";
import {GridApi} from "../gridApi";
import {ColumnApi} from "./columnApi";
export interface ColumnResizeSet {
columns: Column[];
ratios: number[];
width: number;
}
export interface ColumnState {
colId: string,
hide?: boolean,
aggFunc?: string | IAggFunc,
width?: number,
pivotIndex?: number,
pinned?: boolean | string | "left" | "right",
rowGroupIndex?: number
}
@Bean('columnController')
export class ColumnController {
@Autowired('gridOptionsWrapper') private gridOptionsWrapper: GridOptionsWrapper;
@Autowired('expressionService') private expressionService: ExpressionService;
@Autowired('balancedColumnTreeBuilder') private balancedColumnTreeBuilder: BalancedColumnTreeBuilder;
@Autowired('displayedGroupCreator') private displayedGroupCreator: DisplayedGroupCreator;
@Autowired('autoWidthCalculator') private autoWidthCalculator: AutoWidthCalculator;
@Autowired('eventService') private eventService: EventService;
@Autowired('columnUtils') private columnUtils: ColumnUtils;
@Autowired('context') private context: Context;
@Autowired('columnAnimationService') private columnAnimationService: ColumnAnimationService;
@Autowired('autoGroupColService') private autoGroupColService: AutoGroupColService;
@Optional('aggFuncService') private aggFuncService: IAggFuncService;
@Optional('valueCache') private valueCache: ValueCache;
@Autowired('columnApi') private columnApi: ColumnApi;
@Autowired('gridApi') private gridApi: GridApi;
// these are the columns provided by the client. this doesn't change, even if the
// order or state of the columns and groups change. it will only change if the client
// provides a new set of column definitions. otherwise this tree is used to build up
// the groups for displaying.
private primaryBalancedTree: OriginalColumnGroupChild[];
// header row count, based on user provided columns
private primaryHeaderRowCount = 0;
// all columns provided by the user. basically it's the leaf level nodes of the
// tree above (originalBalancedTree)
private primaryColumns: Column[]; // every column available
// if pivoting, these are the generated columns as a result of the pivot
private secondaryBalancedTree: OriginalColumnGroupChild[];
private secondaryColumns: Column[];
private secondaryHeaderRowCount = 0;
private secondaryColumnsPresent = false;
// the columns the quick filter should use. this will be all primary columns
// plus the autoGroupColumns if any exist
private columnsForQuickFilter: Column[];
// these are all columns that are available to the grid for rendering after pivot
private gridBalancedTree: OriginalColumnGroupChild[];
private gridColumns: Column[];
// header row count, either above, or based on pivoting if we are pivoting
private gridHeaderRowCount = 0;
private lastPrimaryOrder: Column[];
private gridColsArePrimary: boolean;
// these are the columns actually shown on the screen. used by the header renderer,
// as header needs to know about column groups and the tree structure.
private displayedLeftColumnTree: ColumnGroupChild[];
private displayedRightColumnTree: ColumnGroupChild[];
private displayedCentreColumnTree: ColumnGroupChild[];
private displayedLeftHeaderRows: {[row: number]: ColumnGroupChild[]};
private displayedRightHeaderRows: {[row: number]: ColumnGroupChild[]};
private displayedCentreHeaderRows: {[row: number]: ColumnGroupChild[]};
// these are the lists used by the rowRenderer to render nodes. almost the leaf nodes of the above
// displayed trees, however it also takes into account if the groups are open or not.
private displayedLeftColumns: Column[] = [];
private displayedRightColumns: Column[] = [];
private displayedCenterColumns: Column[] = [];
// all three lists above combined
private allDisplayedColumns: Column[] = [];
// same as above, except trimmed down to only columns within the viewport
private allDisplayedVirtualColumns: Column[] = [];
private allDisplayedCenterVirtualColumns: Column[] = [];
// true if we are doing column spanning
private colSpanActive: boolean;
// primate columns that have colDef.autoHeight set
private autoRowHeightColumns: Column[];
private suppressColumnVirtualisation: boolean;
private rowGroupColumns: Column[] = [];
private valueColumns: Column[] = [];
private pivotColumns: Column[] = [];
private groupAutoColumns: Column[];
private groupDisplayColumns: Column[];
private ready = false;
private logger: Logger;
private autoGroupsNeedBuilding = false;
private pivotMode = false;
private usingTreeData: boolean;
// for horizontal visualisation of columns
private scrollWidth: number;
private scrollPosition: number;
private bodyWidth = 0;
private leftWidth = 0;
private rightWidth = 0;
private bodyWidthDirty = true;
private viewportLeft: number;
private viewportRight: number;
@PostConstruct
public init(): void {
let pivotMode = this.gridOptionsWrapper.isPivotMode();
this.suppressColumnVirtualisation = this.gridOptionsWrapper.isSuppressColumnVirtualisation();
if (this.isPivotSettingAllowed(pivotMode)) {
this.pivotMode = pivotMode;
}
this.usingTreeData = this.gridOptionsWrapper.isTreeData();
}
public isAutoRowHeightActive(): boolean {
return this.autoRowHeightColumns && this.autoRowHeightColumns.length > 0;
}
public getAllAutoRowHeightCols(): Column[] {
return this.autoRowHeightColumns;
}
private setVirtualViewportLeftAndRight(): void {
if (this.gridOptionsWrapper.isEnableRtl()) {
this.viewportLeft = this.bodyWidth - this.scrollPosition - this.scrollWidth;
this.viewportRight = this.bodyWidth - this.scrollPosition;
} else {
this.viewportLeft = this.scrollPosition;
this.viewportRight = this.scrollWidth + this.scrollPosition;
}
}
// used by clipboard service, to know what columns to paste into
public getDisplayedColumnsStartingAt(column: Column): Column[] {
let currentColumn = column;
let result: Column[] = [];
while (_.exists(currentColumn)) {
result.push(currentColumn);
currentColumn = this.getDisplayedColAfter(currentColumn);
}
return result;
}
// checks what columns are currently displayed due to column virtualisation. fires an event
// if the list of columns has changed.
// + setColumnWidth(), setVirtualViewportPosition(), setColumnDefs(), sizeColumnsToFit()
private checkDisplayedVirtualColumns(): void {
// check displayCenterColumnTree exists first, as it won't exist when grid is initialising
if (_.exists(this.displayedCenterColumns)) {
let hashBefore = this.allDisplayedVirtualColumns.map( column => column.getId() ).join('#');
this.updateVirtualSets();
let hashAfter = this.allDisplayedVirtualColumns.map( column => column.getId() ).join('#');
if (hashBefore !== hashAfter) {
let event: VirtualColumnsChangedEvent = {
type: Events.EVENT_VIRTUAL_COLUMNS_CHANGED,
api: this.gridApi,
columnApi: this.columnApi
};
this.eventService.dispatchEvent(event);
}
}
}
public setVirtualViewportPosition(scrollWidth: number, scrollPosition: number): void {
if (scrollWidth!==this.scrollWidth || scrollPosition!==this.scrollPosition || this.bodyWidthDirty) {
this.scrollWidth = scrollWidth;
this.scrollPosition = scrollPosition;
// we need to call setVirtualViewportLeftAndRight() at least once after the body width changes,
// as the viewport can stay the same, but in RTL, if body width changes, we need to work out the
// virtual columns again
this.bodyWidthDirty = true;
this.setVirtualViewportLeftAndRight();
if (this.ready) {
this.checkDisplayedVirtualColumns();
}
}
}
public isPivotMode(): boolean {
return this.pivotMode;
}
private isPivotSettingAllowed(pivot: boolean): boolean {
if (pivot) {
if (this.gridOptionsWrapper.isTreeData()) {
console.warn("ag-Grid: Pivot mode not available in conjunction Tree Data i.e. 'gridOptions.treeData: true'");
return false;
} else {
return true;
}
} else {
return true;
}
}
public setPivotMode(pivotMode: boolean, source: ColumnEventType = "api"): void {
if (pivotMode === this.pivotMode) { return; }
if (!this.isPivotSettingAllowed(this.pivotMode)) { return; }
this.pivotMode = pivotMode;
this.updateDisplayedColumns(source);
let event: ColumnPivotModeChangedEvent = {
type: Events.EVENT_COLUMN_PIVOT_MODE_CHANGED,
api: this.gridApi,
columnApi: this.columnApi
};
this.eventService.dispatchEvent(event);
}
public getSecondaryPivotColumn(pivotKeys: string[], valueColKey: Column|string): Column {
if (!this.secondaryColumnsPresent) {
return null;
}
let valueColumnToFind = this.getPrimaryColumn(valueColKey);
let foundColumn: Column = null;
this.secondaryColumns.forEach( column => {
let thisPivotKeys = column.getColDef().pivotKeys;
let pivotValueColumn = column.getColDef().pivotValueColumn;
let pivotKeyMatches = _.compareArrays(thisPivotKeys, pivotKeys);
let pivotValueMatches = pivotValueColumn === valueColumnToFind;
if (pivotKeyMatches && pivotValueMatches) {
foundColumn = column;
}
});
return foundColumn;
}
private setBeans(@Qualifier('loggerFactory') loggerFactory: LoggerFactory) {
this.logger = loggerFactory.create('ColumnController');
}
private setFirstRightAndLastLeftPinned(source: ColumnEventType): void {
let lastLeft: Column;
let firstRight: Column;
if (this.gridOptionsWrapper.isEnableRtl()) {
lastLeft = this.displayedLeftColumns ? this.displayedLeftColumns[0] : null;
firstRight = this.displayedRightColumns ? this.displayedRightColumns[this.displayedRightColumns.length - 1] : null;
} else {
lastLeft = this.displayedLeftColumns ? this.displayedLeftColumns[this.displayedLeftColumns.length - 1] : null;
firstRight = this.displayedRightColumns ? this.displayedRightColumns[0] : null;
}
this.gridColumns.forEach( (column: Column) => {
column.setLastLeftPinned(column === lastLeft, source);
column.setFirstRightPinned(column === firstRight, source);
} );
}
public autoSizeColumns(keys: (string|Column)[], source: ColumnEventType = "api"): void {
// because of column virtualisation, we can only do this function on columns that are
// actually rendered, as non-rendered columns (outside the viewport and not rendered
// due to column virtualisation) are not present. this can result in all rendered columns
// getting narrowed, which in turn introduces more rendered columns on the RHS which
// did not get autosized in the original run, leaving the visible grid with columns on
// the LHS sized, but RHS no. so we keep looping through teh visible columns until
// no more cols are available (rendered) to be resized
// keep track of which cols we have resized in here
let columnsAutosized: Column[] = [];
// initialise with anything except 0 so that while loop executs at least once
let changesThisTimeAround = -1;
while (changesThisTimeAround!==0) {
changesThisTimeAround = 0;
this.actionOnGridColumns(keys, (column: Column): boolean => {
// if already autosized, skip it
if (columnsAutosized.indexOf(column) >= 0) { return; }
// get how wide this col should be
let preferredWidth = this.autoWidthCalculator.getPreferredWidthForColumn(column);
// preferredWidth = -1 if this col is not on the screen
if (preferredWidth>0) {
let newWidth = this.normaliseColumnWidth(column, preferredWidth);
column.setActualWidth(newWidth, source);
columnsAutosized.push(column);
changesThisTimeAround++;
}
return true;
}, source);
}
if (columnsAutosized.length > 0) {
let event: ColumnResizedEvent = {
type: Events.EVENT_COLUMN_RESIZED,
columns: columnsAutosized,
column: columnsAutosized.length === 1 ? columnsAutosized[0] : null,
finished: true,
api: this.gridApi,
columnApi: this.columnApi,
source: "autosizeColumns"
};
this.eventService.dispatchEvent(event);
}
}
public autoSizeColumn(key: string|Column, source: ColumnEventType = "api"): void {
this.autoSizeColumns([key], source);
}
public autoSizeAllColumns(source: ColumnEventType = "api"): void {
let allDisplayedColumns = this.getAllDisplayedColumns();
this.autoSizeColumns(allDisplayedColumns, source);
}
private getColumnsFromTree(rootColumns: OriginalColumnGroupChild[]): Column[] {
let result: Column[] = [];
recursiveFindColumns(rootColumns);
return result;
function recursiveFindColumns(childColumns: OriginalColumnGroupChild[]): void {
for (let i = 0; i<childColumns.length; i++) {
let child = childColumns[i];
if (child instanceof Column) {
result.push(<Column>child);
} else if (child instanceof OriginalColumnGroup) {
recursiveFindColumns((<OriginalColumnGroup>child).getChildren());
}
}
}
}
public getAllDisplayedColumnGroups(): ColumnGroupChild[] {
if (this.displayedLeftColumnTree && this.displayedRightColumnTree && this.displayedCentreColumnTree) {
return this.displayedLeftColumnTree
.concat(this.displayedCentreColumnTree)
.concat(this.displayedRightColumnTree);
} else {
return null;
}
}
// + columnSelectPanel
public getPrimaryColumnTree(): OriginalColumnGroupChild[] {
return this.primaryBalancedTree;
}
// + gridPanel -> for resizing the body and setting top margin
public getHeaderRowCount(): number {
return this.gridHeaderRowCount;
}
// + headerRenderer -> setting pinned body width
public getLeftDisplayedColumnGroups(): ColumnGroupChild[] {
return this.displayedLeftColumnTree;
}
// + headerRenderer -> setting pinned body width
public getRightDisplayedColumnGroups(): ColumnGroupChild[] {
return this.displayedRightColumnTree;
}
// + headerRenderer -> setting pinned body width
public getCenterDisplayedColumnGroups(): ColumnGroupChild[] {
return this.displayedCentreColumnTree;
}
public getDisplayedColumnGroups(type: string): ColumnGroupChild[] {
switch (type) {
case Column.PINNED_LEFT: return this.getLeftDisplayedColumnGroups();
case Column.PINNED_RIGHT: return this.getRightDisplayedColumnGroups();
default: return this.getCenterDisplayedColumnGroups();
}
}
// gridPanel -> ensureColumnVisible
public isColumnDisplayed(column: Column): boolean {
return this.getAllDisplayedColumns().indexOf(column) >= 0;
}
// + csvCreator
public getAllDisplayedColumns(): Column[] {
return this.allDisplayedColumns;
}
public getAllDisplayedVirtualColumns(): Column[] {
return this.allDisplayedVirtualColumns;
}
public getDisplayedLeftColumnsForRow(rowNode: RowNode): Column[] {
if (!this.colSpanActive) {
return this.displayedLeftColumns;
} else {
return this.getDisplayedColumnsForRow(rowNode, this.displayedLeftColumns);
}
}
public getDisplayedRightColumnsForRow(rowNode: RowNode): Column[] {
if (!this.colSpanActive) {
return this.displayedRightColumns;
} else {
return this.getDisplayedColumnsForRow(rowNode, this.displayedRightColumns);
}
}
private getDisplayedColumnsForRow(rowNode: RowNode, displayedColumns: Column[],
filterCallback?: (column: Column)=>boolean,
emptySpaceBeforeColumn?: (column: Column)=>boolean): Column[] {
let result: Column[] = [];
let lastConsideredCol: Column = null;
for (let i = 0; i<displayedColumns.length; i++) {
let col = displayedColumns[i];
let colSpan = col.getColSpan(rowNode);
let columnsToCheckFilter: Column[] = [col];
if (colSpan > 1) {
let colsToRemove = colSpan - 1;
for (let j = 1; j<=colsToRemove; j++) {
columnsToCheckFilter.push(displayedColumns[i+j]);
}
i += colsToRemove;
}
// see which cols we should take out for column virtualisation
let filterPasses: boolean;
if (filterCallback) {
// if user provided a callback, means some columns may not be in the viewport.
// the user will NOT provide a callback if we are talking about pinned areas,
// as pinned areas have no horizontal scroll and do not virtualise the columns.
// if lots of columns, that means column spanning, and we set filterPasses = true
// if one or more of the columns spanned pass the filter.
filterPasses = false;
columnsToCheckFilter.forEach( colForFilter => {
if (filterCallback(colForFilter)) filterPasses = true;
});
} else {
filterPasses = true
}
if (filterPasses) {
if (result.length===0 && lastConsideredCol) {
let gapBeforeColumn = emptySpaceBeforeColumn ? emptySpaceBeforeColumn(col) : false;
if (gapBeforeColumn) {
result.push(lastConsideredCol);
}
}
result.push(col);
}
lastConsideredCol = col;
}
return result;
}
// + rowRenderer
// if we are not column spanning, this just returns back the virtual centre columns,
// however if we are column spanning, then different rows can have different virtual
// columns, so we have to work out the list for each individual row.
public getAllDisplayedCenterVirtualColumnsForRow(rowNode: RowNode): Column[] {
if (!this.colSpanActive) {
return this.allDisplayedCenterVirtualColumns;
}
let emptySpaceBeforeColumn = (col: Column) => col.getLeft() > this.viewportLeft;
// if doing column virtualisation, then we filter based on the viewport.
let filterCallback = this.suppressColumnVirtualisation ? null : this.isColumnInViewport.bind(this);
return this.getDisplayedColumnsForRow(rowNode, this.displayedCenterColumns,
filterCallback, emptySpaceBeforeColumn);
}
private isColumnInViewport(col: Column): boolean {
let columnLeft = col.getLeft();
let columnRight = col.getLeft() + col.getActualWidth();
let columnToMuchLeft = columnLeft < this.viewportLeft && columnRight < this.viewportLeft;
let columnToMuchRight = columnLeft > this.viewportRight && columnRight > this.viewportRight;
return !columnToMuchLeft && !columnToMuchRight;
}
// used by:
// + angularGrid -> setting pinned body width
// note: this should be cached
public getPinnedLeftContainerWidth() {
return this.getWidthOfColsInList(this.displayedLeftColumns);
}
// note: this should be cached
public getPinnedRightContainerWidth() {
return this.getWidthOfColsInList(this.displayedRightColumns);
}
public updatePrimaryColumnList( keys: (string|Column)[],
masterList: Column[],
actionIsAdd: boolean,
columnCallback: (column: Column)=>void,
eventType: string,
source: ColumnEventType = "api") {
if (_.missingOrEmpty(keys)) { return; }
let atLeastOne = false;
keys.forEach( key => {
let columnToAdd = this.getPrimaryColumn(key);
if (!columnToAdd) {return;}
if (actionIsAdd) {
if (masterList.indexOf(columnToAdd)>=0) {return;}
masterList.push(columnToAdd);
} else {
if (masterList.indexOf(columnToAdd)<0) {return;}
_.removeFromArray(masterList, columnToAdd);
}
columnCallback(columnToAdd);
atLeastOne = true;
});
if (!atLeastOne) { return; }
if (this.autoGroupsNeedBuilding) {
this.updateGridColumns();
}
this.updateDisplayedColumns(source);
let event: ColumnEvent = {
type: eventType,
columns: masterList,
column: masterList.length === 1 ? masterList[0] : null,
api: this.gridApi,
columnApi: this.columnApi,
source: source
};
this.eventService.dispatchEvent(event);
}
public setRowGroupColumns(colKeys: (string|Column)[], source: ColumnEventType = "api"): void {
this.autoGroupsNeedBuilding = true;
this.setPrimaryColumnList(colKeys, this.rowGroupColumns,
Events.EVENT_COLUMN_ROW_GROUP_CHANGED,
this.setRowGroupActive.bind(this),
source);
}
private setRowGroupActive(active: boolean, column: Column, source: ColumnEventType): void {
if (active === column.isRowGroupActive()) {return;}
column.setRowGroupActive(active, source);
if (!active && !this.gridOptionsWrapper.isSuppressMakeColumnVisibleAfterUnGroup()) {
column.setVisible(true, source);
}
}
public addRowGroupColumn(key: string|Column, source: ColumnEventType = "api"): void {
this.addRowGroupColumns([key], source);
}
public addRowGroupColumns(keys: (string|Column)[], source: ColumnEventType = "api"): void {
this.autoGroupsNeedBuilding = true;
this.updatePrimaryColumnList(keys, this.rowGroupColumns, true,
this.setRowGroupActive.bind(this, true),
Events.EVENT_COLUMN_ROW_GROUP_CHANGED,
source);
}
public removeRowGroupColumns(keys: (string|Column)[], source: ColumnEventType = "api"): void {
this.autoGroupsNeedBuilding = true;
this.updatePrimaryColumnList(keys, this.rowGroupColumns, false,
this.setRowGroupActive.bind(this, false),
Events.EVENT_COLUMN_ROW_GROUP_CHANGED,
source);
}
public removeRowGroupColumn(key: string|Column, source: ColumnEventType = "api"): void {
this.removeRowGroupColumns([key], source);
}
public addPivotColumns(keys: (string|Column)[], source: ColumnEventType = "api"): void {
this.updatePrimaryColumnList(keys, this.pivotColumns, true,
column => column.setPivotActive(true, source),
Events.EVENT_COLUMN_PIVOT_CHANGED, source);
}
public setPivotColumns(colKeys: (string|Column)[], source: ColumnEventType = "api"): void {
this.setPrimaryColumnList(colKeys, this.pivotColumns, Events.EVENT_COLUMN_PIVOT_CHANGED,
(added: boolean, column: Column) => {
column.setPivotActive(added, source);
}, source
);
}
public addPivotColumn(key: string|Column, source: ColumnEventType = "api"): void {
this.addPivotColumns([key], source);
}
public removePivotColumns(keys: (string|Column)[], source: ColumnEventType = "api"): void {
this.updatePrimaryColumnList(keys, this.pivotColumns, false,
column => column.setPivotActive(false, source),
Events.EVENT_COLUMN_PIVOT_CHANGED,
source);
}
public removePivotColumn(key: string|Column, source: ColumnEventType = "api"): void {
this.removePivotColumns([key], source);
}
private setPrimaryColumnList(colKeys: (string|Column)[],
masterList: Column[],
eventName: string,
columnCallback: (added: boolean, column: Column)=>void,
source: ColumnEventType
): void {
masterList.length = 0;
if (_.exists(colKeys)) {
colKeys.forEach( key => {
let column = this.getPrimaryColumn(key);
masterList.push(column);
});
}
this.primaryColumns.forEach( column => {
let added = masterList.indexOf(column) >= 0;
columnCallback(added, column);
});
if (this.autoGroupsNeedBuilding) {
this.updateGridColumns();
}
this.updateDisplayedColumns(source);
let event: ColumnEvent = {
type: eventName,
columns: masterList,
column: masterList.length === 1 ? masterList[0] : null,
api: this.gridApi,
columnApi: this.columnApi,
source: source
};
this.eventService.dispatchEvent(event);
}
public setValueColumns(colKeys: (string|Column)[], source: ColumnEventType = "api"): void {
this.setPrimaryColumnList(colKeys, this.valueColumns,
Events.EVENT_COLUMN_VALUE_CHANGED,
this.setValueActive.bind(this),
source);
}
private setValueActive(active: boolean, column: Column, source: ColumnEventType): void {
if (active === column.isValueActive()) {return;}
column.setValueActive(active, source);
if (active && !column.getAggFunc()) {
let defaultAggFunc = this.aggFuncService.getDefaultAggFunc(column);
column.setAggFunc(defaultAggFunc);
}
}
public addValueColumns(keys: (string|Column)[], source: ColumnEventType = "api"): void {
this.updatePrimaryColumnList(keys, this.valueColumns, true,
this.setValueActive.bind(this, true),
Events.EVENT_COLUMN_VALUE_CHANGED,
source);
}
public addValueColumn(colKey: (string|Column), source: ColumnEventType = "api"): void {
this.addValueColumns([colKey], source);
}
public removeValueColumn(colKey: (string|Column), source: ColumnEventType = "api"): void {
this.removeValueColumns([colKey], source);
}
public removeValueColumns(keys: (string|Column)[], source: ColumnEventType = "api"): void {
this.updatePrimaryColumnList(keys, this.valueColumns, false,
this.setValueActive.bind(this, false),
Events.EVENT_COLUMN_VALUE_CHANGED,
source);
}
// returns the width we can set to this col, taking into consideration min and max widths
private normaliseColumnWidth(column: Column, newWidth: number): number {
if (newWidth < column.getMinWidth()) {
newWidth = column.getMinWidth();
}
if (column.isGreaterThanMax(newWidth)) {
newWidth = column.getMaxWidth();
}
return newWidth;
}
private getPrimaryOrGridColumn(key: string|Column): Column {
let column = this.getPrimaryColumn(key);
if (column) {
return column;
} else {
return this.getGridColumn(key);
}
}
public setColumnWidth(
key: string|Column, // @key - the column who's size we want to change
newWidth: number, // @newWidth - width in pixels
shiftKey: boolean, // @takeFromAdjacent - if user has 'shift' pressed, then pixels are taken from adjacent column
finished: boolean, // @finished - ends up in the event, tells the user if more events are to come
source: ColumnEventType = "api"): void {
let col = this.getPrimaryOrGridColumn(key);
if (!col) {
return;
}
let sets: ColumnResizeSet[] = [];
sets.push({
width: newWidth,
ratios: [1],
columns: [col]
});
// if user wants to do shift resize by default, then we invert the shift operation
let defaultIsShift = this.gridOptionsWrapper.getColResizeDefault() === 'shift';
if (defaultIsShift) {
shiftKey = !shiftKey;
}
if (shiftKey) {
let otherCol = this.getDisplayedColAfter(col);
if (!otherCol) { return; }
let widthDiff = col.getActualWidth() - newWidth;
let otherColWidth = otherCol.getActualWidth() + widthDiff;
sets.push({
width: otherColWidth,
ratios: [1],
columns: [otherCol]
})
}
this.resizeColumnSets(sets, finished, source);
}
private checkMinAndMaxWidthsForSet(columnResizeSet: ColumnResizeSet): boolean {
let {columns, width} = columnResizeSet;
// every col has a min width, so sum them all up and see if we have enough room
// for all the min widths
let minWidthAccumulated = 0;
let maxWidthAccumulated = 0;
let maxWidthActive = true;
columns.forEach( col => {
minWidthAccumulated += col.getMinWidth();
if (col.getMaxWidth() > 0) {
maxWidthAccumulated += col.getMaxWidth();
} else {
// if at least one columns has no max width, it means the group of columns
// then has no max width, as at least one column can take as much width as possible
maxWidthActive = false;
}
});
let minWidthPasses = width >= minWidthAccumulated;
let maxWidthPasses = !maxWidthActive || (width <= maxWidthAccumulated);
return minWidthPasses && maxWidthPasses;
}
// method takes sets of columns and resizes them. either all sets will be resized, or nothing
// be resized. this is used for example when user tries to resize a group and holds shift key,
// then both the current group (grows), and the adjacent group (shrinks), will get resized,
// so that's two sets for this method.
public resizeColumnSets(resizeSets: ColumnResizeSet[],
finished: boolean,
source: ColumnEventType): void {
let passMinMaxCheck = _.every(resizeSets, this.checkMinAndMaxWidthsForSet.bind(this));
if (!passMinMaxCheck) { return; }
let changedCols: Column[] = [];
let allCols: Column[] = [];
resizeSets.forEach( set => {
let {width, columns, ratios} = set;
// keep track of pixels used, and last column gets the remaining,
// to cater for rounding errors, and min width adjustments
let newWidths: {[colId: string]: number} = {};
let finishedCols: {[colId: string]: boolean} = {};
columns.forEach( col => allCols.push(col) );
// the loop below goes through each col. if a col exceeds it's min/max width,
// it then gets set to its min/max width and the column is removed marked as 'finished'
// and the calculation is done again leaving this column out. take for example columns
// {A, width: 50, maxWidth: 100}
// {B, width: 50}
// {C, width: 50}
// and then the set is set to width 600 - on the first pass the grid tries to set each column
// to 200. it checks A and sees 200 > 100 and so sets the width to 100. col A is then marked
// as 'finished' and the calculation is done again with the remaining cols B and C, which end up
// splitting the remaining 500 pixels.
let finishedColsGrew = true;
let loopCount = 0;
while (finishedColsGrew) {
loopCount++;
if (loopCount>1000) {
// this should never happen, but in the future, someone might introduce a bug here,
// so we stop the browser from hanging and report bug properly
console.error('ag-Grid: infinite loop in resizeColumnSets');
break;
}
finishedColsGrew = false;
let subsetCols: Column[] = [];
let subsetRatios: number[] = [];
let subsetRatioTotal = 0;
let pixelsToDistribute = width;
columns.forEach( (col: Column, index: number) => {
let thisColFinished = finishedCols[col.getId()];
if (thisColFinished) {
pixelsToDistribute -= newWidths[col.getId()];
} else {
subsetCols.push(col);
let ratioThisCol = ratios[index];
subsetRatioTotal += ratioThisCol;
subsetRatios.push(ratioThisCol);
}
});
// because we are not using all of the ratios (cols can be missing),
// we scale the ratio. if all columns are included, then subsetRatioTotal=1,
// and so the ratioScale will be 1.
let ratioScale = 1 / subsetRatioTotal;
subsetCols.forEach( (col: Column, index: number) => {
let lastCol = index === (subsetCols.length - 1);
let colNewWidth: number;
if (lastCol) {
colNewWidth = pixelsToDistribute;
} else {
colNewWidth = Math.round(ratios[index] * width * ratioScale);
pixelsToDistribute -= colNewWidth;
}
if (colNewWidth < col.getMinWidth()) {
colNewWidth = col.getMinWidth();
finishedCols[col.getId()] = true;
finishedColsGrew = true;
} else if (col.getMaxWidth() > 0 && colNewWidth > col.getMaxWidth()) {
colNewWidth = col.getMaxWidth();
finishedCols[col.getId()] = true;
finishedColsGrew = true;
}
newWidths[col.getId()] = colNewWidth;
});
}
columns.forEach( col => {
let newWidth = newWidths[col.getId()];
if (col.getActualWidth() !== newWidth) {
col.setActualWidth(newWidth);
changedCols.push(col);
}
});
});
// if no cols changed, then no need to update more or send event.
let atLeastOneColChanged = changedCols.length > 0;
if (atLeastOneColChanged) {
this.setLeftValues(source);
this.updateBodyWidths();
this.checkDisplayedVirtualColumns();
}
// check for change first, to avoid unnecessary firing of events
// however we always fire 'finished' events. this is important
// when groups are resized, as if the group is changing slowly,
// eg 1 pixel at a time, then each change will fire change events
// in all the columns in the group, but only one with get the pixel.
if (atLeastOneColChanged || finished) {
let event: ColumnResizedEvent = {
type: Events.EVENT_COLUMN_RESIZED,
columns: allCols,
column: allCols.length === 1 ? allCols[0] : null,
finished: finished,
api: this.gridApi,
columnApi: this.columnApi,
source: source
};
this.eventService.dispatchEvent(event);
}
}
public setColumnAggFunc(column: Column, aggFunc: string, source: ColumnEventType = "api"): void {
column.setAggFunc(aggFunc);
let event: ColumnValueChangedEvent = {
type: Events.EVENT_COLUMN_VALUE_CHANGED,
columns: [column],
column: column,
api: this.gridApi,
columnApi: this.columnApi,
source: source
};
this.eventService.dispatchEvent(event);
}
public moveRowGroupColumn(fromIndex: number, toIndex: number, source: ColumnEventType = "api"): void {
let column = this.rowGroupColumns[fromIndex];
this.rowGroupColumns.splice(fromIndex, 1);
this.rowGroupColumns.splice(toIndex, 0, column);
let event: ColumnRowGroupChangedEvent = {
type: Events.EVENT_COLUMN_ROW_GROUP_CHANGED,
columns: this.rowGroupColumns,
column: this.rowGroupColumns.length === 1 ? this.rowGroupColumns[0] : null,
api: this.gridApi,
columnApi: this.columnApi,
source: source
};
this.eventService.dispatchEvent(event);
}
public moveColumns(columnsToMoveKeys: (string|Column)[], toIndex: number, source: ColumnEventType = "api"): void {
this.columnAnimationService.start();
if (toIndex > this.gridColumns.length - columnsToMoveKeys.length) {
console.warn('ag-Grid: tried to insert columns in invalid location, toIndex = ' + toIndex);
console.warn('ag-Grid: remember that you should not count the moving columns when calculating the new index');
return;
}
// we want to pull all the columns out first and put them into an ordered list
let columnsToMove = this.getGridColumns(columnsToMoveKeys);
let failedRules = !this.doesMovePassRules(columnsToMove, toIndex);
if (failedRules) { return; }
_.moveInArray(this.gridColumns, columnsToMove, toIndex);
this.updateDisplayedColumns(source);
let event: ColumnMovedEvent = {
type: Events.EVENT_COLUMN_MOVED,
columns: columnsToMove,
column: columnsToMove.length === 1 ? columnsToMove[0] : null,
toIndex: toIndex,
api: this.gridApi,
columnApi: this.columnApi,
source: source
};
this.eventService.dispatchEvent(event);
this.columnAnimationService.finish();
}
public doesMovePassRules(columnsToMove: Column[], toIndex: number): boolean {
// make a copy of what the grid columns would look like after the move
let proposedColumnOrder = this.gridColumns.slice();
_.moveInArray(proposedColumnOrder, columnsToMove, toIndex);
// then check that the new proposed order of the columns passes all rules
if (!this.doesMovePassMarryChildren(proposedColumnOrder)) { return false; }
if (!this.doesMovePassLockedPositions(proposedColumnOrder)) { return false; }
return true;
}
public doesMovePassLockedPositions(proposedColumnOrder: Column[]): boolean {
let foundNonLocked = false;
let rulePassed = true;
// go though the cols, see if any non-locked appear before any locked
proposedColumnOrder.forEach( col => {
if (col.isLockPosition()) {
if (foundNonLocked) {
rulePassed = false;
}
} else {
foundNonLocked = true;
}
});
return rulePassed;
}
public doesMovePassMarryChildren(allColumnsCopy: Column[]): boolean {
let rulePassed = true;
this.columnUtils.depthFirstOriginalTreeSearch(this.gridBalancedTree, child => {
if (!(child instanceof OriginalColumnGroup)) { return; }
let columnGroup = <OriginalColumnGroup> child;
let marryChildren = columnGroup.getColGroupDef() && columnGroup.getColGroupDef().marryChildren;
if (!marryChildren) { return; }
let newIndexes: number[] = [];
columnGroup.getLeafColumns().forEach( col => {
let newColIndex = allColumnsCopy.indexOf(col);
newIndexes.push(newColIndex);
} );
let maxIndex = Math.max.apply(Math, newIndexes);
let minIndex = Math.min.apply(Math, newIndexes);
// spread is how far the first column in this group is away from the last column
let spread = maxIndex - minIndex;
let maxSpread = columnGroup.getLeafColumns().length - 1;
// if the columns
if (spread > maxSpread) {
rulePassed = false;
}
// console.log(`maxIndex = ${maxIndex}, minIndex = ${minIndex}, spread = ${spread}, maxSpread = ${maxSpread}, fail = ${spread > (count-1)}`)
// console.log(allColumnsCopy.map( col => col.getColDef().field).join(','));
});
return rulePassed;
}
public moveColumn(key: string|Column, toIndex: number, source: ColumnEventType = "api") {
this.moveColumns([key], toIndex, source);
}
public moveColumnByIndex(fromIndex: number, toIndex: number, source: ColumnEventType = "api"): void {
let column = this.gridColumns[fromIndex];
this.moveColumn(column, toIndex, source);
}
// used by:
// + angularGrid -> for setting body width
// + rowController -> setting main row widths (when inserting and resizing)
// need to cache this
public getBodyContainerWidth(): number {
return this.bodyWidth;
}
public getContainerWidth(pinned: string): number {
switch (pinned) {
case Column.PINNED_LEFT: return this.leftWidth;
case Column.PINNED_RIGHT: return this.rightWidth;
default: return this.bodyWidth;
}
}
// after setColumnWidth or updateGroupsAndDisplayedColumns
private updateBodyWidths(): void {
let newBodyWidth = this.getWidthOfColsInList(this.displayedCenterColumns);
let newLeftWidth = this.getWidthOfColsInList(this.displayedLeftColumns);
let newRightWidth = this.getWidthOfColsInList(this.displayedRightColumns);
// this is used by virtual col calculation, for RTL only, as a change to body width can impact displayed
// columns, due to RTL inverting the y coordinates
this.bodyWidthDirty = this.bodyWidth !== newBodyWidth;
let atLeastOneChanged = this.bodyWidth !== newBodyWidth || this.leftWidth !== newLeftWidth || this.rightWidth !== newRightWidth;
if (atLeastOneChanged) {
this.bodyWidth = newBodyWidth;
this.leftWidth = newLeftWidth;
this.rightWidth = newRightWidth;
// when this fires, it is picked up by the gridPanel, which ends up in
// gridPanel calling setWidthAndScrollPosition(), which in turn calls setVirtualViewportPosition()
let event: DisplayedColumnsWidthChangedEvent = {
type: Events.EVENT_DISPLAYED_COLUMNS_WIDTH_CHANGED,
api: this.gridApi,
columnApi: this.columnApi
};
this.eventService.dispatchEvent(event);
}
}
// + rowController
public getValueColumns(): Column[] {
return this.valueColumns ? this.valueColumns : [];
}
// + rowController
public getPivotColumns(): Column[] {
return this.pivotColumns ? this.pivotColumns : [];
}
// + clientSideRowModel
public isPivotActive(): boolean {
return this.pivotColumns && this.pivotColumns.length > 0 && this.pivotMode;
}
// + toolPanel
public getRowGroupColumns(): Column[] {
return this.rowGroupColumns ? this.rowGroupColumns : [];
}
// + rowController -> while inserting rows
public getDisplayedCenterColumns(): Column[] {
return this.displayedCenterColumns;
}
// + rowController -> while inserting rows
public getDisplayedLeftColumns(): Column[] {
return this.displayedLeftColumns;
}
public getDisplayedRightColumns(): Column[] {
return this.displayedRightColumns;
}
public getDisplayedColumns(type: string): Column[] {
switch (type) {
case Column.PINNED_LEFT: return this.getDisplayedLeftColumns();
case Column.PINNED_RIGHT: return this.getDisplayedRightColumns();
default: return this.getDisplayedCenterColumns();
}
}
// used by:
// + clientSideRowController -> sorting, building quick filter text
// + headerRenderer -> sorting (clearing icon)
public getAllPrimaryColumns(): Column[] {
return this.primaryColumns;
}
public getAllColumnsForQuickFilter(): Column[] {
return this.columnsForQuickFilter;
}
// + moveColumnController
public getAllGridColumns(): Column[] {
return this.gridColumns;
}
public isEmpty(): boolean {
return _.missingOrEmpty(this.gridColumns);
}
public isRowGroupEmpty(): boolean {
return _.missingOrEmpty(this.rowGroupColumns);
}
public setColumnVisible(key: string|Column, visible: boolean, source: ColumnEventType = "api"): void {
this.setColumnsVisible([key], visible, source);
}
public setColumnsVisible(keys: (string|Column)[], visible: boolean, source: ColumnEventType = "api"): void {
this.columnAnimationService.start();
this.actionOnGridColumns(keys, (column: Column): boolean => {
if (column.isVisible()!==visible) {
column.setVisible(visible, source);
return true;
} else {
return false;
}
}, source,()=> {
let event: ColumnVisibleEvent = {
type: Events.EVENT_COLUMN_VISIBLE,
visible: visible,
column: null,
columns: null,
api: this.gridApi,
columnApi: this.columnApi,
source: source
};
return event;
});
this.columnAnimationService.finish();
}
public setColumnPinned(key: string|Column, pinned: string|boolean, source: ColumnEventType = "api"): void {
this.setColumnsPinned([key], pinned, source);
}
public setColumnsPinned(keys: (string|Column)[], pinned: string|boolean, source: ColumnEventType = "api"): void {
this.columnAnimationService.start();
let actualPinned: string;
if (pinned === true || pinned === Column.PINNED_LEFT) {
actualPinned = Column.PINNED_LEFT;
} else if (pinned === Column.PINNED_RIGHT) {
actualPinned = Column.PINNED_RIGHT;
} else {
actualPinned = null;
}
this.actionOnGridColumns(keys, (col: Column): boolean => {
if (col.getPinned() !== actualPinned) {
col.setPinned(actualPinned);
return true;
} else {
return false;
}
}, source,()=> {
let event: ColumnPinnedEvent = {
type: Events.EVENT_COLUMN_PINNED,
pinned: actualPinned,
column: null,
columns: null,
api: this.gridApi,
columnApi: this.columnApi,
source: source
};
return event;
});
this.columnAnimationService.finish();
}
// does an action on a set of columns. provides common functionality for looking up the
// columns based on key, getting a list of effected columns, and then updated the event
// with either one column (if it was just one col) or a list of columns
// used by: autoResize, setVisible, setPinned
private actionOnGridColumns(// the column keys this action will be on
keys: (string|Column)[],
// the action to do - if this returns false, the column was skipped
// and won't be included in the event
action: (column: Column) => boolean,
// should return back a column event of the right type
source: ColumnEventType,
createEvent?: ()=> ColumnEvent,
): void {
if (_.missingOrEmpty(keys)) { return; }
let updatedColumns: Column[] = [];
keys.forEach( (key: string|Column)=> {
let column = this.getGridColumn(key);
if (!column) {return;}
// need to check for false with type (ie !== instead of !=)
// as not returning anything (undefined) would also be false
let resultOfAction = action(column);
if (resultOfAction!==false) {
updatedColumns.push(column);
}
});
if (updatedColumns.length===0) { return; }
this.updateDisplayedColumns(source);
if (_.exists(createEvent)) {
let event = createEvent();
event.columns = updatedColumns;
event.column = updatedColumns.length === 1 ? updatedColumns[0] : null;
this.eventService.dispatchEvent(event);
}
}
public getDisplayedColBefore(col: Column): Column {
let allDisplayedColumns = this.getAllDisplayedColumns();
let oldIndex = allDisplayedColumns.indexOf(col);
if (oldIndex > 0) {
return allDisplayedColumns[oldIndex - 1];
} else {
return null;
}
}
// used by:
// + rowRenderer -> for navigation
public getDisplayedColAfter(col: Column): Column {
let allDisplayedColumns = this.getAllDisplayedColumns();
let oldIndex = allDisplayedColumns.indexOf(col);
if (oldIndex < (allDisplayedColumns.length - 1)) {
return allDisplayedColumns[oldIndex + 1];
} else {
return null;
}
}
public getDisplayedGroupAfter(columnGroup: ColumnGroup): ColumnGroup {
// pick one col in this group at random
let col = columnGroup.getDisplayedLeafColumns()[0];
let requiredLevel = columnGroup.getOriginalColumnGroup().getLevel();
while (true) {
// keep moving to the next col, until we get to another group
col = this.getDisplayedColAfter(col);
// if no col after, means no group after
if (!col) { return null; }
// get group at same level as the one we are looking for
let groupPointer = col.getParent();
while (groupPointer.getOriginalColumnGroup().getLevel() !== requiredLevel) {
groupPointer = groupPointer.getParent();
}
if (groupPointer!==columnGroup) {
return groupPointer;
}
}
}
public isPinningLeft(): boolean {
return this.displayedLeftColumns.length > 0;
}
public isPinningRight(): boolean {
return this.displayedRightColumns.length > 0;
}
public getPrimaryAndSecondaryAndAutoColumns(): Column[] {
let result = this.primaryColumns ? this.primaryColumns.slice(0) : [];
if (_.exists(this.groupAutoColumns)) {
this.groupAutoColumns.forEach( col => result.push(col) );
}
if (this.secondaryColumnsPresent) {
this.secondaryColumns.forEach( column => result.push(column) );
}
return result;
}
private createStateItemFromColumn(column: Column): ColumnState {
let rowGroupIndex = column.isRowGroupActive() ? this.rowGroupColumns.indexOf(column) : null;
let pivotIndex = column.isPivotActive() ? this.pivotColumns.indexOf(column) : null;
let aggFunc = column.isValueActive() ? column.getAggFunc() : null;
return {
colId: column.getColId(),
hide: !column.isVisible(),
aggFunc: aggFunc,
width: column.getActualWidth(),
pivotIndex: pivotIndex,
pinned: column.getPinned(),
rowGroupIndex: rowGroupIndex
};
}
public getColumnState(): ColumnState[] {
if (_.missing(this.primaryColumns)) {
return [];
}
let columnStateList: ColumnState[]
= <ColumnState[]> this.primaryColumns.map(this.createStateItemFromColumn.bind(this));
if (!this.pivotMode) {
this.orderColumnStateList(columnStateList);
}
return columnStateList;
}
private orderColumnStateList(columnStateList: any[]): void {
let gridColumnIds = this.gridColumns.map( column => column.getColId() );
columnStateList.sort( (itemA: any, itemB: any) => {
let posA = gridColumnIds.indexOf(itemA.colId);
let posB = gridColumnIds.indexOf(itemB.colId);
return posA - posB;
});
}
public resetColumnState(source: ColumnEventType = "api"): void {
// we can't use 'allColumns' as the order might of messed up, so get the primary ordered list
let primaryColumns = this.getColumnsFromTree(this.primaryBalancedTree);
let state: any[] = [];
if (primaryColumns) {
primaryColumns.forEach( (column) => {
state.push({
colId: column.getColId(),
aggFunc: column.getColDef().aggFunc,
hide: column.getColDef().hide,
pinned: column.getColDef().pinned,
rowGroupIndex: column.getColDef().rowGroupIndex,
pivotIndex: column.getColDef().pivotIndex,
width: column.getColDef().width
});
});
}
this.setColumnState(state, source);
}
public setColumnState(columnState: ColumnState[], source: ColumnEventType = "api"): boolean {
if (_.missingOrEmpty(this.primaryColumns)) {
return false;
}
this.autoGroupsNeedBuilding = true;
// at the end below, this list will have all columns we got no state for
let columnsWithNoState = this.primaryColumns.slice();
this.rowGroupColumns = [];
this.valueColumns = [];
this.pivotColumns = [];
let success = true;
let rowGroupIndexes: { [key: string]: number } = {};
let pivotIndexes: { [key: string]: number } = {};
if (columnState) {
columnState.forEach((stateItem: ColumnState) => {
let column = this.getPrimaryColumn(stateItem.colId);
if (!column) {
console.warn('ag-grid: column ' + stateItem.colId + ' not found');
success = false;
} else {
this.syncColumnWithStateItem(column, stateItem, rowGroupIndexes, pivotIndexes, source);
_.removeFromArray(columnsWithNoState, column);
}
});
}
// anything left over, we got no data for, so add in the column as non-value, non-rowGroup and hidden
columnsWithNoState.forEach(this.syncColumnWithNoState.bind(this));
// sort the lists according to the indexes that were provided
this.rowGroupColumns.sort(this.sortColumnListUsingIndexes.bind(this, rowGroupIndexes));
this.pivotColumns.sort(this.sortColumnListUsingIndexes.bind(this, pivotIndexes));
this.updateGridColumns();
if (columnState) {
let orderOfColIds = columnState.map(stateItem => stateItem.colId);
this.gridColumns.sort((colA: Column, colB: Column) => {
let indexA = orderOfColIds.indexOf(colA.getId());
let indexB = orderOfColIds.indexOf(colB.getId());
return indexA - indexB;
});
}
this.updateDisplayedColumns(source);
let event: ColumnEverythingChangedEvent = {
type: Events.EVENT_COLUMN_EVERYTHING_CHANGED,
api: this.gridApi,
columnApi: this.columnApi,
source: source
};
this.eventService.dispatchEvent(event);
return success;
}
private sortColumnListUsingIndexes(indexes: {[key: string]: number}, colA: Column, colB: Column): number {
let indexA = indexes[colA.getId()];
let indexB = indexes[colB.getId()];
return indexA - indexB;
}
private syncColumnWithNoState(column: Column, source: ColumnEventType): void {
column.setVisible(false, source);
column.setAggFunc(null);
column.setPinned(null);
column.setRowGroupActive(false, source);
column.setPivotActive(false, source);
column.setValueActive(false, source);
}
private syncColumnWithStateItem(column: Column, stateItem: ColumnState,
rowGroupIndexes: {[key: string]: number},
pivotIndexes: {[key: string]: number},
source: ColumnEventType): void {
// following ensures we are left with boolean true or false, eg converts (null, undefined, 0) all to true
column.setVisible(!stateItem.hide, source);
// sets pinned to 'left' or 'right'
column.setPinned(stateItem.pinned);
// if width provided and valid, use it, otherwise stick with the old width
if (stateItem.width >= this.gridOptionsWrapper.getMinColWidth()) {
column.setActualWidth(stateItem.width, source);
}
if (typeof stateItem.aggFunc === 'string') {
column.setAggFunc(stateItem.aggFunc);
column.setValueActive(true, source);
this.valueColumns.push(column);
} else {
if (_.exists(stateItem.aggFunc)) {
console.warn('ag-Grid: stateItem.aggFunc must be a string. if using your own aggregation ' +
'functions, register the functions first before using them in get/set state. This is because it is' +
'intended for the column state to be stored and retrieved as simple JSON.');
}
column.setAggFunc(null);
column.setValueActive(false, source);
}
if (typeof stateItem.rowGroupIndex === 'number') {
this.rowGroupColumns.push(column);
column.setRowGroupActive(true, source);
rowGroupIndexes[column.getId()] = stateItem.rowGroupIndex;
} else {
column.setRowGroupActive(false, source);
}
if (typeof stateItem.pivotIndex === 'number') {
this.pivotColumns.push(column);
column.setPivotActive(true, source);
pivotIndexes[column.getId()] = stateItem.pivotIndex;
} else {
column.setPivotActive(false, source);
}
}
public getGridColumns(keys: (string|Column)[]): Column[] {
return this.getColumns(keys, this.getGridColumn.bind(this));
}
private getColumns(keys: (string|Column)[], columnLookupCallback: (key: string|Column)=>Column ): Column[] {
let foundColumns: Column[] = [];
if (keys) {
keys.forEach( (key: (string|Column)) => {
let column = columnLookupCallback(key);
if (column) {
foundColumns.push(column);
}
});
}
return foundColumns;
}
// used by growGroupPanel
public getColumnWithValidation(key: string|Column): Column {
let column = this.getPrimaryColumn(key);
if (!column) {
console.warn('ag-Grid: could not find column ' + column);
}
return column;
}
public getPrimaryColumn(key: string|Column): Column {
return this.getColumn(key, this.primaryColumns);
}
public getGridColumn(key: string|Column): Column {
return this.getColumn(key, this.gridColumns);
}
private getColumn(key: string|Column, columnList: Column[]): Column {
if (!key) {return null;}
for (let i = 0; i < columnList.length; i++) {
if (this.columnsMatch(columnList[i], key)) {
return columnList[i];
}
}
return this.getAutoColumn(key);
}
private getAutoColumn(key: string|Column): Column {
if (!_.exists(this.groupAutoColumns) || _.missing(this.groupAutoColumns)) { return null; }
return _.find(this.groupAutoColumns, groupCol => {
return this.columnsMatch(groupCol, key);
});
}
private columnsMatch(column: Column, key: string|Column): boolean {
let columnMatches = column === key;
let colDefMatches = column.getColDef() === key;
let idMatches = column.getColId() == key;
return columnMatches || colDefMatches || idMatches;
}
public getDisplayNameForColumn(column: Column, location: string, includeAggFunc = false): string {
let headerName = this.getHeaderName(column.getColDef(), column, null, null, location);
if (includeAggFunc) {
return this.wrapHeaderNameWithAggFunc(column, headerName);
} else {
return headerName;
}
}
public getDisplayNameForOriginalColumnGroup(columnGroup: ColumnGroup, originalColumnGroup: OriginalColumnGroup, location: string): string {
let colGroupDef = originalColumnGroup.getColGroupDef();
if (colGroupDef) {
return this.getHeaderName(colGroupDef, null, columnGroup, originalColumnGroup, location);
} else {
return null;
}
}
public getDisplayNameForColumnGroup(columnGroup: ColumnGroup, location: string): string {
return this.getDisplayNameForOriginalColumnGroup(columnGroup, columnGroup.getOriginalColumnGroup(), location);
}
// location is where the column is going to appear, ie who is calling us
private getHeaderName(colDef: AbstractColDef, column: Column, columnGroup: ColumnGroup,
originalColumnGroup: OriginalColumnGroup, location: string): string {
let headerValueGetter = colDef.headerValueGetter;
if (headerValueGetter) {
let params = {
colDef: colDef,
column: column,
columnGroup: columnGroup,
originalColumnGroup: originalColumnGroup,
location: location,
api: this.gridOptionsWrapper.getApi(),
context: this.gridOptionsWrapper.getContext()
};
if (typeof headerValueGetter === 'function') {
// valueGetter is a function, so just call it
return headerValueGetter(params);
} else if (typeof headerValueGetter === 'string') {
// valueGetter is an expression, so execute the expression
return this.expressionService.evaluate(headerValueGetter, params);
} else {
console.warn('ag-grid: headerValueGetter must be a function or a string');
return '';
}
} else if (colDef.headerName != null) {
return colDef.headerName;
} else if ((<ColDef>colDef).field) {
return _.camelCaseToHumanText((<ColDef>colDef).field);
} else {
return '';
}
}
/*
private getHeaderGroupName(columnGroup: ColumnGroup): string {
let colGroupDef = columnGroup.getOriginalColumnGroup().getColGroupDef();
let headerValueGetter = colGroupDef.headerValueGetter;
if (headerValueGetter) {
let params = {
columnGroup: columnGroup,
colDef: colGroupDef,
api: this.gridOptionsWrapper.getApi(),
context: this.gridOptionsWrapper.getContext()
};
if (typeof headerValueGetter === 'function') {
// valueGetter is a function, so just call it
return headerValueGetter(params);
} else if (typeof headerValueGetter === 'string') {
// valueGetter is an expression, so execute the expression
return this.expressionService.evaluate(headerValueGetter, params);
} else {
console.warn('ag-grid: headerValueGetter must be a function or a string');
return '';
}
} else {
return colGroupDef.headerName;
}
}
*/
private wrapHeaderNameWithAggFunc(column: Column, headerName: string): string {
if (this.gridOptionsWrapper.isSuppressAggFuncInHeader()) {
return headerName;
}
// only columns with aggregation active can have aggregations
let pivotValueColumn = column.getColDef().pivotValueColumn;
let pivotActiveOnThisColumn = _.exists(pivotValueColumn);
let aggFunc: string | IAggFunc = null;
let aggFuncFound: boolean;
// otherwise we have a measure that is active, and we are doing aggregation on it
if (pivotActiveOnThisColumn) {
aggFunc = pivotValueColumn.getAggFunc();
aggFuncFound = true;
} else {
let measureActive = column.isValueActive();
let aggregationPresent = this.pivotMode || !this.isRowGroupEmpty();
if (measureActive && aggregationPresent) {
aggFunc = column.getAggFunc();
aggFuncFound = true;
} else {
aggFuncFound = false;
}
}
if (aggFuncFound) {
let aggFuncString = (typeof aggFunc === 'string') ? <string> aggFunc : 'func';
let localeTextFunc = this.gridOptionsWrapper.getLocaleTextFunc();
let aggFuncStringTranslated = localeTextFunc (aggFuncString, aggFuncString);
return `${aggFuncStringTranslated}(${headerName})`;
} else {
return headerName;
}
}
// returns the group with matching colId and instanceId. If instanceId is missing,
// matches only on the colId.
public getColumnGroup(colId: string|ColumnGroup, instanceId?: number): ColumnGroup {
if (!colId) {return null;}
if (colId instanceof ColumnGroup) {
return colId;
}
let allColumnGroups = this.getAllDisplayedColumnGroups();
let checkInstanceId = typeof instanceId === 'number';
let result: ColumnGroup = null;
this.columnUtils.depthFirstAllColumnTreeSearch(allColumnGroups, (child: ColumnGroupChild)=> {
if (child instanceof ColumnGroup) {
let columnGroup = <ColumnGroup> child;
let matched: boolean;
if (checkInstanceId) {
matched = colId === columnGroup.getGroupId() && instanceId === columnGroup.getInstanceId();
} else {
matched = colId === columnGroup.getGroupId();
}
if (matched) {
result = columnGroup;
}
}
});
return result;
}
public setColumnDefs(columnDefs: (ColDef|ColGroupDef)[], source: ColumnEventType = "api") {
// always invalidate cache on changing columns, as the column id's for the new columns
// could overlap with the old id's, so the cache would return old values for new columns.
this.valueCache.expire();
// NOTE ==================
// we should be destroying the existing columns and groups if they exist, for example, the original column
// group adds a listener to the columns, it should be also removing the listeners
this.autoGroupsNeedBuilding = true;
let balancedTreeResult = this.balancedColumnTreeBuilder.createBalancedColumnGroups(columnDefs, true);
this.primaryBalancedTree = balancedTreeResult.balancedTree;
this.primaryHeaderRowCount = balancedTreeResult.treeDept + 1;
this.primaryColumns = this.getColumnsFromTree(this.primaryBalancedTree);
this.autoRowHeightColumns = this.primaryColumns.filter( col => col.getColDef().autoHeight );
this.extractRowGroupColumns(source);
this.extractPivotColumns(source);
this.createValueColumns(source);
this.updateGridColumns();
this.updateDisplayedColumns(source);
this.checkDisplayedVirtualColumns();
this.ready = true;
let eventEverythingChanged: ColumnEverythingChangedEvent = {
type: Events.EVENT_COLUMN_EVERYTHING_CHANGED,
api: this.gridApi,
columnApi: this.columnApi,
source: source
};
this.eventService.dispatchEvent(eventEverythingChanged);
let newColumnsLoadedEvent: NewColumnsLoadedEvent = {
type: Events.EVENT_NEW_COLUMNS_LOADED,
api: this.gridApi,
columnApi: this.columnApi
};
this.eventService.dispatchEvent(newColumnsLoadedEvent);
}
public isReady(): boolean {
return this.ready;
}
private extractRowGroupColumns(source: ColumnEventType): void {
this.rowGroupColumns.forEach( column => column.setRowGroupActive(false, source) );
this.rowGroupColumns = [];
// pull out items with rowGroupIndex
this.primaryColumns.forEach( column => {
if (typeof column.getColDef().rowGroupIndex === 'number') {
this.rowGroupColumns.push(column);
column.setRowGroupActive(true, source);
}
});
// then sort them
this.rowGroupColumns.sort(function(colA: Column, colB: Column): number {
return colA.getColDef().rowGroupIndex - colB.getColDef().rowGroupIndex;
});
// now just pull out items rowGroup, they will be added at the end
// after the indexed ones, but in the order the columns appear
this.primaryColumns.forEach( column => {
if (column.getColDef().rowGroup) {
// if user already specified rowGroupIndex then we skip it as this col already included
if (this.rowGroupColumns.indexOf(column)>=0) { return; }
this.rowGroupColumns.push(column);
column.setRowGroupActive(true, source);
}
});
}
private extractPivotColumns(source: ColumnEventType): void {
this.pivotColumns.forEach( column => column.setPivotActive(false, source) );
this.pivotColumns = [];
// pull out items with pivotIndex
this.primaryColumns.forEach( (column: Column) => {
if (typeof column.getColDef().pivotIndex === 'number') {
this.pivotColumns.push(column);
column.setPivotActive(true, source);
}
});
// then sort them
this.pivotColumns.sort(function(colA: Column, colB: Column): number {
return colA.getColDef().pivotIndex - colB.getColDef().pivotIndex;
});
// now check the boolean equivalent
this.primaryColumns.forEach( (column: Column) => {
if (column.getColDef().pivot) {
// if user already specified pivotIndex then we skip it as this col already included
if (this.pivotColumns.indexOf(column)>=0) { return; }
this.pivotColumns.push(column);
column.setPivotActive(true, source);
}
});
}
public resetColumnGroupState(source: ColumnEventType = "api"): void {
let stateItems: {groupId: string, open: boolean}[] = [];
this.columnUtils.depthFirstOriginalTreeSearch(this.primaryBalancedTree, child => {
if (child instanceof OriginalColumnGroup) {
let groupState = {
groupId: child.getGroupId(),
open: child.getColGroupDef().openByDefault
};
stateItems.push(groupState);
}
});
this.setColumnGroupState(stateItems, source);
}
public getColumnGroupState(): {groupId: string, open: boolean}[] {
let columnGroupState: {groupId: string, open: boolean}[] = [];
this.columnUtils.depthFirstOriginalTreeSearch(this.gridBalancedTree, node => {
if (node instanceof OriginalColumnGroup) {
let originalColumnGroup = <OriginalColumnGroup> node;
columnGroupState.push({
groupId: originalColumnGroup.getGroupId(),
open: originalColumnGroup.isExpanded()
});
}
});
return columnGroupState;
}
public setColumnGroupState(stateItems: {groupId: string, open: boolean}[], source: ColumnEventType = "api"): void {
this.columnAnimationService.start();
let impactedGroups: OriginalColumnGroup[] = [];
stateItems.forEach( stateItem => {
let groupKey = stateItem.groupId;
let newValue = stateItem.open;
let originalColumnGroup: OriginalColumnGroup = this.getOriginalColumnGroup(groupKey);
if (!originalColumnGroup) { return; }
if (originalColumnGroup.isExpanded() === newValue) { return; }
this.logger.log('columnGroupOpened(' + originalColumnGroup.getGroupId() + ',' + newValue + ')');
originalColumnGroup.setExpanded(newValue);
impactedGroups.push(originalColumnGroup);
});
this.updateGroupsAndDisplayedColumns(source);
impactedGroups.forEach( originalColumnGroup => {
let event: ColumnGroupOpenedEvent = {
type: Events.EVENT_COLUMN_GROUP_OPENED,
columnGroup: originalColumnGroup,
api: this.gridApi,
columnApi: this.columnApi
};
this.eventService.dispatchEvent(event);
});
this.columnAnimationService.finish();
}
// called by headerRenderer - when a header is opened or closed
public setColumnGroupOpened(key: OriginalColumnGroup|string, newValue: boolean, source: ColumnEventType = "api"): void {
let keyAsString: string;
if (key instanceof OriginalColumnGroup) {
keyAsString = (<OriginalColumnGroup>key).getId();
} else {
keyAsString = <string>key;
}
this.setColumnGroupState([{groupId: keyAsString, open: newValue}], source);
}
public getOriginalColumnGroup(key: OriginalColumnGroup|string): OriginalColumnGroup {
if (key instanceof OriginalColumnGroup) {
return <OriginalColumnGroup> key;
}
if (typeof key !== 'string') {
console.error('ag-Grid: group key must be a string');
}
// otherwise, search for the column group by id
let res: OriginalColumnGroup = null;
this.columnUtils.depthFirstOriginalTreeSearch(this.gridBalancedTree, node => {
if (node instanceof OriginalColumnGroup) {
let originalColumnGroup = <OriginalColumnGroup> node;
if (originalColumnGroup.getId() === key) {
res = originalColumnGroup;
}
}
});
return res;
}
private calculateColumnsForDisplay(): Column[] {
let columnsForDisplay: Column[];
if (this.pivotMode && !this.secondaryColumnsPresent) {
// pivot mode is on, but we are not pivoting, so we only
// show columns we are aggregating on
columnsForDisplay = _.filter(this.gridColumns, column => {
let isAutoGroupCol = this.groupAutoColumns && this.groupAutoColumns.indexOf(column) >= 0;
let isValueCol = this.valueColumns && this.valueColumns.indexOf(column) >= 0;
return isAutoGroupCol || isValueCol;
});
} else {
// otherwise continue as normal. this can be working on the primary
// or secondary columns, whatever the gridColumns are set to
columnsForDisplay = _.filter(this.gridColumns, column => {
// keep col if a) it's auto-group or b) it's visible
let isAutoGroupCol = this.groupAutoColumns && this.groupAutoColumns.indexOf(column) >= 0;
return isAutoGroupCol || column.isVisible();
} );
}
return columnsForDisplay;
}
private checkColSpanActiveInCols(columns: Column[]): boolean {
let result = false;
columns.forEach( col => {
if (_.exists(col.getColDef().colSpan)) {
result = true;
}
});
return result;
}
private calculateColumnsForGroupDisplay(): void {
this.groupDisplayColumns = [];
let checkFunc = (col: Column) => {
let colDef = col.getColDef();
if (colDef && _.exists(colDef.showRowGroup)) {
this.groupDisplayColumns.push(col);
}
};
this.gridColumns.forEach(checkFunc);
if (this.groupAutoColumns) {
this.groupAutoColumns.forEach(checkFunc);
}
}
public getGroupDisplayColumns(): Column[] {
return this.groupDisplayColumns;
}
private updateDisplayedColumns(source: ColumnEventType): void {
let columnsForDisplay = this.calculateColumnsForDisplay();
this.buildDisplayedTrees(columnsForDisplay);
this.calculateColumnsForGroupDisplay();
// this is also called when a group is opened or closed
this.updateGroupsAndDisplayedColumns(source);
this.setFirstRightAndLastLeftPinned(source);
}
public isSecondaryColumnsPresent(): boolean {
return this.secondaryColumnsPresent;
}
public setSecondaryColumns(colDefs: (ColDef|ColGroupDef)[], source: ColumnEventType = "api"): void {
let newColsPresent = colDefs && colDefs.length>0;
// if not cols passed, and we had to cols anyway, then do nothing
if (!newColsPresent && !this.secondaryColumnsPresent) { return; }
if (newColsPresent) {
this.processSecondaryColumnDefinitions(colDefs);
let balancedTreeResult = this.balancedColumnTreeBuilder.createBalancedColumnGroups(colDefs, false);
this.secondaryBalancedTree = balancedTreeResult.balancedTree;
this.secondaryHeaderRowCount = balancedTreeResult.treeDept + 1;
this.secondaryColumns = this.getColumnsFromTree(this.secondaryBalancedTree);
this.secondaryColumnsPresent = true;
} else {
this.secondaryBalancedTree = null;
this.secondaryHeaderRowCount = -1;
this.secondaryColumns = null;
this.secondaryColumnsPresent = false;
}
this.updateGridColumns();
this.updateDisplayedColumns(source);
}
private processSecondaryColumnDefinitions(colDefs: (ColDef|ColGroupDef)[]): (ColDef|ColGroupDef)[] {
let columnCallback = this.gridOptionsWrapper.getProcessSecondaryColDefFunc();
let groupCallback = this.gridOptionsWrapper.getProcessSecondaryColGroupDefFunc();
if (!columnCallback && !groupCallback) { return; }
searchForColDefs(colDefs);
function searchForColDefs(colDefs2: (ColDef|ColGroupDef)[]): void {
colDefs2.forEach( function(abstractColDef: AbstractColDef) {
let isGroup = _.exists((<any>abstractColDef).children);
if (isGroup) {
let colGroupDef = <ColGroupDef> abstractColDef;
if (groupCallback) {
groupCallback(colGroupDef);
}
searchForColDefs(colGroupDef.children);
} else {
let colDef = <ColGroupDef> abstractColDef;
if (columnCallback) {
columnCallback(colDef);
}
}
});
}
}
// called from: setColumnState, setColumnDefs, setSecondaryColumns
private updateGridColumns(): void {
if (this.gridColsArePrimary) {
this.lastPrimaryOrder = this.gridColumns;
}
if (this.secondaryColumns) {
this.gridBalancedTree = this.secondaryBalancedTree.slice();
this.gridHeaderRowCount = this.secondaryHeaderRowCount;
this.gridColumns = this.secondaryColumns.slice();
this.gridColsArePrimary = false;
} else {
this.gridBalancedTree = this.primaryBalancedTree.slice();
this.gridHeaderRowCount = this.primaryHeaderRowCount;
this.gridColumns = this.primaryColumns.slice();
this.gridColsArePrimary = true;
// updateGridColumns gets called after user adds a row group. we want to maintain the order of the columns
// when this happens (eg if user moved a column) rather than revert back to the original column order.
// likewise if changing in/out of pivot mode, we want to maintain the order of the primary cols
this.orderGridColsLikeLastPrimary();
}
this.addAutoGroupToGridColumns();
this.putFixedColumnsFirst();
this.setupQuickFilterColumns();
this.clearDisplayedColumns();
this.colSpanActive = this.checkColSpanActiveInCols(this.gridColumns);
let event: GridColumnsChangedEvent = {
type: Events.EVENT_GRID_COLUMNS_CHANGED,
api: this.gridApi,
columnApi: this.columnApi
};
this.eventService.dispatchEvent(event);
}
private orderGridColsLikeLastPrimary(): void {
if (_.missing(this.lastPrimaryOrder)) { return; }
// only do the sort if all columns are accounted for. columns will be not accounted for
// if changing from secondary to primary columns
let oneMissing = false;
this.gridColumns.forEach( col => {
if (this.lastPrimaryOrder.indexOf(col)<0) { oneMissing = true; }
});
if (oneMissing) { return; }
this.gridColumns.sort( (colA: Column, colB: Column): number => {
let indexA = this.lastPrimaryOrder.indexOf(colA);
let indexB = this.lastPrimaryOrder.indexOf(colB);
return indexA - indexB;
});
}
public isPrimaryColumnGroupsPresent(): boolean {
return this.primaryHeaderRowCount > 1;
}
// if we are using autoGroupCols, then they should be included for quick filter. this covers the
// following scenarios:
// a) user provides 'field' into autoGroupCol of normal grid, so now because a valid col to filter leafs on
// b) using tree data and user depends on autoGroupCol for first col, and we also want to filter on this
// (tree data is a bit different, as parent rows can be filtered on, unlike row grouping)
private setupQuickFilterColumns(): void {
if (this.groupAutoColumns) {
this.columnsForQuickFilter = this.primaryColumns.concat(this.groupAutoColumns);
} else {
this.columnsForQuickFilter = this.primaryColumns;
}
}
private putFixedColumnsFirst(): void {
let locked = this.gridColumns.filter(c => c.isLockPosition());
let unlocked = this.gridColumns.filter(c => !c.isLockPosition());
this.gridColumns = locked.concat(unlocked);
}
private addAutoGroupToGridColumns(): void {
// add in auto-group here
this.createGroupAutoColumnsIfNeeded();
if (_.missing(this.groupAutoColumns)) { return; }
this.gridColumns = this.groupAutoColumns.concat(this.gridColumns);
let autoColBalancedTree = this.balancedColumnTreeBuilder.createForAutoGroups(this.groupAutoColumns, this.gridBalancedTree);
this.gridBalancedTree = autoColBalancedTree.concat(this.gridBalancedTree);
}
// gets called after we copy down grid columns, to make sure any part of the gui
// that tries to draw, eg the header, it will get empty lists of columns rather
// than stale columns. for example, the header will received gridColumnsChanged
// event, so will try and draw, but it will draw successfully when it acts on the
// virtualColumnsChanged event
private clearDisplayedColumns(): void {
this.displayedLeftColumnTree = [];
this.displayedRightColumnTree = [];
this.displayedCentreColumnTree = [];
this.displayedLeftHeaderRows = {};
this.displayedRightHeaderRows = {};
this.displayedCentreHeaderRows = {};
this.displayedLeftColumns = [];
this.displayedRightColumns = [];
this.displayedCenterColumns = [];
this.allDisplayedColumns = [];
this.allDisplayedVirtualColumns = [];
}
private updateGroupsAndDisplayedColumns(source: ColumnEventType) {
this.updateOpenClosedVisibilityInColumnGroups();
this.updateDisplayedColumnsFromTrees(source);
this.updateVirtualSets();
this.updateBodyWidths();
// this event is picked up by the gui, headerRenderer and rowRenderer, to recalculate what columns to display
let event: DisplayedColumnsChangedEvent = {
type: Events.EVENT_DISPLAYED_COLUMNS_CHANGED,
api: this.gridApi,
columnApi: this.columnApi
};
this.eventService.dispatchEvent(event);
}
private updateDisplayedColumnsFromTrees(source: ColumnEventType): void {
this.addToDisplayedColumns(this.displayedLeftColumnTree, this.displayedLeftColumns);
this.addToDisplayedColumns(this.displayedCentreColumnTree, this.displayedCenterColumns);
this.addToDisplayedColumns(this.displayedRightColumnTree, this.displayedRightColumns);
this.setupAllDisplayedColumns();
this.setLeftValues(source);
}
private setupAllDisplayedColumns(): void {
if (this.gridOptionsWrapper.isEnableRtl()) {
this.allDisplayedColumns = this.displayedRightColumns
.concat(this.displayedCenterColumns)
.concat(this.displayedLeftColumns);
} else {
this.allDisplayedColumns = this.displayedLeftColumns
.concat(this.displayedCenterColumns)
.concat(this.displayedRightColumns);
}
}
// sets the left pixel position of each column
private setLeftValues(source: ColumnEventType): void {
this.setLeftValuesOfColumns(source);
this.setLeftValuesOfGroups();
}
private setLeftValuesOfColumns(source: ColumnEventType): void {
// go through each list of displayed columns
let allColumns = this.primaryColumns.slice(0);
// let totalColumnWidth = this.getWidthOfColsInList()
let doingRtl = this.gridOptionsWrapper.isEnableRtl();
[this.displayedLeftColumns,this.displayedRightColumns,this.displayedCenterColumns].forEach( columns => {
if (doingRtl) {
// when doing RTL, we start at the top most pixel (ie RHS) and work backwards
let left = this.getWidthOfColsInList(columns);
columns.forEach( column => {
left -= column.getActualWidth();
column.setLeft(left, source);
});
} else {
// otherwise normal LTR, we start at zero
let left = 0;
columns.forEach( column => {
column.setLeft(left, source);
left += column.getActualWidth();
});
}
_.removeAllFromArray(allColumns, columns);
});
// items left in allColumns are columns not displayed, so remove the left position. this is
// important for the rows, as if a col is made visible, then taken out, then made visible again,
// we don't want the animation of the cell floating in from the old position, whatever that was.
allColumns.forEach( (column: Column) => {
column.setLeft(null, source);
});
}
private setLeftValuesOfGroups(): void {
// a groups left value is the lest left value of it's children
[this.displayedLeftColumnTree,this.displayedRightColumnTree,this.displayedCentreColumnTree].forEach( columns => {
columns.forEach( column => {
if (column instanceof ColumnGroup) {
let columnGroup = <ColumnGroup> column;
columnGroup.checkLeft();
}
});
});
}
private addToDisplayedColumns(displayedColumnTree: ColumnGroupChild[], displayedColumns: Column[]): void {
displayedColumns.length = 0;
this.columnUtils.depthFirstDisplayedColumnTreeSearch(displayedColumnTree, (child: ColumnGroupChild)=> {
if (child instanceof Column) {
displayedColumns.push(child);
}
});
}
private updateDisplayedCenterVirtualColumns(): {[key: string]: boolean} {
if (this.suppressColumnVirtualisation) {
// no virtualisation, so don't filter
this.allDisplayedCenterVirtualColumns = this.displayedCenterColumns;
} else {
// filter out what should be visible
this.allDisplayedCenterVirtualColumns = this.filterOutColumnsWithinViewport();
}
this.allDisplayedVirtualColumns = this.allDisplayedCenterVirtualColumns
.concat(this.displayedLeftColumns)
.concat(this.displayedRightColumns);
// return map of virtual col id's, for easy lookup when building the groups.
// the map will be colId=>true, ie col id's mapping to 'true'.
let result: any = {};
this.allDisplayedVirtualColumns.forEach( (col: Column) => {
result[col.getId()] = true;
});
return result;
}
public getVirtualHeaderGroupRow(type: string, dept: number): ColumnGroupChild[] {
let result: ColumnGroupChild[];
switch (type) {
case Column.PINNED_LEFT:
result = this.displayedLeftHeaderRows[dept];
break;
case Column.PINNED_RIGHT:
result = this.displayedRightHeaderRows[dept];
break;
default:
result = this.displayedCentreHeaderRows[dept];
break;
}
if (_.missing(result)) {
result = [];
}
return result;
}
private updateDisplayedVirtualGroups(virtualColIds: any): void {
// go through each group, see if any of it's cols are displayed, and if yes,
// then this group is included
this.displayedLeftHeaderRows = {};
this.displayedRightHeaderRows = {};
this.displayedCentreHeaderRows = {};
testGroup(this.displayedLeftColumnTree, this.displayedLeftHeaderRows, 0);
testGroup(this.displayedRightColumnTree, this.displayedRightHeaderRows, 0);
testGroup(this.displayedCentreColumnTree, this.displayedCentreHeaderRows, 0);
function testGroup(children: ColumnGroupChild[], result: {[row: number]: ColumnGroupChild[]}, dept: number): boolean {
let returnValue = false;
for (let i = 0; i<children.length; i++) {
// see if this item is within viewport
let child = children[i];
let addThisItem: boolean;
if (child instanceof Column) {
// for column, test if column is included
addThisItem = virtualColIds[child.getId()] === true;
} else {
// if group, base decision on children
let columnGroup = <ColumnGroup> child;
addThisItem = testGroup(columnGroup.getDisplayedChildren(), result, dept+1);
}
if (addThisItem) {
returnValue = true;
if (!result[dept]) {
result[dept] = [];
}
result[dept].push(child);
}
}
return returnValue;
}
}
private updateVirtualSets(): void {
let virtualColIds = this.updateDisplayedCenterVirtualColumns();
this.updateDisplayedVirtualGroups(virtualColIds);
}
private filterOutColumnsWithinViewport(): Column[] {
return _.filter(this.displayedCenterColumns, this.isColumnInViewport.bind(this));
}
// called from api
public sizeColumnsToFit(gridWidth: any, source: ColumnEventType = "api"): void {
// avoid divide by zero
let allDisplayedColumns = this.getAllDisplayedColumns();
if (gridWidth <= 0 || allDisplayedColumns.length === 0) {
return;
}
let colsToNotSpread = _.filter(allDisplayedColumns, (column: Column): boolean => {
return column.getColDef().suppressSizeToFit === true;
});
let colsToSpread = _.filter(allDisplayedColumns, (column: Column): boolean => {
return column.getColDef().suppressSizeToFit !== true;
});
// make a copy of the cols that are going to be resized
let colsToFireEventFor = colsToSpread.slice(0);
let finishedResizing = false;
while (!finishedResizing) {
finishedResizing = true;
let availablePixels = gridWidth - this.getWidthOfColsInList(colsToNotSpread);
if (availablePixels <= 0) {
// no width, set everything to minimum
colsToSpread.forEach( (column: Column) => {
column.setMinimum(source);
});
} else {
let scale = availablePixels / this.getWidthOfColsInList(colsToSpread);
// we set the pixels for the last col based on what's left, as otherwise
// we could be a pixel or two short or extra because of rounding errors.
let pixelsForLastCol = availablePixels;
// backwards through loop, as we are removing items as we go
for (let i = colsToSpread.length - 1; i >= 0; i--) {
let column = colsToSpread[i];
let newWidth = Math.round(column.getActualWidth() * scale);
if (newWidth < column.getMinWidth()) {
column.setMinimum(source);
moveToNotSpread(column);
finishedResizing = false;
} else if (column.isGreaterThanMax(newWidth)) {
column.setActualWidth(column.getMaxWidth(), source);
moveToNotSpread(column);
finishedResizing = false;
} else {
let onLastCol = i === 0;
if (onLastCol) {
column.setActualWidth(pixelsForLastCol, source);
} else {
column.setActualWidth(newWidth, source);
}
}
pixelsForLastCol -= newWidth;
}
}
}
this.setLeftValues(source);
this.updateBodyWidths();
colsToFireEventFor.forEach( (column: Column) => {
let event: ColumnResizedEvent = {
type: Events.EVENT_COLUMN_RESIZED,
column: column,
columns: [column],
finished: true,
api: this.gridApi,
columnApi: this.columnApi,
source: "sizeColumnsToFit"
};
this.eventService.dispatchEvent(event);
});
function moveToNotSpread(column: Column) {
_.removeFromArray(colsToSpread, column);
colsToNotSpread.push(column);
}
}
private buildDisplayedTrees(visibleColumns: Column[]) {
let leftVisibleColumns = _.filter(visibleColumns, (column)=> {
return column.getPinned() === 'left';
});
let rightVisibleColumns = _.filter(visibleColumns, (column)=> {
return column.getPinned() === 'right';
});
let centerVisibleColumns = _.filter(visibleColumns, (column)=> {
return column.getPinned() !== 'left' && column.getPinned() !== 'right';
});
let groupInstanceIdCreator = new GroupInstanceIdCreator();
this.displayedLeftColumnTree = this.displayedGroupCreator.createDisplayedGroups(
leftVisibleColumns, this.gridBalancedTree, groupInstanceIdCreator, this.displayedLeftColumnTree);
this.displayedRightColumnTree = this.displayedGroupCreator.createDisplayedGroups(
rightVisibleColumns, this.gridBalancedTree, groupInstanceIdCreator, this.displayedRightColumnTree);
this.displayedCentreColumnTree = this.displayedGroupCreator.createDisplayedGroups(
centerVisibleColumns, this.gridBalancedTree, groupInstanceIdCreator, this.displayedCentreColumnTree);
}
private updateOpenClosedVisibilityInColumnGroups(): void {
let allColumnGroups = this.getAllDisplayedColumnGroups();
this.columnUtils.depthFirstAllColumnTreeSearch(allColumnGroups, child => {
if (child instanceof ColumnGroup) {
let columnGroup = <ColumnGroup> child;
columnGroup.calculateDisplayedColumns();
}
});
}
public getGroupAutoColumns(): Column[] {
return this.groupAutoColumns;
}
private createGroupAutoColumnsIfNeeded(): void {
if (!this.autoGroupsNeedBuilding) { return; }
this.autoGroupsNeedBuilding = false;
// see if we need to insert the default grouping column
let needAutoColumns = (this.rowGroupColumns.length > 0 || this.usingTreeData)
&& !this.gridOptionsWrapper.isGroupSuppressAutoColumn()
&& !this.gridOptionsWrapper.isGroupUseEntireRow()
&& !this.gridOptionsWrapper.isGroupSuppressRow();
if (needAutoColumns) {
this.groupAutoColumns = this.autoGroupColService.createAutoGroupColumns(this.rowGroupColumns);
} else {
this.groupAutoColumns = null;
}
}
private createValueColumns(source: ColumnEventType): void {
this.valueColumns.forEach( column => column.setValueActive(false, source) );
this.valueColumns = [];
// override with columns that have the aggFunc specified explicitly
for (let i = 0; i < this.primaryColumns.length; i++) {
let column = this.primaryColumns[i];
if (column.getColDef().aggFunc) {
column.setAggFunc(column.getColDef().aggFunc);
this.valueColumns.push(column);
column.setValueActive(true, source);
}
}
}
private getWidthOfColsInList(columnList: Column[]) {
let result = 0;
for (let i = 0; i<columnList.length; i++) {
result += columnList[i].getActualWidth();
}
return result;
}
public getGridBalancedTree(): OriginalColumnGroupChild[] {
return this.gridBalancedTree;
}
}