/******************************************************************************** * Copyright (C) 2024-2025 EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the MIT License as outlined in the LICENSE file. **********************************************************************************/ import { CDTTreeItem, CDTTreeItemResource } from '../../common'; export interface TreeNavigatorProps { ref: React.RefObject; rowIndex: Map; expandedRowKeys: string[]; expand: (expanded: boolean, record: CDTTreeItem) => void; select: (record: CDTTreeItem) => void; } /** * TreeNavigator is a helper class to navigate * through a tree table. */ export class TreeNavigator { constructor(private readonly props: TreeNavigatorProps) {} public next(node: CDTTreeItem) { if (node.children && node.children.length > 0 && this.props.expandedRowKeys.includes(node.id)) { // Go deeper this.select(node.children[0]); } else { let nextNode = this.getNext(node); if (nextNode) { this.select(nextNode); } else { // Go to parent sibling recursively nextNode = this.getParentNext(node.parent); if (nextNode) { this.select(nextNode); } } } } public nextPage() { this.scrollRelative(this.visibleDomElementCount); } private getSiblings(node: CDTTreeItem): CDTTreeItem[] { return node.parent?.children?.filter(child => this.props.rowIndex.has(child.id)) ?? []; } private getNext(node: CDTTreeItem): CDTTreeItem | undefined { const siblings = this.getSiblings(node); const index = siblings.findIndex(n => n.id === node.id); return siblings[index + 1]; } private getParentNext(node: CDTTreeItem | undefined): CDTTreeItem | undefined { if (!node) return undefined; const nextSibling = this.getNext(node); if (nextSibling) { return nextSibling; } else { return this.getParentNext(node.parent); } } public previous(node: CDTTreeItem) { let prevNode = this.getPrevious(node); if (prevNode) { // Go deeper to the last child if the previous node has children and is expanded while (prevNode.children && prevNode.children.length > 0 && this.props.expandedRowKeys.includes(prevNode.id)) { prevNode = prevNode.children[prevNode.children.length - 1]; } this.select(prevNode); } else { const parent = node.parent; // Go to parent if no previous sibling if (parent && !CDTTreeItem.isRoot(parent)) { this.select(parent); } } } public previousPage() { this.scrollRelative(-(this.visibleDomElementCount - 1)); } private getPrevious(node: CDTTreeItem): CDTTreeItem | undefined { const siblings = this.getSiblings(node); const index = siblings.findIndex(n => n.id === node.id); return siblings[index - 1]; } public toggle(node: CDTTreeItem) { if (this.props.expandedRowKeys.includes(node.id)) { this.collapse(node); } else { this.expand(node); } } public expand(node: CDTTreeItem) { if (node.children && node.children.length > 0) { if (this.props.expandedRowKeys.includes(node.id)) { this.next(node); } else { this.props.expand(true, node as CDTTreeItem); } } } public collapse(node: CDTTreeItem) { if (node.children && node.children.length > 0 && this.props.expandedRowKeys.includes(node.id)) { this.props.expand(false, node as CDTTreeItem); } else if (node.parent && !CDTTreeItem.isRoot(node.parent)) { this.select(node.parent, 'absolute'); } } private select(node: CDTTreeItem, scrollMode: 'relative' | 'absolute' = 'absolute') { // Virtual scrolling may have hidden the node if (!this.isDomVisible(node)) { if (scrollMode === 'absolute') { this.scrollAbsolute(node); this.props.select(node as CDTTreeItem); } else { this.scrollRelative(-(this.visibleDomElementCount / 2)); } this.props.select(node as CDTTreeItem); // Allow the DOM to update before focusing setTimeout(() => this.getDomElement(node)?.focus(), 100); } else { this.props.select(node as CDTTreeItem); this.getDomElement(node)?.focus(); } } // ==== DOM ==== private scrollRelative(count = this.visibleDomElementCount) { const rowHeight = this.props.ref.current?.querySelector('.ant-table-row')?.clientHeight ?? 22; const body = this.props.ref.current?.querySelector('.ant-table-tbody-virtual-holder'); if (body) { body.scrollTop = Math.max(body.scrollTop + count * rowHeight, 0); } } private scrollAbsolute(node: CDTTreeItem) { const rowHeight = this.props.ref.current?.querySelector('.ant-table-row')?.clientHeight ?? 22; const body = this.props.ref.current?.querySelector('.ant-table-tbody-virtual-holder'); if (body) { const index = this.props.rowIndex.get(node.id) ?? 1; body.scrollTop = Math.max(index * rowHeight, 0); } } private getDomElement(record: CDTTreeItem) { return this.props.ref.current?.querySelector(`[data-row-key="${record.key}"]`); } private isDomVisible(record: CDTTreeItem) { return !!this.getDomElement(record); } private get visibleDomElementCount() { return this.props.ref.current?.querySelectorAll('.ant-table-row').length ?? 1; } }