UNPKG

ag-grid

Version:

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

1,272 lines (1,053 loc) 102 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"; 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