// 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);
}
}