UNPKG

igniteui-angular

Version:

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

1,183 lines (1,178 loc) 208 kB
import * as i0 from '@angular/core'; import { Injectable, inject, EventEmitter, reflectComponentType, createComponent, booleanAttribute, Output, Input, Directive, QueryList, TemplateRef, ContentChildren, ContentChild, forwardRef, ChangeDetectionStrategy, Component, Pipe, ElementRef, HostBinding, ViewChild, ViewChildren, ChangeDetectorRef, ViewContainerRef, CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { NgClass, NgTemplateOutlet, DecimalPipe, PercentPipe, CurrencyPipe, DatePipe, NgStyle } from '@angular/common'; import { Subject } from 'rxjs'; import { GridBaseAPIService, IgxGridNavigationService, IgxGridTransaction, IGX_GRID_SERVICE_BASE, IgxColumnComponent, IgxColumnGroupComponent, IgxSummaryOperand, IgxGridToolbarDirective, IgxFilteringService, IgxGridSelectionService, IGX_GRID_BASE, IgxGridCellComponent, IgxGridCellImageAltPipe, IgxStringReplacePipe, IgxColumnFormatterPipe, IgxRowDirective, IgxRowDragDirective, IgxGridNotGroupedPipe, IgxGridCellStylesPipe, IgxGridCellStyleClassesPipe, IgxGridDataMapperPipe, IgxGridTransactionStatePipe, IgxHierarchicalGridRow, IgxGridCell, IgxGridHeaderRowComponent, IgxGridBodyDirective, IgxGridDragSelectDirective, IgxColumnMovingDropDirective, IgxSummaryRowComponent, IgxRowEditTabStopDirective, IgxGridColumnResizerComponent, IgxGridTransactionPipe, IgxHasVisibleColumnsPipe, IgxGridRowPinningPipe, IgxGridAddRowPipe, IgxGridRowClassesPipe, IgxGridRowStylesPipe, IgxSummaryDataPipe, IgxGridCRUDService, IgxGridValidationService, IgxGridSummaryService, IgxColumnResizingService, IgxRowAddTextDirective, IgxRowEditActionsDirective, IgxRowEditTextDirective, IgxGridFooterComponent, IgxAdvancedFilteringDialogComponent, IgxRowExpandedIndicatorDirective, IgxRowCollapsedIndicatorDirective, IgxHeaderExpandedIndicatorDirective, IgxHeaderCollapsedIndicatorDirective, IgxExcelStyleHeaderIconDirective, IgxSortAscendingHeaderIconDirective, IgxSortDescendingHeaderIconDirective, IgxSortHeaderIconDirective, IgxDragIndicatorIconDirective, IgxRowDragGhostDirective, IgxGridStateDirective, IgxGridPinningActionsComponent, IgxGridEditingActionsComponent, IgxGridActionsBaseDirective, IgxGridActionButtonComponent, IgxGridHeaderComponent, IgxGridHeaderGroupComponent, IgxFilterCellTemplateDirective, IgxSummaryTemplateDirective, IgxCellTemplateDirective, IgxCellValidationErrorDirective, IgxCellHeaderTemplateDirective, IgxCellFooterTemplateDirective, IgxCellEditorTemplateDirective, IgxCollapsibleIndicatorTemplateDirective, IgxColumnLayoutComponent, IgxColumnActionsComponent, IgxColumnHidingDirective, IgxColumnPinningDirective, IgxRowSelectorDirective, IgxGroupByRowSelectorDirective, IgxHeadSelectorDirective, IgxCSVTextDirective, IgxExcelTextDirective, IgxGridToolbarActionsComponent, IgxGridToolbarAdvancedFilteringComponent, IgxGridToolbarComponent, IgxGridToolbarExporterComponent, IgxGridToolbarHidingComponent, IgxGridToolbarPinningComponent, IgxGridToolbarTitleComponent, IgxGridExcelStyleFilteringComponent, IgxExcelStyleHeaderComponent, IgxExcelStyleSortingComponent, IgxExcelStylePinningComponent, IgxExcelStyleHidingComponent, IgxExcelStyleSelectingComponent, IgxExcelStyleClearFiltersComponent, IgxExcelStyleConditionalFilterComponent, IgxExcelStyleMovingComponent, IgxExcelStyleSearchComponent, IgxExcelStyleColumnOperationsTemplateDirective, IgxExcelStyleFilterOperationsTemplateDirective, IgxExcelStyleLoadingValuesTemplateDirective, IgxColumnRequiredValidatorDirective, IgxColumnMinValidatorDirective, IgxColumnMaxValidatorDirective, IgxColumnEmailValidatorDirective, IgxColumnMinLengthValidatorDirective, IgxColumnMaxLengthValidatorDirective, IgxColumnPatternValidatorDirective } from 'igniteui-angular/grids/core'; import { first, takeUntil, pluck, filter } from 'rxjs/operators'; import { SUPPORTED_KEYS, NAVIGATION_KEYS, IgxTransactionService, IgxActionStripToken, cloneArray, resolveNestedPath, columnFieldPath, DataUtil, HammerGesturesManager, flatten as flatten$1, IgxOverlayOutletDirective } from 'igniteui-angular/core'; import { IgxGridBaseDirective, IgxGridComponent, IgxGridSortingPipe, IgxGridFilteringPipe, IgxGridCellMergePipe, IgxGridUnmergeActivePipe } from 'igniteui-angular/grids/grid'; import { IgxPaginatorDirective, IgxPaginatorToken, IgxPaginatorComponent, IgxPageNavigationComponent, IgxPageSizeSelectorComponent, IgxPaginatorContentDirective } from 'igniteui-angular/paginator'; import * as i1 from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { IgxChipComponent } from 'igniteui-angular/chips'; import { IgxTextHighlightDirective, IgxFocusDirective, IgxTextSelectionDirective, IgxDateTimeEditorDirective, IgxTooltipTargetDirective, IgxTooltipDirective, IgxGridForOfDirective, IgxTemplateOutletDirective, IgxToggleDirective, IgxButtonDirective, IgxRippleDirective, IgxScrollInertiaDirective, IgxForOfSyncService, IgxForOfScrollSyncService } from 'igniteui-angular/directives'; import { IgxIconComponent } from 'igniteui-angular/icon'; import { IgxInputGroupComponent, IgxInputDirective, IgxPrefixDirective, IgxSuffixDirective } from 'igniteui-angular/input-group'; import { IgxCheckboxComponent } from 'igniteui-angular/checkbox'; import { IgxDatePickerComponent } from 'igniteui-angular/date-picker'; import { IgxTimePickerComponent } from 'igniteui-angular/time-picker'; import { IgxCircularProgressBarComponent } from 'igniteui-angular/progressbar'; import { IgxSnackbarComponent } from 'igniteui-angular/snackbar'; class IgxHierarchicalGridAPIService extends GridBaseAPIService { constructor() { super(...arguments); this.childRowIslands = new Map(); this.childGrids = new Map(); } registerChildRowIsland(rowIsland) { this.childRowIslands.set(rowIsland.key, rowIsland); this.destroyMap.set(rowIsland.key, new Subject()); } unsetChildRowIsland(rowIsland) { this.childGrids.delete(rowIsland.key); this.childRowIslands.delete(rowIsland.key); this.destroyMap.delete(rowIsland.key); } getChildRowIsland(key) { return this.childRowIslands.get(key); } getChildGrid(path) { const currPath = path; let grid; const pathElem = currPath.shift(); const childrenForLayout = this.childGrids.get(pathElem.rowIslandKey); if (childrenForLayout) { const childGrid = childrenForLayout.get(pathElem.rowKey); if (currPath.length === 0) { grid = childGrid; } else { grid = childGrid.gridAPI.getChildGrid(currPath); } } return grid; } getChildGrids(inDepth) { let allChildren = []; this.childGrids.forEach((layoutMap) => { layoutMap.forEach((grid) => { allChildren.push(grid); if (inDepth) { const children = grid.gridAPI.getChildGrids(inDepth); allChildren = allChildren.concat(children); } }); }); return allChildren; } getParentRowId(childGrid) { let rowID; this.childGrids.forEach((layoutMap) => { layoutMap.forEach((grid, key) => { if (grid === childGrid) { rowID = key; return; } }); }); return rowID; } registerChildGrid(parentRowID, rowIslandKey, grid) { let childrenForLayout = this.childGrids.get(rowIslandKey); if (!childrenForLayout) { this.childGrids.set(rowIslandKey, new Map()); childrenForLayout = this.childGrids.get(rowIslandKey); } childrenForLayout.set(parentRowID, grid); } getChildGridsForRowIsland(rowIslandKey) { const childrenForLayout = this.childGrids.get(rowIslandKey); const children = []; if (childrenForLayout) { childrenForLayout.forEach((child) => { children.push(child); }); } return children; } getChildGridByID(rowIslandKey, rowID) { const childrenForLayout = this.childGrids.get(rowIslandKey); return childrenForLayout.get(rowID); } get_row_expansion_state(record) { let inState; if (record.childGridsData !== undefined) { const ri = record.key; const states = this.grid.expansionStates; const expanded = states.get(ri); if (expanded !== undefined) { return expanded; } else { return this.grid.getDefaultExpandState(record); } } else { inState = !!super.get_row_expansion_state(record); } return inState && this.grid.childLayoutList.length !== 0; } allow_expansion_state_change(rowID, expanded) { const rec = this.get_rec_by_id(rowID); const grid = this.grid; if (grid.hasChildrenKey && !rec[grid.hasChildrenKey]) { return false; } return !!rec && this.grid.expansionStates.get(rowID) !== expanded; } get_rec_by_id(rowID) { const data = this.get_all_data(false); const index = this.get_row_index_in_data(rowID, data); return data[index]; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxHierarchicalGridAPIService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxHierarchicalGridAPIService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxHierarchicalGridAPIService, decorators: [{ type: Injectable }] }); class IgxHierarchicalGridNavigationService extends IgxGridNavigationService { constructor() { super(...arguments); this._pendingNavigation = false; } dispatchEvent(event) { const key = event.key.toLowerCase(); const cellOrRowInEdit = this.grid.crudService.cell || this.grid.crudService.row; if (!this.activeNode || !(SUPPORTED_KEYS.has(key) || (key === 'tab' && cellOrRowInEdit))) { return; } const targetGrid = this.getClosestElemByTag(event.target, 'igx-hierarchical-grid') || this.getClosestElemByTag(event.target, 'igc-hierarchical-grid'); if (targetGrid !== this.grid.nativeElement) { return; } if (this._pendingNavigation && NAVIGATION_KEYS.has(key)) { // In case focus needs to be moved from one grid to another, however there is a pending scroll operation // which is an async operation, any additional navigation keys should be ignored // untill operation complete. event.preventDefault(); return; } super.dispatchEvent(event); } navigateInBody(rowIndex, visibleColIndex, cb = null) { const rec = this.grid.dataView[rowIndex]; if (rec && this.grid.isChildGridRecord(rec)) { // target is child grid const virtState = this.grid.verticalScrollContainer.state; const inView = rowIndex >= virtState.startIndex && rowIndex <= virtState.startIndex + virtState.chunkSize; const isNext = this.activeNode.row < rowIndex; const targetLayoutIndex = isNext ? null : this.grid.childLayoutKeys.length - 1; if (inView) { this._moveToChild(rowIndex, visibleColIndex, isNext, targetLayoutIndex, cb); } else { let scrollAmount = this.grid.verticalScrollContainer.getScrollForIndex(rowIndex, !isNext); scrollAmount += isNext ? 1 : -1; this.grid.verticalScrollContainer.getScroll().scrollTop = scrollAmount; this._pendingNavigation = true; this.grid.verticalScrollContainer.chunkLoad.pipe(first()).subscribe(() => { this._moveToChild(rowIndex, visibleColIndex, isNext, targetLayoutIndex, cb); this._pendingNavigation = false; }); } return; } const isLast = rowIndex === this.grid.dataView.length; if ((rowIndex === -1 || isLast) && this.grid.parent !== null) { // reached end of child grid const nextSiblingIndex = this.nextSiblingIndex(isLast); if (nextSiblingIndex !== null) { this.grid.parent.navigation._moveToChild(this.grid.childRow.index, visibleColIndex, isLast, nextSiblingIndex, cb); } else { this._moveToParent(isLast, visibleColIndex, cb); } return; } if (this.grid.parent) { const isNext = this.activeNode && typeof this.activeNode.row === 'number' ? rowIndex > this.activeNode.row : false; const cbHandler = (args) => { this._handleScrollInChild(rowIndex, isNext); cb(args); }; if (!this.activeNode) { this.activeNode = { row: null, column: null }; } super.navigateInBody(rowIndex, visibleColIndex, cbHandler); return; } if (!this.activeNode) { this.activeNode = { row: null, column: null }; } super.navigateInBody(rowIndex, visibleColIndex, cb); } shouldPerformVerticalScroll(index, visibleColumnIndex = -1, isNext) { const targetRec = this.grid.dataView[index]; if (this.grid.isChildGridRecord(targetRec)) { const scrollAmount = this.grid.verticalScrollContainer.getScrollForIndex(index, !isNext); const currScroll = this.grid.verticalScrollContainer.getScroll().scrollTop; const shouldScroll = !isNext ? scrollAmount > currScroll : currScroll < scrollAmount; return shouldScroll; } else { return super.shouldPerformVerticalScroll(index, visibleColumnIndex); } } focusTbody(event) { if (!this.activeNode || this.activeNode.row === null) { this.activeNode = { row: 0, column: 0 }; this.grid.navigateTo(0, 0, (obj) => { this.grid.clearCellSelection(); obj.target.activate(event); }); } else { super.focusTbody(event); } } nextSiblingIndex(isNext) { const layoutKey = this.grid.childRow.layout.key; const layoutIndex = this.grid.parent.childLayoutKeys.indexOf(layoutKey); const nextIndex = isNext ? layoutIndex + 1 : layoutIndex - 1; if (nextIndex <= this.grid.parent.childLayoutKeys.length - 1 && nextIndex > -1) { return nextIndex; } else { return null; } } /** * Handles scrolling in child grid and ensures target child row is in main grid view port. * * @param rowIndex The row index which should be in view. * @param isNext Optional. Whether we are navigating to next. Used to determine scroll direction. * @param cb Optional.Callback function called when operation is complete. */ _handleScrollInChild(rowIndex, isNext, cb) { const shouldScroll = this.shouldPerformVerticalScroll(rowIndex, -1, isNext); if (shouldScroll) { this.grid.navigation.performVerticalScrollToCell(rowIndex, -1, () => { this.positionInParent(rowIndex, isNext, cb); }); } else { this.positionInParent(rowIndex, isNext, cb); } } /** * * @param rowIndex Row index that should come in view. * @param isNext Whether we are navigating to next. Used to determine scroll direction. * @param cb Optional.Callback function called when operation is complete. */ positionInParent(rowIndex, isNext, cb) { const row = this.grid.gridAPI.get_row_by_index(rowIndex); if (!row) { if (cb) { cb(); } return; } const positionInfo = this.getPositionInfo(row, isNext); if (!positionInfo.inView) { // stop event from triggering multiple times before scrolling is complete. this._pendingNavigation = true; const scrollableGrid = isNext ? this.getNextScrollableDown(this.grid) : this.getNextScrollableUp(this.grid); scrollableGrid.grid.verticalScrollContainer.recalcUpdateSizes(); scrollableGrid.grid.verticalScrollContainer.addScrollTop(positionInfo.offset); scrollableGrid.grid.verticalScrollContainer.chunkLoad.pipe(first()).subscribe(() => { this._pendingNavigation = false; if (cb) { cb(); } }); } else { if (cb) { cb(); } } } /** * Navigates to the specific child grid based on the array of paths leading to it * * @param pathToChildGrid Array of IPathSegments that describe the path to the child grid * each segment is described by the rowKey of the parent row and the rowIslandKey. */ navigateToChildGrid(pathToChildGrid, cb) { if (pathToChildGrid.length == 0) { if (cb) { cb(); } return; } const pathElem = pathToChildGrid.shift(); const rowKey = pathElem.rowKey; const rowIndex = this.grid.gridAPI.get_row_index_in_data(rowKey); if (rowIndex === -1) { if (cb) { cb(); } return; } // scroll to row, since it can be out of view this.performVerticalScrollToCell(rowIndex, -1, () => { this.grid.cdr.detectChanges(); // next, expand row, if it is collapsed const row = this.grid.getRowByIndex(rowIndex); if (!row.expanded) { row.expanded = true; // update sizes after expand this.grid.verticalScrollContainer.recalcUpdateSizes(); this.grid.cdr.detectChanges(); } const childGrid = this.grid.gridAPI.getChildGrid([pathElem]); if (!childGrid) { if (cb) { cb(); } return; } const positionInfo = this.getElementPosition(childGrid.nativeElement, false); this.grid.verticalScrollContainer.addScrollTop(positionInfo.offset); this.grid.verticalScrollContainer.chunkLoad.pipe(first()).subscribe(() => { childGrid.navigation.navigateToChildGrid(pathToChildGrid, cb); }); }); } /** * Moves navigation to child grid. * * @param parentRowIndex The parent row index, at which the child grid is rendered. * @param childLayoutIndex Optional. The index of the child row island to which the child grid belongs to. Uses first if not set. */ _moveToChild(parentRowIndex, visibleColIndex, isNext, childLayoutIndex, cb) { const ri = typeof childLayoutIndex !== 'number' ? this.grid.childLayoutList.first : this.grid.childLayoutList.toArray()[childLayoutIndex]; const rowId = this.grid.dataView[parentRowIndex].rowID; const pathSegment = { rowID: rowId, rowKey: rowId, rowIslandKey: ri.key }; const childGrid = this.grid.gridAPI.getChildGrid([pathSegment]); const targetIndex = isNext ? 0 : childGrid.dataView.length - 1; const targetRec = childGrid.dataView[targetIndex]; if (!targetRec) { // if no target rec, then move on in next sibling or parent childGrid.navigation.navigateInBody(targetIndex, visibleColIndex, cb); return; } if (childGrid.isChildGridRecord(targetRec)) { // if target is a child grid record should move into it. this.grid.navigation.activeNode.row = null; childGrid.navigation.activeNode = { row: targetIndex, column: this.activeNode.column }; childGrid.navigation._handleScrollInChild(targetIndex, isNext, () => { const targetLayoutIndex = isNext ? 0 : childGrid.childLayoutList.toArray().length - 1; childGrid.navigation._moveToChild(targetIndex, visibleColIndex, isNext, targetLayoutIndex, cb); }); return; } const childGridNav = childGrid.navigation; this.clearActivation(); const lastVisibleIndex = childGridNav.lastColumnIndex; const columnIndex = visibleColIndex <= lastVisibleIndex ? visibleColIndex : lastVisibleIndex; childGridNav.activeNode = { row: targetIndex, column: columnIndex }; childGrid.tbody.nativeElement.focus({ preventScroll: true }); this._pendingNavigation = false; childGrid.navigation._handleScrollInChild(targetIndex, isNext, () => { childGrid.navigateTo(targetIndex, columnIndex, cb); }); } /** * Moves navigation back to parent grid. * * @param rowIndex */ _moveToParent(isNext, columnIndex, cb) { const indexInParent = this.grid.childRow.index; const hasNextTarget = this.hasNextTarget(this.grid.parent, indexInParent, isNext); if (!hasNextTarget) { return; } this.clearActivation(); const targetRowIndex = isNext ? indexInParent + 1 : indexInParent - 1; const lastVisibleIndex = this.grid.parent.navigation.lastColumnIndex; const nextColumnIndex = columnIndex <= lastVisibleIndex ? columnIndex : lastVisibleIndex; this._pendingNavigation = true; const cbFunc = (args) => { this._pendingNavigation = false; cb(args); args.target.grid.tbody.nativeElement.focus(); }; this.grid.parent.navigation.navigateInBody(targetRowIndex, nextColumnIndex, cbFunc); } /** * Gets information on the row position relative to the root grid view port. * Returns whether the row is in view and its offset. * * @param rowObj * @param isNext */ getPositionInfo(row, isNext) { // XXX: Fix type let rowElem = row.nativeElement; if (row.layout) { const childLayoutKeys = this.grid.childLayoutKeys; const riKey = isNext ? childLayoutKeys[0] : childLayoutKeys[childLayoutKeys.length - 1]; const pathSegment = { rowID: row.data.rowID, rowKey: row.data.rowID, rowIslandKey: riKey }; const childGrid = this.grid.gridAPI.getChildGrid([pathSegment]); rowElem = childGrid.tfoot.nativeElement; } return this.getElementPosition(rowElem, isNext); } getElementPosition(element, isNext) { // Special handling for scenarios where there is css transformations applied that affects scale. // getBoundingClientRect().height returns size after transformations // element.offsetHeight returns size without any transformations // get the ratio to figure out if anything has applied transformations const scaling = element.getBoundingClientRect().height / element.offsetHeight; const gridBottom = this._getMinBottom(this.grid); const diffBottom = element.getBoundingClientRect().bottom - gridBottom; const gridTop = this._getMaxTop(this.grid); const diffTop = element.getBoundingClientRect().bottom - element.getBoundingClientRect().height - gridTop; // Adding Math.Round because Chrome has some inconsistencies when the page is zoomed const isInView = isNext ? Math.round(diffBottom) <= 0 : Math.round(diffTop) >= 0; const calcOffset = isNext ? diffBottom : diffTop; return { inView: isInView, offset: calcOffset / scaling }; } /** * Gets closest element by its tag name. * * @param sourceElem The element from which to start the search. * @param targetTag The target element tag name, for which to search. */ getClosestElemByTag(sourceElem, targetTag) { let result = sourceElem; while (result !== null && result.nodeType === 1) { if (result.tagName.toLowerCase() === targetTag.toLowerCase()) { return result; } result = result.parentNode; } return null; } clearActivation() { // clear if previous activation exists. if (this.activeNode && Object.keys(this.activeNode).length) { this.activeNode = Object.assign({}); } } hasNextTarget(grid, index, isNext) { const targetRowIndex = isNext ? index + 1 : index - 1; const hasTargetRecord = !!grid.dataView[targetRowIndex]; if (hasTargetRecord) { return true; } else { let hasTargetRecordInParent = false; if (grid.parent) { const indexInParent = grid.childRow.index; hasTargetRecordInParent = this.hasNextTarget(grid.parent, indexInParent, isNext); } return hasTargetRecordInParent; } } /** * Gets the max top view in the current grid hierarchy. * * @param grid */ _getMaxTop(grid) { let currGrid = grid; let top = currGrid.tbody.nativeElement.getBoundingClientRect().top; while (currGrid.parent) { currGrid = currGrid.parent; const pinnedRowsHeight = currGrid.hasPinnedRecords && currGrid.isRowPinningToTop ? currGrid.pinnedRowHeight : 0; top = Math.max(top, currGrid.tbody.nativeElement.getBoundingClientRect().top + pinnedRowsHeight); } return top; } /** * Gets the min bottom view in the current grid hierarchy. * * @param grid */ _getMinBottom(grid) { let currGrid = grid; let bottom = currGrid.tbody.nativeElement.getBoundingClientRect().bottom; while (currGrid.parent) { currGrid = currGrid.parent; const pinnedRowsHeight = currGrid.hasPinnedRecords && !currGrid.isRowPinningToTop ? currGrid.pinnedRowHeight : 0; bottom = Math.min(bottom, currGrid.tbody.nativeElement.getBoundingClientRect().bottom - pinnedRowsHeight); } return bottom; } /** * Finds the next grid that allows scrolling down. * * @param grid The grid from which to begin the search. */ getNextScrollableDown(grid) { let currGrid = grid.parent; if (!currGrid) { return { grid, prev: null }; } let scrollTop = currGrid.verticalScrollContainer.scrollPosition; let scrollHeight = currGrid.verticalScrollContainer.getScroll().scrollHeight; let nonScrollable = scrollHeight === 0 || Math.round(scrollTop + currGrid.verticalScrollContainer.igxForContainerSize) === scrollHeight; let prev = grid; while (nonScrollable && currGrid.parent !== null) { prev = currGrid; currGrid = currGrid.parent; scrollTop = currGrid.verticalScrollContainer.scrollPosition; scrollHeight = currGrid.verticalScrollContainer.getScroll().scrollHeight; nonScrollable = scrollHeight === 0 || Math.round(scrollTop + currGrid.verticalScrollContainer.igxForContainerSize) === scrollHeight; } return { grid: currGrid, prev }; } /** * Finds the next grid that allows scrolling up. * * @param grid The grid from which to begin the search. */ getNextScrollableUp(grid) { let currGrid = grid.parent; if (!currGrid) { return { grid, prev: null }; } let nonScrollable = currGrid.verticalScrollContainer.scrollPosition === 0; let prev = grid; while (nonScrollable && currGrid.parent !== null) { prev = currGrid; currGrid = currGrid.parent; nonScrollable = currGrid.verticalScrollContainer.scrollPosition === 0; } return { grid: currGrid, prev }; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxHierarchicalGridNavigationService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxHierarchicalGridNavigationService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxHierarchicalGridNavigationService, decorators: [{ type: Injectable }] }); const hierarchicalTransactionServiceFactory = () => new IgxTransactionService(); const IgxHierarchicalTransactionServiceFactory = { provide: IgxGridTransaction, useFactory: hierarchicalTransactionServiceFactory }; /* blazorIndirectRender blazorComponent omitModule wcSkipComponentSuffix */ class IgxHierarchicalGridBaseDirective extends IgxGridBaseDirective { constructor() { super(...arguments); this.gridAPI = inject(IGX_GRID_SERVICE_BASE); this.navigation = inject(IgxHierarchicalGridNavigationService); /** * Gets/Sets whether the expand/collapse all button in the header should be rendered. * * @remarks * The default value is false. * @example * ```html * <igx-hierarchical-grid #grid [data]="localData" [showExpandAll]="true"> * </igx-hierarchical-grid> * ``` */ this.showExpandAll = false; /** * Emitted when a new chunk of data is loaded from virtualization. * * @example * ```typescript * <igx-hierarchical-grid [id]="'igx-grid-1'" [data]="Data" [autoGenerate]="true" (dataPreLoad)="handleEvent()"> * </igx-hierarchical-grid> * ``` */ this.dataPreLoad = new EventEmitter(); /** @hidden @internal */ this.batchEditingChange = new EventEmitter(); } /** @hidden @internal */ get type() { return 'hierarchical'; } /* blazorSuppress */ /** * Gets the outlet used to attach the grid's overlays to. * * @remarks * If set, returns the outlet defined outside the grid. Otherwise returns the grid's internal outlet directive. */ get outlet() { return this.rootGrid ? this.rootGrid.resolveOutlet() : this.resolveOutlet(); } /* blazorSuppress */ /** * Sets the outlet used to attach the grid's overlays to. */ set outlet(val) { this._userOutletDirective = val; } get batchEditing() { return this._batchEditing; } set batchEditing(val) { if (val !== this._batchEditing) { delete this._transactions; this.switchTransactionService(val); this.subscribeToTransactions(); this.batchEditingChange.emit(val); this._batchEditing = val; } } /** * @hidden */ createColumnsList(cols) { const columns = []; const topLevelCols = cols.filter(c => c.level === 0); topLevelCols.forEach((col) => { col.grid = this; const ref = this._createColumn(col); ref.changeDetectorRef.detectChanges(); columns.push(ref.instance); }); const result = flatten(columns); this.updateColumns(result); this.initPinning(); result.forEach(col => { this.columnInit.emit(col); }); const mirror = reflectComponentType(IgxColumnComponent); const outputs = mirror.outputs.filter(o => o.propName !== 'columnChange'); outputs.forEach(output => { this.columns.forEach(column => { if (column[output.propName]) { column[output.propName].pipe(takeUntil(column.destroy$)).subscribe((args) => { const rowIslandColumn = this.parentIsland.columnList.find((col) => col.field ? col.field === column.field : col.header === column.header); rowIslandColumn[output.propName].emit({ args, owner: this }); }); } }); }); } _createColumn(col) { let ref; if (col instanceof IgxColumnGroupComponent) { ref = this._createColGroupComponent(col); } else { ref = this._createColComponent(col); } return ref; } _createColGroupComponent(col) { const ref = createComponent(IgxColumnGroupComponent, { environmentInjector: this.envInjector, elementInjector: this.injector }); ref.changeDetectorRef.detectChanges(); const mirror = reflectComponentType(IgxColumnGroupComponent); mirror.inputs.forEach((input) => { const propName = input.propName; ref.instance[propName] = col[propName]; }); if (col.children.length > 0) { const newChildren = []; col.children.forEach(child => { const newCol = this._createColumn(child).instance; newCol.parent = ref.instance; newChildren.push(newCol); }); ref.instance.children.reset(newChildren); ref.instance.children.notifyOnChanges(); } return ref; } _createColComponent(col) { const ref = createComponent(IgxColumnComponent, { environmentInjector: this.envInjector, elementInjector: this.injector }); const mirror = reflectComponentType(IgxColumnComponent); mirror.inputs.forEach((input) => { const propName = input.propName; if (!(col[propName] instanceof IgxSummaryOperand)) { ref.instance[propName] = col[propName]; } else { ref.instance[propName] = col[propName].constructor; } }); ref.instance.validators = col.validators; return ref; } getGridsForIsland(rowIslandID) { return this.gridAPI.getChildGridsForRowIsland(rowIslandID); } getChildGrid(path) { if (!path) { return; } return this.gridAPI.getChildGrid(path); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxHierarchicalGridBaseDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "21.0.2", type: IgxHierarchicalGridBaseDirective, isStandalone: true, inputs: { hasChildrenKey: "hasChildrenKey", showExpandAll: ["showExpandAll", "showExpandAll", booleanAttribute] }, outputs: { dataPreLoad: "dataPreLoad" }, usesInheritance: true, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxHierarchicalGridBaseDirective, decorators: [{ type: Directive }], propDecorators: { hasChildrenKey: [{ type: Input }], showExpandAll: [{ type: Input, args: [{ transform: booleanAttribute }] }], dataPreLoad: [{ type: Output }] } }); const flatten = (arr) => { let result = []; arr.forEach(el => { result.push(el); if (el.children) { result = result.concat(flatten(el.children.toArray())); } }); return result; }; class IgxRowIslandAPIService { constructor() { this.change = new Subject(); this.state = new Map(); this.destroyMap = new Map(); this.childRowIslands = new Map(); this.childGrids = new Map(); } register(rowIsland) { this.state.set(rowIsland.id, rowIsland); this.destroyMap.set(rowIsland.id, new Subject()); } unsubscribe(rowIsland) { this.state.delete(rowIsland.id); } get(id) { return this.state.get(id); } unset(id) { this.state.delete(id); this.destroyMap.delete(id); } reset(oldId, newId) { const destroy = this.destroyMap.get(oldId); const rowIsland = this.get(oldId); this.unset(oldId); if (rowIsland) { this.state.set(newId, rowIsland); } if (destroy) { this.destroyMap.set(newId, destroy); } } registerChildRowIsland(rowIsland) { this.childRowIslands.set(rowIsland.key, rowIsland); this.destroyMap.set(rowIsland.key, new Subject()); } unsetChildRowIsland(rowIsland) { this.childRowIslands.delete(rowIsland.key); this.destroyMap.delete(rowIsland.key); } getChildRowIsland(rowIslandKey) { return this.childRowIslands.get(rowIslandKey); } registerChildGrid(parentRowID, grid) { this.childGrids.set(parentRowID, grid); } getChildGrids(inDepth) { let allChildren = []; this.childGrids.forEach((grid) => { allChildren.push(grid); }); if (inDepth) { this.childRowIslands.forEach((layout) => { allChildren = allChildren.concat(layout.rowIslandAPI.getChildGrids(inDepth)); }); } return allChildren; } getChildGridByID(rowID) { return this.childGrids.get(rowID); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxRowIslandAPIService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxRowIslandAPIService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxRowIslandAPIService, decorators: [{ type: Injectable }] }); /* blazorCopyInheritedMembers */ /* blazorElement */ /* wcElementTag: igc-row-island */ /* blazorIndirectRender */ /* jsonAPIManageCollectionInMarkup */ /* jsonAPIManageItemInMarkup */ /* mustUseNGParentAnchor */ /* additionalIdentifier: ChildDataKey */ /* contentParent: RowIsland */ /* contentParent: HierarchicalGrid */ /** * Row island * * @igxModule IgxHierarchicalGridModule * @igxParent IgxHierarchicalGridComponent, IgxRowIslandComponent * */ class IgxRowIslandComponent extends IgxHierarchicalGridBaseDirective { constructor() { super(...arguments); this.rowIslandAPI = inject(IgxRowIslandAPIService); /** * @hidden */ this.children = new QueryList(); /* contentChildren */ /* blazorInclude */ /* blazorTreatAsCollection */ /* blazorCollectionName: RowIslandCollection */ /** * @hidden @internal */ this.childLayoutList = new QueryList(); /** * @hidden */ this.childColumns = new QueryList(); /** * @hidden */ this.layoutChange = new EventEmitter(); /** * Event emitted when a grid is being created based on this row island. * ```html * <igx-hierarchical-grid [data]="Data" [autoGenerate]="true"> * <igx-row-island [key]="'childData'" (gridCreated)="gridCreated($event)" #rowIsland> * <!-- ... --> * </igx-row-island> * </igx-hierarchical-grid> * ``` * * @memberof IgxRowIslandComponent */ this.gridCreated = new EventEmitter(); /** * Emitted after a grid is being initialized for this row island. * The emitting is done in `ngAfterViewInit`. * ```html * <igx-hierarchical-grid [data]="Data" [autoGenerate]="true"> * <igx-row-island [key]="'childData'" (gridInitialized)="gridInitialized($event)" #rowIsland> * <!-- ... --> * </igx-row-island> * </igx-hierarchical-grid> * ``` * * @memberof IgxRowIslandComponent */ this.gridInitialized = new EventEmitter(); /** * @hidden */ this.initialChanges = []; /** * @hidden */ this.rootGrid = null; //#region inert, not-a-grid component /** @hidden @internal */ this.tabindex = -1; /** @hidden @internal */ this.hostRole = null; this.baseClass = null; this.displayStyle = 'none'; this.templateRows = null; this.layout_id = `igx-row-island-`; this.isInit = false; this._childColumns = []; } /* blazorInclude,wcInclude TODO: Move to Elements-only component */ /** * Sets the key of the row island by which child data would be taken from the row data if such is provided. * @hidden @internal */ get childDataKey() { return this.key; } /* blazorInclude,wcInclude */ set childDataKey(value) { this.key = value; } /* csSuppress */ /** * Sets/Gets the toolbar template for each child grid created from this row island. */ get toolbarTemplate() { return this._toolbarTemplate || this.toolbarDirectiveTemplate; } set toolbarTemplate(template) { this._toolbarTemplate = template; } /* csSuppress */ /** * Sets/Gets the paginator template for each child grid created from this row island. */ get paginatorTemplate() { return this._paginatorTemplate || this.paginatorDirectiveTemplate; } set paginatorTemplate(template) { this._paginatorTemplate = template; } /** @hidden */ get hiddenColumnsCount() { return 0; } /** @hidden */ get pinnedColumnsCount() { return 0; } /** @hidden */ get lastSearchInfo() { return null; } /** @hidden */ get filteredData() { return []; } /** @hidden */ get filteredSortedData() { return []; } /** @hidden */ get virtualizationState() { return null; } /** @hidden */ get pinnedColumns() { return []; } /** @hidden */ get unpinnedColumns() { return []; } /** @hidden */ get visibleColumns() { return []; } /** @hidden */ get dataView() { return []; } /** @hidden @internal */ get hostWidth() { return null; } /** * Sets if all immediate children of the grids for this `IgxRowIslandComponent` should be expanded/collapsed. * ```html * <igx-hierarchical-grid [data]="Data" [autoGenerate]="true"> * <igx-row-island [key]="'childData'" [expandChildren]="true" #rowIsland> * <!-- ... --> * </igx-row-island> * </igx-hierarchical-grid> * ``` * * @memberof IgxRowIslandComponent */ set expandChildren(value) { this._defaultExpandState = value; this.rowIslandAPI.getChildGrids().forEach((grid) => { if (this.document.body.contains(grid.nativeElement)) { // Detect changes right away if the grid is visible grid.expandChildren = value; grid.cdr.detectChanges(); } else { // Else defer the detection on changes when the grid gets into view for performance. grid.updateOnRender = true; } }); } /** * Gets if all immediate children of the grids for this `IgxRowIslandComponent` have been set to be expanded/collapsed. * ```typescript * const expanded = this.rowIsland.expandChildren; * ``` * * @memberof IgxRowIslandComponent */ get expandChildren() { return this._defaultExpandState; } /** * @hidden */ get id() { const pId = this.parentId ? this.parentId.substring(this.parentId.indexOf(this.layout_id) + this.layout_id.length) + '-' : ''; return this.layout_id + pId + this.key; } /** * @hidden */ get parentId() { return this.parentIsland ? this.parentIsland.id : null; } /** * @hidden */ get level() { let ptr = this.parentIsland; let lvl = 0; while (ptr) { lvl++; ptr = ptr.parentIsland; } return lvl + 1; } /** * @hidden */ ngOnInit() { this.filteringService.grid = this; this.rootGrid = this.gridAPI.grid; this.rowIslandAPI.rowIsland = this; this.ri_columnListDiffer = this.differs.find([]).create(null); } /** * @hidden */ ngAfterContentInit() { this.updateChildren(); this.children.notifyOnChanges(); this.children.changes.pipe(takeUntil(this.destroy$)) .subscribe(() => { this.updateChildren(); // update existing grids since their child ri have been changed. this.rowIslandAPI.getChildGrids(false).forEach(grid => { grid.onRowIslandChange(this.children); }); }); const nestedColumns = this.children.map((layout) => layout.columnList.toArray()); const colsArray = [].concat.apply([], nestedColumns); const topCols = this.columnList.filter((item) => colsArray.indexOf(item) === -1); this._childColumns = topCols; this.updateColumns(this._childColumns); this.columnList.changes.pipe(takeUntil(this.destroy$)).subscribe(() => { Promise.resolve().then(() => { this.updateColumnList(); }); }); // handle column changes so that they are passed to child grid instances when columnChange is emitted. this.ri_columnListDiffer.diff(this.childColumns); this._childColumns.forEach(x => x.columnChange.pipe(takeUntil(x.destroy$)).subscribe(() => this.updateColumnList())); this.childColumns.changes.pipe(takeUntil(this.destroy$)).subscribe((change) => { const diff = this.ri_columnListDiffer.diff(change); if (diff) { diff.forEachAddedItem((record) => { record.item.columnChange.pipe(takeUntil(record.item.destroy$)).subscribe(() => this.updateColumnList()); }); } }); if (this.actionStrip) { this.actionStrip.menuOverlaySettings.outlet = this.outlet; } } /** * @hidden */ ngAfterViewInit() { this.rowIslandAPI.register(this); if (this.parentIsland) { this.parentIsland.rowIslandAPI.registerChildRowIsland(this); } else { this.rootGrid.gridAPI.registerChildRowIsland(this); } this._init = false; // Create the child toolbar if the parent island has a toolbar definition this.gridCreated.pipe(pluck('grid'), takeUntil(this.destroy$)).subscribe(grid => { grid.rendered$.pipe(first(), filter(() => !!this.toolbarTemplate)) .subscribe(() => grid.toolbarOutlet.createEmbeddedView(this.toolbarTemplate, { $implicit: grid }, { injector: grid.toolbarOutlet.injector })); grid.rendered$.pipe(first(), filter(() => !!this.paginatorTemplate)) .subscribe(() => { this.rootGrid.paginatorList.changes.pipe(takeUntil(this.destroy$)).subscribe((changes) => { changes.forEach(p => { if (p.nativeElement.offsetParent?.id === grid.id) { // Optimize update only for those grids that have related changed paginator. grid.setUpPaginator(); } }); }); grid.paginatorOutlet.createEmbeddedView(this.paginatorTemplate, { $implicit: grid }); }); }); } /** * @hidden */ ngOnChanges(changes) { this.layoutChange.emit(changes); if (!this.isInit) { this.initialChanges.push(changes); } } /** * @hidden */ ngOnDestroy() { // Override the base destroy because we have not rendered anything to use removeEventListener on this.destroy$.next(true); this.destroy$.complete(); this._destroyed = true; this.rowIslandAPI.unset(this.id); if (this.parentIsland) { this.getGridsForIsland(this.key).forEach(grid => { this.cleanGridState(grid); grid.gridAPI.unsetChildRowIsland(this); }); this.parentIsland.rowIslandAPI.unsetChildRowIsland(this); } else { this.rootGrid.gridAPI.unsetChildRowIsland(this); this.cleanGridState(this.rootGrid); } } /** * @hidden */ reflow() { } /** * @hidden */ calculateGridHeight() { } /** * @hidden */ calculateGridWidth() { } updateColumnList() { const nestedColumns = this.children.map((layout) => layout.columnList.toArray()); const colsArray = [].concat.apply([], nestedColumns); const topCols = this.columnList.filter((item) => { if (colsArray.indexOf(item) === -1) { /* Reset the default width of the columns that come into this row island, because the root catches them first during the detectChanges() and sets their defaultWidt