@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
JavaScript
// © 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