UNPKG

@nova-ui/dashboards

Version:

Nova Dashboards is a framework designed to provide feature developers with a common solution for presenting data coming from various sources within a single view, as well as a set of predefined widget visualizations that are 100% configuration-driven and

612 lines 120 kB
// © 2022 SolarWinds Worldwide, LLC. All rights reserved. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to // deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or // sell copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import { CdkVirtualScrollViewport } from "@angular/cdk/scrolling"; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, HostBinding, Inject, Input, NgZone, Optional, QueryList, ViewChild, ViewChildren, } from "@angular/core"; import get from "lodash/get"; import isEqual from "lodash/isEqual"; import omit from "lodash/omit"; // eslint-disable-next-line import/no-deprecated import { BehaviorSubject, merge, of, Subject } from "rxjs"; // eslint-disable-next-line import/no-deprecated import { filter, map, take, takeUntil, tap } from "rxjs/operators"; import { DEFAULT_INTERACTIVE_ELEMENTS, EventBus, LoggerService, PaginatorComponent, SelectorService, SorterDirection, TableComponent, TableRowComponent, TableSelectionMode, VirtualViewportManager, } from "@nova-ui/bits"; import { PizzagnaService } from "../../pizzagna/services/pizzagna.service"; import { TableFormatterRegistryService } from "../../services/table-formatter-registry.service"; import { CHANGE_SELECTION, INTERACTION, REFRESH, SELECTED_ITEMS, SELECTION, SET_NEXT_PAGE, WIDGET_READY, WIDGET_RESIZE, } from "../../services/types"; import { WidgetConfigurationService } from "../../services/widget-configuration.service"; import { DATA_SOURCE, PIZZAGNA_EVENT_BUS, PizzagnaLayer, WellKnownDataSourceFeatures, } from "../../types"; import { PaginatorFeatureAddonService } from "./addons/paginator-feature-addon.service"; import { SearchFeatureAddonService } from "./addons/search-feature-addon.service"; import { VirtualScrollFeatureAddonService } from "./addons/virtual-scroll-feature-addon.service"; import { ScrollType, } from "./types"; import * as i0 from "@angular/core"; import * as i1 from "../../services/widget-configuration.service"; import * as i2 from "../../pizzagna/services/pizzagna.service"; import * as i3 from "@nova-ui/bits"; import * as i4 from "./addons/search-feature-addon.service"; import * as i5 from "./addons/paginator-feature-addon.service"; import * as i6 from "./addons/virtual-scroll-feature-addon.service"; import * as i7 from "../../services/table-formatter-registry.service"; import * as i8 from "@angular/common"; import * as i9 from "@angular/cdk/portal"; import * as i10 from "../../pizzagna/directives/component-portal/component-portal.directive"; import * as i11 from "@angular/cdk/scrolling"; import * as i12 from "./delayed-mouse-presence-detection.directive"; /** * @ignore */ export class TableWidgetComponent { static { this.lateLoadKey = "TableWidgetComponent"; } constructor(eventBus, dataSource, widgetConfigurationService, changeDetector, pizzagnaService, viewportManager, zone, el, logger, searchAddon, paginatorAddon, virtualScrollAddon, formattersRegistryService, selectorService) { this.eventBus = eventBus; this.dataSource = dataSource; this.widgetConfigurationService = widgetConfigurationService; this.changeDetector = changeDetector; this.pizzagnaService = pizzagnaService; this.viewportManager = viewportManager; this.zone = zone; this.el = el; this.logger = logger; this.searchAddon = searchAddon; this.paginatorAddon = paginatorAddon; this.virtualScrollAddon = virtualScrollAddon; this.formattersRegistryService = formattersRegistryService; this.selectorService = selectorService; // 70 px stands for widget header and margins this.indentFromTop = 70; this.sortable = true; this.delayedMousePresenceDetectionEnabled = true; this.tableData = []; this.columns = []; this.columnsWidthMap = new Map(); this.scrollType = ScrollType.virtual; this.isSearchEnabled = false; this.searchTerm$ = new Subject(); this.onDestroy$ = new Subject(); this.tableUpdate$ = new Subject(); this.mousePresent$ = new BehaviorSubject(false); this.rowHeight = 24; this.selection = { isAllPages: false, include: [], exclude: [], }; this.totalPages = 0; this.lastPageFetched = 0; this.isBusy = this.lastPageFetched !== this.totalPages; this.sortableSet = {}; this.defaultColumnAlignment = "left"; this.idle = false; this._scrollBuffer = 0; } get headerTooltipsEnabled() { // Note: If tooltip state is not provided treat is as true; return this.configuration.headerTooltipsEnabled ?? true; } set scrollBuffer(value) { if (value > 100 || value < 0) { throw new Error("Invalid scroll buffer provided. Required range 1..100"); } this._scrollBuffer = value; } get range() { return this.getTableScrollRange(); } set range(value) { this._range = value; } get interactive() { return (this.configuration?.interactive ?? this.dataSource?.features?.getFeatureConfig(WellKnownDataSourceFeatures.Interactivity)?.enabled ?? false); } get clickableRow() { const configuration = this.configuration?.selectionConfiguration; return (!!configuration?.enabled && (configuration.clickableRow || configuration.selectionMode === TableSelectionMode.Single)); } get hasVirtualScroll() { return this.scrollType === ScrollType.virtual; } get hasPaginator() { return this.scrollType === ScrollType.paginator; } ngOnChanges(changes) { if (changes.dataFields) { if (this.dataFields?.length) { this.setSortableSet(); } } if (changes.widgetData || changes.configuration || changes.dataFields) { this.updateTable(); // Note: CDK Viewport does not have a proper mechanism to identify the changes and trigger checkViewportSize // we have to do it manually. Using optional chaining because vscrollViewport can be unavailable at initialization. // Related issue: https://github.com/angular/components/issues/10117 this.vscrollViewport?.checkViewportSize(); } if (changes.configuration) { this.resolveSortBy(changes); this.resolveScrollType(changes); this.resolveSearch(changes); } if (changes.totalItems) { this.totalPages = Math.floor((this.totalItems ?? 0) / this.range); } } resolveScrollType(changes) { // Since removing "hasVirtualScroll" from configuration would cause breaking changes, it is used as primary source of truth const newHasVirtualScroll = get(changes, "configuration.currentValue.hasVirtualScroll", true); const scrollType = get(changes, "configuration.currentValue.scrollType", ScrollType.virtual); this.scrollType = newHasVirtualScroll ? ScrollType.virtual : scrollType; this.initPrefetchAddon(); if (this.scrollTypeChanged(changes.configuration)) { this.dataSource.applyFilters(); } } ngOnInit() { if (this.dataSource) { // Since the sortFilter is not initialized, we have to retrieve and set the correct filter value from the configuration before registering it this.setSortFilter(); this.registerSorter(); } } ngAfterViewInit() { if (!this.dataSource) { return; } this.dataSource.busy ?.pipe( // eslint-disable-next-line import/no-deprecated tap((isBusy) => (this.isBusy = isBusy)), takeUntil(this.onDestroy$)) .subscribe(); this.initPrefetchAddon(); this.searchAddon.initWidget(this); const tableHeightChanged$ = this.eventBus .getStream(WIDGET_RESIZE) .pipe(filter((event) => event.payload?.widgetId?.toString() === this.widgetConfigurationService.getWidget().id), // eslint-disable-next-line import/no-deprecated map((event) => event.payload?.height ?? 0)); // subscribing to widget resize event from dashboard and update virtual scroll viewport size tableHeightChanged$ .pipe(takeUntil(this.onDestroy$)) .subscribe((height) => { this.tableContainerHeight = height - this.indentFromTop; this.vscrollViewport?.checkViewportSize(); this.changeDetector.detectChanges(); }); // Note: Secondary stream used to trigger widget ready event when table is properly displayed and we can start // loading data for our virtual scroll // setTimeout is needed because this.el.nativeElement.getBoundingClientRect().height is 0 when it switches components from // fallback component to table widget and it needs to have height setTimeout(() => { // eslint-disable-next-line import/no-deprecated merge(of(this.range * this.rowHeight), tableHeightChanged$, of(this.el.nativeElement.getBoundingClientRect().height)) .pipe(filter((value) => !!value), take(1), // eslint-disable-next-line import/no-deprecated tap((value) => { this.tableWidgetHeight = value; if (this.hasVirtualScroll) { this.virtualScrollAddon.subscribeToVirtualScroll(); } this.eventBus.getStream(WIDGET_READY).next({}); })) .subscribe(); }); this.eventBus .getStream(CHANGE_SELECTION) .pipe(takeUntil(this.onDestroy$)) .subscribe(({ payload: selection }) => { if (selection) { this.selection = selection; } else { this.logger.warn(`widget CHANGE_SELECTION, ${this.componentId} has received the incorrect selection object`); } }); } ngOnDestroy() { this.onDestroy$.next(); this.onDestroy$.complete(); this.tableUpdate$.complete(); this.searchTerm$?.complete(); // erase the error status when this component gets destroyed to prevent an error status leak when changing data sources this.pizzagnaService.setProperty({ componentId: "bodyContent", propertyPath: ["fallbackKey"], pizzagnaKey: PizzagnaLayer.Data, }, undefined); } /** Checks if table should be displayed */ shouldDisplayTable() { const columnsCondition = this.columns.length > 0; const dataCondition = this.tableData?.length > 0; return this.isSearchEnabled || (this.hasVirtualScroll && (this.isBusy || this.idle)) ? columnsCondition : columnsCondition && dataCondition; } dataTrackBy() { const configuration = this?.configuration?.selectionConfiguration; const trackByProperty = (configuration?.enabled && configuration.trackByProperty) || "id"; // we are returning a function here because the scope of "this" changes once it's passed // to the table component and are therefore unable to access the configuration in its body return (index, item) => { // selection uses a set of the columns in prior of selection correct data id we need to use record if (item["__record"]) { item = item["__record"]; } return item ? item[trackByProperty] : index; }; } columnTrackBy(index, item) { return item.id; } /** * Handles updating of columns. * @param configuration */ updateColumns(configuration) { this.headers = configuration.columns .filter((item) => item.isActive) .map((item) => item.id); const allColumnsHaveWidthSpecified = configuration.columns.every((column) => Boolean(column.width)); const columns = configuration.columns.map((column, index, array) => { this.columnsWidthMap.set(column.id, column.width); if (allColumnsHaveWidthSpecified) { const lastColumn = array[array.length - 1]; this.columnsWidthMap.set(lastColumn.id, undefined); } return { ...column, sortable: this.sortableSet[column?.formatter?.properties?.dataFieldIds?.value] ?? true, }; }); if (columns.length > 0 && allColumnsHaveWidthSpecified) { this.logger.warn(`Cannot set width for all columns. Resetting last column width.`); } if (this.columns.length !== columns.length) { this.columns = [...columns]; } else { // This Object.assign is important. It helps to track changes in column header names this.columns.forEach((c, i) => { Object.assign(c, columns[i], {}); }); } } /** * Takes widgetData from back-end, columns which are provided in table widget config and dataFields and maps them to data-format * which is acceptable by table component. * Also it can merge multiple data fields into one column. If you provide more than one dataFieldId in your * widget config, they will be merged to an object with needed data. This data can be passed to formatter and displayed * in one column. * @param widgetData * @param columns * @param dataFields * @returns any[] */ mapTableData(widgetData, columns, dataFields) { if (!dataFields || dataFields.length === 0) { this.logger.warn("There are no data fields defined, so table data cannot be displayed."); return []; } return widgetData.map((record) => { const row = columns.reduce((result, column) => { const dataFieldIds = column.formatter?.properties?.dataFieldIds; if (!dataFieldIds) { return result; } const data = Object.keys(dataFieldIds).reduce((mapping, next) => { mapping[next] = record[dataFieldIds[next]]; return mapping; }, {}); result[column.id] = { data, ...omit(column.formatter?.properties, "dataFieldIds"), }; return result; }, {}); // we want to include original record for row interaction row.__record = record; return row; }); } /** * Handles change of sorting. Gets sorted column from columns array. * @param event */ onSortOrderChanged(event) { this.sortedColumn = event; const columnToSort = this.columns.find((column) => column.id === event.sortBy); if (!columnToSort) { return; } const dataFieldIds = columnToSort.formatter?.properties?.dataFieldIds; const sortField = dataFieldIds?.value; if (!sortField) { return; } const newSortFilter = { sortBy: sortField, direction: event.direction, }; if (isEqual(newSortFilter, this.sortFilter)) { return; } this.sortFilter = newSortFilter; // Note: Since we're relying on the CDK Viewport internal logic to perform data load // we have to move to the start of the range for dataSource to take correct range, // if we will keep old data user can think (NUI-5333) that data is already reloaded. // So we're flushing the old data and waiting for new one to avoid confusion. this.flushTableData(); this.eventBus.getStream(REFRESH).next({}); } onSelectionChange(event) { this.selection = event; this.eventBus.getStream(SELECTED_ITEMS).next({ payload: this.selectorService.getSelectedItems(this.selection, this.widgetData, (a, b) => this.dataTrackBy()(0, a) === b), id: this.componentId, }); this.eventBus.getStream(SELECTION).next({ payload: this.selection }); } onInteraction(row, event) { if (!this.interactive || this.configuration.selectionConfiguration?.enabled) { // allowing interactive and selection at the same time would trigger both // events when clicking on a row (not checkbox/radio) return; } const ignoredSelectors = this.configuration.interactionIgnoredSelectors || DEFAULT_INTERACTIVE_ELEMENTS; // avoid emitting events when an ignored element was clicked if (event.target.closest(ignoredSelectors.join(","))) { return; } this.eventBus .getStream(INTERACTION) .next({ payload: { data: row.__record } }); } onSearchInputChanged(searchTerm) { this.searchValue = searchTerm; this.searchTerm$.next(""); this.selection = { include: [], exclude: [], isAllPages: false, }; } getColumnAlignment(column) { // Note: In case we have provided formatters by old manner (via config) // we don't have to proceed with calculations if (!column?.formatter?.componentType || this.formattersRegistryService.isEmpty) { return this.defaultColumnAlignment; } // Note: We don't want to invoke getFormattersMap() on every change detection cycle // but only when it was changed if (this.formatters?.version !== this.formattersRegistryService.stateVersion) { this.formatters = { // Transforming array into map items: this.formattersRegistryService.getItems().reduce((prev, next) => ({ ...prev, [next.componentType]: next, }), {}), version: this.formattersRegistryService.stateVersion, }; } return (this.formatters.items[column.formatter.componentType]?.alignment || this.defaultColumnAlignment); } setSortFilter() { if (this.configuration) { const sortBy = this.configuration.sorterConfiguration?.sortBy; const columnValue = this.configuration.columns?.find((column) => column.id === sortBy)?.formatter?.properties?.dataFieldIds?.value; this.sortFilter = { direction: this.configuration.sorterConfiguration ?.descendantSorting ? SorterDirection.descending : SorterDirection.ascending, sortBy: columnValue, }; } } /** * Checks if column id has changed or if sorting order has changed * @param configuration */ isSortByUpdated(configuration) { const oldSorterConfiguration = configuration.previousValue?.sorterConfiguration; const newSorterConfiguration = configuration.currentValue.sorterConfiguration; const oldSortById = oldSorterConfiguration?.sortBy; const newSortById = newSorterConfiguration?.sortBy; if (!newSortById) { return Boolean(oldSortById); } const equalSortingState = oldSortById === newSortById && newSorterConfiguration.descendantSorting === oldSorterConfiguration.descendantSorting; return !equalSortingState; } /** * Updates table columns and maps table data. */ updateTable() { if (this.widgetData && this.dataFields && this.configuration.columns) { this.updateColumns(this.configuration); this.idle = false; this.tableData = this.mapTableData(this.widgetData, this.configuration.columns, this.dataFields); this.tableUpdate$.next(); this.changeDetector.detectChanges(); } } /** * Registers sorter filter. */ registerSorter() { this.dataSource.registerComponent({ sorter: { componentInstance: { getFilters: () => ({ type: "sorter", value: this.sortFilter, }), }, }, }); } setSortableSet() { this.sortableSet = {}; this.dataFields.forEach((dataField) => { this.sortableSet[dataField.id] = dataField.sortable ?? true; }); } flushTableData() { this.idle = true; this.tableData = []; } scrollTypeChanged(configurationChange) { return (!!configurationChange.previousValue && configurationChange.previousValue.scrollType !== configurationChange.currentValue.scrollType); } getTableScrollRange() { // Note: To work properly virtual viewport should be scrollable // to ensure that container will be scrollable we're adding 50% more items // Ex: Viewport height: 100px, itemSize: 20px => 5 (range) in current configuration the viewport // will not be scrollable/work properly because all the items fits the screen. // We're adding 50% more items and in this case, we're covered even the user provides 0% buffer. const internalBuffer = 2; const scrollBuffer = (this._scrollBuffer ?? 0) / 100 + 1; if (this._range) { // Note: In case user provided a range we don't need // to add our internal buffer. Totally relying on user calculus return Math.floor(this._range * scrollBuffer); } return Math.floor((this.tableWidgetHeight / this.rowHeight) * internalBuffer * scrollBuffer); } onPagerAction(page) { this.eventBus .getStream(SET_NEXT_PAGE) .next({ payload: page, id: this.componentId }); } initPrefetchAddon() { if (this.hasVirtualScroll) { this.virtualScrollAddon.initVirtualScroll(this); } else if (this.hasPaginator) { this.paginatorAddon.initPaginator(this); } else { this.virtualScrollAddon.initVirtualScroll(this); } } resolveSortBy(changes) { // Note: We don't have to trigger sorting in case sortable flag is false. // Note: Using true as a default sortable value to maintain backward compatibility if (this.isSortByUpdated(changes.configuration) && (changes.configuration.currentValue.sortable ?? true)) { const sortedColumn = { direction: changes.configuration.currentValue .sorterConfiguration.descendantSorting ? SorterDirection.descending : SorterDirection.ascending, sortBy: changes.configuration.currentValue.sorterConfiguration .sortBy, }; this.onSortOrderChanged(sortedColumn); } } resolveSearch(changes) { if (!isEqual(changes.configuration.currentValue ?.searchConfiguration, changes.configuration.previousValue ?.searchConfiguration)) { this.searchAddon.initWidget(this); } } /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TableWidgetComponent, deps: [{ token: PIZZAGNA_EVENT_BUS }, { token: DATA_SOURCE, optional: true }, { token: i1.WidgetConfigurationService, optional: true }, { token: i0.ChangeDetectorRef }, { token: i2.PizzagnaService }, { token: i3.VirtualViewportManager }, { token: i0.NgZone }, { token: i0.ElementRef }, { token: i3.LoggerService }, { token: i4.SearchFeatureAddonService }, { token: i5.PaginatorFeatureAddonService }, { token: i6.VirtualScrollFeatureAddonService }, { token: i7.TableFormatterRegistryService }, { token: i3.SelectorService }], target: i0.ɵɵFactoryTarget.Component }); } /** @nocollapse */ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: TableWidgetComponent, selector: "nui-table-widget", inputs: { widgetData: "widgetData", componentId: "componentId", configuration: "configuration", dataFields: "dataFields", totalItems: "totalItems", indentFromTop: "indentFromTop", sortable: "sortable", delayedMousePresenceDetectionEnabled: "delayedMousePresenceDetectionEnabled", elementClass: "elementClass", range: "range" }, host: { properties: { "class.table-widget-fullwidth": "true", "class": "this.elementClass" } }, providers: [ SearchFeatureAddonService, VirtualScrollFeatureAddonService, PaginatorFeatureAddonService, ], viewQueries: [{ propertyName: "table", first: true, predicate: ["widgetTable"], descendants: true }, { propertyName: "paginator", first: true, predicate: ["paginator"], descendants: true }, { propertyName: "vscrollViewport", first: true, predicate: CdkVirtualScrollViewport, descendants: true }, { propertyName: "tableRows", predicate: TableRowComponent, descendants: true, read: ElementRef }], usesOnChanges: true, ngImport: i0, template: "<div\n class=\"h-100 d-flex justify-content-center align-items-center\"\n *ngIf=\"!shouldDisplayTable()\"\n>\n <nui-image image=\"no-data-to-show\"></nui-image>\n</div>\n<!-- h-100 gets added only when there's data to show to prevent an extra scrollbar when the table is empty -->\n<!-- Using invisible class instead of *ngIf as an non-virtual-scroll breakable way to hide the content. -->\n<div\n class=\"nui-table-widget d-flex flex-column\"\n [ngClass]=\"shouldDisplayTable() ? 'h-100' : 'invisible'\"\n>\n <div class=\"mr-2 mb-2 flex-row\" *ngIf=\"isSearchEnabled\">\n <nui-search\n class=\"unlimited-width d-flex justify-content-end flex-grow-1\"\n [value]=\"searchValue\"\n (inputChange)=\"onSearchInputChanged($event)\"\n (cancel)=\"onSearchInputChanged($event)\"\n >\n </nui-search>\n </div>\n <div\n class=\"flex-row h-100 nui-table-widget-container\"\n [nuiDelayedMousePresenceDetection]=\"\n delayedMousePresenceDetectionEnabled\n \"\n [delay]=\"configuration.scrollActivationDelayMs\"\n [mousePresentSubject]=\"mousePresent$\"\n >\n <!-- Note: Using inline style to let tableStickyHeader pick the actual height and update it with header height when it is detached -->\n <cdk-virtual-scroll-viewport\n class=\"calc-height\"\n style=\"height: calc(100% - 2px)\"\n tableStickyHeader\n [itemSize]=\"rowHeight\"\n [ngClass]=\"{\n 'virtual-scroll-disabled':\n (mousePresent$ | async) === false &&\n delayedMousePresenceDetectionEnabled\n }\"\n >\n <table\n class=\"nui-table-widget__main\"\n nui-table\n [reorderable]=\"configuration?.reorderable\"\n [sortable]=\"sortable\"\n [selection]=\"selection\"\n [selectionConfig]=\"configuration?.selectionConfiguration\"\n [totalItems]=\"totalItems\"\n [paginatorUsed]=\"hasPaginator && configuration?.selectionConfiguration?.allPages\"\n [dataSource]=\"hasVirtualScroll ? virtualScrollAddon.visibleItems : tableData\"\n [sortedColumn]=\"sortedColumn\"\n (sortOrderChanged)=\"onSortOrderChanged($event)\"\n (selectionChange)=\"onSelectionChange($event)\"\n [trackBy]=\"dataTrackBy()\"\n #widgetTable\n >\n <ng-container\n nuiColumnDef=\"{{ column.id }}\"\n *ngFor=\"let column of columns; trackBy: columnTrackBy\"\n >\n <th\n nui-header-cell\n [isColumnSortingDisabled]=\"!column.sortable\"\n *nuiHeaderCellDef\n [style.width.px]=\"columnsWidthMap.get(column.id)\"\n [style.max-width.px]=\"columnsWidthMap.get(column.id)\"\n [style.min-width.px]=\"columnsWidthMap.get(column.id)\"\n [title]=\"column.label\"\n [tooltipText]=\"\n headerTooltipsEnabled ? column.label : undefined\n \"\n [alignment]=\"getColumnAlignment(column)\"\n >\n {{ column.label }}\n </th>\n <ng-container>\n <td\n nui-cell\n *nuiCellDef=\"let element\"\n [style.width.px]=\"columnsWidthMap.get(column.id)\"\n [style.max-width.px]=\"\n columnsWidthMap.get(column.id)\n \"\n [style.min-width.px]=\"\n columnsWidthMap.get(column.id)\n \"\n [alignment]=\"getColumnAlignment(column)\"\n >\n <ng-container\n nuiComponentPortal\n componentId=\"formatter\"\n *ngIf=\"element[column.id] && column.formatter\"\n [componentType]=\"\n column.formatter?.componentType\n \"\n [properties]=\"element[column.id]\"\n #componentPortal=\"nuiComponentPortal\"\n >\n <ng-template\n [cdkPortalOutlet]=\"componentPortal.portal\"\n (attached)=\"\n componentPortal.attached($event)\n \"\n ></ng-template>\n </ng-container>\n </td>\n </ng-container>\n </ng-container>\n <tr nui-header-row *nuiHeaderRowDef=\"headers\"></tr>\n <ng-template\n nuiRowDef\n cdkVirtualFor\n let-row\n [nuiRowDefColumns]=\"headers\"\n [cdkVirtualForOf]=\"tableData\"\n [cdkVirtualForTemplateCacheSize]=\"10\"\n [cdkVirtualForTrackBy]=\"dataTrackBy()\"\n >\n <tr\n nui-row\n (click)=\"onInteraction(row, $event)\"\n [attr.role]=\"interactive ? 'button' : ''\"\n density=\"tiny\"\n [rowObject]=\"row.__record\"\n [clickableRow]=\"clickableRow\"\n ></tr>\n </ng-template>\n </table>\n </cdk-virtual-scroll-viewport>\n </div>\n <div [hidden]=\"!hasPaginator\" class=\"mt-2\">\n <ng-container *ngTemplateOutlet=\"footer\"></ng-container>\n </div>\n</div>\n\n<ng-template #footer>\n <footer>\n <nui-paginator\n #paginator\n id=\"nui-table-widget-paginator\"\n [appendToBody]=\"true\"\n [page]=\"paginatorAddon.paginatorState.page\"\n [pageSize]=\"paginatorAddon.paginatorState.pageSize\"\n [pageSizeSet]=\"paginatorAddon.paginatorState.pageSizeSet\"\n [total]=\"paginatorAddon.paginatorState.total\"\n (pagerAction)=\"onPagerAction($event)\"\n >\n </nui-paginator>\n </footer>\n</ng-template>\n", styles: [".nui-table-widget{overflow:auto;width:100%}.nui-table-widget .display-contents{display:contents}.nui-table-widget__main{flex:1 1 auto}.nui-table-widget__paginator{margin-top:10px}.nui-table-widget__overflow{overflow-y:auto}.nui-table-widget .nui-table-widget-container .virtual-scroll-disabled.cdk-virtual-scroll-viewport{overflow:hidden!important}:host-context(.mobile) .calc-height{min-height:200px}:host.table-widget-fullwidth{width:calc(100% - 10px)}.invisible{height:0}@-moz-document url-prefix(){.nui-table-widget-container{margin-right:10px}}\n"], dependencies: [{ kind: "directive", type: i8.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i8.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i8.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i8.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: i3.ImageComponent, selector: "nui-image", inputs: ["image", "description", "float", "margin", "isWatermark", "width", "height", "autoFill"] }, { kind: "directive", type: i9.CdkPortalOutlet, selector: "[cdkPortalOutlet]", inputs: ["cdkPortalOutlet"], outputs: ["attached"], exportAs: ["cdkPortalOutlet"] }, { kind: "directive", type: i10.ComponentPortalDirective, selector: "[nuiComponentPortal]", inputs: ["componentId", "componentType", "properties", "providers", "outputs"], outputs: ["output"], exportAs: ["nuiComponentPortal"] }, { kind: "component", type: i3.TableComponent, selector: "nui-table, table[nui-table]", inputs: ["reorderable", "sortable", "resizable", "selectable", "selectionConfig", "totalItems", "dataSource", "selection", "sortedColumn", "paginatorUsed"], outputs: ["columnsOrderChange", "sortOrderChanged", "selectionChange", "columnsWidthChange"], exportAs: ["nuiTable"] }, { kind: "directive", type: i3.TableHeaderRowDefDirective, selector: "[nuiHeaderRowDef]", inputs: ["nuiHeaderRowDef", "nuiHeaderRowDefSticky"] }, { kind: "directive", type: i3.TableCellDefDirective, selector: "[nuiCellDef]" }, { kind: "directive", type: i3.TableRowDefDirective, selector: "[nuiRowDef]", inputs: ["nuiRowDefColumns", "nuiRowDefWhen"] }, { kind: "component", type: i3.TableHeaderRowComponent, selector: "nui-header-row, tr[nui-header-row]", inputs: ["density"], exportAs: ["nuiHeaderRow"] }, { kind: "component", type: i3.TableRowComponent, selector: "nui-row, tr[nui-row]", inputs: ["density", "rowObject", "clickableRow", "clickableRowConfig"], exportAs: ["nuiRow"] }, { kind: "directive", type: i3.TableHeaderCellDefDirective, selector: "[nuiHeaderCellDef]" }, { kind: "component", type: i3.TableHeaderCellComponent, selector: "nui-header-cell, th[nui-header-cell]", inputs: ["alignment", "tooltipText", "isColumnSortingDisabled"] }, { kind: "directive", type: i3.TableCellDirective, selector: "nui-cell, td[nui-cell]", inputs: ["tooltipText", "alignment"] }, { kind: "directive", type: i3.TableColumnDefDirective, selector: "[nuiColumnDef]", inputs: ["nuiColumnDef", "type", "columnWidth"] }, { kind: "directive", type: i3.TableStickyHeaderDirective, selector: "cdk-virtual-scroll-viewport[tableStickyHeader]", inputs: ["tableStickyHeader"] }, { kind: "directive", type: i11.CdkFixedSizeVirtualScroll, selector: "cdk-virtual-scroll-viewport[itemSize]", inputs: ["itemSize", "minBufferPx", "maxBufferPx"] }, { kind: "directive", type: i11.CdkVirtualForOf, selector: "[cdkVirtualFor][cdkVirtualForOf]", inputs: ["cdkVirtualForOf", "cdkVirtualForTrackBy", "cdkVirtualForTemplate", "cdkVirtualForTemplateCacheSize"] }, { kind: "component", type: i11.CdkVirtualScrollViewport, selector: "cdk-virtual-scroll-viewport", inputs: ["orientation", "appendOnly"], outputs: ["scrolledIndexChange"] }, { kind: "component", type: i3.SearchComponent, selector: "nui-search", inputs: ["captureFocus", "name", "isInErrorState", "placeholder", "value"], outputs: ["cancel", "focusChange", "inputChange", "search"] }, { kind: "component", type: i3.PaginatorComponent, selector: "nui-paginator", inputs: ["itemsList", "page", "pageSize", "pageSizeSet", "total", "dots", "hideIfEmpty", "hide", "activeClass", "disabledClass", "adjacent", "maxElements", "showPrevNext", "popupDirectionTop", "popupBaseElementSelector", "appendToBody"], outputs: ["pagerAction", "pageChange"] }, { kind: "directive", type: i12.DelayedMousePresenceDetectionDirective, selector: "[nuiDelayedMousePresenceDetection]", inputs: ["nuiDelayedMousePresenceDetection", "mousePresentSubject", "delay"] }, { kind: "pipe", type: i8.AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TableWidgetComponent, decorators: [{ type: Component, args: [{ selector: "nui-table-widget", changeDetection: ChangeDetectionStrategy.OnPush, providers: [ SearchFeatureAddonService, VirtualScrollFeatureAddonService, PaginatorFeatureAddonService, ], host: { // Note: Moved here from configuration to ensure that consumers will not override it. // Used to prevent table overflowing preview container in the edit/configuration mode. "[class.table-widget-fullwidth]": "true", }, template: "<div\n class=\"h-100 d-flex justify-content-center align-items-center\"\n *ngIf=\"!shouldDisplayTable()\"\n>\n <nui-image image=\"no-data-to-show\"></nui-image>\n</div>\n<!-- h-100 gets added only when there's data to show to prevent an extra scrollbar when the table is empty -->\n<!-- Using invisible class instead of *ngIf as an non-virtual-scroll breakable way to hide the content. -->\n<div\n class=\"nui-table-widget d-flex flex-column\"\n [ngClass]=\"shouldDisplayTable() ? 'h-100' : 'invisible'\"\n>\n <div class=\"mr-2 mb-2 flex-row\" *ngIf=\"isSearchEnabled\">\n <nui-search\n class=\"unlimited-width d-flex justify-content-end flex-grow-1\"\n [value]=\"searchValue\"\n (inputChange)=\"onSearchInputChanged($event)\"\n (cancel)=\"onSearchInputChanged($event)\"\n >\n </nui-search>\n </div>\n <div\n class=\"flex-row h-100 nui-table-widget-container\"\n [nuiDelayedMousePresenceDetection]=\"\n delayedMousePresenceDetectionEnabled\n \"\n [delay]=\"configuration.scrollActivationDelayMs\"\n [mousePresentSubject]=\"mousePresent$\"\n >\n <!-- Note: Using inline style to let tableStickyHeader pick the actual height and update it with header height when it is detached -->\n <cdk-virtual-scroll-viewport\n class=\"calc-height\"\n style=\"height: calc(100% - 2px)\"\n tableStickyHeader\n [itemSize]=\"rowHeight\"\n [ngClass]=\"{\n 'virtual-scroll-disabled':\n (mousePresent$ | async) === false &&\n delayedMousePresenceDetectionEnabled\n }\"\n >\n <table\n class=\"nui-table-widget__main\"\n nui-table\n [reorderable]=\"configuration?.reorderable\"\n [sortable]=\"sortable\"\n [selection]=\"selection\"\n [selectionConfig]=\"configuration?.selectionConfiguration\"\n [totalItems]=\"totalItems\"\n [paginatorUsed]=\"hasPaginator && configuration?.selectionConfiguration?.allPages\"\n [dataSource]=\"hasVirtualScroll ? virtualScrollAddon.visibleItems : tableData\"\n [sortedColumn]=\"sortedColumn\"\n (sortOrderChanged)=\"onSortOrderChanged($event)\"\n (selectionChange)=\"onSelectionChange($event)\"\n [trackBy]=\"dataTrackBy()\"\n #widgetTable\n >\n <ng-container\n nuiColumnDef=\"{{ column.id }}\"\n *ngFor=\"let column of columns; trackBy: columnTrackBy\"\n >\n <th\n nui-header-cell\n [isColumnSortingDisabled]=\"!column.sortable\"\n *nuiHeaderCellDef\n [style.width.px]=\"columnsWidthMap.get(column.id)\"\n [style.max-width.px]=\"columnsWidthMap.get(column.id)\"\n [style.min-width.px]=\"columnsWidthMap.get(column.id)\"\n [title]=\"column.label\"\n [tooltipText]=\"\n headerTooltipsEnabled ? column.label : undefined\n \"\n [alignment]=\"getColumnAlignment(column)\"\n >\n {{ column.label }}\n </th>\n <ng-container>\n <td\n nui-cell\n *nuiCellDef=\"let element\"\n [style.width.px]=\"columnsWidthMap.get(column.id)\"\n [style.max-width.px]=\"\n columnsWidthMap.get(column.id)\n \"\n [style.min-width.px]=\"\n columnsWidthMap.get(column.id)\n \"\n [alignment]=\"getColumnAlignment(column)\"\n >\n <ng-container\n nuiComponentPortal\n componentId=\"formatter\"\n *ngIf=\"element[column.id] && column.formatter\"\n [componentType]=\"\n column.formatter?.componentType\n \"\n [properties]=\"element[column.id]\"\n #componentPortal=\"nuiComponentPortal\"\n >\n <ng-template\n [cdkPortalOutlet]=\"componentPortal.portal\"\n (attached)=\"\n componentPortal.attached($event)\n \"\n ></ng-template>\n </ng-container>\n </td>\n </ng-container>\n </ng-container>\n <tr nui-header-row *nuiHeaderRowDef=\"headers\"></tr>\n <ng-template\n nuiRowDef\n cdkVirtualFor\n let-row\n [nuiRowDefColumns]=\"headers\"\n [cdkVirtualForOf]=\"tableData\"\n [cdkVirtualForTemplateCacheSize]=\"10\"\n [cdkVirtualForTrackBy]=\"dataTrackBy()\"\n >\n <tr\n nui-row\n (click)=\"onInteraction(row, $event)\"\n [attr.role]=\"interactive ? 'button' : ''\"\n density=\"tiny\"\n [rowObject]=\"row.__record\"\n [clickableRow]=\"clickableRow\"\n ></tr>\n </ng-template>\n </table>\n </cdk-virtual-scroll-viewport>\n </div>\n <div [hidden]=\"!hasPaginator\" class=\"mt-2\">\n <ng-container *ngTemplateOutlet=\"footer\"></ng-container>\n </div>\n</div>\n\n<ng-template #footer>\n <footer>\n <nui-paginator\n #paginator\n id=\"nui-table-widget-paginator\"\n [appendToBody]=\"true\"\n [page]=\"paginatorAddon.paginatorState.page\"\n [pageSize]=\"paginatorAddon.paginatorState.pageSize\"\n [pageSizeSet]=\"paginatorAddon.paginatorState.pageSizeSet\"\n [total]=\"paginatorAddon.paginatorState.total\"\n (pagerAction)=\"onPagerAction($event)\"\n >\n </nui-paginator>\n </footer>\n</ng-template>\n", styles: [".nui-table-widget{overflow:auto;width:100%}.nui-table-widget .display-contents{display:contents}.nui-table-widget__main{flex:1 1 auto}.nui-table-widget__paginator{margin-top:10px}.nui-table-widget__overflow{overflow-y:auto}.nui-table-widget .nui-table-widget-container .virtual-scroll-disabled.cdk-virtual-scroll-viewport{overflow:hidden!important}:host-context(.mobile) .calc-height{min-height:200px}:host.table-widget-fullwidth{width:calc(100% - 10px)}.invisible{height:0}@-moz-document url-prefix(){.nui-table-widget-container{margin-right:10px}}\n"] }] }], ctorParameters: () => [{ type: i3.EventBus, decorators: [{ type: Inject, args: [PIZZAGNA_EVENT_BUS] }] }, { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [DATA_SOURCE] }] }, { type: i1.WidgetConfigurationService, decorators: [{ type: Optional }] }, { type: i0.ChangeDetectorRef }, { type: i2.PizzagnaService }, { type: i3.VirtualViewportManager }, { type: i0.NgZone }, { type: i0.ElementRef }, { type: i3.LoggerService }, { type: i4.SearchFeatureAddonService }, { type: i5.PaginatorFeatureAddonService }, { type: i6.VirtualScrollFeatureAddonService }, { type: i7.TableFormatterRegistryService }, { type: i3.SelectorService }], propDecorators: { widgetData: [{ type: Input }], componentId: [{ type: Input }], configuration: [{ type: Input }], dataFields: [{ type: Input }], totalItems: [{ type: Input }], indentFromTop: [{ type: Input }], sortable: [{ type: Input }], delayedMousePresenceDetectionEnabled: [{ type: Input }], elementClass: [{ type: Input }, { type: HostBinding, args: ["class"] }], table: [{ type: ViewChild, args: ["widgetTable"] }], paginator: [{ type: ViewChild, args: ["paginator"] }], vscrollViewport: [{ type: ViewChild, args: [CdkVirtualScrollViewport] }], tableRows: [{ type: ViewChildren, args: [TableRowComponent, { read: ElementRef }] }], range: [{ type: Input }] } }); //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGFibGUtd2lkZ2V0LmNvbXBvbmVudC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uL3NyYy9saWIvY29tcG9uZW50cy90YWJsZS13aWRnZXQvdGFibGUtd2lkZ2V0LmNvbXBvbmVudC50cyIsIi4uLy4uLy4uLy4uLy4uL3NyYy9saWIvY29tcG9uZW50cy90YWJsZS13aWRnZXQvdGFibGUtd2lkZ2V0LmNvbXBvbmVudC5odG1sIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLHlEQUF5RDtBQUN6RCxFQUFFO0FBQ0YsK0VBQStFO0FBQy9FLDRFQUE0RTtBQUM1RSw4RUFBOEU7QUFDOUUsK0VBQStFO0FBQy9FLDhFQUE4RTtBQUM5RSw0REFBNEQ7QUFDNUQsRUFBRTtBQUNGLDZFQUE2RTtBQUM3RSx1REFBdUQ7QUFDdkQsRUFBRTtBQUNGLDZFQUE2RTtBQUM3RSw0RUFBNEU7QUFDNUUsK0VBQStFO0FBQy9FLDBFQUEwRTtBQUMxRSxpRkFBaUY7QUFDakYsNkVBQTZFO0FBQzdFLGlCQUFpQjtBQUVqQixPQUFPLEVBQUUsd0JBQXdCLEVBQUUsTUFBTSx3QkFBd0IsQ0FBQztBQUNsRSxPQUFPLEVBRUgsdUJBQXVCLEVBQ3ZCLGlCQUFpQixFQUNqQixTQUFTLEVBQ1QsVUFBVSxFQUNWLFdBQVcsRUFDWCxNQUFNLEVBQ04sS0FBSyxFQUNMLE1BQU0sRUFJTixRQUFRLEVBQ1IsU0FBUyxFQUdULFNBQVMsRUFDVCxZQUFZLEdBQ2YsTUFBTSxlQUFlLENBQUM7QUFDdkIsT0FBTyxHQUFHLE1BQU0sWUFBWSxDQUFDO0FBQzdCLE9BQU8sT0FBTyxNQUFNLGdCQUFnQixDQUFDO0FBQ3JDLE9BQU8sSUFBSSxNQUFNLGFBQWEsQ0FBQztBQUMvQixnREFBZ0Q7QUFDaEQsT0FBTyxFQUFFLGVBQWUsRUFBRSxLQUFLLEVBQWMsRUFBRSxFQUFFLE9BQU8sRUFBRSxNQUFNLE1BQU0sQ0FBQztBQUN2RSxnREFBZ0Q7QUFDaEQsT0FBTyxFQUFFLE1BQU0sRUFBRSxHQUFHLEVBQUUsSUFBSSxFQUFFLFNBQVMsRUFBRSxHQUFHLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUVuRSxPQUFPLEVBQ0gsNEJBQTRCLEVBQzVCLFFBQVEsRUFPUixhQUFhLEVBQ2Isa0JBQWtCLEVBQ2xCLGVBQWUsRUFDZixlQUFlLEVBRWYsY0FBYyxFQUNkLGlCQUFpQixFQUNqQixrQkFBa0IsRUFDbEIsc0JBQXNCLEdBQ3pCLE1BQU0sZUFBZSxDQUFDO0FBRXZCLE9BQU8sRUFBRSxlQUFlLEVBQUUsTUFBTSwwQ0FBMEMsQ0FBQztBQUMzRSxPQUFPLEVBQUUsNkJBQTZCLEVBQUUsTUFBTSxpREFBaUQsQ0FBQztBQUNoRyxPQUFPLEVBQ0gsZ0JBQWdCLEVBQ2hCLFdBQVcsRUFDWCxPQUFPLEVBQ1AsY0FBYyxFQUNkLFNBQVMsRUFDVCxhQUFhLEVBQ2IsWUFBWSxFQUNaLGFBQWEsR0FDaEIsTUFBTSxzQkFBc0IsQ0FBQztBQUM5QixPQUFPLEVBQUUsMEJBQTBCLEVBQU