From 4a68b647b44747ac9eba37c79f78954ec7dcd33b Mon Sep 17 00:00:00 2001 From: TMaster Date: Fri, 17 Nov 2017 14:17:05 +0200 Subject: [PATCH 1/6] Support checking / unchecking nodes --- src/demo/app/app.component.ts | 264 ++++++++++++++++++--------------- src/tree-controller.ts | 9 +- src/tree-internal.component.ts | 66 ++++++++- src/tree.component.ts | 21 ++- src/tree.events.ts | 12 ++ src/tree.service.ts | 14 +- src/tree.ts | 1 + src/tree.types.ts | 4 +- 8 files changed, 257 insertions(+), 134 deletions(-) diff --git a/src/demo/app/app.component.ts b/src/demo/app/app.component.ts index d246647b..9d3a088a 100644 --- a/src/demo/app/app.component.ts +++ b/src/demo/app/app.component.ts @@ -51,7 +51,8 @@ declare const alertify: any; (nodeMoved)="onNodeMoved($event)" (nodeCreated)="onNodeFFSCreated($event)" (nodeExpanded)="onNodeExpanded($event)" - (nodeCollapsed)="onNodeCollapsed($event)"> + (nodeCollapsed)="onNodeCollapsed($event)" + [settings]="settings"> @@ -76,6 +77,9 @@ declare const alertify: any; + +
@@ -172,7 +176,8 @@ declare const alertify: any; }) export class AppComponent implements OnInit { public settings: Ng2TreeSettings = { - rootIsVisible: false + rootIsVisible: false, + enableCheckboxes: true }; public fonts: TreeModel = { @@ -185,18 +190,18 @@ export class AppComponent implements OnInit { 'static': true }, children: [ - {value: 'Antiqua with HTML tags.', id: 2}, - {value: 'DejaVu Serif', id: 3}, - {value: 'Garamond', id: 4}, - {value: 'Georgia', id: 5}, - {value: 'Times New Roman', id: 6}, + { value: 'Antiqua with HTML tags.', id: 2 }, + { value: 'DejaVu Serif', id: 3 }, + { value: 'Garamond', id: 4 }, + { value: 'Georgia', id: 5 }, + { value: 'Times New Roman', id: 6 }, { value: 'Slab serif', id: 7, children: [ - {value: 'Candida', id: 8}, - {value: 'Swift', id: 9}, - {value: 'Guardian Egyptian', id: 10} + { value: 'Candida', id: 8 }, + { value: 'Swift', id: 9 }, + { value: 'Guardian Egyptian', id: 10 } ] } ] @@ -205,29 +210,29 @@ export class AppComponent implements OnInit { value: 'Sans-serif', id: 11, children: [ - {value: 'Arial', id: 12}, - {value: 'Century Gothic', id: 13}, - {value: 'DejaVu Sans', id: 14}, - {value: 'Futura', id: 15}, - {value: 'Geneva', id: 16}, - {value: 'Liberation Sans', id: 17} + { value: 'Arial', id: 12 }, + { value: 'Century Gothic', id: 13 }, + { value: 'DejaVu Sans', id: 14 }, + { value: 'Futura', id: 15 }, + { value: 'Geneva', id: 16 }, + { value: 'Liberation Sans', id: 17 } ] }, { value: 'Monospace - With ASYNC CHILDREN', id: 18, // children property is ignored if "loadChildren" is present - children: [{value: 'I am the font that will be ignored'}], + children: [{ value: 'I am the font that will be ignored' }], loadChildren: (callback) => { setTimeout(() => { callback([ - {value: 'Input Mono', id: 19}, - {value: 'Roboto Mono', id: 20}, - {value: 'Liberation Mono', id: 21}, - {value: 'Hack', id: 22}, - {value: 'Consolas', id: 23}, - {value: 'Menlo', id: 24}, - {value: 'Source Code Pro', id: 25} + { value: 'Input Mono', id: 19 }, + { value: 'Roboto Mono', id: 20 }, + { value: 'Liberation Mono', id: 21 }, + { value: 'Hack', id: 22 }, + { value: 'Consolas', id: 23 }, + { value: 'Menlo', id: 24 }, + { value: 'Source Code Pro', id: 25 } ]); }, 5000); } @@ -243,6 +248,7 @@ export class AppComponent implements OnInit { value: '/', id: 1, settings: { + cssClasses: { expanded: 'fa fa-caret-down', collapsed: 'fa fa-caret-right', @@ -259,16 +265,16 @@ export class AppComponent implements OnInit { value: 'bin', id: 2, children: [ - {value: 'bash', id: 3}, - {value: 'umount', id: 4}, - {value: 'cp', id: 5}, - {value: 'less', id: 6}, - {value: 'rmdir', id: 7}, - {value: 'touch', id: 8}, - {value: 'chgrp', id: 9}, - {value: 'chmod', id: 10}, - {value: 'chown', id: 11}, - {value: 'nano', id: 12} + { value: 'bash', id: 3 }, + { value: 'umount', id: 4 }, + { value: 'cp', id: 5 }, + { value: 'less', id: 6 }, + { value: 'rmdir', id: 7 }, + { value: 'touch', id: 8 }, + { value: 'chgrp', id: 9 }, + { value: 'chmod', id: 10 }, + { value: 'chown', id: 11 }, + { value: 'nano', id: 12 } ] }, { @@ -279,13 +285,13 @@ export class AppComponent implements OnInit { value: 'grub', id: 14, children: [ - {value: 'fonts', id: 15}, - {value: 'gfxblacklist.txt', id: 16}, - {value: 'grub.cfg', id: 17}, - {value: 'grubenv', id: 18}, - {value: 'i386-pc', id: 19}, - {value: 'locale', id: 20}, - {value: 'unicode.pf2', id: 21} + { value: 'fonts', id: 15 }, + { value: 'gfxblacklist.txt', id: 16 }, + { value: 'grub.cfg', id: 17 }, + { value: 'grubenv', id: 18 }, + { value: 'i386-pc', id: 19 }, + { value: 'locale', id: 20 }, + { value: 'unicode.pf2', id: 21 } ] }, { @@ -293,15 +299,15 @@ export class AppComponent implements OnInit { id: 22, children: [] }, - {value: 'abi-4.4.0-57-generic', id: 23}, - {value: 'config-4.4.0-57-generic', id: 24}, - {value: 'initrd.img-4.4.0-47-generic', id: 25}, - {value: 'initrd.img-4.4.0-57-generic', id: 26}, - {value: 'memtest86+.bin', id: 27}, - {value: 'System.map-4.4.0-57-generic', id: 28}, - {value: 'memtest86+.elf', id: 29}, - {value: 'vmlinuz-4.4.0-57-generic', id: 30}, - {value: 'memtest86+_multiboot.bin', id: 31} + { value: 'abi-4.4.0-57-generic', id: 23 }, + { value: 'config-4.4.0-57-generic', id: 24 }, + { value: 'initrd.img-4.4.0-47-generic', id: 25 }, + { value: 'initrd.img-4.4.0-57-generic', id: 26 }, + { value: 'memtest86+.bin', id: 27 }, + { value: 'System.map-4.4.0-57-generic', id: 28 }, + { value: 'memtest86+.elf', id: 29 }, + { value: 'vmlinuz-4.4.0-57-generic', id: 30 }, + { value: 'memtest86+_multiboot.bin', id: 31 } ] }, { @@ -335,8 +341,8 @@ export class AppComponent implements OnInit { } ] }, - {value: 'cdrom', id: 34, children: []}, - {value: 'dev', id: 35, children: []}, + { value: 'cdrom', id: 34, children: [] }, + { value: 'dev', id: 35, children: [] }, { value: 'etc', id: 36, @@ -344,10 +350,10 @@ export class AppComponent implements OnInit { console.log('callback function called to load etc`s children'); setTimeout(() => { callback([ - {value: 'apache2', id: 82, children: []}, - {value: 'nginx', id: 83, children: []}, - {value: 'dhcp', id: 84, children: []}, - {value: 'dpkg', id: 85, children: []} + { value: 'apache2', id: 82, children: [] }, + { value: 'nginx', id: 83, children: [] }, + { value: 'dhcp', id: 84, children: [] }, + { value: 'dpkg', id: 85, children: [] } ]); }); } @@ -372,38 +378,38 @@ export class AppComponent implements OnInit { value: 'bills', id: 41, children: [ - {value: '2016-07-01-mobile.pdf', id: 42}, - {value: '2016-07-01-electricity.pdf', id: 43}, - {value: '2016-07-01-water.pdf', id: 44}, - {value: '2016-07-01-internet.pdf', id: 45}, - {value: '2016-08-01-mobile.pdf', id: 46}, - {value: '2016-10-01-internet.pdf', id: 47} + { value: '2016-07-01-mobile.pdf', id: 42 }, + { value: '2016-07-01-electricity.pdf', id: 43 }, + { value: '2016-07-01-water.pdf', id: 44 }, + { value: '2016-07-01-internet.pdf', id: 45 }, + { value: '2016-08-01-mobile.pdf', id: 46 }, + { value: '2016-10-01-internet.pdf', id: 47 } ] }, - {value: 'photos', id: 48, children: []} + { value: 'photos', id: 48, children: [] } ] } ] }, - {value: 'Downloads', id: 49, children: []}, - {value: 'Desktop', id: 50, children: []}, - {value: 'Pictures', id: 51, children: []}, + { value: 'Downloads', id: 49, children: [] }, + { value: 'Desktop', id: 50, children: [] }, + { value: 'Pictures', id: 51, children: [] }, { value: 'Music', id: 52, - children: [{value: 'won\'t be displayed'}], + children: [{ value: 'won\'t be displayed' }], loadChildren: (callback) => { setTimeout(() => { callback([ - {value: '2Cellos', id: 78, children: []}, - {value: 'Michael Jackson', id: 79, children: []}, - {value: 'AC/DC', id: 80, children: []}, - {value: 'Adel', id: 81, children: []} + { value: '2Cellos', id: 78, children: [] }, + { value: 'Michael Jackson', id: 79, children: [] }, + { value: 'AC/DC', id: 80, children: [] }, + { value: 'Adel', id: 81, children: [] } ]); }, 5000); } }, - {value: 'Public', id: 53, children: []} + { value: 'Public', id: 53, children: [] } ] }, { @@ -413,7 +419,7 @@ export class AppComponent implements OnInit { leftMenu: true }, children: [ - {value: 'Documents', id: 55, children: []}, + { value: 'Documents', id: 55, children: [] }, { value: 'Downloads - custom left menu template', id: 56, @@ -423,33 +429,33 @@ export class AppComponent implements OnInit { } }, children: [ - {value: 'Actobat3', id: 57}, - {value: 'Complib', id: 58}, - {value: 'Eudora', id: 59}, - {value: 'java', id: 60}, - {value: 'drivers', id: 61}, - {value: 'kathy', id: 62} + { value: 'Actobat3', id: 57 }, + { value: 'Complib', id: 58 }, + { value: 'Eudora', id: 59 }, + { value: 'java', id: 60 }, + { value: 'drivers', id: 61 }, + { value: 'kathy', id: 62 } ] }, - {value: 'Desktop', id: 63, children: []}, - {value: 'Pictures', id: 64, children: []}, - {value: 'Music', id: 65, children: []}, - {value: 'Public', id: 66, children: []} + { value: 'Desktop', id: 63, children: [] }, + { value: 'Pictures', id: 64, children: [] }, + { value: 'Music', id: 65, children: [] }, + { value: 'Public', id: 66, children: [] } ] } ] }, - {value: 'lib', id: 67, children: []}, - {value: 'media', id: 68, children: []}, - {value: 'opt', id: 69, children: []}, - {value: 'proc', id: 70, children: []}, - {value: 'root', id: 71, children: []}, - {value: 'run', id: 72, children: []}, - {value: 'sbin', id: 73, children: []}, - {value: 'srv', id: 74, children: []}, - {value: 'sys', id: 75, children: []}, - {value: 'usr', id: 76, children: []}, - {value: 'var', id: 77, children: []} + { value: 'lib', id: 67, children: [] }, + { value: 'media', id: 68, children: [] }, + { value: 'opt', id: 69, children: [] }, + { value: 'proc', id: 70, children: [] }, + { value: 'root', id: 71, children: [] }, + { value: 'run', id: 72, children: [] }, + { value: 'sbin', id: 73, children: [] }, + { value: 'srv', id: 74, children: [] }, + { value: 'sys', id: 75, children: [] }, + { value: 'usr', id: 76, children: [] }, + { value: 'var', id: 77, children: [] } ] }; private lastFFSNodeId = 86; @@ -462,31 +468,31 @@ export class AppComponent implements OnInit { { value: 'Web Application Icons', children: [ - {value: 'calendar', icon: 'fa-calendar' }, - {value: 'download', icon: 'fa-download' }, - {value: 'group', icon: 'fa-group' }, - {value: 'print', icon: 'fa-print' } + { value: 'calendar', icon: 'fa-calendar' }, + { value: 'download', icon: 'fa-download' }, + { value: 'group', icon: 'fa-group' }, + { value: 'print', icon: 'fa-print' } ] }, { value: 'Hand Icons', children: [ - {value: 'pointer', icon: 'fa-hand-pointer-o' }, - {value: 'grab', icon: 'fa-hand-rock-o' }, - {value: 'thumbs up', icon: 'fa-thumbs-o-up ' }, - {value: 'thumbs down', icon: 'fa-thumbs-o-down' } + { value: 'pointer', icon: 'fa-hand-pointer-o' }, + { value: 'grab', icon: 'fa-hand-rock-o' }, + { value: 'thumbs up', icon: 'fa-thumbs-o-up ' }, + { value: 'thumbs down', icon: 'fa-thumbs-o-down' } ] }, { value: 'File Type Icons', children: [ - {value: 'file', icon: 'fa-file-o' }, - {value: 'audio', icon: 'fa-file-audio-o' }, - {value: 'movie', icon: 'fa-file-movie-o ' }, - {value: 'archive', icon: 'fa-file-zip-o' } + { value: 'file', icon: 'fa-file-o' }, + { value: 'audio', icon: 'fa-file-audio-o' }, + { value: 'movie', icon: 'fa-file-movie-o ' }, + { value: 'archive', icon: 'fa-file-zip-o' } ] }, - ] + ] }; private static logEvent(e: NodeEvent, message: string): void { @@ -502,8 +508,8 @@ export class AppComponent implements OnInit { { value: 'Aspect-oriented programming', children: [ - {value: 'AspectJ'}, - {value: 'AspectC++'} + { value: 'AspectJ' }, + { value: 'AspectC++' } ] }, { @@ -520,16 +526,16 @@ export class AppComponent implements OnInit { } } as RenamableNode }, - {value: 'C++'}, - {value: 'C#'} + { value: 'C++' }, + { value: 'C#' } ] }, { value: 'Prototype-based programming', children: [ - {value: 'JavaScript'}, - {value: 'CoffeeScript'}, - {value: 'TypeScript'} + { value: 'JavaScript' }, + { value: 'CoffeeScript' }, + { value: 'TypeScript' } ] } ] @@ -594,11 +600,11 @@ export class AppComponent implements OnInit { const treeController = this.treeFFS.getControllerByNodeId(id); if (treeController && typeof treeController.setChildren === 'function') { treeController.setChildren([ - {value: 'apache2', id: 82, children: []}, - {value: 'nginx', id: 83, children: []}, - {value: 'dhcp', id: 84, children: []}, - {value: 'dpkg', id: 85, children: []}, - {value: 'gdb', id: 86, children: []} + { value: 'apache2', id: 82, children: [] }, + { value: 'nginx', id: 83, children: [] }, + { value: 'dhcp', id: 84, children: [] }, + { value: 'dpkg', id: 85, children: [] }, + { value: 'gdb', id: 86, children: [] } ]); } else { console.log('There isn`t a controller for a node with id - ' + id); @@ -614,4 +620,24 @@ export class AppComponent implements OnInit { console.log(`Controller is absent for a node with id: ${id}`); } } + + public checkFolder(id: number): void { + const treeController = this.treeFFS.getControllerByNodeId(id); + if (treeController) { + treeController.check(); + } else { + console.log(`Controller is absent for a node with id: ${id}`); + } + + } + + public uncheckFolder(id: number): void { + const treeController = this.treeFFS.getControllerByNodeId(id); + if (treeController) { + treeController.uncheck(); + } else { + console.log(`Controller is absent for a node with id: ${id}`); + } + + } } diff --git a/src/tree-controller.ts b/src/tree-controller.ts index 90e2423c..747cba79 100644 --- a/src/tree-controller.ts +++ b/src/tree-controller.ts @@ -88,6 +88,13 @@ export class TreeController { public startRenaming(): void { this.tree.markAsBeingRenamed(); + } - } + public check() : void { + this.component.onNodeChecked() +} + +public uncheck() : void { + this.component.onNodeUnchecked() +} } diff --git a/src/tree-internal.component.ts b/src/tree-internal.component.ts index 98d9ee8b..7e106add 100644 --- a/src/tree-internal.component.ts +++ b/src/tree-internal.component.ts @@ -1,10 +1,11 @@ -import { Component, ElementRef, TemplateRef, Inject, Input, OnDestroy, OnInit, OnChanges, SimpleChanges } from '@angular/core'; +import { Component, ElementRef, TemplateRef, Inject, Input, OnDestroy, OnInit, OnChanges, SimpleChanges, ViewChild } from '@angular/core'; import * as TreeTypes from './tree.types'; import { Tree } from './tree'; import { TreeController } from './tree-controller'; import { NodeMenuService } from './menu/node-menu.service'; import { NodeMenuItemAction, NodeMenuItemSelectedEvent } from './menu/menu.events'; import { NodeEditableEvent, NodeEditableEventAction } from './editable/editable.events'; +import { NodeRemovedEvent, NodeCheckedEvent } from './tree.events' import { TreeService } from './tree.service'; import * as EventUtils from './utils/event.utils'; import { NodeDraggableEvent } from './draggable/draggable.events'; @@ -24,6 +25,11 @@ import { get } from './utils/fn.utils'; [tree]="tree">
+ +
+ +
+
- @@ -49,7 +55,7 @@ import { get } from './utils/fn.utils'; - + @@ -68,13 +74,18 @@ export class TreeInternalComponent implements OnInit, OnChanges, OnDestroy { public isSelected = false; public isRightMenuVisible = false; public isLeftMenuVisible = false; + public isChecked = false; public controller: TreeController; + + @ViewChild('checkbox') + _checkboxElement: ElementRef; + private subscriptions: Subscription[] = []; - public constructor(@Inject(NodeMenuService) private nodeMenuService: NodeMenuService, - @Inject(TreeService) public treeService: TreeService, - @Inject(ElementRef) public element: ElementRef) { + public constructor( @Inject(NodeMenuService) private nodeMenuService: NodeMenuService, + @Inject(TreeService) public treeService: TreeService, + @Inject(ElementRef) public element: ElementRef) { } public ngOnInit(): void { @@ -83,7 +94,9 @@ export class TreeInternalComponent implements OnInit, OnChanges, OnDestroy { this.treeService.setController(this.tree.node.id, this.controller); } - this.settings = this.settings || { rootIsVisible: true }; + this.settings = this.settings || { rootIsVisible: true, enableCheckboxes: false }; + + this.isChecked = this.tree.isChecked; this.subscriptions.push(this.nodeMenuService.hideMenuStream(this.element) .subscribe(() => { @@ -235,4 +248,43 @@ export class TreeInternalComponent implements OnInit, OnChanges, OnDestroy { public isRootHidden(): boolean { return this.tree.isRoot() && !this.settings.rootIsVisible; } + + public NodeCheckSatusChanged() { + if (!this.isChecked) { + this.onNodeChecked(); + } + else { + this.onNodeUnchecked(); + } + + } + + public onNodeChecked(): void { + this._checkboxElement.nativeElement.indeterminate = false; + this.treeService.fireNodeChecked(this.tree); + this.executeOnChildController(controller => controller.check()); + this.isChecked = true; + this.tree.isChecked = true; + } + + public onNodeUnchecked(): void { + this._checkboxElement.nativeElement.indeterminate = false; + + this.treeService.fireNodeUnchecked(this.tree); + + this.executeOnChildController(controller => controller.uncheck()); + this.isChecked = false; + this.tree.isChecked = false; + } + + private executeOnChildController(executor: (controller: TreeController) => void) { + if (this.tree.children) { + this.tree.children.forEach((child: Tree) => { + let controller = this.treeService.getController(child.id); + if (controller != null) { + executor(controller); + } + }); + } + } } diff --git a/src/tree.component.ts b/src/tree.component.ts index 58409faa..b888913b 100644 --- a/src/tree.component.ts +++ b/src/tree.component.ts @@ -4,7 +4,7 @@ import { } from '@angular/core'; import { TreeService } from './tree.service'; import * as TreeTypes from './tree.types'; -import { NodeEvent } from './tree.events'; +import { NodeEvent, NodeCheckedEvent, NodeUncheckedEvent } from './tree.events'; import { Tree } from './tree'; import { TreeController } from './tree-controller'; import { Subscription } from 'rxjs/Subscription'; @@ -15,7 +15,7 @@ import { Subscription } from 'rxjs/Subscription'; providers: [TreeService] }) export class TreeComponent implements OnInit, OnChanges, OnDestroy { - private static EMPTY_TREE: Tree = new Tree({value: ''}); + private static EMPTY_TREE: Tree = new Tree({ value: '' }); /* tslint:disable:no-input-rename */ @Input('tree') @@ -46,8 +46,14 @@ export class TreeComponent implements OnInit, OnChanges, OnDestroy { @Output() public nodeCollapsed: EventEmitter = new EventEmitter(); -@Output() -public loadNextLevel: EventEmitter = new EventEmitter(); + @Output() + public loadNextLevel: EventEmitter = new EventEmitter(); + + @Output() + public nodeChecked: EventEmitter = new EventEmitter(); + + @Output() + public nodeUnchecked: EventEmitter = new EventEmitter(); public tree: Tree; @ViewChild('rootComponent') public rootComponent; @@ -56,7 +62,7 @@ public loadNextLevel: EventEmitter = new EventEmitter(); private subscriptions: Subscription[] = []; - public constructor(@Inject(TreeService) private treeService: TreeService) { + public constructor( @Inject(TreeService) private treeService: TreeService) { } public ngOnChanges(changes: SimpleChanges): void { @@ -99,6 +105,11 @@ public loadNextLevel: EventEmitter = new EventEmitter(); this.subscriptions.push(this.treeService.loadNextLevel$.subscribe((e: NodeEvent) => { this.loadNextLevel.emit(e); })); + + this.subscriptions.push(this.treeService.nodeChecked$.subscribe((e: NodeCheckedEvent) => this.nodeChecked.emit(e))); + + this.subscriptions.push(this.treeService.nodeUnchecked$.subscribe((e: NodeUncheckedEvent) => this.nodeChecked.emit(e))); + } public getController(): TreeController { diff --git a/src/tree.events.ts b/src/tree.events.ts index ac546b68..4c060211 100644 --- a/src/tree.events.ts +++ b/src/tree.events.ts @@ -60,3 +60,15 @@ export class LoadNextLevelEvent extends NodeEvent { super(node); } } + +export class NodeCheckedEvent extends NodeEvent { + public constructor(node: Tree) { + super(node); + } +} + +export class NodeUncheckedEvent extends NodeEvent { + public constructor(node: Tree) { + super(node); + } +} diff --git a/src/tree.service.ts b/src/tree.service.ts index 5b3f30cb..dfcbcfff 100644 --- a/src/tree.service.ts +++ b/src/tree.service.ts @@ -6,7 +6,9 @@ import { NodeRemovedEvent, NodeRenamedEvent, NodeSelectedEvent, - LoadNextLevelEvent + LoadNextLevelEvent, + NodeCheckedEvent, + NodeUncheckedEvent } from './tree.events'; import { RenamableNode } from './tree.types'; import { Tree } from './tree'; @@ -27,6 +29,8 @@ export class TreeService { public nodeExpanded$: Subject = new Subject(); public nodeCollapsed$: Subject = new Subject(); public loadNextLevel$: Subject = new Subject(); + public nodeChecked$ : Subject = new Subject(); + public nodeUnchecked$ : Subject = new Subject(); private controllers: Map = new Map(); @@ -81,6 +85,14 @@ export class TreeService { this.loadNextLevel$.next(new LoadNextLevelEvent(tree)); } + public fireNodeChecked(tree: Tree) : void { + this.nodeChecked$.next(new NodeCheckedEvent(tree)); + } + + public fireNodeUnchecked(tree: Tree) : void { + this.nodeUnchecked$.next(new NodeUncheckedEvent(tree)); + } + public draggedStream(tree: Tree, element: ElementRef): Observable { return this.nodeDraggableService.draggableNodeEvents$ .filter((e: NodeDraggableEvent) => e.target === element) diff --git a/src/tree.ts b/src/tree.ts index 98d9875e..5a68bb4e 100644 --- a/src/tree.ts +++ b/src/tree.ts @@ -40,6 +40,7 @@ export class Tree { public node: TreeModel; public parent: Tree; + public isChecked: boolean; // STATIC METHODS ---------------------------------------------------------------------------------------------------- diff --git a/src/tree.types.ts b/src/tree.types.ts index 562d163b..7862a1c6 100644 --- a/src/tree.types.ts +++ b/src/tree.types.ts @@ -26,6 +26,7 @@ export interface TreeModel { _status?: TreeStatus; _foldingType?: FoldingType; [additionalData: string]: any; + checked?: boolean; } export interface CssClasses { @@ -85,7 +86,7 @@ export class TreeModelSettings { public static?: boolean; public static merge(sourceA: TreeModel, sourceB: TreeModel): TreeModelSettings { - return defaultsDeep({}, get(sourceA, 'settings'), get(sourceB, 'settings'), {static: false, leftMenu: false, rightMenu: true}); + return defaultsDeep({}, get(sourceA, 'settings'), get(sourceB, 'settings'), { static: false, leftMenu: false, rightMenu: true }); } } @@ -96,6 +97,7 @@ export interface Ng2TreeSettings { * @type boolean */ rootIsVisible?: boolean; + enableCheckboxes?: boolean; } export enum TreeStatus { From e255869473e355c2778b41e8b2be0e4fd80dbcd6 Mon Sep 17 00:00:00 2001 From: TMaster Date: Fri, 24 Nov 2017 12:51:18 +0200 Subject: [PATCH 2/6] Support readonly checkboxes --- src/demo/app/app.component.ts | 13 ++++++++++--- src/tree-internal.component.ts | 9 ++++++--- src/tree.types.ts | 1 + 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/demo/app/app.component.ts b/src/demo/app/app.component.ts index 9d3a088a..487baca8 100644 --- a/src/demo/app/app.component.ts +++ b/src/demo/app/app.component.ts @@ -28,7 +28,7 @@ declare const alertify: any;
-
+
@@ -75,6 +75,7 @@ export class TreeInternalComponent implements OnInit, OnChanges, OnDestroy { public isRightMenuVisible = false; public isLeftMenuVisible = false; public isChecked = false; + public isReadOnly = false; public controller: TreeController; @@ -94,10 +95,12 @@ export class TreeInternalComponent implements OnInit, OnChanges, OnDestroy { this.treeService.setController(this.tree.node.id, this.controller); } - this.settings = this.settings || { rootIsVisible: true, enableCheckboxes: false }; + this.settings = this.settings || { rootIsVisible: true, showCheckboxes: false, enableCheckboxes: true }; this.isChecked = this.tree.isChecked; + this.isReadOnly = has(this.settings, 'enableCheckboxes') ? !this.settings.enableCheckboxes : false; + this.subscriptions.push(this.nodeMenuService.hideMenuStream(this.element) .subscribe(() => { this.isRightMenuVisible = false; diff --git a/src/tree.types.ts b/src/tree.types.ts index 7862a1c6..75b0b4e2 100644 --- a/src/tree.types.ts +++ b/src/tree.types.ts @@ -97,6 +97,7 @@ export interface Ng2TreeSettings { * @type boolean */ rootIsVisible?: boolean; + showCheckboxes?: boolean; enableCheckboxes?: boolean; } From 6994152bda55b2051ce880ecd3aef5b61006fb9a Mon Sep 17 00:00:00 2001 From: TMaster Date: Sat, 25 Nov 2017 12:43:06 +0200 Subject: [PATCH 3/6] Fixing merge issues --- src/tree-internal.component.ts | 1 + src/tree.component.ts | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/tree-internal.component.ts b/src/tree-internal.component.ts index d70fe039..75c66294 100644 --- a/src/tree-internal.component.ts +++ b/src/tree-internal.component.ts @@ -312,6 +312,7 @@ export class TreeInternalComponent implements OnInit, OnChanges, OnDestroy { } }); } + } public hasCustomMenu(): boolean { return this.tree.hasCustomMenu(); diff --git a/src/tree.component.ts b/src/tree.component.ts index a6b6c2f2..735ff7e8 100644 --- a/src/tree.component.ts +++ b/src/tree.component.ts @@ -7,8 +7,6 @@ import * as TreeTypes from './tree.types'; import { NodeEvent, NodeCheckedEvent, NodeUncheckedEvent,MenuItemSelectedEvent } from './tree.events'; -import { NodeEvent, MenuItemSelectedEvent } from './tree.events'; - import { Tree } from './tree'; import { TreeController } from './tree-controller'; import { Subscription } from 'rxjs/Subscription'; From 92a96afa835573a97b3f1588b35174b54f75cf65 Mon Sep 17 00:00:00 2001 From: TMaster Date: Sat, 2 Dec 2017 13:02:37 +0200 Subject: [PATCH 4/6] Support indeterminated state --- src/tree-internal.component.ts | 74 +++++++++++++++++++++++++++++++--- src/tree.events.ts | 6 +++ src/tree.service.ts | 8 +++- 3 files changed, 81 insertions(+), 7 deletions(-) diff --git a/src/tree-internal.component.ts b/src/tree-internal.component.ts index 75c66294..c450e9ed 100644 --- a/src/tree-internal.component.ts +++ b/src/tree-internal.component.ts @@ -8,7 +8,7 @@ import { OnInit, SimpleChanges, TemplateRef, - ViewChild + ViewChild } from '@angular/core'; import * as TreeTypes from './tree.types'; @@ -17,7 +17,7 @@ import { TreeController } from './tree-controller'; import { NodeMenuService } from './menu/node-menu.service'; import { NodeMenuItemAction, NodeMenuItemSelectedEvent } from './menu/menu.events'; import { NodeEditableEvent, NodeEditableEventAction } from './editable/editable.events'; -import { NodeRemovedEvent, NodeCheckedEvent } from './tree.events' +import { NodeEvent, NodeRemovedEvent, NodeCheckedEvent, NodeIndeterminateEvent } from './tree.events' import { TreeService } from './tree.service'; import * as EventUtils from './utils/event.utils'; import { NodeDraggableEvent } from './draggable/draggable.events'; @@ -104,8 +104,8 @@ export class TreeInternalComponent implements OnInit, OnChanges, OnDestroy { public constructor(private nodeMenuService: NodeMenuService, - public treeService: TreeService, - public element: ElementRef) { + public treeService: TreeService, + public element: ElementRef) { } public ngOnInit(): void { @@ -140,6 +140,28 @@ export class TreeInternalComponent implements OnInit, OnChanges, OnDestroy { this.moveNodeToParentTreeAndRemoveFromPreviousOne(e, this.tree); } })); + + this.subscriptions.push(this.treeService.nodeChecked$ + .filter((e: NodeCheckedEvent) => this.eventContainsId(e) && this.tree.children + && this.tree.children.some((child: Tree) => child.id === e.node.id)) + .subscribe((e: NodeCheckedEvent) => { + this.updateIndeterminateState(); + })); + + + this.subscriptions.push(this.treeService.nodeUnchecked$ + .filter((e: NodeCheckedEvent) => this.eventContainsId(e) && this.tree.children + && this.tree.children.some((child: Tree) => child.id === e.node.id)) + .subscribe((e: NodeCheckedEvent) => { + this.updateIndeterminateState(); + })); + + + this.subscriptions.push(this.treeService.nodeIndeterminate$ + .filter((e: NodeIndeterminateEvent) => this.eventContainsId(e) && + this.tree.children && this.tree.children.some((child: Tree) => child.id === e.node.id)) + .subscribe((e: NodeIndeterminateEvent) => { + })); } public ngOnChanges(changes: SimpleChanges): void { @@ -275,6 +297,10 @@ export class TreeInternalComponent implements OnInit, OnChanges, OnDestroy { return this.tree.isRoot() && !this.settings.rootIsVisible; } + public hasCustomMenu(): boolean { + return this.tree.hasCustomMenu(); + } + public NodeCheckSatusChanged() { if (!this.isChecked) { this.onNodeChecked(); @@ -314,8 +340,44 @@ export class TreeInternalComponent implements OnInit, OnChanges, OnDestroy { } } - public hasCustomMenu(): boolean { - return this.tree.hasCustomMenu(); + public updateIndeterminateState(): void { + + setTimeout(() => { //calling setTimeout so the value of isChecked will be updated and after that I'll check the children status. + this.updateIndeterminateStateInternal(); + }, 1) + }; + + + private updateIndeterminateStateInternal(): void { + const checkedChildren = this.tree.children.filter(child => child.isChecked).length; + if (checkedChildren === 0) { + this._checkboxElement.nativeElement.indeterminate = false; + this.isChecked = false; + this.tree.isChecked = false; + this.treeService.fireNodeUnchecked(this.tree) + } + else if (checkedChildren === this.tree.children.length) { + this._checkboxElement.nativeElement.indeterminate = false; + this.isChecked = true; + this.tree.isChecked = true; + this.treeService.fireNodeChecked(this.tree) + } + else { + this.setNodeInderminated(); + } + } + + private setNodeInderminated(): void { + this._checkboxElement.nativeElement.indeterminate = true; + this.treeService.fireNodeIndeterminate(this.tree); + } + + private eventContainsId(event: NodeEvent): boolean { + if (!event.node.id) { + console.log('Checking feature requires a well known id for every node, lease prvide a unique id.') + return false; + } + return true; } } diff --git a/src/tree.events.ts b/src/tree.events.ts index d05aae34..f4e317de 100644 --- a/src/tree.events.ts +++ b/src/tree.events.ts @@ -78,3 +78,9 @@ export class NodeUncheckedEvent extends NodeEvent { super(node); } } + +export class NodeIndeterminateEvent extends NodeEvent { + public constructor(node: Tree) { + super(node); + } +} \ No newline at end of file diff --git a/src/tree.service.ts b/src/tree.service.ts index 3844fa2e..7042b090 100644 --- a/src/tree.service.ts +++ b/src/tree.service.ts @@ -9,7 +9,8 @@ import { LoadNextLevelEvent, NodeCheckedEvent, NodeUncheckedEvent, - MenuItemSelectedEvent} from './tree.events'; + MenuItemSelectedEvent, +NodeIndeterminateEvent} from './tree.events'; import { RenamableNode } from './tree.types'; import { Tree } from './tree'; import { TreeController } from './tree-controller'; @@ -33,6 +34,7 @@ export class TreeService { public loadNextLevel$: Subject = new Subject(); public nodeChecked$ : Subject = new Subject(); public nodeUnchecked$ : Subject = new Subject(); + public nodeIndeterminate$ : Subject = new Subject(); private controllers: Map = new Map(); @@ -140,4 +142,8 @@ export class TreeService { return shouldLoadNextLevel; } + + public fireNodeIndeterminate(tree: Tree) : void { + this.nodeIndeterminate$.next(new NodeIndeterminateEvent(tree)); + } } From f62e3b572ee73a9a09991aea463a089107a4646a Mon Sep 17 00:00:00 2001 From: TMaster Date: Sat, 2 Dec 2017 13:08:49 +0200 Subject: [PATCH 5/6] Start comment with a space --- src/tree-internal.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tree-internal.component.ts b/src/tree-internal.component.ts index c450e9ed..d542ba33 100644 --- a/src/tree-internal.component.ts +++ b/src/tree-internal.component.ts @@ -342,7 +342,7 @@ export class TreeInternalComponent implements OnInit, OnChanges, OnDestroy { public updateIndeterminateState(): void { - setTimeout(() => { //calling setTimeout so the value of isChecked will be updated and after that I'll check the children status. + setTimeout(() => { // Calling setTimeout so the value of isChecked will be updated and after that I'll check the children status. this.updateIndeterminateStateInternal(); }, 1) }; From b86776113d7ca366b110b4b4b6fa51455982d642 Mon Sep 17 00:00:00 2001 From: Georgiy Rychko Date: Sun, 11 Feb 2018 16:40:53 +0200 Subject: [PATCH 6/6] feat(tree): add checkboxes support --- package.json | 2 +- src/demo/app/app.component.ts | 7 +- src/rxjs-imports.ts | 1 + src/tree-controller.ts | 23 ++- src/tree-internal.component.ts | 165 ++++++++---------- src/tree.component.ts | 11 +- src/tree.events.ts | 4 +- src/tree.service.ts | 37 ++-- src/tree.ts | 33 +++- src/tree.types.ts | 13 +- test/data-provider/tree.data-provider.ts | 34 ++-- .../left-menu.tree-internal.component.spec.ts | 10 +- ...right-menu.tree-internal.component.spec.ts | 10 +- test/tree-controller.spec.ts | 78 ++++++++- test/tree-internal.component.spec.ts | 28 +-- test/tree.spec.ts | 5 +- 16 files changed, 279 insertions(+), 182 deletions(-) diff --git a/package.json b/package.json index 3c5f31ba..34bafc8c 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "webdriver-update": "node ./node_modules/protractor/bin/webdriver-manager update" }, "devDependencies": { - "@angular/cli": "1.5.4", + "@angular/cli": "1.6.8", "@angular/common": "5.0.2", "@angular/compiler": "5.0.2", "@angular/compiler-cli": "5.0.2", diff --git a/src/demo/app/app.component.ts b/src/demo/app/app.component.ts index 92cf53ac..0a64eb94 100644 --- a/src/demo/app/app.component.ts +++ b/src/demo/app/app.component.ts @@ -80,7 +80,7 @@ declare const alertify: any; -
@@ -318,7 +318,10 @@ export class AppComponent implements OnInit { { value: 'lost+found', id: 22, - children: [] + children: [], + settings: { + checked: true + } }, { value: 'abi-4.4.0-57-generic', id: 23 }, { value: 'config-4.4.0-57-generic', id: 24 }, diff --git a/src/rxjs-imports.ts b/src/rxjs-imports.ts index 10f1dd35..9a0019a9 100644 --- a/src/rxjs-imports.ts +++ b/src/rxjs-imports.ts @@ -1,5 +1,6 @@ import 'rxjs/add/operator/filter'; import 'rxjs/add/observable/of'; +import 'rxjs/add/operator/merge'; // This forces angular compiler to generate a "rxjs-imports.metadata.json" // with a valid metadata instead of "[null]" diff --git a/src/tree-controller.ts b/src/tree-controller.ts index ff351f89..42768901 100644 --- a/src/tree-controller.ts +++ b/src/tree-controller.ts @@ -4,6 +4,7 @@ import { TreeModel } from './tree.types'; import { NodeMenuItemAction } from './menu/menu.events'; import { TreeInternalComponent } from './tree-internal.component'; import { MouseButtons } from './utils/event.utils'; +import { get } from './utils/fn.utils'; export class TreeController { private tree: Tree; @@ -92,13 +93,21 @@ export class TreeController { public startRenaming(): void { this.tree.markAsBeingRenamed(); - } + } - public check() : void { - this.component.onNodeChecked() -} + public check(): void { + this.component.onNodeChecked(); + } -public uncheck() : void { - this.component.onNodeUnchecked() -} + public uncheck(): void { + this.component.onNodeUnchecked(); + } + + public isChecked(): boolean { + return this.tree.checked; + } + + public isIndetermined(): boolean { + return get(this.component, 'checkboxElementRef.nativeElement.indeterminate'); + } } diff --git a/src/tree-internal.component.ts b/src/tree-internal.component.ts index d542ba33..4e582945 100644 --- a/src/tree-internal.component.ts +++ b/src/tree-internal.component.ts @@ -1,4 +1,3 @@ - import { Component, ElementRef, @@ -8,7 +7,9 @@ import { OnInit, SimpleChanges, TemplateRef, - ViewChild + ViewChild, + AfterViewInit, + AfterContentChecked } from '@angular/core'; import * as TreeTypes from './tree.types'; @@ -17,12 +18,13 @@ import { TreeController } from './tree-controller'; import { NodeMenuService } from './menu/node-menu.service'; import { NodeMenuItemAction, NodeMenuItemSelectedEvent } from './menu/menu.events'; import { NodeEditableEvent, NodeEditableEventAction } from './editable/editable.events'; -import { NodeEvent, NodeRemovedEvent, NodeCheckedEvent, NodeIndeterminateEvent } from './tree.events' +import { NodeEvent, NodeRemovedEvent, NodeCheckedEvent, NodeIndeterminedEvent } from './tree.events'; import { TreeService } from './tree.service'; import * as EventUtils from './utils/event.utils'; import { NodeDraggableEvent } from './draggable/draggable.events'; import { Subscription } from 'rxjs/Subscription'; -import { get, has } from './utils/fn.utils'; +import { get, has, size, isNil } from './utils/fn.utils'; +import { Ng2TreeSettings } from './tree.types'; @Component({ selector: 'tree-internal', @@ -33,13 +35,13 @@ import { get, has } from './utils/fn.utils'; [ngClass]="{rootless: isRootHidden()}" [class.selected]="isSelected" (contextmenu)="showRightMenu($event)" - [nodeDraggable]="element" + [nodeDraggable]="nodeElementRef" [tree]="tree">
-
- +
+
- @@ -79,7 +81,7 @@ import { get, has } from './utils/fn.utils'; ` }) -export class TreeInternalComponent implements OnInit, OnChanges, OnDestroy { +export class TreeInternalComponent implements OnInit, OnChanges, OnDestroy, AfterContentChecked { @Input() public tree: Tree; @@ -92,36 +94,39 @@ export class TreeInternalComponent implements OnInit, OnChanges, OnDestroy { public isSelected = false; public isRightMenuVisible = false; public isLeftMenuVisible = false; - public isChecked = false; public isReadOnly = false; public controller: TreeController; - @ViewChild('checkbox') - _checkboxElement: ElementRef; + public checkboxElementRef: ElementRef; private subscriptions: Subscription[] = []; - public constructor(private nodeMenuService: NodeMenuService, - public treeService: TreeService, - public element: ElementRef) { + public treeService: TreeService, + public nodeElementRef: ElementRef) { } - public ngOnInit(): void { - this.controller = new TreeController(this); - if (get(this.tree, 'node.id', '')) { - this.treeService.setController(this.tree.node.id, this.controller); + public ngAfterContentChecked(): void { + // if a node was checked in settings + // we should notify parent nodes about this + // (they need to switch to appropriate state as well) + if (this.tree.checked) { + this.treeService.fireNodeChecked(this.tree); } + } + public ngOnInit(): void { + const nodeId = get(this.tree, 'node.id', ''); + if (nodeId) { + this.controller = new TreeController(this); + this.treeService.setController(nodeId, this.controller); + } - this.settings = this.settings || { rootIsVisible: true, showCheckboxes: false, enableCheckboxes: true }; - - this.isChecked = this.tree.isChecked; - - this.isReadOnly = has(this.settings, 'enableCheckboxes') ? !this.settings.enableCheckboxes : false; + this.settings = this.settings || new Ng2TreeSettings(); + this.isReadOnly = !get(this.settings, 'enableCheckboxes', true); - this.subscriptions.push(this.nodeMenuService.hideMenuStream(this.element) + this.subscriptions.push(this.nodeMenuService.hideMenuStream(this.nodeElementRef) .subscribe(() => { this.isRightMenuVisible = false; this.isLeftMenuVisible = false; @@ -130,7 +135,7 @@ export class TreeInternalComponent implements OnInit, OnChanges, OnDestroy { this.subscriptions.push(this.treeService.unselectStream(this.tree) .subscribe(() => this.isSelected = false)); - this.subscriptions.push(this.treeService.draggedStream(this.tree, this.element) + this.subscriptions.push(this.treeService.draggedStream(this.tree, this.nodeElementRef) .subscribe((e: NodeDraggableEvent) => { if (this.tree.hasSibling(e.captured.tree)) { this.swapWithSibling(e.captured.tree, this.tree); @@ -141,26 +146,10 @@ export class TreeInternalComponent implements OnInit, OnChanges, OnDestroy { } })); - this.subscriptions.push(this.treeService.nodeChecked$ - .filter((e: NodeCheckedEvent) => this.eventContainsId(e) && this.tree.children - && this.tree.children.some((child: Tree) => child.id === e.node.id)) + this.subscriptions.push(this.treeService.nodeChecked$.merge(this.treeService.nodeUnchecked$) + .filter((e: NodeCheckedEvent) => this.eventContainsId(e) && this.tree.hasChild(e.node)) .subscribe((e: NodeCheckedEvent) => { - this.updateIndeterminateState(); - })); - - - this.subscriptions.push(this.treeService.nodeUnchecked$ - .filter((e: NodeCheckedEvent) => this.eventContainsId(e) && this.tree.children - && this.tree.children.some((child: Tree) => child.id === e.node.id)) - .subscribe((e: NodeCheckedEvent) => { - this.updateIndeterminateState(); - })); - - - this.subscriptions.push(this.treeService.nodeIndeterminate$ - .filter((e: NodeIndeterminateEvent) => this.eventContainsId(e) && - this.tree.children && this.tree.children.some((child: Tree) => child.id === e.node.id)) - .subscribe((e: NodeIndeterminateEvent) => { + this.updateCheckboxState(); })); } @@ -207,7 +196,7 @@ export class TreeInternalComponent implements OnInit, OnChanges, OnDestroy { if (EventUtils.isRightButtonClicked(e)) { this.isRightMenuVisible = !this.isRightMenuVisible; - this.nodeMenuService.hideMenuForAllNodesExcept(this.element); + this.nodeMenuService.hideMenuForAllNodesExcept(this.nodeElementRef); } e.preventDefault(); } @@ -219,7 +208,7 @@ export class TreeInternalComponent implements OnInit, OnChanges, OnDestroy { if (EventUtils.isLeftButtonClicked(e)) { this.isLeftMenuVisible = !this.isLeftMenuVisible; - this.nodeMenuService.hideMenuForAllNodesExcept(this.element); + this.nodeMenuService.hideMenuForAllNodesExcept(this.nodeElementRef); if (this.isLeftMenuVisible) { e.preventDefault(); } @@ -301,81 +290,73 @@ export class TreeInternalComponent implements OnInit, OnChanges, OnDestroy { return this.tree.hasCustomMenu(); } - public NodeCheckSatusChanged() { - if (!this.isChecked) { + public switchNodeCheckStatus() { + if (!this.tree.checked) { this.onNodeChecked(); - } - else { + } else { this.onNodeUnchecked(); } - } public onNodeChecked(): void { - this._checkboxElement.nativeElement.indeterminate = false; + if (!this.checkboxElementRef) { + return; + } + + this.checkboxElementRef.nativeElement.indeterminate = false; this.treeService.fireNodeChecked(this.tree); this.executeOnChildController(controller => controller.check()); - this.isChecked = true; - this.tree.isChecked = true; + this.tree.checked = true; } public onNodeUnchecked(): void { - this._checkboxElement.nativeElement.indeterminate = false; + if (!this.checkboxElementRef) { + return; + } + this.checkboxElementRef.nativeElement.indeterminate = false; this.treeService.fireNodeUnchecked(this.tree); - this.executeOnChildController(controller => controller.uncheck()); - this.isChecked = false; - this.tree.isChecked = false; + this.tree.checked = false; } private executeOnChildController(executor: (controller: TreeController) => void) { - if (this.tree.children) { + if (this.tree.hasLoadedChildern()) { this.tree.children.forEach((child: Tree) => { - let controller = this.treeService.getController(child.id); - if (controller != null) { + const controller = this.treeService.getController(child.id); + if (!isNil(controller)) { executor(controller); } }); } } - public updateIndeterminateState(): void { - - setTimeout(() => { // Calling setTimeout so the value of isChecked will be updated and after that I'll check the children status. - this.updateIndeterminateStateInternal(); - }, 1) - }; - - - private updateIndeterminateStateInternal(): void { - const checkedChildren = this.tree.children.filter(child => child.isChecked).length; - - if (checkedChildren === 0) { - this._checkboxElement.nativeElement.indeterminate = false; - this.isChecked = false; - this.tree.isChecked = false; - this.treeService.fireNodeUnchecked(this.tree) - } - else if (checkedChildren === this.tree.children.length) { - this._checkboxElement.nativeElement.indeterminate = false; - this.isChecked = true; - this.tree.isChecked = true; - this.treeService.fireNodeChecked(this.tree) - } - else { - this.setNodeInderminated(); + updateCheckboxState(): void { + if (!this.checkboxElementRef) { + return; } - } - private setNodeInderminated(): void { - this._checkboxElement.nativeElement.indeterminate = true; - this.treeService.fireNodeIndeterminate(this.tree); + // Calling setTimeout so the value of isChecked will be updated and after that I'll check the children status. + setTimeout(() => { + const checkedChildrenAmount = this.tree.checkedChildrenAmount(); + if (checkedChildrenAmount === 0) { + this.checkboxElementRef.nativeElement.indeterminate = false; + this.tree.checked = false; + this.treeService.fireNodeUnchecked(this.tree); + } else if (checkedChildrenAmount === this.tree.loadedChildrenAmount()) { + this.checkboxElementRef.nativeElement.indeterminate = false; + this.tree.checked = true; + this.treeService.fireNodeChecked(this.tree); + } else { + this.checkboxElementRef.nativeElement.indeterminate = true; + this.treeService.fireNodeIndetermined(this.tree); + } + }); } private eventContainsId(event: NodeEvent): boolean { if (!event.node.id) { - console.log('Checking feature requires a well known id for every node, lease prvide a unique id.') + console.warn('"Node with checkbox" feature requires a unique id assigned to every node, please consider to add it.'); return false; } return true; diff --git a/src/tree.component.ts b/src/tree.component.ts index 735ff7e8..f4319f3a 100644 --- a/src/tree.component.ts +++ b/src/tree.component.ts @@ -5,7 +5,7 @@ import { import { TreeService } from './tree.service'; import * as TreeTypes from './tree.types'; -import { NodeEvent, NodeCheckedEvent, NodeUncheckedEvent,MenuItemSelectedEvent } from './tree.events'; +import { NodeEvent, NodeCheckedEvent, NodeUncheckedEvent, MenuItemSelectedEvent } from './tree.events'; import { Tree } from './tree'; import { TreeController } from './tree-controller'; @@ -115,10 +115,13 @@ export class TreeComponent implements OnInit, OnChanges, OnDestroy { this.loadNextLevel.emit(e); })); - this.subscriptions.push(this.treeService.nodeChecked$.subscribe((e: NodeCheckedEvent) => this.nodeChecked.emit(e))); - - this.subscriptions.push(this.treeService.nodeUnchecked$.subscribe((e: NodeUncheckedEvent) => this.nodeChecked.emit(e))); + this.subscriptions.push(this.treeService.nodeChecked$.subscribe((e: NodeCheckedEvent) => { + this.nodeChecked.emit(e); + })); + this.subscriptions.push(this.treeService.nodeUnchecked$.subscribe((e: NodeUncheckedEvent) => { + this.nodeUnchecked.emit(e); + })); } public getController(): TreeController { diff --git a/src/tree.events.ts b/src/tree.events.ts index f4e317de..3df84d83 100644 --- a/src/tree.events.ts +++ b/src/tree.events.ts @@ -79,8 +79,8 @@ export class NodeUncheckedEvent extends NodeEvent { } } -export class NodeIndeterminateEvent extends NodeEvent { +export class NodeIndeterminedEvent extends NodeEvent { public constructor(node: Tree) { super(node); } -} \ No newline at end of file +} diff --git a/src/tree.service.ts b/src/tree.service.ts index 7042b090..e3bd83f0 100644 --- a/src/tree.service.ts +++ b/src/tree.service.ts @@ -10,7 +10,8 @@ import { NodeCheckedEvent, NodeUncheckedEvent, MenuItemSelectedEvent, -NodeIndeterminateEvent} from './tree.events'; + NodeIndeterminedEvent +} from './tree.events'; import { RenamableNode } from './tree.types'; import { Tree } from './tree'; import { TreeController } from './tree-controller'; @@ -32,9 +33,9 @@ export class TreeService { public nodeCollapsed$: Subject = new Subject(); public menuItemSelected$: Subject = new Subject(); public loadNextLevel$: Subject = new Subject(); - public nodeChecked$ : Subject = new Subject(); - public nodeUnchecked$ : Subject = new Subject(); - public nodeIndeterminate$ : Subject = new Subject(); + public nodeChecked$: Subject = new Subject(); + public nodeUnchecked$: Subject = new Subject(); + public nodeIndetermined$: Subject = new Subject(); private controllers: Map = new Map(); @@ -93,11 +94,11 @@ export class TreeService { this.loadNextLevel$.next(new LoadNextLevelEvent(tree)); } - public fireNodeChecked(tree: Tree) : void { + public fireNodeChecked(tree: Tree): void { this.nodeChecked$.next(new NodeCheckedEvent(tree)); } - public fireNodeUnchecked(tree: Tree) : void { + public fireNodeUnchecked(tree: Tree): void { this.nodeUnchecked$.next(new NodeUncheckedEvent(tree)); } @@ -130,20 +131,20 @@ export class TreeService { } private shouldFireLoadNextLevel(tree: Tree): boolean { + const shouldLoadNextLevel = + tree.node.emitLoadNextLevel + && !tree.node.loadChildren + && !tree.childrenAreBeingLoaded() + && isEmpty(tree.children); + + if (shouldLoadNextLevel) { + tree.loadingChildrenRequested(); + } - const shouldLoadNextLevel = tree.node.emitLoadNextLevel && - !tree.node.loadChildren && - !tree.childrenAreBeingLoaded() && - (!tree.children || isEmpty(tree.children)); - - if (shouldLoadNextLevel) { - tree.loadingChildrenRequested(); - } - - return shouldLoadNextLevel; + return shouldLoadNextLevel; } - public fireNodeIndeterminate(tree: Tree) : void { - this.nodeIndeterminate$.next(new NodeIndeterminateEvent(tree)); + public fireNodeIndetermined(tree: Tree): void { + this.nodeIndetermined$.next(new NodeIndeterminedEvent(tree)); } } diff --git a/src/tree.ts b/src/tree.ts index 07f6e2e7..efb9236f 100644 --- a/src/tree.ts +++ b/src/tree.ts @@ -45,7 +45,6 @@ export class Tree { public node: TreeModel; public parent: Tree; - public isChecked: boolean; // STATIC METHODS ---------------------------------------------------------------------------------------------------- @@ -94,9 +93,11 @@ export class Tree { private buildTreeFromModel(model: TreeModel, parent: Tree, isBranch: boolean): void { this.parent = parent; - this.node = Object.assign(omit(model, 'children') as TreeModel, { - settings: TreeModelSettings.merge(model, get(parent, 'node') as TreeModel) - }, { emitLoadNextLevel: model.emitLoadNextLevel === true }) as TreeModel; + this.node = Object.assign( + omit(model, 'children') as TreeModel, + { settings: TreeModelSettings.merge(model, get(parent, 'node')) }, + { emitLoadNextLevel: model.emitLoadNextLevel === true } + ) as TreeModel; if (isFunction(this.node.loadChildren)) { this._loadChildren = this.node.loadChildren; @@ -227,6 +228,30 @@ export class Tree { return this.node.value; } + public set checked(checked: boolean) { + this.node.settings = Object.assign({}, this.node.settings, { checked }); + } + + public get checked(): boolean { + return !!get(this.node.settings, 'checked'); + } + + public get checkedChildren(): Tree[] { + return this.hasLoadedChildern() ? this.children.filter(child => child.checked) : []; + } + + hasLoadedChildern() { + return !isEmpty(this.children); + } + + loadedChildrenAmount() { + return size(this.children); + } + + checkedChildrenAmount() { + return size(this.checkedChildren); + } + /** * Set the value of the current node * @param {(string|RenamableNode)} value - The new value of the node. diff --git a/src/tree.types.ts b/src/tree.types.ts index 23fd1d52..f21ba887 100644 --- a/src/tree.types.ts +++ b/src/tree.types.ts @@ -27,7 +27,6 @@ export interface TreeModel { _status?: TreeStatus; _foldingType?: FoldingType; [additionalData: string]: any; - checked?: boolean; } export interface CssClasses { @@ -95,25 +94,27 @@ export class TreeModelSettings { public isCollapsedOnInit?: boolean; + public checked?: boolean; + public static merge(sourceA: TreeModel, sourceB: TreeModel): TreeModelSettings { return defaultsDeep( {}, get(sourceA, 'settings'), get(sourceB, 'settings'), - {static: false, leftMenu: false, rightMenu: true, isCollapsedOnInit: false} + {static: false, leftMenu: false, rightMenu: true, isCollapsedOnInit: false, checked: false} ); } } -export interface Ng2TreeSettings { +export class Ng2TreeSettings { /** * Indicates root visibility in the tree. When true - root is invisible. * @name Ng2TreeSettings#rootIsVisible * @type boolean */ - rootIsVisible?: boolean; - showCheckboxes?: boolean; - enableCheckboxes?: boolean; + rootIsVisible? = true; + showCheckboxes? = false; + enableCheckboxes? = true; } export enum TreeStatus { diff --git a/test/data-provider/tree.data-provider.ts b/test/data-provider/tree.data-provider.ts index 2396fb89..26444f95 100644 --- a/test/data-provider/tree.data-provider.ts +++ b/test/data-provider/tree.data-provider.ts @@ -3,72 +3,72 @@ export class TreeDataProvider { 'default values': { treeModelA: { value: '42' }, treeModelB: { value: '12' }, - result: { static: false, leftMenu: false, rightMenu: true, isCollapsedOnInit: false } + result: { static: false, leftMenu: false, rightMenu: true, isCollapsedOnInit: false, checked: false } }, 'first settings source has higher priority': { - treeModelA: { value: '42', settings: { static: true, leftMenu: true, rightMenu: true, isCollapsedOnInit: true } }, - treeModelB: { value: '12', settings: { static: false, leftMenu: false, rightMenu: false, isCollapsedOnInit: false } }, - result: { static: true, leftMenu: true, rightMenu: true, isCollapsedOnInit: true } + treeModelA: { value: '42', settings: { static: true, leftMenu: true, rightMenu: true, isCollapsedOnInit: true, checked: true } }, + treeModelB: { value: '12', settings: { static: false, leftMenu: false, rightMenu: false, isCollapsedOnInit: false, checked: false } }, + result: { static: true, leftMenu: true, rightMenu: true, isCollapsedOnInit: true, checked: true } }, 'second settings source has priority if first settings source doesn\'t have the option': { treeModelA: { value: '42' }, - treeModelB: { value: '12', settings: { static: true, leftMenu: true, rightMenu: false, isCollapsedOnInit: true } }, - result: { static: true, leftMenu: true, rightMenu: false, isCollapsedOnInit: true } + treeModelB: { value: '12', settings: { static: true, leftMenu: true, rightMenu: false, isCollapsedOnInit: true, checked: true } }, + result: { static: true, leftMenu: true, rightMenu: false, isCollapsedOnInit: true, checked: true } }, 'first expanded property of cssClasses has higher priority': { treeModelA: { value: '12', settings: { cssClasses: { expanded: 'arrow-down-o' } } }, treeModelB: { value: '42', settings: { cssClasses: { expanded: 'arrow-down', collapsed: 'arrow-right', empty: 'arrow-gray', leaf: 'dot' } } }, - result: { isCollapsedOnInit: false, static: false, leftMenu: false, rightMenu: true, cssClasses: { expanded: 'arrow-down-o', collapsed: 'arrow-right', empty: 'arrow-gray', leaf: 'dot' } } + result: { isCollapsedOnInit: false, static: false, leftMenu: false, rightMenu: true, checked: false, cssClasses: { expanded: 'arrow-down-o', collapsed: 'arrow-right', empty: 'arrow-gray', leaf: 'dot' } } }, 'first collapsed property of cssClasses has higher priority': { treeModelA: { value: '12', settings: { cssClasses: { collapsed: 'arrow-right-o' } } }, treeModelB: { value: '42', settings: { cssClasses: { expanded: 'arrow-down', collapsed: 'arrow-right', empty: 'arrow-gray', leaf: 'dot' } } }, - result: { isCollapsedOnInit: false, static: false, leftMenu: false, rightMenu: true, cssClasses: { expanded: 'arrow-down', collapsed: 'arrow-right-o', empty: 'arrow-gray', leaf: 'dot' } } + result: { isCollapsedOnInit: false, static: false, leftMenu: false, rightMenu: true, checked: false, cssClasses: { expanded: 'arrow-down', collapsed: 'arrow-right-o', empty: 'arrow-gray', leaf: 'dot' } } }, 'first empty property of cssClasses has higher priority': { treeModelA: { value: '12', settings: { cssClasses: { empty: 'arrow-gray-o' } } }, treeModelB: { value: '42', settings: { cssClasses: { expanded: 'arrow-down', collapsed: 'arrow-right', empty: 'arrow-gray', leaf: 'dot' } } }, - result: { isCollapsedOnInit: false, static: false, leftMenu: false, rightMenu: true, cssClasses: { expanded: 'arrow-down', collapsed: 'arrow-right', empty: 'arrow-gray-o', leaf: 'dot' } } + result: { isCollapsedOnInit: false, static: false, leftMenu: false, rightMenu: true, checked: false, cssClasses: { expanded: 'arrow-down', collapsed: 'arrow-right', empty: 'arrow-gray-o', leaf: 'dot' } } }, 'first leaf property of cssClasses has higher priority': { treeModelA: { value: '12', settings: { cssClasses: { leaf: 'dot-o' } } }, treeModelB: { value: '42', settings: { cssClasses: { expanded: 'arrow-down', collapsed: 'arrow-right', empty: 'arrow-gray', leaf: 'dot' } } }, - result: { isCollapsedOnInit: false, static: false, leftMenu: false, rightMenu: true, cssClasses: { expanded: 'arrow-down', collapsed: 'arrow-right', empty: 'arrow-gray', leaf: 'dot-o' } } + result: { isCollapsedOnInit: false, static: false, leftMenu: false, rightMenu: true, checked: false, cssClasses: { expanded: 'arrow-down', collapsed: 'arrow-right', empty: 'arrow-gray', leaf: 'dot-o' } } }, 'first properties of cssClasses has higher priority': { treeModelA: { value: '12', settings: { cssClasses: { expanded: 'arrow-down-o', collapsed: 'arrow-right-o', empty: 'arrow-gray-o', leaf: 'dot-o' } } }, treeModelB: { value: '42', settings: { cssClasses: { expanded: 'arrow-down', collapsed: 'arrow-right', empty: 'arrow-gray', leaf: 'dot' } } }, - result: { isCollapsedOnInit: false, static: false, leftMenu: false, rightMenu: true, cssClasses: { expanded: 'arrow-down-o', collapsed: 'arrow-right-o', empty: 'arrow-gray-o', leaf: 'dot-o' } } + result: { isCollapsedOnInit: false, static: false, leftMenu: false, rightMenu: true, checked: false, cssClasses: { expanded: 'arrow-down-o', collapsed: 'arrow-right-o', empty: 'arrow-gray-o', leaf: 'dot-o' } } }, 'second properties of cssClasses in settings has priority, if first source doesn\'t have them': { treeModelA: { value: '42', settings: { static: true, leftMenu: true, rightMenu: false } }, treeModelB: { value: '12', settings: { cssClasses: { expanded: 'arrow-down-o', collapsed: 'arrow-right-o', empty: 'arrow-gray-o', leaf: 'dot-o' } } }, - result: { isCollapsedOnInit: false, static: true, leftMenu: true, rightMenu: false, cssClasses: { expanded: 'arrow-down-o', collapsed: 'arrow-right-o', empty: 'arrow-gray-o', leaf: 'dot-o' } } + result: { isCollapsedOnInit: false, static: true, leftMenu: true, rightMenu: false, checked: false, cssClasses: { expanded: 'arrow-down-o', collapsed: 'arrow-right-o', empty: 'arrow-gray-o', leaf: 'dot-o' } } }, 'first node property of templates has higher priority': { treeModelA: { value: '12', settings: { templates: { node: '' } } }, treeModelB: { value: '42', settings: { templates: { node: '', leaf: '', leftMenu: '' } } }, - result: { isCollapsedOnInit: false, static: false, leftMenu: false, rightMenu: true, templates: { node: '', leaf: '', leftMenu: '' } } + result: { isCollapsedOnInit: false, static: false, leftMenu: false, rightMenu: true, checked: false, templates: { node: '', leaf: '', leftMenu: '' } } }, 'first leaf property in templates has higher priority': { treeModelA: { value: '12', settings: { templates: { leaf: '' } } }, treeModelB: { value: '42', settings: { templates: { node: '', leaf: '', leftMenu: '' } } }, - result: { isCollapsedOnInit: false, static: false, leftMenu: false, rightMenu: true, templates: { node: '', leaf: '', leftMenu: '' } } + result: { isCollapsedOnInit: false, static: false, leftMenu: false, rightMenu: true, checked: false, templates: { node: '', leaf: '', leftMenu: '' } } }, 'first leftMenu property in templates has higher priority': { treeModelA: { value: '12', settings: { templates: { leftMenu: '' } } }, treeModelB: { value: '42', settings: { templates: { node: '', leaf: '', leftMenu: '' } } }, - result: { isCollapsedOnInit: false, static: false, leftMenu: false, rightMenu: true, templates: { node: '', leaf: '', leftMenu: '' } } + result: { isCollapsedOnInit: false, static: false, leftMenu: false, rightMenu: true, checked: false, templates: { node: '', leaf: '', leftMenu: '' } } }, 'first properties of templates has higher priority': { treeModelA: { value: '12', settings: { templates: { node: '', leaf: '', leftMenu: '' } } }, treeModelB: { value: '42', settings: { templates: { node: '', leaf: '', leftMenu: '' } } }, - result: { isCollapsedOnInit: false, static: false, leftMenu: false, rightMenu: true, templates: { node: '', leaf: '', leftMenu: '' } } + result: { isCollapsedOnInit: false, static: false, leftMenu: false, rightMenu: true, checked: false, templates: { node: '', leaf: '', leftMenu: '' } } }, 'second properties of templates in settings has priority, if first source doesn\'t have them': { treeModelA: { value: '42', settings: { static: true, leftMenu: true, rightMenu: false } }, treeModelB: { value: '12', settings: { templates: { node: '', leaf: '', leftMenu: '' } } }, - result: { isCollapsedOnInit: false, static: true, leftMenu: true, rightMenu: false, templates: { node: '', leaf: '', leftMenu: '' } } + result: { isCollapsedOnInit: false, static: true, leftMenu: true, rightMenu: false, checked: false, templates: { node: '', leaf: '', leftMenu: '' } } } }; } diff --git a/test/left-menu.tree-internal.component.spec.ts b/test/left-menu.tree-internal.component.spec.ts index ca49375d..f585f9e9 100644 --- a/test/left-menu.tree-internal.component.spec.ts +++ b/test/left-menu.tree-internal.component.spec.ts @@ -179,7 +179,7 @@ describe('LeftMenu-TreeInternalComponent', () => { expect(masterComponentInstance.isLeftMenuVisible).toEqual(true); expect(event.preventDefault).toHaveBeenCalled(); expect(nodeMenuService.hideMenuForAllNodesExcept).toHaveBeenCalledTimes(1); - expect(nodeMenuService.hideMenuForAllNodesExcept).toHaveBeenCalledWith(masterComponentInstance.element); + expect(nodeMenuService.hideMenuForAllNodesExcept).toHaveBeenCalledWith(masterComponentInstance.nodeElementRef); }); it('shouldn`t have a left menu on node and it`s child by default', () => { @@ -580,8 +580,8 @@ describe('LeftMenu-TreeInternalComponent', () => { expect(staticInternalTreeEl.componentInstance.tree.children[0].value).toEqual('Eyes'); expect(staticInternalTreeEl.componentInstance.tree.children[2].value).toEqual('Lips'); - const capturedNode = new CapturedNode(eyesEl.componentInstance.element, eyesEl.componentInstance.tree); - nodeDraggableService.fireNodeDragged(capturedNode, lipsEl.componentInstance.element); + const capturedNode = new CapturedNode(eyesEl.componentInstance.nodeElementRef, eyesEl.componentInstance.tree); + nodeDraggableService.fireNodeDragged(capturedNode, lipsEl.componentInstance.nodeElementRef); fixture.detectChanges(); @@ -615,8 +615,8 @@ describe('LeftMenu-TreeInternalComponent', () => { expect(staticInternalTreeEl.componentInstance.tree.children[1].children[0].value).toEqual('Eyelash'); expect(staticInternalTreeEl.componentInstance.tree.children[1].children[1].value).toEqual('Eyebow'); - const capturedNode = new CapturedNode(eyelashEl.componentInstance.element, eyelashEl.componentInstance.tree); - nodeDraggableService.fireNodeDragged(capturedNode, eyebowEl.componentInstance.element); + const capturedNode = new CapturedNode(eyelashEl.componentInstance.nodeElementRef, eyelashEl.componentInstance.tree); + nodeDraggableService.fireNodeDragged(capturedNode, eyebowEl.componentInstance.nodeElementRef); fixture.detectChanges(); diff --git a/test/right-menu.tree-internal.component.spec.ts b/test/right-menu.tree-internal.component.spec.ts index 9aeb1ee8..86937e2a 100644 --- a/test/right-menu.tree-internal.component.spec.ts +++ b/test/right-menu.tree-internal.component.spec.ts @@ -175,7 +175,7 @@ describe('RightMenu-TreeInternalComponent', () => { expect(masterComponentInstance.isRightMenuVisible).toEqual(true); expect(event.preventDefault).toHaveBeenCalled(); expect(nodeMenuService.hideMenuForAllNodesExcept).toHaveBeenCalledTimes(1); - expect(nodeMenuService.hideMenuForAllNodesExcept).toHaveBeenCalledWith(masterComponentInstance.element); + expect(nodeMenuService.hideMenuForAllNodesExcept).toHaveBeenCalledWith(masterComponentInstance.nodeElementRef); }); it('should show right menu on node by default', () => { @@ -575,8 +575,8 @@ describe('RightMenu-TreeInternalComponent', () => { expect(staticInternalTreeEl.componentInstance.tree.children[0].value).toEqual('Eyes'); expect(staticInternalTreeEl.componentInstance.tree.children[2].value).toEqual('Lips'); - const capturedNode = new CapturedNode(eyesEl.componentInstance.element, eyesEl.componentInstance.tree); - nodeDraggableService.fireNodeDragged(capturedNode, lipsEl.componentInstance.element); + const capturedNode = new CapturedNode(eyesEl.componentInstance.nodeElementRef, eyesEl.componentInstance.tree); + nodeDraggableService.fireNodeDragged(capturedNode, lipsEl.componentInstance.nodeElementRef); fixture.detectChanges(); @@ -611,8 +611,8 @@ describe('RightMenu-TreeInternalComponent', () => { expect(staticInternalTreeEl.componentInstance.tree.children[1].children[0].value).toEqual('Eyelash'); expect(staticInternalTreeEl.componentInstance.tree.children[1].children[1].value).toEqual('Eyebow'); - const capturedNode = new CapturedNode(eyelashEl.componentInstance.element, eyelashEl.componentInstance.tree); - nodeDraggableService.fireNodeDragged(capturedNode, eyebowEl.componentInstance.element); + const capturedNode = new CapturedNode(eyelashEl.componentInstance.nodeElementRef, eyelashEl.componentInstance.tree); + nodeDraggableService.fireNodeDragged(capturedNode, eyebowEl.componentInstance.nodeElementRef); fixture.detectChanges(); diff --git a/test/tree-controller.spec.ts b/test/tree-controller.spec.ts index 4ab26a81..a434e5c0 100644 --- a/test/tree-controller.spec.ts +++ b/test/tree-controller.spec.ts @@ -1,4 +1,4 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { Component, DebugElement, ElementRef, ViewChild } from '@angular/core'; import { TreeInternalComponent } from '../src/tree-internal.component'; @@ -14,6 +14,8 @@ import { NodeEditableDirective } from '../src/editable/node-editable.directive'; import { TreeStatus } from '../src/tree.types'; import * as EventUtils from '../src/utils/event.utils'; import { SafeHtmlPipe } from '../src/utils/safe-html.pipe'; +import { Ng2TreeSettings, Tree } from '../index'; +import { isEmpty } from '../src/utils/fn.utils'; let fixture: ComponentFixture; let lordTreeInstance: TreeComponent; @@ -52,15 +54,19 @@ const treeLord: TreeModel = { @Component({ template: ` -
+
` }) class TestComponent { + public settings = new Ng2TreeSettings(); public treeLord: TreeModel = treeLord; @ViewChild('lordTreeInstance') public lordTreeComponent; - public constructor(public treeHolder: ElementRef) { } + public constructor(public treeHolder: ElementRef) { + this.settings.enableCheckboxes = true; + this.settings.showCheckboxes = true; + } } describe('TreeController', () => { @@ -89,6 +95,72 @@ describe('TreeController', () => { expect(treeService.getController(lordInternalTreeInstance.tree.id)).toBeDefined(); }); + it('can check a node', () => { + const controller = treeService.getController(lordInternalTreeInstance.tree.id); + expect(controller.isChecked()).toBe(false); + + controller.check(); + + fixture.detectChanges(); + + expect(controller.isChecked()).toBe(true); + }); + + it('can uncheck a node', () => { + const controller = treeService.getController(lordInternalTreeInstance.tree.id); + expect(controller.isChecked()).toBe(false); + + controller.check(); + fixture.detectChanges(); + + controller.uncheck(); + fixture.detectChanges(); + + expect(controller.isChecked()).toBe(false); + }); + + it('checks all the children down the branch', () => { + const tree = lordInternalTreeInstance.tree; + const controller = treeService.getController(tree.id); + + controller.check(); + fixture.detectChanges(); + + const checkChildChecked = (children: Tree[], checked: boolean) => + isEmpty(children) ? checked : children.every(child => child.checked && checkChildChecked(child.children, child.checked)); + + expect(checkChildChecked(tree.children, tree.checked)).toBe(true, 'All the children should be checked'); + }); + + it('unchecks all the children down the branch', () => { + const tree = lordInternalTreeInstance.tree; + const controller = treeService.getController(tree.id); + + controller.check(); + fixture.detectChanges(); + + controller.uncheck(); + fixture.detectChanges(); + + const checkChildChecked = (children: Tree[], checked: boolean) => + isEmpty(children) ? checked : children.every(child => child.checked && checkChildChecked(child.children, child.checked)); + + expect(checkChildChecked(tree.children, tree.checked)).toBe(false, 'All the children should be unchecked'); + }); + + it('detects indetermined node', fakeAsync(() => { + const tree = lordInternalTreeInstance.tree; + const controller = treeService.getController(tree.id); + const childController = treeService.getController(tree.children[0].id); + + childController.check(); + fixture.detectChanges(); + tick(); + + expect(childController.isChecked()).toBe(true, 'Node should be checked'); + expect(controller.isIndetermined()).toBe(true, 'Node should be in indetermined state'); + })); + it('knows when node is selected', () => { const event = jasmine.createSpyObj('e', ['preventDefault']); event.button = EventUtils.MouseButtons.Left; diff --git a/test/tree-internal.component.spec.ts b/test/tree-internal.component.spec.ts index 70d5b775..9b00cf13 100644 --- a/test/tree-internal.component.spec.ts +++ b/test/tree-internal.component.spec.ts @@ -181,8 +181,8 @@ describe('TreeInternalComponent', () => { expect(masterInternalTreeEl.componentInstance.tree.children[0].value).toEqual('Servant#1'); expect(masterInternalTreeEl.componentInstance.tree.children[1].value).toEqual('Servant#2'); - const capturedNode = new CapturedNode(servant1InternalTreeEl.componentInstance.element, servant1InternalTreeEl.componentInstance.tree); - nodeDraggableService.fireNodeDragged(capturedNode, servant2InternalTreeEl.componentInstance.element); + const capturedNode = new CapturedNode(servant1InternalTreeEl.componentInstance.nodeElementRef, servant1InternalTreeEl.componentInstance.tree); + nodeDraggableService.fireNodeDragged(capturedNode, servant2InternalTreeEl.componentInstance.nodeElementRef); fixture.detectChanges(); @@ -211,8 +211,8 @@ describe('TreeInternalComponent', () => { expect(lordInternalTreeEl.componentInstance.tree.children[0].value).toEqual('Disciple#1'); expect(lordInternalTreeEl.componentInstance.tree.children[1].value).toEqual('Disciple#2'); - const capturedNode = new CapturedNode(disciple1InternalTreeEl.componentInstance.element, disciple1InternalTreeEl.componentInstance.tree); - nodeDraggableService.fireNodeDragged(capturedNode, disciple2InternalTreeEl.componentInstance.element); + const capturedNode = new CapturedNode(disciple1InternalTreeEl.componentInstance.nodeElementRef, disciple1InternalTreeEl.componentInstance.tree); + nodeDraggableService.fireNodeDragged(capturedNode, disciple2InternalTreeEl.componentInstance.nodeElementRef); fixture.detectChanges(); @@ -239,8 +239,8 @@ describe('TreeInternalComponent', () => { const internalTreeChildren = masterInternalTreeEl.queryAll(By.directive(TreeInternalComponent)); const servant2InternalTreeEl = internalTreeChildren[1]; - const capturedNode = new CapturedNode(masterComponentInstance.element, masterComponentInstance.tree); - nodeDraggableService.fireNodeDragged(capturedNode, servant2InternalTreeEl.componentInstance.element); + const capturedNode = new CapturedNode(masterComponentInstance.nodeElementRef, masterComponentInstance.tree); + nodeDraggableService.fireNodeDragged(capturedNode, servant2InternalTreeEl.componentInstance.nodeElementRef); fixture.detectChanges(); @@ -267,8 +267,8 @@ describe('TreeInternalComponent', () => { expect(subDisciple2InternalTreeEl.componentInstance.tree.value).toEqual('SubDisciple#2'); expect(disciple2InternalTreeEl.componentInstance.tree.value).toEqual('Disciple#2'); - const capturedNode = new CapturedNode(subDisciple1InternalTreeEl.componentInstance.element, subDisciple1InternalTreeEl.componentInstance.tree); - nodeDraggableService.fireNodeDragged(capturedNode, disciple2InternalTreeEl.componentInstance.element); + const capturedNode = new CapturedNode(subDisciple1InternalTreeEl.componentInstance.nodeElementRef, subDisciple1InternalTreeEl.componentInstance.tree); + nodeDraggableService.fireNodeDragged(capturedNode, disciple2InternalTreeEl.componentInstance.nodeElementRef); fixture.detectChanges(); @@ -298,8 +298,8 @@ describe('TreeInternalComponent', () => { const masterInternalTreeChildren = masterInternalTreeEl.queryAll(By.directive(TreeInternalComponent)); const servant1InternalTreeEl = masterInternalTreeChildren[0]; - const capturedNode = new CapturedNode(servant1InternalTreeEl.componentInstance.element, servant1InternalTreeEl.componentInstance.tree); - nodeDraggableService.fireNodeDragged(capturedNode, subDisciple1InternalTreeEl.componentInstance.element); + const capturedNode = new CapturedNode(servant1InternalTreeEl.componentInstance.nodeElementRef, servant1InternalTreeEl.componentInstance.tree); + nodeDraggableService.fireNodeDragged(capturedNode, subDisciple1InternalTreeEl.componentInstance.nodeElementRef); fixture.detectChanges(); @@ -335,8 +335,8 @@ describe('TreeInternalComponent', () => { const masterInternalTreeChildren = masterInternalTreeEl.queryAll(By.directive(TreeInternalComponent)); const servant1InternalTreeEl = masterInternalTreeChildren[0]; - const capturedNode = new CapturedNode(servant1InternalTreeEl.componentInstance.element, servant1InternalTreeEl.componentInstance.tree); - nodeDraggableService.fireNodeDragged(capturedNode, disciple1InternalTreeEl.componentInstance.element); + const capturedNode = new CapturedNode(servant1InternalTreeEl.componentInstance.nodeElementRef, servant1InternalTreeEl.componentInstance.tree); + nodeDraggableService.fireNodeDragged(capturedNode, disciple1InternalTreeEl.componentInstance.nodeElementRef); fixture.detectChanges(); @@ -473,8 +473,8 @@ describe('TreeInternalComponent', () => { expect(lipsEl.componentInstance.tree.value).toEqual('Lips'); expect(lipsEl.componentInstance.tree.positionInParent).toEqual(1); - const capturedNode = new CapturedNode(eyesEl.componentInstance.element, eyesEl.componentInstance.tree); - nodeDraggableService.fireNodeDragged(capturedNode, lipsEl.componentInstance.element); + const capturedNode = new CapturedNode(eyesEl.componentInstance.nodeElementRef, eyesEl.componentInstance.tree); + nodeDraggableService.fireNodeDragged(capturedNode, lipsEl.componentInstance.nodeElementRef); fixture.detectChanges(); diff --git a/test/tree.spec.ts b/test/tree.spec.ts index 30a68a95..68257a39 100644 --- a/test/tree.spec.ts +++ b/test/tree.spec.ts @@ -1140,13 +1140,14 @@ describe('Tree', () => { isCollapsedOnInit: true, static: false, leftMenu: false, - rightMenu: true + rightMenu: true, + checked: true }, children: [ { value: 'child#1', emitLoadNextLevel: false, - settings: { isCollapsedOnInit: true, static: false, leftMenu: false, rightMenu: true } } + settings: { isCollapsedOnInit: true, static: false, leftMenu: false, rightMenu: true, checked: true } } ] };