@progress/kendo-angular-gantt
Version:
Kendo UI Angular Gantt
1,351 lines (1,350 loc) • 110 kB
JavaScript
/**-----------------------------------------------------------------------------------------
* 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)) {