From 2f967c3e6dba7afb1b4fb5271bd9b3d10c18e395 Mon Sep 17 00:00:00 2001 From: Georgiy Rychko Date: Sun, 18 Feb 2018 20:14:06 +0200 Subject: [PATCH] feat(selection): add ability to allow and forbid a node selection (closes #220) --- src/demo/app/app.component.ts | 5 ++ src/tree-controller.ts | 12 +++++ src/tree-internal.component.ts | 4 ++ src/tree.ts | 9 ++++ src/tree.types.ts | 14 +++-- src/utils/fn.utils.ts | 10 +++- test/data-provider/tree.data-provider.ts | 65 +++++++++++++++++++--- test/tree-controller.spec.ts | 26 +++++++++ test/tree.spec.ts | 68 +++++++++++++++++++++++- 9 files changed, 199 insertions(+), 14 deletions(-) diff --git a/src/demo/app/app.component.ts b/src/demo/app/app.component.ts index b2214cd2..b2841364 100644 --- a/src/demo/app/app.component.ts +++ b/src/demo/app/app.component.ts @@ -66,6 +66,8 @@ declare const alertify: any;

Tree API exposed via TreeController

+ + @@ -310,6 +312,9 @@ export class AppComponent implements OnInit { { value: 'boot', id: 13, + settings: { + selectionAllowed: false + }, children: [ { value: 'grub', diff --git a/src/tree-controller.ts b/src/tree-controller.ts index ff948040..b309204e 100644 --- a/src/tree-controller.ts +++ b/src/tree-controller.ts @@ -128,4 +128,16 @@ export class TreeController { public isIndetermined(): boolean { return get(this.component, 'checkboxElementRef.nativeElement.indeterminate'); } + + public allowSelection() { + this.tree.selectionAllowed = true; + } + + public forbidSelection() { + this.tree.selectionAllowed = false; + } + + public isSelectionAllowed(): boolean { + return this.tree.selectionAllowed; + } } diff --git a/src/tree-internal.component.ts b/src/tree-internal.component.ts index b20094c4..91d49f4e 100644 --- a/src/tree-internal.component.ts +++ b/src/tree-internal.component.ts @@ -183,6 +183,10 @@ export class TreeInternalComponent implements OnInit, OnChanges, OnDestroy, Afte } public onNodeSelected(e: { button: number }): void { + if (!this.tree.selectionAllowed) { + return; + } + if (EventUtils.isLeftButtonClicked(e as MouseEvent)) { this.isSelected = true; this.treeService.fireNodeSelected(this.tree); diff --git a/src/tree.ts b/src/tree.ts index c052416f..6019ed43 100644 --- a/src/tree.ts +++ b/src/tree.ts @@ -240,6 +240,15 @@ export class Tree { return this.hasLoadedChildern() ? this.children.filter(child => child.checked) : []; } + public set selectionAllowed(selectionAllowed: boolean) { + this.node.settings = Object.assign({}, this.node.settings, { selectionAllowed }); + } + + public get selectionAllowed(): boolean { + const value = get(this.node.settings, 'selectionAllowed'); + return isNil(value) ? true : !!value; + } + hasLoadedChildern() { return !isEmpty(this.children); } diff --git a/src/tree.types.ts b/src/tree.types.ts index 68ac5192..a1c1b823 100644 --- a/src/tree.types.ts +++ b/src/tree.types.ts @@ -1,4 +1,4 @@ -import { defaultsDeep, get } from './utils/fn.utils'; +import { defaultsDeep, get, omit } from './utils/fn.utils'; import { NodeMenuItem } from './menu/node-menu.component'; export class FoldingType { @@ -95,13 +95,19 @@ export class TreeModelSettings { public checked?: boolean; - public static merge(sourceA: TreeModel, sourceB: TreeModel): TreeModelSettings { - return defaultsDeep({}, get(sourceA, 'settings'), get(sourceB, 'settings'), { + public selectionAllowed?: boolean; + + public static readonly NOT_CASCADING_SETTINGS = ['selectionAllowed']; + + public static merge(child: TreeModel, parent: TreeModel): TreeModelSettings { + const parentCascadingSettings = omit(get(parent, 'settings'), TreeModelSettings.NOT_CASCADING_SETTINGS); + return defaultsDeep({}, get(child, 'settings'), parentCascadingSettings, { static: false, leftMenu: false, rightMenu: true, isCollapsedOnInit: false, - checked: false + checked: false, + selectionAllowed: true }); } } diff --git a/src/utils/fn.utils.ts b/src/utils/fn.utils.ts index 37d50dca..cd8b0891 100644 --- a/src/utils/fn.utils.ts +++ b/src/utils/fn.utils.ts @@ -36,9 +36,15 @@ export function get(value: any, path: string, defaultValue?: any) { return isNil(result) || result === value ? defaultValue : result; } -export function omit(value: any, propToSkip: string): any { +export function omit(value: any, propsToSkip: string | string[]): any { + if (!value) { + return value; + } + + const normalizedPropsToSkip = typeof propsToSkip === 'string' ? [propsToSkip] : propsToSkip; + return Object.keys(value).reduce((result, prop) => { - if (prop === propToSkip) { + if (includes(normalizedPropsToSkip, prop)) { return result; } return Object.assign(result, { [prop]: value[prop] }); diff --git a/test/data-provider/tree.data-provider.ts b/test/data-provider/tree.data-provider.ts index 28a5b2f5..08fd162f 100644 --- a/test/data-provider/tree.data-provider.ts +++ b/test/data-provider/tree.data-provider.ts @@ -3,26 +3,68 @@ export class TreeDataProvider { 'default values': { treeModelA: { value: '42' }, treeModelB: { value: '12' }, - result: { static: false, leftMenu: false, rightMenu: true, isCollapsedOnInit: false, checked: false } + result: { + static: false, + leftMenu: false, + rightMenu: true, + isCollapsedOnInit: false, + checked: false, + selectionAllowed: true + } }, 'first settings source has higher priority': { treeModelA: { value: '42', - settings: { static: true, leftMenu: true, rightMenu: true, isCollapsedOnInit: true, checked: true } + settings: { + static: true, + leftMenu: true, + rightMenu: true, + isCollapsedOnInit: true, + checked: true, + selectionAllowed: false + } }, treeModelB: { value: '12', - settings: { static: false, leftMenu: false, rightMenu: false, isCollapsedOnInit: false, checked: false } + settings: { + static: false, + leftMenu: false, + rightMenu: false, + isCollapsedOnInit: false, + checked: false, + selectionAllowed: true + } }, - result: { static: true, leftMenu: true, rightMenu: true, isCollapsedOnInit: true, checked: true } + result: { + static: true, + leftMenu: true, + rightMenu: true, + isCollapsedOnInit: true, + checked: true, + selectionAllowed: false + } }, 'second settings source has priority if first settings source does not have the option': { treeModelA: { value: '42' }, treeModelB: { value: '12', - settings: { static: true, leftMenu: true, rightMenu: false, isCollapsedOnInit: true, checked: true } + settings: { + static: true, + leftMenu: true, + rightMenu: false, + isCollapsedOnInit: true, + checked: true, + selectionAllowed: false + } }, - result: { static: true, leftMenu: true, rightMenu: false, isCollapsedOnInit: true, checked: true } + result: { + static: true, + leftMenu: true, + rightMenu: false, + isCollapsedOnInit: true, + checked: true, + selectionAllowed: true + } }, 'first expanded property of cssClasses has higher priority': { treeModelA: { value: '12', settings: { cssClasses: { expanded: 'arrow-down-o' } } }, @@ -36,6 +78,7 @@ export class TreeDataProvider { leftMenu: false, rightMenu: true, checked: false, + selectionAllowed: true, cssClasses: { expanded: 'arrow-down-o', collapsed: 'arrow-right', empty: 'arrow-gray', leaf: 'dot' } } }, @@ -51,6 +94,7 @@ export class TreeDataProvider { leftMenu: false, rightMenu: true, checked: false, + selectionAllowed: true, cssClasses: { expanded: 'arrow-down', collapsed: 'arrow-right-o', empty: 'arrow-gray', leaf: 'dot' } } }, @@ -66,6 +110,7 @@ export class TreeDataProvider { leftMenu: false, rightMenu: true, checked: false, + selectionAllowed: true, cssClasses: { expanded: 'arrow-down', collapsed: 'arrow-right', empty: 'arrow-gray-o', leaf: 'dot' } } }, @@ -81,6 +126,7 @@ export class TreeDataProvider { leftMenu: false, rightMenu: true, checked: false, + selectionAllowed: true, cssClasses: { expanded: 'arrow-down', collapsed: 'arrow-right', empty: 'arrow-gray', leaf: 'dot-o' } } }, @@ -101,6 +147,7 @@ export class TreeDataProvider { leftMenu: false, rightMenu: true, checked: false, + selectionAllowed: true, cssClasses: { expanded: 'arrow-down-o', collapsed: 'arrow-right-o', empty: 'arrow-gray-o', leaf: 'dot-o' } } }, @@ -118,6 +165,7 @@ export class TreeDataProvider { leftMenu: true, rightMenu: false, checked: false, + selectionAllowed: true, cssClasses: { expanded: 'arrow-down-o', collapsed: 'arrow-right-o', empty: 'arrow-gray-o', leaf: 'dot-o' } } }, @@ -139,6 +187,7 @@ export class TreeDataProvider { leftMenu: false, rightMenu: true, checked: false, + selectionAllowed: true, templates: { node: '', leaf: '', @@ -164,6 +213,7 @@ export class TreeDataProvider { leftMenu: false, rightMenu: true, checked: false, + selectionAllowed: true, templates: { node: '', leaf: '', @@ -189,6 +239,7 @@ export class TreeDataProvider { leftMenu: false, rightMenu: true, checked: false, + selectionAllowed: true, templates: { node: '', leaf: '', @@ -223,6 +274,7 @@ export class TreeDataProvider { leftMenu: false, rightMenu: true, checked: false, + selectionAllowed: true, templates: { node: '', leaf: '', @@ -248,6 +300,7 @@ export class TreeDataProvider { leftMenu: true, rightMenu: false, checked: false, + selectionAllowed: true, templates: { node: '', leaf: '', diff --git a/test/tree-controller.spec.ts b/test/tree-controller.spec.ts index 22440057..6d3d4ad2 100644 --- a/test/tree-controller.spec.ts +++ b/test/tree-controller.spec.ts @@ -121,6 +121,32 @@ describe('TreeController', () => { expect(controller.isChecked()).toBe(false); }); + it('forbids selection', () => { + const controller = treeService.getController(lordInternalTreeInstance.tree.id); + expect(controller.isSelectionAllowed()).toBe(true); + + controller.forbidSelection(); + + fixture.detectChanges(); + + expect(controller.isSelectionAllowed()).toBe(false); + }); + + it('allows selection', () => { + const controller = treeService.getController(lordInternalTreeInstance.tree.id); + expect(controller.isSelectionAllowed()).toBe(true); + + controller.forbidSelection(); + fixture.detectChanges(); + + expect(controller.isSelectionAllowed()).toBe(false); + + controller.allowSelection(); + fixture.detectChanges(); + + expect(controller.isSelectionAllowed()).toBe(true); + }); + it('checks all the children down the branch', () => { const tree = lordInternalTreeInstance.tree; const controller = treeService.getController(tree.id); diff --git a/test/tree.spec.ts b/test/tree.spec.ts index 967be897..1e7ab4fd 100644 --- a/test/tree.spec.ts +++ b/test/tree.spec.ts @@ -1047,13 +1047,21 @@ describe('Tree', () => { static: false, leftMenu: false, rightMenu: true, - checked: true + checked: true, + selectionAllowed: true }, children: [ { value: 'child#1', emitLoadNextLevel: false, - settings: { isCollapsedOnInit: true, static: false, leftMenu: false, rightMenu: true, checked: true } + settings: { + isCollapsedOnInit: true, + static: false, + leftMenu: false, + rightMenu: true, + checked: true, + selectionAllowed: true + } } ] }; @@ -1063,6 +1071,62 @@ describe('Tree', () => { expect(tree.toTreeModel()).toEqual(model); }); + it('has selection allowed by default', () => { + const model: TreeModel = { + id: 6, + value: 'root' + }; + + const tree: Tree = new Tree(model); + + expect(tree.selectionAllowed).toBe(true); + }); + + it('can forbid selection', () => { + const model: TreeModel = { + id: 6, + value: 'root' + }; + + const tree: Tree = new Tree(model); + tree.selectionAllowed = false; + + expect(tree.selectionAllowed).toBe(false); + }); + + it('can allow selection', () => { + const model: TreeModel = { + id: 6, + value: 'root', + settings: { + selectionAllowed: false + } + }; + + const tree: Tree = new Tree(model); + + expect(tree.selectionAllowed).toBe(false); + + tree.selectionAllowed = true; + expect(tree.selectionAllowed).toBe(true); + }); + + it('does not cascade selectionAllowed setting', () => { + const model: TreeModel = { + id: 6, + value: 'root', + settings: { + selectionAllowed: false + }, + children: [{ value: 'foo' }] + }; + + const tree: Tree = new Tree(model); + + expect(tree.selectionAllowed).toBe(false); + expect(tree.children[0].selectionAllowed).toBe(true); + }); + it('has an access to menu items', () => { const model: TreeModel = { id: 42,