igniteui-angular-sovn
Version:
Ignite UI for Angular is a dependency-free Angular toolkit for building modern web apps
728 lines (617 loc) • 25.3 kB
text/typescript
import { Injectable } from '@angular/core';
import { first } from 'rxjs/operators';
import { IGridEditDoneEventArgs, IGridEditEventArgs, IRowDataEventArgs } from '../common/events';
import { GridType, RowType } from './grid.interface';
import { Subject } from 'rxjs';
import { copyDescriptors, isEqual, isDate } from '../../core/utils';
import { FormGroup } from '@angular/forms';
import { DateTimeUtil } from '../../date-common/util/date-time.util';
export class IgxEditRow {
public transactionState: any;
public state: any;
public newData: any;
public rowFormGroup = new FormGroup({});
constructor(public id: any, public index: number, public data: any, public grid: GridType) {
this.rowFormGroup = this.grid.validation.create(id, data);
}
public createEditEventArgs(includeNewValue = true, event?: Event): IGridEditEventArgs {
const args: IGridEditEventArgs = {
rowID: this.id,
primaryKey: this.id,
rowData: this.data,
oldValue: this.data,
cancel: false,
owner: this.grid,
isAddRow: false,
valid: this.rowFormGroup.valid,
event
};
if (includeNewValue) {
args.newValue = this.newData ?? this.data;
}
return args;
}
public createDoneEditEventArgs(cachedRowData: any, event?: Event): IGridEditDoneEventArgs {
const updatedData = this.grid.transactions.enabled ?
this.grid.transactions.getAggregatedValue(this.id, true) : this.grid.gridAPI.getRowData(this.id);
const rowData = updatedData ?? this.grid.gridAPI.getRowData(this.id);
const args: IGridEditDoneEventArgs = {
rowID: this.id,
primaryKey: this.id,
rowData,
oldValue: cachedRowData,
newValue: updatedData,
owner: this.grid,
isAddRow: false,
valid: true,
event
};
return args;
}
public getClassName() {
return this.constructor.name;
}
}
export class IgxAddRow extends IgxEditRow {
public isAddRow = true;
constructor(id: any,
index: number,
data: any,
public recordRef: any,
grid: GridType) {
super(id, index, data, grid);
}
public override createEditEventArgs(includeNewValue = true, event?: Event): IGridEditEventArgs {
const args = super.createEditEventArgs(includeNewValue, event);
args.oldValue = null;
args.isAddRow = true;
return args;
}
public override createDoneEditEventArgs(cachedRowData: any, event?: Event): IGridEditDoneEventArgs {
const args = super.createDoneEditEventArgs(null, event);
args.isAddRow = true;
return args;
}
}
export interface IgxAddRowParent {
rowID: string;
index: number;
asChild: boolean;
isPinned: boolean;
}
export class IgxCell {
public primaryKey: any;
public state: any;
public pendingValue: any;
constructor(
public id,
public rowIndex: number,
public column,
public value: any,
public _editValue: any,
public rowData: any,
public grid: GridType) {
this.grid.validation.create(id.rowID, rowData);
}
public get editValue() {
const formControl = this.grid.validation.getFormControl(this.id.rowID, this.column.field);
if (formControl) {
return formControl.value;
}
}
public set editValue(value) {
const formControl = this.grid.validation.getFormControl(this.id.rowID, this.column.field);
if (this.grid.validationTrigger === 'change') {
// in case trigger is change, mark as touched.
formControl.setValue(value);
formControl.markAsTouched();
} else {
this.pendingValue = value;
}
}
public castToNumber(value: any): any {
if (this.column.dataType === 'number' && !this.column.inlineEditorTemplate) {
const v = parseFloat(value);
return !isNaN(v) && isFinite(v) ? v : 0;
}
return value;
}
public createEditEventArgs(includeNewValue = true, event?: Event): IGridEditEventArgs {
const formControl = this.grid.validation.getFormControl(this.id.rowID, this.column.field);
const args: IGridEditEventArgs = {
rowID: this.id.rowID,
primaryKey: this.id.rowID,
cellID: this.id,
rowData: this.rowData,
oldValue: this.value,
cancel: false,
column: this.column,
owner: this.grid,
valid: formControl ? formControl.valid : true,
event
};
if (includeNewValue) {
args.newValue = this.castToNumber(this.editValue);
}
return args;
}
public createDoneEditEventArgs(value: any, event?: Event): IGridEditDoneEventArgs {
const updatedData = this.grid.transactions.enabled ?
this.grid.transactions.getAggregatedValue(this.id.rowID, true) : this.rowData;
const rowData = updatedData === null ? this.grid.gridAPI.getRowData(this.id.rowID) : updatedData;
const formControl = this.grid.validation.getFormControl(this.id.rowID, this.column.field);
const args: IGridEditDoneEventArgs = {
rowID: this.id.rowID,
primaryKey: this.id.rowID,
cellID: this.id,
// rowData - should be the updated/committed rowData - this effectively should be the newValue
// the only case we use this.rowData directly, is when there is no rowEditing or transactions enabled
rowData,
oldValue: this.value,
valid: formControl ? formControl.valid : true,
newValue: value,
column: this.column,
owner: this.grid,
event
};
return args;
}
}
export class IgxCellCrudState {
public grid: GridType;
public cell: IgxCell | null = null;
public row: IgxEditRow | null = null;
public isInCompositionMode = false;
public createCell(cell): IgxCell {
return this.cell = new IgxCell(cell.cellID || cell.id, cell.row.index, cell.column, cell.value, cell.value,
cell.row.data, cell.grid);
}
public createRow(cell: IgxCell): IgxEditRow {
return this.row = new IgxEditRow(cell.id.rowID, cell.rowIndex, cell.rowData, cell.grid);
}
public sameRow(rowID): boolean {
return this.row && this.row.id === rowID;
}
public sameCell(cell: IgxCell): boolean {
return (this.cell.id.rowID === cell.id.rowID &&
this.cell.id.columnID === cell.id.columnID);
}
public get cellInEditMode(): boolean {
return !!this.cell;
}
public beginCellEdit(event?: Event) {
const args = this.cell.createEditEventArgs(false, event);
this.grid.cellEditEnter.emit(args);
if (args.cancel) {
this.endCellEdit();
}
}
public cellEdit(event?: Event) {
const args = this.cell.createEditEventArgs(true, event);
this.grid.cellEdit.emit(args);
return args;
}
public updateCell(exit: boolean, event?: Event): IGridEditEventArgs {
if (!this.cell) {
return;
}
const formControl = this.grid.validation.getFormControl(this.cell.id.rowID, this.cell.column.field);
if (this.grid.validationTrigger === 'blur' && this.cell.pendingValue !== undefined) {
// in case trigger is blur, update value if there's a pending one and mark as touched.
formControl.setValue(this.cell.pendingValue);
formControl.markAsTouched();
}
if (this.grid.validationTrigger === 'blur') {
this.grid.tbody.nativeElement.focus({ preventScroll: true });
}
let doneArgs;
if (this.cell.column.dataType === 'date' && !isDate(this.cell.value)) {
if (isEqual(DateTimeUtil.parseIsoDate(this.cell.value), this.cell.editValue)) {
doneArgs = this.exitCellEdit(event);
return doneArgs;
}
}
if (isEqual(this.cell.value, this.cell.editValue)) {
doneArgs = this.exitCellEdit(event);
return doneArgs;
}
const args = this.cellEdit(event);
if (args.cancel) {
return args;
}
this.grid.gridAPI.update_cell(this.cell);
doneArgs = this.cellEditDone(event, false);
if (exit) {
doneArgs = this.exitCellEdit(event);
}
return { ...args, ...doneArgs };
}
public cellEditDone(event, addRow: boolean): IGridEditDoneEventArgs {
const newValue = this.cell.castToNumber(this.cell.editValue);
const doneArgs = this.cell.createDoneEditEventArgs(newValue, event);
this.grid.cellEditDone.emit(doneArgs);
if (addRow) {
doneArgs.rowData = this.row.data;
}
return doneArgs;
}
/** Exit cell edit mode */
public exitCellEdit(event?: Event): IGridEditDoneEventArgs {
if (!this.cell) {
return;
}
const newValue = this.cell.castToNumber(this.cell.editValue);
const args = this.cell?.createDoneEditEventArgs(newValue, event);
this.cell.value = newValue;
this.grid.cellEditExit.emit(args);
this.endCellEdit();
return args;
}
/** Clears cell editing state */
public endCellEdit() {
this.cell = null;
}
/** Returns whether the targeted cell is in edit mode */
public targetInEdit(rowIndex: number, columnIndex: number): boolean {
if (!this.cell) {
return false;
}
const res = this.cell.column.index === columnIndex && this.cell.rowIndex === rowIndex;
return res;
}
}
export class IgxRowCrudState extends IgxCellCrudState {
public closeRowEditingOverlay = new Subject();
private _rowEditingBlocked = false;
private _rowEditingStarted = false;
public get primaryKey(): any {
return this.grid.primaryKey;
}
public get rowInEditMode(): RowType {
const editRowState = this.row;
return editRowState !== null ? this.grid.rowList.find(e => e.key === editRowState.id) : null;
}
public get rowEditing(): boolean {
return this.grid.rowEditable;
}
public get nonEditable(): boolean {
return this.grid.rowEditable && (this.grid.primaryKey === undefined || this.grid.primaryKey === null);
}
public get rowEditingBlocked() {
return this._rowEditingBlocked;
}
public set rowEditingBlocked(val: boolean) {
this._rowEditingBlocked = val;
}
/** Enters row edit mode */
public beginRowEdit(event?: Event) {
if (!this.row || !(this.row.getClassName() === IgxEditRow.name)) {
if (!this.row) {
this.createRow(this.cell);
}
if (!this._rowEditingStarted) {
const rowArgs = this.row.createEditEventArgs(false, event);
this.grid.rowEditEnter.emit(rowArgs);
if (rowArgs.cancel) {
this.endEditMode();
return true;
}
this._rowEditingStarted = true;
}
this.row.transactionState = this.grid.transactions.getAggregatedValue(this.row.id, true);
this.grid.transactions.startPending();
this.grid.openRowOverlay(this.row.id);
}
}
public rowEdit(event: Event): IGridEditEventArgs {
const args = this.row.createEditEventArgs(true, event);
this.grid.rowEdit.emit(args);
return args;
}
public updateRow(commit: boolean, event?: Event): IGridEditEventArgs {
if (!this.grid.rowEditable ||
this.grid.rowEditingOverlay &&
this.grid.rowEditingOverlay.collapsed || !this.row) {
return {} as IGridEditEventArgs;
}
let args;
if (commit) {
this.row.newData = this.grid.transactions.getAggregatedValue(this.row.id, true);
this.updateRowEditData(this.row, this.row.newData);
args = this.rowEdit(event);
if (args.cancel) {
return args;
}
}
args = this.endRowTransaction(commit, event);
return args;
}
/**
* @hidden @internal
*/
public endRowTransaction(commit: boolean, event?: Event): IGridEditEventArgs {
this.row.newData = this.grid.transactions.getAggregatedValue(this.row.id, true);
let rowEditArgs = this.row.createEditEventArgs(true, event);
let nonCancelableArgs;
if (!commit) {
this.grid.transactions.endPending(false);
const isAddRow = this.row && this.row.getClassName() === IgxAddRow.name;
const id = this.row ? this.row.id : this.cell.id.rowID;
if (isAddRow) {
this.grid.validation.clear(id);
} else {
this.grid.validation.update(id, rowEditArgs.oldValue);
}
} else if (this.row.getClassName() === IgxEditRow.name) {
rowEditArgs = this.grid.gridAPI.update_row(this.row, this.row.newData, event);
nonCancelableArgs = this.rowEditDone(rowEditArgs.oldValue, event);
} else {
const rowAddArgs = this.row.createEditEventArgs(true, event);
this.grid.rowAdd.emit(rowAddArgs);
if (rowAddArgs.cancel) {
return rowAddArgs;
}
this.grid.transactions.endPending(false);
const parentId = this.getParentRowId();
this.grid.gridAPI.addRowToData(this.row.newData ?? this.row.data, parentId);
this.grid.triggerPipes();
nonCancelableArgs = this.rowEditDone(null, event);
}
nonCancelableArgs = this.exitRowEdit(rowEditArgs.oldValue, event);
return { ...nonCancelableArgs, ...rowEditArgs };
}
public rowEditDone(cachedRowData, event: Event) {
const doneArgs = this.row.createDoneEditEventArgs(cachedRowData, event);
this.grid.rowEditDone.emit(doneArgs);
return doneArgs;
}
/** Exit row edit mode */
public exitRowEdit(cachedRowData, event?: Event): IGridEditDoneEventArgs {
const nonCancelableArgs = this.row.createDoneEditEventArgs(cachedRowData, event);
this.grid.rowEditExit.emit(nonCancelableArgs);
this.grid.closeRowEditingOverlay();
this.endRowEdit();
return nonCancelableArgs;
}
/** Clears row editing state */
public endRowEdit() {
this.row = null;
this.rowEditingBlocked = false;
this._rowEditingStarted = false;
}
/** Clears cell and row editing state and closes row editing template if it is open */
public endEditMode() {
this.endCellEdit();
if (this.grid.rowEditable) {
this.endRowEdit();
this.grid.closeRowEditingOverlay();
}
}
public updateRowEditData(row: IgxEditRow, value?: any) {
const grid = this.grid;
const rowInEditMode = grid.gridAPI.crudService.row;
row.newData = value ?? rowInEditMode.transactionState;
if (rowInEditMode && row.id === rowInEditMode.id) {
// do not use spread operator here as it will copy everything over an empty object with no descriptors
row.data = Object.assign(copyDescriptors(row.data), row.data, rowInEditMode.transactionState);
// TODO: Workaround for updating a row in edit mode through the API
} else if (this.grid.transactions.enabled) {
const state = grid.transactions.getState(row.id);
row.data = state ? Object.assign({}, row.data, state.value) : row.data;
}
}
protected getParentRowId() {
return null;
}
}
export class IgxRowAddCrudState extends IgxRowCrudState {
public addRowParent: IgxAddRowParent = null;
/**
* @hidden @internal
*/
public createAddRow(parentRow: RowType, asChild?: boolean) {
this.createAddRowParent(parentRow, asChild);
const newRec = this.grid.getEmptyRecordObjectFor(parentRow);
const addRowIndex = this.addRowParent.index + 1;
return this.row = new IgxAddRow(newRec.rowID, addRowIndex, newRec.data, newRec.recordRef, this.grid);
}
/**
* @hidden @internal
*/
public createAddRowParent(row: RowType, newRowAsChild?: boolean) {
const rowIndex = row ? row.index : -1;
const rowId = row ? row.key : (rowIndex >= 0 ? this.grid.rowList.last.key : null);
const isInPinnedArea = this.grid.isRecordPinnedByViewIndex(rowIndex);
const pinIndex = this.grid.pinnedRecords.findIndex(x => x[this.primaryKey] === rowId);
const unpinIndex = this.grid.getUnpinnedIndexById(rowId);
this.addRowParent = {
rowID: rowId,
index: isInPinnedArea ? pinIndex : unpinIndex,
asChild: newRowAsChild,
isPinned: isInPinnedArea
};
}
/**
* @hidden @internal
*/
public override endRowTransaction(commit: boolean, event?: Event): IGridEditEventArgs {
const isAddRow = this.row && this.row.getClassName() === IgxAddRow.name;
if (isAddRow) {
this.grid.rowAdded.pipe(first()).subscribe((addRowArgs: IRowDataEventArgs) => {
const rowData = addRowArgs.data;
const pinnedIndex = this.grid.pinnedRecords.findIndex(x => x[this.primaryKey] === rowData[this.primaryKey]);
// A check whether the row is in the current view
const viewIndex = pinnedIndex !== -1 ? pinnedIndex : this._findRecordIndexInView(rowData);
const dataIndex = this.grid.filteredSortedData.findIndex(data => data[this.primaryKey] === rowData[this.primaryKey]);
const isInView = viewIndex !== -1 && !this.grid.navigation.shouldPerformVerticalScroll(viewIndex, 0);
const showIndex = isInView ? -1 : dataIndex;
this.grid.showSnackbarFor(showIndex);
});
}
const args = super.endRowTransaction(commit, event);
if (args.cancel) {
return args;
}
if (isAddRow) {
this.endAddRow();
if (commit) {
// this.grid.rowAddedNotifier.next({ data: args.newValue, owner: this.grid });
// this.grid.rowAdded.emit({ data: args.newValue, owner: this.grid });
const rowAddedEventArgs: IRowDataEventArgs = { data: args.newValue, owner: this.grid, primaryKey: args.newValue[this.grid.primaryKey] }
this.grid.rowAddedNotifier.next(rowAddedEventArgs);
this.grid.rowAdded.emit(rowAddedEventArgs);
}
}
return args;
}
/**
* @hidden @internal
*/
public endAddRow() {
this.addRowParent = null;
this.grid.triggerPipes();
}
/**
* @hidden
* @internal
* TODO: consider changing modifier
*/
public _findRecordIndexInView(rec) {
return this.grid.dataView.findIndex(data => data[this.primaryKey] === rec[this.primaryKey]);
}
protected override getParentRowId() {
if (this.addRowParent.asChild) {
return this.addRowParent.asChild ? this.addRowParent.rowID : undefined;
} else if (this.addRowParent.rowID !== null && this.addRowParent.rowID !== undefined) {
const spawnedForRecord = this.grid.gridAPI.get_rec_by_id(this.addRowParent.rowID);
return spawnedForRecord?.parent?.rowID;
}
}
}
export class IgxGridCRUDService extends IgxRowAddCrudState {
public enterEditMode(cell, event?: Event) {
if (this.isInCompositionMode) {
return;
}
if (this.nonEditable) {
console.warn('The grid must have a `primaryKey` specified when using `rowEditable`!');
return;
}
if (this.cellInEditMode) {
// TODO: case solely for f2/enter nav that uses enterEditMode as toggle. Refactor.
const canceled = this.endEdit(true, event);
if (!canceled || !this.cell) {
this.grid.tbody.nativeElement.focus();
}
} else {
if (this.rowEditing) {
// TODO rowData
if (this.row && !this.sameRow(cell?.cellID?.rowID)) {
this.rowEditingBlocked = this.endEdit(true, event);
if (this.rowEditingBlocked) {
return true;
}
this.rowEditingBlocked = false;
this.endRowEdit();
}
this.createCell(cell);
const canceled = this.beginRowEdit(event);
if (!canceled) {
this.beginCellEdit(event);
}
} else {
this.createCell(cell);
this.beginCellEdit(event);
}
}
}
/**
* Enters add row mode by creating temporary dummy so the user can fill in new row cells.
*
* @param parentRow Parent row after which the Add Row UI will be rendered.
* If `null` will show it at the bottom after all rows (or top if there are not rows).
* @param asChild Specifies if the new row should be added as a child to a tree row.
* @param event Base event that triggered the add row mode.
*/
public enterAddRowMode(parentRow: RowType, asChild?: boolean, event?: Event) {
if (!this.rowEditing && (this.grid.primaryKey === undefined || this.grid.primaryKey === null)) {
console.warn('The grid must use row edit mode to perform row adding! Please set rowEditable to true.');
return;
}
this.endEdit(true, event);
if (parentRow != null && this.grid.expansionStates.get(parentRow.key)) {
this.grid.collapseRow(parentRow.key);
}
this.createAddRow(parentRow, asChild);
this.grid.transactions.startPending();
if (this.addRowParent.isPinned) {
// If parent is pinned, add the new row to pinned records
(this.grid as any)._pinnedRecordIDs.splice(this.row.index, 0, this.row.id);
}
this.grid.triggerPipes();
this.grid.notifyChanges(true);
this.grid.navigateTo(this.row.index, -1);
// when selecting the dummy row we need to adjust for top pinned rows
const indexAdjust = this.grid.isRowPinningToTop ?
(!this.addRowParent.isPinned ? this.grid.pinnedRows.length : 0) :
(!this.addRowParent.isPinned ? 0 : this.grid.unpinnedRecords.length);
// TODO: Type this without shoving a bunch of internal properties in the row type
const dummyRow = this.grid.gridAPI.get_row_by_index(this.row.index + indexAdjust) as any;
dummyRow.triggerAddAnimation();
dummyRow.cdr.detectChanges();
dummyRow.addAnimationEnd.pipe(first()).subscribe(() => {
const cell = dummyRow.cells.find(c => c.editable);
if (cell) {
this.grid.gridAPI.update_cell(this.cell);
this.enterEditMode(cell, event);
cell.activate();
}
});
}
/**
* Finishes the row transactions on the current row.
*
* @remarks
* If `commit === true`, passes them from the pending state to the data (or transaction service)
* @example
* ```html
* <button igxButton (click)="grid.endEdit(true)">Commit Row</button>
* ```
* @param commit
*/
// TODO: Implement the same representation of the method without evt emission.
public endEdit(commit = true, event?: Event) {
if (!this.row && !this.cell) {
return;
}
let args;
if (commit) {
args = this.updateCell(true, event);
if (args && args.cancel) {
return args.cancel;
}
} else {
// needede because this.cell is null after exitCellEdit
// thus the next if is always false
const cell = this.cell;
this.exitCellEdit(event);
if (!this.grid.rowEditable && cell) {
const value = this.grid.transactions.getAggregatedValue(cell.id.rowID, true) || cell.rowData;
this.grid.validation.update(cell.id.rowID, value);
}
}
args = this.updateRow(commit, event);
this.rowEditingBlocked = args.cancel;
if (args.cancel) {
return true;
}
const activeCell = this.grid.selectionService.activeElement;
if (event && activeCell) {
const rowIndex = activeCell.row;
const visibleColIndex = activeCell.layout ? activeCell.layout.columnVisibleIndex : activeCell.column;
this.grid.navigateTo(rowIndex, visibleColIndex);
}
return false;
}
}