UNPKG

@progress/kendo-angular-gantt

Version:
1,351 lines (1,350 loc) 110 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import { Component, forwardRef, HostBinding, Input, Output, EventEmitter, ViewChild, ContentChildren, ContentChild, QueryList, isDevMode, Renderer2, ElementRef, NgZone, Inject, LOCALE_ID } from '@angular/core'; import { TreeListComponent, DataBoundTreeComponent, ExpandableTreeComponent, TreeListSpacerComponent, CustomMessagesComponent } from '@progress/kendo-angular-treelist'; import { Day } from '@progress/kendo-date-math'; import { Subscription } from 'rxjs'; import { validatePackage } from '@progress/kendo-licensing'; import { packageMetadata } from './package-metadata'; import { anyChanged, EventsOutsideAngularDirective, hasObservers, Keys, shouldShowValidationUI, getLicenseMessage, WatermarkOverlayComponent, normalizeKeys } from '@progress/kendo-angular-common'; import { getIndexFromViewDigitKeyCode, isArrowUpDownKey, isExpandCollapseKey, isNavigationKey, isViewDigitKey } from './navigation/utils'; import { GanttTimelineComponent } from './timeline/gantt-timeline.component'; import { GanttColumnBase } from './columns/columns'; import { fetchChildren, hasChildren, isSelected, rowClassCallback, taskClassCallback } from './common/default-callbacks'; import { DependencyType } from './models/models'; import { L10N_PREFIX, LocalizationService } from '@progress/kendo-angular-l10n'; import { TimelineViewService } from './timeline/timeline-view.service'; import { TimelineDayViewService } from './timeline/timeline-day-view.service'; import { TimelineWeekViewService } from './timeline/timeline-week-view.service'; import { TimelineMonthViewService } from './timeline/timeline-month-view.service'; import { ScrollSyncService } from './scrolling/scroll-sync.service'; import { DependencyDomService } from './dependencies/dependency-dom.service'; import { MappingService } from './common/mapping.service'; import { OptionChangesService } from './common/option-changes.service'; import { EditService } from './editing/edit.service'; import { TimelineScrollService } from './scrolling/timeline-scroll.service'; import { GanttLocalizationService } from './localization/gantt-localization.service'; import { NavigationService } from './navigation/navigation.service'; import { areParentChild, getClosestTaskIndex, isClearButton, isColumnGroup, isPresent, isTask, isToolbar, normalizeGanttData, scrollbarWidth } from './utils'; import { DEFAULT_TIMELINE_PANE_SETTINGS, DEFAULT_TREELIST_PANE_SETTINGS } from './models/splitter-pane-options.interface'; import { GanttTaskTemplateDirective } from './template-directives/task-template.directive'; import { GanttSummaryTaskTemplateDirective } from './template-directives/summary-task-template.directive'; import { GanttTaskContentTemplateDirective } from './template-directives/task-content-template.directive'; import { ToolbarTemplateDirective } from './toolbar/toolbar-template.directive'; import { ViewBase } from './timeline/view-base'; import { getEditItem } from './editing/utils'; import { CellCloseEvent } from './models/events/cell-close-event.interface'; import { TimeLineYearViewService } from './timeline/timeline-year-view.service'; import { CurrentTimeMarkerService } from './timeline/current-time-marker.service'; import { EditDialogComponent } from './editing/edit-dialog.component'; import { ToolbarComponent } from './toolbar/toolbar.component'; import { LocalizedMessagesDirective } from './localization/localized-messages.directive'; import { SplitterComponent, SplitterPaneComponent } from '@progress/kendo-angular-layout'; import { DialogActionsComponent, DialogComponent } from '@progress/kendo-angular-dialog'; import { ButtonComponent } from '@progress/kendo-angular-buttons'; import { GanttTaskTooltipTemplateDirective } from './template-directives/task-tooltip-template.directive'; import { IntlService } from '@progress/kendo-angular-intl'; import * as i0 from "@angular/core"; import * as i1 from "./timeline/timeline-view.service"; import * as i2 from "./scrolling/scroll-sync.service"; import * as i3 from "./common/mapping.service"; import * as i4 from "./common/option-changes.service"; import * as i5 from "./dependencies/dependency-dom.service"; import * as i6 from "./editing/edit.service"; import * as i7 from "@progress/kendo-angular-l10n"; import * as i8 from "./navigation/navigation.service"; import * as i9 from "./timeline/current-time-marker.service"; import * as i10 from "@progress/kendo-angular-intl"; const TREELIST_GROUP_COLUMNS_CLASS = 'k-gantt-treelist-nested-columns'; const DEFAULT_VIEW = 'week'; const DEFAULT_DRAG_SCROLL_SETTINGS = { enabled: true, step: 3, interval: 1, threshold: 10 }; /** * Represents the [Kendo UI Gantt component for Angular](slug:overview_gantt). * * Use the Gantt component to display and manage project tasks and dependencies in a timeline view. * * @example * ```ts * import { Component } from '@angular/core'; * import { GanttComponent, DependencyType } from '@progress/kendo-angular-gantt'; * * @Component({ * selector: 'my-app', * template: ` * <kendo-gantt * [style.height.px]="500" * [kendoGanttHierarchyBinding]="data" * childrenField="subtasks" * [dependencies]="dependencies"> * <kendo-gantt-column field="title" title="Task" [width]="200" [expandable]="true"></kendo-gantt-column> * <kendo-gantt-column field="start" title="Start" format="dd-MMM-yyyy" [width]="120"></kendo-gantt-column> * <kendo-gantt-column field="end" title="End" format="dd-MMM-yyyy" [width]="120"></kendo-gantt-column> * <kendo-gantt-timeline-day-view></kendo-gantt-timeline-day-view> * <kendo-gantt-timeline-week-view></kendo-gantt-timeline-week-view> * <kendo-gantt-timeline-month-view></kendo-gantt-timeline-month-view> * </kendo-gantt> * ` * }) * export class AppComponent { * public data: Task[] = [ * { * id: 1, * title: 'Planning', * start: new Date('2024-01-01'), * end: new Date('2024-01-05'), * subtasks: [] * } * ]; * public dependencies = [ * { id: 1, fromId: 1, toId: 2, type: DependencyType.FS } * ]; * } * ``` * * @remarks * Supported children components are: * {@link GanttColumnComponent}, * {@link GanttSpanColumnComponent}, * {@link GanttColumnGroupComponent}, * {@link TimelineDayViewComponent}, * {@link TimelineWeekViewComponent}, * {@link TimelineMonthViewComponent}, * {@link TimelineYearViewComponent}, * {@link CustomMessagesComponent}. */ export class GanttComponent { timelineViewService; scrollSyncService; renderer; mapper; optionChangesService; dependencyDomService; editService; localizationService; hostElement; zone; navigation; currentTimeMarkerService; intlService; localeId; treeList; timeline; /** * @hidden * * Queries the template for a task content template declaration. * In newer Angular versions the ngIf-ed value gets evaluated after the static query is resolved. * Therefore the `static` property needs to be set to `false`. */ taskContentTemplate; /** * @hidden * * Queries the template for a task template declaration. * In newer Angular versions the ngIf-ed value gets evaluated after the static query is resolved. * Therefore the `static` property needs to be set to `false`. */ taskTemplate; /** * @hidden * * Queries the template for a task tooltip template declaration. * In newer Angular versions the ngIf-ed value gets evaluated after the static query is resolved. * Therefore the `static` property needs to be set to `false`. */ taskTooltipTemplate; /** * @hidden * * Queries the template for a task summary template declaration. * In newer Angular versions the ngIf-ed value gets evaluated after the static query is resolved. * Therefore the `static` property needs to be set to `false`. */ summaryTaskTemplate; toolbarTemplateChildren; get toolbarTemplate() { if (this._customToolbarTemplate) { return this._customToolbarTemplate; } return this.toolbarTemplateChildren ? this.toolbarTemplateChildren.first : undefined; } set toolbarTemplate(customToolbarTemplate) { this._customToolbarTemplate = customToolbarTemplate; } /** * @hidden */ roleDescription = 'Gantt Chart'; get hostRoleDescriptionAttr() { return this.roleDescription; } /** * @hidden */ role = 'application'; get hostRoleAttr() { return this.role; } hostClasses = true; get dir() { return this.direction; } /** * A query list of all declared columns. */ set columns(columns) { this._columns = columns; this.updateTreeListGroupClass(columns); } get columns() { return this._columns; } /** * Sets the fields used to extract task data from the `data` array items. * The `id` field is also used as a unique identifier for TreeList data items. * If not set, task data items must match the [`GanttTask`](slug:api_gantt_gantttask) interface. */ set taskModelFields(fields) { this.mapper.taskFields = fields; } /** * Sets the fields used to extract dependency data from the `dependencies` array items. * If not set, dependency data items must match the [`GanttDependency`](slug:api_gantt_ganttdependency) interface. */ set dependencyModelFields(fields) { this.mapper.dependencyFields = fields; } /** * A query list of all declared views. */ views; /** * Sets the active timeline view. * @default 'week' */ set activeView(view) { this._activeView = view; } get activeView() { /** * If the default/provided activeView is not among the provided views, * fallback to setting the first provided view as active */ const view = this.views.find(view => view.type === this._activeView) ? this._activeView : this.views.first.type; this.currentTimeMarkerService.activeView = view; return view; } /** * @hidden * */ get activeViewCurrentTimeMarker() { const activeViewCurrentTimeMarker = this.views.find(view => view.type === this._activeView)?.currentTimeMarker; if (activeViewCurrentTimeMarker === false) { return false; } return activeViewCurrentTimeMarker || this.currentTimeMarker; } /** * Gets or sets the Gantt data. * The task data items must match the [`GanttTask`]({% slug api_gantt_gantttask %}) interface or use [`taskModelFields`]({% slug api_gantt_ganttcomponent %}#toc-taskmodelfields). */ set data(data) { this._data = normalizeGanttData(data); this.loadTimelineData(); } get data() { return this._data; } /** * Specifies a callback to determine if a task is selected ([see example]({% slug selection_gantt %}#toc-custom-selection)). * Set [`selectable`]({% slug api_gantt_ganttcomponent %}#toc-selectable) to `true` to use this callback. */ isSelected = isSelected; /** * Specifies a callback to validate new dependencies. * Use this callback to control the valid dependencies that users can create ([see example]({% slug editing_drag_create_dependencies_gantt %}#toc-validation)). */ validateNewDependency = this.defaultValidateNewDependencyCallback.bind(this); /** * Fires when the Gantt selection changes through user interaction. * The event data contains the affected items and the action type. */ selectionChange = new EventEmitter(); /** * Enables or disables selection in the Gantt ([see example]({% slug selection_gantt %}#toc-custom-selection)). * Set to `true` to allow selection. * > When set to `true`, the [`isSelected`]({% slug api_gantt_ganttcomponent %}#toc-isselected) callback has to be provided. * > When applied, the [`SelectableDirective`]({% slug api_gantt_selectabledirective %}) sets `selectable` to `true` internally. * @default false */ selectable = false; /** * Configures the toolbar position and content. * Set `position`, `addTaskTool`, and `viewSelectorTool` as needed. */ set toolbarSettings(value) { this._toolbarSettings = { position: value.position || 'top', addTaskTool: value.addTaskTool || 'none', viewSelectorTool: value.viewSelectorTool || 'top' }; } get toolbarSettings() { return this._toolbarSettings; } /** * Sets the `aria-label` attribute value for the toolbar. * Use this to improve accessibility. * @default "Toolbar" */ toolbarAriaLabel = 'Toolbar'; /** * Sets the callback function to retrieve child items for a data item. */ set fetchChildren(fn) { this._fetchChildren = fn; this.editService.fetchChildren = fn; } get fetchChildren() { return this._fetchChildren; } /** * Sets the callback function to indicate if a data item has child items. */ set hasChildren(fn) { this._hasChildren = fn; this.editService.hasChildren = fn; } get hasChildren() { return this._hasChildren; } /** * Sets the dependencies to display between tasks. * Dependency data items must match the [`GanttDependency`]({% slug api_gantt_ganttdependency %}) interface or use [`dependencyModelFields`]({% slug api_gantt_ganttcomponent %}#toc-dependencymodelfields). */ dependencies = []; /** * Enables sorting for columns with a `field` option. */ sortable = false; /** * Sets the descriptors for sorting the data. */ sort = []; /** * Enables filtering for columns with a `field` option. * @default false */ filterable = false; /** * Sets the descriptor for filtering the data. */ filter; /** * Sets the start time of the work day in `HH:mm` format. * @default "08:00" */ workDayStart = '08:00'; /** * Sets the end time of the work day in `HH:mm` format. * @default "17:00" */ workDayEnd = '17:00'; /** * Sets the start day of the work week (index based). * @default 1 */ workWeekStart = 1; /** * Sets the end day of the work week (index based). * @default 5 */ workWeekEnd = 5; /** * Enables keyboard navigation for the Gantt. * By default, navigation is enabled for the TreeList and Timeline parts of the component ([see example]({% slug keyboard_navigation_gantt %})). * @default true */ navigable = true; /** * Sets the options for the timeline splitter pane. * By default, the pane is collapsible, resizable, not collapsed, and its size is `'50%'`. */ set timelinePaneOptions(value) { if (this._timelinePaneOptions.collapsed && !value.collapsed) { this.dependencyDomService.notifyChanges(); } this._timelinePaneOptions = { ...DEFAULT_TIMELINE_PANE_SETTINGS, ...value }; } get timelinePaneOptions() { return { ...this._timelinePaneOptions, size: this.treeListPaneOptions.collapsed ? '100%' : this._timelinePaneOptions.size }; } /** * Sets the options for the treelist splitter pane. * By default the pane is collapsible and not collapsed. */ set treeListPaneOptions(value) { this._treeListPaneOptions = { ...DEFAULT_TREELIST_PANE_SETTINGS, ...value }; } get treeListPaneOptions() { return this._treeListPaneOptions; } /** * Sets a function to apply custom CSS classes to each task. * The function receives the task data item. */ set taskClass(fn) { if (isDevMode() && typeof fn !== 'function') { throw new Error(`taskClass must be a function, but received ${JSON.stringify(fn)}.`); } this._taskClass = fn; } get taskClass() { return this._taskClass; } /** * Sets a function to apply custom CSS classes to each data row. * The function receives the row data item. */ set rowClass(fn) { if (isDevMode() && typeof fn !== 'function') { throw new Error(`rowClass must be a function, but received ${JSON.stringify(fn)}.`); } this._rowClass = fn; } get rowClass() { return this._rowClass; } /** * Gets the name of the field that contains the unique identifier for task data items. * @default "id" */ get taskIdField() { return this.mapper.taskFields.id; } /** * Sets a callback function to indicate if a data item is expanded. * If not set, all items are expanded and no expand icons are shown. */ isExpanded; /** * Enables automatic resizing of columns to fit their content. * @default false */ columnsAutoSize = false; /** * Sets the current time marker settings for the Gantt. * Applies to all views unless overridden by the settings of a particular view. * @default true */ currentTimeMarker = true; /** * Enables the column menu for all columns. * @default false */ columnMenu = false; /** * Enables reordering of the columns by dragging their header cells. * @default false */ columnsReorderable = false; /** * Enables resizing of the columns by dragging the header cell edges (resize handles). * @default false */ columnsResizable = false; /** * Defines the settings for auto-scrolling during dragging when the pointer moves outside the container ([see example](slug:editing_drag_create_dependencies_gantt#auto-scrolling)). */ set dragScrollSettings(settings) { this._dragScrollSettings = { ...DEFAULT_DRAG_SCROLL_SETTINGS, ...settings }; } get dragScrollSettings() { return this._dragScrollSettings; } /** * Sets the options for the task tooltip, such as `position`, `callout`, and `showAfter`. * @default { position: 'top', callout: true, showAfter: 100 } */ taskTooltipOptions = { position: 'top', callout: true, showAfter: 100 }; /** * Fires when a row is expanded. */ rowExpand = new EventEmitter(); /** * Fires when a Gantt task in the timeline pane is double-clicked. * The event data contains the clicked task. Use this event to open a task editing dialog if needed. */ taskDblClick = new EventEmitter(); /** * Fires when a cell is double-clicked. */ cellDblClick = new EventEmitter(); /** * Fires when an edited cell is closed. */ cellClose = new EventEmitter(); /** * Fires when the user clicks the `Delete` button in the task editing dialog, * the task delete icon, or presses the `Delete` key on a focused task. * Use this event to open a confirmation dialog if needed. */ taskDelete = new EventEmitter(); /** * Fires when a row is collapsed. */ rowCollapse = new EventEmitter(); /** * Fires when the user confirms deleting a task. */ remove = new EventEmitter(); /** * Fires when the user cancels editing a task. */ cancel = new EventEmitter(); /** * Fires when the user saves an edited task. */ save = new EventEmitter(); /** * Fires when the user adds a task. */ taskAdd = new EventEmitter(); /** * Fires when the user adds a dependency by dragging [see example]({% slug editing_drag_create_dependencies_gantt %}#toc-basic-concepts). */ dependencyAdd = new EventEmitter(); /** * Fires when there are changes in the Gantt sorting. * Handle this event to sort the data. */ sortChange = new EventEmitter(); /** * Fires when there are changes in the Gantt filtering. * Handle this event to filter the data. */ filterChange = new EventEmitter(); /** * Fires when the filter or sort state changes. */ dataStateChange = new EventEmitter(); /** * Fires when the collapsed state of the treelist pane changes. */ treeListPaneCollapsedChange = new EventEmitter(); /** * Fires when the collapsed state of the timeline pane changes. */ timelinePaneCollapsedChange = new EventEmitter(); /** * Fires when the user resizes the timeline pane. */ timelinePaneSizeChange = new EventEmitter(); /** * Fires when the user selects a different view type. * The event data contains the type of the new view. */ activeViewChange = new EventEmitter(); /** * Fires when the user completes resizing a column. */ columnResize = new EventEmitter(); /** * Fires when the user completes reordering a column. */ columnReorder = new EventEmitter(); /** * Fires when the user changes column visibility from the column menu or chooser. */ columnVisibilityChange = new EventEmitter(); /** * @hidden * * Fires when the user changes the locked state of the columns from the column menu or by reordering the columns. */ columnLockedChange = new EventEmitter(); /** * Fires when a cell is clicked. */ cellClick = new EventEmitter(); /** * Fires when a task is clicked. */ taskClick = new EventEmitter(); /** * @hidden */ get renderedTreeListItems() { if (!isPresent(this.treeList)) { return []; } return this.treeList.view.data.map(item => item.data); } /** * @hidden */ get viewItems() { if (!isPresent(this.treeList)) { return []; } return this.treeList.view.data; } /** * @hidden */ get filterMenu() { return this.filterable ? 'menu' : false; } /** * @hidden * * Specifies whether the dependency drag clues will be rendered. * Set internally by the dependency-drag-create directive. * * @default false */ renderDependencyDragClues = false; /** * @hidden */ timelineSlots; /** * @hidden */ timelineGroupSlots; /** * @hidden */ tableWidth; /** * @hidden */ get viewService() { if (!this.views || !this.views.length) { return null; } // TODO: review if this is a performance concern? return this.timelineViewService.service(this.activeView); } /** * @hidden * * Retrieves the `isSelected` callback if `selectable` is set to `true` * Otherwise returns the default callback, which always returns `false`. */ get isTaskSelected() { return this.selectable ? this.isSelected : isSelected; } /** * @hidden * * Used by the GanttExpandableDirective. */ get idGetter() { if (isPresent(this.treeList)) { return this.treeList.idGetter; } } /** * @hidden * * Used by the views selector. */ get viewTypes() { return this.views.map(view => view.type); } /** * @hidden * * Used by the GanttExpandableDirective. */ expandStateChange = new EventEmitter(); /** * @hidden */ showEditingDialog = false; /** * @hidden */ showConfirmationDialog = false; /** * @hidden */ get isInEditMode() { return this.showEditingDialog || this.showConfirmationDialog || this.treeList.isEditing(); } /** * @hidden */ showLicenseWatermark = false; /** * @hidden */ licenseMessage; _columns = new QueryList(); _data = []; _dragScrollSettings = { ...DEFAULT_DRAG_SCROLL_SETTINGS }; _timelinePaneOptions = { ...DEFAULT_TIMELINE_PANE_SETTINGS }; _treeListPaneOptions = { ...DEFAULT_TREELIST_PANE_SETTINGS }; _customToolbarTemplate; _rowClass = rowClassCallback; _taskClass = taskClassCallback; _activeView = DEFAULT_VIEW; _toolbarSettings = { position: 'top', addTaskTool: 'none', viewSelectorTool: 'top' }; _fetchChildren = fetchChildren; _hasChildren = hasChildren; lastTreeListCellClick; direction; rtl = false; editItem; optionChangesSubscriptions = new Subscription(); editServiceSubscription = new Subscription(); localizationSubscription; intlSubscription; keydownListenerDisposers; constructor(timelineViewService, scrollSyncService, renderer, mapper, optionChangesService, dependencyDomService, editService, localizationService, hostElement, zone, navigation, currentTimeMarkerService, intlService, localeId) { this.timelineViewService = timelineViewService; this.scrollSyncService = scrollSyncService; this.renderer = renderer; this.mapper = mapper; this.optionChangesService = optionChangesService; this.dependencyDomService = dependencyDomService; this.editService = editService; this.localizationService = localizationService; this.hostElement = hostElement; this.zone = zone; this.navigation = navigation; this.currentTimeMarkerService = currentTimeMarkerService; this.intlService = intlService; this.localeId = localeId; const isValid = validatePackage(packageMetadata); this.licenseMessage = getLicenseMessage(packageMetadata); this.showLicenseWatermark = shouldShowValidationUI(isValid); intlService.localeId = this.localeId; this.intlSubscription = this.intlService.changes.subscribe(() => { this.loadTimelineData(); this.activeViewChange.emit(this.activeView); }); this.optionChangesSubscriptions.add(this.optionChangesService.viewChanges.subscribe(() => { this.loadTimelineData(); })); this.optionChangesSubscriptions.add(this.optionChangesService.dateFormatChanges.subscribe(() => { this.loadTimelineData(); })); this.optionChangesSubscriptions.add(this.optionChangesService.columnChanges.subscribe(() => { this.treeList.columns.notifyOnChanges(); })); this.editService.getSelectedItem = this.getFirstSelectedItem.bind(this); this.editServiceSubscription.add(this.editService.showEditingDialog.subscribe(show => this.showEditingDialog = show)); this.editServiceSubscription.add(this.editService.taskDelete.subscribe(task => { if (hasObservers(this.taskDelete)) { this.zone.run(() => this.notifyTaskDelete(task)); } })); this.editServiceSubscription.add(this.editService.editEvent.subscribe(args => { this[args.editResultType].emit({ taskFormGroup: args.taskFormGroup, item: getEditItem(args.dataItem, this.treeList.view.data, this.mapper), dependencies: args.dependencies, sender: this }); this.showConfirmationDialog = this.showEditingDialog = false; this.editService.dataItem = this.editService.taskFormGroup = null; this.updateView(); if (this.navigable) { this.focus(); } })); this.editServiceSubscription.add(this.editService.addEvent.subscribe(args => { const selectedItem = this.getFirstSelectedItem(); this.taskAdd.emit({ actionType: args.actionType, selectedItem: selectedItem ? getEditItem(selectedItem, this.treeList.view.data, this.mapper) : null }); this.updateView(); })); this.localizationSubscription = this.localizationService.changes.subscribe(({ rtl }) => { this.rtl = rtl; this.direction = this.rtl ? 'rtl' : 'ltr'; this.currentTimeMarkerService.rtl = rtl; }); } ngOnChanges(changes) { if (anyChanged(['data', 'activeView', 'workWeekStart', 'workWeekEnd', 'workDayStart', 'workDayEnd'], changes)) { this.loadTimelineData(); } } ngAfterViewInit() { this.updateTreeListMargin(); this.zone.runOutsideAngular(() => { this.keydownListenerDisposers = this.renderer.listen(this.hostElement.nativeElement, 'keydown', this.handleKeydown.bind(this)); }); if (this.navigable) { this.navigation.initialize({ treeList: this.treeList, host: this.hostElement.nativeElement, treeListElement: this.treeList.wrapper.nativeElement, timelineElement: this.timeline.timelineContent.nativeElement, columns: this.columns, data: this.data }); } const leftContainer = this.treeList.wrapper.nativeElement.querySelector('kendo-treelist-list > div'); this.scrollSyncService.registerElement(leftContainer, 'treelist'); } ngAfterContentInit() { if (isDevMode() && this.views.length === 0) { throw new Error('No views declared for <kendo-gantt>. Please, declare at least one view.'); } this.loadTimelineData(); this.updateTreeListGroupClass(); } ngOnDestroy() { this.optionChangesSubscriptions.unsubscribe(); this.editServiceSubscription.unsubscribe(); if (this.localizationSubscription) { this.localizationSubscription.unsubscribe(); } if (this.intlSubscription) { this.intlSubscription.unsubscribe(); } if (isPresent(this.keydownListenerDisposers)) { this.keydownListenerDisposers(); this.keydownListenerDisposers = null; } } /** * Focuses the last active cell or task in the Gantt. * If no item was previously focused, focuses the first TreeList cell ([see example]({% slug keyboard_navigation_gantt %}#toc-controlling-the-focus)). */ focus() { if (this.navigable) { this.navigation.focusLastActiveItem(); } } /** * Focuses the specified cell in the TreeList ([see example]({% slug keyboard_navigation_gantt %}#toc-controlling-the-focus)). * @param rowIndex The row index. * @param colIndex The column index. */ focusCell(rowIndex, colIndex) { if (this.navigable) { this.navigation.focusCell(rowIndex, colIndex); } } /** * Focuses the specified task in the Timeline ([see example]({% slug keyboard_navigation_gantt %}#toc-controlling-the-focus)). * @param taskIndex The index of the task. */ focusTask(taskIndex) { if (this.navigable) { this.navigation.focusTask(taskIndex); } } /** * Sets the minimum width for the specified column so that its content fits. * The Gantt must be resizable. * @param column The column to auto-fit. */ autoFitColumn(column) { if (isPresent(this.treeList)) { this.treeList.autoFitColumn(column); } } /** * Adjusts the width of the specified columns to fit their content. * If no columns are specified, fits all columns. * The Gantt must be resizable to use this method. * @param columns The columns to auto-fit. */ autoFitColumns(columns = this.columns) { if (isPresent(this.treeList)) { this.treeList.autoFitColumns(columns); } } /** * Clears loaded children for the data item so the Gantt fetches them again. * @param dataItem The data item to reload. * @param reloadChildren Whether to reload children. */ reload(dataItem, reloadChildren) { if (isPresent(this.treeList)) { this.treeList.reload(dataItem, reloadChildren); } } /** * Changes the position of the specified column. * The source column must be visible. * @param source The column to move. * @param destIndex The new position index. * @param options Additional options. */ reorderColumn(source, destIndex, options = { before: false }) { if (isPresent(this.treeList)) { this.treeList.reorderColumn(source, destIndex, options); } } /** * Forces the Gantt to re-evaluate data items and re-render the rows and Timeline period, if needed. * Also redraws dependencies and executes row-related callbacks. */ updateView() { if (isPresent(this.treeList)) { this.treeList.updateView(); } this.loadTimelineData(); this.dependencyDomService.notifyChanges(); } /** * Opens the task editing dialog for the specified data item. * @param dataItem The task data item. * @param formGroup The form group for editing. */ editTask(dataItem, formGroup) { if (!this.showEditingDialog) { const taskId = this.mapper.extractFromTask(dataItem, 'id'); const dependencies = this.dependencies.filter(item => this.mapper.extractFromDependency(item, 'toId') === taskId || this.mapper.extractFromDependency(item, 'fromId') === taskId); this.editService.createEditDialog(dataItem, formGroup, dependencies); } } /** * Closes the task editing dialog. */ closeTaskDialog() { if (this.showEditingDialog) { this.editService.closeEditDialog(); } } /** * Opens the delete task confirmation dialog. */ openConfirmationDialog() { this.showConfirmationDialog = true; } /** * @hidden */ handleConfirmationDialogClose() { this.showConfirmationDialog = false; if (this.navigable) { this.focus(); } } /** * Opens a cell for editing. * @param dataItem The data item. * @param column The column index, name, or object. * @param formGroup The form group for editing. */ editCell(dataItem, column, formGroup) { this.treeList.editCell(dataItem, column, formGroup); } /** * Closes the currently edited cell. */ closeCell() { this.treeList.closeCell(); } /** * @hidden */ handleCellClose(e) { this.cellClose.emit(new CellCloseEvent({ ...e, item: this.editItem, sender: this })); this.dependencyDomService.notifyChanges(); } /** * @hidden */ onTreeListCollapsedChange(collapsed) { this.treeListPaneCollapsedChange.emit(collapsed); if (!collapsed) { this.scrollSyncService.syncScrollTop('timeline', 'treelist'); } } /** * @hidden */ onTimelineCollapsedChange(collapsed) { this.timelinePaneCollapsedChange.emit(collapsed); if (!collapsed) { this.scrollSyncService.syncScrollTop('treelist', 'timeline'); this.dependencyDomService.notifyChanges(); } } /** * @hidden */ loadTimelineData() { if (!isPresent(this.viewService)) { return; } const activeViewOptions = this.getActiveViewOptions(); this.viewService.options = { workWeekStart: this.workWeekStart, workWeekEnd: this.workWeekEnd, workDayStart: this.workDayStart, workDayEnd: this.workDayEnd, ...activeViewOptions }; this.tableWidth = this.viewService.getTableWidth(this.data); const [groupedSlots, slots] = this.viewService.getSlots(this.data); this.timelineSlots = slots; this.timelineGroupSlots = groupedSlots; } /** * @hidden */ showToolbar(position) { return this.toolbarSettings.position !== 'none' && ([position, 'both'].indexOf(this.toolbarSettings.position) > -1); } /** * @hidden */ handleColumnVisibilityChange(event) { this.columnVisibilityChange.emit(event); this.updateTreeListGroupClass(); } /** * @hidden */ onTimelinePaneSizeChange(e) { this._timelinePaneOptions.size = e; this.timelinePaneSizeChange.emit(e); } /** * @hidden */ handleTimelineRightClick(event) { const target = event.target; const gantt = this.hostElement.nativeElement; if (!isTask(target, gantt) || isClearButton(target, gantt)) { return; } if (hasObservers(this.taskClick)) { const taskIndex = getClosestTaskIndex(target, gantt); const task = this.renderedTreeListItems[taskIndex]; this.zone.run(() => this.notifyTaskClick(event, task, taskIndex)); } } /** * @hidden */ handleTimelineClick(event) { const target = event.target; const gantt = this.hostElement.nativeElement; if (!isTask(target, gantt) || isClearButton(target, gantt)) { return; } const taskIndex = getClosestTaskIndex(target, gantt); const task = this.renderedTreeListItems[taskIndex]; const selectionAction = this.getSelectionAction(event, task); if ((hasObservers(this.selectionChange) && !this.isSameSelection(selectionAction, task)) || hasObservers(this.taskClick)) { this.zone.run(() => { this.notifySelectionChange(task, selectionAction); this.notifyTaskClick(event, task, taskIndex); }); } } /** * @hidden */ handleTreeListDoubleClick(event) { if (!isPresent(this.lastTreeListCellClick) || event.target !== this.lastTreeListCellClick.originalEvent.target) { return; } this.editItem = getEditItem(this.lastTreeListCellClick.dataItem, this.treeList.view.data, this.mapper); if (hasObservers(this.cellDblClick)) { this.zone.run(() => { this.cellDblClick.emit({ column: this.lastTreeListCellClick.column, columnIndex: this.lastTreeListCellClick.columnIndex, dataItem: this.lastTreeListCellClick.dataItem, isEdited: this.lastTreeListCellClick.isEdited, originalEvent: this.lastTreeListCellClick.originalEvent, rowIndex: this.lastTreeListCellClick.rowIndex, type: 'dblclick', sender: this }); }); } } /** * @hidden */ handleTreeListSelectionChange(event) { // prevent selection change from right-click if (isPresent(this.lastTreeListCellClick) && this.lastTreeListCellClick.type === 'contextmenu') { return; } const task = event.items.map(item => item.dataItem)[0]; // single selection only currently available const action = event.action; this.notifySelectionChange(task, action); } /** * @hidden */ handleTreeListCellClick(event) { this.lastTreeListCellClick = event; this.cellClick.emit({ column: event.column, columnIndex: event.columnIndex, dataItem: event.dataItem, isEdited: event.isEdited, originalEvent: event.originalEvent, rowIndex: event.rowIndex, type: event.type, sender: this }); } /** * @hidden */ handleDeleteConfirmation() { this.editService.triggerEditEvent('remove'); } /** * @hidden */ handleTimelineMouseDown(event) { const target = event.target; const gantt = this.hostElement.nativeElement; if (!isTask(target, gantt) || isClearButton(target, gantt)) { return; } event.preventDefault(); } /** * @hidden */ handleTimelineDblClick(event) { const target = event.target; const gantt = this.hostElement.nativeElement; if (!isTask(target, gantt) || isClearButton(target, gantt)) { return; } if (hasObservers(this.taskDblClick)) { const taskIndex = getClosestTaskIndex(target, gantt); const task = this.renderedTreeListItems[taskIndex]; this.zone.run(() => this.taskDblClick.emit({ dataItem: task, originalEvent: event, sender: this, index: taskIndex, type: 'dblclick' })); } } /** * @hidden */ getText(token) { return this.localizationService.get(token); } /** * @hidden */ changeActiveView(view) { if (view !== this.activeView) { this.activeView = view; this.loadTimelineData(); this.scrollSyncService.resetTimelineScrollLeft(); this.activeViewChange.emit(view); this.currentTimeMarkerService.slots = this.timelineSlots; this.currentTimeMarkerService.rows = this.viewItems; this.currentTimeMarkerService.activeView = view; } } /** * @hidden */ notifyTaskClick(event, dataItem, itemIndex) { // simulates the TreeList `cellClick` event triggered by enter press (type: 'click') const type = event instanceof KeyboardEvent ? 'click' : event.type; this.taskClick.emit({ originalEvent: event, dataItem: dataItem, index: itemIndex, type: type, sender: this }); } /** * @hidden */ notifySelectionChange(dataItem, action) { if (this.isSameSelection(action, dataItem)) { return; } this.selectionChange.emit({ action: action, items: [dataItem], sender: this }); this.treeList.updateView(); } /** * @hidden */ notifyTaskDelete(task) { this.editService.dataItem = task; this.taskDelete.emit({ item: getEditItem(task, this.treeList.view.data, this.mapper), sender: this }); } /** * @hidden */ isSameSelection(action, dataItem) { return action === 'select' && this.isSelected(dataItem); } /** * @hidden */ getSelectionAction({ ctrlKey, metaKey }, dataItem) { const shouldToggleSelection = ctrlKey || metaKey; return (shouldToggleSelection && this.isSelected(dataItem)) ? 'remove' : 'select'; } updateTreeListGroupClass(columns = this.columns) { if (!isPresent(this.treeList)) { return; } const hasColumns = isPresent(columns) && columns.length > 0; const hasVisibleGroupedColumns = hasColumns && columns.some(column => isColumnGroup(column) && column.childrenArray.some(childColumn => childColumn.isVisible)); if (hasVisibleGroupedColumns) { this.renderer.addClass(this.treeList.wrapper.nativeElement, TREELIST_GROUP_COLUMNS_CLASS); } else { this.renderer.removeClass(this.treeList.wrapper.nativeElement, TREELIST_GROUP_COLUMNS_CLASS); } } /** * Used to hide the vertical scrollbar */ updateTreeListMargin() { const treeListContentEl = this.treeList.wrapper.nativeElement.querySelector('.k-treelist .k-grid-content'); this.renderer.setStyle(treeListContentEl, 'margin-right', `${-Math.abs(scrollbarWidth() - 1)}px`); } get activeTimelineIndex() { return this.navigation.activeTimelineIndex; } getActiveViewOptions() { if (!this.views) { return; } return this.views.find(view => view.type === this.activeView); } getFirstSelectedItem() { const isSelectedCallback = this.isSelected || isSelected; const loadedItems = this.renderedTreeListItems || []; return loadedItems.find(isSelectedCallback); } defaultValidateNewDependencyCallback(dependency) { const fromTaskId = this.mapper.extractFromDependency(dependency, 'fromId'); const toTaskId = this.mapper.extractFromDependency(dependency, 'toId'); const fromTask = this.treeList.view.data.find(task => this.mapper.extractFromTask(task.data, 'id') === fromTaskId); const toTask = this.treeList.view.data.find(task => this.mapper.extractFromTask(task.data, 'id') === toTaskId); // mark as invalid if the attempted dependency is lacking valid from- and to-tasks // or when the from- and to-tasks are actually the same task if (!isPresent(fromTask) || !isPresent(fromTask.data) || !isPresent(toTask) || !isPresent(toTask.data) || fromTask.data === toTask.data) { return false; } const tasksDependentOnOneAnother = this.dependencies.some(current => { const currentFromId = this.mapper.extractFromDependency(current, 'fromId'); const currentToId = this.mapper.extractFromDependency(current, 'toId'); return (fromTaskId === currentFromId && toTaskId === currentToId) || (toTaskId === currentFromId && fromTaskId === currentToId); }); // mark as invalid if the attempted dependency is trying to connect already dependent tasks // mark as invalid if the two tasks are in parent-child relationship if (tasksDependentOnOneAnother || areParentChild(fromTask, toTask)) { return false; } const fromTaskStart = this.mapper.extractFromTask(fromTask.data, 'start'); const fromTaskEnd = this.mapper.extractFromTask(fromTask.data, 'end'); const toTaskStart = this.mapper.extractFromTask(toTask.data, 'start'); const toTaskEnd = this.mapper.extractFromTask(toTask.data, 'end'); // if the two tasks are available to be connected via a dependency, // check if their start and end time allow for the attempted dependency type switch (this.mapper.extractFromDependency(dependency, 'type')) { // finish to finish (FF) — the from-task ends before the to-task can end case DependencyType.FF: return fromTaskEnd <= toTaskEnd; // finish to start (FS) — the from-task ends before the to-task can begin case DependencyType.FS: return fromTaskEnd <= toTaskStart; // start to finish (SF) — the from-task begins before the to-task can end case DependencyType.SF: return fromTaskStart <= toTaskEnd; // start to start (SS) — the from-task begins before the to-task can begin case DependencyType.SS: return fromTaskStart <= toTaskStart; default: return false; } } handleKeydown(event) { const { target, altKey } = event; // on some keyboards arrow keys, PageUp/Down, and Home/End are mapped to Numpad keys const code = normalizeKeys(event); const isTimelineActive = this.timeline.timelineContent.nativeElement.contains(target); if (isTimelineActive) { if (isArrowUpDownKey(code)) { const direction = code === Keys.ArrowUp ? -1 : 1; this.navigation.activeTimelineIndex = this.activeTimelineIndex + direction; this.navigation.updateActiveTreeListCell(); } else if (code === Keys.Home) { this.navigation.activeTimelineIndex = 0; this.navigation.updateActiveTreeListCell(); } else if (code === Keys.End) { const lastAvailableIndex = this.treeList.view.data.length - 1; this.navigation.activeTimelineIndex = lastAvailableIndex; this.navigation.updateActiveTreeListCell(); } if (isNavigationKey(code)) { this.navigation.scrollHorizontallyToTask(); this.scrollSyncService.syncScrollTop('timeline', 'treelist'); this.navigation.notifyTaskStatusChange(); event.preventDefault(); } if (code === Keys.Space && hasObservers(this.selectionChange)) { const task = this.renderedTreeListItems[this.activeTimelineIndex]; const selectionAction = this.getSelectionAction(event, task); if (isPresent(task) && !this.isSameSelection(selectionAction, task)) { this.zone.run(() => this.notifySelectionChange(task, selectionAction)); } event.preventDefault(); } if ((code === Keys.Enter) && hasObservers(this.taskClick)) { const task = this.renderedTreeListItems[this.activeTimelineIndex]; if (isPresent(task)) { this.zone.run(() => this.notifyTaskClick(event, task, this.activeTimelineIndex)); } event.preventDefault(); } if (isExpandCollapseKey(code, altKey)) { const task = this.renderedTreeListItems[this.activeTimelineIndex]; if (isPresent(task) && this.hasChildren(task)) {