carbon-components-angular
Version:
Next generation components
654 lines (647 loc) • 25.2 kB
JavaScript
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