@progress/kendo-angular-gantt
Version:
Kendo UI Angular Gantt
1,319 lines (1,318 loc) • 116 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, WatermarkOverlayComponent } 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 { NgIf } from '@angular/common';
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.
*
* @example
* ```ts-preview
* _@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>
* `
* })
* class AppComponent {
* public data: Task[] = [{
* id: 7, title: 'Validation and R&D', start: new Date('2014-06-02T00:00:00.000Z'),
* end: new Date('2014-06-19T00:00:00.000Z'), completionRatio: 0.45708333333333334,
* subtasks: [
* { id: 18, title: 'Project Kickoff', start: new Date('2014-06-02T00:00:00.000Z'),
* end: new Date('2014-06-02T00:00:00.000Z'), completionRatio: 0.23 },
* { id: 11, title: 'Research', start: new Date('2014-06-02T00:00:00.000Z'),
* end: new Date('2014-06-07T00:00:00.000Z'), completionRatio: 0.5766666666666667,
* subtasks: [
* { id: 19, title: 'Validation', start: new Date('2014-06-02T00:00:00.000Z'),
* end: new Date('2014-06-04T00:00:00.000Z'), completionRatio: 0.25 },
* { id: 39, title: 'Specification', start: new Date('2014-06-04T00:00:00.000Z'),
* end: new Date('2014-06-07T00:00:00.000Z'), completionRatio: 0.66 }]
* }, { id: 13, title: 'Implementation', start: new Date('2014-06-08T00:00:00.000Z'),
* end: new Date('2014-06-19T00:00:00.000Z'), completionRatio: 0.77,
* subtasks: [
* { id: 24, title: 'Prototype', start: new Date('2014-06-08T00:00:00.000Z'),
* end: new Date('2014-06-14T00:00:00.000Z'), completionRatio: 0.77 },
* { id: 29, title: 'UI and Interaction', start: new Date('2014-06-14T00:00:00.000Z'),
* end: new Date('2014-06-19T00:00:00.000Z'), completionRatio: 0.6 }]
* }, { id: 17, title: 'Release', start: new Date('2014-06-19T00:00:00.000Z'),
* end: new Date('2014-06-19T00:00:00.000Z'), completionRatio: 0 }]
* }];
*
* public dependencies: GanttDependency[] = [
* { id: 528, fromId: 18, toId: 19, type: DependencyType.FS },
* { id: 529, fromId: 19, toId: 39, type: DependencyType.FS },
* { id: 535, fromId: 24, toId: 29, type: DependencyType.FS },
* { id: 551, fromId: 13, toId: 29, type: DependencyType.FF },
* { id: 777, fromId: 7, toId: 11, type: DependencyType.SF },
* { id: 556, fromId: 39, toId: 24, type: DependencyType.FS },
* { id: 546, fromId: 29, toId: 17, type: DependencyType.FS },
* ];
* }
* ```
*/
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 which will be used to extract the task data from the provided `data` array items.
* The `id` field is also used as a TreeList data item unique identifier (defaults to `'id'`).
*
* > If no object is provided, the Gantt task data items will have to conform to the [`GanttTask`]({% slug api_gantt_gantttask %}) interface.
*/
set taskModelFields(fields) {
this.mapper.taskFields = fields;
}
/**
* Sets the fields which will be used to extract the dependency data from the provided `dependencies` array items.
*
* > If no object is provided, the Gantt dependency data items will have to conform to the [`GanttDependency`]({% slug api_gantt_ganttdependency %}) interface.
*/
set dependencyModelFields(fields) {
this.mapper.dependencyFields = fields;
}
/**
* A query list of all declared views.
*/
views;
/**
* 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 data of the Gantt.
*
* > The task data items should either conform to the [`GanttTask`]({% slug api_gantt_gantttask %}) interface, or a [`taskModelFields`]({% slug api_gantt_ganttcomponent %}#toc-taskmodelfields) object has to be provided.
*/
set data(data) {
this._data = normalizeGanttData(data);
this.loadTimelineData();
}
get data() {
return this._data;
}
/**
* Specifies a callback that determines if the given task is selected ([see example]({% slug selection_gantt %}#toc-custom-selection)).
*
* > The [`selectable`]({% slug api_gantt_ganttcomponent %}#toc-selectable) prop has to be set to `true` in order for this callback to be executed.
*/
isSelected = isSelected;
/**
* Specifies a callback that determines if a new dependency is valid.
* Used when evaluating if an attempt to create a new dependency will result in a valid link between the two tasks
* [see example]({% slug editing_drag_create_dependencies_gantt %}#toc-validation).
*
* By defalut, dependencies are deemed invalid when:
* - The two tasks are in a parent-child relationship.
* - The two tasks are already dependent on one another. Only one dependency is allowed per pair.
* - The start or end times of the two tasks are incompatible with the attempted dependency type.
*/
validateNewDependency = this.defaultValidateNewDependencyCallback.bind(this);
/**
* Fires when the Gantt selection is changed through user interaction.
*
* Holds data about the affected [`items`]({% slug api_gantt_selectionchangeevent %}#toc-items) and the attempted [`action`]({% slug api_gantt_selectionchangeevent %}#toc-action):
* - `select` - Triggered on `click` or `ctrl + click` on deselected items.
* - `remove` - Triggered on `ctrl + click` on selected items.
*/
selectionChange = new EventEmitter();
/**
* Enables or disables the Gantt selection mechanism ([see example]({% slug selection_gantt %}#toc-custom-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.
*/
selectable = false;
/**
* The toolbar configuration. Defines the position and content of the toolbar(s).
* The available properties are `position`, `addTaskTool`, and `viewSelectorTool`.
* All are optional and default to `top`.
*
* The possible values for each option are:
* - `top`—Positions the toolbar above the Gantt panes. Renders the respective tool in the top toolbar.
* - `bottom`—Positions the toolbar below the Gantt panes. Renders the respective tool in the bottom toolbar.
* - `both`—Displays two toolbar instances. Positions the first one above,
* and the second one - below the Gantt panes. Renders the respective tool in the both toolbars.
* - `none`—No toolbar is rendered when used for setting `position`.
* No add task or view selector tool is rendered when used for setting `addTaskTool` or `viewSelectorTool`.
*/
set toolbarSettings(value) {
this._toolbarSettings = {
position: value.position || 'top',
addTaskTool: value.addTaskTool || 'none',
viewSelectorTool: value.viewSelectorTool || 'top'
};
}
get toolbarSettings() {
return this._toolbarSettings;
}
/**
* Allows setting the toolbar(s) `aria-label` attribute value as necessary to comply with accessibility requirements.
* Typically, toolbars need an `aria-label` when there is more than one toolbar in the application.
*/
toolbarAriaLabel = 'Toolbar';
/**
* Gets or sets the callback function that retrieves the child items for a particular item.
*/
set fetchChildren(fn) {
this._fetchChildren = fn;
this.editService.fetchChildren = fn;
}
get fetchChildren() {
return this._fetchChildren;
}
/**
* Gets or sets the callback function that indicates if a particular item has child items.
*/
set hasChildren(fn) {
this._hasChildren = fn;
this.editService.hasChildren = fn;
}
get hasChildren() {
return this._hasChildren;
}
/**
* Defines the dependencies that will be drawn between the rendered tasks.
*
* > The dependency data items should either conform to the [`GanttDependency`]({% slug api_gantt_ganttdependency %}) interface, or a [`dependencyModelFields`]({% slug api_gantt_ganttcomponent %}#toc-dependencymodelfields) object has to be provided.
*/
dependencies = [];
/**
* Enables the sorting of the Gantt columns that have their `field` option set.
*/
sortable = false;
/**
* The descriptors by which the data will be sorted.
*/
sort = [];
/**
* Enables the filtering of the Gantt columns that have their `field` option set.
*
* @default false
*/
filterable = false;
/**
* The descriptor by which the data will be filtered.
*/
filter;
/**
* The start time of the work day.
* Accepts string values in the `HH:mm` format.
*/
workDayStart = '08:00';
/**
* The end time of the work day.
* Accepts string values in the `HH:mm` format.
*/
workDayEnd = '17:00';
/**
* The start of the work week (index based).
*/
workWeekStart = 1;
/**
* The end of the work week (index based).
*/
workWeekEnd = 5;
/**
* When `true`, the user can use dedicated shortcuts to interact with 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;
/**
* The options of 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 };
}
/**
* The options of 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;
}
/**
* Defines a function that is executed for every task in the component.
* The classes returned from the function are applied to the task wrapper element.
*/
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;
}
/**
* Defines a function that is executed for every data row in the component.
*/
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;
}
/**
* The name of the field which contains the unique identifier of the task data item.
* Defaults to 'id'.
*/
get taskIdField() {
return this.mapper.taskFields.id;
}
/**
* Sets the callback function that indicates if a particular item is expanded.
* If no callback is set, all items will be expanded and no expand icons will be rendered.
*/
isExpanded;
/**
* Indicates whether the Gantt columns will be resized during initialization so that they fit their headers and row content.
* Columns with autoSize set to false are excluded.
*
* @default false
*/
columnsAutoSize = false;
/**
* Specifies the Gantt current time marker settings.
* The settings will be applied for all views.
* If the `currentTimeMarker` is set for a view then it takes precedence.
*
* @default true
*/
currentTimeMarker = true;
/**
* Specifies if the column menu of the columns will be displayed.
*
* @default false
*/
columnMenu = false;
/**
* If set to true, the user can reorder columns by dragging their header cells.
*
* @default false
*/
columnsReorderable = false;
/**
* If set to true, the user can resize columns by dragging the edges (resize handles) of their header cells.
*
* @default false
*/
columnsResizable = false;
/**
* Specifies the settings for auto-scrolling during dragging
* when the pointer moves outside of the container bounderies
* [see example]({% slug editing_drag_create_dependencies_gantt %}#toc-auto-scrolling).
*/
set dragScrollSettings(settings) {
this._dragScrollSettings = { ...DEFAULT_DRAG_SCROLL_SETTINGS, ...settings };
}
get dragScrollSettings() {
return this._dragScrollSettings;
}
/**
* Allows setting the task tooltip `position`, `callout`, and `showAfter` options.
*
* @default { position: 'top', callout: true, showAfter: 100 }
*/
taskTooltipOptions = {
position: 'top',
callout: true,
showAfter: 100
};
/**
* Fires when an item is expanded.
*/
rowExpand = new EventEmitter();
/**
* Fires when a Gantt task in the timeline pane is double-clicked. The data item, associated with the clicked task,
* is available in the event data. Use the event handler to open a task editing dialog as necessary.
*/
taskDblClick = new EventEmitter();
/**
* Fires when the user double clicks a cell.
*/
cellDblClick = new EventEmitter();
/**
* Fires when the user leaves an edited cell.
*/
cellClose = new EventEmitter();
/**
* Fires when the end user clicks the `Delete` button in the task editing dialog,
* the task delete icon, or presses the `Delete` key on the keyboard when a task in the timeline is focused.
* Use the event handler to open a confirmation dialog when necessary.
*/
taskDelete = new EventEmitter();
/**
* Fires when an item 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 via dragging
* [see example]({% slug editing_drag_create_dependencies_gantt %}#toc-basic-concepts).
*/
dependencyAdd = new EventEmitter();
/**
* Fires when the sorting of the Gantt is changed.
* You have to handle the event yourself and sort the data.
*/
sortChange = new EventEmitter();
/**
* Fires when the Gantt filter is modified.
* You have to handle the event yourself and filter the data.
*/
filterChange = new EventEmitter();
/**
* Fires when the filter or sort state of the Gantt is changed.
*/
dataStateChange = new EventEmitter();
/**
* Fires when the collapsed state of the treelist pane is changed.
*/
treeListPaneCollapsedChange = new EventEmitter();
/**
* Fires when the collapsed state of the timeline pane is changed.
*/
timelinePaneCollapsedChange = new EventEmitter();
/**
* Fires each time the user resizes the timeline pane.
*/
timelinePaneSizeChange = new EventEmitter();
/**
* Fires each time the user selects a different view type. The event data contains the type of the newly selected view.
*/
activeViewChange = new EventEmitter();
/**
* Fires when the user completes the resizing of the column.
*/
columnResize = new EventEmitter();
/**
* Fires when the user completes the reordering of the column.
*/
columnReorder = new EventEmitter();
/**
* Fires when the user changes the visibility of the columns from the column menu or column 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;
_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.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 has previously been focused, the first cell of the TreeList part will receive focus,
* ([see example]({% slug keyboard_navigation_gantt %}#toc-controlling-the-focus)).
*/
focus() {
if (this.navigable) {
this.navigation.focusLastActiveItem();
}
}
/**
* Focuses the targeted cell in the TreeList part of the component,
* ([see example]({% slug keyboard_navigation_gantt %}#toc-controlling-the-focus)).
*/
focusCell(rowIndex, colIndex) {
if (this.navigable) {
this.navigation.focusCell(rowIndex, colIndex);
}
}
/**
* Focuses the targeted task in the Timeline part of the component,
* ([see example]({% slug keyboard_navigation_gantt %}#toc-controlling-the-focus)).
*/
focusTask(taskIndex) {
if (this.navigable) {
this.navigation.focusTask(taskIndex);
}
}
/**
* Applies the minimum possible width for the specified column,
* so that the whole text fits without wrapping. This method expects the Gantt
* to be resizable (set `resizable` to `true`).
* Makes sense to execute this method only after the Gantt is already populated with data.
*/
autoFitColumn(column) {
if (isPresent(this.treeList)) {
this.treeList.autoFitColumn(column);
}
}
/**
* Adjusts the width of the specified columns to fit the entire content, including headers, without wrapping.
* If no columns are specified, `autoFitColumns` is applied to all columns.
*
* This method requires the Gantt to be resizable (set `resizable` to `true`).
*/
autoFitColumns(columns = this.columns) {
if (isPresent(this.treeList)) {
this.treeList.autoFitColumns(columns);
}
}
/**
* Clears the already loaded children for the dataItem so that the Gantt will fetch them again the next time it is rendered.
*/
reload(dataItem, reloadChildren) {
if (isPresent(this.treeList)) {
this.treeList.reload(dataItem, reloadChildren);
}
}
/**
* Changes the position of the specified column.
* The reordering of columns operates only on the level
* which is inferred by the source column.
* For the `reorderColumn` method to work properly,
* the `source` column has to be visible.
*
* @param {ColumnBase} source - The column whose position will be changed.
* @param {number} destIndex - The new position of the column.
* @param {ColumnReorderConfig} options - Additional options.
*/
reorderColumn(source, destIndex, options = { before: false }) {
if (isPresent(this.treeList)) {
this.treeList.reorderColumn(source, destIndex, options);
}
}
/**
* Forces the TreeList to evaluate if some data items have changed and re-renders the rows information, if needed.
* Recalculates and re-renders the Timeline period, if needed.
* Redraws changed dependencies, if needed.
* Executes all row-related callbacks anew.
*/
updateView() {
if (isPresent(this.treeList)) {
this.treeList.updateView();
}
this.loadTimelineData();
this.dependencyDomService.notifyChanges();
}
/**
* Opens the task editing dialog.
*/
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.
*/
editCell(dataItem, column, formGroup) {
this.treeList.editCell(dataItem, column, formGroup);
}
/**
* Closes an 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 => {