/// /// Copyright 2015-2026 Micro Focus or one of its affiliates. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. /// You may obtain a copy of the License at /// /// http://www.apache.org/licenses/LICENSE-2.0 /// /// Unless required by applicable law or agreed to in writing, software /// distributed under the License is distributed on an "AS IS" BASIS, /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. /// See the License for the specific language governing permissions and /// limitations under the License. /// import { NgTemplateOutlet, AsyncPipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, ElementRef, inject, Input, OnDestroy, QueryList, ViewChild, ViewChildren, } from '@angular/core'; import { BehaviorSubject, Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { FocusIndicatorOriginDirective } from '../../../directives/accessibility/focus-indicator/focus-indicator-origin/focus-indicator-origin.directive'; import { FocusIndicatorDirective } from '../../../directives/accessibility/focus-indicator/focus-indicator.directive'; import { TabbableListItemDirective } from '../../../directives/accessibility/tabbable-list/tabbable-list-item.directive'; import { TabbableListDirective } from '../../../directives/accessibility/tabbable-list/tabbable-list.directive'; import { FocusIfDirective } from '../../../directives/focus-if/focus-if.directive'; import { ResizeDirective } from '../../../directives/resize/resize.directive'; import { IconComponent } from '../../icon/icon.component'; import { PopoverDirective } from '../../popover/popover.directive'; import { HierarchyBarNodeComponent } from '../hierarchy-bar-node/hierarchy-bar-node.component'; import { HierarchyBarPopoverItemComponent } from '../hierarchy-bar-popover-item/hierarchy-bar-popover-item.component'; import { HierarchyBarPopoverComponent } from '../hierarchy-bar-popover/hierarchy-bar-popover.component'; import { HierarchyBarService } from '../hierarchy-bar.service'; import { HierarchyBarNode } from '../interfaces/hierarchy-bar-node.interface'; @Component({ selector: 'ux-hierarchy-bar-standard', templateUrl: './hierarchy-bar-standard.component.html', changeDetection: ChangeDetectionStrategy.OnPush, imports: [ PopoverDirective, NgTemplateOutlet, FocusIndicatorDirective, FocusIndicatorOriginDirective, IconComponent, ResizeDirective, HierarchyBarNodeComponent, HierarchyBarPopoverComponent, TabbableListDirective, HierarchyBarPopoverItemComponent, TabbableListItemDirective, FocusIfDirective, AsyncPipe, ], }) export class HierarchyBarStandardComponent implements OnDestroy { readonly hierarchyBar = inject(HierarchyBarService); /** Determine the mode of the hierarchy bar */ @Input() mode: string; /** Determine read only state */ @Input() readonly: boolean; /** Get the elementRef of the node list */ @ViewChild('nodelist', { static: true }) nodelist: ElementRef; /** Get elementRefs for all the nodes */ @ViewChildren(HierarchyBarNodeComponent, { read: ElementRef }) nodes: QueryList; /** Get instances for all the nodes */ @ViewChildren(HierarchyBarNodeComponent) nodeInstances: QueryList; @ViewChildren('barNodes', { read: ElementRef }) barNodes: QueryList; /** Value in pixels to translate the visible nodes by to fill the empty space occupied by hidden nodes */ overflowTranslateOffset = 0; /** Identify which nodes are overflowing */ overflow$ = new BehaviorSubject([]); /** Determine if there is any overflow */ isOverflowing$ = new BehaviorSubject(false); /** Unsubscribe from all subscriptions when component is destroyed */ private readonly _onDestroy = new Subject(); constructor() { // subscribe to changes in the selected node - update the UI after the render this.hierarchyBar.nodes$ .pipe(takeUntil(this._onDestroy)) .subscribe(() => requestAnimationFrame(this.scrollIntoView.bind(this))); } ngOnDestroy(): void { this._onDestroy.next(); this._onDestroy.complete(); } /** * When there is overflow ensure that the rightmost * node remains in view at all times. The nodes no longer * visible should be displayed in a popover available on the * overflow indicator */ scrollIntoView(): void { if (!this.nodelist) { return; } // get the native element const { nativeElement } = this.nodelist; const isOverflowing = nativeElement.scrollWidth > nativeElement.offsetWidth; // emit whether we are overflowing or not this.isOverflowing$.next(isOverflowing); // we don't need to do anything else if there is no overflow if (!isOverflowing) { this.nodeInstances.forEach(node => (node.visible = true)); this.overflowTranslateOffset = 0; return; } let isFull: boolean = false; // find the nodes that should be visible (we start at the rightmost node) const nodes = this.nodeInstances .toArray() .reduceRight((visibleNodes, node) => { // there must always be one visible node if (visibleNodes.length === 0) { return [node]; } // if the hierarchy bar is already occupying the available space then we can skip calculations if (isFull) { // hide the node node.visible = false; return visibleNodes; } // get the cumulative width of all the visible nodes const consumedWidth = visibleNodes.reduce( (totalWidth, visibleNode) => totalWidth + visibleNode.width, 0 ); // get the width that would be consumed if this node was included const width = node.width + consumedWidth; isFull = width > nativeElement.offsetWidth; node.visible = !isFull; if (isFull) { this.overflowTranslateOffset = this.nodelist.nativeElement.clientWidth - consumedWidth; } return isFull ? visibleNodes : [...visibleNodes, node]; }, []); if (nodes.length === 1 && this.nodelist.nativeElement.offsetWidth < nodes[0].width) { this.overflowTranslateOffset = 0; } // move the scroll position to always show the last item this.nodelist.nativeElement.scrollLeft = nativeElement.scrollWidth - nativeElement.offsetWidth; // determine which nodes should be hidden const nodesHidden = this.nodeInstances.filter(node => { return !node.visible; }); this.overflow$.next(nodesHidden.map(node => node.node)); } }