UNPKG

igniteui-angular-sovn

Version:

Ignite UI for Angular is a dependency-free Angular toolkit for building modern web apps

801 lines (725 loc) 26.5 kB
import { IGroupByRecord } from '../data-operations/groupby-record.interface'; import { IgxAddRow, IgxEditRow } from './common/crud.service'; import { GridInstanceType, GridSummaryCalculationMode, GridSummaryPosition } from './common/enums'; import { IgxGridCell } from './grid-public-cell'; import { IgxSummaryResult } from './summaries/grid-summary'; import { ITreeGridRecord } from './tree-grid/tree-grid.interfaces'; import { mergeWith } from 'lodash-es'; import { CellType, GridServiceType, GridType, IGridValidationState, RowType, ValidationStatus } from './common/grid.interface'; abstract class BaseRow implements RowType { public index: number; /** * The grid that contains the row. */ public grid: GridType; protected _data?: any; /** * Returns the view index calculated per the grid page. */ public get viewIndex(): number { return this.index + this.grid.page * this.grid.perPage; } /** * Gets the row key. * A row in the grid is identified either by: * - primaryKey data value, * - the whole rowData, if the primaryKey is omitted. * * ```typescript * let rowKey = row.key; * ``` */ public get key(): any { const data = this._data ?? this.grid.dataView[this.index]; const primaryKey = this.grid.primaryKey; return primaryKey ? data[primaryKey] : data; } /** * Gets if this represents add row UI * * ```typescript * let isAddRow = row.addRowUI; * ``` */ public get addRowUI(): boolean { return !!this.grid.crudService.row && this.grid.crudService.row.getClassName() === IgxAddRow.name && this.grid.crudService.row.id === this.key; } /** Gets the validation status and errors, if any. * ```typescript * let validation = row.validation; * let errors = validation.errors; * ``` */ public get validation(): IGridValidationState { const formGroup = this.grid.validation.getFormGroup(this.key); return { status: formGroup?.status as ValidationStatus || 'VALID', errors: formGroup?.errors } as const; } /** * The data record that populates the row. * * ```typescript * let rowData = row.data; * ``` */ public get data(): any { if (this.inEditMode) { return mergeWith(this.grid.dataCloneStrategy.clone(this._data ?? this.grid.dataView[this.index]), this.grid.transactions.getAggregatedValue(this.key, false), (objValue, srcValue) => { if (Array.isArray(srcValue)) { return objValue = srcValue; } }); } return this._data ?? this.grid.dataView[this.index]; } /** * Returns if the row is currently in edit mode. */ public get inEditMode(): boolean { if (this.grid.rowEditable) { const editRowState = this.grid.crudService.row; return (editRowState && editRowState.id === this.key) || false; } else { return false; } } /** * Gets whether the row is pinned. * Default value is `false`. * ```typescript * const isPinned = row.pinned; * ``` */ public get pinned(): boolean { return this.grid.isRecordPinned(this.data); } /** * Sets whether the row is pinned. * Default value is `false`. * ```typescript * row.pinned = !row.pinned; * ``` */ public set pinned(val: boolean) { if (val) { this.pin(); } else { this.unpin(); } } /** * Gets the row expanded/collapsed state. * * ```typescript * const isExpanded = row.expanded; * ``` */ public get expanded(): boolean { return this.grid.gridAPI.get_row_expansion_state(this.data); } /** * Expands/collapses the row. * * ```typescript * row.expanded = true; * ``` */ public set expanded(val: boolean) { this.grid.gridAPI.set_row_expansion_state(this.key, val); } /** * Gets whether the row is selected. * Default value is `false`. * ```typescript * row.selected = true; * ``` */ public get selected(): boolean { return this.grid.selectionService.isRowSelected(this.key); } /** * Sets whether the row is selected. * Default value is `false`. * ```typescript * row.selected = !row.selected; * ``` */ public set selected(val: boolean) { if (val) { this.grid.selectionService.selectRowsWithNoEvent([this.key]); } else { this.grid.selectionService.deselectRowsWithNoEvent([this.key]); } this.grid.cdr.markForCheck(); } /** * Returns if the row is in delete state. */ public get deleted(): boolean { return this.grid.gridAPI.row_deleted_transaction(this.key); } /** * Returns if the row has child rows. Always return false for IgxGridRow. */ public get hasChildren(): boolean { return false; } public get disabled(): boolean { return this.grid.isGhostRecord(this.data); } /** * Gets the rendered cells in the row component. */ public get cells(): CellType[] { const res: CellType[] = []; this.grid.columns.forEach(col => { const cell: CellType = new IgxGridCell(this.grid, this.index, col.field); res.push(cell); }); return res; } /** * Pins the specified row. * This method emits `onRowPinning` event. * * ```typescript * // pin the selected row from the grid * this.grid.selectedRows[0].pin(); * ``` */ public pin(): boolean { return this.grid.pinRow(this.key, this.index); } /** * Unpins the specified row. * This method emits `onRowPinning` event. * * ```typescript * // unpin the selected row from the grid * this.grid.selectedRows[0].unpin(); * ``` */ public unpin(): boolean { return this.grid.unpinRow(this.key); } /** * Updates the specified row object and the data source record with the passed value. * * ```typescript * // update the second selected row's value * let newValue = "Apple"; * this.grid.selectedRows[1].update(newValue); * ``` */ public update(value: any): void { const crudService = this.grid.crudService; if (crudService.cellInEditMode && crudService.cell.id.rowID === this.key) { this.grid.transactions.endPending(false); } const row = new IgxEditRow(this.key, this.index, this.data, this.grid); this.grid.gridAPI.update_row(row, value); this.grid.notifyChanges(); } /** * Removes the specified row from the grid's data source. * This method emits `onRowDeleted` event. * * ```typescript * // delete the third selected row from the grid * this.grid.selectedRows[2].delete(); * ``` */ public delete(): void { this.grid.deleteRowById(this.key); } } export class IgxGridRow extends BaseRow implements RowType { /** * @hidden */ constructor( public override grid: GridType, public override index: number, data?: any ) { super(); this._data = data && data.addRow && data.recordRef ? data.recordRef : data; } /** * Returns the view index calculated per the grid page. */ public override get viewIndex(): number { if (this.grid.paginator) { const precedingDetailRows = []; const precedingGroupRows = []; const firstRow = this.grid.dataView[0]; const hasDetailRows = this.grid.expansionStates.size; const hasGroupedRows = this.grid.groupingExpressions.length; let precedingSummaryRows = 0; const firstRowInd = this.grid.groupingFlatResult.indexOf(firstRow); // from groupingFlatResult, resolve two other collections: // precedingGroupedRows -> use it to resolve summaryRow for each group in previous pages // precedingDetailRows -> ise it to resolve the detail row for each expanded grid row in previous pages if (hasDetailRows || hasGroupedRows) { this.grid.groupingFlatResult.forEach((r, ind) => { const rowID = this.grid.primaryKey ? r[this.grid.primaryKey] : r; if (hasGroupedRows && ind < firstRowInd && this.grid.isGroupByRecord(r)) { precedingGroupRows.push(r); } if (this.grid.expansionStates.get(rowID) && ind < firstRowInd && !this.grid.isGroupByRecord(r)) { precedingDetailRows.push(r); } }); } if (this.grid.summaryCalculationMode !== GridSummaryCalculationMode.rootLevelOnly) { // if firstRow is a child of the last item in precedingGroupRows, // then summaryRow for this given groupedRecord is rendered after firstRow, // i.e. need to decrease firstRowInd to account for the above. precedingSummaryRows = precedingGroupRows.filter(gr => this.grid.isExpandedGroup(gr)).length; if (this.grid.summaryPosition === GridSummaryPosition.bottom && precedingGroupRows.length && precedingGroupRows[precedingGroupRows.length - 1].records.indexOf(firstRow) > -1) { precedingSummaryRows += -1; } } return precedingDetailRows.length + precedingSummaryRows + firstRowInd + this.index; } else { return this.index; } } /** * Returns the parent row, if grid is grouped. */ public get parent(): RowType { let parent: IgxGroupByRow; if (!this.grid.groupingExpressions.length) { return undefined; } let i = this.index - 1; while (i >= 0 && !parent) { const rec = this.grid.dataView[i]; if (this.grid.isGroupByRecord(rec)) { parent = new IgxGroupByRow(this.grid, i, rec); } i--; } return parent; } } export class IgxTreeGridRow extends BaseRow implements RowType { /** * @hidden */ constructor( public override grid: GridType, public override index: number, data?: any, private _treeRow?: ITreeGridRecord ) { super(); this._data = data && data.addRow && data.recordRef ? data.recordRef : data; } /** * Returns the view index calculated per the grid page. */ public override get viewIndex(): number { if (this.grid.hasSummarizedColumns && this.grid.page > 0) { if (this.grid.summaryCalculationMode !== GridSummaryCalculationMode.rootLevelOnly) { const firstRowIndex = this.grid.processedExpandedFlatData.indexOf(this.grid.dataView[0].data); // firstRowIndex is based on data result after all pipes triggered, excluding summary pipe const precedingSummaryRows = this.grid.summaryPosition === GridSummaryPosition.bottom ? this.grid.rootRecords.indexOf(this.getRootParent(this.grid.dataView[0])) : this.grid.rootRecords.indexOf(this.getRootParent(this.grid.dataView[0])) + 1; // there is a summary row for each root record, so we calculate how many root records are rendered before the current row return firstRowIndex + precedingSummaryRows + this.index; } } return this.index + this.grid.page * this.grid.perPage; } /** * The data passed to the row component. * * ```typescript * let selectedRowData = this.grid.selectedRows[0].data; * ``` */ public override get data(): any { if (this.inEditMode) { return mergeWith(this.grid.dataCloneStrategy.clone(this._data ?? this.grid.dataView[this.index]), this.grid.transactions.getAggregatedValue(this.key, false), (objValue, srcValue) => { if (Array.isArray(srcValue)) { return objValue = srcValue; } }); } const rec = this.grid.dataView[this.index]; return this._data ? this._data : this.grid.isTreeRow(rec) ? rec.data : rec; } /** * Returns the child rows. */ public get children(): RowType[] { const children: IgxTreeGridRow[] = []; if (this.treeRow.expanded) { this.treeRow.children.forEach((rec, i) => { const row = new IgxTreeGridRow(this.grid, this.index + 1 + i, rec.data); children.push(row); }); } return children; } /** * Returns the parent row. */ public get parent(): RowType { const row = this.grid.getRowByKey(this.treeRow.parent?.key); return row; } /** * Returns true if child rows exist. Always return false for IgxGridRow. */ public override get hasChildren(): boolean { if (this.treeRow.children) { return this.treeRow.children.length > 0; } else { return false; } } /** * The `ITreeGridRecord` with metadata about the row in the context of the tree grid. * * ```typescript * const rowParent = this.treeGrid.getRowByKey(1).treeRow.parent; * ``` */ public get treeRow(): ITreeGridRecord { return this._treeRow ?? this.grid.records.get(this.key); } /** * Gets whether the row is pinned. * * ```typescript * let isPinned = row.pinned; * ``` */ public override get pinned(): boolean { return this.grid.isRecordPinned(this); } /** * Sets whether the row is pinned. * Default value is `false`. * ```typescript * row.pinned = !row.pinned; * ``` */ public override set pinned(val: boolean) { if (val) { this.pin(); } else { this.unpin(); } } /** * Gets whether the row is expanded. * * ```typescript * let esExpanded = row.expanded; * ``` */ public override get expanded(): boolean { return this.grid.gridAPI.get_row_expansion_state(this.treeRow); } /** * Expands/collapses the row. * * ```typescript * row.expanded = true; * ``` */ public override set expanded(val: boolean) { this.grid.gridAPI.set_row_expansion_state(this.key, val); } public override get disabled(): boolean { // TODO cell return this.grid.isGhostRecord(this.data) ? this.treeRow.isFilteredOutParent === undefined : false; } private getRootParent(row: ITreeGridRecord): ITreeGridRecord { while (row.parent) { row = row.parent; } return row; } } export class IgxHierarchicalGridRow extends BaseRow implements RowType { /** * @hidden */ constructor( public override grid: GridType, public override index: number, data?: any ) { super(); this._data = data && data.addRow && data.recordRef ? data.recordRef : data; } /** * Returns true if row islands exist. */ public override get hasChildren(): boolean { return !!this.grid.childLayoutKeys.length; } /** * Returns the view index calculated per the grid page. */ public override get viewIndex() { const firstRowInd = this.grid.filteredSortedData.indexOf(this.grid.dataView[0]); const expandedRows = this.grid.filteredSortedData.filter((rec, ind) => { const rowID = this.grid.primaryKey ? rec[this.grid.primaryKey] : rec; return this.grid.expansionStates.get(rowID) && ind < firstRowInd; }); return firstRowInd + expandedRows.length + this.index; } /** * Gets the rendered cells in the row component. */ public override get cells(): CellType[] { const res: CellType[] = []; this.grid.columns.forEach(col => { const cell: CellType = new IgxGridCell(this.grid, this.index, col.field); res.push(cell); }); return res; } } export class IgxGroupByRow implements RowType { /** * Returns the row index. */ public index: number; /** * The grid that contains the row. */ public grid: GridType; /** * Returns always true, because this is in instance of an IgxGroupByRow. */ public isGroupByRow: boolean; /** * The IGroupByRecord object, representing the group record, if the row is a GroupByRow. */ public get groupRow(): IGroupByRecord { return this._groupRow ? this._groupRow : this.grid.dataView[this.index]; } /** * Returns the child rows. */ public get children(): RowType[] { const children: IgxGridRow[] = []; this.groupRow.records.forEach((rec, i) => { const row = new IgxGridRow(this.grid, this.index + 1 + i, rec); children.push(row); }); return children; } /** * Returns the view index calculated per the grid page. */ public get viewIndex(): number { if (this.grid.page) { const precedingDetailRows = []; const precedingGroupRows = []; const firstRow = this.grid.dataView[0]; const hasDetailRows = this.grid.expansionStates.size; const hasGroupedRows = this.grid.groupingExpressions.length; let precedingSummaryRows = 0; const firstRowInd = this.grid.groupingFlatResult.indexOf(firstRow); // from groupingFlatResult, resolve two other collections: // precedingGroupedRows -> use it to resolve summaryRow for each group in previous pages // precedingDetailRows -> ise it to resolve the detail row for each expanded grid row in previous pages if (hasDetailRows || hasGroupedRows) { this.grid.groupingFlatResult.forEach((r, ind) => { const rowID = this.grid.primaryKey ? r[this.grid.primaryKey] : r; if (hasGroupedRows && ind < firstRowInd && this.grid.isGroupByRecord(r)) { precedingGroupRows.push(r); } if (this.grid.expansionStates.get(rowID) && ind < firstRowInd && !this.grid.isGroupByRecord(r)) { precedingDetailRows.push(r); } }); } if (this.grid.summaryCalculationMode !== GridSummaryCalculationMode.rootLevelOnly) { // if firstRow is a child of the last item in precedingGroupRows, // then summaryRow for this given groupedRecord is rendered after firstRow, // i.e. need to decrease firstRowInd to account for the above. precedingSummaryRows = precedingGroupRows.filter(gr => this.grid.isExpandedGroup(gr)).length; if (this.grid.summaryPosition === GridSummaryPosition.bottom && precedingGroupRows.length && precedingGroupRows[precedingGroupRows.length - 1].records.indexOf(firstRow) > -1) { precedingSummaryRows += -1; } } return precedingDetailRows.length + precedingSummaryRows + firstRowInd + this.index; } else { return this.index; } } /** * @hidden */ constructor(grid: GridType, index: number, private _groupRow?: IGroupByRecord) { this.grid = grid; this.index = index; this.isGroupByRow = true; } /** * Gets whether the row is selected. * Default value is `false`. * ```typescript * row.selected = true; * ``` */ public get selected(): boolean { return this.children.every(row => row.selected); } /** * Sets whether the row is selected. * Default value is `false`. * ```typescript * row.selected = !row.selected; * ``` */ public set selected(val: boolean) { if (val) { this.children.forEach(row => { this.grid.selectionService.selectRowsWithNoEvent([row.key]); }); } else { this.children.forEach(row => { this.grid.selectionService.deselectRowsWithNoEvent([row.key]); }); } this.grid.cdr.markForCheck(); } /** * Gets/sets whether the group row is expanded. * ```typescript * const groupRowExpanded = groupRow.expanded; * ``` */ public get expanded(): boolean { return this.grid.isExpandedGroup(this.groupRow); } public set expanded(value: boolean) { this.gridAPI.set_grouprow_expansion_state(this.groupRow, value); } public isActive(): boolean { return this.grid.navigation.activeNode ? this.grid.navigation.activeNode.row === this.index : false; } /** * Toggles the group row expanded/collapsed state. * ```typescript * groupRow.toggle() * ``` */ public toggle(): void { this.grid.toggleGroup(this.groupRow); } private get gridAPI(): GridServiceType { return this.grid.gridAPI as GridServiceType; } } export class IgxSummaryRow implements RowType { /** * Returns the row index. */ public index: number; /** * The grid that contains the row. */ public grid: GridType; /** * Returns always true, because this is in instance of an IgxGroupByRow. */ public isSummaryRow: boolean; /** * Returns the curent grid type */ private gridType: GridInstanceType; /** * The IGroupByRecord object, representing the group record, if the row is a GroupByRow. */ public get summaries(): Map<string, IgxSummaryResult[]> { return this._summaries ? this._summaries : this.grid.dataView[this.index].summaries; } /** * Returns the view index calculated per the grid page. */ public get viewIndex(): number { if (this.grid.hasSummarizedColumns && this.grid.page > 0) { if (this.gridType === GridInstanceType.Grid) { if (this.grid.page) { const precedingDetailRows = []; const precedingGroupRows = []; const firstRow = this.grid.dataView[0]; const hasDetailRows = this.grid.expansionStates.size; const hasGroupedRows = this.grid.groupingExpressions.length; let precedingSummaryRows = 0; const firstRowInd = this.grid.groupingFlatResult.indexOf(firstRow); // from groupingFlatResult, resolve two other collections: // precedingGroupedRows -> use it to resolve summaryRow for each group in previous pages // precedingDetailRows -> ise it to resolve the detail row for each expanded grid row in previous pages if (hasDetailRows || hasGroupedRows) { this.grid.groupingFlatResult.forEach((r, ind) => { const rowID = this.grid.primaryKey ? r[this.grid.primaryKey] : r; if (hasGroupedRows && ind < firstRowInd && this.grid.isGroupByRecord(r)) { precedingGroupRows.push(r); } if (this.grid.expansionStates.get(rowID) && ind < firstRowInd && !this.grid.isGroupByRecord(r)) { precedingDetailRows.push(r); } }); } if (this.grid.summaryCalculationMode !== GridSummaryCalculationMode.rootLevelOnly) { // if firstRow is a child of the last item in precedingGroupRows, // then summaryRow for this given groupedRecord is rendered after firstRow, // i.e. need to decrease firstRowInd to account for the above. precedingSummaryRows = precedingGroupRows.filter(gr => this.grid.isExpandedGroup(gr)).length; if (this.grid.summaryPosition === GridSummaryPosition.bottom && precedingGroupRows.length && precedingGroupRows[precedingGroupRows.length - 1].records.indexOf(firstRow) > -1) { precedingSummaryRows += -1; } } return precedingDetailRows.length + precedingSummaryRows + firstRowInd + this.index; } else { return this.index; } } else if (this.gridType === GridInstanceType.TreeGrid) { if (this.grid.summaryCalculationMode !== GridSummaryCalculationMode.rootLevelOnly) { const firstRowIndex = this.grid.processedExpandedFlatData.indexOf(this.grid.dataView[0].data); const precedingSummaryRows = this.grid.summaryPosition === GridSummaryPosition.bottom ? this.grid.rootRecords.indexOf(this.getRootParent(this.grid.dataView[0])) : this.grid.rootRecords.indexOf(this.getRootParent(this.grid.dataView[0])) + 1; return firstRowIndex + precedingSummaryRows + this.index; } } } return this.index + this.grid.page * this.grid.perPage; } /** * @hidden */ constructor( grid: GridType, index: number, private _summaries?: Map<string, IgxSummaryResult[]>, type?: GridInstanceType ) { this.grid = grid; this.index = index; this.isSummaryRow = true; this.gridType = type; } private getRootParent(row: ITreeGridRecord): ITreeGridRecord { while (row.parent) { row = row.parent; } return row; } }