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 e335df66..0a64eb94 100644 --- a/src/demo/app/app.component.ts +++ b/src/demo/app/app.component.ts @@ -31,7 +31,7 @@ declare const alertify: any;
+ (nodeCollapsed)="onNodeCollapsed($event)" + [settings]="settings">
@@ -79,6 +80,9 @@ declare const alertify: any; + +
@@ -175,9 +179,17 @@ declare const alertify: any; }) export class AppComponent implements OnInit { public settings: Ng2TreeSettings = { - rootIsVisible: false + rootIsVisible: false, + showCheckboxes: true, + }; + + public disabledCheckboxesSettings: Ng2TreeSettings = { + rootIsVisible: false, + showCheckboxes: true, + enableCheckboxes: false }; + public fonts: TreeModel = { value: 'Fonts', children: [ @@ -188,18 +200,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 } ] } ] @@ -215,29 +227,29 @@ export class AppComponent implements OnInit { ] }, 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); } @@ -253,6 +265,7 @@ export class AppComponent implements OnInit { value: '/', id: 1, settings: { + cssClasses: { expanded: 'fa fa-caret-down', collapsed: 'fa fa-caret-right', @@ -269,6 +282,7 @@ export class AppComponent implements OnInit { value: 'bin', id: 2, children: [ + {value: 'bash', id: 3}, {value: 'umount', id: 4}, {value: 'cp', id: 5}, @@ -292,29 +306,32 @@ 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 } ] }, { 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}, - {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 } ] }, { @@ -348,8 +365,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, @@ -357,10 +374,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: [] } ]); }); } @@ -385,38 +402,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: [] } ] }, { @@ -426,7 +443,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, @@ -436,33 +453,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; @@ -475,31 +492,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 { @@ -515,8 +532,8 @@ export class AppComponent implements OnInit { { value: 'Aspect-oriented programming', children: [ - {value: 'AspectJ'}, - {value: 'AspectC++'} + { value: 'AspectJ' }, + { value: 'AspectC++' } ] }, { @@ -533,16 +550,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' } ] } ] @@ -611,11 +628,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); @@ -631,4 +648,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/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 86fbe530..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,6 +93,21 @@ export class TreeController { public startRenaming(): void { this.tree.markAsBeingRenamed(); + } + + public check(): void { + this.component.onNodeChecked(); + } + + 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 874c73fe..4e582945 100644 --- a/src/tree-internal.component.ts +++ b/src/tree-internal.component.ts @@ -6,19 +6,25 @@ import { OnDestroy, OnInit, SimpleChanges, - TemplateRef + TemplateRef, + ViewChild, + AfterViewInit, + AfterContentChecked } 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 { 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 } from './utils/fn.utils'; +import { get, has, size, isNil } from './utils/fn.utils'; +import { Ng2TreeSettings } from './tree.types'; @Component({ selector: 'tree-internal', @@ -29,10 +35,15 @@ import { get } from './utils/fn.utils'; [ngClass]="{rootless: isRootHidden()}" [class.selected]="isSelected" (contextmenu)="showRightMenu($event)" - [nodeDraggable]="element" + [nodeDraggable]="nodeElementRef" [tree]="tree">
+ +
+ +
+
- + ` }) -export class TreeInternalComponent implements OnInit, OnChanges, OnDestroy { +export class TreeInternalComponent implements OnInit, OnChanges, OnDestroy, AfterContentChecked { @Input() public tree: Tree; @@ -83,23 +94,39 @@ export class TreeInternalComponent implements OnInit, OnChanges, OnDestroy { public isSelected = false; public isRightMenuVisible = false; public isLeftMenuVisible = false; + public isReadOnly = false; public controller: TreeController; + @ViewChild('checkbox') + public checkboxElementRef: ElementRef; + private subscriptions: Subscription[] = []; public constructor(private nodeMenuService: NodeMenuService, public treeService: TreeService, - public element: ElementRef) { + public nodeElementRef: ElementRef) { + } + + 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 { - this.controller = new TreeController(this); - if (get(this.tree, 'node.id', '')) { - this.treeService.setController(this.tree.node.id, this.controller); + 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 }; - this.subscriptions.push(this.nodeMenuService.hideMenuStream(this.element) + this.settings = this.settings || new Ng2TreeSettings(); + this.isReadOnly = !get(this.settings, 'enableCheckboxes', true); + + this.subscriptions.push(this.nodeMenuService.hideMenuStream(this.nodeElementRef) .subscribe(() => { this.isRightMenuVisible = false; this.isLeftMenuVisible = false; @@ -108,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); @@ -118,6 +145,12 @@ export class TreeInternalComponent implements OnInit, OnChanges, OnDestroy { this.moveNodeToParentTreeAndRemoveFromPreviousOne(e, this.tree); } })); + + 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.updateCheckboxState(); + })); } public ngOnChanges(changes: SimpleChanges): void { @@ -163,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(); } @@ -175,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(); } @@ -256,4 +289,76 @@ export class TreeInternalComponent implements OnInit, OnChanges, OnDestroy { public hasCustomMenu(): boolean { return this.tree.hasCustomMenu(); } + + public switchNodeCheckStatus() { + if (!this.tree.checked) { + this.onNodeChecked(); + } else { + this.onNodeUnchecked(); + } + } + + public onNodeChecked(): void { + if (!this.checkboxElementRef) { + return; + } + + this.checkboxElementRef.nativeElement.indeterminate = false; + this.treeService.fireNodeChecked(this.tree); + this.executeOnChildController(controller => controller.check()); + this.tree.checked = true; + } + + public onNodeUnchecked(): void { + if (!this.checkboxElementRef) { + return; + } + + this.checkboxElementRef.nativeElement.indeterminate = false; + this.treeService.fireNodeUnchecked(this.tree); + this.executeOnChildController(controller => controller.uncheck()); + this.tree.checked = false; + } + + private executeOnChildController(executor: (controller: TreeController) => void) { + if (this.tree.hasLoadedChildern()) { + this.tree.children.forEach((child: Tree) => { + const controller = this.treeService.getController(child.id); + if (!isNil(controller)) { + executor(controller); + } + }); + } + } + + updateCheckboxState(): void { + if (!this.checkboxElementRef) { + return; + } + + // 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.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 c5540830..f4319f3a 100644 --- a/src/tree.component.ts +++ b/src/tree.component.ts @@ -4,7 +4,9 @@ import { } from '@angular/core'; import { TreeService } from './tree.service'; import * as TreeTypes from './tree.types'; -import { NodeEvent, MenuItemSelectedEvent } from './tree.events'; + +import { NodeEvent, NodeCheckedEvent, NodeUncheckedEvent, MenuItemSelectedEvent } from './tree.events'; + import { Tree } from './tree'; import { TreeController } from './tree-controller'; import { Subscription } from 'rxjs/Subscription'; @@ -15,7 +17,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') @@ -47,11 +49,17 @@ export class TreeComponent implements OnInit, OnChanges, OnDestroy { public nodeCollapsed: EventEmitter = new EventEmitter(); @Output() - public menuItemSelected: EventEmitter = new EventEmitter(); - @Output() public loadNextLevel: EventEmitter = new EventEmitter(); + @Output() + public nodeChecked: EventEmitter = new EventEmitter(); + + @Output() + public nodeUnchecked: EventEmitter = new EventEmitter(); + + public menuItemSelected: EventEmitter = new EventEmitter(); + public tree: Tree; @ViewChild('rootComponent') public rootComponent; @@ -59,7 +67,7 @@ export class TreeComponent implements OnInit, OnChanges, OnDestroy { private subscriptions: Subscription[] = []; - public constructor(@Inject(TreeService) private treeService: TreeService) { + public constructor( @Inject(TreeService) private treeService: TreeService) { } public ngOnChanges(changes: SimpleChanges): void { @@ -106,6 +114,14 @@ export class TreeComponent implements OnInit, OnChanges, OnDestroy { 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.nodeUnchecked.emit(e); + })); } public getController(): TreeController { diff --git a/src/tree.events.ts b/src/tree.events.ts index 63b3e73f..3df84d83 100644 --- a/src/tree.events.ts +++ b/src/tree.events.ts @@ -66,3 +66,21 @@ 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); + } +} + +export class NodeIndeterminedEvent extends NodeEvent { + public constructor(node: Tree) { + super(node); + } +} diff --git a/src/tree.service.ts b/src/tree.service.ts index 44751ba9..e3bd83f0 100644 --- a/src/tree.service.ts +++ b/src/tree.service.ts @@ -6,8 +6,11 @@ import { NodeRemovedEvent, NodeRenamedEvent, NodeSelectedEvent, + LoadNextLevelEvent, + NodeCheckedEvent, + NodeUncheckedEvent, MenuItemSelectedEvent, - LoadNextLevelEvent + NodeIndeterminedEvent } from './tree.events'; import { RenamableNode } from './tree.types'; import { Tree } from './tree'; @@ -30,6 +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 nodeIndetermined$: Subject = new Subject(); private controllers: Map = new Map(); @@ -88,6 +94,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) @@ -117,16 +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 fireNodeIndetermined(tree: Tree): void { + this.nodeIndetermined$.next(new NodeIndeterminedEvent(tree)); } } diff --git a/src/tree.ts b/src/tree.ts index 780f4952..efb9236f 100644 --- a/src/tree.ts +++ b/src/tree.ts @@ -93,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; @@ -226,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 3633afeb..f21ba887 100644 --- a/src/tree.types.ts +++ b/src/tree.types.ts @@ -94,23 +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; + 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 } } ] };