UNPKG

carbon-components-angular

Version:
654 lines (647 loc) 25.2 kB
import * as i0 from '@angular/core'; import { Injectable, EventEmitter, TemplateRef, Component, Input, Output, Inject, ViewChild, NgModule } from '@angular/core'; import * as i2 from '@angular/common'; import { DOCUMENT, CommonModule } from '@angular/common'; import * as i3 from 'carbon-components-angular/icon'; import { IconModule } from 'carbon-components-angular/icon'; import { ReplaySubject } from 'rxjs'; class TreeViewService { constructor() { /** * Variable used across all nodes and wrapper to determine if we should allow content projection * or generate the tree * * Value is updated by passing a value to `tree` input in wrapper component. */ this.contentProjected = true; /** * **Experimental** */ this.isMultiSelect = false; this.selectionSubject = new ReplaySubject(1); /** * Hold's list of selected nodes and preserves order */ this.value = new Map(); this.selectionObservable = this.selectionSubject.asObservable(); } /** * Store selected node in map * @param node: Node */ selectNode(node) { if (!node) { return; } // Since multiselect is not enabled, we clear existing map if (!this.isMultiSelect) { this.value.clear(); } this.value.set(node.id, node); this.selectionSubject.next(this.value); } } TreeViewService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TreeViewService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); TreeViewService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TreeViewService }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TreeViewService, decorators: [{ type: Injectable }], ctorParameters: function () { return []; } }); class TreeNodeComponent { constructor(treeViewService) { this.treeViewService = treeViewService; this.id = `tree-node-${TreeNodeComponent.treeNodeCount++}`; this.active = false; this.disabled = false; this.expanded = false; this.selected = false; this.children = []; /** * Determines the depth of the node * Calculated by default when passing `Node` array to `TreeViewComponent`, manual entry required otherwise */ this.depth = 0; this.nodeFocus = new EventEmitter(); this.nodeBlur = new EventEmitter(); this.nodeSelect = new EventEmitter(); this.nodetoggle = new EventEmitter(); } /** * Simple way to set all attributes of Node component via node object * Would simplify setting component attributes when dynamically rendering node. */ set node(node) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m; this._node = node; this.id = (_a = node.id) !== null && _a !== void 0 ? _a : this.id; this.active = (_b = node.active) !== null && _b !== void 0 ? _b : this.active; this.disabled = (_c = node.disabled) !== null && _c !== void 0 ? _c : this.disabled; this.expanded = (_d = node.expanded) !== null && _d !== void 0 ? _d : this.expanded; this.label = (_e = node.label) !== null && _e !== void 0 ? _e : this.label; this.labelContext = (_f = node.labelContext) !== null && _f !== void 0 ? _f : this.labelContext; this.value = (_g = node.value) !== null && _g !== void 0 ? _g : this.value; this.icon = (_h = node.icon) !== null && _h !== void 0 ? _h : this.icon; this.selected = (_j = node.selected) !== null && _j !== void 0 ? _j : this.selected; this.depth = (_k = node.depth) !== null && _k !== void 0 ? _k : this.depth; this.children = (_l = node.children) !== null && _l !== void 0 ? _l : this.children; this.iconContext = (_m = node.iconText) !== null && _m !== void 0 ? _m : this.iconContext; } get node() { return this._node; } /** * Caclulate offset for margin/padding */ ngAfterContentChecked() { this.offset = this.calculateOffset(); } /** * Highlight the node */ ngOnInit() { // Highlight the node this.subscription = this.treeViewService.selectionObservable.subscribe((value) => { this.selected = value.has(this.id); this.active = this.selected; }); } /** * Unsubscribe from subscriptions */ ngOnDestroy() { var _a; (_a = this.subscription) === null || _a === void 0 ? void 0 : _a.unsubscribe(); } /** * Selects the node and emits the event from the tree view component * @param event */ nodeClick(event) { if (!this.disabled) { this.selected = true; this.active = true; event.target.parentElement.focus(); // Passes event to all nodes to update highlighting & parent to emit this.treeViewService.selectNode({ id: this.id, label: this.label, value: this.value }); } } /** * Calculate the node offset * @returns Number */ calculateOffset() { // Parent node with icon if (this.children.length && this.icon) { return this.depth + 1 + this.depth * 0.5; } // parent node without icon if (this.children.length) { return this.depth + 1; } // leaf node with icon if (this.icon) { return this.depth + 2 + this.depth * 0.5; } return this.depth + 2.5; } emitFocusEvent(event) { this.nodeFocus.emit({ node: { id: this.id, label: this.label, value: this.value }, event }); } emitBlurEvent(event) { this.nodeBlur.emit({ node: { id: this.id, label: this.label, value: this.value }, event }); } /** * Expand children if not disabled * @param event: Event */ toggleExpanded(event) { if (!this.disabled) { this.nodetoggle.emit({ node: { id: this.id, label: this.label, value: this.value }, event }); this.expanded = !this.expanded; // Prevent selection of the node event.stopPropagation(); } } /** * Manages the keyboard accessibility for children expansion & selection */ navigateTree(event) { if (event.key === "ArrowLeft" || event.key === "ArrowRight" || event.key === "Enter") { event.stopPropagation(); } // Unexpand if (event.key === "ArrowLeft") { if (this.expanded && this.children) { this.toggleExpanded(event); } } if (event.key === "ArrowRight") { if (!this.expanded && this.children) { this.toggleExpanded(event); } } if (event.key === "Enter") { event.preventDefault(); this.nodeClick(event); } } isTemplate(value) { return value instanceof TemplateRef; } isProjected() { return this.treeViewService.contentProjected; } } TreeNodeComponent.treeNodeCount = 0; TreeNodeComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TreeNodeComponent, deps: [{ token: TreeViewService }], target: i0.ɵɵFactoryTarget.Component }); TreeNodeComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.3.0", type: TreeNodeComponent, selector: "cds-tree-node", inputs: { id: "id", active: "active", disabled: "disabled", expanded: "expanded", label: "label", labelContext: "labelContext", selected: "selected", value: "value", icon: "icon", iconContext: "iconContext", children: "children", depth: "depth", node: "node" }, outputs: { nodeFocus: "nodeFocus", nodeBlur: "nodeBlur", nodeSelect: "nodeSelect", nodetoggle: "nodetoggle" }, ngImport: i0, template: ` <div [id]="id" class="cds--tree-node" [ngClass]="{ 'cds--tree-node--active': active, 'cds--tree-node--disabled': disabled, 'cds--tree-node--selected': selected, 'cds--tree-leaf-node': !children.length, 'cds--tree-parent-node': children.length, 'cds--tree-node--with-icon': icon }" [attr.aria-expanded]="expanded || null" [attr.aria-current]="active || null" [attr.aria-selected]="disabled ? null : selected" [attr.aria-disabled]="disabled" role="treeitem" [attr.tabindex]="selected ? 0 : -1" (focus)="emitFocusEvent($event)" (blur)="emitBlurEvent($event)" (keydown)="navigateTree($event)"> <div *ngIf="!children.length" class="cds--tree-node__label" [style.padding-inline-start.rem]="offset" [style.margin-inline-start.rem]="-offset" (click)="nodeClick($event)"> <!-- Icon --> <ng-container *ngIf="icon && !isTemplate(icon)"> <svg class="cds--tree-node__icon" [cdsIcon]="icon" size="16"> </svg> </ng-container> <ng-template *ngIf="isTemplate(icon)" [ngTemplateOutlet]="icon"></ng-template> <ng-container *ngIf="!isTemplate(label)">{{label}}</ng-container> <ng-template *ngIf="isTemplate(label)" [ngTemplateOutlet]="label" [ngTemplateOutletContext]="{ $implicit: labelContext }"> </ng-template> </div> <div *ngIf="children.length" class="cds--tree-node__label" [style.padding-inline-start.rem]="offset" [style.margin-inline-start.rem]="-offset" role="group" (click)="nodeClick($event)"> <span class="cds--tree-parent-node__toggle" [attr.disabled]="disabled || null" (click)="toggleExpanded($event)"> <svg class="cds--tree-parent-node__toggle-icon" [ngClass]="{'cds--tree-parent-node__toggle-icon--expanded' : expanded}" ibmIcon="caret--down" size="16"> </svg> </span> <span class="cds--tree-node__label__details"> <!-- Icon --> <ng-container *ngIf="icon && !isTemplate(icon)"> <svg class="cds--tree-node__icon" [cdsIcon]="icon" size="16"> </svg> </ng-container> <ng-template *ngIf="isTemplate(icon)" [ngTemplateOutlet]="icon" [ngTemplateOutletContext]="{ $implicit: iconContext }"> </ng-template> <ng-container *ngIf="!isTemplate(label)">{{label}}</ng-container> <ng-template *ngIf="isTemplate(label)" [ngTemplateOutlet]="label" [ngTemplateOutletContext]="{ $implicit: labelContext }"> </ng-template> </span> </div> <div *ngIf="expanded" role="group" class="cds--tree-node__children"> <ng-container *ngIf="isProjected(); else notProjected"> <ng-content></ng-content> </ng-container> <ng-template #notProjected> <cds-tree-node *ngFor="let childNode of children" [node]="childNode" [depth]="depth + 1" [disabled]="disabled"> </cds-tree-node> </ng-template> </div> </div> `, isInline: true, dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i3.IconDirective, selector: "[cdsIcon], [ibmIcon]", inputs: ["ibmIcon", "cdsIcon", "size", "title", "ariaLabel", "ariaLabelledBy", "ariaHidden", "isFocusable"] }, { kind: "component", type: TreeNodeComponent, selector: "cds-tree-node", inputs: ["id", "active", "disabled", "expanded", "label", "labelContext", "selected", "value", "icon", "iconContext", "children", "depth", "node"], outputs: ["nodeFocus", "nodeBlur", "nodeSelect", "nodetoggle"] }] }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TreeNodeComponent, decorators: [{ type: Component, args: [{ selector: "cds-tree-node", template: ` <div [id]="id" class="cds--tree-node" [ngClass]="{ 'cds--tree-node--active': active, 'cds--tree-node--disabled': disabled, 'cds--tree-node--selected': selected, 'cds--tree-leaf-node': !children.length, 'cds--tree-parent-node': children.length, 'cds--tree-node--with-icon': icon }" [attr.aria-expanded]="expanded || null" [attr.aria-current]="active || null" [attr.aria-selected]="disabled ? null : selected" [attr.aria-disabled]="disabled" role="treeitem" [attr.tabindex]="selected ? 0 : -1" (focus)="emitFocusEvent($event)" (blur)="emitBlurEvent($event)" (keydown)="navigateTree($event)"> <div *ngIf="!children.length" class="cds--tree-node__label" [style.padding-inline-start.rem]="offset" [style.margin-inline-start.rem]="-offset" (click)="nodeClick($event)"> <!-- Icon --> <ng-container *ngIf="icon && !isTemplate(icon)"> <svg class="cds--tree-node__icon" [cdsIcon]="icon" size="16"> </svg> </ng-container> <ng-template *ngIf="isTemplate(icon)" [ngTemplateOutlet]="icon"></ng-template> <ng-container *ngIf="!isTemplate(label)">{{label}}</ng-container> <ng-template *ngIf="isTemplate(label)" [ngTemplateOutlet]="label" [ngTemplateOutletContext]="{ $implicit: labelContext }"> </ng-template> </div> <div *ngIf="children.length" class="cds--tree-node__label" [style.padding-inline-start.rem]="offset" [style.margin-inline-start.rem]="-offset" role="group" (click)="nodeClick($event)"> <span class="cds--tree-parent-node__toggle" [attr.disabled]="disabled || null" (click)="toggleExpanded($event)"> <svg class="cds--tree-parent-node__toggle-icon" [ngClass]="{'cds--tree-parent-node__toggle-icon--expanded' : expanded}" ibmIcon="caret--down" size="16"> </svg> </span> <span class="cds--tree-node__label__details"> <!-- Icon --> <ng-container *ngIf="icon && !isTemplate(icon)"> <svg class="cds--tree-node__icon" [cdsIcon]="icon" size="16"> </svg> </ng-container> <ng-template *ngIf="isTemplate(icon)" [ngTemplateOutlet]="icon" [ngTemplateOutletContext]="{ $implicit: iconContext }"> </ng-template> <ng-container *ngIf="!isTemplate(label)">{{label}}</ng-container> <ng-template *ngIf="isTemplate(label)" [ngTemplateOutlet]="label" [ngTemplateOutletContext]="{ $implicit: labelContext }"> </ng-template> </span> </div> <div *ngIf="expanded" role="group" class="cds--tree-node__children"> <ng-container *ngIf="isProjected(); else notProjected"> <ng-content></ng-content> </ng-container> <ng-template #notProjected> <cds-tree-node *ngFor="let childNode of children" [node]="childNode" [depth]="depth + 1" [disabled]="disabled"> </cds-tree-node> </ng-template> </div> </div> ` }] }], ctorParameters: function () { return [{ type: TreeViewService }]; }, propDecorators: { id: [{ type: Input }], active: [{ type: Input }], disabled: [{ type: Input }], expanded: [{ type: Input }], label: [{ type: Input }], labelContext: [{ type: Input }], selected: [{ type: Input }], value: [{ type: Input }], icon: [{ type: Input }], iconContext: [{ type: Input }], children: [{ type: Input }], depth: [{ type: Input }], node: [{ type: Input }], nodeFocus: [{ type: Output }], nodeBlur: [{ type: Output }], nodeSelect: [{ type: Output }], nodetoggle: [{ type: Output }] } }); /** * Get started with importing the module: * * ```typescript * import { TreeviewModule } from 'carbon-components-angular'; * ``` * * [See demo](../../?path=/story/components-tree-view--basic) */ class TreeViewComponent { constructor(document, treeViewService, elementRef) { this.document = document; this.treeViewService = treeViewService; this.elementRef = elementRef; this.id = `tree-view-${TreeViewComponent.treeViewCount++}`; /** * Specify the size of the list items in the tree */ this.size = "sm"; this.select = new EventEmitter(); this._tree = []; } /** * Pass `Node[]` array to have tree view render the nodes * Passing value will disregard projected content */ set tree(treeNodes) { this._tree = treeNodes.map((node) => Object.assign({}, node)); this.treeViewService.contentProjected = false; } get tree() { return this._tree; } /** * **Experimental** - Enable to select multiple nodes */ set isMultiSelect(isMulti) { this.treeViewService.isMultiSelect = isMulti; } /** * Subscribe for node selection */ ngOnInit() { this.subscription = this.treeViewService.selectionObservable.subscribe((nodesMap) => { // Get all values from the map to emit const nodes = [...nodesMap.values()]; // Update focus to reset arrow key traversal // Select the current highlight node as the last node, since we preserve order in map this.treeWalker.currentNode = this.elementRef.nativeElement.querySelector(`#${CSS.escape(nodes[nodes.length - 1].id)}`); this.select.emit(this.treeViewService.isMultiSelect ? nodes : nodes[0]); }); } ngOnDestroy() { this.subscription.unsubscribe(); } /** * Initialize tree walker to support keyboard navigation */ ngAfterViewInit() { this.treeWalker = this.document.createTreeWalker(this.root.nativeElement, NodeFilter.SHOW_ELEMENT, { acceptNode: function (node) { if (node.classList.contains(`cds--tree-node--disabled`)) { return NodeFilter.FILTER_REJECT; } if (node.matches(`div.cds--tree-node`)) { return NodeFilter.FILTER_ACCEPT; } return NodeFilter.FILTER_SKIP; } }); } /** * Navigate tree using tree walker * @param event - KeyboardEvent */ navigateTree(event) { var _a, _b; if (event.key === "ArrowUp") { (_a = this.treeWalker.previousNode()) === null || _a === void 0 ? void 0 : _a.focus(); } if (event.key === "ArrowDown") { (_b = this.treeWalker.nextNode()) === null || _b === void 0 ? void 0 : _b.focus(); } } isTemplate(value) { return value instanceof TemplateRef; } isProjected() { return this.treeViewService.contentProjected; } } TreeViewComponent.treeViewCount = 0; TreeViewComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TreeViewComponent, deps: [{ token: DOCUMENT }, { token: TreeViewService }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component }); TreeViewComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.3.0", type: TreeViewComponent, selector: "cds-tree-view", inputs: { tree: "tree", id: "id", label: "label", labelContext: "labelContext", size: "size", isMultiSelect: "isMultiSelect" }, outputs: { select: "select" }, providers: [TreeViewService], viewQueries: [{ propertyName: "root", first: true, predicate: ["treeWrapper"], descendants: true }], ngImport: i0, template: ` <label *ngIf="label" [id]="id" class="cds--label"> <ng-container *ngIf="!isTemplate(label)">{{label}}</ng-container> <ng-template *ngIf="isTemplate(label)" [ngTemplateOutlet]="label" [ngTemplateOutletContext]="{ $implicit: labelContext }"> </ng-template> </label> <div class="cds--tree" [ngClass]="{ 'cds--tree--sm': size === 'sm', 'cds--tree--xs': size === 'xs' }" [attr.aria-label]="label ? label : null" [attr.aria-labelledby]="!label ? id : null" [attr.aria-multiselectable]="isMultiSelect || null" role="tree" (keydown)="navigateTree($event)" #treeWrapper> <ng-container *ngIf="isProjected(); else notProjected"> <ng-content></ng-content> </ng-container> <ng-template #notProjected> <cds-tree-node *ngFor="let node of tree" [node]="node"> </cds-tree-node> </ng-template> </div> `, isInline: true, dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: TreeNodeComponent, selector: "cds-tree-node", inputs: ["id", "active", "disabled", "expanded", "label", "labelContext", "selected", "value", "icon", "iconContext", "children", "depth", "node"], outputs: ["nodeFocus", "nodeBlur", "nodeSelect", "nodetoggle"] }] }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TreeViewComponent, decorators: [{ type: Component, args: [{ selector: "cds-tree-view", template: ` <label *ngIf="label" [id]="id" class="cds--label"> <ng-container *ngIf="!isTemplate(label)">{{label}}</ng-container> <ng-template *ngIf="isTemplate(label)" [ngTemplateOutlet]="label" [ngTemplateOutletContext]="{ $implicit: labelContext }"> </ng-template> </label> <div class="cds--tree" [ngClass]="{ 'cds--tree--sm': size === 'sm', 'cds--tree--xs': size === 'xs' }" [attr.aria-label]="label ? label : null" [attr.aria-labelledby]="!label ? id : null" [attr.aria-multiselectable]="isMultiSelect || null" role="tree" (keydown)="navigateTree($event)" #treeWrapper> <ng-container *ngIf="isProjected(); else notProjected"> <ng-content></ng-content> </ng-container> <ng-template #notProjected> <cds-tree-node *ngFor="let node of tree" [node]="node"> </cds-tree-node> </ng-template> </div> `, providers: [TreeViewService] }] }], ctorParameters: function () { return [{ type: Document, decorators: [{ type: Inject, args: [DOCUMENT] }] }, { type: TreeViewService }, { type: i0.ElementRef }]; }, propDecorators: { tree: [{ type: Input }], id: [{ type: Input }], label: [{ type: Input }], labelContext: [{ type: Input }], size: [{ type: Input }], isMultiSelect: [{ type: Input }], select: [{ type: Output }], root: [{ type: ViewChild, args: ["treeWrapper"] }] } }); class TreeviewModule { } TreeviewModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TreeviewModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); TreeviewModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "14.3.0", ngImport: i0, type: TreeviewModule, declarations: [TreeViewComponent, TreeNodeComponent], imports: [CommonModule, IconModule], exports: [TreeViewComponent, TreeNodeComponent] }); TreeviewModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TreeviewModule, imports: [CommonModule, IconModule] }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TreeviewModule, decorators: [{ type: NgModule, args: [{ declarations: [TreeViewComponent, TreeNodeComponent], exports: [TreeViewComponent, TreeNodeComponent], imports: [CommonModule, IconModule] }] }] }); /** * Generated bundle index. Do not edit. */ export { TreeNodeComponent, TreeViewComponent, TreeViewService, TreeviewModule }; //# sourceMappingURL=carbon-components-angular-treeview.mjs.map