UNPKG

@progress/kendo-angular-gantt

Version:
272 lines (271 loc) 12.9 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import { Injectable, NgZone, Renderer2 } from '@angular/core'; import { Subject } from 'rxjs'; import { ScrollSyncService } from '../scrolling/scroll-sync.service'; import { fitToRange, getClosestTaskIndex, isClearButton, isPresent, isTask } from '../utils'; import * as i0 from "@angular/core"; import * as i1 from "../scrolling/scroll-sync.service"; /** * @hidden */ export class NavigationService { zone; renderer; scrollSyncService; /** * Notifies when the tasks' focused and interactive (tabindex) state has changed. * * All tasks are rendered with tabindex="-1". * When one is clicked, or when some navigation key keyboard key is pressed, it should be focused, assigned the focus class, and its tabindex updated to 0. * All other tasks should get -1 tabindex and have the focus class removed from them. */ taskStatusChanges = new Subject(); /** * Specifies whether navigation is enabled. */ get enabled() { return this._enabled; } /** * Used to retrieve read-only data about the currently active task. */ get activeTask() { return { activeIndex: this.activeTimelineIndex, isFocused: this.isTimelineFocused }; } /** * Persists the expected Timeline focused task index. * When the cells in the TreeList are navigated through, the expected Timeline focus target should also change, * in order to allow tabbing from the TreeList to the same row in the Timeline. */ set activeTimelineIndex(index) { this._activeTimelineIndex = index; } get activeTimelineIndex() { const firstAvailableIndex = 0; const lastAvailableIndex = this.metadata.treeList.view.data.length - 1; return fitToRange(this._activeTimelineIndex, firstAvailableIndex, lastAvailableIndex); } /** * Persists the expected TreeList focused cell coords. * When the tasks in the Timeline are navigated through, the expected TreeList focus target should also change, * in order to allow back-tabbing from the Timeline to the same row in the TreeList. */ set activeTreeListCell(cell) { this._activeTreeListCell = cell; } get activeTreeListCell() { const firstAvailableIndex = 0; const lastAvailableRowIndex = this.treeListHeaderRowsCount + this.metadata.treeList.view.data.length - 1; const rowIndex = fitToRange(this._activeTreeListCell.rowIndex, firstAvailableIndex, lastAvailableRowIndex); const lastAvailableColIndex = this.metadata.columns.length; const colIndex = fitToRange(this._activeTreeListCell.colIndex, firstAvailableIndex, lastAvailableColIndex); return { rowIndex, colIndex }; } /** * Keeps track of whether the Timeline part is focused. * Used when the index of the task elements change (tasks are changed, pushed to, spliced from, etc.) * and their status should be updated accordingly. */ isTimelineFocused = false; /** * The TreeList row index takes into account the header and filter rows. * Used when translating Timeline task indices to TreeList row indices. */ get treeListHeaderRowsCount() { // captures nested group header rows + filter row if we start supporting it at some point return this.metadata.treeListElement.querySelectorAll('.k-grid-header tr').length; } /** * Keeps track of which part has last been focused. * Used when calling `gantt.focus()` to determine which part of the component should receive focus. */ treeListLastActive = false; /** * Keeps track of which part has last been focused. * Used when calling `gantt.focus()` to determine which part of the component should receive focus. */ timelineLastActive = false; metadata; _enabled = false; _activeTimelineIndex = 0; _activeTreeListCell = { rowIndex: 0, colIndex: 0 }; eventListenerDisposers; constructor(zone, renderer, scrollSyncService) { this.zone = zone; this.renderer = renderer; this.scrollSyncService = scrollSyncService; } initialize(metadata) { // no private property setters in TypeScript, so use a getter and a poorly named private prop for this value this._enabled = true; this.metadata = metadata; // TODO: fix in the splitter package and remove // move the splitbar HTML element between the two panes to keep the visial tabbing order in tact const splitbar = this.metadata.host.querySelector('.k-splitbar'); if (isPresent(splitbar) && isPresent(splitbar.previousElementSibling) && isPresent(splitbar.after)) { splitbar.after(splitbar.previousElementSibling); } this.zone.runOutsideAngular(() => { this.eventListenerDisposers = [ this.renderer.listen(this.metadata.treeListElement, 'mousedown', this.focusTreeList.bind(this)), this.renderer.listen(this.metadata.treeListElement, 'focusin', this.handleTreeListFocusIn.bind(this)), this.renderer.listen(this.metadata.timelineElement, 'mousedown', this.handleTimelineMousedown.bind(this)), this.renderer.listen(this.metadata.timelineElement, 'focusin', this.handleTimelineFocusIn.bind(this)), this.renderer.listen(this.metadata.timelineElement, 'focusout', this.handleTimelineFocusOut.bind(this)) ]; }); } ngOnDestroy() { if (isPresent(this.eventListenerDisposers)) { this.eventListenerDisposers.forEach(removeListener => removeListener()); this.eventListenerDisposers = null; } this.metadata = null; } /** * Focuses either the last active TreeList cell, or the last active Timeline task, * dependening on which of the two last held focus. * * Focuses the first TreeList cell by default. */ focusLastActiveItem() { if (this.metadata.data.length === 0 || (!this.treeListLastActive && !this.timelineLastActive)) { this.focusCell(0, 0); } else if (this.treeListLastActive) { const { rowIndex, colIndex } = this.activeTreeListCell; this.metadata.treeList.focusCell(rowIndex, colIndex); } else if (this.timelineLastActive) { this.focusTask(this.activeTimelineIndex); } } /** * Focuses the targeted TreeList cell regardless of the last peresisted target. */ focusCell(rowIndex, colIndex) { this.activeTreeListCell = { rowIndex, colIndex }; this.activeTimelineIndex = rowIndex - this.treeListHeaderRowsCount; this.metadata.treeList.focusCell(this.activeTreeListCell.rowIndex, this.activeTreeListCell.colIndex); } /** * Focuses the targeted Timeline task regardless of the last peresisted target. */ focusTask(index) { this.activeTimelineIndex = index; this.isTimelineFocused = true; this.activeTreeListCell = { rowIndex: index + this.treeListHeaderRowsCount, colIndex: this.activeTreeListCell.colIndex }; this.notifyTaskStatusChange(); } /** * Updates the focus target flags and notifies the active task to update its focused state. */ handleTimelineFocusIn({ target }) { this.treeListLastActive = false; this.timelineLastActive = true; this.isTimelineFocused = true; if (isTask(target, this.metadata.timelineElement)) { this.notifyTaskStatusChange(); } } /** * Updates the timeline focus state flag and notifies the active task to update its focused state. */ handleTimelineFocusOut({ relatedTarget }) { this.isTimelineFocused = this.metadata.timelineElement.contains(relatedTarget); // update the task element only if the new focus target is not in the Timeline - focus change between tasks is handled in the focusin handler if (!isTask(relatedTarget, this.metadata.timelineElement)) { this.notifyTaskStatusChange(); } } /** * Updates the focus target flags and corrects the TreeList focus target if needed. * As the TreeList will keep its last focused cell with tabindex="0", * this methods forcefully focuses the correct cell, * when navigating in the Timeline has updated the expected TreeList focus target. */ handleTreeListFocusIn(event) { this.treeListLastActive = true; this.timelineLastActive = false; // if the previous focus target was in the TreeList, rely on its component navigation and just record the focused item index if (this.metadata.treeListElement.contains(event.relatedTarget)) { const { colIndex, rowIndex } = this.metadata.treeList.activeCell; this.activeTreeListCell = { colIndex, rowIndex }; } else { // if the previous focus target was outside the TreeList, ensure the expected focus coords are used const { rowIndex, colIndex } = this.activeTreeListCell; this.metadata.treeList.focusCell(rowIndex, colIndex); // activates the target cell even if it has tabindex="-1" } this.activeTimelineIndex = this.metadata.treeList.activeCell.dataRowIndex; this.notifyTaskStatusChange(); if (this.metadata.treeList.activeCell.dataRowIndex >= 0) { this.scrollHorizontallyToTask(); this.scrollSyncService.syncScrollTop('treelist', 'timeline'); } } updateActiveTimeLineIndex(index) { this.activeTimelineIndex = index; } updateActiveTreeListCell() { this.activeTreeListCell = { rowIndex: this.activeTimelineIndex + this.treeListHeaderRowsCount, colIndex: this.activeTreeListCell.colIndex }; } /** * Fires the `taskStatusChanges` event with active and focused status retrieved from * `this.activeTimelineIndex` and `this.isTimelineFocused`. */ notifyTaskStatusChange() { this.taskStatusChanges.next(this.activeTask); } /** * Scrolls horizontally to the beginning of the target task if the beginning of its content is not in the viewport. */ scrollHorizontallyToTask() { const index = this.activeTimelineIndex; const task = this.metadata.timelineElement.querySelectorAll('.k-task-wrap').item(index); if (!isPresent(task)) { return; } // scroll horizontally to the item if less than 200px from the beginning of its content are visible const targetVisibleWidth = 200; const isScrollBeforeTask = (this.metadata.timelineElement.clientWidth + this.metadata.timelineElement.scrollLeft) < (task.offsetLeft + targetVisibleWidth); const isScrollAfterTask = this.metadata.timelineElement.scrollLeft > task.offsetLeft; if (isScrollBeforeTask || isScrollAfterTask) { this.metadata.timelineElement.scrollLeft = task.offsetLeft; } } /** * Filters for task mousedown in the Timeline. */ handleTimelineMousedown({ target }) { if (isTask(target, this.metadata.host) && !isClearButton(target, this.metadata.host)) { const taskIndex = getClosestTaskIndex(target, this.metadata.host); this.focusTask(taskIndex); } } /** * Focus the TreeList on TreeList mousedown. * A nasty hack to trick `handleTreeListFocusIn` into regarding the previous focus target as again the TreeList. * Otherwise cell clicks are wrongly overwritten in `handleTreeListFocusIn` and the click focus target is not respected. */ focusTreeList() { this.metadata.treeList.focus(); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: NavigationService, deps: [{ token: i0.NgZone }, { token: i0.Renderer2 }, { token: i1.ScrollSyncService }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: NavigationService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: NavigationService, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: i0.NgZone }, { type: i0.Renderer2 }, { type: i1.ScrollSyncService }] });