UNPKG

@codehint-ng/tree-table

Version:

A simple component to display tree-hierarchically data in table.

416 lines (405 loc) 22.4 kB
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