import { DOCUMENT, formatNumber, getLocaleNumberFormat, NumberFormatStyle } from '@angular/common'; import { AfterContentInit, AfterViewInit, ApplicationRef, ChangeDetectorRef, ComponentRef, ContentChild, ContentChildren, createComponent, Directive, DoCheck, ElementRef, EnvironmentInjector, EventEmitter, HostBinding, HostListener, Inject, Injector, Input, IterableChangeRecord, IterableDiffers, LOCALE_ID, NgZone, OnDestroy, OnInit, Optional, Output, QueryList, TemplateRef, ViewChild, ViewChildren, ViewContainerRef } from '@angular/core'; import { formatDate, resizeObservable } from '../core/utils'; import 'igniteui-trial-watermark'; import { Subject, pipe, fromEvent, animationFrameScheduler, merge } from 'rxjs'; import { takeUntil, first, filter, throttleTime, map, shareReplay, takeWhile } from 'rxjs/operators'; import { cloneArray, mergeObjects, compareMaps, resolveNestedPath, isObject, PlatformUtil } from '../core/utils'; import { GridColumnDataType } from '../data-operations/data-util'; import { FilteringLogic, IFilteringExpression } from '../data-operations/filtering-expression.interface'; import { IGroupByRecord } from '../data-operations/groupby-record.interface'; import { IForOfDataChangingEventArgs, IgxGridForOfDirective } from '../directives/for-of/for_of.directive'; import { IgxTextHighlightDirective } from '../directives/text-highlight/text-highlight.directive'; import { ISummaryExpression } from './summaries/grid-summary'; import { RowEditPositionStrategy } from './grid.common'; import { IgxGridToolbarComponent } from './toolbar/grid-toolbar.component'; import { IgxRowDirective } from './row.directive'; import { IgxOverlayOutletDirective, IgxToggleDirective } from '../directives/toggle/toggle.directive'; import { FilteringExpressionsTree, IFilteringExpressionsTree, FilteringExpressionsTreeType } from '../data-operations/filtering-expressions-tree'; import { IFilteringOperation } from '../data-operations/filtering-condition'; import { Transaction, TransactionType, TransactionService, State } from '../services/public_api'; import { IgxRowAddTextDirective, IgxRowEditTemplateDirective, IgxRowEditTabStopDirective, IgxRowEditTextDirective, IgxRowEditActionsDirective } from './grid.rowEdit.directive'; import { IgxGridNavigationService, IActiveNode } from './grid-navigation.service'; import { IDisplayDensityOptions, DisplayDensityToken, DisplayDensityBase, DisplayDensity } from '../core/density'; import { IgxFilteringService } from './filtering/grid-filtering.service'; import { IgxGridFilteringCellComponent } from './filtering/base/grid-filtering-cell.component'; import { WatchChanges } from './watch-changes'; import { IgxGridHeaderGroupComponent } from './headers/grid-header-group.component'; import { IGridResourceStrings } from '../core/i18n/grid-resources'; import { CurrentResourceStrings } from '../core/i18n/resources'; import { IgxGridSummaryService } from './summaries/grid-summary.service'; import { IgxSummaryRowComponent } from './summaries/summary-row.component'; import { IgxGridSelectionService } from './selection/selection.service'; import { IgxEditRow, IgxCell, IgxAddRow } from './common/crud.service'; import { ICachedViewLoadedEventArgs, IgxTemplateOutletDirective } from '../directives/template-outlet/template_outlet.directive'; import { IgxExcelStyleLoadingValuesTemplateDirective } from './filtering/excel-style/excel-style-search.component'; import { IgxGridColumnResizerComponent } from './resizing/resizer.component'; import { CharSeparatedValueData } from '../services/csv/char-separated-value-data'; import { IgxColumnResizingService } from './resizing/resizing.service'; import { FilteringStrategy, IFilteringStrategy } from '../data-operations/filtering-strategy'; import { IgxRowExpandedIndicatorDirective, IgxRowCollapsedIndicatorDirective, IgxHeaderExpandedIndicatorDirective, IgxHeaderCollapsedIndicatorDirective, IgxExcelStyleHeaderIconDirective, IgxSortAscendingHeaderIconDirective, IgxSortDescendingHeaderIconDirective, IgxSortHeaderIconDirective } from './grid/grid.directives'; import { GridKeydownTargetType, GridSelectionMode, GridSummaryPosition, GridSummaryCalculationMode, FilterMode, ColumnPinningPosition, RowPinningPosition, GridPagingMode, GridValidationTrigger } from './common/enums'; import { IGridCellEventArgs, IRowSelectionEventArgs, IPinColumnEventArgs, IGridEditEventArgs, IRowDataEventArgs, IColumnResizeEventArgs, IColumnMovingStartEventArgs, IColumnMovingEventArgs, IColumnMovingEndEventArgs, IGridKeydownEventArgs, IRowDragStartEventArgs, IRowDragEndEventArgs, IGridClipboardEvent, IGridToolbarExportEventArgs, ISearchInfo, ICellPosition, IRowToggleEventArgs, IColumnSelectionEventArgs, IPinRowEventArgs, IGridScrollEventArgs, IGridEditDoneEventArgs, IActiveNodeChangeEventArgs, ISortingEventArgs, IFilteringEventArgs, IColumnVisibilityChangedEventArgs, IColumnVisibilityChangingEventArgs, IPinColumnCancellableEventArgs } from './common/events'; import { IgxAdvancedFilteringDialogComponent } from './filtering/advanced-filtering/advanced-filtering-dialog.component'; import { ColumnType, GridServiceType, GridType, IGridFormGroupCreatedEventArgs, IGridValidationStatusEventArgs, IgxGridEmptyTemplateContext, IgxGridHeaderTemplateContext, IgxGridRowDragGhostContext, IgxGridRowEditActionsTemplateContext, IgxGridRowEditTemplateContext, IgxGridRowEditTextTemplateContext, IgxGridRowTemplateContext, IgxGridTemplateContext, IgxHeadSelectorTemplateContext, IgxRowSelectorTemplateContext, IGX_GRID_SERVICE_BASE, ISizeInfo, RowType, IPinningConfig } from './common/grid.interface'; import { DropPosition } from './moving/moving.service'; import { IgxHeadSelectorDirective, IgxRowSelectorDirective } from './selection/row-selectors'; import { IgxColumnComponent } from './columns/column.component'; import { IgxColumnGroupComponent } from './columns/column-group.component'; import { IgxRowDragGhostDirective, IgxDragIndicatorIconDirective } from './row-drag.directive'; import { IgxSnackbarComponent } from '../snackbar/snackbar.component'; import { v4 as uuidv4 } from 'uuid'; import { IgxActionStripComponent } from '../action-strip/action-strip.component'; import { IgxGridRowComponent } from './grid/grid-row.component'; import { IgxPaginatorComponent } from '../paginator/paginator.component'; import { IgxGridHeaderRowComponent } from './headers/grid-header-row.component'; import { IgxGridGroupByAreaComponent } from './grouping/grid-group-by-area.component'; import { IgxFlatTransactionFactory, TRANSACTION_TYPE } from '../services/transaction/transaction-factory.service'; import { ISortingOptions } from './columns/interfaces'; import { GridSelectionRange, IgxGridTransaction } from './common/types'; import { VerticalAlignment, HorizontalAlignment, PositionSettings, OverlaySettings } from '../services/overlay/utilities'; import { IgxOverlayService } from '../services/overlay/overlay'; import { ConnectedPositioningStrategy } from '../services/overlay/position/connected-positioning-strategy'; import { ContainerPositionStrategy } from '../services/overlay/position/container-position-strategy'; import { AbsoluteScrollStrategy } from '../services/overlay/scroll/absolute-scroll-strategy'; import { Action, StateUpdateEvent, TransactionEventOrigin } from '../services/transaction/transaction'; import { ISortingExpression } from '../data-operations/sorting-strategy'; import { IGridSortingStrategy } from './common/strategy'; import { IgxGridExcelStyleFilteringComponent } from './filtering/excel-style/excel-style-filtering.component'; import { IgxGridHeaderComponent } from './headers/grid-header.component'; import { IgxGridFilteringRowComponent } from './filtering/base/grid-filtering-row.component'; import { DefaultDataCloneStrategy, IDataCloneStrategy } from '../data-operations/data-clone-strategy'; import { IgxGridCellComponent } from './cell.component'; import { IgxGridValidationService } from './grid/grid-validation.service'; let FAKE_ROW_ID = -1; const DEFAULT_ITEMS_PER_PAGE = 15; const MINIMUM_COLUMN_WIDTH = 136; // By default row editing overlay outlet is inside grid body so that overlay is hidden below grid header when scrolling. // In cases when grid has 1-2 rows there isn't enough space in grid body and row editing overlay should be shown above header. // Default row editing overlay height is higher then row height that is why the case is valid also for row with 2 rows. // More accurate calculation is not possible, cause row editing overlay is still not shown and we don't know its height, // but in the same time we need to set row editing overlay outlet before opening the overlay itself. const MIN_ROW_EDITING_COUNT_THRESHOLD = 2; @Directive() export abstract class IgxGridBaseDirective extends DisplayDensityBase implements GridType, OnInit, DoCheck, OnDestroy, AfterContentInit, AfterViewInit { /** * Gets/Sets the display time for the row adding snackbar notification. * * @remarks * By default it is 6000ms. */ @Input() public snackbarDisplayTime = 6000; /** * Gets/Sets whether to auto-generate the columns. * * @remarks * The default value is false. When set to true, it will override all columns declared through code or in markup. * @example * ```html * * ``` */ @Input() public autoGenerate = false; /** * Gets/Sets a list of property keys to be excluded from the generated column collection * @remarks * The collection is only used during initialization and changing it will not cause any changes in the generated columns at runtime * unless the grid is destroyed and recreated. To modify the columns visible in the UI at runtime, please use their * [hidden](https://www.infragistics.com/products/ignite-ui-angular/docs/typescript/latest/classes/IgxColumnComponent.html#hidden) property. * @example * ```html * * ``` * ```typescript * const Data = [{ 'Id': '1', 'ProductName': 'name1', 'Description': 'description1', 'Count': 5 }] * ``` */ @Input() public autoGenerateExclude: string[] = []; /** * Controls whether columns moving is enabled in the grid. * */ @Input() public moving = false; /** * Gets/Sets a custom template when empty. * * @example * ```html * * ``` */ @Input() public emptyGridTemplate: TemplateRef; /** * Gets/Sets a custom template for adding row UI when grid is empty. * * @example * ```html * * ``` */ @Input() public addRowEmptyTemplate: TemplateRef; /** * Gets/Sets a custom template when loading. * * @example * ```html * * ``` */ @Input() public loadingGridTemplate: TemplateRef; /** * Get/Set IgxSummaryRow height */ @Input() public set summaryRowHeight(value: number) { this._summaryRowHeight = value | 0; this.summaryService.summaryHeight = value; if (!this._init) { this.reflow(); } } public get summaryRowHeight(): number { if (this.hasSummarizedColumns && this.rootSummariesEnabled) { return this._summaryRowHeight || this.summaryService.calcMaxSummaryHeight(); } return 0; } /** @hidden @internal */ public get hasColumnsToAutosize() { return this._columns.some(x => x.width === 'fit-content'); } /** * Gets/Sets the data clone strategy of the grid when in edit mode. * * @example * ```html * * ``` */ @Input() public get dataCloneStrategy(): IDataCloneStrategy { return this._dataCloneStrategy; } public set dataCloneStrategy(strategy: IDataCloneStrategy) { if (strategy) { this._dataCloneStrategy = strategy; this._transactions.cloneStrategy = strategy; } } /** * Controls the copy behavior of the grid. */ @Input() public clipboardOptions = { /** * Enables/disables the copy behavior */ enabled: true, /** * Include the columns headers in the clipboard output. */ copyHeaders: true, /** * Apply the columns formatters (if any) on the data in the clipboard output. */ copyFormatters: true, /** * The separator used for formatting the copy output. Defaults to `\t`. */ separator: '\t' }; /** * Emitted after filtering is performed. * * @remarks * Returns the filtering expressions tree of the column for which filtering was performed. * @example * ```html * * ``` */ @Output() public filteringExpressionsTreeChange = new EventEmitter(); /** * Emitted after advanced filtering is performed. * * @remarks * Returns the advanced filtering expressions tree. * @example * ```html * * ``` */ @Output() public advancedFilteringExpressionsTreeChange = new EventEmitter(); /** * Emitted when grid is scrolled horizontally/vertically. * * @example * ```html * * ``` */ @Output() public gridScroll = new EventEmitter(); /** * Sets a conditional class selector to the grid's row element. * Accepts an object literal, containing key-value pairs, * where the key is the name of the CSS class and the value is * either a callback function that returns a boolean, or boolean, like so: * ```typescript * callback = (row: RowType) => { return row.selected > 6; } * rowClasses = { 'className' : this.callback }; * ``` * ```html * * ``` * * @memberof IgxColumnComponent */ @Input() public rowClasses: any; /** * Sets conditional style properties on the grid row element. * It accepts an object literal where the keys are * the style properties and the value is an expression to be evaluated. * ```typescript * styles = { * background: 'yellow', * color: (row: RowType) => row.selected : 'red': 'white' * } * ``` * ```html * * ``` * * @memberof IgxColumnComponent */ @Input() public rowStyles = null; /** * Gets/Sets the primary key. * * @example * ```html * * ``` */ @WatchChanges() @Input() public primaryKey: any; /** * Gets/Sets a unique values strategy used by the Excel Style Filtering * * @remarks * Provides a callback for loading unique column values on demand. * If this property is provided, the unique values it generates will be used by the Excel Style Filtering. * @example * ```html * * ``` */ @Input() public uniqueColumnValuesStrategy: (column: ColumnType, filteringExpressionsTree: IFilteringExpressionsTree, done: (values: any[]) => void) => void; /** @hidden @internal */ @ContentChildren(IgxGridExcelStyleFilteringComponent, { read: IgxGridExcelStyleFilteringComponent, descendants: false }) public excelStyleFilteringComponents: QueryList; /** @hidden @internal */ public get excelStyleFilteringComponent() { return this.excelStyleFilteringComponents?.first; } /** @hidden @internal */ public get headerGroups() { return this.theadRow.groups; } /** * Emitted when a cell is clicked. * * @remarks * Returns the `IgxGridCell`. * @example * ```html * * ``` */ @Output() public cellClick = new EventEmitter(); /** * Emitted when formGroup is created on edit of row/cell. * * @example * ```html * * ``` */ @Output() public formGroupCreated = new EventEmitter(); /** * Emitted when grid's validation status changes. * * @example * ```html * * ``` */ @Output() public validationStatusChange = new EventEmitter(); /** * Emitted when a cell is selected. * * @remarks * Returns the `IgxGridCell`. * @example * ```html * * ``` */ @Output() public selected = new EventEmitter(); /** * Emitted when `IgxGridRowComponent` is selected. * * @example * ```html * * ``` */ @Output() public rowSelectionChanging = new EventEmitter(); /** * Emitted when `IgxColumnComponent` is selected. * * @example * ```html * * ``` */ @Output() public columnSelectionChanging = new EventEmitter(); /** * Emitted before `IgxColumnComponent` is pinned. * * @remarks * The index at which to insert the column may be changed through the `insertAtIndex` property. * @example * ```typescript * public columnPinning(event) { * if (event.column.field === "Name") { * event.insertAtIndex = 0; * } * } * ``` */ @Output() public columnPin = new EventEmitter(); /** * Emitted after `IgxColumnComponent` is pinned. * * @remarks * The index that the column is inserted at may be changed through the `insertAtIndex` property. * @example * ```typescript * public columnPinning(event) { * if (event.column.field === "Name") { * event.insertAtIndex = 0; * } * } * ``` */ @Output() public columnPinned = new EventEmitter(); /** * Emitted when cell enters edit mode. * * @remarks * This event is cancelable. * @example * ```html * * * ``` */ @Output() public cellEditEnter = new EventEmitter(); /** * Emitted when cell exits edit mode. * * @example * ```html * * * ``` */ @Output() public cellEditExit = new EventEmitter(); /** * Emitted when cell has been edited. * * @remarks * Event is fired after editing is completed, when the cell is exiting edit mode. * This event is cancelable. * @example * ```html * * * ``` */ @Output() public cellEdit = new EventEmitter(); /** * Emitted after cell has been edited and editing has been committed. * * @example * ```html * * * ``` */ @Output() public cellEditDone = new EventEmitter(); /** * Emitted when a row enters edit mode. * * @remarks * Emitted when [rowEditable]="true". * This event is cancelable. * @example * ```html * * * ``` */ @Output() public rowEditEnter = new EventEmitter(); /** * Emitted when exiting edit mode for a row. * * @remarks * Emitted when [rowEditable]="true" & `endEdit(true)` is called. * Emitted when changing rows during edit mode, selecting an un-editable cell in the edited row, * performing paging operation, column resizing, pinning, moving or hitting `Done` * button inside of the rowEditingOverlay, or hitting the `Enter` key while editing a cell. * This event is cancelable. * @example * ```html * * * ``` */ @Output() public rowEdit = new EventEmitter(); /** * Emitted after exiting edit mode for a row and editing has been committed. * * @remarks * Emitted when [rowEditable]="true" & `endEdit(true)` is called. * Emitted when changing rows during edit mode, selecting an un-editable cell in the edited row, * performing paging operation, column resizing, pinning, moving or hitting `Done` * button inside of the rowEditingOverlay, or hitting the `Enter` key while editing a cell. * @example * ```html * * * ``` */ @Output() public rowEditDone = new EventEmitter(); /** * Emitted when row editing is canceled. * * @remarks * Emits when [rowEditable]="true" & `endEdit(false)` is called. * Emitted when changing hitting `Esc` key during cell editing and when click on the `Cancel` button * in the row editing overlay. * @example * ```html * * * ``` */ @Output() public rowEditExit = new EventEmitter(); /** * Emitted when a column is initialized. * * @remarks * Returns the column object. * @example * ```html * * ``` */ @Output() public columnInit = new EventEmitter(); /** * Emitted before sorting expressions are applied. * * @remarks * Returns an `ISortingEventArgs` object. `sortingExpressions` key holds the sorting expressions. * @example * ```html * * ``` */ @Output() public sorting = new EventEmitter(); /** * Emitted after sorting is completed. * * @remarks * Returns the sorting expression. * @example * ```html * * ``` */ @Output() public sortingDone = new EventEmitter(); /** * Emitted before filtering expressions are applied. * * @remarks * Returns an `IFilteringEventArgs` object. `filteringExpressions` key holds the filtering expressions for the column. * @example * ```html * * ``` */ @Output() public filtering = new EventEmitter(); /** * Emitted after filtering is performed through the UI. * * @remarks * Returns the filtering expressions tree of the column for which filtering was performed. * @example * ```html * * ``` */ @Output() public filteringDone = new EventEmitter(); /** * Emitted when a row is added. * * @remarks * Returns the data for the new `IgxGridRowComponent` object. * @example * ```html * * ``` */ @Output() public rowAdded = new EventEmitter(); /** * Emitted when a row is deleted. * * @remarks * Returns an `IRowDataEventArgs` object. * @example * ```html * * ``` */ @Output() public rowDeleted = new EventEmitter(); /** * Emmited when deleting a row. * * @remarks * This event is cancelable. * Returns an `IGridEditEventArgs` object. * @example * ```html * * ``` */ @Output() public rowDelete = new EventEmitter(); /** * Emmited just before the newly added row is commited. * * @remarks * This event is cancelable. * Returns an `IGridEditEventArgs` object. * @example * ```html * * ``` */ @Output() public rowAdd = new EventEmitter(); /** * Emitted after column is resized. * * @remarks * Returns the `IgxColumnComponent` object's old and new width. * @example * ```html * * ``` */ @Output() public columnResized = new EventEmitter(); /** * Emitted when a cell is right clicked. * * @remarks * Returns the `IgxGridCell` object. * ```html * * ``` */ @Output() public contextMenu = new EventEmitter(); /** * Emitted when a cell is double clicked. * * @remarks * Returns the `IgxGridCell` object. * @example * ```html * * ``` */ @Output() public doubleClick = new EventEmitter(); /** * Emitted before column visibility is changed. * * @remarks * Args: { column: any, newValue: boolean } * @example * ```html * * ``` */ @Output() public columnVisibilityChanging = new EventEmitter(); /** * Emitted after column visibility is changed. * * @remarks * Args: { column: IgxColumnComponent, newValue: boolean } * @example * ```html * * ``` */ @Output() public columnVisibilityChanged = new EventEmitter(); /** * Emitted when column moving starts. * * @remarks * Returns the moved `IgxColumnComponent` object. * @example * ```html * * ``` */ @Output() public columnMovingStart = new EventEmitter(); /** * Emitted during the column moving operation. * * @remarks * Returns the source and target `IgxColumnComponent` objects. This event is cancelable. * @example * ```html * * ``` */ @Output() public columnMoving = new EventEmitter(); /** * Emitted when column moving ends. * * @remarks * Returns the source and target `IgxColumnComponent` objects. * @example * ```html * * ``` */ @Output() public columnMovingEnd = new EventEmitter(); /** * Emitted when keydown is triggered over element inside grid's body. * * @remarks * This event is fired only if the key combination is supported in the grid. * Return the target type, target object and the original event. This event is cancelable. * @example * ```html * * ``` */ @Output() public gridKeydown = new EventEmitter(); /** * Emitted when start dragging a row. * * @remarks * Return the dragged row. */ @Output() public rowDragStart = new EventEmitter(); /** * Emitted when dropping a row. * * @remarks * Return the dropped row. */ @Output() public rowDragEnd = new EventEmitter(); /** * Emitted when a copy operation is executed. * * @remarks * Fired only if copy behavior is enabled through the [`clipboardOptions`]{@link IgxGridBaseDirective#clipboardOptions}. */ @Output() public gridCopy = new EventEmitter(); /** * @hidden @internal */ @Output() public expansionStatesChange = new EventEmitter>(); /** * Emitted when the expanded state of a row gets changed. * * @example * ```html * * ``` */ @Output() public rowToggle = new EventEmitter(); /** * Emitted when the pinned state of a row is changed. * * @example * ```html * * ``` */ @Output() public rowPinning = new EventEmitter(); /** * Emitted when the pinned state of a row is changed. * * @example * ```html * * ``` */ @Output() public rowPinned = new EventEmitter(); /** * Emmited when the active node is changed. * * @example * ``` * * ``` */ @Output() public activeNodeChange = new EventEmitter(); /** * Emitted before sorting is performed. * * @remarks * Returns the sorting expressions. * @example * ```html * * ``` */ @Output() public sortingExpressionsChange = new EventEmitter(); /** * Emitted when an export process is initiated by the user. * * @example * ```typescript * toolbarExporting(event: IGridToolbarExportEventArgs){ * const toolbarExporting = event; * } * ``` */ @Output() public toolbarExporting = new EventEmitter(); /* End of toolbar related definitions */ /** * Emitted when making a range selection. * * @remarks * Range selection can be made either through drag selection or through keyboard selection. */ @Output() public rangeSelected = new EventEmitter(); /** Emitted after the ngAfterViewInit hook. At this point the grid exists in the DOM */ @Output() public rendered = new EventEmitter(); /** * @hidden @internal */ @Output() public localeChange = new EventEmitter(); /** * Emitted before the grid's data view is changed because of a data operation, rebinding, etc. * * @example * ```typescript * * ``` */ @Output() public dataChanging = new EventEmitter(); /** * Emitted after the grid's data view is changed because of a data operation, rebinding, etc. * * @example * ```typescript * * ``` */ @Output() public dataChanged = new EventEmitter(); /** * @hidden @internal */ @ViewChild(IgxSnackbarComponent) public addRowSnackbar: IgxSnackbarComponent; /** * @hidden @internal */ @ViewChild(IgxGridColumnResizerComponent) public resizeLine: IgxGridColumnResizerComponent; /** * @hidden @internal */ @ViewChild('loadingOverlay', { read: IgxToggleDirective, static: true }) public loadingOverlay: IgxToggleDirective; /** * @hidden @internal */ @ViewChild('igxLoadingOverlayOutlet', { read: IgxOverlayOutletDirective, static: true }) public loadingOutlet: IgxOverlayOutletDirective; /** * @hidden @internal */ @ContentChildren(IgxColumnComponent, { read: IgxColumnComponent, descendants: true }) public columnList: QueryList = new QueryList(); /** @hidden @internal */ @ContentChild(IgxActionStripComponent) public actionStrip: IgxActionStripComponent; /** * @hidden @internal */ @ContentChild(IgxExcelStyleLoadingValuesTemplateDirective, { read: IgxExcelStyleLoadingValuesTemplateDirective, static: true }) public excelStyleLoadingValuesTemplateDirective: IgxExcelStyleLoadingValuesTemplateDirective; /** @hidden @internal */ @ViewChild('emptyFilteredGrid', { read: TemplateRef, static: true }) public emptyFilteredGridTemplate: TemplateRef; /** @hidden @internal */ @ViewChild('defaultEmptyGrid', { read: TemplateRef, static: true }) public emptyGridDefaultTemplate: TemplateRef; /** * @hidden @internal */ @ViewChild('defaultLoadingGrid', { read: TemplateRef, static: true }) public loadingGridDefaultTemplate: TemplateRef; /** * @hidden @internal */ @ViewChild('scrollContainer', { read: IgxGridForOfDirective, static: true }) public parentVirtDir: IgxGridForOfDirective; /** * @hidden * @internal */ @ContentChildren(IgxHeadSelectorDirective, { read: TemplateRef, descendants: false }) public headSelectorsTemplates: QueryList>; /** * @hidden * @internal */ @ContentChildren(IgxRowSelectorDirective, { read: TemplateRef, descendants: false }) public rowSelectorsTemplates: QueryList>; /** * @hidden * @internal */ @ContentChildren(IgxRowDragGhostDirective, { read: TemplateRef, descendants: false }) public dragGhostCustomTemplates: QueryList>; /** * Gets the custom template, if any, used for row drag ghost. */ @Input() public get dragGhostCustomTemplate() { return this._dragGhostCustomTemplate || this.dragGhostCustomTemplates?.first; } /** * Sets a custom template for the row drag ghost. *```html * * menu * * ``` * ```typescript * @ViewChild("'template'", {read: TemplateRef }) * public template: TemplateRef; * this.grid.dragGhostCustomTemplate = this.template; * ``` */ public set dragGhostCustomTemplate(template: TemplateRef) { this._dragGhostCustomTemplate = template; } /** * @hidden @internal */ @ViewChild('verticalScrollContainer', { read: IgxGridForOfDirective, static: true }) public verticalScrollContainer: IgxGridForOfDirective; /** * @hidden @internal */ @ViewChild('verticalScrollHolder', { read: IgxGridForOfDirective, static: true }) public verticalScroll: IgxGridForOfDirective; /** * @hidden @internal */ @ViewChild('scr', { read: ElementRef, static: true }) public scr: ElementRef; /** @hidden @internal */ @ViewChild('headSelectorBaseTemplate', { read: TemplateRef, static: true }) public headerSelectorBaseTemplate: TemplateRef; /** * @hidden @internal */ @ViewChild('footer', { read: ElementRef }) public footer: ElementRef; /** @hidden @internal */ public get headerContainer() { return this.theadRow?.headerForOf; } /** @hidden @internal */ public get headerSelectorContainer() { return this.theadRow?.headerSelectorContainer; } /** @hidden @internal */ public get headerDragContainer() { return this.theadRow?.headerDragContainer; } /** @hidden @internal */ public get headerGroupContainer() { return this.theadRow?.headerGroupContainer; } /** @hidden @internal */ public get filteringRow(): IgxGridFilteringRowComponent { return this.theadRow?.filterRow; } /** @hidden @internal */ @ViewChild(IgxGridHeaderRowComponent, { static: true }) public theadRow: IgxGridHeaderRowComponent; /** @hidden @internal */ @ViewChild(IgxGridGroupByAreaComponent) public groupArea: IgxGridGroupByAreaComponent; /** * @hidden @internal */ @ViewChild('tbody', { static: true }) public tbody: ElementRef; /** * @hidden @internal */ @ViewChild('pinContainer', { read: ElementRef }) public pinContainer: ElementRef; /** * @hidden @internal */ @ViewChild('tfoot', { static: true }) public tfoot: ElementRef; /** * @hidden @internal */ @ViewChild('igxRowEditingOverlayOutlet', { read: IgxOverlayOutletDirective, static: true }) public rowEditingOutletDirective: IgxOverlayOutletDirective; /** * @hidden @internal */ @ViewChildren(IgxTemplateOutletDirective, { read: IgxTemplateOutletDirective }) public tmpOutlets: QueryList = new QueryList(); /** * @hidden * @internal */ @ViewChild('dragIndicatorIconBase', { read: TemplateRef, static: true }) public dragIndicatorIconBase: TemplateRef; /** * @hidden @internal */ @ContentChildren(IgxRowEditTemplateDirective, { descendants: false, read: TemplateRef }) public rowEditCustomDirectives: QueryList>; /** * @hidden @internal */ @ContentChildren(IgxRowEditTextDirective, { descendants: false, read: TemplateRef }) public rowEditTextDirectives: QueryList>; /** * Gets the row edit text template. */ @Input() public get rowEditTextTemplate(): TemplateRef { return this._rowEditTextTemplate || this.rowEditTextDirectives?.first; } /** * Sets the row edit text template. *```html * * Changes: {{rowChangesCount}} * * ``` *```typescript * @ViewChild('template', {read: TemplateRef }) * public template: TemplateRef; * this.grid.rowEditTextTemplate = this.template; * ``` */ public set rowEditTextTemplate(template: TemplateRef) { this._rowEditTextTemplate = template; } /** * @hidden @internal */ @ContentChild(IgxRowAddTextDirective, { read: TemplateRef }) public rowAddText: TemplateRef; /** * Gets the row add text template. */ @Input() public get rowAddTextTemplate(): TemplateRef { return this._rowAddTextTemplate || this.rowAddText; } /** * Sets the row add text template. *```html * * Adding Row * * ``` *```typescript * @ViewChild('template', {read: TemplateRef }) * public template: TemplateRef; * this.grid.rowAddTextTemplate = this.template; * ``` */ public set rowAddTextTemplate(template: TemplateRef) { this._rowAddTextTemplate = template; } /** * @hidden @internal */ @ContentChildren(IgxRowEditActionsDirective, { descendants: false, read: TemplateRef }) public rowEditActionsDirectives: QueryList>; /** * Gets the row edit actions template. */ @Input() public get rowEditActionsTemplate(): TemplateRef { return this._rowEditActionsTemplate || this.rowEditActionsDirectives?.first; } /** * Sets the row edit actions template. *```html * * * * * ``` *```typescript * @ViewChild('template', {read: TemplateRef }) * public template: TemplateRef; * this.grid.rowEditActionsTemplate = this.template; * ``` */ public set rowEditActionsTemplate(template: TemplateRef) { this._rowEditActionsTemplate = template; } /** * The custom template, if any, that should be used when rendering a row expand indicator. */ @ContentChild(IgxRowExpandedIndicatorDirective, { read: TemplateRef }) protected rowExpandedIndicatorDirectiveTemplate: TemplateRef = null; /** * Gets the row expand indicator template. */ @Input() public get rowExpandedIndicatorTemplate(): TemplateRef { return this._rowExpandedIndicatorTemplate || this.rowExpandedIndicatorDirectiveTemplate; } /** * Sets the row expand indicator template. *```html * * remove * * ``` *```typescript * @ViewChild('template', {read: TemplateRef }) * public template: TemplateRef; * this.grid.rowExpandedIndicatorTemplate = this.template; * ``` */ public set rowExpandedIndicatorTemplate(template: TemplateRef) { this._rowExpandedIndicatorTemplate = template; } /** * The custom template, if any, that should be used when rendering a row collapse indicator. */ @ContentChild(IgxRowCollapsedIndicatorDirective, { read: TemplateRef }) protected rowCollapsedIndicatorDirectiveTemplate: TemplateRef = null; /** * Gets the row collapse indicator template. */ @Input() public get rowCollapsedIndicatorTemplate(): TemplateRef { return this._rowCollapsedIndicatorTemplate || this.rowCollapsedIndicatorDirectiveTemplate; } /** * Sets the row collapse indicator template. *```html * * add * * ``` *```typescript * @ViewChild('template', {read: TemplateRef }) * public template: TemplateRef; * this.grid.rowCollapsedIndicatorTemplate = this.template; * ``` */ public set rowCollapsedIndicatorTemplate(template: TemplateRef) { this._rowCollapsedIndicatorTemplate = template; } /** * The custom template, if any, that should be used when rendering a header expand indicator. */ @ContentChild(IgxHeaderExpandedIndicatorDirective, { read: TemplateRef }) protected headerExpandedIndicatorDirectiveTemplate: TemplateRef = null; /** * Gets the header expand indicator template. */ @Input() public get headerExpandedIndicatorTemplate(): TemplateRef { return this._headerExpandIndicatorTemplate || this.headerExpandedIndicatorDirectiveTemplate; } /** * Sets the header expand indicator template. *```html * * remove * * ``` *```typescript * @ViewChild('template', {read: TemplateRef }) * public template: TemplateRef; * this.grid.headerExpandedIndicatorTemplate = this.template; * ``` */ public set headerExpandedIndicatorTemplate(template: TemplateRef) { this._headerExpandIndicatorTemplate = template; } /** * The custom template, if any, that should be used when rendering a header collapse indicator. */ @ContentChild(IgxHeaderCollapsedIndicatorDirective, { read: TemplateRef }) protected headerCollapsedIndicatorDirectiveTemplate: TemplateRef = null; /** * Gets the row collapse indicator template. */ @Input() public get headerCollapsedIndicatorTemplate(): TemplateRef { return this._headerCollapseIndicatorTemplate || this.headerCollapsedIndicatorDirectiveTemplate; } /** * Sets the row collapse indicator template. *```html * * add * * ``` *```typescript * @ViewChild('template', {read: TemplateRef }) * public template: TemplateRef; * this.grid.headerCollapsedIndicatorTemplate = this.template; * ``` */ public set headerCollapsedIndicatorTemplate(template: TemplateRef) { this._headerCollapseIndicatorTemplate = template; } /** @hidden @internal */ @ContentChild(IgxExcelStyleHeaderIconDirective, { read: TemplateRef }) public excelStyleHeaderIconDirectiveTemplate: TemplateRef = null; /** * Gets the excel style header icon. */ @Input() public get excelStyleHeaderIconTemplate(): TemplateRef { return this._excelStyleHeaderIconTemplate || this.excelStyleHeaderIconDirectiveTemplate; } /** * Sets the excel style header icon. *```html * * filter_alt * * ``` *```typescript * @ViewChild('template', {read: TemplateRef }) * public template: TemplateRef; * this.grid.excelStyleHeaderIconTemplate = this.template; * ``` */ public set excelStyleHeaderIconTemplate(template: TemplateRef) { this._excelStyleHeaderIconTemplate = template; } /** * @hidden * @internal */ @ContentChild(IgxSortAscendingHeaderIconDirective, { read: TemplateRef }) public sortAscendingHeaderIconDirectiveTemplate: TemplateRef = null; /** * The custom template, if any, that should be used when rendering a header sorting indicator when columns are sorted in asc order. */ @Input() public get sortAscendingHeaderIconTemplate(): TemplateRef { return this._sortAscendingHeaderIconTemplate; } /** * Sets a custom template that should be used when rendering a header sorting indicator when columns are sorted in asc order. *```html * * expand_less * * ``` * ```typescript * @ViewChild("'template'", {read: TemplateRef }) * public template: TemplateRef; * this.grid.sortAscendingHeaderIconTemplate = this.template; * ``` */ public set sortAscendingHeaderIconTemplate(template: TemplateRef) { this._sortAscendingHeaderIconTemplate = template; } /** @hidden @internal */ @ContentChild(IgxSortDescendingHeaderIconDirective, { read: TemplateRef }) public sortDescendingHeaderIconDirectiveTemplate: TemplateRef = null; /** * The custom template, if any, that should be used when rendering a header sorting indicator when columns are sorted in desc order. */ @Input() public get sortDescendingHeaderIconTemplate() { return this._sortDescendingHeaderIconTemplate; } /** * Sets a custom template that should be used when rendering a header sorting indicator when columns are sorted in desc order. *```html * * expand_more * * ``` * ```typescript * @ViewChild("'template'", {read: TemplateRef }) * public template: TemplateRef; * this.grid.sortDescendingHeaderIconTemplate = this.template; * ``` */ public set sortDescendingHeaderIconTemplate(template: TemplateRef) { this._sortDescendingHeaderIconTemplate = template; } /** * @hidden * @internal */ @ContentChild(IgxSortHeaderIconDirective, { read: TemplateRef }) public sortHeaderIconDirectiveTemplate: TemplateRef = null; /** * Gets custom template, if any, that should be used when rendering a header sorting indicator when columns are not sorted. */ @Input() public get sortHeaderIconTemplate(): TemplateRef { return this._sortHeaderIconTemplate; } /** * Sets a custom template that should be used when rendering a header sorting indicator when columns are not sorted. *```html * * unfold_more * * ``` * ```typescript * @ViewChild("'template'", {read: TemplateRef }) * public template: TemplateRef; * this.grid.sortHeaderIconTemplate = this.template; * ``` */ public set sortHeaderIconTemplate(template: TemplateRef) { this._sortHeaderIconTemplate = template; } /** * @hidden * @internal */ @ContentChildren(IgxDragIndicatorIconDirective, { read: TemplateRef, descendants: false }) public dragIndicatorIconTemplates: QueryList>; /** * @hidden @internal */ @ViewChildren(IgxRowEditTabStopDirective) public rowEditTabsDEFAULT: QueryList; /** * @hidden @internal */ @ContentChildren(IgxRowEditTabStopDirective, { descendants: true }) public rowEditTabsCUSTOM: QueryList; /** * @hidden @internal */ @ViewChild('rowEditingOverlay', { read: IgxToggleDirective }) public rowEditingOverlay: IgxToggleDirective; /** * @hidden @internal */ @HostBinding('attr.tabindex') public tabindex = 0; /** * @hidden @internal */ @HostBinding('attr.role') public hostRole = 'grid'; /** @hidden @internal */ @ContentChildren(IgxGridToolbarComponent) public toolbar: QueryList; /** @hidden @internal */ @ContentChildren(IgxPaginatorComponent) protected paginationComponents: QueryList; /** * @hidden @internal */ @ViewChild('igxFilteringOverlayOutlet', { read: IgxOverlayOutletDirective, static: true }) protected _outletDirective: IgxOverlayOutletDirective; /** * @hidden @internal */ @ViewChild('defaultExpandedTemplate', { read: TemplateRef, static: true }) protected defaultExpandedTemplate: TemplateRef; /** * @hidden @internal */ @ViewChild('defaultCollapsedTemplate', { read: TemplateRef, static: true }) protected defaultCollapsedTemplate: TemplateRef; /** * @hidden @internal */ @ViewChild('defaultESFHeaderIcon', { read: TemplateRef, static: true }) protected defaultESFHeaderIconTemplate: TemplateRef; @ViewChildren('summaryRow', { read: IgxSummaryRowComponent }) protected _summaryRowList: QueryList; @ViewChildren('row') private _rowList: QueryList; @ViewChildren('pinnedRow') private _pinnedRowList: QueryList; /** * @hidden @internal */ @ViewChild('defaultRowEditTemplate', { read: TemplateRef, static: true }) private defaultRowEditTemplate: TemplateRef; @ViewChildren(IgxRowDirective, { read: IgxRowDirective }) private _dataRowList: QueryList; @HostBinding('class') private get hostClass(): string { return this.getComponentDensityClass('igx-grid'); } /** * Gets/Sets the resource strings. * * @remarks * By default it uses EN resources. */ @Input() public set resourceStrings(value: IGridResourceStrings) { this._resourceStrings = Object.assign({}, this._resourceStrings, value); } public get resourceStrings(): IGridResourceStrings { if (!this._resourceStrings) { this._resourceStrings = CurrentResourceStrings.GridResStrings; } return this._resourceStrings; } /** * Gets/Sets the filtering logic of the `IgxGridComponent`. * * @remarks * The default is AND. * @example * ```html * * ``` */ @WatchChanges() @Input() public get filteringLogic() { return this._filteringExpressionsTree.operator; } public set filteringLogic(value: FilteringLogic) { this._filteringExpressionsTree.operator = value; } /** * Gets/Sets the filtering state. * * @example * ```html * * ``` * @remarks * Supports two-way binding. */ @WatchChanges() @Input() public get filteringExpressionsTree() { return this._filteringExpressionsTree; } public set filteringExpressionsTree(value) { if (value && value instanceof FilteringExpressionsTree) { const val = (value as FilteringExpressionsTree); for (let index = 0; index < val.filteringOperands.length; index++) { if (!(val.filteringOperands[index] instanceof FilteringExpressionsTree)) { const newExpressionsTree = new FilteringExpressionsTree(FilteringLogic.And, val.filteringOperands[index].fieldName); newExpressionsTree.filteringOperands.push(val.filteringOperands[index] as IFilteringExpression); val.filteringOperands[index] = newExpressionsTree; } } value.type = FilteringExpressionsTreeType.Regular; this._filteringExpressionsTree = value; this.filteringPipeTrigger++; this.filteringExpressionsTreeChange.emit(this._filteringExpressionsTree); if (this.filteringService.isFilteringExpressionsTreeEmpty(this._filteringExpressionsTree) && this.filteringService.isFilteringExpressionsTreeEmpty(this._advancedFilteringExpressionsTree)) { this._filteredData = null; } this.filteringService.refreshExpressions(); this.selectionService.clearHeaderCBState(); this.summaryService.clearSummaryCache(); this.notifyChanges(); } } /** * Gets/Sets the advanced filtering state. * * @example * ```typescript * let advancedFilteringExpressionsTree = this.grid.advancedFilteringExpressionsTree; * this.grid.advancedFilteringExpressionsTree = logic; * ``` */ @WatchChanges() @Input() public get advancedFilteringExpressionsTree() { return this._advancedFilteringExpressionsTree; } public set advancedFilteringExpressionsTree(value) { if (value && value instanceof FilteringExpressionsTree) { value.type = FilteringExpressionsTreeType.Advanced; this._advancedFilteringExpressionsTree = value; this.filteringPipeTrigger++; } else { this._advancedFilteringExpressionsTree = null; } this.advancedFilteringExpressionsTreeChange.emit(this._advancedFilteringExpressionsTree); if (this.filteringService.isFilteringExpressionsTreeEmpty(this._filteringExpressionsTree) && this.filteringService.isFilteringExpressionsTreeEmpty(this._advancedFilteringExpressionsTree)) { this._filteredData = null; } this.selectionService.clearHeaderCBState(); this.summaryService.clearSummaryCache(); this.notifyChanges(); // Wait for the change detection to update filtered data through the pipes and then emit the event. requestAnimationFrame(() => this.filteringDone.emit(this._advancedFilteringExpressionsTree)); } /** * Gets/Sets the locale. * * @remarks * If not set, returns browser's language. */ @Input() public get locale(): string { return this._locale; } public set locale(value: string) { if (value !== this._locale) { this._locale = value; this._currencyPositionLeft = undefined; this.summaryService.clearSummaryCache(); this.pipeTrigger++; this.notifyChanges(); this.localeChange.next(); } } @Input() public get pagingMode() { return this._pagingMode; } public set pagingMode(val: GridPagingMode) { this._pagingMode = val; this.pipeTrigger++; this.notifyChanges(true); } /** @hidden @internal */ public get page(): number { return this.paginator?.page || 0; } public set page(val: number) { if (this.paginator) { this.paginator.page = val; } } /** @hidden @internal */ public get perPage(): number { return this.paginator?.perPage || DEFAULT_ITEMS_PER_PAGE; } public set perPage(val: number) { if (this.paginator) { this.paginator.perPage = val; } } /** * Gets/Sets if the row selectors are hidden. * * @remarks * By default row selectors are shown */ @WatchChanges() @Input() public get hideRowSelectors() { return this._hideRowSelectors; } public set hideRowSelectors(value: boolean) { this._hideRowSelectors = value; this.notifyChanges(true); } /** * Gets/Sets whether rows can be moved. * * @example * ```html * * ``` */ @Input() public get rowDraggable(): boolean { return this._rowDrag && this.hasVisibleColumns; } public set rowDraggable(val: boolean) { this._rowDrag = val; this.notifyChanges(true); } /** * Gets/Sets the trigger for validators used when editing the grid. * * @example * ```html * * ``` */ @Input() public validationTrigger: GridValidationTrigger = 'change'; /** * @hidden * @internal */ public rowDragging = false; /** @hidden @internal */ public dragRowID = null; /** * Gets/Sets whether the rows are editable. * * @remarks * By default it is set to false. * @example * ```html * * ``` */ @WatchChanges() @Input() public get rowEditable(): boolean { return this._rowEditable; } public set rowEditable(val: boolean) { if (!this._init) { this.refreshGridState(); } this._rowEditable = val; this.notifyChanges(); } /** * Gets/Sets the height. * * @example * ```html * * ``` */ @WatchChanges() @HostBinding('style.height') @Input() public get height(): string | null { return this._height; } public set height(value: string | null) { if (this._height !== value) { this._height = value; this.nativeElement.style.height = value; this.notifyChanges(true); } } /** * @hidden @internal */ @HostBinding('style.width') public get hostWidth() { return this._width || this._hostWidth; } /** * Gets/Sets the width of the grid. * * @example * ```typescript * let gridWidth = this.grid.width; * ``` */ @WatchChanges() @Input() public get width(): string | null { return this._width; } public set width(value: string | null) { if (this._width !== value) { this._width = value; this.nativeElement.style.width = value; this.notifyChanges(true); } } /** @hidden @internal */ public get headerWidth() { return parseInt(this.width, 10) - 17; } /** * Gets/Sets the row height. * * @example * ```html * * ``` */ @WatchChanges() @Input() public get rowHeight(): number { return this._rowHeight ? this._rowHeight : this.defaultRowHeight; } public set rowHeight(value: number | string) { if (typeof value !== 'number') { value = parseInt(value, 10); } this._rowHeight = value; } /** * Gets/Sets the default width of the columns. * * @example * ```html * * ``` */ @WatchChanges() @Input() public get columnWidth(): string { return this._columnWidth; } public set columnWidth(value: string) { this._columnWidth = value; this.columnWidthSetByUser = true; this.notifyChanges(true); } /** * Get/Sets the message displayed when there are no records. * * @example * ```html * * ``` */ @Input() public set emptyGridMessage(value: string) { this._emptyGridMessage = value; } public get emptyGridMessage(): string { return this._emptyGridMessage || this.resourceStrings.igx_grid_emptyGrid_message; } /** * Gets/Sets whether the grid is going to show a loading indicator. * * @example * ```html * * ``` */ @WatchChanges() @Input() public set isLoading(value: boolean) { if (this._isLoading !== value) { this._isLoading = value; if (this.data) { this.evaluateLoadingState(); } } Promise.resolve().then(() => { // wait for the current detection cycle to end before triggering a new one. this.notifyChanges(); }); } public get isLoading(): boolean { return this._isLoading; } /** * Gets/Sets whether the columns should be auto-generated once again after the initialization of the grid * * @remarks * This will allow to bind the grid to remote data and having auto-generated columns at the same time. * Note that after generating the columns, this property would be disabled to avoid re-creating * columns each time a new data is assigned. * @example * ```typescript * this.grid.shouldGenerate = true; * ``` */ public shouldGenerate: boolean; /** * Gets/Sets the message displayed when there are no records and the grid is filtered. * * @example * ```html * * ``` */ @Input() public set emptyFilteredGridMessage(value: string) { this._emptyFilteredGridMessage = value; } public get emptyFilteredGridMessage(): string { return this._emptyFilteredGridMessage || this.resourceStrings.igx_grid_emptyFilteredGrid_message; } /** * Gets/Sets the initial pinning configuration. * * @remarks * Allows to apply pinning the columns to the start or the end. * Note that pinning to both sides at a time is not allowed. * @example * ```html * * ``` */ @Input() public get pinning() { return this._pinning; } public set pinning(value) { if (value !== this._pinning) { this.resetCaches(); } this._pinning = value; } /** * Gets/Sets if the filtering is enabled. * * @example * ```html * * ``` */ @Input() public get allowFiltering() { return this._allowFiltering; } public set allowFiltering(value) { if (this._allowFiltering !== value) { this._allowFiltering = value; this.filteringService.registerSVGIcons(); if (!this._init) { this.calcGridHeadRow(); } this.filteringService.isFilterRowVisible = false; this.filteringService.filteredColumn = null; this.notifyChanges(true); } } /** * Gets/Sets a value indicating whether the advanced filtering is enabled. * * @example * ```html * * ``` */ @Input() public get allowAdvancedFiltering() { return this._allowAdvancedFiltering; } public set allowAdvancedFiltering(value) { if (this._allowAdvancedFiltering !== value) { this._allowAdvancedFiltering = value; this.filteringService.registerSVGIcons(); if (!this._init) { this.notifyChanges(true); } } } /** * Gets/Sets the filter mode. * * @example * ```html * * ``` * @remarks * By default it's set to FilterMode.quickFilter. */ @Input() public get filterMode() { return this._filterMode; } public set filterMode(value: FilterMode) { switch (value) { case FilterMode.excelStyleFilter: case FilterMode.quickFilter: this._filterMode = value; break; default: break; } if (this.filteringService.isFilterRowVisible) { this.filteringRow.close(); } this.notifyChanges(true); } /** * Gets/Sets the summary position. * * @example * ```html * * ``` * @remarks * By default it is bottom. */ @Input() public get summaryPosition() { return this._summaryPosition; } public set summaryPosition(value: GridSummaryPosition) { this._summaryPosition = value; this.notifyChanges(); } /** * Gets/Sets the summary calculation mode. * * @example * ```html * * ``` * @remarks * By default it is rootAndChildLevels which means the summaries are calculated for the root level and each child level. */ @Input() public get summaryCalculationMode() { return this._summaryCalculationMode; } public set summaryCalculationMode(value: GridSummaryCalculationMode) { this._summaryCalculationMode = value; if (!this._init) { this.crudService.endEdit(false); this.summaryService.resetSummaryHeight(); this.notifyChanges(true); } } /** * Controls whether the summary row is visible when groupBy/parent row is collapsed. * * @example * ```html * * ``` * @remarks * By default showSummaryOnCollapse is set to 'false' which means that the summary row is not visible * when the groupBy/parent row is collapsed. */ @Input() public get showSummaryOnCollapse() { return this._showSummaryOnCollapse; } public set showSummaryOnCollapse(value: boolean) { this._showSummaryOnCollapse = value; this.notifyChanges(); } /** * Gets/Sets the filtering strategy of the grid. * * @example * ```html * * ``` */ @Input() public get filterStrategy(): IFilteringStrategy { return this._filterStrategy; } public set filterStrategy(classRef: IFilteringStrategy) { this._filterStrategy = classRef; } /** * Gets/Sets the sorting strategy of the grid. * * @example * ```html * * ``` */ @Input() public get sortStrategy(): IGridSortingStrategy { return this._sortingStrategy; } public set sortStrategy(value: IGridSortingStrategy) { this._sortingStrategy = value; } /** * Gets/Sets the sorting options - single or multiple sorting. * Accepts an `ISortingOptions` object with any of the `mode` properties. * * @example * ```typescript * const _sortingOptions: ISortingOptions = { * mode: 'single' * } * ```html * * ``` */ @Input() public set sortingOptions(value: ISortingOptions) { this.clearSort(); this._sortingOptions = Object.assign(this._sortingOptions, value); } public get sortingOptions() { return this._sortingOptions; } /** * Gets/Sets the current selection state. * * @remarks * Represents the selected rows' IDs (primary key or rowData) * @example * ```html * * ``` */ @Input() public set selectedRows(rowIDs: any[]) { this.selectRows(rowIDs || [], true); } public get selectedRows(): any[] { return this.selectionService.getSelectedRows(); } /** @hidden @internal */ public get headerGroupsList(): IgxGridHeaderGroupComponent[] { return this.theadRow.groups; } /** @hidden @internal */ public get headerCellList(): IgxGridHeaderComponent[] { return this.headerGroupsList.map(headerGroup => headerGroup.header).filter(header => header); } /** @hidden @internal */ public get filterCellList(): IgxGridFilteringCellComponent[] { return this.headerGroupsList.map(group => group.filter).filter(cell => cell); } /** * @hidden @internal */ public get summariesRowList() { const res = new QueryList(); if (!this._summaryRowList) { return res; } const sumList = this._summaryRowList.filter((item) => item.element.nativeElement.parentElement !== null); res.reset(sumList); return res; } /** * A list of `IgxGridRowComponent`. * * @example * ```typescript * const rowList = this.grid.rowList; * ``` */ public get rowList() { const res = new QueryList(); if (!this._rowList) { return res; } const rList = this._rowList .filter((item) => item.element.nativeElement.parentElement !== null) .sort((a, b) => a.index - b.index); res.reset(rList); return res; } /** * A list of currently rendered `IgxGridRowComponent`'s. * * @example * ```typescript * const dataList = this.grid.dataRowList; * ``` */ public get dataRowList(): QueryList { const res = new QueryList(); if (!this._dataRowList) { return res; } const rList = this._dataRowList.filter(item => item.element.nativeElement.parentElement !== null).sort((a, b) => a.index - b.index); res.reset(rList); return res; } /** * Gets the header row selector template. */ @Input() public get headSelectorTemplate(): TemplateRef { return this._headSelectorTemplate || this.headSelectorsTemplates?.first; } /** * Sets the header row selector template. * ```html * * {{ headContext.selectedCount }} / {{ headContext.totalCount }} * * ``` * ```typescript * @ViewChild("'template'", {read: TemplateRef }) * public template: TemplateRef; * this.grid.headSelectorTemplate = this.template; * ``` */ public set headSelectorTemplate(template: TemplateRef) { this._headSelectorTemplate = template; } /** * @hidden * @internal */ public get isPinningToStart() { return this.pinning.columns !== ColumnPinningPosition.End; } /** * @hidden * @internal */ public get isRowPinningToTop() { return this.pinning.rows !== RowPinningPosition.Bottom; } /** * Gets the row selector template. */ @Input() public get rowSelectorTemplate(): TemplateRef { return this._rowSelectorTemplate || this.rowSelectorsTemplates?.first; } /** * Sets a custom template for the row selectors. * ```html * * * * ``` * ```typescript * @ViewChild("'template'", {read: TemplateRef }) * public template: TemplateRef; * this.grid.rowSelectorTemplate = this.template; * ``` */ public set rowSelectorTemplate(template: TemplateRef) { this._rowSelectorTemplate = template; } /** * @hidden @internal */ public get rowOutletDirective() { return this.rowEditingOutletDirective; } /** * @hidden @internal */ public get parentRowOutletDirective() { return this.outlet; } /** * @hidden @internal */ public get rowEditCustom(): TemplateRef { if (this.rowEditCustomDirectives && this.rowEditCustomDirectives.first) { return this.rowEditCustomDirectives.first; } return null; } /** /** * @hidden @internal */ public get rowEditContainer(): TemplateRef { return this.rowEditCustom ? this.rowEditCustom : this.defaultRowEditTemplate; } /** * The custom template, if any, that should be used when rendering the row drag indicator icon */ @Input() public get dragIndicatorIconTemplate(): TemplateRef { return this._customDragIndicatorIconTemplate || this.dragIndicatorIconTemplates?.first; } /** * Sets a custom template that should be used when rendering the row drag indicator icon. *```html * * expand_less * * ``` * ```typescript * @ViewChild("'template'", {read: TemplateRef }) * public template: TemplateRef; * this.grid.dragIndicatorIconTemplate = this.template; * ``` */ public set dragIndicatorIconTemplate(val: TemplateRef) { this._customDragIndicatorIconTemplate = val; } /** * @hidden @internal */ public get firstEditableColumnIndex(): number { const index = this.visibleColumns.filter(col => col.editable) .map(c => c.visibleIndex).sort((a, b) => a - b); return index.length ? index[0] : null; } /** * @hidden @internal */ public get lastEditableColumnIndex(): number { const index = this.visibleColumns.filter(col => col.editable) .map(c => c.visibleIndex).sort((a, b) => a > b ? -1 : 1); return index.length ? index[0] : null; } /** * @hidden @internal * TODO: Nav service logic doesn't handle 0 results from this querylist */ public get rowEditTabs(): QueryList { return this.rowEditTabsCUSTOM.length ? this.rowEditTabsCUSTOM : this.rowEditTabsDEFAULT; } /** @hidden @internal */ public get activeDescendant() { const activeElem = this.navigation.activeNode; if (!activeElem || !Object.keys(activeElem).length) { return this.id; } return activeElem.row < 0 ? `${this.id}_${activeElem.row}_${activeElem.mchCache.level}_${activeElem.column}` : `${this.id}_${activeElem.row}_${activeElem.column}`; } /** @hidden @internal */ public get bannerClass(): string { const position = this.rowEditPositioningStrategy.isTop ? 'igx-banner__border-top' : 'igx-banner__border-bottom'; return `${this.getComponentDensityClass('igx-banner')} ${position}`; } /** * Gets/Sets the sorting state. * * @remarks * Supports two-way data binding. * @example * ```html * * ``` */ @WatchChanges() @Input() public get sortingExpressions(): ISortingExpression[] { return this._sortingExpressions; } public set sortingExpressions(value: ISortingExpression[]) { this._sortingExpressions = cloneArray(value); this.sortingExpressionsChange.emit(this._sortingExpressions); this.notifyChanges(); } /** * @hidden @internal */ public get maxLevelHeaderDepth() { if (this._maxLevelHeaderDepth === null) { this._maxLevelHeaderDepth = this.hasColumnLayouts ? this._columns.reduce((acc, col) => Math.max(acc, col.rowStart), 0) : this._columns.reduce((acc, col) => Math.max(acc, col.level), 0); } return this._maxLevelHeaderDepth; } /** * Gets the number of hidden columns. * * @example * ```typescript * const hiddenCol = this.grid.hiddenColumnsCount; * `` */ public get hiddenColumnsCount() { return this._columns.filter((col) => col.columnGroup === false && col.hidden === true).length; } /** * Gets the number of pinned columns. */ public get pinnedColumnsCount() { return this.pinnedColumns.filter(col => !col.columnLayout).length; } /** * Gets/Sets whether the grid has batch editing enabled. * When batch editing is enabled, changes are not made directly to the underlying data. * Instead, they are stored as transactions, which can later be committed w/ the `commit` method. * * @example * ```html * * * ``` */ @Input() public get batchEditing(): boolean { return this._batchEditing; } public set batchEditing(val: boolean) { if (val !== this._batchEditing) { delete this._transactions; this._batchEditing = val; this.switchTransactionService(val); this.subscribeToTransactions(); } } /** * Get transactions service for the grid. */ public get transactions(): TransactionService { if (this._diTransactions && !this.batchEditing) { return this._diTransactions; } return this._transactions; } /** * @hidden @internal */ public get currentRowState(): any { return this._currentRowState; } /** * @hidden @internal */ public get currencyPositionLeft(): boolean { if (this._currencyPositionLeft !== undefined) { return this._currencyPositionLeft; } const format = getLocaleNumberFormat(this.locale, NumberFormatStyle.Currency); const formatParts = format.split(','); const i = formatParts.indexOf(formatParts.find(c => c.includes('¤'))); return this._currencyPositionLeft = i < 1; } /** * Gets/Sets cell selection mode. * * @remarks * By default the cell selection mode is multiple * @param selectionMode: GridSelectionMode */ @WatchChanges() @Input() public get cellSelection() { return this._cellSelectionMode; } public set cellSelection(selectionMode: GridSelectionMode) { this._cellSelectionMode = selectionMode; // if (this.gridAPI.grid) { this.selectionService.clear(true); this.notifyChanges(); // } } /** * Gets/Sets row selection mode * * @remarks * By default the row selection mode is 'none' * Note that in IgxGrid and IgxHierarchicalGrid 'multipleCascade' behaves like 'multiple' */ @WatchChanges() @Input() public get rowSelection() { return this._rowSelectionMode; } public set rowSelection(selectionMode: GridSelectionMode) { this._rowSelectionMode = selectionMode; if (!this._init) { this.selectionService.clearAllSelectedRows(); this.notifyChanges(true); } } /** * Gets/Sets column selection mode * * @remarks * By default the row selection mode is none * @param selectionMode: GridSelectionMode */ @WatchChanges() @Input() public get columnSelection() { return this._columnSelectionMode; } public set columnSelection(selectionMode: GridSelectionMode) { this._columnSelectionMode = selectionMode; // if (this.gridAPI.grid) { this.selectionService.clearAllSelectedColumns(); this.notifyChanges(true); // } } /** * @hidden @internal */ public set pagingState(value) { this._pagingState = value; if (this.paginator && !this._init) { this.paginator.totalRecords = value.metadata.countRecords; } } public get pagingState() { return this._pagingState; } /** * @hidden @internal */ public rowEditMessage; /** * @hidden @internal */ public snackbarActionText = this.resourceStrings.igx_grid_snackbar_addrow_actiontext; /** * @hidden @internal */ public calcWidth: number; /** * @hidden @internal */ public calcHeight = 0; /** * @hidden @internal */ public tfootHeight: number; /** * @hidden @internal */ public disableTransitions = false; /** * @hidden @internal */ public lastSearchInfo: ISearchInfo = { searchText: '', caseSensitive: false, exactMatch: false, activeMatchIndex: 0, matchInfoCache: [] }; /** * @hidden @internal */ public columnWidthSetByUser = false; /** * @hidden @internal */ public pinnedRecords: any[]; /** * @hidden @internal */ public unpinnedRecords: any[]; /** * @hidden @internal */ public rendered$ = this.rendered.asObservable().pipe(shareReplay({ bufferSize: 1, refCount: true })); /** @hidden @internal */ public resizeNotify = new Subject(); /** @hidden @internal */ public rowAddedNotifier = new Subject(); /** @hidden @internal */ public rowDeletedNotifier = new Subject(); /** @hidden @internal */ public pipeTriggerNotifier = new Subject(); /** @hidden @internal */ public _filteredSortedPinnedData: any[]; /** @hidden @internal */ public _filteredSortedUnpinnedData: any[]; /** @hidden @internal */ public _filteredPinnedData: any[]; /** * @hidden */ public _filteredUnpinnedData; /** * @hidden @internal */ public _destroyed = false; /** * @hidden @internal */ public _totalRecords = -1; /** * @hidden @internal */ public columnsWithNoSetWidths = null; /** * @hidden @internal */ public pipeTrigger = 0; /** * @hidden @internal */ public filteringPipeTrigger = 0; /** * @hidden @internal */ public summaryPipeTrigger = 0; /** * @hidden @internal */ public groupablePipeTrigger = 0; /** * @hidden @internal */ public EMPTY_DATA = []; /** @hidden @internal */ public isPivot = false; /** @hidden @internal */ public _baseFontSize: number; /** * @hidden */ public destroy$ = new Subject(); /** * @hidden */ protected _pagingMode = GridPagingMode.Local; /** * @hidden */ protected _pagingState; /** * @hidden */ protected _hideRowSelectors = false; /** * @hidden */ protected _rowDrag = false; /** * @hidden */ protected _columns: IgxColumnComponent[] = []; /** * @hidden */ protected _pinnedColumns: IgxColumnComponent[] = []; /** * @hidden */ protected _unpinnedColumns: IgxColumnComponent[] = []; /** * @hidden */ protected _filteringExpressionsTree: IFilteringExpressionsTree = new FilteringExpressionsTree(FilteringLogic.And); /** * @hidden */ protected _advancedFilteringExpressionsTree: IFilteringExpressionsTree; /** * @hidden */ protected _sortingExpressions: Array = []; /** * @hidden */ protected _maxLevelHeaderDepth = null; /** * @hidden */ protected _columnHiding = false; /** * @hidden */ protected _columnPinning = false; protected _pinnedRecordIDs = []; /** * @hidden */ protected _hasVisibleColumns; protected _allowFiltering = false; protected _allowAdvancedFiltering = false; protected _filterMode: FilterMode = FilterMode.quickFilter; protected _defaultTargetRecordNumber = 10; protected _expansionStates: Map = new Map(); protected _defaultExpandState = false; protected _headerFeaturesWidth = NaN; protected _init = true; protected _cdrRequestRepaint = false; protected _userOutletDirective: IgxOverlayOutletDirective; protected _transactions: TransactionService; protected _batchEditing = false; protected _sortingOptions: ISortingOptions = { mode: 'multiple' }; protected _filterStrategy: IFilteringStrategy = new FilteringStrategy(); protected _autoGeneratedCols = []; protected _dataView = []; /** @hidden @internal */ public get paginator() { return this.paginationComponents?.first; } /** * @hidden @internal */ public get scrollSize() { return this.verticalScrollContainer.getScrollNativeSize(); } private _rowEditable = false; private _currentRowState: any; private _filteredSortedData = null; private _filteredData = null; private _customDragIndicatorIconTemplate: TemplateRef; private _excelStyleHeaderIconTemplate: TemplateRef; private _rowSelectorTemplate: TemplateRef; private _headSelectorTemplate: TemplateRef; private _rowEditTextTemplate: TemplateRef; private _rowAddTextTemplate: TemplateRef; private _rowEditActionsTemplate: TemplateRef; private _dragGhostCustomTemplate: TemplateRef; private _rowExpandedIndicatorTemplate: TemplateRef; private _rowCollapsedIndicatorTemplate: TemplateRef; private _headerExpandIndicatorTemplate: TemplateRef; private _headerCollapseIndicatorTemplate: TemplateRef; private _cdrRequests = false; private _resourceStrings; private _emptyGridMessage = null; private _emptyFilteredGridMessage = null; private _isLoading = false; private _locale: string; private overlayIDs = []; private _sortingStrategy: IGridSortingStrategy; private _pinning: IPinningConfig = { columns: ColumnPinningPosition.Start }; private _hostWidth; private _advancedFilteringOverlayId: string; private _advancedFilteringPositionSettings: PositionSettings = { verticalDirection: VerticalAlignment.Middle, horizontalDirection: HorizontalAlignment.Center, horizontalStartPoint: HorizontalAlignment.Center, verticalStartPoint: VerticalAlignment.Middle }; private _advancedFilteringOverlaySettings: OverlaySettings = { closeOnOutsideClick: false, modal: false, positionStrategy: new ConnectedPositioningStrategy(this._advancedFilteringPositionSettings), }; private columnListDiffer; private rowListDiffer; private _height: string | null = '100%'; private _width: string | null = '100%'; private _rowHeight: number | undefined; private _horizontalForOfs: Array> = []; private _multiRowLayoutRowSize = 1; // Caches private _totalWidth = NaN; private _pinnedVisible = []; private _unpinnedVisible = []; private _pinnedWidth = NaN; private _unpinnedWidth = NaN; private _visibleColumns = []; private _columnGroups = false; private _columnWidth: string; private _summaryPosition: GridSummaryPosition = GridSummaryPosition.bottom; private _summaryCalculationMode: GridSummaryCalculationMode = GridSummaryCalculationMode.rootAndChildLevels; private _showSummaryOnCollapse = false; private _summaryRowHeight = 0; private _cellSelectionMode: GridSelectionMode = GridSelectionMode.multiple; private _rowSelectionMode: GridSelectionMode = GridSelectionMode.none; private _selectRowOnClick = true; private _columnSelectionMode: GridSelectionMode = GridSelectionMode.none; private lastAddedRowIndex; private _currencyPositionLeft: boolean; private rowEditPositioningStrategy = new RowEditPositionStrategy({ horizontalDirection: HorizontalAlignment.Right, verticalDirection: VerticalAlignment.Bottom, horizontalStartPoint: HorizontalAlignment.Left, verticalStartPoint: VerticalAlignment.Bottom, closeAnimation: null }); private rowEditSettings: OverlaySettings = { scrollStrategy: new AbsoluteScrollStrategy(), modal: false, closeOnOutsideClick: false, outlet: this.rowOutletDirective, positionStrategy: this.rowEditPositioningStrategy }; private transactionChange$ = new Subject(); private _rendered = false; private readonly DRAG_SCROLL_DELTA = 10; private _dataCloneStrategy: IDataCloneStrategy = new DefaultDataCloneStrategy(); private _autoSize = false; private _sortHeaderIconTemplate: TemplateRef = null; private _sortAscendingHeaderIconTemplate: TemplateRef = null; private _sortDescendingHeaderIconTemplate: TemplateRef = null; /** * @hidden @internal */ protected get minColumnWidth() { return MINIMUM_COLUMN_WIDTH; } /** * @hidden @internal */ public abstract id: string; public abstract data: any[] | null; /** * Returns an array of objects containing the filtered data. * * @example * ```typescript * let filteredData = this.grid.filteredData; * ``` */ public get filteredData() { return this._filteredData; } /** * Returns an array containing the filtered sorted data. * * @example * ```typescript * const filteredSortedData = this.grid1.filteredSortedData; * ``` */ public get filteredSortedData(): any[] { return this._filteredSortedData; } /** * @hidden @internal */ public get rowChangesCount() { if (!this.crudService.row) { return 0; } const f = (obj: any) => { let changes = 0; Object.keys(obj).forEach(key => isObject(obj[key]) ? changes += f(obj[key]) : changes++); return changes; }; if (this.transactions.getState(this.crudService.row.id)?.type === TransactionType.ADD) { return this._columns.filter(c => c.field).length; } const rowChanges = this.transactions.getAggregatedValue(this.crudService.row.id, false); return rowChanges ? f(rowChanges) : 0; } /** * @hidden @internal */ public get dataWithAddedInTransactionRows() { const result = cloneArray(this.gridAPI.get_all_data()); if (this.transactions.enabled) { result.push(...this.transactions.getAggregatedChanges(true) .filter(t => t.type === TransactionType.ADD) .map(t => t.newValue)); } if (this.crudService.row && this.crudService.row.getClassName() === IgxAddRow.name) { result.splice(this.crudService.row.index, 0, this.crudService.row.data); } return result; } /** * @hidden @internal */ public get dataLength() { return this.transactions.enabled ? this.dataWithAddedInTransactionRows.length : this.gridAPI.get_all_data().length; } /** * @hidden @internal */ public get template(): TemplateRef { if (this.isLoading && (this.hasZeroResultFilter || this.hasNoData)) { return this.loadingGridTemplate ? this.loadingGridTemplate : this.loadingGridDefaultTemplate; } if (this.hasZeroResultFilter) { return this.emptyGridTemplate ? this.emptyGridTemplate : this.emptyFilteredGridTemplate; } if (this.hasNoData) { return this.emptyGridTemplate ? this.emptyGridTemplate : this.emptyGridDefaultTemplate; } } /** * @hidden @internal */ private get hasZeroResultFilter(): boolean { return this.filteredData && this.filteredData.length === 0; } /** * @hidden @internal */ private get hasNoData(): boolean { return !this.data || this.dataLength === 0; } /** * @hidden @internal */ public get shouldOverlayLoading(): boolean { return this.isLoading && !this.hasNoData && !this.hasZeroResultFilter; } /** * @hidden @internal */ public get isMultiRowSelectionEnabled(): boolean { return this.rowSelection === GridSelectionMode.multiple || this.rowSelection === GridSelectionMode.multipleCascade; } /** * @hidden @internal */ public get isRowSelectable(): boolean { return this.rowSelection !== GridSelectionMode.none; } /** * @hidden @internal */ public get isCellSelectable() { return this.cellSelection !== GridSelectionMode.none; } /** * @hidden @internal */ public get columnInDrag() { return this.gridAPI.cms.column; } constructor( public readonly validation: IgxGridValidationService, /** @hidden @internal */ public readonly selectionService: IgxGridSelectionService, protected colResizingService: IgxColumnResizingService, @Inject(IGX_GRID_SERVICE_BASE) public readonly gridAPI: GridServiceType, protected transactionFactory: IgxFlatTransactionFactory, private elementRef: ElementRef, protected zone: NgZone, /** @hidden @internal */ @Inject(DOCUMENT) public document: any, public readonly cdr: ChangeDetectorRef, protected differs: IterableDiffers, protected viewRef: ViewContainerRef, private appRef: ApplicationRef, protected injector: Injector, protected envInjector: EnvironmentInjector, public navigation: IgxGridNavigationService, /** @hidden @internal */ public filteringService: IgxFilteringService, @Inject(IgxOverlayService) protected overlayService: IgxOverlayService, /** @hidden @internal */ public summaryService: IgxGridSummaryService, @Optional() @Inject(DisplayDensityToken) protected _displayDensityOptions: IDisplayDensityOptions, @Inject(LOCALE_ID) private localeId: string, protected platform: PlatformUtil, @Optional() @Inject(IgxGridTransaction) protected _diTransactions?: TransactionService ) { super(_displayDensityOptions); this.locale = this.locale || this.localeId; this._transactions = this.transactionFactory.create(TRANSACTION_TYPE.None); this._transactions.cloneStrategy = this.dataCloneStrategy; this.cdr.detach(); } /** * @hidden * @internal */ @HostListener('mouseleave') public hideActionStrip() { this.actionStrip?.hide(); } /** * @hidden * @internal */ public get headerFeaturesWidth() { return this._headerFeaturesWidth; } /** * @hidden * @internal */ public isDetailRecord(_rec) { return false; } /** * @hidden * @internal */ public isGroupByRecord(_rec) { return false; } /** * @hidden @internal */ public isGhostRecord(record: any): boolean { return record.ghostRecord !== undefined; } /** * @hidden @internal */ public isAddRowRecord(record: any): boolean { return record.addRow !== undefined; } /** * @hidden * Returns the row index of a row that takes into account the full view data like pinning. */ public getDataViewIndex(rowIndex, pinned) { if (pinned && !this.isRowPinningToTop) { rowIndex = rowIndex + this.unpinnedDataView.length; } else if (!pinned && this.isRowPinningToTop) { rowIndex = rowIndex + this.pinnedDataView.length; } return rowIndex; } /** * @hidden * @internal */ public get hasDetails() { return false; } /** * Returns the state of the grid virtualization. * * @remarks * Includes the start index and how many records are rendered. * @example * ```typescript * const gridVirtState = this.grid1.virtualizationState; * ``` */ public get virtualizationState() { return this.verticalScrollContainer.state; } /** * @hidden * @internal */ public hideOverlays() { this.overlayIDs.forEach(overlayID => { const overlay = this.overlayService.getOverlayById(overlayID); if (overlay?.visible && !overlay.closeAnimationPlayer?.hasStarted()) { this.overlayService.hide(overlayID); this.nativeElement.focus(); } }); } /** * Returns whether the record is pinned or not. * * @param rowIndex Index of the record in the `dataView` collection. * * @hidden * @internal */ public isRecordPinnedByViewIndex(rowIndex: number) { return this.hasPinnedRecords && (this.isRowPinningToTop && rowIndex < this.pinnedDataView.length) || (!this.isRowPinningToTop && rowIndex >= this.unpinnedDataView.length); } /** * Returns whether the record is pinned or not. * * @param rowIndex Index of the record in the `filteredSortedData` collection. */ public isRecordPinnedByIndex(rowIndex: number) { return this.hasPinnedRecords && (this.isRowPinningToTop && rowIndex < this._filteredSortedPinnedData.length) || (!this.isRowPinningToTop && rowIndex >= this._filteredSortedUnpinnedData.length); } /** * @hidden * @internal */ public isRecordPinned(rec) { return this.getInitialPinnedIndex(rec) !== -1; } /** * @hidden * @internal * Returns the record index in order of pinning by the user. Does not consider sorting/filtering. */ public getInitialPinnedIndex(rec) { const id = this.gridAPI.get_row_id(rec); return this._pinnedRecordIDs.indexOf(id); } /** * @hidden * @internal */ public get hasPinnedRecords() { return this._pinnedRecordIDs.length > 0; } /** * @hidden * @internal */ public get pinnedRecordsCount() { return this._pinnedRecordIDs.length; } /** * @hidden * @internal */ public get crudService() { return this.gridAPI.crudService; } /** * @hidden * @internal */ public _setupServices() { this.gridAPI.grid = this as any; this.crudService.grid = this as any; this.selectionService.grid = this as any; this.validation.grid = this as any; this.navigation.grid = this as any; this.filteringService.grid = this as any; this.summaryService.grid = this as any; } /** * @hidden * @internal */ public _setupListeners() { const destructor = takeUntil(this.destroy$); fromEvent(this.nativeElement, 'focusout').pipe(filter(() => !!this.navigation.activeNode), destructor).subscribe((event) => { if (!this.crudService.cell && !!this.navigation.activeNode && ((event.target === this.tbody.nativeElement && this.navigation.activeNode.row >= 0 && this.navigation.activeNode.row < this.dataView.length) || (event.target === this.theadRow.nativeElement && this.navigation.activeNode.row === -1) || (event.target === this.tfoot.nativeElement && this.navigation.activeNode.row === this.dataView.length)) && !(this.rowEditable && this.crudService.rowEditingBlocked && this.crudService.rowInEditMode)) { this.navigation.lastActiveNode = this.navigation.activeNode; this.navigation.activeNode = {} as IActiveNode; this.notifyChanges(); } }); this.rowAddedNotifier.pipe(destructor).subscribe(args => this.refreshGridState(args)); this.rowDeletedNotifier.pipe(destructor).subscribe(args => { this.summaryService.deleteOperation = true; this.summaryService.clearSummaryCache(args); }); this.subscribeToTransactions(); this.resizeNotify.pipe( filter(() => !this._init), throttleTime(0, animationFrameScheduler, { leading: true, trailing: true }), destructor ) .subscribe(() => { this.zone.run(() => { // do not trigger reflow if element is detached. if (this.document.contains(this.nativeElement)) { this.notifyChanges(true); } }); }); this.pipeTriggerNotifier.pipe(takeUntil(this.destroy$)).subscribe(() => this.pipeTrigger++); this.columnMovingEnd.pipe(destructor).subscribe(() => this.crudService.endEdit(false)); this.overlayService.opening.pipe(destructor).subscribe((event) => { if (this._advancedFilteringOverlayId === event.id) { const instance = event.componentRef.instance as IgxAdvancedFilteringDialogComponent; if (instance) { instance.initialize(this as any, this.overlayService, event.id); } } }); this.overlayService.opened.pipe(destructor).subscribe((event) => { const overlaySettings = this.overlayService.getOverlayById(event.id)?.settings; // do not hide the advanced filtering overlay on scroll if (this._advancedFilteringOverlayId === event.id) { const instance = event.componentRef.instance as IgxAdvancedFilteringDialogComponent; if (instance) { instance.lastActiveNode = this.navigation.activeNode; instance.queryBuilder.setAddButtonFocus(); } return; } // do not hide the overlay if it's attached to a row if (this.rowEditingOverlay?.overlayId === event.id) { return; } if (overlaySettings?.outlet === this.outlet && this.overlayIDs.indexOf(event.id) === -1) { this.overlayIDs.push(event.id); } }); this.overlayService.closed.pipe(filter(() => !this._init), destructor).subscribe((event) => { if (this._advancedFilteringOverlayId === event.id) { this.overlayService.detach(this._advancedFilteringOverlayId); this._advancedFilteringOverlayId = null; return; } const ind = this.overlayIDs.indexOf(event.id); if (ind !== -1) { this.overlayIDs.splice(ind, 1); } }); this.verticalScrollContainer.dataChanging.pipe(filter(() => !this._init), destructor).subscribe(($event) => { const shouldRecalcSize = this.isPercentHeight && (!this.calcHeight || this.calcHeight === this.getDataBasedBodyHeight() || this.calcHeight === this.renderedRowHeight * this._defaultTargetRecordNumber); if (shouldRecalcSize) { this.calculateGridHeight(); $event.containerSize = this.calcHeight; } this.evaluateLoadingState(); }); this.verticalScrollContainer.scrollbarVisibilityChanged.pipe(filter(() => !this._init), destructor).subscribe(() => { // called to recalc all widths that may have changes as a result of // the vert. scrollbar showing/hiding this.notifyChanges(true); this.cdr.detectChanges(); }); this.verticalScrollContainer.contentSizeChange.pipe(filter(() => !this._init), throttleTime(30), destructor).subscribe(() => { this.notifyChanges(true); }); this.densityChanged.pipe(destructor).subscribe(() => { this._autoSize = this.isPercentHeight && this.calcHeight !== this.getDataBasedBodyHeight(); this.crudService.endEdit(false); if (this._summaryRowHeight === 0) { this.summaryService.summaryHeight = 0; } this.notifyChanges(true); }); } /** * @hidden */ public override ngOnInit() { super.ngOnInit(); this._setupServices(); this._setupListeners(); this.rowListDiffer = this.differs.find([]).create(null); // compare based on field, not on object ref. this.columnListDiffer = this.differs.find([]).create((index, col: ColumnType) => col.field); this.calcWidth = this.width && this.width.indexOf('%') === -1 ? parseInt(this.width, 10) : 0; this.shouldGenerate = this.autoGenerate; } /** * @hidden * @internal */ public resetColumnsCaches() { this._columns.forEach(column => column.resetCaches()); } /** * @hidden @internal */ public generateRowID(): string | number { const primaryColumn = this._columns.find(col => col.field === this.primaryKey); const idType = this.data.length ? this.resolveDataTypes(this.data[0][this.primaryKey]) : primaryColumn ? primaryColumn.dataType : 'string'; return idType === 'string' ? uuidv4() : FAKE_ROW_ID--; } /** * @hidden * @internal */ public resetForOfCache() { const firstVirtRow = this.dataRowList.first; if (firstVirtRow) { if (this._cdrRequests) { firstVirtRow.virtDirRow.cdr.detectChanges(); } firstVirtRow.virtDirRow.assumeMaster(); } } /** * @hidden * @internal */ public setFilteredData(data, pinned: boolean) { if (this.hasPinnedRecords && pinned) { this._filteredPinnedData = data || []; const filteredUnpinned = this._filteredUnpinnedData || []; const filteredData = [... this._filteredPinnedData, ...filteredUnpinned]; this._filteredData = filteredData.length > 0 ? filteredData : this._filteredUnpinnedData; } else if (this.hasPinnedRecords && !pinned) { this._filteredUnpinnedData = data; } else { this._filteredData = data; } } /** * @hidden * @internal */ public resetColumnCollections() { this._visibleColumns.length = 0; this._pinnedVisible.length = 0; this._unpinnedVisible.length = 0; } /** * @hidden * @internal */ public resetCachedWidths() { this._unpinnedWidth = NaN; this._pinnedWidth = NaN; this._totalWidth = NaN; } /** * @hidden * @internal */ public resetCaches(recalcFeatureWidth = true) { if (recalcFeatureWidth) { this._headerFeaturesWidth = NaN; } this.resetForOfCache(); this.resetColumnsCaches(); this.resetColumnCollections(); this.resetCachedWidths(); this.hasVisibleColumns = undefined; this._columnGroups = this._columns.some(col => col.columnGroup); } /** * @hidden */ public ngAfterContentInit() { if (this.sortHeaderIconDirectiveTemplate) { this.sortHeaderIconTemplate = this.sortHeaderIconDirectiveTemplate; } if (this.sortAscendingHeaderIconDirectiveTemplate) { this.sortAscendingHeaderIconTemplate = this.sortAscendingHeaderIconDirectiveTemplate; } if (this.sortDescendingHeaderIconDirectiveTemplate) { this.sortDescendingHeaderIconTemplate = this.sortDescendingHeaderIconDirectiveTemplate; } this.setupColumns(); this.toolbar.changes.pipe(filter(() => !this._init), takeUntil(this.destroy$)).subscribe(() => this.notifyChanges(true)); this.setUpPaginator(); this.paginationComponents.changes.pipe(takeUntil(this.destroy$)).subscribe(() => { this.setUpPaginator(); }); if (this.actionStrip) { this.actionStrip.menuOverlaySettings.outlet = this.outlet; } } /** * @hidden @internal */ public dataRebinding(event: IForOfDataChangingEventArgs) { this.dataChanging.emit(event); } /** * @hidden @internal */ public dataRebound(event) { this.selectionService.clearHeaderCBState(); this.dataChanged.emit(event); } /** @hidden @internal */ public createFilterDropdown(column: ColumnType, options: OverlaySettings) { options.outlet = this.outlet; if (this.excelStyleFilteringComponent) { this.excelStyleFilteringComponent.initialize(column, this.overlayService); const id = this.overlayService.attach(this.excelStyleFilteringComponent.element, options); this.excelStyleFilteringComponent.overlayComponentId = id; return { id, ref: undefined }; } const ref = this.createComponentInstance(IgxGridExcelStyleFilteringComponent); ref.instance.initialize(column, this.overlayService); const id = this.overlayService.attach(ref.instance.element, options); ref.instance.overlayComponentId = id; return { ref, id }; } private createComponentInstance(component: any) { const dynamicComponent: ComponentRef = createComponent(component, { environmentInjector: this.envInjector, elementInjector: this.injector } ); this.appRef.attachView(dynamicComponent.hostView); return dynamicComponent; } /** @hidden @internal */ public setUpPaginator() { if (this.paginator) { this.paginator.pageChange.pipe(takeWhile(() => !!this.paginator), filter(() => !this._init)) .subscribe(() => { this.selectionService.clear(true); this.crudService.endEdit(false); this.pipeTrigger++; this.navigateTo(0); this.notifyChanges(); }); this.paginator.perPageChange.pipe(takeWhile(() => !!this.paginator), filter(() => !this._init)) .subscribe(() => { this.selectionService.clear(true); this.page = 0; this.crudService.endEdit(false); this.notifyChanges(); }); } else { this.markForCheck(); } } /** * @hidden * @internal */ public setFilteredSortedData(data, pinned: boolean) { data = data || []; if (this.pinnedRecordsCount > 0) { if (pinned) { this._filteredSortedPinnedData = data; this.pinnedRecords = data; this._filteredSortedData = this.isRowPinningToTop ? [... this._filteredSortedPinnedData, ... this._filteredSortedUnpinnedData] : [... this._filteredSortedUnpinnedData, ... this._filteredSortedPinnedData]; this.refreshSearch(true, false); } else { this._filteredSortedUnpinnedData = data; } } else { this._filteredSortedData = data; this.refreshSearch(true, false); } this.buildDataView(data); } /** * @hidden @internal */ public resetHorizontalVirtualization() { const elementFilter = (item: IgxRowDirective | IgxSummaryRowComponent) => this.isDefined(item.nativeElement.parentElement); this._horizontalForOfs = [ ...this._dataRowList.filter(elementFilter).map(item => item.virtDirRow), ...this._summaryRowList.filter(elementFilter).map(item => item.virtDirRow) ]; } /** * @hidden @internal */ public _setupRowObservers() { const elementFilter = (item: IgxRowDirective | IgxSummaryRowComponent) => this.isDefined(item.nativeElement.parentElement); const extractForOfs = pipe(map((collection: any[]) => collection.filter(elementFilter).map(item => item.virtDirRow))); const rowListObserver = extractForOfs(this._dataRowList.changes); const summaryRowObserver = extractForOfs(this._summaryRowList.changes); rowListObserver.pipe(takeUntil(this.destroy$)).subscribe(() => { this.resetHorizontalVirtualization(); }); summaryRowObserver.pipe(takeUntil(this.destroy$)).subscribe(() => { this.resetHorizontalVirtualization(); }); this.resetHorizontalVirtualization(); } /** * @hidden @internal */ public _zoneBegoneListeners() { this.zone.runOutsideAngular(() => { this.verticalScrollContainer.getScroll().addEventListener('scroll', this.verticalScrollHandler.bind(this)); this.headerContainer?.getScroll().addEventListener('scroll', this.horizontalScrollHandler.bind(this)); if (this.hasColumnsToAutosize) { this.headerContainer?.dataChanged.pipe(takeUntil(this.destroy$)).subscribe(() => { this.cdr.detectChanges(); this.zone.onStable.pipe(first()).subscribe(() => { this.autoSizeColumnsInView(); }); }); } fromEvent(window, 'resize').pipe(takeUntil(this.destroy$)).subscribe(() => this.resizeNotify.next()); resizeObservable(this.nativeElement).pipe(takeUntil(this.destroy$)).subscribe(() => this.resizeNotify.next()); }); } /** * @hidden */ public ngAfterViewInit() { this.initPinning(); this.calculateGridSizes(); this._init = false; this.cdr.reattach(); this._setupRowObservers(); this._zoneBegoneListeners(); const vertScrDC = this.verticalScrollContainer.displayContainer; vertScrDC.addEventListener('scroll', this.preventContainerScroll.bind(this)); this._pinnedRowList.changes .pipe(takeUntil(this.destroy$)) .subscribe((change: QueryList) => { this.onPinnedRowsChanged(change); }); this.addRowSnackbar?.clicked.subscribe(() => { const rec = this.filteredSortedData[this.lastAddedRowIndex]; this.scrollTo(rec, 0); this.addRowSnackbar.close(); }); // Keep the stream open for future subscribers this.rendered$.pipe(takeUntil(this.destroy$)).subscribe(() => { if (this.paginator) { this.paginator.totalRecords = this.totalRecords ? this.totalRecords : this.paginator.totalRecords; this.paginator.overlaySettings = { outlet: this.outlet }; } if (this.hasColumnsToAutosize) { this.autoSizeColumnsInView(); } this._rendered = true; }); Promise.resolve().then(() => this.rendered.next(true)); } /** * @hidden @internal */ public notifyChanges(repaint = false) { this._cdrRequests = true; this._cdrRequestRepaint = repaint; this.cdr.markForCheck(); } /** * @hidden @internal */ public override ngDoCheck() { super.ngDoCheck(); if (this._init) { return; } if (this._cdrRequestRepaint) { this.resetNotifyChanges(); this.calculateGridSizes(); this.refreshSearch(true); return; } if (this._cdrRequests) { this.resetNotifyChanges(); this.cdr.detectChanges(); } } /** * @hidden * @internal */ public getDragGhostCustomTemplate() { return this.dragGhostCustomTemplate; } /** * @hidden @internal */ public ngOnDestroy() { this.tmpOutlets.forEach((tmplOutlet) => { tmplOutlet.cleanCache(); }); this.destroy$.next(true); this.destroy$.complete(); this.transactionChange$.next(); this.transactionChange$.complete(); this._destroyed = true; if (this._advancedFilteringOverlayId) { this.overlayService.detach(this._advancedFilteringOverlayId); delete this._advancedFilteringOverlayId; } this.overlayIDs.forEach(overlayID => { const overlay = this.overlayService.getOverlayById(overlayID); if (overlay && !overlay.detached) { this.overlayService.detach(overlayID); } }); this.zone.runOutsideAngular(() => { this.verticalScrollContainer?.getScroll()?.removeEventListener('scroll', this.verticalScrollHandler); this.headerContainer?.getScroll()?.removeEventListener('scroll', this.horizontalScrollHandler); const vertScrDC = this.verticalScrollContainer?.displayContainer; vertScrDC?.removeEventListener('scroll', this.preventContainerScroll); }); } /** * Toggles the specified column's visibility. * * @example * ```typescript * this.grid1.toggleColumnVisibility({ * column: this.grid1.columns[0], * newValue: true * }); * ``` */ public toggleColumnVisibility(args: IColumnVisibilityChangedEventArgs) { const col = args.column ? this._columns.find((c) => c === args.column) : undefined; if (!col) { return; } col.toggleVisibility(args.newValue); } /** * Gets/Sets a list of key-value pairs [row ID, expansion state]. * * @remarks * Includes only states that differ from the default one. * Supports two-way binding. * @example * ```html * * * ``` */ @Input() public get expansionStates() { return this._expansionStates; } public set expansionStates(value) { this._expansionStates = new Map(value); this.expansionStatesChange.emit(this._expansionStates); this.notifyChanges(true); if (this.gridAPI.grid) { this.cdr.detectChanges(); } } /** * Expands all rows. * * @example * ```typescript * this.grid.expandAll(); * ``` */ public expandAll() { this._defaultExpandState = true; this.expansionStates = new Map(); } /** * Collapses all rows. * * @example * ```typescript * this.grid.collapseAll(); * ``` */ public collapseAll() { this._defaultExpandState = false; this.expansionStates = new Map(); } /** * Expands the row by its id. * * @remarks * ID is either the primaryKey value or the data record instance. * @example * ```typescript * this.grid.expandRow(rowID); * ``` * @param rowID The row id - primaryKey value or the data record instance. */ public expandRow(rowID: any) { this.gridAPI.set_row_expansion_state(rowID, true); } /** * Collapses the row by its id. * * @remarks * ID is either the primaryKey value or the data record instance. * @example * ```typescript * this.grid.collapseRow(rowID); * ``` * @param rowID The row id - primaryKey value or the data record instance. */ public collapseRow(rowID: any) { this.gridAPI.set_row_expansion_state(rowID, false); } /** * Toggles the row by its id. * * @remarks * ID is either the primaryKey value or the data record instance. * @example * ```typescript * this.grid.toggleRow(rowID); * ``` * @param rowID The row id - primaryKey value or the data record instance. */ public toggleRow(rowID: any) { const rec = this.gridAPI.get_rec_by_id(rowID); const state = this.gridAPI.get_row_expansion_state(rec); this.gridAPI.set_row_expansion_state(rowID, !state); } /** * @hidden * @internal */ public getDefaultExpandState(_rec: any) { return this._defaultExpandState; } /** * Gets the native element. * * @example * ```typescript * const nativeEl = this.grid.nativeElement. * ``` */ public get nativeElement() { return this.elementRef.nativeElement; } /** * Gets/Sets the outlet used to attach the grid's overlays to. * * @remark * If set, returns the outlet defined outside the grid. Otherwise returns the grid's internal outlet directive. */ @Input() public get outlet() { return this.resolveOutlet(); } public set outlet(val: IgxOverlayOutletDirective) { this._userOutletDirective = val; } /** * Gets the default row height. * * @example * ```typescript * const rowHeigh = this.grid.defaultRowHeight; * ``` */ public get defaultRowHeight(): number { switch (this.displayDensity) { case DisplayDensity.cosy: return 40; case DisplayDensity.compact: return 32; default: return 50; } } /** * @hidden @internal */ public get defaultSummaryHeight(): number { switch (this.displayDensity) { case DisplayDensity.cosy: return 30; case DisplayDensity.compact: return 24; default: return 36; } } /** * Returns the `IgxGridHeaderGroupComponent`'s minimum allowed width. * * @remarks * Used internally for restricting header group component width. * The values below depend on the header cell default right/left padding values. */ public get defaultHeaderGroupMinWidth(): number { switch (this.displayDensity) { case DisplayDensity.cosy: return 32; case DisplayDensity.compact: return 24; default: return 48; } } /** @hidden @internal */ public get pinnedWidth() { if (!isNaN(this._pinnedWidth)) { return this._pinnedWidth; } this._pinnedWidth = this.getPinnedWidth(); return this._pinnedWidth; } /** @hidden @internal */ public get unpinnedWidth() { if (!isNaN(this._unpinnedWidth)) { return this._unpinnedWidth; } this._unpinnedWidth = this.getUnpinnedWidth(); return this._unpinnedWidth; } /** * @hidden @internal */ public get isHorizontalScrollHidden() { const diff = this.unpinnedWidth - this.totalWidth; return this.width === null || diff >= 0; } /** * @hidden @internal * Gets the header cell inner width for auto-sizing. */ public getHeaderCellWidth(element: HTMLElement): ISizeInfo { const range = this.document.createRange(); const headerWidth = this.platform.getNodeSizeViaRange(range, element, element.parentElement); const headerStyle = this.document.defaultView.getComputedStyle(element); const headerPadding = parseFloat(headerStyle.paddingLeft) + parseFloat(headerStyle.paddingRight) + parseFloat(headerStyle.borderRightWidth); // Take into consideration the header group element, since column pinning applies borders to it if its not a columnGroup. const headerGroupStyle = this.document.defaultView.getComputedStyle(element.parentElement); const borderSize = parseFloat(headerGroupStyle.borderRightWidth) + parseFloat(headerGroupStyle.borderLeftWidth); return { width: Math.ceil(headerWidth), padding: Math.ceil(headerPadding + borderSize) }; } /** * @hidden @internal * Gets the combined width of the columns that are specific to the enabled grid features. They are fixed. */ public featureColumnsWidth(expander?: ElementRef) { if (Number.isNaN(this._headerFeaturesWidth)) { // TODO: platformUtil.isBrowser check const rowSelectArea = this.headerSelectorContainer?.nativeElement?.getBoundingClientRect ? this.headerSelectorContainer.nativeElement.getBoundingClientRect().width : 0; const rowDragArea = this.rowDraggable && this.headerDragContainer?.nativeElement?.getBoundingClientRect ? this.headerDragContainer.nativeElement.getBoundingClientRect().width : 0; const groupableArea = this.headerGroupContainer?.nativeElement?.getBoundingClientRect ? this.headerGroupContainer.nativeElement.getBoundingClientRect().width : 0; const expanderWidth = expander?.nativeElement?.getBoundingClientRect ? expander.nativeElement.getBoundingClientRect().width : 0; this._headerFeaturesWidth = rowSelectArea + rowDragArea + groupableArea + expanderWidth; } return this._headerFeaturesWidth; } /** * @hidden @internal */ public get summariesMargin() { return this.featureColumnsWidth(); } /** * Gets an array of `IgxColumnComponent`s. * * @example * ```typescript * const colums = this.grid.columns. * ``` */ public get columns(): IgxColumnComponent[] { return this._columns || []; } /** * Gets an array of the pinned `IgxColumnComponent`s. * * @example * ```typescript * const pinnedColumns = this.grid.pinnedColumns. * ``` */ public get pinnedColumns(): IgxColumnComponent[] { if (this._pinnedVisible.length) { return this._pinnedVisible; } this._pinnedVisible = this._pinnedColumns.filter(col => !col.hidden); return this._pinnedVisible; } /** * Gets an array of the pinned `IgxRowComponent`s. * * @example * ```typescript * const pinnedRow = this.grid.pinnedRows; * ``` */ public get pinnedRows(): IgxGridRowComponent[] { return this._pinnedRowList.toArray().sort((a, b) => a.index - b.index); } /** * Gets an array of unpinned `IgxColumnComponent`s. * * @example * ```typescript * const unpinnedColumns = this.grid.unpinnedColumns. * ``` */ public get unpinnedColumns(): IgxColumnComponent[] { if (this._unpinnedVisible.length) { return this._unpinnedVisible; } this._unpinnedVisible = this._unpinnedColumns.filter((col) => !col.hidden); return this._unpinnedVisible; } /** * Gets the `width` to be set on `IgxGridHeaderGroupComponent`. */ public getHeaderGroupWidth(column: IgxColumnComponent): string { return this.hasColumnLayouts ? '' : `${Math.max(parseFloat(column.calcWidth), this.defaultHeaderGroupMinWidth)}px`; } /** * Returns the `IgxColumnComponent` by field name. * * @example * ```typescript * const myCol = this.grid1.getColumnByName("ID"); * ``` * @param name */ public getColumnByName(name: string): IgxColumnComponent { return this._columns.find((col) => col.field === name); } public getColumnByVisibleIndex(index: number): IgxColumnComponent { return this.visibleColumns.find((col) => !col.columnGroup && !col.columnLayout && col.visibleIndex === index ); } /** * Recalculates all widths of columns that have size set to `auto`. * * @example * ```typescript * this.grid1.recalculateAutoSizes(); * ``` */ public recalculateAutoSizes() { // reset auto-size and calculate it again. this._columns.forEach(x => x.autoSize = undefined); this.resetCaches(); this.zone.onStable.pipe(first()).subscribe(() => { this.cdr.detectChanges(); this.autoSizeColumnsInView(); }); } /** * Returns an array of visible `IgxColumnComponent`s. * * @example * ```typescript * const visibleColumns = this.grid.visibleColumns. * ``` */ public get visibleColumns(): IgxColumnComponent[] { if (this._visibleColumns.length) { return this._visibleColumns; } this._visibleColumns = this._columns.filter(c => !c.hidden); return this._visibleColumns; } /** * Returns the total number of records. * * @remarks * Only functions when paging is enabled. * @example * ```typescript * const totalRecords = this.grid.totalRecords; * ``` */ @Input() public get totalRecords(): number { return this._totalRecords >= 0 ? this._totalRecords : this.pagingState?.metadata.countRecords; } public set totalRecords(total: number) { if (total >= 0) { if (this.paginator) { this.paginator.totalRecords = total; } this._totalRecords = total; this.pipeTrigger++; this.notifyChanges(); } } /** @hidden @internal */ public get totalWidth(): number { if (!isNaN(this._totalWidth)) { return this._totalWidth; } // Take only top level columns const cols = this.visibleColumns.filter(col => col.level === 0 && !col.pinned); let totalWidth = 0; let i = 0; for (i; i < cols.length; i++) { totalWidth += parseInt(cols[i].calcWidth, 10) || 0; } this._totalWidth = totalWidth; return totalWidth; } /** * @hidden * @internal */ public get showRowSelectors(): boolean { return this.isRowSelectable && this.hasVisibleColumns && !this.hideRowSelectors; } /** * @hidden * @internal */ public get showAddButton() { return this.rowEditable && this.dataView.length === 0 && this._columns.length > 0; } /** * @hidden * @internal */ public get showDragIcons(): boolean { return this.rowDraggable && this._columns.length > this.hiddenColumnsCount; } /** * @hidden * @internal */ protected _getDataViewIndex(index: number): number { let newIndex = index; if ((index < 0 || index >= this.dataView.length) && this.pagingMode === 1 && this.page !== 0) { newIndex = index - this.perPage * this.page; } else if (this.gridAPI.grid.verticalScrollContainer.isRemote) { newIndex = index - this.gridAPI.grid.virtualizationState.startIndex; } return newIndex; } /** * @hidden * @internal */ protected getDataIndex(dataViewIndex: number): number { let newIndex = dataViewIndex; if (this.gridAPI.grid.verticalScrollContainer.isRemote) { newIndex = dataViewIndex + this.gridAPI.grid.virtualizationState.startIndex; } return newIndex; } /** * Places a column before or after the specified target column. * * @example * ```typescript * grid.moveColumn(column, target); * ``` */ public moveColumn(column: IgxColumnComponent, target: IgxColumnComponent, pos: DropPosition = DropPosition.AfterDropTarget) { // M.A. May 11th, 2021 #9508 Make the event cancelable const eventArgs: IColumnMovingEndEventArgs = { source: column, target, cancel: false }; this.columnMovingEnd.emit(eventArgs); if (eventArgs.cancel) { return; } if (column === target || (column.level !== target.level) || (column.topLevelParent !== target.topLevelParent)) { return; } if (column.level) { this._moveChildColumns(column.parent, column, target, pos); } // let columnPinStateChanged; // pinning and unpinning will work correctly even without passing index // but is easier to calclulate the index here, and later use it in the pinning event args if (target.pinned && !column.pinned) { const pinnedIndex = this._pinnedColumns.indexOf(target); const index = pos === DropPosition.AfterDropTarget ? pinnedIndex + 1 : pinnedIndex; column.pin(index); } if (!target.pinned && column.pinned) { const unpinnedIndex = this._unpinnedColumns.indexOf(target); const index = pos === DropPosition.AfterDropTarget ? unpinnedIndex + 1 : unpinnedIndex; column.unpin(index); } // if (target.pinned && column.pinned && !columnPinStateChanged) { // this._reorderColumns(column, target, pos, this._pinnedColumns); // } // if (!target.pinned && !column.pinned && !columnPinStateChanged) { // this._reorderColumns(column, target, pos, this._unpinnedColumns); // } this._moveColumns(column, target, pos); this._columnsReordered(column); } /** * Triggers change detection for the `IgxGridComponent`. * Calling markForCheck also triggers the grid pipes explicitly, resulting in all updates being processed. * May degrade performance if used when not needed, or if misused: * ```typescript * // DON'Ts: * // don't call markForCheck from inside a loop * // don't call markForCheck when a primitive has changed * grid.data.forEach(rec => { * rec = newValue; * grid.markForCheck(); * }); * * // DOs * // call markForCheck after updating a nested property * grid.data.forEach(rec => { * rec.nestedProp1.nestedProp2 = newValue; * }); * grid.markForCheck(); * ``` * * @example * ```typescript * grid.markForCheck(); * ``` */ public markForCheck() { this.pipeTrigger++; this.cdr.detectChanges(); } /** * Creates a new `IgxGridRowComponent` and adds the data record to the end of the data source. * * @example * ```typescript * this.grid1.addRow(record); * ``` * @param data */ public addRow(data: any): void { // commit pending states prior to adding a row this.crudService.endEdit(true); this.gridAPI.addRowToData(data); this.pipeTrigger++; this.rowAddedNotifier.next({ data: data, owner: this, primaryKey: data[this.primaryKey] }); this.notifyChanges(); } /** * Removes the `IgxGridRowComponent` and the corresponding data record by primary key. * * @remarks * Requires that the `primaryKey` property is set. * The method accept rowSelector as a parameter, which is the rowID. * @example * ```typescript * this.grid1.deleteRow(0); * ``` * @param rowSelector */ public deleteRow(rowSelector: any): any { if (this.primaryKey !== undefined && this.primaryKey !== null) { return this.deleteRowById(rowSelector); } } /** @hidden */ public deleteRowById(rowId: any): any { const args = { rowID: rowId, primaryKey: rowId, cancel: false, rowData: this.getRowData(rowId), oldValue: null, owner: this }; this.rowDelete.emit(args); if (args.cancel) { return; } const record = this.gridAPI.deleteRowById(rowId); if (record !== null && record !== undefined) { const rowDeletedEventArgs: IRowDataEventArgs = { data: record, owner: this, primaryKey: record[this.primaryKey] }; this.rowDeleted.emit(rowDeletedEventArgs); } return record; } /** * Updates the `IgxGridRowComponent` and the corresponding data record by primary key. * * @remarks * Requires that the `primaryKey` property is set. * @example * ```typescript * this.gridWithPK.updateCell('Updated', 1, 'ProductName'); * ``` * @param value the new value which is to be set. * @param rowSelector corresponds to rowID. * @param column corresponds to column field. */ public updateCell(value: any, rowSelector: any, column: string): void { if (this.isDefined(this.primaryKey)) { const col = this._columns.find(c => c.field === column); if (col) { // Simplify const rowData = this.gridAPI.getRowData(rowSelector); const index = this.gridAPI.get_row_index_in_data(rowSelector); // If row passed is invalid if (index < 0) { return; } const id = { rowID: rowSelector, columnID: col.index, rowIndex: index }; const cell = new IgxCell(id, index, col, rowData[col.field], value, rowData, this as any); const formControl = this.validation.getFormControl(cell.id.rowID, cell.column.field); formControl.setValue(value); this.gridAPI.update_cell(cell); this.cdr.detectChanges(); } } } /** * Updates the `IgxGridRowComponent` * * @remarks * The row is specified by * rowSelector parameter and the data source record with the passed value. * This method will apply requested update only if primary key is specified in the grid. * @example * ```typescript * grid.updateRow({ * ProductID: 1, ProductName: 'Spearmint', InStock: true, UnitsInStock: 1, OrderDate: new Date('2005-03-21') * }, 1); * ``` * @param value– * @param rowSelector correspond to rowID */ // TODO: prevent event invocation public updateRow(value: any, rowSelector: any): void { if (this.isDefined(this.primaryKey)) { const editableCell = this.crudService.cell; if (editableCell && editableCell.id.rowID === rowSelector) { this.crudService.endCellEdit(); } const row = new IgxEditRow(rowSelector, -1, this.gridAPI.getRowData(rowSelector), this as any); this.gridAPI.update_row(row, value); // TODO: fix for #5934 and probably break for #5763 // consider adding of third optional boolean parameter in updateRow. // If developer set this parameter to true we should call notifyChanges(true), and // vise-versa if developer set it to false we should call notifyChanges(false). // The parameter should default to false this.notifyChanges(); } } /** * Returns the data that is contained in the row component. * * @remarks * If the primary key is not specified the row selector match the row data. * @example * ```typescript * const data = grid.getRowData(94741); * ``` * @param rowSelector correspond to rowID */ public getRowData(rowSelector: any) { if (!this.primaryKey) { return rowSelector; } const data = this.gridAPI.get_all_data(this.transactions.enabled); const index = this.gridAPI.get_row_index_in_data(rowSelector); return index < 0 ? {} : data[index]; } /** * Sort a single `IgxColumnComponent`. * * @remarks * Sort the `IgxGridComponent`'s `IgxColumnComponent` based on the provided array of sorting expressions. * @example * ```typescript * this.grid.sort({ fieldName: name, dir: SortingDirection.Asc, ignoreCase: false }); * ``` */ public sort(expression: ISortingExpression | Array): void { const sortingState = cloneArray(this.sortingExpressions); if (expression instanceof Array) { for (const each of expression) { this.gridAPI.prepare_sorting_expression([sortingState], each); } } else { if (this._sortingOptions.mode === 'single') { this._columns.forEach((col) => { if (!(col.field === expression.fieldName)) { this.clearSort(col.field); } }); } this.gridAPI.prepare_sorting_expression([sortingState], expression); } const eventArgs: ISortingEventArgs = { owner: this, sortingExpressions: sortingState, cancel: false }; this.sorting.emit(eventArgs); if (eventArgs.cancel) { return; } this.crudService.endEdit(false); if (expression instanceof Array) { this.gridAPI.sort_multiple(expression); } else { this.gridAPI.sort(expression); } requestAnimationFrame(() => this.sortingDone.emit(expression)); } /** * Filters a single `IgxColumnComponent`. * * @example * ```typescript * public filter(term) { * this.grid.filter("ProductName", term, IgxStringFilteringOperand.instance().condition("contains")); * } * ``` * @param name * @param value * @param conditionOrExpressionTree * @param ignoreCase */ public filter(name: string, value: any, conditionOrExpressionTree?: IFilteringOperation | IFilteringExpressionsTree, ignoreCase?: boolean) { this.filteringService.filter(name, value, conditionOrExpressionTree, ignoreCase); } /** * Filters all the `IgxColumnComponent` in the `IgxGridComponent` with the same condition. * * @example * ```typescript * grid.filterGlobal('some', IgxStringFilteringOperand.instance().condition('contains')); * ``` * @param value * @param condition * @param ignoreCase */ public filterGlobal(value: any, condition, ignoreCase?) { this.filteringService.filterGlobal(value, condition, ignoreCase); } /** * Enables summaries for the specified column and applies your customSummary. * * @remarks * If you do not provide the customSummary, then the default summary for the column data type will be applied. * @example * ```typescript * grid.enableSummaries([{ fieldName: 'ProductName' }, { fieldName: 'ID' }]); * ``` * Enable summaries for the listed columns. * @example * ```typescript * grid.enableSummaries('ProductName'); * ``` * @param rest */ public enableSummaries(...rest) { if (rest.length === 1 && Array.isArray(rest[0])) { this._multipleSummaries(rest[0], true); } else { this._summaries(rest[0], true, rest[1]); } } /** * Disable summaries for the specified column. * * @example * ```typescript * grid.disableSummaries('ProductName'); * ``` * @remarks * Disable summaries for the listed columns. * @example * ```typescript * grid.disableSummaries([{ fieldName: 'ProductName' }]); * ``` */ public disableSummaries(...rest) { if (rest.length === 1 && Array.isArray(rest[0])) { this._disableMultipleSummaries(rest[0]); } else { this._summaries(rest[0], false); } } /** * If name is provided, clears the filtering state of the corresponding `IgxColumnComponent`. * * @remarks * Otherwise clears the filtering state of all `IgxColumnComponent`s. * @example * ```typescript * this.grid.clearFilter(); * ``` * @param name */ public clearFilter(name?: string) { this.filteringService.clearFilter(name); } /** * If name is provided, clears the sorting state of the corresponding `IgxColumnComponent`. * * @remarks * otherwise clears the sorting state of all `IgxColumnComponent`. * @example * ```typescript * this.grid.clearSort(); * ``` * @param name */ public clearSort(name?: string) { if (!name) { this.sortingExpressions = []; return; } if (!this.gridAPI.get_column_by_name(name)) { return; } this.gridAPI.clear_sort(name); } /** * @hidden @internal */ public refreshGridState(_args?) { this.crudService.endEdit(true); this.selectionService.clearHeaderCBState(); this.summaryService.clearSummaryCache(); this.summaryPipeTrigger++; this.cdr.detectChanges(); } // TODO: We have return values here. Move them to event args ?? /** * Pins a column by field name. * * @remarks * Returns whether the operation is successful. * @example * ```typescript * this.grid.pinColumn("ID"); * ``` * @param columnName * @param index */ public pinColumn(columnName: string | IgxColumnComponent, index?): boolean { const col = columnName instanceof IgxColumnComponent ? columnName : this.getColumnByName(columnName); return col.pin(index); } /** * Unpins a column by field name. Returns whether the operation is successful. * * @example * ```typescript * this.grid.pinColumn("ID"); * ``` * @param columnName * @param index */ public unpinColumn(columnName: string | IgxColumnComponent, index?): boolean { const col = columnName instanceof IgxColumnComponent ? columnName : this.getColumnByName(columnName); return col.unpin(index); } /** * Pin the row by its id. * * @remarks * ID is either the primaryKey value or the data record instance. * @example * ```typescript * this.grid.pinRow(rowID); * ``` * @param rowID The row id - primaryKey value or the data record instance. * @param index The index at which to insert the row in the pinned collection. */ public pinRow(rowID: any, index?: number, row?: RowType): boolean { if (this._pinnedRecordIDs.indexOf(rowID) !== -1) { return false; } const eventArgs = this.gridAPI.get_pin_row_event_args(rowID, index, row, true); this.rowPinning.emit(eventArgs); if (eventArgs.cancel) { return; } this.crudService.endEdit(false); const insertIndex = typeof eventArgs.insertAtIndex === 'number' ? eventArgs.insertAtIndex : this._pinnedRecordIDs.length; this._pinnedRecordIDs.splice(insertIndex, 0, rowID); this.pipeTrigger++; if (this.gridAPI.grid) { this.cdr.detectChanges(); this.rowPinned.emit(eventArgs); } return true; } /** * Unpin the row by its id. * * @remarks * ID is either the primaryKey value or the data record instance. * @example * ```typescript * this.grid.unpinRow(rowID); * ``` * @param rowID The row id - primaryKey value or the data record instance. */ public unpinRow(rowID: any, row?: RowType): boolean { const index = this._pinnedRecordIDs.indexOf(rowID); if (index === -1) { return false; } const eventArgs = this.gridAPI.get_pin_row_event_args(rowID, null, row, false); this.rowPinning.emit(eventArgs); if (eventArgs.cancel) { return; } this.crudService.endEdit(false); this._pinnedRecordIDs.splice(index, 1); this.pipeTrigger++; if (this.gridAPI.grid) { this.cdr.detectChanges(); this.rowPinned.emit(eventArgs); } return true; } /** @hidden @internal */ public get pinnedRowHeight() { const containerHeight = this.pinContainer ? this.pinContainer.nativeElement.offsetHeight : 0; return this.hasPinnedRecords ? containerHeight : 0; } /** @hidden @internal */ public get totalHeight() { return this.calcHeight ? this.calcHeight + this.pinnedRowHeight : this.calcHeight; } /** * Recalculates grid width/height dimensions. * * @remarks * Should be run when changing DOM elements dimentions manually that affect the grid's size. * @example * ```typescript * this.grid.reflow(); * ``` */ public reflow() { this.calculateGridSizes(); } /** * Finds the next occurrence of a given string in the grid and scrolls to the cell if it isn't visible. * * @remarks * Returns how many times the grid contains the string. * @example * ```typescript * this.grid.findNext("financial"); * ``` * @param text the string to search. * @param caseSensitive optionally, if the search should be case sensitive (defaults to false). * @param exactMatch optionally, if the text should match the entire value (defaults to false). */ public findNext(text: string, caseSensitive?: boolean, exactMatch?: boolean): number { return this.find(text, 1, caseSensitive, exactMatch); } /** * Finds the previous occurrence of a given string in the grid and scrolls to the cell if it isn't visible. * * @remarks * Returns how many times the grid contains the string. * @example * ```typescript * this.grid.findPrev("financial"); * ``` * @param text the string to search. * @param caseSensitive optionally, if the search should be case sensitive (defaults to false). * @param exactMatch optionally, if the text should match the entire value (defaults to false). */ public findPrev(text: string, caseSensitive?: boolean, exactMatch?: boolean): number { return this.find(text, -1, caseSensitive, exactMatch); } /** * Reapplies the existing search. * * @remarks * Returns how many times the grid contains the last search. * @example * ```typescript * this.grid.refreshSearch(); * ``` * @param updateActiveInfo */ public refreshSearch(updateActiveInfo?: boolean, endEdit = true): number { if (this.lastSearchInfo.searchText) { this.rebuildMatchCache(); if (updateActiveInfo) { const activeInfo = IgxTextHighlightDirective.highlightGroupsMap.get(this.id); this.lastSearchInfo.matchInfoCache.forEach((match, i) => { if (match.column === activeInfo.column && match.row === activeInfo.row && match.index === activeInfo.index && compareMaps(match.metadata, activeInfo.metadata)) { this.lastSearchInfo.activeMatchIndex = i; } }); } return this.find(this.lastSearchInfo.searchText, 0, this.lastSearchInfo.caseSensitive, this.lastSearchInfo.exactMatch, false, endEdit); } else { return 0; } } /** * Removes all the highlights in the cell. * * @example * ```typescript * this.grid.clearSearch(); * ``` */ public clearSearch() { this.lastSearchInfo = { searchText: '', caseSensitive: false, exactMatch: false, activeMatchIndex: 0, matchInfoCache: [] }; this.rowList.forEach((row) => { if (row.cells) { row.cells.forEach((c: IgxGridCellComponent) => { c.clearHighlight(); }); } }); } /** @hidden @internal */ public get hasEditableColumns(): boolean { return this._columns.some((col) => col.editable); } /** @hidden @internal */ public get hasSummarizedColumns(): boolean { const summarizedColumns = this._columns.filter(col => col.hasSummary && !col.hidden); return summarizedColumns.length > 0; } /** * @hidden @internal */ public get rootSummariesEnabled(): boolean { return this.summaryCalculationMode !== GridSummaryCalculationMode.childLevelsOnly; } /** * @hidden @internal */ public get hasVisibleColumns(): boolean { if (this._hasVisibleColumns === undefined) { return this._columns ? this._columns.some(c => !c.hidden) : false; } return this._hasVisibleColumns; } public set hasVisibleColumns(value) { this._hasVisibleColumns = value; } /** @hidden @internal */ public get hasMovableColumns(): boolean { return this.moving; } /** @hidden @internal */ public get hasColumnGroups(): boolean { return this._columnGroups; } /** @hidden @internal */ public get hasColumnLayouts() { return !!this._columns.some(col => col.columnLayout); } /** * @hidden @internal */ public get multiRowLayoutRowSize() { return this._multiRowLayoutRowSize; } /** * @hidden */ protected get rowBasedHeight() { return this.dataLength * this.rowHeight; } /** * @hidden */ protected get isPercentWidth() { return this.width && this.width.indexOf('%') !== -1; } /** * @hidden @internal */ public get isPercentHeight() { return this._height && this._height.indexOf('%') !== -1; } /** * @hidden */ protected get defaultTargetBodyHeight(): number { const allItems = this.dataLength; return this.renderedRowHeight * Math.min(this._defaultTargetRecordNumber, this.paginator ? Math.min(allItems, this.paginator.perPage) : allItems); } /** * @hidden @internal * The rowHeight input is bound to min-height css prop of rows that adds a 1px border in all cases */ public get renderedRowHeight(): number { return this.rowHeight + 1; } /** * @hidden @internal */ public get outerWidth() { return this.hasVerticalScroll() ? this.calcWidth + this.scrollSize : this.calcWidth; } /** * @hidden @internal * Gets the visible content height that includes header + tbody + footer. */ public getVisibleContentHeight() { let height = this.theadRow.nativeElement.clientHeight + this.tbody.nativeElement.clientHeight; if (this.hasSummarizedColumns) { height += this.tfoot.nativeElement.clientHeight; } return height; } /** * @hidden @internal */ public getPossibleColumnWidth(baseWidth: number = null) { let computedWidth; if (baseWidth !== null) { computedWidth = baseWidth; } else { computedWidth = this.calcWidth || parseInt(this.document.defaultView.getComputedStyle(this.nativeElement).getPropertyValue('width'), 10); } computedWidth -= this.featureColumnsWidth(); const visibleChildColumns = this.visibleColumns.filter(c => !c.columnGroup); // Column layouts related let visibleCols = []; const columnBlocks = this.visibleColumns.filter(c => c.columnGroup); const colsPerBlock = columnBlocks.map(block => block.getInitialChildColumnSizes(block.children)); const combinedBlocksSize = colsPerBlock.reduce((acc, item) => acc + item.length, 0); colsPerBlock.forEach(blockCols => visibleCols = visibleCols.concat(blockCols)); // const columnsWithSetWidths = this.hasColumnLayouts ? visibleCols.filter(c => c.widthSetByUser) : visibleChildColumns.filter(c => c.widthSetByUser); const columnsToSize = this.hasColumnLayouts ? combinedBlocksSize - columnsWithSetWidths.length : visibleChildColumns.length - columnsWithSetWidths.length; const sumExistingWidths = columnsWithSetWidths .reduce((prev, curr) => { const colWidth = curr.width; let widthValue = parseInt(colWidth, 10); if (isNaN(widthValue)) { widthValue = MINIMUM_COLUMN_WIDTH; } const currWidth = colWidth && typeof colWidth === 'string' && colWidth.indexOf('%') !== -1 ? widthValue / 100 * computedWidth : widthValue; return prev + currWidth; }, 0); // When all columns are hidden, return 0px width if (!sumExistingWidths && !columnsToSize) { return '0px'; } const columnWidth = Math.floor(!Number.isFinite(sumExistingWidths) ? Math.max(computedWidth / columnsToSize, this.minColumnWidth) : Math.max((computedWidth - sumExistingWidths) / columnsToSize, this.minColumnWidth)); return columnWidth + 'px'; } /** * @hidden @internal */ public hasVerticalScroll() { if (this._init) { return false; } const isScrollable = this.verticalScrollContainer ? this.verticalScrollContainer.isScrollable() : false; return !!(this.calcWidth && this.dataView && this.dataView.length > 0 && isScrollable); } /** * Gets calculated width of the pinned area. * * @example * ```typescript * const pinnedWidth = this.grid.getPinnedWidth(); * ``` * @param takeHidden If we should take into account the hidden columns in the pinned area. */ public getPinnedWidth(takeHidden = false) { const fc = takeHidden ? this._pinnedColumns : this.pinnedColumns; let sum = 0; for (const col of fc) { if (col.level === 0) { sum += parseInt(col.calcWidth, 10); } } if (this.isPinningToStart) { sum += this.featureColumnsWidth(); } return sum; } /** * @hidden @internal */ public isColumnGrouped(_fieldName: string): boolean { return false; } /** * @hidden @internal * TODO: REMOVE */ public onHeaderSelectorClick(event) { if (!this.isMultiRowSelectionEnabled) { return; } if (this.selectionService.areAllRowSelected()) { this.selectionService.clearRowSelection(event); } else { this.selectionService.selectAllRows(event); } } /** * @hidden @internal */ public get headSelectorBaseAriaLabel() { if (this._filteringExpressionsTree.filteringOperands.length > 0) { return this.selectionService.areAllRowSelected() ? 'Deselect all filtered' : 'Select all filtered'; } return this.selectionService.areAllRowSelected() ? 'Deselect all' : 'Select all'; } /** * @hidden * @internal */ public get totalRowsCountAfterFilter() { if (this.data) { return this.selectionService.allData.length; } return 0; } /** @hidden @internal */ public get pinnedDataView(): any[] { return this.pinnedRecords ? this.pinnedRecords : []; } /** @hidden @internal */ public get unpinnedDataView(): any[] { return this.unpinnedRecords ? this.unpinnedRecords : this.verticalScrollContainer?.igxForOf || []; } /** * Returns the currently transformed paged/filtered/sorted/grouped/pinned/unpinned row data, displayed in the grid. * * @example * ```typescript * const dataView = this.grid.dataView; * ``` */ public get dataView() { return this._dataView; } /** * Gets/Sets whether clicking over a row should select/deselect it * * @remarks * By default it is set to true * @param enabled: boolean */ @WatchChanges() @Input() public get selectRowOnClick() { return this._selectRowOnClick; } public set selectRowOnClick(enabled: boolean) { this._selectRowOnClick = enabled; } /** * Select specified rows by ID. * * @example * ```typescript * this.grid.selectRows([1,2,5], true); * ``` * @param rowIDs * @param clearCurrentSelection if true clears the current selection */ public selectRows(rowIDs: any[], clearCurrentSelection?: boolean) { this.selectionService.selectRowsWithNoEvent(rowIDs, clearCurrentSelection); this.notifyChanges(); } /** * Deselect specified rows by ID. * * @example * ```typescript * this.grid.deselectRows([1,2,5]); * ``` * @param rowIDs */ public deselectRows(rowIDs: any[]) { this.selectionService.deselectRowsWithNoEvent(rowIDs); this.notifyChanges(); } /** * Selects all rows * * @remarks * By default if filtering is in place, selectAllRows() and deselectAllRows() select/deselect all filtered rows. * If you set the parameter onlyFilterData to false that will select all rows in the grid exept deleted rows. * @example * ```typescript * this.grid.selectAllRows(); * this.grid.selectAllRows(false); * ``` * @param onlyFilterData */ public selectAllRows(onlyFilterData = true) { const data = onlyFilterData && this.filteredData ? this.filteredData : this.gridAPI.get_all_data(true); const rowIDs = this.selectionService.getRowIDs(data).filter(rID => !this.gridAPI.row_deleted_transaction(rID)); this.selectRows(rowIDs); } /** * Deselects all rows * * @remarks * By default if filtering is in place, selectAllRows() and deselectAllRows() select/deselect all filtered rows. * If you set the parameter onlyFilterData to false that will deselect all rows in the grid exept deleted rows. * @example * ```typescript * this.grid.deselectAllRows(); * ``` * @param onlyFilterData */ public deselectAllRows(onlyFilterData = true) { if (onlyFilterData && this.filteredData && this.filteredData.length > 0) { this.deselectRows(this.selectionService.getRowIDs(this.filteredData)); } else { this.selectionService.clearAllSelectedRows(); this.notifyChanges(); } } /** * Deselect selected cells. * @example * ```typescript * this.grid.clearCellSelection(); * ``` */ public clearCellSelection(): void { this.selectionService.clear(true); this.notifyChanges(); } /** * @hidden @internal */ public dragScroll(delta: { left: number; top: number }): void { const horizontal = this.headerContainer.getScroll(); const vertical = this.verticalScrollContainer.getScroll(); const { left, top } = delta; horizontal.scrollLeft += left * this.DRAG_SCROLL_DELTA; vertical.scrollTop += top * this.DRAG_SCROLL_DELTA; } /** * @hidden @internal */ public isDefined(arg: any): boolean { return arg !== undefined && arg !== null; } /** * Select range(s) of cells between certain rows and columns of the grid. */ public selectRange(arg: GridSelectionRange | GridSelectionRange[] | null | undefined): void { if (!this.isDefined(arg)) { this.clearCellSelection(); return; } if (arg instanceof Array) { arg.forEach(range => this.setSelection(range)); } else { this.setSelection(arg); } this.notifyChanges(); } /** * @hidden @internal */ public columnToVisibleIndex(field: string | number): number { const visibleColumns = this.visibleColumns; if (typeof field === 'number') { return field; } return visibleColumns.find(column => column.field === field).visibleIndex; } /** * @hidden @internal */ public setSelection(range: GridSelectionRange): void { const startNode = { row: range.rowStart, column: this.columnToVisibleIndex(range.columnStart) }; const endNode = { row: range.rowEnd, column: this.columnToVisibleIndex(range.columnEnd) }; this.selectionService.pointerState.node = startNode; this.selectionService.selectRange(endNode, this.selectionService.pointerState); this.selectionService.addRangeMeta(endNode, this.selectionService.pointerState); this.selectionService.initPointerState(); } /** * Get the currently selected ranges in the grid. */ public getSelectedRanges(): GridSelectionRange[] { return this.selectionService.ranges; } /** * * Returns an array of the current cell selection in the form of `[{ column.field: cell.value }, ...]`. * * @remarks * If `formatters` is enabled, the cell value will be formatted by its respective column formatter (if any). * If `headers` is enabled, it will use the column header (if any) instead of the column field. */ public getSelectedData(formatters = false, headers = false) { const source = this.filteredSortedData; return this.extractDataFromSelection(source, formatters, headers); } /** * Get current selected columns. * * @example * Returns an array with selected columns * ```typescript * const selectedColumns = this.grid.selectedColumns(); * ``` */ public selectedColumns(): ColumnType[] { const fields = this.selectionService.getSelectedColumns(); return fields.map(field => this.getColumnByName(field)).filter(field => field); } /** * Select specified columns. * * @example * ```typescript * this.grid.selectColumns(['ID','Name'], true); * ``` * @param columns * @param clearCurrentSelection if true clears the current selection */ public selectColumns(columns: string[] | ColumnType[], clearCurrentSelection?: boolean) { let fieldToSelect: string[] = []; if (columns.length === 0 || typeof columns[0] === 'string') { fieldToSelect = columns as string[]; } else { (columns as ColumnType[]).forEach(col => { if (col.columnGroup) { const children = col.allChildren.filter(c => !c.columnGroup).map(c => c.field); fieldToSelect = [...fieldToSelect, ...children]; } else { fieldToSelect.push(col.field); } }); } this.selectionService.selectColumnsWithNoEvent(fieldToSelect, clearCurrentSelection); this.notifyChanges(); } /** * Deselect specified columns by field. * * @example * ```typescript * this.grid.deselectColumns(['ID','Name']); * ``` * @param columns */ public deselectColumns(columns: string[] | ColumnType[]) { let fieldToDeselect: string[] = []; if (columns.length === 0 || typeof columns[0] === 'string') { fieldToDeselect = columns as string[]; } else { (columns as ColumnType[]).forEach(col => { if (col.columnGroup) { const children = col.allChildren.filter(c => !c.columnGroup).map(c => c.field); fieldToDeselect = [...fieldToDeselect, ...children]; } else { fieldToDeselect.push(col.field); } }); } this.selectionService.deselectColumnsWithNoEvent(fieldToDeselect); this.notifyChanges(); } /** * Deselects all columns * * @example * ```typescript * this.grid.deselectAllColumns(); * ``` */ public deselectAllColumns() { this.selectionService.clearAllSelectedColumns(); this.notifyChanges(); } /** * Selects all columns * * @example * ```typescript * this.grid.deselectAllColumns(); * ``` */ public selectAllColumns() { this.selectColumns(this._columns.filter(c => !c.columnGroup)); } /** * * Returns an array of the current columns selection in the form of `[{ column.field: cell.value }, ...]`. * * @remarks * If `formatters` is enabled, the cell value will be formatted by its respective column formatter (if any). * If `headers` is enabled, it will use the column header (if any) instead of the column field. */ public getSelectedColumnsData(formatters = false, headers = false) { const source = this.filteredSortedData ? this.filteredSortedData : this.data; return this.extractDataFromColumnsSelection(source, formatters, headers); } /** @hidden @internal **/ public combineSelectedCellAndColumnData(columnData: any[], formatters = false, headers = false) { const source = this.filteredSortedData; return this.extractDataFromSelection(source, formatters, headers, columnData); } /** * @hidden @internal */ public preventContainerScroll = (evt) => { if (evt.target.scrollTop !== 0) { this.verticalScrollContainer.addScrollTop(evt.target.scrollTop); evt.target.scrollTop = 0; } if (evt.target.scrollLeft !== 0) { this.headerContainer.scrollPosition += evt.target.scrollLeft; evt.target.scrollLeft = 0; } }; /** * @hidden * @internal */ public copyHandler(event) { const eventPathElements = event.composedPath().map(el => el.tagName?.toLowerCase()); if (eventPathElements.includes('igx-grid-filtering-row') || eventPathElements.includes('igx-grid-filtering-cell')) { return; } const selectedColumns = this.gridAPI.grid.selectedColumns(); const columnData = this.getSelectedColumnsData(this.clipboardOptions.copyFormatters, this.clipboardOptions.copyHeaders); let selectedData; if (event.type === 'copy') { selectedData = this.getSelectedData(this.clipboardOptions.copyFormatters, this.clipboardOptions.copyHeaders); } let data = []; let result; if (event.code === 'KeyC' && (event.ctrlKey || event.metaKey) && event.currentTarget.className === 'igx-grid-thead__wrapper') { if (selectedData.length) { if (columnData.length === 0) { result = this.prepareCopyData(event, selectedData); } else { data = this.combineSelectedCellAndColumnData(columnData, this.clipboardOptions.copyFormatters, this.clipboardOptions.copyHeaders); result = this.prepareCopyData(event, data[0], data[1]); } } else { data = columnData; result = this.prepareCopyData(event, data); } navigator.clipboard.writeText(result).then().catch(e => console.error(e)); } else if (!this.clipboardOptions.enabled || this.crudService.cellInEditMode || event.type === 'keydown') { return; } else { if (selectedColumns.length) { data = this.combineSelectedCellAndColumnData(columnData, this.clipboardOptions.copyFormatters, this.clipboardOptions.copyHeaders); result = this.prepareCopyData(event, data[0], data[1]); } else { data = selectedData; result = this.prepareCopyData(event, data); } event.clipboardData.setData('text/plain', result); } } /** * @hidden @internal */ public prepareCopyData(event, data, keys?) { const ev = { data, cancel: false } as IGridClipboardEvent; this.gridCopy.emit(ev); if (ev.cancel) { return; } const transformer = new CharSeparatedValueData(ev.data, this.clipboardOptions.separator); let result = keys ? transformer.prepareData(keys) : transformer.prepareData(); if (!this.clipboardOptions.copyHeaders) { result = result.substring(result.indexOf('\n') + 1); } if (data && data.length > 0 && Object.values(data[0]).length === 1) { result = result.slice(0, -2); } event.preventDefault(); /* Necessary for the hiearachical case but will probably have to change how getSelectedData is propagated in the hiearachical grid */ event.stopPropagation(); return result; } /** * @hidden @internal */ public showSnackbarFor(index: number) { this.addRowSnackbar.actionText = index === -1 ? '' : this.snackbarActionText; this.lastAddedRowIndex = index; this.addRowSnackbar.open(); } /** * Navigates to a position in the grid based on provided `rowindex` and `visibleColumnIndex`. * * @remarks * Also can execute a custom logic over the target element, * through a callback function that accepts { targetType: GridKeydownTargetType, target: Object } * @example * ```typescript * this.grid.navigateTo(10, 3, (args) => { args.target.nativeElement.focus(); }); * ``` */ public navigateTo(rowIndex: number, visibleColIndex = -1, cb: (args: any) => void = null) { const totalItems = (this as any).totalItemCount ?? this.dataView.length - 1; if (rowIndex < 0 || rowIndex > totalItems || (visibleColIndex !== -1 && this._columns.map(col => col.visibleIndex).indexOf(visibleColIndex) === -1)) { return; } if (this.dataView.slice(rowIndex, rowIndex + 1).find(rec => rec.expression || rec.childGridsData)) { visibleColIndex = -1; } // If the target row is pinned no need to scroll as well. const shouldScrollVertically = this.navigation.shouldPerformVerticalScroll(rowIndex, visibleColIndex); const shouldScrollHorizontally = this.navigation.shouldPerformHorizontalScroll(visibleColIndex, rowIndex); if (shouldScrollVertically) { this.navigation.performVerticalScrollToCell(rowIndex, visibleColIndex, () => { if (shouldScrollHorizontally) { this.navigation.performHorizontalScrollToCell(visibleColIndex, () => this.executeCallback(rowIndex, visibleColIndex, cb)); } else { this.executeCallback(rowIndex, visibleColIndex, cb); } }); } else if (shouldScrollHorizontally) { this.navigation.performHorizontalScrollToCell(visibleColIndex, () => { if (shouldScrollVertically) { this.navigation.performVerticalScrollToCell(rowIndex, visibleColIndex, () => this.executeCallback(rowIndex, visibleColIndex, cb)); } else { this.executeCallback(rowIndex, visibleColIndex, cb); } }); } else { this.executeCallback(rowIndex, visibleColIndex, cb); } } /** * Returns `ICellPosition` which defines the next cell, * according to the current position, that match specific criteria. * * @remarks * You can pass callback function as a third parameter of `getPreviousCell` method. * The callback function accepts IgxColumnComponent as a param * @example * ```typescript * const nextEditableCellPosition = this.grid.getNextCell(0, 3, (column) => column.editable); * ``` */ public getNextCell(currRowIndex: number, curVisibleColIndex: number, callback: (IgxColumnComponent) => boolean = null): ICellPosition { const columns = this._columns.filter(col => !col.columnGroup && col.visibleIndex >= 0); const dataViewIndex = this._getDataViewIndex(currRowIndex); if (!this.isValidPosition(dataViewIndex, curVisibleColIndex)) { return { rowIndex: currRowIndex, visibleColumnIndex: curVisibleColIndex }; } const colIndexes = callback ? columns.filter((col) => callback(col)).map(editCol => editCol.visibleIndex).sort((a, b) => a - b) : columns.map(editCol => editCol.visibleIndex).sort((a, b) => a - b); const nextCellIndex = colIndexes.find(index => index > curVisibleColIndex); if (this.dataView.slice(dataViewIndex, dataViewIndex + 1) .find(rec => !rec.expression && !rec.summaries && !rec.childGridsData && !rec.detailsData) && nextCellIndex !== undefined) { return { rowIndex: currRowIndex, visibleColumnIndex: nextCellIndex }; } else { const nextIndex = this.getNextDataRowIndex(currRowIndex) if (colIndexes.length === 0 || nextIndex === currRowIndex) { return { rowIndex: currRowIndex, visibleColumnIndex: curVisibleColIndex }; } else { return { rowIndex: nextIndex, visibleColumnIndex: colIndexes[0] }; } } } /** * Returns `ICellPosition` which defines the previous cell, * according to the current position, that match specific criteria. * * @remarks * You can pass callback function as a third parameter of `getPreviousCell` method. * The callback function accepts IgxColumnComponent as a param * @example * ```typescript * const previousEditableCellPosition = this.grid.getPreviousCell(0, 3, (column) => column.editable); * ``` */ public getPreviousCell(currRowIndex: number, curVisibleColIndex: number, callback: (IgxColumnComponent) => boolean = null): ICellPosition { const columns = this._columns.filter(col => !col.columnGroup && col.visibleIndex >= 0); const dataViewIndex = this._getDataViewIndex(currRowIndex); if (!this.isValidPosition(dataViewIndex, curVisibleColIndex)) { return { rowIndex: currRowIndex, visibleColumnIndex: curVisibleColIndex }; } const colIndexes = callback ? columns.filter((col) => callback(col)).map(editCol => editCol.visibleIndex).sort((a, b) => b - a) : columns.map(editCol => editCol.visibleIndex).sort((a, b) => b - a); const prevCellIndex = colIndexes.find(index => index < curVisibleColIndex); if (this.dataView.slice(dataViewIndex, dataViewIndex + 1) .find(rec => !rec.expression && !rec.summaries && !rec.childGridsData && !rec.detailsData) && prevCellIndex !== undefined) { return { rowIndex: currRowIndex, visibleColumnIndex: prevCellIndex }; } else { const prevIndex = this.getNextDataRowIndex(currRowIndex, true); if (colIndexes.length === 0 || prevIndex === currRowIndex) { return { rowIndex: currRowIndex, visibleColumnIndex: curVisibleColIndex }; } else { return { rowIndex: prevIndex, visibleColumnIndex: colIndexes[0] }; } } } /** * @hidden * @internal */ public endRowEditTabStop(commit = true, event?: Event) { const canceled = this.crudService.endEdit(commit, event); if (canceled) { return true; } const activeCell = this.gridAPI.grid.navigation.activeNode; if (activeCell && activeCell.row !== -1) { this.tbody.nativeElement.focus(); } } /** * @hidden @internal */ public trackColumnChanges(index, col) { return col.field + col._calcWidth; } /** * @hidden */ public isExpandedGroup(_group: IGroupByRecord): boolean { return undefined; } /** * @hidden @internal * TODO: MOVE to CRUD */ public openRowOverlay(id) { this.configureRowEditingOverlay(id, this.rowList.length <= MIN_ROW_EDITING_COUNT_THRESHOLD); this.rowEditingOverlay.open(this.rowEditSettings); this.rowEditingOverlay.element.addEventListener('wheel', this.rowEditingWheelHandler.bind(this)); } /** * @hidden @internal */ public closeRowEditingOverlay() { this.rowEditingOverlay.element.removeEventListener('wheel', this.rowEditingWheelHandler); this.rowEditPositioningStrategy.isTopInitialPosition = null; this.rowEditingOverlay.close(); this.rowEditingOverlay.element.parentElement.style.display = ''; } /** * @hidden @internal */ public toggleRowEditingOverlay(show) { const rowStyle = this.rowEditingOverlay.element.style; if (show) { rowStyle.display = 'block'; } else { rowStyle.display = 'none'; } } /** * @hidden @internal */ public repositionRowEditingOverlay(row: RowType) { if (row && !this.rowEditingOverlay.collapsed) { const rowStyle = this.rowEditingOverlay.element.parentElement.style; if (row) { rowStyle.display = ''; this.configureRowEditingOverlay(row.key); this.rowEditingOverlay.reposition(); } else { rowStyle.display = 'none'; } } } /** * @hidden @internal */ public cachedViewLoaded(args: ICachedViewLoadedEventArgs) { if (this.hasHorizontalScroll()) { const tmplId = args.context.templateID.type; const index = args.context.index; args.view.detectChanges(); this.zone.onStable.pipe(first()).subscribe(() => { const row = tmplId === 'dataRow' ? this.gridAPI.get_row_by_index(index) : null; const summaryRow = tmplId === 'summaryRow' ? this.summariesRowList.find((sr) => sr.dataRowIndex === index) : null; if (row && row instanceof IgxRowDirective) { this._restoreVirtState(row); } else if (summaryRow) { this._restoreVirtState(summaryRow); } }); } } /** * Opens the advanced filtering dialog. */ public openAdvancedFilteringDialog(overlaySettings?: OverlaySettings) { const settings = overlaySettings ? overlaySettings : this._advancedFilteringOverlaySettings; if (!this._advancedFilteringOverlayId) { this._advancedFilteringOverlaySettings.target = (this as any).rootGrid ? (this as any).rootGrid.nativeElement : this.nativeElement; this._advancedFilteringOverlaySettings.outlet = this.outlet; this._advancedFilteringOverlayId = this.overlayService.attach( IgxAdvancedFilteringDialogComponent, this.viewRef, settings); this.overlayService.show(this._advancedFilteringOverlayId); } } /** * Closes the advanced filtering dialog. * * @param applyChanges indicates whether the changes should be applied */ public closeAdvancedFilteringDialog(applyChanges: boolean) { if (this._advancedFilteringOverlayId) { const advancedFilteringOverlay = this.overlayService.getOverlayById(this._advancedFilteringOverlayId); const advancedFilteringDialog = advancedFilteringOverlay.componentRef.instance as IgxAdvancedFilteringDialogComponent; if (applyChanges) { advancedFilteringDialog.applyChanges(); } advancedFilteringDialog.closeDialog(); } } /** * @hidden @internal */ public getEmptyRecordObjectFor(inRow: RowType) { const row = { ...inRow?.data }; Object.keys(row).forEach(key => row[key] = undefined); const id = this.generateRowID(); row[this.primaryKey] = id; return { rowID: id, data: row, recordRef: row }; } /** * @hidden @internal */ public hasHorizontalScroll() { return this.totalWidth - this.unpinnedWidth > 0; } /** * @hidden @internal */ public isSummaryRow(rowData): boolean { return rowData && rowData.summaries && (rowData.summaries instanceof Map); } /** * @hidden @internal */ public triggerPipes() { this.pipeTrigger++; this.cdr.detectChanges(); } /** * @hidden */ public rowEditingWheelHandler(event: WheelEvent) { if (event.deltaY > 0) { this.verticalScrollContainer.scrollNext(); } else { this.verticalScrollContainer.scrollPrev(); } } /** * @hidden */ public getUnpinnedIndexById(id) { return this.unpinnedRecords.findIndex(x => x[this.primaryKey] === id); } /** * 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 * * ``` * @param commit */ // TODO: Facade for crud service refactoring. To be removed // TODO: do not remove this, as it is used in rowEditTemplate, but mark is as internal and hidden public endEdit(commit = true, event?: Event) { this.crudService.endEdit(commit, event); } /** * Enters add mode by spawning the UI under the specified row by rowID. * * @remarks * If null is passed as rowID, the row adding UI is spawned as the first record in the data view * @remarks * Spawning the UI to add a child for a record only works if you provide a rowID * @example * ```typescript * this.grid.beginAddRowById('ALFKI'); * this.grid.beginAddRowById('ALFKI', true); * this.grid.beginAddRowById(null); * ``` * @param rowID - The rowID to spawn the add row UI for, or null to spawn it as the first record in the data view * @param asChild - Whether the record should be added as a child. Only applicable to igxTreeGrid. */ public beginAddRowById(rowID: any, asChild?: boolean): void { let index = rowID; if (rowID == null) { if (asChild) { console.warn('The record cannot be added as a child to an unspecified record.'); return; } index = null; } else { // find the index of the record with that PK index = this.gridAPI.get_rec_index_by_id(rowID, this.dataView); if (index === -1) { console.warn('No row with the specified ID was found.'); return; } } this._addRowForIndex(index, asChild); } protected _addRowForIndex(index: number, asChild?: boolean) { if (!this.dataView.length) { this.beginAddRowForIndex(index, asChild); return; } // check if the index is valid - won't support anything outside the data view if (index >= 0 && index < this.dataView.length) { // check if the index is in the view port if ((index < this.virtualizationState.startIndex || index >= this.virtualizationState.startIndex + this.virtualizationState.chunkSize) && !this.isRecordPinnedByViewIndex(index)) { this.verticalScrollContainer.chunkLoad .pipe(first(), takeUntil(this.destroy$)) .subscribe(() => { this.beginAddRowForIndex(index, asChild); }); this.navigateTo(index); this.notifyChanges(true); return; } this.beginAddRowForIndex(index, asChild); } else { console.warn('The row with the specified PK or index is outside of the current data view.'); } } /** * Enters add mode by spawning the UI at the specified index. * * @remarks * Accepted values for index are integers from 0 to this.grid.dataView.length * @example * ```typescript * this.grid.beginAddRowByIndex(0); * ``` * @param index - The index to spawn the UI at. Accepts integers from 0 to this.grid.dataView.length */ public beginAddRowByIndex(index: number): void { if (index === 0) { return this.beginAddRowById(null); } return this._addRowForIndex(index - 1); } /** * @hidden */ public preventHeaderScroll(args) { if (args.target.scrollLeft !== 0) { (this.navigation as any).forOfDir().getScroll().scrollLeft = args.target.scrollLeft; args.target.scrollLeft = 0; } } protected beginAddRowForIndex(index: number, asChild = false) { // TODO is row from rowList suitable for enterAddRowMode const row = index == null ? null : this.rowList.find(r => r.index === index); if (row !== undefined) { this.crudService.enterAddRowMode(row, asChild); } else { console.warn('No row with the specified PK or index was found.'); } } protected switchTransactionService(val: boolean) { if (val) { this._transactions = this.transactionFactory.create(TRANSACTION_TYPE.Base); } else { this._transactions = this.transactionFactory.create(TRANSACTION_TYPE.None); } if (this.dataCloneStrategy) { this._transactions.cloneStrategy = this.dataCloneStrategy; } } protected subscribeToTransactions(): void { this.transactionChange$.next(); this.transactions.onStateUpdate.pipe(takeUntil(merge(this.destroy$, this.transactionChange$))) .subscribe(this.transactionStatusUpdate.bind(this)); } protected transactionStatusUpdate(event: StateUpdateEvent) { let actions: Action[] = []; if (event.origin === TransactionEventOrigin.REDO) { actions = event.actions ? event.actions.filter(x => x.transaction.type === TransactionType.DELETE) : []; } else if (event.origin === TransactionEventOrigin.UNDO) { actions = event.actions ? event.actions.filter(x => x.transaction.type === TransactionType.ADD) : []; } if (actions.length > 0) { for (const action of actions) { if (this.selectionService.isRowSelected(action.transaction.id)) { this.selectionService.deselectRow(action.transaction.id); } } } if (event.origin === TransactionEventOrigin.REDO || event.origin === TransactionEventOrigin.UNDO) { event.actions.forEach(x => { if (x.transaction.type === TransactionType.UPDATE) { const value = this.transactions.getAggregatedValue(x.transaction.id, true); this.validation.update(x.transaction.id, value ?? x.recordRef); } else if (x.transaction.type === TransactionType.DELETE || x.transaction.type === TransactionType.ADD) { const value = this.transactions.getAggregatedValue(x.transaction.id, true); if (value) { this.validation.create(x.transaction.id, value ?? x.recordRef); this.validation.update(x.transaction.id, value ?? x.recordRef); this.validation.markAsTouched(x.transaction.id); } else { this.validation.clear(x.transaction.id); } } }); } this.selectionService.clearHeaderCBState(); this.summaryService.clearSummaryCache(); this.pipeTrigger++; this.notifyChanges(); } protected writeToData(rowIndex: number, value: any) { mergeObjects(this.gridAPI.get_all_data()[rowIndex], value); } protected _restoreVirtState(row) { // check virtualization state of data record added from cache // in case state is no longer valid - update it. const rowForOf = row.virtDirRow; const gridScrLeft = rowForOf.getScroll().scrollLeft; const left = -parseInt(rowForOf.dc.instance._viewContainer.element.nativeElement.style.left, 10); const actualScrollLeft = left + rowForOf.getColumnScrollLeft(rowForOf.state.startIndex); if (gridScrLeft !== actualScrollLeft) { rowForOf.onHScroll(gridScrLeft); rowForOf.cdr.detectChanges(); } } protected changeRowEditingOverlayStateOnScroll(row: RowType) { if (!this.rowEditable || !this.rowEditingOverlay || this.rowEditingOverlay.collapsed) { return; } if (!row) { this.toggleRowEditingOverlay(false); } else { this.repositionRowEditingOverlay(row); } } /** * Should be called when data and/or isLoading input changes so that the overlay can be * hidden/shown based on the current value of shouldOverlayLoading */ protected evaluateLoadingState() { if (this.shouldOverlayLoading) { // a new overlay should be shown const overlaySettings: OverlaySettings = { outlet: this.loadingOutlet, closeOnOutsideClick: false, positionStrategy: new ContainerPositionStrategy() }; this.loadingOverlay.open(overlaySettings); } else { this.loadingOverlay.close(); } } /** * @hidden * Sets grid width i.e. this.calcWidth */ protected calculateGridWidth() { let width; if (this.isPercentWidth) { /* width in %*/ const computed = this.document.defaultView.getComputedStyle(this.nativeElement).getPropertyValue('width'); width = computed.indexOf('%') === -1 ? parseInt(computed, 10) : null; } else { width = parseInt(this.width, 10); } if (!width && this.nativeElement) { width = this.nativeElement.offsetWidth; } if (this.width === null || !width) { width = this.getColumnWidthSum(); } if (this.hasVerticalScroll() && this.width !== null) { width -= this.scrollSize; } if ((Number.isFinite(width) || width === null) && width !== this.calcWidth) { this.calcWidth = width; } this._derivePossibleWidth(); } /** * @hidden * Sets columns defaultWidth property */ protected _derivePossibleWidth() { if (!this.columnWidthSetByUser) { this._columnWidth = this.width !== null ? this.getPossibleColumnWidth() : this.minColumnWidth + 'px'; } this._columns.forEach((column: IgxColumnComponent) => { if (this.hasColumnLayouts && parseInt(this._columnWidth, 10)) { const columnWidthCombined = parseInt(this._columnWidth, 10) * (column.colEnd ? column.colEnd - column.colStart : 1); column.defaultWidth = columnWidthCombined + 'px'; } else { // D.K. March 29th, 2021 #9145 Consider min/max width when setting defaultWidth property column.defaultWidth = this.getExtremumBasedColWidth(column); column.resetCaches(); } }); this.resetCachedWidths(); } /** * @hidden * @internal */ protected getExtremumBasedColWidth(column: IgxColumnComponent): string { let width = this._columnWidth; if (width && typeof width !== 'string') { width = String(width); } const minWidth = width.indexOf('%') === -1 ? column.minWidthPx : column.minWidthPercent; const maxWidth = width.indexOf('%') === -1 ? column.maxWidthPx : column.maxWidthPercent; if (column.hidden) { return width; } if (minWidth > parseFloat(width)) { width = String(column.minWidth); } else if (maxWidth < parseFloat(width)) { width = String(column.maxWidth); } // if no px or % are defined in maxWidth/minWidth consider it px if (width.indexOf('%') === -1 && width.indexOf('px') === -1) { width += 'px'; } return width; } protected resetNotifyChanges() { this._cdrRequestRepaint = false; this._cdrRequests = false; } /** @hidden @internal */ public resolveOutlet() { return this._userOutletDirective ? this._userOutletDirective : this._outletDirective; } /** * Reorder columns in the main columnList and _columns collections. * * @hidden */ protected _moveColumns(from: IgxColumnComponent, to: IgxColumnComponent, pos: DropPosition) { const orderedList = this._pinnedColumns.concat(this._unpinnedColumns); const list = orderedList; this._reorderColumns(from, to, pos, list); const newList = this._resetColumnList(list); this.updateColumns(newList); } /** * Update internal column's collection. * @hidden */ public updateColumns(newColumns: IgxColumnComponent[]) { // update internal collections to retain order. this._pinnedColumns = newColumns .filter((c) => c.pinned); this._unpinnedColumns = newColumns.filter((c) => !c.pinned); this._columns = newColumns; this.resetCaches(); } /** * @hidden */ protected _resetColumnList(list?) { if (!list) { list = this._columns; } let newList = []; list.filter(c => c.level === 0).forEach(p => { newList.push(p); if (p.columnGroup) { newList = newList.concat(p.allChildren); } }); return newList; } /** * Reorders columns inside the passed column collection. * When reordering column group collection, the collection is not flattened. * In all other cases, the columns collection is flattened, this is why adittional calculations on the dropIndex are done. * * @hidden */ protected _reorderColumns(from: IgxColumnComponent, to: IgxColumnComponent, position: DropPosition, columnCollection: any[], inGroup = false) { const fromIndex = columnCollection.indexOf(from); const childColumnsCount = inGroup ? 1 : from.allChildren.length + 1; columnCollection.splice(fromIndex, childColumnsCount); let dropIndex = columnCollection.indexOf(to); if (position === DropPosition.AfterDropTarget) { dropIndex++; if (!inGroup && to.columnGroup) { dropIndex += to.allChildren.length; } } columnCollection.splice(dropIndex, 0, from); } /** * Reorder column group collection. * * @hidden */ protected _moveChildColumns(parent: IgxColumnComponent, from: IgxColumnComponent, to: IgxColumnComponent, pos: DropPosition) { const buffer = parent.children.toArray(); this._reorderColumns(from, to, pos, buffer, true); parent.children.reset(buffer); } /** * @hidden @internal */ protected setupColumns() { if (this.autoGenerate) { this.autogenerateColumns(); } else { this._columns = this.getColumnList(); } this.initColumns(this._columns, (col: IgxColumnComponent) => this.columnInit.emit(col)); this.columnListDiffer.diff(this.columnList); this.columnList.changes .pipe(takeUntil(this.destroy$)) .subscribe((change: QueryList) => { this.onColumnsChanged(change); }); } protected getColumnList() { return this.columnList.toArray(); } /** * @hidden */ protected 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 if (index !== -1) { if (this.transactions.enabled) { const transaction: Transaction = { id: rowID, type: TransactionType.DELETE, newValue: null }; this.transactions.add(transaction, this.data[index]); } else { this.data.splice(index, 1); } } else { const state: State = this.transactions.getState(rowID); this.transactions.add({ id: rowID, type: TransactionType.DELETE, newValue: null }, state && state.recordRef); } } /** * @hidden @internal */ protected getDataBasedBodyHeight(): number { return !this.data || (this.data.length < this._defaultTargetRecordNumber) ? 0 : this.defaultTargetBodyHeight; } /** * @hidden @internal */ protected onPinnedRowsChanged(change: QueryList) { const diff = this.rowListDiffer.diff(change); if (diff) { this.notifyChanges(true); } } /** * @hidden */ protected onColumnsChanged(change: QueryList) { const diff = this.columnListDiffer.diff(change); if (this.autoGenerate && this._columns.length === 0 && this._autoGeneratedCols.length > 0) { // In Ivy if there are nested conditional templates the content children are re-evaluated // hence autogenerated columns are cleared and need to be reset. this.updateColumns(this._autoGeneratedCols); return; } if (diff) { let added = false; let removed = false; diff.forEachAddedItem((record: IterableChangeRecord) => { added = true; if (record.item.pinned) { this._pinnedColumns.push(record.item); } else { this._unpinnedColumns.push(record.item); } }); this.initColumns(this.columnList.toArray(), (col: IgxColumnComponent) => this.columnInit.emit(col)); diff.forEachRemovedItem((record: IterableChangeRecord) => { const isColumnGroup = record.item instanceof IgxColumnGroupComponent; if (!isColumnGroup) { // Clear Grouping this.gridAPI.clear_groupby(record.item.field); // Clear Filtering this.filteringService.clear_filter(record.item.field); // Close filter row if (this.filteringService.isFilterRowVisible && this.filteringService.filteredColumn && this.filteringService.filteredColumn.field === record.item.field) { this.filteringRow.close(); } // Clear Sorting this.gridAPI.clear_sort(record.item.field); // Remove column selection this.selectionService.deselectColumnsWithNoEvent([record.item.field]); } removed = true; }); this.resetCaches(); if (added || removed) { this.summaryService.clearSummaryCache(); this.groupablePipeTrigger++; Promise.resolve().then(() => { // `onColumnsChanged` can be executed midway a current detectChange cycle and markForCheck will be ignored then. // This ensures that we will wait for the current cycle to end so we can trigger a new one and ngDoCheck to fire. this.notifyChanges(true); }); } } } /** * @hidden */ protected calculateGridSizes(recalcFeatureWidth = true) { /* TODO: (R.K.) This layered lasagne should be refactored ASAP. The reason I have to reset the caches so many times is because after teach `detectChanges` call they are filled with invalid state. Of course all of this happens midway through the grid sizing process which of course, uses values from the caches, thus resulting in a broken layout. */ this.resetCaches(recalcFeatureWidth); this.cdr.detectChanges(); const hasScroll = this.hasVerticalScroll(); this.calculateGridWidth(); this.resetCaches(recalcFeatureWidth); this.cdr.detectChanges(); this.calculateGridHeight(); if (this.rowEditable) { this.repositionRowEditingOverlay(this.crudService.rowInEditMode); } if (this.filteringService.isFilterRowVisible) { this.filteringRow.resetChipsArea(); } this.cdr.detectChanges(); // in case scrollbar has appeared recalc to size correctly. if (hasScroll !== this.hasVerticalScroll()) { this.calculateGridWidth(); this.cdr.detectChanges(); } if (this.zone.isStable) { this.zone.run(() => { this._applyWidthHostBinding(); this.cdr.detectChanges(); }); } else { this.zone.onStable.pipe(first()).subscribe(() => { this.zone.run(() => { this._applyWidthHostBinding(); }); }); } this.resetCaches(recalcFeatureWidth); if (this.hasColumnsToAutosize) { this.cdr.detectChanges(); this.zone.onStable.pipe(first()).subscribe(() => { this.autoSizeColumnsInView(); }); } } /** * @hidden * @internal */ protected calcGridHeadRow() { if (this.maxLevelHeaderDepth) { this._baseFontSize = parseFloat(getComputedStyle(this.document.documentElement).getPropertyValue('font-size')); const hasFilterRow = this._allowFiltering && this._filterMode === FilterMode.quickFilter; const minSize = (this.maxLevelHeaderDepth + 1 + (hasFilterRow ? 1 : 0)) * this.defaultRowHeight / this._baseFontSize; this.theadRow.nativeElement.style.minHeight = `${minSize}rem`; } } /** * @hidden * Sets TBODY height i.e. this.calcHeight */ protected calculateGridHeight() { this.calcGridHeadRow(); this.calcHeight = this._calculateGridBodyHeight(); if (this.pinnedRowHeight && this.calcHeight) { this.calcHeight -= this.pinnedRowHeight; } } /** * @hidden */ protected getGroupAreaHeight(): number { return 0; } /** * @hidden */ protected getComputedHeight(elem) { return elem.offsetHeight ? parseFloat(this.document.defaultView.getComputedStyle(elem).getPropertyValue('height')) : 0; } /** * @hidden */ protected getFooterHeight(): number { return this.summaryRowHeight || this.getComputedHeight(this.tfoot.nativeElement); } /** * @hidden */ protected getTheadRowHeight(): number { const height = this.getComputedHeight(this.theadRow.nativeElement); return (!this.allowFiltering || (this.allowFiltering && this.filterMode !== FilterMode.quickFilter)) ? height - this.getFilterCellHeight() : height; } /** * @hidden */ protected getToolbarHeight(): number { let toolbarHeight = 0; if (this.toolbar.first) { toolbarHeight = this.getComputedHeight(this.toolbar.first.nativeElement); } return toolbarHeight; } /** * @hidden */ protected getPagingFooterHeight(): number { let pagingHeight = 0; if (this.footer) { const height = this.getComputedHeight(this.footer.nativeElement); pagingHeight = this.footer.nativeElement.firstElementChild ? height : 0; } return pagingHeight; } /** * @hidden */ protected getFilterCellHeight(): number { const headerGroupNativeEl = (this.headerGroupsList.length !== 0) ? this.headerGroupsList[0].nativeElement : null; const filterCellNativeEl = (headerGroupNativeEl) ? headerGroupNativeEl.querySelector('igx-grid-filtering-cell') as HTMLElement : null; return (filterCellNativeEl) ? filterCellNativeEl.offsetHeight : 0; } /** * @hidden */ protected _calculateGridBodyHeight(): number { if (!this._height) { return null; } const actualTheadRow = this.getTheadRowHeight(); const footerHeight = this.getFooterHeight(); const toolbarHeight = this.getToolbarHeight(); const pagingHeight = this.getPagingFooterHeight(); const groupAreaHeight = this.getGroupAreaHeight(); const scrHeight = this.getComputedHeight(this.scr.nativeElement); const renderedHeight = toolbarHeight + actualTheadRow + footerHeight + pagingHeight + groupAreaHeight + scrHeight; let gridHeight = 0; if (this.isPercentHeight) { const computed = this.document.defaultView.getComputedStyle(this.nativeElement).getPropertyValue('height'); const autoSize = this._shouldAutoSize(renderedHeight); if (autoSize || computed.indexOf('%') !== -1) { const bodyHeight = this.getDataBasedBodyHeight(); return bodyHeight > 0 ? bodyHeight : null; } gridHeight = parseFloat(computed); } else { gridHeight = parseInt(this._height, 10); } const height = Math.abs(gridHeight - renderedHeight); if (Math.round(height) === 0 || isNaN(gridHeight)) { const bodyHeight = this.defaultTargetBodyHeight; return bodyHeight > 0 ? bodyHeight : null; } return height; } protected checkContainerSizeChange() { const parentElement = this.nativeElement.parentElement || (this.nativeElement.getRootNode() as any).host; const origHeight = parentElement.offsetHeight; this.nativeElement.style.display = 'none'; const height = parentElement.offsetHeight; this.nativeElement.style.display = ''; return origHeight !== height; } protected _shouldAutoSize(renderedHeight) { this.tbody.nativeElement.style.display = 'none'; const parentElement = this.nativeElement.parentElement || (this.nativeElement.getRootNode() as any).host; let res = !parentElement || parentElement.clientHeight === 0 || parentElement.clientHeight === renderedHeight; if ((!this.platform.isChromium && !this.platform.isFirefox) || this._autoSize) { // If grid causes the parent container to extend (for example when container is flex) // we should always auto-size since the actual size of the container will continuously change as the grid renders elements. this._autoSize = false; res = this.checkContainerSizeChange(); } this.tbody.nativeElement.style.display = ''; return res; } /** * @hidden * Gets calculated width of the unpinned area * @param takeHidden If we should take into account the hidden columns in the pinned area. */ protected getUnpinnedWidth(takeHidden = false) { let width = this.isPercentWidth ? this.calcWidth : parseInt(this.width, 10) || parseInt(this.hostWidth, 10) || this.calcWidth; if (this.hasVerticalScroll() && !this.isPercentWidth) { width -= this.scrollSize; } if (!this.isPinningToStart) { width -= this.featureColumnsWidth(); } return width - this.getPinnedWidth(takeHidden); } /** * @hidden */ protected _summaries(fieldName: string, hasSummary: boolean, summaryOperand?: any) { const column = this.gridAPI.get_column_by_name(fieldName); if (column) { column.hasSummary = hasSummary; if (summaryOperand) { if (this.rootSummariesEnabled) { this.summaryService.retriggerRootPipe++; } column.summaries = summaryOperand; } } } /** * @hidden */ protected _multipleSummaries(expressions: ISummaryExpression[], hasSummary: boolean) { expressions.forEach((element) => { this._summaries(element.fieldName, hasSummary, element.customSummary); }); } /** * @hidden */ protected _disableMultipleSummaries(expressions) { expressions.forEach((column) => { const columnName = column && column.fieldName ? column.fieldName : column; this._summaries(columnName, false); }); } /** * @hidden */ public resolveDataTypes(rec) { if (typeof rec === 'number') { return GridColumnDataType.Number; } else if (typeof rec === 'boolean') { return GridColumnDataType.Boolean; } else if (typeof rec === 'object' && rec instanceof Date) { return GridColumnDataType.Date; } else if (typeof rec === 'string' && (/\.(gif|jpe?g|tiff?|png|webp|bmp)$/i).test(rec)) { return GridColumnDataType.Image; } return GridColumnDataType.String; } /** * @hidden */ protected autogenerateColumns() { const data = this.gridAPI.get_data(); const fields = this.generateDataFields(data); const columns = []; fields.forEach((field) => { const ref = createComponent(IgxColumnComponent, { environmentInjector: this.envInjector, elementInjector: this.injector}); ref.instance.field = field; ref.instance.dataType = this.resolveDataTypes(data[0][field]); ref.changeDetectorRef.detectChanges(); columns.push(ref.instance); }); this._autoGeneratedCols = columns; this.updateColumns(columns); if (data && data.length > 0) { this.shouldGenerate = false; } } protected generateDataFields(data: any[]): string[] { return Object.keys(data && data.length !== 0 ? data[0] : []) .filter(key => !this.autoGenerateExclude.includes(key)); } /** * @hidden */ protected initColumns(collection: IgxColumnComponent[], cb: (args: any) => void = null) { this._columnGroups = collection.some(col => col.columnGroup); if (this.hasColumnLayouts) { // Set overall row layout size collection.forEach((col) => { if (col.columnLayout) { const layoutSize = col.children ? col.children.reduce((acc, val) => Math.max(val.rowStart + val.gridRowSpan - 1, acc), 1) : 1; this._multiRowLayoutRowSize = Math.max(layoutSize, this._multiRowLayoutRowSize); } }); } if (this.hasColumnLayouts && this.hasColumnGroups) { // invalid configuration - multi-row and column groups // remove column groups const columnLayoutColumns = collection.filter((col) => col.columnLayout || col.columnLayoutChild); collection = columnLayoutColumns; } this._maxLevelHeaderDepth = null; collection.forEach((column: IgxColumnComponent) => { column.defaultWidth = this.columnWidthSetByUser ? this._columnWidth : column.defaultWidth ? column.defaultWidth : ''; if (cb) { cb(column); } }); this.updateColumns(collection); if (this.hasColumnLayouts) { collection.forEach((column: IgxColumnComponent) => { column.populateVisibleIndexes(); }); } } /** * @hidden */ protected reinitPinStates() { this._pinnedColumns = this._columns .filter((c) => c.pinned).sort((a, b) => this._pinnedColumns.indexOf(a) - this._pinnedColumns.indexOf(b)); this._unpinnedColumns = this.hasColumnGroups ? this._columns.filter((c) => !c.pinned) : this._columns.filter((c) => !c.pinned) .sort((a, b) => this._unpinnedColumns.indexOf(a) - this._unpinnedColumns.indexOf(b)); } protected extractDataFromSelection(source: any[], formatters = false, headers = false, columnData?: any[]): any[] { let columnsArray: IgxColumnComponent[]; let record = {}; let selectedData = []; let keys = []; const selectionCollection = new Map(); const keysAndData = []; const activeEl = this.selectionService.activeElement; if (this.nativeElement.tagName.toLowerCase() === 'igx-hierarchical-grid') { const expansionRowIndexes = []; for (const [key, value] of this.expansionStates.entries()) { if (value) { const rowIndex = this.gridAPI.get_rec_index_by_id(key, this.dataView); expansionRowIndexes.push(rowIndex); } } if (this.selectionService.selection.size > 0) { if (expansionRowIndexes.length > 0) { for (const [key, value] of this.selectionService.selection.entries()) { const updatedKey = key; let subtract = 0; expansionRowIndexes.forEach((row) => { if (updatedKey > Number(row)) { subtract++; } }); selectionCollection.set(updatedKey - subtract, value); } } } else if (activeEl) { let subtract = 0; if (expansionRowIndexes.length > 0) { expansionRowIndexes.forEach(row => { if (activeEl.row > Number(row)) { subtract++; } }); activeEl.row -= subtract; } } } const totalItems = (this as any).totalItemCount ?? 0; const isRemote = totalItems && totalItems > this.dataView.length; let selectionMap; if (this.nativeElement.tagName.toLowerCase() === 'igx-hierarchical-grid' && selectionCollection.size > 0) { selectionMap = isRemote ? Array.from(selectionCollection) : Array.from(selectionCollection).filter((tuple) => tuple[0] < source.length); } else { selectionMap = isRemote ? Array.from(this.selectionService.selection) : Array.from(this.selectionService.selection).filter((tuple) => tuple[0] < source.length); } if (this.cellSelection === GridSelectionMode.single && activeEl) { selectionMap.push([activeEl.row, new Set().add(activeEl.column)]); } if (this.cellSelection === GridSelectionMode.none && activeEl) { selectionMap.push([activeEl.row, new Set().add(activeEl.column)]); } if (columnData) { selectedData = columnData; } // eslint-disable-next-line prefer-const for (let [row, set] of selectionMap) { row = this.paginator && (this.pagingMode === GridPagingMode.Local && source === this.filteredSortedData) ? row + (this.perPage * this.page) : row; row = isRemote ? row - this.virtualizationState.startIndex : row; if (!source[row] || source[row].detailsData !== undefined) { continue; } const temp = Array.from(set); for (const each of temp) { columnsArray = this.getSelectableColumnsAt(each); columnsArray.forEach((col) => { if (col) { const key = !this.isPivot && headers ? col.header || col.field : col.field; const rowData = source[row].ghostRecord ? source[row].recordRef : source[row]; const value = this.isPivot ? rowData.aggregationValues.get(col.field) : resolveNestedPath(rowData, col.field); record[key] = formatters && col.formatter ? col.formatter(value, rowData) : value; if (columnData) { if (!record[key]) { record[key] = ''; } record[key] = record[key].toString().concat('recordRow-' + row); } } }); } if (Object.keys(record).length) { if (columnData) { if (!keys.length) { keys = Object.keys(columnData[0]); } for (const [key, value] of Object.entries(record)) { if (!keys.includes(key)) { keys.push(key); } let c: any = value; const rowNumber = +c.split('recordRow-')[1]; c = c.split('recordRow-')[0]; record[key] = c; const mergedObj = Object.assign(selectedData[rowNumber], record); selectedData[rowNumber] = mergedObj; } } else { selectedData.push(record); } } record = {}; } if (keys.length) { keysAndData.push(selectedData); keysAndData.push(keys); return keysAndData; } else { return selectedData; } } protected getSelectableColumnsAt(index) { if (this.hasColumnLayouts) { const visibleLayoutColumns = this.visibleColumns .filter(col => col.columnLayout) .sort((a, b) => a.visibleIndex - b.visibleIndex); const colLayout = visibleLayoutColumns[index]; return colLayout ? colLayout.children.toArray() : []; } else { const visibleColumns = this.visibleColumns .filter(col => !col.columnGroup) .sort((a, b) => a.visibleIndex - b.visibleIndex); return [visibleColumns[index]]; } } protected autoSizeColumnsInView() { if (!this.hasColumnsToAutosize) return; const vState = this.headerContainer.state; let colResized = false; const unpinnedInView = this.headerContainer.igxGridForOf.slice(vState.startIndex, vState.startIndex + vState.chunkSize).flatMap(x => x.columnGroup ? x.allChildren : x); const columnsInView = this.pinnedColumns.concat(unpinnedInView as IgxColumnComponent[]); for (const col of columnsInView) { if (!col.autoSize && col.headerCell) { const cellsContentWidths = []; if (col._cells.length !== this.rowList.length) { this.rowList.forEach(x => x.cdr.detectChanges()); } const cells = this._dataRowList.map(x => x.cells.find(c => c.column === col)); cells.forEach((cell) => cellsContentWidths.push(cell?.nativeElement?.offsetWidth || 0)); const max = Math.max(...cellsContentWidths); if (max === 0) { // cells not in DOM yet... continue; } const header = this.headerCellList.find(x => x.column === col); cellsContentWidths.push(header.nativeElement.offsetWidth); let maxSize = Math.ceil(Math.max(...cellsContentWidths)) + 1; if (col.maxWidth && maxSize > col.maxWidthPx) { maxSize = col.maxWidthPx; } else if (maxSize < col.minWidthPx) { maxSize = col.minWidthPx; } col.autoSize = maxSize; col.resetCaches(); colResized = true; } } if (colResized) { this.resetCachedWidths(); this.cdr.detectChanges(); } } protected extractDataFromColumnsSelection(source: any[], formatters = false, headers = false): any[] { let record = {}; const selectedData = []; const selectedColumns = this.selectedColumns(); if (selectedColumns.length === 0) { return []; } for (const data of source) { selectedColumns.forEach((col) => { const key = headers ? col.header || col.field : col.field; record[key] = formatters && col.formatter ? col.formatter(data[col.field], data) : data[col.field]; }); if (Object.keys(record).length) { selectedData.push(record); } record = {}; } return selectedData; } /** * @hidden */ protected initPinning() { const pinnedColumns = []; const unpinnedColumns = []; this.calculateGridWidth(); this.resetCaches(); // When a column is a group or is inside a group, pin all related. this._pinnedColumns.forEach(col => { if (col.parent) { col.parent.pinned = true; } if (col.columnGroup) { col.children.forEach(child => child.pinned = true); } }); // Make sure we don't exceed unpinned area min width and get pinned and unpinned col collections. // We take into account top level columns (top level groups and non groups). // If top level is unpinned the pinning handles all children to be unpinned as well. for (const column of this._columns) { if (column.pinned && !column.parent) { pinnedColumns.push(column); } else if (column.pinned && column.parent) { if (column.topLevelParent.pinned) { pinnedColumns.push(column); } else { column.pinned = false; unpinnedColumns.push(column); } } else { unpinnedColumns.push(column); } } // Assign the applicable collections. this._pinnedColumns = pinnedColumns; this._unpinnedColumns = unpinnedColumns; this.notifyChanges(); } /** * @hidden */ protected scrollTo(row: any | number, column: any | number, inCollection = this._filteredSortedUnpinnedData): void { let delayScrolling = false; if (this.paginator && typeof (row) !== 'number') { const rowIndex = inCollection.indexOf(row); const page = Math.floor(rowIndex / this.perPage); if (this.page !== page) { delayScrolling = true; this.page = page; } } if (delayScrolling) { this.verticalScrollContainer.dataChanged.pipe(first()).subscribe(() => { this.scrollDirective(this.verticalScrollContainer, typeof (row) === 'number' ? row : this.unpinnedDataView.indexOf(row)); }); } else { this.scrollDirective(this.verticalScrollContainer, typeof (row) === 'number' ? row : this.unpinnedDataView.indexOf(row)); } this.scrollToHorizontally(column); } /** * @hidden */ protected scrollToHorizontally(column: any | number) { let columnIndex = typeof column === 'number' ? column : this.getColumnByName(column).visibleIndex; const scrollRow = this.rowList.find(r => !!r.virtDirRow); const virtDir = scrollRow ? scrollRow.virtDirRow : null; if (this.isPinningToStart && this.pinnedColumns.length) { if (columnIndex >= this.pinnedColumns.length) { columnIndex -= this.pinnedColumns.length; this.scrollDirective(virtDir, columnIndex); } } else { this.scrollDirective(virtDir, columnIndex); } } /** * @hidden */ protected scrollDirective(directive: IgxGridForOfDirective, goal: number): void { if (!directive) { return; } directive.scrollTo(goal); } /** * @hidden */ protected getColumnWidthSum(): number { let colSum = 0; const cols = this.hasColumnLayouts ? this.visibleColumns.filter(x => x.columnLayout) : this.visibleColumns.filter(x => !x.columnGroup); cols.forEach((item) => { colSum += parseInt((item.calcWidth || item.defaultWidth), 10) || this.minColumnWidth; }); if (!colSum) { return null; } this.cdr.detectChanges(); colSum += this.featureColumnsWidth(); return colSum; } /** * Notify changes, reset cache and populateVisibleIndexes. * * @hidden */ private _columnsReordered(column: IgxColumnComponent) { this.notifyChanges(); if (this.hasColumnLayouts) { this._columns.filter(x => x.columnLayout).forEach(x => x.populateVisibleIndexes()); } // after reordering is done reset cached column collections. this.resetColumnCollections(); column.resetCaches(); } protected buildDataView(_data: any[]) { this._dataView = this.isRowPinningToTop ? [...this.pinnedDataView, ...this.unpinnedDataView] : [...this.unpinnedDataView, ...this.pinnedDataView]; } private _applyWidthHostBinding() { let width = this._width; if (width === null) { let currentWidth = this.calcWidth; if (this.hasVerticalScroll()) { currentWidth += this.scrollSize; } width = currentWidth + 'px'; this.resetCaches(); } this._hostWidth = width; this.cdr.markForCheck(); } protected verticalScrollHandler(event) { this.verticalScrollContainer.onScroll(event); this.disableTransitions = true; this.zone.run(() => { this.zone.onStable.pipe(first()).subscribe(() => { this.verticalScrollContainer.chunkLoad.emit(this.verticalScrollContainer.state); if (this.rowEditable) { this.changeRowEditingOverlayStateOnScroll(this.crudService.rowInEditMode); } }); }); this.disableTransitions = false; this.hideOverlays(); this.actionStrip?.hide(); if (this.actionStrip) { this.actionStrip.context = null; } const args: IGridScrollEventArgs = { direction: 'vertical', event, scrollPosition: this.verticalScrollContainer.scrollPosition }; this.gridScroll.emit(args); } protected horizontalScrollHandler(event) { const scrollLeft = event.target.scrollLeft; this.headerContainer.onHScroll(scrollLeft); this._horizontalForOfs.forEach(vfor => vfor.onHScroll(scrollLeft)); this.cdr.markForCheck(); this.zone.run(() => { this.zone.onStable.pipe(first()).subscribe(() => { this.parentVirtDir.chunkLoad.emit(this.headerContainer.state); requestAnimationFrame(() => { this.autoSizeColumnsInView(); }); }); }); if(!this.navigation.isColumnFullyVisible(this.navigation.lastColumnIndex)) { this.hideOverlays(); } const args: IGridScrollEventArgs = { direction: 'horizontal', event, scrollPosition: this.headerContainer.scrollPosition }; this.gridScroll.emit(args); } private executeCallback(rowIndex, visibleColIndex = -1, cb: (args: any) => void = null) { if (!cb) { return; } let row = this.summariesRowList.filter(s => s.index !== 0).concat(this.rowList.toArray()).find(r => r.index === rowIndex); if (!row) { if ((this as any).totalItemCount) { this.verticalScrollContainer.dataChanged.pipe(first()).subscribe(() => { this.cdr.detectChanges(); row = this.summariesRowList.filter(s => s.index !== 0).concat(this.rowList.toArray()).find(r => r.index === rowIndex); const cbArgs = this.getNavigationArguments(row, visibleColIndex); cb(cbArgs); }); } const dataViewIndex = this._getDataViewIndex(rowIndex); if (this.dataView[dataViewIndex].detailsData) { this.navigation.setActiveNode({ row: rowIndex }); this.cdr.detectChanges(); } return; } const args = this.getNavigationArguments(row, visibleColIndex); cb(args); } private getNavigationArguments(row, visibleColIndex) { let targetType: GridKeydownTargetType; let target; switch (row.nativeElement.tagName.toLowerCase()) { case 'igx-grid-groupby-row': targetType = 'groupRow'; target = row; break; case 'igx-grid-summary-row': targetType = 'summaryCell'; target = visibleColIndex !== -1 ? row.summaryCells.find(c => c.visibleColumnIndex === visibleColIndex) : row.summaryCells.first; break; case 'igx-child-grid-row': targetType = 'hierarchicalRow'; target = row; break; default: targetType = 'dataCell'; target = visibleColIndex !== -1 ? row.cells.find(c => c.visibleColumnIndex === visibleColIndex) : row.cells.first; break; } return { targetType, target }; } private getNextDataRowIndex(currentRowIndex, previous = false): number { const resolvedIndex = this._getDataViewIndex(currentRowIndex); if (currentRowIndex < 0 || (currentRowIndex === 0 && previous) || (resolvedIndex >= this.dataView.length - 1 && !previous)) { return currentRowIndex; } // find next/prev record that is editable. const nextRowIndex = previous ? this.findPrevEditableDataRowIndex(currentRowIndex) : this.dataView.findIndex((rec, index) => index > resolvedIndex && this.isEditableDataRecordAtIndex(index)); const nextDataIndex = this.getDataIndex(nextRowIndex); return nextDataIndex !== -1 ? nextDataIndex : currentRowIndex; } /** * Returns the previous editable row index or -1 if no such row is found. * * @param currentIndex The index of the current editable record. */ private findPrevEditableDataRowIndex(currentIndex): number { let i = this.dataView.length; const resolvedIndex = this._getDataViewIndex(currentIndex); while (i--) { if (i < resolvedIndex && this.isEditableDataRecordAtIndex(i)) { return i; } } return -1; } /** * Returns if the record at the specified data view index is a an editable data record. * If record is group rec, summary rec, child rec, ghost rec. etc. it is not editable. * * @param dataViewIndex The index of that record in the data view. * */ // TODO: Consider moving it into CRUD private isEditableDataRecordAtIndex(dataViewIndex) { const rec = this.dataView[dataViewIndex]; return !rec.expression && !rec.summaries && !rec.childGridsData && !rec.detailsData && !this.isGhostRecordAtIndex(dataViewIndex); } /** * Returns if the record at the specified data view index is a ghost. * If record is pinned but is not in pinned area then it is a ghost record. * * @param dataViewIndex The index of that record in the data view. */ private isGhostRecordAtIndex(dataViewIndex) { const isPinned = this.isRecordPinned(this.dataView[dataViewIndex]); const isInPinnedArea = this.isRecordPinnedByViewIndex(dataViewIndex); return isPinned && !isInPinnedArea; } private isValidPosition(rowIndex, colIndex): boolean { const rows = this.summariesRowList.filter(s => s.index !== 0).concat(this.rowList.toArray()).length; const cols = this._columns.filter(col => !col.columnGroup && col.visibleIndex >= 0 && !col.hidden).length; if (rows < 1 || cols < 1) { return false; } if (rowIndex > -1 && rowIndex < this.dataView.length && colIndex > - 1 && colIndex <= Math.max(...this.visibleColumns.map(c => c.visibleIndex))) { return true; } return false; } private find(text: string, increment: number, caseSensitive?: boolean, exactMatch?: boolean, scroll?: boolean, endEdit = true) { if (!this.rowList) { return 0; } if (endEdit) { this.crudService.endEdit(false); } if (!text) { this.clearSearch(); return 0; } const caseSensitiveResolved = caseSensitive ? true : false; const exactMatchResolved = exactMatch ? true : false; let rebuildCache = false; if (this.lastSearchInfo.searchText !== text || this.lastSearchInfo.caseSensitive !== caseSensitiveResolved || this.lastSearchInfo.exactMatch !== exactMatchResolved) { this.lastSearchInfo = { searchText: text, activeMatchIndex: 0, caseSensitive: caseSensitiveResolved, exactMatch: exactMatchResolved, matchInfoCache: [] }; rebuildCache = true; } else { this.lastSearchInfo.activeMatchIndex += increment; } if (rebuildCache) { this.rowList.forEach((row) => { if (row.cells) { row.cells.forEach((c: IgxGridCellComponent) => { c.highlightText(text, caseSensitiveResolved, exactMatchResolved); }); } }); this.rebuildMatchCache(); } if (this.lastSearchInfo.activeMatchIndex >= this.lastSearchInfo.matchInfoCache.length) { this.lastSearchInfo.activeMatchIndex = 0; } else if (this.lastSearchInfo.activeMatchIndex < 0) { this.lastSearchInfo.activeMatchIndex = this.lastSearchInfo.matchInfoCache.length - 1; } if (this.lastSearchInfo.matchInfoCache.length) { const matchInfo = this.lastSearchInfo.matchInfoCache[this.lastSearchInfo.activeMatchIndex]; this.lastSearchInfo = { ...this.lastSearchInfo }; if (scroll !== false) { this.scrollTo(matchInfo.row, matchInfo.column); } IgxTextHighlightDirective.setActiveHighlight(this.id, { column: matchInfo.column, row: matchInfo.row, index: matchInfo.index, metadata: matchInfo.metadata, }); } else { IgxTextHighlightDirective.clearActiveHighlight(this.id); } return this.lastSearchInfo.matchInfoCache.length; } private rebuildMatchCache() { this.lastSearchInfo.matchInfoCache = []; const caseSensitive = this.lastSearchInfo.caseSensitive; const exactMatch = this.lastSearchInfo.exactMatch; const searchText = caseSensitive ? this.lastSearchInfo.searchText : this.lastSearchInfo.searchText.toLowerCase(); const data = this.filteredSortedData; const columnItems = this.visibleColumns.filter((c) => !c.columnGroup).sort((c1, c2) => c1.visibleIndex - c2.visibleIndex); data.forEach((dataRow, rowIndex) => { columnItems.forEach((c) => { const pipeArgs = this.getColumnByName(c.field).pipeArgs; const value = c.formatter ? c.formatter(resolveNestedPath(dataRow, c.field), dataRow) : c.dataType === 'number' ? formatNumber(resolveNestedPath(dataRow, c.field), this.locale, pipeArgs.digitsInfo) : c.dataType === 'date' ? formatDate(resolveNestedPath(dataRow, c.field), pipeArgs.format, this.locale, pipeArgs.timezone) : resolveNestedPath(dataRow, c.field); if (value !== undefined && value !== null && c.searchable) { let searchValue = caseSensitive ? String(value) : String(value).toLowerCase(); if (exactMatch) { if (searchValue === searchText) { const metadata = new Map(); metadata.set('pinned', this.isRecordPinnedByIndex(rowIndex)); this.lastSearchInfo.matchInfoCache.push({ row: dataRow, column: c.field, index: 0, metadata, }); } } else { let occurenceIndex = 0; let searchIndex = searchValue.indexOf(searchText); while (searchIndex !== -1) { const metadata = new Map(); metadata.set('pinned', this.isRecordPinnedByIndex(rowIndex)); this.lastSearchInfo.matchInfoCache.push({ row: dataRow, column: c.field, index: occurenceIndex++, metadata, }); searchValue = searchValue.substring(searchIndex + searchText.length); searchIndex = searchValue.indexOf(searchText); } } } }); }); } // TODO: About to Move to CRUD private configureRowEditingOverlay(rowID: any, useOuter = false) { let settings = this.rowEditSettings; const overlay = this.overlayService.getOverlayById(this.rowEditingOverlay.overlayId); if (overlay) { settings = overlay.settings; } settings.outlet = useOuter ? this.parentRowOutletDirective : this.rowOutletDirective; this.rowEditPositioningStrategy.settings.container = this.tbody.nativeElement; const pinned = this._pinnedRecordIDs.indexOf(rowID) !== -1; const targetRow = !pinned ? this.gridAPI.get_row_by_key(rowID) as IgxRowDirective : this.pinnedRows.find(x => x.key === rowID) as IgxRowDirective; if (!targetRow) { return; } settings.target = targetRow.element.nativeElement; this.toggleRowEditingOverlay(true); } }