novo-elements
Version:
1 lines • 238 kB
Source Map (JSON)
{"version":3,"file":"novo-elements-elements-data-table.mjs","sources":["../../../projects/novo-elements/src/elements/data-table/data-table.token.ts","../../../projects/novo-elements/src/elements/data-table/cell-headers/data-table-checkbox-header-cell.component.ts","../../../projects/novo-elements/src/elements/data-table/cell-headers/data-table-expand-header-cell.component.ts","../../../projects/novo-elements/src/elements/data-table/services/data-table-filter-utils.ts","../../../projects/novo-elements/src/elements/data-table/state/data-table-state.service.ts","../../../projects/novo-elements/src/elements/data-table/sort-filter/sort-direction.ts","../../../projects/novo-elements/src/elements/data-table/sort-filter/sort-button.animations.ts","../../../projects/novo-elements/src/elements/data-table/sort-filter/sort-button.component.ts","../../../projects/novo-elements/src/elements/data-table/sort-filter/sort-button.component.html","../../../projects/novo-elements/src/elements/data-table/sort-filter/sort-filter.directive.ts","../../../projects/novo-elements/src/elements/data-table/cell-headers/data-table-header-cell-filter-header.component.ts","../../../projects/novo-elements/src/elements/data-table/cell-headers/data-table-header-cell.component.ts","../../../projects/novo-elements/src/elements/data-table/cell-headers/data-table-header-cell.directive.ts","../../../projects/novo-elements/src/elements/data-table/cells/data-table-cell.component.ts","../../../projects/novo-elements/src/elements/data-table/cells/data-table-checkbox-cell.component.ts","../../../projects/novo-elements/src/elements/data-table/cells/data-table-expand-cell.component.ts","../../../projects/novo-elements/src/elements/data-table/data-table-clear-button.component.ts","../../../projects/novo-elements/src/elements/data-table/data-table-expand.directive.ts","../../../projects/novo-elements/src/elements/data-table/data-table.source.ts","../../../projects/novo-elements/src/elements/data-table/services/static-data-table.service.ts","../../../projects/novo-elements/src/elements/data-table/rows/data-table-header-row.component.ts","../../../projects/novo-elements/src/elements/data-table/rows/data-table-row.component.ts","../../../projects/novo-elements/src/elements/data-table/pagination/data-table-pagination.component.ts","../../../projects/novo-elements/src/elements/data-table/data-table.pipes.ts","../../../projects/novo-elements/src/elements/data-table/data-table.component.ts","../../../projects/novo-elements/src/elements/data-table/data-table.component.html","../../../projects/novo-elements/src/elements/data-table/data-table.module.ts","../../../projects/novo-elements/src/elements/data-table/interfaces.ts","../../../projects/novo-elements/src/elements/data-table/services/remote-data-table.service.ts","../../../projects/novo-elements/src/elements/data-table/novo-elements-elements-data-table.ts"],"sourcesContent":["import { EventEmitter, InjectionToken } from '@angular/core';\nimport { DataTableSource } from './data-table.source';\nimport { DataTableState } from './state';\n\n/**\n * Describes a parent component that manages a list of options.\n * Contains properties that the options can inherit.\n * @docs-private\n */\nexport interface NovoDataTableRef<T = any> {\n isExpanded(row: T): boolean;\n expandRow(row: T): void;\n isSelected(row: T): boolean;\n selectRow(row: T, evt: string): void;\n selectRows(selected: boolean): void;\n expandRows(expanded: boolean): void;\n allCurrentRowsSelected(): boolean;\n allCurrentRowsExpanded(): boolean;\n allSelected: EventEmitter<any>;\n canSelectAll: boolean;\n allMatchingSelected: boolean;\n state: DataTableState<T>;\n dataSource: DataTableSource<T>;\n}\n\n/**\n * Injection token used to provide the parent component to options.\n */\nexport const NOVO_DATA_TABLE_REF = new InjectionToken<NovoDataTableRef>('NOVO_DATA_TABLE_REF');\n","import { CdkColumnDef, CdkHeaderCell } from '@angular/cdk/table';\nimport {\n ChangeDetectionStrategy,\n ChangeDetectorRef,\n Component,\n ElementRef,\n HostBinding,\n Inject,\n Input,\n OnDestroy,\n Renderer2,\n} from '@angular/core';\nimport { Subscription } from 'rxjs';\nimport { NovoToastService } from 'novo-elements/elements/toast';\nimport { NovoDataTableRef, NOVO_DATA_TABLE_REF } from '../data-table.token';\n\n@Component({\n selector: 'novo-data-table-checkbox-header-cell',\n template: `\n <div class=\"data-table-checkbox\" (click)=\"onClick()\">\n <input type=\"checkbox\" [checked]=\"checked\" />\n <label>\n <i [class.bhi-checkbox-empty]=\"!checked\" [class.bhi-checkbox-filled]=\"checked\"></i>\n </label>\n </div>\n `,\n changeDetection: ChangeDetectionStrategy.OnPush,\n standalone: false,\n})\nexport class NovoDataTableCheckboxHeaderCell<T> extends CdkHeaderCell implements OnDestroy {\n @HostBinding('attr.role')\n public role = 'columnheader';\n @Input()\n public maxSelected: number = undefined;\n\n public checked: boolean = false;\n private selectionSubscription: Subscription;\n private paginationSubscription: Subscription;\n private resetSubscription: Subscription;\n\n get isAtLimit(): boolean {\n return (\n this.maxSelected && this.dataTable.state.selectedRows.size + this.dataTable.dataSource.data.length > this.maxSelected && !this.checked\n );\n }\n\n constructor(\n columnDef: CdkColumnDef,\n elementRef: ElementRef,\n renderer: Renderer2,\n @Inject(NOVO_DATA_TABLE_REF) private dataTable: NovoDataTableRef,\n private ref: ChangeDetectorRef,\n private toaster: NovoToastService,\n ) {\n super(columnDef, elementRef);\n renderer.setAttribute(elementRef.nativeElement, 'data-automation-id', `novo-checkbox-column-header-${columnDef.cssClassFriendlyName}`);\n renderer.addClass(elementRef.nativeElement, `novo-checkbox-column-${columnDef.cssClassFriendlyName}`);\n renderer.addClass(elementRef.nativeElement, 'novo-data-table-checkbox-header-cell');\n\n this.selectionSubscription = this.dataTable.state.selectionSource.subscribe(() => {\n this.checked = this.dataTable.allCurrentRowsSelected() || (this.dataTable?.canSelectAll && this.dataTable?.allMatchingSelected);\n if (this.dataTable?.canSelectAll) {\n this.selectAllChanged();\n }\n this.ref.markForCheck();\n });\n this.paginationSubscription = this.dataTable.state.paginationSource.subscribe((event: { isPageSizeChange: boolean }) => {\n if (event.isPageSizeChange) {\n this.checked = false;\n if (this.dataTable?.canSelectAll) {\n this.selectAllChanged();\n }\n this.dataTable.selectRows(false);\n this.dataTable.state.checkRetainment('pageSize');\n this.dataTable.state.reset(false, true);\n } else {\n this.checked = this.dataTable.allCurrentRowsSelected() || (this.dataTable?.canSelectAll && this.dataTable?.allMatchingSelected);\n if (this.dataTable?.canSelectAll) {\n this.selectAllChanged();\n }\n }\n this.ref.markForCheck();\n });\n this.resetSubscription = this.dataTable.state.resetSource.subscribe(() => {\n this.checked = false;\n if (this.dataTable?.canSelectAll) {\n this.resetAllMatchingSelected();\n }\n this.ref.markForCheck();\n });\n }\n\n public ngOnDestroy(): void {\n if (this.selectionSubscription) {\n this.selectionSubscription.unsubscribe();\n }\n if (this.paginationSubscription) {\n this.paginationSubscription.unsubscribe();\n }\n if (this.resetSubscription) {\n this.resetSubscription.unsubscribe();\n }\n }\n\n public onClick(): void {\n if (this.isAtLimit) {\n this.toaster.alert({\n theme: 'danger',\n position: 'fixedTop',\n message: 'Error, more than 500 items are not able to be selected at one time',\n icon: 'caution',\n });\n } else {\n this.dataTable.selectRows(!this.checked);\n }\n if (this.dataTable?.canSelectAll) {\n if (this.checked) {\n this.resetAllMatchingSelected();\n } else {\n this.selectAllChanged();\n }\n }\n }\n\n private resetAllMatchingSelected(): void {\n this.dataTable.state?.allMatchingSelectedSource?.next(false);\n this.dataTable.state?.onSelectionChange();\n }\n\n public selectAllChanged(): void {\n const allSelectedEvent = {\n allSelected: this.checked,\n selectedCount: this.dataTable?.state?.selected?.length,\n allMatchingSelected: this.dataTable?.allMatchingSelected,\n };\n this.dataTable.allSelected.emit(allSelectedEvent);\n }\n}\n","import { CdkColumnDef, CdkHeaderCell } from '@angular/cdk/table';\nimport {\n ChangeDetectionStrategy,\n ChangeDetectorRef,\n Component,\n ElementRef,\n HostBinding,\n Inject,\n OnDestroy,\n Renderer2,\n} from '@angular/core';\nimport { Subscription } from 'rxjs';\nimport { NovoDataTableRef, NOVO_DATA_TABLE_REF } from '../data-table.token';\n\n@Component({\n selector: 'novo-data-table-expand-header-cell',\n template: ' <i class=\"bhi-next data-table-icon\" novo-data-table-expander=\"true\" (click)=\"expandAll()\" [class.expanded]=\"expanded\"></i> ',\n changeDetection: ChangeDetectionStrategy.OnPush,\n standalone: false,\n})\nexport class NovoDataTableExpandHeaderCell<T> extends CdkHeaderCell implements OnDestroy {\n @HostBinding('attr.role')\n public role = 'columnheader';\n\n public expanded: boolean = false;\n private expandSubscription: Subscription;\n\n constructor(\n columnDef: CdkColumnDef,\n elementRef: ElementRef,\n renderer: Renderer2,\n @Inject(NOVO_DATA_TABLE_REF) private dataTable: NovoDataTableRef,\n private ref: ChangeDetectorRef,\n ) {\n super(columnDef, elementRef);\n renderer.setAttribute(elementRef.nativeElement, 'data-automation-id', `novo-expand-column-header-${columnDef.cssClassFriendlyName}`);\n renderer.addClass(elementRef.nativeElement, `novo-expand-column-${columnDef.cssClassFriendlyName}`);\n renderer.addClass(elementRef.nativeElement, 'novo-data-table-expand-header-cell');\n\n this.expandSubscription = this.dataTable.state.expandSource.subscribe(() => {\n this.expanded = this.dataTable.allCurrentRowsExpanded();\n this.ref.markForCheck();\n });\n }\n\n public ngOnDestroy(): void {\n if (this.expandSubscription) {\n this.expandSubscription.unsubscribe();\n }\n }\n\n public expandAll(): void {\n this.dataTable.expandRows(!this.expanded);\n }\n}\n","import { endOfToday, startOfToday } from 'date-fns';\nimport { DateUtil } from 'novo-elements/utils';\n\nexport class NovoDataTableFilterUtils {\n static constructFilter(filter?: any, type?: any, multiSelect?: boolean) {\n let actualFilter = filter;\n if (filter) {\n if (type && type === 'date') {\n if (filter.startDate && filter.endDate) {\n actualFilter = {\n min: DateUtil.startOfDay(filter.startDate.date),\n max: DateUtil.startOfDay(DateUtil.addDays(DateUtil.startOfDay(filter.endDate.date), 1)),\n };\n } else {\n actualFilter = {\n min: filter.min ? DateUtil.addDays(startOfToday(), filter.min) : startOfToday(),\n max: filter.max ? DateUtil.addDays(endOfToday(), filter.max) : endOfToday(),\n };\n }\n }\n\n if (multiSelect && Array.isArray(filter)) {\n actualFilter = filter.map((filterItem) => {\n if (filterItem && filterItem.hasOwnProperty('value')) {\n return filterItem.value;\n }\n return filterItem;\n });\n } else if (actualFilter && actualFilter.hasOwnProperty('value')) {\n actualFilter = filter.value;\n }\n }\n return actualFilter;\n }\n}\n","import { EventEmitter, Injectable } from '@angular/core';\nimport { Helpers } from 'novo-elements/utils';\nimport { Subject } from 'rxjs';\nimport {\n AdaptiveCriteria,\n AppliedSearchType,\n IDataTableChangeEvent,\n IDataTableFilter,\n IDataTablePreferences,\n IDataTableSelectionOption,\n IDataTableSort,\n} from '../interfaces';\nimport { NovoDataTableFilterUtils } from '../services/data-table-filter-utils';\n\n@Injectable()\nexport class DataTableState<T> {\n public selectionSource = new Subject<void>();\n public paginationSource = new Subject();\n public sortFilterSource = new Subject();\n public resetSource = new Subject<void>();\n public expandSource = new Subject();\n public allMatchingSelectedSource = new Subject();\n public dataLoaded = new Subject<void>();\n public dataLoadingSource = new Subject();\n\n sort: IDataTableSort = undefined;\n filter: IDataTableFilter | IDataTableFilter[] = undefined;\n where: { query: string; criteria?: AdaptiveCriteria; form: any } = undefined;\n page: number = 0;\n pageSize: number = undefined;\n globalSearch: string = undefined;\n selectedRows: Map<string, T> = new Map<string, T>();\n expandedRows: Set<string> = new Set<string>();\n outsideFilter: any;\n isForceRefresh: boolean = false;\n selectionOptions: IDataTableSelectionOption[];\n updates: EventEmitter<IDataTableChangeEvent> = new EventEmitter<IDataTableChangeEvent>();\n retainSelected: boolean = false;\n savedSearchName: string = undefined;\n appliedSearchType: AppliedSearchType;\n displayedColumns: string[] = undefined;\n\n get userFiltered(): boolean {\n return !!(this.filter || this.sort || this.globalSearch || this.outsideFilter || this.where);\n }\n\n get userFilteredInternal(): boolean {\n return !!(this.filter || this.sort || this.globalSearch || this.where);\n }\n\n get selected(): T[] {\n return Array.from(this.selectedRows.values());\n }\n\n public reset(fireUpdate: boolean = true, persistUserFilters?): void {\n this.setState({} as IDataTablePreferences, fireUpdate, persistUserFilters)\n }\n\n public clearSort(fireUpdate: boolean = true): void {\n this.sort = undefined;\n this.page = 0;\n this.checkRetainment('sort');\n this.reset(fireUpdate, true);\n this.onSortFilterChange();\n if (fireUpdate) {\n this.updates.emit({\n sort: this.sort,\n filter: this.filter,\n globalSearch: this.globalSearch,\n where: this.where,\n });\n }\n }\n\n public clearFilter(fireUpdate: boolean = true): void {\n this.filter = undefined;\n this.globalSearch = undefined;\n this.page = 0;\n this.checkRetainment('filter');\n this.reset(fireUpdate, true);\n this.onSortFilterChange();\n if (fireUpdate) {\n this.updates.emit({\n sort: this.sort,\n filter: this.filter,\n globalSearch: this.globalSearch,\n where: this.where,\n });\n }\n }\n\n public clearQuery(fireUpdate: boolean = true): void {\n this.where = undefined;\n this.page = 0;\n this.checkRetainment('where');\n this.reset(fireUpdate, true);\n this.onSortFilterChange();\n if (fireUpdate) {\n this.updates.emit({\n sort: this.sort,\n filter: this.filter,\n globalSearch: this.globalSearch,\n where: this.where,\n });\n }\n }\n\n public clearSelected(fireUpdate: boolean = true): void {\n this.allMatchingSelectedSource.next(false);\n this.globalSearch = undefined;\n this.page = 0;\n this.reset(fireUpdate, true);\n this.onSelectionChange();\n if (fireUpdate) {\n this.updates.emit({\n sort: this.sort,\n filter: this.filter,\n globalSearch: this.globalSearch,\n where: this.where,\n });\n }\n }\n\n public onSelectionChange(): void {\n this.selectionSource.next();\n }\n\n public onExpandChange(targetId?: number): void {\n this.expandSource.next(targetId);\n }\n\n public onPaginationChange(isPageSizeChange: boolean, pageSize: number): void {\n this.checkRetainment('page');\n this.paginationSource.next({ isPageSizeChange, pageSize });\n }\n\n public onSortFilterChange(): void {\n this.checkRetainment('sort');\n this.checkRetainment('filter');\n this.checkRetainment('where');\n this.sortFilterSource.next({\n sort: this.sort,\n filter: this.filter,\n globalSearch: this.globalSearch,\n where: this.where,\n savedSearchName: this.savedSearchName,\n appliedSearchType: this.appliedSearchType,\n });\n }\n\n public setInitialSortFilter(preferences): void {\n if (preferences) {\n if (preferences.where) {\n this.where = preferences.where;\n }\n\n if (preferences.sort) {\n this.sort = preferences.sort;\n }\n\n if (preferences.filter) {\n this.filter = this.transformFilters(preferences.filter);\n }\n\n if (preferences.globalSearch) {\n this.globalSearch = preferences.globalSearch;\n }\n\n if (preferences.savedSearchName) {\n this.savedSearchName = preferences.savedSearchName;\n }\n\n if (preferences.appliedSearchType) {\n this.appliedSearchType = preferences.appliedSearchType;\n }\n }\n }\n\n public setState(preferences: IDataTablePreferences, fireUpdate = true, persistUserFilters = false): void {\n if (!persistUserFilters) {\n this.where = preferences.where;\n this.sort = preferences.sort;\n this.filter = preferences.filter ? this.transformFilters(preferences.filter) : undefined;\n this.globalSearch = preferences.globalSearch;\n this.savedSearchName = preferences.savedSearchName;\n if (preferences.displayedColumns?.length) {\n this.displayedColumns = preferences.displayedColumns;\n }\n this.appliedSearchType = preferences.appliedSearchType;\n }\n\n this.page = 0;\n if (!this.retainSelected) {\n this.selectedRows.clear();\n this.resetSource.next();\n }\n\n this.onSortFilterChange();\n this.retainSelected = false;\n\n if (fireUpdate) {\n this.updates.emit({\n sort: this.sort,\n filter: this.filter,\n globalSearch: this.globalSearch,\n where: this.where,\n savedSearchName: this.savedSearchName,\n displayedColumns: this.displayedColumns,\n appliedSearchType: this.appliedSearchType,\n });\n }\n }\n\n public checkRetainment(caller: string, allMatchingSelected = false): void {\n this.retainSelected = this.selectionOptions?.some((option) => option.label === caller) || this.retainSelected || allMatchingSelected;\n }\n\n private transformFilters(filters) {\n const filterArray = Helpers.convertToArray(filters);\n filterArray.forEach((filter) => {\n filter.value =\n filter.selectedOption && filter.type\n ? NovoDataTableFilterUtils.constructFilter(filter.selectedOption, filter.type)\n : filter.value;\n });\n return filterArray;\n }\n}\n","export enum SortDirection {\n ASC = 'ascending',\n DESC = 'descending',\n NONE = 'none',\n}\n","import { animate, AnimationTriggerMetadata, state, style, transition, trigger } from '@angular/animations';\nimport { SortDirection } from './sort-direction';\n\nconst activeStyle = { opacity: 1, top: 0 };\nconst inactiveStyle = { opacity: 0 };\n\n/** Animation that moves the sort indicator. */\nexport const sortAscAnim: AnimationTriggerMetadata = trigger('sortAsc', [\n // ...\n state(SortDirection.ASC, style(activeStyle)),\n state(SortDirection.DESC, style(inactiveStyle)),\n state(SortDirection.NONE, style(inactiveStyle)),\n transition('* => ascending', [animate('1s')]),\n transition('ascending => *', [animate('0.5s')]),\n]);\n\nexport const sortDescAnim: AnimationTriggerMetadata = trigger('sortDesc', [\n // ...\n state(SortDirection.ASC, style(inactiveStyle)),\n state(SortDirection.DESC, style(activeStyle)),\n state(SortDirection.NONE, style(inactiveStyle)),\n transition('* => descending', [animate('1s')]),\n transition('descending => *', [animate('0.5s')]),\n]);\n\nexport const sortNoneAnim: AnimationTriggerMetadata = trigger('sortNone', [\n // ...\n state(SortDirection.ASC, style(inactiveStyle)),\n state(SortDirection.DESC, style(inactiveStyle)),\n state(SortDirection.NONE, style(activeStyle)),\n transition('* => none', [animate('1s')]),\n transition('none => *', [animate('0.5s')]),\n]);\n","import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core';\nimport { NovoLabelService } from 'novo-elements/services';\nimport { DataTableState } from '../state/data-table-state.service';\nimport { sortAscAnim, sortDescAnim, sortNoneAnim } from './sort-button.animations';\nimport { SortDirection } from './sort-direction';\n@Component({\n selector: 'novo-sort-button',\n styleUrls: ['./sort-button.component.scss'],\n templateUrl: './sort-button.component.html',\n changeDetection: ChangeDetectionStrategy.OnPush,\n animations: [sortAscAnim, sortDescAnim, sortNoneAnim],\n standalone: false,\n})\nexport class NovoDataTableSortButton<T> {\n @Output() sortChange: EventEmitter<SortDirection> = new EventEmitter();\n public SortDirection = SortDirection;\n\n @Input()\n public get value(): SortDirection {\n return this._value;\n }\n public set value(value: SortDirection) {\n this._value = value;\n }\n\n public get isActive() {\n return this.value !== SortDirection.NONE;\n }\n\n private _value: SortDirection = SortDirection.NONE;\n\n constructor(public state: DataTableState<T>, private ref: ChangeDetectorRef, public labels: NovoLabelService) {}\n\n changeSort(dir: SortDirection): void {\n this.value = dir;\n this.sortChange.emit(dir);\n }\n\n clearSort(): void {\n this.state.clearSort();\n this.sortChange.emit(SortDirection.NONE);\n }\n}\n","<novo-icon\n class=\"novo-sort-asc-icon\"\n [class.sort-active]=\"isActive\"\n [class.sort-hidden]=\"value !== SortDirection.ASC\"\n [@sortAsc]=\"value\"\n (click)=\"changeSort(SortDirection.DESC)\">arrow-up</novo-icon>\n<novo-icon\n class=\"novo-sort-desc-icon\"\n [class.sort-active]=\"isActive\"\n [class.sort-hidden]=\"value !== SortDirection.DESC\"\n [@sortDesc]=\"value\"\n (click)=\"changeSort(SortDirection.NONE)\">arrow-down</novo-icon>\n<novo-icon\n class=\"novo-sortable-icon\"\n [class.sort-active]=\"isActive\"\n [class.sort-hidden]=\"value !== SortDirection.NONE\"\n [@sortNone]=\"value\"\n (click)=\"changeSort(SortDirection.ASC)\">sortable</novo-icon>","import { Directive } from '@angular/core';\nimport { Helpers } from 'novo-elements/utils';\nimport { DataTableState } from '../state/data-table-state.service';\n\n@Directive({\n selector: '[novoDataTableSortFilter]',\n standalone: false,\n})\nexport class NovoDataTableSortFilter<T> {\n constructor(private state: DataTableState<T>) {}\n\n public filter(\n id: string,\n type: string,\n value: any,\n transform: Function,\n allowMultipleFilters: boolean = false,\n selectedOption?: Object,\n ): void {\n let filter;\n\n if (allowMultipleFilters) {\n filter = this.resolveMultiFilter(id, type, value, transform, selectedOption);\n } else {\n if (!Helpers.isBlank(value)) {\n filter = { id, type, value, transform, ...(selectedOption && { selectedOption }) };\n } else {\n filter = undefined;\n }\n }\n\n this.state.filter = filter;\n this.state.checkRetainment('filter');\n this.state.reset(false, true);\n this.state.updates.next({ filter, sort: this.state.sort });\n this.state.onSortFilterChange();\n }\n\n public sort(id: string, value: string, transform: Function): void {\n const sort = { id, value, transform };\n this.state.sort = sort;\n this.state.checkRetainment('sort');\n this.state.reset(false, true);\n this.state.updates.next({ sort, filter: this.state.filter });\n this.state.onSortFilterChange();\n }\n\n public resolveMultiFilter(id: string, type: string, value: any, transform: Function, selectedOption: Object) {\n let filter;\n\n filter = Helpers.convertToArray(this.state.filter);\n\n const filterIndex = filter.findIndex((aFilter) => aFilter && aFilter.id === id);\n if (filterIndex > -1) {\n filter.splice(filterIndex, 1);\n }\n\n if (!Helpers.isBlank(value)) {\n filter = [...filter, { id, type, value, transform, ...(selectedOption && { selectedOption }) }];\n }\n\n if (filter.length < 1) {\n filter = undefined;\n }\n\n return filter;\n }\n}\n","import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core';\nimport { NovoLabelService } from 'novo-elements/services';\nimport { Helpers } from 'novo-elements/utils';\n\n@Component({\n selector: 'novo-data-table-cell-filter-header',\n template: `\n <div class=\"header\">\n <novo-label>{{ label || labels.filters }}</novo-label>\n <novo-button\n theme=\"dialogue\"\n color=\"negative\"\n size=\"small\"\n icon=\"times\"\n (click)=\"clearFilter.emit()\"\n *ngIf=\"hasFilter\"\n data-automation-id=\"novo-data-table-filter-clear\">\n {{ labels.clear }}\n </novo-button>\n </div>\n `,\n changeDetection: ChangeDetectionStrategy.OnPush,\n standalone: false,\n})\nexport class NovoDataTableCellFilterHeader {\n @Input() label: string | number;\n\n @Input()\n set filter(filter: any) {\n this._filter = filter;\n this.hasFilter = !Helpers.isEmpty(filter);\n }\n get filter(): any {\n return this._filter;\n }\n private _filter: any;\n\n public hasFilter = false;\n\n @Output() clearFilter: EventEmitter<void> = new EventEmitter<void>();\n\n constructor(public changeDetectorRef: ChangeDetectorRef, public labels: NovoLabelService) {}\n}\n","import { CdkColumnDef } from '@angular/cdk/table';\nimport {\n ChangeDetectionStrategy,\n ChangeDetectorRef,\n Component,\n ElementRef,\n EventEmitter,\n HostBinding,\n HostListener,\n Input,\n OnDestroy,\n OnInit,\n Optional,\n Output,\n Renderer2,\n TemplateRef,\n ViewChild,\n} from '@angular/core';\nimport { fromEvent, Subscription } from 'rxjs';\nimport { NovoLabelService } from 'novo-elements/services';\nimport { Helpers, Key } from 'novo-elements/utils';\nimport { NovoDropdownElement } from 'novo-elements/elements/dropdown';\nimport {\n IDataTableChangeEvent,\n IDataTableColumn,\n IDataTableColumnFilterConfig,\n IDataTableColumnFilterOption,\n IDataTableColumnSortConfig,\n IDataTableSortFilter,\n} from '../interfaces';\nimport { NovoDataTableFilterUtils } from '../services/data-table-filter-utils';\nimport { SortDirection } from '../sort-filter';\nimport { NovoDataTableSortFilter } from '../sort-filter/sort-filter.directive';\nimport { DataTableState } from '../state/data-table-state.service';\n\n@Component({\n selector: '[novo-data-table-cell-config]',\n template: `\n <i class=\"bhi-{{ labelIcon }} label-icon\" *ngIf=\"labelIcon\" data-automation-id=\"novo-data-table-header-icon\"></i>\n <label data-automation-id=\"novo-data-table-label\">{{ label }}</label>\n <div>\n <novo-sort-button\n *ngIf=\"config.sortable\"\n data-automation-id=\"novo-data-table-sort\"\n tooltipPosition=\"left\"\n [tooltip]=\"labels.sort\"\n [attr.data-feature-id]=\"'novo-data-table-sort-' + this.id\"\n (sortChange)=\"sort()\"\n [value]=\"sortValue\"></novo-sort-button>\n <novo-dropdown\n *ngIf=\"config.filterable\"\n side=\"left\"\n parentScrollSelector=\".novo-data-table-container\"\n containerClass=\"data-table-dropdown\"\n data-automation-id=\"novo-data-table-filter\"\n [multiple]=\"multiSelect\">\n <novo-icon\n dropdownTrigger\n class=\"filter-button\"\n [class.filter-active]=\"filterActive\"\n [tooltip]=\"labels.filters\"\n tooltipPosition=\"right\"\n [attr.data-feature-id]=\"'novo-data-table-filter-' + this.id\"\n (click)=\"clickedFilter($event)\">filter</novo-icon>\n <ng-container [ngSwitch]=\"config.filterConfig.type\">\n <ng-container *ngSwitchCase=\"'date'\" (keydown.escape)=\"handleEscapeKeydown($event)\">\n <novo-data-table-cell-filter-header [filter]=\"filter\" (clearFilter)=\"clearFilter()\"></novo-data-table-cell-filter-header>\n <div class=\"optgroup-container\">\n <novo-optgroup>\n <ng-container *ngIf=\"!showCustomRange\">\n <novo-option\n [class.active]=\"activeDateFilter === option.label\"\n *ngFor=\"let option of config.filterConfig.options\"\n (click)=\"filterData(option)\"\n [attr.data-automation-id]=\"'novo-data-table-filter-' + option.label\">\n <span>{{ option.label }}</span>\n <novo-icon novoSuffix color=\"positive\" *ngIf=\"activeDateFilter === option.label\">check</novo-icon>\n </novo-option>\n </ng-container>\n <novo-option\n [class.active]=\"labels.customDateRange === activeDateFilter\"\n (click)=\"toggleCustomRange($event, true)\"\n *ngIf=\"config.filterConfig.allowCustomRange && !showCustomRange\">\n <span>{{ labels.customDateRange }}</span>\n <novo-icon novoSuffix color=\"positive\" *ngIf=\"labels.customDateRange === activeDateFilter\">check</novo-icon>\n </novo-option>\n <novo-option class=\"calendar-container\" *ngIf=\"showCustomRange\" keepOpen>\n <novo-stack>\n <div class=\"back-link\" (click)=\"toggleCustomRange($event, false)\">\n <i class=\"bhi-previous\"></i>\n {{ labels.backToPresetFilters }}\n </div>\n <novo-date-picker\n (onSelect)=\"filterData($event)\"\n [(ngModel)]=\"filter\"\n range=\"true\"\n (keydown.escape)=\"handleEscapeKeydown($event)\"></novo-date-picker>\n </novo-stack>\n </novo-option>\n </novo-optgroup>\n </div>\n </ng-container>\n <ng-container *ngSwitchCase=\"'select'\">\n <novo-data-table-cell-filter-header [filter]=\"filter\" (clearFilter)=\"clearFilter()\"></novo-data-table-cell-filter-header>\n <div class=\"optgroup-container\">\n <novo-optgroup>\n <novo-option\n [class.active]=\"filter === option\"\n *ngFor=\"let option of config.filterConfig.options\"\n (click)=\"filterData(option)\"\n [attr.data-automation-id]=\"'novo-data-table-filter-' + (option?.label || option)\">\n <span>{{ option?.label || option }}</span>\n <novo-icon novoSuffix color=\"positive\" *ngIf=\"option.hasOwnProperty('value') ? filter === option.value : filter === option\">\n check</novo-icon>\n </novo-option>\n </novo-optgroup>\n </div>\n </ng-container>\n <ng-container *ngSwitchCase=\"'multi-select'\">\n <novo-data-table-cell-filter-header [filter]=\"filter\" (clearFilter)=\"clearFilter()\"></novo-data-table-cell-filter-header>\n <div class=\"optgroup-container\">\n <novo-optgroup class=\"dropdown-list-filter\" (keydown)=\"multiSelectOptionFilterHandleKeydown($event)\">\n <novo-option class=\"filter-search\" novoInert>\n <novo-field flex>\n <input\n novoInput\n [(ngModel)]=\"optionFilter\"\n (ngModelChange)=\"multiSelectOptionFilter($event)\"\n #optionFilterInput\n data-automation-id=\"novo-data-table-multi-select-option-filter-input\"\n (keydown.enter)=\"multiSelectOptionFilterHandleKeydown($event)\" />\n <novo-icon novoSuffix>search</novo-icon>\n <novo-error class=\"error-text\" [hidden]=\"!error || !multiSelectHasVisibleOptions()\">\n {{ labels.selectFilterOptions }}\n </novo-error>\n </novo-field>\n </novo-option>\n </novo-optgroup>\n <novo-optgroup class=\"dropdown-list-options\" (keydown.escape)=\"handleEscapeKeydown($event)\">\n <novo-option\n *ngFor=\"let option of config.filterConfig.options\"\n [hidden]=\"multiSelectOptionIsHidden(option)\"\n (click)=\"toggleSelection(option)\"\n [attr.data-automation-id]=\"'novo-data-table-filter-' + (option?.label || option)\">\n <span>{{ option?.label || option }}</span>\n <novo-icon novoSuffix color=\"positive\">\n {{ isSelected(option, multiSelectedOptions) ? 'checkbox-filled' : 'checkbox-empty' }}\n </novo-icon>\n </novo-option>\n </novo-optgroup>\n <novo-option class=\"filter-null-results\" [hidden]=\"multiSelectHasVisibleOptions()\">{{ labels.pickerEmpty }}</novo-option>\n </div>\n </ng-container>\n <ng-container *ngSwitchCase=\"'custom'\">\n <ng-container *ngIf=\"dropdown\">\n <novo-data-table-cell-filter-header *ngIf=\"!config.filterConfig?.useCustomHeader\" [filter]=\"filter\" (clearFilter)=\"clearFilter()\"></novo-data-table-cell-filter-header>\n <div class=\"optgroup-container\">\n <ng-container *ngTemplateOutlet=\"filterTemplate; context: { $implicit: config, column, dropdown, filter }\"></ng-container>\n </div>\n </ng-container>\n </ng-container>\n <ng-container *ngSwitchDefault (keydown.escape)=\"handleEscapeKeydown($event)\">\n <novo-data-table-cell-filter-header [filter]=\"filter\" (clearFilter)=\"clearFilter()\"></novo-data-table-cell-filter-header>\n <div class=\"optgroup-container\">\n <novo-optgroup>\n <novo-option class=\"filter-search\" novoInert>\n <novo-field flex fullWidth>\n <input\n novoInput\n [type]=\"config.filterConfig.type\"\n [(ngModel)]=\"filter\"\n (ngModelChange)=\"filterData($event)\"\n #filterInput\n data-automation-id=\"novo-data-table-filter-input\"\n (keydown.escape)=\"handleEscapeKeydown($event)\" />\n <novo-icon novoSuffix>search</novo-icon>\n </novo-field>\n </novo-option>\n </novo-optgroup>\n </div>\n </ng-container>\n </ng-container>\n <div class=\"footer\" *ngIf=\"multiSelect\">\n <novo-button theme=\"dialogue\" color=\"dark\" (click)=\"cancel()\" data-automation-id=\"novo-data-table-multi-select-cancel\">\n {{ labels.cancel }}\n </novo-button>\n <novo-button\n theme=\"dialogue\"\n color=\"positive\"\n (click)=\"filterMultiSelect()\"\n data-automation-id=\"novo-data-table-multi-select-filter\">\n {{ labels.filters }}\n </novo-button>\n </div>\n </novo-dropdown>\n </div>\n <div class=\"spacer\"></div>\n <div class=\"data-table-header-resizable\" *ngIf=\"config.resizable\"><span (mousedown)=\"startResize($event)\"> </span></div>\n `,\n changeDetection: ChangeDetectionStrategy.OnPush,\n standalone: false,\n})\nexport class NovoDataTableCellHeader<T> implements IDataTableSortFilter, OnInit, OnDestroy {\n @ViewChild('filterInput')\n filterInput: ElementRef;\n @ViewChild(NovoDropdownElement)\n dropdown: NovoDropdownElement;\n @ViewChild('optionFilterInput')\n optionFilterInput: ElementRef;\n\n @Input()\n defaultSort: { id: string; value: string };\n\n @Input()\n allowMultipleFilters: boolean = false;\n\n @Input()\n resized: EventEmitter<IDataTableColumn<T>>;\n @Input()\n filterTemplate: TemplateRef<any>;\n\n @Output()\n toggledFilter = new EventEmitter<string>();\n @HostBinding('class.resizable')\n public resizable: boolean;\n\n @Input('novo-data-table-cell-config')\n set column(column: IDataTableColumn<T>) {\n this._column = column;\n this.label = column.type === 'action' ? '' : column.label;\n this.labelIcon = column.labelIcon;\n\n this.config = {\n sortable: !!column.sortable,\n filterable: !!column.filterable,\n resizable: !!column.resizable,\n };\n this.resizable = this.config.resizable;\n\n const transforms: { filter?: Function; sort?: Function } = {};\n\n if (column.filterable && Helpers.isObject(column.filterable)) {\n this.config.filterConfig = column.filterable as IDataTableColumnFilterConfig;\n if (!this.config.filterConfig.type) {\n this.config.filterConfig = { type: 'text' };\n }\n if ((column.filterable as IDataTableColumnFilterConfig).transform) {\n transforms.filter = (column.filterable as IDataTableColumnFilterConfig).transform;\n }\n } else {\n this.config.filterConfig = { type: 'text' };\n }\n\n if (column.sortable && Helpers.isObject(column.sortable)) {\n if ((column.sortable as IDataTableColumnSortConfig).transform) {\n transforms.sort = (column.sortable as IDataTableColumnSortConfig).transform;\n }\n }\n\n if (this.config.filterConfig.type === 'date' && !this.config.filterConfig.options) {\n this.config.filterConfig.options = this.getDefaultDateFilterOptions();\n }\n\n this.config.transforms = transforms;\n }\n get column(): IDataTableColumn<T> {\n return this._column;\n }\n private _column: IDataTableColumn<T>;\n\n private _rerenderSubscription: Subscription;\n private changeTimeout: any;\n\n public label: string;\n public icon: string = 'sortable';\n public labelIcon: string;\n public id: string;\n public filter: any;\n public direction: string;\n public filterActive: boolean = false;\n public sortActive: boolean = false;\n public sortValue: SortDirection = SortDirection.NONE;\n public showCustomRange: boolean = false;\n public activeDateFilter: string;\n public config: {\n sortable: boolean;\n filterable: boolean;\n resizable: boolean;\n transforms?: { filter?: Function; sort?: Function };\n filterConfig?: IDataTableColumnFilterConfig;\n };\n public multiSelect: boolean = false;\n public multiSelectedOptions: Array<any> = [];\n private multiSelectedOptionIsHidden: Array<{ option: string | IDataTableColumnFilterOption; hidden: boolean }> = [];\n public optionFilter: string = '';\n public error: boolean = false;\n private subscriptions: Subscription[] = [];\n\n constructor(\n public changeDetectorRef: ChangeDetectorRef,\n public labels: NovoLabelService,\n private state: DataTableState<T>,\n private renderer: Renderer2,\n private elementRef: ElementRef,\n @Optional() public _sort: NovoDataTableSortFilter<T>,\n @Optional() public _cdkColumnDef: CdkColumnDef,\n ) {\n this._rerenderSubscription = state.updates.subscribe((change: IDataTableChangeEvent) => this.checkSortFilterState(change));\n }\n\n public ngOnInit(): void {\n if (this._cdkColumnDef) {\n this.id = this._cdkColumnDef.name;\n }\n this.setupFilterOptions();\n\n this.changeDetectorRef.markForCheck();\n }\n\n public setupFilterOptions() {\n this.checkSortFilterState({ filter: this.state.filter, sort: this.state.sort }, true);\n\n this.multiSelect = this.config.filterConfig && this.config.filterConfig.type ? this.config.filterConfig.type === 'multi-select' : false;\n if (this.multiSelect) {\n this.multiSelectedOptions = this.filter ? [...this.filter] : [];\n }\n }\n\n public ngOnDestroy(): void {\n this._rerenderSubscription.unsubscribe();\n this.subscriptions.forEach((subscription: Subscription) => {\n subscription.unsubscribe();\n });\n }\n\n public checkSortFilterState(sortFilterState: IDataTableChangeEvent, initialConfig: boolean = false): void {\n if (sortFilterState.sort && sortFilterState.sort.id === this.id) {\n this.icon = `sort-${sortFilterState.sort.value}`;\n this.sortValue = sortFilterState.sort.value === 'asc' ? SortDirection.ASC : SortDirection.DESC;\n this.sortActive = true;\n } else {\n this.icon = 'sortable';\n this.sortValue = SortDirection.NONE;\n this.sortActive = false;\n }\n\n const tableFilter = Helpers.convertToArray(sortFilterState.filter);\n const thisFilter = tableFilter.find((filter) => filter && filter.id === this.id);\n\n if (thisFilter) {\n this.filterActive = true;\n if (initialConfig && thisFilter.type === 'date' && thisFilter.selectedOption) {\n this.activeDateFilter = thisFilter.selectedOption.label || this.labels.customDateRange;\n }\n this.filter = thisFilter.value;\n } else {\n this.filterActive = false;\n this.filter = undefined;\n this.activeDateFilter = undefined;\n this.multiSelectedOptions = [];\n }\n if (this.defaultSort && this.id === this.defaultSort.id) {\n this.icon = `sort-${this.defaultSort.value}`;\n this.sortActive = true;\n }\n this.multiSelect = this.config.filterConfig && this.config.filterConfig.type ? this.config.filterConfig.type === 'multi-select' : false;\n if (this.multiSelect) {\n this.multiSelectedOptions = this.filter ? [...this.filter] : [];\n if (this.config.filterConfig.options) {\n if (typeof this.config.filterConfig.options[0] === 'string') {\n this.multiSelectedOptionIsHidden = (this.config.filterConfig.options as string[]).map(\n (\n option: string,\n ): {\n option: string;\n hidden: boolean;\n } => ({ option, hidden: false }),\n );\n } else {\n this.multiSelectedOptionIsHidden = (this.config.filterConfig.options as IDataTableColumnFilterOption[]).map(\n (option: IDataTableColumnFilterOption): { option: IDataTableColumnFilterOption; hidden: boolean } => ({\n option,\n hidden: false,\n }),\n );\n }\n }\n }\n this.changeDetectorRef.markForCheck();\n }\n\n public isSelected(option, optionsList) {\n if (optionsList) {\n const optionValue = option.hasOwnProperty('value') ? option.value : option;\n\n const found = optionsList.find((item) => this.optionPresentCheck(item, optionValue));\n return found !== undefined;\n }\n return false;\n }\n\n public toggleSelection(option) {\n const optionValue = option.hasOwnProperty('value') ? option.value : option;\n\n const optionIndex = this.multiSelectedOptions.findIndex((item) => this.optionPresentCheck(item, optionValue));\n this.error = false;\n if (optionIndex > -1) {\n this.multiSelectedOptions.splice(optionIndex, 1);\n if (this.optionFilter && !this.getOptionText(option).toLowerCase().startsWith(this.optionFilter.toLowerCase())) {\n this.multiSelectedOptionIsHidden[this.multiSelectedOptionIsHidden.findIndex((record) => record.option === option)].hidden = true;\n }\n } else {\n this.multiSelectedOptions.push(optionValue);\n }\n }\n\n public optionPresentCheck(item, optionValue): boolean {\n if (item.hasOwnProperty('value')) {\n return item.value === optionValue;\n } else {\n return item === optionValue;\n }\n }\n\n public cancel(): void {\n this.multiSelectedOptions = this.filter ? [...this.filter] : [];\n this.dropdown.closePanel();\n this.clearOptionFilter();\n }\n\n public filterMultiSelect(): void {\n if (this.multiSelectedOptions.length === 0 && !this.filter) {\n this.multiSelectHasVisibleOptions() && this.dropdown ? (this.error = true) : null;\n } else {\n this.clearOptionFilter();\n const actualFilter = this.multiSelectedOptions.length > 0 ? [...this.multiSelectedOptions] : undefined;\n this.filterData(actualFilter);\n this.dropdown.closePanel();\n }\n }\n\n public multiSelectOptionFilter(optionFilter: string) {\n this.multiSelectedOptionIsHidden.forEach((record) => {\n if (record.option) {\n record.hidden = !(\n this.getOptionText(record.option).toLowerCase().startsWith(optionFilter.toLowerCase()) ||\n this.isSelected(record.option, this.multiSelectedOptions)\n );\n }\n });\n }\n\n public multiSelectOptionIsHidden(option: string | IDataTableColumnFilterOption): boolean {\n return this.multiSelectedOptionIsHidden.find((record) => record.option === option).hidden;\n }\n\n public multiSelectHasVisibleOptions(): boolean {\n return this.multiSelectedOptionIsHidden.some((record) => !record.hidden);\n }\n\n private getOptionText(option: string | IDataTableColumnFilterOption): string {\n if (typeof option !== 'object') {\n return option.toString();\n } else {\n const opt = option as IDataTableColumnFilterOption;\n return (opt.label.length > 0 ? opt.label : opt.value).toString();\n }\n }\n\n @HostListener('keydown', ['$event'])\n public multiSelectOptionFilterHandleKeydown(event: KeyboardEvent) {\n if (this.multiSelect) {\n this.error = false;\n if (this.dropdown.panelOpen && event.key === Key.Escape) {\n // escape should clear text box and close\n Helpers.swallowEvent(event);\n this.clearOptionFilter();\n this.dropdown.closePanel();\n } else if (event.key === Key.Enter) {\n Helpers.swallowEvent(event);\n this.filterMultiSelect();\n } else if (\n (event.keyCode >= 65 && event.keyCode <= 90) ||\n (event.keyCode >= 96 && event.keyCode <= 105) ||\n (event.keyCode >= 48 && event.keyCode <= 57)\n ) {\n this.optionFilterInput.nativeElement.focus();\n }\n }\n }\n\n @HostListener('keydown.escape', ['$event'])\n public handleEscapeKeydown(event: KeyboardEvent) {\n if (!this.multiSelect) {\n this.error = false;\n this.dropdown.closePanel();\n }\n }\n\n private clearOptionFilter() {\n this.error = false;\n if (this.optionFilter.length > 0) {\n this.optionFilter = '';\n this.multiSelectedOptionIsHidden.forEach((record) => {\n record.hidden = false;\n });\n }\n }\n\n public startResize(mouseDownEvent: MouseEvent): void {\n mouseDownEvent.preventDefault();\n const minimumWidth = 60 + (this.config.filterable ? 30 : 0) + (this.config.sortable ? 30 : 0);\n const startingWidth: number = this.elementRef.nativeElement.getBoundingClientRect().width;\n const mouseMoveSubscription: Subscription = fromEvent(window.document, 'mousemove').subscribe((middleMouseEvent: MouseEvent) => {\n const differenceWidth: number = middleMouseEvent.clientX - mouseDownEvent.clientX;\n let width: number = startingWidth + differenceWidth;\n if (width < minimumWidth) {\n width = minimumWidth;\n }\n this.setWidth(width)\n });\n\n const mouseUpSubscription: Subscription = fromEvent(window.document, 'mouseup').subscribe(() => {\n mouseUpSubscription.unsubscribe();\n mouseMoveSubscription.unsubscribe();\n this.changeDetectorRef.markForCheck();\n });\n this.subscriptions.push(mouseMoveSubscription);\n this.subscriptions.push(mouseUpSubscription);\n }\n\n public setWidth(width: number) {\n this._column.width = width;\n this.renderer.setStyle(this.elementRef.nativeElement, 'min-width', `${width}px`);\n this.renderer.setStyle(this.elementRef.nativeElement, 'max-width', `${width}px`);\n this.renderer.setStyle(this.elementRef.nativeElement, 'width', `${width}px`);\n this.changeDetectorRef.markForCheck();\n this.resized.next(this._column);\n }\n\n public toggleCustomRange(event: Event, value: boolean): void {\n Helpers.swallowEvent(event);\n this.showCustomRange = value;\n this.changeDetectorRef.markForCheck();\n this.dropdown.openPanel(); // Ensures that the panel correctly updates to the dynamic size of the dropdown\n }\n\n public clickedFilter(clickEvt: MouseEvent): void {\n if ((typeof this.config.filterConfig === 'object') && this.config.filterConfig.type === 'custom' && !this.filterTemplate) {\n this.toggledFilter.next(this.id);\n clickEvt.stopImmediatePropagation();\n return;\n }\n this.focusInput();\n if (this.multiSelect && this.dropdown) {\n this.dropdown._handleKeydown = (event: KeyboardEvent) => {\n this.multiSelectOptionFilterHandleKeydown(event);\n };\n this.changeDetectorRef.markForCheck();\n }\n }\n\n public focusInput(): void {\n if (this.filterInput?.nativeElement) {\n setTimeout(() => this.filterInput.nativeElement.focus());\n }\n }\n\n public sort(): void {\n if (this.changeTimeout) {\n clearTimeout(this.changeTimeout);\n }\n this.changeTimeout = setTimeout(() => {\n this.direction = this.getNextSortDirection(this.direction);\n this._sort.sort(this.id, this.direction, this.config.transforms.sort);\n this.changeDetectorRef.markForCheck();\n }, 300);\n }\n\n public filterData(filter?: any): void {\n let actualFilter = NovoDataTableFilterUtils.constructFilter(filter, this.config.filterConfig.type, this.multiSelect);\n const selectedOption = this.config.filterConfig.type === 'date' && filter ? filter : undefined;\n this.activeDateFilter = selectedOption ? selectedOption.label : undefined;\n\n if (this.ch