UNPKG

@progress/kendo-angular-gantt

Version:
1,319 lines (1,318 loc) 116 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, 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`&mdash;Positions the toolbar above the Gantt panes. Renders the respective tool in the top toolbar. * - `bottom`&mdash;Positions the toolbar below the Gantt panes. Renders the respective tool in the bottom toolbar. * - `both`&mdash;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`&mdash;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 => {