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;
@@ -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 } }
]
};