UNPKG

ag-grid

Version:

Advanced Data Grid / Data Table supporting Javascript / React / AngularJS / Web Components

1,291 lines (1,075 loc) 90.5 kB
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"; @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('gridPanel') private gridPanel: GridPanel; @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; // 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; 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(); if (this.isPivotSettingAllowed(pivotMode)) { this.pivotMode = pivotMode; } this.usingTreeData = this.gridOptionsWrapper.isTreeData(); } 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, gapBeforeCallback?: (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); if (colSpan > 1) { let colsToRemove = colSpan - 1; i += colsToRemove; } let filterPasses = filterCallback ? filterCallback(col) : true; if (filterPasses) { let gapBeforeColumn = gapBeforeCallback ? gapBeforeCallback(col) : false; let addInPreviousColumn = result.length===0 && gapBeforeColumn && lastConsideredCol; if (addInPreviousColumn) { 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 gapBeforeCallback = (col: Column) => col.getLeft() > this.viewportLeft; return this.getDisplayedColumnsForRow(rowNode, this.displayedCenterColumns, this.isColumnInViewport.bind(this), gapBeforeCallback); } 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 // todo: this needs to be cached public getPinnedLeftContainerWidth() { return this.getWidthOfColsInList(this.displayedLeftColumns); } // todo: this needs to 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) { 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, newWidth: number, finished: boolean, source: ColumnEventType = "api"): void { let column = this.getPrimaryOrGridColumn(key); if (!column) { return; } newWidth = this.normaliseColumnWidth(column, newWidth); let widthChanged = column.getActualWidth() !== newWidth; if (widthChanged) { column.setActualWidth(newWidth, source); 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 (finished || widthChanged) { let event: ColumnResizedEvent = { type: Events.EVENT_COLUMN_RESIZED, columns: [column], column: column, 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 : []; } // + inMemoryRowModel 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: // + inMemoryRowController -> 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 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): any { 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; let resultItem = { colId: column.getColId(), hide: !column.isVisible(), aggFunc: aggFunc, width: column.getActualWidth(), pivotIndex: pivotIndex, pinned: column.getPinned(), rowGroupIndex: rowGroupIndex }; return resultItem; } public getColumnState(): any[] { if (_.missing(this.primaryColumns)) { return <any>[]; } let columnStateList = 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: any[], 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: any) => { 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: any, rowGroupIndexes: {[key: string]: number}, pivotIndexes: {[key: string]: number}, source: ColumnEventType): void { // following ensures we ar