mirror of https://github.com/abpframework/abp.git
31 changed files with 829 additions and 1 deletions
@ -0,0 +1,36 @@ |
|||
{ |
|||
"extends": ["../../.eslintrc.json"], |
|||
"ignorePatterns": ["!**/*"], |
|||
"overrides": [ |
|||
{ |
|||
"files": ["*.ts"], |
|||
"extends": [ |
|||
"plugin:@nrwl/nx/angular", |
|||
"plugin:@angular-eslint/template/process-inline-templates" |
|||
], |
|||
"rules": { |
|||
"@angular-eslint/directive-selector": [ |
|||
"error", |
|||
{ |
|||
"type": "attribute", |
|||
"prefix": "abp", |
|||
"style": "camelCase" |
|||
} |
|||
], |
|||
"@angular-eslint/component-selector": [ |
|||
"error", |
|||
{ |
|||
"type": "element", |
|||
"prefix": "abp", |
|||
"style": "kebab-case" |
|||
} |
|||
] |
|||
} |
|||
}, |
|||
{ |
|||
"files": ["*.html"], |
|||
"extends": ["plugin:@nrwl/nx/angular-template"], |
|||
"rules": {} |
|||
} |
|||
] |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<h1> @abp/ng.components </h1> |
|||
|
|||
[docs.abp.io](https://docs.abp.io) |
|||
@ -0,0 +1,20 @@ |
|||
module.exports = { |
|||
displayName: 'components', |
|||
preset: '../../jest.preset.js', |
|||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'], |
|||
globals: { |
|||
'ts-jest': { |
|||
tsconfig: '<rootDir>/tsconfig.spec.json', |
|||
stringifyContentPathRegex: '\\.(html|svg)$', |
|||
}, |
|||
}, |
|||
coverageDirectory: '../../coverage/packages/components', |
|||
transform: { |
|||
'^.+\\.(ts|js|html)$': 'jest-preset-angular', |
|||
}, |
|||
snapshotSerializers: [ |
|||
'jest-preset-angular/build/serializers/no-ng-attributes', |
|||
'jest-preset-angular/build/serializers/ng-snapshot', |
|||
'jest-preset-angular/build/serializers/html-comment', |
|||
], |
|||
}; |
|||
@ -0,0 +1,8 @@ |
|||
{ |
|||
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json", |
|||
"dest": "../../dist/packages/components", |
|||
"lib": { |
|||
"entryFile": "src/public-api.ts" |
|||
}, |
|||
"allowedNonPeerDependencies": ["ng-zorro-antd"] |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
{ |
|||
"name": "@abp/ng.components", |
|||
"version": "4.4.0", |
|||
"homepage": "https://abp.io", |
|||
"repository": { |
|||
"type": "git", |
|||
"url": "https://github.com/abpframework/abp.git" |
|||
}, |
|||
"peerDependencies": { |
|||
"@abp/ng.core": ">=4.4.0", |
|||
"@abp/ng.theme.shared": ">=4.4.0", |
|||
"@ng-bootstrap/ng-bootstrap": ">=6.0.0" |
|||
}, |
|||
"dependencies": { |
|||
"ng-zorro-antd": "^11.0.0", |
|||
"tslib": "^2.0.0" |
|||
}, |
|||
"publishConfig": { |
|||
"access": "public" |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
{ |
|||
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", |
|||
"dest": "../../../dist/packages/components/page", |
|||
"lib": { |
|||
"entryFile": "src/public-api.ts" |
|||
} |
|||
} |
|||
@ -0,0 +1,95 @@ |
|||
import { |
|||
Directive, |
|||
TemplateRef, |
|||
ViewContainerRef, |
|||
Input, |
|||
InjectionToken, |
|||
Optional, |
|||
Inject, |
|||
OnInit, |
|||
OnDestroy, |
|||
Injector, |
|||
OnChanges, |
|||
SimpleChanges, |
|||
SimpleChange, |
|||
} from '@angular/core'; |
|||
import { Observable, Subscription, of } from 'rxjs'; |
|||
|
|||
export interface PageRenderStrategy { |
|||
shouldRender(type?: string): boolean | Observable<boolean>; |
|||
onInit?(type?: string, injector?: Injector, context?: any): void; |
|||
onDestroy?(type?: string, injector?: Injector, context?: any): void; |
|||
onContextUpdate?(change?: SimpleChange): void; |
|||
} |
|||
|
|||
export const PAGE_RENDER_STRATEGY = new InjectionToken<PageRenderStrategy>('PAGE_RENDER_STRATEGY'); |
|||
|
|||
@Directive({ selector: '[abpPagePart]' }) |
|||
export class PagePartDirective implements OnInit, OnDestroy, OnChanges { |
|||
hasRendered = false; |
|||
type: string; |
|||
subscription: Subscription; |
|||
|
|||
@Input('abpPagePartContext') context: any; |
|||
@Input() set abpPagePart(type: string) { |
|||
this.type = type; |
|||
this.createRenderStream(type); |
|||
} |
|||
|
|||
render = (shouldRender: boolean) => { |
|||
if (shouldRender && !this.hasRendered) { |
|||
this.viewContainer.createEmbeddedView(this.templateRef); |
|||
this.hasRendered = true; |
|||
} else if (!shouldRender && this.hasRendered) { |
|||
this.viewContainer.clear(); |
|||
this.hasRendered = false; |
|||
} |
|||
}; |
|||
|
|||
constructor( |
|||
private templateRef: TemplateRef<any>, |
|||
private viewContainer: ViewContainerRef, |
|||
@Optional() @Inject(PAGE_RENDER_STRATEGY) private renderLogic: PageRenderStrategy, |
|||
private injector: Injector, |
|||
) {} |
|||
|
|||
ngOnChanges({ context }: SimpleChanges): void { |
|||
if (this.renderLogic?.onContextUpdate) { |
|||
this.renderLogic.onContextUpdate(context); |
|||
} |
|||
} |
|||
|
|||
ngOnInit() { |
|||
if (this.renderLogic?.onInit) { |
|||
this.renderLogic.onInit(this.type, this.injector, this.context); |
|||
} |
|||
} |
|||
|
|||
ngOnDestroy() { |
|||
this.clearSubscription(); |
|||
|
|||
if (this.renderLogic?.onDestroy) { |
|||
this.renderLogic.onDestroy(this.type, this.injector, this.context); |
|||
} |
|||
} |
|||
|
|||
shouldRender(type: string) { |
|||
if (this.renderLogic) { |
|||
const willRender = this.renderLogic.shouldRender(type); |
|||
return willRender instanceof Observable ? willRender : of(willRender); |
|||
} |
|||
return of(true); |
|||
} |
|||
|
|||
protected createRenderStream(type: string) { |
|||
this.clearSubscription(); |
|||
|
|||
this.subscription = this.shouldRender(type).subscribe(this.render); |
|||
} |
|||
|
|||
protected clearSubscription() { |
|||
if (this.subscription) { |
|||
this.subscription.unsubscribe(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
import { Component, ViewEncapsulation } from '@angular/core'; |
|||
|
|||
export enum PageParts { |
|||
title = 'PageTitleContainerComponent', |
|||
breadcrumb = 'PageBreadcrumbContainerComponent', |
|||
toolbar = 'PageToolbarContainerComponent', |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'abp-page-title-container', |
|||
template: ` |
|||
<ng-content></ng-content> |
|||
`,
|
|||
encapsulation: ViewEncapsulation.None, |
|||
}) |
|||
export class PageTitleContainerComponent {} |
|||
|
|||
@Component({ |
|||
selector: 'abp-page-breadcrumb-container', |
|||
template: ` |
|||
<ng-content></ng-content> |
|||
`,
|
|||
encapsulation: ViewEncapsulation.None, |
|||
}) |
|||
export class PageBreadcrumbContainerComponent {} |
|||
|
|||
@Component({ |
|||
selector: 'abp-page-toolbar-container', |
|||
template: ` |
|||
<ng-content></ng-content> |
|||
`,
|
|||
encapsulation: ViewEncapsulation.None, |
|||
}) |
|||
export class PageToolbarContainerComponent {} |
|||
@ -0,0 +1,41 @@ |
|||
<div class="row entry-row" *ngIf="shouldRenderRow"> |
|||
<ng-container *ngIf="customTitle; else defaultTitleTemplate"> |
|||
<ng-content select="abp-page-title-container"></ng-content> |
|||
</ng-container> |
|||
|
|||
<ng-container *ngIf="customBreadcrumb; else defaultBreadcrumbTemplate"> |
|||
<ng-content select="abp-page-breadcrumb-container"></ng-content> |
|||
</ng-container> |
|||
|
|||
<ng-container *ngIf="customToolbar; else defaultPageToolbarTemplate"> |
|||
<ng-content select="abp-page-toolbar-container"></ng-content> |
|||
</ng-container> |
|||
</div> |
|||
|
|||
<ng-content></ng-content> |
|||
|
|||
<ng-template #defaultTitleTemplate> |
|||
<ng-container *ngIf="title"> |
|||
<div class="col-auto" *abpPagePart="pageParts.title"> |
|||
<h1 class="content-header-title"> |
|||
{{ title }} |
|||
</h1> |
|||
</div> |
|||
</ng-container> |
|||
</ng-template> |
|||
|
|||
<ng-template #defaultBreadcrumbTemplate> |
|||
<ng-container *ngIf="breadcrumbVisible"> |
|||
<div class="col-lg-auto pl-lg-0" *abpPagePart="pageParts.breadcrumb"> |
|||
<abp-breadcrumb></abp-breadcrumb> |
|||
</div> |
|||
</ng-container> |
|||
</ng-template> |
|||
|
|||
<ng-template #defaultPageToolbarTemplate> |
|||
<ng-container *ngIf="toolbarVisible"> |
|||
<div class="col" *abpPagePart="pageParts.toolbar; context: toolbarData"> |
|||
<abp-page-toolbar [record]="toolbarData"></abp-page-toolbar> |
|||
</div> |
|||
</ng-container> |
|||
</ng-template> |
|||
@ -0,0 +1,51 @@ |
|||
import { Component, Input, ViewEncapsulation, ContentChild } from '@angular/core'; |
|||
import { |
|||
PageTitleContainerComponent, |
|||
PageBreadcrumbContainerComponent, |
|||
PageToolbarContainerComponent, |
|||
PageParts, |
|||
} from './page-parts.component'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-page', |
|||
templateUrl: './page.component.html', |
|||
encapsulation: ViewEncapsulation.None, |
|||
}) |
|||
export class PageComponent { |
|||
@Input() title: string; |
|||
|
|||
toolbarVisible = false; |
|||
_toolbarData: any; |
|||
@Input('toolbar') set toolbarData(val: any) { |
|||
this._toolbarData = val; |
|||
this.toolbarVisible = true; |
|||
} |
|||
|
|||
get toolbarData() { |
|||
return this._toolbarData; |
|||
} |
|||
|
|||
@Input('breadcrumb') breadcrumbVisible = true; |
|||
|
|||
pageParts = { |
|||
title: PageParts.title, |
|||
breadcrumb: PageParts.breadcrumb, |
|||
toolbar: PageParts.toolbar, |
|||
}; |
|||
|
|||
@ContentChild(PageTitleContainerComponent) customTitle: PageTitleContainerComponent; |
|||
@ContentChild(PageBreadcrumbContainerComponent) |
|||
customBreadcrumb: PageBreadcrumbContainerComponent; |
|||
@ContentChild(PageToolbarContainerComponent) customToolbar: PageToolbarContainerComponent; |
|||
|
|||
get shouldRenderRow() { |
|||
return !!( |
|||
this.title || |
|||
this.toolbarVisible || |
|||
this.breadcrumbVisible || |
|||
this.customTitle || |
|||
this.customBreadcrumb || |
|||
this.customToolbar |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
import { NgModule } from '@angular/core'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { UiExtensionsModule } from '@abp/ng.theme.shared/extensions'; |
|||
import { ThemeSharedModule } from '@abp/ng.theme.shared'; |
|||
import { CoreModule } from '@abp/ng.core'; |
|||
import { PageComponent } from './page.component'; |
|||
import { |
|||
PageTitleContainerComponent, |
|||
PageBreadcrumbContainerComponent, |
|||
PageToolbarContainerComponent, |
|||
} from './page-parts.component'; |
|||
import { PagePartDirective } from './page-part.directive'; |
|||
|
|||
const exportedDeclarations = [ |
|||
PageComponent, |
|||
PageTitleContainerComponent, |
|||
PageBreadcrumbContainerComponent, |
|||
PageToolbarContainerComponent, |
|||
PagePartDirective, |
|||
]; |
|||
|
|||
@NgModule({ |
|||
declarations: [...exportedDeclarations], |
|||
imports: [CommonModule, UiExtensionsModule, CoreModule, ThemeSharedModule], |
|||
exports: [...exportedDeclarations], |
|||
}) |
|||
export class PageModule {} |
|||
@ -0,0 +1,4 @@ |
|||
export * from './page.module'; |
|||
export * from './page-part.directive'; |
|||
export * from './page-parts.component'; |
|||
export * from './page.component'; |
|||
@ -0,0 +1,5 @@ |
|||
/* |
|||
* Public API Surface of components |
|||
*/ |
|||
|
|||
export {}; |
|||
@ -0,0 +1 @@ |
|||
import 'jest-preset-angular/setup-jest'; |
|||
@ -0,0 +1,7 @@ |
|||
{ |
|||
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", |
|||
"dest": "../../../dist/packages/components/tree", |
|||
"lib": { |
|||
"entryFile": "src/public-api.ts" |
|||
} |
|||
} |
|||
@ -0,0 +1,96 @@ |
|||
<nz-tree |
|||
[nzBeforeDrop]="beforeDrop" |
|||
[nzDraggable]="draggable" |
|||
[nzCheckStrictly]="checkStrictly" |
|||
[nzCheckable]="checkable" |
|||
[nzCheckedKeys]="checkedKeys" |
|||
[nzData]="nodes" |
|||
[nzTreeTemplate]="treeTemplate" |
|||
[nzExpandedKeys]="expandedKeys" |
|||
[nzExpandedIcon]="expandedIconTemplate?.template || defaultIconTemplate" |
|||
(nzExpandChange)="onExpandedKeysChange($event)" |
|||
(nzCheckBoxChange)="onCheckboxChange($event)" |
|||
(nzOnDrop)="onDrop($event)" |
|||
[nzNoAnimation]="noAnimation" |
|||
(nzContextMenu)="dropdowns[$event.node?.key]?.toggle()" |
|||
></nz-tree> |
|||
<ng-template #treeTemplate let-node> |
|||
<div |
|||
class="node-wrapper" |
|||
[class.selected]="isNodeSelected(node)" |
|||
[title]="node.title" |
|||
(click)="onSelectedNodeChange(node)" |
|||
> |
|||
<ng-container *ngTemplateOutlet="nodeTemplate; context: { $implicit: node }"></ng-container> |
|||
|
|||
<ng-template #nodeTemplate let-node> |
|||
<div class="d-inline-block"> |
|||
<ng-container |
|||
*ngTemplateOutlet=" |
|||
customNodeTemplate ? customNodeTemplate?.template : defaultNodeTemplate; |
|||
context: { $implicit: node } |
|||
" |
|||
></ng-container> |
|||
</div> |
|||
|
|||
<div |
|||
#dropdown="ngbDropdown" |
|||
*ngIf="menu" |
|||
class="d-inline-block ml-1" |
|||
ngbDropdown |
|||
placement="bottom" |
|||
container="body" |
|||
(abpInit)="initDropdown(node.key, dropdown)" |
|||
> |
|||
<i |
|||
class="fas fa-caret-down text-muted" |
|||
ngbDropdownToggle |
|||
[class.dropdown-toggle]="false" |
|||
></i> |
|||
<div ngbDropdownMenu> |
|||
<ng-template *ngTemplateOutlet="menu; context: { $implicit: node }"></ng-template> |
|||
</div> |
|||
</div> |
|||
</ng-template> |
|||
|
|||
<ng-template #defaultNodeTemplate let-node> |
|||
<span>{{ node.title }}</span> |
|||
</ng-template> |
|||
</div> |
|||
</ng-template> |
|||
|
|||
<ng-template #defaultIconTemplate let-node let-origin="origin"> |
|||
<i style="line-height: 28px" aria-hidden="true"> |
|||
<ng-container *ngTemplateOutlet="node.isExpanded ? minusIcon : plusIcon"></ng-container |
|||
></i> |
|||
</ng-template> |
|||
|
|||
<ng-template #minusIcon> |
|||
<svg |
|||
width="15" |
|||
height="15" |
|||
viewBox="0 0 24 24" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
fill-rule="evenodd" |
|||
clip-rule="evenodd" |
|||
> |
|||
<path |
|||
d="M11.5 0c6.347 0 11.5 5.153 11.5 11.5s-5.153 11.5-11.5 11.5-11.5-5.153-11.5-11.5 5.153-11.5 11.5-11.5zm0 1c5.795 0 10.5 4.705 10.5 10.5s-4.705 10.5-10.5 10.5-10.5-4.705-10.5-10.5 4.705-10.5 10.5-10.5zm-6.5 10h13v1h-13v-1z" |
|||
/> |
|||
</svg> |
|||
</ng-template> |
|||
|
|||
<ng-template #plusIcon> |
|||
<svg |
|||
width="15" |
|||
height="15" |
|||
viewBox="0 0 24 24" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
fill-rule="evenodd" |
|||
clip-rule="evenodd" |
|||
> |
|||
<path |
|||
d="M11.5 0c6.347 0 11.5 5.153 11.5 11.5s-5.153 11.5-11.5 11.5-11.5-5.153-11.5-11.5 5.153-11.5 11.5-11.5zm0 1c5.795 0 10.5 4.705 10.5 10.5s-4.705 10.5-10.5 10.5-10.5-4.705-10.5-10.5 4.705-10.5 10.5-10.5zm.5 10h6v1h-6v6h-1v-6h-6v-1h6v-6h1v6z" |
|||
/> |
|||
</svg> |
|||
</ng-template> |
|||
@ -0,0 +1,38 @@ |
|||
.ant-tree { |
|||
color: inherit; |
|||
|
|||
.ant-tree-node-content-wrapper.ant-tree-node-selected { |
|||
background-color: transparent; |
|||
} |
|||
|
|||
.ant-tree-switcher { |
|||
line-height: 17px; |
|||
} |
|||
|
|||
.ant-tree-node-content-wrapper { |
|||
width: 100%; |
|||
padding: 0; |
|||
} |
|||
|
|||
.node-wrapper { |
|||
width: 100%; |
|||
position: relative; |
|||
display: inline-block; |
|||
margin: 0; |
|||
padding: 0 5px; |
|||
line-height: 24px; |
|||
text-decoration: none; |
|||
vertical-align: top; |
|||
border-radius: 2px; |
|||
cursor: pointer; |
|||
padding-left: 8px; |
|||
border: 1px solid transparent; |
|||
} |
|||
|
|||
.ellipsis { |
|||
position: absolute; |
|||
right: 8px; |
|||
top: 1px; |
|||
cursor: pointer; |
|||
} |
|||
} |
|||
@ -0,0 +1,79 @@ |
|||
import { |
|||
Component, |
|||
ContentChild, |
|||
EventEmitter, |
|||
Input, |
|||
Output, |
|||
TemplateRef, |
|||
ViewEncapsulation, |
|||
} from '@angular/core'; |
|||
import { NzFormatEmitEvent, NzFormatBeforeDropEvent } from 'ng-zorro-antd/tree'; |
|||
import { of } from 'rxjs'; |
|||
import { TreeNodeTemplateDirective } from '../templates/tree-node-template.directive'; |
|||
import { ExpandedIconTemplateDirective } from '../templates/expanded-icon-template.directive'; |
|||
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'; |
|||
|
|||
export type DropEvent = NzFormatEmitEvent & { pos: number }; |
|||
|
|||
@Component({ |
|||
selector: 'abp-tree', |
|||
templateUrl: 'tree.component.html', |
|||
styleUrls: [ |
|||
'../../../../../../node_modules/ng-zorro-antd/tree/style/index.min.css', |
|||
'tree.component.scss', |
|||
], |
|||
encapsulation: ViewEncapsulation.None, |
|||
}) |
|||
export class TreeComponent { |
|||
dropPosition: number; |
|||
|
|||
dropdowns = {} as { [key: string]: NgbDropdown }; |
|||
|
|||
@ContentChild('menu') menu: TemplateRef<any>; |
|||
@ContentChild(TreeNodeTemplateDirective) customNodeTemplate: TreeNodeTemplateDirective; |
|||
@ContentChild(ExpandedIconTemplateDirective) expandedIconTemplate: ExpandedIconTemplateDirective; |
|||
@Output() readonly checkedKeysChange = new EventEmitter(); |
|||
@Output() readonly expandedKeysChange = new EventEmitter<string[]>(); |
|||
@Output() readonly selectedNodeChange = new EventEmitter(); |
|||
@Output() readonly dropOver = new EventEmitter<DropEvent>(); |
|||
@Input() noAnimation = true; |
|||
@Input() draggable: boolean; |
|||
@Input() checkable: boolean; |
|||
@Input() checkStrictly: boolean; |
|||
@Input() checkedKeys = []; |
|||
@Input() nodes = []; |
|||
@Input() expandedKeys: string[] = []; |
|||
@Input() selectedNode: any; |
|||
@Input() isNodeSelected = node => this.selectedNode?.id === node.key; |
|||
@Input() beforeDrop = (event: NzFormatBeforeDropEvent) => { |
|||
this.dropPosition = event.pos; |
|||
return of(false); |
|||
}; |
|||
|
|||
onSelectedNodeChange(node) { |
|||
this.selectedNode = node.origin.entity; |
|||
this.selectedNodeChange.emit(node.origin.entity); |
|||
} |
|||
|
|||
onCheckboxChange(event) { |
|||
this.checkedKeys = [...event.keys]; |
|||
this.checkedKeysChange.emit(event.keys); |
|||
} |
|||
|
|||
onExpandedKeysChange(event) { |
|||
this.expandedKeys = [...event.keys]; |
|||
this.expandedKeysChange.emit(event.keys); |
|||
} |
|||
|
|||
onDrop(event: DropEvent) { |
|||
event.event.stopPropagation(); |
|||
event.event.preventDefault(); |
|||
event.pos = this.dropPosition; |
|||
|
|||
this.dropOver.emit(event); |
|||
} |
|||
|
|||
initDropdown(key: string, dropdown: NgbDropdown) { |
|||
this.dropdowns[key] = dropdown; |
|||
} |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
import { Directive, TemplateRef } from '@angular/core'; |
|||
|
|||
@Directive({ |
|||
selector: '[abpTreeExpandedIconTemplate],[abp-tree-expanded-icon-template]', |
|||
}) |
|||
export class ExpandedIconTemplateDirective { |
|||
constructor(public template: TemplateRef<any>) {} |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
import { Directive, TemplateRef } from '@angular/core'; |
|||
|
|||
@Directive({ |
|||
selector: '[abpTreeNodeTemplate],[abp-tree-node-template]', |
|||
}) |
|||
export class TreeNodeTemplateDirective { |
|||
constructor(public template: TemplateRef<any>) {} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
import { CoreModule } from '@abp/ng.core'; |
|||
import { NgModule } from '@angular/core'; |
|||
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; |
|||
import { NzNoAnimationModule } from 'ng-zorro-antd/core/no-animation'; |
|||
import { NzTreeModule } from 'ng-zorro-antd/tree'; |
|||
import { TreeComponent } from './components/tree.component'; |
|||
import { ExpandedIconTemplateDirective } from './templates/expanded-icon-template.directive'; |
|||
import { TreeNodeTemplateDirective } from './templates/tree-node-template.directive'; |
|||
|
|||
const templates = [TreeNodeTemplateDirective, ExpandedIconTemplateDirective]; |
|||
|
|||
const exported = [...templates, TreeComponent]; |
|||
|
|||
@NgModule({ |
|||
imports: [CoreModule, NzTreeModule, NgbDropdownModule, NzNoAnimationModule], |
|||
exports: [...exported], |
|||
declarations: [...exported], |
|||
}) |
|||
export class TreeModule {} |
|||
@ -0,0 +1,112 @@ |
|||
export abstract class BaseNode { |
|||
name?: string; |
|||
displayName?: string; |
|||
|
|||
constructor(public id: string, public parentId: string | null) {} |
|||
} |
|||
|
|||
class TreeNode<T extends BaseNode> extends BaseNode { |
|||
title: string | undefined; |
|||
key: string; |
|||
icon: string | null = null; |
|||
children: TreeNode<T>[] = []; |
|||
isLeaf = true; |
|||
checked = false; |
|||
selected = false; |
|||
expanded = false; |
|||
selectable = true; |
|||
disabled = false; |
|||
disableCheckbox = false; |
|||
parentNode?: TreeNode<T> | null; |
|||
|
|||
constructor(public entity: T, private nameResolver = ent => ent.displayName || ent.name) { |
|||
super(entity.id, entity.parentId); |
|||
this.key = entity.id; |
|||
this.title = nameResolver(entity); |
|||
} |
|||
} |
|||
|
|||
export class TreeAdapter<T extends BaseNode = BaseNode> { |
|||
private tree: TreeNode<T>[]; |
|||
|
|||
constructor(private list: T[] = []) { |
|||
this.tree = createTreeFromList(this.list); |
|||
} |
|||
|
|||
getList() { |
|||
return this.list; |
|||
} |
|||
|
|||
getTree() { |
|||
return this.tree; |
|||
} |
|||
|
|||
handleDrop({ key, parentNode }: TreeNode<T>) { |
|||
const index = this.list.findIndex(({ id }) => id === key); |
|||
this.list[index].parentId = parentNode ? parentNode.key : null; |
|||
this.tree = createTreeFromList(this.list); |
|||
} |
|||
|
|||
handleRemove({ key }: TreeNode<T>) { |
|||
this.updateTreeFromList(this.list.filter(item => item.id !== key)); |
|||
} |
|||
|
|||
handleUpdate({ key, children }: { key: string; children: T[] }) { |
|||
/** |
|||
* When we need to update a node with new children, first we need to remove any descendant nodes. |
|||
* If we remove immediate children and create a new tree, any other descendant nodes will be removed |
|||
* and we won't need to recursively remove sub children. |
|||
* Then, you simply add back the new children and create a new tree. |
|||
*/ |
|||
const listWithDescendantNodesRemoved = this.updateTreeFromList( |
|||
this.list.filter(item => item.parentId !== key), |
|||
); |
|||
this.updateTreeFromList(listWithDescendantNodesRemoved.concat(children)); |
|||
} |
|||
|
|||
updateTreeFromList(list: T[]) { |
|||
this.tree = createTreeFromList(list); |
|||
this.list = createListFromTree(this.tree); |
|||
return this.list; |
|||
} |
|||
} |
|||
|
|||
// UTILITY FUNCTIONS
|
|||
|
|||
function createTreeFromList<T extends BaseNode>(list: T[]): TreeNode<T>[] { |
|||
const map = createMapFromList(list); |
|||
const tree: TreeNode<T>[] = []; |
|||
|
|||
list.forEach(row => { |
|||
const parentId = row.parentId; |
|||
const node = map.get(row.id); |
|||
if (parentId) { |
|||
const parent = map.get(parentId); |
|||
if (!parent) return; |
|||
parent.children.push(node); |
|||
parent.isLeaf = false; |
|||
} else { |
|||
tree.push(node); |
|||
} |
|||
}); |
|||
|
|||
return tree; |
|||
} |
|||
|
|||
function createListFromTree<T extends BaseNode>(tree: TreeNode<T>[], list: T[] = []): T[] { |
|||
tree.forEach(node => { |
|||
list.push({ ...node.entity, parentId: node.parentId }); |
|||
if (node.children) createListFromTree(node.children, list); |
|||
}); |
|||
|
|||
return list; |
|||
} |
|||
|
|||
function createMapFromList<T extends BaseNode>( |
|||
list: T[], |
|||
map = new Map<string, TreeNode<T>>(), |
|||
): Map<string, TreeNode<T>> { |
|||
list.forEach(row => map.set(row.id, new TreeNode(row))); |
|||
|
|||
return map; |
|||
} |
|||
@ -0,0 +1,5 @@ |
|||
export * from './lib/tree.module'; |
|||
export * from './lib/components/tree.component'; |
|||
export * from './lib/utils/nz-tree-adapter'; |
|||
export * from './lib/templates/tree-node-template.directive'; |
|||
export * from './lib/templates/expanded-icon-template.directive'; |
|||
@ -0,0 +1,13 @@ |
|||
{ |
|||
"extends": "../../tsconfig.base.json", |
|||
"files": [], |
|||
"include": [], |
|||
"references": [ |
|||
{ |
|||
"path": "./tsconfig.lib.json" |
|||
}, |
|||
{ |
|||
"path": "./tsconfig.spec.json" |
|||
} |
|||
] |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
{ |
|||
"extends": "./tsconfig.json", |
|||
"compilerOptions": { |
|||
"outDir": "../../dist/out-tsc", |
|||
"target": "es2015", |
|||
"declaration": true, |
|||
"declarationMap": true, |
|||
"inlineSources": true, |
|||
"types": [], |
|||
"lib": ["dom", "es2018"] |
|||
}, |
|||
"exclude": ["src/test-setup.ts", "**/*.spec.ts"], |
|||
"include": ["**/*.ts"] |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
/* To learn more about this file see: https://angular.io/config/tsconfig. */ |
|||
{ |
|||
"extends": "./tsconfig.lib.json", |
|||
"compilerOptions": { |
|||
"declarationMap": false |
|||
}, |
|||
"angularCompilerOptions": { |
|||
"compilationMode": "partial", |
|||
"enableIvy": false |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
{ |
|||
"extends": "./tsconfig.json", |
|||
"compilerOptions": { |
|||
"outDir": "../../dist/out-tsc", |
|||
"module": "commonjs", |
|||
"types": ["jest", "node"] |
|||
}, |
|||
"files": ["src/test-setup.ts"], |
|||
"include": ["**/*.spec.ts", "**/*.d.ts"] |
|||
} |
|||
@ -1 +1,3 @@ |
|||
import * as ThemeSharedTesting from './lib/models'; |
|||
export * from './lib/theme-shared-testing.module'; |
|||
export { ThemeSharedTesting }; |
|||
|
|||
Loading…
Reference in new issue