diff --git a/projects/igniteui-angular/src/lib/banner/banner.component.ts b/projects/igniteui-angular/src/lib/banner/banner.component.ts index c1196c0156f..972a20da04c 100644 --- a/projects/igniteui-angular/src/lib/banner/banner.component.ts +++ b/projects/igniteui-angular/src/lib/banner/banner.component.ts @@ -1,7 +1,6 @@ import { Component, NgModule, EventEmitter, Output, Input, ViewChild, ElementRef, ContentChild, HostBinding } from '@angular/core'; import { IgxExpansionPanelModule } from '../expansion-panel/expansion-panel.module'; -import { AnimationSettings } from '../expansion-panel/expansion-panel.component'; import { IgxExpansionPanelComponent } from '../expansion-panel/public_api'; import { IgxIconModule, IgxIconComponent } from '../icon/public_api'; import { IToggleView } from '../core/navigation'; @@ -10,6 +9,7 @@ import { IgxRippleModule } from '../directives/ripple/ripple.directive'; import { IgxBannerActionsDirective } from './banner.directives'; import { CommonModule } from '@angular/common'; import { CancelableEventArgs, IBaseEventArgs } from '../core/utils'; +import { ToggleAnimationSettings } from '../expansion-panel/toggle-animation-component'; export interface BannerEventArgs extends IBaseEventArgs { banner: IgxBannerComponent; @@ -111,11 +111,11 @@ export class IgxBannerComponent implements IToggleView { /** * Get the animation settings used by the banner open/close methods * ```typescript - * let currentAnimations: AnimationSettings = banner.animationSettings + * let currentAnimations: ToggleAnimationSettings = banner.animationSettings * ``` */ @Input() - public get animationSettings(): AnimationSettings { + public get animationSettings(): ToggleAnimationSettings { return this._animationSettings ? this._animationSettings : this._expansionPanel.animationSettings; } @@ -124,10 +124,10 @@ export class IgxBannerComponent implements IToggleView { * ```typescript * import { slideInLeft, slideOutRight } from 'igniteui-angular'; * ... - * banner.animationSettings: AnimationSettings = { openAnimation: slideInLeft, closeAnimation: slideOutRight }; + * banner.animationSettings: ToggleAnimationSettings = { openAnimation: slideInLeft, closeAnimation: slideOutRight }; * ``` */ - public set animationSettings(settings: AnimationSettings) { + public set animationSettings(settings: ToggleAnimationSettings) { this._animationSettings = settings; } /** @@ -166,7 +166,7 @@ export class IgxBannerComponent implements IToggleView { private _bannerActionTemplate: IgxBannerActionsDirective; private _bannerEvent: BannerEventArgs; - private _animationSettings: AnimationSettings; + private _animationSettings: ToggleAnimationSettings; constructor(public elementRef: ElementRef) { } diff --git a/projects/igniteui-angular/src/lib/expansion-panel/expansion-panel.component.ts b/projects/igniteui-angular/src/lib/expansion-panel/expansion-panel.component.ts index 6324a084c8e..14531e43ff2 100644 --- a/projects/igniteui-angular/src/lib/expansion-panel/expansion-panel.component.ts +++ b/projects/igniteui-angular/src/lib/expansion-panel/expansion-panel.component.ts @@ -8,24 +8,20 @@ import { ContentChild, AfterContentInit } from '@angular/core'; -import { AnimationBuilder, AnimationReferenceMetadata, useAnimation } from '@angular/animations'; -import { growVerOut, growVerIn } from '../animations/main'; +import { AnimationBuilder } from '@angular/animations'; import { IgxExpansionPanelBodyComponent } from './expansion-panel-body.component'; import { IgxExpansionPanelHeaderComponent } from './expansion-panel-header.component'; import { IGX_EXPANSION_PANEL_COMPONENT, IgxExpansionPanelBase, IExpansionPanelEventArgs } from './expansion-panel.common'; +import { ToggleAnimationPlayer, ToggleAnimationSettings } from './toggle-animation-component'; let NEXT_ID = 0; -export interface AnimationSettings { - openAnimation: AnimationReferenceMetadata; - closeAnimation: AnimationReferenceMetadata; -} @Component({ selector: 'igx-expansion-panel', templateUrl: 'expansion-panel.component.html', providers: [{ provide: IGX_EXPANSION_PANEL_COMPONENT, useExisting: IgxExpansionPanelComponent }] }) -export class IgxExpansionPanelComponent implements IgxExpansionPanelBase, AfterContentInit { +export class IgxExpansionPanelComponent extends ToggleAnimationPlayer implements IgxExpansionPanelBase, AfterContentInit { /** * Sets/gets the animation settings of the expansion panel component * Open and Close animation should be passed @@ -58,10 +54,12 @@ export class IgxExpansionPanelComponent implements IgxExpansionPanelBase, AfterC * ``` */ @Input() - public animationSettings: AnimationSettings = { - openAnimation: growVerIn, - closeAnimation: growVerOut - }; + public get animationSettings(): ToggleAnimationSettings { + return this._animationSettings; + } + public set animationSettings(value: ToggleAnimationSettings) { + this._animationSettings = value; + } /** * Sets/gets the `id` of the expansion panel component. @@ -165,10 +163,12 @@ export class IgxExpansionPanelComponent implements IgxExpansionPanelBase, AfterC private _collapsed = true; - constructor(private cdr: ChangeDetectorRef, private builder: AnimationBuilder) { } + constructor(private cdr: ChangeDetectorRef, protected builder: AnimationBuilder) { + super(builder); + } /** @hidden */ - ngAfterContentInit(): void { + public ngAfterContentInit(): void { if (this.body && this.header) { // schedule at end of turn: Promise.resolve().then(() => { @@ -188,11 +188,12 @@ export class IgxExpansionPanelComponent implements IgxExpansionPanelBase, AfterC * * ``` */ - collapse(evt?: Event) { + public collapse(evt?: Event) { if (this.collapsed) { // If expansion panel is already collapsed, do nothing return; } this.playCloseAnimation( + this.body?.element, () => { this.onCollapsed.emit({ event: evt, panel: this, owner: this }); this.collapsed = true; @@ -211,13 +212,14 @@ export class IgxExpansionPanelComponent implements IgxExpansionPanelBase, AfterC * * ``` */ - expand(evt?: Event) { + public expand(evt?: Event) { if (!this.collapsed) { // If the panel is already opened, do nothing return; } this.collapsed = false; this.cdr.detectChanges(); this.playOpenAnimation( + this.body?.element, () => { this.onExpanded.emit({ event: evt, panel: this, owner: this }); } @@ -234,7 +236,7 @@ export class IgxExpansionPanelComponent implements IgxExpansionPanelBase, AfterC * * ``` */ - toggle(evt?: Event) { + public toggle(evt?: Event) { if (this.collapsed) { this.open(evt); } else { @@ -242,41 +244,11 @@ export class IgxExpansionPanelComponent implements IgxExpansionPanelBase, AfterC } } - open(evt?: Event) { + public open(evt?: Event) { this.expand(evt); } - close(evt?: Event) { - this.collapse(evt); - } - - private playOpenAnimation(cb: () => void) { - if (!this.body) { // if not body element is passed, there is nothing to animate - return; - } - const animation = useAnimation(this.animationSettings.openAnimation); - const animationBuilder = this.builder.build(animation); - const openAnimationPlayer = animationBuilder.create(this.body.element.nativeElement); - - openAnimationPlayer.onDone(() => { - cb(); - openAnimationPlayer.reset(); - }); - - openAnimationPlayer.play(); - } - private playCloseAnimation(cb: () => void) { - if (!this.body) { // if not body element is passed, there is nothing to animate - return; - } - const animation = useAnimation(this.animationSettings.closeAnimation); - const animationBuilder = this.builder.build(animation); - const closeAnimationPlayer = animationBuilder.create(this.body.element.nativeElement); - closeAnimationPlayer.onDone(() => { - cb(); - closeAnimationPlayer.reset(); - }); - - closeAnimationPlayer.play(); + public close(evt?: Event) { + this.collapse(evt); } } diff --git a/projects/igniteui-angular/src/lib/expansion-panel/toggle-animation-component.ts b/projects/igniteui-angular/src/lib/expansion-panel/toggle-animation-component.ts new file mode 100644 index 00000000000..9bd90a01f82 --- /dev/null +++ b/projects/igniteui-angular/src/lib/expansion-panel/toggle-animation-component.ts @@ -0,0 +1,179 @@ +import { AnimationBuilder, AnimationPlayer, AnimationReferenceMetadata, useAnimation } from '@angular/animations'; +import { Directive, ElementRef, EventEmitter, OnDestroy } from '@angular/core'; +import { noop, Subject } from 'rxjs'; +import { growVerIn, growVerOut } from '../animations/grow'; + +/**@hidden @internal */ +export interface ToggleAnimationSettings { + openAnimation: AnimationReferenceMetadata; + closeAnimation: AnimationReferenceMetadata; +} + +export interface ToggleAnimationOwner { + animationSettings: ToggleAnimationSettings; + openAnimationStart: EventEmitter; + openAnimationDone: EventEmitter; + closeAnimationStart: EventEmitter; + closeAnimationDone: EventEmitter; + openAnimationPlayer: AnimationPlayer; + closeAnimationPlayer: AnimationPlayer; + playOpenAnimation(element: ElementRef, onDone: () => void): void; + playCloseAnimation(element: ElementRef, onDone: () => void): void; +} + +enum ANIMATION_TYPE { + OPEN = 'open', + CLOSE = 'close', +} + +/**@hidden @internal */ +@Directive() +// eslint-disable-next-line @angular-eslint/directive-class-suffix +export abstract class ToggleAnimationPlayer implements ToggleAnimationOwner, OnDestroy { + + + /** @hidden @internal */ + public openAnimationDone: EventEmitter = new EventEmitter(); + /** @hidden @internal */ + public closeAnimationDone: EventEmitter = new EventEmitter(); + /** @hidden @internal */ + public openAnimationStart: EventEmitter = new EventEmitter(); + /** @hidden @internal */ + public closeAnimationStart: EventEmitter = new EventEmitter(); + + public get animationSettings(): ToggleAnimationSettings { + return this._animationSettings; + } + public set animationSettings(value: ToggleAnimationSettings) { + this._animationSettings = value; + } + + /** @hidden @internal */ + public openAnimationPlayer: AnimationPlayer = null; + + /** @hidden @internal */ + public closeAnimationPlayer: AnimationPlayer = null; + + + + protected destroy$: Subject = new Subject(); + protected players: Map = new Map(); + protected _animationSettings: ToggleAnimationSettings = { + openAnimation: growVerIn, + closeAnimation: growVerOut + }; + + private _defaultClosedCallback = noop; + private _defaultOpenedCallback = noop; + private onClosedCallback: () => any = this._defaultClosedCallback; + private onOpenedCallback: () => any = this._defaultOpenedCallback; + + constructor(protected builder: AnimationBuilder) { + } + + /** @hidden @internal */ + public playOpenAnimation(targetElement: ElementRef, onDone?: () => void): void { + this.startPlayer(ANIMATION_TYPE.OPEN, targetElement, onDone || this._defaultOpenedCallback); + } + + /** @hidden @internal */ + public playCloseAnimation(targetElement: ElementRef, onDone?: () => void): void { + this.startPlayer(ANIMATION_TYPE.CLOSE, targetElement, onDone || this._defaultClosedCallback); + } + public ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + private startPlayer(type: ANIMATION_TYPE, targetElement: ElementRef, callback: () => void): void { + if (!targetElement) { // if no element is passed, there is nothing to animate + return; + } + + let target = this.getPlayer(type); + if (!target) { + target = this.initializePlayer(type, targetElement, callback); + } + + if (target.hasStarted()) { + return; + } + + const targetEmitter = type === ANIMATION_TYPE.OPEN ? this.openAnimationStart : this.closeAnimationStart; + targetEmitter.emit(); + target.play(); + } + + private initializePlayer(type: ANIMATION_TYPE, targetElement: ElementRef, callback: () => void): AnimationPlayer { + const oppositeType = type === ANIMATION_TYPE.OPEN ? ANIMATION_TYPE.CLOSE : ANIMATION_TYPE.OPEN; + const animationSettings = type === ANIMATION_TYPE.OPEN ? + this.animationSettings.openAnimation : this.animationSettings.closeAnimation; + const animation = useAnimation(animationSettings); + const animationBuilder = this.builder.build(animation); + const opposite = this.getPlayer(oppositeType); + let oppositePosition = 1; + if (opposite) { + if (opposite.hasStarted()) { + // .getPosition() still returns 0 sometimes, regardless of the fix for https://github.com/angular/angular/issues/18891; + oppositePosition = (opposite as any)._renderer.engine.players[0].getPosition(); + } + this.cleanUpPlayer(oppositeType); + } + if (type === ANIMATION_TYPE.OPEN) { + this.openAnimationPlayer = animationBuilder.create(targetElement.nativeElement); + } else if (type === ANIMATION_TYPE.CLOSE) { + this.closeAnimationPlayer = animationBuilder.create(targetElement.nativeElement); + } + const target = this.getPlayer(type); + target.init(); + this.getPlayer(type).setPosition(1 - oppositePosition); + if (type === ANIMATION_TYPE.OPEN) { + this.onOpenedCallback = callback; + } else if (type === ANIMATION_TYPE.CLOSE) { + this.onClosedCallback = callback; + } + const targetCallback = type === ANIMATION_TYPE.OPEN ? this.onOpenedCallback : this.onClosedCallback; + const targetEmitter = type === ANIMATION_TYPE.OPEN ? this.openAnimationDone : this.closeAnimationDone; + target.onDone(() => { + targetCallback(); + targetEmitter.emit(); + this.cleanUpPlayer(type); + }); + return target; + } + + + private cleanUpPlayer(target: ANIMATION_TYPE) { + switch (target) { + case ANIMATION_TYPE.CLOSE: + if (this.closeAnimationPlayer != null) { + this.closeAnimationPlayer.reset(); + this.closeAnimationPlayer.destroy(); + this.closeAnimationPlayer = null; + } + this.onClosedCallback = this._defaultClosedCallback; + break; + case ANIMATION_TYPE.OPEN: + if (this.openAnimationPlayer != null) { + this.openAnimationPlayer.reset(); + this.openAnimationPlayer.destroy(); + this.openAnimationPlayer = null; + } + this.onOpenedCallback = this._defaultOpenedCallback; + break; + default: + break; + } + } + + private getPlayer(type: ANIMATION_TYPE): AnimationPlayer { + switch (type) { + case ANIMATION_TYPE.OPEN: + return this.openAnimationPlayer; + case ANIMATION_TYPE.CLOSE: + return this.closeAnimationPlayer; + default: + return null; + } + } +} diff --git a/projects/igniteui-angular/src/lib/services/overlay/overlay.ts b/projects/igniteui-angular/src/lib/services/overlay/overlay.ts index f32907cd314..67c9e4bda85 100644 --- a/projects/igniteui-angular/src/lib/services/overlay/overlay.ts +++ b/projects/igniteui-angular/src/lib/services/overlay/overlay.ts @@ -723,6 +723,7 @@ export class IgxOverlayService implements OnDestroy { // is done, 0.75 if 3/4 of the animation is done. As we need to start next animation from where // the previous has finished we need the amount up to 1, therefore we are subtracting what // getPosition() returns from one + // TODO: This assumes opening and closing animations are mirrored. const position = 1 - info.openAnimationInnerPlayer.getPosition(); info.openAnimationPlayer.reset(); // calling reset does not change hasStarted to false. This is why we are doing it her via internal field diff --git a/projects/igniteui-angular/src/lib/tree/README.md b/projects/igniteui-angular/src/lib/tree/README.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/projects/igniteui-angular/src/lib/tree/common.ts b/projects/igniteui-angular/src/lib/tree/common.ts new file mode 100644 index 00000000000..4250bacdd05 --- /dev/null +++ b/projects/igniteui-angular/src/lib/tree/common.ts @@ -0,0 +1,69 @@ +import { EventEmitter, InjectionToken, QueryList, TemplateRef } from '@angular/core'; +import { IBaseCancelableBrowserEventArgs, IBaseEventArgs, mkenum } from '../core/utils'; +import { ToggleAnimationSettings } from '../expansion-panel/toggle-animation-component'; + +// Component interfaces + +export type IgxTreeSearchResolver = (data: any, node: IgxTreeNode) => boolean; +export interface IgxTree { + id: string; + singleBranchExpand: boolean; + selection: IGX_TREE_SELECTION_TYPE; + selectMarker: TemplateRef; + expandIndicator: TemplateRef; + animationSettings: ToggleAnimationSettings; + nodeExpanding: EventEmitter; + nodeExpanded: EventEmitter; + nodeCollapsing: EventEmitter; + nodeCollapsed: EventEmitter; + expandAll(nodes: IgxTreeNode[]): void; + collapseAll(nodes: IgxTreeNode[]): void; + selectAll(node: IgxTreeNode[]): void; + findNodes(searchTerm: any, comparer?: IgxTreeSearchResolver): IgxTreeNode[] | null; +} + +// Item interfaces +export interface IgxTreeNode { + id: any; + parentNode?: IgxTreeNode | null; + expanded: boolean | null; + selected: boolean | null; + level: number; + data: T; + children: QueryList> | null; +} + +// Events +export interface ITreeNodeSelectionEvent extends IBaseCancelableBrowserEventArgs { + node: IgxTreeNode; +} + +export interface ITreeNodeEditingEvent extends IBaseCancelableBrowserEventArgs { + node: IgxTreeNode; + value: string; +} + +export interface ITreeNodeEditedEvent extends IBaseEventArgs { + node: IgxTreeNode; + value: any; +} + +export interface ITreeNodeTogglingEventArgs extends IBaseEventArgs, IBaseCancelableBrowserEventArgs { + node: IgxTreeNode; +} + +export interface ITreeNodeToggledEventArgs extends IBaseEventArgs { + node: IgxTreeNode; +} + +// Enums +export const IGX_TREE_SELECTION_TYPE = mkenum({ + None: 'None', + BiState: 'BiState', + Cascading: 'Cascading' +}); +export type IGX_TREE_SELECTION_TYPE = (typeof IGX_TREE_SELECTION_TYPE)[keyof typeof IGX_TREE_SELECTION_TYPE]; + +// Token +export const IGX_TREE_COMPONENT = new InjectionToken('IgxTreeToken'); +export const IGX_TREE_NODE_COMPONENT = new InjectionToken>('IgxTreeNodeToken'); diff --git a/projects/igniteui-angular/src/lib/tree/public_api.ts b/projects/igniteui-angular/src/lib/tree/public_api.ts new file mode 100644 index 00000000000..05bf3f8d4bd --- /dev/null +++ b/projects/igniteui-angular/src/lib/tree/public_api.ts @@ -0,0 +1,4 @@ +export * from './tree.component'; +export * from './tree.service'; +export * from './tree-node/tree-node.component'; +export * from './common'; diff --git a/projects/igniteui-angular/src/lib/tree/tree-node/tree-node.component.html b/projects/igniteui-angular/src/lib/tree/tree-node/tree-node.component.html new file mode 100644 index 00000000000..708aeaf8209 --- /dev/null +++ b/projects/igniteui-angular/src/lib/tree/tree-node/tree-node.component.html @@ -0,0 +1,29 @@ +
+
+
+ + +
+
+ + +
+
+ +
+
+ +
+ + + + {{ expanded ? "arrow_drop_down" : "arrow_right" }} + + + + + + + \ No newline at end of file diff --git a/projects/igniteui-angular/src/lib/tree/tree-node/tree-node.component.scss b/projects/igniteui-angular/src/lib/tree/tree-node/tree-node.component.scss new file mode 100644 index 00000000000..5c0d04c448f --- /dev/null +++ b/projects/igniteui-angular/src/lib/tree/tree-node/tree-node.component.scss @@ -0,0 +1,27 @@ +:host { + display: flex; + flex-direction: column; + padding-left: 16px; +} + +.igx-tree-node__header { + display: flex; + flex-direction: row; + align-items: center; + user-select: none; + + .igx-tree-node__indicators { + display: flex; + flex-direction: row; + align-items: flex-end; + padding-right: 8px; + } +} + +.igx-tree-node__group { + overflow: hidden; +} + +.hidden { + visibility: hidden; +} \ No newline at end of file diff --git a/projects/igniteui-angular/src/lib/tree/tree-node/tree-node.component.ts b/projects/igniteui-angular/src/lib/tree/tree-node/tree-node.component.ts new file mode 100644 index 00000000000..329ca4c5df2 --- /dev/null +++ b/projects/igniteui-angular/src/lib/tree/tree-node/tree-node.component.ts @@ -0,0 +1,188 @@ +import { AnimationBuilder } from '@angular/animations'; +import { + Component, OnInit, + OnDestroy, Input, Inject, ViewChild, TemplateRef, AfterViewInit, QueryList, ContentChildren, Optional, SkipSelf, + HostBinding, + ElementRef, + ChangeDetectorRef +} from '@angular/core'; +import { takeUntil } from 'rxjs/operators'; +import { IgxSelectionAPIService } from '../../core/selection'; +import { ToggleAnimationPlayer, ToggleAnimationSettings } from '../../expansion-panel/toggle-animation-component'; +import { IGX_TREE_COMPONENT, IgxTree, IgxTreeNode, IGX_TREE_NODE_COMPONENT, ITreeNodeTogglingEventArgs } from '../common'; +import { IgxTreeService } from '../tree.service'; + + +let nodeId = 0; + +/** + * + * The tree node component represents a child node of the tree component or another tree node. + * Usage: + * + * ```html + * + * ... + * + * {{ data.FirstName }} {{ data.LastName }} + * + * ... + * + * ``` + */ +@Component({ + selector: 'igx-tree-node', + templateUrl: 'tree-node.component.html', + styleUrls: ['tree-node.component.scss'], + providers: [ + { provide: IGX_TREE_NODE_COMPONENT, useExisting: IgxTreeNodeComponent } + ] +}) +export class IgxTreeNodeComponent extends ToggleAnimationPlayer implements IgxTreeNode, OnInit, AfterViewInit, OnDestroy { + @Input() + public data: T; + + public set animationSettings(val: ToggleAnimationSettings) {} + + public get animationSettings(): ToggleAnimationSettings { + return this.tree.animationSettings; + } + + @Input() + public selectMarker: TemplateRef; + + @Input() + public expandIndicator: TemplateRef; + + @HostBinding('class.igx-tree-node') + public cssClass = 'igx-tree-node'; + + @ContentChildren(IGX_TREE_NODE_COMPONENT, { read: IGX_TREE_NODE_COMPONENT }) + public children: QueryList>; + + @ViewChild('defaultSelect', { read: TemplateRef, static: true }) + private _defaultSelectMarker: TemplateRef; + + @ViewChild('defaultIndicator', { read: TemplateRef, static: true }) + private _defaultExpandIndicatorTemplate: TemplateRef; + + @ViewChild('childrenContainer', { read: ElementRef }) + private childrenContainer: ElementRef; + + public inEdit = false; + + public id = `igxTreeNode_${nodeId++}`; + + constructor( + @Inject(IGX_TREE_COMPONENT) public tree: IgxTree, + protected selectionService: IgxSelectionAPIService, + protected treeService: IgxTreeService, + protected cdr: ChangeDetectorRef, + protected builder: AnimationBuilder, + @Optional() @SkipSelf() @Inject(IGX_TREE_NODE_COMPONENT) public parentNode: IgxTreeNode + ) { + super(builder); + } + + public get level(): number { + return this.parentNode ? this.parentNode.level + 1 : 0; + } + + @Input() + public get selected(): boolean { + return this.selectionService.get(this.tree.id).has(this.id); + } + + public set selected(val: boolean) { + if (val) { + this.treeService.select(this); + } else { + this.treeService.deselect(this); + } + } + + @Input() + public get expanded() { + return this.treeService.isExpanded(this.id); + } + + public set expanded(val: boolean) { + if (val) { + this.expand(); + } else { + this.collapse(); + } + } + + public get selectMarkerTemplate(): TemplateRef { + return this.selectMarker ? this.selectMarker : this._defaultSelectMarker; + } + + public get expandIndicatorTemplate(): TemplateRef { + return this.expandIndicator ? this.expandIndicator : this._defaultExpandIndicatorTemplate; + } + + public get templateContext(): any { + return { + $implicit: this + }; + } + + public ngOnInit() { + this.openAnimationDone.pipe(takeUntil(this.destroy$)).subscribe( + () => { + this.tree.nodeExpanded.emit({ owner: this.tree, node: this }); + } + ); + this.closeAnimationDone.pipe(takeUntil(this.destroy$)).subscribe(() => { + this.tree.nodeCollapsed.emit({ owner: this.tree, node: this }); + this.treeService.collapse(this.id); + this.cdr.markForCheck(); + }); + } + + public ngAfterViewInit() { + } + + public ngOnDestroy() { + super.ngOnDestroy(); + } + + private expand() { + if (this.treeService.isExpanded(this.id)) { + return; + } + const args: ITreeNodeTogglingEventArgs = { + owner: this.tree, + node: this, + cancel: false + + }; + this.tree.nodeExpanding.emit(args); + if (!args.cancel) { + this.treeService.expand(this); + this.cdr.detectChanges(); + this.playOpenAnimation( + this.childrenContainer + ); + } + } + + private collapse() { + if (!this.treeService.isExpanded(this.id)) { + return; + } + const args: ITreeNodeTogglingEventArgs = { + owner: this.tree, + node: this, + cancel: false + + }; + this.tree.nodeCollapsing.emit(args); + if (!args.cancel) { + this.playCloseAnimation( + this.childrenContainer + ); + } + } +} diff --git a/projects/igniteui-angular/src/lib/tree/tree.component.html b/projects/igniteui-angular/src/lib/tree/tree.component.html new file mode 100644 index 00000000000..ea506f86d54 --- /dev/null +++ b/projects/igniteui-angular/src/lib/tree/tree.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/projects/igniteui-angular/src/lib/tree/tree.component.scss b/projects/igniteui-angular/src/lib/tree/tree.component.scss new file mode 100644 index 00000000000..f09d32524ad --- /dev/null +++ b/projects/igniteui-angular/src/lib/tree/tree.component.scss @@ -0,0 +1,3 @@ +:host { + display: block; +} \ No newline at end of file diff --git a/projects/igniteui-angular/src/lib/tree/tree.component.ts b/projects/igniteui-angular/src/lib/tree/tree.component.ts new file mode 100644 index 00000000000..2d1cbcf4ad0 --- /dev/null +++ b/projects/igniteui-angular/src/lib/tree/tree.component.ts @@ -0,0 +1,143 @@ +import { CommonModule } from '@angular/common'; +import { Component, QueryList, Input, Output, EventEmitter, ContentChild, Directive, + NgModule, TemplateRef, OnInit, AfterViewInit, ContentChildren, OnDestroy } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { growVerIn, growVerOut } from '../animations/grow'; +import { IgxCheckboxModule } from '../checkbox/checkbox.component'; +import { IgxSelectionAPIService } from '../core/selection'; +import { IgxExpansionPanelModule } from '../expansion-panel/public_api'; +import { ToggleAnimationSettings } from '../expansion-panel/toggle-animation-component'; +import { IgxIconModule } from '../icon/public_api'; +import { IgxInputGroupModule } from '../input-group/public_api'; +import { IGX_TREE_COMPONENT, IGX_TREE_SELECTION_TYPE, IgxTree, ITreeNodeToggledEventArgs, + ITreeNodeTogglingEventArgs, ITreeNodeSelectionEvent, IgxTreeNode, IgxTreeSearchResolver } from './common'; +import { IgxTreeNodeComponent } from './tree-node/tree-node.component'; +import { IgxTreeService } from './tree.service'; + +let init_id = 0; + +@Directive({ + selector: '[igxTreeSelectMarker]' +}) +export class IgxTreeSelectMarkerDirective { +} + +@Directive({ + selector: '[igxTreeExpandIndicator]' +}) +export class IgxTreeExpandIndicatorDirective { +} + +@Component({ + selector: 'igx-tree', + templateUrl: 'tree.component.html', + styleUrls: ['tree.component.scss'], + providers: [ + IgxTreeService, + { provide: IGX_TREE_COMPONENT, useExisting: IgxTreeComponent} + ] +}) +export class IgxTreeComponent implements IgxTree, OnInit, AfterViewInit, OnDestroy { + + + @Input() + public selection: IGX_TREE_SELECTION_TYPE = IGX_TREE_SELECTION_TYPE.BiState; + + @Input() + public singleBranchExpand = false; + + @Input() + public animationSettings: ToggleAnimationSettings = { + openAnimation: growVerIn, + closeAnimation: growVerOut + }; + + @Output() + public nodeSelection = new EventEmitter(); + + @Output() + public nodeExpanding = new EventEmitter(); + + @Output() + public nodeExpanded = new EventEmitter(); + + @Output() + public nodeCollapsing = new EventEmitter(); + + @Output() + public nodeCollapsed = new EventEmitter(); + + @ContentChild(IgxTreeSelectMarkerDirective, { read: TemplateRef }) + public selectMarker: TemplateRef; + + @ContentChild(IgxTreeExpandIndicatorDirective, { read: TemplateRef }) + public expandIndicator: TemplateRef; + + @ContentChildren(IgxTreeNodeComponent, { descendants: true }) + private nodes: QueryList>; + + public id = `tree-${init_id++}`; + + constructor(private selectionService: IgxSelectionAPIService, private treeService: IgxTreeService) { + this.treeService.register(this); + } + + public expandAll(nodes: IgxTreeNode[]) {} + public collapseAll(nodes: IgxTreeNode[]) {} + public selectAll(nodes: IgxTreeNode[]) {} + + public isNodeSelected(node: IgxTreeNodeComponent): boolean { + return this.selectionService.get(this.id).has(node.id); + } + + + public findNodes(searchTerm: T, comparer?: IgxTreeSearchResolver): IgxTreeNode[] | null { + const compareFunc = comparer || this._comparer; + return this.nodes.filter(e => compareFunc(searchTerm, e)); + } + + public ngOnInit() { + this.selectionService.set(this.id, new Set()); + } + public ngAfterViewInit() { + + } + + public ngOnDestroy() { + this.selectionService.clear(this.id); + } + + private _comparer = (data: T, node: IgxTreeNodeComponent, ) => node.data === data; +} + +/** + * NgModule defining the components and directives needed for `igx-tree` + */ +@NgModule({ + declarations: [ + IgxTreeSelectMarkerDirective, + IgxTreeExpandIndicatorDirective, + IgxTreeComponent, + IgxTreeNodeComponent + ], + imports: [ + CommonModule, + FormsModule, + IgxIconModule, + IgxInputGroupModule, + IgxCheckboxModule, + IgxExpansionPanelModule + ], + exports: [ + IgxTreeSelectMarkerDirective, + IgxTreeExpandIndicatorDirective, + IgxTreeComponent, + IgxTreeNodeComponent, + IgxIconModule, + IgxInputGroupModule, + IgxCheckboxModule, + IgxExpansionPanelModule + ] +}) +export class IgxTreeModule { +} diff --git a/projects/igniteui-angular/src/lib/tree/tree.service.ts b/projects/igniteui-angular/src/lib/tree/tree.service.ts new file mode 100644 index 00000000000..f7a4e6f3ffa --- /dev/null +++ b/projects/igniteui-angular/src/lib/tree/tree.service.ts @@ -0,0 +1,40 @@ +import { Injectable } from '@angular/core'; +import { IgxTree, IgxTreeNode } from './common'; + +/** @hidden @internal */ +@Injectable() +export class IgxTreeService { + public expandedNodes: Set = new Set(); + private tree: IgxTree; + + public expand(node: IgxTreeNode): void { + this.expandedNodes.add(node.id); + if (this.tree.singleBranchExpand) { + this.tree.findNodes(node, this.siblingComparer)?.forEach(e => { + e.expanded = false; + }); + } + } + + public collapse(id: string): void { + this.expandedNodes.delete(id); + } + + public isExpanded(id: string): boolean { + return this.expandedNodes.has(id); + } + + public select(node: IgxTreeNode): void { + } + + public deselect(node: IgxTreeNode): void { + } + + public register(tree: IgxTree) { + this.tree = tree; + } + + private siblingComparer: + (data: IgxTreeNode, node: IgxTreeNode) => boolean = + (data: IgxTreeNode, node: IgxTreeNode) => node !== data && node.level === data.level; +} diff --git a/projects/igniteui-angular/src/public_api.ts b/projects/igniteui-angular/src/public_api.ts index bbc27d15858..34f49e383dd 100644 --- a/projects/igniteui-angular/src/public_api.ts +++ b/projects/igniteui-angular/src/public_api.ts @@ -102,6 +102,7 @@ export * from './lib/date-range-picker/public_api'; export * from './lib/grids/column-actions/column-actions-base.directive'; export * from './lib/grids/column-actions/column-actions.component'; +export * from './lib/tree/public_api'; /** * Exporter services, classes, interfaces and enums diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 7e5bf2c17fb..2b702a81642 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -460,6 +460,11 @@ export class AppComponent implements OnInit { icon: 'view_column', name: 'HierarchicalGrid Add Row' }, + { + link: '/tree', + icon: 'account_tree', + name: 'Tree' + }, { link: '/treeGrid', icon: 'view_column', diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 2b5221f0840..2211918e8a8 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -146,6 +146,7 @@ import { CommonModule } from '@angular/common'; import { GridEventsComponent } from './grid-events/grid-events.component'; import { GridUpdatesComponent } from './grid-updates-test/grid-updates.component'; import { TestInterceptorClass } from './interceptor.service'; +import { TreeSampleComponent } from './tree/tree.sample'; const components = [ ActionStripSampleComponent, @@ -241,6 +242,7 @@ const components = [ AnimationsSampleComponent, ShadowsSampleComponent, TypographySampleComponent, + TreeSampleComponent, RadioSampleComponent, TooltipSampleComponent, HierarchicalGridSampleComponent, diff --git a/src/app/app.routing.ts b/src/app/app.routing.ts index 5ba400414a0..78ddf6ab45f 100644 --- a/src/app/app.routing.ts +++ b/src/app/app.routing.ts @@ -86,6 +86,7 @@ import { GridFormattingComponent } from './grid-formatting/grid-formatting.compo import { MainComponent } from './grid-finjs/main.component'; import { GridEventsComponent } from './grid-events/grid-events.component'; import { GridUpdatesComponent } from './grid-updates-test/grid-updates.component'; +import { TreeSampleComponent } from './tree/tree.sample'; const appRoutes = [ { @@ -342,6 +343,10 @@ const appRoutes = [ path: 'gridMasterDetail', component: GridMasterDetailSampleComponent }, + { + path: 'tree', + component: TreeSampleComponent + }, { path: 'treeGrid', component: TreeGridSampleComponent diff --git a/src/app/routing.ts b/src/app/routing.ts index 4cbb696e122..c2652812ee0 100644 --- a/src/app/routing.ts +++ b/src/app/routing.ts @@ -117,6 +117,7 @@ import { GridFormattingComponent } from './grid-formatting/grid-formatting.compo import { MainComponent } from './grid-finjs/main.component'; import { GridEventsComponent } from './grid-events/grid-events.component'; import { GridUpdatesComponent } from './grid-updates-test/grid-updates.component'; +import { TreeSampleComponent } from './tree/tree.sample'; const appRoutes = [ { @@ -484,6 +485,9 @@ const appRoutes = [ { path: 'gridFinJS', component: MainComponent + },{ + path: 'tree', + component: TreeSampleComponent }, { path: 'gridUpdates', diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 8a1f1bc0541..49042e26215 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -40,7 +40,8 @@ import { IgxToggleModule, IgxTooltipModule, IgxSelectModule, - IgxDateRangePickerModule + IgxDateRangePickerModule, + IgxTreeModule } from 'igniteui-angular'; @@ -79,6 +80,7 @@ const igniteModules = [ IgxSnackbarModule, IgxSwitchModule, IgxSplitterModule, + IgxTreeModule, IgxTabsModule, IgxTimePickerModule, IgxToastModule, diff --git a/src/app/tree/tree.sample.html b/src/app/tree/tree.sample.html new file mode 100644 index 00000000000..6e2088d894c --- /dev/null +++ b/src/app/tree/tree.sample.html @@ -0,0 +1,56 @@ +
+
+ + + + +
+ +
+ + + + + +
+
+ + Single Branch Expand +
+
+ + + +
+
+

IgxTree

+ + + {{ node.CompanyName }} + + {{ child.CompanyName }} + + {{ leafchild.CompanyName }} + + + + +
+ + +
+

IgxTree 2: The Branchening

+ + + {{ node.CompanyName }} + + {{ child.CompanyName }} + + {{ leafchild.CompanyName }} + + + + +
+
\ No newline at end of file diff --git a/src/app/tree/tree.sample.scss b/src/app/tree/tree.sample.scss new file mode 100644 index 00000000000..69098ac0dca --- /dev/null +++ b/src/app/tree/tree.sample.scss @@ -0,0 +1,26 @@ +.row { + display: flex; + flex-flow: row; +} + +.column { + display: flex; + flex-direction: column; +} + +.tree-container { + width: 50%; +} + +.controls { + padding: 40px; + .row { + border: 2px solid#d1d1d1; + padding: 12px; + margin: 12px; + } +} + +.meduim { + width: 400px +} \ No newline at end of file diff --git a/src/app/tree/tree.sample.ts b/src/app/tree/tree.sample.ts new file mode 100644 index 00000000000..47b59c89634 --- /dev/null +++ b/src/app/tree/tree.sample.ts @@ -0,0 +1,43 @@ +import { useAnimation } from '@angular/animations'; +import { Component, ViewChild } from '@angular/core'; +import { growVerIn, growVerOut, IgxTreeComponent, IgxTreeNodeComponent, IgxTreeSearchResolver } from 'igniteui-angular'; +import { HIERARCHICAL_SAMPLE_DATA } from '../shared/sample-data'; + +@Component({ + selector: 'app-tree-sample', + templateUrl: 'tree.sample.html', + styleUrls: ['tree.sample.scss'] +}) +export class TreeSampleComponent { + @ViewChild('tree1', { read: IgxTreeComponent }) + public tree: IgxTreeComponent; + + public animationDuration = 400; + + public data = HIERARCHICAL_SAMPLE_DATA; + + public singleBranchExpand = false; + + public get animationSettings() { + return { + openAnimation: useAnimation(growVerIn, { + params: { + duration: `${this.animationDuration}ms` + } + }), + closeAnimation: useAnimation(growVerOut, { + params: { + duration: `${this.animationDuration}ms` + } + }) + }; + } + + public customSearch(term: string) { + const searchResult = this.tree.findNodes(term, this.containsComparer); + console.log(searchResult); + } + + private containsComparer: IgxTreeSearchResolver = + (term: any, node: IgxTreeNodeComponent) => node.data.ID.ToLowerCase().indexOf(term.ToLowerCase()) > -1; +}