@masterteam/timeline 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,1101 @@
|
|
|
1
|
+
import * as i1 from '@angular/common';
|
|
2
|
+
import { CommonModule, NgTemplateOutlet } from '@angular/common';
|
|
3
|
+
import * as i0 from '@angular/core';
|
|
4
|
+
import { inject, TemplateRef, Directive, Input, input, output, Component, signal, computed, ViewChild, model, contentChild, contentChildren } from '@angular/core';
|
|
5
|
+
import { Card } from '@masterteam/components/card';
|
|
6
|
+
import { Popover } from 'primeng/popover';
|
|
7
|
+
import { Icon } from '@masterteam/icons';
|
|
8
|
+
import * as i1$1 from '@angular/forms';
|
|
9
|
+
import { FormsModule } from '@angular/forms';
|
|
10
|
+
import { SelectField } from '@masterteam/components/select-field';
|
|
11
|
+
|
|
12
|
+
class TimelineGanttTemplateDirective {
|
|
13
|
+
templateRef = inject(TemplateRef);
|
|
14
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: TimelineGanttTemplateDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
15
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.3", type: TimelineGanttTemplateDirective, isStandalone: true, selector: "ng-template[mtTimelineGantt]", ngImport: i0 });
|
|
16
|
+
}
|
|
17
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: TimelineGanttTemplateDirective, decorators: [{
|
|
18
|
+
type: Directive,
|
|
19
|
+
args: [{
|
|
20
|
+
selector: 'ng-template[mtTimelineGantt]',
|
|
21
|
+
standalone: true,
|
|
22
|
+
}]
|
|
23
|
+
}] });
|
|
24
|
+
|
|
25
|
+
class TimelineColumnTemplateDirective {
|
|
26
|
+
key = '';
|
|
27
|
+
templateRef = inject(TemplateRef);
|
|
28
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: TimelineColumnTemplateDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
29
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.3", type: TimelineColumnTemplateDirective, isStandalone: true, selector: "ng-template[mtTimelineColumn]", inputs: { key: ["mtTimelineColumn", "key"] }, ngImport: i0 });
|
|
30
|
+
}
|
|
31
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: TimelineColumnTemplateDirective, decorators: [{
|
|
32
|
+
type: Directive,
|
|
33
|
+
args: [{
|
|
34
|
+
selector: 'ng-template[mtTimelineColumn]',
|
|
35
|
+
standalone: true,
|
|
36
|
+
}]
|
|
37
|
+
}], propDecorators: { key: [{
|
|
38
|
+
type: Input,
|
|
39
|
+
args: ['mtTimelineColumn']
|
|
40
|
+
}] } });
|
|
41
|
+
|
|
42
|
+
class TimelinePopoverTemplateDirective {
|
|
43
|
+
templateRef = inject(TemplateRef);
|
|
44
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: TimelinePopoverTemplateDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
45
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.3", type: TimelinePopoverTemplateDirective, isStandalone: true, selector: "ng-template[mtTimelinePopover]", ngImport: i0 });
|
|
46
|
+
}
|
|
47
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: TimelinePopoverTemplateDirective, decorators: [{
|
|
48
|
+
type: Directive,
|
|
49
|
+
args: [{
|
|
50
|
+
selector: 'ng-template[mtTimelinePopover]',
|
|
51
|
+
standalone: true,
|
|
52
|
+
}]
|
|
53
|
+
}] });
|
|
54
|
+
|
|
55
|
+
class TimelineProgressTemplateDirective {
|
|
56
|
+
templateRef = inject(TemplateRef);
|
|
57
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: TimelineProgressTemplateDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
58
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.3", type: TimelineProgressTemplateDirective, isStandalone: true, selector: "ng-template[mtTimelineProgress]", ngImport: i0 });
|
|
59
|
+
}
|
|
60
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: TimelineProgressTemplateDirective, decorators: [{
|
|
61
|
+
type: Directive,
|
|
62
|
+
args: [{
|
|
63
|
+
selector: 'ng-template[mtTimelineProgress]',
|
|
64
|
+
standalone: true,
|
|
65
|
+
}]
|
|
66
|
+
}] });
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Built-in details popover body used when no custom `mtTimelinePopover`
|
|
70
|
+
* template is provided by the consumer.
|
|
71
|
+
*
|
|
72
|
+
* This component is intentionally dumb/presentational: it only receives a
|
|
73
|
+
* resolved row item and exposes a close event.
|
|
74
|
+
*/
|
|
75
|
+
class TimelineDefaultPopover {
|
|
76
|
+
// Resolved timeline row currently selected by user.
|
|
77
|
+
item = input.required(...(ngDevMode ? [{ debugName: "item" }] : []));
|
|
78
|
+
// Requests parent popover host to close.
|
|
79
|
+
requestClose = output();
|
|
80
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: TimelineDefaultPopover, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
81
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.0.3", type: TimelineDefaultPopover, isStandalone: true, selector: "mt-timeline-default-popover", inputs: { item: { classPropertyName: "item", publicName: "item", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { requestClose: "requestClose" }, ngImport: i0, template: "<!-- Default details view shown inside PrimeNG popover. -->\r\n<div class=\"w-[22.5rem]\">\r\n <div class=\"mb-3 flex items-center justify-between\">\r\n <h4 class=\"text-sm font-semibold\">Level Details</h4>\r\n <button\r\n type=\"button\"\r\n class=\"rounded border border-surface px-2 py-1 text-xs\"\r\n (click)=\"requestClose.emit()\"\r\n >\r\n Close\r\n </button>\r\n </div>\r\n\r\n <div class=\"grid gap-2 text-sm sm:grid-cols-2\">\r\n <div><span class=\"font-semibold\">Name:</span> {{ item().title }}</div>\r\n <div>\r\n <span class=\"font-semibold\">Level:</span>\r\n {{ item().levelDescription }}\r\n </div>\r\n <div><span class=\"font-semibold\">Owner:</span> {{ item().owner }}</div>\r\n <div>\r\n <span class=\"font-semibold\">Status:</span>\r\n <span [style.color]=\"item().statusColor\">{{ item().statusName }}</span>\r\n </div>\r\n <div>\r\n <span class=\"font-semibold\">Progress:</span>\r\n {{ item().progressLabel }}\r\n </div>\r\n <div><span class=\"font-semibold\">Range:</span> {{ item().phase }}</div>\r\n </div>\r\n</div>\r\n" });
|
|
82
|
+
}
|
|
83
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: TimelineDefaultPopover, decorators: [{
|
|
84
|
+
type: Component,
|
|
85
|
+
args: [{ selector: 'mt-timeline-default-popover', standalone: true, template: "<!-- Default details view shown inside PrimeNG popover. -->\r\n<div class=\"w-[22.5rem]\">\r\n <div class=\"mb-3 flex items-center justify-between\">\r\n <h4 class=\"text-sm font-semibold\">Level Details</h4>\r\n <button\r\n type=\"button\"\r\n class=\"rounded border border-surface px-2 py-1 text-xs\"\r\n (click)=\"requestClose.emit()\"\r\n >\r\n Close\r\n </button>\r\n </div>\r\n\r\n <div class=\"grid gap-2 text-sm sm:grid-cols-2\">\r\n <div><span class=\"font-semibold\">Name:</span> {{ item().title }}</div>\r\n <div>\r\n <span class=\"font-semibold\">Level:</span>\r\n {{ item().levelDescription }}\r\n </div>\r\n <div><span class=\"font-semibold\">Owner:</span> {{ item().owner }}</div>\r\n <div>\r\n <span class=\"font-semibold\">Status:</span>\r\n <span [style.color]=\"item().statusColor\">{{ item().statusName }}</span>\r\n </div>\r\n <div>\r\n <span class=\"font-semibold\">Progress:</span>\r\n {{ item().progressLabel }}\r\n </div>\r\n <div><span class=\"font-semibold\">Range:</span> {{ item().phase }}</div>\r\n </div>\r\n</div>\r\n" }]
|
|
86
|
+
}], propDecorators: { item: [{ type: i0.Input, args: [{ isSignal: true, alias: "item", required: true }] }], requestClose: [{ type: i0.Output, args: ["requestClose"] }] } });
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Header renderer for the Gantt table:
|
|
90
|
+
* - left sticky metadata columns
|
|
91
|
+
* - right timeline segment columns
|
|
92
|
+
*
|
|
93
|
+
* Kept separate to reduce template and logic noise in the Gantt container.
|
|
94
|
+
*/
|
|
95
|
+
class TimelineGanttHeader {
|
|
96
|
+
monthLabels = [
|
|
97
|
+
'Jan',
|
|
98
|
+
'Feb',
|
|
99
|
+
'Mar',
|
|
100
|
+
'Apr',
|
|
101
|
+
'May',
|
|
102
|
+
'Jun',
|
|
103
|
+
'Jul',
|
|
104
|
+
'Aug',
|
|
105
|
+
'Sep',
|
|
106
|
+
'Oct',
|
|
107
|
+
'Nov',
|
|
108
|
+
'Dec',
|
|
109
|
+
];
|
|
110
|
+
timelineMode = input('quarterly', ...(ngDevMode ? [{ debugName: "timelineMode" }] : []));
|
|
111
|
+
resolvedColumns = input([], ...(ngDevMode ? [{ debugName: "resolvedColumns" }] : []));
|
|
112
|
+
orderedGanttSegments = input([], ...(ngDevMode ? [{ debugName: "orderedGanttSegments" }] : []));
|
|
113
|
+
ganttCanvasWidth = input(0, ...(ngDevMode ? [{ debugName: "ganttCanvasWidth" }] : []));
|
|
114
|
+
effectiveGanttSegmentWidthPx = input(96, ...(ngDevMode ? [{ debugName: "effectiveGanttSegmentWidthPx" }] : []));
|
|
115
|
+
renderColumns = input(true, ...(ngDevMode ? [{ debugName: "renderColumns" }] : []));
|
|
116
|
+
renderTimeline = input(true, ...(ngDevMode ? [{ debugName: "renderTimeline" }] : []));
|
|
117
|
+
// Converts monthly segments from "M N" to calendar abbreviations.
|
|
118
|
+
resolveSegmentTitle(segment) {
|
|
119
|
+
if (this.timelineMode() !== 'monthly') {
|
|
120
|
+
return segment.title;
|
|
121
|
+
}
|
|
122
|
+
const month = Number(String(segment.title).replace(/\D/g, ''));
|
|
123
|
+
return Number.isFinite(month) && month >= 1 && month <= 12
|
|
124
|
+
? this.monthLabels[month - 1]
|
|
125
|
+
: segment.title;
|
|
126
|
+
}
|
|
127
|
+
resolveHeaderAlignClass(column) {
|
|
128
|
+
return {
|
|
129
|
+
'text-start': column.position === 'start',
|
|
130
|
+
'text-center': column.position === 'center',
|
|
131
|
+
'text-end': column.position === 'end',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: TimelineGanttHeader, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
135
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.3", type: TimelineGanttHeader, isStandalone: true, selector: "mt-timeline-gantt-header", inputs: { timelineMode: { classPropertyName: "timelineMode", publicName: "timelineMode", isSignal: true, isRequired: false, transformFunction: null }, resolvedColumns: { classPropertyName: "resolvedColumns", publicName: "resolvedColumns", isSignal: true, isRequired: false, transformFunction: null }, orderedGanttSegments: { classPropertyName: "orderedGanttSegments", publicName: "orderedGanttSegments", isSignal: true, isRequired: false, transformFunction: null }, ganttCanvasWidth: { classPropertyName: "ganttCanvasWidth", publicName: "ganttCanvasWidth", isSignal: true, isRequired: false, transformFunction: null }, effectiveGanttSegmentWidthPx: { classPropertyName: "effectiveGanttSegmentWidthPx", publicName: "effectiveGanttSegmentWidthPx", isSignal: true, isRequired: false, transformFunction: null }, renderColumns: { classPropertyName: "renderColumns", publicName: "renderColumns", isSignal: true, isRequired: false, transformFunction: null }, renderTimeline: { classPropertyName: "renderTimeline", publicName: "renderTimeline", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<!-- Sticky metadata columns + timeline period header. -->\r\n<div class=\"flex border-b border-surface bg-surface-50\">\r\n <!-- Left sticky columns (title/status/custom columns). -->\r\n @if (renderColumns()) {\r\n @for (column of resolvedColumns(); track $index) {\r\n <div\r\n class=\"shrink-0 border-e border-surface bg-surface-50 p-3 text-xs font-semibold uppercase text-surface-600\"\r\n [style.width.px]=\"column.widthPx\"\r\n [ngClass]=\"resolveHeaderAlignClass(column)\"\r\n >\r\n {{ column.header }}\r\n </div>\r\n }\r\n }\r\n\r\n <!-- Time scale columns (month/quarter/half/year). -->\r\n @if (renderTimeline()) {\r\n <div\r\n class=\"flex shrink-0 items-center bg-surface-50\"\r\n [style.width.px]=\"ganttCanvasWidth()\"\r\n >\r\n @for (column of orderedGanttSegments(); track $index) {\r\n <div\r\n class=\"shrink-0 border-e border-surface px-2 py-3 text-center text-xs font-semibold uppercase text-surface-600\"\r\n [style.width.px]=\"effectiveGanttSegmentWidthPx()\"\r\n >\r\n {{ resolveSegmentTitle(column) }}\r\n @if (column.year !== undefined && column.year !== null) {\r\n {{ column.year }}\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n</div>\r\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }] });
|
|
136
|
+
}
|
|
137
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: TimelineGanttHeader, decorators: [{
|
|
138
|
+
type: Component,
|
|
139
|
+
args: [{ selector: 'mt-timeline-gantt-header', standalone: true, imports: [CommonModule], template: "<!-- Sticky metadata columns + timeline period header. -->\r\n<div class=\"flex border-b border-surface bg-surface-50\">\r\n <!-- Left sticky columns (title/status/custom columns). -->\r\n @if (renderColumns()) {\r\n @for (column of resolvedColumns(); track $index) {\r\n <div\r\n class=\"shrink-0 border-e border-surface bg-surface-50 p-3 text-xs font-semibold uppercase text-surface-600\"\r\n [style.width.px]=\"column.widthPx\"\r\n [ngClass]=\"resolveHeaderAlignClass(column)\"\r\n >\r\n {{ column.header }}\r\n </div>\r\n }\r\n }\r\n\r\n <!-- Time scale columns (month/quarter/half/year). -->\r\n @if (renderTimeline()) {\r\n <div\r\n class=\"flex shrink-0 items-center bg-surface-50\"\r\n [style.width.px]=\"ganttCanvasWidth()\"\r\n >\r\n @for (column of orderedGanttSegments(); track $index) {\r\n <div\r\n class=\"shrink-0 border-e border-surface px-2 py-3 text-center text-xs font-semibold uppercase text-surface-600\"\r\n [style.width.px]=\"effectiveGanttSegmentWidthPx()\"\r\n >\r\n {{ resolveSegmentTitle(column) }}\r\n @if (column.year !== undefined && column.year !== null) {\r\n {{ column.year }}\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n</div>\r\n" }]
|
|
140
|
+
}], propDecorators: { timelineMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "timelineMode", required: false }] }], resolvedColumns: [{ type: i0.Input, args: [{ isSignal: true, alias: "resolvedColumns", required: false }] }], orderedGanttSegments: [{ type: i0.Input, args: [{ isSignal: true, alias: "orderedGanttSegments", required: false }] }], ganttCanvasWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "ganttCanvasWidth", required: false }] }], effectiveGanttSegmentWidthPx: [{ type: i0.Input, args: [{ isSignal: true, alias: "effectiveGanttSegmentWidthPx", required: false }] }], renderColumns: [{ type: i0.Input, args: [{ isSignal: true, alias: "renderColumns", required: false }] }], renderTimeline: [{ type: i0.Input, args: [{ isSignal: true, alias: "renderTimeline", required: false }] }] } });
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Single-row renderer for the Gantt body.
|
|
144
|
+
*
|
|
145
|
+
* Responsibilities:
|
|
146
|
+
* - Render sticky metadata cells for one row.
|
|
147
|
+
* - Render progress lane (default or custom progress template).
|
|
148
|
+
* - Emit row-level events (collapse + progress selection).
|
|
149
|
+
*
|
|
150
|
+
* Keep heavy computations out of this component. Parent components should pass
|
|
151
|
+
* already-resolved row geometry and style values.
|
|
152
|
+
*/
|
|
153
|
+
class TimelineGanttRow {
|
|
154
|
+
item = input.required(...(ngDevMode ? [{ debugName: "item" }] : []));
|
|
155
|
+
resolvedColumns = input([], ...(ngDevMode ? [{ debugName: "resolvedColumns" }] : []));
|
|
156
|
+
ganttCanvasWidth = input(0, ...(ngDevMode ? [{ debugName: "ganttCanvasWidth" }] : []));
|
|
157
|
+
renderColumns = input(true, ...(ngDevMode ? [{ debugName: "renderColumns" }] : []));
|
|
158
|
+
renderTimeline = input(true, ...(ngDevMode ? [{ debugName: "renderTimeline" }] : []));
|
|
159
|
+
columnTemplatesByKey = input(new Map(), ...(ngDevMode ? [{ debugName: "columnTemplatesByKey" }] : []));
|
|
160
|
+
progressTemplateDirective = input(null, ...(ngDevMode ? [{ debugName: "progressTemplateDirective" }] : []));
|
|
161
|
+
toggleCollapse = output();
|
|
162
|
+
progressClick = output();
|
|
163
|
+
// Forwards collapse toggles to container component.
|
|
164
|
+
onToggleCollapse(item) {
|
|
165
|
+
this.toggleCollapse.emit(item);
|
|
166
|
+
}
|
|
167
|
+
// Forwards progress-bar click with original mouse event for popover anchoring.
|
|
168
|
+
onProgressClick(item, event) {
|
|
169
|
+
this.progressClick.emit({ item, event });
|
|
170
|
+
}
|
|
171
|
+
collapseIcon(item) {
|
|
172
|
+
if (!item.isCollapsed) {
|
|
173
|
+
return 'arrow.chevron-down';
|
|
174
|
+
}
|
|
175
|
+
return this.isRtl() ? 'arrow.chevron-left' : 'arrow.chevron-right';
|
|
176
|
+
}
|
|
177
|
+
resolveColumnTemplate(column) {
|
|
178
|
+
return this.columnTemplatesByKey().get(column.key)?.templateRef ?? null;
|
|
179
|
+
}
|
|
180
|
+
resolveColumnText(column, item) {
|
|
181
|
+
const value = this.resolveColumnValue(column, item);
|
|
182
|
+
if (value === null || value === undefined || value === '') {
|
|
183
|
+
if (column.isTreeColumn) {
|
|
184
|
+
return item.title;
|
|
185
|
+
}
|
|
186
|
+
return '-';
|
|
187
|
+
}
|
|
188
|
+
return String(value);
|
|
189
|
+
}
|
|
190
|
+
resolveColumnValue(column, item) {
|
|
191
|
+
const source = item.source ?? item;
|
|
192
|
+
if (column.value) {
|
|
193
|
+
if (typeof column.value === 'function') {
|
|
194
|
+
return column.value({ item: source, resolved: item });
|
|
195
|
+
}
|
|
196
|
+
return this.path(source, column.value);
|
|
197
|
+
}
|
|
198
|
+
if (column.isTreeColumn) {
|
|
199
|
+
return item.title;
|
|
200
|
+
}
|
|
201
|
+
const sourceValue = this.path(source, column.key);
|
|
202
|
+
if (sourceValue !== undefined && sourceValue !== null) {
|
|
203
|
+
return sourceValue;
|
|
204
|
+
}
|
|
205
|
+
return this.path(item, column.key);
|
|
206
|
+
}
|
|
207
|
+
resolveColumnAlignClass(column) {
|
|
208
|
+
return {
|
|
209
|
+
'justify-start': column.position === 'start',
|
|
210
|
+
'justify-center': column.position === 'center',
|
|
211
|
+
'justify-end': column.position === 'end',
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
getColumnTemplateContext(column, item) {
|
|
215
|
+
const source = item.source ?? item;
|
|
216
|
+
return {
|
|
217
|
+
$implicit: source,
|
|
218
|
+
item: source,
|
|
219
|
+
resolved: item,
|
|
220
|
+
column,
|
|
221
|
+
value: this.resolveColumnValue(column, item),
|
|
222
|
+
canToggle: item.hasChildren,
|
|
223
|
+
isCollapsed: item.isCollapsed,
|
|
224
|
+
toggle: () => this.onToggleCollapse(item),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
getProgressTemplateContext(item) {
|
|
228
|
+
const source = item.source ?? item;
|
|
229
|
+
return {
|
|
230
|
+
$implicit: source,
|
|
231
|
+
item: source,
|
|
232
|
+
resolved: item,
|
|
233
|
+
startOffsetPx: item.startOffsetPx + 12,
|
|
234
|
+
trackWidthPx: item.trackWidthPx,
|
|
235
|
+
fillWidthPx: item.progressFillWidthPx,
|
|
236
|
+
progressValue: item.progressValue,
|
|
237
|
+
progressLabel: item.progressLabel,
|
|
238
|
+
color: item.statusColor,
|
|
239
|
+
trackColor: item.progressTrackColor,
|
|
240
|
+
isRtl: this.isRtl(),
|
|
241
|
+
onSelect: (event) => this.onProgressClick(item, event),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
// Reads document direction to support sticky offsets and progress alignment.
|
|
245
|
+
isRtl() {
|
|
246
|
+
if (typeof document === 'undefined') {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
const dir = document.documentElement.getAttribute('dir') ??
|
|
250
|
+
document.body?.getAttribute('dir') ??
|
|
251
|
+
'ltr';
|
|
252
|
+
return dir.toLowerCase() === 'rtl';
|
|
253
|
+
}
|
|
254
|
+
// Local safe deep accessor used by dynamic column value path mapping.
|
|
255
|
+
path(item, value) {
|
|
256
|
+
if (!value) {
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
return value
|
|
260
|
+
.replace(/\[(\d+)\]/g, '.$1')
|
|
261
|
+
.split('.')
|
|
262
|
+
.filter(Boolean)
|
|
263
|
+
.reduce((current, key) => {
|
|
264
|
+
if (current === null ||
|
|
265
|
+
current === undefined ||
|
|
266
|
+
typeof current !== 'object') {
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
return current[key];
|
|
270
|
+
}, item);
|
|
271
|
+
}
|
|
272
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: TimelineGanttRow, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
273
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.3", type: TimelineGanttRow, isStandalone: true, selector: "mt-timeline-gantt-row", inputs: { item: { classPropertyName: "item", publicName: "item", isSignal: true, isRequired: true, transformFunction: null }, resolvedColumns: { classPropertyName: "resolvedColumns", publicName: "resolvedColumns", isSignal: true, isRequired: false, transformFunction: null }, ganttCanvasWidth: { classPropertyName: "ganttCanvasWidth", publicName: "ganttCanvasWidth", isSignal: true, isRequired: false, transformFunction: null }, renderColumns: { classPropertyName: "renderColumns", publicName: "renderColumns", isSignal: true, isRequired: false, transformFunction: null }, renderTimeline: { classPropertyName: "renderTimeline", publicName: "renderTimeline", isSignal: true, isRequired: false, transformFunction: null }, columnTemplatesByKey: { classPropertyName: "columnTemplatesByKey", publicName: "columnTemplatesByKey", isSignal: true, isRequired: false, transformFunction: null }, progressTemplateDirective: { classPropertyName: "progressTemplateDirective", publicName: "progressTemplateDirective", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { toggleCollapse: "toggleCollapse", progressClick: "progressClick" }, ngImport: i0, template: "<!-- One gantt data row: sticky metadata cells + progress lane. -->\r\n<div class=\"relative flex\">\r\n <!-- Left sticky metadata cells. -->\r\n @if (renderColumns()) {\r\n @for (column of resolvedColumns(); track $index) {\r\n <div\r\n class=\"flex h-16 shrink-0 items-center overflow-hidden border-e border-b border-surface bg-content p-3\"\r\n [style.width.px]=\"column.widthPx\"\r\n [ngClass]=\"resolveColumnAlignClass(column)\"\r\n >\r\n <!-- Per-column custom template, when provided by consumer. -->\r\n @if (resolveColumnTemplate(column); as template) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"template\"\r\n [ngTemplateOutletContext]=\"getColumnTemplateContext(column, item())\"\r\n ></ng-container>\r\n } @else {\r\n <!-- Built-in fallback renderers by column configuration. -->\r\n @if (column.isTreeColumn) {\r\n <div\r\n class=\"flex min-w-0 items-center gap-2\"\r\n [style.padding-inline-start.px]=\"item().levelDepth * 16\"\r\n >\r\n @if (item().hasChildren) {\r\n <mt-icon\r\n class=\"cursor-pointer\"\r\n (click)=\"onToggleCollapse(item())\"\r\n [icon]=\"collapseIcon(item())\"\r\n ></mt-icon>\r\n } @else {\r\n <span class=\"inline-block w-5\"></span>\r\n }\r\n <span class=\"truncate text-sm font-semibold\">\r\n {{ resolveColumnText(column, item()) }}\r\n </span>\r\n </div>\r\n } @else {\r\n <span class=\"truncate text-sm\">\r\n {{ resolveColumnText(column, item()) }}\r\n </span>\r\n }\r\n }\r\n </div>\r\n }\r\n }\r\n\r\n <!-- Right timeline canvas cell for progress track/fill. -->\r\n @if (renderTimeline()) {\r\n <div\r\n class=\"relative z-0 h-16 shrink-0 overflow-hidden border-b border-surface px-3 py-4\"\r\n [style.width.px]=\"ganttCanvasWidth()\"\r\n [style.z-index]=\"1\"\r\n >\r\n <div class=\"h-8\"></div>\r\n <!-- Consumer custom progress template. -->\r\n @if (progressTemplateDirective(); as template) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"template.templateRef\"\r\n [ngTemplateOutletContext]=\"getProgressTemplateContext(item())\"\r\n ></ng-container>\r\n } @else {\r\n <!-- Built-in progress renderer. -->\r\n <div\r\n class=\"absolute top-1/2 h-7 -translate-y-1/2 cursor-pointer rounded-full\"\r\n [style.left.px]=\"isRtl() ? null : item().startOffsetPx + 12\"\r\n [style.right.px]=\"isRtl() ? item().startOffsetPx + 12 : null\"\r\n [style.width.px]=\"item().trackWidthPx\"\r\n [style.background-color]=\"item().progressTrackColor\"\r\n [style.z-index]=\"1\"\r\n [attr.title]=\"item().progressLabel\"\r\n (click)=\"onProgressClick(item(), $event)\"\r\n >\r\n <div\r\n class=\"relative h-full overflow-hidden rounded-full\"\r\n [style.width.px]=\"item().progressFillWidthPx\"\r\n [style.background-color]=\"item().statusColor\"\r\n >\r\n @if (item().progressFillWidthPx > 0) {\r\n <span\r\n class=\"pointer-events-none absolute top-1/2 rounded-full bg-white px-2 py-0.5 text-[10px] font-semibold text-black shadow-sm -translate-y-1/2\"\r\n [style.left.px]=\"isRtl() ? null : 8\"\r\n [style.right.px]=\"isRtl() ? 8 : null\"\r\n [attr.title]=\"item().progressLabel\"\r\n >\r\n {{ item().progressLabel }}\r\n </span>\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n }\r\n</div>\r\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: Icon, selector: "mt-icon", inputs: ["icon"] }] });
|
|
274
|
+
}
|
|
275
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: TimelineGanttRow, decorators: [{
|
|
276
|
+
type: Component,
|
|
277
|
+
args: [{ selector: 'mt-timeline-gantt-row', standalone: true, imports: [CommonModule, Icon, NgTemplateOutlet], template: "<!-- One gantt data row: sticky metadata cells + progress lane. -->\r\n<div class=\"relative flex\">\r\n <!-- Left sticky metadata cells. -->\r\n @if (renderColumns()) {\r\n @for (column of resolvedColumns(); track $index) {\r\n <div\r\n class=\"flex h-16 shrink-0 items-center overflow-hidden border-e border-b border-surface bg-content p-3\"\r\n [style.width.px]=\"column.widthPx\"\r\n [ngClass]=\"resolveColumnAlignClass(column)\"\r\n >\r\n <!-- Per-column custom template, when provided by consumer. -->\r\n @if (resolveColumnTemplate(column); as template) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"template\"\r\n [ngTemplateOutletContext]=\"getColumnTemplateContext(column, item())\"\r\n ></ng-container>\r\n } @else {\r\n <!-- Built-in fallback renderers by column configuration. -->\r\n @if (column.isTreeColumn) {\r\n <div\r\n class=\"flex min-w-0 items-center gap-2\"\r\n [style.padding-inline-start.px]=\"item().levelDepth * 16\"\r\n >\r\n @if (item().hasChildren) {\r\n <mt-icon\r\n class=\"cursor-pointer\"\r\n (click)=\"onToggleCollapse(item())\"\r\n [icon]=\"collapseIcon(item())\"\r\n ></mt-icon>\r\n } @else {\r\n <span class=\"inline-block w-5\"></span>\r\n }\r\n <span class=\"truncate text-sm font-semibold\">\r\n {{ resolveColumnText(column, item()) }}\r\n </span>\r\n </div>\r\n } @else {\r\n <span class=\"truncate text-sm\">\r\n {{ resolveColumnText(column, item()) }}\r\n </span>\r\n }\r\n }\r\n </div>\r\n }\r\n }\r\n\r\n <!-- Right timeline canvas cell for progress track/fill. -->\r\n @if (renderTimeline()) {\r\n <div\r\n class=\"relative z-0 h-16 shrink-0 overflow-hidden border-b border-surface px-3 py-4\"\r\n [style.width.px]=\"ganttCanvasWidth()\"\r\n [style.z-index]=\"1\"\r\n >\r\n <div class=\"h-8\"></div>\r\n <!-- Consumer custom progress template. -->\r\n @if (progressTemplateDirective(); as template) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"template.templateRef\"\r\n [ngTemplateOutletContext]=\"getProgressTemplateContext(item())\"\r\n ></ng-container>\r\n } @else {\r\n <!-- Built-in progress renderer. -->\r\n <div\r\n class=\"absolute top-1/2 h-7 -translate-y-1/2 cursor-pointer rounded-full\"\r\n [style.left.px]=\"isRtl() ? null : item().startOffsetPx + 12\"\r\n [style.right.px]=\"isRtl() ? item().startOffsetPx + 12 : null\"\r\n [style.width.px]=\"item().trackWidthPx\"\r\n [style.background-color]=\"item().progressTrackColor\"\r\n [style.z-index]=\"1\"\r\n [attr.title]=\"item().progressLabel\"\r\n (click)=\"onProgressClick(item(), $event)\"\r\n >\r\n <div\r\n class=\"relative h-full overflow-hidden rounded-full\"\r\n [style.width.px]=\"item().progressFillWidthPx\"\r\n [style.background-color]=\"item().statusColor\"\r\n >\r\n @if (item().progressFillWidthPx > 0) {\r\n <span\r\n class=\"pointer-events-none absolute top-1/2 rounded-full bg-white px-2 py-0.5 text-[10px] font-semibold text-black shadow-sm -translate-y-1/2\"\r\n [style.left.px]=\"isRtl() ? null : 8\"\r\n [style.right.px]=\"isRtl() ? 8 : null\"\r\n [attr.title]=\"item().progressLabel\"\r\n >\r\n {{ item().progressLabel }}\r\n </span>\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n }\r\n</div>\r\n" }]
|
|
278
|
+
}], propDecorators: { item: [{ type: i0.Input, args: [{ isSignal: true, alias: "item", required: true }] }], resolvedColumns: [{ type: i0.Input, args: [{ isSignal: true, alias: "resolvedColumns", required: false }] }], ganttCanvasWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "ganttCanvasWidth", required: false }] }], renderColumns: [{ type: i0.Input, args: [{ isSignal: true, alias: "renderColumns", required: false }] }], renderTimeline: [{ type: i0.Input, args: [{ isSignal: true, alias: "renderTimeline", required: false }] }], columnTemplatesByKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "columnTemplatesByKey", required: false }] }], progressTemplateDirective: [{ type: i0.Input, args: [{ isSignal: true, alias: "progressTemplateDirective", required: false }] }], toggleCollapse: [{ type: i0.Output, args: ["toggleCollapse"] }], progressClick: [{ type: i0.Output, args: ["progressClick"] }] } });
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Gantt view orchestrator.
|
|
282
|
+
*
|
|
283
|
+
* Responsibilities:
|
|
284
|
+
* - Resolve dynamic/legacy columns.
|
|
285
|
+
* - Compute table/canvas geometry based on viewport width + segment count.
|
|
286
|
+
* - Flatten mapped tree data into render-ready rows.
|
|
287
|
+
* - Coordinate child header/row components and re-emit row events.
|
|
288
|
+
*
|
|
289
|
+
* Non-responsibilities:
|
|
290
|
+
* - Raw data mapping from consumer models (handled by parent timeline component).
|
|
291
|
+
* - Popover state and selection management (handled by parent timeline component).
|
|
292
|
+
*/
|
|
293
|
+
class TimelineGantt {
|
|
294
|
+
set ganttSplitContainerRef(value) {
|
|
295
|
+
this.ganttSplitContainer = value;
|
|
296
|
+
this.bindSplitResizeObserver();
|
|
297
|
+
}
|
|
298
|
+
set ganttScrollViewportRef(value) {
|
|
299
|
+
this.ganttScrollViewport = value;
|
|
300
|
+
this.bindResizeObserver();
|
|
301
|
+
}
|
|
302
|
+
ganttSplitContainer;
|
|
303
|
+
ganttScrollViewport;
|
|
304
|
+
resizeObserver;
|
|
305
|
+
splitResizeObserver;
|
|
306
|
+
// Width of the visible horizontal viewport used for adaptive segment sizing.
|
|
307
|
+
ganttViewportWidth = signal(0, ...(ngDevMode ? [{ debugName: "ganttViewportWidth" }] : []));
|
|
308
|
+
ganttContainerWidth = signal(0, ...(ngDevMode ? [{ debugName: "ganttContainerWidth" }] : []));
|
|
309
|
+
splitterWidthPx = 10;
|
|
310
|
+
columnsPaneUserWidthPx = signal(null, ...(ngDevMode ? [{ debugName: "columnsPaneUserWidthPx" }] : []));
|
|
311
|
+
lastExpandedColumnsPaneWidthPx = signal(null, ...(ngDevMode ? [{ debugName: "lastExpandedColumnsPaneWidthPx" }] : []));
|
|
312
|
+
isColumnsResizing = signal(false, ...(ngDevMode ? [{ debugName: "isColumnsResizing" }] : []));
|
|
313
|
+
canResizeColumnsPane = computed(() => this.resolvedColumns().length > 0, ...(ngDevMode ? [{ debugName: "canResizeColumnsPane" }] : []));
|
|
314
|
+
resizeStartClientX = 0;
|
|
315
|
+
resizeStartColumnsPaneWidthPx = 0;
|
|
316
|
+
timelineMode = input('quarterly', ...(ngDevMode ? [{ debugName: "timelineMode" }] : []));
|
|
317
|
+
columns = input(null, ...(ngDevMode ? [{ debugName: "columns" }] : []));
|
|
318
|
+
ganttTitleColumnLabel = input('Portfolio Name', ...(ngDevMode ? [{ debugName: "ganttTitleColumnLabel" }] : []));
|
|
319
|
+
ganttStatusColumnLabel = input('Status', ...(ngDevMode ? [{ debugName: "ganttStatusColumnLabel" }] : []));
|
|
320
|
+
ganttInitiativeColumnWidthPx = input(288, ...(ngDevMode ? [{ debugName: "ganttInitiativeColumnWidthPx" }] : []));
|
|
321
|
+
ganttStatusColumnWidthPx = input(160, ...(ngDevMode ? [{ debugName: "ganttStatusColumnWidthPx" }] : []));
|
|
322
|
+
showGanttStatusColumn = input(false, ...(ngDevMode ? [{ debugName: "showGanttStatusColumn" }] : []));
|
|
323
|
+
ganttSegmentWidthPx = input(96, ...(ngDevMode ? [{ debugName: "ganttSegmentWidthPx" }] : []));
|
|
324
|
+
columnsPaneMinWidthPx = input(0, ...(ngDevMode ? [{ debugName: "columnsPaneMinWidthPx" }] : []));
|
|
325
|
+
columnsPaneMaxWidthPx = input(null, ...(ngDevMode ? [{ debugName: "columnsPaneMaxWidthPx" }] : []));
|
|
326
|
+
mappedGanttNodes = input([], ...(ngDevMode ? [{ debugName: "mappedGanttNodes" }] : []));
|
|
327
|
+
orderedGanttSegments = input([], ...(ngDevMode ? [{ debugName: "orderedGanttSegments" }] : []));
|
|
328
|
+
collapsedGanttIds = input(new Set(), ...(ngDevMode ? [{ debugName: "collapsedGanttIds" }] : []));
|
|
329
|
+
columnTemplatesByKey = input(new Map(), ...(ngDevMode ? [{ debugName: "columnTemplatesByKey" }] : []));
|
|
330
|
+
progressTemplateDirective = input(null, ...(ngDevMode ? [{ debugName: "progressTemplateDirective" }] : []));
|
|
331
|
+
toggleCollapse = output();
|
|
332
|
+
progressClick = output();
|
|
333
|
+
// Builds sticky metadata columns and computes their sticky offsets.
|
|
334
|
+
resolvedColumns = computed(() => {
|
|
335
|
+
const providedColumns = this.columns();
|
|
336
|
+
const baseColumns = providedColumns === null ? this.buildLegacyColumns() : providedColumns;
|
|
337
|
+
const resolvedTreeColumnIndex = Math.max(0, baseColumns.findIndex((column) => column.tree === true));
|
|
338
|
+
return baseColumns.map((column, index) => {
|
|
339
|
+
const widthPx = Math.max(0, Math.floor(column.widthPx ?? this.ganttInitiativeColumnWidthPx()));
|
|
340
|
+
const resolvedColumn = {
|
|
341
|
+
...column,
|
|
342
|
+
header: column.header ?? column.key,
|
|
343
|
+
widthPx,
|
|
344
|
+
position: column.position ?? 'start',
|
|
345
|
+
isTreeColumn: index === resolvedTreeColumnIndex,
|
|
346
|
+
};
|
|
347
|
+
return resolvedColumn;
|
|
348
|
+
});
|
|
349
|
+
}, ...(ngDevMode ? [{ debugName: "resolvedColumns" }] : []));
|
|
350
|
+
stickyColumnsWidth = computed(() => this.resolvedColumns().reduce((total, column) => total + column.widthPx, 0), ...(ngDevMode ? [{ debugName: "stickyColumnsWidth" }] : []));
|
|
351
|
+
resolvedColumnsPaneWidthPx = computed(() => {
|
|
352
|
+
const userWidth = this.columnsPaneUserWidthPx();
|
|
353
|
+
const preferredWidth = userWidth ?? this.stickyColumnsWidth();
|
|
354
|
+
return this.clampColumnsPaneWidth(preferredWidth);
|
|
355
|
+
}, ...(ngDevMode ? [{ debugName: "resolvedColumnsPaneWidthPx" }] : []));
|
|
356
|
+
// Segment width expands to fill viewport when possible but never goes below base width.
|
|
357
|
+
effectiveGanttSegmentWidthPx = computed(() => {
|
|
358
|
+
const segmentCount = this.orderedGanttSegments().length;
|
|
359
|
+
const baseWidth = this.ganttSegmentWidthPx();
|
|
360
|
+
if (!segmentCount) {
|
|
361
|
+
return baseWidth;
|
|
362
|
+
}
|
|
363
|
+
const availableWidth = this.ganttViewportWidth();
|
|
364
|
+
if (availableWidth <= 0) {
|
|
365
|
+
return baseWidth;
|
|
366
|
+
}
|
|
367
|
+
return Math.max(baseWidth, Math.floor(availableWidth / segmentCount));
|
|
368
|
+
}, ...(ngDevMode ? [{ debugName: "effectiveGanttSegmentWidthPx" }] : []));
|
|
369
|
+
ganttCanvasWidth = computed(() => this.orderedGanttSegments().length * this.effectiveGanttSegmentWidthPx(), ...(ngDevMode ? [{ debugName: "ganttCanvasWidth" }] : []));
|
|
370
|
+
// Flattens the mapped hierarchy into rows using current collapse state.
|
|
371
|
+
resolvedGanttItems = computed(() => {
|
|
372
|
+
const segments = this.orderedGanttSegments();
|
|
373
|
+
if (!segments.length) {
|
|
374
|
+
return [];
|
|
375
|
+
}
|
|
376
|
+
const firstSeq = segments[0].seq;
|
|
377
|
+
const lastSeq = segments[segments.length - 1].seq;
|
|
378
|
+
const out = [];
|
|
379
|
+
this.flattenMapped(this.mappedGanttNodes(), 0, segments, firstSeq, lastSeq, this.collapsedGanttIds(), out);
|
|
380
|
+
return out;
|
|
381
|
+
}, ...(ngDevMode ? [{ debugName: "resolvedGanttItems" }] : []));
|
|
382
|
+
ngAfterViewInit() {
|
|
383
|
+
this.bindResizeObserver();
|
|
384
|
+
}
|
|
385
|
+
ngOnDestroy() {
|
|
386
|
+
this.resizeObserver?.disconnect();
|
|
387
|
+
this.splitResizeObserver?.disconnect();
|
|
388
|
+
this.detachResizeListeners();
|
|
389
|
+
}
|
|
390
|
+
onToggleCollapse(item) {
|
|
391
|
+
this.toggleCollapse.emit(item);
|
|
392
|
+
}
|
|
393
|
+
// Bridges row-level click event to parent timeline component.
|
|
394
|
+
onRowProgressClick(payload) {
|
|
395
|
+
this.progressClick.emit(payload);
|
|
396
|
+
}
|
|
397
|
+
onSplitterPointerDown(event) {
|
|
398
|
+
if (event.button !== 0 || !this.canResizeColumnsPane()) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
event.preventDefault();
|
|
402
|
+
event.stopPropagation();
|
|
403
|
+
this.isColumnsResizing.set(true);
|
|
404
|
+
this.resizeStartClientX = event.clientX;
|
|
405
|
+
this.resizeStartColumnsPaneWidthPx = this.resolvedColumnsPaneWidthPx();
|
|
406
|
+
const target = event.currentTarget;
|
|
407
|
+
target?.setPointerCapture?.(event.pointerId);
|
|
408
|
+
window.addEventListener('pointermove', this.onSplitterPointerMove, {
|
|
409
|
+
passive: true,
|
|
410
|
+
});
|
|
411
|
+
window.addEventListener('pointerup', this.onSplitterPointerUp);
|
|
412
|
+
window.addEventListener('pointercancel', this.onSplitterPointerUp);
|
|
413
|
+
}
|
|
414
|
+
onSplitterKeyDown(event) {
|
|
415
|
+
if (!this.canResizeColumnsPane()) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const currentWidth = this.resolvedColumnsPaneWidthPx();
|
|
419
|
+
const direction = this.isRtl() ? -1 : 1;
|
|
420
|
+
const stepPx = event.shiftKey ? 48 : 16;
|
|
421
|
+
switch (event.key) {
|
|
422
|
+
case 'ArrowLeft': {
|
|
423
|
+
event.preventDefault();
|
|
424
|
+
this.applyColumnsPaneWidth(currentWidth - direction * stepPx);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
case 'ArrowRight': {
|
|
428
|
+
event.preventDefault();
|
|
429
|
+
this.applyColumnsPaneWidth(currentWidth + direction * stepPx);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
case 'Home': {
|
|
433
|
+
event.preventDefault();
|
|
434
|
+
this.applyColumnsPaneWidth(0);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
case 'End': {
|
|
438
|
+
event.preventDefault();
|
|
439
|
+
this.applyColumnsPaneWidth(this.columnsPaneMaxAllowedWidthPx());
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
case 'Enter':
|
|
443
|
+
case ' ': {
|
|
444
|
+
event.preventDefault();
|
|
445
|
+
this.toggleColumnsPaneCollapsed();
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
default:
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
onSplitterDoubleClick() {
|
|
453
|
+
if (!this.canResizeColumnsPane()) {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
this.toggleColumnsPaneCollapsed();
|
|
457
|
+
}
|
|
458
|
+
// Observes viewport size changes so timeline columns can adapt width.
|
|
459
|
+
bindResizeObserver() {
|
|
460
|
+
const viewport = this.ganttScrollViewport?.nativeElement;
|
|
461
|
+
if (!viewport) {
|
|
462
|
+
this.resizeObserver?.disconnect();
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
this.ganttViewportWidth.set(viewport.clientWidth);
|
|
466
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
this.resizeObserver?.disconnect();
|
|
470
|
+
this.resizeObserver = new ResizeObserver((entries) => {
|
|
471
|
+
const width = entries[0]?.contentRect.width ??
|
|
472
|
+
this.ganttScrollViewport?.nativeElement.clientWidth ??
|
|
473
|
+
0;
|
|
474
|
+
this.ganttViewportWidth.set(width);
|
|
475
|
+
});
|
|
476
|
+
this.resizeObserver.observe(viewport);
|
|
477
|
+
}
|
|
478
|
+
bindSplitResizeObserver() {
|
|
479
|
+
const container = this.ganttSplitContainer?.nativeElement;
|
|
480
|
+
if (!container) {
|
|
481
|
+
this.splitResizeObserver?.disconnect();
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
this.ganttContainerWidth.set(container.clientWidth);
|
|
485
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
this.splitResizeObserver?.disconnect();
|
|
489
|
+
this.splitResizeObserver = new ResizeObserver((entries) => {
|
|
490
|
+
const width = entries[0]?.contentRect.width ??
|
|
491
|
+
this.ganttSplitContainer?.nativeElement.clientWidth ??
|
|
492
|
+
0;
|
|
493
|
+
this.ganttContainerWidth.set(width);
|
|
494
|
+
});
|
|
495
|
+
this.splitResizeObserver.observe(container);
|
|
496
|
+
}
|
|
497
|
+
onSplitterPointerMove = (event) => {
|
|
498
|
+
if (!this.isColumnsResizing()) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
const deltaPx = this.isRtl()
|
|
502
|
+
? this.resizeStartClientX - event.clientX
|
|
503
|
+
: event.clientX - this.resizeStartClientX;
|
|
504
|
+
this.applyColumnsPaneWidth(this.resizeStartColumnsPaneWidthPx + deltaPx);
|
|
505
|
+
};
|
|
506
|
+
onSplitterPointerUp = () => {
|
|
507
|
+
if (!this.isColumnsResizing()) {
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
this.isColumnsResizing.set(false);
|
|
511
|
+
this.detachResizeListeners();
|
|
512
|
+
};
|
|
513
|
+
detachResizeListeners() {
|
|
514
|
+
window.removeEventListener('pointermove', this.onSplitterPointerMove);
|
|
515
|
+
window.removeEventListener('pointerup', this.onSplitterPointerUp);
|
|
516
|
+
window.removeEventListener('pointercancel', this.onSplitterPointerUp);
|
|
517
|
+
}
|
|
518
|
+
applyColumnsPaneWidth(widthPx) {
|
|
519
|
+
const clampedWidthPx = this.clampColumnsPaneWidth(widthPx);
|
|
520
|
+
this.columnsPaneUserWidthPx.set(clampedWidthPx);
|
|
521
|
+
if (clampedWidthPx > 0) {
|
|
522
|
+
this.lastExpandedColumnsPaneWidthPx.set(clampedWidthPx);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
toggleColumnsPaneCollapsed() {
|
|
526
|
+
const currentWidth = this.resolvedColumnsPaneWidthPx();
|
|
527
|
+
if (currentWidth > 0) {
|
|
528
|
+
this.lastExpandedColumnsPaneWidthPx.set(currentWidth);
|
|
529
|
+
this.applyColumnsPaneWidth(0);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const restoreWidth = this.lastExpandedColumnsPaneWidthPx() ?? this.stickyColumnsWidth();
|
|
533
|
+
this.applyColumnsPaneWidth(restoreWidth);
|
|
534
|
+
}
|
|
535
|
+
// Legacy fallback columns when consumer does not provide `columns`.
|
|
536
|
+
buildLegacyColumns() {
|
|
537
|
+
const columns = [
|
|
538
|
+
{
|
|
539
|
+
key: 'title',
|
|
540
|
+
header: this.ganttTitleColumnLabel(),
|
|
541
|
+
widthPx: this.ganttInitiativeColumnWidthPx(),
|
|
542
|
+
tree: true,
|
|
543
|
+
},
|
|
544
|
+
];
|
|
545
|
+
if (this.showGanttStatusColumn()) {
|
|
546
|
+
columns.push({
|
|
547
|
+
key: 'status',
|
|
548
|
+
header: this.ganttStatusColumnLabel(),
|
|
549
|
+
widthPx: this.ganttStatusColumnWidthPx(),
|
|
550
|
+
position: 'center',
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
return columns;
|
|
554
|
+
}
|
|
555
|
+
flattenMapped(nodes, levelDepth, segments, firstSeq, lastSeq, collapsed, out) {
|
|
556
|
+
// Pre-order flatten preserves parent-child visual grouping.
|
|
557
|
+
for (const node of nodes) {
|
|
558
|
+
const startSegment = this.segmentByTime(node.startAt ?? node.endAt, segments) ?? segments[0];
|
|
559
|
+
const endSegment = this.segmentByTime(node.endAt ?? node.startAt, segments) ??
|
|
560
|
+
segments[segments.length - 1];
|
|
561
|
+
const startLabel = startSegment.year !== undefined
|
|
562
|
+
? `${startSegment.title} ${startSegment.year}`
|
|
563
|
+
: startSegment.title;
|
|
564
|
+
const endLabel = endSegment.year !== undefined
|
|
565
|
+
? `${endSegment.title} ${endSegment.year}`
|
|
566
|
+
: endSegment.title;
|
|
567
|
+
const resolved = this.createResolvedItem(node, startSegment.seq, endSegment.seq, `${startLabel} - ${endLabel}`, levelDepth, collapsed, firstSeq, lastSeq);
|
|
568
|
+
out.push(resolved);
|
|
569
|
+
if (node.children.length && !resolved.isCollapsed) {
|
|
570
|
+
this.flattenMapped(node.children, levelDepth + 1, segments, firstSeq, lastSeq, collapsed, out);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
// Converts one mapped node into a fully resolved row model.
|
|
575
|
+
createResolvedItem(node, startSeqRaw, endSeqRaw, phaseFallback, levelDepth, collapsed, firstSeq, lastSeq) {
|
|
576
|
+
const startSeq = Number.isFinite(startSeqRaw) ? startSeqRaw : firstSeq;
|
|
577
|
+
const endSeq = Number.isFinite(endSeqRaw) ? endSeqRaw : lastSeq;
|
|
578
|
+
const normalizedStartSeq = Math.max(firstSeq, Math.min(startSeq, endSeq));
|
|
579
|
+
const normalizedEndSeq = Math.min(lastSeq, Math.max(startSeq, endSeq));
|
|
580
|
+
const widthSegments = normalizedEndSeq - normalizedStartSeq + 1;
|
|
581
|
+
const widthPx = Math.max(this.effectiveGanttSegmentWidthPx(), widthSegments * this.effectiveGanttSegmentWidthPx());
|
|
582
|
+
const trackWidthPx = Math.max(0, widthPx - 24);
|
|
583
|
+
const progressValue = this.clampProgress(node.progress.value);
|
|
584
|
+
const statusColor = this.resolveBaseColor(node.color ?? node.progress.color ?? null);
|
|
585
|
+
return {
|
|
586
|
+
id: node.id,
|
|
587
|
+
source: node.source,
|
|
588
|
+
title: node.title,
|
|
589
|
+
startSeq: normalizedStartSeq,
|
|
590
|
+
endSeq: normalizedEndSeq,
|
|
591
|
+
progressValue,
|
|
592
|
+
progressLabel: node.progress.label ?? `${Math.round(progressValue)}%`,
|
|
593
|
+
statusKey: node.statusKey ?? undefined,
|
|
594
|
+
statusName: node.statusName ?? node.statusKey ?? '-',
|
|
595
|
+
statusColor,
|
|
596
|
+
progressTrackColor: this.resolveTrackColor(statusColor),
|
|
597
|
+
owner: node.owner ?? '-',
|
|
598
|
+
phase: node.phase ?? phaseFallback,
|
|
599
|
+
levelDescription: node.levelDescription ?? 'Level',
|
|
600
|
+
levelColor: node.levelColor ?? '#94a3b8',
|
|
601
|
+
levelDepth,
|
|
602
|
+
hasChildren: node.children.length > 0,
|
|
603
|
+
isCollapsed: collapsed.has(node.id),
|
|
604
|
+
startOffsetPx: (normalizedStartSeq - firstSeq) * this.effectiveGanttSegmentWidthPx(),
|
|
605
|
+
trackWidthPx,
|
|
606
|
+
progressFillWidthPx: (trackWidthPx * progressValue) / 100,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
segmentByTime(value, segments) {
|
|
610
|
+
if (value === null) {
|
|
611
|
+
return undefined;
|
|
612
|
+
}
|
|
613
|
+
return segments.find((segment) => value >= segment.startAt && value <= segment.endAt);
|
|
614
|
+
}
|
|
615
|
+
// Applies fallback theme color when item does not specify one.
|
|
616
|
+
resolveBaseColor(color) {
|
|
617
|
+
return color ?? 'var(--p-primary-color)';
|
|
618
|
+
}
|
|
619
|
+
// Generates lighter track color from fill color for visual contrast.
|
|
620
|
+
resolveTrackColor(color) {
|
|
621
|
+
return `color-mix(in srgb, ${color} 22%, white)`;
|
|
622
|
+
}
|
|
623
|
+
clampProgress(value) {
|
|
624
|
+
if (!Number.isFinite(value)) {
|
|
625
|
+
return 0;
|
|
626
|
+
}
|
|
627
|
+
return Math.max(0, Math.min(100, value));
|
|
628
|
+
}
|
|
629
|
+
columnsPaneMaxAllowedWidthPx() {
|
|
630
|
+
const containerWidth = this.ganttContainerWidth();
|
|
631
|
+
if (containerWidth <= 0) {
|
|
632
|
+
return Number.POSITIVE_INFINITY;
|
|
633
|
+
}
|
|
634
|
+
const maxByContainer = Math.max(0, containerWidth - this.splitterWidthPx);
|
|
635
|
+
const configuredMax = this.columnsPaneMaxWidthPx();
|
|
636
|
+
if (configuredMax === null || configuredMax === undefined) {
|
|
637
|
+
return maxByContainer;
|
|
638
|
+
}
|
|
639
|
+
return Math.max(0, Math.min(maxByContainer, configuredMax));
|
|
640
|
+
}
|
|
641
|
+
clampColumnsPaneWidth(widthPx) {
|
|
642
|
+
if (!Number.isFinite(widthPx)) {
|
|
643
|
+
return 0;
|
|
644
|
+
}
|
|
645
|
+
const maxWidth = this.columnsPaneMaxAllowedWidthPx();
|
|
646
|
+
const minWidth = Math.max(0, this.columnsPaneMinWidthPx());
|
|
647
|
+
const effectiveMin = Math.min(minWidth, maxWidth);
|
|
648
|
+
return Math.max(effectiveMin, Math.min(widthPx, maxWidth));
|
|
649
|
+
}
|
|
650
|
+
isRtl() {
|
|
651
|
+
if (typeof document === 'undefined') {
|
|
652
|
+
return false;
|
|
653
|
+
}
|
|
654
|
+
const dir = document.documentElement.getAttribute('dir') ??
|
|
655
|
+
document.body?.getAttribute('dir') ??
|
|
656
|
+
'ltr';
|
|
657
|
+
return dir.toLowerCase() === 'rtl';
|
|
658
|
+
}
|
|
659
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: TimelineGantt, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
660
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.3", type: TimelineGantt, isStandalone: true, selector: "mt-timeline-gantt", inputs: { timelineMode: { classPropertyName: "timelineMode", publicName: "timelineMode", isSignal: true, isRequired: false, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: false, transformFunction: null }, ganttTitleColumnLabel: { classPropertyName: "ganttTitleColumnLabel", publicName: "ganttTitleColumnLabel", isSignal: true, isRequired: false, transformFunction: null }, ganttStatusColumnLabel: { classPropertyName: "ganttStatusColumnLabel", publicName: "ganttStatusColumnLabel", isSignal: true, isRequired: false, transformFunction: null }, ganttInitiativeColumnWidthPx: { classPropertyName: "ganttInitiativeColumnWidthPx", publicName: "ganttInitiativeColumnWidthPx", isSignal: true, isRequired: false, transformFunction: null }, ganttStatusColumnWidthPx: { classPropertyName: "ganttStatusColumnWidthPx", publicName: "ganttStatusColumnWidthPx", isSignal: true, isRequired: false, transformFunction: null }, showGanttStatusColumn: { classPropertyName: "showGanttStatusColumn", publicName: "showGanttStatusColumn", isSignal: true, isRequired: false, transformFunction: null }, ganttSegmentWidthPx: { classPropertyName: "ganttSegmentWidthPx", publicName: "ganttSegmentWidthPx", isSignal: true, isRequired: false, transformFunction: null }, columnsPaneMinWidthPx: { classPropertyName: "columnsPaneMinWidthPx", publicName: "columnsPaneMinWidthPx", isSignal: true, isRequired: false, transformFunction: null }, columnsPaneMaxWidthPx: { classPropertyName: "columnsPaneMaxWidthPx", publicName: "columnsPaneMaxWidthPx", isSignal: true, isRequired: false, transformFunction: null }, mappedGanttNodes: { classPropertyName: "mappedGanttNodes", publicName: "mappedGanttNodes", isSignal: true, isRequired: false, transformFunction: null }, orderedGanttSegments: { classPropertyName: "orderedGanttSegments", publicName: "orderedGanttSegments", isSignal: true, isRequired: false, transformFunction: null }, collapsedGanttIds: { classPropertyName: "collapsedGanttIds", publicName: "collapsedGanttIds", isSignal: true, isRequired: false, transformFunction: null }, columnTemplatesByKey: { classPropertyName: "columnTemplatesByKey", publicName: "columnTemplatesByKey", isSignal: true, isRequired: false, transformFunction: null }, progressTemplateDirective: { classPropertyName: "progressTemplateDirective", publicName: "progressTemplateDirective", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { toggleCollapse: "toggleCollapse", progressClick: "progressClick" }, viewQueries: [{ propertyName: "ganttSplitContainerRef", first: true, predicate: ["ganttSplitContainer"], descendants: true }, { propertyName: "ganttScrollViewportRef", first: true, predicate: ["ganttScrollViewport"], descendants: true }], ngImport: i0, template: "<!-- Gantt view shell: scroll viewport + sized timeline table. -->\r\n<div class=\"min-h-56 min-w-0 overflow-hidden p-4\">\r\n <div\r\n class=\"flex min-w-0 border border-surface bg-content\"\r\n #ganttSplitContainer\r\n >\r\n <!-- Fixed metadata columns pane. -->\r\n <div\r\n class=\"shrink-0 overflow-hidden\"\r\n [style.width.px]=\"resolvedColumnsPaneWidthPx()\"\r\n >\r\n <mt-timeline-gantt-header\r\n [timelineMode]=\"timelineMode()\"\r\n [resolvedColumns]=\"resolvedColumns()\"\r\n [orderedGanttSegments]=\"orderedGanttSegments()\"\r\n [ganttCanvasWidth]=\"0\"\r\n [effectiveGanttSegmentWidthPx]=\"effectiveGanttSegmentWidthPx()\"\r\n [renderTimeline]=\"false\"\r\n />\r\n\r\n <div>\r\n @for (item of resolvedGanttItems(); track $index) {\r\n <mt-timeline-gantt-row\r\n [item]=\"item\"\r\n [resolvedColumns]=\"resolvedColumns()\"\r\n [ganttCanvasWidth]=\"0\"\r\n [columnTemplatesByKey]=\"columnTemplatesByKey()\"\r\n [progressTemplateDirective]=\"progressTemplateDirective()\"\r\n [renderTimeline]=\"false\"\r\n (toggleCollapse)=\"onToggleCollapse($event)\"\r\n (progressClick)=\"onRowProgressClick($event)\"\r\n />\r\n }\r\n </div>\r\n </div>\r\n\r\n @if (canResizeColumnsPane()) {\r\n <button\r\n type=\"button\"\r\n class=\"mt-timeline-splitter shrink-0\"\r\n [class.mt-timeline-splitter-active]=\"isColumnsResizing()\"\r\n (pointerdown)=\"onSplitterPointerDown($event)\"\r\n (dblclick)=\"onSplitterDoubleClick()\"\r\n (keydown)=\"onSplitterKeyDown($event)\"\r\n aria-label=\"Resize columns pane\"\r\n title=\"Drag to resize. Double-click to collapse/restore.\"\r\n ></button>\r\n }\r\n\r\n <!-- Scrollable timeline pane (horizontal scroll is limited to this area). -->\r\n <div\r\n class=\"mt-timeline-scroll min-w-0 flex-1 overflow-x-auto overflow-y-hidden\"\r\n #ganttScrollViewport\r\n >\r\n <div class=\"min-w-full\" [style.width.px]=\"ganttCanvasWidth()\">\r\n <mt-timeline-gantt-header\r\n [timelineMode]=\"timelineMode()\"\r\n [resolvedColumns]=\"resolvedColumns()\"\r\n [orderedGanttSegments]=\"orderedGanttSegments()\"\r\n [ganttCanvasWidth]=\"ganttCanvasWidth()\"\r\n [effectiveGanttSegmentWidthPx]=\"effectiveGanttSegmentWidthPx()\"\r\n [renderColumns]=\"false\"\r\n />\r\n\r\n <div>\r\n @for (item of resolvedGanttItems(); track $index) {\r\n <mt-timeline-gantt-row\r\n [item]=\"item\"\r\n [resolvedColumns]=\"[]\"\r\n [ganttCanvasWidth]=\"ganttCanvasWidth()\"\r\n [columnTemplatesByKey]=\"columnTemplatesByKey()\"\r\n [progressTemplateDirective]=\"progressTemplateDirective()\"\r\n [renderColumns]=\"false\"\r\n (toggleCollapse)=\"onToggleCollapse($event)\"\r\n (progressClick)=\"onRowProgressClick($event)\"\r\n />\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n</div>\r\n", styles: [":host{display:block;min-width:0;max-width:100%}.mt-timeline-scroll{width:100%;max-width:100%;min-width:0;scrollbar-width:thin;scrollbar-color:var(--p-surface-400) transparent}.mt-timeline-scroll::-webkit-scrollbar{height:10px}.mt-timeline-scroll::-webkit-scrollbar-track{background:color-mix(in srgb,var(--p-surface-200) 30%,transparent);border-radius:9999px}.mt-timeline-scroll::-webkit-scrollbar-thumb{background:color-mix(in srgb,var(--p-surface-500) 60%,transparent);border-radius:9999px;border:2px solid transparent;background-clip:content-box}.mt-timeline-scroll::-webkit-scrollbar-thumb:hover{background:color-mix(in srgb,var(--p-surface-600) 65%,transparent);background-clip:content-box}.mt-timeline-splitter{position:relative;width:10px;min-width:10px;border:0;border-inline:1px solid color-mix(in srgb,var(--p-surface-300) 60%,transparent);background:color-mix(in srgb,var(--p-surface-50) 50%,transparent);cursor:col-resize;touch-action:none;transition:background-color .12s ease}.mt-timeline-splitter:before{content:\"\";position:absolute;top:8px;bottom:8px;inset-inline-start:50%;width:2px;transform:translate(-50%);border-radius:9999px;background:color-mix(in srgb,var(--p-surface-500) 45%,transparent);transition:background-color .12s ease}.mt-timeline-splitter:hover:before,.mt-timeline-splitter-active:before{background:color-mix(in srgb,var(--p-primary-color) 70%,var(--p-surface-400))}.mt-timeline-splitter:hover{background:color-mix(in srgb,var(--p-surface-100) 60%,transparent)}.mt-timeline-splitter:focus-visible{outline:2px solid color-mix(in srgb,var(--p-primary-color) 60%,transparent);outline-offset:-1px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: TimelineGanttHeader, selector: "mt-timeline-gantt-header", inputs: ["timelineMode", "resolvedColumns", "orderedGanttSegments", "ganttCanvasWidth", "effectiveGanttSegmentWidthPx", "renderColumns", "renderTimeline"] }, { kind: "component", type: TimelineGanttRow, selector: "mt-timeline-gantt-row", inputs: ["item", "resolvedColumns", "ganttCanvasWidth", "renderColumns", "renderTimeline", "columnTemplatesByKey", "progressTemplateDirective"], outputs: ["toggleCollapse", "progressClick"] }] });
|
|
661
|
+
}
|
|
662
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: TimelineGantt, decorators: [{
|
|
663
|
+
type: Component,
|
|
664
|
+
args: [{ selector: 'mt-timeline-gantt', standalone: true, imports: [CommonModule, TimelineGanttHeader, TimelineGanttRow], template: "<!-- Gantt view shell: scroll viewport + sized timeline table. -->\r\n<div class=\"min-h-56 min-w-0 overflow-hidden p-4\">\r\n <div\r\n class=\"flex min-w-0 border border-surface bg-content\"\r\n #ganttSplitContainer\r\n >\r\n <!-- Fixed metadata columns pane. -->\r\n <div\r\n class=\"shrink-0 overflow-hidden\"\r\n [style.width.px]=\"resolvedColumnsPaneWidthPx()\"\r\n >\r\n <mt-timeline-gantt-header\r\n [timelineMode]=\"timelineMode()\"\r\n [resolvedColumns]=\"resolvedColumns()\"\r\n [orderedGanttSegments]=\"orderedGanttSegments()\"\r\n [ganttCanvasWidth]=\"0\"\r\n [effectiveGanttSegmentWidthPx]=\"effectiveGanttSegmentWidthPx()\"\r\n [renderTimeline]=\"false\"\r\n />\r\n\r\n <div>\r\n @for (item of resolvedGanttItems(); track $index) {\r\n <mt-timeline-gantt-row\r\n [item]=\"item\"\r\n [resolvedColumns]=\"resolvedColumns()\"\r\n [ganttCanvasWidth]=\"0\"\r\n [columnTemplatesByKey]=\"columnTemplatesByKey()\"\r\n [progressTemplateDirective]=\"progressTemplateDirective()\"\r\n [renderTimeline]=\"false\"\r\n (toggleCollapse)=\"onToggleCollapse($event)\"\r\n (progressClick)=\"onRowProgressClick($event)\"\r\n />\r\n }\r\n </div>\r\n </div>\r\n\r\n @if (canResizeColumnsPane()) {\r\n <button\r\n type=\"button\"\r\n class=\"mt-timeline-splitter shrink-0\"\r\n [class.mt-timeline-splitter-active]=\"isColumnsResizing()\"\r\n (pointerdown)=\"onSplitterPointerDown($event)\"\r\n (dblclick)=\"onSplitterDoubleClick()\"\r\n (keydown)=\"onSplitterKeyDown($event)\"\r\n aria-label=\"Resize columns pane\"\r\n title=\"Drag to resize. Double-click to collapse/restore.\"\r\n ></button>\r\n }\r\n\r\n <!-- Scrollable timeline pane (horizontal scroll is limited to this area). -->\r\n <div\r\n class=\"mt-timeline-scroll min-w-0 flex-1 overflow-x-auto overflow-y-hidden\"\r\n #ganttScrollViewport\r\n >\r\n <div class=\"min-w-full\" [style.width.px]=\"ganttCanvasWidth()\">\r\n <mt-timeline-gantt-header\r\n [timelineMode]=\"timelineMode()\"\r\n [resolvedColumns]=\"resolvedColumns()\"\r\n [orderedGanttSegments]=\"orderedGanttSegments()\"\r\n [ganttCanvasWidth]=\"ganttCanvasWidth()\"\r\n [effectiveGanttSegmentWidthPx]=\"effectiveGanttSegmentWidthPx()\"\r\n [renderColumns]=\"false\"\r\n />\r\n\r\n <div>\r\n @for (item of resolvedGanttItems(); track $index) {\r\n <mt-timeline-gantt-row\r\n [item]=\"item\"\r\n [resolvedColumns]=\"[]\"\r\n [ganttCanvasWidth]=\"ganttCanvasWidth()\"\r\n [columnTemplatesByKey]=\"columnTemplatesByKey()\"\r\n [progressTemplateDirective]=\"progressTemplateDirective()\"\r\n [renderColumns]=\"false\"\r\n (toggleCollapse)=\"onToggleCollapse($event)\"\r\n (progressClick)=\"onRowProgressClick($event)\"\r\n />\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n</div>\r\n", styles: [":host{display:block;min-width:0;max-width:100%}.mt-timeline-scroll{width:100%;max-width:100%;min-width:0;scrollbar-width:thin;scrollbar-color:var(--p-surface-400) transparent}.mt-timeline-scroll::-webkit-scrollbar{height:10px}.mt-timeline-scroll::-webkit-scrollbar-track{background:color-mix(in srgb,var(--p-surface-200) 30%,transparent);border-radius:9999px}.mt-timeline-scroll::-webkit-scrollbar-thumb{background:color-mix(in srgb,var(--p-surface-500) 60%,transparent);border-radius:9999px;border:2px solid transparent;background-clip:content-box}.mt-timeline-scroll::-webkit-scrollbar-thumb:hover{background:color-mix(in srgb,var(--p-surface-600) 65%,transparent);background-clip:content-box}.mt-timeline-splitter{position:relative;width:10px;min-width:10px;border:0;border-inline:1px solid color-mix(in srgb,var(--p-surface-300) 60%,transparent);background:color-mix(in srgb,var(--p-surface-50) 50%,transparent);cursor:col-resize;touch-action:none;transition:background-color .12s ease}.mt-timeline-splitter:before{content:\"\";position:absolute;top:8px;bottom:8px;inset-inline-start:50%;width:2px;transform:translate(-50%);border-radius:9999px;background:color-mix(in srgb,var(--p-surface-500) 45%,transparent);transition:background-color .12s ease}.mt-timeline-splitter:hover:before,.mt-timeline-splitter-active:before{background:color-mix(in srgb,var(--p-primary-color) 70%,var(--p-surface-400))}.mt-timeline-splitter:hover{background:color-mix(in srgb,var(--p-surface-100) 60%,transparent)}.mt-timeline-splitter:focus-visible{outline:2px solid color-mix(in srgb,var(--p-primary-color) 60%,transparent);outline-offset:-1px}\n"] }]
|
|
665
|
+
}], propDecorators: { ganttSplitContainerRef: [{
|
|
666
|
+
type: ViewChild,
|
|
667
|
+
args: ['ganttSplitContainer']
|
|
668
|
+
}], ganttScrollViewportRef: [{
|
|
669
|
+
type: ViewChild,
|
|
670
|
+
args: ['ganttScrollViewport']
|
|
671
|
+
}], timelineMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "timelineMode", required: false }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: false }] }], ganttTitleColumnLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ganttTitleColumnLabel", required: false }] }], ganttStatusColumnLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ganttStatusColumnLabel", required: false }] }], ganttInitiativeColumnWidthPx: [{ type: i0.Input, args: [{ isSignal: true, alias: "ganttInitiativeColumnWidthPx", required: false }] }], ganttStatusColumnWidthPx: [{ type: i0.Input, args: [{ isSignal: true, alias: "ganttStatusColumnWidthPx", required: false }] }], showGanttStatusColumn: [{ type: i0.Input, args: [{ isSignal: true, alias: "showGanttStatusColumn", required: false }] }], ganttSegmentWidthPx: [{ type: i0.Input, args: [{ isSignal: true, alias: "ganttSegmentWidthPx", required: false }] }], columnsPaneMinWidthPx: [{ type: i0.Input, args: [{ isSignal: true, alias: "columnsPaneMinWidthPx", required: false }] }], columnsPaneMaxWidthPx: [{ type: i0.Input, args: [{ isSignal: true, alias: "columnsPaneMaxWidthPx", required: false }] }], mappedGanttNodes: [{ type: i0.Input, args: [{ isSignal: true, alias: "mappedGanttNodes", required: false }] }], orderedGanttSegments: [{ type: i0.Input, args: [{ isSignal: true, alias: "orderedGanttSegments", required: false }] }], collapsedGanttIds: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsedGanttIds", required: false }] }], columnTemplatesByKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "columnTemplatesByKey", required: false }] }], progressTemplateDirective: [{ type: i0.Input, args: [{ isSignal: true, alias: "progressTemplateDirective", required: false }] }], toggleCollapse: [{ type: i0.Output, args: ["toggleCollapse"] }], progressClick: [{ type: i0.Output, args: ["progressClick"] }] } });
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Shared timeline header.
|
|
675
|
+
*
|
|
676
|
+
* Responsibilities:
|
|
677
|
+
* - Render title + mode picker UI.
|
|
678
|
+
* - Emit the selected scale mode to parent orchestration.
|
|
679
|
+
*
|
|
680
|
+
* Notes for maintainers:
|
|
681
|
+
* - Keep this component presentation-focused. Business logic should remain in
|
|
682
|
+
* the parent timeline container.
|
|
683
|
+
*/
|
|
684
|
+
class TimelineHeader {
|
|
685
|
+
title = input('Timeline', ...(ngDevMode ? [{ debugName: "title" }] : []));
|
|
686
|
+
timelineMode = input('quarterly', ...(ngDevMode ? [{ debugName: "timelineMode" }] : []));
|
|
687
|
+
timelineModeOptions = input([], ...(ngDevMode ? [{ debugName: "timelineModeOptions" }] : []));
|
|
688
|
+
timelineModeChange = output();
|
|
689
|
+
// Re-emits selector changes so parent owns state updates.
|
|
690
|
+
onTimelineModeChange(mode) {
|
|
691
|
+
this.timelineModeChange.emit(mode);
|
|
692
|
+
}
|
|
693
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: TimelineHeader, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
694
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.0.3", type: TimelineHeader, isStandalone: true, selector: "mt-timeline-header", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, timelineMode: { classPropertyName: "timelineMode", publicName: "timelineMode", isSignal: true, isRequired: false, transformFunction: null }, timelineModeOptions: { classPropertyName: "timelineModeOptions", publicName: "timelineModeOptions", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { timelineModeChange: "timelineModeChange" }, ngImport: i0, template: "<!-- Timeline title + scale mode selector. -->\r\n<div class=\"flex flex-wrap items-center justify-between gap-3\">\r\n <div class=\"flex items-center gap-2\">\r\n <mt-icon icon=\"custom.timeline-point\" class=\"text-base\"></mt-icon>\r\n <h3 class=\"text-base font-semibold\">{{ title() }}</h3>\r\n </div>\r\n\r\n <!-- Scale mode switcher (monthly/quarterly/etc.). -->\r\n <div class=\"flex flex-wrap items-center justify-end gap-2\">\r\n <div class=\"w-44\">\r\n <mt-select-field\r\n [field]=\"false\"\r\n [options]=\"timelineModeOptions()\"\r\n optionLabel=\"label\"\r\n optionValue=\"value\"\r\n [showClear]=\"false\"\r\n [hasPlaceholderPrefix]=\"false\"\r\n placeholder=\"Timeline mode\"\r\n [ngModel]=\"timelineMode()\"\r\n (onChange)=\"onTimelineModeChange($event)\"\r\n />\r\n </div>\r\n </div>\r\n</div>\r\n", dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: SelectField, selector: "mt-select-field", inputs: ["field", "label", "placeholder", "hasPlaceholderPrefix", "class", "readonly", "pInputs", "options", "optionValue", "optionLabel", "filter", "filterBy", "dataKey", "showClear", "clearAfterSelect", "required", "group", "size", "optionGroupLabel", "optionGroupChildren", "loading"], outputs: ["onChange"] }, { kind: "component", type: Icon, selector: "mt-icon", inputs: ["icon"] }] });
|
|
695
|
+
}
|
|
696
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: TimelineHeader, decorators: [{
|
|
697
|
+
type: Component,
|
|
698
|
+
args: [{ selector: 'mt-timeline-header', standalone: true, imports: [FormsModule, SelectField, Icon], template: "<!-- Timeline title + scale mode selector. -->\r\n<div class=\"flex flex-wrap items-center justify-between gap-3\">\r\n <div class=\"flex items-center gap-2\">\r\n <mt-icon icon=\"custom.timeline-point\" class=\"text-base\"></mt-icon>\r\n <h3 class=\"text-base font-semibold\">{{ title() }}</h3>\r\n </div>\r\n\r\n <!-- Scale mode switcher (monthly/quarterly/etc.). -->\r\n <div class=\"flex flex-wrap items-center justify-end gap-2\">\r\n <div class=\"w-44\">\r\n <mt-select-field\r\n [field]=\"false\"\r\n [options]=\"timelineModeOptions()\"\r\n optionLabel=\"label\"\r\n optionValue=\"value\"\r\n [showClear]=\"false\"\r\n [hasPlaceholderPrefix]=\"false\"\r\n placeholder=\"Timeline mode\"\r\n [ngModel]=\"timelineMode()\"\r\n (onChange)=\"onTimelineModeChange($event)\"\r\n />\r\n </div>\r\n </div>\r\n</div>\r\n" }]
|
|
699
|
+
}], propDecorators: { title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], timelineMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "timelineMode", required: false }] }], timelineModeOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "timelineModeOptions", required: false }] }], timelineModeChange: [{ type: i0.Output, args: ["timelineModeChange"] }] } });
|
|
700
|
+
|
|
701
|
+
// Default scale options shown in the timeline mode dropdown.
|
|
702
|
+
const DEFAULT_TIMELINE_MODES = [
|
|
703
|
+
{ label: 'Monthly', value: 'monthly' },
|
|
704
|
+
{ label: 'Quarterly', value: 'quarterly' },
|
|
705
|
+
{ label: 'Semi Annual', value: 'biannually' },
|
|
706
|
+
{ label: 'Annually', value: 'annually' },
|
|
707
|
+
];
|
|
708
|
+
/**
|
|
709
|
+
* Dynamic Gantt timeline component.
|
|
710
|
+
*
|
|
711
|
+
* Data flow:
|
|
712
|
+
* 1) Raw input nodes are mapped via `ganttMapping` to `TimelineMappedNode`.
|
|
713
|
+
* 2) Timeline range is derived from min start/max end across the mapped tree.
|
|
714
|
+
* 3) Time segments are generated from range + selected scale.
|
|
715
|
+
* 4) Tree nodes are flattened into render-ready rows with pixel geometry.
|
|
716
|
+
*
|
|
717
|
+
* Column system:
|
|
718
|
+
* - `columns = null` uses legacy defaults (title + optional status).
|
|
719
|
+
* - `columns = []` renders progress-only mode with no leading columns.
|
|
720
|
+
* - `columns = [...]` renders provided columns and supports per-column templates.
|
|
721
|
+
*/
|
|
722
|
+
class Timeline {
|
|
723
|
+
// PrimeNG popover instance controlling row details overlay.
|
|
724
|
+
detailsPopover;
|
|
725
|
+
// Invisible anchor used to position popover at exact mouse click coordinates.
|
|
726
|
+
detailsPopoverClickAnchor;
|
|
727
|
+
title = input('Timeline', ...(ngDevMode ? [{ debugName: "title" }] : []));
|
|
728
|
+
emptyMessage = input('No data to display', ...(ngDevMode ? [{ debugName: "emptyMessage" }] : []));
|
|
729
|
+
isLoading = input(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
|
|
730
|
+
showHeader = input(true, ...(ngDevMode ? [{ debugName: "showHeader" }] : []));
|
|
731
|
+
timelineMode = model('quarterly', ...(ngDevMode ? [{ debugName: "timelineMode" }] : []));
|
|
732
|
+
timelineModeOptions = input([
|
|
733
|
+
...DEFAULT_TIMELINE_MODES,
|
|
734
|
+
], ...(ngDevMode ? [{ debugName: "timelineModeOptions" }] : []));
|
|
735
|
+
ganttData = input([], ...(ngDevMode ? [{ debugName: "ganttData" }] : []));
|
|
736
|
+
ganttMapping = input(null, ...(ngDevMode ? [{ debugName: "ganttMapping" }] : []));
|
|
737
|
+
// Null means "legacy defaults". Empty array means "progress-only" mode.
|
|
738
|
+
columns = input(null, ...(ngDevMode ? [{ debugName: "columns" }] : []));
|
|
739
|
+
ganttSegmentWidthPx = input(96, ...(ngDevMode ? [{ debugName: "ganttSegmentWidthPx" }] : []));
|
|
740
|
+
columnsPaneMinWidthPx = input(0, ...(ngDevMode ? [{ debugName: "columnsPaneMinWidthPx" }] : []));
|
|
741
|
+
columnsPaneMaxWidthPx = input(null, ...(ngDevMode ? [{ debugName: "columnsPaneMaxWidthPx" }] : []));
|
|
742
|
+
// Backward-compatible defaults when `columns` is null.
|
|
743
|
+
ganttTitleColumnLabel = input('Portfolio Name', ...(ngDevMode ? [{ debugName: "ganttTitleColumnLabel" }] : []));
|
|
744
|
+
ganttStatusColumnLabel = input('Status', ...(ngDevMode ? [{ debugName: "ganttStatusColumnLabel" }] : []));
|
|
745
|
+
ganttInitiativeColumnWidthPx = input(288, ...(ngDevMode ? [{ debugName: "ganttInitiativeColumnWidthPx" }] : []));
|
|
746
|
+
ganttStatusColumnWidthPx = input(160, ...(ngDevMode ? [{ debugName: "ganttStatusColumnWidthPx" }] : []));
|
|
747
|
+
showGanttStatusColumn = input(false, ...(ngDevMode ? [{ debugName: "showGanttStatusColumn" }] : []));
|
|
748
|
+
showGanttDetailsPopup = input(true, ...(ngDevMode ? [{ debugName: "showGanttDetailsPopup" }] : []));
|
|
749
|
+
// Public events exposed to consumers.
|
|
750
|
+
timelineModeChangeEvent = output();
|
|
751
|
+
ganttItemClick = output();
|
|
752
|
+
// Optional full-template override for the Gantt body.
|
|
753
|
+
ganttTemplate = contentChild(TimelineGanttTemplateDirective, ...(ngDevMode ? [{ debugName: "ganttTemplate" }] : []));
|
|
754
|
+
// Optional cell template override for a specific column key.
|
|
755
|
+
columnTemplates = contentChildren((TimelineColumnTemplateDirective), ...(ngDevMode ? [{ debugName: "columnTemplates" }] : []));
|
|
756
|
+
// Optional popup content override (clicked progress item details).
|
|
757
|
+
popoverTemplate = contentChild((TimelinePopoverTemplateDirective), ...(ngDevMode ? [{ debugName: "popoverTemplate" }] : []));
|
|
758
|
+
// Optional progress bar renderer override.
|
|
759
|
+
progressTemplate = contentChild((TimelineProgressTemplateDirective), ...(ngDevMode ? [{ debugName: "progressTemplate" }] : []));
|
|
760
|
+
collapsedGanttIds = signal(new Set(), ...(ngDevMode ? [{ debugName: "collapsedGanttIds" }] : []));
|
|
761
|
+
selectedGanttItem = signal(null, ...(ngDevMode ? [{ debugName: "selectedGanttItem" }] : []));
|
|
762
|
+
// Maps `mtTimelineColumn` templates by key for fast lookup in row rendering.
|
|
763
|
+
columnTemplatesByKey = computed(() => {
|
|
764
|
+
const map = new Map();
|
|
765
|
+
for (const template of this.columnTemplates()) {
|
|
766
|
+
if (template.key) {
|
|
767
|
+
map.set(template.key, template);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return map;
|
|
771
|
+
}, ...(ngDevMode ? [{ debugName: "columnTemplatesByKey" }] : []));
|
|
772
|
+
mappedGanttNodes = computed(() => {
|
|
773
|
+
const mapping = this.ganttMapping();
|
|
774
|
+
const data = this.ganttData();
|
|
775
|
+
if (!mapping || !data.length) {
|
|
776
|
+
return [];
|
|
777
|
+
}
|
|
778
|
+
const state = { nextId: 1 };
|
|
779
|
+
return data
|
|
780
|
+
.map((item) => this.mapNode(item, mapping, state))
|
|
781
|
+
.filter((item) => item !== null);
|
|
782
|
+
}, ...(ngDevMode ? [{ debugName: "mappedGanttNodes" }] : []));
|
|
783
|
+
// Builds time columns from the mapped data date range and selected mode.
|
|
784
|
+
orderedGanttSegments = computed(() => {
|
|
785
|
+
const nodes = this.mappedGanttNodes();
|
|
786
|
+
if (!nodes.length) {
|
|
787
|
+
return [];
|
|
788
|
+
}
|
|
789
|
+
const range = this.resolveDateRange(nodes);
|
|
790
|
+
if (!range) {
|
|
791
|
+
return [];
|
|
792
|
+
}
|
|
793
|
+
return this.buildSegments(range.startAt, range.endAt, this.timelineMode());
|
|
794
|
+
}, ...(ngDevMode ? [{ debugName: "orderedGanttSegments" }] : []));
|
|
795
|
+
// Timeline mode is controlled by parent model signal to keep it externally bindable.
|
|
796
|
+
onTimelineModeChange(mode) {
|
|
797
|
+
if (!mode || this.timelineMode() === mode) {
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
this.timelineMode.set(mode);
|
|
801
|
+
this.timelineModeChangeEvent.emit(mode);
|
|
802
|
+
}
|
|
803
|
+
// Handles row progress clicks: emits public event and opens details popover.
|
|
804
|
+
onGanttProgressClick(item, event) {
|
|
805
|
+
event.stopPropagation();
|
|
806
|
+
this.ganttItemClick.emit(item.source ?? item);
|
|
807
|
+
if (!this.showGanttDetailsPopup()) {
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
this.selectedGanttItem.set(item);
|
|
811
|
+
const popover = this.detailsPopover;
|
|
812
|
+
if (!popover) {
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
const clickTarget = this.positionPopoverClickAnchor(event);
|
|
816
|
+
// If popover is already open, reopen it at the new anchor event.
|
|
817
|
+
if (popover.overlayVisible) {
|
|
818
|
+
popover.hide();
|
|
819
|
+
setTimeout(() => popover.show(event, clickTarget), 0);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
popover.show(event, clickTarget);
|
|
823
|
+
}
|
|
824
|
+
onDetailsPopoverHide() {
|
|
825
|
+
this.selectedGanttItem.set(null);
|
|
826
|
+
}
|
|
827
|
+
// Closes popover and clears selected row context.
|
|
828
|
+
hideDetailsPopover() {
|
|
829
|
+
this.detailsPopover?.hide();
|
|
830
|
+
this.selectedGanttItem.set(null);
|
|
831
|
+
}
|
|
832
|
+
// Maintains collapse state for hierarchical rows.
|
|
833
|
+
toggleGanttCollapse(item) {
|
|
834
|
+
if (!item.hasChildren) {
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
this.collapsedGanttIds.update((current) => {
|
|
838
|
+
const next = new Set(current);
|
|
839
|
+
if (next.has(item.id)) {
|
|
840
|
+
next.delete(item.id);
|
|
841
|
+
}
|
|
842
|
+
else {
|
|
843
|
+
next.add(item.id);
|
|
844
|
+
}
|
|
845
|
+
return next;
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
getPopoverTemplateContext(item) {
|
|
849
|
+
const source = item.source ?? item;
|
|
850
|
+
return {
|
|
851
|
+
$implicit: source,
|
|
852
|
+
item: source,
|
|
853
|
+
resolved: item,
|
|
854
|
+
close: () => this.hideDetailsPopover(),
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
// Recursively maps consumer item shape using accessors from `ganttMapping`.
|
|
858
|
+
mapNode(source, mapping, state) {
|
|
859
|
+
const title = this.resolveString(source, mapping.title) ?? 'Untitled';
|
|
860
|
+
const idRaw = this.resolveAccessor(source, mapping.id);
|
|
861
|
+
const id = idRaw !== null && idRaw !== undefined && String(idRaw).length
|
|
862
|
+
? idRaw
|
|
863
|
+
: `item-${state.nextId++}`;
|
|
864
|
+
const progress = this.normalizeProgress(this.resolveAccessor(source, mapping.progress));
|
|
865
|
+
const childrenValue = this.resolveAccessor(source, mapping.children) ??
|
|
866
|
+
this.path(source, 'children');
|
|
867
|
+
const children = (Array.isArray(childrenValue) ? childrenValue : [])
|
|
868
|
+
.map((child) => this.mapNode(child, mapping, state))
|
|
869
|
+
.filter((item) => item !== null);
|
|
870
|
+
return {
|
|
871
|
+
source,
|
|
872
|
+
id,
|
|
873
|
+
title,
|
|
874
|
+
startAt: this.toTime(this.resolveAccessor(source, mapping.dateFrom)),
|
|
875
|
+
endAt: this.toTime(this.resolveAccessor(source, mapping.dateTo)),
|
|
876
|
+
progress,
|
|
877
|
+
color: this.resolveColor(source, mapping.color),
|
|
878
|
+
statusKey: this.resolveString(source, mapping.statusKey),
|
|
879
|
+
statusName: this.resolveString(source, mapping.statusName),
|
|
880
|
+
owner: this.resolveString(source, mapping.owner),
|
|
881
|
+
phase: this.resolveString(source, mapping.phase),
|
|
882
|
+
levelDescription: this.resolveString(source, mapping.levelDescription),
|
|
883
|
+
levelColor: this.resolveString(source, mapping.levelColor),
|
|
884
|
+
children,
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
// Finds min/max timestamps in the mapped tree to define timeline bounds.
|
|
888
|
+
resolveDateRange(nodes) {
|
|
889
|
+
let min = Number.POSITIVE_INFINITY;
|
|
890
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
891
|
+
const visit = (items) => {
|
|
892
|
+
for (const item of items) {
|
|
893
|
+
if (item.startAt !== null) {
|
|
894
|
+
min = Math.min(min, item.startAt);
|
|
895
|
+
max = Math.max(max, item.startAt);
|
|
896
|
+
}
|
|
897
|
+
if (item.endAt !== null) {
|
|
898
|
+
min = Math.min(min, item.endAt);
|
|
899
|
+
max = Math.max(max, item.endAt);
|
|
900
|
+
}
|
|
901
|
+
if (item.children.length) {
|
|
902
|
+
visit(item.children);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
};
|
|
906
|
+
visit(nodes);
|
|
907
|
+
if (!Number.isFinite(min) || !Number.isFinite(max)) {
|
|
908
|
+
return null;
|
|
909
|
+
}
|
|
910
|
+
return { startAt: min, endAt: max };
|
|
911
|
+
}
|
|
912
|
+
// Dispatcher for segment generation by selected scale mode.
|
|
913
|
+
buildSegments(startAt, endAt, mode) {
|
|
914
|
+
switch (mode) {
|
|
915
|
+
case 'monthly':
|
|
916
|
+
return this.genSegments(startAt, endAt, 1, (month) => `M ${month + 1}`, 'M');
|
|
917
|
+
case 'quarterly':
|
|
918
|
+
return this.genSegments(startAt, endAt, 3, (month) => `Q ${Math.floor(month / 3) + 1}`, 'Q');
|
|
919
|
+
case 'biannually':
|
|
920
|
+
return this.genSegments(startAt, endAt, 6, (month) => `H ${month < 6 ? 1 : 2}`, 'H');
|
|
921
|
+
case 'annually':
|
|
922
|
+
return this.genSegments(startAt, endAt, 12, () => 'Y', 'Y');
|
|
923
|
+
default:
|
|
924
|
+
return this.genSegments(startAt, endAt, 3, (month) => `Q ${Math.floor(month / 3) + 1}`, 'Q');
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
// Generates contiguous segments between range boundaries.
|
|
928
|
+
genSegments(startAt, endAt, stepMonths, title, prefix) {
|
|
929
|
+
const segments = [];
|
|
930
|
+
const start = new Date(startAt);
|
|
931
|
+
const end = new Date(endAt);
|
|
932
|
+
let year = start.getUTCFullYear();
|
|
933
|
+
let month = start.getUTCMonth();
|
|
934
|
+
let seq = 0;
|
|
935
|
+
if (stepMonths > 1) {
|
|
936
|
+
month = Math.floor(month / stepMonths) * stepMonths;
|
|
937
|
+
}
|
|
938
|
+
while (year < end.getUTCFullYear() ||
|
|
939
|
+
(year === end.getUTCFullYear() && month <= end.getUTCMonth())) {
|
|
940
|
+
segments.push({
|
|
941
|
+
id: `${prefix}${month + 1}_${year}`,
|
|
942
|
+
title: title(month),
|
|
943
|
+
seq,
|
|
944
|
+
year,
|
|
945
|
+
startAt: Date.UTC(year, month, 1),
|
|
946
|
+
endAt: Date.UTC(year, month + stepMonths, 0, 23, 59, 59, 999),
|
|
947
|
+
});
|
|
948
|
+
seq += 1;
|
|
949
|
+
month += stepMonths;
|
|
950
|
+
if (month > 11) {
|
|
951
|
+
year += Math.floor(month / 12);
|
|
952
|
+
month %= 12;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
return segments;
|
|
956
|
+
}
|
|
957
|
+
// Supports both dot-path accessors and function accessors.
|
|
958
|
+
resolveAccessor(item, accessor) {
|
|
959
|
+
if (!accessor) {
|
|
960
|
+
return undefined;
|
|
961
|
+
}
|
|
962
|
+
if (typeof accessor === 'function') {
|
|
963
|
+
return accessor(item);
|
|
964
|
+
}
|
|
965
|
+
return this.path(item, accessor);
|
|
966
|
+
}
|
|
967
|
+
resolveString(item, accessor) {
|
|
968
|
+
const value = this.resolveAccessor(item, accessor);
|
|
969
|
+
if (value === null || value === undefined) {
|
|
970
|
+
return null;
|
|
971
|
+
}
|
|
972
|
+
return String(value);
|
|
973
|
+
}
|
|
974
|
+
// Robust color resolution for string/object return shapes.
|
|
975
|
+
resolveColor(item, accessor) {
|
|
976
|
+
const value = this.resolveAccessor(item, accessor);
|
|
977
|
+
if (value === null || value === undefined) {
|
|
978
|
+
return null;
|
|
979
|
+
}
|
|
980
|
+
if (typeof value === 'string') {
|
|
981
|
+
return value;
|
|
982
|
+
}
|
|
983
|
+
if (typeof value === 'object') {
|
|
984
|
+
const color = value['color'];
|
|
985
|
+
if (typeof color === 'string') {
|
|
986
|
+
return color;
|
|
987
|
+
}
|
|
988
|
+
const detailsColor = this.path(value, 'details.color');
|
|
989
|
+
if (typeof detailsColor === 'string') {
|
|
990
|
+
return detailsColor;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
return String(value);
|
|
994
|
+
}
|
|
995
|
+
// Safe deep property accessor (supports bracket and dot notation).
|
|
996
|
+
path(item, value) {
|
|
997
|
+
if (!value) {
|
|
998
|
+
return undefined;
|
|
999
|
+
}
|
|
1000
|
+
return value
|
|
1001
|
+
.replace(/\[(\d+)\]/g, '.$1')
|
|
1002
|
+
.split('.')
|
|
1003
|
+
.filter(Boolean)
|
|
1004
|
+
.reduce((current, key) => {
|
|
1005
|
+
if (current === null ||
|
|
1006
|
+
current === undefined ||
|
|
1007
|
+
typeof current !== 'object') {
|
|
1008
|
+
return undefined;
|
|
1009
|
+
}
|
|
1010
|
+
return current[key];
|
|
1011
|
+
}, item);
|
|
1012
|
+
}
|
|
1013
|
+
// Normalizes all supported progress input shapes into a single object model.
|
|
1014
|
+
normalizeProgress(value) {
|
|
1015
|
+
if (value === null || value === undefined) {
|
|
1016
|
+
return { value: 0, label: '0%' };
|
|
1017
|
+
}
|
|
1018
|
+
if (typeof value === 'number') {
|
|
1019
|
+
const safeValue = this.clampProgress(value);
|
|
1020
|
+
return { value: safeValue, label: `${Math.round(safeValue)}%` };
|
|
1021
|
+
}
|
|
1022
|
+
if (typeof value === 'string') {
|
|
1023
|
+
const safeValue = this.clampProgress(Number(value.replace(/[^\d.-]/g, '')));
|
|
1024
|
+
return {
|
|
1025
|
+
value: safeValue,
|
|
1026
|
+
label: value.trim().length ? value : `${Math.round(safeValue)}%`,
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
const safeValue = this.clampProgress(value.value);
|
|
1030
|
+
return {
|
|
1031
|
+
value: safeValue,
|
|
1032
|
+
label: value.label ?? `${Math.round(safeValue)}%`,
|
|
1033
|
+
color: value.color,
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
// Accepts date values as Date, timestamp, ISO string, or `{ actualValue }`.
|
|
1037
|
+
toTime(value) {
|
|
1038
|
+
if (value === null || value === undefined) {
|
|
1039
|
+
return null;
|
|
1040
|
+
}
|
|
1041
|
+
if (value instanceof Date) {
|
|
1042
|
+
return Number.isFinite(value.getTime()) ? value.getTime() : null;
|
|
1043
|
+
}
|
|
1044
|
+
if (typeof value === 'number') {
|
|
1045
|
+
return Number.isFinite(value) ? value : null;
|
|
1046
|
+
}
|
|
1047
|
+
if (typeof value === 'string') {
|
|
1048
|
+
const time = new Date(value).getTime();
|
|
1049
|
+
return Number.isFinite(time) ? time : null;
|
|
1050
|
+
}
|
|
1051
|
+
if (typeof value === 'object') {
|
|
1052
|
+
const actualValue = value['actualValue'];
|
|
1053
|
+
if (actualValue !== undefined) {
|
|
1054
|
+
return this.toTime(actualValue);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
return null;
|
|
1058
|
+
}
|
|
1059
|
+
clampProgress(value) {
|
|
1060
|
+
if (!Number.isFinite(value)) {
|
|
1061
|
+
return 0;
|
|
1062
|
+
}
|
|
1063
|
+
return Math.max(0, Math.min(100, value));
|
|
1064
|
+
}
|
|
1065
|
+
positionPopoverClickAnchor(event) {
|
|
1066
|
+
const anchor = this.detailsPopoverClickAnchor?.nativeElement;
|
|
1067
|
+
if (!anchor) {
|
|
1068
|
+
return undefined;
|
|
1069
|
+
}
|
|
1070
|
+
anchor.style.left = `${event.clientX}px`;
|
|
1071
|
+
anchor.style.top = `${event.clientY}px`;
|
|
1072
|
+
return anchor;
|
|
1073
|
+
}
|
|
1074
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: Timeline, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1075
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.3", type: Timeline, isStandalone: true, selector: "mt-timeline", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, isLoading: { classPropertyName: "isLoading", publicName: "isLoading", isSignal: true, isRequired: false, transformFunction: null }, showHeader: { classPropertyName: "showHeader", publicName: "showHeader", isSignal: true, isRequired: false, transformFunction: null }, timelineMode: { classPropertyName: "timelineMode", publicName: "timelineMode", isSignal: true, isRequired: false, transformFunction: null }, timelineModeOptions: { classPropertyName: "timelineModeOptions", publicName: "timelineModeOptions", isSignal: true, isRequired: false, transformFunction: null }, ganttData: { classPropertyName: "ganttData", publicName: "ganttData", isSignal: true, isRequired: false, transformFunction: null }, ganttMapping: { classPropertyName: "ganttMapping", publicName: "ganttMapping", isSignal: true, isRequired: false, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: false, transformFunction: null }, ganttSegmentWidthPx: { classPropertyName: "ganttSegmentWidthPx", publicName: "ganttSegmentWidthPx", isSignal: true, isRequired: false, transformFunction: null }, columnsPaneMinWidthPx: { classPropertyName: "columnsPaneMinWidthPx", publicName: "columnsPaneMinWidthPx", isSignal: true, isRequired: false, transformFunction: null }, columnsPaneMaxWidthPx: { classPropertyName: "columnsPaneMaxWidthPx", publicName: "columnsPaneMaxWidthPx", isSignal: true, isRequired: false, transformFunction: null }, ganttTitleColumnLabel: { classPropertyName: "ganttTitleColumnLabel", publicName: "ganttTitleColumnLabel", isSignal: true, isRequired: false, transformFunction: null }, ganttStatusColumnLabel: { classPropertyName: "ganttStatusColumnLabel", publicName: "ganttStatusColumnLabel", isSignal: true, isRequired: false, transformFunction: null }, ganttInitiativeColumnWidthPx: { classPropertyName: "ganttInitiativeColumnWidthPx", publicName: "ganttInitiativeColumnWidthPx", isSignal: true, isRequired: false, transformFunction: null }, ganttStatusColumnWidthPx: { classPropertyName: "ganttStatusColumnWidthPx", publicName: "ganttStatusColumnWidthPx", isSignal: true, isRequired: false, transformFunction: null }, showGanttStatusColumn: { classPropertyName: "showGanttStatusColumn", publicName: "showGanttStatusColumn", isSignal: true, isRequired: false, transformFunction: null }, showGanttDetailsPopup: { classPropertyName: "showGanttDetailsPopup", publicName: "showGanttDetailsPopup", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { timelineMode: "timelineModeChange", timelineModeChangeEvent: "timelineModeChangeEvent", ganttItemClick: "ganttItemClick" }, host: { classAttribute: "block" }, queries: [{ propertyName: "ganttTemplate", first: true, predicate: TimelineGanttTemplateDirective, descendants: true, isSignal: true }, { propertyName: "columnTemplates", predicate: (TimelineColumnTemplateDirective), isSignal: true }, { propertyName: "popoverTemplate", first: true, predicate: (TimelinePopoverTemplateDirective), descendants: true, isSignal: true }, { propertyName: "progressTemplate", first: true, predicate: (TimelineProgressTemplateDirective), descendants: true, isSignal: true }], viewQueries: [{ propertyName: "detailsPopover", first: true, predicate: ["detailsPopover"], descendants: true }, { propertyName: "detailsPopoverClickAnchor", first: true, predicate: ["detailsPopoverClickAnchor"], descendants: true }], ngImport: i0, template: "<mt-card class=\"mt-3 block overflow-hidden\">\r\n <!-- Optional top header (title + timeline mode selector). -->\r\n @if (showHeader()) {\r\n <mt-timeline-header\r\n [title]=\"title()\"\r\n [timelineMode]=\"timelineMode()\"\r\n [timelineModeOptions]=\"timelineModeOptions()\"\r\n (timelineModeChange)=\"onTimelineModeChange($event)\"\r\n />\r\n }\r\n\r\n <!-- Loading state. -->\r\n @if (isLoading()) {\r\n <div class=\"flex min-h-56 items-center justify-center p-8\">\r\n <div\r\n class=\"h-8 w-8 animate-spin rounded-full border-2 border-surface-200\"\r\n style=\"border-top-color: var(--p-primary-color)\"\r\n ></div>\r\n </div>\r\n } @else if (ganttTemplate(); as template) {\r\n <!-- Full custom gantt-body override. -->\r\n <div class=\"min-h-56 p-4\">\r\n <ng-container [ngTemplateOutlet]=\"template.templateRef\"></ng-container>\r\n </div>\r\n } @else if (\r\n mappedGanttNodes().length > 0 && orderedGanttSegments().length > 0\r\n ) {\r\n <!-- Built-in gantt renderer. -->\r\n <mt-timeline-gantt\r\n [timelineMode]=\"timelineMode()\"\r\n [columns]=\"columns()\"\r\n [ganttTitleColumnLabel]=\"ganttTitleColumnLabel()\"\r\n [ganttStatusColumnLabel]=\"ganttStatusColumnLabel()\"\r\n [ganttInitiativeColumnWidthPx]=\"ganttInitiativeColumnWidthPx()\"\r\n [ganttStatusColumnWidthPx]=\"ganttStatusColumnWidthPx()\"\r\n [showGanttStatusColumn]=\"showGanttStatusColumn()\"\r\n [ganttSegmentWidthPx]=\"ganttSegmentWidthPx()\"\r\n [columnsPaneMinWidthPx]=\"columnsPaneMinWidthPx()\"\r\n [columnsPaneMaxWidthPx]=\"columnsPaneMaxWidthPx()\"\r\n [mappedGanttNodes]=\"mappedGanttNodes()\"\r\n [orderedGanttSegments]=\"orderedGanttSegments()\"\r\n [collapsedGanttIds]=\"collapsedGanttIds()\"\r\n [columnTemplatesByKey]=\"columnTemplatesByKey()\"\r\n [progressTemplateDirective]=\"progressTemplate()\"\r\n (toggleCollapse)=\"toggleGanttCollapse($event)\"\r\n (progressClick)=\"onGanttProgressClick($event.item, $event.event)\"\r\n />\r\n } @else {\r\n <!-- Empty state. -->\r\n <div\r\n class=\"flex min-h-56 items-center justify-center p-8 text-sm text-surface-500\"\r\n >\r\n {{ emptyMessage() }}\r\n </div>\r\n }\r\n\r\n <!-- Details popover host (custom template or built-in fallback). -->\r\n <p-popover #detailsPopover appendTo=\"body\" (onHide)=\"onDetailsPopoverHide()\">\r\n @if (showGanttDetailsPopup() && selectedGanttItem(); as item) {\r\n @if (popoverTemplate(); as template) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"template.templateRef\"\r\n [ngTemplateOutletContext]=\"getPopoverTemplateContext(item)\"\r\n ></ng-container>\r\n } @else {\r\n <mt-timeline-default-popover\r\n [item]=\"item\"\r\n (requestClose)=\"hideDetailsPopover()\"\r\n />\r\n }\r\n }\r\n </p-popover>\r\n\r\n <!-- Point anchor used to place the popover exactly where user clicked. -->\r\n <span\r\n #detailsPopoverClickAnchor\r\n class=\"pointer-events-none fixed h-0 w-0\"\r\n [style.left.px]=\"-9999\"\r\n [style.top.px]=\"-9999\"\r\n aria-hidden=\"true\"\r\n ></span>\r\n</mt-card>\r\n", styles: [":host{display:block}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: Card, selector: "mt-card", inputs: ["class", "title", "paddingless"] }, { kind: "component", type: TimelineHeader, selector: "mt-timeline-header", inputs: ["title", "timelineMode", "timelineModeOptions"], outputs: ["timelineModeChange"] }, { kind: "component", type: TimelineGantt, selector: "mt-timeline-gantt", inputs: ["timelineMode", "columns", "ganttTitleColumnLabel", "ganttStatusColumnLabel", "ganttInitiativeColumnWidthPx", "ganttStatusColumnWidthPx", "showGanttStatusColumn", "ganttSegmentWidthPx", "columnsPaneMinWidthPx", "columnsPaneMaxWidthPx", "mappedGanttNodes", "orderedGanttSegments", "collapsedGanttIds", "columnTemplatesByKey", "progressTemplateDirective"], outputs: ["toggleCollapse", "progressClick"] }, { kind: "component", type: TimelineDefaultPopover, selector: "mt-timeline-default-popover", inputs: ["item"], outputs: ["requestClose"] }, { kind: "component", type: Popover, selector: "p-popover", inputs: ["ariaLabel", "ariaLabelledBy", "dismissable", "style", "styleClass", "appendTo", "autoZIndex", "ariaCloseLabel", "baseZIndex", "focusOnShow", "showTransitionOptions", "hideTransitionOptions", "motionOptions"], outputs: ["onShow", "onHide"] }] });
|
|
1076
|
+
}
|
|
1077
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: Timeline, decorators: [{
|
|
1078
|
+
type: Component,
|
|
1079
|
+
args: [{ selector: 'mt-timeline', standalone: true, imports: [
|
|
1080
|
+
CommonModule,
|
|
1081
|
+
Card,
|
|
1082
|
+
TimelineHeader,
|
|
1083
|
+
TimelineGantt,
|
|
1084
|
+
TimelineDefaultPopover,
|
|
1085
|
+
Popover,
|
|
1086
|
+
NgTemplateOutlet,
|
|
1087
|
+
], host: { class: 'block' }, template: "<mt-card class=\"mt-3 block overflow-hidden\">\r\n <!-- Optional top header (title + timeline mode selector). -->\r\n @if (showHeader()) {\r\n <mt-timeline-header\r\n [title]=\"title()\"\r\n [timelineMode]=\"timelineMode()\"\r\n [timelineModeOptions]=\"timelineModeOptions()\"\r\n (timelineModeChange)=\"onTimelineModeChange($event)\"\r\n />\r\n }\r\n\r\n <!-- Loading state. -->\r\n @if (isLoading()) {\r\n <div class=\"flex min-h-56 items-center justify-center p-8\">\r\n <div\r\n class=\"h-8 w-8 animate-spin rounded-full border-2 border-surface-200\"\r\n style=\"border-top-color: var(--p-primary-color)\"\r\n ></div>\r\n </div>\r\n } @else if (ganttTemplate(); as template) {\r\n <!-- Full custom gantt-body override. -->\r\n <div class=\"min-h-56 p-4\">\r\n <ng-container [ngTemplateOutlet]=\"template.templateRef\"></ng-container>\r\n </div>\r\n } @else if (\r\n mappedGanttNodes().length > 0 && orderedGanttSegments().length > 0\r\n ) {\r\n <!-- Built-in gantt renderer. -->\r\n <mt-timeline-gantt\r\n [timelineMode]=\"timelineMode()\"\r\n [columns]=\"columns()\"\r\n [ganttTitleColumnLabel]=\"ganttTitleColumnLabel()\"\r\n [ganttStatusColumnLabel]=\"ganttStatusColumnLabel()\"\r\n [ganttInitiativeColumnWidthPx]=\"ganttInitiativeColumnWidthPx()\"\r\n [ganttStatusColumnWidthPx]=\"ganttStatusColumnWidthPx()\"\r\n [showGanttStatusColumn]=\"showGanttStatusColumn()\"\r\n [ganttSegmentWidthPx]=\"ganttSegmentWidthPx()\"\r\n [columnsPaneMinWidthPx]=\"columnsPaneMinWidthPx()\"\r\n [columnsPaneMaxWidthPx]=\"columnsPaneMaxWidthPx()\"\r\n [mappedGanttNodes]=\"mappedGanttNodes()\"\r\n [orderedGanttSegments]=\"orderedGanttSegments()\"\r\n [collapsedGanttIds]=\"collapsedGanttIds()\"\r\n [columnTemplatesByKey]=\"columnTemplatesByKey()\"\r\n [progressTemplateDirective]=\"progressTemplate()\"\r\n (toggleCollapse)=\"toggleGanttCollapse($event)\"\r\n (progressClick)=\"onGanttProgressClick($event.item, $event.event)\"\r\n />\r\n } @else {\r\n <!-- Empty state. -->\r\n <div\r\n class=\"flex min-h-56 items-center justify-center p-8 text-sm text-surface-500\"\r\n >\r\n {{ emptyMessage() }}\r\n </div>\r\n }\r\n\r\n <!-- Details popover host (custom template or built-in fallback). -->\r\n <p-popover #detailsPopover appendTo=\"body\" (onHide)=\"onDetailsPopoverHide()\">\r\n @if (showGanttDetailsPopup() && selectedGanttItem(); as item) {\r\n @if (popoverTemplate(); as template) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"template.templateRef\"\r\n [ngTemplateOutletContext]=\"getPopoverTemplateContext(item)\"\r\n ></ng-container>\r\n } @else {\r\n <mt-timeline-default-popover\r\n [item]=\"item\"\r\n (requestClose)=\"hideDetailsPopover()\"\r\n />\r\n }\r\n }\r\n </p-popover>\r\n\r\n <!-- Point anchor used to place the popover exactly where user clicked. -->\r\n <span\r\n #detailsPopoverClickAnchor\r\n class=\"pointer-events-none fixed h-0 w-0\"\r\n [style.left.px]=\"-9999\"\r\n [style.top.px]=\"-9999\"\r\n aria-hidden=\"true\"\r\n ></span>\r\n</mt-card>\r\n", styles: [":host{display:block}\n"] }]
|
|
1088
|
+
}], propDecorators: { detailsPopover: [{
|
|
1089
|
+
type: ViewChild,
|
|
1090
|
+
args: ['detailsPopover']
|
|
1091
|
+
}], detailsPopoverClickAnchor: [{
|
|
1092
|
+
type: ViewChild,
|
|
1093
|
+
args: ['detailsPopoverClickAnchor']
|
|
1094
|
+
}], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], isLoading: [{ type: i0.Input, args: [{ isSignal: true, alias: "isLoading", required: false }] }], showHeader: [{ type: i0.Input, args: [{ isSignal: true, alias: "showHeader", required: false }] }], timelineMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "timelineMode", required: false }] }, { type: i0.Output, args: ["timelineModeChange"] }], timelineModeOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "timelineModeOptions", required: false }] }], ganttData: [{ type: i0.Input, args: [{ isSignal: true, alias: "ganttData", required: false }] }], ganttMapping: [{ type: i0.Input, args: [{ isSignal: true, alias: "ganttMapping", required: false }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: false }] }], ganttSegmentWidthPx: [{ type: i0.Input, args: [{ isSignal: true, alias: "ganttSegmentWidthPx", required: false }] }], columnsPaneMinWidthPx: [{ type: i0.Input, args: [{ isSignal: true, alias: "columnsPaneMinWidthPx", required: false }] }], columnsPaneMaxWidthPx: [{ type: i0.Input, args: [{ isSignal: true, alias: "columnsPaneMaxWidthPx", required: false }] }], ganttTitleColumnLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ganttTitleColumnLabel", required: false }] }], ganttStatusColumnLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ganttStatusColumnLabel", required: false }] }], ganttInitiativeColumnWidthPx: [{ type: i0.Input, args: [{ isSignal: true, alias: "ganttInitiativeColumnWidthPx", required: false }] }], ganttStatusColumnWidthPx: [{ type: i0.Input, args: [{ isSignal: true, alias: "ganttStatusColumnWidthPx", required: false }] }], showGanttStatusColumn: [{ type: i0.Input, args: [{ isSignal: true, alias: "showGanttStatusColumn", required: false }] }], showGanttDetailsPopup: [{ type: i0.Input, args: [{ isSignal: true, alias: "showGanttDetailsPopup", required: false }] }], timelineModeChangeEvent: [{ type: i0.Output, args: ["timelineModeChangeEvent"] }], ganttItemClick: [{ type: i0.Output, args: ["ganttItemClick"] }], ganttTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TimelineGanttTemplateDirective), { isSignal: true }] }], columnTemplates: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => TimelineColumnTemplateDirective), { isSignal: true }] }], popoverTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TimelinePopoverTemplateDirective), { isSignal: true }] }], progressTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TimelineProgressTemplateDirective), { isSignal: true }] }] } });
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* Generated bundle index. Do not edit.
|
|
1098
|
+
*/
|
|
1099
|
+
|
|
1100
|
+
export { Timeline, TimelineColumnTemplateDirective, TimelineGanttTemplateDirective, TimelinePopoverTemplateDirective, TimelineProgressTemplateDirective };
|
|
1101
|
+
//# sourceMappingURL=masterteam-timeline.mjs.map
|