ag-grid-enterprise
Version:
ag-Grid Enterprise Features
465 lines (372 loc) • 16.2 kB
text/typescript
import {
_,
Autowired,
Context,
RowRenderer,
IServerSideGetRowsParams,
IServerSideGetRowsRequest,
Logger,
NumberSequence,
PostConstruct,
RowNodeBlock,
LoggerFactory,
Qualifier,
RowNode,
Column,
ColumnController,
ValueService,
GridOptionsWrapper,
RowBounds
} from "ag-grid-community";
import {ServerSideCache, ServerSideCacheParams} from "./serverSideCache";
export class ServerSideBlock extends RowNodeBlock {
private context: Context;
private rowRenderer: RowRenderer;
private columnController: ColumnController;
private valueService: ValueService;
private gridOptionsWrapper: GridOptionsWrapper;
private logger: Logger;
private displayIndexStart: number;
private displayIndexEnd: number;
private blockTop: number;
private blockHeight: number;
private params: ServerSideCacheParams;
private parentCache: ServerSideCache;
private parentRowNode: RowNode;
private level: number;
private groupLevel: boolean;
private leafGroup: boolean;
private groupField: string;
private rowGroupColumn: Column;
private nodeIdPrefix: string;
private usingTreeData: boolean;
constructor(pageNumber: number, parentRowNode: RowNode, params: ServerSideCacheParams, parentCache: ServerSideCache) {
super(pageNumber, params);
this.params = params;
this.parentRowNode = parentRowNode;
this.parentCache = parentCache;
this.level = parentRowNode.level + 1;
this.groupLevel = params.rowGroupCols ? this.level < params.rowGroupCols.length : undefined;
this.leafGroup = params.rowGroupCols ? this.level === params.rowGroupCols.length - 1 : false;
}
protected init(): void {
this.usingTreeData = this.gridOptionsWrapper.isTreeData();
if (!this.usingTreeData && this.groupLevel) {
let groupColVo = this.params.rowGroupCols[this.level];
this.groupField = groupColVo.field;
this.rowGroupColumn = this.columnController.getRowGroupColumns()[this.level];
}
this.createNodeIdPrefix();
super.init({
context: this.context,
rowRenderer: this.rowRenderer
});
}
private setBeans( loggerFactory: LoggerFactory) {
this.logger = loggerFactory.create('ServerSideBlock');
}
private createNodeIdPrefix(): void {
let parts: string[] = [];
let rowNode = this.parentRowNode;
// pull keys from all parent nodes, but do not include the root node
while (rowNode.level >= 0) {
parts.push(rowNode.key);
rowNode = rowNode.parent;
}
if (parts.length>0) {
this.nodeIdPrefix = parts.reverse().join('-') + '-';
}
}
protected createIdForIndex(index: number): string {
if (_.exists(this.nodeIdPrefix)) {
return this.nodeIdPrefix + index.toString();
} else {
return index.toString();
}
}
public getNodeIdPrefix(): string {
return this.nodeIdPrefix;
}
public getRow(displayRowIndex: number): RowNode {
// do binary search of tree
// http://oli.me.uk/2013/06/08/searching-javascript-arrays-with-a-binary-search/
let bottomPointer = this.getStartRow();
// the end row depends on whether all this block is used or not. if the virtual row count
// is before the end, then not all the row is used
let virtualRowCount = this.parentCache.getVirtualRowCount();
let endRow = this.getEndRow();
let actualEnd = (virtualRowCount < endRow) ? virtualRowCount : endRow;
let topPointer = actualEnd - 1;
if (_.missing(topPointer) || _.missing(bottomPointer)) {
console.warn(`ag-grid: error: topPointer = ${topPointer}, bottomPointer = ${bottomPointer}`);
return null;
}
while (true) {
let midPointer = Math.floor((bottomPointer + topPointer) / 2);
let currentRowNode = super.getRowUsingLocalIndex(midPointer);
if (currentRowNode.rowIndex === displayRowIndex) {
return currentRowNode;
}
let childrenCache = <ServerSideCache> currentRowNode.childrenCache;
if (currentRowNode.rowIndex === displayRowIndex) {
return currentRowNode;
} else if (currentRowNode.expanded && childrenCache && childrenCache.isDisplayIndexInCache(displayRowIndex)) {
return childrenCache.getRow(displayRowIndex);
} else if (currentRowNode.rowIndex < displayRowIndex) {
bottomPointer = midPointer + 1;
} else if (currentRowNode.rowIndex > displayRowIndex) {
topPointer = midPointer - 1;
}
}
}
protected setDataAndId(rowNode: RowNode, data: any, index: number): void {
rowNode.stub = false;
if (_.exists(data)) {
// if the user is not providing id's, then we build an id based on the index.
// for infinite scrolling, the index is used on it's own. for Server Side Row Model,
// we combine the index with the level and group key, so that the id is
// unique across the set.
//
// unique id is needed for selection (so selection can be maintained when
// doing server side sorting / filtering) - if user is not providing id's
// (and we use the indexes) then selection will not work between sorting &
// filtering.
//
// id's are also used by the row renderer for updating the dom as it identifies
// rowNodes by id
let idToUse = this.createIdForIndex(index);
rowNode.setDataAndId(data, idToUse);
rowNode.setRowHeight(this.gridOptionsWrapper.getRowHeightForNode(rowNode));
if (this.usingTreeData) {
let getServerSideGroupKey = this.gridOptionsWrapper.getServerSideGroupKeyFunc();
if (_.exists(getServerSideGroupKey)) {
rowNode.key = getServerSideGroupKey(rowNode.data);
}
let isServerSideGroup = this.gridOptionsWrapper.getIsServerSideGroupFunc();
if (_.exists(isServerSideGroup)) {
rowNode.group = isServerSideGroup(rowNode.data);
}
} else if (rowNode.group) {
rowNode.key = this.valueService.getValue(this.rowGroupColumn, rowNode);
if (rowNode.key===null || rowNode.key===undefined) {
_.doOnce( ()=> {
console.warn(`null and undefined values are not allowed for server side row model keys`);
if (this.rowGroupColumn) { console.warn(`column = ${this.rowGroupColumn.getId()}`);}
console.warn(`data is `, rowNode.data);
}, 'ServerSideBlock-CannotHaveNullOrUndefinedForKey');
}
}
} else {
rowNode.setDataAndId(undefined, undefined);
rowNode.key = null;
}
if (this.usingTreeData || this.groupLevel) {
this.setGroupDataIntoRowNode(rowNode);
this.setChildCountIntoRowNode(rowNode);
}
}
private setChildCountIntoRowNode(rowNode: RowNode): void {
let getChildCount = this.gridOptionsWrapper.getChildCountFunc();
if (getChildCount) {
rowNode.allChildrenCount = getChildCount(rowNode.data);
}
}
private setGroupDataIntoRowNode(rowNode: RowNode): void {
let groupDisplayCols: Column[] = this.columnController.getGroupDisplayColumns();
let usingTreeData = this.gridOptionsWrapper.isTreeData();
groupDisplayCols.forEach(col => {
if (usingTreeData) {
if (_.missing(rowNode.groupData)) {
rowNode.groupData = {};
}
rowNode.groupData[col.getColId()] = rowNode.key;
} else if (col.isRowGroupDisplayed(this.rowGroupColumn.getId())) {
let groupValue = this.valueService.getValue(this.rowGroupColumn, rowNode);
if (_.missing(rowNode.groupData)) {
rowNode.groupData = {};
}
rowNode.groupData[col.getColId()] = groupValue;
}
});
}
protected loadFromDatasource(): void {
let params = this.createLoadParams();
setTimeout(()=> {
this.params.datasource.getRows(params);
}, 0);
}
protected createBlankRowNode(rowIndex: number): RowNode {
let rowNode = super.createBlankRowNode(rowIndex);
rowNode.group = this.groupLevel;
rowNode.leafGroup = this.leafGroup;
rowNode.level = this.level;
rowNode.uiLevel = this.level;
rowNode.parent = this.parentRowNode;
// stub gets set to true here, and then false when this rowNode gets it's data
rowNode.stub = true;
if (rowNode.group) {
rowNode.expanded = false;
rowNode.field = this.groupField;
rowNode.rowGroupColumn = this.rowGroupColumn;
}
return rowNode;
}
private createGroupKeys(groupNode: RowNode): string[] {
let keys: string[] = [];
let pointer = groupNode;
while (pointer.level >= 0) {
keys.push(pointer.key);
pointer = pointer.parent;
}
keys.reverse();
return keys;
}
public isPixelInRange(pixel: number): boolean {
return pixel >= this.blockTop && pixel < (this.blockTop + this.blockHeight);
}
public getRowBounds(index: number, virtualRowCount: number): RowBounds {
let start = this.getStartRow();
let end = this.getEndRow();
for (let i = start; i<=end; i++) {
// the blocks can have extra rows in them, if they are the last block
// in the cache and the virtual row count doesn't divide evenly by the
if (i >= virtualRowCount) { continue; }
let rowNode = this.getRowUsingLocalIndex(i);
if (rowNode) {
if (rowNode.rowIndex === index) {
return {
rowHeight: rowNode.rowHeight,
rowTop: rowNode.rowTop
};
}
if (rowNode.group && rowNode.expanded && _.exists(rowNode.childrenCache)) {
let serverSideCache = <ServerSideCache> rowNode.childrenCache;
if (serverSideCache.isDisplayIndexInCache(index)) {
return serverSideCache.getRowBounds(index);
}
}
}
}
console.error(`ag-Grid: looking for invalid row index in Server Side Row Model, index=${index}`);
return null;
}
public getRowIndexAtPixel(pixel: number, virtualRowCount: number): number {
let start = this.getStartRow();
let end = this.getEndRow();
for (let i = start; i<=end; i++) {
// the blocks can have extra rows in them, if they are the last block
// in the cache and the virtual row count doesn't divide evenly by the
if (i >= virtualRowCount) { continue; }
let rowNode = this.getRowUsingLocalIndex(i);
if (rowNode) {
if (rowNode.isPixelInRange(pixel)) {
return rowNode.rowIndex;
}
if (rowNode.group && rowNode.expanded && _.exists(rowNode.childrenCache)) {
let serverSideCache = <ServerSideCache> rowNode.childrenCache;
if (serverSideCache.isPixelInRange(pixel)) {
return serverSideCache.getRowIndexAtPixel(pixel);
}
}
}
}
console.warn(`ag-Grid: invalid pixel range for server side block ${pixel}`);
return 0;
}
public clearRowTops(virtualRowCount: number): void {
this.forEachRowNode(virtualRowCount, rowNode => {
rowNode.clearRowTop();
let hasChildCache = rowNode.group && _.exists(rowNode.childrenCache);
if (hasChildCache) {
let serverSideCache = <ServerSideCache> rowNode.childrenCache;
serverSideCache.clearRowTops();
}
});
}
public setDisplayIndexes(displayIndexSeq: NumberSequence,
virtualRowCount: number,
nextRowTop: {value: number}): void {
this.displayIndexStart = displayIndexSeq.peek();
this.blockTop = nextRowTop.value;
this.forEachRowNode(virtualRowCount, rowNode => {
let rowIndex = displayIndexSeq.next();
rowNode.setRowIndex(rowIndex);
rowNode.setRowTop(nextRowTop.value);
nextRowTop.value += rowNode.rowHeight;
let hasChildCache = rowNode.group && _.exists(rowNode.childrenCache);
if (hasChildCache) {
let serverSideCache = <ServerSideCache> rowNode.childrenCache;
if (rowNode.expanded) {
serverSideCache.setDisplayIndexes(displayIndexSeq, nextRowTop);
} else {
// we need to clear the row tops, as the row renderer depends on
// this to know if the row should be faded out
serverSideCache.clearRowTops();
}
}
});
this.displayIndexEnd = displayIndexSeq.peek();
this.blockHeight = nextRowTop.value - this.blockTop;
}
private forEachRowNode(virtualRowCount: number, callback: (rowNode: RowNode)=>void): void {
let start = this.getStartRow();
let end = this.getEndRow();
for (let i = start; i<=end; i++) {
// the blocks can have extra rows in them, if they are the last block
// in the cache and the virtual row count doesn't divide evenly by the
if (i >= virtualRowCount) {
continue;
}
let rowNode = this.getRowUsingLocalIndex(i);
if (rowNode) {
callback(rowNode);
}
}
}
private createLoadParams(): IServerSideGetRowsParams {
let groupKeys = this.createGroupKeys(this.parentRowNode);
let request: IServerSideGetRowsRequest = {
startRow: this.getStartRow(),
endRow: this.getEndRow(),
rowGroupCols: this.params.rowGroupCols,
valueCols: this.params.valueCols,
pivotCols: this.params.pivotCols,
pivotMode: this.params.pivotMode,
groupKeys: groupKeys,
filterModel: this.params.filterModel,
sortModel: this.params.sortModel
};
let params = <IServerSideGetRowsParams> {
successCallback: this.pageLoaded.bind(this, this.getVersion()),
failCallback: this.pageLoadFailed.bind(this),
request: request,
parentNode: this.parentRowNode
};
return params;
}
public isDisplayIndexInBlock(displayIndex: number): boolean {
return displayIndex >= this.displayIndexStart && displayIndex < this.displayIndexEnd;
}
public isBlockBefore(displayIndex: number): boolean {
return displayIndex >= this.displayIndexEnd;
}
public getDisplayIndexStart(): number {
return this.displayIndexStart;
}
public getDisplayIndexEnd(): number {
return this.displayIndexEnd;
}
public getBlockHeight(): number {
return this.blockHeight;
}
public getBlockTop(): number {
return this.blockTop;
}
public isGroupLevel(): boolean {
return this.groupLevel;
}
public getGroupField(): string {
return this.groupField;
}
}