// Angular imports // import { Component, OnInit, AfterContentInit, ContentChildren, TemplateRef, Input, Output, Inject, ElementRef, EventEmitter, QueryList } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { SlicePipe } from '@angular/common'; // Components // import * as _ from 'underscore'; // Interfaces // // Services // // Directives // import { FbTableRowDirective as fbTableRow } from './fbTableRow.directive'; import { FbTableFootDirective as fbTableFoot } from './fbTableFoot.directive'; // Pipes // import { OrderByObjectPipe } from '@fb/common/pipes/orderByObject.pipe'; // För utskrift declare const jQuery: JQueryStatic; /** * Inspirerad av https://www.primefaces.org/primeng/#/datalist * * Visar en tabell, baserad på fbTable.ts, med stöd för sortering och flera sidor. * Begränsningar jämfört med fbTable.ts: * - onViewUpdated är inte implementerad. Det kan vara ganska dyrt att lyssna efter vyförändringar. Om man senare kommer fram till att den krävs så kan den implementeras * genom att man manuellt håller koll på när visibleValueList ändras * - passthrough är inte implementerad. Förmodligen behövs den inte längre men kan implementeras vid behovs. * - countRows verkade inte användas på något vettigt sätt och därför borttagen * - showWarningDeselectAll är inte implementerad. Användes alltid tillsammans med showWarningIsAllSelected och funktionaliteten ytterst oklar. Borttagen. * * @example * * * * * * Kolumn 1 * * * Kolumn 2 * * * * * * * {{d.kolumn1}} * ... * * * * * {{d.kolumn1}} * ... * * * * * * * Parametrar th/td: * fbSelectElement Kolumnen används för att markera raden när man klickar på den. Tar aktuell rad som värde. Om inget värde sätts så markeras alla rader. * * Metoder: * isSelected(rad) är en metod på tabellen som kan användas för att se om raden är markerad * isAllSelected() / isAllSelectedInPage() är metoder som kan användas för att se om alla rader är markerade * * fbTableRow: * Sätts typiskt på ng-container, enligt syntaxen ovan. Första mallen används för rader. Andra mallen används för underrader. * * fbTableFoot: * Sätts typiskt på ng-container, enligt syntaxen ovan. Används som mall för footer (det i tfoot). Hindrar annan footer från att användas . * */ @Component({ selector: 'fb-table', templateUrl: './fb-table.component.html' }) export class FbTableComponent implements OnInit, AfterContentInit { @ContentChildren(fbTableRow) templateList: QueryList; @ContentChildren(fbTableFoot) footTemplateList: QueryList; /** * Lista med värden att visa i tabellen */ @Input() values: any[]; /** * "single" eller "multiple". Måste vara satt om fbSelectElement används */ @Input() select: string; /** * Visa varning (fb-alert) om alla rader markeras */ @Input() showWarningIsAllSelected: boolean; /** * Lista med värden som markerats */ @Input() selectedItems: any[]; /** * Parameter-namn att sortera efter i början */ @Input() defaultOrder: string; /** * Sorterar som standard fallande om satt till true */ @Input() defaultOrderReverse: boolean; /** * True om tabellen ska kunna skrivas ut */ @Input() printable: boolean; /** * Funktion som alla värderader ska skickas till. Funktionen ska returnera true om värdet ska inkluderas i tabellen. * Kan typiskt användas för dynamisk filtrering. */ @Input() tableFilter: (data: any) => boolean; /** * Sätt attributet för att dela upp tabellen i sidor. Varje sida innehåller "itemsPerPage" rader. */ @Input() itemsPerPage: number; /** * Parameter som ska användas för att leta efter underrader. Kräver två stycken fbTableRow */ @Input() subGroupProperty: string; /** * Händelse som sker då rader markeras. Raden skickas som event */ @Output() onSelect: EventEmitter = new EventEmitter(); /** * Händelse som sker då sorteringen ändras. $event.order och $event.reversed kan användas för information om sortering */ @Output() onOrderUpdate: EventEmitter<{ order: string, reversed: boolean }> = new EventEmitter<{ order: string, reversed: boolean }>(); page: number = 0; subValueTemplate: TemplateRef; footTemplate: TemplateRef; warningIsAllSelectedInPage: boolean; warningIsAllSelected: boolean; private valueTemplate: TemplateRef; private orderReversed: boolean; private order: string; private latestSelect: any; get visibleValueList(): any[] { let values: any[] = this.orderByObject.transform( this.values, this.order, this.orderReversed); if (this.tableFilter) { values = values.filter(this.tableFilter); } if (this.itemsPerPage) { // Se till att vi inte hamnat på en ogiltig sida, t.ex. av att // värdemängden har minskat via filtret if (values.length === 0) { this.page = 0; } else if (this.page >= values.length / this.itemsPerPage) { this.page = Math.floor((values.length - 1) / this.itemsPerPage); } values = new SlicePipe().transform(values, this.page * this.itemsPerPage, this.page * this.itemsPerPage + this.itemsPerPage); } return values; } constructor( private readonly orderByObject: OrderByObjectPipe, private readonly elementRef: ElementRef, private readonly translate: TranslateService, @Inject('UtskriftService') private readonly utskrift: fb.IUtskriftService) { } ngOnInit(): void { this.selectedItems = this.selectedItems || []; this.values = this.values || []; this.orderReversed = !!this.defaultOrderReverse; if (this.defaultOrder) { this.order = this.defaultOrder; } } ngAfterContentInit(): void { this.valueTemplate = this.templateList.first.template; if (this.templateList.length === 2) { if (!this.subGroupProperty) { throw new Error('subGroupProperty är inte satt'); } // Subrow this.subValueTemplate = this.templateList.last.template; } else if (this.templateList.length > 3) { // Vi tillåter bara en row-template och en subrow-template throw new Error('För många fbTableRow i tabellen'); } if (this.footTemplateList.length === 1) { this.footTemplate = this.footTemplateList.first.template; } else if (this.footTemplateList.length > 1) { // Vi tillåter bara en foot-template throw new Error('För många fbTableFoot i tabellen'); } } // Selection isAllSelected(): boolean { return this.selectedItems.length > 0 && this.selectedItems.length === this.getRowCount(); } isAllSelectedInPage(): boolean { return this.visibleValueList.every(r => this.selectedItems.indexOf(r) !== -1); } isSelected(value: any): boolean { return this.selectedItems.indexOf(value) >= 0; } selectItem(value: any, event: MouseEvent): void { if (this.select === 'single') { this.singleSelect(value); } else if (this.select === 'multiple') { this.multipleSelect(value, event); } else { throw new Error('select måste vara \'single\' eller \'multiple\''); } } selectNone(): void { this.selectedItems.length = 0; this.warningIsAllSelected = false; this.warningIsAllSelectedInPage = false; } selectAll(): void { if (this.select !== 'multiple') { throw new Error('Select är inte satt till \'multiple\''); } const selectedItems: any[] = this.selectedItems; if (selectedItems.length === this.getRowCount()) { selectedItems.length = 0; } else { selectedItems.length = 0; const rows: any[] = this.tableFilter ? _.filter(this.values || [], this.tableFilter) : (this.values || []); const subGroupProperty: string = this.subGroupProperty; _.each(rows, r => { selectedItems.push(r); const subArr: any[] = r[subGroupProperty]; if (_.isArray(subArr)) { for (const item in subArr) { selectedItems.push(item); } } }); } if (this.showWarningIsAllSelected) { this.warningIsAllSelectedInPage = false; this.warningIsAllSelected = true; } } toggleSelectAllInPage(): void { if (this.select !== 'multiple') { throw new Error('Select är inte satt till \'multiple\''); } if (!this.isAllSelectedInPage()) { const selectObjects: any[] = this.getRowsIncludedSub(this.visibleValueList .filter(r => !_.contains(this.selectedItems, r))); selectObjects.forEach(e => { this.selectedItems.push(e); }); if (this.selectedItems.length > 0 && !this.isAllSelected()) { this.warningIsAllSelectedInPage = true; } } else { // Ta bort alla som syns på den här sidan const tempArr: any[] = this.selectedItems .filter(obj => !_.contains(this.getRowsIncludedSub(this.visibleValueList), obj)); this.selectedItems.length = 0; tempArr.forEach(e => { this.selectedItems.push(e); }); this.warningIsAllSelectedInPage = false; } } setOrder(order: string): void { if (order !== this.order) { this.orderReversed = false; this.order = order; } else { this.orderReversed = !this.orderReversed; } this.sortSelected(); } isAscending(order: string): boolean { return !this.orderReversed && this.order === order; } isDescending(order: string): boolean { return this.orderReversed && this.order === order; } clickRow(rowValue: any): void { this.onSelect.emit(rowValue); } printTable(): void { this.utskrift.skrivUtTabellHtml(jQuery(this.elementRef.nativeElement)); } pages(): number[] { return Array.apply(null, new Array(this.lastPage() + 1)).map((_v, i) => i); } lastPage(): number { return this.values ? (Math.ceil(this.getRowCount() / this.itemsPerPage) - 1) : 0; } nbrResultInCurrentPage(): string { if (this.values.length > 0) { const showingRowsFrom: number = this.page * this.itemsPerPage; const showingRowsTo: number = Math.min(showingRowsFrom + this.itemsPerPage, this.getRowCount()); return this.translate.instant('OBJEKTLISTA.VISAR_TRAEFFAR') + ' ' + (showingRowsFrom + 1) + '-' + showingRowsTo + '.'; } return ''; } private sortSelected(): void { const order: string = this.order; this.selectedItems.sort((a: string, b: string) => { const splitted: string[] = order.split('.'); let valA: string = a; let valB: string = b; for (const item of splitted) { valA = valA[item]; valB = valB[item]; if (!valA && !valB) { return 0; } else if (!valA) { return -1; } else if (!valB) { return -1; } } if (valA > valB) { return 1; } else if (valA === valB) { return 0; } else { return -1; } }); if (this.orderReversed) { this.selectedItems.reverse(); } this.onOrderUpdate.emit({ order: this.order, reversed: this.orderReversed }); } private singleSelect(value: any): void { if (this.selectedItems[0] === value) { delete this.selectedItems[0]; } else { this.selectedItems[0] = value; } } private multipleSelect(value: any, event: MouseEvent): void { if (event.shiftKey && this.latestSelect) { this.selectedItems.length = 0; let list: any[] = this.orderByObject.transform( this.values, this.order, this.orderReversed); const temp: any[] = []; _.each(list, item => { temp.push(item); _.each(item.subRows, item2 => { temp.push(item2); }); }); list = temp; const index1: number = list.indexOf(value); const index2: number = list.indexOf(this.latestSelect); const minIndex: number = index1 < index2 ? index1 : index2; const maxIndex: number = index1 > index2 ? index1 : index2; for (let k: number = minIndex; k <= maxIndex; k++) { this.selectedItems.push(list[k]); } this.latestSelect = value; } else { if (this.selectedItems.indexOf(value) >= 0) { this.selectedItems.splice(this.selectedItems.indexOf(value), 1); } else { this.selectedItems.push(value); this.latestSelect = value; } } } getRowCount(): number { const rows: any[] = this.tableFilter ? _.filter(this.values || [], this.tableFilter) : (this.values || []); let count: number = rows.length; const subGroupProperty: string = this.subGroupProperty; _.each(rows, r => { const subArr: any[] = r[subGroupProperty]; if (_.isArray(subArr)) { count += subArr.length; } }); return count; } // "Flattens" subGroupProperty på @array private getRowsIncludedSub(array: any[]): any[] { const subArr: any[] = array .map(sO => sO[this.subGroupProperty]) .filter((arr): boolean => _.isArray(arr)) .reduce((acc, val) => acc.concat(val), []); return array.concat(subArr); } }