igniteui-angular-sovn
Version:
Ignite UI for Angular is a dependency-free Angular toolkit for building modern web apps
632 lines (550 loc) • 24.6 kB
text/typescript
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
import { cloneArray, reverseMapper, mergeObjects } from '../core/utils';
import { DataUtil, GridColumnDataType } from '../data-operations/data-util';
import { IFilteringExpressionsTree } from '../data-operations/filtering-expressions-tree';
import { Transaction, TransactionType, State } from '../services/transaction/transaction';
import { IgxCell, IgxGridCRUDService, IgxEditRow } from './common/crud.service';
import { CellType, ColumnType, GridServiceType, GridType, RowType } from './common/grid.interface';
import { IGridEditEventArgs, IPinRowEventArgs, IRowToggleEventArgs } from './common/events';
import { IgxColumnMovingService } from './moving/moving.service';
import { IGroupingExpression } from '../data-operations/grouping-expression.interface';
import { ISortingExpression, SortingDirection } from '../data-operations/sorting-strategy';
import { FilterUtil } from '../data-operations/filtering-strategy';
/**
* @hidden
*/
export class GridBaseAPIService<T extends GridType> implements GridServiceType {
public grid: T;
protected destroyMap: Map<string, Subject<boolean>> = new Map<string, Subject<boolean>>();
constructor(
public crudService: IgxGridCRUDService,
public cms: IgxColumnMovingService
) { }
public get_column_by_name(name: string): ColumnType {
return this.grid.columns.find((col: ColumnType) => col.field === name);
}
public get_summary_data() {
const grid = this.grid;
let data = grid.filteredData;
if (data && grid.hasPinnedRecords) {
data = grid._filteredUnpinnedData;
}
if (!data) {
if (grid.transactions.enabled) {
data = DataUtil.mergeTransactions(
cloneArray(grid.data),
grid.transactions.getAggregatedChanges(true),
grid.primaryKey,
grid.dataCloneStrategy
);
const deletedRows = grid.transactions.getTransactionLog().filter(t => t.type === TransactionType.DELETE).map(t => t.id);
deletedRows.forEach(rowID => {
const tempData = grid.primaryKey ? data.map(rec => rec[grid.primaryKey]) : data;
const index = tempData.indexOf(rowID);
if (index !== -1) {
data.splice(index, 1);
}
});
} else {
data = grid.data;
}
}
return data;
}
/**
* @hidden
* @internal
*/
public getRowData(rowID: any) {
const data = this.get_all_data(this.grid.transactions.enabled);
const index = this.get_row_index_in_data(rowID, data);
return data[index];
}
public get_row_index_in_data(rowID: any, dataCollection?: any[]): number {
const grid = this.grid;
if (!grid) {
return -1;
}
const data = dataCollection ?? this.get_all_data(grid.transactions.enabled);
return grid.primaryKey ? data.findIndex(record => record.recordRef ? record.recordRef[grid.primaryKey] === rowID
: record[grid.primaryKey] === rowID) : data.indexOf(rowID);
}
public get_row_by_key(rowSelector: any): RowType {
if (!this.grid) {
return null;
}
const primaryKey = this.grid.primaryKey;
if (primaryKey !== undefined && primaryKey !== null) {
return this.grid.dataRowList.find((row) => row.data[primaryKey] === rowSelector);
} else {
return this.grid.dataRowList.find((row) => row.data === rowSelector);
}
}
public get_row_by_index(rowIndex: number): RowType {
return this.grid.rowList.find((row) => row.index === rowIndex);
}
/**
* Gets the rowID of the record at the specified data view index
*
* @param index
* @param dataCollection
*/
public get_rec_id_by_index(index: number, dataCollection?: any[]): any {
dataCollection = dataCollection || this.grid.data;
if (index >= 0 && index < dataCollection.length) {
const rec = dataCollection[index];
return this.grid.primaryKey ? rec[this.grid.primaryKey] : rec;
}
return null;
}
public get_cell_by_key(rowSelector: any, field: string): CellType {
const row = this.get_row_by_key(rowSelector);
if (row && row.cells) {
return row.cells.find((cell) => cell.column.field === field);
}
}
public get_cell_by_index(rowIndex: number, columnID: number | string): CellType {
const row = this.get_row_by_index(rowIndex);
const hasCells = row && row.cells;
if (hasCells && typeof columnID === 'number') {
return row.cells.find((cell) => cell.column.index === columnID);
}
if (hasCells && typeof columnID === 'string') {
return row.cells.find((cell) => cell.column.field === columnID);
}
}
public get_cell_by_visible_index(rowIndex: number, columnIndex: number): CellType {
const row = this.get_row_by_index(rowIndex);
if (row && row.cells) {
return row.cells.find((cell) => cell.visibleColumnIndex === columnIndex);
}
}
public update_cell(cell: IgxCell): IGridEditEventArgs {
if (!cell) {
return;
}
const args = cell.createEditEventArgs(true);
this.grid.summaryService.clearSummaryCache(args);
const data = this.getRowData(cell.id.rowID);
const newRowData = reverseMapper(cell.column.field, args.newValue);
this.updateData(this.grid, cell.id.rowID, data, cell.rowData, newRowData);
if (!this.grid.crudService.row) {
this.grid.validation.update(cell.id.rowID, newRowData);
}
if (this.grid.primaryKey === cell.column.field) {
if (this.grid.pinnedRecords.length > 0) {
const rowIndex = this.grid.pinnedRecords.indexOf(cell.rowData);
if (rowIndex !== -1) {
const previousRowId = cell.value;
const rowType = this.grid.getRowByIndex(cell.rowIndex);
this.unpin_row(previousRowId, rowType);
this.pin_row(args.newValue, rowIndex, rowType);
}
}
if (this.grid.selectionService.isRowSelected(cell.id.rowID)) {
this.grid.selectionService.deselectRow(cell.id.rowID);
this.grid.selectionService.selectRowById(args.newValue);
}
if (this.grid.hasSummarizedColumns) {
this.grid.summaryService.removeSummaries(cell.id.rowID);
}
}
if (!this.grid.rowEditable || !this.crudService.row ||
this.crudService.row.id !== cell.id.rowID || !this.grid.transactions.enabled) {
this.grid.summaryService.clearSummaryCache(args);
this.grid.pipeTrigger++;
}
return args;
}
// TODO: CRUD refactor to not emit editing evts.
public update_row(row: IgxEditRow, value: any, event?: Event) {
const grid = this.grid;
const selected = grid.selectionService.isRowSelected(row.id);
const rowInEditMode = this.crudService.row;
const data = this.get_all_data(grid.transactions.enabled);
const index = this.get_row_index_in_data(row.id, data);
const hasSummarized = grid.hasSummarizedColumns;
this.crudService.updateRowEditData(row, value);
const args = row.createEditEventArgs(true, event);
// If no valid row is found
if (index === -1) {
return args;
}
if (rowInEditMode) {
const hasChanges = grid.transactions.getState(args.rowID, true);
grid.transactions.endPending(false);
if (!hasChanges) {
return args;
}
}
if (!args.newValue) {
return args;
}
if (hasSummarized) {
grid.summaryService.removeSummaries(args.rowID);
}
this.updateData(grid, row.id, data[index], args.oldValue, args.newValue);
this.grid.validation.update(row.id, args.newValue);
const newId = grid.primaryKey ? args.newValue[grid.primaryKey] : args.newValue;
if (selected) {
grid.selectionService.deselectRow(row.id);
grid.selectionService.selectRowById(newId);
}
// make sure selection is handled prior to updating the row.id
row.id = newId;
if (hasSummarized) {
grid.summaryService.removeSummaries(newId);
}
grid.pipeTrigger++;
return args;
}
public sort(expression: ISortingExpression): void {
if (expression.dir === SortingDirection.None) {
this.remove_grouping_expression(expression.fieldName);
}
const sortingState = cloneArray(this.grid.sortingExpressions);
this.prepare_sorting_expression([sortingState], expression);
this.grid.sortingExpressions = sortingState;
}
public sort_decoupled(expression: IGroupingExpression): void {
if (expression.dir === SortingDirection.None) {
this.remove_grouping_expression(expression.fieldName);
}
const groupingState = cloneArray((this.grid as any).groupingExpressions);
this.prepare_grouping_expression([groupingState], expression);
(this.grid as any).groupingExpressions = groupingState;
}
public sort_multiple(expressions: ISortingExpression[]): void {
const sortingState = cloneArray(this.grid.sortingExpressions);
for (const each of expressions) {
if (each.dir === SortingDirection.None) {
this.remove_grouping_expression(each.fieldName);
}
this.prepare_sorting_expression([sortingState], each);
}
this.grid.sortingExpressions = sortingState;
}
public sort_groupBy_multiple(expressions: ISortingExpression[]): void {
const groupingState = cloneArray((this.grid as any).groupingExpressions);
for (const each of expressions) {
if (each.dir === SortingDirection.None) {
this.remove_grouping_expression(each.fieldName);
}
this.prepare_grouping_expression([groupingState], each);
}
}
public clear_sort(fieldName: string) {
const sortingState = this.grid.sortingExpressions;
const index = sortingState.findIndex((expr) => expr.fieldName === fieldName);
if (index > -1) {
sortingState.splice(index, 1);
this.grid.sortingExpressions = sortingState;
}
}
public clear_groupby(_name?: string | Array<string>) {
}
public should_apply_number_style(column: ColumnType): boolean {
return column.dataType === GridColumnDataType.Number;
}
public get_data(): any[] {
const grid = this.grid;
const data = grid.data ? grid.data : [];
return data;
}
public get_all_data(includeTransactions = false): any[] {
const grid = this.grid;
let data = grid && grid.data ? grid.data : [];
data = includeTransactions ? grid.dataWithAddedInTransactionRows : data;
return data;
}
public get_filtered_data(): any[] {
return this.grid.filteredData;
}
public addRowToData(rowData: any, _parentID?: any) {
// Add row goes to transactions and if rowEditable is properly implemented, added rows will go to pending transactions
// If there is a row in edit - > commit and close
const grid = this.grid;
const rowId = grid.primaryKey ? rowData[grid.primaryKey] : rowData;
if (grid.transactions.enabled) {
const transaction: Transaction = { id: rowId, type: TransactionType.ADD, newValue: rowData };
grid.transactions.add(transaction);
} else {
grid.data.push(rowData);
}
grid.validation.markAsTouched(rowId);
grid.validation.update(rowId, rowData);
}
public deleteRowFromData(rowID: any, index: number) {
// if there is a row (index !== 0) delete it
// if there is a row in ADD or UPDATE state change it's state to DELETE
const grid = this.grid;
if (index !== -1) {
if (grid.transactions.enabled) {
const transaction: Transaction = { id: rowID, type: TransactionType.DELETE, newValue: null };
grid.transactions.add(transaction, grid.data[index]);
} else {
grid.data.splice(index, 1);
}
} else {
const state: State = grid.transactions.getState(rowID);
grid.transactions.add({ id: rowID, type: TransactionType.DELETE, newValue: null }, state && state.recordRef);
}
grid.validation.clear(rowID);
}
public deleteRowById(rowId: any): any {
let index: number;
const grid = this.grid;
const data = this.get_all_data();
if (grid.primaryKey) {
// eslint-disable-next-line @typescript-eslint/no-shadow
index = data.map((record) => record[grid.primaryKey]).indexOf(rowId);
} else {
index = data.indexOf(rowId);
}
const state: State = grid.transactions.getState(rowId);
const hasRowInNonDeletedState = state && state.type !== TransactionType.DELETE;
// if there is a row (index !== -1) and the we have cell in edit mode on same row exit edit mode
// if there is no row (index === -1), but there is a row in ADD or UPDATE state do as above
// Otherwise just exit - there is nothing to delete
if (index !== -1 || hasRowInNonDeletedState) {
// Always exit edit when row is deleted
this.crudService.endEdit(true);
} else {
return;
}
const record = data[index];
const key = record ? record[grid.primaryKey] : undefined;
grid.rowDeletedNotifier.next({ data: record, owner: grid, primaryKey: key });
this.deleteRowFromData(rowId, index);
if (grid.selectionService.isRowSelected(rowId)) {
grid.selectionService.deselectRowsWithNoEvent([rowId]);
} else {
grid.selectionService.clearHeaderCBState();
}
grid.pipeTrigger++;
grid.notifyChanges();
// Data needs to be recalculated if transactions are in place
// If no transactions, `data` will be a reference to the grid getter, otherwise it will be stale
const dataAfterDelete = grid.transactions.enabled ? grid.dataWithAddedInTransactionRows : data;
grid.refreshSearch();
if (dataAfterDelete.length % grid.perPage === 0 && dataAfterDelete.length / grid.perPage - 1 < grid.page && grid.page !== 0) {
grid.page--;
}
return record;
}
public get_row_id(rowData) {
return this.grid.primaryKey ? rowData[this.grid.primaryKey] : rowData;
}
public row_deleted_transaction(rowID: any): boolean {
const grid = this.grid;
if (!grid) {
return false;
}
if (!grid.transactions.enabled) {
return false;
}
const state = grid.transactions.getState(rowID);
if (state) {
return state.type === TransactionType.DELETE;
}
return false;
}
public get_row_expansion_state(record: any): boolean {
const grid = this.grid;
const states = grid.expansionStates;
const rowID = grid.primaryKey ? record[grid.primaryKey] : record;
const expanded = states.get(rowID);
if (expanded !== undefined) {
return expanded;
} else {
return grid.getDefaultExpandState(record);
}
}
public set_row_expansion_state(rowID: any, expanded: boolean, event?: Event) {
const grid = this.grid;
const expandedStates = grid.expansionStates;
if (!this.allow_expansion_state_change(rowID, expanded)) {
return;
}
const args: IRowToggleEventArgs = {
rowID,
expanded,
event,
cancel: false
};
grid.rowToggle.emit(args);
if (args.cancel) {
return;
}
expandedStates.set(rowID, expanded);
grid.expansionStates = expandedStates;
// K.D. 28 Feb, 2022 #10634 Don't trigger endEdit/commit upon row expansion state change
// this.crudService.endEdit(false);
}
public get_rec_by_id(rowID) {
return this.grid.primaryKey ? this.getRowData(rowID) : rowID;
}
/**
* Returns the index of the record in the data view by pk or -1 if not found or primaryKey is not set.
*
* @param pk
* @param dataCollection
*/
public get_rec_index_by_id(pk: string | number, dataCollection?: any[]): number {
dataCollection = dataCollection || this.grid.data;
return this.grid.primaryKey ? dataCollection.findIndex(rec => rec[this.grid.primaryKey] === pk) : -1;
}
public allow_expansion_state_change(rowID, expanded) {
return this.grid.expansionStates.get(rowID) !== expanded;
}
public prepare_sorting_expression(stateCollections: Array<Array<any>>, expression: ISortingExpression) {
if (expression.dir === SortingDirection.None) {
stateCollections.forEach(state => {
state.splice(state.findIndex((expr) => expr.fieldName === expression.fieldName), 1);
});
return;
}
/**
* We need to make sure the states in each collection with same fields point to the same object reference.
* If the different state collections provided have different sizes we need to get the largest one.
* That way we can get the state reference from the largest one that has the same fieldName as the expression to prepare.
*/
let maxCollection = stateCollections[0];
for (let i = 1; i < stateCollections.length; i++) {
if (maxCollection.length < stateCollections[i].length) {
maxCollection = stateCollections[i];
}
}
const maxExpr = maxCollection.find((expr) => expr.fieldName === expression.fieldName);
stateCollections.forEach(collection => {
const myExpr = collection.find((expr) => expr.fieldName === expression.fieldName);
if (!myExpr && !maxExpr) {
// Expression with this fieldName is missing from the current and the max collection.
collection.push(expression);
} else if (!myExpr && maxExpr) {
// Expression with this fieldName is missing from the current and but the max collection has.
collection.push(maxExpr);
Object.assign(maxExpr, expression);
} else {
// The current collection has the expression so just update it.
Object.assign(myExpr, expression);
}
});
}
public prepare_grouping_expression(stateCollections: Array<Array<any>>, expression: IGroupingExpression) {
if (expression.dir === SortingDirection.None) {
stateCollections.forEach(state => {
state.splice(state.findIndex((expr) => expr.fieldName === expression.fieldName), 1);
});
return;
}
/**
* We need to make sure the states in each collection with same fields point to the same object reference.
* If the different state collections provided have different sizes we need to get the largest one.
* That way we can get the state reference from the largest one that has the same fieldName as the expression to prepare.
*/
let maxCollection = stateCollections[0];
for (let i = 1; i < stateCollections.length; i++) {
if (maxCollection.length < stateCollections[i].length) {
maxCollection = stateCollections[i];
}
}
const maxExpr = maxCollection.find((expr) => expr.fieldName === expression.fieldName);
stateCollections.forEach(collection => {
const myExpr = collection.find((expr) => expr.fieldName === expression.fieldName);
if (!myExpr && !maxExpr) {
// Expression with this fieldName is missing from the current and the max collection.
collection.push(expression);
} else if (!myExpr && maxExpr) {
// Expression with this fieldName is missing from the current and but the max collection has.
collection.push(maxExpr);
Object.assign(maxExpr, expression);
} else {
// The current collection has the expression so just update it.
Object.assign(myExpr, expression);
}
});
}
public remove_grouping_expression(_fieldName) {
}
public filterDataByExpressions(expressionsTree: IFilteringExpressionsTree): any[] {
let data = this.get_all_data();
if (expressionsTree.filteringOperands.length) {
const state = { expressionsTree, strategy: this.grid.filterStrategy };
data = FilterUtil.filter(cloneArray(data), state, this.grid);
}
return data;
}
public sortDataByExpressions(data: any[], expressions: ISortingExpression[]) {
return DataUtil.sort(cloneArray(data), expressions, this.grid.sortStrategy, this.grid);
}
public pin_row(rowID: any, index?: number, row?: RowType): void {
const grid = (this.grid as any);
if (grid._pinnedRecordIDs.indexOf(rowID) !== -1) {
return;
}
const eventArgs = this.get_pin_row_event_args(rowID, index, row, true);
grid.rowPinning.emit(eventArgs);
if (eventArgs.cancel) {
return;
}
const insertIndex = typeof eventArgs.insertAtIndex === 'number' ? eventArgs.insertAtIndex : grid._pinnedRecordIDs.length;
grid._pinnedRecordIDs.splice(insertIndex, 0, rowID);
}
public unpin_row(rowID: any, row: RowType): void {
const grid = (this.grid as any);
const index = grid._pinnedRecordIDs.indexOf(rowID);
if (index === -1) {
return;
}
const eventArgs = this.get_pin_row_event_args(rowID, null , row, false);
grid.rowPinning.emit(eventArgs);
if (eventArgs.cancel) {
return;
}
grid._pinnedRecordIDs.splice(index, 1);
}
public get_pin_row_event_args(rowID: any, index?: number, row?: RowType, pinned?: boolean) {
const eventArgs: IPinRowEventArgs = {
isPinned: pinned ? true : false,
rowID,
row,
cancel: false
}
if (typeof index === 'number') {
eventArgs.insertAtIndex = index <= this.grid.pinnedRecords.length ? index : this.grid.pinnedRecords.length;
}
return eventArgs;
}
/**
* Updates related row of provided grid's data source with provided new row value
*
* @param grid Grid to update data for
* @param rowID ID of the row to update
* @param rowValueInDataSource Initial value of the row as it is in data source
* @param rowCurrentValue Current value of the row as it is with applied previous transactions
* @param rowNewValue New value of the row
*/
protected updateData(grid, rowID, rowValueInDataSource: any, rowCurrentValue: any, rowNewValue: { [x: string]: any }) {
if (grid.transactions.enabled) {
const transaction: Transaction = {
id: rowID,
type: TransactionType.UPDATE,
newValue: rowNewValue
};
grid.transactions.add(transaction, rowCurrentValue);
} else {
mergeObjects(rowValueInDataSource, rowNewValue);
}
}
protected update_row_in_array(value: any, rowID: any, index: number) {
const grid = this.grid;
grid.data[index] = value;
}
protected getSortStrategyPerColumn(fieldName: string) {
return this.get_column_by_name(fieldName) ?
this.get_column_by_name(fieldName).sortStrategy : undefined;
}
}