@codehint-ng/tree-table
Version:
A simple component to display tree-hierarchically data in table.
416 lines (405 loc) • 22.4 kB
JavaScript
import * as i0 from '@angular/core';
import { Directive, Input, Pipe, EventEmitter, Component, Output, NgModule } from '@angular/core';
import { of } from 'rxjs';
import * as i1 from '@angular/common';
import { CommonModule } from '@angular/common';
let Helper$1 = class Helper {
static sortItems(items, sortFunction) {
const sortFunc = (a, b) => {
return !!a.data && !!b.data ? sortFunction(a.data, b.data) : 0;
};
return items.sort(sortFunc);
}
static allParentsExpanded(shell) {
let parent = shell.parent;
while (!!parent) {
if (!parent.item.isExpanded) {
return false;
}
parent = parent.parent;
}
return true;
}
static itemsToShells(items, level, parent) {
const shells = items.map(item => {
const shell = {
item,
haveChildren: false,
level,
isVisible: level === 0,
parent: parent.item.id === null ? null : parent
};
return shell;
});
return shells;
}
static arrayInsertItems(arr, index, newItems) {
const newArr = [...arr.slice(0, index), ...newItems, ...arr.slice(index)];
return newArr;
}
// stable
static selectLevelItems(items, parent) {
const levelItems = [];
const otherItems = [];
items.forEach(item => {
if (item.parentId === parent.id) {
levelItems.push(item);
}
else {
otherItems.push(item);
}
});
return { levelItems, otherItems };
}
static removeAllShellsChildren(outerShells, parentId) {
let shells = outerShells;
const removedIds = shells
.filter(i => i.item.parentId === parentId).map(i => i.item.id);
removedIds.forEach((id) => {
shells = this.removeAllShellsChildren(shells, id);
});
return shells.filter(i => !removedIds.includes(i.item.id));
}
static removeAllChildren(outerItems, parentId) {
let items = outerItems;
const removedIds = items
.filter(i => i.parentId === parentId).map(i => i.id);
removedIds.forEach((id) => {
items = this.removeAllChildren(items, id);
});
return items.filter(i => !removedIds.includes(i.id));
}
};
class SortHelper {
static { this.otherShells = []; }
static { this.shells = []; }
static { this.sortFunction = null; }
static getSorted(shells, sortFunction) {
this.otherShells = [...shells];
this.shells = [];
this.sortFunction = sortFunction;
this.getShellsForLevel(0, null);
return this.shells;
}
static getShellsForLevel(index, parentId) {
let { levelShells, otherShells } = this.selectLevelShells(this.otherShells, parentId);
if (this.sortFunction) {
levelShells = this.sortShells(levelShells, this.sortFunction);
}
this.shells = Helper$1.arrayInsertItems(this.shells, index, levelShells);
this.otherShells = [...otherShells];
levelShells.forEach(shell => {
const shellIndex = this.shells.findIndex(i => i.item.id === shell.item.id);
this.getShellsForLevel(shellIndex + 1, shell.item.id);
});
}
static selectLevelShells(shells, parentId) {
const levelShells = [];
const otherShells = [];
shells.forEach(shell => {
if (shell.item.parentId === parentId) {
levelShells.push(shell);
}
else {
otherShells.push(shell);
}
});
return { levelShells, otherShells };
}
static sortShells(shells, sortFunction) {
const sortFunc = (a, b) => {
const data1 = a.item.data;
const data2 = b.item.data;
return !!data1 && !!data2 ? sortFunction(data1, data2) : 0;
};
return shells.sort(sortFunc);
}
}
class ResizableTableColumnDirective {
constructor(elementRef) {
this.elementRef = elementRef;
this.resizer = new Resizer();
}
ngOnInit() {
if (this.appResizableTableColumn || this.appResizableTableColumn === '') {
const resizeDiv = Helper.createResizeDiv(this.elementRef.nativeElement);
this.resizer.attachEvents(resizeDiv);
}
}
ngOnDestroy() {
this.resizer.detachEvents();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.3", ngImport: i0, type: ResizableTableColumnDirective, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.1.3", type: ResizableTableColumnDirective, selector: "[appResizableTableColumn]", inputs: { appResizableTableColumn: "appResizableTableColumn" }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.3", ngImport: i0, type: ResizableTableColumnDirective, decorators: [{
type: Directive,
args: [{
selector: '[appResizableTableColumn]'
}]
}], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { appResizableTableColumn: [{
type: Input
}] } });
class Resizer {
attachEvents(resizeDiv) {
this.resizeDiv = resizeDiv;
this.resizeDiv.addEventListener('mousedown', this.resizeDivMouseDown.bind(this));
document.addEventListener('mousemove', this.documentMouseMove.bind(this));
document.addEventListener('mouseup', this.documentMouseUp.bind(this));
}
detachEvents() {
if (this.resizeDiv) {
this.resizeDiv.removeEventListener('mousedown', this.resizeDivMouseDown.bind(this));
document.removeEventListener('mousemove', this.documentMouseMove.bind(this));
document.removeEventListener('mouseup', this.documentMouseUp.bind(this));
}
}
resizeDivMouseDown(e) {
this.curCol = e.target.parentElement;
this.nxtCol = this.curCol.nextElementSibling;
this.pageX = e.pageX;
this.curColWidth = this.curCol.offsetWidth;
if (this.nxtCol) {
this.nxtColWidth = this.nxtCol.offsetWidth;
}
}
documentMouseMove(e) {
if (this.curCol) {
const diffX = e.pageX - this.pageX;
if (this.nxtCol) {
this.nxtCol.style.width = (this.nxtColWidth - (diffX)) + 'px';
}
this.curCol.style.width = (this.curColWidth + diffX) + 'px';
}
}
documentMouseUp(e) {
this.curCol = undefined;
this.nxtCol = undefined;
this.pageX = undefined;
this.nxtColWidth = undefined;
this.curColWidth = undefined;
}
}
class Helper {
static createResizeDiv(thElement) {
const div = this.createDiv(thElement.offsetHeight);
thElement.appendChild(div);
// thElement.style.position = 'relative';
return div;
}
static createDiv(height) {
const div = document.createElement('div');
div.style.top = '0';
div.style.right = '0';
div.style.width = '4px';
div.style.position = 'absolute';
div.style.cursor = 'col-resize';
/* remove backGroundColor later */
div.style.backgroundColor = '#f2f2f2';
div.style.opacity = '0.2';
div.style.userSelect = 'none';
/* table height */
div.style.height = height + 'px';
div.classList.add('col-resize');
return div;
}
}
class NumberToArrayPipe {
transform(value) {
return new Array(value);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.3", ngImport: i0, type: NumberToArrayPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "18.1.3", ngImport: i0, type: NumberToArrayPipe, name: "appNumberToArray" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.3", ngImport: i0, type: NumberToArrayPipe, decorators: [{
type: Pipe,
args: [{
name: 'appNumberToArray'
}]
}] });
class CngTreeTableComponent {
constructor() {
this.toggleExpandItem = new EventEmitter();
this.rowClick = new EventEmitter();
this.tableClass = '';
this.treeTableItemShells = [];
this.otherItems = []; // to avoid unexpected infinite looping, if parent ids errored with never-ending cycle.
}
insertLevelItemsFor(index, parentShell, level) {
const { levelItems, otherItems } = Helper$1.selectLevelItems(this.otherItems, parentShell.item);
if (levelItems.length < 1) {
return;
}
const levelItems2 = this.sortFunction ? Helper$1.sortItems(levelItems, this.sortFunction) : levelItems;
const newLevelShells = Helper$1.itemsToShells(levelItems2, level, parentShell);
this.treeTableItemShells = Helper$1.arrayInsertItems(this.treeTableItemShells, index, newLevelShells);
this.otherItems = [...otherItems];
newLevelShells.forEach((shell) => {
shell.haveChildren = this.otherItems.some(i => i.parentId === shell.item.id);
});
}
toggleExpand(event, shell) {
event.preventDefault();
event.stopPropagation();
shell.item.isExpanded = !shell.item.isExpanded;
this.toggleExpandItem.emit(shell.item);
if (shell.item.isExpanded) {
const shellIndex = this.treeTableItemShells.findIndex(i => i.item.id === shell.item.id);
this.insertLevelItemsFor(shellIndex + 1, shell, shell.level + 1);
}
this.updateVisibility();
}
localRowClick(shell) {
this.rowClick.emit(shell.item);
}
isSelected$(shell) {
const data = shell.item.data;
if (data && this.isSelectedFunc$) {
return this.isSelectedFunc$(data);
}
return of(false);
}
updateVisibility() {
this.treeTableItemShells.forEach(sh => {
sh.isVisible = Helper$1.allParentsExpanded(sh);
});
}
// external functions
clearItems() {
this.treeTableItemShells = [];
this.otherItems = [];
}
appendItems(items) {
this.otherItems = [...this.otherItems, ...items];
const root = { item: { id: null } };
this.insertLevelItemsFor(0, root, 0);
this.treeTableItemShells.forEach(shell => {
if (shell.item.isExpanded) {
const shellIndex = this.treeTableItemShells.findIndex(i => i.item.id === shell.item.id);
this.insertLevelItemsFor(shellIndex + 1, shell, shell.level + 1);
}
else {
shell.haveChildren = this.otherItems.some(i => i.parentId === shell.item.id);
}
});
// fix have children for already added items, but reseted above adding code
this.treeTableItemShells.forEach(shell => {
if (!shell.haveChildren) {
shell.haveChildren = this.isExistsChildrenFor(shell.item.id);
}
});
this.updateVisibility();
}
isExistsChildrenFor(itemId) {
return this.treeTableItemShells
.some(i => {
if (!i) {
return false;
}
if (!i.parent) {
return false;
}
if (!i.parent.item) {
return false;
}
return i.parent.item.id === itemId;
});
}
getChildren(id) {
const fromShells = this.treeTableItemShells
.map(i => i.item)
.filter(i => i.parentId === id);
const fromItems = this.otherItems.filter(i => i.parentId === id);
return [...fromShells, ...fromItems];
}
removeItem(id) {
this.treeTableItemShells = Helper$1.removeAllShellsChildren([...this.treeTableItemShells], id);
const shell = this.treeTableItemShells.find(i => i.item.id === id);
if (shell && shell.parent) {
const parentId = shell.parent.item.id;
const isExistChildShell = this.treeTableItemShells.some(i => i.item.parentId === parentId);
const isExistChild = this.otherItems.some(i => i.parentId === parentId);
shell.parent.haveChildren = isExistChildShell || isExistChild;
}
this.treeTableItemShells = this.treeTableItemShells.filter(i => i.item.id !== id);
this.otherItems = Helper$1.removeAllChildren([...this.otherItems], id);
this.otherItems = this.otherItems
.filter(i => i.id !== id);
}
sort() {
if (!this.sortFunction) {
console.warn('cng-tree-table: there is not sortFunction');
return;
}
this.treeTableItemShells = SortHelper.getSorted(this.treeTableItemShells, this.sortFunction);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.3", ngImport: i0, type: CngTreeTableComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.1.3", type: CngTreeTableComponent, selector: "cng-tree-table", inputs: { columns: "columns", sortFunction: "sortFunction", isSelectedFunc$: "isSelectedFunc$", conditionalCellClassFunc: "conditionalCellClassFunc", tableClass: "tableClass" }, outputs: { toggleExpandItem: "toggleExpandItem", rowClick: "rowClick" }, ngImport: i0, template: "<table [ngClass]=\"tableClass\">\r\n <thead>\r\n <tr>\r\n <th *ngFor=\"let column of columns; let last=last;\"\r\n [appResizableTableColumn]=\"!last\"\r\n class=\"tree-item-header\"\r\n [ngClass]=\"[column.thClass || '']\">\r\n {{column.titleTemplate ? '' : column.title}}\r\n <ng-container *ngTemplateOutlet=\"column.titleTemplate; context: { $implicit: column.title }\">\r\n </ng-container>\r\n </th>\r\n </tr>\r\n </thead>\r\n\r\n <tbody>\r\n <tr [ngClass]=\"{'selected': (isSelected$(shell) | async)}\"\r\n *ngFor=\"let shell of treeTableItemShells\"\r\n [hidden]=\"!shell.isVisible\" (click)=\"localRowClick(shell)\">\r\n\r\n <td [className]=\"'tree-item ' + (conditionalCellClassFunc ? conditionalCellClassFunc(shell.item, column) : '')\"\r\n *ngFor=\"let column of columns; let i = index;\">\r\n <ng-container *ngIf=\"i === 0\">\r\n <div class=\"level-indent\" *ngFor=\"let foo of shell.level | appNumberToArray\"></div>\r\n <div class=\"tree-item-arrow\"\r\n [ngClass]=\"{'expanded': shell.item.isExpanded, 'vis-hidden': !shell.haveChildren}\"\r\n (click)=\"toggleExpand($event, shell)\"\r\n ><div class=\"arrow\"></div>\r\n </div>\r\n </ng-container>\r\n <ng-container *ngTemplateOutlet=\"column.template; context: { $implicit: shell.item }\">\r\n </ng-container>\r\n </td>\r\n </tr>\r\n </tbody>\r\n\r\n</table>\r\n", styles: ["table{table-layout:fixed}table td,table th{overflow:hidden;white-space:nowrap;-moz-text-overflow:ellipsis;-ms-text-overflow:ellipsis;-o-text-overflow:ellipsis;text-overflow:ellipsis}table tbody tr td .level-indent{display:inline-block;width:22px}table tbody tr td .tree-item-arrow{cursor:pointer;display:inline-block;width:14px;height:12px}table tbody tr td .tree-item-arrow .arrow{display:inline-block;border:solid darkslateblue;border-width:0 3px 3px 0;padding:3px;transform:rotate(-45deg);-webkit-transform:rotate(-45deg)}table tbody tr td .tree-item-arrow.expanded .arrow{transform:rotate(45deg);-webkit-transform:rotate(45deg)}table tbody tr td .tree-item-arrow.vis-hidden{visibility:hidden}table{border-collapse:collapse}table,th,td{padding:4px 8px;border:1px solid darkgray}\n"], dependencies: [{ kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: ResizableTableColumnDirective, selector: "[appResizableTableColumn]", inputs: ["appResizableTableColumn"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }, { kind: "pipe", type: NumberToArrayPipe, name: "appNumberToArray" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.3", ngImport: i0, type: CngTreeTableComponent, decorators: [{
type: Component,
args: [{ selector: 'cng-tree-table', template: "<table [ngClass]=\"tableClass\">\r\n <thead>\r\n <tr>\r\n <th *ngFor=\"let column of columns; let last=last;\"\r\n [appResizableTableColumn]=\"!last\"\r\n class=\"tree-item-header\"\r\n [ngClass]=\"[column.thClass || '']\">\r\n {{column.titleTemplate ? '' : column.title}}\r\n <ng-container *ngTemplateOutlet=\"column.titleTemplate; context: { $implicit: column.title }\">\r\n </ng-container>\r\n </th>\r\n </tr>\r\n </thead>\r\n\r\n <tbody>\r\n <tr [ngClass]=\"{'selected': (isSelected$(shell) | async)}\"\r\n *ngFor=\"let shell of treeTableItemShells\"\r\n [hidden]=\"!shell.isVisible\" (click)=\"localRowClick(shell)\">\r\n\r\n <td [className]=\"'tree-item ' + (conditionalCellClassFunc ? conditionalCellClassFunc(shell.item, column) : '')\"\r\n *ngFor=\"let column of columns; let i = index;\">\r\n <ng-container *ngIf=\"i === 0\">\r\n <div class=\"level-indent\" *ngFor=\"let foo of shell.level | appNumberToArray\"></div>\r\n <div class=\"tree-item-arrow\"\r\n [ngClass]=\"{'expanded': shell.item.isExpanded, 'vis-hidden': !shell.haveChildren}\"\r\n (click)=\"toggleExpand($event, shell)\"\r\n ><div class=\"arrow\"></div>\r\n </div>\r\n </ng-container>\r\n <ng-container *ngTemplateOutlet=\"column.template; context: { $implicit: shell.item }\">\r\n </ng-container>\r\n </td>\r\n </tr>\r\n </tbody>\r\n\r\n</table>\r\n", styles: ["table{table-layout:fixed}table td,table th{overflow:hidden;white-space:nowrap;-moz-text-overflow:ellipsis;-ms-text-overflow:ellipsis;-o-text-overflow:ellipsis;text-overflow:ellipsis}table tbody tr td .level-indent{display:inline-block;width:22px}table tbody tr td .tree-item-arrow{cursor:pointer;display:inline-block;width:14px;height:12px}table tbody tr td .tree-item-arrow .arrow{display:inline-block;border:solid darkslateblue;border-width:0 3px 3px 0;padding:3px;transform:rotate(-45deg);-webkit-transform:rotate(-45deg)}table tbody tr td .tree-item-arrow.expanded .arrow{transform:rotate(45deg);-webkit-transform:rotate(45deg)}table tbody tr td .tree-item-arrow.vis-hidden{visibility:hidden}table{border-collapse:collapse}table,th,td{padding:4px 8px;border:1px solid darkgray}\n"] }]
}], ctorParameters: () => [], propDecorators: { columns: [{
type: Input
}], sortFunction: [{
type: Input
}], isSelectedFunc$: [{
type: Input
}], conditionalCellClassFunc: [{
type: Input
}], toggleExpandItem: [{
type: Output
}], rowClick: [{
type: Output
}], tableClass: [{
type: Input
}] } });
class SharedModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.3", ngImport: i0, type: SharedModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "18.1.3", ngImport: i0, type: SharedModule, declarations: [NumberToArrayPipe,
ResizableTableColumnDirective], imports: [CommonModule], exports: [NumberToArrayPipe,
ResizableTableColumnDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.1.3", ngImport: i0, type: SharedModule, imports: [CommonModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.3", ngImport: i0, type: SharedModule, decorators: [{
type: NgModule,
args: [{
imports: [
CommonModule
],
declarations: [
NumberToArrayPipe,
ResizableTableColumnDirective
],
exports: [
NumberToArrayPipe,
ResizableTableColumnDirective
]
}]
}] });
class CngTreeTableModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.3", ngImport: i0, type: CngTreeTableModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "18.1.3", ngImport: i0, type: CngTreeTableModule, declarations: [CngTreeTableComponent], imports: [CommonModule,
SharedModule], exports: [CngTreeTableComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.1.3", ngImport: i0, type: CngTreeTableModule, imports: [CommonModule,
SharedModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.3", ngImport: i0, type: CngTreeTableModule, decorators: [{
type: NgModule,
args: [{
imports: [
CommonModule,
SharedModule
],
declarations: [CngTreeTableComponent],
exports: [CngTreeTableComponent]
}]
}] });
/*
* Public API Surface of tree-table
*/
/**
* Generated bundle index. Do not edit.
*/
export { CngTreeTableComponent, CngTreeTableModule };
//# sourceMappingURL=codehint-ng-tree-table.mjs.map